diff --git a/.appveyor.yml b/.appveyor.yml index 6ce5200b6..c3c31ffbc 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -1,100 +1,100 @@ -skip_commits: - files: - - ".github/**/*" - - ".gitmodules" - - "docs/**/*" - - "wheels/**/*" - -version: '{build}' -clone_folder: c:\pillow -init: -- ECHO %PYTHON% -#- ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) -# Uncomment previous line to get RDP access during the build. - -environment: - COVERAGE_CORE: sysmon - EXECUTABLE: python.exe - TEST_OPTIONS: - DEPLOY: YES - matrix: - - PYTHON: C:/Python312 - ARCHITECTURE: x86 - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022 - - PYTHON: C:/Python38-x64 - ARCHITECTURE: AMD64 - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 - - -install: -- '%PYTHON%\%EXECUTABLE% --version' -- '%PYTHON%\%EXECUTABLE% -m pip install --upgrade pip' -- curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip -- 7z x pillow-test-images.zip -oc:\ -- xcopy /S /Y c:\test-images-main\* c:\pillow\tests\images -- curl -fsSL -o nasm-win64.zip https://raw.githubusercontent.com/python-pillow/pillow-depends/main/nasm-2.16.03-win64.zip -- 7z x nasm-win64.zip -oc:\ -- choco install ghostscript --version=10.3.1 -- path c:\nasm-2.16.03;C:\Program Files\gs\gs10.03.1\bin;%PATH% -- cd c:\pillow\winbuild\ -- ps: | - c:\python38\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\ - c:\pillow\winbuild\build\build_dep_all.cmd - $host.SetShouldExit(0) -- path C:\pillow\winbuild\build\bin;%PATH% - -build_script: -- cd c:\pillow -- winbuild\build\build_env.cmd -- '%PYTHON%\%EXECUTABLE% -m pip install -v -C raqm=vendor -C fribidi=vendor .' -- '%PYTHON%\%EXECUTABLE% selftest.py --installed' - -test_script: -- cd c:\pillow -- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout defusedxml numpy olefile pyroma' -- c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE% -- '%PYTHON%\%EXECUTABLE% -c "from PIL import Image"' -- '%PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests' -#- '%PYTHON%\%EXECUTABLE% test-installed.py -v -s %TEST_OPTIONS%' TODO TEST_OPTIONS with pytest? - -after_test: -- curl -Os https://uploader.codecov.io/latest/windows/codecov.exe -- .\codecov.exe --file coverage.xml --name %PYTHON% --flags AppVeyor - -matrix: - fast_finish: true - -cache: -- '%LOCALAPPDATA%\pip\Cache' - -artifacts: -- path: pillow\*.egg - name: egg -- path: pillow\*.whl - name: wheel - -before_deploy: - - cd c:\pillow - - '%PYTHON%\%EXECUTABLE% -m pip wheel -v -C raqm=vendor -C fribidi=vendor .' - - ps: Get-ChildItem .\*.whl | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name } - -deploy: - provider: S3 - region: us-west-2 - access_key_id: AKIAIRAXC62ZNTVQJMOQ - secret_access_key: - secure: Hwb6klTqtBeMgxAjRoDltiiqpuH8xbwD4UooDzBSiCWXjuFj1lyl4kHgHwTCCGqi - bucket: pillow-nightly - folder: win/$(APPVEYOR_BUILD_NUMBER)/ - artifact: /.*egg|wheel/ - on: - APPVEYOR_REPO_NAME: python-pillow/Pillow - branch: main - deploy: YES - - -# Uncomment the following lines to get RDP access after the build/test and block for -# up to the timeout limit (~1hr) -# -#on_finish: -#- ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) +skip_commits: + files: + - ".github/**/*" + - ".gitmodules" + - "docs/**/*" + - "wheels/**/*" + +version: '{build}' +clone_folder: c:\pillow +init: +- ECHO %PYTHON% +#- ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) +# Uncomment previous line to get RDP access during the build. + +environment: + COVERAGE_CORE: sysmon + EXECUTABLE: python.exe + TEST_OPTIONS: + DEPLOY: YES + matrix: + - PYTHON: C:/Python312 + ARCHITECTURE: x86 + APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022 + - PYTHON: C:/Python38-x64 + ARCHITECTURE: AMD64 + APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 + + +install: +- '%PYTHON%\%EXECUTABLE% --version' +- '%PYTHON%\%EXECUTABLE% -m pip install --upgrade pip' +- curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip +- 7z x pillow-test-images.zip -oc:\ +- xcopy /S /Y c:\test-images-main\* c:\pillow\tests\images +- curl -fsSL -o nasm-win64.zip https://raw.githubusercontent.com/python-pillow/pillow-depends/main/nasm-2.16.03-win64.zip +- 7z x nasm-win64.zip -oc:\ +- choco install ghostscript --version=10.3.1 +- path c:\nasm-2.16.03;C:\Program Files\gs\gs10.03.1\bin;%PATH% +- cd c:\pillow\winbuild\ +- ps: | + c:\python38\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\ + c:\pillow\winbuild\build\build_dep_all.cmd + $host.SetShouldExit(0) +- path C:\pillow\winbuild\build\bin;%PATH% + +build_script: +- cd c:\pillow +- winbuild\build\build_env.cmd +- '%PYTHON%\%EXECUTABLE% -m pip install -v -C raqm=vendor -C fribidi=vendor .' +- '%PYTHON%\%EXECUTABLE% selftest.py --installed' + +test_script: +- cd c:\pillow +- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout defusedxml numpy olefile pyroma' +- c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE% +- '%PYTHON%\%EXECUTABLE% -c "from PIL import Image"' +- '%PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests' +#- '%PYTHON%\%EXECUTABLE% test-installed.py -v -s %TEST_OPTIONS%' TODO TEST_OPTIONS with pytest? + +after_test: +- curl -Os https://uploader.codecov.io/latest/windows/codecov.exe +- .\codecov.exe --file coverage.xml --name %PYTHON% --flags AppVeyor + +matrix: + fast_finish: true + +cache: +- '%LOCALAPPDATA%\pip\Cache' + +artifacts: +- path: pillow\*.egg + name: egg +- path: pillow\*.whl + name: wheel + +before_deploy: + - cd c:\pillow + - '%PYTHON%\%EXECUTABLE% -m pip wheel -v -C raqm=vendor -C fribidi=vendor .' + - ps: Get-ChildItem .\*.whl | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name } + +deploy: + provider: S3 + region: us-west-2 + access_key_id: AKIAIRAXC62ZNTVQJMOQ + secret_access_key: + secure: Hwb6klTqtBeMgxAjRoDltiiqpuH8xbwD4UooDzBSiCWXjuFj1lyl4kHgHwTCCGqi + bucket: pillow-nightly + folder: win/$(APPVEYOR_BUILD_NUMBER)/ + artifact: /.*egg|wheel/ + on: + APPVEYOR_REPO_NAME: python-pillow/Pillow + branch: main + deploy: YES + + +# Uncomment the following lines to get RDP access after the build/test and block for +# up to the timeout limit (~1hr) +# +#on_finish: +#- ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) diff --git a/.ci/after_success.sh b/.ci/after_success.sh index c71546f00..291f1e637 100755 --- a/.ci/after_success.sh +++ b/.ci/after_success.sh @@ -1,9 +1,9 @@ -#!/bin/bash - -# 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 +#!/bin/bash + +# 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 diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt index 0d0f81fbf..d8c43340c 100644 --- a/.ci/requirements-cibw.txt +++ b/.ci/requirements-cibw.txt @@ -1 +1 @@ -cibuildwheel==2.19.1 +cibuildwheel==2.19.1 diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt index a0dcb92d2..754e87774 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -1 +1 @@ -mypy==1.10.0 +mypy==1.10.0 diff --git a/.clang-format b/.clang-format index 3199e330b..028bd1a39 100644 --- a/.clang-format +++ b/.clang-format @@ -1,21 +1,21 @@ -# A clang-format style that approximates Python's PEP 7 -# Useful for IDE integration -BasedOnStyle: Google -AlwaysBreakAfterReturnType: All -AllowShortIfStatementsOnASingleLine: false -AlignAfterOpenBracket: AlwaysBreak -BinPackArguments: false -BinPackParameters: false -BreakBeforeBraces: Attach -ColumnLimit: 88 -DerivePointerAlignment: false -IndentGotoLabels: false -IndentWidth: 4 -Language: Cpp -PointerAlignment: Right -ReflowComments: true -SortIncludes: false -SpaceBeforeParens: ControlStatements -SpacesInParentheses: false -TabWidth: 4 -UseTab: Never +# A clang-format style that approximates Python's PEP 7 +# Useful for IDE integration +BasedOnStyle: Google +AlwaysBreakAfterReturnType: All +AllowShortIfStatementsOnASingleLine: false +AlignAfterOpenBracket: AlwaysBreak +BinPackArguments: false +BinPackParameters: false +BreakBeforeBraces: Attach +ColumnLimit: 88 +DerivePointerAlignment: false +IndentGotoLabels: false +IndentWidth: 4 +Language: Cpp +PointerAlignment: Right +ReflowComments: true +SortIncludes: false +SpaceBeforeParens: ControlStatements +SpacesInParentheses: false +TabWidth: 4 +UseTab: Never diff --git a/.coveragerc b/.coveragerc index 018cc1cbf..1d471aacb 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,24 +1,27 @@ -# .coveragerc to control coverage.py - -[report] -# Regexes for lines to exclude from consideration -exclude_also = - # Don't complain if non-runnable code isn't run - if 0: - if __name__ == .__main__.: - # Don't complain about debug code - if DEBUG: - # Don't complain about compatibility code for missing optional dependencies - except ImportError - if TYPE_CHECKING: - @abc.abstractmethod - # Empty bodies in protocols or abstract methods - ^\s*def [a-zA-Z0-9_]+\(.*\)(\s*->.*)?:\s*\.\.\.(\s*#.*)?$ - ^\s*\.\.\.(\s*#.*)?$ - -[run] -omit = - Tests/32bit_segfault_check.py - Tests/bench_cffi_access.py - Tests/check_*.py - Tests/createfontdatachunk.py +# .coveragerc to control coverage.py + +[report] +# Regexes for lines to exclude from consideration +exclude_also = + # Don't complain if non-runnable code isn't run + if 0: + if __name__ == .__main__.: + # Don't complain about debug code + if DEBUG: + # Don't complain about compatibility code for missing optional dependencies + except ImportError + if TYPE_CHECKING: + @abc.abstractmethod + # Empty bodies in protocols or abstract methods + ^\s*def [a-zA-Z0-9_]+\(.*\)(\s*->.*)?:\s*\.\.\.(\s*#.*)?$ + ^\s*\.\.\.(\s*#.*)?$ + +[run] +omit = + # Tests/32bit_segfault_check.py + # Tests/bench_cffi_access.py + # Tests/check_*.py + # Tests/createfontdatachunk.py + Tests/* + src/* + diff --git a/.editorconfig b/.editorconfig index c3627ae4f..bed20308d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,22 +1,22 @@ -# Top-most EditorConfig file -root = true - -[*] -# Unix-style newlines with a newline ending every file -end_of_line = lf -insert_final_newline = true -charset = utf-8 - -# Four-space indentation -indent_size = 4 -indent_style = space - -trim_trailing_whitespace = true - -[*.{toml,yml}] -# Two-space indentation -indent_size = 2 - -# Tab indentation (no size specified) -[Makefile] -indent_style = tab +# Top-most EditorConfig file +root = true + +[*] +# Unix-style newlines with a newline ending every file +end_of_line = lf +insert_final_newline = true +charset = utf-8 + +# Four-space indentation +indent_size = 4 +indent_style = space + +trim_trailing_whitespace = true + +[*.{toml,yml}] +# Two-space indentation +indent_size = 2 + +# Tab indentation (no size specified) +[Makefile] +indent_style = tab diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index a2be59c52..23563c555 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -1,6 +1,6 @@ -# Flake8 -8de95676e0fd89f2326b3953488ab66ff29cd2d0 -# Format with Black -53a7e3500437a9fd5826bc04758f7116bd7e52dc -# Format the C code with ClangFormat -46b7e86bab79450ec0a2866c6c0c679afb659d17 +# Flake8 +8de95676e0fd89f2326b3953488ab66ff29cd2d0 +# Format with Black +53a7e3500437a9fd5826bc04758f7116bd7e52dc +# Format the C code with ClangFormat +46b7e86bab79450ec0a2866c6c0c679afb659d17 diff --git a/.gitattributes b/.gitattributes index 2cf25ab1a..cdb8a6e38 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,3 @@ -*.eps binary -*.ppm binary -*.container binary +*.eps binary +*.ppm binary +*.container binary diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index d03fcf0d9..3165cff25 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,39 +1,39 @@ -# Contributing to Pillow - -Bug fixes, feature additions, tests, documentation and more can be contributed via [issues](https://github.com/python-pillow/Pillow/issues) and/or [pull requests](https://github.com/python-pillow/Pillow/pulls). All contributions are welcome. - -## Bug fixes, feature additions, etc. - -Please send a pull request to the `main` branch. Please include [documentation](https://pillow.readthedocs.io) and [tests](../Tests/README.rst) for new features. Tests or documentation without bug fixes or feature additions are welcome too. Feel free to ask questions [via issues](https://github.com/python-pillow/Pillow/issues/new), [discussions](https://github.com/python-pillow/Pillow/discussions/new), [Gitter](https://gitter.im/python-pillow/Pillow) or irc://irc.freenode.net#pil - -- Fork the Pillow repository. -- Create a branch from `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. -- Create a pull request to pull the changes from your branch to the Pillow `main`. - -### Guidelines - -- Separate code commits from reformatting commits. -- Provide tests for any newly added code. -- Follow PEP 8. -- When committing only documentation changes please include `[ci skip]` in the commit message to avoid running tests on AppVeyor. -- Include [release notes](https://github.com/python-pillow/Pillow/tree/main/docs/releasenotes) as needed or appropriate with your bug fixes, feature additions and tests. -- 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 - -When reporting issues, please include code that reproduces the issue and whenever possible, an image that demonstrates the issue. Please upload images to GitHub, not to third-party file hosting sites. If necessary, add the image to a zip or tar archive. - -The best reproductions are self-contained scripts with minimal dependencies. If you are using a framework such as plone, Django, or buildout, try to replicate the issue just using Pillow. - -### Provide details - -- What did you do? -- What did you expect to happen? -- What actually happened? -- What versions of Pillow and Python are you using? - -## Security vulnerabilities - -Please see our [security policy](https://github.com/python-pillow/Pillow/blob/main/.github/SECURITY.md). +# Contributing to Pillow + +Bug fixes, feature additions, tests, documentation and more can be contributed via [issues](https://github.com/python-pillow/Pillow/issues) and/or [pull requests](https://github.com/python-pillow/Pillow/pulls). All contributions are welcome. + +## Bug fixes, feature additions, etc. + +Please send a pull request to the `main` branch. Please include [documentation](https://pillow.readthedocs.io) and [tests](../Tests/README.rst) for new features. Tests or documentation without bug fixes or feature additions are welcome too. Feel free to ask questions [via issues](https://github.com/python-pillow/Pillow/issues/new), [discussions](https://github.com/python-pillow/Pillow/discussions/new), [Gitter](https://gitter.im/python-pillow/Pillow) or irc://irc.freenode.net#pil + +- Fork the Pillow repository. +- Create a branch from `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. +- Create a pull request to pull the changes from your branch to the Pillow `main`. + +### Guidelines + +- Separate code commits from reformatting commits. +- Provide tests for any newly added code. +- Follow PEP 8. +- When committing only documentation changes please include `[ci skip]` in the commit message to avoid running tests on AppVeyor. +- Include [release notes](https://github.com/python-pillow/Pillow/tree/main/docs/releasenotes) as needed or appropriate with your bug fixes, feature additions and tests. +- 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 + +When reporting issues, please include code that reproduces the issue and whenever possible, an image that demonstrates the issue. Please upload images to GitHub, not to third-party file hosting sites. If necessary, add the image to a zip or tar archive. + +The best reproductions are self-contained scripts with minimal dependencies. If you are using a framework such as plone, Django, or buildout, try to replicate the issue just using Pillow. + +### Provide details + +- What did you do? +- What did you expect to happen? +- What actually happened? +- What versions of Pillow and Python are you using? + +## Security vulnerabilities + +Please see our [security policy](https://github.com/python-pillow/Pillow/blob/main/.github/SECURITY.md). diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 8fc6bd0ad..042231413 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -tidelift: "pypi/pillow" +tidelift: "pypi/pillow" diff --git a/.github/ISSUE_TEMPLATE/ISSUE_REPORT.md b/.github/ISSUE_TEMPLATE/ISSUE_REPORT.md index ea4c0e92b..803fc97a5 100644 --- a/.github/ISSUE_TEMPLATE/ISSUE_REPORT.md +++ b/.github/ISSUE_TEMPLATE/ISSUE_REPORT.md @@ -1,74 +1,74 @@ ---- -name: Issue report -about: Create a report to help us improve Pillow ---- - - - -### What did you do? - -### What did you expect to happen? - -### What actually happened? - -### What are your OS, Python and Pillow versions? - -* OS: -* Python: -* Pillow: - -```text -Please paste here the output of running: - -python3 -m PIL.report -or -python3 -m PIL --report - -Or the output of the following Python code: - -from PIL import report -# or -from PIL import features -features.pilinfo(supported_formats=False) -``` - - - -```python -code goes here -``` +--- +name: Issue report +about: Create a report to help us improve Pillow +--- + + + +### What did you do? + +### What did you expect to happen? + +### What actually happened? + +### What are your OS, Python and Pillow versions? + +* OS: +* Python: +* Pillow: + +```text +Please paste here the output of running: + +python3 -m PIL.report +or +python3 -m PIL --report + +Or the output of the following Python code: + +from PIL import report +# or +from PIL import features +features.pilinfo(supported_formats=False) +``` + + + +```python +code goes here +``` diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 8413f29d3..680aee678 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,7 +1,7 @@ -Fixes # . - -Changes proposed in this pull request: - - * - * - * +Fixes # . + +Changes proposed in this pull request: + + * + * + * diff --git a/.github/SECURITY.md b/.github/SECURITY.md index c6369fdef..678359048 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -1,5 +1,5 @@ -# Security policy - -To report sensitive vulnerability information, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. - -If your organisation/employer is a distributor of Pillow and would like advance notification of security-related bugs, please let us know your preferred contact method. +# Security policy + +To report sensitive vulnerability information, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. + +If your organisation/employer is a distributor of Pillow and would like advance notification of security-related bugs, please let us know your preferred contact method. diff --git a/.github/mergify.yml b/.github/mergify.yml index 3c2066137..fc7e715a3 100644 --- a/.github/mergify.yml +++ b/.github/mergify.yml @@ -1,15 +1,15 @@ -pull_request_rules: - - name: Automatic merge - conditions: - - "#approved-reviews-by>=1" - - label=automerge - - status-success=Lint - - status-success=Test Successful - - status-success=Docker Test Successful - - status-success=Windows Test Successful - - status-success=MinGW - - status-success=Cygwin Test Successful - - status-success=continuous-integration/appveyor/pr - actions: - merge: - method: merge +pull_request_rules: + - name: Automatic merge + conditions: + - "#approved-reviews-by>=1" + - label=automerge + - status-success=Lint + - status-success=Test Successful + - status-success=Docker Test Successful + - status-success=Windows Test Successful + - status-success=MinGW + - status-success=Cygwin Test Successful + - status-success=continuous-integration/appveyor/pr + actions: + merge: + method: merge diff --git a/.github/problem-matchers/gcc.json b/.github/problem-matchers/gcc.json index 8e2866afe..ea978a740 100644 --- a/.github/problem-matchers/gcc.json +++ b/.github/problem-matchers/gcc.json @@ -1,18 +1,18 @@ -{ - "__comment": "Based on vscode-cpptools' Extension/package.json gcc rule", - "problemMatcher": [ - { - "owner": "gcc-problem-matcher", - "pattern": [ - { - "regexp": "^\\s*(.*):(\\d+):(\\d+):\\s+(?:fatal\\s+)?(warning|error):\\s+(.*)$", - "file": 1, - "line": 2, - "column": 3, - "severity": 4, - "message": 5 - } - ] - } - ] -} +{ + "__comment": "Based on vscode-cpptools' Extension/package.json gcc rule", + "problemMatcher": [ + { + "owner": "gcc-problem-matcher", + "pattern": [ + { + "regexp": "^\\s*(.*):(\\d+):(\\d+):\\s+(?:fatal\\s+)?(warning|error):\\s+(.*)$", + "file": 1, + "line": 2, + "column": 3, + "severity": 4, + "message": 5 + } + ] + } + ] +} diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 3711d91f0..721b387f9 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -1,28 +1,28 @@ -name-template: "$NEXT_MINOR_VERSION" -tag-template: "$NEXT_MINOR_VERSION" -change-template: '- $TITLE #$NUMBER [@$AUTHOR]' - -categories: - - title: "Dependencies" - label: "Dependency" - - title: "Deprecations" - label: "Deprecation" - - title: "Documentation" - label: "Documentation" - - title: "Removals" - label: "Removal" - - title: "Testing" - label: "Testing" - - title: "Type hints" - label: "Type hints" - -exclude-labels: - - "changelog: skip" - -template: | - - https://pillow.readthedocs.io/en/stable/releasenotes/$NEXT_MINOR_VERSION.html - - ## Changes - - $CHANGES +name-template: "$NEXT_MINOR_VERSION" +tag-template: "$NEXT_MINOR_VERSION" +change-template: '- $TITLE #$NUMBER [@$AUTHOR]' + +categories: + - title: "Dependencies" + label: "Dependency" + - title: "Deprecations" + label: "Deprecation" + - title: "Documentation" + label: "Documentation" + - title: "Removals" + label: "Removal" + - title: "Testing" + label: "Testing" + - title: "Type hints" + label: "Type hints" + +exclude-labels: + - "changelog: skip" + +template: | + + https://pillow.readthedocs.io/en/stable/releasenotes/$NEXT_MINOR_VERSION.html + + ## Changes + + $CHANGES diff --git a/.github/renovate.json b/.github/renovate.json index d1d824335..4b060c9e2 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -1,17 +1,17 @@ -{ - "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": [ - "config:base" - ], - "labels": [ - "Dependency" - ], - "packageRules": [ - { - "groupName": "github-actions", - "matchManagers": ["github-actions"], - "separateMajorMinor": "false" - } - ], - "schedule": ["on the 3rd day of the month"] -} +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:base" + ], + "labels": [ + "Dependency" + ], + "packageRules": [ + { + "groupName": "github-actions", + "matchManagers": ["github-actions"], + "separateMajorMinor": "false" + } + ], + "schedule": ["on the 3rd day of the month"] +} diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml index eb73fc6a7..ae74d12a3 100644 --- a/.github/workflows/cifuzz.yml +++ b/.github/workflows/cifuzz.yml @@ -1,60 +1,60 @@ -name: CIFuzz - -on: - push: - branches: - - "**" - paths: - - ".github/workflows/cifuzz.yml" - - "**.c" - - "**.h" - pull_request: - paths: - - ".github/workflows/cifuzz.yml" - - "**.c" - - "**.h" - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - Fuzzing: - runs-on: ubuntu-latest - steps: - - name: Build Fuzzers - id: build - uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master - with: - oss-fuzz-project-name: 'pillow' - language: python - dry-run: false - - name: Run Fuzzers - id: run - uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master - with: - oss-fuzz-project-name: 'pillow' - fuzz-seconds: 600 - language: python - dry-run: false - - name: Upload New Crash - uses: actions/upload-artifact@v4 - if: failure() && steps.build.outcome == 'success' - with: - name: artifacts - path: ./out/artifacts - - name: Upload Legacy Crash - uses: actions/upload-artifact@v4 - if: steps.run.outcome == 'success' - with: - name: crash - path: ./out/crash* - - name: Fail on legacy crash - if: success() - run: | - [ ! -e out/crash-* ] - echo No legacy crash detected +name: CIFuzz + +on: + push: + branches: + - "**" + paths: + - ".github/workflows/cifuzz.yml" + - "**.c" + - "**.h" + pull_request: + paths: + - ".github/workflows/cifuzz.yml" + - "**.c" + - "**.h" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + Fuzzing: + runs-on: ubuntu-latest + steps: + - name: Build Fuzzers + id: build + uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master + with: + oss-fuzz-project-name: 'pillow' + language: python + dry-run: false + - name: Run Fuzzers + id: run + uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master + with: + oss-fuzz-project-name: 'pillow' + fuzz-seconds: 600 + language: python + dry-run: false + - name: Upload New Crash + uses: actions/upload-artifact@v4 + if: failure() && steps.build.outcome == 'success' + with: + name: artifacts + path: ./out/artifacts + - name: Upload Legacy Crash + uses: actions/upload-artifact@v4 + if: steps.run.outcome == 'success' + with: + name: crash + path: ./out/crash* + - name: Fail on legacy crash + if: success() + run: | + [ ! -e out/crash-* ] + echo No legacy crash detected diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 92e860cb5..edd631ed1 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,69 +1,69 @@ -name: Docs - -on: - push: - branches: - - "**" - paths: - - ".github/workflows/docs.yml" - - "docs/**" - - "src/PIL/**" - pull_request: - paths: - - ".github/workflows/docs.yml" - - "docs/**" - - "src/PIL/**" - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -env: - FORCE_COLOR: 1 - -jobs: - build: - - runs-on: ubuntu-latest - name: Docs - - steps: - - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.x" - cache: pip - cache-dependency-path: | - ".ci/*.sh" - "pyproject.toml" - - - name: Build system information - run: python3 .github/workflows/system-info.py - - - name: Cache libimagequant - uses: actions/cache@v4 - id: cache-libimagequant - with: - path: ~/cache-libimagequant - key: ${{ runner.os }}-libimagequant-${{ hashFiles('depends/install_imagequant.sh') }} - - - name: Install Linux dependencies - run: | - .ci/install.sh - env: - GHA_PYTHON_VERSION: "3.x" - GHA_LIBIMAGEQUANT_CACHE_HIT: ${{ steps.cache-libimagequant.outputs.cache-hit }} - - - name: Build - run: | - .ci/build.sh - - - name: Docs - run: | - make doccheck +name: Docs + +on: + push: + branches: + - "**" + paths: + - ".github/workflows/docs.yml" + - "docs/**" + - "src/PIL/**" + pull_request: + paths: + - ".github/workflows/docs.yml" + - "docs/**" + - "src/PIL/**" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + FORCE_COLOR: 1 + +jobs: + build: + + runs-on: ubuntu-latest + name: Docs + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + cache: pip + cache-dependency-path: | + ".ci/*.sh" + "pyproject.toml" + + - name: Build system information + run: python3 .github/workflows/system-info.py + + - name: Cache libimagequant + uses: actions/cache@v4 + id: cache-libimagequant + with: + path: ~/cache-libimagequant + key: ${{ runner.os }}-libimagequant-${{ hashFiles('depends/install_imagequant.sh') }} + + - name: Install Linux dependencies + run: | + .ci/install.sh + env: + GHA_PYTHON_VERSION: "3.x" + GHA_LIBIMAGEQUANT_CACHE_HIT: ${{ steps.cache-libimagequant.outputs.cache-hit }} + + - name: Build + run: | + .ci/build.sh + + - name: Docs + run: | + make doccheck diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index cc4760288..672a8270f 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,54 +1,54 @@ -name: Lint - -on: [push, pull_request, workflow_dispatch] - -env: - FORCE_COLOR: 1 - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - build: - - runs-on: ubuntu-latest - - name: Lint - - steps: - - uses: actions/checkout@v4 - - - name: pre-commit cache - uses: actions/cache@v4 - with: - path: ~/.cache/pre-commit - key: lint-pre-commit-${{ hashFiles('**/.pre-commit-config.yaml') }} - restore-keys: | - lint-pre-commit- - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.x" - cache: pip - cache-dependency-path: "setup.py" - - - name: Build system information - run: python3 .github/workflows/system-info.py - - - name: Install dependencies - run: | - python3 -m pip install -U pip - python3 -m pip install -U tox - - - name: Lint - run: tox -e lint - env: - PRE_COMMIT_COLOR: always - - - name: Mypy - run: tox -e mypy +name: Lint + +on: [push, pull_request, workflow_dispatch] + +env: + FORCE_COLOR: 1 + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + + runs-on: ubuntu-latest + + name: Lint + + steps: + - uses: actions/checkout@v4 + + - name: pre-commit cache + uses: actions/cache@v4 + with: + path: ~/.cache/pre-commit + key: lint-pre-commit-${{ hashFiles('**/.pre-commit-config.yaml') }} + restore-keys: | + lint-pre-commit- + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + cache: pip + cache-dependency-path: "setup.py" + + - name: Build system information + run: python3 .github/workflows/system-info.py + + - name: Install dependencies + run: | + python3 -m pip install -U pip + python3 -m pip install -U tox + + - name: Lint + run: tox -e lint + env: + PRE_COMMIT_COLOR: always + + - name: Mypy + run: tox -e mypy diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh index f8f191d38..60ac26ee8 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -1,36 +1,36 @@ -#!/bin/bash - -set -e - -brew install \ - freetype \ - ghostscript \ - libimagequant \ - libjpeg \ - libtiff \ - little-cms2 \ - openjpeg \ - webp -if [[ "$ImageOS" == "macos13" ]]; then - brew install --ignore-dependencies libraqm -else - brew install libraqm -fi -export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig" - -# TODO Update condition when cffi supports 3.13 -if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then PYTHONOPTIMIZE=0 python3 -m pip install cffi ; fi - -python3 -m pip install coverage -python3 -m pip install defusedxml -python3 -m pip install olefile -python3 -m pip install -U pytest -python3 -m pip install -U pytest-cov -python3 -m pip install -U pytest-timeout -python3 -m pip install pyroma - -# TODO Update condition when NumPy supports 3.13 -if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then python3 -m pip install numpy ; fi - -# extra test images -pushd depends && ./install_extra_test_images.sh && popd +#!/bin/bash + +set -e + +brew install \ + freetype \ + ghostscript \ + libimagequant \ + libjpeg \ + libtiff \ + little-cms2 \ + openjpeg \ + webp +if [[ "$ImageOS" == "macos13" ]]; then + brew install --ignore-dependencies libraqm +else + brew install libraqm +fi +export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig" + +# TODO Update condition when cffi supports 3.13 +if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then PYTHONOPTIMIZE=0 python3 -m pip install cffi ; fi + +python3 -m pip install coverage +python3 -m pip install defusedxml +python3 -m pip install olefile +python3 -m pip install -U pytest +python3 -m pip install -U pytest-cov +python3 -m pip install -U pytest-timeout +python3 -m pip install pyroma + +# TODO Update condition when NumPy supports 3.13 +if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then python3 -m pip install numpy ; fi + +# extra test images +pushd depends && ./install_extra_test_images.sh && popd diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index a8ddef22c..c740bcadd 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -1,28 +1,28 @@ -name: Release drafter - -on: - push: - # branches to consider in the event; optional, defaults to all - branches: - - main - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - update_release_draft: - permissions: - contents: write # for release-drafter/release-drafter to create a github release - pull-requests: write # for release-drafter/release-drafter to add label to PR - if: github.repository == 'python-pillow/Pillow' - runs-on: ubuntu-latest - steps: - # Drafts your next release notes as pull requests are merged into "main" - - uses: release-drafter/release-drafter@v6 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} +name: Release drafter + +on: + push: + # branches to consider in the event; optional, defaults to all + branches: + - main + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + update_release_draft: + permissions: + contents: write # for release-drafter/release-drafter to create a github release + pull-requests: write # for release-drafter/release-drafter to add label to PR + if: github.repository == 'python-pillow/Pillow' + runs-on: ubuntu-latest + steps: + # Drafts your next release notes as pull requests are merged into "main" + - uses: release-drafter/release-drafter@v6 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 545c2e364..af450ed14 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -1,31 +1,31 @@ -name: Close stale issues - -on: - schedule: - - cron: "10 0 * * *" - workflow_dispatch: - -permissions: - issues: write - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - stale: - if: github.repository_owner == 'python-pillow' - - runs-on: ubuntu-latest - - steps: - - name: "Check issues" - uses: actions/stale@v9 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - only-labels: "Awaiting OP Action" - close-issue-message: "Closing this issue as no feedback has been received." - days-before-stale: 7 - days-before-issue-close: 0 - days-before-pr-close: -1 - labels-to-remove-when-unstale: "Awaiting OP Action" +name: Close stale issues + +on: + schedule: + - cron: "10 0 * * *" + workflow_dispatch: + +permissions: + issues: write + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + stale: + if: github.repository_owner == 'python-pillow' + + runs-on: ubuntu-latest + + steps: + - name: "Check issues" + uses: actions/stale@v9 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + only-labels: "Awaiting OP Action" + close-issue-message: "Closing this issue as no feedback has been received." + days-before-stale: 7 + days-before-issue-close: 0 + days-before-pr-close: -1 + labels-to-remove-when-unstale: "Awaiting OP Action" diff --git a/.github/workflows/system-info.py b/.github/workflows/system-info.py index 9e97b8971..6f4e85a0c 100644 --- a/.github/workflows/system-info.py +++ b/.github/workflows/system-info.py @@ -1,28 +1,28 @@ -""" -Print out some handy system info like Travis CI does. - -This sort of info is missing from GitHub Actions. - -Requested here: -https://github.com/actions/virtual-environments/issues/79 -""" - -from __future__ import annotations - -import os -import platform -import sys - -print("Build system information") -print() - -print("sys.version\t\t", sys.version.split("\n")) -print("os.name\t\t\t", os.name) -print("sys.platform\t\t", sys.platform) -print("platform.system()\t", platform.system()) -print("platform.machine()\t", platform.machine()) -print("platform.platform()\t", platform.platform()) -print("platform.version()\t", platform.version()) -print("platform.uname()\t", platform.uname()) -if sys.platform == "darwin": - print("platform.mac_ver()\t", platform.mac_ver()) +""" +Print out some handy system info like Travis CI does. + +This sort of info is missing from GitHub Actions. + +Requested here: +https://github.com/actions/virtual-environments/issues/79 +""" + +from __future__ import annotations + +import os +import platform +import sys + +print("Build system information") +print() + +print("sys.version\t\t", sys.version.split("\n")) +print("os.name\t\t\t", os.name) +print("sys.platform\t\t", sys.platform) +print("platform.system()\t", platform.system()) +print("platform.machine()\t", platform.machine()) +print("platform.platform()\t", platform.platform()) +print("platform.version()\t", platform.version()) +print("platform.uname()\t", platform.uname()) +if sys.platform == "darwin": + print("platform.mac_ver()\t", platform.mac_ver()) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 1269ef8cb..9ec208a26 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -1,151 +1,151 @@ -name: Test Cygwin - -on: - push: - branches: - - "**" - paths-ignore: - - ".github/workflows/docs.yml" - - ".github/workflows/wheels*" - - ".gitmodules" - - "docs/**" - - "wheels/**" - pull_request: - paths-ignore: - - ".github/workflows/docs.yml" - - ".github/workflows/wheels*" - - ".gitmodules" - - "docs/**" - - "wheels/**" - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -env: - COVERAGE_CORE: sysmon - -jobs: - build: - runs-on: windows-latest - strategy: - fail-fast: false - matrix: - python-minor-version: [8, 9] - - timeout-minutes: 40 - - name: Python 3.${{ matrix.python-minor-version }} - - steps: - - name: Fix line endings - run: | - git config --global core.autocrlf input - - - name: Checkout Pillow - uses: actions/checkout@v4 - - - name: Install Cygwin - uses: cygwin/cygwin-install-action@v4 - with: - packages: > - gcc-g++ - ghostscript - git - ImageMagick - jpeg - libfreetype-devel - libimagequant-devel - libjpeg-devel - liblapack-devel - liblcms2-devel - libopenjp2-devel - libraqm-devel - libtiff-devel - libwebp-devel - libxcb-devel - libxcb-xinerama0 - make - netpbm - perl - python3${{ matrix.python-minor-version }}-cffi - python3${{ matrix.python-minor-version }}-cython - python3${{ matrix.python-minor-version }}-devel - python3${{ matrix.python-minor-version }}-numpy - python3${{ matrix.python-minor-version }}-sip - python3${{ matrix.python-minor-version }}-tkinter - wget - xorg-server-extra - zlib-devel - - - name: Add Lapack to PATH - uses: egor-tensin/cleanup-path@v4 - with: - dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack' - - - name: Select Python version - run: | - ln -sf c:/cygwin/bin/python3.${{ matrix.python-minor-version }} c:/cygwin/bin/python3 - - - name: pip cache - uses: actions/cache@v4 - with: - path: 'C:\cygwin\home\runneradmin\.cache\pip' - key: ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-${{ hashFiles('.ci/install.sh') }} - restore-keys: | - ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}- - - - name: Build system information - run: | - dash.exe -c "python3 .github/workflows/system-info.py" - - - name: Install dependencies - run: | - bash.exe .ci/install.sh - - - name: Build - shell: bash.exe -eo pipefail -o igncr "{0}" - run: | - .ci/build.sh - - - name: Test - run: | - bash.exe xvfb-run -s '-screen 0 1024x768x24' .ci/test.sh - - - name: Prepare to upload errors - if: failure() - run: | - dash.exe -c "mkdir -p Tests/errors" - - - name: Upload errors - uses: actions/upload-artifact@v4 - if: failure() - with: - name: errors - path: Tests/errors - - - name: After success - run: | - bash.exe .ci/after_success.sh - - - name: Upload coverage - uses: codecov/codecov-action@v4 - with: - file: ./coverage.xml - flags: GHA_Cygwin - name: Cygwin Python 3.${{ matrix.python-minor-version }} - token: ${{ secrets.CODECOV_ORG_TOKEN }} - - success: - permissions: - contents: none - needs: build - runs-on: ubuntu-latest - name: Cygwin Test Successful - steps: - - name: Success - run: echo Cygwin Test Successful +name: Test Cygwin + +on: + push: + branches: + - "**" + paths-ignore: + - ".github/workflows/docs.yml" + - ".github/workflows/wheels*" + - ".gitmodules" + - "docs/**" + - "wheels/**" + pull_request: + paths-ignore: + - ".github/workflows/docs.yml" + - ".github/workflows/wheels*" + - ".gitmodules" + - "docs/**" + - "wheels/**" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + COVERAGE_CORE: sysmon + +jobs: + build: + runs-on: windows-latest + strategy: + fail-fast: false + matrix: + python-minor-version: [8, 9] + + timeout-minutes: 40 + + name: Python 3.${{ matrix.python-minor-version }} + + steps: + - name: Fix line endings + run: | + git config --global core.autocrlf input + + - name: Checkout Pillow + uses: actions/checkout@v4 + + - name: Install Cygwin + uses: cygwin/cygwin-install-action@v4 + with: + packages: > + gcc-g++ + ghostscript + git + ImageMagick + jpeg + libfreetype-devel + libimagequant-devel + libjpeg-devel + liblapack-devel + liblcms2-devel + libopenjp2-devel + libraqm-devel + libtiff-devel + libwebp-devel + libxcb-devel + libxcb-xinerama0 + make + netpbm + perl + python3${{ matrix.python-minor-version }}-cffi + python3${{ matrix.python-minor-version }}-cython + python3${{ matrix.python-minor-version }}-devel + python3${{ matrix.python-minor-version }}-numpy + python3${{ matrix.python-minor-version }}-sip + python3${{ matrix.python-minor-version }}-tkinter + wget + xorg-server-extra + zlib-devel + + - name: Add Lapack to PATH + uses: egor-tensin/cleanup-path@v4 + with: + dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack' + + - name: Select Python version + run: | + ln -sf c:/cygwin/bin/python3.${{ matrix.python-minor-version }} c:/cygwin/bin/python3 + + - name: pip cache + uses: actions/cache@v4 + with: + path: 'C:\cygwin\home\runneradmin\.cache\pip' + key: ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-${{ hashFiles('.ci/install.sh') }} + restore-keys: | + ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}- + + - name: Build system information + run: | + dash.exe -c "python3 .github/workflows/system-info.py" + + - name: Install dependencies + run: | + bash.exe .ci/install.sh + + - name: Build + shell: bash.exe -eo pipefail -o igncr "{0}" + run: | + .ci/build.sh + + - name: Test + run: | + bash.exe xvfb-run -s '-screen 0 1024x768x24' .ci/test.sh + + - name: Prepare to upload errors + if: failure() + run: | + dash.exe -c "mkdir -p Tests/errors" + + - name: Upload errors + uses: actions/upload-artifact@v4 + if: failure() + with: + name: errors + path: Tests/errors + + - name: After success + run: | + bash.exe .ci/after_success.sh + + - name: Upload coverage + uses: codecov/codecov-action@v4 + with: + file: ./coverage.xml + flags: GHA_Cygwin + name: Cygwin Python 3.${{ matrix.python-minor-version }} + token: ${{ secrets.CODECOV_ORG_TOKEN }} + + success: + permissions: + contents: none + needs: build + runs-on: ubuntu-latest + name: Cygwin Test Successful + steps: + - name: Success + run: echo Cygwin Test Successful diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 6afed74db..144747544 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -1,118 +1,118 @@ -name: Test Docker - -on: - push: - branches: - - "**" - paths-ignore: - - ".github/workflows/docs.yml" - - ".github/workflows/wheels*" - - ".gitmodules" - - "docs/**" - - "wheels/**" - pull_request: - paths-ignore: - - ".github/workflows/docs.yml" - - ".github/workflows/wheels*" - - ".gitmodules" - - "docs/**" - - "wheels/**" - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - build: - - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - docker: [ - # Run slower jobs first to give them a headstart and reduce waiting time - ubuntu-22.04-jammy-arm64v8, - ubuntu-24.04-noble-ppc64le, - ubuntu-24.04-noble-s390x, - # Then run the remainder - alpine, - amazon-2-amd64, - amazon-2023-amd64, - arch, - centos-stream-9-amd64, - debian-11-bullseye-amd64, - debian-12-bookworm-x86, - debian-12-bookworm-amd64, - fedora-39-amd64, - fedora-40-amd64, - gentoo, - ubuntu-20.04-focal-amd64, - ubuntu-22.04-jammy-amd64, - ubuntu-24.04-noble-amd64, - ] - dockerTag: [main] - include: - - docker: "ubuntu-22.04-jammy-arm64v8" - qemu-arch: "aarch64" - - docker: "ubuntu-24.04-noble-ppc64le" - qemu-arch: "ppc64le" - - docker: "ubuntu-24.04-noble-s390x" - qemu-arch: "s390x" - - name: ${{ matrix.docker }} - - steps: - - uses: actions/checkout@v4 - - - name: Build system information - run: python3 .github/workflows/system-info.py - - - name: Set up QEMU - if: "matrix.qemu-arch" - run: | - docker run --rm --privileged aptman/qus -s -- -p ${{ matrix.qemu-arch }} - - - name: Docker pull - run: | - docker pull pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} - - - name: Docker build - run: | - # The Pillow user in the docker container is UID 1001 - sudo chown -R 1001 $GITHUB_WORKSPACE - docker run --name pillow_container -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} - sudo chown -R runner $GITHUB_WORKSPACE - - - name: After success - run: | - PATH="$PATH:~/.local/bin" - docker start pillow_container - pil_path=`docker exec pillow_container /vpy3/bin/python -c 'import os, PIL;print(os.path.realpath(os.path.dirname(PIL.__file__)))'` - docker stop pillow_container - sudo mkdir -p $pil_path - sudo cp src/PIL/*.py $pil_path - .ci/after_success.sh - env: - MATRIX_DOCKER: ${{ matrix.docker }} - - - name: Upload coverage - uses: codecov/codecov-action@v4 - with: - flags: GHA_Docker - name: ${{ matrix.docker }} - gcov: true - token: ${{ secrets.CODECOV_ORG_TOKEN }} - - success: - permissions: - contents: none - needs: build - runs-on: ubuntu-latest - name: Docker Test Successful - steps: - - name: Success - run: echo Docker Test Successful +name: Test Docker + +on: + push: + branches: + - "**" + paths-ignore: + - ".github/workflows/docs.yml" + - ".github/workflows/wheels*" + - ".gitmodules" + - "docs/**" + - "wheels/**" + pull_request: + paths-ignore: + - ".github/workflows/docs.yml" + - ".github/workflows/wheels*" + - ".gitmodules" + - "docs/**" + - "wheels/**" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + docker: [ + # Run slower jobs first to give them a headstart and reduce waiting time + ubuntu-22.04-jammy-arm64v8, + ubuntu-24.04-noble-ppc64le, + ubuntu-24.04-noble-s390x, + # Then run the remainder + alpine, + amazon-2-amd64, + amazon-2023-amd64, + arch, + centos-stream-9-amd64, + debian-11-bullseye-amd64, + debian-12-bookworm-x86, + debian-12-bookworm-amd64, + fedora-39-amd64, + fedora-40-amd64, + gentoo, + ubuntu-20.04-focal-amd64, + ubuntu-22.04-jammy-amd64, + ubuntu-24.04-noble-amd64, + ] + dockerTag: [main] + include: + - docker: "ubuntu-22.04-jammy-arm64v8" + qemu-arch: "aarch64" + - docker: "ubuntu-24.04-noble-ppc64le" + qemu-arch: "ppc64le" + - docker: "ubuntu-24.04-noble-s390x" + qemu-arch: "s390x" + + name: ${{ matrix.docker }} + + steps: + - uses: actions/checkout@v4 + + - name: Build system information + run: python3 .github/workflows/system-info.py + + - name: Set up QEMU + if: "matrix.qemu-arch" + run: | + docker run --rm --privileged aptman/qus -s -- -p ${{ matrix.qemu-arch }} + + - name: Docker pull + run: | + docker pull pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} + + - name: Docker build + run: | + # The Pillow user in the docker container is UID 1001 + sudo chown -R 1001 $GITHUB_WORKSPACE + docker run --name pillow_container -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} + sudo chown -R runner $GITHUB_WORKSPACE + + - name: After success + run: | + PATH="$PATH:~/.local/bin" + docker start pillow_container + pil_path=`docker exec pillow_container /vpy3/bin/python -c 'import os, PIL;print(os.path.realpath(os.path.dirname(PIL.__file__)))'` + docker stop pillow_container + sudo mkdir -p $pil_path + sudo cp src/PIL/*.py $pil_path + .ci/after_success.sh + env: + MATRIX_DOCKER: ${{ matrix.docker }} + + - name: Upload coverage + uses: codecov/codecov-action@v4 + with: + flags: GHA_Docker + name: ${{ matrix.docker }} + gcov: true + token: ${{ secrets.CODECOV_ORG_TOKEN }} + + success: + permissions: + contents: none + needs: build + runs-on: ubuntu-latest + name: Docker Test Successful + steps: + - name: Success + run: echo Docker Test Successful diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index a773ca453..dd581b120 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -1,93 +1,93 @@ -name: Test MinGW - -on: - push: - branches: - - "**" - paths-ignore: - - ".github/workflows/docs.yml" - - ".github/workflows/wheels*" - - ".gitmodules" - - "docs/**" - - "wheels/**" - pull_request: - paths-ignore: - - ".github/workflows/docs.yml" - - ".github/workflows/wheels*" - - ".gitmodules" - - "docs/**" - - "wheels/**" - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -env: - COVERAGE_CORE: sysmon - -jobs: - build: - runs-on: windows-latest - - defaults: - run: - shell: bash.exe --login -eo pipefail "{0}" - env: - MSYSTEM: MINGW64 - CHERE_INVOKING: 1 - - timeout-minutes: 30 - name: "MinGW" - - steps: - - name: Checkout Pillow - uses: actions/checkout@v4 - - - name: Set up shell - run: echo "C:\msys64\usr\bin\" >> $env:GITHUB_PATH - shell: pwsh - - - name: Install dependencies - run: | - pacman -S --noconfirm \ - mingw-w64-x86_64-freetype \ - mingw-w64-x86_64-gcc \ - mingw-w64-x86_64-ghostscript \ - mingw-w64-x86_64-lcms2 \ - mingw-w64-x86_64-libimagequant \ - mingw-w64-x86_64-libjpeg-turbo \ - mingw-w64-x86_64-libraqm \ - mingw-w64-x86_64-libtiff \ - mingw-w64-x86_64-libwebp \ - mingw-w64-x86_64-openjpeg2 \ - mingw-w64-x86_64-python3-cffi \ - mingw-w64-x86_64-python3-numpy \ - mingw-w64-x86_64-python3-olefile \ - mingw-w64-x86_64-python3-setuptools \ - mingw-w64-x86_64-python-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 . - - - name: Test Pillow - run: | - python3 selftest.py --installed - python3 -c "from PIL import Image" - python3 -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests - - - name: Upload coverage - uses: codecov/codecov-action@v4 - with: - file: ./coverage.xml - flags: GHA_Windows - name: "MSYS2 MinGW" - token: ${{ secrets.CODECOV_ORG_TOKEN }} +name: Test MinGW + +on: + push: + branches: + - "**" + paths-ignore: + - ".github/workflows/docs.yml" + - ".github/workflows/wheels*" + - ".gitmodules" + - "docs/**" + - "wheels/**" + pull_request: + paths-ignore: + - ".github/workflows/docs.yml" + - ".github/workflows/wheels*" + - ".gitmodules" + - "docs/**" + - "wheels/**" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + COVERAGE_CORE: sysmon + +jobs: + build: + runs-on: windows-latest + + defaults: + run: + shell: bash.exe --login -eo pipefail "{0}" + env: + MSYSTEM: MINGW64 + CHERE_INVOKING: 1 + + timeout-minutes: 30 + name: "MinGW" + + steps: + - name: Checkout Pillow + uses: actions/checkout@v4 + + - name: Set up shell + run: echo "C:\msys64\usr\bin\" >> $env:GITHUB_PATH + shell: pwsh + + - name: Install dependencies + run: | + pacman -S --noconfirm \ + mingw-w64-x86_64-freetype \ + mingw-w64-x86_64-gcc \ + mingw-w64-x86_64-ghostscript \ + mingw-w64-x86_64-lcms2 \ + mingw-w64-x86_64-libimagequant \ + mingw-w64-x86_64-libjpeg-turbo \ + mingw-w64-x86_64-libraqm \ + mingw-w64-x86_64-libtiff \ + mingw-w64-x86_64-libwebp \ + mingw-w64-x86_64-openjpeg2 \ + mingw-w64-x86_64-python3-cffi \ + mingw-w64-x86_64-python3-numpy \ + mingw-w64-x86_64-python3-olefile \ + mingw-w64-x86_64-python3-setuptools \ + mingw-w64-x86_64-python-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 . + + - name: Test Pillow + run: | + python3 selftest.py --installed + python3 -c "from PIL import Image" + python3 -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests + + - name: Upload coverage + uses: codecov/codecov-action@v4 + with: + file: ./coverage.xml + flags: GHA_Windows + name: "MSYS2 MinGW" + token: ${{ secrets.CODECOV_ORG_TOKEN }} diff --git a/.github/workflows/test-valgrind.yml b/.github/workflows/test-valgrind.yml index 63aec586b..2c85c9e66 100644 --- a/.github/workflows/test-valgrind.yml +++ b/.github/workflows/test-valgrind.yml @@ -1,56 +1,56 @@ -name: Test Valgrind - -# like the Docker tests, but running valgrind only on *.c/*.h changes. - -on: - push: - branches: - - "**" - paths: - - ".github/workflows/test-valgrind.yml" - - "**.c" - - "**.h" - pull_request: - paths: - - ".github/workflows/test-valgrind.yml" - - "**.c" - - "**.h" - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - build: - - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - docker: [ - ubuntu-22.04-jammy-amd64-valgrind, - ] - dockerTag: [main] - - name: ${{ matrix.docker }} - - steps: - - uses: actions/checkout@v4 - - - 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 }} - sudo chown -R runner $GITHUB_WORKSPACE +name: Test Valgrind + +# like the Docker tests, but running valgrind only on *.c/*.h changes. + +on: + push: + branches: + - "**" + paths: + - ".github/workflows/test-valgrind.yml" + - "**.c" + - "**.h" + pull_request: + paths: + - ".github/workflows/test-valgrind.yml" + - "**.c" + - "**.h" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + docker: [ + ubuntu-22.04-jammy-amd64-valgrind, + ] + dockerTag: [main] + + name: ${{ matrix.docker }} + + steps: + - uses: actions/checkout@v4 + + - 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 }} + sudo chown -R runner $GITHUB_WORKSPACE diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index ee265774b..10a3e3bb1 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -1,231 +1,231 @@ -name: Test Windows - -on: - push: - branches: - - "**" - paths-ignore: - - ".github/workflows/docs.yml" - - ".github/workflows/wheels*" - - ".gitmodules" - - "docs/**" - - "wheels/**" - pull_request: - paths-ignore: - - ".github/workflows/docs.yml" - - ".github/workflows/wheels*" - - ".gitmodules" - - "docs/**" - - "wheels/**" - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -env: - COVERAGE_CORE: sysmon - -jobs: - build: - runs-on: windows-latest - strategy: - fail-fast: false - matrix: - python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] - - timeout-minutes: 30 - - name: Python ${{ matrix.python-version }} - - steps: - - name: Checkout Pillow - uses: actions/checkout@v4 - - - name: Checkout cached dependencies - uses: actions/checkout@v4 - with: - repository: python-pillow/pillow-depends - path: winbuild\depends - - - name: Checkout extra test images - uses: actions/checkout@v4 - with: - repository: python-pillow/test-images - path: Tests\test-images - - # sets env: pythonLocation - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - allow-prereleases: true - 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: Install dependencies - id: install - run: | - choco install nasm --no-progress - echo "C:\Program Files\NASM" >> $env:GITHUB_PATH - - choco install ghostscript --version=10.3.1 --no-progress - echo "C:\Program Files\gs\gs10.00.0\bin" >> $env:GITHUB_PATH - - # Install extra test images - xcopy /S /Y Tests\test-images\* Tests\images - - # make cache key depend on VS version - & "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" ` - | find """catalog_buildVersion""" ` - | ForEach-Object { $a = $_.split(" ")[1]; echo "vs=$a" >> $env:GITHUB_OUTPUT } - shell: pwsh - - - name: Cache build - id: build-cache - uses: actions/cache@v4 - with: - path: winbuild\build - key: - ${{ hashFiles('winbuild\build_prepare.py') }}-${{ hashFiles('.github\workflows\test-windows.yml') }}-${{ env.pythonLocation }}-${{ steps.install.outputs.vs }} - - - name: Prepare build - if: steps.build-cache.outputs.cache-hit != 'true' - run: | - & python.exe winbuild\build_prepare.py -v - shell: pwsh - - - name: Build dependencies / libjpeg-turbo - if: steps.build-cache.outputs.cache-hit != 'true' - run: "& winbuild\\build\\build_dep_libjpeg.cmd" - - - name: Build dependencies / zlib - if: steps.build-cache.outputs.cache-hit != 'true' - run: "& winbuild\\build\\build_dep_zlib.cmd" - - - name: Build dependencies / xz - if: steps.build-cache.outputs.cache-hit != 'true' - run: "& winbuild\\build\\build_dep_xz.cmd" - - - name: Build dependencies / WebP - if: steps.build-cache.outputs.cache-hit != 'true' - run: "& winbuild\\build\\build_dep_libwebp.cmd" - - - name: Build dependencies / LibTiff - if: steps.build-cache.outputs.cache-hit != 'true' - run: "& winbuild\\build\\build_dep_libtiff.cmd" - - # for FreeType CBDT/SBIX font support - - name: Build dependencies / libpng - if: steps.build-cache.outputs.cache-hit != 'true' - run: "& winbuild\\build\\build_dep_libpng.cmd" - - # for FreeType WOFF2 font support - - name: Build dependencies / brotli - if: steps.build-cache.outputs.cache-hit != 'true' - run: "& winbuild\\build\\build_dep_brotli.cmd" - - - name: Build dependencies / FreeType - if: steps.build-cache.outputs.cache-hit != 'true' - run: "& winbuild\\build\\build_dep_freetype.cmd" - - - name: Build dependencies / LCMS2 - if: steps.build-cache.outputs.cache-hit != 'true' - run: "& winbuild\\build\\build_dep_lcms2.cmd" - - - name: Build dependencies / OpenJPEG - if: steps.build-cache.outputs.cache-hit != 'true' - run: "& winbuild\\build\\build_dep_openjpeg.cmd" - - # GPL licensed - - name: Build dependencies / libimagequant - if: steps.build-cache.outputs.cache-hit != 'true' - run: "& winbuild\\build\\build_dep_libimagequant.cmd" - - # Raqm dependencies - - name: Build dependencies / HarfBuzz - if: steps.build-cache.outputs.cache-hit != 'true' - run: "& winbuild\\build\\build_dep_harfbuzz.cmd" - - # Raqm dependencies - - name: Build dependencies / FriBidi - if: steps.build-cache.outputs.cache-hit != 'true' - run: "& winbuild\\build\\build_dep_fribidi.cmd" - - # trim ~150MB for each job - - name: Optimize build cache - if: steps.build-cache.outputs.cache-hit != 'true' - run: rmdir /S /Q winbuild\build\src - shell: cmd - - - name: Build Pillow - run: | - $FLAGS="-C raqm=vendor -C fribidi=vendor" - cmd /c "winbuild\build\build_env.cmd && $env:pythonLocation\python.exe -m pip install -v $FLAGS ." - & $env:pythonLocation\python.exe selftest.py --installed - shell: pwsh - - # skip PyPy for speed - - name: Enable heap verification - if: "!contains(matrix.python-version, 'pypy')" - run: | - & reg.exe add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\python.exe" /v "GlobalFlag" /t REG_SZ /d "0x02000000" /f - - - name: Test Pillow - run: | - path %GITHUB_WORKSPACE%\\winbuild\\build\\bin;%PATH% - python.exe -m pytest -vx -W always --cov PIL --cov Tests --cov-report term --cov-report xml Tests - shell: cmd - - - name: Prepare to upload errors - if: failure() - run: | - mkdir -p Tests/errors - shell: bash - - - name: Upload errors - uses: actions/upload-artifact@v4 - if: failure() - with: - name: errors - path: Tests/errors - - - name: After success - run: | - .ci/after_success.sh - shell: pwsh - - - name: Upload coverage - uses: codecov/codecov-action@v4 - with: - file: ./coverage.xml - flags: GHA_Windows - name: ${{ runner.os }} Python ${{ matrix.python-version }} - token: ${{ secrets.CODECOV_ORG_TOKEN }} - - success: - permissions: - contents: none - needs: build - runs-on: ubuntu-latest - name: Windows Test Successful - steps: - - name: Success - run: echo Windows Test Successful +name: Test Windows + +on: + push: + branches: + - "**" + paths-ignore: + - ".github/workflows/docs.yml" + - ".github/workflows/wheels*" + - ".gitmodules" + - "docs/**" + - "wheels/**" + pull_request: + paths-ignore: + - ".github/workflows/docs.yml" + - ".github/workflows/wheels*" + - ".gitmodules" + - "docs/**" + - "wheels/**" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + COVERAGE_CORE: sysmon + +jobs: + build: + runs-on: windows-latest + strategy: + fail-fast: false + matrix: + python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + + timeout-minutes: 30 + + name: Python ${{ matrix.python-version }} + + steps: + - name: Checkout Pillow + uses: actions/checkout@v4 + + - name: Checkout cached dependencies + uses: actions/checkout@v4 + with: + repository: python-pillow/pillow-depends + path: winbuild\depends + + - name: Checkout extra test images + uses: actions/checkout@v4 + with: + repository: python-pillow/test-images + path: Tests\test-images + + # sets env: pythonLocation + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + allow-prereleases: true + 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: Install dependencies + id: install + run: | + choco install nasm --no-progress + echo "C:\Program Files\NASM" >> $env:GITHUB_PATH + + choco install ghostscript --version=10.3.1 --no-progress + echo "C:\Program Files\gs\gs10.00.0\bin" >> $env:GITHUB_PATH + + # Install extra test images + xcopy /S /Y Tests\test-images\* Tests\images + + # make cache key depend on VS version + & "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" ` + | find """catalog_buildVersion""" ` + | ForEach-Object { $a = $_.split(" ")[1]; echo "vs=$a" >> $env:GITHUB_OUTPUT } + shell: pwsh + + - name: Cache build + id: build-cache + uses: actions/cache@v4 + with: + path: winbuild\build + key: + ${{ hashFiles('winbuild\build_prepare.py') }}-${{ hashFiles('.github\workflows\test-windows.yml') }}-${{ env.pythonLocation }}-${{ steps.install.outputs.vs }} + + - name: Prepare build + if: steps.build-cache.outputs.cache-hit != 'true' + run: | + & python.exe winbuild\build_prepare.py -v + shell: pwsh + + - name: Build dependencies / libjpeg-turbo + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_libjpeg.cmd" + + - name: Build dependencies / zlib + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_zlib.cmd" + + - name: Build dependencies / xz + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_xz.cmd" + + - name: Build dependencies / WebP + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_libwebp.cmd" + + - name: Build dependencies / LibTiff + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_libtiff.cmd" + + # for FreeType CBDT/SBIX font support + - name: Build dependencies / libpng + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_libpng.cmd" + + # for FreeType WOFF2 font support + - name: Build dependencies / brotli + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_brotli.cmd" + + - name: Build dependencies / FreeType + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_freetype.cmd" + + - name: Build dependencies / LCMS2 + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_lcms2.cmd" + + - name: Build dependencies / OpenJPEG + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_openjpeg.cmd" + + # GPL licensed + - name: Build dependencies / libimagequant + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_libimagequant.cmd" + + # Raqm dependencies + - name: Build dependencies / HarfBuzz + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_harfbuzz.cmd" + + # Raqm dependencies + - name: Build dependencies / FriBidi + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_fribidi.cmd" + + # trim ~150MB for each job + - name: Optimize build cache + if: steps.build-cache.outputs.cache-hit != 'true' + run: rmdir /S /Q winbuild\build\src + shell: cmd + + - name: Build Pillow + run: | + $FLAGS="-C raqm=vendor -C fribidi=vendor" + cmd /c "winbuild\build\build_env.cmd && $env:pythonLocation\python.exe -m pip install -v $FLAGS ." + & $env:pythonLocation\python.exe selftest.py --installed + shell: pwsh + + # skip PyPy for speed + - name: Enable heap verification + if: "!contains(matrix.python-version, 'pypy')" + run: | + & reg.exe add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\python.exe" /v "GlobalFlag" /t REG_SZ /d "0x02000000" /f + + - name: Test Pillow + run: | + path %GITHUB_WORKSPACE%\\winbuild\\build\\bin;%PATH% + python.exe -m pytest -vx -W always --cov PIL --cov Tests --cov-report term --cov-report xml Tests + shell: cmd + + - name: Prepare to upload errors + if: failure() + run: | + mkdir -p Tests/errors + shell: bash + + - name: Upload errors + uses: actions/upload-artifact@v4 + if: failure() + with: + name: errors + path: Tests/errors + + - name: After success + run: | + .ci/after_success.sh + shell: pwsh + + - name: Upload coverage + uses: codecov/codecov-action@v4 + with: + file: ./coverage.xml + flags: GHA_Windows + name: ${{ runner.os }} Python ${{ matrix.python-version }} + token: ${{ secrets.CODECOV_ORG_TOKEN }} + + success: + permissions: + contents: none + needs: build + runs-on: ubuntu-latest + name: Windows Test Successful + steps: + - name: Success + run: echo Windows Test Successful diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index aa5646caf..2b8f47c8d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,168 +1,168 @@ -name: Test - -on: - push: - branches: - - "**" - paths-ignore: - - ".github/workflows/docs.yml" - - ".github/workflows/wheels*" - - ".gitmodules" - - "docs/**" - - "wheels/**" - pull_request: - paths-ignore: - - ".github/workflows/docs.yml" - - ".github/workflows/wheels*" - - ".gitmodules" - - "docs/**" - - "wheels/**" - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -env: - COVERAGE_CORE: sysmon - FORCE_COLOR: 1 - -jobs: - build: - - strategy: - fail-fast: false - matrix: - os: [ - "macos-14", - "ubuntu-latest", - ] - python-version: [ - "pypy3.10", - "pypy3.9", - "3.13", - "3.12", - "3.11", - "3.10", - "3.9", - "3.8", - ] - include: - - python-version: "3.11" - PYTHONOPTIMIZE: 1 - REVERSE: "--reverse" - - python-version: "3.10" - PYTHONOPTIMIZE: 2 - # M1 only available for 3.10+ - - os: "macos-13" - python-version: "3.9" - - os: "macos-13" - python-version: "3.8" - exclude: - - os: "macos-14" - python-version: "3.9" - - os: "macos-14" - python-version: "3.8" - - runs-on: ${{ matrix.os }} - name: ${{ matrix.os }} Python ${{ matrix.python-version }} - - steps: - - uses: actions/checkout@v4 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - allow-prereleases: true - cache: pip - cache-dependency-path: | - ".ci/*.sh" - "pyproject.toml" - - - name: Build system information - run: python3 .github/workflows/system-info.py - - - name: Cache libimagequant - if: startsWith(matrix.os, 'ubuntu') - uses: actions/cache@v4 - id: cache-libimagequant - with: - path: ~/cache-libimagequant - key: ${{ runner.os }}-libimagequant-${{ hashFiles('depends/install_imagequant.sh') }} - - - name: Install Linux dependencies - if: startsWith(matrix.os, 'ubuntu') - run: | - .ci/install.sh - env: - GHA_PYTHON_VERSION: ${{ matrix.python-version }} - GHA_LIBIMAGEQUANT_CACHE_HIT: ${{ steps.cache-libimagequant.outputs.cache-hit }} - - - name: Install macOS dependencies - if: startsWith(matrix.os, 'macOS') - run: | - .github/workflows/macos-install.sh - env: - GHA_PYTHON_VERSION: ${{ matrix.python-version }} - - - name: Register gcc problem matcher - if: "matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'" - run: echo "::add-matcher::.github/problem-matchers/gcc.json" - - - name: Build - run: | - .ci/build.sh - - - name: Test - run: | - if [ $REVERSE ]; then - python3 -m pip install pytest-reverse - fi - if [ "${{ matrix.os }}" = "ubuntu-latest" ]; then - xvfb-run -s '-screen 0 1024x768x24' sway& - export WAYLAND_DISPLAY=wayland-1 - .ci/test.sh - else - .ci/test.sh - fi - env: - PYTHONOPTIMIZE: ${{ matrix.PYTHONOPTIMIZE }} - REVERSE: ${{ matrix.REVERSE }} - - - name: Prepare to upload errors - if: failure() - run: | - mkdir -p Tests/errors - - - name: Upload errors - uses: actions/upload-artifact@v4 - if: failure() - with: - name: errors - path: Tests/errors - - - name: After success - run: | - .ci/after_success.sh - - - name: Upload coverage - uses: codecov/codecov-action@v4 - 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: - permissions: - contents: none - needs: build - runs-on: ubuntu-latest - name: Test Successful - steps: - - name: Success - run: echo Test Successful +name: Test + +on: + push: + branches: + - "**" + paths-ignore: + - ".github/workflows/docs.yml" + - ".github/workflows/wheels*" + - ".gitmodules" + - "docs/**" + - "wheels/**" + pull_request: + paths-ignore: + - ".github/workflows/docs.yml" + - ".github/workflows/wheels*" + - ".gitmodules" + - "docs/**" + - "wheels/**" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + COVERAGE_CORE: sysmon + FORCE_COLOR: 1 + +jobs: + build: + + strategy: + fail-fast: false + matrix: + os: [ + "macos-14", + "ubuntu-latest", + ] + python-version: [ + "pypy3.10", + "pypy3.9", + "3.13", + "3.12", + "3.11", + "3.10", + "3.9", + "3.8", + ] + include: + - python-version: "3.11" + PYTHONOPTIMIZE: 1 + REVERSE: "--reverse" + - python-version: "3.10" + PYTHONOPTIMIZE: 2 + # M1 only available for 3.10+ + - os: "macos-13" + python-version: "3.9" + - os: "macos-13" + python-version: "3.8" + exclude: + - os: "macos-14" + python-version: "3.9" + - os: "macos-14" + python-version: "3.8" + + runs-on: ${{ matrix.os }} + name: ${{ matrix.os }} Python ${{ matrix.python-version }} + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + allow-prereleases: true + cache: pip + cache-dependency-path: | + ".ci/*.sh" + "pyproject.toml" + + - name: Build system information + run: python3 .github/workflows/system-info.py + + - name: Cache libimagequant + if: startsWith(matrix.os, 'ubuntu') + uses: actions/cache@v4 + id: cache-libimagequant + with: + path: ~/cache-libimagequant + key: ${{ runner.os }}-libimagequant-${{ hashFiles('depends/install_imagequant.sh') }} + + - name: Install Linux dependencies + if: startsWith(matrix.os, 'ubuntu') + run: | + .ci/install.sh + env: + GHA_PYTHON_VERSION: ${{ matrix.python-version }} + GHA_LIBIMAGEQUANT_CACHE_HIT: ${{ steps.cache-libimagequant.outputs.cache-hit }} + + - name: Install macOS dependencies + if: startsWith(matrix.os, 'macOS') + run: | + .github/workflows/macos-install.sh + env: + GHA_PYTHON_VERSION: ${{ matrix.python-version }} + + - name: Register gcc problem matcher + if: "matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'" + run: echo "::add-matcher::.github/problem-matchers/gcc.json" + + - name: Build + run: | + .ci/build.sh + + - name: Test + run: | + if [ $REVERSE ]; then + python3 -m pip install pytest-reverse + fi + if [ "${{ matrix.os }}" = "ubuntu-latest" ]; then + xvfb-run -s '-screen 0 1024x768x24' sway& + export WAYLAND_DISPLAY=wayland-1 + .ci/test.sh + else + .ci/test.sh + fi + env: + PYTHONOPTIMIZE: ${{ matrix.PYTHONOPTIMIZE }} + REVERSE: ${{ matrix.REVERSE }} + + - name: Prepare to upload errors + if: failure() + run: | + mkdir -p Tests/errors + + - name: Upload errors + uses: actions/upload-artifact@v4 + if: failure() + with: + name: errors + path: Tests/errors + + - name: After success + run: | + .ci/after_success.sh + + - name: Upload coverage + uses: codecov/codecov-action@v4 + 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: + permissions: + contents: none + needs: build + runs-on: ubuntu-latest + name: Test Successful + steps: + - name: Success + run: echo Test Successful diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 97f70ed84..96cfba19a 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -1,152 +1,152 @@ -#!/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} -fi -export PLAT=$CIBW_ARCHS -source wheels/multibuild/common_utils.sh -source wheels/multibuild/library_builders.sh -if [ -z "$IS_MACOS" ]; then - source wheels/multibuild/manylinux_utils.sh -fi - -ARCHIVE_SDIR=pillow-depends-main - -# Package versions for fresh source builds -FREETYPE_VERSION=2.13.2 -HARFBUZZ_VERSION=8.5.0 -LIBPNG_VERSION=1.6.43 -JPEGTURBO_VERSION=3.0.3 -OPENJPEG_VERSION=2.5.2 -XZ_VERSION=5.4.5 -TIFF_VERSION=4.6.0 -LCMS2_VERSION=2.16 -if [[ -n "$IS_MACOS" ]]; then - GIFLIB_VERSION=5.2.2 -else - GIFLIB_VERSION=5.2.1 -fi -if [[ -n "$IS_MACOS" ]] || [[ "$MB_ML_VER" != 2014 ]]; then - ZLIB_VERSION=1.3.1 -else - ZLIB_VERSION=1.2.8 -fi -LIBWEBP_VERSION=1.4.0 -BZIP2_VERSION=1.0.8 -LIBXCB_VERSION=1.17.0 -BROTLI_VERSION=1.1.0 - -if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "x86_64" ]]; then - function build_openjpeg { - local out_dir=$(fetch_unpack https://github.com/uclouvain/openjpeg/archive/v${OPENJPEG_VERSION}.tar.gz openjpeg-${OPENJPEG_VERSION}.tar.gz) - (cd $out_dir \ - && cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \ - && make install) - touch openjpeg-stamp - } -fi - -function build_brotli { - local cmake=$(get_modern_cmake) - local out_dir=$(fetch_unpack https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-1.1.0.tar.gz) - (cd $out_dir \ - && $cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \ - && make install) - if [[ "$MB_ML_LIBC" == "manylinux" ]]; then - cp /usr/local/lib64/libbrotli* /usr/local/lib - cp /usr/local/lib64/pkgconfig/libbrotli* /usr/local/lib/pkgconfig - fi -} - -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 - yum remove -y zlib-devel - fi - build_new_zlib - - 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 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 - fi - build_simple libxcb $LIBXCB_VERSION https://www.x.org/releases/individual/lib - - build_libjpeg_turbo - build_tiff - 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" - if [[ -n "$IS_MACOS" ]]; then - CFLAGS="$CFLAGS -Wl,-headerpad_max_install_names" - fi - build_libwebp - CFLAGS=$ORIGINAL_CFLAGS - - build_brotli - - if [ -n "$IS_MACOS" ]; then - # Custom freetype build - build_simple freetype $FREETYPE_VERSION https://download.savannah.gnu.org/releases/freetype tar.gz --with-harfbuzz=no - else - build_freetype - fi - - if [ -z "$IS_MACOS" ]; then - export FREETYPE_LIBS=-lfreetype - export FREETYPE_CFLAGS=-I/usr/local/include/freetype2/ - fi - build_simple harfbuzz $HARFBUZZ_VERSION https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION tar.xz --with-freetype=yes --with-glib=no - if [ -z "$IS_MACOS" ]; then - export FREETYPE_LIBS="" - export FREETYPE_CFLAGS="" - fi -} - -# 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 [[ -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 - - brew install pkg-config -fi - -wrap_wheel_builder build - -# Append licenses -for filename in wheels/dependency_licenses/*; do - echo -e "\n\n----\n\n$(basename $filename | cut -f 1 -d '.')\n" | cat >> LICENSE - cat $filename >> LICENSE -done +#!/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} +fi +export PLAT=$CIBW_ARCHS +source wheels/multibuild/common_utils.sh +source wheels/multibuild/library_builders.sh +if [ -z "$IS_MACOS" ]; then + source wheels/multibuild/manylinux_utils.sh +fi + +ARCHIVE_SDIR=pillow-depends-main + +# Package versions for fresh source builds +FREETYPE_VERSION=2.13.2 +HARFBUZZ_VERSION=8.5.0 +LIBPNG_VERSION=1.6.43 +JPEGTURBO_VERSION=3.0.3 +OPENJPEG_VERSION=2.5.2 +XZ_VERSION=5.4.5 +TIFF_VERSION=4.6.0 +LCMS2_VERSION=2.16 +if [[ -n "$IS_MACOS" ]]; then + GIFLIB_VERSION=5.2.2 +else + GIFLIB_VERSION=5.2.1 +fi +if [[ -n "$IS_MACOS" ]] || [[ "$MB_ML_VER" != 2014 ]]; then + ZLIB_VERSION=1.3.1 +else + ZLIB_VERSION=1.2.8 +fi +LIBWEBP_VERSION=1.4.0 +BZIP2_VERSION=1.0.8 +LIBXCB_VERSION=1.17.0 +BROTLI_VERSION=1.1.0 + +if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "x86_64" ]]; then + function build_openjpeg { + local out_dir=$(fetch_unpack https://github.com/uclouvain/openjpeg/archive/v${OPENJPEG_VERSION}.tar.gz openjpeg-${OPENJPEG_VERSION}.tar.gz) + (cd $out_dir \ + && cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \ + && make install) + touch openjpeg-stamp + } +fi + +function build_brotli { + local cmake=$(get_modern_cmake) + local out_dir=$(fetch_unpack https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-1.1.0.tar.gz) + (cd $out_dir \ + && $cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \ + && make install) + if [[ "$MB_ML_LIBC" == "manylinux" ]]; then + cp /usr/local/lib64/libbrotli* /usr/local/lib + cp /usr/local/lib64/pkgconfig/libbrotli* /usr/local/lib/pkgconfig + fi +} + +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 + yum remove -y zlib-devel + fi + build_new_zlib + + 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 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 + fi + build_simple libxcb $LIBXCB_VERSION https://www.x.org/releases/individual/lib + + build_libjpeg_turbo + build_tiff + 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" + if [[ -n "$IS_MACOS" ]]; then + CFLAGS="$CFLAGS -Wl,-headerpad_max_install_names" + fi + build_libwebp + CFLAGS=$ORIGINAL_CFLAGS + + build_brotli + + if [ -n "$IS_MACOS" ]; then + # Custom freetype build + build_simple freetype $FREETYPE_VERSION https://download.savannah.gnu.org/releases/freetype tar.gz --with-harfbuzz=no + else + build_freetype + fi + + if [ -z "$IS_MACOS" ]; then + export FREETYPE_LIBS=-lfreetype + export FREETYPE_CFLAGS=-I/usr/local/include/freetype2/ + fi + build_simple harfbuzz $HARFBUZZ_VERSION https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION tar.xz --with-freetype=yes --with-glib=no + if [ -z "$IS_MACOS" ]; then + export FREETYPE_LIBS="" + export FREETYPE_CFLAGS="" + fi +} + +# 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 [[ -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 + + brew install pkg-config +fi + +wrap_wheel_builder build + +# Append licenses +for filename in wheels/dependency_licenses/*; do + echo -e "\n\n----\n\n$(basename $filename | cut -f 1 -d '.')\n" | cat >> LICENSE + cat $filename >> LICENSE +done diff --git a/.github/workflows/wheels-test.ps1 b/.github/workflows/wheels-test.ps1 index f593c7228..196ef3456 100644 --- a/.github/workflows/wheels-test.ps1 +++ b/.github/workflows/wheels-test.ps1 @@ -1,22 +1,22 @@ -param ([string]$venv, [string]$pillow="C:\pillow") -$ErrorActionPreference = 'Stop' -$ProgressPreference = 'SilentlyContinue' -Set-PSDebug -Trace 1 -if ("$venv" -like "*\cibw-run-*\pp*-win_amd64\*") { - # unlike CPython, PyPy requires Visual C++ Redistributable to be installed - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 - Invoke-WebRequest -Uri 'https://aka.ms/vs/15/release/vc_redist.x64.exe' -OutFile 'vc_redist.x64.exe' - C:\vc_redist.x64.exe /install /quiet /norestart | Out-Null -} -$env:path += ";$pillow\winbuild\build\bin\" -& "$venv\Scripts\activate.ps1" -& reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\python.exe" /v "GlobalFlag" /t REG_SZ /d "0x02000000" /f -cd $pillow -& python -VV -if (!$?) { exit $LASTEXITCODE } -& python selftest.py -if (!$?) { exit $LASTEXITCODE } -& python -m pytest -vx Tests\check_wheel.py -if (!$?) { exit $LASTEXITCODE } -& python -m pytest -vx Tests -if (!$?) { exit $LASTEXITCODE } +param ([string]$venv, [string]$pillow="C:\pillow") +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'SilentlyContinue' +Set-PSDebug -Trace 1 +if ("$venv" -like "*\cibw-run-*\pp*-win_amd64\*") { + # unlike CPython, PyPy requires Visual C++ Redistributable to be installed + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + Invoke-WebRequest -Uri 'https://aka.ms/vs/15/release/vc_redist.x64.exe' -OutFile 'vc_redist.x64.exe' + C:\vc_redist.x64.exe /install /quiet /norestart | Out-Null +} +$env:path += ";$pillow\winbuild\build\bin\" +& "$venv\Scripts\activate.ps1" +& reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\python.exe" /v "GlobalFlag" /t REG_SZ /d "0x02000000" /f +cd $pillow +& python -VV +if (!$?) { exit $LASTEXITCODE } +& python selftest.py +if (!$?) { exit $LASTEXITCODE } +& python -m pytest -vx Tests\check_wheel.py +if (!$?) { exit $LASTEXITCODE } +& python -m pytest -vx Tests +if (!$?) { exit $LASTEXITCODE } diff --git a/.github/workflows/wheels-test.sh b/.github/workflows/wheels-test.sh index a3376ac92..0791f2761 100755 --- a/.github/workflows/wheels-test.sh +++ b/.github/workflows/wheels-test.sh @@ -1,28 +1,28 @@ -#!/bin/bash -set -e - -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 - fi -elif [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then - apk add curl fribidi -else - yum install -y fribidi -fi -if [ "${AUDITWHEEL_POLICY::9}" != "musllinux" ] && !([[ "$OSTYPE" == "darwin"* ]] && [[ $(python3 --version) == *"3.13."* ]]); then - python3 -m pip install numpy -fi - -if [ ! -d "test-images-main" ]; then - curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip - unzip pillow-test-images.zip - mv test-images-main/* Tests/images -fi - -# Runs tests -python3 selftest.py -python3 -m pytest Tests/check_wheel.py -python3 -m pytest +#!/bin/bash +set -e + +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 + fi +elif [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then + apk add curl fribidi +else + yum install -y fribidi +fi +if [ "${AUDITWHEEL_POLICY::9}" != "musllinux" ] && !([[ "$OSTYPE" == "darwin"* ]] && [[ $(python3 --version) == *"3.13."* ]]); then + python3 -m pip install numpy +fi + +if [ ! -d "test-images-main" ]; then + curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip + unzip pillow-test-images.zip + mv test-images-main/* Tests/images +fi + +# Runs tests +python3 selftest.py +python3 -m pytest Tests/check_wheel.py +python3 -m pytest diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 3d6099c1c..88f5a018b 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -1,270 +1,270 @@ -name: Wheels - -on: - push: - paths: - - ".ci/requirements-cibw.txt" - - ".github/workflows/wheel*" - - "setup.py" - - "wheels/*" - - "winbuild/build_prepare.py" - - "winbuild/fribidi.cmake" - tags: - - "*" - pull_request: - paths: - - ".ci/requirements-cibw.txt" - - ".github/workflows/wheel*" - - "setup.py" - - "wheels/*" - - "winbuild/build_prepare.py" - - "winbuild/fribidi.cmake" - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -env: - FORCE_COLOR: 1 - -jobs: - build-1-QEMU-emulated-wheels: - name: aarch64 ${{ matrix.python-version }} ${{ matrix.spec }} - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - python-version: - - pp39 - - pp310 - - cp38 - - cp39 - - cp310 - - cp311 - - cp312 - - cp313 - spec: - - manylinux2014 - - manylinux_2_28 - - musllinux - exclude: - - { python-version: pp39, spec: musllinux } - - { python-version: pp310, spec: musllinux } - - steps: - - uses: actions/checkout@v4 - with: - submodules: true - - - uses: actions/setup-python@v5 - with: - python-version: "3.x" - - # https://github.com/docker/setup-qemu-action - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Install cibuildwheel - run: | - python3 -m pip install -r .ci/requirements-cibw.txt - - - name: Build wheels - run: | - python3 -m cibuildwheel --output-dir wheelhouse - env: - # Build only the currently selected Linux architecture (so we can - # parallelise for speed). - CIBW_ARCHS: "aarch64" - # Likewise, select only one Python version per job to speed this up. - CIBW_BUILD: "${{ matrix.python-version }}-${{ matrix.spec == 'musllinux' && 'musllinux' || 'manylinux' }}*" - CIBW_PRERELEASE_PYTHONS: True - # Extra options for manylinux. - CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.spec }} - CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.spec }} - - - uses: actions/upload-artifact@v4 - with: - name: dist-qemu-${{ matrix.python-version }}-${{ matrix.spec }} - path: ./wheelhouse/*.whl - - build-2-native-wheels: - name: ${{ matrix.name }} - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - include: - - name: "macOS x86_64" - os: macos-13 - cibw_arch: x86_64 - macosx_deployment_target: "10.10" - - name: "macOS arm64" - os: macos-14 - cibw_arch: arm64 - macosx_deployment_target: "11.0" - - name: "manylinux2014 and musllinux x86_64" - os: ubuntu-latest - cibw_arch: x86_64 - - name: "manylinux_2_28 x86_64" - os: ubuntu-latest - cibw_arch: x86_64 - build: "*manylinux*" - manylinux: "manylinux_2_28" - steps: - - uses: actions/checkout@v4 - with: - submodules: true - - - uses: actions/setup-python@v5 - with: - python-version: "3.x" - - - name: Install cibuildwheel - run: | - python3 -m pip install -r .ci/requirements-cibw.txt - - - name: Build wheels - run: | - python3 -m cibuildwheel --output-dir wheelhouse - env: - CIBW_ARCHS: ${{ matrix.cibw_arch }} - CIBW_BUILD: ${{ matrix.build }} - CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }} - CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }} - CIBW_PRERELEASE_PYTHONS: True - CIBW_SKIP: pp38-* - CIBW_TEST_SKIP: cp38-macosx_arm64 - MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }} - - - uses: actions/upload-artifact@v4 - with: - name: dist-${{ matrix.os }}-${{ matrix.cibw_arch }}${{ matrix.manylinux && format('-{0}', matrix.manylinux) }} - path: ./wheelhouse/*.whl - - windows: - name: Windows ${{ matrix.cibw_arch }} - runs-on: windows-latest - strategy: - fail-fast: false - matrix: - include: - - cibw_arch: x86 - - cibw_arch: AMD64 - - cibw_arch: ARM64 - steps: - - uses: actions/checkout@v4 - - - name: Checkout extra test images - uses: actions/checkout@v4 - with: - repository: python-pillow/test-images - path: Tests\test-images - - - uses: actions/setup-python@v5 - with: - python-version: "3.x" - - - name: Install cibuildwheel - run: | - python.exe -m pip install -r .ci/requirements-cibw.txt - - - name: Prepare for build - run: | - choco install nasm --no-progress - echo "C:\Program Files\NASM" >> $env:GITHUB_PATH - - # Install extra test images - xcopy /S /Y Tests\test-images\* Tests\images - - & python.exe winbuild\build_prepare.py -v --no-imagequant --architecture=${{ matrix.cibw_arch }} - shell: pwsh - - - name: Build wheels - run: | - setlocal EnableDelayedExpansion - for %%f in (winbuild\build\license\*) do ( - set x=%%~nf - rem Skip FriBiDi license, it is not included in the wheel. - set fribidi=!x:~0,7! - if NOT !fribidi!==fribidi ( - rem Skip imagequant license, it is not included in the wheel. - set libimagequant=!x:~0,13! - if NOT !libimagequant!==libimagequant ( - echo. >> LICENSE - echo ===== %%~nf ===== >> LICENSE - echo. >> LICENSE - type %%f >> LICENSE - ) - ) - ) - call winbuild\\build\\build_env.cmd - %pythonLocation%\python.exe -m cibuildwheel . --output-dir wheelhouse - env: - CIBW_ARCHS: ${{ matrix.cibw_arch }} - CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd" - CIBW_CACHE_PATH: "C:\\cibw" - CIBW_PRERELEASE_PYTHONS: True - CIBW_SKIP: pp38-* - CIBW_TEST_SKIP: "*-win_arm64" - CIBW_TEST_COMMAND: 'docker run --rm - -v {project}:C:\pillow - -v C:\cibw:C:\cibw - -v %CD%\..\venv-test:%CD%\..\venv-test - -e CI -e GITHUB_ACTIONS - mcr.microsoft.com/windows/servercore:ltsc2022 - powershell C:\pillow\.github\workflows\wheels-test.ps1 %CD%\..\venv-test' - shell: cmd - - - name: Upload wheels - uses: actions/upload-artifact@v4 - with: - name: dist-windows-${{ matrix.cibw_arch }} - path: ./wheelhouse/*.whl - - - name: Upload fribidi.dll - uses: actions/upload-artifact@v4 - with: - name: fribidi-windows-${{ matrix.cibw_arch }} - path: winbuild\build\bin\fribidi* - - sdist: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.x" - cache: pip - cache-dependency-path: "Makefile" - - - run: make sdist - - - uses: actions/upload-artifact@v4 - with: - name: dist-sdist - path: dist/*.tar.gz - - pypi-publish: - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') - needs: [build-1-QEMU-emulated-wheels, build-2-native-wheels, windows, sdist] - runs-on: ubuntu-latest - name: Upload release to PyPI - environment: - name: release-pypi - url: https://pypi.org/p/Pillow - permissions: - id-token: write - steps: - - uses: actions/download-artifact@v4 - with: - pattern: dist-* - path: dist - merge-multiple: true - - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 +name: Wheels + +on: + push: + paths: + - ".ci/requirements-cibw.txt" + - ".github/workflows/wheel*" + - "setup.py" + - "wheels/*" + - "winbuild/build_prepare.py" + - "winbuild/fribidi.cmake" + tags: + - "*" + pull_request: + paths: + - ".ci/requirements-cibw.txt" + - ".github/workflows/wheel*" + - "setup.py" + - "wheels/*" + - "winbuild/build_prepare.py" + - "winbuild/fribidi.cmake" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + FORCE_COLOR: 1 + +jobs: + build-1-QEMU-emulated-wheels: + name: aarch64 ${{ matrix.python-version }} ${{ matrix.spec }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: + - pp39 + - pp310 + - cp38 + - cp39 + - cp310 + - cp311 + - cp312 + - cp313 + spec: + - manylinux2014 + - manylinux_2_28 + - musllinux + exclude: + - { python-version: pp39, spec: musllinux } + - { python-version: pp310, spec: musllinux } + + steps: + - uses: actions/checkout@v4 + with: + submodules: true + + - uses: actions/setup-python@v5 + with: + python-version: "3.x" + + # https://github.com/docker/setup-qemu-action + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Install cibuildwheel + run: | + python3 -m pip install -r .ci/requirements-cibw.txt + + - name: Build wheels + run: | + python3 -m cibuildwheel --output-dir wheelhouse + env: + # Build only the currently selected Linux architecture (so we can + # parallelise for speed). + CIBW_ARCHS: "aarch64" + # Likewise, select only one Python version per job to speed this up. + CIBW_BUILD: "${{ matrix.python-version }}-${{ matrix.spec == 'musllinux' && 'musllinux' || 'manylinux' }}*" + CIBW_PRERELEASE_PYTHONS: True + # Extra options for manylinux. + CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.spec }} + CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.spec }} + + - uses: actions/upload-artifact@v4 + with: + name: dist-qemu-${{ matrix.python-version }}-${{ matrix.spec }} + path: ./wheelhouse/*.whl + + build-2-native-wheels: + name: ${{ matrix.name }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - name: "macOS x86_64" + os: macos-13 + cibw_arch: x86_64 + macosx_deployment_target: "10.10" + - name: "macOS arm64" + os: macos-14 + cibw_arch: arm64 + macosx_deployment_target: "11.0" + - name: "manylinux2014 and musllinux x86_64" + os: ubuntu-latest + cibw_arch: x86_64 + - name: "manylinux_2_28 x86_64" + os: ubuntu-latest + cibw_arch: x86_64 + build: "*manylinux*" + manylinux: "manylinux_2_28" + steps: + - uses: actions/checkout@v4 + with: + submodules: true + + - uses: actions/setup-python@v5 + with: + python-version: "3.x" + + - name: Install cibuildwheel + run: | + python3 -m pip install -r .ci/requirements-cibw.txt + + - name: Build wheels + run: | + python3 -m cibuildwheel --output-dir wheelhouse + env: + CIBW_ARCHS: ${{ matrix.cibw_arch }} + CIBW_BUILD: ${{ matrix.build }} + CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }} + CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }} + CIBW_PRERELEASE_PYTHONS: True + CIBW_SKIP: pp38-* + CIBW_TEST_SKIP: cp38-macosx_arm64 + MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }} + + - uses: actions/upload-artifact@v4 + with: + name: dist-${{ matrix.os }}-${{ matrix.cibw_arch }}${{ matrix.manylinux && format('-{0}', matrix.manylinux) }} + path: ./wheelhouse/*.whl + + windows: + name: Windows ${{ matrix.cibw_arch }} + runs-on: windows-latest + strategy: + fail-fast: false + matrix: + include: + - cibw_arch: x86 + - cibw_arch: AMD64 + - cibw_arch: ARM64 + steps: + - uses: actions/checkout@v4 + + - name: Checkout extra test images + uses: actions/checkout@v4 + with: + repository: python-pillow/test-images + path: Tests\test-images + + - uses: actions/setup-python@v5 + with: + python-version: "3.x" + + - name: Install cibuildwheel + run: | + python.exe -m pip install -r .ci/requirements-cibw.txt + + - name: Prepare for build + run: | + choco install nasm --no-progress + echo "C:\Program Files\NASM" >> $env:GITHUB_PATH + + # Install extra test images + xcopy /S /Y Tests\test-images\* Tests\images + + & python.exe winbuild\build_prepare.py -v --no-imagequant --architecture=${{ matrix.cibw_arch }} + shell: pwsh + + - name: Build wheels + run: | + setlocal EnableDelayedExpansion + for %%f in (winbuild\build\license\*) do ( + set x=%%~nf + rem Skip FriBiDi license, it is not included in the wheel. + set fribidi=!x:~0,7! + if NOT !fribidi!==fribidi ( + rem Skip imagequant license, it is not included in the wheel. + set libimagequant=!x:~0,13! + if NOT !libimagequant!==libimagequant ( + echo. >> LICENSE + echo ===== %%~nf ===== >> LICENSE + echo. >> LICENSE + type %%f >> LICENSE + ) + ) + ) + call winbuild\\build\\build_env.cmd + %pythonLocation%\python.exe -m cibuildwheel . --output-dir wheelhouse + env: + CIBW_ARCHS: ${{ matrix.cibw_arch }} + CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd" + CIBW_CACHE_PATH: "C:\\cibw" + CIBW_PRERELEASE_PYTHONS: True + CIBW_SKIP: pp38-* + CIBW_TEST_SKIP: "*-win_arm64" + CIBW_TEST_COMMAND: 'docker run --rm + -v {project}:C:\pillow + -v C:\cibw:C:\cibw + -v %CD%\..\venv-test:%CD%\..\venv-test + -e CI -e GITHUB_ACTIONS + mcr.microsoft.com/windows/servercore:ltsc2022 + powershell C:\pillow\.github\workflows\wheels-test.ps1 %CD%\..\venv-test' + shell: cmd + + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: dist-windows-${{ matrix.cibw_arch }} + path: ./wheelhouse/*.whl + + - name: Upload fribidi.dll + uses: actions/upload-artifact@v4 + with: + name: fribidi-windows-${{ matrix.cibw_arch }} + path: winbuild\build\bin\fribidi* + + sdist: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + cache: pip + cache-dependency-path: "Makefile" + + - run: make sdist + + - uses: actions/upload-artifact@v4 + with: + name: dist-sdist + path: dist/*.tar.gz + + pypi-publish: + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') + needs: [build-1-QEMU-emulated-wheels, build-2-native-wheels, windows, sdist] + runs-on: ubuntu-latest + name: Upload release to PyPI + environment: + name: release-pypi + url: https://pypi.org/p/Pillow + permissions: + id-token: write + steps: + - uses: actions/download-artifact@v4 + with: + pattern: dist-* + path: dist + merge-multiple: true + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.gitignore b/.gitignore index 1dd6c9175..d9eebaa5f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,94 +1,94 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] - -# C extensions -*.so - -# Distribution / packaging -.eggs/ -.Python -env/ -bin/ -build/ -develop-eggs/ -dist/ -eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -*.egg-info/ -.installed.cfg -*.egg - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.coverage -.cache -.pytest_cache -coverage.xml - -# Test files -test_images - -# Translations -*.mo - -# Mr Developer -.mr.developer.cfg -.project -.pydevproject - -# Rope -.ropeproject - -# Django stuff: -*.log -*.pot - -# Sphinx documentation -docs/_build/ - -# viewdoc output -.long-description.html - -# Vim cruft -.*.swp - -#emacs -*~ -\#*# -.#* - -#VS Code -.vscode - -#Komodo -*.komodoproject - -#OS -.DS_Store - -# JetBrains -.idea - -# Extra test images installed from python-pillow/test-images -Tests/images/README.md -Tests/images/crash_1.tif -Tests/images/crash_2.tif -Tests/images/crash-81154a65438ba5aaeca73fd502fa4850fbde60f8.tif -Tests/images/string_dimension.tiff -Tests/images/jpeg2000 -Tests/images/msp -Tests/images/picins -Tests/images/sunraster - -# pyinstaller -*.spec +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.eggs/ +.Python +env/ +bin/ +build/ +develop-eggs/ +dist/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +.pytest_cache +coverage.xml + +# Test files +test_images + +# Translations +*.mo + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# Rope +.ropeproject + +# Django stuff: +*.log +*.pot + +# Sphinx documentation +docs/_build/ + +# viewdoc output +.long-description.html + +# Vim cruft +.*.swp + +#emacs +*~ +\#*# +.#* + +#VS Code +.vscode + +#Komodo +*.komodoproject + +#OS +.DS_Store + +# JetBrains +.idea + +# Extra test images installed from python-pillow/test-images +Tests/images/README.md +Tests/images/crash_1.tif +Tests/images/crash_2.tif +Tests/images/crash-81154a65438ba5aaeca73fd502fa4850fbde60f8.tif +Tests/images/string_dimension.tiff +Tests/images/jpeg2000 +Tests/images/msp +Tests/images/picins +Tests/images/sunraster + +# pyinstaller +*.spec diff --git a/.gitmodules b/.gitmodules index 80d5ab16c..e01d777eb 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ -[submodule "multibuild"] - path = wheels/multibuild - url = https://github.com/multi-build/multibuild.git +[submodule "multibuild"] + path = wheels/multibuild + url = https://github.com/multi-build/multibuild.git diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6a76e8c00..9cc863c4c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,85 +1,85 @@ -repos: - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.4.7 - hooks: - - id: ruff - args: [--exit-non-zero-on-fix] - - - repo: https://github.com/psf/black-pre-commit-mirror - rev: 24.4.2 - hooks: - - id: black - - - repo: https://github.com/PyCQA/bandit - rev: 1.7.8 - hooks: - - id: bandit - args: [--severity-level=high] - files: ^src/ - - - repo: https://github.com/Lucas-C/pre-commit-hooks - rev: v1.5.5 - hooks: - - id: remove-tabs - exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) - - - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v18.1.5 - hooks: - - id: clang-format - types: [c] - exclude: ^src/thirdparty/ - - - repo: https://github.com/pre-commit/pygrep-hooks - rev: v1.10.0 - hooks: - - id: rst-backticks - - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 - hooks: - - id: check-executables-have-shebangs - - id: check-shebang-scripts-are-executable - - id: check-merge-conflict - - id: check-json - - id: check-toml - - id: check-yaml - - 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.28.4 - hooks: - - id: check-github-workflows - - id: check-readthedocs - - id: check-renovate - - - repo: https://github.com/sphinx-contrib/sphinx-lint - rev: v0.9.1 - hooks: - - id: sphinx-lint - - - repo: https://github.com/tox-dev/pyproject-fmt - rev: 1.8.0 - hooks: - - id: pyproject-fmt - - - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.18 - hooks: - - id: validate-pyproject - - - repo: https://github.com/tox-dev/tox-ini-fmt - rev: 1.3.1 - hooks: - - id: tox-ini-fmt - - - repo: meta - hooks: - - id: check-hooks-apply - - id: check-useless-excludes - -ci: - autoupdate_schedule: monthly +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.4.7 + hooks: + - id: ruff + args: [--exit-non-zero-on-fix] + + - repo: https://github.com/psf/black-pre-commit-mirror + rev: 24.4.2 + hooks: + - id: black + + - repo: https://github.com/PyCQA/bandit + rev: 1.7.8 + hooks: + - id: bandit + args: [--severity-level=high] + files: ^src/ + + - repo: https://github.com/Lucas-C/pre-commit-hooks + rev: v1.5.5 + hooks: + - id: remove-tabs + exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) + + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: v18.1.5 + hooks: + - id: clang-format + types: [c] + exclude: ^src/thirdparty/ + + - repo: https://github.com/pre-commit/pygrep-hooks + rev: v1.10.0 + hooks: + - id: rst-backticks + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: check-executables-have-shebangs + - id: check-shebang-scripts-are-executable + - id: check-merge-conflict + - id: check-json + - id: check-toml + - id: check-yaml + - 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.28.4 + hooks: + - id: check-github-workflows + - id: check-readthedocs + - id: check-renovate + + - repo: https://github.com/sphinx-contrib/sphinx-lint + rev: v0.9.1 + hooks: + - id: sphinx-lint + + - repo: https://github.com/tox-dev/pyproject-fmt + rev: 1.8.0 + hooks: + - id: pyproject-fmt + + - repo: https://github.com/abravalheri/validate-pyproject + rev: v0.18 + hooks: + - id: validate-pyproject + + - repo: https://github.com/tox-dev/tox-ini-fmt + rev: 1.3.1 + hooks: + - id: tox-ini-fmt + + - repo: meta + hooks: + - id: check-hooks-apply + - id: check-useless-excludes + +ci: + autoupdate_schedule: monthly diff --git a/.readthedocs.yml b/.readthedocs.yml index def6282dd..f255f1434 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,19 +1,19 @@ -version: 2 - -formats: [pdf] - -build: - os: ubuntu-lts-latest - tools: - python: "3" - jobs: - post_checkout: - - git remote add upstream https://github.com/python-pillow/Pillow.git # For forks - - git fetch upstream --tags - -python: - install: - - method: pip - path: . - extra_requirements: - - docs +version: 2 + +formats: [pdf] + +build: + os: ubuntu-lts-latest + tools: + python: "3" + jobs: + post_checkout: + - git remote add upstream https://github.com/python-pillow/Pillow.git # For forks + - git fetch upstream --tags + +python: + install: + - method: pip + path: . + extra_requirements: + - docs diff --git a/CHANGES.rst b/CHANGES.rst index d7231ebea..646257257 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,7648 +1,7648 @@ - -Changelog (Pillow) -================== - -10.4.0 (unreleased) -------------------- - -- Accept 't' suffix for libtiff version #8126, #8129 - [radarhere] - -- Deprecate ImageDraw.getdraw hints parameter #8124 - [radarhere, hugovk] - -- Added ImageDraw circle() #8085 - [void4, hugovk, radarhere] - -- Add mypy target to Makefile #8077 - [Yay295] - -- Added more modes to Image.MODES #7984 - [radarhere] - -- Deprecate BGR;15, BGR;16 and BGR;24 modes #7978 - [radarhere, hugovk] - -- Fix ImagingAccess for I;16N on big-endian #7921 - [Yay295, radarhere] - -- Support reading P mode TIFF images with padding #7996 - [radarhere] - -- Deprecate support for libtiff < 4 #7998 - [radarhere, hugovk] - -- Corrected ImageShow UnixViewer command #7987 - [radarhere] - -- Use functools.cached_property in ImageStat #7952 - [nulano, hugovk, radarhere] - -- Add support for reading BITMAPV2INFOHEADER and BITMAPV3INFOHEADER #7956 - [Cirras, radarhere] - -- Support reading CMYK JPEG2000 images #7947 - [radarhere] - -10.3.0 (2024-04-01) -------------------- - -- CVE-2024-28219: Use ``strncpy`` to avoid buffer overflow #7928 - [radarhere, hugovk] - -- Deprecate ``eval()``, replacing it with ``lambda_eval()`` and ``unsafe_eval()`` #7927 - [radarhere, hugovk] - -- Raise ``ValueError`` if seeking to greater than offset-sized integer in TIFF #7883 - [radarhere] - -- Add ``--report`` argument to ``__main__.py`` to omit supported formats #7818 - [nulano, radarhere, hugovk] - -- Added RGB to I;16, I;16L, I;16B and I;16N conversion #7918, #7920 - [radarhere] - -- Fix editable installation with custom build backend and configuration options #7658 - [nulano, radarhere] - -- Fix putdata() for I;16N on big-endian #7209 - [Yay295, hugovk, radarhere] - -- Determine MPO size from markers, not EXIF data #7884 - [radarhere] - -- Improved conversion from RGB to RGBa, LA and La #7888 - [radarhere] - -- Support FITS images with GZIP_1 compression #7894 - [radarhere] - -- Use I;16 mode for 9-bit JPEG 2000 images #7900 - [scaramallion, radarhere] - -- Raise ValueError if kmeans is negative #7891 - [radarhere] - -- Remove TIFF tag OSUBFILETYPE when saving using libtiff #7893 - [radarhere] - -- Raise ValueError for negative values when loading P1-P3 PPM images #7882 - [radarhere] - -- Added reading of JPEG2000 palettes #7870 - [radarhere] - -- Added alpha_quality argument when saving WebP images #7872 - [radarhere] - -- Fixed joined corners for ImageDraw rounded_rectangle() non-integer dimensions #7881 - [radarhere] - -- Stop reading EPS image at EOF marker #7753 - [radarhere] - -- PSD layer co-ordinates may be negative #7706 - [radarhere] - -- Use subprocess with CREATE_NO_WINDOW flag in ImageShow WindowsViewer #7791 - [radarhere] - -- When saving GIF frame that restores to background color, do not fill identical pixels #7788 - [radarhere] - -- Fixed reading PNG iCCP compression method #7823 - [radarhere] - -- Allow writing IFDRational to UNDEFINED tag #7840 - [radarhere] - -- Fix logged tag name when loading Exif data #7842 - [radarhere] - -- Use maximum frame size in IHDR chunk when saving APNG images #7821 - [radarhere] - -- Prevent opening P TGA images without a palette #7797 - [radarhere] - -- Use palette when loading ICO images #7798 - [radarhere] - -- Use consistent arguments for load_read and load_seek #7713 - [radarhere] - -- Turn off nullability warnings for macOS SDK #7827 - [radarhere] - -- Fix shift-sign issue in Convert.c #7838 - [r-barnes, radarhere] - -- Open 16-bit grayscale PNGs as I;16 #7849 - [radarhere] - -- Handle truncated chunks at the end of PNG images #7709 - [lajiyuan, radarhere] - -- Match mask size to pasted image size in GifImagePlugin #7779 - [radarhere] - -- Release GIL while calling ``WebPAnimDecoderGetNext`` #7782 - [evanmiller, radarhere] - -- Fixed reading FLI/FLC images with a prefix chunk #7804 - [twolife] - -- Update wl-paste handling and return None for some errors in grabclipboard() on Linux #7745 - [nik012003, radarhere] - -- Remove execute bit from ``setup.py`` #7760 - [hugovk] - -- Do not support using test-image-results to upload images after test failures #7739 - [radarhere] - -- Changed ImageMath.ops to be static #7721 - [radarhere] - -- Fix APNG info after seeking backwards more than twice #7701 - [esoma, radarhere] - -- Deprecate ImageCms constants and versions() function #7702 - [nulano, radarhere] - -- Added PerspectiveTransform #7699 - [radarhere] - -- Add support for reading and writing grayscale PFM images #7696 - [nulano, hugovk] - -- Add LCMS2 flags to ImageCms #7676 - [nulano, radarhere, hugovk] - -- Rename x64 to AMD64 in winbuild #7693 - [nulano] - -10.2.0 (2024-01-02) -------------------- - -- Add ``keep_rgb`` option when saving JPEG to prevent conversion of RGB colorspace #7553 - [bgilbert, radarhere] - -- Trim glyph size in ImageFont.getmask() #7669, #7672 - [radarhere, nulano] - -- Deprecate IptcImagePlugin helpers #7664 - [nulano, hugovk, radarhere] - -- Allow uncompressed TIFF images to be saved in chunks #7650 - [radarhere] - -- Concatenate multiple JPEG EXIF markers #7496 - [radarhere] - -- Changed IPTC tile tuple to match other plugins #7661 - [radarhere] - -- Do not assign new fp attribute when exiting context manager #7566 - [radarhere] - -- Support arbitrary masks for uncompressed RGB DDS images #7589 - [radarhere, akx] - -- Support setting ROWSPERSTRIP tag #7654 - [radarhere] - -- Apply ImageFont.MAX_STRING_LENGTH to ImageFont.getmask() #7662 - [radarhere] - -- Optimise ``ImageColor`` using ``functools.lru_cache`` #7657 - [hugovk] - -- Restricted environment keys for ImageMath.eval() #7655 - [wiredfool, radarhere] - -- Optimise ``ImageMode.getmode`` using ``functools.lru_cache`` #7641 - [hugovk, radarhere] - -- Fix incorrect color blending for overlapping glyphs #7497 - [ZachNagengast, nulano, radarhere] - -- Attempt memory mapping when tile args is a string #7565 - [radarhere] - -- Fill identical pixels with transparency in subsequent frames when saving GIF #7568 - [radarhere] - -- Corrected duration when combining multiple GIF frames into single frame #7521 - [radarhere] - -- Handle disposing GIF background from outside palette #7515 - [radarhere] - -- Seek past the data when skipping a PSD layer #7483 - [radarhere] - -- Import plugins relative to the module #7576 - [deliangyang, jaxx0n] - -- Translate encoder error codes to strings; deprecate ``ImageFile.raise_oserror()`` #7609 - [bgilbert, radarhere] - -- Support reading BC4U and DX10 BC1 images #6486 - [REDxEYE, radarhere, hugovk] - -- Optimize ImageStat.Stat.extrema #7593 - [florath, radarhere] - -- Handle pathlib.Path in FreeTypeFont #7578 - [radarhere, hugovk, nulano] - -- Added support for reading DX10 BC4 DDS images #7603 - [sambvfx, radarhere] - -- Optimized ImageStat.Stat.count #7599 - [florath] - -- Correct PDF palette size when saving #7555 - [radarhere] - -- Fixed closing file pointer with olefile 0.47 #7594 - [radarhere] - -- Raise ValueError when TrueType font size is not greater than zero #7584, #7587 - [akx, radarhere] - -- If absent, do not try to close fp when closing image #7557 - [RaphaelVRossi, radarhere] - -- Allow configuring JPEG restart marker interval on save #7488 - [bgilbert, radarhere] - -- Decrement reference count for PyObject #7549 - [radarhere] - -- Implement ``streamtype=1`` option for tables-only JPEG encoding #7491 - [bgilbert, radarhere] - -- If save_all PNG only has one frame, do not create animated image #7522 - [radarhere] - -- Fixed frombytes() for images with a zero dimension #7493 - [radarhere] - -10.1.0 (2023-10-15) -------------------- - -- Added TrueType default font to allow for different sizes #7354 - [radarhere] - -- Fixed invalid argument warning #7442 - [radarhere] - -- Added ImageOps cover method #7412 - [radarhere, hugovk] - -- Catch struct.error from truncated EXIF when reading JPEG DPI #7458 - [radarhere] - -- Consider default image when selecting mode for PNG save_all #7437 - [radarhere] - -- Support BGR;15, BGR;16 and BGR;24 access, unpacking and putdata #7303 - [radarhere] - -- Added CMYK to RGB unpacker #7310 - [radarhere] - -- Improved flexibility of XMP parsing #7274 - [radarhere] - -- Support reading 8-bit YCbCr TIFF images #7415 - [radarhere] - -- Allow saving I;16B images as PNG #7302 - [radarhere] - -- Corrected drawing I;16 points and writing I;16 text #7257 - [radarhere] - -- Set blue channel to 128 for BC5S #7413 - [radarhere] - -- Increase flexibility when reading IPTC fields #7319 - [radarhere] - -- Set C palette to be empty by default #7289 - [radarhere] - -- Added gs_binary to control Ghostscript use on all platforms #7392 - [radarhere] - -- Read bounding box information from the trailer of EPS files if specified #7382 - [nopperl, radarhere] - -- Added reading 8-bit color DDS images #7426 - [radarhere] - -- Added has_transparency_data #7420 - [radarhere, hugovk] - -- Fixed bug when reading BC5S DDS images #7401 - [radarhere] - -- Prevent TIFF orientation from being applied more than once #7383 - [radarhere] - -- Use previous pixel alpha for QOI_OP_RGB #7357 - [radarhere] - -- Added BC5U reading #7358 - [radarhere] - -- Allow getpixel() to accept a list #7355 - [radarhere, homm] - -- Allow GaussianBlur and BoxBlur to accept a sequence of x and y radii #7336 - [radarhere] - -- Expand JPEG buffer size when saving optimized or progressive #7345 - [radarhere] - -- Added session type check for Linux in ImageGrab.grabclipboard() #7332 - [TheNooB2706, radarhere, hugovk] - -- Allow "loop=None" when saving GIF images #7329 - [radarhere] - -- Fixed transparency when saving P mode images to PDF #7323 - [radarhere] - -- Added saving LA images as PDFs #7299 - [radarhere] - -- Set SMaskInData to 1 for PDFs with alpha #7316, #7317 - [radarhere] - -- Changed Image mode property to be read-only by default #7307 - [radarhere] - -- Silence exceptions in _repr_jpeg_ and _repr_png_ #7266 - [mtreinish, radarhere] - -- Do not use transparency when saving GIF if it has been removed when normalizing mode #7284 - [radarhere] - -- Fix missing symbols when libtiff depends on libjpeg #7270 - [heitbaum] - -10.0.1 (2023-09-15) -------------------- - -- Updated libwebp to 1.3.2 #7395 - [radarhere] - -- Updated zlib to 1.3 #7344 - [radarhere] - -10.0.0 (2023-07-01) -------------------- - -- Fixed deallocating mask images #7246 - [radarhere] - -- Added ImageFont.MAX_STRING_LENGTH #7244 - [radarhere, hugovk] - -- Fix Windows build with pyproject.toml #7230 - [hugovk, nulano, radarhere] - -- Do not close provided file handles with libtiff #7199 - [radarhere] - -- Convert to HSV if mode is HSV in getcolor() #7226 - [radarhere] - -- Added alpha_only argument to getbbox() #7123 - [radarhere. hugovk] - -- Prioritise speed in _repr_png_ #7242 - [radarhere] - -- Do not use CFFI access by default on PyPy #7236 - [radarhere] - -- Limit size even if one dimension is zero in decompression bomb check #7235 - [radarhere] - -- Use --config-settings instead of deprecated --global-option #7171 - [radarhere] - -- Better C integer definitions #6645 - [Yay295, hugovk] - -- Fixed finding dependencies on Cygwin #7175 - [radarhere] - -- Changed grabclipboard() to use PNG instead of JPG compression on macOS #7219 - [abey79, radarhere] - -- Added in_place argument to ImageOps.exif_transpose() #7092 - [radarhere] - -- Fixed calling putpalette() on L and LA images before load() #7187 - [radarhere] - -- Fixed saving TIFF multiframe images with LONG8 tag types #7078 - [radarhere] - -- Fixed combining single duration across duplicate APNG frames #7146 - [radarhere] - -- Remove temporary file when error is raised #7148 - [radarhere] - -- Do not use temporary file when grabbing clipboard on Linux #7200 - [radarhere] - -- If the clipboard fails to open on Windows, wait and try again #7141 - [radarhere] - -- Fixed saving multiple 1 mode frames to GIF #7181 - [radarhere] - -- Replaced absolute PIL import with relative import #7173 - [radarhere] - -- Replaced deprecated Py_FileSystemDefaultEncoding for Python >= 3.12 #7192 - [radarhere] - -- Improved wl-paste mimetype handling in ImageGrab #7094 - [rrcgat, radarhere] - -- Added _repr_jpeg_() for IPython display_jpeg #7135 - [n3011, radarhere, nulano] - -- Use "/sbin/ldconfig" if ldconfig is not found #7068 - [radarhere] - -- Prefer screenshots using XCB over gnome-screenshot #7143 - [nulano, radarhere] - -- Fixed joined corners for ImageDraw rounded_rectangle() odd dimensions #7151 - [radarhere] - -- Support reading signed 8-bit TIFF images #7111 - [radarhere] - -- Added width argument to ImageDraw regular_polygon #7132 - [radarhere] - -- Support I mode for ImageFilter.BuiltinFilter #7108 - [radarhere] - -- Raise error from stderr of Linux ImageGrab.grabclipboard() command #7112 - [radarhere] - -- Added unpacker from I;16B to I;16 #7125 - [radarhere] - -- Support float font sizes #7107 - [radarhere] - -- Use later value for duplicate xref entries in PdfParser #7102 - [radarhere] - -- Load before getting size in __getstate__ #7105 - [bigcat88, radarhere] - -- Fixed type handling for include and lib directories #7069 - [adisbladis, radarhere] - -- Remove deprecations for Pillow 10.0.0 #7059, #7080 - [hugovk, radarhere] - -- Drop support for soon-EOL Python 3.7 #7058 - [hugovk, radarhere] - -9.5.0 (2023-04-01) ------------------- - -- Added ImageSourceData to TAGS_V2 #7053 - [radarhere] - -- Clear PPM half token after use #7052 - [radarhere] - -- Removed absolute path to ldconfig #7044 - [radarhere] - -- Support custom comments and PLT markers when saving JPEG2000 images #6903 - [joshware, radarhere, hugovk] - -- Load before getting size in __array_interface__ #7034 - [radarhere] - -- Support creating BGR;15, BGR;16 and BGR;24 images, but drop support for BGR;32 #7010 - [radarhere] - -- Consider transparency when applying APNG blend mask #7018 - [radarhere] - -- Round duration when saving animated WebP images #6996 - [radarhere] - -- Added reading of JPEG2000 comments #6909 - [radarhere] - -- Decrement reference count #7003 - [radarhere, nulano] - -- Allow libtiff_support_custom_tags to be missing #7020 - [radarhere] - -- Improved I;16N support #6834 - [radarhere] - -- Added QOI reading #6852 - [radarhere, hugovk] - -- Added saving RGBA images as PDFs #6925 - [radarhere] - -- Do not raise an error if os.environ does not contain PATH #6935 - [radarhere, hugovk] - -- Close OleFileIO instance when closing or exiting FPX or MIC #7005 - [radarhere] - -- Added __int__ to IFDRational for Python >= 3.11 #6998 - [radarhere] - -- Added memoryview support to Dib.frombytes() #6988 - [radarhere, nulano] - -- Close file pointer copy in the libtiff encoder if still open #6986 - [fcarron, radarhere] - -- Raise an error if ImageDraw co-ordinates are incorrectly ordered #6978 - [radarhere] - -- Added "corners" argument to ImageDraw rounded_rectangle() #6954 - [radarhere] - -- Added memoryview support to frombytes() #6974 - [radarhere] - -- Allow comments in FITS images #6973 - [radarhere] - -- Support saving PDF with different X and Y resolutions #6961 - [jvanderneutstulen, radarhere, hugovk] - -- Fixed writing int as UNDEFINED tag #6950 - [radarhere] - -- Raise an error if EXIF data is too long when saving JPEG #6939 - [radarhere] - -- Handle more than one directory returned by pkg-config #6896 - [sebastic, radarhere] - -- Do not retry past formats when loading all formats for the first time #6902 - [radarhere] - -- Do not retry specified formats if they failed when opening #6893 - [radarhere] - -- Do not unintentionally load TIFF format at first #6892 - [radarhere] - -- Stop reading when EPS line becomes too long #6897 - [radarhere] - -- Allow writing IFDRational to BYTE tag #6890 - [radarhere] - -- Raise ValueError for BoxBlur filter with negative radius #6874 - [hugovk, radarhere] - -- Support arbitrary number of loaded modules on Windows #6761 - [javidcf, radarhere, nulano] - -9.4.0 (2023-01-02) ------------------- - -- Fixed null pointer dereference crash with malformed font #6846 - [wiredfool, radarhere] - -- Return from ImagingFill early if image has a zero dimension #6842 - [radarhere] - -- Reversed deprecations for Image constants, except for duplicate Resampling attributes #6830 - [radarhere] - -- Improve exception traceback readability #6836 - [hugovk, radarhere] - -- Do not attempt to read IFD1 if absent #6840 - [radarhere] - -- Fixed writing int as ASCII tag #6800 - [radarhere] - -- If available, use wl-paste or xclip for grabclipboard() on Linux #6783 - [radarhere] - -- Added signed option when saving JPEG2000 images #6709 - [radarhere] - -- Patch OpenJPEG to include ARM64 fix #6718 - [radarhere] - -- Added support for I;16 modes in putdata() #6825 - [radarhere] - -- Added conversion from RGBa to RGB #6708 - [radarhere] - -- Added DDS support for uncompressed L and LA images #6820 - [radarhere, REDxEYE] - -- Added LightSource tag values to ExifTags #6749 - [radarhere] - -- Fixed PyAccess after changing ICO size #6821 - [radarhere] - -- Do not use EXIF from info when saving PNG images #6819 - [radarhere] - -- Fixed saving EXIF data to MPO #6817 - [radarhere] - -- Added Exif hide_offsets() #6762 - [radarhere] - -- Only compare to previous frame when checking for duplicate GIF frames while saving #6787 - [radarhere] - -- Always initialize all plugins in registered_extensions() #6811 - [radarhere] - -- Ignore non-opaque WebP background when saving as GIF #6792 - [radarhere] - -- Only set tile in ImageFile __setstate__ #6793 - [radarhere] - -- When reading BLP, do not trust JPEG decoder to determine image is CMYK #6767 - [radarhere] - -- Added IFD enum to ExifTags #6748 - [radarhere] - -- Fixed bug combining GIF frame durations #6779 - [radarhere] - -- Support saving JPEG comments #6774 - [smason, radarhere] - -- Added getxmp() to WebPImagePlugin #6758 - [radarhere] - -- Added "exact" option when saving WebP #6747 - [ashafaei, radarhere] - -- Use fractional coordinates when drawing text #6722 - [radarhere] - -- Fixed writing int as BYTE tag #6740 - [radarhere] - -- Added MP Format Version when saving MPO #6735 - [radarhere] - -- Added Interop to ExifTags #6724 - [radarhere] - -- CVE-2007-4559 patch when building on Windows #6704 - [TrellixVulnTeam, nulano, radarhere] - -- Fix compiler warning: accessing 64 bytes in a region of size 48 #6714 - [wiredfool] - -- Use verbose flag for pip install #6713 - [wiredfool, radarhere] - -9.3.0 (2022-10-29) ------------------- - -- Limit SAMPLESPERPIXEL to avoid runtime DOS #6700 - [wiredfool] - -- Initialize libtiff buffer when saving #6699 - [radarhere] - -- Inline fname2char to fix memory leak #6329 - [nulano] - -- Fix memory leaks related to text features #6330 - [nulano] - -- Use double quotes for version check on old CPython on Windows #6695 - [hugovk] - -- Remove backup implementation of Round for Windows platforms #6693 - [cgohlke] - -- Fixed set_variation_by_name offset #6445 - [radarhere] - -- Fix malloc in _imagingft.c:font_setvaraxes #6690 - [cgohlke] - -- Release Python GIL when converting images using matrix operations #6418 - [hmaarrfk] - -- Added ExifTags enums #6630 - [radarhere] - -- Do not modify previous frame when calculating delta in PNG #6683 - [radarhere] - -- Added support for reading BMP images with RLE4 compression #6674 - [npjg, radarhere] - -- Decode JPEG compressed BLP1 data in original mode #6678 - [radarhere] - -- Added GPS TIFF tag info #6661 - [radarhere] - -- Added conversion between RGB/RGBA/RGBX and LAB #6647 - [radarhere] - -- Do not attempt normalization if mode is already normal #6644 - [radarhere] - -- Fixed seeking to an L frame in a GIF #6576 - [radarhere] - -- Consider all frames when selecting mode for PNG save_all #6610 - [radarhere] - -- Don't reassign crc on ChunkStream close #6627 - [wiredfool, radarhere] - -- Raise a warning if NumPy failed to raise an error during conversion #6594 - [radarhere] - -- Show all frames in ImageShow #6611 - [radarhere] - -- Allow FLI palette chunk to not be first #6626 - [radarhere] - -- If first GIF frame has transparency for RGB_ALWAYS loading strategy, use RGBA mode #6592 - [radarhere] - -- Round box position to integer when pasting embedded color #6517 - [radarhere, nulano] - -- Removed EXIF prefix when saving WebP #6582 - [radarhere] - -- Pad IM palette to 768 bytes when saving #6579 - [radarhere] - -- Added DDS BC6H reading #6449 - [ShadelessFox, REDxEYE, radarhere] - -- Added support for opening WhiteIsZero 16-bit integer TIFF images #6642 - [JayWiz, radarhere] - -- Raise an error when allocating translucent color to RGB palette #6654 - [jsbueno, radarhere] - -- Added reading of TIFF child images #6569 - [radarhere] - -- Improved ImageOps palette handling #6596 - [PososikTeam, radarhere] - -- Defer parsing of palette into colors #6567 - [radarhere] - -- Apply transparency to P images in ImageTk.PhotoImage #6559 - [radarhere] - -- Use rounding in ImageOps contain() and pad() #6522 - [bibinhashley, radarhere] - -- Fixed GIF remapping to palette with duplicate entries #6548 - [radarhere] - -- Allow remap_palette() to return an image with less than 256 palette entries #6543 - [radarhere] - -- Corrected BMP and TGA palette size when saving #6500 - [radarhere] - -- Do not call load() before draft() in Image.thumbnail #6539 - [radarhere] - -- Copy palette when converting from P to PA #6497 - [radarhere] - -- Allow RGB and RGBA values for PA image putpixel #6504 - [radarhere] - -- Removed support for tkinter in PyPy before Python 3.6 #6551 - [nulano] - -- Do not use CCITTFaxDecode filter if libtiff is not available #6518 - [radarhere] - -- Fallback to not using mmap if buffer is not large enough #6510 - [radarhere] - -- Fixed writing bytes as ASCII tag #6493 - [radarhere] - -- Open 1 bit EPS in mode 1 #6499 - [radarhere] - -- Removed support for tkinter before Python 1.5.2 #6549 - [radarhere] - -- Allow default ImageDraw font to be set #6484 - [radarhere, hugovk] - -- Save 1 mode PDF using CCITTFaxDecode filter #6470 - [radarhere] - -- Added support for RGBA PSD images #6481 - [radarhere] - -- Parse orientation from XMP tag contents #6463 - [bigcat88, radarhere] - -- Added support for reading ATI1/ATI2 (BC4/BC5) DDS images #6457 - [REDxEYE, radarhere] - -- Do not clear GIF tile when checking number of frames #6455 - [radarhere] - -- Support saving multiple MPO frames #6444 - [radarhere] - -- Do not double quote Pillow version for setuptools >= 60 #6450 - [radarhere] - -- Added ABGR BMP mask mode #6436 - [radarhere] - -- Fixed PSDraw rectangle #6429 - [radarhere] - -- Raise ValueError if PNG sRGB chunk is truncated #6431 - [radarhere] - -- Handle missing Python executable in ImageShow on macOS #6416 - [bryant1410, radarhere] - -9.2.0 (2022-07-01) ------------------- - -- Deprecate ImageFont.getsize and related functions #6381 - [nulano, radarhere] - -- Fixed null check for fribidi_version_info in FriBiDi shim #6376 - [nulano] - -- Added GIF decompression bomb check #6402 - [radarhere] - -- Handle PCF fonts files with less than 256 characters #6386 - [dawidcrivelli, radarhere] - -- Improved GIF optimize condition #6378 - [raygard, radarhere] - -- Reverted to __array_interface__ with the release of NumPy 1.23 #6394 - [radarhere] - -- Pad PCX palette to 768 bytes when saving #6391 - [radarhere] - -- Fixed bug with rounding pixels to palette colors #6377 - [btrekkie, radarhere] - -- Use gnome-screenshot on Linux if available #6361 - [radarhere, nulano] - -- Fixed loading L mode BMP RLE8 images #6384 - [radarhere] - -- Fixed incorrect operator in ImageCms error #6370 - [LostBenjamin, hugovk, radarhere] - -- Limit FPX tile size to avoid extending outside image #6368 - [radarhere] - -- Added support for decoding plain PPM formats #5242 - [Piolie, radarhere] - -- Added apply_transparency() #6352 - [radarhere] - -- Fixed behaviour change from endian fix #6197 - [radarhere] - -- Allow remapping P images with RGBA palettes #6350 - [radarhere] - -- Fixed drawing translucent 1px high polygons #6278 - [radarhere] - -- Pad COLORMAP to 768 items when saving TIFF #6232 - [radarhere] - -- Fix P -> PA conversion #6337 - [RedShy, radarhere] - -- Once exif data is parsed, do not reload unless it changes #6335 - [radarhere] - -- Only try to connect discontiguous corners at the end of edges #6303 - [radarhere] - -- Improve transparency handling when saving GIF images #6176 - [radarhere] - -- Do not update GIF frame position until local image is found #6219 - [radarhere] - -- Netscape GIF extension belongs after the global color table #6211 - [radarhere] - -- Only write GIF comments at the beginning of the file #6300 - [raygard, radarhere] - -- Separate multiple GIF comment blocks with newlines #6294 - [raygard, radarhere] - -- Always use GIF89a for comments #6292 - [raygard, radarhere] - -- Ignore compression value from BMP info dictionary when saving as TIFF #6231 - [radarhere] - -- If font is file-like object, do not re-read from object to get variant #6234 - [radarhere] - -- Raise ValueError when trying to access internal fp after close #6213 - [radarhere] - -- Support more affine expression forms in im.point() #6254 - [benrg, radarhere] - -- Populate Python palette in fromarray() #6283 - [radarhere] - -- Raise ValueError if PNG chunks are truncated #6253 - [radarhere] - -- Use durations from each frame by default when saving GIFs #6265 - [radarhere] - -- Adjust BITSPERSAMPLE to match SAMPLESPERPIXEL when opening TIFFs #6270 - [radarhere] - -- Search pkgconf system libs/cflags #6138 - [jameshilliard, radarhere] - -- Raise ValueError for invalid PPM maxval #6242 - [radarhere] - -- Corrected screencapture argument in ImageGrab.grab() #6244 - [axt-one] - -- Deprecate support for Qt 5 (PyQt5 and PySide2) #6237 - [hugovk, radarhere] - -- Increase wait time of temporary file deletion on Windows #6224 - [AlexTedeschi] - -- Deprecate FreeTypeFont.getmask2 fill parameter #6220 - [nulano, radarhere, hugovk] - -- Round lut values where necessary #6188 - [radarhere] - -- Load before getting size in resize() #6190 - [radarhere] - -- Load image before performing size calculations in thumbnail() #6186 - [radarhere] - -- Deprecated PhotoImage.paste() box parameter #6178 - [radarhere] - -9.1.1 (2022-05-17) ------------------- - -- When reading past the end of a TGA scan line, reduce bytes left. CVE-2022-30595 - [radarhere] - -- Do not open images with zero or negative height #6269 - [radarhere] - -9.1.0 (2022-04-01) ------------------- - -- Add support for multiple component transformation to JPEG2000 #5500 - [scaramallion, radarhere, hugovk] - -- Fix loading FriBiDi on Alpine #6165 - [nulano] - -- Added setting for converting GIF P frames to RGB #6150 - [radarhere] - -- Allow 1 mode images to be inverted #6034 - [radarhere] - -- Raise ValueError when trying to save empty JPEG #6159 - [radarhere] - -- Always save TIFF with contiguous planar configuration #5973 - [radarhere] - -- Connected discontiguous polygon corners #5980 - [radarhere] - -- Ensure Tkinter hook is activated for getimage() #6032 - [radarhere] - -- Use screencapture arguments to crop on macOS #6152 - [radarhere] - -- Do not mark L mode JPEG as 1 bit in PDF #6151 - [radarhere] - -- Added support for reading I;16R TIFF images #6132 - [radarhere] - -- If an error occurs after creating a file, remove the file #6134 - [radarhere] - -- Fixed calling DisplayViewer or XVViewer without a title #6136 - [radarhere] - -- Retain RGBA transparency when saving multiple GIF frames #6128 - [radarhere] - -- Save additional ICO frames with other bit depths if supplied #6122 - [radarhere] - -- Handle EXIF data truncated to just the header #6124 - [radarhere] - -- Added support for reading BMP images with RLE8 compression #6102 - [radarhere] - -- Support Python distributions where _tkinter is compiled in #6006 - [lukegb] - -- Added support for PPM arbitrary maxval #6119 - [radarhere] - -- Added BigTIFF reading #6097 - [radarhere] - -- When converting, clip I;16 to be unsigned, not signed #6112 - [radarhere] - -- Fixed loading L mode GIF with transparency #6086 - [radarhere] - -- Improved handling of PPM header #5121 - [Piolie, radarhere] - -- Reset size when seeking away from "Large Thumbnail" MPO frame #6101 - [radarhere] - -- Replace requirements.txt with extras #6072 - [hugovk, radarhere] - -- Added PyEncoder and support BLP saving #6069 - [radarhere] - -- Handle TGA images with packets that cross scan lines #6087 - [radarhere] - -- Added FITS reading #6056 - [radarhere, hugovk] - -- Added rawmode argument to Image.getpalette() #6061 - [radarhere] - -- Fixed BUFR, GRIB and HDF5 stub saving #6071 - [radarhere] - -- Do not automatically remove temporary ImageShow files on Unix #6045 - [radarhere] - -- Correctly read JPEG compressed BLP images #4685 - [Meithal, radarhere] - -- Merged _MODE_CONV typ into ImageMode as typestr #6057 - [radarhere] - -- Consider palette size when converting and in getpalette() #6060 - [radarhere] - -- Added enums #5954 - [radarhere] - -- Ensure image is opaque after converting P to PA with RGB palette #6052 - [radarhere] - -- Attach RGBA palettes from putpalette() when suitable #6054 - [radarhere] - -- Added get_photoshop_blocks() to parse Photoshop TIFF tag #6030 - [radarhere] - -- Drop excess values in BITSPERSAMPLE #6041 - [mikhail-iurkov] - -- Added unpacker from RGBA;15 to RGB #6031 - [radarhere] - -- Enable arm64 for MSVC on Windows #5811 - [gaborkertesz-linaro, gaborkertesz] - -- Keep IPython/Jupyter text/plain output stable #5891 - [shamrin, radarhere] - -- Raise an error when performing a negative crop #5972 - [radarhere, hugovk] - -- Deprecated show_file "file" argument in favour of "path" #5959 - [radarhere] - -- Fixed SPIDER images for use with Bio-formats library #5956 - [radarhere] - -- Ensure duplicated file pointer is closed #5946 - [radarhere] - -- Added specific error if path coordinate type is incorrect #5942 - [radarhere] - -- Return an empty bytestring from tobytes() for an empty image #5938 - [radarhere] - -- Remove readonly from Image.__eq__ #5930 - [hugovk] - -9.0.1 (2022-02-03) ------------------- - -- In show_file, use os.remove to remove temporary images. CVE-2022-24303 #6010 - [radarhere, hugovk] - -- Restrict builtins within lambdas for ImageMath.eval. CVE-2022-22817 #6009 - [radarhere] - -9.0.0 (2022-01-02) ------------------- - -- Restrict builtins for ImageMath.eval(). CVE-2022-22817 #5923 - [radarhere] - -- Ensure JpegImagePlugin stops at the end of a truncated file #5921 - [radarhere] - -- Fixed ImagePath.Path array handling. CVE-2022-22815, CVE-2022-22816 #5920 - [radarhere] - -- Remove consecutive duplicate tiles that only differ by their offset #5919 - [radarhere] - -- Improved I;16 operations on big endian #5901 - [radarhere] - -- Limit quantized palette to number of colors #5879 - [radarhere] - -- Fixed palette index for zeroed color in FASTOCTREE quantize #5869 - [radarhere] - -- When saving RGBA to GIF, make use of first transparent palette entry #5859 - [radarhere] - -- Pass SAMPLEFORMAT to libtiff #5848 - [radarhere] - -- Added rounding when converting P and PA #5824 - [radarhere] - -- Improved putdata() documentation and data handling #5910 - [radarhere] - -- Exclude carriage return in PDF regex to help prevent ReDoS #5912 - [hugovk] - -- Fixed freeing pointer in ImageDraw.Outline.transform #5909 - [radarhere] - -- Added ImageShow support for xdg-open #5897 - [m-shinder, radarhere] - -- Support 16-bit grayscale ImageQt conversion #5856 - [cmbruns, radarhere] - -- Convert subsequent GIF frames to RGB or RGBA #5857 - [radarhere] - -- Do not prematurely return in ImageFile when saving to stdout #5665 - [infmagic2047, radarhere] - -- Added support for top right and bottom right TGA orientations #5829 - [radarhere] - -- Corrected ICNS file length in header #5845 - [radarhere] - -- Block tile TIFF tags when saving #5839 - [radarhere] - -- Added line width argument to polygon #5694 - [radarhere] - -- Do not redeclare class each time when converting to NumPy #5844 - [radarhere] - -- Only prevent repeated polygon pixels when drawing with transparency #5835 - [radarhere] - -- Add support for pickling TrueType fonts #5826 - [hugovk, radarhere] - -- Only prefer command line tools SDK on macOS over default MacOSX SDK #5828 - [radarhere] - -- Drop support for soon-EOL Python 3.6 #5768 - [hugovk, nulano, radarhere] - -- Fix compilation on 64-bit Termux #5793 - [landfillbaby] - -- Use title for display in ImageShow #5788 - [radarhere] - -- Remove support for FreeType 2.7 and older #5777 - [hugovk, radarhere] - -- Fix for PyQt6 #5775 - [hugovk, radarhere] - -- Removed deprecated PILLOW_VERSION, Image.show command parameter, Image._showxv and ImageFile.raise_ioerror #5776 - [radarhere] - -8.4.0 (2021-10-15) ------------------- - -- Prefer global transparency in GIF when replacing with background color #5756 - [radarhere] - -- Added "exif" keyword argument to TIFF saving #5575 - [radarhere] - -- Copy Python palette to new image in quantize() #5696 - [radarhere] - -- Read ICO AND mask from end #5667 - [radarhere] - -- Actually check the framesize in FliDecode.c #5659 - [wiredfool] - -- Determine JPEG2000 mode purely from ihdr header box #5654 - [radarhere] - -- Fixed using info dictionary when writing multiple APNG frames #5611 - [radarhere] - -- Allow saving 1 and L mode TIFF with PhotometricInterpretation 0 #5655 - [radarhere] - -- For GIF save_all with palette, do not include palette with each frame #5603 - [radarhere] - -- Keep transparency when converting from P to LA or PA #5606 - [radarhere] - -- Copy palette to new image in transform() #5647 - [radarhere] - -- Added "transparency" argument to EpsImagePlugin load() #5620 - [radarhere] - -- Corrected pathlib.Path detection when saving #5633 - [radarhere] - -- Added WalImageFile class #5618 - [radarhere] - -- Consider I;16 pixel size when drawing text #5598 - [radarhere] - -- If default conversion from P is RGB with transparency, convert to RGBA #5594 - [radarhere] - -- Speed up rotating square images by 90 or 270 degrees #5646 - [radarhere] - -- Add support for reading DPI information from JPEG2000 images - [rogermb, radarhere] - -- Catch TypeError from corrupted DPI value in EXIF #5639 - [homm, radarhere] - -- Do not close file pointer when saving SGI images #5645 - [farizrahman4u, radarhere] - -- Deprecate ImagePalette size parameter #5641 - [radarhere, hugovk] - -- Prefer command line tools SDK on macOS #5624 - [radarhere] - -- Added tags when saving YCbCr TIFF #5597 - [radarhere] - -- PSD layer count may be negative #5613 - [radarhere] - -- Fixed ImageOps expand with tuple border on P image #5615 - [radarhere] - -- Fixed error saving APNG with duplicate frames and different duration times #5609 - [thak1411, radarhere] - -8.3.2 (2021-09-02) ------------------- - -- CVE-2021-23437 Raise ValueError if color specifier is too long - [hugovk, radarhere] - -- Fix 6-byte OOB read in FliDecode - [wiredfool] - -- Add support for Python 3.10 #5569, #5570 - [hugovk, radarhere] - -- Ensure TIFF ``RowsPerStrip`` is multiple of 8 for JPEG compression #5588 - [kmilos, radarhere] - -- Updates for ``ImagePalette`` channel order #5599 - [radarhere] - -- Hide FriBiDi shim symbols to avoid conflict with real FriBiDi library #5651 - [nulano] - -8.3.1 (2021-07-06) ------------------- - -- Catch OSError when checking if fp is sys.stdout #5585 - [radarhere] - -- Handle removing orientation from alternate types of EXIF data #5584 - [radarhere] - -- Make Image.__array__ take optional dtype argument #5572 - [t-vi, radarhere] - -8.3.0 (2021-07-01) ------------------- - -- Use snprintf instead of sprintf. CVE-2021-34552 #5567 - [radarhere] - -- Limit TIFF strip size when saving with LibTIFF #5514 - [kmilos] - -- Allow ICNS save on all operating systems #4526 - [baletu, radarhere, newpanjing, hugovk] - -- De-zigzag JPEG's DQT when loading; deprecate convert_dict_qtables #4989 - [gofr, radarhere] - -- Replaced xml.etree.ElementTree #5565 - [radarhere] - -- Moved CVE image to pillow-depends #5561 - [radarhere] - -- Added tag data for IFD groups #5554 - [radarhere] - -- Improved ImagePalette #5552 - [radarhere] - -- Add DDS saving #5402 - [radarhere] - -- Improved getxmp() #5455 - [radarhere] - -- Convert to float for comparison with float in IFDRational __eq__ #5412 - [radarhere] - -- Allow getexif() to access TIFF tag_v2 data #5416 - [radarhere] - -- Read FITS image mode and size #5405 - [radarhere] - -- Merge parallel horizontal edges in ImagingDrawPolygon #5347 - [radarhere, hrdrq] - -- Use transparency behind first GIF frame and when disposing to background #5557 - [radarhere, zewt] - -- Avoid unstable nature of qsort in Quant.c #5367 - [radarhere] - -- Copy palette to new images in ImageOps expand #5551 - [radarhere] - -- Ensure palette string matches RGB mode #5549 - [radarhere] - -- Do not modify EXIF of original image instance in exif_transpose() #5547 - [radarhere] - -- Fixed default numresolution for small JPEG2000 images #5540 - [radarhere] - -- Added DDS BC5 reading #5501 - [radarhere] - -- Raise an error if ImageDraw.textbbox is used without a TrueType font #5510 - [radarhere] - -- Added ICO saving in BMP format #5513 - [radarhere] - -- Ensure PNG seeks to end of previous chunk at start of load_end #5493 - [radarhere] - -- Do not allow TIFF to seek to a past frame #5473 - [radarhere] - -- Avoid race condition when displaying images with eog #5507 - [mconst] - -- Added specific error messages when ink has incorrect number of bands #5504 - [radarhere] - -- Allow converting an image to a numpy array to raise errors #5379 - [radarhere] - -- Removed DPI rounding from BMP, JPEG, PNG and WMF loading #5476, #5470 - [radarhere] - -- Remove spikes when drawing thin pieslices #5460 - [xtsm] - -- Updated default value for SAMPLESPERPIXEL TIFF tag #5452 - [radarhere] - -- Removed TIFF DPI rounding #5446 - [radarhere, hugovk] - -- Include code in WebP error #5471 - [radarhere] - -- Do not alter pixels outside mask when drawing text on an image with transparency #5434 - [radarhere] - -- Reset handle when seeking backwards in TIFF #5443 - [radarhere] - -- Replace sys.stdout with sys.stdout.buffer when saving #5437 - [radarhere] - -- Fixed UNDEFINED TIFF tag of length 0 being changed in roundtrip #5426 - [radarhere] - -- Fixed bug when checking FreeType2 version if it is not installed #5445 - [radarhere] - -- Do not round dimensions when saving PDF #5459 - [radarhere] - -- Added ImageOps contain() #5417 - [radarhere, hugovk] - -- Changed WebP default "method" value to 4 #5450 - [radarhere] - -- Switched to saving 1-bit PDFs with DCTDecode #5430 - [radarhere] - -- Use bpp from ICO header #5429 - [radarhere] - -- Corrected JPEG APP14 transform value #5408 - [radarhere] - -- Changed TIFF tag 33723 length to 1 #5425 - [radarhere] - -- Changed ImageMorph incorrect mode errors to ValueError #5414 - [radarhere] - -- Add EXIF tags specified in EXIF 2.32 #5419 - [gladiusglad] - -- Treat previous contents of first GIF frame as transparent #5391 - [radarhere] - -- For special image modes, revert default resize resampling to NEAREST #5411 - [radarhere] - -- JPEG2000: Support decoding subsampled RGB and YCbCr images #4996 - [nulano, radarhere] - -- Stop decoding BC1 punchthrough alpha in BC2&3 #4144 - [jansol] - -- Use zero if GIF background color index is missing #5390 - [radarhere] - -- Fixed ensuring that GIF previous frame was loaded #5386 - [radarhere] - -- Valgrind fixes #5397 - [wiredfool] - -- Round down the radius in rounded_rectangle #5382 - [radarhere] - -- Fixed reading uncompressed RGB data from DDS #5383 - [radarhere] - -8.2.0 (2021-04-01) ------------------- - -- Added getxmp() method #5144 - [UrielMaD, radarhere] - -- Add ImageShow support for GraphicsMagick #5349 - [latosha-maltba, radarhere] - -- Do not load transparent pixels from subsequent GIF frames #5333 - [zewt, radarhere] - -- Use LZW encoding when saving GIF images #5291 - [raygard] - -- Set all transparent colors to be equal in quantize() #5282 - [radarhere] - -- Allow PixelAccess to use Python __int__ when parsing x and y #5206 - [radarhere] - -- Removed Image._MODEINFO #5316 - [radarhere] - -- Add preserve_tone option to autocontrast #5350 - [elejke, radarhere] - -- Fixed linear_gradient and radial_gradient I and F modes #5274 - [radarhere] - -- Add support for reading TIFFs with PlanarConfiguration=2 #5364 - [kkopachev, wiredfool, nulano] - -- Deprecated categories #5351 - [radarhere] - -- Do not premultiply alpha when resizing with Image.NEAREST resampling #5304 - [nulano] - -- Dynamically link FriBiDi instead of Raqm #5062 - [nulano] - -- Allow fewer PNG palette entries than the bit depth maximum when saving #5330 - [radarhere] - -- Use duration from info dictionary when saving WebP #5338 - [radarhere] - -- Stop flattening EXIF IFD into getexif() #4947 - [radarhere, kkopachev] - -- Replaced tiff_deflate with tiff_adobe_deflate compression when saving TIFF images #5343 - [radarhere] - -- Save ICC profile from TIFF encoderinfo #5321 - [radarhere] - -- Moved RGB fix inside ImageQt class #5268 - [radarhere] - -- Allow alpha_composite destination to be negative #5313 - [radarhere] - -- Ensure file is closed if it is opened by ImageQt.ImageQt #5260 - [radarhere] - -- Added ImageDraw rounded_rectangle method #5208 - [radarhere] - -- Added IPythonViewer #5289 - [radarhere, Kipkurui-mutai] - -- Only draw each rectangle outline pixel once #5183 - [radarhere] - -- Use mmap instead of built-in Win32 mapper #5224 - [radarhere, cgohlke] - -- Handle PCX images with an odd stride #5214 - [radarhere] - -- Only read different sizes for "Large Thumbnail" MPO frames #5168 - [radarhere] - -- Added PyQt6 support #5258 - [radarhere] - -- Changed Image.open formats parameter to be case-insensitive #5250 - [Piolie, radarhere] - -- Deprecate Tk/Tcl 8.4, to be removed in Pillow 10 (2023-07-01) #5216 - [radarhere] - -- Added tk version to pilinfo #5226 - [radarhere, nulano] - -- Support for ignoring tests when running valgrind #5150 - [wiredfool, radarhere, hugovk] - -- OSS-Fuzz support #5189 - [wiredfool, radarhere] - -8.1.2 (2021-03-06) ------------------- - -- Fix Memory DOS in BLP (CVE-2021-27921), ICNS (CVE-2021-27922) and ICO (CVE-2021-27923) Image Plugins - [wiredfool] - -8.1.1 (2021-03-01) ------------------- - -- Use more specific regex chars to prevent ReDoS. CVE-2021-25292 - [hugovk] - -- Fix OOB Read in TiffDecode.c, and check the tile validity before reading. CVE-2021-25291 - [wiredfool] - -- Fix negative size read in TiffDecode.c. CVE-2021-25290 - [wiredfool] - -- Fix OOB read in SgiRleDecode.c. CVE-2021-25293 - [wiredfool] - -- Incorrect error code checking in TiffDecode.c. CVE-2021-25289 - [wiredfool] - -- PyModule_AddObject fix for Python 3.10 #5194 - [radarhere] - -8.1.0 (2021-01-02) ------------------- - -- Fix TIFF OOB Write error. CVE-2020-35654 #5175 - [wiredfool] - -- Fix for Read Overflow in PCX Decoding. CVE-2020-35653 #5174 - [wiredfool, radarhere] - -- Fix for SGI Decode buffer overrun. CVE-2020-35655 #5173 - [wiredfool, radarhere] - -- Fix OOB Read when saving GIF of xsize=1 #5149 - [wiredfool] - -- Makefile updates #5159 - [wiredfool, radarhere] - -- Add support for PySide6 #5161 - [hugovk] - -- Use disposal settings from previous frame in APNG #5126 - [radarhere] - -- Added exception explaining that _repr_png_ saves to PNG #5139 - [radarhere] - -- Use previous disposal method in GIF load_end #5125 - [radarhere] - -- Allow putpalette to accept 1024 integers to include alpha values #5089 - [radarhere] - -- Fix OOB Read when writing TIFF with custom Metadata #5148 - [wiredfool] - -- Added append_images support for ICO #4568 - [ziplantil, radarhere] - -- Block TIFFTAG_SUBIFD #5120 - [radarhere] - -- Fixed dereferencing potential null pointers #5108, #5111 - [cgohlke, radarhere] - -- Deprecate FreeType 2.7 #5098 - [hugovk, radarhere] - -- Moved warning to end of execution #4965 - [radarhere] - -- Removed unused fromstring and tostring C methods #5026 - [radarhere] - -- init() if one of the formats is unrecognised #5037 - [radarhere] - -- Moved string_dimension CVE image to pillow-depends #4993 - [radarhere] - -- Support raw rgba8888 for DDS #4760 - [qiankanglai] - -8.0.1 (2020-10-22) ------------------- - -- Update FreeType used in binary wheels to 2.10.4 to fix CVE-2020-15999. - [radarhere] - -- Moved string_dimension image to pillow-depends #4993 - [radarhere] - -8.0.0 (2020-10-15) ------------------- - -- Drop support for EOL Python 3.5 #4746, #4794 - [hugovk, radarhere, nulano] - -- Drop support for PyPy3 < 7.2.0 #4964 - [nulano] - -- Remove ImageCms.CmsProfile attributes deprecated since 3.2.0 #4768 - [hugovk, radarhere] - -- Remove long-deprecated Image.py functions #4798 - [hugovk, nulano, radarhere] - -- Add support for 16-bit precision JPEG quantization values #4918 - [gofr] - -- Added reading of IFD tag type #4979 - [radarhere] - -- Initialize offset memory for PyImagingPhotoPut #4806 - [nqbit] - -- Fix TiffDecode comparison warnings #4756 - [nulano] - -- Docs: Add dark mode #4968 - [hugovk, nulano] - -- Added macOS SDK install path to library and include directories #4974 - [radarhere, fxcoudert] - -- Imaging.h: prevent confusion with system #4923 - [ax3l, ,radarhere] - -- Avoid using pkg_resources in PIL.features.pilinfo #4975 - [nulano] - -- Add getlength and getbbox functions for TrueType fonts #4959 - [nulano, radarhere, hugovk] - -- Allow tuples with one item to give single color value in getink #4927 - [radarhere, nulano] - -- Add support for CBDT and COLR fonts #4955 - [nulano, hugovk] - -- Removed OSError in favour of DecompressionBombError for BMP #4966 - [radarhere] - -- Implemented another ellipse drawing algorithm #4523 - [xtsm, radarhere] - -- Removed unused JpegImagePlugin._fixup_dict function #4957 - [radarhere] - -- Added reading and writing of private PNG chunks #4292 - [radarhere] - -- Implement anchor for TrueType fonts #4930 - [nulano, hugovk] - -- Fixed bug in Exif __delitem__ #4942 - [radarhere] - -- Fix crash in ImageTk.PhotoImage on MinGW 64-bit #4946 - [nulano] - -- Moved CVE images to pillow-depends #4929 - [radarhere] - -- Refactor font_getsize and font_render #4910 - [nulano] - -- Fixed loading profile with non-ASCII path on Windows #4914 - [radarhere] - -- Fixed effect_spread bug for zero distance #4908 - [radarhere, hugovk] - -- Added formats parameter to Image.open #4837 - [nulano, radarhere] - -- Added regular_polygon draw method #4846 - [comhar] - -- Raise proper TypeError in putpixel #4882 - [nulano, hugovk] - -- Added writing of subIFDs #4862 - [radarhere] - -- Fix IFDRational __eq__ bug #4888 - [luphord, radarhere] - -- Fixed duplicate variable name #4885 - [liZe, radarhere] - -- Added homebrew zlib include directory #4842 - [radarhere] - -- Corrected inverted PDF CMYK colors #4866 - [radarhere] - -- Do not try to close file pointer if file pointer is empty #4823 - [radarhere] - -- ImageOps.autocontrast: add mask parameter #4843 - [navneeth, hugovk] - -- Read EXIF data tEXt chunk into info as bytes instead of string #4828 - [radarhere] - -- Replaced distutils with setuptools #4797, #4809, #4814, #4817, #4829, #4890 - [hugovk, radarhere] - -- Add MIME type to PsdImagePlugin #4788 - [samamorgan] - -- Allow ImageOps.autocontrast to specify low and high cutoffs separately #4749 - [millionhz, radarhere] - -7.2.0 (2020-07-01) ------------------- - -- Do not convert I;16 images when showing PNGs #4744 - [radarhere] - -- Fixed ICNS file pointer saving #4741 - [radarhere] - -- Fixed loading non-RGBA mode APNGs with dispose background #4742 - [radarhere] - -- Deprecated _showxv #4714 - [radarhere] - -- Deprecate Image.show(command="...") #4646 - [nulano, hugovk, radarhere] - -- Updated JPEG magic number #4707 - [Cykooz, radarhere] - -- Change STRIPBYTECOUNTS to LONG if necessary when saving #4626 - [radarhere, hugovk] - -- Write JFIF header when saving JPEG #4639 - [radarhere] - -- Replaced tiff_jpeg with jpeg compression when saving TIFF images #4627 - [radarhere] - -- Writing TIFF tags: improved BYTE, added UNDEFINED #4605 - [radarhere] - -- Consider transparency when pasting text on an RGBA image #4566 - [radarhere] - -- Added method argument to single frame WebP saving #4547 - [radarhere] - -- Use ImageFileDirectory_v2 in Image.Exif #4637 - [radarhere] - -- Corrected reading EXIF metadata without prefix #4677 - [radarhere] - -- Fixed drawing a jointed line with a sequence of numeric values #4580 - [radarhere] - -- Added support for 1-D NumPy arrays #4608 - [radarhere] - -- Parse orientation from XMP tags #4560 - [radarhere] - -- Speed up text layout by not rendering glyphs #4652 - [nulano] - -- Fixed ZeroDivisionError in Image.thumbnail #4625 - [radarhere] - -- Replaced TiffImagePlugin DEBUG with logging #4550 - [radarhere] - -- Fix repeatedly loading .gbr #4620 - [ElinksFr, radarhere] - -- JPEG: Truncate icclist instead of setting to None #4613 - [homm] - -- Fixes default offset for Exif #4594 - [rodrigob, radarhere] - -- Fixed bug when unpickling TIFF images #4565 - [radarhere] - -- Fix pickling WebP #4561 - [hugovk, radarhere] - -- Replace IOError and WindowsError aliases with OSError #4536 - [hugovk, radarhere] - -7.1.2 (2020-04-25) ------------------- - -- Raise an EOFError when seeking too far in PNG #4528 - [radarhere] - -7.1.1 (2020-04-02) ------------------- - -- Fix regression seeking and telling PNGs #4512 #4514 - [hugovk, radarhere] - -7.1.0 (2020-04-01) ------------------- - -- Fix multiple OOB reads in FLI decoding #4503 - [wiredfool] - -- Fix buffer overflow in SGI-RLE decoding #4504 - [wiredfool, hugovk] - -- Fix bounds overflow in JPEG 2000 decoding #4505 - [wiredfool] - -- Fix bounds overflow in PCX decoding #4506 - [wiredfool] - -- Fix 2 buffer overflows in TIFF decoding #4507 - [wiredfool] - -- Add APNG support #4243 - [pmrowla, radarhere, hugovk] - -- ImageGrab.grab() for Linux with XCB #4260 - [nulano, radarhere] - -- Added three new channel operations #4230 - [dwastberg, radarhere] - -- Prevent masking of Image reduce method in Jpeg2KImagePlugin #4474 - [radarhere, homm] - -- Added reading of earlier ImageMagick PNG EXIF data #4471 - [radarhere] - -- Fixed endian handling for I;16 getextrema #4457 - [radarhere] - -- Release buffer if function returns prematurely #4381 - [radarhere] - -- Add JPEG comment to info dictionary #4455 - [radarhere] - -- Fix size calculation of Image.thumbnail() #4404 - [orlnub123] - -- Fixed stroke on FreeType < 2.9 #4401 - [radarhere] - -- If present, only use alpha channel for bounding box #4454 - [radarhere] - -- Warn if an unknown feature is passed to features.check() #4438 - [jdufresne] - -- Fix Name field length when saving IM images #4424 - [hugovk, radarhere] - -- Allow saving of zero quality JPEG images #4440 - [radarhere] - -- Allow explicit zero width to hide outline #4334 - [radarhere] - -- Change ContainerIO return type to match file object mode #4297 - [jdufresne, radarhere] - -- Only draw each polygon pixel once #4333 - [radarhere] - -- Add support for shooting situation Exif IFD tags #4398 - [alexagv] - -- Handle multiple and malformed JPEG APP13 markers #4370 - [homm] - -- Depends: Update libwebp to 1.1.0 #4342, libjpeg to 9d #4352 - [radarhere] - -7.0.0 (2020-01-02) ------------------- - -- Drop support for EOL Python 2.7 #4109 - [hugovk, radarhere, jdufresne] - -- Fix rounding error on RGB to L conversion #4320 - [homm] - -- Exif writing fixes: Rational boundaries and signed/unsigned types #3980 - [kkopachev, radarhere] - -- Allow loading of WMF images at a given DPI #4311 - [radarhere] - -- Added reduce operation #4251 - [homm] - -- Raise ValueError for io.StringIO in Image.open #4302 - [radarhere, hugovk] - -- Fix thumbnail geometry when DCT scaling is used #4231 - [homm, radarhere] - -- Use default DPI when exif provides invalid x_resolution #4147 - [beipang2, radarhere] - -- Change default resize resampling filter from NEAREST to BICUBIC #4255 - [homm] - -- Fixed black lines on upscaled images with the BOX filter #4278 - [homm] - -- Better thumbnail aspect ratio preservation #4256 - [homm] - -- Add La mode packing and unpacking #4248 - [homm] - -- Include tests in coverage reports #4173 - [hugovk] - -- Handle broken Photoshop data #4239 - [radarhere] - -- Raise a specific exception if no data is found for an MPO frame #4240 - [radarhere] - -- Fix Unicode support for PyPy #4145 - [nulano] - -- Added UnidentifiedImageError #4182 - [radarhere, hugovk] - -- Remove deprecated __version__ from plugins #4197 - [hugovk, radarhere] - -- Fixed freeing unallocated pointer when resizing with height too large #4116 - [radarhere] - -- Copy info in Image.transform #4128 - [radarhere] - -- Corrected DdsImagePlugin setting info gamma #4171 - [radarhere] - -- Depends: Update libtiff to 4.1.0 #4195, Tk Tcl to 8.6.10 #4229, libimagequant to 2.12.6 #4318 - [radarhere] - -- Improve handling of file resources #3577 - [jdufresne] - -- Removed CI testing of Fedora 29 #4165 - [hugovk] - -- Added pypy3 to tox envlist #4137 - [jdufresne] - -- Drop support for EOL PyQt4 and PySide #4108 - [hugovk, radarhere] - -- Removed deprecated setting of TIFF image sizes #4114 - [radarhere] - -- Removed deprecated PILLOW_VERSION #4107 - [hugovk] - -- Changed default frombuffer raw decoder args #1730 - [radarhere] - -6.2.2 (2020-01-02) ------------------- - -- This is the last Pillow release to support Python 2.7 #3642 - -- Overflow checks for realloc for tiff decoding. CVE-2020-5310 - [wiredfool, radarhere] - -- Catch SGI buffer overrun. CVE-2020-5311 - [radarhere] - -- Catch PCX P mode buffer overrun. CVE-2020-5312 - [radarhere] - -- Catch FLI buffer overrun. CVE-2020-5313 - [radarhere] - -- Raise an error for an invalid number of bands in FPX image. CVE-2019-19911 - [wiredfool, radarhere] - -6.2.1 (2019-10-21) ------------------- - -- Add support for Python 3.8 #4141 - [hugovk] - -6.2.0 (2019-10-01) ------------------- - -- Catch buffer overruns #4104 - [radarhere] - -- Initialize rows_per_strip when RowsPerStrip tag is missing #4034 - [cgohlke, radarhere] - -- Raise error if TIFF dimension is a string #4103 - [radarhere] - -- Added decompression bomb checks #4102 - [radarhere] - -- Fix ImageGrab.grab DPI scaling on Windows 10 version 1607+ #4000 - [nulano, radarhere] - -- Corrected negative seeks #4101 - [radarhere] - -- Added argument to capture all screens on Windows #3950 - [nulano, radarhere] - -- Updated warning to specify when Image.frombuffer defaults will change #4086 - [radarhere] - -- Changed WindowsViewer format to PNG #4080 - [radarhere] - -- Use TIFF orientation #4063 - [radarhere] - -- Raise the same error if a truncated image is loaded a second time #3965 - [radarhere] - -- Lazily use ImageFileDirectory_v1 values from Exif #4031 - [radarhere] - -- Improved HSV conversion #4004 - [radarhere] - -- Added text stroking #3978 - [radarhere, hugovk] - -- No more deprecated bdist_wininst .exe installers #4029 - [hugovk] - -- Do not allow floodfill to extend into negative coordinates #4017 - [radarhere] - -- Fixed arc drawing bug for a non-whole number of degrees #4014 - [radarhere] - -- Fix bug when merging identical images to GIF with a list of durations #4003 - [djy0, radarhere] - -- Fix bug in TIFF loading of BufferedReader #3998 - [chadawagner] - -- Added fallback for finding ld on MinGW Cygwin #4019 - [radarhere] - -- Remove indirect dependencies from requirements.txt #3976 - [hugovk] - -- Depends: Update libwebp to 1.0.3 #3983, libimagequant to 2.12.5 #3993, freetype to 2.10.1 #3991 - [radarhere] - -- Change overflow check to use PY_SSIZE_T_MAX #3964 - [radarhere] - -- Report reason for pytest skips #3942 - [hugovk] - -6.1.0 (2019-07-01) ------------------- - -- Deprecate Image.__del__ #3929 - [jdufresne] - -- Tiff: Add support for JPEG quality #3886 - [olt] - -- Respect the PKG_CONFIG environment variable when building #3928 - [chewi] - -- Use explicit memcpy() to avoid unaligned memory accesses #3225 - [DerDakon] - -- Improve encoding of TIFF tags #3861 - [olt] - -- Update Py_UNICODE to Py_UCS4 #3780 - [nulano] - -- Consider I;16 pixel size when drawing #3899 - [radarhere] - -- Add TIFFTAG_SAMPLEFORMAT to blocklist #3926 - [cgohlke, radarhere] - -- Create GIF deltas from background colour of GIF frames if disposal mode is 2 #3708 - [sircinnamon, radarhere] - -- Added ImageSequence all_frames #3778 - [radarhere] - -- Use unsigned int to store TIFF IFD offsets #3923 - [cgohlke] - -- Include CPPFLAGS when searching for libraries #3819 - [jefferyto] - -- Updated TIFF tile descriptors to match current decoding functionality #3795 - [dmnisson] - -- Added an ``image.entropy()`` method (second revision) #3608 - [fish2000] - -- Pass the correct types to PyArg_ParseTuple #3880 - [QuLogic] - -- Fixed crash when loading non-font bytes #3912 - [radarhere] - -- Fix SPARC memory alignment issues in Pack/Unpack functions #3858 - [kulikjak] - -- Added CMYK;16B and CMYK;16N unpackers #3913 - [radarhere] - -- Fixed bugs in calculating text size #3864 - [radarhere] - -- Add __main__.py to output basic format and support information #3870 - [jdufresne] - -- Added variation font support #3802 - [radarhere] - -- Do not down-convert if image is LA when showing with PNG format #3869 - [radarhere] - -- Improve handling of PSD frames #3759 - [radarhere] - -- Improved ICO and ICNS loading #3897 - [radarhere] - -- Changed Preview application path so that it is no longer static #3896 - [radarhere] - -- Corrected ttb text positioning #3856 - [radarhere] - -- Handle unexpected ICO image sizes #3836 - [radarhere] - -- Fixed bits value for RGB;16N unpackers #3837 - [kkopachev] - -- Travis CI: Add Fedora 30, remove Fedora 28 #3821 - [hugovk] - -- Added reading of CMYK;16L TIFF images #3817 - [radarhere] - -- Fixed dimensions of 1-bit PDFs #3827 - [radarhere] - -- Fixed opening mmap image through Path on Windows #3825 - [radarhere] - -- Fixed ImageDraw arc gaps #3824 - [radarhere] - -- Expand GIF to include frames with extents outside the image size #3822 - [radarhere] - -- Fixed ImageTk getimage #3814 - [radarhere] - -- Fixed bug in decoding large images #3791 - [radarhere] - -- Fixed reading APP13 marker without Photoshop data #3771 - [radarhere] - -- Added option to include layered windows in ImageGrab.grab on Windows #3808 - [radarhere] - -- Detect libimagequant when installed by pacman on MingW #3812 - [radarhere] - -- Fixed raqm layout bug #3787 - [radarhere] - -- Fixed loading font with non-Unicode path on Windows #3785 - [radarhere] - -- Travis CI: Upgrade PyPy from 6.0.0 to 7.1.1 #3783 - [hugovk, johnthagen] - -- Depends: Updated openjpeg to 2.3.1 #3794, raqm to 0.7.0 #3877, libimagequant to 2.12.3 #3889 - [radarhere] - -- Fix numpy bool bug #3790 - [radarhere] - -6.0.0 (2019-04-01) ------------------- - -- Python 2.7 support will be removed in Pillow 7.0.0 #3682 - [hugovk] - -- Add EXIF class #3625 - [radarhere] - -- Add ImageOps exif_transpose method #3687 - [radarhere] - -- Added warnings to deprecated CMSProfile attributes #3615 - [hugovk] - -- Documented reading TIFF multiframe images #3720 - [akuchling] - -- Improved speed of opening an MPO file #3658 - [Glandos] - -- Update palette in quantize #3721 - [radarhere] - -- Improvements to TIFF is_animated and n_frames #3714 - [radarhere] - -- Fixed incompatible pointer type warnings #3754 - [radarhere] - -- Improvements to PA and LA conversion and palette operations #3728 - [radarhere] - -- Consistent DPI rounding #3709 - [radarhere] - -- Change size of MPO image to match frame #3588 - [radarhere] - -- Read Photoshop resolution data #3701 - [radarhere] - -- Ensure image is mutable before saving #3724 - [radarhere] - -- Correct remap_palette documentation #3740 - [radarhere] - -- Promote P images to PA in putalpha #3726 - [radarhere] - -- Allow RGB and RGBA values for new P images #3719 - [radarhere] - -- Fixed TIFF bug when seeking backwards and then forwards #3713 - [radarhere] - -- Cache EXIF information #3498 - [Glandos] - -- Added transparency for all PNG grayscale modes #3744 - [radarhere] - -- Fix deprecation warnings in Python 3.8 #3749 - [radarhere] - -- Fixed GIF bug when rewinding to a non-zero frame #3716 - [radarhere] - -- Only close original fp in __del__ and __exit__ if original fp is exclusive #3683 - [radarhere] - -- Fix BytesWarning in Tests/test_numpy.py #3725 - [jdufresne] - -- Add missing MIME types and extensions #3520 - [pirate486743186] - -- Add I;16 PNG save #3566 - [radarhere] - -- Add support for BMP RGBA bitfield compression #3705 - [radarhere] - -- Added ability to set language for text rendering #3693 - [iwsfutcmd] - -- Only close exclusive fp on Image __exit__ #3698 - [radarhere] - -- Changed EPS subprocess stdout from devnull to None #3635 - [radarhere] - -- Add reading old-JPEG compressed TIFFs #3489 - [kkopachev] - -- Add EXIF support for PNG #3674 - [radarhere] - -- Add option to set dither param on quantize #3699 - [glasnt] - -- Add reading of DDS uncompressed RGB data #3673 - [radarhere] - -- Correct length of Tiff BYTE tags #3672 - [radarhere] - -- Add DIB saving and loading through Image open #3691 - [radarhere] - -- Removed deprecated VERSION #3624 - [hugovk] - -- Fix 'BytesWarning: Comparison between bytes and string' in PdfDict #3580 - [jdufresne] - -- Do not resize in Image.thumbnail if already the destination size #3632 - [radarhere] - -- Replace .seek() magic numbers with io.SEEK_* constants #3572 - [jdufresne] - -- Make ContainerIO.isatty() return a bool, not int #3568 - [jdufresne] - -- Add support to all transpose operations for I;16 modes #3563, #3741 - [radarhere] - -- Deprecate support for PyQt4 and PySide #3655 - [hugovk, radarhere] - -- Add TIFF compression codecs: LZMA, Zstd, WebP #3555 - [cgohlke] - -- Fixed pickling of iTXt class with protocol > 1 #3537 - [radarhere] - -- _util.isPath returns True for pathlib.Path objects #3616 - [wbadart] - -- Remove unnecessary unittest.main() boilerplate from test files #3631 - [jdufresne] - -- Exif: Seek to IFD offset #3584 - [radarhere] - -- Deprecate PIL.*ImagePlugin.__version__ attributes #3628 - [jdufresne] - -- Docs: Add note about ImageDraw operations that exceed image bounds #3620 - [radarhere] - -- Allow for unknown PNG chunks after image data #3558 - [radarhere] - -- Changed EPS subprocess stdin from devnull to None #3611 - [radarhere] - -- Fix possible integer overflow #3609 - [cgohlke] - -- Catch BaseException for resource cleanup handlers #3574 - [jdufresne] - -- Improve pytest configuration to allow specific tests as CLI args #3579 - [jdufresne] - -- Drop support for Python 3.4 #3596 - [hugovk] - -- Remove deprecated PIL.OleFileIO #3598 - [hugovk] - -- Remove deprecated ImageOps undocumented functions #3599 - [hugovk] - -- Depends: Update libwebp to 1.0.2 #3602 - [radarhere] - -- Detect MIME types #3525 - [radarhere] - -5.4.1 (2019-01-06) ------------------- - -- File closing: Only close __fp if not fp #3540 - [radarhere] - -- Fix build for Termux #3529 - [pslacerda] - -- PNG: Detect MIME types #3525 - [radarhere] - -- PNG: Handle IDAT chunks after image end #3532 - [radarhere] - -5.4.0 (2019-01-01) ------------------- - -- Docs: Improved ImageChops documentation #3522 - [radarhere] - -- Allow RGB and RGBA values for P image putpixel #3519 - [radarhere] - -- Add APNG extension to PNG plugin #3501 - [pirate486743186, radarhere] - -- Lookup ld.so.cache instead of hardcoding search paths #3245 - [pslacerda] - -- Added custom string TIFF tags #3513 - [radarhere] - -- Improve setup.py configuration #3395 - [diorcety] - -- Read textual chunks located after IDAT chunks for PNG #3506 - [radarhere] - -- Performance: Don't try to hash value if enum is empty #3503 - [Glandos] - -- Added custom int and float TIFF tags #3350 - [radarhere] - -- Fixes for issues reported by static code analysis #3393 - [frenzymadness] - -- GIF: Wait until mode is normalized to copy im.info into encoderinfo #3187 - [radarhere] - -- Docs: Add page of deprecations and removals #3486 - [hugovk] - -- Travis CI: Upgrade PyPy from 5.8.0 to 6.0 #3488 - [hugovk] - -- Travis CI: Allow lint job to fail #3467 - [hugovk] - -- Resolve __fp when closing and deleting #3261 - [radarhere] - -- Close exclusive fp before discarding #3461 - [radarhere] - -- Updated open files documentation #3490 - [radarhere] - -- Added libjpeg_turbo to check_feature #3493 - [radarhere] - -- Change color table index background to tuple when saving as WebP #3471 - [radarhere] - -- Allow arbitrary number of comment extension subblocks #3479 - [radarhere] - -- Ensure previous FLI frame is loaded before seeking to the next #3478 - [radarhere] - -- ImageShow improvements #3450 - [radarhere] - -- Depends: Update libimagequant to 2.12.2 #3442, libtiff to 4.0.10 #3458, libwebp to 1.0.1 #3468, Tk Tcl to 8.6.9 #3465 - [radarhere] - -- Check quality_layers type #3464 - [radarhere] - -- Add context manager, __del__ and close methods to TarIO #3455 - [radarhere] - -- Test: Do not play sound when running screencapture command #3454 - [radarhere] - -- Close exclusive fp on open exception #3456 - [radarhere] - -- Only close existing fp in WebP if fp is exclusive #3418 - [radarhere] - -- Docs: Re-add the downloads badge #3443 - [hugovk] - -- Added negative index to PixelAccess #3406 - [Nazime] - -- Change tuple background to global color table index when saving as GIF #3385 - [radarhere] - -- Test: Improved ImageGrab tests #3424 - [radarhere] - -- Flake8 fixes #3422, #3440 - [radarhere, hugovk] - -- Only ask for YCbCr->RGB libtiff conversion for jpeg-compressed tiffs #3417 - [kkopachev] - -- Optimise ImageOps.fit by combining resize and crop #3409 - [homm] - -5.3.0 (2018-10-01) ------------------- - -- Changed Image size property to be read-only by default #3203 - [radarhere] - -- Add warnings if image file identification fails due to lack of WebP support #3169 - [radarhere, hugovk] - -- Hide the Ghostscript progress dialog popup on Windows #3378 - [hugovk] - -- Adding support to reading tiled and YcbCr jpeg tiffs through libtiff #3227 - [kkopachev] - -- Fixed None as TIFF compression argument #3310 - [radarhere] - -- Changed GIF seek to remove previous info items #3324 - [radarhere] - -- Improved PDF document info #3274 - [radarhere] - -- Add line width parameter to rectangle and ellipse-based shapes #3094 - [hugovk, radarhere] - -- Fixed decompression bomb check in _crop #3313 - [dinkolubina, hugovk] - -- Added support to ImageDraw.floodfill for non-RGB colors #3377 - [radarhere] - -- Tests: Avoid catching unexpected exceptions in tests #2203 - [jdufresne] - -- Use TextIOWrapper.detach() instead of NoCloseStream #2214 - [jdufresne] - -- Added transparency to matrix conversion #3205 - [radarhere] - -- Added ImageOps pad method #3364 - [radarhere] - -- Give correct extrema for I;16 format images #3359 - [bz2] - -- Added PySide2 #3279 - [radarhere] - -- Corrected TIFF tags #3369 - [radarhere] - -- CI: Install CFFI and pycparser without any PYTHONOPTIMIZE #3374 - [hugovk] - -- Read/Save RGB webp as RGB (instead of RGBX) #3298 - [kkopachev] - -- ImageDraw: Add line joints #3250 - [radarhere] - -- Improved performance of ImageDraw floodfill method #3294 - [yo1995] - -- Fix builds with --parallel #3272 - [hsoft] - -- Add more raw Tiff modes (RGBaX, RGBaXX, RGBAX, RGBAXX) #3335 - [homm] - -- Close existing WebP fp before setting new fp #3341 - [radarhere] - -- Add orientation, compression and id_section as TGA save keyword arguments #3327 - [radarhere] - -- Convert int values of RATIONAL TIFF tags to floats #3338 - [radarhere, wiredfool] - -- Fix code for PYTHONOPTIMIZE #3233 - [hugovk] - -- Changed ImageFilter.Kernel to subclass ImageFilter.BuiltinFilter, instead of the other way around #3273 - [radarhere] - -- Remove unused draw.draw_line, draw.draw_point and font.getabc methods #3232 - [hugovk] - -- Tests: Added ImageFilter tests #3295 - [radarhere] - -- Tests: Added ImageChops tests #3230 - [hugovk, radarhere] - -- AppVeyor: Download lib if not present in pillow-depends #3316 - [radarhere] - -- Travis CI: Add Python 3.7 and Xenial #3234 - [hugovk] - -- Docs: Added documentation for NumPy conversion #3301 - [radarhere] - -- Depends: Update libimagequant to 2.12.1 #3281 - [radarhere] - -- Add three-color support to ImageOps.colorize #3242 - [tsennott] - -- Tests: Add LA to TGA test modes #3222 - [danpla] - -- Skip outline if the draw operation fills with the same colour #2922 - [radarhere] - -- Flake8 fixes #3173, #3380 - [radarhere] - -- Avoid deprecated 'U' mode when opening files #2187 - [jdufresne] - -5.2.0 (2018-07-01) ------------------- - -- Fixed saving a multiframe image as a single frame PDF #3137 - [radarhere] - -- If a Qt version is already imported, attempt to use it first #3143 - [radarhere] - -- Fix transform fill color for alpha images #3147 - [fozcode] - -- TGA: Add support for writing RLE data #3186 - [danpla] - -- TGA: Read and write LA data #3178 - [danpla] - -- QuantOctree.c: Remove erroneous attempt to average over an empty range #3196 - [tkoeppe] - -- Changed ICNS format tests to pass on OS X 10.11 #3202 - [radarhere] - -- Fixed bug in ImageDraw.multiline_textsize() #3114 - [tianyu139] - -- Added getsize_multiline support for PIL.ImageFont #3113 - [tianyu139] - -- Added ImageFile get_format_mimetype method #3190 - [radarhere] - -- Changed mmap file pointer to use context manager #3216 - [radarhere] - -- Changed ellipse point calculations to be more evenly distributed #3142 - [radarhere] - -- Only extract first Exif segment #2946 - [hugovk] - -- Tests: Test ImageDraw2, WalImageFile #3135, #2989 - [hugovk] - -- Remove unnecessary '#if 0' code #3075 - [hugovk] - -- Tests: Added GD tests #1817 - [radarhere] - -- Fix collections ABCs DeprecationWarning in Python 3.7 #3123 - [hugovk] - -- unpack_from is faster than unpack of slice #3201 - [landfillbaby] - -- Docs: Add coordinate system links and file handling links in documentation #3204, #3214 - [radarhere] - -- Tests: TestFilePng: Fix test_save_l_transparency() #3182 - [danpla] - -- Docs: Correct argument name #3171 - [radarhere] - -- Docs: Update CMake download URL #3166 - [radarhere] - -- Docs: Improve Image.transform documentation #3164 - [radarhere] - -- Fix transform fillcolor argument when image mode is RGBA or LA #3163 - [radarhere] - -- Tests: More specific Exception testing #3158 - [radarhere] - -- Add getrgb HSB/HSV color strings #3148 - [radarhere] - -- Allow float values in getrgb HSL color string #3146 - [radarhere] - -- AppVeyor: Upgrade to Python 2.7.15 and 3.4.4 #3140 - [radarhere] - -- AppVeyor: Upgrade to PyPy 6.0.0 #3133 - [hugovk] - -- Deprecate PILLOW_VERSION and VERSION #3090 - [hugovk] - -- Support Python 3.7 #3076 - [hugovk] - -- Depends: Update freetype to 2.9.1, libjpeg to 9c, libwebp to 1.0.0 #3121, #3136, #3108 - [radarhere] - -- Build macOS wheels with Xcode 6.4, supporting older macOS versions #3068 - [wiredfool] - -- Fix _i2f compilation on some GCC versions #3067 - [homm] - -- Changed encoderinfo to have priority over info when saving GIF images #3086 - [radarhere] - -- Rename PIL.version to PIL._version and remove it from module #3083 - [homm] - -- Enable background colour parameter on rotate #3057 - [storesource] - -- Remove unnecessary ``#if 1`` directive #3072 - [jdufresne] - -- Remove unused Python class, Path #3070 - [jdufresne] - -- Fix dereferencing type-punned pointer will break strict-aliasing #3069 - [jdufresne] - -5.1.0 (2018-04-02) ------------------- - -- Close fp before return in ImagingSavePPM #3061 - [kathryndavies] - -- Added documentation for ICNS append_images #3051 - [radarhere] - -- Docs: Move intro text below its header #3021 - [hugovk] - -- CI: Rename appveyor.yml as .appveyor.yml #2978 - [hugovk] - -- Fix TypeError for JPEG2000 parser feed #3042 - [hugovk] - -- Certain corrupted jpegs can result in no data read #3023 - [kkopachev] - -- Add support for BLP file format #3007 - [jleclanche] - -- Simplify version checks #2998 - [hugovk] - -- Fix "invalid escape sequence" warning on Python 3.6+ #2996 - [timgraham] - -- Allow append_images to set .icns scaled images #3005 - [radarhere] - -- Support appending to existing PDFs #2965 - [vashek] - -- Fix and improve efficient saving of ICNS on macOS #3004 - [radarhere] - -- Build: Enable pip cache in AppVeyor build #3009 - [thijstriemstra] - -- Trim trailing whitespace #2985 - [Metallicow] - -- Docs: Correct reference to Image.new method #3000 - [radarhere] - -- Rearrange ImageFilter classes into alphabetical order #2990 - [radarhere] - -- Test: Remove duplicate line #2983 - [radarhere] - -- Build: Update AppVeyor PyPy version #3003 - [radarhere] - -- Tiff: Open 8 bit Tiffs with 5 or 6 channels, discarding extra channels #2938 - [homm] - -- Readme: Added Twitter badge #2930 - [hugovk] - -- Removed __main__ code from ImageCms #2942 - [radarhere] - -- Test: Changed assert statements to unittest calls #2961 - [radarhere] - -- Depends: Update libimagequant to 2.11.10, raqm to 0.5.0, freetype to 2.9 #3036, #3017, #2957 - [radarhere] - -- Remove _imaging.crc32 in favor of builtin Python crc32 implementation #2935 - [wiredfool] - -- Move Tk directory to src directory #2928 - [hugovk] - -- Enable pip cache in Travis CI #2933 - [jdufresne] - -- Remove unused and duplicate imports #2927 - [radarhere] - -- Docs: Changed documentation references to 2.x to 2.7 #2921 - [radarhere] - -- Fix memory leak when opening webp files #2974 - [wiredfool] - -- Setup: Fix "TypeError: 'NoneType' object is not iterable" for PPC and CRUX #2951 - [hugovk] - -- Setup: Add libdirs for ppc64le and armv7l #2968 - [nehaljwani] - -5.0.0 (2018-01-01) ------------------- - -- Docs: Added docstrings from documentation #2914 - [radarhere] - -- Test: Switch from nose to pytest #2815 - [hugovk] - -- Rework Source directory layout, preventing accidental import of PIL. #2911 - [wiredfool] - -- Dynamically link libraqm #2753 - [wiredfool] - -- Removed scripts directory #2901 - [wiredfool] - -- TIFF: Run all compressed tiffs through libtiff decoder #2899 - [wiredfool] - -- GIF: Add disposal option when saving GIFs #2902 - [linnil1, wiredfool] - -- EPS: Allow for an empty line in EPS header data #2903 - [radarhere] - -- PNG: Add support for sRGB and cHRM chunks, permit sRGB when no iCCP chunk present #2898 - [wiredfool] - -- Dependencies: Update Tk Tcl to 8.6.8 #2905 - [radarhere] - -- Decompression bomb error now raised for images 2x larger than a decompression bomb warning #2583 - [wiredfool] - -- Test: avoid random failure in test_effect_noise #2894 - [hugovk] - -- Increased epsilon for test_file_eps.py:test_showpage due to Arch update. #2896 - [wiredfool] - -- Removed check parameter from _save in BmpImagePlugin, PngImagePlugin, ImImagePlugin, PalmImagePlugin, and PcxImagePlugin. #2873 - [radarhere] - -- Make PngImagePlugin.add_text() zip argument type bool #2890 - [jdufresne] - -- Depends: Updated libwebp to 0.6.1 #2880 - [radarhere] - -- Remove unnecessary bool() calls in Image.registered_extensions and skipKnownBadTests #2891 - [jdufresne] - -- Fix count of BITSPERSAMPLE items in broken TIFF files #2883 - [homm] - -- Fillcolor parameter for Image.Transform #2852 - [wiredfool] - -- Test: Display differences for test failures #2862 - [wiredfool] - -- Added executable flag to file with shebang line #2884 - [radarhere] - -- Setup: Specify compatible Python versions for pip #2877 - [hugovk] - -- Dependencies: Updated libimagequant to 2.11.4 #2878 - [radarhere] - -- Setup: Warn if trying to install for Py3.7 on Windows #2855 - [hugovk] - -- Doc: Fonts can be loaded from a file-like object, not just filename #2861 - [robin-norwood] - -- Add eog support for Ubuntu Image Viewer #2864 - [NafisFaysal] - -- Test: Test on 3.7-dev on Travis CI #2870 - [hugovk] - -- Dependencies: Update libtiff to 4.0.9 #2871 - [radarhere] - -- Setup: Replace deprecated platform.dist with file existence check #2869 - [wiredfool] - -- Build: Fix setup.py on Debian #2853 - [wiredfool] - -- Docs: Correct error in ImageDraw documentation #2858 - [meribold] - -- Test: Drop Ubuntu Precise, Fedora 24, Fedora 25, add Fedora 27, Centos 7, Amazon v2 CI Support #2854, #2843, #2895, #2897 - [wiredfool] - -- Dependencies: Updated libimagequant to 2.11.3 #2849 - [radarhere] - -- Test: Fix test_image.py to use tempfile #2841 - [radarhere] - -- Replace PIL.OleFileIO deprecation warning with descriptive ImportError #2833 - [hugovk] - -- WebP: Add support for animated WebP files #2761 - [jd20] - -- PDF: Set encoderinfo for images when saving multi-page PDF. Fixes #2804. #2805 - [ixio] - -- Allow the olefile dependency to be optional #2789 - [jdufresne] - -- GIF: Permit LZW code lengths up to 12 bits in GIF decode #2813 - [wiredfool] - -- Fix unterminated string and unchecked exception in _font_text_asBytes. #2825 - [wiredfool] - -- PPM: Use fixed list of whitespace, rather relying on locale, fixes #272. #2831 - [markmiscavage] - -- Added support for generators when using append_images #2829, #2835 - [radarhere] - -- Doc: Correct PixelAccess.rst #2824 - [hasahmed] - -- Depends: Update raqm to 0.3.0 #2822 - [radarhere] - -- Docs: Link to maintained version of aggdraw #2809 - [hugovk] - -- Include license file in the generated wheel packages #2801 - [jdufresne] - -- Depends: Update openjpeg to 2.3.0 #2791 - [radarhere] - -- Add option to Makefile to build and install with C coverage #2781 - [hugovk] - -- Add context manager support to ImageFile.Parser and PngImagePlugin.ChunkStream #2793 - [radarhere] - -- ImageDraw.textsize: fix zero length error #2788 - [wiredfool, hugovk] - -4.3.0 (2017-10-02) ------------------- - -- Fix warning on pointer cast in isblock #2775, #2778 - [cgohlke] - -- Doc: Added macOS High Sierra tested Pillow version #2777 - [radarhere] - -- Use correct Windows handle type on 64 bit in imagingcms #2774 - [cgohlke] - -- 64 Bit Windows fix for block storage #2773 - [cgohlke] - -- Fix "expression result unused" warning #2764 - [radarhere] - -- Add 16bit Read/Write and RLE read support to SgiImageFile #2769 - [jbltx, wiredfool] - -- Block & array hybrid storage #2738 - [homm] - -- Common seek frame position check #1849 - [radarhere] - -- Doc: Add note about aspect ratio to Image thumbnail script #2281 - [wilsonge] - -- Fix ValueError: invalid version number '1.0.0rc1' in scipy release candidate #2771 - [cgohlke] - -- Unfreeze requirements.txt #2766 - [hugovk] - -- Test: ResourceWarning tests #2756 - [hugovk] - -- Use n_frames to determine is_animated if possible #2315 - [radarhere] - -- Doc: Corrected parameters in documentation #2768 - [radarhere] - -- Avoid unnecessary Image operations #1891 - [radarhere] - -- Added register_extensions method #1860 - [radarhere] - -- Fix TIFF support for I;16S, I;16BS, and I;32BS rawmodes #2748 - [wiredfool] - -- Fixed doc syntax in ImageDraw #2752 - [radarhere] - -- Fixed support for building on Windows/msys2. Added Appveyor CI coverage for python3 on msys2 #2746 - [wiredfool] - -- Fix ValueError in Exif/Tiff IFD #2719 - [wiredfool] - -- Use pathlib2 for Path objects on Python < 3.4 #2291 - [asergi] - -- Export only required properties in unsafe_ptrs #2740 - [homm] - -- Alpha composite fixes #2709 - [homm] - -- Faster Transpose operations, added 'Transverse' option #2730 - [homm] - -- Deprecate ImageOps undocumented functions gaussian_blur, gblur, unsharp_mask, usm and box_blur in favor of ImageFilter implementations #2735 - [homm] - -- Dependencies: Updated freetype to 2.8.1 #2741 - [radarhere] - -- Bug: Player skipped first image #2742 - [radarhere] - -- Faster filter operations for Kernel, Gaussian, and Unsharp Mask filters #2679 - [homm] - -- EPS: Add showpage to force rendering of some EPS images #2636 - [kaplun] - -- DOC: Fix type of palette parameter in Image.quantize. #2703 - [kkopachev] - -- DOC: Fix Ico docs to match code #2712 - [hugovk] - -- Added file pointer save to SpiderImagePlugin #2647 - [radarhere] - -- Add targa version 2 footer #2713 - [jhultgre] - -- Removed redundant lines #2714 - [radarhere] - -- Travis CI: Use default pypy/pypy3 #2721 - [hugovk] - -- Fix for SystemError when rendering an empty string, added in 4.2.0 #2706 - [wiredfool] - -- Fix for memory leaks in font handling added in 4.2.0 #2634 - [wiredfool] - -- Tests: cleanup, more tests. Fixed WMF save handler #2689 - [radarhere] - -- Removed debugging interface for Image.core.grabclipboard #2708 - [radarhere] - -- Doc syntax fix #2710 - [radarhere] - -- Faster packing and unpacking for RGB, LA, and related storage modes #2693 - [homm] - -- Use RGBX rawmode for RGB JPEG images where possible #1989 - [homm] - -- Remove palettes from non-palette modes in _new #2704 - [wiredfool] - -- Delete transparency info when convert'ing RGB/L to RGBA #2633 - [olt] - -- Code tweaks to ease type annotations #2687 - [neiljp] - -- Fixed incorrect use of 's#' to byteslike object #2691 - [wiredfool] - -- Fix JPEG subsampling labels for subsampling=2 #2698 - [homm] - -- Region of interest (box) for resampling #2254 - [homm] - -- Basic support for Termux (android) in setup.py #2684 - [wiredfool] - -- Bug: Fix Image.fromarray for numpy.bool type. #2683 - [wiredfool] - -- CI: Add Fedora 24 and 26 to Docker tests - [wiredfool] - -- JPEG: Fix ZeroDivisionError when EXIF contains invalid DPI (0/0). #2667 - [vytisb] - -- Depends: Updated openjpeg to 2.2.0 #2669 - [radarhere] - -- Depends: Updated Tk Tcl to 8.6.7 #2668 - [radarhere] - -- Depends: Updated libimagequant to 2.10.2 #2660 - [radarhere] - -- Test: Added test for ImImagePlugin tell() #2675 - [radarhere] - -- Test: Additional tests for SGIImagePlugin #2659 - [radarhere] - -- New Image.getchannel method #2661 - [homm] - -- Remove unused im.copy2 and core.copy methods #2657 - [homm] - -- Fast Image.merge() #2677 - [homm] - -- Fast Image.split() #2676 - [homm] - -- Fast image allocation #2655 - [homm] - -- Storage cleanup #2654 - [homm] - -- FLI: Use frame count from FLI header #2674 - [radarhere] - -- Test: Test animated FLI file #2650 - [hugovk] - -- Bug: Fixed uninitialized memory in bc5 decoding #2648 - [ifeherva] - -- Moved SgiImagePlugin save error to before the start of write operations #2646 - [radarhere] - -- Move createfontdatachunk.py so isn't installed globally #2645 - [hugovk] - -- Bug: Fix unexpected keyword argument 'align' #2641 - [hugovk] - -- Add newlines to error message for clarity #2640 - [hugovk] - -- Docs: Updated redirected URL #2637 - [radarhere] - -- Bug: Fix JPEG DPI when EXIF is invalid #2632 - [wiredfool] - -- Bug: Fix for font getsize on empty string #2624 - [radarhere] - -- Docs: Improved ImageDraw documentation #2626 - [radarhere] - -- Docs: Corrected alpha_composite args documentation #2627 - [radarhere] - -- Docs: added the description of the filename attribute to images.rst #2621 - [dasdachs] - -- Dependencies: Updated libimagequant to 2.10.1 #2616 - [radarhere] - -- PDF: Renamed parameter to not shadow built-in dict #2612 - [kijeong] - -4.2.1 (2017-07-06) ------------------- - -- CI: Fix version specification and test on CI for PyPy/Windows #2608 - [wiredfool] - -4.2.0 (2017-07-01) ------------------- - -- Doc: Clarified Image.save:append_images documentation #2604 - [radarhere] - -- CI: Amazon Linux and Centos6 docker images added to Travis CI #2585 - [wiredfool] - -- Image.alpha_composite added #2595 - [wiredfool] - -- Complex Text Support #2576 - [ShamsaHamed, Fahad-Alsaidi, wiredfool] - -- Added threshold parameter to ImageDraw.floodfill #2599 - [nediamond] - -- Added dBATCH parameter to ghostscript command #2588 - [radarhere] - -- JPEG: Adjust buffer size when icc_profile > MAXBLOCK #2596 - [Darou] - -- Specify Pillow Version in one place #2517 - [wiredfool] - -- CI: Change the owner of the TRAVIS_BUILD_DIR, fixing broken docker runs #2587 - [wiredfool] - -- Fix truncated PNG loading for some images, Fix memory leak on truncated PNG images. #2541, #2598 - [homm] - -- Add decompression bomb check to Image.crop #2410 - [wiredfool] - -- ImageFile: Ensure that the ``err_code`` variable is initialized in case of exception. #2363 - [alexkiro] - -- Tiff: Support append_images for saving multipage TIFFs #2406 - [blochl] - -- Doc: Clarify that draft is only implemented for JPEG and PCD #2409 - [wiredfool] - -- Test: MicImagePlugin #2447 - [hugovk] - -- Use round() instead of floor() to eliminate zero coefficients in resample #2558 - [homm] - -- Remove deprecated code #2549 - [hugovk] - -- Added append_images to PDF saving #2526 - [radarhere] - -- Remove unused function core image function new_array #2548 - [hugovk] - -- Remove unnecessary calls to dict.keys() #2551 - [jdufresne] - -- Add more ImageDraw.py tests and remove unused Draw.c code #2533 - [hugovk] - -- Test: More tests for ImageMorph #2554 - [hugovk] - -- Test: McIDAS area file #2552 - [radarhere] - -- Update Feature Detection #2520 - [wiredfool] - -- CI: Update pypy on Travis CI #2573 - [hugovk] - -- ImageMorph: Fix wrong expected size of MRLs read from disk #2561 - [dov] - -- Docs: Update install docs for FreeBSD #2546 - [wiredfool] - -- Build: Ignore OpenJpeg 1.5 on FreeBSD #2544 - [melvyn-sopacua] - -- Remove 'not yet implemented' methods from PIL 1.1.4 #2538 - [hugovk] - -- Dependencies: Update FreeType to 2.8, LibTIFF to 4.0.8 and libimagequant to 2.9.1 #2535 #2537 #2540 - [radarhere] - -- Raise TypeError and not also UnboundLocalError in ImageFile.Parser() #2525 - [joshblum] - -- Test: Use Codecov for coverage #2528 - [hugovk] - -- Use PNG for Image.show() #2527 - [HinTak, wiredfool] - -- Remove WITH_DEBUG compilation flag #2522 - [wiredfool] - -- Fix return value on parameter parse error in _webp.c #2521 - [adw1n] - -- Set executable flag on scripts with shebang line #2295 - [radarhere] - -- Flake8 #2460 - [radarhere] - -- Doc: Release Process Changes #2516 - [wiredfool] - -- CI: Added region for s3 deployment on appveyor #2515 - [wiredfool] - -- Doc: Updated references to point to existing files #2507 - [radarhere] - -- Return copy on Image crop if crop dimensions match the image #2471 - [radarhere] - -- Test: Optimize CI speed #2464, #2466 - [hugovk] - -4.1.1 (2017-04-28) ------------------- - -- Undef PySlice_GetIndicesEx, see https://bugs.python.org/issue29943 #2493 - [cgohlke] - -- Fix for file with DPI in EXIF but not metadata, and XResolution is an int rather than tuple #2484 - [hugovk] - -- Docs: Removed broken download counter badge #2487 - [hugovk] - -- Docs: Fixed rst syntax error #2477 - [thebjorn] - -4.1.0 (2017-04-03) ------------------- - -- Close files after loading if possible #2330 - [homm, wiredfool] - -- Fix Image Access to be reloadable when embedding the Python interpreter #2296 - [wiredfool, cgohlke] - -- Fetch DPI from EXIF if not specified in JPEG header #2449, #2472 - [hugovk] - -- Removed winbuild checksum verification #2468 - [radarhere] - -- Git: Set ContainerIO test file as binary #2469 - [cgohlke] - -- Remove superfluous import of FixTk #2455 - [cgohlke) - -- Fix import of tkinter/Tkinter #2456 - [cgohlke) - -- Pure Python Decoders, including Python decoder to fix for MSP images #1938 - [wiredfool, hugovk] - -- Reorganized GifImagePlugin, fixes #2314. #2374 - [radarhere, wiredfool] - -- Doc: Reordered operating systems in Compatibility Matrix #2436 - [radarhere] - -- Test: Additional tests for BufrStub, Eps, Container, GribStub, IPTC, Wmf, XVThumb, ImageDraw, ImageMorph, ImageShow #2425 - [radarhere] - -- Health fixes #2437 - [radarhere] - -- Test: Correctness tests ContainerIO, XVThumbImagePlugin, BufrStubImagePlugin, GribStubImagePlugin, FitsStubImagePlugin, Hdf5StubImagePlugin, PixarImageFile, PsdImageFile #2443, #2442, #2441, #2440, #2431, #2430, #2428, #2427 - [hugovk] - -- Remove unused imports #1822 - [radarhere] - -- Replaced KeyError catch with dictionary get method #2424 - [radarhere] - -- Test: Removed unrunnable code in test_image_toqimage #2415 - [hugovk] - -- Removed use of spaces in TIFF kwargs names, deprecated in 2.7 #1390 - [radarhere] - -- Removed deprecated ImageDraw setink, setfill, setfont methods #2220 - [jdufresne] - -- Send unwanted subprocess output to /dev/null #2253 - [jdufresne] - -- Fix division by zero when creating 0x0 image from numpy array #2419 - [hugovk] - -- Test: Added matrix convert tests #2381 - [hugovk] - -- Replaced broken URL to partners.adobe.com #2413 - [radarhere] - -- Removed unused private functions in setup.py and build_dep.py #2414 - [radarhere] - -- Test: Fixed Qt tests for QT5 and saving 1 bit PNG #2394 - [wiredfool] - -- Test: docker builds for Arch and Debian Stretch #2394 - [wiredfool] - -- Updated libwebp to 0.6.0 on appveyor #2395 - [radarhere] - -- More explicit error message when saving to a file with invalid extension #2399 - [ces42] - -- Docs: Update some http urls to https #2403 - [hugovk] - -- Preserve aux/alpha channels when performing Imagecms transforms #2355 - [gunjambi] - -- Test linear and radial gradient effects #2382 - [hugovk] - -- Test ImageDraw.Outline and and ImageDraw.Shape #2389 - [hugovk] - -- Added PySide to ImageQt documentation #2392 - [radarhere] - -- BUG: Empty image mode no longer causes a crash #2380 - [evalapply] - -- Exclude .travis and contents from manifest #2386 - [radarhere] - -- Remove 'MIT-like' from license #2145 - [wiredfool] - -- Tests: Add tests for several Image operations #2379 - [radarhere] - -- PNG: Moved iCCP chunk before PLTE chunk when saving as PNG, restricted chunks known value/ordering #2347 - [radarhere] - -- Default to inch-interpretation for missing ResolutionUnit in TiffImagePlugin #2365 - [lambdafu] - -- Bug: Fixed segfault when using ImagingTk on pypy Issue #2376, #2359. - [wiredfool] - -- Bug: Fixed Integer overflow using ImagingTk on 32 bit platforms #2359 - [wiredfool, QuLogic] - -- Tests: Added docker images for testing alternate platforms. See also https://github.com/python-pillow/docker-images. #2368 - [wiredfool] - -- Removed PIL 1.0 era TK readme that concerns Windows 95/NT #2360 - [wiredfool] - -- Prevent ``nose -v`` printing docstrings #2369 - [hugovk] - -- Replaced absolute PIL imports with relative imports #2349 - [radarhere] - -- Added context managers for file handling #2307 - [radarhere] - -- Expose registered file extensions in Image #2343 - [iggomez, radarhere] - -- Make mode descriptor cache initialization thread-safe. #2351 - [gunjambi] - -- Updated Windows test dependencies: Freetype 2.7.1, zlib 1.2.11 #2331, #2332, #2357 - [radarhere] - -- Followed upstream pngquant packaging reorg to libimagquant #2354 - [radarhere] - -- Fix invalid string escapes #2352 - [hugovk] - -- Add test for crop operation with no argument #2333 - [radarhere] - -4.0.0 (2017-01-01) ------------------- - -- Refactor out postprocessing hack to load_end in PcdImageFile - [wiredfool] - -- Add center and translate option to Image.rotate. #2328 - [lambdafu] - -- Test: Relax WMF test condition, fixes #2323. #2327 - [wiredfool] - -- Allow 0 size images, Fixes #2259, Reverts to pre-3.4 behavior. #2262 - [wiredfool] - -- SGI: Save uncompressed SGI/BW/RGB/RGBA files #2325 - [jbltx] - -- Depends: Updated pngquant to 2.8.2 #2319 - [radarhere] - -- Test: Added correctness tests for opening SGI images #2324 - [wiredfool] - -- Allow passing a list or tuple of individual frame durations when saving a GIF #2298 - [Xdynix] - -- Unified different GIF optimize conditions #2196 - [radarhere] - -- Build: Refactor dependency installation #2305 - [hugovk] - -- Test: Add python 3.6 to travis, tox #2304 - [hugovk] - -- Test: Fix coveralls coverage for Python+C #2300 - [hugovk] - -- Remove executable bit and shebang from OleFileIO.py #2308 - [jwilk, radarhere] - -- PyPy: Buffer interface workaround #2294 - [wiredfool] - -- Test: Switch to Ubuntu Trusty 14.04 on Travis CI #2294 - -- Remove vendored version of olefile Python package in favor of upstream #2199 - [jdufresne] - -- Updated comments to use print as a function #2234 - [radarhere] - -- Set executable flag on selftest.py, setup.py and added shebang line #2282, #2277 - [radarhere, homm] - -- Test: Increase epsilon for FreeType 2.7 as rendering is slightly different. #2286 - [hugovk] - -- Test: Faster assert_image_similar #2279 - [homm] - -- Removed deprecated internal "stretch" method #2276 - [homm] - -- Removed the handles_eof flag in decode.c #2223 - [wiredfool] - -- Tiff: Fix for writing Tiff to BytesIO using libtiff #2263 - [wiredfool] - -- Doc: Design docs #2269 - [wiredfool] - -- Test: Move tests requiring libtiff to test_file_libtiff #2273 - [wiredfool] - -- Update Maxblock heuristic #2275 - [wiredfool] - -- Fix for 2-bit palette corruption #2274 - [pdknsk, wiredfool] - -- Tiff: Update info.icc_profile when using libtiff reader. #2193 - [lambdafu] - -- Test: Fix bug in test_ifd_rational_save when libtiff is not available #2270 - [ChristopherHogan] - -- ICO: Only save relevant sizes #2267 - [hugovk] - -- ICO: Allow saving .ico files of 256x256 instead of 255x255 #2265 - [hugovk] - -- Fix TIFFImagePlugin ICC color profile saving. #2087 - [cskau] - -- Doc: Improved description of ImageOps.deform resample parameter #2256 - [radarhere] - -- EMF: support negative bounding box coordinates #2249 - [glexey] - -- Close file if opened in WalImageFile #2216 - [radarhere] - -- Use Image._new() instead of _makeself() #2248 - [homm] - -- SunImagePlugin fixes #2241 - [wiredfool] - -- Use minimal scale for jpeg drafts #2240 - [homm] - -- Updated dependency scripts to use FreeType 2.7, OpenJpeg 2.1.2, WebP 0.5.2 and Tcl/Tk 8.6.6 #2235, #2236, #2237, #2290, #2302 - [radarhere] - -- Fix "invalid escape sequence" bytestring warnings in Python 3.6 #2186 - [timgraham] - -- Removed support for Python 2.6 and Python 3.2 #2192 - [jdufresne] - -- Setup: Raise custom exceptions when required/requested dependencies are not found #2213 - [wiredfool] - -- Use a context manager in FontFile.save() to ensure file is always closed #2226 - [jdufresne] - -- Fixed bug in saving to fp-objects in Python >= 3.4 #2227 - [radarhere] - -- Use a context manager in ImageFont._load_pilfont() to ensure file is always closed #2232 - [jdufresne] - -- Use generator expressions instead of list comprehension #2225 - [jdufresne] - -- Close file after reading in ImagePalette.load() #2215 - [jdufresne] - -- Changed behaviour of default box argument for paste method to match docs #2211 - [radarhere] - -- Add support for another BMP bitfield #2221 - [jmerdich] - -- Added missing top-level test __main__ #2222 - [radarhere] - -- Replaced range(len()) #2197 - [radarhere] - -- Fix for ImageQt Segfault, fixes #1370 #2182 - [wiredfool] - -- Setup: Close file in setup.py after finished reading #2208 - [jdufresne] - -- Setup: optionally use pkg-config (when present) to detect dependencies #2074 - [garbas] - -- Search for tkinter first in builtins #2210 - [matthew-brett] - -- Tests: Replace try/except/fail pattern with TestCase.assertRaises() #2200 - [jdufresne] - -- Tests: Remove unused, open files at top level of tests #2188 - [jdufresne] - -- Replace type() equality checks with isinstance #2184 - [jdufresne] - -- Doc: Move ICO out of the list of read-only file formats #2180 - [alexwlchan] - -- Doc: Fix formatting, too-short title underlines and malformed table #2175 - [hugovk] - -- Fix BytesWarnings #2172 - [jdufresne] - -- Use Integer division to eliminate deprecation warning. #2168 - [mastermatt] - -- Doc: Update compatibility matrix - [daavve, wiredfool] - - -3.4.2 (2016-10-18) ------------------- - -- Fix Resample coefficient calculation #2162 - [homm] - - -3.4.1 (2016-10-04) ------------------- - -- Allow lists as arguments for Image.new() #2149 - [homm] - -- Fix fix for map.c overflow #2151 (also in 3.3.3) - [wiredfool] - -3.4.0 (2016-10-03) ------------------- - -- Removed Image.core.open_ppm, added negative image size checks in Image.py. #2146 - [wiredfool] - -- Windows build: fetch dependencies from pillow-depends #2095 - [hugovk] - -- Add TIFF save_all writer. #2140 - [lambdafu, vashek] - -- Move libtiff fd duplication to _load_libtiff #2141 - [sekrause] - -- Speed up GIF save optimization step, fixes #2093. #2133 - [wiredfool] - -- Fix for ImageCms Segfault, Issue #2037. #2131 - [wiredfool] - -- Make Image.crop an immediate operation, not lazy. #2138 - [wiredfool] - -- Skip empty values in ImageFileDirectory #2024 - [homm] - -- Force reloading palette when using mmap in ImageFile. #2139 - [lambdafu] - -- Fix "invalid escape sequence" warning in Python 3.6 #2136 - [timgraham] - -- Update documentation about drafts #2137 - [radarhere] - -- Converted documentation parameter format, comments to docstrings #2021 - [radarhere] - -- Fixed typos #2128 #2142 - [radarhere] - -- Renamed references to OS X to macOS #2125 2130 - [radarhere] - -- Use truth value when checking for progressive and optimize option on save #2115, #2129 - [radarhere] - -- Convert DPI to ints when saving as JPEG #2102 - [radarhere] - -- Added append_images parameter to GIF saving #2103 - [radarhere] - -- Speedup paste with masks up to 80% #2015 - [homm] - -- Rewrite DDS decoders in C, add DXT3 and BC7 decoders #2068 - [Mischanix] - -- Fix PyArg_ParseTuple format in getink() #2070 - [arjennienhuis] - -- Fix saving originally missing TIFF tags. #2111 - [anntzer] - -- Allow pathlib.Path in Image.open on Python 2.7 #2110 - [patricksnape] - -- Use modern base64 interface over deprecated #2121 - [hugovk] - -- ImageColor.getrgb hexadecimal RGBA #2114 - [homm] - -- Test fix for bigendian machines #2092 - [wiredfool] - -- Resampling lookups, trailing empty coefficients, precision #2008 - [homm] - -- Add (un)packing between RGBA and BGRa #2057 - [arjennienhuis] - -- Added return for J2k (and fpx) Load to return a pixel access object #2061 - [wiredfool] - -- Skip failing numpy tests on Pypy <= 5.3.1 #2090 - [arjennienhuis] - -- Show warning when trying to save RGBA image as JPEG #2010 - [homm] - -- Respect pixel centers during transform #2022 - [homm] - -- TOC for supported file formats #2056 - [polarize] - -- Fix conversion of bit images to numpy arrays Fixes #350, #2058 - [matthew-brett] - -- Add ImageOps.scale to expand or contract a PIL image by a factor #2011 - [vlmath] - -- Flake8 fixes #2050 - [hugovk] - -- Updated freetype to 2.6.5 on Appveyor builds #2035 - [radarhere] - -- PCX encoder fixes #2023, pr #2041 - [homm] - -- Docs: Windows console prompts are > #2031 - [techtonik] - -- Expose Pillow package version as PIL.__version__ #2027 - [techtonik] - -- Add Box and Hamming filters for resampling #1959 - [homm] - -- Retain a reference to core image object in PyAccess #2009 - [homm] - -3.3.3 (2016-10-04) ------------------- - -- Fix fix for map.c overflow #2151 - [wiredfool] - -3.3.2 (2016-10-03) ------------------- - -- Fix negative image sizes in Storage.c #2146 - [wiredfool] - -- Fix integer overflow in map.c #2146 - [wiredfool] - -3.3.1 (2016-08-18) ------------------- - -- Fix C90 compilation error for Tcl / Tk rewrite #2033 - [matthew-brett] - -- Fix image loading when rotating by 0 deg #2052 - [homm] - -3.3.0 (2016-07-01) ------------------- - -- Fixed enums for Resolution Unit and Predictor in TiffTags.py #1998 - [wiredfool] - -- Fix issue converting P mode to LA #1986 - [didrix] - -- Moved test_j2k_overflow to check_j2k_overflow, prevent DOS of our 32bit testing machines #1995 - [wiredfool] - -- Skip CRC checks in PNG files when LOAD_TRUNCATED_IMAGES is enabled #1991 - [kkopachev] - -- Added CMYK mode for opening EPS files #1826 - [radarhere] - -- Docs: OSX build instruction clarification #1994 - [wiredfool] - -- Docs: Filter comparison table #1993 - [homm] - -- Removal of pthread based Incremental.c, new interface for file decoders/encoders to access the python file. Fixes assorted J2k Hangs. #1934 - [wiredfool] - -- Skip unnecessary passes when resizing #1954 - [homm] - -- Removed duplicate code in ImagePalette #1832 - [radarhere] - -- test_imagecms: Reduce precision of extended info due to 32 bit machine precision #1990 - [AbdealiJK] - -- Binary Tiff Metadata/ICC profile. #1988 - [wiredfool] - -- Ignore large text blocks in PNG if LOAD_TRUNCATED_IMAGES is enabled #1970 - [homm] - -- Replace index = index+1 in docs with +=1 - [cclauss] - -- Skip extra 0xff00 in jpeg #1977 - [kkopachev] - -- Use bytearray for palette mutable storage #1985 - [radarhere, wiredfool] - -- Added additional uint modes for Image.fromarray, more extensive tests of fromarray #1984 - [mairsbw, wiredfool] - -- Fix for program importing PyQt4 when PyQt5 also installed #1942 - [hugovk] - -- Changed depends/install_*.sh urls to point to github pillow-depends repo #1983 - [wiredfool] - -- Allow ICC profile from ``encoderinfo`` while saving PNGs #1909 - [homm] - -- Fix integer overflow on ILP32 systems (32-bit Linux). #1975 - [lambdafu] - -- Change function declaration to match Tcl_CmdProc type #1966 - [homm] - -- Integer overflow checks on all calls to \*alloc #1781 - [wiredfool] - -- Change equals method on Image so it short circuits #1967 - [mattBoros] - -- Runtime loading of TCL/TK libraries, eliminating build time dependency. #1932 - [matthew-brett] - -- Cleanup of transform methods #1941 - [homm] - -- Fix "Fatal Python error: UNREF invalid object" in debug builds #1936 - [wiredfool] - -- Setup fixes for Alpine linux #1937 - [wiredfool] - -- Split resample into horizontal + vertical passes #1933 - [homm] - -- Box blur with premultiplied alpha #1914 - [homm] - -- Add libimagequant support in quantize() #1889 - [rr-] - -- Added internal Premultiplied luminosity (La) mode #1912 - [homm] - -- Fixed point integer resample #1881 - [homm] - -- Removed docs/BUILDME script #1924 - [radarhere] - -- Moved comments to docstrings #1926 - [hugovk] - -- Include Python.h before wchar.h so _GNU_SOURCE is set consistently #1906 - [hugovk] - -- Updated example decoder in documentation #1899 - [radarhere] - -- Added support for GIF comment extension #1896 - [radarhere] - -- Removed support for pre- 1.5.2 list form of Image info in Image.new #1897 - [radarhere] - -- Fix typos in TIFF tags #1918 - [radarhere] - -- Skip tests that require libtiff if it is not installed #1893 (fixes #1866) - [wiredfool] - -- Skip test when icc profile is not available, fixes #1887. #1892 - [doko42] - -- Make deprecated functions raise NotImplementedError instead of Exception. #1862, #1890 - [daniel-leicht, radarhere] - -- Replaced os.system with subprocess.call in setup.py #1879 - [radarhere] - -- Corrected Image show documentation #1886 - [radarhere] - -- Added check for executable permissions to ImageShow #1880 - [radarhere] - -- Fixed tutorial code and added explanation #1877 - [radarhere] - -- Added OS X support for ImageGrab grabclipboard #1837 - [radarhere] - -- Combined duplicate code in ImageTk #1856 - [radarhere] - -- Added --disable-platform-guessing option to setup.py build extension #1861 - [angeloc] - -- Fixed loading Transparent PNGs with a transparent black color #1840 - [olt] - -- Add support for LA mode in Image.fromarray #1865 - [pierriko] - -- Make ImageFile load images in read-only mode #1864 - [hdante] - -- Added _accept hook for XVThumbImagePlugin #1853 - [radarhere] - -- Test TIFF with LZW compression #1855, TGA RLE file #1854 - [hugovk] - -- Improved SpiderImagePlugin help text #1863 - [radarhere] - -- Updated Sphinx project description #1870 - [radarhere] - -- Remove support for Python 3.0 from _imaging.c #1851 - [radarhere] - -- Jpeg qtables are unsigned chars #1814, #1921 - [thebostik] - -- Added additional EXIF tags #1841, TIFF Tags #1821 - [radarhere] - -- Changed documentation to refer to ImageSequence Iterator #1833 - [radarhere] - -- Fix Fedora prerequisites in installation docs, depends script #1842 - [living180] - -- Added _accept hook for PixarImagePlugin #1843 - [radarhere] - -- Removed outdated scanner classifier #1823 - [radarhere] - -- Combined identical error messages in _imaging #1825 - [radarhere] - -- Added debug option for setup.py to trace header and library finding #1790 - [wiredfool] - -- Fix doc building on travis #1820, #1844 - [wiredfool] - -- Fix for DIB/BMP images #1813, #1847 - [wiredfool] - -- Add PixarImagePlugin file extension #1809 - [radarhere] - -- Catch struct.errors when verifying png files #1805 - [wiredfool] - -- SpiderImagePlugin: raise an error when seeking in a non-stack file #1794 - [radarhere, jmichalon] - -- Added support for 2/4 bpp Tiff grayscale images #1789 - [zwhfly] - -- Removed unused variable from selftest #1788 - [radarhere] - -- Added warning for as_dict method (deprecated in 3.0.0) #1799 - [radarhere] - -- Removed powf support for older Python versions #1784 - [radarhere] - -- Health fixes #1625 #1903 - [radarhere] - -3.2.0 (2016-04-01) ------------------- - -- Added install docs for Fedora 23 and FreeBSD #1729, #1739, #1792 - [koobs, zandermartin, wiredfool] - -- Fixed TIFF multiframe load when the frames have different compression types #1782 - [radarhere, geka000] - -- Added __copy__ method to Image #1772 - [radarhere] - -- Updated dates in PIL license in OleFileIO README #1787 - [radarhere] - -- Corrected Tiff tag names #1786 - [radarhere] - -- Fixed documented name of JPEG property #1783 - [radarhere] - -- Fixed UnboundLocalError when loading a corrupt jpeg2k file #1780 - [wiredfool] - -- Fixed integer overflow in path.c #1773 - [wiredfool, nedwill] - -- Added debug to command line help text for pilprint #1766 - [radarhere] - -- Expose many more fields in ICC Profiles #1756 - [lambdafu] - -- Documentation changes, URL update, transpose, release checklist - [radarhere] - -- Fixed saving to nonexistent files specified by pathlib.Path objects #1748 (fixes #1747) - [radarhere] - -- Round Image.crop arguments to the nearest integer #1745 (fixes #1744) - [hugovk] - -- Fix uninitialized variable warning in _imaging.c:getink #1663 (fixes #486) - [wiredfool] - -- Disable multiprocessing install on cygwin #1700 (fixes #1690) - [wiredfool] - -- Fix the error reported when libz is not found #1764 - [wiredfool] - -- More general error check to avoid Symbol not found: _PyUnicodeUCS2_AsLatin1String on OS X #1761 - [wiredfool] - -- Added py35 to tox envlist #1724 - [radarhere] - -- Fix EXIF tag name typos #1736 - [zarlant, radarhere] - -- Updated freetype to 2.6.3, Tk/Tcl to 8.6.5 and 8.5.19 #1725, #1752 - [radarhere] - -- Add a loader for the FTEX format from Independence War 2: Edge of Chaos #1688 - [jleclanche] - -- Improved alpha_composite documentation #1698 - [radarhere] - -- Extend ImageDraw.text method to pass on multiline_text method specific arguments #1647 - [radarhere] - -- Allow ImageSequence to seek to zero #1686 - [radarhere] - -- ImageSequence Iterator is now an iterator #1649 - [radarhere] - -- Updated windows test builds to jpeg9b #1673 - [radarhere] - -- Fixed support for .gbr version 1 images, added support for version 2 in GbrImagePlugin #1653 - [wiredfool] - -- Clarified which YCbCr format is used #1677 - [radarhere] - -- Added TiffTags documentation, Moved windows build documentation to winbuild/ #1667 - [wiredfool] - -- Add tests for OLE file based formats #1678 - [radarhere] - -- Add TIFF IFD test #1671 - [radarhere] - -- Add a basic DDS image plugin with more tests #1654 - [jleclanche, hugovk, wiredfool] - -- Fix incorrect conditional in encode.c #1638 - [manisandro] - - -3.1.2 (2016-04-01) ------------------- - -- Fixed an integer overflow in Jpeg2KEncode.c causing a buffer overflow. CVE-2016-3076 - [wiredfool] - -3.1.1 (2016-02-04) ------------------- - -- Fixed an integer overflow in Resample.c causing writes in the Python heap. - [nedwill] - -- Fixed a buffer overflow in PcdDecode.c causing a segfault when opening PhotoCD files. CVE-2016-2533 - [wiredfool] - -- Fixed a buffer overflow in FliDecode.c causing a segfault when opening FLI files. CVE-2016-0775 - [wiredfool] - -- Fixed a buffer overflow in TiffDecode.c causing an arbitrary amount of memory to be overwritten when opening a specially crafted invalid TIFF file. CVE-2016-0740 - [wiredfool] - - -3.1.0 (2016-01-04) ------------------- - -- Fixing test failures on Python 2.6/Windows #1633 - [wiredfool] - -- Limit metadata tags when writing using libtiff #1620 - [wiredfool] - -- Rolling back exif support to pre-3.0 format #1627 - [wiredfool] - -- Fix Divide by zero in Exif, add IFDRational class #1531 - [wiredfool] - -- Catch the IFD error near the source #1622 - [wiredfool] - -- Added release notes for 3.1.0 #1623 - [radarhere] - -- Updated spacing to be consistent between multiline methods #1624 - [radarhere] - -- Let EditorConfig take care of some basic formatting #1489 - [hugovk] - -- Restore gpsexif data to the v1 form #1619 - [wiredfool] - -- Add /usr/local include and library directories for freebsd #1613 - [leforestier] - -- Updated installation docs for new versions of dependencies #1611 - [radarhere] - -- Removed unrunnable test file #1610 - [radarhere] - -- Changed register calls to use format property #1608 - [radarhere] - -- Added field type constants to TiffTags #1596 - [radarhere] - -- Allow saving RowsPerStrip with libtiff #1594 - [wiredfool] - -- Enabled conversion to numpy array for HSV images #1578 - [cartisan] - -- Changed some urls in the docs to use https #1580 - [hugovk] - -- Removed logger.exception from ImageFile.py #1590 - [radarhere] - -- Removed warnings module check #1587 - [radarhere] - -- Changed arcs, chords and pie slices to use floats #1577 - [radarhere] - -- Update unit test asserts #1584, #1598 - [radarhere] - -- Fix command to invoke ghostscript for eps files #1478 - [baumatron, radarhere] - -- Consistent multiline text spacing #1574 - [wiredfool, hugovk] - -- Removed unused lines in BDFFontFile #1530 - [radarhere] - -- Changed ImageQt import of Image #1560 - [radarhere, ericfrederich] - -- Throw TypeError if no cursors were found in .cur file #1556 - [radarhere] - -- Fix crash in ImageTk.PhotoImage on win-amd64 #1553 - [cgohlke] - -- ExtraSamples tag should be a SHORT, not a BYTE #1555 - [Nexuapex] - -- Docs and code health fixes #1565 #1566 #1581 #1586 #1591 #1621 - [radarhere] - -- Updated freetype to 2.6.2 #1564 - [radarhere] - -- Updated WebP to 0.5.0 for Travis #1515 #1609 - [radarhere] - -- Fix missing 'version' key value in __array_interface__ #1519 - [mattip] - -- Replaced os.popen with subprocess.Popen to pilprint script #1523 - [radarhere] - -- Catch OverflowError in SpiderImagePlugin #1545 - [radarhere, MrShark] - -- Fix the definition of icc_profile in TiffTags #1539 - [wiredfool] - -- Remove old _imagingtiff.c and pilplus stuff #1499 - [hugovk] - -- Fix Exception when requiring jpeg #1501 - [hansmosh] - -- Dependency scripts for Debian and Ubuntu #1486 - [wiredfool] - -- Added Usage message to painter script #1482 - [radarhere] - -- Add tag info for iccprofile, fixes #1462. #1465 - [wiredfool] - -- Added some requirements for make release-test #1451 - [wiredfool] - -- Flatten tiff metadata value SAMPLEFORMAT to initial value #1467 (fixes #1466) - [wiredfool] - -- Fix handling of pathlib in Image.save #1464 (fixes #1460) - [wiredfool] - -- Make tests more robust #1469 - [hugovk] - -- Use correctly sized pointers for windows handle types #1458 - [nu744] - -3.0.0 (2015-10-01) ------------------- - -- Check flush method existence for file-like object #1398 - [mrTable, radarhere] - -- Added PDF multipage saving #1445 - [radarhere] - -- Removed deprecated code, Image.tostring, Image.fromstring, Image.offset, ImageDraw.setink, ImageDraw.setfill, ImageFileIO, ImageFont.FreeTypeFont and ImageFont.truetype ``file`` kwarg, ImagePalette private _make functions, ImageWin.fromstring and ImageWin.tostring #1343 - [radarhere] - -- Load more broken images #1428 - [homm] - -- Require zlib and libjpeg #1439 - [wiredfool] - -- Preserve alpha when converting from a QImage to a Pillow Image by using png instead of ppm #1429 - [ericfrederich] - -- Qt needs 32 bit aligned image data #1430 - [ericfrederich] - -- Tiff ImageFileDirectory rewrite #1419 - [anntzer, wiredfool, homm] - -- Removed spammy debug logging #1423 - [wiredfool] - -- Save as GiF89a with support for animation parameters #1384 - [radarhere] - -- Correct convert matrix docs #1426 - [wiredfool] - -- Catch TypeError in _getexif #1414 - [radarhere, wiredfool] - -- Fix for UnicodeDecodeError in TiffImagePlugin #1416 - [bogdan199, wiredfool] - -- Dedup code in image.open #1415 - [wiredfool] - -- Skip any number extraneous chars at the end of JPEG chunks #1337 - [homm] - -- Single threaded build for pypy3, refactor #1413 - [wiredfool] - -- Fix loading of truncated images with LOAD_TRUNCATED_IMAGES enabled #1366 - [homm] - -- Documentation update for concepts: bands #1406 - [merriam] - -- Add Solaris/SmartOS include and library directories #1356 - [njones11] - -- Improved handling of getink color #1387 - [radarhere] - -- Disable compiler optimizations for topalette and tobilevel functions for all msvc versions #1402 (fixes #1357) - [cgohlke] - -- Skip ImageFont_bitmap test if _imagingft C module is not installed #1409 - [homm] - -- Add param documentation to ImagePalette #1381 - [bwrsandman] - -- Corrected scripts path #1407 - [radarhere] - -- Updated libtiff to 4.0.6 #1405, #1421 - [radarhere] - -- Updated Platform Support for Yosemite #1403 - [radarhere] - -- Fixed infinite loop on truncated file #1401 - [radarhere] - -- Check that images are L mode in ImageMorph methods #1400 - [radarhere] - -- In tutorial of pasting images, add to mask text #1389 - [merriam] - -- Style/health fixes #1391, #1397, #1417, #1418 - [radarhere] - -- Test on Python 3.5 dev and 3.6 nightly #1361 - [hugovk] - -- Fix fast rotate operations #1373 - [radarhere] - -- Added support for pathlib Path objects to open and save #1372 - [radarhere] - -- Changed register calls to use format property #1333 - [radarhere] - -- Added support for ImageGrab.grab to OS X #1367, #1443 - [radarhere, hugovk] - -- Fixed PSDraw stdout Python 3 compatibility #1365 - [radarhere] - -- Added Python 3.3 to AppVeyor #1363 - [radarhere] - -- Treat MPO with unknown header as base JPEG file #1350 - [hugovk, radarhere] - -- Added various tests #1330, #1344 - [radarhere] - -- More ImageFont tests #1327 - [hugovk] - -- Use logging instead of print #1207 - [anntzer] - -2.9.0 (2015-07-01) ------------------- - -- Added test for GimpPaletteFile #1324 - [radarhere] - -- Merged gifmaker script to allow saving of multi-frame GIF images #1320 - [radarhere] - -- Added is_animated property to multi-frame formats #1319 - [radarhere] - -- Fixed ValueError in Python 2.6 #1315 #1316 - [cgohlke, radarhere] - -- Fixed tox test script path #1308 - [radarhere] - -- Added width and height properties #1304 - [radarhere] - -- Update tiff and tk tcl 8.5 versions #1303 - [radarhere, wiredfool] - -- Add functions to convert: Image <-> QImage; Image <-> QPixmap #1217 - [radarhere, rominf] - -- Remove duplicate code in gifmaker script #1294 - [radarhere] - -- Multiline text in ImageDraw #1177 - [allo-, radarhere] - -- Automated Windows CI/build support #1278 - [wiredfool] - -- Removed support for Tk versions earlier than 8.4 #1288 - [radarhere] - -- Fixed polygon edge drawing #1255 (fixes #1252) - [radarhere] - -- Check prefix length in _accept methods #1267 - [radarhere] - -- Register MIME type for BMP #1277 - [coldmind] - -- Adjusted ImageQt use of unicode() for 2/3 compatibility #1218 - [radarhere] - -- Identify XBM file created with filename including underscore #1230 (fixes #1229) - [hugovk] - -- Copy image when saving in GifImagePlugin #1231 (fixes #718) - [radarhere] - -- Removed support for FreeType 2.0 #1247 - [radarhere] - -- Added background saving to GifImagePlugin #1273 - [radarhere] - -- Provide n_frames attribute to multi-frame formats #1261 - [anntzer, radarhere] - -- Add duration and loop set to GifImagePlugin #1172, #1269 - [radarhere] - -- Ico files are little endian #1232 - [wiredfool] - -- Upgrade olefile from 0.30 to 0.42b #1226 - [radarhere, decalage2] - -- Setting transparency value to 0 when the tRNS contains only null byte(s) #1239 - [juztin] - -- Separated out feature checking from selftest #1233 - [radarhere] - -- Style/health fixes - [radarhere] - -- Update WebP from 0.4.1 to 0.4.3 #1235 - [radarhere] - -- Release GIL during image load (decode) #1224 - [lkesteloot] - -- Added icns save #1185 - [radarhere] - -- Fix putdata memory leak #1196 - [benoit-pierre] - -- Keep user-specified ordering of icon sizes #1193 - [karimbahgat] - -- Tiff: allow writing floating point tag values #1113 - [bpedersen2] - -2.8.2 (2015-06-06) ------------------- - -- Bug fix: Fixed Tiff handling of bad EXIF data - [radarhere] - -2.8.1 (2015-04-02) ------------------- - -- Bug fix: Catch struct.error on invalid JPEG, fixes #1163. #1165 - [wiredfool, hugovk] - -2.8.0 (2015-04-01) ------------------- - -- Fix 32-bit BMP loading (RGBA or RGBX) #1125 - [artscoop] - -- Fix UnboundLocalError in ImageFile #1131 - [davarisg] - -- Re-enable test image caching #982 - [hugovk, homm] - -- Fix: Cannot identify EPS images #1152 (fixes #1104) - [hugovk] - -- Configure setuptools to run nosetests, fixes #729 - [aclark4life] - -- Style/health fixes - [radarhere, hugovk] - -- Add support for HTTP response objects to Image.open() #1151 - [mfitzp] - -- Improve reference docs for PIL.ImageDraw.Draw.pieslice() #1145 - [audreyr] - -- Added copy method font_variant() and accessible properties to truetype() #1123 - [radarhere] - -- Fix ImagingEffectNoise #1128 - [hugovk] - -- Remove unreachable code #1126 - [hugovk] - -- Let Python do the endian stuff + tests #1121 - [amoibos, radarhere] - -- Fix webp decode memory leak #1114 - [benoit-pierre] - -- Fast path for opaque pixels in RGBa unpacker #1088 - [bgilbert] - -- Enable basic support for 'RGBa' raw encoding/decoding #1096 - [immerrr] - -- Fix pickling L mode images with no palette, #1095 - [hugovk] - -- iPython display hook #1091 - [wiredfool] - -- Adjust buffer size when quality=keep #1079 (fixes #148 again) - [wiredfool] - -- Fix for corrupted bitmaps embedded in truetype fonts #1072 - [jackyyf, wiredfool] - -2.7.0 (2015-01-01) ------------------- - -- Split Sane into a separate repo: https://github.com/python-pillow/Sane - [hugovk] - -- Look for OS X and Linux fonts in common places #1054 - [charleslaw] - -- Fix CVE-2014-9601, potential PNG decompression DOS #1060 - [wiredfool] - -- Use underscores, not spaces, in TIFF tag kwargs #1044, #1058 - [anntzer, hugovk] - -- Update PSDraw for Python3, add tests #1055 - [hugovk] - -- Use Bicubic filtering by default for thumbnails. Don't use Jpeg Draft mode for thumbnails #1029 - [homm] - -- Fix MSVC compiler error: Use Py_ssize_t instead of ssize_t #1051 - [cgohlke] - -- Fix compiler error: MSVC needs variables defined at the start of the block #1048 - [cgohlke] - -- The GIF Palette optimization algorithm is only applicable to mode='P' or 'L' #993 - [moriyoshi] - -- Use PySide as an alternative to PyQt4/5 #1024 - [holg] - -- Replace affine-based im.resize implementation with convolution-based im.stretch #997 - [homm] - -- Replace Gaussian Blur implementation with iterated fast box blur. #961 Note: Radius parameter is interpreted differently than before. - [homm] - -- Better docs explaining import _imaging failure #1016, build #1017, mode #1018, PyAccess, PixelAccess objects #1019 Image.quantize #1020 and Image.save #1021 - [wiredfool] - -- Fix for saving TIFF image into an io.BytesIO buffer #1011 - [mfergie] - -- Fix antialias compilation on debug versions of Python #1010 - [wiredfool] - -- Fix for Image.putdata segfault #1009 - [wiredfool] - -- Ico save, additional tests #1007 - [exherb] - -- Use PyQt4 if it has already been imported, otherwise prefer PyQt5 #1003 - [AurelienBallier] - -- Speedup resample implementation up to 2.5 times #977 - [homm] - -- Speed up rotation by using cache aware loops, added transpose to rotations #994 - [homm] - -- Fix Bicubic interpolation #970 - [homm] - -- Support for 4-bit grayscale TIFF images #980 - [hugovk] - -- Updated manifest #957 - [wiredfool] - -- Fix PyPy 2.4 regression #958 - [wiredfool] - -- Webp Metadata Skip Test comments #954 - [wiredfool] - -- Fixes for things rpmlint complains about #942 - [manisandro] - -2.6.2 (2015-01-01) ------------------- - -- Fix CVE-2014-9601, potential PNG decompression DOS #1060 - [wiredfool] - -- Fix Regression in PyPy 2.4 in streamio #958 - [wiredfool] - -2.6.1 (2014-10-11) ------------------- - -- Fix SciPy regression in Image.resize #945 - [wiredfool] - -- Fix manifest to include all test files. - [aclark4life] - -2.6.0 (2014-10-01) ------------------- - -- Relax precision of ImageDraw tests for x86, GimpGradient for PPC #930 - [wiredfool] - -2.6.0-rc1 (2014-09-29) ----------------------- - -- Use redistributable image for testing #884 - [hugovk] - -- Use redistributable ICC profiles for testing, skip if not available #923 - [wiredfool] - -- Additional documentation for JPEG info and save options #922 - [wiredfool] - -- Fix JPEG Encoding memory leak when exif or qtables were specified #921 - [wiredfool] - -- Image.tobytes() and Image.tostring() documentation update #916 #917 - [mgedmin] - -- On Windows, do not execute convert.exe without specifying path #912 - [cgohlke] - -- Fix msvc build error #911 - [cgohlke] - -- Fix for handling P + transparency -> RGBA conversions #904 - [wiredfool] - -- Retain alpha in ImageEnhance operations #909 - [wiredfool] - -- Jpeg2k Decode/encode memory leak fix #898 - [joshware, wiredfool] - -- EpsFilePlugin Speed improvements #886 - [wiredfool, karstenw] - -- Don't resize if already the right size #892 - [radarhere] - -- Fix for reading multipage TIFFs #885 - [kostrom, wiredfool] - -- Correctly handle saving gray and CMYK JPEGs with quality=keep #857 - [etienned] - -- Correct duplicate Tiff Metadata and Exif tag values - [hugovk] - -- Windows fixes #871 - [wiredfool] - -- Fix TGA files with image ID field #856 - [megabuz] - -- Fixed wrong P-mode of small, unoptimized L-mode GIF #843 - [uvNikita] - -- Fixed CVE-2014-3598, a DOS in the Jpeg2KImagePlugin - [Andrew Drake] - -- Fixed CVE-2014-3589, a DOS in the IcnsImagePlugin - [Andrew Drake] - -- setup.py: Close open file handle before deleting #844 - [divergentdave] - -- Return Profile with Transformed Images #837 - [wiredfool] - -- Changed docstring to refer to the correct function #836 - [MatMoore] - -- Adding coverage support for C code tests #833 - [wiredfool] - -- PyPy performance improvements #821 - [wiredfool] - -- Added support for reading MPO files #822 - [Feneric] - -- Added support for encoding and decoding iTXt chunks #818 - [dolda2000] - -- HSV Support #816 - [wiredfool] - -- Removed unusable ImagePalette.new() - [hugovk] - -- Fix Scrambled XPM #808 - [wiredfool] - -- Doc cleanup - [wiredfool] - -- Fix ``ImageStat`` docs #796 - [akx] - -- Added docs for ExifTags #794 - [Wintermute3] - -- More tests for CurImagePlugin, DcxImagePlugin, Effects.c, GimpGradientFile, ImageFont, ImageMath, ImagePalette, IptcImagePlugin, SpiderImagePlugin, SgiImagePlugin, XpmImagePlugin and _util - [hugovk] - -- Fix return value of FreeTypeFont.textsize() does not include font offsets #784 - [tk0miya] - -- Fix dispose calculations for animated GIFs #765 - [larsjsol] - -- Added class checking to Image __eq__ function #775 - [radarhere, hugovk] - -- Test PalmImagePlugin and method to skip known bad tests #776 - [hugovk, wiredfool] - -2.5.3 (2014-08-18) ------------------- - -- Fixed CVE-2014-3598, a DOS in the Jpeg2KImagePlugin (backport) - [Andrew Drake] - - -2.5.2 (2014-08-13) ------------------- - -- Fixed CVE-2014-3589, a DOS in the IcnsImagePlugin (backport) - [Andrew Drake] - -2.5.1 (2014-07-10) ------------------- - -- Fixed install issue if Multiprocessing.Pool is not available - [wiredfool] - -- 32bit mult overflow fix #782 - [wiredfool] - -2.5.0 (2014-07-01) ------------------- - -- Imagedraw rewrite #737 - [terseus, wiredfool] - -- Add support for multithreaded test execution #755 - [wiredfool] - -- Prevent shell injection #748 - [mbrown1413, wiredfool] - -- Support for Resolution in BMP files #734 - [gcq] - -- Fix error in setup.py for Python 3 #744 - [matthew-brett] - -- Pyroma fix and add Python 3.4 to setup metadata #742 - [wirefool] - -- Top level flake8 fixes #741 - [aclark4life] - -- Remove obsolete Animated Raster Graphics (ARG) support #736 - [hugovk] - -- Fix test_imagedraw failures #727 - [cgohlke] - -- Fix AttributeError: class Image has no attribute 'DEBUG' #726 - [cgohlke] - -- Fix msvc warning: 'inline' : macro redefinition #725 - [cgohlke] - -- Cleanup #654 - [dvska, hugovk, wiredfool] - -- 16-bit monochrome support for JPEG2000 #730 - [videan42] - -- Fixed ImagePalette.save - [brightpisces] - -- Support JPEG qtables #677 - [csinchok] - -- Add binary morphology addon - [dov, wiredfool] - -- Decompression bomb protection #674 - [hugovk] - -- Put images in a single directory #708 - [hugovk] - -- Support OpenJpeg 2.1 #681 - [al45tair, wiredfool] - -- Remove unistd.h #include for all platforms #704 - [wiredfool] - -- Use unittest for tests - [hugovk] - -- ImageCms fixes - [hugovk] - -- Added more ImageDraw tests - [hugovk] - -- Added tests for Spider files - [hugovk] - -- Use libtiff to write any compressed tiff files #669 - [wiredfool] - -- Support for pickling Image objects - [hugovk] - -- Fixed resolution handling for EPS thumbnails #619 - [eliempje] - -- Fixed rendering of some binary EPS files (Issue #302) - [eliempje] - -- Rename variables not to use built-in function names #670 - [hugovk] - -- Ignore junk JPEG markers - [hugovk] - -- Change default interpolation for Image.thumbnail to Image.ANTIALIAS - [hugovk] - -- Add tests and fixes for saving PDFs - [hugovk] - -- Remove transparency resource after P->RGBA conversion - [hugovk] - -- Clean up preprocessor cruft for Windows #652 - [CounterPillow] - -- Adjust Homebrew freetype detection logic #656 - [jacknagel] - -- Added Image.close, context manager support - [wiredfool] - -- Added support for 16 bit PGM files - [wiredfool] - -- Updated OleFileIO to version 0.30 from upstream #618 - [hugovk] - -- Added support for additional TIFF floating point format - [Hijackal] - -- Have the tempfile use a suffix with a dot - [wiredfool] - -- Fix variable name used for transparency manipulations #604 - [nijel] - -2.4.0 (2014-04-01) ------------------- - -- Indexed Transparency handled for conversions between L, RGB, and P modes #574 (fixes #510) - [wiredfool] - -- Conversions enabled from RGBA->P #574 (fixes #544) - [wiredfool] - -- Improved icns support #565 - [al45tair] - -- Fix libtiff leaking open files #580 (fixes #526) - [wiredfool] - -- Fixes for Jpeg encoding in Python 3 #578 (fixes #577) - [wiredfool] - -- Added support for JPEG 2000 #547 - [al45tair] - -- Add more detailed error messages to Image.py #566 - [larsmans] - -- Avoid conflicting _expand functions in PIL & MINGW, fixes #538 - [aclark4life] - -- Merge from Philippe Lagadec’s OleFileIO_PL fork #512 - [vadmium] - -- Fix ImageColor.getcolor #534 - [homm] - -- Make ICO files work with the ImageFile.Parser interface #525 (fixes #522) - [wiredfool] - -- Handle 32bit compiled python on 64bit architecture #521 - [choppsv1] - -- Fix support for characters >128 using .pcf or .pil fonts in Py3k #517 (fixes #505) - [wiredfool] - -- Skip CFFI test earlier if it's not installed #516 - [wiredfool] - -- Fixed opening and saving odd sized .pcx files #535 (fixes #523) - [wiredfool] - -- Fixed palette handling when converting from mode P->RGB->P - [d-schmidt] - -- Fixed saving mode P image as a PNG with transparency = palette color 0 - [d-schmidt] - -- Improve heuristic used when saving progressive and optimized JPEGs with high quality values #504 - [e98cuenc] - -- Fixed DOS with invalid palette size or invalid image size in BMP file - [wiredfool] - -- Added support for BMP version 4 and 5 - [eddwardo, wiredfool] - -- Fix segfault in getfont when passed a memory resident font - [wiredfool] - -- Fix crash on Saving a PNG when icc-profile is None #496 - [brutasse] - -- Cffi+Python implementation of the PixelAccess object - [wiredfool] - -- PixelAccess returns unsigned ints for I16 mode - [wiredfool] - -- Minor patch on booleans + Travis #474 - [sciunto] - -- Look in multiarch paths in GNU platforms #511 - [pinotree] - -- Add arch support for pcc64, s390, s390x, armv7l, aarch64 #475 - [manisandro] - -- Add arch support for ppc - [wiredfool] - -- Correctly quote file names for WindowsViewer command - [cgohlke] - -- Prefer homebrew freetype over X11 freetype (but still allow both) #466 - [dmckeone] - -2.3.2 (2014-08-13) ------------------- - -- Fixed CVE-2014-3589, a DOS in the IcnsImagePlugin (backport) - [Andrew Drake] - -2.3.1 (2014-03-14) ------------------- - -- Fix insecure use of tempfile.mktemp (CVE-2014-1932 CVE-2014-1933) - [wiredfool] - -2.3.0 (2014-01-01) ------------------- - -- Stop leaking filename parameter passed to getfont #459 - [jpharvey] - -- Report availability of LIBTIFF during setup and selftest - [cgohlke] - -- Fix msvc build error C1189: "No Target Architecture" #460 - [cgohlke] - -- Fix memory leak in font_getsize - [wiredfool] - -- Correctly prioritize include and library paths #442 - [ohanar] - -- Image.point fixes for numpy.array and docs #441 - [wiredfool] - -- Save the transparency header by default for PNGs #424 - [wiredfool] - -- Support for PNG tRNS header when converting from RGB->RGBA #423 - [wiredfool] - -- PyQT5 Support #418 - [wiredfool] - -- Updates for saving color tiffs w/compression using libtiff #417 - [wiredfool] - -- 2gigapix image fixes and redux - [wiredfool] - -- Save arbitrary tags in Tiff image files #369 - [wiredfool] - -- Quote filenames and title before using on command line #398 - [tmccombs] - -- Fixed Viewer.show to return properly #399 - [tmccombs] - -- Documentation fixes - [wiredfool] - -- Fixed memory leak saving images as webp when webpmux is available #429 - [cezarsa] - -- Fix compiling with FreeType 2.5.1 #427 - [stromnov] - -- Adds directories for NetBSD #411 - [deepy] - -- Support RGBA TIFF with missing ExtraSamples tag #393 - [cgohlke] - -- Lossless WEBP Support #390 - [wiredfool] - -- Take compression as an option in the save call for tiffs #389 - [wiredfool] - -- Add support for saving lossless WebP. Just pass 'lossless=True' to save() #386 - [liftoff] - -- LCMS support upgraded from version 1 to version 2 #380 (fixes #343) - [wiredfool] - -- Added more raw decoder 16 bit pixel formats #379 - [svanheulen] - -- Document remaining Image* modules listed in PIL handbook - [irksep] - -- Document ImageEnhance, ImageFile, ImageFilter, ImageFont, ImageGrab, ImageMath, and ImageOps - [irksep] - -- Port and update docs for Image, ImageChops, ImageColor, and ImageDraw - [irksep] - -- Move or copy content from README.rst to docs/ - [irksep] - -- Respect CFLAGS/LDFLAGS when searching for headers/libs - [iElectric] - -- Port PIL Handbook tutorial and appendices - [irksep] - -- Alpha Premultiplication support for transform and resize #364 - [wiredfool] - -- Fixes to make Pypy 2.1.0 work on Ubuntu 12.04/64 #359 - [wiredfool] - -2.2.2 (2013-12-11) ------------------- - -- Fix compiling with FreeType 2.5.1 #427 - [stromnov] - -2.2.1 (2013-10-02) ------------------- - -- Error installing Pillow 2.2.0 on Mac OS X (due to hard dep on brew) #357 (fixes #356) - [wiredfool] - -2.2.0 (2013-10-02) ------------------- - -- Bug in image transformations resulting from uninitialized memory #348 (fixes #254) - [nikmolnar] - -- Fix for encoding of b_whitespace #346 (similar to closed issue #272) - [mhogg] - -- Add numpy array interface support for 16 and 32 bit integer modes #347 (fixes #273) - [cgohlke] - -- Partial fix for #290: Add preliminary support for TIFF tags. - [wiredfool] - -- Fix #251 and #326: circumvent classification of pngtest_bad.png as malware - [cgohlke] - -- Add typedef uint64_t for MSVC #339 - [cgohlke] - -- setup.py: better support for C_INCLUDE_PATH, LD_RUN_PATH, etc. #336 (fixes #329) - [nu774] - -- _imagingcms.c: include windef.h to fix build issue on MSVC #335 (fixes #328) - [nu774] - -- Automatically discover homebrew include/ and lib/ paths on OS X #330 - [donspaulding] - -- Fix bytes which should be bytearray #325 - [manisandro] - -- Add respective paths for C_INCLUDE_PATH, LD_RUN_PATH (rpath) to build - if specified as environment variables #324 - [seanupton] - -- Fix #312 + gif optimize improvement - [d-schmidt] - -- Be more tolerant of tag read failures #320 - [ericbuehl] - -- Catch truncated zTXt errors #321 (fixes #318) - [vytisb] - -- Fix IOError when saving progressive JPEGs #313 - [e98cuenc] - -- Add RGBA support to ImageColor #309 - [yoavweiss] - -- Test for ``str``, not ``"utf-8"`` #306 (fixes #304) - [mjpieters] - -- Fix missing import os in _util.py #303 - [mnowotka] - -- Added missing exif tags #300 - [freyes] - -- Fail on all import errors #298, #299 (fixes #297) - [macfreek, wiredfool] - -- Fixed Windows fallback (wasn't using correct file in Windows fonts) #295 - [lmollea] - -- Moved ImageFile and ImageFileIO comments to docstrings #293 - [freyes] - -- Restore compatibility with ISO C #289 - [cgohlke] - -- Use correct format character for C int type #288 - [cgohlke] - -- Allocate enough memory to hold pointers in encode.c #287 - [cgohlke] - -- Fillorder double shuffling bug when FillOrder ==2 and decoding using libtiff #284 (fixes #279) - [wiredfool] - -- Moved Image module comments to docstrings. - [freyes] - -- Add 16-bit TIFF support #277 (fixes #274) - [wiredfool] - -- Ignore high ascii characters in string.whitespace #276 (fixes #272) - [wiredfool] - -- Added clean/build to tox to make it behave like Travis #275 - [freyes] - -- Adding support for metadata in webp images #271 - [heynemann] - -2.1.0 (2013-07-02) ------------------- - -- Add /usr/bin/env python shebangs to all scripts in /Scripts #197 - [mgorny] - -- Add several TIFF decoders and encoders #268 - [megabuz] - -- Added support for alpha transparent webp images. - -- Adding Python 3 support for StringIO. - -- Adding Python3 basestring compatibility without changing basestring. - -- Fix webp encode errors on win-amd64 #259 - [cgohlke] - -- Better fix for ZeroDivisionError in ImageOps.fit for image.size height is 1 #267 - [chrispbailey] - -- Better support for ICO images. - -- Changed PY_VERSION_HEX #190 (fixes #166) - -- Changes to put everything under the PIL namespace #191 - [wiredfool] - -- Changing StringIO to BytesIO. - -- Cleanup whitespace. - [Arfrever] - -- Don't skip 'import site' on initialization when running tests for inplace builds. - [cgohlke] - -- Enable warnings for test suite #227 - [wiredfool] - -- Fix for ZeroDivisionError in ImageOps.fit for image.size == (1,1) #255 - [pterk] - -- Fix for if isinstance(filter, collections.Callable) crash. Python bug #7624 on <2.6.6 - -- Remove double typedef declaration #194 (fixes #193) - [evertrol] - -- Fix msvc compile errors (#230). - -- Fix rendered characters have been chipped for some TrueType fonts - [tk0miya] - -- Fix usage of pilfont.py script #184 - [fabiomcosta] - -- Fresh start for docs, generated by sphinx-apidoc. - -- Introduce --enable-x and fail if it is given and x is not available. - -- Partial work to add a wrapper for WebPGetFeatures to correctly support #220 (fixes #204) - -- Significant performance improvement of ``alpha_composite`` function #156 - [homm] - -- Support explicitly disabling features via --disable-* options #240 - [mgorny] - -- Support selftest.py --installed, fixes #263 - -- Transparent WebP Support #220 (fixes #204) - [euangoddard, wiredfool] - -- Use PyCapsule for py3.1 #238 (fixes #237) - [wiredfool] - -- Workaround for: https://bugs.python.org/issue16754 in 3.2.x < 3.2.4 and 3.3.0. - -2.0.0 (2013-03-15) ------------------- - -.. Note:: Special thanks to Christoph Gohlke and Eric Soroos for assisting with a pre-PyCon 2013 release! - -- Many other bug fixes and enhancements by many other people. - -- Add Python 3 support. (Pillow >= 2.0.0 supports Python 2.6, 2.7, 3.2, 3.3. Pillow < 2.0.0 supports Python 2.4, 2.5, 2.6, 2.7.) - [fluggo] - -- Add PyPy support (experimental, please see #67) - -- Add WebP support #96 - [lqs] - -- Add Tiff G3/G4 support (experimental) - [wiredfool] - -- Backport PIL's PNG/Zip improvements #95, #97 - [olt] - -- Various 64-bit and Windows fixes. - [cgohlke] - -- Add testing suite. - [cgohlke, fluggo] - -- Added support for PNG images with transparency palette. - [d-schmidt] - -1.7.8 (2012-11-01) ------------------- - -- Removed doctests.py that made tests of other packages fail. - [thomasdesvenain] - -- Fix opening psd files with RGBA layers when A mode is not of type 65535 but 3. - Fixes #3 - [thomasdesvenain] - - -1.7.7 (2012-04-04) ------------------- - -- UNDEF more types before including windows headers - [mattip] - -1.7.6 (2012-01-20) ------------------- - -- Bug fix: freetype not found on Mac OS X with case-sensitive filesystem - [gjo] - -- Bug fix: Backport fix to split() after open() (regression introduced in PIL 1.1.7). - [sfllaw] - -1.7.5 (2011-09-07) ------------------- - -- Fix for sys.platform = "linux3" - [blueyed] - -- Package cleanup and additional documentation - [aclark4life] - -1.7.4 (2011-07-21) ------------------- - -- Fix brown bag release - [aclark4life] - -1.7.3 (2011-07-20) ------------------- - -- Fix : resize need int values, append int conversion in thumbnail method - [harobed] - -1.7.2 (2011-06-02) ------------------- - -- Bug fix: Python 2.4 compat - [aclark4life] - -1.7.1 (2011-05-31) ------------------- - -- More multi-arch support - [SteveM, regebro, barry, aclark4life] - -1.7.0 (2011-05-27) ------------------- - -- Add support for multi-arch library directory /usr/lib/x86_64-linux-gnu - [aclark4life] - -1.6 (12/01/2010) ----------------- - -- Bug fix: /usr/x11/include should be added to include_dirs not library_dirs - [elro] - -- Doc fixes - [aclark4life] - -1.5 (11/28/2010) ----------------- - -- Module and package fixes - [aclark4life] - -1.4 (11/28/2010) ----------------- - -- Doc fixes - [aclark4life] - -1.3 (11/28/2010) ----------------- - -- Add support for /lib64 and /usr/lib64 library directories on Linux - [aclark4life] - -- Doc fixes - [aclark4life] - -1.2 (08/02/2010) ----------------- - -- On OS X also check for freetype2 in the X11 path - [jezdez] - -- Doc fixes - [aclark4life] - -1.1 (07/31/2010) ----------------- - -- Removed setuptools_hg requirement - [aclark4life] - -- Doc fixes - [aclark4life] - -1.0 (07/30/2010) ----------------- - -- Remove support for ``import Image``. ``from PIL import Image`` now required. -- Forked PIL based on `Chris McDonough and Hanno Schlichting's setuptools compatible re-packaging `_ - [aclark4life] - -Pre-fork -======== - -0.2b5-1.1.7 ------------ - -:: - - -*- coding: utf-8 -*- - - The Python Imaging Library - $Id$ - - ACKNOWLEDGEMENTS: PIL wouldn't be what it is without the help of: - David Ascher, Phil Austin, Douglas Bagnall, Larry Bates, Anthony - Baxter, William Baxter, Denis Benoit, Jan Blom, Duncan Booth, Alexey - Borzenkov, Jeff Breidenbach, Roger Burnham, Zac Burns, Gene Cash, - Kevin Cazabon, Fred Clare, Greg Coats, Chris Cogdon, Greg Couch, Bill - Crutchfield, Abel Deuring, Tim Docker, Fred Drake, Graham Dumpleton, - Matthew Ellis, Eric Etheridge, Daniel Fetchinson, Robin Friedrich, - Pier Paolo Glave, Federico Di Gregorio, Markus Gritsch, Daniel - Haertle, Greg Hamilton, Mark Hammond, Bernhard Herzog, Rob Hooft, Bob - Ippolito, Jack Jansen, Bill Janssen, Edward Jones, Richard Jones, - Håkan Karlsson, Robert Kern, David Kirtley, Bob Klimek, Matthias - Klose, Andrew Kuchling, Magnus Källström, Victor Lacina, Ben Last, - Hamish Lawson, Cesare Leonardi, Andrew MacIntyre, Jan Matejek, Naveen - Michaud-Agrawal, Gordon McMillan, Skip Montanaro, Fredrik Nehr, - Russell Nelson, Luciano Nocera, Travis Oliphant, Piet van Oostrum, - Richard Oudkerk, Paul Pharr, Andres Polit, Conrado Porto Lopes Gouvêa, - Eric Raymond, Victor Reijs, Bertil Reinhammar, Nicholas Riley, Don - Rozenberg, Toby Sargeant, Barry Scott, Les Schaffer, Joel Shprentz, - Klamer Shutte, Gene Skonicki, Niki Spahiev, D. Alan Stewart, Perry - Stoll, Paul Svensson, Ulrik Svensson, Miki Tebeka, Michael van - Tellingen, Ivan Tkatchev, Dan Torop, Adam Twardoch, Rune Uhlin, Dmitry - Vasiliev, Sasha Voynow, Charles Waldman, Collin Winter, Dan Wolfe, - Ka-Ping Yee, and many others (if your name should be on this list, let - me know.) - -1.1.6 to 1.1.7 --------------- - -This section may not be fully complete. For changes since this file -was last updated, see the repository revision history: -http://svn.effbot.org/public/pil/ - -1.1.7 final ------------ - -- Set GIF loop info property to the number of iterations if a NETSCAPE - loop extension is present, instead of always setting it to 1 (from - Valentino Volonghi). - -1.1.7c1 -------- - -- Improved PNG compression (from Alexey Borzenkov). - -- Read interlaced PNG files (from Conrado Porto Lopes Gouvêa) - -- Added various TGA improvements from Alexey Borzenkov, including - support for specifying image orientation. - -- Bumped block threshold to 16 megabytes, made size estimation a bit - more accurate. This speeds up allocation of large images. - -- Fixed rounding error in ImagingDrawWideLine. - - "gormish" writes: ImagingDrawWideLine() in Draw.c has a bug in every - version I've seen, which leads to different width lines depending on - the order of the points in the line. This is especially bad at some - angles where a 'width=2' line can completely disappear. - -- Added support for RGBA mode to the SGI module (based on code by - Karsten Hiddemann). - -- Handle repeated IPTC tags (adapted from a patch by Eric Bruning). - - Eric writes: According to the specification, some IPTC tags can be - repeated, e.g., tag 2:25 (keywords). PIL 1.1.6 only retained the last - instance of that tag. Below is a patch to store all tags. If there are - multiple tag instances, they are stored in a (python) list. Single tag - instances remain as strings. - -- Fixed potential crash in ImageFilter for small target images - (reported by Zac Burns and Daniel Fetchinson). - -- Use BMP instead of JPEG as temporary show format on Mac OS X. - -- Fixed putpixel/new for I;16 with colors > 255. - -- Added integer power support to ImagingMath. - -- Added limited support for I;16L mode (explicit little endian). - -- Moved WMF support into Image.core; enable WMF rendering by default - if renderer is available. - -- Mark the ARG plugin as obsolete. - -- Added version query mechanism to ImageCms and ImageFont, for - debugging. - -- Added (experimental) ImageCms function for fetching the ICC profile - for the current display (currently Windows only). - - Added HWND/HDC support to ImageCms.get_display_profile(). - -- Added WMF renderer (Windows only). - -- Added ImagePointHandler and ImageTransformHandler mixins; made - ImageCmsTransform work with im.point. - -- Fixed potential endless loop in the XVThumbnail reader (from Nikolai - Ugelvik). - -- Added Kevin Cazabon's pyCMS package. - - The C code has been moved to _imagingcms.c, the Python interface - module is installed as PIL.ImageCMS. - - Added support for in-memory ICC profiles. - - Unified buildTransform and buildTransformFromOpenProfiles. - - The profile can now be either a filename, a profile object, or a - file-like object containing an in-memory profile. - - Additional fixes from Florian Böch: - - Very nice - it just needs LCMS flags support so we can use black - point compensation and softproofing :) See attached patches. They - also fix a naming issue which could cause confusion - display - profile (ImageCms wording) actually means proof profile (lcms - wording), so I changed variable names and docstrings where - applicable. Patches are tested under Python 2.6. - -- Improved support for layer names in PSD files (from Sylvain Baubeau) - - Sylvain writes: I needed to be able to retrieve the names of the - layers in a PSD files. But PsdImagePlugin.py didn't do the job so I - wrote this very small patch. - -- Improved RGBA support for ImageTk for 8.4 and newer (from Con - Radchenko). - - This replaces the slow run-length based encoding model with true - compositing at the Tk level. - -- Added support for 16- and 32-bit images to McIdas loader. - - Based on file samples and stand-alone reader code provided by Craig - Swank. - -- Added ImagePalette support to putpalette. - -- Fixed problem with incremental parsing of PNG files. - -- Make selftest.py report non-zero status on failure (from Mark - Sienkiewicz) - -- Add big endian save support and multipage infrastructure to the TIFF - writer (from Sebastian Haase). - -- Handle files with GPS IFD but no basic EXIF IFD (reported by Kurt - Schwehr). - -- Added zTXT support (from Andrew Kuchling via Lowell Alleman). - -- Fixed potential infinite loop bug in ImageFont (from Guilherme Polo). - -- Added sample ICC profiles (from Kevin Cazabon) - -- Fixed array interface for I, F, and RGBA/RGBX images. - -- Added Chroma subsampling support for JPEG (from Justin Huff). - - Justin writes: Attached is a patch (against PIL 1.1.6) to provide - control over the chroma subsampling done by the JPEG encoder. This - is often useful for reducing compression artifacts around edges of - clipart and text. - -- Added USM/Gaussian Blur code from Kevin Cazabon. - -- Fixed bug w. uninitialized image data when cropping outside the - source image. - -- Use ImageShow to implement the Image.show method. - - Most notably, this picks the 'display' utility when available. It - also allows application code to register new display utilities via - the ImageShow registry. - -- Release the GIL in the PNG compressor (from Michael van Tellingen). - -- Revised JPEG CMYK handling. - - Always assume Adobe behaviour, both when reading and writing (based on - a patch by Kevin Cazabon, and test data by Tim V. and Charlie Clark, and - additional debugging by Michael van Tellingen). - -- Support for preserving ICC profiles (by Florian Böch via Tim Hatch). - - Florian writes: - - It's a beta, so still needs some testing, but should allow you to: - - - retain embedded ICC profiles when saving from/to JPEG, PNG, TIFF. - Existing code doesn't need to be changed. - - access embedded profiles in JPEG, PNG, PSD, TIFF. - - It also includes patches for TIFF to retain IPTC, Photoshop and XMP - metadata when saving as TIFF again, read/write TIFF resolution - information correctly, and to correct inverted CMYK JPEG files. - -- Fixed potential memory leak in median cut quantizer (from Evgeny Salmin). - -- Fixed OverflowError when reading upside-down BMP images. - -- Added resolution save option for PDF files. - - Andreas Kostyrka writes: I've included a patched PdfImagePlugin.py - based on 1.1.6 as included in Ubuntu, that supports a "resolution" - save option. Not great, but it makes the PDF saving more useful by - allowing PDFs that are not exactly 72dpi. - -- Look for Tcl/Tk include files in version-specific include directory - (from Encolpe Degoute). - -- Fixed grayscale rounding error in ImageColor.getcolor (from Tim - Hatch). - -- Fixed calculation of mean value in ImageEnhance.Contrast (reported - by "roop" and Scott David Daniels). - -- Fixed truetype positioning when first character has a negative left - bearing (from Ned Batchelder): - - Ned writes: In PIL 1.1.6, ImageDraw.text will position the string - incorrectly if the first character has a negative left bearing. To - see the problem, show a string like "///" in an italic font. The - first slash will be clipped at the left, and the string will be - mis-positioned. - -- Fixed resolution unit bug in tiff reader/writer (based on code by - Florian Höch, Gary Bloom, and others). - -- Added simple transparency support for RGB images (reported by - Sebastian Spaeth). - -- Added support for Unicode filenames in ImageFont.truetype (from Donn - Ingle). - -- Fixed potential crash in ImageFont.getname method (from Donn Ingle). - -- Fixed encoding issue in PIL/WalImageFile (from Santiago M. Mola). - -1.1.6 ------ - -- Fixed some 64-bit compatibility warnings for Python 2.5. - -- Added threading support for the Sane driver (from Abel Deuring). - -1.1.6b2 -------- - -- Added experimental "floodfill" function to the ImageDraw module - (based on code by Eric Raymond). - -- The default arguments for "frombuffer" doesn't match "fromstring" - and the documentation; this is a bug, and will most likely be fixed - in a future version. In this release, PIL prints a warning message - instead. To silence the warning, change any calls of the form - "frombuffer(mode, size, data)" to:: - - frombuffer(mode, size, data, "raw", mode, 0, 1) - -- Added "fromarray" function, which takes an object implementing the - NumPy array interface and creates a PIL Image from it. (from Travis - Oliphant). - -- Added NumPy array interface support (__array_interface__) to the - Image class (based on code by Travis Oliphant). - - This allows you to easily convert between PIL image memories and - NumPy arrays:: - - import numpy, Image - im = Image.open('hopper.jpg') - a = numpy.asarray(im) # a is readonly - im = Image.fromarray(a) - -- Fixed CMYK polarity for JPEG images, by treating all images as - "Adobe CMYK" images. (thanks to Cesare Leonardi and Kevin Cazabon - for samples, debugging, and patches). - -1.1.6b1 -------- - -- Added 'expand' option to the Image 'rotate' method. If true, the - output image is made large enough to hold the entire rotated image. - -- Changed the ImageDraw 'line' method to always draw the last pixel in - a polyline, independent of line angle. - -- Fixed bearing calculation and clipping in the ImageFont truetype - renderer; this could lead to clipped text, or crashes in the low-level - _imagingft module. (based on input from Adam Twardoch and - others). - -- Added ImageQt wrapper module, for converting PIL Image objects to - QImage objects in an efficient way. - -- Fixed 'getmodebands' to return the number of bands also for "PA" - and "LA" modes. Added 'getmodebandnames' helper that return the - band names. - -1.1.6a2 -------- - -- Added float/double support to the TIFF loader (from Russell - Nelson). - -- Fixed broken use of realloc() in path.c (from Jan Matejek) - -- Added save support for Spider images (from William Baxter). - -- Fixed broken 'paste' and 'resize' operations in pildriver - (from Bill Janssen). - -- Added support for duplex scanning to the Sane interface (Abel - Deuring). - -1.1.6a1 -------- - -- Fixed a memory leak in "convert(mode)", when converting from - L to P. - -- Added pixel access object. The "load" method now returns a - access object that can be used to directly get and set pixel - values, using ordinary [x, y] notation:: - - pixel = im.load() - v = pixel[x, y] - pixel[x, y] = v - - If you're accessing more than a few pixels, this is a lot - faster than using getpixel/putpixel. - -- Fixed building on Cygwin (from Miki Tebeka). - -- Fixed "point(callable)" on unloaded images (reported by Håkan - Karlsson). - -- Fixed size bug in ImageWin.ImageWindow constructor (from Victor - Reijs) - -- Fixed ImageMath float() and int() operations for Python 2.4 - (reported by Don Rozenberg). - -- Fixed "RuntimeError: encoder error -8 in tostring" problem for - wide "RGB", "I", and "F" images. - -- Fixed line width calculation. - -1.1.6a0 -------- - -- Fixed byte order issue in Image.paste(ink) (from Ka-Ping Yee). - -- Fixed off-by-0.5 errors in the ANTIALIAS code (based on input - from Douglas Bagnall). - -- Added buffer interface support to the Path constructor. If - a buffer is provided, it is assumed to contain a flat array - of float coordinates (e.g. array.array('f', seq)). - -- Added new ImageMath module. - -- Fixed ImageOps.equalize when used with a small number of distinct - values (reported by David Kirtley). - -- Fixed potential integer division in PSDraw.image (from Eric Etheridge). - -1.1.5c2 and 1.1.5 final ------------------------ - -- Added experimental PERSPECTIVE transform method (from Jeff Breidenbach). - -1.1.5c1 -------- - -- Make sure "thumbnail" never generates zero-wide or zero-high images - (reported by Gene Skonicki) - -- Fixed a "getcolors" bug that could result in a zero count for some - colors (reported by Richard Oudkerk). - -- Changed default "convert" palette to avoid "rounding errors" when - round-tripping white source pixels (reported by Henryk Gerlach and - Jeff Epler). - -1.1.5b3 -------- - -- Don't crash in "quantize" method if the number of colors requested - is larger than 256. This release raises a ValueError exception; - future versions may return a mode "RGB" image instead (reported - by Richard Oudkerk). - -- Added WBMP read/write support (based on code by Duncan Booth). - -1.1.5b2 -------- - -- Added DPI read/write support to the PNG codec. The decoder sets - the info["dpi"] attribute for PNG files with appropriate resolution - settings. The encoder uses the "dpi" option (based on code by Niki - Spahiev). - -- Added limited support for "point" mappings from mode "I" to mode "L". - Only 16-bit values are supported (other values are clipped), the lookup - table must contain exactly 65536 entries, and the mode argument must be - set to "L". - -- Added support for Mac OS X icns files (based on code by Bob Ippolito). - -- Added "ModeFilter" support to the ImageFilter module. - -- Added support for Spider images (from William Baxter). See the - comments in PIL/SpiderImagePlugin.py for more information on this - format. - -1.1.5b1 -------- - -- Added new Sane release (from Ralph Heinkel). See the Sane/README - and Sane/CHANGES files for more information. - -- Added experimental PngInfo chunk container to the PngImageFile - module. This can be used to add arbitrary chunks to a PNG file. - Create a PngInfo instance, use "add" or "add_text" to add chunks, - and pass the instance as the "pnginfo" option when saving the - file. - -- Added "getpalette" method. This returns the palette as a list, - or None if the image has no palette. To modify the palette, use - "getpalette" to fetch the current palette, modify the list, and - put it back using "putpalette". - -- Added optional flattening to the ImagePath "tolist" method. - tolist() or tolist(0) returns a list of 2-tuples, as before. - tolist(1) returns a flattened list instead. - -1.1.5a5 -------- - -- Fixed BILINEAR/BICUBIC/ANTIALIAS filtering for mode "LA". - -- Added "getcolors()" method. This is similar to the existing histogram - method, but looks at color values instead of individual layers, - and returns an unsorted list of (count, color) tuples. - - By default, the method returns None if finds more than 256 colors. - If you need to look for more colors, you can pass in a limit (this - is used to allocate internal tables, so you probably don't want to - pass in too large values). - -- Build improvements: Fixed building under AIX, improved detection of - FreeType2 and Mac OS X framework libraries, and more. Many thanks - to everyone who helped test the new "setup.py" script! - -1.1.5a4 -------- - -- The "save" method now looks for a file format driver before - creating the file. - -- Don't use antialiased truetype fonts when drawing in mode "P", "I", - and "F" images. - -- Rewrote the "setup.py" file. The new version scans for available - support libraries, and configures both the libImaging core library - and the bindings in one step. - - To use specific versions of the libraries, edit the ROOT variables - in the setup.py file. - -- Removed threaded "show" viewer; use the old "show" implementation - instead (Windows). - -- Added deprecation warnings to Image.offset, ImageDraw.setink, and - ImageDraw.setfill. - -- Added width option to ImageDraw.line(). The current implementation - works best for straight lines; it does not support line joins, so - polylines won't look good. - -- ImageDraw.Draw is now a factory function instead of a class. If - you need to create custom draw classes, inherit from the ImageDraw - class. All other code should use the factory function. - -- Fixed loading of certain PCX files (problem reported by Greg - Hamilton, who also provided samples). - -- Changed _imagingft.c to require FreeType 2.1 or newer. The - module can still be built with earlier versions; see comments - in _imagingft.c for details. - -1.1.5a3 -------- - -- Added 'getim' method, which returns a PyCObject wrapping an - Imaging pointer. The description string is set to IMAGING_MAGIC. - See Imaging.h for pointer and string declarations. - -- Fixed reading of TIFF JPEG images (problem reported by Ulrik - Svensson). - -- Made ImageColor work under Python 1.5.2 - -- Fixed division by zero "equalize" on very small images (from - Douglas Bagnall). - -1.1.5a2 -------- - -- The "paste" method now supports the alternative "paste(im, mask)" - syntax (in this case, the box defaults to im's bounding box). - -- The "ImageFile.Parser" class now works also for PNG files with - more than one IDAT block. - -- Added DPI read/write to the TIFF codec, and fixed writing of - rational values. The decoder sets the info["dpi"] attribute - for TIFF files with appropriate resolution settings. The - encoder uses the "dpi" option. - -- Disable interlacing for small (or narrow) GIF images, to - work around what appears to be a hard-to-find bug in PIL's - GIF encoder. - -- Fixed writing of mode "P" PDF images. Made mode "1" PDF - images smaller. - -- Made the XBM reader a bit more robust; the file may now start - with a few whitespace characters. - -- Added support for enhanced metafiles to the WMF driver. The - separate PILWMF kit lets you render both placeable WMF files - and EMF files as raster images. See - http://effbot.org/downloads#pilwmf - -1.1.5a1 -------- - -- Replaced broken WMF driver with a WMF stub plugin (see below). - -- Fixed writing of mode "1", "L", and "CMYK" PDF images (based on - input from Nicholas Riley and others). - -- Fixed adaptive palette conversion for zero-width or zero-height - images (from Chris Cogdon) - -- Fixed reading of PNG images from QuickTime 6 (from Paul Pharr) - -- Added support for StubImageFile plugins, including stub plugins - for BUFR, FITS, GRIB, and HDF5 files. A stub plugin can identify - a given file format, but relies on application code to open and - save files in that format. - -- Added optional "encoding" argument to the ImageFont.truetype - factory. This argument can be used to specify non-Unicode character - maps for fonts that support that. For example, to draw text using - the Microsoft Symbol font, use:: - - font = ImageFont.truetype("symbol.ttf", 16, encoding="symb") - draw.text((0, 0), unichr(0xF000 + 0xAA)) - - (note that the symbol font uses characters in the 0xF000-0xF0FF - range) - - Common encodings are "unic" (Unicode), "symb" (Microsoft Symbol), - "ADOB" (Adobe Standard), "ADBE" (Adobe Expert), and "armn" (Apple - Roman). See the FreeType documentation for more information. - -- Made "putalpha" a bit more robust; you can now attach an alpha - layer to a plain "L" or "RGB" image, and you can also specify - constant alphas instead of alpha layers (using integers or colour - names). - -- Added experimental "LA" mode support. - - An "LA" image is an "L" image with an attached transparency layer. - Note that support for "LA" is not complete; some operations may - fail or produce unexpected results. - -- Added "RankFilter", "MinFilter", "MedianFilter", and "MaxFilter" - classes to the ImageFilter module. - -- Improved support for applications using multiple threads; PIL - now releases the global interpreter lock for many CPU-intensive - operations (based on work by Kevin Cazabon). - -- Ignore Unicode characters in the PCF loader (from Andres Polit) - -- Fixed typo in OleFileIO.loadfat, which could affect loading of - FlashPix and Image Composer images (Daniel Haertle) - -- Fixed building on platforms that have Freetype but don't have - Tcl/Tk (Jack Jansen, Luciano Nocera, Piet van Oostrum and others) - -- Added EXIF GPSInfo read support for JPEG files. To extract - GPSInfo information, open the file, extract the exif dictionary, - and check for the key 0x8825 (GPSInfo). If present, it contains - a dictionary mapping GPS keys to GPS values. For a list of keys, - see the EXIF specification. - - The "ExifTags" module contains a GPSTAGS dictionary mapping GPS - tags to tag names. - -- Added DPI read support to the PCX and DCX codecs (info["dpi"]). - -- The "show" methods now uses a built-in image viewer on Windows. - This viewer creates an instance of the ImageWindow class (see - below) and keeps it running in a separate thread. NOTE: This - was disabled in 1.1.5a4. - -- Added experimental "Window" and "ImageWindow" classes to the - ImageWin module. These classes allow you to create a WCK-style - toplevel window, and use it to display raster data. - -- Fixed some Python 1.5.2 issues (to build under 1.5.2, use the - Makefile.pre.in/Setup.in approach) - -- Added support for the TIFF FillOrder tag. PIL can read mode "1", - "L", "P" and "RGB" images with non-standard FillOrder (based on - input from Jeff Breidenbach). - -1.1.4 final ------------ - -- Fixed ImageTk build problem on Unix. - -1.1.4b2 -------- - -- Improved building on Mac OS X (from Jack Jansen). - -- Improved building on Windows with MinGW (from Klamer Shutte). - -- If no font is specified, ImageDraw now uses the embedded default - font. Use the "load" or "truetype" methods to load a real font. - -- Added embedded default font to the ImageFont module (currently - an 8-pixel Courier font, taken from the X window distribution). - -1.1.4b1 -------- - -- Added experimental EXIF support for JPEG files. To extract EXIF - information from a JPEG file, open the file as usual, and call the - "_getexif" method. If successful, this method returns a dictionary - mapping EXIF TIFF tags to values. If the file does not contain EXIF - data, the "_getexif" method returns None. - - The "ExifTags" module contains a dictionary mapping tags to tag - names. - - This interface will most likely change in future versions. - -- Fixed a bug when using the "transparency" option with the GIF - writer. - -- Added limited support for "bitfield compression" in BMP files - and DIB buffers, for 15-bit, 16-bit, and 32-bit images. This - also fixes a problem with ImageGrab module when copying screendumps - from the clipboard on 15/16/32-bit displays. - -- Added experimental WAL (Quake 2 textures) loader. To use this - loader, import WalImageFile and call the "open" method in that - module. - -1.1.4a4 -------- - -- Added updated SANE driver (Andrew Kuchling, Abel Deuring) - -- Use Python's "mmap" module on non-Windows platforms to read some - uncompressed formats using memory mapping. Also added a "frombuffer" - function that allows you to access the contents of an existing string - or buffer object as if it were an image object. - -- Fixed a memory leak that could appear when processing mode "P" - images (from Pier Paolo Glave) - -- Ignore Unicode characters in the BDF loader (from Graham Dumpleton) - -1.1.4a3 released; Windows only ------------------------------- - -- Added experimental RGBA-on-RGB drawing support. To use RGBA - colours on an RGB image, pass "RGBA" as the second string to - the ImageDraw.Draw constructor. - -- Added support for non-ASCII strings (Latin-1) and Unicode - to the truetype font renderer. - -- The ImageWin "Dib" object can now be constructed directly from - an image object. - -- The ImageWin module now allows you use window handles as well - as device contexts. To use a window handle, wrap the handle in - an ImageWin.HWND object, and pass in this object instead of the - device context. - -1.1.4a2 -------- - -- Improved support for 16-bit unsigned integer images (mode "I;16"). - This includes TIFF reader support, and support for "getextrema" - and "point" (from Klamer Shutte). - -- Made the BdfFontFile reader a bit more robust (from Kevin Cazabon - and Dmitry Vasiliev) - -- Changed TIFF writer to always write Compression tag, even when - using the default compression (from Greg Couch). - -- Added "show" support for Mac OS X (from Dan Wolfe). - -- Added clipboard support to the "ImageGrab" module (Windows only). - The "grabclipboard" function returns an Image object, a list of - filenames (not in 1.1.4), or None if neither was found. - -1.1.4a1 -------- - -- Improved support for drawing RGB data in palette images. You can - now use RGB tuples or colour names (see below) when drawing in a - mode "P" image. The drawing layer automatically assigns color - indexes, as long as you don't use more than 256 unique colours. - -- Moved self test from MiniTest/test.py to ./selftest.py. - -- Added support for CSS3-style color strings to most places that - accept colour codes/tuples. This includes the "ImageDraw" module, - the Image "new" function, and the Image "paste" method. - - Colour strings can use one of the following formats: "#f00", - "#ff0000", "rgb(255,0,0)", "rgb(100%,0%,0%)", "hsl(0, 100%, 50%)", - or "red" (most X11-style colour names are supported). See the - documentation for the "ImageColor" module for more information. - -- Fixed DCX decoder (based on input from Larry Bates) - -- Added "IptcImagePlugin.getiptcinfo" helper to extract IPTC/NAA - newsphoto properties from JPEG, TIFF, or IPTC files. - -- Support for TrueType/OpenType fonts has been added to - the standard distribution. You need the freetype 2.0 - library. - -- Made the PCX reader a bit more robust when reading 2-bit - and 4-bit PCX images with odd image sizes. - -- Added "Kernel" class to the ImageFilter module. This class - allows you to filter images with user-defined 3x3 and 5x5 - convolution kernels. - -- Added "putdata" support for mode "I", "F" and "RGB". - -- The GIF writer now supports the transparency option (from - Denis Benoit). - -- A HTML version of the module documentation is now shipped - with the source code distribution. You'll find the files in - the Doc subdirectory. - -- Added support for Palm pixmaps (from Bill Janssen). This - change was listed for 1.1.3, but the "PalmImagePlugin" driver - didn't make it into the distribution. - -- Improved decoder error messages. - -1.1.3 final ------------ - -- Made setup.py look for old versions of zlib. For some background, - see: https://zlib.net/advisory-2002-03-11.txt - -1.1.3c2 -------- - -- Added setup.py file (tested on Unix and Windows). You still - need to build libImaging/imaging.lib in the traditional way, - but the setup.py script takes care of the rest. - - The old Setup.in/Makefile.pre.in build method is still - supported. - -- Fixed segmentation violation in ANTIALIAS filter (an internal - buffer wasn't properly allocated). - -1.1.3c1 -------- - -- Added ANTIALIAS downsampling filter for high-quality "resize" - and "thumbnail" operations. Also added filter option to the - "thumbnail" operation; the default value is NEAREST, but this - will most likely change in future versions. - -- Fixed plugin loader to be more robust if the __file__ - variable isn't set. - -- Added seek/tell support (for layers) to the PhotoShop - loader. Layer 0 is the main image. - -- Added new (but experimental) "ImageOps" module, which provides - shortcuts for commonly used operations on entire images. - -- Don't mess up when loading PNG images if the decoder leaves - data in the output buffer. This could cause internal errors - on some PNG images, with some versions of ZLIB. (Bug report - and patch provided by Bernhard Herzog.) - -- Don't mess up on Unicode filenames. - -- Don't mess up when drawing on big endian platforms. - -- Made the TIFF loader a bit more robust; it can now read some - more slightly broken TIFF files (based on input from Ted Wright, - Bob Klimek, and D. Alan Stewart) - -- Added OS/2 EMX build files (from Andrew MacIntyre) - -- Change "ImageFont" to reject image files if they don't have the - right mode. Older versions could leak memory for "P" images. - (Bug reported by Markus Gritsch). - -- Renamed some internal functions to avoid potential build - problem on Mac OS X. - -- Added DL_EXPORT where relevant (for Cygwin, based on input - from Robert Yodlowski) - -- (re)moved bogus __init__ call in BdfFontFile (bug spotted - by Fred Clare) - -- Added "ImageGrab" support (Windows only) - -- Added support for XBM hotspots (based on code contributed by - Bernhard Herzog). - -- Added write support for more TIFF tags, namely the Artist, - Copyright, DateTime, ResolutionUnit, Software, XResolution and - YResolution tags (from Greg Couch) - -- Added TransposedFont wrapper to ImageFont module - -- Added "optimize" flag to GIF encoder. If optimize is present - and non-zero, PIL will work harder to create a small file. - -- Raise "EOFError" (not IndexError) when reading beyond the - end of a TIFF sequence. - -- Support rewind ("seek(0)") for GIF and TIFF sequences. - -- Load grayscale GIF images as mode "L" - -- Added DPI read/write support to the JPEG codec. The decoder - sets the info["dpi"] attribute for JPEG files with JFIF dpi - settings. The encoder uses the "dpi" option:: - - im = Image.open("file.jpg") - dpi = im.info["dpi"] # raises KeyError if DPI not known - im.save("out.jpg", dpi=dpi) - - Note that PIL doesn't always preserve the "info" attribute - for normal image operations. - -1.1.2c1 and 1.1.2 final ------------------------ - -- Adapted to Python 2.1. Among other things, all uses of the - "regex" module have been replaced with "re". - -- Fixed attribute error when reading large PNG files (this bug - was introduced in maintenance code released after the 1.1.1 - release) - -- Ignore non-string objects in sys.path - -- Fixed Image.transform(EXTENT) for negative xoffsets - -- Fixed loading of image plugins if PIL is installed as a package. - (The plugin loader now always looks in the directory where the - Image.py module itself is found, even if that directory isn't on - the standard search path) - -- The Png plugin has been added to the list of preloaded standard - formats - -- Fixed bitmap/text drawing in fill mode. - -- Fixed "getextrema" to work also for multiband images. - -- Added transparency support for L and P images to the PNG codec. - -- Improved support for read-only images. The "load" method now - sets the "readonly" attribute for memory-mapped images. Operations - that modifies an image in place (such as "paste" and drawing operations) - creates an in-memory copy of the image, if necessary. (before this - change, any attempt to modify a memory-mapped image resulted in a - core dump...) - -- Added special cases for lists everywhere PIL expects a sequence. - This should speed up things like "putdata" and drawing operations. - -- The Image.offset method is deprecated. Use the ImageChops.offset - function instead. - -- Changed ImageChops operators to copy palette and info dictionary - from the first image argument. - -1.1.1 ------ - -- Additional fixes for Python 1.6/2.0, including TIFF "save" bug. - -- Changed "init" to properly load plugins when PIL is used as a - package. - -- Fixed broken "show" method (on Unix) - -1.0 to 1.1 ----------- - -- Adapted to Python 1.6 ("append" and other method changes) - -- Fixed Image.paste when pasting with solid colour and matte - layers ("L" or "RGBA" masks) (bug reported by Robert Kern) - -- To make it easier to distribute prebuilt versions of PIL, - the tkinit binding stuff has been moved to a separate - extension module, named "_imagingtk". - -0.3b2 to 1.0 final ------------------- - -- If there's no 16-bit integer (like on a Cray T3E), set - INT16 to the smallest integer available. Most of the - library works just fine anyway (from Bill Crutchfield) - -- Tweaks to make drawing work on big-endian platforms. - -1.0c2 ------ - -- If PIL is built with the WITH_TKINTER flag, ImageTk can - automatically hook into a standard Tkinter build. You - no longer need to build your own Tkinter to use the - ImageTk module. - - The old way still works, though. For more information, - see Tk/install.txt. - -- Some tweaks to ImageTk to support multiple Tk interpreters - (from Greg Couch). - -- ImageFont "load_path" now scans directory mentioned in .pth - files (from Richard Jones). - -1.0c1 ------ - -- The TIFF plugin has been rewritten. The new plugin fully - supports all major PIL image modes (including F and I). - -- The ImageFile module now includes a Parser class, which can - be used to incrementally decode an image file (while downloading - it from the net, for example). See the handbook for - details. - -- "show" now converts non-standard modes to "L" or "RGB" (as - appropriate), rather than writing weird things to disk for - "xv" to choke upon. (bug reported by Les Schaffer). - -1.0b2 ------ - -- Major speedups for rotate, transform(EXTENT), and transform(AFFINE) - when using nearest neighbour resampling. - -- Modified ImageDraw to be compatible with the Arrow graphics - interface. See the handbook for details. - -- PIL now automatically loads file codecs when used as a package - (from The Dragon De Monsyne). Also included an __init__.py file - in the standard distribution. - -- The GIF encoder has been modified to produce much smaller files. - - PIL now uses a run-length encoding method to encode GIF files. - On a random selection of GIF images grabbed from the web, this - version makes the images about twice as large as the original - LZW files, where the earlier version made them over 5 times - larger. YMMV, of course. - -- Added PCX write support (works with "1", "P", "L", and "RGB") - -- Added "bitmap" and "textsize" methods to ImageDraw. - -- Improved font rendering code. Fixed a bug or two, and moved - most of the time critical stuff to C. - -- Removed "bdf2pil.py". Use "pilfont.py" instead! - -- Improved 16-bit support (still experimental, though). - - The following methods now support "I;16" and "I;16B" images: - "getpixel", "copy", "convert" (to and from mode "I"), "resize", - "rotate", and "transform" with nearest neighbour filters, and - "save" using the IM format. The "new" and "open" functions - also work as expected. On Windows, 16-bit files are memory - mapped. - - NOTE: ALL other operations are still UNDEFINED on 16-bit images. - -- The "paste" method now supports constant sources. - - Just pass a colour value (a number or a tuple, depending on - the target image mode) instead of the source image. - - This was in fact implemented in an inefficient way in - earlier versions (the "paste" method generated a temporary - source image if you passed it a colour instead of an image). - In this version, this is handled on the C level instead. - -- Added experimental "RGBa" mode support. - - An "RGBa" image is an RGBA image where the colour components - have have been premultiplied with the alpha value. PIL allows - you to convert an RGBA image to an RGBa image, and to paste - RGBa images on top of RGB images. Since this saves a bunch - of multiplications and shifts, it is typically about twice - as fast an ordinary RGBA paste. - -- Eliminated extra conversion step when pasting "RGBA" or "RGBa" - images on top of "RGB" images. - -- Fixed Image.BICUBIC resampling for "RGB" images. - -- Fixed PCX image file handler to properly read 8-bit PCX - files (bug introduced in 1.0b1, reported by Bernhard - Herzog) - -- Fixed PSDraw "image" method to restore the coordinate - system. - -- Fixed "blend" problem when applied to images that was - not already loaded (reported by Edward C. Jones) - -- Fixed -f option to "pilconvert.py" (from Anthony Baxter) - -1.0b1 ------ - -- Added Toby J. Sargeant's quantization package. To enable - quantization, use the "palette" option to "convert":: - - imOut = im.convert("P", palette=Image.ADAPTIVE) - - This can be used with "L", "P", and "RGB" images. In this - version, dithering cannot be used with adaptive palettes. - - Note: ADAPTIVE currently maps to median cut quantization - with 256 colours. The quantization package also contains - a maximum coverage quantizer, which will be supported by - future versions of PIL. - -- Added Eric S. Raymond's "pildriver" image calculator to the - distribution. See the docstring for more information. - -- The "offset" method no longer dumps core if given positive - offsets (from Charles Waldman). - -- Fixed a resource leak that could cause ImageWin to run out of - GDI resources (from Roger Burnham). - -- Added "arc", "chord", and "pieslice" methods to ImageDraw (inspired - by code contributed by Richard Jones). - -- Added experimental 16-bit support, via modes "I;16" (little endian - data) and "I;16B" (big endian). Only a few methods properly support - such images (see above). - -- Added XV thumbnail file handler (from Gene Cash). - -- Fixed BMP image file handler to handle palette images with small - palettes (from Rob Hooft). - -- Fixed Sun raster file handler for palette images (from Charles - Waldman). - -- Improved various internal error messages. - -- Fixed Path constructor to handle arbitrary sequence objects. This - also affects the ImageDraw class (from Richard Jones). - -- Fixed a bug in JpegDecode that caused PIL to report "decoder error - -2" for some progressive JPEG files (reported by Magnus Källström, - who also provided samples). - -- Fixed a bug in JpegImagePlugin that caused PIL to hang when loading - JPEG files using 16-bit quantization tables. - -- The Image "transform" method now supports Image.QUAD transforms. - The data argument is an 8-tuple giving the upper left, lower - left, lower right, and upper right corner of the source quadrilateral. - Also added Image.MESH transform which takes a list - of quadrilaterals. - -- The Image "resize", "rotate", and "transform" methods now support - Image.BILINEAR (2x2) and Image.BICUBIC (4x4) resampling filters. - Filters can be used with all transform methods. - -- The ImageDraw "rectangle" method now includes both the right - and the bottom edges when drawing filled rectangles. - -- The TGA decoder now works properly for runlength encoded images - which have more than one byte per pixel. - -- "getbands" on an YCbCr image now returns ("Y", "Cb", "Cr") - -- Some file drivers didn't handle the optional "modify" argument - to the load method. This resulted in exceptions when you used - "paste" (and other methods that modify an image in place) on a - newly opened file. - -0.3b2 ------ - -The test suite includes 825 individual tests. - -- An Image "getbands" method has been added. It returns a tuple - containing the individual band names for this image. To figure - out how many bands an image has, use "len(im.getbands())". - -- An Image "putpixel" method has been added. - -- The Image "point" method can now be used to convert "L" images - to any other format, via a lookup table. That table should - contain 256 values for each band in the output image. - -- Some file drivers (including FLI/FLC, GIF, and IM) accidentally - overwrote the offset method with an internal attribute. All - drivers have been updated to use private attributes where - possible. - -- The Image "histogram" method now works for "I" and "F" images. - For these modes, PIL divides the range between the min and - max values used in the image into 256 bins. You can also - pass in your own min and max values via the "extrema" option:: - - h = im.histogram(extrema=(0, 255)) - -- An Image "getextrema" method has been added. It returns the - min and max values used in the image. In this release, this - works for single band images only. - -- Changed the PNG driver to load and save mode "I" images as - 16-bit images. When saving, values outside the range 0..65535 - are clipped. - -- Fixed ImageFont.py to work with the new "pilfont" compiler. - -- Added JPEG "save" and "draft" support for mode "YCbCr" images. - Note that if you save an "YCbCr" image as a JPEG file and read - it back, it is read as an RGB file. To get around this, you - can use the "draft" method:: - - im = Image.open("color.jpg") - im.draft("YCbCr", im.size) - -- Read "RGBA" TGA images. Also fixed the orientation bug; all - images should now come out the right way. - -- Changed mode name (and internal representation) from "YCrCb" - to "YCbCr" (!) - **WARNING: MAY BREAK EXISTING CODE** - -0.3b1 ------ - -The test suite includes 750 individual tests. - -- The "pilfont" package is now included in the standard PIL - distribution. The pilfont utility can be used to convert - X BDF and PCF raster font files to a format understood by - the ImageFont module. - -- GIF files are now interlaced by default. To write a - non-interlaced file, pass interlace=0 to the "save" - method. - -- The default string format has changed for the "fromstring" - and "tostring" methods. - **WARNING: MAY BREAK EXISTING CODE** - - NOTE: If no extra arguments are given, the first line in - the string buffer is the top line of the image, instead of - the bottom line. For RGB images, the string now contains - 3 bytes per pixel instead of 4. These changes were made - to make the methods compatible with the "fromstring" - factory function. - - To get the old behaviour, use the following syntax:: - - data = im.tostring("raw", "RGBX", 0, -1) - im.fromstring(data, "raw", "RGBX", 0, -1) - -- "new" no longer gives a MemoryError if the width or height - is zero (this only happened on platforms where malloc(0) - or calloc(0) returns NULL). - -- "new" now adds a default palette object to "P" images. - -- You can now convert directly between all modes supported by - PIL. When converting colour images to "P", PIL defaults to - a "web" palette and dithering. When converting grayscale - images to "1", PIL uses a thresholding and dithering. - -- Added a "dither" option to "convert". By default, "convert" - uses floyd-steinberg error diffusion for "P" and "1" targets, - so this option is only used to *disable* dithering. Allowed - values are NONE (no dithering) or FLOYDSTEINBERG (default). - :: - - imOut = im.convert("P", dither=Image.NONE) - -- Added a full set of "I" decoders. You can use "fromstring" - (and file decoders) to read any standard integer type as an - "I" image. - -- Added some support for "YCbCr" images (creation, conversion - from/to "L" and "RGB", IM YCC load/save) - -- "getpixel" now works properly with fractional coordinates. - -- ImageDraw "setink" now works with "I", "F", "RGB", "RGBA", - "RGBX", "CMYK", and "YCbCr" images. - -- ImImagePlugin no longer attaches palettes to "RGB" images. - -- Various minor fixes. - -0.3a4 ------ - -- Added experimental IPTC/NAA support. - -- Eliminated AttributeError exceptions after "crop" (from - Skip Montanaro) - -- Reads some uncompressed formats via memory mapping (this - is currently supported on Win32 only) - -- Fixed some last minute glitches in the last alpha release - (Types instead of types in Image.py, version numbers, etc.) - -- Eliminated some more bogus compiler warnings. - -- Various fixes to make PIL compile and run smoother on Macs - (from Jack Jansen). - -- Fixed "fromstring" and "tostring" for mode "I" images. - -0.3a3 ------ - -The test suite includes 530 individual tests. - -- Eliminated unexpected side-effect in "paste" with matte. "paste" - now works properly also if compiled with "gcc". - -- Adapted to Python 1.5 (build issues only) - -- Fixed the ImageDraw "point" method to draw also the last - point (!). - -- Added "I" and "RGBX" support to Image.new. - -- The plugin path is now properly prepended to the module search - path when a plugin module is imported. - -- Added "draw" method to the ImageWin.Dib class. This is used by - Topaz to print images on Windows printers. - -- "convert" now supports conversions from "P" to "1" and "F". - -- "paste" can now take a colour instead of an image as the first argument. - The colour must match the colour argument given to the new function, and - match the mode of the target image. - -- Fixed "paste" to allow a mask also for mode "F" images. - -- The BMP driver now saves mode "1" images. When loading images, the mode - is set to "L" for 8-bit files with grayscale palettes, and to "P" for - other 8-bit files. - -- The IM driver now reads and saves "1" images (file modes "0 1" or "L 1"). - -- The JPEG and GIF drivers now saves "1" images. For JPEG, the image - is saved as 8-bit grayscale (it will load as mode "L"). For GIF, the - image will be loaded as a "P" image. - -- Fixed a potential buffer overrun in the GIF encoder. - -0.3a2 ------ - -The test suite includes 400 individual tests. - -- Improvements to the test suite revealed a number of minor bugs, which - are all fixed. Note that crop/paste, 32-bit ImageDraw, and ImageFont - are still weak spots in this release. - -- Added "putpalette" method to the Image class. You can use this - to add or modify the palette for "P" and "L" images. If a palette - is added to an "L" image, it is automatically converted to a "P" - image. - -- Fixed ImageDraw to properly handle 32-bit image memories - ("RGB", "RGBA", "CMYK", "F") - -- Fixed "fromstring" and "tostring" not to mess up the mode attribute - in default mode. - -- Changed ImPlatform.h to work on CRAY's (don't have one at home, so I - haven't tried it). The previous version assumed that either "short" - or "int" were 16-bit wide. PIL still won't compile on platforms where - neither "short", "int" nor "long" are 32-bit wide. - -- Added file= and data= keyword arguments to PhotoImage and BitmapImage. - This allows you to use them as drop-in replacements for the corresponding - Tkinter classes. - -- Removed bogus references to the crack coder (ImagingCrack). - -0.3a1 ------ - -- Make sure image is loaded in "tostring". - -- Added floating point packer (native 32-bit floats only). - -0.1b1 to 0.2 (b5) ------------------ - -- Modified "fromstring" and "tostring" methods to use file codecs. - Also added "fromstring" factory method to create an image directly - from data in a string. - -- Added support for 32-bit floating point images (mode "F"). You - can convert between "L" and "F" images, and apply a subset of the - available image processing methods on the "F" image. You can also - read virtually any data format into a floating point image memory; - see the section on "Decoding Floating Point Data" in the handbook - for more information. - -0.2b5 released; on windows only -------------------------------- - -- Fixed the tobitmap() method to work properly for small bitmaps. - -- Added RMS and standard deviation to the ImageStat.Stat class. Also - modified the constructor to take an optional feature mask, and also - to accept either an image or a list containing the histogram data. - -- The BitmapImage code in ImageTk can now use a special bitmap - decoder, which has to be patched into Tk. See the "Tk/pilbitmap.txt" - file for details. If not installed, bitmaps are transferred to Tk as - XBM strings. - -- The PhotoImage code in ImageTk now uses a Tcl command ("PyImagingPaste") - instead of a special image type. This gives somewhat better performance, - and also allows PIL to support transparency. - **WARNING: TKAPPINIT MUST BE MODIFIED** - -- ImageTk now honours the alpha layer in RGBA images. Only fully - transparent pixels are made transparent (that is, the alpha layer - is treated as a mask). To treat the alpha laters as a matte, you - must paste the image on the background before handing it over to - ImageTk. - -- Added McIdas reader (supports 8-bit images only). - -- PIL now preloads drivers for BMP, GIF, JPEG, PPM, and TIFF. As - long as you only load and save these formats, you don't have to - wait for a full scan for drivers. To force scanning, call the - Image.init() function. - -- The "seek" and "tell" methods are now always available, also for - single-frame images. - -- Added optional mask argument to histogram method. The mask may - be an "1" or "L" image with the same size as the original image. - Only pixels where the mask is non-zero are included in the - histogram. - -- The "paste" method now allows you to specify only the lower left - corner (a 2-tuple), instead of the full region (a 4-tuple). - -- Reverted to old plugin scanning model; now scans all directory - names in the path when looking for plugins. - -- Added PIXAR raster support. Only uncompressed ("dumped") RGB - images can currently be read (based on information provided - by Greg Coats). - -- Added FlashPix (FPX) read support. Reads all pixel formats, but - only the highest resolution is read, and the viewing transform is - currently ignored. - -- Made PNG encoding somewhat more efficient in "optimize" mode; a - bug in 0.2b4 didn't enable all predictor filters when optimized - storage were requested. - -- Added Microsoft Image Composer (MIC) read support. When opened, - the first sprite in the file is loaded. You can use the seek method - to load additional sprites from the file. - -- Properly reads "P" and "CMYK" PSD images. - -- "pilconvert" no longer optimizes by default; use the -o option to - make the file as small as possible (at the expense of speed); use - the -q option to set the quality when compressing to JPEG. - -- Fixed "crop" not to drop the palette for "P" images. - -- Added and verified FLC support. - -- Paste with "L" or "RGBA" alpha is now several times faster on most - platforms. - -- Changed Image.new() to initialize the image to black, as described - in the handbook. To get an uninitialized image, use None as the - colour. - -- Fixed the PDF encoder to produce a valid header; Acrobat no longer - complains when you load PDF images created by PIL. - -- PIL only scans fully-qualified directory names in the path when - looking for plugins. - **WARNING: MAY BREAK EXISTING CODE** - -- Faster implementation of "save" used when filename is given, - or when file object has "fileno" and "flush" methods. - -- Don't crash in "crop" if region extends outside the source image. - -- Eliminated a massive memory leak in the "save" function. - -- The GIF decoder doesn't crash if the code size is set to an illegal - value. This could happen since another bug didn't handle local - palettes properly if they didn't have the same size as the - global palette (not very common). - -- Added predictor support (TIFF 6.0 section 14) to the TIFF decoder. - -- Fixed palette and padding problems in BMP driver. Now properly - writes "1", "L", "P" and "RGB" images. - -- Fixed getpixel()/getdata() to return correct pixel values. - -- Added PSD (PhotoShop) read support. Reads both uncompressed - and compressed images of most types. - -- Added GIF write support (writes "uncompressed" GIF files only, - due to unresolvable licensing issues). The "gifmaker.py" script - can be used to create GIF animations. - -- Reads 8-bit "L" and "P" TGA images. Also reads 16-bit "RGB" - images. - -- Added FLI read support. This driver has only been tested - on a few FLI samples. - -- Reads 2-bit and 4-bit PCX images. - -- Added MSP read and write support. Both version 1 and 2 can be - read, but only version 1 (uncompressed) files are written. - -- Fixed a bug in the FLI/FLC identification code that caused the - driver to raise an exception when parsing valid FLI/FLC files. - -- Improved performance when loading file format plugins, and when - opening files. - -- Added GIF animation support, via the "seek" and "tell" methods. - You can use "player.py" to play an animated GIF file. - -- Removed MNG support, since the spec is changing faster than I - can change the code. I've added support for the experimental - ARG format instead. Contact me for more information on this - format. - -- Added keyword options to the "save" method. The following options - are currently supported: - - .. list-table:: - :widths: 25 25 50 - :header-rows: 1 - - * - Format - - Option - - Description - * - JPEG - - optimize - - Minimize output file at the expense of compression speed. - * - JPEG - - progressive - - Enable progressive output. The option value is ignored. - * - JPEG - - quality - - Set compression quality (1-100). The default value is 75. - * - JPEG - - smooth - - Smooth dithered images. Value is strength (1-100). Default is off (0). - * - PNG - - optimize - - Minimize output file at the expense of compression speed. - - Expect more options in future releases. Also note that - file writers silently ignore unknown options. - -- Plugged memory leaks in the PNG and TIFF decoders. - -- Added PNG write support. - -- (internal) RGB unpackers and converters now set the pad byte - to 255 (full opacity). - -- Properly handles the "transparency" property for GIF, PNG - and XPM files. - -- Added a "putalpha" method, allowing you to attach a "1" or "L" - image as the alpha layer to an "RGBA" image. - -- Various improvements to the sample scripts: - - .. list-table:: - :widths: 25 75 - - * - pilconvert - - Carries out some extra tricks in order to make - the resulting file as small as possible. - * - explode - - (NEW) Split an image sequence into individual frames. - * - gifmaker - - (NEW) Convert a sequence file into a GIF animation. - Note that the GIF encoder create "uncompressed" GIF - files, so animations created by this script are - rather large (typically 2-5 times the compressed - sizes). - * - image2py - - (NEW) Convert a single image to a python module. See - comments in this script for details. - * - player - - If multiple images are given on the command line, - they are interpreted as frames in a sequence. The - script assumes that they all have the same size. - Also note that this script now can play FLI/FLC - and GIF animations. - - This player can also execute embedded Python - animation applets (ARG format only). - * - viewer - - Transparent images ("P" with transparency property, - and "RGBA") are superimposed on the standard Tk background. - -- Fixed colour argument to "new". For multilayer images, pass a - tuple: (Red, Green, Blue), (Red, Green, Blue, Alpha), or (Cyan, - Magenta, Yellow, Black). - -- Added XPM (X pixmap) read support. - -0.2b3 ------ - -- Added MNG (multi-image network graphics) read support. "Ming" - is a proposed animation standard, based on the PNG file format. - - You can use the "player" sample script to display some flavours - of this format. The MNG standard is still under development, - as is this driver. More information, including sample files, - can be found at - -- Added a "verify" method to images loaded from file. This method - scans the file for errors, without actually decoding the image - data, and raises a suitable exception if it finds any problems. - Currently implemented for PNG and MNG files only. - -- Added support for interlaced GIF images. - -- Added PNG read support -- if linked with the ZLIB compression library, - PIL reads all kinds of PNG images, except interlaced files. - -- Improved PNG identification support -- doesn't mess up on unknown - chunks, identifies all possible PNG modes, and verifies checksum - on PNG header chunks. - -- Added an experimental reader for placable Windows Meta Files (WMF). - This reader is still very incomplete, but it illustrates how PIL's - drawing capabilities can be used to render vector and metafile - formats. - -- Added restricted drivers for images from Image Tools (grayscale - only) and LabEye/IFUNC (common interchange modes only). - -- Some minor improvements to the sample scripts provided in the - "Scripts" directory. - -- The test images have been moved to the "Images" directory. - -0.2b2 released. 0.2b1 released for Windows only ------------------------------------------------ - -- Fixed filling of complex polygons. The ImageDraw "line" and - "polygon" methods also accept Path objects. - -- The ImageTk "PhotoImage" object can now be constructed directly - from an image. You can also pass the object itself to Tkinter, - instead of using the "image" attribute. Finally, using "paste" - on a displayed image automatically updates the display. - -- The ImageTk "BitmapImage" object allows you to create transparent - overlays from 1-bit images. You can pass the object itself to - Tkinter. The constructor takes the same arguments as the Tkinter - BitmapImage class; use the "foreground" option to set the colour - of the overlay. - -- Added a "putdata" method to the Image class. This can be used to - load a 1-layer image with data from a sequence object or a string. - An optional floating point scale and offset can be used to adjust - the data to fit into the 8-bit pixel range. Also see the "getdata" - method. - -- Added the EXTENT method to the Image "transform" method. This can - be used to quickly crop, stretch, shrink, or mirror a subregion - from another image. - -- Adapted to Python 1.4. - -- Added a project makefile for Visual C++ 4.x. This allows you to - easily build a dynamically linked version of PIL for Windows 95 - and NT. - -- A Tk "booster" patch for Windows is available. It gives dramatic - performance improvements for some displays. Has been tested with - Tk 4.2 only, but is likely to work with Tk 4.1 as well. See the Tk - subdirectory for details. - -- You can now save 1-bit images in the XBM format. In addition, the - Image class now provides a "tobitmap" method which returns a string - containing an XBM representation of the image. Quite handy to use - with Tk. - -- More conversions, including "RGB" to "1" and more. - -0.2a1 ------ - -- Where earlier versions accepted lists, this version accepts arbitrary - Python sequences (including strings, in some cases). A few resource - leaks were plugged in the process. - -- The Image "paste" method now allows the box to extend outside - the target image. The size of the box, the image to be pasted, - and the optional mask must still match. - -- The ImageDraw module now supports filled polygons, outlined and - filled ellipses, and text. Font support is rudimentary, though. - -- The Image "point" method now takes an optional mode argument, - allowing you to convert the image while translating it. Currently, - this can only be used to convert "L" or "P" images to "1" images - (creating thresholded images or "matte" masks). - -- An Image "getpixel" method has been added. For single band images, - it returns the pixel value at a given position as an integer. - For n-band images, it returns an n-tuple of integers. - -- An Image "getdata" method has been added. It returns a sequence - object representing the image as a 1-dimensional array. Only len() - and [] can be used with this sequence. This method returns a - reference to the existing image data, so changes in the image - will be immediately reflected in the sequence object. - -- Fixed alignment problems in the Windows BMP writer. - -- If converting an "RGB" image to "RGB" or "L", you can give a second - argument containing a colour conversion matrix. - -- An Image "getbbox" method has been added. It returns the bounding - box of data in an image, considering the value 0 as background. - -- An Image "offset" method has been added. It returns a new image - where the contents of the image have been offset the given distance - in X and/or Y direction. Data wraps between edges. - -- Saves PDF images. The driver creates a binary PDF 1.1 files, using - JPEG compression for "L", "RGB", and "CMYK" images, and hex encoding - (same as for PostScript) for other formats. - -- The "paste" method now accepts "1" masks. Zero means transparent, - any other pixel value means opaque. This is faster than using an - "L" transparency mask. - -- Properly writes EPS files (and properly prints images to PostScript - printers as well). - -- Reads 4-bit BMP files, as well as 4 and 8-bit Windows ICO and CUR - files. Cursor animations are not supported. - -- Fixed alignment problems in the Sun raster loader. - -- Added "draft" and "thumbnail" methods. The draft method is used - to optimize loading of JPEG and PCD files, the thumbnail method is - used to create a thumbnail representation of an image. - -- Added Windows display support, via the ImageWin class (see the - handbook for details). - -- Added raster conversion for EPS files. This requires GNU or Aladdin - Ghostscript, and probably works on UNIX only. - -- Reads PhotoCD (PCD) images. The base resolution (768x512) can be - read from a PhotoCD file. - -- Eliminated some compiler warnings. Bindings now compile cleanly in C++ - mode. Note that the Imaging library itself must be compiled in C mode. - -- Added "bdf2pil.py", which converts BDF fonts into images with associated - metrics. This is definitely work in progress. For info, see description - in script for details. - -- Fixed a bug in the "ImageEnhance.py" module. - -- Fixed a bug in the netpbm save hack in "GifImagePlugin.py" - -- Fixed 90 and 270 degree rotation of rectangular images. - -- Properly reads 8-bit TIFF palette-color images. - -- Reads plane separated RGB and CMYK TIFF images. - -- Added driver debug mode. This is enabled by setting Image.DEBUG - to a non-zero value. Try the -D option to "pilfile.py" and see what - happens. - -- Don't crash on "atend" constructs in PostScript files. - -- Only the Image module imports _imaging directly. Other modules - should refer to the binding module as "Image.core". - -0.0 to 0.1 (b1) ---------------- - -- A handbook is available (distributed separately). - -- The coordinate system is changed so that (0,0) is now located - in the upper left corner. This is in compliance with ISO 12087 - and 90% of all other image processing and graphics libraries. - -- Modes "1" (bilevel) and "P" (palette) have been introduced. Note - that bilevel images are stored with one byte per pixel. - -- The Image "crop" and "paste" methods now accepts None as the - box argument, to refer to the full image (self, that is). - -- The Image "crop" method now works properly. - -- The Image "point" method is now available. You can use either a - lookup table or a function taking one argument. - -- The Image join function has been renamed to "merge". - -- An Image "composite" function has been added. It is identical - to copy() followed by paste(mask). - -- An Image "eval" function has been added. It is currently identical - to point(function); that is, only a single image can be processed. - -- A set of channel operations has been added. See the "ImageChops" - module, test_chops.py, and the handbook for details. - -- Added the "pilconvert" utility, which converts image files. Note - that the number of output formats are still quite restricted. - -- Added the "pilfile" utility, which quickly identifies image files - (without loading them, in most cases). - -- Added the "pilprint" utility, which prints image files to PostScript - printers. - -- Added a rudimentary version of the "pilview" utility, which is - simple image viewer based on Tk. Only File/Exit and Image/Next - works properly. - -- An interface to Tk has been added. See "Lib/ImageTk.py" and README - for details. - -- An interface to Jack Jansen's Img library has been added (thanks to - Jack). This allows you to read images through the Img extensions file - format handlers. See the file "Lib/ImgExtImagePlugin.py" for details. - -- PostScript printing is provided through the PSDraw module. See the - handbook for details. + +Changelog (Pillow) +================== + +10.4.0 (unreleased) +------------------- + +- Accept 't' suffix for libtiff version #8126, #8129 + [radarhere] + +- Deprecate ImageDraw.getdraw hints parameter #8124 + [radarhere, hugovk] + +- Added ImageDraw circle() #8085 + [void4, hugovk, radarhere] + +- Add mypy target to Makefile #8077 + [Yay295] + +- Added more modes to Image.MODES #7984 + [radarhere] + +- Deprecate BGR;15, BGR;16 and BGR;24 modes #7978 + [radarhere, hugovk] + +- Fix ImagingAccess for I;16N on big-endian #7921 + [Yay295, radarhere] + +- Support reading P mode TIFF images with padding #7996 + [radarhere] + +- Deprecate support for libtiff < 4 #7998 + [radarhere, hugovk] + +- Corrected ImageShow UnixViewer command #7987 + [radarhere] + +- Use functools.cached_property in ImageStat #7952 + [nulano, hugovk, radarhere] + +- Add support for reading BITMAPV2INFOHEADER and BITMAPV3INFOHEADER #7956 + [Cirras, radarhere] + +- Support reading CMYK JPEG2000 images #7947 + [radarhere] + +10.3.0 (2024-04-01) +------------------- + +- CVE-2024-28219: Use ``strncpy`` to avoid buffer overflow #7928 + [radarhere, hugovk] + +- Deprecate ``eval()``, replacing it with ``lambda_eval()`` and ``unsafe_eval()`` #7927 + [radarhere, hugovk] + +- Raise ``ValueError`` if seeking to greater than offset-sized integer in TIFF #7883 + [radarhere] + +- Add ``--report`` argument to ``__main__.py`` to omit supported formats #7818 + [nulano, radarhere, hugovk] + +- Added RGB to I;16, I;16L, I;16B and I;16N conversion #7918, #7920 + [radarhere] + +- Fix editable installation with custom build backend and configuration options #7658 + [nulano, radarhere] + +- Fix putdata() for I;16N on big-endian #7209 + [Yay295, hugovk, radarhere] + +- Determine MPO size from markers, not EXIF data #7884 + [radarhere] + +- Improved conversion from RGB to RGBa, LA and La #7888 + [radarhere] + +- Support FITS images with GZIP_1 compression #7894 + [radarhere] + +- Use I;16 mode for 9-bit JPEG 2000 images #7900 + [scaramallion, radarhere] + +- Raise ValueError if kmeans is negative #7891 + [radarhere] + +- Remove TIFF tag OSUBFILETYPE when saving using libtiff #7893 + [radarhere] + +- Raise ValueError for negative values when loading P1-P3 PPM images #7882 + [radarhere] + +- Added reading of JPEG2000 palettes #7870 + [radarhere] + +- Added alpha_quality argument when saving WebP images #7872 + [radarhere] + +- Fixed joined corners for ImageDraw rounded_rectangle() non-integer dimensions #7881 + [radarhere] + +- Stop reading EPS image at EOF marker #7753 + [radarhere] + +- PSD layer co-ordinates may be negative #7706 + [radarhere] + +- Use subprocess with CREATE_NO_WINDOW flag in ImageShow WindowsViewer #7791 + [radarhere] + +- When saving GIF frame that restores to background color, do not fill identical pixels #7788 + [radarhere] + +- Fixed reading PNG iCCP compression method #7823 + [radarhere] + +- Allow writing IFDRational to UNDEFINED tag #7840 + [radarhere] + +- Fix logged tag name when loading Exif data #7842 + [radarhere] + +- Use maximum frame size in IHDR chunk when saving APNG images #7821 + [radarhere] + +- Prevent opening P TGA images without a palette #7797 + [radarhere] + +- Use palette when loading ICO images #7798 + [radarhere] + +- Use consistent arguments for load_read and load_seek #7713 + [radarhere] + +- Turn off nullability warnings for macOS SDK #7827 + [radarhere] + +- Fix shift-sign issue in Convert.c #7838 + [r-barnes, radarhere] + +- Open 16-bit grayscale PNGs as I;16 #7849 + [radarhere] + +- Handle truncated chunks at the end of PNG images #7709 + [lajiyuan, radarhere] + +- Match mask size to pasted image size in GifImagePlugin #7779 + [radarhere] + +- Release GIL while calling ``WebPAnimDecoderGetNext`` #7782 + [evanmiller, radarhere] + +- Fixed reading FLI/FLC images with a prefix chunk #7804 + [twolife] + +- Update wl-paste handling and return None for some errors in grabclipboard() on Linux #7745 + [nik012003, radarhere] + +- Remove execute bit from ``setup.py`` #7760 + [hugovk] + +- Do not support using test-image-results to upload images after test failures #7739 + [radarhere] + +- Changed ImageMath.ops to be static #7721 + [radarhere] + +- Fix APNG info after seeking backwards more than twice #7701 + [esoma, radarhere] + +- Deprecate ImageCms constants and versions() function #7702 + [nulano, radarhere] + +- Added PerspectiveTransform #7699 + [radarhere] + +- Add support for reading and writing grayscale PFM images #7696 + [nulano, hugovk] + +- Add LCMS2 flags to ImageCms #7676 + [nulano, radarhere, hugovk] + +- Rename x64 to AMD64 in winbuild #7693 + [nulano] + +10.2.0 (2024-01-02) +------------------- + +- Add ``keep_rgb`` option when saving JPEG to prevent conversion of RGB colorspace #7553 + [bgilbert, radarhere] + +- Trim glyph size in ImageFont.getmask() #7669, #7672 + [radarhere, nulano] + +- Deprecate IptcImagePlugin helpers #7664 + [nulano, hugovk, radarhere] + +- Allow uncompressed TIFF images to be saved in chunks #7650 + [radarhere] + +- Concatenate multiple JPEG EXIF markers #7496 + [radarhere] + +- Changed IPTC tile tuple to match other plugins #7661 + [radarhere] + +- Do not assign new fp attribute when exiting context manager #7566 + [radarhere] + +- Support arbitrary masks for uncompressed RGB DDS images #7589 + [radarhere, akx] + +- Support setting ROWSPERSTRIP tag #7654 + [radarhere] + +- Apply ImageFont.MAX_STRING_LENGTH to ImageFont.getmask() #7662 + [radarhere] + +- Optimise ``ImageColor`` using ``functools.lru_cache`` #7657 + [hugovk] + +- Restricted environment keys for ImageMath.eval() #7655 + [wiredfool, radarhere] + +- Optimise ``ImageMode.getmode`` using ``functools.lru_cache`` #7641 + [hugovk, radarhere] + +- Fix incorrect color blending for overlapping glyphs #7497 + [ZachNagengast, nulano, radarhere] + +- Attempt memory mapping when tile args is a string #7565 + [radarhere] + +- Fill identical pixels with transparency in subsequent frames when saving GIF #7568 + [radarhere] + +- Corrected duration when combining multiple GIF frames into single frame #7521 + [radarhere] + +- Handle disposing GIF background from outside palette #7515 + [radarhere] + +- Seek past the data when skipping a PSD layer #7483 + [radarhere] + +- Import plugins relative to the module #7576 + [deliangyang, jaxx0n] + +- Translate encoder error codes to strings; deprecate ``ImageFile.raise_oserror()`` #7609 + [bgilbert, radarhere] + +- Support reading BC4U and DX10 BC1 images #6486 + [REDxEYE, radarhere, hugovk] + +- Optimize ImageStat.Stat.extrema #7593 + [florath, radarhere] + +- Handle pathlib.Path in FreeTypeFont #7578 + [radarhere, hugovk, nulano] + +- Added support for reading DX10 BC4 DDS images #7603 + [sambvfx, radarhere] + +- Optimized ImageStat.Stat.count #7599 + [florath] + +- Correct PDF palette size when saving #7555 + [radarhere] + +- Fixed closing file pointer with olefile 0.47 #7594 + [radarhere] + +- Raise ValueError when TrueType font size is not greater than zero #7584, #7587 + [akx, radarhere] + +- If absent, do not try to close fp when closing image #7557 + [RaphaelVRossi, radarhere] + +- Allow configuring JPEG restart marker interval on save #7488 + [bgilbert, radarhere] + +- Decrement reference count for PyObject #7549 + [radarhere] + +- Implement ``streamtype=1`` option for tables-only JPEG encoding #7491 + [bgilbert, radarhere] + +- If save_all PNG only has one frame, do not create animated image #7522 + [radarhere] + +- Fixed frombytes() for images with a zero dimension #7493 + [radarhere] + +10.1.0 (2023-10-15) +------------------- + +- Added TrueType default font to allow for different sizes #7354 + [radarhere] + +- Fixed invalid argument warning #7442 + [radarhere] + +- Added ImageOps cover method #7412 + [radarhere, hugovk] + +- Catch struct.error from truncated EXIF when reading JPEG DPI #7458 + [radarhere] + +- Consider default image when selecting mode for PNG save_all #7437 + [radarhere] + +- Support BGR;15, BGR;16 and BGR;24 access, unpacking and putdata #7303 + [radarhere] + +- Added CMYK to RGB unpacker #7310 + [radarhere] + +- Improved flexibility of XMP parsing #7274 + [radarhere] + +- Support reading 8-bit YCbCr TIFF images #7415 + [radarhere] + +- Allow saving I;16B images as PNG #7302 + [radarhere] + +- Corrected drawing I;16 points and writing I;16 text #7257 + [radarhere] + +- Set blue channel to 128 for BC5S #7413 + [radarhere] + +- Increase flexibility when reading IPTC fields #7319 + [radarhere] + +- Set C palette to be empty by default #7289 + [radarhere] + +- Added gs_binary to control Ghostscript use on all platforms #7392 + [radarhere] + +- Read bounding box information from the trailer of EPS files if specified #7382 + [nopperl, radarhere] + +- Added reading 8-bit color DDS images #7426 + [radarhere] + +- Added has_transparency_data #7420 + [radarhere, hugovk] + +- Fixed bug when reading BC5S DDS images #7401 + [radarhere] + +- Prevent TIFF orientation from being applied more than once #7383 + [radarhere] + +- Use previous pixel alpha for QOI_OP_RGB #7357 + [radarhere] + +- Added BC5U reading #7358 + [radarhere] + +- Allow getpixel() to accept a list #7355 + [radarhere, homm] + +- Allow GaussianBlur and BoxBlur to accept a sequence of x and y radii #7336 + [radarhere] + +- Expand JPEG buffer size when saving optimized or progressive #7345 + [radarhere] + +- Added session type check for Linux in ImageGrab.grabclipboard() #7332 + [TheNooB2706, radarhere, hugovk] + +- Allow "loop=None" when saving GIF images #7329 + [radarhere] + +- Fixed transparency when saving P mode images to PDF #7323 + [radarhere] + +- Added saving LA images as PDFs #7299 + [radarhere] + +- Set SMaskInData to 1 for PDFs with alpha #7316, #7317 + [radarhere] + +- Changed Image mode property to be read-only by default #7307 + [radarhere] + +- Silence exceptions in _repr_jpeg_ and _repr_png_ #7266 + [mtreinish, radarhere] + +- Do not use transparency when saving GIF if it has been removed when normalizing mode #7284 + [radarhere] + +- Fix missing symbols when libtiff depends on libjpeg #7270 + [heitbaum] + +10.0.1 (2023-09-15) +------------------- + +- Updated libwebp to 1.3.2 #7395 + [radarhere] + +- Updated zlib to 1.3 #7344 + [radarhere] + +10.0.0 (2023-07-01) +------------------- + +- Fixed deallocating mask images #7246 + [radarhere] + +- Added ImageFont.MAX_STRING_LENGTH #7244 + [radarhere, hugovk] + +- Fix Windows build with pyproject.toml #7230 + [hugovk, nulano, radarhere] + +- Do not close provided file handles with libtiff #7199 + [radarhere] + +- Convert to HSV if mode is HSV in getcolor() #7226 + [radarhere] + +- Added alpha_only argument to getbbox() #7123 + [radarhere. hugovk] + +- Prioritise speed in _repr_png_ #7242 + [radarhere] + +- Do not use CFFI access by default on PyPy #7236 + [radarhere] + +- Limit size even if one dimension is zero in decompression bomb check #7235 + [radarhere] + +- Use --config-settings instead of deprecated --global-option #7171 + [radarhere] + +- Better C integer definitions #6645 + [Yay295, hugovk] + +- Fixed finding dependencies on Cygwin #7175 + [radarhere] + +- Changed grabclipboard() to use PNG instead of JPG compression on macOS #7219 + [abey79, radarhere] + +- Added in_place argument to ImageOps.exif_transpose() #7092 + [radarhere] + +- Fixed calling putpalette() on L and LA images before load() #7187 + [radarhere] + +- Fixed saving TIFF multiframe images with LONG8 tag types #7078 + [radarhere] + +- Fixed combining single duration across duplicate APNG frames #7146 + [radarhere] + +- Remove temporary file when error is raised #7148 + [radarhere] + +- Do not use temporary file when grabbing clipboard on Linux #7200 + [radarhere] + +- If the clipboard fails to open on Windows, wait and try again #7141 + [radarhere] + +- Fixed saving multiple 1 mode frames to GIF #7181 + [radarhere] + +- Replaced absolute PIL import with relative import #7173 + [radarhere] + +- Replaced deprecated Py_FileSystemDefaultEncoding for Python >= 3.12 #7192 + [radarhere] + +- Improved wl-paste mimetype handling in ImageGrab #7094 + [rrcgat, radarhere] + +- Added _repr_jpeg_() for IPython display_jpeg #7135 + [n3011, radarhere, nulano] + +- Use "/sbin/ldconfig" if ldconfig is not found #7068 + [radarhere] + +- Prefer screenshots using XCB over gnome-screenshot #7143 + [nulano, radarhere] + +- Fixed joined corners for ImageDraw rounded_rectangle() odd dimensions #7151 + [radarhere] + +- Support reading signed 8-bit TIFF images #7111 + [radarhere] + +- Added width argument to ImageDraw regular_polygon #7132 + [radarhere] + +- Support I mode for ImageFilter.BuiltinFilter #7108 + [radarhere] + +- Raise error from stderr of Linux ImageGrab.grabclipboard() command #7112 + [radarhere] + +- Added unpacker from I;16B to I;16 #7125 + [radarhere] + +- Support float font sizes #7107 + [radarhere] + +- Use later value for duplicate xref entries in PdfParser #7102 + [radarhere] + +- Load before getting size in __getstate__ #7105 + [bigcat88, radarhere] + +- Fixed type handling for include and lib directories #7069 + [adisbladis, radarhere] + +- Remove deprecations for Pillow 10.0.0 #7059, #7080 + [hugovk, radarhere] + +- Drop support for soon-EOL Python 3.7 #7058 + [hugovk, radarhere] + +9.5.0 (2023-04-01) +------------------ + +- Added ImageSourceData to TAGS_V2 #7053 + [radarhere] + +- Clear PPM half token after use #7052 + [radarhere] + +- Removed absolute path to ldconfig #7044 + [radarhere] + +- Support custom comments and PLT markers when saving JPEG2000 images #6903 + [joshware, radarhere, hugovk] + +- Load before getting size in __array_interface__ #7034 + [radarhere] + +- Support creating BGR;15, BGR;16 and BGR;24 images, but drop support for BGR;32 #7010 + [radarhere] + +- Consider transparency when applying APNG blend mask #7018 + [radarhere] + +- Round duration when saving animated WebP images #6996 + [radarhere] + +- Added reading of JPEG2000 comments #6909 + [radarhere] + +- Decrement reference count #7003 + [radarhere, nulano] + +- Allow libtiff_support_custom_tags to be missing #7020 + [radarhere] + +- Improved I;16N support #6834 + [radarhere] + +- Added QOI reading #6852 + [radarhere, hugovk] + +- Added saving RGBA images as PDFs #6925 + [radarhere] + +- Do not raise an error if os.environ does not contain PATH #6935 + [radarhere, hugovk] + +- Close OleFileIO instance when closing or exiting FPX or MIC #7005 + [radarhere] + +- Added __int__ to IFDRational for Python >= 3.11 #6998 + [radarhere] + +- Added memoryview support to Dib.frombytes() #6988 + [radarhere, nulano] + +- Close file pointer copy in the libtiff encoder if still open #6986 + [fcarron, radarhere] + +- Raise an error if ImageDraw co-ordinates are incorrectly ordered #6978 + [radarhere] + +- Added "corners" argument to ImageDraw rounded_rectangle() #6954 + [radarhere] + +- Added memoryview support to frombytes() #6974 + [radarhere] + +- Allow comments in FITS images #6973 + [radarhere] + +- Support saving PDF with different X and Y resolutions #6961 + [jvanderneutstulen, radarhere, hugovk] + +- Fixed writing int as UNDEFINED tag #6950 + [radarhere] + +- Raise an error if EXIF data is too long when saving JPEG #6939 + [radarhere] + +- Handle more than one directory returned by pkg-config #6896 + [sebastic, radarhere] + +- Do not retry past formats when loading all formats for the first time #6902 + [radarhere] + +- Do not retry specified formats if they failed when opening #6893 + [radarhere] + +- Do not unintentionally load TIFF format at first #6892 + [radarhere] + +- Stop reading when EPS line becomes too long #6897 + [radarhere] + +- Allow writing IFDRational to BYTE tag #6890 + [radarhere] + +- Raise ValueError for BoxBlur filter with negative radius #6874 + [hugovk, radarhere] + +- Support arbitrary number of loaded modules on Windows #6761 + [javidcf, radarhere, nulano] + +9.4.0 (2023-01-02) +------------------ + +- Fixed null pointer dereference crash with malformed font #6846 + [wiredfool, radarhere] + +- Return from ImagingFill early if image has a zero dimension #6842 + [radarhere] + +- Reversed deprecations for Image constants, except for duplicate Resampling attributes #6830 + [radarhere] + +- Improve exception traceback readability #6836 + [hugovk, radarhere] + +- Do not attempt to read IFD1 if absent #6840 + [radarhere] + +- Fixed writing int as ASCII tag #6800 + [radarhere] + +- If available, use wl-paste or xclip for grabclipboard() on Linux #6783 + [radarhere] + +- Added signed option when saving JPEG2000 images #6709 + [radarhere] + +- Patch OpenJPEG to include ARM64 fix #6718 + [radarhere] + +- Added support for I;16 modes in putdata() #6825 + [radarhere] + +- Added conversion from RGBa to RGB #6708 + [radarhere] + +- Added DDS support for uncompressed L and LA images #6820 + [radarhere, REDxEYE] + +- Added LightSource tag values to ExifTags #6749 + [radarhere] + +- Fixed PyAccess after changing ICO size #6821 + [radarhere] + +- Do not use EXIF from info when saving PNG images #6819 + [radarhere] + +- Fixed saving EXIF data to MPO #6817 + [radarhere] + +- Added Exif hide_offsets() #6762 + [radarhere] + +- Only compare to previous frame when checking for duplicate GIF frames while saving #6787 + [radarhere] + +- Always initialize all plugins in registered_extensions() #6811 + [radarhere] + +- Ignore non-opaque WebP background when saving as GIF #6792 + [radarhere] + +- Only set tile in ImageFile __setstate__ #6793 + [radarhere] + +- When reading BLP, do not trust JPEG decoder to determine image is CMYK #6767 + [radarhere] + +- Added IFD enum to ExifTags #6748 + [radarhere] + +- Fixed bug combining GIF frame durations #6779 + [radarhere] + +- Support saving JPEG comments #6774 + [smason, radarhere] + +- Added getxmp() to WebPImagePlugin #6758 + [radarhere] + +- Added "exact" option when saving WebP #6747 + [ashafaei, radarhere] + +- Use fractional coordinates when drawing text #6722 + [radarhere] + +- Fixed writing int as BYTE tag #6740 + [radarhere] + +- Added MP Format Version when saving MPO #6735 + [radarhere] + +- Added Interop to ExifTags #6724 + [radarhere] + +- CVE-2007-4559 patch when building on Windows #6704 + [TrellixVulnTeam, nulano, radarhere] + +- Fix compiler warning: accessing 64 bytes in a region of size 48 #6714 + [wiredfool] + +- Use verbose flag for pip install #6713 + [wiredfool, radarhere] + +9.3.0 (2022-10-29) +------------------ + +- Limit SAMPLESPERPIXEL to avoid runtime DOS #6700 + [wiredfool] + +- Initialize libtiff buffer when saving #6699 + [radarhere] + +- Inline fname2char to fix memory leak #6329 + [nulano] + +- Fix memory leaks related to text features #6330 + [nulano] + +- Use double quotes for version check on old CPython on Windows #6695 + [hugovk] + +- Remove backup implementation of Round for Windows platforms #6693 + [cgohlke] + +- Fixed set_variation_by_name offset #6445 + [radarhere] + +- Fix malloc in _imagingft.c:font_setvaraxes #6690 + [cgohlke] + +- Release Python GIL when converting images using matrix operations #6418 + [hmaarrfk] + +- Added ExifTags enums #6630 + [radarhere] + +- Do not modify previous frame when calculating delta in PNG #6683 + [radarhere] + +- Added support for reading BMP images with RLE4 compression #6674 + [npjg, radarhere] + +- Decode JPEG compressed BLP1 data in original mode #6678 + [radarhere] + +- Added GPS TIFF tag info #6661 + [radarhere] + +- Added conversion between RGB/RGBA/RGBX and LAB #6647 + [radarhere] + +- Do not attempt normalization if mode is already normal #6644 + [radarhere] + +- Fixed seeking to an L frame in a GIF #6576 + [radarhere] + +- Consider all frames when selecting mode for PNG save_all #6610 + [radarhere] + +- Don't reassign crc on ChunkStream close #6627 + [wiredfool, radarhere] + +- Raise a warning if NumPy failed to raise an error during conversion #6594 + [radarhere] + +- Show all frames in ImageShow #6611 + [radarhere] + +- Allow FLI palette chunk to not be first #6626 + [radarhere] + +- If first GIF frame has transparency for RGB_ALWAYS loading strategy, use RGBA mode #6592 + [radarhere] + +- Round box position to integer when pasting embedded color #6517 + [radarhere, nulano] + +- Removed EXIF prefix when saving WebP #6582 + [radarhere] + +- Pad IM palette to 768 bytes when saving #6579 + [radarhere] + +- Added DDS BC6H reading #6449 + [ShadelessFox, REDxEYE, radarhere] + +- Added support for opening WhiteIsZero 16-bit integer TIFF images #6642 + [JayWiz, radarhere] + +- Raise an error when allocating translucent color to RGB palette #6654 + [jsbueno, radarhere] + +- Added reading of TIFF child images #6569 + [radarhere] + +- Improved ImageOps palette handling #6596 + [PososikTeam, radarhere] + +- Defer parsing of palette into colors #6567 + [radarhere] + +- Apply transparency to P images in ImageTk.PhotoImage #6559 + [radarhere] + +- Use rounding in ImageOps contain() and pad() #6522 + [bibinhashley, radarhere] + +- Fixed GIF remapping to palette with duplicate entries #6548 + [radarhere] + +- Allow remap_palette() to return an image with less than 256 palette entries #6543 + [radarhere] + +- Corrected BMP and TGA palette size when saving #6500 + [radarhere] + +- Do not call load() before draft() in Image.thumbnail #6539 + [radarhere] + +- Copy palette when converting from P to PA #6497 + [radarhere] + +- Allow RGB and RGBA values for PA image putpixel #6504 + [radarhere] + +- Removed support for tkinter in PyPy before Python 3.6 #6551 + [nulano] + +- Do not use CCITTFaxDecode filter if libtiff is not available #6518 + [radarhere] + +- Fallback to not using mmap if buffer is not large enough #6510 + [radarhere] + +- Fixed writing bytes as ASCII tag #6493 + [radarhere] + +- Open 1 bit EPS in mode 1 #6499 + [radarhere] + +- Removed support for tkinter before Python 1.5.2 #6549 + [radarhere] + +- Allow default ImageDraw font to be set #6484 + [radarhere, hugovk] + +- Save 1 mode PDF using CCITTFaxDecode filter #6470 + [radarhere] + +- Added support for RGBA PSD images #6481 + [radarhere] + +- Parse orientation from XMP tag contents #6463 + [bigcat88, radarhere] + +- Added support for reading ATI1/ATI2 (BC4/BC5) DDS images #6457 + [REDxEYE, radarhere] + +- Do not clear GIF tile when checking number of frames #6455 + [radarhere] + +- Support saving multiple MPO frames #6444 + [radarhere] + +- Do not double quote Pillow version for setuptools >= 60 #6450 + [radarhere] + +- Added ABGR BMP mask mode #6436 + [radarhere] + +- Fixed PSDraw rectangle #6429 + [radarhere] + +- Raise ValueError if PNG sRGB chunk is truncated #6431 + [radarhere] + +- Handle missing Python executable in ImageShow on macOS #6416 + [bryant1410, radarhere] + +9.2.0 (2022-07-01) +------------------ + +- Deprecate ImageFont.getsize and related functions #6381 + [nulano, radarhere] + +- Fixed null check for fribidi_version_info in FriBiDi shim #6376 + [nulano] + +- Added GIF decompression bomb check #6402 + [radarhere] + +- Handle PCF fonts files with less than 256 characters #6386 + [dawidcrivelli, radarhere] + +- Improved GIF optimize condition #6378 + [raygard, radarhere] + +- Reverted to __array_interface__ with the release of NumPy 1.23 #6394 + [radarhere] + +- Pad PCX palette to 768 bytes when saving #6391 + [radarhere] + +- Fixed bug with rounding pixels to palette colors #6377 + [btrekkie, radarhere] + +- Use gnome-screenshot on Linux if available #6361 + [radarhere, nulano] + +- Fixed loading L mode BMP RLE8 images #6384 + [radarhere] + +- Fixed incorrect operator in ImageCms error #6370 + [LostBenjamin, hugovk, radarhere] + +- Limit FPX tile size to avoid extending outside image #6368 + [radarhere] + +- Added support for decoding plain PPM formats #5242 + [Piolie, radarhere] + +- Added apply_transparency() #6352 + [radarhere] + +- Fixed behaviour change from endian fix #6197 + [radarhere] + +- Allow remapping P images with RGBA palettes #6350 + [radarhere] + +- Fixed drawing translucent 1px high polygons #6278 + [radarhere] + +- Pad COLORMAP to 768 items when saving TIFF #6232 + [radarhere] + +- Fix P -> PA conversion #6337 + [RedShy, radarhere] + +- Once exif data is parsed, do not reload unless it changes #6335 + [radarhere] + +- Only try to connect discontiguous corners at the end of edges #6303 + [radarhere] + +- Improve transparency handling when saving GIF images #6176 + [radarhere] + +- Do not update GIF frame position until local image is found #6219 + [radarhere] + +- Netscape GIF extension belongs after the global color table #6211 + [radarhere] + +- Only write GIF comments at the beginning of the file #6300 + [raygard, radarhere] + +- Separate multiple GIF comment blocks with newlines #6294 + [raygard, radarhere] + +- Always use GIF89a for comments #6292 + [raygard, radarhere] + +- Ignore compression value from BMP info dictionary when saving as TIFF #6231 + [radarhere] + +- If font is file-like object, do not re-read from object to get variant #6234 + [radarhere] + +- Raise ValueError when trying to access internal fp after close #6213 + [radarhere] + +- Support more affine expression forms in im.point() #6254 + [benrg, radarhere] + +- Populate Python palette in fromarray() #6283 + [radarhere] + +- Raise ValueError if PNG chunks are truncated #6253 + [radarhere] + +- Use durations from each frame by default when saving GIFs #6265 + [radarhere] + +- Adjust BITSPERSAMPLE to match SAMPLESPERPIXEL when opening TIFFs #6270 + [radarhere] + +- Search pkgconf system libs/cflags #6138 + [jameshilliard, radarhere] + +- Raise ValueError for invalid PPM maxval #6242 + [radarhere] + +- Corrected screencapture argument in ImageGrab.grab() #6244 + [axt-one] + +- Deprecate support for Qt 5 (PyQt5 and PySide2) #6237 + [hugovk, radarhere] + +- Increase wait time of temporary file deletion on Windows #6224 + [AlexTedeschi] + +- Deprecate FreeTypeFont.getmask2 fill parameter #6220 + [nulano, radarhere, hugovk] + +- Round lut values where necessary #6188 + [radarhere] + +- Load before getting size in resize() #6190 + [radarhere] + +- Load image before performing size calculations in thumbnail() #6186 + [radarhere] + +- Deprecated PhotoImage.paste() box parameter #6178 + [radarhere] + +9.1.1 (2022-05-17) +------------------ + +- When reading past the end of a TGA scan line, reduce bytes left. CVE-2022-30595 + [radarhere] + +- Do not open images with zero or negative height #6269 + [radarhere] + +9.1.0 (2022-04-01) +------------------ + +- Add support for multiple component transformation to JPEG2000 #5500 + [scaramallion, radarhere, hugovk] + +- Fix loading FriBiDi on Alpine #6165 + [nulano] + +- Added setting for converting GIF P frames to RGB #6150 + [radarhere] + +- Allow 1 mode images to be inverted #6034 + [radarhere] + +- Raise ValueError when trying to save empty JPEG #6159 + [radarhere] + +- Always save TIFF with contiguous planar configuration #5973 + [radarhere] + +- Connected discontiguous polygon corners #5980 + [radarhere] + +- Ensure Tkinter hook is activated for getimage() #6032 + [radarhere] + +- Use screencapture arguments to crop on macOS #6152 + [radarhere] + +- Do not mark L mode JPEG as 1 bit in PDF #6151 + [radarhere] + +- Added support for reading I;16R TIFF images #6132 + [radarhere] + +- If an error occurs after creating a file, remove the file #6134 + [radarhere] + +- Fixed calling DisplayViewer or XVViewer without a title #6136 + [radarhere] + +- Retain RGBA transparency when saving multiple GIF frames #6128 + [radarhere] + +- Save additional ICO frames with other bit depths if supplied #6122 + [radarhere] + +- Handle EXIF data truncated to just the header #6124 + [radarhere] + +- Added support for reading BMP images with RLE8 compression #6102 + [radarhere] + +- Support Python distributions where _tkinter is compiled in #6006 + [lukegb] + +- Added support for PPM arbitrary maxval #6119 + [radarhere] + +- Added BigTIFF reading #6097 + [radarhere] + +- When converting, clip I;16 to be unsigned, not signed #6112 + [radarhere] + +- Fixed loading L mode GIF with transparency #6086 + [radarhere] + +- Improved handling of PPM header #5121 + [Piolie, radarhere] + +- Reset size when seeking away from "Large Thumbnail" MPO frame #6101 + [radarhere] + +- Replace requirements.txt with extras #6072 + [hugovk, radarhere] + +- Added PyEncoder and support BLP saving #6069 + [radarhere] + +- Handle TGA images with packets that cross scan lines #6087 + [radarhere] + +- Added FITS reading #6056 + [radarhere, hugovk] + +- Added rawmode argument to Image.getpalette() #6061 + [radarhere] + +- Fixed BUFR, GRIB and HDF5 stub saving #6071 + [radarhere] + +- Do not automatically remove temporary ImageShow files on Unix #6045 + [radarhere] + +- Correctly read JPEG compressed BLP images #4685 + [Meithal, radarhere] + +- Merged _MODE_CONV typ into ImageMode as typestr #6057 + [radarhere] + +- Consider palette size when converting and in getpalette() #6060 + [radarhere] + +- Added enums #5954 + [radarhere] + +- Ensure image is opaque after converting P to PA with RGB palette #6052 + [radarhere] + +- Attach RGBA palettes from putpalette() when suitable #6054 + [radarhere] + +- Added get_photoshop_blocks() to parse Photoshop TIFF tag #6030 + [radarhere] + +- Drop excess values in BITSPERSAMPLE #6041 + [mikhail-iurkov] + +- Added unpacker from RGBA;15 to RGB #6031 + [radarhere] + +- Enable arm64 for MSVC on Windows #5811 + [gaborkertesz-linaro, gaborkertesz] + +- Keep IPython/Jupyter text/plain output stable #5891 + [shamrin, radarhere] + +- Raise an error when performing a negative crop #5972 + [radarhere, hugovk] + +- Deprecated show_file "file" argument in favour of "path" #5959 + [radarhere] + +- Fixed SPIDER images for use with Bio-formats library #5956 + [radarhere] + +- Ensure duplicated file pointer is closed #5946 + [radarhere] + +- Added specific error if path coordinate type is incorrect #5942 + [radarhere] + +- Return an empty bytestring from tobytes() for an empty image #5938 + [radarhere] + +- Remove readonly from Image.__eq__ #5930 + [hugovk] + +9.0.1 (2022-02-03) +------------------ + +- In show_file, use os.remove to remove temporary images. CVE-2022-24303 #6010 + [radarhere, hugovk] + +- Restrict builtins within lambdas for ImageMath.eval. CVE-2022-22817 #6009 + [radarhere] + +9.0.0 (2022-01-02) +------------------ + +- Restrict builtins for ImageMath.eval(). CVE-2022-22817 #5923 + [radarhere] + +- Ensure JpegImagePlugin stops at the end of a truncated file #5921 + [radarhere] + +- Fixed ImagePath.Path array handling. CVE-2022-22815, CVE-2022-22816 #5920 + [radarhere] + +- Remove consecutive duplicate tiles that only differ by their offset #5919 + [radarhere] + +- Improved I;16 operations on big endian #5901 + [radarhere] + +- Limit quantized palette to number of colors #5879 + [radarhere] + +- Fixed palette index for zeroed color in FASTOCTREE quantize #5869 + [radarhere] + +- When saving RGBA to GIF, make use of first transparent palette entry #5859 + [radarhere] + +- Pass SAMPLEFORMAT to libtiff #5848 + [radarhere] + +- Added rounding when converting P and PA #5824 + [radarhere] + +- Improved putdata() documentation and data handling #5910 + [radarhere] + +- Exclude carriage return in PDF regex to help prevent ReDoS #5912 + [hugovk] + +- Fixed freeing pointer in ImageDraw.Outline.transform #5909 + [radarhere] + +- Added ImageShow support for xdg-open #5897 + [m-shinder, radarhere] + +- Support 16-bit grayscale ImageQt conversion #5856 + [cmbruns, radarhere] + +- Convert subsequent GIF frames to RGB or RGBA #5857 + [radarhere] + +- Do not prematurely return in ImageFile when saving to stdout #5665 + [infmagic2047, radarhere] + +- Added support for top right and bottom right TGA orientations #5829 + [radarhere] + +- Corrected ICNS file length in header #5845 + [radarhere] + +- Block tile TIFF tags when saving #5839 + [radarhere] + +- Added line width argument to polygon #5694 + [radarhere] + +- Do not redeclare class each time when converting to NumPy #5844 + [radarhere] + +- Only prevent repeated polygon pixels when drawing with transparency #5835 + [radarhere] + +- Add support for pickling TrueType fonts #5826 + [hugovk, radarhere] + +- Only prefer command line tools SDK on macOS over default MacOSX SDK #5828 + [radarhere] + +- Drop support for soon-EOL Python 3.6 #5768 + [hugovk, nulano, radarhere] + +- Fix compilation on 64-bit Termux #5793 + [landfillbaby] + +- Use title for display in ImageShow #5788 + [radarhere] + +- Remove support for FreeType 2.7 and older #5777 + [hugovk, radarhere] + +- Fix for PyQt6 #5775 + [hugovk, radarhere] + +- Removed deprecated PILLOW_VERSION, Image.show command parameter, Image._showxv and ImageFile.raise_ioerror #5776 + [radarhere] + +8.4.0 (2021-10-15) +------------------ + +- Prefer global transparency in GIF when replacing with background color #5756 + [radarhere] + +- Added "exif" keyword argument to TIFF saving #5575 + [radarhere] + +- Copy Python palette to new image in quantize() #5696 + [radarhere] + +- Read ICO AND mask from end #5667 + [radarhere] + +- Actually check the framesize in FliDecode.c #5659 + [wiredfool] + +- Determine JPEG2000 mode purely from ihdr header box #5654 + [radarhere] + +- Fixed using info dictionary when writing multiple APNG frames #5611 + [radarhere] + +- Allow saving 1 and L mode TIFF with PhotometricInterpretation 0 #5655 + [radarhere] + +- For GIF save_all with palette, do not include palette with each frame #5603 + [radarhere] + +- Keep transparency when converting from P to LA or PA #5606 + [radarhere] + +- Copy palette to new image in transform() #5647 + [radarhere] + +- Added "transparency" argument to EpsImagePlugin load() #5620 + [radarhere] + +- Corrected pathlib.Path detection when saving #5633 + [radarhere] + +- Added WalImageFile class #5618 + [radarhere] + +- Consider I;16 pixel size when drawing text #5598 + [radarhere] + +- If default conversion from P is RGB with transparency, convert to RGBA #5594 + [radarhere] + +- Speed up rotating square images by 90 or 270 degrees #5646 + [radarhere] + +- Add support for reading DPI information from JPEG2000 images + [rogermb, radarhere] + +- Catch TypeError from corrupted DPI value in EXIF #5639 + [homm, radarhere] + +- Do not close file pointer when saving SGI images #5645 + [farizrahman4u, radarhere] + +- Deprecate ImagePalette size parameter #5641 + [radarhere, hugovk] + +- Prefer command line tools SDK on macOS #5624 + [radarhere] + +- Added tags when saving YCbCr TIFF #5597 + [radarhere] + +- PSD layer count may be negative #5613 + [radarhere] + +- Fixed ImageOps expand with tuple border on P image #5615 + [radarhere] + +- Fixed error saving APNG with duplicate frames and different duration times #5609 + [thak1411, radarhere] + +8.3.2 (2021-09-02) +------------------ + +- CVE-2021-23437 Raise ValueError if color specifier is too long + [hugovk, radarhere] + +- Fix 6-byte OOB read in FliDecode + [wiredfool] + +- Add support for Python 3.10 #5569, #5570 + [hugovk, radarhere] + +- Ensure TIFF ``RowsPerStrip`` is multiple of 8 for JPEG compression #5588 + [kmilos, radarhere] + +- Updates for ``ImagePalette`` channel order #5599 + [radarhere] + +- Hide FriBiDi shim symbols to avoid conflict with real FriBiDi library #5651 + [nulano] + +8.3.1 (2021-07-06) +------------------ + +- Catch OSError when checking if fp is sys.stdout #5585 + [radarhere] + +- Handle removing orientation from alternate types of EXIF data #5584 + [radarhere] + +- Make Image.__array__ take optional dtype argument #5572 + [t-vi, radarhere] + +8.3.0 (2021-07-01) +------------------ + +- Use snprintf instead of sprintf. CVE-2021-34552 #5567 + [radarhere] + +- Limit TIFF strip size when saving with LibTIFF #5514 + [kmilos] + +- Allow ICNS save on all operating systems #4526 + [baletu, radarhere, newpanjing, hugovk] + +- De-zigzag JPEG's DQT when loading; deprecate convert_dict_qtables #4989 + [gofr, radarhere] + +- Replaced xml.etree.ElementTree #5565 + [radarhere] + +- Moved CVE image to pillow-depends #5561 + [radarhere] + +- Added tag data for IFD groups #5554 + [radarhere] + +- Improved ImagePalette #5552 + [radarhere] + +- Add DDS saving #5402 + [radarhere] + +- Improved getxmp() #5455 + [radarhere] + +- Convert to float for comparison with float in IFDRational __eq__ #5412 + [radarhere] + +- Allow getexif() to access TIFF tag_v2 data #5416 + [radarhere] + +- Read FITS image mode and size #5405 + [radarhere] + +- Merge parallel horizontal edges in ImagingDrawPolygon #5347 + [radarhere, hrdrq] + +- Use transparency behind first GIF frame and when disposing to background #5557 + [radarhere, zewt] + +- Avoid unstable nature of qsort in Quant.c #5367 + [radarhere] + +- Copy palette to new images in ImageOps expand #5551 + [radarhere] + +- Ensure palette string matches RGB mode #5549 + [radarhere] + +- Do not modify EXIF of original image instance in exif_transpose() #5547 + [radarhere] + +- Fixed default numresolution for small JPEG2000 images #5540 + [radarhere] + +- Added DDS BC5 reading #5501 + [radarhere] + +- Raise an error if ImageDraw.textbbox is used without a TrueType font #5510 + [radarhere] + +- Added ICO saving in BMP format #5513 + [radarhere] + +- Ensure PNG seeks to end of previous chunk at start of load_end #5493 + [radarhere] + +- Do not allow TIFF to seek to a past frame #5473 + [radarhere] + +- Avoid race condition when displaying images with eog #5507 + [mconst] + +- Added specific error messages when ink has incorrect number of bands #5504 + [radarhere] + +- Allow converting an image to a numpy array to raise errors #5379 + [radarhere] + +- Removed DPI rounding from BMP, JPEG, PNG and WMF loading #5476, #5470 + [radarhere] + +- Remove spikes when drawing thin pieslices #5460 + [xtsm] + +- Updated default value for SAMPLESPERPIXEL TIFF tag #5452 + [radarhere] + +- Removed TIFF DPI rounding #5446 + [radarhere, hugovk] + +- Include code in WebP error #5471 + [radarhere] + +- Do not alter pixels outside mask when drawing text on an image with transparency #5434 + [radarhere] + +- Reset handle when seeking backwards in TIFF #5443 + [radarhere] + +- Replace sys.stdout with sys.stdout.buffer when saving #5437 + [radarhere] + +- Fixed UNDEFINED TIFF tag of length 0 being changed in roundtrip #5426 + [radarhere] + +- Fixed bug when checking FreeType2 version if it is not installed #5445 + [radarhere] + +- Do not round dimensions when saving PDF #5459 + [radarhere] + +- Added ImageOps contain() #5417 + [radarhere, hugovk] + +- Changed WebP default "method" value to 4 #5450 + [radarhere] + +- Switched to saving 1-bit PDFs with DCTDecode #5430 + [radarhere] + +- Use bpp from ICO header #5429 + [radarhere] + +- Corrected JPEG APP14 transform value #5408 + [radarhere] + +- Changed TIFF tag 33723 length to 1 #5425 + [radarhere] + +- Changed ImageMorph incorrect mode errors to ValueError #5414 + [radarhere] + +- Add EXIF tags specified in EXIF 2.32 #5419 + [gladiusglad] + +- Treat previous contents of first GIF frame as transparent #5391 + [radarhere] + +- For special image modes, revert default resize resampling to NEAREST #5411 + [radarhere] + +- JPEG2000: Support decoding subsampled RGB and YCbCr images #4996 + [nulano, radarhere] + +- Stop decoding BC1 punchthrough alpha in BC2&3 #4144 + [jansol] + +- Use zero if GIF background color index is missing #5390 + [radarhere] + +- Fixed ensuring that GIF previous frame was loaded #5386 + [radarhere] + +- Valgrind fixes #5397 + [wiredfool] + +- Round down the radius in rounded_rectangle #5382 + [radarhere] + +- Fixed reading uncompressed RGB data from DDS #5383 + [radarhere] + +8.2.0 (2021-04-01) +------------------ + +- Added getxmp() method #5144 + [UrielMaD, radarhere] + +- Add ImageShow support for GraphicsMagick #5349 + [latosha-maltba, radarhere] + +- Do not load transparent pixels from subsequent GIF frames #5333 + [zewt, radarhere] + +- Use LZW encoding when saving GIF images #5291 + [raygard] + +- Set all transparent colors to be equal in quantize() #5282 + [radarhere] + +- Allow PixelAccess to use Python __int__ when parsing x and y #5206 + [radarhere] + +- Removed Image._MODEINFO #5316 + [radarhere] + +- Add preserve_tone option to autocontrast #5350 + [elejke, radarhere] + +- Fixed linear_gradient and radial_gradient I and F modes #5274 + [radarhere] + +- Add support for reading TIFFs with PlanarConfiguration=2 #5364 + [kkopachev, wiredfool, nulano] + +- Deprecated categories #5351 + [radarhere] + +- Do not premultiply alpha when resizing with Image.NEAREST resampling #5304 + [nulano] + +- Dynamically link FriBiDi instead of Raqm #5062 + [nulano] + +- Allow fewer PNG palette entries than the bit depth maximum when saving #5330 + [radarhere] + +- Use duration from info dictionary when saving WebP #5338 + [radarhere] + +- Stop flattening EXIF IFD into getexif() #4947 + [radarhere, kkopachev] + +- Replaced tiff_deflate with tiff_adobe_deflate compression when saving TIFF images #5343 + [radarhere] + +- Save ICC profile from TIFF encoderinfo #5321 + [radarhere] + +- Moved RGB fix inside ImageQt class #5268 + [radarhere] + +- Allow alpha_composite destination to be negative #5313 + [radarhere] + +- Ensure file is closed if it is opened by ImageQt.ImageQt #5260 + [radarhere] + +- Added ImageDraw rounded_rectangle method #5208 + [radarhere] + +- Added IPythonViewer #5289 + [radarhere, Kipkurui-mutai] + +- Only draw each rectangle outline pixel once #5183 + [radarhere] + +- Use mmap instead of built-in Win32 mapper #5224 + [radarhere, cgohlke] + +- Handle PCX images with an odd stride #5214 + [radarhere] + +- Only read different sizes for "Large Thumbnail" MPO frames #5168 + [radarhere] + +- Added PyQt6 support #5258 + [radarhere] + +- Changed Image.open formats parameter to be case-insensitive #5250 + [Piolie, radarhere] + +- Deprecate Tk/Tcl 8.4, to be removed in Pillow 10 (2023-07-01) #5216 + [radarhere] + +- Added tk version to pilinfo #5226 + [radarhere, nulano] + +- Support for ignoring tests when running valgrind #5150 + [wiredfool, radarhere, hugovk] + +- OSS-Fuzz support #5189 + [wiredfool, radarhere] + +8.1.2 (2021-03-06) +------------------ + +- Fix Memory DOS in BLP (CVE-2021-27921), ICNS (CVE-2021-27922) and ICO (CVE-2021-27923) Image Plugins + [wiredfool] + +8.1.1 (2021-03-01) +------------------ + +- Use more specific regex chars to prevent ReDoS. CVE-2021-25292 + [hugovk] + +- Fix OOB Read in TiffDecode.c, and check the tile validity before reading. CVE-2021-25291 + [wiredfool] + +- Fix negative size read in TiffDecode.c. CVE-2021-25290 + [wiredfool] + +- Fix OOB read in SgiRleDecode.c. CVE-2021-25293 + [wiredfool] + +- Incorrect error code checking in TiffDecode.c. CVE-2021-25289 + [wiredfool] + +- PyModule_AddObject fix for Python 3.10 #5194 + [radarhere] + +8.1.0 (2021-01-02) +------------------ + +- Fix TIFF OOB Write error. CVE-2020-35654 #5175 + [wiredfool] + +- Fix for Read Overflow in PCX Decoding. CVE-2020-35653 #5174 + [wiredfool, radarhere] + +- Fix for SGI Decode buffer overrun. CVE-2020-35655 #5173 + [wiredfool, radarhere] + +- Fix OOB Read when saving GIF of xsize=1 #5149 + [wiredfool] + +- Makefile updates #5159 + [wiredfool, radarhere] + +- Add support for PySide6 #5161 + [hugovk] + +- Use disposal settings from previous frame in APNG #5126 + [radarhere] + +- Added exception explaining that _repr_png_ saves to PNG #5139 + [radarhere] + +- Use previous disposal method in GIF load_end #5125 + [radarhere] + +- Allow putpalette to accept 1024 integers to include alpha values #5089 + [radarhere] + +- Fix OOB Read when writing TIFF with custom Metadata #5148 + [wiredfool] + +- Added append_images support for ICO #4568 + [ziplantil, radarhere] + +- Block TIFFTAG_SUBIFD #5120 + [radarhere] + +- Fixed dereferencing potential null pointers #5108, #5111 + [cgohlke, radarhere] + +- Deprecate FreeType 2.7 #5098 + [hugovk, radarhere] + +- Moved warning to end of execution #4965 + [radarhere] + +- Removed unused fromstring and tostring C methods #5026 + [radarhere] + +- init() if one of the formats is unrecognised #5037 + [radarhere] + +- Moved string_dimension CVE image to pillow-depends #4993 + [radarhere] + +- Support raw rgba8888 for DDS #4760 + [qiankanglai] + +8.0.1 (2020-10-22) +------------------ + +- Update FreeType used in binary wheels to 2.10.4 to fix CVE-2020-15999. + [radarhere] + +- Moved string_dimension image to pillow-depends #4993 + [radarhere] + +8.0.0 (2020-10-15) +------------------ + +- Drop support for EOL Python 3.5 #4746, #4794 + [hugovk, radarhere, nulano] + +- Drop support for PyPy3 < 7.2.0 #4964 + [nulano] + +- Remove ImageCms.CmsProfile attributes deprecated since 3.2.0 #4768 + [hugovk, radarhere] + +- Remove long-deprecated Image.py functions #4798 + [hugovk, nulano, radarhere] + +- Add support for 16-bit precision JPEG quantization values #4918 + [gofr] + +- Added reading of IFD tag type #4979 + [radarhere] + +- Initialize offset memory for PyImagingPhotoPut #4806 + [nqbit] + +- Fix TiffDecode comparison warnings #4756 + [nulano] + +- Docs: Add dark mode #4968 + [hugovk, nulano] + +- Added macOS SDK install path to library and include directories #4974 + [radarhere, fxcoudert] + +- Imaging.h: prevent confusion with system #4923 + [ax3l, ,radarhere] + +- Avoid using pkg_resources in PIL.features.pilinfo #4975 + [nulano] + +- Add getlength and getbbox functions for TrueType fonts #4959 + [nulano, radarhere, hugovk] + +- Allow tuples with one item to give single color value in getink #4927 + [radarhere, nulano] + +- Add support for CBDT and COLR fonts #4955 + [nulano, hugovk] + +- Removed OSError in favour of DecompressionBombError for BMP #4966 + [radarhere] + +- Implemented another ellipse drawing algorithm #4523 + [xtsm, radarhere] + +- Removed unused JpegImagePlugin._fixup_dict function #4957 + [radarhere] + +- Added reading and writing of private PNG chunks #4292 + [radarhere] + +- Implement anchor for TrueType fonts #4930 + [nulano, hugovk] + +- Fixed bug in Exif __delitem__ #4942 + [radarhere] + +- Fix crash in ImageTk.PhotoImage on MinGW 64-bit #4946 + [nulano] + +- Moved CVE images to pillow-depends #4929 + [radarhere] + +- Refactor font_getsize and font_render #4910 + [nulano] + +- Fixed loading profile with non-ASCII path on Windows #4914 + [radarhere] + +- Fixed effect_spread bug for zero distance #4908 + [radarhere, hugovk] + +- Added formats parameter to Image.open #4837 + [nulano, radarhere] + +- Added regular_polygon draw method #4846 + [comhar] + +- Raise proper TypeError in putpixel #4882 + [nulano, hugovk] + +- Added writing of subIFDs #4862 + [radarhere] + +- Fix IFDRational __eq__ bug #4888 + [luphord, radarhere] + +- Fixed duplicate variable name #4885 + [liZe, radarhere] + +- Added homebrew zlib include directory #4842 + [radarhere] + +- Corrected inverted PDF CMYK colors #4866 + [radarhere] + +- Do not try to close file pointer if file pointer is empty #4823 + [radarhere] + +- ImageOps.autocontrast: add mask parameter #4843 + [navneeth, hugovk] + +- Read EXIF data tEXt chunk into info as bytes instead of string #4828 + [radarhere] + +- Replaced distutils with setuptools #4797, #4809, #4814, #4817, #4829, #4890 + [hugovk, radarhere] + +- Add MIME type to PsdImagePlugin #4788 + [samamorgan] + +- Allow ImageOps.autocontrast to specify low and high cutoffs separately #4749 + [millionhz, radarhere] + +7.2.0 (2020-07-01) +------------------ + +- Do not convert I;16 images when showing PNGs #4744 + [radarhere] + +- Fixed ICNS file pointer saving #4741 + [radarhere] + +- Fixed loading non-RGBA mode APNGs with dispose background #4742 + [radarhere] + +- Deprecated _showxv #4714 + [radarhere] + +- Deprecate Image.show(command="...") #4646 + [nulano, hugovk, radarhere] + +- Updated JPEG magic number #4707 + [Cykooz, radarhere] + +- Change STRIPBYTECOUNTS to LONG if necessary when saving #4626 + [radarhere, hugovk] + +- Write JFIF header when saving JPEG #4639 + [radarhere] + +- Replaced tiff_jpeg with jpeg compression when saving TIFF images #4627 + [radarhere] + +- Writing TIFF tags: improved BYTE, added UNDEFINED #4605 + [radarhere] + +- Consider transparency when pasting text on an RGBA image #4566 + [radarhere] + +- Added method argument to single frame WebP saving #4547 + [radarhere] + +- Use ImageFileDirectory_v2 in Image.Exif #4637 + [radarhere] + +- Corrected reading EXIF metadata without prefix #4677 + [radarhere] + +- Fixed drawing a jointed line with a sequence of numeric values #4580 + [radarhere] + +- Added support for 1-D NumPy arrays #4608 + [radarhere] + +- Parse orientation from XMP tags #4560 + [radarhere] + +- Speed up text layout by not rendering glyphs #4652 + [nulano] + +- Fixed ZeroDivisionError in Image.thumbnail #4625 + [radarhere] + +- Replaced TiffImagePlugin DEBUG with logging #4550 + [radarhere] + +- Fix repeatedly loading .gbr #4620 + [ElinksFr, radarhere] + +- JPEG: Truncate icclist instead of setting to None #4613 + [homm] + +- Fixes default offset for Exif #4594 + [rodrigob, radarhere] + +- Fixed bug when unpickling TIFF images #4565 + [radarhere] + +- Fix pickling WebP #4561 + [hugovk, radarhere] + +- Replace IOError and WindowsError aliases with OSError #4536 + [hugovk, radarhere] + +7.1.2 (2020-04-25) +------------------ + +- Raise an EOFError when seeking too far in PNG #4528 + [radarhere] + +7.1.1 (2020-04-02) +------------------ + +- Fix regression seeking and telling PNGs #4512 #4514 + [hugovk, radarhere] + +7.1.0 (2020-04-01) +------------------ + +- Fix multiple OOB reads in FLI decoding #4503 + [wiredfool] + +- Fix buffer overflow in SGI-RLE decoding #4504 + [wiredfool, hugovk] + +- Fix bounds overflow in JPEG 2000 decoding #4505 + [wiredfool] + +- Fix bounds overflow in PCX decoding #4506 + [wiredfool] + +- Fix 2 buffer overflows in TIFF decoding #4507 + [wiredfool] + +- Add APNG support #4243 + [pmrowla, radarhere, hugovk] + +- ImageGrab.grab() for Linux with XCB #4260 + [nulano, radarhere] + +- Added three new channel operations #4230 + [dwastberg, radarhere] + +- Prevent masking of Image reduce method in Jpeg2KImagePlugin #4474 + [radarhere, homm] + +- Added reading of earlier ImageMagick PNG EXIF data #4471 + [radarhere] + +- Fixed endian handling for I;16 getextrema #4457 + [radarhere] + +- Release buffer if function returns prematurely #4381 + [radarhere] + +- Add JPEG comment to info dictionary #4455 + [radarhere] + +- Fix size calculation of Image.thumbnail() #4404 + [orlnub123] + +- Fixed stroke on FreeType < 2.9 #4401 + [radarhere] + +- If present, only use alpha channel for bounding box #4454 + [radarhere] + +- Warn if an unknown feature is passed to features.check() #4438 + [jdufresne] + +- Fix Name field length when saving IM images #4424 + [hugovk, radarhere] + +- Allow saving of zero quality JPEG images #4440 + [radarhere] + +- Allow explicit zero width to hide outline #4334 + [radarhere] + +- Change ContainerIO return type to match file object mode #4297 + [jdufresne, radarhere] + +- Only draw each polygon pixel once #4333 + [radarhere] + +- Add support for shooting situation Exif IFD tags #4398 + [alexagv] + +- Handle multiple and malformed JPEG APP13 markers #4370 + [homm] + +- Depends: Update libwebp to 1.1.0 #4342, libjpeg to 9d #4352 + [radarhere] + +7.0.0 (2020-01-02) +------------------ + +- Drop support for EOL Python 2.7 #4109 + [hugovk, radarhere, jdufresne] + +- Fix rounding error on RGB to L conversion #4320 + [homm] + +- Exif writing fixes: Rational boundaries and signed/unsigned types #3980 + [kkopachev, radarhere] + +- Allow loading of WMF images at a given DPI #4311 + [radarhere] + +- Added reduce operation #4251 + [homm] + +- Raise ValueError for io.StringIO in Image.open #4302 + [radarhere, hugovk] + +- Fix thumbnail geometry when DCT scaling is used #4231 + [homm, radarhere] + +- Use default DPI when exif provides invalid x_resolution #4147 + [beipang2, radarhere] + +- Change default resize resampling filter from NEAREST to BICUBIC #4255 + [homm] + +- Fixed black lines on upscaled images with the BOX filter #4278 + [homm] + +- Better thumbnail aspect ratio preservation #4256 + [homm] + +- Add La mode packing and unpacking #4248 + [homm] + +- Include tests in coverage reports #4173 + [hugovk] + +- Handle broken Photoshop data #4239 + [radarhere] + +- Raise a specific exception if no data is found for an MPO frame #4240 + [radarhere] + +- Fix Unicode support for PyPy #4145 + [nulano] + +- Added UnidentifiedImageError #4182 + [radarhere, hugovk] + +- Remove deprecated __version__ from plugins #4197 + [hugovk, radarhere] + +- Fixed freeing unallocated pointer when resizing with height too large #4116 + [radarhere] + +- Copy info in Image.transform #4128 + [radarhere] + +- Corrected DdsImagePlugin setting info gamma #4171 + [radarhere] + +- Depends: Update libtiff to 4.1.0 #4195, Tk Tcl to 8.6.10 #4229, libimagequant to 2.12.6 #4318 + [radarhere] + +- Improve handling of file resources #3577 + [jdufresne] + +- Removed CI testing of Fedora 29 #4165 + [hugovk] + +- Added pypy3 to tox envlist #4137 + [jdufresne] + +- Drop support for EOL PyQt4 and PySide #4108 + [hugovk, radarhere] + +- Removed deprecated setting of TIFF image sizes #4114 + [radarhere] + +- Removed deprecated PILLOW_VERSION #4107 + [hugovk] + +- Changed default frombuffer raw decoder args #1730 + [radarhere] + +6.2.2 (2020-01-02) +------------------ + +- This is the last Pillow release to support Python 2.7 #3642 + +- Overflow checks for realloc for tiff decoding. CVE-2020-5310 + [wiredfool, radarhere] + +- Catch SGI buffer overrun. CVE-2020-5311 + [radarhere] + +- Catch PCX P mode buffer overrun. CVE-2020-5312 + [radarhere] + +- Catch FLI buffer overrun. CVE-2020-5313 + [radarhere] + +- Raise an error for an invalid number of bands in FPX image. CVE-2019-19911 + [wiredfool, radarhere] + +6.2.1 (2019-10-21) +------------------ + +- Add support for Python 3.8 #4141 + [hugovk] + +6.2.0 (2019-10-01) +------------------ + +- Catch buffer overruns #4104 + [radarhere] + +- Initialize rows_per_strip when RowsPerStrip tag is missing #4034 + [cgohlke, radarhere] + +- Raise error if TIFF dimension is a string #4103 + [radarhere] + +- Added decompression bomb checks #4102 + [radarhere] + +- Fix ImageGrab.grab DPI scaling on Windows 10 version 1607+ #4000 + [nulano, radarhere] + +- Corrected negative seeks #4101 + [radarhere] + +- Added argument to capture all screens on Windows #3950 + [nulano, radarhere] + +- Updated warning to specify when Image.frombuffer defaults will change #4086 + [radarhere] + +- Changed WindowsViewer format to PNG #4080 + [radarhere] + +- Use TIFF orientation #4063 + [radarhere] + +- Raise the same error if a truncated image is loaded a second time #3965 + [radarhere] + +- Lazily use ImageFileDirectory_v1 values from Exif #4031 + [radarhere] + +- Improved HSV conversion #4004 + [radarhere] + +- Added text stroking #3978 + [radarhere, hugovk] + +- No more deprecated bdist_wininst .exe installers #4029 + [hugovk] + +- Do not allow floodfill to extend into negative coordinates #4017 + [radarhere] + +- Fixed arc drawing bug for a non-whole number of degrees #4014 + [radarhere] + +- Fix bug when merging identical images to GIF with a list of durations #4003 + [djy0, radarhere] + +- Fix bug in TIFF loading of BufferedReader #3998 + [chadawagner] + +- Added fallback for finding ld on MinGW Cygwin #4019 + [radarhere] + +- Remove indirect dependencies from requirements.txt #3976 + [hugovk] + +- Depends: Update libwebp to 1.0.3 #3983, libimagequant to 2.12.5 #3993, freetype to 2.10.1 #3991 + [radarhere] + +- Change overflow check to use PY_SSIZE_T_MAX #3964 + [radarhere] + +- Report reason for pytest skips #3942 + [hugovk] + +6.1.0 (2019-07-01) +------------------ + +- Deprecate Image.__del__ #3929 + [jdufresne] + +- Tiff: Add support for JPEG quality #3886 + [olt] + +- Respect the PKG_CONFIG environment variable when building #3928 + [chewi] + +- Use explicit memcpy() to avoid unaligned memory accesses #3225 + [DerDakon] + +- Improve encoding of TIFF tags #3861 + [olt] + +- Update Py_UNICODE to Py_UCS4 #3780 + [nulano] + +- Consider I;16 pixel size when drawing #3899 + [radarhere] + +- Add TIFFTAG_SAMPLEFORMAT to blocklist #3926 + [cgohlke, radarhere] + +- Create GIF deltas from background colour of GIF frames if disposal mode is 2 #3708 + [sircinnamon, radarhere] + +- Added ImageSequence all_frames #3778 + [radarhere] + +- Use unsigned int to store TIFF IFD offsets #3923 + [cgohlke] + +- Include CPPFLAGS when searching for libraries #3819 + [jefferyto] + +- Updated TIFF tile descriptors to match current decoding functionality #3795 + [dmnisson] + +- Added an ``image.entropy()`` method (second revision) #3608 + [fish2000] + +- Pass the correct types to PyArg_ParseTuple #3880 + [QuLogic] + +- Fixed crash when loading non-font bytes #3912 + [radarhere] + +- Fix SPARC memory alignment issues in Pack/Unpack functions #3858 + [kulikjak] + +- Added CMYK;16B and CMYK;16N unpackers #3913 + [radarhere] + +- Fixed bugs in calculating text size #3864 + [radarhere] + +- Add __main__.py to output basic format and support information #3870 + [jdufresne] + +- Added variation font support #3802 + [radarhere] + +- Do not down-convert if image is LA when showing with PNG format #3869 + [radarhere] + +- Improve handling of PSD frames #3759 + [radarhere] + +- Improved ICO and ICNS loading #3897 + [radarhere] + +- Changed Preview application path so that it is no longer static #3896 + [radarhere] + +- Corrected ttb text positioning #3856 + [radarhere] + +- Handle unexpected ICO image sizes #3836 + [radarhere] + +- Fixed bits value for RGB;16N unpackers #3837 + [kkopachev] + +- Travis CI: Add Fedora 30, remove Fedora 28 #3821 + [hugovk] + +- Added reading of CMYK;16L TIFF images #3817 + [radarhere] + +- Fixed dimensions of 1-bit PDFs #3827 + [radarhere] + +- Fixed opening mmap image through Path on Windows #3825 + [radarhere] + +- Fixed ImageDraw arc gaps #3824 + [radarhere] + +- Expand GIF to include frames with extents outside the image size #3822 + [radarhere] + +- Fixed ImageTk getimage #3814 + [radarhere] + +- Fixed bug in decoding large images #3791 + [radarhere] + +- Fixed reading APP13 marker without Photoshop data #3771 + [radarhere] + +- Added option to include layered windows in ImageGrab.grab on Windows #3808 + [radarhere] + +- Detect libimagequant when installed by pacman on MingW #3812 + [radarhere] + +- Fixed raqm layout bug #3787 + [radarhere] + +- Fixed loading font with non-Unicode path on Windows #3785 + [radarhere] + +- Travis CI: Upgrade PyPy from 6.0.0 to 7.1.1 #3783 + [hugovk, johnthagen] + +- Depends: Updated openjpeg to 2.3.1 #3794, raqm to 0.7.0 #3877, libimagequant to 2.12.3 #3889 + [radarhere] + +- Fix numpy bool bug #3790 + [radarhere] + +6.0.0 (2019-04-01) +------------------ + +- Python 2.7 support will be removed in Pillow 7.0.0 #3682 + [hugovk] + +- Add EXIF class #3625 + [radarhere] + +- Add ImageOps exif_transpose method #3687 + [radarhere] + +- Added warnings to deprecated CMSProfile attributes #3615 + [hugovk] + +- Documented reading TIFF multiframe images #3720 + [akuchling] + +- Improved speed of opening an MPO file #3658 + [Glandos] + +- Update palette in quantize #3721 + [radarhere] + +- Improvements to TIFF is_animated and n_frames #3714 + [radarhere] + +- Fixed incompatible pointer type warnings #3754 + [radarhere] + +- Improvements to PA and LA conversion and palette operations #3728 + [radarhere] + +- Consistent DPI rounding #3709 + [radarhere] + +- Change size of MPO image to match frame #3588 + [radarhere] + +- Read Photoshop resolution data #3701 + [radarhere] + +- Ensure image is mutable before saving #3724 + [radarhere] + +- Correct remap_palette documentation #3740 + [radarhere] + +- Promote P images to PA in putalpha #3726 + [radarhere] + +- Allow RGB and RGBA values for new P images #3719 + [radarhere] + +- Fixed TIFF bug when seeking backwards and then forwards #3713 + [radarhere] + +- Cache EXIF information #3498 + [Glandos] + +- Added transparency for all PNG grayscale modes #3744 + [radarhere] + +- Fix deprecation warnings in Python 3.8 #3749 + [radarhere] + +- Fixed GIF bug when rewinding to a non-zero frame #3716 + [radarhere] + +- Only close original fp in __del__ and __exit__ if original fp is exclusive #3683 + [radarhere] + +- Fix BytesWarning in Tests/test_numpy.py #3725 + [jdufresne] + +- Add missing MIME types and extensions #3520 + [pirate486743186] + +- Add I;16 PNG save #3566 + [radarhere] + +- Add support for BMP RGBA bitfield compression #3705 + [radarhere] + +- Added ability to set language for text rendering #3693 + [iwsfutcmd] + +- Only close exclusive fp on Image __exit__ #3698 + [radarhere] + +- Changed EPS subprocess stdout from devnull to None #3635 + [radarhere] + +- Add reading old-JPEG compressed TIFFs #3489 + [kkopachev] + +- Add EXIF support for PNG #3674 + [radarhere] + +- Add option to set dither param on quantize #3699 + [glasnt] + +- Add reading of DDS uncompressed RGB data #3673 + [radarhere] + +- Correct length of Tiff BYTE tags #3672 + [radarhere] + +- Add DIB saving and loading through Image open #3691 + [radarhere] + +- Removed deprecated VERSION #3624 + [hugovk] + +- Fix 'BytesWarning: Comparison between bytes and string' in PdfDict #3580 + [jdufresne] + +- Do not resize in Image.thumbnail if already the destination size #3632 + [radarhere] + +- Replace .seek() magic numbers with io.SEEK_* constants #3572 + [jdufresne] + +- Make ContainerIO.isatty() return a bool, not int #3568 + [jdufresne] + +- Add support to all transpose operations for I;16 modes #3563, #3741 + [radarhere] + +- Deprecate support for PyQt4 and PySide #3655 + [hugovk, radarhere] + +- Add TIFF compression codecs: LZMA, Zstd, WebP #3555 + [cgohlke] + +- Fixed pickling of iTXt class with protocol > 1 #3537 + [radarhere] + +- _util.isPath returns True for pathlib.Path objects #3616 + [wbadart] + +- Remove unnecessary unittest.main() boilerplate from test files #3631 + [jdufresne] + +- Exif: Seek to IFD offset #3584 + [radarhere] + +- Deprecate PIL.*ImagePlugin.__version__ attributes #3628 + [jdufresne] + +- Docs: Add note about ImageDraw operations that exceed image bounds #3620 + [radarhere] + +- Allow for unknown PNG chunks after image data #3558 + [radarhere] + +- Changed EPS subprocess stdin from devnull to None #3611 + [radarhere] + +- Fix possible integer overflow #3609 + [cgohlke] + +- Catch BaseException for resource cleanup handlers #3574 + [jdufresne] + +- Improve pytest configuration to allow specific tests as CLI args #3579 + [jdufresne] + +- Drop support for Python 3.4 #3596 + [hugovk] + +- Remove deprecated PIL.OleFileIO #3598 + [hugovk] + +- Remove deprecated ImageOps undocumented functions #3599 + [hugovk] + +- Depends: Update libwebp to 1.0.2 #3602 + [radarhere] + +- Detect MIME types #3525 + [radarhere] + +5.4.1 (2019-01-06) +------------------ + +- File closing: Only close __fp if not fp #3540 + [radarhere] + +- Fix build for Termux #3529 + [pslacerda] + +- PNG: Detect MIME types #3525 + [radarhere] + +- PNG: Handle IDAT chunks after image end #3532 + [radarhere] + +5.4.0 (2019-01-01) +------------------ + +- Docs: Improved ImageChops documentation #3522 + [radarhere] + +- Allow RGB and RGBA values for P image putpixel #3519 + [radarhere] + +- Add APNG extension to PNG plugin #3501 + [pirate486743186, radarhere] + +- Lookup ld.so.cache instead of hardcoding search paths #3245 + [pslacerda] + +- Added custom string TIFF tags #3513 + [radarhere] + +- Improve setup.py configuration #3395 + [diorcety] + +- Read textual chunks located after IDAT chunks for PNG #3506 + [radarhere] + +- Performance: Don't try to hash value if enum is empty #3503 + [Glandos] + +- Added custom int and float TIFF tags #3350 + [radarhere] + +- Fixes for issues reported by static code analysis #3393 + [frenzymadness] + +- GIF: Wait until mode is normalized to copy im.info into encoderinfo #3187 + [radarhere] + +- Docs: Add page of deprecations and removals #3486 + [hugovk] + +- Travis CI: Upgrade PyPy from 5.8.0 to 6.0 #3488 + [hugovk] + +- Travis CI: Allow lint job to fail #3467 + [hugovk] + +- Resolve __fp when closing and deleting #3261 + [radarhere] + +- Close exclusive fp before discarding #3461 + [radarhere] + +- Updated open files documentation #3490 + [radarhere] + +- Added libjpeg_turbo to check_feature #3493 + [radarhere] + +- Change color table index background to tuple when saving as WebP #3471 + [radarhere] + +- Allow arbitrary number of comment extension subblocks #3479 + [radarhere] + +- Ensure previous FLI frame is loaded before seeking to the next #3478 + [radarhere] + +- ImageShow improvements #3450 + [radarhere] + +- Depends: Update libimagequant to 2.12.2 #3442, libtiff to 4.0.10 #3458, libwebp to 1.0.1 #3468, Tk Tcl to 8.6.9 #3465 + [radarhere] + +- Check quality_layers type #3464 + [radarhere] + +- Add context manager, __del__ and close methods to TarIO #3455 + [radarhere] + +- Test: Do not play sound when running screencapture command #3454 + [radarhere] + +- Close exclusive fp on open exception #3456 + [radarhere] + +- Only close existing fp in WebP if fp is exclusive #3418 + [radarhere] + +- Docs: Re-add the downloads badge #3443 + [hugovk] + +- Added negative index to PixelAccess #3406 + [Nazime] + +- Change tuple background to global color table index when saving as GIF #3385 + [radarhere] + +- Test: Improved ImageGrab tests #3424 + [radarhere] + +- Flake8 fixes #3422, #3440 + [radarhere, hugovk] + +- Only ask for YCbCr->RGB libtiff conversion for jpeg-compressed tiffs #3417 + [kkopachev] + +- Optimise ImageOps.fit by combining resize and crop #3409 + [homm] + +5.3.0 (2018-10-01) +------------------ + +- Changed Image size property to be read-only by default #3203 + [radarhere] + +- Add warnings if image file identification fails due to lack of WebP support #3169 + [radarhere, hugovk] + +- Hide the Ghostscript progress dialog popup on Windows #3378 + [hugovk] + +- Adding support to reading tiled and YcbCr jpeg tiffs through libtiff #3227 + [kkopachev] + +- Fixed None as TIFF compression argument #3310 + [radarhere] + +- Changed GIF seek to remove previous info items #3324 + [radarhere] + +- Improved PDF document info #3274 + [radarhere] + +- Add line width parameter to rectangle and ellipse-based shapes #3094 + [hugovk, radarhere] + +- Fixed decompression bomb check in _crop #3313 + [dinkolubina, hugovk] + +- Added support to ImageDraw.floodfill for non-RGB colors #3377 + [radarhere] + +- Tests: Avoid catching unexpected exceptions in tests #2203 + [jdufresne] + +- Use TextIOWrapper.detach() instead of NoCloseStream #2214 + [jdufresne] + +- Added transparency to matrix conversion #3205 + [radarhere] + +- Added ImageOps pad method #3364 + [radarhere] + +- Give correct extrema for I;16 format images #3359 + [bz2] + +- Added PySide2 #3279 + [radarhere] + +- Corrected TIFF tags #3369 + [radarhere] + +- CI: Install CFFI and pycparser without any PYTHONOPTIMIZE #3374 + [hugovk] + +- Read/Save RGB webp as RGB (instead of RGBX) #3298 + [kkopachev] + +- ImageDraw: Add line joints #3250 + [radarhere] + +- Improved performance of ImageDraw floodfill method #3294 + [yo1995] + +- Fix builds with --parallel #3272 + [hsoft] + +- Add more raw Tiff modes (RGBaX, RGBaXX, RGBAX, RGBAXX) #3335 + [homm] + +- Close existing WebP fp before setting new fp #3341 + [radarhere] + +- Add orientation, compression and id_section as TGA save keyword arguments #3327 + [radarhere] + +- Convert int values of RATIONAL TIFF tags to floats #3338 + [radarhere, wiredfool] + +- Fix code for PYTHONOPTIMIZE #3233 + [hugovk] + +- Changed ImageFilter.Kernel to subclass ImageFilter.BuiltinFilter, instead of the other way around #3273 + [radarhere] + +- Remove unused draw.draw_line, draw.draw_point and font.getabc methods #3232 + [hugovk] + +- Tests: Added ImageFilter tests #3295 + [radarhere] + +- Tests: Added ImageChops tests #3230 + [hugovk, radarhere] + +- AppVeyor: Download lib if not present in pillow-depends #3316 + [radarhere] + +- Travis CI: Add Python 3.7 and Xenial #3234 + [hugovk] + +- Docs: Added documentation for NumPy conversion #3301 + [radarhere] + +- Depends: Update libimagequant to 2.12.1 #3281 + [radarhere] + +- Add three-color support to ImageOps.colorize #3242 + [tsennott] + +- Tests: Add LA to TGA test modes #3222 + [danpla] + +- Skip outline if the draw operation fills with the same colour #2922 + [radarhere] + +- Flake8 fixes #3173, #3380 + [radarhere] + +- Avoid deprecated 'U' mode when opening files #2187 + [jdufresne] + +5.2.0 (2018-07-01) +------------------ + +- Fixed saving a multiframe image as a single frame PDF #3137 + [radarhere] + +- If a Qt version is already imported, attempt to use it first #3143 + [radarhere] + +- Fix transform fill color for alpha images #3147 + [fozcode] + +- TGA: Add support for writing RLE data #3186 + [danpla] + +- TGA: Read and write LA data #3178 + [danpla] + +- QuantOctree.c: Remove erroneous attempt to average over an empty range #3196 + [tkoeppe] + +- Changed ICNS format tests to pass on OS X 10.11 #3202 + [radarhere] + +- Fixed bug in ImageDraw.multiline_textsize() #3114 + [tianyu139] + +- Added getsize_multiline support for PIL.ImageFont #3113 + [tianyu139] + +- Added ImageFile get_format_mimetype method #3190 + [radarhere] + +- Changed mmap file pointer to use context manager #3216 + [radarhere] + +- Changed ellipse point calculations to be more evenly distributed #3142 + [radarhere] + +- Only extract first Exif segment #2946 + [hugovk] + +- Tests: Test ImageDraw2, WalImageFile #3135, #2989 + [hugovk] + +- Remove unnecessary '#if 0' code #3075 + [hugovk] + +- Tests: Added GD tests #1817 + [radarhere] + +- Fix collections ABCs DeprecationWarning in Python 3.7 #3123 + [hugovk] + +- unpack_from is faster than unpack of slice #3201 + [landfillbaby] + +- Docs: Add coordinate system links and file handling links in documentation #3204, #3214 + [radarhere] + +- Tests: TestFilePng: Fix test_save_l_transparency() #3182 + [danpla] + +- Docs: Correct argument name #3171 + [radarhere] + +- Docs: Update CMake download URL #3166 + [radarhere] + +- Docs: Improve Image.transform documentation #3164 + [radarhere] + +- Fix transform fillcolor argument when image mode is RGBA or LA #3163 + [radarhere] + +- Tests: More specific Exception testing #3158 + [radarhere] + +- Add getrgb HSB/HSV color strings #3148 + [radarhere] + +- Allow float values in getrgb HSL color string #3146 + [radarhere] + +- AppVeyor: Upgrade to Python 2.7.15 and 3.4.4 #3140 + [radarhere] + +- AppVeyor: Upgrade to PyPy 6.0.0 #3133 + [hugovk] + +- Deprecate PILLOW_VERSION and VERSION #3090 + [hugovk] + +- Support Python 3.7 #3076 + [hugovk] + +- Depends: Update freetype to 2.9.1, libjpeg to 9c, libwebp to 1.0.0 #3121, #3136, #3108 + [radarhere] + +- Build macOS wheels with Xcode 6.4, supporting older macOS versions #3068 + [wiredfool] + +- Fix _i2f compilation on some GCC versions #3067 + [homm] + +- Changed encoderinfo to have priority over info when saving GIF images #3086 + [radarhere] + +- Rename PIL.version to PIL._version and remove it from module #3083 + [homm] + +- Enable background colour parameter on rotate #3057 + [storesource] + +- Remove unnecessary ``#if 1`` directive #3072 + [jdufresne] + +- Remove unused Python class, Path #3070 + [jdufresne] + +- Fix dereferencing type-punned pointer will break strict-aliasing #3069 + [jdufresne] + +5.1.0 (2018-04-02) +------------------ + +- Close fp before return in ImagingSavePPM #3061 + [kathryndavies] + +- Added documentation for ICNS append_images #3051 + [radarhere] + +- Docs: Move intro text below its header #3021 + [hugovk] + +- CI: Rename appveyor.yml as .appveyor.yml #2978 + [hugovk] + +- Fix TypeError for JPEG2000 parser feed #3042 + [hugovk] + +- Certain corrupted jpegs can result in no data read #3023 + [kkopachev] + +- Add support for BLP file format #3007 + [jleclanche] + +- Simplify version checks #2998 + [hugovk] + +- Fix "invalid escape sequence" warning on Python 3.6+ #2996 + [timgraham] + +- Allow append_images to set .icns scaled images #3005 + [radarhere] + +- Support appending to existing PDFs #2965 + [vashek] + +- Fix and improve efficient saving of ICNS on macOS #3004 + [radarhere] + +- Build: Enable pip cache in AppVeyor build #3009 + [thijstriemstra] + +- Trim trailing whitespace #2985 + [Metallicow] + +- Docs: Correct reference to Image.new method #3000 + [radarhere] + +- Rearrange ImageFilter classes into alphabetical order #2990 + [radarhere] + +- Test: Remove duplicate line #2983 + [radarhere] + +- Build: Update AppVeyor PyPy version #3003 + [radarhere] + +- Tiff: Open 8 bit Tiffs with 5 or 6 channels, discarding extra channels #2938 + [homm] + +- Readme: Added Twitter badge #2930 + [hugovk] + +- Removed __main__ code from ImageCms #2942 + [radarhere] + +- Test: Changed assert statements to unittest calls #2961 + [radarhere] + +- Depends: Update libimagequant to 2.11.10, raqm to 0.5.0, freetype to 2.9 #3036, #3017, #2957 + [radarhere] + +- Remove _imaging.crc32 in favor of builtin Python crc32 implementation #2935 + [wiredfool] + +- Move Tk directory to src directory #2928 + [hugovk] + +- Enable pip cache in Travis CI #2933 + [jdufresne] + +- Remove unused and duplicate imports #2927 + [radarhere] + +- Docs: Changed documentation references to 2.x to 2.7 #2921 + [radarhere] + +- Fix memory leak when opening webp files #2974 + [wiredfool] + +- Setup: Fix "TypeError: 'NoneType' object is not iterable" for PPC and CRUX #2951 + [hugovk] + +- Setup: Add libdirs for ppc64le and armv7l #2968 + [nehaljwani] + +5.0.0 (2018-01-01) +------------------ + +- Docs: Added docstrings from documentation #2914 + [radarhere] + +- Test: Switch from nose to pytest #2815 + [hugovk] + +- Rework Source directory layout, preventing accidental import of PIL. #2911 + [wiredfool] + +- Dynamically link libraqm #2753 + [wiredfool] + +- Removed scripts directory #2901 + [wiredfool] + +- TIFF: Run all compressed tiffs through libtiff decoder #2899 + [wiredfool] + +- GIF: Add disposal option when saving GIFs #2902 + [linnil1, wiredfool] + +- EPS: Allow for an empty line in EPS header data #2903 + [radarhere] + +- PNG: Add support for sRGB and cHRM chunks, permit sRGB when no iCCP chunk present #2898 + [wiredfool] + +- Dependencies: Update Tk Tcl to 8.6.8 #2905 + [radarhere] + +- Decompression bomb error now raised for images 2x larger than a decompression bomb warning #2583 + [wiredfool] + +- Test: avoid random failure in test_effect_noise #2894 + [hugovk] + +- Increased epsilon for test_file_eps.py:test_showpage due to Arch update. #2896 + [wiredfool] + +- Removed check parameter from _save in BmpImagePlugin, PngImagePlugin, ImImagePlugin, PalmImagePlugin, and PcxImagePlugin. #2873 + [radarhere] + +- Make PngImagePlugin.add_text() zip argument type bool #2890 + [jdufresne] + +- Depends: Updated libwebp to 0.6.1 #2880 + [radarhere] + +- Remove unnecessary bool() calls in Image.registered_extensions and skipKnownBadTests #2891 + [jdufresne] + +- Fix count of BITSPERSAMPLE items in broken TIFF files #2883 + [homm] + +- Fillcolor parameter for Image.Transform #2852 + [wiredfool] + +- Test: Display differences for test failures #2862 + [wiredfool] + +- Added executable flag to file with shebang line #2884 + [radarhere] + +- Setup: Specify compatible Python versions for pip #2877 + [hugovk] + +- Dependencies: Updated libimagequant to 2.11.4 #2878 + [radarhere] + +- Setup: Warn if trying to install for Py3.7 on Windows #2855 + [hugovk] + +- Doc: Fonts can be loaded from a file-like object, not just filename #2861 + [robin-norwood] + +- Add eog support for Ubuntu Image Viewer #2864 + [NafisFaysal] + +- Test: Test on 3.7-dev on Travis CI #2870 + [hugovk] + +- Dependencies: Update libtiff to 4.0.9 #2871 + [radarhere] + +- Setup: Replace deprecated platform.dist with file existence check #2869 + [wiredfool] + +- Build: Fix setup.py on Debian #2853 + [wiredfool] + +- Docs: Correct error in ImageDraw documentation #2858 + [meribold] + +- Test: Drop Ubuntu Precise, Fedora 24, Fedora 25, add Fedora 27, Centos 7, Amazon v2 CI Support #2854, #2843, #2895, #2897 + [wiredfool] + +- Dependencies: Updated libimagequant to 2.11.3 #2849 + [radarhere] + +- Test: Fix test_image.py to use tempfile #2841 + [radarhere] + +- Replace PIL.OleFileIO deprecation warning with descriptive ImportError #2833 + [hugovk] + +- WebP: Add support for animated WebP files #2761 + [jd20] + +- PDF: Set encoderinfo for images when saving multi-page PDF. Fixes #2804. #2805 + [ixio] + +- Allow the olefile dependency to be optional #2789 + [jdufresne] + +- GIF: Permit LZW code lengths up to 12 bits in GIF decode #2813 + [wiredfool] + +- Fix unterminated string and unchecked exception in _font_text_asBytes. #2825 + [wiredfool] + +- PPM: Use fixed list of whitespace, rather relying on locale, fixes #272. #2831 + [markmiscavage] + +- Added support for generators when using append_images #2829, #2835 + [radarhere] + +- Doc: Correct PixelAccess.rst #2824 + [hasahmed] + +- Depends: Update raqm to 0.3.0 #2822 + [radarhere] + +- Docs: Link to maintained version of aggdraw #2809 + [hugovk] + +- Include license file in the generated wheel packages #2801 + [jdufresne] + +- Depends: Update openjpeg to 2.3.0 #2791 + [radarhere] + +- Add option to Makefile to build and install with C coverage #2781 + [hugovk] + +- Add context manager support to ImageFile.Parser and PngImagePlugin.ChunkStream #2793 + [radarhere] + +- ImageDraw.textsize: fix zero length error #2788 + [wiredfool, hugovk] + +4.3.0 (2017-10-02) +------------------ + +- Fix warning on pointer cast in isblock #2775, #2778 + [cgohlke] + +- Doc: Added macOS High Sierra tested Pillow version #2777 + [radarhere] + +- Use correct Windows handle type on 64 bit in imagingcms #2774 + [cgohlke] + +- 64 Bit Windows fix for block storage #2773 + [cgohlke] + +- Fix "expression result unused" warning #2764 + [radarhere] + +- Add 16bit Read/Write and RLE read support to SgiImageFile #2769 + [jbltx, wiredfool] + +- Block & array hybrid storage #2738 + [homm] + +- Common seek frame position check #1849 + [radarhere] + +- Doc: Add note about aspect ratio to Image thumbnail script #2281 + [wilsonge] + +- Fix ValueError: invalid version number '1.0.0rc1' in scipy release candidate #2771 + [cgohlke] + +- Unfreeze requirements.txt #2766 + [hugovk] + +- Test: ResourceWarning tests #2756 + [hugovk] + +- Use n_frames to determine is_animated if possible #2315 + [radarhere] + +- Doc: Corrected parameters in documentation #2768 + [radarhere] + +- Avoid unnecessary Image operations #1891 + [radarhere] + +- Added register_extensions method #1860 + [radarhere] + +- Fix TIFF support for I;16S, I;16BS, and I;32BS rawmodes #2748 + [wiredfool] + +- Fixed doc syntax in ImageDraw #2752 + [radarhere] + +- Fixed support for building on Windows/msys2. Added Appveyor CI coverage for python3 on msys2 #2746 + [wiredfool] + +- Fix ValueError in Exif/Tiff IFD #2719 + [wiredfool] + +- Use pathlib2 for Path objects on Python < 3.4 #2291 + [asergi] + +- Export only required properties in unsafe_ptrs #2740 + [homm] + +- Alpha composite fixes #2709 + [homm] + +- Faster Transpose operations, added 'Transverse' option #2730 + [homm] + +- Deprecate ImageOps undocumented functions gaussian_blur, gblur, unsharp_mask, usm and box_blur in favor of ImageFilter implementations #2735 + [homm] + +- Dependencies: Updated freetype to 2.8.1 #2741 + [radarhere] + +- Bug: Player skipped first image #2742 + [radarhere] + +- Faster filter operations for Kernel, Gaussian, and Unsharp Mask filters #2679 + [homm] + +- EPS: Add showpage to force rendering of some EPS images #2636 + [kaplun] + +- DOC: Fix type of palette parameter in Image.quantize. #2703 + [kkopachev] + +- DOC: Fix Ico docs to match code #2712 + [hugovk] + +- Added file pointer save to SpiderImagePlugin #2647 + [radarhere] + +- Add targa version 2 footer #2713 + [jhultgre] + +- Removed redundant lines #2714 + [radarhere] + +- Travis CI: Use default pypy/pypy3 #2721 + [hugovk] + +- Fix for SystemError when rendering an empty string, added in 4.2.0 #2706 + [wiredfool] + +- Fix for memory leaks in font handling added in 4.2.0 #2634 + [wiredfool] + +- Tests: cleanup, more tests. Fixed WMF save handler #2689 + [radarhere] + +- Removed debugging interface for Image.core.grabclipboard #2708 + [radarhere] + +- Doc syntax fix #2710 + [radarhere] + +- Faster packing and unpacking for RGB, LA, and related storage modes #2693 + [homm] + +- Use RGBX rawmode for RGB JPEG images where possible #1989 + [homm] + +- Remove palettes from non-palette modes in _new #2704 + [wiredfool] + +- Delete transparency info when convert'ing RGB/L to RGBA #2633 + [olt] + +- Code tweaks to ease type annotations #2687 + [neiljp] + +- Fixed incorrect use of 's#' to byteslike object #2691 + [wiredfool] + +- Fix JPEG subsampling labels for subsampling=2 #2698 + [homm] + +- Region of interest (box) for resampling #2254 + [homm] + +- Basic support for Termux (android) in setup.py #2684 + [wiredfool] + +- Bug: Fix Image.fromarray for numpy.bool type. #2683 + [wiredfool] + +- CI: Add Fedora 24 and 26 to Docker tests + [wiredfool] + +- JPEG: Fix ZeroDivisionError when EXIF contains invalid DPI (0/0). #2667 + [vytisb] + +- Depends: Updated openjpeg to 2.2.0 #2669 + [radarhere] + +- Depends: Updated Tk Tcl to 8.6.7 #2668 + [radarhere] + +- Depends: Updated libimagequant to 2.10.2 #2660 + [radarhere] + +- Test: Added test for ImImagePlugin tell() #2675 + [radarhere] + +- Test: Additional tests for SGIImagePlugin #2659 + [radarhere] + +- New Image.getchannel method #2661 + [homm] + +- Remove unused im.copy2 and core.copy methods #2657 + [homm] + +- Fast Image.merge() #2677 + [homm] + +- Fast Image.split() #2676 + [homm] + +- Fast image allocation #2655 + [homm] + +- Storage cleanup #2654 + [homm] + +- FLI: Use frame count from FLI header #2674 + [radarhere] + +- Test: Test animated FLI file #2650 + [hugovk] + +- Bug: Fixed uninitialized memory in bc5 decoding #2648 + [ifeherva] + +- Moved SgiImagePlugin save error to before the start of write operations #2646 + [radarhere] + +- Move createfontdatachunk.py so isn't installed globally #2645 + [hugovk] + +- Bug: Fix unexpected keyword argument 'align' #2641 + [hugovk] + +- Add newlines to error message for clarity #2640 + [hugovk] + +- Docs: Updated redirected URL #2637 + [radarhere] + +- Bug: Fix JPEG DPI when EXIF is invalid #2632 + [wiredfool] + +- Bug: Fix for font getsize on empty string #2624 + [radarhere] + +- Docs: Improved ImageDraw documentation #2626 + [radarhere] + +- Docs: Corrected alpha_composite args documentation #2627 + [radarhere] + +- Docs: added the description of the filename attribute to images.rst #2621 + [dasdachs] + +- Dependencies: Updated libimagequant to 2.10.1 #2616 + [radarhere] + +- PDF: Renamed parameter to not shadow built-in dict #2612 + [kijeong] + +4.2.1 (2017-07-06) +------------------ + +- CI: Fix version specification and test on CI for PyPy/Windows #2608 + [wiredfool] + +4.2.0 (2017-07-01) +------------------ + +- Doc: Clarified Image.save:append_images documentation #2604 + [radarhere] + +- CI: Amazon Linux and Centos6 docker images added to Travis CI #2585 + [wiredfool] + +- Image.alpha_composite added #2595 + [wiredfool] + +- Complex Text Support #2576 + [ShamsaHamed, Fahad-Alsaidi, wiredfool] + +- Added threshold parameter to ImageDraw.floodfill #2599 + [nediamond] + +- Added dBATCH parameter to ghostscript command #2588 + [radarhere] + +- JPEG: Adjust buffer size when icc_profile > MAXBLOCK #2596 + [Darou] + +- Specify Pillow Version in one place #2517 + [wiredfool] + +- CI: Change the owner of the TRAVIS_BUILD_DIR, fixing broken docker runs #2587 + [wiredfool] + +- Fix truncated PNG loading for some images, Fix memory leak on truncated PNG images. #2541, #2598 + [homm] + +- Add decompression bomb check to Image.crop #2410 + [wiredfool] + +- ImageFile: Ensure that the ``err_code`` variable is initialized in case of exception. #2363 + [alexkiro] + +- Tiff: Support append_images for saving multipage TIFFs #2406 + [blochl] + +- Doc: Clarify that draft is only implemented for JPEG and PCD #2409 + [wiredfool] + +- Test: MicImagePlugin #2447 + [hugovk] + +- Use round() instead of floor() to eliminate zero coefficients in resample #2558 + [homm] + +- Remove deprecated code #2549 + [hugovk] + +- Added append_images to PDF saving #2526 + [radarhere] + +- Remove unused function core image function new_array #2548 + [hugovk] + +- Remove unnecessary calls to dict.keys() #2551 + [jdufresne] + +- Add more ImageDraw.py tests and remove unused Draw.c code #2533 + [hugovk] + +- Test: More tests for ImageMorph #2554 + [hugovk] + +- Test: McIDAS area file #2552 + [radarhere] + +- Update Feature Detection #2520 + [wiredfool] + +- CI: Update pypy on Travis CI #2573 + [hugovk] + +- ImageMorph: Fix wrong expected size of MRLs read from disk #2561 + [dov] + +- Docs: Update install docs for FreeBSD #2546 + [wiredfool] + +- Build: Ignore OpenJpeg 1.5 on FreeBSD #2544 + [melvyn-sopacua] + +- Remove 'not yet implemented' methods from PIL 1.1.4 #2538 + [hugovk] + +- Dependencies: Update FreeType to 2.8, LibTIFF to 4.0.8 and libimagequant to 2.9.1 #2535 #2537 #2540 + [radarhere] + +- Raise TypeError and not also UnboundLocalError in ImageFile.Parser() #2525 + [joshblum] + +- Test: Use Codecov for coverage #2528 + [hugovk] + +- Use PNG for Image.show() #2527 + [HinTak, wiredfool] + +- Remove WITH_DEBUG compilation flag #2522 + [wiredfool] + +- Fix return value on parameter parse error in _webp.c #2521 + [adw1n] + +- Set executable flag on scripts with shebang line #2295 + [radarhere] + +- Flake8 #2460 + [radarhere] + +- Doc: Release Process Changes #2516 + [wiredfool] + +- CI: Added region for s3 deployment on appveyor #2515 + [wiredfool] + +- Doc: Updated references to point to existing files #2507 + [radarhere] + +- Return copy on Image crop if crop dimensions match the image #2471 + [radarhere] + +- Test: Optimize CI speed #2464, #2466 + [hugovk] + +4.1.1 (2017-04-28) +------------------ + +- Undef PySlice_GetIndicesEx, see https://bugs.python.org/issue29943 #2493 + [cgohlke] + +- Fix for file with DPI in EXIF but not metadata, and XResolution is an int rather than tuple #2484 + [hugovk] + +- Docs: Removed broken download counter badge #2487 + [hugovk] + +- Docs: Fixed rst syntax error #2477 + [thebjorn] + +4.1.0 (2017-04-03) +------------------ + +- Close files after loading if possible #2330 + [homm, wiredfool] + +- Fix Image Access to be reloadable when embedding the Python interpreter #2296 + [wiredfool, cgohlke] + +- Fetch DPI from EXIF if not specified in JPEG header #2449, #2472 + [hugovk] + +- Removed winbuild checksum verification #2468 + [radarhere] + +- Git: Set ContainerIO test file as binary #2469 + [cgohlke] + +- Remove superfluous import of FixTk #2455 + [cgohlke) + +- Fix import of tkinter/Tkinter #2456 + [cgohlke) + +- Pure Python Decoders, including Python decoder to fix for MSP images #1938 + [wiredfool, hugovk] + +- Reorganized GifImagePlugin, fixes #2314. #2374 + [radarhere, wiredfool] + +- Doc: Reordered operating systems in Compatibility Matrix #2436 + [radarhere] + +- Test: Additional tests for BufrStub, Eps, Container, GribStub, IPTC, Wmf, XVThumb, ImageDraw, ImageMorph, ImageShow #2425 + [radarhere] + +- Health fixes #2437 + [radarhere] + +- Test: Correctness tests ContainerIO, XVThumbImagePlugin, BufrStubImagePlugin, GribStubImagePlugin, FitsStubImagePlugin, Hdf5StubImagePlugin, PixarImageFile, PsdImageFile #2443, #2442, #2441, #2440, #2431, #2430, #2428, #2427 + [hugovk] + +- Remove unused imports #1822 + [radarhere] + +- Replaced KeyError catch with dictionary get method #2424 + [radarhere] + +- Test: Removed unrunnable code in test_image_toqimage #2415 + [hugovk] + +- Removed use of spaces in TIFF kwargs names, deprecated in 2.7 #1390 + [radarhere] + +- Removed deprecated ImageDraw setink, setfill, setfont methods #2220 + [jdufresne] + +- Send unwanted subprocess output to /dev/null #2253 + [jdufresne] + +- Fix division by zero when creating 0x0 image from numpy array #2419 + [hugovk] + +- Test: Added matrix convert tests #2381 + [hugovk] + +- Replaced broken URL to partners.adobe.com #2413 + [radarhere] + +- Removed unused private functions in setup.py and build_dep.py #2414 + [radarhere] + +- Test: Fixed Qt tests for QT5 and saving 1 bit PNG #2394 + [wiredfool] + +- Test: docker builds for Arch and Debian Stretch #2394 + [wiredfool] + +- Updated libwebp to 0.6.0 on appveyor #2395 + [radarhere] + +- More explicit error message when saving to a file with invalid extension #2399 + [ces42] + +- Docs: Update some http urls to https #2403 + [hugovk] + +- Preserve aux/alpha channels when performing Imagecms transforms #2355 + [gunjambi] + +- Test linear and radial gradient effects #2382 + [hugovk] + +- Test ImageDraw.Outline and and ImageDraw.Shape #2389 + [hugovk] + +- Added PySide to ImageQt documentation #2392 + [radarhere] + +- BUG: Empty image mode no longer causes a crash #2380 + [evalapply] + +- Exclude .travis and contents from manifest #2386 + [radarhere] + +- Remove 'MIT-like' from license #2145 + [wiredfool] + +- Tests: Add tests for several Image operations #2379 + [radarhere] + +- PNG: Moved iCCP chunk before PLTE chunk when saving as PNG, restricted chunks known value/ordering #2347 + [radarhere] + +- Default to inch-interpretation for missing ResolutionUnit in TiffImagePlugin #2365 + [lambdafu] + +- Bug: Fixed segfault when using ImagingTk on pypy Issue #2376, #2359. + [wiredfool] + +- Bug: Fixed Integer overflow using ImagingTk on 32 bit platforms #2359 + [wiredfool, QuLogic] + +- Tests: Added docker images for testing alternate platforms. See also https://github.com/python-pillow/docker-images. #2368 + [wiredfool] + +- Removed PIL 1.0 era TK readme that concerns Windows 95/NT #2360 + [wiredfool] + +- Prevent ``nose -v`` printing docstrings #2369 + [hugovk] + +- Replaced absolute PIL imports with relative imports #2349 + [radarhere] + +- Added context managers for file handling #2307 + [radarhere] + +- Expose registered file extensions in Image #2343 + [iggomez, radarhere] + +- Make mode descriptor cache initialization thread-safe. #2351 + [gunjambi] + +- Updated Windows test dependencies: Freetype 2.7.1, zlib 1.2.11 #2331, #2332, #2357 + [radarhere] + +- Followed upstream pngquant packaging reorg to libimagquant #2354 + [radarhere] + +- Fix invalid string escapes #2352 + [hugovk] + +- Add test for crop operation with no argument #2333 + [radarhere] + +4.0.0 (2017-01-01) +------------------ + +- Refactor out postprocessing hack to load_end in PcdImageFile + [wiredfool] + +- Add center and translate option to Image.rotate. #2328 + [lambdafu] + +- Test: Relax WMF test condition, fixes #2323. #2327 + [wiredfool] + +- Allow 0 size images, Fixes #2259, Reverts to pre-3.4 behavior. #2262 + [wiredfool] + +- SGI: Save uncompressed SGI/BW/RGB/RGBA files #2325 + [jbltx] + +- Depends: Updated pngquant to 2.8.2 #2319 + [radarhere] + +- Test: Added correctness tests for opening SGI images #2324 + [wiredfool] + +- Allow passing a list or tuple of individual frame durations when saving a GIF #2298 + [Xdynix] + +- Unified different GIF optimize conditions #2196 + [radarhere] + +- Build: Refactor dependency installation #2305 + [hugovk] + +- Test: Add python 3.6 to travis, tox #2304 + [hugovk] + +- Test: Fix coveralls coverage for Python+C #2300 + [hugovk] + +- Remove executable bit and shebang from OleFileIO.py #2308 + [jwilk, radarhere] + +- PyPy: Buffer interface workaround #2294 + [wiredfool] + +- Test: Switch to Ubuntu Trusty 14.04 on Travis CI #2294 + +- Remove vendored version of olefile Python package in favor of upstream #2199 + [jdufresne] + +- Updated comments to use print as a function #2234 + [radarhere] + +- Set executable flag on selftest.py, setup.py and added shebang line #2282, #2277 + [radarhere, homm] + +- Test: Increase epsilon for FreeType 2.7 as rendering is slightly different. #2286 + [hugovk] + +- Test: Faster assert_image_similar #2279 + [homm] + +- Removed deprecated internal "stretch" method #2276 + [homm] + +- Removed the handles_eof flag in decode.c #2223 + [wiredfool] + +- Tiff: Fix for writing Tiff to BytesIO using libtiff #2263 + [wiredfool] + +- Doc: Design docs #2269 + [wiredfool] + +- Test: Move tests requiring libtiff to test_file_libtiff #2273 + [wiredfool] + +- Update Maxblock heuristic #2275 + [wiredfool] + +- Fix for 2-bit palette corruption #2274 + [pdknsk, wiredfool] + +- Tiff: Update info.icc_profile when using libtiff reader. #2193 + [lambdafu] + +- Test: Fix bug in test_ifd_rational_save when libtiff is not available #2270 + [ChristopherHogan] + +- ICO: Only save relevant sizes #2267 + [hugovk] + +- ICO: Allow saving .ico files of 256x256 instead of 255x255 #2265 + [hugovk] + +- Fix TIFFImagePlugin ICC color profile saving. #2087 + [cskau] + +- Doc: Improved description of ImageOps.deform resample parameter #2256 + [radarhere] + +- EMF: support negative bounding box coordinates #2249 + [glexey] + +- Close file if opened in WalImageFile #2216 + [radarhere] + +- Use Image._new() instead of _makeself() #2248 + [homm] + +- SunImagePlugin fixes #2241 + [wiredfool] + +- Use minimal scale for jpeg drafts #2240 + [homm] + +- Updated dependency scripts to use FreeType 2.7, OpenJpeg 2.1.2, WebP 0.5.2 and Tcl/Tk 8.6.6 #2235, #2236, #2237, #2290, #2302 + [radarhere] + +- Fix "invalid escape sequence" bytestring warnings in Python 3.6 #2186 + [timgraham] + +- Removed support for Python 2.6 and Python 3.2 #2192 + [jdufresne] + +- Setup: Raise custom exceptions when required/requested dependencies are not found #2213 + [wiredfool] + +- Use a context manager in FontFile.save() to ensure file is always closed #2226 + [jdufresne] + +- Fixed bug in saving to fp-objects in Python >= 3.4 #2227 + [radarhere] + +- Use a context manager in ImageFont._load_pilfont() to ensure file is always closed #2232 + [jdufresne] + +- Use generator expressions instead of list comprehension #2225 + [jdufresne] + +- Close file after reading in ImagePalette.load() #2215 + [jdufresne] + +- Changed behaviour of default box argument for paste method to match docs #2211 + [radarhere] + +- Add support for another BMP bitfield #2221 + [jmerdich] + +- Added missing top-level test __main__ #2222 + [radarhere] + +- Replaced range(len()) #2197 + [radarhere] + +- Fix for ImageQt Segfault, fixes #1370 #2182 + [wiredfool] + +- Setup: Close file in setup.py after finished reading #2208 + [jdufresne] + +- Setup: optionally use pkg-config (when present) to detect dependencies #2074 + [garbas] + +- Search for tkinter first in builtins #2210 + [matthew-brett] + +- Tests: Replace try/except/fail pattern with TestCase.assertRaises() #2200 + [jdufresne] + +- Tests: Remove unused, open files at top level of tests #2188 + [jdufresne] + +- Replace type() equality checks with isinstance #2184 + [jdufresne] + +- Doc: Move ICO out of the list of read-only file formats #2180 + [alexwlchan] + +- Doc: Fix formatting, too-short title underlines and malformed table #2175 + [hugovk] + +- Fix BytesWarnings #2172 + [jdufresne] + +- Use Integer division to eliminate deprecation warning. #2168 + [mastermatt] + +- Doc: Update compatibility matrix + [daavve, wiredfool] + + +3.4.2 (2016-10-18) +------------------ + +- Fix Resample coefficient calculation #2162 + [homm] + + +3.4.1 (2016-10-04) +------------------ + +- Allow lists as arguments for Image.new() #2149 + [homm] + +- Fix fix for map.c overflow #2151 (also in 3.3.3) + [wiredfool] + +3.4.0 (2016-10-03) +------------------ + +- Removed Image.core.open_ppm, added negative image size checks in Image.py. #2146 + [wiredfool] + +- Windows build: fetch dependencies from pillow-depends #2095 + [hugovk] + +- Add TIFF save_all writer. #2140 + [lambdafu, vashek] + +- Move libtiff fd duplication to _load_libtiff #2141 + [sekrause] + +- Speed up GIF save optimization step, fixes #2093. #2133 + [wiredfool] + +- Fix for ImageCms Segfault, Issue #2037. #2131 + [wiredfool] + +- Make Image.crop an immediate operation, not lazy. #2138 + [wiredfool] + +- Skip empty values in ImageFileDirectory #2024 + [homm] + +- Force reloading palette when using mmap in ImageFile. #2139 + [lambdafu] + +- Fix "invalid escape sequence" warning in Python 3.6 #2136 + [timgraham] + +- Update documentation about drafts #2137 + [radarhere] + +- Converted documentation parameter format, comments to docstrings #2021 + [radarhere] + +- Fixed typos #2128 #2142 + [radarhere] + +- Renamed references to OS X to macOS #2125 2130 + [radarhere] + +- Use truth value when checking for progressive and optimize option on save #2115, #2129 + [radarhere] + +- Convert DPI to ints when saving as JPEG #2102 + [radarhere] + +- Added append_images parameter to GIF saving #2103 + [radarhere] + +- Speedup paste with masks up to 80% #2015 + [homm] + +- Rewrite DDS decoders in C, add DXT3 and BC7 decoders #2068 + [Mischanix] + +- Fix PyArg_ParseTuple format in getink() #2070 + [arjennienhuis] + +- Fix saving originally missing TIFF tags. #2111 + [anntzer] + +- Allow pathlib.Path in Image.open on Python 2.7 #2110 + [patricksnape] + +- Use modern base64 interface over deprecated #2121 + [hugovk] + +- ImageColor.getrgb hexadecimal RGBA #2114 + [homm] + +- Test fix for bigendian machines #2092 + [wiredfool] + +- Resampling lookups, trailing empty coefficients, precision #2008 + [homm] + +- Add (un)packing between RGBA and BGRa #2057 + [arjennienhuis] + +- Added return for J2k (and fpx) Load to return a pixel access object #2061 + [wiredfool] + +- Skip failing numpy tests on Pypy <= 5.3.1 #2090 + [arjennienhuis] + +- Show warning when trying to save RGBA image as JPEG #2010 + [homm] + +- Respect pixel centers during transform #2022 + [homm] + +- TOC for supported file formats #2056 + [polarize] + +- Fix conversion of bit images to numpy arrays Fixes #350, #2058 + [matthew-brett] + +- Add ImageOps.scale to expand or contract a PIL image by a factor #2011 + [vlmath] + +- Flake8 fixes #2050 + [hugovk] + +- Updated freetype to 2.6.5 on Appveyor builds #2035 + [radarhere] + +- PCX encoder fixes #2023, pr #2041 + [homm] + +- Docs: Windows console prompts are > #2031 + [techtonik] + +- Expose Pillow package version as PIL.__version__ #2027 + [techtonik] + +- Add Box and Hamming filters for resampling #1959 + [homm] + +- Retain a reference to core image object in PyAccess #2009 + [homm] + +3.3.3 (2016-10-04) +------------------ + +- Fix fix for map.c overflow #2151 + [wiredfool] + +3.3.2 (2016-10-03) +------------------ + +- Fix negative image sizes in Storage.c #2146 + [wiredfool] + +- Fix integer overflow in map.c #2146 + [wiredfool] + +3.3.1 (2016-08-18) +------------------ + +- Fix C90 compilation error for Tcl / Tk rewrite #2033 + [matthew-brett] + +- Fix image loading when rotating by 0 deg #2052 + [homm] + +3.3.0 (2016-07-01) +------------------ + +- Fixed enums for Resolution Unit and Predictor in TiffTags.py #1998 + [wiredfool] + +- Fix issue converting P mode to LA #1986 + [didrix] + +- Moved test_j2k_overflow to check_j2k_overflow, prevent DOS of our 32bit testing machines #1995 + [wiredfool] + +- Skip CRC checks in PNG files when LOAD_TRUNCATED_IMAGES is enabled #1991 + [kkopachev] + +- Added CMYK mode for opening EPS files #1826 + [radarhere] + +- Docs: OSX build instruction clarification #1994 + [wiredfool] + +- Docs: Filter comparison table #1993 + [homm] + +- Removal of pthread based Incremental.c, new interface for file decoders/encoders to access the python file. Fixes assorted J2k Hangs. #1934 + [wiredfool] + +- Skip unnecessary passes when resizing #1954 + [homm] + +- Removed duplicate code in ImagePalette #1832 + [radarhere] + +- test_imagecms: Reduce precision of extended info due to 32 bit machine precision #1990 + [AbdealiJK] + +- Binary Tiff Metadata/ICC profile. #1988 + [wiredfool] + +- Ignore large text blocks in PNG if LOAD_TRUNCATED_IMAGES is enabled #1970 + [homm] + +- Replace index = index+1 in docs with +=1 + [cclauss] + +- Skip extra 0xff00 in jpeg #1977 + [kkopachev] + +- Use bytearray for palette mutable storage #1985 + [radarhere, wiredfool] + +- Added additional uint modes for Image.fromarray, more extensive tests of fromarray #1984 + [mairsbw, wiredfool] + +- Fix for program importing PyQt4 when PyQt5 also installed #1942 + [hugovk] + +- Changed depends/install_*.sh urls to point to github pillow-depends repo #1983 + [wiredfool] + +- Allow ICC profile from ``encoderinfo`` while saving PNGs #1909 + [homm] + +- Fix integer overflow on ILP32 systems (32-bit Linux). #1975 + [lambdafu] + +- Change function declaration to match Tcl_CmdProc type #1966 + [homm] + +- Integer overflow checks on all calls to \*alloc #1781 + [wiredfool] + +- Change equals method on Image so it short circuits #1967 + [mattBoros] + +- Runtime loading of TCL/TK libraries, eliminating build time dependency. #1932 + [matthew-brett] + +- Cleanup of transform methods #1941 + [homm] + +- Fix "Fatal Python error: UNREF invalid object" in debug builds #1936 + [wiredfool] + +- Setup fixes for Alpine linux #1937 + [wiredfool] + +- Split resample into horizontal + vertical passes #1933 + [homm] + +- Box blur with premultiplied alpha #1914 + [homm] + +- Add libimagequant support in quantize() #1889 + [rr-] + +- Added internal Premultiplied luminosity (La) mode #1912 + [homm] + +- Fixed point integer resample #1881 + [homm] + +- Removed docs/BUILDME script #1924 + [radarhere] + +- Moved comments to docstrings #1926 + [hugovk] + +- Include Python.h before wchar.h so _GNU_SOURCE is set consistently #1906 + [hugovk] + +- Updated example decoder in documentation #1899 + [radarhere] + +- Added support for GIF comment extension #1896 + [radarhere] + +- Removed support for pre- 1.5.2 list form of Image info in Image.new #1897 + [radarhere] + +- Fix typos in TIFF tags #1918 + [radarhere] + +- Skip tests that require libtiff if it is not installed #1893 (fixes #1866) + [wiredfool] + +- Skip test when icc profile is not available, fixes #1887. #1892 + [doko42] + +- Make deprecated functions raise NotImplementedError instead of Exception. #1862, #1890 + [daniel-leicht, radarhere] + +- Replaced os.system with subprocess.call in setup.py #1879 + [radarhere] + +- Corrected Image show documentation #1886 + [radarhere] + +- Added check for executable permissions to ImageShow #1880 + [radarhere] + +- Fixed tutorial code and added explanation #1877 + [radarhere] + +- Added OS X support for ImageGrab grabclipboard #1837 + [radarhere] + +- Combined duplicate code in ImageTk #1856 + [radarhere] + +- Added --disable-platform-guessing option to setup.py build extension #1861 + [angeloc] + +- Fixed loading Transparent PNGs with a transparent black color #1840 + [olt] + +- Add support for LA mode in Image.fromarray #1865 + [pierriko] + +- Make ImageFile load images in read-only mode #1864 + [hdante] + +- Added _accept hook for XVThumbImagePlugin #1853 + [radarhere] + +- Test TIFF with LZW compression #1855, TGA RLE file #1854 + [hugovk] + +- Improved SpiderImagePlugin help text #1863 + [radarhere] + +- Updated Sphinx project description #1870 + [radarhere] + +- Remove support for Python 3.0 from _imaging.c #1851 + [radarhere] + +- Jpeg qtables are unsigned chars #1814, #1921 + [thebostik] + +- Added additional EXIF tags #1841, TIFF Tags #1821 + [radarhere] + +- Changed documentation to refer to ImageSequence Iterator #1833 + [radarhere] + +- Fix Fedora prerequisites in installation docs, depends script #1842 + [living180] + +- Added _accept hook for PixarImagePlugin #1843 + [radarhere] + +- Removed outdated scanner classifier #1823 + [radarhere] + +- Combined identical error messages in _imaging #1825 + [radarhere] + +- Added debug option for setup.py to trace header and library finding #1790 + [wiredfool] + +- Fix doc building on travis #1820, #1844 + [wiredfool] + +- Fix for DIB/BMP images #1813, #1847 + [wiredfool] + +- Add PixarImagePlugin file extension #1809 + [radarhere] + +- Catch struct.errors when verifying png files #1805 + [wiredfool] + +- SpiderImagePlugin: raise an error when seeking in a non-stack file #1794 + [radarhere, jmichalon] + +- Added support for 2/4 bpp Tiff grayscale images #1789 + [zwhfly] + +- Removed unused variable from selftest #1788 + [radarhere] + +- Added warning for as_dict method (deprecated in 3.0.0) #1799 + [radarhere] + +- Removed powf support for older Python versions #1784 + [radarhere] + +- Health fixes #1625 #1903 + [radarhere] + +3.2.0 (2016-04-01) +------------------ + +- Added install docs for Fedora 23 and FreeBSD #1729, #1739, #1792 + [koobs, zandermartin, wiredfool] + +- Fixed TIFF multiframe load when the frames have different compression types #1782 + [radarhere, geka000] + +- Added __copy__ method to Image #1772 + [radarhere] + +- Updated dates in PIL license in OleFileIO README #1787 + [radarhere] + +- Corrected Tiff tag names #1786 + [radarhere] + +- Fixed documented name of JPEG property #1783 + [radarhere] + +- Fixed UnboundLocalError when loading a corrupt jpeg2k file #1780 + [wiredfool] + +- Fixed integer overflow in path.c #1773 + [wiredfool, nedwill] + +- Added debug to command line help text for pilprint #1766 + [radarhere] + +- Expose many more fields in ICC Profiles #1756 + [lambdafu] + +- Documentation changes, URL update, transpose, release checklist + [radarhere] + +- Fixed saving to nonexistent files specified by pathlib.Path objects #1748 (fixes #1747) + [radarhere] + +- Round Image.crop arguments to the nearest integer #1745 (fixes #1744) + [hugovk] + +- Fix uninitialized variable warning in _imaging.c:getink #1663 (fixes #486) + [wiredfool] + +- Disable multiprocessing install on cygwin #1700 (fixes #1690) + [wiredfool] + +- Fix the error reported when libz is not found #1764 + [wiredfool] + +- More general error check to avoid Symbol not found: _PyUnicodeUCS2_AsLatin1String on OS X #1761 + [wiredfool] + +- Added py35 to tox envlist #1724 + [radarhere] + +- Fix EXIF tag name typos #1736 + [zarlant, radarhere] + +- Updated freetype to 2.6.3, Tk/Tcl to 8.6.5 and 8.5.19 #1725, #1752 + [radarhere] + +- Add a loader for the FTEX format from Independence War 2: Edge of Chaos #1688 + [jleclanche] + +- Improved alpha_composite documentation #1698 + [radarhere] + +- Extend ImageDraw.text method to pass on multiline_text method specific arguments #1647 + [radarhere] + +- Allow ImageSequence to seek to zero #1686 + [radarhere] + +- ImageSequence Iterator is now an iterator #1649 + [radarhere] + +- Updated windows test builds to jpeg9b #1673 + [radarhere] + +- Fixed support for .gbr version 1 images, added support for version 2 in GbrImagePlugin #1653 + [wiredfool] + +- Clarified which YCbCr format is used #1677 + [radarhere] + +- Added TiffTags documentation, Moved windows build documentation to winbuild/ #1667 + [wiredfool] + +- Add tests for OLE file based formats #1678 + [radarhere] + +- Add TIFF IFD test #1671 + [radarhere] + +- Add a basic DDS image plugin with more tests #1654 + [jleclanche, hugovk, wiredfool] + +- Fix incorrect conditional in encode.c #1638 + [manisandro] + + +3.1.2 (2016-04-01) +------------------ + +- Fixed an integer overflow in Jpeg2KEncode.c causing a buffer overflow. CVE-2016-3076 + [wiredfool] + +3.1.1 (2016-02-04) +------------------ + +- Fixed an integer overflow in Resample.c causing writes in the Python heap. + [nedwill] + +- Fixed a buffer overflow in PcdDecode.c causing a segfault when opening PhotoCD files. CVE-2016-2533 + [wiredfool] + +- Fixed a buffer overflow in FliDecode.c causing a segfault when opening FLI files. CVE-2016-0775 + [wiredfool] + +- Fixed a buffer overflow in TiffDecode.c causing an arbitrary amount of memory to be overwritten when opening a specially crafted invalid TIFF file. CVE-2016-0740 + [wiredfool] + + +3.1.0 (2016-01-04) +------------------ + +- Fixing test failures on Python 2.6/Windows #1633 + [wiredfool] + +- Limit metadata tags when writing using libtiff #1620 + [wiredfool] + +- Rolling back exif support to pre-3.0 format #1627 + [wiredfool] + +- Fix Divide by zero in Exif, add IFDRational class #1531 + [wiredfool] + +- Catch the IFD error near the source #1622 + [wiredfool] + +- Added release notes for 3.1.0 #1623 + [radarhere] + +- Updated spacing to be consistent between multiline methods #1624 + [radarhere] + +- Let EditorConfig take care of some basic formatting #1489 + [hugovk] + +- Restore gpsexif data to the v1 form #1619 + [wiredfool] + +- Add /usr/local include and library directories for freebsd #1613 + [leforestier] + +- Updated installation docs for new versions of dependencies #1611 + [radarhere] + +- Removed unrunnable test file #1610 + [radarhere] + +- Changed register calls to use format property #1608 + [radarhere] + +- Added field type constants to TiffTags #1596 + [radarhere] + +- Allow saving RowsPerStrip with libtiff #1594 + [wiredfool] + +- Enabled conversion to numpy array for HSV images #1578 + [cartisan] + +- Changed some urls in the docs to use https #1580 + [hugovk] + +- Removed logger.exception from ImageFile.py #1590 + [radarhere] + +- Removed warnings module check #1587 + [radarhere] + +- Changed arcs, chords and pie slices to use floats #1577 + [radarhere] + +- Update unit test asserts #1584, #1598 + [radarhere] + +- Fix command to invoke ghostscript for eps files #1478 + [baumatron, radarhere] + +- Consistent multiline text spacing #1574 + [wiredfool, hugovk] + +- Removed unused lines in BDFFontFile #1530 + [radarhere] + +- Changed ImageQt import of Image #1560 + [radarhere, ericfrederich] + +- Throw TypeError if no cursors were found in .cur file #1556 + [radarhere] + +- Fix crash in ImageTk.PhotoImage on win-amd64 #1553 + [cgohlke] + +- ExtraSamples tag should be a SHORT, not a BYTE #1555 + [Nexuapex] + +- Docs and code health fixes #1565 #1566 #1581 #1586 #1591 #1621 + [radarhere] + +- Updated freetype to 2.6.2 #1564 + [radarhere] + +- Updated WebP to 0.5.0 for Travis #1515 #1609 + [radarhere] + +- Fix missing 'version' key value in __array_interface__ #1519 + [mattip] + +- Replaced os.popen with subprocess.Popen to pilprint script #1523 + [radarhere] + +- Catch OverflowError in SpiderImagePlugin #1545 + [radarhere, MrShark] + +- Fix the definition of icc_profile in TiffTags #1539 + [wiredfool] + +- Remove old _imagingtiff.c and pilplus stuff #1499 + [hugovk] + +- Fix Exception when requiring jpeg #1501 + [hansmosh] + +- Dependency scripts for Debian and Ubuntu #1486 + [wiredfool] + +- Added Usage message to painter script #1482 + [radarhere] + +- Add tag info for iccprofile, fixes #1462. #1465 + [wiredfool] + +- Added some requirements for make release-test #1451 + [wiredfool] + +- Flatten tiff metadata value SAMPLEFORMAT to initial value #1467 (fixes #1466) + [wiredfool] + +- Fix handling of pathlib in Image.save #1464 (fixes #1460) + [wiredfool] + +- Make tests more robust #1469 + [hugovk] + +- Use correctly sized pointers for windows handle types #1458 + [nu744] + +3.0.0 (2015-10-01) +------------------ + +- Check flush method existence for file-like object #1398 + [mrTable, radarhere] + +- Added PDF multipage saving #1445 + [radarhere] + +- Removed deprecated code, Image.tostring, Image.fromstring, Image.offset, ImageDraw.setink, ImageDraw.setfill, ImageFileIO, ImageFont.FreeTypeFont and ImageFont.truetype ``file`` kwarg, ImagePalette private _make functions, ImageWin.fromstring and ImageWin.tostring #1343 + [radarhere] + +- Load more broken images #1428 + [homm] + +- Require zlib and libjpeg #1439 + [wiredfool] + +- Preserve alpha when converting from a QImage to a Pillow Image by using png instead of ppm #1429 + [ericfrederich] + +- Qt needs 32 bit aligned image data #1430 + [ericfrederich] + +- Tiff ImageFileDirectory rewrite #1419 + [anntzer, wiredfool, homm] + +- Removed spammy debug logging #1423 + [wiredfool] + +- Save as GiF89a with support for animation parameters #1384 + [radarhere] + +- Correct convert matrix docs #1426 + [wiredfool] + +- Catch TypeError in _getexif #1414 + [radarhere, wiredfool] + +- Fix for UnicodeDecodeError in TiffImagePlugin #1416 + [bogdan199, wiredfool] + +- Dedup code in image.open #1415 + [wiredfool] + +- Skip any number extraneous chars at the end of JPEG chunks #1337 + [homm] + +- Single threaded build for pypy3, refactor #1413 + [wiredfool] + +- Fix loading of truncated images with LOAD_TRUNCATED_IMAGES enabled #1366 + [homm] + +- Documentation update for concepts: bands #1406 + [merriam] + +- Add Solaris/SmartOS include and library directories #1356 + [njones11] + +- Improved handling of getink color #1387 + [radarhere] + +- Disable compiler optimizations for topalette and tobilevel functions for all msvc versions #1402 (fixes #1357) + [cgohlke] + +- Skip ImageFont_bitmap test if _imagingft C module is not installed #1409 + [homm] + +- Add param documentation to ImagePalette #1381 + [bwrsandman] + +- Corrected scripts path #1407 + [radarhere] + +- Updated libtiff to 4.0.6 #1405, #1421 + [radarhere] + +- Updated Platform Support for Yosemite #1403 + [radarhere] + +- Fixed infinite loop on truncated file #1401 + [radarhere] + +- Check that images are L mode in ImageMorph methods #1400 + [radarhere] + +- In tutorial of pasting images, add to mask text #1389 + [merriam] + +- Style/health fixes #1391, #1397, #1417, #1418 + [radarhere] + +- Test on Python 3.5 dev and 3.6 nightly #1361 + [hugovk] + +- Fix fast rotate operations #1373 + [radarhere] + +- Added support for pathlib Path objects to open and save #1372 + [radarhere] + +- Changed register calls to use format property #1333 + [radarhere] + +- Added support for ImageGrab.grab to OS X #1367, #1443 + [radarhere, hugovk] + +- Fixed PSDraw stdout Python 3 compatibility #1365 + [radarhere] + +- Added Python 3.3 to AppVeyor #1363 + [radarhere] + +- Treat MPO with unknown header as base JPEG file #1350 + [hugovk, radarhere] + +- Added various tests #1330, #1344 + [radarhere] + +- More ImageFont tests #1327 + [hugovk] + +- Use logging instead of print #1207 + [anntzer] + +2.9.0 (2015-07-01) +------------------ + +- Added test for GimpPaletteFile #1324 + [radarhere] + +- Merged gifmaker script to allow saving of multi-frame GIF images #1320 + [radarhere] + +- Added is_animated property to multi-frame formats #1319 + [radarhere] + +- Fixed ValueError in Python 2.6 #1315 #1316 + [cgohlke, radarhere] + +- Fixed tox test script path #1308 + [radarhere] + +- Added width and height properties #1304 + [radarhere] + +- Update tiff and tk tcl 8.5 versions #1303 + [radarhere, wiredfool] + +- Add functions to convert: Image <-> QImage; Image <-> QPixmap #1217 + [radarhere, rominf] + +- Remove duplicate code in gifmaker script #1294 + [radarhere] + +- Multiline text in ImageDraw #1177 + [allo-, radarhere] + +- Automated Windows CI/build support #1278 + [wiredfool] + +- Removed support for Tk versions earlier than 8.4 #1288 + [radarhere] + +- Fixed polygon edge drawing #1255 (fixes #1252) + [radarhere] + +- Check prefix length in _accept methods #1267 + [radarhere] + +- Register MIME type for BMP #1277 + [coldmind] + +- Adjusted ImageQt use of unicode() for 2/3 compatibility #1218 + [radarhere] + +- Identify XBM file created with filename including underscore #1230 (fixes #1229) + [hugovk] + +- Copy image when saving in GifImagePlugin #1231 (fixes #718) + [radarhere] + +- Removed support for FreeType 2.0 #1247 + [radarhere] + +- Added background saving to GifImagePlugin #1273 + [radarhere] + +- Provide n_frames attribute to multi-frame formats #1261 + [anntzer, radarhere] + +- Add duration and loop set to GifImagePlugin #1172, #1269 + [radarhere] + +- Ico files are little endian #1232 + [wiredfool] + +- Upgrade olefile from 0.30 to 0.42b #1226 + [radarhere, decalage2] + +- Setting transparency value to 0 when the tRNS contains only null byte(s) #1239 + [juztin] + +- Separated out feature checking from selftest #1233 + [radarhere] + +- Style/health fixes + [radarhere] + +- Update WebP from 0.4.1 to 0.4.3 #1235 + [radarhere] + +- Release GIL during image load (decode) #1224 + [lkesteloot] + +- Added icns save #1185 + [radarhere] + +- Fix putdata memory leak #1196 + [benoit-pierre] + +- Keep user-specified ordering of icon sizes #1193 + [karimbahgat] + +- Tiff: allow writing floating point tag values #1113 + [bpedersen2] + +2.8.2 (2015-06-06) +------------------ + +- Bug fix: Fixed Tiff handling of bad EXIF data + [radarhere] + +2.8.1 (2015-04-02) +------------------ + +- Bug fix: Catch struct.error on invalid JPEG, fixes #1163. #1165 + [wiredfool, hugovk] + +2.8.0 (2015-04-01) +------------------ + +- Fix 32-bit BMP loading (RGBA or RGBX) #1125 + [artscoop] + +- Fix UnboundLocalError in ImageFile #1131 + [davarisg] + +- Re-enable test image caching #982 + [hugovk, homm] + +- Fix: Cannot identify EPS images #1152 (fixes #1104) + [hugovk] + +- Configure setuptools to run nosetests, fixes #729 + [aclark4life] + +- Style/health fixes + [radarhere, hugovk] + +- Add support for HTTP response objects to Image.open() #1151 + [mfitzp] + +- Improve reference docs for PIL.ImageDraw.Draw.pieslice() #1145 + [audreyr] + +- Added copy method font_variant() and accessible properties to truetype() #1123 + [radarhere] + +- Fix ImagingEffectNoise #1128 + [hugovk] + +- Remove unreachable code #1126 + [hugovk] + +- Let Python do the endian stuff + tests #1121 + [amoibos, radarhere] + +- Fix webp decode memory leak #1114 + [benoit-pierre] + +- Fast path for opaque pixels in RGBa unpacker #1088 + [bgilbert] + +- Enable basic support for 'RGBa' raw encoding/decoding #1096 + [immerrr] + +- Fix pickling L mode images with no palette, #1095 + [hugovk] + +- iPython display hook #1091 + [wiredfool] + +- Adjust buffer size when quality=keep #1079 (fixes #148 again) + [wiredfool] + +- Fix for corrupted bitmaps embedded in truetype fonts #1072 + [jackyyf, wiredfool] + +2.7.0 (2015-01-01) +------------------ + +- Split Sane into a separate repo: https://github.com/python-pillow/Sane + [hugovk] + +- Look for OS X and Linux fonts in common places #1054 + [charleslaw] + +- Fix CVE-2014-9601, potential PNG decompression DOS #1060 + [wiredfool] + +- Use underscores, not spaces, in TIFF tag kwargs #1044, #1058 + [anntzer, hugovk] + +- Update PSDraw for Python3, add tests #1055 + [hugovk] + +- Use Bicubic filtering by default for thumbnails. Don't use Jpeg Draft mode for thumbnails #1029 + [homm] + +- Fix MSVC compiler error: Use Py_ssize_t instead of ssize_t #1051 + [cgohlke] + +- Fix compiler error: MSVC needs variables defined at the start of the block #1048 + [cgohlke] + +- The GIF Palette optimization algorithm is only applicable to mode='P' or 'L' #993 + [moriyoshi] + +- Use PySide as an alternative to PyQt4/5 #1024 + [holg] + +- Replace affine-based im.resize implementation with convolution-based im.stretch #997 + [homm] + +- Replace Gaussian Blur implementation with iterated fast box blur. #961 Note: Radius parameter is interpreted differently than before. + [homm] + +- Better docs explaining import _imaging failure #1016, build #1017, mode #1018, PyAccess, PixelAccess objects #1019 Image.quantize #1020 and Image.save #1021 + [wiredfool] + +- Fix for saving TIFF image into an io.BytesIO buffer #1011 + [mfergie] + +- Fix antialias compilation on debug versions of Python #1010 + [wiredfool] + +- Fix for Image.putdata segfault #1009 + [wiredfool] + +- Ico save, additional tests #1007 + [exherb] + +- Use PyQt4 if it has already been imported, otherwise prefer PyQt5 #1003 + [AurelienBallier] + +- Speedup resample implementation up to 2.5 times #977 + [homm] + +- Speed up rotation by using cache aware loops, added transpose to rotations #994 + [homm] + +- Fix Bicubic interpolation #970 + [homm] + +- Support for 4-bit grayscale TIFF images #980 + [hugovk] + +- Updated manifest #957 + [wiredfool] + +- Fix PyPy 2.4 regression #958 + [wiredfool] + +- Webp Metadata Skip Test comments #954 + [wiredfool] + +- Fixes for things rpmlint complains about #942 + [manisandro] + +2.6.2 (2015-01-01) +------------------ + +- Fix CVE-2014-9601, potential PNG decompression DOS #1060 + [wiredfool] + +- Fix Regression in PyPy 2.4 in streamio #958 + [wiredfool] + +2.6.1 (2014-10-11) +------------------ + +- Fix SciPy regression in Image.resize #945 + [wiredfool] + +- Fix manifest to include all test files. + [aclark4life] + +2.6.0 (2014-10-01) +------------------ + +- Relax precision of ImageDraw tests for x86, GimpGradient for PPC #930 + [wiredfool] + +2.6.0-rc1 (2014-09-29) +---------------------- + +- Use redistributable image for testing #884 + [hugovk] + +- Use redistributable ICC profiles for testing, skip if not available #923 + [wiredfool] + +- Additional documentation for JPEG info and save options #922 + [wiredfool] + +- Fix JPEG Encoding memory leak when exif or qtables were specified #921 + [wiredfool] + +- Image.tobytes() and Image.tostring() documentation update #916 #917 + [mgedmin] + +- On Windows, do not execute convert.exe without specifying path #912 + [cgohlke] + +- Fix msvc build error #911 + [cgohlke] + +- Fix for handling P + transparency -> RGBA conversions #904 + [wiredfool] + +- Retain alpha in ImageEnhance operations #909 + [wiredfool] + +- Jpeg2k Decode/encode memory leak fix #898 + [joshware, wiredfool] + +- EpsFilePlugin Speed improvements #886 + [wiredfool, karstenw] + +- Don't resize if already the right size #892 + [radarhere] + +- Fix for reading multipage TIFFs #885 + [kostrom, wiredfool] + +- Correctly handle saving gray and CMYK JPEGs with quality=keep #857 + [etienned] + +- Correct duplicate Tiff Metadata and Exif tag values + [hugovk] + +- Windows fixes #871 + [wiredfool] + +- Fix TGA files with image ID field #856 + [megabuz] + +- Fixed wrong P-mode of small, unoptimized L-mode GIF #843 + [uvNikita] + +- Fixed CVE-2014-3598, a DOS in the Jpeg2KImagePlugin + [Andrew Drake] + +- Fixed CVE-2014-3589, a DOS in the IcnsImagePlugin + [Andrew Drake] + +- setup.py: Close open file handle before deleting #844 + [divergentdave] + +- Return Profile with Transformed Images #837 + [wiredfool] + +- Changed docstring to refer to the correct function #836 + [MatMoore] + +- Adding coverage support for C code tests #833 + [wiredfool] + +- PyPy performance improvements #821 + [wiredfool] + +- Added support for reading MPO files #822 + [Feneric] + +- Added support for encoding and decoding iTXt chunks #818 + [dolda2000] + +- HSV Support #816 + [wiredfool] + +- Removed unusable ImagePalette.new() + [hugovk] + +- Fix Scrambled XPM #808 + [wiredfool] + +- Doc cleanup + [wiredfool] + +- Fix ``ImageStat`` docs #796 + [akx] + +- Added docs for ExifTags #794 + [Wintermute3] + +- More tests for CurImagePlugin, DcxImagePlugin, Effects.c, GimpGradientFile, ImageFont, ImageMath, ImagePalette, IptcImagePlugin, SpiderImagePlugin, SgiImagePlugin, XpmImagePlugin and _util + [hugovk] + +- Fix return value of FreeTypeFont.textsize() does not include font offsets #784 + [tk0miya] + +- Fix dispose calculations for animated GIFs #765 + [larsjsol] + +- Added class checking to Image __eq__ function #775 + [radarhere, hugovk] + +- Test PalmImagePlugin and method to skip known bad tests #776 + [hugovk, wiredfool] + +2.5.3 (2014-08-18) +------------------ + +- Fixed CVE-2014-3598, a DOS in the Jpeg2KImagePlugin (backport) + [Andrew Drake] + + +2.5.2 (2014-08-13) +------------------ + +- Fixed CVE-2014-3589, a DOS in the IcnsImagePlugin (backport) + [Andrew Drake] + +2.5.1 (2014-07-10) +------------------ + +- Fixed install issue if Multiprocessing.Pool is not available + [wiredfool] + +- 32bit mult overflow fix #782 + [wiredfool] + +2.5.0 (2014-07-01) +------------------ + +- Imagedraw rewrite #737 + [terseus, wiredfool] + +- Add support for multithreaded test execution #755 + [wiredfool] + +- Prevent shell injection #748 + [mbrown1413, wiredfool] + +- Support for Resolution in BMP files #734 + [gcq] + +- Fix error in setup.py for Python 3 #744 + [matthew-brett] + +- Pyroma fix and add Python 3.4 to setup metadata #742 + [wirefool] + +- Top level flake8 fixes #741 + [aclark4life] + +- Remove obsolete Animated Raster Graphics (ARG) support #736 + [hugovk] + +- Fix test_imagedraw failures #727 + [cgohlke] + +- Fix AttributeError: class Image has no attribute 'DEBUG' #726 + [cgohlke] + +- Fix msvc warning: 'inline' : macro redefinition #725 + [cgohlke] + +- Cleanup #654 + [dvska, hugovk, wiredfool] + +- 16-bit monochrome support for JPEG2000 #730 + [videan42] + +- Fixed ImagePalette.save + [brightpisces] + +- Support JPEG qtables #677 + [csinchok] + +- Add binary morphology addon + [dov, wiredfool] + +- Decompression bomb protection #674 + [hugovk] + +- Put images in a single directory #708 + [hugovk] + +- Support OpenJpeg 2.1 #681 + [al45tair, wiredfool] + +- Remove unistd.h #include for all platforms #704 + [wiredfool] + +- Use unittest for tests + [hugovk] + +- ImageCms fixes + [hugovk] + +- Added more ImageDraw tests + [hugovk] + +- Added tests for Spider files + [hugovk] + +- Use libtiff to write any compressed tiff files #669 + [wiredfool] + +- Support for pickling Image objects + [hugovk] + +- Fixed resolution handling for EPS thumbnails #619 + [eliempje] + +- Fixed rendering of some binary EPS files (Issue #302) + [eliempje] + +- Rename variables not to use built-in function names #670 + [hugovk] + +- Ignore junk JPEG markers + [hugovk] + +- Change default interpolation for Image.thumbnail to Image.ANTIALIAS + [hugovk] + +- Add tests and fixes for saving PDFs + [hugovk] + +- Remove transparency resource after P->RGBA conversion + [hugovk] + +- Clean up preprocessor cruft for Windows #652 + [CounterPillow] + +- Adjust Homebrew freetype detection logic #656 + [jacknagel] + +- Added Image.close, context manager support + [wiredfool] + +- Added support for 16 bit PGM files + [wiredfool] + +- Updated OleFileIO to version 0.30 from upstream #618 + [hugovk] + +- Added support for additional TIFF floating point format + [Hijackal] + +- Have the tempfile use a suffix with a dot + [wiredfool] + +- Fix variable name used for transparency manipulations #604 + [nijel] + +2.4.0 (2014-04-01) +------------------ + +- Indexed Transparency handled for conversions between L, RGB, and P modes #574 (fixes #510) + [wiredfool] + +- Conversions enabled from RGBA->P #574 (fixes #544) + [wiredfool] + +- Improved icns support #565 + [al45tair] + +- Fix libtiff leaking open files #580 (fixes #526) + [wiredfool] + +- Fixes for Jpeg encoding in Python 3 #578 (fixes #577) + [wiredfool] + +- Added support for JPEG 2000 #547 + [al45tair] + +- Add more detailed error messages to Image.py #566 + [larsmans] + +- Avoid conflicting _expand functions in PIL & MINGW, fixes #538 + [aclark4life] + +- Merge from Philippe Lagadec’s OleFileIO_PL fork #512 + [vadmium] + +- Fix ImageColor.getcolor #534 + [homm] + +- Make ICO files work with the ImageFile.Parser interface #525 (fixes #522) + [wiredfool] + +- Handle 32bit compiled python on 64bit architecture #521 + [choppsv1] + +- Fix support for characters >128 using .pcf or .pil fonts in Py3k #517 (fixes #505) + [wiredfool] + +- Skip CFFI test earlier if it's not installed #516 + [wiredfool] + +- Fixed opening and saving odd sized .pcx files #535 (fixes #523) + [wiredfool] + +- Fixed palette handling when converting from mode P->RGB->P + [d-schmidt] + +- Fixed saving mode P image as a PNG with transparency = palette color 0 + [d-schmidt] + +- Improve heuristic used when saving progressive and optimized JPEGs with high quality values #504 + [e98cuenc] + +- Fixed DOS with invalid palette size or invalid image size in BMP file + [wiredfool] + +- Added support for BMP version 4 and 5 + [eddwardo, wiredfool] + +- Fix segfault in getfont when passed a memory resident font + [wiredfool] + +- Fix crash on Saving a PNG when icc-profile is None #496 + [brutasse] + +- Cffi+Python implementation of the PixelAccess object + [wiredfool] + +- PixelAccess returns unsigned ints for I16 mode + [wiredfool] + +- Minor patch on booleans + Travis #474 + [sciunto] + +- Look in multiarch paths in GNU platforms #511 + [pinotree] + +- Add arch support for pcc64, s390, s390x, armv7l, aarch64 #475 + [manisandro] + +- Add arch support for ppc + [wiredfool] + +- Correctly quote file names for WindowsViewer command + [cgohlke] + +- Prefer homebrew freetype over X11 freetype (but still allow both) #466 + [dmckeone] + +2.3.2 (2014-08-13) +------------------ + +- Fixed CVE-2014-3589, a DOS in the IcnsImagePlugin (backport) + [Andrew Drake] + +2.3.1 (2014-03-14) +------------------ + +- Fix insecure use of tempfile.mktemp (CVE-2014-1932 CVE-2014-1933) + [wiredfool] + +2.3.0 (2014-01-01) +------------------ + +- Stop leaking filename parameter passed to getfont #459 + [jpharvey] + +- Report availability of LIBTIFF during setup and selftest + [cgohlke] + +- Fix msvc build error C1189: "No Target Architecture" #460 + [cgohlke] + +- Fix memory leak in font_getsize + [wiredfool] + +- Correctly prioritize include and library paths #442 + [ohanar] + +- Image.point fixes for numpy.array and docs #441 + [wiredfool] + +- Save the transparency header by default for PNGs #424 + [wiredfool] + +- Support for PNG tRNS header when converting from RGB->RGBA #423 + [wiredfool] + +- PyQT5 Support #418 + [wiredfool] + +- Updates for saving color tiffs w/compression using libtiff #417 + [wiredfool] + +- 2gigapix image fixes and redux + [wiredfool] + +- Save arbitrary tags in Tiff image files #369 + [wiredfool] + +- Quote filenames and title before using on command line #398 + [tmccombs] + +- Fixed Viewer.show to return properly #399 + [tmccombs] + +- Documentation fixes + [wiredfool] + +- Fixed memory leak saving images as webp when webpmux is available #429 + [cezarsa] + +- Fix compiling with FreeType 2.5.1 #427 + [stromnov] + +- Adds directories for NetBSD #411 + [deepy] + +- Support RGBA TIFF with missing ExtraSamples tag #393 + [cgohlke] + +- Lossless WEBP Support #390 + [wiredfool] + +- Take compression as an option in the save call for tiffs #389 + [wiredfool] + +- Add support for saving lossless WebP. Just pass 'lossless=True' to save() #386 + [liftoff] + +- LCMS support upgraded from version 1 to version 2 #380 (fixes #343) + [wiredfool] + +- Added more raw decoder 16 bit pixel formats #379 + [svanheulen] + +- Document remaining Image* modules listed in PIL handbook + [irksep] + +- Document ImageEnhance, ImageFile, ImageFilter, ImageFont, ImageGrab, ImageMath, and ImageOps + [irksep] + +- Port and update docs for Image, ImageChops, ImageColor, and ImageDraw + [irksep] + +- Move or copy content from README.rst to docs/ + [irksep] + +- Respect CFLAGS/LDFLAGS when searching for headers/libs + [iElectric] + +- Port PIL Handbook tutorial and appendices + [irksep] + +- Alpha Premultiplication support for transform and resize #364 + [wiredfool] + +- Fixes to make Pypy 2.1.0 work on Ubuntu 12.04/64 #359 + [wiredfool] + +2.2.2 (2013-12-11) +------------------ + +- Fix compiling with FreeType 2.5.1 #427 + [stromnov] + +2.2.1 (2013-10-02) +------------------ + +- Error installing Pillow 2.2.0 on Mac OS X (due to hard dep on brew) #357 (fixes #356) + [wiredfool] + +2.2.0 (2013-10-02) +------------------ + +- Bug in image transformations resulting from uninitialized memory #348 (fixes #254) + [nikmolnar] + +- Fix for encoding of b_whitespace #346 (similar to closed issue #272) + [mhogg] + +- Add numpy array interface support for 16 and 32 bit integer modes #347 (fixes #273) + [cgohlke] + +- Partial fix for #290: Add preliminary support for TIFF tags. + [wiredfool] + +- Fix #251 and #326: circumvent classification of pngtest_bad.png as malware + [cgohlke] + +- Add typedef uint64_t for MSVC #339 + [cgohlke] + +- setup.py: better support for C_INCLUDE_PATH, LD_RUN_PATH, etc. #336 (fixes #329) + [nu774] + +- _imagingcms.c: include windef.h to fix build issue on MSVC #335 (fixes #328) + [nu774] + +- Automatically discover homebrew include/ and lib/ paths on OS X #330 + [donspaulding] + +- Fix bytes which should be bytearray #325 + [manisandro] + +- Add respective paths for C_INCLUDE_PATH, LD_RUN_PATH (rpath) to build + if specified as environment variables #324 + [seanupton] + +- Fix #312 + gif optimize improvement + [d-schmidt] + +- Be more tolerant of tag read failures #320 + [ericbuehl] + +- Catch truncated zTXt errors #321 (fixes #318) + [vytisb] + +- Fix IOError when saving progressive JPEGs #313 + [e98cuenc] + +- Add RGBA support to ImageColor #309 + [yoavweiss] + +- Test for ``str``, not ``"utf-8"`` #306 (fixes #304) + [mjpieters] + +- Fix missing import os in _util.py #303 + [mnowotka] + +- Added missing exif tags #300 + [freyes] + +- Fail on all import errors #298, #299 (fixes #297) + [macfreek, wiredfool] + +- Fixed Windows fallback (wasn't using correct file in Windows fonts) #295 + [lmollea] + +- Moved ImageFile and ImageFileIO comments to docstrings #293 + [freyes] + +- Restore compatibility with ISO C #289 + [cgohlke] + +- Use correct format character for C int type #288 + [cgohlke] + +- Allocate enough memory to hold pointers in encode.c #287 + [cgohlke] + +- Fillorder double shuffling bug when FillOrder ==2 and decoding using libtiff #284 (fixes #279) + [wiredfool] + +- Moved Image module comments to docstrings. + [freyes] + +- Add 16-bit TIFF support #277 (fixes #274) + [wiredfool] + +- Ignore high ascii characters in string.whitespace #276 (fixes #272) + [wiredfool] + +- Added clean/build to tox to make it behave like Travis #275 + [freyes] + +- Adding support for metadata in webp images #271 + [heynemann] + +2.1.0 (2013-07-02) +------------------ + +- Add /usr/bin/env python shebangs to all scripts in /Scripts #197 + [mgorny] + +- Add several TIFF decoders and encoders #268 + [megabuz] + +- Added support for alpha transparent webp images. + +- Adding Python 3 support for StringIO. + +- Adding Python3 basestring compatibility without changing basestring. + +- Fix webp encode errors on win-amd64 #259 + [cgohlke] + +- Better fix for ZeroDivisionError in ImageOps.fit for image.size height is 1 #267 + [chrispbailey] + +- Better support for ICO images. + +- Changed PY_VERSION_HEX #190 (fixes #166) + +- Changes to put everything under the PIL namespace #191 + [wiredfool] + +- Changing StringIO to BytesIO. + +- Cleanup whitespace. + [Arfrever] + +- Don't skip 'import site' on initialization when running tests for inplace builds. + [cgohlke] + +- Enable warnings for test suite #227 + [wiredfool] + +- Fix for ZeroDivisionError in ImageOps.fit for image.size == (1,1) #255 + [pterk] + +- Fix for if isinstance(filter, collections.Callable) crash. Python bug #7624 on <2.6.6 + +- Remove double typedef declaration #194 (fixes #193) + [evertrol] + +- Fix msvc compile errors (#230). + +- Fix rendered characters have been chipped for some TrueType fonts + [tk0miya] + +- Fix usage of pilfont.py script #184 + [fabiomcosta] + +- Fresh start for docs, generated by sphinx-apidoc. + +- Introduce --enable-x and fail if it is given and x is not available. + +- Partial work to add a wrapper for WebPGetFeatures to correctly support #220 (fixes #204) + +- Significant performance improvement of ``alpha_composite`` function #156 + [homm] + +- Support explicitly disabling features via --disable-* options #240 + [mgorny] + +- Support selftest.py --installed, fixes #263 + +- Transparent WebP Support #220 (fixes #204) + [euangoddard, wiredfool] + +- Use PyCapsule for py3.1 #238 (fixes #237) + [wiredfool] + +- Workaround for: https://bugs.python.org/issue16754 in 3.2.x < 3.2.4 and 3.3.0. + +2.0.0 (2013-03-15) +------------------ + +.. Note:: Special thanks to Christoph Gohlke and Eric Soroos for assisting with a pre-PyCon 2013 release! + +- Many other bug fixes and enhancements by many other people. + +- Add Python 3 support. (Pillow >= 2.0.0 supports Python 2.6, 2.7, 3.2, 3.3. Pillow < 2.0.0 supports Python 2.4, 2.5, 2.6, 2.7.) + [fluggo] + +- Add PyPy support (experimental, please see #67) + +- Add WebP support #96 + [lqs] + +- Add Tiff G3/G4 support (experimental) + [wiredfool] + +- Backport PIL's PNG/Zip improvements #95, #97 + [olt] + +- Various 64-bit and Windows fixes. + [cgohlke] + +- Add testing suite. + [cgohlke, fluggo] + +- Added support for PNG images with transparency palette. + [d-schmidt] + +1.7.8 (2012-11-01) +------------------ + +- Removed doctests.py that made tests of other packages fail. + [thomasdesvenain] + +- Fix opening psd files with RGBA layers when A mode is not of type 65535 but 3. + Fixes #3 + [thomasdesvenain] + + +1.7.7 (2012-04-04) +------------------ + +- UNDEF more types before including windows headers + [mattip] + +1.7.6 (2012-01-20) +------------------ + +- Bug fix: freetype not found on Mac OS X with case-sensitive filesystem + [gjo] + +- Bug fix: Backport fix to split() after open() (regression introduced in PIL 1.1.7). + [sfllaw] + +1.7.5 (2011-09-07) +------------------ + +- Fix for sys.platform = "linux3" + [blueyed] + +- Package cleanup and additional documentation + [aclark4life] + +1.7.4 (2011-07-21) +------------------ + +- Fix brown bag release + [aclark4life] + +1.7.3 (2011-07-20) +------------------ + +- Fix : resize need int values, append int conversion in thumbnail method + [harobed] + +1.7.2 (2011-06-02) +------------------ + +- Bug fix: Python 2.4 compat + [aclark4life] + +1.7.1 (2011-05-31) +------------------ + +- More multi-arch support + [SteveM, regebro, barry, aclark4life] + +1.7.0 (2011-05-27) +------------------ + +- Add support for multi-arch library directory /usr/lib/x86_64-linux-gnu + [aclark4life] + +1.6 (12/01/2010) +---------------- + +- Bug fix: /usr/x11/include should be added to include_dirs not library_dirs + [elro] + +- Doc fixes + [aclark4life] + +1.5 (11/28/2010) +---------------- + +- Module and package fixes + [aclark4life] + +1.4 (11/28/2010) +---------------- + +- Doc fixes + [aclark4life] + +1.3 (11/28/2010) +---------------- + +- Add support for /lib64 and /usr/lib64 library directories on Linux + [aclark4life] + +- Doc fixes + [aclark4life] + +1.2 (08/02/2010) +---------------- + +- On OS X also check for freetype2 in the X11 path + [jezdez] + +- Doc fixes + [aclark4life] + +1.1 (07/31/2010) +---------------- + +- Removed setuptools_hg requirement + [aclark4life] + +- Doc fixes + [aclark4life] + +1.0 (07/30/2010) +---------------- + +- Remove support for ``import Image``. ``from PIL import Image`` now required. +- Forked PIL based on `Chris McDonough and Hanno Schlichting's setuptools compatible re-packaging `_ + [aclark4life] + +Pre-fork +======== + +0.2b5-1.1.7 +----------- + +:: + + -*- coding: utf-8 -*- + + The Python Imaging Library + $Id$ + + ACKNOWLEDGEMENTS: PIL wouldn't be what it is without the help of: + David Ascher, Phil Austin, Douglas Bagnall, Larry Bates, Anthony + Baxter, William Baxter, Denis Benoit, Jan Blom, Duncan Booth, Alexey + Borzenkov, Jeff Breidenbach, Roger Burnham, Zac Burns, Gene Cash, + Kevin Cazabon, Fred Clare, Greg Coats, Chris Cogdon, Greg Couch, Bill + Crutchfield, Abel Deuring, Tim Docker, Fred Drake, Graham Dumpleton, + Matthew Ellis, Eric Etheridge, Daniel Fetchinson, Robin Friedrich, + Pier Paolo Glave, Federico Di Gregorio, Markus Gritsch, Daniel + Haertle, Greg Hamilton, Mark Hammond, Bernhard Herzog, Rob Hooft, Bob + Ippolito, Jack Jansen, Bill Janssen, Edward Jones, Richard Jones, + Håkan Karlsson, Robert Kern, David Kirtley, Bob Klimek, Matthias + Klose, Andrew Kuchling, Magnus Källström, Victor Lacina, Ben Last, + Hamish Lawson, Cesare Leonardi, Andrew MacIntyre, Jan Matejek, Naveen + Michaud-Agrawal, Gordon McMillan, Skip Montanaro, Fredrik Nehr, + Russell Nelson, Luciano Nocera, Travis Oliphant, Piet van Oostrum, + Richard Oudkerk, Paul Pharr, Andres Polit, Conrado Porto Lopes Gouvêa, + Eric Raymond, Victor Reijs, Bertil Reinhammar, Nicholas Riley, Don + Rozenberg, Toby Sargeant, Barry Scott, Les Schaffer, Joel Shprentz, + Klamer Shutte, Gene Skonicki, Niki Spahiev, D. Alan Stewart, Perry + Stoll, Paul Svensson, Ulrik Svensson, Miki Tebeka, Michael van + Tellingen, Ivan Tkatchev, Dan Torop, Adam Twardoch, Rune Uhlin, Dmitry + Vasiliev, Sasha Voynow, Charles Waldman, Collin Winter, Dan Wolfe, + Ka-Ping Yee, and many others (if your name should be on this list, let + me know.) + +1.1.6 to 1.1.7 +-------------- + +This section may not be fully complete. For changes since this file +was last updated, see the repository revision history: +http://svn.effbot.org/public/pil/ + +1.1.7 final +----------- + +- Set GIF loop info property to the number of iterations if a NETSCAPE + loop extension is present, instead of always setting it to 1 (from + Valentino Volonghi). + +1.1.7c1 +------- + +- Improved PNG compression (from Alexey Borzenkov). + +- Read interlaced PNG files (from Conrado Porto Lopes Gouvêa) + +- Added various TGA improvements from Alexey Borzenkov, including + support for specifying image orientation. + +- Bumped block threshold to 16 megabytes, made size estimation a bit + more accurate. This speeds up allocation of large images. + +- Fixed rounding error in ImagingDrawWideLine. + + "gormish" writes: ImagingDrawWideLine() in Draw.c has a bug in every + version I've seen, which leads to different width lines depending on + the order of the points in the line. This is especially bad at some + angles where a 'width=2' line can completely disappear. + +- Added support for RGBA mode to the SGI module (based on code by + Karsten Hiddemann). + +- Handle repeated IPTC tags (adapted from a patch by Eric Bruning). + + Eric writes: According to the specification, some IPTC tags can be + repeated, e.g., tag 2:25 (keywords). PIL 1.1.6 only retained the last + instance of that tag. Below is a patch to store all tags. If there are + multiple tag instances, they are stored in a (python) list. Single tag + instances remain as strings. + +- Fixed potential crash in ImageFilter for small target images + (reported by Zac Burns and Daniel Fetchinson). + +- Use BMP instead of JPEG as temporary show format on Mac OS X. + +- Fixed putpixel/new for I;16 with colors > 255. + +- Added integer power support to ImagingMath. + +- Added limited support for I;16L mode (explicit little endian). + +- Moved WMF support into Image.core; enable WMF rendering by default + if renderer is available. + +- Mark the ARG plugin as obsolete. + +- Added version query mechanism to ImageCms and ImageFont, for + debugging. + +- Added (experimental) ImageCms function for fetching the ICC profile + for the current display (currently Windows only). + + Added HWND/HDC support to ImageCms.get_display_profile(). + +- Added WMF renderer (Windows only). + +- Added ImagePointHandler and ImageTransformHandler mixins; made + ImageCmsTransform work with im.point. + +- Fixed potential endless loop in the XVThumbnail reader (from Nikolai + Ugelvik). + +- Added Kevin Cazabon's pyCMS package. + + The C code has been moved to _imagingcms.c, the Python interface + module is installed as PIL.ImageCMS. + + Added support for in-memory ICC profiles. + + Unified buildTransform and buildTransformFromOpenProfiles. + + The profile can now be either a filename, a profile object, or a + file-like object containing an in-memory profile. + + Additional fixes from Florian Böch: + + Very nice - it just needs LCMS flags support so we can use black + point compensation and softproofing :) See attached patches. They + also fix a naming issue which could cause confusion - display + profile (ImageCms wording) actually means proof profile (lcms + wording), so I changed variable names and docstrings where + applicable. Patches are tested under Python 2.6. + +- Improved support for layer names in PSD files (from Sylvain Baubeau) + + Sylvain writes: I needed to be able to retrieve the names of the + layers in a PSD files. But PsdImagePlugin.py didn't do the job so I + wrote this very small patch. + +- Improved RGBA support for ImageTk for 8.4 and newer (from Con + Radchenko). + + This replaces the slow run-length based encoding model with true + compositing at the Tk level. + +- Added support for 16- and 32-bit images to McIdas loader. + + Based on file samples and stand-alone reader code provided by Craig + Swank. + +- Added ImagePalette support to putpalette. + +- Fixed problem with incremental parsing of PNG files. + +- Make selftest.py report non-zero status on failure (from Mark + Sienkiewicz) + +- Add big endian save support and multipage infrastructure to the TIFF + writer (from Sebastian Haase). + +- Handle files with GPS IFD but no basic EXIF IFD (reported by Kurt + Schwehr). + +- Added zTXT support (from Andrew Kuchling via Lowell Alleman). + +- Fixed potential infinite loop bug in ImageFont (from Guilherme Polo). + +- Added sample ICC profiles (from Kevin Cazabon) + +- Fixed array interface for I, F, and RGBA/RGBX images. + +- Added Chroma subsampling support for JPEG (from Justin Huff). + + Justin writes: Attached is a patch (against PIL 1.1.6) to provide + control over the chroma subsampling done by the JPEG encoder. This + is often useful for reducing compression artifacts around edges of + clipart and text. + +- Added USM/Gaussian Blur code from Kevin Cazabon. + +- Fixed bug w. uninitialized image data when cropping outside the + source image. + +- Use ImageShow to implement the Image.show method. + + Most notably, this picks the 'display' utility when available. It + also allows application code to register new display utilities via + the ImageShow registry. + +- Release the GIL in the PNG compressor (from Michael van Tellingen). + +- Revised JPEG CMYK handling. + + Always assume Adobe behaviour, both when reading and writing (based on + a patch by Kevin Cazabon, and test data by Tim V. and Charlie Clark, and + additional debugging by Michael van Tellingen). + +- Support for preserving ICC profiles (by Florian Böch via Tim Hatch). + + Florian writes: + + It's a beta, so still needs some testing, but should allow you to: + + - retain embedded ICC profiles when saving from/to JPEG, PNG, TIFF. + Existing code doesn't need to be changed. + - access embedded profiles in JPEG, PNG, PSD, TIFF. + + It also includes patches for TIFF to retain IPTC, Photoshop and XMP + metadata when saving as TIFF again, read/write TIFF resolution + information correctly, and to correct inverted CMYK JPEG files. + +- Fixed potential memory leak in median cut quantizer (from Evgeny Salmin). + +- Fixed OverflowError when reading upside-down BMP images. + +- Added resolution save option for PDF files. + + Andreas Kostyrka writes: I've included a patched PdfImagePlugin.py + based on 1.1.6 as included in Ubuntu, that supports a "resolution" + save option. Not great, but it makes the PDF saving more useful by + allowing PDFs that are not exactly 72dpi. + +- Look for Tcl/Tk include files in version-specific include directory + (from Encolpe Degoute). + +- Fixed grayscale rounding error in ImageColor.getcolor (from Tim + Hatch). + +- Fixed calculation of mean value in ImageEnhance.Contrast (reported + by "roop" and Scott David Daniels). + +- Fixed truetype positioning when first character has a negative left + bearing (from Ned Batchelder): + + Ned writes: In PIL 1.1.6, ImageDraw.text will position the string + incorrectly if the first character has a negative left bearing. To + see the problem, show a string like "///" in an italic font. The + first slash will be clipped at the left, and the string will be + mis-positioned. + +- Fixed resolution unit bug in tiff reader/writer (based on code by + Florian Höch, Gary Bloom, and others). + +- Added simple transparency support for RGB images (reported by + Sebastian Spaeth). + +- Added support for Unicode filenames in ImageFont.truetype (from Donn + Ingle). + +- Fixed potential crash in ImageFont.getname method (from Donn Ingle). + +- Fixed encoding issue in PIL/WalImageFile (from Santiago M. Mola). + +1.1.6 +----- + +- Fixed some 64-bit compatibility warnings for Python 2.5. + +- Added threading support for the Sane driver (from Abel Deuring). + +1.1.6b2 +------- + +- Added experimental "floodfill" function to the ImageDraw module + (based on code by Eric Raymond). + +- The default arguments for "frombuffer" doesn't match "fromstring" + and the documentation; this is a bug, and will most likely be fixed + in a future version. In this release, PIL prints a warning message + instead. To silence the warning, change any calls of the form + "frombuffer(mode, size, data)" to:: + + frombuffer(mode, size, data, "raw", mode, 0, 1) + +- Added "fromarray" function, which takes an object implementing the + NumPy array interface and creates a PIL Image from it. (from Travis + Oliphant). + +- Added NumPy array interface support (__array_interface__) to the + Image class (based on code by Travis Oliphant). + + This allows you to easily convert between PIL image memories and + NumPy arrays:: + + import numpy, Image + im = Image.open('hopper.jpg') + a = numpy.asarray(im) # a is readonly + im = Image.fromarray(a) + +- Fixed CMYK polarity for JPEG images, by treating all images as + "Adobe CMYK" images. (thanks to Cesare Leonardi and Kevin Cazabon + for samples, debugging, and patches). + +1.1.6b1 +------- + +- Added 'expand' option to the Image 'rotate' method. If true, the + output image is made large enough to hold the entire rotated image. + +- Changed the ImageDraw 'line' method to always draw the last pixel in + a polyline, independent of line angle. + +- Fixed bearing calculation and clipping in the ImageFont truetype + renderer; this could lead to clipped text, or crashes in the low-level + _imagingft module. (based on input from Adam Twardoch and + others). + +- Added ImageQt wrapper module, for converting PIL Image objects to + QImage objects in an efficient way. + +- Fixed 'getmodebands' to return the number of bands also for "PA" + and "LA" modes. Added 'getmodebandnames' helper that return the + band names. + +1.1.6a2 +------- + +- Added float/double support to the TIFF loader (from Russell + Nelson). + +- Fixed broken use of realloc() in path.c (from Jan Matejek) + +- Added save support for Spider images (from William Baxter). + +- Fixed broken 'paste' and 'resize' operations in pildriver + (from Bill Janssen). + +- Added support for duplex scanning to the Sane interface (Abel + Deuring). + +1.1.6a1 +------- + +- Fixed a memory leak in "convert(mode)", when converting from + L to P. + +- Added pixel access object. The "load" method now returns a + access object that can be used to directly get and set pixel + values, using ordinary [x, y] notation:: + + pixel = im.load() + v = pixel[x, y] + pixel[x, y] = v + + If you're accessing more than a few pixels, this is a lot + faster than using getpixel/putpixel. + +- Fixed building on Cygwin (from Miki Tebeka). + +- Fixed "point(callable)" on unloaded images (reported by Håkan + Karlsson). + +- Fixed size bug in ImageWin.ImageWindow constructor (from Victor + Reijs) + +- Fixed ImageMath float() and int() operations for Python 2.4 + (reported by Don Rozenberg). + +- Fixed "RuntimeError: encoder error -8 in tostring" problem for + wide "RGB", "I", and "F" images. + +- Fixed line width calculation. + +1.1.6a0 +------- + +- Fixed byte order issue in Image.paste(ink) (from Ka-Ping Yee). + +- Fixed off-by-0.5 errors in the ANTIALIAS code (based on input + from Douglas Bagnall). + +- Added buffer interface support to the Path constructor. If + a buffer is provided, it is assumed to contain a flat array + of float coordinates (e.g. array.array('f', seq)). + +- Added new ImageMath module. + +- Fixed ImageOps.equalize when used with a small number of distinct + values (reported by David Kirtley). + +- Fixed potential integer division in PSDraw.image (from Eric Etheridge). + +1.1.5c2 and 1.1.5 final +----------------------- + +- Added experimental PERSPECTIVE transform method (from Jeff Breidenbach). + +1.1.5c1 +------- + +- Make sure "thumbnail" never generates zero-wide or zero-high images + (reported by Gene Skonicki) + +- Fixed a "getcolors" bug that could result in a zero count for some + colors (reported by Richard Oudkerk). + +- Changed default "convert" palette to avoid "rounding errors" when + round-tripping white source pixels (reported by Henryk Gerlach and + Jeff Epler). + +1.1.5b3 +------- + +- Don't crash in "quantize" method if the number of colors requested + is larger than 256. This release raises a ValueError exception; + future versions may return a mode "RGB" image instead (reported + by Richard Oudkerk). + +- Added WBMP read/write support (based on code by Duncan Booth). + +1.1.5b2 +------- + +- Added DPI read/write support to the PNG codec. The decoder sets + the info["dpi"] attribute for PNG files with appropriate resolution + settings. The encoder uses the "dpi" option (based on code by Niki + Spahiev). + +- Added limited support for "point" mappings from mode "I" to mode "L". + Only 16-bit values are supported (other values are clipped), the lookup + table must contain exactly 65536 entries, and the mode argument must be + set to "L". + +- Added support for Mac OS X icns files (based on code by Bob Ippolito). + +- Added "ModeFilter" support to the ImageFilter module. + +- Added support for Spider images (from William Baxter). See the + comments in PIL/SpiderImagePlugin.py for more information on this + format. + +1.1.5b1 +------- + +- Added new Sane release (from Ralph Heinkel). See the Sane/README + and Sane/CHANGES files for more information. + +- Added experimental PngInfo chunk container to the PngImageFile + module. This can be used to add arbitrary chunks to a PNG file. + Create a PngInfo instance, use "add" or "add_text" to add chunks, + and pass the instance as the "pnginfo" option when saving the + file. + +- Added "getpalette" method. This returns the palette as a list, + or None if the image has no palette. To modify the palette, use + "getpalette" to fetch the current palette, modify the list, and + put it back using "putpalette". + +- Added optional flattening to the ImagePath "tolist" method. + tolist() or tolist(0) returns a list of 2-tuples, as before. + tolist(1) returns a flattened list instead. + +1.1.5a5 +------- + +- Fixed BILINEAR/BICUBIC/ANTIALIAS filtering for mode "LA". + +- Added "getcolors()" method. This is similar to the existing histogram + method, but looks at color values instead of individual layers, + and returns an unsorted list of (count, color) tuples. + + By default, the method returns None if finds more than 256 colors. + If you need to look for more colors, you can pass in a limit (this + is used to allocate internal tables, so you probably don't want to + pass in too large values). + +- Build improvements: Fixed building under AIX, improved detection of + FreeType2 and Mac OS X framework libraries, and more. Many thanks + to everyone who helped test the new "setup.py" script! + +1.1.5a4 +------- + +- The "save" method now looks for a file format driver before + creating the file. + +- Don't use antialiased truetype fonts when drawing in mode "P", "I", + and "F" images. + +- Rewrote the "setup.py" file. The new version scans for available + support libraries, and configures both the libImaging core library + and the bindings in one step. + + To use specific versions of the libraries, edit the ROOT variables + in the setup.py file. + +- Removed threaded "show" viewer; use the old "show" implementation + instead (Windows). + +- Added deprecation warnings to Image.offset, ImageDraw.setink, and + ImageDraw.setfill. + +- Added width option to ImageDraw.line(). The current implementation + works best for straight lines; it does not support line joins, so + polylines won't look good. + +- ImageDraw.Draw is now a factory function instead of a class. If + you need to create custom draw classes, inherit from the ImageDraw + class. All other code should use the factory function. + +- Fixed loading of certain PCX files (problem reported by Greg + Hamilton, who also provided samples). + +- Changed _imagingft.c to require FreeType 2.1 or newer. The + module can still be built with earlier versions; see comments + in _imagingft.c for details. + +1.1.5a3 +------- + +- Added 'getim' method, which returns a PyCObject wrapping an + Imaging pointer. The description string is set to IMAGING_MAGIC. + See Imaging.h for pointer and string declarations. + +- Fixed reading of TIFF JPEG images (problem reported by Ulrik + Svensson). + +- Made ImageColor work under Python 1.5.2 + +- Fixed division by zero "equalize" on very small images (from + Douglas Bagnall). + +1.1.5a2 +------- + +- The "paste" method now supports the alternative "paste(im, mask)" + syntax (in this case, the box defaults to im's bounding box). + +- The "ImageFile.Parser" class now works also for PNG files with + more than one IDAT block. + +- Added DPI read/write to the TIFF codec, and fixed writing of + rational values. The decoder sets the info["dpi"] attribute + for TIFF files with appropriate resolution settings. The + encoder uses the "dpi" option. + +- Disable interlacing for small (or narrow) GIF images, to + work around what appears to be a hard-to-find bug in PIL's + GIF encoder. + +- Fixed writing of mode "P" PDF images. Made mode "1" PDF + images smaller. + +- Made the XBM reader a bit more robust; the file may now start + with a few whitespace characters. + +- Added support for enhanced metafiles to the WMF driver. The + separate PILWMF kit lets you render both placeable WMF files + and EMF files as raster images. See + http://effbot.org/downloads#pilwmf + +1.1.5a1 +------- + +- Replaced broken WMF driver with a WMF stub plugin (see below). + +- Fixed writing of mode "1", "L", and "CMYK" PDF images (based on + input from Nicholas Riley and others). + +- Fixed adaptive palette conversion for zero-width or zero-height + images (from Chris Cogdon) + +- Fixed reading of PNG images from QuickTime 6 (from Paul Pharr) + +- Added support for StubImageFile plugins, including stub plugins + for BUFR, FITS, GRIB, and HDF5 files. A stub plugin can identify + a given file format, but relies on application code to open and + save files in that format. + +- Added optional "encoding" argument to the ImageFont.truetype + factory. This argument can be used to specify non-Unicode character + maps for fonts that support that. For example, to draw text using + the Microsoft Symbol font, use:: + + font = ImageFont.truetype("symbol.ttf", 16, encoding="symb") + draw.text((0, 0), unichr(0xF000 + 0xAA)) + + (note that the symbol font uses characters in the 0xF000-0xF0FF + range) + + Common encodings are "unic" (Unicode), "symb" (Microsoft Symbol), + "ADOB" (Adobe Standard), "ADBE" (Adobe Expert), and "armn" (Apple + Roman). See the FreeType documentation for more information. + +- Made "putalpha" a bit more robust; you can now attach an alpha + layer to a plain "L" or "RGB" image, and you can also specify + constant alphas instead of alpha layers (using integers or colour + names). + +- Added experimental "LA" mode support. + + An "LA" image is an "L" image with an attached transparency layer. + Note that support for "LA" is not complete; some operations may + fail or produce unexpected results. + +- Added "RankFilter", "MinFilter", "MedianFilter", and "MaxFilter" + classes to the ImageFilter module. + +- Improved support for applications using multiple threads; PIL + now releases the global interpreter lock for many CPU-intensive + operations (based on work by Kevin Cazabon). + +- Ignore Unicode characters in the PCF loader (from Andres Polit) + +- Fixed typo in OleFileIO.loadfat, which could affect loading of + FlashPix and Image Composer images (Daniel Haertle) + +- Fixed building on platforms that have Freetype but don't have + Tcl/Tk (Jack Jansen, Luciano Nocera, Piet van Oostrum and others) + +- Added EXIF GPSInfo read support for JPEG files. To extract + GPSInfo information, open the file, extract the exif dictionary, + and check for the key 0x8825 (GPSInfo). If present, it contains + a dictionary mapping GPS keys to GPS values. For a list of keys, + see the EXIF specification. + + The "ExifTags" module contains a GPSTAGS dictionary mapping GPS + tags to tag names. + +- Added DPI read support to the PCX and DCX codecs (info["dpi"]). + +- The "show" methods now uses a built-in image viewer on Windows. + This viewer creates an instance of the ImageWindow class (see + below) and keeps it running in a separate thread. NOTE: This + was disabled in 1.1.5a4. + +- Added experimental "Window" and "ImageWindow" classes to the + ImageWin module. These classes allow you to create a WCK-style + toplevel window, and use it to display raster data. + +- Fixed some Python 1.5.2 issues (to build under 1.5.2, use the + Makefile.pre.in/Setup.in approach) + +- Added support for the TIFF FillOrder tag. PIL can read mode "1", + "L", "P" and "RGB" images with non-standard FillOrder (based on + input from Jeff Breidenbach). + +1.1.4 final +----------- + +- Fixed ImageTk build problem on Unix. + +1.1.4b2 +------- + +- Improved building on Mac OS X (from Jack Jansen). + +- Improved building on Windows with MinGW (from Klamer Shutte). + +- If no font is specified, ImageDraw now uses the embedded default + font. Use the "load" or "truetype" methods to load a real font. + +- Added embedded default font to the ImageFont module (currently + an 8-pixel Courier font, taken from the X window distribution). + +1.1.4b1 +------- + +- Added experimental EXIF support for JPEG files. To extract EXIF + information from a JPEG file, open the file as usual, and call the + "_getexif" method. If successful, this method returns a dictionary + mapping EXIF TIFF tags to values. If the file does not contain EXIF + data, the "_getexif" method returns None. + + The "ExifTags" module contains a dictionary mapping tags to tag + names. + + This interface will most likely change in future versions. + +- Fixed a bug when using the "transparency" option with the GIF + writer. + +- Added limited support for "bitfield compression" in BMP files + and DIB buffers, for 15-bit, 16-bit, and 32-bit images. This + also fixes a problem with ImageGrab module when copying screendumps + from the clipboard on 15/16/32-bit displays. + +- Added experimental WAL (Quake 2 textures) loader. To use this + loader, import WalImageFile and call the "open" method in that + module. + +1.1.4a4 +------- + +- Added updated SANE driver (Andrew Kuchling, Abel Deuring) + +- Use Python's "mmap" module on non-Windows platforms to read some + uncompressed formats using memory mapping. Also added a "frombuffer" + function that allows you to access the contents of an existing string + or buffer object as if it were an image object. + +- Fixed a memory leak that could appear when processing mode "P" + images (from Pier Paolo Glave) + +- Ignore Unicode characters in the BDF loader (from Graham Dumpleton) + +1.1.4a3 released; Windows only +------------------------------ + +- Added experimental RGBA-on-RGB drawing support. To use RGBA + colours on an RGB image, pass "RGBA" as the second string to + the ImageDraw.Draw constructor. + +- Added support for non-ASCII strings (Latin-1) and Unicode + to the truetype font renderer. + +- The ImageWin "Dib" object can now be constructed directly from + an image object. + +- The ImageWin module now allows you use window handles as well + as device contexts. To use a window handle, wrap the handle in + an ImageWin.HWND object, and pass in this object instead of the + device context. + +1.1.4a2 +------- + +- Improved support for 16-bit unsigned integer images (mode "I;16"). + This includes TIFF reader support, and support for "getextrema" + and "point" (from Klamer Shutte). + +- Made the BdfFontFile reader a bit more robust (from Kevin Cazabon + and Dmitry Vasiliev) + +- Changed TIFF writer to always write Compression tag, even when + using the default compression (from Greg Couch). + +- Added "show" support for Mac OS X (from Dan Wolfe). + +- Added clipboard support to the "ImageGrab" module (Windows only). + The "grabclipboard" function returns an Image object, a list of + filenames (not in 1.1.4), or None if neither was found. + +1.1.4a1 +------- + +- Improved support for drawing RGB data in palette images. You can + now use RGB tuples or colour names (see below) when drawing in a + mode "P" image. The drawing layer automatically assigns color + indexes, as long as you don't use more than 256 unique colours. + +- Moved self test from MiniTest/test.py to ./selftest.py. + +- Added support for CSS3-style color strings to most places that + accept colour codes/tuples. This includes the "ImageDraw" module, + the Image "new" function, and the Image "paste" method. + + Colour strings can use one of the following formats: "#f00", + "#ff0000", "rgb(255,0,0)", "rgb(100%,0%,0%)", "hsl(0, 100%, 50%)", + or "red" (most X11-style colour names are supported). See the + documentation for the "ImageColor" module for more information. + +- Fixed DCX decoder (based on input from Larry Bates) + +- Added "IptcImagePlugin.getiptcinfo" helper to extract IPTC/NAA + newsphoto properties from JPEG, TIFF, or IPTC files. + +- Support for TrueType/OpenType fonts has been added to + the standard distribution. You need the freetype 2.0 + library. + +- Made the PCX reader a bit more robust when reading 2-bit + and 4-bit PCX images with odd image sizes. + +- Added "Kernel" class to the ImageFilter module. This class + allows you to filter images with user-defined 3x3 and 5x5 + convolution kernels. + +- Added "putdata" support for mode "I", "F" and "RGB". + +- The GIF writer now supports the transparency option (from + Denis Benoit). + +- A HTML version of the module documentation is now shipped + with the source code distribution. You'll find the files in + the Doc subdirectory. + +- Added support for Palm pixmaps (from Bill Janssen). This + change was listed for 1.1.3, but the "PalmImagePlugin" driver + didn't make it into the distribution. + +- Improved decoder error messages. + +1.1.3 final +----------- + +- Made setup.py look for old versions of zlib. For some background, + see: https://zlib.net/advisory-2002-03-11.txt + +1.1.3c2 +------- + +- Added setup.py file (tested on Unix and Windows). You still + need to build libImaging/imaging.lib in the traditional way, + but the setup.py script takes care of the rest. + + The old Setup.in/Makefile.pre.in build method is still + supported. + +- Fixed segmentation violation in ANTIALIAS filter (an internal + buffer wasn't properly allocated). + +1.1.3c1 +------- + +- Added ANTIALIAS downsampling filter for high-quality "resize" + and "thumbnail" operations. Also added filter option to the + "thumbnail" operation; the default value is NEAREST, but this + will most likely change in future versions. + +- Fixed plugin loader to be more robust if the __file__ + variable isn't set. + +- Added seek/tell support (for layers) to the PhotoShop + loader. Layer 0 is the main image. + +- Added new (but experimental) "ImageOps" module, which provides + shortcuts for commonly used operations on entire images. + +- Don't mess up when loading PNG images if the decoder leaves + data in the output buffer. This could cause internal errors + on some PNG images, with some versions of ZLIB. (Bug report + and patch provided by Bernhard Herzog.) + +- Don't mess up on Unicode filenames. + +- Don't mess up when drawing on big endian platforms. + +- Made the TIFF loader a bit more robust; it can now read some + more slightly broken TIFF files (based on input from Ted Wright, + Bob Klimek, and D. Alan Stewart) + +- Added OS/2 EMX build files (from Andrew MacIntyre) + +- Change "ImageFont" to reject image files if they don't have the + right mode. Older versions could leak memory for "P" images. + (Bug reported by Markus Gritsch). + +- Renamed some internal functions to avoid potential build + problem on Mac OS X. + +- Added DL_EXPORT where relevant (for Cygwin, based on input + from Robert Yodlowski) + +- (re)moved bogus __init__ call in BdfFontFile (bug spotted + by Fred Clare) + +- Added "ImageGrab" support (Windows only) + +- Added support for XBM hotspots (based on code contributed by + Bernhard Herzog). + +- Added write support for more TIFF tags, namely the Artist, + Copyright, DateTime, ResolutionUnit, Software, XResolution and + YResolution tags (from Greg Couch) + +- Added TransposedFont wrapper to ImageFont module + +- Added "optimize" flag to GIF encoder. If optimize is present + and non-zero, PIL will work harder to create a small file. + +- Raise "EOFError" (not IndexError) when reading beyond the + end of a TIFF sequence. + +- Support rewind ("seek(0)") for GIF and TIFF sequences. + +- Load grayscale GIF images as mode "L" + +- Added DPI read/write support to the JPEG codec. The decoder + sets the info["dpi"] attribute for JPEG files with JFIF dpi + settings. The encoder uses the "dpi" option:: + + im = Image.open("file.jpg") + dpi = im.info["dpi"] # raises KeyError if DPI not known + im.save("out.jpg", dpi=dpi) + + Note that PIL doesn't always preserve the "info" attribute + for normal image operations. + +1.1.2c1 and 1.1.2 final +----------------------- + +- Adapted to Python 2.1. Among other things, all uses of the + "regex" module have been replaced with "re". + +- Fixed attribute error when reading large PNG files (this bug + was introduced in maintenance code released after the 1.1.1 + release) + +- Ignore non-string objects in sys.path + +- Fixed Image.transform(EXTENT) for negative xoffsets + +- Fixed loading of image plugins if PIL is installed as a package. + (The plugin loader now always looks in the directory where the + Image.py module itself is found, even if that directory isn't on + the standard search path) + +- The Png plugin has been added to the list of preloaded standard + formats + +- Fixed bitmap/text drawing in fill mode. + +- Fixed "getextrema" to work also for multiband images. + +- Added transparency support for L and P images to the PNG codec. + +- Improved support for read-only images. The "load" method now + sets the "readonly" attribute for memory-mapped images. Operations + that modifies an image in place (such as "paste" and drawing operations) + creates an in-memory copy of the image, if necessary. (before this + change, any attempt to modify a memory-mapped image resulted in a + core dump...) + +- Added special cases for lists everywhere PIL expects a sequence. + This should speed up things like "putdata" and drawing operations. + +- The Image.offset method is deprecated. Use the ImageChops.offset + function instead. + +- Changed ImageChops operators to copy palette and info dictionary + from the first image argument. + +1.1.1 +----- + +- Additional fixes for Python 1.6/2.0, including TIFF "save" bug. + +- Changed "init" to properly load plugins when PIL is used as a + package. + +- Fixed broken "show" method (on Unix) + +1.0 to 1.1 +---------- + +- Adapted to Python 1.6 ("append" and other method changes) + +- Fixed Image.paste when pasting with solid colour and matte + layers ("L" or "RGBA" masks) (bug reported by Robert Kern) + +- To make it easier to distribute prebuilt versions of PIL, + the tkinit binding stuff has been moved to a separate + extension module, named "_imagingtk". + +0.3b2 to 1.0 final +------------------ + +- If there's no 16-bit integer (like on a Cray T3E), set + INT16 to the smallest integer available. Most of the + library works just fine anyway (from Bill Crutchfield) + +- Tweaks to make drawing work on big-endian platforms. + +1.0c2 +----- + +- If PIL is built with the WITH_TKINTER flag, ImageTk can + automatically hook into a standard Tkinter build. You + no longer need to build your own Tkinter to use the + ImageTk module. + + The old way still works, though. For more information, + see Tk/install.txt. + +- Some tweaks to ImageTk to support multiple Tk interpreters + (from Greg Couch). + +- ImageFont "load_path" now scans directory mentioned in .pth + files (from Richard Jones). + +1.0c1 +----- + +- The TIFF plugin has been rewritten. The new plugin fully + supports all major PIL image modes (including F and I). + +- The ImageFile module now includes a Parser class, which can + be used to incrementally decode an image file (while downloading + it from the net, for example). See the handbook for + details. + +- "show" now converts non-standard modes to "L" or "RGB" (as + appropriate), rather than writing weird things to disk for + "xv" to choke upon. (bug reported by Les Schaffer). + +1.0b2 +----- + +- Major speedups for rotate, transform(EXTENT), and transform(AFFINE) + when using nearest neighbour resampling. + +- Modified ImageDraw to be compatible with the Arrow graphics + interface. See the handbook for details. + +- PIL now automatically loads file codecs when used as a package + (from The Dragon De Monsyne). Also included an __init__.py file + in the standard distribution. + +- The GIF encoder has been modified to produce much smaller files. + + PIL now uses a run-length encoding method to encode GIF files. + On a random selection of GIF images grabbed from the web, this + version makes the images about twice as large as the original + LZW files, where the earlier version made them over 5 times + larger. YMMV, of course. + +- Added PCX write support (works with "1", "P", "L", and "RGB") + +- Added "bitmap" and "textsize" methods to ImageDraw. + +- Improved font rendering code. Fixed a bug or two, and moved + most of the time critical stuff to C. + +- Removed "bdf2pil.py". Use "pilfont.py" instead! + +- Improved 16-bit support (still experimental, though). + + The following methods now support "I;16" and "I;16B" images: + "getpixel", "copy", "convert" (to and from mode "I"), "resize", + "rotate", and "transform" with nearest neighbour filters, and + "save" using the IM format. The "new" and "open" functions + also work as expected. On Windows, 16-bit files are memory + mapped. + + NOTE: ALL other operations are still UNDEFINED on 16-bit images. + +- The "paste" method now supports constant sources. + + Just pass a colour value (a number or a tuple, depending on + the target image mode) instead of the source image. + + This was in fact implemented in an inefficient way in + earlier versions (the "paste" method generated a temporary + source image if you passed it a colour instead of an image). + In this version, this is handled on the C level instead. + +- Added experimental "RGBa" mode support. + + An "RGBa" image is an RGBA image where the colour components + have have been premultiplied with the alpha value. PIL allows + you to convert an RGBA image to an RGBa image, and to paste + RGBa images on top of RGB images. Since this saves a bunch + of multiplications and shifts, it is typically about twice + as fast an ordinary RGBA paste. + +- Eliminated extra conversion step when pasting "RGBA" or "RGBa" + images on top of "RGB" images. + +- Fixed Image.BICUBIC resampling for "RGB" images. + +- Fixed PCX image file handler to properly read 8-bit PCX + files (bug introduced in 1.0b1, reported by Bernhard + Herzog) + +- Fixed PSDraw "image" method to restore the coordinate + system. + +- Fixed "blend" problem when applied to images that was + not already loaded (reported by Edward C. Jones) + +- Fixed -f option to "pilconvert.py" (from Anthony Baxter) + +1.0b1 +----- + +- Added Toby J. Sargeant's quantization package. To enable + quantization, use the "palette" option to "convert":: + + imOut = im.convert("P", palette=Image.ADAPTIVE) + + This can be used with "L", "P", and "RGB" images. In this + version, dithering cannot be used with adaptive palettes. + + Note: ADAPTIVE currently maps to median cut quantization + with 256 colours. The quantization package also contains + a maximum coverage quantizer, which will be supported by + future versions of PIL. + +- Added Eric S. Raymond's "pildriver" image calculator to the + distribution. See the docstring for more information. + +- The "offset" method no longer dumps core if given positive + offsets (from Charles Waldman). + +- Fixed a resource leak that could cause ImageWin to run out of + GDI resources (from Roger Burnham). + +- Added "arc", "chord", and "pieslice" methods to ImageDraw (inspired + by code contributed by Richard Jones). + +- Added experimental 16-bit support, via modes "I;16" (little endian + data) and "I;16B" (big endian). Only a few methods properly support + such images (see above). + +- Added XV thumbnail file handler (from Gene Cash). + +- Fixed BMP image file handler to handle palette images with small + palettes (from Rob Hooft). + +- Fixed Sun raster file handler for palette images (from Charles + Waldman). + +- Improved various internal error messages. + +- Fixed Path constructor to handle arbitrary sequence objects. This + also affects the ImageDraw class (from Richard Jones). + +- Fixed a bug in JpegDecode that caused PIL to report "decoder error + -2" for some progressive JPEG files (reported by Magnus Källström, + who also provided samples). + +- Fixed a bug in JpegImagePlugin that caused PIL to hang when loading + JPEG files using 16-bit quantization tables. + +- The Image "transform" method now supports Image.QUAD transforms. + The data argument is an 8-tuple giving the upper left, lower + left, lower right, and upper right corner of the source quadrilateral. + Also added Image.MESH transform which takes a list + of quadrilaterals. + +- The Image "resize", "rotate", and "transform" methods now support + Image.BILINEAR (2x2) and Image.BICUBIC (4x4) resampling filters. + Filters can be used with all transform methods. + +- The ImageDraw "rectangle" method now includes both the right + and the bottom edges when drawing filled rectangles. + +- The TGA decoder now works properly for runlength encoded images + which have more than one byte per pixel. + +- "getbands" on an YCbCr image now returns ("Y", "Cb", "Cr") + +- Some file drivers didn't handle the optional "modify" argument + to the load method. This resulted in exceptions when you used + "paste" (and other methods that modify an image in place) on a + newly opened file. + +0.3b2 +----- + +The test suite includes 825 individual tests. + +- An Image "getbands" method has been added. It returns a tuple + containing the individual band names for this image. To figure + out how many bands an image has, use "len(im.getbands())". + +- An Image "putpixel" method has been added. + +- The Image "point" method can now be used to convert "L" images + to any other format, via a lookup table. That table should + contain 256 values for each band in the output image. + +- Some file drivers (including FLI/FLC, GIF, and IM) accidentally + overwrote the offset method with an internal attribute. All + drivers have been updated to use private attributes where + possible. + +- The Image "histogram" method now works for "I" and "F" images. + For these modes, PIL divides the range between the min and + max values used in the image into 256 bins. You can also + pass in your own min and max values via the "extrema" option:: + + h = im.histogram(extrema=(0, 255)) + +- An Image "getextrema" method has been added. It returns the + min and max values used in the image. In this release, this + works for single band images only. + +- Changed the PNG driver to load and save mode "I" images as + 16-bit images. When saving, values outside the range 0..65535 + are clipped. + +- Fixed ImageFont.py to work with the new "pilfont" compiler. + +- Added JPEG "save" and "draft" support for mode "YCbCr" images. + Note that if you save an "YCbCr" image as a JPEG file and read + it back, it is read as an RGB file. To get around this, you + can use the "draft" method:: + + im = Image.open("color.jpg") + im.draft("YCbCr", im.size) + +- Read "RGBA" TGA images. Also fixed the orientation bug; all + images should now come out the right way. + +- Changed mode name (and internal representation) from "YCrCb" + to "YCbCr" (!) + **WARNING: MAY BREAK EXISTING CODE** + +0.3b1 +----- + +The test suite includes 750 individual tests. + +- The "pilfont" package is now included in the standard PIL + distribution. The pilfont utility can be used to convert + X BDF and PCF raster font files to a format understood by + the ImageFont module. + +- GIF files are now interlaced by default. To write a + non-interlaced file, pass interlace=0 to the "save" + method. + +- The default string format has changed for the "fromstring" + and "tostring" methods. + **WARNING: MAY BREAK EXISTING CODE** + + NOTE: If no extra arguments are given, the first line in + the string buffer is the top line of the image, instead of + the bottom line. For RGB images, the string now contains + 3 bytes per pixel instead of 4. These changes were made + to make the methods compatible with the "fromstring" + factory function. + + To get the old behaviour, use the following syntax:: + + data = im.tostring("raw", "RGBX", 0, -1) + im.fromstring(data, "raw", "RGBX", 0, -1) + +- "new" no longer gives a MemoryError if the width or height + is zero (this only happened on platforms where malloc(0) + or calloc(0) returns NULL). + +- "new" now adds a default palette object to "P" images. + +- You can now convert directly between all modes supported by + PIL. When converting colour images to "P", PIL defaults to + a "web" palette and dithering. When converting grayscale + images to "1", PIL uses a thresholding and dithering. + +- Added a "dither" option to "convert". By default, "convert" + uses floyd-steinberg error diffusion for "P" and "1" targets, + so this option is only used to *disable* dithering. Allowed + values are NONE (no dithering) or FLOYDSTEINBERG (default). + :: + + imOut = im.convert("P", dither=Image.NONE) + +- Added a full set of "I" decoders. You can use "fromstring" + (and file decoders) to read any standard integer type as an + "I" image. + +- Added some support for "YCbCr" images (creation, conversion + from/to "L" and "RGB", IM YCC load/save) + +- "getpixel" now works properly with fractional coordinates. + +- ImageDraw "setink" now works with "I", "F", "RGB", "RGBA", + "RGBX", "CMYK", and "YCbCr" images. + +- ImImagePlugin no longer attaches palettes to "RGB" images. + +- Various minor fixes. + +0.3a4 +----- + +- Added experimental IPTC/NAA support. + +- Eliminated AttributeError exceptions after "crop" (from + Skip Montanaro) + +- Reads some uncompressed formats via memory mapping (this + is currently supported on Win32 only) + +- Fixed some last minute glitches in the last alpha release + (Types instead of types in Image.py, version numbers, etc.) + +- Eliminated some more bogus compiler warnings. + +- Various fixes to make PIL compile and run smoother on Macs + (from Jack Jansen). + +- Fixed "fromstring" and "tostring" for mode "I" images. + +0.3a3 +----- + +The test suite includes 530 individual tests. + +- Eliminated unexpected side-effect in "paste" with matte. "paste" + now works properly also if compiled with "gcc". + +- Adapted to Python 1.5 (build issues only) + +- Fixed the ImageDraw "point" method to draw also the last + point (!). + +- Added "I" and "RGBX" support to Image.new. + +- The plugin path is now properly prepended to the module search + path when a plugin module is imported. + +- Added "draw" method to the ImageWin.Dib class. This is used by + Topaz to print images on Windows printers. + +- "convert" now supports conversions from "P" to "1" and "F". + +- "paste" can now take a colour instead of an image as the first argument. + The colour must match the colour argument given to the new function, and + match the mode of the target image. + +- Fixed "paste" to allow a mask also for mode "F" images. + +- The BMP driver now saves mode "1" images. When loading images, the mode + is set to "L" for 8-bit files with grayscale palettes, and to "P" for + other 8-bit files. + +- The IM driver now reads and saves "1" images (file modes "0 1" or "L 1"). + +- The JPEG and GIF drivers now saves "1" images. For JPEG, the image + is saved as 8-bit grayscale (it will load as mode "L"). For GIF, the + image will be loaded as a "P" image. + +- Fixed a potential buffer overrun in the GIF encoder. + +0.3a2 +----- + +The test suite includes 400 individual tests. + +- Improvements to the test suite revealed a number of minor bugs, which + are all fixed. Note that crop/paste, 32-bit ImageDraw, and ImageFont + are still weak spots in this release. + +- Added "putpalette" method to the Image class. You can use this + to add or modify the palette for "P" and "L" images. If a palette + is added to an "L" image, it is automatically converted to a "P" + image. + +- Fixed ImageDraw to properly handle 32-bit image memories + ("RGB", "RGBA", "CMYK", "F") + +- Fixed "fromstring" and "tostring" not to mess up the mode attribute + in default mode. + +- Changed ImPlatform.h to work on CRAY's (don't have one at home, so I + haven't tried it). The previous version assumed that either "short" + or "int" were 16-bit wide. PIL still won't compile on platforms where + neither "short", "int" nor "long" are 32-bit wide. + +- Added file= and data= keyword arguments to PhotoImage and BitmapImage. + This allows you to use them as drop-in replacements for the corresponding + Tkinter classes. + +- Removed bogus references to the crack coder (ImagingCrack). + +0.3a1 +----- + +- Make sure image is loaded in "tostring". + +- Added floating point packer (native 32-bit floats only). + +0.1b1 to 0.2 (b5) +----------------- + +- Modified "fromstring" and "tostring" methods to use file codecs. + Also added "fromstring" factory method to create an image directly + from data in a string. + +- Added support for 32-bit floating point images (mode "F"). You + can convert between "L" and "F" images, and apply a subset of the + available image processing methods on the "F" image. You can also + read virtually any data format into a floating point image memory; + see the section on "Decoding Floating Point Data" in the handbook + for more information. + +0.2b5 released; on windows only +------------------------------- + +- Fixed the tobitmap() method to work properly for small bitmaps. + +- Added RMS and standard deviation to the ImageStat.Stat class. Also + modified the constructor to take an optional feature mask, and also + to accept either an image or a list containing the histogram data. + +- The BitmapImage code in ImageTk can now use a special bitmap + decoder, which has to be patched into Tk. See the "Tk/pilbitmap.txt" + file for details. If not installed, bitmaps are transferred to Tk as + XBM strings. + +- The PhotoImage code in ImageTk now uses a Tcl command ("PyImagingPaste") + instead of a special image type. This gives somewhat better performance, + and also allows PIL to support transparency. + **WARNING: TKAPPINIT MUST BE MODIFIED** + +- ImageTk now honours the alpha layer in RGBA images. Only fully + transparent pixels are made transparent (that is, the alpha layer + is treated as a mask). To treat the alpha laters as a matte, you + must paste the image on the background before handing it over to + ImageTk. + +- Added McIdas reader (supports 8-bit images only). + +- PIL now preloads drivers for BMP, GIF, JPEG, PPM, and TIFF. As + long as you only load and save these formats, you don't have to + wait for a full scan for drivers. To force scanning, call the + Image.init() function. + +- The "seek" and "tell" methods are now always available, also for + single-frame images. + +- Added optional mask argument to histogram method. The mask may + be an "1" or "L" image with the same size as the original image. + Only pixels where the mask is non-zero are included in the + histogram. + +- The "paste" method now allows you to specify only the lower left + corner (a 2-tuple), instead of the full region (a 4-tuple). + +- Reverted to old plugin scanning model; now scans all directory + names in the path when looking for plugins. + +- Added PIXAR raster support. Only uncompressed ("dumped") RGB + images can currently be read (based on information provided + by Greg Coats). + +- Added FlashPix (FPX) read support. Reads all pixel formats, but + only the highest resolution is read, and the viewing transform is + currently ignored. + +- Made PNG encoding somewhat more efficient in "optimize" mode; a + bug in 0.2b4 didn't enable all predictor filters when optimized + storage were requested. + +- Added Microsoft Image Composer (MIC) read support. When opened, + the first sprite in the file is loaded. You can use the seek method + to load additional sprites from the file. + +- Properly reads "P" and "CMYK" PSD images. + +- "pilconvert" no longer optimizes by default; use the -o option to + make the file as small as possible (at the expense of speed); use + the -q option to set the quality when compressing to JPEG. + +- Fixed "crop" not to drop the palette for "P" images. + +- Added and verified FLC support. + +- Paste with "L" or "RGBA" alpha is now several times faster on most + platforms. + +- Changed Image.new() to initialize the image to black, as described + in the handbook. To get an uninitialized image, use None as the + colour. + +- Fixed the PDF encoder to produce a valid header; Acrobat no longer + complains when you load PDF images created by PIL. + +- PIL only scans fully-qualified directory names in the path when + looking for plugins. + **WARNING: MAY BREAK EXISTING CODE** + +- Faster implementation of "save" used when filename is given, + or when file object has "fileno" and "flush" methods. + +- Don't crash in "crop" if region extends outside the source image. + +- Eliminated a massive memory leak in the "save" function. + +- The GIF decoder doesn't crash if the code size is set to an illegal + value. This could happen since another bug didn't handle local + palettes properly if they didn't have the same size as the + global palette (not very common). + +- Added predictor support (TIFF 6.0 section 14) to the TIFF decoder. + +- Fixed palette and padding problems in BMP driver. Now properly + writes "1", "L", "P" and "RGB" images. + +- Fixed getpixel()/getdata() to return correct pixel values. + +- Added PSD (PhotoShop) read support. Reads both uncompressed + and compressed images of most types. + +- Added GIF write support (writes "uncompressed" GIF files only, + due to unresolvable licensing issues). The "gifmaker.py" script + can be used to create GIF animations. + +- Reads 8-bit "L" and "P" TGA images. Also reads 16-bit "RGB" + images. + +- Added FLI read support. This driver has only been tested + on a few FLI samples. + +- Reads 2-bit and 4-bit PCX images. + +- Added MSP read and write support. Both version 1 and 2 can be + read, but only version 1 (uncompressed) files are written. + +- Fixed a bug in the FLI/FLC identification code that caused the + driver to raise an exception when parsing valid FLI/FLC files. + +- Improved performance when loading file format plugins, and when + opening files. + +- Added GIF animation support, via the "seek" and "tell" methods. + You can use "player.py" to play an animated GIF file. + +- Removed MNG support, since the spec is changing faster than I + can change the code. I've added support for the experimental + ARG format instead. Contact me for more information on this + format. + +- Added keyword options to the "save" method. The following options + are currently supported: + + .. list-table:: + :widths: 25 25 50 + :header-rows: 1 + + * - Format + - Option + - Description + * - JPEG + - optimize + - Minimize output file at the expense of compression speed. + * - JPEG + - progressive + - Enable progressive output. The option value is ignored. + * - JPEG + - quality + - Set compression quality (1-100). The default value is 75. + * - JPEG + - smooth + - Smooth dithered images. Value is strength (1-100). Default is off (0). + * - PNG + - optimize + - Minimize output file at the expense of compression speed. + + Expect more options in future releases. Also note that + file writers silently ignore unknown options. + +- Plugged memory leaks in the PNG and TIFF decoders. + +- Added PNG write support. + +- (internal) RGB unpackers and converters now set the pad byte + to 255 (full opacity). + +- Properly handles the "transparency" property for GIF, PNG + and XPM files. + +- Added a "putalpha" method, allowing you to attach a "1" or "L" + image as the alpha layer to an "RGBA" image. + +- Various improvements to the sample scripts: + + .. list-table:: + :widths: 25 75 + + * - pilconvert + - Carries out some extra tricks in order to make + the resulting file as small as possible. + * - explode + - (NEW) Split an image sequence into individual frames. + * - gifmaker + - (NEW) Convert a sequence file into a GIF animation. + Note that the GIF encoder create "uncompressed" GIF + files, so animations created by this script are + rather large (typically 2-5 times the compressed + sizes). + * - image2py + - (NEW) Convert a single image to a python module. See + comments in this script for details. + * - player + - If multiple images are given on the command line, + they are interpreted as frames in a sequence. The + script assumes that they all have the same size. + Also note that this script now can play FLI/FLC + and GIF animations. + + This player can also execute embedded Python + animation applets (ARG format only). + * - viewer + - Transparent images ("P" with transparency property, + and "RGBA") are superimposed on the standard Tk background. + +- Fixed colour argument to "new". For multilayer images, pass a + tuple: (Red, Green, Blue), (Red, Green, Blue, Alpha), or (Cyan, + Magenta, Yellow, Black). + +- Added XPM (X pixmap) read support. + +0.2b3 +----- + +- Added MNG (multi-image network graphics) read support. "Ming" + is a proposed animation standard, based on the PNG file format. + + You can use the "player" sample script to display some flavours + of this format. The MNG standard is still under development, + as is this driver. More information, including sample files, + can be found at + +- Added a "verify" method to images loaded from file. This method + scans the file for errors, without actually decoding the image + data, and raises a suitable exception if it finds any problems. + Currently implemented for PNG and MNG files only. + +- Added support for interlaced GIF images. + +- Added PNG read support -- if linked with the ZLIB compression library, + PIL reads all kinds of PNG images, except interlaced files. + +- Improved PNG identification support -- doesn't mess up on unknown + chunks, identifies all possible PNG modes, and verifies checksum + on PNG header chunks. + +- Added an experimental reader for placable Windows Meta Files (WMF). + This reader is still very incomplete, but it illustrates how PIL's + drawing capabilities can be used to render vector and metafile + formats. + +- Added restricted drivers for images from Image Tools (grayscale + only) and LabEye/IFUNC (common interchange modes only). + +- Some minor improvements to the sample scripts provided in the + "Scripts" directory. + +- The test images have been moved to the "Images" directory. + +0.2b2 released. 0.2b1 released for Windows only +----------------------------------------------- + +- Fixed filling of complex polygons. The ImageDraw "line" and + "polygon" methods also accept Path objects. + +- The ImageTk "PhotoImage" object can now be constructed directly + from an image. You can also pass the object itself to Tkinter, + instead of using the "image" attribute. Finally, using "paste" + on a displayed image automatically updates the display. + +- The ImageTk "BitmapImage" object allows you to create transparent + overlays from 1-bit images. You can pass the object itself to + Tkinter. The constructor takes the same arguments as the Tkinter + BitmapImage class; use the "foreground" option to set the colour + of the overlay. + +- Added a "putdata" method to the Image class. This can be used to + load a 1-layer image with data from a sequence object or a string. + An optional floating point scale and offset can be used to adjust + the data to fit into the 8-bit pixel range. Also see the "getdata" + method. + +- Added the EXTENT method to the Image "transform" method. This can + be used to quickly crop, stretch, shrink, or mirror a subregion + from another image. + +- Adapted to Python 1.4. + +- Added a project makefile for Visual C++ 4.x. This allows you to + easily build a dynamically linked version of PIL for Windows 95 + and NT. + +- A Tk "booster" patch for Windows is available. It gives dramatic + performance improvements for some displays. Has been tested with + Tk 4.2 only, but is likely to work with Tk 4.1 as well. See the Tk + subdirectory for details. + +- You can now save 1-bit images in the XBM format. In addition, the + Image class now provides a "tobitmap" method which returns a string + containing an XBM representation of the image. Quite handy to use + with Tk. + +- More conversions, including "RGB" to "1" and more. + +0.2a1 +----- + +- Where earlier versions accepted lists, this version accepts arbitrary + Python sequences (including strings, in some cases). A few resource + leaks were plugged in the process. + +- The Image "paste" method now allows the box to extend outside + the target image. The size of the box, the image to be pasted, + and the optional mask must still match. + +- The ImageDraw module now supports filled polygons, outlined and + filled ellipses, and text. Font support is rudimentary, though. + +- The Image "point" method now takes an optional mode argument, + allowing you to convert the image while translating it. Currently, + this can only be used to convert "L" or "P" images to "1" images + (creating thresholded images or "matte" masks). + +- An Image "getpixel" method has been added. For single band images, + it returns the pixel value at a given position as an integer. + For n-band images, it returns an n-tuple of integers. + +- An Image "getdata" method has been added. It returns a sequence + object representing the image as a 1-dimensional array. Only len() + and [] can be used with this sequence. This method returns a + reference to the existing image data, so changes in the image + will be immediately reflected in the sequence object. + +- Fixed alignment problems in the Windows BMP writer. + +- If converting an "RGB" image to "RGB" or "L", you can give a second + argument containing a colour conversion matrix. + +- An Image "getbbox" method has been added. It returns the bounding + box of data in an image, considering the value 0 as background. + +- An Image "offset" method has been added. It returns a new image + where the contents of the image have been offset the given distance + in X and/or Y direction. Data wraps between edges. + +- Saves PDF images. The driver creates a binary PDF 1.1 files, using + JPEG compression for "L", "RGB", and "CMYK" images, and hex encoding + (same as for PostScript) for other formats. + +- The "paste" method now accepts "1" masks. Zero means transparent, + any other pixel value means opaque. This is faster than using an + "L" transparency mask. + +- Properly writes EPS files (and properly prints images to PostScript + printers as well). + +- Reads 4-bit BMP files, as well as 4 and 8-bit Windows ICO and CUR + files. Cursor animations are not supported. + +- Fixed alignment problems in the Sun raster loader. + +- Added "draft" and "thumbnail" methods. The draft method is used + to optimize loading of JPEG and PCD files, the thumbnail method is + used to create a thumbnail representation of an image. + +- Added Windows display support, via the ImageWin class (see the + handbook for details). + +- Added raster conversion for EPS files. This requires GNU or Aladdin + Ghostscript, and probably works on UNIX only. + +- Reads PhotoCD (PCD) images. The base resolution (768x512) can be + read from a PhotoCD file. + +- Eliminated some compiler warnings. Bindings now compile cleanly in C++ + mode. Note that the Imaging library itself must be compiled in C mode. + +- Added "bdf2pil.py", which converts BDF fonts into images with associated + metrics. This is definitely work in progress. For info, see description + in script for details. + +- Fixed a bug in the "ImageEnhance.py" module. + +- Fixed a bug in the netpbm save hack in "GifImagePlugin.py" + +- Fixed 90 and 270 degree rotation of rectangular images. + +- Properly reads 8-bit TIFF palette-color images. + +- Reads plane separated RGB and CMYK TIFF images. + +- Added driver debug mode. This is enabled by setting Image.DEBUG + to a non-zero value. Try the -D option to "pilfile.py" and see what + happens. + +- Don't crash on "atend" constructs in PostScript files. + +- Only the Image module imports _imaging directly. Other modules + should refer to the binding module as "Image.core". + +0.0 to 0.1 (b1) +--------------- + +- A handbook is available (distributed separately). + +- The coordinate system is changed so that (0,0) is now located + in the upper left corner. This is in compliance with ISO 12087 + and 90% of all other image processing and graphics libraries. + +- Modes "1" (bilevel) and "P" (palette) have been introduced. Note + that bilevel images are stored with one byte per pixel. + +- The Image "crop" and "paste" methods now accepts None as the + box argument, to refer to the full image (self, that is). + +- The Image "crop" method now works properly. + +- The Image "point" method is now available. You can use either a + lookup table or a function taking one argument. + +- The Image join function has been renamed to "merge". + +- An Image "composite" function has been added. It is identical + to copy() followed by paste(mask). + +- An Image "eval" function has been added. It is currently identical + to point(function); that is, only a single image can be processed. + +- A set of channel operations has been added. See the "ImageChops" + module, test_chops.py, and the handbook for details. + +- Added the "pilconvert" utility, which converts image files. Note + that the number of output formats are still quite restricted. + +- Added the "pilfile" utility, which quickly identifies image files + (without loading them, in most cases). + +- Added the "pilprint" utility, which prints image files to PostScript + printers. + +- Added a rudimentary version of the "pilview" utility, which is + simple image viewer based on Tk. Only File/Exit and Image/Next + works properly. + +- An interface to Tk has been added. See "Lib/ImageTk.py" and README + for details. + +- An interface to Jack Jansen's Img library has been added (thanks to + Jack). This allows you to read images through the Img extensions file + format handlers. See the file "Lib/ImgExtImagePlugin.py" for details. + +- PostScript printing is provided through the PSDraw module. See the + handbook for details. diff --git a/LICENSE b/LICENSE index 7990a6e57..065014729 100644 --- a/LICENSE +++ b/LICENSE @@ -1,30 +1,30 @@ -The Python Imaging Library (PIL) is - - Copyright © 1997-2011 by Secret Labs AB - Copyright © 1995-2011 by Fredrik Lundh and contributors - -Pillow is the friendly PIL fork. It is - - Copyright © 2010-2024 by Jeffrey A. Clark and contributors - -Like PIL, Pillow is licensed under the open source HPND License: - -By obtaining, using, and/or copying this software and/or its associated -documentation, you agree that you have read, understood, and will comply -with the following terms and conditions: - -Permission to use, copy, modify and distribute this software and its -documentation for any purpose and without fee is hereby granted, -provided that the above copyright notice appears in all copies, and that -both that copyright notice and this permission notice appear in supporting -documentation, and that the name of Secret Labs AB or the author not be -used in advertising or publicity pertaining to distribution of the software -without specific, written prior permission. - -SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS -SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. -IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR BE LIABLE FOR ANY SPECIAL, -INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE -OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -PERFORMANCE OF THIS SOFTWARE. +The Python Imaging Library (PIL) is + + Copyright © 1997-2011 by Secret Labs AB + Copyright © 1995-2011 by Fredrik Lundh and contributors + +Pillow is the friendly PIL fork. It is + + Copyright © 2010-2024 by Jeffrey A. Clark and contributors + +Like PIL, Pillow is licensed under the open source HPND License: + +By obtaining, using, and/or copying this software and/or its associated +documentation, you agree that you have read, understood, and will comply +with the following terms and conditions: + +Permission to use, copy, modify and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appears in all copies, and that +both that copyright notice and this permission notice appear in supporting +documentation, and that the name of Secret Labs AB or the author not be +used in advertising or publicity pertaining to distribution of the software +without specific, written prior permission. + +SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS +SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. +IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR BE LIABLE FOR ANY SPECIAL, +INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in index af25dfd2d..6588d3345 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,34 +1,34 @@ -include *.c -include *.h -include *.in -include *.md -include *.py -include *.rst -include *.sh -include *.toml -include *.txt -include *.yaml -include .flake8 -include LICENSE -include Makefile -include tox.ini -graft Tests -graft src -graft depends -graft winbuild -graft docs -graft _custom_build - -# build/src control detritus -exclude .appveyor.yml -exclude .clang-format -exclude .coveragerc -exclude .editorconfig -exclude .readthedocs.yml -exclude codecov.yml -exclude renovate.json -global-exclude .git* -global-exclude *.pyc -global-exclude *.so -prune .ci -prune wheels +include *.c +include *.h +include *.in +include *.md +include *.py +include *.rst +include *.sh +include *.toml +include *.txt +include *.yaml +include .flake8 +include LICENSE +include Makefile +include tox.ini +graft Tests +graft src +graft depends +graft winbuild +graft docs +graft _custom_build + +# build/src control detritus +exclude .appveyor.yml +exclude .clang-format +exclude .coveragerc +exclude .editorconfig +exclude .readthedocs.yml +exclude codecov.yml +exclude renovate.json +global-exclude .git* +global-exclude *.pyc +global-exclude *.so +prune .ci +prune wheels diff --git a/Makefile b/Makefile index 94f7565d8..e49716dd3 100644 --- a/Makefile +++ b/Makefile @@ -1,125 +1,125 @@ -.DEFAULT_GOAL := help - -.PHONY: clean -clean: - rm src/PIL/*.so || true - rm -r build || true - find . -name __pycache__ | xargs rm -r || true - -.PHONY: coverage -coverage: - python3 -c "import pytest" > /dev/null 2>&1 || python3 -m pip install pytest - python3 -m pytest -qq - rm -r htmlcov || true - python3 -c "import coverage" > /dev/null 2>&1 || python3 -m pip install coverage - python3 -m coverage report - -.PHONY: doc -.PHONY: html -doc html: - python3 -c "import PIL" > /dev/null 2>&1 || python3 -m pip install . - $(MAKE) -C docs html - -.PHONY: htmlview -htmlview: - python3 -c "import PIL" > /dev/null 2>&1 || python3 -m pip install . - $(MAKE) -C docs htmlview - -.PHONY: doccheck -doccheck: - $(MAKE) doc -# Don't make our tests rely on the links in the docs being up every single build. -# We don't control them. But do check, and update them to the target of their redirects. - $(MAKE) -C docs linkcheck || true - -.PHONY: docserve -docserve: - cd docs/_build/html && python3 -m http.server 2> /dev/null& - -.PHONY: help -help: - @echo "Welcome to Pillow development. Please use \`make \` where is one of" - @echo " clean remove build products" - @echo " coverage run coverage test (in progress)" - @echo " doc make HTML docs" - @echo " docserve run an HTTP server on the docs directory" - @echo " html make HTML docs" - @echo " htmlview open the index page built by the html target in your browser" - @echo " install make and install" - @echo " install-coverage make and install with C coverage" - @echo " lint run the lint checks" - @echo " lint-fix run Ruff to (mostly) fix lint issues" - @echo " release-test run code and package tests before release" - @echo " test run tests on installed Pillow" - -.PHONY: install -install: - python3 -m pip -v install . - python3 selftest.py - -.PHONY: install-coverage -install-coverage: - CFLAGS="-coverage -Werror=implicit-function-declaration" python3 -m pip -v install . - python3 selftest.py - -.PHONY: debug -debug: -# make a debug version if we don't have a -dbg python. Leaves in symbols -# for our stuff, kills optimization, and redirects to dev null so we -# see any build failures. - make clean > /dev/null - CFLAGS='-g -O0' python3 -m pip -v install . > /dev/null - -.PHONY: release-test -release-test: - python3 Tests/check_release_notes.py - python3 -m pip install -e .[tests] - python3 selftest.py - python3 -m pytest Tests - python3 -m pip install . - python3 -m pytest -qq - python3 -m check_manifest - python3 -m pyroma . - $(MAKE) readme - -.PHONY: sdist -sdist: - python3 -m build --help > /dev/null 2>&1 || python3 -m pip install build - python3 -m build --sdist - python3 -m twine --help > /dev/null 2>&1 || python3 -m pip install twine - python3 -m twine check --strict dist/* - -.PHONY: test -test: - python3 -c "import pytest" > /dev/null 2>&1 || python3 -m pip install pytest - python3 -m pytest -qq - -.PHONY: valgrind -valgrind: - python3 -c "import pytest_valgrind" > /dev/null 2>&1 || python3 -m pip install pytest-valgrind - PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp --leak-check=no \ - --log-file=/tmp/valgrind-output \ - python3 -m pytest --no-memcheck -vv --valgrind --valgrind-log=/tmp/valgrind-output - -.PHONY: readme -readme: - python3 -c "import markdown2" > /dev/null 2>&1 || python3 -m pip install markdown2 - python3 -m markdown2 README.md > .long-description.html && open .long-description.html - - -.PHONY: lint -lint: - python3 -c "import tox" > /dev/null 2>&1 || python3 -m pip install tox - python3 -m tox -e lint - -.PHONY: lint-fix -lint-fix: - python3 -c "import black" > /dev/null 2>&1 || python3 -m pip install black - python3 -m black . - python3 -c "import ruff" > /dev/null 2>&1 || python3 -m pip install ruff - python3 -m ruff --fix . - -.PHONY: mypy -mypy: - python3 -c "import tox" > /dev/null 2>&1 || python3 -m pip install tox - python3 -m tox -e mypy +.DEFAULT_GOAL := help + +.PHONY: clean +clean: + rm src/PIL/*.so || true + rm -r build || true + find . -name __pycache__ | xargs rm -r || true + +.PHONY: coverage +coverage: + python3 -c "import pytest" > /dev/null 2>&1 || python3 -m pip install pytest + python3 -m pytest -qq + rm -r htmlcov || true + python3 -c "import coverage" > /dev/null 2>&1 || python3 -m pip install coverage + python3 -m coverage report + +.PHONY: doc +.PHONY: html +doc html: + python3 -c "import PIL" > /dev/null 2>&1 || python3 -m pip install . + $(MAKE) -C docs html + +.PHONY: htmlview +htmlview: + python3 -c "import PIL" > /dev/null 2>&1 || python3 -m pip install . + $(MAKE) -C docs htmlview + +.PHONY: doccheck +doccheck: + $(MAKE) doc +# Don't make our tests rely on the links in the docs being up every single build. +# We don't control them. But do check, and update them to the target of their redirects. + $(MAKE) -C docs linkcheck || true + +.PHONY: docserve +docserve: + cd docs/_build/html && python3 -m http.server 2> /dev/null& + +.PHONY: help +help: + @echo "Welcome to Pillow development. Please use \`make \` where is one of" + @echo " clean remove build products" + @echo " coverage run coverage test (in progress)" + @echo " doc make HTML docs" + @echo " docserve run an HTTP server on the docs directory" + @echo " html make HTML docs" + @echo " htmlview open the index page built by the html target in your browser" + @echo " install make and install" + @echo " install-coverage make and install with C coverage" + @echo " lint run the lint checks" + @echo " lint-fix run Ruff to (mostly) fix lint issues" + @echo " release-test run code and package tests before release" + @echo " test run tests on installed Pillow" + +.PHONY: install +install: + python3 -m pip -v install . + python3 selftest.py + +.PHONY: install-coverage +install-coverage: + CFLAGS="-coverage -Werror=implicit-function-declaration" python3 -m pip -v install . + python3 selftest.py + +.PHONY: debug +debug: +# make a debug version if we don't have a -dbg python. Leaves in symbols +# for our stuff, kills optimization, and redirects to dev null so we +# see any build failures. + make clean > /dev/null + CFLAGS='-g -O0' python3 -m pip -v install . > /dev/null + +.PHONY: release-test +release-test: + python3 Tests/check_release_notes.py + python3 -m pip install -e .[tests] + python3 selftest.py + python3 -m pytest Tests + python3 -m pip install . + python3 -m pytest -qq + python3 -m check_manifest + python3 -m pyroma . + $(MAKE) readme + +.PHONY: sdist +sdist: + python3 -m build --help > /dev/null 2>&1 || python3 -m pip install build + python3 -m build --sdist + python3 -m twine --help > /dev/null 2>&1 || python3 -m pip install twine + python3 -m twine check --strict dist/* + +.PHONY: test +test: + python3 -c "import pytest" > /dev/null 2>&1 || python3 -m pip install pytest + python3 -m pytest -qq + +.PHONY: valgrind +valgrind: + python3 -c "import pytest_valgrind" > /dev/null 2>&1 || python3 -m pip install pytest-valgrind + PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp --leak-check=no \ + --log-file=/tmp/valgrind-output \ + python3 -m pytest --no-memcheck -vv --valgrind --valgrind-log=/tmp/valgrind-output + +.PHONY: readme +readme: + python3 -c "import markdown2" > /dev/null 2>&1 || python3 -m pip install markdown2 + python3 -m markdown2 README.md > .long-description.html && open .long-description.html + + +.PHONY: lint +lint: + python3 -c "import tox" > /dev/null 2>&1 || python3 -m pip install tox + python3 -m tox -e lint + +.PHONY: lint-fix +lint-fix: + python3 -c "import black" > /dev/null 2>&1 || python3 -m pip install black + python3 -m black . + python3 -c "import ruff" > /dev/null 2>&1 || python3 -m pip install ruff + python3 -m ruff --fix . + +.PHONY: mypy +mypy: + python3 -c "import tox" > /dev/null 2>&1 || python3 -m pip install tox + python3 -m tox -e mypy diff --git a/README.md b/README.md index b4c6d2987..1010c4705 100644 --- a/README.md +++ b/README.md @@ -1,115 +1,115 @@ -

- Pillow logo -

- -# Pillow - -## Python Imaging Library (Fork) - -Pillow is the friendly PIL fork by [Jeffrey A. Clark and -contributors](https://github.com/python-pillow/Pillow/graphs/contributors). -PIL is the Python Imaging Library by Fredrik Lundh and contributors. -As of 2019, Pillow development is -[supported by Tidelift](https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=readme&utm_campaign=enterprise). - - - - - - - - - - - - - - - - - - -
docs - Documentation Status -
tests - GitHub Actions build status (Lint) - GitHub Actions build status (Test Linux and macOS) - GitHub Actions build status (Test Windows) - GitHub Actions build status (Test MinGW) - GitHub Actions build status (Test Cygwin) - GitHub Actions build status (Test Docker) - AppVeyor CI build status (Windows) - GitHub Actions build status (Wheels) - Code coverage - Fuzzing Status -
package - Zenodo - Tidelift - Newest PyPI version - Number of PyPI downloads - OpenSSF Best Practices -
social - Join the chat at https://gitter.im/python-pillow/Pillow - Follow on https://fosstodon.org/@pillow -
- -## Overview - -The Python Imaging Library adds image processing capabilities to your Python interpreter. - -This library provides extensive file format support, an efficient internal representation, and fairly powerful image processing capabilities. - -The core image library is designed for fast access to data stored in a few basic pixel formats. It should provide a solid foundation for a general image processing tool. - -## More Information - -- [Documentation](https://pillow.readthedocs.io/) - - [Installation](https://pillow.readthedocs.io/en/latest/installation/basic-installation.html) - - [Handbook](https://pillow.readthedocs.io/en/latest/handbook/index.html) -- [Contribute](https://github.com/python-pillow/Pillow/blob/main/.github/CONTRIBUTING.md) - - [Issues](https://github.com/python-pillow/Pillow/issues) - - [Pull requests](https://github.com/python-pillow/Pillow/pulls) -- [Release notes](https://pillow.readthedocs.io/en/stable/releasenotes/index.html) -- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) - - [Pre-fork](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst#pre-fork) - -## Report a Vulnerability - -To report a security vulnerability, please follow the procedure described in the [Tidelift security policy](https://tidelift.com/docs/security). +

+ Pillow logo +

+ +# Pillow + +## Python Imaging Library (Fork) + +Pillow is the friendly PIL fork by [Jeffrey A. Clark and +contributors](https://github.com/python-pillow/Pillow/graphs/contributors). +PIL is the Python Imaging Library by Fredrik Lundh and contributors. +As of 2019, Pillow development is +[supported by Tidelift](https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=readme&utm_campaign=enterprise). + + + + + + + + + + + + + + + + + + +
docs + Documentation Status +
tests + GitHub Actions build status (Lint) + GitHub Actions build status (Test Linux and macOS) + GitHub Actions build status (Test Windows) + GitHub Actions build status (Test MinGW) + GitHub Actions build status (Test Cygwin) + GitHub Actions build status (Test Docker) + AppVeyor CI build status (Windows) + GitHub Actions build status (Wheels) + Code coverage + Fuzzing Status +
package + Zenodo + Tidelift + Newest PyPI version + Number of PyPI downloads + OpenSSF Best Practices +
social + Join the chat at https://gitter.im/python-pillow/Pillow + Follow on https://fosstodon.org/@pillow +
+ +## Overview + +The Python Imaging Library adds image processing capabilities to your Python interpreter. + +This library provides extensive file format support, an efficient internal representation, and fairly powerful image processing capabilities. + +The core image library is designed for fast access to data stored in a few basic pixel formats. It should provide a solid foundation for a general image processing tool. + +## More Information + +- [Documentation](https://pillow.readthedocs.io/) + - [Installation](https://pillow.readthedocs.io/en/latest/installation/basic-installation.html) + - [Handbook](https://pillow.readthedocs.io/en/latest/handbook/index.html) +- [Contribute](https://github.com/python-pillow/Pillow/blob/main/.github/CONTRIBUTING.md) + - [Issues](https://github.com/python-pillow/Pillow/issues) + - [Pull requests](https://github.com/python-pillow/Pillow/pulls) +- [Release notes](https://pillow.readthedocs.io/en/stable/releasenotes/index.html) +- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) + - [Pre-fork](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst#pre-fork) + +## Report a Vulnerability + +To report a security vulnerability, please follow the procedure described in the [Tidelift security policy](https://tidelift.com/docs/security). diff --git a/RELEASING.md b/RELEASING.md index 9e6ec5dd4..991b138c2 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,102 +1,102 @@ -# Release Checklist - -See https://pillow.readthedocs.io/en/stable/releasenotes/versioning.html for -information about how the version numbers line up with releases. - -## Main Release - -Released quarterly on 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 - -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`. -* [ ] 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.: - ```bash - git tag 5.2.1 - git push --tags - ``` -* [ ] Create and check source distribution: - ```bash - make sdist - ``` -* [ ] 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) and then: - ```bash - git push - ``` - -## Embargoed Release - -Released as needed privately to individual vendors for critical security-related bug fixes. - -* [ ] Prepare patch for all versions that will get a fix. Test against local installations. -* [ ] Commit against `main`, cherry pick to affected release branches. -* [ ] Run local test matrix on each release & Python version. -* [ ] Privately send to distros. -* [ ] Run pre-release check via `make release-test` -* [ ] Amend any commits with the CVE # -* [ ] On release date, tag and push to GitHub. - ```bash - git checkout 2.5.x - git tag 2.5.3 - git push origin --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) and then: - ```bash - git push origin 2.5.x - ``` - -## 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]] - ``` +# Release Checklist + +See https://pillow.readthedocs.io/en/stable/releasenotes/versioning.html for +information about how the version numbers line up with releases. + +## Main Release + +Released quarterly on 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 + +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`. +* [ ] 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.: + ```bash + git tag 5.2.1 + git push --tags + ``` +* [ ] Create and check source distribution: + ```bash + make sdist + ``` +* [ ] 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) and then: + ```bash + git push + ``` + +## Embargoed Release + +Released as needed privately to individual vendors for critical security-related bug fixes. + +* [ ] Prepare patch for all versions that will get a fix. Test against local installations. +* [ ] Commit against `main`, cherry pick to affected release branches. +* [ ] Run local test matrix on each release & Python version. +* [ ] Privately send to distros. +* [ ] Run pre-release check via `make release-test` +* [ ] Amend any commits with the CVE # +* [ ] On release date, tag and push to GitHub. + ```bash + git checkout 2.5.x + git tag 2.5.3 + git push origin --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) and then: + ```bash + git push origin 2.5.x + ``` + +## 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/Tests/32bit_segfault_check.py b/Tests/32bit_segfault_check.py index 06ed2ed2f..51b6b98b5 100755 --- a/Tests/32bit_segfault_check.py +++ b/Tests/32bit_segfault_check.py @@ -1,9 +1,9 @@ -#!/usr/bin/env python3 -from __future__ import annotations - -import sys - -from PIL import Image - -if sys.maxsize < 2**32: - im = Image.new("L", (999999, 999999), 0) +#!/usr/bin/env python3 +from __future__ import annotations + +import sys + +from PIL import Image + +if sys.maxsize < 2**32: + im = Image.new("L", (999999, 999999), 0) diff --git a/Tests/README.rst b/Tests/README.rst index 2d014e5a4..ee9c55d64 100644 --- a/Tests/README.rst +++ b/Tests/README.rst @@ -1,32 +1,32 @@ -Pillow Tests -============ - -Test scripts are named ``test_xxx.py``. Helper classes and functions can be found in ``helper.py``. - -Dependencies ------------- - -Install:: - - python3 -m pip install pytest pytest-cov pytest-timeout - -Execution ---------- - -To run an individual test:: - - pytest Tests/test_image.py - -Or:: - - pytest -k test_image.py - -Run all the tests from the root of the Pillow source distribution:: - - pytest - -Or with coverage:: - - pytest --cov PIL --cov Tests --cov-report term - coverage html - open htmlcov/index.html +Pillow Tests +============ + +Test scripts are named ``test_xxx.py``. Helper classes and functions can be found in ``helper.py``. + +Dependencies +------------ + +Install:: + + python3 -m pip install pytest pytest-cov pytest-timeout + +Execution +--------- + +To run an individual test:: + + pytest Tests/test_image.py + +Or:: + + pytest -k test_image.py + +Run all the tests from the root of the Pillow source distribution:: + + pytest + +Or with coverage:: + + pytest --cov PIL --cov Tests --cov-report term + coverage html + open htmlcov/index.html diff --git a/Tests/bench_cffi_access.py b/Tests/bench_cffi_access.py index c7d105836..29423f1f3 100644 --- a/Tests/bench_cffi_access.py +++ b/Tests/bench_cffi_access.py @@ -1,54 +1,54 @@ -from __future__ import annotations - -import time - -from PIL import PyAccess - -from .helper import hopper - -# Not running this test by default. No DOS against CI. - - -def iterate_get(size, access) -> None: - (w, h) = size - for x in range(w): - for y in range(h): - access[(x, y)] - - -def iterate_set(size, access) -> None: - (w, h) = size - for x in range(w): - for y in range(h): - access[(x, y)] = (x % 256, y % 256, 0) - - -def timer(func, label, *args) -> None: - iterations = 5000 - starttime = time.time() - for x in range(iterations): - func(*args) - if time.time() - starttime > 10: - break - endtime = time.time() - print( - f"{label}: completed {x + 1} iterations in {endtime - starttime:.4f}s, " - f"{(endtime - starttime) / (x + 1.0):.6f}s per iteration" - ) - - -def test_direct() -> None: - im = hopper() - im.load() - # im = Image.new("RGB", (2000, 2000), (1, 3, 2)) - caccess = im.im.pixel_access(False) - access = PyAccess.new(im, False) - - assert access is not None - assert caccess[(0, 0)] == access[(0, 0)] - - print(f"Size: {im.width}x{im.height}") - timer(iterate_get, "PyAccess - get", im.size, access) - timer(iterate_set, "PyAccess - set", im.size, access) - timer(iterate_get, "C-api - get", im.size, caccess) - timer(iterate_set, "C-api - set", im.size, caccess) +from __future__ import annotations + +import time + +from PIL import PyAccess + +from .helper import hopper + +# Not running this test by default. No DOS against CI. + + +def iterate_get(size, access) -> None: + (w, h) = size + for x in range(w): + for y in range(h): + access[(x, y)] + + +def iterate_set(size, access) -> None: + (w, h) = size + for x in range(w): + for y in range(h): + access[(x, y)] = (x % 256, y % 256, 0) + + +def timer(func, label, *args) -> None: + iterations = 5000 + starttime = time.time() + for x in range(iterations): + func(*args) + if time.time() - starttime > 10: + break + endtime = time.time() + print( + f"{label}: completed {x + 1} iterations in {endtime - starttime:.4f}s, " + f"{(endtime - starttime) / (x + 1.0):.6f}s per iteration" + ) + + +def test_direct() -> None: + im = hopper() + im.load() + # im = Image.new("RGB", (2000, 2000), (1, 3, 2)) + caccess = im.im.pixel_access(False) + access = PyAccess.new(im, False) + + assert access is not None + assert caccess[(0, 0)] == access[(0, 0)] + + print(f"Size: {im.width}x{im.height}") + timer(iterate_get, "PyAccess - get", im.size, access) + timer(iterate_set, "PyAccess - set", im.size, access) + timer(iterate_get, "C-api - get", im.size, caccess) + timer(iterate_set, "C-api - set", im.size, caccess) diff --git a/Tests/check_fli_oob.py b/Tests/check_fli_oob.py index e0057a2c2..8254d7e2b 100644 --- a/Tests/check_fli_oob.py +++ b/Tests/check_fli_oob.py @@ -1,68 +1,68 @@ -from __future__ import annotations - -from PIL import Image - -repro_ss2 = ( - "images/fli_oob/06r/06r00.fli", - "images/fli_oob/06r/others/06r01.fli", - "images/fli_oob/06r/others/06r02.fli", - "images/fli_oob/06r/others/06r03.fli", - "images/fli_oob/06r/others/06r04.fli", -) - -repro_lc = ( - "images/fli_oob/05r/05r00.fli", - "images/fli_oob/05r/others/05r03.fli", - "images/fli_oob/05r/others/05r06.fli", - "images/fli_oob/05r/others/05r05.fli", - "images/fli_oob/05r/others/05r01.fli", - "images/fli_oob/05r/others/05r04.fli", - "images/fli_oob/05r/others/05r02.fli", - "images/fli_oob/05r/others/05r07.fli", - "images/fli_oob/patch0/000000", - "images/fli_oob/patch0/000001", - "images/fli_oob/patch0/000002", - "images/fli_oob/patch0/000003", -) - - -repro_advance = ( - "images/fli_oob/03r/03r00.fli", - "images/fli_oob/03r/others/03r01.fli", - "images/fli_oob/03r/others/03r09.fli", - "images/fli_oob/03r/others/03r11.fli", - "images/fli_oob/03r/others/03r05.fli", - "images/fli_oob/03r/others/03r10.fli", - "images/fli_oob/03r/others/03r06.fli", - "images/fli_oob/03r/others/03r08.fli", - "images/fli_oob/03r/others/03r03.fli", - "images/fli_oob/03r/others/03r07.fli", - "images/fli_oob/03r/others/03r02.fli", - "images/fli_oob/03r/others/03r04.fli", -) - -repro_brun = ( - "images/fli_oob/04r/initial.fli", - "images/fli_oob/04r/others/04r02.fli", - "images/fli_oob/04r/others/04r05.fli", - "images/fli_oob/04r/others/04r04.fli", - "images/fli_oob/04r/others/04r03.fli", - "images/fli_oob/04r/others/04r01.fli", - "images/fli_oob/04r/04r00.fli", -) - -repro_copy = ( - "images/fli_oob/02r/others/02r02.fli", - "images/fli_oob/02r/others/02r04.fli", - "images/fli_oob/02r/others/02r03.fli", - "images/fli_oob/02r/others/02r01.fli", - "images/fli_oob/02r/02r00.fli", -) - - -for path in repro_ss2 + repro_lc + repro_advance + repro_brun + repro_copy: - with Image.open(path) as im: - try: - im.load() - except Exception as msg: - print(msg) +from __future__ import annotations + +from PIL import Image + +repro_ss2 = ( + "images/fli_oob/06r/06r00.fli", + "images/fli_oob/06r/others/06r01.fli", + "images/fli_oob/06r/others/06r02.fli", + "images/fli_oob/06r/others/06r03.fli", + "images/fli_oob/06r/others/06r04.fli", +) + +repro_lc = ( + "images/fli_oob/05r/05r00.fli", + "images/fli_oob/05r/others/05r03.fli", + "images/fli_oob/05r/others/05r06.fli", + "images/fli_oob/05r/others/05r05.fli", + "images/fli_oob/05r/others/05r01.fli", + "images/fli_oob/05r/others/05r04.fli", + "images/fli_oob/05r/others/05r02.fli", + "images/fli_oob/05r/others/05r07.fli", + "images/fli_oob/patch0/000000", + "images/fli_oob/patch0/000001", + "images/fli_oob/patch0/000002", + "images/fli_oob/patch0/000003", +) + + +repro_advance = ( + "images/fli_oob/03r/03r00.fli", + "images/fli_oob/03r/others/03r01.fli", + "images/fli_oob/03r/others/03r09.fli", + "images/fli_oob/03r/others/03r11.fli", + "images/fli_oob/03r/others/03r05.fli", + "images/fli_oob/03r/others/03r10.fli", + "images/fli_oob/03r/others/03r06.fli", + "images/fli_oob/03r/others/03r08.fli", + "images/fli_oob/03r/others/03r03.fli", + "images/fli_oob/03r/others/03r07.fli", + "images/fli_oob/03r/others/03r02.fli", + "images/fli_oob/03r/others/03r04.fli", +) + +repro_brun = ( + "images/fli_oob/04r/initial.fli", + "images/fli_oob/04r/others/04r02.fli", + "images/fli_oob/04r/others/04r05.fli", + "images/fli_oob/04r/others/04r04.fli", + "images/fli_oob/04r/others/04r03.fli", + "images/fli_oob/04r/others/04r01.fli", + "images/fli_oob/04r/04r00.fli", +) + +repro_copy = ( + "images/fli_oob/02r/others/02r02.fli", + "images/fli_oob/02r/others/02r04.fli", + "images/fli_oob/02r/others/02r03.fli", + "images/fli_oob/02r/others/02r01.fli", + "images/fli_oob/02r/02r00.fli", +) + + +for path in repro_ss2 + repro_lc + repro_advance + repro_brun + repro_copy: + with Image.open(path) as im: + try: + im.load() + except Exception as msg: + print(msg) diff --git a/Tests/check_fli_overflow.py b/Tests/check_fli_overflow.py index 5c89efc76..9951c302a 100644 --- a/Tests/check_fli_overflow.py +++ b/Tests/check_fli_overflow.py @@ -1,11 +1,11 @@ -from __future__ import annotations - -from PIL import Image - -TEST_FILE = "Tests/images/fli_overflow.fli" - - -def test_fli_overflow() -> None: - # this should not crash with a malloc error or access violation - with Image.open(TEST_FILE) as im: - im.load() +from __future__ import annotations + +from PIL import Image + +TEST_FILE = "Tests/images/fli_overflow.fli" + + +def test_fli_overflow() -> None: + # this should not crash with a malloc error or access violation + with Image.open(TEST_FILE) as im: + im.load() diff --git a/Tests/check_icns_dos.py b/Tests/check_icns_dos.py index ac6be4869..1b9fe0784 100644 --- a/Tests/check_icns_dos.py +++ b/Tests/check_icns_dos.py @@ -1,10 +1,10 @@ -# Tests potential DOS of IcnsImagePlugin with 0 length block. -# Run from anywhere that PIL is importable. -from __future__ import annotations - -from io import BytesIO - -from PIL import Image - -with Image.open(BytesIO(b"icns\x00\x00\x00\x10hang\x00\x00\x00\x00")): - pass +# Tests potential DOS of IcnsImagePlugin with 0 length block. +# Run from anywhere that PIL is importable. +from __future__ import annotations + +from io import BytesIO + +from PIL import Image + +with Image.open(BytesIO(b"icns\x00\x00\x00\x10hang\x00\x00\x00\x00")): + pass diff --git a/Tests/check_imaging_leaks.py b/Tests/check_imaging_leaks.py index 231789ca0..7310aebf6 100755 --- a/Tests/check_imaging_leaks.py +++ b/Tests/check_imaging_leaks.py @@ -1,55 +1,55 @@ -#!/usr/bin/env python3 -from __future__ import annotations - -from typing import Any, Callable - -import pytest - -from PIL import Image - -from .helper import is_win32 - -min_iterations = 100 -max_iterations = 10000 - -pytestmark = pytest.mark.skipif(is_win32(), reason="requires Unix or macOS") - - -def _get_mem_usage() -> float: - from resource import RUSAGE_SELF, getpagesize, getrusage - - mem = getrusage(RUSAGE_SELF).ru_maxrss - return mem * getpagesize() / 1024 / 1024 - - -def _test_leak( - min_iterations: int, - max_iterations: int, - fn: Callable[..., Image.Image | None], - *args: Any, -) -> None: - mem_limit = None - for i in range(max_iterations): - fn(*args) - mem = _get_mem_usage() - if i < min_iterations: - mem_limit = mem + 1 - continue - msg = f"memory usage limit exceeded after {i + 1} iterations" - assert mem_limit is not None - assert mem <= mem_limit, msg - - -def test_leak_putdata() -> None: - im = Image.new("RGB", (25, 25)) - _test_leak(min_iterations, max_iterations, im.putdata, im.getdata()) - - -def test_leak_getlist() -> None: - im = Image.new("P", (25, 25)) - _test_leak( - min_iterations, - max_iterations, - # Pass a new list at each iteration. - lambda: im.point(range(256)), - ) +#!/usr/bin/env python3 +from __future__ import annotations + +from typing import Any, Callable + +import pytest + +from PIL import Image + +from .helper import is_win32 + +min_iterations = 100 +max_iterations = 10000 + +pytestmark = pytest.mark.skipif(is_win32(), reason="requires Unix or macOS") + + +def _get_mem_usage() -> float: + from resource import RUSAGE_SELF, getpagesize, getrusage + + mem = getrusage(RUSAGE_SELF).ru_maxrss + return mem * getpagesize() / 1024 / 1024 + + +def _test_leak( + min_iterations: int, + max_iterations: int, + fn: Callable[..., Image.Image | None], + *args: Any, +) -> None: + mem_limit = None + for i in range(max_iterations): + fn(*args) + mem = _get_mem_usage() + if i < min_iterations: + mem_limit = mem + 1 + continue + msg = f"memory usage limit exceeded after {i + 1} iterations" + assert mem_limit is not None + assert mem <= mem_limit, msg + + +def test_leak_putdata() -> None: + im = Image.new("RGB", (25, 25)) + _test_leak(min_iterations, max_iterations, im.putdata, im.getdata()) + + +def test_leak_getlist() -> None: + im = Image.new("P", (25, 25)) + _test_leak( + min_iterations, + max_iterations, + # Pass a new list at each iteration. + lambda: im.point(range(256)), + ) diff --git a/Tests/check_j2k_dos.py b/Tests/check_j2k_dos.py index 2c63c3402..1da62f390 100644 --- a/Tests/check_j2k_dos.py +++ b/Tests/check_j2k_dos.py @@ -1,12 +1,12 @@ -# Tests potential DOS of Jpeg2kImagePlugin with 0 length block. -# Run from anywhere that PIL is importable. -from __future__ import annotations - -from io import BytesIO - -from PIL import Image - -with Image.open( - BytesIO(b"\x00\x00\x00\x0cjP\x20\x20\x0d\x0a\x87\x0a\x00\x00\x00\x00hang") -): - pass +# Tests potential DOS of Jpeg2kImagePlugin with 0 length block. +# Run from anywhere that PIL is importable. +from __future__ import annotations + +from io import BytesIO + +from PIL import Image + +with Image.open( + BytesIO(b"\x00\x00\x00\x0cjP\x20\x20\x0d\x0a\x87\x0a\x00\x00\x00\x00hang") +): + pass diff --git a/Tests/check_j2k_leaks.py b/Tests/check_j2k_leaks.py index bbe35b591..f4e4e5488 100644 --- a/Tests/check_j2k_leaks.py +++ b/Tests/check_j2k_leaks.py @@ -1,44 +1,44 @@ -from __future__ import annotations - -from io import BytesIO - -import pytest - -from PIL import Image - -from .helper import is_win32, skip_unless_feature - -# Limits for testing the leak -mem_limit = 1024 * 1048576 -stack_size = 8 * 1048576 -iterations = int((mem_limit / stack_size) * 2) -test_file = "Tests/images/rgb_trns_ycbc.jp2" - -pytestmark = [ - pytest.mark.skipif(is_win32(), reason="requires Unix or macOS"), - skip_unless_feature("jpg_2000"), -] - - -def test_leak_load() -> None: - from resource import RLIMIT_AS, RLIMIT_STACK, setrlimit - - setrlimit(RLIMIT_STACK, (stack_size, stack_size)) - setrlimit(RLIMIT_AS, (mem_limit, mem_limit)) - for _ in range(iterations): - with Image.open(test_file) as im: - im.load() - - -def test_leak_save() -> None: - from resource import RLIMIT_AS, RLIMIT_STACK, setrlimit - - setrlimit(RLIMIT_STACK, (stack_size, stack_size)) - setrlimit(RLIMIT_AS, (mem_limit, mem_limit)) - for _ in range(iterations): - with Image.open(test_file) as im: - im.load() - test_output = BytesIO() - im.save(test_output, "JPEG2000") - test_output.seek(0) - test_output.read() +from __future__ import annotations + +from io import BytesIO + +import pytest + +from PIL import Image + +from .helper import is_win32, skip_unless_feature + +# Limits for testing the leak +mem_limit = 1024 * 1048576 +stack_size = 8 * 1048576 +iterations = int((mem_limit / stack_size) * 2) +test_file = "Tests/images/rgb_trns_ycbc.jp2" + +pytestmark = [ + pytest.mark.skipif(is_win32(), reason="requires Unix or macOS"), + skip_unless_feature("jpg_2000"), +] + + +def test_leak_load() -> None: + from resource import RLIMIT_AS, RLIMIT_STACK, setrlimit + + setrlimit(RLIMIT_STACK, (stack_size, stack_size)) + setrlimit(RLIMIT_AS, (mem_limit, mem_limit)) + for _ in range(iterations): + with Image.open(test_file) as im: + im.load() + + +def test_leak_save() -> None: + from resource import RLIMIT_AS, RLIMIT_STACK, setrlimit + + setrlimit(RLIMIT_STACK, (stack_size, stack_size)) + setrlimit(RLIMIT_AS, (mem_limit, mem_limit)) + for _ in range(iterations): + with Image.open(test_file) as im: + im.load() + test_output = BytesIO() + im.save(test_output, "JPEG2000") + test_output.seek(0) + test_output.read() diff --git a/Tests/check_j2k_overflow.py b/Tests/check_j2k_overflow.py index dbdd5a4f5..e185e8ea1 100644 --- a/Tests/check_j2k_overflow.py +++ b/Tests/check_j2k_overflow.py @@ -1,14 +1,14 @@ -from __future__ import annotations - -from pathlib import Path - -import pytest - -from PIL import Image - - -def test_j2k_overflow(tmp_path: Path) -> None: - im = Image.new("RGBA", (1024, 131584)) - target = str(tmp_path / "temp.jpc") - with pytest.raises(OSError): - im.save(target) +from __future__ import annotations + +from pathlib import Path + +import pytest + +from PIL import Image + + +def test_j2k_overflow(tmp_path: Path) -> None: + im = Image.new("RGBA", (1024, 131584)) + target = str(tmp_path / "temp.jpc") + with pytest.raises(OSError): + im.save(target) diff --git a/Tests/check_jp2_overflow.py b/Tests/check_jp2_overflow.py index 954d68bf7..fad2fce04 100644 --- a/Tests/check_jp2_overflow.py +++ b/Tests/check_jp2_overflow.py @@ -1,24 +1,24 @@ -# Reproductions/tests for OOB read errors in FliDecode.c - -# When run in python, all of these images should fail for -# one reason or another, either as a buffer overrun, -# unrecognized datastream, or truncated image file. -# There shouldn't be any segfaults. -# -# if run like -# `valgrind --tool=memcheck python check_jp2_overflow.py 2>&1 | grep Decode.c` -# the output should be empty. There may be python issues -# in the valgrind especially if run in a debug python -# version. -from __future__ import annotations - -from PIL import Image - -repro = ("00r0_gray_l.jp2", "00r1_graya_la.jp2") - -for path in repro: - with Image.open(path) as im: - try: - im.load() - except Exception as msg: - print(msg) +# Reproductions/tests for OOB read errors in FliDecode.c + +# When run in python, all of these images should fail for +# one reason or another, either as a buffer overrun, +# unrecognized datastream, or truncated image file. +# There shouldn't be any segfaults. +# +# if run like +# `valgrind --tool=memcheck python check_jp2_overflow.py 2>&1 | grep Decode.c` +# the output should be empty. There may be python issues +# in the valgrind especially if run in a debug python +# version. +from __future__ import annotations + +from PIL import Image + +repro = ("00r0_gray_l.jp2", "00r1_graya_la.jp2") + +for path in repro: + with Image.open(path) as im: + try: + im.load() + except Exception as msg: + print(msg) diff --git a/Tests/check_jpeg_leaks.py b/Tests/check_jpeg_leaks.py index 5f290c6cd..ae1c10ecd 100644 --- a/Tests/check_jpeg_leaks.py +++ b/Tests/check_jpeg_leaks.py @@ -1,214 +1,214 @@ -from __future__ import annotations - -from io import BytesIO - -import pytest - -from .helper import hopper, is_win32 - -iterations = 5000 - - -""" -When run on a system without the jpeg leak fixes, -the valgrind runs look like this. - -valgrind --tool=massif python test-installed.py -s -v Tests/check_jpeg_leaks.py - -""" - - -pytestmark = pytest.mark.skipif(is_win32(), reason="requires Unix or macOS") - -""" -pre patch: - - MB -31.62^ : - | @:@:@:@#:: - | @:@:@@:@:@:@:@:@#:: - | ::::::::@:@:@@:@:@:@:@:@#:: - | :::::@::::::: ::::@:@:@@:@:@:@:@:@#:: - | @:::::::::::::@: : ::: ::::@:@:@@:@:@:@:@:@#:: - | ::::::@::::@:::::::::::::@: : ::: ::::@:@:@@:@:@:@:@:@#:: - | ::::::: : :@: : @:::::::::::::@: : ::: ::::@:@:@@:@:@:@:@:@#:: - | :::::::: : :@: : @:::::::::::::@: : ::: ::::@:@:@@:@:@:@:@:@#:: - | ::::::::: : :@: : @:::::::::::::@: : ::: ::::@:@:@@:@:@:@:@:@#:: - | ::::::::: : :@: : @:::::::::::::@: : ::: ::::@:@:@@:@:@:@:@:@#:: - | ::::::::::: : :@: : @:::::::::::::@: : ::: ::::@:@:@@:@:@:@:@:@#:: - | : ::::::::: : :@: : @:::::::::::::@: : ::: ::::@:@:@@:@:@:@:@:@#:: - | @: ::::::::: : :@: : @:::::::::::::@: : ::: ::::@:@:@@:@:@:@:@:@#:: - | @@: ::::::::: : :@: : @:::::::::::::@: : ::: ::::@:@:@@:@:@:@:@:@#:: - | :@@: ::::::::: : :@: : @:::::::::::::@: : ::: ::::@:@:@@:@:@:@:@:@#:: - | :@@: ::::::::: : :@: : @:::::::::::::@: : ::: ::::@:@:@@:@:@:@:@:@#:: - | :@:@@: ::::::::: : :@: : @:::::::::::::@: : ::: ::::@:@:@@:@:@:@:@:@#:: - | :@:@@: ::::::::: : :@: : @:::::::::::::@: : ::: ::::@:@:@@:@:@:@:@:@#:: - | :@:@@: ::::::::: : :@: : @:::::::::::::@: : ::: ::::@:@:@@:@:@:@:@:@#:: - 0 +----------------------------------------------------------------------->Gi - 0 8.535 - - -post-patch: - - MB -21.03^ :::@@:::@::::@@:::::::@@::::::::@::::::::::::@:::@:::::::@:::: - | #:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: - | #:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: - | :#:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: - | :#:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: - | :#:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: - | :::#:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: - | : :#:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: - | : :#:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: - | @: :#:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: - | @@: :#:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: - | @@: :#:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: - | @@: :#:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: - | :@@: :#:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: - | :@@: :#:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: - | :@:@@: :#:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: - | :@:@@: :#:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: - | :@:@@: :#:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: - | :@:@@: :#:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: - | :@:@@: :#:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: - 0 +----------------------------------------------------------------------->Gi - 0 8.421 - -""" - - -standard_l_qtable = ( - # fmt: off - 16, 11, 10, 16, 24, 40, 51, 61, - 12, 12, 14, 19, 26, 58, 60, 55, - 14, 13, 16, 24, 40, 57, 69, 56, - 14, 17, 22, 29, 51, 87, 80, 62, - 18, 22, 37, 56, 68, 109, 103, 77, - 24, 35, 55, 64, 81, 104, 113, 92, - 49, 64, 78, 87, 103, 121, 120, 101, - 72, 92, 95, 98, 112, 100, 103, 99, - # fmt: on -) - -standard_chrominance_qtable = ( - # fmt: off - 17, 18, 24, 47, 99, 99, 99, 99, - 18, 21, 26, 66, 99, 99, 99, 99, - 24, 26, 56, 99, 99, 99, 99, 99, - 47, 66, 99, 99, 99, 99, 99, 99, - 99, 99, 99, 99, 99, 99, 99, 99, - 99, 99, 99, 99, 99, 99, 99, 99, - 99, 99, 99, 99, 99, 99, 99, 99, - 99, 99, 99, 99, 99, 99, 99, 99, - # fmt: on -) - - -@pytest.mark.parametrize( - "qtables", - ( - (standard_l_qtable, standard_chrominance_qtable), - [standard_l_qtable, standard_chrominance_qtable], - ), -) -def test_qtables_leak(qtables: tuple[tuple[int, ...]] | list[tuple[int, ...]]) -> None: - im = hopper("RGB") - for _ in range(iterations): - test_output = BytesIO() - im.save(test_output, "JPEG", qtables=qtables) - - -def test_exif_leak() -> None: - """ - pre patch: - - MB - 177.1^ # - | @@@# - | :@@@@@@# - | ::::@@@@@@# - | ::::::::@@@@@@# - | @@::::: ::::@@@@@@# - | @@@@ ::::: ::::@@@@@@# - | @@@@@@@ ::::: ::::@@@@@@# - | @@::@@@@@@@ ::::: ::::@@@@@@# - | @@@@ : @@@@@@@ ::::: ::::@@@@@@# - | @@@@@@ @@ : @@@@@@@ ::::: ::::@@@@@@# - | @@@@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# - | @::@@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# - | ::::@: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# - | :@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# - | ::@@::@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# - | @@::: @ ::@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# - | @::@ : : @ ::@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# - | :::@: @ : : @ ::@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# - | @@@:: @: @ : : @ ::@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# - 0 +----------------------------------------------------------------------->Gi - 0 11.37 - - - post patch: - - MB - 21.06^ ::::::::::::::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | ##::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | # ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | # ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | # ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @@@@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @ @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @ @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @ @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @ @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - 0 +----------------------------------------------------------------------->Gi - 0 11.33 - """ - im = hopper("RGB") - exif = b"12345678" * 4096 - - for _ in range(iterations): - test_output = BytesIO() - im.save(test_output, "JPEG", exif=exif) - - -def test_base_save() -> None: - """ - base case: - MB - 20.99^ ::::: :::::::::::::::::::::::::::::::::::::::::::@::: - | ##: : ::::::@::::::: :::: :::: : : : : : : :::::::::::: :::@::: - | # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | @@# : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | @@@ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | @ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | :@@@@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | :@ @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | :@ @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | :@ @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | :@ @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - 0 +----------------------------------------------------------------------->Gi - 0 7.882""" - im = hopper("RGB") - - for _ in range(iterations): - test_output = BytesIO() - im.save(test_output, "JPEG") +from __future__ import annotations + +from io import BytesIO + +import pytest + +from .helper import hopper, is_win32 + +iterations = 5000 + + +""" +When run on a system without the jpeg leak fixes, +the valgrind runs look like this. + +valgrind --tool=massif python test-installed.py -s -v Tests/check_jpeg_leaks.py + +""" + + +pytestmark = pytest.mark.skipif(is_win32(), reason="requires Unix or macOS") + +""" +pre patch: + + MB +31.62^ : + | @:@:@:@#:: + | @:@:@@:@:@:@:@:@#:: + | ::::::::@:@:@@:@:@:@:@:@#:: + | :::::@::::::: ::::@:@:@@:@:@:@:@:@#:: + | @:::::::::::::@: : ::: ::::@:@:@@:@:@:@:@:@#:: + | ::::::@::::@:::::::::::::@: : ::: ::::@:@:@@:@:@:@:@:@#:: + | ::::::: : :@: : @:::::::::::::@: : ::: ::::@:@:@@:@:@:@:@:@#:: + | :::::::: : :@: : @:::::::::::::@: : ::: ::::@:@:@@:@:@:@:@:@#:: + | ::::::::: : :@: : @:::::::::::::@: : ::: ::::@:@:@@:@:@:@:@:@#:: + | ::::::::: : :@: : @:::::::::::::@: : ::: ::::@:@:@@:@:@:@:@:@#:: + | ::::::::::: : :@: : @:::::::::::::@: : ::: ::::@:@:@@:@:@:@:@:@#:: + | : ::::::::: : :@: : @:::::::::::::@: : ::: ::::@:@:@@:@:@:@:@:@#:: + | @: ::::::::: : :@: : @:::::::::::::@: : ::: ::::@:@:@@:@:@:@:@:@#:: + | @@: ::::::::: : :@: : @:::::::::::::@: : ::: ::::@:@:@@:@:@:@:@:@#:: + | :@@: ::::::::: : :@: : @:::::::::::::@: : ::: ::::@:@:@@:@:@:@:@:@#:: + | :@@: ::::::::: : :@: : @:::::::::::::@: : ::: ::::@:@:@@:@:@:@:@:@#:: + | :@:@@: ::::::::: : :@: : @:::::::::::::@: : ::: ::::@:@:@@:@:@:@:@:@#:: + | :@:@@: ::::::::: : :@: : @:::::::::::::@: : ::: ::::@:@:@@:@:@:@:@:@#:: + | :@:@@: ::::::::: : :@: : @:::::::::::::@: : ::: ::::@:@:@@:@:@:@:@:@#:: + 0 +----------------------------------------------------------------------->Gi + 0 8.535 + + +post-patch: + + MB +21.03^ :::@@:::@::::@@:::::::@@::::::::@::::::::::::@:::@:::::::@:::: + | #:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: + | #:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: + | :#:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: + | :#:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: + | :#:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: + | :::#:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: + | : :#:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: + | : :#:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: + | @: :#:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: + | @@: :#:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: + | @@: :#:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: + | @@: :#:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: + | :@@: :#:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: + | :@@: :#:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: + | :@:@@: :#:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: + | :@:@@: :#:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: + | :@:@@: :#:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: + | :@:@@: :#:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: + | :@:@@: :#:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: + 0 +----------------------------------------------------------------------->Gi + 0 8.421 + +""" + + +standard_l_qtable = ( + # fmt: off + 16, 11, 10, 16, 24, 40, 51, 61, + 12, 12, 14, 19, 26, 58, 60, 55, + 14, 13, 16, 24, 40, 57, 69, 56, + 14, 17, 22, 29, 51, 87, 80, 62, + 18, 22, 37, 56, 68, 109, 103, 77, + 24, 35, 55, 64, 81, 104, 113, 92, + 49, 64, 78, 87, 103, 121, 120, 101, + 72, 92, 95, 98, 112, 100, 103, 99, + # fmt: on +) + +standard_chrominance_qtable = ( + # fmt: off + 17, 18, 24, 47, 99, 99, 99, 99, + 18, 21, 26, 66, 99, 99, 99, 99, + 24, 26, 56, 99, 99, 99, 99, 99, + 47, 66, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + # fmt: on +) + + +@pytest.mark.parametrize( + "qtables", + ( + (standard_l_qtable, standard_chrominance_qtable), + [standard_l_qtable, standard_chrominance_qtable], + ), +) +def test_qtables_leak(qtables: tuple[tuple[int, ...]] | list[tuple[int, ...]]) -> None: + im = hopper("RGB") + for _ in range(iterations): + test_output = BytesIO() + im.save(test_output, "JPEG", qtables=qtables) + + +def test_exif_leak() -> None: + """ + pre patch: + + MB + 177.1^ # + | @@@# + | :@@@@@@# + | ::::@@@@@@# + | ::::::::@@@@@@# + | @@::::: ::::@@@@@@# + | @@@@ ::::: ::::@@@@@@# + | @@@@@@@ ::::: ::::@@@@@@# + | @@::@@@@@@@ ::::: ::::@@@@@@# + | @@@@ : @@@@@@@ ::::: ::::@@@@@@# + | @@@@@@ @@ : @@@@@@@ ::::: ::::@@@@@@# + | @@@@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# + | @::@@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# + | ::::@: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# + | :@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# + | ::@@::@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# + | @@::: @ ::@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# + | @::@ : : @ ::@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# + | :::@: @ : : @ ::@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# + | @@@:: @: @ : : @ ::@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# + 0 +----------------------------------------------------------------------->Gi + 0 11.37 + + + post patch: + + MB + 21.06^ ::::::::::::::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | ##::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | # ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | # ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | # ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @@@@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @ @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @ @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @ @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @ @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + 0 +----------------------------------------------------------------------->Gi + 0 11.33 + """ + im = hopper("RGB") + exif = b"12345678" * 4096 + + for _ in range(iterations): + test_output = BytesIO() + im.save(test_output, "JPEG", exif=exif) + + +def test_base_save() -> None: + """ + base case: + MB + 20.99^ ::::: :::::::::::::::::::::::::::::::::::::::::::@::: + | ##: : ::::::@::::::: :::: :::: : : : : : : :::::::::::: :::@::: + | # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | @@# : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | @@@ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | @ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | :@@@@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | :@ @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | :@ @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | :@ @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | :@ @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + 0 +----------------------------------------------------------------------->Gi + 0 7.882""" + im = hopper("RGB") + + for _ in range(iterations): + test_output = BytesIO() + im.save(test_output, "JPEG") diff --git a/Tests/check_large_memory.py b/Tests/check_large_memory.py index a9ce79e57..2df3600b8 100644 --- a/Tests/check_large_memory.py +++ b/Tests/check_large_memory.py @@ -1,54 +1,54 @@ -from __future__ import annotations - -import sys -from pathlib import Path -from types import ModuleType - -import pytest - -from PIL import Image - -# This test is not run automatically. -# -# It requires > 2gb memory for the >2 gigapixel image generated in the -# second test. Running this automatically would amount to a denial of -# service on our testing infrastructure. I expect this test to fail -# on any 32-bit machine, as well as any smallish things (like -# Raspberry Pis). It does succeed on a 3gb Ubuntu 12.04x64 VM on Python -# 2.7 and 3.2. - - -numpy: ModuleType | None -try: - import numpy -except ImportError: - numpy = None - -YDIM = 32769 -XDIM = 48000 - - -pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system") - - -def _write_png(tmp_path: Path, xdim: int, ydim: int) -> None: - f = str(tmp_path / "temp.png") - im = Image.new("L", (xdim, ydim), 0) - im.save(f) - - -def test_large(tmp_path: Path) -> None: - """succeeded prepatch""" - _write_png(tmp_path, XDIM, YDIM) - - -def test_2gpx(tmp_path: Path) -> None: - """failed prepatch""" - _write_png(tmp_path, XDIM, XDIM) - - -@pytest.mark.skipif(numpy is None, reason="Numpy is not installed") -def test_size_greater_than_int() -> None: - assert numpy is not None - arr = numpy.ndarray(shape=(16394, 16394)) - Image.fromarray(arr) +from __future__ import annotations + +import sys +from pathlib import Path +from types import ModuleType + +import pytest + +from PIL import Image + +# This test is not run automatically. +# +# It requires > 2gb memory for the >2 gigapixel image generated in the +# second test. Running this automatically would amount to a denial of +# service on our testing infrastructure. I expect this test to fail +# on any 32-bit machine, as well as any smallish things (like +# Raspberry Pis). It does succeed on a 3gb Ubuntu 12.04x64 VM on Python +# 2.7 and 3.2. + + +numpy: ModuleType | None +try: + import numpy +except ImportError: + numpy = None + +YDIM = 32769 +XDIM = 48000 + + +pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system") + + +def _write_png(tmp_path: Path, xdim: int, ydim: int) -> None: + f = str(tmp_path / "temp.png") + im = Image.new("L", (xdim, ydim), 0) + im.save(f) + + +def test_large(tmp_path: Path) -> None: + """succeeded prepatch""" + _write_png(tmp_path, XDIM, YDIM) + + +def test_2gpx(tmp_path: Path) -> None: + """failed prepatch""" + _write_png(tmp_path, XDIM, XDIM) + + +@pytest.mark.skipif(numpy is None, reason="Numpy is not installed") +def test_size_greater_than_int() -> None: + assert numpy is not None + arr = numpy.ndarray(shape=(16394, 16394)) + Image.fromarray(arr) diff --git a/Tests/check_large_memory_numpy.py b/Tests/check_large_memory_numpy.py index f4ca8d0aa..94e6e8044 100644 --- a/Tests/check_large_memory_numpy.py +++ b/Tests/check_large_memory_numpy.py @@ -1,43 +1,43 @@ -from __future__ import annotations - -import sys -from pathlib import Path - -import pytest - -from PIL import Image - -# This test is not run automatically. -# -# It requires > 2gb memory for the >2 gigapixel image generated in the -# second test. Running this automatically would amount to a denial of -# service on our testing infrastructure. I expect this test to fail -# on any 32-bit machine, as well as any smallish things (like -# Raspberry Pis). - - -np = pytest.importorskip("numpy", reason="NumPy not installed") - -YDIM = 32769 -XDIM = 48000 - - -pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system") - - -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") - im = Image.fromarray(a, "L") - im.save(f) - - -def test_large(tmp_path: Path) -> None: - """succeeded prepatch""" - _write_png(tmp_path, XDIM, YDIM) - - -def test_2gpx(tmp_path: Path) -> None: - """failed prepatch""" - _write_png(tmp_path, XDIM, XDIM) +from __future__ import annotations + +import sys +from pathlib import Path + +import pytest + +from PIL import Image + +# This test is not run automatically. +# +# It requires > 2gb memory for the >2 gigapixel image generated in the +# second test. Running this automatically would amount to a denial of +# service on our testing infrastructure. I expect this test to fail +# on any 32-bit machine, as well as any smallish things (like +# Raspberry Pis). + + +np = pytest.importorskip("numpy", reason="NumPy not installed") + +YDIM = 32769 +XDIM = 48000 + + +pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system") + + +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") + im = Image.fromarray(a, "L") + im.save(f) + + +def test_large(tmp_path: Path) -> None: + """succeeded prepatch""" + _write_png(tmp_path, XDIM, YDIM) + + +def test_2gpx(tmp_path: Path) -> None: + """failed prepatch""" + _write_png(tmp_path, XDIM, XDIM) diff --git a/Tests/check_libtiff_segfault.py b/Tests/check_libtiff_segfault.py index 84bda53ed..bd49fdf2e 100644 --- a/Tests/check_libtiff_segfault.py +++ b/Tests/check_libtiff_segfault.py @@ -1,17 +1,17 @@ -from __future__ import annotations - -import pytest - -from PIL import Image - -TEST_FILE = "Tests/images/libtiff_segfault.tif" - - -def test_libtiff_segfault() -> None: - """This test should not segfault. It will on Pillow <= 3.1.0 and - libtiff >= 4.0.0 - """ - - with pytest.raises(OSError): - with Image.open(TEST_FILE) as im: - im.load() +from __future__ import annotations + +import pytest + +from PIL import Image + +TEST_FILE = "Tests/images/libtiff_segfault.tif" + + +def test_libtiff_segfault() -> None: + """This test should not segfault. It will on Pillow <= 3.1.0 and + libtiff >= 4.0.0 + """ + + with pytest.raises(OSError): + with Image.open(TEST_FILE) as im: + im.load() diff --git a/Tests/check_png_dos.py b/Tests/check_png_dos.py index 63d6657bc..4c17a3014 100644 --- a/Tests/check_png_dos.py +++ b/Tests/check_png_dos.py @@ -1,65 +1,65 @@ -from __future__ import annotations - -import zlib -from io import BytesIO - -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 - - try: - im = Image.open(TEST_FILE) - 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" - - for s in im.info.values(): - assert len(s) < 1024 * 1024, "Text chunk larger than 1M" - - -def test_dos_text() -> None: - try: - im = Image.open(TEST_FILE) - im.load() - except ValueError as msg: - assert msg, "Decompressed Data Too Large" - return - - assert isinstance(im, PngImagePlugin.PngImageFile) - for s in im.text.values(): - assert len(s) < 1024 * 1024, "Text chunk larger than 1M" - - -def test_dos_total_memory() -> None: - im = Image.new("L", (1, 1)) - compressed_data = zlib.compress(b"a" * 1024 * 1023) - - info = PngImagePlugin.PngInfo() - - for x in range(64): - info.add_text(f"t{x}", compressed_data, zip=True) - info.add_itxt(f"i{x}", compressed_data, zip=True) - - b = BytesIO() - im.save(b, "PNG", pnginfo=info) - b.seek(0) - - try: - im2 = Image.open(b) - except ValueError as msg: - assert "Too much memory" in str(msg) - return - - total_len = 0 - assert isinstance(im2, PngImagePlugin.PngImageFile) - for txt in im2.text.values(): - total_len += len(txt) - assert total_len < 64 * 1024 * 1024, "Total text chunks greater than 64M" +from __future__ import annotations + +import zlib +from io import BytesIO + +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 + + try: + im = Image.open(TEST_FILE) + 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" + + for s in im.info.values(): + assert len(s) < 1024 * 1024, "Text chunk larger than 1M" + + +def test_dos_text() -> None: + try: + im = Image.open(TEST_FILE) + im.load() + except ValueError as msg: + assert msg, "Decompressed Data Too Large" + return + + assert isinstance(im, PngImagePlugin.PngImageFile) + for s in im.text.values(): + assert len(s) < 1024 * 1024, "Text chunk larger than 1M" + + +def test_dos_total_memory() -> None: + im = Image.new("L", (1, 1)) + compressed_data = zlib.compress(b"a" * 1024 * 1023) + + info = PngImagePlugin.PngInfo() + + for x in range(64): + info.add_text(f"t{x}", compressed_data, zip=True) + info.add_itxt(f"i{x}", compressed_data, zip=True) + + b = BytesIO() + im.save(b, "PNG", pnginfo=info) + b.seek(0) + + try: + im2 = Image.open(b) + except ValueError as msg: + assert "Too much memory" in str(msg) + return + + total_len = 0 + assert isinstance(im2, PngImagePlugin.PngImageFile) + for txt in im2.text.values(): + total_len += len(txt) + assert total_len < 64 * 1024 * 1024, "Total text chunks greater than 64M" diff --git a/Tests/check_release_notes.py b/Tests/check_release_notes.py index cf414d7ff..af4c956ec 100644 --- a/Tests/check_release_notes.py +++ b/Tests/check_release_notes.py @@ -1,8 +1,8 @@ -from __future__ import annotations - -import sys -from pathlib import Path - -for rst in Path("docs/releasenotes").glob("[1-9]*.rst"): - if "TODO" in open(rst).read(): - sys.exit(f"Error: remove TODO from {rst}") +from __future__ import annotations + +import sys +from pathlib import Path + +for rst in Path("docs/releasenotes").glob("[1-9]*.rst"): + if "TODO" in open(rst).read(): + sys.exit(f"Error: remove TODO from {rst}") diff --git a/Tests/check_wheel.py b/Tests/check_wheel.py index 4b91984f5..336a676ae 100644 --- a/Tests/check_wheel.py +++ b/Tests/check_wheel.py @@ -1,43 +1,43 @@ -from __future__ import annotations - -import sys - -from PIL import features - - -def test_wheel_modules() -> None: - expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp"} - - # tkinter is not available in cibuildwheel installed CPython on Windows - try: - import tkinter - - assert tkinter - except ImportError: - expected_modules.remove("tkinter") - - assert set(features.get_supported_modules()) == expected_modules - - -def test_wheel_codecs() -> None: - expected_codecs = {"jpg", "jpg_2000", "zlib", "libtiff"} - - assert set(features.get_supported_codecs()) == expected_codecs - - -def test_wheel_features() -> None: - expected_features = { - "webp_anim", - "webp_mux", - "transp_webp", - "raqm", - "fribidi", - "harfbuzz", - "libjpeg_turbo", - "xcb", - } - - if sys.platform == "win32": - expected_features.remove("xcb") - - assert set(features.get_supported_features()) == expected_features +from __future__ import annotations + +import sys + +from PIL import features + + +def test_wheel_modules() -> None: + expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp"} + + # tkinter is not available in cibuildwheel installed CPython on Windows + try: + import tkinter + + assert tkinter + except ImportError: + expected_modules.remove("tkinter") + + assert set(features.get_supported_modules()) == expected_modules + + +def test_wheel_codecs() -> None: + expected_codecs = {"jpg", "jpg_2000", "zlib", "libtiff"} + + assert set(features.get_supported_codecs()) == expected_codecs + + +def test_wheel_features() -> None: + expected_features = { + "webp_anim", + "webp_mux", + "transp_webp", + "raqm", + "fribidi", + "harfbuzz", + "libjpeg_turbo", + "xcb", + } + + if sys.platform == "win32": + expected_features.remove("xcb") + + assert set(features.get_supported_features()) == expected_features diff --git a/Tests/conftest.py b/Tests/conftest.py index e00d1f019..9ba31b377 100644 --- a/Tests/conftest.py +++ b/Tests/conftest.py @@ -1,35 +1,35 @@ -from __future__ import annotations - -import io - -import pytest - - -def pytest_report_header(config: pytest.Config) -> str: - try: - from PIL import features - - with io.StringIO() as out: - features.pilinfo(out=out, supported_formats=False) - return out.getvalue() - except Exception as e: - return f"pytest_report_header failed: {e}" - - -def pytest_configure(config: pytest.Config) -> None: - config.addinivalue_line( - "markers", - "pil_noop_mark: A conditional mark where nothing special happens", - ) - - # We're marking some tests to ignore valgrind errors and XFAIL them. - # Ensure that the mark is defined - # even in cases where pytest-valgrind isn't installed - try: - config.addinivalue_line( - "markers", - "valgrind_known_error: Tests that have known issues with valgrind", - ) - except Exception: - # valgrind is already installed - pass +from __future__ import annotations + +import io + +import pytest + + +def pytest_report_header(config: pytest.Config) -> str: + try: + from PIL import features + + with io.StringIO() as out: + features.pilinfo(out=out, supported_formats=False) + return out.getvalue() + except Exception as e: + return f"pytest_report_header failed: {e}" + + +def pytest_configure(config: pytest.Config) -> None: + config.addinivalue_line( + "markers", + "pil_noop_mark: A conditional mark where nothing special happens", + ) + + # We're marking some tests to ignore valgrind errors and XFAIL them. + # Ensure that the mark is defined + # even in cases where pytest-valgrind isn't installed + try: + config.addinivalue_line( + "markers", + "valgrind_known_error: Tests that have known issues with valgrind", + ) + except Exception: + # valgrind is already installed + pass diff --git a/Tests/createfontdatachunk.py b/Tests/createfontdatachunk.py index 41c76f87e..add0da547 100755 --- a/Tests/createfontdatachunk.py +++ b/Tests/createfontdatachunk.py @@ -1,18 +1,18 @@ -#!/usr/bin/env python3 -from __future__ import annotations - -import base64 -import os - -if __name__ == "__main__": - # create font data chunk for embedding - font = "Tests/images/courB08" - print(" f._load_pilfont_data(") - print(f" # {os.path.basename(font)}") - print(" BytesIO(base64.decodestring(b'''") - with open(font + ".pil", "rb") as fp: - print(base64.b64encode(fp.read()).decode()) - print("''')), Image.open(BytesIO(base64.decodestring(b'''") - with open(font + ".pbm", "rb") as fp: - print(base64.b64encode(fp.read()).decode()) - print("'''))))") +#!/usr/bin/env python3 +from __future__ import annotations + +import base64 +import os + +if __name__ == "__main__": + # create font data chunk for embedding + font = "Tests/images/courB08" + print(" f._load_pilfont_data(") + print(f" # {os.path.basename(font)}") + print(" BytesIO(base64.decodestring(b'''") + with open(font + ".pil", "rb") as fp: + print(base64.b64encode(fp.read()).decode()) + print("''')), Image.open(BytesIO(base64.decodestring(b'''") + with open(font + ".pbm", "rb") as fp: + print(base64.b64encode(fp.read()).decode()) + print("'''))))") diff --git a/Tests/fonts/DejaVuSans/LICENSE.txt b/Tests/fonts/DejaVuSans/LICENSE.txt index be6a4d84c..57a69c6ad 100644 --- a/Tests/fonts/DejaVuSans/LICENSE.txt +++ b/Tests/fonts/DejaVuSans/LICENSE.txt @@ -1,40 +1,40 @@ -DejaVuSans-24-{1,2,4,8}-stripped.ttf are based on DejaVuSans.ttf converted using FontForge to add bitmap strikes and keep only the ASCII range. - -DejaVu Fonts — License -Fonts are © Bitstream (see below). DejaVu changes are in public domain. Explanation of copyright is on Gnome page on Bitstream Vera fonts. Glyphs imported from Arev fonts are © Tavmjung Bah (see below) - -Bitstream Vera Fonts Copyright -Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is a trademark of Bitstream, Inc. - -Permission is hereby granted, free of charge, to any person obtaining a copy of the fonts accompanying this license ("Fonts") and associated documentation files (the "Font Software"), to reproduce and distribute the Font Software, including without limitation the rights to use, copy, merge, publish, distribute, and/or sell copies of the Font Software, and to permit persons to whom the Font Software is furnished to do so, subject to the following conditions: - -The above copyright and trademark notices and this permission notice shall be included in all copies of one or more of the Font Software typefaces. - -The Font Software may be modified, altered, or added to, and in particular the designs of glyphs or characters in the Fonts may be modified and additional glyphs or characters may be added to the Fonts, only if the fonts are renamed to names not containing either the words "Bitstream" or the word "Vera". - -This License becomes null and void to the extent applicable to Fonts or Font Software that has been modified and is distributed under the "Bitstream Vera" names. - -The Font Software may be sold as part of a larger software package but no copy of one or more of the Font Software typefaces may be sold by itself. - -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. - -Except as contained in this notice, the names of Gnome, the Gnome Foundation, and Bitstream Inc., shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Font Software without prior written authorization from the Gnome Foundation or Bitstream Inc., respectively. For further information, contact: fonts at gnome dot org. - -Arev Fonts Copyright -Original text - -Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved. - -Permission is hereby granted, free of charge, to any person obtaining a copy of the fonts accompanying this license ("Fonts") and associated documentation files (the "Font Software"), to reproduce and distribute the modifications to the Bitstream Vera Font Software, including without limitation the rights to use, copy, merge, publish, distribute, and/or sell copies of the Font Software, and to permit persons to whom the Font Software is furnished to do so, subject to the following conditions: - -The above copyright and trademark notices and this permission notice shall be included in all copies of one or more of the Font Software typefaces. - -The Font Software may be modified, altered, or added to, and in particular the designs of glyphs or characters in the Fonts may be modified and additional glyphs or characters may be added to the Fonts, only if the fonts are renamed to names not containing either the words "Tavmjong Bah" or the word "Arev". - -This License becomes null and void to the extent applicable to Fonts or Font Software that has been modified and is distributed under the "Tavmjong Bah Arev" names. - -The Font Software may be sold as part of a larger software package but no copy of one or more of the Font Software typefaces may be sold by itself. - -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL TAVMJONG BAH BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. - -Except as contained in this notice, the name of Tavmjong Bah shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Font Software without prior written authorization from Tavmjong Bah. For further information, contact: tavmjong @ free . fr. +DejaVuSans-24-{1,2,4,8}-stripped.ttf are based on DejaVuSans.ttf converted using FontForge to add bitmap strikes and keep only the ASCII range. + +DejaVu Fonts — License +Fonts are © Bitstream (see below). DejaVu changes are in public domain. Explanation of copyright is on Gnome page on Bitstream Vera fonts. Glyphs imported from Arev fonts are © Tavmjung Bah (see below) + +Bitstream Vera Fonts Copyright +Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is a trademark of Bitstream, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of the fonts accompanying this license ("Fonts") and associated documentation files (the "Font Software"), to reproduce and distribute the Font Software, including without limitation the rights to use, copy, merge, publish, distribute, and/or sell copies of the Font Software, and to permit persons to whom the Font Software is furnished to do so, subject to the following conditions: + +The above copyright and trademark notices and this permission notice shall be included in all copies of one or more of the Font Software typefaces. + +The Font Software may be modified, altered, or added to, and in particular the designs of glyphs or characters in the Fonts may be modified and additional glyphs or characters may be added to the Fonts, only if the fonts are renamed to names not containing either the words "Bitstream" or the word "Vera". + +This License becomes null and void to the extent applicable to Fonts or Font Software that has been modified and is distributed under the "Bitstream Vera" names. + +The Font Software may be sold as part of a larger software package but no copy of one or more of the Font Software typefaces may be sold by itself. + +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. + +Except as contained in this notice, the names of Gnome, the Gnome Foundation, and Bitstream Inc., shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Font Software without prior written authorization from the Gnome Foundation or Bitstream Inc., respectively. For further information, contact: fonts at gnome dot org. + +Arev Fonts Copyright +Original text + +Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy of the fonts accompanying this license ("Fonts") and associated documentation files (the "Font Software"), to reproduce and distribute the modifications to the Bitstream Vera Font Software, including without limitation the rights to use, copy, merge, publish, distribute, and/or sell copies of the Font Software, and to permit persons to whom the Font Software is furnished to do so, subject to the following conditions: + +The above copyright and trademark notices and this permission notice shall be included in all copies of one or more of the Font Software typefaces. + +The Font Software may be modified, altered, or added to, and in particular the designs of glyphs or characters in the Fonts may be modified and additional glyphs or characters may be added to the Fonts, only if the fonts are renamed to names not containing either the words "Tavmjong Bah" or the word "Arev". + +This License becomes null and void to the extent applicable to Fonts or Font Software that has been modified and is distributed under the "Tavmjong Bah Arev" names. + +The Font Software may be sold as part of a larger software package but no copy of one or more of the Font Software typefaces may be sold by itself. + +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL TAVMJONG BAH BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. + +Except as contained in this notice, the name of Tavmjong Bah shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Font Software without prior written authorization from Tavmjong Bah. For further information, contact: tavmjong @ free . fr. diff --git a/Tests/fonts/LICENSE.txt b/Tests/fonts/LICENSE.txt index 3c8a23197..6046f5c71 100644 --- a/Tests/fonts/LICENSE.txt +++ b/Tests/fonts/LICENSE.txt @@ -1,28 +1,28 @@ - -NotoNastaliqUrdu-Regular.ttf and NotoSansSymbols-Regular.ttf, from https://github.com/googlei18n/noto-fonts -NotoSans-Regular.ttf, from https://www.google.com/get/noto/ -NotoSansJP-Thin.otf, from https://www.google.com/get/noto/help/cjk/ -AdobeVFPrototype.ttf, from https://github.com/adobe-fonts/adobe-variable-font-prototype -TINY5x3GX.ttf, from http://velvetyne.fr/fonts/tiny -ArefRuqaa-Regular.ttf, from https://github.com/google/fonts/tree/master/ofl/arefruqaa -ter-x20b.pcf, from http://terminus-font.sourceforge.net/ -BungeeColor-Regular_colr_Windows.ttf, from https://github.com/djrrb/bungee -OpenSans.woff2, from https://fonts.googleapis.com/css?family=Open+Sans - -All of the above fonts are published under the SIL Open Font License (OFL) v1.1 (http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL), which allows you to copy, modify, and redistribute them if you need to. - -FreeMono.ttf is licensed under GPLv3, with the GPL font exception. - -OpenSansCondensed-LightItalic.tt, from https://fonts.google.com/specimen/Open+Sans, under Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - -chromacheck-sbix.woff, from https://github.com/RoelN/ChromaCheck, under The MIT License (MIT), Copyright (c) 2018 Roel Nieskens, https://pixelambacht.nl Copyright (c) 2018 Google LLC - -KhmerOSBattambang-Regular.ttf is licensed under LGPL-2.1 or later. - -FreeMono.ttf is licensed under GPLv3. - -10x20-ISO8859-1.pcf, from https://packages.ubuntu.com/xenial/xfonts-base - -"Public domain font. Share and enjoy." - -CBDTTestFont.ttf and EBDTTestFont.ttf from https://github.com/nulano/font-tests are public domain. + +NotoNastaliqUrdu-Regular.ttf and NotoSansSymbols-Regular.ttf, from https://github.com/googlei18n/noto-fonts +NotoSans-Regular.ttf, from https://www.google.com/get/noto/ +NotoSansJP-Thin.otf, from https://www.google.com/get/noto/help/cjk/ +AdobeVFPrototype.ttf, from https://github.com/adobe-fonts/adobe-variable-font-prototype +TINY5x3GX.ttf, from http://velvetyne.fr/fonts/tiny +ArefRuqaa-Regular.ttf, from https://github.com/google/fonts/tree/master/ofl/arefruqaa +ter-x20b.pcf, from http://terminus-font.sourceforge.net/ +BungeeColor-Regular_colr_Windows.ttf, from https://github.com/djrrb/bungee +OpenSans.woff2, from https://fonts.googleapis.com/css?family=Open+Sans + +All of the above fonts are published under the SIL Open Font License (OFL) v1.1 (http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL), which allows you to copy, modify, and redistribute them if you need to. + +FreeMono.ttf is licensed under GPLv3, with the GPL font exception. + +OpenSansCondensed-LightItalic.tt, from https://fonts.google.com/specimen/Open+Sans, under Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + +chromacheck-sbix.woff, from https://github.com/RoelN/ChromaCheck, under The MIT License (MIT), Copyright (c) 2018 Roel Nieskens, https://pixelambacht.nl Copyright (c) 2018 Google LLC + +KhmerOSBattambang-Regular.ttf is licensed under LGPL-2.1 or later. + +FreeMono.ttf is licensed under GPLv3. + +10x20-ISO8859-1.pcf, from https://packages.ubuntu.com/xenial/xfonts-base + +"Public domain font. Share and enjoy." + +CBDTTestFont.ttf and EBDTTestFont.ttf from https://github.com/nulano/font-tests are public domain. diff --git a/Tests/fonts/fuzz_font-5203009437302784 b/Tests/fonts/fuzz_font-5203009437302784 index 0465e48c2..e301b8418 100644 --- a/Tests/fonts/fuzz_font-5203009437302784 +++ b/Tests/fonts/fuzz_font-5203009437302784 @@ -1,10 +1,10 @@ -STARTFONT -FONT ÿ -SIZE 10 -FONTBOUNDINGBOX -CHARS -STARTCHAR -ENCODING -BBX 2 5 -ENDCHAR -ENDFONT +STARTFONT +FONT ÿ +SIZE 10 +FONTBOUNDINGBOX +CHARS +STARTCHAR +ENCODING +BBX 2 5 +ENDCHAR +ENDFONT diff --git a/Tests/helper.py b/Tests/helper.py index fe337c09f..c3559ea21 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -1,372 +1,372 @@ -""" -Helper functions. -""" - -from __future__ import annotations - -import logging -import os -import shutil -import subprocess -import sys -import sysconfig -import tempfile -from functools import lru_cache -from io import BytesIO -from typing import Any, Callable, Sequence - -import pytest -from packaging.version import parse as parse_version - -from PIL import Image, ImageMath, features - -logger = logging.getLogger(__name__) - -uploader = None -if os.environ.get("SHOW_ERRORS"): - uploader = "show" -elif "GITHUB_ACTIONS" in os.environ: - uploader = "github_actions" - - -def upload(a: Image.Image, b: Image.Image) -> str | None: - if uploader == "show": - # local img.show for errors. - a.show() - b.show() - elif uploader == "github_actions": - dir_errors = os.path.join(os.path.dirname(__file__), "errors") - os.makedirs(dir_errors, exist_ok=True) - tmpdir = tempfile.mkdtemp(dir=dir_errors) - a.save(os.path.join(tmpdir, "a.png")) - b.save(os.path.join(tmpdir, "b.png")) - return tmpdir - return None - - -def convert_to_comparable( - a: Image.Image, b: Image.Image -) -> tuple[Image.Image, Image.Image]: - new_a, new_b = a, b - if a.mode == "P": - new_a = Image.new("L", a.size) - new_b = Image.new("L", b.size) - new_a.putdata(a.getdata()) - new_b.putdata(b.getdata()) - elif a.mode == "I;16": - new_a = a.convert("I") - new_b = b.convert("I") - return new_a, new_b - - -def assert_deep_equal( - a: Sequence[Any], b: Sequence[Any], msg: str | None = None -) -> None: - try: - assert len(a) == len(b), msg or f"got length {len(a)}, expected {len(b)}" - except Exception: - assert a == b, msg - - -def assert_image( - im: Image.Image, mode: str, size: tuple[int, int], msg: str | None = None -) -> None: - if mode is not None: - assert im.mode == mode, ( - msg or f"got mode {repr(im.mode)}, expected {repr(mode)}" - ) - - if size is not None: - assert im.size == size, ( - msg or f"got size {repr(im.size)}, expected {repr(size)}" - ) - - -def assert_image_equal(a: Image.Image, b: Image.Image, msg: str | None = None) -> None: - assert a.mode == b.mode, msg or f"got mode {repr(a.mode)}, expected {repr(b.mode)}" - assert a.size == b.size, msg or f"got size {repr(a.size)}, expected {repr(b.size)}" - if a.tobytes() != b.tobytes(): - try: - url = upload(a, b) - if url: - logger.error("URL for test images: %s", url) - except Exception: - pass - - pytest.fail(msg or "got different content") - - -def assert_image_equal_tofile( - a: Image.Image, filename: str, msg: str | None = None, mode: str | None = None -) -> None: - with Image.open(filename) as img: - if mode: - img = img.convert(mode) - assert_image_equal(a, img, msg) - - -def assert_image_similar( - a: Image.Image, b: Image.Image, epsilon: float, msg: str | None = None -) -> None: - assert a.mode == b.mode, msg or f"got mode {repr(a.mode)}, expected {repr(b.mode)}" - assert a.size == b.size, msg or f"got size {repr(a.size)}, expected {repr(b.size)}" - - a, b = convert_to_comparable(a, b) - - diff = 0 - for ach, bch in zip(a.split(), b.split()): - chdiff = ImageMath.lambda_eval( - lambda args: abs(args["a"] - args["b"]), a=ach, b=bch - ).convert("L") - diff += sum(i * num for i, num in enumerate(chdiff.histogram())) - - ave_diff = diff / (a.size[0] * a.size[1]) - try: - assert epsilon >= ave_diff, ( - (msg or "") - + f" average pixel value difference {ave_diff:.4f} > epsilon {epsilon:.4f}" - ) - except Exception as e: - try: - url = upload(a, b) - if url: - logger.exception("URL for test images: %s", url) - except Exception: - pass - raise e - - -def assert_image_similar_tofile( - a: Image.Image, - filename: str, - 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 - - -def assert_tuple_approx_equal( - actuals: Sequence[int], targets: tuple[int, ...], threshold: int, msg: str -) -> None: - """Tests if actuals has values within threshold from targets""" - for i, target in enumerate(targets): - if not (target - threshold <= actuals[i] <= target + threshold): - pytest.fail(msg + ": " + repr(actuals) + " != " + repr(targets)) - - -def skip_unless_feature(feature: str) -> pytest.MarkDecorator: - reason = f"{feature} not available" - return pytest.mark.skipif(not features.check(feature), reason=reason) - - -def skip_unless_feature_version( - feature: str, required: str, reason: str | None = None -) -> pytest.MarkDecorator: - version = features.version(feature) - if version is None: - return pytest.mark.skip(f"{feature} not available") - if reason is None: - reason = f"{feature} is older than {required}" - version_required = parse_version(required) - version_available = parse_version(version) - return pytest.mark.skipif(version_available < version_required, reason=reason) - - -def mark_if_feature_version( - mark: pytest.MarkDecorator, - feature: str, - version_blacklist: str, - reason: str | None = None, -) -> pytest.MarkDecorator: - version = features.version(feature) - if version is None: - return pytest.mark.pil_noop_mark() - if reason is None: - reason = f"{feature} is {version_blacklist}" - version_required = parse_version(version_blacklist) - version_available = parse_version(version) - if ( - version_available.major == version_required.major - and version_available.minor == version_required.minor - ): - return mark(reason=reason) - return pytest.mark.pil_noop_mark() - - -@pytest.mark.skipif(sys.platform.startswith("win32"), reason="Requires Unix or macOS") -class PillowLeakTestCase: - # requires unix/macOS - iterations = 100 # count - mem_limit = 512 # k - - def _get_mem_usage(self) -> float: - """ - Gets the RUSAGE memory usage, returns in K. Encapsulates the difference - between macOS and Linux rss reporting - - :returns: memory usage in kilobytes - """ - - from resource import RUSAGE_SELF, getrusage - - mem = getrusage(RUSAGE_SELF).ru_maxrss - # man 2 getrusage: - # ru_maxrss - # This is the maximum resident set size utilized - # in bytes on macOS, in kilobytes on Linux - return mem / 1024 if sys.platform == "darwin" else mem - - def _test_leak(self, core: Callable[[], None]) -> None: - start_mem = self._get_mem_usage() - for cycle in range(self.iterations): - core() - mem = self._get_mem_usage() - start_mem - msg = f"memory usage limit exceeded in iteration {cycle}" - assert mem < self.mem_limit, msg - - -# helpers - - -def fromstring(data: bytes) -> Image.Image: - return Image.open(BytesIO(data)) - - -def tostring(im: Image.Image, string_format: str, **options: Any) -> bytes: - out = BytesIO() - im.save(out, string_format, **options) - return out.getvalue() - - -def hopper(mode: str | None = None) -> Image.Image: - # Use caching to reduce reading from disk, but return a copy - # so that the cached image isn't modified by the tests - # (for fast, isolated, repeatable tests). - - if mode is None: - # Always return fresh not-yet-loaded version of image. - # Operations on not-yet-loaded images are a separate class of errors - # that we should catch. - return Image.open("Tests/images/hopper.ppm") - - return _cached_hopper(mode).copy() - - -@lru_cache -def _cached_hopper(mode: str) -> Image.Image: - if mode == "F": - im = hopper("L") - else: - im = hopper() - if mode.startswith("BGR;"): - with pytest.warns(DeprecationWarning): - im = im.convert(mode) - else: - try: - im = im.convert(mode) - except ImportError: - if mode == "LAB": - im = Image.open("Tests/images/hopper.Lab.tif") - else: - raise - return im - - -def djpeg_available() -> bool: - if shutil.which("djpeg"): - try: - subprocess.check_call(["djpeg", "-version"]) - return True - except subprocess.CalledProcessError: # pragma: no cover - return False - return False - - -def cjpeg_available() -> bool: - if shutil.which("cjpeg"): - try: - subprocess.check_call(["cjpeg", "-version"]) - return True - except subprocess.CalledProcessError: # pragma: no cover - return False - return False - - -def netpbm_available() -> bool: - return bool(shutil.which("ppmquant") and shutil.which("ppmtogif")) - - -def magick_command() -> list[str] | None: - if sys.platform == "win32": - magickhome = os.environ.get("MAGICK_HOME") - if magickhome: - imagemagick = [os.path.join(magickhome, "convert.exe")] - graphicsmagick = [os.path.join(magickhome, "gm.exe"), "convert"] - else: - imagemagick = None - graphicsmagick = None - else: - imagemagick = ["convert"] - graphicsmagick = ["gm", "convert"] - - if imagemagick and shutil.which(imagemagick[0]): - return imagemagick - if graphicsmagick and shutil.which(graphicsmagick[0]): - return graphicsmagick - 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 - - -def is_big_endian() -> bool: - return sys.byteorder == "big" - - -def is_ppc64le() -> bool: - import platform - - return platform.machine() == "ppc64le" - - -def is_win32() -> bool: - return sys.platform.startswith("win32") - - -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 - - def __get__(self, instance: Any, cls: type[Any] | None = None) -> Any: - result = instance.__dict__[self.func.__name__] = self.func(instance) - return result +""" +Helper functions. +""" + +from __future__ import annotations + +import logging +import os +import shutil +import subprocess +import sys +import sysconfig +import tempfile +from functools import lru_cache +from io import BytesIO +from typing import Any, Callable, Sequence + +import pytest +from packaging.version import parse as parse_version + +from PIL import Image, ImageMath, features + +logger = logging.getLogger(__name__) + +uploader = None +if os.environ.get("SHOW_ERRORS"): + uploader = "show" +elif "GITHUB_ACTIONS" in os.environ: + uploader = "github_actions" + + +def upload(a: Image.Image, b: Image.Image) -> str | None: + if uploader == "show": + # local img.show for errors. + a.show() + b.show() + elif uploader == "github_actions": + dir_errors = os.path.join(os.path.dirname(__file__), "errors") + os.makedirs(dir_errors, exist_ok=True) + tmpdir = tempfile.mkdtemp(dir=dir_errors) + a.save(os.path.join(tmpdir, "a.png")) + b.save(os.path.join(tmpdir, "b.png")) + return tmpdir + return None + + +def convert_to_comparable( + a: Image.Image, b: Image.Image +) -> tuple[Image.Image, Image.Image]: + new_a, new_b = a, b + if a.mode == "P": + new_a = Image.new("L", a.size) + new_b = Image.new("L", b.size) + new_a.putdata(a.getdata()) + new_b.putdata(b.getdata()) + elif a.mode == "I;16": + new_a = a.convert("I") + new_b = b.convert("I") + return new_a, new_b + + +def assert_deep_equal( + a: Sequence[Any], b: Sequence[Any], msg: str | None = None +) -> None: + try: + assert len(a) == len(b), msg or f"got length {len(a)}, expected {len(b)}" + except Exception: + assert a == b, msg + + +def assert_image( + im: Image.Image, mode: str, size: tuple[int, int], msg: str | None = None +) -> None: + if mode is not None: + assert im.mode == mode, ( + msg or f"got mode {repr(im.mode)}, expected {repr(mode)}" + ) + + if size is not None: + assert im.size == size, ( + msg or f"got size {repr(im.size)}, expected {repr(size)}" + ) + + +def assert_image_equal(a: Image.Image, b: Image.Image, msg: str | None = None) -> None: + assert a.mode == b.mode, msg or f"got mode {repr(a.mode)}, expected {repr(b.mode)}" + assert a.size == b.size, msg or f"got size {repr(a.size)}, expected {repr(b.size)}" + if a.tobytes() != b.tobytes(): + try: + url = upload(a, b) + if url: + logger.error("URL for test images: %s", url) + except Exception: + pass + + pytest.fail(msg or "got different content") + + +def assert_image_equal_tofile( + a: Image.Image, filename: str, msg: str | None = None, mode: str | None = None +) -> None: + with Image.open(filename) as img: + if mode: + img = img.convert(mode) + assert_image_equal(a, img, msg) + + +def assert_image_similar( + a: Image.Image, b: Image.Image, epsilon: float, msg: str | None = None +) -> None: + assert a.mode == b.mode, msg or f"got mode {repr(a.mode)}, expected {repr(b.mode)}" + assert a.size == b.size, msg or f"got size {repr(a.size)}, expected {repr(b.size)}" + + a, b = convert_to_comparable(a, b) + + diff = 0 + for ach, bch in zip(a.split(), b.split()): + chdiff = ImageMath.lambda_eval( + lambda args: abs(args["a"] - args["b"]), a=ach, b=bch + ).convert("L") + diff += sum(i * num for i, num in enumerate(chdiff.histogram())) + + ave_diff = diff / (a.size[0] * a.size[1]) + try: + assert epsilon >= ave_diff, ( + (msg or "") + + f" average pixel value difference {ave_diff:.4f} > epsilon {epsilon:.4f}" + ) + except Exception as e: + try: + url = upload(a, b) + if url: + logger.exception("URL for test images: %s", url) + except Exception: + pass + raise e + + +def assert_image_similar_tofile( + a: Image.Image, + filename: str, + 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 + + +def assert_tuple_approx_equal( + actuals: Sequence[int], targets: tuple[int, ...], threshold: int, msg: str +) -> None: + """Tests if actuals has values within threshold from targets""" + for i, target in enumerate(targets): + if not (target - threshold <= actuals[i] <= target + threshold): + pytest.fail(msg + ": " + repr(actuals) + " != " + repr(targets)) + + +def skip_unless_feature(feature: str) -> pytest.MarkDecorator: + reason = f"{feature} not available" + return pytest.mark.skipif(not features.check(feature), reason=reason) + + +def skip_unless_feature_version( + feature: str, required: str, reason: str | None = None +) -> pytest.MarkDecorator: + version = features.version(feature) + if version is None: + return pytest.mark.skip(f"{feature} not available") + if reason is None: + reason = f"{feature} is older than {required}" + version_required = parse_version(required) + version_available = parse_version(version) + return pytest.mark.skipif(version_available < version_required, reason=reason) + + +def mark_if_feature_version( + mark: pytest.MarkDecorator, + feature: str, + version_blacklist: str, + reason: str | None = None, +) -> pytest.MarkDecorator: + version = features.version(feature) + if version is None: + return pytest.mark.pil_noop_mark() + if reason is None: + reason = f"{feature} is {version_blacklist}" + version_required = parse_version(version_blacklist) + version_available = parse_version(version) + if ( + version_available.major == version_required.major + and version_available.minor == version_required.minor + ): + return mark(reason=reason) + return pytest.mark.pil_noop_mark() + + +@pytest.mark.skipif(sys.platform.startswith("win32"), reason="Requires Unix or macOS") +class PillowLeakTestCase: + # requires unix/macOS + iterations = 100 # count + mem_limit = 512 # k + + def _get_mem_usage(self) -> float: + """ + Gets the RUSAGE memory usage, returns in K. Encapsulates the difference + between macOS and Linux rss reporting + + :returns: memory usage in kilobytes + """ + + from resource import RUSAGE_SELF, getrusage + + mem = getrusage(RUSAGE_SELF).ru_maxrss + # man 2 getrusage: + # ru_maxrss + # This is the maximum resident set size utilized + # in bytes on macOS, in kilobytes on Linux + return mem / 1024 if sys.platform == "darwin" else mem + + def _test_leak(self, core: Callable[[], None]) -> None: + start_mem = self._get_mem_usage() + for cycle in range(self.iterations): + core() + mem = self._get_mem_usage() - start_mem + msg = f"memory usage limit exceeded in iteration {cycle}" + assert mem < self.mem_limit, msg + + +# helpers + + +def fromstring(data: bytes) -> Image.Image: + return Image.open(BytesIO(data)) + + +def tostring(im: Image.Image, string_format: str, **options: Any) -> bytes: + out = BytesIO() + im.save(out, string_format, **options) + return out.getvalue() + + +def hopper(mode: str | None = None) -> Image.Image: + # Use caching to reduce reading from disk, but return a copy + # so that the cached image isn't modified by the tests + # (for fast, isolated, repeatable tests). + + if mode is None: + # Always return fresh not-yet-loaded version of image. + # Operations on not-yet-loaded images are a separate class of errors + # that we should catch. + return Image.open("Tests/images/hopper.ppm") + + return _cached_hopper(mode).copy() + + +@lru_cache +def _cached_hopper(mode: str) -> Image.Image: + if mode == "F": + im = hopper("L") + else: + im = hopper() + if mode.startswith("BGR;"): + with pytest.warns(DeprecationWarning): + im = im.convert(mode) + else: + try: + im = im.convert(mode) + except ImportError: + if mode == "LAB": + im = Image.open("Tests/images/hopper.Lab.tif") + else: + raise + return im + + +def djpeg_available() -> bool: + if shutil.which("djpeg"): + try: + subprocess.check_call(["djpeg", "-version"]) + return True + except subprocess.CalledProcessError: # pragma: no cover + return False + return False + + +def cjpeg_available() -> bool: + if shutil.which("cjpeg"): + try: + subprocess.check_call(["cjpeg", "-version"]) + return True + except subprocess.CalledProcessError: # pragma: no cover + return False + return False + + +def netpbm_available() -> bool: + return bool(shutil.which("ppmquant") and shutil.which("ppmtogif")) + + +def magick_command() -> list[str] | None: + if sys.platform == "win32": + magickhome = os.environ.get("MAGICK_HOME") + if magickhome: + imagemagick = [os.path.join(magickhome, "convert.exe")] + graphicsmagick = [os.path.join(magickhome, "gm.exe"), "convert"] + else: + imagemagick = None + graphicsmagick = None + else: + imagemagick = ["convert"] + graphicsmagick = ["gm", "convert"] + + if imagemagick and shutil.which(imagemagick[0]): + return imagemagick + if graphicsmagick and shutil.which(graphicsmagick[0]): + return graphicsmagick + 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 + + +def is_big_endian() -> bool: + return sys.byteorder == "big" + + +def is_ppc64le() -> bool: + import platform + + return platform.machine() == "ppc64le" + + +def is_win32() -> bool: + return sys.platform.startswith("win32") + + +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 + + def __get__(self, instance: Any, cls: type[Any] | None = None) -> Any: + result = instance.__dict__[self.func.__name__] = self.func(instance) + return result diff --git a/Tests/icc/LICENSE.txt b/Tests/icc/LICENSE.txt index 7119461ed..38c4e1214 100644 --- a/Tests/icc/LICENSE.txt +++ b/Tests/icc/LICENSE.txt @@ -1,24 +1,24 @@ -from http://www.color.org/srgbprofiles.xalter - -Terms of use - -To anyone who acknowledges that the file "sRGB_v4_ICC_preference.icc" -is provided "AS IS" WITH NO EXPRESS OR IMPLIED WARRANTY, permission -to use, copy and distribute this file for any purpose is hereby -granted without fee, provided that the file is not changed including -the ICC copyright notice tag, and that the name of ICC shall not be -used in advertising or publicity pertaining to distribution of the -software without specific, written prior permission. ICC makes no -representations about the suitability of this software for any -purpose. - - -To anyone who acknowledges that the file -"sRGB_IEC61966-2-1_black_scaled.icc" is provided "AS IS" WITH NO -EXPRESS OR IMPLIED WARRANTY, permission to use, copy and distribute -these file for any purpose is hereby granted without fee, provided -that the file is not changed including the ICC copyright notice tag, -and that the name of ICC shall not be used in advertising or publicity -pertaining to distribution of the software without specific, written -prior permission. ICC makes no representations about the suitability -of this software for any purpose. +from http://www.color.org/srgbprofiles.xalter + +Terms of use + +To anyone who acknowledges that the file "sRGB_v4_ICC_preference.icc" +is provided "AS IS" WITH NO EXPRESS OR IMPLIED WARRANTY, permission +to use, copy and distribute this file for any purpose is hereby +granted without fee, provided that the file is not changed including +the ICC copyright notice tag, and that the name of ICC shall not be +used in advertising or publicity pertaining to distribution of the +software without specific, written prior permission. ICC makes no +representations about the suitability of this software for any +purpose. + + +To anyone who acknowledges that the file +"sRGB_IEC61966-2-1_black_scaled.icc" is provided "AS IS" WITH NO +EXPRESS OR IMPLIED WARRANTY, permission to use, copy and distribute +these file for any purpose is hereby granted without fee, provided +that the file is not changed including the ICC copyright notice tag, +and that the name of ICC shall not be used in advertising or publicity +pertaining to distribution of the software without specific, written +prior permission. ICC makes no representations about the suitability +of this software for any purpose. diff --git a/Tests/images/bad_palette_entry.gpl b/Tests/images/bad_palette_entry.gpl index 162037184..cab310f04 100644 --- a/Tests/images/bad_palette_entry.gpl +++ b/Tests/images/bad_palette_entry.gpl @@ -1,12 +1,12 @@ -GIMP Palette -Name: badpaletteentry -Columns: 4 -# - 0 0 0 Index 3 - 65 38 -103 62 49 Index 6 - 79 73 72 Index 7 -114 101 97 Index 8 -208 127 100 Index 9 -151 144 142 Index 10 -221 207 199 Index 11 +GIMP Palette +Name: badpaletteentry +Columns: 4 +# + 0 0 0 Index 3 + 65 38 +103 62 49 Index 6 + 79 73 72 Index 7 +114 101 97 Index 8 +208 127 100 Index 9 +151 144 142 Index 10 +221 207 199 Index 11 diff --git a/Tests/images/bad_palette_file.gpl b/Tests/images/bad_palette_file.gpl index c366cc8db..125e113fa 100644 --- a/Tests/images/bad_palette_file.gpl +++ b/Tests/images/bad_palette_file.gpl @@ -1,12 +1,12 @@ -GIMP Palette -Name: badpalettefile -Columns: 4 -# - 0 0 0 Index 3 -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789 -103 62 49 Index 6 - 79 73 72 Index 7 -114 101 97 Index 8 -208 127 100 Index 9 -151 144 142 Index 10 -221 207 199 Index 11 +GIMP Palette +Name: badpalettefile +Columns: 4 +# + 0 0 0 Index 3 +01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789 +103 62 49 Index 6 + 79 73 72 Index 7 +114 101 97 Index 8 +208 127 100 Index 9 +151 144 142 Index 10 +221 207 199 Index 11 diff --git a/Tests/images/bmp/README.txt b/Tests/images/bmp/README.txt index 4829834f4..c6d599e7e 100644 --- a/Tests/images/bmp/README.txt +++ b/Tests/images/bmp/README.txt @@ -1,3 +1,3 @@ -These images are from the bmpsuite: -https://github.com/jsummers/bmpsuite and are in the public domain -according to the readme in the project. +These images are from the bmpsuite: +https://github.com/jsummers/bmpsuite and are in the public domain +according to the readme in the project. diff --git a/Tests/images/bmp/html/bmpsuite.html b/Tests/images/bmp/html/bmpsuite.html index b8e327ed9..702e18e1a 100644 --- a/Tests/images/bmp/html/bmpsuite.html +++ b/Tests/images/bmp/html/bmpsuite.html @@ -1,578 +1,578 @@ - - - - - -BMP Suite Image List - - - - - - - -

BMP Suite Image List

- -

For BMP Suite -version 2.3

- -

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

- -

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

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

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

- or
8-bit version of q/pal4rletrns.bmp.
g/pal8w126.bmp3Images with different widths and heights. - In BMP format, rows are padded to a multiple of four bytes, so we - test all four possibilities.
g/pal8w125.bmp3
g/pal8w124.bmp3
g/pal8topdown.bmp3BMP images are normally stored from the bottom up, but - there is a way to store them from the top down.
q/pal8offs.bmp3A file with some unused bytes between the palette and the - image. This is probably valid, but I’m not 100% sure.
q/pal8oversizepal.bmp3An 8-bit image with 300 palette colors. This may be invalid, - because the documentation could - be interpreted to imply that 8-bit images aren’t allowed - to have more than 256 colors.
g/pal8nonsquare.bmp3 -
- or
- -
An image with non-square pixels: the X pixels/meter is twice - the Y pixels/meter. Image editors can be expected to - leave the image “squashed”; image viewers should - consider stretching it to its correct proportions.
g/pal8os2.bmpOS/2v1An OS/2-style bitmap.
q/pal8os2sp.bmpOS/2v1An OS/2v1 with a less-than-full-sized palette. - Probably not valid, but such files have been seen in the wild.
q/pal8os2v2.bmpOS/2v2My attempt to make an OS/2v2 bitmap.
q/pal8os2v2-16.bmpOS/2v2An OS/2v2 bitmap whose header has only 16 bytes, instead of the full 64.
g/pal8v4.bmp4A v4 bitmap. I’m not sure that the gamma and chromaticity values in - this file are sensible, because I can’t find any detailed documentation - of them.
g/pal8v5.bmp5A v5 bitmap. Version 5 has additional colorspace options over v4, so it - is easier to create, and ought to be more portable.
g/rgb16.bmp3A 16-bit image with the default color format: 5 bits each for red, - green, and blue, and 1 unused bit. - The whitest colors should (I assume) be displayed as pure white: - (255,255,255), not - (248,248,248).
g/rgb16-565.bmp3A 16-bit image with a BITFIELDS segment indicating 5 red, 6 green, - and 5 blue bits. This is a standard 16-bit format, even supported by - old versions of Windows that don’t support any other non-default 16-bit - formats. - The whitest colors should be displayed as pure white: - (255,255,255), not - (248,252,248).
g/rgb16-565pal.bmp3A 16-bit image with both a BITFIELDS segment and a palette.
q/rgb16-231.bmp3An unusual and silly 16-bit image, with 2 red bits, 3 green bits, and 1 - blue bit. Most viewers do support this image, but the colors may be darkened - with a yellow-green shadow. That’s because they’re doing simple - bit-shifting (possibly including one round of bit replication), instead of - proper scaling.
q/rgba16-4444.bmp5A 16-bit image with an alpha channel. There are 4 bits for each color - channel, and 4 bits for the alpha channel. - It’s not clear if this is valid, but I can’t find anything that - suggests it isn’t. -
g/rgb24.bmp3A perfectly ordinary 24-bit (truecolor) image.
g/rgb24pal.bmp3A 24-bit image, with a palette containing 256 colors. There is little if - any reason for a truecolor image to contain a palette, but it is legal.
q/rgb24largepal.bmp3A 24-bit image, with a palette containing 300 colors. - The fact that the palette has more than 256 colors may cause some viewers - to complain, but the documentation does not mention a size limit.
q/rgb24prof.bmp5My attempt to make a BMP file with an embedded color profile.
q/rgb24lprof.bmp5My attempt to make a BMP file with a linked color profile.
q/rgb24jpeg.bmp5My attempt to make BMP files with embedded JPEG and PNG images. - These are not likely to be supported by much of anything (they’re - intended for printers).
q/rgb24png.bmp5
g/rgb32.bmp3A 32-bit image using the default color format for 32-bit images (no - BITFIELDS segment). There are 8 bits per color channel, and 8 unused - bits. The unused bits are set to 0.
g/rgb32bf.bmp3A 32-bit image with a BITFIELDS segment. As usual, there are 8 bits per - color channel, and 8 unused bits. But the color channels are in an unusual - order, so the viewer must read the BITFIELDS, and not just guess.
q/rgb32fakealpha.bmp3
- or
- -
Same as g/rgb32.bmp, except that the unused bits are set to something - other than 0. - If the image becomes transparent toward the bottom, it probably means - the viewer uses heuristics to guess whether the undefined - data represents transparency.
q/rgb32-111110.bmp3A 32 bits/pixel image, with all 32 bits used: 11 each for red and - green, and 10 for blue. As far as I know, this is perfectly valid, but it - is unusual.
q/rgba32.bmp5A BMP with an alpha channel. Transparency is barely documented, - so it’s possible that this file is not correctly formed. - The color channels are in an unusual order, to prevent viewers from - passing this test by making a lucky guess.
q/rgba32abf.bmp3An image of type BI_ALHPABITFIELDS. Supposedly, this was used on - Windows CE. I don’t know whether it is constructed correctly.
b/badbitcount.bmp3N/AHeader indicates an absurdly large number of bits/pixel.
b/badbitssize.bmp3N/AHeader incorrectly indicates that the bitmap is several GB in size.
b/baddens1.bmp3N/ADensity (pixels per meter) suggests the image is much - larger in one dimension than the other.
b/baddens2.bmp3N/A
b/badfilesize.bmp3N/AHeader incorrectly indicates that the file is several GB in size.
b/badheadersize.bmp?N/AHeader size is 66 bytes, which is not a valid size for any known BMP - version.
b/badpalettesize.bmp3N/AHeader incorrectly indicates that the palette contains an absurdly large - number of colors.
b/badplanes.bmp3N/AThe “planes” setting, which is required to be 1, is not 1.
b/badrle.bmp3N/AAn invalid RLE-compressed image that tries to cause buffer overruns.
b/badwidth.bmp3N/AThe image claims to be a negative number of pixels in width.
b/pal8badindex.bmp3N/AMany of the palette indices used in the image are not present in the - palette.
b/reallybig.bmp3N/AAn image with a very large reported width and height.
b/rletopdown.bmp3N/AAn RLE-compressed image that tries to use top-down orientation, - which isn’t allowed.
b/shortfile.bmp3N/AA file that has been truncated in the middle of the bitmap.
- - - - + + + + + +BMP Suite Image List + + + + + + + +

BMP Suite Image List

+ +

For BMP Suite +version 2.3

+ +

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

+ +

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

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

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

+ or
8-bit version of q/pal4rletrns.bmp.
g/pal8w126.bmp3Images with different widths and heights. + In BMP format, rows are padded to a multiple of four bytes, so we + test all four possibilities.
g/pal8w125.bmp3
g/pal8w124.bmp3
g/pal8topdown.bmp3BMP images are normally stored from the bottom up, but + there is a way to store them from the top down.
q/pal8offs.bmp3A file with some unused bytes between the palette and the + image. This is probably valid, but I’m not 100% sure.
q/pal8oversizepal.bmp3An 8-bit image with 300 palette colors. This may be invalid, + because the documentation could + be interpreted to imply that 8-bit images aren’t allowed + to have more than 256 colors.
g/pal8nonsquare.bmp3 +
+ or
+ +
An image with non-square pixels: the X pixels/meter is twice + the Y pixels/meter. Image editors can be expected to + leave the image “squashed”; image viewers should + consider stretching it to its correct proportions.
g/pal8os2.bmpOS/2v1An OS/2-style bitmap.
q/pal8os2sp.bmpOS/2v1An OS/2v1 with a less-than-full-sized palette. + Probably not valid, but such files have been seen in the wild.
q/pal8os2v2.bmpOS/2v2My attempt to make an OS/2v2 bitmap.
q/pal8os2v2-16.bmpOS/2v2An OS/2v2 bitmap whose header has only 16 bytes, instead of the full 64.
g/pal8v4.bmp4A v4 bitmap. I’m not sure that the gamma and chromaticity values in + this file are sensible, because I can’t find any detailed documentation + of them.
g/pal8v5.bmp5A v5 bitmap. Version 5 has additional colorspace options over v4, so it + is easier to create, and ought to be more portable.
g/rgb16.bmp3A 16-bit image with the default color format: 5 bits each for red, + green, and blue, and 1 unused bit. + The whitest colors should (I assume) be displayed as pure white: + (255,255,255), not + (248,248,248).
g/rgb16-565.bmp3A 16-bit image with a BITFIELDS segment indicating 5 red, 6 green, + and 5 blue bits. This is a standard 16-bit format, even supported by + old versions of Windows that don’t support any other non-default 16-bit + formats. + The whitest colors should be displayed as pure white: + (255,255,255), not + (248,252,248).
g/rgb16-565pal.bmp3A 16-bit image with both a BITFIELDS segment and a palette.
q/rgb16-231.bmp3An unusual and silly 16-bit image, with 2 red bits, 3 green bits, and 1 + blue bit. Most viewers do support this image, but the colors may be darkened + with a yellow-green shadow. That’s because they’re doing simple + bit-shifting (possibly including one round of bit replication), instead of + proper scaling.
q/rgba16-4444.bmp5A 16-bit image with an alpha channel. There are 4 bits for each color + channel, and 4 bits for the alpha channel. + It’s not clear if this is valid, but I can’t find anything that + suggests it isn’t. +
g/rgb24.bmp3A perfectly ordinary 24-bit (truecolor) image.
g/rgb24pal.bmp3A 24-bit image, with a palette containing 256 colors. There is little if + any reason for a truecolor image to contain a palette, but it is legal.
q/rgb24largepal.bmp3A 24-bit image, with a palette containing 300 colors. + The fact that the palette has more than 256 colors may cause some viewers + to complain, but the documentation does not mention a size limit.
q/rgb24prof.bmp5My attempt to make a BMP file with an embedded color profile.
q/rgb24lprof.bmp5My attempt to make a BMP file with a linked color profile.
q/rgb24jpeg.bmp5My attempt to make BMP files with embedded JPEG and PNG images. + These are not likely to be supported by much of anything (they’re + intended for printers).
q/rgb24png.bmp5
g/rgb32.bmp3A 32-bit image using the default color format for 32-bit images (no + BITFIELDS segment). There are 8 bits per color channel, and 8 unused + bits. The unused bits are set to 0.
g/rgb32bf.bmp3A 32-bit image with a BITFIELDS segment. As usual, there are 8 bits per + color channel, and 8 unused bits. But the color channels are in an unusual + order, so the viewer must read the BITFIELDS, and not just guess.
q/rgb32fakealpha.bmp3
+ or
+ +
Same as g/rgb32.bmp, except that the unused bits are set to something + other than 0. + If the image becomes transparent toward the bottom, it probably means + the viewer uses heuristics to guess whether the undefined + data represents transparency.
q/rgb32-111110.bmp3A 32 bits/pixel image, with all 32 bits used: 11 each for red and + green, and 10 for blue. As far as I know, this is perfectly valid, but it + is unusual.
q/rgba32.bmp5A BMP with an alpha channel. Transparency is barely documented, + so it’s possible that this file is not correctly formed. + The color channels are in an unusual order, to prevent viewers from + passing this test by making a lucky guess.
q/rgba32abf.bmp3An image of type BI_ALHPABITFIELDS. Supposedly, this was used on + Windows CE. I don’t know whether it is constructed correctly.
b/badbitcount.bmp3N/AHeader indicates an absurdly large number of bits/pixel.
b/badbitssize.bmp3N/AHeader incorrectly indicates that the bitmap is several GB in size.
b/baddens1.bmp3N/ADensity (pixels per meter) suggests the image is much + larger in one dimension than the other.
b/baddens2.bmp3N/A
b/badfilesize.bmp3N/AHeader incorrectly indicates that the file is several GB in size.
b/badheadersize.bmp?N/AHeader size is 66 bytes, which is not a valid size for any known BMP + version.
b/badpalettesize.bmp3N/AHeader incorrectly indicates that the palette contains an absurdly large + number of colors.
b/badplanes.bmp3N/AThe “planes” setting, which is required to be 1, is not 1.
b/badrle.bmp3N/AAn invalid RLE-compressed image that tries to cause buffer overruns.
b/badwidth.bmp3N/AThe image claims to be a negative number of pixels in width.
b/pal8badindex.bmp3N/AMany of the palette indices used in the image are not present in the + palette.
b/reallybig.bmp3N/AAn image with a very large reported width and height.
b/rletopdown.bmp3N/AAn RLE-compressed image that tries to use top-down orientation, + which isn’t allowed.
b/shortfile.bmp3N/AA file that has been truncated in the middle of the bitmap.
+ + + + diff --git a/Tests/images/courB08.bdf b/Tests/images/courB08.bdf index 2be6ab6b9..f23bb8fb6 100644 --- a/Tests/images/courB08.bdf +++ b/Tests/images/courB08.bdf @@ -1,2676 +1,2676 @@ -STARTFONT 2.1 -COMMENT $XConsortium: courB08.bdf,v 1.5 94/04/10 21:46:11 gildea Exp $ -COMMENT -COMMENT Copyright 1984, 1987 Adobe Systems, Inc. -COMMENT Portions Copyright 1988 Digital Equipment Corporation -COMMENT -COMMENT Adobe is a registered trademark of Adobe Systems, Inc. Permission -COMMENT to use these trademarks is hereby granted only in association with the -COMMENT images described in this file. -COMMENT -COMMENT Permission to use, copy, modify, and distribute this software and -COMMENT its documentation for any purpose and without fee is hereby granted, -COMMENT provided that the above copyright notices appear in all copies and -COMMENT that both those copyright notices and this permission notice appear -COMMENT in supporting documentation, and that the names of Adobe Systems and -COMMENT Digital Equipment Corporation not be used in advertising or -COMMENT publicity pertaining to distribution of the software without -COMMENT specific, written prior permission. Adobe Systems and Digital -COMMENT Equipment Corporation make no representations about the suitability -COMMENT of this software for any purpose. It is provided "as is" without -COMMENT express or implied warranty. -COMMENT -COMMENT ADOBE SYSTEMS AND DIGITAL EQUIPMENT CORPORATION DISCLAIM ALL -COMMENT WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED -COMMENT WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL ADOBE -COMMENT SYSTEMS AND DIGITAL EQUIPMENT CORPORATION BE LIABLE FOR ANY SPECIAL, -COMMENT INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER -COMMENT RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF -COMMENT CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN -COMMENT CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -COMMENT -COMMENT -FONT -Adobe-Courier-Bold-R-Normal--11-80-100-100-M-60-ISO8859-1 -SIZE 8 100 100 -FONTBOUNDINGBOX 8 11 -1 -2 -STARTPROPERTIES 22 -COMMENT Begin LogicalFontDescription -FONTNAME_REGISTRY "" -FAMILY_NAME "Courier" -FOUNDRY "Adobe" -WEIGHT_NAME "Bold" -SLANT "R" -SETWIDTH_NAME "Normal" -ADD_STYLE_NAME "" -PIXEL_SIZE 11 -POINT_SIZE 80 -RESOLUTION_X 100 -RESOLUTION_Y 100 -SPACING "M" -AVERAGE_WIDTH 60 -CHARSET_REGISTRY "ISO8859" -CHARSET_ENCODING "1" -COMMENT END LogicalFontDescription -CHARSET_COLLECTIONS "ASCII ISO8859-1 ADOBE-STANDARD" -FACE_NAME "Courier Bold" -COPYRIGHT "Copyright (c) 1987 Adobe Systems, Inc., Portions Copyright 1988 Digital Equipment Corp." -COMMENT ***** end of inserted font properties -FONT_ASCENT 8 -FONT_DESCENT 2 -CAP_HEIGHT 6 -X_HEIGHT 5 -ENDPROPERTIES -CHARS 194 -STARTCHAR space -ENCODING 32 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 1 1 0 0 -BITMAP -00 -ENDCHAR -STARTCHAR exclam -ENCODING 33 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 2 6 1 0 -BITMAP -C0 -C0 -C0 -C0 -00 -C0 -ENDCHAR -STARTCHAR quotedbl -ENCODING 34 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 3 3 1 3 -BITMAP -A0 -A0 -A0 -ENDCHAR -STARTCHAR numbersign -ENCODING 35 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 8 0 -1 -BITMAP -50 -50 -F8 -50 -50 -F8 -50 -50 -ENDCHAR -STARTCHAR dollar -ENCODING 36 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 9 0 -1 -BITMAP -20 -78 -C8 -F0 -78 -18 -D8 -F0 -20 -ENDCHAR -STARTCHAR percent -ENCODING 37 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 7 0 0 -BITMAP -E0 -A8 -F0 -20 -78 -A8 -38 -ENDCHAR -STARTCHAR ampersand -ENCODING 38 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 6 -1 0 -BITMAP -38 -60 -30 -7C -D8 -7C -ENDCHAR -STARTCHAR quoteright -ENCODING 39 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 3 3 1 4 -BITMAP -60 -40 -80 -ENDCHAR -STARTCHAR parenleft -ENCODING 40 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 3 8 1 -1 -BITMAP -20 -40 -C0 -C0 -C0 -C0 -40 -20 -ENDCHAR -STARTCHAR parenright -ENCODING 41 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 3 8 1 -1 -BITMAP -80 -40 -60 -60 -60 -60 -40 -80 -ENDCHAR -STARTCHAR asterisk -ENCODING 42 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 4 4 0 3 -BITMAP -20 -F0 -60 -90 -ENDCHAR -STARTCHAR plus -ENCODING 43 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 5 0 1 -BITMAP -20 -20 -F8 -20 -20 -ENDCHAR -STARTCHAR comma -ENCODING 44 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 3 3 1 -2 -BITMAP -60 -40 -80 -ENDCHAR -STARTCHAR minus -ENCODING 45 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 1 0 3 -BITMAP -F8 -ENDCHAR -STARTCHAR period -ENCODING 46 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 2 1 1 0 -BITMAP -C0 -ENDCHAR -STARTCHAR slash -ENCODING 47 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 4 8 1 -1 -BITMAP -10 -10 -20 -20 -40 -40 -80 -80 -ENDCHAR -STARTCHAR zero -ENCODING 48 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 7 0 0 -BITMAP -70 -D8 -D8 -D8 -D8 -D8 -70 -ENDCHAR -STARTCHAR one -ENCODING 49 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 7 0 0 -BITMAP -30 -F0 -30 -30 -30 -30 -FC -ENDCHAR -STARTCHAR two -ENCODING 50 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 7 0 0 -BITMAP -70 -D8 -18 -30 -60 -D8 -F8 -ENDCHAR -STARTCHAR three -ENCODING 51 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 7 0 0 -BITMAP -70 -D8 -18 -70 -18 -D8 -70 -ENDCHAR -STARTCHAR four -ENCODING 52 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 7 0 0 -BITMAP -18 -38 -58 -D8 -FC -18 -18 -ENDCHAR -STARTCHAR five -ENCODING 53 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 7 0 0 -BITMAP -F8 -C0 -F0 -D8 -18 -98 -F0 -ENDCHAR -STARTCHAR six -ENCODING 54 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 7 0 0 -BITMAP -70 -D8 -C0 -F0 -D8 -D8 -70 -ENDCHAR -STARTCHAR seven -ENCODING 55 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 7 0 0 -BITMAP -F8 -D8 -18 -30 -30 -60 -60 -ENDCHAR -STARTCHAR eight -ENCODING 56 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 7 0 0 -BITMAP -70 -D8 -D8 -70 -D8 -D8 -70 -ENDCHAR -STARTCHAR nine -ENCODING 57 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 7 0 0 -BITMAP -70 -D8 -D8 -78 -18 -D8 -70 -ENDCHAR -STARTCHAR colon -ENCODING 58 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 2 4 1 0 -BITMAP -C0 -00 -00 -C0 -ENDCHAR -STARTCHAR semicolon -ENCODING 59 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 3 6 0 -2 -BITMAP -60 -00 -00 -60 -40 -80 -ENDCHAR -STARTCHAR less -ENCODING 60 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 4 5 0 1 -BITMAP -30 -60 -C0 -60 -30 -ENDCHAR -STARTCHAR equal -ENCODING 61 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 4 3 0 2 -BITMAP -F0 -00 -F0 -ENDCHAR -STARTCHAR greater -ENCODING 62 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 4 5 1 1 -BITMAP -C0 -60 -30 -60 -C0 -ENDCHAR -STARTCHAR question -ENCODING 63 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 6 0 0 -BITMAP -70 -98 -30 -60 -00 -60 -ENDCHAR -STARTCHAR at -ENCODING 64 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 8 0 -1 -BITMAP -70 -C8 -98 -A8 -A8 -9C -C0 -70 -ENDCHAR -STARTCHAR A -ENCODING 65 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 7 6 -1 0 -BITMAP -78 -38 -28 -7C -6C -EE -ENDCHAR -STARTCHAR B -ENCODING 66 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 6 -1 0 -BITMAP -F8 -6C -78 -6C -6C -F8 -ENDCHAR -STARTCHAR C -ENCODING 67 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 6 0 0 -BITMAP -78 -D8 -C0 -C0 -D8 -70 -ENDCHAR -STARTCHAR D -ENCODING 68 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 6 -1 0 -BITMAP -F8 -6C -6C -6C -6C -F8 -ENDCHAR -STARTCHAR E -ENCODING 69 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 6 -1 0 -BITMAP -FC -60 -78 -60 -6C -FC -ENDCHAR -STARTCHAR F -ENCODING 70 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 6 -1 0 -BITMAP -FC -60 -78 -60 -60 -F0 -ENDCHAR -STARTCHAR G -ENCODING 71 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 6 0 0 -BITMAP -70 -D8 -C0 -F8 -D8 -78 -ENDCHAR -STARTCHAR H -ENCODING 72 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 7 6 -1 0 -BITMAP -EE -6C -7C -6C -6C -EE -ENDCHAR -STARTCHAR I -ENCODING 73 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 4 6 0 0 -BITMAP -F0 -60 -60 -60 -60 -F0 -ENDCHAR -STARTCHAR J -ENCODING 74 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 6 -1 0 -BITMAP -3C -18 -18 -D8 -D8 -70 -ENDCHAR -STARTCHAR K -ENCODING 75 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 7 6 -1 0 -BITMAP -EC -68 -70 -78 -6C -F6 -ENDCHAR -STARTCHAR L -ENCODING 76 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 6 -1 0 -BITMAP -F0 -60 -60 -60 -6C -FC -ENDCHAR -STARTCHAR M -ENCODING 77 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 6 -1 0 -BITMAP -C4 -6C -6C -7C -54 -D4 -ENDCHAR -STARTCHAR N -ENCODING 78 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 7 6 -1 0 -BITMAP -EE -74 -74 -6C -6C -E4 -ENDCHAR -STARTCHAR O -ENCODING 79 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 6 0 0 -BITMAP -70 -D8 -D8 -D8 -D8 -70 -ENDCHAR -STARTCHAR P -ENCODING 80 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 6 -1 0 -BITMAP -F8 -6C -6C -78 -60 -F0 -ENDCHAR -STARTCHAR Q -ENCODING 81 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 7 0 -1 -BITMAP -70 -D8 -D8 -D8 -D8 -70 -18 -ENDCHAR -STARTCHAR R -ENCODING 82 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 7 6 -1 0 -BITMAP -F8 -6C -6C -78 -6C -F6 -ENDCHAR -STARTCHAR S -ENCODING 83 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 6 0 0 -BITMAP -78 -C8 -F0 -38 -98 -F0 -ENDCHAR -STARTCHAR T -ENCODING 84 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 6 -1 0 -BITMAP -FC -B4 -30 -30 -30 -78 -ENDCHAR -STARTCHAR U -ENCODING 85 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 7 6 -1 0 -BITMAP -EE -6C -6C -6C -6C -38 -ENDCHAR -STARTCHAR V -ENCODING 86 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 7 6 -1 0 -BITMAP -EE -6C -28 -38 -38 -10 -ENDCHAR -STARTCHAR W -ENCODING 87 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 7 6 -1 0 -BITMAP -D6 -54 -54 -7C -38 -28 -ENDCHAR -STARTCHAR X -ENCODING 88 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 6 0 0 -BITMAP -CC -78 -30 -30 -78 -CC -ENDCHAR -STARTCHAR Y -ENCODING 89 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 7 6 -1 0 -BITMAP -E6 -66 -3C -18 -18 -3C -ENDCHAR -STARTCHAR Z -ENCODING 90 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 6 0 0 -BITMAP -F8 -D8 -30 -60 -D8 -F8 -ENDCHAR -STARTCHAR bracketleft -ENCODING 91 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 3 8 1 -1 -BITMAP -E0 -C0 -C0 -C0 -C0 -C0 -C0 -E0 -ENDCHAR -STARTCHAR backslash -ENCODING 92 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 4 8 0 -1 -BITMAP -80 -80 -40 -40 -20 -20 -10 -10 -ENDCHAR -STARTCHAR bracketright -ENCODING 93 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 3 8 1 -1 -BITMAP -E0 -60 -60 -60 -60 -60 -60 -E0 -ENDCHAR -STARTCHAR asciicircum -ENCODING 94 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 3 0 4 -BITMAP -20 -70 -D8 -ENDCHAR -STARTCHAR underscore -ENCODING 95 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 1 0 -2 -BITMAP -FC -ENDCHAR -STARTCHAR quoteleft -ENCODING 96 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 3 3 1 4 -BITMAP -C0 -40 -20 -ENDCHAR -STARTCHAR a -ENCODING 97 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 5 0 0 -BITMAP -70 -D8 -78 -D8 -FC -ENDCHAR -STARTCHAR b -ENCODING 98 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 7 -1 0 -BITMAP -E0 -60 -78 -6C -6C -6C -F8 -ENDCHAR -STARTCHAR c -ENCODING 99 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 5 0 0 -BITMAP -70 -D8 -C0 -D8 -70 -ENDCHAR -STARTCHAR d -ENCODING 100 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 7 0 0 -BITMAP -38 -18 -78 -D8 -D8 -D8 -7C -ENDCHAR -STARTCHAR e -ENCODING 101 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 5 0 0 -BITMAP -70 -D8 -F8 -C0 -78 -ENDCHAR -STARTCHAR f -ENCODING 102 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 7 0 0 -BITMAP -38 -60 -F8 -60 -60 -60 -F8 -ENDCHAR -STARTCHAR g -ENCODING 103 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 7 0 -2 -BITMAP -6C -D8 -D8 -D8 -78 -18 -F0 -ENDCHAR -STARTCHAR h -ENCODING 104 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 7 -1 0 -BITMAP -E0 -60 -78 -6C -6C -6C -6C -ENDCHAR -STARTCHAR i -ENCODING 105 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 7 0 0 -BITMAP -30 -00 -F0 -30 -30 -30 -FC -ENDCHAR -STARTCHAR j -ENCODING 106 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 4 9 0 -2 -BITMAP -30 -00 -F0 -30 -30 -30 -30 -30 -E0 -ENDCHAR -STARTCHAR k -ENCODING 107 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 7 7 -1 0 -BITMAP -E0 -60 -6C -78 -70 -78 -6E -ENDCHAR -STARTCHAR l -ENCODING 108 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 7 0 0 -BITMAP -F0 -30 -30 -30 -30 -30 -FC -ENDCHAR -STARTCHAR m -ENCODING 109 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 5 -1 0 -BITMAP -F8 -7C -54 -54 -54 -ENDCHAR -STARTCHAR n -ENCODING 110 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 5 -1 0 -BITMAP -D8 -6C -6C -6C -6C -ENDCHAR -STARTCHAR o -ENCODING 111 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 5 0 0 -BITMAP -70 -D8 -D8 -D8 -70 -ENDCHAR -STARTCHAR p -ENCODING 112 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 7 -1 -2 -BITMAP -F8 -6C -6C -6C -78 -60 -F0 -ENDCHAR -STARTCHAR q -ENCODING 113 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 7 0 -2 -BITMAP -6C -D8 -D8 -D8 -78 -18 -3C -ENDCHAR -STARTCHAR r -ENCODING 114 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 5 0 0 -BITMAP -DC -74 -60 -60 -F0 -ENDCHAR -STARTCHAR s -ENCODING 115 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 5 0 0 -BITMAP -78 -E0 -78 -1C -F8 -ENDCHAR -STARTCHAR t -ENCODING 116 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 7 0 0 -BITMAP -60 -60 -F8 -60 -60 -6C -38 -ENDCHAR -STARTCHAR u -ENCODING 117 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 7 5 -1 0 -BITMAP -EC -6C -6C -6C -3E -ENDCHAR -STARTCHAR v -ENCODING 118 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 5 -1 0 -BITMAP -EC -6C -38 -38 -10 -ENDCHAR -STARTCHAR w -ENCODING 119 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 7 5 -1 0 -BITMAP -D6 -54 -7C -3C -28 -ENDCHAR -STARTCHAR x -ENCODING 120 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 5 0 0 -BITMAP -EC -78 -30 -78 -DC -ENDCHAR -STARTCHAR y -ENCODING 121 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 7 7 -1 -2 -BITMAP -EE -6C -6C -28 -38 -30 -E0 -ENDCHAR -STARTCHAR z -ENCODING 122 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 5 0 0 -BITMAP -F8 -B0 -60 -D8 -F8 -ENDCHAR -STARTCHAR braceleft -ENCODING 123 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 4 8 1 -1 -BITMAP -30 -60 -60 -C0 -60 -60 -60 -30 -ENDCHAR -STARTCHAR bar -ENCODING 124 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 1 7 2 -1 -BITMAP -80 -80 -80 -80 -80 -80 -80 -ENDCHAR -STARTCHAR braceright -ENCODING 125 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 4 8 0 -1 -BITMAP -C0 -60 -60 -30 -60 -60 -60 -C0 -ENDCHAR -STARTCHAR asciitilde -ENCODING 126 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 2 0 3 -BITMAP -68 -B0 -ENDCHAR -STARTCHAR exclamdown -ENCODING 161 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 2 7 1 -2 -BITMAP -C0 -00 -C0 -C0 -C0 -C0 -C0 -ENDCHAR -STARTCHAR cent -ENCODING 162 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 8 0 -1 -BITMAP -20 -20 -78 -C8 -C0 -78 -20 -20 -ENDCHAR -STARTCHAR sterling -ENCODING 163 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 7 0 0 -BITMAP -38 -68 -20 -F8 -20 -64 -F8 -ENDCHAR -STARTCHAR currency -ENCODING 164 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 5 0 1 -BITMAP -88 -70 -50 -70 -88 -ENDCHAR -STARTCHAR yen -ENCODING 165 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 7 0 0 -BITMAP -CC -48 -FC -30 -FC -30 -78 -ENDCHAR -STARTCHAR brokenbar -ENCODING 166 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 1 9 2 -2 -BITMAP -80 -80 -80 -80 -00 -80 -80 -80 -80 -ENDCHAR -STARTCHAR section -ENCODING 167 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 8 0 -1 -BITMAP -78 -48 -60 -90 -48 -30 -90 -F0 -ENDCHAR -STARTCHAR dieresis -ENCODING 168 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 3 1 1 5 -BITMAP -A0 -ENDCHAR -STARTCHAR copyright -ENCODING 169 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 7 0 0 -BITMAP -30 -48 -B4 -A4 -B4 -48 -30 -ENDCHAR -STARTCHAR ordfeminine -ENCODING 170 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 4 5 1 2 -BITMAP -E0 -10 -D0 -00 -F0 -ENDCHAR -STARTCHAR guillemotleft -ENCODING 171 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 7 5 -1 0 -BITMAP -36 -6C -D8 -6C -36 -ENDCHAR -STARTCHAR logicalnot -ENCODING 172 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 3 0 2 -BITMAP -F8 -08 -08 -ENDCHAR -STARTCHAR hyphen -ENCODING 173 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 1 0 3 -BITMAP -F8 -ENDCHAR -STARTCHAR registered -ENCODING 174 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 7 7 0 0 -BITMAP -38 -44 -BA -B2 -AA -44 -38 -ENDCHAR -STARTCHAR macron -ENCODING 175 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 4 1 0 5 -BITMAP -F0 -ENDCHAR -STARTCHAR degree -ENCODING 176 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 4 3 0 4 -BITMAP -60 -90 -60 -ENDCHAR -STARTCHAR plusminus -ENCODING 177 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 6 0 0 -BITMAP -20 -20 -F8 -20 -00 -F8 -ENDCHAR -STARTCHAR twosuperior -ENCODING 178 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 3 4 1 3 -BITMAP -60 -A0 -40 -E0 -ENDCHAR -STARTCHAR threesuperior -ENCODING 179 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 3 4 1 3 -BITMAP -E0 -40 -20 -C0 -ENDCHAR -STARTCHAR acute -ENCODING 180 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 2 2 2 5 -BITMAP -40 -80 -ENDCHAR -STARTCHAR mu -ENCODING 181 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 7 7 -1 -2 -BITMAP -EC -6C -6C -6C -7E -40 -40 -ENDCHAR -STARTCHAR paragraph -ENCODING 182 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 8 0 -1 -BITMAP -7C -A8 -A8 -68 -28 -28 -28 -6C -ENDCHAR -STARTCHAR periodcentered -ENCODING 183 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 2 1 1 3 -BITMAP -C0 -ENDCHAR -STARTCHAR cedilla -ENCODING 184 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 3 3 1 -2 -BITMAP -40 -20 -C0 -ENDCHAR -STARTCHAR onesuperior -ENCODING 185 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 3 4 1 3 -BITMAP -C0 -40 -40 -E0 -ENDCHAR -STARTCHAR ordmasculine -ENCODING 186 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 4 5 1 2 -BITMAP -60 -90 -60 -00 -F0 -ENDCHAR -STARTCHAR guillemotright -ENCODING 187 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 7 5 -1 0 -BITMAP -D8 -6C -36 -6C -D8 -ENDCHAR -STARTCHAR onequarter -ENCODING 188 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 7 7 -1 0 -BITMAP -C0 -44 -48 -F4 -2C -5E -04 -ENDCHAR -STARTCHAR onehalf -ENCODING 189 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 7 7 -1 0 -BITMAP -C0 -44 -48 -F6 -2A -44 -0E -ENDCHAR -STARTCHAR threequarters -ENCODING 190 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 7 7 -1 0 -BITMAP -E0 -44 -28 -D4 -2C -5E -04 -ENDCHAR -STARTCHAR questiondown -ENCODING 191 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 7 0 -2 -BITMAP -30 -00 -30 -30 -60 -C8 -70 -ENDCHAR -STARTCHAR Agrave -ENCODING 192 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 7 9 -1 0 -BITMAP -20 -10 -00 -78 -38 -28 -7C -6C -EE -ENDCHAR -STARTCHAR Aacute -ENCODING 193 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 7 9 -1 0 -BITMAP -10 -20 -00 -78 -38 -28 -7C -6C -EE -ENDCHAR -STARTCHAR Acircumflex -ENCODING 194 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 7 9 -1 0 -BITMAP -10 -28 -00 -78 -38 -28 -7C -6C -EE -ENDCHAR -STARTCHAR Atilde -ENCODING 195 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 7 9 -1 0 -BITMAP -34 -48 -00 -78 -38 -28 -7C -6C -EE -ENDCHAR -STARTCHAR Adieresis -ENCODING 196 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 7 8 -1 0 -BITMAP -28 -00 -78 -38 -28 -7C -6C -EE -ENDCHAR -STARTCHAR Aring -ENCODING 197 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 7 9 -1 0 -BITMAP -30 -48 -30 -78 -38 -28 -7C -6C -EE -ENDCHAR -STARTCHAR AE -ENCODING 198 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 7 6 -1 0 -BITMAP -7E -3A -6C -78 -DA -DE -ENDCHAR -STARTCHAR Ccedilla -ENCODING 199 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 8 0 -2 -BITMAP -78 -D8 -C0 -C0 -D8 -70 -10 -60 -ENDCHAR -STARTCHAR Egrave -ENCODING 200 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 9 -1 0 -BITMAP -20 -10 -00 -FC -64 -78 -60 -6C -FC -ENDCHAR -STARTCHAR Eacute -ENCODING 201 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 9 -1 0 -BITMAP -10 -20 -00 -FC -64 -78 -60 -6C -FC -ENDCHAR -STARTCHAR Ecircumflex -ENCODING 202 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 9 -1 0 -BITMAP -20 -50 -00 -FC -64 -78 -60 -6C -FC -ENDCHAR -STARTCHAR Edieresis -ENCODING 203 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 8 -1 0 -BITMAP -50 -00 -FC -64 -78 -60 -6C -FC -ENDCHAR -STARTCHAR Igrave -ENCODING 204 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 4 9 0 0 -BITMAP -40 -20 -00 -F0 -60 -60 -60 -60 -F0 -ENDCHAR -STARTCHAR Iacute -ENCODING 205 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 4 9 0 0 -BITMAP -20 -40 -00 -F0 -60 -60 -60 -60 -F0 -ENDCHAR -STARTCHAR Icircumflex -ENCODING 206 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 4 9 0 0 -BITMAP -40 -A0 -00 -F0 -60 -60 -60 -60 -F0 -ENDCHAR -STARTCHAR Idieresis -ENCODING 207 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 4 8 0 0 -BITMAP -A0 -00 -F0 -60 -60 -60 -60 -F0 -ENDCHAR -STARTCHAR Eth -ENCODING 208 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 6 -1 0 -BITMAP -F8 -6C -F4 -64 -6C -F8 -ENDCHAR -STARTCHAR Ntilde -ENCODING 209 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 7 9 -1 0 -BITMAP -34 -48 -00 -EE -64 -74 -7C -6C -EC -ENDCHAR -STARTCHAR Ograve -ENCODING 210 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 9 0 0 -BITMAP -40 -20 -00 -70 -D8 -D8 -D8 -D8 -70 -ENDCHAR -STARTCHAR Oacute -ENCODING 211 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 9 0 0 -BITMAP -20 -40 -00 -70 -D8 -D8 -D8 -D8 -70 -ENDCHAR -STARTCHAR Ocircumflex -ENCODING 212 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 9 0 0 -BITMAP -20 -50 -00 -70 -D8 -D8 -D8 -D8 -70 -ENDCHAR -STARTCHAR Otilde -ENCODING 213 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 9 0 0 -BITMAP -68 -90 -00 -70 -D8 -D8 -D8 -D8 -70 -ENDCHAR -STARTCHAR Odieresis -ENCODING 214 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 8 0 0 -BITMAP -50 -00 -70 -D8 -D8 -D8 -D8 -70 -ENDCHAR -STARTCHAR multiply -ENCODING 215 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 5 0 1 -BITMAP -88 -50 -20 -50 -88 -ENDCHAR -STARTCHAR Oslash -ENCODING 216 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 7 6 -1 0 -BITMAP -3A -6C -7C -6C -6C -B8 -ENDCHAR -STARTCHAR Ugrave -ENCODING 217 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 7 9 -1 0 -BITMAP -20 -10 -00 -EE -6C -6C -6C -6C -38 -ENDCHAR -STARTCHAR Uacute -ENCODING 218 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 7 9 -1 0 -BITMAP -08 -10 -00 -EE -6C -6C -6C -6C -38 -ENDCHAR -STARTCHAR Ucircumflex -ENCODING 219 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 7 9 -1 0 -BITMAP -10 -28 -00 -EE -6C -6C -6C -6C -38 -ENDCHAR -STARTCHAR Udieresis -ENCODING 220 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 7 8 -1 0 -BITMAP -28 -00 -EE -6C -6C -6C -6C -38 -ENDCHAR -STARTCHAR Yacute -ENCODING 221 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 7 9 -1 0 -BITMAP -04 -08 -00 -E6 -66 -3C -18 -18 -3C -ENDCHAR -STARTCHAR Thorn -ENCODING 222 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 6 -1 0 -BITMAP -E0 -78 -6C -6C -78 -E0 -ENDCHAR -STARTCHAR germandbls -ENCODING 223 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 7 6 -1 0 -BITMAP -38 -68 -7C -66 -66 -EC -ENDCHAR -STARTCHAR agrave -ENCODING 224 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 8 0 0 -BITMAP -20 -10 -00 -70 -98 -78 -D8 -FC -ENDCHAR -STARTCHAR aacute -ENCODING 225 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 8 0 0 -BITMAP -10 -20 -00 -70 -98 -78 -D8 -FC -ENDCHAR -STARTCHAR acircumflex -ENCODING 226 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 8 0 0 -BITMAP -20 -50 -00 -70 -98 -78 -D8 -FC -ENDCHAR -STARTCHAR atilde -ENCODING 227 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 8 0 0 -BITMAP -68 -90 -00 -70 -98 -78 -D8 -FC -ENDCHAR -STARTCHAR adieresis -ENCODING 228 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 7 0 0 -BITMAP -50 -00 -70 -98 -78 -D8 -FC -ENDCHAR -STARTCHAR aring -ENCODING 229 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 9 0 0 -BITMAP -30 -48 -30 -00 -70 -98 -78 -D8 -FC -ENDCHAR -STARTCHAR ae -ENCODING 230 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 5 -1 0 -BITMAP -6C -B4 -7C -B0 -DC -ENDCHAR -STARTCHAR ccedilla -ENCODING 231 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 7 0 -2 -BITMAP -70 -D8 -C0 -D8 -70 -10 -60 -ENDCHAR -STARTCHAR egrave -ENCODING 232 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 8 0 0 -BITMAP -40 -20 -00 -70 -D8 -F8 -C0 -78 -ENDCHAR -STARTCHAR eacute -ENCODING 233 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 8 0 0 -BITMAP -20 -40 -00 -70 -D8 -F8 -C0 -78 -ENDCHAR -STARTCHAR ecircumflex -ENCODING 234 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 8 0 0 -BITMAP -20 -50 -00 -70 -D8 -F8 -C0 -78 -ENDCHAR -STARTCHAR edieresis -ENCODING 235 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 7 0 0 -BITMAP -50 -00 -70 -D8 -F8 -C0 -78 -ENDCHAR -STARTCHAR igrave -ENCODING 236 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 8 0 0 -BITMAP -20 -10 -00 -70 -30 -30 -30 -FC -ENDCHAR -STARTCHAR iacute -ENCODING 237 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 8 0 0 -BITMAP -10 -20 -00 -70 -30 -30 -30 -FC -ENDCHAR -STARTCHAR icircumflex -ENCODING 238 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 8 0 0 -BITMAP -20 -50 -00 -70 -30 -30 -30 -FC -ENDCHAR -STARTCHAR idieresis -ENCODING 239 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 7 0 0 -BITMAP -50 -00 -70 -30 -30 -30 -FC -ENDCHAR -STARTCHAR eth -ENCODING 240 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 8 0 0 -BITMAP -D0 -60 -B0 -78 -D8 -D8 -D8 -70 -ENDCHAR -STARTCHAR ntilde -ENCODING 241 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 7 8 -1 0 -BITMAP -34 -48 -00 -D8 -6C -6C -6C -6E -ENDCHAR -STARTCHAR ograve -ENCODING 242 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 8 0 0 -BITMAP -40 -20 -00 -70 -D8 -D8 -D8 -70 -ENDCHAR -STARTCHAR oacute -ENCODING 243 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 8 0 0 -BITMAP -20 -40 -00 -70 -D8 -D8 -D8 -70 -ENDCHAR -STARTCHAR ocircumflex -ENCODING 244 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 8 0 0 -BITMAP -20 -50 -00 -70 -D8 -D8 -D8 -70 -ENDCHAR -STARTCHAR otilde -ENCODING 245 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 8 0 0 -BITMAP -68 -90 -00 -70 -D8 -D8 -D8 -70 -ENDCHAR -STARTCHAR odieresis -ENCODING 246 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 7 0 0 -BITMAP -50 -00 -70 -D8 -D8 -D8 -70 -ENDCHAR -STARTCHAR divide -ENCODING 247 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 5 0 1 -BITMAP -20 -00 -F8 -00 -20 -ENDCHAR -STARTCHAR oslash -ENCODING 248 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 5 7 0 -1 -BITMAP -08 -70 -D8 -F8 -D8 -70 -80 -ENDCHAR -STARTCHAR ugrave -ENCODING 249 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 7 8 -1 0 -BITMAP -20 -10 -00 -EC -6C -6C -6C -3E -ENDCHAR -STARTCHAR uacute -ENCODING 250 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 7 8 -1 0 -BITMAP -10 -20 -00 -EC -6C -6C -6C -3E -ENDCHAR -STARTCHAR ucircumflex -ENCODING 251 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 7 8 -1 0 -BITMAP -10 -28 -00 -EC -6C -6C -6C -3E -ENDCHAR -STARTCHAR udieresis -ENCODING 252 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 7 7 -1 0 -BITMAP -28 -00 -EC -6C -6C -6C -3E -ENDCHAR -STARTCHAR yacute -ENCODING 253 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 7 10 -1 -2 -BITMAP -08 -10 -00 -EE -6C -6C -28 -38 -30 -F0 -ENDCHAR -STARTCHAR thorn -ENCODING 254 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 9 -1 -2 -BITMAP -E0 -60 -78 -6C -6C -6C -78 -60 -F0 -ENDCHAR -STARTCHAR ydieresis -ENCODING 255 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 7 9 -1 -2 -BITMAP -28 -00 -EE -6C -6C -28 -38 -30 -F0 -ENDCHAR -STARTCHAR trademark -ENCODING 8482 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 7 4 -1 3 -BITMAP -F6 -5E -5A -5A -ENDCHAR -STARTCHAR perthousand -ENCODING 8240 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 7 0 0 -BITMAP -60 -A8 -D0 -60 -6C -B4 -D8 -ENDCHAR -STARTCHAR oe -ENCODING 339 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 6 5 0 0 -BITMAP -6C -B4 -BC -B0 -6C -ENDCHAR -STARTCHAR fl -ENCODING 64258 -SWIDTH 600 0 -DWIDTH 6 0 -BBX 7 7 -1 0 -BITMAP -3C -6C -FC -6C -6C -6C -FE -ENDCHAR -ENDFONT +STARTFONT 2.1 +COMMENT $XConsortium: courB08.bdf,v 1.5 94/04/10 21:46:11 gildea Exp $ +COMMENT +COMMENT Copyright 1984, 1987 Adobe Systems, Inc. +COMMENT Portions Copyright 1988 Digital Equipment Corporation +COMMENT +COMMENT Adobe is a registered trademark of Adobe Systems, Inc. Permission +COMMENT to use these trademarks is hereby granted only in association with the +COMMENT images described in this file. +COMMENT +COMMENT Permission to use, copy, modify, and distribute this software and +COMMENT its documentation for any purpose and without fee is hereby granted, +COMMENT provided that the above copyright notices appear in all copies and +COMMENT that both those copyright notices and this permission notice appear +COMMENT in supporting documentation, and that the names of Adobe Systems and +COMMENT Digital Equipment Corporation not be used in advertising or +COMMENT publicity pertaining to distribution of the software without +COMMENT specific, written prior permission. Adobe Systems and Digital +COMMENT Equipment Corporation make no representations about the suitability +COMMENT of this software for any purpose. It is provided "as is" without +COMMENT express or implied warranty. +COMMENT +COMMENT ADOBE SYSTEMS AND DIGITAL EQUIPMENT CORPORATION DISCLAIM ALL +COMMENT WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED +COMMENT WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL ADOBE +COMMENT SYSTEMS AND DIGITAL EQUIPMENT CORPORATION BE LIABLE FOR ANY SPECIAL, +COMMENT INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER +COMMENT RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF +COMMENT CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +COMMENT CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +COMMENT +COMMENT +FONT -Adobe-Courier-Bold-R-Normal--11-80-100-100-M-60-ISO8859-1 +SIZE 8 100 100 +FONTBOUNDINGBOX 8 11 -1 -2 +STARTPROPERTIES 22 +COMMENT Begin LogicalFontDescription +FONTNAME_REGISTRY "" +FAMILY_NAME "Courier" +FOUNDRY "Adobe" +WEIGHT_NAME "Bold" +SLANT "R" +SETWIDTH_NAME "Normal" +ADD_STYLE_NAME "" +PIXEL_SIZE 11 +POINT_SIZE 80 +RESOLUTION_X 100 +RESOLUTION_Y 100 +SPACING "M" +AVERAGE_WIDTH 60 +CHARSET_REGISTRY "ISO8859" +CHARSET_ENCODING "1" +COMMENT END LogicalFontDescription +CHARSET_COLLECTIONS "ASCII ISO8859-1 ADOBE-STANDARD" +FACE_NAME "Courier Bold" +COPYRIGHT "Copyright (c) 1987 Adobe Systems, Inc., Portions Copyright 1988 Digital Equipment Corp." +COMMENT ***** end of inserted font properties +FONT_ASCENT 8 +FONT_DESCENT 2 +CAP_HEIGHT 6 +X_HEIGHT 5 +ENDPROPERTIES +CHARS 194 +STARTCHAR space +ENCODING 32 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 1 1 0 0 +BITMAP +00 +ENDCHAR +STARTCHAR exclam +ENCODING 33 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 2 6 1 0 +BITMAP +C0 +C0 +C0 +C0 +00 +C0 +ENDCHAR +STARTCHAR quotedbl +ENCODING 34 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 3 3 1 3 +BITMAP +A0 +A0 +A0 +ENDCHAR +STARTCHAR numbersign +ENCODING 35 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 8 0 -1 +BITMAP +50 +50 +F8 +50 +50 +F8 +50 +50 +ENDCHAR +STARTCHAR dollar +ENCODING 36 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 9 0 -1 +BITMAP +20 +78 +C8 +F0 +78 +18 +D8 +F0 +20 +ENDCHAR +STARTCHAR percent +ENCODING 37 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 7 0 0 +BITMAP +E0 +A8 +F0 +20 +78 +A8 +38 +ENDCHAR +STARTCHAR ampersand +ENCODING 38 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 6 -1 0 +BITMAP +38 +60 +30 +7C +D8 +7C +ENDCHAR +STARTCHAR quoteright +ENCODING 39 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 3 3 1 4 +BITMAP +60 +40 +80 +ENDCHAR +STARTCHAR parenleft +ENCODING 40 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 3 8 1 -1 +BITMAP +20 +40 +C0 +C0 +C0 +C0 +40 +20 +ENDCHAR +STARTCHAR parenright +ENCODING 41 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 3 8 1 -1 +BITMAP +80 +40 +60 +60 +60 +60 +40 +80 +ENDCHAR +STARTCHAR asterisk +ENCODING 42 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 4 4 0 3 +BITMAP +20 +F0 +60 +90 +ENDCHAR +STARTCHAR plus +ENCODING 43 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 5 0 1 +BITMAP +20 +20 +F8 +20 +20 +ENDCHAR +STARTCHAR comma +ENCODING 44 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 3 3 1 -2 +BITMAP +60 +40 +80 +ENDCHAR +STARTCHAR minus +ENCODING 45 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 1 0 3 +BITMAP +F8 +ENDCHAR +STARTCHAR period +ENCODING 46 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 2 1 1 0 +BITMAP +C0 +ENDCHAR +STARTCHAR slash +ENCODING 47 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 4 8 1 -1 +BITMAP +10 +10 +20 +20 +40 +40 +80 +80 +ENDCHAR +STARTCHAR zero +ENCODING 48 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 7 0 0 +BITMAP +70 +D8 +D8 +D8 +D8 +D8 +70 +ENDCHAR +STARTCHAR one +ENCODING 49 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 7 0 0 +BITMAP +30 +F0 +30 +30 +30 +30 +FC +ENDCHAR +STARTCHAR two +ENCODING 50 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 7 0 0 +BITMAP +70 +D8 +18 +30 +60 +D8 +F8 +ENDCHAR +STARTCHAR three +ENCODING 51 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 7 0 0 +BITMAP +70 +D8 +18 +70 +18 +D8 +70 +ENDCHAR +STARTCHAR four +ENCODING 52 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 7 0 0 +BITMAP +18 +38 +58 +D8 +FC +18 +18 +ENDCHAR +STARTCHAR five +ENCODING 53 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 7 0 0 +BITMAP +F8 +C0 +F0 +D8 +18 +98 +F0 +ENDCHAR +STARTCHAR six +ENCODING 54 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 7 0 0 +BITMAP +70 +D8 +C0 +F0 +D8 +D8 +70 +ENDCHAR +STARTCHAR seven +ENCODING 55 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 7 0 0 +BITMAP +F8 +D8 +18 +30 +30 +60 +60 +ENDCHAR +STARTCHAR eight +ENCODING 56 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 7 0 0 +BITMAP +70 +D8 +D8 +70 +D8 +D8 +70 +ENDCHAR +STARTCHAR nine +ENCODING 57 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 7 0 0 +BITMAP +70 +D8 +D8 +78 +18 +D8 +70 +ENDCHAR +STARTCHAR colon +ENCODING 58 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 2 4 1 0 +BITMAP +C0 +00 +00 +C0 +ENDCHAR +STARTCHAR semicolon +ENCODING 59 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 3 6 0 -2 +BITMAP +60 +00 +00 +60 +40 +80 +ENDCHAR +STARTCHAR less +ENCODING 60 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 4 5 0 1 +BITMAP +30 +60 +C0 +60 +30 +ENDCHAR +STARTCHAR equal +ENCODING 61 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 4 3 0 2 +BITMAP +F0 +00 +F0 +ENDCHAR +STARTCHAR greater +ENCODING 62 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 4 5 1 1 +BITMAP +C0 +60 +30 +60 +C0 +ENDCHAR +STARTCHAR question +ENCODING 63 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 6 0 0 +BITMAP +70 +98 +30 +60 +00 +60 +ENDCHAR +STARTCHAR at +ENCODING 64 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 8 0 -1 +BITMAP +70 +C8 +98 +A8 +A8 +9C +C0 +70 +ENDCHAR +STARTCHAR A +ENCODING 65 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 7 6 -1 0 +BITMAP +78 +38 +28 +7C +6C +EE +ENDCHAR +STARTCHAR B +ENCODING 66 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 6 -1 0 +BITMAP +F8 +6C +78 +6C +6C +F8 +ENDCHAR +STARTCHAR C +ENCODING 67 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 6 0 0 +BITMAP +78 +D8 +C0 +C0 +D8 +70 +ENDCHAR +STARTCHAR D +ENCODING 68 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 6 -1 0 +BITMAP +F8 +6C +6C +6C +6C +F8 +ENDCHAR +STARTCHAR E +ENCODING 69 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 6 -1 0 +BITMAP +FC +60 +78 +60 +6C +FC +ENDCHAR +STARTCHAR F +ENCODING 70 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 6 -1 0 +BITMAP +FC +60 +78 +60 +60 +F0 +ENDCHAR +STARTCHAR G +ENCODING 71 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 6 0 0 +BITMAP +70 +D8 +C0 +F8 +D8 +78 +ENDCHAR +STARTCHAR H +ENCODING 72 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 7 6 -1 0 +BITMAP +EE +6C +7C +6C +6C +EE +ENDCHAR +STARTCHAR I +ENCODING 73 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 4 6 0 0 +BITMAP +F0 +60 +60 +60 +60 +F0 +ENDCHAR +STARTCHAR J +ENCODING 74 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 6 -1 0 +BITMAP +3C +18 +18 +D8 +D8 +70 +ENDCHAR +STARTCHAR K +ENCODING 75 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 7 6 -1 0 +BITMAP +EC +68 +70 +78 +6C +F6 +ENDCHAR +STARTCHAR L +ENCODING 76 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 6 -1 0 +BITMAP +F0 +60 +60 +60 +6C +FC +ENDCHAR +STARTCHAR M +ENCODING 77 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 6 -1 0 +BITMAP +C4 +6C +6C +7C +54 +D4 +ENDCHAR +STARTCHAR N +ENCODING 78 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 7 6 -1 0 +BITMAP +EE +74 +74 +6C +6C +E4 +ENDCHAR +STARTCHAR O +ENCODING 79 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 6 0 0 +BITMAP +70 +D8 +D8 +D8 +D8 +70 +ENDCHAR +STARTCHAR P +ENCODING 80 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 6 -1 0 +BITMAP +F8 +6C +6C +78 +60 +F0 +ENDCHAR +STARTCHAR Q +ENCODING 81 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 7 0 -1 +BITMAP +70 +D8 +D8 +D8 +D8 +70 +18 +ENDCHAR +STARTCHAR R +ENCODING 82 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 7 6 -1 0 +BITMAP +F8 +6C +6C +78 +6C +F6 +ENDCHAR +STARTCHAR S +ENCODING 83 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 6 0 0 +BITMAP +78 +C8 +F0 +38 +98 +F0 +ENDCHAR +STARTCHAR T +ENCODING 84 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 6 -1 0 +BITMAP +FC +B4 +30 +30 +30 +78 +ENDCHAR +STARTCHAR U +ENCODING 85 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 7 6 -1 0 +BITMAP +EE +6C +6C +6C +6C +38 +ENDCHAR +STARTCHAR V +ENCODING 86 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 7 6 -1 0 +BITMAP +EE +6C +28 +38 +38 +10 +ENDCHAR +STARTCHAR W +ENCODING 87 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 7 6 -1 0 +BITMAP +D6 +54 +54 +7C +38 +28 +ENDCHAR +STARTCHAR X +ENCODING 88 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 6 0 0 +BITMAP +CC +78 +30 +30 +78 +CC +ENDCHAR +STARTCHAR Y +ENCODING 89 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 7 6 -1 0 +BITMAP +E6 +66 +3C +18 +18 +3C +ENDCHAR +STARTCHAR Z +ENCODING 90 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 6 0 0 +BITMAP +F8 +D8 +30 +60 +D8 +F8 +ENDCHAR +STARTCHAR bracketleft +ENCODING 91 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 3 8 1 -1 +BITMAP +E0 +C0 +C0 +C0 +C0 +C0 +C0 +E0 +ENDCHAR +STARTCHAR backslash +ENCODING 92 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 4 8 0 -1 +BITMAP +80 +80 +40 +40 +20 +20 +10 +10 +ENDCHAR +STARTCHAR bracketright +ENCODING 93 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 3 8 1 -1 +BITMAP +E0 +60 +60 +60 +60 +60 +60 +E0 +ENDCHAR +STARTCHAR asciicircum +ENCODING 94 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 3 0 4 +BITMAP +20 +70 +D8 +ENDCHAR +STARTCHAR underscore +ENCODING 95 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 1 0 -2 +BITMAP +FC +ENDCHAR +STARTCHAR quoteleft +ENCODING 96 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 3 3 1 4 +BITMAP +C0 +40 +20 +ENDCHAR +STARTCHAR a +ENCODING 97 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 5 0 0 +BITMAP +70 +D8 +78 +D8 +FC +ENDCHAR +STARTCHAR b +ENCODING 98 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 7 -1 0 +BITMAP +E0 +60 +78 +6C +6C +6C +F8 +ENDCHAR +STARTCHAR c +ENCODING 99 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 5 0 0 +BITMAP +70 +D8 +C0 +D8 +70 +ENDCHAR +STARTCHAR d +ENCODING 100 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 7 0 0 +BITMAP +38 +18 +78 +D8 +D8 +D8 +7C +ENDCHAR +STARTCHAR e +ENCODING 101 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 5 0 0 +BITMAP +70 +D8 +F8 +C0 +78 +ENDCHAR +STARTCHAR f +ENCODING 102 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 7 0 0 +BITMAP +38 +60 +F8 +60 +60 +60 +F8 +ENDCHAR +STARTCHAR g +ENCODING 103 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 7 0 -2 +BITMAP +6C +D8 +D8 +D8 +78 +18 +F0 +ENDCHAR +STARTCHAR h +ENCODING 104 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 7 -1 0 +BITMAP +E0 +60 +78 +6C +6C +6C +6C +ENDCHAR +STARTCHAR i +ENCODING 105 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 7 0 0 +BITMAP +30 +00 +F0 +30 +30 +30 +FC +ENDCHAR +STARTCHAR j +ENCODING 106 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 4 9 0 -2 +BITMAP +30 +00 +F0 +30 +30 +30 +30 +30 +E0 +ENDCHAR +STARTCHAR k +ENCODING 107 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 7 7 -1 0 +BITMAP +E0 +60 +6C +78 +70 +78 +6E +ENDCHAR +STARTCHAR l +ENCODING 108 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 7 0 0 +BITMAP +F0 +30 +30 +30 +30 +30 +FC +ENDCHAR +STARTCHAR m +ENCODING 109 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 5 -1 0 +BITMAP +F8 +7C +54 +54 +54 +ENDCHAR +STARTCHAR n +ENCODING 110 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 5 -1 0 +BITMAP +D8 +6C +6C +6C +6C +ENDCHAR +STARTCHAR o +ENCODING 111 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 5 0 0 +BITMAP +70 +D8 +D8 +D8 +70 +ENDCHAR +STARTCHAR p +ENCODING 112 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 7 -1 -2 +BITMAP +F8 +6C +6C +6C +78 +60 +F0 +ENDCHAR +STARTCHAR q +ENCODING 113 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 7 0 -2 +BITMAP +6C +D8 +D8 +D8 +78 +18 +3C +ENDCHAR +STARTCHAR r +ENCODING 114 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 5 0 0 +BITMAP +DC +74 +60 +60 +F0 +ENDCHAR +STARTCHAR s +ENCODING 115 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 5 0 0 +BITMAP +78 +E0 +78 +1C +F8 +ENDCHAR +STARTCHAR t +ENCODING 116 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 7 0 0 +BITMAP +60 +60 +F8 +60 +60 +6C +38 +ENDCHAR +STARTCHAR u +ENCODING 117 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 7 5 -1 0 +BITMAP +EC +6C +6C +6C +3E +ENDCHAR +STARTCHAR v +ENCODING 118 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 5 -1 0 +BITMAP +EC +6C +38 +38 +10 +ENDCHAR +STARTCHAR w +ENCODING 119 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 7 5 -1 0 +BITMAP +D6 +54 +7C +3C +28 +ENDCHAR +STARTCHAR x +ENCODING 120 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 5 0 0 +BITMAP +EC +78 +30 +78 +DC +ENDCHAR +STARTCHAR y +ENCODING 121 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 7 7 -1 -2 +BITMAP +EE +6C +6C +28 +38 +30 +E0 +ENDCHAR +STARTCHAR z +ENCODING 122 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 5 0 0 +BITMAP +F8 +B0 +60 +D8 +F8 +ENDCHAR +STARTCHAR braceleft +ENCODING 123 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 4 8 1 -1 +BITMAP +30 +60 +60 +C0 +60 +60 +60 +30 +ENDCHAR +STARTCHAR bar +ENCODING 124 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 1 7 2 -1 +BITMAP +80 +80 +80 +80 +80 +80 +80 +ENDCHAR +STARTCHAR braceright +ENCODING 125 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 4 8 0 -1 +BITMAP +C0 +60 +60 +30 +60 +60 +60 +C0 +ENDCHAR +STARTCHAR asciitilde +ENCODING 126 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 2 0 3 +BITMAP +68 +B0 +ENDCHAR +STARTCHAR exclamdown +ENCODING 161 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 2 7 1 -2 +BITMAP +C0 +00 +C0 +C0 +C0 +C0 +C0 +ENDCHAR +STARTCHAR cent +ENCODING 162 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 8 0 -1 +BITMAP +20 +20 +78 +C8 +C0 +78 +20 +20 +ENDCHAR +STARTCHAR sterling +ENCODING 163 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 7 0 0 +BITMAP +38 +68 +20 +F8 +20 +64 +F8 +ENDCHAR +STARTCHAR currency +ENCODING 164 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 5 0 1 +BITMAP +88 +70 +50 +70 +88 +ENDCHAR +STARTCHAR yen +ENCODING 165 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 7 0 0 +BITMAP +CC +48 +FC +30 +FC +30 +78 +ENDCHAR +STARTCHAR brokenbar +ENCODING 166 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 1 9 2 -2 +BITMAP +80 +80 +80 +80 +00 +80 +80 +80 +80 +ENDCHAR +STARTCHAR section +ENCODING 167 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 8 0 -1 +BITMAP +78 +48 +60 +90 +48 +30 +90 +F0 +ENDCHAR +STARTCHAR dieresis +ENCODING 168 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 3 1 1 5 +BITMAP +A0 +ENDCHAR +STARTCHAR copyright +ENCODING 169 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 7 0 0 +BITMAP +30 +48 +B4 +A4 +B4 +48 +30 +ENDCHAR +STARTCHAR ordfeminine +ENCODING 170 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 4 5 1 2 +BITMAP +E0 +10 +D0 +00 +F0 +ENDCHAR +STARTCHAR guillemotleft +ENCODING 171 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 7 5 -1 0 +BITMAP +36 +6C +D8 +6C +36 +ENDCHAR +STARTCHAR logicalnot +ENCODING 172 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 3 0 2 +BITMAP +F8 +08 +08 +ENDCHAR +STARTCHAR hyphen +ENCODING 173 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 1 0 3 +BITMAP +F8 +ENDCHAR +STARTCHAR registered +ENCODING 174 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 7 7 0 0 +BITMAP +38 +44 +BA +B2 +AA +44 +38 +ENDCHAR +STARTCHAR macron +ENCODING 175 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 4 1 0 5 +BITMAP +F0 +ENDCHAR +STARTCHAR degree +ENCODING 176 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 4 3 0 4 +BITMAP +60 +90 +60 +ENDCHAR +STARTCHAR plusminus +ENCODING 177 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 6 0 0 +BITMAP +20 +20 +F8 +20 +00 +F8 +ENDCHAR +STARTCHAR twosuperior +ENCODING 178 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 3 4 1 3 +BITMAP +60 +A0 +40 +E0 +ENDCHAR +STARTCHAR threesuperior +ENCODING 179 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 3 4 1 3 +BITMAP +E0 +40 +20 +C0 +ENDCHAR +STARTCHAR acute +ENCODING 180 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 2 2 2 5 +BITMAP +40 +80 +ENDCHAR +STARTCHAR mu +ENCODING 181 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 7 7 -1 -2 +BITMAP +EC +6C +6C +6C +7E +40 +40 +ENDCHAR +STARTCHAR paragraph +ENCODING 182 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 8 0 -1 +BITMAP +7C +A8 +A8 +68 +28 +28 +28 +6C +ENDCHAR +STARTCHAR periodcentered +ENCODING 183 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 2 1 1 3 +BITMAP +C0 +ENDCHAR +STARTCHAR cedilla +ENCODING 184 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 3 3 1 -2 +BITMAP +40 +20 +C0 +ENDCHAR +STARTCHAR onesuperior +ENCODING 185 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 3 4 1 3 +BITMAP +C0 +40 +40 +E0 +ENDCHAR +STARTCHAR ordmasculine +ENCODING 186 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 4 5 1 2 +BITMAP +60 +90 +60 +00 +F0 +ENDCHAR +STARTCHAR guillemotright +ENCODING 187 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 7 5 -1 0 +BITMAP +D8 +6C +36 +6C +D8 +ENDCHAR +STARTCHAR onequarter +ENCODING 188 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 7 7 -1 0 +BITMAP +C0 +44 +48 +F4 +2C +5E +04 +ENDCHAR +STARTCHAR onehalf +ENCODING 189 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 7 7 -1 0 +BITMAP +C0 +44 +48 +F6 +2A +44 +0E +ENDCHAR +STARTCHAR threequarters +ENCODING 190 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 7 7 -1 0 +BITMAP +E0 +44 +28 +D4 +2C +5E +04 +ENDCHAR +STARTCHAR questiondown +ENCODING 191 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 7 0 -2 +BITMAP +30 +00 +30 +30 +60 +C8 +70 +ENDCHAR +STARTCHAR Agrave +ENCODING 192 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 7 9 -1 0 +BITMAP +20 +10 +00 +78 +38 +28 +7C +6C +EE +ENDCHAR +STARTCHAR Aacute +ENCODING 193 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 7 9 -1 0 +BITMAP +10 +20 +00 +78 +38 +28 +7C +6C +EE +ENDCHAR +STARTCHAR Acircumflex +ENCODING 194 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 7 9 -1 0 +BITMAP +10 +28 +00 +78 +38 +28 +7C +6C +EE +ENDCHAR +STARTCHAR Atilde +ENCODING 195 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 7 9 -1 0 +BITMAP +34 +48 +00 +78 +38 +28 +7C +6C +EE +ENDCHAR +STARTCHAR Adieresis +ENCODING 196 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 7 8 -1 0 +BITMAP +28 +00 +78 +38 +28 +7C +6C +EE +ENDCHAR +STARTCHAR Aring +ENCODING 197 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 7 9 -1 0 +BITMAP +30 +48 +30 +78 +38 +28 +7C +6C +EE +ENDCHAR +STARTCHAR AE +ENCODING 198 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 7 6 -1 0 +BITMAP +7E +3A +6C +78 +DA +DE +ENDCHAR +STARTCHAR Ccedilla +ENCODING 199 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 8 0 -2 +BITMAP +78 +D8 +C0 +C0 +D8 +70 +10 +60 +ENDCHAR +STARTCHAR Egrave +ENCODING 200 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 9 -1 0 +BITMAP +20 +10 +00 +FC +64 +78 +60 +6C +FC +ENDCHAR +STARTCHAR Eacute +ENCODING 201 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 9 -1 0 +BITMAP +10 +20 +00 +FC +64 +78 +60 +6C +FC +ENDCHAR +STARTCHAR Ecircumflex +ENCODING 202 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 9 -1 0 +BITMAP +20 +50 +00 +FC +64 +78 +60 +6C +FC +ENDCHAR +STARTCHAR Edieresis +ENCODING 203 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 8 -1 0 +BITMAP +50 +00 +FC +64 +78 +60 +6C +FC +ENDCHAR +STARTCHAR Igrave +ENCODING 204 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 4 9 0 0 +BITMAP +40 +20 +00 +F0 +60 +60 +60 +60 +F0 +ENDCHAR +STARTCHAR Iacute +ENCODING 205 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 4 9 0 0 +BITMAP +20 +40 +00 +F0 +60 +60 +60 +60 +F0 +ENDCHAR +STARTCHAR Icircumflex +ENCODING 206 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 4 9 0 0 +BITMAP +40 +A0 +00 +F0 +60 +60 +60 +60 +F0 +ENDCHAR +STARTCHAR Idieresis +ENCODING 207 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 4 8 0 0 +BITMAP +A0 +00 +F0 +60 +60 +60 +60 +F0 +ENDCHAR +STARTCHAR Eth +ENCODING 208 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 6 -1 0 +BITMAP +F8 +6C +F4 +64 +6C +F8 +ENDCHAR +STARTCHAR Ntilde +ENCODING 209 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 7 9 -1 0 +BITMAP +34 +48 +00 +EE +64 +74 +7C +6C +EC +ENDCHAR +STARTCHAR Ograve +ENCODING 210 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 9 0 0 +BITMAP +40 +20 +00 +70 +D8 +D8 +D8 +D8 +70 +ENDCHAR +STARTCHAR Oacute +ENCODING 211 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 9 0 0 +BITMAP +20 +40 +00 +70 +D8 +D8 +D8 +D8 +70 +ENDCHAR +STARTCHAR Ocircumflex +ENCODING 212 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 9 0 0 +BITMAP +20 +50 +00 +70 +D8 +D8 +D8 +D8 +70 +ENDCHAR +STARTCHAR Otilde +ENCODING 213 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 9 0 0 +BITMAP +68 +90 +00 +70 +D8 +D8 +D8 +D8 +70 +ENDCHAR +STARTCHAR Odieresis +ENCODING 214 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +50 +00 +70 +D8 +D8 +D8 +D8 +70 +ENDCHAR +STARTCHAR multiply +ENCODING 215 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 5 0 1 +BITMAP +88 +50 +20 +50 +88 +ENDCHAR +STARTCHAR Oslash +ENCODING 216 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 7 6 -1 0 +BITMAP +3A +6C +7C +6C +6C +B8 +ENDCHAR +STARTCHAR Ugrave +ENCODING 217 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 7 9 -1 0 +BITMAP +20 +10 +00 +EE +6C +6C +6C +6C +38 +ENDCHAR +STARTCHAR Uacute +ENCODING 218 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 7 9 -1 0 +BITMAP +08 +10 +00 +EE +6C +6C +6C +6C +38 +ENDCHAR +STARTCHAR Ucircumflex +ENCODING 219 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 7 9 -1 0 +BITMAP +10 +28 +00 +EE +6C +6C +6C +6C +38 +ENDCHAR +STARTCHAR Udieresis +ENCODING 220 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 7 8 -1 0 +BITMAP +28 +00 +EE +6C +6C +6C +6C +38 +ENDCHAR +STARTCHAR Yacute +ENCODING 221 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 7 9 -1 0 +BITMAP +04 +08 +00 +E6 +66 +3C +18 +18 +3C +ENDCHAR +STARTCHAR Thorn +ENCODING 222 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 6 -1 0 +BITMAP +E0 +78 +6C +6C +78 +E0 +ENDCHAR +STARTCHAR germandbls +ENCODING 223 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 7 6 -1 0 +BITMAP +38 +68 +7C +66 +66 +EC +ENDCHAR +STARTCHAR agrave +ENCODING 224 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 8 0 0 +BITMAP +20 +10 +00 +70 +98 +78 +D8 +FC +ENDCHAR +STARTCHAR aacute +ENCODING 225 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 8 0 0 +BITMAP +10 +20 +00 +70 +98 +78 +D8 +FC +ENDCHAR +STARTCHAR acircumflex +ENCODING 226 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 8 0 0 +BITMAP +20 +50 +00 +70 +98 +78 +D8 +FC +ENDCHAR +STARTCHAR atilde +ENCODING 227 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 8 0 0 +BITMAP +68 +90 +00 +70 +98 +78 +D8 +FC +ENDCHAR +STARTCHAR adieresis +ENCODING 228 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 7 0 0 +BITMAP +50 +00 +70 +98 +78 +D8 +FC +ENDCHAR +STARTCHAR aring +ENCODING 229 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 9 0 0 +BITMAP +30 +48 +30 +00 +70 +98 +78 +D8 +FC +ENDCHAR +STARTCHAR ae +ENCODING 230 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 5 -1 0 +BITMAP +6C +B4 +7C +B0 +DC +ENDCHAR +STARTCHAR ccedilla +ENCODING 231 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 7 0 -2 +BITMAP +70 +D8 +C0 +D8 +70 +10 +60 +ENDCHAR +STARTCHAR egrave +ENCODING 232 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +40 +20 +00 +70 +D8 +F8 +C0 +78 +ENDCHAR +STARTCHAR eacute +ENCODING 233 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +20 +40 +00 +70 +D8 +F8 +C0 +78 +ENDCHAR +STARTCHAR ecircumflex +ENCODING 234 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +20 +50 +00 +70 +D8 +F8 +C0 +78 +ENDCHAR +STARTCHAR edieresis +ENCODING 235 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 7 0 0 +BITMAP +50 +00 +70 +D8 +F8 +C0 +78 +ENDCHAR +STARTCHAR igrave +ENCODING 236 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 8 0 0 +BITMAP +20 +10 +00 +70 +30 +30 +30 +FC +ENDCHAR +STARTCHAR iacute +ENCODING 237 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 8 0 0 +BITMAP +10 +20 +00 +70 +30 +30 +30 +FC +ENDCHAR +STARTCHAR icircumflex +ENCODING 238 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 8 0 0 +BITMAP +20 +50 +00 +70 +30 +30 +30 +FC +ENDCHAR +STARTCHAR idieresis +ENCODING 239 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 7 0 0 +BITMAP +50 +00 +70 +30 +30 +30 +FC +ENDCHAR +STARTCHAR eth +ENCODING 240 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +D0 +60 +B0 +78 +D8 +D8 +D8 +70 +ENDCHAR +STARTCHAR ntilde +ENCODING 241 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 7 8 -1 0 +BITMAP +34 +48 +00 +D8 +6C +6C +6C +6E +ENDCHAR +STARTCHAR ograve +ENCODING 242 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +40 +20 +00 +70 +D8 +D8 +D8 +70 +ENDCHAR +STARTCHAR oacute +ENCODING 243 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +20 +40 +00 +70 +D8 +D8 +D8 +70 +ENDCHAR +STARTCHAR ocircumflex +ENCODING 244 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +20 +50 +00 +70 +D8 +D8 +D8 +70 +ENDCHAR +STARTCHAR otilde +ENCODING 245 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +68 +90 +00 +70 +D8 +D8 +D8 +70 +ENDCHAR +STARTCHAR odieresis +ENCODING 246 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 7 0 0 +BITMAP +50 +00 +70 +D8 +D8 +D8 +70 +ENDCHAR +STARTCHAR divide +ENCODING 247 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 5 0 1 +BITMAP +20 +00 +F8 +00 +20 +ENDCHAR +STARTCHAR oslash +ENCODING 248 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 5 7 0 -1 +BITMAP +08 +70 +D8 +F8 +D8 +70 +80 +ENDCHAR +STARTCHAR ugrave +ENCODING 249 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 7 8 -1 0 +BITMAP +20 +10 +00 +EC +6C +6C +6C +3E +ENDCHAR +STARTCHAR uacute +ENCODING 250 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 7 8 -1 0 +BITMAP +10 +20 +00 +EC +6C +6C +6C +3E +ENDCHAR +STARTCHAR ucircumflex +ENCODING 251 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 7 8 -1 0 +BITMAP +10 +28 +00 +EC +6C +6C +6C +3E +ENDCHAR +STARTCHAR udieresis +ENCODING 252 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 7 7 -1 0 +BITMAP +28 +00 +EC +6C +6C +6C +3E +ENDCHAR +STARTCHAR yacute +ENCODING 253 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 7 10 -1 -2 +BITMAP +08 +10 +00 +EE +6C +6C +28 +38 +30 +F0 +ENDCHAR +STARTCHAR thorn +ENCODING 254 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 9 -1 -2 +BITMAP +E0 +60 +78 +6C +6C +6C +78 +60 +F0 +ENDCHAR +STARTCHAR ydieresis +ENCODING 255 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 7 9 -1 -2 +BITMAP +28 +00 +EE +6C +6C +28 +38 +30 +F0 +ENDCHAR +STARTCHAR trademark +ENCODING 8482 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 7 4 -1 3 +BITMAP +F6 +5E +5A +5A +ENDCHAR +STARTCHAR perthousand +ENCODING 8240 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 7 0 0 +BITMAP +60 +A8 +D0 +60 +6C +B4 +D8 +ENDCHAR +STARTCHAR oe +ENCODING 339 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 6 5 0 0 +BITMAP +6C +B4 +BC +B0 +6C +ENDCHAR +STARTCHAR fl +ENCODING 64258 +SWIDTH 600 0 +DWIDTH 6 0 +BBX 7 7 -1 0 +BITMAP +3C +6C +FC +6C +6C +6C +FE +ENDCHAR +ENDFONT diff --git a/Tests/images/create_eps.gnuplot b/Tests/images/create_eps.gnuplot index 57a3c8c97..33be44e64 100644 --- a/Tests/images/create_eps.gnuplot +++ b/Tests/images/create_eps.gnuplot @@ -1,28 +1,28 @@ -#This is the script that was used to create our sample EPS files -#We used the following version of the gnuplot program -#G N U P L O T -#Version 4.6 patchlevel 3 last modified 2013-04-12 -#Build System: Darwin x86_64 - -#This file will generate the non_zero_bb.eps variant, in order to get the -#zero_bb.eps variant you will need to edit line6 in the result file to -#be "%%BoundingBox: 0 0 460 352" instead of "%%BoundingBox: 50 50 410 302" - -set t postscript eps color -set o "sample.eps" -set dummy u,v -set key bmargin center horizontal Right noreverse enhanced autotitles nobox -set parametric -set view 50, 30, 1, 1 -set isosamples 10, 10 -set hidden3d back offset 1 trianglepattern 3 undefined 1 altdiagonal bentover -set ticslevel 0 -set title "Interlocking Tori" - -set style line 1 lt 1 lw 1 pt 3 lc rgb "red" -set style line 2 lt 1 lw 1 pt 3 lc rgb "blue" - -set urange [ -3.14159 : 3.14159 ] noreverse nowriteback -set vrange [ -3.14159 : 3.14159 ] noreverse nowriteback -splot cos(u)+.5*cos(u)*cos(v),sin(u)+.5*sin(u)*cos(v),.5*sin(v) ls 1,\ - 1+cos(u)+.5*cos(u)*cos(v),.5*sin(v),sin(u)+.5*sin(u)*cos(v) ls 2 +#This is the script that was used to create our sample EPS files +#We used the following version of the gnuplot program +#G N U P L O T +#Version 4.6 patchlevel 3 last modified 2013-04-12 +#Build System: Darwin x86_64 + +#This file will generate the non_zero_bb.eps variant, in order to get the +#zero_bb.eps variant you will need to edit line6 in the result file to +#be "%%BoundingBox: 0 0 460 352" instead of "%%BoundingBox: 50 50 410 302" + +set t postscript eps color +set o "sample.eps" +set dummy u,v +set key bmargin center horizontal Right noreverse enhanced autotitles nobox +set parametric +set view 50, 30, 1, 1 +set isosamples 10, 10 +set hidden3d back offset 1 trianglepattern 3 undefined 1 altdiagonal bentover +set ticslevel 0 +set title "Interlocking Tori" + +set style line 1 lt 1 lw 1 pt 3 lc rgb "red" +set style line 2 lt 1 lw 1 pt 3 lc rgb "blue" + +set urange [ -3.14159 : 3.14159 ] noreverse nowriteback +set vrange [ -3.14159 : 3.14159 ] noreverse nowriteback +splot cos(u)+.5*cos(u)*cos(v),sin(u)+.5*sin(u)*cos(v),.5*sin(v) ls 1,\ + 1+cos(u)+.5*cos(u)*cos(v),.5*sin(v),sin(u)+.5*sin(u)*cos(v) ls 2 diff --git a/Tests/images/custom_gimp_palette.gpl b/Tests/images/custom_gimp_palette.gpl index 08ea70028..7cb4cb667 100644 --- a/Tests/images/custom_gimp_palette.gpl +++ b/Tests/images/custom_gimp_palette.gpl @@ -1,12 +1,12 @@ -GIMP Palette -Name: custompalette -Columns: 4 -# - 0 0 0 Index 3 - 65 38 30 Index 4 -103 62 49 Index 6 - 79 73 72 Index 7 -114 101 97 Index 8 -208 127 100 Index 9 -151 144 142 Index 10 -221 207 199 Index 11 +GIMP Palette +Name: custompalette +Columns: 4 +# + 0 0 0 Index 3 + 65 38 30 Index 4 +103 62 49 Index 6 + 79 73 72 Index 7 +114 101 97 Index 8 +208 127 100 Index 9 +151 144 142 Index 10 +221 207 199 Index 11 diff --git a/Tests/images/fli_oob/02r/notes b/Tests/images/fli_oob/02r/notes index 49f92b19b..c656af8a7 100644 --- a/Tests/images/fli_oob/02r/notes +++ b/Tests/images/fli_oob/02r/notes @@ -1 +1 @@ -Is this because a file-originating field is being interpreted as a *signed* int32, allowing it to provide negative values for 'advance'? +Is this because a file-originating field is being interpreted as a *signed* int32, allowing it to provide negative values for 'advance'? diff --git a/Tests/images/fli_oob/02r/reproducing b/Tests/images/fli_oob/02r/reproducing index 3286d94f1..86065d911 100644 --- a/Tests/images/fli_oob/02r/reproducing +++ b/Tests/images/fli_oob/02r/reproducing @@ -1 +1 @@ -Image.open(...).seek(212) +Image.open(...).seek(212) diff --git a/Tests/images/fli_oob/03r/notes b/Tests/images/fli_oob/03r/notes index d75605cea..51b36ed80 100644 --- a/Tests/images/fli_oob/03r/notes +++ b/Tests/images/fli_oob/03r/notes @@ -1 +1 @@ -ridiculous bytes value passed to ImagingFliDecode +ridiculous bytes value passed to ImagingFliDecode diff --git a/Tests/images/fli_oob/03r/reproducing b/Tests/images/fli_oob/03r/reproducing index 145b8b074..33995fb0f 100644 --- a/Tests/images/fli_oob/03r/reproducing +++ b/Tests/images/fli_oob/03r/reproducing @@ -1 +1 @@ -im = Image.open(d); im.seek(0); im.getdata() +im = Image.open(d); im.seek(0); im.getdata() diff --git a/Tests/images/fli_oob/04r/notes b/Tests/images/fli_oob/04r/notes index 7922e0ba8..b8676f7c2 100644 --- a/Tests/images/fli_oob/04r/notes +++ b/Tests/images/fli_oob/04r/notes @@ -1 +1 @@ -failure to check input buffer (`data`) boundaries in BRUN chunk +failure to check input buffer (`data`) boundaries in BRUN chunk diff --git a/Tests/images/fli_oob/04r/reproducing b/Tests/images/fli_oob/04r/reproducing index 145b8b074..33995fb0f 100644 --- a/Tests/images/fli_oob/04r/reproducing +++ b/Tests/images/fli_oob/04r/reproducing @@ -1 +1 @@ -im = Image.open(d); im.seek(0); im.getdata() +im = Image.open(d); im.seek(0); im.getdata() diff --git a/Tests/images/fli_oob/05r/notes b/Tests/images/fli_oob/05r/notes index bec9db779..52d814704 100644 --- a/Tests/images/fli_oob/05r/notes +++ b/Tests/images/fli_oob/05r/notes @@ -1 +1 @@ -failure to check input buffer (`data`) boundaries in LC chunk +failure to check input buffer (`data`) boundaries in LC chunk diff --git a/Tests/images/fli_oob/05r/reproducing b/Tests/images/fli_oob/05r/reproducing index 145b8b074..33995fb0f 100644 --- a/Tests/images/fli_oob/05r/reproducing +++ b/Tests/images/fli_oob/05r/reproducing @@ -1 +1 @@ -im = Image.open(d); im.seek(0); im.getdata() +im = Image.open(d); im.seek(0); im.getdata() diff --git a/Tests/images/fli_oob/06r/notes b/Tests/images/fli_oob/06r/notes index 397ad4748..8016e5298 100644 --- a/Tests/images/fli_oob/06r/notes +++ b/Tests/images/fli_oob/06r/notes @@ -1 +1 @@ -failure to check input buffer (`data`) boundaries in SS2 chunk +failure to check input buffer (`data`) boundaries in SS2 chunk diff --git a/Tests/images/fli_oob/06r/reproducing b/Tests/images/fli_oob/06r/reproducing index 145b8b074..33995fb0f 100644 --- a/Tests/images/fli_oob/06r/reproducing +++ b/Tests/images/fli_oob/06r/reproducing @@ -1 +1 @@ -im = Image.open(d); im.seek(0); im.getdata() +im = Image.open(d); im.seek(0); im.getdata() diff --git a/Tests/images/gimp_gradient.ggr b/Tests/images/gimp_gradient.ggr index 4644e5537..fa07d05c0 100644 --- a/Tests/images/gimp_gradient.ggr +++ b/Tests/images/gimp_gradient.ggr @@ -1,6 +1,6 @@ -GIMP Gradient -4 -0.000000 0.351923 0.534893 1.000000 1.000000 1.000000 0.910000 0.730303 0.510606 1.000000 0.480000 1 0 -0.501504 0.611002 0.637730 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 1.000000 1 0 -0.931891 0.951264 1.000000 0.531255 0.316078 1.031173 1.000000 0.000000 0.000000 0.000000 1.000000 0 0 +GIMP Gradient +4 +0.000000 0.351923 0.534893 1.000000 1.000000 1.000000 0.910000 0.730303 0.510606 1.000000 0.480000 1 0 +0.501504 0.611002 0.637730 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 1.000000 1 0 +0.931891 0.951264 1.000000 0.531255 0.316078 1.031173 1.000000 0.000000 0.000000 0.000000 1.000000 0 0 0.810551 0.881217 0.921883 0.717576 0.441331 0.081217 1.000000 0.751576 0.410331 0.081217 1.000000 0 0 \ No newline at end of file diff --git a/Tests/images/gimp_gradient_with_name.ggr b/Tests/images/gimp_gradient_with_name.ggr index ab1c1d734..667b6df4e 100644 --- a/Tests/images/gimp_gradient_with_name.ggr +++ b/Tests/images/gimp_gradient_with_name.ggr @@ -1,7 +1,7 @@ -GIMP Gradient -Name: A GIMP 1.3 gradient file -4 -0.000000 0.351923 0.534893 1.000000 1.000000 1.000000 0.910000 0.730303 0.510606 1.000000 0.480000 1 0 -0.501504 0.611002 0.637730 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 1.000000 1 0 -0.931891 0.951264 1.000000 0.531255 0.316078 1.031173 1.000000 0.000000 0.000000 0.000000 1.000000 0 0 +GIMP Gradient +Name: A GIMP 1.3 gradient file +4 +0.000000 0.351923 0.534893 1.000000 1.000000 1.000000 0.910000 0.730303 0.510606 1.000000 0.480000 1 0 +0.501504 0.611002 0.637730 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 1.000000 1 0 +0.931891 0.951264 1.000000 0.531255 0.316078 1.031173 1.000000 0.000000 0.000000 0.000000 1.000000 0 0 0.810551 0.881217 0.921883 0.717576 0.441331 0.081217 1.000000 0.751576 0.410331 0.081217 1.000000 0 0 \ No newline at end of file diff --git a/Tests/images/hopper.xbm b/Tests/images/hopper.xbm index 2106fbfa2..caeb559f7 100644 --- a/Tests/images/hopper.xbm +++ b/Tests/images/hopper.xbm @@ -1,174 +1,174 @@ -#define hopper_width 128 -#define hopper_height 128 -static char hopper_bits[] = { - 0xFF, 0xFF, 0xFF, 0x1F, 0xF0, 0x3D, 0x80, 0x96, 0xDA, 0xB6, 0xD6, 0x2A, - 0xA9, 0x6D, 0x25, 0x29, 0xFF, 0xFF, 0xFF, 0xBF, 0xFE, 0x7D, 0xC0, 0xFB, - 0x69, 0x69, 0xA9, 0xD5, 0x96, 0x96, 0x5E, 0x9A, 0xFF, 0xFF, 0xFF, 0x9F, - 0xFE, 0xFD, 0x80, 0xFF, 0x97, 0xB6, 0x5A, 0x6A, 0x5D, 0x6D, 0x5A, 0x62, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFD, 0xC1, 0xCF, 0x5F, 0x89, 0xA5, 0xD5, - 0xAA, 0x92, 0xA5, 0x59, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFD, 0xF8, 0x00, - 0xB8, 0x59, 0xAA, 0x56, 0xDA, 0x5A, 0x56, 0x49, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFE, 0xFC, 0xF9, 0x00, 0xF0, 0xA6, 0x55, 0xA9, 0x65, 0xA5, 0xA9, 0xA2, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFD, 0x71, 0x00, 0x60, 0x70, 0xAA, 0x54, - 0xAA, 0x5A, 0x54, 0x69, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFC, 0x38, 0x00, - 0xE0, 0x80, 0x55, 0xAB, 0x55, 0x75, 0xAB, 0x56, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFE, 0x1D, 0x70, 0x00, 0xF0, 0x00, 0xAE, 0xAA, 0xAA, 0xA6, 0x95, 0x55, - 0xFB, 0xFF, 0xFF, 0xFF, 0xFE, 0x1D, 0xF0, 0x00, 0xF0, 0x00, 0x50, 0x55, - 0x55, 0x59, 0x6A, 0xAA, 0xF3, 0xFF, 0xFF, 0xFF, 0xFE, 0x04, 0xF0, 0x00, - 0xF0, 0x00, 0xA4, 0xAA, 0xA9, 0xA5, 0x95, 0x55, 0x21, 0xFF, 0xFF, 0xFF, - 0xFE, 0x01, 0xF8, 0x00, 0xF0, 0x00, 0x50, 0x55, 0x56, 0x5A, 0x6A, 0xAA, - 0x83, 0x9F, 0xFF, 0xFF, 0xFE, 0x00, 0xFC, 0x00, 0xF0, 0x01, 0x50, 0xD5, - 0x95, 0x95, 0xA5, 0x5A, 0x83, 0x9F, 0xFF, 0xFF, 0xFE, 0x81, 0xFF, 0x00, - 0xE0, 0x0B, 0xA0, 0xAA, 0x7A, 0x6A, 0x5A, 0x65, 0x80, 0x1F, 0xF0, 0xFF, - 0xFE, 0xF0, 0xFF, 0x00, 0xF0, 0x3F, 0x50, 0x55, 0xA5, 0xB5, 0xA5, 0xAA, - 0x00, 0x1F, 0xF8, 0xFD, 0xFE, 0xF0, 0xF7, 0x00, 0xF0, 0x6D, 0xA0, 0xAA, - 0x5A, 0x6A, 0x5A, 0x55, 0x83, 0x0F, 0xF8, 0xC9, 0xFE, 0x79, 0x00, 0xF8, - 0x02, 0xC0, 0x81, 0x55, 0xAD, 0xD5, 0xA5, 0xA6, 0xE3, 0x03, 0xFC, 0xC0, - 0xFE, 0xFF, 0x04, 0xF0, 0x00, 0x80, 0x63, 0xAA, 0x52, 0x2A, 0x5A, 0x58, - 0xE7, 0x03, 0xFC, 0xE1, 0xFE, 0xFF, 0x2F, 0xFC, 0x03, 0x0C, 0xFE, 0x55, - 0xA9, 0x55, 0x66, 0x15, 0xF3, 0x0F, 0x7C, 0xF0, 0xFE, 0xFF, 0xFF, 0xFD, - 0xEF, 0x38, 0xFF, 0xAA, 0x56, 0xAA, 0x99, 0xAA, 0xF7, 0x1F, 0x78, 0xF0, - 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xF1, 0xFF, 0x55, 0xAA, 0x5B, 0x66, 0xA4, - 0xFF, 0x9F, 0xFD, 0xF0, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xAB, - 0x65, 0xA4, 0x99, 0x1B, 0xFF, 0xDF, 0xFF, 0xE0, 0xFE, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0x69, 0x51, 0x5A, 0x24, 0x62, 0xFF, 0xDF, 0xFF, 0xEC, - 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x95, 0xAA, 0x24, 0x59, 0x55, - 0xFF, 0xFF, 0xFF, 0xFC, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x25, - 0x49, 0xAD, 0x55, 0x2A, 0xFF, 0xFF, 0xFF, 0xFC, 0xFD, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0x9B, 0xA5, 0x52, 0x2A, 0x94, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x67, 0x64, 0xC1, 0x92, 0x6D, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x95, - 0x49, 0x2C, 0x55, 0x92, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0xFF, 0xFF, 0xFF, - 0x7F, 0xFF, 0xFF, 0x15, 0xB4, 0x62, 0x44, 0x49, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFD, 0xFF, 0x51, 0x30, 0x03, 0xE0, 0xFF, 0x53, 0x03, 0x89, 0x96, 0x92, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFD, 0x7F, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xCD, - 0x68, 0x19, 0x68, 0x99, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x1F, 0x00, 0x00, - 0x00, 0x00, 0xFF, 0x13, 0x4A, 0x64, 0x23, 0x49, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x47, 0x8A, 0xA4, 0xD8, 0x30, - 0xFE, 0xFF, 0xFF, 0xFF, 0xFC, 0x07, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x39, - 0x29, 0x49, 0x06, 0x86, 0xCC, 0xFF, 0xFF, 0xFF, 0xFC, 0x03, 0x00, 0x00, - 0x80, 0x00, 0xF2, 0x81, 0x50, 0x31, 0xA2, 0x8C, 0xE4, 0xFF, 0xFF, 0xFF, - 0xFF, 0x0A, 0x08, 0x00, 0x00, 0x00, 0xF0, 0xA7, 0x46, 0x8C, 0x28, 0x31, - 0xC0, 0xFC, 0xFF, 0xFF, 0xFC, 0x03, 0x10, 0x00, 0xE0, 0x01, 0xE0, 0x19, - 0x41, 0x61, 0x66, 0x05, 0xE0, 0xB8, 0xFF, 0xFF, 0x7E, 0x0C, 0x44, 0x00, - 0x40, 0xBF, 0xEF, 0x41, 0x5C, 0x04, 0x00, 0xA8, 0xF0, 0xC1, 0xFF, 0xFF, - 0xFE, 0x21, 0x9E, 0x00, 0xE0, 0xFF, 0xED, 0xA5, 0x12, 0x92, 0xDB, 0xA2, - 0xE0, 0xC0, 0xFF, 0xFF, 0x7D, 0xFA, 0xFF, 0x00, 0x70, 0x00, 0xFF, 0x42, - 0x84, 0x0C, 0x00, 0x08, 0xC0, 0xC0, 0xFF, 0xFC, 0x7D, 0xFF, 0x90, 0x07, - 0xC0, 0x1F, 0xDB, 0x0A, 0x24, 0xC8, 0x68, 0x09, 0xCC, 0xC0, 0x7F, 0xFE, - 0x1E, 0x7F, 0x3E, 0x00, 0xD4, 0xFF, 0x3E, 0x60, 0x81, 0x21, 0x02, 0xA0, - 0xFC, 0xE0, 0x7F, 0xFF, 0xCE, 0x9F, 0xFF, 0x05, 0xFA, 0xFF, 0xBF, 0x04, - 0x24, 0x0A, 0x61, 0x1B, 0xFC, 0xC0, 0xBF, 0xFF, 0xAC, 0x7D, 0xFF, 0x03, - 0x40, 0x1F, 0x80, 0x91, 0xC2, 0xA0, 0x05, 0x80, 0xFF, 0x8D, 0x7F, 0xFF, - 0xEF, 0xF5, 0xDF, 0x00, 0x00, 0x94, 0x42, 0x81, 0x01, 0x10, 0x92, 0x85, - 0xFF, 0xFD, 0xFF, 0xFE, 0x2C, 0x62, 0x03, 0x00, 0x20, 0x00, 0x10, 0x2B, - 0x24, 0x82, 0x00, 0x20, 0xFF, 0xFD, 0xFF, 0xFF, 0xBD, 0x55, 0x01, 0x00, - 0x40, 0x00, 0x08, 0x19, 0x81, 0x58, 0x54, 0x14, 0xFF, 0xFF, 0xFF, 0xFF, - 0x3C, 0x82, 0x05, 0x08, 0x00, 0x24, 0x20, 0x81, 0x24, 0x02, 0x02, 0x82, - 0xFF, 0xFF, 0xFF, 0xFF, 0x3D, 0x82, 0x01, 0x00, 0x00, 0x00, 0x69, 0x22, - 0x90, 0xA0, 0x49, 0x06, 0xFF, 0xFF, 0xFF, 0xFF, 0x2D, 0x26, 0x00, 0x00, - 0x00, 0x00, 0x20, 0x20, 0x0A, 0x02, 0x20, 0xA0, 0xFF, 0xFF, 0xFF, 0xFF, - 0x04, 0x81, 0x01, 0x00, 0x10, 0x00, 0xA0, 0x04, 0x02, 0x0A, 0x09, 0x00, - 0xFF, 0xFF, 0xFF, 0xFF, 0x81, 0x19, 0x02, 0x02, 0x00, 0x00, 0x02, 0x42, - 0x50, 0xA0, 0x20, 0x95, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x60, 0x00, 0x01, - 0x80, 0x02, 0x42, 0x52, 0x40, 0x20, 0xA0, 0x20, 0xFF, 0xFF, 0xFF, 0x7F, - 0x00, 0x00, 0x8C, 0x01, 0x40, 0x00, 0x00, 0x00, 0x06, 0x06, 0x09, 0x05, - 0xFF, 0xFF, 0xFF, 0x3F, 0x00, 0x50, 0x00, 0x00, 0x40, 0x00, 0x02, 0x18, - 0xA0, 0x90, 0x84, 0x64, 0xFF, 0xFF, 0xFF, 0x3F, 0x00, 0x10, 0x81, 0x07, - 0xD0, 0x00, 0x48, 0x82, 0x02, 0x02, 0x20, 0x00, 0xFF, 0xFF, 0xFF, 0x0F, - 0x00, 0x04, 0xC0, 0x7E, 0x9F, 0x01, 0x10, 0x04, 0x00, 0x00, 0x24, 0x25, - 0xFF, 0xFF, 0xFF, 0x0F, 0x00, 0x60, 0x48, 0xC0, 0xC1, 0x13, 0x00, 0x41, - 0x9A, 0x99, 0x81, 0x00, 0xFF, 0xFF, 0xFF, 0x03, 0x40, 0x40, 0x60, 0x00, - 0x84, 0x0B, 0x00, 0x50, 0x02, 0x40, 0x24, 0x90, 0xFF, 0xFF, 0xFF, 0x01, - 0x00, 0x00, 0x32, 0x00, 0x00, 0x57, 0x00, 0x04, 0x40, 0x02, 0x01, 0x05, - 0xF8, 0xFF, 0xFF, 0x01, 0x40, 0x00, 0x38, 0x00, 0x00, 0x0E, 0x10, 0x05, - 0x99, 0x0A, 0xA4, 0x0A, 0xFC, 0xFF, 0x7F, 0x00, 0x80, 0x30, 0x10, 0x00, - 0x00, 0x84, 0x04, 0x60, 0x00, 0x50, 0x00, 0x60, 0xFC, 0xFF, 0xFF, 0x00, - 0x00, 0x3A, 0x08, 0x00, 0x00, 0x04, 0x8E, 0x06, 0x00, 0x41, 0x56, 0x00, - 0xF8, 0xFE, 0xFF, 0x01, 0x80, 0x79, 0xA0, 0x7F, 0xFF, 0x01, 0x06, 0x20, - 0x51, 0x08, 0x00, 0x2A, 0xF8, 0xFF, 0xFF, 0x00, 0xE0, 0x3F, 0xE0, 0xAF, - 0x00, 0x00, 0x06, 0x11, 0x91, 0x52, 0x02, 0x92, 0xF8, 0xFF, 0xFF, 0x00, - 0x00, 0x7F, 0x00, 0x00, 0x80, 0x02, 0x02, 0x08, 0x00, 0x00, 0x50, 0x00, - 0x7E, 0xFF, 0xFF, 0x01, 0x40, 0x7F, 0x00, 0x00, 0x10, 0x80, 0x86, 0x00, - 0x08, 0x09, 0x06, 0x08, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFC, 0x20, 0x51, - 0x0D, 0x20, 0x23, 0x94, 0x91, 0x50, 0x20, 0x92, 0xFF, 0xFE, 0xFF, 0x03, - 0x20, 0x70, 0x05, 0x88, 0x01, 0x80, 0x21, 0x06, 0x84, 0x44, 0x80, 0x25, - 0xFF, 0xFF, 0xFF, 0x03, 0x40, 0xEA, 0x90, 0x00, 0x20, 0xA4, 0x05, 0x00, - 0x00, 0x10, 0x26, 0x00, 0xFF, 0xFF, 0xFF, 0x07, 0x40, 0xD8, 0x08, 0x00, - 0x00, 0xA0, 0x81, 0x92, 0x01, 0x01, 0x84, 0x40, 0xFF, 0xFF, 0xFF, 0x0F, - 0x20, 0xC3, 0x42, 0x00, 0x00, 0x0A, 0x24, 0x00, 0x44, 0x48, 0x20, 0x25, - 0xFF, 0xFF, 0xFF, 0x0F, 0x20, 0xD5, 0x05, 0x02, 0x00, 0x46, 0x12, 0x40, - 0x02, 0x22, 0x44, 0x00, 0xFF, 0xFF, 0xFF, 0x1F, 0x90, 0x50, 0x90, 0x00, - 0x00, 0x9A, 0x40, 0x02, 0x40, 0x00, 0x10, 0x64, 0xFF, 0xFF, 0xFF, 0x1F, - 0x98, 0x62, 0x7C, 0x10, 0x11, 0xDF, 0x07, 0x14, 0x28, 0x08, 0x01, 0x05, - 0xFF, 0xFF, 0xFF, 0x1F, 0x20, 0x74, 0xF8, 0xE5, 0xEA, 0x87, 0x23, 0x01, - 0x81, 0x11, 0x48, 0x20, 0xFF, 0xFF, 0xFF, 0x1F, 0x50, 0x38, 0xF2, 0xFF, - 0xFF, 0xC7, 0x8F, 0x80, 0x00, 0x84, 0x40, 0x04, 0xFF, 0xFF, 0xFF, 0xBF, - 0x68, 0x3D, 0xE9, 0xFF, 0x7F, 0xC1, 0x1F, 0x28, 0x14, 0x10, 0x06, 0x50, - 0xFF, 0xFF, 0xFF, 0x3F, 0x90, 0x3F, 0x81, 0xFE, 0x5F, 0x89, 0x3F, 0x82, - 0x00, 0x02, 0xA2, 0x01, 0xFF, 0xFF, 0xFF, 0xBF, 0xB0, 0x3F, 0xB1, 0xFF, - 0xBF, 0xC1, 0xFF, 0x00, 0x82, 0x80, 0x00, 0x24, 0xFF, 0xFB, 0xFF, 0x7F, - 0xF8, 0x3F, 0xA9, 0xFC, 0x5F, 0x81, 0xFF, 0x0F, 0x08, 0x00, 0x04, 0x48, - 0xFF, 0xF0, 0xFF, 0xBF, 0xFC, 0x3F, 0x41, 0xEB, 0xE7, 0xC0, 0xFF, 0x1F, - 0x40, 0x24, 0x40, 0x01, 0xFF, 0xE0, 0xFF, 0xFF, 0xFF, 0x3F, 0xB3, 0x7D, - 0xAD, 0x80, 0xFF, 0xFF, 0x01, 0x00, 0xA0, 0x90, 0x7F, 0xC0, 0xFF, 0xFF, - 0xFF, 0x3F, 0x43, 0xFA, 0x27, 0xC0, 0xFF, 0xFF, 0x93, 0x51, 0x04, 0x04, - 0x7F, 0x80, 0xFF, 0xFF, 0xFF, 0x3F, 0x60, 0x6D, 0x56, 0xC0, 0xFF, 0xFF, - 0x1F, 0x20, 0x0A, 0x40, 0x7F, 0x80, 0xFF, 0xFF, 0xFF, 0x3F, 0x84, 0x6A, - 0x6D, 0xC0, 0xFF, 0xFF, 0x3F, 0x00, 0x00, 0x02, 0x7F, 0x00, 0xFF, 0xFF, - 0xFF, 0x3F, 0x2C, 0xBA, 0x22, 0xC0, 0xFF, 0xFF, 0xFF, 0x87, 0x04, 0x98, - 0x7F, 0x80, 0xFF, 0xFF, 0xFF, 0x3F, 0x48, 0x66, 0xD5, 0xC0, 0xFF, 0xFF, - 0xFF, 0x3F, 0x90, 0x02, 0x7F, 0xC0, 0xFF, 0xFF, 0xFF, 0x7F, 0x80, 0xA6, - 0x63, 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0x08, 0x24, 0x7F, 0xF8, 0xFF, 0xFF, - 0xFF, 0x7F, 0x90, 0x78, 0x3A, 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0x27, 0x00, - 0x7F, 0xFC, 0xFF, 0xFF, 0xFF, 0x7F, 0x00, 0xF9, 0x06, 0xF0, 0xFF, 0xFF, - 0xFF, 0xFF, 0x8F, 0x61, 0x7F, 0xFC, 0xFF, 0xFF, 0xFF, 0x7F, 0x00, 0xA4, - 0x0B, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0x04, 0x7F, 0xFE, 0xFF, 0xFF, - 0xFF, 0xFF, 0x00, 0xD0, 0x01, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x08, - 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x01, 0x00, 0xF8, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0x61, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x88, - 0x01, 0xF8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFE, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x48, - 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x07, 0xFC, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0x01, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0xD0, - 0x0F, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x69, 0xF8, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0x03, 0xFF, 0x0F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x05, - 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0xFF, 0x3F, 0xFE, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0x21, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x83, 0xFF, - 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0xF0, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xC3, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x6B, - 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0x23, 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xEF, 0xFB, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x27, - 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F, 0xF0, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0x0F, 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, - 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F, 0xF0, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0x03, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x8F, - 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0xF0, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0x1F, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, - 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x8F, 0xF0, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0x00, 0xF8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x2F, - 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xF8, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0x1F, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0x00, - 0xF8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 0xF0, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0x3F, 0x00, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x9F, - 0xF8, 0xFF, 0x07, 0xFF, 0xFF, 0xFF, 0x7F, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0x1F, 0xF0, 0xFF, 0x03, 0x00, 0xF1, 0xFF, 0x7F, 0x80, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 0xF0, 0xFF, 0x03, 0x00, - 0xF0, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0x7F, 0x6F, 0xFD, 0xFF, 0xFF, 0x1F, - 0xF8, 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0xFF, 0x80, 0xFF, 0xFF, 0x7F, 0x7F, - 0xFD, 0xFF, 0xFF, 0x7F, 0xF0, 0xFF, 0x03, 0x00, 0xC0, 0xFF, 0xFF, 0x90, - 0xFF, 0xFF, 0x5F, 0xA2, 0xF4, 0xFF, 0xFF, 0x3F, 0xF8, 0xFF, 0x03, 0x00, - 0x80, 0xFF, 0xFF, 0x80, 0xFF, 0xFF, 0x0F, 0xAA, 0x42, 0xF1, 0xFF, 0x3F, - 0xF8, 0xFF, 0xAF, 0x00, 0x80, 0xFF, 0xFF, 0xD9, 0xFF, 0xFF, 0x0F, 0x02, - 0x64, 0xF1, 0xFF, 0x3F, 0xF8, 0xFF, 0xFF, 0x3F, 0xC2, 0xFF, 0xFF, 0xDB, - 0xFF, 0xFF, 0xCB, 0xD1, 0xDA, 0xF7, 0xFF, 0x7F, 0xF8, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x71, 0x44, 0xFC, 0xF6, 0xFF, 0x3F, - 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xB0, 0xC0, - 0xA8, 0xFF, 0xFF, 0x3F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xBF, 0xFC, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, - 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0x7F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, }; +#define hopper_width 128 +#define hopper_height 128 +static char hopper_bits[] = { + 0xFF, 0xFF, 0xFF, 0x1F, 0xF0, 0x3D, 0x80, 0x96, 0xDA, 0xB6, 0xD6, 0x2A, + 0xA9, 0x6D, 0x25, 0x29, 0xFF, 0xFF, 0xFF, 0xBF, 0xFE, 0x7D, 0xC0, 0xFB, + 0x69, 0x69, 0xA9, 0xD5, 0x96, 0x96, 0x5E, 0x9A, 0xFF, 0xFF, 0xFF, 0x9F, + 0xFE, 0xFD, 0x80, 0xFF, 0x97, 0xB6, 0x5A, 0x6A, 0x5D, 0x6D, 0x5A, 0x62, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFD, 0xC1, 0xCF, 0x5F, 0x89, 0xA5, 0xD5, + 0xAA, 0x92, 0xA5, 0x59, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFD, 0xF8, 0x00, + 0xB8, 0x59, 0xAA, 0x56, 0xDA, 0x5A, 0x56, 0x49, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFE, 0xFC, 0xF9, 0x00, 0xF0, 0xA6, 0x55, 0xA9, 0x65, 0xA5, 0xA9, 0xA2, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFD, 0x71, 0x00, 0x60, 0x70, 0xAA, 0x54, + 0xAA, 0x5A, 0x54, 0x69, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFC, 0x38, 0x00, + 0xE0, 0x80, 0x55, 0xAB, 0x55, 0x75, 0xAB, 0x56, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFE, 0x1D, 0x70, 0x00, 0xF0, 0x00, 0xAE, 0xAA, 0xAA, 0xA6, 0x95, 0x55, + 0xFB, 0xFF, 0xFF, 0xFF, 0xFE, 0x1D, 0xF0, 0x00, 0xF0, 0x00, 0x50, 0x55, + 0x55, 0x59, 0x6A, 0xAA, 0xF3, 0xFF, 0xFF, 0xFF, 0xFE, 0x04, 0xF0, 0x00, + 0xF0, 0x00, 0xA4, 0xAA, 0xA9, 0xA5, 0x95, 0x55, 0x21, 0xFF, 0xFF, 0xFF, + 0xFE, 0x01, 0xF8, 0x00, 0xF0, 0x00, 0x50, 0x55, 0x56, 0x5A, 0x6A, 0xAA, + 0x83, 0x9F, 0xFF, 0xFF, 0xFE, 0x00, 0xFC, 0x00, 0xF0, 0x01, 0x50, 0xD5, + 0x95, 0x95, 0xA5, 0x5A, 0x83, 0x9F, 0xFF, 0xFF, 0xFE, 0x81, 0xFF, 0x00, + 0xE0, 0x0B, 0xA0, 0xAA, 0x7A, 0x6A, 0x5A, 0x65, 0x80, 0x1F, 0xF0, 0xFF, + 0xFE, 0xF0, 0xFF, 0x00, 0xF0, 0x3F, 0x50, 0x55, 0xA5, 0xB5, 0xA5, 0xAA, + 0x00, 0x1F, 0xF8, 0xFD, 0xFE, 0xF0, 0xF7, 0x00, 0xF0, 0x6D, 0xA0, 0xAA, + 0x5A, 0x6A, 0x5A, 0x55, 0x83, 0x0F, 0xF8, 0xC9, 0xFE, 0x79, 0x00, 0xF8, + 0x02, 0xC0, 0x81, 0x55, 0xAD, 0xD5, 0xA5, 0xA6, 0xE3, 0x03, 0xFC, 0xC0, + 0xFE, 0xFF, 0x04, 0xF0, 0x00, 0x80, 0x63, 0xAA, 0x52, 0x2A, 0x5A, 0x58, + 0xE7, 0x03, 0xFC, 0xE1, 0xFE, 0xFF, 0x2F, 0xFC, 0x03, 0x0C, 0xFE, 0x55, + 0xA9, 0x55, 0x66, 0x15, 0xF3, 0x0F, 0x7C, 0xF0, 0xFE, 0xFF, 0xFF, 0xFD, + 0xEF, 0x38, 0xFF, 0xAA, 0x56, 0xAA, 0x99, 0xAA, 0xF7, 0x1F, 0x78, 0xF0, + 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xF1, 0xFF, 0x55, 0xAA, 0x5B, 0x66, 0xA4, + 0xFF, 0x9F, 0xFD, 0xF0, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xAB, + 0x65, 0xA4, 0x99, 0x1B, 0xFF, 0xDF, 0xFF, 0xE0, 0xFE, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x69, 0x51, 0x5A, 0x24, 0x62, 0xFF, 0xDF, 0xFF, 0xEC, + 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x95, 0xAA, 0x24, 0x59, 0x55, + 0xFF, 0xFF, 0xFF, 0xFC, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x25, + 0x49, 0xAD, 0x55, 0x2A, 0xFF, 0xFF, 0xFF, 0xFC, 0xFD, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x9B, 0xA5, 0x52, 0x2A, 0x94, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x67, 0x64, 0xC1, 0x92, 0x6D, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x95, + 0x49, 0x2C, 0x55, 0x92, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0xFF, 0xFF, 0xFF, + 0x7F, 0xFF, 0xFF, 0x15, 0xB4, 0x62, 0x44, 0x49, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFD, 0xFF, 0x51, 0x30, 0x03, 0xE0, 0xFF, 0x53, 0x03, 0x89, 0x96, 0x92, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFD, 0x7F, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xCD, + 0x68, 0x19, 0x68, 0x99, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x1F, 0x00, 0x00, + 0x00, 0x00, 0xFF, 0x13, 0x4A, 0x64, 0x23, 0x49, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x47, 0x8A, 0xA4, 0xD8, 0x30, + 0xFE, 0xFF, 0xFF, 0xFF, 0xFC, 0x07, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x39, + 0x29, 0x49, 0x06, 0x86, 0xCC, 0xFF, 0xFF, 0xFF, 0xFC, 0x03, 0x00, 0x00, + 0x80, 0x00, 0xF2, 0x81, 0x50, 0x31, 0xA2, 0x8C, 0xE4, 0xFF, 0xFF, 0xFF, + 0xFF, 0x0A, 0x08, 0x00, 0x00, 0x00, 0xF0, 0xA7, 0x46, 0x8C, 0x28, 0x31, + 0xC0, 0xFC, 0xFF, 0xFF, 0xFC, 0x03, 0x10, 0x00, 0xE0, 0x01, 0xE0, 0x19, + 0x41, 0x61, 0x66, 0x05, 0xE0, 0xB8, 0xFF, 0xFF, 0x7E, 0x0C, 0x44, 0x00, + 0x40, 0xBF, 0xEF, 0x41, 0x5C, 0x04, 0x00, 0xA8, 0xF0, 0xC1, 0xFF, 0xFF, + 0xFE, 0x21, 0x9E, 0x00, 0xE0, 0xFF, 0xED, 0xA5, 0x12, 0x92, 0xDB, 0xA2, + 0xE0, 0xC0, 0xFF, 0xFF, 0x7D, 0xFA, 0xFF, 0x00, 0x70, 0x00, 0xFF, 0x42, + 0x84, 0x0C, 0x00, 0x08, 0xC0, 0xC0, 0xFF, 0xFC, 0x7D, 0xFF, 0x90, 0x07, + 0xC0, 0x1F, 0xDB, 0x0A, 0x24, 0xC8, 0x68, 0x09, 0xCC, 0xC0, 0x7F, 0xFE, + 0x1E, 0x7F, 0x3E, 0x00, 0xD4, 0xFF, 0x3E, 0x60, 0x81, 0x21, 0x02, 0xA0, + 0xFC, 0xE0, 0x7F, 0xFF, 0xCE, 0x9F, 0xFF, 0x05, 0xFA, 0xFF, 0xBF, 0x04, + 0x24, 0x0A, 0x61, 0x1B, 0xFC, 0xC0, 0xBF, 0xFF, 0xAC, 0x7D, 0xFF, 0x03, + 0x40, 0x1F, 0x80, 0x91, 0xC2, 0xA0, 0x05, 0x80, 0xFF, 0x8D, 0x7F, 0xFF, + 0xEF, 0xF5, 0xDF, 0x00, 0x00, 0x94, 0x42, 0x81, 0x01, 0x10, 0x92, 0x85, + 0xFF, 0xFD, 0xFF, 0xFE, 0x2C, 0x62, 0x03, 0x00, 0x20, 0x00, 0x10, 0x2B, + 0x24, 0x82, 0x00, 0x20, 0xFF, 0xFD, 0xFF, 0xFF, 0xBD, 0x55, 0x01, 0x00, + 0x40, 0x00, 0x08, 0x19, 0x81, 0x58, 0x54, 0x14, 0xFF, 0xFF, 0xFF, 0xFF, + 0x3C, 0x82, 0x05, 0x08, 0x00, 0x24, 0x20, 0x81, 0x24, 0x02, 0x02, 0x82, + 0xFF, 0xFF, 0xFF, 0xFF, 0x3D, 0x82, 0x01, 0x00, 0x00, 0x00, 0x69, 0x22, + 0x90, 0xA0, 0x49, 0x06, 0xFF, 0xFF, 0xFF, 0xFF, 0x2D, 0x26, 0x00, 0x00, + 0x00, 0x00, 0x20, 0x20, 0x0A, 0x02, 0x20, 0xA0, 0xFF, 0xFF, 0xFF, 0xFF, + 0x04, 0x81, 0x01, 0x00, 0x10, 0x00, 0xA0, 0x04, 0x02, 0x0A, 0x09, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0x81, 0x19, 0x02, 0x02, 0x00, 0x00, 0x02, 0x42, + 0x50, 0xA0, 0x20, 0x95, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x60, 0x00, 0x01, + 0x80, 0x02, 0x42, 0x52, 0x40, 0x20, 0xA0, 0x20, 0xFF, 0xFF, 0xFF, 0x7F, + 0x00, 0x00, 0x8C, 0x01, 0x40, 0x00, 0x00, 0x00, 0x06, 0x06, 0x09, 0x05, + 0xFF, 0xFF, 0xFF, 0x3F, 0x00, 0x50, 0x00, 0x00, 0x40, 0x00, 0x02, 0x18, + 0xA0, 0x90, 0x84, 0x64, 0xFF, 0xFF, 0xFF, 0x3F, 0x00, 0x10, 0x81, 0x07, + 0xD0, 0x00, 0x48, 0x82, 0x02, 0x02, 0x20, 0x00, 0xFF, 0xFF, 0xFF, 0x0F, + 0x00, 0x04, 0xC0, 0x7E, 0x9F, 0x01, 0x10, 0x04, 0x00, 0x00, 0x24, 0x25, + 0xFF, 0xFF, 0xFF, 0x0F, 0x00, 0x60, 0x48, 0xC0, 0xC1, 0x13, 0x00, 0x41, + 0x9A, 0x99, 0x81, 0x00, 0xFF, 0xFF, 0xFF, 0x03, 0x40, 0x40, 0x60, 0x00, + 0x84, 0x0B, 0x00, 0x50, 0x02, 0x40, 0x24, 0x90, 0xFF, 0xFF, 0xFF, 0x01, + 0x00, 0x00, 0x32, 0x00, 0x00, 0x57, 0x00, 0x04, 0x40, 0x02, 0x01, 0x05, + 0xF8, 0xFF, 0xFF, 0x01, 0x40, 0x00, 0x38, 0x00, 0x00, 0x0E, 0x10, 0x05, + 0x99, 0x0A, 0xA4, 0x0A, 0xFC, 0xFF, 0x7F, 0x00, 0x80, 0x30, 0x10, 0x00, + 0x00, 0x84, 0x04, 0x60, 0x00, 0x50, 0x00, 0x60, 0xFC, 0xFF, 0xFF, 0x00, + 0x00, 0x3A, 0x08, 0x00, 0x00, 0x04, 0x8E, 0x06, 0x00, 0x41, 0x56, 0x00, + 0xF8, 0xFE, 0xFF, 0x01, 0x80, 0x79, 0xA0, 0x7F, 0xFF, 0x01, 0x06, 0x20, + 0x51, 0x08, 0x00, 0x2A, 0xF8, 0xFF, 0xFF, 0x00, 0xE0, 0x3F, 0xE0, 0xAF, + 0x00, 0x00, 0x06, 0x11, 0x91, 0x52, 0x02, 0x92, 0xF8, 0xFF, 0xFF, 0x00, + 0x00, 0x7F, 0x00, 0x00, 0x80, 0x02, 0x02, 0x08, 0x00, 0x00, 0x50, 0x00, + 0x7E, 0xFF, 0xFF, 0x01, 0x40, 0x7F, 0x00, 0x00, 0x10, 0x80, 0x86, 0x00, + 0x08, 0x09, 0x06, 0x08, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFC, 0x20, 0x51, + 0x0D, 0x20, 0x23, 0x94, 0x91, 0x50, 0x20, 0x92, 0xFF, 0xFE, 0xFF, 0x03, + 0x20, 0x70, 0x05, 0x88, 0x01, 0x80, 0x21, 0x06, 0x84, 0x44, 0x80, 0x25, + 0xFF, 0xFF, 0xFF, 0x03, 0x40, 0xEA, 0x90, 0x00, 0x20, 0xA4, 0x05, 0x00, + 0x00, 0x10, 0x26, 0x00, 0xFF, 0xFF, 0xFF, 0x07, 0x40, 0xD8, 0x08, 0x00, + 0x00, 0xA0, 0x81, 0x92, 0x01, 0x01, 0x84, 0x40, 0xFF, 0xFF, 0xFF, 0x0F, + 0x20, 0xC3, 0x42, 0x00, 0x00, 0x0A, 0x24, 0x00, 0x44, 0x48, 0x20, 0x25, + 0xFF, 0xFF, 0xFF, 0x0F, 0x20, 0xD5, 0x05, 0x02, 0x00, 0x46, 0x12, 0x40, + 0x02, 0x22, 0x44, 0x00, 0xFF, 0xFF, 0xFF, 0x1F, 0x90, 0x50, 0x90, 0x00, + 0x00, 0x9A, 0x40, 0x02, 0x40, 0x00, 0x10, 0x64, 0xFF, 0xFF, 0xFF, 0x1F, + 0x98, 0x62, 0x7C, 0x10, 0x11, 0xDF, 0x07, 0x14, 0x28, 0x08, 0x01, 0x05, + 0xFF, 0xFF, 0xFF, 0x1F, 0x20, 0x74, 0xF8, 0xE5, 0xEA, 0x87, 0x23, 0x01, + 0x81, 0x11, 0x48, 0x20, 0xFF, 0xFF, 0xFF, 0x1F, 0x50, 0x38, 0xF2, 0xFF, + 0xFF, 0xC7, 0x8F, 0x80, 0x00, 0x84, 0x40, 0x04, 0xFF, 0xFF, 0xFF, 0xBF, + 0x68, 0x3D, 0xE9, 0xFF, 0x7F, 0xC1, 0x1F, 0x28, 0x14, 0x10, 0x06, 0x50, + 0xFF, 0xFF, 0xFF, 0x3F, 0x90, 0x3F, 0x81, 0xFE, 0x5F, 0x89, 0x3F, 0x82, + 0x00, 0x02, 0xA2, 0x01, 0xFF, 0xFF, 0xFF, 0xBF, 0xB0, 0x3F, 0xB1, 0xFF, + 0xBF, 0xC1, 0xFF, 0x00, 0x82, 0x80, 0x00, 0x24, 0xFF, 0xFB, 0xFF, 0x7F, + 0xF8, 0x3F, 0xA9, 0xFC, 0x5F, 0x81, 0xFF, 0x0F, 0x08, 0x00, 0x04, 0x48, + 0xFF, 0xF0, 0xFF, 0xBF, 0xFC, 0x3F, 0x41, 0xEB, 0xE7, 0xC0, 0xFF, 0x1F, + 0x40, 0x24, 0x40, 0x01, 0xFF, 0xE0, 0xFF, 0xFF, 0xFF, 0x3F, 0xB3, 0x7D, + 0xAD, 0x80, 0xFF, 0xFF, 0x01, 0x00, 0xA0, 0x90, 0x7F, 0xC0, 0xFF, 0xFF, + 0xFF, 0x3F, 0x43, 0xFA, 0x27, 0xC0, 0xFF, 0xFF, 0x93, 0x51, 0x04, 0x04, + 0x7F, 0x80, 0xFF, 0xFF, 0xFF, 0x3F, 0x60, 0x6D, 0x56, 0xC0, 0xFF, 0xFF, + 0x1F, 0x20, 0x0A, 0x40, 0x7F, 0x80, 0xFF, 0xFF, 0xFF, 0x3F, 0x84, 0x6A, + 0x6D, 0xC0, 0xFF, 0xFF, 0x3F, 0x00, 0x00, 0x02, 0x7F, 0x00, 0xFF, 0xFF, + 0xFF, 0x3F, 0x2C, 0xBA, 0x22, 0xC0, 0xFF, 0xFF, 0xFF, 0x87, 0x04, 0x98, + 0x7F, 0x80, 0xFF, 0xFF, 0xFF, 0x3F, 0x48, 0x66, 0xD5, 0xC0, 0xFF, 0xFF, + 0xFF, 0x3F, 0x90, 0x02, 0x7F, 0xC0, 0xFF, 0xFF, 0xFF, 0x7F, 0x80, 0xA6, + 0x63, 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0x08, 0x24, 0x7F, 0xF8, 0xFF, 0xFF, + 0xFF, 0x7F, 0x90, 0x78, 0x3A, 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0x27, 0x00, + 0x7F, 0xFC, 0xFF, 0xFF, 0xFF, 0x7F, 0x00, 0xF9, 0x06, 0xF0, 0xFF, 0xFF, + 0xFF, 0xFF, 0x8F, 0x61, 0x7F, 0xFC, 0xFF, 0xFF, 0xFF, 0x7F, 0x00, 0xA4, + 0x0B, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0x04, 0x7F, 0xFE, 0xFF, 0xFF, + 0xFF, 0xFF, 0x00, 0xD0, 0x01, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x08, + 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x01, 0x00, 0xF8, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x61, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x88, + 0x01, 0xF8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFE, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x48, + 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x07, 0xFC, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x01, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0xD0, + 0x0F, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x69, 0xF8, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0x03, 0xFF, 0x0F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x05, + 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0xFF, 0x3F, 0xFE, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x21, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x83, 0xFF, + 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0xF0, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xC3, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x6B, + 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x23, 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xEF, 0xFB, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x27, + 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F, 0xF0, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x0F, 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, + 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F, 0xF0, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x03, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x8F, + 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0xF0, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x1F, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, + 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x8F, 0xF0, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x00, 0xF8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x2F, + 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xF8, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x1F, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0x00, + 0xF8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 0xF0, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0x3F, 0x00, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x9F, + 0xF8, 0xFF, 0x07, 0xFF, 0xFF, 0xFF, 0x7F, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x1F, 0xF0, 0xFF, 0x03, 0x00, 0xF1, 0xFF, 0x7F, 0x80, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 0xF0, 0xFF, 0x03, 0x00, + 0xF0, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0x7F, 0x6F, 0xFD, 0xFF, 0xFF, 0x1F, + 0xF8, 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0xFF, 0x80, 0xFF, 0xFF, 0x7F, 0x7F, + 0xFD, 0xFF, 0xFF, 0x7F, 0xF0, 0xFF, 0x03, 0x00, 0xC0, 0xFF, 0xFF, 0x90, + 0xFF, 0xFF, 0x5F, 0xA2, 0xF4, 0xFF, 0xFF, 0x3F, 0xF8, 0xFF, 0x03, 0x00, + 0x80, 0xFF, 0xFF, 0x80, 0xFF, 0xFF, 0x0F, 0xAA, 0x42, 0xF1, 0xFF, 0x3F, + 0xF8, 0xFF, 0xAF, 0x00, 0x80, 0xFF, 0xFF, 0xD9, 0xFF, 0xFF, 0x0F, 0x02, + 0x64, 0xF1, 0xFF, 0x3F, 0xF8, 0xFF, 0xFF, 0x3F, 0xC2, 0xFF, 0xFF, 0xDB, + 0xFF, 0xFF, 0xCB, 0xD1, 0xDA, 0xF7, 0xFF, 0x7F, 0xF8, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x71, 0x44, 0xFC, 0xF6, 0xFF, 0x3F, + 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xB0, 0xC0, + 0xA8, 0xFF, 0xFF, 0x3F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xBF, 0xFC, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, + 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x7F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, }; diff --git a/Tests/images/hopper.xpm b/Tests/images/hopper.xpm index 27f51a4fe..8f5e824a6 100644 --- a/Tests/images/hopper.xpm +++ b/Tests/images/hopper.xpm @@ -1,178 +1,178 @@ -/* XPM */ -static char *hopper[] = { -/* columns rows colors chars-per-pixel */ -"128 128 44 1 ", -" c #110E13", -". c #2A1110", -"X c #13122C", -"o c #25192A", -"O c #312831", -"+ c #302219", -"@ c #5F241A", -"# c #6F4832", -"$ c #17174A", -"% c #26264F", -"& c #65595D", -"* c #4B3C54", -"= c #8E1F1A", -"- c #914E33", -"; c #A75335", -": c #AC6736", -"> c #B26F4E", -", c #9B6657", -"< c #CA7652", -"1 c #CC7866", -"2 c #C25135", -"3 c #B0896D", -"4 c #D28859", -"5 c #D28E6D", -"6 c #E79371", -"7 c #E5A36E", -"8 c #D58932", -"9 c #3E65AE", -"0 c #4970B5", -"q c #606D95", -"w c #557CC1", -"e c #91778A", -"r c #6A86B9", -"t c #5C83C3", -"y c #6D8FCA", -"u c #7799E3", -"i c #A89A9D", -"p c #CFAA90", -"a c #F4B08F", -"s c #D6A799", -"d c #ECCCAB", -"f c #F9F4F4", -"g c #E3D8D5", -"h c #AAAECA", -/* pixels */ -"$XX$$XXX$%$$$$$$$$$$$$$$$$$$$egip3dr %% @p# Oqgffgfgfgo%q999q9999999999999999999099999099099990999999909909990w999000wwww00w00w0", -"$$X$X$XX$$$$$$$$9$$$$XX$$$$$$ws&7+O&%$Xo@3#XXX&ihffggg%%9q*9*%q99qq9q99999999999999090900909909909099999999999999999000wwwwww0ww", -"X$X$$XXX$$$$$$$$$$$$$$$$$$$$$eq@i#+$$$%X#pOX$%$%*dfffgiX%%OX%XO*%**990909999999009090990900999999099990999w999990990000wwwwwwwww", -"$XX$$$XX$$$$$$$$$$$$XX$$$$$$$*O*7#XX%$%X@p*XX$%Xohfi3hOXOoX*r3*OOO%O99099999q0q990990009090909000999090999999099909999ww0w0000w0", -"$XX$$XXX$$$$$$$$$9$$$$$$$$$$o$.&3++X$$$o#p+X$$%$OhgOX oo+ii*if&+ri&OO%q0990099009090090990909009990999099w999990999090w0000w0ww0", -"X$X$$XXX$$$$$$$$$$$$$$$$$$$$$oX#p#OO%$$o#7O%X$$XOg&O o.&fffgifirfffiOO*&qq990990000000009909099000990990999990999w90099000000000", -"XX$$$XXX$$$$$$$$$$$$$$$$$$$$$oo*3+o$$%$.#7+X$%XOOhii+X,fgifffffffddfgOOrhi0qq000090000090099990909909009w9w9909990999099900000w0", -"X$X$$$XX$$$$$%$$$$$$X$X$$$$$X$X#7@X$$$$o:3+XX$OqgffrXOe&&#*&iggi&&3&*&OOfffgrq900009000099099090000909009999990999900900000000w0", -"X$q$XXXX$$$X%$$$$$$$X$$$$$$$$$X*p+O$%$$o-3oOX%gffff&o++3pgg3fddd3fdg&+OOhffffgt0099090090009090990909990999990999000099090090000", -"X$g&$XXX%$X$%$$$$$$$$$X$$$$$$$o*p+X%$%$X,5+ *gfffff& ++&pddffffffgdd#OOOiffffffhq9qt009w990090900999w9w9900999w90999909009900000", -"X$ggO %$$$X$%$$$$$$$$$$$$$$$Xo*3#$$%%$X,3+&ffffffg#+.+Odddgggggggpd*+OOifffffffgtqt09t909090990090909099990099w0990090909090000", -"X%hfr&h&$$$$%q$X$$$$XXX$$$$$Xoo*p@o$$$$X:3OffffgppdO. ++,ddfffffffd3+ ++&ffffffffgq900909090900999099090w9w990990009009090009900", -"Xohfffh&X$o$$h&$$$$$$$X$$$$$Xo$*7# %%$$ 35dffg73&#++++ +pd3gfffffg3d,++++3ddffffffhq90009099990999909090990909w99990099900900000", -"oegffgiO$$$$$gh$$%&qXXXX$%XX$X.*3# X%X$+#pgfd3&+. + ++3iddgfffddpd&+O++++#pgfffffrq9900900q99090909990099009990009990999090000", -"hffffg&X$%$X$gg*&hg*XXXX%i%$$Xo*5#XX$$$o,dfg&+.. .#&++ +&ddd3gfgdddd#++++&+++#3dfgghq009099090000909090990999090909000099000000w", -"hgffffp*X$X%Ogggfgh$XXX$%g*XXooO5#o$%$$+&df3...&#++3 O#+#pd7di&,ppdd&+++#3+&&..#iffgqq009009009909900990090009990009999009990000", -"o%qffgh&o%o*iffgfg%%X$XX*gi OqO*p#+$%$XO&h3 . +pdpp3pdgdd3#+++++++#ppggpp3ddp+##+ifghqrq09090090090090900909999009009w900009w0w9", -"Xo*fh**%$%igfgfgg&%$$X$X&ggih3o+p# X%X$XX .+33+pdppdddfp,#+O&dfggdddd3pdddd+++dh&%%&q0090909009909090090000900w000w99w0900ww", -"X%&fq%oX$%&hgfffgi%oXXX &fgshOoO3#+X%%XX+ . +#&++7dp3&#O&O&#&33dd3#3&.+#ppiiO#+ X%9909000909099090090900009w9w009ww9ww9w00", -"X$%g*o$XX%Xo*gffffq$X%&egfhp&oXO7# XXX + #++.+++ +..O+ ++O++#ppp+++&3++ o XXq9000900009000090009000009w909w09w00w00w", -"$$*i$$oX$XoX%hfeegg%$%&idggh+X$O4#+XOX + . + ..o + + ++ +&3gg#. X999909000000000000009000000000w09w0www0w", -"$$%%$$$X$$o$$hh*$$q&$o o3ggd,ooo3#+ ++ +++ ++*++ X%0q9q900000ww000000000000ww99w00w9w99ww0", -"$$$$%%X$$$X$%g0$$%%%oX .&fshg*Xo3: X + + X*0000000000w000000000w00000ww0www90wwww0", -"$%$$%$$XX$$X%h$%$%$$XXX qg*XiqXo3:+ %0q0000000w00000w0ww000w0ww990009w0w99ww", -"$$$%%%$$$XX$$%%$%$$$XXXXihXXX%X.,: %t000t90w0w0000090w00w00009w000wwww9www9", -"$$$$$%X$X$$$$%$$$$$$XXXXq*XXXXXo3: . %0q0000t0w0ww00ww00ww00ww0w9w0w0w99ww9ww", -"$$$$$$$XX$X$$$$$$$$$$XXX*$$XXXX :> %q0t0t00w0w0w0w00ww0w0w0w00w0w0www90w09w", -"$$$$$$XX$$XX$$$$%$$$$XXX$$$$XXX.:3. . . .... .. . ....... X%qw000t00www0wwwwwww0ww0www0w00w00w09w9w", -"$$$$%%$$$XX$$$$$$$$$XXXX$$$$XXX -3 . ....+@@@@@@.@@....@@@@#####*@.. %ttt9ttt0t0tw0www0ww0ww00w9ww0w09w9www0w", -"$$$%$%$$X$$$$$$$%%9$XXXX$$$$XXX :3 ...#->1>>1,>>,,--;,--,,,1531>13>#@. X09q9r00t0t0wwwww0w0wwwwwwww0wwwwww0w00w", -"$$$$$$XXX$$$$$$$$$$$XX X$$$$$XX.:: .#11ss65assa56a511155557a775555p5,,. Xqttr0trt0wtwwwwww0ww0wwwwww0wwwww0ww0ww", -"$$$$$$$X$$$$9$9$$$$$XXXXX$$$XX$ >;. ...#,pps55paaaaasadasaaaaaaaaa777a77p53,*... Xt0tq9t0ttwww0wwwwwwww0wwww0wwww0wwwwwww", -"*o$%$%$XX$$$$$$$%$$$$XXXX$$$$XXX,> +#*-,5pp7apasaadddaaaaddadaaadddaaa6pp733,##. Xqrtttttt0t0rt0tt000t0t0t0t00t0t0ttt0ttt", -"go%$$*%$XXX$$$$$$$$$$X XX$$$$XX :> .&,#,5p77p7aaaaadaaadaddaaadaaaasaapp7775534#@ Xr90tq0ttt0tt0ttt0ttttt00tttt0tt0t0tttt0", -"fio%whXXX%$$$$$$$$$$$XXX$$$$X$X.,8. o###-155pppadaasasaaadaaaaaaaaa51>3355455535>>#+ %t0r0r0rttt00tt0ttttttt0tttttt0ttttttttt", -"gg&gf&X *qo$$$$$$$$$$X X$$$$$XX 3: OO##,135565431>113sadaaaddsss611>1>>55s5355333,*+ %rr0r0ttt0tttttrtt0ttqttt0ttttttttt0tttt", -"gffghOXX*g*$$$%$$$$$XXXX$$$$XXX -> +###,>3575p5<><355saaaaaaaap615>;;-;>5a7p335333#+ %rrtttt0t0trtttttttttttttttttttttttttttt", -"fgfg&oo &giO$qi%X$$$XX XX$$$XX .::+ +#,,,3355p55>>;;;,>16s777aaas61>;=--@#@@@@##&##&&+X*rrtttttttt0tttttttttttttttttttttttttttt", -"gggg&o &gg,sfe$X$$$$X XX$$$XXX :: +&#-,,35333>>,-,-,>>15apaadap1>>;@@@#;-##@+,&@##&O+&rttttt0ttttttt0tttttttttttttttttttttttt", -"gffgg*oX&gfggh%X$X$$XX X%%XXX 3:+ ++3:-####@@.@..@++@@->6aaaaaa74>=@,3,11335d&+@+O*&##qrrttttytttttttrtttttttttttttttttttttttt", -"sggggho*dgggg&O$$$$$ *+Oii$X X .::+++&,-##+@+@@:ss1,1i1--=:4a77764::a1,@@@@=,,4d#+##*&,-eqyrwyrwttttttttttttttytytttytytttyttttt", -"id**qi&Ogggdd+X$$$$$o*&eh*X XXX.::.O#,>-#@...+,,,-@@@@-5><<<76748:2<>-@#.@@@@=-@##@@+#,isiryywyyrttrttttyttytytttttytttttttttytu", -",hO%X$Xo&gggg&O$$$$XoO,e&OXXXXX.3:.@33;#+@@@.-,>-@.@..@=::;<4<4<:28<2@#@.@@@@==-,-##-,1,eheywyywyttttyytytyttttttyttttttttytyttt", -"i&o$$X Ogggfg%X$$$$oO,&+ XX X :3+@3,,;##@#@##,@@@..@-;==;426a76<<<;>;--#==;;::4>4>:;:==1ryewywttyrtytttytyytyyttyttuytyttyttyt", -"**%$$$XX+hiO&h&%$$$XX*,,& XXX X ::+#1-#-;,,>,#-==#=#;-;<4444;6ad7<661<<11>2;2<<<445<>:<<@,ryytytyytyyttyyttytyttytyttyttyttyuytt", -"%X$$$$X Og*o%$*X$$$$X*&&i* XXXX :3.#,=;>,-35:-#===:4s51446a6;6aaa6661<66aaa6<664445>:15<-,ewyytyytytyyytyyytytytytyyytyyttuttttu", -"X$$$$XXXOi$$$$$X$$$XXX.o&qXo X .>>.#-@>;-:<6:<;==2<66a6<<6a<-:44:<82<<11<<=54;;<<<<1<;2<<<1aada6:7adadd74aaadaa6446764444;=>s,eitryyyyyyyyyyyyttyyyyyyyytyyttyttutytu", -"XX$$$XXXX$$$$XX$$$$$XXXXXX$X$X .::.@,;6<-;<<1<4::46aadddaa46aadddad66aaaaaa7a76564>4<-;51eryyttyyyttryytyyyyyryrtuyytyyyyyyttytt", -"X$$$$XX XX$$$$X$$$XXXXX XXXXXXX :8+ia>44;;<<<44<;87dadddda>5a221eiiwtyyyyyyyyyyyyyyyyyyytyytytyytttytuyy", -"$X$X$X XX$X$$X$$$$$XX XX$XX$XX #73pda6<>56>2<44<26addaa6>:6adaaadda2;16666a7a6<4>47a1<1ieyyyyytyyytyyyyyyryyyyyyyyytutyyuyyytyy", -"X$$$XX XX$$$XX$$$$XXXX XXXXXX :75ps565577>:<:46<<1aa11;;6aaadddaada<2>2;<6676<<435a1>5eiyytyyyyyyyyyyyyytyyyyyyyyyyyyyyytyyuty", -"XX$$$XX XX$$$$$$$$X$XXXXXXXXX X3773sdp565p742<:<61<2;2;2=<66aaaaadaaa1>6a66a665<5>456>15ewyyyyyyyytyyyyyyyyyyyyytyytyyyyytuyyttu", -"$$$XXX X$$$$$X$$$XXXXXXXXXX &pp73dd357577<<:>481611612;661a1saas6111;21aa664444>56<163iyyytyyyyyyyyyyyyyyyyryyutyyyyyyyyyyyyyy", -"$X$$XX XX$$X$$$$XXXXXXXXXX *ip773ss345775<<<848111aa1;=;-=><1511;;====116166165>4>6615rtyyyyyyyyyyyyyyyyyyyyyyyyyyyyytyyyyytuy", -"X$$X$X XX$$$XX$X$XXXXX X XX*ipp773dd33783::<<<4<<6664;=6<@===-==#;=;1;@21<<646655>>>>6iryyyryyyyyyyyyyyyyyyyyywyryyyyyyyyyyyutu", -"XXX$XX X$$$$$$X$XXXX XXXX&iipp783pp33735771<<46<644<=<66641,-@@:41112==2<4<44545a775piiyyyyyryyyyyyyyyyyyywyyyyyywyyyytyyyutyy", -"XX$*XX XX$$$$$$$$XXXXXXX oriipsp784dp333a7p761446<442=2661666611<6161<1<@=;2<46555aa5ppqryyyyrryryyyyyyyyyyyyyryywyywyywuytyyyyy", -"*oe*XX XXXXX$XXX$XXX Xoiihhisp774ppp837a7dp>4464<<2=<16666666666116661===;<1<445a77p3iryyyyyyyyyyyyyyyyyryyyyyyyyyyyyytyyyyyyy", -"iiiXXXX XXXXX$XX$X X o@iihishpp788dpp#:7p7a,3447842=;166aaa6aaaaaa16666<;=;<<4643ap33iirryyyryyryyyyyyyyyyryyyyyyyyyyyytyyttyyy", -"di&o o %*%XXXX$XXXX Oeiishippp773dd3:#:57>.-47664;@8<61a6a1aaaaaa676a11<=<<6548->53iirrryyyyyyyyyyyyyyyyyyyryyyrryyywyyyyyutuy", -"ppO.oX *q&XXXXXX$XXX..@,eshsipdp783pp3:##:,@.#:4474<:<<11<>1<1661<21<<<6<6;166643##,7irrryyyyyyyyyyyyyryyyyyywyyyryrywtyyyyyyyty", -"di*+X. *&OoXXXXXXX$ .o@&,sshsiip377pd3,##:3,@@4466718<<<2=====22=2===-=;2<6676158##333iryyyyytyyyyyyyyyyyyyyyyyyyyyyyyyyytytyyyy", -"gge. *OoXXXXXXXXXX..@@,3shsipp778dp3,-@@@@+@-46667<22==;22,22=2111<6<<<<8677453##33iriyyyyyyyyyyyyyyyyyyyyyyyryyyyytyyyyyyyyyy", -"esi* &*XXXXXXXXXX .@@=;ppssppp77377:-:@+.+++-446774<<<1116666aa16a61<66<6766558#,3ihrryyyyyyyyyyyyyyyyyyyrryyyyyyyyyytytyyyyyy", -"&.&eo oi& XXX$XXXXXX.@@@=ishhpsp873p3,:28@@.@+=4<4774868<1116aa6a611<88668664453##&3iryyyyyyyyhyyyyyyyyyyyyyyyyyyyryyyyyytyyyyyy", -"oXXo XX&eXXXXX XXXXX..=@@1ssipsp875p3::888:@.@@;44464<68<<2112;;;22<<6664<16443>##3iiryhuyyyyyyyyyyyyryyryyyyrryyyyyyryyyyyyyyyy", -"XXXXX X&OXXXXXXXX .@@===6ssis577377>::88882@@-:<<68<66611<2;;;2=;:<<<<644<<<5-#3&3iyhyyyyyyyyyyyyyyyyyyyyyyyyyryyyyyryyyyyyyyy", -"XXXX X %OoXXXXXXXX..@=@=@1ssis578377:::82822;@;:4<8244<8<1<46a56144<<<656<6<<1#&eerryryyyyyyuyyyyyyyyyyyyyyyyyyyyyyyyyytytytyty", -"XXXXX XXXXXXXXX..@=@====sspp578877:8228282;=;<<><46666666aaaaaa66777a5<4<1,i-&3riryyyyyyyyyuyyyyyyyyyyyyyyyyyyyyyyyryyyyyyyyy", -"X$XXX XXXXXX ..@@==@===1ppp5878p8222828282==<:<<6<<<84667aaaa6aa777744<;113,eiryyyyyyyyyyuyuyhyyyiyyyyyyyyyyyyyyyyyyyytyyyyy", -"XXXXX XXXXXX X .@===@=====1ap387838:8:282822=@>4:>44>44644666a66aaa744>>:::>3,erryyuuyyyyyyyyhyyyyyyuyyyyyyyyyyyyyyytyyyyyyuty", -"XXXX XXXXXXXo@@@===@@===11s>87888:8:828828@-p4;::<:<44<4446455656548>#---33Oirryyuyyyyyyuyyuyyyyyyyyhyyyyhyyyuyyyyuyyytuyyyy", -"XXXXX XXXXXX .@@==========;1s187882;8:28888=+sp1,-=;-:<<444<<><141<<:;=@@=>p,XOrryuuuyyyyyyyyyhyyyyyyyyyyyyyyyyyyyyyyyyyytyyyy", -"XXXX X XXXX .@@======@===@=1118788:;8:8288;.+d151-@@@@;:>2:---;,,-;--#@@@:333 ++qyyyyuyyyyyyyyyyuyyyuyyyyyyyyyuyyyyyyyyyyyutuy", -"XXXXX XXXXX..@@=======@==@==>5<8788:;8:8:8:..&d,35>-@@@@-@@@@@@@@@@@@==@@,55p, Xqyyyyyyyyyhyyyyyyyyyyyyyyyyyyyyyuyyyyyyuttyyy", -"$XXX o...@@=========@====262888:;-82::-.. ip#513:=@@@@@..@......@@---;1>53, Xqhyyhyyyyuyyyyyhyuyhhuuyhyyyyyyyyyyyyyyyyuyy", -"XXXXX oXoO@@@=======@=@=====s;878:=-:-;@.. hp@74>:---##@@......@@#-,--,<51p3 +Oryyuhuyyyyyyyyyyyyyyuyyyyyyyyyyuyyyyuyyyyy", -"XXXX X .o**==========@@@=@===1;878:;-:#.. gd@54>3:-:--@@@.....@@-;--;<>13d& + +O&ryyyyhyuyuyyyuyyyyyyyyyyyuyyyyyuyyyyyyyu", -"XXXXX .&h1@=========@@@=====2;778:@@. hd@343<;;;,--@@@@O@@@#;;-->1>5pg&X X%qyyyyyyyyyuyyyuyyyyyyyyyuyuyyyyyyyyyyy", -"XXXXX Xisi1;===@======@@=====>>88#+ gg#,5>:>:-;---@@@@@@=-;;-;<>53df& X X&ryyyyyyyyuhyuyhyhyyyyhyyyuyyyyyyyyy", -"XXXX &idi31=========@@@=@==@=. + dg+-51<:;;;-#==#@#---->;-:>>5dff& XXXX*rhyyhuyyyyyyuyyyuyyyyyuyyhyyyyyyy", -"X XX +iipi3s2==@======@@@=@@.. df@#55<>:>,---#=#-;=-;;;:>>35gggO X XXX$XOqrhyyyyhuyuyyyyyyyuuyuyyyyyyyuy", -"X X X+pssiiss2======@....... igs@>1<>::>;-----;;;;;>;>>3>dffg . XX XO %qyyyyyyyhhyyuuyyyuyyuyyyyhyyy", -"XXX .iissshss===@@... .. egf,-1<:<:>:----;>;;;>:;;>>5ggfs X XO+OX O*qiyyyyyyyyyyuyuyyuyyyyyyyy", -"XXXX .ppssssdg,...... &gfd@>4::>>>----;;;;;>>;>>,gfggq X+ X XXXOOO9rryyyyhyyyyyyyuyyyyyyy", -"XXX X .iphhshhsO X&gff,#><>:>;;---;>;2;>::--pgfgf* o ++ + X XXX++O++%&ryyyhyyuyyyyyyyyyy", -"XXXX +3pdphi3 *ggfg#-><;:>:-;=;-;;;>;-#3gfgfg X+XX +X XXXXXXXXX%qyhyyyyyyuyyyyyy", -"XXX iphi++ %ggffd-><><3---@----;>:@,gfffgi. +XX X+X X+ XXXoXXXXXX%qyyrhryyuyyyyy", -"XXXX. pdi+ . Ogffffg;>1<>:-;@---=2,-,gffffgO XXXX X +X++X+X+ X+X XXX%%riryyyuyyyyy", -" X X .ih& hgffffd-56>::-=--#->,-dfffffg X XXX X + X+o X+XOXXXX%ryyyyyyyyy", -" o X ii + *ggffffp:553>;-,--,>#gffffffr XX X XX+X+ Xo o+ OXo XXX%yyyyyyyyy", -"O o i& +ggfffff3,555>,3>,1,gfffffffO o+XXXX XXX X o XoXX +XXX$ryuyyuyyy", -",. 3+. qgffffff3>531>1>13gfffffffh . X X++XX +X+XX X X X + + X$*yyyuyyyy", -"iO & o&gfffffff35pddddsiffffffff& X X XXXX X X o X X X X XXrhyyytyy", -"si . O ogfffffffffgfdhpO Ogfffffg ++ + X X+XX+ +X X+ X X XX X Xrryuyyry", -"si# ... Xifffffffe**O+o ++ OgffffqX +X OX +X X X+X XX X+ X X X&yyyyyyy", -"ssi@ &ffffffq+ + + o OgffgOX + X++ X++ OX+ XX+XXXXXXXX XX X X X$yyyyyyy", -"sip&.... &gffff& +o + %gfr + + X X X X+XX+X X +X+ +X X X X$yyyyyyy", -"pisi+. . +hfff&X + + &gO X X+ X+XO+X + X X X XX o XX X X Xqryryuy", -"ppp3#. +eff& o + *+ + + + + XX X OXXX X +X X+ X XX X Xhyyyyu", -"ppi3,. X*fq X + + O + ++ X++XX+X X X+XXXX+ Xo X X X XXrrhyyy", -"p5pi,.. O& + +XX X + X X X O+X X X X X X X XX X*yryyu", -"p5pi3o o + X o+OO X X +X+ X+ + +X X o o X X+ XX X X XXryyuy", -"spp3,. X riggggghy+ + + + X+ X Xo XXo+X X X X+ X X X X Xqyyyu", -"ppi3, . X O+ &gfffffffg&+ + +X X X +X o +X X+ X qiyyu", -"6pp3,. X +hgfffffffgqX o +XX + X X ooX X X X X X X X %yyyu", -"pp5i, X+ +X &gffffffffgO X X + ++ + X oX X X XX Xyhyy", -"ppi3# . +X Ohgfffffffgi+ + ++ X X X +X +X o X XX XX X Xryyu", -"p5ii# Oigffffffffg* X X+X ++XX+ + +XXXoX XX X X X Xryyu", -"5pp1*. X +XX Oiggfffffffhh OO X+ + + X+XX +.X X o+ X XX X X&uyy", -"5pp3@ X +XX ihffffffffgh#oX X+O + X X o o X X XX X X*uuu", -"p3pi. + X+eggffffffgiqO +X++X oo o + . ooo + X X X%uuu", -"p53p. &ii&&&&#O+ + + O*hgfffffgi&O X o O ++ oo .o.o$OO% . X X X $hyu", -"p5i3. 3hfffi3rihhhii3r&& X oihffffgi&&XXoX+X X O+ X ooooo+*@*=#=*=*&, X . XX X$hyu", -"p5i3 .idffg&iiiiphhddhip& Xo &hgffgi&&* o X oX X X X oXX o.Oe,22,,2112s%oXX . X Xryy", -"5ii,. Oihgggiiiihhphhhiihi* X X XXOhdggh&ii XX O XXX+X+X+X+ oo .ooe,=222e22eso.o$... . XXryy", -"p3p# Opgghfiiihgggdgghhhhi* oX ehgheid*XXoXXX X+ XX+X+ X ..&ieeq*1ee,eeee&,e=2=#--&* X Xquh", -"pppO *rpggiqqiiiiihiphpphiio OX o.&gsieii. X+ XX+X + .@&ifseeeheeeeehq,de122=17i*. . X*yu", -"p3p. . O&&q&&iiiiiihihihiiiii X X + hp&&3O X + X+ + + +.@=,sgh1e,heeesqhe,s11222221&% + X+ X%uu", -"ppi. . +oO*&&e&i,iiiiiX XX X &i**& + X + XX X +@-,2212eeieqee,,ee*,22222222@o X X X%hu", -"ii,. .XoO +&OO+ XX ++ +..+@>6a5=2,eih*ihf*&hie=22222==1o + X XXOyy", -"5p,. X +** X +X ..*-375522=eigqeig&,ie,222222=2 c #B26F4E", +", c #9B6657", +"< c #CA7652", +"1 c #CC7866", +"2 c #C25135", +"3 c #B0896D", +"4 c #D28859", +"5 c #D28E6D", +"6 c #E79371", +"7 c #E5A36E", +"8 c #D58932", +"9 c #3E65AE", +"0 c #4970B5", +"q c #606D95", +"w c #557CC1", +"e c #91778A", +"r c #6A86B9", +"t c #5C83C3", +"y c #6D8FCA", +"u c #7799E3", +"i c #A89A9D", +"p c #CFAA90", +"a c #F4B08F", +"s c #D6A799", +"d c #ECCCAB", +"f c #F9F4F4", +"g c #E3D8D5", +"h c #AAAECA", +/* pixels */ +"$XX$$XXX$%$$$$$$$$$$$$$$$$$$$egip3dr %% @p# Oqgffgfgfgo%q999q9999999999999999999099999099099990999999909909990w999000wwww00w00w0", +"$$X$X$XX$$$$$$$$9$$$$XX$$$$$$ws&7+O&%$Xo@3#XXX&ihffggg%%9q*9*%q99qq9q99999999999999090900909909909099999999999999999000wwwwww0ww", +"X$X$$XXX$$$$$$$$$$$$$$$$$$$$$eq@i#+$$$%X#pOX$%$%*dfffgiX%%OX%XO*%**990909999999009090990900999999099990999w999990990000wwwwwwwww", +"$XX$$$XX$$$$$$$$$$$$XX$$$$$$$*O*7#XX%$%X@p*XX$%Xohfi3hOXOoX*r3*OOO%O99099999q0q990990009090909000999090999999099909999ww0w0000w0", +"$XX$$XXX$$$$$$$$$9$$$$$$$$$$o$.&3++X$$$o#p+X$$%$OhgOX oo+ii*if&+ri&OO%q0990099009090090990909009990999099w999990999090w0000w0ww0", +"X$X$$XXX$$$$$$$$$$$$$$$$$$$$$oX#p#OO%$$o#7O%X$$XOg&O o.&fffgifirfffiOO*&qq990990000000009909099000990990999990999w90099000000000", +"XX$$$XXX$$$$$$$$$$$$$$$$$$$$$oo*3+o$$%$.#7+X$%XOOhii+X,fgifffffffddfgOOrhi0qq000090000090099990909909009w9w9909990999099900000w0", +"X$X$$$XX$$$$$%$$$$$$X$X$$$$$X$X#7@X$$$$o:3+XX$OqgffrXOe&&#*&iggi&&3&*&OOfffgrq900009000099099090000909009999990999900900000000w0", +"X$q$XXXX$$$X%$$$$$$$X$$$$$$$$$X*p+O$%$$o-3oOX%gffff&o++3pgg3fddd3fdg&+OOhffffgt0099090090009090990909990999990999000099090090000", +"X$g&$XXX%$X$%$$$$$$$$$X$$$$$$$o*p+X%$%$X,5+ *gfffff& ++&pddffffffgdd#OOOiffffffhq9qt009w990090900999w9w9900999w90999909009900000", +"X$ggO %$$$X$%$$$$$$$$$$$$$$$Xo*3#$$%%$X,3+&ffffffg#+.+Odddgggggggpd*+OOifffffffgtqt09t909090990090909099990099w0990090909090000", +"X%hfr&h&$$$$%q$X$$$$XXX$$$$$Xoo*p@o$$$$X:3OffffgppdO. ++,ddfffffffd3+ ++&ffffffffgq900909090900999099090w9w990990009009090009900", +"Xohfffh&X$o$$h&$$$$$$$X$$$$$Xo$*7# %%$$ 35dffg73&#++++ +pd3gfffffg3d,++++3ddffffffhq90009099990999909090990909w99990099900900000", +"oegffgiO$$$$$gh$$%&qXXXX$%XX$X.*3# X%X$+#pgfd3&+. + ++3iddgfffddpd&+O++++#pgfffffrq9900900q99090909990099009990009990999090000", +"hffffg&X$%$X$gg*&hg*XXXX%i%$$Xo*5#XX$$$o,dfg&+.. .#&++ +&ddd3gfgdddd#++++&+++#3dfgghq009099090000909090990999090909000099000000w", +"hgffffp*X$X%Ogggfgh$XXX$%g*XXooO5#o$%$$+&df3...&#++3 O#+#pd7di&,ppdd&+++#3+&&..#iffgqq009009009909900990090009990009999009990000", +"o%qffgh&o%o*iffgfg%%X$XX*gi OqO*p#+$%$XO&h3 . +pdpp3pdgdd3#+++++++#ppggpp3ddp+##+ifghqrq09090090090090900909999009009w900009w0w9", +"Xo*fh**%$%igfgfgg&%$$X$X&ggih3o+p# X%X$XX .+33+pdppdddfp,#+O&dfggdddd3pdddd+++dh&%%&q0090909009909090090000900w000w99w0900ww", +"X%&fq%oX$%&hgfffgi%oXXX &fgshOoO3#+X%%XX+ . +#&++7dp3&#O&O&#&33dd3#3&.+#ppiiO#+ X%9909000909099090090900009w9w009ww9ww9w00", +"X$%g*o$XX%Xo*gffffq$X%&egfhp&oXO7# XXX + #++.+++ +..O+ ++O++#ppp+++&3++ o XXq9000900009000090009000009w909w09w00w00w", +"$$*i$$oX$XoX%hfeegg%$%&idggh+X$O4#+XOX + . + ..o + + ++ +&3gg#. X999909000000000000009000000000w09w0www0w", +"$$%%$$$X$$o$$hh*$$q&$o o3ggd,ooo3#+ ++ +++ ++*++ X%0q9q900000ww000000000000ww99w00w9w99ww0", +"$$$$%%X$$$X$%g0$$%%%oX .&fshg*Xo3: X + + X*0000000000w000000000w00000ww0www90wwww0", +"$%$$%$$XX$$X%h$%$%$$XXX qg*XiqXo3:+ %0q0000000w00000w0ww000w0ww990009w0w99ww", +"$$$%%%$$$XX$$%%$%$$$XXXXihXXX%X.,: %t000t90w0w0000090w00w00009w000wwww9www9", +"$$$$$%X$X$$$$%$$$$$$XXXXq*XXXXXo3: . %0q0000t0w0ww00ww00ww00ww0w9w0w0w99ww9ww", +"$$$$$$$XX$X$$$$$$$$$$XXX*$$XXXX :> %q0t0t00w0w0w0w00ww0w0w0w00w0w0www90w09w", +"$$$$$$XX$$XX$$$$%$$$$XXX$$$$XXX.:3. . . .... .. . ....... X%qw000t00www0wwwwwww0ww0www0w00w00w09w9w", +"$$$$%%$$$XX$$$$$$$$$XXXX$$$$XXX -3 . ....+@@@@@@.@@....@@@@#####*@.. %ttt9ttt0t0tw0www0ww0ww00w9ww0w09w9www0w", +"$$$%$%$$X$$$$$$$%%9$XXXX$$$$XXX :3 ...#->1>>1,>>,,--;,--,,,1531>13>#@. X09q9r00t0t0wwwww0w0wwwwwwww0wwwwww0w00w", +"$$$$$$XXX$$$$$$$$$$$XX X$$$$$XX.:: .#11ss65assa56a511155557a775555p5,,. Xqttr0trt0wtwwwwww0ww0wwwwww0wwwww0ww0ww", +"$$$$$$$X$$$$9$9$$$$$XXXXX$$$XX$ >;. ...#,pps55paaaaasadasaaaaaaaaa777a77p53,*... Xt0tq9t0ttwww0wwwwwwww0wwww0wwww0wwwwwww", +"*o$%$%$XX$$$$$$$%$$$$XXXX$$$$XXX,> +#*-,5pp7apasaadddaaaaddadaaadddaaa6pp733,##. Xqrtttttt0t0rt0tt000t0t0t0t00t0t0ttt0ttt", +"go%$$*%$XXX$$$$$$$$$$X XX$$$$XX :> .&,#,5p77p7aaaaadaaadaddaaadaaaasaapp7775534#@ Xr90tq0ttt0tt0ttt0ttttt00tttt0tt0t0tttt0", +"fio%whXXX%$$$$$$$$$$$XXX$$$$X$X.,8. o###-155pppadaasasaaadaaaaaaaaa51>3355455535>>#+ %t0r0r0rttt00tt0ttttttt0tttttt0ttttttttt", +"gg&gf&X *qo$$$$$$$$$$X X$$$$$XX 3: OO##,135565431>113sadaaaddsss611>1>>55s5355333,*+ %rr0r0ttt0tttttrtt0ttqttt0ttttttttt0tttt", +"gffghOXX*g*$$$%$$$$$XXXX$$$$XXX -> +###,>3575p5<><355saaaaaaaap615>;;-;>5a7p335333#+ %rrtttt0t0trtttttttttttttttttttttttttttt", +"fgfg&oo &giO$qi%X$$$XX XX$$$XX .::+ +#,,,3355p55>>;;;,>16s777aaas61>;=--@#@@@@##&##&&+X*rrtttttttt0tttttttttttttttttttttttttttt", +"gggg&o &gg,sfe$X$$$$X XX$$$XXX :: +&#-,,35333>>,-,-,>>15apaadap1>>;@@@#;-##@+,&@##&O+&rttttt0ttttttt0tttttttttttttttttttttttt", +"gffgg*oX&gfggh%X$X$$XX X%%XXX 3:+ ++3:-####@@.@..@++@@->6aaaaaa74>=@,3,11335d&+@+O*&##qrrttttytttttttrtttttttttttttttttttttttt", +"sggggho*dgggg&O$$$$$ *+Oii$X X .::+++&,-##+@+@@:ss1,1i1--=:4a77764::a1,@@@@=,,4d#+##*&,-eqyrwyrwttttttttttttttytytttytytttyttttt", +"id**qi&Ogggdd+X$$$$$o*&eh*X XXX.::.O#,>-#@...+,,,-@@@@-5><<<76748:2<>-@#.@@@@=-@##@@+#,isiryywyyrttrttttyttytytttttytttttttttytu", +",hO%X$Xo&gggg&O$$$$XoO,e&OXXXXX.3:.@33;#+@@@.-,>-@.@..@=::;<4<4<:28<2@#@.@@@@==-,-##-,1,eheywyywyttttyytytyttttttyttttttttytyttt", +"i&o$$X Ogggfg%X$$$$oO,&+ XX X :3+@3,,;##@#@##,@@@..@-;==;426a76<<<;>;--#==;;::4>4>:;:==1ryewywttyrtytttytyytyyttyttuytyttyttyt", +"**%$$$XX+hiO&h&%$$$XX*,,& XXX X ::+#1-#-;,,>,#-==#=#;-;<4444;6ad7<661<<11>2;2<<<445<>:<<@,ryytytyytyyttyyttytyttytyttyttyttyuytt", +"%X$$$$X Og*o%$*X$$$$X*&&i* XXXX :3.#,=;>,-35:-#===:4s51446a6;6aaa6661<66aaa6<664445>:15<-,ewyytyytytyyytyyytytytytyyytyyttuttttu", +"X$$$$XXXOi$$$$$X$$$XXX.o&qXo X .>>.#-@>;-:<6:<;==2<66a6<<6a<-:44:<82<<11<<=54;;<<<<1<;2<<<1aada6:7adadd74aaadaa6446764444;=>s,eitryyyyyyyyyyyyttyyyyyyyytyyttyttutytu", +"XX$$$XXXX$$$$XX$$$$$XXXXXX$X$X .::.@,;6<-;<<1<4::46aadddaa46aadddad66aaaaaa7a76564>4<-;51eryyttyyyttryytyyyyyryrtuyytyyyyyyttytt", +"X$$$$XX XX$$$$X$$$XXXXX XXXXXXX :8+ia>44;;<<<44<;87dadddda>5a221eiiwtyyyyyyyyyyyyyyyyyyytyytytyytttytuyy", +"$X$X$X XX$X$$X$$$$$XX XX$XX$XX #73pda6<>56>2<44<26addaa6>:6adaaadda2;16666a7a6<4>47a1<1ieyyyyytyyytyyyyyyryyyyyyyyytutyyuyyytyy", +"X$$$XX XX$$$XX$$$$XXXX XXXXXX :75ps565577>:<:46<<1aa11;;6aaadddaada<2>2;<6676<<435a1>5eiyytyyyyyyyyyyyyytyyyyyyyyyyyyyyytyyuty", +"XX$$$XX XX$$$$$$$$X$XXXXXXXXX X3773sdp565p742<:<61<2;2;2=<66aaaaadaaa1>6a66a665<5>456>15ewyyyyyyyytyyyyyyyyyyyyytyytyyyyytuyyttu", +"$$$XXX X$$$$$X$$$XXXXXXXXXX &pp73dd357577<<:>481611612;661a1saas6111;21aa664444>56<163iyyytyyyyyyyyyyyyyyyyryyutyyyyyyyyyyyyyy", +"$X$$XX XX$$X$$$$XXXXXXXXXX *ip773ss345775<<<848111aa1;=;-=><1511;;====116166165>4>6615rtyyyyyyyyyyyyyyyyyyyyyyyyyyyyytyyyyytuy", +"X$$X$X XX$$$XX$X$XXXXX X XX*ipp773dd33783::<<<4<<6664;=6<@===-==#;=;1;@21<<646655>>>>6iryyyryyyyyyyyyyyyyyyyyywyryyyyyyyyyyyutu", +"XXX$XX X$$$$$$X$XXXX XXXX&iipp783pp33735771<<46<644<=<66641,-@@:41112==2<4<44545a775piiyyyyyryyyyyyyyyyyyywyyyyyywyyyytyyyutyy", +"XX$*XX XX$$$$$$$$XXXXXXX oriipsp784dp333a7p761446<442=2661666611<6161<1<@=;2<46555aa5ppqryyyyrryryyyyyyyyyyyyyryywyywyywuytyyyyy", +"*oe*XX XXXXX$XXX$XXX Xoiihhisp774ppp837a7dp>4464<<2=<16666666666116661===;<1<445a77p3iryyyyyyyyyyyyyyyyyryyyyyyyyyyyyytyyyyyyy", +"iiiXXXX XXXXX$XX$X X o@iihishpp788dpp#:7p7a,3447842=;166aaa6aaaaaa16666<;=;<<4643ap33iirryyyryyryyyyyyyyyyryyyyyyyyyyyytyyttyyy", +"di&o o %*%XXXX$XXXX Oeiishippp773dd3:#:57>.-47664;@8<61a6a1aaaaaa676a11<=<<6548->53iirrryyyyyyyyyyyyyyyyyyyryyyrryyywyyyyyutuy", +"ppO.oX *q&XXXXXX$XXX..@,eshsipdp783pp3:##:,@.#:4474<:<<11<>1<1661<21<<<6<6;166643##,7irrryyyyyyyyyyyyyryyyyyywyyyryrywtyyyyyyyty", +"di*+X. *&OoXXXXXXX$ .o@&,sshsiip377pd3,##:3,@@4466718<<<2=====22=2===-=;2<6676158##333iryyyyytyyyyyyyyyyyyyyyyyyyyyyyyyyytytyyyy", +"gge. *OoXXXXXXXXXX..@@,3shsipp778dp3,-@@@@+@-46667<22==;22,22=2111<6<<<<8677453##33iriyyyyyyyyyyyyyyyyyyyyyyyryyyyytyyyyyyyyyy", +"esi* &*XXXXXXXXXX .@@=;ppssppp77377:-:@+.+++-446774<<<1116666aa16a61<66<6766558#,3ihrryyyyyyyyyyyyyyyyyyyrryyyyyyyyyytytyyyyyy", +"&.&eo oi& XXX$XXXXXX.@@@=ishhpsp873p3,:28@@.@+=4<4774868<1116aa6a611<88668664453##&3iryyyyyyyyhyyyyyyyyyyyyyyyyyyyryyyyyytyyyyyy", +"oXXo XX&eXXXXX XXXXX..=@@1ssipsp875p3::888:@.@@;44464<68<<2112;;;22<<6664<16443>##3iiryhuyyyyyyyyyyyyryyryyyyrryyyyyyryyyyyyyyyy", +"XXXXX X&OXXXXXXXX .@@===6ssis577377>::88882@@-:<<68<66611<2;;;2=;:<<<<644<<<5-#3&3iyhyyyyyyyyyyyyyyyyyyyyyyyyyryyyyyryyyyyyyyy", +"XXXX X %OoXXXXXXXX..@=@=@1ssis578377:::82822;@;:4<8244<8<1<46a56144<<<656<6<<1#&eerryryyyyyyuyyyyyyyyyyyyyyyyyyyyyyyyyytytytyty", +"XXXXX XXXXXXXXX..@=@====sspp578877:8228282;=;<<><46666666aaaaaa66777a5<4<1,i-&3riryyyyyyyyyuyyyyyyyyyyyyyyyyyyyyyyyryyyyyyyyy", +"X$XXX XXXXXX ..@@==@===1ppp5878p8222828282==<:<<6<<<84667aaaa6aa777744<;113,eiryyyyyyyyyyuyuyhyyyiyyyyyyyyyyyyyyyyyyyytyyyyy", +"XXXXX XXXXXX X .@===@=====1ap387838:8:282822=@>4:>44>44644666a66aaa744>>:::>3,erryyuuyyyyyyyyhyyyyyyuyyyyyyyyyyyyyyytyyyyyyuty", +"XXXX XXXXXXXo@@@===@@===11s>87888:8:828828@-p4;::<:<44<4446455656548>#---33Oirryyuyyyyyyuyyuyyyyyyyyhyyyyhyyyuyyyyuyyytuyyyy", +"XXXXX XXXXXX .@@==========;1s187882;8:28888=+sp1,-=;-:<<444<<><141<<:;=@@=>p,XOrryuuuyyyyyyyyyhyyyyyyyyyyyyyyyyyyyyyyyyyytyyyy", +"XXXX X XXXX .@@======@===@=1118788:;8:8288;.+d151-@@@@;:>2:---;,,-;--#@@@:333 ++qyyyyuyyyyyyyyyyuyyyuyyyyyyyyyuyyyyyyyyyyyutuy", +"XXXXX XXXXX..@@=======@==@==>5<8788:;8:8:8:..&d,35>-@@@@-@@@@@@@@@@@@==@@,55p, Xqyyyyyyyyyhyyyyyyyyyyyyyyyyyyyyyuyyyyyyuttyyy", +"$XXX o...@@=========@====262888:;-82::-.. ip#513:=@@@@@..@......@@---;1>53, Xqhyyhyyyyuyyyyyhyuyhhuuyhyyyyyyyyyyyyyyyyuyy", +"XXXXX oXoO@@@=======@=@=====s;878:=-:-;@.. hp@74>:---##@@......@@#-,--,<51p3 +Oryyuhuyyyyyyyyyyyyyyuyyyyyyyyyyuyyyyuyyyyy", +"XXXX X .o**==========@@@=@===1;878:;-:#.. gd@54>3:-:--@@@.....@@-;--;<>13d& + +O&ryyyyhyuyuyyyuyyyyyyyyyyyuyyyyyuyyyyyyyu", +"XXXXX .&h1@=========@@@=====2;778:@@. hd@343<;;;,--@@@@O@@@#;;-->1>5pg&X X%qyyyyyyyyyuyyyuyyyyyyyyyuyuyyyyyyyyyyy", +"XXXXX Xisi1;===@======@@=====>>88#+ gg#,5>:>:-;---@@@@@@=-;;-;<>53df& X X&ryyyyyyyyuhyuyhyhyyyyhyyyuyyyyyyyyy", +"XXXX &idi31=========@@@=@==@=. + dg+-51<:;;;-#==#@#---->;-:>>5dff& XXXX*rhyyhuyyyyyyuyyyuyyyyyuyyhyyyyyyy", +"X XX +iipi3s2==@======@@@=@@.. df@#55<>:>,---#=#-;=-;;;:>>35gggO X XXX$XOqrhyyyyhuyuyyyyyyyuuyuyyyyyyyuy", +"X X X+pssiiss2======@....... igs@>1<>::>;-----;;;;;>;>>3>dffg . XX XO %qyyyyyyyhhyyuuyyyuyyuyyyyhyyy", +"XXX .iissshss===@@... .. egf,-1<:<:>:----;>;;;>:;;>>5ggfs X XO+OX O*qiyyyyyyyyyyuyuyyuyyyyyyyy", +"XXXX .ppssssdg,...... &gfd@>4::>>>----;;;;;>>;>>,gfggq X+ X XXXOOO9rryyyyhyyyyyyyuyyyyyyy", +"XXX X .iphhshhsO X&gff,#><>:>;;---;>;2;>::--pgfgf* o ++ + X XXX++O++%&ryyyhyyuyyyyyyyyyy", +"XXXX +3pdphi3 *ggfg#-><;:>:-;=;-;;;>;-#3gfgfg X+XX +X XXXXXXXXX%qyhyyyyyyuyyyyyy", +"XXX iphi++ %ggffd-><><3---@----;>:@,gfffgi. +XX X+X X+ XXXoXXXXXX%qyyrhryyuyyyyy", +"XXXX. pdi+ . Ogffffg;>1<>:-;@---=2,-,gffffgO XXXX X +X++X+X+ X+X XXX%%riryyyuyyyyy", +" X X .ih& hgffffd-56>::-=--#->,-dfffffg X XXX X + X+o X+XOXXXX%ryyyyyyyyy", +" o X ii + *ggffffp:553>;-,--,>#gffffffr XX X XX+X+ Xo o+ OXo XXX%yyyyyyyyy", +"O o i& +ggfffff3,555>,3>,1,gfffffffO o+XXXX XXX X o XoXX +XXX$ryuyyuyyy", +",. 3+. qgffffff3>531>1>13gfffffffh . X X++XX +X+XX X X X + + X$*yyyuyyyy", +"iO & o&gfffffff35pddddsiffffffff& X X XXXX X X o X X X X XXrhyyytyy", +"si . O ogfffffffffgfdhpO Ogfffffg ++ + X X+XX+ +X X+ X X XX X Xrryuyyry", +"si# ... Xifffffffe**O+o ++ OgffffqX +X OX +X X X+X XX X+ X X X&yyyyyyy", +"ssi@ &ffffffq+ + + o OgffgOX + X++ X++ OX+ XX+XXXXXXXX XX X X X$yyyyyyy", +"sip&.... &gffff& +o + %gfr + + X X X X+XX+X X +X+ +X X X X$yyyyyyy", +"pisi+. . +hfff&X + + &gO X X+ X+XO+X + X X X XX o XX X X Xqryryuy", +"ppp3#. +eff& o + *+ + + + + XX X OXXX X +X X+ X XX X Xhyyyyu", +"ppi3,. X*fq X + + O + ++ X++XX+X X X+XXXX+ Xo X X X XXrrhyyy", +"p5pi,.. O& + +XX X + X X X O+X X X X X X X XX X*yryyu", +"p5pi3o o + X o+OO X X +X+ X+ + +X X o o X X+ XX X X XXryyuy", +"spp3,. X riggggghy+ + + + X+ X Xo XXo+X X X X+ X X X X Xqyyyu", +"ppi3, . X O+ &gfffffffg&+ + +X X X +X o +X X+ X qiyyu", +"6pp3,. X +hgfffffffgqX o +XX + X X ooX X X X X X X X %yyyu", +"pp5i, X+ +X &gffffffffgO X X + ++ + X oX X X XX Xyhyy", +"ppi3# . +X Ohgfffffffgi+ + ++ X X X +X +X o X XX XX X Xryyu", +"p5ii# Oigffffffffg* X X+X ++XX+ + +XXXoX XX X X X Xryyu", +"5pp1*. X +XX Oiggfffffffhh OO X+ + + X+XX +.X X o+ X XX X X&uyy", +"5pp3@ X +XX ihffffffffgh#oX X+O + X X o o X X XX X X*uuu", +"p3pi. + X+eggffffffgiqO +X++X oo o + . ooo + X X X%uuu", +"p53p. &ii&&&&#O+ + + O*hgfffffgi&O X o O ++ oo .o.o$OO% . X X X $hyu", +"p5i3. 3hfffi3rihhhii3r&& X oihffffgi&&XXoX+X X O+ X ooooo+*@*=#=*=*&, X . XX X$hyu", +"p5i3 .idffg&iiiiphhddhip& Xo &hgffgi&&* o X oX X X X oXX o.Oe,22,,2112s%oXX . X Xryy", +"5ii,. Oihgggiiiihhphhhiihi* X X XXOhdggh&ii XX O XXX+X+X+X+ oo .ooe,=222e22eso.o$... . XXryy", +"p3p# Opgghfiiihgggdgghhhhi* oX ehgheid*XXoXXX X+ XX+X+ X ..&ieeq*1ee,eeee&,e=2=#--&* X Xquh", +"pppO *rpggiqqiiiiihiphpphiio OX o.&gsieii. X+ XX+X + .@&ifseeeheeeeehq,de122=17i*. . X*yu", +"p3p. . O&&q&&iiiiiihihihiiiii X X + hp&&3O X + X+ + + +.@=,sgh1e,heeesqhe,s11222221&% + X+ X%uu", +"ppi. . +oO*&&e&i,iiiiiX XX X &i**& + X + XX X +@-,2212eeieqee,,ee*,22222222@o X X X%hu", +"ii,. .XoO +&OO+ XX ++ +..+@>6a5=2,eih*ihf*&hie=22222==1o + X XXOyy", +"5p,. X +** X +X ..*-375522=eigqeig&,ie,222222=2 $OUT/fuzz_pillow.dict +#!/bin/bash -eu +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +################################################################################ + +# Generate image dictionaries here for each of the fuzzers and put them in the +# $OUT directory, named for the fuzzer + +git clone --depth 1 https://github.com/google/fuzzing +cat fuzzing/dictionaries/bmp.dict \ + fuzzing/dictionaries/dds.dict \ + fuzzing/dictionaries/gif.dict \ + fuzzing/dictionaries/icns.dict \ + fuzzing/dictionaries/jpeg.dict \ + fuzzing/dictionaries/jpeg2000.dict \ + fuzzing/dictionaries/pbm.dict \ + fuzzing/dictionaries/png.dict \ + fuzzing/dictionaries/psd.dict \ + fuzzing/dictionaries/tiff.dict \ + fuzzing/dictionaries/webp.dict \ + > $OUT/fuzz_pillow.dict diff --git a/Tests/oss-fuzz/fuzz_font.py b/Tests/oss-fuzz/fuzz_font.py index 8788d7021..abd9d71dc 100755 --- a/Tests/oss-fuzz/fuzz_font.py +++ b/Tests/oss-fuzz/fuzz_font.py @@ -1,43 +1,43 @@ -#!/usr/bin/python3 - -# Copyright 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -import atheris - -with atheris.instrument_imports(): - import sys - - import fuzzers - - -def TestOneInput(data: bytes) -> None: - try: - fuzzers.fuzz_font(data) - except Exception: - # We're catching all exceptions because Pillow's exceptions are - # directly inheriting from Exception. - pass - - -def main() -> None: - fuzzers.enable_decompressionbomb_error() - atheris.Setup(sys.argv, TestOneInput) - atheris.Fuzz() - fuzzers.disable_decompressionbomb_error() - - -if __name__ == "__main__": - main() +#!/usr/bin/python3 + +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import atheris + +with atheris.instrument_imports(): + import sys + + import fuzzers + + +def TestOneInput(data: bytes) -> None: + try: + fuzzers.fuzz_font(data) + except Exception: + # We're catching all exceptions because Pillow's exceptions are + # directly inheriting from Exception. + pass + + +def main() -> None: + fuzzers.enable_decompressionbomb_error() + atheris.Setup(sys.argv, TestOneInput) + atheris.Fuzz() + fuzzers.disable_decompressionbomb_error() + + +if __name__ == "__main__": + main() diff --git a/Tests/oss-fuzz/fuzz_pillow.py b/Tests/oss-fuzz/fuzz_pillow.py index 9137391b6..0fdafff6f 100644 --- a/Tests/oss-fuzz/fuzz_pillow.py +++ b/Tests/oss-fuzz/fuzz_pillow.py @@ -1,41 +1,41 @@ -# Copyright 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -import atheris - -with atheris.instrument_imports(): - import sys - - import fuzzers - - -def TestOneInput(data: bytes) -> None: - try: - fuzzers.fuzz_image(data) - except Exception: - # We're catching all exceptions because Pillow's exceptions are - # directly inheriting from Exception. - pass - - -def main() -> None: - fuzzers.enable_decompressionbomb_error() - atheris.Setup(sys.argv, TestOneInput) - atheris.Fuzz() - fuzzers.disable_decompressionbomb_error() - - -if __name__ == "__main__": - main() +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import atheris + +with atheris.instrument_imports(): + import sys + + import fuzzers + + +def TestOneInput(data: bytes) -> None: + try: + fuzzers.fuzz_image(data) + except Exception: + # We're catching all exceptions because Pillow's exceptions are + # directly inheriting from Exception. + pass + + +def main() -> None: + fuzzers.enable_decompressionbomb_error() + atheris.Setup(sys.argv, TestOneInput) + atheris.Fuzz() + fuzzers.disable_decompressionbomb_error() + + +if __name__ == "__main__": + main() diff --git a/Tests/oss-fuzz/fuzzers.py b/Tests/oss-fuzz/fuzzers.py index d6c1fab71..a0ef0cf98 100644 --- a/Tests/oss-fuzz/fuzzers.py +++ b/Tests/oss-fuzz/fuzzers.py @@ -1,43 +1,43 @@ -from __future__ import annotations - -import io -import warnings - -from PIL import Image, ImageDraw, ImageFile, ImageFilter, ImageFont - - -def enable_decompressionbomb_error() -> None: - ImageFile.LOAD_TRUNCATED_IMAGES = True - warnings.filterwarnings("ignore") - warnings.simplefilter("error", Image.DecompressionBombWarning) - - -def disable_decompressionbomb_error() -> None: - ImageFile.LOAD_TRUNCATED_IMAGES = False - warnings.resetwarnings() - - -def fuzz_image(data: bytes) -> None: - # This will fail on some images in the corpus, as we have many - # invalid images in the test suite. - with Image.open(io.BytesIO(data)) as im: - im.rotate(45) - im.filter(ImageFilter.DETAIL) - im.save(io.BytesIO(), "BMP") - - -def fuzz_font(data: bytes) -> None: - wrapper = io.BytesIO(data) - try: - font = ImageFont.truetype(wrapper) - except OSError: - # Catch pcf/pilfonts/random garbage here. They return - # different font objects. - return - - font.getbbox("ABC") - font.getmask("test text") - with Image.new(mode="RGBA", size=(200, 200)) as im: - draw = ImageDraw.Draw(im) - draw.multiline_textbbox((10, 10), "ABC\nAaaa", font, stroke_width=2) - draw.text((10, 10), "Test Text", font=font, fill="#000") +from __future__ import annotations + +import io +import warnings + +from PIL import Image, ImageDraw, ImageFile, ImageFilter, ImageFont + + +def enable_decompressionbomb_error() -> None: + ImageFile.LOAD_TRUNCATED_IMAGES = True + warnings.filterwarnings("ignore") + warnings.simplefilter("error", Image.DecompressionBombWarning) + + +def disable_decompressionbomb_error() -> None: + ImageFile.LOAD_TRUNCATED_IMAGES = False + warnings.resetwarnings() + + +def fuzz_image(data: bytes) -> None: + # This will fail on some images in the corpus, as we have many + # invalid images in the test suite. + with Image.open(io.BytesIO(data)) as im: + im.rotate(45) + im.filter(ImageFilter.DETAIL) + im.save(io.BytesIO(), "BMP") + + +def fuzz_font(data: bytes) -> None: + wrapper = io.BytesIO(data) + try: + font = ImageFont.truetype(wrapper) + except OSError: + # Catch pcf/pilfonts/random garbage here. They return + # different font objects. + return + + font.getbbox("ABC") + font.getmask("test text") + with Image.new(mode="RGBA", size=(200, 200)) as im: + draw = ImageDraw.Draw(im) + draw.multiline_textbbox((10, 10), "ABC\nAaaa", font, stroke_width=2) + draw.text((10, 10), "Test Text", font=font, fill="#000") diff --git a/Tests/oss-fuzz/python.supp b/Tests/oss-fuzz/python.supp index 94cc87db9..f56cf440c 100644 --- a/Tests/oss-fuzz/python.supp +++ b/Tests/oss-fuzz/python.supp @@ -1,16 +1,16 @@ -{ - - Memcheck:Cond - ... - fun:encode_current_locale -} - - -{ - - Memcheck:Cond - fun:inflate - fun:ZIPDecode - fun:_TIFFReadEncodedTileAndAllocBuffer - ... -} +{ + + Memcheck:Cond + ... + fun:encode_current_locale +} + + +{ + + Memcheck:Cond + fun:inflate + fun:ZIPDecode + fun:_TIFFReadEncodedTileAndAllocBuffer + ... +} diff --git a/Tests/oss-fuzz/test_fuzzers.py b/Tests/oss-fuzz/test_fuzzers.py index 90eb8713a..495737ce4 100644 --- a/Tests/oss-fuzz/test_fuzzers.py +++ b/Tests/oss-fuzz/test_fuzzers.py @@ -1,67 +1,67 @@ -from __future__ import annotations - -import subprocess -import sys - -import fuzzers -import packaging -import pytest - -from PIL import Image, UnidentifiedImageError, features -from Tests.helper import skip_unless_feature - -if sys.platform.startswith("win32"): - pytest.skip("Fuzzer is linux only", allow_module_level=True) -libjpeg_turbo_version = features.version("libjpeg_turbo") -if libjpeg_turbo_version is not None: - version = packaging.version.parse(libjpeg_turbo_version) - if version.major == 2 and version.minor == 0: - pytestmark = pytest.mark.valgrind_known_error( - reason="Known failing with libjpeg_turbo 2.0" - ) - - -@pytest.mark.parametrize( - "path", - subprocess.check_output("find Tests/images -type f", shell=True).split(b"\n"), -) -def test_fuzz_images(path: str) -> None: - fuzzers.enable_decompressionbomb_error() - try: - with open(path, "rb") as f: - fuzzers.fuzz_image(f.read()) - assert True - except ( - OSError, - SyntaxError, - MemoryError, - ValueError, - NotImplementedError, - OverflowError, - ): - # Known exceptions that are through from Pillow - assert True - except ( - Image.DecompressionBombError, - Image.DecompressionBombWarning, - UnidentifiedImageError, - ): - # Known Image.* exceptions - assert True - finally: - fuzzers.disable_decompressionbomb_error() - - -@skip_unless_feature("freetype2") -@pytest.mark.parametrize( - "path", subprocess.check_output("find Tests/fonts -type f", shell=True).split(b"\n") -) -def test_fuzz_fonts(path: str) -> None: - if not path: - return - with open(path, "rb") as f: - try: - fuzzers.fuzz_font(f.read()) - except (Image.DecompressionBombError, Image.DecompressionBombWarning, OSError): - pass - assert True +from __future__ import annotations + +import subprocess +import sys + +import fuzzers +import packaging +import pytest + +from PIL import Image, UnidentifiedImageError, features +from Tests.helper import skip_unless_feature + +if sys.platform.startswith("win32"): + pytest.skip("Fuzzer is linux only", allow_module_level=True) +libjpeg_turbo_version = features.version("libjpeg_turbo") +if libjpeg_turbo_version is not None: + version = packaging.version.parse(libjpeg_turbo_version) + if version.major == 2 and version.minor == 0: + pytestmark = pytest.mark.valgrind_known_error( + reason="Known failing with libjpeg_turbo 2.0" + ) + + +@pytest.mark.parametrize( + "path", + subprocess.check_output("find Tests/images -type f", shell=True).split(b"\n"), +) +def test_fuzz_images(path: str) -> None: + fuzzers.enable_decompressionbomb_error() + try: + with open(path, "rb") as f: + fuzzers.fuzz_image(f.read()) + assert True + except ( + OSError, + SyntaxError, + MemoryError, + ValueError, + NotImplementedError, + OverflowError, + ): + # Known exceptions that are through from Pillow + assert True + except ( + Image.DecompressionBombError, + Image.DecompressionBombWarning, + UnidentifiedImageError, + ): + # Known Image.* exceptions + assert True + finally: + fuzzers.disable_decompressionbomb_error() + + +@skip_unless_feature("freetype2") +@pytest.mark.parametrize( + "path", subprocess.check_output("find Tests/fonts -type f", shell=True).split(b"\n") +) +def test_fuzz_fonts(path: str) -> None: + if not path: + return + with open(path, "rb") as f: + try: + fuzzers.fuzz_font(f.read()) + except (Image.DecompressionBombError, Image.DecompressionBombWarning, OSError): + pass + assert True diff --git a/Tests/test_000_sanity.py b/Tests/test_000_sanity.py index c3926250f..603654e76 100644 --- a/Tests/test_000_sanity.py +++ b/Tests/test_000_sanity.py @@ -1,20 +1,20 @@ -from __future__ import annotations - -from PIL import Image - - -def test_sanity() -> None: - # Make sure we have the binary extension - Image.core.new("L", (100, 100)) - - # Create an image and do stuff with it. - im = Image.new("1", (100, 100)) - assert (im.mode, im.size) == ("1", (100, 100)) - assert len(im.tobytes()) == 1300 - - # Create images in all remaining major modes. - Image.new("L", (100, 100)) - Image.new("P", (100, 100)) - Image.new("RGB", (100, 100)) - Image.new("I", (100, 100)) - Image.new("F", (100, 100)) +from __future__ import annotations + +from PIL import Image + + +def test_sanity() -> None: + # Make sure we have the binary extension + Image.core.new("L", (100, 100)) + + # Create an image and do stuff with it. + im = Image.new("1", (100, 100)) + assert (im.mode, im.size) == ("1", (100, 100)) + assert len(im.tobytes()) == 1300 + + # Create images in all remaining major modes. + Image.new("L", (100, 100)) + Image.new("P", (100, 100)) + Image.new("RGB", (100, 100)) + Image.new("I", (100, 100)) + Image.new("F", (100, 100)) diff --git a/Tests/test_binary.py b/Tests/test_binary.py index d19799a09..f7a3dd274 100644 --- a/Tests/test_binary.py +++ b/Tests/test_binary.py @@ -1,24 +1,24 @@ -from __future__ import annotations - -from PIL import _binary - - -def test_standard() -> None: - assert _binary.i8(b"*") == 42 - assert _binary.o8(42) == b"*" - - -def test_little_endian() -> None: - assert _binary.i16le(b"\xff\xff\x00\x00") == 65535 - assert _binary.i32le(b"\xff\xff\x00\x00") == 65535 - - assert _binary.o16le(65535) == b"\xff\xff" - assert _binary.o32le(65535) == b"\xff\xff\x00\x00" - - -def test_big_endian() -> None: - assert _binary.i16be(b"\x00\x00\xff\xff") == 0 - assert _binary.i32be(b"\x00\x00\xff\xff") == 65535 - - assert _binary.o16be(65535) == b"\xff\xff" - assert _binary.o32be(65535) == b"\x00\x00\xff\xff" +from __future__ import annotations + +from PIL import _binary + + +def test_standard() -> None: + assert _binary.i8(b"*") == 42 + assert _binary.o8(42) == b"*" + + +def test_little_endian() -> None: + assert _binary.i16le(b"\xff\xff\x00\x00") == 65535 + assert _binary.i32le(b"\xff\xff\x00\x00") == 65535 + + assert _binary.o16le(65535) == b"\xff\xff" + assert _binary.o32le(65535) == b"\xff\xff\x00\x00" + + +def test_big_endian() -> None: + assert _binary.i16be(b"\x00\x00\xff\xff") == 0 + assert _binary.i32be(b"\x00\x00\xff\xff") == 65535 + + assert _binary.o16be(65535) == b"\xff\xff" + assert _binary.o32be(65535) == b"\x00\x00\xff\xff" diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py index 7f8487921..f45f9dd4d 100644 --- a/Tests/test_bmp_reference.py +++ b/Tests/test_bmp_reference.py @@ -1,114 +1,114 @@ -from __future__ import annotations - -import os -import warnings - -from PIL import Image - -from .helper import assert_image_similar - -base = os.path.join("Tests", "images", "bmp") - - -def get_files(d: str, ext: str = ".bmp") -> list[str]: - return [ - os.path.join(base, d, f) for f in os.listdir(os.path.join(base, d)) if ext in f - ] - - -def test_bad() -> None: - """These shouldn't crash/dos, but they shouldn't return anything - either""" - for f in get_files("b"): - # Assert that there is no unclosed file warning - with warnings.catch_warnings(): - try: - with Image.open(f) as im: - im.load() - except Exception: # as msg: - pass - - -def test_questionable() -> None: - """These shouldn't crash/dos, but it's not well defined that these - are in spec""" - supported = [ - "pal8os2v2.bmp", - "rgb24prof.bmp", - "pal1p1.bmp", - "pal4rletrns.bmp", - "pal8offs.bmp", - "rgb24lprof.bmp", - "rgb32fakealpha.bmp", - "rgb24largepal.bmp", - "pal8os2sp.bmp", - "pal8rletrns.bmp", - "rgb32bf-xbgr.bmp", - "rgba32.bmp", - "rgb32h52.bmp", - "rgba32h56.bmp", - ] - for f in get_files("q"): - try: - with Image.open(f) as im: - im.load() - if os.path.basename(f) not in supported: - print(f"Please add {f} to the partially supported bmp specs.") - except Exception: # as msg: - if os.path.basename(f) in supported: - raise - - -def test_good() -> None: - """These should all work. There's a set of target files in the - html directory that we can compare against.""" - - # Target files, if they're not just replacing the extension - file_map = { - "pal1wb.bmp": "pal1.png", - "pal4rle.bmp": "pal4.png", - "pal8-0.bmp": "pal8.png", - "pal8rle.bmp": "pal8.png", - "pal8topdown.bmp": "pal8.png", - "pal8nonsquare.bmp": "pal8nonsquare-v.png", - "pal8os2.bmp": "pal8.png", - "pal8os2sp.bmp": "pal8.png", - "pal8os2v2.bmp": "pal8.png", - "pal8os2v2-16.bmp": "pal8.png", - "pal8v4.bmp": "pal8.png", - "pal8v5.bmp": "pal8.png", - "rgb16-565pal.bmp": "rgb16-565.png", - "rgb24pal.bmp": "rgb24.png", - "rgb32.bmp": "rgb24.png", - "rgb32bf.bmp": "rgb24.png", - } - - def get_compare(f: str) -> str: - name = os.path.split(f)[1] - if name in file_map: - return os.path.join(base, "html", file_map[name]) - name = os.path.splitext(name)[0] - return os.path.join(base, "html", f"{name}.png") - - for f in get_files("g"): - try: - with Image.open(f) as im: - im.load() - with Image.open(get_compare(f)) as compare: - compare.load() - if im.mode == "P": - # assert image similar doesn't really work - # with paletized image, since the palette might - # be differently ordered for an equivalent image. - im = im.convert("RGBA") - compare = im.convert("RGBA") - assert_image_similar(im, compare, 5) - - except Exception as msg: - # there are three here that are unsupported: - unsupported = ( - os.path.join(base, "g", "rgb32bf.bmp"), - os.path.join(base, "g", "pal8rle.bmp"), - os.path.join(base, "g", "pal4rle.bmp"), - ) - assert f in unsupported, f"Unsupported Image {f}: {msg}" +from __future__ import annotations + +import os +import warnings + +from PIL import Image + +from .helper import assert_image_similar + +base = os.path.join("Tests", "images", "bmp") + + +def get_files(d: str, ext: str = ".bmp") -> list[str]: + return [ + os.path.join(base, d, f) for f in os.listdir(os.path.join(base, d)) if ext in f + ] + + +def test_bad() -> None: + """These shouldn't crash/dos, but they shouldn't return anything + either""" + for f in get_files("b"): + # Assert that there is no unclosed file warning + with warnings.catch_warnings(): + try: + with Image.open(f) as im: + im.load() + except Exception: # as msg: + pass + + +def test_questionable() -> None: + """These shouldn't crash/dos, but it's not well defined that these + are in spec""" + supported = [ + "pal8os2v2.bmp", + "rgb24prof.bmp", + "pal1p1.bmp", + "pal4rletrns.bmp", + "pal8offs.bmp", + "rgb24lprof.bmp", + "rgb32fakealpha.bmp", + "rgb24largepal.bmp", + "pal8os2sp.bmp", + "pal8rletrns.bmp", + "rgb32bf-xbgr.bmp", + "rgba32.bmp", + "rgb32h52.bmp", + "rgba32h56.bmp", + ] + for f in get_files("q"): + try: + with Image.open(f) as im: + im.load() + if os.path.basename(f) not in supported: + print(f"Please add {f} to the partially supported bmp specs.") + except Exception: # as msg: + if os.path.basename(f) in supported: + raise + + +def test_good() -> None: + """These should all work. There's a set of target files in the + html directory that we can compare against.""" + + # Target files, if they're not just replacing the extension + file_map = { + "pal1wb.bmp": "pal1.png", + "pal4rle.bmp": "pal4.png", + "pal8-0.bmp": "pal8.png", + "pal8rle.bmp": "pal8.png", + "pal8topdown.bmp": "pal8.png", + "pal8nonsquare.bmp": "pal8nonsquare-v.png", + "pal8os2.bmp": "pal8.png", + "pal8os2sp.bmp": "pal8.png", + "pal8os2v2.bmp": "pal8.png", + "pal8os2v2-16.bmp": "pal8.png", + "pal8v4.bmp": "pal8.png", + "pal8v5.bmp": "pal8.png", + "rgb16-565pal.bmp": "rgb16-565.png", + "rgb24pal.bmp": "rgb24.png", + "rgb32.bmp": "rgb24.png", + "rgb32bf.bmp": "rgb24.png", + } + + def get_compare(f: str) -> str: + name = os.path.split(f)[1] + if name in file_map: + return os.path.join(base, "html", file_map[name]) + name = os.path.splitext(name)[0] + return os.path.join(base, "html", f"{name}.png") + + for f in get_files("g"): + try: + with Image.open(f) as im: + im.load() + with Image.open(get_compare(f)) as compare: + compare.load() + if im.mode == "P": + # assert image similar doesn't really work + # with paletized image, since the palette might + # be differently ordered for an equivalent image. + im = im.convert("RGBA") + compare = im.convert("RGBA") + assert_image_similar(im, compare, 5) + + except Exception as msg: + # there are three here that are unsupported: + unsupported = ( + os.path.join(base, "g", "rgb32bf.bmp"), + os.path.join(base, "g", "pal8rle.bmp"), + os.path.join(base, "g", "pal4rle.bmp"), + ) + assert f in unsupported, f"Unsupported Image {f}: {msg}" diff --git a/Tests/test_box_blur.py b/Tests/test_box_blur.py index 1f6ed6127..fd0203b31 100644 --- a/Tests/test_box_blur.py +++ b/Tests/test_box_blur.py @@ -1,272 +1,272 @@ -from __future__ import annotations - -import pytest - -from PIL import Image, ImageFilter - -sample = Image.new("L", (7, 5)) -# fmt: off -sample.putdata(sum([ - [210, 50, 20, 10, 220, 230, 80], - [190, 210, 20, 180, 170, 40, 110], - [120, 210, 250, 60, 220, 0, 220], - [220, 40, 230, 80, 130, 250, 40], - [250, 0, 80, 30, 60, 20, 110], -], [])) -# fmt: on - - -def test_imageops_box_blur() -> None: - i = sample.filter(ImageFilter.BoxBlur(1)) - assert i.mode == sample.mode - assert i.size == sample.size - assert isinstance(i, Image.Image) - - -def box_blur(image: Image.Image, radius: float = 1, n: int = 1) -> Image.Image: - return image._new(image.im.box_blur((radius, radius), n)) - - -def assert_image(im: Image.Image, data: list[list[int]], delta: int = 0) -> None: - it = iter(im.getdata()) - for data_row in data: - im_row = [next(it) for _ in range(im.size[0])] - if any(abs(data_v - im_v) > delta for data_v, im_v in zip(data_row, im_row)): - assert im_row == data_row - with pytest.raises(StopIteration): - next(it) - - -def assert_blur( - im: Image.Image, - radius: float, - data: list[list[int]], - passes: int = 1, - delta: int = 0, -) -> None: - # check grayscale image - assert_image(box_blur(im, radius, passes), data, delta) - rgba = Image.merge("RGBA", (im, im, im, im)) - for band in box_blur(rgba, radius, passes).split(): - assert_image(band, data, delta) - - -def test_color_modes() -> None: - with pytest.raises(ValueError): - box_blur(sample.convert("1")) - with pytest.raises(ValueError): - box_blur(sample.convert("P")) - box_blur(sample.convert("L")) - box_blur(sample.convert("LA")) - box_blur(sample.convert("LA").convert("La")) - with pytest.raises(ValueError): - box_blur(sample.convert("I")) - with pytest.raises(ValueError): - box_blur(sample.convert("F")) - box_blur(sample.convert("RGB")) - box_blur(sample.convert("RGBA")) - box_blur(sample.convert("RGBA").convert("RGBa")) - box_blur(sample.convert("CMYK")) - with pytest.raises(ValueError): - box_blur(sample.convert("YCbCr")) - - -def test_radius_0() -> None: - assert_blur( - sample, - 0, - [ - # fmt: off - [210, 50, 20, 10, 220, 230, 80], - [190, 210, 20, 180, 170, 40, 110], - [120, 210, 250, 60, 220, 0, 220], - [220, 40, 230, 80, 130, 250, 40], - [250, 0, 80, 30, 60, 20, 110], - # fmt: on - ], - ) - - -def test_radius_0_02() -> None: - assert_blur( - sample, - 0.02, - [ - # fmt: off - [206, 55, 20, 17, 215, 223, 83], - [189, 203, 31, 171, 169, 46, 110], - [125, 206, 241, 69, 210, 13, 210], - [215, 49, 221, 82, 131, 235, 48], - [244, 7, 80, 32, 60, 27, 107], - # fmt: on - ], - delta=2, - ) - - -def test_radius_0_05() -> None: - assert_blur( - sample, - 0.05, - [ - # fmt: off - [202, 62, 22, 27, 209, 215, 88], - [188, 194, 44, 161, 168, 56, 111], - [131, 201, 229, 81, 198, 31, 198], - [209, 62, 209, 86, 133, 216, 59], - [237, 17, 80, 36, 60, 35, 103], - # fmt: on - ], - delta=2, - ) - - -def test_radius_0_1() -> None: - assert_blur( - sample, - 0.1, - [ - # fmt: off - [196, 72, 24, 40, 200, 203, 93], - [187, 183, 62, 148, 166, 68, 111], - [139, 193, 213, 96, 182, 54, 182], - [201, 78, 193, 91, 133, 191, 73], - [227, 31, 80, 42, 61, 47, 99], - # fmt: on - ], - delta=1, - ) - - -def test_radius_0_5() -> None: - assert_blur( - sample, - 0.5, - [ - # fmt: off - [176, 101, 46, 83, 163, 165, 111], - [176, 149, 108, 122, 144, 120, 117], - [164, 171, 159, 141, 134, 119, 129], - [170, 136, 133, 114, 116, 124, 109], - [184, 95, 72, 70, 69, 81, 89], - # fmt: on - ], - delta=1, - ) - - -def test_radius_1() -> None: - assert_blur( - sample, - 1, - [ - # fmt: off - [170, 109, 63, 97, 146, 153, 116], - [168, 142, 112, 128, 126, 143, 121], - [169, 166, 142, 149, 126, 131, 114], - [159, 156, 109, 127, 94, 117, 112], - [164, 128, 63, 87, 76, 89, 90], - # fmt: on - ], - delta=1, - ) - - -def test_radius_1_5() -> None: - assert_blur( - sample, - 1.5, - [ - # fmt: off - [155, 120, 105, 112, 124, 137, 130], - [160, 136, 124, 125, 127, 134, 130], - [166, 147, 130, 125, 120, 121, 119], - [168, 145, 119, 109, 103, 105, 110], - [168, 134, 96, 85, 85, 89, 97], - # fmt: on - ], - delta=1, - ) - - -def test_radius_bigger_then_half() -> None: - assert_blur( - sample, - 3, - [ - # fmt: off - [144, 145, 142, 128, 114, 115, 117], - [148, 145, 137, 122, 109, 111, 112], - [152, 145, 131, 117, 103, 107, 108], - [156, 144, 126, 111, 97, 102, 103], - [160, 144, 121, 106, 92, 98, 99], - # fmt: on - ], - delta=1, - ) - - -def test_radius_bigger_then_width() -> None: - assert_blur( - sample, - 10, - [ - [158, 153, 147, 141, 135, 129, 123], - [159, 153, 147, 141, 136, 130, 124], - [159, 154, 148, 142, 136, 130, 124], - [160, 154, 148, 142, 137, 131, 125], - [160, 155, 149, 143, 137, 131, 125], - ], - delta=0, - ) - - -def test_extreme_large_radius() -> None: - assert_blur( - sample, - 600, - [ - [162, 162, 162, 162, 162, 162, 162], - [162, 162, 162, 162, 162, 162, 162], - [162, 162, 162, 162, 162, 162, 162], - [162, 162, 162, 162, 162, 162, 162], - [162, 162, 162, 162, 162, 162, 162], - ], - delta=1, - ) - - -def test_two_passes() -> None: - assert_blur( - sample, - 1, - [ - # fmt: off - [153, 123, 102, 109, 132, 135, 129], - [159, 138, 123, 121, 133, 131, 126], - [162, 147, 136, 124, 127, 121, 121], - [159, 140, 125, 108, 111, 106, 108], - [154, 126, 105, 87, 94, 93, 97], - # fmt: on - ], - passes=2, - delta=1, - ) - - -def test_three_passes() -> None: - assert_blur( - sample, - 1, - [ - # fmt: off - [146, 131, 116, 118, 126, 131, 130], - [151, 138, 125, 123, 126, 128, 127], - [154, 143, 129, 123, 120, 120, 119], - [152, 139, 122, 113, 108, 108, 108], - [148, 132, 112, 102, 97, 99, 100], - # fmt: on - ], - passes=3, - delta=1, - ) +from __future__ import annotations + +import pytest + +from PIL import Image, ImageFilter + +sample = Image.new("L", (7, 5)) +# fmt: off +sample.putdata(sum([ + [210, 50, 20, 10, 220, 230, 80], + [190, 210, 20, 180, 170, 40, 110], + [120, 210, 250, 60, 220, 0, 220], + [220, 40, 230, 80, 130, 250, 40], + [250, 0, 80, 30, 60, 20, 110], +], [])) +# fmt: on + + +def test_imageops_box_blur() -> None: + i = sample.filter(ImageFilter.BoxBlur(1)) + assert i.mode == sample.mode + assert i.size == sample.size + assert isinstance(i, Image.Image) + + +def box_blur(image: Image.Image, radius: float = 1, n: int = 1) -> Image.Image: + return image._new(image.im.box_blur((radius, radius), n)) + + +def assert_image(im: Image.Image, data: list[list[int]], delta: int = 0) -> None: + it = iter(im.getdata()) + for data_row in data: + im_row = [next(it) for _ in range(im.size[0])] + if any(abs(data_v - im_v) > delta for data_v, im_v in zip(data_row, im_row)): + assert im_row == data_row + with pytest.raises(StopIteration): + next(it) + + +def assert_blur( + im: Image.Image, + radius: float, + data: list[list[int]], + passes: int = 1, + delta: int = 0, +) -> None: + # check grayscale image + assert_image(box_blur(im, radius, passes), data, delta) + rgba = Image.merge("RGBA", (im, im, im, im)) + for band in box_blur(rgba, radius, passes).split(): + assert_image(band, data, delta) + + +def test_color_modes() -> None: + with pytest.raises(ValueError): + box_blur(sample.convert("1")) + with pytest.raises(ValueError): + box_blur(sample.convert("P")) + box_blur(sample.convert("L")) + box_blur(sample.convert("LA")) + box_blur(sample.convert("LA").convert("La")) + with pytest.raises(ValueError): + box_blur(sample.convert("I")) + with pytest.raises(ValueError): + box_blur(sample.convert("F")) + box_blur(sample.convert("RGB")) + box_blur(sample.convert("RGBA")) + box_blur(sample.convert("RGBA").convert("RGBa")) + box_blur(sample.convert("CMYK")) + with pytest.raises(ValueError): + box_blur(sample.convert("YCbCr")) + + +def test_radius_0() -> None: + assert_blur( + sample, + 0, + [ + # fmt: off + [210, 50, 20, 10, 220, 230, 80], + [190, 210, 20, 180, 170, 40, 110], + [120, 210, 250, 60, 220, 0, 220], + [220, 40, 230, 80, 130, 250, 40], + [250, 0, 80, 30, 60, 20, 110], + # fmt: on + ], + ) + + +def test_radius_0_02() -> None: + assert_blur( + sample, + 0.02, + [ + # fmt: off + [206, 55, 20, 17, 215, 223, 83], + [189, 203, 31, 171, 169, 46, 110], + [125, 206, 241, 69, 210, 13, 210], + [215, 49, 221, 82, 131, 235, 48], + [244, 7, 80, 32, 60, 27, 107], + # fmt: on + ], + delta=2, + ) + + +def test_radius_0_05() -> None: + assert_blur( + sample, + 0.05, + [ + # fmt: off + [202, 62, 22, 27, 209, 215, 88], + [188, 194, 44, 161, 168, 56, 111], + [131, 201, 229, 81, 198, 31, 198], + [209, 62, 209, 86, 133, 216, 59], + [237, 17, 80, 36, 60, 35, 103], + # fmt: on + ], + delta=2, + ) + + +def test_radius_0_1() -> None: + assert_blur( + sample, + 0.1, + [ + # fmt: off + [196, 72, 24, 40, 200, 203, 93], + [187, 183, 62, 148, 166, 68, 111], + [139, 193, 213, 96, 182, 54, 182], + [201, 78, 193, 91, 133, 191, 73], + [227, 31, 80, 42, 61, 47, 99], + # fmt: on + ], + delta=1, + ) + + +def test_radius_0_5() -> None: + assert_blur( + sample, + 0.5, + [ + # fmt: off + [176, 101, 46, 83, 163, 165, 111], + [176, 149, 108, 122, 144, 120, 117], + [164, 171, 159, 141, 134, 119, 129], + [170, 136, 133, 114, 116, 124, 109], + [184, 95, 72, 70, 69, 81, 89], + # fmt: on + ], + delta=1, + ) + + +def test_radius_1() -> None: + assert_blur( + sample, + 1, + [ + # fmt: off + [170, 109, 63, 97, 146, 153, 116], + [168, 142, 112, 128, 126, 143, 121], + [169, 166, 142, 149, 126, 131, 114], + [159, 156, 109, 127, 94, 117, 112], + [164, 128, 63, 87, 76, 89, 90], + # fmt: on + ], + delta=1, + ) + + +def test_radius_1_5() -> None: + assert_blur( + sample, + 1.5, + [ + # fmt: off + [155, 120, 105, 112, 124, 137, 130], + [160, 136, 124, 125, 127, 134, 130], + [166, 147, 130, 125, 120, 121, 119], + [168, 145, 119, 109, 103, 105, 110], + [168, 134, 96, 85, 85, 89, 97], + # fmt: on + ], + delta=1, + ) + + +def test_radius_bigger_then_half() -> None: + assert_blur( + sample, + 3, + [ + # fmt: off + [144, 145, 142, 128, 114, 115, 117], + [148, 145, 137, 122, 109, 111, 112], + [152, 145, 131, 117, 103, 107, 108], + [156, 144, 126, 111, 97, 102, 103], + [160, 144, 121, 106, 92, 98, 99], + # fmt: on + ], + delta=1, + ) + + +def test_radius_bigger_then_width() -> None: + assert_blur( + sample, + 10, + [ + [158, 153, 147, 141, 135, 129, 123], + [159, 153, 147, 141, 136, 130, 124], + [159, 154, 148, 142, 136, 130, 124], + [160, 154, 148, 142, 137, 131, 125], + [160, 155, 149, 143, 137, 131, 125], + ], + delta=0, + ) + + +def test_extreme_large_radius() -> None: + assert_blur( + sample, + 600, + [ + [162, 162, 162, 162, 162, 162, 162], + [162, 162, 162, 162, 162, 162, 162], + [162, 162, 162, 162, 162, 162, 162], + [162, 162, 162, 162, 162, 162, 162], + [162, 162, 162, 162, 162, 162, 162], + ], + delta=1, + ) + + +def test_two_passes() -> None: + assert_blur( + sample, + 1, + [ + # fmt: off + [153, 123, 102, 109, 132, 135, 129], + [159, 138, 123, 121, 133, 131, 126], + [162, 147, 136, 124, 127, 121, 121], + [159, 140, 125, 108, 111, 106, 108], + [154, 126, 105, 87, 94, 93, 97], + # fmt: on + ], + passes=2, + delta=1, + ) + + +def test_three_passes() -> None: + assert_blur( + sample, + 1, + [ + # fmt: off + [146, 131, 116, 118, 126, 131, 130], + [151, 138, 125, 123, 126, 128, 127], + [154, 143, 129, 123, 120, 120, 119], + [152, 139, 122, 113, 108, 108, 108], + [148, 132, 112, 102, 97, 99, 100], + # fmt: on + ], + passes=3, + delta=1, + ) diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py index c8886a779..020ee7537 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -1,653 +1,653 @@ -from __future__ import annotations - -from array import array -from types import ModuleType - -import pytest - -from PIL import Image, ImageFilter - -from .helper import assert_image_equal - -numpy: ModuleType | None -try: - import numpy -except ImportError: - numpy = None - - -class TestColorLut3DCoreAPI: - def generate_identity_table( - self, channels: int, size: int | tuple[int, int, int] - ) -> tuple[int, int, int, int, list[float]]: - if isinstance(size, tuple): - size_1d, size_2d, size_3d = size - else: - size_1d, size_2d, size_3d = (size, size, size) - - table = [ - [ - r / (size_1d - 1) if size_1d != 1 else 0, - g / (size_2d - 1) if size_2d != 1 else 0, - b / (size_3d - 1) if size_3d != 1 else 0, - r / (size_1d - 1) if size_1d != 1 else 0, - g / (size_2d - 1) if size_2d != 1 else 0, - ][:channels] - for b in range(size_3d) - for g in range(size_2d) - for r in range(size_1d) - ] - return ( - channels, - size_1d, - size_2d, - size_3d, - [item for sublist in table for item in sublist], - ) - - def test_wrong_args(self) -> None: - im = Image.new("RGB", (10, 10), 0) - - with pytest.raises(ValueError, match="filter"): - im.im.color_lut_3d( - "RGB", Image.Resampling.BICUBIC, *self.generate_identity_table(3, 3) - ) - - with pytest.raises(ValueError, match="image mode"): - im.im.color_lut_3d( - "wrong", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) - ) - - with pytest.raises(ValueError, match="table_channels"): - im.im.color_lut_3d( - "RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(5, 3) - ) - - with pytest.raises(ValueError, match="table_channels"): - im.im.color_lut_3d( - "RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(1, 3) - ) - - with pytest.raises(ValueError, match="table_channels"): - im.im.color_lut_3d( - "RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(2, 3) - ) - - with pytest.raises(ValueError, match="Table size"): - im.im.color_lut_3d( - "RGB", - Image.Resampling.BILINEAR, - *self.generate_identity_table(3, (1, 3, 3)), - ) - - with pytest.raises(ValueError, match="Table size"): - im.im.color_lut_3d( - "RGB", - Image.Resampling.BILINEAR, - *self.generate_identity_table(3, (66, 3, 3)), - ) - - with pytest.raises(ValueError, match=r"size1D \* size2D \* size3D"): - im.im.color_lut_3d( - "RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, [0, 0, 0] * 7 - ) - - with pytest.raises(ValueError, match=r"size1D \* size2D \* size3D"): - im.im.color_lut_3d( - "RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, [0, 0, 0] * 9 - ) - - with pytest.raises(TypeError): - im.im.color_lut_3d( - "RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, [0, 0, "0"] * 8 - ) - - with pytest.raises(TypeError): - im.im.color_lut_3d("RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, 16) - - def test_correct_args(self) -> None: - im = Image.new("RGB", (10, 10), 0) - - im.im.color_lut_3d( - "RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) - ) - - im.im.color_lut_3d( - "CMYK", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3) - ) - - im.im.color_lut_3d( - "RGB", - Image.Resampling.BILINEAR, - *self.generate_identity_table(3, (2, 3, 3)), - ) - - im.im.color_lut_3d( - "RGB", - Image.Resampling.BILINEAR, - *self.generate_identity_table(3, (65, 3, 3)), - ) - - im.im.color_lut_3d( - "RGB", - Image.Resampling.BILINEAR, - *self.generate_identity_table(3, (3, 65, 3)), - ) - - im.im.color_lut_3d( - "RGB", - Image.Resampling.BILINEAR, - *self.generate_identity_table(3, (3, 3, 65)), - ) - - def test_wrong_mode(self) -> None: - with pytest.raises(ValueError, match="wrong mode"): - im = Image.new("L", (10, 10), 0) - im.im.color_lut_3d( - "RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) - ) - - with pytest.raises(ValueError, match="wrong mode"): - im = Image.new("RGB", (10, 10), 0) - im.im.color_lut_3d( - "L", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) - ) - - with pytest.raises(ValueError, match="wrong mode"): - im = Image.new("L", (10, 10), 0) - im.im.color_lut_3d( - "L", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) - ) - - with pytest.raises(ValueError, match="wrong mode"): - im = Image.new("RGB", (10, 10), 0) - im.im.color_lut_3d( - "RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) - ) - - with pytest.raises(ValueError, match="wrong mode"): - im = Image.new("RGB", (10, 10), 0) - im.im.color_lut_3d( - "RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3) - ) - - def test_correct_mode(self) -> None: - im = Image.new("RGBA", (10, 10), 0) - im.im.color_lut_3d( - "RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) - ) - - im = Image.new("RGBA", (10, 10), 0) - im.im.color_lut_3d( - "RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3) - ) - - im = Image.new("RGB", (10, 10), 0) - im.im.color_lut_3d( - "HSV", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) - ) - - im = Image.new("RGB", (10, 10), 0) - im.im.color_lut_3d( - "RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3) - ) - - def test_identities(self) -> None: - g = Image.linear_gradient("L") - im = Image.merge( - "RGB", - [ - g, - g.transpose(Image.Transpose.ROTATE_90), - g.transpose(Image.Transpose.ROTATE_180), - ], - ) - - # Fast test with small cubes - for size in [2, 3, 5, 7, 11, 16, 17]: - assert_image_equal( - im, - im._new( - im.im.color_lut_3d( - "RGB", - Image.Resampling.BILINEAR, - *self.generate_identity_table(3, size), - ) - ), - ) - - # Not so fast - assert_image_equal( - im, - im._new( - im.im.color_lut_3d( - "RGB", - Image.Resampling.BILINEAR, - *self.generate_identity_table(3, (2, 2, 65)), - ) - ), - ) - - def test_identities_4_channels(self) -> None: - g = Image.linear_gradient("L") - im = Image.merge( - "RGB", - [ - g, - g.transpose(Image.Transpose.ROTATE_90), - g.transpose(Image.Transpose.ROTATE_180), - ], - ) - - # Red channel copied to alpha - assert_image_equal( - Image.merge("RGBA", (im.split() * 2)[:4]), - im._new( - im.im.color_lut_3d( - "RGBA", - Image.Resampling.BILINEAR, - *self.generate_identity_table(4, 17), - ) - ), - ) - - def test_copy_alpha_channel(self) -> None: - g = Image.linear_gradient("L") - im = Image.merge( - "RGBA", - [ - g, - g.transpose(Image.Transpose.ROTATE_90), - g.transpose(Image.Transpose.ROTATE_180), - g.transpose(Image.Transpose.ROTATE_270), - ], - ) - - assert_image_equal( - im, - im._new( - im.im.color_lut_3d( - "RGBA", - Image.Resampling.BILINEAR, - *self.generate_identity_table(3, 17), - ) - ), - ) - - def test_channels_order(self) -> None: - g = Image.linear_gradient("L") - im = Image.merge( - "RGB", - [ - g, - g.transpose(Image.Transpose.ROTATE_90), - g.transpose(Image.Transpose.ROTATE_180), - ], - ) - - # Reverse channels by splitting and using table - # fmt: off - assert_image_equal( - Image.merge('RGB', im.split()[::-1]), - im._new(im.im.color_lut_3d('RGB', Image.Resampling.BILINEAR, - 3, 2, 2, 2, [ - 0, 0, 0, 0, 0, 1, - 0, 1, 0, 0, 1, 1, - - 1, 0, 0, 1, 0, 1, - 1, 1, 0, 1, 1, 1, - ]))) - # fmt: on - - def test_overflow(self) -> None: - g = Image.linear_gradient("L") - im = Image.merge( - "RGB", - [ - g, - g.transpose(Image.Transpose.ROTATE_90), - g.transpose(Image.Transpose.ROTATE_180), - ], - ) - - # fmt: off - transformed = im._new(im.im.color_lut_3d('RGB', Image.Resampling.BILINEAR, - 3, 2, 2, 2, - [ - -1, -1, -1, 2, -1, -1, - -1, 2, -1, 2, 2, -1, - - -1, -1, 2, 2, -1, 2, - -1, 2, 2, 2, 2, 2, - ])).load() - # fmt: on - assert transformed[0, 0] == (0, 0, 255) - assert transformed[50, 50] == (0, 0, 255) - assert transformed[255, 0] == (0, 255, 255) - assert transformed[205, 50] == (0, 255, 255) - assert transformed[0, 255] == (255, 0, 0) - assert transformed[50, 205] == (255, 0, 0) - assert transformed[255, 255] == (255, 255, 0) - assert transformed[205, 205] == (255, 255, 0) - - # fmt: off - transformed = im._new(im.im.color_lut_3d('RGB', Image.Resampling.BILINEAR, - 3, 2, 2, 2, - [ - -3, -3, -3, 5, -3, -3, - -3, 5, -3, 5, 5, -3, - - -3, -3, 5, 5, -3, 5, - -3, 5, 5, 5, 5, 5, - ])).load() - # fmt: on - assert transformed[0, 0] == (0, 0, 255) - assert transformed[50, 50] == (0, 0, 255) - assert transformed[255, 0] == (0, 255, 255) - assert transformed[205, 50] == (0, 255, 255) - assert transformed[0, 255] == (255, 0, 0) - assert transformed[50, 205] == (255, 0, 0) - assert transformed[255, 255] == (255, 255, 0) - assert transformed[205, 205] == (255, 255, 0) - - -class TestColorLut3DFilter: - def test_wrong_args(self) -> None: - with pytest.raises(ValueError, match="should be either an integer"): - ImageFilter.Color3DLUT("small", [1]) - - with pytest.raises(ValueError, match="should be either an integer"): - ImageFilter.Color3DLUT((11, 11), [1]) - - with pytest.raises(ValueError, match=r"in \[2, 65\] range"): - ImageFilter.Color3DLUT((11, 11, 1), [1]) - - with pytest.raises(ValueError, match=r"in \[2, 65\] range"): - ImageFilter.Color3DLUT((11, 11, 66), [1]) - - with pytest.raises(ValueError, match="table should have .+ items"): - ImageFilter.Color3DLUT((3, 3, 3), [1, 1, 1]) - - with pytest.raises(ValueError, match="table should have .+ items"): - ImageFilter.Color3DLUT((3, 3, 3), [[1, 1, 1]] * 2) - - with pytest.raises(ValueError, match="should have a length of 4"): - ImageFilter.Color3DLUT((3, 3, 3), [[1, 1, 1]] * 27, channels=4) - - with pytest.raises(ValueError, match="should have a length of 3"): - ImageFilter.Color3DLUT((2, 2, 2), [[1, 1]] * 8) - - with pytest.raises(ValueError, match="Only 3 or 4 output"): - ImageFilter.Color3DLUT((2, 2, 2), [[1, 1]] * 8, channels=2) - - def test_convert_table(self) -> None: - lut = ImageFilter.Color3DLUT(2, [0, 1, 2] * 8) - assert tuple(lut.size) == (2, 2, 2) - assert lut.name == "Color 3D LUT" - - # fmt: off - lut = ImageFilter.Color3DLUT((2, 2, 2), [ - (0, 1, 2), (3, 4, 5), (6, 7, 8), (9, 10, 11), - (12, 13, 14), (15, 16, 17), (18, 19, 20), (21, 22, 23)]) - # fmt: on - assert tuple(lut.size) == (2, 2, 2) - assert lut.table == list(range(24)) - - lut = ImageFilter.Color3DLUT((2, 2, 2), [(0, 1, 2, 3)] * 8, channels=4) - assert tuple(lut.size) == (2, 2, 2) - assert lut.table == list(range(4)) * 8 - - @pytest.mark.skipif(numpy is None, reason="NumPy not installed") - def test_numpy_sources(self) -> None: - assert numpy is not None - table = numpy.ones((5, 6, 7, 3), dtype=numpy.float16) - with pytest.raises(ValueError, match="should have either channels"): - lut = ImageFilter.Color3DLUT((5, 6, 7), table) - - table = numpy.ones((7, 6, 5, 3), dtype=numpy.float16) - lut = ImageFilter.Color3DLUT((5, 6, 7), table) - assert isinstance(lut.table, numpy.ndarray) - assert lut.table.dtype == table.dtype - assert lut.table.shape == (table.size,) - - table = numpy.ones((7 * 6 * 5, 3), dtype=numpy.float16) - lut = ImageFilter.Color3DLUT((5, 6, 7), table) - assert lut.table.shape == (table.size,) - - table = numpy.ones((7 * 6 * 5 * 3), dtype=numpy.float16) - lut = ImageFilter.Color3DLUT((5, 6, 7), table) - assert lut.table.shape == (table.size,) - - # Check application - Image.new("RGB", (10, 10), 0).filter(lut) - - # Check copy - table[0] = 33 - assert lut.table[0] == 1 - - # Check not copy - table = numpy.ones((7 * 6 * 5 * 3), dtype=numpy.float16) - lut = ImageFilter.Color3DLUT((5, 6, 7), table, _copy_table=False) - table[0] = 33 - assert lut.table[0] == 33 - - @pytest.mark.skipif(numpy is None, reason="NumPy not installed") - def test_numpy_formats(self) -> None: - assert numpy is not None - g = Image.linear_gradient("L") - im = Image.merge( - "RGB", - [ - g, - g.transpose(Image.Transpose.ROTATE_90), - g.transpose(Image.Transpose.ROTATE_180), - ], - ) - - lut = ImageFilter.Color3DLUT.generate((7, 9, 11), lambda r, g, b: (r, g, b)) - lut.table = numpy.array(lut.table, dtype=numpy.float32)[:-1] - with pytest.raises(ValueError, match="should have table_channels"): - im.filter(lut) - - lut = ImageFilter.Color3DLUT.generate((7, 9, 11), lambda r, g, b: (r, g, b)) - lut.table = numpy.array(lut.table, dtype=numpy.float32).reshape((7 * 9 * 11), 3) - with pytest.raises(ValueError, match="should have table_channels"): - im.filter(lut) - - lut = ImageFilter.Color3DLUT.generate((7, 9, 11), lambda r, g, b: (r, g, b)) - lut.table = numpy.array(lut.table, dtype=numpy.float16) - assert_image_equal(im, im.filter(lut)) - - lut = ImageFilter.Color3DLUT.generate((7, 9, 11), lambda r, g, b: (r, g, b)) - lut.table = numpy.array(lut.table, dtype=numpy.float32) - assert_image_equal(im, im.filter(lut)) - - lut = ImageFilter.Color3DLUT.generate((7, 9, 11), lambda r, g, b: (r, g, b)) - lut.table = numpy.array(lut.table, dtype=numpy.float64) - assert_image_equal(im, im.filter(lut)) - - lut = ImageFilter.Color3DLUT.generate((7, 9, 11), lambda r, g, b: (r, g, b)) - lut.table = numpy.array(lut.table, dtype=numpy.int32) - im.filter(lut) - lut.table = numpy.array(lut.table, dtype=numpy.int8) - im.filter(lut) - - def test_repr(self) -> None: - lut = ImageFilter.Color3DLUT(2, [0, 1, 2] * 8) - assert repr(lut) == "" - - lut = ImageFilter.Color3DLUT( - (3, 4, 5), - array("f", [0, 0, 0, 0] * (3 * 4 * 5)), - channels=4, - target_mode="YCbCr", - _copy_table=False, - ) - assert ( - repr(lut) - == "" - ) - - -class TestGenerateColorLut3D: - def test_wrong_channels_count(self) -> None: - with pytest.raises(ValueError, match="3 or 4 output channels"): - ImageFilter.Color3DLUT.generate( - 5, channels=2, callback=lambda r, g, b: (r, g, b) - ) - - with pytest.raises(ValueError, match="should have either channels"): - ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b, r)) - - with pytest.raises(ValueError, match="should have either channels"): - ImageFilter.Color3DLUT.generate( - 5, channels=4, callback=lambda r, g, b: (r, g, b) - ) - - def test_3_channels(self) -> None: - lut = ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b)) - assert tuple(lut.size) == (5, 5, 5) - assert lut.name == "Color 3D LUT" - # fmt: off - assert lut.table[:24] == [ - 0.0, 0.0, 0.0, 0.25, 0.0, 0.0, 0.5, 0.0, 0.0, 0.75, 0.0, 0.0, - 1.0, 0.0, 0.0, 0.0, 0.25, 0.0, 0.25, 0.25, 0.0, 0.5, 0.25, 0.0] - # fmt: on - - def test_4_channels(self) -> None: - lut = ImageFilter.Color3DLUT.generate( - 5, channels=4, callback=lambda r, g, b: (b, r, g, (r + g + b) / 2) - ) - assert tuple(lut.size) == (5, 5, 5) - assert lut.name == "Color 3D LUT" - # fmt: off - assert lut.table[:24] == [ - 0.0, 0.0, 0.0, 0.0, 0.0, 0.25, 0.0, 0.125, 0.0, 0.5, 0.0, 0.25, - 0.0, 0.75, 0.0, 0.375, 0.0, 1.0, 0.0, 0.5, 0.0, 0.0, 0.25, 0.125 - ] - # fmt: on - - def test_apply(self) -> None: - lut = ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b)) - - g = Image.linear_gradient("L") - im = Image.merge( - "RGB", - [ - g, - g.transpose(Image.Transpose.ROTATE_90), - g.transpose(Image.Transpose.ROTATE_180), - ], - ) - assert im == im.filter(lut) - - -class TestTransformColorLut3D: - def test_wrong_args(self) -> None: - source = ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b)) - - with pytest.raises(ValueError, match="Only 3 or 4 output"): - source.transform(lambda r, g, b: (r, g, b), channels=8) - - with pytest.raises(ValueError, match="should have either channels"): - source.transform(lambda r, g, b: (r, g, b), channels=4) - - with pytest.raises(ValueError, match="should have either channels"): - source.transform(lambda r, g, b: (r, g, b, 1)) - - with pytest.raises(TypeError): - source.transform(lambda r, g, b, a: (r, g, b)) - - def test_target_mode(self) -> None: - source = ImageFilter.Color3DLUT.generate( - 2, lambda r, g, b: (r, g, b), target_mode="HSV" - ) - - lut = source.transform(lambda r, g, b: (r, g, b)) - assert lut.mode == "HSV" - - lut = source.transform(lambda r, g, b: (r, g, b), target_mode="RGB") - assert lut.mode == "RGB" - - def test_3_to_3_channels(self) -> None: - source = ImageFilter.Color3DLUT.generate((3, 4, 5), lambda r, g, b: (r, g, b)) - lut = source.transform(lambda r, g, b: (r * r, g * g, b * b)) - assert tuple(lut.size) == tuple(source.size) - assert len(lut.table) == len(source.table) - assert lut.table != source.table - assert lut.table[:10] == [0.0, 0.0, 0.0, 0.25, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0] - - def test_3_to_4_channels(self) -> None: - source = ImageFilter.Color3DLUT.generate((6, 5, 4), lambda r, g, b: (r, g, b)) - lut = source.transform(lambda r, g, b: (r * r, g * g, b * b, 1), channels=4) - assert tuple(lut.size) == tuple(source.size) - assert len(lut.table) != len(source.table) - assert lut.table != source.table - # fmt: off - assert lut.table[:16] == [ - 0.0, 0.0, 0.0, 1, 0.2**2, 0.0, 0.0, 1, - 0.4**2, 0.0, 0.0, 1, 0.6**2, 0.0, 0.0, 1] - # fmt: on - - def test_4_to_3_channels(self) -> None: - source = ImageFilter.Color3DLUT.generate( - (3, 6, 5), lambda r, g, b: (r, g, b, 1), channels=4 - ) - lut = source.transform( - lambda r, g, b, a: (a - r * r, a - g * g, a - b * b), channels=3 - ) - assert tuple(lut.size) == tuple(source.size) - assert len(lut.table) != len(source.table) - assert lut.table != source.table - # fmt: off - assert lut.table[:18] == [ - 1.0, 1.0, 1.0, 0.75, 1.0, 1.0, 0.0, 1.0, 1.0, - 1.0, 0.96, 1.0, 0.75, 0.96, 1.0, 0.0, 0.96, 1.0] - # fmt: on - - def test_4_to_4_channels(self) -> None: - source = ImageFilter.Color3DLUT.generate( - (6, 5, 4), lambda r, g, b: (r, g, b, 1), channels=4 - ) - lut = source.transform(lambda r, g, b, a: (r * r, g * g, b * b, a - 0.5)) - assert tuple(lut.size) == tuple(source.size) - assert len(lut.table) == len(source.table) - assert lut.table != source.table - # fmt: off - assert lut.table[:16] == [ - 0.0, 0.0, 0.0, 0.5, 0.2**2, 0.0, 0.0, 0.5, - 0.4**2, 0.0, 0.0, 0.5, 0.6**2, 0.0, 0.0, 0.5] - # fmt: on - - def test_with_normals_3_channels(self) -> None: - source = ImageFilter.Color3DLUT.generate( - (6, 5, 4), lambda r, g, b: (r * r, g * g, b * b) - ) - lut = source.transform( - lambda nr, ng, nb, r, g, b: (nr - r, ng - g, nb - b), with_normals=True - ) - assert tuple(lut.size) == tuple(source.size) - assert len(lut.table) == len(source.table) - assert lut.table != source.table - # fmt: off - assert lut.table[:18] == [ - 0.0, 0.0, 0.0, 0.16, 0.0, 0.0, 0.24, 0.0, 0.0, - 0.24, 0.0, 0.0, 0.8 - (0.8**2), 0, 0, 0, 0, 0] - # fmt: on - - def test_with_normals_4_channels(self) -> None: - source = ImageFilter.Color3DLUT.generate( - (3, 6, 5), lambda r, g, b: (r * r, g * g, b * b, 1), channels=4 - ) - lut = source.transform( - lambda nr, ng, nb, r, g, b, a: (nr - r, ng - g, nb - b, a - 0.5), - with_normals=True, - ) - assert tuple(lut.size) == tuple(source.size) - assert len(lut.table) == len(source.table) - assert lut.table != source.table - # fmt: off - assert lut.table[:16] == [ - 0.0, 0.0, 0.0, 0.5, 0.25, 0.0, 0.0, 0.5, - 0.0, 0.0, 0.0, 0.5, 0.0, 0.16, 0.0, 0.5] - # fmt: on +from __future__ import annotations + +from array import array +from types import ModuleType + +import pytest + +from PIL import Image, ImageFilter + +from .helper import assert_image_equal + +numpy: ModuleType | None +try: + import numpy +except ImportError: + numpy = None + + +class TestColorLut3DCoreAPI: + def generate_identity_table( + self, channels: int, size: int | tuple[int, int, int] + ) -> tuple[int, int, int, int, list[float]]: + if isinstance(size, tuple): + size_1d, size_2d, size_3d = size + else: + size_1d, size_2d, size_3d = (size, size, size) + + table = [ + [ + r / (size_1d - 1) if size_1d != 1 else 0, + g / (size_2d - 1) if size_2d != 1 else 0, + b / (size_3d - 1) if size_3d != 1 else 0, + r / (size_1d - 1) if size_1d != 1 else 0, + g / (size_2d - 1) if size_2d != 1 else 0, + ][:channels] + for b in range(size_3d) + for g in range(size_2d) + for r in range(size_1d) + ] + return ( + channels, + size_1d, + size_2d, + size_3d, + [item for sublist in table for item in sublist], + ) + + def test_wrong_args(self) -> None: + im = Image.new("RGB", (10, 10), 0) + + with pytest.raises(ValueError, match="filter"): + im.im.color_lut_3d( + "RGB", Image.Resampling.BICUBIC, *self.generate_identity_table(3, 3) + ) + + with pytest.raises(ValueError, match="image mode"): + im.im.color_lut_3d( + "wrong", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) + ) + + with pytest.raises(ValueError, match="table_channels"): + im.im.color_lut_3d( + "RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(5, 3) + ) + + with pytest.raises(ValueError, match="table_channels"): + im.im.color_lut_3d( + "RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(1, 3) + ) + + with pytest.raises(ValueError, match="table_channels"): + im.im.color_lut_3d( + "RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(2, 3) + ) + + with pytest.raises(ValueError, match="Table size"): + im.im.color_lut_3d( + "RGB", + Image.Resampling.BILINEAR, + *self.generate_identity_table(3, (1, 3, 3)), + ) + + with pytest.raises(ValueError, match="Table size"): + im.im.color_lut_3d( + "RGB", + Image.Resampling.BILINEAR, + *self.generate_identity_table(3, (66, 3, 3)), + ) + + with pytest.raises(ValueError, match=r"size1D \* size2D \* size3D"): + im.im.color_lut_3d( + "RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, [0, 0, 0] * 7 + ) + + with pytest.raises(ValueError, match=r"size1D \* size2D \* size3D"): + im.im.color_lut_3d( + "RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, [0, 0, 0] * 9 + ) + + with pytest.raises(TypeError): + im.im.color_lut_3d( + "RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, [0, 0, "0"] * 8 + ) + + with pytest.raises(TypeError): + im.im.color_lut_3d("RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, 16) + + def test_correct_args(self) -> None: + im = Image.new("RGB", (10, 10), 0) + + im.im.color_lut_3d( + "RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) + ) + + im.im.color_lut_3d( + "CMYK", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3) + ) + + im.im.color_lut_3d( + "RGB", + Image.Resampling.BILINEAR, + *self.generate_identity_table(3, (2, 3, 3)), + ) + + im.im.color_lut_3d( + "RGB", + Image.Resampling.BILINEAR, + *self.generate_identity_table(3, (65, 3, 3)), + ) + + im.im.color_lut_3d( + "RGB", + Image.Resampling.BILINEAR, + *self.generate_identity_table(3, (3, 65, 3)), + ) + + im.im.color_lut_3d( + "RGB", + Image.Resampling.BILINEAR, + *self.generate_identity_table(3, (3, 3, 65)), + ) + + def test_wrong_mode(self) -> None: + with pytest.raises(ValueError, match="wrong mode"): + im = Image.new("L", (10, 10), 0) + im.im.color_lut_3d( + "RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) + ) + + with pytest.raises(ValueError, match="wrong mode"): + im = Image.new("RGB", (10, 10), 0) + im.im.color_lut_3d( + "L", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) + ) + + with pytest.raises(ValueError, match="wrong mode"): + im = Image.new("L", (10, 10), 0) + im.im.color_lut_3d( + "L", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) + ) + + with pytest.raises(ValueError, match="wrong mode"): + im = Image.new("RGB", (10, 10), 0) + im.im.color_lut_3d( + "RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) + ) + + with pytest.raises(ValueError, match="wrong mode"): + im = Image.new("RGB", (10, 10), 0) + im.im.color_lut_3d( + "RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3) + ) + + def test_correct_mode(self) -> None: + im = Image.new("RGBA", (10, 10), 0) + im.im.color_lut_3d( + "RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) + ) + + im = Image.new("RGBA", (10, 10), 0) + im.im.color_lut_3d( + "RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3) + ) + + im = Image.new("RGB", (10, 10), 0) + im.im.color_lut_3d( + "HSV", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) + ) + + im = Image.new("RGB", (10, 10), 0) + im.im.color_lut_3d( + "RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3) + ) + + def test_identities(self) -> None: + g = Image.linear_gradient("L") + im = Image.merge( + "RGB", + [ + g, + g.transpose(Image.Transpose.ROTATE_90), + g.transpose(Image.Transpose.ROTATE_180), + ], + ) + + # Fast test with small cubes + for size in [2, 3, 5, 7, 11, 16, 17]: + assert_image_equal( + im, + im._new( + im.im.color_lut_3d( + "RGB", + Image.Resampling.BILINEAR, + *self.generate_identity_table(3, size), + ) + ), + ) + + # Not so fast + assert_image_equal( + im, + im._new( + im.im.color_lut_3d( + "RGB", + Image.Resampling.BILINEAR, + *self.generate_identity_table(3, (2, 2, 65)), + ) + ), + ) + + def test_identities_4_channels(self) -> None: + g = Image.linear_gradient("L") + im = Image.merge( + "RGB", + [ + g, + g.transpose(Image.Transpose.ROTATE_90), + g.transpose(Image.Transpose.ROTATE_180), + ], + ) + + # Red channel copied to alpha + assert_image_equal( + Image.merge("RGBA", (im.split() * 2)[:4]), + im._new( + im.im.color_lut_3d( + "RGBA", + Image.Resampling.BILINEAR, + *self.generate_identity_table(4, 17), + ) + ), + ) + + def test_copy_alpha_channel(self) -> None: + g = Image.linear_gradient("L") + im = Image.merge( + "RGBA", + [ + g, + g.transpose(Image.Transpose.ROTATE_90), + g.transpose(Image.Transpose.ROTATE_180), + g.transpose(Image.Transpose.ROTATE_270), + ], + ) + + assert_image_equal( + im, + im._new( + im.im.color_lut_3d( + "RGBA", + Image.Resampling.BILINEAR, + *self.generate_identity_table(3, 17), + ) + ), + ) + + def test_channels_order(self) -> None: + g = Image.linear_gradient("L") + im = Image.merge( + "RGB", + [ + g, + g.transpose(Image.Transpose.ROTATE_90), + g.transpose(Image.Transpose.ROTATE_180), + ], + ) + + # Reverse channels by splitting and using table + # fmt: off + assert_image_equal( + Image.merge('RGB', im.split()[::-1]), + im._new(im.im.color_lut_3d('RGB', Image.Resampling.BILINEAR, + 3, 2, 2, 2, [ + 0, 0, 0, 0, 0, 1, + 0, 1, 0, 0, 1, 1, + + 1, 0, 0, 1, 0, 1, + 1, 1, 0, 1, 1, 1, + ]))) + # fmt: on + + def test_overflow(self) -> None: + g = Image.linear_gradient("L") + im = Image.merge( + "RGB", + [ + g, + g.transpose(Image.Transpose.ROTATE_90), + g.transpose(Image.Transpose.ROTATE_180), + ], + ) + + # fmt: off + transformed = im._new(im.im.color_lut_3d('RGB', Image.Resampling.BILINEAR, + 3, 2, 2, 2, + [ + -1, -1, -1, 2, -1, -1, + -1, 2, -1, 2, 2, -1, + + -1, -1, 2, 2, -1, 2, + -1, 2, 2, 2, 2, 2, + ])).load() + # fmt: on + assert transformed[0, 0] == (0, 0, 255) + assert transformed[50, 50] == (0, 0, 255) + assert transformed[255, 0] == (0, 255, 255) + assert transformed[205, 50] == (0, 255, 255) + assert transformed[0, 255] == (255, 0, 0) + assert transformed[50, 205] == (255, 0, 0) + assert transformed[255, 255] == (255, 255, 0) + assert transformed[205, 205] == (255, 255, 0) + + # fmt: off + transformed = im._new(im.im.color_lut_3d('RGB', Image.Resampling.BILINEAR, + 3, 2, 2, 2, + [ + -3, -3, -3, 5, -3, -3, + -3, 5, -3, 5, 5, -3, + + -3, -3, 5, 5, -3, 5, + -3, 5, 5, 5, 5, 5, + ])).load() + # fmt: on + assert transformed[0, 0] == (0, 0, 255) + assert transformed[50, 50] == (0, 0, 255) + assert transformed[255, 0] == (0, 255, 255) + assert transformed[205, 50] == (0, 255, 255) + assert transformed[0, 255] == (255, 0, 0) + assert transformed[50, 205] == (255, 0, 0) + assert transformed[255, 255] == (255, 255, 0) + assert transformed[205, 205] == (255, 255, 0) + + +class TestColorLut3DFilter: + def test_wrong_args(self) -> None: + with pytest.raises(ValueError, match="should be either an integer"): + ImageFilter.Color3DLUT("small", [1]) + + with pytest.raises(ValueError, match="should be either an integer"): + ImageFilter.Color3DLUT((11, 11), [1]) + + with pytest.raises(ValueError, match=r"in \[2, 65\] range"): + ImageFilter.Color3DLUT((11, 11, 1), [1]) + + with pytest.raises(ValueError, match=r"in \[2, 65\] range"): + ImageFilter.Color3DLUT((11, 11, 66), [1]) + + with pytest.raises(ValueError, match="table should have .+ items"): + ImageFilter.Color3DLUT((3, 3, 3), [1, 1, 1]) + + with pytest.raises(ValueError, match="table should have .+ items"): + ImageFilter.Color3DLUT((3, 3, 3), [[1, 1, 1]] * 2) + + with pytest.raises(ValueError, match="should have a length of 4"): + ImageFilter.Color3DLUT((3, 3, 3), [[1, 1, 1]] * 27, channels=4) + + with pytest.raises(ValueError, match="should have a length of 3"): + ImageFilter.Color3DLUT((2, 2, 2), [[1, 1]] * 8) + + with pytest.raises(ValueError, match="Only 3 or 4 output"): + ImageFilter.Color3DLUT((2, 2, 2), [[1, 1]] * 8, channels=2) + + def test_convert_table(self) -> None: + lut = ImageFilter.Color3DLUT(2, [0, 1, 2] * 8) + assert tuple(lut.size) == (2, 2, 2) + assert lut.name == "Color 3D LUT" + + # fmt: off + lut = ImageFilter.Color3DLUT((2, 2, 2), [ + (0, 1, 2), (3, 4, 5), (6, 7, 8), (9, 10, 11), + (12, 13, 14), (15, 16, 17), (18, 19, 20), (21, 22, 23)]) + # fmt: on + assert tuple(lut.size) == (2, 2, 2) + assert lut.table == list(range(24)) + + lut = ImageFilter.Color3DLUT((2, 2, 2), [(0, 1, 2, 3)] * 8, channels=4) + assert tuple(lut.size) == (2, 2, 2) + assert lut.table == list(range(4)) * 8 + + @pytest.mark.skipif(numpy is None, reason="NumPy not installed") + def test_numpy_sources(self) -> None: + assert numpy is not None + table = numpy.ones((5, 6, 7, 3), dtype=numpy.float16) + with pytest.raises(ValueError, match="should have either channels"): + lut = ImageFilter.Color3DLUT((5, 6, 7), table) + + table = numpy.ones((7, 6, 5, 3), dtype=numpy.float16) + lut = ImageFilter.Color3DLUT((5, 6, 7), table) + assert isinstance(lut.table, numpy.ndarray) + assert lut.table.dtype == table.dtype + assert lut.table.shape == (table.size,) + + table = numpy.ones((7 * 6 * 5, 3), dtype=numpy.float16) + lut = ImageFilter.Color3DLUT((5, 6, 7), table) + assert lut.table.shape == (table.size,) + + table = numpy.ones((7 * 6 * 5 * 3), dtype=numpy.float16) + lut = ImageFilter.Color3DLUT((5, 6, 7), table) + assert lut.table.shape == (table.size,) + + # Check application + Image.new("RGB", (10, 10), 0).filter(lut) + + # Check copy + table[0] = 33 + assert lut.table[0] == 1 + + # Check not copy + table = numpy.ones((7 * 6 * 5 * 3), dtype=numpy.float16) + lut = ImageFilter.Color3DLUT((5, 6, 7), table, _copy_table=False) + table[0] = 33 + assert lut.table[0] == 33 + + @pytest.mark.skipif(numpy is None, reason="NumPy not installed") + def test_numpy_formats(self) -> None: + assert numpy is not None + g = Image.linear_gradient("L") + im = Image.merge( + "RGB", + [ + g, + g.transpose(Image.Transpose.ROTATE_90), + g.transpose(Image.Transpose.ROTATE_180), + ], + ) + + lut = ImageFilter.Color3DLUT.generate((7, 9, 11), lambda r, g, b: (r, g, b)) + lut.table = numpy.array(lut.table, dtype=numpy.float32)[:-1] + with pytest.raises(ValueError, match="should have table_channels"): + im.filter(lut) + + lut = ImageFilter.Color3DLUT.generate((7, 9, 11), lambda r, g, b: (r, g, b)) + lut.table = numpy.array(lut.table, dtype=numpy.float32).reshape((7 * 9 * 11), 3) + with pytest.raises(ValueError, match="should have table_channels"): + im.filter(lut) + + lut = ImageFilter.Color3DLUT.generate((7, 9, 11), lambda r, g, b: (r, g, b)) + lut.table = numpy.array(lut.table, dtype=numpy.float16) + assert_image_equal(im, im.filter(lut)) + + lut = ImageFilter.Color3DLUT.generate((7, 9, 11), lambda r, g, b: (r, g, b)) + lut.table = numpy.array(lut.table, dtype=numpy.float32) + assert_image_equal(im, im.filter(lut)) + + lut = ImageFilter.Color3DLUT.generate((7, 9, 11), lambda r, g, b: (r, g, b)) + lut.table = numpy.array(lut.table, dtype=numpy.float64) + assert_image_equal(im, im.filter(lut)) + + lut = ImageFilter.Color3DLUT.generate((7, 9, 11), lambda r, g, b: (r, g, b)) + lut.table = numpy.array(lut.table, dtype=numpy.int32) + im.filter(lut) + lut.table = numpy.array(lut.table, dtype=numpy.int8) + im.filter(lut) + + def test_repr(self) -> None: + lut = ImageFilter.Color3DLUT(2, [0, 1, 2] * 8) + assert repr(lut) == "" + + lut = ImageFilter.Color3DLUT( + (3, 4, 5), + array("f", [0, 0, 0, 0] * (3 * 4 * 5)), + channels=4, + target_mode="YCbCr", + _copy_table=False, + ) + assert ( + repr(lut) + == "" + ) + + +class TestGenerateColorLut3D: + def test_wrong_channels_count(self) -> None: + with pytest.raises(ValueError, match="3 or 4 output channels"): + ImageFilter.Color3DLUT.generate( + 5, channels=2, callback=lambda r, g, b: (r, g, b) + ) + + with pytest.raises(ValueError, match="should have either channels"): + ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b, r)) + + with pytest.raises(ValueError, match="should have either channels"): + ImageFilter.Color3DLUT.generate( + 5, channels=4, callback=lambda r, g, b: (r, g, b) + ) + + def test_3_channels(self) -> None: + lut = ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b)) + assert tuple(lut.size) == (5, 5, 5) + assert lut.name == "Color 3D LUT" + # fmt: off + assert lut.table[:24] == [ + 0.0, 0.0, 0.0, 0.25, 0.0, 0.0, 0.5, 0.0, 0.0, 0.75, 0.0, 0.0, + 1.0, 0.0, 0.0, 0.0, 0.25, 0.0, 0.25, 0.25, 0.0, 0.5, 0.25, 0.0] + # fmt: on + + def test_4_channels(self) -> None: + lut = ImageFilter.Color3DLUT.generate( + 5, channels=4, callback=lambda r, g, b: (b, r, g, (r + g + b) / 2) + ) + assert tuple(lut.size) == (5, 5, 5) + assert lut.name == "Color 3D LUT" + # fmt: off + assert lut.table[:24] == [ + 0.0, 0.0, 0.0, 0.0, 0.0, 0.25, 0.0, 0.125, 0.0, 0.5, 0.0, 0.25, + 0.0, 0.75, 0.0, 0.375, 0.0, 1.0, 0.0, 0.5, 0.0, 0.0, 0.25, 0.125 + ] + # fmt: on + + def test_apply(self) -> None: + lut = ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b)) + + g = Image.linear_gradient("L") + im = Image.merge( + "RGB", + [ + g, + g.transpose(Image.Transpose.ROTATE_90), + g.transpose(Image.Transpose.ROTATE_180), + ], + ) + assert im == im.filter(lut) + + +class TestTransformColorLut3D: + def test_wrong_args(self) -> None: + source = ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b)) + + with pytest.raises(ValueError, match="Only 3 or 4 output"): + source.transform(lambda r, g, b: (r, g, b), channels=8) + + with pytest.raises(ValueError, match="should have either channels"): + source.transform(lambda r, g, b: (r, g, b), channels=4) + + with pytest.raises(ValueError, match="should have either channels"): + source.transform(lambda r, g, b: (r, g, b, 1)) + + with pytest.raises(TypeError): + source.transform(lambda r, g, b, a: (r, g, b)) + + def test_target_mode(self) -> None: + source = ImageFilter.Color3DLUT.generate( + 2, lambda r, g, b: (r, g, b), target_mode="HSV" + ) + + lut = source.transform(lambda r, g, b: (r, g, b)) + assert lut.mode == "HSV" + + lut = source.transform(lambda r, g, b: (r, g, b), target_mode="RGB") + assert lut.mode == "RGB" + + def test_3_to_3_channels(self) -> None: + source = ImageFilter.Color3DLUT.generate((3, 4, 5), lambda r, g, b: (r, g, b)) + lut = source.transform(lambda r, g, b: (r * r, g * g, b * b)) + assert tuple(lut.size) == tuple(source.size) + assert len(lut.table) == len(source.table) + assert lut.table != source.table + assert lut.table[:10] == [0.0, 0.0, 0.0, 0.25, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0] + + def test_3_to_4_channels(self) -> None: + source = ImageFilter.Color3DLUT.generate((6, 5, 4), lambda r, g, b: (r, g, b)) + lut = source.transform(lambda r, g, b: (r * r, g * g, b * b, 1), channels=4) + assert tuple(lut.size) == tuple(source.size) + assert len(lut.table) != len(source.table) + assert lut.table != source.table + # fmt: off + assert lut.table[:16] == [ + 0.0, 0.0, 0.0, 1, 0.2**2, 0.0, 0.0, 1, + 0.4**2, 0.0, 0.0, 1, 0.6**2, 0.0, 0.0, 1] + # fmt: on + + def test_4_to_3_channels(self) -> None: + source = ImageFilter.Color3DLUT.generate( + (3, 6, 5), lambda r, g, b: (r, g, b, 1), channels=4 + ) + lut = source.transform( + lambda r, g, b, a: (a - r * r, a - g * g, a - b * b), channels=3 + ) + assert tuple(lut.size) == tuple(source.size) + assert len(lut.table) != len(source.table) + assert lut.table != source.table + # fmt: off + assert lut.table[:18] == [ + 1.0, 1.0, 1.0, 0.75, 1.0, 1.0, 0.0, 1.0, 1.0, + 1.0, 0.96, 1.0, 0.75, 0.96, 1.0, 0.0, 0.96, 1.0] + # fmt: on + + def test_4_to_4_channels(self) -> None: + source = ImageFilter.Color3DLUT.generate( + (6, 5, 4), lambda r, g, b: (r, g, b, 1), channels=4 + ) + lut = source.transform(lambda r, g, b, a: (r * r, g * g, b * b, a - 0.5)) + assert tuple(lut.size) == tuple(source.size) + assert len(lut.table) == len(source.table) + assert lut.table != source.table + # fmt: off + assert lut.table[:16] == [ + 0.0, 0.0, 0.0, 0.5, 0.2**2, 0.0, 0.0, 0.5, + 0.4**2, 0.0, 0.0, 0.5, 0.6**2, 0.0, 0.0, 0.5] + # fmt: on + + def test_with_normals_3_channels(self) -> None: + source = ImageFilter.Color3DLUT.generate( + (6, 5, 4), lambda r, g, b: (r * r, g * g, b * b) + ) + lut = source.transform( + lambda nr, ng, nb, r, g, b: (nr - r, ng - g, nb - b), with_normals=True + ) + assert tuple(lut.size) == tuple(source.size) + assert len(lut.table) == len(source.table) + assert lut.table != source.table + # fmt: off + assert lut.table[:18] == [ + 0.0, 0.0, 0.0, 0.16, 0.0, 0.0, 0.24, 0.0, 0.0, + 0.24, 0.0, 0.0, 0.8 - (0.8**2), 0, 0, 0, 0, 0] + # fmt: on + + def test_with_normals_4_channels(self) -> None: + source = ImageFilter.Color3DLUT.generate( + (3, 6, 5), lambda r, g, b: (r * r, g * g, b * b, 1), channels=4 + ) + lut = source.transform( + lambda nr, ng, nb, r, g, b, a: (nr - r, ng - g, nb - b, a - 0.5), + with_normals=True, + ) + assert tuple(lut.size) == tuple(source.size) + assert len(lut.table) == len(source.table) + assert lut.table != source.table + # fmt: off + assert lut.table[:16] == [ + 0.0, 0.0, 0.0, 0.5, 0.25, 0.0, 0.0, 0.5, + 0.0, 0.0, 0.0, 0.5, 0.0, 0.16, 0.0, 0.5] + # fmt: on diff --git a/Tests/test_core_resources.py b/Tests/test_core_resources.py index 2c1de8bc3..377add04d 100644 --- a/Tests/test_core_resources.py +++ b/Tests/test_core_resources.py @@ -1,192 +1,192 @@ -from __future__ import annotations - -import sys - -import pytest - -from PIL import Image - -from .helper import is_pypy - - -def test_get_stats() -> None: - # Create at least one image - Image.new("RGB", (10, 10)) - - stats = Image.core.get_stats() - assert "new_count" in stats - assert "reused_blocks" in stats - assert "freed_blocks" in stats - assert "allocated_blocks" in stats - assert "reallocated_blocks" in stats - assert "blocks_cached" in stats - - -def test_reset_stats() -> None: - Image.core.reset_stats() - - stats = Image.core.get_stats() - assert stats["new_count"] == 0 - assert stats["reused_blocks"] == 0 - assert stats["freed_blocks"] == 0 - assert stats["allocated_blocks"] == 0 - assert stats["reallocated_blocks"] == 0 - assert stats["blocks_cached"] == 0 - - -class TestCoreMemory: - def teardown_method(self) -> None: - # Restore default values - Image.core.set_alignment(1) - Image.core.set_block_size(1024 * 1024) - Image.core.set_blocks_max(0) - Image.core.clear_cache() - - def test_get_alignment(self) -> None: - alignment = Image.core.get_alignment() - - assert alignment > 0 - - def test_set_alignment(self) -> None: - for i in [1, 2, 4, 8, 16, 32]: - Image.core.set_alignment(i) - alignment = Image.core.get_alignment() - assert alignment == i - - # Try to construct new image - Image.new("RGB", (10, 10)) - - with pytest.raises(ValueError): - Image.core.set_alignment(0) - with pytest.raises(ValueError): - Image.core.set_alignment(-1) - with pytest.raises(ValueError): - Image.core.set_alignment(3) - - def test_get_block_size(self) -> None: - block_size = Image.core.get_block_size() - - assert block_size >= 4096 - - def test_set_block_size(self) -> None: - for i in [4096, 2 * 4096, 3 * 4096]: - Image.core.set_block_size(i) - block_size = Image.core.get_block_size() - assert block_size == i - - # Try to construct new image - Image.new("RGB", (10, 10)) - - with pytest.raises(ValueError): - Image.core.set_block_size(0) - with pytest.raises(ValueError): - Image.core.set_block_size(-1) - with pytest.raises(ValueError): - Image.core.set_block_size(4000) - - def test_set_block_size_stats(self) -> None: - Image.core.reset_stats() - Image.core.set_blocks_max(0) - Image.core.set_block_size(4096) - Image.new("RGB", (256, 256)) - - stats = Image.core.get_stats() - assert stats["new_count"] >= 1 - assert stats["allocated_blocks"] >= 64 - if not is_pypy(): - assert stats["freed_blocks"] >= 64 - - def test_get_blocks_max(self) -> None: - blocks_max = Image.core.get_blocks_max() - - assert blocks_max >= 0 - - def test_set_blocks_max(self) -> None: - for i in [0, 1, 10]: - Image.core.set_blocks_max(i) - blocks_max = Image.core.get_blocks_max() - assert blocks_max == i - - # Try to construct new image - Image.new("RGB", (10, 10)) - - with pytest.raises(ValueError): - Image.core.set_blocks_max(-1) - if sys.maxsize < 2**32: - with pytest.raises(ValueError): - Image.core.set_blocks_max(2**29) - - @pytest.mark.skipif(is_pypy(), reason="Images not collected") - def test_set_blocks_max_stats(self) -> None: - Image.core.reset_stats() - Image.core.set_blocks_max(128) - Image.core.set_block_size(4096) - Image.new("RGB", (256, 256)) - Image.new("RGB", (256, 256)) - - stats = Image.core.get_stats() - assert stats["new_count"] >= 2 - assert stats["allocated_blocks"] >= 64 - assert stats["reused_blocks"] >= 64 - assert stats["freed_blocks"] == 0 - assert stats["blocks_cached"] == 64 - - @pytest.mark.skipif(is_pypy(), reason="Images not collected") - def test_clear_cache_stats(self) -> None: - Image.core.reset_stats() - Image.core.clear_cache() - Image.core.set_blocks_max(128) - Image.core.set_block_size(4096) - Image.new("RGB", (256, 256)) - Image.new("RGB", (256, 256)) - # Keep 16 blocks in cache - Image.core.clear_cache(16) - - stats = Image.core.get_stats() - assert stats["new_count"] >= 2 - assert stats["allocated_blocks"] >= 64 - assert stats["reused_blocks"] >= 64 - assert stats["freed_blocks"] >= 48 - assert stats["blocks_cached"] == 16 - - def test_large_images(self) -> None: - Image.core.reset_stats() - Image.core.set_blocks_max(0) - Image.core.set_block_size(4096) - Image.new("RGB", (2048, 16)) - Image.core.clear_cache() - - stats = Image.core.get_stats() - assert stats["new_count"] >= 1 - assert stats["allocated_blocks"] >= 16 - assert stats["reused_blocks"] >= 0 - assert stats["blocks_cached"] == 0 - if not is_pypy(): - assert stats["freed_blocks"] >= 16 - - -class TestEnvVars: - def teardown_method(self) -> None: - # Restore default values - Image.core.set_alignment(1) - Image.core.set_block_size(1024 * 1024) - Image.core.set_blocks_max(0) - Image.core.clear_cache() - - def test_units(self) -> None: - Image._apply_env_variables({"PILLOW_BLOCKS_MAX": "2K"}) - assert Image.core.get_blocks_max() == 2 * 1024 - Image._apply_env_variables({"PILLOW_BLOCK_SIZE": "2m"}) - assert Image.core.get_block_size() == 2 * 1024 * 1024 - - @pytest.mark.parametrize( - "var", - ( - {"PILLOW_ALIGNMENT": "15"}, - {"PILLOW_BLOCK_SIZE": "1024"}, - {"PILLOW_BLOCKS_MAX": "wat"}, - ), - ) - def test_warnings(self, var: dict[str, str]) -> None: - with pytest.warns(UserWarning): - Image._apply_env_variables(var) +from __future__ import annotations + +import sys + +import pytest + +from PIL import Image + +from .helper import is_pypy + + +def test_get_stats() -> None: + # Create at least one image + Image.new("RGB", (10, 10)) + + stats = Image.core.get_stats() + assert "new_count" in stats + assert "reused_blocks" in stats + assert "freed_blocks" in stats + assert "allocated_blocks" in stats + assert "reallocated_blocks" in stats + assert "blocks_cached" in stats + + +def test_reset_stats() -> None: + Image.core.reset_stats() + + stats = Image.core.get_stats() + assert stats["new_count"] == 0 + assert stats["reused_blocks"] == 0 + assert stats["freed_blocks"] == 0 + assert stats["allocated_blocks"] == 0 + assert stats["reallocated_blocks"] == 0 + assert stats["blocks_cached"] == 0 + + +class TestCoreMemory: + def teardown_method(self) -> None: + # Restore default values + Image.core.set_alignment(1) + Image.core.set_block_size(1024 * 1024) + Image.core.set_blocks_max(0) + Image.core.clear_cache() + + def test_get_alignment(self) -> None: + alignment = Image.core.get_alignment() + + assert alignment > 0 + + def test_set_alignment(self) -> None: + for i in [1, 2, 4, 8, 16, 32]: + Image.core.set_alignment(i) + alignment = Image.core.get_alignment() + assert alignment == i + + # Try to construct new image + Image.new("RGB", (10, 10)) + + with pytest.raises(ValueError): + Image.core.set_alignment(0) + with pytest.raises(ValueError): + Image.core.set_alignment(-1) + with pytest.raises(ValueError): + Image.core.set_alignment(3) + + def test_get_block_size(self) -> None: + block_size = Image.core.get_block_size() + + assert block_size >= 4096 + + def test_set_block_size(self) -> None: + for i in [4096, 2 * 4096, 3 * 4096]: + Image.core.set_block_size(i) + block_size = Image.core.get_block_size() + assert block_size == i + + # Try to construct new image + Image.new("RGB", (10, 10)) + + with pytest.raises(ValueError): + Image.core.set_block_size(0) + with pytest.raises(ValueError): + Image.core.set_block_size(-1) + with pytest.raises(ValueError): + Image.core.set_block_size(4000) + + def test_set_block_size_stats(self) -> None: + Image.core.reset_stats() + Image.core.set_blocks_max(0) + Image.core.set_block_size(4096) + Image.new("RGB", (256, 256)) + + stats = Image.core.get_stats() + assert stats["new_count"] >= 1 + assert stats["allocated_blocks"] >= 64 + if not is_pypy(): + assert stats["freed_blocks"] >= 64 + + def test_get_blocks_max(self) -> None: + blocks_max = Image.core.get_blocks_max() + + assert blocks_max >= 0 + + def test_set_blocks_max(self) -> None: + for i in [0, 1, 10]: + Image.core.set_blocks_max(i) + blocks_max = Image.core.get_blocks_max() + assert blocks_max == i + + # Try to construct new image + Image.new("RGB", (10, 10)) + + with pytest.raises(ValueError): + Image.core.set_blocks_max(-1) + if sys.maxsize < 2**32: + with pytest.raises(ValueError): + Image.core.set_blocks_max(2**29) + + @pytest.mark.skipif(is_pypy(), reason="Images not collected") + def test_set_blocks_max_stats(self) -> None: + Image.core.reset_stats() + Image.core.set_blocks_max(128) + Image.core.set_block_size(4096) + Image.new("RGB", (256, 256)) + Image.new("RGB", (256, 256)) + + stats = Image.core.get_stats() + assert stats["new_count"] >= 2 + assert stats["allocated_blocks"] >= 64 + assert stats["reused_blocks"] >= 64 + assert stats["freed_blocks"] == 0 + assert stats["blocks_cached"] == 64 + + @pytest.mark.skipif(is_pypy(), reason="Images not collected") + def test_clear_cache_stats(self) -> None: + Image.core.reset_stats() + Image.core.clear_cache() + Image.core.set_blocks_max(128) + Image.core.set_block_size(4096) + Image.new("RGB", (256, 256)) + Image.new("RGB", (256, 256)) + # Keep 16 blocks in cache + Image.core.clear_cache(16) + + stats = Image.core.get_stats() + assert stats["new_count"] >= 2 + assert stats["allocated_blocks"] >= 64 + assert stats["reused_blocks"] >= 64 + assert stats["freed_blocks"] >= 48 + assert stats["blocks_cached"] == 16 + + def test_large_images(self) -> None: + Image.core.reset_stats() + Image.core.set_blocks_max(0) + Image.core.set_block_size(4096) + Image.new("RGB", (2048, 16)) + Image.core.clear_cache() + + stats = Image.core.get_stats() + assert stats["new_count"] >= 1 + assert stats["allocated_blocks"] >= 16 + assert stats["reused_blocks"] >= 0 + assert stats["blocks_cached"] == 0 + if not is_pypy(): + assert stats["freed_blocks"] >= 16 + + +class TestEnvVars: + def teardown_method(self) -> None: + # Restore default values + Image.core.set_alignment(1) + Image.core.set_block_size(1024 * 1024) + Image.core.set_blocks_max(0) + Image.core.clear_cache() + + def test_units(self) -> None: + Image._apply_env_variables({"PILLOW_BLOCKS_MAX": "2K"}) + assert Image.core.get_blocks_max() == 2 * 1024 + Image._apply_env_variables({"PILLOW_BLOCK_SIZE": "2m"}) + assert Image.core.get_block_size() == 2 * 1024 * 1024 + + @pytest.mark.parametrize( + "var", + ( + {"PILLOW_ALIGNMENT": "15"}, + {"PILLOW_BLOCK_SIZE": "1024"}, + {"PILLOW_BLOCKS_MAX": "wat"}, + ), + ) + def test_warnings(self, var: dict[str, str]) -> None: + with pytest.warns(UserWarning): + Image._apply_env_variables(var) diff --git a/Tests/test_decompression_bomb.py b/Tests/test_decompression_bomb.py index 9c21efa45..3bdad031b 100644 --- a/Tests/test_decompression_bomb.py +++ b/Tests/test_decompression_bomb.py @@ -1,112 +1,112 @@ -from __future__ import annotations - -import pytest - -from PIL import Image - -from .helper import hopper - -TEST_FILE = "Tests/images/hopper.ppm" - -ORIGINAL_LIMIT = Image.MAX_IMAGE_PIXELS - - -class TestDecompressionBomb: - def teardown_method(self, method) -> 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: - # Arrange - # Turn limit off - Image.MAX_IMAGE_PIXELS = None - assert Image.MAX_IMAGE_PIXELS is None - - # Act / Assert - # Implicit assert: no warning. - # A warning would cause a failure. - with Image.open(TEST_FILE): - pass - - def test_warning(self) -> None: - # Set limit to trigger warning on the test file - 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: - # Set limit to trigger exception on the test file - Image.MAX_IMAGE_PIXELS = 64 * 128 - 1 - assert Image.MAX_IMAGE_PIXELS == 64 * 128 - 1 - - with pytest.raises(Image.DecompressionBombError): - with Image.open(TEST_FILE): - pass - - def test_exception_ico(self) -> None: - with pytest.raises(Image.DecompressionBombError): - with Image.open("Tests/images/decompression_bomb.ico"): - pass - - def test_exception_gif(self) -> None: - with pytest.raises(Image.DecompressionBombError): - with Image.open("Tests/images/decompression_bomb.gif"): - pass - - def test_exception_gif_extents(self) -> None: - with Image.open("Tests/images/decompression_bomb_extents.gif") as im: - with pytest.raises(Image.DecompressionBombError): - im.seek(1) - - def test_exception_gif_zero_width(self) -> None: - # Set limit to trigger exception on the test file - Image.MAX_IMAGE_PIXELS = 4 * 64 * 128 - assert Image.MAX_IMAGE_PIXELS == 4 * 64 * 128 - - with pytest.raises(Image.DecompressionBombError): - with Image.open("Tests/images/zero_width.gif"): - pass - - def test_exception_bmp(self) -> None: - with pytest.raises(Image.DecompressionBombError): - with Image.open("Tests/images/bmp/b/reallybig.bmp"): - pass - - -class TestDecompressionCrop: - @classmethod - def setup_class(cls) -> None: - width, height = 128, 128 - Image.MAX_IMAGE_PIXELS = height * width * 4 - 1 - - @classmethod - def teardown_class(cls) -> None: - Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT - - def test_enlarge_crop(self) -> None: - # Crops can extend the extents, therefore we should have the - # same decompression bomb warnings on them. - with hopper() as src: - box = (0, 0, src.width * 2, src.height * 2) - with pytest.warns(Image.DecompressionBombWarning): - src.crop(box) - - def test_crop_decompression_checks(self) -> None: - im = Image.new("RGB", (100, 100)) - - for value in ((-9999, -9999, -9990, -9990), (-999, -999, -990, -990)): - assert im.crop(value).size == (9, 9) - - with pytest.warns(Image.DecompressionBombWarning): - im.crop((-160, -160, 99, 99)) - - with pytest.raises(Image.DecompressionBombError): - im.crop((-99909, -99990, 99999, 99999)) +from __future__ import annotations + +import pytest + +from PIL import Image + +from .helper import hopper + +TEST_FILE = "Tests/images/hopper.ppm" + +ORIGINAL_LIMIT = Image.MAX_IMAGE_PIXELS + + +class TestDecompressionBomb: + def teardown_method(self, method) -> 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: + # Arrange + # Turn limit off + Image.MAX_IMAGE_PIXELS = None + assert Image.MAX_IMAGE_PIXELS is None + + # Act / Assert + # Implicit assert: no warning. + # A warning would cause a failure. + with Image.open(TEST_FILE): + pass + + def test_warning(self) -> None: + # Set limit to trigger warning on the test file + 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: + # Set limit to trigger exception on the test file + Image.MAX_IMAGE_PIXELS = 64 * 128 - 1 + assert Image.MAX_IMAGE_PIXELS == 64 * 128 - 1 + + with pytest.raises(Image.DecompressionBombError): + with Image.open(TEST_FILE): + pass + + def test_exception_ico(self) -> None: + with pytest.raises(Image.DecompressionBombError): + with Image.open("Tests/images/decompression_bomb.ico"): + pass + + def test_exception_gif(self) -> None: + with pytest.raises(Image.DecompressionBombError): + with Image.open("Tests/images/decompression_bomb.gif"): + pass + + def test_exception_gif_extents(self) -> None: + with Image.open("Tests/images/decompression_bomb_extents.gif") as im: + with pytest.raises(Image.DecompressionBombError): + im.seek(1) + + def test_exception_gif_zero_width(self) -> None: + # Set limit to trigger exception on the test file + Image.MAX_IMAGE_PIXELS = 4 * 64 * 128 + assert Image.MAX_IMAGE_PIXELS == 4 * 64 * 128 + + with pytest.raises(Image.DecompressionBombError): + with Image.open("Tests/images/zero_width.gif"): + pass + + def test_exception_bmp(self) -> None: + with pytest.raises(Image.DecompressionBombError): + with Image.open("Tests/images/bmp/b/reallybig.bmp"): + pass + + +class TestDecompressionCrop: + @classmethod + def setup_class(cls) -> None: + width, height = 128, 128 + Image.MAX_IMAGE_PIXELS = height * width * 4 - 1 + + @classmethod + def teardown_class(cls) -> None: + Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT + + def test_enlarge_crop(self) -> None: + # Crops can extend the extents, therefore we should have the + # same decompression bomb warnings on them. + with hopper() as src: + box = (0, 0, src.width * 2, src.height * 2) + with pytest.warns(Image.DecompressionBombWarning): + src.crop(box) + + def test_crop_decompression_checks(self) -> None: + im = Image.new("RGB", (100, 100)) + + for value in ((-9999, -9999, -9990, -9990), (-999, -999, -990, -990)): + assert im.crop(value).size == (9, 9) + + with pytest.warns(Image.DecompressionBombWarning): + im.crop((-160, -160, 99, 99)) + + with pytest.raises(Image.DecompressionBombError): + im.crop((-99909, -99990, 99999, 99999)) diff --git a/Tests/test_deprecate.py b/Tests/test_deprecate.py index 584d8f91d..abfeb270a 100644 --- a/Tests/test_deprecate.py +++ b/Tests/test_deprecate.py @@ -1,93 +1,93 @@ -from __future__ import annotations - -import pytest - -from PIL import _deprecate - - -@pytest.mark.parametrize( - "version, expected", - [ - ( - 11, - "Old thing is deprecated and will be removed in Pillow 11 " - r"\(2024-10-15\)\. Use new thing instead\.", - ), - ( - None, - r"Old thing is deprecated and will be removed in a future version\. " - r"Use new thing instead\.", - ), - ], -) -def test_version(version: int | None, expected: str) -> None: - with pytest.warns(DeprecationWarning, match=expected): - _deprecate.deprecate("Old thing", version, "new thing") - - -def test_unknown_version() -> None: - expected = r"Unknown removal version: 12345. Update PIL\._deprecate\?" - with pytest.raises(ValueError, match=expected): - _deprecate.deprecate("Old thing", 12345, "new thing") - - -@pytest.mark.parametrize( - "deprecated, plural, expected", - [ - ( - "Old thing", - False, - r"Old thing is deprecated and should be removed\.", - ), - ( - "Old things", - True, - r"Old things are deprecated and should be removed\.", - ), - ], -) -def test_old_version(deprecated: str, plural: bool, expected: str) -> None: - expected = r"" - with pytest.raises(RuntimeError, match=expected): - _deprecate.deprecate(deprecated, 1, plural=plural) - - -def test_plural() -> None: - expected = ( - r"Old things are deprecated and will be removed in Pillow 11 \(2024-10-15\)\. " - r"Use new thing instead\." - ) - with pytest.warns(DeprecationWarning, match=expected): - _deprecate.deprecate("Old things", 11, "new thing", plural=True) - - -def test_replacement_and_action() -> None: - expected = "Use only one of 'replacement' and 'action'" - with pytest.raises(ValueError, match=expected): - _deprecate.deprecate( - "Old thing", 11, replacement="new thing", action="Upgrade to new thing" - ) - - -@pytest.mark.parametrize( - "action", - [ - "Upgrade to new thing", - "Upgrade to new thing.", - ], -) -def test_action(action: str) -> None: - expected = ( - r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)\. " - r"Upgrade to new thing\." - ) - with pytest.warns(DeprecationWarning, match=expected): - _deprecate.deprecate("Old thing", 11, action=action) - - -def test_no_replacement_or_action() -> None: - expected = ( - r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)" - ) - with pytest.warns(DeprecationWarning, match=expected): - _deprecate.deprecate("Old thing", 11) +from __future__ import annotations + +import pytest + +from PIL import _deprecate + + +@pytest.mark.parametrize( + "version, expected", + [ + ( + 11, + "Old thing is deprecated and will be removed in Pillow 11 " + r"\(2024-10-15\)\. Use new thing instead\.", + ), + ( + None, + r"Old thing is deprecated and will be removed in a future version\. " + r"Use new thing instead\.", + ), + ], +) +def test_version(version: int | None, expected: str) -> None: + with pytest.warns(DeprecationWarning, match=expected): + _deprecate.deprecate("Old thing", version, "new thing") + + +def test_unknown_version() -> None: + expected = r"Unknown removal version: 12345. Update PIL\._deprecate\?" + with pytest.raises(ValueError, match=expected): + _deprecate.deprecate("Old thing", 12345, "new thing") + + +@pytest.mark.parametrize( + "deprecated, plural, expected", + [ + ( + "Old thing", + False, + r"Old thing is deprecated and should be removed\.", + ), + ( + "Old things", + True, + r"Old things are deprecated and should be removed\.", + ), + ], +) +def test_old_version(deprecated: str, plural: bool, expected: str) -> None: + expected = r"" + with pytest.raises(RuntimeError, match=expected): + _deprecate.deprecate(deprecated, 1, plural=plural) + + +def test_plural() -> None: + expected = ( + r"Old things are deprecated and will be removed in Pillow 11 \(2024-10-15\)\. " + r"Use new thing instead\." + ) + with pytest.warns(DeprecationWarning, match=expected): + _deprecate.deprecate("Old things", 11, "new thing", plural=True) + + +def test_replacement_and_action() -> None: + expected = "Use only one of 'replacement' and 'action'" + with pytest.raises(ValueError, match=expected): + _deprecate.deprecate( + "Old thing", 11, replacement="new thing", action="Upgrade to new thing" + ) + + +@pytest.mark.parametrize( + "action", + [ + "Upgrade to new thing", + "Upgrade to new thing.", + ], +) +def test_action(action: str) -> None: + expected = ( + r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)\. " + r"Upgrade to new thing\." + ) + with pytest.warns(DeprecationWarning, match=expected): + _deprecate.deprecate("Old thing", 11, action=action) + + +def test_no_replacement_or_action() -> None: + expected = ( + r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)" + ) + with pytest.warns(DeprecationWarning, match=expected): + _deprecate.deprecate("Old thing", 11) diff --git a/Tests/test_features.py b/Tests/test_features.py index b7eefa09a..2d346d7f8 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -1,160 +1,160 @@ -from __future__ import annotations - -import io -import re -from typing import Callable - -import pytest - -from PIL import features - -from .helper import skip_unless_feature - -try: - from PIL import _webp -except ImportError: - pass - - -def test_check() -> None: - # Check the correctness of the convenience function - for module in features.modules: - assert features.check_module(module) == features.check(module) - for codec in features.codecs: - assert features.check_codec(codec) == features.check(codec) - for feature in features.features: - assert features.check_feature(feature) == features.check(feature) - - -def test_version() -> None: - # Check the correctness of the convenience function - # and the format of version numbers - - def test(name: str, function: Callable[[str], str | None]) -> None: - version = features.version(name) - if not features.check(name): - assert version is 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) - assert version is None or re.search(r"\d+(\.\d+)*$", version) - - for module in features.modules: - test(module, features.version_module) - for codec in features.codecs: - test(codec, features.version_codec) - for feature in features.features: - test(feature, features.version_feature) - - -@skip_unless_feature("webp") -def test_webp_transparency() -> None: - assert features.check("transp_webp") != _webp.WebPDecoderBuggyAlpha() - assert features.check("transp_webp") == _webp.HAVE_TRANSPARENCY - - -@skip_unless_feature("webp") -def test_webp_mux() -> None: - assert features.check("webp_mux") == _webp.HAVE_WEBPMUX - - -@skip_unless_feature("webp") -def test_webp_anim() -> None: - assert features.check("webp_anim") == _webp.HAVE_WEBPANIM - - -@skip_unless_feature("libjpeg_turbo") -def test_libjpeg_turbo_version() -> None: - version = features.version("libjpeg_turbo") - assert version is not None - assert re.search(r"\d+\.\d+\.\d+$", version) - - -@skip_unless_feature("libimagequant") -def test_libimagequant_version() -> None: - version = features.version("libimagequant") - assert version is not None - assert re.search(r"\d+\.\d+\.\d+$", version) - - -@pytest.mark.parametrize("feature", features.modules) -def test_check_modules(feature: str) -> None: - assert features.check_module(feature) in [True, False] - - -@pytest.mark.parametrize("feature", features.codecs) -def test_check_codecs(feature: str) -> None: - assert features.check_codec(feature) in [True, False] - - -def test_check_warns_on_nonexistent() -> None: - with pytest.warns(UserWarning) as cm: - has_feature = features.check("typo") - assert has_feature is False - assert str(cm[-1].message) == "Unknown feature 'typo'." - - -def test_supported_modules() -> None: - assert isinstance(features.get_supported_modules(), list) - assert isinstance(features.get_supported_codecs(), list) - assert isinstance(features.get_supported_features(), list) - assert isinstance(features.get_supported(), list) - - -def test_unsupported_codec() -> None: - # Arrange - codec = "unsupported_codec" - # Act / Assert - with pytest.raises(ValueError): - features.check_codec(codec) - with pytest.raises(ValueError): - features.version_codec(codec) - - -def test_unsupported_module() -> None: - # Arrange - module = "unsupported_module" - # Act / Assert - with pytest.raises(ValueError): - features.check_module(module) - with pytest.raises(ValueError): - features.version_module(module) - - -@pytest.mark.parametrize("supported_formats", (True, False)) -def test_pilinfo(supported_formats: bool) -> None: - buf = io.StringIO() - features.pilinfo(buf, supported_formats=supported_formats) - out = buf.getvalue() - lines = out.splitlines() - assert lines[0] == "-" * 68 - assert lines[1].startswith("Pillow ") - assert lines[2].startswith("Python ") - lines = lines[3:] - while lines[0].startswith(" "): - lines = lines[1:] - assert lines[0] == "-" * 68 - assert lines[1].startswith("Python executable is") - lines = lines[2:] - if lines[0].startswith("Environment Python files loaded from"): - lines = lines[1:] - assert lines[0].startswith("System Python files loaded from") - assert lines[1] == "-" * 68 - assert lines[2].startswith("Python Pillow modules loaded from ") - assert lines[3].startswith("Binary Pillow modules loaded from ") - assert lines[4] == "-" * 68 - jpeg = ( - "\n" - + "-" * 68 - + "\n" - + "JPEG image/jpeg\n" - + "Extensions: .jfif, .jpe, .jpeg, .jpg\n" - + "Features: open, save\n" - + "-" * 68 - + "\n" - ) - assert supported_formats == (jpeg in out) +from __future__ import annotations + +import io +import re +from typing import Callable + +import pytest + +from PIL import features + +from .helper import skip_unless_feature + +try: + from PIL import _webp +except ImportError: + pass + + +def test_check() -> None: + # Check the correctness of the convenience function + for module in features.modules: + assert features.check_module(module) == features.check(module) + for codec in features.codecs: + assert features.check_codec(codec) == features.check(codec) + for feature in features.features: + assert features.check_feature(feature) == features.check(feature) + + +def test_version() -> None: + # Check the correctness of the convenience function + # and the format of version numbers + + def test(name: str, function: Callable[[str], str | None]) -> None: + version = features.version(name) + if not features.check(name): + assert version is 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) + assert version is None or re.search(r"\d+(\.\d+)*$", version) + + for module in features.modules: + test(module, features.version_module) + for codec in features.codecs: + test(codec, features.version_codec) + for feature in features.features: + test(feature, features.version_feature) + + +@skip_unless_feature("webp") +def test_webp_transparency() -> None: + assert features.check("transp_webp") != _webp.WebPDecoderBuggyAlpha() + assert features.check("transp_webp") == _webp.HAVE_TRANSPARENCY + + +@skip_unless_feature("webp") +def test_webp_mux() -> None: + assert features.check("webp_mux") == _webp.HAVE_WEBPMUX + + +@skip_unless_feature("webp") +def test_webp_anim() -> None: + assert features.check("webp_anim") == _webp.HAVE_WEBPANIM + + +@skip_unless_feature("libjpeg_turbo") +def test_libjpeg_turbo_version() -> None: + version = features.version("libjpeg_turbo") + assert version is not None + assert re.search(r"\d+\.\d+\.\d+$", version) + + +@skip_unless_feature("libimagequant") +def test_libimagequant_version() -> None: + version = features.version("libimagequant") + assert version is not None + assert re.search(r"\d+\.\d+\.\d+$", version) + + +@pytest.mark.parametrize("feature", features.modules) +def test_check_modules(feature: str) -> None: + assert features.check_module(feature) in [True, False] + + +@pytest.mark.parametrize("feature", features.codecs) +def test_check_codecs(feature: str) -> None: + assert features.check_codec(feature) in [True, False] + + +def test_check_warns_on_nonexistent() -> None: + with pytest.warns(UserWarning) as cm: + has_feature = features.check("typo") + assert has_feature is False + assert str(cm[-1].message) == "Unknown feature 'typo'." + + +def test_supported_modules() -> None: + assert isinstance(features.get_supported_modules(), list) + assert isinstance(features.get_supported_codecs(), list) + assert isinstance(features.get_supported_features(), list) + assert isinstance(features.get_supported(), list) + + +def test_unsupported_codec() -> None: + # Arrange + codec = "unsupported_codec" + # Act / Assert + with pytest.raises(ValueError): + features.check_codec(codec) + with pytest.raises(ValueError): + features.version_codec(codec) + + +def test_unsupported_module() -> None: + # Arrange + module = "unsupported_module" + # Act / Assert + with pytest.raises(ValueError): + features.check_module(module) + with pytest.raises(ValueError): + features.version_module(module) + + +@pytest.mark.parametrize("supported_formats", (True, False)) +def test_pilinfo(supported_formats: bool) -> None: + buf = io.StringIO() + features.pilinfo(buf, supported_formats=supported_formats) + out = buf.getvalue() + lines = out.splitlines() + assert lines[0] == "-" * 68 + assert lines[1].startswith("Pillow ") + assert lines[2].startswith("Python ") + lines = lines[3:] + while lines[0].startswith(" "): + lines = lines[1:] + assert lines[0] == "-" * 68 + assert lines[1].startswith("Python executable is") + lines = lines[2:] + if lines[0].startswith("Environment Python files loaded from"): + lines = lines[1:] + assert lines[0].startswith("System Python files loaded from") + assert lines[1] == "-" * 68 + assert lines[2].startswith("Python Pillow modules loaded from ") + assert lines[3].startswith("Binary Pillow modules loaded from ") + assert lines[4] == "-" * 68 + jpeg = ( + "\n" + + "-" * 68 + + "\n" + + "JPEG image/jpeg\n" + + "Extensions: .jfif, .jpe, .jpeg, .jpg\n" + + "Features: open, save\n" + + "-" * 68 + + "\n" + ) + assert supported_formats == (jpeg in out) diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index 1b393a3ff..c6fd840db 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -1,715 +1,715 @@ -from __future__ import annotations - -from pathlib import Path - -import pytest - -from PIL import Image, ImageSequence, PngImagePlugin - - -# APNG browser support tests and fixtures via: -# https://philip.html5.org/tests/apng/tests.html -# (referenced from https://wiki.mozilla.org/APNG_Specification) -def test_apng_basic() -> None: - with Image.open("Tests/images/apng/single_frame.png") as im: - assert not im.is_animated - assert im.n_frames == 1 - assert im.get_format_mimetype() == "image/apng" - assert im.info.get("default_image") is None - assert im.getpixel((0, 0)) == (0, 255, 0, 255) - assert im.getpixel((64, 32)) == (0, 255, 0, 255) - - with Image.open("Tests/images/apng/single_frame_default.png") as im: - assert im.is_animated - assert im.n_frames == 2 - assert im.get_format_mimetype() == "image/apng" - assert im.info.get("default_image") - assert im.getpixel((0, 0)) == (255, 0, 0, 255) - assert im.getpixel((64, 32)) == (255, 0, 0, 255) - im.seek(1) - assert im.getpixel((0, 0)) == (0, 255, 0, 255) - assert im.getpixel((64, 32)) == (0, 255, 0, 255) - - # test out of bounds seek - with pytest.raises(EOFError): - im.seek(2) - - # test rewind support - im.seek(0) - assert im.getpixel((0, 0)) == (255, 0, 0, 255) - assert im.getpixel((64, 32)) == (255, 0, 0, 255) - im.seek(1) - assert im.getpixel((0, 0)) == (0, 255, 0, 255) - assert im.getpixel((64, 32)) == (0, 255, 0, 255) - - -@pytest.mark.parametrize( - "filename", - ("Tests/images/apng/split_fdat.png", "Tests/images/apng/split_fdat_zero_chunk.png"), -) -def test_apng_fdat(filename: str) -> None: - with Image.open(filename) as im: - im.seek(im.n_frames - 1) - assert im.getpixel((0, 0)) == (0, 255, 0, 255) - assert im.getpixel((64, 32)) == (0, 255, 0, 255) - - -def test_apng_dispose() -> None: - with Image.open("Tests/images/apng/dispose_op_none.png") as im: - im.seek(im.n_frames - 1) - assert im.getpixel((0, 0)) == (0, 255, 0, 255) - assert im.getpixel((64, 32)) == (0, 255, 0, 255) - - with Image.open("Tests/images/apng/dispose_op_background.png") as im: - im.seek(im.n_frames - 1) - assert im.getpixel((0, 0)) == (0, 0, 0, 0) - assert im.getpixel((64, 32)) == (0, 0, 0, 0) - - with Image.open("Tests/images/apng/dispose_op_background_final.png") as im: - im.seek(im.n_frames - 1) - assert im.getpixel((0, 0)) == (0, 255, 0, 255) - assert im.getpixel((64, 32)) == (0, 255, 0, 255) - - with Image.open("Tests/images/apng/dispose_op_previous.png") as im: - im.seek(im.n_frames - 1) - assert im.getpixel((0, 0)) == (0, 255, 0, 255) - assert im.getpixel((64, 32)) == (0, 255, 0, 255) - - with Image.open("Tests/images/apng/dispose_op_previous_final.png") as im: - im.seek(im.n_frames - 1) - assert im.getpixel((0, 0)) == (0, 255, 0, 255) - assert im.getpixel((64, 32)) == (0, 255, 0, 255) - - with Image.open("Tests/images/apng/dispose_op_previous_first.png") as im: - im.seek(im.n_frames - 1) - assert im.getpixel((0, 0)) == (0, 0, 0, 0) - assert im.getpixel((64, 32)) == (0, 0, 0, 0) - - -def test_apng_dispose_region() -> None: - with Image.open("Tests/images/apng/dispose_op_none_region.png") as im: - im.seek(im.n_frames - 1) - assert im.getpixel((0, 0)) == (0, 255, 0, 255) - assert im.getpixel((64, 32)) == (0, 255, 0, 255) - - with Image.open("Tests/images/apng/dispose_op_background_before_region.png") as im: - im.seek(im.n_frames - 1) - assert im.getpixel((0, 0)) == (0, 0, 0, 0) - assert im.getpixel((64, 32)) == (0, 0, 0, 0) - - with Image.open("Tests/images/apng/dispose_op_background_region.png") as im: - im.seek(im.n_frames - 1) - assert im.getpixel((0, 0)) == (0, 0, 255, 255) - assert im.getpixel((64, 32)) == (0, 0, 0, 0) - - with Image.open("Tests/images/apng/dispose_op_previous_region.png") as im: - im.seek(im.n_frames - 1) - assert im.getpixel((0, 0)) == (0, 255, 0, 255) - assert im.getpixel((64, 32)) == (0, 255, 0, 255) - - -def test_apng_dispose_op_previous_frame() -> None: - # Test that the dispose settings being used are from the previous frame - # - # Image created with: - # red = Image.new("RGBA", (128, 64), (255, 0, 0, 255)) - # green = red.copy() - # green.paste(Image.new("RGBA", (64, 32), (0, 255, 0, 255))) - # blue = red.copy() - # blue.paste(Image.new("RGBA", (64, 32), (0, 255, 0, 255)), (64, 32)) - # - # red.save( - # "Tests/images/apng/dispose_op_previous_frame.png", - # save_all=True, - # append_images=[green, blue], - # disposal=[ - # PngImagePlugin.Disposal.OP_NONE, - # PngImagePlugin.Disposal.OP_PREVIOUS, - # PngImagePlugin.Disposal.OP_PREVIOUS - # ], - # ) - with Image.open("Tests/images/apng/dispose_op_previous_frame.png") as im: - im.seek(im.n_frames - 1) - assert im.getpixel((0, 0)) == (255, 0, 0, 255) - - -def test_apng_dispose_op_background_p_mode() -> None: - with Image.open("Tests/images/apng/dispose_op_background_p_mode.png") as im: - im.seek(1) - im.load() - assert im.size == (128, 64) - - -def test_apng_blend() -> None: - with Image.open("Tests/images/apng/blend_op_source_solid.png") as im: - im.seek(im.n_frames - 1) - assert im.getpixel((0, 0)) == (0, 255, 0, 255) - assert im.getpixel((64, 32)) == (0, 255, 0, 255) - - with Image.open("Tests/images/apng/blend_op_source_transparent.png") as im: - im.seek(im.n_frames - 1) - assert im.getpixel((0, 0)) == (0, 0, 0, 0) - assert im.getpixel((64, 32)) == (0, 0, 0, 0) - - with Image.open("Tests/images/apng/blend_op_source_near_transparent.png") as im: - im.seek(im.n_frames - 1) - assert im.getpixel((0, 0)) == (0, 255, 0, 2) - assert im.getpixel((64, 32)) == (0, 255, 0, 2) - - with Image.open("Tests/images/apng/blend_op_over.png") as im: - im.seek(im.n_frames - 1) - assert im.getpixel((0, 0)) == (0, 255, 0, 255) - assert im.getpixel((64, 32)) == (0, 255, 0, 255) - - with Image.open("Tests/images/apng/blend_op_over_near_transparent.png") as im: - im.seek(im.n_frames - 1) - assert im.getpixel((0, 0)) == (0, 255, 0, 97) - assert im.getpixel((64, 32)) == (0, 255, 0, 255) - - -def test_apng_blend_transparency() -> None: - with Image.open("Tests/images/blend_transparency.png") as im: - im.seek(1) - assert im.getpixel((0, 0)) == (255, 0, 0) - - -def test_apng_chunk_order() -> None: - with Image.open("Tests/images/apng/fctl_actl.png") as im: - im.seek(im.n_frames - 1) - assert im.getpixel((0, 0)) == (0, 255, 0, 255) - assert im.getpixel((64, 32)) == (0, 255, 0, 255) - - -def test_apng_delay() -> None: - with Image.open("Tests/images/apng/delay.png") as im: - im.seek(1) - assert im.info.get("duration") == 500.0 - im.seek(2) - assert im.info.get("duration") == 1000.0 - im.seek(3) - assert im.info.get("duration") == 500.0 - im.seek(4) - assert im.info.get("duration") == 1000.0 - - with Image.open("Tests/images/apng/delay_round.png") as im: - im.seek(1) - assert im.info.get("duration") == 500.0 - im.seek(2) - assert im.info.get("duration") == 1000.0 - - with Image.open("Tests/images/apng/delay_short_max.png") as im: - im.seek(1) - assert im.info.get("duration") == 500.0 - im.seek(2) - assert im.info.get("duration") == 1000.0 - - with Image.open("Tests/images/apng/delay_zero_denom.png") as im: - im.seek(1) - assert im.info.get("duration") == 500.0 - im.seek(2) - assert im.info.get("duration") == 1000.0 - - with Image.open("Tests/images/apng/delay_zero_numer.png") as im: - im.seek(1) - assert im.info.get("duration") == 0.0 - im.seek(2) - assert im.info.get("duration") == 0.0 - im.seek(3) - assert im.info.get("duration") == 500.0 - im.seek(4) - assert im.info.get("duration") == 1000.0 - - -def test_apng_num_plays() -> None: - with Image.open("Tests/images/apng/num_plays.png") as im: - assert im.info.get("loop") == 0 - - with Image.open("Tests/images/apng/num_plays_1.png") as im: - assert im.info.get("loop") == 1 - - -def test_apng_mode() -> None: - with Image.open("Tests/images/apng/mode_16bit.png") as im: - assert im.mode == "RGBA" - im.seek(im.n_frames - 1) - assert im.getpixel((0, 0)) == (0, 0, 128, 191) - assert im.getpixel((64, 32)) == (0, 0, 128, 191) - - with Image.open("Tests/images/apng/mode_grayscale.png") as im: - assert im.mode == "L" - im.seek(im.n_frames - 1) - assert im.getpixel((0, 0)) == 128 - assert im.getpixel((64, 32)) == 255 - - with Image.open("Tests/images/apng/mode_grayscale_alpha.png") as im: - assert im.mode == "LA" - im.seek(im.n_frames - 1) - assert im.getpixel((0, 0)) == (128, 191) - assert im.getpixel((64, 32)) == (128, 191) - - with Image.open("Tests/images/apng/mode_palette.png") as im: - assert im.mode == "P" - im.seek(im.n_frames - 1) - im = im.convert("RGB") - assert im.getpixel((0, 0)) == (0, 255, 0) - assert im.getpixel((64, 32)) == (0, 255, 0) - - with Image.open("Tests/images/apng/mode_palette_alpha.png") as im: - assert im.mode == "P" - im.seek(im.n_frames - 1) - im = im.convert("RGBA") - assert im.getpixel((0, 0)) == (255, 0, 0, 0) - assert im.getpixel((64, 32)) == (255, 0, 0, 0) - - with Image.open("Tests/images/apng/mode_palette_1bit_alpha.png") as im: - assert im.mode == "P" - im.seek(im.n_frames - 1) - im = im.convert("RGBA") - assert im.getpixel((0, 0)) == (0, 0, 255, 128) - assert im.getpixel((64, 32)) == (0, 0, 255, 128) - - -def test_apng_chunk_errors() -> None: - with Image.open("Tests/images/apng/chunk_no_actl.png") as im: - 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 Image.open("Tests/images/apng/chunk_actl_after_idat.png") as im: - assert not im.is_animated - - with Image.open("Tests/images/apng/chunk_no_fctl.png") as im: - with pytest.raises(SyntaxError): - im.seek(im.n_frames - 1) - - with Image.open("Tests/images/apng/chunk_repeat_fctl.png") as im: - with pytest.raises(SyntaxError): - im.seek(im.n_frames - 1) - - with Image.open("Tests/images/apng/chunk_no_fdat.png") as im: - with pytest.raises(SyntaxError): - im.seek(im.n_frames - 1) - - -def test_apng_syntax_errors() -> None: - with pytest.warns(UserWarning): - with Image.open("Tests/images/apng/syntax_num_frames_zero.png") as im: - assert not im.is_animated - with pytest.raises(OSError): - im.load() - - with pytest.warns(UserWarning): - with Image.open("Tests/images/apng/syntax_num_frames_zero_default.png") as im: - assert not im.is_animated - im.load() - - # we can handle this case gracefully - exception = None - with Image.open("Tests/images/apng/syntax_num_frames_low.png") as im: - try: - im.seek(im.n_frames - 1) - except Exception as e: - exception = e - assert exception is None - - with pytest.raises(OSError): - with Image.open("Tests/images/apng/syntax_num_frames_high.png") as im: - im.seek(im.n_frames - 1) - im.load() - - with pytest.warns(UserWarning): - with Image.open("Tests/images/apng/syntax_num_frames_invalid.png") as im: - assert not im.is_animated - im.load() - - -@pytest.mark.parametrize( - "test_file", - ( - "sequence_start.png", - "sequence_gap.png", - "sequence_repeat.png", - "sequence_repeat_chunk.png", - "sequence_reorder.png", - "sequence_reorder_chunk.png", - "sequence_fdat_fctl.png", - ), -) -def test_apng_sequence_errors(test_file: str) -> None: - with pytest.raises(SyntaxError): - with Image.open(f"Tests/images/apng/{test_file}") as im: - im.seek(im.n_frames - 1) - im.load() - - -def test_apng_save(tmp_path: Path) -> None: - with Image.open("Tests/images/apng/single_frame.png") as im: - test_file = str(tmp_path / "temp.png") - im.save(test_file, save_all=True) - - with Image.open(test_file) as im: - im.load() - assert not im.is_animated - assert im.n_frames == 1 - assert im.get_format_mimetype() == "image/png" - assert im.info.get("default_image") is None - assert im.getpixel((0, 0)) == (0, 255, 0, 255) - assert im.getpixel((64, 32)) == (0, 255, 0, 255) - - with Image.open("Tests/images/apng/single_frame_default.png") as im: - frames = [frame_im.copy() for frame_im in ImageSequence.Iterator(im)] - frames[0].save( - test_file, save_all=True, default_image=True, append_images=frames[1:] - ) - - with Image.open(test_file) as im: - im.load() - assert im.is_animated - assert im.n_frames == 2 - assert im.get_format_mimetype() == "image/apng" - assert im.info.get("default_image") - im.seek(1) - assert im.getpixel((0, 0)) == (0, 255, 0, 255) - assert im.getpixel((64, 32)) == (0, 255, 0, 255) - - -def test_apng_save_alpha(tmp_path: Path) -> None: - test_file = str(tmp_path / "temp.png") - - im = Image.new("RGBA", (1, 1), (255, 0, 0, 255)) - im2 = Image.new("RGBA", (1, 1), (255, 0, 0, 127)) - im.save(test_file, save_all=True, append_images=[im2]) - - with Image.open(test_file) as reloaded: - assert reloaded.getpixel((0, 0)) == (255, 0, 0, 255) - - reloaded.seek(1) - assert reloaded.getpixel((0, 0)) == (255, 0, 0, 127) - - -def test_apng_save_split_fdat(tmp_path: Path) -> None: - # test to make sure we do not generate sequence errors when writing - # frames with image data spanning multiple fdAT chunks (in this case - # both the default image and first animation frame will span multiple - # data chunks) - test_file = str(tmp_path / "temp.png") - with Image.open("Tests/images/old-style-jpeg-compression.png") as im: - frames = [im.copy(), Image.new("RGBA", im.size, (255, 0, 0, 255))] - im.save( - test_file, - save_all=True, - default_image=True, - append_images=frames, - ) - with Image.open(test_file) as im: - exception = None - try: - im.seek(im.n_frames - 1) - im.load() - except Exception as e: - exception = e - assert exception is None - - -def test_apng_save_duration_loop(tmp_path: Path) -> None: - test_file = str(tmp_path / "temp.png") - with Image.open("Tests/images/apng/delay.png") as im: - frames = [] - durations = [] - loop = im.info.get("loop") - default_image = im.info.get("default_image") - for i, frame_im in enumerate(ImageSequence.Iterator(im)): - frames.append(frame_im.copy()) - if i != 0 or not default_image: - durations.append(frame_im.info.get("duration", 0)) - frames[0].save( - test_file, - save_all=True, - default_image=default_image, - append_images=frames[1:], - duration=durations, - loop=loop, - ) - - with Image.open(test_file) as im: - im.load() - assert im.info.get("loop") == loop - im.seek(1) - assert im.info.get("duration") == 500.0 - im.seek(2) - assert im.info.get("duration") == 1000.0 - im.seek(3) - assert im.info.get("duration") == 500.0 - im.seek(4) - assert im.info.get("duration") == 1000.0 - - # test removal of duplicated frames - frame = Image.new("RGBA", (128, 64), (255, 0, 0, 255)) - frame.save( - test_file, save_all=True, append_images=[frame, frame], duration=[500, 100, 150] - ) - with Image.open(test_file) as im: - assert im.n_frames == 1 - assert "duration" not in im.info - - different_frame = Image.new("RGBA", (128, 64)) - frame.save( - test_file, - save_all=True, - append_images=[frame, different_frame], - duration=[500, 100, 150], - ) - with Image.open(test_file) as im: - assert im.n_frames == 2 - assert im.info["duration"] == 600 - - im.seek(1) - assert im.info["duration"] == 150 - - # test info duration - frame.info["duration"] = 300 - frame.save(test_file, save_all=True, append_images=[frame, different_frame]) - with Image.open(test_file) as im: - 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") - size = (128, 64) - red = Image.new("RGBA", size, (255, 0, 0, 255)) - green = Image.new("RGBA", size, (0, 255, 0, 255)) - transparent = Image.new("RGBA", size, (0, 0, 0, 0)) - - # test OP_NONE - red.save( - test_file, - save_all=True, - append_images=[green, transparent], - disposal=PngImagePlugin.Disposal.OP_NONE, - blend=PngImagePlugin.Blend.OP_OVER, - ) - with Image.open(test_file) as im: - im.seek(2) - assert im.getpixel((0, 0)) == (0, 255, 0, 255) - assert im.getpixel((64, 32)) == (0, 255, 0, 255) - - # test OP_BACKGROUND - disposal = [ - PngImagePlugin.Disposal.OP_NONE, - PngImagePlugin.Disposal.OP_BACKGROUND, - PngImagePlugin.Disposal.OP_NONE, - ] - red.save( - test_file, - save_all=True, - append_images=[red, transparent], - disposal=disposal, - blend=PngImagePlugin.Blend.OP_OVER, - ) - with Image.open(test_file) as im: - im.seek(2) - assert im.getpixel((0, 0)) == (0, 0, 0, 0) - assert im.getpixel((64, 32)) == (0, 0, 0, 0) - - disposal = [ - PngImagePlugin.Disposal.OP_NONE, - PngImagePlugin.Disposal.OP_BACKGROUND, - ] - red.save( - test_file, - save_all=True, - append_images=[green], - disposal=disposal, - blend=PngImagePlugin.Blend.OP_OVER, - ) - with Image.open(test_file) as im: - im.seek(1) - assert im.getpixel((0, 0)) == (0, 255, 0, 255) - assert im.getpixel((64, 32)) == (0, 255, 0, 255) - - # test OP_PREVIOUS - disposal = [ - PngImagePlugin.Disposal.OP_NONE, - PngImagePlugin.Disposal.OP_PREVIOUS, - PngImagePlugin.Disposal.OP_NONE, - ] - red.save( - test_file, - save_all=True, - append_images=[green, red, transparent], - default_image=True, - disposal=disposal, - blend=PngImagePlugin.Blend.OP_OVER, - ) - with Image.open(test_file) as im: - im.seek(3) - assert im.getpixel((0, 0)) == (0, 255, 0, 255) - assert im.getpixel((64, 32)) == (0, 255, 0, 255) - - disposal = [ - PngImagePlugin.Disposal.OP_NONE, - PngImagePlugin.Disposal.OP_PREVIOUS, - ] - red.save( - test_file, - save_all=True, - append_images=[green], - disposal=disposal, - blend=PngImagePlugin.Blend.OP_OVER, - ) - with Image.open(test_file) as im: - im.seek(1) - assert im.getpixel((0, 0)) == (0, 255, 0, 255) - assert im.getpixel((64, 32)) == (0, 255, 0, 255) - - # test info disposal - red.info["disposal"] = PngImagePlugin.Disposal.OP_BACKGROUND - red.save( - test_file, - save_all=True, - append_images=[Image.new("RGBA", (10, 10), (0, 255, 0, 255))], - ) - with Image.open(test_file) as im: - im.seek(1) - assert im.getpixel((64, 32)) == (0, 0, 0, 0) - - -def test_apng_save_disposal_previous(tmp_path: Path) -> None: - test_file = str(tmp_path / "temp.png") - size = (128, 64) - blue = Image.new("RGBA", size, (0, 0, 255, 255)) - red = Image.new("RGBA", size, (255, 0, 0, 255)) - green = Image.new("RGBA", size, (0, 255, 0, 255)) - - # test OP_NONE - blue.save( - test_file, - save_all=True, - append_images=[red, green], - disposal=PngImagePlugin.Disposal.OP_PREVIOUS, - ) - with Image.open(test_file) as im: - assert im.getpixel((0, 0)) == (0, 0, 255, 255) - - im.seek(2) - assert im.getpixel((0, 0)) == (0, 255, 0, 255) - assert im.getpixel((64, 32)) == (0, 255, 0, 255) - - -def test_apng_save_blend(tmp_path: Path) -> None: - test_file = str(tmp_path / "temp.png") - size = (128, 64) - red = Image.new("RGBA", size, (255, 0, 0, 255)) - green = Image.new("RGBA", size, (0, 255, 0, 255)) - transparent = Image.new("RGBA", size, (0, 0, 0, 0)) - - # test OP_SOURCE on solid color - blend = [ - PngImagePlugin.Blend.OP_OVER, - PngImagePlugin.Blend.OP_SOURCE, - ] - red.save( - test_file, - save_all=True, - append_images=[red, green], - default_image=True, - disposal=PngImagePlugin.Disposal.OP_NONE, - blend=blend, - ) - with Image.open(test_file) as im: - im.seek(2) - assert im.getpixel((0, 0)) == (0, 255, 0, 255) - assert im.getpixel((64, 32)) == (0, 255, 0, 255) - - # test OP_SOURCE on transparent color - blend = [ - PngImagePlugin.Blend.OP_OVER, - PngImagePlugin.Blend.OP_SOURCE, - ] - red.save( - test_file, - save_all=True, - append_images=[red, transparent], - default_image=True, - disposal=PngImagePlugin.Disposal.OP_NONE, - blend=blend, - ) - with Image.open(test_file) as im: - im.seek(2) - assert im.getpixel((0, 0)) == (0, 0, 0, 0) - assert im.getpixel((64, 32)) == (0, 0, 0, 0) - - # test OP_OVER - red.save( - test_file, - save_all=True, - append_images=[green, transparent], - default_image=True, - disposal=PngImagePlugin.Disposal.OP_NONE, - blend=PngImagePlugin.Blend.OP_OVER, - ) - with Image.open(test_file) as im: - im.seek(1) - assert im.getpixel((0, 0)) == (0, 255, 0, 255) - assert im.getpixel((64, 32)) == (0, 255, 0, 255) - im.seek(2) - assert im.getpixel((0, 0)) == (0, 255, 0, 255) - assert im.getpixel((64, 32)) == (0, 255, 0, 255) - - # test info blend - red.info["blend"] = PngImagePlugin.Blend.OP_OVER - red.save(test_file, save_all=True, append_images=[green, transparent]) - with Image.open(test_file) as im: - im.seek(2) - assert im.getpixel((0, 0)) == (0, 255, 0, 255) - - -def test_apng_save_size(tmp_path: Path) -> None: - test_file = str(tmp_path / "temp.png") - - im = Image.new("L", (100, 100)) - im.save(test_file, save_all=True, append_images=[Image.new("L", (200, 200))]) - - with Image.open(test_file) as reloaded: - assert reloaded.size == (200, 200) - - -def test_seek_after_close() -> None: - im = Image.open("Tests/images/apng/delay.png") - im.seek(1) - im.close() - - with pytest.raises(ValueError): - im.seek(0) - - -@pytest.mark.parametrize("mode", ("RGBA", "RGB", "P")) -@pytest.mark.parametrize("default_image", (True, False)) -@pytest.mark.parametrize("duplicate", (True, False)) -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") - - im = Image.new("L", (1, 1)) - im.save( - test_file, - save_all=True, - default_image=default_image, - append_images=[im.convert(mode) if duplicate else Image.new(mode, (1, 1), 1)], - ) - with Image.open(test_file) as reloaded: - assert reloaded.mode == mode - - -def test_apng_repeated_seeks_give_correct_info() -> None: - with Image.open("Tests/images/apng/different_durations.png") as im: - for i in range(3): - im.seek(0) - assert im.info["duration"] == 4000 - im.seek(1) - assert im.info["duration"] == 1000 +from __future__ import annotations + +from pathlib import Path + +import pytest + +from PIL import Image, ImageSequence, PngImagePlugin + + +# APNG browser support tests and fixtures via: +# https://philip.html5.org/tests/apng/tests.html +# (referenced from https://wiki.mozilla.org/APNG_Specification) +def test_apng_basic() -> None: + with Image.open("Tests/images/apng/single_frame.png") as im: + assert not im.is_animated + assert im.n_frames == 1 + assert im.get_format_mimetype() == "image/apng" + assert im.info.get("default_image") is None + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + with Image.open("Tests/images/apng/single_frame_default.png") as im: + assert im.is_animated + assert im.n_frames == 2 + assert im.get_format_mimetype() == "image/apng" + assert im.info.get("default_image") + assert im.getpixel((0, 0)) == (255, 0, 0, 255) + assert im.getpixel((64, 32)) == (255, 0, 0, 255) + im.seek(1) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + # test out of bounds seek + with pytest.raises(EOFError): + im.seek(2) + + # test rewind support + im.seek(0) + assert im.getpixel((0, 0)) == (255, 0, 0, 255) + assert im.getpixel((64, 32)) == (255, 0, 0, 255) + im.seek(1) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + +@pytest.mark.parametrize( + "filename", + ("Tests/images/apng/split_fdat.png", "Tests/images/apng/split_fdat_zero_chunk.png"), +) +def test_apng_fdat(filename: str) -> None: + with Image.open(filename) as im: + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + +def test_apng_dispose() -> None: + with Image.open("Tests/images/apng/dispose_op_none.png") as im: + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + with Image.open("Tests/images/apng/dispose_op_background.png") as im: + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (0, 0, 0, 0) + assert im.getpixel((64, 32)) == (0, 0, 0, 0) + + with Image.open("Tests/images/apng/dispose_op_background_final.png") as im: + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + with Image.open("Tests/images/apng/dispose_op_previous.png") as im: + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + with Image.open("Tests/images/apng/dispose_op_previous_final.png") as im: + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + with Image.open("Tests/images/apng/dispose_op_previous_first.png") as im: + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (0, 0, 0, 0) + assert im.getpixel((64, 32)) == (0, 0, 0, 0) + + +def test_apng_dispose_region() -> None: + with Image.open("Tests/images/apng/dispose_op_none_region.png") as im: + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + with Image.open("Tests/images/apng/dispose_op_background_before_region.png") as im: + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (0, 0, 0, 0) + assert im.getpixel((64, 32)) == (0, 0, 0, 0) + + with Image.open("Tests/images/apng/dispose_op_background_region.png") as im: + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (0, 0, 255, 255) + assert im.getpixel((64, 32)) == (0, 0, 0, 0) + + with Image.open("Tests/images/apng/dispose_op_previous_region.png") as im: + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + +def test_apng_dispose_op_previous_frame() -> None: + # Test that the dispose settings being used are from the previous frame + # + # Image created with: + # red = Image.new("RGBA", (128, 64), (255, 0, 0, 255)) + # green = red.copy() + # green.paste(Image.new("RGBA", (64, 32), (0, 255, 0, 255))) + # blue = red.copy() + # blue.paste(Image.new("RGBA", (64, 32), (0, 255, 0, 255)), (64, 32)) + # + # red.save( + # "Tests/images/apng/dispose_op_previous_frame.png", + # save_all=True, + # append_images=[green, blue], + # disposal=[ + # PngImagePlugin.Disposal.OP_NONE, + # PngImagePlugin.Disposal.OP_PREVIOUS, + # PngImagePlugin.Disposal.OP_PREVIOUS + # ], + # ) + with Image.open("Tests/images/apng/dispose_op_previous_frame.png") as im: + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (255, 0, 0, 255) + + +def test_apng_dispose_op_background_p_mode() -> None: + with Image.open("Tests/images/apng/dispose_op_background_p_mode.png") as im: + im.seek(1) + im.load() + assert im.size == (128, 64) + + +def test_apng_blend() -> None: + with Image.open("Tests/images/apng/blend_op_source_solid.png") as im: + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + with Image.open("Tests/images/apng/blend_op_source_transparent.png") as im: + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (0, 0, 0, 0) + assert im.getpixel((64, 32)) == (0, 0, 0, 0) + + with Image.open("Tests/images/apng/blend_op_source_near_transparent.png") as im: + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (0, 255, 0, 2) + assert im.getpixel((64, 32)) == (0, 255, 0, 2) + + with Image.open("Tests/images/apng/blend_op_over.png") as im: + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + with Image.open("Tests/images/apng/blend_op_over_near_transparent.png") as im: + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (0, 255, 0, 97) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + +def test_apng_blend_transparency() -> None: + with Image.open("Tests/images/blend_transparency.png") as im: + im.seek(1) + assert im.getpixel((0, 0)) == (255, 0, 0) + + +def test_apng_chunk_order() -> None: + with Image.open("Tests/images/apng/fctl_actl.png") as im: + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + +def test_apng_delay() -> None: + with Image.open("Tests/images/apng/delay.png") as im: + im.seek(1) + assert im.info.get("duration") == 500.0 + im.seek(2) + assert im.info.get("duration") == 1000.0 + im.seek(3) + assert im.info.get("duration") == 500.0 + im.seek(4) + assert im.info.get("duration") == 1000.0 + + with Image.open("Tests/images/apng/delay_round.png") as im: + im.seek(1) + assert im.info.get("duration") == 500.0 + im.seek(2) + assert im.info.get("duration") == 1000.0 + + with Image.open("Tests/images/apng/delay_short_max.png") as im: + im.seek(1) + assert im.info.get("duration") == 500.0 + im.seek(2) + assert im.info.get("duration") == 1000.0 + + with Image.open("Tests/images/apng/delay_zero_denom.png") as im: + im.seek(1) + assert im.info.get("duration") == 500.0 + im.seek(2) + assert im.info.get("duration") == 1000.0 + + with Image.open("Tests/images/apng/delay_zero_numer.png") as im: + im.seek(1) + assert im.info.get("duration") == 0.0 + im.seek(2) + assert im.info.get("duration") == 0.0 + im.seek(3) + assert im.info.get("duration") == 500.0 + im.seek(4) + assert im.info.get("duration") == 1000.0 + + +def test_apng_num_plays() -> None: + with Image.open("Tests/images/apng/num_plays.png") as im: + assert im.info.get("loop") == 0 + + with Image.open("Tests/images/apng/num_plays_1.png") as im: + assert im.info.get("loop") == 1 + + +def test_apng_mode() -> None: + with Image.open("Tests/images/apng/mode_16bit.png") as im: + assert im.mode == "RGBA" + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (0, 0, 128, 191) + assert im.getpixel((64, 32)) == (0, 0, 128, 191) + + with Image.open("Tests/images/apng/mode_grayscale.png") as im: + assert im.mode == "L" + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == 128 + assert im.getpixel((64, 32)) == 255 + + with Image.open("Tests/images/apng/mode_grayscale_alpha.png") as im: + assert im.mode == "LA" + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (128, 191) + assert im.getpixel((64, 32)) == (128, 191) + + with Image.open("Tests/images/apng/mode_palette.png") as im: + assert im.mode == "P" + im.seek(im.n_frames - 1) + im = im.convert("RGB") + assert im.getpixel((0, 0)) == (0, 255, 0) + assert im.getpixel((64, 32)) == (0, 255, 0) + + with Image.open("Tests/images/apng/mode_palette_alpha.png") as im: + assert im.mode == "P" + im.seek(im.n_frames - 1) + im = im.convert("RGBA") + assert im.getpixel((0, 0)) == (255, 0, 0, 0) + assert im.getpixel((64, 32)) == (255, 0, 0, 0) + + with Image.open("Tests/images/apng/mode_palette_1bit_alpha.png") as im: + assert im.mode == "P" + im.seek(im.n_frames - 1) + im = im.convert("RGBA") + assert im.getpixel((0, 0)) == (0, 0, 255, 128) + assert im.getpixel((64, 32)) == (0, 0, 255, 128) + + +def test_apng_chunk_errors() -> None: + with Image.open("Tests/images/apng/chunk_no_actl.png") as im: + 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 Image.open("Tests/images/apng/chunk_actl_after_idat.png") as im: + assert not im.is_animated + + with Image.open("Tests/images/apng/chunk_no_fctl.png") as im: + with pytest.raises(SyntaxError): + im.seek(im.n_frames - 1) + + with Image.open("Tests/images/apng/chunk_repeat_fctl.png") as im: + with pytest.raises(SyntaxError): + im.seek(im.n_frames - 1) + + with Image.open("Tests/images/apng/chunk_no_fdat.png") as im: + with pytest.raises(SyntaxError): + im.seek(im.n_frames - 1) + + +def test_apng_syntax_errors() -> None: + with pytest.warns(UserWarning): + with Image.open("Tests/images/apng/syntax_num_frames_zero.png") as im: + assert not im.is_animated + with pytest.raises(OSError): + im.load() + + with pytest.warns(UserWarning): + with Image.open("Tests/images/apng/syntax_num_frames_zero_default.png") as im: + assert not im.is_animated + im.load() + + # we can handle this case gracefully + exception = None + with Image.open("Tests/images/apng/syntax_num_frames_low.png") as im: + try: + im.seek(im.n_frames - 1) + except Exception as e: + exception = e + assert exception is None + + with pytest.raises(OSError): + with Image.open("Tests/images/apng/syntax_num_frames_high.png") as im: + im.seek(im.n_frames - 1) + im.load() + + with pytest.warns(UserWarning): + with Image.open("Tests/images/apng/syntax_num_frames_invalid.png") as im: + assert not im.is_animated + im.load() + + +@pytest.mark.parametrize( + "test_file", + ( + "sequence_start.png", + "sequence_gap.png", + "sequence_repeat.png", + "sequence_repeat_chunk.png", + "sequence_reorder.png", + "sequence_reorder_chunk.png", + "sequence_fdat_fctl.png", + ), +) +def test_apng_sequence_errors(test_file: str) -> None: + with pytest.raises(SyntaxError): + with Image.open(f"Tests/images/apng/{test_file}") as im: + im.seek(im.n_frames - 1) + im.load() + + +def test_apng_save(tmp_path: Path) -> None: + with Image.open("Tests/images/apng/single_frame.png") as im: + test_file = str(tmp_path / "temp.png") + im.save(test_file, save_all=True) + + with Image.open(test_file) as im: + im.load() + assert not im.is_animated + assert im.n_frames == 1 + assert im.get_format_mimetype() == "image/png" + assert im.info.get("default_image") is None + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + with Image.open("Tests/images/apng/single_frame_default.png") as im: + frames = [frame_im.copy() for frame_im in ImageSequence.Iterator(im)] + frames[0].save( + test_file, save_all=True, default_image=True, append_images=frames[1:] + ) + + with Image.open(test_file) as im: + im.load() + assert im.is_animated + assert im.n_frames == 2 + assert im.get_format_mimetype() == "image/apng" + assert im.info.get("default_image") + im.seek(1) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + +def test_apng_save_alpha(tmp_path: Path) -> None: + test_file = str(tmp_path / "temp.png") + + im = Image.new("RGBA", (1, 1), (255, 0, 0, 255)) + im2 = Image.new("RGBA", (1, 1), (255, 0, 0, 127)) + im.save(test_file, save_all=True, append_images=[im2]) + + with Image.open(test_file) as reloaded: + assert reloaded.getpixel((0, 0)) == (255, 0, 0, 255) + + reloaded.seek(1) + assert reloaded.getpixel((0, 0)) == (255, 0, 0, 127) + + +def test_apng_save_split_fdat(tmp_path: Path) -> None: + # test to make sure we do not generate sequence errors when writing + # frames with image data spanning multiple fdAT chunks (in this case + # both the default image and first animation frame will span multiple + # data chunks) + test_file = str(tmp_path / "temp.png") + with Image.open("Tests/images/old-style-jpeg-compression.png") as im: + frames = [im.copy(), Image.new("RGBA", im.size, (255, 0, 0, 255))] + im.save( + test_file, + save_all=True, + default_image=True, + append_images=frames, + ) + with Image.open(test_file) as im: + exception = None + try: + im.seek(im.n_frames - 1) + im.load() + except Exception as e: + exception = e + assert exception is None + + +def test_apng_save_duration_loop(tmp_path: Path) -> None: + test_file = str(tmp_path / "temp.png") + with Image.open("Tests/images/apng/delay.png") as im: + frames = [] + durations = [] + loop = im.info.get("loop") + default_image = im.info.get("default_image") + for i, frame_im in enumerate(ImageSequence.Iterator(im)): + frames.append(frame_im.copy()) + if i != 0 or not default_image: + durations.append(frame_im.info.get("duration", 0)) + frames[0].save( + test_file, + save_all=True, + default_image=default_image, + append_images=frames[1:], + duration=durations, + loop=loop, + ) + + with Image.open(test_file) as im: + im.load() + assert im.info.get("loop") == loop + im.seek(1) + assert im.info.get("duration") == 500.0 + im.seek(2) + assert im.info.get("duration") == 1000.0 + im.seek(3) + assert im.info.get("duration") == 500.0 + im.seek(4) + assert im.info.get("duration") == 1000.0 + + # test removal of duplicated frames + frame = Image.new("RGBA", (128, 64), (255, 0, 0, 255)) + frame.save( + test_file, save_all=True, append_images=[frame, frame], duration=[500, 100, 150] + ) + with Image.open(test_file) as im: + assert im.n_frames == 1 + assert "duration" not in im.info + + different_frame = Image.new("RGBA", (128, 64)) + frame.save( + test_file, + save_all=True, + append_images=[frame, different_frame], + duration=[500, 100, 150], + ) + with Image.open(test_file) as im: + assert im.n_frames == 2 + assert im.info["duration"] == 600 + + im.seek(1) + assert im.info["duration"] == 150 + + # test info duration + frame.info["duration"] = 300 + frame.save(test_file, save_all=True, append_images=[frame, different_frame]) + with Image.open(test_file) as im: + 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") + size = (128, 64) + red = Image.new("RGBA", size, (255, 0, 0, 255)) + green = Image.new("RGBA", size, (0, 255, 0, 255)) + transparent = Image.new("RGBA", size, (0, 0, 0, 0)) + + # test OP_NONE + red.save( + test_file, + save_all=True, + append_images=[green, transparent], + disposal=PngImagePlugin.Disposal.OP_NONE, + blend=PngImagePlugin.Blend.OP_OVER, + ) + with Image.open(test_file) as im: + im.seek(2) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + # test OP_BACKGROUND + disposal = [ + PngImagePlugin.Disposal.OP_NONE, + PngImagePlugin.Disposal.OP_BACKGROUND, + PngImagePlugin.Disposal.OP_NONE, + ] + red.save( + test_file, + save_all=True, + append_images=[red, transparent], + disposal=disposal, + blend=PngImagePlugin.Blend.OP_OVER, + ) + with Image.open(test_file) as im: + im.seek(2) + assert im.getpixel((0, 0)) == (0, 0, 0, 0) + assert im.getpixel((64, 32)) == (0, 0, 0, 0) + + disposal = [ + PngImagePlugin.Disposal.OP_NONE, + PngImagePlugin.Disposal.OP_BACKGROUND, + ] + red.save( + test_file, + save_all=True, + append_images=[green], + disposal=disposal, + blend=PngImagePlugin.Blend.OP_OVER, + ) + with Image.open(test_file) as im: + im.seek(1) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + # test OP_PREVIOUS + disposal = [ + PngImagePlugin.Disposal.OP_NONE, + PngImagePlugin.Disposal.OP_PREVIOUS, + PngImagePlugin.Disposal.OP_NONE, + ] + red.save( + test_file, + save_all=True, + append_images=[green, red, transparent], + default_image=True, + disposal=disposal, + blend=PngImagePlugin.Blend.OP_OVER, + ) + with Image.open(test_file) as im: + im.seek(3) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + disposal = [ + PngImagePlugin.Disposal.OP_NONE, + PngImagePlugin.Disposal.OP_PREVIOUS, + ] + red.save( + test_file, + save_all=True, + append_images=[green], + disposal=disposal, + blend=PngImagePlugin.Blend.OP_OVER, + ) + with Image.open(test_file) as im: + im.seek(1) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + # test info disposal + red.info["disposal"] = PngImagePlugin.Disposal.OP_BACKGROUND + red.save( + test_file, + save_all=True, + append_images=[Image.new("RGBA", (10, 10), (0, 255, 0, 255))], + ) + with Image.open(test_file) as im: + im.seek(1) + assert im.getpixel((64, 32)) == (0, 0, 0, 0) + + +def test_apng_save_disposal_previous(tmp_path: Path) -> None: + test_file = str(tmp_path / "temp.png") + size = (128, 64) + blue = Image.new("RGBA", size, (0, 0, 255, 255)) + red = Image.new("RGBA", size, (255, 0, 0, 255)) + green = Image.new("RGBA", size, (0, 255, 0, 255)) + + # test OP_NONE + blue.save( + test_file, + save_all=True, + append_images=[red, green], + disposal=PngImagePlugin.Disposal.OP_PREVIOUS, + ) + with Image.open(test_file) as im: + assert im.getpixel((0, 0)) == (0, 0, 255, 255) + + im.seek(2) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + +def test_apng_save_blend(tmp_path: Path) -> None: + test_file = str(tmp_path / "temp.png") + size = (128, 64) + red = Image.new("RGBA", size, (255, 0, 0, 255)) + green = Image.new("RGBA", size, (0, 255, 0, 255)) + transparent = Image.new("RGBA", size, (0, 0, 0, 0)) + + # test OP_SOURCE on solid color + blend = [ + PngImagePlugin.Blend.OP_OVER, + PngImagePlugin.Blend.OP_SOURCE, + ] + red.save( + test_file, + save_all=True, + append_images=[red, green], + default_image=True, + disposal=PngImagePlugin.Disposal.OP_NONE, + blend=blend, + ) + with Image.open(test_file) as im: + im.seek(2) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + # test OP_SOURCE on transparent color + blend = [ + PngImagePlugin.Blend.OP_OVER, + PngImagePlugin.Blend.OP_SOURCE, + ] + red.save( + test_file, + save_all=True, + append_images=[red, transparent], + default_image=True, + disposal=PngImagePlugin.Disposal.OP_NONE, + blend=blend, + ) + with Image.open(test_file) as im: + im.seek(2) + assert im.getpixel((0, 0)) == (0, 0, 0, 0) + assert im.getpixel((64, 32)) == (0, 0, 0, 0) + + # test OP_OVER + red.save( + test_file, + save_all=True, + append_images=[green, transparent], + default_image=True, + disposal=PngImagePlugin.Disposal.OP_NONE, + blend=PngImagePlugin.Blend.OP_OVER, + ) + with Image.open(test_file) as im: + im.seek(1) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + im.seek(2) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + # test info blend + red.info["blend"] = PngImagePlugin.Blend.OP_OVER + red.save(test_file, save_all=True, append_images=[green, transparent]) + with Image.open(test_file) as im: + im.seek(2) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + + +def test_apng_save_size(tmp_path: Path) -> None: + test_file = str(tmp_path / "temp.png") + + im = Image.new("L", (100, 100)) + im.save(test_file, save_all=True, append_images=[Image.new("L", (200, 200))]) + + with Image.open(test_file) as reloaded: + assert reloaded.size == (200, 200) + + +def test_seek_after_close() -> None: + im = Image.open("Tests/images/apng/delay.png") + im.seek(1) + im.close() + + with pytest.raises(ValueError): + im.seek(0) + + +@pytest.mark.parametrize("mode", ("RGBA", "RGB", "P")) +@pytest.mark.parametrize("default_image", (True, False)) +@pytest.mark.parametrize("duplicate", (True, False)) +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") + + im = Image.new("L", (1, 1)) + im.save( + test_file, + save_all=True, + default_image=default_image, + append_images=[im.convert(mode) if duplicate else Image.new(mode, (1, 1), 1)], + ) + with Image.open(test_file) as reloaded: + assert reloaded.mode == mode + + +def test_apng_repeated_seeks_give_correct_info() -> None: + with Image.open("Tests/images/apng/different_durations.png") as im: + for i in range(3): + im.seek(0) + assert im.info["duration"] == 4000 + im.seek(1) + assert im.info["duration"] == 1000 diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py index 1e2f20c40..b1d36b81c 100644 --- a/Tests/test_file_blp.py +++ b/Tests/test_file_blp.py @@ -1,78 +1,78 @@ -from __future__ import annotations - -from pathlib import Path - -import pytest - -from PIL import Image - -from .helper import ( - assert_image_equal, - assert_image_equal_tofile, - assert_image_similar, - hopper, -) - - -def test_load_blp1() -> None: - with Image.open("Tests/images/blp/blp1_jpeg.blp") as im: - assert_image_equal_tofile(im, "Tests/images/blp/blp1_jpeg.png") - - with Image.open("Tests/images/blp/blp1_jpeg2.blp") as im: - im.load() - - -def test_load_blp2_raw() -> None: - with Image.open("Tests/images/blp/blp2_raw.blp") as im: - assert_image_equal_tofile(im, "Tests/images/blp/blp2_raw.png") - - -def test_load_blp2_dxt1() -> None: - with Image.open("Tests/images/blp/blp2_dxt1.blp") as im: - assert_image_equal_tofile(im, "Tests/images/blp/blp2_dxt1.png") - - -def test_load_blp2_dxt1a() -> None: - with Image.open("Tests/images/blp/blp2_dxt1a.blp") as im: - assert_image_equal_tofile(im, "Tests/images/blp/blp2_dxt1a.png") - - -def test_save(tmp_path: Path) -> None: - f = str(tmp_path / "temp.blp") - - for version in ("BLP1", "BLP2"): - im = hopper("P") - im.save(f, blp_version=version) - - with Image.open(f) as reloaded: - assert_image_equal(im.convert("RGB"), reloaded) - - with Image.open("Tests/images/transparent.png") as im: - f = str(tmp_path / "temp.blp") - im.convert("P").save(f, blp_version=version) - - with Image.open(f) as reloaded: - assert_image_similar(im, reloaded, 8) - - im = hopper() - with pytest.raises(ValueError): - im.save(f) - - -@pytest.mark.parametrize( - "test_file", - [ - "Tests/images/timeout-060745d3f534ad6e4128c51d336ea5489182c69d.blp", - "Tests/images/timeout-31c8f86233ea728339c6e586be7af661a09b5b98.blp", - "Tests/images/timeout-60d8b7c8469d59fc9ffff6b3a3dc0faeae6ea8ee.blp", - "Tests/images/timeout-8073b430977660cdd48d96f6406ddfd4114e69c7.blp", - "Tests/images/timeout-bba4f2e026b5786529370e5dfe9a11b1bf991f07.blp", - "Tests/images/timeout-d6ec061c4afdef39d3edf6da8927240bb07fe9b7.blp", - "Tests/images/timeout-ef9112a065e7183fa7faa2e18929b03e44ee16bf.blp", - ], -) -def test_crashes(test_file: str) -> None: - with open(test_file, "rb") as f: - with Image.open(f) as im: - with pytest.raises(OSError): - im.load() +from __future__ import annotations + +from pathlib import Path + +import pytest + +from PIL import Image + +from .helper import ( + assert_image_equal, + assert_image_equal_tofile, + assert_image_similar, + hopper, +) + + +def test_load_blp1() -> None: + with Image.open("Tests/images/blp/blp1_jpeg.blp") as im: + assert_image_equal_tofile(im, "Tests/images/blp/blp1_jpeg.png") + + with Image.open("Tests/images/blp/blp1_jpeg2.blp") as im: + im.load() + + +def test_load_blp2_raw() -> None: + with Image.open("Tests/images/blp/blp2_raw.blp") as im: + assert_image_equal_tofile(im, "Tests/images/blp/blp2_raw.png") + + +def test_load_blp2_dxt1() -> None: + with Image.open("Tests/images/blp/blp2_dxt1.blp") as im: + assert_image_equal_tofile(im, "Tests/images/blp/blp2_dxt1.png") + + +def test_load_blp2_dxt1a() -> None: + with Image.open("Tests/images/blp/blp2_dxt1a.blp") as im: + assert_image_equal_tofile(im, "Tests/images/blp/blp2_dxt1a.png") + + +def test_save(tmp_path: Path) -> None: + f = str(tmp_path / "temp.blp") + + for version in ("BLP1", "BLP2"): + im = hopper("P") + im.save(f, blp_version=version) + + with Image.open(f) as reloaded: + assert_image_equal(im.convert("RGB"), reloaded) + + with Image.open("Tests/images/transparent.png") as im: + f = str(tmp_path / "temp.blp") + im.convert("P").save(f, blp_version=version) + + with Image.open(f) as reloaded: + assert_image_similar(im, reloaded, 8) + + im = hopper() + with pytest.raises(ValueError): + im.save(f) + + +@pytest.mark.parametrize( + "test_file", + [ + "Tests/images/timeout-060745d3f534ad6e4128c51d336ea5489182c69d.blp", + "Tests/images/timeout-31c8f86233ea728339c6e586be7af661a09b5b98.blp", + "Tests/images/timeout-60d8b7c8469d59fc9ffff6b3a3dc0faeae6ea8ee.blp", + "Tests/images/timeout-8073b430977660cdd48d96f6406ddfd4114e69c7.blp", + "Tests/images/timeout-bba4f2e026b5786529370e5dfe9a11b1bf991f07.blp", + "Tests/images/timeout-d6ec061c4afdef39d3edf6da8927240bb07fe9b7.blp", + "Tests/images/timeout-ef9112a065e7183fa7faa2e18929b03e44ee16bf.blp", + ], +) +def test_crashes(test_file: str) -> None: + with open(test_file, "rb") as f: + with Image.open(f) as im: + with pytest.raises(OSError): + im.load() diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index 2ff4160bd..c53568438 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -1,232 +1,232 @@ -from __future__ import annotations - -import io -from pathlib import Path - -import pytest - -from PIL import BmpImagePlugin, Image, _binary - -from .helper import ( - assert_image_equal, - assert_image_equal_tofile, - assert_image_similar_tofile, - hopper, -) - - -def test_sanity(tmp_path: Path) -> None: - def roundtrip(im: Image.Image) -> None: - outfile = str(tmp_path / "temp.bmp") - - 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")) - - -def test_invalid_file() -> None: - with open("Tests/images/flower.jpg", "rb") as fp: - with pytest.raises(SyntaxError): - BmpImagePlugin.BmpImageFile(fp) - - -def test_fallback_if_mmap_errors() -> None: - # This image has been truncated, - # so that the buffer is not large enough when using mmap - with Image.open("Tests/images/mmap_error.bmp") as im: - assert_image_equal_tofile(im, "Tests/images/pal8_offset.bmp") - - -def test_save_to_bytes() -> None: - output = io.BytesIO() - im = hopper() - im.save(output, "BMP") - - output.seek(0) - with Image.open(output) as reloaded: - assert im.mode == reloaded.mode - assert im.size == reloaded.size - assert reloaded.format == "BMP" - - -def test_small_palette(tmp_path: Path) -> None: - im = Image.new("P", (1, 1)) - colors = [0, 0, 0, 125, 125, 125, 255, 255, 255] - im.putpalette(colors) - - out = str(tmp_path / "temp.bmp") - im.save(out) - - with Image.open(out) as reloaded: - assert reloaded.getpalette() == colors - - -def test_save_too_large(tmp_path: Path) -> None: - outfile = str(tmp_path / "temp.bmp") - with Image.new("RGB", (1, 1)) as im: - im._size = (37838, 37838) - with pytest.raises(ValueError): - im.save(outfile) - - -def test_dpi() -> None: - dpi = (72, 72) - - output = io.BytesIO() - with hopper() as im: - im.save(output, "BMP", dpi=dpi) - - output.seek(0) - with Image.open(output) as reloaded: - assert reloaded.info["dpi"] == (72.008961115161, 72.008961115161) - - -def test_save_bmp_with_dpi(tmp_path: Path) -> None: - # Test for #1301 - # Arrange - outfile = str(tmp_path / "temp.jpg") - with Image.open("Tests/images/hopper.bmp") as im: - assert im.info["dpi"] == (95.98654816726399, 95.98654816726399) - - # Act - im.save(outfile, "JPEG", dpi=im.info["dpi"]) - - # Assert - with Image.open(outfile) as reloaded: - reloaded.load() - assert reloaded.info["dpi"] == (96, 96) - assert reloaded.size == im.size - assert reloaded.format == "JPEG" - - -def test_save_float_dpi(tmp_path: Path) -> None: - outfile = str(tmp_path / "temp.bmp") - with Image.open("Tests/images/hopper.bmp") as im: - im.save(outfile, dpi=(72.21216100543306, 72.21216100543306)) - with Image.open(outfile) as reloaded: - assert reloaded.info["dpi"] == (72.21216100543306, 72.21216100543306) - - -def test_load_dib() -> None: - # test for #1293, Imagegrab returning Unsupported Bitfields Format - with Image.open("Tests/images/clipboard.dib") as im: - assert im.format == "DIB" - assert im.get_format_mimetype() == "image/bmp" - - assert_image_equal_tofile(im, "Tests/images/clipboard_target.png") - - -@pytest.mark.parametrize( - "header_size, path", - ( - (12, "g/pal8os2.bmp"), - (40, "g/pal1.bmp"), - (52, "q/rgb32h52.bmp"), - (56, "q/rgba32h56.bmp"), - (64, "q/pal8os2v2.bmp"), - (108, "g/pal8v4.bmp"), - (124, "g/pal8v5.bmp"), - ), -) -def test_dib_header_size(header_size: int, path: str) -> None: - image_path = "Tests/images/bmp/" + path - with open(image_path, "rb") as fp: - data = fp.read()[14:] - assert _binary.i32le(data) == header_size - - dib = io.BytesIO(data) - with Image.open(dib) as im: - im.load() - - -def test_save_dib(tmp_path: Path) -> None: - outfile = str(tmp_path / "temp.dib") - - with Image.open("Tests/images/clipboard.dib") as im: - im.save(outfile) - - with Image.open(outfile) as reloaded: - assert reloaded.format == "DIB" - assert reloaded.get_format_mimetype() == "image/bmp" - assert_image_equal(im, reloaded) - - -def test_rgba_bitfields() -> None: - # This test image has been manually hexedited - # to change the bitfield compression in the header from XBGR to RGBA - with Image.open("Tests/images/rgb32bf-rgba.bmp") as im: - # So before the comparing the image, swap the channels - b, g, r = im.split()[1:] - im = Image.merge("RGB", (r, g, b)) - - assert_image_equal_tofile(im, "Tests/images/bmp/q/rgb32bf-xbgr.bmp") - - # This test image has been manually hexedited - # to change the bitfield compression in the header from XBGR to ABGR - with Image.open("Tests/images/rgb32bf-abgr.bmp") as im: - assert_image_equal_tofile( - im.convert("RGB"), "Tests/images/bmp/q/rgb32bf-xbgr.bmp" - ) - - -def test_rle8() -> None: - with Image.open("Tests/images/hopper_rle8.bmp") as im: - assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.bmp", 12) - - with Image.open("Tests/images/hopper_rle8_grayscale.bmp") as im: - assert_image_equal_tofile(im, "Tests/images/bw_gradient.png") - - # This test image has been manually hexedited - # to have rows with too much data - with Image.open("Tests/images/hopper_rle8_row_overflow.bmp") as im: - assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.bmp", 12) - - # Signal end of bitmap before the image is finished - with open("Tests/images/bmp/g/pal8rle.bmp", "rb") as fp: - data = fp.read(1063) + b"\x01" - with Image.open(io.BytesIO(data)) as im: - with pytest.raises(ValueError): - im.load() - - -def test_rle4() -> None: - with Image.open("Tests/images/bmp/g/pal4rle.bmp") as im: - assert_image_similar_tofile(im, "Tests/images/bmp/g/pal4.bmp", 12) - - -@pytest.mark.parametrize( - "file_name,length", - ( - # EOF immediately after the header - ("Tests/images/hopper_rle8.bmp", 1078), - # EOF during delta - ("Tests/images/bmp/q/pal8rletrns.bmp", 3670), - # EOF when reading data in absolute mode - ("Tests/images/bmp/g/pal8rle.bmp", 1064), - ), -) -def test_rle8_eof(file_name: str, length: int) -> None: - with open(file_name, "rb") as fp: - data = fp.read(length) - with Image.open(io.BytesIO(data)) as im: - with pytest.raises(ValueError): - im.load() - - -def test_offset() -> None: - # This image has been hexedited - # to exclude the palette size from the pixel data offset - with Image.open("Tests/images/pal8_offset.bmp") as im: - assert_image_equal_tofile(im, "Tests/images/bmp/g/pal8.bmp") +from __future__ import annotations + +import io +from pathlib import Path + +import pytest + +from PIL import BmpImagePlugin, Image, _binary + +from .helper import ( + assert_image_equal, + assert_image_equal_tofile, + assert_image_similar_tofile, + hopper, +) + + +def test_sanity(tmp_path: Path) -> None: + def roundtrip(im: Image.Image) -> None: + outfile = str(tmp_path / "temp.bmp") + + 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")) + + +def test_invalid_file() -> None: + with open("Tests/images/flower.jpg", "rb") as fp: + with pytest.raises(SyntaxError): + BmpImagePlugin.BmpImageFile(fp) + + +def test_fallback_if_mmap_errors() -> None: + # This image has been truncated, + # so that the buffer is not large enough when using mmap + with Image.open("Tests/images/mmap_error.bmp") as im: + assert_image_equal_tofile(im, "Tests/images/pal8_offset.bmp") + + +def test_save_to_bytes() -> None: + output = io.BytesIO() + im = hopper() + im.save(output, "BMP") + + output.seek(0) + with Image.open(output) as reloaded: + assert im.mode == reloaded.mode + assert im.size == reloaded.size + assert reloaded.format == "BMP" + + +def test_small_palette(tmp_path: Path) -> None: + im = Image.new("P", (1, 1)) + colors = [0, 0, 0, 125, 125, 125, 255, 255, 255] + im.putpalette(colors) + + out = str(tmp_path / "temp.bmp") + im.save(out) + + with Image.open(out) as reloaded: + assert reloaded.getpalette() == colors + + +def test_save_too_large(tmp_path: Path) -> None: + outfile = str(tmp_path / "temp.bmp") + with Image.new("RGB", (1, 1)) as im: + im._size = (37838, 37838) + with pytest.raises(ValueError): + im.save(outfile) + + +def test_dpi() -> None: + dpi = (72, 72) + + output = io.BytesIO() + with hopper() as im: + im.save(output, "BMP", dpi=dpi) + + output.seek(0) + with Image.open(output) as reloaded: + assert reloaded.info["dpi"] == (72.008961115161, 72.008961115161) + + +def test_save_bmp_with_dpi(tmp_path: Path) -> None: + # Test for #1301 + # Arrange + outfile = str(tmp_path / "temp.jpg") + with Image.open("Tests/images/hopper.bmp") as im: + assert im.info["dpi"] == (95.98654816726399, 95.98654816726399) + + # Act + im.save(outfile, "JPEG", dpi=im.info["dpi"]) + + # Assert + with Image.open(outfile) as reloaded: + reloaded.load() + assert reloaded.info["dpi"] == (96, 96) + assert reloaded.size == im.size + assert reloaded.format == "JPEG" + + +def test_save_float_dpi(tmp_path: Path) -> None: + outfile = str(tmp_path / "temp.bmp") + with Image.open("Tests/images/hopper.bmp") as im: + im.save(outfile, dpi=(72.21216100543306, 72.21216100543306)) + with Image.open(outfile) as reloaded: + assert reloaded.info["dpi"] == (72.21216100543306, 72.21216100543306) + + +def test_load_dib() -> None: + # test for #1293, Imagegrab returning Unsupported Bitfields Format + with Image.open("Tests/images/clipboard.dib") as im: + assert im.format == "DIB" + assert im.get_format_mimetype() == "image/bmp" + + assert_image_equal_tofile(im, "Tests/images/clipboard_target.png") + + +@pytest.mark.parametrize( + "header_size, path", + ( + (12, "g/pal8os2.bmp"), + (40, "g/pal1.bmp"), + (52, "q/rgb32h52.bmp"), + (56, "q/rgba32h56.bmp"), + (64, "q/pal8os2v2.bmp"), + (108, "g/pal8v4.bmp"), + (124, "g/pal8v5.bmp"), + ), +) +def test_dib_header_size(header_size: int, path: str) -> None: + image_path = "Tests/images/bmp/" + path + with open(image_path, "rb") as fp: + data = fp.read()[14:] + assert _binary.i32le(data) == header_size + + dib = io.BytesIO(data) + with Image.open(dib) as im: + im.load() + + +def test_save_dib(tmp_path: Path) -> None: + outfile = str(tmp_path / "temp.dib") + + with Image.open("Tests/images/clipboard.dib") as im: + im.save(outfile) + + with Image.open(outfile) as reloaded: + assert reloaded.format == "DIB" + assert reloaded.get_format_mimetype() == "image/bmp" + assert_image_equal(im, reloaded) + + +def test_rgba_bitfields() -> None: + # This test image has been manually hexedited + # to change the bitfield compression in the header from XBGR to RGBA + with Image.open("Tests/images/rgb32bf-rgba.bmp") as im: + # So before the comparing the image, swap the channels + b, g, r = im.split()[1:] + im = Image.merge("RGB", (r, g, b)) + + assert_image_equal_tofile(im, "Tests/images/bmp/q/rgb32bf-xbgr.bmp") + + # This test image has been manually hexedited + # to change the bitfield compression in the header from XBGR to ABGR + with Image.open("Tests/images/rgb32bf-abgr.bmp") as im: + assert_image_equal_tofile( + im.convert("RGB"), "Tests/images/bmp/q/rgb32bf-xbgr.bmp" + ) + + +def test_rle8() -> None: + with Image.open("Tests/images/hopper_rle8.bmp") as im: + assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.bmp", 12) + + with Image.open("Tests/images/hopper_rle8_grayscale.bmp") as im: + assert_image_equal_tofile(im, "Tests/images/bw_gradient.png") + + # This test image has been manually hexedited + # to have rows with too much data + with Image.open("Tests/images/hopper_rle8_row_overflow.bmp") as im: + assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.bmp", 12) + + # Signal end of bitmap before the image is finished + with open("Tests/images/bmp/g/pal8rle.bmp", "rb") as fp: + data = fp.read(1063) + b"\x01" + with Image.open(io.BytesIO(data)) as im: + with pytest.raises(ValueError): + im.load() + + +def test_rle4() -> None: + with Image.open("Tests/images/bmp/g/pal4rle.bmp") as im: + assert_image_similar_tofile(im, "Tests/images/bmp/g/pal4.bmp", 12) + + +@pytest.mark.parametrize( + "file_name,length", + ( + # EOF immediately after the header + ("Tests/images/hopper_rle8.bmp", 1078), + # EOF during delta + ("Tests/images/bmp/q/pal8rletrns.bmp", 3670), + # EOF when reading data in absolute mode + ("Tests/images/bmp/g/pal8rle.bmp", 1064), + ), +) +def test_rle8_eof(file_name: str, length: int) -> None: + with open(file_name, "rb") as fp: + data = fp.read(length) + with Image.open(io.BytesIO(data)) as im: + with pytest.raises(ValueError): + im.load() + + +def test_offset() -> None: + # This image has been hexedited + # to exclude the palette size from the pixel data offset + with Image.open("Tests/images/pal8_offset.bmp") as im: + assert_image_equal_tofile(im, "Tests/images/bmp/g/pal8.bmp") diff --git a/Tests/test_file_bufrstub.py b/Tests/test_file_bufrstub.py index 939e82e77..2e6000bfb 100644 --- a/Tests/test_file_bufrstub.py +++ b/Tests/test_file_bufrstub.py @@ -1,83 +1,83 @@ -from __future__ import annotations - -from pathlib import Path -from typing import IO - -import pytest - -from PIL import BufrStubImagePlugin, Image, ImageFile - -from .helper import hopper - -TEST_FILE = "Tests/images/gfs.t06z.rassda.tm00.bufr_d" - - -def test_open() -> None: - # Act - with Image.open(TEST_FILE) as im: - # Assert - assert im.format == "BUFR" - - # Dummy data from the stub - assert im.mode == "F" - assert im.size == (1, 1) - - -def test_invalid_file() -> None: - # Arrange - invalid_file = "Tests/images/flower.jpg" - - # Act / Assert - with pytest.raises(SyntaxError): - BufrStubImagePlugin.BufrStubImageFile(invalid_file) - - -def test_load() -> None: - # Arrange - with Image.open(TEST_FILE) as im: - # Act / Assert: stub cannot load without an implemented handler - with pytest.raises(OSError): - im.load() - - -def test_save(tmp_path: Path) -> None: - # Arrange - im = hopper() - tmpfile = str(tmp_path / "temp.bufr") - - # Act / Assert: stub cannot save without an implemented handler - with pytest.raises(OSError): - im.save(tmpfile) - - -def test_handler(tmp_path: Path) -> None: - class TestHandler(ImageFile.StubHandler): - opened = False - loaded = False - saved = False - - def open(self, im: ImageFile.StubImageFile) -> None: - self.opened = True - - def load(self, im: ImageFile.StubImageFile) -> Image.Image: - self.loaded = True - im.fp.close() - return Image.new("RGB", (1, 1)) - - def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None: - self.saved = True - - handler = TestHandler() - BufrStubImagePlugin.register_handler(handler) - with Image.open(TEST_FILE) as im: - assert handler.opened - assert not handler.loaded - - im.load() - assert handler.loaded - - temp_file = str(tmp_path / "temp.bufr") - im.save(temp_file) - assert handler.saved - - BufrStubImagePlugin._handler = None +from __future__ import annotations + +from pathlib import Path +from typing import IO + +import pytest + +from PIL import BufrStubImagePlugin, Image, ImageFile + +from .helper import hopper + +TEST_FILE = "Tests/images/gfs.t06z.rassda.tm00.bufr_d" + + +def test_open() -> None: + # Act + with Image.open(TEST_FILE) as im: + # Assert + assert im.format == "BUFR" + + # Dummy data from the stub + assert im.mode == "F" + assert im.size == (1, 1) + + +def test_invalid_file() -> None: + # Arrange + invalid_file = "Tests/images/flower.jpg" + + # Act / Assert + with pytest.raises(SyntaxError): + BufrStubImagePlugin.BufrStubImageFile(invalid_file) + + +def test_load() -> None: + # Arrange + with Image.open(TEST_FILE) as im: + # Act / Assert: stub cannot load without an implemented handler + with pytest.raises(OSError): + im.load() + + +def test_save(tmp_path: Path) -> None: + # Arrange + im = hopper() + tmpfile = str(tmp_path / "temp.bufr") + + # Act / Assert: stub cannot save without an implemented handler + with pytest.raises(OSError): + im.save(tmpfile) + + +def test_handler(tmp_path: Path) -> None: + class TestHandler(ImageFile.StubHandler): + opened = False + loaded = False + saved = False + + def open(self, im: ImageFile.StubImageFile) -> None: + self.opened = True + + def load(self, im: ImageFile.StubImageFile) -> Image.Image: + self.loaded = True + im.fp.close() + return Image.new("RGB", (1, 1)) + + def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None: + self.saved = True + + handler = TestHandler() + BufrStubImagePlugin.register_handler(handler) + with Image.open(TEST_FILE) as im: + assert handler.opened + assert not handler.loaded + + im.load() + assert handler.loaded + + temp_file = str(tmp_path / "temp.bufr") + im.save(temp_file) + assert handler.saved + + BufrStubImagePlugin._handler = None diff --git a/Tests/test_file_container.py b/Tests/test_file_container.py index 7f76fb47a..98a72769d 100644 --- a/Tests/test_file_container.py +++ b/Tests/test_file_container.py @@ -1,132 +1,132 @@ -from __future__ import annotations - -from typing import Literal - -import pytest - -from PIL import ContainerIO, Image - -from .helper import hopper - -TEST_FILE = "Tests/images/dummy.container" - - -def test_sanity() -> None: - dir(Image) - dir(ContainerIO) - - -def test_isatty() -> None: - with hopper() as im: - container = ContainerIO.ContainerIO(im, 0, 0) - - assert container.isatty() is False - - -@pytest.mark.parametrize( - "mode, expected_position", - ( - (0, 33), - (1, 66), - (2, 100), - ), -) -def test_seek_mode(mode: Literal[0, 1, 2], expected_position: int) -> None: - # Arrange - with open(TEST_FILE, "rb") as fh: - container = ContainerIO.ContainerIO(fh, 22, 100) - - # Act - container.seek(33, mode) - container.seek(33, mode) - - # Assert - assert container.tell() == expected_position - - -@pytest.mark.parametrize("bytesmode", (True, False)) -def test_read_n0(bytesmode: bool) -> None: - # Arrange - with open(TEST_FILE, "rb" if bytesmode else "r") as fh: - container = ContainerIO.ContainerIO(fh, 22, 100) - - # Act - container.seek(81) - data = container.read() - - # Assert - if bytesmode: - data = data.decode() - assert data == "7\nThis is line 8\n" - - -@pytest.mark.parametrize("bytesmode", (True, False)) -def test_read_n(bytesmode: bool) -> None: - # Arrange - with open(TEST_FILE, "rb" if bytesmode else "r") as fh: - container = ContainerIO.ContainerIO(fh, 22, 100) - - # Act - container.seek(81) - data = container.read(3) - - # Assert - if bytesmode: - data = data.decode() - assert data == "7\nT" - - -@pytest.mark.parametrize("bytesmode", (True, False)) -def test_read_eof(bytesmode: bool) -> None: - # Arrange - with open(TEST_FILE, "rb" if bytesmode else "r") as fh: - container = ContainerIO.ContainerIO(fh, 22, 100) - - # Act - container.seek(100) - data = container.read() - - # Assert - if bytesmode: - data = data.decode() - assert data == "" - - -@pytest.mark.parametrize("bytesmode", (True, False)) -def test_readline(bytesmode: bool) -> None: - # Arrange - with open(TEST_FILE, "rb" if bytesmode else "r") as fh: - container = ContainerIO.ContainerIO(fh, 0, 120) - - # Act - data = container.readline() - - # Assert - if bytesmode: - data = data.decode() - assert data == "This is line 1\n" - - -@pytest.mark.parametrize("bytesmode", (True, False)) -def test_readlines(bytesmode: bool) -> None: - # Arrange - expected = [ - "This is line 1\n", - "This is line 2\n", - "This is line 3\n", - "This is line 4\n", - "This is line 5\n", - "This is line 6\n", - "This is line 7\n", - "This is line 8\n", - ] - with open(TEST_FILE, "rb" if bytesmode else "r") as fh: - container = ContainerIO.ContainerIO(fh, 0, 120) - - # Act - data = container.readlines() - - # Assert - if bytesmode: - data = [line.decode() for line in data] - assert data == expected +from __future__ import annotations + +from typing import Literal + +import pytest + +from PIL import ContainerIO, Image + +from .helper import hopper + +TEST_FILE = "Tests/images/dummy.container" + + +def test_sanity() -> None: + dir(Image) + dir(ContainerIO) + + +def test_isatty() -> None: + with hopper() as im: + container = ContainerIO.ContainerIO(im, 0, 0) + + assert container.isatty() is False + + +@pytest.mark.parametrize( + "mode, expected_position", + ( + (0, 33), + (1, 66), + (2, 100), + ), +) +def test_seek_mode(mode: Literal[0, 1, 2], expected_position: int) -> None: + # Arrange + with open(TEST_FILE, "rb") as fh: + container = ContainerIO.ContainerIO(fh, 22, 100) + + # Act + container.seek(33, mode) + container.seek(33, mode) + + # Assert + assert container.tell() == expected_position + + +@pytest.mark.parametrize("bytesmode", (True, False)) +def test_read_n0(bytesmode: bool) -> None: + # Arrange + with open(TEST_FILE, "rb" if bytesmode else "r") as fh: + container = ContainerIO.ContainerIO(fh, 22, 100) + + # Act + container.seek(81) + data = container.read() + + # Assert + if bytesmode: + data = data.decode() + assert data == "7\nThis is line 8\n" + + +@pytest.mark.parametrize("bytesmode", (True, False)) +def test_read_n(bytesmode: bool) -> None: + # Arrange + with open(TEST_FILE, "rb" if bytesmode else "r") as fh: + container = ContainerIO.ContainerIO(fh, 22, 100) + + # Act + container.seek(81) + data = container.read(3) + + # Assert + if bytesmode: + data = data.decode() + assert data == "7\nT" + + +@pytest.mark.parametrize("bytesmode", (True, False)) +def test_read_eof(bytesmode: bool) -> None: + # Arrange + with open(TEST_FILE, "rb" if bytesmode else "r") as fh: + container = ContainerIO.ContainerIO(fh, 22, 100) + + # Act + container.seek(100) + data = container.read() + + # Assert + if bytesmode: + data = data.decode() + assert data == "" + + +@pytest.mark.parametrize("bytesmode", (True, False)) +def test_readline(bytesmode: bool) -> None: + # Arrange + with open(TEST_FILE, "rb" if bytesmode else "r") as fh: + container = ContainerIO.ContainerIO(fh, 0, 120) + + # Act + data = container.readline() + + # Assert + if bytesmode: + data = data.decode() + assert data == "This is line 1\n" + + +@pytest.mark.parametrize("bytesmode", (True, False)) +def test_readlines(bytesmode: bool) -> None: + # Arrange + expected = [ + "This is line 1\n", + "This is line 2\n", + "This is line 3\n", + "This is line 4\n", + "This is line 5\n", + "This is line 6\n", + "This is line 7\n", + "This is line 8\n", + ] + with open(TEST_FILE, "rb" if bytesmode else "r") as fh: + container = ContainerIO.ContainerIO(fh, 0, 120) + + # Act + data = container.readlines() + + # Assert + if bytesmode: + data = [line.decode() for line in data] + assert data == expected diff --git a/Tests/test_file_cur.py b/Tests/test_file_cur.py index dbf1b866d..a598ac682 100644 --- a/Tests/test_file_cur.py +++ b/Tests/test_file_cur.py @@ -1,32 +1,32 @@ -from __future__ import annotations - -import pytest - -from PIL import CurImagePlugin, Image - -TEST_FILE = "Tests/images/deerstalker.cur" - - -def test_sanity() -> None: - with Image.open(TEST_FILE) as im: - assert im.size == (32, 32) - assert isinstance(im, CurImagePlugin.CurImageFile) - # Check some pixel colors to ensure image is loaded properly - assert im.getpixel((10, 1)) == (0, 0, 0, 0) - assert im.getpixel((11, 1)) == (253, 254, 254, 1) - assert im.getpixel((16, 16)) == (84, 87, 86, 255) - - -def test_invalid_file() -> None: - invalid_file = "Tests/images/flower.jpg" - - with pytest.raises(SyntaxError): - CurImagePlugin.CurImageFile(invalid_file) - - no_cursors_file = "Tests/images/no_cursors.cur" - - cur = CurImagePlugin.CurImageFile(TEST_FILE) - cur.fp.close() - with open(no_cursors_file, "rb") as cur.fp: - with pytest.raises(TypeError): - cur._open() +from __future__ import annotations + +import pytest + +from PIL import CurImagePlugin, Image + +TEST_FILE = "Tests/images/deerstalker.cur" + + +def test_sanity() -> None: + with Image.open(TEST_FILE) as im: + assert im.size == (32, 32) + assert isinstance(im, CurImagePlugin.CurImageFile) + # Check some pixel colors to ensure image is loaded properly + assert im.getpixel((10, 1)) == (0, 0, 0, 0) + assert im.getpixel((11, 1)) == (253, 254, 254, 1) + assert im.getpixel((16, 16)) == (84, 87, 86, 255) + + +def test_invalid_file() -> None: + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(SyntaxError): + CurImagePlugin.CurImageFile(invalid_file) + + no_cursors_file = "Tests/images/no_cursors.cur" + + cur = CurImagePlugin.CurImageFile(TEST_FILE) + cur.fp.close() + with open(no_cursors_file, "rb") as cur.fp: + with pytest.raises(TypeError): + cur._open() diff --git a/Tests/test_file_dcx.py b/Tests/test_file_dcx.py index 65337cad9..2ee555217 100644 --- a/Tests/test_file_dcx.py +++ b/Tests/test_file_dcx.py @@ -1,92 +1,92 @@ -from __future__ import annotations - -import warnings - -import pytest - -from PIL import DcxImagePlugin, Image - -from .helper import assert_image_equal, hopper, is_pypy - -# Created with ImageMagick: convert hopper.ppm hopper.dcx -TEST_FILE = "Tests/images/hopper.dcx" - - -def test_sanity() -> None: - # Arrange - - # Act - with Image.open(TEST_FILE) as im: - # Assert - assert im.size == (128, 128) - assert isinstance(im, DcxImagePlugin.DcxImageFile) - orig = hopper() - assert_image_equal(im, orig) - - -@pytest.mark.skipif(is_pypy(), reason="Requires CPython") -def test_unclosed_file() -> None: - def open() -> None: - im = Image.open(TEST_FILE) - im.load() - - with pytest.warns(ResourceWarning): - open() - - -def test_closed_file() -> None: - with warnings.catch_warnings(): - im = Image.open(TEST_FILE) - im.load() - im.close() - - -def test_context_manager() -> None: - with warnings.catch_warnings(): - with Image.open(TEST_FILE) as im: - im.load() - - -def test_invalid_file() -> None: - with open("Tests/images/flower.jpg", "rb") as fp: - with pytest.raises(SyntaxError): - DcxImagePlugin.DcxImageFile(fp) - - -def test_tell() -> None: - # Arrange - with Image.open(TEST_FILE) as im: - # Act - frame = im.tell() - - # Assert - assert frame == 0 - - -def test_n_frames() -> None: - with Image.open(TEST_FILE) as im: - assert im.n_frames == 1 - assert not im.is_animated - - -def test_eoferror() -> None: - with Image.open(TEST_FILE) as im: - n_frames = im.n_frames - - # Test seeking past the last frame - with pytest.raises(EOFError): - im.seek(n_frames) - assert im.tell() < n_frames - - # Test that seeking to the last frame does not raise an error - im.seek(n_frames - 1) - - -def test_seek_too_far() -> None: - # Arrange - with Image.open(TEST_FILE) as im: - frame = 999 # too big on purpose - - # Act / Assert - with pytest.raises(EOFError): - im.seek(frame) +from __future__ import annotations + +import warnings + +import pytest + +from PIL import DcxImagePlugin, Image + +from .helper import assert_image_equal, hopper, is_pypy + +# Created with ImageMagick: convert hopper.ppm hopper.dcx +TEST_FILE = "Tests/images/hopper.dcx" + + +def test_sanity() -> None: + # Arrange + + # Act + with Image.open(TEST_FILE) as im: + # Assert + assert im.size == (128, 128) + assert isinstance(im, DcxImagePlugin.DcxImageFile) + orig = hopper() + assert_image_equal(im, orig) + + +@pytest.mark.skipif(is_pypy(), reason="Requires CPython") +def test_unclosed_file() -> None: + def open() -> None: + im = Image.open(TEST_FILE) + im.load() + + with pytest.warns(ResourceWarning): + open() + + +def test_closed_file() -> None: + with warnings.catch_warnings(): + im = Image.open(TEST_FILE) + im.load() + im.close() + + +def test_context_manager() -> None: + with warnings.catch_warnings(): + with Image.open(TEST_FILE) as im: + im.load() + + +def test_invalid_file() -> None: + with open("Tests/images/flower.jpg", "rb") as fp: + with pytest.raises(SyntaxError): + DcxImagePlugin.DcxImageFile(fp) + + +def test_tell() -> None: + # Arrange + with Image.open(TEST_FILE) as im: + # Act + frame = im.tell() + + # Assert + assert frame == 0 + + +def test_n_frames() -> None: + with Image.open(TEST_FILE) as im: + assert im.n_frames == 1 + assert not im.is_animated + + +def test_eoferror() -> None: + with Image.open(TEST_FILE) as im: + n_frames = im.n_frames + + # Test seeking past the last frame + with pytest.raises(EOFError): + im.seek(n_frames) + assert im.tell() < n_frames + + # Test that seeking to the last frame does not raise an error + im.seek(n_frames - 1) + + +def test_seek_too_far() -> None: + # Arrange + with Image.open(TEST_FILE) as im: + frame = 999 # too big on purpose + + # Act / Assert + with pytest.raises(EOFError): + im.seek(frame) diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index ebc0e89a1..55296a173 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -1,391 +1,391 @@ -"""Test DdsImagePlugin""" - -from __future__ import annotations - -from io import BytesIO -from pathlib import Path - -import pytest - -from PIL import DdsImagePlugin, Image - -from .helper import assert_image_equal, assert_image_equal_tofile, hopper - -TEST_FILE_DXT1 = "Tests/images/dxt1-rgb-4bbp-noalpha_MipMaps-1.dds" -TEST_FILE_DXT3 = "Tests/images/dxt3-argb-8bbp-explicitalpha_MipMaps-1.dds" -TEST_FILE_DXT5 = "Tests/images/dxt5-argb-8bbp-interpolatedalpha_MipMaps-1.dds" -TEST_FILE_ATI1 = "Tests/images/ati1.dds" -TEST_FILE_ATI2 = "Tests/images/ati2.dds" -TEST_FILE_DX10_BC4_TYPELESS = "Tests/images/bc4_typeless.dds" -TEST_FILE_DX10_BC4_UNORM = "Tests/images/bc4_unorm.dds" -TEST_FILE_DX10_BC5_TYPELESS = "Tests/images/bc5_typeless.dds" -TEST_FILE_DX10_BC5_UNORM = "Tests/images/bc5_unorm.dds" -TEST_FILE_DX10_BC5_SNORM = "Tests/images/bc5_snorm.dds" -TEST_FILE_DX10_BC1 = "Tests/images/bc1.dds" -TEST_FILE_DX10_BC1_TYPELESS = "Tests/images/bc1_typeless.dds" -TEST_FILE_BC4U = "Tests/images/bc4u.dds" -TEST_FILE_BC5S = "Tests/images/bc5s.dds" -TEST_FILE_BC5U = "Tests/images/bc5u.dds" -TEST_FILE_BC6H = "Tests/images/bc6h.dds" -TEST_FILE_BC6HS = "Tests/images/bc6h_sf.dds" -TEST_FILE_DX10_BC7 = "Tests/images/bc7-argb-8bpp_MipMaps-1.dds" -TEST_FILE_DX10_BC7_UNORM_SRGB = "Tests/images/DXGI_FORMAT_BC7_UNORM_SRGB.dds" -TEST_FILE_DX10_R8G8B8A8 = "Tests/images/argb-32bpp_MipMaps-1.dds" -TEST_FILE_DX10_R8G8B8A8_UNORM_SRGB = "Tests/images/DXGI_FORMAT_R8G8B8A8_UNORM_SRGB.dds" -TEST_FILE_UNCOMPRESSED_L = "Tests/images/uncompressed_l.dds" -TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA = "Tests/images/uncompressed_la.dds" -TEST_FILE_UNCOMPRESSED_RGB = "Tests/images/hopper.dds" -TEST_FILE_UNCOMPRESSED_BGR15 = "Tests/images/bgr15.dds" -TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds" - - -@pytest.mark.parametrize( - "image_path", - ( - TEST_FILE_DXT1, - # hexeditted to use DX10 FourCC - TEST_FILE_DX10_BC1, - TEST_FILE_DX10_BC1_TYPELESS, - ), -) -def test_sanity_dxt1_bc1(image_path: str) -> None: - """Check DXT1 and BC1 images can be opened""" - with Image.open(TEST_FILE_DXT1.replace(".dds", ".png")) as target: - target = target.convert("RGBA") - with Image.open(image_path) as im: - im.load() - - assert im.format == "DDS" - assert im.mode == "RGBA" - assert im.size == (256, 256) - - assert_image_equal(im, target) - - -def test_sanity_dxt3() -> None: - """Check DXT3 images can be opened""" - - with Image.open(TEST_FILE_DXT3) as im: - im.load() - - assert im.format == "DDS" - assert im.mode == "RGBA" - assert im.size == (256, 256) - - assert_image_equal_tofile(im, TEST_FILE_DXT3.replace(".dds", ".png")) - - -def test_sanity_dxt5() -> None: - """Check DXT5 images can be opened""" - - with Image.open(TEST_FILE_DXT5) as im: - im.load() - - assert im.format == "DDS" - assert im.mode == "RGBA" - assert im.size == (256, 256) - - assert_image_equal_tofile(im, TEST_FILE_DXT5.replace(".dds", ".png")) - - -@pytest.mark.parametrize( - "image_path", - ( - TEST_FILE_ATI1, - # hexeditted to use BC4U FourCC - TEST_FILE_BC4U, - ), -) -def test_sanity_ati1_bc4u(image_path: str) -> None: - """Check ATI1 and BC4U images can be opened""" - - with Image.open(image_path) as im: - im.load() - - assert im.format == "DDS" - assert im.mode == "L" - assert im.size == (64, 64) - - assert_image_equal_tofile(im, TEST_FILE_ATI1.replace(".dds", ".png")) - - -@pytest.mark.parametrize( - "image_path", - ( - TEST_FILE_DX10_BC4_UNORM, - # hexeditted to be typeless - TEST_FILE_DX10_BC4_TYPELESS, - ), -) -def test_dx10_bc4(image_path: str) -> None: - """Check DX10 BC4 images can be opened""" - - with Image.open(image_path) as im: - im.load() - - assert im.format == "DDS" - assert im.mode == "L" - assert im.size == (64, 64) - - assert_image_equal_tofile(im, TEST_FILE_DX10_BC4_UNORM.replace(".dds", ".png")) - - -@pytest.mark.parametrize( - "image_path", - ( - TEST_FILE_ATI2, - # hexeditted to use BC5U FourCC - TEST_FILE_BC5U, - ), -) -def test_sanity_ati2_bc5u(image_path: str) -> None: - """Check ATI2 and BC5U images can be opened""" - - with Image.open(image_path) as im: - im.load() - - assert im.format == "DDS" - assert im.mode == "RGB" - assert im.size == (256, 256) - - assert_image_equal_tofile(im, TEST_FILE_DX10_BC5_UNORM.replace(".dds", ".png")) - - -@pytest.mark.parametrize( - ("image_path", "expected_path"), - ( - # hexeditted to be typeless - (TEST_FILE_DX10_BC5_TYPELESS, TEST_FILE_DX10_BC5_UNORM), - (TEST_FILE_DX10_BC5_UNORM, TEST_FILE_DX10_BC5_UNORM), - # hexeditted to use DX10 FourCC - (TEST_FILE_DX10_BC5_SNORM, TEST_FILE_BC5S), - (TEST_FILE_BC5S, TEST_FILE_BC5S), - ), -) -def test_dx10_bc5(image_path: str, expected_path: str) -> None: - """Check DX10 BC5 images can be opened""" - - with Image.open(image_path) as im: - im.load() - - assert im.format == "DDS" - assert im.mode == "RGB" - assert im.size == (256, 256) - - assert_image_equal_tofile(im, expected_path.replace(".dds", ".png")) - - -@pytest.mark.parametrize("image_path", (TEST_FILE_BC6H, TEST_FILE_BC6HS)) -def test_dx10_bc6h(image_path: str) -> None: - """Check DX10 BC6H/BC6HS images can be opened""" - - with Image.open(image_path) as im: - im.load() - - assert im.format == "DDS" - assert im.mode == "RGB" - assert im.size == (128, 128) - - assert_image_equal_tofile(im, image_path.replace(".dds", ".png")) - - -def test_dx10_bc7() -> None: - """Check DX10 images can be opened""" - - with Image.open(TEST_FILE_DX10_BC7) as im: - im.load() - - assert im.format == "DDS" - assert im.mode == "RGBA" - assert im.size == (256, 256) - - assert_image_equal_tofile(im, TEST_FILE_DX10_BC7.replace(".dds", ".png")) - - -def test_dx10_bc7_unorm_srgb() -> None: - """Check DX10 unsigned normalized integer images can be opened""" - - with Image.open(TEST_FILE_DX10_BC7_UNORM_SRGB) as im: - im.load() - - assert im.format == "DDS" - assert im.mode == "RGBA" - assert im.size == (16, 16) - assert im.info["gamma"] == 1 / 2.2 - - assert_image_equal_tofile( - im, TEST_FILE_DX10_BC7_UNORM_SRGB.replace(".dds", ".png") - ) - - -def test_dx10_r8g8b8a8() -> None: - """Check DX10 images can be opened""" - - with Image.open(TEST_FILE_DX10_R8G8B8A8) as im: - im.load() - - assert im.format == "DDS" - assert im.mode == "RGBA" - assert im.size == (256, 256) - - assert_image_equal_tofile(im, TEST_FILE_DX10_R8G8B8A8.replace(".dds", ".png")) - - -def test_dx10_r8g8b8a8_unorm_srgb() -> None: - """Check DX10 unsigned normalized integer images can be opened""" - - with Image.open(TEST_FILE_DX10_R8G8B8A8_UNORM_SRGB) as im: - im.load() - - assert im.format == "DDS" - assert im.mode == "RGBA" - assert im.size == (16, 16) - assert im.info["gamma"] == 1 / 2.2 - - assert_image_equal_tofile( - im, TEST_FILE_DX10_R8G8B8A8_UNORM_SRGB.replace(".dds", ".png") - ) - - -@pytest.mark.parametrize( - ("mode", "size", "test_file"), - [ - ("L", (128, 128), TEST_FILE_UNCOMPRESSED_L), - ("LA", (128, 128), TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA), - ("RGB", (128, 128), TEST_FILE_UNCOMPRESSED_RGB), - ("RGB", (128, 128), TEST_FILE_UNCOMPRESSED_BGR15), - ("RGBA", (800, 600), TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA), - ], -) -def test_uncompressed(mode: str, size: tuple[int, int], test_file: str) -> None: - """Check uncompressed images can be opened""" - - with Image.open(test_file) as im: - assert im.format == "DDS" - assert im.mode == mode - assert im.size == size - - assert_image_equal_tofile(im, test_file.replace(".dds", ".png")) - - -def test__accept_true() -> None: - """Check valid prefix""" - # Arrange - prefix = b"DDS etc" - - # Act - output = DdsImagePlugin._accept(prefix) - - # Assert - assert output - - -def test__accept_false() -> None: - """Check invalid prefix""" - # Arrange - prefix = b"something invalid" - - # Act - output = DdsImagePlugin._accept(prefix) - - # Assert - assert not output - - -def test_invalid_file() -> None: - invalid_file = "Tests/images/flower.jpg" - - with pytest.raises(SyntaxError): - DdsImagePlugin.DdsImageFile(invalid_file) - - -def test_short_header() -> None: - """Check a short header""" - with open(TEST_FILE_DXT5, "rb") as f: - img_file = f.read() - - def short_header() -> None: - with Image.open(BytesIO(img_file[:119])): - pass # pragma: no cover - - with pytest.raises(OSError): - short_header() - - -def test_short_file() -> None: - """Check that the appropriate error is thrown for a short file""" - - with open(TEST_FILE_DXT5, "rb") as f: - img_file = f.read() - - def short_file() -> None: - with Image.open(BytesIO(img_file[:-100])) as im: - im.load() - - with pytest.raises(OSError): - short_file() - - -def test_dxt5_colorblock_alpha_issue_4142() -> None: - """Check that colorblocks are decoded correctly in DXT5""" - - with Image.open("Tests/images/dxt5-colorblock-alpha-issue-4142.dds") as im: - px = im.getpixel((0, 0)) - assert px[0] != 0 - assert px[1] != 0 - assert px[2] != 0 - - px = im.getpixel((1, 0)) - assert px[0] != 0 - assert px[1] != 0 - assert px[2] != 0 - - -def test_palette() -> None: - with Image.open("Tests/images/palette.dds") as im: - assert_image_equal_tofile(im, "Tests/images/transparent.gif") - - -def test_unsupported_bitcount() -> None: - with pytest.raises(OSError): - with Image.open("Tests/images/unsupported_bitcount.dds"): - pass - - -@pytest.mark.parametrize( - "test_file", - ( - "Tests/images/unimplemented_dxgi_format.dds", - "Tests/images/unimplemented_pfflags.dds", - ), -) -def test_not_implemented(test_file: str) -> None: - with pytest.raises(NotImplementedError): - with Image.open(test_file): - pass - - -def test_save_unsupported_mode(tmp_path: Path) -> None: - out = str(tmp_path / "temp.dds") - im = hopper("HSV") - with pytest.raises(OSError): - im.save(out) - - -@pytest.mark.parametrize( - ("mode", "test_file"), - [ - ("L", "Tests/images/linear_gradient.png"), - ("LA", "Tests/images/uncompressed_la.png"), - ("RGB", "Tests/images/hopper.png"), - ("RGBA", "Tests/images/pil123rgba.png"), - ], -) -def test_save(mode: str, test_file: str, tmp_path: Path) -> None: - out = str(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) +"""Test DdsImagePlugin""" + +from __future__ import annotations + +from io import BytesIO +from pathlib import Path + +import pytest + +from PIL import DdsImagePlugin, Image + +from .helper import assert_image_equal, assert_image_equal_tofile, hopper + +TEST_FILE_DXT1 = "Tests/images/dxt1-rgb-4bbp-noalpha_MipMaps-1.dds" +TEST_FILE_DXT3 = "Tests/images/dxt3-argb-8bbp-explicitalpha_MipMaps-1.dds" +TEST_FILE_DXT5 = "Tests/images/dxt5-argb-8bbp-interpolatedalpha_MipMaps-1.dds" +TEST_FILE_ATI1 = "Tests/images/ati1.dds" +TEST_FILE_ATI2 = "Tests/images/ati2.dds" +TEST_FILE_DX10_BC4_TYPELESS = "Tests/images/bc4_typeless.dds" +TEST_FILE_DX10_BC4_UNORM = "Tests/images/bc4_unorm.dds" +TEST_FILE_DX10_BC5_TYPELESS = "Tests/images/bc5_typeless.dds" +TEST_FILE_DX10_BC5_UNORM = "Tests/images/bc5_unorm.dds" +TEST_FILE_DX10_BC5_SNORM = "Tests/images/bc5_snorm.dds" +TEST_FILE_DX10_BC1 = "Tests/images/bc1.dds" +TEST_FILE_DX10_BC1_TYPELESS = "Tests/images/bc1_typeless.dds" +TEST_FILE_BC4U = "Tests/images/bc4u.dds" +TEST_FILE_BC5S = "Tests/images/bc5s.dds" +TEST_FILE_BC5U = "Tests/images/bc5u.dds" +TEST_FILE_BC6H = "Tests/images/bc6h.dds" +TEST_FILE_BC6HS = "Tests/images/bc6h_sf.dds" +TEST_FILE_DX10_BC7 = "Tests/images/bc7-argb-8bpp_MipMaps-1.dds" +TEST_FILE_DX10_BC7_UNORM_SRGB = "Tests/images/DXGI_FORMAT_BC7_UNORM_SRGB.dds" +TEST_FILE_DX10_R8G8B8A8 = "Tests/images/argb-32bpp_MipMaps-1.dds" +TEST_FILE_DX10_R8G8B8A8_UNORM_SRGB = "Tests/images/DXGI_FORMAT_R8G8B8A8_UNORM_SRGB.dds" +TEST_FILE_UNCOMPRESSED_L = "Tests/images/uncompressed_l.dds" +TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA = "Tests/images/uncompressed_la.dds" +TEST_FILE_UNCOMPRESSED_RGB = "Tests/images/hopper.dds" +TEST_FILE_UNCOMPRESSED_BGR15 = "Tests/images/bgr15.dds" +TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds" + + +@pytest.mark.parametrize( + "image_path", + ( + TEST_FILE_DXT1, + # hexeditted to use DX10 FourCC + TEST_FILE_DX10_BC1, + TEST_FILE_DX10_BC1_TYPELESS, + ), +) +def test_sanity_dxt1_bc1(image_path: str) -> None: + """Check DXT1 and BC1 images can be opened""" + with Image.open(TEST_FILE_DXT1.replace(".dds", ".png")) as target: + target = target.convert("RGBA") + with Image.open(image_path) as im: + im.load() + + assert im.format == "DDS" + assert im.mode == "RGBA" + assert im.size == (256, 256) + + assert_image_equal(im, target) + + +def test_sanity_dxt3() -> None: + """Check DXT3 images can be opened""" + + with Image.open(TEST_FILE_DXT3) as im: + im.load() + + assert im.format == "DDS" + assert im.mode == "RGBA" + assert im.size == (256, 256) + + assert_image_equal_tofile(im, TEST_FILE_DXT3.replace(".dds", ".png")) + + +def test_sanity_dxt5() -> None: + """Check DXT5 images can be opened""" + + with Image.open(TEST_FILE_DXT5) as im: + im.load() + + assert im.format == "DDS" + assert im.mode == "RGBA" + assert im.size == (256, 256) + + assert_image_equal_tofile(im, TEST_FILE_DXT5.replace(".dds", ".png")) + + +@pytest.mark.parametrize( + "image_path", + ( + TEST_FILE_ATI1, + # hexeditted to use BC4U FourCC + TEST_FILE_BC4U, + ), +) +def test_sanity_ati1_bc4u(image_path: str) -> None: + """Check ATI1 and BC4U images can be opened""" + + with Image.open(image_path) as im: + im.load() + + assert im.format == "DDS" + assert im.mode == "L" + assert im.size == (64, 64) + + assert_image_equal_tofile(im, TEST_FILE_ATI1.replace(".dds", ".png")) + + +@pytest.mark.parametrize( + "image_path", + ( + TEST_FILE_DX10_BC4_UNORM, + # hexeditted to be typeless + TEST_FILE_DX10_BC4_TYPELESS, + ), +) +def test_dx10_bc4(image_path: str) -> None: + """Check DX10 BC4 images can be opened""" + + with Image.open(image_path) as im: + im.load() + + assert im.format == "DDS" + assert im.mode == "L" + assert im.size == (64, 64) + + assert_image_equal_tofile(im, TEST_FILE_DX10_BC4_UNORM.replace(".dds", ".png")) + + +@pytest.mark.parametrize( + "image_path", + ( + TEST_FILE_ATI2, + # hexeditted to use BC5U FourCC + TEST_FILE_BC5U, + ), +) +def test_sanity_ati2_bc5u(image_path: str) -> None: + """Check ATI2 and BC5U images can be opened""" + + with Image.open(image_path) as im: + im.load() + + assert im.format == "DDS" + assert im.mode == "RGB" + assert im.size == (256, 256) + + assert_image_equal_tofile(im, TEST_FILE_DX10_BC5_UNORM.replace(".dds", ".png")) + + +@pytest.mark.parametrize( + ("image_path", "expected_path"), + ( + # hexeditted to be typeless + (TEST_FILE_DX10_BC5_TYPELESS, TEST_FILE_DX10_BC5_UNORM), + (TEST_FILE_DX10_BC5_UNORM, TEST_FILE_DX10_BC5_UNORM), + # hexeditted to use DX10 FourCC + (TEST_FILE_DX10_BC5_SNORM, TEST_FILE_BC5S), + (TEST_FILE_BC5S, TEST_FILE_BC5S), + ), +) +def test_dx10_bc5(image_path: str, expected_path: str) -> None: + """Check DX10 BC5 images can be opened""" + + with Image.open(image_path) as im: + im.load() + + assert im.format == "DDS" + assert im.mode == "RGB" + assert im.size == (256, 256) + + assert_image_equal_tofile(im, expected_path.replace(".dds", ".png")) + + +@pytest.mark.parametrize("image_path", (TEST_FILE_BC6H, TEST_FILE_BC6HS)) +def test_dx10_bc6h(image_path: str) -> None: + """Check DX10 BC6H/BC6HS images can be opened""" + + with Image.open(image_path) as im: + im.load() + + assert im.format == "DDS" + assert im.mode == "RGB" + assert im.size == (128, 128) + + assert_image_equal_tofile(im, image_path.replace(".dds", ".png")) + + +def test_dx10_bc7() -> None: + """Check DX10 images can be opened""" + + with Image.open(TEST_FILE_DX10_BC7) as im: + im.load() + + assert im.format == "DDS" + assert im.mode == "RGBA" + assert im.size == (256, 256) + + assert_image_equal_tofile(im, TEST_FILE_DX10_BC7.replace(".dds", ".png")) + + +def test_dx10_bc7_unorm_srgb() -> None: + """Check DX10 unsigned normalized integer images can be opened""" + + with Image.open(TEST_FILE_DX10_BC7_UNORM_SRGB) as im: + im.load() + + assert im.format == "DDS" + assert im.mode == "RGBA" + assert im.size == (16, 16) + assert im.info["gamma"] == 1 / 2.2 + + assert_image_equal_tofile( + im, TEST_FILE_DX10_BC7_UNORM_SRGB.replace(".dds", ".png") + ) + + +def test_dx10_r8g8b8a8() -> None: + """Check DX10 images can be opened""" + + with Image.open(TEST_FILE_DX10_R8G8B8A8) as im: + im.load() + + assert im.format == "DDS" + assert im.mode == "RGBA" + assert im.size == (256, 256) + + assert_image_equal_tofile(im, TEST_FILE_DX10_R8G8B8A8.replace(".dds", ".png")) + + +def test_dx10_r8g8b8a8_unorm_srgb() -> None: + """Check DX10 unsigned normalized integer images can be opened""" + + with Image.open(TEST_FILE_DX10_R8G8B8A8_UNORM_SRGB) as im: + im.load() + + assert im.format == "DDS" + assert im.mode == "RGBA" + assert im.size == (16, 16) + assert im.info["gamma"] == 1 / 2.2 + + assert_image_equal_tofile( + im, TEST_FILE_DX10_R8G8B8A8_UNORM_SRGB.replace(".dds", ".png") + ) + + +@pytest.mark.parametrize( + ("mode", "size", "test_file"), + [ + ("L", (128, 128), TEST_FILE_UNCOMPRESSED_L), + ("LA", (128, 128), TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA), + ("RGB", (128, 128), TEST_FILE_UNCOMPRESSED_RGB), + ("RGB", (128, 128), TEST_FILE_UNCOMPRESSED_BGR15), + ("RGBA", (800, 600), TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA), + ], +) +def test_uncompressed(mode: str, size: tuple[int, int], test_file: str) -> None: + """Check uncompressed images can be opened""" + + with Image.open(test_file) as im: + assert im.format == "DDS" + assert im.mode == mode + assert im.size == size + + assert_image_equal_tofile(im, test_file.replace(".dds", ".png")) + + +def test__accept_true() -> None: + """Check valid prefix""" + # Arrange + prefix = b"DDS etc" + + # Act + output = DdsImagePlugin._accept(prefix) + + # Assert + assert output + + +def test__accept_false() -> None: + """Check invalid prefix""" + # Arrange + prefix = b"something invalid" + + # Act + output = DdsImagePlugin._accept(prefix) + + # Assert + assert not output + + +def test_invalid_file() -> None: + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(SyntaxError): + DdsImagePlugin.DdsImageFile(invalid_file) + + +def test_short_header() -> None: + """Check a short header""" + with open(TEST_FILE_DXT5, "rb") as f: + img_file = f.read() + + def short_header() -> None: + with Image.open(BytesIO(img_file[:119])): + pass # pragma: no cover + + with pytest.raises(OSError): + short_header() + + +def test_short_file() -> None: + """Check that the appropriate error is thrown for a short file""" + + with open(TEST_FILE_DXT5, "rb") as f: + img_file = f.read() + + def short_file() -> None: + with Image.open(BytesIO(img_file[:-100])) as im: + im.load() + + with pytest.raises(OSError): + short_file() + + +def test_dxt5_colorblock_alpha_issue_4142() -> None: + """Check that colorblocks are decoded correctly in DXT5""" + + with Image.open("Tests/images/dxt5-colorblock-alpha-issue-4142.dds") as im: + px = im.getpixel((0, 0)) + assert px[0] != 0 + assert px[1] != 0 + assert px[2] != 0 + + px = im.getpixel((1, 0)) + assert px[0] != 0 + assert px[1] != 0 + assert px[2] != 0 + + +def test_palette() -> None: + with Image.open("Tests/images/palette.dds") as im: + assert_image_equal_tofile(im, "Tests/images/transparent.gif") + + +def test_unsupported_bitcount() -> None: + with pytest.raises(OSError): + with Image.open("Tests/images/unsupported_bitcount.dds"): + pass + + +@pytest.mark.parametrize( + "test_file", + ( + "Tests/images/unimplemented_dxgi_format.dds", + "Tests/images/unimplemented_pfflags.dds", + ), +) +def test_not_implemented(test_file: str) -> None: + with pytest.raises(NotImplementedError): + with Image.open(test_file): + pass + + +def test_save_unsupported_mode(tmp_path: Path) -> None: + out = str(tmp_path / "temp.dds") + im = hopper("HSV") + with pytest.raises(OSError): + im.save(out) + + +@pytest.mark.parametrize( + ("mode", "test_file"), + [ + ("L", "Tests/images/linear_gradient.png"), + ("LA", "Tests/images/uncompressed_la.png"), + ("RGB", "Tests/images/hopper.png"), + ("RGBA", "Tests/images/pil123rgba.png"), + ], +) +def test_save(mode: str, test_file: str, tmp_path: Path) -> None: + out = str(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) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 1c21aa8ca..258e02b8f 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -1,445 +1,445 @@ -from __future__ import annotations - -import io -from pathlib import Path - -import pytest - -from PIL import EpsImagePlugin, Image, UnidentifiedImageError, features - -from .helper import ( - assert_image_similar, - assert_image_similar_tofile, - hopper, - is_win32, - mark_if_feature_version, - skip_unless_feature, -) - -HAS_GHOSTSCRIPT = EpsImagePlugin.has_ghostscript() - -# Our two EPS test files (they are identical except for their bounding boxes) -FILE1 = "Tests/images/zero_bb.eps" -FILE2 = "Tests/images/non_zero_bb.eps" - -# Due to palletization, we'll need to convert these to RGB after load -FILE1_COMPARE = "Tests/images/zero_bb.png" -FILE1_COMPARE_SCALE2 = "Tests/images/zero_bb_scale2.png" - -FILE2_COMPARE = "Tests/images/non_zero_bb.png" -FILE2_COMPARE_SCALE2 = "Tests/images/non_zero_bb_scale2.png" - -# EPS test files with binary preview -FILE3 = "Tests/images/binary_preview_map.eps" - -# Three unsigned 32bit little-endian values: -# 0xC6D3D0C5 magic number -# byte position of start of postscript section (12) -# byte length of postscript section (0) -# this byte length isn't valid, but we don't read it -simple_binary_header = b"\xc5\xd0\xd3\xc6\x0c\x00\x00\x00\x00\x00\x00\x00" - -# taken from page 8 of the specification -# https://web.archive.org/web/20220120164601/https://www.adobe.com/content/dam/acom/en/devnet/actionscript/articles/5002.EPSF_Spec.pdf -simple_eps_file = ( - b"%!PS-Adobe-3.0 EPSF-3.0", - b"%%BoundingBox: 5 5 105 105", - b"10 setlinewidth", - b"10 10 moveto", - b"0 90 rlineto 90 0 rlineto 0 -90 rlineto closepath", - b"stroke", -) -simple_eps_file_with_comments = ( - simple_eps_file[:1] - + ( - b"%%Comment1: Some Value", - b"%%SecondComment: Another Value", - ) - + simple_eps_file[1:] -) -simple_eps_file_without_version = simple_eps_file[1:] -simple_eps_file_without_boundingbox = simple_eps_file[:1] + simple_eps_file[2:] -simple_eps_file_with_invalid_boundingbox = ( - simple_eps_file[:1] + (b"%%BoundingBox: a b c d",) + simple_eps_file[2:] -) -simple_eps_file_with_invalid_boundingbox_valid_imagedata = ( - simple_eps_file_with_invalid_boundingbox + (b"%ImageData: 100 100 8 3",) -) -simple_eps_file_with_long_ascii_comment = ( - simple_eps_file[:2] + (b"%%Comment: " + b"X" * 300,) + simple_eps_file[2:] -) -simple_eps_file_with_long_binary_data = ( - simple_eps_file[:2] - + ( - b"%%BeginBinary: 300", - b"\0" * 300, - b"%%EndBinary", - ) - + simple_eps_file[2:] -) - - -@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -@pytest.mark.parametrize( - ("filename", "size"), ((FILE1, (460, 352)), (FILE2, (360, 252))) -) -@pytest.mark.parametrize("scale", (1, 2)) -def test_sanity(filename: str, size: tuple[int, int], scale: int) -> None: - expected_size = tuple(s * scale for s in size) - with Image.open(filename) as image: - image.load(scale=scale) - assert image.mode == "RGB" - assert image.size == expected_size - assert image.format == "EPS" - - -@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) - - # Test again now that it has already been loaded once - assert im.load()[0, 0] == (255, 255, 255) - - -def test_binary() -> None: - if HAS_GHOSTSCRIPT: - assert EpsImagePlugin.gs_binary is not None - else: - assert EpsImagePlugin.gs_binary is False - - if not is_win32(): - assert EpsImagePlugin.gs_windows_binary is None - elif not HAS_GHOSTSCRIPT: - assert EpsImagePlugin.gs_windows_binary is False - else: - assert EpsImagePlugin.gs_windows_binary is not None - - -def test_invalid_file() -> None: - invalid_file = "Tests/images/flower.jpg" - with pytest.raises(SyntaxError): - EpsImagePlugin.EpsImageFile(invalid_file) - - -def test_binary_header_only() -> None: - data = io.BytesIO(simple_binary_header) - with pytest.raises(SyntaxError, match='EPS header missing "%!PS-Adobe" comment'): - EpsImagePlugin.EpsImageFile(data) - - -@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_missing_version_comment(prefix: bytes) -> None: - data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_version)) - with pytest.raises(SyntaxError): - EpsImagePlugin.EpsImageFile(data) - - -@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_missing_boundingbox_comment(prefix: bytes) -> None: - data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_boundingbox)) - with pytest.raises(SyntaxError, match='EPS header missing "%%BoundingBox" comment'): - EpsImagePlugin.EpsImageFile(data) - - -@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_invalid_boundingbox_comment(prefix: bytes) -> None: - data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox)) - with pytest.raises(OSError, match="cannot determine EPS bounding box"): - EpsImagePlugin.EpsImageFile(data) - - -@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_invalid_boundingbox_comment_valid_imagedata_comment(prefix: bytes) -> None: - data = io.BytesIO( - prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox_valid_imagedata) - ) - with Image.open(data) as img: - assert img.mode == "RGB" - assert img.size == (100, 100) - assert img.format == "EPS" - - -@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_ascii_comment_too_long(prefix: bytes) -> None: - data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_ascii_comment)) - with pytest.raises(SyntaxError, match="not an EPS file"): - EpsImagePlugin.EpsImageFile(data) - - -@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_long_binary_data(prefix: bytes) -> None: - data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data)) - EpsImagePlugin.EpsImageFile(data) - - -@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_load_long_binary_data(prefix: bytes) -> None: - data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data)) - with Image.open(data) as img: - img.load() - assert img.mode == "RGB" - assert img.size == (100, 100) - assert img.format == "EPS" - - -@mark_if_feature_version( - pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" -) -@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -def test_cmyk() -> None: - with Image.open("Tests/images/pil_sample_cmyk.eps") as cmyk_image: - assert cmyk_image.mode == "CMYK" - assert cmyk_image.size == (100, 100) - assert cmyk_image.format == "EPS" - - cmyk_image.load() - assert cmyk_image.mode == "RGB" - - if features.check("jpg"): - assert_image_similar_tofile( - cmyk_image, "Tests/images/pil_sample_rgb.jpg", 10 - ) - - -@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -def test_showpage() -> None: - # See https://github.com/python-pillow/Pillow/issues/2615 - with Image.open("Tests/images/reqd_showpage.eps") as plot_image: - with Image.open("Tests/images/reqd_showpage.png") as target: - # should not crash/hang - plot_image.load() - # fonts could be slightly different - assert_image_similar(plot_image, target, 6) - - -@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -def test_transparency() -> None: - with Image.open("Tests/images/reqd_showpage.eps") as plot_image: - plot_image.load(transparency=True) - assert plot_image.mode == "RGBA" - - with Image.open("Tests/images/reqd_showpage_transparency.png") as target: - # fonts could be slightly different - assert_image_similar(plot_image, target, 6) - - -@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -def test_file_object(tmp_path: Path) -> None: - # issue 479 - with Image.open(FILE1) as image1: - with open(str(tmp_path / "temp.eps"), "wb") as fh: - image1.save(fh, "EPS") - - -@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -def test_bytesio_object() -> None: - with open(FILE1, "rb") as f: - img_bytes = io.BytesIO(f.read()) - - with Image.open(img_bytes) as img: - img.load() - - with Image.open(FILE1_COMPARE) as image1_scale1_compare: - image1_scale1_compare = image1_scale1_compare.convert("RGB") - image1_scale1_compare.load() - assert_image_similar(img, image1_scale1_compare, 5) - - -def test_1_mode() -> None: - with Image.open("Tests/images/1.eps") as im: - assert im.mode == "1" - - -def test_image_mode_not_supported(tmp_path: Path) -> None: - im = hopper("RGBA") - tmpfile = str(tmp_path / "temp.eps") - with pytest.raises(ValueError): - im.save(tmpfile) - - -@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -@skip_unless_feature("zlib") -def test_render_scale1() -> None: - # We need png support for these render test - - # Zero bounding box - with Image.open(FILE1) as image1_scale1: - image1_scale1.load() - with Image.open(FILE1_COMPARE) as image1_scale1_compare: - image1_scale1_compare = image1_scale1_compare.convert("RGB") - image1_scale1_compare.load() - assert_image_similar(image1_scale1, image1_scale1_compare, 5) - - # Non-zero bounding box - with Image.open(FILE2) as image2_scale1: - image2_scale1.load() - with Image.open(FILE2_COMPARE) as image2_scale1_compare: - image2_scale1_compare = image2_scale1_compare.convert("RGB") - image2_scale1_compare.load() - assert_image_similar(image2_scale1, image2_scale1_compare, 10) - - -@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -@skip_unless_feature("zlib") -def test_render_scale2() -> None: - # We need png support for these render test - - # Zero bounding box - with Image.open(FILE1) as image1_scale2: - image1_scale2.load(scale=2) - with Image.open(FILE1_COMPARE_SCALE2) as image1_scale2_compare: - image1_scale2_compare = image1_scale2_compare.convert("RGB") - image1_scale2_compare.load() - assert_image_similar(image1_scale2, image1_scale2_compare, 5) - - # Non-zero bounding box - with Image.open(FILE2) as image2_scale2: - image2_scale2.load(scale=2) - with Image.open(FILE2_COMPARE_SCALE2) as image2_scale2_compare: - image2_scale2_compare = image2_scale2_compare.convert("RGB") - image2_scale2_compare.load() - assert_image_similar(image2_scale2, image2_scale2_compare, 10) - - -@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -@pytest.mark.parametrize("filename", (FILE1, FILE2, "Tests/images/illu10_preview.eps")) -def test_resize(filename: str) -> None: - with Image.open(filename) as im: - new_size = (100, 100) - im = im.resize(new_size) - assert im.size == new_size - - -@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -@pytest.mark.parametrize("filename", (FILE1, FILE2)) -def test_thumbnail(filename: str) -> None: - # Issue #619 - with Image.open(filename) as im: - new_size = (100, 100) - im.thumbnail(new_size) - assert max(im.size) == max(new_size) - - -def test_read_binary_preview() -> None: - # Issue 302 - # open image with binary preview - with Image.open(FILE3): - pass - - -def test_readline_psfile(tmp_path: Path) -> None: - # check all the freaking line endings possible from the spec - # test_string = u'something\r\nelse\n\rbaz\rbif\n' - line_endings = ["\r\n", "\n", "\n\r", "\r"] - strings = ["something", "else", "baz", "bif"] - - def _test_readline(t: EpsImagePlugin.PSFile, ending: str) -> None: - ending = f"Failure with line ending: {''.join(str(ord(s)) for s in ending)}" - assert t.readline().strip("\r\n") == "something", ending - assert t.readline().strip("\r\n") == "else", ending - assert t.readline().strip("\r\n") == "baz", ending - assert t.readline().strip("\r\n") == "bif", ending - - def _test_readline_io_psfile(test_string: str, ending: str) -> None: - f = io.BytesIO(test_string.encode("latin-1")) - with pytest.warns(DeprecationWarning): - t = EpsImagePlugin.PSFile(f) - _test_readline(t, ending) - - def _test_readline_file_psfile(test_string: str, ending: str) -> None: - f = str(tmp_path / "temp.txt") - with open(f, "wb") as w: - w.write(test_string.encode("latin-1")) - - with open(f, "rb") as r: - with pytest.warns(DeprecationWarning): - t = EpsImagePlugin.PSFile(r) - _test_readline(t, ending) - - for ending in line_endings: - s = ending.join(strings) - _test_readline_io_psfile(s, ending) - _test_readline_file_psfile(s, ending) - - -def test_psfile_deprecation() -> None: - with pytest.warns(DeprecationWarning): - EpsImagePlugin.PSFile(None) - - -@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -@pytest.mark.parametrize( - "line_ending", - (b"\r\n", b"\n", b"\n\r", b"\r"), -) -def test_readline(prefix: bytes, line_ending: bytes) -> None: - simple_file = prefix + line_ending.join(simple_eps_file_with_comments) - data = io.BytesIO(simple_file) - test_file = EpsImagePlugin.EpsImageFile(data) - assert test_file.info["Comment1"] == "Some Value" - assert test_file.info["SecondComment"] == "Another Value" - assert test_file.size == (100, 100) - - -@pytest.mark.parametrize( - "filename", - ( - "Tests/images/illu10_no_preview.eps", - "Tests/images/illu10_preview.eps", - "Tests/images/illuCS6_no_preview.eps", - "Tests/images/illuCS6_preview.eps", - ), -) -def test_open_eps(filename: str) -> None: - # https://github.com/python-pillow/Pillow/issues/1104 - with Image.open(filename) as img: - assert img.mode == "RGB" - - -@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -def test_emptyline() -> None: - # Test file includes an empty line in the header data - emptyline_file = "Tests/images/zero_bb_emptyline.eps" - - with Image.open(emptyline_file) as image: - image.load() - assert image.mode == "RGB" - assert image.size == (460, 352) - assert image.format == "EPS" - - -@pytest.mark.timeout(timeout=5) -@pytest.mark.parametrize( - "test_file", - ["Tests/images/timeout-d675703545fee17acab56e5fec644c19979175de.eps"], -) -def test_timeout(test_file: str) -> None: - with open(test_file, "rb") as f: - with pytest.raises(UnidentifiedImageError): - with Image.open(f): - pass - - -def test_bounding_box_in_trailer() -> None: - # Check bounding boxes are parsed in the same way - # when specified in the header and the trailer - with Image.open("Tests/images/zero_bb_trailer.eps") as trailer_image, Image.open( - FILE1 - ) as header_image: - assert trailer_image.size == header_image.size - - -def test_eof_before_bounding_box() -> None: - with pytest.raises(OSError): - with Image.open("Tests/images/zero_bb_eof_before_boundingbox.eps"): - pass - - -def test_invalid_data_after_eof() -> None: - with open("Tests/images/illuCS6_preview.eps", "rb") as f: - img_bytes = io.BytesIO(f.read() + b"\r\n%" + (b" " * 255)) - - with Image.open(img_bytes) as img: - assert img.mode == "RGB" +from __future__ import annotations + +import io +from pathlib import Path + +import pytest + +from PIL import EpsImagePlugin, Image, UnidentifiedImageError, features + +from .helper import ( + assert_image_similar, + assert_image_similar_tofile, + hopper, + is_win32, + mark_if_feature_version, + skip_unless_feature, +) + +HAS_GHOSTSCRIPT = EpsImagePlugin.has_ghostscript() + +# Our two EPS test files (they are identical except for their bounding boxes) +FILE1 = "Tests/images/zero_bb.eps" +FILE2 = "Tests/images/non_zero_bb.eps" + +# Due to palletization, we'll need to convert these to RGB after load +FILE1_COMPARE = "Tests/images/zero_bb.png" +FILE1_COMPARE_SCALE2 = "Tests/images/zero_bb_scale2.png" + +FILE2_COMPARE = "Tests/images/non_zero_bb.png" +FILE2_COMPARE_SCALE2 = "Tests/images/non_zero_bb_scale2.png" + +# EPS test files with binary preview +FILE3 = "Tests/images/binary_preview_map.eps" + +# Three unsigned 32bit little-endian values: +# 0xC6D3D0C5 magic number +# byte position of start of postscript section (12) +# byte length of postscript section (0) +# this byte length isn't valid, but we don't read it +simple_binary_header = b"\xc5\xd0\xd3\xc6\x0c\x00\x00\x00\x00\x00\x00\x00" + +# taken from page 8 of the specification +# https://web.archive.org/web/20220120164601/https://www.adobe.com/content/dam/acom/en/devnet/actionscript/articles/5002.EPSF_Spec.pdf +simple_eps_file = ( + b"%!PS-Adobe-3.0 EPSF-3.0", + b"%%BoundingBox: 5 5 105 105", + b"10 setlinewidth", + b"10 10 moveto", + b"0 90 rlineto 90 0 rlineto 0 -90 rlineto closepath", + b"stroke", +) +simple_eps_file_with_comments = ( + simple_eps_file[:1] + + ( + b"%%Comment1: Some Value", + b"%%SecondComment: Another Value", + ) + + simple_eps_file[1:] +) +simple_eps_file_without_version = simple_eps_file[1:] +simple_eps_file_without_boundingbox = simple_eps_file[:1] + simple_eps_file[2:] +simple_eps_file_with_invalid_boundingbox = ( + simple_eps_file[:1] + (b"%%BoundingBox: a b c d",) + simple_eps_file[2:] +) +simple_eps_file_with_invalid_boundingbox_valid_imagedata = ( + simple_eps_file_with_invalid_boundingbox + (b"%ImageData: 100 100 8 3",) +) +simple_eps_file_with_long_ascii_comment = ( + simple_eps_file[:2] + (b"%%Comment: " + b"X" * 300,) + simple_eps_file[2:] +) +simple_eps_file_with_long_binary_data = ( + simple_eps_file[:2] + + ( + b"%%BeginBinary: 300", + b"\0" * 300, + b"%%EndBinary", + ) + + simple_eps_file[2:] +) + + +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +@pytest.mark.parametrize( + ("filename", "size"), ((FILE1, (460, 352)), (FILE2, (360, 252))) +) +@pytest.mark.parametrize("scale", (1, 2)) +def test_sanity(filename: str, size: tuple[int, int], scale: int) -> None: + expected_size = tuple(s * scale for s in size) + with Image.open(filename) as image: + image.load(scale=scale) + assert image.mode == "RGB" + assert image.size == expected_size + assert image.format == "EPS" + + +@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) + + # Test again now that it has already been loaded once + assert im.load()[0, 0] == (255, 255, 255) + + +def test_binary() -> None: + if HAS_GHOSTSCRIPT: + assert EpsImagePlugin.gs_binary is not None + else: + assert EpsImagePlugin.gs_binary is False + + if not is_win32(): + assert EpsImagePlugin.gs_windows_binary is None + elif not HAS_GHOSTSCRIPT: + assert EpsImagePlugin.gs_windows_binary is False + else: + assert EpsImagePlugin.gs_windows_binary is not None + + +def test_invalid_file() -> None: + invalid_file = "Tests/images/flower.jpg" + with pytest.raises(SyntaxError): + EpsImagePlugin.EpsImageFile(invalid_file) + + +def test_binary_header_only() -> None: + data = io.BytesIO(simple_binary_header) + with pytest.raises(SyntaxError, match='EPS header missing "%!PS-Adobe" comment'): + EpsImagePlugin.EpsImageFile(data) + + +@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) +def test_missing_version_comment(prefix: bytes) -> None: + data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_version)) + with pytest.raises(SyntaxError): + EpsImagePlugin.EpsImageFile(data) + + +@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) +def test_missing_boundingbox_comment(prefix: bytes) -> None: + data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_boundingbox)) + with pytest.raises(SyntaxError, match='EPS header missing "%%BoundingBox" comment'): + EpsImagePlugin.EpsImageFile(data) + + +@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) +def test_invalid_boundingbox_comment(prefix: bytes) -> None: + data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox)) + with pytest.raises(OSError, match="cannot determine EPS bounding box"): + EpsImagePlugin.EpsImageFile(data) + + +@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) +def test_invalid_boundingbox_comment_valid_imagedata_comment(prefix: bytes) -> None: + data = io.BytesIO( + prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox_valid_imagedata) + ) + with Image.open(data) as img: + assert img.mode == "RGB" + assert img.size == (100, 100) + assert img.format == "EPS" + + +@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) +def test_ascii_comment_too_long(prefix: bytes) -> None: + data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_ascii_comment)) + with pytest.raises(SyntaxError, match="not an EPS file"): + EpsImagePlugin.EpsImageFile(data) + + +@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) +def test_long_binary_data(prefix: bytes) -> None: + data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data)) + EpsImagePlugin.EpsImageFile(data) + + +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) +def test_load_long_binary_data(prefix: bytes) -> None: + data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data)) + with Image.open(data) as img: + img.load() + assert img.mode == "RGB" + assert img.size == (100, 100) + assert img.format == "EPS" + + +@mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" +) +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +def test_cmyk() -> None: + with Image.open("Tests/images/pil_sample_cmyk.eps") as cmyk_image: + assert cmyk_image.mode == "CMYK" + assert cmyk_image.size == (100, 100) + assert cmyk_image.format == "EPS" + + cmyk_image.load() + assert cmyk_image.mode == "RGB" + + if features.check("jpg"): + assert_image_similar_tofile( + cmyk_image, "Tests/images/pil_sample_rgb.jpg", 10 + ) + + +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +def test_showpage() -> None: + # See https://github.com/python-pillow/Pillow/issues/2615 + with Image.open("Tests/images/reqd_showpage.eps") as plot_image: + with Image.open("Tests/images/reqd_showpage.png") as target: + # should not crash/hang + plot_image.load() + # fonts could be slightly different + assert_image_similar(plot_image, target, 6) + + +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +def test_transparency() -> None: + with Image.open("Tests/images/reqd_showpage.eps") as plot_image: + plot_image.load(transparency=True) + assert plot_image.mode == "RGBA" + + with Image.open("Tests/images/reqd_showpage_transparency.png") as target: + # fonts could be slightly different + assert_image_similar(plot_image, target, 6) + + +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +def test_file_object(tmp_path: Path) -> None: + # issue 479 + with Image.open(FILE1) as image1: + with open(str(tmp_path / "temp.eps"), "wb") as fh: + image1.save(fh, "EPS") + + +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +def test_bytesio_object() -> None: + with open(FILE1, "rb") as f: + img_bytes = io.BytesIO(f.read()) + + with Image.open(img_bytes) as img: + img.load() + + with Image.open(FILE1_COMPARE) as image1_scale1_compare: + image1_scale1_compare = image1_scale1_compare.convert("RGB") + image1_scale1_compare.load() + assert_image_similar(img, image1_scale1_compare, 5) + + +def test_1_mode() -> None: + with Image.open("Tests/images/1.eps") as im: + assert im.mode == "1" + + +def test_image_mode_not_supported(tmp_path: Path) -> None: + im = hopper("RGBA") + tmpfile = str(tmp_path / "temp.eps") + with pytest.raises(ValueError): + im.save(tmpfile) + + +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +@skip_unless_feature("zlib") +def test_render_scale1() -> None: + # We need png support for these render test + + # Zero bounding box + with Image.open(FILE1) as image1_scale1: + image1_scale1.load() + with Image.open(FILE1_COMPARE) as image1_scale1_compare: + image1_scale1_compare = image1_scale1_compare.convert("RGB") + image1_scale1_compare.load() + assert_image_similar(image1_scale1, image1_scale1_compare, 5) + + # Non-zero bounding box + with Image.open(FILE2) as image2_scale1: + image2_scale1.load() + with Image.open(FILE2_COMPARE) as image2_scale1_compare: + image2_scale1_compare = image2_scale1_compare.convert("RGB") + image2_scale1_compare.load() + assert_image_similar(image2_scale1, image2_scale1_compare, 10) + + +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +@skip_unless_feature("zlib") +def test_render_scale2() -> None: + # We need png support for these render test + + # Zero bounding box + with Image.open(FILE1) as image1_scale2: + image1_scale2.load(scale=2) + with Image.open(FILE1_COMPARE_SCALE2) as image1_scale2_compare: + image1_scale2_compare = image1_scale2_compare.convert("RGB") + image1_scale2_compare.load() + assert_image_similar(image1_scale2, image1_scale2_compare, 5) + + # Non-zero bounding box + with Image.open(FILE2) as image2_scale2: + image2_scale2.load(scale=2) + with Image.open(FILE2_COMPARE_SCALE2) as image2_scale2_compare: + image2_scale2_compare = image2_scale2_compare.convert("RGB") + image2_scale2_compare.load() + assert_image_similar(image2_scale2, image2_scale2_compare, 10) + + +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +@pytest.mark.parametrize("filename", (FILE1, FILE2, "Tests/images/illu10_preview.eps")) +def test_resize(filename: str) -> None: + with Image.open(filename) as im: + new_size = (100, 100) + im = im.resize(new_size) + assert im.size == new_size + + +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +@pytest.mark.parametrize("filename", (FILE1, FILE2)) +def test_thumbnail(filename: str) -> None: + # Issue #619 + with Image.open(filename) as im: + new_size = (100, 100) + im.thumbnail(new_size) + assert max(im.size) == max(new_size) + + +def test_read_binary_preview() -> None: + # Issue 302 + # open image with binary preview + with Image.open(FILE3): + pass + + +def test_readline_psfile(tmp_path: Path) -> None: + # check all the freaking line endings possible from the spec + # test_string = u'something\r\nelse\n\rbaz\rbif\n' + line_endings = ["\r\n", "\n", "\n\r", "\r"] + strings = ["something", "else", "baz", "bif"] + + def _test_readline(t: EpsImagePlugin.PSFile, ending: str) -> None: + ending = f"Failure with line ending: {''.join(str(ord(s)) for s in ending)}" + assert t.readline().strip("\r\n") == "something", ending + assert t.readline().strip("\r\n") == "else", ending + assert t.readline().strip("\r\n") == "baz", ending + assert t.readline().strip("\r\n") == "bif", ending + + def _test_readline_io_psfile(test_string: str, ending: str) -> None: + f = io.BytesIO(test_string.encode("latin-1")) + with pytest.warns(DeprecationWarning): + t = EpsImagePlugin.PSFile(f) + _test_readline(t, ending) + + def _test_readline_file_psfile(test_string: str, ending: str) -> None: + f = str(tmp_path / "temp.txt") + with open(f, "wb") as w: + w.write(test_string.encode("latin-1")) + + with open(f, "rb") as r: + with pytest.warns(DeprecationWarning): + t = EpsImagePlugin.PSFile(r) + _test_readline(t, ending) + + for ending in line_endings: + s = ending.join(strings) + _test_readline_io_psfile(s, ending) + _test_readline_file_psfile(s, ending) + + +def test_psfile_deprecation() -> None: + with pytest.warns(DeprecationWarning): + EpsImagePlugin.PSFile(None) + + +@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) +@pytest.mark.parametrize( + "line_ending", + (b"\r\n", b"\n", b"\n\r", b"\r"), +) +def test_readline(prefix: bytes, line_ending: bytes) -> None: + simple_file = prefix + line_ending.join(simple_eps_file_with_comments) + data = io.BytesIO(simple_file) + test_file = EpsImagePlugin.EpsImageFile(data) + assert test_file.info["Comment1"] == "Some Value" + assert test_file.info["SecondComment"] == "Another Value" + assert test_file.size == (100, 100) + + +@pytest.mark.parametrize( + "filename", + ( + "Tests/images/illu10_no_preview.eps", + "Tests/images/illu10_preview.eps", + "Tests/images/illuCS6_no_preview.eps", + "Tests/images/illuCS6_preview.eps", + ), +) +def test_open_eps(filename: str) -> None: + # https://github.com/python-pillow/Pillow/issues/1104 + with Image.open(filename) as img: + assert img.mode == "RGB" + + +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +def test_emptyline() -> None: + # Test file includes an empty line in the header data + emptyline_file = "Tests/images/zero_bb_emptyline.eps" + + with Image.open(emptyline_file) as image: + image.load() + assert image.mode == "RGB" + assert image.size == (460, 352) + assert image.format == "EPS" + + +@pytest.mark.timeout(timeout=5) +@pytest.mark.parametrize( + "test_file", + ["Tests/images/timeout-d675703545fee17acab56e5fec644c19979175de.eps"], +) +def test_timeout(test_file: str) -> None: + with open(test_file, "rb") as f: + with pytest.raises(UnidentifiedImageError): + with Image.open(f): + pass + + +def test_bounding_box_in_trailer() -> None: + # Check bounding boxes are parsed in the same way + # when specified in the header and the trailer + with Image.open("Tests/images/zero_bb_trailer.eps") as trailer_image, Image.open( + FILE1 + ) as header_image: + assert trailer_image.size == header_image.size + + +def test_eof_before_bounding_box() -> None: + with pytest.raises(OSError): + with Image.open("Tests/images/zero_bb_eof_before_boundingbox.eps"): + pass + + +def test_invalid_data_after_eof() -> None: + with open("Tests/images/illuCS6_preview.eps", "rb") as f: + img_bytes = io.BytesIO(f.read() + b"\r\n%" + (b" " * 255)) + + with Image.open(img_bytes) as img: + assert img.mode == "RGB" diff --git a/Tests/test_file_fits.py b/Tests/test_file_fits.py index 1c1df0d98..b907a82b1 100644 --- a/Tests/test_file_fits.py +++ b/Tests/test_file_fits.py @@ -1,57 +1,57 @@ -from __future__ import annotations - -from io import BytesIO - -import pytest - -from PIL import FitsImagePlugin, Image - -from .helper import assert_image_equal, assert_image_equal_tofile, hopper - -TEST_FILE = "Tests/images/hopper.fits" - - -def test_open() -> None: - # Act - with Image.open(TEST_FILE) as im: - # Assert - assert im.format == "FITS" - assert im.size == (128, 128) - assert im.mode == "L" - - assert_image_equal(im, hopper("L")) - - -def test_gzip1() -> None: - with Image.open("Tests/images/m13_gzip.fits") as im: - assert_image_equal_tofile(im, "Tests/images/m13.fits") - - -def test_invalid_file() -> None: - # Arrange - invalid_file = "Tests/images/flower.jpg" - - # Act / Assert - with pytest.raises(SyntaxError): - FitsImagePlugin.FitsImageFile(invalid_file) - - -def test_truncated_fits() -> None: - # No END to headers - image_data = b"SIMPLE = T" + b" " * 50 + b"TRUNCATE" - with pytest.raises(OSError): - FitsImagePlugin.FitsImageFile(BytesIO(image_data)) - - -def test_naxis_zero() -> None: - # This test image has been manually hexedited - # to set the number of data axes to zero - with pytest.raises(ValueError): - with Image.open("Tests/images/hopper_naxis_zero.fits"): - pass - - -def test_comment() -> None: - image_data = b"SIMPLE = T / comment string" - with pytest.raises(OSError): - FitsImagePlugin.FitsImageFile(BytesIO(image_data)) +from __future__ import annotations + +from io import BytesIO + +import pytest + +from PIL import FitsImagePlugin, Image + +from .helper import assert_image_equal, assert_image_equal_tofile, hopper + +TEST_FILE = "Tests/images/hopper.fits" + + +def test_open() -> None: + # Act + with Image.open(TEST_FILE) as im: + # Assert + assert im.format == "FITS" + assert im.size == (128, 128) + assert im.mode == "L" + + assert_image_equal(im, hopper("L")) + + +def test_gzip1() -> None: + with Image.open("Tests/images/m13_gzip.fits") as im: + assert_image_equal_tofile(im, "Tests/images/m13.fits") + + +def test_invalid_file() -> None: + # Arrange + invalid_file = "Tests/images/flower.jpg" + + # Act / Assert + with pytest.raises(SyntaxError): + FitsImagePlugin.FitsImageFile(invalid_file) + + +def test_truncated_fits() -> None: + # No END to headers + image_data = b"SIMPLE = T" + b" " * 50 + b"TRUNCATE" + with pytest.raises(OSError): + FitsImagePlugin.FitsImageFile(BytesIO(image_data)) + + +def test_naxis_zero() -> None: + # This test image has been manually hexedited + # to set the number of data axes to zero + with pytest.raises(ValueError): + with Image.open("Tests/images/hopper_naxis_zero.fits"): + pass + + +def test_comment() -> None: + image_data = b"SIMPLE = T / comment string" + with pytest.raises(OSError): + FitsImagePlugin.FitsImageFile(BytesIO(image_data)) diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index f86fb8d09..97efd32f2 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -1,188 +1,188 @@ -from __future__ import annotations - -import warnings - -import pytest - -from PIL import FliImagePlugin, Image, ImageFile - -from .helper import assert_image_equal, assert_image_equal_tofile, is_pypy - -# created as an export of a palette image from Gimp2.6 -# save as...-> hopper.fli, default options. -static_test_file = "Tests/images/hopper.fli" - -# From https://samples.ffmpeg.org/fli-flc/ -animated_test_file = "Tests/images/a.fli" - -# From https://samples.ffmpeg.org/fli-flc/ -animated_test_file_with_prefix_chunk = "Tests/images/2422.flc" - - -def test_sanity() -> None: - with Image.open(static_test_file) as im: - im.load() - assert im.mode == "P" - assert im.size == (128, 128) - assert im.format == "FLI" - assert not im.is_animated - - with Image.open(animated_test_file) as im: - assert im.mode == "P" - assert im.size == (320, 200) - assert im.format == "FLI" - assert im.info["duration"] == 71 - assert im.is_animated - - -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 - - 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 - - -@pytest.mark.skipif(is_pypy(), reason="Requires CPython") -def test_unclosed_file() -> None: - def open() -> None: - im = Image.open(static_test_file) - im.load() - - with pytest.warns(ResourceWarning): - open() - - -def test_closed_file() -> None: - with warnings.catch_warnings(): - im = Image.open(static_test_file) - im.load() - im.close() - - -def test_seek_after_close() -> None: - im = Image.open(animated_test_file) - im.seek(1) - im.close() - - with pytest.raises(ValueError): - im.seek(0) - - -def test_context_manager() -> None: - with warnings.catch_warnings(): - with Image.open(static_test_file) as im: - im.load() - - -def test_tell() -> None: - # Arrange - with Image.open(static_test_file) as im: - # Act - frame = im.tell() - - # Assert - assert frame == 0 - - -def test_invalid_file() -> None: - invalid_file = "Tests/images/flower.jpg" - - with pytest.raises(SyntaxError): - FliImagePlugin.FliImageFile(invalid_file) - - -def test_palette_chunk_second() -> None: - with Image.open("Tests/images/hopper_palette_chunk_second.fli") as im: - with Image.open(static_test_file) as expected: - assert_image_equal(im.convert("RGB"), expected.convert("RGB")) - - -def test_n_frames() -> None: - with Image.open(static_test_file) as im: - assert im.n_frames == 1 - assert not im.is_animated - - with Image.open(animated_test_file) as im: - assert im.n_frames == 384 - assert im.is_animated - - -def test_eoferror() -> None: - with Image.open(animated_test_file) as im: - n_frames = im.n_frames - - # Test seeking past the last frame - with pytest.raises(EOFError): - im.seek(n_frames) - assert im.tell() < n_frames - - # Test that seeking to the last frame does not raise an error - im.seek(n_frames - 1) - - -def test_seek_tell() -> None: - with Image.open(animated_test_file) as im: - layer_number = im.tell() - assert layer_number == 0 - - im.seek(0) - layer_number = im.tell() - assert layer_number == 0 - - im.seek(1) - layer_number = im.tell() - assert layer_number == 1 - - im.seek(2) - layer_number = im.tell() - assert layer_number == 2 - - im.seek(1) - layer_number = im.tell() - assert layer_number == 1 - - -def test_seek() -> None: - with Image.open(animated_test_file) as im: - im.seek(50) - - assert_image_equal_tofile(im, "Tests/images/a_fli.png") - - -@pytest.mark.parametrize( - "test_file", - [ - "Tests/images/timeout-9139147ce93e20eb14088fe238e541443ffd64b3.fli", - "Tests/images/timeout-bff0a9dc7243a8e6ede2408d2ffa6a9964698b87.fli", - ], -) -@pytest.mark.timeout(timeout=3) -def test_timeouts(test_file: str) -> None: - with open(test_file, "rb") as f: - with Image.open(f) as im: - with pytest.raises(OSError): - im.load() - - -@pytest.mark.parametrize( - "test_file", - [ - "Tests/images/crash-5762152299364352.fli", - ], -) -def test_crash(test_file: str) -> None: - with open(test_file, "rb") as f: - with Image.open(f) as im: - with pytest.raises(OSError): - im.load() +from __future__ import annotations + +import warnings + +import pytest + +from PIL import FliImagePlugin, Image, ImageFile + +from .helper import assert_image_equal, assert_image_equal_tofile, is_pypy + +# created as an export of a palette image from Gimp2.6 +# save as...-> hopper.fli, default options. +static_test_file = "Tests/images/hopper.fli" + +# From https://samples.ffmpeg.org/fli-flc/ +animated_test_file = "Tests/images/a.fli" + +# From https://samples.ffmpeg.org/fli-flc/ +animated_test_file_with_prefix_chunk = "Tests/images/2422.flc" + + +def test_sanity() -> None: + with Image.open(static_test_file) as im: + im.load() + assert im.mode == "P" + assert im.size == (128, 128) + assert im.format == "FLI" + assert not im.is_animated + + with Image.open(animated_test_file) as im: + assert im.mode == "P" + assert im.size == (320, 200) + assert im.format == "FLI" + assert im.info["duration"] == 71 + assert im.is_animated + + +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 + + 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 + + +@pytest.mark.skipif(is_pypy(), reason="Requires CPython") +def test_unclosed_file() -> None: + def open() -> None: + im = Image.open(static_test_file) + im.load() + + with pytest.warns(ResourceWarning): + open() + + +def test_closed_file() -> None: + with warnings.catch_warnings(): + im = Image.open(static_test_file) + im.load() + im.close() + + +def test_seek_after_close() -> None: + im = Image.open(animated_test_file) + im.seek(1) + im.close() + + with pytest.raises(ValueError): + im.seek(0) + + +def test_context_manager() -> None: + with warnings.catch_warnings(): + with Image.open(static_test_file) as im: + im.load() + + +def test_tell() -> None: + # Arrange + with Image.open(static_test_file) as im: + # Act + frame = im.tell() + + # Assert + assert frame == 0 + + +def test_invalid_file() -> None: + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(SyntaxError): + FliImagePlugin.FliImageFile(invalid_file) + + +def test_palette_chunk_second() -> None: + with Image.open("Tests/images/hopper_palette_chunk_second.fli") as im: + with Image.open(static_test_file) as expected: + assert_image_equal(im.convert("RGB"), expected.convert("RGB")) + + +def test_n_frames() -> None: + with Image.open(static_test_file) as im: + assert im.n_frames == 1 + assert not im.is_animated + + with Image.open(animated_test_file) as im: + assert im.n_frames == 384 + assert im.is_animated + + +def test_eoferror() -> None: + with Image.open(animated_test_file) as im: + n_frames = im.n_frames + + # Test seeking past the last frame + with pytest.raises(EOFError): + im.seek(n_frames) + assert im.tell() < n_frames + + # Test that seeking to the last frame does not raise an error + im.seek(n_frames - 1) + + +def test_seek_tell() -> None: + with Image.open(animated_test_file) as im: + layer_number = im.tell() + assert layer_number == 0 + + im.seek(0) + layer_number = im.tell() + assert layer_number == 0 + + im.seek(1) + layer_number = im.tell() + assert layer_number == 1 + + im.seek(2) + layer_number = im.tell() + assert layer_number == 2 + + im.seek(1) + layer_number = im.tell() + assert layer_number == 1 + + +def test_seek() -> None: + with Image.open(animated_test_file) as im: + im.seek(50) + + assert_image_equal_tofile(im, "Tests/images/a_fli.png") + + +@pytest.mark.parametrize( + "test_file", + [ + "Tests/images/timeout-9139147ce93e20eb14088fe238e541443ffd64b3.fli", + "Tests/images/timeout-bff0a9dc7243a8e6ede2408d2ffa6a9964698b87.fli", + ], +) +@pytest.mark.timeout(timeout=3) +def test_timeouts(test_file: str) -> None: + with open(test_file, "rb") as f: + with Image.open(f) as im: + with pytest.raises(OSError): + im.load() + + +@pytest.mark.parametrize( + "test_file", + [ + "Tests/images/crash-5762152299364352.fli", + ], +) +def test_crash(test_file: str) -> None: + with open(test_file, "rb") as f: + with Image.open(f) as im: + with pytest.raises(OSError): + im.load() diff --git a/Tests/test_file_fpx.py b/Tests/test_file_fpx.py index e32f30a01..72bf56232 100644 --- a/Tests/test_file_fpx.py +++ b/Tests/test_file_fpx.py @@ -1,48 +1,48 @@ -from __future__ import annotations - -import pytest - -from PIL import Image - -from .helper import assert_image_equal_tofile - -FpxImagePlugin = pytest.importorskip( - "PIL.FpxImagePlugin", reason="olefile not installed" -) - - -def test_sanity() -> None: - with Image.open("Tests/images/input_bw_one_band.fpx") as im: - assert im.mode == "L" - assert im.size == (70, 46) - assert im.format == "FPX" - - assert_image_equal_tofile(im, "Tests/images/input_bw_one_band.png") - - -def test_close() -> None: - with Image.open("Tests/images/input_bw_one_band.fpx") as im: - pass - assert im.ole.fp.closed - - im = Image.open("Tests/images/input_bw_one_band.fpx") - im.close() - assert im.ole.fp.closed - - -def test_invalid_file() -> None: - # Test an invalid OLE file - invalid_file = "Tests/images/flower.jpg" - with pytest.raises(SyntaxError): - FpxImagePlugin.FpxImageFile(invalid_file) - - # Test a valid OLE file, but not an FPX file - ole_file = "Tests/images/test-ole-file.doc" - with pytest.raises(SyntaxError): - FpxImagePlugin.FpxImageFile(ole_file) - - -def test_fpx_invalid_number_of_bands() -> None: - with pytest.raises(OSError, match="Invalid number of bands"): - with Image.open("Tests/images/input_bw_five_bands.fpx"): - pass +from __future__ import annotations + +import pytest + +from PIL import Image + +from .helper import assert_image_equal_tofile + +FpxImagePlugin = pytest.importorskip( + "PIL.FpxImagePlugin", reason="olefile not installed" +) + + +def test_sanity() -> None: + with Image.open("Tests/images/input_bw_one_band.fpx") as im: + assert im.mode == "L" + assert im.size == (70, 46) + assert im.format == "FPX" + + assert_image_equal_tofile(im, "Tests/images/input_bw_one_band.png") + + +def test_close() -> None: + with Image.open("Tests/images/input_bw_one_band.fpx") as im: + pass + assert im.ole.fp.closed + + im = Image.open("Tests/images/input_bw_one_band.fpx") + im.close() + assert im.ole.fp.closed + + +def test_invalid_file() -> None: + # Test an invalid OLE file + invalid_file = "Tests/images/flower.jpg" + with pytest.raises(SyntaxError): + FpxImagePlugin.FpxImageFile(invalid_file) + + # Test a valid OLE file, but not an FPX file + ole_file = "Tests/images/test-ole-file.doc" + with pytest.raises(SyntaxError): + FpxImagePlugin.FpxImageFile(ole_file) + + +def test_fpx_invalid_number_of_bands() -> None: + with pytest.raises(OSError, match="Invalid number of bands"): + with Image.open("Tests/images/input_bw_five_bands.fpx"): + pass diff --git a/Tests/test_file_ftex.py b/Tests/test_file_ftex.py index 0c544245a..ff1f83779 100644 --- a/Tests/test_file_ftex.py +++ b/Tests/test_file_ftex.py @@ -1,25 +1,25 @@ -from __future__ import annotations - -import pytest - -from PIL import FtexImagePlugin, Image - -from .helper import assert_image_equal_tofile, assert_image_similar - - -def test_load_raw() -> None: - with Image.open("Tests/images/ftex_uncompressed.ftu") as im: - assert_image_equal_tofile(im, "Tests/images/ftex_uncompressed.png") - - -def test_load_dxt1() -> None: - with Image.open("Tests/images/ftex_dxt1.ftc") as im: - with Image.open("Tests/images/ftex_dxt1.png") as target: - assert_image_similar(im, target.convert("RGBA"), 15) - - -def test_invalid_file() -> None: - invalid_file = "Tests/images/flower.jpg" - - with pytest.raises(SyntaxError): - FtexImagePlugin.FtexImageFile(invalid_file) +from __future__ import annotations + +import pytest + +from PIL import FtexImagePlugin, Image + +from .helper import assert_image_equal_tofile, assert_image_similar + + +def test_load_raw() -> None: + with Image.open("Tests/images/ftex_uncompressed.ftu") as im: + assert_image_equal_tofile(im, "Tests/images/ftex_uncompressed.png") + + +def test_load_dxt1() -> None: + with Image.open("Tests/images/ftex_dxt1.ftc") as im: + with Image.open("Tests/images/ftex_dxt1.png") as target: + assert_image_similar(im, target.convert("RGBA"), 15) + + +def test_invalid_file() -> None: + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(SyntaxError): + FtexImagePlugin.FtexImageFile(invalid_file) diff --git a/Tests/test_file_gbr.py b/Tests/test_file_gbr.py index be98b08f2..0fc8b64b5 100644 --- a/Tests/test_file_gbr.py +++ b/Tests/test_file_gbr.py @@ -1,34 +1,34 @@ -from __future__ import annotations - -import pytest - -from PIL import GbrImagePlugin, Image - -from .helper import assert_image_equal_tofile - - -def test_gbr_file() -> None: - with Image.open("Tests/images/gbr.gbr") as im: - assert_image_equal_tofile(im, "Tests/images/gbr.png") - - -def test_load() -> None: - with Image.open("Tests/images/gbr.gbr") as im: - assert im.load()[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) - - -def test_multiple_load_operations() -> None: - with Image.open("Tests/images/gbr.gbr") as im: - im.load() - im.load() - assert_image_equal_tofile(im, "Tests/images/gbr.png") - - -def test_invalid_file() -> None: - invalid_file = "Tests/images/flower.jpg" - - with pytest.raises(SyntaxError): - GbrImagePlugin.GbrImageFile(invalid_file) +from __future__ import annotations + +import pytest + +from PIL import GbrImagePlugin, Image + +from .helper import assert_image_equal_tofile + + +def test_gbr_file() -> None: + with Image.open("Tests/images/gbr.gbr") as im: + assert_image_equal_tofile(im, "Tests/images/gbr.png") + + +def test_load() -> None: + with Image.open("Tests/images/gbr.gbr") as im: + assert im.load()[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) + + +def test_multiple_load_operations() -> None: + with Image.open("Tests/images/gbr.gbr") as im: + im.load() + im.load() + assert_image_equal_tofile(im, "Tests/images/gbr.png") + + +def test_invalid_file() -> None: + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(SyntaxError): + GbrImagePlugin.GbrImageFile(invalid_file) diff --git a/Tests/test_file_gd.py b/Tests/test_file_gd.py index d512df284..423e2af89 100644 --- a/Tests/test_file_gd.py +++ b/Tests/test_file_gd.py @@ -1,25 +1,25 @@ -from __future__ import annotations - -import pytest - -from PIL import GdImageFile, UnidentifiedImageError - -TEST_GD_FILE = "Tests/images/hopper.gd" - - -def test_sanity() -> None: - with GdImageFile.open(TEST_GD_FILE) as im: - assert im.size == (128, 128) - assert im.format == "GD" - - -def test_bad_mode() -> None: - with pytest.raises(ValueError): - GdImageFile.open(TEST_GD_FILE, "bad mode") - - -def test_invalid_file() -> None: - invalid_file = "Tests/images/flower.jpg" - - with pytest.raises(UnidentifiedImageError): - GdImageFile.open(invalid_file) +from __future__ import annotations + +import pytest + +from PIL import GdImageFile, UnidentifiedImageError + +TEST_GD_FILE = "Tests/images/hopper.gd" + + +def test_sanity() -> None: + with GdImageFile.open(TEST_GD_FILE) as im: + assert im.size == (128, 128) + assert im.format == "GD" + + +def test_bad_mode() -> None: + with pytest.raises(ValueError): + GdImageFile.open(TEST_GD_FILE, "bad mode") + + +def test_invalid_file() -> None: + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(UnidentifiedImageError): + GdImageFile.open(invalid_file) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index e19c88a47..b45dec037 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1,1408 +1,1408 @@ -from __future__ import annotations - -import warnings -from io import BytesIO -from pathlib import Path -from typing import Generator - -import pytest - -from PIL import GifImagePlugin, Image, ImageDraw, ImagePalette, ImageSequence, features - -from .helper import ( - assert_image_equal, - assert_image_equal_tofile, - assert_image_similar, - hopper, - is_pypy, - netpbm_available, -) - -# sample gif stream -TEST_GIF = "Tests/images/hopper.gif" - -with open(TEST_GIF, "rb") as f: - data = f.read() - - -def test_sanity() -> None: - with Image.open(TEST_GIF) as im: - im.load() - assert im.mode == "P" - assert im.size == (128, 128) - assert im.format == "GIF" - assert im.info["version"] == b"GIF89a" - - -@pytest.mark.skipif(is_pypy(), reason="Requires CPython") -def test_unclosed_file() -> None: - def open() -> None: - im = Image.open(TEST_GIF) - im.load() - - with pytest.warns(ResourceWarning): - open() - - -def test_closed_file() -> None: - with warnings.catch_warnings(): - im = Image.open(TEST_GIF) - im.load() - im.close() - - -def test_seek_after_close() -> None: - im = Image.open("Tests/images/iss634.gif") - assert isinstance(im, GifImagePlugin.GifImageFile) - im.load() - im.close() - - with pytest.raises(ValueError): - im.is_animated - with pytest.raises(ValueError): - im.n_frames - with pytest.raises(ValueError): - im.seek(1) - - -def test_context_manager() -> None: - with warnings.catch_warnings(): - with Image.open(TEST_GIF) as im: - im.load() - - -def test_invalid_file() -> None: - invalid_file = "Tests/images/flower.jpg" - - with pytest.raises(SyntaxError): - GifImagePlugin.GifImageFile(invalid_file) - - -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.info["transparency"] == 255 - - im.seek(1) - assert im.mode == "L" - assert im.load()[0, 0] == 128 - - -def test_l_mode_after_rgb() -> None: - with Image.open("Tests/images/no_palette_after_rgb.gif") as im: - im.seek(1) - assert im.mode == "RGB" - - im.seek(2) - assert im.mode == "RGB" - - -def test_palette_not_needed_for_second_frame() -> None: - with Image.open("Tests/images/palette_not_needed_for_second_frame.gif") as im: - im.seek(1) - assert_image_similar(im, hopper("L").convert("RGB"), 8) - - -def test_strategy() -> None: - with Image.open("Tests/images/iss634.gif") as im: - expected_rgb_always = im.convert("RGB") - - with Image.open("Tests/images/chi.gif") as im: - expected_rgb_always_rgba = im.convert("RGBA") - - im.seek(1) - expected_different = im.convert("RGB") - - try: - GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS - with Image.open("Tests/images/iss634.gif") as im: - assert im.mode == "RGB" - assert_image_equal(im, expected_rgb_always) - - with Image.open("Tests/images/chi.gif") as im: - assert im.mode == "RGBA" - assert_image_equal(im, expected_rgb_always_rgba) - - GifImagePlugin.LOADING_STRATEGY = ( - GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY - ) - # Stay in P mode with only a global palette - with Image.open("Tests/images/chi.gif") as im: - assert im.mode == "P" - - im.seek(1) - assert im.mode == "P" - assert_image_equal(im.convert("RGB"), expected_different) - - # 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 - - -def test_optimize() -> None: - def test_grayscale(optimize: int) -> int: - im = Image.new("L", (1, 1), 0) - filename = BytesIO() - im.save(filename, "GIF", optimize=optimize) - return len(filename.getvalue()) - - def test_bilevel(optimize: int) -> int: - im = Image.new("1", (1, 1), 0) - test_file = BytesIO() - im.save(test_file, "GIF", optimize=optimize) - return len(test_file.getvalue()) - - assert test_grayscale(0) == 799 - assert test_grayscale(1) == 43 - assert test_bilevel(0) == 799 - assert test_bilevel(1) == 799 - - -@pytest.mark.parametrize( - "colors, size, expected_palette_length", - ( - # These do optimize the palette - (256, 511, 256), - (255, 511, 255), - (129, 511, 129), - (128, 511, 128), - (64, 511, 64), - (4, 511, 4), - # These don't optimize the palette - (128, 513, 256), - (64, 513, 256), - (4, 513, 256), - ), -) -def test_optimize_correctness( - colors: int, size: int, expected_palette_length: int -) -> None: - # 256 color Palette image, posterize to > 128 and < 128 levels. - # Size bigger and smaller than 512x512. - # Check the palette for number of colors allocated. - # Check for correctness after conversion back to RGB. - - # make an image with empty colors in the start of the palette range - im = Image.frombytes( - "P", (colors, colors), bytes(range(256 - colors, 256)) * colors - ) - im = im.resize((size, size)) - outfile = BytesIO() - im.save(outfile, "GIF") - outfile.seek(0) - with Image.open(outfile) as reloaded: - # check palette length - palette_length = max(i + 1 for i, v in enumerate(reloaded.histogram()) if v) - assert expected_palette_length == palette_length - - assert_image_equal(im.convert("RGB"), reloaded.convert("RGB")) - - -def test_optimize_full_l() -> None: - im = Image.frombytes("L", (16, 16), bytes(range(256))) - test_file = BytesIO() - im.save(test_file, "GIF", optimize=True) - assert im.mode == "L" - - -def test_optimize_if_palette_can_be_reduced_by_half() -> None: - im = Image.new("P", (8, 1)) - im.palette = ImagePalette.raw("RGB", bytes((0, 0, 0) * 150)) - for i in range(8): - im.putpixel((i, 0), (i + 1, 0, 0)) - - for optimize, colors in ((False, 256), (True, 8)): - out = BytesIO() - im.save(out, "GIF", optimize=optimize) - with Image.open(out) as reloaded: - assert len(reloaded.palette.palette) // 3 == colors - - -def test_full_palette_second_frame(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") - im = Image.new("P", (1, 256)) - - full_palette_im = Image.new("P", (1, 256)) - for i in range(256): - full_palette_im.putpixel((0, i), i) - full_palette_im.palette = ImagePalette.ImagePalette( - "RGB", bytearray(i // 3 for i in range(768)) - ) - full_palette_im.palette.dirty = 1 - - im.save(out, save_all=True, append_images=[full_palette_im]) - - with Image.open(out) as reloaded: - reloaded.seek(1) - - for i in range(256): - reloaded.getpixel((0, i)) == i - - -def test_roundtrip(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") - im = hopper() - im.save(out) - with Image.open(out) as reread: - assert_image_similar(reread.convert("RGB"), im, 50) - - -def test_roundtrip2(tmp_path: Path) -> None: - # see https://github.com/python-pillow/Pillow/issues/403 - out = str(tmp_path / "temp.gif") - with Image.open(TEST_GIF) as im: - im2 = im.copy() - im2.save(out) - with Image.open(out) as reread: - assert_image_similar(reread.convert("RGB"), hopper(), 50) - - -def test_roundtrip_save_all(tmp_path: Path) -> None: - # Single frame image - out = str(tmp_path / "temp.gif") - im = hopper() - im.save(out, save_all=True) - with Image.open(out) as reread: - assert_image_similar(reread.convert("RGB"), im, 50) - - # Multiframe image - with Image.open("Tests/images/dispose_bgnd.gif") as im: - out = str(tmp_path / "temp.gif") - im.save(out, save_all=True) - - with Image.open(out) as reread: - assert reread.n_frames == 5 - - -def test_roundtrip_save_all_1(tmp_path: Path) -> None: - out = str(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]) - - with Image.open(out) as reloaded: - assert reloaded.getpixel((0, 0)) == 0 - - reloaded.seek(1) - assert reloaded.getpixel((0, 0)) == 255 - - -@pytest.mark.parametrize( - "path, mode", - ( - ("Tests/images/dispose_bgnd.gif", "RGB"), - # Hexeditted copy of dispose_bgnd to add transparency - ("Tests/images/dispose_bgnd_rgba.gif", "RGBA"), - ), -) -def test_loading_multiple_palettes(path: str, mode: str) -> None: - with Image.open(path) as im: - assert im.mode == "P" - first_frame_colors = im.palette.colors.keys() - original_color = im.convert("RGB").load()[0, 0] - - im.seek(1) - assert im.mode == mode - if mode == "RGBA": - im = im.convert("RGB") - - # Check a color only from the old palette - assert im.load()[0, 0] == original_color - - # Check a color from the new palette - assert im.load()[24, 24] not in first_frame_colors - - -def test_headers_saving_for_animated_gifs(tmp_path: Path) -> None: - important_headers = ["background", "version", "duration", "loop"] - # Multiframe image - with Image.open("Tests/images/dispose_bgnd.gif") as im: - info = im.info.copy() - - out = str(tmp_path / "temp.gif") - im.save(out, save_all=True) - with Image.open(out) as reread: - for header in important_headers: - assert info[header] == reread.info[header] - - -def test_palette_handling(tmp_path: Path) -> None: - # see https://github.com/python-pillow/Pillow/issues/513 - - with Image.open(TEST_GIF) as im: - im = im.convert("RGB") - - im = im.resize((100, 100), Image.Resampling.LANCZOS) - im2 = im.convert("P", palette=Image.Palette.ADAPTIVE, colors=256) - - f = str(tmp_path / "temp.gif") - im2.save(f, optimize=True) - - with Image.open(f) as reloaded: - assert_image_similar(im, reloaded.convert("RGB"), 10) - - -def test_palette_434(tmp_path: Path) -> None: - # see https://github.com/python-pillow/Pillow/issues/434 - - def roundtrip(im: Image.Image, **kwargs: bool) -> Image.Image: - out = str(tmp_path / "temp.gif") - im.copy().save(out, **kwargs) - reloaded = Image.open(out) - - return reloaded - - orig = "Tests/images/test.colors.gif" - with Image.open(orig) as im: - with roundtrip(im) as reloaded: - assert_image_similar(im, reloaded, 1) - with roundtrip(im, optimize=True) as reloaded: - assert_image_similar(im, reloaded, 1) - - im = im.convert("RGB") - # check automatic P conversion - with roundtrip(im) as reloaded: - reloaded = reloaded.convert("RGB") - assert_image_equal(im, reloaded) - - -@pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") -def test_save_netpbm_bmp_mode(tmp_path: Path) -> None: - with Image.open(TEST_GIF) as img: - img = img.convert("RGB") - - tempfile = str(tmp_path / "temp.gif") - b = BytesIO() - GifImagePlugin._save_netpbm(img, b, tempfile) - with Image.open(tempfile) as reloaded: - assert_image_similar(img, reloaded.convert("RGB"), 0) - - -@pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") -def test_save_netpbm_l_mode(tmp_path: Path) -> None: - with Image.open(TEST_GIF) as img: - img = img.convert("L") - - tempfile = str(tmp_path / "temp.gif") - b = BytesIO() - GifImagePlugin._save_netpbm(img, b, tempfile) - with Image.open(tempfile) as reloaded: - assert_image_similar(img, reloaded.convert("L"), 0) - - -def test_seek() -> None: - with Image.open("Tests/images/dispose_none.gif") as img: - frame_count = 0 - try: - while True: - frame_count += 1 - img.seek(img.tell() + 1) - except EOFError: - assert frame_count == 5 - - -def test_seek_info() -> None: - with Image.open("Tests/images/iss634.gif") as im: - info = im.info.copy() - - im.seek(1) - im.seek(0) - - assert im.info == info - - -def test_seek_rewind() -> None: - with Image.open("Tests/images/iss634.gif") as im: - im.seek(2) - im.seek(1) - - with Image.open("Tests/images/iss634.gif") as expected: - expected.seek(1) - assert_image_equal(im, expected) - - -@pytest.mark.parametrize( - "path, n_frames", - ( - (TEST_GIF, 1), - ("Tests/images/comment_after_last_frame.gif", 2), - ("Tests/images/iss634.gif", 42), - ), -) -def test_n_frames(path: str, n_frames: int) -> None: - # Test is_animated before n_frames - with Image.open(path) as im: - assert im.is_animated == (n_frames != 1) - - # Test is_animated after n_frames - with Image.open(path) as im: - assert im.n_frames == n_frames - assert im.is_animated == (n_frames != 1) - - -def test_no_change() -> None: - # Test n_frames does not change the image - with Image.open("Tests/images/dispose_bgnd.gif") as im: - im.seek(1) - expected = im.copy() - assert im.n_frames == 5 - assert_image_equal(im, expected) - - # Test is_animated does not change the image - with Image.open("Tests/images/dispose_bgnd.gif") as im: - im.seek(3) - expected = im.copy() - assert im.is_animated - assert_image_equal(im, expected) - - with Image.open("Tests/images/comment_after_only_frame.gif") as im: - expected = Image.new("P", (1, 1)) - assert not im.is_animated - assert_image_equal(im, expected) - - -def test_eoferror() -> None: - with Image.open(TEST_GIF) as im: - n_frames = im.n_frames - - # Test seeking past the last frame - with pytest.raises(EOFError): - im.seek(n_frames) - assert im.tell() < n_frames - - # Test that seeking to the last frame does not raise an error - im.seek(n_frames - 1) - - -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"] - - -def test_dispose_none() -> None: - with Image.open("Tests/images/dispose_none.gif") as img: - try: - while True: - img.seek(img.tell() + 1) - assert img.disposal_method == 1 - except EOFError: - pass - - -def test_dispose_none_load_end() -> None: - # Test image created with: - # - # im = Image.open("transparent.gif") - # im_rotated = im.rotate(180) - # im.save("dispose_none_load_end.gif", - # save_all=True, append_images=[im_rotated], disposal=[1,2]) - with Image.open("Tests/images/dispose_none_load_end.gif") as img: - img.seek(1) - - assert_image_equal_tofile(img, "Tests/images/dispose_none_load_end_second.png") - - -def test_dispose_background() -> None: - with Image.open("Tests/images/dispose_bgnd.gif") as img: - try: - while True: - img.seek(img.tell() + 1) - assert img.disposal_method == 2 - except EOFError: - pass - - -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 - - -@pytest.mark.parametrize( - "loading_strategy, expected_colors", - ( - ( - GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST, - ( - (2, 1, 2), - ((0, 255, 24, 255), (0, 0, 255, 255), (0, 255, 24, 255)), - ((0, 0, 0, 0), (0, 0, 255, 255), (0, 0, 0, 0)), - ), - ), - ( - GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY, - ( - (2, 1, 2), - (0, 1, 0), - (2, 1, 2), - ), - ), - ), -) -def test_transparent_dispose( - loading_strategy: GifImagePlugin.LoadingStrategy, - expected_colors: tuple[tuple[int | tuple[int, int, int, int], ...]], -) -> 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 - - -def test_dispose_previous() -> None: - with Image.open("Tests/images/dispose_prev.gif") as img: - try: - while True: - img.seek(img.tell() + 1) - assert img.disposal_method == 3 - except EOFError: - pass - - -def test_dispose_previous_first_frame() -> None: - with Image.open("Tests/images/dispose_prev_first_frame.gif") as im: - im.seek(1) - assert_image_equal_tofile( - im, "Tests/images/dispose_prev_first_frame_seeked.png" - ) - - -def test_previous_frame_loaded() -> None: - with Image.open("Tests/images/dispose_none.gif") as img: - img.load() - img.seek(1) - img.load() - img.seek(2) - with Image.open("Tests/images/dispose_none.gif") as img_skipped: - img_skipped.seek(2) - assert_image_equal(img_skipped, img) - - -def test_save_dispose(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") - im_list = [ - Image.new("L", (100, 100), "#000"), - Image.new("L", (100, 100), "#111"), - Image.new("L", (100, 100), "#222"), - ] - for method in range(0, 4): - im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=method) - with Image.open(out) as img: - for _ in range(2): - img.seek(img.tell() + 1) - assert img.disposal_method == method - - # Check per frame disposal - im_list[0].save( - out, - save_all=True, - append_images=im_list[1:], - disposal=tuple(range(len(im_list))), - ) - - with Image.open(out) as img: - 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") - - # Four colors: white, gray, black, red - circles = [(255, 255, 255), (153, 153, 153), (0, 0, 0), (255, 0, 0)] - - im_list = [] - for circle in circles: - # Red background - img = Image.new("RGB", (100, 100), (255, 0, 0)) - - # Circle in center of each frame - d = ImageDraw.Draw(img) - d.ellipse([(40, 40), (60, 60)], fill=circle) - - im_list.append(img) - - im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=2) - - with Image.open(out) as img: - for i, circle in enumerate(circles): - img.seek(i) - rgb_img = img.convert("RGB") - - # Check top left pixel matches background - assert rgb_img.getpixel((0, 0)) == (255, 0, 0) - - # Center remains red every frame - assert rgb_img.getpixel((50, 50)) == circle - - # Check that frame transparency wasn't added unnecessarily - assert getattr(img, "_frame_transparency") is None - - -def test_dispose2_diff(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") - - # 4 frames: red/blue, red/red, blue/blue, red/blue - circles = [ - ((255, 0, 0, 255), (0, 0, 255, 255)), - ((255, 0, 0, 255), (255, 0, 0, 255)), - ((0, 0, 255, 255), (0, 0, 255, 255)), - ((255, 0, 0, 255), (0, 0, 255, 255)), - ] - - im_list = [] - for i in range(len(circles)): - # Transparent BG - img = Image.new("RGBA", (100, 100), (255, 255, 255, 0)) - - # Two circles per frame - d = ImageDraw.Draw(img) - d.ellipse([(0, 30), (40, 70)], fill=circles[i][0]) - d.ellipse([(60, 30), (100, 70)], fill=circles[i][1]) - - im_list.append(img) - - im_list[0].save( - out, save_all=True, append_images=im_list[1:], disposal=2, transparency=0 - ) - - with Image.open(out) as img: - for i, colours in enumerate(circles): - img.seek(i) - rgb_img = img.convert("RGBA") - - # Check left circle is correct colour - assert rgb_img.getpixel((20, 50)) == colours[0] - - # Check right circle is correct colour - assert rgb_img.getpixel((80, 50)) == colours[1] - - # Check BG is correct colour - assert rgb_img.getpixel((1, 1)) == (255, 255, 255, 0) - - -def test_dispose2_background(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") - - im_list = [] - - im = Image.new("P", (100, 100)) - d = ImageDraw.Draw(im) - d.rectangle([(50, 0), (100, 100)], fill="#f00") - d.rectangle([(0, 0), (50, 100)], fill="#0f0") - im_list.append(im) - - im = Image.new("P", (100, 100)) - d = ImageDraw.Draw(im) - d.rectangle([(0, 0), (100, 50)], fill="#f00") - d.rectangle([(0, 50), (100, 100)], fill="#0f0") - im_list.append(im) - - im_list[0].save( - out, save_all=True, append_images=im_list[1:], disposal=[0, 2], background=1 - ) - - with Image.open(out) as im: - im.seek(1) - assert im.getpixel((0, 0)) == (255, 0, 0) - - -def test_dispose2_background_frame(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") - - im_list = [Image.new("RGBA", (1, 20))] - - different_frame = Image.new("RGBA", (1, 20)) - different_frame.putpixel((0, 10), (255, 0, 0, 255)) - im_list.append(different_frame) - - # Frame that matches the background - im_list.append(Image.new("RGBA", (1, 20))) - - im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=2) - - with Image.open(out) as im: - assert im.n_frames == 3 - - -def test_dispose2_previous_frame(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") - - im = Image.new("P", (100, 100)) - im.info["transparency"] = 0 - d = ImageDraw.Draw(im) - d.rectangle([(0, 0), (100, 50)], 1) - im.putpalette((0, 0, 0, 255, 0, 0)) - - im2 = Image.new("P", (100, 100)) - im2.putpalette((0, 0, 0)) - - im.save(out, save_all=True, append_images=[im2], disposal=[0, 2]) - - with Image.open(out) as im: - im.seek(1) - assert im.getpixel((0, 0)) == (0, 0, 0, 255) - - -def test_transparency_in_second_frame(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") - with Image.open("Tests/images/different_transparency.gif") as im: - assert im.info["transparency"] == 0 - - # Seek to the second frame - im.seek(im.tell() + 1) - assert "transparency" not in im.info - - assert_image_equal_tofile(im, "Tests/images/different_transparency_merged.png") - - im.save(out, save_all=True) - - with Image.open(out) as reread: - reread.seek(reread.tell() + 1) - assert_image_equal_tofile( - reread, "Tests/images/different_transparency_merged.png" - ) - - -def test_no_transparency_in_second_frame() -> None: - with Image.open("Tests/images/iss634.gif") as img: - # Seek to the second frame - img.seek(img.tell() + 1) - assert "transparency" not in img.info - - # All transparent pixels should be replaced with the color from the first frame - assert img.histogram()[255] == 0 - - -def test_remapped_transparency(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") - - im = Image.new("P", (1, 2)) - im2 = im.copy() - - # Add transparency at a higher index - # so that it will be optimized to a lower index - im.putpixel((0, 1), 5) - im.info["transparency"] = 5 - im.save(out, save_all=True, append_images=[im2]) - - with Image.open(out) as reloaded: - assert reloaded.info["transparency"] == reloaded.getpixel((0, 1)) - - -def test_duration(tmp_path: Path) -> None: - duration = 1000 - - out = str(tmp_path / "temp.gif") - im = Image.new("L", (100, 100), "#000") - - # Check that the argument has priority over the info settings - im.info["duration"] = 100 - im.save(out, duration=duration) - - with Image.open(out) as reread: - assert reread.info["duration"] == duration - - -def test_multiple_duration(tmp_path: Path) -> None: - duration_list = [1000, 2000, 3000] - - out = str(tmp_path / "temp.gif") - im_list = [ - Image.new("L", (100, 100), "#000"), - Image.new("L", (100, 100), "#111"), - Image.new("L", (100, 100), "#222"), - ] - - # Duration as list - im_list[0].save( - out, save_all=True, append_images=im_list[1:], duration=duration_list - ) - with Image.open(out) as reread: - for duration in duration_list: - assert reread.info["duration"] == duration - try: - reread.seek(reread.tell() + 1) - except EOFError: - pass - - # Duration as tuple - im_list[0].save( - out, save_all=True, append_images=im_list[1:], duration=tuple(duration_list) - ) - with Image.open(out) as reread: - for duration in duration_list: - assert reread.info["duration"] == duration - try: - reread.seek(reread.tell() + 1) - except EOFError: - pass - - -def test_roundtrip_info_duration(tmp_path: Path) -> None: - duration_list = [100, 500, 500] - - out = str(tmp_path / "temp.gif") - with Image.open("Tests/images/transparent_dispose.gif") as im: - assert [ - frame.info["duration"] for frame in ImageSequence.Iterator(im) - ] == duration_list - - im.save(out, save_all=True) - - with Image.open(out) as reloaded: - assert [ - frame.info["duration"] for frame in ImageSequence.Iterator(reloaded) - ] == duration_list - - -def test_roundtrip_info_duration_combined(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") - with Image.open("Tests/images/duplicate_frame.gif") as im: - assert [frame.info["duration"] for frame in ImageSequence.Iterator(im)] == [ - 1000, - 1000, - 1000, - ] - im.save(out, save_all=True) - - with Image.open(out) as reloaded: - assert [ - frame.info["duration"] for frame in ImageSequence.Iterator(reloaded) - ] == [1000, 2000] - - -def test_identical_frames(tmp_path: Path) -> None: - duration_list = [1000, 1500, 2000, 4000] - - out = str(tmp_path / "temp.gif") - im_list = [ - Image.new("L", (100, 100), "#000"), - Image.new("L", (100, 100), "#000"), - Image.new("L", (100, 100), "#000"), - Image.new("L", (100, 100), "#111"), - ] - - # Duration as list - im_list[0].save( - out, save_all=True, append_images=im_list[1:], duration=duration_list - ) - with Image.open(out) as reread: - # Assert that the first three frames were combined - assert reread.n_frames == 2 - - # Assert that the new duration is the total of the identical frames - assert reread.info["duration"] == 4500 - - -@pytest.mark.parametrize( - "duration", - ( - [1000, 1500, 2000], - (1000, 1500, 2000), - # One more duration than the number of frames - [1000, 1500, 2000, 4000], - 1500, - ), -) -def test_identical_frames_to_single_frame( - duration: int | list[int], tmp_path: Path -) -> None: - out = str(tmp_path / "temp.gif") - im_list = [ - Image.new("L", (100, 100), "#000"), - Image.new("L", (100, 100), "#000"), - Image.new("L", (100, 100), "#000"), - ] - - im_list[0].save(out, save_all=True, append_images=im_list[1:], duration=duration) - with Image.open(out) as reread: - # Assert that all frames were combined - assert reread.n_frames == 1 - - # Assert that the new duration is the total of the identical frames - assert reread.info["duration"] == 4500 - - -def test_loop_none(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") - im = Image.new("L", (100, 100), "#000") - im.save(out, loop=None) - with Image.open(out) as reread: - assert "loop" not in reread.info - - -def test_number_of_loops(tmp_path: Path) -> None: - number_of_loops = 2 - - out = str(tmp_path / "temp.gif") - im = Image.new("L", (100, 100), "#000") - im.save(out, loop=number_of_loops) - with Image.open(out) as reread: - assert reread.info["loop"] == number_of_loops - - # Check that even if a subsequent GIF frame has the number of loops specified, - # only the value from the first frame is used - with Image.open("Tests/images/duplicate_number_of_loops.gif") as im: - assert im.info["loop"] == 2 - - im.seek(1) - assert im.info["loop"] == 2 - - -def test_background(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") - im = Image.new("L", (100, 100), "#000") - im.info["background"] = 1 - im.save(out) - with Image.open(out) as reread: - assert reread.info["background"] == im.info["background"] - - -def test_webp_background(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") - - # Test opaque WebP background - if features.check("webp") and features.check("webp_anim"): - with Image.open("Tests/images/hopper.webp") as im: - assert im.info["background"] == (255, 255, 255, 255) - im.save(out) - - # Test non-opaque WebP background - im = Image.new("L", (100, 100), "#000") - im.info["background"] = (0, 0, 0, 0) - im.save(out) - - -def test_comment(tmp_path: Path) -> None: - with Image.open(TEST_GIF) as im: - assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0" - - out = str(tmp_path / "temp.gif") - im = Image.new("L", (100, 100), "#000") - im.info["comment"] = b"Test comment text" - im.save(out) - with Image.open(out) as reread: - assert reread.info["comment"] == im.info["comment"] - - im.info["comment"] = "Test comment text" - im.save(out) - with Image.open(out) as reread: - assert reread.info["comment"] == im.info["comment"].encode() - - # Test that GIF89a is used for comments - assert reread.info["version"] == b"GIF89a" - - -def test_comment_over_255(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") - im = Image.new("L", (100, 100), "#000") - comment = b"Test comment text" - while len(comment) < 256: - comment += comment - im.info["comment"] = comment - im.save(out) - with Image.open(out) as reread: - assert reread.info["comment"] == comment - - # Test that GIF89a is used for comments - assert reread.info["version"] == b"GIF89a" - - -def test_zero_comment_subblocks() -> None: - with Image.open("Tests/images/hopper_zero_comment_subblocks.gif") as im: - assert_image_equal_tofile(im, TEST_GIF) - - -def test_read_multiple_comment_blocks() -> None: - with Image.open("Tests/images/multiple_comments.gif") as im: - # Multiple comment blocks in a frame are separated not concatenated - assert im.info["comment"] == b"Test comment 1\nTest comment 2" - - -def test_empty_string_comment(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") - with Image.open("Tests/images/chi.gif") as im: - assert "comment" in im.info - - # Empty string comment should suppress existing comment - im.save(out, save_all=True, comment="") - - with Image.open(out) as reread: - for frame in ImageSequence.Iterator(reread): - assert "comment" not in frame.info - - -def test_retain_comment_in_subsequent_frames(tmp_path: Path) -> None: - # Test that a comment block at the beginning is kept - with Image.open("Tests/images/chi.gif") as im: - for frame in ImageSequence.Iterator(im): - assert frame.info["comment"] == b"Created with GIMP" - - with Image.open("Tests/images/second_frame_comment.gif") as im: - assert "comment" not in im.info - - # Test that a comment in the middle is read - im.seek(1) - assert im.info["comment"] == b"Comment in the second frame" - - # Test that it is still present in a later frame - im.seek(2) - assert im.info["comment"] == b"Comment in the second frame" - - # Test that rewinding removes the comment - im.seek(0) - assert "comment" not in im.info - - # Test that a saved image keeps the comment - out = str(tmp_path / "temp.gif") - with Image.open("Tests/images/dispose_prev.gif") as im: - im.save(out, save_all=True, comment="Test") - - with Image.open(out) as reread: - for frame in ImageSequence.Iterator(reread): - assert frame.info["comment"] == b"Test" - - -def test_version(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") - - def assert_version_after_save(im: Image.Image, version: bytes) -> None: - im.save(out) - with Image.open(out) as reread: - assert reread.info["version"] == version - - # Test that GIF87a is used by default - im = Image.new("L", (100, 100), "#000") - assert_version_after_save(im, b"GIF87a") - - # Test setting the version to 89a - im = Image.new("L", (100, 100), "#000") - im.info["version"] = b"89a" - assert_version_after_save(im, b"GIF89a") - - # Test that adding a GIF89a feature changes the version - im.info["transparency"] = 1 - assert_version_after_save(im, b"GIF89a") - - # Test that a GIF87a image is also saved in that format - with Image.open("Tests/images/test.colors.gif") as im: - assert_version_after_save(im, b"GIF87a") - - # Test that a GIF89a image is also saved in that format - im.info["version"] = b"GIF89a" - assert_version_after_save(im, b"GIF87a") - - -def test_append_images(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") - - # Test appending single frame images - im = Image.new("RGB", (100, 100), "#f00") - ims = [Image.new("RGB", (100, 100), color) for color in ["#0f0", "#00f"]] - im.copy().save(out, save_all=True, append_images=ims) - - with Image.open(out) as reread: - assert reread.n_frames == 3 - - # Tests appending using a generator - def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]: - yield from ims - - im.save(out, save_all=True, append_images=im_generator(ims)) - - with Image.open(out) as reread: - assert reread.n_frames == 3 - - # Tests appending single and multiple frame images - with Image.open("Tests/images/dispose_none.gif") as im: - with Image.open("Tests/images/dispose_prev.gif") as im2: - im.save(out, save_all=True, append_images=[im2]) - - with Image.open(out) as reread: - assert reread.n_frames == 10 - - -def test_append_different_size_image(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") - - im = Image.new("RGB", (100, 100)) - bigger_im = Image.new("RGB", (200, 200), "#f00") - - im.save(out, save_all=True, append_images=[bigger_im]) - - with Image.open(out) as reread: - assert reread.size == (100, 100) - - reread.seek(1) - assert reread.size == (100, 100) - - -def test_transparent_optimize(tmp_path: Path) -> None: - # From issue #2195, if the transparent color is incorrectly optimized out, GIF loses - # transparency. - # Need a palette that isn't using the 0 color, - # where the transparent color is actually the top palette entry to trigger the bug. - - data = bytes(range(1, 254)) - palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) - - im = Image.new("L", (253, 1)) - im.frombytes(data) - im.putpalette(palette) - - out = str(tmp_path / "temp.gif") - im.save(out, transparency=im.getpixel((252, 0))) - - with Image.open(out) as reloaded: - assert reloaded.info["transparency"] == reloaded.getpixel((252, 0)) - - -def test_removed_transparency(tmp_path: Path) -> None: - out = str(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): - im.save(out) - - with Image.open(out) as reloaded: - assert "transparency" not in reloaded.info - - -def test_rgb_transparency(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") - - # Single frame - im = Image.new("RGB", (1, 1)) - im.info["transparency"] = (255, 0, 0) - im.save(out) - - with Image.open(out) as reloaded: - assert "transparency" in reloaded.info - - # Multiple frames - im = Image.new("RGB", (1, 1)) - im.info["transparency"] = b"" - ims = [Image.new("RGB", (1, 1))] - with pytest.warns(UserWarning): - im.save(out, save_all=True, append_images=ims) - - with Image.open(out) as reloaded: - assert "transparency" not in reloaded.info - - -def test_rgba_transparency(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") - - im = hopper("P") - im.save(out, save_all=True, append_images=[Image.new("RGBA", im.size)]) - - with Image.open(out) as reloaded: - reloaded.seek(1) - assert_image_equal(hopper("P").convert("RGB"), reloaded) - - -def test_background_outside_palettte(tmp_path: Path) -> 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") - - im = Image.new("RGB", (100, 100), "#fff") - ims = [Image.new("RGB", (100, 100), "#000")] - im.save(out, save_all=True, append_images=ims) - - with Image.open(out) as reread: - assert reread.n_frames == 2 - - -def test_bbox_alpha(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") - - im = Image.new("RGBA", (1, 2), (255, 0, 0, 255)) - im.putpixel((0, 1), (255, 0, 0, 0)) - im2 = Image.new("RGBA", (1, 2), (255, 0, 0, 0)) - im.save(out, save_all=True, append_images=[im2]) - - with Image.open(out) as reread: - assert reread.n_frames == 2 - - -def test_palette_save_L(tmp_path: Path) -> None: - # Generate an L mode image with a separate palette - - im = hopper("P") - im_l = Image.frombytes("L", im.size, im.tobytes()) - palette = im.getpalette() - assert palette is not None - - out = str(tmp_path / "temp.gif") - im_l.save(out, palette=bytes(palette)) - - with Image.open(out) as reloaded: - assert_image_equal(reloaded.convert("RGB"), im.convert("RGB")) - - -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") - im.save(out, palette=bytes((1, 2, 3, 4, 5, 6))) - - with Image.open(out) as reloaded: - reloaded_rgb = reloaded.convert("RGB") - - assert reloaded_rgb.getpixel((0, 0)) == (1, 2, 3) - assert reloaded_rgb.getpixel((0, 1)) == (4, 5, 6) - - -def test_palette_save_duplicate_entries(tmp_path: Path) -> None: - im = Image.new("P", (1, 2)) - im.putpixel((0, 1), 1) - - im.putpalette((0, 0, 0, 0, 0, 0)) - - out = str(tmp_path / "temp.gif") - im.save(out, palette=[0, 0, 0, 0, 0, 0, 1, 1, 1]) - - with Image.open(out) as reloaded: - assert reloaded.convert("RGB").getpixel((0, 1)) == (0, 0, 0) - - -def test_palette_save_all_P(tmp_path: Path) -> None: - frames = [] - colors = ((255, 0, 0), (0, 255, 0)) - for color in colors: - frame = Image.new("P", (100, 100)) - frame.putpalette(color) - frames.append(frame) - - out = str(tmp_path / "temp.gif") - frames[0].save( - out, save_all=True, palette=[255, 0, 0, 0, 255, 0], append_images=frames[1:] - ) - - with Image.open(out) as im: - # Assert that the frames are correct, and each frame has the same palette - assert_image_equal(im.convert("RGB"), frames[0].convert("RGB")) - assert im.palette.palette == im.global_palette.palette - - im.seek(1) - assert_image_equal(im.convert("RGB"), frames[1].convert("RGB")) - assert im.palette.palette == im.global_palette.palette - - -def test_palette_save_ImagePalette(tmp_path: Path) -> None: - # Pass in a different palette, as an ImagePalette.ImagePalette - # effectively the same as test_palette_save_P - - im = hopper("P") - palette = ImagePalette.ImagePalette("RGB", list(range(256))[::-1] * 3) - - out = str(tmp_path / "temp.gif") - im.save(out, palette=palette) - - with Image.open(out) as reloaded: - im.putpalette(palette) - assert_image_equal(reloaded.convert("RGB"), im.convert("RGB")) - - -def test_save_I(tmp_path: Path) -> None: - # Test saving something that would trigger the auto-convert to 'L' - - im = hopper("I") - - out = str(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: - # Test getheader/getdata against legacy values. - # Create a 'P' image with holes in the palette. - im = Image._wedge().resize((16, 16), Image.Resampling.NEAREST) - im.putpalette(ImagePalette.ImagePalette("RGB")) - im.info = {"background": 0} - - passed_palette = bytes(255 - i // 3 for i in range(768)) - - GifImagePlugin._FORCE_OPTIMIZE = True - try: - h = GifImagePlugin.getheader(im, passed_palette) - d = GifImagePlugin.getdata(im) - - import pickle - - # Enable to get target values on pre-refactor version - # with open('Tests/images/gif_header_data.pkl', 'wb') as f: - # pickle.dump((h, d), f, 1) - with open("Tests/images/gif_header_data.pkl", "rb") as f: - (h_target, d_target) = pickle.load(f) - - assert h == h_target - assert d == d_target - finally: - GifImagePlugin._FORCE_OPTIMIZE = False - - -def test_lzw_bits() -> 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 - # codec error prepatch - im.load() - - -def test_extents() -> None: - with Image.open("Tests/images/test_extents.gif") as im: - assert im.size == (100, 100) - - # Check that n_frames does not change the size - assert im.n_frames == 2 - assert im.size == (100, 100) - - im.seek(1) - assert im.size == (150, 150) - - -def test_missing_background() -> None: - # The Global Color Table Flag isn't set, so there is no background color index, - # but the disposal method is "Restore to background color" - with Image.open("Tests/images/missing_background.gif") as im: - im.seek(1) - assert_image_equal_tofile(im, "Tests/images/missing_background_first_frame.png") - - -def test_saving_rgba(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") - with Image.open("Tests/images/transparent.png") as im: - im.save(out) - - with Image.open(out) as reloaded: - reloaded_rgba = reloaded.convert("RGBA") - assert reloaded_rgba.load()[0, 0][3] == 0 +from __future__ import annotations + +import warnings +from io import BytesIO +from pathlib import Path +from typing import Generator + +import pytest + +from PIL import GifImagePlugin, Image, ImageDraw, ImagePalette, ImageSequence, features + +from .helper import ( + assert_image_equal, + assert_image_equal_tofile, + assert_image_similar, + hopper, + is_pypy, + netpbm_available, +) + +# sample gif stream +TEST_GIF = "Tests/images/hopper.gif" + +with open(TEST_GIF, "rb") as f: + data = f.read() + + +def test_sanity() -> None: + with Image.open(TEST_GIF) as im: + im.load() + assert im.mode == "P" + assert im.size == (128, 128) + assert im.format == "GIF" + assert im.info["version"] == b"GIF89a" + + +@pytest.mark.skipif(is_pypy(), reason="Requires CPython") +def test_unclosed_file() -> None: + def open() -> None: + im = Image.open(TEST_GIF) + im.load() + + with pytest.warns(ResourceWarning): + open() + + +def test_closed_file() -> None: + with warnings.catch_warnings(): + im = Image.open(TEST_GIF) + im.load() + im.close() + + +def test_seek_after_close() -> None: + im = Image.open("Tests/images/iss634.gif") + assert isinstance(im, GifImagePlugin.GifImageFile) + im.load() + im.close() + + with pytest.raises(ValueError): + im.is_animated + with pytest.raises(ValueError): + im.n_frames + with pytest.raises(ValueError): + im.seek(1) + + +def test_context_manager() -> None: + with warnings.catch_warnings(): + with Image.open(TEST_GIF) as im: + im.load() + + +def test_invalid_file() -> None: + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(SyntaxError): + GifImagePlugin.GifImageFile(invalid_file) + + +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.info["transparency"] == 255 + + im.seek(1) + assert im.mode == "L" + assert im.load()[0, 0] == 128 + + +def test_l_mode_after_rgb() -> None: + with Image.open("Tests/images/no_palette_after_rgb.gif") as im: + im.seek(1) + assert im.mode == "RGB" + + im.seek(2) + assert im.mode == "RGB" + + +def test_palette_not_needed_for_second_frame() -> None: + with Image.open("Tests/images/palette_not_needed_for_second_frame.gif") as im: + im.seek(1) + assert_image_similar(im, hopper("L").convert("RGB"), 8) + + +def test_strategy() -> None: + with Image.open("Tests/images/iss634.gif") as im: + expected_rgb_always = im.convert("RGB") + + with Image.open("Tests/images/chi.gif") as im: + expected_rgb_always_rgba = im.convert("RGBA") + + im.seek(1) + expected_different = im.convert("RGB") + + try: + GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS + with Image.open("Tests/images/iss634.gif") as im: + assert im.mode == "RGB" + assert_image_equal(im, expected_rgb_always) + + with Image.open("Tests/images/chi.gif") as im: + assert im.mode == "RGBA" + assert_image_equal(im, expected_rgb_always_rgba) + + GifImagePlugin.LOADING_STRATEGY = ( + GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY + ) + # Stay in P mode with only a global palette + with Image.open("Tests/images/chi.gif") as im: + assert im.mode == "P" + + im.seek(1) + assert im.mode == "P" + assert_image_equal(im.convert("RGB"), expected_different) + + # 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 + + +def test_optimize() -> None: + def test_grayscale(optimize: int) -> int: + im = Image.new("L", (1, 1), 0) + filename = BytesIO() + im.save(filename, "GIF", optimize=optimize) + return len(filename.getvalue()) + + def test_bilevel(optimize: int) -> int: + im = Image.new("1", (1, 1), 0) + test_file = BytesIO() + im.save(test_file, "GIF", optimize=optimize) + return len(test_file.getvalue()) + + assert test_grayscale(0) == 799 + assert test_grayscale(1) == 43 + assert test_bilevel(0) == 799 + assert test_bilevel(1) == 799 + + +@pytest.mark.parametrize( + "colors, size, expected_palette_length", + ( + # These do optimize the palette + (256, 511, 256), + (255, 511, 255), + (129, 511, 129), + (128, 511, 128), + (64, 511, 64), + (4, 511, 4), + # These don't optimize the palette + (128, 513, 256), + (64, 513, 256), + (4, 513, 256), + ), +) +def test_optimize_correctness( + colors: int, size: int, expected_palette_length: int +) -> None: + # 256 color Palette image, posterize to > 128 and < 128 levels. + # Size bigger and smaller than 512x512. + # Check the palette for number of colors allocated. + # Check for correctness after conversion back to RGB. + + # make an image with empty colors in the start of the palette range + im = Image.frombytes( + "P", (colors, colors), bytes(range(256 - colors, 256)) * colors + ) + im = im.resize((size, size)) + outfile = BytesIO() + im.save(outfile, "GIF") + outfile.seek(0) + with Image.open(outfile) as reloaded: + # check palette length + palette_length = max(i + 1 for i, v in enumerate(reloaded.histogram()) if v) + assert expected_palette_length == palette_length + + assert_image_equal(im.convert("RGB"), reloaded.convert("RGB")) + + +def test_optimize_full_l() -> None: + im = Image.frombytes("L", (16, 16), bytes(range(256))) + test_file = BytesIO() + im.save(test_file, "GIF", optimize=True) + assert im.mode == "L" + + +def test_optimize_if_palette_can_be_reduced_by_half() -> None: + im = Image.new("P", (8, 1)) + im.palette = ImagePalette.raw("RGB", bytes((0, 0, 0) * 150)) + for i in range(8): + im.putpixel((i, 0), (i + 1, 0, 0)) + + for optimize, colors in ((False, 256), (True, 8)): + out = BytesIO() + im.save(out, "GIF", optimize=optimize) + with Image.open(out) as reloaded: + assert len(reloaded.palette.palette) // 3 == colors + + +def test_full_palette_second_frame(tmp_path: Path) -> None: + out = str(tmp_path / "temp.gif") + im = Image.new("P", (1, 256)) + + full_palette_im = Image.new("P", (1, 256)) + for i in range(256): + full_palette_im.putpixel((0, i), i) + full_palette_im.palette = ImagePalette.ImagePalette( + "RGB", bytearray(i // 3 for i in range(768)) + ) + full_palette_im.palette.dirty = 1 + + im.save(out, save_all=True, append_images=[full_palette_im]) + + with Image.open(out) as reloaded: + reloaded.seek(1) + + for i in range(256): + reloaded.getpixel((0, i)) == i + + +def test_roundtrip(tmp_path: Path) -> None: + out = str(tmp_path / "temp.gif") + im = hopper() + im.save(out) + with Image.open(out) as reread: + assert_image_similar(reread.convert("RGB"), im, 50) + + +def test_roundtrip2(tmp_path: Path) -> None: + # see https://github.com/python-pillow/Pillow/issues/403 + out = str(tmp_path / "temp.gif") + with Image.open(TEST_GIF) as im: + im2 = im.copy() + im2.save(out) + with Image.open(out) as reread: + assert_image_similar(reread.convert("RGB"), hopper(), 50) + + +def test_roundtrip_save_all(tmp_path: Path) -> None: + # Single frame image + out = str(tmp_path / "temp.gif") + im = hopper() + im.save(out, save_all=True) + with Image.open(out) as reread: + assert_image_similar(reread.convert("RGB"), im, 50) + + # Multiframe image + with Image.open("Tests/images/dispose_bgnd.gif") as im: + out = str(tmp_path / "temp.gif") + im.save(out, save_all=True) + + with Image.open(out) as reread: + assert reread.n_frames == 5 + + +def test_roundtrip_save_all_1(tmp_path: Path) -> None: + out = str(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]) + + with Image.open(out) as reloaded: + assert reloaded.getpixel((0, 0)) == 0 + + reloaded.seek(1) + assert reloaded.getpixel((0, 0)) == 255 + + +@pytest.mark.parametrize( + "path, mode", + ( + ("Tests/images/dispose_bgnd.gif", "RGB"), + # Hexeditted copy of dispose_bgnd to add transparency + ("Tests/images/dispose_bgnd_rgba.gif", "RGBA"), + ), +) +def test_loading_multiple_palettes(path: str, mode: str) -> None: + with Image.open(path) as im: + assert im.mode == "P" + first_frame_colors = im.palette.colors.keys() + original_color = im.convert("RGB").load()[0, 0] + + im.seek(1) + assert im.mode == mode + if mode == "RGBA": + im = im.convert("RGB") + + # Check a color only from the old palette + assert im.load()[0, 0] == original_color + + # Check a color from the new palette + assert im.load()[24, 24] not in first_frame_colors + + +def test_headers_saving_for_animated_gifs(tmp_path: Path) -> None: + important_headers = ["background", "version", "duration", "loop"] + # Multiframe image + with Image.open("Tests/images/dispose_bgnd.gif") as im: + info = im.info.copy() + + out = str(tmp_path / "temp.gif") + im.save(out, save_all=True) + with Image.open(out) as reread: + for header in important_headers: + assert info[header] == reread.info[header] + + +def test_palette_handling(tmp_path: Path) -> None: + # see https://github.com/python-pillow/Pillow/issues/513 + + with Image.open(TEST_GIF) as im: + im = im.convert("RGB") + + im = im.resize((100, 100), Image.Resampling.LANCZOS) + im2 = im.convert("P", palette=Image.Palette.ADAPTIVE, colors=256) + + f = str(tmp_path / "temp.gif") + im2.save(f, optimize=True) + + with Image.open(f) as reloaded: + assert_image_similar(im, reloaded.convert("RGB"), 10) + + +def test_palette_434(tmp_path: Path) -> None: + # see https://github.com/python-pillow/Pillow/issues/434 + + def roundtrip(im: Image.Image, **kwargs: bool) -> Image.Image: + out = str(tmp_path / "temp.gif") + im.copy().save(out, **kwargs) + reloaded = Image.open(out) + + return reloaded + + orig = "Tests/images/test.colors.gif" + with Image.open(orig) as im: + with roundtrip(im) as reloaded: + assert_image_similar(im, reloaded, 1) + with roundtrip(im, optimize=True) as reloaded: + assert_image_similar(im, reloaded, 1) + + im = im.convert("RGB") + # check automatic P conversion + with roundtrip(im) as reloaded: + reloaded = reloaded.convert("RGB") + assert_image_equal(im, reloaded) + + +@pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") +def test_save_netpbm_bmp_mode(tmp_path: Path) -> None: + with Image.open(TEST_GIF) as img: + img = img.convert("RGB") + + tempfile = str(tmp_path / "temp.gif") + b = BytesIO() + GifImagePlugin._save_netpbm(img, b, tempfile) + with Image.open(tempfile) as reloaded: + assert_image_similar(img, reloaded.convert("RGB"), 0) + + +@pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") +def test_save_netpbm_l_mode(tmp_path: Path) -> None: + with Image.open(TEST_GIF) as img: + img = img.convert("L") + + tempfile = str(tmp_path / "temp.gif") + b = BytesIO() + GifImagePlugin._save_netpbm(img, b, tempfile) + with Image.open(tempfile) as reloaded: + assert_image_similar(img, reloaded.convert("L"), 0) + + +def test_seek() -> None: + with Image.open("Tests/images/dispose_none.gif") as img: + frame_count = 0 + try: + while True: + frame_count += 1 + img.seek(img.tell() + 1) + except EOFError: + assert frame_count == 5 + + +def test_seek_info() -> None: + with Image.open("Tests/images/iss634.gif") as im: + info = im.info.copy() + + im.seek(1) + im.seek(0) + + assert im.info == info + + +def test_seek_rewind() -> None: + with Image.open("Tests/images/iss634.gif") as im: + im.seek(2) + im.seek(1) + + with Image.open("Tests/images/iss634.gif") as expected: + expected.seek(1) + assert_image_equal(im, expected) + + +@pytest.mark.parametrize( + "path, n_frames", + ( + (TEST_GIF, 1), + ("Tests/images/comment_after_last_frame.gif", 2), + ("Tests/images/iss634.gif", 42), + ), +) +def test_n_frames(path: str, n_frames: int) -> None: + # Test is_animated before n_frames + with Image.open(path) as im: + assert im.is_animated == (n_frames != 1) + + # Test is_animated after n_frames + with Image.open(path) as im: + assert im.n_frames == n_frames + assert im.is_animated == (n_frames != 1) + + +def test_no_change() -> None: + # Test n_frames does not change the image + with Image.open("Tests/images/dispose_bgnd.gif") as im: + im.seek(1) + expected = im.copy() + assert im.n_frames == 5 + assert_image_equal(im, expected) + + # Test is_animated does not change the image + with Image.open("Tests/images/dispose_bgnd.gif") as im: + im.seek(3) + expected = im.copy() + assert im.is_animated + assert_image_equal(im, expected) + + with Image.open("Tests/images/comment_after_only_frame.gif") as im: + expected = Image.new("P", (1, 1)) + assert not im.is_animated + assert_image_equal(im, expected) + + +def test_eoferror() -> None: + with Image.open(TEST_GIF) as im: + n_frames = im.n_frames + + # Test seeking past the last frame + with pytest.raises(EOFError): + im.seek(n_frames) + assert im.tell() < n_frames + + # Test that seeking to the last frame does not raise an error + im.seek(n_frames - 1) + + +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"] + + +def test_dispose_none() -> None: + with Image.open("Tests/images/dispose_none.gif") as img: + try: + while True: + img.seek(img.tell() + 1) + assert img.disposal_method == 1 + except EOFError: + pass + + +def test_dispose_none_load_end() -> None: + # Test image created with: + # + # im = Image.open("transparent.gif") + # im_rotated = im.rotate(180) + # im.save("dispose_none_load_end.gif", + # save_all=True, append_images=[im_rotated], disposal=[1,2]) + with Image.open("Tests/images/dispose_none_load_end.gif") as img: + img.seek(1) + + assert_image_equal_tofile(img, "Tests/images/dispose_none_load_end_second.png") + + +def test_dispose_background() -> None: + with Image.open("Tests/images/dispose_bgnd.gif") as img: + try: + while True: + img.seek(img.tell() + 1) + assert img.disposal_method == 2 + except EOFError: + pass + + +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 + + +@pytest.mark.parametrize( + "loading_strategy, expected_colors", + ( + ( + GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST, + ( + (2, 1, 2), + ((0, 255, 24, 255), (0, 0, 255, 255), (0, 255, 24, 255)), + ((0, 0, 0, 0), (0, 0, 255, 255), (0, 0, 0, 0)), + ), + ), + ( + GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY, + ( + (2, 1, 2), + (0, 1, 0), + (2, 1, 2), + ), + ), + ), +) +def test_transparent_dispose( + loading_strategy: GifImagePlugin.LoadingStrategy, + expected_colors: tuple[tuple[int | tuple[int, int, int, int], ...]], +) -> 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 + + +def test_dispose_previous() -> None: + with Image.open("Tests/images/dispose_prev.gif") as img: + try: + while True: + img.seek(img.tell() + 1) + assert img.disposal_method == 3 + except EOFError: + pass + + +def test_dispose_previous_first_frame() -> None: + with Image.open("Tests/images/dispose_prev_first_frame.gif") as im: + im.seek(1) + assert_image_equal_tofile( + im, "Tests/images/dispose_prev_first_frame_seeked.png" + ) + + +def test_previous_frame_loaded() -> None: + with Image.open("Tests/images/dispose_none.gif") as img: + img.load() + img.seek(1) + img.load() + img.seek(2) + with Image.open("Tests/images/dispose_none.gif") as img_skipped: + img_skipped.seek(2) + assert_image_equal(img_skipped, img) + + +def test_save_dispose(tmp_path: Path) -> None: + out = str(tmp_path / "temp.gif") + im_list = [ + Image.new("L", (100, 100), "#000"), + Image.new("L", (100, 100), "#111"), + Image.new("L", (100, 100), "#222"), + ] + for method in range(0, 4): + im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=method) + with Image.open(out) as img: + for _ in range(2): + img.seek(img.tell() + 1) + assert img.disposal_method == method + + # Check per frame disposal + im_list[0].save( + out, + save_all=True, + append_images=im_list[1:], + disposal=tuple(range(len(im_list))), + ) + + with Image.open(out) as img: + 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") + + # Four colors: white, gray, black, red + circles = [(255, 255, 255), (153, 153, 153), (0, 0, 0), (255, 0, 0)] + + im_list = [] + for circle in circles: + # Red background + img = Image.new("RGB", (100, 100), (255, 0, 0)) + + # Circle in center of each frame + d = ImageDraw.Draw(img) + d.ellipse([(40, 40), (60, 60)], fill=circle) + + im_list.append(img) + + im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=2) + + with Image.open(out) as img: + for i, circle in enumerate(circles): + img.seek(i) + rgb_img = img.convert("RGB") + + # Check top left pixel matches background + assert rgb_img.getpixel((0, 0)) == (255, 0, 0) + + # Center remains red every frame + assert rgb_img.getpixel((50, 50)) == circle + + # Check that frame transparency wasn't added unnecessarily + assert getattr(img, "_frame_transparency") is None + + +def test_dispose2_diff(tmp_path: Path) -> None: + out = str(tmp_path / "temp.gif") + + # 4 frames: red/blue, red/red, blue/blue, red/blue + circles = [ + ((255, 0, 0, 255), (0, 0, 255, 255)), + ((255, 0, 0, 255), (255, 0, 0, 255)), + ((0, 0, 255, 255), (0, 0, 255, 255)), + ((255, 0, 0, 255), (0, 0, 255, 255)), + ] + + im_list = [] + for i in range(len(circles)): + # Transparent BG + img = Image.new("RGBA", (100, 100), (255, 255, 255, 0)) + + # Two circles per frame + d = ImageDraw.Draw(img) + d.ellipse([(0, 30), (40, 70)], fill=circles[i][0]) + d.ellipse([(60, 30), (100, 70)], fill=circles[i][1]) + + im_list.append(img) + + im_list[0].save( + out, save_all=True, append_images=im_list[1:], disposal=2, transparency=0 + ) + + with Image.open(out) as img: + for i, colours in enumerate(circles): + img.seek(i) + rgb_img = img.convert("RGBA") + + # Check left circle is correct colour + assert rgb_img.getpixel((20, 50)) == colours[0] + + # Check right circle is correct colour + assert rgb_img.getpixel((80, 50)) == colours[1] + + # Check BG is correct colour + assert rgb_img.getpixel((1, 1)) == (255, 255, 255, 0) + + +def test_dispose2_background(tmp_path: Path) -> None: + out = str(tmp_path / "temp.gif") + + im_list = [] + + im = Image.new("P", (100, 100)) + d = ImageDraw.Draw(im) + d.rectangle([(50, 0), (100, 100)], fill="#f00") + d.rectangle([(0, 0), (50, 100)], fill="#0f0") + im_list.append(im) + + im = Image.new("P", (100, 100)) + d = ImageDraw.Draw(im) + d.rectangle([(0, 0), (100, 50)], fill="#f00") + d.rectangle([(0, 50), (100, 100)], fill="#0f0") + im_list.append(im) + + im_list[0].save( + out, save_all=True, append_images=im_list[1:], disposal=[0, 2], background=1 + ) + + with Image.open(out) as im: + im.seek(1) + assert im.getpixel((0, 0)) == (255, 0, 0) + + +def test_dispose2_background_frame(tmp_path: Path) -> None: + out = str(tmp_path / "temp.gif") + + im_list = [Image.new("RGBA", (1, 20))] + + different_frame = Image.new("RGBA", (1, 20)) + different_frame.putpixel((0, 10), (255, 0, 0, 255)) + im_list.append(different_frame) + + # Frame that matches the background + im_list.append(Image.new("RGBA", (1, 20))) + + im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=2) + + with Image.open(out) as im: + assert im.n_frames == 3 + + +def test_dispose2_previous_frame(tmp_path: Path) -> None: + out = str(tmp_path / "temp.gif") + + im = Image.new("P", (100, 100)) + im.info["transparency"] = 0 + d = ImageDraw.Draw(im) + d.rectangle([(0, 0), (100, 50)], 1) + im.putpalette((0, 0, 0, 255, 0, 0)) + + im2 = Image.new("P", (100, 100)) + im2.putpalette((0, 0, 0)) + + im.save(out, save_all=True, append_images=[im2], disposal=[0, 2]) + + with Image.open(out) as im: + im.seek(1) + assert im.getpixel((0, 0)) == (0, 0, 0, 255) + + +def test_transparency_in_second_frame(tmp_path: Path) -> None: + out = str(tmp_path / "temp.gif") + with Image.open("Tests/images/different_transparency.gif") as im: + assert im.info["transparency"] == 0 + + # Seek to the second frame + im.seek(im.tell() + 1) + assert "transparency" not in im.info + + assert_image_equal_tofile(im, "Tests/images/different_transparency_merged.png") + + im.save(out, save_all=True) + + with Image.open(out) as reread: + reread.seek(reread.tell() + 1) + assert_image_equal_tofile( + reread, "Tests/images/different_transparency_merged.png" + ) + + +def test_no_transparency_in_second_frame() -> None: + with Image.open("Tests/images/iss634.gif") as img: + # Seek to the second frame + img.seek(img.tell() + 1) + assert "transparency" not in img.info + + # All transparent pixels should be replaced with the color from the first frame + assert img.histogram()[255] == 0 + + +def test_remapped_transparency(tmp_path: Path) -> None: + out = str(tmp_path / "temp.gif") + + im = Image.new("P", (1, 2)) + im2 = im.copy() + + # Add transparency at a higher index + # so that it will be optimized to a lower index + im.putpixel((0, 1), 5) + im.info["transparency"] = 5 + im.save(out, save_all=True, append_images=[im2]) + + with Image.open(out) as reloaded: + assert reloaded.info["transparency"] == reloaded.getpixel((0, 1)) + + +def test_duration(tmp_path: Path) -> None: + duration = 1000 + + out = str(tmp_path / "temp.gif") + im = Image.new("L", (100, 100), "#000") + + # Check that the argument has priority over the info settings + im.info["duration"] = 100 + im.save(out, duration=duration) + + with Image.open(out) as reread: + assert reread.info["duration"] == duration + + +def test_multiple_duration(tmp_path: Path) -> None: + duration_list = [1000, 2000, 3000] + + out = str(tmp_path / "temp.gif") + im_list = [ + Image.new("L", (100, 100), "#000"), + Image.new("L", (100, 100), "#111"), + Image.new("L", (100, 100), "#222"), + ] + + # Duration as list + im_list[0].save( + out, save_all=True, append_images=im_list[1:], duration=duration_list + ) + with Image.open(out) as reread: + for duration in duration_list: + assert reread.info["duration"] == duration + try: + reread.seek(reread.tell() + 1) + except EOFError: + pass + + # Duration as tuple + im_list[0].save( + out, save_all=True, append_images=im_list[1:], duration=tuple(duration_list) + ) + with Image.open(out) as reread: + for duration in duration_list: + assert reread.info["duration"] == duration + try: + reread.seek(reread.tell() + 1) + except EOFError: + pass + + +def test_roundtrip_info_duration(tmp_path: Path) -> None: + duration_list = [100, 500, 500] + + out = str(tmp_path / "temp.gif") + with Image.open("Tests/images/transparent_dispose.gif") as im: + assert [ + frame.info["duration"] for frame in ImageSequence.Iterator(im) + ] == duration_list + + im.save(out, save_all=True) + + with Image.open(out) as reloaded: + assert [ + frame.info["duration"] for frame in ImageSequence.Iterator(reloaded) + ] == duration_list + + +def test_roundtrip_info_duration_combined(tmp_path: Path) -> None: + out = str(tmp_path / "temp.gif") + with Image.open("Tests/images/duplicate_frame.gif") as im: + assert [frame.info["duration"] for frame in ImageSequence.Iterator(im)] == [ + 1000, + 1000, + 1000, + ] + im.save(out, save_all=True) + + with Image.open(out) as reloaded: + assert [ + frame.info["duration"] for frame in ImageSequence.Iterator(reloaded) + ] == [1000, 2000] + + +def test_identical_frames(tmp_path: Path) -> None: + duration_list = [1000, 1500, 2000, 4000] + + out = str(tmp_path / "temp.gif") + im_list = [ + Image.new("L", (100, 100), "#000"), + Image.new("L", (100, 100), "#000"), + Image.new("L", (100, 100), "#000"), + Image.new("L", (100, 100), "#111"), + ] + + # Duration as list + im_list[0].save( + out, save_all=True, append_images=im_list[1:], duration=duration_list + ) + with Image.open(out) as reread: + # Assert that the first three frames were combined + assert reread.n_frames == 2 + + # Assert that the new duration is the total of the identical frames + assert reread.info["duration"] == 4500 + + +@pytest.mark.parametrize( + "duration", + ( + [1000, 1500, 2000], + (1000, 1500, 2000), + # One more duration than the number of frames + [1000, 1500, 2000, 4000], + 1500, + ), +) +def test_identical_frames_to_single_frame( + duration: int | list[int], tmp_path: Path +) -> None: + out = str(tmp_path / "temp.gif") + im_list = [ + Image.new("L", (100, 100), "#000"), + Image.new("L", (100, 100), "#000"), + Image.new("L", (100, 100), "#000"), + ] + + im_list[0].save(out, save_all=True, append_images=im_list[1:], duration=duration) + with Image.open(out) as reread: + # Assert that all frames were combined + assert reread.n_frames == 1 + + # Assert that the new duration is the total of the identical frames + assert reread.info["duration"] == 4500 + + +def test_loop_none(tmp_path: Path) -> None: + out = str(tmp_path / "temp.gif") + im = Image.new("L", (100, 100), "#000") + im.save(out, loop=None) + with Image.open(out) as reread: + assert "loop" not in reread.info + + +def test_number_of_loops(tmp_path: Path) -> None: + number_of_loops = 2 + + out = str(tmp_path / "temp.gif") + im = Image.new("L", (100, 100), "#000") + im.save(out, loop=number_of_loops) + with Image.open(out) as reread: + assert reread.info["loop"] == number_of_loops + + # Check that even if a subsequent GIF frame has the number of loops specified, + # only the value from the first frame is used + with Image.open("Tests/images/duplicate_number_of_loops.gif") as im: + assert im.info["loop"] == 2 + + im.seek(1) + assert im.info["loop"] == 2 + + +def test_background(tmp_path: Path) -> None: + out = str(tmp_path / "temp.gif") + im = Image.new("L", (100, 100), "#000") + im.info["background"] = 1 + im.save(out) + with Image.open(out) as reread: + assert reread.info["background"] == im.info["background"] + + +def test_webp_background(tmp_path: Path) -> None: + out = str(tmp_path / "temp.gif") + + # Test opaque WebP background + if features.check("webp") and features.check("webp_anim"): + with Image.open("Tests/images/hopper.webp") as im: + assert im.info["background"] == (255, 255, 255, 255) + im.save(out) + + # Test non-opaque WebP background + im = Image.new("L", (100, 100), "#000") + im.info["background"] = (0, 0, 0, 0) + im.save(out) + + +def test_comment(tmp_path: Path) -> None: + with Image.open(TEST_GIF) as im: + assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0" + + out = str(tmp_path / "temp.gif") + im = Image.new("L", (100, 100), "#000") + im.info["comment"] = b"Test comment text" + im.save(out) + with Image.open(out) as reread: + assert reread.info["comment"] == im.info["comment"] + + im.info["comment"] = "Test comment text" + im.save(out) + with Image.open(out) as reread: + assert reread.info["comment"] == im.info["comment"].encode() + + # Test that GIF89a is used for comments + assert reread.info["version"] == b"GIF89a" + + +def test_comment_over_255(tmp_path: Path) -> None: + out = str(tmp_path / "temp.gif") + im = Image.new("L", (100, 100), "#000") + comment = b"Test comment text" + while len(comment) < 256: + comment += comment + im.info["comment"] = comment + im.save(out) + with Image.open(out) as reread: + assert reread.info["comment"] == comment + + # Test that GIF89a is used for comments + assert reread.info["version"] == b"GIF89a" + + +def test_zero_comment_subblocks() -> None: + with Image.open("Tests/images/hopper_zero_comment_subblocks.gif") as im: + assert_image_equal_tofile(im, TEST_GIF) + + +def test_read_multiple_comment_blocks() -> None: + with Image.open("Tests/images/multiple_comments.gif") as im: + # Multiple comment blocks in a frame are separated not concatenated + assert im.info["comment"] == b"Test comment 1\nTest comment 2" + + +def test_empty_string_comment(tmp_path: Path) -> None: + out = str(tmp_path / "temp.gif") + with Image.open("Tests/images/chi.gif") as im: + assert "comment" in im.info + + # Empty string comment should suppress existing comment + im.save(out, save_all=True, comment="") + + with Image.open(out) as reread: + for frame in ImageSequence.Iterator(reread): + assert "comment" not in frame.info + + +def test_retain_comment_in_subsequent_frames(tmp_path: Path) -> None: + # Test that a comment block at the beginning is kept + with Image.open("Tests/images/chi.gif") as im: + for frame in ImageSequence.Iterator(im): + assert frame.info["comment"] == b"Created with GIMP" + + with Image.open("Tests/images/second_frame_comment.gif") as im: + assert "comment" not in im.info + + # Test that a comment in the middle is read + im.seek(1) + assert im.info["comment"] == b"Comment in the second frame" + + # Test that it is still present in a later frame + im.seek(2) + assert im.info["comment"] == b"Comment in the second frame" + + # Test that rewinding removes the comment + im.seek(0) + assert "comment" not in im.info + + # Test that a saved image keeps the comment + out = str(tmp_path / "temp.gif") + with Image.open("Tests/images/dispose_prev.gif") as im: + im.save(out, save_all=True, comment="Test") + + with Image.open(out) as reread: + for frame in ImageSequence.Iterator(reread): + assert frame.info["comment"] == b"Test" + + +def test_version(tmp_path: Path) -> None: + out = str(tmp_path / "temp.gif") + + def assert_version_after_save(im: Image.Image, version: bytes) -> None: + im.save(out) + with Image.open(out) as reread: + assert reread.info["version"] == version + + # Test that GIF87a is used by default + im = Image.new("L", (100, 100), "#000") + assert_version_after_save(im, b"GIF87a") + + # Test setting the version to 89a + im = Image.new("L", (100, 100), "#000") + im.info["version"] = b"89a" + assert_version_after_save(im, b"GIF89a") + + # Test that adding a GIF89a feature changes the version + im.info["transparency"] = 1 + assert_version_after_save(im, b"GIF89a") + + # Test that a GIF87a image is also saved in that format + with Image.open("Tests/images/test.colors.gif") as im: + assert_version_after_save(im, b"GIF87a") + + # Test that a GIF89a image is also saved in that format + im.info["version"] = b"GIF89a" + assert_version_after_save(im, b"GIF87a") + + +def test_append_images(tmp_path: Path) -> None: + out = str(tmp_path / "temp.gif") + + # Test appending single frame images + im = Image.new("RGB", (100, 100), "#f00") + ims = [Image.new("RGB", (100, 100), color) for color in ["#0f0", "#00f"]] + im.copy().save(out, save_all=True, append_images=ims) + + with Image.open(out) as reread: + assert reread.n_frames == 3 + + # Tests appending using a generator + def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]: + yield from ims + + im.save(out, save_all=True, append_images=im_generator(ims)) + + with Image.open(out) as reread: + assert reread.n_frames == 3 + + # Tests appending single and multiple frame images + with Image.open("Tests/images/dispose_none.gif") as im: + with Image.open("Tests/images/dispose_prev.gif") as im2: + im.save(out, save_all=True, append_images=[im2]) + + with Image.open(out) as reread: + assert reread.n_frames == 10 + + +def test_append_different_size_image(tmp_path: Path) -> None: + out = str(tmp_path / "temp.gif") + + im = Image.new("RGB", (100, 100)) + bigger_im = Image.new("RGB", (200, 200), "#f00") + + im.save(out, save_all=True, append_images=[bigger_im]) + + with Image.open(out) as reread: + assert reread.size == (100, 100) + + reread.seek(1) + assert reread.size == (100, 100) + + +def test_transparent_optimize(tmp_path: Path) -> None: + # From issue #2195, if the transparent color is incorrectly optimized out, GIF loses + # transparency. + # Need a palette that isn't using the 0 color, + # where the transparent color is actually the top palette entry to trigger the bug. + + data = bytes(range(1, 254)) + palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) + + im = Image.new("L", (253, 1)) + im.frombytes(data) + im.putpalette(palette) + + out = str(tmp_path / "temp.gif") + im.save(out, transparency=im.getpixel((252, 0))) + + with Image.open(out) as reloaded: + assert reloaded.info["transparency"] == reloaded.getpixel((252, 0)) + + +def test_removed_transparency(tmp_path: Path) -> None: + out = str(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): + im.save(out) + + with Image.open(out) as reloaded: + assert "transparency" not in reloaded.info + + +def test_rgb_transparency(tmp_path: Path) -> None: + out = str(tmp_path / "temp.gif") + + # Single frame + im = Image.new("RGB", (1, 1)) + im.info["transparency"] = (255, 0, 0) + im.save(out) + + with Image.open(out) as reloaded: + assert "transparency" in reloaded.info + + # Multiple frames + im = Image.new("RGB", (1, 1)) + im.info["transparency"] = b"" + ims = [Image.new("RGB", (1, 1))] + with pytest.warns(UserWarning): + im.save(out, save_all=True, append_images=ims) + + with Image.open(out) as reloaded: + assert "transparency" not in reloaded.info + + +def test_rgba_transparency(tmp_path: Path) -> None: + out = str(tmp_path / "temp.gif") + + im = hopper("P") + im.save(out, save_all=True, append_images=[Image.new("RGBA", im.size)]) + + with Image.open(out) as reloaded: + reloaded.seek(1) + assert_image_equal(hopper("P").convert("RGB"), reloaded) + + +def test_background_outside_palettte(tmp_path: Path) -> 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") + + im = Image.new("RGB", (100, 100), "#fff") + ims = [Image.new("RGB", (100, 100), "#000")] + im.save(out, save_all=True, append_images=ims) + + with Image.open(out) as reread: + assert reread.n_frames == 2 + + +def test_bbox_alpha(tmp_path: Path) -> None: + out = str(tmp_path / "temp.gif") + + im = Image.new("RGBA", (1, 2), (255, 0, 0, 255)) + im.putpixel((0, 1), (255, 0, 0, 0)) + im2 = Image.new("RGBA", (1, 2), (255, 0, 0, 0)) + im.save(out, save_all=True, append_images=[im2]) + + with Image.open(out) as reread: + assert reread.n_frames == 2 + + +def test_palette_save_L(tmp_path: Path) -> None: + # Generate an L mode image with a separate palette + + im = hopper("P") + im_l = Image.frombytes("L", im.size, im.tobytes()) + palette = im.getpalette() + assert palette is not None + + out = str(tmp_path / "temp.gif") + im_l.save(out, palette=bytes(palette)) + + with Image.open(out) as reloaded: + assert_image_equal(reloaded.convert("RGB"), im.convert("RGB")) + + +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") + im.save(out, palette=bytes((1, 2, 3, 4, 5, 6))) + + with Image.open(out) as reloaded: + reloaded_rgb = reloaded.convert("RGB") + + assert reloaded_rgb.getpixel((0, 0)) == (1, 2, 3) + assert reloaded_rgb.getpixel((0, 1)) == (4, 5, 6) + + +def test_palette_save_duplicate_entries(tmp_path: Path) -> None: + im = Image.new("P", (1, 2)) + im.putpixel((0, 1), 1) + + im.putpalette((0, 0, 0, 0, 0, 0)) + + out = str(tmp_path / "temp.gif") + im.save(out, palette=[0, 0, 0, 0, 0, 0, 1, 1, 1]) + + with Image.open(out) as reloaded: + assert reloaded.convert("RGB").getpixel((0, 1)) == (0, 0, 0) + + +def test_palette_save_all_P(tmp_path: Path) -> None: + frames = [] + colors = ((255, 0, 0), (0, 255, 0)) + for color in colors: + frame = Image.new("P", (100, 100)) + frame.putpalette(color) + frames.append(frame) + + out = str(tmp_path / "temp.gif") + frames[0].save( + out, save_all=True, palette=[255, 0, 0, 0, 255, 0], append_images=frames[1:] + ) + + with Image.open(out) as im: + # Assert that the frames are correct, and each frame has the same palette + assert_image_equal(im.convert("RGB"), frames[0].convert("RGB")) + assert im.palette.palette == im.global_palette.palette + + im.seek(1) + assert_image_equal(im.convert("RGB"), frames[1].convert("RGB")) + assert im.palette.palette == im.global_palette.palette + + +def test_palette_save_ImagePalette(tmp_path: Path) -> None: + # Pass in a different palette, as an ImagePalette.ImagePalette + # effectively the same as test_palette_save_P + + im = hopper("P") + palette = ImagePalette.ImagePalette("RGB", list(range(256))[::-1] * 3) + + out = str(tmp_path / "temp.gif") + im.save(out, palette=palette) + + with Image.open(out) as reloaded: + im.putpalette(palette) + assert_image_equal(reloaded.convert("RGB"), im.convert("RGB")) + + +def test_save_I(tmp_path: Path) -> None: + # Test saving something that would trigger the auto-convert to 'L' + + im = hopper("I") + + out = str(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: + # Test getheader/getdata against legacy values. + # Create a 'P' image with holes in the palette. + im = Image._wedge().resize((16, 16), Image.Resampling.NEAREST) + im.putpalette(ImagePalette.ImagePalette("RGB")) + im.info = {"background": 0} + + passed_palette = bytes(255 - i // 3 for i in range(768)) + + GifImagePlugin._FORCE_OPTIMIZE = True + try: + h = GifImagePlugin.getheader(im, passed_palette) + d = GifImagePlugin.getdata(im) + + import pickle + + # Enable to get target values on pre-refactor version + # with open('Tests/images/gif_header_data.pkl', 'wb') as f: + # pickle.dump((h, d), f, 1) + with open("Tests/images/gif_header_data.pkl", "rb") as f: + (h_target, d_target) = pickle.load(f) + + assert h == h_target + assert d == d_target + finally: + GifImagePlugin._FORCE_OPTIMIZE = False + + +def test_lzw_bits() -> 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 + # codec error prepatch + im.load() + + +def test_extents() -> None: + with Image.open("Tests/images/test_extents.gif") as im: + assert im.size == (100, 100) + + # Check that n_frames does not change the size + assert im.n_frames == 2 + assert im.size == (100, 100) + + im.seek(1) + assert im.size == (150, 150) + + +def test_missing_background() -> None: + # The Global Color Table Flag isn't set, so there is no background color index, + # but the disposal method is "Restore to background color" + with Image.open("Tests/images/missing_background.gif") as im: + im.seek(1) + assert_image_equal_tofile(im, "Tests/images/missing_background_first_frame.png") + + +def test_saving_rgba(tmp_path: Path) -> None: + out = str(tmp_path / "temp.gif") + with Image.open("Tests/images/transparent.png") as im: + im.save(out) + + with Image.open(out) as reloaded: + reloaded_rgba = reloaded.convert("RGBA") + assert reloaded_rgba.load()[0, 0][3] == 0 diff --git a/Tests/test_file_gimpgradient.py b/Tests/test_file_gimpgradient.py index 006ee952d..e579a7987 100644 --- a/Tests/test_file_gimpgradient.py +++ b/Tests/test_file_gimpgradient.py @@ -1,126 +1,126 @@ -from __future__ import annotations - -from PIL import GimpGradientFile, ImagePalette - - -def test_linear_pos_le_middle() -> None: - # Arrange - middle = 0.5 - pos = 0.25 - - # Act - ret = GimpGradientFile.linear(middle, pos) - - # Assert - assert ret == 0.25 - - -def test_linear_pos_le_small_middle() -> None: - # Arrange - middle = 1e-11 - pos = 1e-12 - - # Act - ret = GimpGradientFile.linear(middle, pos) - - # Assert - assert ret == 0.0 - - -def test_linear_pos_gt_middle() -> None: - # Arrange - middle = 0.5 - pos = 0.75 - - # Act - ret = GimpGradientFile.linear(middle, pos) - - # Assert - assert ret == 0.75 - - -def test_linear_pos_gt_small_middle() -> None: - # Arrange - middle = 1 - 1e-11 - pos = 1 - 1e-12 - - # Act - ret = GimpGradientFile.linear(middle, pos) - - # Assert - assert ret == 1.0 - - -def test_curved() -> None: - # Arrange - middle = 0.5 - pos = 0.75 - - # Act - ret = GimpGradientFile.curved(middle, pos) - - # Assert - assert ret == 0.75 - - -def test_sine() -> None: - # Arrange - middle = 0.5 - pos = 0.75 - - # Act - ret = GimpGradientFile.sine(middle, pos) - - # Assert - assert ret == 0.8535533905932737 - - -def test_sphere_increasing() -> None: - # Arrange - middle = 0.5 - pos = 0.75 - - # Act - ret = GimpGradientFile.sphere_increasing(middle, pos) - - # Assert - assert round(abs(ret - 0.9682458365518543), 7) == 0 - - -def test_sphere_decreasing() -> None: - # Arrange - middle = 0.5 - pos = 0.75 - - # Act - ret = GimpGradientFile.sphere_decreasing(middle, pos) - - # Assert - assert ret == 0.3385621722338523 - - -def test_load_via_imagepalette() -> None: - # Arrange - test_file = "Tests/images/gimp_gradient.ggr" - - # Act - palette = ImagePalette.load(test_file) - - # Assert - # load returns raw palette information - assert len(palette[0]) == 1024 - assert palette[1] == "RGBA" - - -def test_load_1_3_via_imagepalette() -> None: - # Arrange - # GIMP 1.3 gradient files contain a name field - test_file = "Tests/images/gimp_gradient_with_name.ggr" - - # Act - palette = ImagePalette.load(test_file) - - # Assert - # load returns raw palette information - assert len(palette[0]) == 1024 - assert palette[1] == "RGBA" +from __future__ import annotations + +from PIL import GimpGradientFile, ImagePalette + + +def test_linear_pos_le_middle() -> None: + # Arrange + middle = 0.5 + pos = 0.25 + + # Act + ret = GimpGradientFile.linear(middle, pos) + + # Assert + assert ret == 0.25 + + +def test_linear_pos_le_small_middle() -> None: + # Arrange + middle = 1e-11 + pos = 1e-12 + + # Act + ret = GimpGradientFile.linear(middle, pos) + + # Assert + assert ret == 0.0 + + +def test_linear_pos_gt_middle() -> None: + # Arrange + middle = 0.5 + pos = 0.75 + + # Act + ret = GimpGradientFile.linear(middle, pos) + + # Assert + assert ret == 0.75 + + +def test_linear_pos_gt_small_middle() -> None: + # Arrange + middle = 1 - 1e-11 + pos = 1 - 1e-12 + + # Act + ret = GimpGradientFile.linear(middle, pos) + + # Assert + assert ret == 1.0 + + +def test_curved() -> None: + # Arrange + middle = 0.5 + pos = 0.75 + + # Act + ret = GimpGradientFile.curved(middle, pos) + + # Assert + assert ret == 0.75 + + +def test_sine() -> None: + # Arrange + middle = 0.5 + pos = 0.75 + + # Act + ret = GimpGradientFile.sine(middle, pos) + + # Assert + assert ret == 0.8535533905932737 + + +def test_sphere_increasing() -> None: + # Arrange + middle = 0.5 + pos = 0.75 + + # Act + ret = GimpGradientFile.sphere_increasing(middle, pos) + + # Assert + assert round(abs(ret - 0.9682458365518543), 7) == 0 + + +def test_sphere_decreasing() -> None: + # Arrange + middle = 0.5 + pos = 0.75 + + # Act + ret = GimpGradientFile.sphere_decreasing(middle, pos) + + # Assert + assert ret == 0.3385621722338523 + + +def test_load_via_imagepalette() -> None: + # Arrange + test_file = "Tests/images/gimp_gradient.ggr" + + # Act + palette = ImagePalette.load(test_file) + + # Assert + # load returns raw palette information + assert len(palette[0]) == 1024 + assert palette[1] == "RGBA" + + +def test_load_1_3_via_imagepalette() -> None: + # Arrange + # GIMP 1.3 gradient files contain a name field + test_file = "Tests/images/gimp_gradient_with_name.ggr" + + # Act + palette = ImagePalette.load(test_file) + + # Assert + # load returns raw palette information + assert len(palette[0]) == 1024 + assert palette[1] == "RGBA" diff --git a/Tests/test_file_gimppalette.py b/Tests/test_file_gimppalette.py index e8d5f1705..a4ee563f9 100644 --- a/Tests/test_file_gimppalette.py +++ b/Tests/test_file_gimppalette.py @@ -1,34 +1,34 @@ -from __future__ import annotations - -import pytest - -from PIL.GimpPaletteFile import GimpPaletteFile - - -def test_sanity() -> None: - with open("Tests/images/test.gpl", "rb") as fp: - GimpPaletteFile(fp) - - with open("Tests/images/hopper.jpg", "rb") as fp: - with pytest.raises(SyntaxError): - GimpPaletteFile(fp) - - with open("Tests/images/bad_palette_file.gpl", "rb") as fp: - with pytest.raises(SyntaxError): - GimpPaletteFile(fp) - - with open("Tests/images/bad_palette_entry.gpl", "rb") as fp: - with pytest.raises(ValueError): - GimpPaletteFile(fp) - - -def test_get_palette() -> None: - # Arrange - with open("Tests/images/custom_gimp_palette.gpl", "rb") as fp: - palette_file = GimpPaletteFile(fp) - - # Act - palette, mode = palette_file.getpalette() - - # Assert - assert mode == "RGB" +from __future__ import annotations + +import pytest + +from PIL.GimpPaletteFile import GimpPaletteFile + + +def test_sanity() -> None: + with open("Tests/images/test.gpl", "rb") as fp: + GimpPaletteFile(fp) + + with open("Tests/images/hopper.jpg", "rb") as fp: + with pytest.raises(SyntaxError): + GimpPaletteFile(fp) + + with open("Tests/images/bad_palette_file.gpl", "rb") as fp: + with pytest.raises(SyntaxError): + GimpPaletteFile(fp) + + with open("Tests/images/bad_palette_entry.gpl", "rb") as fp: + with pytest.raises(ValueError): + GimpPaletteFile(fp) + + +def test_get_palette() -> None: + # Arrange + with open("Tests/images/custom_gimp_palette.gpl", "rb") as fp: + palette_file = GimpPaletteFile(fp) + + # Act + palette, mode = palette_file.getpalette() + + # Assert + assert mode == "RGB" diff --git a/Tests/test_file_gribstub.py b/Tests/test_file_gribstub.py index 86a9064fc..2b0b9cd61 100644 --- a/Tests/test_file_gribstub.py +++ b/Tests/test_file_gribstub.py @@ -1,83 +1,83 @@ -from __future__ import annotations - -from pathlib import Path -from typing import IO - -import pytest - -from PIL import GribStubImagePlugin, Image, ImageFile - -from .helper import hopper - -TEST_FILE = "Tests/images/WAlaska.wind.7days.grb" - - -def test_open() -> None: - # Act - with Image.open(TEST_FILE) as im: - # Assert - assert im.format == "GRIB" - - # Dummy data from the stub - assert im.mode == "F" - assert im.size == (1, 1) - - -def test_invalid_file() -> None: - # Arrange - invalid_file = "Tests/images/flower.jpg" - - # Act / Assert - with pytest.raises(SyntaxError): - GribStubImagePlugin.GribStubImageFile(invalid_file) - - -def test_load() -> None: - # Arrange - with Image.open(TEST_FILE) as im: - # Act / Assert: stub cannot load without an implemented handler - with pytest.raises(OSError): - im.load() - - -def test_save(tmp_path: Path) -> None: - # Arrange - im = hopper() - tmpfile = str(tmp_path / "temp.grib") - - # Act / Assert: stub cannot save without an implemented handler - with pytest.raises(OSError): - im.save(tmpfile) - - -def test_handler(tmp_path: Path) -> None: - class TestHandler(ImageFile.StubHandler): - opened = False - loaded = False - saved = False - - def open(self, im: Image.Image) -> None: - self.opened = True - - def load(self, im: Image.Image) -> Image.Image: - self.loaded = True - im.fp.close() - return Image.new("RGB", (1, 1)) - - def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None: - self.saved = True - - handler = TestHandler() - GribStubImagePlugin.register_handler(handler) - with Image.open(TEST_FILE) as im: - assert handler.opened - assert not handler.loaded - - im.load() - assert handler.loaded - - temp_file = str(tmp_path / "temp.grib") - im.save(temp_file) - assert handler.saved - - GribStubImagePlugin._handler = None +from __future__ import annotations + +from pathlib import Path +from typing import IO + +import pytest + +from PIL import GribStubImagePlugin, Image, ImageFile + +from .helper import hopper + +TEST_FILE = "Tests/images/WAlaska.wind.7days.grb" + + +def test_open() -> None: + # Act + with Image.open(TEST_FILE) as im: + # Assert + assert im.format == "GRIB" + + # Dummy data from the stub + assert im.mode == "F" + assert im.size == (1, 1) + + +def test_invalid_file() -> None: + # Arrange + invalid_file = "Tests/images/flower.jpg" + + # Act / Assert + with pytest.raises(SyntaxError): + GribStubImagePlugin.GribStubImageFile(invalid_file) + + +def test_load() -> None: + # Arrange + with Image.open(TEST_FILE) as im: + # Act / Assert: stub cannot load without an implemented handler + with pytest.raises(OSError): + im.load() + + +def test_save(tmp_path: Path) -> None: + # Arrange + im = hopper() + tmpfile = str(tmp_path / "temp.grib") + + # Act / Assert: stub cannot save without an implemented handler + with pytest.raises(OSError): + im.save(tmpfile) + + +def test_handler(tmp_path: Path) -> None: + class TestHandler(ImageFile.StubHandler): + opened = False + loaded = False + saved = False + + def open(self, im: Image.Image) -> None: + self.opened = True + + def load(self, im: Image.Image) -> Image.Image: + self.loaded = True + im.fp.close() + return Image.new("RGB", (1, 1)) + + def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None: + self.saved = True + + handler = TestHandler() + GribStubImagePlugin.register_handler(handler) + with Image.open(TEST_FILE) as im: + assert handler.opened + assert not handler.loaded + + im.load() + assert handler.loaded + + temp_file = str(tmp_path / "temp.grib") + im.save(temp_file) + assert handler.saved + + GribStubImagePlugin._handler = None diff --git a/Tests/test_file_hdf5stub.py b/Tests/test_file_hdf5stub.py index ee1544c51..87cfcf76b 100644 --- a/Tests/test_file_hdf5stub.py +++ b/Tests/test_file_hdf5stub.py @@ -1,85 +1,85 @@ -from __future__ import annotations - -from io import BytesIO -from pathlib import Path -from typing import IO - -import pytest - -from PIL import Hdf5StubImagePlugin, Image, ImageFile - -TEST_FILE = "Tests/images/hdf5.h5" - - -def test_open() -> None: - # Act - with Image.open(TEST_FILE) as im: - # Assert - assert im.format == "HDF5" - - # Dummy data from the stub - assert im.mode == "F" - assert im.size == (1, 1) - - -def test_invalid_file() -> None: - # Arrange - invalid_file = "Tests/images/flower.jpg" - - # Act / Assert - with pytest.raises(SyntaxError): - Hdf5StubImagePlugin.HDF5StubImageFile(invalid_file) - - -def test_load() -> None: - # Arrange - with Image.open(TEST_FILE) as im: - # Act / Assert: stub cannot load without an implemented handler - with pytest.raises(OSError): - im.load() - - -def test_save() -> None: - # Arrange - with Image.open(TEST_FILE) as im: - dummy_fp = BytesIO() - dummy_filename = "dummy.filename" - - # Act / Assert: stub cannot save without an implemented handler - with pytest.raises(OSError): - im.save(dummy_filename) - with pytest.raises(OSError): - Hdf5StubImagePlugin._save(im, dummy_fp, dummy_filename) - - -def test_handler(tmp_path: Path) -> None: - class TestHandler(ImageFile.StubHandler): - opened = False - loaded = False - saved = False - - def open(self, im: Image.Image) -> None: - self.opened = True - - def load(self, im: Image.Image) -> Image.Image: - self.loaded = True - im.fp.close() - return Image.new("RGB", (1, 1)) - - def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None: - self.saved = True - - handler = TestHandler() - Hdf5StubImagePlugin.register_handler(handler) - with Image.open(TEST_FILE) as im: - assert handler.opened - assert not handler.loaded - - im.load() - assert handler.loaded - - temp_file = str(tmp_path / "temp.h5") - im.save(temp_file) - assert handler.saved - - Hdf5StubImagePlugin._handler = None +from __future__ import annotations + +from io import BytesIO +from pathlib import Path +from typing import IO + +import pytest + +from PIL import Hdf5StubImagePlugin, Image, ImageFile + +TEST_FILE = "Tests/images/hdf5.h5" + + +def test_open() -> None: + # Act + with Image.open(TEST_FILE) as im: + # Assert + assert im.format == "HDF5" + + # Dummy data from the stub + assert im.mode == "F" + assert im.size == (1, 1) + + +def test_invalid_file() -> None: + # Arrange + invalid_file = "Tests/images/flower.jpg" + + # Act / Assert + with pytest.raises(SyntaxError): + Hdf5StubImagePlugin.HDF5StubImageFile(invalid_file) + + +def test_load() -> None: + # Arrange + with Image.open(TEST_FILE) as im: + # Act / Assert: stub cannot load without an implemented handler + with pytest.raises(OSError): + im.load() + + +def test_save() -> None: + # Arrange + with Image.open(TEST_FILE) as im: + dummy_fp = BytesIO() + dummy_filename = "dummy.filename" + + # Act / Assert: stub cannot save without an implemented handler + with pytest.raises(OSError): + im.save(dummy_filename) + with pytest.raises(OSError): + Hdf5StubImagePlugin._save(im, dummy_fp, dummy_filename) + + +def test_handler(tmp_path: Path) -> None: + class TestHandler(ImageFile.StubHandler): + opened = False + loaded = False + saved = False + + def open(self, im: Image.Image) -> None: + self.opened = True + + def load(self, im: Image.Image) -> Image.Image: + self.loaded = True + im.fp.close() + return Image.new("RGB", (1, 1)) + + def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None: + self.saved = True + + handler = TestHandler() + Hdf5StubImagePlugin.register_handler(handler) + with Image.open(TEST_FILE) as im: + assert handler.opened + assert not handler.loaded + + im.load() + assert handler.loaded + + temp_file = str(tmp_path / "temp.h5") + im.save(temp_file) + assert handler.saved + + Hdf5StubImagePlugin._handler = None diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index 488984aef..6c6e17a76 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -1,156 +1,156 @@ -from __future__ import annotations - -import io -import os -import warnings -from pathlib import Path - -import pytest - -from PIL import IcnsImagePlugin, Image, _binary - -from .helper import assert_image_equal, assert_image_similar_tofile, skip_unless_feature - -# sample icon file -TEST_FILE = "Tests/images/pillow.icns" - - -def test_sanity() -> None: - # Loading this icon by default should result in the largest size - # (512x512@2x) being loaded - with Image.open(TEST_FILE) as im: - # Assert that there is no unclosed file warning - with warnings.catch_warnings(): - im.load() - - assert im.mode == "RGBA" - assert im.size == (1024, 1024) - assert im.format == "ICNS" - - -def test_load() -> None: - with Image.open(TEST_FILE) as im: - assert im.load()[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) - - -def test_save(tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.icns") - - with Image.open(TEST_FILE) as im: - im.save(temp_file) - - with Image.open(temp_file) as reread: - assert reread.mode == "RGBA" - assert reread.size == (1024, 1024) - assert reread.format == "ICNS" - - file_length = os.path.getsize(temp_file) - with open(temp_file, "rb") as fp: - fp.seek(4) - assert _binary.i32be(fp.read(4)) == file_length - - -def test_save_append_images(tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.icns") - provided_im = Image.new("RGBA", (32, 32), (255, 0, 0, 128)) - - with Image.open(TEST_FILE) as im: - im.save(temp_file, append_images=[provided_im]) - - assert_image_similar_tofile(im, temp_file, 1) - - with Image.open(temp_file) as reread: - reread.size = (16, 16, 2) - reread.load() - assert_image_equal(reread, provided_im) - - -def test_save_fp() -> None: - fp = io.BytesIO() - - with Image.open(TEST_FILE) as im: - im.save(fp, format="ICNS") - - with Image.open(fp) as reread: - assert reread.mode == "RGBA" - assert reread.size == (1024, 1024) - assert reread.format == "ICNS" - - -def test_sizes() -> 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: - for w, h, r in im.info["sizes"]: - wr = w * r - hr = h * r - im.size = (w, h, r) - im.load() - assert im.mode == "RGBA" - assert im.size == (wr, hr) - - # Check that we cannot load an incorrect size - with pytest.raises(ValueError): - im.size = (1, 1) - - -def test_older_icon() -> None: - # This icon was made with Icon Composer rather than iconutil; it still - # uses PNG rather than JP2, however (since it was made on 10.9). - with Image.open("Tests/images/pillow2.icns") as im: - for w, h, r in im.info["sizes"]: - wr = w * r - hr = h * r - with Image.open("Tests/images/pillow2.icns") as im2: - im2.size = (w, h, r) - im2.load() - assert im2.mode == "RGBA" - assert im2.size == (wr, hr) - - -@skip_unless_feature("jpg_2000") -def test_jp2_icon() -> None: - # This icon uses JPEG 2000 images instead of the PNG images. - # The advantage of doing this is that OS X 10.5 supports JPEG 2000 - # but not PNG; some commercial software therefore does just this. - - with Image.open("Tests/images/pillow3.icns") as im: - for w, h, r in im.info["sizes"]: - wr = w * r - hr = h * r - with Image.open("Tests/images/pillow3.icns") as im2: - im2.size = (w, h, r) - im2.load() - assert im2.mode == "RGBA" - assert im2.size == (wr, hr) - - -def test_getimage() -> None: - with open(TEST_FILE, "rb") as fp: - icns_file = IcnsImagePlugin.IcnsFile(fp) - - im = icns_file.getimage() - assert im.mode == "RGBA" - assert im.size == (1024, 1024) - - im = icns_file.getimage((512, 512)) - assert im.mode == "RGBA" - assert im.size == (512, 512) - - -def test_not_an_icns_file() -> None: - with io.BytesIO(b"invalid\n") as fp: - with pytest.raises(SyntaxError): - IcnsImagePlugin.IcnsFile(fp) - - -@skip_unless_feature("jpg_2000") -def test_icns_decompression_bomb() -> None: - with Image.open( - "Tests/images/oom-8ed3316a4109213ca96fb8a256a0bfefdece1461.icns" - ) as im: - with pytest.raises(Image.DecompressionBombError): - im.load() +from __future__ import annotations + +import io +import os +import warnings +from pathlib import Path + +import pytest + +from PIL import IcnsImagePlugin, Image, _binary + +from .helper import assert_image_equal, assert_image_similar_tofile, skip_unless_feature + +# sample icon file +TEST_FILE = "Tests/images/pillow.icns" + + +def test_sanity() -> None: + # Loading this icon by default should result in the largest size + # (512x512@2x) being loaded + with Image.open(TEST_FILE) as im: + # Assert that there is no unclosed file warning + with warnings.catch_warnings(): + im.load() + + assert im.mode == "RGBA" + assert im.size == (1024, 1024) + assert im.format == "ICNS" + + +def test_load() -> None: + with Image.open(TEST_FILE) as im: + assert im.load()[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) + + +def test_save(tmp_path: Path) -> None: + temp_file = str(tmp_path / "temp.icns") + + with Image.open(TEST_FILE) as im: + im.save(temp_file) + + with Image.open(temp_file) as reread: + assert reread.mode == "RGBA" + assert reread.size == (1024, 1024) + assert reread.format == "ICNS" + + file_length = os.path.getsize(temp_file) + with open(temp_file, "rb") as fp: + fp.seek(4) + assert _binary.i32be(fp.read(4)) == file_length + + +def test_save_append_images(tmp_path: Path) -> None: + temp_file = str(tmp_path / "temp.icns") + provided_im = Image.new("RGBA", (32, 32), (255, 0, 0, 128)) + + with Image.open(TEST_FILE) as im: + im.save(temp_file, append_images=[provided_im]) + + assert_image_similar_tofile(im, temp_file, 1) + + with Image.open(temp_file) as reread: + reread.size = (16, 16, 2) + reread.load() + assert_image_equal(reread, provided_im) + + +def test_save_fp() -> None: + fp = io.BytesIO() + + with Image.open(TEST_FILE) as im: + im.save(fp, format="ICNS") + + with Image.open(fp) as reread: + assert reread.mode == "RGBA" + assert reread.size == (1024, 1024) + assert reread.format == "ICNS" + + +def test_sizes() -> 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: + for w, h, r in im.info["sizes"]: + wr = w * r + hr = h * r + im.size = (w, h, r) + im.load() + assert im.mode == "RGBA" + assert im.size == (wr, hr) + + # Check that we cannot load an incorrect size + with pytest.raises(ValueError): + im.size = (1, 1) + + +def test_older_icon() -> None: + # This icon was made with Icon Composer rather than iconutil; it still + # uses PNG rather than JP2, however (since it was made on 10.9). + with Image.open("Tests/images/pillow2.icns") as im: + for w, h, r in im.info["sizes"]: + wr = w * r + hr = h * r + with Image.open("Tests/images/pillow2.icns") as im2: + im2.size = (w, h, r) + im2.load() + assert im2.mode == "RGBA" + assert im2.size == (wr, hr) + + +@skip_unless_feature("jpg_2000") +def test_jp2_icon() -> None: + # This icon uses JPEG 2000 images instead of the PNG images. + # The advantage of doing this is that OS X 10.5 supports JPEG 2000 + # but not PNG; some commercial software therefore does just this. + + with Image.open("Tests/images/pillow3.icns") as im: + for w, h, r in im.info["sizes"]: + wr = w * r + hr = h * r + with Image.open("Tests/images/pillow3.icns") as im2: + im2.size = (w, h, r) + im2.load() + assert im2.mode == "RGBA" + assert im2.size == (wr, hr) + + +def test_getimage() -> None: + with open(TEST_FILE, "rb") as fp: + icns_file = IcnsImagePlugin.IcnsFile(fp) + + im = icns_file.getimage() + assert im.mode == "RGBA" + assert im.size == (1024, 1024) + + im = icns_file.getimage((512, 512)) + assert im.mode == "RGBA" + assert im.size == (512, 512) + + +def test_not_an_icns_file() -> None: + with io.BytesIO(b"invalid\n") as fp: + with pytest.raises(SyntaxError): + IcnsImagePlugin.IcnsFile(fp) + + +@skip_unless_feature("jpg_2000") +def test_icns_decompression_bomb() -> None: + with Image.open( + "Tests/images/oom-8ed3316a4109213ca96fb8a256a0bfefdece1461.icns" + ) as im: + with pytest.raises(Image.DecompressionBombError): + im.load() diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index fa8c11d5a..ea7834148 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -1,243 +1,243 @@ -from __future__ import annotations - -import io -import os -from pathlib import Path - -import pytest - -from PIL import IcoImagePlugin, Image, ImageDraw - -from .helper import assert_image_equal, assert_image_equal_tofile, hopper - -TEST_ICO_FILE = "Tests/images/hopper.ico" - - -def test_sanity() -> None: - with Image.open(TEST_ICO_FILE) as im: - im.load() - assert im.mode == "RGBA" - assert im.size == (16, 16) - assert im.format == "ICO" - assert im.get_format_mimetype() == "image/x-icon" - - -def test_load() -> None: - with Image.open(TEST_ICO_FILE) as im: - assert im.load()[0, 0] == (1, 1, 9, 255) - - -def test_mask() -> None: - with Image.open("Tests/images/hopper_mask.ico") as im: - assert_image_equal_tofile(im, "Tests/images/hopper_mask.png") - - -def test_black_and_white() -> None: - with Image.open("Tests/images/black_and_white.ico") as im: - assert im.mode == "RGBA" - assert im.size == (16, 16) - - -def test_palette(tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.ico") - - im = Image.new("P", (16, 16)) - im.save(temp_file) - - with Image.open(temp_file) as reloaded: - assert reloaded.mode == "P" - assert reloaded.palette is not None - - -def test_invalid_file() -> None: - with open("Tests/images/flower.jpg", "rb") as fp: - with pytest.raises(SyntaxError): - IcoImagePlugin.IcoImageFile(fp) - - -def test_save_to_bytes() -> None: - output = io.BytesIO() - im = hopper() - im.save(output, "ico", sizes=[(32, 32), (64, 64)]) - - # The default image - output.seek(0) - with Image.open(output) as reloaded: - assert reloaded.info["sizes"] == {(32, 32), (64, 64)} - - assert im.mode == reloaded.mode - assert (64, 64) == reloaded.size - assert reloaded.format == "ICO" - assert_image_equal( - reloaded, hopper().resize((64, 64), Image.Resampling.LANCZOS) - ) - - # The other one - output.seek(0) - with Image.open(output) as reloaded: - reloaded.size = (32, 32) - - assert im.mode == reloaded.mode - assert (32, 32) == reloaded.size - assert reloaded.format == "ICO" - assert_image_equal( - reloaded, hopper().resize((32, 32), Image.Resampling.LANCZOS) - ) - - -def test_getpixel(tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.ico") - - im = hopper() - im.save(temp_file, "ico", sizes=[(32, 32), (64, 64)]) - - with Image.open(temp_file) as reloaded: - reloaded.load() - reloaded.size = (32, 32) - - assert reloaded.getpixel((0, 0)) == (18, 20, 62) - - -def test_no_duplicates(tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.ico") - temp_file2 = str(tmp_path / "temp2.ico") - - im = hopper() - sizes = [(32, 32), (64, 64)] - im.save(temp_file, "ico", sizes=sizes) - - sizes.append(sizes[-1]) - im.save(temp_file2, "ico", sizes=sizes) - - assert os.path.getsize(temp_file) == os.path.getsize(temp_file2) - - -def test_different_bit_depths(tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.ico") - temp_file2 = str(tmp_path / "temp2.ico") - - im = hopper() - im.save(temp_file, "ico", bitmap_format="bmp", sizes=[(128, 128)]) - - hopper("1").save( - temp_file2, - "ico", - bitmap_format="bmp", - sizes=[(128, 128)], - append_images=[im], - ) - - assert os.path.getsize(temp_file) != os.path.getsize(temp_file2) - - # Test that only matching sizes of different bit depths are saved - temp_file3 = str(tmp_path / "temp3.ico") - temp_file4 = str(tmp_path / "temp4.ico") - - im.save(temp_file3, "ico", bitmap_format="bmp", sizes=[(128, 128)]) - im.save( - temp_file4, - "ico", - bitmap_format="bmp", - sizes=[(128, 128)], - append_images=[Image.new("P", (64, 64))], - ) - - assert os.path.getsize(temp_file3) == os.path.getsize(temp_file4) - - -@pytest.mark.parametrize("mode", ("1", "L", "P", "RGB", "RGBA")) -def test_save_to_bytes_bmp(mode: str) -> None: - output = io.BytesIO() - im = hopper(mode) - im.save(output, "ico", bitmap_format="bmp", sizes=[(32, 32), (64, 64)]) - - # The default image - output.seek(0) - with Image.open(output) as reloaded: - assert reloaded.info["sizes"] == {(32, 32), (64, 64)} - - assert "RGBA" == reloaded.mode - assert (64, 64) == reloaded.size - assert reloaded.format == "ICO" - im = hopper(mode).resize((64, 64), Image.Resampling.LANCZOS).convert("RGBA") - assert_image_equal(reloaded, im) - - # The other one - output.seek(0) - with Image.open(output) as reloaded: - reloaded.size = (32, 32) - - assert "RGBA" == reloaded.mode - assert (32, 32) == reloaded.size - assert reloaded.format == "ICO" - im = hopper(mode).resize((32, 32), Image.Resampling.LANCZOS).convert("RGBA") - assert_image_equal(reloaded, im) - - -def test_incorrect_size() -> None: - with Image.open(TEST_ICO_FILE) as im: - with pytest.raises(ValueError): - im.size = (1, 1) - - -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") - - # Act - im.save(outfile) - with Image.open(outfile) as im_saved: - # Assert - assert im_saved.size == (256, 256) - - -def test_only_save_relevant_sizes(tmp_path: Path) -> None: - """Issue #2266 https://github.com/python-pillow/Pillow/issues/2266 - Should save in 16x16, 24x24, 32x32, 48x48 sizes - and not in 16x16, 24x24, 32x32, 48x48, 48x48, 48x48, 48x48 sizes - """ - # Arrange - with Image.open("Tests/images/python.ico") as im: # 16x16, 32x32, 48x48 - outfile = str(tmp_path / "temp_saved_python.ico") - # Act - im.save(outfile) - - with Image.open(outfile) as im_saved: - # Assert - assert im_saved.info["sizes"] == {(16, 16), (24, 24), (32, 32), (48, 48)} - - -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") - im.save(outfile, sizes=[(32, 32), (128, 128)], append_images=[provided_im]) - - with Image.open(outfile) as reread: - assert_image_equal(reread, hopper("RGBA")) - - reread.size = (32, 32) - assert_image_equal(reread, provided_im) - - -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 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") - - draw = ImageDraw.Draw(im) - draw.line((0, 0) + im.size, "#f00") - im.save(outfile) - - with Image.open(outfile) as im: - assert_image_equal_tofile(im, "Tests/images/hopper_draw.ico") +from __future__ import annotations + +import io +import os +from pathlib import Path + +import pytest + +from PIL import IcoImagePlugin, Image, ImageDraw + +from .helper import assert_image_equal, assert_image_equal_tofile, hopper + +TEST_ICO_FILE = "Tests/images/hopper.ico" + + +def test_sanity() -> None: + with Image.open(TEST_ICO_FILE) as im: + im.load() + assert im.mode == "RGBA" + assert im.size == (16, 16) + assert im.format == "ICO" + assert im.get_format_mimetype() == "image/x-icon" + + +def test_load() -> None: + with Image.open(TEST_ICO_FILE) as im: + assert im.load()[0, 0] == (1, 1, 9, 255) + + +def test_mask() -> None: + with Image.open("Tests/images/hopper_mask.ico") as im: + assert_image_equal_tofile(im, "Tests/images/hopper_mask.png") + + +def test_black_and_white() -> None: + with Image.open("Tests/images/black_and_white.ico") as im: + assert im.mode == "RGBA" + assert im.size == (16, 16) + + +def test_palette(tmp_path: Path) -> None: + temp_file = str(tmp_path / "temp.ico") + + im = Image.new("P", (16, 16)) + im.save(temp_file) + + with Image.open(temp_file) as reloaded: + assert reloaded.mode == "P" + assert reloaded.palette is not None + + +def test_invalid_file() -> None: + with open("Tests/images/flower.jpg", "rb") as fp: + with pytest.raises(SyntaxError): + IcoImagePlugin.IcoImageFile(fp) + + +def test_save_to_bytes() -> None: + output = io.BytesIO() + im = hopper() + im.save(output, "ico", sizes=[(32, 32), (64, 64)]) + + # The default image + output.seek(0) + with Image.open(output) as reloaded: + assert reloaded.info["sizes"] == {(32, 32), (64, 64)} + + assert im.mode == reloaded.mode + assert (64, 64) == reloaded.size + assert reloaded.format == "ICO" + assert_image_equal( + reloaded, hopper().resize((64, 64), Image.Resampling.LANCZOS) + ) + + # The other one + output.seek(0) + with Image.open(output) as reloaded: + reloaded.size = (32, 32) + + assert im.mode == reloaded.mode + assert (32, 32) == reloaded.size + assert reloaded.format == "ICO" + assert_image_equal( + reloaded, hopper().resize((32, 32), Image.Resampling.LANCZOS) + ) + + +def test_getpixel(tmp_path: Path) -> None: + temp_file = str(tmp_path / "temp.ico") + + im = hopper() + im.save(temp_file, "ico", sizes=[(32, 32), (64, 64)]) + + with Image.open(temp_file) as reloaded: + reloaded.load() + reloaded.size = (32, 32) + + assert reloaded.getpixel((0, 0)) == (18, 20, 62) + + +def test_no_duplicates(tmp_path: Path) -> None: + temp_file = str(tmp_path / "temp.ico") + temp_file2 = str(tmp_path / "temp2.ico") + + im = hopper() + sizes = [(32, 32), (64, 64)] + im.save(temp_file, "ico", sizes=sizes) + + sizes.append(sizes[-1]) + im.save(temp_file2, "ico", sizes=sizes) + + assert os.path.getsize(temp_file) == os.path.getsize(temp_file2) + + +def test_different_bit_depths(tmp_path: Path) -> None: + temp_file = str(tmp_path / "temp.ico") + temp_file2 = str(tmp_path / "temp2.ico") + + im = hopper() + im.save(temp_file, "ico", bitmap_format="bmp", sizes=[(128, 128)]) + + hopper("1").save( + temp_file2, + "ico", + bitmap_format="bmp", + sizes=[(128, 128)], + append_images=[im], + ) + + assert os.path.getsize(temp_file) != os.path.getsize(temp_file2) + + # Test that only matching sizes of different bit depths are saved + temp_file3 = str(tmp_path / "temp3.ico") + temp_file4 = str(tmp_path / "temp4.ico") + + im.save(temp_file3, "ico", bitmap_format="bmp", sizes=[(128, 128)]) + im.save( + temp_file4, + "ico", + bitmap_format="bmp", + sizes=[(128, 128)], + append_images=[Image.new("P", (64, 64))], + ) + + assert os.path.getsize(temp_file3) == os.path.getsize(temp_file4) + + +@pytest.mark.parametrize("mode", ("1", "L", "P", "RGB", "RGBA")) +def test_save_to_bytes_bmp(mode: str) -> None: + output = io.BytesIO() + im = hopper(mode) + im.save(output, "ico", bitmap_format="bmp", sizes=[(32, 32), (64, 64)]) + + # The default image + output.seek(0) + with Image.open(output) as reloaded: + assert reloaded.info["sizes"] == {(32, 32), (64, 64)} + + assert "RGBA" == reloaded.mode + assert (64, 64) == reloaded.size + assert reloaded.format == "ICO" + im = hopper(mode).resize((64, 64), Image.Resampling.LANCZOS).convert("RGBA") + assert_image_equal(reloaded, im) + + # The other one + output.seek(0) + with Image.open(output) as reloaded: + reloaded.size = (32, 32) + + assert "RGBA" == reloaded.mode + assert (32, 32) == reloaded.size + assert reloaded.format == "ICO" + im = hopper(mode).resize((32, 32), Image.Resampling.LANCZOS).convert("RGBA") + assert_image_equal(reloaded, im) + + +def test_incorrect_size() -> None: + with Image.open(TEST_ICO_FILE) as im: + with pytest.raises(ValueError): + im.size = (1, 1) + + +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") + + # Act + im.save(outfile) + with Image.open(outfile) as im_saved: + # Assert + assert im_saved.size == (256, 256) + + +def test_only_save_relevant_sizes(tmp_path: Path) -> None: + """Issue #2266 https://github.com/python-pillow/Pillow/issues/2266 + Should save in 16x16, 24x24, 32x32, 48x48 sizes + and not in 16x16, 24x24, 32x32, 48x48, 48x48, 48x48, 48x48 sizes + """ + # Arrange + with Image.open("Tests/images/python.ico") as im: # 16x16, 32x32, 48x48 + outfile = str(tmp_path / "temp_saved_python.ico") + # Act + im.save(outfile) + + with Image.open(outfile) as im_saved: + # Assert + assert im_saved.info["sizes"] == {(16, 16), (24, 24), (32, 32), (48, 48)} + + +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") + im.save(outfile, sizes=[(32, 32), (128, 128)], append_images=[provided_im]) + + with Image.open(outfile) as reread: + assert_image_equal(reread, hopper("RGBA")) + + reread.size = (32, 32) + assert_image_equal(reread, provided_im) + + +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 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") + + draw = ImageDraw.Draw(im) + draw.line((0, 0) + im.size, "#f00") + im.save(outfile) + + with Image.open(outfile) as im: + assert_image_equal_tofile(im, "Tests/images/hopper_draw.ico") diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py index 036965bf5..902e9e4c0 100644 --- a/Tests/test_file_im.py +++ b/Tests/test_file_im.py @@ -1,119 +1,119 @@ -from __future__ import annotations - -import filecmp -import warnings -from pathlib import Path - -import pytest - -from PIL import Image, ImImagePlugin - -from .helper import assert_image_equal_tofile, hopper, is_pypy - -# sample im -TEST_IM = "Tests/images/hopper.im" - - -def test_sanity() -> None: - with Image.open(TEST_IM) as im: - im.load() - assert im.mode == "RGB" - assert im.size == (128, 128) - assert im.format == "IM" - - -def test_name_limit(tmp_path: Path) -> None: - out = str(tmp_path / ("name_limit_test" * 7 + ".im")) - with Image.open(TEST_IM) as im: - im.save(out) - assert filecmp.cmp(out, "Tests/images/hopper_long_name.im") - - -@pytest.mark.skipif(is_pypy(), reason="Requires CPython") -def test_unclosed_file() -> None: - def open() -> None: - im = Image.open(TEST_IM) - im.load() - - with pytest.warns(ResourceWarning): - open() - - -def test_closed_file() -> None: - with warnings.catch_warnings(): - im = Image.open(TEST_IM) - im.load() - im.close() - - -def test_context_manager() -> None: - with warnings.catch_warnings(): - with Image.open(TEST_IM) as im: - im.load() - - -def test_tell() -> None: - # Arrange - with Image.open(TEST_IM) as im: - # Act - frame = im.tell() - - # Assert - assert frame == 0 - - -def test_n_frames() -> None: - with Image.open(TEST_IM) as im: - assert im.n_frames == 1 - assert not im.is_animated - - -def test_eoferror() -> None: - with Image.open(TEST_IM) as im: - n_frames = im.n_frames - - # Test seeking past the last frame - with pytest.raises(EOFError): - im.seek(n_frames) - assert im.tell() < n_frames - - # Test that seeking to the last frame does not raise an error - im.seek(n_frames - 1) - - -@pytest.mark.parametrize("mode", ("RGB", "P", "PA")) -def test_roundtrip(mode: str, tmp_path: Path) -> None: - out = str(tmp_path / "temp.im") - im = hopper(mode) - im.save(out) - assert_image_equal_tofile(im, out) - - -def test_small_palette(tmp_path: Path) -> None: - im = Image.new("P", (1, 1)) - colors = [0, 1, 2] - im.putpalette(colors) - - out = str(tmp_path / "temp.im") - im.save(out) - - with Image.open(out) as reloaded: - assert reloaded.getpalette() == colors + [0] * 765 - - -def test_save_unsupported_mode(tmp_path: Path) -> None: - out = str(tmp_path / "temp.im") - im = hopper("HSV") - with pytest.raises(ValueError): - im.save(out) - - -def test_invalid_file() -> None: - invalid_file = "Tests/images/flower.jpg" - - with pytest.raises(SyntaxError): - ImImagePlugin.ImImageFile(invalid_file) - - -def test_number() -> None: - assert ImImagePlugin.number("1.2") == 1.2 +from __future__ import annotations + +import filecmp +import warnings +from pathlib import Path + +import pytest + +from PIL import Image, ImImagePlugin + +from .helper import assert_image_equal_tofile, hopper, is_pypy + +# sample im +TEST_IM = "Tests/images/hopper.im" + + +def test_sanity() -> None: + with Image.open(TEST_IM) as im: + im.load() + assert im.mode == "RGB" + assert im.size == (128, 128) + assert im.format == "IM" + + +def test_name_limit(tmp_path: Path) -> None: + out = str(tmp_path / ("name_limit_test" * 7 + ".im")) + with Image.open(TEST_IM) as im: + im.save(out) + assert filecmp.cmp(out, "Tests/images/hopper_long_name.im") + + +@pytest.mark.skipif(is_pypy(), reason="Requires CPython") +def test_unclosed_file() -> None: + def open() -> None: + im = Image.open(TEST_IM) + im.load() + + with pytest.warns(ResourceWarning): + open() + + +def test_closed_file() -> None: + with warnings.catch_warnings(): + im = Image.open(TEST_IM) + im.load() + im.close() + + +def test_context_manager() -> None: + with warnings.catch_warnings(): + with Image.open(TEST_IM) as im: + im.load() + + +def test_tell() -> None: + # Arrange + with Image.open(TEST_IM) as im: + # Act + frame = im.tell() + + # Assert + assert frame == 0 + + +def test_n_frames() -> None: + with Image.open(TEST_IM) as im: + assert im.n_frames == 1 + assert not im.is_animated + + +def test_eoferror() -> None: + with Image.open(TEST_IM) as im: + n_frames = im.n_frames + + # Test seeking past the last frame + with pytest.raises(EOFError): + im.seek(n_frames) + assert im.tell() < n_frames + + # Test that seeking to the last frame does not raise an error + im.seek(n_frames - 1) + + +@pytest.mark.parametrize("mode", ("RGB", "P", "PA")) +def test_roundtrip(mode: str, tmp_path: Path) -> None: + out = str(tmp_path / "temp.im") + im = hopper(mode) + im.save(out) + assert_image_equal_tofile(im, out) + + +def test_small_palette(tmp_path: Path) -> None: + im = Image.new("P", (1, 1)) + colors = [0, 1, 2] + im.putpalette(colors) + + out = str(tmp_path / "temp.im") + im.save(out) + + with Image.open(out) as reloaded: + assert reloaded.getpalette() == colors + [0] * 765 + + +def test_save_unsupported_mode(tmp_path: Path) -> None: + out = str(tmp_path / "temp.im") + im = hopper("HSV") + with pytest.raises(ValueError): + im.save(out) + + +def test_invalid_file() -> None: + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(SyntaxError): + ImImagePlugin.ImImageFile(invalid_file) + + +def test_number() -> None: + assert ImImagePlugin.number("1.2") == 1.2 diff --git a/Tests/test_file_imt.py b/Tests/test_file_imt.py index 6957dfa0a..1fcf60aae 100644 --- a/Tests/test_file_imt.py +++ b/Tests/test_file_imt.py @@ -1,21 +1,21 @@ -from __future__ import annotations - -import io - -import pytest - -from PIL import Image, ImtImagePlugin - -from .helper import assert_image_equal_tofile - - -def test_sanity() -> None: - with Image.open("Tests/images/bw_gradient.imt") as im: - assert_image_equal_tofile(im, "Tests/images/bw_gradient.png") - - -@pytest.mark.parametrize("data", (b"\n", b"\n-", b"width 1\n")) -def test_invalid_file(data: bytes) -> None: - with io.BytesIO(data) as fp: - with pytest.raises(SyntaxError): - ImtImagePlugin.ImtImageFile(fp) +from __future__ import annotations + +import io + +import pytest + +from PIL import Image, ImtImagePlugin + +from .helper import assert_image_equal_tofile + + +def test_sanity() -> None: + with Image.open("Tests/images/bw_gradient.imt") as im: + assert_image_equal_tofile(im, "Tests/images/bw_gradient.png") + + +@pytest.mark.parametrize("data", (b"\n", b"\n-", b"width 1\n")) +def test_invalid_file(data: bytes) -> None: + with io.BytesIO(data) as fp: + with pytest.raises(SyntaxError): + ImtImagePlugin.ImtImageFile(fp) diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index 88c30d468..297a90ed8 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -1,118 +1,118 @@ -from __future__ import annotations - -import sys -from io import BytesIO, StringIO - -import pytest - -from PIL import Image, IptcImagePlugin - -from .helper import assert_image_equal, hopper - -TEST_FILE = "Tests/images/iptc.jpg" - - -def test_open() -> None: - expected = Image.new("L", (1, 1)) - - f = BytesIO( - b"\x1c\x03<\x00\x02\x01\x00\x1c\x03x\x00\x01\x01\x1c\x03\x14\x00\x01\x01" - b"\x1c\x03\x1e\x00\x01\x01\x1c\x08\n\x00\x01\x00" - ) - with Image.open(f) as im: - assert im.tile == [("iptc", (0, 0, 1, 1), 25, "raw")] - assert_image_equal(im, expected) - - -def test_getiptcinfo_jpg_none() -> None: - # Arrange - with hopper() as im: - # Act - iptc = IptcImagePlugin.getiptcinfo(im) - - # Assert - assert iptc is None - - -def test_getiptcinfo_jpg_found() -> None: - # Arrange - with Image.open(TEST_FILE) as im: - # Act - iptc = IptcImagePlugin.getiptcinfo(im) - - # Assert - assert isinstance(iptc, dict) - assert iptc[(2, 90)] == b"Budapest" - assert iptc[(2, 101)] == b"Hungary" - - -def test_getiptcinfo_fotostation() -> None: - # Arrange - with open(TEST_FILE, "rb") as fp: - data = bytearray(fp.read()) - data[86] = 240 - f = BytesIO(data) - with Image.open(f) as im: - # Act - iptc = IptcImagePlugin.getiptcinfo(im) - - # Assert - for tag in iptc.keys(): - if tag[0] == 240: - return - pytest.fail("FotoStation tag not found") - - -def test_getiptcinfo_zero_padding() -> None: - # Arrange - with Image.open(TEST_FILE) as im: - im.info["photoshop"][0x0404] += b"\x00\x00\x00" - - # Act - iptc = IptcImagePlugin.getiptcinfo(im) - - # Assert - assert isinstance(iptc, dict) - assert len(iptc) == 3 - - -def test_getiptcinfo_tiff_none() -> None: - # Arrange - with Image.open("Tests/images/hopper.tif") as im: - # Act - iptc = IptcImagePlugin.getiptcinfo(im) - - # Assert - assert iptc is None - - -def test_i() -> None: - # Arrange - c = b"a" - - # Act - with pytest.warns(DeprecationWarning): - ret = IptcImagePlugin.i(c) - - # Assert - assert ret == 97 - - -def test_dump(monkeypatch: pytest.MonkeyPatch) -> None: - # Arrange - c = b"abc" - # Temporarily redirect stdout - mystdout = StringIO() - monkeypatch.setattr(sys, "stdout", mystdout) - - # Act - with pytest.warns(DeprecationWarning): - IptcImagePlugin.dump(c) - - # Assert - assert mystdout.getvalue() == "61 62 63 \n" - - -def test_pad_deprecation() -> None: - with pytest.warns(DeprecationWarning): - assert IptcImagePlugin.PAD == b"\0\0\0\0" +from __future__ import annotations + +import sys +from io import BytesIO, StringIO + +import pytest + +from PIL import Image, IptcImagePlugin + +from .helper import assert_image_equal, hopper + +TEST_FILE = "Tests/images/iptc.jpg" + + +def test_open() -> None: + expected = Image.new("L", (1, 1)) + + f = BytesIO( + b"\x1c\x03<\x00\x02\x01\x00\x1c\x03x\x00\x01\x01\x1c\x03\x14\x00\x01\x01" + b"\x1c\x03\x1e\x00\x01\x01\x1c\x08\n\x00\x01\x00" + ) + with Image.open(f) as im: + assert im.tile == [("iptc", (0, 0, 1, 1), 25, "raw")] + assert_image_equal(im, expected) + + +def test_getiptcinfo_jpg_none() -> None: + # Arrange + with hopper() as im: + # Act + iptc = IptcImagePlugin.getiptcinfo(im) + + # Assert + assert iptc is None + + +def test_getiptcinfo_jpg_found() -> None: + # Arrange + with Image.open(TEST_FILE) as im: + # Act + iptc = IptcImagePlugin.getiptcinfo(im) + + # Assert + assert isinstance(iptc, dict) + assert iptc[(2, 90)] == b"Budapest" + assert iptc[(2, 101)] == b"Hungary" + + +def test_getiptcinfo_fotostation() -> None: + # Arrange + with open(TEST_FILE, "rb") as fp: + data = bytearray(fp.read()) + data[86] = 240 + f = BytesIO(data) + with Image.open(f) as im: + # Act + iptc = IptcImagePlugin.getiptcinfo(im) + + # Assert + for tag in iptc.keys(): + if tag[0] == 240: + return + pytest.fail("FotoStation tag not found") + + +def test_getiptcinfo_zero_padding() -> None: + # Arrange + with Image.open(TEST_FILE) as im: + im.info["photoshop"][0x0404] += b"\x00\x00\x00" + + # Act + iptc = IptcImagePlugin.getiptcinfo(im) + + # Assert + assert isinstance(iptc, dict) + assert len(iptc) == 3 + + +def test_getiptcinfo_tiff_none() -> None: + # Arrange + with Image.open("Tests/images/hopper.tif") as im: + # Act + iptc = IptcImagePlugin.getiptcinfo(im) + + # Assert + assert iptc is None + + +def test_i() -> None: + # Arrange + c = b"a" + + # Act + with pytest.warns(DeprecationWarning): + ret = IptcImagePlugin.i(c) + + # Assert + assert ret == 97 + + +def test_dump(monkeypatch: pytest.MonkeyPatch) -> None: + # Arrange + c = b"abc" + # Temporarily redirect stdout + mystdout = StringIO() + monkeypatch.setattr(sys, "stdout", mystdout) + + # Act + with pytest.warns(DeprecationWarning): + IptcImagePlugin.dump(c) + + # Assert + assert mystdout.getvalue() == "61 62 63 \n" + + +def test_pad_deprecation() -> None: + with pytest.warns(DeprecationWarning): + assert IptcImagePlugin.PAD == b"\0\0\0\0" diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 18dc752d8..0a6b2a8cd 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -1,1060 +1,1060 @@ -from __future__ import annotations - -import os -import re -import warnings -from io import BytesIO -from pathlib import Path -from types import ModuleType -from typing import Any, cast - -import pytest - -from PIL import ( - ExifTags, - Image, - ImageFile, - ImageOps, - JpegImagePlugin, - UnidentifiedImageError, - features, -) - -from .helper import ( - assert_image, - assert_image_equal, - assert_image_equal_tofile, - assert_image_similar, - assert_image_similar_tofile, - cjpeg_available, - djpeg_available, - hopper, - is_win32, - mark_if_feature_version, - skip_unless_feature, -) - -ElementTree: ModuleType | None -try: - from defusedxml import ElementTree -except ImportError: - ElementTree = None - -TEST_FILE = "Tests/images/hopper.jpg" - - -@skip_unless_feature("jpg") -class TestFileJpeg: - def roundtrip_with_bytes( - self, im: Image.Image, **options: Any - ) -> tuple[JpegImagePlugin.JpegImageFile, int]: - out = BytesIO() - im.save(out, "JPEG", **options) - test_bytes = out.tell() - out.seek(0) - reloaded = cast(JpegImagePlugin.JpegImageFile, Image.open(out)) - return reloaded, test_bytes - - def roundtrip( - self, im: Image.Image, **options: Any - ) -> JpegImagePlugin.JpegImageFile: - return self.roundtrip_with_bytes(im, **options)[0] - - def gen_random_image(self, size: tuple[int, int], mode: str = "RGB") -> Image.Image: - """Generates a very hard to compress file - :param size: tuple - :param mode: optional image mode - - """ - return Image.frombytes(mode, size, os.urandom(size[0] * size[1] * len(mode))) - - def test_sanity(self) -> None: - # internal version number - version = features.version_codec("jpg") - assert version is not None - assert re.search(r"\d+\.\d+$", version) - - with Image.open(TEST_FILE) as im: - im.load() - assert im.mode == "RGB" - assert im.size == (128, 128) - assert im.format == "JPEG" - assert im.get_format_mimetype() == "image/jpeg" - - @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0))) - def test_zero(self, size: tuple[int, int], tmp_path: Path) -> None: - f = str(tmp_path / "temp.jpg") - im = Image.new("RGB", size) - with pytest.raises(ValueError): - im.save(f) - - def test_app(self) -> None: - # Test APP/COM reader (@PIL135) - with Image.open(TEST_FILE) as im: - assert im.applist[0] == ("APP0", b"JFIF\x00\x01\x01\x01\x00`\x00`\x00\x00") - assert im.applist[1] == ( - "COM", - b"File written by Adobe Photoshop\xa8 4.0\x00", - ) - assert len(im.applist) == 2 - - assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0\x00" - assert im.app["COM"] == im.info["comment"] - - def test_comment_write(self) -> None: - with Image.open(TEST_FILE) as im: - assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0\x00" - - # Test that existing comment is saved by default - out = BytesIO() - im.save(out, format="JPEG") - with Image.open(out) as reloaded: - assert im.info["comment"] == reloaded.info["comment"] - - # Ensure that a blank comment causes any existing comment to be removed - for comment in ("", b"", None): - out = BytesIO() - im.save(out, format="JPEG", comment=comment) - with Image.open(out) as reloaded: - assert "comment" not in reloaded.info - - # Test that a comment argument overrides the default comment - for comment in ("Test comment text", b"Test comment text"): - out = BytesIO() - im.save(out, format="JPEG", comment=comment) - with Image.open(out) as reloaded: - assert reloaded.info["comment"] == b"Test comment text" - - 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))) - assert c == 0.0 - assert m > 0.8 - assert y > 0.8 - assert k == 0.0 - # the opposite corner is black - c, m, y, k = ( - x / 255.0 for x in im.getpixel((im.size[0] - 1, im.size[1] - 1)) - ) - assert k > 0.9 - # roundtrip, and check again - im = self.roundtrip(im) - c, m, y, k = (x / 255.0 for x in im.getpixel((0, 0))) - assert c == 0.0 - assert m > 0.8 - assert y > 0.8 - assert k == 0.0 - c, m, y, k = ( - x / 255.0 for x in im.getpixel((im.size[0] - 1, im.size[1] - 1)) - ) - assert k > 0.9 - - def test_rgb(self) -> None: - def getchannels(im: JpegImagePlugin.JpegImageFile) -> tuple[int, int, int]: - return tuple(v[0] for v in im.layer) - - im = hopper() - im_ycbcr = self.roundtrip(im) - assert getchannels(im_ycbcr) == (1, 2, 3) - assert_image_similar(im, im_ycbcr, 17) - - im_rgb = self.roundtrip(im, keep_rgb=True) - assert getchannels(im_rgb) == (ord("R"), ord("G"), ord("B")) - assert_image_similar(im, im_rgb, 12) - - @pytest.mark.parametrize( - "test_image_path", - [TEST_FILE, "Tests/images/pil_sample_cmyk.jpg"], - ) - def test_dpi(self, test_image_path: str) -> None: - def test(xdpi: int, ydpi: int | None = None) -> tuple[int, int] | None: - with Image.open(test_image_path) as im: - im = self.roundtrip(im, dpi=(xdpi, ydpi or xdpi)) - return im.info.get("dpi") - - assert test(72) == (72, 72) - assert test(300) == (300, 300) - assert test(100, 200) == (100, 200) - assert test(0) is None # square pixels - - @mark_if_feature_version( - pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" - ) - def test_icc(self, tmp_path: Path) -> None: - # Test ICC support - with Image.open("Tests/images/rgb.jpg") as im1: - icc_profile = im1.info["icc_profile"] - assert len(icc_profile) == 3144 - # Roundtrip via physical file. - f = str(tmp_path / "temp.jpg") - im1.save(f, icc_profile=icc_profile) - with Image.open(f) as im2: - assert im2.info.get("icc_profile") == icc_profile - # Roundtrip via memory buffer. - im1 = self.roundtrip(hopper()) - im2 = self.roundtrip(hopper(), icc_profile=icc_profile) - assert_image_equal(im1, im2) - assert not im1.info.get("icc_profile") - assert im2.info.get("icc_profile") - - @pytest.mark.parametrize( - "n", - ( - 0, - 1, - 3, - 4, - 5, - 65533 - 14, # full JPEG marker block - 65533 - 14 + 1, # full block plus one byte - ImageFile.MAXBLOCK, # full buffer block - ImageFile.MAXBLOCK + 1, # full buffer block plus one byte - ImageFile.MAXBLOCK * 4 + 3, # large block - ), - ) - def test_icc_big(self, n: int) -> None: - # Make sure that the "extra" support handles large blocks - # The ICC APP marker can store 65519 bytes per marker, so - # using a 4-byte test code should allow us to detect out of - # order issues. - icc_profile = (b"Test" * int(n / 4 + 1))[:n] - assert len(icc_profile) == n # sanity - im1 = self.roundtrip(hopper(), icc_profile=icc_profile) - assert im1.info.get("icc_profile") == (icc_profile or None) - - @mark_if_feature_version( - pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" - ) - def test_large_icc_meta(self, tmp_path: Path) -> None: - # https://github.com/python-pillow/Pillow/issues/148 - # Sometimes the meta data on the icc_profile block is bigger than - # Image.MAXBLOCK or the image size. - with Image.open("Tests/images/icc_profile_big.jpg") as im: - f = str(tmp_path / "temp.jpg") - icc_profile = im.info["icc_profile"] - # Should not raise OSError for image with icc larger than image size. - im.save( - f, - progressive=True, - quality=95, - icc_profile=icc_profile, - optimize=True, - ) - - with Image.open("Tests/images/flower2.jpg") as im: - f = str(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") - im.save(f, progressive=True, quality=94, exif=b" " * 43668) - - def test_optimize(self) -> None: - im1, im1_bytes = self.roundtrip_with_bytes(hopper()) - im2, im2_bytes = self.roundtrip_with_bytes(hopper(), optimize=0) - im3, im3_bytes = self.roundtrip_with_bytes(hopper(), optimize=1) - assert_image_equal(im1, im2) - assert_image_equal(im1, im3) - assert im1_bytes >= im2_bytes - assert im1_bytes >= im3_bytes - - def test_optimize_large_buffer(self, tmp_path: Path) -> None: - # https://github.com/python-pillow/Pillow/issues/148 - f = str(tmp_path / "temp.jpg") - # this requires ~ 1.5x Image.MAXBLOCK - im = Image.new("RGB", (4096, 4096), 0xFF3333) - im.save(f, format="JPEG", optimize=True) - - def test_progressive(self) -> None: - im1, im1_bytes = self.roundtrip_with_bytes(hopper()) - im2 = self.roundtrip(hopper(), progressive=False) - im3, im3_bytes = self.roundtrip_with_bytes(hopper(), progressive=True) - assert not im1.info.get("progressive") - assert not im2.info.get("progressive") - assert im3.info.get("progressive") - - 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") - # 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") - 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) - - def test_progressive_cmyk_buffer(self) -> None: - # Issue 2272, quality 90 cmyk image is tripping the large buffer bug. - f = BytesIO() - im = self.gen_random_image((256, 256), "CMYK") - im.save(f, format="JPEG", progressive=True, quality=94) - - def test_large_exif(self, tmp_path: Path) -> None: - # https://github.com/python-pillow/Pillow/issues/148 - f = str(tmp_path / "temp.jpg") - im = hopper() - im.save(f, "JPEG", quality=90, exif=b"1" * 65533) - - with pytest.raises(ValueError): - im.save(f, "JPEG", quality=90, exif=b"1" * 65534) - - def test_exif_typeerror(self) -> None: - with Image.open("Tests/images/exif_typeerror.jpg") as im: - # Should not raise a TypeError - im._getexif() - - def test_exif_gps(self, tmp_path: Path) -> None: - expected_exif_gps = { - 0: b"\x00\x00\x00\x01", - 2: 4294967295, - 5: b"\x01", - 30: 65535, - 29: "1999:99:99 99:99:99", - } - gps_index = 34853 - - # Reading - with Image.open("Tests/images/exif_gps.jpg") as im: - exif = im._getexif() - assert exif[gps_index] == expected_exif_gps - - # Writing - f = str(tmp_path / "temp.jpg") - exif = Image.Exif() - exif[gps_index] = expected_exif_gps - hopper().save(f, exif=exif) - - with Image.open(f) as reloaded: - exif = reloaded._getexif() - assert exif[gps_index] == expected_exif_gps - - def test_empty_exif_gps(self) -> None: - with Image.open("Tests/images/empty_gps_ifd.jpg") as im: - exif = im.getexif() - del exif[0x8769] - - # Assert that it needs to be transposed - assert exif[0x0112] == Image.Transpose.TRANSVERSE - - # Assert that the GPS IFD is present and empty - assert exif.get_ifd(0x8825) == {} - - transposed = ImageOps.exif_transpose(im) - assert transposed is not None - exif = transposed.getexif() - assert exif.get_ifd(0x8825) == {} - - # Assert that it was transposed - assert 0x0112 not in exif - - def test_exif_equality(self) -> None: - # In 7.2.0, Exif rationals were changed to be read as - # TiffImagePlugin.IFDRational. This class had a bug in __eq__, - # breaking the self-equality of Exif data - exifs = [] - for i in range(2): - with Image.open("Tests/images/exif-200dpcm.jpg") as im: - exifs.append(im._getexif()) - assert exifs[0] == exifs[1] - - def test_exif_rollback(self) -> None: - # rolling back exif support in 3.1 to pre-3.0 formatting. - # expected from 2.9, with b/u qualifiers switched for 3.2 compatibility - # this test passes on 2.9 and 3.1, but not 3.0 - expected_exif = { - 34867: 4294967295, - 258: (24, 24, 24), - 36867: "2099:09:29 10:10:10", - 34853: { - 0: b"\x00\x00\x00\x01", - 2: 4294967295, - 5: b"\x01", - 30: 65535, - 29: "1999:99:99 99:99:99", - }, - 296: 65535, - 34665: 185, - 41994: 65535, - 514: 4294967295, - 271: "Make", - 272: "XXX-XXX", - 305: "PIL", - 42034: (1, 1, 1, 1), - 42035: "LensMake", - 34856: b"\xaa\xaa\xaa\xaa\xaa\xaa", - 282: 4294967295, - 33434: 4294967295, - } - - with Image.open("Tests/images/exif_gps.jpg") as im: - exif = im._getexif() - - for tag, value in expected_exif.items(): - assert value == exif[tag] - - def test_exif_gps_typeerror(self) -> None: - with Image.open("Tests/images/exif_gps_typeerror.jpg") as im: - # Should not raise a TypeError - im._getexif() - - def test_progressive_compat(self) -> None: - im1 = self.roundtrip(hopper()) - assert not im1.info.get("progressive") - assert not im1.info.get("progression") - - im2 = self.roundtrip(hopper(), progressive=0) - im3 = self.roundtrip(hopper(), progression=0) # compatibility - assert not im2.info.get("progressive") - assert not im2.info.get("progression") - assert not im3.info.get("progressive") - assert not im3.info.get("progression") - - im2 = self.roundtrip(hopper(), progressive=1) - im3 = self.roundtrip(hopper(), progression=1) # compatibility - assert_image_equal(im1, im2) - assert_image_equal(im1, im3) - assert im2.info.get("progressive") - assert im2.info.get("progression") - assert im3.info.get("progressive") - assert im3.info.get("progression") - - def test_quality(self) -> None: - im1, im1_bytes = self.roundtrip_with_bytes(hopper()) - im2, im2_bytes = self.roundtrip_with_bytes(hopper(), quality=50) - assert_image(im1, im2.mode, im2.size) - assert im1_bytes >= im2_bytes - - im3, im3_bytes = self.roundtrip_with_bytes(hopper(), quality=0) - assert_image(im1, im3.mode, im3.size) - assert im2_bytes > im3_bytes - - def test_smooth(self) -> None: - im1 = self.roundtrip(hopper()) - im2 = self.roundtrip(hopper(), smooth=100) - assert_image(im1, im2.mode, im2.size) - - def test_subsampling(self) -> None: - def getsampling(im: JpegImagePlugin.JpegImageFile): - layer = im.layer - return layer[0][1:3] + layer[1][1:3] + layer[2][1:3] - - # experimental API - for subsampling in (-1, 3): # (default, invalid) - im = self.roundtrip(hopper(), subsampling=subsampling) - assert getsampling(im) == (2, 2, 1, 1, 1, 1) - for subsampling1 in (0, "4:4:4"): - im = self.roundtrip(hopper(), subsampling=subsampling1) - assert getsampling(im) == (1, 1, 1, 1, 1, 1) - for subsampling1 in (1, "4:2:2"): - im = self.roundtrip(hopper(), subsampling=subsampling1) - assert getsampling(im) == (2, 1, 1, 1, 1, 1) - for subsampling1 in (2, "4:2:0", "4:1:1"): - im = self.roundtrip(hopper(), subsampling=subsampling1) - assert getsampling(im) == (2, 2, 1, 1, 1, 1) - - # RGB colorspace - for subsampling1 in (-1, 0, "4:4:4"): - # "4:4:4" doesn't really make sense for RGB, but the conversion - # to an integer happens at a higher level - im = self.roundtrip(hopper(), keep_rgb=True, subsampling=subsampling1) - assert getsampling(im) == (1, 1, 1, 1, 1, 1) - for subsampling1 in (1, "4:2:2", 2, "4:2:0", 3): - with pytest.raises(OSError): - self.roundtrip(hopper(), keep_rgb=True, subsampling=subsampling1) - - with pytest.raises(TypeError): - self.roundtrip(hopper(), subsampling="1:1:1") - - def test_exif(self) -> None: - with Image.open("Tests/images/pil_sample_rgb.jpg") as im: - info = im._getexif() - assert info[305] == "Adobe Photoshop CS Macintosh" - - def test_get_child_images(self) -> None: - with Image.open("Tests/images/flower.jpg") as im: - ims = im.get_child_images() - - assert len(ims) == 1 - assert_image_similar_tofile(ims[0], "Tests/images/flower_thumbnail.png", 2.1) - - def test_mp(self) -> None: - with Image.open("Tests/images/pil_sample_rgb.jpg") as im: - 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") - im.save(f, quality="keep") - # Grayscale - with Image.open("Tests/images/hopper_gray.jpg") as im: - f = str(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") - im.save(f, quality="keep") - - def test_junk_jpeg_header(self) -> None: - # https://github.com/python-pillow/Pillow/issues/630 - filename = "Tests/images/junk_jpeg_header.jpg" - with Image.open(filename): - pass - - def test_ff00_jpeg_header(self) -> None: - filename = "Tests/images/jpeg_ff00_header.jpg" - with Image.open(filename): - pass - - @mark_if_feature_version( - pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" - ) - def test_truncated_jpeg_should_read_all_the_data(self) -> None: - filename = "Tests/images/truncated_jpeg.jpg" - ImageFile.LOAD_TRUNCATED_IMAGES = True - with Image.open(filename) as im: - im.load() - ImageFile.LOAD_TRUNCATED_IMAGES = False - assert im.getbbox() is not None - - def test_truncated_jpeg_throws_oserror(self) -> None: - filename = "Tests/images/truncated_jpeg.jpg" - with Image.open(filename) as im: - with pytest.raises(OSError): - im.load() - - # Test that the error is raised if loaded a second time - with pytest.raises(OSError): - im.load() - - @mark_if_feature_version( - pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" - ) - def test_qtables(self, tmp_path: Path) -> None: - def _n_qtables_helper(n: int, test_file: str) -> None: - with Image.open(test_file) as im: - f = str(tmp_path / "temp.jpg") - im.save(f, qtables=[[n] * 64] * n) - with Image.open(f) as im: - assert len(im.quantization) == n - reloaded = self.roundtrip(im, qtables="keep") - assert im.quantization == reloaded.quantization - assert max(reloaded.quantization[0]) <= 255 - - with Image.open("Tests/images/hopper.jpg") as im: - qtables = im.quantization - reloaded = self.roundtrip(im, qtables=qtables, subsampling=0) - assert im.quantization == reloaded.quantization - assert_image_similar(im, self.roundtrip(im, qtables="web_low"), 30) - assert_image_similar(im, self.roundtrip(im, qtables="web_high"), 30) - assert_image_similar(im, self.roundtrip(im, qtables="keep"), 30) - - # valid bounds for baseline qtable - bounds_qtable = [int(s) for s in ("255 1 " * 32).split(None)] - im2 = self.roundtrip(im, qtables=[bounds_qtable]) - assert im2.quantization == {0: bounds_qtable} - - # values from wizard.txt in jpeg9-a src package. - standard_l_qtable = [ - int(s) - for s in """ - 16 11 10 16 24 40 51 61 - 12 12 14 19 26 58 60 55 - 14 13 16 24 40 57 69 56 - 14 17 22 29 51 87 80 62 - 18 22 37 56 68 109 103 77 - 24 35 55 64 81 104 113 92 - 49 64 78 87 103 121 120 101 - 72 92 95 98 112 100 103 99 - """.split( - None - ) - ] - - standard_chrominance_qtable = [ - int(s) - for s in """ - 17 18 24 47 99 99 99 99 - 18 21 26 66 99 99 99 99 - 24 26 56 99 99 99 99 99 - 47 66 99 99 99 99 99 99 - 99 99 99 99 99 99 99 99 - 99 99 99 99 99 99 99 99 - 99 99 99 99 99 99 99 99 - 99 99 99 99 99 99 99 99 - """.split( - None - ) - ] - # list of qtable lists - assert_image_similar( - im, - self.roundtrip( - im, qtables=[standard_l_qtable, standard_chrominance_qtable] - ), - 30, - ) - - # tuple of qtable lists - assert_image_similar( - im, - self.roundtrip( - im, qtables=(standard_l_qtable, standard_chrominance_qtable) - ), - 30, - ) - - # dict of qtable lists - assert_image_similar( - im, - self.roundtrip( - im, qtables={0: standard_l_qtable, 1: standard_chrominance_qtable} - ), - 30, - ) - - _n_qtables_helper(1, "Tests/images/hopper_gray.jpg") - _n_qtables_helper(1, "Tests/images/pil_sample_rgb.jpg") - _n_qtables_helper(2, "Tests/images/pil_sample_rgb.jpg") - _n_qtables_helper(3, "Tests/images/pil_sample_rgb.jpg") - _n_qtables_helper(1, "Tests/images/pil_sample_cmyk.jpg") - _n_qtables_helper(2, "Tests/images/pil_sample_cmyk.jpg") - _n_qtables_helper(3, "Tests/images/pil_sample_cmyk.jpg") - _n_qtables_helper(4, "Tests/images/pil_sample_cmyk.jpg") - - # not a sequence - with pytest.raises(ValueError): - self.roundtrip(im, qtables="a") - # sequence wrong length - with pytest.raises(ValueError): - self.roundtrip(im, qtables=[]) - # sequence wrong length - with pytest.raises(ValueError): - self.roundtrip(im, qtables=[1, 2, 3, 4, 5]) - - # qtable entry not a sequence - with pytest.raises(ValueError): - self.roundtrip(im, qtables=[1]) - # qtable entry has wrong number of items - with pytest.raises(ValueError): - self.roundtrip(im, qtables=[[1, 2, 3, 4]]) - - def test_load_16bit_qtables(self) -> None: - with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im: - assert len(im.quantization) == 2 - assert len(im.quantization[0]) == 64 - assert max(im.quantization[0]) > 255 - - def test_save_multiple_16bit_qtables(self) -> None: - with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im: - im2 = self.roundtrip(im, qtables="keep") - assert im.quantization == im2.quantization - - def test_save_single_16bit_qtable(self) -> None: - with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im: - im2 = self.roundtrip(im, qtables={0: im.quantization[0]}) - assert len(im2.quantization) == 1 - assert im2.quantization[0] == im.quantization[0] - - def test_save_low_quality_baseline_qtables(self) -> None: - with Image.open(TEST_FILE) as im: - im2 = self.roundtrip(im, quality=10) - assert len(im2.quantization) == 2 - assert max(im2.quantization[0]) <= 255 - assert max(im2.quantization[1]) <= 255 - - @pytest.mark.parametrize( - "blocks, rows, markers", - ((0, 0, 0), (1, 0, 15), (3, 0, 5), (8, 0, 1), (0, 1, 3), (0, 2, 1)), - ) - def test_restart_markers(self, blocks: int, rows: int, markers: int) -> None: - im = Image.new("RGB", (32, 32)) # 16 MCUs - out = BytesIO() - im.save( - out, - format="JPEG", - restart_marker_blocks=blocks, - restart_marker_rows=rows, - # force 8x8 pixel MCUs - subsampling=0, - ) - assert len(re.findall(b"\xff[\xd0-\xd7]", out.getvalue())) == markers - - @pytest.mark.skipif(not djpeg_available(), reason="djpeg not available") - def test_load_djpeg(self) -> None: - with Image.open(TEST_FILE) as img: - img.load_djpeg() - assert_image_similar_tofile(img, TEST_FILE, 5) - - @pytest.mark.skipif(not cjpeg_available(), reason="cjpeg not available") - def test_save_cjpeg(self, tmp_path: Path) -> None: - with Image.open(TEST_FILE) as img: - tempfile = str(tmp_path / "temp.jpg") - JpegImagePlugin._save_cjpeg(img, 0, tempfile) - # Default save quality is 75%, so a tiny bit of difference is alright - assert_image_similar_tofile(img, tempfile, 17) - - def test_no_duplicate_0x1001_tag(self) -> None: - # Arrange - tag_ids = {v: k for k, v in ExifTags.TAGS.items()} - - # Assert - assert tag_ids["RelatedImageWidth"] == 0x1001 - assert tag_ids["RelatedImageLength"] == 0x1002 - - def test_MAXBLOCK_scaling(self, tmp_path: Path) -> None: - im = self.gen_random_image((512, 512)) - f = str(tmp_path / "temp.jpeg") - im.save(f, quality=100, optimize=True) - - with Image.open(f) as reloaded: - # none of these should crash - reloaded.save(f, quality="keep") - reloaded.save(f, quality="keep", progressive=True) - reloaded.save(f, quality="keep", optimize=True) - - def test_bad_mpo_header(self) -> None: - """Treat unknown MPO as JPEG""" - # Arrange - - # 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" - - @pytest.mark.parametrize("mode", ("1", "L", "RGB", "RGBX", "CMYK", "YCbCr")) - def test_save_correct_modes(self, mode: str) -> None: - out = BytesIO() - img = Image.new(mode, (20, 20)) - img.save(out, "JPEG") - - @pytest.mark.parametrize("mode", ("LA", "La", "RGBA", "RGBa", "P")) - def test_save_wrong_modes(self, mode: str) -> None: - # ref https://github.com/python-pillow/Pillow/issues/2005 - out = BytesIO() - img = Image.new(mode, (20, 20)) - with pytest.raises(OSError): - img.save(out, "JPEG") - - def test_save_tiff_with_dpi(self, tmp_path: Path) -> None: - # Arrange - outfile = str(tmp_path / "temp.tif") - with Image.open("Tests/images/hopper.tif") as im: - # Act - im.save(outfile, "JPEG", dpi=im.info["dpi"]) - - # Assert - with Image.open(outfile) as reloaded: - reloaded.load() - assert im.info["dpi"] == reloaded.info["dpi"] - - def test_save_dpi_rounding(self, tmp_path: Path) -> None: - outfile = str(tmp_path / "temp.jpg") - with Image.open("Tests/images/hopper.jpg") as im: - im.save(outfile, dpi=(72.2, 72.2)) - - with Image.open(outfile) as reloaded: - assert reloaded.info["dpi"] == (72, 72) - - im.save(outfile, dpi=(72.8, 72.8)) - - with Image.open(outfile) as reloaded: - assert reloaded.info["dpi"] == (73, 73) - - def test_dpi_tuple_from_exif(self) -> None: - # Arrange - # This Photoshop CC 2017 image has DPI in EXIF not metadata - # EXIF XResolution is (2000000, 10000) - with Image.open("Tests/images/photoshop-200dpi.jpg") as im: - # Act / Assert - assert im.info.get("dpi") == (200, 200) - - def test_dpi_int_from_exif(self) -> None: - # Arrange - # This image has DPI in EXIF not metadata - # EXIF XResolution is 72 - with Image.open("Tests/images/exif-72dpi-int.jpg") as im: - # Act / Assert - assert im.info.get("dpi") == (72, 72) - - def test_dpi_from_dpcm_exif(self) -> None: - # Arrange - # This is photoshop-200dpi.jpg with EXIF resolution unit set to cm: - # exiftool -exif:ResolutionUnit=cm photoshop-200dpi.jpg - with Image.open("Tests/images/exif-200dpcm.jpg") as im: - # Act / Assert - assert im.info.get("dpi") == (508, 508) - - def test_dpi_exif_zero_division(self) -> None: - # Arrange - # This is photoshop-200dpi.jpg with EXIF resolution set to 0/0: - # exiftool -XResolution=0/0 -YResolution=0/0 photoshop-200dpi.jpg - with Image.open("Tests/images/exif-dpi-zerodivision.jpg") as im: - # Act / Assert - # This should return the default, and not raise a ZeroDivisionError - assert im.info.get("dpi") == (72, 72) - - def test_dpi_exif_string(self) -> None: - # Arrange - # 0x011A tag in this exif contains string '300300\x02' - with Image.open("Tests/images/broken_exif_dpi.jpg") as im: - # Act / Assert - # This should return the default - assert im.info.get("dpi") == (72, 72) - - def test_dpi_exif_truncated(self) -> None: - # Arrange - with Image.open("Tests/images/truncated_exif_dpi.jpg") as im: - # Act / Assert - # This should return the default - assert im.info.get("dpi") == (72, 72) - - def test_no_dpi_in_exif(self) -> None: - # Arrange - # This is photoshop-200dpi.jpg with resolution removed from EXIF: - # exiftool "-*resolution*"= photoshop-200dpi.jpg - with Image.open("Tests/images/no-dpi-in-exif.jpg") as im: - # Act / Assert - # "When the image resolution is unknown, 72 [dpi] is designated." - # https://web.archive.org/web/20240227115053/https://exiv2.org/tags.html - assert im.info.get("dpi") == (72, 72) - - def test_invalid_exif(self) -> None: - # This is no-dpi-in-exif with the tiff header of the exif block - # hexedited from MM * to FF FF FF FF - with Image.open("Tests/images/invalid-exif.jpg") as im: - # This should return the default, and not a SyntaxError or - # OSError for unidentified image. - assert im.info.get("dpi") == (72, 72) - - @mark_if_feature_version( - pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" - ) - def test_exif_x_resolution(self, tmp_path: Path) -> None: - with Image.open("Tests/images/flower.jpg") as im: - exif = im.getexif() - assert exif[282] == 180 - - out = str(tmp_path / "out.jpg") - with warnings.catch_warnings(): - im.save(out, exif=exif) - - with Image.open(out) as reloaded: - assert reloaded.getexif()[282] == 180 - - def test_invalid_exif_x_resolution(self) -> None: - # When no x or y resolution is defined in EXIF - with Image.open("Tests/images/invalid-exif-without-x-resolution.jpg") as im: - # This should return the default, and not a ValueError or - # OSError for an unidentified image. - assert im.info.get("dpi") == (72, 72) - - def test_ifd_offset_exif(self) -> None: - # Arrange - # This image has been manually hexedited to have an IFD offset of 10, - # in contrast to normal 8 - with Image.open("Tests/images/exif-ifd-offset.jpg") as im: - # Act / Assert - assert im._getexif()[306] == "2017:03:13 23:03:09" - - def test_multiple_exif(self) -> None: - with Image.open("Tests/images/multiple_exif.jpg") as im: - assert im.info["exif"] == b"Exif\x00\x00firstsecond" - - @mark_if_feature_version( - pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" - ) - def test_photoshop(self) -> None: - with Image.open("Tests/images/photoshop-200dpi.jpg") as im: - assert im.info["photoshop"][0x03ED] == { - "XResolution": 200.0, - "DisplayedUnitsX": 1, - "YResolution": 200.0, - "DisplayedUnitsY": 1, - } - - # Test that the image can still load, even with broken Photoshop data - # This image had the APP13 length hexedited to be smaller - assert_image_equal_tofile(im, "Tests/images/photoshop-200dpi-broken.jpg") - - # This image does not contain a Photoshop header string - with Image.open("Tests/images/app13.jpg") as im: - assert "photoshop" not in im.info - - def test_photoshop_malformed_and_multiple(self) -> None: - with Image.open("Tests/images/app13-multiple.jpg") as im: - assert "photoshop" in im.info - assert 24 == len(im.info["photoshop"]) - apps_13_lengths = [len(v) for k, v in im.applist if k == "APP13"] - assert [65504, 24] == apps_13_lengths - - def test_adobe_transform(self) -> None: - with Image.open("Tests/images/pil_sample_rgb.jpg") as im: - assert im.info["adobe_transform"] == 1 - - with Image.open("Tests/images/pil_sample_cmyk.jpg") as im: - assert im.info["adobe_transform"] == 2 - - # This image has been manually hexedited - # so that the APP14 reports its length to be 11, - # leaving no room for "adobe_transform" - with Image.open("Tests/images/truncated_app14.jpg") as im: - assert "adobe" in im.info - assert "adobe_transform" not in im.info - - def test_icc_after_SOF(self) -> None: - with Image.open("Tests/images/icc-after-SOF.jpg") as im: - assert im.info["icc_profile"] == b"profile" - - def test_jpeg_magic_number(self) -> None: - size = 4097 - buffer = BytesIO(b"\xFF" * size) # Many xFF bytes - buffer.max_pos = 0 - orig_read = buffer.read - - def read(n=-1): - res = orig_read(n) - buffer.max_pos = max(buffer.max_pos, buffer.tell()) - return res - - buffer.read = read - with pytest.raises(UnidentifiedImageError): - with Image.open(buffer): - pass - - # Assert the entire file has not been read - assert 0 < buffer.max_pos < size - - def test_getxmp(self) -> None: - with Image.open("Tests/images/xmp_test.jpg") as im: - if ElementTree is None: - with pytest.warns( - UserWarning, - match="XMP data cannot be read without defusedxml dependency", - ): - assert im.getxmp() == {} - else: - xmp = im.getxmp() - - description = xmp["xmpmeta"]["RDF"]["Description"] - assert description["DerivedFrom"] == { - "documentID": "8367D410E636EA95B7DE7EBA1C43A412", - "originalDocumentID": "8367D410E636EA95B7DE7EBA1C43A412", - } - assert description["Look"]["Description"]["Group"]["Alt"]["li"] == { - "lang": "x-default", - "text": "Profiles", - } - assert description["ToneCurve"]["Seq"]["li"] == ["0, 0", "255, 255"] - - # Attribute - assert description["Version"] == "10.4" - - if ElementTree is not None: - with Image.open("Tests/images/hopper.jpg") as im: - assert im.getxmp() == {} - - def test_getxmp_no_prefix(self) -> None: - with Image.open("Tests/images/xmp_no_prefix.jpg") as im: - if ElementTree is None: - with pytest.warns( - UserWarning, - match="XMP data cannot be read without defusedxml dependency", - ): - assert im.getxmp() == {} - else: - assert im.getxmp() == {"xmpmeta": {"key": "value"}} - - def test_getxmp_padded(self) -> None: - with Image.open("Tests/images/xmp_padded.jpg") as im: - if ElementTree is None: - with pytest.warns( - UserWarning, - match="XMP data cannot be read without defusedxml dependency", - ): - assert im.getxmp() == {} - else: - assert im.getxmp() == {"xmpmeta": None} - - @pytest.mark.timeout(timeout=1) - def test_eof(self) -> None: - # Even though this decoder never says that it is finished - # the image should still end when there is no new data - class InfiniteMockPyDecoder(ImageFile.PyDecoder): - def decode(self, buffer: bytes) -> tuple[int, int]: - return 0, 0 - - Image.register_decoder("INFINITE", InfiniteMockPyDecoder) - - with Image.open(TEST_FILE) as im: - im.tile = [ - ("INFINITE", (0, 0, 128, 128), 0, ("RGB", 0, 1)), - ] - ImageFile.LOAD_TRUNCATED_IMAGES = True - im.load() - ImageFile.LOAD_TRUNCATED_IMAGES = False - - def test_separate_tables(self) -> None: - im = hopper() - data = [] # [interchange, tables-only, image-only] - for streamtype in range(3): - out = BytesIO() - im.save(out, format="JPEG", streamtype=streamtype) - data.append(out.getvalue()) - - # SOI, EOI - for marker in b"\xff\xd8", b"\xff\xd9": - assert marker in data[1] and marker in data[2] - # DHT, DQT - for marker in b"\xff\xc4", b"\xff\xdb": - assert marker in data[1] and marker not in data[2] - # SOF0, SOS, APP0 (JFIF header) - for marker in b"\xff\xc0", b"\xff\xda", b"\xff\xe0": - assert marker not in data[1] and marker in data[2] - - with Image.open(BytesIO(data[0])) as interchange_im: - with Image.open(BytesIO(data[1] + data[2])) as combined_im: - assert_image_equal(interchange_im, combined_im) - - def test_repr_jpeg(self) -> None: - im = hopper() - - with Image.open(BytesIO(im._repr_jpeg_())) as repr_jpeg: - assert repr_jpeg.format == "JPEG" - assert_image_similar(im, repr_jpeg, 17) - - def test_repr_jpeg_error_returns_none(self) -> None: - im = hopper("F") - - assert im._repr_jpeg_() is None - - -@pytest.mark.skipif(not is_win32(), reason="Windows only") -@skip_unless_feature("jpg") -class TestFileCloseW32: - def test_fd_leak(self, tmp_path: Path) -> None: - tmpfile = str(tmp_path / "temp.jpg") - - with Image.open("Tests/images/hopper.jpg") as im: - im.save(tmpfile) - - im = Image.open(tmpfile) - fp = im.fp - assert not fp.closed - with pytest.raises(OSError): - os.remove(tmpfile) - im.load() - assert fp.closed - # this should not fail, as load should have closed the file. - os.remove(tmpfile) +from __future__ import annotations + +import os +import re +import warnings +from io import BytesIO +from pathlib import Path +from types import ModuleType +from typing import Any, cast + +import pytest + +from PIL import ( + ExifTags, + Image, + ImageFile, + ImageOps, + JpegImagePlugin, + UnidentifiedImageError, + features, +) + +from .helper import ( + assert_image, + assert_image_equal, + assert_image_equal_tofile, + assert_image_similar, + assert_image_similar_tofile, + cjpeg_available, + djpeg_available, + hopper, + is_win32, + mark_if_feature_version, + skip_unless_feature, +) + +ElementTree: ModuleType | None +try: + from defusedxml import ElementTree +except ImportError: + ElementTree = None + +TEST_FILE = "Tests/images/hopper.jpg" + + +@skip_unless_feature("jpg") +class TestFileJpeg: + def roundtrip_with_bytes( + self, im: Image.Image, **options: Any + ) -> tuple[JpegImagePlugin.JpegImageFile, int]: + out = BytesIO() + im.save(out, "JPEG", **options) + test_bytes = out.tell() + out.seek(0) + reloaded = cast(JpegImagePlugin.JpegImageFile, Image.open(out)) + return reloaded, test_bytes + + def roundtrip( + self, im: Image.Image, **options: Any + ) -> JpegImagePlugin.JpegImageFile: + return self.roundtrip_with_bytes(im, **options)[0] + + def gen_random_image(self, size: tuple[int, int], mode: str = "RGB") -> Image.Image: + """Generates a very hard to compress file + :param size: tuple + :param mode: optional image mode + + """ + return Image.frombytes(mode, size, os.urandom(size[0] * size[1] * len(mode))) + + def test_sanity(self) -> None: + # internal version number + version = features.version_codec("jpg") + assert version is not None + assert re.search(r"\d+\.\d+$", version) + + with Image.open(TEST_FILE) as im: + im.load() + assert im.mode == "RGB" + assert im.size == (128, 128) + assert im.format == "JPEG" + assert im.get_format_mimetype() == "image/jpeg" + + @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0))) + def test_zero(self, size: tuple[int, int], tmp_path: Path) -> None: + f = str(tmp_path / "temp.jpg") + im = Image.new("RGB", size) + with pytest.raises(ValueError): + im.save(f) + + def test_app(self) -> None: + # Test APP/COM reader (@PIL135) + with Image.open(TEST_FILE) as im: + assert im.applist[0] == ("APP0", b"JFIF\x00\x01\x01\x01\x00`\x00`\x00\x00") + assert im.applist[1] == ( + "COM", + b"File written by Adobe Photoshop\xa8 4.0\x00", + ) + assert len(im.applist) == 2 + + assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0\x00" + assert im.app["COM"] == im.info["comment"] + + def test_comment_write(self) -> None: + with Image.open(TEST_FILE) as im: + assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0\x00" + + # Test that existing comment is saved by default + out = BytesIO() + im.save(out, format="JPEG") + with Image.open(out) as reloaded: + assert im.info["comment"] == reloaded.info["comment"] + + # Ensure that a blank comment causes any existing comment to be removed + for comment in ("", b"", None): + out = BytesIO() + im.save(out, format="JPEG", comment=comment) + with Image.open(out) as reloaded: + assert "comment" not in reloaded.info + + # Test that a comment argument overrides the default comment + for comment in ("Test comment text", b"Test comment text"): + out = BytesIO() + im.save(out, format="JPEG", comment=comment) + with Image.open(out) as reloaded: + assert reloaded.info["comment"] == b"Test comment text" + + 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))) + assert c == 0.0 + assert m > 0.8 + assert y > 0.8 + assert k == 0.0 + # the opposite corner is black + c, m, y, k = ( + x / 255.0 for x in im.getpixel((im.size[0] - 1, im.size[1] - 1)) + ) + assert k > 0.9 + # roundtrip, and check again + im = self.roundtrip(im) + c, m, y, k = (x / 255.0 for x in im.getpixel((0, 0))) + assert c == 0.0 + assert m > 0.8 + assert y > 0.8 + assert k == 0.0 + c, m, y, k = ( + x / 255.0 for x in im.getpixel((im.size[0] - 1, im.size[1] - 1)) + ) + assert k > 0.9 + + def test_rgb(self) -> None: + def getchannels(im: JpegImagePlugin.JpegImageFile) -> tuple[int, int, int]: + return tuple(v[0] for v in im.layer) + + im = hopper() + im_ycbcr = self.roundtrip(im) + assert getchannels(im_ycbcr) == (1, 2, 3) + assert_image_similar(im, im_ycbcr, 17) + + im_rgb = self.roundtrip(im, keep_rgb=True) + assert getchannels(im_rgb) == (ord("R"), ord("G"), ord("B")) + assert_image_similar(im, im_rgb, 12) + + @pytest.mark.parametrize( + "test_image_path", + [TEST_FILE, "Tests/images/pil_sample_cmyk.jpg"], + ) + def test_dpi(self, test_image_path: str) -> None: + def test(xdpi: int, ydpi: int | None = None) -> tuple[int, int] | None: + with Image.open(test_image_path) as im: + im = self.roundtrip(im, dpi=(xdpi, ydpi or xdpi)) + return im.info.get("dpi") + + assert test(72) == (72, 72) + assert test(300) == (300, 300) + assert test(100, 200) == (100, 200) + assert test(0) is None # square pixels + + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) + def test_icc(self, tmp_path: Path) -> None: + # Test ICC support + with Image.open("Tests/images/rgb.jpg") as im1: + icc_profile = im1.info["icc_profile"] + assert len(icc_profile) == 3144 + # Roundtrip via physical file. + f = str(tmp_path / "temp.jpg") + im1.save(f, icc_profile=icc_profile) + with Image.open(f) as im2: + assert im2.info.get("icc_profile") == icc_profile + # Roundtrip via memory buffer. + im1 = self.roundtrip(hopper()) + im2 = self.roundtrip(hopper(), icc_profile=icc_profile) + assert_image_equal(im1, im2) + assert not im1.info.get("icc_profile") + assert im2.info.get("icc_profile") + + @pytest.mark.parametrize( + "n", + ( + 0, + 1, + 3, + 4, + 5, + 65533 - 14, # full JPEG marker block + 65533 - 14 + 1, # full block plus one byte + ImageFile.MAXBLOCK, # full buffer block + ImageFile.MAXBLOCK + 1, # full buffer block plus one byte + ImageFile.MAXBLOCK * 4 + 3, # large block + ), + ) + def test_icc_big(self, n: int) -> None: + # Make sure that the "extra" support handles large blocks + # The ICC APP marker can store 65519 bytes per marker, so + # using a 4-byte test code should allow us to detect out of + # order issues. + icc_profile = (b"Test" * int(n / 4 + 1))[:n] + assert len(icc_profile) == n # sanity + im1 = self.roundtrip(hopper(), icc_profile=icc_profile) + assert im1.info.get("icc_profile") == (icc_profile or None) + + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) + def test_large_icc_meta(self, tmp_path: Path) -> None: + # https://github.com/python-pillow/Pillow/issues/148 + # Sometimes the meta data on the icc_profile block is bigger than + # Image.MAXBLOCK or the image size. + with Image.open("Tests/images/icc_profile_big.jpg") as im: + f = str(tmp_path / "temp.jpg") + icc_profile = im.info["icc_profile"] + # Should not raise OSError for image with icc larger than image size. + im.save( + f, + progressive=True, + quality=95, + icc_profile=icc_profile, + optimize=True, + ) + + with Image.open("Tests/images/flower2.jpg") as im: + f = str(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") + im.save(f, progressive=True, quality=94, exif=b" " * 43668) + + def test_optimize(self) -> None: + im1, im1_bytes = self.roundtrip_with_bytes(hopper()) + im2, im2_bytes = self.roundtrip_with_bytes(hopper(), optimize=0) + im3, im3_bytes = self.roundtrip_with_bytes(hopper(), optimize=1) + assert_image_equal(im1, im2) + assert_image_equal(im1, im3) + assert im1_bytes >= im2_bytes + assert im1_bytes >= im3_bytes + + def test_optimize_large_buffer(self, tmp_path: Path) -> None: + # https://github.com/python-pillow/Pillow/issues/148 + f = str(tmp_path / "temp.jpg") + # this requires ~ 1.5x Image.MAXBLOCK + im = Image.new("RGB", (4096, 4096), 0xFF3333) + im.save(f, format="JPEG", optimize=True) + + def test_progressive(self) -> None: + im1, im1_bytes = self.roundtrip_with_bytes(hopper()) + im2 = self.roundtrip(hopper(), progressive=False) + im3, im3_bytes = self.roundtrip_with_bytes(hopper(), progressive=True) + assert not im1.info.get("progressive") + assert not im2.info.get("progressive") + assert im3.info.get("progressive") + + 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") + # 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") + 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) + + def test_progressive_cmyk_buffer(self) -> None: + # Issue 2272, quality 90 cmyk image is tripping the large buffer bug. + f = BytesIO() + im = self.gen_random_image((256, 256), "CMYK") + im.save(f, format="JPEG", progressive=True, quality=94) + + def test_large_exif(self, tmp_path: Path) -> None: + # https://github.com/python-pillow/Pillow/issues/148 + f = str(tmp_path / "temp.jpg") + im = hopper() + im.save(f, "JPEG", quality=90, exif=b"1" * 65533) + + with pytest.raises(ValueError): + im.save(f, "JPEG", quality=90, exif=b"1" * 65534) + + def test_exif_typeerror(self) -> None: + with Image.open("Tests/images/exif_typeerror.jpg") as im: + # Should not raise a TypeError + im._getexif() + + def test_exif_gps(self, tmp_path: Path) -> None: + expected_exif_gps = { + 0: b"\x00\x00\x00\x01", + 2: 4294967295, + 5: b"\x01", + 30: 65535, + 29: "1999:99:99 99:99:99", + } + gps_index = 34853 + + # Reading + with Image.open("Tests/images/exif_gps.jpg") as im: + exif = im._getexif() + assert exif[gps_index] == expected_exif_gps + + # Writing + f = str(tmp_path / "temp.jpg") + exif = Image.Exif() + exif[gps_index] = expected_exif_gps + hopper().save(f, exif=exif) + + with Image.open(f) as reloaded: + exif = reloaded._getexif() + assert exif[gps_index] == expected_exif_gps + + def test_empty_exif_gps(self) -> None: + with Image.open("Tests/images/empty_gps_ifd.jpg") as im: + exif = im.getexif() + del exif[0x8769] + + # Assert that it needs to be transposed + assert exif[0x0112] == Image.Transpose.TRANSVERSE + + # Assert that the GPS IFD is present and empty + assert exif.get_ifd(0x8825) == {} + + transposed = ImageOps.exif_transpose(im) + assert transposed is not None + exif = transposed.getexif() + assert exif.get_ifd(0x8825) == {} + + # Assert that it was transposed + assert 0x0112 not in exif + + def test_exif_equality(self) -> None: + # In 7.2.0, Exif rationals were changed to be read as + # TiffImagePlugin.IFDRational. This class had a bug in __eq__, + # breaking the self-equality of Exif data + exifs = [] + for i in range(2): + with Image.open("Tests/images/exif-200dpcm.jpg") as im: + exifs.append(im._getexif()) + assert exifs[0] == exifs[1] + + def test_exif_rollback(self) -> None: + # rolling back exif support in 3.1 to pre-3.0 formatting. + # expected from 2.9, with b/u qualifiers switched for 3.2 compatibility + # this test passes on 2.9 and 3.1, but not 3.0 + expected_exif = { + 34867: 4294967295, + 258: (24, 24, 24), + 36867: "2099:09:29 10:10:10", + 34853: { + 0: b"\x00\x00\x00\x01", + 2: 4294967295, + 5: b"\x01", + 30: 65535, + 29: "1999:99:99 99:99:99", + }, + 296: 65535, + 34665: 185, + 41994: 65535, + 514: 4294967295, + 271: "Make", + 272: "XXX-XXX", + 305: "PIL", + 42034: (1, 1, 1, 1), + 42035: "LensMake", + 34856: b"\xaa\xaa\xaa\xaa\xaa\xaa", + 282: 4294967295, + 33434: 4294967295, + } + + with Image.open("Tests/images/exif_gps.jpg") as im: + exif = im._getexif() + + for tag, value in expected_exif.items(): + assert value == exif[tag] + + def test_exif_gps_typeerror(self) -> None: + with Image.open("Tests/images/exif_gps_typeerror.jpg") as im: + # Should not raise a TypeError + im._getexif() + + def test_progressive_compat(self) -> None: + im1 = self.roundtrip(hopper()) + assert not im1.info.get("progressive") + assert not im1.info.get("progression") + + im2 = self.roundtrip(hopper(), progressive=0) + im3 = self.roundtrip(hopper(), progression=0) # compatibility + assert not im2.info.get("progressive") + assert not im2.info.get("progression") + assert not im3.info.get("progressive") + assert not im3.info.get("progression") + + im2 = self.roundtrip(hopper(), progressive=1) + im3 = self.roundtrip(hopper(), progression=1) # compatibility + assert_image_equal(im1, im2) + assert_image_equal(im1, im3) + assert im2.info.get("progressive") + assert im2.info.get("progression") + assert im3.info.get("progressive") + assert im3.info.get("progression") + + def test_quality(self) -> None: + im1, im1_bytes = self.roundtrip_with_bytes(hopper()) + im2, im2_bytes = self.roundtrip_with_bytes(hopper(), quality=50) + assert_image(im1, im2.mode, im2.size) + assert im1_bytes >= im2_bytes + + im3, im3_bytes = self.roundtrip_with_bytes(hopper(), quality=0) + assert_image(im1, im3.mode, im3.size) + assert im2_bytes > im3_bytes + + def test_smooth(self) -> None: + im1 = self.roundtrip(hopper()) + im2 = self.roundtrip(hopper(), smooth=100) + assert_image(im1, im2.mode, im2.size) + + def test_subsampling(self) -> None: + def getsampling(im: JpegImagePlugin.JpegImageFile): + layer = im.layer + return layer[0][1:3] + layer[1][1:3] + layer[2][1:3] + + # experimental API + for subsampling in (-1, 3): # (default, invalid) + im = self.roundtrip(hopper(), subsampling=subsampling) + assert getsampling(im) == (2, 2, 1, 1, 1, 1) + for subsampling1 in (0, "4:4:4"): + im = self.roundtrip(hopper(), subsampling=subsampling1) + assert getsampling(im) == (1, 1, 1, 1, 1, 1) + for subsampling1 in (1, "4:2:2"): + im = self.roundtrip(hopper(), subsampling=subsampling1) + assert getsampling(im) == (2, 1, 1, 1, 1, 1) + for subsampling1 in (2, "4:2:0", "4:1:1"): + im = self.roundtrip(hopper(), subsampling=subsampling1) + assert getsampling(im) == (2, 2, 1, 1, 1, 1) + + # RGB colorspace + for subsampling1 in (-1, 0, "4:4:4"): + # "4:4:4" doesn't really make sense for RGB, but the conversion + # to an integer happens at a higher level + im = self.roundtrip(hopper(), keep_rgb=True, subsampling=subsampling1) + assert getsampling(im) == (1, 1, 1, 1, 1, 1) + for subsampling1 in (1, "4:2:2", 2, "4:2:0", 3): + with pytest.raises(OSError): + self.roundtrip(hopper(), keep_rgb=True, subsampling=subsampling1) + + with pytest.raises(TypeError): + self.roundtrip(hopper(), subsampling="1:1:1") + + def test_exif(self) -> None: + with Image.open("Tests/images/pil_sample_rgb.jpg") as im: + info = im._getexif() + assert info[305] == "Adobe Photoshop CS Macintosh" + + def test_get_child_images(self) -> None: + with Image.open("Tests/images/flower.jpg") as im: + ims = im.get_child_images() + + assert len(ims) == 1 + assert_image_similar_tofile(ims[0], "Tests/images/flower_thumbnail.png", 2.1) + + def test_mp(self) -> None: + with Image.open("Tests/images/pil_sample_rgb.jpg") as im: + 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") + im.save(f, quality="keep") + # Grayscale + with Image.open("Tests/images/hopper_gray.jpg") as im: + f = str(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") + im.save(f, quality="keep") + + def test_junk_jpeg_header(self) -> None: + # https://github.com/python-pillow/Pillow/issues/630 + filename = "Tests/images/junk_jpeg_header.jpg" + with Image.open(filename): + pass + + def test_ff00_jpeg_header(self) -> None: + filename = "Tests/images/jpeg_ff00_header.jpg" + with Image.open(filename): + pass + + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) + def test_truncated_jpeg_should_read_all_the_data(self) -> None: + filename = "Tests/images/truncated_jpeg.jpg" + ImageFile.LOAD_TRUNCATED_IMAGES = True + with Image.open(filename) as im: + im.load() + ImageFile.LOAD_TRUNCATED_IMAGES = False + assert im.getbbox() is not None + + def test_truncated_jpeg_throws_oserror(self) -> None: + filename = "Tests/images/truncated_jpeg.jpg" + with Image.open(filename) as im: + with pytest.raises(OSError): + im.load() + + # Test that the error is raised if loaded a second time + with pytest.raises(OSError): + im.load() + + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) + def test_qtables(self, tmp_path: Path) -> None: + def _n_qtables_helper(n: int, test_file: str) -> None: + with Image.open(test_file) as im: + f = str(tmp_path / "temp.jpg") + im.save(f, qtables=[[n] * 64] * n) + with Image.open(f) as im: + assert len(im.quantization) == n + reloaded = self.roundtrip(im, qtables="keep") + assert im.quantization == reloaded.quantization + assert max(reloaded.quantization[0]) <= 255 + + with Image.open("Tests/images/hopper.jpg") as im: + qtables = im.quantization + reloaded = self.roundtrip(im, qtables=qtables, subsampling=0) + assert im.quantization == reloaded.quantization + assert_image_similar(im, self.roundtrip(im, qtables="web_low"), 30) + assert_image_similar(im, self.roundtrip(im, qtables="web_high"), 30) + assert_image_similar(im, self.roundtrip(im, qtables="keep"), 30) + + # valid bounds for baseline qtable + bounds_qtable = [int(s) for s in ("255 1 " * 32).split(None)] + im2 = self.roundtrip(im, qtables=[bounds_qtable]) + assert im2.quantization == {0: bounds_qtable} + + # values from wizard.txt in jpeg9-a src package. + standard_l_qtable = [ + int(s) + for s in """ + 16 11 10 16 24 40 51 61 + 12 12 14 19 26 58 60 55 + 14 13 16 24 40 57 69 56 + 14 17 22 29 51 87 80 62 + 18 22 37 56 68 109 103 77 + 24 35 55 64 81 104 113 92 + 49 64 78 87 103 121 120 101 + 72 92 95 98 112 100 103 99 + """.split( + None + ) + ] + + standard_chrominance_qtable = [ + int(s) + for s in """ + 17 18 24 47 99 99 99 99 + 18 21 26 66 99 99 99 99 + 24 26 56 99 99 99 99 99 + 47 66 99 99 99 99 99 99 + 99 99 99 99 99 99 99 99 + 99 99 99 99 99 99 99 99 + 99 99 99 99 99 99 99 99 + 99 99 99 99 99 99 99 99 + """.split( + None + ) + ] + # list of qtable lists + assert_image_similar( + im, + self.roundtrip( + im, qtables=[standard_l_qtable, standard_chrominance_qtable] + ), + 30, + ) + + # tuple of qtable lists + assert_image_similar( + im, + self.roundtrip( + im, qtables=(standard_l_qtable, standard_chrominance_qtable) + ), + 30, + ) + + # dict of qtable lists + assert_image_similar( + im, + self.roundtrip( + im, qtables={0: standard_l_qtable, 1: standard_chrominance_qtable} + ), + 30, + ) + + _n_qtables_helper(1, "Tests/images/hopper_gray.jpg") + _n_qtables_helper(1, "Tests/images/pil_sample_rgb.jpg") + _n_qtables_helper(2, "Tests/images/pil_sample_rgb.jpg") + _n_qtables_helper(3, "Tests/images/pil_sample_rgb.jpg") + _n_qtables_helper(1, "Tests/images/pil_sample_cmyk.jpg") + _n_qtables_helper(2, "Tests/images/pil_sample_cmyk.jpg") + _n_qtables_helper(3, "Tests/images/pil_sample_cmyk.jpg") + _n_qtables_helper(4, "Tests/images/pil_sample_cmyk.jpg") + + # not a sequence + with pytest.raises(ValueError): + self.roundtrip(im, qtables="a") + # sequence wrong length + with pytest.raises(ValueError): + self.roundtrip(im, qtables=[]) + # sequence wrong length + with pytest.raises(ValueError): + self.roundtrip(im, qtables=[1, 2, 3, 4, 5]) + + # qtable entry not a sequence + with pytest.raises(ValueError): + self.roundtrip(im, qtables=[1]) + # qtable entry has wrong number of items + with pytest.raises(ValueError): + self.roundtrip(im, qtables=[[1, 2, 3, 4]]) + + def test_load_16bit_qtables(self) -> None: + with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im: + assert len(im.quantization) == 2 + assert len(im.quantization[0]) == 64 + assert max(im.quantization[0]) > 255 + + def test_save_multiple_16bit_qtables(self) -> None: + with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im: + im2 = self.roundtrip(im, qtables="keep") + assert im.quantization == im2.quantization + + def test_save_single_16bit_qtable(self) -> None: + with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im: + im2 = self.roundtrip(im, qtables={0: im.quantization[0]}) + assert len(im2.quantization) == 1 + assert im2.quantization[0] == im.quantization[0] + + def test_save_low_quality_baseline_qtables(self) -> None: + with Image.open(TEST_FILE) as im: + im2 = self.roundtrip(im, quality=10) + assert len(im2.quantization) == 2 + assert max(im2.quantization[0]) <= 255 + assert max(im2.quantization[1]) <= 255 + + @pytest.mark.parametrize( + "blocks, rows, markers", + ((0, 0, 0), (1, 0, 15), (3, 0, 5), (8, 0, 1), (0, 1, 3), (0, 2, 1)), + ) + def test_restart_markers(self, blocks: int, rows: int, markers: int) -> None: + im = Image.new("RGB", (32, 32)) # 16 MCUs + out = BytesIO() + im.save( + out, + format="JPEG", + restart_marker_blocks=blocks, + restart_marker_rows=rows, + # force 8x8 pixel MCUs + subsampling=0, + ) + assert len(re.findall(b"\xff[\xd0-\xd7]", out.getvalue())) == markers + + @pytest.mark.skipif(not djpeg_available(), reason="djpeg not available") + def test_load_djpeg(self) -> None: + with Image.open(TEST_FILE) as img: + img.load_djpeg() + assert_image_similar_tofile(img, TEST_FILE, 5) + + @pytest.mark.skipif(not cjpeg_available(), reason="cjpeg not available") + def test_save_cjpeg(self, tmp_path: Path) -> None: + with Image.open(TEST_FILE) as img: + tempfile = str(tmp_path / "temp.jpg") + JpegImagePlugin._save_cjpeg(img, 0, tempfile) + # Default save quality is 75%, so a tiny bit of difference is alright + assert_image_similar_tofile(img, tempfile, 17) + + def test_no_duplicate_0x1001_tag(self) -> None: + # Arrange + tag_ids = {v: k for k, v in ExifTags.TAGS.items()} + + # Assert + assert tag_ids["RelatedImageWidth"] == 0x1001 + assert tag_ids["RelatedImageLength"] == 0x1002 + + def test_MAXBLOCK_scaling(self, tmp_path: Path) -> None: + im = self.gen_random_image((512, 512)) + f = str(tmp_path / "temp.jpeg") + im.save(f, quality=100, optimize=True) + + with Image.open(f) as reloaded: + # none of these should crash + reloaded.save(f, quality="keep") + reloaded.save(f, quality="keep", progressive=True) + reloaded.save(f, quality="keep", optimize=True) + + def test_bad_mpo_header(self) -> None: + """Treat unknown MPO as JPEG""" + # Arrange + + # 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" + + @pytest.mark.parametrize("mode", ("1", "L", "RGB", "RGBX", "CMYK", "YCbCr")) + def test_save_correct_modes(self, mode: str) -> None: + out = BytesIO() + img = Image.new(mode, (20, 20)) + img.save(out, "JPEG") + + @pytest.mark.parametrize("mode", ("LA", "La", "RGBA", "RGBa", "P")) + def test_save_wrong_modes(self, mode: str) -> None: + # ref https://github.com/python-pillow/Pillow/issues/2005 + out = BytesIO() + img = Image.new(mode, (20, 20)) + with pytest.raises(OSError): + img.save(out, "JPEG") + + def test_save_tiff_with_dpi(self, tmp_path: Path) -> None: + # Arrange + outfile = str(tmp_path / "temp.tif") + with Image.open("Tests/images/hopper.tif") as im: + # Act + im.save(outfile, "JPEG", dpi=im.info["dpi"]) + + # Assert + with Image.open(outfile) as reloaded: + reloaded.load() + assert im.info["dpi"] == reloaded.info["dpi"] + + def test_save_dpi_rounding(self, tmp_path: Path) -> None: + outfile = str(tmp_path / "temp.jpg") + with Image.open("Tests/images/hopper.jpg") as im: + im.save(outfile, dpi=(72.2, 72.2)) + + with Image.open(outfile) as reloaded: + assert reloaded.info["dpi"] == (72, 72) + + im.save(outfile, dpi=(72.8, 72.8)) + + with Image.open(outfile) as reloaded: + assert reloaded.info["dpi"] == (73, 73) + + def test_dpi_tuple_from_exif(self) -> None: + # Arrange + # This Photoshop CC 2017 image has DPI in EXIF not metadata + # EXIF XResolution is (2000000, 10000) + with Image.open("Tests/images/photoshop-200dpi.jpg") as im: + # Act / Assert + assert im.info.get("dpi") == (200, 200) + + def test_dpi_int_from_exif(self) -> None: + # Arrange + # This image has DPI in EXIF not metadata + # EXIF XResolution is 72 + with Image.open("Tests/images/exif-72dpi-int.jpg") as im: + # Act / Assert + assert im.info.get("dpi") == (72, 72) + + def test_dpi_from_dpcm_exif(self) -> None: + # Arrange + # This is photoshop-200dpi.jpg with EXIF resolution unit set to cm: + # exiftool -exif:ResolutionUnit=cm photoshop-200dpi.jpg + with Image.open("Tests/images/exif-200dpcm.jpg") as im: + # Act / Assert + assert im.info.get("dpi") == (508, 508) + + def test_dpi_exif_zero_division(self) -> None: + # Arrange + # This is photoshop-200dpi.jpg with EXIF resolution set to 0/0: + # exiftool -XResolution=0/0 -YResolution=0/0 photoshop-200dpi.jpg + with Image.open("Tests/images/exif-dpi-zerodivision.jpg") as im: + # Act / Assert + # This should return the default, and not raise a ZeroDivisionError + assert im.info.get("dpi") == (72, 72) + + def test_dpi_exif_string(self) -> None: + # Arrange + # 0x011A tag in this exif contains string '300300\x02' + with Image.open("Tests/images/broken_exif_dpi.jpg") as im: + # Act / Assert + # This should return the default + assert im.info.get("dpi") == (72, 72) + + def test_dpi_exif_truncated(self) -> None: + # Arrange + with Image.open("Tests/images/truncated_exif_dpi.jpg") as im: + # Act / Assert + # This should return the default + assert im.info.get("dpi") == (72, 72) + + def test_no_dpi_in_exif(self) -> None: + # Arrange + # This is photoshop-200dpi.jpg with resolution removed from EXIF: + # exiftool "-*resolution*"= photoshop-200dpi.jpg + with Image.open("Tests/images/no-dpi-in-exif.jpg") as im: + # Act / Assert + # "When the image resolution is unknown, 72 [dpi] is designated." + # https://web.archive.org/web/20240227115053/https://exiv2.org/tags.html + assert im.info.get("dpi") == (72, 72) + + def test_invalid_exif(self) -> None: + # This is no-dpi-in-exif with the tiff header of the exif block + # hexedited from MM * to FF FF FF FF + with Image.open("Tests/images/invalid-exif.jpg") as im: + # This should return the default, and not a SyntaxError or + # OSError for unidentified image. + assert im.info.get("dpi") == (72, 72) + + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) + def test_exif_x_resolution(self, tmp_path: Path) -> None: + with Image.open("Tests/images/flower.jpg") as im: + exif = im.getexif() + assert exif[282] == 180 + + out = str(tmp_path / "out.jpg") + with warnings.catch_warnings(): + im.save(out, exif=exif) + + with Image.open(out) as reloaded: + assert reloaded.getexif()[282] == 180 + + def test_invalid_exif_x_resolution(self) -> None: + # When no x or y resolution is defined in EXIF + with Image.open("Tests/images/invalid-exif-without-x-resolution.jpg") as im: + # This should return the default, and not a ValueError or + # OSError for an unidentified image. + assert im.info.get("dpi") == (72, 72) + + def test_ifd_offset_exif(self) -> None: + # Arrange + # This image has been manually hexedited to have an IFD offset of 10, + # in contrast to normal 8 + with Image.open("Tests/images/exif-ifd-offset.jpg") as im: + # Act / Assert + assert im._getexif()[306] == "2017:03:13 23:03:09" + + def test_multiple_exif(self) -> None: + with Image.open("Tests/images/multiple_exif.jpg") as im: + assert im.info["exif"] == b"Exif\x00\x00firstsecond" + + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) + def test_photoshop(self) -> None: + with Image.open("Tests/images/photoshop-200dpi.jpg") as im: + assert im.info["photoshop"][0x03ED] == { + "XResolution": 200.0, + "DisplayedUnitsX": 1, + "YResolution": 200.0, + "DisplayedUnitsY": 1, + } + + # Test that the image can still load, even with broken Photoshop data + # This image had the APP13 length hexedited to be smaller + assert_image_equal_tofile(im, "Tests/images/photoshop-200dpi-broken.jpg") + + # This image does not contain a Photoshop header string + with Image.open("Tests/images/app13.jpg") as im: + assert "photoshop" not in im.info + + def test_photoshop_malformed_and_multiple(self) -> None: + with Image.open("Tests/images/app13-multiple.jpg") as im: + assert "photoshop" in im.info + assert 24 == len(im.info["photoshop"]) + apps_13_lengths = [len(v) for k, v in im.applist if k == "APP13"] + assert [65504, 24] == apps_13_lengths + + def test_adobe_transform(self) -> None: + with Image.open("Tests/images/pil_sample_rgb.jpg") as im: + assert im.info["adobe_transform"] == 1 + + with Image.open("Tests/images/pil_sample_cmyk.jpg") as im: + assert im.info["adobe_transform"] == 2 + + # This image has been manually hexedited + # so that the APP14 reports its length to be 11, + # leaving no room for "adobe_transform" + with Image.open("Tests/images/truncated_app14.jpg") as im: + assert "adobe" in im.info + assert "adobe_transform" not in im.info + + def test_icc_after_SOF(self) -> None: + with Image.open("Tests/images/icc-after-SOF.jpg") as im: + assert im.info["icc_profile"] == b"profile" + + def test_jpeg_magic_number(self) -> None: + size = 4097 + buffer = BytesIO(b"\xFF" * size) # Many xFF bytes + buffer.max_pos = 0 + orig_read = buffer.read + + def read(n=-1): + res = orig_read(n) + buffer.max_pos = max(buffer.max_pos, buffer.tell()) + return res + + buffer.read = read + with pytest.raises(UnidentifiedImageError): + with Image.open(buffer): + pass + + # Assert the entire file has not been read + assert 0 < buffer.max_pos < size + + def test_getxmp(self) -> None: + with Image.open("Tests/images/xmp_test.jpg") as im: + if ElementTree is None: + with pytest.warns( + UserWarning, + match="XMP data cannot be read without defusedxml dependency", + ): + assert im.getxmp() == {} + else: + xmp = im.getxmp() + + description = xmp["xmpmeta"]["RDF"]["Description"] + assert description["DerivedFrom"] == { + "documentID": "8367D410E636EA95B7DE7EBA1C43A412", + "originalDocumentID": "8367D410E636EA95B7DE7EBA1C43A412", + } + assert description["Look"]["Description"]["Group"]["Alt"]["li"] == { + "lang": "x-default", + "text": "Profiles", + } + assert description["ToneCurve"]["Seq"]["li"] == ["0, 0", "255, 255"] + + # Attribute + assert description["Version"] == "10.4" + + if ElementTree is not None: + with Image.open("Tests/images/hopper.jpg") as im: + assert im.getxmp() == {} + + def test_getxmp_no_prefix(self) -> None: + with Image.open("Tests/images/xmp_no_prefix.jpg") as im: + if ElementTree is None: + with pytest.warns( + UserWarning, + match="XMP data cannot be read without defusedxml dependency", + ): + assert im.getxmp() == {} + else: + assert im.getxmp() == {"xmpmeta": {"key": "value"}} + + def test_getxmp_padded(self) -> None: + with Image.open("Tests/images/xmp_padded.jpg") as im: + if ElementTree is None: + with pytest.warns( + UserWarning, + match="XMP data cannot be read without defusedxml dependency", + ): + assert im.getxmp() == {} + else: + assert im.getxmp() == {"xmpmeta": None} + + @pytest.mark.timeout(timeout=1) + def test_eof(self) -> None: + # Even though this decoder never says that it is finished + # the image should still end when there is no new data + class InfiniteMockPyDecoder(ImageFile.PyDecoder): + def decode(self, buffer: bytes) -> tuple[int, int]: + return 0, 0 + + Image.register_decoder("INFINITE", InfiniteMockPyDecoder) + + with Image.open(TEST_FILE) as im: + im.tile = [ + ("INFINITE", (0, 0, 128, 128), 0, ("RGB", 0, 1)), + ] + ImageFile.LOAD_TRUNCATED_IMAGES = True + im.load() + ImageFile.LOAD_TRUNCATED_IMAGES = False + + def test_separate_tables(self) -> None: + im = hopper() + data = [] # [interchange, tables-only, image-only] + for streamtype in range(3): + out = BytesIO() + im.save(out, format="JPEG", streamtype=streamtype) + data.append(out.getvalue()) + + # SOI, EOI + for marker in b"\xff\xd8", b"\xff\xd9": + assert marker in data[1] and marker in data[2] + # DHT, DQT + for marker in b"\xff\xc4", b"\xff\xdb": + assert marker in data[1] and marker not in data[2] + # SOF0, SOS, APP0 (JFIF header) + for marker in b"\xff\xc0", b"\xff\xda", b"\xff\xe0": + assert marker not in data[1] and marker in data[2] + + with Image.open(BytesIO(data[0])) as interchange_im: + with Image.open(BytesIO(data[1] + data[2])) as combined_im: + assert_image_equal(interchange_im, combined_im) + + def test_repr_jpeg(self) -> None: + im = hopper() + + with Image.open(BytesIO(im._repr_jpeg_())) as repr_jpeg: + assert repr_jpeg.format == "JPEG" + assert_image_similar(im, repr_jpeg, 17) + + def test_repr_jpeg_error_returns_none(self) -> None: + im = hopper("F") + + assert im._repr_jpeg_() is None + + +@pytest.mark.skipif(not is_win32(), reason="Windows only") +@skip_unless_feature("jpg") +class TestFileCloseW32: + def test_fd_leak(self, tmp_path: Path) -> None: + tmpfile = str(tmp_path / "temp.jpg") + + with Image.open("Tests/images/hopper.jpg") as im: + im.save(tmpfile) + + im = Image.open(tmpfile) + fp = im.fp + assert not fp.closed + with pytest.raises(OSError): + os.remove(tmpfile) + im.load() + assert fp.closed + # this should not fail, as load should have closed the file. + os.remove(tmpfile) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 5a208739f..b198724f7 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -1,466 +1,466 @@ -from __future__ import annotations - -import os -import re -from io import BytesIO -from pathlib import Path -from typing import Any - -import pytest - -from PIL import ( - Image, - ImageFile, - Jpeg2KImagePlugin, - UnidentifiedImageError, - _binary, - features, -) - -from .helper import ( - assert_image_equal, - assert_image_similar, - assert_image_similar_tofile, - skip_unless_feature, - skip_unless_feature_version, -) - -EXTRA_DIR = "Tests/images/jpeg2000" - -pytestmark = skip_unless_feature("jpg_2000") - -test_card = Image.open("Tests/images/test-card.png") -test_card.load() - -# OpenJPEG 2.0.0 outputs this debugging message sometimes; we should -# ignore it---it doesn't represent a test failure. -# 'Not enough memory to handle tile data' - - -def roundtrip(im: Image.Image, **options: Any) -> Image.Image: - out = BytesIO() - im.save(out, "JPEG2000", **options) - out.seek(0) - with Image.open(out) as im: - im.load() - return im - - -def test_sanity() -> None: - # Internal version number - version = features.version_codec("jpg_2000") - assert version is not None - assert re.search(r"\d+\.\d+\.\d+$", version) - - with Image.open("Tests/images/test-card-lossless.jp2") as im: - px = im.load() - assert px[0, 0] == (0, 0, 0) - assert im.mode == "RGB" - assert im.size == (640, 480) - assert im.format == "JPEG2000" - assert im.get_format_mimetype() == "image/jp2" - - -def test_jpf() -> None: - with Image.open("Tests/images/balloon.jpf") as im: - assert im.format == "JPEG2000" - assert im.get_format_mimetype() == "image/jpx" - - -def test_invalid_file() -> None: - invalid_file = "Tests/images/flower.jpg" - - with pytest.raises(SyntaxError): - Jpeg2KImagePlugin.Jpeg2KImageFile(invalid_file) - - -def test_bytesio() -> 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) - - -# 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: - with Image.open("Tests/images/test-card-lossless.jp2") as im: - im.load() - outfile = str(tmp_path / "temp_test-card.png") - im.save(outfile) - assert_image_similar(im, test_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_lossless_rt() -> None: - im = roundtrip(test_card) - assert_image_equal(im, test_card) - - -def test_lossy_rt() -> None: - im = roundtrip(test_card, quality_layers=[20]) - assert_image_similar(im, test_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_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_too_small() -> None: - with pytest.raises(ValueError): - roundtrip(test_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_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_res_rt() -> None: - im = roundtrip(test_card, num_resolutions=8, progression="RLCP") - assert_image_equal(im, test_card) - - -@pytest.mark.parametrize("num_resolutions", range(2, 6)) -def test_default_num_resolutions(num_resolutions: int) -> None: - d = 1 << (num_resolutions - 1) - im = test_card.resize((d - 1, d - 1)) - with pytest.raises(OSError): - roundtrip(im, num_resolutions=num_resolutions) - reloaded = roundtrip(im) - assert_image_equal(im, reloaded) - - -def test_reduce() -> None: - with Image.open("Tests/images/test-card-lossless.jp2") as im: - assert callable(im.reduce) - - im.reduce = 2 - assert im.reduce == 2 - - im.load() - assert im.size == (160, 120) - - im.thumbnail((40, 40)) - assert im.size == (40, 30) - - -def test_load_dpi() -> None: - with Image.open("Tests/images/test-card-lossless.jp2") as im: - assert im.info["dpi"] == (71.9836, 71.9836) - - with Image.open("Tests/images/zero_dpi.jp2") as im: - assert "dpi" not in im.info - - -def test_restricted_icc_profile() -> 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_header_errors() -> None: - for path in ( - "Tests/images/invalid_header_length.jp2", - "Tests/images/not_enough_data.jp2", - ): - with pytest.raises(UnidentifiedImageError): - with Image.open(path): - pass - - with pytest.raises(OSError): - with Image.open("Tests/images/expected_to_read.jp2"): - pass - - -def test_layers_type(tmp_path: Path) -> None: - outfile = str(tmp_path / "temp_layers.jp2") - for quality_layers in [[100, 50, 10], (100, 50, 10), None]: - test_card.save(outfile, quality_layers=quality_layers) - - for quality_layers_str in ["quality_layers", ("100", "50", "10")]: - with pytest.raises(ValueError): - test_card.save(outfile, quality_layers=quality_layers_str) - - -def test_layers() -> None: - out = BytesIO() - test_card.save(out, "JPEG2000", quality_layers=[100, 50, 10], progression="LRCP") - out.seek(0) - - with Image.open(out) as im: - im.layers = 1 - im.load() - assert_image_similar(im, test_card, 13) - - out.seek(0) - with Image.open(out) as im: - im.layers = 3 - im.load() - assert_image_similar(im, test_card, 0.4) - - -@pytest.mark.parametrize( - "name, args, offset, data", - ( - ("foo.j2k", {}, 0, b"\xff\x4f"), - ("foo.jp2", {}, 4, b"jP"), - (None, {"no_jp2": True}, 0, b"\xff\x4f"), - ("foo.j2k", {"no_jp2": True}, 0, b"\xff\x4f"), - ("foo.jp2", {"no_jp2": True}, 0, b"\xff\x4f"), - ("foo.j2k", {"no_jp2": False}, 0, b"\xff\x4f"), - ("foo.jp2", {"no_jp2": False}, 4, b"jP"), - ("foo.jp2", {"no_jp2": False}, 4, b"jP"), - ), -) -def test_no_jp2(name: str, args: dict[str, bool], offset: int, data: bytes) -> None: - out = BytesIO() - if name: - out.name = name - test_card.save(out, "JPEG2000", **args) - out.seek(offset) - assert out.read(2) == data - - -def test_mct() -> None: - # Three component - for val in (0, 1): - out = BytesIO() - test_card.save(out, "JPEG2000", mct=val, no_jp2=True) - - assert out.getvalue()[59] == val - with Image.open(out) as im: - assert_image_similar(im, test_card, 1.0e-3) - - # Single component should have MCT disabled - for val in (0, 1): - out = BytesIO() - with Image.open("Tests/images/16bit.cropped.jp2") as jp2: - jp2.save(out, "JPEG2000", mct=val, no_jp2=True) - - assert out.getvalue()[53] == 0 - with Image.open(out) as im: - assert_image_similar(im, jp2, 1.0e-3) - - -def test_sgnd(tmp_path: Path) -> None: - outfile = str(tmp_path / "temp.jp2") - - im = Image.new("L", (1, 1)) - im.save(outfile) - with Image.open(outfile) as reloaded: - assert reloaded.getpixel((0, 0)) == 0 - - im = Image.new("L", (1, 1)) - im.save(outfile, signed=True) - with Image.open(outfile) as reloaded_signed: - assert reloaded_signed.getpixel((0, 0)) == 128 - - -@pytest.mark.parametrize("ext", (".j2k", ".jp2")) -def test_rgba(ext: str) -> None: - # Arrange - with Image.open("Tests/images/rgb_trns_ycbc" + ext) as im: - # Act - im.load() - - # Assert - assert im.mode == "RGBA" - - -@pytest.mark.skipif( - not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" -) -@skip_unless_feature_version("jpg_2000", "2.5.1") -def test_cmyk() -> None: - with Image.open(f"{EXTRA_DIR}/issue205.jp2") as im: - assert im.mode == "CMYK" - assert im.getpixel((0, 0)) == (185, 134, 0, 0) - - -@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: - im.load() - assert im.mode == "I;16" - - -def test_16bit_monochrome_jp2_like_tiff() -> None: - with Image.open("Tests/images/16bit.cropped.tif") as tiff_16bit: - assert_image_similar_tofile(tiff_16bit, "Tests/images/16bit.cropped.jp2", 1e-3) - - -def test_16bit_monochrome_j2k_like_tiff() -> None: - with Image.open("Tests/images/16bit.cropped.tif") as tiff_16bit: - assert_image_similar_tofile(tiff_16bit, "Tests/images/16bit.cropped.j2k", 1e-3) - - -def test_16bit_j2k_roundtrips() -> None: - with Image.open("Tests/images/16bit.cropped.j2k") as j2k: - im = roundtrip(j2k) - assert_image_equal(im, j2k) - - -def test_16bit_jp2_roundtrips() -> None: - with Image.open("Tests/images/16bit.cropped.jp2") as jp2: - im = roundtrip(jp2) - assert_image_equal(im, jp2) - - -def test_issue_6194() -> None: - with Image.open("Tests/images/issue_6194.j2k") as im: - assert im.getpixel((5, 5)) == 31 - - -def test_unbound_local() -> None: - # prepatch, a malformed jp2 file could cause an UnboundLocalError exception. - with pytest.raises(OSError): - with Image.open("Tests/images/unbound_variable.jp2"): - pass - - -def test_parser_feed() -> None: - # Arrange - with open("Tests/images/test-card-lossless.jp2", "rb") as f: - data = f.read() - - # Act - p = ImageFile.Parser() - p.feed(data) - - # Assert - assert p.image is not None - assert p.image.size == (640, 480) - - -@pytest.mark.skipif( - not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" -) -@pytest.mark.parametrize("name", ("subsampling_1", "subsampling_2", "zoo1", "zoo2")) -def test_subsampling_decode(name: str) -> None: - test = f"{EXTRA_DIR}/{name}.jp2" - reference = f"{EXTRA_DIR}/{name}.ppm" - - with Image.open(test) as im: - epsilon = 3.0 # for YCbCr images - with Image.open(reference) as im2: - width, height = im2.size - if name[-1] == "2": - # RGB reference images are downscaled - epsilon = 3e-3 - width, height = width * 2, height * 2 - expected = im2.resize((width, height), Image.Resampling.NEAREST) - assert_image_similar(im, expected, epsilon) - - -@pytest.mark.skipif( - not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" -) -def test_pclr() -> None: - with Image.open(f"{EXTRA_DIR}/issue104_jpxstream.jp2") as im: - assert im.mode == "P" - assert len(im.palette.colors) == 256 - assert im.palette.colors[(255, 255, 255)] == 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" - - # 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 - - -def test_save_comment() -> None: - for comment in ("Created by Pillow", b"Created by Pillow"): - out = BytesIO() - test_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) - 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" ") - - -@pytest.mark.parametrize( - "test_file", - [ - "Tests/images/crash-4fb027452e6988530aa5dabee76eecacb3b79f8a.j2k", - "Tests/images/crash-7d4c83eb92150fb8f1653a697703ae06ae7c4998.j2k", - "Tests/images/crash-ccca68ff40171fdae983d924e127a721cab2bd50.j2k", - "Tests/images/crash-d2c93af851d3ab9a19e34503626368b2ecde9c03.j2k", - ], -) -def test_crashes(test_file: str) -> None: - with open(test_file, "rb") as f: - with Image.open(f) as im: - # Valgrind should not complain here - try: - im.load() - except OSError: - pass - - -@skip_unless_feature_version("jpg_2000", "2.4.0") -def test_plt_marker() -> None: - # Search the start of the codesteam for PLT - out = BytesIO() - test_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") - - jp2_boxid = _binary.i16be(marker) - if jp2_boxid == 0xFF4F: - # SOC has no length - continue - elif jp2_boxid == 0xFF58: - # PLT - return - elif jp2_boxid == 0xFF93: - pytest.fail("SOD without finding PLT first") - - hdr = out.read(2) - length = _binary.i16be(hdr) - out.seek(length - 2, os.SEEK_CUR) - - -def test_9bit(): - with Image.open("Tests/images/9bit.j2k") as im: - assert im.mode == "I;16" - assert im.size == (128, 128) +from __future__ import annotations + +import os +import re +from io import BytesIO +from pathlib import Path +from typing import Any + +import pytest + +from PIL import ( + Image, + ImageFile, + Jpeg2KImagePlugin, + UnidentifiedImageError, + _binary, + features, +) + +from .helper import ( + assert_image_equal, + assert_image_similar, + assert_image_similar_tofile, + skip_unless_feature, + skip_unless_feature_version, +) + +EXTRA_DIR = "Tests/images/jpeg2000" + +pytestmark = skip_unless_feature("jpg_2000") + +test_card = Image.open("Tests/images/test-card.png") +test_card.load() + +# OpenJPEG 2.0.0 outputs this debugging message sometimes; we should +# ignore it---it doesn't represent a test failure. +# 'Not enough memory to handle tile data' + + +def roundtrip(im: Image.Image, **options: Any) -> Image.Image: + out = BytesIO() + im.save(out, "JPEG2000", **options) + out.seek(0) + with Image.open(out) as im: + im.load() + return im + + +def test_sanity() -> None: + # Internal version number + version = features.version_codec("jpg_2000") + assert version is not None + assert re.search(r"\d+\.\d+\.\d+$", version) + + with Image.open("Tests/images/test-card-lossless.jp2") as im: + px = im.load() + assert px[0, 0] == (0, 0, 0) + assert im.mode == "RGB" + assert im.size == (640, 480) + assert im.format == "JPEG2000" + assert im.get_format_mimetype() == "image/jp2" + + +def test_jpf() -> None: + with Image.open("Tests/images/balloon.jpf") as im: + assert im.format == "JPEG2000" + assert im.get_format_mimetype() == "image/jpx" + + +def test_invalid_file() -> None: + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(SyntaxError): + Jpeg2KImagePlugin.Jpeg2KImageFile(invalid_file) + + +def test_bytesio() -> 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) + + +# 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: + with Image.open("Tests/images/test-card-lossless.jp2") as im: + im.load() + outfile = str(tmp_path / "temp_test-card.png") + im.save(outfile) + assert_image_similar(im, test_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_lossless_rt() -> None: + im = roundtrip(test_card) + assert_image_equal(im, test_card) + + +def test_lossy_rt() -> None: + im = roundtrip(test_card, quality_layers=[20]) + assert_image_similar(im, test_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_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_too_small() -> None: + with pytest.raises(ValueError): + roundtrip(test_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_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_res_rt() -> None: + im = roundtrip(test_card, num_resolutions=8, progression="RLCP") + assert_image_equal(im, test_card) + + +@pytest.mark.parametrize("num_resolutions", range(2, 6)) +def test_default_num_resolutions(num_resolutions: int) -> None: + d = 1 << (num_resolutions - 1) + im = test_card.resize((d - 1, d - 1)) + with pytest.raises(OSError): + roundtrip(im, num_resolutions=num_resolutions) + reloaded = roundtrip(im) + assert_image_equal(im, reloaded) + + +def test_reduce() -> None: + with Image.open("Tests/images/test-card-lossless.jp2") as im: + assert callable(im.reduce) + + im.reduce = 2 + assert im.reduce == 2 + + im.load() + assert im.size == (160, 120) + + im.thumbnail((40, 40)) + assert im.size == (40, 30) + + +def test_load_dpi() -> None: + with Image.open("Tests/images/test-card-lossless.jp2") as im: + assert im.info["dpi"] == (71.9836, 71.9836) + + with Image.open("Tests/images/zero_dpi.jp2") as im: + assert "dpi" not in im.info + + +def test_restricted_icc_profile() -> 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_header_errors() -> None: + for path in ( + "Tests/images/invalid_header_length.jp2", + "Tests/images/not_enough_data.jp2", + ): + with pytest.raises(UnidentifiedImageError): + with Image.open(path): + pass + + with pytest.raises(OSError): + with Image.open("Tests/images/expected_to_read.jp2"): + pass + + +def test_layers_type(tmp_path: Path) -> None: + outfile = str(tmp_path / "temp_layers.jp2") + for quality_layers in [[100, 50, 10], (100, 50, 10), None]: + test_card.save(outfile, quality_layers=quality_layers) + + for quality_layers_str in ["quality_layers", ("100", "50", "10")]: + with pytest.raises(ValueError): + test_card.save(outfile, quality_layers=quality_layers_str) + + +def test_layers() -> None: + out = BytesIO() + test_card.save(out, "JPEG2000", quality_layers=[100, 50, 10], progression="LRCP") + out.seek(0) + + with Image.open(out) as im: + im.layers = 1 + im.load() + assert_image_similar(im, test_card, 13) + + out.seek(0) + with Image.open(out) as im: + im.layers = 3 + im.load() + assert_image_similar(im, test_card, 0.4) + + +@pytest.mark.parametrize( + "name, args, offset, data", + ( + ("foo.j2k", {}, 0, b"\xff\x4f"), + ("foo.jp2", {}, 4, b"jP"), + (None, {"no_jp2": True}, 0, b"\xff\x4f"), + ("foo.j2k", {"no_jp2": True}, 0, b"\xff\x4f"), + ("foo.jp2", {"no_jp2": True}, 0, b"\xff\x4f"), + ("foo.j2k", {"no_jp2": False}, 0, b"\xff\x4f"), + ("foo.jp2", {"no_jp2": False}, 4, b"jP"), + ("foo.jp2", {"no_jp2": False}, 4, b"jP"), + ), +) +def test_no_jp2(name: str, args: dict[str, bool], offset: int, data: bytes) -> None: + out = BytesIO() + if name: + out.name = name + test_card.save(out, "JPEG2000", **args) + out.seek(offset) + assert out.read(2) == data + + +def test_mct() -> None: + # Three component + for val in (0, 1): + out = BytesIO() + test_card.save(out, "JPEG2000", mct=val, no_jp2=True) + + assert out.getvalue()[59] == val + with Image.open(out) as im: + assert_image_similar(im, test_card, 1.0e-3) + + # Single component should have MCT disabled + for val in (0, 1): + out = BytesIO() + with Image.open("Tests/images/16bit.cropped.jp2") as jp2: + jp2.save(out, "JPEG2000", mct=val, no_jp2=True) + + assert out.getvalue()[53] == 0 + with Image.open(out) as im: + assert_image_similar(im, jp2, 1.0e-3) + + +def test_sgnd(tmp_path: Path) -> None: + outfile = str(tmp_path / "temp.jp2") + + im = Image.new("L", (1, 1)) + im.save(outfile) + with Image.open(outfile) as reloaded: + assert reloaded.getpixel((0, 0)) == 0 + + im = Image.new("L", (1, 1)) + im.save(outfile, signed=True) + with Image.open(outfile) as reloaded_signed: + assert reloaded_signed.getpixel((0, 0)) == 128 + + +@pytest.mark.parametrize("ext", (".j2k", ".jp2")) +def test_rgba(ext: str) -> None: + # Arrange + with Image.open("Tests/images/rgb_trns_ycbc" + ext) as im: + # Act + im.load() + + # Assert + assert im.mode == "RGBA" + + +@pytest.mark.skipif( + not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" +) +@skip_unless_feature_version("jpg_2000", "2.5.1") +def test_cmyk() -> None: + with Image.open(f"{EXTRA_DIR}/issue205.jp2") as im: + assert im.mode == "CMYK" + assert im.getpixel((0, 0)) == (185, 134, 0, 0) + + +@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: + im.load() + assert im.mode == "I;16" + + +def test_16bit_monochrome_jp2_like_tiff() -> None: + with Image.open("Tests/images/16bit.cropped.tif") as tiff_16bit: + assert_image_similar_tofile(tiff_16bit, "Tests/images/16bit.cropped.jp2", 1e-3) + + +def test_16bit_monochrome_j2k_like_tiff() -> None: + with Image.open("Tests/images/16bit.cropped.tif") as tiff_16bit: + assert_image_similar_tofile(tiff_16bit, "Tests/images/16bit.cropped.j2k", 1e-3) + + +def test_16bit_j2k_roundtrips() -> None: + with Image.open("Tests/images/16bit.cropped.j2k") as j2k: + im = roundtrip(j2k) + assert_image_equal(im, j2k) + + +def test_16bit_jp2_roundtrips() -> None: + with Image.open("Tests/images/16bit.cropped.jp2") as jp2: + im = roundtrip(jp2) + assert_image_equal(im, jp2) + + +def test_issue_6194() -> None: + with Image.open("Tests/images/issue_6194.j2k") as im: + assert im.getpixel((5, 5)) == 31 + + +def test_unbound_local() -> None: + # prepatch, a malformed jp2 file could cause an UnboundLocalError exception. + with pytest.raises(OSError): + with Image.open("Tests/images/unbound_variable.jp2"): + pass + + +def test_parser_feed() -> None: + # Arrange + with open("Tests/images/test-card-lossless.jp2", "rb") as f: + data = f.read() + + # Act + p = ImageFile.Parser() + p.feed(data) + + # Assert + assert p.image is not None + assert p.image.size == (640, 480) + + +@pytest.mark.skipif( + not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" +) +@pytest.mark.parametrize("name", ("subsampling_1", "subsampling_2", "zoo1", "zoo2")) +def test_subsampling_decode(name: str) -> None: + test = f"{EXTRA_DIR}/{name}.jp2" + reference = f"{EXTRA_DIR}/{name}.ppm" + + with Image.open(test) as im: + epsilon = 3.0 # for YCbCr images + with Image.open(reference) as im2: + width, height = im2.size + if name[-1] == "2": + # RGB reference images are downscaled + epsilon = 3e-3 + width, height = width * 2, height * 2 + expected = im2.resize((width, height), Image.Resampling.NEAREST) + assert_image_similar(im, expected, epsilon) + + +@pytest.mark.skipif( + not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" +) +def test_pclr() -> None: + with Image.open(f"{EXTRA_DIR}/issue104_jpxstream.jp2") as im: + assert im.mode == "P" + assert len(im.palette.colors) == 256 + assert im.palette.colors[(255, 255, 255)] == 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" + + # 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 + + +def test_save_comment() -> None: + for comment in ("Created by Pillow", b"Created by Pillow"): + out = BytesIO() + test_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) + 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" ") + + +@pytest.mark.parametrize( + "test_file", + [ + "Tests/images/crash-4fb027452e6988530aa5dabee76eecacb3b79f8a.j2k", + "Tests/images/crash-7d4c83eb92150fb8f1653a697703ae06ae7c4998.j2k", + "Tests/images/crash-ccca68ff40171fdae983d924e127a721cab2bd50.j2k", + "Tests/images/crash-d2c93af851d3ab9a19e34503626368b2ecde9c03.j2k", + ], +) +def test_crashes(test_file: str) -> None: + with open(test_file, "rb") as f: + with Image.open(f) as im: + # Valgrind should not complain here + try: + im.load() + except OSError: + pass + + +@skip_unless_feature_version("jpg_2000", "2.4.0") +def test_plt_marker() -> None: + # Search the start of the codesteam for PLT + out = BytesIO() + test_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") + + jp2_boxid = _binary.i16be(marker) + if jp2_boxid == 0xFF4F: + # SOC has no length + continue + elif jp2_boxid == 0xFF58: + # PLT + return + elif jp2_boxid == 0xFF93: + pytest.fail("SOD without finding PLT first") + + hdr = out.read(2) + length = _binary.i16be(hdr) + out.seek(length - 2, os.SEEK_CUR) + + +def test_9bit(): + with Image.open("Tests/images/9bit.j2k") as im: + assert im.mode == "I;16" + assert im.size == (128, 128) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index fe9d017c0..b866c546f 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -1,1170 +1,1170 @@ -from __future__ import annotations - -import base64 -import io -import itertools -import os -import re -import sys -from pathlib import Path -from typing import Any, NamedTuple - -import pytest - -from PIL import Image, ImageFilter, ImageOps, TiffImagePlugin, TiffTags, features -from PIL.TiffImagePlugin import OSUBFILETYPE, SAMPLEFORMAT, STRIPOFFSETS, SUBIFD - -from .helper import ( - assert_image_equal, - assert_image_equal_tofile, - assert_image_similar, - assert_image_similar_tofile, - hopper, - mark_if_feature_version, - skip_unless_feature, -) - - -@skip_unless_feature("libtiff") -class LibTiffTestCase: - def _assert_noerr(self, tmp_path: Path, im: TiffImagePlugin.TiffImageFile) -> None: - """Helper tests that assert basic sanity about the g4 tiff reading""" - # 1 bit - assert im.mode == "1" - - # Does the data actually load - im.load() - im.getdata() - - try: - assert im._compression == "group4" - except AttributeError: - print("No _compression") - print(dir(im)) - - # can we write it back out, in a different form. - out = str(tmp_path / "temp.png") - im.save(out) - - out_bytes = io.BytesIO() - im.save(out_bytes, format="tiff", compression="group4") - - -class TestFileLibTiff(LibTiffTestCase): - def test_version(self) -> None: - version = features.version_codec("libtiff") - assert version is not None - assert re.search(r"\d+\.\d+\.\d+t?$", version) - - def test_g4_tiff(self, tmp_path: Path) -> None: - """Test the ordinary file path load path""" - - test_file = "Tests/images/hopper_g4_500.tif" - with Image.open(test_file) as im: - assert im.size == (500, 500) - self._assert_noerr(tmp_path, im) - - def test_g4_large(self, tmp_path: Path) -> None: - test_file = "Tests/images/pport_g4.tif" - with Image.open(test_file) as im: - self._assert_noerr(tmp_path, im) - - def test_g4_tiff_file(self, tmp_path: Path) -> None: - """Testing the string load path""" - - test_file = "Tests/images/hopper_g4_500.tif" - with open(test_file, "rb") as f: - with Image.open(f) as im: - assert im.size == (500, 500) - self._assert_noerr(tmp_path, im) - - def test_g4_tiff_bytesio(self, tmp_path: Path) -> None: - """Testing the stringio loading code path""" - test_file = "Tests/images/hopper_g4_500.tif" - s = io.BytesIO() - with open(test_file, "rb") as f: - s.write(f.read()) - s.seek(0) - with Image.open(s) as im: - assert im.size == (500, 500) - self._assert_noerr(tmp_path, im) - - def test_g4_non_disk_file_object(self, tmp_path: Path) -> None: - """Testing loading from non-disk non-BytesIO file object""" - test_file = "Tests/images/hopper_g4_500.tif" - s = io.BytesIO() - with open(test_file, "rb") as f: - s.write(f.read()) - s.seek(0) - r = io.BufferedReader(s) - with Image.open(r) as im: - assert im.size == (500, 500) - self._assert_noerr(tmp_path, im) - - def test_g4_eq_png(self) -> None: - """Checking that we're actually getting the data that we expect""" - with Image.open("Tests/images/hopper_bw_500.png") as png: - assert_image_equal_tofile(png, "Tests/images/hopper_g4_500.tif") - - # see https://github.com/python-pillow/Pillow/issues/279 - def test_g4_fillorder_eq_png(self) -> None: - """Checking that we're actually getting the data that we expect""" - with Image.open("Tests/images/g4-fillorder-test.tif") as g4: - assert_image_equal_tofile(g4, "Tests/images/g4-fillorder-test.png") - - def test_g4_write(self, tmp_path: Path) -> None: - """Checking to see that the saved image is the same as what we wrote""" - test_file = "Tests/images/hopper_g4_500.tif" - with Image.open(test_file) as orig: - out = str(tmp_path / "temp.tif") - rot = orig.transpose(Image.Transpose.ROTATE_90) - assert rot.size == (500, 500) - rot.save(out) - - with Image.open(out) as reread: - assert reread.size == (500, 500) - self._assert_noerr(tmp_path, reread) - assert_image_equal(reread, rot) - assert reread.info["compression"] == "group4" - - assert reread.info["compression"] == orig.info["compression"] - - assert orig.tobytes() != reread.tobytes() - - def test_adobe_deflate_tiff(self) -> None: - test_file = "Tests/images/tiff_adobe_deflate.tif" - with Image.open(test_file) as im: - assert im.mode == "RGB" - assert im.size == (278, 374) - assert im.tile[0][:3] == ("libtiff", (0, 0, 278, 374), 0) - im.load() - - assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") - - @pytest.mark.parametrize("legacy_api", (False, True)) - def test_write_metadata(self, legacy_api: bool, tmp_path: Path) -> None: - """Test metadata writing through libtiff""" - f = str(tmp_path / "temp.tiff") - with Image.open("Tests/images/hopper_g4.tif") as img: - img.save(f, tiffinfo=img.tag) - - if legacy_api: - original = img.tag.named() - else: - original = img.tag_v2.named() - - # PhotometricInterpretation is set from SAVE_INFO, - # not the original image. - ignored = [ - "StripByteCounts", - "RowsPerStrip", - "PageNumber", - "PhotometricInterpretation", - ] - - with Image.open(f) as loaded: - if legacy_api: - reloaded = loaded.tag.named() - else: - reloaded = loaded.tag_v2.named() - - for tag, value in itertools.chain(reloaded.items(), original.items()): - if tag not in ignored: - val = original[tag] - if tag.endswith("Resolution"): - if legacy_api: - assert val[0][0] / val[0][1] == ( - 4294967295 / 113653537 - ), f"{tag} didn't roundtrip" - else: - assert val == 37.79000115940079, f"{tag} didn't roundtrip" - else: - assert val == value, f"{tag} didn't roundtrip" - - # https://github.com/python-pillow/Pillow/issues/1561 - requested_fields = ["StripByteCounts", "RowsPerStrip", "StripOffsets"] - for field in requested_fields: - assert field in reloaded, f"{field} not in metadata" - - @pytest.mark.valgrind_known_error(reason="Known invalid metadata") - def test_additional_metadata( - self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path - ) -> None: - # these should not crash. Seriously dummy data, most of it doesn't make - # any sense, so we're running up against limits where we're asking - # libtiff to do stupid things. - - # Get the list of the ones that we should be able to write - - core_items = { - tag: info - for tag, info in ((s, TiffTags.lookup(s)) for s in TiffTags.LIBTIFF_CORE) - if info.type is not None - } - - # Exclude ones that have special meaning - # that we're already testing them - with Image.open("Tests/images/hopper_g4.tif") as im: - for tag in im.tag_v2: - try: - del core_items[tag] - except KeyError: - pass - del core_items[320] # colormap is special, tested below - - # Type codes: - # 2: "ascii", - # 3: "short", - # 4: "long", - # 5: "rational", - # 12: "double", - # Type: dummy value - values = { - 2: "test", - 3: 1, - 4: 2**20, - 5: TiffImagePlugin.IFDRational(100, 1), - 12: 1.05, - } - - new_ifd = TiffImagePlugin.ImageFileDirectory_v2() - for tag, info in core_items.items(): - if info.length == 1: - new_ifd[tag] = values[info.type] - if info.length == 0: - new_ifd[tag] = tuple(values[info.type] for _ in range(3)) - else: - new_ifd[tag] = tuple(values[info.type] for _ in range(info.length)) - - # Extra samples really doesn't make sense in this application. - del new_ifd[338] - - out = str(tmp_path / "temp.tif") - monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) - - im.save(out, tiffinfo=new_ifd) - - @pytest.mark.parametrize( - "libtiff", - ( - pytest.param( - True, - marks=pytest.mark.skipif( - not getattr(Image.core, "libtiff_support_custom_tags", False), - reason="Custom tags not supported by older libtiff", - ), - ), - False, - ), - ) - def test_custom_metadata( - self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path, libtiff: bool - ) -> None: - monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", libtiff) - - class Tc(NamedTuple): - value: Any - type: int - supported_by_default: bool - - custom = { - 37000 + k: v - for k, v in enumerate( - [ - Tc(4, TiffTags.SHORT, True), - Tc(123456789, TiffTags.LONG, True), - Tc(-4, TiffTags.SIGNED_BYTE, False), - Tc(-4, TiffTags.SIGNED_SHORT, False), - Tc(-123456789, TiffTags.SIGNED_LONG, False), - Tc(TiffImagePlugin.IFDRational(4, 7), TiffTags.RATIONAL, True), - Tc(4.25, TiffTags.FLOAT, True), - Tc(4.25, TiffTags.DOUBLE, True), - Tc("custom tag value", TiffTags.ASCII, True), - Tc(b"custom tag value", TiffTags.BYTE, True), - Tc((4, 5, 6), TiffTags.SHORT, True), - Tc((123456789, 9, 34, 234, 219387, 92432323), TiffTags.LONG, True), - Tc((-4, 9, 10), TiffTags.SIGNED_BYTE, False), - Tc((-4, 5, 6), TiffTags.SIGNED_SHORT, False), - Tc( - (-123456789, 9, 34, 234, 219387, -92432323), - TiffTags.SIGNED_LONG, - False, - ), - Tc((4.25, 5.25), TiffTags.FLOAT, True), - Tc((4.25, 5.25), TiffTags.DOUBLE, True), - # array of TIFF_BYTE requires bytes instead of tuple for backwards - # compatibility - Tc(bytes([4]), TiffTags.BYTE, True), - Tc(bytes((4, 9, 10)), TiffTags.BYTE, True), - ] - ) - } - - def check_tags( - tiffinfo: TiffImagePlugin.ImageFileDirectory_v2 | dict[int, str] - ) -> None: - im = hopper() - - out = str(tmp_path / "temp.tif") - im.save(out, tiffinfo=tiffinfo) - - with Image.open(out) as reloaded: - for tag, value in tiffinfo.items(): - reloaded_value = reloaded.tag_v2[tag] - if ( - isinstance(reloaded_value, TiffImagePlugin.IFDRational) - and libtiff - ): - # libtiff does not support real RATIONALS - assert round(abs(float(reloaded_value) - float(value)), 7) == 0 - continue - - assert reloaded_value == value - - # Test with types - ifd = TiffImagePlugin.ImageFileDirectory_v2() - for tag, tagdata in custom.items(): - ifd[tag] = tagdata.value - ifd.tagtype[tag] = tagdata.type - check_tags(ifd) - - # Test without types. This only works for some types, int for example are - # always encoded as LONG and not SIGNED_LONG. - check_tags( - { - tag: tagdata.value - for tag, tagdata in custom.items() - if tagdata.supported_by_default - } - ) - - def test_osubfiletype(self, tmp_path: Path) -> None: - outfile = str(tmp_path / "temp.tif") - with Image.open("Tests/images/g4_orientation_6.tif") as im: - im.tag_v2[OSUBFILETYPE] = 1 - im.save(outfile) - - def test_subifd(self, tmp_path: Path) -> None: - outfile = str(tmp_path / "temp.tif") - with Image.open("Tests/images/g4_orientation_6.tif") as im: - im.tag_v2[SUBIFD] = 10000 - - # Should not segfault - im.save(outfile) - - def test_xmlpacket_tag( - self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path - ) -> None: - monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) - - out = str(tmp_path / "temp.tif") - hopper().save(out, tiffinfo={700: b"xmlpacket tag"}) - - with Image.open(out) as reloaded: - if 700 in reloaded.tag_v2: - assert reloaded.tag_v2[700] == b"xmlpacket tag" - - def test_int_dpi(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: - # issue #1765 - im = hopper("RGB") - out = str(tmp_path / "temp.tif") - monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) - im.save(out, dpi=(72, 72)) - with Image.open(out) as reloaded: - assert reloaded.info["dpi"] == (72.0, 72.0) - - 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") - i.save(out, compression="group3") - - with Image.open(out) as reread: - assert reread.info["compression"] == "group3" - assert_image_equal(reread, i) - - def test_little_endian(self, tmp_path: Path) -> None: - with Image.open("Tests/images/16bit.deflate.tif") as im: - assert im.getpixel((0, 0)) == 480 - assert im.mode == "I;16" - - b = im.tobytes() - # Bytes are in image native order (little endian) - assert b[0] == ord(b"\xe0") - assert b[1] == ord(b"\x01") - - out = str(tmp_path / "temp.tif") - # out = "temp.le.tif" - im.save(out) - with Image.open(out) as reread: - assert reread.info["compression"] == im.info["compression"] - assert reread.getpixel((0, 0)) == 480 - # UNDONE - libtiff defaults to writing in native endian, so - # on big endian, we'll get back mode = 'I;16B' here. - - def test_big_endian(self, tmp_path: Path) -> None: - with Image.open("Tests/images/16bit.MM.deflate.tif") as im: - assert im.getpixel((0, 0)) == 480 - assert im.mode == "I;16B" - - b = im.tobytes() - - # Bytes are in image native order (big endian) - assert b[0] == ord(b"\x01") - assert b[1] == ord(b"\xe0") - - out = str(tmp_path / "temp.tif") - im.save(out) - with Image.open(out) as reread: - assert reread.info["compression"] == im.info["compression"] - assert reread.getpixel((0, 0)) == 480 - - def test_g4_string_info(self, tmp_path: Path) -> None: - """Tests String data in info directory""" - test_file = "Tests/images/hopper_g4_500.tif" - with Image.open(test_file) as orig: - out = str(tmp_path / "temp.tif") - - orig.tag[269] = "temp.tif" - orig.save(out) - - with Image.open(out) as reread: - assert "temp.tif" == reread.tag_v2[269] - assert "temp.tif" == reread.tag[269][0] - - def test_12bit_rawmode(self, monkeypatch: pytest.MonkeyPatch) -> None: - """Are we generating the same interpretation - of the image as Imagemagick is?""" - monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True) - with Image.open("Tests/images/12bit.cropped.tif") as im: - im.load() - monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", False) - # to make the target -- - # convert 12bit.cropped.tif -depth 16 tmp.tif - # convert tmp.tif -evaluate RightShift 4 12in16bit2.tif - # imagemagick will auto scale so that a 12bit FFF is 16bit FFF0, - # so we need to unshift so that the integer values are the same. - - assert_image_equal_tofile(im, "Tests/images/12in16bit.tif") - - 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") - with Image.open("Tests/images/pport_g4.tif") as im: - im = im.convert("L") - - im = im.filter(ImageFilter.GaussianBlur(4)) - im.save(out, compression="tiff_adobe_deflate") - - assert_image_equal_tofile(im, out) - - def test_compressions(self, tmp_path: Path) -> None: - # Test various tiff compressions and assert similar image content but reduced - # file sizes. - im = hopper("RGB") - out = str(tmp_path / "temp.tif") - im.save(out) - size_raw = os.path.getsize(out) - - for compression in ("packbits", "tiff_lzw"): - im.save(out, compression=compression) - size_compressed = os.path.getsize(out) - assert_image_equal_tofile(im, out) - - im.save(out, compression="jpeg") - size_jpeg = os.path.getsize(out) - with Image.open(out) as im2: - assert_image_similar(im, im2, 30) - - im.save(out, compression="jpeg", quality=30) - size_jpeg_30 = os.path.getsize(out) - assert_image_similar_tofile(im2, out, 30) - - assert size_raw > size_compressed - assert size_compressed > size_jpeg - assert size_jpeg > size_jpeg_30 - - def test_tiff_jpeg_compression(self, tmp_path: Path) -> None: - im = hopper("RGB") - out = str(tmp_path / "temp.tif") - im.save(out, compression="tiff_jpeg") - - with Image.open(out) as reloaded: - assert reloaded.info["compression"] == "jpeg" - - def test_tiff_deflate_compression(self, tmp_path: Path) -> None: - im = hopper("RGB") - out = str(tmp_path / "temp.tif") - im.save(out, compression="tiff_deflate") - - with Image.open(out) as reloaded: - assert reloaded.info["compression"] == "tiff_adobe_deflate" - - def test_quality(self, tmp_path: Path) -> None: - im = hopper("RGB") - out = str(tmp_path / "temp.tif") - - with pytest.raises(ValueError): - im.save(out, compression="tiff_lzw", quality=50) - with pytest.raises(ValueError): - im.save(out, compression="jpeg", quality=-1) - with pytest.raises(ValueError): - im.save(out, compression="jpeg", quality=101) - with pytest.raises(ValueError): - im.save(out, compression="jpeg", quality="good") - im.save(out, compression="jpeg", quality=0) - im.save(out, compression="jpeg", quality=100) - - def test_cmyk_save(self, tmp_path: Path) -> None: - im = hopper("CMYK") - out = str(tmp_path / "temp.tif") - - im.save(out, compression="tiff_adobe_deflate") - assert_image_equal_tofile(im, out) - - @pytest.mark.parametrize("im", (hopper("P"), Image.new("P", (1, 1), "#000"))) - def test_palette_save( - self, im: Image.Image, monkeypatch: pytest.MonkeyPatch, tmp_path: Path - ) -> None: - out = str(tmp_path / "temp.tif") - - monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) - im.save(out) - - with Image.open(out) as reloaded: - # colormap/palette tag - 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") - - with pytest.raises(OSError): - im.save(out, compression=compression) - - def test_fp_leak(self) -> None: - im: Image.Image | None = Image.open("Tests/images/hopper_g4_500.tif") - assert im is not None - fn = im.fp.fileno() - - os.fstat(fn) - im.load() # this should close it. - with pytest.raises(OSError): - os.fstat(fn) - im = None # this should force even more closed. - with pytest.raises(OSError): - os.fstat(fn) - with pytest.raises(OSError): - os.close(fn) - - def test_multipage(self, monkeypatch: pytest.MonkeyPatch) -> None: - # issue #862 - monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True) - with Image.open("Tests/images/multipage.tiff") as im: - # file is a multipage tiff, 10x10 green, 10x10 red, 20x20 blue - - im.seek(0) - assert im.size == (10, 10) - assert im.convert("RGB").getpixel((0, 0)) == (0, 128, 0) - assert im.tag.next - - im.seek(1) - assert im.size == (10, 10) - assert im.convert("RGB").getpixel((0, 0)) == (255, 0, 0) - assert im.tag.next - - im.seek(2) - assert not im.tag.next - assert im.size == (20, 20) - assert im.convert("RGB").getpixel((0, 0)) == (0, 0, 255) - - def test_multipage_nframes(self, monkeypatch: pytest.MonkeyPatch) -> None: - # issue #862 - monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True) - with Image.open("Tests/images/multipage.tiff") as im: - frames = im.n_frames - assert frames == 3 - for _ in range(frames): - im.seek(0) - # Should not raise ValueError: I/O operation on closed file - im.load() - - def test_multipage_seek_backwards(self, monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True) - with Image.open("Tests/images/multipage.tiff") as im: - im.seek(1) - im.load() - - im.seek(0) - assert im.convert("RGB").getpixel((0, 0)) == (0, 128, 0) - - def test__next(self, monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True) - with Image.open("Tests/images/hopper.tif") as im: - assert not im.tag.next - im.load() - assert not im.tag.next - - def test_4bit(self, monkeypatch: pytest.MonkeyPatch) -> None: - # Arrange - test_file = "Tests/images/hopper_gray_4bpp.tif" - original = hopper("L") - - # Act - monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True) - with Image.open(test_file) as im: - - # Assert - assert im.size == (128, 128) - assert im.mode == "L" - assert_image_similar(im, original, 7.3) - - def test_gray_semibyte_per_pixel(self) -> None: - test_files = ( - ( - 24.8, # epsilon - ( # group - "Tests/images/tiff_gray_2_4_bpp/hopper2.tif", - "Tests/images/tiff_gray_2_4_bpp/hopper2I.tif", - "Tests/images/tiff_gray_2_4_bpp/hopper2R.tif", - "Tests/images/tiff_gray_2_4_bpp/hopper2IR.tif", - ), - ), - ( - 7.3, # epsilon - ( # group - "Tests/images/tiff_gray_2_4_bpp/hopper4.tif", - "Tests/images/tiff_gray_2_4_bpp/hopper4I.tif", - "Tests/images/tiff_gray_2_4_bpp/hopper4R.tif", - "Tests/images/tiff_gray_2_4_bpp/hopper4IR.tif", - ), - ), - ) - original = hopper("L") - for epsilon, group in test_files: - with Image.open(group[0]) as im: - assert im.size == (128, 128) - assert im.mode == "L" - assert_image_similar(im, original, epsilon) - for file in group[1:]: - with Image.open(file) as im2: - assert im2.size == (128, 128) - assert im2.mode == "L" - assert_image_equal(im, im2) - - def test_save_bytesio(self, monkeypatch: pytest.MonkeyPatch) -> None: - # PR 1011 - # Test TIFF saving to io.BytesIO() object. - - monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) - monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True) - - # Generate test image - pilim = hopper() - - def save_bytesio(compression: str | None = None) -> None: - buffer_io = io.BytesIO() - pilim.save(buffer_io, format="tiff", compression=compression) - buffer_io.seek(0) - - with Image.open(buffer_io) as saved_im: - assert_image_similar(pilim, saved_im, 0) - - save_bytesio() - save_bytesio("raw") - save_bytesio("packbits") - save_bytesio("tiff_lzw") - - def test_save_ycbcr(self, tmp_path: Path) -> None: - im = hopper("YCbCr") - outfile = str(tmp_path / "temp.tif") - im.save(outfile, compression="jpeg") - - with Image.open(outfile) as reloaded: - assert reloaded.tag_v2[530] == (1, 1) - assert reloaded.tag_v2[532] == (0, 255, 128, 255, 128, 255) - - def test_exif_ifd(self, tmp_path: Path) -> None: - outfile = str(tmp_path / "temp.tif") - with Image.open("Tests/images/tiff_adobe_deflate.tif") as im: - assert im.tag_v2[34665] == 125456 - im.save(outfile) - - with Image.open(outfile) as reloaded: - if Image.core.libtiff_support_custom_tags: - assert reloaded.tag_v2[34665] == 125456 - - def test_crashing_metadata( - self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path - ) -> None: - # issue 1597 - with Image.open("Tests/images/rdf.tif") as im: - out = str(tmp_path / "temp.tif") - - monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) - # this shouldn't crash - im.save(out, format="TIFF") - - def test_page_number_x_0(self, tmp_path: Path) -> None: - # Issue 973 - # Test TIFF with tag 297 (Page Number) having value of 0 0. - # The first number is the current page number. - # The second is the total number of pages, zero means not available. - outfile = str(tmp_path / "temp.tif") - # Created by printing a page in Chrome to PDF, then: - # /usr/bin/gs -q -sDEVICE=tiffg3 -sOutputFile=total-pages-zero.tif - # -dNOPAUSE /tmp/test.pdf -c quit - infile = "Tests/images/total-pages-zero.tif" - with Image.open(infile) as im: - # Should not divide by zero - im.save(outfile) - - def test_fd_duplication(self, tmp_path: Path) -> None: - # https://github.com/python-pillow/Pillow/issues/1651 - - tmpfile = str(tmp_path / "temp.tif") - with open(tmpfile, "wb") as f: - with open("Tests/images/g4-multi.tiff", "rb") as src: - f.write(src.read()) - - im = Image.open(tmpfile) - assert isinstance(im, TiffImagePlugin.TiffImageFile) - im.n_frames - im.close() - # Should not raise PermissionError. - os.remove(tmpfile) - - def test_read_icc(self, monkeypatch: pytest.MonkeyPatch) -> None: - with Image.open("Tests/images/hopper.iccprofile.tif") as img: - icc = img.info.get("icc_profile") - assert icc is not None - monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True) - with Image.open("Tests/images/hopper.iccprofile.tif") as img: - icc_libtiff = img.info.get("icc_profile") - assert icc_libtiff is not None - assert icc == icc_libtiff - - @pytest.mark.parametrize( - "libtiff", - ( - pytest.param( - True, - marks=pytest.mark.skipif( - not getattr(Image.core, "libtiff_support_custom_tags", False), - reason="Custom tags not supported by older libtiff", - ), - ), - False, - ), - ) - def test_write_icc( - self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path, libtiff: bool - ) -> None: - monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", libtiff) - - with Image.open("Tests/images/hopper.iccprofile.tif") as img: - icc_profile = img.info["icc_profile"] - - out = str(tmp_path / "temp.tif") - img.save(out, icc_profile=icc_profile) - with Image.open(out) as reloaded: - assert icc_profile == reloaded.info["icc_profile"] - - def test_multipage_compression(self) -> None: - with Image.open("Tests/images/compression.tif") as im: - im.seek(0) - assert im._compression == "tiff_ccitt" - assert im.size == (10, 10) - - im.seek(1) - assert im._compression == "packbits" - assert im.size == (10, 10) - im.load() - - im.seek(0) - assert im._compression == "tiff_ccitt" - assert im.size == (10, 10) - im.load() - - def test_save_tiff_with_jpegtables(self, tmp_path: Path) -> None: - # Arrange - outfile = str(tmp_path / "temp.tif") - - # Created with ImageMagick: convert hopper.jpg hopper_jpg.tif - # Contains JPEGTables (347) tag - infile = "Tests/images/hopper_jpg.tif" - with Image.open(infile) as im: - # Act / Assert - # Should not raise UnicodeDecodeError or anything else - im.save(outfile) - - def test_16bit_RGB_tiff(self) -> None: - with Image.open("Tests/images/tiff_16bit_RGB.tiff") as im: - assert im.mode == "RGB" - assert im.size == (100, 40) - assert im.tile, [ - ( - "libtiff", - (0, 0, 100, 40), - 0, - ("RGB;16N", "tiff_adobe_deflate", False, 8), - ) - ] - im.load() - - assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGB_target.png") - - def test_16bit_RGBa_tiff(self) -> None: - with Image.open("Tests/images/tiff_16bit_RGBa.tiff") as im: - assert im.mode == "RGBA" - assert im.size == (100, 40) - assert im.tile, [ - ("libtiff", (0, 0, 100, 40), 0, ("RGBa;16N", "tiff_lzw", False, 38236)) - ] - im.load() - - assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGBa_target.png") - - @skip_unless_feature("jpg") - def test_gimp_tiff(self) -> None: - # Read TIFF JPEG images from GIMP [@PIL168] - filename = "Tests/images/pil168.tif" - with Image.open(filename) as im: - assert im.mode == "RGB" - assert im.size == (256, 256) - assert im.tile == [ - ("libtiff", (0, 0, 256, 256), 0, ("RGB", "jpeg", False, 5122)) - ] - im.load() - - assert_image_equal_tofile(im, "Tests/images/pil168.png") - - def test_sampleformat(self) -> None: - # https://github.com/python-pillow/Pillow/issues/1466 - with Image.open("Tests/images/copyleft.tiff") as im: - assert im.mode == "RGB" - - assert_image_equal_tofile(im, "Tests/images/copyleft.png", mode="RGB") - - def test_sampleformat_write( - self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path - ) -> None: - im = Image.new("F", (1, 1)) - out = str(tmp_path / "temp.tif") - monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) - im.save(out) - - with Image.open(out) as reloaded: - assert reloaded.mode == "F" - assert reloaded.getexif()[SAMPLEFORMAT] == 3 - - def test_lzma(self, capfd: pytest.CaptureFixture[str]) -> None: - try: - with Image.open("Tests/images/hopper_lzma.tif") as im: - assert im.mode == "RGB" - assert im.size == (128, 128) - assert im.format == "TIFF" - im2 = hopper() - assert_image_similar(im, im2, 5) - except OSError: - captured = capfd.readouterr() - if "LZMA compression support is not configured" in captured.err: - pytest.skip("LZMA compression support is not configured") - sys.stdout.write(captured.out) - sys.stderr.write(captured.err) - raise - - def test_webp(self, capfd: pytest.CaptureFixture[str]) -> None: - try: - with Image.open("Tests/images/hopper_webp.tif") as im: - assert im.mode == "RGB" - assert im.size == (128, 128) - assert im.format == "TIFF" - assert_image_similar_tofile(im, "Tests/images/hopper_webp.png", 1) - except OSError: - captured = capfd.readouterr() - if "WEBP compression support is not configured" in captured.err: - pytest.skip("WEBP compression support is not configured") - if ( - "Compression scheme 50001 strip decoding is not implemented" - in captured.err - ): - pytest.skip( - "Compression scheme 50001 strip decoding is not implemented" - ) - sys.stdout.write(captured.out) - sys.stderr.write(captured.err) - raise - - def test_lzw(self) -> None: - with Image.open("Tests/images/hopper_lzw.tif") as im: - assert im.mode == "RGB" - assert im.size == (128, 128) - assert im.format == "TIFF" - im2 = hopper() - assert_image_similar(im, im2, 5) - - def test_strip_cmyk_jpeg(self) -> None: - infile = "Tests/images/tiff_strip_cmyk_jpeg.tif" - with Image.open(infile) as im: - assert_image_similar_tofile(im, "Tests/images/pil_sample_cmyk.jpg", 0.5) - - def test_strip_cmyk_16l_jpeg(self) -> None: - infile = "Tests/images/tiff_strip_cmyk_16l_jpeg.tif" - with Image.open(infile) as im: - assert_image_similar_tofile(im, "Tests/images/pil_sample_cmyk.jpg", 0.5) - - @mark_if_feature_version( - pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" - ) - def test_strip_ycbcr_jpeg_2x2_sampling(self) -> None: - infile = "Tests/images/tiff_strip_ycbcr_jpeg_2x2_sampling.tif" - with Image.open(infile) as im: - assert_image_similar_tofile(im, "Tests/images/flower.jpg", 1.2) - - @mark_if_feature_version( - pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" - ) - def test_strip_ycbcr_jpeg_1x1_sampling(self) -> None: - infile = "Tests/images/tiff_strip_ycbcr_jpeg_1x1_sampling.tif" - with Image.open(infile) as im: - assert_image_similar_tofile(im, "Tests/images/flower2.jpg", 0.01) - - def test_tiled_cmyk_jpeg(self) -> None: - infile = "Tests/images/tiff_tiled_cmyk_jpeg.tif" - with Image.open(infile) as im: - assert_image_similar_tofile(im, "Tests/images/pil_sample_cmyk.jpg", 0.5) - - @mark_if_feature_version( - pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" - ) - def test_tiled_ycbcr_jpeg_1x1_sampling(self) -> None: - infile = "Tests/images/tiff_tiled_ycbcr_jpeg_1x1_sampling.tif" - with Image.open(infile) as im: - assert_image_similar_tofile(im, "Tests/images/flower2.jpg", 0.01) - - @mark_if_feature_version( - pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" - ) - def test_tiled_ycbcr_jpeg_2x2_sampling(self) -> None: - infile = "Tests/images/tiff_tiled_ycbcr_jpeg_2x2_sampling.tif" - with Image.open(infile) as im: - assert_image_similar_tofile(im, "Tests/images/flower.jpg", 1.5) - - def test_strip_planar_rgb(self) -> None: - # gdal_translate -co TILED=no -co INTERLEAVE=BAND -co COMPRESS=LZW \ - # tiff_strip_raw.tif tiff_strip_planar_lzw.tiff - infile = "Tests/images/tiff_strip_planar_lzw.tiff" - with Image.open(infile) as im: - assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") - - def test_tiled_planar_rgb(self) -> None: - # gdal_translate -co TILED=yes -co INTERLEAVE=BAND -co COMPRESS=LZW \ - # tiff_tiled_raw.tif tiff_tiled_planar_lzw.tiff - infile = "Tests/images/tiff_tiled_planar_lzw.tiff" - with Image.open(infile) as im: - assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") - - def test_tiled_planar_16bit_RGB(self) -> None: - # gdal_translate -co TILED=yes -co INTERLEAVE=BAND -co COMPRESS=LZW \ - # tiff_16bit_RGB.tiff tiff_tiled_planar_16bit_RGB.tiff - with Image.open("Tests/images/tiff_tiled_planar_16bit_RGB.tiff") as im: - assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGB_target.png") - - def test_strip_planar_16bit_RGB(self) -> None: - # gdal_translate -co TILED=no -co INTERLEAVE=BAND -co COMPRESS=LZW \ - # tiff_16bit_RGB.tiff tiff_strip_planar_16bit_RGB.tiff - with Image.open("Tests/images/tiff_strip_planar_16bit_RGB.tiff") as im: - assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGB_target.png") - - def test_tiled_planar_16bit_RGBa(self) -> None: - # gdal_translate -co TILED=yes \ - # -co INTERLEAVE=BAND -co COMPRESS=LZW -co ALPHA=PREMULTIPLIED \ - # tiff_16bit_RGBa.tiff tiff_tiled_planar_16bit_RGBa.tiff - with Image.open("Tests/images/tiff_tiled_planar_16bit_RGBa.tiff") as im: - assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGBa_target.png") - - def test_strip_planar_16bit_RGBa(self) -> None: - # gdal_translate -co TILED=no \ - # -co INTERLEAVE=BAND -co COMPRESS=LZW -co ALPHA=PREMULTIPLIED \ - # tiff_16bit_RGBa.tiff tiff_strip_planar_16bit_RGBa.tiff - with Image.open("Tests/images/tiff_strip_planar_16bit_RGBa.tiff") as im: - assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGBa_target.png") - - @pytest.mark.parametrize("compression", (None, "jpeg")) - def test_block_tile_tags(self, compression: str | None, tmp_path: Path) -> None: - im = hopper() - out = str(tmp_path / "temp.tif") - - tags = { - TiffImagePlugin.TILEWIDTH: 256, - TiffImagePlugin.TILELENGTH: 256, - TiffImagePlugin.TILEOFFSETS: 256, - TiffImagePlugin.TILEBYTECOUNTS: 256, - } - im.save(out, exif=tags, compression=compression) - - with Image.open(out) as reloaded: - for tag in tags: - assert tag not in reloaded.getexif() - - def test_old_style_jpeg(self) -> None: - with Image.open("Tests/images/old-style-jpeg-compression.tif") as im: - assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png") - - def test_open_missing_samplesperpixel(self) -> None: - with Image.open( - "Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif" - ) as im: - assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png") - - @pytest.mark.parametrize( - "file_name, mode, size, tile", - [ - ( - "tiff_wrong_bits_per_sample.tiff", - "RGBA", - (52, 53), - [("raw", (0, 0, 52, 53), 160, ("RGBA", 0, 1))], - ), - ( - "tiff_wrong_bits_per_sample_2.tiff", - "RGB", - (16, 16), - [("raw", (0, 0, 16, 16), 8, ("RGB", 0, 1))], - ), - ( - "tiff_wrong_bits_per_sample_3.tiff", - "RGBA", - (512, 256), - [("libtiff", (0, 0, 512, 256), 0, ("RGBA", "tiff_lzw", False, 48782))], - ), - ], - ) - def test_wrong_bits_per_sample( - self, file_name: str, mode: str, size: tuple[int, int], tile - ) -> None: - with Image.open("Tests/images/" + file_name) as im: - assert im.mode == mode - assert im.size == size - assert im.tile == tile - im.load() - - def test_no_rows_per_strip(self) -> None: - # This image does not have a RowsPerStrip TIFF tag - infile = "Tests/images/no_rows_per_strip.tif" - with Image.open(infile) as im: - im.load() - assert im.size == (950, 975) - - def test_orientation(self) -> None: - with Image.open("Tests/images/g4_orientation_1.tif") as base_im: - for i in range(2, 9): - with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im: - assert 274 in im.tag_v2 - - im.load() - assert 274 not in im.tag_v2 - - assert_image_similar(base_im, im, 0.7) - - def test_exif_transpose(self) -> None: - with Image.open("Tests/images/g4_orientation_1.tif") as base_im: - for i in range(2, 9): - with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im: - im = ImageOps.exif_transpose(im) - - assert_image_similar(base_im, im, 0.7) - - @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 - # when saving to a new file. - # Pillow 6.0 fails with "OSError: cannot identify image file". - tiff = io.BytesIO( - base64.b64decode( - b"SUkqAAgAAAAPAP4ABAABAAAAAAAAAAABBAABAAAAAQAAAAEBBAABAAAAAQAA" - b"AAIBAwADAAAAwgAAAAMBAwABAAAACAAAAAYBAwABAAAAAgAAABEBBAABAAAA" - b"4AAAABUBAwABAAAAAwAAABYBBAABAAAAAQAAABcBBAABAAAACwAAABoBBQAB" - b"AAAAyAAAABsBBQABAAAA0AAAABwBAwABAAAAAQAAACgBAwABAAAAAQAAAFMB" - b"AwADAAAA2AAAAAAAAAAIAAgACAABAAAAAQAAAAEAAAABAAAAAQABAAEAAAB4" - b"nGNgYAAAAAMAAQ==" - ) - ) - out = io.BytesIO() - with Image.open(tiff) as im: - im.save(out, format="tiff") - out.seek(0) - with Image.open(out) as im: - im.load() - - def test_realloc_overflow(self, 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" - - @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") - im.save(out, compression=compression) - - with Image.open(out) as im: - # Assert that there are multiple strips - assert isinstance(im, TiffImagePlugin.TiffImageFile) - 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: - im = hopper("RGB").resize((256, 256)) - out = str(tmp_path / "temp.tif") - - if not argument: - TiffImagePlugin.STRIP_SIZE = 2**18 - try: - arguments: dict[str, str | int] = {"compression": "tiff_adobe_deflate"} - if argument: - arguments["strip_size"] = 2**18 - im.save(out, **arguments) - - with Image.open(out) as im: - assert isinstance(im, TiffImagePlugin.TiffImageFile) - assert len(im.tag_v2[STRIPOFFSETS]) == 1 - finally: - TiffImagePlugin.STRIP_SIZE = 65536 - - @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", None)) - def test_save_zero(self, compression: str | None, tmp_path: Path) -> None: - im = Image.new("RGB", (0, 0)) - out = str(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") - for _ in range(10000): - im.save(out, compression="jpeg") - - @pytest.mark.parametrize( - "path, sizes", - ( - ("Tests/images/hopper.tif", ()), - ("Tests/images/child_ifd.tiff", (16, 8)), - ("Tests/images/child_ifd_jpeg.tiff", (20,)), - ), - ) - def test_get_child_images(self, path: str, sizes: tuple[int, ...]) -> None: - with Image.open(path) as im: - ims = im.get_child_images() - - assert len(ims) == len(sizes) - for i, im in enumerate(ims): - w = sizes[i] - expected = Image.new("RGB", (w, w), "#f00") - assert_image_similar(im, expected, 1) +from __future__ import annotations + +import base64 +import io +import itertools +import os +import re +import sys +from pathlib import Path +from typing import Any, NamedTuple + +import pytest + +from PIL import Image, ImageFilter, ImageOps, TiffImagePlugin, TiffTags, features +from PIL.TiffImagePlugin import OSUBFILETYPE, SAMPLEFORMAT, STRIPOFFSETS, SUBIFD + +from .helper import ( + assert_image_equal, + assert_image_equal_tofile, + assert_image_similar, + assert_image_similar_tofile, + hopper, + mark_if_feature_version, + skip_unless_feature, +) + + +@skip_unless_feature("libtiff") +class LibTiffTestCase: + def _assert_noerr(self, tmp_path: Path, im: TiffImagePlugin.TiffImageFile) -> None: + """Helper tests that assert basic sanity about the g4 tiff reading""" + # 1 bit + assert im.mode == "1" + + # Does the data actually load + im.load() + im.getdata() + + try: + assert im._compression == "group4" + except AttributeError: + print("No _compression") + print(dir(im)) + + # can we write it back out, in a different form. + out = str(tmp_path / "temp.png") + im.save(out) + + out_bytes = io.BytesIO() + im.save(out_bytes, format="tiff", compression="group4") + + +class TestFileLibTiff(LibTiffTestCase): + def test_version(self) -> None: + version = features.version_codec("libtiff") + assert version is not None + assert re.search(r"\d+\.\d+\.\d+t?$", version) + + def test_g4_tiff(self, tmp_path: Path) -> None: + """Test the ordinary file path load path""" + + test_file = "Tests/images/hopper_g4_500.tif" + with Image.open(test_file) as im: + assert im.size == (500, 500) + self._assert_noerr(tmp_path, im) + + def test_g4_large(self, tmp_path: Path) -> None: + test_file = "Tests/images/pport_g4.tif" + with Image.open(test_file) as im: + self._assert_noerr(tmp_path, im) + + def test_g4_tiff_file(self, tmp_path: Path) -> None: + """Testing the string load path""" + + test_file = "Tests/images/hopper_g4_500.tif" + with open(test_file, "rb") as f: + with Image.open(f) as im: + assert im.size == (500, 500) + self._assert_noerr(tmp_path, im) + + def test_g4_tiff_bytesio(self, tmp_path: Path) -> None: + """Testing the stringio loading code path""" + test_file = "Tests/images/hopper_g4_500.tif" + s = io.BytesIO() + with open(test_file, "rb") as f: + s.write(f.read()) + s.seek(0) + with Image.open(s) as im: + assert im.size == (500, 500) + self._assert_noerr(tmp_path, im) + + def test_g4_non_disk_file_object(self, tmp_path: Path) -> None: + """Testing loading from non-disk non-BytesIO file object""" + test_file = "Tests/images/hopper_g4_500.tif" + s = io.BytesIO() + with open(test_file, "rb") as f: + s.write(f.read()) + s.seek(0) + r = io.BufferedReader(s) + with Image.open(r) as im: + assert im.size == (500, 500) + self._assert_noerr(tmp_path, im) + + def test_g4_eq_png(self) -> None: + """Checking that we're actually getting the data that we expect""" + with Image.open("Tests/images/hopper_bw_500.png") as png: + assert_image_equal_tofile(png, "Tests/images/hopper_g4_500.tif") + + # see https://github.com/python-pillow/Pillow/issues/279 + def test_g4_fillorder_eq_png(self) -> None: + """Checking that we're actually getting the data that we expect""" + with Image.open("Tests/images/g4-fillorder-test.tif") as g4: + assert_image_equal_tofile(g4, "Tests/images/g4-fillorder-test.png") + + def test_g4_write(self, tmp_path: Path) -> None: + """Checking to see that the saved image is the same as what we wrote""" + test_file = "Tests/images/hopper_g4_500.tif" + with Image.open(test_file) as orig: + out = str(tmp_path / "temp.tif") + rot = orig.transpose(Image.Transpose.ROTATE_90) + assert rot.size == (500, 500) + rot.save(out) + + with Image.open(out) as reread: + assert reread.size == (500, 500) + self._assert_noerr(tmp_path, reread) + assert_image_equal(reread, rot) + assert reread.info["compression"] == "group4" + + assert reread.info["compression"] == orig.info["compression"] + + assert orig.tobytes() != reread.tobytes() + + def test_adobe_deflate_tiff(self) -> None: + test_file = "Tests/images/tiff_adobe_deflate.tif" + with Image.open(test_file) as im: + assert im.mode == "RGB" + assert im.size == (278, 374) + assert im.tile[0][:3] == ("libtiff", (0, 0, 278, 374), 0) + im.load() + + assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") + + @pytest.mark.parametrize("legacy_api", (False, True)) + def test_write_metadata(self, legacy_api: bool, tmp_path: Path) -> None: + """Test metadata writing through libtiff""" + f = str(tmp_path / "temp.tiff") + with Image.open("Tests/images/hopper_g4.tif") as img: + img.save(f, tiffinfo=img.tag) + + if legacy_api: + original = img.tag.named() + else: + original = img.tag_v2.named() + + # PhotometricInterpretation is set from SAVE_INFO, + # not the original image. + ignored = [ + "StripByteCounts", + "RowsPerStrip", + "PageNumber", + "PhotometricInterpretation", + ] + + with Image.open(f) as loaded: + if legacy_api: + reloaded = loaded.tag.named() + else: + reloaded = loaded.tag_v2.named() + + for tag, value in itertools.chain(reloaded.items(), original.items()): + if tag not in ignored: + val = original[tag] + if tag.endswith("Resolution"): + if legacy_api: + assert val[0][0] / val[0][1] == ( + 4294967295 / 113653537 + ), f"{tag} didn't roundtrip" + else: + assert val == 37.79000115940079, f"{tag} didn't roundtrip" + else: + assert val == value, f"{tag} didn't roundtrip" + + # https://github.com/python-pillow/Pillow/issues/1561 + requested_fields = ["StripByteCounts", "RowsPerStrip", "StripOffsets"] + for field in requested_fields: + assert field in reloaded, f"{field} not in metadata" + + @pytest.mark.valgrind_known_error(reason="Known invalid metadata") + def test_additional_metadata( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path + ) -> None: + # these should not crash. Seriously dummy data, most of it doesn't make + # any sense, so we're running up against limits where we're asking + # libtiff to do stupid things. + + # Get the list of the ones that we should be able to write + + core_items = { + tag: info + for tag, info in ((s, TiffTags.lookup(s)) for s in TiffTags.LIBTIFF_CORE) + if info.type is not None + } + + # Exclude ones that have special meaning + # that we're already testing them + with Image.open("Tests/images/hopper_g4.tif") as im: + for tag in im.tag_v2: + try: + del core_items[tag] + except KeyError: + pass + del core_items[320] # colormap is special, tested below + + # Type codes: + # 2: "ascii", + # 3: "short", + # 4: "long", + # 5: "rational", + # 12: "double", + # Type: dummy value + values = { + 2: "test", + 3: 1, + 4: 2**20, + 5: TiffImagePlugin.IFDRational(100, 1), + 12: 1.05, + } + + new_ifd = TiffImagePlugin.ImageFileDirectory_v2() + for tag, info in core_items.items(): + if info.length == 1: + new_ifd[tag] = values[info.type] + if info.length == 0: + new_ifd[tag] = tuple(values[info.type] for _ in range(3)) + else: + new_ifd[tag] = tuple(values[info.type] for _ in range(info.length)) + + # Extra samples really doesn't make sense in this application. + del new_ifd[338] + + out = str(tmp_path / "temp.tif") + monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) + + im.save(out, tiffinfo=new_ifd) + + @pytest.mark.parametrize( + "libtiff", + ( + pytest.param( + True, + marks=pytest.mark.skipif( + not getattr(Image.core, "libtiff_support_custom_tags", False), + reason="Custom tags not supported by older libtiff", + ), + ), + False, + ), + ) + def test_custom_metadata( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path, libtiff: bool + ) -> None: + monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", libtiff) + + class Tc(NamedTuple): + value: Any + type: int + supported_by_default: bool + + custom = { + 37000 + k: v + for k, v in enumerate( + [ + Tc(4, TiffTags.SHORT, True), + Tc(123456789, TiffTags.LONG, True), + Tc(-4, TiffTags.SIGNED_BYTE, False), + Tc(-4, TiffTags.SIGNED_SHORT, False), + Tc(-123456789, TiffTags.SIGNED_LONG, False), + Tc(TiffImagePlugin.IFDRational(4, 7), TiffTags.RATIONAL, True), + Tc(4.25, TiffTags.FLOAT, True), + Tc(4.25, TiffTags.DOUBLE, True), + Tc("custom tag value", TiffTags.ASCII, True), + Tc(b"custom tag value", TiffTags.BYTE, True), + Tc((4, 5, 6), TiffTags.SHORT, True), + Tc((123456789, 9, 34, 234, 219387, 92432323), TiffTags.LONG, True), + Tc((-4, 9, 10), TiffTags.SIGNED_BYTE, False), + Tc((-4, 5, 6), TiffTags.SIGNED_SHORT, False), + Tc( + (-123456789, 9, 34, 234, 219387, -92432323), + TiffTags.SIGNED_LONG, + False, + ), + Tc((4.25, 5.25), TiffTags.FLOAT, True), + Tc((4.25, 5.25), TiffTags.DOUBLE, True), + # array of TIFF_BYTE requires bytes instead of tuple for backwards + # compatibility + Tc(bytes([4]), TiffTags.BYTE, True), + Tc(bytes((4, 9, 10)), TiffTags.BYTE, True), + ] + ) + } + + def check_tags( + tiffinfo: TiffImagePlugin.ImageFileDirectory_v2 | dict[int, str] + ) -> None: + im = hopper() + + out = str(tmp_path / "temp.tif") + im.save(out, tiffinfo=tiffinfo) + + with Image.open(out) as reloaded: + for tag, value in tiffinfo.items(): + reloaded_value = reloaded.tag_v2[tag] + if ( + isinstance(reloaded_value, TiffImagePlugin.IFDRational) + and libtiff + ): + # libtiff does not support real RATIONALS + assert round(abs(float(reloaded_value) - float(value)), 7) == 0 + continue + + assert reloaded_value == value + + # Test with types + ifd = TiffImagePlugin.ImageFileDirectory_v2() + for tag, tagdata in custom.items(): + ifd[tag] = tagdata.value + ifd.tagtype[tag] = tagdata.type + check_tags(ifd) + + # Test without types. This only works for some types, int for example are + # always encoded as LONG and not SIGNED_LONG. + check_tags( + { + tag: tagdata.value + for tag, tagdata in custom.items() + if tagdata.supported_by_default + } + ) + + def test_osubfiletype(self, tmp_path: Path) -> None: + outfile = str(tmp_path / "temp.tif") + with Image.open("Tests/images/g4_orientation_6.tif") as im: + im.tag_v2[OSUBFILETYPE] = 1 + im.save(outfile) + + def test_subifd(self, tmp_path: Path) -> None: + outfile = str(tmp_path / "temp.tif") + with Image.open("Tests/images/g4_orientation_6.tif") as im: + im.tag_v2[SUBIFD] = 10000 + + # Should not segfault + im.save(outfile) + + def test_xmlpacket_tag( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path + ) -> None: + monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) + + out = str(tmp_path / "temp.tif") + hopper().save(out, tiffinfo={700: b"xmlpacket tag"}) + + with Image.open(out) as reloaded: + if 700 in reloaded.tag_v2: + assert reloaded.tag_v2[700] == b"xmlpacket tag" + + def test_int_dpi(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + # issue #1765 + im = hopper("RGB") + out = str(tmp_path / "temp.tif") + monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) + im.save(out, dpi=(72, 72)) + with Image.open(out) as reloaded: + assert reloaded.info["dpi"] == (72.0, 72.0) + + 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") + i.save(out, compression="group3") + + with Image.open(out) as reread: + assert reread.info["compression"] == "group3" + assert_image_equal(reread, i) + + def test_little_endian(self, tmp_path: Path) -> None: + with Image.open("Tests/images/16bit.deflate.tif") as im: + assert im.getpixel((0, 0)) == 480 + assert im.mode == "I;16" + + b = im.tobytes() + # Bytes are in image native order (little endian) + assert b[0] == ord(b"\xe0") + assert b[1] == ord(b"\x01") + + out = str(tmp_path / "temp.tif") + # out = "temp.le.tif" + im.save(out) + with Image.open(out) as reread: + assert reread.info["compression"] == im.info["compression"] + assert reread.getpixel((0, 0)) == 480 + # UNDONE - libtiff defaults to writing in native endian, so + # on big endian, we'll get back mode = 'I;16B' here. + + def test_big_endian(self, tmp_path: Path) -> None: + with Image.open("Tests/images/16bit.MM.deflate.tif") as im: + assert im.getpixel((0, 0)) == 480 + assert im.mode == "I;16B" + + b = im.tobytes() + + # Bytes are in image native order (big endian) + assert b[0] == ord(b"\x01") + assert b[1] == ord(b"\xe0") + + out = str(tmp_path / "temp.tif") + im.save(out) + with Image.open(out) as reread: + assert reread.info["compression"] == im.info["compression"] + assert reread.getpixel((0, 0)) == 480 + + def test_g4_string_info(self, tmp_path: Path) -> None: + """Tests String data in info directory""" + test_file = "Tests/images/hopper_g4_500.tif" + with Image.open(test_file) as orig: + out = str(tmp_path / "temp.tif") + + orig.tag[269] = "temp.tif" + orig.save(out) + + with Image.open(out) as reread: + assert "temp.tif" == reread.tag_v2[269] + assert "temp.tif" == reread.tag[269][0] + + def test_12bit_rawmode(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Are we generating the same interpretation + of the image as Imagemagick is?""" + monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True) + with Image.open("Tests/images/12bit.cropped.tif") as im: + im.load() + monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", False) + # to make the target -- + # convert 12bit.cropped.tif -depth 16 tmp.tif + # convert tmp.tif -evaluate RightShift 4 12in16bit2.tif + # imagemagick will auto scale so that a 12bit FFF is 16bit FFF0, + # so we need to unshift so that the integer values are the same. + + assert_image_equal_tofile(im, "Tests/images/12in16bit.tif") + + 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") + with Image.open("Tests/images/pport_g4.tif") as im: + im = im.convert("L") + + im = im.filter(ImageFilter.GaussianBlur(4)) + im.save(out, compression="tiff_adobe_deflate") + + assert_image_equal_tofile(im, out) + + def test_compressions(self, tmp_path: Path) -> None: + # Test various tiff compressions and assert similar image content but reduced + # file sizes. + im = hopper("RGB") + out = str(tmp_path / "temp.tif") + im.save(out) + size_raw = os.path.getsize(out) + + for compression in ("packbits", "tiff_lzw"): + im.save(out, compression=compression) + size_compressed = os.path.getsize(out) + assert_image_equal_tofile(im, out) + + im.save(out, compression="jpeg") + size_jpeg = os.path.getsize(out) + with Image.open(out) as im2: + assert_image_similar(im, im2, 30) + + im.save(out, compression="jpeg", quality=30) + size_jpeg_30 = os.path.getsize(out) + assert_image_similar_tofile(im2, out, 30) + + assert size_raw > size_compressed + assert size_compressed > size_jpeg + assert size_jpeg > size_jpeg_30 + + def test_tiff_jpeg_compression(self, tmp_path: Path) -> None: + im = hopper("RGB") + out = str(tmp_path / "temp.tif") + im.save(out, compression="tiff_jpeg") + + with Image.open(out) as reloaded: + assert reloaded.info["compression"] == "jpeg" + + def test_tiff_deflate_compression(self, tmp_path: Path) -> None: + im = hopper("RGB") + out = str(tmp_path / "temp.tif") + im.save(out, compression="tiff_deflate") + + with Image.open(out) as reloaded: + assert reloaded.info["compression"] == "tiff_adobe_deflate" + + def test_quality(self, tmp_path: Path) -> None: + im = hopper("RGB") + out = str(tmp_path / "temp.tif") + + with pytest.raises(ValueError): + im.save(out, compression="tiff_lzw", quality=50) + with pytest.raises(ValueError): + im.save(out, compression="jpeg", quality=-1) + with pytest.raises(ValueError): + im.save(out, compression="jpeg", quality=101) + with pytest.raises(ValueError): + im.save(out, compression="jpeg", quality="good") + im.save(out, compression="jpeg", quality=0) + im.save(out, compression="jpeg", quality=100) + + def test_cmyk_save(self, tmp_path: Path) -> None: + im = hopper("CMYK") + out = str(tmp_path / "temp.tif") + + im.save(out, compression="tiff_adobe_deflate") + assert_image_equal_tofile(im, out) + + @pytest.mark.parametrize("im", (hopper("P"), Image.new("P", (1, 1), "#000"))) + def test_palette_save( + self, im: Image.Image, monkeypatch: pytest.MonkeyPatch, tmp_path: Path + ) -> None: + out = str(tmp_path / "temp.tif") + + monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) + im.save(out) + + with Image.open(out) as reloaded: + # colormap/palette tag + 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") + + with pytest.raises(OSError): + im.save(out, compression=compression) + + def test_fp_leak(self) -> None: + im: Image.Image | None = Image.open("Tests/images/hopper_g4_500.tif") + assert im is not None + fn = im.fp.fileno() + + os.fstat(fn) + im.load() # this should close it. + with pytest.raises(OSError): + os.fstat(fn) + im = None # this should force even more closed. + with pytest.raises(OSError): + os.fstat(fn) + with pytest.raises(OSError): + os.close(fn) + + def test_multipage(self, monkeypatch: pytest.MonkeyPatch) -> None: + # issue #862 + monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True) + with Image.open("Tests/images/multipage.tiff") as im: + # file is a multipage tiff, 10x10 green, 10x10 red, 20x20 blue + + im.seek(0) + assert im.size == (10, 10) + assert im.convert("RGB").getpixel((0, 0)) == (0, 128, 0) + assert im.tag.next + + im.seek(1) + assert im.size == (10, 10) + assert im.convert("RGB").getpixel((0, 0)) == (255, 0, 0) + assert im.tag.next + + im.seek(2) + assert not im.tag.next + assert im.size == (20, 20) + assert im.convert("RGB").getpixel((0, 0)) == (0, 0, 255) + + def test_multipage_nframes(self, monkeypatch: pytest.MonkeyPatch) -> None: + # issue #862 + monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True) + with Image.open("Tests/images/multipage.tiff") as im: + frames = im.n_frames + assert frames == 3 + for _ in range(frames): + im.seek(0) + # Should not raise ValueError: I/O operation on closed file + im.load() + + def test_multipage_seek_backwards(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True) + with Image.open("Tests/images/multipage.tiff") as im: + im.seek(1) + im.load() + + im.seek(0) + assert im.convert("RGB").getpixel((0, 0)) == (0, 128, 0) + + def test__next(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True) + with Image.open("Tests/images/hopper.tif") as im: + assert not im.tag.next + im.load() + assert not im.tag.next + + def test_4bit(self, monkeypatch: pytest.MonkeyPatch) -> None: + # Arrange + test_file = "Tests/images/hopper_gray_4bpp.tif" + original = hopper("L") + + # Act + monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True) + with Image.open(test_file) as im: + + # Assert + assert im.size == (128, 128) + assert im.mode == "L" + assert_image_similar(im, original, 7.3) + + def test_gray_semibyte_per_pixel(self) -> None: + test_files = ( + ( + 24.8, # epsilon + ( # group + "Tests/images/tiff_gray_2_4_bpp/hopper2.tif", + "Tests/images/tiff_gray_2_4_bpp/hopper2I.tif", + "Tests/images/tiff_gray_2_4_bpp/hopper2R.tif", + "Tests/images/tiff_gray_2_4_bpp/hopper2IR.tif", + ), + ), + ( + 7.3, # epsilon + ( # group + "Tests/images/tiff_gray_2_4_bpp/hopper4.tif", + "Tests/images/tiff_gray_2_4_bpp/hopper4I.tif", + "Tests/images/tiff_gray_2_4_bpp/hopper4R.tif", + "Tests/images/tiff_gray_2_4_bpp/hopper4IR.tif", + ), + ), + ) + original = hopper("L") + for epsilon, group in test_files: + with Image.open(group[0]) as im: + assert im.size == (128, 128) + assert im.mode == "L" + assert_image_similar(im, original, epsilon) + for file in group[1:]: + with Image.open(file) as im2: + assert im2.size == (128, 128) + assert im2.mode == "L" + assert_image_equal(im, im2) + + def test_save_bytesio(self, monkeypatch: pytest.MonkeyPatch) -> None: + # PR 1011 + # Test TIFF saving to io.BytesIO() object. + + monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) + monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True) + + # Generate test image + pilim = hopper() + + def save_bytesio(compression: str | None = None) -> None: + buffer_io = io.BytesIO() + pilim.save(buffer_io, format="tiff", compression=compression) + buffer_io.seek(0) + + with Image.open(buffer_io) as saved_im: + assert_image_similar(pilim, saved_im, 0) + + save_bytesio() + save_bytesio("raw") + save_bytesio("packbits") + save_bytesio("tiff_lzw") + + def test_save_ycbcr(self, tmp_path: Path) -> None: + im = hopper("YCbCr") + outfile = str(tmp_path / "temp.tif") + im.save(outfile, compression="jpeg") + + with Image.open(outfile) as reloaded: + assert reloaded.tag_v2[530] == (1, 1) + assert reloaded.tag_v2[532] == (0, 255, 128, 255, 128, 255) + + def test_exif_ifd(self, tmp_path: Path) -> None: + outfile = str(tmp_path / "temp.tif") + with Image.open("Tests/images/tiff_adobe_deflate.tif") as im: + assert im.tag_v2[34665] == 125456 + im.save(outfile) + + with Image.open(outfile) as reloaded: + if Image.core.libtiff_support_custom_tags: + assert reloaded.tag_v2[34665] == 125456 + + def test_crashing_metadata( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path + ) -> None: + # issue 1597 + with Image.open("Tests/images/rdf.tif") as im: + out = str(tmp_path / "temp.tif") + + monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) + # this shouldn't crash + im.save(out, format="TIFF") + + def test_page_number_x_0(self, tmp_path: Path) -> None: + # Issue 973 + # Test TIFF with tag 297 (Page Number) having value of 0 0. + # The first number is the current page number. + # The second is the total number of pages, zero means not available. + outfile = str(tmp_path / "temp.tif") + # Created by printing a page in Chrome to PDF, then: + # /usr/bin/gs -q -sDEVICE=tiffg3 -sOutputFile=total-pages-zero.tif + # -dNOPAUSE /tmp/test.pdf -c quit + infile = "Tests/images/total-pages-zero.tif" + with Image.open(infile) as im: + # Should not divide by zero + im.save(outfile) + + def test_fd_duplication(self, tmp_path: Path) -> None: + # https://github.com/python-pillow/Pillow/issues/1651 + + tmpfile = str(tmp_path / "temp.tif") + with open(tmpfile, "wb") as f: + with open("Tests/images/g4-multi.tiff", "rb") as src: + f.write(src.read()) + + im = Image.open(tmpfile) + assert isinstance(im, TiffImagePlugin.TiffImageFile) + im.n_frames + im.close() + # Should not raise PermissionError. + os.remove(tmpfile) + + def test_read_icc(self, monkeypatch: pytest.MonkeyPatch) -> None: + with Image.open("Tests/images/hopper.iccprofile.tif") as img: + icc = img.info.get("icc_profile") + assert icc is not None + monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True) + with Image.open("Tests/images/hopper.iccprofile.tif") as img: + icc_libtiff = img.info.get("icc_profile") + assert icc_libtiff is not None + assert icc == icc_libtiff + + @pytest.mark.parametrize( + "libtiff", + ( + pytest.param( + True, + marks=pytest.mark.skipif( + not getattr(Image.core, "libtiff_support_custom_tags", False), + reason="Custom tags not supported by older libtiff", + ), + ), + False, + ), + ) + def test_write_icc( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path, libtiff: bool + ) -> None: + monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", libtiff) + + with Image.open("Tests/images/hopper.iccprofile.tif") as img: + icc_profile = img.info["icc_profile"] + + out = str(tmp_path / "temp.tif") + img.save(out, icc_profile=icc_profile) + with Image.open(out) as reloaded: + assert icc_profile == reloaded.info["icc_profile"] + + def test_multipage_compression(self) -> None: + with Image.open("Tests/images/compression.tif") as im: + im.seek(0) + assert im._compression == "tiff_ccitt" + assert im.size == (10, 10) + + im.seek(1) + assert im._compression == "packbits" + assert im.size == (10, 10) + im.load() + + im.seek(0) + assert im._compression == "tiff_ccitt" + assert im.size == (10, 10) + im.load() + + def test_save_tiff_with_jpegtables(self, tmp_path: Path) -> None: + # Arrange + outfile = str(tmp_path / "temp.tif") + + # Created with ImageMagick: convert hopper.jpg hopper_jpg.tif + # Contains JPEGTables (347) tag + infile = "Tests/images/hopper_jpg.tif" + with Image.open(infile) as im: + # Act / Assert + # Should not raise UnicodeDecodeError or anything else + im.save(outfile) + + def test_16bit_RGB_tiff(self) -> None: + with Image.open("Tests/images/tiff_16bit_RGB.tiff") as im: + assert im.mode == "RGB" + assert im.size == (100, 40) + assert im.tile, [ + ( + "libtiff", + (0, 0, 100, 40), + 0, + ("RGB;16N", "tiff_adobe_deflate", False, 8), + ) + ] + im.load() + + assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGB_target.png") + + def test_16bit_RGBa_tiff(self) -> None: + with Image.open("Tests/images/tiff_16bit_RGBa.tiff") as im: + assert im.mode == "RGBA" + assert im.size == (100, 40) + assert im.tile, [ + ("libtiff", (0, 0, 100, 40), 0, ("RGBa;16N", "tiff_lzw", False, 38236)) + ] + im.load() + + assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGBa_target.png") + + @skip_unless_feature("jpg") + def test_gimp_tiff(self) -> None: + # Read TIFF JPEG images from GIMP [@PIL168] + filename = "Tests/images/pil168.tif" + with Image.open(filename) as im: + assert im.mode == "RGB" + assert im.size == (256, 256) + assert im.tile == [ + ("libtiff", (0, 0, 256, 256), 0, ("RGB", "jpeg", False, 5122)) + ] + im.load() + + assert_image_equal_tofile(im, "Tests/images/pil168.png") + + def test_sampleformat(self) -> None: + # https://github.com/python-pillow/Pillow/issues/1466 + with Image.open("Tests/images/copyleft.tiff") as im: + assert im.mode == "RGB" + + assert_image_equal_tofile(im, "Tests/images/copyleft.png", mode="RGB") + + def test_sampleformat_write( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path + ) -> None: + im = Image.new("F", (1, 1)) + out = str(tmp_path / "temp.tif") + monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) + im.save(out) + + with Image.open(out) as reloaded: + assert reloaded.mode == "F" + assert reloaded.getexif()[SAMPLEFORMAT] == 3 + + def test_lzma(self, capfd: pytest.CaptureFixture[str]) -> None: + try: + with Image.open("Tests/images/hopper_lzma.tif") as im: + assert im.mode == "RGB" + assert im.size == (128, 128) + assert im.format == "TIFF" + im2 = hopper() + assert_image_similar(im, im2, 5) + except OSError: + captured = capfd.readouterr() + if "LZMA compression support is not configured" in captured.err: + pytest.skip("LZMA compression support is not configured") + sys.stdout.write(captured.out) + sys.stderr.write(captured.err) + raise + + def test_webp(self, capfd: pytest.CaptureFixture[str]) -> None: + try: + with Image.open("Tests/images/hopper_webp.tif") as im: + assert im.mode == "RGB" + assert im.size == (128, 128) + assert im.format == "TIFF" + assert_image_similar_tofile(im, "Tests/images/hopper_webp.png", 1) + except OSError: + captured = capfd.readouterr() + if "WEBP compression support is not configured" in captured.err: + pytest.skip("WEBP compression support is not configured") + if ( + "Compression scheme 50001 strip decoding is not implemented" + in captured.err + ): + pytest.skip( + "Compression scheme 50001 strip decoding is not implemented" + ) + sys.stdout.write(captured.out) + sys.stderr.write(captured.err) + raise + + def test_lzw(self) -> None: + with Image.open("Tests/images/hopper_lzw.tif") as im: + assert im.mode == "RGB" + assert im.size == (128, 128) + assert im.format == "TIFF" + im2 = hopper() + assert_image_similar(im, im2, 5) + + def test_strip_cmyk_jpeg(self) -> None: + infile = "Tests/images/tiff_strip_cmyk_jpeg.tif" + with Image.open(infile) as im: + assert_image_similar_tofile(im, "Tests/images/pil_sample_cmyk.jpg", 0.5) + + def test_strip_cmyk_16l_jpeg(self) -> None: + infile = "Tests/images/tiff_strip_cmyk_16l_jpeg.tif" + with Image.open(infile) as im: + assert_image_similar_tofile(im, "Tests/images/pil_sample_cmyk.jpg", 0.5) + + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) + def test_strip_ycbcr_jpeg_2x2_sampling(self) -> None: + infile = "Tests/images/tiff_strip_ycbcr_jpeg_2x2_sampling.tif" + with Image.open(infile) as im: + assert_image_similar_tofile(im, "Tests/images/flower.jpg", 1.2) + + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) + def test_strip_ycbcr_jpeg_1x1_sampling(self) -> None: + infile = "Tests/images/tiff_strip_ycbcr_jpeg_1x1_sampling.tif" + with Image.open(infile) as im: + assert_image_similar_tofile(im, "Tests/images/flower2.jpg", 0.01) + + def test_tiled_cmyk_jpeg(self) -> None: + infile = "Tests/images/tiff_tiled_cmyk_jpeg.tif" + with Image.open(infile) as im: + assert_image_similar_tofile(im, "Tests/images/pil_sample_cmyk.jpg", 0.5) + + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) + def test_tiled_ycbcr_jpeg_1x1_sampling(self) -> None: + infile = "Tests/images/tiff_tiled_ycbcr_jpeg_1x1_sampling.tif" + with Image.open(infile) as im: + assert_image_similar_tofile(im, "Tests/images/flower2.jpg", 0.01) + + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) + def test_tiled_ycbcr_jpeg_2x2_sampling(self) -> None: + infile = "Tests/images/tiff_tiled_ycbcr_jpeg_2x2_sampling.tif" + with Image.open(infile) as im: + assert_image_similar_tofile(im, "Tests/images/flower.jpg", 1.5) + + def test_strip_planar_rgb(self) -> None: + # gdal_translate -co TILED=no -co INTERLEAVE=BAND -co COMPRESS=LZW \ + # tiff_strip_raw.tif tiff_strip_planar_lzw.tiff + infile = "Tests/images/tiff_strip_planar_lzw.tiff" + with Image.open(infile) as im: + assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") + + def test_tiled_planar_rgb(self) -> None: + # gdal_translate -co TILED=yes -co INTERLEAVE=BAND -co COMPRESS=LZW \ + # tiff_tiled_raw.tif tiff_tiled_planar_lzw.tiff + infile = "Tests/images/tiff_tiled_planar_lzw.tiff" + with Image.open(infile) as im: + assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") + + def test_tiled_planar_16bit_RGB(self) -> None: + # gdal_translate -co TILED=yes -co INTERLEAVE=BAND -co COMPRESS=LZW \ + # tiff_16bit_RGB.tiff tiff_tiled_planar_16bit_RGB.tiff + with Image.open("Tests/images/tiff_tiled_planar_16bit_RGB.tiff") as im: + assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGB_target.png") + + def test_strip_planar_16bit_RGB(self) -> None: + # gdal_translate -co TILED=no -co INTERLEAVE=BAND -co COMPRESS=LZW \ + # tiff_16bit_RGB.tiff tiff_strip_planar_16bit_RGB.tiff + with Image.open("Tests/images/tiff_strip_planar_16bit_RGB.tiff") as im: + assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGB_target.png") + + def test_tiled_planar_16bit_RGBa(self) -> None: + # gdal_translate -co TILED=yes \ + # -co INTERLEAVE=BAND -co COMPRESS=LZW -co ALPHA=PREMULTIPLIED \ + # tiff_16bit_RGBa.tiff tiff_tiled_planar_16bit_RGBa.tiff + with Image.open("Tests/images/tiff_tiled_planar_16bit_RGBa.tiff") as im: + assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGBa_target.png") + + def test_strip_planar_16bit_RGBa(self) -> None: + # gdal_translate -co TILED=no \ + # -co INTERLEAVE=BAND -co COMPRESS=LZW -co ALPHA=PREMULTIPLIED \ + # tiff_16bit_RGBa.tiff tiff_strip_planar_16bit_RGBa.tiff + with Image.open("Tests/images/tiff_strip_planar_16bit_RGBa.tiff") as im: + assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGBa_target.png") + + @pytest.mark.parametrize("compression", (None, "jpeg")) + def test_block_tile_tags(self, compression: str | None, tmp_path: Path) -> None: + im = hopper() + out = str(tmp_path / "temp.tif") + + tags = { + TiffImagePlugin.TILEWIDTH: 256, + TiffImagePlugin.TILELENGTH: 256, + TiffImagePlugin.TILEOFFSETS: 256, + TiffImagePlugin.TILEBYTECOUNTS: 256, + } + im.save(out, exif=tags, compression=compression) + + with Image.open(out) as reloaded: + for tag in tags: + assert tag not in reloaded.getexif() + + def test_old_style_jpeg(self) -> None: + with Image.open("Tests/images/old-style-jpeg-compression.tif") as im: + assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png") + + def test_open_missing_samplesperpixel(self) -> None: + with Image.open( + "Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif" + ) as im: + assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png") + + @pytest.mark.parametrize( + "file_name, mode, size, tile", + [ + ( + "tiff_wrong_bits_per_sample.tiff", + "RGBA", + (52, 53), + [("raw", (0, 0, 52, 53), 160, ("RGBA", 0, 1))], + ), + ( + "tiff_wrong_bits_per_sample_2.tiff", + "RGB", + (16, 16), + [("raw", (0, 0, 16, 16), 8, ("RGB", 0, 1))], + ), + ( + "tiff_wrong_bits_per_sample_3.tiff", + "RGBA", + (512, 256), + [("libtiff", (0, 0, 512, 256), 0, ("RGBA", "tiff_lzw", False, 48782))], + ), + ], + ) + def test_wrong_bits_per_sample( + self, file_name: str, mode: str, size: tuple[int, int], tile + ) -> None: + with Image.open("Tests/images/" + file_name) as im: + assert im.mode == mode + assert im.size == size + assert im.tile == tile + im.load() + + def test_no_rows_per_strip(self) -> None: + # This image does not have a RowsPerStrip TIFF tag + infile = "Tests/images/no_rows_per_strip.tif" + with Image.open(infile) as im: + im.load() + assert im.size == (950, 975) + + def test_orientation(self) -> None: + with Image.open("Tests/images/g4_orientation_1.tif") as base_im: + for i in range(2, 9): + with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im: + assert 274 in im.tag_v2 + + im.load() + assert 274 not in im.tag_v2 + + assert_image_similar(base_im, im, 0.7) + + def test_exif_transpose(self) -> None: + with Image.open("Tests/images/g4_orientation_1.tif") as base_im: + for i in range(2, 9): + with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im: + im = ImageOps.exif_transpose(im) + + assert_image_similar(base_im, im, 0.7) + + @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 + # when saving to a new file. + # Pillow 6.0 fails with "OSError: cannot identify image file". + tiff = io.BytesIO( + base64.b64decode( + b"SUkqAAgAAAAPAP4ABAABAAAAAAAAAAABBAABAAAAAQAAAAEBBAABAAAAAQAA" + b"AAIBAwADAAAAwgAAAAMBAwABAAAACAAAAAYBAwABAAAAAgAAABEBBAABAAAA" + b"4AAAABUBAwABAAAAAwAAABYBBAABAAAAAQAAABcBBAABAAAACwAAABoBBQAB" + b"AAAAyAAAABsBBQABAAAA0AAAABwBAwABAAAAAQAAACgBAwABAAAAAQAAAFMB" + b"AwADAAAA2AAAAAAAAAAIAAgACAABAAAAAQAAAAEAAAABAAAAAQABAAEAAAB4" + b"nGNgYAAAAAMAAQ==" + ) + ) + out = io.BytesIO() + with Image.open(tiff) as im: + im.save(out, format="tiff") + out.seek(0) + with Image.open(out) as im: + im.load() + + def test_realloc_overflow(self, 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" + + @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") + im.save(out, compression=compression) + + with Image.open(out) as im: + # Assert that there are multiple strips + assert isinstance(im, TiffImagePlugin.TiffImageFile) + 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: + im = hopper("RGB").resize((256, 256)) + out = str(tmp_path / "temp.tif") + + if not argument: + TiffImagePlugin.STRIP_SIZE = 2**18 + try: + arguments: dict[str, str | int] = {"compression": "tiff_adobe_deflate"} + if argument: + arguments["strip_size"] = 2**18 + im.save(out, **arguments) + + with Image.open(out) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) + assert len(im.tag_v2[STRIPOFFSETS]) == 1 + finally: + TiffImagePlugin.STRIP_SIZE = 65536 + + @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", None)) + def test_save_zero(self, compression: str | None, tmp_path: Path) -> None: + im = Image.new("RGB", (0, 0)) + out = str(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") + for _ in range(10000): + im.save(out, compression="jpeg") + + @pytest.mark.parametrize( + "path, sizes", + ( + ("Tests/images/hopper.tif", ()), + ("Tests/images/child_ifd.tiff", (16, 8)), + ("Tests/images/child_ifd_jpeg.tiff", (20,)), + ), + ) + def test_get_child_images(self, path: str, sizes: tuple[int, ...]) -> None: + with Image.open(path) as im: + ims = im.get_child_images() + + assert len(ims) == len(sizes) + for i, im in enumerate(ims): + w = sizes[i] + expected = Image.new("RGB", (w, w), "#f00") + assert_image_similar(im, expected, 1) diff --git a/Tests/test_file_libtiff_small.py b/Tests/test_file_libtiff_small.py index 617e1e89c..501ff0394 100644 --- a/Tests/test_file_libtiff_small.py +++ b/Tests/test_file_libtiff_small.py @@ -1,46 +1,46 @@ -from __future__ import annotations - -from io import BytesIO -from pathlib import Path - -from PIL import Image - -from .test_file_libtiff import LibTiffTestCase - - -class TestFileLibTiffSmall(LibTiffTestCase): - """The small lena image was failing on open in the libtiff - decoder because the file pointer was set to the wrong place - by a spurious seek. It wasn't failing with the byteio method. - - It was fixed by forcing an lseek to the beginning of the - file just before reading in libtiff. These tests remain - to ensure that it stays fixed.""" - - def test_g4_hopper_file(self, tmp_path: Path) -> None: - """Testing the open file load path""" - - test_file = "Tests/images/hopper_g4.tif" - with open(test_file, "rb") as f: - with Image.open(f) as im: - assert im.size == (128, 128) - self._assert_noerr(tmp_path, im) - - def test_g4_hopper_bytesio(self, tmp_path: Path) -> None: - """Testing the bytesio loading code path""" - test_file = "Tests/images/hopper_g4.tif" - s = BytesIO() - with open(test_file, "rb") as f: - s.write(f.read()) - s.seek(0) - with Image.open(s) as im: - assert im.size == (128, 128) - self._assert_noerr(tmp_path, im) - - def test_g4_hopper(self, tmp_path: Path) -> None: - """The 128x128 lena image failed for some reason.""" - - test_file = "Tests/images/hopper_g4.tif" - with Image.open(test_file) as im: - assert im.size == (128, 128) - self._assert_noerr(tmp_path, im) +from __future__ import annotations + +from io import BytesIO +from pathlib import Path + +from PIL import Image + +from .test_file_libtiff import LibTiffTestCase + + +class TestFileLibTiffSmall(LibTiffTestCase): + """The small lena image was failing on open in the libtiff + decoder because the file pointer was set to the wrong place + by a spurious seek. It wasn't failing with the byteio method. + + It was fixed by forcing an lseek to the beginning of the + file just before reading in libtiff. These tests remain + to ensure that it stays fixed.""" + + def test_g4_hopper_file(self, tmp_path: Path) -> None: + """Testing the open file load path""" + + test_file = "Tests/images/hopper_g4.tif" + with open(test_file, "rb") as f: + with Image.open(f) as im: + assert im.size == (128, 128) + self._assert_noerr(tmp_path, im) + + def test_g4_hopper_bytesio(self, tmp_path: Path) -> None: + """Testing the bytesio loading code path""" + test_file = "Tests/images/hopper_g4.tif" + s = BytesIO() + with open(test_file, "rb") as f: + s.write(f.read()) + s.seek(0) + with Image.open(s) as im: + assert im.size == (128, 128) + self._assert_noerr(tmp_path, im) + + def test_g4_hopper(self, tmp_path: Path) -> None: + """The 128x128 lena image failed for some reason.""" + + test_file = "Tests/images/hopper_g4.tif" + with Image.open(test_file) as im: + assert im.size == (128, 128) + self._assert_noerr(tmp_path, im) diff --git a/Tests/test_file_mcidas.py b/Tests/test_file_mcidas.py index e11e6bb52..1046e3dec 100644 --- a/Tests/test_file_mcidas.py +++ b/Tests/test_file_mcidas.py @@ -1,32 +1,32 @@ -from __future__ import annotations - -import pytest - -from PIL import Image, McIdasImagePlugin - -from .helper import assert_image_equal_tofile - - -def test_invalid_file() -> None: - invalid_file = "Tests/images/flower.jpg" - - with pytest.raises(SyntaxError): - McIdasImagePlugin.McIdasImageFile(invalid_file) - - -def test_valid_file() -> None: - # Arrange - # https://ghrc.nsstc.nasa.gov/hydro/details/cmx3g8 - # https://ghrc.nsstc.nasa.gov/pub/fieldCampaigns/camex3/cmx3g8/browse/ - test_file = "Tests/images/cmx3g8_wv_1998.260_0745_mcidas.ara" - saved_file = "Tests/images/cmx3g8_wv_1998.260_0745_mcidas.tiff" - - # Act - with Image.open(test_file) as im: - im.load() - - # Assert - assert im.format == "MCIDAS" - assert im.mode == "I" - assert im.size == (1800, 400) - assert_image_equal_tofile(im, saved_file) +from __future__ import annotations + +import pytest + +from PIL import Image, McIdasImagePlugin + +from .helper import assert_image_equal_tofile + + +def test_invalid_file() -> None: + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(SyntaxError): + McIdasImagePlugin.McIdasImageFile(invalid_file) + + +def test_valid_file() -> None: + # Arrange + # https://ghrc.nsstc.nasa.gov/hydro/details/cmx3g8 + # https://ghrc.nsstc.nasa.gov/pub/fieldCampaigns/camex3/cmx3g8/browse/ + test_file = "Tests/images/cmx3g8_wv_1998.260_0745_mcidas.ara" + saved_file = "Tests/images/cmx3g8_wv_1998.260_0745_mcidas.tiff" + + # Act + with Image.open(test_file) as im: + im.load() + + # Assert + assert im.format == "MCIDAS" + assert im.mode == "I" + assert im.size == (1800, 400) + assert_image_equal_tofile(im, saved_file) diff --git a/Tests/test_file_mic.py b/Tests/test_file_mic.py index 9a6f13ea3..7565c11f5 100644 --- a/Tests/test_file_mic.py +++ b/Tests/test_file_mic.py @@ -1,75 +1,75 @@ -from __future__ import annotations - -import pytest - -from PIL import Image, ImagePalette - -from .helper import assert_image_similar, hopper, skip_unless_feature - -MicImagePlugin = pytest.importorskip( - "PIL.MicImagePlugin", reason="olefile not installed" -) -pytestmark = skip_unless_feature("libtiff") -TEST_FILE = "Tests/images/hopper.mic" - - -def test_sanity() -> None: - with Image.open(TEST_FILE) as im: - im.load() - assert im.mode == "RGBA" - assert im.size == (128, 128) - assert im.format == "MIC" - - # Adjust for the gamma of 2.2 encoded into the file - lut = ImagePalette.make_gamma_lut(1 / 2.2) - im = Image.merge("RGBA", [chan.point(lut) for chan in im.split()]) - - im2 = hopper("RGBA") - assert_image_similar(im, im2, 10) - - -def test_n_frames() -> None: - with Image.open(TEST_FILE) as im: - assert im.n_frames == 1 - - -def test_is_animated() -> None: - with Image.open(TEST_FILE) as im: - assert not im.is_animated - - -def test_tell() -> None: - with Image.open(TEST_FILE) as im: - assert im.tell() == 0 - - -def test_seek() -> None: - with Image.open(TEST_FILE) as im: - im.seek(0) - assert im.tell() == 0 - - with pytest.raises(EOFError): - im.seek(99) - assert im.tell() == 0 - - -def test_close() -> None: - with Image.open(TEST_FILE) as im: - pass - assert im.ole.fp.closed - - im = Image.open(TEST_FILE) - im.close() - assert im.ole.fp.closed - - -def test_invalid_file() -> None: - # Test an invalid OLE file - invalid_file = "Tests/images/flower.jpg" - with pytest.raises(SyntaxError): - MicImagePlugin.MicImageFile(invalid_file) - - # Test a valid OLE file, but not a MIC file - ole_file = "Tests/images/test-ole-file.doc" - with pytest.raises(SyntaxError): - MicImagePlugin.MicImageFile(ole_file) +from __future__ import annotations + +import pytest + +from PIL import Image, ImagePalette + +from .helper import assert_image_similar, hopper, skip_unless_feature + +MicImagePlugin = pytest.importorskip( + "PIL.MicImagePlugin", reason="olefile not installed" +) +pytestmark = skip_unless_feature("libtiff") +TEST_FILE = "Tests/images/hopper.mic" + + +def test_sanity() -> None: + with Image.open(TEST_FILE) as im: + im.load() + assert im.mode == "RGBA" + assert im.size == (128, 128) + assert im.format == "MIC" + + # Adjust for the gamma of 2.2 encoded into the file + lut = ImagePalette.make_gamma_lut(1 / 2.2) + im = Image.merge("RGBA", [chan.point(lut) for chan in im.split()]) + + im2 = hopper("RGBA") + assert_image_similar(im, im2, 10) + + +def test_n_frames() -> None: + with Image.open(TEST_FILE) as im: + assert im.n_frames == 1 + + +def test_is_animated() -> None: + with Image.open(TEST_FILE) as im: + assert not im.is_animated + + +def test_tell() -> None: + with Image.open(TEST_FILE) as im: + assert im.tell() == 0 + + +def test_seek() -> None: + with Image.open(TEST_FILE) as im: + im.seek(0) + assert im.tell() == 0 + + with pytest.raises(EOFError): + im.seek(99) + assert im.tell() == 0 + + +def test_close() -> None: + with Image.open(TEST_FILE) as im: + pass + assert im.ole.fp.closed + + im = Image.open(TEST_FILE) + im.close() + assert im.ole.fp.closed + + +def test_invalid_file() -> None: + # Test an invalid OLE file + invalid_file = "Tests/images/flower.jpg" + with pytest.raises(SyntaxError): + MicImagePlugin.MicImageFile(invalid_file) + + # Test a valid OLE file, but not a MIC file + ole_file = "Tests/images/test-ole-file.doc" + with pytest.raises(SyntaxError): + MicImagePlugin.MicImageFile(ole_file) diff --git a/Tests/test_file_mpeg.py b/Tests/test_file_mpeg.py index 468aef8a9..44895c9ba 100644 --- a/Tests/test_file_mpeg.py +++ b/Tests/test_file_mpeg.py @@ -1,39 +1,39 @@ -from __future__ import annotations - -from io import BytesIO - -import pytest - -from PIL import Image, MpegImagePlugin - - -def test_identify() -> None: - # Arrange - b = BytesIO(b"\x00\x00\x01\xb3\x01\x00\x01") - - # Act - with Image.open(b) as im: - # Assert - assert im.format == "MPEG" - - assert im.mode == "RGB" - assert im.size == (16, 1) - - -def test_invalid_file() -> None: - # Arrange - invalid_file = "Tests/images/flower.jpg" - - # Act / Assert - with pytest.raises(SyntaxError): - MpegImagePlugin.MpegImageFile(invalid_file) - - -def test_load() -> None: - # Arrange - b = BytesIO(b"\x00\x00\x01\xb3\x01\x00\x01") - - with Image.open(b) as im: - # Act / Assert: cannot load - with pytest.raises(OSError): - im.load() +from __future__ import annotations + +from io import BytesIO + +import pytest + +from PIL import Image, MpegImagePlugin + + +def test_identify() -> None: + # Arrange + b = BytesIO(b"\x00\x00\x01\xb3\x01\x00\x01") + + # Act + with Image.open(b) as im: + # Assert + assert im.format == "MPEG" + + assert im.mode == "RGB" + assert im.size == (16, 1) + + +def test_invalid_file() -> None: + # Arrange + invalid_file = "Tests/images/flower.jpg" + + # Act / Assert + with pytest.raises(SyntaxError): + MpegImagePlugin.MpegImageFile(invalid_file) + + +def test_load() -> None: + # Arrange + b = BytesIO(b"\x00\x00\x01\xb3\x01\x00\x01") + + with Image.open(b) as im: + # Act / Assert: cannot load + with pytest.raises(OSError): + im.load() diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index a50188700..3c35049a9 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -1,280 +1,280 @@ -from __future__ import annotations - -import warnings -from io import BytesIO -from typing import Any, cast - -import pytest - -from PIL import Image, MpoImagePlugin - -from .helper import ( - assert_image_equal, - assert_image_similar, - is_pypy, - skip_unless_feature, -) - -test_files = ["Tests/images/sugarshack.mpo", "Tests/images/frozenpond.mpo"] - -pytestmark = skip_unless_feature("jpg") - - -def roundtrip(im: Image.Image, **options: Any) -> MpoImagePlugin.MpoImageFile: - out = BytesIO() - im.save(out, "MPO", **options) - out.seek(0) - return cast(MpoImagePlugin.MpoImageFile, Image.open(out)) - - -@pytest.mark.parametrize("test_file", test_files) -def test_sanity(test_file: str) -> None: - with Image.open(test_file) as im: - im.load() - assert im.mode == "RGB" - assert im.size == (640, 480) - assert im.format == "MPO" - - -@pytest.mark.skipif(is_pypy(), reason="Requires CPython") -def test_unclosed_file() -> None: - def open() -> None: - im = Image.open(test_files[0]) - im.load() - - with pytest.warns(ResourceWarning): - open() - - -def test_closed_file() -> None: - with warnings.catch_warnings(): - im = Image.open(test_files[0]) - im.load() - im.close() - - -def test_seek_after_close() -> None: - im = Image.open(test_files[0]) - im.close() - - with pytest.raises(ValueError): - im.seek(1) - - -def test_context_manager() -> None: - with warnings.catch_warnings(): - with Image.open(test_files[0]) as im: - im.load() - - -@pytest.mark.parametrize("test_file", test_files) -def test_app(test_file: str) -> None: - # Test APP/COM reader (@PIL135) - with Image.open(test_file) as im: - assert im.applist[0][0] == "APP1" - assert im.applist[1][0] == "APP2" - assert ( - im.applist[1][1][:16] == b"MPF\x00MM\x00*\x00\x00\x00\x08\x00\x03\xb0\x00" - ) - assert len(im.applist) == 2 - - -@pytest.mark.parametrize("test_file", test_files) -def test_exif(test_file: str) -> None: - with Image.open(test_file) as im_original: - im_reloaded = roundtrip(im_original, save_all=True, exif=im_original.getexif()) - - for im in (im_original, im_reloaded): - info = im._getexif() - assert info[272] == "Nintendo 3DS" - assert info[296] == 2 - assert info[34665] == 188 - - -def test_frame_size() -> None: - # This image has been hexedited to contain a different size - # in the SOF marker of the second frame - with Image.open("Tests/images/sugarshack_frame_size.mpo") as im: - assert im.size == (640, 480) - - im.seek(1) - assert im.size == (680, 480) - - im.seek(0) - assert im.size == (640, 480) - - -def test_ignore_frame_size() -> None: - # Ignore the different size of the second frame - # since this is not a "Large Thumbnail" image - with Image.open("Tests/images/ignore_frame_size.mpo") as im: - assert im.size == (64, 64) - - im.seek(1) - assert ( - im.mpinfo[0xB002][1]["Attribute"]["MPType"] - == "Multi-Frame Image: (Disparity)" - ) - assert im.size == (64, 64) - - -def test_parallax() -> None: - # Nintendo - with Image.open("Tests/images/sugarshack.mpo") as im: - exif = im.getexif() - assert exif.get_ifd(0x927C)[0x1101]["Parallax"] == -44.798187255859375 - - # Fujifilm - with Image.open("Tests/images/fujifilm.mpo") as im: - im.seek(1) - exif = im.getexif() - assert exif.get_ifd(0x927C)[0xB211] == -3.125 - - -def test_reload_exif_after_seek() -> None: - with Image.open("Tests/images/sugarshack.mpo") as im: - exif = im.getexif() - del exif[296] - - im.seek(1) - assert 296 in exif - - -@pytest.mark.parametrize("test_file", test_files) -def test_mp(test_file: str) -> None: - with Image.open(test_file) as im: - mpinfo = im._getmp() - assert mpinfo[45056] == b"0100" - assert mpinfo[45057] == 2 - - -def test_mp_offset() -> None: - # This image has been manually hexedited to have an IFD offset of 10 - # in APP2 data, in contrast to normal 8 - with Image.open("Tests/images/sugarshack_ifd_offset.mpo") as im: - mpinfo = im._getmp() - assert mpinfo[45056] == b"0100" - assert mpinfo[45057] == 2 - - -def test_mp_no_data() -> None: - # This image has been manually hexedited to have the second frame - # beyond the end of the file - with Image.open("Tests/images/sugarshack_no_data.mpo") as im: - with pytest.raises(ValueError): - im.seek(1) - - -@pytest.mark.parametrize("test_file", test_files) -def test_mp_attribute(test_file: str) -> None: - with Image.open(test_file) as im: - mpinfo = im._getmp() - for frame_number, mpentry in enumerate(mpinfo[0xB002]): - mpattr = mpentry["Attribute"] - if frame_number: - assert not mpattr["RepresentativeImageFlag"] - else: - assert mpattr["RepresentativeImageFlag"] - assert not mpattr["DependentParentImageFlag"] - assert not mpattr["DependentChildImageFlag"] - assert mpattr["ImageDataFormat"] == "JPEG" - assert mpattr["MPType"] == "Multi-Frame Image: (Disparity)" - assert mpattr["Reserved"] == 0 - - -@pytest.mark.parametrize("test_file", test_files) -def test_seek(test_file: str) -> None: - with Image.open(test_file) as im: - assert im.tell() == 0 - # prior to first image raises an error, both blatant and borderline - with pytest.raises(EOFError): - im.seek(-1) - with pytest.raises(EOFError): - im.seek(-523) - # after the final image raises an error, - # both blatant and borderline - with pytest.raises(EOFError): - im.seek(2) - with pytest.raises(EOFError): - im.seek(523) - # bad calls shouldn't change the frame - assert im.tell() == 0 - # this one will work - im.seek(1) - assert im.tell() == 1 - # and this one, too - im.seek(0) - assert im.tell() == 0 - - -def test_n_frames() -> None: - with Image.open("Tests/images/sugarshack.mpo") as im: - assert im.n_frames == 2 - assert im.is_animated - - -def test_eoferror() -> None: - with Image.open("Tests/images/sugarshack.mpo") as im: - n_frames = im.n_frames - - # Test seeking past the last frame - with pytest.raises(EOFError): - im.seek(n_frames) - assert im.tell() < n_frames - - # Test that seeking to the last frame does not raise an error - im.seek(n_frames - 1) - - -@pytest.mark.parametrize("test_file", test_files) -def test_image_grab(test_file: str) -> None: - with Image.open(test_file) as im: - assert im.tell() == 0 - im0 = im.tobytes() - im.seek(1) - assert im.tell() == 1 - im1 = im.tobytes() - im.seek(0) - assert im.tell() == 0 - im02 = im.tobytes() - assert im0 == im02 - assert im0 != im1 - - -@pytest.mark.parametrize("test_file", test_files) -def test_save(test_file: str) -> None: - with Image.open(test_file) as im: - assert im.tell() == 0 - jpg0 = roundtrip(im) - assert_image_similar(im, jpg0, 30) - im.seek(1) - assert im.tell() == 1 - jpg1 = roundtrip(im) - assert_image_similar(im, jpg1, 30) - - -def test_save_all() -> None: - for test_file in test_files: - with Image.open(test_file) as im: - im_reloaded = roundtrip(im, save_all=True) - - im.seek(0) - assert_image_similar(im, im_reloaded, 30) - - im.seek(1) - im_reloaded.seek(1) - assert_image_similar(im, im_reloaded, 30) - - im = Image.new("RGB", (1, 1)) - im2 = Image.new("RGB", (1, 1), "#f00") - im_reloaded = roundtrip(im, save_all=True, append_images=[im2]) - - assert_image_equal(im, im_reloaded) - assert im_reloaded.mpinfo[45056] == b"0100" - - im_reloaded.seek(1) - assert_image_similar(im2, im_reloaded, 1) - - # 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 +from __future__ import annotations + +import warnings +from io import BytesIO +from typing import Any, cast + +import pytest + +from PIL import Image, MpoImagePlugin + +from .helper import ( + assert_image_equal, + assert_image_similar, + is_pypy, + skip_unless_feature, +) + +test_files = ["Tests/images/sugarshack.mpo", "Tests/images/frozenpond.mpo"] + +pytestmark = skip_unless_feature("jpg") + + +def roundtrip(im: Image.Image, **options: Any) -> MpoImagePlugin.MpoImageFile: + out = BytesIO() + im.save(out, "MPO", **options) + out.seek(0) + return cast(MpoImagePlugin.MpoImageFile, Image.open(out)) + + +@pytest.mark.parametrize("test_file", test_files) +def test_sanity(test_file: str) -> None: + with Image.open(test_file) as im: + im.load() + assert im.mode == "RGB" + assert im.size == (640, 480) + assert im.format == "MPO" + + +@pytest.mark.skipif(is_pypy(), reason="Requires CPython") +def test_unclosed_file() -> None: + def open() -> None: + im = Image.open(test_files[0]) + im.load() + + with pytest.warns(ResourceWarning): + open() + + +def test_closed_file() -> None: + with warnings.catch_warnings(): + im = Image.open(test_files[0]) + im.load() + im.close() + + +def test_seek_after_close() -> None: + im = Image.open(test_files[0]) + im.close() + + with pytest.raises(ValueError): + im.seek(1) + + +def test_context_manager() -> None: + with warnings.catch_warnings(): + with Image.open(test_files[0]) as im: + im.load() + + +@pytest.mark.parametrize("test_file", test_files) +def test_app(test_file: str) -> None: + # Test APP/COM reader (@PIL135) + with Image.open(test_file) as im: + assert im.applist[0][0] == "APP1" + assert im.applist[1][0] == "APP2" + assert ( + im.applist[1][1][:16] == b"MPF\x00MM\x00*\x00\x00\x00\x08\x00\x03\xb0\x00" + ) + assert len(im.applist) == 2 + + +@pytest.mark.parametrize("test_file", test_files) +def test_exif(test_file: str) -> None: + with Image.open(test_file) as im_original: + im_reloaded = roundtrip(im_original, save_all=True, exif=im_original.getexif()) + + for im in (im_original, im_reloaded): + info = im._getexif() + assert info[272] == "Nintendo 3DS" + assert info[296] == 2 + assert info[34665] == 188 + + +def test_frame_size() -> None: + # This image has been hexedited to contain a different size + # in the SOF marker of the second frame + with Image.open("Tests/images/sugarshack_frame_size.mpo") as im: + assert im.size == (640, 480) + + im.seek(1) + assert im.size == (680, 480) + + im.seek(0) + assert im.size == (640, 480) + + +def test_ignore_frame_size() -> None: + # Ignore the different size of the second frame + # since this is not a "Large Thumbnail" image + with Image.open("Tests/images/ignore_frame_size.mpo") as im: + assert im.size == (64, 64) + + im.seek(1) + assert ( + im.mpinfo[0xB002][1]["Attribute"]["MPType"] + == "Multi-Frame Image: (Disparity)" + ) + assert im.size == (64, 64) + + +def test_parallax() -> None: + # Nintendo + with Image.open("Tests/images/sugarshack.mpo") as im: + exif = im.getexif() + assert exif.get_ifd(0x927C)[0x1101]["Parallax"] == -44.798187255859375 + + # Fujifilm + with Image.open("Tests/images/fujifilm.mpo") as im: + im.seek(1) + exif = im.getexif() + assert exif.get_ifd(0x927C)[0xB211] == -3.125 + + +def test_reload_exif_after_seek() -> None: + with Image.open("Tests/images/sugarshack.mpo") as im: + exif = im.getexif() + del exif[296] + + im.seek(1) + assert 296 in exif + + +@pytest.mark.parametrize("test_file", test_files) +def test_mp(test_file: str) -> None: + with Image.open(test_file) as im: + mpinfo = im._getmp() + assert mpinfo[45056] == b"0100" + assert mpinfo[45057] == 2 + + +def test_mp_offset() -> None: + # This image has been manually hexedited to have an IFD offset of 10 + # in APP2 data, in contrast to normal 8 + with Image.open("Tests/images/sugarshack_ifd_offset.mpo") as im: + mpinfo = im._getmp() + assert mpinfo[45056] == b"0100" + assert mpinfo[45057] == 2 + + +def test_mp_no_data() -> None: + # This image has been manually hexedited to have the second frame + # beyond the end of the file + with Image.open("Tests/images/sugarshack_no_data.mpo") as im: + with pytest.raises(ValueError): + im.seek(1) + + +@pytest.mark.parametrize("test_file", test_files) +def test_mp_attribute(test_file: str) -> None: + with Image.open(test_file) as im: + mpinfo = im._getmp() + for frame_number, mpentry in enumerate(mpinfo[0xB002]): + mpattr = mpentry["Attribute"] + if frame_number: + assert not mpattr["RepresentativeImageFlag"] + else: + assert mpattr["RepresentativeImageFlag"] + assert not mpattr["DependentParentImageFlag"] + assert not mpattr["DependentChildImageFlag"] + assert mpattr["ImageDataFormat"] == "JPEG" + assert mpattr["MPType"] == "Multi-Frame Image: (Disparity)" + assert mpattr["Reserved"] == 0 + + +@pytest.mark.parametrize("test_file", test_files) +def test_seek(test_file: str) -> None: + with Image.open(test_file) as im: + assert im.tell() == 0 + # prior to first image raises an error, both blatant and borderline + with pytest.raises(EOFError): + im.seek(-1) + with pytest.raises(EOFError): + im.seek(-523) + # after the final image raises an error, + # both blatant and borderline + with pytest.raises(EOFError): + im.seek(2) + with pytest.raises(EOFError): + im.seek(523) + # bad calls shouldn't change the frame + assert im.tell() == 0 + # this one will work + im.seek(1) + assert im.tell() == 1 + # and this one, too + im.seek(0) + assert im.tell() == 0 + + +def test_n_frames() -> None: + with Image.open("Tests/images/sugarshack.mpo") as im: + assert im.n_frames == 2 + assert im.is_animated + + +def test_eoferror() -> None: + with Image.open("Tests/images/sugarshack.mpo") as im: + n_frames = im.n_frames + + # Test seeking past the last frame + with pytest.raises(EOFError): + im.seek(n_frames) + assert im.tell() < n_frames + + # Test that seeking to the last frame does not raise an error + im.seek(n_frames - 1) + + +@pytest.mark.parametrize("test_file", test_files) +def test_image_grab(test_file: str) -> None: + with Image.open(test_file) as im: + assert im.tell() == 0 + im0 = im.tobytes() + im.seek(1) + assert im.tell() == 1 + im1 = im.tobytes() + im.seek(0) + assert im.tell() == 0 + im02 = im.tobytes() + assert im0 == im02 + assert im0 != im1 + + +@pytest.mark.parametrize("test_file", test_files) +def test_save(test_file: str) -> None: + with Image.open(test_file) as im: + assert im.tell() == 0 + jpg0 = roundtrip(im) + assert_image_similar(im, jpg0, 30) + im.seek(1) + assert im.tell() == 1 + jpg1 = roundtrip(im) + assert_image_similar(im, jpg1, 30) + + +def test_save_all() -> None: + for test_file in test_files: + with Image.open(test_file) as im: + im_reloaded = roundtrip(im, save_all=True) + + im.seek(0) + assert_image_similar(im, im_reloaded, 30) + + im.seek(1) + im_reloaded.seek(1) + assert_image_similar(im, im_reloaded, 30) + + im = Image.new("RGB", (1, 1)) + im2 = Image.new("RGB", (1, 1), "#f00") + im_reloaded = roundtrip(im, save_all=True, append_images=[im2]) + + assert_image_equal(im, im_reloaded) + assert im_reloaded.mpinfo[45056] == b"0100" + + im_reloaded.seek(1) + assert_image_similar(im2, im_reloaded, 1) + + # Test that a single frame image will not be saved as an MPO + jpg = roundtrip(im, save_all=True) + assert "mp" not in jpg.info diff --git a/Tests/test_file_msp.py b/Tests/test_file_msp.py index b0964aabe..656dd692d 100644 --- a/Tests/test_file_msp.py +++ b/Tests/test_file_msp.py @@ -1,91 +1,91 @@ -from __future__ import annotations - -import os -from pathlib import Path - -import pytest - -from PIL import Image, MspImagePlugin - -from .helper import assert_image_equal, assert_image_equal_tofile, hopper - -TEST_FILE = "Tests/images/hopper.msp" -EXTRA_DIR = "Tests/images/picins" -YA_EXTRA_DIR = "Tests/images/msp" - - -def test_sanity(tmp_path: Path) -> None: - test_file = str(tmp_path / "temp.msp") - - hopper("1").save(test_file) - - with Image.open(test_file) as im: - im.load() - assert im.mode == "1" - assert im.size == (128, 128) - assert im.format == "MSP" - - -def test_invalid_file() -> None: - invalid_file = "Tests/images/flower.jpg" - - with pytest.raises(SyntaxError): - MspImagePlugin.MspImageFile(invalid_file) - - -def test_bad_checksum() -> None: - # Arrange - # This was created by forcing Pillow to save with checksum=0 - bad_checksum = "Tests/images/hopper_bad_checksum.msp" - - # Act / Assert - with pytest.raises(SyntaxError): - MspImagePlugin.MspImageFile(bad_checksum) - - -def test_open_windows_v1() -> None: - # Arrange - # Act - with Image.open(TEST_FILE) as im: - # Assert - assert_image_equal(im, hopper("1")) - assert isinstance(im, MspImagePlugin.MspImageFile) - - -def _assert_file_image_equal(source_path: str, target_path: str) -> None: - with Image.open(source_path) as im: - assert_image_equal_tofile(im, target_path) - - -@pytest.mark.skipif( - not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" -) -def test_open_windows_v2() -> None: - files = ( - os.path.join(EXTRA_DIR, f) - for f in os.listdir(EXTRA_DIR) - if os.path.splitext(f)[1] == ".msp" - ) - for path in files: - _assert_file_image_equal(path, path.replace(".msp", ".png")) - - -@pytest.mark.skipif( - not os.path.exists(YA_EXTRA_DIR), reason="Even More Extra image files not installed" -) -def test_msp_v2() -> None: - for f in os.listdir(YA_EXTRA_DIR): - if ".MSP" not in f: - continue - path = os.path.join(YA_EXTRA_DIR, f) - _assert_file_image_equal(path, path.replace(".MSP", ".png")) - - -def test_cannot_save_wrong_mode(tmp_path: Path) -> None: - # Arrange - im = hopper() - filename = str(tmp_path / "temp.msp") - - # Act/Assert - with pytest.raises(OSError): - im.save(filename) +from __future__ import annotations + +import os +from pathlib import Path + +import pytest + +from PIL import Image, MspImagePlugin + +from .helper import assert_image_equal, assert_image_equal_tofile, hopper + +TEST_FILE = "Tests/images/hopper.msp" +EXTRA_DIR = "Tests/images/picins" +YA_EXTRA_DIR = "Tests/images/msp" + + +def test_sanity(tmp_path: Path) -> None: + test_file = str(tmp_path / "temp.msp") + + hopper("1").save(test_file) + + with Image.open(test_file) as im: + im.load() + assert im.mode == "1" + assert im.size == (128, 128) + assert im.format == "MSP" + + +def test_invalid_file() -> None: + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(SyntaxError): + MspImagePlugin.MspImageFile(invalid_file) + + +def test_bad_checksum() -> None: + # Arrange + # This was created by forcing Pillow to save with checksum=0 + bad_checksum = "Tests/images/hopper_bad_checksum.msp" + + # Act / Assert + with pytest.raises(SyntaxError): + MspImagePlugin.MspImageFile(bad_checksum) + + +def test_open_windows_v1() -> None: + # Arrange + # Act + with Image.open(TEST_FILE) as im: + # Assert + assert_image_equal(im, hopper("1")) + assert isinstance(im, MspImagePlugin.MspImageFile) + + +def _assert_file_image_equal(source_path: str, target_path: str) -> None: + with Image.open(source_path) as im: + assert_image_equal_tofile(im, target_path) + + +@pytest.mark.skipif( + not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" +) +def test_open_windows_v2() -> None: + files = ( + os.path.join(EXTRA_DIR, f) + for f in os.listdir(EXTRA_DIR) + if os.path.splitext(f)[1] == ".msp" + ) + for path in files: + _assert_file_image_equal(path, path.replace(".msp", ".png")) + + +@pytest.mark.skipif( + not os.path.exists(YA_EXTRA_DIR), reason="Even More Extra image files not installed" +) +def test_msp_v2() -> None: + for f in os.listdir(YA_EXTRA_DIR): + if ".MSP" not in f: + continue + path = os.path.join(YA_EXTRA_DIR, f) + _assert_file_image_equal(path, path.replace(".MSP", ".png")) + + +def test_cannot_save_wrong_mode(tmp_path: Path) -> None: + # Arrange + im = hopper() + filename = str(tmp_path / "temp.msp") + + # Act/Assert + with pytest.raises(OSError): + im.save(filename) diff --git a/Tests/test_file_palm.py b/Tests/test_file_palm.py index 194f39b30..3500a1521 100644 --- a/Tests/test_file_palm.py +++ b/Tests/test_file_palm.py @@ -1,71 +1,71 @@ -from __future__ import annotations - -import os.path -import subprocess -from pathlib import Path - -import pytest - -from PIL import Image - -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")) - - # Act - im.save(outfile) - - # Assert - assert os.path.isfile(outfile) - assert os.path.getsize(outfile) > 0 - - -def open_with_magick(magick: list[str], tmp_path: Path, f: str) -> Image.Image: - outfile = str(tmp_path / "temp.png") - rc = subprocess.call( - magick + [f, outfile], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT - ) - assert not rc - return Image.open(outfile) - - -def roundtrip(tmp_path: Path, mode: str) -> None: - magick = magick_command() - if not magick: - return - - im = hopper(mode) - outfile = str(tmp_path / "temp.palm") - - im.save(outfile) - converted = open_with_magick(magick, tmp_path, outfile) - assert_image_equal(converted, im) - - -def test_monochrome(tmp_path: Path) -> None: - # Arrange - mode = "1" - - # Act / Assert - helper_save_as_palm(tmp_path, mode) - roundtrip(tmp_path, mode) - - -@pytest.mark.xfail(reason="Palm P image is wrong") -def test_p_mode(tmp_path: Path) -> None: - # Arrange - mode = "P" - - # Act / Assert - helper_save_as_palm(tmp_path, mode) - roundtrip(tmp_path, mode) - - -@pytest.mark.parametrize("mode", ("L", "RGB")) -def test_oserror(tmp_path: Path, mode: str) -> None: - with pytest.raises(OSError): - helper_save_as_palm(tmp_path, mode) +from __future__ import annotations + +import os.path +import subprocess +from pathlib import Path + +import pytest + +from PIL import Image + +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")) + + # Act + im.save(outfile) + + # Assert + assert os.path.isfile(outfile) + assert os.path.getsize(outfile) > 0 + + +def open_with_magick(magick: list[str], tmp_path: Path, f: str) -> Image.Image: + outfile = str(tmp_path / "temp.png") + rc = subprocess.call( + magick + [f, outfile], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT + ) + assert not rc + return Image.open(outfile) + + +def roundtrip(tmp_path: Path, mode: str) -> None: + magick = magick_command() + if not magick: + return + + im = hopper(mode) + outfile = str(tmp_path / "temp.palm") + + im.save(outfile) + converted = open_with_magick(magick, tmp_path, outfile) + assert_image_equal(converted, im) + + +def test_monochrome(tmp_path: Path) -> None: + # Arrange + mode = "1" + + # Act / Assert + helper_save_as_palm(tmp_path, mode) + roundtrip(tmp_path, mode) + + +@pytest.mark.xfail(reason="Palm P image is wrong") +def test_p_mode(tmp_path: Path) -> None: + # Arrange + mode = "P" + + # Act / Assert + helper_save_as_palm(tmp_path, mode) + roundtrip(tmp_path, mode) + + +@pytest.mark.parametrize("mode", ("L", "RGB")) +def test_oserror(tmp_path: Path, mode: str) -> None: + with pytest.raises(OSError): + helper_save_as_palm(tmp_path, mode) diff --git a/Tests/test_file_pcd.py b/Tests/test_file_pcd.py index 81a316fc1..c63ec8e1c 100644 --- a/Tests/test_file_pcd.py +++ b/Tests/test_file_pcd.py @@ -1,17 +1,17 @@ -from __future__ import annotations - -from PIL import Image - - -def test_load_raw() -> None: - with Image.open("Tests/images/hopper.pcd") as im: - im.load() # should not segfault. - - # Note that this image was created with a resized hopper - # image, which was then converted to pcd with imagemagick - # and the colors are wonky in Pillow. It's unclear if this - # is a pillow or a convert issue, as other images not generated - # from convert look find on pillow and not imagemagick. - - # target = hopper().resize((768,512)) - # assert_image_similar(im, target, 10) +from __future__ import annotations + +from PIL import Image + + +def test_load_raw() -> None: + with Image.open("Tests/images/hopper.pcd") as im: + im.load() # should not segfault. + + # Note that this image was created with a resized hopper + # image, which was then converted to pcd with imagemagick + # and the colors are wonky in Pillow. It's unclear if this + # is a pillow or a convert issue, as other images not generated + # from convert look find on pillow and not imagemagick. + + # target = hopper().resize((768,512)) + # assert_image_similar(im, target, 10) diff --git a/Tests/test_file_pcx.py b/Tests/test_file_pcx.py index ab9f9663e..01caf42e9 100644 --- a/Tests/test_file_pcx.py +++ b/Tests/test_file_pcx.py @@ -1,160 +1,160 @@ -from __future__ import annotations - -from pathlib import Path - -import pytest - -from PIL import Image, ImageFile, PcxImagePlugin - -from .helper import assert_image_equal, hopper - - -def _roundtrip(tmp_path: Path, im: Image.Image) -> None: - f = str(tmp_path / "temp.pcx") - im.save(f) - with Image.open(f) as im2: - assert im2.mode == im.mode - assert im2.size == im.size - assert im2.format == "PCX" - assert im2.get_format_mimetype() == "image/x-pcx" - assert_image_equal(im2, im) - - -def test_sanity(tmp_path: Path) -> None: - for mode in ("1", "L", "P", "RGB"): - _roundtrip(tmp_path, hopper(mode)) - - # Test a palette with less than 256 colors - im = Image.new("P", (1, 1)) - im.putpalette((255, 0, 0)) - _roundtrip(tmp_path, im) - - # Test an unsupported mode - f = str(tmp_path / "temp.pcx") - im = hopper("RGBA") - with pytest.raises(ValueError): - im.save(f) - - -def test_invalid_file() -> None: - invalid_file = "Tests/images/flower.jpg" - - with pytest.raises(SyntaxError): - PcxImagePlugin.PcxImageFile(invalid_file) - - -@pytest.mark.parametrize("mode", ("1", "L", "P", "RGB")) -def test_odd(tmp_path: Path, mode: str) -> None: - # See issue #523, odd sized images should have a stride that's even. - # Not that ImageMagick or GIMP write PCX that way. - # We were not handling properly. - # larger, odd sized images are better here to ensure that - # we handle interrupted scan lines properly. - _roundtrip(tmp_path, hopper(mode).resize((511, 511))) - - -def test_odd_read() -> None: - # Reading an image with an odd stride, making it malformed - with Image.open("Tests/images/odd_stride.pcx") as im: - im.load() - - assert im.size == (371, 150) - - -def test_pil184() -> None: - # Check reading of files where xmin/xmax is not zero. - - test_file = "Tests/images/pil184.pcx" - with Image.open(test_file) as im: - assert im.size == (447, 144) - assert im.tile[0][1] == (0, 0, 447, 144) - - # Make sure all pixels are either 0 or 255. - assert im.histogram()[0] + im.histogram()[255] == 447 * 144 - - -def test_1px_width(tmp_path: Path) -> None: - im = Image.new("L", (1, 256)) - px = im.load() - for y in range(256): - px[0, y] = y - _roundtrip(tmp_path, im) - - -def test_large_count(tmp_path: Path) -> None: - im = Image.new("L", (256, 1)) - px = im.load() - for x in range(256): - px[x, 0] = x // 67 * 67 - _roundtrip(tmp_path, im) - - -def _test_buffer_overflow(tmp_path: Path, im: Image.Image, size: int = 1024) -> None: - _last = ImageFile.MAXBLOCK - ImageFile.MAXBLOCK = size - try: - _roundtrip(tmp_path, im) - finally: - ImageFile.MAXBLOCK = _last - - -def test_break_in_count_overflow(tmp_path: Path) -> None: - im = Image.new("L", (256, 5)) - px = im.load() - for y in range(4): - for x in range(256): - px[x, y] = x % 128 - _test_buffer_overflow(tmp_path, im) - - -def test_break_one_in_loop(tmp_path: Path) -> None: - im = Image.new("L", (256, 5)) - px = im.load() - for y in range(5): - for x in range(256): - px[x, y] = x % 128 - _test_buffer_overflow(tmp_path, im) - - -def test_break_many_in_loop(tmp_path: Path) -> None: - im = Image.new("L", (256, 5)) - px = im.load() - for y in range(4): - for x in range(256): - px[x, y] = x % 128 - for x in range(8): - px[x, 4] = 16 - _test_buffer_overflow(tmp_path, im) - - -def test_break_one_at_end(tmp_path: Path) -> None: - im = Image.new("L", (256, 5)) - px = im.load() - for y in range(5): - for x in range(256): - px[x, y] = x % 128 - px[0, 3] = 128 + 64 - _test_buffer_overflow(tmp_path, im) - - -def test_break_many_at_end(tmp_path: Path) -> None: - im = Image.new("L", (256, 5)) - px = im.load() - for y in range(5): - for x in range(256): - px[x, y] = x % 128 - for x in range(4): - px[x * 2, 3] = 128 + 64 - px[x + 256 - 4, 3] = 0 - _test_buffer_overflow(tmp_path, im) - - -def test_break_padding(tmp_path: Path) -> None: - im = Image.new("L", (257, 5)) - px = im.load() - for y in range(5): - for x in range(257): - px[x, y] = x % 128 - for x in range(5): - px[x, 3] = 0 - _test_buffer_overflow(tmp_path, im) +from __future__ import annotations + +from pathlib import Path + +import pytest + +from PIL import Image, ImageFile, PcxImagePlugin + +from .helper import assert_image_equal, hopper + + +def _roundtrip(tmp_path: Path, im: Image.Image) -> None: + f = str(tmp_path / "temp.pcx") + im.save(f) + with Image.open(f) as im2: + assert im2.mode == im.mode + assert im2.size == im.size + assert im2.format == "PCX" + assert im2.get_format_mimetype() == "image/x-pcx" + assert_image_equal(im2, im) + + +def test_sanity(tmp_path: Path) -> None: + for mode in ("1", "L", "P", "RGB"): + _roundtrip(tmp_path, hopper(mode)) + + # Test a palette with less than 256 colors + im = Image.new("P", (1, 1)) + im.putpalette((255, 0, 0)) + _roundtrip(tmp_path, im) + + # Test an unsupported mode + f = str(tmp_path / "temp.pcx") + im = hopper("RGBA") + with pytest.raises(ValueError): + im.save(f) + + +def test_invalid_file() -> None: + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(SyntaxError): + PcxImagePlugin.PcxImageFile(invalid_file) + + +@pytest.mark.parametrize("mode", ("1", "L", "P", "RGB")) +def test_odd(tmp_path: Path, mode: str) -> None: + # See issue #523, odd sized images should have a stride that's even. + # Not that ImageMagick or GIMP write PCX that way. + # We were not handling properly. + # larger, odd sized images are better here to ensure that + # we handle interrupted scan lines properly. + _roundtrip(tmp_path, hopper(mode).resize((511, 511))) + + +def test_odd_read() -> None: + # Reading an image with an odd stride, making it malformed + with Image.open("Tests/images/odd_stride.pcx") as im: + im.load() + + assert im.size == (371, 150) + + +def test_pil184() -> None: + # Check reading of files where xmin/xmax is not zero. + + test_file = "Tests/images/pil184.pcx" + with Image.open(test_file) as im: + assert im.size == (447, 144) + assert im.tile[0][1] == (0, 0, 447, 144) + + # Make sure all pixels are either 0 or 255. + assert im.histogram()[0] + im.histogram()[255] == 447 * 144 + + +def test_1px_width(tmp_path: Path) -> None: + im = Image.new("L", (1, 256)) + px = im.load() + for y in range(256): + px[0, y] = y + _roundtrip(tmp_path, im) + + +def test_large_count(tmp_path: Path) -> None: + im = Image.new("L", (256, 1)) + px = im.load() + for x in range(256): + px[x, 0] = x // 67 * 67 + _roundtrip(tmp_path, im) + + +def _test_buffer_overflow(tmp_path: Path, im: Image.Image, size: int = 1024) -> None: + _last = ImageFile.MAXBLOCK + ImageFile.MAXBLOCK = size + try: + _roundtrip(tmp_path, im) + finally: + ImageFile.MAXBLOCK = _last + + +def test_break_in_count_overflow(tmp_path: Path) -> None: + im = Image.new("L", (256, 5)) + px = im.load() + for y in range(4): + for x in range(256): + px[x, y] = x % 128 + _test_buffer_overflow(tmp_path, im) + + +def test_break_one_in_loop(tmp_path: Path) -> None: + im = Image.new("L", (256, 5)) + px = im.load() + for y in range(5): + for x in range(256): + px[x, y] = x % 128 + _test_buffer_overflow(tmp_path, im) + + +def test_break_many_in_loop(tmp_path: Path) -> None: + im = Image.new("L", (256, 5)) + px = im.load() + for y in range(4): + for x in range(256): + px[x, y] = x % 128 + for x in range(8): + px[x, 4] = 16 + _test_buffer_overflow(tmp_path, im) + + +def test_break_one_at_end(tmp_path: Path) -> None: + im = Image.new("L", (256, 5)) + px = im.load() + for y in range(5): + for x in range(256): + px[x, y] = x % 128 + px[0, 3] = 128 + 64 + _test_buffer_overflow(tmp_path, im) + + +def test_break_many_at_end(tmp_path: Path) -> None: + im = Image.new("L", (256, 5)) + px = im.load() + for y in range(5): + for x in range(256): + px[x, y] = x % 128 + for x in range(4): + px[x * 2, 3] = 128 + 64 + px[x + 256 - 4, 3] = 0 + _test_buffer_overflow(tmp_path, im) + + +def test_break_padding(tmp_path: Path) -> None: + im = Image.new("L", (257, 5)) + px = im.load() + for y in range(5): + for x in range(257): + px[x, y] = x % 128 + for x in range(5): + px[x, 3] = 0 + _test_buffer_overflow(tmp_path, im) diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index d39a86565..bc337b9bb 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -1,349 +1,349 @@ -from __future__ import annotations - -import io -import os -import os.path -import tempfile -import time -from pathlib import Path -from typing import Any, Generator - -import pytest - -from PIL import Image, PdfParser, features - -from .helper import hopper, mark_if_feature_version, skip_unless_feature - - -def helper_save_as_pdf(tmp_path: Path, mode: str, **kwargs: Any) -> str: - # Arrange - im = hopper(mode) - outfile = str(tmp_path / ("temp_" + mode + ".pdf")) - - # Act - im.save(outfile, **kwargs) - - # Assert - assert os.path.isfile(outfile) - assert os.path.getsize(outfile) > 0 - with PdfParser.PdfParser(outfile) as pdf: - if kwargs.get("append_images", False) or kwargs.get("append", False): - assert len(pdf.pages) > 1 - else: - assert len(pdf.pages) > 0 - with open(outfile, "rb") as fp: - contents = fp.read() - size = tuple( - float(d) for d in contents.split(b"/MediaBox [ 0 0 ")[1].split(b"]")[0].split() - ) - assert im.size == size - - return outfile - - -@pytest.mark.parametrize("mode", ("L", "P", "RGB", "CMYK")) -def test_save(tmp_path: Path, mode: str) -> None: - helper_save_as_pdf(tmp_path, mode) - - -@skip_unless_feature("jpg_2000") -@pytest.mark.parametrize("mode", ("LA", "RGBA")) -def test_save_alpha(tmp_path: Path, mode: str) -> None: - helper_save_as_pdf(tmp_path, mode) - - -def test_p_alpha(tmp_path: Path) -> None: - # Arrange - outfile = str(tmp_path / "temp.pdf") - with Image.open("Tests/images/pil123p.png") as im: - assert im.mode == "P" - assert isinstance(im.info["transparency"], bytes) - - # Act - im.save(outfile) - - # Assert - with open(outfile, "rb") as fp: - contents = fp.read() - assert b"\n/SMask " in contents - - -def test_monochrome(tmp_path: Path) -> None: - # Arrange - mode = "1" - - # Act / Assert - outfile = helper_save_as_pdf(tmp_path, mode) - assert os.path.getsize(outfile) < (5000 if features.check("libtiff") else 15000) - - -def test_unsupported_mode(tmp_path: Path) -> None: - im = hopper("PA") - outfile = str(tmp_path / "temp_PA.pdf") - - with pytest.raises(ValueError): - im.save(outfile) - - -def test_resolution(tmp_path: Path) -> None: - im = hopper() - - outfile = str(tmp_path / "temp.pdf") - im.save(outfile, resolution=150) - - with open(outfile, "rb") as fp: - contents = fp.read() - - size = tuple( - float(d) - for d in contents.split(b"stream\nq ")[1].split(b" 0 0 cm")[0].split(b" 0 0 ") - ) - assert size == (61.44, 61.44) - - size = tuple( - float(d) for d in contents.split(b"/MediaBox [ 0 0 ")[1].split(b"]")[0].split() - ) - assert size == (61.44, 61.44) - - -@pytest.mark.parametrize( - "params", - ( - {"dpi": (75, 150)}, - {"dpi": (75, 150), "resolution": 200}, - ), -) -def test_dpi(params: dict[str, int | tuple[int, int]], tmp_path: Path) -> None: - im = hopper() - - outfile = str(tmp_path / "temp.pdf") - im.save(outfile, **params) - - with open(outfile, "rb") as fp: - contents = fp.read() - - size = tuple( - float(d) - for d in contents.split(b"stream\nq ")[1].split(b" 0 0 cm")[0].split(b" 0 0 ") - ) - assert size == (122.88, 61.44) - - size = tuple( - float(d) for d in contents.split(b"/MediaBox [ 0 0 ")[1].split(b"]")[0].split() - ) - assert size == (122.88, 61.44) - - -@mark_if_feature_version( - pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" -) -def test_save_all(tmp_path: Path) -> None: - # Single frame image - helper_save_as_pdf(tmp_path, "RGB", save_all=True) - - # Multiframe image - with Image.open("Tests/images/dispose_bgnd.gif") as im: - outfile = str(tmp_path / "temp.pdf") - im.save(outfile, save_all=True) - - assert os.path.isfile(outfile) - assert os.path.getsize(outfile) > 0 - - # Append images - ims = [hopper()] - im.copy().save(outfile, save_all=True, append_images=ims) - - assert os.path.isfile(outfile) - assert os.path.getsize(outfile) > 0 - - # Test appending using a generator - def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]: - yield from ims - - im.save(outfile, save_all=True, append_images=im_generator(ims)) - - assert os.path.isfile(outfile) - assert os.path.getsize(outfile) > 0 - - # Append JPEG images - with Image.open("Tests/images/flower.jpg") as jpeg: - jpeg.save(outfile, save_all=True, append_images=[jpeg.copy()]) - - assert os.path.isfile(outfile) - assert os.path.getsize(outfile) > 0 - - -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") - im.save(outfile) - - assert os.path.isfile(outfile) - assert os.path.getsize(outfile) > 0 - - -def test_pdf_open(tmp_path: Path) -> None: - # fail on a buffer full of null bytes - with pytest.raises(PdfParser.PdfFormatError): - PdfParser.PdfParser(buf=bytearray(65536)) - - # make an empty PDF object - with PdfParser.PdfParser() as empty_pdf: - assert len(empty_pdf.pages) == 0 - assert len(empty_pdf.info) == 0 - assert not empty_pdf.should_close_buf - assert not empty_pdf.should_close_file - - # make a PDF file - pdf_filename = helper_save_as_pdf(tmp_path, "RGB") - - # open the PDF file - with PdfParser.PdfParser(filename=pdf_filename) as hopper_pdf: - assert len(hopper_pdf.pages) == 1 - assert hopper_pdf.should_close_buf - assert hopper_pdf.should_close_file - - # read a PDF file from a buffer with a non-zero offset - with open(pdf_filename, "rb") as f: - content = b"xyzzy" + f.read() - with PdfParser.PdfParser(buf=content, start_offset=5) as hopper_pdf: - assert len(hopper_pdf.pages) == 1 - assert not hopper_pdf.should_close_buf - assert not hopper_pdf.should_close_file - - # read a PDF file from an already open file - with open(pdf_filename, "rb") as f: - with PdfParser.PdfParser(f=f) as hopper_pdf: - assert len(hopper_pdf.pages) == 1 - assert hopper_pdf.should_close_buf - assert not hopper_pdf.should_close_file - - -def test_pdf_append_fails_on_nonexistent_file() -> None: - im = hopper("RGB") - with tempfile.TemporaryDirectory() as temp_dir: - with pytest.raises(OSError): - im.save(os.path.join(temp_dir, "nonexistent.pdf"), append=True) - - -def check_pdf_pages_consistency(pdf: PdfParser.PdfParser) -> None: - pages_info = pdf.read_indirect(pdf.pages_ref) - assert b"Parent" not in pages_info - assert b"Kids" in pages_info - kids_not_used = pages_info[b"Kids"] - for page_ref in pdf.pages: - while True: - if page_ref in kids_not_used: - kids_not_used.remove(page_ref) - page_info = pdf.read_indirect(page_ref) - assert b"Parent" in page_info - page_ref = page_info[b"Parent"] - if page_ref == pdf.pages_ref: - break - assert pdf.pages_ref == page_info[b"Parent"] - assert kids_not_used == [] - - -def test_pdf_append(tmp_path: Path) -> None: - # make a PDF file - pdf_filename = helper_save_as_pdf(tmp_path, "RGB", producer="PdfParser") - - # open it, check pages and info - with PdfParser.PdfParser(pdf_filename, mode="r+b") as pdf: - assert len(pdf.pages) == 1 - assert len(pdf.info) == 4 - assert pdf.info.Title == os.path.splitext(os.path.basename(pdf_filename))[0] - assert pdf.info.Producer == "PdfParser" - assert b"CreationDate" in pdf.info - assert b"ModDate" in pdf.info - check_pdf_pages_consistency(pdf) - - # append some info - pdf.info.Title = "abc" - pdf.info.Author = "def" - pdf.info.Subject = "ghi\uABCD" - pdf.info.Keywords = "qw)e\\r(ty" - pdf.info.Creator = "hopper()" - pdf.start_writing() - pdf.write_xref_and_trailer() - - # open it again, check pages and info again - with PdfParser.PdfParser(pdf_filename) as pdf: - assert len(pdf.pages) == 1 - assert len(pdf.info) == 8 - assert pdf.info.Title == "abc" - assert b"CreationDate" in pdf.info - assert b"ModDate" in pdf.info - check_pdf_pages_consistency(pdf) - - # append two images - mode_cmyk = hopper("CMYK") - mode_p = hopper("P") - mode_cmyk.save(pdf_filename, append=True, save_all=True, append_images=[mode_p]) - - # open the PDF again, check pages and info again - with PdfParser.PdfParser(pdf_filename) as pdf: - assert len(pdf.pages) == 3 - assert len(pdf.info) == 8 - assert PdfParser.decode_text(pdf.info[b"Title"]) == "abc" - assert pdf.info.Title == "abc" - assert pdf.info.Producer == "PdfParser" - assert pdf.info.Keywords == "qw)e\\r(ty" - assert pdf.info.Subject == "ghi\uABCD" - assert b"CreationDate" in pdf.info - assert b"ModDate" in pdf.info - check_pdf_pages_consistency(pdf) - - -def test_pdf_info(tmp_path: Path) -> None: - # make a PDF file - pdf_filename = helper_save_as_pdf( - tmp_path, - "RGB", - title="title", - author="author", - subject="subject", - keywords="keywords", - creator="creator", - producer="producer", - creationDate=time.strptime("2000", "%Y"), - modDate=time.strptime("2001", "%Y"), - ) - - # open it, check pages and info - with PdfParser.PdfParser(pdf_filename) as pdf: - assert len(pdf.info) == 8 - assert pdf.info.Title == "title" - assert pdf.info.Author == "author" - assert pdf.info.Subject == "subject" - assert pdf.info.Keywords == "keywords" - assert pdf.info.Creator == "creator" - assert pdf.info.Producer == "producer" - assert pdf.info.CreationDate == time.strptime("2000", "%Y") - assert pdf.info.ModDate == time.strptime("2001", "%Y") - check_pdf_pages_consistency(pdf) - - -def test_pdf_append_to_bytesio() -> None: - im = hopper("RGB") - f = io.BytesIO() - im.save(f, format="PDF") - initial_size = len(f.getvalue()) - assert initial_size > 0 - im = hopper("P") - f = io.BytesIO(f.getvalue()) - im.save(f, format="PDF", append=True) - assert len(f.getvalue()) > initial_size - - -@pytest.mark.timeout(1) -@pytest.mark.skipif("PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower") -@pytest.mark.parametrize("newline", (b"\r", b"\n")) -def test_redos(newline: bytes) -> None: - malicious = b" trailer<<>>" + newline * 3456 - - # This particular exception isn't relevant here. - # The important thing is it doesn't timeout, cause a ReDoS (CVE-2021-25292). - with pytest.raises(PdfParser.PdfFormatError): - PdfParser.PdfParser(buf=malicious) +from __future__ import annotations + +import io +import os +import os.path +import tempfile +import time +from pathlib import Path +from typing import Any, Generator + +import pytest + +from PIL import Image, PdfParser, features + +from .helper import hopper, mark_if_feature_version, skip_unless_feature + + +def helper_save_as_pdf(tmp_path: Path, mode: str, **kwargs: Any) -> str: + # Arrange + im = hopper(mode) + outfile = str(tmp_path / ("temp_" + mode + ".pdf")) + + # Act + im.save(outfile, **kwargs) + + # Assert + assert os.path.isfile(outfile) + assert os.path.getsize(outfile) > 0 + with PdfParser.PdfParser(outfile) as pdf: + if kwargs.get("append_images", False) or kwargs.get("append", False): + assert len(pdf.pages) > 1 + else: + assert len(pdf.pages) > 0 + with open(outfile, "rb") as fp: + contents = fp.read() + size = tuple( + float(d) for d in contents.split(b"/MediaBox [ 0 0 ")[1].split(b"]")[0].split() + ) + assert im.size == size + + return outfile + + +@pytest.mark.parametrize("mode", ("L", "P", "RGB", "CMYK")) +def test_save(tmp_path: Path, mode: str) -> None: + helper_save_as_pdf(tmp_path, mode) + + +@skip_unless_feature("jpg_2000") +@pytest.mark.parametrize("mode", ("LA", "RGBA")) +def test_save_alpha(tmp_path: Path, mode: str) -> None: + helper_save_as_pdf(tmp_path, mode) + + +def test_p_alpha(tmp_path: Path) -> None: + # Arrange + outfile = str(tmp_path / "temp.pdf") + with Image.open("Tests/images/pil123p.png") as im: + assert im.mode == "P" + assert isinstance(im.info["transparency"], bytes) + + # Act + im.save(outfile) + + # Assert + with open(outfile, "rb") as fp: + contents = fp.read() + assert b"\n/SMask " in contents + + +def test_monochrome(tmp_path: Path) -> None: + # Arrange + mode = "1" + + # Act / Assert + outfile = helper_save_as_pdf(tmp_path, mode) + assert os.path.getsize(outfile) < (5000 if features.check("libtiff") else 15000) + + +def test_unsupported_mode(tmp_path: Path) -> None: + im = hopper("PA") + outfile = str(tmp_path / "temp_PA.pdf") + + with pytest.raises(ValueError): + im.save(outfile) + + +def test_resolution(tmp_path: Path) -> None: + im = hopper() + + outfile = str(tmp_path / "temp.pdf") + im.save(outfile, resolution=150) + + with open(outfile, "rb") as fp: + contents = fp.read() + + size = tuple( + float(d) + for d in contents.split(b"stream\nq ")[1].split(b" 0 0 cm")[0].split(b" 0 0 ") + ) + assert size == (61.44, 61.44) + + size = tuple( + float(d) for d in contents.split(b"/MediaBox [ 0 0 ")[1].split(b"]")[0].split() + ) + assert size == (61.44, 61.44) + + +@pytest.mark.parametrize( + "params", + ( + {"dpi": (75, 150)}, + {"dpi": (75, 150), "resolution": 200}, + ), +) +def test_dpi(params: dict[str, int | tuple[int, int]], tmp_path: Path) -> None: + im = hopper() + + outfile = str(tmp_path / "temp.pdf") + im.save(outfile, **params) + + with open(outfile, "rb") as fp: + contents = fp.read() + + size = tuple( + float(d) + for d in contents.split(b"stream\nq ")[1].split(b" 0 0 cm")[0].split(b" 0 0 ") + ) + assert size == (122.88, 61.44) + + size = tuple( + float(d) for d in contents.split(b"/MediaBox [ 0 0 ")[1].split(b"]")[0].split() + ) + assert size == (122.88, 61.44) + + +@mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" +) +def test_save_all(tmp_path: Path) -> None: + # Single frame image + helper_save_as_pdf(tmp_path, "RGB", save_all=True) + + # Multiframe image + with Image.open("Tests/images/dispose_bgnd.gif") as im: + outfile = str(tmp_path / "temp.pdf") + im.save(outfile, save_all=True) + + assert os.path.isfile(outfile) + assert os.path.getsize(outfile) > 0 + + # Append images + ims = [hopper()] + im.copy().save(outfile, save_all=True, append_images=ims) + + assert os.path.isfile(outfile) + assert os.path.getsize(outfile) > 0 + + # Test appending using a generator + def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]: + yield from ims + + im.save(outfile, save_all=True, append_images=im_generator(ims)) + + assert os.path.isfile(outfile) + assert os.path.getsize(outfile) > 0 + + # Append JPEG images + with Image.open("Tests/images/flower.jpg") as jpeg: + jpeg.save(outfile, save_all=True, append_images=[jpeg.copy()]) + + assert os.path.isfile(outfile) + assert os.path.getsize(outfile) > 0 + + +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") + im.save(outfile) + + assert os.path.isfile(outfile) + assert os.path.getsize(outfile) > 0 + + +def test_pdf_open(tmp_path: Path) -> None: + # fail on a buffer full of null bytes + with pytest.raises(PdfParser.PdfFormatError): + PdfParser.PdfParser(buf=bytearray(65536)) + + # make an empty PDF object + with PdfParser.PdfParser() as empty_pdf: + assert len(empty_pdf.pages) == 0 + assert len(empty_pdf.info) == 0 + assert not empty_pdf.should_close_buf + assert not empty_pdf.should_close_file + + # make a PDF file + pdf_filename = helper_save_as_pdf(tmp_path, "RGB") + + # open the PDF file + with PdfParser.PdfParser(filename=pdf_filename) as hopper_pdf: + assert len(hopper_pdf.pages) == 1 + assert hopper_pdf.should_close_buf + assert hopper_pdf.should_close_file + + # read a PDF file from a buffer with a non-zero offset + with open(pdf_filename, "rb") as f: + content = b"xyzzy" + f.read() + with PdfParser.PdfParser(buf=content, start_offset=5) as hopper_pdf: + assert len(hopper_pdf.pages) == 1 + assert not hopper_pdf.should_close_buf + assert not hopper_pdf.should_close_file + + # read a PDF file from an already open file + with open(pdf_filename, "rb") as f: + with PdfParser.PdfParser(f=f) as hopper_pdf: + assert len(hopper_pdf.pages) == 1 + assert hopper_pdf.should_close_buf + assert not hopper_pdf.should_close_file + + +def test_pdf_append_fails_on_nonexistent_file() -> None: + im = hopper("RGB") + with tempfile.TemporaryDirectory() as temp_dir: + with pytest.raises(OSError): + im.save(os.path.join(temp_dir, "nonexistent.pdf"), append=True) + + +def check_pdf_pages_consistency(pdf: PdfParser.PdfParser) -> None: + pages_info = pdf.read_indirect(pdf.pages_ref) + assert b"Parent" not in pages_info + assert b"Kids" in pages_info + kids_not_used = pages_info[b"Kids"] + for page_ref in pdf.pages: + while True: + if page_ref in kids_not_used: + kids_not_used.remove(page_ref) + page_info = pdf.read_indirect(page_ref) + assert b"Parent" in page_info + page_ref = page_info[b"Parent"] + if page_ref == pdf.pages_ref: + break + assert pdf.pages_ref == page_info[b"Parent"] + assert kids_not_used == [] + + +def test_pdf_append(tmp_path: Path) -> None: + # make a PDF file + pdf_filename = helper_save_as_pdf(tmp_path, "RGB", producer="PdfParser") + + # open it, check pages and info + with PdfParser.PdfParser(pdf_filename, mode="r+b") as pdf: + assert len(pdf.pages) == 1 + assert len(pdf.info) == 4 + assert pdf.info.Title == os.path.splitext(os.path.basename(pdf_filename))[0] + assert pdf.info.Producer == "PdfParser" + assert b"CreationDate" in pdf.info + assert b"ModDate" in pdf.info + check_pdf_pages_consistency(pdf) + + # append some info + pdf.info.Title = "abc" + pdf.info.Author = "def" + pdf.info.Subject = "ghi\uABCD" + pdf.info.Keywords = "qw)e\\r(ty" + pdf.info.Creator = "hopper()" + pdf.start_writing() + pdf.write_xref_and_trailer() + + # open it again, check pages and info again + with PdfParser.PdfParser(pdf_filename) as pdf: + assert len(pdf.pages) == 1 + assert len(pdf.info) == 8 + assert pdf.info.Title == "abc" + assert b"CreationDate" in pdf.info + assert b"ModDate" in pdf.info + check_pdf_pages_consistency(pdf) + + # append two images + mode_cmyk = hopper("CMYK") + mode_p = hopper("P") + mode_cmyk.save(pdf_filename, append=True, save_all=True, append_images=[mode_p]) + + # open the PDF again, check pages and info again + with PdfParser.PdfParser(pdf_filename) as pdf: + assert len(pdf.pages) == 3 + assert len(pdf.info) == 8 + assert PdfParser.decode_text(pdf.info[b"Title"]) == "abc" + assert pdf.info.Title == "abc" + assert pdf.info.Producer == "PdfParser" + assert pdf.info.Keywords == "qw)e\\r(ty" + assert pdf.info.Subject == "ghi\uABCD" + assert b"CreationDate" in pdf.info + assert b"ModDate" in pdf.info + check_pdf_pages_consistency(pdf) + + +def test_pdf_info(tmp_path: Path) -> None: + # make a PDF file + pdf_filename = helper_save_as_pdf( + tmp_path, + "RGB", + title="title", + author="author", + subject="subject", + keywords="keywords", + creator="creator", + producer="producer", + creationDate=time.strptime("2000", "%Y"), + modDate=time.strptime("2001", "%Y"), + ) + + # open it, check pages and info + with PdfParser.PdfParser(pdf_filename) as pdf: + assert len(pdf.info) == 8 + assert pdf.info.Title == "title" + assert pdf.info.Author == "author" + assert pdf.info.Subject == "subject" + assert pdf.info.Keywords == "keywords" + assert pdf.info.Creator == "creator" + assert pdf.info.Producer == "producer" + assert pdf.info.CreationDate == time.strptime("2000", "%Y") + assert pdf.info.ModDate == time.strptime("2001", "%Y") + check_pdf_pages_consistency(pdf) + + +def test_pdf_append_to_bytesio() -> None: + im = hopper("RGB") + f = io.BytesIO() + im.save(f, format="PDF") + initial_size = len(f.getvalue()) + assert initial_size > 0 + im = hopper("P") + f = io.BytesIO(f.getvalue()) + im.save(f, format="PDF", append=True) + assert len(f.getvalue()) > initial_size + + +@pytest.mark.timeout(1) +@pytest.mark.skipif("PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower") +@pytest.mark.parametrize("newline", (b"\r", b"\n")) +def test_redos(newline: bytes) -> None: + malicious = b" trailer<<>>" + newline * 3456 + + # This particular exception isn't relevant here. + # The important thing is it doesn't timeout, cause a ReDoS (CVE-2021-25292). + with pytest.raises(PdfParser.PdfFormatError): + PdfParser.PdfParser(buf=malicious) diff --git a/Tests/test_file_pixar.py b/Tests/test_file_pixar.py index 8f208cfbf..a8de7ebcd 100644 --- a/Tests/test_file_pixar.py +++ b/Tests/test_file_pixar.py @@ -1,28 +1,28 @@ -from __future__ import annotations - -import pytest - -from PIL import Image, PixarImagePlugin - -from .helper import assert_image_similar, hopper - -TEST_FILE = "Tests/images/hopper.pxr" - - -def test_sanity() -> None: - with Image.open(TEST_FILE) as im: - im.load() - assert im.mode == "RGB" - assert im.size == (128, 128) - assert im.format == "PIXAR" - assert im.get_format_mimetype() is None - - im2 = hopper() - assert_image_similar(im, im2, 4.8) - - -def test_invalid_file() -> None: - invalid_file = "Tests/images/flower.jpg" - - with pytest.raises(SyntaxError): - PixarImagePlugin.PixarImageFile(invalid_file) +from __future__ import annotations + +import pytest + +from PIL import Image, PixarImagePlugin + +from .helper import assert_image_similar, hopper + +TEST_FILE = "Tests/images/hopper.pxr" + + +def test_sanity() -> None: + with Image.open(TEST_FILE) as im: + im.load() + assert im.mode == "RGB" + assert im.size == (128, 128) + assert im.format == "PIXAR" + assert im.get_format_mimetype() is None + + im2 = hopper() + assert_image_similar(im, im2, 4.8) + + +def test_invalid_file() -> None: + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(SyntaxError): + PixarImagePlugin.PixarImageFile(invalid_file) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index c7c9f6fab..f4329f456 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -1,826 +1,826 @@ -from __future__ import annotations - -import re -import sys -import warnings -import zlib -from io import BytesIO -from pathlib import Path -from types import ModuleType -from typing import Any, cast - -import pytest - -from PIL import Image, ImageFile, PngImagePlugin, features - -from .helper import ( - PillowLeakTestCase, - assert_image, - assert_image_equal, - assert_image_equal_tofile, - hopper, - is_win32, - mark_if_feature_version, - skip_unless_feature, -) - -ElementTree: ModuleType | None -try: - from defusedxml import ElementTree -except ImportError: - ElementTree = None - -# sample png stream - -TEST_PNG_FILE = "Tests/images/hopper.png" - -# stuff to create inline PNG images - -MAGIC = PngImagePlugin._MAGIC - - -def chunk(cid: bytes, *data: bytes) -> bytes: - test_file = BytesIO() - PngImagePlugin.putchunk(*(test_file, cid) + data) - return test_file.getvalue() - - -o32 = PngImagePlugin.o32 - -IHDR = chunk(b"IHDR", o32(1), o32(1), b"\x08\x02", b"\0\0\0") -IDAT = chunk(b"IDAT") -IEND = chunk(b"IEND") - -HEAD = MAGIC + IHDR -TAIL = IDAT + IEND - - -def load(data: bytes) -> Image.Image: - return Image.open(BytesIO(data)) - - -def roundtrip(im: Image.Image, **options: Any) -> PngImagePlugin.PngImageFile: - out = BytesIO() - im.save(out, "PNG", **options) - out.seek(0) - return cast(PngImagePlugin.PngImageFile, Image.open(out)) - - -@skip_unless_feature("zlib") -class TestFilePng: - def get_chunks(self, filename: str) -> list[bytes]: - chunks = [] - with open(filename, "rb") as fp: - fp.read(8) - with PngImagePlugin.PngStream(fp) as png: - while True: - cid, pos, length = png.read() - chunks.append(cid) - try: - s = png.call(cid, pos, length) - except EOFError: - break - png.crc(cid, s) - return chunks - - def test_sanity(self, tmp_path: Path) -> None: - # internal version number - version = features.version_codec("zlib") - assert version is not None - assert re.search(r"\d+(\.\d+){1,3}(\.zlib\-ng)?$", version) - - test_file = str(tmp_path / "temp.png") - - hopper("RGB").save(test_file) - - with Image.open(test_file) as im: - im.load() - assert im.mode == "RGB" - assert im.size == (128, 128) - assert im.format == "PNG" - assert im.get_format_mimetype() == "image/png" - - for mode in ["1", "L", "P", "RGB", "I", "I;16", "I;16B"]: - im = hopper(mode) - im.save(test_file) - with Image.open(test_file) as reloaded: - if mode in ("I", "I;16B"): - reloaded = reloaded.convert(mode) - assert_image_equal(reloaded, im) - - def test_invalid_file(self) -> None: - invalid_file = "Tests/images/flower.jpg" - - with pytest.raises(SyntaxError): - PngImagePlugin.PngImageFile(invalid_file) - - def test_broken(self) -> None: - # Check reading of totally broken files. In this case, the test - # file was checked into Subversion as a text file. - - test_file = "Tests/images/broken.png" - with pytest.raises(OSError): - with Image.open(test_file): - pass - - def test_bad_text(self) -> None: - # Make sure PIL can read malformed tEXt chunks (@PIL152) - - im = load(HEAD + chunk(b"tEXt") + TAIL) - assert im.info == {} - - im = load(HEAD + chunk(b"tEXt", b"spam") + TAIL) - assert im.info == {"spam": ""} - - im = load(HEAD + chunk(b"tEXt", b"spam\0") + TAIL) - assert im.info == {"spam": ""} - - im = load(HEAD + chunk(b"tEXt", b"spam\0egg") + TAIL) - assert im.info == {"spam": "egg"} - - im = load(HEAD + chunk(b"tEXt", b"spam\0egg\0") + TAIL) - assert im.info == {"spam": "egg\x00"} - - def test_bad_ztxt(self) -> None: - # Test reading malformed zTXt chunks (python-pillow/Pillow#318) - - im = load(HEAD + chunk(b"zTXt") + TAIL) - assert im.info == {} - - im = load(HEAD + chunk(b"zTXt", b"spam") + TAIL) - assert im.info == {"spam": ""} - - im = load(HEAD + chunk(b"zTXt", b"spam\0") + TAIL) - assert im.info == {"spam": ""} - - im = load(HEAD + chunk(b"zTXt", b"spam\0\0") + TAIL) - assert im.info == {"spam": ""} - - im = load(HEAD + chunk(b"zTXt", b"spam\0\0" + zlib.compress(b"egg")[:1]) + TAIL) - assert im.info == {"spam": ""} - - im = load(HEAD + chunk(b"zTXt", b"spam\0\0" + zlib.compress(b"egg")) + TAIL) - assert im.info == {"spam": "egg"} - - def test_bad_itxt(self) -> None: - im = load(HEAD + chunk(b"iTXt") + TAIL) - assert im.info == {} - - im = load(HEAD + chunk(b"iTXt", b"spam") + TAIL) - assert im.info == {} - - im = load(HEAD + chunk(b"iTXt", b"spam\0") + TAIL) - assert im.info == {} - - im = load(HEAD + chunk(b"iTXt", b"spam\0\x02") + TAIL) - assert im.info == {} - - im = load(HEAD + chunk(b"iTXt", b"spam\0\0\0foo\0") + TAIL) - assert im.info == {} - - im = load(HEAD + chunk(b"iTXt", b"spam\0\0\0en\0Spam\0egg") + TAIL) - assert im.info == {"spam": "egg"} - assert im.info["spam"].lang == "en" - assert im.info["spam"].tkey == "Spam" - - im = load( - HEAD - + chunk(b"iTXt", b"spam\0\1\0en\0Spam\0" + zlib.compress(b"egg")[:1]) - + TAIL - ) - assert im.info == {"spam": ""} - - im = load( - HEAD - + chunk(b"iTXt", b"spam\0\1\1en\0Spam\0" + zlib.compress(b"egg")) - + TAIL - ) - assert im.info == {} - - im = load( - HEAD - + chunk(b"iTXt", b"spam\0\1\0en\0Spam\0" + zlib.compress(b"egg")) - + TAIL - ) - assert im.info == {"spam": "egg"} - assert im.info["spam"].lang == "en" - assert im.info["spam"].tkey == "Spam" - - def test_interlace(self) -> None: - test_file = "Tests/images/pil123p.png" - with Image.open(test_file) as im: - assert_image(im, "P", (162, 150)) - assert im.info.get("interlace") - - im.load() - - test_file = "Tests/images/pil123rgba.png" - with Image.open(test_file) as im: - assert_image(im, "RGBA", (162, 150)) - assert im.info.get("interlace") - - im.load() - - def test_load_transparent_p(self) -> None: - test_file = "Tests/images/pil123p.png" - with Image.open(test_file) as im: - assert_image(im, "P", (162, 150)) - im = im.convert("RGBA") - assert_image(im, "RGBA", (162, 150)) - - # image has 124 unique alpha values - assert len(im.getchannel("A").getcolors()) == 124 - - def test_load_transparent_rgb(self) -> None: - test_file = "Tests/images/rgb_trns.png" - with Image.open(test_file) as im: - assert im.info["transparency"] == (0, 255, 52) - - assert_image(im, "RGB", (64, 64)) - im = im.convert("RGBA") - assert_image(im, "RGBA", (64, 64)) - - # image has 876 transparent pixels - assert im.getchannel("A").getcolors()[0][0] == 876 - - def test_save_p_transparent_palette(self, tmp_path: Path) -> None: - in_file = "Tests/images/pil123p.png" - with Image.open(in_file) as im: - # 'transparency' contains a byte string with the opacity for - # each palette entry - assert len(im.info["transparency"]) == 256 - - test_file = str(tmp_path / "temp.png") - im.save(test_file) - - # check if saved image contains same transparency - with Image.open(test_file) as im: - assert len(im.info["transparency"]) == 256 - - assert_image(im, "P", (162, 150)) - im = im.convert("RGBA") - assert_image(im, "RGBA", (162, 150)) - - # image has 124 unique alpha values - assert len(im.getchannel("A").getcolors()) == 124 - - def test_save_p_single_transparency(self, tmp_path: Path) -> None: - in_file = "Tests/images/p_trns_single.png" - with Image.open(in_file) as im: - # pixel value 164 is full transparent - assert im.info["transparency"] == 164 - assert im.getpixel((31, 31)) == 164 - - test_file = str(tmp_path / "temp.png") - im.save(test_file) - - # check if saved image contains same transparency - with Image.open(test_file) as im: - assert im.info["transparency"] == 164 - assert im.getpixel((31, 31)) == 164 - assert_image(im, "P", (64, 64)) - im = im.convert("RGBA") - assert_image(im, "RGBA", (64, 64)) - - assert im.getpixel((31, 31)) == (0, 255, 52, 0) - - # image has 876 transparent pixels - assert im.getchannel("A").getcolors()[0][0] == 876 - - def test_save_p_transparent_black(self, tmp_path: Path) -> None: - # check if solid black image with full transparency - # is supported (check for #1838) - im = Image.new("RGBA", (10, 10), (0, 0, 0, 0)) - assert im.getcolors() == [(100, (0, 0, 0, 0))] - - im = im.convert("P") - test_file = str(tmp_path / "temp.png") - im.save(test_file) - - # check if saved image contains same transparency - with Image.open(test_file) as im: - assert len(im.info["transparency"]) == 256 - assert_image(im, "P", (10, 10)) - im = im.convert("RGBA") - assert_image(im, "RGBA", (10, 10)) - assert im.getcolors() == [(100, (0, 0, 0, 0))] - - def test_save_grayscale_transparency(self, tmp_path: Path) -> None: - for mode, num_transparent in {"1": 1994, "L": 559, "I;16": 559}.items(): - in_file = "Tests/images/" + mode.split(";")[0].lower() + "_trns.png" - with Image.open(in_file) as im: - assert im.mode == mode - assert im.info["transparency"] == 255 - - im_rgba = im.convert("RGBA") - assert im_rgba.getchannel("A").getcolors()[0][0] == num_transparent - - test_file = str(tmp_path / "temp.png") - im.save(test_file) - - with Image.open(test_file) as test_im: - assert test_im.mode == mode - assert test_im.info["transparency"] == 255 - assert_image_equal(im, test_im) - - test_im_rgba = test_im.convert("RGBA") - assert test_im_rgba.getchannel("A").getcolors()[0][0] == num_transparent - - def test_save_rgb_single_transparency(self, tmp_path: 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") - im.save(test_file) - - def test_load_verify(self) -> None: - # Check open/load/verify exception (@PIL150) - - with Image.open(TEST_PNG_FILE) as im: - # Assert that there is no unclosed file warning - with warnings.catch_warnings(): - im.verify() - - with Image.open(TEST_PNG_FILE) as im: - im.load() - with pytest.raises(RuntimeError): - im.verify() - - def test_verify_struct_error(self) -> None: - # Check open/load/verify exception (#1755) - - # offsets to test, -10: breaks in i32() in read. (OSError) - # -13: breaks in crc, txt chunk. - # -14: malformed chunk - - for offset in (-10, -13, -14): - with open(TEST_PNG_FILE, "rb") as f: - test_file = f.read()[:offset] - - with Image.open(BytesIO(test_file)) as im: - assert im.fp is not None - with pytest.raises((OSError, SyntaxError)): - im.verify() - - def test_verify_ignores_crc_error(self) -> None: - # check ignores crc errors in ancillary chunks - - chunk_data = chunk(b"tEXt", b"spam") - broken_crc_chunk_data = chunk_data[:-1] + b"q" # break CRC - - image_data = HEAD + broken_crc_chunk_data + TAIL - 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 - - def test_verify_not_ignores_crc_error_in_required_chunk(self) -> 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 - - def test_roundtrip_dpi(self) -> None: - # Check dpi roundtripping - - with Image.open(TEST_PNG_FILE) as im: - im = roundtrip(im, dpi=(100.33, 100.33)) - assert im.info["dpi"] == (100.33, 100.33) - - def test_load_float_dpi(self) -> None: - with Image.open(TEST_PNG_FILE) as im: - assert im.info["dpi"] == (95.9866, 95.9866) - - def test_roundtrip_text(self) -> None: - # Check text roundtripping - - with Image.open(TEST_PNG_FILE) as im: - info = PngImagePlugin.PngInfo() - info.add_text("TXT", "VALUE") - info.add_text("ZIP", "VALUE", zip=True) - - im = roundtrip(im, pnginfo=info) - assert im.info == {"TXT": "VALUE", "ZIP": "VALUE"} - assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"} - - def test_roundtrip_itxt(self) -> None: - # Check iTXt roundtripping - - im = Image.new("RGB", (32, 32)) - info = PngImagePlugin.PngInfo() - info.add_itxt("spam", "Eggs", "en", "Spam") - info.add_text("eggs", PngImagePlugin.iTXt("Spam", "en", "Eggs"), zip=True) - - im = roundtrip(im, pnginfo=info) - assert im.info == {"spam": "Eggs", "eggs": "Spam"} - assert im.text == {"spam": "Eggs", "eggs": "Spam"} - assert im.text["spam"].lang == "en" - assert im.text["spam"].tkey == "Spam" - assert im.text["eggs"].lang == "en" - assert im.text["eggs"].tkey == "Eggs" - - def test_nonunicode_text(self) -> None: - # Check so that non-Unicode text is saved as a tEXt rather than iTXt - - im = Image.new("RGB", (32, 32)) - info = PngImagePlugin.PngInfo() - info.add_text("Text", "Ascii") - im = roundtrip(im, pnginfo=info) - assert isinstance(im.info["Text"], str) - - def test_unicode_text(self) -> None: - # Check preservation of non-ASCII characters - - def rt_text(value: str) -> None: - im = Image.new("RGB", (32, 32)) - info = PngImagePlugin.PngInfo() - info.add_text("Text", value) - im = roundtrip(im, pnginfo=info) - assert im.info == {"Text": value} - - rt_text(" Aa" + chr(0xA0) + chr(0xC4) + chr(0xFF)) # Latin1 - rt_text(chr(0x400) + chr(0x472) + chr(0x4FF)) # Cyrillic - # CJK: - rt_text(chr(0x4E00) + chr(0x66F0) + chr(0x9FBA) + chr(0x3042) + chr(0xAC00)) - rt_text("A" + chr(0xC4) + chr(0x472) + chr(0x3042)) # Combined - - def test_scary(self) -> None: - # Check reading of evil PNG file. For information, see: - # http://scary.beasts.org/security/CESA-2004-001.txt - # The first byte is removed from pngtest_bad.png - # to avoid classification as malware. - - with open("Tests/images/pngtest_bad.png.bin", "rb") as fd: - data = b"\x89" + fd.read() - - pngfile = BytesIO(data) - with pytest.raises(OSError): - with Image.open(pngfile): - pass - - def test_trns_rgb(self) -> None: - # Check writing and reading of tRNS chunks for RGB images. - # Independent file sample provided by Sebastian Spaeth. - - test_file = "Tests/images/caption_6_33_22.png" - with Image.open(test_file) as im: - assert im.info["transparency"] == (248, 248, 248) - - # check saving transparency by default - im = roundtrip(im) - assert im.info["transparency"] == (248, 248, 248) - - im = roundtrip(im, transparency=(0, 1, 2)) - assert im.info["transparency"] == (0, 1, 2) - - def test_trns_p(self, tmp_path: Path) -> None: - # Check writing a transparency of 0, issue #528 - im = hopper("P") - im.info["transparency"] = 0 - - f = str(tmp_path / "temp.png") - im.save(f) - - with Image.open(f) as im2: - assert "transparency" in im2.info - - assert_image_equal(im2.convert("RGBA"), im.convert("RGBA")) - - def test_trns_null(self) -> None: - # Check reading images with null tRNS value, issue #1239 - test_file = "Tests/images/tRNS_null_1x1.png" - with Image.open(test_file) as im: - assert im.info["transparency"] == 0 - - def test_save_icc_profile(self) -> None: - with Image.open("Tests/images/icc_profile_none.png") as im: - assert im.info["icc_profile"] is None - - with Image.open("Tests/images/icc_profile.png") as with_icc: - expected_icc = with_icc.info["icc_profile"] - - im = roundtrip(im, icc_profile=expected_icc) - assert im.info["icc_profile"] == expected_icc - - def test_discard_icc_profile(self) -> None: - with Image.open("Tests/images/icc_profile.png") as im: - assert "icc_profile" in im.info - - 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/icc_profile.png") 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/icc_profile_none.png") as im: - assert im.info["icc_profile"] is None - - im = roundtrip(im) - assert "icc_profile" not in im.info - - def test_repr_png(self) -> None: - im = hopper() - - with Image.open(BytesIO(im._repr_png_())) as repr_png: - assert repr_png.format == "PNG" - assert_image_equal(im, repr_png) - - def test_repr_png_error_returns_none(self) -> None: - im = hopper("F") - - assert im._repr_png_() is None - - def test_chunk_order(self, tmp_path: Path) -> None: - with Image.open("Tests/images/icc_profile.png") as im: - test_file = str(tmp_path / "temp.png") - im.convert("P").save(test_file, dpi=(100, 100)) - - chunks = self.get_chunks(test_file) - - # https://www.w3.org/TR/PNG/#5ChunkOrdering - # IHDR - shall be first - assert chunks.index(b"IHDR") == 0 - # PLTE - before first IDAT - assert chunks.index(b"PLTE") < chunks.index(b"IDAT") - # iCCP - before PLTE and IDAT - assert chunks.index(b"iCCP") < chunks.index(b"PLTE") - assert chunks.index(b"iCCP") < chunks.index(b"IDAT") - # tRNS - after PLTE, before IDAT - assert chunks.index(b"tRNS") > chunks.index(b"PLTE") - assert chunks.index(b"tRNS") < chunks.index(b"IDAT") - # pHYs - before IDAT - assert chunks.index(b"pHYs") < chunks.index(b"IDAT") - - def test_getchunks(self) -> None: - im = hopper() - - chunks = PngImagePlugin.getchunks(im) - assert len(chunks) == 3 - - def test_read_private_chunks(self) -> None: - with Image.open("Tests/images/exif.png") as im: - assert im.private_chunks == [(b"orNT", b"\x01")] - - def test_roundtrip_private_chunk(self) -> None: - # Check private chunk roundtripping - - with Image.open(TEST_PNG_FILE) as im: - info = PngImagePlugin.PngInfo() - info.add(b"prIV", b"VALUE") - info.add(b"atEC", b"VALUE2") - info.add(b"prIV", b"VALUE3", True) - - im = roundtrip(im, pnginfo=info) - assert im.private_chunks == [(b"prIV", b"VALUE"), (b"atEC", b"VALUE2")] - im.load() - assert im.private_chunks == [ - (b"prIV", b"VALUE"), - (b"atEC", b"VALUE2"), - (b"prIV", b"VALUE3", True), - ] - - def test_textual_chunks_after_idat(self) -> None: - with Image.open("Tests/images/hopper.png") as im: - assert "comment" in im.text - for k, v in { - "date:create": "2014-09-04T09:37:08+03:00", - "date:modify": "2014-09-04T09:37:08+03:00", - }.items(): - assert im.text[k] == v - - # Raises a SyntaxError in load_end - with Image.open("Tests/images/broken_data_stream.png") as im: - with pytest.raises(OSError): - assert isinstance(im.text, dict) - - # Raises a UnicodeDecodeError in load_end - with Image.open("Tests/images/truncated_image.png") as im: - # The file is truncated - with pytest.raises(OSError): - im.text() - ImageFile.LOAD_TRUNCATED_IMAGES = True - assert isinstance(im.text, dict) - ImageFile.LOAD_TRUNCATED_IMAGES = False - - # Raises an EOFError in load_end - with Image.open("Tests/images/hopper_idat_after_image_end.png") as im: - assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"} - - def test_unknown_compression_method(self) -> None: - with pytest.raises(SyntaxError, match="Unknown compression method"): - PngImagePlugin.PngImageFile("Tests/images/unknown_compression_method.png") - - def test_padded_idat(self) -> None: - # This image has been manually hexedited - # so that the IDAT chunk has padding at the end - # Set MAXBLOCK to the length of the actual data - # so that the decoder finishes reading before the chunk ends - MAXBLOCK = ImageFile.MAXBLOCK - ImageFile.MAXBLOCK = 45 - ImageFile.LOAD_TRUNCATED_IMAGES = True - - with Image.open("Tests/images/padded_idat.png") as im: - im.load() - - ImageFile.MAXBLOCK = MAXBLOCK - ImageFile.LOAD_TRUNCATED_IMAGES = False - - assert_image_equal_tofile(im, "Tests/images/bw_gradient.png") - - @pytest.mark.parametrize( - "cid", (b"IHDR", b"sRGB", b"pHYs", b"acTL", b"fcTL", b"fdAT") - ) - def test_truncated_chunks(self, cid: bytes) -> None: - fp = BytesIO() - with PngImagePlugin.PngStream(fp) as png: - with pytest.raises(ValueError): - png.call(cid, 0, 0) - - ImageFile.LOAD_TRUNCATED_IMAGES = True - png.call(cid, 0, 0) - ImageFile.LOAD_TRUNCATED_IMAGES = False - - def test_specify_bits(self, tmp_path: Path) -> None: - im = hopper("P") - - out = str(tmp_path / "temp.png") - im.save(out, bits=4) - - with Image.open(out) as reloaded: - assert len(reloaded.png.im_palette[1]) == 48 - - def test_plte_length(self, tmp_path: Path) -> None: - im = Image.new("P", (1, 1)) - im.putpalette((1, 1, 1)) - - out = str(tmp_path / "temp.png") - im.save(str(tmp_path / "temp.png")) - - with Image.open(out) as reloaded: - assert len(reloaded.png.im_palette[1]) == 3 - - def test_getxmp(self) -> None: - with Image.open("Tests/images/color_snakes.png") as im: - if ElementTree is None: - with pytest.warns( - UserWarning, - match="XMP data cannot be read without defusedxml dependency", - ): - assert im.getxmp() == {} - else: - xmp = im.getxmp() - - description = xmp["xmpmeta"]["RDF"]["Description"] - assert description["PixelXDimension"] == "10" - assert description["subject"]["Seq"] is None - - def test_exif(self) -> None: - # With an EXIF chunk - with Image.open("Tests/images/exif.png") as im: - exif = im._getexif() - assert exif[274] == 1 - - # With an ImageMagick zTXt chunk - with Image.open("Tests/images/exif_imagemagick.png") as im: - exif = im._getexif() - assert exif[274] == 1 - - # Assert that info still can be extracted - # when the image is no longer a PngImageFile instance - exif = im.copy().getexif() - assert exif[274] == 1 - - # With a tEXt chunk - with Image.open("Tests/images/exif_text.png") as im: - exif = im._getexif() - assert exif[274] == 1 - - # With XMP tags - with Image.open("Tests/images/xmp_tags_orientation.png") as im: - exif = im.getexif() - assert exif[274] == 3 - - def test_exif_save(self, tmp_path: Path) -> None: - # Test exif is not saved from info - test_file = str(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 reloaded._getexif() is None - - # Test passing in exif - with Image.open("Tests/images/exif.png") as im: - im.save(test_file, exif=im.getexif()) - - with Image.open(test_file) as reloaded: - exif = reloaded._getexif() - assert exif[274] == 1 - - @mark_if_feature_version( - pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" - ) - def test_exif_from_jpg(self, tmp_path: Path) -> None: - with Image.open("Tests/images/pil_sample_rgb.jpg") as im: - test_file = str(tmp_path / "temp.png") - im.save(test_file, exif=im.getexif()) - - with Image.open(test_file) as reloaded: - exif = reloaded._getexif() - assert exif[305] == "Adobe Photoshop CS Macintosh" - - def test_exif_argument(self, tmp_path: Path) -> None: - with Image.open(TEST_PNG_FILE) as im: - test_file = str(tmp_path / "temp.png") - im.save(test_file, exif=b"exifstring") - - with Image.open(test_file) as reloaded: - assert reloaded.info["exif"] == b"Exif\x00\x00exifstring" - - def test_tell(self) -> None: - with Image.open(TEST_PNG_FILE) as im: - assert im.tell() == 0 - - def test_seek(self) -> None: - with Image.open(TEST_PNG_FILE) as im: - im.seek(0) - - with pytest.raises(EOFError): - im.seek(1) - - @pytest.mark.parametrize("buffer", (True, False)) - def test_save_stdout(self, buffer: bool) -> None: - old_stdout = sys.stdout - - if buffer: - - class MyStdOut: - buffer = BytesIO() - - mystdout = MyStdOut() - else: - mystdout = BytesIO() - - sys.stdout = mystdout - - with Image.open(TEST_PNG_FILE) as im: - im.save(sys.stdout, "PNG") - - # Reset stdout - sys.stdout = old_stdout - - if buffer: - mystdout = mystdout.buffer - with Image.open(mystdout) as reloaded: - assert_image_equal_tofile(reloaded, TEST_PNG_FILE) - - def test_truncated_end_chunk(self) -> 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 - - -@pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS") -@skip_unless_feature("zlib") -class TestTruncatedPngPLeaks(PillowLeakTestCase): - mem_limit = 2 * 1024 # max increase in K - iterations = 100 # Leak is 56k/iteration, this will leak 5.6megs - - def test_leak_load(self) -> None: - with open("Tests/images/hopper.png", "rb") as f: - DATA = BytesIO(f.read(16 * 1024)) - - ImageFile.LOAD_TRUNCATED_IMAGES = True - with Image.open(DATA) as im: - im.load() - - def core() -> None: - with Image.open(DATA) as im: - im.load() - - try: - self._test_leak(core) - finally: - ImageFile.LOAD_TRUNCATED_IMAGES = False +from __future__ import annotations + +import re +import sys +import warnings +import zlib +from io import BytesIO +from pathlib import Path +from types import ModuleType +from typing import Any, cast + +import pytest + +from PIL import Image, ImageFile, PngImagePlugin, features + +from .helper import ( + PillowLeakTestCase, + assert_image, + assert_image_equal, + assert_image_equal_tofile, + hopper, + is_win32, + mark_if_feature_version, + skip_unless_feature, +) + +ElementTree: ModuleType | None +try: + from defusedxml import ElementTree +except ImportError: + ElementTree = None + +# sample png stream + +TEST_PNG_FILE = "Tests/images/hopper.png" + +# stuff to create inline PNG images + +MAGIC = PngImagePlugin._MAGIC + + +def chunk(cid: bytes, *data: bytes) -> bytes: + test_file = BytesIO() + PngImagePlugin.putchunk(*(test_file, cid) + data) + return test_file.getvalue() + + +o32 = PngImagePlugin.o32 + +IHDR = chunk(b"IHDR", o32(1), o32(1), b"\x08\x02", b"\0\0\0") +IDAT = chunk(b"IDAT") +IEND = chunk(b"IEND") + +HEAD = MAGIC + IHDR +TAIL = IDAT + IEND + + +def load(data: bytes) -> Image.Image: + return Image.open(BytesIO(data)) + + +def roundtrip(im: Image.Image, **options: Any) -> PngImagePlugin.PngImageFile: + out = BytesIO() + im.save(out, "PNG", **options) + out.seek(0) + return cast(PngImagePlugin.PngImageFile, Image.open(out)) + + +@skip_unless_feature("zlib") +class TestFilePng: + def get_chunks(self, filename: str) -> list[bytes]: + chunks = [] + with open(filename, "rb") as fp: + fp.read(8) + with PngImagePlugin.PngStream(fp) as png: + while True: + cid, pos, length = png.read() + chunks.append(cid) + try: + s = png.call(cid, pos, length) + except EOFError: + break + png.crc(cid, s) + return chunks + + def test_sanity(self, tmp_path: Path) -> None: + # internal version number + version = features.version_codec("zlib") + assert version is not None + assert re.search(r"\d+(\.\d+){1,3}(\.zlib\-ng)?$", version) + + test_file = str(tmp_path / "temp.png") + + hopper("RGB").save(test_file) + + with Image.open(test_file) as im: + im.load() + assert im.mode == "RGB" + assert im.size == (128, 128) + assert im.format == "PNG" + assert im.get_format_mimetype() == "image/png" + + for mode in ["1", "L", "P", "RGB", "I", "I;16", "I;16B"]: + im = hopper(mode) + im.save(test_file) + with Image.open(test_file) as reloaded: + if mode in ("I", "I;16B"): + reloaded = reloaded.convert(mode) + assert_image_equal(reloaded, im) + + def test_invalid_file(self) -> None: + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(SyntaxError): + PngImagePlugin.PngImageFile(invalid_file) + + def test_broken(self) -> None: + # Check reading of totally broken files. In this case, the test + # file was checked into Subversion as a text file. + + test_file = "Tests/images/broken.png" + with pytest.raises(OSError): + with Image.open(test_file): + pass + + def test_bad_text(self) -> None: + # Make sure PIL can read malformed tEXt chunks (@PIL152) + + im = load(HEAD + chunk(b"tEXt") + TAIL) + assert im.info == {} + + im = load(HEAD + chunk(b"tEXt", b"spam") + TAIL) + assert im.info == {"spam": ""} + + im = load(HEAD + chunk(b"tEXt", b"spam\0") + TAIL) + assert im.info == {"spam": ""} + + im = load(HEAD + chunk(b"tEXt", b"spam\0egg") + TAIL) + assert im.info == {"spam": "egg"} + + im = load(HEAD + chunk(b"tEXt", b"spam\0egg\0") + TAIL) + assert im.info == {"spam": "egg\x00"} + + def test_bad_ztxt(self) -> None: + # Test reading malformed zTXt chunks (python-pillow/Pillow#318) + + im = load(HEAD + chunk(b"zTXt") + TAIL) + assert im.info == {} + + im = load(HEAD + chunk(b"zTXt", b"spam") + TAIL) + assert im.info == {"spam": ""} + + im = load(HEAD + chunk(b"zTXt", b"spam\0") + TAIL) + assert im.info == {"spam": ""} + + im = load(HEAD + chunk(b"zTXt", b"spam\0\0") + TAIL) + assert im.info == {"spam": ""} + + im = load(HEAD + chunk(b"zTXt", b"spam\0\0" + zlib.compress(b"egg")[:1]) + TAIL) + assert im.info == {"spam": ""} + + im = load(HEAD + chunk(b"zTXt", b"spam\0\0" + zlib.compress(b"egg")) + TAIL) + assert im.info == {"spam": "egg"} + + def test_bad_itxt(self) -> None: + im = load(HEAD + chunk(b"iTXt") + TAIL) + assert im.info == {} + + im = load(HEAD + chunk(b"iTXt", b"spam") + TAIL) + assert im.info == {} + + im = load(HEAD + chunk(b"iTXt", b"spam\0") + TAIL) + assert im.info == {} + + im = load(HEAD + chunk(b"iTXt", b"spam\0\x02") + TAIL) + assert im.info == {} + + im = load(HEAD + chunk(b"iTXt", b"spam\0\0\0foo\0") + TAIL) + assert im.info == {} + + im = load(HEAD + chunk(b"iTXt", b"spam\0\0\0en\0Spam\0egg") + TAIL) + assert im.info == {"spam": "egg"} + assert im.info["spam"].lang == "en" + assert im.info["spam"].tkey == "Spam" + + im = load( + HEAD + + chunk(b"iTXt", b"spam\0\1\0en\0Spam\0" + zlib.compress(b"egg")[:1]) + + TAIL + ) + assert im.info == {"spam": ""} + + im = load( + HEAD + + chunk(b"iTXt", b"spam\0\1\1en\0Spam\0" + zlib.compress(b"egg")) + + TAIL + ) + assert im.info == {} + + im = load( + HEAD + + chunk(b"iTXt", b"spam\0\1\0en\0Spam\0" + zlib.compress(b"egg")) + + TAIL + ) + assert im.info == {"spam": "egg"} + assert im.info["spam"].lang == "en" + assert im.info["spam"].tkey == "Spam" + + def test_interlace(self) -> None: + test_file = "Tests/images/pil123p.png" + with Image.open(test_file) as im: + assert_image(im, "P", (162, 150)) + assert im.info.get("interlace") + + im.load() + + test_file = "Tests/images/pil123rgba.png" + with Image.open(test_file) as im: + assert_image(im, "RGBA", (162, 150)) + assert im.info.get("interlace") + + im.load() + + def test_load_transparent_p(self) -> None: + test_file = "Tests/images/pil123p.png" + with Image.open(test_file) as im: + assert_image(im, "P", (162, 150)) + im = im.convert("RGBA") + assert_image(im, "RGBA", (162, 150)) + + # image has 124 unique alpha values + assert len(im.getchannel("A").getcolors()) == 124 + + def test_load_transparent_rgb(self) -> None: + test_file = "Tests/images/rgb_trns.png" + with Image.open(test_file) as im: + assert im.info["transparency"] == (0, 255, 52) + + assert_image(im, "RGB", (64, 64)) + im = im.convert("RGBA") + assert_image(im, "RGBA", (64, 64)) + + # image has 876 transparent pixels + assert im.getchannel("A").getcolors()[0][0] == 876 + + def test_save_p_transparent_palette(self, tmp_path: Path) -> None: + in_file = "Tests/images/pil123p.png" + with Image.open(in_file) as im: + # 'transparency' contains a byte string with the opacity for + # each palette entry + assert len(im.info["transparency"]) == 256 + + test_file = str(tmp_path / "temp.png") + im.save(test_file) + + # check if saved image contains same transparency + with Image.open(test_file) as im: + assert len(im.info["transparency"]) == 256 + + assert_image(im, "P", (162, 150)) + im = im.convert("RGBA") + assert_image(im, "RGBA", (162, 150)) + + # image has 124 unique alpha values + assert len(im.getchannel("A").getcolors()) == 124 + + def test_save_p_single_transparency(self, tmp_path: Path) -> None: + in_file = "Tests/images/p_trns_single.png" + with Image.open(in_file) as im: + # pixel value 164 is full transparent + assert im.info["transparency"] == 164 + assert im.getpixel((31, 31)) == 164 + + test_file = str(tmp_path / "temp.png") + im.save(test_file) + + # check if saved image contains same transparency + with Image.open(test_file) as im: + assert im.info["transparency"] == 164 + assert im.getpixel((31, 31)) == 164 + assert_image(im, "P", (64, 64)) + im = im.convert("RGBA") + assert_image(im, "RGBA", (64, 64)) + + assert im.getpixel((31, 31)) == (0, 255, 52, 0) + + # image has 876 transparent pixels + assert im.getchannel("A").getcolors()[0][0] == 876 + + def test_save_p_transparent_black(self, tmp_path: Path) -> None: + # check if solid black image with full transparency + # is supported (check for #1838) + im = Image.new("RGBA", (10, 10), (0, 0, 0, 0)) + assert im.getcolors() == [(100, (0, 0, 0, 0))] + + im = im.convert("P") + test_file = str(tmp_path / "temp.png") + im.save(test_file) + + # check if saved image contains same transparency + with Image.open(test_file) as im: + assert len(im.info["transparency"]) == 256 + assert_image(im, "P", (10, 10)) + im = im.convert("RGBA") + assert_image(im, "RGBA", (10, 10)) + assert im.getcolors() == [(100, (0, 0, 0, 0))] + + def test_save_grayscale_transparency(self, tmp_path: Path) -> None: + for mode, num_transparent in {"1": 1994, "L": 559, "I;16": 559}.items(): + in_file = "Tests/images/" + mode.split(";")[0].lower() + "_trns.png" + with Image.open(in_file) as im: + assert im.mode == mode + assert im.info["transparency"] == 255 + + im_rgba = im.convert("RGBA") + assert im_rgba.getchannel("A").getcolors()[0][0] == num_transparent + + test_file = str(tmp_path / "temp.png") + im.save(test_file) + + with Image.open(test_file) as test_im: + assert test_im.mode == mode + assert test_im.info["transparency"] == 255 + assert_image_equal(im, test_im) + + test_im_rgba = test_im.convert("RGBA") + assert test_im_rgba.getchannel("A").getcolors()[0][0] == num_transparent + + def test_save_rgb_single_transparency(self, tmp_path: 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") + im.save(test_file) + + def test_load_verify(self) -> None: + # Check open/load/verify exception (@PIL150) + + with Image.open(TEST_PNG_FILE) as im: + # Assert that there is no unclosed file warning + with warnings.catch_warnings(): + im.verify() + + with Image.open(TEST_PNG_FILE) as im: + im.load() + with pytest.raises(RuntimeError): + im.verify() + + def test_verify_struct_error(self) -> None: + # Check open/load/verify exception (#1755) + + # offsets to test, -10: breaks in i32() in read. (OSError) + # -13: breaks in crc, txt chunk. + # -14: malformed chunk + + for offset in (-10, -13, -14): + with open(TEST_PNG_FILE, "rb") as f: + test_file = f.read()[:offset] + + with Image.open(BytesIO(test_file)) as im: + assert im.fp is not None + with pytest.raises((OSError, SyntaxError)): + im.verify() + + def test_verify_ignores_crc_error(self) -> None: + # check ignores crc errors in ancillary chunks + + chunk_data = chunk(b"tEXt", b"spam") + broken_crc_chunk_data = chunk_data[:-1] + b"q" # break CRC + + image_data = HEAD + broken_crc_chunk_data + TAIL + 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 + + def test_verify_not_ignores_crc_error_in_required_chunk(self) -> 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 + + def test_roundtrip_dpi(self) -> None: + # Check dpi roundtripping + + with Image.open(TEST_PNG_FILE) as im: + im = roundtrip(im, dpi=(100.33, 100.33)) + assert im.info["dpi"] == (100.33, 100.33) + + def test_load_float_dpi(self) -> None: + with Image.open(TEST_PNG_FILE) as im: + assert im.info["dpi"] == (95.9866, 95.9866) + + def test_roundtrip_text(self) -> None: + # Check text roundtripping + + with Image.open(TEST_PNG_FILE) as im: + info = PngImagePlugin.PngInfo() + info.add_text("TXT", "VALUE") + info.add_text("ZIP", "VALUE", zip=True) + + im = roundtrip(im, pnginfo=info) + assert im.info == {"TXT": "VALUE", "ZIP": "VALUE"} + assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"} + + def test_roundtrip_itxt(self) -> None: + # Check iTXt roundtripping + + im = Image.new("RGB", (32, 32)) + info = PngImagePlugin.PngInfo() + info.add_itxt("spam", "Eggs", "en", "Spam") + info.add_text("eggs", PngImagePlugin.iTXt("Spam", "en", "Eggs"), zip=True) + + im = roundtrip(im, pnginfo=info) + assert im.info == {"spam": "Eggs", "eggs": "Spam"} + assert im.text == {"spam": "Eggs", "eggs": "Spam"} + assert im.text["spam"].lang == "en" + assert im.text["spam"].tkey == "Spam" + assert im.text["eggs"].lang == "en" + assert im.text["eggs"].tkey == "Eggs" + + def test_nonunicode_text(self) -> None: + # Check so that non-Unicode text is saved as a tEXt rather than iTXt + + im = Image.new("RGB", (32, 32)) + info = PngImagePlugin.PngInfo() + info.add_text("Text", "Ascii") + im = roundtrip(im, pnginfo=info) + assert isinstance(im.info["Text"], str) + + def test_unicode_text(self) -> None: + # Check preservation of non-ASCII characters + + def rt_text(value: str) -> None: + im = Image.new("RGB", (32, 32)) + info = PngImagePlugin.PngInfo() + info.add_text("Text", value) + im = roundtrip(im, pnginfo=info) + assert im.info == {"Text": value} + + rt_text(" Aa" + chr(0xA0) + chr(0xC4) + chr(0xFF)) # Latin1 + rt_text(chr(0x400) + chr(0x472) + chr(0x4FF)) # Cyrillic + # CJK: + rt_text(chr(0x4E00) + chr(0x66F0) + chr(0x9FBA) + chr(0x3042) + chr(0xAC00)) + rt_text("A" + chr(0xC4) + chr(0x472) + chr(0x3042)) # Combined + + def test_scary(self) -> None: + # Check reading of evil PNG file. For information, see: + # http://scary.beasts.org/security/CESA-2004-001.txt + # The first byte is removed from pngtest_bad.png + # to avoid classification as malware. + + with open("Tests/images/pngtest_bad.png.bin", "rb") as fd: + data = b"\x89" + fd.read() + + pngfile = BytesIO(data) + with pytest.raises(OSError): + with Image.open(pngfile): + pass + + def test_trns_rgb(self) -> None: + # Check writing and reading of tRNS chunks for RGB images. + # Independent file sample provided by Sebastian Spaeth. + + test_file = "Tests/images/caption_6_33_22.png" + with Image.open(test_file) as im: + assert im.info["transparency"] == (248, 248, 248) + + # check saving transparency by default + im = roundtrip(im) + assert im.info["transparency"] == (248, 248, 248) + + im = roundtrip(im, transparency=(0, 1, 2)) + assert im.info["transparency"] == (0, 1, 2) + + def test_trns_p(self, tmp_path: Path) -> None: + # Check writing a transparency of 0, issue #528 + im = hopper("P") + im.info["transparency"] = 0 + + f = str(tmp_path / "temp.png") + im.save(f) + + with Image.open(f) as im2: + assert "transparency" in im2.info + + assert_image_equal(im2.convert("RGBA"), im.convert("RGBA")) + + def test_trns_null(self) -> None: + # Check reading images with null tRNS value, issue #1239 + test_file = "Tests/images/tRNS_null_1x1.png" + with Image.open(test_file) as im: + assert im.info["transparency"] == 0 + + def test_save_icc_profile(self) -> None: + with Image.open("Tests/images/icc_profile_none.png") as im: + assert im.info["icc_profile"] is None + + with Image.open("Tests/images/icc_profile.png") as with_icc: + expected_icc = with_icc.info["icc_profile"] + + im = roundtrip(im, icc_profile=expected_icc) + assert im.info["icc_profile"] == expected_icc + + def test_discard_icc_profile(self) -> None: + with Image.open("Tests/images/icc_profile.png") as im: + assert "icc_profile" in im.info + + 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/icc_profile.png") 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/icc_profile_none.png") as im: + assert im.info["icc_profile"] is None + + im = roundtrip(im) + assert "icc_profile" not in im.info + + def test_repr_png(self) -> None: + im = hopper() + + with Image.open(BytesIO(im._repr_png_())) as repr_png: + assert repr_png.format == "PNG" + assert_image_equal(im, repr_png) + + def test_repr_png_error_returns_none(self) -> None: + im = hopper("F") + + assert im._repr_png_() is None + + def test_chunk_order(self, tmp_path: Path) -> None: + with Image.open("Tests/images/icc_profile.png") as im: + test_file = str(tmp_path / "temp.png") + im.convert("P").save(test_file, dpi=(100, 100)) + + chunks = self.get_chunks(test_file) + + # https://www.w3.org/TR/PNG/#5ChunkOrdering + # IHDR - shall be first + assert chunks.index(b"IHDR") == 0 + # PLTE - before first IDAT + assert chunks.index(b"PLTE") < chunks.index(b"IDAT") + # iCCP - before PLTE and IDAT + assert chunks.index(b"iCCP") < chunks.index(b"PLTE") + assert chunks.index(b"iCCP") < chunks.index(b"IDAT") + # tRNS - after PLTE, before IDAT + assert chunks.index(b"tRNS") > chunks.index(b"PLTE") + assert chunks.index(b"tRNS") < chunks.index(b"IDAT") + # pHYs - before IDAT + assert chunks.index(b"pHYs") < chunks.index(b"IDAT") + + def test_getchunks(self) -> None: + im = hopper() + + chunks = PngImagePlugin.getchunks(im) + assert len(chunks) == 3 + + def test_read_private_chunks(self) -> None: + with Image.open("Tests/images/exif.png") as im: + assert im.private_chunks == [(b"orNT", b"\x01")] + + def test_roundtrip_private_chunk(self) -> None: + # Check private chunk roundtripping + + with Image.open(TEST_PNG_FILE) as im: + info = PngImagePlugin.PngInfo() + info.add(b"prIV", b"VALUE") + info.add(b"atEC", b"VALUE2") + info.add(b"prIV", b"VALUE3", True) + + im = roundtrip(im, pnginfo=info) + assert im.private_chunks == [(b"prIV", b"VALUE"), (b"atEC", b"VALUE2")] + im.load() + assert im.private_chunks == [ + (b"prIV", b"VALUE"), + (b"atEC", b"VALUE2"), + (b"prIV", b"VALUE3", True), + ] + + def test_textual_chunks_after_idat(self) -> None: + with Image.open("Tests/images/hopper.png") as im: + assert "comment" in im.text + for k, v in { + "date:create": "2014-09-04T09:37:08+03:00", + "date:modify": "2014-09-04T09:37:08+03:00", + }.items(): + assert im.text[k] == v + + # Raises a SyntaxError in load_end + with Image.open("Tests/images/broken_data_stream.png") as im: + with pytest.raises(OSError): + assert isinstance(im.text, dict) + + # Raises a UnicodeDecodeError in load_end + with Image.open("Tests/images/truncated_image.png") as im: + # The file is truncated + with pytest.raises(OSError): + im.text() + ImageFile.LOAD_TRUNCATED_IMAGES = True + assert isinstance(im.text, dict) + ImageFile.LOAD_TRUNCATED_IMAGES = False + + # Raises an EOFError in load_end + with Image.open("Tests/images/hopper_idat_after_image_end.png") as im: + assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"} + + def test_unknown_compression_method(self) -> None: + with pytest.raises(SyntaxError, match="Unknown compression method"): + PngImagePlugin.PngImageFile("Tests/images/unknown_compression_method.png") + + def test_padded_idat(self) -> None: + # This image has been manually hexedited + # so that the IDAT chunk has padding at the end + # Set MAXBLOCK to the length of the actual data + # so that the decoder finishes reading before the chunk ends + MAXBLOCK = ImageFile.MAXBLOCK + ImageFile.MAXBLOCK = 45 + ImageFile.LOAD_TRUNCATED_IMAGES = True + + with Image.open("Tests/images/padded_idat.png") as im: + im.load() + + ImageFile.MAXBLOCK = MAXBLOCK + ImageFile.LOAD_TRUNCATED_IMAGES = False + + assert_image_equal_tofile(im, "Tests/images/bw_gradient.png") + + @pytest.mark.parametrize( + "cid", (b"IHDR", b"sRGB", b"pHYs", b"acTL", b"fcTL", b"fdAT") + ) + def test_truncated_chunks(self, cid: bytes) -> None: + fp = BytesIO() + with PngImagePlugin.PngStream(fp) as png: + with pytest.raises(ValueError): + png.call(cid, 0, 0) + + ImageFile.LOAD_TRUNCATED_IMAGES = True + png.call(cid, 0, 0) + ImageFile.LOAD_TRUNCATED_IMAGES = False + + def test_specify_bits(self, tmp_path: Path) -> None: + im = hopper("P") + + out = str(tmp_path / "temp.png") + im.save(out, bits=4) + + with Image.open(out) as reloaded: + assert len(reloaded.png.im_palette[1]) == 48 + + def test_plte_length(self, tmp_path: Path) -> None: + im = Image.new("P", (1, 1)) + im.putpalette((1, 1, 1)) + + out = str(tmp_path / "temp.png") + im.save(str(tmp_path / "temp.png")) + + with Image.open(out) as reloaded: + assert len(reloaded.png.im_palette[1]) == 3 + + def test_getxmp(self) -> None: + with Image.open("Tests/images/color_snakes.png") as im: + if ElementTree is None: + with pytest.warns( + UserWarning, + match="XMP data cannot be read without defusedxml dependency", + ): + assert im.getxmp() == {} + else: + xmp = im.getxmp() + + description = xmp["xmpmeta"]["RDF"]["Description"] + assert description["PixelXDimension"] == "10" + assert description["subject"]["Seq"] is None + + def test_exif(self) -> None: + # With an EXIF chunk + with Image.open("Tests/images/exif.png") as im: + exif = im._getexif() + assert exif[274] == 1 + + # With an ImageMagick zTXt chunk + with Image.open("Tests/images/exif_imagemagick.png") as im: + exif = im._getexif() + assert exif[274] == 1 + + # Assert that info still can be extracted + # when the image is no longer a PngImageFile instance + exif = im.copy().getexif() + assert exif[274] == 1 + + # With a tEXt chunk + with Image.open("Tests/images/exif_text.png") as im: + exif = im._getexif() + assert exif[274] == 1 + + # With XMP tags + with Image.open("Tests/images/xmp_tags_orientation.png") as im: + exif = im.getexif() + assert exif[274] == 3 + + def test_exif_save(self, tmp_path: Path) -> None: + # Test exif is not saved from info + test_file = str(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 reloaded._getexif() is None + + # Test passing in exif + with Image.open("Tests/images/exif.png") as im: + im.save(test_file, exif=im.getexif()) + + with Image.open(test_file) as reloaded: + exif = reloaded._getexif() + assert exif[274] == 1 + + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) + def test_exif_from_jpg(self, tmp_path: Path) -> None: + with Image.open("Tests/images/pil_sample_rgb.jpg") as im: + test_file = str(tmp_path / "temp.png") + im.save(test_file, exif=im.getexif()) + + with Image.open(test_file) as reloaded: + exif = reloaded._getexif() + assert exif[305] == "Adobe Photoshop CS Macintosh" + + def test_exif_argument(self, tmp_path: Path) -> None: + with Image.open(TEST_PNG_FILE) as im: + test_file = str(tmp_path / "temp.png") + im.save(test_file, exif=b"exifstring") + + with Image.open(test_file) as reloaded: + assert reloaded.info["exif"] == b"Exif\x00\x00exifstring" + + def test_tell(self) -> None: + with Image.open(TEST_PNG_FILE) as im: + assert im.tell() == 0 + + def test_seek(self) -> None: + with Image.open(TEST_PNG_FILE) as im: + im.seek(0) + + with pytest.raises(EOFError): + im.seek(1) + + @pytest.mark.parametrize("buffer", (True, False)) + def test_save_stdout(self, buffer: bool) -> None: + old_stdout = sys.stdout + + if buffer: + + class MyStdOut: + buffer = BytesIO() + + mystdout = MyStdOut() + else: + mystdout = BytesIO() + + sys.stdout = mystdout + + with Image.open(TEST_PNG_FILE) as im: + im.save(sys.stdout, "PNG") + + # Reset stdout + sys.stdout = old_stdout + + if buffer: + mystdout = mystdout.buffer + with Image.open(mystdout) as reloaded: + assert_image_equal_tofile(reloaded, TEST_PNG_FILE) + + def test_truncated_end_chunk(self) -> 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 + + +@pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS") +@skip_unless_feature("zlib") +class TestTruncatedPngPLeaks(PillowLeakTestCase): + mem_limit = 2 * 1024 # max increase in K + iterations = 100 # Leak is 56k/iteration, this will leak 5.6megs + + def test_leak_load(self) -> None: + with open("Tests/images/hopper.png", "rb") as f: + DATA = BytesIO(f.read(16 * 1024)) + + ImageFile.LOAD_TRUNCATED_IMAGES = True + with Image.open(DATA) as im: + im.load() + + def core() -> None: + with Image.open(DATA) as im: + im.load() + + try: + self._test_leak(core) + finally: + ImageFile.LOAD_TRUNCATED_IMAGES = False diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index 1bfd0434e..82c9ed45a 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -1,391 +1,391 @@ -from __future__ import annotations - -import sys -from io import BytesIO -from pathlib import Path - -import pytest - -from PIL import Image, PpmImagePlugin - -from .helper import ( - assert_image_equal, - assert_image_equal_tofile, - assert_image_similar, - hopper, -) - -# sample ppm stream -TEST_FILE = "Tests/images/hopper.ppm" - - -def test_sanity() -> None: - with Image.open(TEST_FILE) as im: - assert im.mode == "RGB" - assert im.size == (128, 128) - assert im.format == "PPM" - assert im.get_format_mimetype() == "image/x-portable-pixmap" - - -@pytest.mark.parametrize( - "data, mode, pixels", - ( - (b"P2 3 1 4 0 2 4", "L", (0, 128, 255)), - (b"P2 3 1 257 0 128 257", "I", (0, 32640, 65535)), - # P3 with maxval < 255 - ( - b"P3 3 1 17 0 1 2 8 9 10 15 16 17", - "RGB", - ((0, 15, 30), (120, 135, 150), (225, 240, 255)), - ), - # P3 with maxval > 255 - # Scale down to 255, since there is no RGB mode with more than 8-bit - ( - b"P3 3 1 257 0 1 2 128 129 130 256 257 257", - "RGB", - ((0, 1, 2), (127, 128, 129), (254, 255, 255)), - ), - (b"P5 3 1 4 \x00\x02\x04", "L", (0, 128, 255)), - (b"P5 3 1 257 \x00\x00\x00\x80\x01\x01", "I", (0, 32640, 65535)), - # P6 with maxval < 255 - ( - b"P6 3 1 17 \x00\x01\x02\x08\x09\x0A\x0F\x10\x11", - "RGB", - ( - (0, 15, 30), - (120, 135, 150), - (225, 240, 255), - ), - ), - # P6 with maxval > 255 - ( - b"P6 3 1 257 \x00\x00\x00\x01\x00\x02" - b"\x00\x80\x00\x81\x00\x82\x01\x00\x01\x01\xFF\xFF", - "RGB", - ( - (0, 1, 2), - (127, 128, 129), - (254, 255, 255), - ), - ), - ), -) -def test_arbitrary_maxval( - data: bytes, mode: str, pixels: tuple[int | tuple[int, int, int], ...] -) -> None: - fp = BytesIO(data) - with Image.open(fp) as im: - assert im.size == (3, 1) - assert im.mode == mode - - px = im.load() - assert tuple(px[x, 0] for x in range(3)) == pixels - - -def test_16bit_pgm() -> None: - with Image.open("Tests/images/16_bit_binary.pgm") as im: - assert im.mode == "I" - assert im.size == (20, 100) - assert im.get_format_mimetype() == "image/x-portable-graymap" - - assert_image_equal_tofile(im, "Tests/images/16_bit_binary_pgm.tiff") - - -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") - im.save(filename, "PPM") - - assert_image_equal_tofile(im, filename) - - -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") - im.save(filename) - - assert_image_equal_tofile(im, filename) - - -def test_pfm(tmp_path: Path) -> None: - with Image.open("Tests/images/hopper.pfm") as im: - assert im.info["scale"] == 1.0 - assert_image_equal(im, hopper("F")) - - filename = str(tmp_path / "tmp.pfm") - im.save(filename) - - assert_image_equal_tofile(im, filename) - - -def test_pfm_big_endian(tmp_path: Path) -> None: - with Image.open("Tests/images/hopper_be.pfm") as im: - assert im.info["scale"] == 2.5 - assert_image_equal(im, hopper("F")) - - filename = str(tmp_path / "tmp.pfm") - im.save(filename) - - assert_image_equal_tofile(im, filename) - - -@pytest.mark.parametrize( - "data", - [ - b"Pf 1 1 NaN \0\0\0\0", - b"Pf 1 1 inf \0\0\0\0", - b"Pf 1 1 -inf \0\0\0\0", - b"Pf 1 1 0.0 \0\0\0\0", - b"Pf 1 1 -0.0 \0\0\0\0", - ], -) -def test_pfm_invalid(data: bytes) -> None: - with pytest.raises(ValueError): - with Image.open(BytesIO(data)): - pass - - -@pytest.mark.parametrize( - "plain_path, raw_path", - ( - ( - "Tests/images/hopper_1bit_plain.pbm", # P1 - "Tests/images/hopper_1bit.pbm", # P4 - ), - ( - "Tests/images/hopper_8bit_plain.pgm", # P2 - "Tests/images/hopper_8bit.pgm", # P5 - ), - ( - "Tests/images/hopper_8bit_plain.ppm", # P3 - "Tests/images/hopper_8bit.ppm", # P6 - ), - ), -) -def test_plain(plain_path: str, raw_path: str) -> None: - with Image.open(plain_path) as im: - assert_image_equal_tofile(im, raw_path) - - -def test_16bit_plain_pgm() -> None: - # P2 with maxval 2 ** 16 - 1 - with Image.open("Tests/images/hopper_16bit_plain.pgm") as im: - assert im.mode == "I" - assert im.size == (128, 128) - assert im.get_format_mimetype() == "image/x-portable-graymap" - - # P5 with maxval 2 ** 16 - 1 - assert_image_equal_tofile(im, "Tests/images/hopper_16bit.pgm") - - -@pytest.mark.parametrize( - "header, data, comment_count", - ( - (b"P1\n2 2", b"1010", 10**6), - (b"P2\n3 1\n4", b"0 2 4", 1), - (b"P3\n2 2\n255", b"0 0 0 001 1 1 2 2 2 255 255 255", 10**6), - ), -) -def test_plain_data_with_comment( - tmp_path: Path, header: bytes, data: bytes, comment_count: int -) -> None: - path1 = str(tmp_path / "temp1.ppm") - path2 = str(tmp_path / "temp2.ppm") - comment = b"# comment" * comment_count - with open(path1, "wb") as f1, open(path2, "wb") as f2: - f1.write(header + b"\n\n" + data) - f2.write(header + b"\n" + comment + b"\n" + data + comment) - - with Image.open(path1) as im: - assert_image_equal_tofile(im, path2) - - -@pytest.mark.parametrize("data", (b"P1\n128 128\n", b"P3\n128 128\n255\n")) -def test_plain_truncated_data(tmp_path: Path, data: bytes) -> None: - path = str(tmp_path / "temp.ppm") - with open(path, "wb") as f: - f.write(data) - - with Image.open(path) as im: - with pytest.raises(ValueError): - im.load() - - -@pytest.mark.parametrize("data", (b"P1\n128 128\n1009", b"P3\n128 128\n255\n100A")) -def test_plain_invalid_data(tmp_path: Path, data: bytes) -> None: - path = str(tmp_path / "temp.ppm") - with open(path, "wb") as f: - f.write(data) - - with Image.open(path) as im: - with pytest.raises(ValueError): - im.load() - - -@pytest.mark.parametrize( - "data", - ( - b"P3\n128 128\n255\n012345678910", # half token too long - b"P3\n128 128\n255\n012345678910 0", # token too long - ), -) -def test_plain_ppm_token_too_long(tmp_path: Path, data: bytes) -> None: - path = str(tmp_path / "temp.ppm") - with open(path, "wb") as f: - f.write(data) - - with Image.open(path) as im: - with pytest.raises(ValueError): - im.load() - - -def test_plain_ppm_value_negative(tmp_path: Path) -> None: - path = str(tmp_path / "temp.ppm") - with open(path, "wb") as f: - f.write(b"P3\n128 128\n255\n-1") - - with Image.open(path) as im: - with pytest.raises(ValueError, match="Channel value is negative"): - im.load() - - -def test_plain_ppm_value_too_large(tmp_path: Path) -> None: - path = str(tmp_path / "temp.ppm") - with open(path, "wb") as f: - f.write(b"P3\n128 128\n255\n256") - - with Image.open(path) as im: - with pytest.raises(ValueError, match="Channel value too large"): - im.load() - - -def test_magic() -> None: - with pytest.raises(SyntaxError): - PpmImagePlugin.PpmImageFile(fp=BytesIO(b"PyInvalid")) - - -def test_header_with_comments(tmp_path: Path) -> None: - path = str(tmp_path / "temp.ppm") - with open(path, "wb") as f: - f.write(b"P6 #comment\n#comment\r12#comment\r8\n128 #comment\n255\n") - - with Image.open(path) as im: - assert im.size == (128, 128) - - -def test_non_integer_token(tmp_path: Path) -> None: - path = str(tmp_path / "temp.ppm") - with open(path, "wb") as f: - f.write(b"P6\nTEST") - - with pytest.raises(ValueError): - with Image.open(path): - pass - - -def test_header_token_too_long(tmp_path: Path) -> None: - path = str(tmp_path / "temp.ppm") - with open(path, "wb") as f: - f.write(b"P6\n 01234567890") - - with pytest.raises(ValueError) as e: - with Image.open(path): - pass - - assert str(e.value) == "Token too long in file header: 01234567890" - - -def test_truncated_file(tmp_path: Path) -> None: - # Test EOF in header - path = str(tmp_path / "temp.pgm") - with open(path, "wb") as f: - f.write(b"P6") - - with pytest.raises(ValueError) as e: - with Image.open(path): - pass - - assert str(e.value) == "Reached EOF while reading header" - - # Test EOF for PyDecoder - fp = BytesIO(b"P5 3 1 4") - with Image.open(fp) as im: - with pytest.raises(ValueError): - im.load() - - -def test_not_enough_image_data(tmp_path: Path) -> None: - path = str(tmp_path / "temp.ppm") - with open(path, "wb") as f: - f.write(b"P2 1 2 255 255") - - with Image.open(path) as im: - with pytest.raises(ValueError): - im.load() - - -@pytest.mark.parametrize("maxval", (b"0", b"65536")) -def test_invalid_maxval(maxval: bytes, tmp_path: Path) -> None: - path = str(tmp_path / "temp.ppm") - with open(path, "wb") as f: - f.write(b"P6\n3 1 " + maxval) - - with pytest.raises(ValueError) as e: - with Image.open(path): - pass - - assert str(e.value) == "maxval must be greater than 0 and less than 65536" - - -def test_neg_ppm() -> None: - # Storage.c accepted negative values for xsize, ysize. the - # internal open_ppm function didn't check for sanity but it - # has been removed. The default opener doesn't accept negative - # sizes. - - with pytest.raises(OSError): - with Image.open("Tests/images/negative_size.ppm"): - pass - - -def test_mimetypes(tmp_path: Path) -> None: - path = str(tmp_path / "temp.pgm") - - with open(path, "wb") as f: - f.write(b"P4\n128 128\n255") - with Image.open(path) as im: - assert im.get_format_mimetype() == "image/x-portable-bitmap" - - with open(path, "wb") as f: - f.write(b"PyCMYK\n128 128\n255") - with Image.open(path) as im: - assert im.get_format_mimetype() == "image/x-portable-anymap" - - -@pytest.mark.parametrize("buffer", (True, False)) -def test_save_stdout(buffer: bool) -> None: - old_stdout = sys.stdout - - if buffer: - - class MyStdOut: - buffer = BytesIO() - - mystdout = MyStdOut() - else: - mystdout = BytesIO() - - sys.stdout = mystdout - - with Image.open(TEST_FILE) as im: - im.save(sys.stdout, "PPM") - - # Reset stdout - sys.stdout = old_stdout - - if buffer: - mystdout = mystdout.buffer - with Image.open(mystdout) as reloaded: - assert_image_equal_tofile(reloaded, TEST_FILE) +from __future__ import annotations + +import sys +from io import BytesIO +from pathlib import Path + +import pytest + +from PIL import Image, PpmImagePlugin + +from .helper import ( + assert_image_equal, + assert_image_equal_tofile, + assert_image_similar, + hopper, +) + +# sample ppm stream +TEST_FILE = "Tests/images/hopper.ppm" + + +def test_sanity() -> None: + with Image.open(TEST_FILE) as im: + assert im.mode == "RGB" + assert im.size == (128, 128) + assert im.format == "PPM" + assert im.get_format_mimetype() == "image/x-portable-pixmap" + + +@pytest.mark.parametrize( + "data, mode, pixels", + ( + (b"P2 3 1 4 0 2 4", "L", (0, 128, 255)), + (b"P2 3 1 257 0 128 257", "I", (0, 32640, 65535)), + # P3 with maxval < 255 + ( + b"P3 3 1 17 0 1 2 8 9 10 15 16 17", + "RGB", + ((0, 15, 30), (120, 135, 150), (225, 240, 255)), + ), + # P3 with maxval > 255 + # Scale down to 255, since there is no RGB mode with more than 8-bit + ( + b"P3 3 1 257 0 1 2 128 129 130 256 257 257", + "RGB", + ((0, 1, 2), (127, 128, 129), (254, 255, 255)), + ), + (b"P5 3 1 4 \x00\x02\x04", "L", (0, 128, 255)), + (b"P5 3 1 257 \x00\x00\x00\x80\x01\x01", "I", (0, 32640, 65535)), + # P6 with maxval < 255 + ( + b"P6 3 1 17 \x00\x01\x02\x08\x09\x0A\x0F\x10\x11", + "RGB", + ( + (0, 15, 30), + (120, 135, 150), + (225, 240, 255), + ), + ), + # P6 with maxval > 255 + ( + b"P6 3 1 257 \x00\x00\x00\x01\x00\x02" + b"\x00\x80\x00\x81\x00\x82\x01\x00\x01\x01\xFF\xFF", + "RGB", + ( + (0, 1, 2), + (127, 128, 129), + (254, 255, 255), + ), + ), + ), +) +def test_arbitrary_maxval( + data: bytes, mode: str, pixels: tuple[int | tuple[int, int, int], ...] +) -> None: + fp = BytesIO(data) + with Image.open(fp) as im: + assert im.size == (3, 1) + assert im.mode == mode + + px = im.load() + assert tuple(px[x, 0] for x in range(3)) == pixels + + +def test_16bit_pgm() -> None: + with Image.open("Tests/images/16_bit_binary.pgm") as im: + assert im.mode == "I" + assert im.size == (20, 100) + assert im.get_format_mimetype() == "image/x-portable-graymap" + + assert_image_equal_tofile(im, "Tests/images/16_bit_binary_pgm.tiff") + + +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") + im.save(filename, "PPM") + + assert_image_equal_tofile(im, filename) + + +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") + im.save(filename) + + assert_image_equal_tofile(im, filename) + + +def test_pfm(tmp_path: Path) -> None: + with Image.open("Tests/images/hopper.pfm") as im: + assert im.info["scale"] == 1.0 + assert_image_equal(im, hopper("F")) + + filename = str(tmp_path / "tmp.pfm") + im.save(filename) + + assert_image_equal_tofile(im, filename) + + +def test_pfm_big_endian(tmp_path: Path) -> None: + with Image.open("Tests/images/hopper_be.pfm") as im: + assert im.info["scale"] == 2.5 + assert_image_equal(im, hopper("F")) + + filename = str(tmp_path / "tmp.pfm") + im.save(filename) + + assert_image_equal_tofile(im, filename) + + +@pytest.mark.parametrize( + "data", + [ + b"Pf 1 1 NaN \0\0\0\0", + b"Pf 1 1 inf \0\0\0\0", + b"Pf 1 1 -inf \0\0\0\0", + b"Pf 1 1 0.0 \0\0\0\0", + b"Pf 1 1 -0.0 \0\0\0\0", + ], +) +def test_pfm_invalid(data: bytes) -> None: + with pytest.raises(ValueError): + with Image.open(BytesIO(data)): + pass + + +@pytest.mark.parametrize( + "plain_path, raw_path", + ( + ( + "Tests/images/hopper_1bit_plain.pbm", # P1 + "Tests/images/hopper_1bit.pbm", # P4 + ), + ( + "Tests/images/hopper_8bit_plain.pgm", # P2 + "Tests/images/hopper_8bit.pgm", # P5 + ), + ( + "Tests/images/hopper_8bit_plain.ppm", # P3 + "Tests/images/hopper_8bit.ppm", # P6 + ), + ), +) +def test_plain(plain_path: str, raw_path: str) -> None: + with Image.open(plain_path) as im: + assert_image_equal_tofile(im, raw_path) + + +def test_16bit_plain_pgm() -> None: + # P2 with maxval 2 ** 16 - 1 + with Image.open("Tests/images/hopper_16bit_plain.pgm") as im: + assert im.mode == "I" + assert im.size == (128, 128) + assert im.get_format_mimetype() == "image/x-portable-graymap" + + # P5 with maxval 2 ** 16 - 1 + assert_image_equal_tofile(im, "Tests/images/hopper_16bit.pgm") + + +@pytest.mark.parametrize( + "header, data, comment_count", + ( + (b"P1\n2 2", b"1010", 10**6), + (b"P2\n3 1\n4", b"0 2 4", 1), + (b"P3\n2 2\n255", b"0 0 0 001 1 1 2 2 2 255 255 255", 10**6), + ), +) +def test_plain_data_with_comment( + tmp_path: Path, header: bytes, data: bytes, comment_count: int +) -> None: + path1 = str(tmp_path / "temp1.ppm") + path2 = str(tmp_path / "temp2.ppm") + comment = b"# comment" * comment_count + with open(path1, "wb") as f1, open(path2, "wb") as f2: + f1.write(header + b"\n\n" + data) + f2.write(header + b"\n" + comment + b"\n" + data + comment) + + with Image.open(path1) as im: + assert_image_equal_tofile(im, path2) + + +@pytest.mark.parametrize("data", (b"P1\n128 128\n", b"P3\n128 128\n255\n")) +def test_plain_truncated_data(tmp_path: Path, data: bytes) -> None: + path = str(tmp_path / "temp.ppm") + with open(path, "wb") as f: + f.write(data) + + with Image.open(path) as im: + with pytest.raises(ValueError): + im.load() + + +@pytest.mark.parametrize("data", (b"P1\n128 128\n1009", b"P3\n128 128\n255\n100A")) +def test_plain_invalid_data(tmp_path: Path, data: bytes) -> None: + path = str(tmp_path / "temp.ppm") + with open(path, "wb") as f: + f.write(data) + + with Image.open(path) as im: + with pytest.raises(ValueError): + im.load() + + +@pytest.mark.parametrize( + "data", + ( + b"P3\n128 128\n255\n012345678910", # half token too long + b"P3\n128 128\n255\n012345678910 0", # token too long + ), +) +def test_plain_ppm_token_too_long(tmp_path: Path, data: bytes) -> None: + path = str(tmp_path / "temp.ppm") + with open(path, "wb") as f: + f.write(data) + + with Image.open(path) as im: + with pytest.raises(ValueError): + im.load() + + +def test_plain_ppm_value_negative(tmp_path: Path) -> None: + path = str(tmp_path / "temp.ppm") + with open(path, "wb") as f: + f.write(b"P3\n128 128\n255\n-1") + + with Image.open(path) as im: + with pytest.raises(ValueError, match="Channel value is negative"): + im.load() + + +def test_plain_ppm_value_too_large(tmp_path: Path) -> None: + path = str(tmp_path / "temp.ppm") + with open(path, "wb") as f: + f.write(b"P3\n128 128\n255\n256") + + with Image.open(path) as im: + with pytest.raises(ValueError, match="Channel value too large"): + im.load() + + +def test_magic() -> None: + with pytest.raises(SyntaxError): + PpmImagePlugin.PpmImageFile(fp=BytesIO(b"PyInvalid")) + + +def test_header_with_comments(tmp_path: Path) -> None: + path = str(tmp_path / "temp.ppm") + with open(path, "wb") as f: + f.write(b"P6 #comment\n#comment\r12#comment\r8\n128 #comment\n255\n") + + with Image.open(path) as im: + assert im.size == (128, 128) + + +def test_non_integer_token(tmp_path: Path) -> None: + path = str(tmp_path / "temp.ppm") + with open(path, "wb") as f: + f.write(b"P6\nTEST") + + with pytest.raises(ValueError): + with Image.open(path): + pass + + +def test_header_token_too_long(tmp_path: Path) -> None: + path = str(tmp_path / "temp.ppm") + with open(path, "wb") as f: + f.write(b"P6\n 01234567890") + + with pytest.raises(ValueError) as e: + with Image.open(path): + pass + + assert str(e.value) == "Token too long in file header: 01234567890" + + +def test_truncated_file(tmp_path: Path) -> None: + # Test EOF in header + path = str(tmp_path / "temp.pgm") + with open(path, "wb") as f: + f.write(b"P6") + + with pytest.raises(ValueError) as e: + with Image.open(path): + pass + + assert str(e.value) == "Reached EOF while reading header" + + # Test EOF for PyDecoder + fp = BytesIO(b"P5 3 1 4") + with Image.open(fp) as im: + with pytest.raises(ValueError): + im.load() + + +def test_not_enough_image_data(tmp_path: Path) -> None: + path = str(tmp_path / "temp.ppm") + with open(path, "wb") as f: + f.write(b"P2 1 2 255 255") + + with Image.open(path) as im: + with pytest.raises(ValueError): + im.load() + + +@pytest.mark.parametrize("maxval", (b"0", b"65536")) +def test_invalid_maxval(maxval: bytes, tmp_path: Path) -> None: + path = str(tmp_path / "temp.ppm") + with open(path, "wb") as f: + f.write(b"P6\n3 1 " + maxval) + + with pytest.raises(ValueError) as e: + with Image.open(path): + pass + + assert str(e.value) == "maxval must be greater than 0 and less than 65536" + + +def test_neg_ppm() -> None: + # Storage.c accepted negative values for xsize, ysize. the + # internal open_ppm function didn't check for sanity but it + # has been removed. The default opener doesn't accept negative + # sizes. + + with pytest.raises(OSError): + with Image.open("Tests/images/negative_size.ppm"): + pass + + +def test_mimetypes(tmp_path: Path) -> None: + path = str(tmp_path / "temp.pgm") + + with open(path, "wb") as f: + f.write(b"P4\n128 128\n255") + with Image.open(path) as im: + assert im.get_format_mimetype() == "image/x-portable-bitmap" + + with open(path, "wb") as f: + f.write(b"PyCMYK\n128 128\n255") + with Image.open(path) as im: + assert im.get_format_mimetype() == "image/x-portable-anymap" + + +@pytest.mark.parametrize("buffer", (True, False)) +def test_save_stdout(buffer: bool) -> None: + old_stdout = sys.stdout + + if buffer: + + class MyStdOut: + buffer = BytesIO() + + mystdout = MyStdOut() + else: + mystdout = BytesIO() + + sys.stdout = mystdout + + with Image.open(TEST_FILE) as im: + im.save(sys.stdout, "PPM") + + # Reset stdout + sys.stdout = old_stdout + + if buffer: + mystdout = mystdout.buffer + with Image.open(mystdout) as reloaded: + assert_image_equal_tofile(reloaded, TEST_FILE) diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index 484a1be8f..a24e277d4 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -1,169 +1,169 @@ -from __future__ import annotations - -import warnings - -import pytest - -from PIL import Image, PsdImagePlugin, UnidentifiedImageError - -from .helper import assert_image_equal_tofile, assert_image_similar, hopper, is_pypy - -test_file = "Tests/images/hopper.psd" - - -def test_sanity() -> None: - with Image.open(test_file) as im: - im.load() - assert im.mode == "RGB" - assert im.size == (128, 128) - assert im.format == "PSD" - assert im.get_format_mimetype() == "image/vnd.adobe.photoshop" - - im2 = hopper() - assert_image_similar(im, im2, 4.8) - - -@pytest.mark.skipif(is_pypy(), reason="Requires CPython") -def test_unclosed_file() -> None: - def open() -> None: - im = Image.open(test_file) - im.load() - - with pytest.warns(ResourceWarning): - open() - - -def test_closed_file() -> None: - with warnings.catch_warnings(): - im = Image.open(test_file) - im.load() - im.close() - - -def test_context_manager() -> None: - with warnings.catch_warnings(): - with Image.open(test_file) as im: - im.load() - - -def test_invalid_file() -> None: - invalid_file = "Tests/images/flower.jpg" - - with pytest.raises(SyntaxError): - PsdImagePlugin.PsdImageFile(invalid_file) - - -def test_n_frames() -> None: - with Image.open("Tests/images/hopper_merged.psd") as im: - assert im.n_frames == 1 - assert not im.is_animated - - for path in [test_file, "Tests/images/negative_layer_count.psd"]: - with Image.open(path) as im: - assert im.n_frames == 2 - assert im.is_animated - - -def test_eoferror() -> None: - with Image.open(test_file) as im: - # PSD seek index starts at 1 rather than 0 - n_frames = im.n_frames + 1 - - # Test seeking past the last frame - with pytest.raises(EOFError): - im.seek(n_frames) - assert im.tell() < n_frames - - # Test that seeking to the last frame does not raise an error - im.seek(n_frames - 1) - - -def test_seek_tell() -> None: - with Image.open(test_file) as im: - layer_number = im.tell() - assert layer_number == 1 - - with pytest.raises(EOFError): - im.seek(0) - - im.seek(1) - layer_number = im.tell() - assert layer_number == 1 - - im.seek(2) - layer_number = im.tell() - assert layer_number == 2 - - -def test_seek_eoferror() -> None: - with Image.open(test_file) as im: - with pytest.raises(EOFError): - im.seek(-1) - - -def test_open_after_exclusive_load() -> None: - with Image.open(test_file) as im: - im.load() - im.seek(im.tell() + 1) - im.load() - - -def test_rgba() -> None: - with Image.open("Tests/images/rgba.psd") as im: - assert_image_equal_tofile(im, "Tests/images/imagedraw_square.png") - - -def test_negative_top_left_layer() -> None: - with Image.open("Tests/images/negative_top_left_layer.psd") as im: - 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 im.n_frames == 1 - - -def test_icc_profile() -> None: - with Image.open(test_file) as im: - assert "icc_profile" in im.info - - icc_profile = im.info["icc_profile"] - assert len(icc_profile) == 3144 - - -def test_no_icc_profile() -> None: - with Image.open("Tests/images/hopper_merged.psd") as im: - assert "icc_profile" not in im.info - - -def test_combined_larger_than_size() -> None: - # The combined size of the individual parts is larger than the - # declared 'size' of the extra data field, resulting in a backwards seek. - - # If we instead take the 'size' of the extra data field as the source of truth, - # then the seek can't be negative - with pytest.raises(OSError): - with Image.open("Tests/images/combined_larger_than_size.psd"): - pass - - -@pytest.mark.parametrize( - "test_file,raises", - [ - ( - "Tests/images/timeout-1ee28a249896e05b83840ae8140622de8e648ba9.psd", - UnidentifiedImageError, - ), - ( - "Tests/images/timeout-598843abc37fc080ec36a2699ebbd44f795d3a6f.psd", - UnidentifiedImageError, - ), - ("Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd", OSError), - ("Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", OSError), - ], -) -def test_crashes(test_file: str, raises) -> None: - with open(test_file, "rb") as f: - with pytest.raises(raises): - with Image.open(f): - pass +from __future__ import annotations + +import warnings + +import pytest + +from PIL import Image, PsdImagePlugin, UnidentifiedImageError + +from .helper import assert_image_equal_tofile, assert_image_similar, hopper, is_pypy + +test_file = "Tests/images/hopper.psd" + + +def test_sanity() -> None: + with Image.open(test_file) as im: + im.load() + assert im.mode == "RGB" + assert im.size == (128, 128) + assert im.format == "PSD" + assert im.get_format_mimetype() == "image/vnd.adobe.photoshop" + + im2 = hopper() + assert_image_similar(im, im2, 4.8) + + +@pytest.mark.skipif(is_pypy(), reason="Requires CPython") +def test_unclosed_file() -> None: + def open() -> None: + im = Image.open(test_file) + im.load() + + with pytest.warns(ResourceWarning): + open() + + +def test_closed_file() -> None: + with warnings.catch_warnings(): + im = Image.open(test_file) + im.load() + im.close() + + +def test_context_manager() -> None: + with warnings.catch_warnings(): + with Image.open(test_file) as im: + im.load() + + +def test_invalid_file() -> None: + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(SyntaxError): + PsdImagePlugin.PsdImageFile(invalid_file) + + +def test_n_frames() -> None: + with Image.open("Tests/images/hopper_merged.psd") as im: + assert im.n_frames == 1 + assert not im.is_animated + + for path in [test_file, "Tests/images/negative_layer_count.psd"]: + with Image.open(path) as im: + assert im.n_frames == 2 + assert im.is_animated + + +def test_eoferror() -> None: + with Image.open(test_file) as im: + # PSD seek index starts at 1 rather than 0 + n_frames = im.n_frames + 1 + + # Test seeking past the last frame + with pytest.raises(EOFError): + im.seek(n_frames) + assert im.tell() < n_frames + + # Test that seeking to the last frame does not raise an error + im.seek(n_frames - 1) + + +def test_seek_tell() -> None: + with Image.open(test_file) as im: + layer_number = im.tell() + assert layer_number == 1 + + with pytest.raises(EOFError): + im.seek(0) + + im.seek(1) + layer_number = im.tell() + assert layer_number == 1 + + im.seek(2) + layer_number = im.tell() + assert layer_number == 2 + + +def test_seek_eoferror() -> None: + with Image.open(test_file) as im: + with pytest.raises(EOFError): + im.seek(-1) + + +def test_open_after_exclusive_load() -> None: + with Image.open(test_file) as im: + im.load() + im.seek(im.tell() + 1) + im.load() + + +def test_rgba() -> None: + with Image.open("Tests/images/rgba.psd") as im: + assert_image_equal_tofile(im, "Tests/images/imagedraw_square.png") + + +def test_negative_top_left_layer() -> None: + with Image.open("Tests/images/negative_top_left_layer.psd") as im: + 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 im.n_frames == 1 + + +def test_icc_profile() -> None: + with Image.open(test_file) as im: + assert "icc_profile" in im.info + + icc_profile = im.info["icc_profile"] + assert len(icc_profile) == 3144 + + +def test_no_icc_profile() -> None: + with Image.open("Tests/images/hopper_merged.psd") as im: + assert "icc_profile" not in im.info + + +def test_combined_larger_than_size() -> None: + # The combined size of the individual parts is larger than the + # declared 'size' of the extra data field, resulting in a backwards seek. + + # If we instead take the 'size' of the extra data field as the source of truth, + # then the seek can't be negative + with pytest.raises(OSError): + with Image.open("Tests/images/combined_larger_than_size.psd"): + pass + + +@pytest.mark.parametrize( + "test_file,raises", + [ + ( + "Tests/images/timeout-1ee28a249896e05b83840ae8140622de8e648ba9.psd", + UnidentifiedImageError, + ), + ( + "Tests/images/timeout-598843abc37fc080ec36a2699ebbd44f795d3a6f.psd", + UnidentifiedImageError, + ), + ("Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd", OSError), + ("Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", OSError), + ], +) +def test_crashes(test_file: str, raises) -> None: + with open(test_file, "rb") as f: + with pytest.raises(raises): + with Image.open(f): + pass diff --git a/Tests/test_file_qoi.py b/Tests/test_file_qoi.py index fd4b981ce..e20275a69 100644 --- a/Tests/test_file_qoi.py +++ b/Tests/test_file_qoi.py @@ -1,30 +1,30 @@ -from __future__ import annotations - -import pytest - -from PIL import Image, QoiImagePlugin - -from .helper import assert_image_equal_tofile - - -def test_sanity() -> None: - with Image.open("Tests/images/hopper.qoi") as im: - assert im.mode == "RGB" - assert im.size == (128, 128) - assert im.format == "QOI" - - assert_image_equal_tofile(im, "Tests/images/hopper.png") - - with Image.open("Tests/images/pil123rgba.qoi") as im: - assert im.mode == "RGBA" - assert im.size == (162, 150) - assert im.format == "QOI" - - assert_image_equal_tofile(im, "Tests/images/pil123rgba.png") - - -def test_invalid_file() -> None: - invalid_file = "Tests/images/flower.jpg" - - with pytest.raises(SyntaxError): - QoiImagePlugin.QoiImageFile(invalid_file) +from __future__ import annotations + +import pytest + +from PIL import Image, QoiImagePlugin + +from .helper import assert_image_equal_tofile + + +def test_sanity() -> None: + with Image.open("Tests/images/hopper.qoi") as im: + assert im.mode == "RGB" + assert im.size == (128, 128) + assert im.format == "QOI" + + assert_image_equal_tofile(im, "Tests/images/hopper.png") + + with Image.open("Tests/images/pil123rgba.qoi") as im: + assert im.mode == "RGBA" + assert im.size == (162, 150) + assert im.format == "QOI" + + assert_image_equal_tofile(im, "Tests/images/pil123rgba.png") + + +def test_invalid_file() -> None: + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(SyntaxError): + QoiImagePlugin.QoiImageFile(invalid_file) diff --git a/Tests/test_file_sgi.py b/Tests/test_file_sgi.py index e13a8019e..724c1b5f7 100644 --- a/Tests/test_file_sgi.py +++ b/Tests/test_file_sgi.py @@ -1,109 +1,109 @@ -from __future__ import annotations - -from pathlib import Path - -import pytest - -from PIL import Image, SgiImagePlugin - -from .helper import ( - assert_image_equal, - assert_image_equal_tofile, - assert_image_similar, - hopper, -) - - -def test_rgb() -> None: - # Created with ImageMagick then renamed: - # convert hopper.ppm -compress None sgi:hopper.rgb - test_file = "Tests/images/hopper.rgb" - - with Image.open(test_file) as im: - assert_image_equal(im, hopper()) - assert im.get_format_mimetype() == "image/rgb" - - -def test_rgb16() -> None: - assert_image_equal_tofile(hopper(), "Tests/images/hopper16.rgb") - - -def test_l() -> None: - # Created with ImageMagick - # convert hopper.ppm -monochrome -compress None sgi:hopper.bw - test_file = "Tests/images/hopper.bw" - - with Image.open(test_file) as im: - assert_image_similar(im, hopper("L"), 2) - assert im.get_format_mimetype() == "image/sgi" - - -def test_rgba() -> None: - # Created with ImageMagick: - # convert transparent.png -compress None transparent.sgi - test_file = "Tests/images/transparent.sgi" - - with Image.open(test_file) as im: - assert_image_equal_tofile(im, "Tests/images/transparent.png") - assert im.get_format_mimetype() == "image/sgi" - - -def test_rle() -> None: - # Created with ImageMagick: - # convert hopper.ppm hopper.sgi - test_file = "Tests/images/hopper.sgi" - - with Image.open(test_file) as im: - assert_image_equal_tofile(im, "Tests/images/hopper.rgb") - - -def test_rle16() -> None: - test_file = "Tests/images/tv16.sgi" - - with Image.open(test_file) as im: - assert_image_equal_tofile(im, "Tests/images/tv.rgb") - - -def test_invalid_file() -> None: - invalid_file = "Tests/images/flower.jpg" - - with pytest.raises(ValueError): - 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") - 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 - - for mode in ("L", "RGB", "RGBA"): - roundtrip(hopper(mode)) - - # Test 1 dimension for an L mode image - roundtrip(Image.new("L", (10, 1))) - - -def test_write16(tmp_path: Path) -> None: - test_file = "Tests/images/hopper16.rgb" - - with Image.open(test_file) as im: - out = str(tmp_path / "temp.sgi") - im.save(out, format="sgi", bpc=2) - - assert_image_equal_tofile(im, out) - - -def test_unsupported_mode(tmp_path: Path) -> None: - im = hopper("LA") - out = str(tmp_path / "temp.sgi") - - with pytest.raises(ValueError): - im.save(out, format="sgi") +from __future__ import annotations + +from pathlib import Path + +import pytest + +from PIL import Image, SgiImagePlugin + +from .helper import ( + assert_image_equal, + assert_image_equal_tofile, + assert_image_similar, + hopper, +) + + +def test_rgb() -> None: + # Created with ImageMagick then renamed: + # convert hopper.ppm -compress None sgi:hopper.rgb + test_file = "Tests/images/hopper.rgb" + + with Image.open(test_file) as im: + assert_image_equal(im, hopper()) + assert im.get_format_mimetype() == "image/rgb" + + +def test_rgb16() -> None: + assert_image_equal_tofile(hopper(), "Tests/images/hopper16.rgb") + + +def test_l() -> None: + # Created with ImageMagick + # convert hopper.ppm -monochrome -compress None sgi:hopper.bw + test_file = "Tests/images/hopper.bw" + + with Image.open(test_file) as im: + assert_image_similar(im, hopper("L"), 2) + assert im.get_format_mimetype() == "image/sgi" + + +def test_rgba() -> None: + # Created with ImageMagick: + # convert transparent.png -compress None transparent.sgi + test_file = "Tests/images/transparent.sgi" + + with Image.open(test_file) as im: + assert_image_equal_tofile(im, "Tests/images/transparent.png") + assert im.get_format_mimetype() == "image/sgi" + + +def test_rle() -> None: + # Created with ImageMagick: + # convert hopper.ppm hopper.sgi + test_file = "Tests/images/hopper.sgi" + + with Image.open(test_file) as im: + assert_image_equal_tofile(im, "Tests/images/hopper.rgb") + + +def test_rle16() -> None: + test_file = "Tests/images/tv16.sgi" + + with Image.open(test_file) as im: + assert_image_equal_tofile(im, "Tests/images/tv.rgb") + + +def test_invalid_file() -> None: + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(ValueError): + 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") + 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 + + for mode in ("L", "RGB", "RGBA"): + roundtrip(hopper(mode)) + + # Test 1 dimension for an L mode image + roundtrip(Image.new("L", (10, 1))) + + +def test_write16(tmp_path: Path) -> None: + test_file = "Tests/images/hopper16.rgb" + + with Image.open(test_file) as im: + out = str(tmp_path / "temp.sgi") + im.save(out, format="sgi", bpc=2) + + assert_image_equal_tofile(im, out) + + +def test_unsupported_mode(tmp_path: Path) -> None: + im = hopper("LA") + out = str(tmp_path / "temp.sgi") + + with pytest.raises(ValueError): + im.save(out, format="sgi") diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index 9b82a962a..d8bf33f80 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -1,164 +1,164 @@ -from __future__ import annotations - -import tempfile -import warnings -from io import BytesIO -from pathlib import Path - -import pytest - -from PIL import Image, ImageSequence, SpiderImagePlugin - -from .helper import assert_image_equal, hopper, is_pypy - -TEST_FILE = "Tests/images/hopper.spider" - - -def test_sanity() -> None: - with Image.open(TEST_FILE) as im: - im.load() - assert im.mode == "F" - assert im.size == (128, 128) - assert im.format == "SPIDER" - - -@pytest.mark.skipif(is_pypy(), reason="Requires CPython") -def test_unclosed_file() -> None: - def open() -> None: - im = Image.open(TEST_FILE) - im.load() - - with pytest.warns(ResourceWarning): - open() - - -def test_closed_file() -> None: - with warnings.catch_warnings(): - im = Image.open(TEST_FILE) - im.load() - im.close() - - -def test_context_manager() -> None: - with warnings.catch_warnings(): - with Image.open(TEST_FILE) as im: - im.load() - - -def test_save(tmp_path: Path) -> None: - # Arrange - temp = str(tmp_path / "temp.spider") - im = hopper() - - # Act - im.save(temp, "SPIDER") - - # Assert - with Image.open(temp) as im2: - assert im2.mode == "F" - assert im2.size == (128, 128) - assert im2.format == "SPIDER" - - -def test_tempfile() -> None: - # Arrange - im = hopper() - - # Act - with tempfile.TemporaryFile() as fp: - im.save(fp, "SPIDER") - - # Assert - fp.seek(0) - with Image.open(fp) as reloaded: - assert reloaded.mode == "F" - assert reloaded.size == (128, 128) - assert reloaded.format == "SPIDER" - - -def test_is_spider_image() -> None: - assert SpiderImagePlugin.isSpiderImage(TEST_FILE) - - -def test_tell() -> None: - # Arrange - with Image.open(TEST_FILE) as im: - # Act - index = im.tell() - - # Assert - assert index == 0 - - -def test_n_frames() -> None: - with Image.open(TEST_FILE) as im: - assert im.n_frames == 1 - assert not im.is_animated - - -def test_load_image_series() -> None: - # Arrange - not_spider_file = "Tests/images/hopper.ppm" - file_list = [TEST_FILE, not_spider_file, "path/not_found.ext"] - - # Act - img_list = SpiderImagePlugin.loadImageSeries(file_list) - - # Assert - assert len(img_list) == 1 - assert isinstance(img_list[0], Image.Image) - assert img_list[0].size == (128, 128) - - -def test_load_image_series_no_input() -> None: - # Arrange - file_list = None - - # Act - img_list = SpiderImagePlugin.loadImageSeries(file_list) - - # Assert - assert img_list is None - - -def test_is_int_not_a_number() -> None: - # Arrange - not_a_number = "a" - - # Act - ret = SpiderImagePlugin.isInt(not_a_number) - - # Assert - assert ret == 0 - - -def test_invalid_file() -> None: - invalid_file = "Tests/images/invalid.spider" - - with pytest.raises(OSError): - with Image.open(invalid_file): - pass - - -def test_nonstack_file() -> None: - with Image.open(TEST_FILE) as im: - with pytest.raises(EOFError): - im.seek(0) - - -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" - - -# for issue #4093 -def test_odd_size() -> None: - data = BytesIO() - width = 100 - im = Image.new("F", (width, 64)) - im.save(data, format="SPIDER") - - data.seek(0) - with Image.open(data) as im2: - assert_image_equal(im, im2) +from __future__ import annotations + +import tempfile +import warnings +from io import BytesIO +from pathlib import Path + +import pytest + +from PIL import Image, ImageSequence, SpiderImagePlugin + +from .helper import assert_image_equal, hopper, is_pypy + +TEST_FILE = "Tests/images/hopper.spider" + + +def test_sanity() -> None: + with Image.open(TEST_FILE) as im: + im.load() + assert im.mode == "F" + assert im.size == (128, 128) + assert im.format == "SPIDER" + + +@pytest.mark.skipif(is_pypy(), reason="Requires CPython") +def test_unclosed_file() -> None: + def open() -> None: + im = Image.open(TEST_FILE) + im.load() + + with pytest.warns(ResourceWarning): + open() + + +def test_closed_file() -> None: + with warnings.catch_warnings(): + im = Image.open(TEST_FILE) + im.load() + im.close() + + +def test_context_manager() -> None: + with warnings.catch_warnings(): + with Image.open(TEST_FILE) as im: + im.load() + + +def test_save(tmp_path: Path) -> None: + # Arrange + temp = str(tmp_path / "temp.spider") + im = hopper() + + # Act + im.save(temp, "SPIDER") + + # Assert + with Image.open(temp) as im2: + assert im2.mode == "F" + assert im2.size == (128, 128) + assert im2.format == "SPIDER" + + +def test_tempfile() -> None: + # Arrange + im = hopper() + + # Act + with tempfile.TemporaryFile() as fp: + im.save(fp, "SPIDER") + + # Assert + fp.seek(0) + with Image.open(fp) as reloaded: + assert reloaded.mode == "F" + assert reloaded.size == (128, 128) + assert reloaded.format == "SPIDER" + + +def test_is_spider_image() -> None: + assert SpiderImagePlugin.isSpiderImage(TEST_FILE) + + +def test_tell() -> None: + # Arrange + with Image.open(TEST_FILE) as im: + # Act + index = im.tell() + + # Assert + assert index == 0 + + +def test_n_frames() -> None: + with Image.open(TEST_FILE) as im: + assert im.n_frames == 1 + assert not im.is_animated + + +def test_load_image_series() -> None: + # Arrange + not_spider_file = "Tests/images/hopper.ppm" + file_list = [TEST_FILE, not_spider_file, "path/not_found.ext"] + + # Act + img_list = SpiderImagePlugin.loadImageSeries(file_list) + + # Assert + assert len(img_list) == 1 + assert isinstance(img_list[0], Image.Image) + assert img_list[0].size == (128, 128) + + +def test_load_image_series_no_input() -> None: + # Arrange + file_list = None + + # Act + img_list = SpiderImagePlugin.loadImageSeries(file_list) + + # Assert + assert img_list is None + + +def test_is_int_not_a_number() -> None: + # Arrange + not_a_number = "a" + + # Act + ret = SpiderImagePlugin.isInt(not_a_number) + + # Assert + assert ret == 0 + + +def test_invalid_file() -> None: + invalid_file = "Tests/images/invalid.spider" + + with pytest.raises(OSError): + with Image.open(invalid_file): + pass + + +def test_nonstack_file() -> None: + with Image.open(TEST_FILE) as im: + with pytest.raises(EOFError): + im.seek(0) + + +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" + + +# for issue #4093 +def test_odd_size() -> None: + data = BytesIO() + width = 100 + im = Image.new("F", (width, 64)) + im.save(data, format="SPIDER") + + data.seek(0) + with Image.open(data) as im2: + assert_image_equal(im, im2) diff --git a/Tests/test_file_sun.py b/Tests/test_file_sun.py index 6cfff8730..02163ad84 100644 --- a/Tests/test_file_sun.py +++ b/Tests/test_file_sun.py @@ -1,49 +1,49 @@ -from __future__ import annotations - -import os - -import pytest - -from PIL import Image, SunImagePlugin - -from .helper import assert_image_equal_tofile, assert_image_similar, hopper - -EXTRA_DIR = "Tests/images/sunraster" - - -def test_sanity() -> None: - # Arrange - # Created with ImageMagick: convert hopper.jpg hopper.ras - test_file = "Tests/images/hopper.ras" - - # Act - with Image.open(test_file) as im: - # Assert - assert im.size == (128, 128) - - assert_image_similar(im, hopper(), 5) # visually verified - - invalid_file = "Tests/images/flower.jpg" - with pytest.raises(SyntaxError): - SunImagePlugin.SunImageFile(invalid_file) - - -def test_im1() -> None: - with Image.open("Tests/images/sunraster.im1") as im: - assert_image_equal_tofile(im, "Tests/images/sunraster.im1.png") - - -@pytest.mark.skipif( - not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" -) -def test_others() -> None: - files = ( - os.path.join(EXTRA_DIR, f) - for f in os.listdir(EXTRA_DIR) - if os.path.splitext(f)[1] in (".sun", ".SUN", ".ras") - ) - for path in files: - with Image.open(path) as im: - im.load() - assert isinstance(im, SunImagePlugin.SunImageFile) - assert_image_equal_tofile(im, f"{os.path.splitext(path)[0]}.png") +from __future__ import annotations + +import os + +import pytest + +from PIL import Image, SunImagePlugin + +from .helper import assert_image_equal_tofile, assert_image_similar, hopper + +EXTRA_DIR = "Tests/images/sunraster" + + +def test_sanity() -> None: + # Arrange + # Created with ImageMagick: convert hopper.jpg hopper.ras + test_file = "Tests/images/hopper.ras" + + # Act + with Image.open(test_file) as im: + # Assert + assert im.size == (128, 128) + + assert_image_similar(im, hopper(), 5) # visually verified + + invalid_file = "Tests/images/flower.jpg" + with pytest.raises(SyntaxError): + SunImagePlugin.SunImageFile(invalid_file) + + +def test_im1() -> None: + with Image.open("Tests/images/sunraster.im1") as im: + assert_image_equal_tofile(im, "Tests/images/sunraster.im1.png") + + +@pytest.mark.skipif( + not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" +) +def test_others() -> None: + files = ( + os.path.join(EXTRA_DIR, f) + for f in os.listdir(EXTRA_DIR) + if os.path.splitext(f)[1] in (".sun", ".SUN", ".ras") + ) + for path in files: + with Image.open(path) as im: + im.load() + assert isinstance(im, SunImagePlugin.SunImageFile) + assert_image_equal_tofile(im, f"{os.path.splitext(path)[0]}.png") diff --git a/Tests/test_file_tar.py b/Tests/test_file_tar.py index 6217ebedd..9b5fbcdba 100644 --- a/Tests/test_file_tar.py +++ b/Tests/test_file_tar.py @@ -1,47 +1,47 @@ -from __future__ import annotations - -import warnings - -import pytest - -from PIL import Image, TarIO, features - -from .helper import is_pypy - -# Sample tar archive -TEST_TAR_FILE = "Tests/images/hopper.tar" - - -@pytest.mark.parametrize( - "codec, test_path, format", - ( - ("zlib", "hopper.png", "PNG"), - ("jpg", "hopper.jpg", "JPEG"), - ), -) -def test_sanity(codec: str, test_path: str, format: str) -> None: - if features.check(codec): - with TarIO.TarIO(TEST_TAR_FILE, test_path) as tar: - with Image.open(tar) as im: - im.load() - assert im.mode == "RGB" - assert im.size == (128, 128) - assert im.format == format - - -@pytest.mark.skipif(is_pypy(), reason="Requires CPython") -def test_unclosed_file() -> None: - with pytest.warns(ResourceWarning): - TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg") - - -def test_close() -> None: - with warnings.catch_warnings(): - tar = TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg") - tar.close() - - -def test_contextmanager() -> None: - with warnings.catch_warnings(): - with TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg"): - pass +from __future__ import annotations + +import warnings + +import pytest + +from PIL import Image, TarIO, features + +from .helper import is_pypy + +# Sample tar archive +TEST_TAR_FILE = "Tests/images/hopper.tar" + + +@pytest.mark.parametrize( + "codec, test_path, format", + ( + ("zlib", "hopper.png", "PNG"), + ("jpg", "hopper.jpg", "JPEG"), + ), +) +def test_sanity(codec: str, test_path: str, format: str) -> None: + if features.check(codec): + with TarIO.TarIO(TEST_TAR_FILE, test_path) as tar: + with Image.open(tar) as im: + im.load() + assert im.mode == "RGB" + assert im.size == (128, 128) + assert im.format == format + + +@pytest.mark.skipif(is_pypy(), reason="Requires CPython") +def test_unclosed_file() -> None: + with pytest.warns(ResourceWarning): + TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg") + + +def test_close() -> None: + with warnings.catch_warnings(): + tar = TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg") + tar.close() + + +def test_contextmanager() -> None: + with warnings.catch_warnings(): + with TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg"): + pass diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index ff6dab00d..8a3038dbe 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -1,262 +1,262 @@ -from __future__ import annotations - -import os -from glob import glob -from itertools import product -from pathlib import Path - -import pytest - -from PIL import Image, UnidentifiedImageError - -from .helper import assert_image_equal, assert_image_equal_tofile, hopper - -_TGA_DIR = os.path.join("Tests", "images", "tga") -_TGA_DIR_COMMON = os.path.join(_TGA_DIR, "common") - - -_MODES = ("L", "LA", "P", "RGB", "RGBA") -_ORIGINS = ("tl", "bl") - -_ORIGIN_TO_ORIENTATION = {"tl": 1, "bl": -1} - - -@pytest.mark.parametrize("mode", _MODES) -def test_sanity(mode: str, tmp_path: Path) -> None: - def roundtrip(original_im: Image.Image) -> None: - out = str(tmp_path / "temp.tga") - - original_im.save(out, rle=rle) - with Image.open(out) as saved_im: - if rle: - assert saved_im.info["compression"] == original_im.info["compression"] - assert saved_im.info["orientation"] == original_im.info["orientation"] - if mode == "P": - assert saved_im.getpalette() == original_im.getpalette() - - assert_image_equal(saved_im, original_im) - - png_paths = glob(os.path.join(_TGA_DIR_COMMON, f"*x*_{mode.lower()}.png")) - - for png_path in png_paths: - with Image.open(png_path) as reference_im: - assert reference_im.mode == mode - - path_no_ext = os.path.splitext(png_path)[0] - for origin, rle in product(_ORIGINS, (True, False)): - tga_path = "{}_{}_{}.tga".format( - path_no_ext, origin, "rle" if rle else "raw" - ) - - with Image.open(tga_path) as original_im: - assert original_im.format == "TGA" - assert original_im.get_format_mimetype() == "image/x-tga" - if rle: - assert original_im.info["compression"] == "tga_rle" - assert ( - original_im.info["orientation"] - == _ORIGIN_TO_ORIENTATION[origin] - ) - if mode == "P": - assert original_im.getpalette() == reference_im.getpalette() - - assert_image_equal(original_im, reference_im) - - roundtrip(original_im) - - -def test_palette_depth_8(tmp_path: Path) -> 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_image_equal_tofile(im.convert("RGB"), "Tests/images/p_16.png") - - out = str(tmp_path / "temp.png") - im.save(out) - with Image.open(out) as reloaded: - assert_image_equal_tofile(reloaded.convert("RGB"), "Tests/images/p_16.png") - - -def test_id_field() -> None: - # tga file with id field - test_file = "Tests/images/tga_id_field.tga" - - # Act - with Image.open(test_file) as im: - # Assert - assert im.size == (100, 100) - - -def test_id_field_rle() -> None: - # tga file with id field - test_file = "Tests/images/rgb32rle.tga" - - # Act - with Image.open(test_file) as im: - # Assert - assert im.size == (199, 199) - - -def test_cross_scan_line() -> None: - with Image.open("Tests/images/cross_scan_line.tga") as im: - assert_image_equal_tofile(im, "Tests/images/cross_scan_line.png") - - with Image.open("Tests/images/cross_scan_line_truncated.tga") as im: - with pytest.raises(OSError): - im.load() - - -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") - - # Save - im.save(out) - with Image.open(out) as test_im: - assert test_im.size == (100, 100) - assert test_im.info["id_section"] == im.info["id_section"] - - # RGBA save - im.convert("RGBA").save(out) - with Image.open(out) as test_im: - assert test_im.size == (100, 100) - - -def test_small_palette(tmp_path: Path) -> None: - im = Image.new("P", (1, 1)) - colors = [0, 0, 0] - im.putpalette(colors) - - out = str(tmp_path / "temp.tga") - im.save(out) - - with Image.open(out) as reloaded: - assert reloaded.getpalette() == colors - - -def test_missing_palette() -> None: - with Image.open("Tests/images/dilation4.lut") as im: - assert im.mode == "L" - - -def test_save_wrong_mode(tmp_path: Path) -> None: - im = hopper("PA") - out = str(tmp_path / "temp.tga") - - with pytest.raises(OSError): - im.save(out) - - -def test_save_mapdepth() -> None: - # This image has been manually hexedited from 200x32_p_bl_raw.tga - # to include an origin - test_file = "Tests/images/200x32_p_bl_raw_origin.tga" - with Image.open(test_file) as im: - assert_image_equal_tofile(im, "Tests/images/tga/common/200x32_p.png") - - -def test_save_id_section(tmp_path: Path) -> None: - test_file = "Tests/images/rgb32rle.tga" - with Image.open(test_file) as im: - out = str(tmp_path / "temp.tga") - - # Check there is no id section - im.save(out) - with Image.open(out) as test_im: - assert "id_section" not in test_im.info - - # Save with custom id section - im.save(out, id_section=b"Test content") - with Image.open(out) as test_im: - assert test_im.info["id_section"] == b"Test content" - - # Save with custom id section greater than 255 characters - id_section = b"Test content" * 25 - with pytest.warns(UserWarning): - im.save(out, id_section=id_section) - - with Image.open(out) as test_im: - assert test_im.info["id_section"] == id_section[:255] - - test_file = "Tests/images/tga_id_field.tga" - with Image.open(test_file) as im: - # Save with no id section - im.save(out, id_section="") - with Image.open(out) as test_im: - assert "id_section" not in test_im.info - - -def test_save_orientation(tmp_path: Path) -> None: - test_file = "Tests/images/rgb32rle.tga" - out = str(tmp_path / "temp.tga") - with Image.open(test_file) as im: - assert im.info["orientation"] == -1 - - im.save(out, orientation=1) - with Image.open(out) as test_im: - assert test_im.info["orientation"] == 1 - - -def test_horizontal_orientations() -> 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) - - with Image.open("Tests/images/rgb32rle_bottom_right.tga") as im: - assert im.load()[90, 90][:3] == (0, 255, 0) - - -def test_save_rle(tmp_path: Path) -> None: - test_file = "Tests/images/rgb32rle.tga" - with Image.open(test_file) as im: - assert im.info["compression"] == "tga_rle" - - out = str(tmp_path / "temp.tga") - - # Save - im.save(out) - with Image.open(out) as test_im: - assert test_im.size == (199, 199) - assert test_im.info["compression"] == "tga_rle" - - # Save without compression - im.save(out, compression=None) - with Image.open(out) as test_im: - assert "compression" not in test_im.info - - # RGBA save - im.convert("RGBA").save(out) - with Image.open(out) as test_im: - assert test_im.size == (199, 199) - - test_file = "Tests/images/tga_id_field.tga" - with Image.open(test_file) as im: - assert "compression" not in im.info - - # Save with compression - im.save(out, compression="tga_rle") - with Image.open(out) as test_im: - assert test_im.info["compression"] == "tga_rle" - - -def test_save_l_transparency(tmp_path: Path) -> None: - # There are 559 transparent pixels in la.tga. - num_transparent = 559 - - in_file = "Tests/images/la.tga" - with Image.open(in_file) as im: - assert im.mode == "LA" - assert im.getchannel("A").getcolors()[0][0] == num_transparent - - out = str(tmp_path / "temp.tga") - im.save(out) - - with Image.open(out) as test_im: - assert test_im.mode == "LA" - assert test_im.getchannel("A").getcolors()[0][0] == num_transparent - - assert_image_equal(im, test_im) +from __future__ import annotations + +import os +from glob import glob +from itertools import product +from pathlib import Path + +import pytest + +from PIL import Image, UnidentifiedImageError + +from .helper import assert_image_equal, assert_image_equal_tofile, hopper + +_TGA_DIR = os.path.join("Tests", "images", "tga") +_TGA_DIR_COMMON = os.path.join(_TGA_DIR, "common") + + +_MODES = ("L", "LA", "P", "RGB", "RGBA") +_ORIGINS = ("tl", "bl") + +_ORIGIN_TO_ORIENTATION = {"tl": 1, "bl": -1} + + +@pytest.mark.parametrize("mode", _MODES) +def test_sanity(mode: str, tmp_path: Path) -> None: + def roundtrip(original_im: Image.Image) -> None: + out = str(tmp_path / "temp.tga") + + original_im.save(out, rle=rle) + with Image.open(out) as saved_im: + if rle: + assert saved_im.info["compression"] == original_im.info["compression"] + assert saved_im.info["orientation"] == original_im.info["orientation"] + if mode == "P": + assert saved_im.getpalette() == original_im.getpalette() + + assert_image_equal(saved_im, original_im) + + png_paths = glob(os.path.join(_TGA_DIR_COMMON, f"*x*_{mode.lower()}.png")) + + for png_path in png_paths: + with Image.open(png_path) as reference_im: + assert reference_im.mode == mode + + path_no_ext = os.path.splitext(png_path)[0] + for origin, rle in product(_ORIGINS, (True, False)): + tga_path = "{}_{}_{}.tga".format( + path_no_ext, origin, "rle" if rle else "raw" + ) + + with Image.open(tga_path) as original_im: + assert original_im.format == "TGA" + assert original_im.get_format_mimetype() == "image/x-tga" + if rle: + assert original_im.info["compression"] == "tga_rle" + assert ( + original_im.info["orientation"] + == _ORIGIN_TO_ORIENTATION[origin] + ) + if mode == "P": + assert original_im.getpalette() == reference_im.getpalette() + + assert_image_equal(original_im, reference_im) + + roundtrip(original_im) + + +def test_palette_depth_8(tmp_path: Path) -> 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_image_equal_tofile(im.convert("RGB"), "Tests/images/p_16.png") + + out = str(tmp_path / "temp.png") + im.save(out) + with Image.open(out) as reloaded: + assert_image_equal_tofile(reloaded.convert("RGB"), "Tests/images/p_16.png") + + +def test_id_field() -> None: + # tga file with id field + test_file = "Tests/images/tga_id_field.tga" + + # Act + with Image.open(test_file) as im: + # Assert + assert im.size == (100, 100) + + +def test_id_field_rle() -> None: + # tga file with id field + test_file = "Tests/images/rgb32rle.tga" + + # Act + with Image.open(test_file) as im: + # Assert + assert im.size == (199, 199) + + +def test_cross_scan_line() -> None: + with Image.open("Tests/images/cross_scan_line.tga") as im: + assert_image_equal_tofile(im, "Tests/images/cross_scan_line.png") + + with Image.open("Tests/images/cross_scan_line_truncated.tga") as im: + with pytest.raises(OSError): + im.load() + + +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") + + # Save + im.save(out) + with Image.open(out) as test_im: + assert test_im.size == (100, 100) + assert test_im.info["id_section"] == im.info["id_section"] + + # RGBA save + im.convert("RGBA").save(out) + with Image.open(out) as test_im: + assert test_im.size == (100, 100) + + +def test_small_palette(tmp_path: Path) -> None: + im = Image.new("P", (1, 1)) + colors = [0, 0, 0] + im.putpalette(colors) + + out = str(tmp_path / "temp.tga") + im.save(out) + + with Image.open(out) as reloaded: + assert reloaded.getpalette() == colors + + +def test_missing_palette() -> None: + with Image.open("Tests/images/dilation4.lut") as im: + assert im.mode == "L" + + +def test_save_wrong_mode(tmp_path: Path) -> None: + im = hopper("PA") + out = str(tmp_path / "temp.tga") + + with pytest.raises(OSError): + im.save(out) + + +def test_save_mapdepth() -> None: + # This image has been manually hexedited from 200x32_p_bl_raw.tga + # to include an origin + test_file = "Tests/images/200x32_p_bl_raw_origin.tga" + with Image.open(test_file) as im: + assert_image_equal_tofile(im, "Tests/images/tga/common/200x32_p.png") + + +def test_save_id_section(tmp_path: Path) -> None: + test_file = "Tests/images/rgb32rle.tga" + with Image.open(test_file) as im: + out = str(tmp_path / "temp.tga") + + # Check there is no id section + im.save(out) + with Image.open(out) as test_im: + assert "id_section" not in test_im.info + + # Save with custom id section + im.save(out, id_section=b"Test content") + with Image.open(out) as test_im: + assert test_im.info["id_section"] == b"Test content" + + # Save with custom id section greater than 255 characters + id_section = b"Test content" * 25 + with pytest.warns(UserWarning): + im.save(out, id_section=id_section) + + with Image.open(out) as test_im: + assert test_im.info["id_section"] == id_section[:255] + + test_file = "Tests/images/tga_id_field.tga" + with Image.open(test_file) as im: + # Save with no id section + im.save(out, id_section="") + with Image.open(out) as test_im: + assert "id_section" not in test_im.info + + +def test_save_orientation(tmp_path: Path) -> None: + test_file = "Tests/images/rgb32rle.tga" + out = str(tmp_path / "temp.tga") + with Image.open(test_file) as im: + assert im.info["orientation"] == -1 + + im.save(out, orientation=1) + with Image.open(out) as test_im: + assert test_im.info["orientation"] == 1 + + +def test_horizontal_orientations() -> 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) + + with Image.open("Tests/images/rgb32rle_bottom_right.tga") as im: + assert im.load()[90, 90][:3] == (0, 255, 0) + + +def test_save_rle(tmp_path: Path) -> None: + test_file = "Tests/images/rgb32rle.tga" + with Image.open(test_file) as im: + assert im.info["compression"] == "tga_rle" + + out = str(tmp_path / "temp.tga") + + # Save + im.save(out) + with Image.open(out) as test_im: + assert test_im.size == (199, 199) + assert test_im.info["compression"] == "tga_rle" + + # Save without compression + im.save(out, compression=None) + with Image.open(out) as test_im: + assert "compression" not in test_im.info + + # RGBA save + im.convert("RGBA").save(out) + with Image.open(out) as test_im: + assert test_im.size == (199, 199) + + test_file = "Tests/images/tga_id_field.tga" + with Image.open(test_file) as im: + assert "compression" not in im.info + + # Save with compression + im.save(out, compression="tga_rle") + with Image.open(out) as test_im: + assert test_im.info["compression"] == "tga_rle" + + +def test_save_l_transparency(tmp_path: Path) -> None: + # There are 559 transparent pixels in la.tga. + num_transparent = 559 + + in_file = "Tests/images/la.tga" + with Image.open(in_file) as im: + assert im.mode == "LA" + assert im.getchannel("A").getcolors()[0][0] == num_transparent + + out = str(tmp_path / "temp.tga") + im.save(out) + + with Image.open(out) as test_im: + assert test_im.mode == "LA" + assert test_im.getchannel("A").getcolors()[0][0] == num_transparent + + assert_image_equal(im, test_im) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 8821fb46a..60a2bb241 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -1,900 +1,900 @@ -from __future__ import annotations - -import os -import warnings -from io import BytesIO -from pathlib import Path -from types import ModuleType -from typing import Generator - -import pytest - -from PIL import Image, ImageFile, TiffImagePlugin, UnidentifiedImageError -from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION - -from .helper import ( - assert_image_equal, - assert_image_equal_tofile, - assert_image_similar, - assert_image_similar_tofile, - hopper, - is_pypy, - is_win32, -) - -ElementTree: ModuleType | None -try: - from defusedxml import ElementTree -except ImportError: - ElementTree = None - - -class TestFileTiff: - def test_sanity(self, tmp_path: Path) -> None: - filename = str(tmp_path / "temp.tif") - - hopper("RGB").save(filename) - - with Image.open(filename) as im: - im.load() - assert im.mode == "RGB" - assert im.size == (128, 128) - assert im.format == "TIFF" - - hopper("1").save(filename) - 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 - - @pytest.mark.skipif(is_pypy(), reason="Requires CPython") - def test_unclosed_file(self) -> None: - def open() -> None: - im = Image.open("Tests/images/multipage.tiff") - im.load() - - with pytest.warns(ResourceWarning): - open() - - def test_closed_file(self) -> None: - with warnings.catch_warnings(): - im = Image.open("Tests/images/multipage.tiff") - im.load() - im.close() - - def test_seek_after_close(self) -> None: - im = Image.open("Tests/images/multipage.tiff") - im.close() - - with pytest.raises(ValueError): - im.n_frames - with pytest.raises(ValueError): - im.seek(1) - - def test_context_manager(self) -> None: - with warnings.catch_warnings(): - with Image.open("Tests/images/multipage.tiff") as im: - im.load() - - def test_mac_tiff(self) -> None: - # Read RGBa images from macOS [@PIL136] - - filename = "Tests/images/pil136.tiff" - with Image.open(filename) as im: - assert im.mode == "RGBA" - assert im.size == (55, 43) - assert im.tile == [("raw", (0, 0, 55, 43), 8, ("RGBa", 0, 1))] - im.load() - - assert_image_similar_tofile(im, "Tests/images/pil136.png", 1) - - def test_bigtiff(self, tmp_path: Path) -> None: - with Image.open("Tests/images/hopper_bigtiff.tif") as im: - assert_image_equal_tofile(im, "Tests/images/hopper.tif") - - with Image.open("Tests/images/hopper_bigtiff.tif") as im: - # multistrip support not yet implemented - del im.tag_v2[273] - - outfile = str(tmp_path / "temp.tif") - im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2) - - def test_seek_too_large(self): - 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: - ifd.legacy_api = None - 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: - # legacy api - assert isinstance(im.tag[X_RESOLUTION][0], tuple) - assert isinstance(im.tag[Y_RESOLUTION][0], tuple) - - # v2 api - assert isinstance(im.tag_v2[X_RESOLUTION], TiffImagePlugin.IFDRational) - assert isinstance(im.tag_v2[Y_RESOLUTION], TiffImagePlugin.IFDRational) - - assert im.info["dpi"] == (72.0, 72.0) - - def test_xyres_fallback_tiff(self) -> None: - filename = "Tests/images/compression.tif" - with Image.open(filename) as im: - # v2 api - assert isinstance(im.tag_v2[X_RESOLUTION], TiffImagePlugin.IFDRational) - assert isinstance(im.tag_v2[Y_RESOLUTION], TiffImagePlugin.IFDRational) - with pytest.raises(KeyError): - im.tag_v2[RESOLUTION_UNIT] - - # Legacy. - assert im.info["resolution"] == (100.0, 100.0) - # Fallback "inch". - assert im.info["dpi"] == (100.0, 100.0) - - def test_int_resolution(self) -> None: - filename = "Tests/images/pil168.tif" - with Image.open(filename) as im: - # Try to read a file where X,Y_RESOLUTION are ints - im.tag_v2[X_RESOLUTION] = 71 - im.tag_v2[Y_RESOLUTION] = 71 - im._setup() - assert im.info["dpi"] == (71.0, 71.0) - - @pytest.mark.parametrize( - "resolution_unit, dpi", - [(None, 72.8), (2, 72.8), (3, 184.912)], - ) - def test_load_float_dpi(self, resolution_unit: int | None, dpi: float) -> None: - with Image.open( - "Tests/images/hopper_float_dpi_" + str(resolution_unit) + ".tif" - ) as im: - assert im.tag_v2.get(RESOLUTION_UNIT) == resolution_unit - assert im.info["dpi"] == (dpi, dpi) - - def test_save_float_dpi(self, tmp_path: Path) -> None: - outfile = str(tmp_path / "temp.tif") - with Image.open("Tests/images/hopper.tif") as im: - dpi = (72.2, 72.2) - im.save(outfile, dpi=dpi) - - with Image.open(outfile) as reloaded: - assert reloaded.info["dpi"] == dpi - - def test_save_setting_missing_resolution(self) -> None: - b = BytesIO() - with Image.open("Tests/images/10ct_32bit_128.tiff") as im: - im.save(b, format="tiff", resolution=123.45) - with Image.open(b) as im: - assert im.tag_v2[X_RESOLUTION] == 123.45 - assert im.tag_v2[Y_RESOLUTION] == 123.45 - - def test_invalid_file(self) -> None: - invalid_file = "Tests/images/flower.jpg" - - with pytest.raises(SyntaxError): - TiffImagePlugin.TiffImageFile(invalid_file) - - TiffImagePlugin.PREFIXES.append(b"\xff\xd8\xff\xe0") - with pytest.raises(SyntaxError): - TiffImagePlugin.TiffImageFile(invalid_file) - TiffImagePlugin.PREFIXES.pop() - - def test_bad_exif(self) -> None: - with Image.open("Tests/images/hopper_bad_exif.jpg") as i: - # Should not raise struct.error. - with pytest.warns(UserWarning): - i._getexif() - - def test_save_rgba(self, tmp_path: Path) -> None: - im = hopper("RGBA") - outfile = str(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") - with pytest.raises(OSError): - im.save(outfile) - - def test_8bit_s(self) -> None: - with Image.open("Tests/images/8bit.s.tif") as im: - im.load() - assert im.mode == "L" - assert im.getpixel((50, 50)) == 184 - - def test_little_endian(self) -> None: - with Image.open("Tests/images/16bit.cropped.tif") as im: - assert im.getpixel((0, 0)) == 480 - assert im.mode == "I;16" - - b = im.tobytes() - # Bytes are in image native order (little endian) - assert b[0] == ord(b"\xe0") - assert b[1] == ord(b"\x01") - - def test_big_endian(self) -> None: - with Image.open("Tests/images/16bit.MM.cropped.tif") as im: - assert im.getpixel((0, 0)) == 480 - assert im.mode == "I;16B" - - b = im.tobytes() - # Bytes are in image native order (big endian) - assert b[0] == ord(b"\x01") - assert b[1] == ord(b"\xe0") - - def test_16bit_r(self) -> None: - with Image.open("Tests/images/16bit.r.tif") as im: - assert im.getpixel((0, 0)) == 480 - assert im.mode == "I;16" - - b = im.tobytes() - assert b[0] == ord(b"\xe0") - assert b[1] == ord(b"\x01") - - def test_16bit_s(self) -> None: - with Image.open("Tests/images/16bit.s.tif") as im: - im.load() - assert im.mode == "I" - assert im.getpixel((0, 0)) == 32767 - assert im.getpixel((0, 1)) == 0 - - def test_12bit_rawmode(self) -> None: - """Are we generating the same interpretation - of the image as Imagemagick is?""" - - with Image.open("Tests/images/12bit.cropped.tif") as im: - # to make the target -- - # convert 12bit.cropped.tif -depth 16 tmp.tif - # convert tmp.tif -evaluate RightShift 4 12in16bit2.tif - # imagemagick will auto scale so that a 12bit FFF is 16bit FFF0, - # so we need to unshift so that the integer values are the same. - - assert_image_equal_tofile(im, "Tests/images/12in16bit.tif") - - def test_32bit_float(self) -> None: - # Issue 614, specific 32-bit float format - path = "Tests/images/10ct_32bit_128.tiff" - with Image.open(path) as im: - im.load() - - assert im.getpixel((0, 0)) == -0.4526388943195343 - assert im.getextrema() == (-3.140936851501465, 3.140684127807617) - - def test_unknown_pixel_mode(self) -> None: - with pytest.raises(OSError): - with Image.open("Tests/images/hopper_unknown_pixel_mode.tif"): - pass - - @pytest.mark.parametrize( - "path, n_frames", - ( - ("Tests/images/multipage-lastframe.tif", 1), - ("Tests/images/multipage.tiff", 3), - ), - ) - def test_n_frames(self, path: str, n_frames: int) -> None: - with Image.open(path) as im: - 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: - n_frames = im.n_frames - - # Test seeking past the last frame - with pytest.raises(EOFError): - im.seek(n_frames) - assert im.tell() < n_frames - - # Test that seeking to the last frame does not raise an error - im.seek(n_frames - 1) - - def test_multipage(self) -> None: - # issue #862 - with Image.open("Tests/images/multipage.tiff") as im: - # file is a multipage tiff: 10x10 green, 10x10 red, 20x20 blue - - im.seek(0) - assert im.size == (10, 10) - assert im.convert("RGB").getpixel((0, 0)) == (0, 128, 0) - - im.seek(1) - im.load() - assert im.size == (10, 10) - assert im.convert("RGB").getpixel((0, 0)) == (255, 0, 0) - - im.seek(0) - im.load() - assert im.size == (10, 10) - assert im.convert("RGB").getpixel((0, 0)) == (0, 128, 0) - - im.seek(2) - im.load() - assert im.size == (20, 20) - assert im.convert("RGB").getpixel((0, 0)) == (0, 0, 255) - - def test_multipage_last_frame(self) -> None: - with Image.open("Tests/images/multipage-lastframe.tif") as im: - im.load() - assert im.size == (20, 20) - assert im.convert("RGB").getpixel((0, 0)) == (0, 0, 255) - - def test_frame_order(self) -> None: - # A frame can't progress to itself after reading - with Image.open("Tests/images/multipage_single_frame_loop.tiff") as im: - assert im.n_frames == 1 - - # A frame can't progress to a frame that has already been read - with Image.open("Tests/images/multipage_multiple_frame_loop.tiff") as im: - assert im.n_frames == 2 - - # Frames don't have to be in sequence - with Image.open("Tests/images/multipage_out_of_order.tiff") as im: - assert im.n_frames == 3 - - def test___str__(self) -> None: - filename = "Tests/images/pil136.tiff" - with Image.open(filename) as im: - # Act - ret = str(im.ifd) - - # Assert - assert isinstance(ret, str) - - def test_dict(self) -> None: - # Arrange - filename = "Tests/images/pil136.tiff" - with Image.open(filename) as im: - # v2 interface - v2_tags = { - 256: 55, - 257: 43, - 258: (8, 8, 8, 8), - 259: 1, - 262: 2, - 296: 2, - 273: (8,), - 338: (1,), - 277: 4, - 279: (9460,), - 282: 72.0, - 283: 72.0, - 284: 1, - } - assert dict(im.tag_v2) == v2_tags - - # legacy interface - legacy_tags = { - 256: (55,), - 257: (43,), - 258: (8, 8, 8, 8), - 259: (1,), - 262: (2,), - 296: (2,), - 273: (8,), - 338: (1,), - 277: (4,), - 279: (9460,), - 282: ((720000, 10000),), - 283: ((720000, 10000),), - 284: (1,), - } - assert dict(im.tag) == legacy_tags - - def test__delitem__(self) -> None: - filename = "Tests/images/pil136.tiff" - with Image.open(filename) as im: - len_before = len(dict(im.ifd)) - del im.ifd[256] - len_after = len(dict(im.ifd)) - assert len_before == len_after + 1 - - @pytest.mark.parametrize("legacy_api", (False, True)) - def test_load_byte(self, legacy_api: bool) -> None: - ifd = TiffImagePlugin.ImageFileDirectory_v2() - data = b"abc" - ret = ifd.load_byte(data, legacy_api) - assert ret == b"abc" - - def test_load_string(self) -> None: - ifd = TiffImagePlugin.ImageFileDirectory_v2() - data = b"abc\0" - ret = ifd.load_string(data, False) - assert ret == "abc" - - def test_load_float(self) -> None: - ifd = TiffImagePlugin.ImageFileDirectory_v2() - data = b"abcdabcd" - ret = ifd.load_float(data, False) - assert ret == (1.6777999408082104e22, 1.6777999408082104e22) - - def test_load_double(self) -> None: - ifd = TiffImagePlugin.ImageFileDirectory_v2() - data = b"abcdefghabcdefgh" - ret = ifd.load_double(data, False) - assert ret == (8.540883223036124e194, 8.540883223036124e194) - - def test_ifd_tag_type(self) -> None: - with Image.open("Tests/images/ifd_tag_type.tiff") as im: - assert 0x8825 in im.tag_v2 - - def test_exif(self, tmp_path: Path) -> None: - def check_exif(exif: Image.Exif) -> None: - assert sorted(exif.keys()) == [ - 256, - 257, - 258, - 259, - 262, - 271, - 272, - 273, - 277, - 278, - 279, - 282, - 283, - 284, - 296, - 297, - 305, - 339, - 700, - 34665, - 34853, - 50735, - ] - assert exif[256] == 640 - assert exif[271] == "FLIR" - - gps = exif.get_ifd(0x8825) - assert list(gps.keys()) == [0, 1, 2, 3, 4, 5, 6, 18] - assert gps[0] == b"\x03\x02\x00\x00" - assert gps[18] == "WGS-84" - - outfile = str(tmp_path / "temp.tif") - with Image.open("Tests/images/ifd_tag_type.tiff") as im: - exif = im.getexif() - check_exif(exif) - - im.save(outfile, exif=exif) - - outfile2 = str(tmp_path / "temp2.tif") - with Image.open(outfile) as im: - exif = im.getexif() - check_exif(exif) - - im.save(outfile2, exif=exif.tobytes()) - - with Image.open(outfile2) as im: - exif = im.getexif() - check_exif(exif) - - def test_modify_exif(self, tmp_path: Path) -> None: - outfile = str(tmp_path / "temp.tif") - with Image.open("Tests/images/ifd_tag_type.tiff") as im: - exif = im.getexif() - exif[264] = 100 - - im.save(outfile, exif=exif) - - with Image.open(outfile) as im: - exif = im.getexif() - assert exif[264] == 100 - - def test_reload_exif_after_seek(self) -> None: - with Image.open("Tests/images/multipage.tiff") as im: - exif = im.getexif() - del exif[256] - im.seek(1) - - assert 256 in exif - - def test_exif_frames(self) -> None: - # Test that EXIF data can change across frames - with Image.open("Tests/images/g4-multi.tiff") as im: - assert im.getexif()[273] == (328, 815) - - im.seek(1) - assert im.getexif()[273] == (1408, 1907) - - @pytest.mark.parametrize("mode", ("1", "L")) - def test_photometric(self, mode: str, tmp_path: Path) -> None: - filename = str(tmp_path / "temp.tif") - im = hopper(mode) - im.save(filename, tiffinfo={262: 0}) - with Image.open(filename) as reloaded: - assert reloaded.tag_v2[262] == 0 - assert_image_equal(im, reloaded) - - def test_seek(self) -> None: - filename = "Tests/images/pil136.tiff" - with Image.open(filename) as im: - im.seek(0) - assert im.tell() == 0 - - def test_seek_eof(self) -> None: - filename = "Tests/images/pil136.tiff" - with Image.open(filename) as im: - assert im.tell() == 0 - with pytest.raises(EOFError): - im.seek(-1) - with pytest.raises(EOFError): - im.seek(1) - - def test__limit_rational_int(self) -> None: - from PIL.TiffImagePlugin import _limit_rational - - value = 34 - ret = _limit_rational(value, 65536) - assert ret == (34, 1) - - def test__limit_rational_float(self) -> None: - from PIL.TiffImagePlugin import _limit_rational - - value = 22.3 - ret = _limit_rational(value, 65536) - assert ret == (223, 10) - - def test_4bit(self) -> None: - test_file = "Tests/images/hopper_gray_4bpp.tif" - original = hopper("L") - with Image.open(test_file) as im: - assert im.size == (128, 128) - assert im.mode == "L" - assert_image_similar(im, original, 7.3) - - def test_gray_semibyte_per_pixel(self) -> None: - test_files = ( - ( - 24.8, # epsilon - ( # group - "Tests/images/tiff_gray_2_4_bpp/hopper2.tif", - "Tests/images/tiff_gray_2_4_bpp/hopper2I.tif", - "Tests/images/tiff_gray_2_4_bpp/hopper2R.tif", - "Tests/images/tiff_gray_2_4_bpp/hopper2IR.tif", - ), - ), - ( - 7.3, # epsilon - ( # group - "Tests/images/tiff_gray_2_4_bpp/hopper4.tif", - "Tests/images/tiff_gray_2_4_bpp/hopper4I.tif", - "Tests/images/tiff_gray_2_4_bpp/hopper4R.tif", - "Tests/images/tiff_gray_2_4_bpp/hopper4IR.tif", - ), - ), - ) - original = hopper("L") - for epsilon, group in test_files: - with Image.open(group[0]) as im: - assert im.size == (128, 128) - assert im.mode == "L" - assert_image_similar(im, original, epsilon) - for file in group[1:]: - with Image.open(file) as im2: - assert im2.size == (128, 128) - assert im2.mode == "L" - assert_image_equal(im, im2) - - def test_with_underscores(self, tmp_path: Path) -> None: - kwargs = {"resolution_unit": "inch", "x_resolution": 72, "y_resolution": 36} - filename = str(tmp_path / "temp.tif") - hopper("RGB").save(filename, **kwargs) - with Image.open(filename) as im: - # legacy interface - assert im.tag[X_RESOLUTION][0][0] == 72 - assert im.tag[Y_RESOLUTION][0][0] == 36 - - # v2 interface - assert im.tag_v2[X_RESOLUTION] == 72 - assert im.tag_v2[Y_RESOLUTION] == 36 - - def test_roundtrip_tiff_uint16(self, tmp_path: Path) -> None: - # Test an image of all '0' values - pixel_value = 0x1234 - infile = "Tests/images/uint16_1_4660.tif" - with Image.open(infile) as im: - assert im.getpixel((0, 0)) == pixel_value - - tmpfile = str(tmp_path / "temp.tif") - im.save(tmpfile) - - assert_image_equal_tofile(im, tmpfile) - - def test_rowsperstrip(self, tmp_path: Path) -> None: - outfile = str(tmp_path / "temp.tif") - im = hopper() - im.save(outfile, tiffinfo={278: 256}) - - with Image.open(outfile) as im: - assert isinstance(im, TiffImagePlugin.TiffImageFile) - 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: - assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") - - def test_strip_planar_raw(self) -> None: - # gdal_translate -of GTiff -co INTERLEAVE=BAND \ - # tiff_strip_raw.tif tiff_strip_planar_raw.tiff - infile = "Tests/images/tiff_strip_planar_raw.tif" - with Image.open(infile) as im: - assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") - - def test_strip_planar_raw_with_overviews(self) -> None: - # gdaladdo tiff_strip_planar_raw2.tif 2 4 8 16 - infile = "Tests/images/tiff_strip_planar_raw_with_overviews.tif" - with Image.open(infile) as im: - assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") - - def test_tiled_planar_raw(self) -> None: - # gdal_translate -of GTiff -co TILED=YES -co BLOCKXSIZE=32 \ - # -co BLOCKYSIZE=32 -co INTERLEAVE=BAND \ - # tiff_tiled_raw.tif tiff_tiled_planar_raw.tiff - infile = "Tests/images/tiff_tiled_planar_raw.tif" - with Image.open(infile) as im: - assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") - - def test_planar_configuration_save(self, tmp_path: Path) -> None: - infile = "Tests/images/tiff_tiled_planar_raw.tif" - with Image.open(infile) as im: - assert im._planar_configuration == 2 - - outfile = str(tmp_path / "temp.tif") - im.save(outfile) - - with Image.open(outfile) as reloaded: - assert_image_equal_tofile(reloaded, infile) - - @pytest.mark.parametrize("mode", ("P", "PA")) - def test_palette(self, mode: str, tmp_path: Path) -> None: - outfile = str(tmp_path / "temp.tif") - - im = hopper(mode) - im.save(outfile) - - with Image.open(outfile) as reloaded: - assert_image_equal(im.convert("RGB"), reloaded.convert("RGB")) - - def test_tiff_save_all(self) -> None: - mp = BytesIO() - with Image.open("Tests/images/multipage.tiff") as im: - im.save(mp, format="tiff", save_all=True) - - mp.seek(0, os.SEEK_SET) - with Image.open(mp) as im: - assert im.n_frames == 3 - - # Test appending images - mp = BytesIO() - im = Image.new("RGB", (100, 100), "#f00") - ims = [Image.new("RGB", (100, 100), color) for color in ["#0f0", "#00f"]] - im.copy().save(mp, format="TIFF", save_all=True, append_images=ims) - - mp.seek(0, os.SEEK_SET) - with Image.open(mp) as reread: - assert reread.n_frames == 3 - - # Test appending using a generator - def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]: - yield from ims - - mp = BytesIO() - im.save(mp, format="TIFF", save_all=True, append_images=im_generator(ims)) - - mp.seek(0, os.SEEK_SET) - with Image.open(mp) as reread: - assert reread.n_frames == 3 - - 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 - # as libtiff does not support embedded ICC profiles, - # ImageFile._save(..) however does. - im = Image.new("RGB", (1, 1)) - im.info["icc_profile"] = "Dummy value" - - # Try save-load round trip to make sure both handle icc_profile. - tmpfile = str(tmp_path / "temp.tif") - im.save(tmpfile, "TIFF", compression="raw") - with Image.open(tmpfile) as reloaded: - assert b"Dummy value" == reloaded.info["icc_profile"] - - def test_save_icc_profile(self, tmp_path: Path) -> None: - im = hopper() - assert "icc_profile" not in im.info - - outfile = str(tmp_path / "temp.tif") - icc_profile = b"Dummy value" - im.save(outfile, icc_profile=icc_profile) - - with Image.open(outfile) as reloaded: - assert reloaded.info["icc_profile"] == icc_profile - - def test_save_bmp_compression(self, tmp_path: Path) -> None: - with Image.open("Tests/images/hopper.bmp") as im: - assert im.info["compression"] == 0 - - outfile = str(tmp_path / "temp.tif") - im.save(outfile) - - def test_discard_icc_profile(self, tmp_path: Path) -> None: - outfile = str(tmp_path / "temp.tif") - - with Image.open("Tests/images/icc_profile.png") as im: - assert "icc_profile" in im.info - - im.save(outfile, icc_profile=None) - - with Image.open(outfile) as reloaded: - assert "icc_profile" not in reloaded.info - - def test_getxmp(self) -> None: - with Image.open("Tests/images/lab.tif") as im: - if ElementTree is None: - with pytest.warns( - UserWarning, - match="XMP data cannot be read without defusedxml dependency", - ): - assert im.getxmp() == {} - else: - xmp = im.getxmp() - - description = xmp["xmpmeta"]["RDF"]["Description"] - assert description[0]["format"] == "image/tiff" - assert description[3]["BitsPerSample"]["Seq"]["li"] == ["8", "8", "8"] - - def test_get_photoshop_blocks(self) -> None: - with Image.open("Tests/images/lab.tif") as im: - assert list(im.get_photoshop_blocks().keys()) == [ - 1061, - 1002, - 1005, - 1062, - 1037, - 1049, - 1011, - 1034, - 10000, - 1013, - 1016, - 1032, - 1054, - 1050, - 1064, - 1041, - 1044, - 1036, - 1057, - 4000, - 4001, - ] - - def test_tiff_chunks(self, tmp_path: Path) -> None: - tmpfile = str(tmp_path / "temp.tif") - - im = hopper() - with open(tmpfile, "wb") as fp: - for y in range(0, 128, 32): - chunk = im.crop((0, y, 128, y + 32)) - if y == 0: - chunk.save( - fp, - "TIFF", - tiffinfo={ - TiffImagePlugin.IMAGEWIDTH: 128, - TiffImagePlugin.IMAGELENGTH: 128, - }, - ) - else: - fp.write(chunk.tobytes()) - - assert_image_equal_tofile(im, tmpfile) - - 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") - - with Image.open("Tests/images/uint16_1_4660.tif") as im: - im.save(tmpfile) - - im = Image.open(tmpfile) - fp = im.fp - assert not fp.closed - im.load() - assert fp.closed - - def test_close_on_load_nonexclusive(self, tmp_path: Path) -> None: - tmpfile = str(tmp_path / "temp.tif") - - with Image.open("Tests/images/uint16_1_4660.tif") as im: - im.save(tmpfile) - - with open(tmpfile, "rb") as f: - im = Image.open(f) - fp = im.fp - assert not fp.closed - im.load() - assert not fp.closed - - # Ignore this UserWarning which triggers for four tags: - # "Possibly corrupt EXIF data. Expecting to read 50404352 bytes but..." - @pytest.mark.filterwarnings("ignore:Possibly corrupt EXIF data") - # Ignore this UserWarning: - @pytest.mark.filterwarnings("ignore:Truncated File Read") - @pytest.mark.skipif( - not os.path.exists("Tests/images/string_dimension.tiff"), - reason="Extra image files not installed", - ) - def test_string_dimension(self) -> None: - # Assert that an error is raised if one of the dimensions is a string - with Image.open("Tests/images/string_dimension.tiff") as im: - with pytest.raises(OSError): - im.load() - - @pytest.mark.timeout(6) - @pytest.mark.filterwarnings("ignore:Truncated File Read") - def test_timeout(self) -> None: - with Image.open("Tests/images/timeout-6646305047838720") as im: - ImageFile.LOAD_TRUNCATED_IMAGES = True - im.load() - ImageFile.LOAD_TRUNCATED_IMAGES = False - - @pytest.mark.parametrize( - "test_file", - [ - "Tests/images/oom-225817ca0f8c663be7ab4b9e717b02c661e66834.tif", - ], - ) - @pytest.mark.timeout(2) - def test_oom(self, test_file: str) -> None: - with pytest.raises(UnidentifiedImageError): - with pytest.warns(UserWarning): - with Image.open(test_file): - pass - - -@pytest.mark.skipif(not is_win32(), reason="Windows only") -class TestFileTiffW32: - def test_fd_leak(self, tmp_path: Path) -> None: - tmpfile = str(tmp_path / "temp.tif") - - # this is an mmaped file. - with Image.open("Tests/images/uint16_1_4660.tif") as im: - im.save(tmpfile) - - im = Image.open(tmpfile) - fp = im.fp - assert not fp.closed - with pytest.raises(OSError): - os.remove(tmpfile) - im.load() - assert fp.closed - - # this closes the mmap - im.close() - - # this should not fail, as load should have closed the file pointer, - # and close should have closed the mmap - os.remove(tmpfile) +from __future__ import annotations + +import os +import warnings +from io import BytesIO +from pathlib import Path +from types import ModuleType +from typing import Generator + +import pytest + +from PIL import Image, ImageFile, TiffImagePlugin, UnidentifiedImageError +from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION + +from .helper import ( + assert_image_equal, + assert_image_equal_tofile, + assert_image_similar, + assert_image_similar_tofile, + hopper, + is_pypy, + is_win32, +) + +ElementTree: ModuleType | None +try: + from defusedxml import ElementTree +except ImportError: + ElementTree = None + + +class TestFileTiff: + def test_sanity(self, tmp_path: Path) -> None: + filename = str(tmp_path / "temp.tif") + + hopper("RGB").save(filename) + + with Image.open(filename) as im: + im.load() + assert im.mode == "RGB" + assert im.size == (128, 128) + assert im.format == "TIFF" + + hopper("1").save(filename) + 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 + + @pytest.mark.skipif(is_pypy(), reason="Requires CPython") + def test_unclosed_file(self) -> None: + def open() -> None: + im = Image.open("Tests/images/multipage.tiff") + im.load() + + with pytest.warns(ResourceWarning): + open() + + def test_closed_file(self) -> None: + with warnings.catch_warnings(): + im = Image.open("Tests/images/multipage.tiff") + im.load() + im.close() + + def test_seek_after_close(self) -> None: + im = Image.open("Tests/images/multipage.tiff") + im.close() + + with pytest.raises(ValueError): + im.n_frames + with pytest.raises(ValueError): + im.seek(1) + + def test_context_manager(self) -> None: + with warnings.catch_warnings(): + with Image.open("Tests/images/multipage.tiff") as im: + im.load() + + def test_mac_tiff(self) -> None: + # Read RGBa images from macOS [@PIL136] + + filename = "Tests/images/pil136.tiff" + with Image.open(filename) as im: + assert im.mode == "RGBA" + assert im.size == (55, 43) + assert im.tile == [("raw", (0, 0, 55, 43), 8, ("RGBa", 0, 1))] + im.load() + + assert_image_similar_tofile(im, "Tests/images/pil136.png", 1) + + def test_bigtiff(self, tmp_path: Path) -> None: + with Image.open("Tests/images/hopper_bigtiff.tif") as im: + assert_image_equal_tofile(im, "Tests/images/hopper.tif") + + with Image.open("Tests/images/hopper_bigtiff.tif") as im: + # multistrip support not yet implemented + del im.tag_v2[273] + + outfile = str(tmp_path / "temp.tif") + im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2) + + def test_seek_too_large(self): + 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: + ifd.legacy_api = None + 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: + # legacy api + assert isinstance(im.tag[X_RESOLUTION][0], tuple) + assert isinstance(im.tag[Y_RESOLUTION][0], tuple) + + # v2 api + assert isinstance(im.tag_v2[X_RESOLUTION], TiffImagePlugin.IFDRational) + assert isinstance(im.tag_v2[Y_RESOLUTION], TiffImagePlugin.IFDRational) + + assert im.info["dpi"] == (72.0, 72.0) + + def test_xyres_fallback_tiff(self) -> None: + filename = "Tests/images/compression.tif" + with Image.open(filename) as im: + # v2 api + assert isinstance(im.tag_v2[X_RESOLUTION], TiffImagePlugin.IFDRational) + assert isinstance(im.tag_v2[Y_RESOLUTION], TiffImagePlugin.IFDRational) + with pytest.raises(KeyError): + im.tag_v2[RESOLUTION_UNIT] + + # Legacy. + assert im.info["resolution"] == (100.0, 100.0) + # Fallback "inch". + assert im.info["dpi"] == (100.0, 100.0) + + def test_int_resolution(self) -> None: + filename = "Tests/images/pil168.tif" + with Image.open(filename) as im: + # Try to read a file where X,Y_RESOLUTION are ints + im.tag_v2[X_RESOLUTION] = 71 + im.tag_v2[Y_RESOLUTION] = 71 + im._setup() + assert im.info["dpi"] == (71.0, 71.0) + + @pytest.mark.parametrize( + "resolution_unit, dpi", + [(None, 72.8), (2, 72.8), (3, 184.912)], + ) + def test_load_float_dpi(self, resolution_unit: int | None, dpi: float) -> None: + with Image.open( + "Tests/images/hopper_float_dpi_" + str(resolution_unit) + ".tif" + ) as im: + assert im.tag_v2.get(RESOLUTION_UNIT) == resolution_unit + assert im.info["dpi"] == (dpi, dpi) + + def test_save_float_dpi(self, tmp_path: Path) -> None: + outfile = str(tmp_path / "temp.tif") + with Image.open("Tests/images/hopper.tif") as im: + dpi = (72.2, 72.2) + im.save(outfile, dpi=dpi) + + with Image.open(outfile) as reloaded: + assert reloaded.info["dpi"] == dpi + + def test_save_setting_missing_resolution(self) -> None: + b = BytesIO() + with Image.open("Tests/images/10ct_32bit_128.tiff") as im: + im.save(b, format="tiff", resolution=123.45) + with Image.open(b) as im: + assert im.tag_v2[X_RESOLUTION] == 123.45 + assert im.tag_v2[Y_RESOLUTION] == 123.45 + + def test_invalid_file(self) -> None: + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(SyntaxError): + TiffImagePlugin.TiffImageFile(invalid_file) + + TiffImagePlugin.PREFIXES.append(b"\xff\xd8\xff\xe0") + with pytest.raises(SyntaxError): + TiffImagePlugin.TiffImageFile(invalid_file) + TiffImagePlugin.PREFIXES.pop() + + def test_bad_exif(self) -> None: + with Image.open("Tests/images/hopper_bad_exif.jpg") as i: + # Should not raise struct.error. + with pytest.warns(UserWarning): + i._getexif() + + def test_save_rgba(self, tmp_path: Path) -> None: + im = hopper("RGBA") + outfile = str(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") + with pytest.raises(OSError): + im.save(outfile) + + def test_8bit_s(self) -> None: + with Image.open("Tests/images/8bit.s.tif") as im: + im.load() + assert im.mode == "L" + assert im.getpixel((50, 50)) == 184 + + def test_little_endian(self) -> None: + with Image.open("Tests/images/16bit.cropped.tif") as im: + assert im.getpixel((0, 0)) == 480 + assert im.mode == "I;16" + + b = im.tobytes() + # Bytes are in image native order (little endian) + assert b[0] == ord(b"\xe0") + assert b[1] == ord(b"\x01") + + def test_big_endian(self) -> None: + with Image.open("Tests/images/16bit.MM.cropped.tif") as im: + assert im.getpixel((0, 0)) == 480 + assert im.mode == "I;16B" + + b = im.tobytes() + # Bytes are in image native order (big endian) + assert b[0] == ord(b"\x01") + assert b[1] == ord(b"\xe0") + + def test_16bit_r(self) -> None: + with Image.open("Tests/images/16bit.r.tif") as im: + assert im.getpixel((0, 0)) == 480 + assert im.mode == "I;16" + + b = im.tobytes() + assert b[0] == ord(b"\xe0") + assert b[1] == ord(b"\x01") + + def test_16bit_s(self) -> None: + with Image.open("Tests/images/16bit.s.tif") as im: + im.load() + assert im.mode == "I" + assert im.getpixel((0, 0)) == 32767 + assert im.getpixel((0, 1)) == 0 + + def test_12bit_rawmode(self) -> None: + """Are we generating the same interpretation + of the image as Imagemagick is?""" + + with Image.open("Tests/images/12bit.cropped.tif") as im: + # to make the target -- + # convert 12bit.cropped.tif -depth 16 tmp.tif + # convert tmp.tif -evaluate RightShift 4 12in16bit2.tif + # imagemagick will auto scale so that a 12bit FFF is 16bit FFF0, + # so we need to unshift so that the integer values are the same. + + assert_image_equal_tofile(im, "Tests/images/12in16bit.tif") + + def test_32bit_float(self) -> None: + # Issue 614, specific 32-bit float format + path = "Tests/images/10ct_32bit_128.tiff" + with Image.open(path) as im: + im.load() + + assert im.getpixel((0, 0)) == -0.4526388943195343 + assert im.getextrema() == (-3.140936851501465, 3.140684127807617) + + def test_unknown_pixel_mode(self) -> None: + with pytest.raises(OSError): + with Image.open("Tests/images/hopper_unknown_pixel_mode.tif"): + pass + + @pytest.mark.parametrize( + "path, n_frames", + ( + ("Tests/images/multipage-lastframe.tif", 1), + ("Tests/images/multipage.tiff", 3), + ), + ) + def test_n_frames(self, path: str, n_frames: int) -> None: + with Image.open(path) as im: + 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: + n_frames = im.n_frames + + # Test seeking past the last frame + with pytest.raises(EOFError): + im.seek(n_frames) + assert im.tell() < n_frames + + # Test that seeking to the last frame does not raise an error + im.seek(n_frames - 1) + + def test_multipage(self) -> None: + # issue #862 + with Image.open("Tests/images/multipage.tiff") as im: + # file is a multipage tiff: 10x10 green, 10x10 red, 20x20 blue + + im.seek(0) + assert im.size == (10, 10) + assert im.convert("RGB").getpixel((0, 0)) == (0, 128, 0) + + im.seek(1) + im.load() + assert im.size == (10, 10) + assert im.convert("RGB").getpixel((0, 0)) == (255, 0, 0) + + im.seek(0) + im.load() + assert im.size == (10, 10) + assert im.convert("RGB").getpixel((0, 0)) == (0, 128, 0) + + im.seek(2) + im.load() + assert im.size == (20, 20) + assert im.convert("RGB").getpixel((0, 0)) == (0, 0, 255) + + def test_multipage_last_frame(self) -> None: + with Image.open("Tests/images/multipage-lastframe.tif") as im: + im.load() + assert im.size == (20, 20) + assert im.convert("RGB").getpixel((0, 0)) == (0, 0, 255) + + def test_frame_order(self) -> None: + # A frame can't progress to itself after reading + with Image.open("Tests/images/multipage_single_frame_loop.tiff") as im: + assert im.n_frames == 1 + + # A frame can't progress to a frame that has already been read + with Image.open("Tests/images/multipage_multiple_frame_loop.tiff") as im: + assert im.n_frames == 2 + + # Frames don't have to be in sequence + with Image.open("Tests/images/multipage_out_of_order.tiff") as im: + assert im.n_frames == 3 + + def test___str__(self) -> None: + filename = "Tests/images/pil136.tiff" + with Image.open(filename) as im: + # Act + ret = str(im.ifd) + + # Assert + assert isinstance(ret, str) + + def test_dict(self) -> None: + # Arrange + filename = "Tests/images/pil136.tiff" + with Image.open(filename) as im: + # v2 interface + v2_tags = { + 256: 55, + 257: 43, + 258: (8, 8, 8, 8), + 259: 1, + 262: 2, + 296: 2, + 273: (8,), + 338: (1,), + 277: 4, + 279: (9460,), + 282: 72.0, + 283: 72.0, + 284: 1, + } + assert dict(im.tag_v2) == v2_tags + + # legacy interface + legacy_tags = { + 256: (55,), + 257: (43,), + 258: (8, 8, 8, 8), + 259: (1,), + 262: (2,), + 296: (2,), + 273: (8,), + 338: (1,), + 277: (4,), + 279: (9460,), + 282: ((720000, 10000),), + 283: ((720000, 10000),), + 284: (1,), + } + assert dict(im.tag) == legacy_tags + + def test__delitem__(self) -> None: + filename = "Tests/images/pil136.tiff" + with Image.open(filename) as im: + len_before = len(dict(im.ifd)) + del im.ifd[256] + len_after = len(dict(im.ifd)) + assert len_before == len_after + 1 + + @pytest.mark.parametrize("legacy_api", (False, True)) + def test_load_byte(self, legacy_api: bool) -> None: + ifd = TiffImagePlugin.ImageFileDirectory_v2() + data = b"abc" + ret = ifd.load_byte(data, legacy_api) + assert ret == b"abc" + + def test_load_string(self) -> None: + ifd = TiffImagePlugin.ImageFileDirectory_v2() + data = b"abc\0" + ret = ifd.load_string(data, False) + assert ret == "abc" + + def test_load_float(self) -> None: + ifd = TiffImagePlugin.ImageFileDirectory_v2() + data = b"abcdabcd" + ret = ifd.load_float(data, False) + assert ret == (1.6777999408082104e22, 1.6777999408082104e22) + + def test_load_double(self) -> None: + ifd = TiffImagePlugin.ImageFileDirectory_v2() + data = b"abcdefghabcdefgh" + ret = ifd.load_double(data, False) + assert ret == (8.540883223036124e194, 8.540883223036124e194) + + def test_ifd_tag_type(self) -> None: + with Image.open("Tests/images/ifd_tag_type.tiff") as im: + assert 0x8825 in im.tag_v2 + + def test_exif(self, tmp_path: Path) -> None: + def check_exif(exif: Image.Exif) -> None: + assert sorted(exif.keys()) == [ + 256, + 257, + 258, + 259, + 262, + 271, + 272, + 273, + 277, + 278, + 279, + 282, + 283, + 284, + 296, + 297, + 305, + 339, + 700, + 34665, + 34853, + 50735, + ] + assert exif[256] == 640 + assert exif[271] == "FLIR" + + gps = exif.get_ifd(0x8825) + assert list(gps.keys()) == [0, 1, 2, 3, 4, 5, 6, 18] + assert gps[0] == b"\x03\x02\x00\x00" + assert gps[18] == "WGS-84" + + outfile = str(tmp_path / "temp.tif") + with Image.open("Tests/images/ifd_tag_type.tiff") as im: + exif = im.getexif() + check_exif(exif) + + im.save(outfile, exif=exif) + + outfile2 = str(tmp_path / "temp2.tif") + with Image.open(outfile) as im: + exif = im.getexif() + check_exif(exif) + + im.save(outfile2, exif=exif.tobytes()) + + with Image.open(outfile2) as im: + exif = im.getexif() + check_exif(exif) + + def test_modify_exif(self, tmp_path: Path) -> None: + outfile = str(tmp_path / "temp.tif") + with Image.open("Tests/images/ifd_tag_type.tiff") as im: + exif = im.getexif() + exif[264] = 100 + + im.save(outfile, exif=exif) + + with Image.open(outfile) as im: + exif = im.getexif() + assert exif[264] == 100 + + def test_reload_exif_after_seek(self) -> None: + with Image.open("Tests/images/multipage.tiff") as im: + exif = im.getexif() + del exif[256] + im.seek(1) + + assert 256 in exif + + def test_exif_frames(self) -> None: + # Test that EXIF data can change across frames + with Image.open("Tests/images/g4-multi.tiff") as im: + assert im.getexif()[273] == (328, 815) + + im.seek(1) + assert im.getexif()[273] == (1408, 1907) + + @pytest.mark.parametrize("mode", ("1", "L")) + def test_photometric(self, mode: str, tmp_path: Path) -> None: + filename = str(tmp_path / "temp.tif") + im = hopper(mode) + im.save(filename, tiffinfo={262: 0}) + with Image.open(filename) as reloaded: + assert reloaded.tag_v2[262] == 0 + assert_image_equal(im, reloaded) + + def test_seek(self) -> None: + filename = "Tests/images/pil136.tiff" + with Image.open(filename) as im: + im.seek(0) + assert im.tell() == 0 + + def test_seek_eof(self) -> None: + filename = "Tests/images/pil136.tiff" + with Image.open(filename) as im: + assert im.tell() == 0 + with pytest.raises(EOFError): + im.seek(-1) + with pytest.raises(EOFError): + im.seek(1) + + def test__limit_rational_int(self) -> None: + from PIL.TiffImagePlugin import _limit_rational + + value = 34 + ret = _limit_rational(value, 65536) + assert ret == (34, 1) + + def test__limit_rational_float(self) -> None: + from PIL.TiffImagePlugin import _limit_rational + + value = 22.3 + ret = _limit_rational(value, 65536) + assert ret == (223, 10) + + def test_4bit(self) -> None: + test_file = "Tests/images/hopper_gray_4bpp.tif" + original = hopper("L") + with Image.open(test_file) as im: + assert im.size == (128, 128) + assert im.mode == "L" + assert_image_similar(im, original, 7.3) + + def test_gray_semibyte_per_pixel(self) -> None: + test_files = ( + ( + 24.8, # epsilon + ( # group + "Tests/images/tiff_gray_2_4_bpp/hopper2.tif", + "Tests/images/tiff_gray_2_4_bpp/hopper2I.tif", + "Tests/images/tiff_gray_2_4_bpp/hopper2R.tif", + "Tests/images/tiff_gray_2_4_bpp/hopper2IR.tif", + ), + ), + ( + 7.3, # epsilon + ( # group + "Tests/images/tiff_gray_2_4_bpp/hopper4.tif", + "Tests/images/tiff_gray_2_4_bpp/hopper4I.tif", + "Tests/images/tiff_gray_2_4_bpp/hopper4R.tif", + "Tests/images/tiff_gray_2_4_bpp/hopper4IR.tif", + ), + ), + ) + original = hopper("L") + for epsilon, group in test_files: + with Image.open(group[0]) as im: + assert im.size == (128, 128) + assert im.mode == "L" + assert_image_similar(im, original, epsilon) + for file in group[1:]: + with Image.open(file) as im2: + assert im2.size == (128, 128) + assert im2.mode == "L" + assert_image_equal(im, im2) + + def test_with_underscores(self, tmp_path: Path) -> None: + kwargs = {"resolution_unit": "inch", "x_resolution": 72, "y_resolution": 36} + filename = str(tmp_path / "temp.tif") + hopper("RGB").save(filename, **kwargs) + with Image.open(filename) as im: + # legacy interface + assert im.tag[X_RESOLUTION][0][0] == 72 + assert im.tag[Y_RESOLUTION][0][0] == 36 + + # v2 interface + assert im.tag_v2[X_RESOLUTION] == 72 + assert im.tag_v2[Y_RESOLUTION] == 36 + + def test_roundtrip_tiff_uint16(self, tmp_path: Path) -> None: + # Test an image of all '0' values + pixel_value = 0x1234 + infile = "Tests/images/uint16_1_4660.tif" + with Image.open(infile) as im: + assert im.getpixel((0, 0)) == pixel_value + + tmpfile = str(tmp_path / "temp.tif") + im.save(tmpfile) + + assert_image_equal_tofile(im, tmpfile) + + def test_rowsperstrip(self, tmp_path: Path) -> None: + outfile = str(tmp_path / "temp.tif") + im = hopper() + im.save(outfile, tiffinfo={278: 256}) + + with Image.open(outfile) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) + 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: + assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") + + def test_strip_planar_raw(self) -> None: + # gdal_translate -of GTiff -co INTERLEAVE=BAND \ + # tiff_strip_raw.tif tiff_strip_planar_raw.tiff + infile = "Tests/images/tiff_strip_planar_raw.tif" + with Image.open(infile) as im: + assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") + + def test_strip_planar_raw_with_overviews(self) -> None: + # gdaladdo tiff_strip_planar_raw2.tif 2 4 8 16 + infile = "Tests/images/tiff_strip_planar_raw_with_overviews.tif" + with Image.open(infile) as im: + assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") + + def test_tiled_planar_raw(self) -> None: + # gdal_translate -of GTiff -co TILED=YES -co BLOCKXSIZE=32 \ + # -co BLOCKYSIZE=32 -co INTERLEAVE=BAND \ + # tiff_tiled_raw.tif tiff_tiled_planar_raw.tiff + infile = "Tests/images/tiff_tiled_planar_raw.tif" + with Image.open(infile) as im: + assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") + + def test_planar_configuration_save(self, tmp_path: Path) -> None: + infile = "Tests/images/tiff_tiled_planar_raw.tif" + with Image.open(infile) as im: + assert im._planar_configuration == 2 + + outfile = str(tmp_path / "temp.tif") + im.save(outfile) + + with Image.open(outfile) as reloaded: + assert_image_equal_tofile(reloaded, infile) + + @pytest.mark.parametrize("mode", ("P", "PA")) + def test_palette(self, mode: str, tmp_path: Path) -> None: + outfile = str(tmp_path / "temp.tif") + + im = hopper(mode) + im.save(outfile) + + with Image.open(outfile) as reloaded: + assert_image_equal(im.convert("RGB"), reloaded.convert("RGB")) + + def test_tiff_save_all(self) -> None: + mp = BytesIO() + with Image.open("Tests/images/multipage.tiff") as im: + im.save(mp, format="tiff", save_all=True) + + mp.seek(0, os.SEEK_SET) + with Image.open(mp) as im: + assert im.n_frames == 3 + + # Test appending images + mp = BytesIO() + im = Image.new("RGB", (100, 100), "#f00") + ims = [Image.new("RGB", (100, 100), color) for color in ["#0f0", "#00f"]] + im.copy().save(mp, format="TIFF", save_all=True, append_images=ims) + + mp.seek(0, os.SEEK_SET) + with Image.open(mp) as reread: + assert reread.n_frames == 3 + + # Test appending using a generator + def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]: + yield from ims + + mp = BytesIO() + im.save(mp, format="TIFF", save_all=True, append_images=im_generator(ims)) + + mp.seek(0, os.SEEK_SET) + with Image.open(mp) as reread: + assert reread.n_frames == 3 + + 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 + # as libtiff does not support embedded ICC profiles, + # ImageFile._save(..) however does. + im = Image.new("RGB", (1, 1)) + im.info["icc_profile"] = "Dummy value" + + # Try save-load round trip to make sure both handle icc_profile. + tmpfile = str(tmp_path / "temp.tif") + im.save(tmpfile, "TIFF", compression="raw") + with Image.open(tmpfile) as reloaded: + assert b"Dummy value" == reloaded.info["icc_profile"] + + def test_save_icc_profile(self, tmp_path: Path) -> None: + im = hopper() + assert "icc_profile" not in im.info + + outfile = str(tmp_path / "temp.tif") + icc_profile = b"Dummy value" + im.save(outfile, icc_profile=icc_profile) + + with Image.open(outfile) as reloaded: + assert reloaded.info["icc_profile"] == icc_profile + + def test_save_bmp_compression(self, tmp_path: Path) -> None: + with Image.open("Tests/images/hopper.bmp") as im: + assert im.info["compression"] == 0 + + outfile = str(tmp_path / "temp.tif") + im.save(outfile) + + def test_discard_icc_profile(self, tmp_path: Path) -> None: + outfile = str(tmp_path / "temp.tif") + + with Image.open("Tests/images/icc_profile.png") as im: + assert "icc_profile" in im.info + + im.save(outfile, icc_profile=None) + + with Image.open(outfile) as reloaded: + assert "icc_profile" not in reloaded.info + + def test_getxmp(self) -> None: + with Image.open("Tests/images/lab.tif") as im: + if ElementTree is None: + with pytest.warns( + UserWarning, + match="XMP data cannot be read without defusedxml dependency", + ): + assert im.getxmp() == {} + else: + xmp = im.getxmp() + + description = xmp["xmpmeta"]["RDF"]["Description"] + assert description[0]["format"] == "image/tiff" + assert description[3]["BitsPerSample"]["Seq"]["li"] == ["8", "8", "8"] + + def test_get_photoshop_blocks(self) -> None: + with Image.open("Tests/images/lab.tif") as im: + assert list(im.get_photoshop_blocks().keys()) == [ + 1061, + 1002, + 1005, + 1062, + 1037, + 1049, + 1011, + 1034, + 10000, + 1013, + 1016, + 1032, + 1054, + 1050, + 1064, + 1041, + 1044, + 1036, + 1057, + 4000, + 4001, + ] + + def test_tiff_chunks(self, tmp_path: Path) -> None: + tmpfile = str(tmp_path / "temp.tif") + + im = hopper() + with open(tmpfile, "wb") as fp: + for y in range(0, 128, 32): + chunk = im.crop((0, y, 128, y + 32)) + if y == 0: + chunk.save( + fp, + "TIFF", + tiffinfo={ + TiffImagePlugin.IMAGEWIDTH: 128, + TiffImagePlugin.IMAGELENGTH: 128, + }, + ) + else: + fp.write(chunk.tobytes()) + + assert_image_equal_tofile(im, tmpfile) + + 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") + + with Image.open("Tests/images/uint16_1_4660.tif") as im: + im.save(tmpfile) + + im = Image.open(tmpfile) + fp = im.fp + assert not fp.closed + im.load() + assert fp.closed + + def test_close_on_load_nonexclusive(self, tmp_path: Path) -> None: + tmpfile = str(tmp_path / "temp.tif") + + with Image.open("Tests/images/uint16_1_4660.tif") as im: + im.save(tmpfile) + + with open(tmpfile, "rb") as f: + im = Image.open(f) + fp = im.fp + assert not fp.closed + im.load() + assert not fp.closed + + # Ignore this UserWarning which triggers for four tags: + # "Possibly corrupt EXIF data. Expecting to read 50404352 bytes but..." + @pytest.mark.filterwarnings("ignore:Possibly corrupt EXIF data") + # Ignore this UserWarning: + @pytest.mark.filterwarnings("ignore:Truncated File Read") + @pytest.mark.skipif( + not os.path.exists("Tests/images/string_dimension.tiff"), + reason="Extra image files not installed", + ) + def test_string_dimension(self) -> None: + # Assert that an error is raised if one of the dimensions is a string + with Image.open("Tests/images/string_dimension.tiff") as im: + with pytest.raises(OSError): + im.load() + + @pytest.mark.timeout(6) + @pytest.mark.filterwarnings("ignore:Truncated File Read") + def test_timeout(self) -> None: + with Image.open("Tests/images/timeout-6646305047838720") as im: + ImageFile.LOAD_TRUNCATED_IMAGES = True + im.load() + ImageFile.LOAD_TRUNCATED_IMAGES = False + + @pytest.mark.parametrize( + "test_file", + [ + "Tests/images/oom-225817ca0f8c663be7ab4b9e717b02c661e66834.tif", + ], + ) + @pytest.mark.timeout(2) + def test_oom(self, test_file: str) -> None: + with pytest.raises(UnidentifiedImageError): + with pytest.warns(UserWarning): + with Image.open(test_file): + pass + + +@pytest.mark.skipif(not is_win32(), reason="Windows only") +class TestFileTiffW32: + def test_fd_leak(self, tmp_path: Path) -> None: + tmpfile = str(tmp_path / "temp.tif") + + # this is an mmaped file. + with Image.open("Tests/images/uint16_1_4660.tif") as im: + im.save(tmpfile) + + im = Image.open(tmpfile) + fp = im.fp + assert not fp.closed + with pytest.raises(OSError): + os.remove(tmpfile) + im.load() + assert fp.closed + + # this closes the mmap + im.close() + + # this should not fail, as load should have closed the file pointer, + # and close should have closed the mmap + os.remove(tmpfile) diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 8b816aa4f..5c1dc7cec 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -1,465 +1,465 @@ -from __future__ import annotations - -import io -import struct -from pathlib import Path - -import pytest - -from PIL import Image, TiffImagePlugin, TiffTags -from PIL.TiffImagePlugin import IFDRational - -from .helper import assert_deep_equal, hopper - -TAG_IDS = {info.name: info.value for info in TiffTags.TAGS_V2.values()} - - -def test_rt_metadata(tmp_path: Path) -> None: - """Test writing arbitrary metadata into the tiff image directory - Use case is ImageJ private tags, one numeric, one arbitrary - data. https://github.com/python-pillow/Pillow/issues/291 - """ - - img = hopper() - - # Behaviour change: re #1416 - # Pre ifd rewrite, ImageJMetaData was being written as a string(2), - # Post ifd rewrite, it's defined as arbitrary bytes(7). It should - # roundtrip with the actual bytes, rather than stripped text - # of the premerge tests. - # - # For text items, we still have to decode('ascii','replace') because - # the tiff file format can't take 8 bit bytes in that field. - - base_text_data = "This is some arbitrary metadata for a text field" - bin_data = base_text_data.encode("ascii") + b" \xff" - text_data = base_text_data + " " + chr(255) - reloaded_text_data = base_text_data + " ?" - float_data = 12.345 - double_data = 67.89 - info = TiffImagePlugin.ImageFileDirectory() - - ImageJMetaData = TAG_IDS["ImageJMetaData"] - ImageJMetaDataByteCounts = TAG_IDS["ImageJMetaDataByteCounts"] - ImageDescription = TAG_IDS["ImageDescription"] - - info[ImageJMetaDataByteCounts] = len(bin_data) - info[ImageJMetaData] = bin_data - info[TAG_IDS["RollAngle"]] = float_data - info.tagtype[TAG_IDS["RollAngle"]] = 11 - info[TAG_IDS["YawAngle"]] = double_data - info.tagtype[TAG_IDS["YawAngle"]] = 12 - - info[ImageDescription] = text_data - - f = str(tmp_path / "temp.tif") - - img.save(f, tiffinfo=info) - - with Image.open(f) as loaded: - assert loaded.tag[ImageJMetaDataByteCounts] == (len(bin_data),) - assert loaded.tag_v2[ImageJMetaDataByteCounts] == (len(bin_data),) - - assert loaded.tag[ImageJMetaData] == bin_data - assert loaded.tag_v2[ImageJMetaData] == bin_data - - assert loaded.tag[ImageDescription] == (reloaded_text_data,) - assert loaded.tag_v2[ImageDescription] == reloaded_text_data - - loaded_float = loaded.tag[TAG_IDS["RollAngle"]][0] - assert round(abs(loaded_float - float_data), 5) == 0 - loaded_double = loaded.tag[TAG_IDS["YawAngle"]][0] - assert round(abs(loaded_double - double_data), 7) == 0 - - # check with 2 element ImageJMetaDataByteCounts, issue #2006 - - info[ImageJMetaDataByteCounts] = (8, len(bin_data) - 8) - img.save(f, tiffinfo=info) - with Image.open(f) as loaded: - assert loaded.tag[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8) - assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8) - - -def test_read_metadata() -> None: - with Image.open("Tests/images/hopper_g4.tif") as img: - assert { - "YResolution": IFDRational(4294967295, 113653537), - "PlanarConfiguration": 1, - "BitsPerSample": (1,), - "ImageLength": 128, - "Compression": 4, - "FillOrder": 1, - "RowsPerStrip": 128, - "ResolutionUnit": 3, - "PhotometricInterpretation": 0, - "PageNumber": (0, 1), - "XResolution": IFDRational(4294967295, 113653537), - "ImageWidth": 128, - "Orientation": 1, - "StripByteCounts": (1968,), - "SamplesPerPixel": 1, - "StripOffsets": (8,), - } == img.tag_v2.named() - - assert { - "YResolution": ((4294967295, 113653537),), - "PlanarConfiguration": (1,), - "BitsPerSample": (1,), - "ImageLength": (128,), - "Compression": (4,), - "FillOrder": (1,), - "RowsPerStrip": (128,), - "ResolutionUnit": (3,), - "PhotometricInterpretation": (0,), - "PageNumber": (0, 1), - "XResolution": ((4294967295, 113653537),), - "ImageWidth": (128,), - "Orientation": (1,), - "StripByteCounts": (1968,), - "SamplesPerPixel": (1,), - "StripOffsets": (8,), - } == img.tag.named() - - -def test_write_metadata(tmp_path: Path) -> None: - """Test metadata writing through the python code""" - with Image.open("Tests/images/hopper.tif") as img: - f = str(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: - reloaded = loaded.tag_v2.named() - - ignored = ["StripByteCounts", "RowsPerStrip", "PageNumber", "StripOffsets"] - - for tag, value in reloaded.items(): - if tag in ignored: - continue - if isinstance(original[tag], tuple) and isinstance( - original[tag][0], IFDRational - ): - # Need to compare element by element in the tuple, - # not comparing tuples of object references - assert_deep_equal( - original[tag], - value, - f"{tag} didn't roundtrip, {original[tag]}, {value}", - ) - else: - assert ( - original[tag] == value - ), f"{tag} didn't roundtrip, {original[tag]}, {value}" - - for tag, value in original.items(): - if tag not in ignored: - assert value == reloaded[tag], f"{tag} didn't roundtrip" - - -def test_change_stripbytecounts_tag_type(tmp_path: Path) -> None: - out = str(tmp_path / "temp.tiff") - with Image.open("Tests/images/hopper.tif") as im: - info = im.tag_v2 - del info[278] - - # Resize the image so that STRIPBYTECOUNTS will be larger than a SHORT - im = im.resize((500, 500)) - info[TiffImagePlugin.IMAGEWIDTH] = im.width - - # STRIPBYTECOUNTS can be a SHORT or a LONG - info.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] = TiffTags.SHORT - - im.save(out, tiffinfo=info) - - with Image.open(out) as reloaded: - assert reloaded.tag_v2.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] == TiffTags.LONG - - -def test_no_duplicate_50741_tag() -> None: - assert TAG_IDS["MakerNoteSafety"] == 50741 - assert TAG_IDS["BestQualityScale"] == 50780 - - -def test_iptc(tmp_path: Path) -> None: - out = str(tmp_path / "temp.tiff") - with Image.open("Tests/images/hopper.Lab.tif") as im: - im.save(out) - - -@pytest.mark.parametrize("value, expected", ((b"test", "test"), (1, "1"))) -def test_writing_other_types_to_ascii( - value: bytes | int, expected: str, tmp_path: Path -) -> None: - info = TiffImagePlugin.ImageFileDirectory_v2() - - tag = TiffTags.TAGS_V2[271] - assert tag.type == TiffTags.ASCII - - info[271] = value - - im = hopper() - out = str(tmp_path / "temp.tiff") - im.save(out, tiffinfo=info) - - with Image.open(out) as reloaded: - assert reloaded.tag_v2[271] == expected - - -@pytest.mark.parametrize("value", (1, IFDRational(1))) -def test_writing_other_types_to_bytes(value: int | IFDRational, tmp_path: Path) -> None: - im = hopper() - info = TiffImagePlugin.ImageFileDirectory_v2() - - tag = TiffTags.TAGS_V2[700] - assert tag.type == TiffTags.BYTE - - info[700] = value - - out = str(tmp_path / "temp.tiff") - im.save(out, tiffinfo=info) - - with Image.open(out) as reloaded: - assert reloaded.tag_v2[700] == b"\x01" - - -@pytest.mark.parametrize("value", (1, IFDRational(1))) -def test_writing_other_types_to_undefined( - value: int | IFDRational, tmp_path: Path -) -> None: - im = hopper() - info = TiffImagePlugin.ImageFileDirectory_v2() - - tag = TiffTags.TAGS_V2[33723] - assert tag.type == TiffTags.UNDEFINED - - info[33723] = value - - out = str(tmp_path / "temp.tiff") - im.save(out, tiffinfo=info) - - with Image.open(out) as reloaded: - assert reloaded.tag_v2[33723] == b"1" - - -def test_undefined_zero(tmp_path: Path) -> None: - # Check that the tag has not been changed since this test was created - tag = TiffTags.TAGS_V2[45059] - assert tag.type == TiffTags.UNDEFINED - assert tag.length == 0 - - info = TiffImagePlugin.ImageFileDirectory(b"II*\x00\x08\x00\x00\x00") - info[45059] = b"test" - - # Assert that the tag value does not change by setting it to itself - original = info[45059] - info[45059] = info[45059] - assert info[45059] == original - - -def test_empty_metadata() -> None: - f = io.BytesIO(b"II*\x00\x08\x00\x00\x00") - head = f.read(8) - info = TiffImagePlugin.ImageFileDirectory(head) - # Should not raise struct.error. - with pytest.warns(UserWarning): - info.load(f) - - -def test_iccprofile(tmp_path: Path) -> None: - # https://github.com/python-pillow/Pillow/issues/1462 - out = str(tmp_path / "temp.tiff") - with Image.open("Tests/images/hopper.iccprofile.tif") as im: - im.save(out) - - with Image.open(out) as reloaded: - assert not isinstance(im.info["icc_profile"], tuple) - assert im.info["icc_profile"] == reloaded.info["icc_profile"] - - -def test_iccprofile_binary() -> None: - # https://github.com/python-pillow/Pillow/issues/1526 - # We should be able to load this, - # but probably won't be able to save it. - - with Image.open("Tests/images/hopper.iccprofile_binary.tif") as im: - assert im.tag_v2.tagtype[34675] == 1 - assert im.info["icc_profile"] - - -def test_iccprofile_save_png(tmp_path: Path) -> None: - with Image.open("Tests/images/hopper.iccprofile.tif") as im: - outfile = str(tmp_path / "temp.png") - im.save(outfile) - - -def test_iccprofile_binary_save_png(tmp_path: Path) -> None: - with Image.open("Tests/images/hopper.iccprofile_binary.tif") as im: - outfile = str(tmp_path / "temp.png") - im.save(outfile) - - -def test_exif_div_zero(tmp_path: Path) -> None: - im = hopper() - info = TiffImagePlugin.ImageFileDirectory_v2() - info[41988] = TiffImagePlugin.IFDRational(0, 0) - - out = str(tmp_path / "temp.tiff") - im.save(out, tiffinfo=info, compression="raw") - - with Image.open(out) as reloaded: - assert 0 == reloaded.tag_v2[41988].numerator - assert 0 == reloaded.tag_v2[41988].denominator - - -def test_ifd_unsigned_rational(tmp_path: Path) -> None: - im = hopper() - info = TiffImagePlugin.ImageFileDirectory_v2() - - max_long = 2**32 - 1 - - # 4 bytes unsigned long - numerator = max_long - - info[41493] = TiffImagePlugin.IFDRational(numerator, 1) - - out = str(tmp_path / "temp.tiff") - im.save(out, tiffinfo=info, compression="raw") - - with Image.open(out) as reloaded: - assert max_long == reloaded.tag_v2[41493].numerator - assert 1 == reloaded.tag_v2[41493].denominator - - # out of bounds of 4 byte unsigned long - numerator = max_long + 1 - - info[41493] = TiffImagePlugin.IFDRational(numerator, 1) - - out = str(tmp_path / "temp.tiff") - im.save(out, tiffinfo=info, compression="raw") - - with Image.open(out) as reloaded: - assert max_long == reloaded.tag_v2[41493].numerator - assert 1 == reloaded.tag_v2[41493].denominator - - -def test_ifd_signed_rational(tmp_path: Path) -> None: - im = hopper() - info = TiffImagePlugin.ImageFileDirectory_v2() - - # pair of 4 byte signed longs - numerator = 2**31 - 1 - denominator = -(2**31) - - info[37380] = TiffImagePlugin.IFDRational(numerator, denominator) - - out = str(tmp_path / "temp.tiff") - im.save(out, tiffinfo=info, compression="raw") - - with Image.open(out) as reloaded: - assert numerator == reloaded.tag_v2[37380].numerator - assert denominator == reloaded.tag_v2[37380].denominator - - numerator = -(2**31) - denominator = 2**31 - 1 - - info[37380] = TiffImagePlugin.IFDRational(numerator, denominator) - - out = str(tmp_path / "temp.tiff") - im.save(out, tiffinfo=info, compression="raw") - - with Image.open(out) as reloaded: - assert numerator == reloaded.tag_v2[37380].numerator - assert denominator == reloaded.tag_v2[37380].denominator - - # out of bounds of 4 byte signed long - numerator = -(2**31) - 1 - denominator = 1 - - info[37380] = TiffImagePlugin.IFDRational(numerator, denominator) - - out = str(tmp_path / "temp.tiff") - im.save(out, tiffinfo=info, compression="raw") - - with Image.open(out) as reloaded: - assert 2**31 - 1 == reloaded.tag_v2[37380].numerator - assert -1 == reloaded.tag_v2[37380].denominator - - -def test_ifd_signed_long(tmp_path: Path) -> None: - im = hopper() - info = TiffImagePlugin.ImageFileDirectory_v2() - - info[37000] = -60000 - - out = str(tmp_path / "temp.tiff") - im.save(out, tiffinfo=info, compression="raw") - - with Image.open(out) as reloaded: - assert reloaded.tag_v2[37000] == -60000 - - -def test_empty_values() -> None: - data = io.BytesIO( - b"II*\x00\x08\x00\x00\x00\x03\x00\x1a\x01\x05\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x1b\x01\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x98\x82\x02\x00\x07\x00\x00\x002\x00\x00\x00\x00\x00\x00\x00a " - b"text\x00\x00" - ) - head = data.read(8) - info = TiffImagePlugin.ImageFileDirectory_v2(head) - info.load(data) - # Should not raise ValueError. - info = dict(info) - assert 33432 in info - - -def test_photoshop_info(tmp_path: Path) -> None: - with Image.open("Tests/images/issue_2278.tif") as im: - assert len(im.tag_v2[34377]) == 70 - assert isinstance(im.tag_v2[34377], bytes) - out = str(tmp_path / "temp.tiff") - im.save(out) - with Image.open(out) as reloaded: - assert len(reloaded.tag_v2[34377]) == 70 - assert isinstance(reloaded.tag_v2[34377], bytes) - - -def test_too_many_entries() -> None: - ifd = TiffImagePlugin.ImageFileDirectory_v2() - - # 277: ("SamplesPerPixel", SHORT, 1), - ifd._tagdata[277] = struct.pack(" None: - base_ifd = TiffImagePlugin.ImageFileDirectory_v2() - interop_ifd = TiffImagePlugin.ImageFileDirectory_v2(group=40965) - for ifd in (base_ifd, interop_ifd): - ifd[2] = "test" - ifd[256] = 10 - - assert base_ifd.tagtype[256] == 4 - assert interop_ifd.tagtype[256] != base_ifd.tagtype[256] - - assert interop_ifd.tagtype[2] == 7 - assert base_ifd.tagtype[2] != interop_ifd.tagtype[256] - - -def test_empty_subifd(tmp_path: Path) -> None: - out = str(tmp_path / "temp.jpg") - - im = hopper() - exif = im.getexif() - exif[TiffImagePlugin.EXIFIFD] = {} - im.save(out, exif=exif) - - with Image.open(out) as reloaded: - exif = reloaded.getexif() - assert exif.get_ifd(TiffImagePlugin.EXIFIFD) == {} +from __future__ import annotations + +import io +import struct +from pathlib import Path + +import pytest + +from PIL import Image, TiffImagePlugin, TiffTags +from PIL.TiffImagePlugin import IFDRational + +from .helper import assert_deep_equal, hopper + +TAG_IDS = {info.name: info.value for info in TiffTags.TAGS_V2.values()} + + +def test_rt_metadata(tmp_path: Path) -> None: + """Test writing arbitrary metadata into the tiff image directory + Use case is ImageJ private tags, one numeric, one arbitrary + data. https://github.com/python-pillow/Pillow/issues/291 + """ + + img = hopper() + + # Behaviour change: re #1416 + # Pre ifd rewrite, ImageJMetaData was being written as a string(2), + # Post ifd rewrite, it's defined as arbitrary bytes(7). It should + # roundtrip with the actual bytes, rather than stripped text + # of the premerge tests. + # + # For text items, we still have to decode('ascii','replace') because + # the tiff file format can't take 8 bit bytes in that field. + + base_text_data = "This is some arbitrary metadata for a text field" + bin_data = base_text_data.encode("ascii") + b" \xff" + text_data = base_text_data + " " + chr(255) + reloaded_text_data = base_text_data + " ?" + float_data = 12.345 + double_data = 67.89 + info = TiffImagePlugin.ImageFileDirectory() + + ImageJMetaData = TAG_IDS["ImageJMetaData"] + ImageJMetaDataByteCounts = TAG_IDS["ImageJMetaDataByteCounts"] + ImageDescription = TAG_IDS["ImageDescription"] + + info[ImageJMetaDataByteCounts] = len(bin_data) + info[ImageJMetaData] = bin_data + info[TAG_IDS["RollAngle"]] = float_data + info.tagtype[TAG_IDS["RollAngle"]] = 11 + info[TAG_IDS["YawAngle"]] = double_data + info.tagtype[TAG_IDS["YawAngle"]] = 12 + + info[ImageDescription] = text_data + + f = str(tmp_path / "temp.tif") + + img.save(f, tiffinfo=info) + + with Image.open(f) as loaded: + assert loaded.tag[ImageJMetaDataByteCounts] == (len(bin_data),) + assert loaded.tag_v2[ImageJMetaDataByteCounts] == (len(bin_data),) + + assert loaded.tag[ImageJMetaData] == bin_data + assert loaded.tag_v2[ImageJMetaData] == bin_data + + assert loaded.tag[ImageDescription] == (reloaded_text_data,) + assert loaded.tag_v2[ImageDescription] == reloaded_text_data + + loaded_float = loaded.tag[TAG_IDS["RollAngle"]][0] + assert round(abs(loaded_float - float_data), 5) == 0 + loaded_double = loaded.tag[TAG_IDS["YawAngle"]][0] + assert round(abs(loaded_double - double_data), 7) == 0 + + # check with 2 element ImageJMetaDataByteCounts, issue #2006 + + info[ImageJMetaDataByteCounts] = (8, len(bin_data) - 8) + img.save(f, tiffinfo=info) + with Image.open(f) as loaded: + assert loaded.tag[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8) + assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8) + + +def test_read_metadata() -> None: + with Image.open("Tests/images/hopper_g4.tif") as img: + assert { + "YResolution": IFDRational(4294967295, 113653537), + "PlanarConfiguration": 1, + "BitsPerSample": (1,), + "ImageLength": 128, + "Compression": 4, + "FillOrder": 1, + "RowsPerStrip": 128, + "ResolutionUnit": 3, + "PhotometricInterpretation": 0, + "PageNumber": (0, 1), + "XResolution": IFDRational(4294967295, 113653537), + "ImageWidth": 128, + "Orientation": 1, + "StripByteCounts": (1968,), + "SamplesPerPixel": 1, + "StripOffsets": (8,), + } == img.tag_v2.named() + + assert { + "YResolution": ((4294967295, 113653537),), + "PlanarConfiguration": (1,), + "BitsPerSample": (1,), + "ImageLength": (128,), + "Compression": (4,), + "FillOrder": (1,), + "RowsPerStrip": (128,), + "ResolutionUnit": (3,), + "PhotometricInterpretation": (0,), + "PageNumber": (0, 1), + "XResolution": ((4294967295, 113653537),), + "ImageWidth": (128,), + "Orientation": (1,), + "StripByteCounts": (1968,), + "SamplesPerPixel": (1,), + "StripOffsets": (8,), + } == img.tag.named() + + +def test_write_metadata(tmp_path: Path) -> None: + """Test metadata writing through the python code""" + with Image.open("Tests/images/hopper.tif") as img: + f = str(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: + reloaded = loaded.tag_v2.named() + + ignored = ["StripByteCounts", "RowsPerStrip", "PageNumber", "StripOffsets"] + + for tag, value in reloaded.items(): + if tag in ignored: + continue + if isinstance(original[tag], tuple) and isinstance( + original[tag][0], IFDRational + ): + # Need to compare element by element in the tuple, + # not comparing tuples of object references + assert_deep_equal( + original[tag], + value, + f"{tag} didn't roundtrip, {original[tag]}, {value}", + ) + else: + assert ( + original[tag] == value + ), f"{tag} didn't roundtrip, {original[tag]}, {value}" + + for tag, value in original.items(): + if tag not in ignored: + assert value == reloaded[tag], f"{tag} didn't roundtrip" + + +def test_change_stripbytecounts_tag_type(tmp_path: Path) -> None: + out = str(tmp_path / "temp.tiff") + with Image.open("Tests/images/hopper.tif") as im: + info = im.tag_v2 + del info[278] + + # Resize the image so that STRIPBYTECOUNTS will be larger than a SHORT + im = im.resize((500, 500)) + info[TiffImagePlugin.IMAGEWIDTH] = im.width + + # STRIPBYTECOUNTS can be a SHORT or a LONG + info.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] = TiffTags.SHORT + + im.save(out, tiffinfo=info) + + with Image.open(out) as reloaded: + assert reloaded.tag_v2.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] == TiffTags.LONG + + +def test_no_duplicate_50741_tag() -> None: + assert TAG_IDS["MakerNoteSafety"] == 50741 + assert TAG_IDS["BestQualityScale"] == 50780 + + +def test_iptc(tmp_path: Path) -> None: + out = str(tmp_path / "temp.tiff") + with Image.open("Tests/images/hopper.Lab.tif") as im: + im.save(out) + + +@pytest.mark.parametrize("value, expected", ((b"test", "test"), (1, "1"))) +def test_writing_other_types_to_ascii( + value: bytes | int, expected: str, tmp_path: Path +) -> None: + info = TiffImagePlugin.ImageFileDirectory_v2() + + tag = TiffTags.TAGS_V2[271] + assert tag.type == TiffTags.ASCII + + info[271] = value + + im = hopper() + out = str(tmp_path / "temp.tiff") + im.save(out, tiffinfo=info) + + with Image.open(out) as reloaded: + assert reloaded.tag_v2[271] == expected + + +@pytest.mark.parametrize("value", (1, IFDRational(1))) +def test_writing_other_types_to_bytes(value: int | IFDRational, tmp_path: Path) -> None: + im = hopper() + info = TiffImagePlugin.ImageFileDirectory_v2() + + tag = TiffTags.TAGS_V2[700] + assert tag.type == TiffTags.BYTE + + info[700] = value + + out = str(tmp_path / "temp.tiff") + im.save(out, tiffinfo=info) + + with Image.open(out) as reloaded: + assert reloaded.tag_v2[700] == b"\x01" + + +@pytest.mark.parametrize("value", (1, IFDRational(1))) +def test_writing_other_types_to_undefined( + value: int | IFDRational, tmp_path: Path +) -> None: + im = hopper() + info = TiffImagePlugin.ImageFileDirectory_v2() + + tag = TiffTags.TAGS_V2[33723] + assert tag.type == TiffTags.UNDEFINED + + info[33723] = value + + out = str(tmp_path / "temp.tiff") + im.save(out, tiffinfo=info) + + with Image.open(out) as reloaded: + assert reloaded.tag_v2[33723] == b"1" + + +def test_undefined_zero(tmp_path: Path) -> None: + # Check that the tag has not been changed since this test was created + tag = TiffTags.TAGS_V2[45059] + assert tag.type == TiffTags.UNDEFINED + assert tag.length == 0 + + info = TiffImagePlugin.ImageFileDirectory(b"II*\x00\x08\x00\x00\x00") + info[45059] = b"test" + + # Assert that the tag value does not change by setting it to itself + original = info[45059] + info[45059] = info[45059] + assert info[45059] == original + + +def test_empty_metadata() -> None: + f = io.BytesIO(b"II*\x00\x08\x00\x00\x00") + head = f.read(8) + info = TiffImagePlugin.ImageFileDirectory(head) + # Should not raise struct.error. + with pytest.warns(UserWarning): + info.load(f) + + +def test_iccprofile(tmp_path: Path) -> None: + # https://github.com/python-pillow/Pillow/issues/1462 + out = str(tmp_path / "temp.tiff") + with Image.open("Tests/images/hopper.iccprofile.tif") as im: + im.save(out) + + with Image.open(out) as reloaded: + assert not isinstance(im.info["icc_profile"], tuple) + assert im.info["icc_profile"] == reloaded.info["icc_profile"] + + +def test_iccprofile_binary() -> None: + # https://github.com/python-pillow/Pillow/issues/1526 + # We should be able to load this, + # but probably won't be able to save it. + + with Image.open("Tests/images/hopper.iccprofile_binary.tif") as im: + assert im.tag_v2.tagtype[34675] == 1 + assert im.info["icc_profile"] + + +def test_iccprofile_save_png(tmp_path: Path) -> None: + with Image.open("Tests/images/hopper.iccprofile.tif") as im: + outfile = str(tmp_path / "temp.png") + im.save(outfile) + + +def test_iccprofile_binary_save_png(tmp_path: Path) -> None: + with Image.open("Tests/images/hopper.iccprofile_binary.tif") as im: + outfile = str(tmp_path / "temp.png") + im.save(outfile) + + +def test_exif_div_zero(tmp_path: Path) -> None: + im = hopper() + info = TiffImagePlugin.ImageFileDirectory_v2() + info[41988] = TiffImagePlugin.IFDRational(0, 0) + + out = str(tmp_path / "temp.tiff") + im.save(out, tiffinfo=info, compression="raw") + + with Image.open(out) as reloaded: + assert 0 == reloaded.tag_v2[41988].numerator + assert 0 == reloaded.tag_v2[41988].denominator + + +def test_ifd_unsigned_rational(tmp_path: Path) -> None: + im = hopper() + info = TiffImagePlugin.ImageFileDirectory_v2() + + max_long = 2**32 - 1 + + # 4 bytes unsigned long + numerator = max_long + + info[41493] = TiffImagePlugin.IFDRational(numerator, 1) + + out = str(tmp_path / "temp.tiff") + im.save(out, tiffinfo=info, compression="raw") + + with Image.open(out) as reloaded: + assert max_long == reloaded.tag_v2[41493].numerator + assert 1 == reloaded.tag_v2[41493].denominator + + # out of bounds of 4 byte unsigned long + numerator = max_long + 1 + + info[41493] = TiffImagePlugin.IFDRational(numerator, 1) + + out = str(tmp_path / "temp.tiff") + im.save(out, tiffinfo=info, compression="raw") + + with Image.open(out) as reloaded: + assert max_long == reloaded.tag_v2[41493].numerator + assert 1 == reloaded.tag_v2[41493].denominator + + +def test_ifd_signed_rational(tmp_path: Path) -> None: + im = hopper() + info = TiffImagePlugin.ImageFileDirectory_v2() + + # pair of 4 byte signed longs + numerator = 2**31 - 1 + denominator = -(2**31) + + info[37380] = TiffImagePlugin.IFDRational(numerator, denominator) + + out = str(tmp_path / "temp.tiff") + im.save(out, tiffinfo=info, compression="raw") + + with Image.open(out) as reloaded: + assert numerator == reloaded.tag_v2[37380].numerator + assert denominator == reloaded.tag_v2[37380].denominator + + numerator = -(2**31) + denominator = 2**31 - 1 + + info[37380] = TiffImagePlugin.IFDRational(numerator, denominator) + + out = str(tmp_path / "temp.tiff") + im.save(out, tiffinfo=info, compression="raw") + + with Image.open(out) as reloaded: + assert numerator == reloaded.tag_v2[37380].numerator + assert denominator == reloaded.tag_v2[37380].denominator + + # out of bounds of 4 byte signed long + numerator = -(2**31) - 1 + denominator = 1 + + info[37380] = TiffImagePlugin.IFDRational(numerator, denominator) + + out = str(tmp_path / "temp.tiff") + im.save(out, tiffinfo=info, compression="raw") + + with Image.open(out) as reloaded: + assert 2**31 - 1 == reloaded.tag_v2[37380].numerator + assert -1 == reloaded.tag_v2[37380].denominator + + +def test_ifd_signed_long(tmp_path: Path) -> None: + im = hopper() + info = TiffImagePlugin.ImageFileDirectory_v2() + + info[37000] = -60000 + + out = str(tmp_path / "temp.tiff") + im.save(out, tiffinfo=info, compression="raw") + + with Image.open(out) as reloaded: + assert reloaded.tag_v2[37000] == -60000 + + +def test_empty_values() -> None: + data = io.BytesIO( + b"II*\x00\x08\x00\x00\x00\x03\x00\x1a\x01\x05\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x1b\x01\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x98\x82\x02\x00\x07\x00\x00\x002\x00\x00\x00\x00\x00\x00\x00a " + b"text\x00\x00" + ) + head = data.read(8) + info = TiffImagePlugin.ImageFileDirectory_v2(head) + info.load(data) + # Should not raise ValueError. + info = dict(info) + assert 33432 in info + + +def test_photoshop_info(tmp_path: Path) -> None: + with Image.open("Tests/images/issue_2278.tif") as im: + assert len(im.tag_v2[34377]) == 70 + assert isinstance(im.tag_v2[34377], bytes) + out = str(tmp_path / "temp.tiff") + im.save(out) + with Image.open(out) as reloaded: + assert len(reloaded.tag_v2[34377]) == 70 + assert isinstance(reloaded.tag_v2[34377], bytes) + + +def test_too_many_entries() -> None: + ifd = TiffImagePlugin.ImageFileDirectory_v2() + + # 277: ("SamplesPerPixel", SHORT, 1), + ifd._tagdata[277] = struct.pack(" None: + base_ifd = TiffImagePlugin.ImageFileDirectory_v2() + interop_ifd = TiffImagePlugin.ImageFileDirectory_v2(group=40965) + for ifd in (base_ifd, interop_ifd): + ifd[2] = "test" + ifd[256] = 10 + + assert base_ifd.tagtype[256] == 4 + assert interop_ifd.tagtype[256] != base_ifd.tagtype[256] + + assert interop_ifd.tagtype[2] == 7 + assert base_ifd.tagtype[2] != interop_ifd.tagtype[256] + + +def test_empty_subifd(tmp_path: Path) -> None: + out = str(tmp_path / "temp.jpg") + + im = hopper() + exif = im.getexif() + exif[TiffImagePlugin.EXIFIFD] = {} + im.save(out, exif=exif) + + with Image.open(out) as reloaded: + exif = reloaded.getexif() + assert exif.get_ifd(TiffImagePlugin.EXIFIFD) == {} diff --git a/Tests/test_file_wal.py b/Tests/test_file_wal.py index b34975e83..7c1585afe 100644 --- a/Tests/test_file_wal.py +++ b/Tests/test_file_wal.py @@ -1,27 +1,27 @@ -from __future__ import annotations - -from PIL import WalImageFile - -from .helper import assert_image_equal_tofile - -TEST_FILE = "Tests/images/hopper.wal" - - -def test_open() -> None: - with WalImageFile.open(TEST_FILE) as im: - assert im.format == "WAL" - assert im.format_description == "Quake2 Texture" - assert im.mode == "P" - assert im.size == (128, 128) - - assert isinstance(im, WalImageFile.WalImageFile) - - assert_image_equal_tofile(im, "Tests/images/hopper_wal.png") - - -def test_load() -> None: - with WalImageFile.open(TEST_FILE) as im: - assert im.load()[0, 0] == 122 - - # Test again now that it has already been loaded once - assert im.load()[0, 0] == 122 +from __future__ import annotations + +from PIL import WalImageFile + +from .helper import assert_image_equal_tofile + +TEST_FILE = "Tests/images/hopper.wal" + + +def test_open() -> None: + with WalImageFile.open(TEST_FILE) as im: + assert im.format == "WAL" + assert im.format_description == "Quake2 Texture" + assert im.mode == "P" + assert im.size == (128, 128) + + assert isinstance(im, WalImageFile.WalImageFile) + + assert_image_equal_tofile(im, "Tests/images/hopper_wal.png") + + +def test_load() -> None: + with WalImageFile.open(TEST_FILE) as im: + assert im.load()[0, 0] == 122 + + # Test again now that it has already been loaded once + assert im.load()[0, 0] == 122 diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index 1caf032f6..827631871 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -1,254 +1,254 @@ -from __future__ import annotations - -import io -import re -import sys -import warnings -from pathlib import Path - -import pytest - -from PIL import Image, WebPImagePlugin, features - -from .helper import ( - assert_image_equal, - assert_image_similar, - assert_image_similar_tofile, - hopper, - skip_unless_feature, -) - -try: - from PIL import _webp - - HAVE_WEBP = True -except ImportError: - HAVE_WEBP = False - - -class TestUnsupportedWebp: - def test_unsupported(self) -> None: - if HAVE_WEBP: - WebPImagePlugin.SUPPORTED = False - - file_path = "Tests/images/hopper.webp" - with pytest.warns(UserWarning): - with pytest.raises(OSError): - with Image.open(file_path): - pass - - if HAVE_WEBP: - WebPImagePlugin.SUPPORTED = True - - -@skip_unless_feature("webp") -class TestFileWebp: - def setup_method(self) -> None: - self.rgb_mode = "RGB" - - def test_version(self) -> None: - _webp.WebPDecoderVersion() - _webp.WebPDecoderBuggyAlpha() - version = features.version_module("webp") - assert version is not None - assert re.search(r"\d+\.\d+\.\d+$", version) - - def test_read_rgb(self) -> None: - """ - Can we read a RGB mode WebP file without error? - Does it have the bits we expect? - """ - - with Image.open("Tests/images/hopper.webp") as image: - assert image.mode == self.rgb_mode - assert image.size == (128, 128) - assert image.format == "WEBP" - image.load() - image.getdata() - - # generated with: - # dwebp -ppm ../../Tests/images/hopper.webp -o hopper_webp_bits.ppm - assert_image_similar_tofile(image, "Tests/images/hopper_webp_bits.ppm", 1.0) - - def _roundtrip(self, tmp_path: Path, mode, epsilon, args={}) -> None: - temp_file = str(tmp_path / "temp.webp") - - hopper(mode).save(temp_file, **args) - with Image.open(temp_file) as image: - assert image.mode == self.rgb_mode - assert image.size == (128, 128) - assert image.format == "WEBP" - image.load() - image.getdata() - - if mode == self.rgb_mode: - # generated with: dwebp -ppm temp.webp -o hopper_webp_write.ppm - assert_image_similar_tofile( - image, "Tests/images/hopper_webp_write.ppm", 12.0 - ) - - # This test asserts that the images are similar. If the average pixel - # difference between the two images is less than the epsilon value, - # then we're going to accept that it's a reasonable lossy version of - # the image. - target = hopper(mode) - if mode != self.rgb_mode: - target = target.convert(self.rgb_mode) - assert_image_similar(image, target, epsilon) - - def test_write_rgb(self, tmp_path: Path) -> None: - """ - Can we write a RGB mode file to webp without error? - Does it have the bits we expect? - """ - - self._roundtrip(tmp_path, self.rgb_mode, 12.5) - - def test_write_method(self, tmp_path: Path) -> None: - self._roundtrip(tmp_path, self.rgb_mode, 12.0, {"method": 6}) - - buffer_no_args = io.BytesIO() - hopper().save(buffer_no_args, format="WEBP") - - buffer_method = io.BytesIO() - hopper().save(buffer_method, format="WEBP", method=6) - assert buffer_no_args.getbuffer() != buffer_method.getbuffer() - - @skip_unless_feature("webp_anim") - def test_save_all(self, tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.webp") - im = Image.new("RGB", (1, 1)) - im2 = Image.new("RGB", (1, 1), "#f00") - im.save(temp_file, save_all=True, append_images=[im2]) - - with Image.open(temp_file) as reloaded: - assert_image_equal(im, reloaded) - - reloaded.seek(1) - assert_image_similar(im2, reloaded, 1) - - def test_icc_profile(self, tmp_path: Path) -> None: - self._roundtrip(tmp_path, self.rgb_mode, 12.5, {"icc_profile": None}) - if _webp.HAVE_WEBPANIM: - self._roundtrip( - tmp_path, self.rgb_mode, 12.5, {"icc_profile": None, "save_all": True} - ) - - def test_write_unsupported_mode_L(self, tmp_path: Path) -> None: - """ - Saving a black-and-white file to WebP format should work, and be - similar to the original file. - """ - - self._roundtrip(tmp_path, "L", 10.0) - - def test_write_unsupported_mode_P(self, tmp_path: Path) -> None: - """ - Saving a palette-based file to WebP format should work, and be - similar to the original file. - """ - - self._roundtrip(tmp_path, "P", 50.0) - - @pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") - def test_write_encoding_error_message(self, tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.webp") - im = Image.new("RGB", (15000, 15000)) - with pytest.raises(ValueError) as e: - im.save(temp_file, method=0) - assert str(e.value) == "encoding error 6" - - def test_WebPEncode_with_invalid_args(self) -> None: - """ - Calling encoder functions with no arguments should result in an error. - """ - - if _webp.HAVE_WEBPANIM: - with pytest.raises(TypeError): - _webp.WebPAnimEncoder() - with pytest.raises(TypeError): - _webp.WebPEncode() - - def test_WebPDecode_with_invalid_args(self) -> None: - """ - Calling decoder functions with no arguments should result in an error. - """ - - if _webp.HAVE_WEBPANIM: - with pytest.raises(TypeError): - _webp.WebPAnimDecoder() - with pytest.raises(TypeError): - _webp.WebPDecode() - - def test_no_resource_warning(self, tmp_path: Path) -> None: - file_path = "Tests/images/hopper.webp" - with Image.open(file_path) as image: - temp_file = str(tmp_path / "temp.webp") - with warnings.catch_warnings(): - image.save(temp_file) - - def test_file_pointer_could_be_reused(self) -> None: - file_path = "Tests/images/hopper.webp" - with open(file_path, "rb") as blob: - Image.open(blob).load() - Image.open(blob).load() - - @pytest.mark.parametrize( - "background", - (0, (0,), (-1, 0, 1, 2), (253, 254, 255, 256)), - ) - @skip_unless_feature("webp_anim") - def test_invalid_background( - self, background: int | tuple[int, ...], tmp_path: Path - ) -> None: - temp_file = str(tmp_path / "temp.webp") - im = hopper() - with pytest.raises(OSError): - im.save(temp_file, save_all=True, append_images=[im], background=background) - - @skip_unless_feature("webp_anim") - def test_background_from_gif(self, tmp_path: Path) -> None: - # Save L mode GIF with background - with Image.open("Tests/images/no_palette_with_background.gif") as im: - out_webp = str(tmp_path / "temp.webp") - im.save(out_webp, save_all=True) - - # Save P mode GIF with background - with Image.open("Tests/images/chi.gif") as im: - original_value = im.convert("RGB").getpixel((1, 1)) - - # Save as WEBP - out_webp = str(tmp_path / "temp.webp") - im.save(out_webp, save_all=True) - - # Save as GIF - out_gif = str(tmp_path / "temp.gif") - with Image.open(out_webp) as im: - im.save(out_gif) - - with Image.open(out_gif) as reread: - reread_value = reread.convert("RGB").getpixel((1, 1)) - difference = sum(abs(original_value[i] - reread_value[i]) for i in range(0, 3)) - assert difference < 5 - - @skip_unless_feature("webp_anim") - def test_duration(self, tmp_path: Path) -> None: - with Image.open("Tests/images/dispose_bgnd.gif") as im: - assert im.info["duration"] == 1000 - - out_webp = str(tmp_path / "temp.webp") - im.save(out_webp, save_all=True) - - with Image.open(out_webp) as reloaded: - reloaded.load() - assert reloaded.info["duration"] == 1000 - - def test_roundtrip_rgba_palette(self, tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.webp") - im = Image.new("RGBA", (1, 1)).convert("P") - assert im.mode == "P" - assert im.palette.mode == "RGBA" - im.save(temp_file) - - with Image.open(temp_file) as im: - assert im.getpixel((0, 0)) == (0, 0, 0, 0) +from __future__ import annotations + +import io +import re +import sys +import warnings +from pathlib import Path + +import pytest + +from PIL import Image, WebPImagePlugin, features + +from .helper import ( + assert_image_equal, + assert_image_similar, + assert_image_similar_tofile, + hopper, + skip_unless_feature, +) + +try: + from PIL import _webp + + HAVE_WEBP = True +except ImportError: + HAVE_WEBP = False + + +class TestUnsupportedWebp: + def test_unsupported(self) -> None: + if HAVE_WEBP: + WebPImagePlugin.SUPPORTED = False + + file_path = "Tests/images/hopper.webp" + with pytest.warns(UserWarning): + with pytest.raises(OSError): + with Image.open(file_path): + pass + + if HAVE_WEBP: + WebPImagePlugin.SUPPORTED = True + + +@skip_unless_feature("webp") +class TestFileWebp: + def setup_method(self) -> None: + self.rgb_mode = "RGB" + + def test_version(self) -> None: + _webp.WebPDecoderVersion() + _webp.WebPDecoderBuggyAlpha() + version = features.version_module("webp") + assert version is not None + assert re.search(r"\d+\.\d+\.\d+$", version) + + def test_read_rgb(self) -> None: + """ + Can we read a RGB mode WebP file without error? + Does it have the bits we expect? + """ + + with Image.open("Tests/images/hopper.webp") as image: + assert image.mode == self.rgb_mode + assert image.size == (128, 128) + assert image.format == "WEBP" + image.load() + image.getdata() + + # generated with: + # dwebp -ppm ../../Tests/images/hopper.webp -o hopper_webp_bits.ppm + assert_image_similar_tofile(image, "Tests/images/hopper_webp_bits.ppm", 1.0) + + def _roundtrip(self, tmp_path: Path, mode, epsilon, args={}) -> None: + temp_file = str(tmp_path / "temp.webp") + + hopper(mode).save(temp_file, **args) + with Image.open(temp_file) as image: + assert image.mode == self.rgb_mode + assert image.size == (128, 128) + assert image.format == "WEBP" + image.load() + image.getdata() + + if mode == self.rgb_mode: + # generated with: dwebp -ppm temp.webp -o hopper_webp_write.ppm + assert_image_similar_tofile( + image, "Tests/images/hopper_webp_write.ppm", 12.0 + ) + + # This test asserts that the images are similar. If the average pixel + # difference between the two images is less than the epsilon value, + # then we're going to accept that it's a reasonable lossy version of + # the image. + target = hopper(mode) + if mode != self.rgb_mode: + target = target.convert(self.rgb_mode) + assert_image_similar(image, target, epsilon) + + def test_write_rgb(self, tmp_path: Path) -> None: + """ + Can we write a RGB mode file to webp without error? + Does it have the bits we expect? + """ + + self._roundtrip(tmp_path, self.rgb_mode, 12.5) + + def test_write_method(self, tmp_path: Path) -> None: + self._roundtrip(tmp_path, self.rgb_mode, 12.0, {"method": 6}) + + buffer_no_args = io.BytesIO() + hopper().save(buffer_no_args, format="WEBP") + + buffer_method = io.BytesIO() + hopper().save(buffer_method, format="WEBP", method=6) + assert buffer_no_args.getbuffer() != buffer_method.getbuffer() + + @skip_unless_feature("webp_anim") + def test_save_all(self, tmp_path: Path) -> None: + temp_file = str(tmp_path / "temp.webp") + im = Image.new("RGB", (1, 1)) + im2 = Image.new("RGB", (1, 1), "#f00") + im.save(temp_file, save_all=True, append_images=[im2]) + + with Image.open(temp_file) as reloaded: + assert_image_equal(im, reloaded) + + reloaded.seek(1) + assert_image_similar(im2, reloaded, 1) + + def test_icc_profile(self, tmp_path: Path) -> None: + self._roundtrip(tmp_path, self.rgb_mode, 12.5, {"icc_profile": None}) + if _webp.HAVE_WEBPANIM: + self._roundtrip( + tmp_path, self.rgb_mode, 12.5, {"icc_profile": None, "save_all": True} + ) + + def test_write_unsupported_mode_L(self, tmp_path: Path) -> None: + """ + Saving a black-and-white file to WebP format should work, and be + similar to the original file. + """ + + self._roundtrip(tmp_path, "L", 10.0) + + def test_write_unsupported_mode_P(self, tmp_path: Path) -> None: + """ + Saving a palette-based file to WebP format should work, and be + similar to the original file. + """ + + self._roundtrip(tmp_path, "P", 50.0) + + @pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") + def test_write_encoding_error_message(self, tmp_path: Path) -> None: + temp_file = str(tmp_path / "temp.webp") + im = Image.new("RGB", (15000, 15000)) + with pytest.raises(ValueError) as e: + im.save(temp_file, method=0) + assert str(e.value) == "encoding error 6" + + def test_WebPEncode_with_invalid_args(self) -> None: + """ + Calling encoder functions with no arguments should result in an error. + """ + + if _webp.HAVE_WEBPANIM: + with pytest.raises(TypeError): + _webp.WebPAnimEncoder() + with pytest.raises(TypeError): + _webp.WebPEncode() + + def test_WebPDecode_with_invalid_args(self) -> None: + """ + Calling decoder functions with no arguments should result in an error. + """ + + if _webp.HAVE_WEBPANIM: + with pytest.raises(TypeError): + _webp.WebPAnimDecoder() + with pytest.raises(TypeError): + _webp.WebPDecode() + + def test_no_resource_warning(self, tmp_path: Path) -> None: + file_path = "Tests/images/hopper.webp" + with Image.open(file_path) as image: + temp_file = str(tmp_path / "temp.webp") + with warnings.catch_warnings(): + image.save(temp_file) + + def test_file_pointer_could_be_reused(self) -> None: + file_path = "Tests/images/hopper.webp" + with open(file_path, "rb") as blob: + Image.open(blob).load() + Image.open(blob).load() + + @pytest.mark.parametrize( + "background", + (0, (0,), (-1, 0, 1, 2), (253, 254, 255, 256)), + ) + @skip_unless_feature("webp_anim") + def test_invalid_background( + self, background: int | tuple[int, ...], tmp_path: Path + ) -> None: + temp_file = str(tmp_path / "temp.webp") + im = hopper() + with pytest.raises(OSError): + im.save(temp_file, save_all=True, append_images=[im], background=background) + + @skip_unless_feature("webp_anim") + def test_background_from_gif(self, tmp_path: Path) -> None: + # Save L mode GIF with background + with Image.open("Tests/images/no_palette_with_background.gif") as im: + out_webp = str(tmp_path / "temp.webp") + im.save(out_webp, save_all=True) + + # Save P mode GIF with background + with Image.open("Tests/images/chi.gif") as im: + original_value = im.convert("RGB").getpixel((1, 1)) + + # Save as WEBP + out_webp = str(tmp_path / "temp.webp") + im.save(out_webp, save_all=True) + + # Save as GIF + out_gif = str(tmp_path / "temp.gif") + with Image.open(out_webp) as im: + im.save(out_gif) + + with Image.open(out_gif) as reread: + reread_value = reread.convert("RGB").getpixel((1, 1)) + difference = sum(abs(original_value[i] - reread_value[i]) for i in range(0, 3)) + assert difference < 5 + + @skip_unless_feature("webp_anim") + def test_duration(self, tmp_path: Path) -> None: + with Image.open("Tests/images/dispose_bgnd.gif") as im: + assert im.info["duration"] == 1000 + + out_webp = str(tmp_path / "temp.webp") + im.save(out_webp, save_all=True) + + with Image.open(out_webp) as reloaded: + reloaded.load() + assert reloaded.info["duration"] == 1000 + + def test_roundtrip_rgba_palette(self, tmp_path: Path) -> None: + temp_file = str(tmp_path / "temp.webp") + im = Image.new("RGBA", (1, 1)).convert("P") + assert im.mode == "P" + assert im.palette.mode == "RGBA" + im.save(temp_file) + + with Image.open(temp_file) as im: + assert im.getpixel((0, 0)) == (0, 0, 0, 0) diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py index c74452121..f1a109f9b 100644 --- a/Tests/test_file_webp_alpha.py +++ b/Tests/test_file_webp_alpha.py @@ -1,165 +1,165 @@ -from __future__ import annotations - -from pathlib import Path - -import pytest - -from PIL import Image - -from .helper import ( - assert_image_equal, - assert_image_similar, - assert_image_similar_tofile, - hopper, -) - -_webp = pytest.importorskip("PIL._webp", reason="WebP support not installed") - - -def setup_module() -> None: - if _webp.WebPDecoderBuggyAlpha(): - pytest.skip("Buggy early version of WebP installed, not testing transparency") - - -def test_read_rgba() -> None: - """ - Can we read an RGBA mode file without error? - Does it have the bits we expect? - """ - - # Generated with `cwebp transparent.png -o transparent.webp` - file_path = "Tests/images/transparent.webp" - with Image.open(file_path) as image: - assert image.mode == "RGBA" - assert image.size == (200, 150) - assert image.format == "WEBP" - image.load() - image.getdata() - - image.tobytes() - - assert_image_similar_tofile(image, "Tests/images/transparent.png", 20.0) - - -def test_write_lossless_rgb(tmp_path: Path) -> None: - """ - Can we write an RGBA mode file with lossless compression without error? - Does it have the bits we expect? - """ - - temp_file = str(tmp_path / "temp.webp") - # temp_file = "temp.webp" - - pil_image = hopper("RGBA") - - mask = Image.new("RGBA", (64, 64), (128, 128, 128, 128)) - # Add some partially transparent bits: - pil_image.paste(mask, (0, 0), mask) - - pil_image.save(temp_file, lossless=True) - - with Image.open(temp_file) as image: - image.load() - - assert image.mode == "RGBA" - assert image.size == pil_image.size - assert image.format == "WEBP" - image.load() - image.getdata() - - assert_image_equal(image, pil_image) - - -def test_write_rgba(tmp_path: Path) -> None: - """ - Can we write a RGBA mode file to WebP without error. - Does it have the bits we expect? - """ - - temp_file = str(tmp_path / "temp.webp") - - pil_image = Image.new("RGBA", (10, 10), (255, 0, 0, 20)) - pil_image.save(temp_file) - - if _webp.WebPDecoderBuggyAlpha(): - return - - with Image.open(temp_file) as image: - image.load() - - assert image.mode == "RGBA" - assert image.size == (10, 10) - assert image.format == "WEBP" - image.load() - image.getdata() - - # Early versions of WebP are known to produce higher deviations: - # deal with it - if _webp.WebPDecoderVersion() <= 0x201: - assert_image_similar(image, pil_image, 3.0) - else: - assert_image_similar(image, pil_image, 1.0) - - -def test_keep_rgb_values_when_transparent(tmp_path: Path) -> None: - """ - Saving transparent pixels should retain their original RGB values - when using the "exact" parameter. - """ - - image = hopper("RGB") - - # create a copy of the image - # with the left half transparent - half_transparent_image = image.copy() - new_alpha = Image.new("L", (128, 128), 255) - new_alpha.paste(0, (0, 0, 64, 128)) - half_transparent_image.putalpha(new_alpha) - - # save with transparent area preserved - temp_file = str(tmp_path / "temp.webp") - half_transparent_image.save(temp_file, exact=True, lossless=True) - - with Image.open(temp_file) as reloaded: - assert reloaded.mode == "RGBA" - assert reloaded.format == "WEBP" - - # even though it is lossless, if we don't use exact=True - # in libwebp >= 0.5, the transparent area will be filled with black - # (or something more conducive to compression) - assert_image_equal(reloaded.convert("RGB"), image) - - -def test_write_unsupported_mode_PA(tmp_path: Path) -> None: - """ - Saving a palette-based file with transparency to WebP format - should work, and be similar to the original file. - """ - - temp_file = str(tmp_path / "temp.webp") - file_path = "Tests/images/transparent.gif" - with Image.open(file_path) as im: - im.save(temp_file) - with Image.open(temp_file) as image: - assert image.mode == "RGBA" - assert image.size == (200, 150) - assert image.format == "WEBP" - - image.load() - image.getdata() - with Image.open(file_path) as im: - target = im.convert("RGBA") - - assert_image_similar(image, target, 25.0) - - -def test_alpha_quality(tmp_path: Path) -> None: - with Image.open("Tests/images/transparent.png") as im: - out = str(tmp_path / "temp.webp") - im.save(out) - - out_quality = str(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: - assert reloaded.tobytes() != reloaded_quality.tobytes() +from __future__ import annotations + +from pathlib import Path + +import pytest + +from PIL import Image + +from .helper import ( + assert_image_equal, + assert_image_similar, + assert_image_similar_tofile, + hopper, +) + +_webp = pytest.importorskip("PIL._webp", reason="WebP support not installed") + + +def setup_module() -> None: + if _webp.WebPDecoderBuggyAlpha(): + pytest.skip("Buggy early version of WebP installed, not testing transparency") + + +def test_read_rgba() -> None: + """ + Can we read an RGBA mode file without error? + Does it have the bits we expect? + """ + + # Generated with `cwebp transparent.png -o transparent.webp` + file_path = "Tests/images/transparent.webp" + with Image.open(file_path) as image: + assert image.mode == "RGBA" + assert image.size == (200, 150) + assert image.format == "WEBP" + image.load() + image.getdata() + + image.tobytes() + + assert_image_similar_tofile(image, "Tests/images/transparent.png", 20.0) + + +def test_write_lossless_rgb(tmp_path: Path) -> None: + """ + Can we write an RGBA mode file with lossless compression without error? + Does it have the bits we expect? + """ + + temp_file = str(tmp_path / "temp.webp") + # temp_file = "temp.webp" + + pil_image = hopper("RGBA") + + mask = Image.new("RGBA", (64, 64), (128, 128, 128, 128)) + # Add some partially transparent bits: + pil_image.paste(mask, (0, 0), mask) + + pil_image.save(temp_file, lossless=True) + + with Image.open(temp_file) as image: + image.load() + + assert image.mode == "RGBA" + assert image.size == pil_image.size + assert image.format == "WEBP" + image.load() + image.getdata() + + assert_image_equal(image, pil_image) + + +def test_write_rgba(tmp_path: Path) -> None: + """ + Can we write a RGBA mode file to WebP without error. + Does it have the bits we expect? + """ + + temp_file = str(tmp_path / "temp.webp") + + pil_image = Image.new("RGBA", (10, 10), (255, 0, 0, 20)) + pil_image.save(temp_file) + + if _webp.WebPDecoderBuggyAlpha(): + return + + with Image.open(temp_file) as image: + image.load() + + assert image.mode == "RGBA" + assert image.size == (10, 10) + assert image.format == "WEBP" + image.load() + image.getdata() + + # Early versions of WebP are known to produce higher deviations: + # deal with it + if _webp.WebPDecoderVersion() <= 0x201: + assert_image_similar(image, pil_image, 3.0) + else: + assert_image_similar(image, pil_image, 1.0) + + +def test_keep_rgb_values_when_transparent(tmp_path: Path) -> None: + """ + Saving transparent pixels should retain their original RGB values + when using the "exact" parameter. + """ + + image = hopper("RGB") + + # create a copy of the image + # with the left half transparent + half_transparent_image = image.copy() + new_alpha = Image.new("L", (128, 128), 255) + new_alpha.paste(0, (0, 0, 64, 128)) + half_transparent_image.putalpha(new_alpha) + + # save with transparent area preserved + temp_file = str(tmp_path / "temp.webp") + half_transparent_image.save(temp_file, exact=True, lossless=True) + + with Image.open(temp_file) as reloaded: + assert reloaded.mode == "RGBA" + assert reloaded.format == "WEBP" + + # even though it is lossless, if we don't use exact=True + # in libwebp >= 0.5, the transparent area will be filled with black + # (or something more conducive to compression) + assert_image_equal(reloaded.convert("RGB"), image) + + +def test_write_unsupported_mode_PA(tmp_path: Path) -> None: + """ + Saving a palette-based file with transparency to WebP format + should work, and be similar to the original file. + """ + + temp_file = str(tmp_path / "temp.webp") + file_path = "Tests/images/transparent.gif" + with Image.open(file_path) as im: + im.save(temp_file) + with Image.open(temp_file) as image: + assert image.mode == "RGBA" + assert image.size == (200, 150) + assert image.format == "WEBP" + + image.load() + image.getdata() + with Image.open(file_path) as im: + target = im.convert("RGBA") + + assert_image_similar(image, target, 25.0) + + +def test_alpha_quality(tmp_path: Path) -> None: + with Image.open("Tests/images/transparent.png") as im: + out = str(tmp_path / "temp.webp") + im.save(out) + + out_quality = str(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: + assert reloaded.tobytes() != reloaded_quality.tobytes() diff --git a/Tests/test_file_webp_animated.py b/Tests/test_file_webp_animated.py index 882dccb32..976b209f6 100644 --- a/Tests/test_file_webp_animated.py +++ b/Tests/test_file_webp_animated.py @@ -1,210 +1,210 @@ -from __future__ import annotations - -from pathlib import Path - -import pytest -from packaging.version import parse as parse_version - -from PIL import Image, features - -from .helper import ( - assert_image_equal, - assert_image_similar, - is_big_endian, - skip_unless_feature, -) - -pytestmark = [ - skip_unless_feature("webp"), - skip_unless_feature("webp_anim"), -] - - -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 im.n_frames == 1 - assert not im.is_animated - - with Image.open("Tests/images/iss634.webp") as im: - assert im.n_frames == 42 - assert im.is_animated - - -def test_write_animation_L(tmp_path: Path) -> None: - """ - Convert an animated GIF to animated WebP, then compare the frame count, and first - and last frames to ensure they're visually similar. - """ - - with Image.open("Tests/images/iss634.gif") as orig: - assert orig.n_frames > 1 - - temp_file = str(tmp_path / "temp.webp") - orig.save(temp_file, save_all=True) - with Image.open(temp_file) as im: - assert im.n_frames == orig.n_frames - - # Compare first and last frames to the original animated GIF - orig.load() - im.load() - assert_image_similar(im, orig.convert("RGBA"), 32.9) - - if is_big_endian(): - version = features.version_module("webp") - assert version is not None - if parse_version(version) < parse_version("1.2.2"): - pytest.skip("Fails with libwebp earlier than 1.2.2") - orig.seek(orig.n_frames - 1) - im.seek(im.n_frames - 1) - orig.load() - im.load() - assert_image_similar(im, orig.convert("RGBA"), 32.9) - - -def test_write_animation_RGB(tmp_path: Path) -> None: - """ - Write an animated WebP from RGB frames, and ensure the frames - are visually similar to the originals. - """ - - def check(temp_file: str) -> None: - with Image.open(temp_file) as im: - assert im.n_frames == 2 - - # Compare first frame to original - im.load() - assert_image_equal(im, frame1.convert("RGBA")) - - # Compare second frame to original - if is_big_endian(): - version = features.version_module("webp") - assert version is not None - if parse_version(version) < parse_version("1.2.2"): - pytest.skip("Fails with libwebp earlier than 1.2.2") - im.seek(1) - im.load() - assert_image_equal(im, frame2.convert("RGBA")) - - with Image.open("Tests/images/anim_frame1.webp") as frame1: - with Image.open("Tests/images/anim_frame2.webp") as frame2: - temp_file1 = str(tmp_path / "temp.webp") - frame1.copy().save( - temp_file1, save_all=True, append_images=[frame2], lossless=True - ) - check(temp_file1) - - # Tests appending using a generator - def im_generator(ims): - yield from ims - - temp_file2 = str(tmp_path / "temp_generator.webp") - frame1.copy().save( - temp_file2, - save_all=True, - append_images=im_generator([frame2]), - lossless=True, - ) - check(temp_file2) - - -def test_timestamp_and_duration(tmp_path: Path) -> None: - """ - Try passing a list of durations, and make sure the encoded - timestamps and durations are correct. - """ - - durations = [0, 10, 20, 30, 40] - temp_file = str(tmp_path / "temp.webp") - with Image.open("Tests/images/anim_frame1.webp") as frame1: - with Image.open("Tests/images/anim_frame2.webp") as frame2: - frame1.save( - temp_file, - save_all=True, - append_images=[frame2, frame1, frame2, frame1], - duration=durations, - ) - - with Image.open(temp_file) as im: - assert im.n_frames == 5 - assert im.is_animated - - # Check that timestamps and durations match original values specified - ts = 0 - for frame in range(im.n_frames): - im.seek(frame) - im.load() - assert im.info["duration"] == durations[frame] - assert im.info["timestamp"] == ts - ts += durations[frame] - - -def test_float_duration(tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.webp") - with Image.open("Tests/images/iss634.apng") as im: - assert im.info["duration"] == 70.0 - - im.save(temp_file, save_all=True) - - with Image.open(temp_file) as reloaded: - reloaded.load() - assert reloaded.info["duration"] == 70 - - -def test_seeking(tmp_path: Path) -> None: - """ - Create an animated WebP file, and then try seeking through frames in reverse-order, - verifying the timestamps and durations are correct. - """ - - dur = 33 - temp_file = str(tmp_path / "temp.webp") - with Image.open("Tests/images/anim_frame1.webp") as frame1: - with Image.open("Tests/images/anim_frame2.webp") as frame2: - frame1.save( - temp_file, - save_all=True, - append_images=[frame2, frame1, frame2, frame1], - duration=dur, - ) - - with Image.open(temp_file) as im: - assert im.n_frames == 5 - assert im.is_animated - - # Traverse frames in reverse, checking timestamps and durations - ts = dur * (im.n_frames - 1) - for frame in reversed(range(im.n_frames)): - im.seek(frame) - im.load() - assert im.info["duration"] == dur - assert im.info["timestamp"] == ts - ts -= dur - - -def test_seek_errors() -> None: - with Image.open("Tests/images/iss634.webp") as im: - with pytest.raises(EOFError): - im.seek(-1) - - with pytest.raises(EOFError): - im.seek(42) - - -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") - first_frame.save(out, save_all=True, append_images=[im]) - - out_quality = str(tmp_path / "quality.webp") - first_frame.save( - out_quality, save_all=True, append_images=[im], alpha_quality=50 - ) - with Image.open(out) as reloaded: - reloaded.seek(1) - with Image.open(out_quality) as reloaded_quality: - reloaded_quality.seek(1) - assert reloaded.tobytes() != reloaded_quality.tobytes() +from __future__ import annotations + +from pathlib import Path + +import pytest +from packaging.version import parse as parse_version + +from PIL import Image, features + +from .helper import ( + assert_image_equal, + assert_image_similar, + is_big_endian, + skip_unless_feature, +) + +pytestmark = [ + skip_unless_feature("webp"), + skip_unless_feature("webp_anim"), +] + + +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 im.n_frames == 1 + assert not im.is_animated + + with Image.open("Tests/images/iss634.webp") as im: + assert im.n_frames == 42 + assert im.is_animated + + +def test_write_animation_L(tmp_path: Path) -> None: + """ + Convert an animated GIF to animated WebP, then compare the frame count, and first + and last frames to ensure they're visually similar. + """ + + with Image.open("Tests/images/iss634.gif") as orig: + assert orig.n_frames > 1 + + temp_file = str(tmp_path / "temp.webp") + orig.save(temp_file, save_all=True) + with Image.open(temp_file) as im: + assert im.n_frames == orig.n_frames + + # Compare first and last frames to the original animated GIF + orig.load() + im.load() + assert_image_similar(im, orig.convert("RGBA"), 32.9) + + if is_big_endian(): + version = features.version_module("webp") + assert version is not None + if parse_version(version) < parse_version("1.2.2"): + pytest.skip("Fails with libwebp earlier than 1.2.2") + orig.seek(orig.n_frames - 1) + im.seek(im.n_frames - 1) + orig.load() + im.load() + assert_image_similar(im, orig.convert("RGBA"), 32.9) + + +def test_write_animation_RGB(tmp_path: Path) -> None: + """ + Write an animated WebP from RGB frames, and ensure the frames + are visually similar to the originals. + """ + + def check(temp_file: str) -> None: + with Image.open(temp_file) as im: + assert im.n_frames == 2 + + # Compare first frame to original + im.load() + assert_image_equal(im, frame1.convert("RGBA")) + + # Compare second frame to original + if is_big_endian(): + version = features.version_module("webp") + assert version is not None + if parse_version(version) < parse_version("1.2.2"): + pytest.skip("Fails with libwebp earlier than 1.2.2") + im.seek(1) + im.load() + assert_image_equal(im, frame2.convert("RGBA")) + + with Image.open("Tests/images/anim_frame1.webp") as frame1: + with Image.open("Tests/images/anim_frame2.webp") as frame2: + temp_file1 = str(tmp_path / "temp.webp") + frame1.copy().save( + temp_file1, save_all=True, append_images=[frame2], lossless=True + ) + check(temp_file1) + + # Tests appending using a generator + def im_generator(ims): + yield from ims + + temp_file2 = str(tmp_path / "temp_generator.webp") + frame1.copy().save( + temp_file2, + save_all=True, + append_images=im_generator([frame2]), + lossless=True, + ) + check(temp_file2) + + +def test_timestamp_and_duration(tmp_path: Path) -> None: + """ + Try passing a list of durations, and make sure the encoded + timestamps and durations are correct. + """ + + durations = [0, 10, 20, 30, 40] + temp_file = str(tmp_path / "temp.webp") + with Image.open("Tests/images/anim_frame1.webp") as frame1: + with Image.open("Tests/images/anim_frame2.webp") as frame2: + frame1.save( + temp_file, + save_all=True, + append_images=[frame2, frame1, frame2, frame1], + duration=durations, + ) + + with Image.open(temp_file) as im: + assert im.n_frames == 5 + assert im.is_animated + + # Check that timestamps and durations match original values specified + ts = 0 + for frame in range(im.n_frames): + im.seek(frame) + im.load() + assert im.info["duration"] == durations[frame] + assert im.info["timestamp"] == ts + ts += durations[frame] + + +def test_float_duration(tmp_path: Path) -> None: + temp_file = str(tmp_path / "temp.webp") + with Image.open("Tests/images/iss634.apng") as im: + assert im.info["duration"] == 70.0 + + im.save(temp_file, save_all=True) + + with Image.open(temp_file) as reloaded: + reloaded.load() + assert reloaded.info["duration"] == 70 + + +def test_seeking(tmp_path: Path) -> None: + """ + Create an animated WebP file, and then try seeking through frames in reverse-order, + verifying the timestamps and durations are correct. + """ + + dur = 33 + temp_file = str(tmp_path / "temp.webp") + with Image.open("Tests/images/anim_frame1.webp") as frame1: + with Image.open("Tests/images/anim_frame2.webp") as frame2: + frame1.save( + temp_file, + save_all=True, + append_images=[frame2, frame1, frame2, frame1], + duration=dur, + ) + + with Image.open(temp_file) as im: + assert im.n_frames == 5 + assert im.is_animated + + # Traverse frames in reverse, checking timestamps and durations + ts = dur * (im.n_frames - 1) + for frame in reversed(range(im.n_frames)): + im.seek(frame) + im.load() + assert im.info["duration"] == dur + assert im.info["timestamp"] == ts + ts -= dur + + +def test_seek_errors() -> None: + with Image.open("Tests/images/iss634.webp") as im: + with pytest.raises(EOFError): + im.seek(-1) + + with pytest.raises(EOFError): + im.seek(42) + + +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") + first_frame.save(out, save_all=True, append_images=[im]) + + out_quality = str(tmp_path / "quality.webp") + first_frame.save( + out_quality, save_all=True, append_images=[im], alpha_quality=50 + ) + with Image.open(out) as reloaded: + reloaded.seek(1) + with Image.open(out_quality) as reloaded_quality: + reloaded_quality.seek(1) + assert reloaded.tobytes() != reloaded_quality.tobytes() diff --git a/Tests/test_file_webp_lossless.py b/Tests/test_file_webp_lossless.py index 32e29de56..d278f8961 100644 --- a/Tests/test_file_webp_lossless.py +++ b/Tests/test_file_webp_lossless.py @@ -1,32 +1,32 @@ -from __future__ import annotations - -from pathlib import Path - -import pytest - -from PIL import Image - -from .helper import assert_image_equal, hopper - -_webp = pytest.importorskip("PIL._webp", reason="WebP support not installed") -RGB_MODE = "RGB" - - -def test_write_lossless_rgb(tmp_path: Path) -> None: - if _webp.WebPDecoderVersion() < 0x0200: - pytest.skip("lossless not included") - - temp_file = str(tmp_path / "temp.webp") - - hopper(RGB_MODE).save(temp_file, lossless=True) - - with Image.open(temp_file) as image: - image.load() - - assert image.mode == RGB_MODE - assert image.size == (128, 128) - assert image.format == "WEBP" - image.load() - image.getdata() - - assert_image_equal(image, hopper(RGB_MODE)) +from __future__ import annotations + +from pathlib import Path + +import pytest + +from PIL import Image + +from .helper import assert_image_equal, hopper + +_webp = pytest.importorskip("PIL._webp", reason="WebP support not installed") +RGB_MODE = "RGB" + + +def test_write_lossless_rgb(tmp_path: Path) -> None: + if _webp.WebPDecoderVersion() < 0x0200: + pytest.skip("lossless not included") + + temp_file = str(tmp_path / "temp.webp") + + hopper(RGB_MODE).save(temp_file, lossless=True) + + with Image.open(temp_file) as image: + image.load() + + assert image.mode == RGB_MODE + assert image.size == (128, 128) + assert image.format == "WEBP" + image.load() + image.getdata() + + assert_image_equal(image, hopper(RGB_MODE)) diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index 875941240..ce383c2e7 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -1,162 +1,162 @@ -from __future__ import annotations - -from io import BytesIO -from pathlib import Path -from types import ModuleType - -import pytest - -from PIL import Image - -from .helper import mark_if_feature_version, skip_unless_feature - -pytestmark = [ - skip_unless_feature("webp"), - skip_unless_feature("webp_mux"), -] - -ElementTree: ModuleType | None -try: - from defusedxml import ElementTree -except ImportError: - ElementTree = None - - -def test_read_exif_metadata() -> None: - file_path = "Tests/images/flower.webp" - with Image.open(file_path) as image: - assert image.format == "WEBP" - exif_data = image.info.get("exif", None) - assert exif_data - - exif = image._getexif() - - # Camera make - assert exif[271] == "Canon" - - with Image.open("Tests/images/flower.jpg") as jpeg_image: - expected_exif = jpeg_image.info["exif"] - - assert exif_data == expected_exif - - -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" - - exif = im.getexif() - assert exif[305] == "Adobe Photoshop CS6 (Macintosh)" - - -@mark_if_feature_version( - pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" -) -def test_write_exif_metadata() -> None: - file_path = "Tests/images/flower.jpg" - test_buffer = BytesIO() - with Image.open(file_path) as image: - expected_exif = image.info["exif"] - - image.save(test_buffer, "webp", exif=expected_exif) - - test_buffer.seek(0) - with Image.open(test_buffer) as webp_image: - webp_exif = webp_image.info.get("exif", None) - assert webp_exif == expected_exif[6:], "WebP EXIF didn't match" - - -def test_read_icc_profile() -> None: - file_path = "Tests/images/flower2.webp" - with Image.open(file_path) as image: - assert image.format == "WEBP" - assert image.info.get("icc_profile", None) - - icc = image.info["icc_profile"] - - with Image.open("Tests/images/flower2.jpg") as jpeg_image: - expected_icc = jpeg_image.info["icc_profile"] - - assert icc == expected_icc - - -@mark_if_feature_version( - pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" -) -def test_write_icc_metadata() -> None: - file_path = "Tests/images/flower2.jpg" - test_buffer = BytesIO() - with Image.open(file_path) as image: - expected_icc_profile = image.info["icc_profile"] - - image.save(test_buffer, "webp", icc_profile=expected_icc_profile) - - test_buffer.seek(0) - with Image.open(test_buffer) as webp_image: - webp_icc_profile = webp_image.info.get("icc_profile", None) - - assert webp_icc_profile - if webp_icc_profile: - assert webp_icc_profile == expected_icc_profile, "Webp ICC didn't match" - - -@mark_if_feature_version( - pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" -) -def test_read_no_exif() -> None: - file_path = "Tests/images/flower.jpg" - test_buffer = BytesIO() - with Image.open(file_path) as image: - assert "exif" in image.info - - image.save(test_buffer, "webp") - - test_buffer.seek(0) - with Image.open(test_buffer) as webp_image: - assert not webp_image._getexif() - - -def test_getxmp() -> None: - with Image.open("Tests/images/flower.webp") as im: - assert "xmp" not in im.info - assert im.getxmp() == {} - - with Image.open("Tests/images/flower2.webp") as im: - if ElementTree is None: - with pytest.warns( - UserWarning, - match="XMP data cannot be read without defusedxml dependency", - ): - assert im.getxmp() == {} - else: - assert ( - im.getxmp()["xmpmeta"]["xmptk"] - == "Adobe XMP Core 5.3-c011 66.145661, 2012/02/06-14:56:27 " - ) - - -@skip_unless_feature("webp_anim") -def test_write_animated_metadata(tmp_path: Path) -> None: - iccp_data = b"" - exif_data = b"" - xmp_data = b"" - - temp_file = str(tmp_path / "temp.webp") - with Image.open("Tests/images/anim_frame1.webp") as frame1: - with Image.open("Tests/images/anim_frame2.webp") as frame2: - frame1.save( - temp_file, - save_all=True, - append_images=[frame2, frame1, frame2], - icc_profile=iccp_data, - exif=exif_data, - xmp=xmp_data, - ) - - with Image.open(temp_file) as image: - assert "icc_profile" in image.info - assert "exif" in image.info - assert "xmp" in image.info - assert iccp_data == image.info.get("icc_profile", None) - assert exif_data == image.info.get("exif", None) - assert xmp_data == image.info.get("xmp", None) +from __future__ import annotations + +from io import BytesIO +from pathlib import Path +from types import ModuleType + +import pytest + +from PIL import Image + +from .helper import mark_if_feature_version, skip_unless_feature + +pytestmark = [ + skip_unless_feature("webp"), + skip_unless_feature("webp_mux"), +] + +ElementTree: ModuleType | None +try: + from defusedxml import ElementTree +except ImportError: + ElementTree = None + + +def test_read_exif_metadata() -> None: + file_path = "Tests/images/flower.webp" + with Image.open(file_path) as image: + assert image.format == "WEBP" + exif_data = image.info.get("exif", None) + assert exif_data + + exif = image._getexif() + + # Camera make + assert exif[271] == "Canon" + + with Image.open("Tests/images/flower.jpg") as jpeg_image: + expected_exif = jpeg_image.info["exif"] + + assert exif_data == expected_exif + + +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" + + exif = im.getexif() + assert exif[305] == "Adobe Photoshop CS6 (Macintosh)" + + +@mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" +) +def test_write_exif_metadata() -> None: + file_path = "Tests/images/flower.jpg" + test_buffer = BytesIO() + with Image.open(file_path) as image: + expected_exif = image.info["exif"] + + image.save(test_buffer, "webp", exif=expected_exif) + + test_buffer.seek(0) + with Image.open(test_buffer) as webp_image: + webp_exif = webp_image.info.get("exif", None) + assert webp_exif == expected_exif[6:], "WebP EXIF didn't match" + + +def test_read_icc_profile() -> None: + file_path = "Tests/images/flower2.webp" + with Image.open(file_path) as image: + assert image.format == "WEBP" + assert image.info.get("icc_profile", None) + + icc = image.info["icc_profile"] + + with Image.open("Tests/images/flower2.jpg") as jpeg_image: + expected_icc = jpeg_image.info["icc_profile"] + + assert icc == expected_icc + + +@mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" +) +def test_write_icc_metadata() -> None: + file_path = "Tests/images/flower2.jpg" + test_buffer = BytesIO() + with Image.open(file_path) as image: + expected_icc_profile = image.info["icc_profile"] + + image.save(test_buffer, "webp", icc_profile=expected_icc_profile) + + test_buffer.seek(0) + with Image.open(test_buffer) as webp_image: + webp_icc_profile = webp_image.info.get("icc_profile", None) + + assert webp_icc_profile + if webp_icc_profile: + assert webp_icc_profile == expected_icc_profile, "Webp ICC didn't match" + + +@mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" +) +def test_read_no_exif() -> None: + file_path = "Tests/images/flower.jpg" + test_buffer = BytesIO() + with Image.open(file_path) as image: + assert "exif" in image.info + + image.save(test_buffer, "webp") + + test_buffer.seek(0) + with Image.open(test_buffer) as webp_image: + assert not webp_image._getexif() + + +def test_getxmp() -> None: + with Image.open("Tests/images/flower.webp") as im: + assert "xmp" not in im.info + assert im.getxmp() == {} + + with Image.open("Tests/images/flower2.webp") as im: + if ElementTree is None: + with pytest.warns( + UserWarning, + match="XMP data cannot be read without defusedxml dependency", + ): + assert im.getxmp() == {} + else: + assert ( + im.getxmp()["xmpmeta"]["xmptk"] + == "Adobe XMP Core 5.3-c011 66.145661, 2012/02/06-14:56:27 " + ) + + +@skip_unless_feature("webp_anim") +def test_write_animated_metadata(tmp_path: Path) -> None: + iccp_data = b"" + exif_data = b"" + xmp_data = b"" + + temp_file = str(tmp_path / "temp.webp") + with Image.open("Tests/images/anim_frame1.webp") as frame1: + with Image.open("Tests/images/anim_frame2.webp") as frame2: + frame1.save( + temp_file, + save_all=True, + append_images=[frame2, frame1, frame2], + icc_profile=iccp_data, + exif=exif_data, + xmp=xmp_data, + ) + + with Image.open(temp_file) as image: + assert "icc_profile" in image.info + assert "exif" in image.info + assert "xmp" in image.info + assert iccp_data == image.info.get("icc_profile", None) + assert exif_data == image.info.get("exif", None) + assert xmp_data == image.info.get("xmp", None) diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index 79e707263..ce6f9d0ea 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -1,82 +1,82 @@ -from __future__ import annotations - -from pathlib import Path -from typing import IO - -import pytest - -from PIL import Image, ImageFile, WmfImagePlugin - -from .helper import assert_image_similar_tofile, hopper - - -def test_load_raw() -> None: - # Test basic EMF open and rendering - with Image.open("Tests/images/drawing.emf") as im: - if hasattr(Image.core, "drawwmf"): - # Currently, support for WMF/EMF is Windows-only - im.load() - # Compare to reference rendering - assert_image_similar_tofile(im, "Tests/images/drawing_emf_ref.png", 0) - - # Test basic WMF open and rendering - with Image.open("Tests/images/drawing.wmf") as im: - if hasattr(Image.core, "drawwmf"): - # Currently, support for WMF/EMF is Windows-only - im.load() - # Compare to reference rendering - assert_image_similar_tofile(im, "Tests/images/drawing_wmf_ref.png", 2.0) - - -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) - - -def test_register_handler(tmp_path: Path) -> None: - class TestHandler(ImageFile.StubHandler): - methodCalled = False - - def load(self, im: ImageFile.StubImageFile) -> Image.Image: - return Image.new("RGB", (1, 1)) - - def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None: - self.methodCalled = True - - handler = TestHandler() - original_handler = WmfImagePlugin._handler - WmfImagePlugin.register_handler(handler) - - im = hopper() - tmpfile = str(tmp_path / "temp.wmf") - im.save(tmpfile) - assert handler.methodCalled - - # Restore the state before this test - WmfImagePlugin.register_handler(original_handler) - - -def test_load_float_dpi() -> None: - with Image.open("Tests/images/drawing.emf") as im: - assert im.info["dpi"] == 1423.7668161434979 - - -def test_load_set_dpi() -> None: - with Image.open("Tests/images/drawing.wmf") as im: - assert im.size == (82, 82) - - if hasattr(Image.core, "drawwmf"): - im.load(144) - assert im.size == (164, 164) - - assert_image_similar_tofile(im, "Tests/images/drawing_wmf_ref_144.png", 2.1) - - -@pytest.mark.parametrize("ext", (".wmf", ".emf")) -def test_save(ext: str, tmp_path: Path) -> None: - im = hopper() - - tmpfile = str(tmp_path / ("temp" + ext)) - with pytest.raises(OSError): - im.save(tmpfile) +from __future__ import annotations + +from pathlib import Path +from typing import IO + +import pytest + +from PIL import Image, ImageFile, WmfImagePlugin + +from .helper import assert_image_similar_tofile, hopper + + +def test_load_raw() -> None: + # Test basic EMF open and rendering + with Image.open("Tests/images/drawing.emf") as im: + if hasattr(Image.core, "drawwmf"): + # Currently, support for WMF/EMF is Windows-only + im.load() + # Compare to reference rendering + assert_image_similar_tofile(im, "Tests/images/drawing_emf_ref.png", 0) + + # Test basic WMF open and rendering + with Image.open("Tests/images/drawing.wmf") as im: + if hasattr(Image.core, "drawwmf"): + # Currently, support for WMF/EMF is Windows-only + im.load() + # Compare to reference rendering + assert_image_similar_tofile(im, "Tests/images/drawing_wmf_ref.png", 2.0) + + +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) + + +def test_register_handler(tmp_path: Path) -> None: + class TestHandler(ImageFile.StubHandler): + methodCalled = False + + def load(self, im: ImageFile.StubImageFile) -> Image.Image: + return Image.new("RGB", (1, 1)) + + def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None: + self.methodCalled = True + + handler = TestHandler() + original_handler = WmfImagePlugin._handler + WmfImagePlugin.register_handler(handler) + + im = hopper() + tmpfile = str(tmp_path / "temp.wmf") + im.save(tmpfile) + assert handler.methodCalled + + # Restore the state before this test + WmfImagePlugin.register_handler(original_handler) + + +def test_load_float_dpi() -> None: + with Image.open("Tests/images/drawing.emf") as im: + assert im.info["dpi"] == 1423.7668161434979 + + +def test_load_set_dpi() -> None: + with Image.open("Tests/images/drawing.wmf") as im: + assert im.size == (82, 82) + + if hasattr(Image.core, "drawwmf"): + im.load(144) + assert im.size == (164, 164) + + assert_image_similar_tofile(im, "Tests/images/drawing_wmf_ref_144.png", 2.1) + + +@pytest.mark.parametrize("ext", (".wmf", ".emf")) +def test_save(ext: str, tmp_path: Path) -> None: + im = hopper() + + tmpfile = str(tmp_path / ("temp" + ext)) + with pytest.raises(OSError): + im.save(tmpfile) diff --git a/Tests/test_file_xbm.py b/Tests/test_file_xbm.py index 44dd2541f..387e70b94 100644 --- a/Tests/test_file_xbm.py +++ b/Tests/test_file_xbm.py @@ -1,90 +1,90 @@ -from __future__ import annotations - -from io import BytesIO -from pathlib import Path - -import pytest - -from PIL import Image, XbmImagePlugin - -from .helper import hopper - -PIL151 = b""" -#define basic_width 32 -#define basic_height 32 -static char basic_bits[] = { -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, -0x00, 0x00, 0x00, 0x00, -0x80, 0xff, 0xff, 0x01, 0x40, 0x00, 0x00, 0x02, -0x20, 0x00, 0x00, 0x04, 0x20, 0x00, 0x00, 0x04, 0x10, 0x00, 0x00, 0x08, -0x10, 0x00, 0x00, 0x08, -0x10, 0x00, 0x00, 0x08, 0x10, 0x00, 0x00, 0x08, -0x10, 0x00, 0x00, 0x08, 0x10, 0x00, 0x00, 0x08, 0x10, 0x00, 0x00, 0x08, -0x20, 0x00, 0x00, 0x04, -0x20, 0x00, 0x00, 0x04, 0x40, 0x00, 0x00, 0x02, -0x80, 0xff, 0xff, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, -0x00, 0x00, 0x00, 0x00, -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, -0x00, 0x00, 0x00, 0x00, -}; -""" - - -def test_pil151() -> None: - with Image.open(BytesIO(PIL151)) as im: - im.load() - assert im.mode == "1" - assert im.size == (32, 32) - - -def test_open() -> None: - # Arrange - # Created with `convert hopper.png hopper.xbm` - filename = "Tests/images/hopper.xbm" - - # Act - with Image.open(filename) as im: - # Assert - assert im.mode == "1" - assert im.size == (128, 128) - - -def test_open_filename_with_underscore() -> None: - # Arrange - # Created with `convert hopper.png hopper_underscore.xbm` - filename = "Tests/images/hopper_underscore.xbm" - - # Act - with Image.open(filename) as im: - # Assert - assert im.mode == "1" - assert im.size == (128, 128) - - -def test_invalid_file() -> None: - invalid_file = "Tests/images/flower.jpg" - - with pytest.raises(SyntaxError): - XbmImagePlugin.XbmImageFile(invalid_file) - - -def test_save_wrong_mode(tmp_path: Path) -> None: - im = hopper() - out = str(tmp_path / "temp.xbm") - - with pytest.raises(OSError): - im.save(out) - - -def test_hotspot(tmp_path: Path) -> None: - im = hopper("1") - out = str(tmp_path / "temp.xbm") - - hotspot = (0, 7) - im.save(out, hotspot=hotspot) - - with Image.open(out) as reloaded: - assert reloaded.info["hotspot"] == hotspot +from __future__ import annotations + +from io import BytesIO +from pathlib import Path + +import pytest + +from PIL import Image, XbmImagePlugin + +from .helper import hopper + +PIL151 = b""" +#define basic_width 32 +#define basic_height 32 +static char basic_bits[] = { +0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +0x00, 0x00, 0x00, 0x00, +0x80, 0xff, 0xff, 0x01, 0x40, 0x00, 0x00, 0x02, +0x20, 0x00, 0x00, 0x04, 0x20, 0x00, 0x00, 0x04, 0x10, 0x00, 0x00, 0x08, +0x10, 0x00, 0x00, 0x08, +0x10, 0x00, 0x00, 0x08, 0x10, 0x00, 0x00, 0x08, +0x10, 0x00, 0x00, 0x08, 0x10, 0x00, 0x00, 0x08, 0x10, 0x00, 0x00, 0x08, +0x20, 0x00, 0x00, 0x04, +0x20, 0x00, 0x00, 0x04, 0x40, 0x00, 0x00, 0x02, +0x80, 0xff, 0xff, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +0x00, 0x00, 0x00, 0x00, +0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +0x00, 0x00, 0x00, 0x00, +}; +""" + + +def test_pil151() -> None: + with Image.open(BytesIO(PIL151)) as im: + im.load() + assert im.mode == "1" + assert im.size == (32, 32) + + +def test_open() -> None: + # Arrange + # Created with `convert hopper.png hopper.xbm` + filename = "Tests/images/hopper.xbm" + + # Act + with Image.open(filename) as im: + # Assert + assert im.mode == "1" + assert im.size == (128, 128) + + +def test_open_filename_with_underscore() -> None: + # Arrange + # Created with `convert hopper.png hopper_underscore.xbm` + filename = "Tests/images/hopper_underscore.xbm" + + # Act + with Image.open(filename) as im: + # Assert + assert im.mode == "1" + assert im.size == (128, 128) + + +def test_invalid_file() -> None: + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(SyntaxError): + XbmImagePlugin.XbmImageFile(invalid_file) + + +def test_save_wrong_mode(tmp_path: Path) -> None: + im = hopper() + out = str(tmp_path / "temp.xbm") + + with pytest.raises(OSError): + im.save(out) + + +def test_hotspot(tmp_path: Path) -> None: + im = hopper("1") + out = str(tmp_path / "temp.xbm") + + hotspot = (0, 7) + im.save(out, hotspot=hotspot) + + with Image.open(out) as reloaded: + assert reloaded.info["hotspot"] == hotspot diff --git a/Tests/test_file_xpm.py b/Tests/test_file_xpm.py index 26afe93f4..6c3f4febc 100644 --- a/Tests/test_file_xpm.py +++ b/Tests/test_file_xpm.py @@ -1,39 +1,39 @@ -from __future__ import annotations - -import pytest - -from PIL import Image, XpmImagePlugin - -from .helper import assert_image_similar, hopper - -TEST_FILE = "Tests/images/hopper.xpm" - - -def test_sanity() -> None: - with Image.open(TEST_FILE) as im: - im.load() - assert im.mode == "P" - assert im.size == (128, 128) - assert im.format == "XPM" - - # large error due to quantization->44 colors. - assert_image_similar(im.convert("RGB"), hopper("RGB"), 60) - - -def test_invalid_file() -> None: - invalid_file = "Tests/images/flower.jpg" - - with pytest.raises(SyntaxError): - XpmImagePlugin.XpmImageFile(invalid_file) - - -def test_load_read() -> None: - # Arrange - with Image.open(TEST_FILE) as im: - dummy_bytes = 1 - - # Act - data = im.load_read(dummy_bytes) - - # Assert - assert len(data) == 16384 +from __future__ import annotations + +import pytest + +from PIL import Image, XpmImagePlugin + +from .helper import assert_image_similar, hopper + +TEST_FILE = "Tests/images/hopper.xpm" + + +def test_sanity() -> None: + with Image.open(TEST_FILE) as im: + im.load() + assert im.mode == "P" + assert im.size == (128, 128) + assert im.format == "XPM" + + # large error due to quantization->44 colors. + assert_image_similar(im.convert("RGB"), hopper("RGB"), 60) + + +def test_invalid_file() -> None: + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(SyntaxError): + XpmImagePlugin.XpmImageFile(invalid_file) + + +def test_load_read() -> None: + # Arrange + with Image.open(TEST_FILE) as im: + dummy_bytes = 1 + + # Act + data = im.load_read(dummy_bytes) + + # Assert + assert len(data) == 16384 diff --git a/Tests/test_file_xvthumb.py b/Tests/test_file_xvthumb.py index 6b8115930..a8188153e 100644 --- a/Tests/test_file_xvthumb.py +++ b/Tests/test_file_xvthumb.py @@ -1,39 +1,39 @@ -from __future__ import annotations - -import pytest - -from PIL import Image, XVThumbImagePlugin - -from .helper import assert_image_similar, hopper - -TEST_FILE = "Tests/images/hopper.p7" - - -def test_open() -> None: - # Act - with Image.open(TEST_FILE) as im: - # Assert - assert im.format == "XVThumb" - - # Create a Hopper image with a similar XV palette - im_hopper = hopper().quantize(palette=im) - assert_image_similar(im, im_hopper, 9) - - -def test_unexpected_eof() -> None: - # Test unexpected EOF reading XV thumbnail file - # Arrange - bad_file = "Tests/images/hopper_bad.p7" - - # Act / Assert - with pytest.raises(SyntaxError): - XVThumbImagePlugin.XVThumbImageFile(bad_file) - - -def test_invalid_file() -> None: - # Arrange - invalid_file = "Tests/images/flower.jpg" - - # Act / Assert - with pytest.raises(SyntaxError): - XVThumbImagePlugin.XVThumbImageFile(invalid_file) +from __future__ import annotations + +import pytest + +from PIL import Image, XVThumbImagePlugin + +from .helper import assert_image_similar, hopper + +TEST_FILE = "Tests/images/hopper.p7" + + +def test_open() -> None: + # Act + with Image.open(TEST_FILE) as im: + # Assert + assert im.format == "XVThumb" + + # Create a Hopper image with a similar XV palette + im_hopper = hopper().quantize(palette=im) + assert_image_similar(im, im_hopper, 9) + + +def test_unexpected_eof() -> None: + # Test unexpected EOF reading XV thumbnail file + # Arrange + bad_file = "Tests/images/hopper_bad.p7" + + # Act / Assert + with pytest.raises(SyntaxError): + XVThumbImagePlugin.XVThumbImageFile(bad_file) + + +def test_invalid_file() -> None: + # Arrange + invalid_file = "Tests/images/flower.jpg" + + # Act / Assert + with pytest.raises(SyntaxError): + XVThumbImagePlugin.XVThumbImageFile(invalid_file) diff --git a/Tests/test_font_bdf.py b/Tests/test_font_bdf.py index 136070f9e..e47226825 100644 --- a/Tests/test_font_bdf.py +++ b/Tests/test_font_bdf.py @@ -1,21 +1,21 @@ -from __future__ import annotations - -import pytest - -from PIL import BdfFontFile, FontFile - -filename = "Tests/images/courB08.bdf" - - -def test_sanity() -> None: - with open(filename, "rb") as test_file: - font = BdfFontFile.BdfFontFile(test_file) - - assert isinstance(font, FontFile.FontFile) - assert len([_f for _f in font.glyph if _f]) == 190 - - -def test_invalid_file() -> None: - with open("Tests/images/flower.jpg", "rb") as fp: - with pytest.raises(SyntaxError): - BdfFontFile.BdfFontFile(fp) +from __future__ import annotations + +import pytest + +from PIL import BdfFontFile, FontFile + +filename = "Tests/images/courB08.bdf" + + +def test_sanity() -> None: + with open(filename, "rb") as test_file: + font = BdfFontFile.BdfFontFile(test_file) + + assert isinstance(font, FontFile.FontFile) + assert len([_f for _f in font.glyph if _f]) == 190 + + +def test_invalid_file() -> None: + with open("Tests/images/flower.jpg", "rb") as fp: + with pytest.raises(SyntaxError): + BdfFontFile.BdfFontFile(fp) diff --git a/Tests/test_font_crash.py b/Tests/test_font_crash.py index b82340ef7..3764b32c8 100644 --- a/Tests/test_font_crash.py +++ b/Tests/test_font_crash.py @@ -1,24 +1,24 @@ -from __future__ import annotations - -import pytest - -from PIL import Image, ImageDraw, ImageFont - -from .helper import skip_unless_feature - - -class TestFontCrash: - def _fuzz_font(self, font: ImageFont.FreeTypeFont) -> None: - # from fuzzers.fuzz_font - font.getbbox("ABC") - font.getmask("test text") - with Image.new(mode="RGBA", size=(200, 200)) as im: - draw = ImageDraw.Draw(im) - draw.multiline_textbbox((10, 10), "ABC\nAaaa", font, stroke_width=2) - draw.text((10, 10), "Test Text", font=font, fill="#000") - - @skip_unless_feature("freetype2") - def test_segfault(self) -> None: - with pytest.raises(OSError): - font = ImageFont.truetype("Tests/fonts/fuzz_font-5203009437302784") - self._fuzz_font(font) +from __future__ import annotations + +import pytest + +from PIL import Image, ImageDraw, ImageFont + +from .helper import skip_unless_feature + + +class TestFontCrash: + def _fuzz_font(self, font: ImageFont.FreeTypeFont) -> None: + # from fuzzers.fuzz_font + font.getbbox("ABC") + font.getmask("test text") + with Image.new(mode="RGBA", size=(200, 200)) as im: + draw = ImageDraw.Draw(im) + draw.multiline_textbbox((10, 10), "ABC\nAaaa", font, stroke_width=2) + draw.text((10, 10), "Test Text", font=font, fill="#000") + + @skip_unless_feature("freetype2") + def test_segfault(self) -> None: + with pytest.raises(OSError): + font = ImageFont.truetype("Tests/fonts/fuzz_font-5203009437302784") + self._fuzz_font(font) diff --git a/Tests/test_font_leaks.py b/Tests/test_font_leaks.py index 3fb92a62e..05d503894 100644 --- a/Tests/test_font_leaks.py +++ b/Tests/test_font_leaks.py @@ -1,43 +1,43 @@ -from __future__ import annotations - -from PIL import Image, ImageDraw, ImageFont, _util - -from .helper import PillowLeakTestCase, features, skip_unless_feature - -original_core = ImageFont.core - - -class TestTTypeFontLeak(PillowLeakTestCase): - # fails at iteration 3 in main - iterations = 10 - mem_limit = 4096 # k - - def _test_font(self, font: ImageFont.FreeTypeFont | ImageFont.ImageFont) -> None: - im = Image.new("RGB", (255, 255), "white") - draw = ImageDraw.ImageDraw(im) - self._test_leak( - lambda: draw.text( - (0, 0), "some text " * 1024, font=font, fill="black" # ~10k - ) - ) - - @skip_unless_feature("freetype2") - def test_leak(self) -> None: - ttype = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 20) - self._test_font(ttype) - - -class TestDefaultFontLeak(TestTTypeFontLeak): - # fails at iteration 37 in main - iterations = 100 - mem_limit = 1024 # k - - def test_leak(self) -> None: - if features.check_module("freetype2"): - ImageFont.core = _util.DeferredError(ImportError) - try: - default_font = ImageFont.load_default() - finally: - ImageFont.core = original_core - - self._test_font(default_font) +from __future__ import annotations + +from PIL import Image, ImageDraw, ImageFont, _util + +from .helper import PillowLeakTestCase, features, skip_unless_feature + +original_core = ImageFont.core + + +class TestTTypeFontLeak(PillowLeakTestCase): + # fails at iteration 3 in main + iterations = 10 + mem_limit = 4096 # k + + def _test_font(self, font: ImageFont.FreeTypeFont | ImageFont.ImageFont) -> None: + im = Image.new("RGB", (255, 255), "white") + draw = ImageDraw.ImageDraw(im) + self._test_leak( + lambda: draw.text( + (0, 0), "some text " * 1024, font=font, fill="black" # ~10k + ) + ) + + @skip_unless_feature("freetype2") + def test_leak(self) -> None: + ttype = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 20) + self._test_font(ttype) + + +class TestDefaultFontLeak(TestTTypeFontLeak): + # fails at iteration 37 in main + iterations = 100 + mem_limit = 1024 # k + + def test_leak(self) -> None: + if features.check_module("freetype2"): + ImageFont.core = _util.DeferredError(ImportError) + try: + default_font = ImageFont.load_default() + finally: + ImageFont.core = original_core + + self._test_font(default_font) diff --git a/Tests/test_font_pcf.py b/Tests/test_font_pcf.py index 997809e46..ce78fcb53 100644 --- a/Tests/test_font_pcf.py +++ b/Tests/test_font_pcf.py @@ -1,109 +1,109 @@ -from __future__ import annotations - -import os -from pathlib import Path - -import pytest - -from PIL import FontFile, Image, ImageDraw, ImageFont, PcfFontFile - -from .helper import ( - assert_image_equal_tofile, - assert_image_similar_tofile, - skip_unless_feature, -) - -fontname = "Tests/fonts/10x20-ISO8859-1.pcf" - -message = "hello, world" - - -pytestmark = skip_unless_feature("zlib") - - -def save_font(request: pytest.FixtureRequest, tmp_path: Path) -> str: - with open(fontname, "rb") as test_file: - font = PcfFontFile.PcfFontFile(test_file) - assert isinstance(font, FontFile.FontFile) - # check the number of characters in the font - assert len([_f for _f in font.glyph if _f]) == 223 - - tempname = str(tmp_path / "temp.pil") - - def delete_tempfile() -> None: - try: - os.remove(tempname[:-4] + ".pbm") - except OSError: - pass # report? - - request.addfinalizer(delete_tempfile) - font.save(tempname) - - with Image.open(tempname.replace(".pil", ".pbm")) as loaded: - assert_image_equal_tofile(loaded, "Tests/fonts/10x20.pbm") - - with open(tempname, "rb") as f_loaded: - with open("Tests/fonts/10x20.pil", "rb") as f_target: - assert f_loaded.read() == f_target.read() - return tempname - - -def test_sanity(request: pytest.FixtureRequest, tmp_path: Path) -> None: - save_font(request, tmp_path) - - -def test_less_than_256_characters() -> None: - with open("Tests/fonts/10x20-ISO8859-1-fewer-characters.pcf", "rb") as test_file: - font = PcfFontFile.PcfFontFile(test_file) - assert isinstance(font, FontFile.FontFile) - # check the number of characters in the font - assert len([_f for _f in font.glyph if _f]) == 127 - - -def test_invalid_file() -> None: - with open("Tests/images/flower.jpg", "rb") as fp: - with pytest.raises(SyntaxError): - PcfFontFile.PcfFontFile(fp) - - -def test_draw(request: pytest.FixtureRequest, tmp_path: Path) -> None: - tempname = save_font(request, tmp_path) - font = ImageFont.load(tempname) - im = Image.new("L", (130, 30), "white") - draw = ImageDraw.Draw(im) - draw.text((0, 0), message, "black", font=font) - assert_image_similar_tofile(im, "Tests/images/test_draw_pbm_target.png", 0) - - -def test_textsize(request: pytest.FixtureRequest, tmp_path: Path) -> None: - tempname = save_font(request, tmp_path) - font = ImageFont.load(tempname) - for i in range(255): - (ox, oy, dx, dy) = font.getbbox(chr(i)) - assert ox == 0 - assert oy == 0 - assert dy == 20 - assert dx in (0, 10) - assert font.getlength(chr(i)) == dx - for i in range(len(message)): - msg = message[: i + 1] - assert font.getlength(msg) == len(msg) * 10 - assert font.getbbox(msg) == (0, 0, len(msg) * 10, 20) - - -def _test_high_characters( - request: pytest.FixtureRequest, tmp_path: Path, message: str | bytes -) -> None: - tempname = save_font(request, tmp_path) - font = ImageFont.load(tempname) - im = Image.new("L", (750, 30), "white") - draw = ImageDraw.Draw(im) - draw.text((0, 0), message, "black", font=font) - assert_image_similar_tofile(im, "Tests/images/high_ascii_chars.png", 0) - - -def test_high_characters(request: pytest.FixtureRequest, tmp_path: Path) -> None: - message = "".join(chr(i + 1) for i in range(140, 232)) - _test_high_characters(request, tmp_path, message) - # accept bytes instances. - _test_high_characters(request, tmp_path, message.encode("latin1")) +from __future__ import annotations + +import os +from pathlib import Path + +import pytest + +from PIL import FontFile, Image, ImageDraw, ImageFont, PcfFontFile + +from .helper import ( + assert_image_equal_tofile, + assert_image_similar_tofile, + skip_unless_feature, +) + +fontname = "Tests/fonts/10x20-ISO8859-1.pcf" + +message = "hello, world" + + +pytestmark = skip_unless_feature("zlib") + + +def save_font(request: pytest.FixtureRequest, tmp_path: Path) -> str: + with open(fontname, "rb") as test_file: + font = PcfFontFile.PcfFontFile(test_file) + assert isinstance(font, FontFile.FontFile) + # check the number of characters in the font + assert len([_f for _f in font.glyph if _f]) == 223 + + tempname = str(tmp_path / "temp.pil") + + def delete_tempfile() -> None: + try: + os.remove(tempname[:-4] + ".pbm") + except OSError: + pass # report? + + request.addfinalizer(delete_tempfile) + font.save(tempname) + + with Image.open(tempname.replace(".pil", ".pbm")) as loaded: + assert_image_equal_tofile(loaded, "Tests/fonts/10x20.pbm") + + with open(tempname, "rb") as f_loaded: + with open("Tests/fonts/10x20.pil", "rb") as f_target: + assert f_loaded.read() == f_target.read() + return tempname + + +def test_sanity(request: pytest.FixtureRequest, tmp_path: Path) -> None: + save_font(request, tmp_path) + + +def test_less_than_256_characters() -> None: + with open("Tests/fonts/10x20-ISO8859-1-fewer-characters.pcf", "rb") as test_file: + font = PcfFontFile.PcfFontFile(test_file) + assert isinstance(font, FontFile.FontFile) + # check the number of characters in the font + assert len([_f for _f in font.glyph if _f]) == 127 + + +def test_invalid_file() -> None: + with open("Tests/images/flower.jpg", "rb") as fp: + with pytest.raises(SyntaxError): + PcfFontFile.PcfFontFile(fp) + + +def test_draw(request: pytest.FixtureRequest, tmp_path: Path) -> None: + tempname = save_font(request, tmp_path) + font = ImageFont.load(tempname) + im = Image.new("L", (130, 30), "white") + draw = ImageDraw.Draw(im) + draw.text((0, 0), message, "black", font=font) + assert_image_similar_tofile(im, "Tests/images/test_draw_pbm_target.png", 0) + + +def test_textsize(request: pytest.FixtureRequest, tmp_path: Path) -> None: + tempname = save_font(request, tmp_path) + font = ImageFont.load(tempname) + for i in range(255): + (ox, oy, dx, dy) = font.getbbox(chr(i)) + assert ox == 0 + assert oy == 0 + assert dy == 20 + assert dx in (0, 10) + assert font.getlength(chr(i)) == dx + for i in range(len(message)): + msg = message[: i + 1] + assert font.getlength(msg) == len(msg) * 10 + assert font.getbbox(msg) == (0, 0, len(msg) * 10, 20) + + +def _test_high_characters( + request: pytest.FixtureRequest, tmp_path: Path, message: str | bytes +) -> None: + tempname = save_font(request, tmp_path) + font = ImageFont.load(tempname) + im = Image.new("L", (750, 30), "white") + draw = ImageDraw.Draw(im) + draw.text((0, 0), message, "black", font=font) + assert_image_similar_tofile(im, "Tests/images/high_ascii_chars.png", 0) + + +def test_high_characters(request: pytest.FixtureRequest, tmp_path: Path) -> None: + message = "".join(chr(i + 1) for i in range(140, 232)) + _test_high_characters(request, tmp_path, message) + # accept bytes instances. + _test_high_characters(request, tmp_path, message.encode("latin1")) diff --git a/Tests/test_font_pcf_charsets.py b/Tests/test_font_pcf_charsets.py index 895458d9d..26812ea0a 100644 --- a/Tests/test_font_pcf_charsets.py +++ b/Tests/test_font_pcf_charsets.py @@ -1,108 +1,108 @@ -from __future__ import annotations - -import os -from pathlib import Path -from typing import TypedDict - -import pytest - -from PIL import FontFile, Image, ImageDraw, ImageFont, PcfFontFile - -from .helper import ( - assert_image_equal_tofile, - assert_image_similar_tofile, - skip_unless_feature, -) - -fontname = "Tests/fonts/ter-x20b.pcf" - - -class Charset(TypedDict): - glyph_count: int - message: str - image1: str - - -charsets: dict[str, Charset] = { - "iso8859-1": { - "glyph_count": 223, - "message": "hello, world", - "image1": "Tests/images/test_draw_pbm_ter_en_target.png", - }, - "iso8859-2": { - "glyph_count": 223, - "message": "witaj świecie", - "image1": "Tests/images/test_draw_pbm_ter_pl_target.png", - }, - "cp1250": { - "glyph_count": 250, - "message": "witaj świecie", - "image1": "Tests/images/test_draw_pbm_ter_pl_target.png", - }, -} - - -pytestmark = skip_unless_feature("zlib") - - -def save_font(request: pytest.FixtureRequest, tmp_path: Path, encoding: str) -> str: - with open(fontname, "rb") as test_file: - font = PcfFontFile.PcfFontFile(test_file, encoding) - assert isinstance(font, FontFile.FontFile) - # check the number of characters in the font - assert len([_f for _f in font.glyph if _f]) == charsets[encoding]["glyph_count"] - - tempname = str(tmp_path / "temp.pil") - - def delete_tempfile() -> None: - try: - os.remove(tempname[:-4] + ".pbm") - except OSError: - pass # report? - - request.addfinalizer(delete_tempfile) - font.save(tempname) - - with Image.open(tempname.replace(".pil", ".pbm")) as loaded: - assert_image_equal_tofile(loaded, f"Tests/fonts/ter-x20b-{encoding}.pbm") - - with open(tempname, "rb") as f_loaded: - with open(f"Tests/fonts/ter-x20b-{encoding}.pil", "rb") as f_target: - assert f_loaded.read() == f_target.read() - return tempname - - -@pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250")) -def test_sanity(request: pytest.FixtureRequest, tmp_path: Path, encoding: str) -> None: - save_font(request, tmp_path, encoding) - - -@pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250")) -def test_draw(request: pytest.FixtureRequest, tmp_path: Path, encoding: str) -> None: - tempname = save_font(request, tmp_path, encoding) - font = ImageFont.load(tempname) - im = Image.new("L", (150, 30), "white") - draw = ImageDraw.Draw(im) - message = charsets[encoding]["message"].encode(encoding) - draw.text((0, 0), message, "black", font=font) - assert_image_similar_tofile(im, charsets[encoding]["image1"], 0) - - -@pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250")) -def test_textsize( - request: pytest.FixtureRequest, tmp_path: Path, encoding: str -) -> None: - tempname = save_font(request, tmp_path, encoding) - font = ImageFont.load(tempname) - for i in range(255): - (ox, oy, dx, dy) = font.getbbox(bytearray([i])) - assert ox == 0 - assert oy == 0 - assert dy == 20 - assert dx in (0, 10) - assert font.getlength(bytearray([i])) == dx - message = charsets[encoding]["message"].encode(encoding) - for i in range(len(message)): - msg = message[: i + 1] - assert font.getlength(msg) == len(msg) * 10 - assert font.getbbox(msg) == (0, 0, len(msg) * 10, 20) +from __future__ import annotations + +import os +from pathlib import Path +from typing import TypedDict + +import pytest + +from PIL import FontFile, Image, ImageDraw, ImageFont, PcfFontFile + +from .helper import ( + assert_image_equal_tofile, + assert_image_similar_tofile, + skip_unless_feature, +) + +fontname = "Tests/fonts/ter-x20b.pcf" + + +class Charset(TypedDict): + glyph_count: int + message: str + image1: str + + +charsets: dict[str, Charset] = { + "iso8859-1": { + "glyph_count": 223, + "message": "hello, world", + "image1": "Tests/images/test_draw_pbm_ter_en_target.png", + }, + "iso8859-2": { + "glyph_count": 223, + "message": "witaj świecie", + "image1": "Tests/images/test_draw_pbm_ter_pl_target.png", + }, + "cp1250": { + "glyph_count": 250, + "message": "witaj świecie", + "image1": "Tests/images/test_draw_pbm_ter_pl_target.png", + }, +} + + +pytestmark = skip_unless_feature("zlib") + + +def save_font(request: pytest.FixtureRequest, tmp_path: Path, encoding: str) -> str: + with open(fontname, "rb") as test_file: + font = PcfFontFile.PcfFontFile(test_file, encoding) + assert isinstance(font, FontFile.FontFile) + # check the number of characters in the font + assert len([_f for _f in font.glyph if _f]) == charsets[encoding]["glyph_count"] + + tempname = str(tmp_path / "temp.pil") + + def delete_tempfile() -> None: + try: + os.remove(tempname[:-4] + ".pbm") + except OSError: + pass # report? + + request.addfinalizer(delete_tempfile) + font.save(tempname) + + with Image.open(tempname.replace(".pil", ".pbm")) as loaded: + assert_image_equal_tofile(loaded, f"Tests/fonts/ter-x20b-{encoding}.pbm") + + with open(tempname, "rb") as f_loaded: + with open(f"Tests/fonts/ter-x20b-{encoding}.pil", "rb") as f_target: + assert f_loaded.read() == f_target.read() + return tempname + + +@pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250")) +def test_sanity(request: pytest.FixtureRequest, tmp_path: Path, encoding: str) -> None: + save_font(request, tmp_path, encoding) + + +@pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250")) +def test_draw(request: pytest.FixtureRequest, tmp_path: Path, encoding: str) -> None: + tempname = save_font(request, tmp_path, encoding) + font = ImageFont.load(tempname) + im = Image.new("L", (150, 30), "white") + draw = ImageDraw.Draw(im) + message = charsets[encoding]["message"].encode(encoding) + draw.text((0, 0), message, "black", font=font) + assert_image_similar_tofile(im, charsets[encoding]["image1"], 0) + + +@pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250")) +def test_textsize( + request: pytest.FixtureRequest, tmp_path: Path, encoding: str +) -> None: + tempname = save_font(request, tmp_path, encoding) + font = ImageFont.load(tempname) + for i in range(255): + (ox, oy, dx, dy) = font.getbbox(bytearray([i])) + assert ox == 0 + assert oy == 0 + assert dy == 20 + assert dx in (0, 10) + assert font.getlength(bytearray([i])) == dx + message = charsets[encoding]["message"].encode(encoding) + for i in range(len(message)): + msg = message[: i + 1] + assert font.getlength(msg) == len(msg) * 10 + assert font.getbbox(msg) == (0, 0, len(msg) * 10, 20) diff --git a/Tests/test_fontfile.py b/Tests/test_fontfile.py index 206499a04..2d87fc25d 100644 --- a/Tests/test_fontfile.py +++ b/Tests/test_fontfile.py @@ -1,15 +1,15 @@ -from __future__ import annotations - -from pathlib import Path - -import pytest - -from PIL import FontFile - - -def test_save(tmp_path: Path) -> None: - tempname = str(tmp_path / "temp.pil") - - font = FontFile.FontFile() - with pytest.raises(ValueError): - font.save(tempname) +from __future__ import annotations + +from pathlib import Path + +import pytest + +from PIL import FontFile + + +def test_save(tmp_path: Path) -> None: + tempname = str(tmp_path / "temp.pil") + + font = FontFile.FontFile() + with pytest.raises(ValueError): + font.save(tempname) diff --git a/Tests/test_format_hsv.py b/Tests/test_format_hsv.py index c07024a2c..65950c2c2 100644 --- a/Tests/test_format_hsv.py +++ b/Tests/test_format_hsv.py @@ -1,154 +1,154 @@ -from __future__ import annotations - -import colorsys -import itertools -from typing import Callable - -from PIL import Image - -from .helper import assert_image_similar, hopper - - -def int_to_float(i: int) -> float: - return i / 255 - - -def tuple_to_ints(tp: tuple[float, float, float]) -> tuple[int, int, int]: - x, y, z = tp - return int(x * 255.0), int(y * 255.0), int(z * 255.0) - - -def test_sanity() -> None: - Image.new("HSV", (100, 100)) - - -def wedge() -> Image.Image: - w = Image._wedge() - w90 = w.rotate(90) - - (px, h) = w.size - - r = Image.new("L", (px * 3, h)) - g = r.copy() - b = r.copy() - - r.paste(w, (0, 0)) - r.paste(w90, (px, 0)) - - g.paste(w90, (0, 0)) - g.paste(w, (2 * px, 0)) - - b.paste(w, (px, 0)) - b.paste(w90, (2 * px, 0)) - - img = Image.merge("RGB", (r, g, b)) - - return img - - -def to_xxx_colorsys( - im: Image.Image, - func: Callable[[float, float, float], tuple[float, float, float]], - mode: str, -) -> Image.Image: - # convert the hard way using the library colorsys routines. - - (r, g, b) = im.split() - - conv_func = int_to_float - - converted = [ - tuple_to_ints(func(conv_func(_r), conv_func(_g), conv_func(_b))) - for (_r, _g, _b) in itertools.zip_longest(r.tobytes(), g.tobytes(), b.tobytes()) - ] - - new_bytes = b"".join( - bytes(chr(h) + chr(s) + chr(v), "latin-1") for (h, s, v) in converted - ) - - hsv = Image.frombytes(mode, r.size, new_bytes) - - return hsv - - -def to_hsv_colorsys(im: Image.Image) -> Image.Image: - return to_xxx_colorsys(im, colorsys.rgb_to_hsv, "HSV") - - -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) - im = src.convert("HSV") - comparable = to_hsv_colorsys(src) - - assert_image_similar( - im.getchannel(0), comparable.getchannel(0), 1, "Hue conversion is wrong" - ) - assert_image_similar( - im.getchannel(1), - comparable.getchannel(1), - 1, - "Saturation conversion is wrong", - ) - assert_image_similar( - im.getchannel(2), comparable.getchannel(2), 1, "Value conversion is wrong" - ) - - comparable = src - im = im.convert("RGB") - - assert_image_similar( - im.getchannel(0), comparable.getchannel(0), 3, "R conversion is wrong" - ) - assert_image_similar( - im.getchannel(1), comparable.getchannel(1), 3, "G conversion is wrong" - ) - assert_image_similar( - im.getchannel(2), comparable.getchannel(2), 3, "B conversion is wrong" - ) - - -def test_convert() -> None: - im = hopper("RGB").convert("HSV") - comparable = to_hsv_colorsys(hopper("RGB")) - - assert_image_similar( - im.getchannel(0), comparable.getchannel(0), 1, "Hue conversion is wrong" - ) - assert_image_similar( - im.getchannel(1), - comparable.getchannel(1), - 1, - "Saturation conversion is wrong", - ) - assert_image_similar( - im.getchannel(2), comparable.getchannel(2), 1, "Value conversion is wrong" - ) - - -def test_hsv_to_rgb() -> None: - comparable = to_hsv_colorsys(hopper("RGB")) - converted = comparable.convert("RGB") - comparable = to_rgb_colorsys(comparable) - - assert_image_similar( - converted.getchannel(0), - comparable.getchannel(0), - 3, - "R conversion is wrong", - ) - assert_image_similar( - converted.getchannel(1), - comparable.getchannel(1), - 3, - "G conversion is wrong", - ) - assert_image_similar( - converted.getchannel(2), - comparable.getchannel(2), - 3, - "B conversion is wrong", - ) +from __future__ import annotations + +import colorsys +import itertools +from typing import Callable + +from PIL import Image + +from .helper import assert_image_similar, hopper + + +def int_to_float(i: int) -> float: + return i / 255 + + +def tuple_to_ints(tp: tuple[float, float, float]) -> tuple[int, int, int]: + x, y, z = tp + return int(x * 255.0), int(y * 255.0), int(z * 255.0) + + +def test_sanity() -> None: + Image.new("HSV", (100, 100)) + + +def wedge() -> Image.Image: + w = Image._wedge() + w90 = w.rotate(90) + + (px, h) = w.size + + r = Image.new("L", (px * 3, h)) + g = r.copy() + b = r.copy() + + r.paste(w, (0, 0)) + r.paste(w90, (px, 0)) + + g.paste(w90, (0, 0)) + g.paste(w, (2 * px, 0)) + + b.paste(w, (px, 0)) + b.paste(w90, (2 * px, 0)) + + img = Image.merge("RGB", (r, g, b)) + + return img + + +def to_xxx_colorsys( + im: Image.Image, + func: Callable[[float, float, float], tuple[float, float, float]], + mode: str, +) -> Image.Image: + # convert the hard way using the library colorsys routines. + + (r, g, b) = im.split() + + conv_func = int_to_float + + converted = [ + tuple_to_ints(func(conv_func(_r), conv_func(_g), conv_func(_b))) + for (_r, _g, _b) in itertools.zip_longest(r.tobytes(), g.tobytes(), b.tobytes()) + ] + + new_bytes = b"".join( + bytes(chr(h) + chr(s) + chr(v), "latin-1") for (h, s, v) in converted + ) + + hsv = Image.frombytes(mode, r.size, new_bytes) + + return hsv + + +def to_hsv_colorsys(im: Image.Image) -> Image.Image: + return to_xxx_colorsys(im, colorsys.rgb_to_hsv, "HSV") + + +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) + im = src.convert("HSV") + comparable = to_hsv_colorsys(src) + + assert_image_similar( + im.getchannel(0), comparable.getchannel(0), 1, "Hue conversion is wrong" + ) + assert_image_similar( + im.getchannel(1), + comparable.getchannel(1), + 1, + "Saturation conversion is wrong", + ) + assert_image_similar( + im.getchannel(2), comparable.getchannel(2), 1, "Value conversion is wrong" + ) + + comparable = src + im = im.convert("RGB") + + assert_image_similar( + im.getchannel(0), comparable.getchannel(0), 3, "R conversion is wrong" + ) + assert_image_similar( + im.getchannel(1), comparable.getchannel(1), 3, "G conversion is wrong" + ) + assert_image_similar( + im.getchannel(2), comparable.getchannel(2), 3, "B conversion is wrong" + ) + + +def test_convert() -> None: + im = hopper("RGB").convert("HSV") + comparable = to_hsv_colorsys(hopper("RGB")) + + assert_image_similar( + im.getchannel(0), comparable.getchannel(0), 1, "Hue conversion is wrong" + ) + assert_image_similar( + im.getchannel(1), + comparable.getchannel(1), + 1, + "Saturation conversion is wrong", + ) + assert_image_similar( + im.getchannel(2), comparable.getchannel(2), 1, "Value conversion is wrong" + ) + + +def test_hsv_to_rgb() -> None: + comparable = to_hsv_colorsys(hopper("RGB")) + converted = comparable.convert("RGB") + comparable = to_rgb_colorsys(comparable) + + assert_image_similar( + converted.getchannel(0), + comparable.getchannel(0), + 3, + "R conversion is wrong", + ) + assert_image_similar( + converted.getchannel(1), + comparable.getchannel(1), + 3, + "G conversion is wrong", + ) + assert_image_similar( + converted.getchannel(2), + comparable.getchannel(2), + 3, + "B conversion is wrong", + ) diff --git a/Tests/test_format_lab.py b/Tests/test_format_lab.py index 4fcc37e88..c3d4ecc4d 100644 --- a/Tests/test_format_lab.py +++ b/Tests/test_format_lab.py @@ -1,40 +1,40 @@ -from __future__ import annotations - -from PIL import Image - - -def test_white() -> None: - with Image.open("Tests/images/lab.tif") as i: - i.load() - - assert i.mode == "LAB" - - assert i.getbands() == ("L", "A", "B") - - k = i.getpixel((0, 0)) - - L = i.getdata(0) - a = i.getdata(1) - b = i.getdata(2) - - assert k == (255, 128, 128) - - assert list(L) == [255] * 100 - assert list(a) == [128] * 100 - assert list(b) == [128] * 100 - - -def test_green() -> None: - # l= 50 (/100), a = -100 (-128 .. 128) b=0 in PS - # == RGB: 0, 152, 117 - with Image.open("Tests/images/lab-green.tif") as i: - k = i.getpixel((0, 0)) - assert k == (128, 28, 128) - - -def test_red() -> None: - # l= 50 (/100), a = 100 (-128 .. 128) b=0 in PS - # == RGB: 255, 0, 124 - with Image.open("Tests/images/lab-red.tif") as i: - k = i.getpixel((0, 0)) - assert k == (128, 228, 128) +from __future__ import annotations + +from PIL import Image + + +def test_white() -> None: + with Image.open("Tests/images/lab.tif") as i: + i.load() + + assert i.mode == "LAB" + + assert i.getbands() == ("L", "A", "B") + + k = i.getpixel((0, 0)) + + L = i.getdata(0) + a = i.getdata(1) + b = i.getdata(2) + + assert k == (255, 128, 128) + + assert list(L) == [255] * 100 + assert list(a) == [128] * 100 + assert list(b) == [128] * 100 + + +def test_green() -> None: + # l= 50 (/100), a = -100 (-128 .. 128) b=0 in PS + # == RGB: 0, 152, 117 + with Image.open("Tests/images/lab-green.tif") as i: + k = i.getpixel((0, 0)) + assert k == (128, 28, 128) + + +def test_red() -> None: + # l= 50 (/100), a = 100 (-128 .. 128) b=0 in PS + # == RGB: 255, 0, 124 + with Image.open("Tests/images/lab-red.tif") as i: + k = i.getpixel((0, 0)) + assert k == (128, 228, 128) diff --git a/Tests/test_image.py b/Tests/test_image.py index d6a739c79..c1065d463 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -1,1089 +1,1092 @@ -from __future__ import annotations - -import io -import logging -import os -import shutil -import sys -import tempfile -import warnings -from pathlib import Path -from typing import IO - -import pytest - -from PIL import ( - ExifTags, - Image, - ImageDraw, - ImageFile, - ImagePalette, - UnidentifiedImageError, - features, -) - -from .helper import ( - assert_image_equal, - assert_image_equal_tofile, - assert_image_similar, - assert_image_similar_tofile, - assert_not_all_same, - hopper, - is_big_endian, - is_win32, - mark_if_feature_version, - skip_unless_feature, -) - - -# Deprecation helper -def helper_image_new(mode: str, size: tuple[int, int]) -> Image.Image: - if mode.startswith("BGR;"): - with pytest.warns(DeprecationWarning): - return Image.new(mode, size) - else: - return Image.new(mode, size) - - -class TestImage: - @pytest.mark.parametrize("mode", Image.MODES + ["BGR;15", "BGR;16", "BGR;24"]) - def test_image_modes_success(self, mode: str) -> None: - helper_image_new(mode, (1, 1)) - - @pytest.mark.parametrize("mode", ("", "bad", "very very long")) - def test_image_modes_fail(self, mode: str) -> None: - with pytest.raises(ValueError) as e: - 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: - class Pretty: - def text(self, text: str) -> None: - self.pretty_output = text - - im = Image.new("L", (100, 100)) - - p = Pretty() - im._repr_pretty_(p, None) - assert p.pretty_output == "" - - def test_open_formats(self) -> None: - PNGFILE = "Tests/images/hopper.png" - JPGFILE = "Tests/images/hopper.jpg" - - with pytest.raises(TypeError): - with Image.open(PNGFILE, formats=123): # type: ignore[arg-type] - pass - - format_list: list[list[str] | tuple[str, ...]] = [ - ["JPEG"], - ("JPEG",), - ["jpeg"], - ["Jpeg"], - ["jPeG"], - ["JpEg"], - ] - for formats in format_list: - with pytest.raises(UnidentifiedImageError): - with Image.open(PNGFILE, formats=formats): - pass - - with Image.open(JPGFILE, formats=formats) as im: - assert im.mode == "RGB" - assert im.size == (128, 128) - - for file in [PNGFILE, JPGFILE]: - with Image.open(file, formats=None) as im: - assert im.mode == "RGB" - assert im.size == (128, 128) - - def test_width_height(self) -> None: - im = Image.new("RGB", (1, 2)) - assert im.width == 1 - assert im.height == 2 - - with pytest.raises(AttributeError): - im.size = (3, 4) # type: ignore[misc] - - def test_set_mode(self) -> None: - im = Image.new("RGB", (1, 1)) - - with pytest.raises(AttributeError): - im.mode = "P" # type: ignore[misc] - - def test_invalid_image(self) -> None: - im = io.BytesIO(b"") - with pytest.raises(UnidentifiedImageError): - with Image.open(im): - pass - - def test_bad_mode(self) -> None: - with pytest.raises(ValueError): - with Image.open("filename", "bad mode"): # type: ignore[arg-type] - pass - - def test_stringio(self) -> None: - with pytest.raises(ValueError): - with Image.open(io.StringIO()): - pass - - def test_pathlib(self, tmp_path: Path) -> None: - with Image.open(Path("Tests/images/multipage-mmap.tiff")) as im: - assert im.mode == "P" - assert im.size == (10, 10) - - with Image.open(Path("Tests/images/hopper.jpg")) as im: - assert im.mode == "RGB" - assert im.size == (128, 128) - - for ext in (".jpg", ".jp2"): - if ext == ".jp2" and not features.check_codec("jpg_2000"): - pytest.skip("jpg_2000 not available") - temp_file = str(tmp_path / ("temp." + ext)) - if os.path.exists(temp_file): - os.remove(temp_file) - im.save(Path(temp_file)) - - def test_fp_name(self, tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.jpg") - - class FP: - name: str - - def write(self, b: bytes) -> None: - pass - - fp = FP() - fp.name = temp_file - - im = hopper() - im.save(fp) - - def test_tempfile(self) -> None: - # see #1460, pathlib support breaks tempfile.TemporaryFile on py27 - # Will error out on save on 3.0.0 - im = hopper() - with tempfile.TemporaryFile() as fp: - im.save(fp, "JPEG") - fp.seek(0) - with Image.open(fp) as reloaded: - 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) - - def test_internals(self) -> None: - im = Image.new("L", (100, 100)) - im.readonly = 1 - im._copy() - assert not im.readonly - - im.readonly = 1 - im.paste(0, (0, 0, 100, 100)) - assert not im.readonly - - @pytest.mark.skipif(is_win32(), reason="Test requires opening tempfile twice") - @pytest.mark.skipif( - sys.platform == "cygwin", - reason="Test requires opening an mmaped file for writing", - ) - def test_readonly_save(self, tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.bmp") - shutil.copy("Tests/images/rgb32bf-rgba.bmp", temp_file) - - with Image.open(temp_file) as im: - assert im.readonly - im.save(temp_file) - - def test_dump(self, tmp_path: Path) -> None: - im = Image.new("L", (10, 10)) - im._dump(str(tmp_path / "temp_L.ppm")) - - im = Image.new("RGB", (10, 10)) - im._dump(str(tmp_path / "temp_RGB.ppm")) - - im = Image.new("HSV", (10, 10)) - with pytest.raises(ValueError): - im._dump(str(tmp_path / "temp_HSV.ppm")) - - def test_comparison_with_other_type(self) -> None: - # Arrange - item = Image.new("RGB", (25, 25), "#000") - num = 12 - - # Act/Assert - # Shouldn't cause AttributeError (#774) - assert item is not None - assert item != num - - def test_expand_x(self) -> None: - # Arrange - im = hopper() - orig_size = im.size - xmargin = 5 - - # Act - im = im._expand(xmargin) - - # Assert - assert im.size[0] == orig_size[0] + 2 * xmargin - assert im.size[1] == orig_size[1] + 2 * xmargin - - def test_expand_xy(self) -> None: - # Arrange - im = hopper() - orig_size = im.size - xmargin = 5 - ymargin = 3 - - # Act - im = im._expand(xmargin, ymargin) - - # Assert - assert im.size[0] == orig_size[0] + 2 * xmargin - assert im.size[1] == orig_size[1] + 2 * ymargin - - def test_getbands(self) -> None: - # Assert - assert hopper("RGB").getbands() == ("R", "G", "B") - assert hopper("YCbCr").getbands() == ("Y", "Cb", "Cr") - - def test_getchannel_wrong_params(self) -> None: - im = hopper() - - with pytest.raises(ValueError): - im.getchannel(-1) - with pytest.raises(ValueError): - im.getchannel(3) - with pytest.raises(ValueError): - im.getchannel("Z") - with pytest.raises(ValueError): - im.getchannel("1") - - def test_getchannel(self) -> None: - im = hopper("YCbCr") - Y, Cb, Cr = im.split() - - assert_image_equal(Y, im.getchannel(0)) - assert_image_equal(Y, im.getchannel("Y")) - assert_image_equal(Cb, im.getchannel(1)) - assert_image_equal(Cb, im.getchannel("Cb")) - assert_image_equal(Cr, im.getchannel(2)) - assert_image_equal(Cr, im.getchannel("Cr")) - - def test_getbbox(self) -> None: - # Arrange - im = hopper() - - # Act - bbox = im.getbbox() - - # Assert - assert bbox == (0, 0, 128, 128) - - def test_ne(self) -> None: - # Arrange - im1 = Image.new("RGB", (25, 25), "black") - im2 = Image.new("RGB", (25, 25), "white") - - # Act / Assert - assert im1 != im2 - - def test_alpha_composite(self) -> None: - # https://stackoverflow.com/questions/3374878 - # Arrange - expected_colors = sorted( - [ - (1122, (128, 127, 0, 255)), - (1089, (0, 255, 0, 255)), - (3300, (255, 0, 0, 255)), - (1156, (170, 85, 0, 192)), - (1122, (0, 255, 0, 128)), - (1122, (255, 0, 0, 128)), - (1089, (0, 255, 0, 0)), - ] - ) - - dst = Image.new("RGBA", size=(100, 100), color=(0, 255, 0, 255)) - draw = ImageDraw.Draw(dst) - draw.rectangle((0, 33, 100, 66), fill=(0, 255, 0, 128)) - draw.rectangle((0, 67, 100, 100), fill=(0, 255, 0, 0)) - src = Image.new("RGBA", size=(100, 100), color=(255, 0, 0, 255)) - draw = ImageDraw.Draw(src) - draw.rectangle((33, 0, 66, 100), fill=(255, 0, 0, 128)) - draw.rectangle((67, 0, 100, 100), fill=(255, 0, 0, 0)) - - # Act - img = Image.alpha_composite(dst, src) - - # Assert - img_colors = sorted(img.getcolors()) - assert img_colors == expected_colors - - def test_alpha_inplace(self) -> None: - src = Image.new("RGBA", (128, 128), "blue") - - over = Image.new("RGBA", (128, 128), "red") - mask = hopper("L") - over.putalpha(mask) - - target = Image.alpha_composite(src, over) - - # basic - full = src.copy() - full.alpha_composite(over) - assert_image_equal(full, target) - - # with offset down to right - offset = src.copy() - offset.alpha_composite(over, (64, 64)) - assert_image_equal(offset.crop((64, 64, 127, 127)), target.crop((0, 0, 63, 63))) - assert offset.size == (128, 128) - - # with negative offset - offset = src.copy() - offset.alpha_composite(over, (-64, -64)) - assert_image_equal(offset.crop((0, 0, 63, 63)), target.crop((64, 64, 127, 127))) - assert offset.size == (128, 128) - - # offset and crop - box = src.copy() - box.alpha_composite(over, (64, 64), (0, 0, 32, 32)) - assert_image_equal(box.crop((64, 64, 96, 96)), target.crop((0, 0, 32, 32))) - assert_image_equal(box.crop((96, 96, 128, 128)), src.crop((0, 0, 32, 32))) - assert box.size == (128, 128) - - # source point - source = src.copy() - source.alpha_composite(over, (32, 32), (32, 32, 96, 96)) - - assert_image_equal(source.crop((32, 32, 96, 96)), target.crop((32, 32, 96, 96))) - assert source.size == (128, 128) - - # errors - with pytest.raises(ValueError): - source.alpha_composite(over, "invalid source") - with pytest.raises(ValueError): - source.alpha_composite(over, (0, 0), "invalid destination") - with pytest.raises(ValueError): - source.alpha_composite(over, 0) - with pytest.raises(ValueError): - source.alpha_composite(over, (0, 0), 0) - with pytest.raises(ValueError): - source.alpha_composite(over, (0, 0), (0, -1)) - - def test_register_open_duplicates(self) -> None: - # Arrange - factory, accept = Image.OPEN["JPEG"] - id_length = len(Image.ID) - - # Act - Image.register_open("JPEG", factory, accept) - - # Assert - assert len(Image.ID) == id_length - - def test_registered_extensions_uninitialized(self) -> None: - # Arrange - Image._initialized = 0 - - # Act - Image.registered_extensions() - - # Assert - assert Image._initialized == 2 - - def test_registered_extensions(self) -> None: - # Arrange - # Open an image to trigger plugin registration - with Image.open("Tests/images/rgb.jpg"): - pass - - # Act - extensions = Image.registered_extensions() - - # Assert - assert extensions - for ext in [".cur", ".icns", ".tif", ".tiff"]: - assert ext in extensions - - def test_effect_mandelbrot(self) -> None: - # Arrange - size = (512, 512) - extent = (-3, -2.5, 2, 2.5) - quality = 100 - - # Act - im = Image.effect_mandelbrot(size, extent, quality) - - # Assert - assert im.size == (512, 512) - assert_image_equal_tofile(im, "Tests/images/effect_mandelbrot.png") - - def test_effect_mandelbrot_bad_arguments(self) -> None: - # Arrange - size = (512, 512) - # Get coordinates the wrong way round: - extent = (+3, +2.5, -2, -2.5) - # Quality < 2: - quality = 1 - - # Act/Assert - with pytest.raises(ValueError): - Image.effect_mandelbrot(size, extent, quality) - - def test_effect_noise(self) -> None: - # Arrange - size = (100, 100) - sigma = 128 - - # Act - im = Image.effect_noise(size, sigma) - - # Assert - assert im.size == (100, 100) - assert im.mode == "L" - p0 = im.getpixel((0, 0)) - p1 = im.getpixel((0, 1)) - p2 = im.getpixel((0, 2)) - p3 = im.getpixel((0, 3)) - p4 = im.getpixel((0, 4)) - assert_not_all_same([p0, p1, p2, p3, p4]) - - def test_effect_spread(self) -> None: - # Arrange - im = hopper() - distance = 10 - - # Act - im2 = im.effect_spread(distance) - - # Assert - assert im.size == (128, 128) - assert_image_similar_tofile(im2, "Tests/images/effect_spread.png", 110) - - def test_effect_spread_zero(self) -> None: - # Arrange - im = hopper() - distance = 0 - - # Act - im2 = im.effect_spread(distance) - - # Assert - assert_image_equal(im, im2) - - def test_check_size(self) -> None: - # Checking that the _check_size function throws value errors when we want it to - with pytest.raises(ValueError): - # not a tuple - Image.new("RGB", 0) # type: ignore[arg-type] - with pytest.raises(ValueError): - # tuple too short - Image.new("RGB", (0,)) # type: ignore[arg-type] - with pytest.raises(ValueError): - Image.new("RGB", (-1, -1)) # w,h < 0 - - # this should pass with 0 sized images, #2259 - im = Image.new("L", (0, 0)) - assert im.size == (0, 0) - - im = Image.new("L", (0, 100)) - assert im.size == (0, 100) - - im = Image.new("L", (100, 0)) - assert im.size == (100, 0) - - assert Image.new("RGB", (1, 1)) - # Should pass lists too - 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" - ) - @pytest.mark.parametrize("size", ((0, 100000000), (100000000, 0))) - def test_empty_image(self, size: tuple[int, int]) -> None: - Image.new("RGB", size) - - def test_storage_neg(self) -> None: - # Storage.c accepted negative values for xsize, ysize. Was - # test_neg_ppm, but the core function for that has been - # removed Calling directly into core to test the error in - # Storage.c, rather than the size check above - - with pytest.raises(ValueError): - Image.core.fill("RGB", (2, -2), (0, 0, 0)) - - def test_one_item_tuple(self) -> None: - for mode in ("I", "F", "L"): - im = Image.new(mode, (100, 100), (5,)) - px = im.load() - assert px[0, 0] == 5 - - def test_linear_gradient_wrong_mode(self) -> None: - # Arrange - wrong_mode = "RGB" - - # Act / Assert - with pytest.raises(ValueError): - Image.linear_gradient(wrong_mode) - - @pytest.mark.parametrize("mode", ("L", "P", "I", "F")) - def test_linear_gradient(self, mode: str) -> None: - # Arrange - target_file = "Tests/images/linear_gradient.png" - - # Act - im = Image.linear_gradient(mode) - - # Assert - assert im.size == (256, 256) - assert im.mode == mode - assert im.getpixel((0, 0)) == 0 - assert im.getpixel((255, 255)) == 255 - with Image.open(target_file) as target: - target = target.convert(mode) - assert_image_equal(im, target) - - def test_radial_gradient_wrong_mode(self) -> None: - # Arrange - wrong_mode = "RGB" - - # Act / Assert - with pytest.raises(ValueError): - Image.radial_gradient(wrong_mode) - - @pytest.mark.parametrize("mode", ("L", "P", "I", "F")) - def test_radial_gradient(self, mode: str) -> None: - # Arrange - target_file = "Tests/images/radial_gradient.png" - - # Act - im = Image.radial_gradient(mode) - - # Assert - assert im.size == (256, 256) - assert im.mode == mode - assert im.getpixel((0, 0)) == 255 - assert im.getpixel((128, 128)) == 0 - with Image.open(target_file) as target: - target = target.convert(mode) - assert_image_equal(im, target) - - def test_register_extensions(self) -> None: - test_format = "a" - exts = ["b", "c"] - for ext in exts: - Image.register_extension(test_format, ext) - ext_individual = Image.EXTENSION.copy() - for ext in exts: - del Image.EXTENSION[ext] - - Image.register_extensions(test_format, exts) - ext_multiple = Image.EXTENSION.copy() - for ext in exts: - del Image.EXTENSION[ext] - - assert ext_individual == ext_multiple - - def test_remap_palette(self) -> None: - # Test identity transform - with Image.open("Tests/images/hopper.gif") as im: - assert_image_equal(im, im.remap_palette(list(range(256)))) - - # Test identity transform with an RGBA palette - im = Image.new("P", (256, 1)) - for x in range(256): - im.putpixel((x, 0), x) - im.putpalette(list(range(256)) * 4, "RGBA") - im_remapped = im.remap_palette(list(range(256))) - assert_image_equal(im, im_remapped) - assert im.palette.palette == im_remapped.palette.palette - - # Test illegal image mode - with hopper() as im: - with pytest.raises(ValueError): - im.remap_palette(None) - - def test_remap_palette_transparency(self) -> None: - im = Image.new("P", (1, 2), (0, 0, 0)) - im.putpixel((0, 1), (255, 0, 0)) - im.info["transparency"] = 0 - - im_remapped = im.remap_palette([1, 0]) - assert im_remapped.info["transparency"] == 1 - assert len(im_remapped.getpalette()) == 6 - - # Test unused transparency - im.info["transparency"] = 2 - - im_remapped = im.remap_palette([1, 0]) - assert "transparency" not in im_remapped.info - - def test__new(self) -> None: - im = hopper("RGB") - im_p = hopper("P") - - blank_p = Image.new("P", (10, 10)) - blank_pa = Image.new("PA", (10, 10)) - blank_p.palette = None - blank_pa.palette = None - - def _make_new( - base_image: Image.Image, - image: Image.Image, - palette_result: ImagePalette.ImagePalette | None = None, - ) -> None: - new_image = base_image._new(image.im) - assert new_image.mode == image.mode - assert new_image.size == image.size - assert new_image.info == base_image.info - if palette_result is not None: - assert new_image.palette.tobytes() == palette_result.tobytes() - else: - assert new_image.palette is None - - _make_new(im, im_p, ImagePalette.ImagePalette(list(range(256)) * 3)) - _make_new(im_p, im, None) - _make_new(im, blank_p, ImagePalette.ImagePalette()) - _make_new(im, blank_pa, ImagePalette.ImagePalette()) - - @pytest.mark.parametrize( - "mode, color", - ( - ("RGB", "#DDEEFF"), - ("RGB", (221, 238, 255)), - ("RGBA", (221, 238, 255, 255)), - ), - ) - def test_p_from_rgb_rgba(self, mode: str, color: str | tuple[int, ...]) -> None: - im = Image.new("P", (100, 100), color) - expected = Image.new(mode, (100, 100), color) - assert_image_equal(im.convert(mode), expected) - - def test_no_resource_warning_on_save(self, tmp_path: Path) -> None: - # https://github.com/python-pillow/Pillow/issues/835 - # Arrange - test_file = "Tests/images/hopper.png" - temp_file = str(tmp_path / "temp.jpg") - - # Act/Assert - with Image.open(test_file) as im: - with warnings.catch_warnings(): - im.save(temp_file) - - def test_no_new_file_on_error(self, tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.jpg") - - im = Image.new("RGB", (0, 0)) - with pytest.raises(ValueError): - im.save(temp_file) - - assert not os.path.exists(temp_file) - - def test_load_on_nonexclusive_multiframe(self) -> None: - with open("Tests/images/frozenpond.mpo", "rb") as fp: - - def act(fp: IO[bytes]) -> None: - im = Image.open(fp) - im.load() - - act(fp) - - with Image.open(fp) as im: - im.load() - - assert not fp.closed - - def test_empty_exif(self) -> None: - with Image.open("Tests/images/exif.png") as im: - exif = im.getexif() - assert dict(exif) - - # Test that exif data is cleared after another load - exif.load(None) - assert not dict(exif) - - # Test loading just the EXIF header - exif.load(b"Exif\x00\x00") - assert not dict(exif) - - @mark_if_feature_version( - pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" - ) - def test_exif_jpeg(self, tmp_path: Path) -> None: - with Image.open("Tests/images/exif-72dpi-int.jpg") as im: # Little endian - exif = im.getexif() - assert 258 not in exif - assert 274 in exif - assert 282 in exif - assert exif[296] == 2 - assert exif[11] == "gThumb 3.0.1" - - out = str(tmp_path / "temp.jpg") - exif[258] = 8 - del exif[274] - del exif[282] - exif[296] = 455 - exif[11] = "Pillow test" - im.save(out, exif=exif) - with Image.open(out) as reloaded: - reloaded_exif = reloaded.getexif() - assert reloaded_exif[258] == 8 - assert 274 not in reloaded_exif - assert 282 not in reloaded_exif - assert reloaded_exif[296] == 455 - assert reloaded_exif[11] == "Pillow test" - - with Image.open("Tests/images/no-dpi-in-exif.jpg") as im: # Big endian - exif = im.getexif() - assert 258 not in exif - assert 306 in exif - assert exif[274] == 1 - assert exif[305] == "Adobe Photoshop CC 2017 (Macintosh)" - - out = str(tmp_path / "temp.jpg") - exif[258] = 8 - del exif[306] - exif[274] = 455 - exif[305] = "Pillow test" - im.save(out, exif=exif) - with Image.open(out) as reloaded: - reloaded_exif = reloaded.getexif() - assert reloaded_exif[258] == 8 - assert 306 not in reloaded_exif - assert reloaded_exif[274] == 455 - assert reloaded_exif[305] == "Pillow test" - - @skip_unless_feature("webp") - @skip_unless_feature("webp_anim") - def test_exif_webp(self, tmp_path: Path) -> None: - with Image.open("Tests/images/hopper.webp") as im: - exif = im.getexif() - assert exif == {} - - out = str(tmp_path / "temp.webp") - exif[258] = 8 - exif[40963] = 455 - exif[305] = "Pillow test" - - def check_exif() -> None: - with Image.open(out) as reloaded: - reloaded_exif = reloaded.getexif() - assert reloaded_exif[258] == 8 - assert reloaded_exif[40963] == 455 - assert reloaded_exif[305] == "Pillow test" - - im.save(out, exif=exif) - check_exif() - im.save(out, exif=exif, save_all=True) - check_exif() - - def test_exif_png(self, tmp_path: Path) -> None: - with Image.open("Tests/images/exif.png") as im: - exif = im.getexif() - assert exif == {274: 1} - - out = str(tmp_path / "temp.png") - exif[258] = 8 - del exif[274] - exif[40963] = 455 - exif[305] = "Pillow test" - im.save(out, exif=exif) - - with Image.open(out) as reloaded: - reloaded_exif = reloaded.getexif() - assert reloaded_exif == {258: 8, 40963: 455, 305: "Pillow test"} - - def test_exif_interop(self) -> None: - with Image.open("Tests/images/flower.jpg") as im: - exif = im.getexif() - assert exif.get_ifd(0xA005) == { - 1: "R98", - 2: b"0100", - 4097: 2272, - 4098: 1704, - } - - reloaded_exif = Image.Exif() - reloaded_exif.load(exif.tobytes()) - assert reloaded_exif.get_ifd(0xA005) == exif.get_ifd(0xA005) - - def test_exif_ifd1(self) -> None: - with Image.open("Tests/images/flower.jpg") as im: - exif = im.getexif() - assert exif.get_ifd(ExifTags.IFD.IFD1) == { - 513: 2036, - 514: 5448, - 259: 6, - 296: 2, - 282: 180.0, - 283: 180.0, - } - - def test_exif_ifd(self) -> None: - with Image.open("Tests/images/flower.jpg") as im: - exif = im.getexif() - del exif.get_ifd(0x8769)[0xA005] - - reloaded_exif = Image.Exif() - reloaded_exif.load(exif.tobytes()) - assert reloaded_exif.get_ifd(0x8769) == exif.get_ifd(0x8769) - - def test_exif_load_from_fp(self) -> None: - with Image.open("Tests/images/flower.jpg") as im: - data = im.info["exif"] - if data.startswith(b"Exif\x00\x00"): - data = data[6:] - fp = io.BytesIO(data) - - exif = Image.Exif() - exif.load_from_fp(fp) - assert exif == { - 271: "Canon", - 272: "Canon PowerShot S40", - 274: 1, - 282: 180.0, - 283: 180.0, - 296: 2, - 306: "2003:12:14 12:01:44", - 531: 1, - 34665: 196, - } - - def test_exif_hide_offsets(self) -> None: - with Image.open("Tests/images/flower.jpg") as im: - exif = im.getexif() - - # Check offsets are present initially - assert 0x8769 in exif - for tag in (0xA005, 0x927C): - assert tag in exif.get_ifd(0x8769) - assert exif.get_ifd(0xA005) - loaded_exif = exif - - with Image.open("Tests/images/flower.jpg") as im: - new_exif = im.getexif() - - for exif in (loaded_exif, new_exif): - exif.hide_offsets() - - # Assert they are hidden afterwards, - # but that the IFDs are still available - assert 0x8769 not in exif - assert exif.get_ifd(0x8769) - for tag in (0xA005, 0x927C): - assert tag not in exif.get_ifd(0x8769) - assert exif.get_ifd(0xA005) - - @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0))) - def test_zero_tobytes(self, size: tuple[int, int]) -> None: - im = Image.new("RGB", size) - assert im.tobytes() == b"" - - @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0))) - def test_zero_frombytes(self, size: tuple[int, int]) -> None: - Image.frombytes("RGB", size, b"") - - im = Image.new("RGB", size) - im.frombytes(b"") - - def test_has_transparency_data(self) -> None: - for mode in ("1", "L", "P", "RGB"): - im = Image.new(mode, (1, 1)) - assert not im.has_transparency_data - - for mode in ("LA", "La", "PA", "RGBA", "RGBa"): - im = Image.new(mode, (1, 1)) - assert im.has_transparency_data - - # P mode with "transparency" info - with Image.open("Tests/images/first_frame_transparency.gif") as im: - assert "transparency" in im.info - assert im.has_transparency_data - - # RGB mode with "transparency" info - with Image.open("Tests/images/rgb_trns.png") as im: - assert "transparency" in im.info - assert im.has_transparency_data - - # P mode with RGBA palette - im = Image.new("RGBA", (1, 1)).convert("P") - assert im.mode == "P" - assert im.palette.mode == "RGBA" - assert im.has_transparency_data - - def test_apply_transparency(self) -> None: - im = Image.new("P", (1, 1)) - im.putpalette((0, 0, 0, 1, 1, 1)) - assert im.palette.colors == {(0, 0, 0): 0, (1, 1, 1): 1} - - # Test that no transformation is applied without transparency - im.apply_transparency() - assert im.palette.colors == {(0, 0, 0): 0, (1, 1, 1): 1} - - # Test that a transparency index is applied - im.info["transparency"] = 0 - im.apply_transparency() - assert "transparency" not in im.info - assert im.palette.colors == {(0, 0, 0, 0): 0, (1, 1, 1, 255): 1} - - # Test that existing transparency is kept - im = Image.new("P", (1, 1)) - im.putpalette((0, 0, 0, 255, 1, 1, 1, 128), "RGBA") - im.info["transparency"] = 0 - im.apply_transparency() - assert im.palette.colors == {(0, 0, 0, 0): 0, (1, 1, 1, 128): 1} - - # Test that transparency bytes are applied - with Image.open("Tests/images/pil123p.png") as im: - assert isinstance(im.info["transparency"], bytes) - assert im.palette.colors[(27, 35, 6)] == 24 - im.apply_transparency() - assert im.palette.colors[(27, 35, 6, 214)] == 24 - - def test_constants(self) -> None: - for enum in ( - Image.Transpose, - Image.Transform, - Image.Resampling, - Image.Dither, - Image.Palette, - Image.Quantize, - ): - for name in enum.__members__: - assert getattr(Image, name) == enum[name] - - @pytest.mark.parametrize( - "path", - [ - "fli_overrun.bin", - "sgi_overrun.bin", - "sgi_overrun_expandrow.bin", - "sgi_overrun_expandrow2.bin", - "pcx_overrun.bin", - "pcx_overrun2.bin", - "ossfuzz-4836216264589312.pcx", - "01r_00.pcx", - ], - ) - def test_overrun(self, path: str) -> None: - """For overrun completeness, test as: - valgrind pytest -qq Tests/test_image.py::TestImage::test_overrun | grep decode.c - """ - with Image.open(os.path.join("Tests/images", path)) as im: - try: - im.load() - pytest.fail() - except OSError as e: - buffer_overrun = str(e) == "buffer overrun when reading image file" - truncated = "image file is truncated" in str(e) - - assert buffer_overrun or truncated - - def test_fli_overrun2(self) -> None: - with Image.open("Tests/images/fli_overrun2.bin") as im: - try: - im.seek(1) - pytest.fail() - except OSError as e: - assert str(e) == "buffer overrun when reading image file" - - def test_exit_fp(self) -> None: - with Image.new("L", (1, 1)) as im: - pass - assert not hasattr(im, "fp") - - def test_close_graceful(self, caplog: pytest.LogCaptureFixture) -> None: - with Image.open("Tests/images/hopper.jpg") as im: - copy = im.copy() - with caplog.at_level(logging.DEBUG): - im.close() - copy.close() - assert len(caplog.records) == 0 - assert im.fp is None - - -class TestImageBytes: - @pytest.mark.parametrize("mode", Image.MODES + ["BGR;15", "BGR;16", "BGR;24"]) - def test_roundtrip_bytes_constructor(self, mode: str) -> None: - im = hopper(mode) - source_bytes = im.tobytes() - - if mode.startswith("BGR;"): - with pytest.warns(DeprecationWarning): - reloaded = Image.frombytes(mode, im.size, source_bytes) - else: - reloaded = Image.frombytes(mode, im.size, source_bytes) - assert reloaded.tobytes() == source_bytes - - @pytest.mark.parametrize("mode", Image.MODES + ["BGR;15", "BGR;16", "BGR;24"]) - def test_roundtrip_bytes_method(self, mode: str) -> None: - im = hopper(mode) - source_bytes = im.tobytes() - - reloaded = helper_image_new(mode, im.size) - reloaded.frombytes(source_bytes) - assert reloaded.tobytes() == source_bytes - - @pytest.mark.parametrize("mode", Image.MODES + ["BGR;15", "BGR;16", "BGR;24"]) - def test_getdata_putdata(self, mode: str) -> None: - if is_big_endian() and mode == "BGR;15": - pytest.xfail("Known failure of BGR;15 on big-endian") - im = hopper(mode) - reloaded = helper_image_new(mode, im.size) - reloaded.putdata(im.getdata()) - assert_image_equal(im, reloaded) - - -class MockEncoder(ImageFile.PyEncoder): - pass - - -class TestRegistry: - def test_encode_registry(self) -> None: - Image.register_encoder("MOCK", MockEncoder) - assert "MOCK" in Image.ENCODERS - - enc = Image._getencoder("RGB", "MOCK", ("args",), extra=("extra",)) - - assert isinstance(enc, MockEncoder) - assert enc.mode == "RGB" - assert enc.args == ("args", "extra") - - def test_encode_registry_fail(self) -> None: - with pytest.raises(OSError): - Image._getencoder("RGB", "DoesNotExist", ("args",), extra=("extra",)) +from __future__ import annotations + +import io +import logging +import os +import shutil +import sys +import tempfile +import warnings +from pathlib import Path +from typing import IO + +import pytest + +from PIL import ( + ExifTags, + Image, + ImageDraw, + ImageFile, + ImagePalette, + UnidentifiedImageError, + features, +) + +from .helper import ( + assert_image_equal, + assert_image_equal_tofile, + assert_image_similar, + assert_image_similar_tofile, + assert_not_all_same, + hopper, + is_big_endian, + is_win32, + mark_if_feature_version, + skip_unless_feature, +) + + +# Deprecation helper +def helper_image_new(mode: str, size: tuple[int, int]) -> Image.Image: + if mode.startswith("BGR;"): + with pytest.warns(DeprecationWarning): + return Image.new(mode, size) + else: + return Image.new(mode, size) + + +class TestImage: + @pytest.mark.parametrize("mode", Image.MODES + ["BGR;15", "BGR;16", "BGR;24"]) + def test_image_modes_success(self, mode: str) -> None: + helper_image_new(mode, (1, 1)) + + @pytest.mark.parametrize("mode", ("", "bad", "very very long")) + def test_image_modes_fail(self, mode: str) -> None: + with pytest.raises(ValueError) as e: + 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: + class Pretty: + def text(self, text: str) -> None: + self.pretty_output = text + + im = Image.new("L", (100, 100)) + + p = Pretty() + im._repr_pretty_(p, None) + assert p.pretty_output == "" + + def test_open_formats(self) -> None: + PNGFILE = "Tests/images/hopper.png" + JPGFILE = "Tests/images/hopper.jpg" + + with pytest.raises(TypeError): + with Image.open(PNGFILE, formats=123): # type: ignore[arg-type] + pass + + format_list: list[list[str] | tuple[str, ...]] = [ + ["JPEG"], + ("JPEG",), + ["jpeg"], + ["Jpeg"], + ["jPeG"], + ["JpEg"], + ] + for formats in format_list: + with pytest.raises(UnidentifiedImageError): + with Image.open(PNGFILE, formats=formats): + pass + + with Image.open(JPGFILE, formats=formats) as im: + assert im.mode == "RGB" + assert im.size == (128, 128) + + for file in [PNGFILE, JPGFILE]: + with Image.open(file, formats=None) as im: + assert im.mode == "RGB" + assert im.size == (128, 128) + + def test_width_height(self) -> None: + im = Image.new("RGB", (1, 2)) + assert im.width == 1 + assert im.height == 2 + + with pytest.raises(AttributeError): + im.size = (3, 4) # type: ignore[misc] + + def test_set_mode(self) -> None: + im = Image.new("RGB", (1, 1)) + + with pytest.raises(AttributeError): + im.mode = "P" # type: ignore[misc] + + def test_invalid_image(self) -> None: + im = io.BytesIO(b"") + with pytest.raises(UnidentifiedImageError): + with Image.open(im): + pass + + def test_bad_mode(self) -> None: + with pytest.raises(ValueError): + with Image.open("filename", "bad mode"): # type: ignore[arg-type] + pass + + def test_stringio(self) -> None: + with pytest.raises(ValueError): + with Image.open(io.StringIO()): + pass + + def test_pathlib(self, tmp_path: Path) -> None: + with Image.open(Path("Tests/images/multipage-mmap.tiff")) as im: + assert im.mode == "P" + assert im.size == (10, 10) + + with Image.open(Path("Tests/images/hopper.jpg")) as im: + assert im.mode == "RGB" + assert im.size == (128, 128) + + for ext in (".jpg", ".jp2"): + if ext == ".jp2" and not features.check_codec("jpg_2000"): + pytest.skip("jpg_2000 not available") + temp_file = str(tmp_path / ("temp." + ext)) + if os.path.exists(temp_file): + os.remove(temp_file) + im.save(Path(temp_file)) + + def test_fp_name(self, tmp_path: Path) -> None: + temp_file = str(tmp_path / "temp.jpg") + + class FP: + name: str + + def write(self, b: bytes) -> None: + pass + + fp = FP() + fp.name = temp_file + + im = hopper() + im.save(fp) + + def test_tempfile(self) -> None: + # see #1460, pathlib support breaks tempfile.TemporaryFile on py27 + # Will error out on save on 3.0.0 + im = hopper() + with tempfile.TemporaryFile() as fp: + im.save(fp, "JPEG") + fp.seek(0) + with Image.open(fp) as reloaded: + 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) + + def test_internals(self) -> None: + im = Image.new("L", (100, 100)) + im.readonly = 1 + im._copy() + assert not im.readonly + + im.readonly = 1 + im.paste(0, (0, 0, 100, 100)) + assert not im.readonly + + @pytest.mark.skipif(is_win32(), reason="Test requires opening tempfile twice") + @pytest.mark.skipif( + sys.platform == "cygwin", + reason="Test requires opening an mmaped file for writing", + ) + def test_readonly_save(self, tmp_path: Path) -> None: + temp_file = str(tmp_path / "temp.bmp") + shutil.copy("Tests/images/rgb32bf-rgba.bmp", temp_file) + + with Image.open(temp_file) as im: + assert im.readonly + im.save(temp_file) + + def test_dump(self, tmp_path: Path) -> None: + im = Image.new("L", (10, 10)) + im._dump(str(tmp_path / "temp_L.ppm")) + + im = Image.new("RGB", (10, 10)) + im._dump(str(tmp_path / "temp_RGB.ppm")) + + im = Image.new("HSV", (10, 10)) + with pytest.raises(ValueError): + im._dump(str(tmp_path / "temp_HSV.ppm")) + + def test_comparison_with_other_type(self) -> None: + # Arrange + item = Image.new("RGB", (25, 25), "#000") + num = 12 + + # Act/Assert + # Shouldn't cause AttributeError (#774) + assert item is not None + assert item != num + + def test_expand_x(self) -> None: + # Arrange + im = hopper() + orig_size = im.size + xmargin = 5 + + # Act + im = im._expand(xmargin) + + # Assert + assert im.size[0] == orig_size[0] + 2 * xmargin + assert im.size[1] == orig_size[1] + 2 * xmargin + + def test_expand_xy(self) -> None: + # Arrange + im = hopper() + orig_size = im.size + xmargin = 5 + ymargin = 3 + + # Act + im = im._expand(xmargin, ymargin) + + # Assert + assert im.size[0] == orig_size[0] + 2 * xmargin + assert im.size[1] == orig_size[1] + 2 * ymargin + + def test_getbands(self) -> None: + # Assert + assert hopper("RGB").getbands() == ("R", "G", "B") + assert hopper("YCbCr").getbands() == ("Y", "Cb", "Cr") + + def test_getchannel_wrong_params(self) -> None: + im = hopper() + + with pytest.raises(ValueError): + im.getchannel(-1) + with pytest.raises(ValueError): + im.getchannel(3) + with pytest.raises(ValueError): + im.getchannel("Z") + with pytest.raises(ValueError): + im.getchannel("1") + + def test_getchannel(self) -> None: + im = hopper("YCbCr") + Y, Cb, Cr = im.split() + + assert_image_equal(Y, im.getchannel(0)) + assert_image_equal(Y, im.getchannel("Y")) + assert_image_equal(Cb, im.getchannel(1)) + assert_image_equal(Cb, im.getchannel("Cb")) + assert_image_equal(Cr, im.getchannel(2)) + assert_image_equal(Cr, im.getchannel("Cr")) + + def test_getbbox(self) -> None: + # Arrange + im = hopper() + + # Act + bbox = im.getbbox() + + # Assert + assert bbox == (0, 0, 128, 128) + + def test_ne(self) -> None: + # Arrange + im1 = Image.new("RGB", (25, 25), "black") + im2 = Image.new("RGB", (25, 25), "white") + + # Act / Assert + assert im1 != im2 + + def test_alpha_composite(self) -> None: + # https://stackoverflow.com/questions/3374878 + # Arrange + expected_colors = sorted( + [ + (1122, (128, 127, 0, 255)), + (1089, (0, 255, 0, 255)), + (3300, (255, 0, 0, 255)), + (1156, (170, 85, 0, 192)), + (1122, (0, 255, 0, 128)), + (1122, (255, 0, 0, 128)), + (1089, (0, 255, 0, 0)), + ] + ) + + dst = Image.new("RGBA", size=(100, 100), color=(0, 255, 0, 255)) + draw = ImageDraw.Draw(dst) + draw.rectangle((0, 33, 100, 66), fill=(0, 255, 0, 128)) + draw.rectangle((0, 67, 100, 100), fill=(0, 255, 0, 0)) + src = Image.new("RGBA", size=(100, 100), color=(255, 0, 0, 255)) + draw = ImageDraw.Draw(src) + draw.rectangle((33, 0, 66, 100), fill=(255, 0, 0, 128)) + draw.rectangle((67, 0, 100, 100), fill=(255, 0, 0, 0)) + + # Act + img = Image.alpha_composite(dst, src) + + # Assert + img_colors = sorted(img.getcolors()) + assert img_colors == expected_colors + + def test_alpha_inplace(self) -> None: + src = Image.new("RGBA", (128, 128), "blue") + + over = Image.new("RGBA", (128, 128), "red") + mask = hopper("L") + over.putalpha(mask) + + target = Image.alpha_composite(src, over) + + # basic + full = src.copy() + full.alpha_composite(over) + assert_image_equal(full, target) + + # with offset down to right + offset = src.copy() + offset.alpha_composite(over, (64, 64)) + assert_image_equal(offset.crop((64, 64, 127, 127)), target.crop((0, 0, 63, 63))) + assert offset.size == (128, 128) + + # with negative offset + offset = src.copy() + offset.alpha_composite(over, (-64, -64)) + assert_image_equal(offset.crop((0, 0, 63, 63)), target.crop((64, 64, 127, 127))) + assert offset.size == (128, 128) + + # offset and crop + box = src.copy() + box.alpha_composite(over, (64, 64), (0, 0, 32, 32)) + assert_image_equal(box.crop((64, 64, 96, 96)), target.crop((0, 0, 32, 32))) + assert_image_equal(box.crop((96, 96, 128, 128)), src.crop((0, 0, 32, 32))) + assert box.size == (128, 128) + + # source point + source = src.copy() + source.alpha_composite(over, (32, 32), (32, 32, 96, 96)) + + assert_image_equal(source.crop((32, 32, 96, 96)), target.crop((32, 32, 96, 96))) + assert source.size == (128, 128) + + # errors + with pytest.raises(ValueError): + source.alpha_composite(over, "invalid source") + with pytest.raises(ValueError): + source.alpha_composite(over, (0, 0), "invalid destination") + with pytest.raises(ValueError): + source.alpha_composite(over, 0) + with pytest.raises(ValueError): + source.alpha_composite(over, (0, 0), 0) + with pytest.raises(ValueError): + source.alpha_composite(over, (0, 0), (0, -1)) + + def test_register_open_duplicates(self) -> None: + # Arrange + factory, accept = Image.OPEN["JPEG"] + id_length = len(Image.ID) + + # Act + Image.register_open("JPEG", factory, accept) + + # Assert + assert len(Image.ID) == id_length + + def test_registered_extensions_uninitialized(self) -> None: + # Arrange + Image._initialized = 0 + + # Act + Image.registered_extensions() + + # Assert + assert Image._initialized == 2 + + def test_registered_extensions(self) -> None: + # Arrange + # Open an image to trigger plugin registration + with Image.open("Tests/images/rgb.jpg"): + pass + + # Act + extensions = Image.registered_extensions() + + # Assert + assert extensions + for ext in [".cur", ".icns", ".tif", ".tiff"]: + assert ext in extensions + + def test_effect_mandelbrot(self) -> None: + # Arrange + size = (512, 512) + extent = (-3, -2.5, 2, 2.5) + quality = 100 + + # Act + im = Image.effect_mandelbrot(size, extent, quality) + + # Assert + assert im.size == (512, 512) + assert_image_equal_tofile(im, "Tests/images/effect_mandelbrot.png") + + def test_effect_mandelbrot_bad_arguments(self) -> None: + # Arrange + size = (512, 512) + # Get coordinates the wrong way round: + extent = (+3, +2.5, -2, -2.5) + # Quality < 2: + quality = 1 + + # Act/Assert + with pytest.raises(ValueError): + Image.effect_mandelbrot(size, extent, quality) + + def test_effect_noise(self) -> None: + # Arrange + size = (100, 100) + sigma = 128 + + # Act + im = Image.effect_noise(size, sigma) + + # Assert + assert im.size == (100, 100) + assert im.mode == "L" + p0 = im.getpixel((0, 0)) + p1 = im.getpixel((0, 1)) + p2 = im.getpixel((0, 2)) + p3 = im.getpixel((0, 3)) + p4 = im.getpixel((0, 4)) + assert_not_all_same([p0, p1, p2, p3, p4]) + + def test_effect_spread(self) -> None: + # Arrange + im = hopper() + distance = 10 + + # Act + im2 = im.effect_spread(distance) + + # Assert + assert im.size == (128, 128) + assert_image_similar_tofile(im2, "Tests/images/effect_spread.png", 110) + + def test_effect_spread_zero(self) -> None: + # Arrange + im = hopper() + distance = 0 + + # Act + im2 = im.effect_spread(distance) + + # Assert + assert_image_equal(im, im2) + + def test_check_size(self) -> None: + # Checking that the _check_size function throws value errors when we want it to + with pytest.raises(ValueError): + # not a tuple + Image.new("RGB", 0) # type: ignore[arg-type] + with pytest.raises(ValueError): + # tuple too short + Image.new("RGB", (0,)) # type: ignore[arg-type] + with pytest.raises(ValueError): + Image.new("RGB", (-1, -1)) # w,h < 0 + + # this should pass with 0 sized images, #2259 + im = Image.new("L", (0, 0)) + assert im.size == (0, 0) + + im = Image.new("L", (0, 100)) + assert im.size == (0, 100) + + im = Image.new("L", (100, 0)) + assert im.size == (100, 0) + + assert Image.new("RGB", (1, 1)) + # Should pass lists too + 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" + ) + @pytest.mark.parametrize("size", ((0, 100000000), (100000000, 0))) + def test_empty_image(self, size: tuple[int, int]) -> None: + Image.new("RGB", size) + + def test_storage_neg(self) -> None: + # Storage.c accepted negative values for xsize, ysize. Was + # test_neg_ppm, but the core function for that has been + # removed Calling directly into core to test the error in + # Storage.c, rather than the size check above + + with pytest.raises(ValueError): + Image.core.fill("RGB", (2, -2), (0, 0, 0)) + + def test_one_item_tuple(self) -> None: + for mode in ("I", "F", "L"): + im = Image.new(mode, (100, 100), (5,)) + px = im.load() + assert px[0, 0] == 5 + + def test_linear_gradient_wrong_mode(self) -> None: + # Arrange + wrong_mode = "RGB" + + # Act / Assert + with pytest.raises(ValueError): + Image.linear_gradient(wrong_mode) + + @pytest.mark.parametrize("mode", ("L", "P", "I", "F")) + def test_linear_gradient(self, mode: str) -> None: + # Arrange + target_file = "Tests/images/linear_gradient.png" + + # Act + im = Image.linear_gradient(mode) + + # Assert + assert im.size == (256, 256) + assert im.mode == mode + assert im.getpixel((0, 0)) == 0 + assert im.getpixel((255, 255)) == 255 + with Image.open(target_file) as target: + target = target.convert(mode) + assert_image_equal(im, target) + + def test_radial_gradient_wrong_mode(self) -> None: + # Arrange + wrong_mode = "RGB" + + # Act / Assert + with pytest.raises(ValueError): + Image.radial_gradient(wrong_mode) + + @pytest.mark.parametrize("mode", ("L", "P", "I", "F")) + def test_radial_gradient(self, mode: str) -> None: + # Arrange + target_file = "Tests/images/radial_gradient.png" + + # Act + im = Image.radial_gradient(mode) + + # Assert + assert im.size == (256, 256) + assert im.mode == mode + assert im.getpixel((0, 0)) == 255 + assert im.getpixel((128, 128)) == 0 + with Image.open(target_file) as target: + target = target.convert(mode) + assert_image_equal(im, target) + + def test_register_extensions(self) -> None: + test_format = "a" + exts = ["b", "c"] + for ext in exts: + Image.register_extension(test_format, ext) + ext_individual = Image.EXTENSION.copy() + for ext in exts: + del Image.EXTENSION[ext] + + Image.register_extensions(test_format, exts) + ext_multiple = Image.EXTENSION.copy() + for ext in exts: + del Image.EXTENSION[ext] + + assert ext_individual == ext_multiple + + def test_remap_palette(self) -> None: + # Test identity transform + with Image.open("Tests/images/hopper.gif") as im: + assert_image_equal(im, im.remap_palette(list(range(256)))) + + # Test identity transform with an RGBA palette + im = Image.new("P", (256, 1)) + for x in range(256): + im.putpixel((x, 0), x) + im.putpalette(list(range(256)) * 4, "RGBA") + im_remapped = im.remap_palette(list(range(256))) + assert_image_equal(im, im_remapped) + assert im.palette.palette == im_remapped.palette.palette + + # Test illegal image mode + with hopper() as im: + with pytest.raises(ValueError): + im.remap_palette(None) + + def test_remap_palette_transparency(self) -> None: + im = Image.new("P", (1, 2), (0, 0, 0)) + im.putpixel((0, 1), (255, 0, 0)) + im.info["transparency"] = 0 + + im_remapped = im.remap_palette([1, 0]) + assert im_remapped.info["transparency"] == 1 + assert len(im_remapped.getpalette()) == 6 + + # Test unused transparency + im.info["transparency"] = 2 + + im_remapped = im.remap_palette([1, 0]) + assert "transparency" not in im_remapped.info + + def test__new(self) -> None: + im = hopper("RGB") + im_p = hopper("P") + + blank_p = Image.new("P", (10, 10)) + blank_pa = Image.new("PA", (10, 10)) + blank_p.palette = None + blank_pa.palette = None + + def _make_new( + base_image: Image.Image, + image: Image.Image, + palette_result: ImagePalette.ImagePalette | None = None, + ) -> None: + new_image = base_image._new(image.im) + assert new_image.mode == image.mode + assert new_image.size == image.size + assert new_image.info == base_image.info + if palette_result is not None: + assert new_image.palette.tobytes() == palette_result.tobytes() + else: + assert new_image.palette is None + + _make_new(im, im_p, ImagePalette.ImagePalette(list(range(256)) * 3)) + _make_new(im_p, im, None) + _make_new(im, blank_p, ImagePalette.ImagePalette()) + _make_new(im, blank_pa, ImagePalette.ImagePalette()) + + @pytest.mark.parametrize( + "mode, color", + ( + ("RGB", "#DDEEFF"), + ("RGB", (221, 238, 255)), + ("RGBA", (221, 238, 255, 255)), + ), + ) + def test_p_from_rgb_rgba(self, mode: str, color: str | tuple[int, ...]) -> None: + im = Image.new("P", (100, 100), color) + expected = Image.new(mode, (100, 100), color) + assert_image_equal(im.convert(mode), expected) + + def test_no_resource_warning_on_save(self, tmp_path: Path) -> None: + # https://github.com/python-pillow/Pillow/issues/835 + # Arrange + test_file = "Tests/images/hopper.png" + temp_file = str(tmp_path / "temp.jpg") + + # Act/Assert + with Image.open(test_file) as im: + with warnings.catch_warnings(): + im.save(temp_file) + + def test_no_new_file_on_error(self, tmp_path: Path) -> None: + temp_file = str(tmp_path / "temp.jpg") + + im = Image.new("RGB", (0, 0)) + with pytest.raises(ValueError): + im.save(temp_file) + + assert not os.path.exists(temp_file) + + def test_load_on_nonexclusive_multiframe(self) -> None: + with open("Tests/images/frozenpond.mpo", "rb") as fp: + + def act(fp: IO[bytes]) -> None: + im = Image.open(fp) + im.load() + + act(fp) + + with Image.open(fp) as im: + im.load() + + assert not fp.closed + + def test_empty_exif(self) -> None: + with Image.open("Tests/images/exif.png") as im: + exif = im.getexif() + assert dict(exif) + + # Test that exif data is cleared after another load + exif.load(None) + assert not dict(exif) + + # Test loading just the EXIF header + exif.load(b"Exif\x00\x00") + assert not dict(exif) + + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) + def test_exif_jpeg(self, tmp_path: Path) -> None: + with Image.open("Tests/images/exif-72dpi-int.jpg") as im: # Little endian + exif = im.getexif() + assert 258 not in exif + assert 274 in exif + assert 282 in exif + assert exif[296] == 2 + assert exif[11] == "gThumb 3.0.1" + + out = str(tmp_path / "temp.jpg") + exif[258] = 8 + del exif[274] + del exif[282] + exif[296] = 455 + exif[11] = "Pillow test" + im.save(out, exif=exif) + with Image.open(out) as reloaded: + reloaded_exif = reloaded.getexif() + assert reloaded_exif[258] == 8 + assert 274 not in reloaded_exif + assert 282 not in reloaded_exif + assert reloaded_exif[296] == 455 + assert reloaded_exif[11] == "Pillow test" + + with Image.open("Tests/images/no-dpi-in-exif.jpg") as im: # Big endian + exif = im.getexif() + assert 258 not in exif + assert 306 in exif + assert exif[274] == 1 + assert exif[305] == "Adobe Photoshop CC 2017 (Macintosh)" + + out = str(tmp_path / "temp.jpg") + exif[258] = 8 + del exif[306] + exif[274] = 455 + exif[305] = "Pillow test" + im.save(out, exif=exif) + with Image.open(out) as reloaded: + reloaded_exif = reloaded.getexif() + assert reloaded_exif[258] == 8 + assert 306 not in reloaded_exif + assert reloaded_exif[274] == 455 + assert reloaded_exif[305] == "Pillow test" + + @skip_unless_feature("webp") + @skip_unless_feature("webp_anim") + def test_exif_webp(self, tmp_path: Path) -> None: + with Image.open("Tests/images/hopper.webp") as im: + exif = im.getexif() + assert exif == {} + + out = str(tmp_path / "temp.webp") + exif[258] = 8 + exif[40963] = 455 + exif[305] = "Pillow test" + + def check_exif() -> None: + with Image.open(out) as reloaded: + reloaded_exif = reloaded.getexif() + assert reloaded_exif[258] == 8 + assert reloaded_exif[40963] == 455 + assert reloaded_exif[305] == "Pillow test" + + im.save(out, exif=exif) + check_exif() + im.save(out, exif=exif, save_all=True) + check_exif() + + def test_exif_png(self, tmp_path: Path) -> None: + with Image.open("Tests/images/exif.png") as im: + exif = im.getexif() + assert exif == {274: 1} + + out = str(tmp_path / "temp.png") + exif[258] = 8 + del exif[274] + exif[40963] = 455 + exif[305] = "Pillow test" + im.save(out, exif=exif) + + with Image.open(out) as reloaded: + reloaded_exif = reloaded.getexif() + assert reloaded_exif == {258: 8, 40963: 455, 305: "Pillow test"} + + def test_exif_interop(self) -> None: + with Image.open("Tests/images/flower.jpg") as im: + exif = im.getexif() + assert exif.get_ifd(0xA005) == { + 1: "R98", + 2: b"0100", + 4097: 2272, + 4098: 1704, + } + + reloaded_exif = Image.Exif() + reloaded_exif.load(exif.tobytes()) + assert reloaded_exif.get_ifd(0xA005) == exif.get_ifd(0xA005) + + def test_exif_ifd1(self) -> None: + with Image.open("Tests/images/flower.jpg") as im: + exif = im.getexif() + assert exif.get_ifd(ExifTags.IFD.IFD1) == { + 513: 2036, + 514: 5448, + 259: 6, + 296: 2, + 282: 180.0, + 283: 180.0, + } + + def test_exif_ifd(self) -> None: + with Image.open("Tests/images/flower.jpg") as im: + exif = im.getexif() + del exif.get_ifd(0x8769)[0xA005] + + reloaded_exif = Image.Exif() + reloaded_exif.load(exif.tobytes()) + assert reloaded_exif.get_ifd(0x8769) == exif.get_ifd(0x8769) + + def test_exif_load_from_fp(self) -> None: + with Image.open("Tests/images/flower.jpg") as im: + data = im.info["exif"] + if data.startswith(b"Exif\x00\x00"): + data = data[6:] + fp = io.BytesIO(data) + + exif = Image.Exif() + exif.load_from_fp(fp) + assert exif == { + 271: "Canon", + 272: "Canon PowerShot S40", + 274: 1, + 282: 180.0, + 283: 180.0, + 296: 2, + 306: "2003:12:14 12:01:44", + 531: 1, + 34665: 196, + } + + def test_exif_hide_offsets(self) -> None: + with Image.open("Tests/images/flower.jpg") as im: + exif = im.getexif() + + # Check offsets are present initially + assert 0x8769 in exif + for tag in (0xA005, 0x927C): + assert tag in exif.get_ifd(0x8769) + assert exif.get_ifd(0xA005) + loaded_exif = exif + + with Image.open("Tests/images/flower.jpg") as im: + new_exif = im.getexif() + + for exif in (loaded_exif, new_exif): + exif.hide_offsets() + + # Assert they are hidden afterwards, + # but that the IFDs are still available + assert 0x8769 not in exif + assert exif.get_ifd(0x8769) + for tag in (0xA005, 0x927C): + assert tag not in exif.get_ifd(0x8769) + assert exif.get_ifd(0xA005) + + @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0))) + def test_zero_tobytes(self, size: tuple[int, int]) -> None: + im = Image.new("RGB", size) + assert im.tobytes() == b"" + + @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0))) + def test_zero_frombytes(self, size: tuple[int, int]) -> None: + Image.frombytes("RGB", size, b"") + + im = Image.new("RGB", size) + im.frombytes(b"") + + def test_has_transparency_data(self) -> None: + for mode in ("1", "L", "P", "RGB"): + im = Image.new(mode, (1, 1)) + assert not im.has_transparency_data + + for mode in ("LA", "La", "PA", "RGBA", "RGBa"): + im = Image.new(mode, (1, 1)) + assert im.has_transparency_data + + # P mode with "transparency" info + with Image.open("Tests/images/first_frame_transparency.gif") as im: + assert "transparency" in im.info + assert im.has_transparency_data + + # RGB mode with "transparency" info + with Image.open("Tests/images/rgb_trns.png") as im: + assert "transparency" in im.info + assert im.has_transparency_data + + # P mode with RGBA palette + im = Image.new("RGBA", (1, 1)).convert("P") + assert im.mode == "P" + assert im.palette.mode == "RGBA" + assert im.has_transparency_data + + def test_apply_transparency(self) -> None: + im = Image.new("P", (1, 1)) + im.putpalette((0, 0, 0, 1, 1, 1)) + assert im.palette.colors == {(0, 0, 0): 0, (1, 1, 1): 1} + + # Test that no transformation is applied without transparency + im.apply_transparency() + assert im.palette.colors == {(0, 0, 0): 0, (1, 1, 1): 1} + + # Test that a transparency index is applied + im.info["transparency"] = 0 + im.apply_transparency() + assert "transparency" not in im.info + assert im.palette.colors == {(0, 0, 0, 0): 0, (1, 1, 1, 255): 1} + + # Test that existing transparency is kept + im = Image.new("P", (1, 1)) + im.putpalette((0, 0, 0, 255, 1, 1, 1, 128), "RGBA") + im.info["transparency"] = 0 + im.apply_transparency() + assert im.palette.colors == {(0, 0, 0, 0): 0, (1, 1, 1, 128): 1} + + # Test that transparency bytes are applied + with Image.open("Tests/images/pil123p.png") as im: + assert isinstance(im.info["transparency"], bytes) + assert im.palette.colors[(27, 35, 6)] == 24 + im.apply_transparency() + assert im.palette.colors[(27, 35, 6, 214)] == 24 + + def test_constants(self) -> None: + for enum in ( + Image.Transpose, + Image.Transform, + Image.Resampling, + Image.Dither, + Image.Palette, + Image.Quantize, + ): + for name in enum.__members__: + assert getattr(Image, name) == enum[name] + + @pytest.mark.parametrize( + "path", + [ + "fli_overrun.bin", + "sgi_overrun.bin", + "sgi_overrun_expandrow.bin", + "sgi_overrun_expandrow2.bin", + "pcx_overrun.bin", + "pcx_overrun2.bin", + "ossfuzz-4836216264589312.pcx", + "01r_00.pcx", + ], + ) + def test_overrun(self, path: str) -> None: + """For overrun completeness, test as: + valgrind pytest -qq Tests/test_image.py::TestImage::test_overrun | grep decode.c + """ + with Image.open(os.path.join("Tests/images", path)) as im: + try: + im.load() + pytest.fail() + except OSError as e: + buffer_overrun = str(e) == "buffer overrun when reading image file" + truncated = "image file is truncated" in str(e) + + assert buffer_overrun or truncated + + def test_fli_overrun2(self) -> None: + with Image.open("Tests/images/fli_overrun2.bin") as im: + try: + im.seek(1) + pytest.fail() + except OSError as e: + assert str(e) == "buffer overrun when reading image file" + + def test_exit_fp(self) -> None: + with Image.new("L", (1, 1)) as im: + pass + assert not hasattr(im, "fp") + + def test_close_graceful(self, caplog: pytest.LogCaptureFixture) -> None: + with Image.open("Tests/images/hopper.jpg") as im: + copy = im.copy() + with caplog.at_level(logging.DEBUG): + im.close() + copy.close() + assert len(caplog.records) == 0 + assert im.fp is None + + +class TestImageBytes: + @pytest.mark.parametrize("mode", Image.MODES + ["BGR;15", "BGR;16", "BGR;24"]) + def test_roundtrip_bytes_constructor(self, mode: str) -> None: + im = hopper(mode) + source_bytes = im.tobytes() + + if mode.startswith("BGR;"): + with pytest.warns(DeprecationWarning): + reloaded = Image.frombytes(mode, im.size, source_bytes) + else: + reloaded = Image.frombytes(mode, im.size, source_bytes) + assert reloaded.tobytes() == source_bytes + + @pytest.mark.parametrize("mode", Image.MODES + ["BGR;15", "BGR;16", "BGR;24"]) + def test_roundtrip_bytes_method(self, mode: str) -> None: + im = hopper(mode) + source_bytes = im.tobytes() + + reloaded = helper_image_new(mode, im.size) + reloaded.frombytes(source_bytes) + assert reloaded.tobytes() == source_bytes + + @pytest.mark.parametrize("mode", Image.MODES + ["BGR;15", "BGR;16", "BGR;24"]) + def test_getdata_putdata(self, mode: str) -> None: + if is_big_endian() and mode == "BGR;15": + pytest.xfail("Known failure of BGR;15 on big-endian") + im = hopper(mode) + reloaded = helper_image_new(mode, im.size) + reloaded.putdata(im.getdata()) + assert_image_equal(im, reloaded) + + +class MockEncoder(ImageFile.PyEncoder): + pass + + +class TestRegistry: + def test_encode_registry(self) -> None: + Image.register_encoder("MOCK", MockEncoder) + assert "MOCK" in Image.ENCODERS + + enc = Image._getencoder("RGB", "MOCK", ("args",), extra=("extra",)) + + assert isinstance(enc, MockEncoder) + assert enc.mode == "RGB" + assert enc.args == ("args", "extra") + + def test_encode_registry_fail(self) -> None: + with pytest.raises(OSError): + Image._getencoder("RGB", "DoesNotExist", ("args",), extra=("extra",)) + + + diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 8abb1f69f..3fd839fc0 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -1,463 +1,463 @@ -from __future__ import annotations - -import os -import subprocess -import sys -import sysconfig -from types import ModuleType - -import pytest - -from PIL import Image - -from .helper import assert_image_equal, hopper, is_win32 - -# CFFI imports pycparser which doesn't support PYTHONOPTIMIZE=2 -# https://github.com/eliben/pycparser/pull/198#issuecomment-317001670 -cffi: ModuleType | None -if os.environ.get("PYTHONOPTIMIZE") == "2": - cffi = None -else: - try: - import cffi - - from PIL import PyAccess - except ImportError: - cffi = None - -numpy: ModuleType | None -try: - import numpy -except ImportError: - numpy = None - - -class AccessTest: - # Initial value - _init_cffi_access = Image.USE_CFFI_ACCESS - _need_cffi_access = False - - @classmethod - def setup_class(cls) -> None: - Image.USE_CFFI_ACCESS = cls._need_cffi_access - - @classmethod - def teardown_class(cls) -> None: - Image.USE_CFFI_ACCESS = cls._init_cffi_access - - -class TestImagePutPixel(AccessTest): - def test_sanity(self) -> None: - im1 = hopper() - im2 = Image.new(im1.mode, im1.size, 0) - - for y in range(im1.size[1]): - for x in range(im1.size[0]): - pos = x, y - im2.putpixel(pos, im1.getpixel(pos)) - - assert_image_equal(im1, im2) - - im2 = Image.new(im1.mode, im1.size, 0) - im2.readonly = 1 - - for y in range(im1.size[1]): - for x in range(im1.size[0]): - pos = x, y - im2.putpixel(pos, im1.getpixel(pos)) - - assert not im2.readonly - assert_image_equal(im1, im2) - - im2 = Image.new(im1.mode, im1.size, 0) - - pix1 = im1.load() - pix2 = im2.load() - - with pytest.raises(TypeError): - pix1[0, "0"] - with pytest.raises(TypeError): - pix1["0", 0] - - for y in range(im1.size[1]): - for x in range(im1.size[0]): - pix2[x, y] = pix1[x, y] - - assert_image_equal(im1, im2) - - def test_sanity_negative_index(self) -> None: - im1 = hopper() - im2 = Image.new(im1.mode, im1.size, 0) - - width, height = im1.size - assert im1.getpixel((0, 0)) == im1.getpixel((-width, -height)) - assert im1.getpixel((-1, -1)) == im1.getpixel((width - 1, height - 1)) - - for y in range(-1, -im1.size[1] - 1, -1): - for x in range(-1, -im1.size[0] - 1, -1): - pos = x, y - im2.putpixel(pos, im1.getpixel(pos)) - - assert_image_equal(im1, im2) - - im2 = Image.new(im1.mode, im1.size, 0) - im2.readonly = 1 - - for y in range(-1, -im1.size[1] - 1, -1): - for x in range(-1, -im1.size[0] - 1, -1): - pos = x, y - im2.putpixel(pos, im1.getpixel(pos)) - - assert not im2.readonly - assert_image_equal(im1, im2) - - im2 = Image.new(im1.mode, im1.size, 0) - - pix1 = im1.load() - pix2 = im2.load() - - for y in range(-1, -im1.size[1] - 1, -1): - for x in range(-1, -im1.size[0] - 1, -1): - pix2[x, y] = pix1[x, y] - - assert_image_equal(im1, im2) - - @pytest.mark.skipif(numpy is None, reason="NumPy not installed") - def test_numpy(self) -> None: - im = hopper() - pix = im.load() - - assert numpy is not None - assert pix[numpy.int32(1), numpy.int32(2)] == (18, 20, 59) - - -class TestImageGetPixel(AccessTest): - @staticmethod - def color(mode: str) -> int | tuple[int, ...]: - bands = Image.getmodebands(mode) - if bands == 1: - return 1 - if mode in ("BGR;15", "BGR;16"): - # These modes have less than 8 bits per band, - # so (1, 2, 3) cannot be roundtripped. - return (16, 32, 49) - return tuple(range(1, bands + 1)) - - def check(self, mode: str, expected_color_int: int | None = None) -> None: - if self._need_cffi_access and mode.startswith("BGR;"): - pytest.skip("Support not added to deprecated module for BGR;* modes") - - expected_color = ( - self.color(mode) if expected_color_int is None else expected_color_int - ) - - # Check putpixel - im = Image.new(mode, (1, 1), None) - im.putpixel((0, 0), expected_color) - actual_color = im.getpixel((0, 0)) - assert actual_color == expected_color, ( - f"put/getpixel roundtrip failed for mode {mode}, " - f"expected {expected_color} got {actual_color}" - ) - - # Check putpixel negative index - im.putpixel((-1, -1), expected_color) - actual_color = im.getpixel((-1, -1)) - assert actual_color == expected_color, ( - f"put/getpixel roundtrip negative index failed for mode {mode}, " - f"expected {expected_color} got {actual_color}" - ) - - # Check 0x0 image with None initial color - im = Image.new(mode, (0, 0), None) - assert im.load() is not None - error = ValueError if self._need_cffi_access else IndexError - with pytest.raises(error): - im.putpixel((0, 0), expected_color) - with pytest.raises(error): - im.getpixel((0, 0)) - # Check negative index - with pytest.raises(error): - im.putpixel((-1, -1), expected_color) - with pytest.raises(error): - im.getpixel((-1, -1)) - - # Check initial color - im = Image.new(mode, (1, 1), expected_color) - actual_color = im.getpixel((0, 0)) - assert actual_color == expected_color, ( - f"initial color failed for mode {mode}, " - f"expected {expected_color} got {actual_color}" - ) - - # Check initial color negative index - actual_color = im.getpixel((-1, -1)) - assert actual_color == expected_color, ( - f"initial color failed with negative index for mode {mode}, " - f"expected {expected_color} got {actual_color}" - ) - - # Check 0x0 image with initial color - im = Image.new(mode, (0, 0), expected_color) - with pytest.raises(error): - im.getpixel((0, 0)) - # Check negative index - with pytest.raises(error): - im.getpixel((-1, -1)) - - @pytest.mark.parametrize("mode", Image.MODES) - def test_basic(self, mode: str) -> None: - self.check(mode) - - @pytest.mark.parametrize("mode", ("BGR;15", "BGR;16", "BGR;24")) - def test_deprecated(self, mode: str) -> None: - with pytest.warns(DeprecationWarning): - self.check(mode) - - def test_list(self) -> None: - im = hopper() - assert im.getpixel([0, 0]) == (20, 20, 70) - - @pytest.mark.parametrize("mode", ("I;16", "I;16B")) - @pytest.mark.parametrize("expected_color", (2**15 - 1, 2**15, 2**15 + 1, 2**16 - 1)) - def test_signedness(self, mode: str, expected_color: int) -> None: - # See https://github.com/python-pillow/Pillow/issues/452 - # pixelaccess is using signed int* instead of uint* - self.check(mode, expected_color) - - @pytest.mark.parametrize("mode", ("P", "PA")) - @pytest.mark.parametrize("color", ((255, 0, 0), (255, 0, 0, 255))) - def test_p_putpixel_rgb_rgba(self, mode: str, color: tuple[int, ...]) -> None: - im = Image.new(mode, (1, 1)) - im.putpixel((0, 0), color) - - alpha = color[3] if len(color) == 4 and mode == "PA" else 255 - assert im.convert("RGBA").getpixel((0, 0)) == (255, 0, 0, alpha) - - -@pytest.mark.filterwarnings("ignore::DeprecationWarning") -@pytest.mark.skipif(cffi is None, reason="No CFFI") -class TestCffiPutPixel(TestImagePutPixel): - _need_cffi_access = True - - -@pytest.mark.filterwarnings("ignore::DeprecationWarning") -@pytest.mark.skipif(cffi is None, reason="No CFFI") -class TestCffiGetPixel(TestImageGetPixel): - _need_cffi_access = True - - -@pytest.mark.skipif(cffi is None, reason="No CFFI") -class TestCffi(AccessTest): - _need_cffi_access = True - - def _test_get_access(self, im: Image.Image) -> None: - """Do we get the same thing as the old pixel access - - Using private interfaces, forcing a capi access and - a pyaccess for the same image""" - caccess = im.im.pixel_access(False) - with pytest.warns(DeprecationWarning): - access = PyAccess.new(im, False) - assert access is not None - - w, h = im.size - for x in range(0, w, 10): - for y in range(0, h, 10): - assert access[(x, y)] == caccess[(x, y)] - - # Access an out-of-range pixel - with pytest.raises(ValueError): - access[(access.xsize + 1, access.ysize + 1)] - - def test_get_vs_c(self) -> None: - with pytest.warns(DeprecationWarning): - rgb = hopper("RGB") - rgb.load() - self._test_get_access(rgb) - for mode in ("RGBA", "L", "LA", "1", "P", "F"): - self._test_get_access(hopper(mode)) - - for mode in ("I;16", "I;16L", "I;16B", "I;16N", "I"): - im = Image.new(mode, (10, 10), 40000) - self._test_get_access(im) - - def _test_set_access(self, im: Image.Image, color: tuple[int, ...] | float) -> None: - """Are we writing the correct bits into the image? - - Using private interfaces, forcing a capi access and - a pyaccess for the same image""" - caccess = im.im.pixel_access(False) - with pytest.warns(DeprecationWarning): - access = PyAccess.new(im, False) - assert access is not None - - w, h = im.size - for x in range(0, w, 10): - for y in range(0, h, 10): - access[(x, y)] = color - assert color == caccess[(x, y)] - - # Attempt to set the value on a read-only image - with pytest.warns(DeprecationWarning): - access = PyAccess.new(im, True) - assert access is not None - - with pytest.raises(ValueError): - access[(0, 0)] = color - - def test_set_vs_c(self) -> None: - rgb = hopper("RGB") - with pytest.warns(DeprecationWarning): - rgb.load() - self._test_set_access(rgb, (255, 128, 0)) - self._test_set_access(hopper("RGBA"), (255, 192, 128, 0)) - self._test_set_access(hopper("L"), 128) - self._test_set_access(hopper("LA"), (128, 128)) - self._test_set_access(hopper("1"), 255) - self._test_set_access(hopper("P"), 128) - self._test_set_access(hopper("PA"), (128, 128)) - self._test_set_access(hopper("F"), 1024.0) - - for mode in ("I;16", "I;16L", "I;16B", "I;16N", "I"): - im = Image.new(mode, (10, 10), 40000) - self._test_set_access(im, 45000) - - @pytest.mark.filterwarnings("ignore::DeprecationWarning") - def test_not_implemented(self) -> None: - assert PyAccess.new(hopper("BGR;15")) is None - - # Ref https://github.com/python-pillow/Pillow/pull/2009 - def test_reference_counting(self) -> None: - size = 10 - - for _ in range(10): - # Do not save references to the image, only to the access object - with pytest.warns(DeprecationWarning): - px = Image.new("L", (size, 1), 0).load() - for i in range(size): - # Pixels can contain garbage if image is released - assert px[i, 0] == 0 - - @pytest.mark.parametrize("mode", ("P", "PA")) - def test_p_putpixel_rgb_rgba(self, mode: str) -> None: - for color in ((255, 0, 0), (255, 0, 0, 127 if mode == "PA" else 255)): - im = Image.new(mode, (1, 1)) - with pytest.warns(DeprecationWarning): - access = PyAccess.new(im, False) - assert access is not None - - access.putpixel((0, 0), color) - - if len(color) == 3: - color += (255,) - assert im.convert("RGBA").getpixel((0, 0)) == color - - -class TestImagePutPixelError(AccessTest): - IMAGE_MODES1 = ["LA", "RGB", "RGBA", "BGR;15"] - IMAGE_MODES2 = ["L", "I", "I;16"] - INVALID_TYPES = ["foo", 1.0, None] - - @pytest.mark.parametrize("mode", IMAGE_MODES1) - def test_putpixel_type_error1(self, mode: str) -> None: - im = hopper(mode) - for v in self.INVALID_TYPES: - with pytest.raises(TypeError, match="color must be int or tuple"): - im.putpixel((0, 0), v) - - @pytest.mark.parametrize( - ("mode", "band_numbers", "match"), - ( - ("L", (0, 2), "color must be int or single-element tuple"), - ("LA", (0, 3), "color must be int, or tuple of one or two elements"), - ( - "BGR;15", - (0, 2), - "color must be int, or tuple of one or three elements", - ), - ( - "RGB", - (0, 2, 5), - "color must be int, or tuple of one, three or four elements", - ), - ), - ) - def test_putpixel_invalid_number_of_bands( - self, mode: str, band_numbers: tuple[int, ...], match: str - ) -> None: - im = hopper(mode) - for band_number in band_numbers: - with pytest.raises(TypeError, match=match): - im.putpixel((0, 0), (0,) * band_number) - - @pytest.mark.parametrize("mode", IMAGE_MODES2) - def test_putpixel_type_error2(self, mode: str) -> None: - im = hopper(mode) - for v in self.INVALID_TYPES: - with pytest.raises( - TypeError, match="color must be int or single-element tuple" - ): - im.putpixel((0, 0), v) - - @pytest.mark.parametrize("mode", IMAGE_MODES1 + IMAGE_MODES2) - def test_putpixel_overflow_error(self, mode: str) -> None: - im = hopper(mode) - with pytest.raises(OverflowError): - im.putpixel((0, 0), 2**80) - - -class TestEmbeddable: - @pytest.mark.xfail(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 - - with open("embed_pil.c", "w", encoding="utf-8") as fh: - home = sys.prefix.replace("\\", "\\\\") - fh.write( - f""" -#include "Python.h" - -int main(int argc, char* argv[]) -{{ - char *home = "{home}"; - wchar_t *whome = Py_DecodeLocale(home, NULL); - Py_SetPythonHome(whome); - - Py_InitializeEx(0); - Py_DECREF(PyImport_ImportModule("PIL.Image")); - Py_Finalize(); - - Py_InitializeEx(0); - Py_DECREF(PyImport_ImportModule("PIL.Image")); - Py_Finalize(); - - PyMem_RawFree(whome); - - return 0; -}} - """ - ) - - compiler = getattr(build_ext, "new_compiler")() - compiler.add_include_dir(sysconfig.get_config_var("INCLUDEPY")) - - libdir = sysconfig.get_config_var("LIBDIR") or sysconfig.get_config_var( - "INCLUDEPY" - ).replace("include", "libs") - compiler.add_library_dir(libdir) - objects = compiler.compile(["embed_pil.c"]) - compiler.link_executable(objects, "embed_pil") - - env = os.environ.copy() - env["PATH"] = sys.prefix + ";" + env["PATH"] - - # Do not display the Windows Error Reporting dialog - getattr(ctypes, "windll").kernel32.SetErrorMode(0x0002) - - process = subprocess.Popen(["embed_pil.exe"], env=env) - process.communicate() - assert process.returncode == 0 +from __future__ import annotations + +import os +import subprocess +import sys +import sysconfig +from types import ModuleType + +import pytest + +from PIL import Image + +from .helper import assert_image_equal, hopper, is_win32 + +# CFFI imports pycparser which doesn't support PYTHONOPTIMIZE=2 +# https://github.com/eliben/pycparser/pull/198#issuecomment-317001670 +cffi: ModuleType | None +if os.environ.get("PYTHONOPTIMIZE") == "2": + cffi = None +else: + try: + import cffi + + from PIL import PyAccess + except ImportError: + cffi = None + +numpy: ModuleType | None +try: + import numpy +except ImportError: + numpy = None + + +class AccessTest: + # Initial value + _init_cffi_access = Image.USE_CFFI_ACCESS + _need_cffi_access = False + + @classmethod + def setup_class(cls) -> None: + Image.USE_CFFI_ACCESS = cls._need_cffi_access + + @classmethod + def teardown_class(cls) -> None: + Image.USE_CFFI_ACCESS = cls._init_cffi_access + + +class TestImagePutPixel(AccessTest): + def test_sanity(self) -> None: + im1 = hopper() + im2 = Image.new(im1.mode, im1.size, 0) + + for y in range(im1.size[1]): + for x in range(im1.size[0]): + pos = x, y + im2.putpixel(pos, im1.getpixel(pos)) + + assert_image_equal(im1, im2) + + im2 = Image.new(im1.mode, im1.size, 0) + im2.readonly = 1 + + for y in range(im1.size[1]): + for x in range(im1.size[0]): + pos = x, y + im2.putpixel(pos, im1.getpixel(pos)) + + assert not im2.readonly + assert_image_equal(im1, im2) + + im2 = Image.new(im1.mode, im1.size, 0) + + pix1 = im1.load() + pix2 = im2.load() + + with pytest.raises(TypeError): + pix1[0, "0"] + with pytest.raises(TypeError): + pix1["0", 0] + + for y in range(im1.size[1]): + for x in range(im1.size[0]): + pix2[x, y] = pix1[x, y] + + assert_image_equal(im1, im2) + + def test_sanity_negative_index(self) -> None: + im1 = hopper() + im2 = Image.new(im1.mode, im1.size, 0) + + width, height = im1.size + assert im1.getpixel((0, 0)) == im1.getpixel((-width, -height)) + assert im1.getpixel((-1, -1)) == im1.getpixel((width - 1, height - 1)) + + for y in range(-1, -im1.size[1] - 1, -1): + for x in range(-1, -im1.size[0] - 1, -1): + pos = x, y + im2.putpixel(pos, im1.getpixel(pos)) + + assert_image_equal(im1, im2) + + im2 = Image.new(im1.mode, im1.size, 0) + im2.readonly = 1 + + for y in range(-1, -im1.size[1] - 1, -1): + for x in range(-1, -im1.size[0] - 1, -1): + pos = x, y + im2.putpixel(pos, im1.getpixel(pos)) + + assert not im2.readonly + assert_image_equal(im1, im2) + + im2 = Image.new(im1.mode, im1.size, 0) + + pix1 = im1.load() + pix2 = im2.load() + + for y in range(-1, -im1.size[1] - 1, -1): + for x in range(-1, -im1.size[0] - 1, -1): + pix2[x, y] = pix1[x, y] + + assert_image_equal(im1, im2) + + @pytest.mark.skipif(numpy is None, reason="NumPy not installed") + def test_numpy(self) -> None: + im = hopper() + pix = im.load() + + assert numpy is not None + assert pix[numpy.int32(1), numpy.int32(2)] == (18, 20, 59) + + +class TestImageGetPixel(AccessTest): + @staticmethod + def color(mode: str) -> int | tuple[int, ...]: + bands = Image.getmodebands(mode) + if bands == 1: + return 1 + if mode in ("BGR;15", "BGR;16"): + # These modes have less than 8 bits per band, + # so (1, 2, 3) cannot be roundtripped. + return (16, 32, 49) + return tuple(range(1, bands + 1)) + + def check(self, mode: str, expected_color_int: int | None = None) -> None: + if self._need_cffi_access and mode.startswith("BGR;"): + pytest.skip("Support not added to deprecated module for BGR;* modes") + + expected_color = ( + self.color(mode) if expected_color_int is None else expected_color_int + ) + + # Check putpixel + im = Image.new(mode, (1, 1), None) + im.putpixel((0, 0), expected_color) + actual_color = im.getpixel((0, 0)) + assert actual_color == expected_color, ( + f"put/getpixel roundtrip failed for mode {mode}, " + f"expected {expected_color} got {actual_color}" + ) + + # Check putpixel negative index + im.putpixel((-1, -1), expected_color) + actual_color = im.getpixel((-1, -1)) + assert actual_color == expected_color, ( + f"put/getpixel roundtrip negative index failed for mode {mode}, " + f"expected {expected_color} got {actual_color}" + ) + + # Check 0x0 image with None initial color + im = Image.new(mode, (0, 0), None) + assert im.load() is not None + error = ValueError if self._need_cffi_access else IndexError + with pytest.raises(error): + im.putpixel((0, 0), expected_color) + with pytest.raises(error): + im.getpixel((0, 0)) + # Check negative index + with pytest.raises(error): + im.putpixel((-1, -1), expected_color) + with pytest.raises(error): + im.getpixel((-1, -1)) + + # Check initial color + im = Image.new(mode, (1, 1), expected_color) + actual_color = im.getpixel((0, 0)) + assert actual_color == expected_color, ( + f"initial color failed for mode {mode}, " + f"expected {expected_color} got {actual_color}" + ) + + # Check initial color negative index + actual_color = im.getpixel((-1, -1)) + assert actual_color == expected_color, ( + f"initial color failed with negative index for mode {mode}, " + f"expected {expected_color} got {actual_color}" + ) + + # Check 0x0 image with initial color + im = Image.new(mode, (0, 0), expected_color) + with pytest.raises(error): + im.getpixel((0, 0)) + # Check negative index + with pytest.raises(error): + im.getpixel((-1, -1)) + + @pytest.mark.parametrize("mode", Image.MODES) + def test_basic(self, mode: str) -> None: + self.check(mode) + + @pytest.mark.parametrize("mode", ("BGR;15", "BGR;16", "BGR;24")) + def test_deprecated(self, mode: str) -> None: + with pytest.warns(DeprecationWarning): + self.check(mode) + + def test_list(self) -> None: + im = hopper() + assert im.getpixel([0, 0]) == (20, 20, 70) + + @pytest.mark.parametrize("mode", ("I;16", "I;16B")) + @pytest.mark.parametrize("expected_color", (2**15 - 1, 2**15, 2**15 + 1, 2**16 - 1)) + def test_signedness(self, mode: str, expected_color: int) -> None: + # See https://github.com/python-pillow/Pillow/issues/452 + # pixelaccess is using signed int* instead of uint* + self.check(mode, expected_color) + + @pytest.mark.parametrize("mode", ("P", "PA")) + @pytest.mark.parametrize("color", ((255, 0, 0), (255, 0, 0, 255))) + def test_p_putpixel_rgb_rgba(self, mode: str, color: tuple[int, ...]) -> None: + im = Image.new(mode, (1, 1)) + im.putpixel((0, 0), color) + + alpha = color[3] if len(color) == 4 and mode == "PA" else 255 + assert im.convert("RGBA").getpixel((0, 0)) == (255, 0, 0, alpha) + + +@pytest.mark.filterwarnings("ignore::DeprecationWarning") +@pytest.mark.skipif(cffi is None, reason="No CFFI") +class TestCffiPutPixel(TestImagePutPixel): + _need_cffi_access = True + + +@pytest.mark.filterwarnings("ignore::DeprecationWarning") +@pytest.mark.skipif(cffi is None, reason="No CFFI") +class TestCffiGetPixel(TestImageGetPixel): + _need_cffi_access = True + + +@pytest.mark.skipif(cffi is None, reason="No CFFI") +class TestCffi(AccessTest): + _need_cffi_access = True + + def _test_get_access(self, im: Image.Image) -> None: + """Do we get the same thing as the old pixel access + + Using private interfaces, forcing a capi access and + a pyaccess for the same image""" + caccess = im.im.pixel_access(False) + with pytest.warns(DeprecationWarning): + access = PyAccess.new(im, False) + assert access is not None + + w, h = im.size + for x in range(0, w, 10): + for y in range(0, h, 10): + assert access[(x, y)] == caccess[(x, y)] + + # Access an out-of-range pixel + with pytest.raises(ValueError): + access[(access.xsize + 1, access.ysize + 1)] + + def test_get_vs_c(self) -> None: + with pytest.warns(DeprecationWarning): + rgb = hopper("RGB") + rgb.load() + self._test_get_access(rgb) + for mode in ("RGBA", "L", "LA", "1", "P", "F"): + self._test_get_access(hopper(mode)) + + for mode in ("I;16", "I;16L", "I;16B", "I;16N", "I"): + im = Image.new(mode, (10, 10), 40000) + self._test_get_access(im) + + def _test_set_access(self, im: Image.Image, color: tuple[int, ...] | float) -> None: + """Are we writing the correct bits into the image? + + Using private interfaces, forcing a capi access and + a pyaccess for the same image""" + caccess = im.im.pixel_access(False) + with pytest.warns(DeprecationWarning): + access = PyAccess.new(im, False) + assert access is not None + + w, h = im.size + for x in range(0, w, 10): + for y in range(0, h, 10): + access[(x, y)] = color + assert color == caccess[(x, y)] + + # Attempt to set the value on a read-only image + with pytest.warns(DeprecationWarning): + access = PyAccess.new(im, True) + assert access is not None + + with pytest.raises(ValueError): + access[(0, 0)] = color + + def test_set_vs_c(self) -> None: + rgb = hopper("RGB") + with pytest.warns(DeprecationWarning): + rgb.load() + self._test_set_access(rgb, (255, 128, 0)) + self._test_set_access(hopper("RGBA"), (255, 192, 128, 0)) + self._test_set_access(hopper("L"), 128) + self._test_set_access(hopper("LA"), (128, 128)) + self._test_set_access(hopper("1"), 255) + self._test_set_access(hopper("P"), 128) + self._test_set_access(hopper("PA"), (128, 128)) + self._test_set_access(hopper("F"), 1024.0) + + for mode in ("I;16", "I;16L", "I;16B", "I;16N", "I"): + im = Image.new(mode, (10, 10), 40000) + self._test_set_access(im, 45000) + + @pytest.mark.filterwarnings("ignore::DeprecationWarning") + def test_not_implemented(self) -> None: + assert PyAccess.new(hopper("BGR;15")) is None + + # Ref https://github.com/python-pillow/Pillow/pull/2009 + def test_reference_counting(self) -> None: + size = 10 + + for _ in range(10): + # Do not save references to the image, only to the access object + with pytest.warns(DeprecationWarning): + px = Image.new("L", (size, 1), 0).load() + for i in range(size): + # Pixels can contain garbage if image is released + assert px[i, 0] == 0 + + @pytest.mark.parametrize("mode", ("P", "PA")) + def test_p_putpixel_rgb_rgba(self, mode: str) -> None: + for color in ((255, 0, 0), (255, 0, 0, 127 if mode == "PA" else 255)): + im = Image.new(mode, (1, 1)) + with pytest.warns(DeprecationWarning): + access = PyAccess.new(im, False) + assert access is not None + + access.putpixel((0, 0), color) + + if len(color) == 3: + color += (255,) + assert im.convert("RGBA").getpixel((0, 0)) == color + + +class TestImagePutPixelError(AccessTest): + IMAGE_MODES1 = ["LA", "RGB", "RGBA", "BGR;15"] + IMAGE_MODES2 = ["L", "I", "I;16"] + INVALID_TYPES = ["foo", 1.0, None] + + @pytest.mark.parametrize("mode", IMAGE_MODES1) + def test_putpixel_type_error1(self, mode: str) -> None: + im = hopper(mode) + for v in self.INVALID_TYPES: + with pytest.raises(TypeError, match="color must be int or tuple"): + im.putpixel((0, 0), v) + + @pytest.mark.parametrize( + ("mode", "band_numbers", "match"), + ( + ("L", (0, 2), "color must be int or single-element tuple"), + ("LA", (0, 3), "color must be int, or tuple of one or two elements"), + ( + "BGR;15", + (0, 2), + "color must be int, or tuple of one or three elements", + ), + ( + "RGB", + (0, 2, 5), + "color must be int, or tuple of one, three or four elements", + ), + ), + ) + def test_putpixel_invalid_number_of_bands( + self, mode: str, band_numbers: tuple[int, ...], match: str + ) -> None: + im = hopper(mode) + for band_number in band_numbers: + with pytest.raises(TypeError, match=match): + im.putpixel((0, 0), (0,) * band_number) + + @pytest.mark.parametrize("mode", IMAGE_MODES2) + def test_putpixel_type_error2(self, mode: str) -> None: + im = hopper(mode) + for v in self.INVALID_TYPES: + with pytest.raises( + TypeError, match="color must be int or single-element tuple" + ): + im.putpixel((0, 0), v) + + @pytest.mark.parametrize("mode", IMAGE_MODES1 + IMAGE_MODES2) + def test_putpixel_overflow_error(self, mode: str) -> None: + im = hopper(mode) + with pytest.raises(OverflowError): + im.putpixel((0, 0), 2**80) + + +class TestEmbeddable: + @pytest.mark.xfail(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 + + with open("embed_pil.c", "w", encoding="utf-8") as fh: + home = sys.prefix.replace("\\", "\\\\") + fh.write( + f""" +#include "Python.h" + +int main(int argc, char* argv[]) +{{ + char *home = "{home}"; + wchar_t *whome = Py_DecodeLocale(home, NULL); + Py_SetPythonHome(whome); + + Py_InitializeEx(0); + Py_DECREF(PyImport_ImportModule("PIL.Image")); + Py_Finalize(); + + Py_InitializeEx(0); + Py_DECREF(PyImport_ImportModule("PIL.Image")); + Py_Finalize(); + + PyMem_RawFree(whome); + + return 0; +}} + """ + ) + + compiler = getattr(build_ext, "new_compiler")() + compiler.add_include_dir(sysconfig.get_config_var("INCLUDEPY")) + + libdir = sysconfig.get_config_var("LIBDIR") or sysconfig.get_config_var( + "INCLUDEPY" + ).replace("include", "libs") + compiler.add_library_dir(libdir) + objects = compiler.compile(["embed_pil.c"]) + compiler.link_executable(objects, "embed_pil") + + env = os.environ.copy() + env["PATH"] = sys.prefix + ";" + env["PATH"] + + # Do not display the Windows Error Reporting dialog + getattr(ctypes, "windll").kernel32.SetErrorMode(0x0002) + + process = subprocess.Popen(["embed_pil.exe"], env=env) + process.communicate() + assert process.returncode == 0 diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py index d7e6c562c..0e6d41ef5 100644 --- a/Tests/test_image_array.py +++ b/Tests/test_image_array.py @@ -1,113 +1,113 @@ -from __future__ import annotations - -from typing import Any - -import pytest -from packaging.version import parse as parse_version - -from PIL import Image - -from .helper import hopper - -numpy = pytest.importorskip("numpy", reason="NumPy not installed") - -im = hopper().resize((128, 100)) - - -def test_toarray() -> None: - def test(mode: str) -> tuple[tuple[int, ...], str, int]: - ai = numpy.array(im.convert(mode)) - return ai.shape, ai.dtype.str, ai.nbytes - - def test_with_dtype(dtype) -> None: - ai = numpy.array(im, dtype=dtype) - assert ai.dtype == dtype - - # assert test("1") == ((100, 128), '|b1', 1600)) - assert test("L") == ((100, 128), "|u1", 12800) - - # FIXME: wrong? - assert test("I") == ((100, 128), Image._ENDIAN + "i4", 51200) - # FIXME: wrong? - assert test("F") == ((100, 128), Image._ENDIAN + "f4", 51200) - - assert test("LA") == ((100, 128, 2), "|u1", 25600) - assert test("RGB") == ((100, 128, 3), "|u1", 38400) - assert test("RGBA") == ((100, 128, 4), "|u1", 51200) - assert test("RGBX") == ((100, 128, 4), "|u1", 51200) - - test_with_dtype(numpy.float64) - test_with_dtype(numpy.uint8) - - with Image.open("Tests/images/truncated_jpeg.jpg") as im_truncated: - if parse_version(numpy.__version__) >= parse_version("1.23"): - with pytest.raises(OSError): - numpy.array(im_truncated) - else: - with pytest.warns(UserWarning): - numpy.array(im_truncated) - - -def test_fromarray() -> None: - class Wrapper: - """Class with API matching Image.fromarray""" - - def __init__(self, img: Image.Image, arr_params: dict[str, Any]) -> None: - self.img = img - self.__array_interface__ = arr_params - - def tobytes(self) -> bytes: - return self.img.tobytes() - - def test(mode: str) -> tuple[str, tuple[int, int], bool]: - i = im.convert(mode) - a = numpy.array(i) - # Make wrapper instance for image, new array interface - wrapped = Wrapper( - i, - { - "shape": a.shape, - "typestr": a.dtype.str, - "version": 3, - "data": a.data, - "strides": 1, # pretend it's non-contiguous - }, - ) - out = Image.fromarray(wrapped) - return out.mode, out.size, list(i.getdata()) == list(out.getdata()) - - # assert test("1") == ("1", (128, 100), True) - assert test("L") == ("L", (128, 100), True) - assert test("I") == ("I", (128, 100), True) - assert test("F") == ("F", (128, 100), True) - assert test("LA") == ("LA", (128, 100), True) - assert test("RGB") == ("RGB", (128, 100), True) - assert test("RGBA") == ("RGBA", (128, 100), True) - assert test("RGBX") == ("RGBA", (128, 100), True) - - # Test mode is None with no "typestr" in the array interface - wrapped = Wrapper(hopper("L"), {"shape": (100, 128)}) - with pytest.raises(TypeError): - Image.fromarray(wrapped) - - -def test_fromarray_strides_without_tobytes() -> None: - class Wrapper: - def __init__(self, arr_params: dict[str, Any]) -> None: - self.__array_interface__ = arr_params - - with pytest.raises(ValueError): - wrapped = Wrapper({"shape": (1, 1), "strides": (1, 1)}) - Image.fromarray(wrapped, "L") - - -def test_fromarray_palette() -> None: - # Arrange - i = im.convert("L") - a = numpy.array(i) - - # Act - out = Image.fromarray(a, "P") - - # Assert that the Python and C palettes match - assert len(out.palette.colors) == len(out.im.getpalette()) / 3 +from __future__ import annotations + +from typing import Any + +import pytest +from packaging.version import parse as parse_version + +from PIL import Image + +from .helper import hopper + +numpy = pytest.importorskip("numpy", reason="NumPy not installed") + +im = hopper().resize((128, 100)) + + +def test_toarray() -> None: + def test(mode: str) -> tuple[tuple[int, ...], str, int]: + ai = numpy.array(im.convert(mode)) + return ai.shape, ai.dtype.str, ai.nbytes + + def test_with_dtype(dtype) -> None: + ai = numpy.array(im, dtype=dtype) + assert ai.dtype == dtype + + # assert test("1") == ((100, 128), '|b1', 1600)) + assert test("L") == ((100, 128), "|u1", 12800) + + # FIXME: wrong? + assert test("I") == ((100, 128), Image._ENDIAN + "i4", 51200) + # FIXME: wrong? + assert test("F") == ((100, 128), Image._ENDIAN + "f4", 51200) + + assert test("LA") == ((100, 128, 2), "|u1", 25600) + assert test("RGB") == ((100, 128, 3), "|u1", 38400) + assert test("RGBA") == ((100, 128, 4), "|u1", 51200) + assert test("RGBX") == ((100, 128, 4), "|u1", 51200) + + test_with_dtype(numpy.float64) + test_with_dtype(numpy.uint8) + + with Image.open("Tests/images/truncated_jpeg.jpg") as im_truncated: + if parse_version(numpy.__version__) >= parse_version("1.23"): + with pytest.raises(OSError): + numpy.array(im_truncated) + else: + with pytest.warns(UserWarning): + numpy.array(im_truncated) + + +def test_fromarray() -> None: + class Wrapper: + """Class with API matching Image.fromarray""" + + def __init__(self, img: Image.Image, arr_params: dict[str, Any]) -> None: + self.img = img + self.__array_interface__ = arr_params + + def tobytes(self) -> bytes: + return self.img.tobytes() + + def test(mode: str) -> tuple[str, tuple[int, int], bool]: + i = im.convert(mode) + a = numpy.array(i) + # Make wrapper instance for image, new array interface + wrapped = Wrapper( + i, + { + "shape": a.shape, + "typestr": a.dtype.str, + "version": 3, + "data": a.data, + "strides": 1, # pretend it's non-contiguous + }, + ) + out = Image.fromarray(wrapped) + return out.mode, out.size, list(i.getdata()) == list(out.getdata()) + + # assert test("1") == ("1", (128, 100), True) + assert test("L") == ("L", (128, 100), True) + assert test("I") == ("I", (128, 100), True) + assert test("F") == ("F", (128, 100), True) + assert test("LA") == ("LA", (128, 100), True) + assert test("RGB") == ("RGB", (128, 100), True) + assert test("RGBA") == ("RGBA", (128, 100), True) + assert test("RGBX") == ("RGBA", (128, 100), True) + + # Test mode is None with no "typestr" in the array interface + wrapped = Wrapper(hopper("L"), {"shape": (100, 128)}) + with pytest.raises(TypeError): + Image.fromarray(wrapped) + + +def test_fromarray_strides_without_tobytes() -> None: + class Wrapper: + def __init__(self, arr_params: dict[str, Any]) -> None: + self.__array_interface__ = arr_params + + with pytest.raises(ValueError): + wrapped = Wrapper({"shape": (1, 1), "strides": (1, 1)}) + Image.fromarray(wrapped, "L") + + +def test_fromarray_palette() -> None: + # Arrange + i = im.convert("L") + a = numpy.array(i) + + # Act + out = Image.fromarray(a, "P") + + # Assert that the Python and C palettes match + assert len(out.palette.colors) == len(out.im.getpalette()) / 3 diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index 2fb45854a..3b6e7e315 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -1,351 +1,351 @@ -from __future__ import annotations - -from pathlib import Path - -import pytest - -from PIL import Image - -from .helper import assert_image, assert_image_equal, assert_image_similar, hopper - - -def test_sanity() -> None: - def convert(im: Image.Image, mode: str) -> None: - out = im.convert(mode) - assert out.mode == mode - assert out.size == im.size - - modes = ( - "1", - "L", - "LA", - "P", - "PA", - "I", - "F", - "RGB", - "RGBA", - "RGBX", - "CMYK", - "YCbCr", - "HSV", - ) - - for input_mode in modes: - im = hopper(input_mode) - for output_mode in modes: - convert(im, output_mode) - - # Check 0 - im = Image.new(input_mode, (0, 0)) - for output_mode in modes: - convert(im, output_mode) - - -def test_unsupported_conversion() -> None: - im = hopper() - with pytest.raises(ValueError): - im.convert("INVALID") - - -def test_default() -> None: - im = hopper("P") - assert im.mode == "P" - converted_im = im.convert() - assert_image(converted_im, "RGB", im.size) - converted_im = im.convert() - assert_image(converted_im, "RGB", im.size) - - im.info["transparency"] = 0 - converted_im = im.convert() - assert_image(converted_im, "RGBA", im.size) - - -# ref https://github.com/python-pillow/Pillow/issues/274 - - -def _test_float_conversion(im: Image.Image) -> None: - orig = im.getpixel((5, 5)) - converted = im.convert("F").getpixel((5, 5)) - assert orig == converted - - -def test_8bit() -> None: - with Image.open("Tests/images/hopper.jpg") as im: - _test_float_conversion(im.convert("L")) - - -def test_16bit() -> None: - with Image.open("Tests/images/16bit.cropped.tif") as im: - _test_float_conversion(im) - - for color in (65535, 65536): - im = Image.new("I", (1, 1), color) - im_i16 = im.convert("I;16") - assert im_i16.getpixel((0, 0)) == 65535 - - -def test_16bit_workaround() -> None: - with Image.open("Tests/images/16bit.cropped.tif") as im: - _test_float_conversion(im.convert("I")) - - -def test_opaque() -> None: - alpha = hopper("P").convert("PA").getchannel("A") - - solid = Image.new("L", (128, 128), 255) - assert_image_equal(alpha, solid) - - -def test_rgba_p() -> None: - im = hopper("RGBA") - im.putalpha(hopper("L")) - - converted = im.convert("P") - comparable = converted.convert("RGBA") - - assert_image_similar(im, comparable, 20) - - -def test_rgba() -> None: - with Image.open("Tests/images/transparent.png") as im: - assert im.mode == "RGBA" - - assert_image_similar(im.convert("RGBa").convert("RGB"), im.convert("RGB"), 1.5) - - -def test_trns_p(tmp_path: Path) -> None: - im = hopper("P") - im.info["transparency"] = 0 - - f = str(tmp_path / "temp.png") - - im_l = im.convert("L") - assert im_l.info["transparency"] == 0 - im_l.save(f) - - im_rgb = im.convert("RGB") - assert im_rgb.info["transparency"] == (0, 0, 0) - im_rgb.save(f) - - -# ref https://github.com/python-pillow/Pillow/issues/664 - - -@pytest.mark.parametrize("mode", ("LA", "PA", "RGBA")) -def test_trns_p_transparency(mode: str) -> None: - # Arrange - im = hopper("P") - im.info["transparency"] = 128 - - # Act - converted_im = im.convert(mode) - - # Assert - assert "transparency" not in converted_im.info - if mode == "PA": - assert converted_im.palette is not None - else: - # https://github.com/python-pillow/Pillow/issues/2702 - assert converted_im.palette is None - - -def test_trns_l(tmp_path: Path) -> None: - im = hopper("L") - im.info["transparency"] = 128 - - f = str(tmp_path / "temp.png") - - im_la = im.convert("LA") - assert "transparency" not in im_la.info - im_la.save(f) - - im_rgb = im.convert("RGB") - assert im_rgb.info["transparency"] == (128, 128, 128) # undone - im_rgb.save(f) - - im_p = im.convert("P") - assert "transparency" in im_p.info - im_p.save(f) - - im_p = im.convert("P", palette=Image.Palette.ADAPTIVE) - assert "transparency" in im_p.info - im_p.save(f) - - -def test_trns_RGB(tmp_path: Path) -> None: - im = hopper("RGB") - im.info["transparency"] = im.getpixel((0, 0)) - - f = str(tmp_path / "temp.png") - - im_l = im.convert("L") - assert im_l.info["transparency"] == im_l.getpixel((0, 0)) # undone - im_l.save(f) - - im_la = im.convert("LA") - assert "transparency" not in im_la.info - im_la.save(f) - - im_la = im.convert("La") - assert "transparency" not in im_la.info - assert im_la.getpixel((0, 0)) == (0, 0) - - im_p = im.convert("P") - assert "transparency" in im_p.info - im_p.save(f) - - im_rgba = im.convert("RGBA") - assert "transparency" not in im_rgba.info - im_rgba.save(f) - - im_rgba = im.convert("RGBa") - 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) - assert "transparency" not in im_p.info - im_p.save(f) - - im = Image.new("RGB", (1, 1)) - im.info["transparency"] = im.getpixel((0, 0)) - im_p = im.convert("P", palette=Image.Palette.ADAPTIVE) - assert im_p.info["transparency"] == im_p.getpixel((0, 0)) - im_p.save(f) - - -@pytest.mark.parametrize("convert_mode", ("L", "LA", "I")) -def test_l_macro_rounding(convert_mode: str) -> None: - for mode in ("P", "PA"): - im = Image.new(mode, (1, 1)) - im.palette.getcolor((0, 1, 2)) - - converted_im = im.convert(convert_mode) - px = converted_im.load() - converted_color = px[0, 0] - if convert_mode == "LA": - converted_color = converted_color[0] - assert converted_color == 1 - - -def test_gif_with_rgba_palette_to_p() -> None: - # See https://github.com/python-pillow/Pillow/issues/2433 - with Image.open("Tests/images/hopper.gif") as im: - im.info["transparency"] = 255 - im.load() - assert im.palette.mode == "RGB" - im_p = im.convert("P") - - # Should not raise ValueError: unrecognized raw mode - im_p.load() - - -def test_p_la() -> None: - im = hopper("RGBA") - alpha = hopper("L") - im.putalpha(alpha) - - comparable = im.convert("P").convert("LA").getchannel("A") - - assert_image_similar(alpha, comparable, 5) - - -def test_p2pa_alpha() -> None: - with Image.open("Tests/images/tiny.png") as im: - assert im.mode == "P" - - im_pa = im.convert("PA") - assert im_pa.mode == "PA" - - im_a = im_pa.getchannel("A") - for x in range(4): - alpha = 255 if x > 1 else 0 - for y in range(4): - assert im_a.getpixel((x, y)) == alpha - - -def test_p2pa_palette() -> None: - with Image.open("Tests/images/tiny.png") as im: - im_pa = im.convert("PA") - assert im_pa.getpalette() == im.getpalette() - - -def test_matrix_illegal_conversion() -> None: - # Arrange - im = hopper("CMYK") - # fmt: off - matrix = ( - 0.412453, 0.357580, 0.180423, 0, - 0.212671, 0.715160, 0.072169, 0, - 0.019334, 0.119193, 0.950227, 0) - # fmt: on - assert im.mode != "RGB" - - # Act / Assert - with pytest.raises(ValueError): - im.convert(mode="CMYK", matrix=matrix) - - -def test_matrix_wrong_mode() -> None: - # Arrange - im = hopper("L") - # fmt: off - matrix = ( - 0.412453, 0.357580, 0.180423, 0, - 0.212671, 0.715160, 0.072169, 0, - 0.019334, 0.119193, 0.950227, 0) - # fmt: on - assert im.mode == "L" - - # Act / Assert - with pytest.raises(ValueError): - im.convert(mode="L", matrix=matrix) - - -@pytest.mark.parametrize("mode", ("RGB", "L")) -def test_matrix_xyz(mode: str) -> None: - # Arrange - im = hopper("RGB") - im.info["transparency"] = (255, 0, 0) - # fmt: off - matrix = ( - 0.412453, 0.357580, 0.180423, 0, - 0.212671, 0.715160, 0.072169, 0, - 0.019334, 0.119193, 0.950227, 0) - # fmt: on - assert im.mode == "RGB" - - # Act - # Convert an RGB image to the CIE XYZ colour space - converted_im = im.convert(mode=mode, matrix=matrix) - - # Assert - assert converted_im.mode == mode - assert converted_im.size == im.size - with Image.open("Tests/images/hopper-XYZ.png") as target: - if converted_im.mode == "RGB": - assert_image_similar(converted_im, target, 3) - assert converted_im.info["transparency"] == (105, 54, 4) - else: - assert_image_similar(converted_im, target.getchannel(0), 1) - assert converted_im.info["transparency"] == 105 - - -def test_matrix_identity() -> None: - # Arrange - im = hopper("RGB") - # fmt: off - identity_matrix = ( - 1, 0, 0, 0, - 0, 1, 0, 0, - 0, 0, 1, 0) - # fmt: on - assert im.mode == "RGB" - - # Act - # Convert with an identity matrix - converted_im = im.convert(mode="RGB", matrix=identity_matrix) - - # Assert - # No change - assert_image_equal(converted_im, im) +from __future__ import annotations + +from pathlib import Path + +import pytest + +from PIL import Image + +from .helper import assert_image, assert_image_equal, assert_image_similar, hopper + + +def test_sanity() -> None: + def convert(im: Image.Image, mode: str) -> None: + out = im.convert(mode) + assert out.mode == mode + assert out.size == im.size + + modes = ( + "1", + "L", + "LA", + "P", + "PA", + "I", + "F", + "RGB", + "RGBA", + "RGBX", + "CMYK", + "YCbCr", + "HSV", + ) + + for input_mode in modes: + im = hopper(input_mode) + for output_mode in modes: + convert(im, output_mode) + + # Check 0 + im = Image.new(input_mode, (0, 0)) + for output_mode in modes: + convert(im, output_mode) + + +def test_unsupported_conversion() -> None: + im = hopper() + with pytest.raises(ValueError): + im.convert("INVALID") + + +def test_default() -> None: + im = hopper("P") + assert im.mode == "P" + converted_im = im.convert() + assert_image(converted_im, "RGB", im.size) + converted_im = im.convert() + assert_image(converted_im, "RGB", im.size) + + im.info["transparency"] = 0 + converted_im = im.convert() + assert_image(converted_im, "RGBA", im.size) + + +# ref https://github.com/python-pillow/Pillow/issues/274 + + +def _test_float_conversion(im: Image.Image) -> None: + orig = im.getpixel((5, 5)) + converted = im.convert("F").getpixel((5, 5)) + assert orig == converted + + +def test_8bit() -> None: + with Image.open("Tests/images/hopper.jpg") as im: + _test_float_conversion(im.convert("L")) + + +def test_16bit() -> None: + with Image.open("Tests/images/16bit.cropped.tif") as im: + _test_float_conversion(im) + + for color in (65535, 65536): + im = Image.new("I", (1, 1), color) + im_i16 = im.convert("I;16") + assert im_i16.getpixel((0, 0)) == 65535 + + +def test_16bit_workaround() -> None: + with Image.open("Tests/images/16bit.cropped.tif") as im: + _test_float_conversion(im.convert("I")) + + +def test_opaque() -> None: + alpha = hopper("P").convert("PA").getchannel("A") + + solid = Image.new("L", (128, 128), 255) + assert_image_equal(alpha, solid) + + +def test_rgba_p() -> None: + im = hopper("RGBA") + im.putalpha(hopper("L")) + + converted = im.convert("P") + comparable = converted.convert("RGBA") + + assert_image_similar(im, comparable, 20) + + +def test_rgba() -> None: + with Image.open("Tests/images/transparent.png") as im: + assert im.mode == "RGBA" + + assert_image_similar(im.convert("RGBa").convert("RGB"), im.convert("RGB"), 1.5) + + +def test_trns_p(tmp_path: Path) -> None: + im = hopper("P") + im.info["transparency"] = 0 + + f = str(tmp_path / "temp.png") + + im_l = im.convert("L") + assert im_l.info["transparency"] == 0 + im_l.save(f) + + im_rgb = im.convert("RGB") + assert im_rgb.info["transparency"] == (0, 0, 0) + im_rgb.save(f) + + +# ref https://github.com/python-pillow/Pillow/issues/664 + + +@pytest.mark.parametrize("mode", ("LA", "PA", "RGBA")) +def test_trns_p_transparency(mode: str) -> None: + # Arrange + im = hopper("P") + im.info["transparency"] = 128 + + # Act + converted_im = im.convert(mode) + + # Assert + assert "transparency" not in converted_im.info + if mode == "PA": + assert converted_im.palette is not None + else: + # https://github.com/python-pillow/Pillow/issues/2702 + assert converted_im.palette is None + + +def test_trns_l(tmp_path: Path) -> None: + im = hopper("L") + im.info["transparency"] = 128 + + f = str(tmp_path / "temp.png") + + im_la = im.convert("LA") + assert "transparency" not in im_la.info + im_la.save(f) + + im_rgb = im.convert("RGB") + assert im_rgb.info["transparency"] == (128, 128, 128) # undone + im_rgb.save(f) + + im_p = im.convert("P") + assert "transparency" in im_p.info + im_p.save(f) + + im_p = im.convert("P", palette=Image.Palette.ADAPTIVE) + assert "transparency" in im_p.info + im_p.save(f) + + +def test_trns_RGB(tmp_path: Path) -> None: + im = hopper("RGB") + im.info["transparency"] = im.getpixel((0, 0)) + + f = str(tmp_path / "temp.png") + + im_l = im.convert("L") + assert im_l.info["transparency"] == im_l.getpixel((0, 0)) # undone + im_l.save(f) + + im_la = im.convert("LA") + assert "transparency" not in im_la.info + im_la.save(f) + + im_la = im.convert("La") + assert "transparency" not in im_la.info + assert im_la.getpixel((0, 0)) == (0, 0) + + im_p = im.convert("P") + assert "transparency" in im_p.info + im_p.save(f) + + im_rgba = im.convert("RGBA") + assert "transparency" not in im_rgba.info + im_rgba.save(f) + + im_rgba = im.convert("RGBa") + 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) + assert "transparency" not in im_p.info + im_p.save(f) + + im = Image.new("RGB", (1, 1)) + im.info["transparency"] = im.getpixel((0, 0)) + im_p = im.convert("P", palette=Image.Palette.ADAPTIVE) + assert im_p.info["transparency"] == im_p.getpixel((0, 0)) + im_p.save(f) + + +@pytest.mark.parametrize("convert_mode", ("L", "LA", "I")) +def test_l_macro_rounding(convert_mode: str) -> None: + for mode in ("P", "PA"): + im = Image.new(mode, (1, 1)) + im.palette.getcolor((0, 1, 2)) + + converted_im = im.convert(convert_mode) + px = converted_im.load() + converted_color = px[0, 0] + if convert_mode == "LA": + converted_color = converted_color[0] + assert converted_color == 1 + + +def test_gif_with_rgba_palette_to_p() -> None: + # See https://github.com/python-pillow/Pillow/issues/2433 + with Image.open("Tests/images/hopper.gif") as im: + im.info["transparency"] = 255 + im.load() + assert im.palette.mode == "RGB" + im_p = im.convert("P") + + # Should not raise ValueError: unrecognized raw mode + im_p.load() + + +def test_p_la() -> None: + im = hopper("RGBA") + alpha = hopper("L") + im.putalpha(alpha) + + comparable = im.convert("P").convert("LA").getchannel("A") + + assert_image_similar(alpha, comparable, 5) + + +def test_p2pa_alpha() -> None: + with Image.open("Tests/images/tiny.png") as im: + assert im.mode == "P" + + im_pa = im.convert("PA") + assert im_pa.mode == "PA" + + im_a = im_pa.getchannel("A") + for x in range(4): + alpha = 255 if x > 1 else 0 + for y in range(4): + assert im_a.getpixel((x, y)) == alpha + + +def test_p2pa_palette() -> None: + with Image.open("Tests/images/tiny.png") as im: + im_pa = im.convert("PA") + assert im_pa.getpalette() == im.getpalette() + + +def test_matrix_illegal_conversion() -> None: + # Arrange + im = hopper("CMYK") + # fmt: off + matrix = ( + 0.412453, 0.357580, 0.180423, 0, + 0.212671, 0.715160, 0.072169, 0, + 0.019334, 0.119193, 0.950227, 0) + # fmt: on + assert im.mode != "RGB" + + # Act / Assert + with pytest.raises(ValueError): + im.convert(mode="CMYK", matrix=matrix) + + +def test_matrix_wrong_mode() -> None: + # Arrange + im = hopper("L") + # fmt: off + matrix = ( + 0.412453, 0.357580, 0.180423, 0, + 0.212671, 0.715160, 0.072169, 0, + 0.019334, 0.119193, 0.950227, 0) + # fmt: on + assert im.mode == "L" + + # Act / Assert + with pytest.raises(ValueError): + im.convert(mode="L", matrix=matrix) + + +@pytest.mark.parametrize("mode", ("RGB", "L")) +def test_matrix_xyz(mode: str) -> None: + # Arrange + im = hopper("RGB") + im.info["transparency"] = (255, 0, 0) + # fmt: off + matrix = ( + 0.412453, 0.357580, 0.180423, 0, + 0.212671, 0.715160, 0.072169, 0, + 0.019334, 0.119193, 0.950227, 0) + # fmt: on + assert im.mode == "RGB" + + # Act + # Convert an RGB image to the CIE XYZ colour space + converted_im = im.convert(mode=mode, matrix=matrix) + + # Assert + assert converted_im.mode == mode + assert converted_im.size == im.size + with Image.open("Tests/images/hopper-XYZ.png") as target: + if converted_im.mode == "RGB": + assert_image_similar(converted_im, target, 3) + assert converted_im.info["transparency"] == (105, 54, 4) + else: + assert_image_similar(converted_im, target.getchannel(0), 1) + assert converted_im.info["transparency"] == 105 + + +def test_matrix_identity() -> None: + # Arrange + im = hopper("RGB") + # fmt: off + identity_matrix = ( + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0) + # fmt: on + assert im.mode == "RGB" + + # Act + # Convert with an identity matrix + converted_im = im.convert(mode="RGB", matrix=identity_matrix) + + # Assert + # No change + assert_image_equal(converted_im, im) diff --git a/Tests/test_image_copy.py b/Tests/test_image_copy.py index 027e5338b..d2bad4619 100644 --- a/Tests/test_image_copy.py +++ b/Tests/test_image_copy.py @@ -1,53 +1,53 @@ -from __future__ import annotations - -import copy - -import pytest - -from PIL import Image - -from .helper import hopper, skip_unless_feature - - -@pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F")) -def test_copy(mode: str) -> None: - cropped_coordinates = (10, 10, 20, 20) - cropped_size = (10, 10) - - # Internal copy method - im = hopper(mode) - out = im.copy() - assert out.mode == im.mode - assert out.size == im.size - - # Python's copy method - im = hopper(mode) - out = copy.copy(im) - assert out.mode == im.mode - assert out.size == im.size - - # Internal copy method on a cropped image - im = hopper(mode) - out = im.crop(cropped_coordinates).copy() - assert out.mode == im.mode - assert out.size == cropped_size - - # Python's copy method on a cropped image - im = hopper(mode) - out = copy.copy(im.crop(cropped_coordinates)) - assert out.mode == im.mode - assert out.size == cropped_size - - -def test_copy_zero() -> None: - im = Image.new("RGB", (0, 0)) - out = im.copy() - assert out.mode == im.mode - assert out.size == im.size - - -@skip_unless_feature("libtiff") -def test_deepcopy() -> None: - with Image.open("Tests/images/g4_orientation_5.tif") as im: - out = copy.deepcopy(im) - assert out.size == (590, 88) +from __future__ import annotations + +import copy + +import pytest + +from PIL import Image + +from .helper import hopper, skip_unless_feature + + +@pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F")) +def test_copy(mode: str) -> None: + cropped_coordinates = (10, 10, 20, 20) + cropped_size = (10, 10) + + # Internal copy method + im = hopper(mode) + out = im.copy() + assert out.mode == im.mode + assert out.size == im.size + + # Python's copy method + im = hopper(mode) + out = copy.copy(im) + assert out.mode == im.mode + assert out.size == im.size + + # Internal copy method on a cropped image + im = hopper(mode) + out = im.crop(cropped_coordinates).copy() + assert out.mode == im.mode + assert out.size == cropped_size + + # Python's copy method on a cropped image + im = hopper(mode) + out = copy.copy(im.crop(cropped_coordinates)) + assert out.mode == im.mode + assert out.size == cropped_size + + +def test_copy_zero() -> None: + im = Image.new("RGB", (0, 0)) + out = im.copy() + assert out.mode == im.mode + assert out.size == im.size + + +@skip_unless_feature("libtiff") +def test_deepcopy() -> None: + with Image.open("Tests/images/g4_orientation_5.tif") as im: + out = copy.deepcopy(im) + assert out.size == (590, 88) diff --git a/Tests/test_image_crop.py b/Tests/test_image_crop.py index 07fec2e64..d95a87743 100644 --- a/Tests/test_image_crop.py +++ b/Tests/test_image_crop.py @@ -1,104 +1,104 @@ -from __future__ import annotations - -import pytest - -from PIL import Image - -from .helper import assert_image_equal, hopper - - -@pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F")) -def test_crop(mode: str) -> None: - im = hopper(mode) - assert_image_equal(im.crop(), im) - - cropped = im.crop((50, 50, 100, 100)) - assert cropped.mode == mode - assert cropped.size == (50, 50) - - -def test_wide_crop() -> None: - def crop(bbox: tuple[int, int, int, int]) -> tuple[int, ...]: - i = im.crop(bbox) - h = i.histogram() - while h and not h[-1]: - del h[-1] - return tuple(h) - - im = Image.new("L", (100, 100), 1) - - assert crop((0, 0, 100, 100)) == (0, 10000) - assert crop((25, 25, 75, 75)) == (0, 2500) - - # sides - assert crop((-25, 0, 25, 50)) == (1250, 1250) - assert crop((0, -25, 50, 25)) == (1250, 1250) - assert crop((75, 0, 125, 50)) == (1250, 1250) - assert crop((0, 75, 50, 125)) == (1250, 1250) - - assert crop((-25, 25, 125, 75)) == (2500, 5000) - assert crop((25, -25, 75, 125)) == (2500, 5000) - - # corners - assert crop((-25, -25, 25, 25)) == (1875, 625) - assert crop((75, -25, 125, 25)) == (1875, 625) - assert crop((75, 75, 125, 125)) == (1875, 625) - assert crop((-25, 75, 25, 125)) == (1875, 625) - - -@pytest.mark.parametrize("box", ((8, 2, 2, 8), (2, 8, 8, 2), (8, 8, 2, 2))) -def test_negative_crop(box: tuple[int, int, int, int]) -> None: - im = Image.new("RGB", (10, 10)) - - with pytest.raises(ValueError): - im.crop(box) - - -def test_crop_float() -> None: - # Check cropping floats are rounded to nearest integer - # https://github.com/python-pillow/Pillow/issues/1744 - - # Arrange - im = Image.new("RGB", (10, 10)) - assert im.size == (10, 10) - - # Act - cropped = im.crop((0.9, 1.1, 4.2, 5.8)) - - # Assert - assert cropped.size == (3, 5) - - -def test_crop_crash() -> None: - # Image.crop crashes prepatch with an access violation - # apparently a use after free on Windows, see - # https://github.com/python-pillow/Pillow/issues/1077 - - test_img = "Tests/images/bmp/g/pal8-0.bmp" - extents = (1, 1, 10, 10) - # works prepatch - with Image.open(test_img) as img: - img2 = img.crop(extents) - img2.load() - - # fail prepatch - with Image.open(test_img) as img: - img = img.crop(extents) - img.load() - - -def test_crop_zero() -> None: - im = Image.new("RGB", (0, 0), "white") - - cropped = im.crop((0, 0, 0, 0)) - assert cropped.size == (0, 0) - - cropped = im.crop((10, 10, 20, 20)) - assert cropped.size == (10, 10) - assert cropped.getdata()[0] == (0, 0, 0) - - im = Image.new("RGB", (0, 0)) - - cropped = im.crop((10, 10, 20, 20)) - assert cropped.size == (10, 10) - assert cropped.getdata()[2] == (0, 0, 0) +from __future__ import annotations + +import pytest + +from PIL import Image + +from .helper import assert_image_equal, hopper + + +@pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F")) +def test_crop(mode: str) -> None: + im = hopper(mode) + assert_image_equal(im.crop(), im) + + cropped = im.crop((50, 50, 100, 100)) + assert cropped.mode == mode + assert cropped.size == (50, 50) + + +def test_wide_crop() -> None: + def crop(bbox: tuple[int, int, int, int]) -> tuple[int, ...]: + i = im.crop(bbox) + h = i.histogram() + while h and not h[-1]: + del h[-1] + return tuple(h) + + im = Image.new("L", (100, 100), 1) + + assert crop((0, 0, 100, 100)) == (0, 10000) + assert crop((25, 25, 75, 75)) == (0, 2500) + + # sides + assert crop((-25, 0, 25, 50)) == (1250, 1250) + assert crop((0, -25, 50, 25)) == (1250, 1250) + assert crop((75, 0, 125, 50)) == (1250, 1250) + assert crop((0, 75, 50, 125)) == (1250, 1250) + + assert crop((-25, 25, 125, 75)) == (2500, 5000) + assert crop((25, -25, 75, 125)) == (2500, 5000) + + # corners + assert crop((-25, -25, 25, 25)) == (1875, 625) + assert crop((75, -25, 125, 25)) == (1875, 625) + assert crop((75, 75, 125, 125)) == (1875, 625) + assert crop((-25, 75, 25, 125)) == (1875, 625) + + +@pytest.mark.parametrize("box", ((8, 2, 2, 8), (2, 8, 8, 2), (8, 8, 2, 2))) +def test_negative_crop(box: tuple[int, int, int, int]) -> None: + im = Image.new("RGB", (10, 10)) + + with pytest.raises(ValueError): + im.crop(box) + + +def test_crop_float() -> None: + # Check cropping floats are rounded to nearest integer + # https://github.com/python-pillow/Pillow/issues/1744 + + # Arrange + im = Image.new("RGB", (10, 10)) + assert im.size == (10, 10) + + # Act + cropped = im.crop((0.9, 1.1, 4.2, 5.8)) + + # Assert + assert cropped.size == (3, 5) + + +def test_crop_crash() -> None: + # Image.crop crashes prepatch with an access violation + # apparently a use after free on Windows, see + # https://github.com/python-pillow/Pillow/issues/1077 + + test_img = "Tests/images/bmp/g/pal8-0.bmp" + extents = (1, 1, 10, 10) + # works prepatch + with Image.open(test_img) as img: + img2 = img.crop(extents) + img2.load() + + # fail prepatch + with Image.open(test_img) as img: + img = img.crop(extents) + img.load() + + +def test_crop_zero() -> None: + im = Image.new("RGB", (0, 0), "white") + + cropped = im.crop((0, 0, 0, 0)) + assert cropped.size == (0, 0) + + cropped = im.crop((10, 10, 20, 20)) + assert cropped.size == (10, 10) + assert cropped.getdata()[0] == (0, 0, 0) + + im = Image.new("RGB", (0, 0)) + + cropped = im.crop((10, 10, 20, 20)) + assert cropped.size == (10, 10) + assert cropped.getdata()[2] == (0, 0, 0) diff --git a/Tests/test_image_draft.py b/Tests/test_image_draft.py index 1ce1a7cd8..5b03c5ba5 100644 --- a/Tests/test_image_draft.py +++ b/Tests/test_image_draft.py @@ -1,79 +1,79 @@ -from __future__ import annotations - -from PIL import Image - -from .helper import fromstring, skip_unless_feature, tostring - -pytestmark = skip_unless_feature("jpg") - - -def draft_roundtrip( - in_mode: str, - in_size: tuple[int, int], - req_mode: str | None, - req_size: tuple[int, int] | None, -) -> Image.Image: - im = Image.new(in_mode, in_size) - data = tostring(im, "JPEG") - im = fromstring(data) - mode, box = im.draft(req_mode, req_size) - scale, _ = im.decoderconfig - assert box[:2] == (0, 0) - assert (im.width - scale) < box[2] <= im.width - assert (im.height - scale) < box[3] <= im.height - return im - - -def test_size() -> None: - for in_size, req_size, out_size in [ - ((435, 361), (2048, 2048), (435, 361)), # bigger - ((435, 361), (435, 361), (435, 361)), # same - ((128, 128), (64, 64), (64, 64)), - ((128, 128), (32, 32), (32, 32)), - ((128, 128), (16, 16), (16, 16)), - # large requested width - ((435, 361), (218, 128), (435, 361)), # almost 2x - ((435, 361), (217, 128), (218, 181)), # more than 2x - ((435, 361), (109, 64), (218, 181)), # almost 4x - ((435, 361), (108, 64), (109, 91)), # more than 4x - ((435, 361), (55, 32), (109, 91)), # almost 8x - ((435, 361), (54, 32), (55, 46)), # more than 8x - ((435, 361), (27, 16), (55, 46)), # more than 16x - # and vice versa - ((435, 361), (128, 181), (435, 361)), # almost 2x - ((435, 361), (128, 180), (218, 181)), # more than 2x - ((435, 361), (64, 91), (218, 181)), # almost 4x - ((435, 361), (64, 90), (109, 91)), # more than 4x - ((435, 361), (32, 46), (109, 91)), # almost 8x - ((435, 361), (32, 45), (55, 46)), # more than 8x - ((435, 361), (16, 22), (55, 46)), # more than 16x - ]: - im = draft_roundtrip("L", in_size, None, req_size) - im.load() - assert im.size == out_size - - -def test_mode() -> None: - for in_mode, req_mode, out_mode in [ - ("RGB", "1", "RGB"), - ("RGB", "L", "L"), - ("RGB", "RGB", "RGB"), - ("RGB", "YCbCr", "YCbCr"), - ("L", "1", "L"), - ("L", "L", "L"), - ("L", "RGB", "L"), - ("L", "YCbCr", "L"), - ("CMYK", "1", "CMYK"), - ("CMYK", "L", "CMYK"), - ("CMYK", "RGB", "CMYK"), - ("CMYK", "YCbCr", "CMYK"), - ]: - im = draft_roundtrip(in_mode, (64, 64), req_mode, None) - im.load() - assert im.mode == out_mode - - -def test_several_drafts() -> None: - im = draft_roundtrip("L", (128, 128), None, (64, 64)) - im.draft(None, (64, 64)) - im.load() +from __future__ import annotations + +from PIL import Image + +from .helper import fromstring, skip_unless_feature, tostring + +pytestmark = skip_unless_feature("jpg") + + +def draft_roundtrip( + in_mode: str, + in_size: tuple[int, int], + req_mode: str | None, + req_size: tuple[int, int] | None, +) -> Image.Image: + im = Image.new(in_mode, in_size) + data = tostring(im, "JPEG") + im = fromstring(data) + mode, box = im.draft(req_mode, req_size) + scale, _ = im.decoderconfig + assert box[:2] == (0, 0) + assert (im.width - scale) < box[2] <= im.width + assert (im.height - scale) < box[3] <= im.height + return im + + +def test_size() -> None: + for in_size, req_size, out_size in [ + ((435, 361), (2048, 2048), (435, 361)), # bigger + ((435, 361), (435, 361), (435, 361)), # same + ((128, 128), (64, 64), (64, 64)), + ((128, 128), (32, 32), (32, 32)), + ((128, 128), (16, 16), (16, 16)), + # large requested width + ((435, 361), (218, 128), (435, 361)), # almost 2x + ((435, 361), (217, 128), (218, 181)), # more than 2x + ((435, 361), (109, 64), (218, 181)), # almost 4x + ((435, 361), (108, 64), (109, 91)), # more than 4x + ((435, 361), (55, 32), (109, 91)), # almost 8x + ((435, 361), (54, 32), (55, 46)), # more than 8x + ((435, 361), (27, 16), (55, 46)), # more than 16x + # and vice versa + ((435, 361), (128, 181), (435, 361)), # almost 2x + ((435, 361), (128, 180), (218, 181)), # more than 2x + ((435, 361), (64, 91), (218, 181)), # almost 4x + ((435, 361), (64, 90), (109, 91)), # more than 4x + ((435, 361), (32, 46), (109, 91)), # almost 8x + ((435, 361), (32, 45), (55, 46)), # more than 8x + ((435, 361), (16, 22), (55, 46)), # more than 16x + ]: + im = draft_roundtrip("L", in_size, None, req_size) + im.load() + assert im.size == out_size + + +def test_mode() -> None: + for in_mode, req_mode, out_mode in [ + ("RGB", "1", "RGB"), + ("RGB", "L", "L"), + ("RGB", "RGB", "RGB"), + ("RGB", "YCbCr", "YCbCr"), + ("L", "1", "L"), + ("L", "L", "L"), + ("L", "RGB", "L"), + ("L", "YCbCr", "L"), + ("CMYK", "1", "CMYK"), + ("CMYK", "L", "CMYK"), + ("CMYK", "RGB", "CMYK"), + ("CMYK", "YCbCr", "CMYK"), + ]: + im = draft_roundtrip(in_mode, (64, 64), req_mode, None) + im.load() + assert im.mode == out_mode + + +def test_several_drafts() -> None: + im = draft_roundtrip("L", (128, 128), None, (64, 64)) + im.draft(None, (64, 64)) + im.load() diff --git a/Tests/test_image_entropy.py b/Tests/test_image_entropy.py index c1dbb879b..42b96ac99 100644 --- a/Tests/test_image_entropy.py +++ b/Tests/test_image_entropy.py @@ -1,18 +1,18 @@ -from __future__ import annotations - -from .helper import hopper - - -def test_entropy() -> None: - def entropy(mode: str) -> float: - return hopper(mode).entropy() - - assert round(abs(entropy("1") - 0.9138803254693582), 7) == 0 - assert round(abs(entropy("L") - 7.063008716585465), 7) == 0 - assert round(abs(entropy("I") - 7.063008716585465), 7) == 0 - assert round(abs(entropy("F") - 7.063008716585465), 7) == 0 - assert round(abs(entropy("P") - 5.082506854662517), 7) == 0 - assert round(abs(entropy("RGB") - 8.821286587714319), 7) == 0 - assert round(abs(entropy("RGBA") - 7.42724306524488), 7) == 0 - assert round(abs(entropy("CMYK") - 7.4272430652448795), 7) == 0 - assert round(abs(entropy("YCbCr") - 7.698360534903628), 7) == 0 +from __future__ import annotations + +from .helper import hopper + + +def test_entropy() -> None: + def entropy(mode: str) -> float: + return hopper(mode).entropy() + + assert round(abs(entropy("1") - 0.9138803254693582), 7) == 0 + assert round(abs(entropy("L") - 7.063008716585465), 7) == 0 + assert round(abs(entropy("I") - 7.063008716585465), 7) == 0 + assert round(abs(entropy("F") - 7.063008716585465), 7) == 0 + assert round(abs(entropy("P") - 5.082506854662517), 7) == 0 + assert round(abs(entropy("RGB") - 8.821286587714319), 7) == 0 + assert round(abs(entropy("RGBA") - 7.42724306524488), 7) == 0 + assert round(abs(entropy("CMYK") - 7.4272430652448795), 7) == 0 + assert round(abs(entropy("YCbCr") - 7.698360534903628), 7) == 0 diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index 1f0644471..ba42860d6 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -1,199 +1,199 @@ -from __future__ import annotations - -import pytest - -from PIL import Image, ImageFilter - -from .helper import assert_image_equal, hopper - - -@pytest.mark.parametrize( - "filter_to_apply", - ( - ImageFilter.BLUR, - ImageFilter.CONTOUR, - ImageFilter.DETAIL, - ImageFilter.EDGE_ENHANCE, - ImageFilter.EDGE_ENHANCE_MORE, - ImageFilter.EMBOSS, - ImageFilter.FIND_EDGES, - ImageFilter.SMOOTH, - ImageFilter.SMOOTH_MORE, - ImageFilter.SHARPEN, - ImageFilter.MaxFilter, - ImageFilter.MedianFilter, - ImageFilter.MinFilter, - ImageFilter.ModeFilter, - ImageFilter.GaussianBlur, - ImageFilter.GaussianBlur(0), - ImageFilter.GaussianBlur(5), - ImageFilter.GaussianBlur((2, 5)), - ImageFilter.BoxBlur(0), - ImageFilter.BoxBlur(5), - ImageFilter.BoxBlur((2, 5)), - ImageFilter.UnsharpMask, - ImageFilter.UnsharpMask(10), - ), -) -@pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK")) -def test_sanity(filter_to_apply: ImageFilter.Filter, mode: str) -> None: - im = hopper(mode) - if mode != "I" or isinstance(filter_to_apply, ImageFilter.BuiltinFilter): - out = im.filter(filter_to_apply) - assert out.mode == im.mode - assert out.size == im.size - - -@pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK")) -def test_sanity_error(mode: str) -> None: - im = hopper(mode) - with pytest.raises(TypeError): - im.filter("hello") # type: ignore[arg-type] - - -# crashes on small images -@pytest.mark.parametrize("size", ((1, 1), (2, 2), (3, 3))) -def test_crash(size: tuple[int, int]) -> None: - im = Image.new("RGB", size) - im.filter(ImageFilter.SMOOTH) - - -@pytest.mark.parametrize( - "mode, expected", - ( - ("1", (4, 0)), - ("L", (4, 0)), - ("P", (4, 0)), - ("RGB", ((4, 0, 0), (0, 0, 0))), - ), -) -def test_modefilter( - mode: str, - expected: tuple[int, int] | tuple[tuple[int, int, int], tuple[int, int, int]], -) -> None: - im = Image.new(mode, (3, 3), None) - im.putdata(list(range(9))) - # image is: - # 0 1 2 - # 3 4 5 - # 6 7 8 - mod = im.filter(ImageFilter.ModeFilter).getpixel((1, 1)) - im.putdata([0, 0, 1, 2, 5, 1, 5, 2, 0]) # mode=0 - mod2 = im.filter(ImageFilter.ModeFilter).getpixel((1, 1)) - assert (mod, mod2) == expected - - -@pytest.mark.parametrize( - "mode, expected", - ( - ("1", (0, 4, 8)), - ("L", (0, 4, 8)), - ("RGB", ((0, 0, 0), (4, 0, 0), (8, 0, 0))), - ("I", (0, 4, 8)), - ("F", (0.0, 4.0, 8.0)), - ), -) -def test_rankfilter( - mode: str, - expected: ( - tuple[float, float, float] - | tuple[tuple[int, int, int], tuple[int, int, int], tuple[int, int, int]] - ), -) -> None: - im = Image.new(mode, (3, 3), None) - im.putdata(list(range(9))) - # image is: - # 0 1 2 - # 3 4 5 - # 6 7 8 - minimum = im.filter(ImageFilter.MinFilter).getpixel((1, 1)) - med = im.filter(ImageFilter.MedianFilter).getpixel((1, 1)) - maximum = im.filter(ImageFilter.MaxFilter).getpixel((1, 1)) - assert (minimum, med, maximum) == expected - - -@pytest.mark.parametrize( - "filter", (ImageFilter.MinFilter, ImageFilter.MedianFilter, ImageFilter.MaxFilter) -) -def test_rankfilter_error(filter: ImageFilter.RankFilter) -> None: - with pytest.raises(ValueError): - im = Image.new("P", (3, 3), None) - im.putdata(list(range(9))) - # image is: - # 0 1 2 - # 3 4 5 - # 6 7 8 - im.filter(filter).getpixel((1, 1)) - - -def test_rankfilter_properties() -> None: - rankfilter = ImageFilter.RankFilter(1, 2) - - assert rankfilter.size == 1 - assert rankfilter.rank == 2 - - -def test_builtinfilter_p() -> None: - builtin_filter = ImageFilter.BuiltinFilter() - - with pytest.raises(ValueError): - builtin_filter.filter(hopper("P")) - - -def test_kernel_not_enough_coefficients() -> None: - with pytest.raises(ValueError): - ImageFilter.Kernel((3, 3), (0, 0)) - - -@pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK")) -def test_consistency_3x3(mode: str) -> None: - with Image.open("Tests/images/hopper.bmp") as source: - with Image.open("Tests/images/hopper_emboss.bmp") as reference: - kernel = ImageFilter.Kernel( - (3, 3), - # fmt: off - (-1, -1, 0, - -1, 0, 1, - 0, 1, 1), - # fmt: on - 0.3, - ) - assert_image_equal(source.filter(kernel), reference) - - -@pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK")) -def test_consistency_5x5(mode: str) -> None: - with Image.open("Tests/images/hopper.bmp") as source: - with Image.open("Tests/images/hopper_emboss_more.bmp") as reference: - kernel = ImageFilter.Kernel( - (5, 5), - # fmt: off - (-1, -1, -1, -1, 0, - -1, -1, -1, 0, 1, - -1, -1, 0, 1, 1, - -1, 0, 1, 1, 1, - 0, 1, 1, 1, 1), - # fmt: on - 0.3, - ) - assert_image_equal(source.filter(kernel), reference) - - -@pytest.mark.parametrize( - "radius", - ( - -2, - (-2, -2), - (-2, 2), - (2, -2), - ), -) -def test_invalid_box_blur_filter(radius: int | tuple[int, int]) -> None: - with pytest.raises(ValueError): - ImageFilter.BoxBlur(radius) - - im = hopper() - box_blur_filter = ImageFilter.BoxBlur(2) - box_blur_filter.radius = radius - with pytest.raises(ValueError): - im.filter(box_blur_filter) +from __future__ import annotations + +import pytest + +from PIL import Image, ImageFilter + +from .helper import assert_image_equal, hopper + + +@pytest.mark.parametrize( + "filter_to_apply", + ( + ImageFilter.BLUR, + ImageFilter.CONTOUR, + ImageFilter.DETAIL, + ImageFilter.EDGE_ENHANCE, + ImageFilter.EDGE_ENHANCE_MORE, + ImageFilter.EMBOSS, + ImageFilter.FIND_EDGES, + ImageFilter.SMOOTH, + ImageFilter.SMOOTH_MORE, + ImageFilter.SHARPEN, + ImageFilter.MaxFilter, + ImageFilter.MedianFilter, + ImageFilter.MinFilter, + ImageFilter.ModeFilter, + ImageFilter.GaussianBlur, + ImageFilter.GaussianBlur(0), + ImageFilter.GaussianBlur(5), + ImageFilter.GaussianBlur((2, 5)), + ImageFilter.BoxBlur(0), + ImageFilter.BoxBlur(5), + ImageFilter.BoxBlur((2, 5)), + ImageFilter.UnsharpMask, + ImageFilter.UnsharpMask(10), + ), +) +@pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK")) +def test_sanity(filter_to_apply: ImageFilter.Filter, mode: str) -> None: + im = hopper(mode) + if mode != "I" or isinstance(filter_to_apply, ImageFilter.BuiltinFilter): + out = im.filter(filter_to_apply) + assert out.mode == im.mode + assert out.size == im.size + + +@pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK")) +def test_sanity_error(mode: str) -> None: + im = hopper(mode) + with pytest.raises(TypeError): + im.filter("hello") # type: ignore[arg-type] + + +# crashes on small images +@pytest.mark.parametrize("size", ((1, 1), (2, 2), (3, 3))) +def test_crash(size: tuple[int, int]) -> None: + im = Image.new("RGB", size) + im.filter(ImageFilter.SMOOTH) + + +@pytest.mark.parametrize( + "mode, expected", + ( + ("1", (4, 0)), + ("L", (4, 0)), + ("P", (4, 0)), + ("RGB", ((4, 0, 0), (0, 0, 0))), + ), +) +def test_modefilter( + mode: str, + expected: tuple[int, int] | tuple[tuple[int, int, int], tuple[int, int, int]], +) -> None: + im = Image.new(mode, (3, 3), None) + im.putdata(list(range(9))) + # image is: + # 0 1 2 + # 3 4 5 + # 6 7 8 + mod = im.filter(ImageFilter.ModeFilter).getpixel((1, 1)) + im.putdata([0, 0, 1, 2, 5, 1, 5, 2, 0]) # mode=0 + mod2 = im.filter(ImageFilter.ModeFilter).getpixel((1, 1)) + assert (mod, mod2) == expected + + +@pytest.mark.parametrize( + "mode, expected", + ( + ("1", (0, 4, 8)), + ("L", (0, 4, 8)), + ("RGB", ((0, 0, 0), (4, 0, 0), (8, 0, 0))), + ("I", (0, 4, 8)), + ("F", (0.0, 4.0, 8.0)), + ), +) +def test_rankfilter( + mode: str, + expected: ( + tuple[float, float, float] + | tuple[tuple[int, int, int], tuple[int, int, int], tuple[int, int, int]] + ), +) -> None: + im = Image.new(mode, (3, 3), None) + im.putdata(list(range(9))) + # image is: + # 0 1 2 + # 3 4 5 + # 6 7 8 + minimum = im.filter(ImageFilter.MinFilter).getpixel((1, 1)) + med = im.filter(ImageFilter.MedianFilter).getpixel((1, 1)) + maximum = im.filter(ImageFilter.MaxFilter).getpixel((1, 1)) + assert (minimum, med, maximum) == expected + + +@pytest.mark.parametrize( + "filter", (ImageFilter.MinFilter, ImageFilter.MedianFilter, ImageFilter.MaxFilter) +) +def test_rankfilter_error(filter: ImageFilter.RankFilter) -> None: + with pytest.raises(ValueError): + im = Image.new("P", (3, 3), None) + im.putdata(list(range(9))) + # image is: + # 0 1 2 + # 3 4 5 + # 6 7 8 + im.filter(filter).getpixel((1, 1)) + + +def test_rankfilter_properties() -> None: + rankfilter = ImageFilter.RankFilter(1, 2) + + assert rankfilter.size == 1 + assert rankfilter.rank == 2 + + +def test_builtinfilter_p() -> None: + builtin_filter = ImageFilter.BuiltinFilter() + + with pytest.raises(ValueError): + builtin_filter.filter(hopper("P")) + + +def test_kernel_not_enough_coefficients() -> None: + with pytest.raises(ValueError): + ImageFilter.Kernel((3, 3), (0, 0)) + + +@pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK")) +def test_consistency_3x3(mode: str) -> None: + with Image.open("Tests/images/hopper.bmp") as source: + with Image.open("Tests/images/hopper_emboss.bmp") as reference: + kernel = ImageFilter.Kernel( + (3, 3), + # fmt: off + (-1, -1, 0, + -1, 0, 1, + 0, 1, 1), + # fmt: on + 0.3, + ) + assert_image_equal(source.filter(kernel), reference) + + +@pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK")) +def test_consistency_5x5(mode: str) -> None: + with Image.open("Tests/images/hopper.bmp") as source: + with Image.open("Tests/images/hopper_emboss_more.bmp") as reference: + kernel = ImageFilter.Kernel( + (5, 5), + # fmt: off + (-1, -1, -1, -1, 0, + -1, -1, -1, 0, 1, + -1, -1, 0, 1, 1, + -1, 0, 1, 1, 1, + 0, 1, 1, 1, 1), + # fmt: on + 0.3, + ) + assert_image_equal(source.filter(kernel), reference) + + +@pytest.mark.parametrize( + "radius", + ( + -2, + (-2, -2), + (-2, 2), + (2, -2), + ), +) +def test_invalid_box_blur_filter(radius: int | tuple[int, int]) -> None: + with pytest.raises(ValueError): + ImageFilter.BoxBlur(radius) + + im = hopper() + box_blur_filter = ImageFilter.BoxBlur(2) + box_blur_filter.radius = radius + with pytest.raises(ValueError): + im.filter(box_blur_filter) diff --git a/Tests/test_image_frombytes.py b/Tests/test_image_frombytes.py index 98c0ea0b4..c191c0a6f 100644 --- a/Tests/test_image_frombytes.py +++ b/Tests/test_image_frombytes.py @@ -1,19 +1,19 @@ -from __future__ import annotations - -import pytest - -from PIL import Image - -from .helper import assert_image_equal, hopper - - -@pytest.mark.parametrize("data_type", ("bytes", "memoryview")) -def test_sanity(data_type: str) -> None: - im1 = hopper() - - data = im1.tobytes() - if data_type == "memoryview": - data = memoryview(data) - im2 = Image.frombytes(im1.mode, im1.size, data) - - assert_image_equal(im1, im2) +from __future__ import annotations + +import pytest + +from PIL import Image + +from .helper import assert_image_equal, hopper + + +@pytest.mark.parametrize("data_type", ("bytes", "memoryview")) +def test_sanity(data_type: str) -> None: + im1 = hopper() + + data = im1.tobytes() + if data_type == "memoryview": + data = memoryview(data) + im2 = Image.frombytes(im1.mode, im1.size, data) + + assert_image_equal(im1, im2) diff --git a/Tests/test_image_fromqimage.py b/Tests/test_image_fromqimage.py index 1149e2964..a725d89c7 100644 --- a/Tests/test_image_fromqimage.py +++ b/Tests/test_image_fromqimage.py @@ -1,67 +1,67 @@ -from __future__ import annotations - -import warnings - -import pytest - -from PIL import Image - -with warnings.catch_warnings(): - warnings.simplefilter("ignore", category=DeprecationWarning) - from PIL import ImageQt - -from .helper import assert_image_equal, hopper - -pytestmark = pytest.mark.skipif( - not ImageQt.qt_is_installed, reason="Qt bindings are not installed" -) - -ims: list[Image.Image] = [] - - -def setup_module() -> None: - ims.append(hopper()) - ims.append(Image.open("Tests/images/transparent.png")) - ims.append(Image.open("Tests/images/7x13.png")) - - -def teardown_module() -> None: - for im in ims: - im.close() - - -def roundtrip(expected: Image.Image) -> None: - # PIL -> Qt - intermediate = expected.toqimage() - # Qt -> PIL - result = ImageQt.fromqimage(intermediate) - - if intermediate.hasAlphaChannel(): - assert_image_equal(result, expected.convert("RGBA")) - else: - assert_image_equal(result, expected.convert("RGB")) - - -def test_sanity_1() -> None: - for im in ims: - roundtrip(im.convert("1")) - - -def test_sanity_rgb() -> None: - for im in ims: - roundtrip(im.convert("RGB")) - - -def test_sanity_rgba() -> None: - for im in ims: - roundtrip(im.convert("RGBA")) - - -def test_sanity_l() -> None: - for im in ims: - roundtrip(im.convert("L")) - - -def test_sanity_p() -> None: - for im in ims: - roundtrip(im.convert("P")) +from __future__ import annotations + +import warnings + +import pytest + +from PIL import Image + +with warnings.catch_warnings(): + warnings.simplefilter("ignore", category=DeprecationWarning) + from PIL import ImageQt + +from .helper import assert_image_equal, hopper + +pytestmark = pytest.mark.skipif( + not ImageQt.qt_is_installed, reason="Qt bindings are not installed" +) + +ims: list[Image.Image] = [] + + +def setup_module() -> None: + ims.append(hopper()) + ims.append(Image.open("Tests/images/transparent.png")) + ims.append(Image.open("Tests/images/7x13.png")) + + +def teardown_module() -> None: + for im in ims: + im.close() + + +def roundtrip(expected: Image.Image) -> None: + # PIL -> Qt + intermediate = expected.toqimage() + # Qt -> PIL + result = ImageQt.fromqimage(intermediate) + + if intermediate.hasAlphaChannel(): + assert_image_equal(result, expected.convert("RGBA")) + else: + assert_image_equal(result, expected.convert("RGB")) + + +def test_sanity_1() -> None: + for im in ims: + roundtrip(im.convert("1")) + + +def test_sanity_rgb() -> None: + for im in ims: + roundtrip(im.convert("RGB")) + + +def test_sanity_rgba() -> None: + for im in ims: + roundtrip(im.convert("RGBA")) + + +def test_sanity_l() -> None: + for im in ims: + roundtrip(im.convert("L")) + + +def test_sanity_p() -> None: + for im in ims: + roundtrip(im.convert("P")) diff --git a/Tests/test_image_getbands.py b/Tests/test_image_getbands.py index 887553fc0..9c03845a5 100644 --- a/Tests/test_image_getbands.py +++ b/Tests/test_image_getbands.py @@ -1,15 +1,15 @@ -from __future__ import annotations - -from PIL import Image - - -def test_getbands() -> None: - assert Image.new("1", (1, 1)).getbands() == ("1",) - assert Image.new("L", (1, 1)).getbands() == ("L",) - assert Image.new("I", (1, 1)).getbands() == ("I",) - assert Image.new("F", (1, 1)).getbands() == ("F",) - assert Image.new("P", (1, 1)).getbands() == ("P",) - assert Image.new("RGB", (1, 1)).getbands() == ("R", "G", "B") - assert Image.new("RGBA", (1, 1)).getbands() == ("R", "G", "B", "A") - assert Image.new("CMYK", (1, 1)).getbands() == ("C", "M", "Y", "K") - assert Image.new("YCbCr", (1, 1)).getbands() == ("Y", "Cb", "Cr") +from __future__ import annotations + +from PIL import Image + + +def test_getbands() -> None: + assert Image.new("1", (1, 1)).getbands() == ("1",) + assert Image.new("L", (1, 1)).getbands() == ("L",) + assert Image.new("I", (1, 1)).getbands() == ("I",) + assert Image.new("F", (1, 1)).getbands() == ("F",) + assert Image.new("P", (1, 1)).getbands() == ("P",) + assert Image.new("RGB", (1, 1)).getbands() == ("R", "G", "B") + assert Image.new("RGBA", (1, 1)).getbands() == ("R", "G", "B", "A") + assert Image.new("CMYK", (1, 1)).getbands() == ("C", "M", "Y", "K") + assert Image.new("YCbCr", (1, 1)).getbands() == ("Y", "Cb", "Cr") diff --git a/Tests/test_image_getbbox.py b/Tests/test_image_getbbox.py index 18c6f6579..d4ec6f42d 100644 --- a/Tests/test_image_getbbox.py +++ b/Tests/test_image_getbbox.py @@ -1,57 +1,57 @@ -from __future__ import annotations - -import pytest - -from PIL import Image - -from .helper import hopper - - -def test_sanity() -> None: - bbox = hopper().getbbox() - assert isinstance(bbox, tuple) - - -def test_bbox() -> None: - def check(im: Image.Image, fill_color: int | tuple[int, ...]) -> None: - assert im.getbbox() is None - - im.paste(fill_color, (10, 25, 90, 75)) - assert im.getbbox() == (10, 25, 90, 75) - - im.paste(fill_color, (25, 10, 75, 90)) - assert im.getbbox() == (10, 10, 90, 90) - - im.paste(fill_color, (-10, -10, 110, 110)) - assert im.getbbox() == (0, 0, 100, 100) - - # 8-bit mode - im = Image.new("L", (100, 100), 0) - check(im, 255) - - # 32-bit mode - im = Image.new("RGB", (100, 100), 0) - check(im, 255) - - for mode in ("RGBA", "RGBa"): - for rgba_color in ((0, 0, 0, 0), (127, 127, 127, 0), (255, 255, 255, 0)): - im = Image.new(mode, (100, 100), rgba_color) - check(im, (255, 255, 255, 255)) - - for mode in ("La", "LA", "PA"): - for color in ((0, 0), (127, 0), (255, 0)): - im = Image.new(mode, (100, 100), color) - check(im, (255, 255)) - - -@pytest.mark.parametrize("mode", ("RGBA", "RGBa", "La", "LA", "PA")) -def test_bbox_alpha_only_false(mode: str) -> None: - im = Image.new(mode, (100, 100)) - assert im.getbbox(alpha_only=False) is None - - fill_color = [1] * Image.getmodebands(mode) - fill_color[-1] = 0 - im.paste(tuple(fill_color), (25, 25, 75, 75)) - assert im.getbbox(alpha_only=False) == (25, 25, 75, 75) - - assert im.getbbox() is None +from __future__ import annotations + +import pytest + +from PIL import Image + +from .helper import hopper + + +def test_sanity() -> None: + bbox = hopper().getbbox() + assert isinstance(bbox, tuple) + + +def test_bbox() -> None: + def check(im: Image.Image, fill_color: int | tuple[int, ...]) -> None: + assert im.getbbox() is None + + im.paste(fill_color, (10, 25, 90, 75)) + assert im.getbbox() == (10, 25, 90, 75) + + im.paste(fill_color, (25, 10, 75, 90)) + assert im.getbbox() == (10, 10, 90, 90) + + im.paste(fill_color, (-10, -10, 110, 110)) + assert im.getbbox() == (0, 0, 100, 100) + + # 8-bit mode + im = Image.new("L", (100, 100), 0) + check(im, 255) + + # 32-bit mode + im = Image.new("RGB", (100, 100), 0) + check(im, 255) + + for mode in ("RGBA", "RGBa"): + for rgba_color in ((0, 0, 0, 0), (127, 127, 127, 0), (255, 255, 255, 0)): + im = Image.new(mode, (100, 100), rgba_color) + check(im, (255, 255, 255, 255)) + + for mode in ("La", "LA", "PA"): + for color in ((0, 0), (127, 0), (255, 0)): + im = Image.new(mode, (100, 100), color) + check(im, (255, 255)) + + +@pytest.mark.parametrize("mode", ("RGBA", "RGBa", "La", "LA", "PA")) +def test_bbox_alpha_only_false(mode: str) -> None: + im = Image.new(mode, (100, 100)) + assert im.getbbox(alpha_only=False) is None + + fill_color = [1] * Image.getmodebands(mode) + fill_color[-1] = 0 + im.paste(tuple(fill_color), (25, 25, 75, 75)) + assert im.getbbox(alpha_only=False) == (25, 25, 75, 75) + + assert im.getbbox() is None diff --git a/Tests/test_image_getcolors.py b/Tests/test_image_getcolors.py index 8f8870f4f..39c0411f0 100644 --- a/Tests/test_image_getcolors.py +++ b/Tests/test_image_getcolors.py @@ -1,70 +1,70 @@ -from __future__ import annotations - -from .helper import hopper - - -def test_getcolors() -> None: - def getcolors(mode: str, limit: int | None = None) -> int | None: - im = hopper(mode) - if limit: - colors = im.getcolors(limit) - else: - colors = im.getcolors() - if colors: - return len(colors) - return None - - assert getcolors("1") == 2 - assert getcolors("L") == 255 - assert getcolors("I") == 255 - assert getcolors("F") == 255 - assert getcolors("P") == 96 # fixed palette - assert getcolors("RGB") is None - assert getcolors("RGBA") is None - assert getcolors("CMYK") is None - assert getcolors("YCbCr") is None - - assert getcolors("L", 128) is None - assert getcolors("L", 1024) == 255 - - assert getcolors("RGB", 8192) is None - assert getcolors("RGB", 16384) == 10100 - assert getcolors("RGB", 100000) == 10100 - - assert getcolors("RGBA", 16384) == 10100 - assert getcolors("CMYK", 16384) == 10100 - assert getcolors("YCbCr", 16384) == 9329 - - -# -------------------------------------------------------------------- - - -def test_pack() -> None: - # Pack problems for small tables (@PIL209) - - im = hopper().quantize(3).convert("RGB") - - expected = [ - (4039, (172, 166, 181)), - (4385, (124, 113, 134)), - (7960, (31, 20, 33)), - ] - - A = im.getcolors(maxcolors=2) - assert A is None - - A = im.getcolors(maxcolors=3) - A.sort() - assert A == expected - - A = im.getcolors(maxcolors=4) - A.sort() - assert A == expected - - A = im.getcolors(maxcolors=8) - A.sort() - assert A == expected - - A = im.getcolors(maxcolors=16) - A.sort() - assert A == expected +from __future__ import annotations + +from .helper import hopper + + +def test_getcolors() -> None: + def getcolors(mode: str, limit: int | None = None) -> int | None: + im = hopper(mode) + if limit: + colors = im.getcolors(limit) + else: + colors = im.getcolors() + if colors: + return len(colors) + return None + + assert getcolors("1") == 2 + assert getcolors("L") == 255 + assert getcolors("I") == 255 + assert getcolors("F") == 255 + assert getcolors("P") == 96 # fixed palette + assert getcolors("RGB") is None + assert getcolors("RGBA") is None + assert getcolors("CMYK") is None + assert getcolors("YCbCr") is None + + assert getcolors("L", 128) is None + assert getcolors("L", 1024) == 255 + + assert getcolors("RGB", 8192) is None + assert getcolors("RGB", 16384) == 10100 + assert getcolors("RGB", 100000) == 10100 + + assert getcolors("RGBA", 16384) == 10100 + assert getcolors("CMYK", 16384) == 10100 + assert getcolors("YCbCr", 16384) == 9329 + + +# -------------------------------------------------------------------- + + +def test_pack() -> None: + # Pack problems for small tables (@PIL209) + + im = hopper().quantize(3).convert("RGB") + + expected = [ + (4039, (172, 166, 181)), + (4385, (124, 113, 134)), + (7960, (31, 20, 33)), + ] + + A = im.getcolors(maxcolors=2) + assert A is None + + A = im.getcolors(maxcolors=3) + A.sort() + assert A == expected + + A = im.getcolors(maxcolors=4) + A.sort() + assert A == expected + + A = im.getcolors(maxcolors=8) + A.sort() + assert A == expected + + A = im.getcolors(maxcolors=16) + A.sort() + assert A == expected diff --git a/Tests/test_image_getdata.py b/Tests/test_image_getdata.py index dd3d70b34..5fea2157a 100644 --- a/Tests/test_image_getdata.py +++ b/Tests/test_image_getdata.py @@ -1,30 +1,30 @@ -from __future__ import annotations - -from PIL import Image - -from .helper import hopper - - -def test_sanity() -> None: - data = hopper().getdata() - - len(data) - list(data) - - assert data[0] == (20, 20, 70) - - -def test_mode() -> None: - def getdata(mode: str) -> tuple[float | tuple[int, ...], int, int]: - im = hopper(mode).resize((32, 30), Image.Resampling.NEAREST) - data = im.getdata() - return data[0], len(data), len(list(data)) - - assert getdata("1") == (0, 960, 960) - assert getdata("L") == (17, 960, 960) - assert getdata("I") == (17, 960, 960) - assert getdata("F") == (17.0, 960, 960) - assert getdata("RGB") == ((11, 13, 52), 960, 960) - assert getdata("RGBA") == ((11, 13, 52, 255), 960, 960) - assert getdata("CMYK") == ((244, 242, 203, 0), 960, 960) - assert getdata("YCbCr") == ((16, 147, 123), 960, 960) +from __future__ import annotations + +from PIL import Image + +from .helper import hopper + + +def test_sanity() -> None: + data = hopper().getdata() + + len(data) + list(data) + + assert data[0] == (20, 20, 70) + + +def test_mode() -> None: + def getdata(mode: str) -> tuple[float | tuple[int, ...], int, int]: + im = hopper(mode).resize((32, 30), Image.Resampling.NEAREST) + data = im.getdata() + return data[0], len(data), len(list(data)) + + assert getdata("1") == (0, 960, 960) + assert getdata("L") == (17, 960, 960) + assert getdata("I") == (17, 960, 960) + assert getdata("F") == (17.0, 960, 960) + assert getdata("RGB") == ((11, 13, 52), 960, 960) + assert getdata("RGBA") == ((11, 13, 52, 255), 960, 960) + assert getdata("CMYK") == ((244, 242, 203, 0), 960, 960) + assert getdata("YCbCr") == ((16, 147, 123), 960, 960) diff --git a/Tests/test_image_getextrema.py b/Tests/test_image_getextrema.py index de5956f3e..068adbaa7 100644 --- a/Tests/test_image_getextrema.py +++ b/Tests/test_image_getextrema.py @@ -1,27 +1,27 @@ -from __future__ import annotations - -from PIL import Image - -from .helper import hopper - - -def test_extrema() -> None: - def extrema(mode: str) -> tuple[float, float] | tuple[tuple[int, int], ...]: - return hopper(mode).getextrema() - - assert extrema("1") == (0, 255) - assert extrema("L") == (1, 255) - assert extrema("I") == (1, 255) - assert extrema("F") == (1, 255) - assert extrema("P") == (0, 225) # fixed palette - assert extrema("RGB") == ((0, 255), (0, 255), (0, 255)) - assert extrema("RGBA") == ((0, 255), (0, 255), (0, 255), (255, 255)) - assert extrema("CMYK") == ((0, 255), (0, 255), (0, 255), (0, 0)) - assert extrema("I;16") == (1, 255) - - -def test_true_16() -> None: - with Image.open("Tests/images/16_bit_noise.tif") as im: - assert im.mode == "I;16" - extrema = im.getextrema() - assert extrema == (106, 285) +from __future__ import annotations + +from PIL import Image + +from .helper import hopper + + +def test_extrema() -> None: + def extrema(mode: str) -> tuple[float, float] | tuple[tuple[int, int], ...]: + return hopper(mode).getextrema() + + assert extrema("1") == (0, 255) + assert extrema("L") == (1, 255) + assert extrema("I") == (1, 255) + assert extrema("F") == (1, 255) + assert extrema("P") == (0, 225) # fixed palette + assert extrema("RGB") == ((0, 255), (0, 255), (0, 255)) + assert extrema("RGBA") == ((0, 255), (0, 255), (0, 255), (255, 255)) + assert extrema("CMYK") == ((0, 255), (0, 255), (0, 255), (0, 0)) + assert extrema("I;16") == (1, 255) + + +def test_true_16() -> None: + with Image.open("Tests/images/16_bit_noise.tif") as im: + assert im.mode == "I;16" + extrema = im.getextrema() + assert extrema == (106, 285) diff --git a/Tests/test_image_getim.py b/Tests/test_image_getim.py index 9afa02b0a..2f7ab279c 100644 --- a/Tests/test_image_getim.py +++ b/Tests/test_image_getim.py @@ -1,11 +1,11 @@ -from __future__ import annotations - -from .helper import hopper - - -def test_sanity() -> None: - im = hopper() - type_repr = repr(type(im.getim())) - - assert "PyCapsule" in type_repr - assert isinstance(im.im.id, int) +from __future__ import annotations + +from .helper import hopper + + +def test_sanity() -> None: + im = hopper() + type_repr = repr(type(im.getim())) + + assert "PyCapsule" in type_repr + assert isinstance(im.im.id, int) diff --git a/Tests/test_image_getpalette.py b/Tests/test_image_getpalette.py index 6a8f157fc..847784183 100644 --- a/Tests/test_image_getpalette.py +++ b/Tests/test_image_getpalette.py @@ -1,46 +1,46 @@ -from __future__ import annotations - -from PIL import Image - -from .helper import hopper - - -def test_palette() -> None: - def palette(mode: str) -> list[int] | None: - p = hopper(mode).getpalette() - if p: - return p[:10] - return None - - assert palette("1") is None - assert palette("L") is None - assert palette("I") is None - assert palette("F") is None - assert palette("P") == [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - assert palette("RGB") is None - assert palette("RGBA") is None - assert palette("CMYK") is None - assert palette("YCbCr") is None - - -def test_palette_rawmode() -> None: - im = Image.new("P", (1, 1)) - im.putpalette((1, 2, 3)) - - for rawmode in ("RGB", None): - rgb = im.getpalette(rawmode) - assert rgb == [1, 2, 3] - - # Convert the RGB palette to RGBA - rgba = im.getpalette("RGBA") - assert rgba == [1, 2, 3, 255] - - im.putpalette((1, 2, 3, 4), "RGBA") - - # Convert the RGBA palette to RGB - rgb = im.getpalette("RGB") - assert rgb == [1, 2, 3] - - for rawmode in ("RGBA", None): - rgba = im.getpalette(rawmode) - assert rgba == [1, 2, 3, 4] +from __future__ import annotations + +from PIL import Image + +from .helper import hopper + + +def test_palette() -> None: + def palette(mode: str) -> list[int] | None: + p = hopper(mode).getpalette() + if p: + return p[:10] + return None + + assert palette("1") is None + assert palette("L") is None + assert palette("I") is None + assert palette("F") is None + assert palette("P") == [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + assert palette("RGB") is None + assert palette("RGBA") is None + assert palette("CMYK") is None + assert palette("YCbCr") is None + + +def test_palette_rawmode() -> None: + im = Image.new("P", (1, 1)) + im.putpalette((1, 2, 3)) + + for rawmode in ("RGB", None): + rgb = im.getpalette(rawmode) + assert rgb == [1, 2, 3] + + # Convert the RGB palette to RGBA + rgba = im.getpalette("RGBA") + assert rgba == [1, 2, 3, 255] + + im.putpalette((1, 2, 3, 4), "RGBA") + + # Convert the RGBA palette to RGB + rgb = im.getpalette("RGB") + assert rgb == [1, 2, 3] + + for rawmode in ("RGBA", None): + rgba = im.getpalette(rawmode) + assert rgba == [1, 2, 3, 4] diff --git a/Tests/test_image_getprojection.py b/Tests/test_image_getprojection.py index 2b5a758ed..8ecf476f6 100644 --- a/Tests/test_image_getprojection.py +++ b/Tests/test_image_getprojection.py @@ -1,31 +1,31 @@ -from __future__ import annotations - -from PIL import Image - -from .helper import hopper - - -def test_sanity() -> None: - im = hopper() - - projection = im.getprojection() - - assert len(projection) == 2 - assert len(projection[0]) == im.size[0] - assert len(projection[1]) == im.size[1] - - # 8-bit image - im = Image.new("L", (10, 10)) - assert im.getprojection()[0] == [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - assert im.getprojection()[1] == [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - im.paste(255, (2, 4, 8, 6)) - assert im.getprojection()[0] == [0, 0, 1, 1, 1, 1, 1, 1, 0, 0] - assert im.getprojection()[1] == [0, 0, 0, 0, 1, 1, 0, 0, 0, 0] - - # 32-bit image - im = Image.new("RGB", (10, 10)) - assert im.getprojection()[0] == [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - assert im.getprojection()[1] == [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - im.paste(255, (2, 4, 8, 6)) - assert im.getprojection()[0] == [0, 0, 1, 1, 1, 1, 1, 1, 0, 0] - assert im.getprojection()[1] == [0, 0, 0, 0, 1, 1, 0, 0, 0, 0] +from __future__ import annotations + +from PIL import Image + +from .helper import hopper + + +def test_sanity() -> None: + im = hopper() + + projection = im.getprojection() + + assert len(projection) == 2 + assert len(projection[0]) == im.size[0] + assert len(projection[1]) == im.size[1] + + # 8-bit image + im = Image.new("L", (10, 10)) + assert im.getprojection()[0] == [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + assert im.getprojection()[1] == [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + im.paste(255, (2, 4, 8, 6)) + assert im.getprojection()[0] == [0, 0, 1, 1, 1, 1, 1, 1, 0, 0] + assert im.getprojection()[1] == [0, 0, 0, 0, 1, 1, 0, 0, 0, 0] + + # 32-bit image + im = Image.new("RGB", (10, 10)) + assert im.getprojection()[0] == [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + assert im.getprojection()[1] == [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + im.paste(255, (2, 4, 8, 6)) + assert im.getprojection()[0] == [0, 0, 1, 1, 1, 1, 1, 1, 0, 0] + assert im.getprojection()[1] == [0, 0, 0, 0, 1, 1, 0, 0, 0, 0] diff --git a/Tests/test_image_histogram.py b/Tests/test_image_histogram.py index dbd55d4c2..045b18e79 100644 --- a/Tests/test_image_histogram.py +++ b/Tests/test_image_histogram.py @@ -1,19 +1,19 @@ -from __future__ import annotations - -from .helper import hopper - - -def test_histogram() -> None: - def histogram(mode: str) -> tuple[int, int, int]: - h = hopper(mode).histogram() - return len(h), min(h), max(h) - - assert histogram("1") == (256, 0, 10994) - assert histogram("L") == (256, 0, 662) - assert histogram("I") == (256, 0, 662) - assert histogram("F") == (256, 0, 662) - assert histogram("P") == (256, 0, 1551) - assert histogram("RGB") == (768, 4, 675) - assert histogram("RGBA") == (1024, 0, 16384) - assert histogram("CMYK") == (1024, 0, 16384) - assert histogram("YCbCr") == (768, 0, 1908) +from __future__ import annotations + +from .helper import hopper + + +def test_histogram() -> None: + def histogram(mode: str) -> tuple[int, int, int]: + h = hopper(mode).histogram() + return len(h), min(h), max(h) + + assert histogram("1") == (256, 0, 10994) + assert histogram("L") == (256, 0, 662) + assert histogram("I") == (256, 0, 662) + assert histogram("F") == (256, 0, 662) + assert histogram("P") == (256, 0, 1551) + assert histogram("RGB") == (768, 4, 675) + assert histogram("RGBA") == (1024, 0, 16384) + assert histogram("CMYK") == (1024, 0, 16384) + assert histogram("YCbCr") == (768, 0, 1908) diff --git a/Tests/test_image_load.py b/Tests/test_image_load.py index 0605821e0..94743afe3 100644 --- a/Tests/test_image_load.py +++ b/Tests/test_image_load.py @@ -1,52 +1,52 @@ -from __future__ import annotations - -import logging -import os - -import pytest - -from PIL import Image - -from .helper import hopper - - -def test_sanity() -> None: - im = hopper() - pix = im.load() - - assert pix[0, 0] == (20, 20, 70) - - -def test_close() -> None: - im = Image.open("Tests/images/hopper.gif") - im.close() - with pytest.raises(ValueError): - im.load() - with pytest.raises(ValueError): - im.getpixel((0, 0)) - - -def test_close_after_load(caplog: pytest.LogCaptureFixture) -> None: - im = Image.open("Tests/images/hopper.gif") - im.load() - with caplog.at_level(logging.DEBUG): - im.close() - assert len(caplog.records) == 0 - - -def test_contextmanager() -> None: - fn = None - with Image.open("Tests/images/hopper.gif") as im: - fn = im.fp.fileno() - os.fstat(fn) - - with pytest.raises(OSError): - os.fstat(fn) - - -def test_contextmanager_non_exclusive_fp() -> None: - with open("Tests/images/hopper.gif", "rb") as fp: - with Image.open(fp): - pass - - assert not fp.closed +from __future__ import annotations + +import logging +import os + +import pytest + +from PIL import Image + +from .helper import hopper + + +def test_sanity() -> None: + im = hopper() + pix = im.load() + + assert pix[0, 0] == (20, 20, 70) + + +def test_close() -> None: + im = Image.open("Tests/images/hopper.gif") + im.close() + with pytest.raises(ValueError): + im.load() + with pytest.raises(ValueError): + im.getpixel((0, 0)) + + +def test_close_after_load(caplog: pytest.LogCaptureFixture) -> None: + im = Image.open("Tests/images/hopper.gif") + im.load() + with caplog.at_level(logging.DEBUG): + im.close() + assert len(caplog.records) == 0 + + +def test_contextmanager() -> None: + fn = None + with Image.open("Tests/images/hopper.gif") as im: + fn = im.fp.fileno() + os.fstat(fn) + + with pytest.raises(OSError): + os.fstat(fn) + + +def test_contextmanager_non_exclusive_fp() -> None: + with open("Tests/images/hopper.gif", "rb") as fp: + with Image.open(fp): + pass + + assert not fp.closed diff --git a/Tests/test_image_merge.py b/Tests/test_image_merge.py new file mode 100644 index 000000000..40e198623 --- /dev/null +++ b/Tests/test_image_merge.py @@ -0,0 +1,25 @@ +import pytest + +from PIL import Image + +def test_merge_wrong_number_of_bands(): + R = Image.new('L', (100, 100), color=255) + G = Image.new('L', (100, 100), color=128) + with pytest.raises(ValueError, match="wrong number of bands"): + Image.merge('RGB', [R, G]) + +def test_merge_mode_mismatch(): + R = Image.new('L', (100, 100), color=255) + G = Image.new('L', (100, 100), color=128) + B = Image.new('1', (100, 100)) # Incorrect mode + with pytest.raises(ValueError, match="mode mismatch"): + Image.merge('RGB', [R, G, B]) + +def test_merge_size_mismatch(): + R = Image.new('L', (100, 100), color=255) + G = Image.new('L', (200, 100), color=128) # Different size + B = Image.new('L', (100, 100), color=0) + with pytest.raises(ValueError, match="size mismatch"): + Image.merge('RGB', [R, G, B]) + + diff --git a/Tests/test_image_mode.py b/Tests/test_image_mode.py index 8e94aafc5..58e5ef46b 100644 --- a/Tests/test_image_mode.py +++ b/Tests/test_image_mode.py @@ -1,76 +1,76 @@ -from __future__ import annotations - -import pytest - -from PIL import Image, ImageMode - -from .helper import hopper - - -def test_sanity() -> None: - with hopper() as im: - im.mode - - ImageMode.getmode("1") - ImageMode.getmode("L") - ImageMode.getmode("P") - ImageMode.getmode("RGB") - ImageMode.getmode("I") - ImageMode.getmode("F") - - m = ImageMode.getmode("1") - assert m.mode == "1" - assert str(m) == "1" - assert m.bands == ("1",) - assert m.basemode == "L" - assert m.basetype == "L" - assert m.typestr == "|b1" - - for mode in ( - "I;16", - "I;16S", - "I;16L", - "I;16LS", - "I;16B", - "I;16BS", - "I;16N", - "I;16NS", - ): - m = ImageMode.getmode(mode) - assert m.mode == mode - assert str(m) == mode - assert m.bands == ("I",) - assert m.basemode == "L" - assert m.basetype == "L" - - m = ImageMode.getmode("RGB") - assert m.mode == "RGB" - assert str(m) == "RGB" - assert m.bands == ("R", "G", "B") - assert m.basemode == "RGB" - assert m.basetype == "L" - assert m.typestr == "|u1" - - -@pytest.mark.parametrize( - "mode, expected_base, expected_type, expected_bands, expected_band_names", - ( - ("1", "L", "L", 1, ("1",)), - ("L", "L", "L", 1, ("L",)), - ("P", "P", "L", 1, ("P",)), - ("I", "L", "I", 1, ("I",)), - ("F", "L", "F", 1, ("F",)), - ("RGB", "RGB", "L", 3, ("R", "G", "B")), - ("RGBA", "RGB", "L", 4, ("R", "G", "B", "A")), - ("RGBX", "RGB", "L", 4, ("R", "G", "B", "X")), - ("CMYK", "RGB", "L", 4, ("C", "M", "Y", "K")), - ("YCbCr", "RGB", "L", 3, ("Y", "Cb", "Cr")), - ), -) -def test_properties( - mode, expected_base, expected_type, expected_bands, expected_band_names -) -> None: - assert Image.getmodebase(mode) == expected_base - assert Image.getmodetype(mode) == expected_type - assert Image.getmodebands(mode) == expected_bands - assert Image.getmodebandnames(mode) == expected_band_names +from __future__ import annotations + +import pytest + +from PIL import Image, ImageMode + +from .helper import hopper + + +def test_sanity() -> None: + with hopper() as im: + im.mode + + ImageMode.getmode("1") + ImageMode.getmode("L") + ImageMode.getmode("P") + ImageMode.getmode("RGB") + ImageMode.getmode("I") + ImageMode.getmode("F") + + m = ImageMode.getmode("1") + assert m.mode == "1" + assert str(m) == "1" + assert m.bands == ("1",) + assert m.basemode == "L" + assert m.basetype == "L" + assert m.typestr == "|b1" + + for mode in ( + "I;16", + "I;16S", + "I;16L", + "I;16LS", + "I;16B", + "I;16BS", + "I;16N", + "I;16NS", + ): + m = ImageMode.getmode(mode) + assert m.mode == mode + assert str(m) == mode + assert m.bands == ("I",) + assert m.basemode == "L" + assert m.basetype == "L" + + m = ImageMode.getmode("RGB") + assert m.mode == "RGB" + assert str(m) == "RGB" + assert m.bands == ("R", "G", "B") + assert m.basemode == "RGB" + assert m.basetype == "L" + assert m.typestr == "|u1" + + +@pytest.mark.parametrize( + "mode, expected_base, expected_type, expected_bands, expected_band_names", + ( + ("1", "L", "L", 1, ("1",)), + ("L", "L", "L", 1, ("L",)), + ("P", "P", "L", 1, ("P",)), + ("I", "L", "I", 1, ("I",)), + ("F", "L", "F", 1, ("F",)), + ("RGB", "RGB", "L", 3, ("R", "G", "B")), + ("RGBA", "RGB", "L", 4, ("R", "G", "B", "A")), + ("RGBX", "RGB", "L", 4, ("R", "G", "B", "X")), + ("CMYK", "RGB", "L", 4, ("C", "M", "Y", "K")), + ("YCbCr", "RGB", "L", 3, ("Y", "Cb", "Cr")), + ), +) +def test_properties( + mode, expected_base, expected_type, expected_bands, expected_band_names +) -> None: + assert Image.getmodebase(mode) == expected_base + assert Image.getmodetype(mode) == expected_type + assert Image.getmodebands(mode) == expected_bands + assert Image.getmodebandnames(mode) == expected_band_names diff --git a/Tests/test_image_paste.py b/Tests/test_image_paste.py index d8f6b65e0..e02ec1fa3 100644 --- a/Tests/test_image_paste.py +++ b/Tests/test_image_paste.py @@ -1,340 +1,340 @@ -from __future__ import annotations - -import pytest - -from PIL import Image - -from .helper import CachedProperty, assert_image_equal - - -class TestImagingPaste: - size = 128 - - def assert_9points_image( - self, im: Image.Image, expected: list[tuple[int, int, int, int]] - ) -> None: - px = im.load() - actual = [ - px[0, 0], - px[self.size // 2, 0], - px[self.size - 1, 0], - px[0, self.size // 2], - px[self.size // 2, self.size // 2], - px[self.size - 1, self.size // 2], - px[0, self.size - 1], - px[self.size // 2, self.size - 1], - px[self.size - 1, self.size - 1], - ] - assert actual == [ - point[0] if im.mode == "L" else point[: len(im.mode)] for point in expected - ] - - def assert_9points_paste( - self, - im: Image.Image, - im2: Image.Image | str | tuple[int, ...], - mask: Image.Image, - expected: list[tuple[int, int, int, int]], - ) -> None: - im3 = im.copy() - im3.paste(im2, (0, 0), mask) - self.assert_9points_image(im3, expected) - - # Abbreviated syntax - im.paste(im2, mask) - self.assert_9points_image(im, expected) - - @CachedProperty - def mask_1(self) -> Image.Image: - mask = Image.new("1", (self.size, self.size)) - px = mask.load() - for y in range(mask.height): - for x in range(mask.width): - px[y, x] = (x + y) % 2 - return mask - - @CachedProperty - def mask_L(self) -> Image.Image: - return self.gradient_L.transpose(Image.Transpose.ROTATE_270) - - @CachedProperty - def gradient_L(self) -> Image.Image: - gradient = Image.new("L", (self.size, self.size)) - px = gradient.load() - for y in range(gradient.height): - for x in range(gradient.width): - px[y, x] = (x + y) % 255 - return gradient - - @CachedProperty - def gradient_RGB(self) -> Image.Image: - return Image.merge( - "RGB", - [ - self.gradient_L, - self.gradient_L.transpose(Image.Transpose.ROTATE_90), - self.gradient_L.transpose(Image.Transpose.ROTATE_180), - ], - ) - - @CachedProperty - def gradient_LA(self) -> Image.Image: - return Image.merge( - "LA", - [ - self.gradient_L, - self.gradient_L.transpose(Image.Transpose.ROTATE_90), - ], - ) - - @CachedProperty - def gradient_RGBA(self) -> Image.Image: - return Image.merge( - "RGBA", - [ - self.gradient_L, - self.gradient_L.transpose(Image.Transpose.ROTATE_90), - self.gradient_L.transpose(Image.Transpose.ROTATE_180), - self.gradient_L.transpose(Image.Transpose.ROTATE_270), - ], - ) - - @CachedProperty - def gradient_RGBa(self) -> Image.Image: - return Image.merge( - "RGBa", - [ - self.gradient_L, - self.gradient_L.transpose(Image.Transpose.ROTATE_90), - self.gradient_L.transpose(Image.Transpose.ROTATE_180), - self.gradient_L.transpose(Image.Transpose.ROTATE_270), - ], - ) - - @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_image_solid(self, mode: str) -> None: - im = Image.new(mode, (200, 200), "red") - im2 = getattr(self, "gradient_" + mode) - - im.paste(im2, (12, 23)) - - im = im.crop((12, 23, im2.width + 12, im2.height + 23)) - assert_image_equal(im, im2) - - @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_image_mask_1(self, mode: str) -> None: - im = Image.new(mode, (200, 200), "white") - im2 = getattr(self, "gradient_" + mode) - - self.assert_9points_paste( - im, - im2, - self.mask_1, - [ - (255, 255, 255, 255), - (255, 255, 255, 255), - (127, 254, 127, 0), - (255, 255, 255, 255), - (255, 255, 255, 255), - (191, 190, 63, 64), - (127, 0, 127, 254), - (191, 64, 63, 190), - (255, 255, 255, 255), - ], - ) - - @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_image_mask_L(self, mode: str) -> None: - im = Image.new(mode, (200, 200), "white") - im2 = getattr(self, "gradient_" + mode) - - self.assert_9points_paste( - im, - im2, - self.mask_L, - [ - (128, 191, 255, 191), - (208, 239, 239, 208), - (255, 255, 255, 255), - (112, 111, 206, 207), - (192, 191, 191, 191), - (239, 239, 207, 207), - (128, 1, 128, 254), - (207, 113, 112, 207), - (255, 191, 128, 191), - ], - ) - - @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_image_mask_LA(self, mode: str) -> None: - im = Image.new(mode, (200, 200), "white") - im2 = getattr(self, "gradient_" + mode) - - self.assert_9points_paste( - im, - im2, - self.gradient_LA, - [ - (128, 191, 255, 191), - (112, 207, 206, 111), - (128, 254, 128, 1), - (208, 208, 239, 239), - (192, 191, 191, 191), - (207, 207, 112, 113), - (255, 255, 255, 255), - (239, 207, 207, 239), - (255, 191, 128, 191), - ], - ) - - @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_image_mask_RGBA(self, mode: str) -> None: - im = Image.new(mode, (200, 200), "white") - im2 = getattr(self, "gradient_" + mode) - - self.assert_9points_paste( - im, - im2, - self.gradient_RGBA, - [ - (128, 191, 255, 191), - (208, 239, 239, 208), - (255, 255, 255, 255), - (112, 111, 206, 207), - (192, 191, 191, 191), - (239, 239, 207, 207), - (128, 1, 128, 254), - (207, 113, 112, 207), - (255, 191, 128, 191), - ], - ) - - @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_image_mask_RGBa(self, mode: str) -> None: - im = Image.new(mode, (200, 200), "white") - im2 = getattr(self, "gradient_" + mode) - - self.assert_9points_paste( - im, - im2, - self.gradient_RGBa, - [ - (128, 255, 126, 255), - (0, 127, 126, 255), - (126, 253, 126, 255), - (128, 127, 254, 255), - (0, 255, 254, 255), - (126, 125, 254, 255), - (128, 1, 128, 255), - (0, 129, 128, 255), - (126, 255, 128, 255), - ], - ) - - @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_color_solid(self, mode: str) -> None: - im = Image.new(mode, (200, 200), "black") - - rect = (12, 23, 128 + 12, 128 + 23) - im.paste("white", rect) - - hist = im.crop(rect).histogram() - while hist: - head, hist = hist[:256], hist[256:] - assert head[255] == 128 * 128 - assert sum(head[:255]) == 0 - - @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_color_mask_1(self, mode: str) -> None: - im = Image.new(mode, (200, 200), (50, 60, 70, 80)[: len(mode)]) - color = (10, 20, 30, 40)[: len(mode)] - - self.assert_9points_paste( - im, - color, - self.mask_1, - [ - (50, 60, 70, 80), - (50, 60, 70, 80), - (10, 20, 30, 40), - (50, 60, 70, 80), - (50, 60, 70, 80), - (10, 20, 30, 40), - (10, 20, 30, 40), - (10, 20, 30, 40), - (50, 60, 70, 80), - ], - ) - - @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_color_mask_L(self, mode: str) -> None: - im = getattr(self, "gradient_" + mode).copy() - color = "white" - - self.assert_9points_paste( - im, - color, - self.mask_L, - [ - (127, 191, 254, 191), - (111, 207, 206, 110), - (127, 254, 127, 0), - (207, 207, 239, 239), - (191, 191, 190, 191), - (207, 206, 111, 112), - (254, 254, 254, 255), - (239, 206, 206, 238), - (254, 191, 127, 191), - ], - ) - - @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_color_mask_RGBA(self, mode: str) -> None: - im = getattr(self, "gradient_" + mode).copy() - color = "white" - - self.assert_9points_paste( - im, - color, - self.gradient_RGBA, - [ - (127, 191, 254, 191), - (111, 207, 206, 110), - (127, 254, 127, 0), - (207, 207, 239, 239), - (191, 191, 190, 191), - (207, 206, 111, 112), - (254, 254, 254, 255), - (239, 206, 206, 238), - (254, 191, 127, 191), - ], - ) - - @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_color_mask_RGBa(self, mode: str) -> None: - im = getattr(self, "gradient_" + mode).copy() - color = "white" - - self.assert_9points_paste( - im, - color, - self.gradient_RGBa, - [ - (255, 63, 126, 63), - (47, 143, 142, 46), - (126, 253, 126, 255), - (15, 15, 47, 47), - (63, 63, 62, 63), - (142, 141, 46, 47), - (255, 255, 255, 0), - (48, 15, 15, 47), - (126, 63, 255, 63), - ], - ) - - def test_different_sizes(self) -> None: - im = Image.new("RGB", (100, 100)) - im2 = Image.new("RGB", (50, 50)) - - im.copy().paste(im2) - im.copy().paste(im2, (0, 0)) +from __future__ import annotations + +import pytest + +from PIL import Image + +from .helper import CachedProperty, assert_image_equal + + +class TestImagingPaste: + size = 128 + + def assert_9points_image( + self, im: Image.Image, expected: list[tuple[int, int, int, int]] + ) -> None: + px = im.load() + actual = [ + px[0, 0], + px[self.size // 2, 0], + px[self.size - 1, 0], + px[0, self.size // 2], + px[self.size // 2, self.size // 2], + px[self.size - 1, self.size // 2], + px[0, self.size - 1], + px[self.size // 2, self.size - 1], + px[self.size - 1, self.size - 1], + ] + assert actual == [ + point[0] if im.mode == "L" else point[: len(im.mode)] for point in expected + ] + + def assert_9points_paste( + self, + im: Image.Image, + im2: Image.Image | str | tuple[int, ...], + mask: Image.Image, + expected: list[tuple[int, int, int, int]], + ) -> None: + im3 = im.copy() + im3.paste(im2, (0, 0), mask) + self.assert_9points_image(im3, expected) + + # Abbreviated syntax + im.paste(im2, mask) + self.assert_9points_image(im, expected) + + @CachedProperty + def mask_1(self) -> Image.Image: + mask = Image.new("1", (self.size, self.size)) + px = mask.load() + for y in range(mask.height): + for x in range(mask.width): + px[y, x] = (x + y) % 2 + return mask + + @CachedProperty + def mask_L(self) -> Image.Image: + return self.gradient_L.transpose(Image.Transpose.ROTATE_270) + + @CachedProperty + def gradient_L(self) -> Image.Image: + gradient = Image.new("L", (self.size, self.size)) + px = gradient.load() + for y in range(gradient.height): + for x in range(gradient.width): + px[y, x] = (x + y) % 255 + return gradient + + @CachedProperty + def gradient_RGB(self) -> Image.Image: + return Image.merge( + "RGB", + [ + self.gradient_L, + self.gradient_L.transpose(Image.Transpose.ROTATE_90), + self.gradient_L.transpose(Image.Transpose.ROTATE_180), + ], + ) + + @CachedProperty + def gradient_LA(self) -> Image.Image: + return Image.merge( + "LA", + [ + self.gradient_L, + self.gradient_L.transpose(Image.Transpose.ROTATE_90), + ], + ) + + @CachedProperty + def gradient_RGBA(self) -> Image.Image: + return Image.merge( + "RGBA", + [ + self.gradient_L, + self.gradient_L.transpose(Image.Transpose.ROTATE_90), + self.gradient_L.transpose(Image.Transpose.ROTATE_180), + self.gradient_L.transpose(Image.Transpose.ROTATE_270), + ], + ) + + @CachedProperty + def gradient_RGBa(self) -> Image.Image: + return Image.merge( + "RGBa", + [ + self.gradient_L, + self.gradient_L.transpose(Image.Transpose.ROTATE_90), + self.gradient_L.transpose(Image.Transpose.ROTATE_180), + self.gradient_L.transpose(Image.Transpose.ROTATE_270), + ], + ) + + @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) + def test_image_solid(self, mode: str) -> None: + im = Image.new(mode, (200, 200), "red") + im2 = getattr(self, "gradient_" + mode) + + im.paste(im2, (12, 23)) + + im = im.crop((12, 23, im2.width + 12, im2.height + 23)) + assert_image_equal(im, im2) + + @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) + def test_image_mask_1(self, mode: str) -> None: + im = Image.new(mode, (200, 200), "white") + im2 = getattr(self, "gradient_" + mode) + + self.assert_9points_paste( + im, + im2, + self.mask_1, + [ + (255, 255, 255, 255), + (255, 255, 255, 255), + (127, 254, 127, 0), + (255, 255, 255, 255), + (255, 255, 255, 255), + (191, 190, 63, 64), + (127, 0, 127, 254), + (191, 64, 63, 190), + (255, 255, 255, 255), + ], + ) + + @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) + def test_image_mask_L(self, mode: str) -> None: + im = Image.new(mode, (200, 200), "white") + im2 = getattr(self, "gradient_" + mode) + + self.assert_9points_paste( + im, + im2, + self.mask_L, + [ + (128, 191, 255, 191), + (208, 239, 239, 208), + (255, 255, 255, 255), + (112, 111, 206, 207), + (192, 191, 191, 191), + (239, 239, 207, 207), + (128, 1, 128, 254), + (207, 113, 112, 207), + (255, 191, 128, 191), + ], + ) + + @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) + def test_image_mask_LA(self, mode: str) -> None: + im = Image.new(mode, (200, 200), "white") + im2 = getattr(self, "gradient_" + mode) + + self.assert_9points_paste( + im, + im2, + self.gradient_LA, + [ + (128, 191, 255, 191), + (112, 207, 206, 111), + (128, 254, 128, 1), + (208, 208, 239, 239), + (192, 191, 191, 191), + (207, 207, 112, 113), + (255, 255, 255, 255), + (239, 207, 207, 239), + (255, 191, 128, 191), + ], + ) + + @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) + def test_image_mask_RGBA(self, mode: str) -> None: + im = Image.new(mode, (200, 200), "white") + im2 = getattr(self, "gradient_" + mode) + + self.assert_9points_paste( + im, + im2, + self.gradient_RGBA, + [ + (128, 191, 255, 191), + (208, 239, 239, 208), + (255, 255, 255, 255), + (112, 111, 206, 207), + (192, 191, 191, 191), + (239, 239, 207, 207), + (128, 1, 128, 254), + (207, 113, 112, 207), + (255, 191, 128, 191), + ], + ) + + @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) + def test_image_mask_RGBa(self, mode: str) -> None: + im = Image.new(mode, (200, 200), "white") + im2 = getattr(self, "gradient_" + mode) + + self.assert_9points_paste( + im, + im2, + self.gradient_RGBa, + [ + (128, 255, 126, 255), + (0, 127, 126, 255), + (126, 253, 126, 255), + (128, 127, 254, 255), + (0, 255, 254, 255), + (126, 125, 254, 255), + (128, 1, 128, 255), + (0, 129, 128, 255), + (126, 255, 128, 255), + ], + ) + + @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) + def test_color_solid(self, mode: str) -> None: + im = Image.new(mode, (200, 200), "black") + + rect = (12, 23, 128 + 12, 128 + 23) + im.paste("white", rect) + + hist = im.crop(rect).histogram() + while hist: + head, hist = hist[:256], hist[256:] + assert head[255] == 128 * 128 + assert sum(head[:255]) == 0 + + @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) + def test_color_mask_1(self, mode: str) -> None: + im = Image.new(mode, (200, 200), (50, 60, 70, 80)[: len(mode)]) + color = (10, 20, 30, 40)[: len(mode)] + + self.assert_9points_paste( + im, + color, + self.mask_1, + [ + (50, 60, 70, 80), + (50, 60, 70, 80), + (10, 20, 30, 40), + (50, 60, 70, 80), + (50, 60, 70, 80), + (10, 20, 30, 40), + (10, 20, 30, 40), + (10, 20, 30, 40), + (50, 60, 70, 80), + ], + ) + + @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) + def test_color_mask_L(self, mode: str) -> None: + im = getattr(self, "gradient_" + mode).copy() + color = "white" + + self.assert_9points_paste( + im, + color, + self.mask_L, + [ + (127, 191, 254, 191), + (111, 207, 206, 110), + (127, 254, 127, 0), + (207, 207, 239, 239), + (191, 191, 190, 191), + (207, 206, 111, 112), + (254, 254, 254, 255), + (239, 206, 206, 238), + (254, 191, 127, 191), + ], + ) + + @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) + def test_color_mask_RGBA(self, mode: str) -> None: + im = getattr(self, "gradient_" + mode).copy() + color = "white" + + self.assert_9points_paste( + im, + color, + self.gradient_RGBA, + [ + (127, 191, 254, 191), + (111, 207, 206, 110), + (127, 254, 127, 0), + (207, 207, 239, 239), + (191, 191, 190, 191), + (207, 206, 111, 112), + (254, 254, 254, 255), + (239, 206, 206, 238), + (254, 191, 127, 191), + ], + ) + + @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) + def test_color_mask_RGBa(self, mode: str) -> None: + im = getattr(self, "gradient_" + mode).copy() + color = "white" + + self.assert_9points_paste( + im, + color, + self.gradient_RGBa, + [ + (255, 63, 126, 63), + (47, 143, 142, 46), + (126, 253, 126, 255), + (15, 15, 47, 47), + (63, 63, 62, 63), + (142, 141, 46, 47), + (255, 255, 255, 0), + (48, 15, 15, 47), + (126, 63, 255, 63), + ], + ) + + def test_different_sizes(self) -> None: + im = Image.new("RGB", (100, 100)) + im2 = Image.new("RGB", (50, 50)) + + im.copy().paste(im2) + im.copy().paste(im2, (0, 0)) diff --git a/Tests/test_image_point.py b/Tests/test_image_point.py index 05f209351..b853b8f38 100644 --- a/Tests/test_image_point.py +++ b/Tests/test_image_point.py @@ -1,64 +1,64 @@ -from __future__ import annotations - -import pytest - -from .helper import assert_image_equal, hopper - - -def test_sanity() -> None: - im = hopper() - - with pytest.raises(ValueError): - im.point(list(range(256))) - im.point(list(range(256)) * 3) - im.point(lambda x: x) - im.point(lambda x: x * 1.2) - - im = im.convert("I") - with pytest.raises(ValueError): - im.point(list(range(256))) - im.point(lambda x: x * 1) - im.point(lambda x: x + 1) - im.point(lambda x: x - 1) - im.point(lambda x: x * 1 + 1) - im.point(lambda x: 0.1 + 0.2 * x) - im.point(lambda x: -x) - im.point(lambda x: x - 0.5) - im.point(lambda x: 1 - x / 2) - im.point(lambda x: (2 + x) / 3) - im.point(lambda x: 0.5) - im.point(lambda x: x / 1) - im.point(lambda x: x + x) - with pytest.raises(TypeError): - im.point(lambda x: x * x) - with pytest.raises(TypeError): - im.point(lambda x: x / x) - with pytest.raises(TypeError): - im.point(lambda x: 1 / x) - with pytest.raises(TypeError): - im.point(lambda x: x // 2) - - -def test_16bit_lut() -> None: - """Tests for 16 bit -> 8 bit lut for converting I->L images - see https://github.com/python-pillow/Pillow/issues/440 - """ - im = hopper("I") - im.point(list(range(256)) * 256, "L") - - -def test_f_lut() -> None: - """Tests for floating point lut of 8bit gray image""" - im = hopper("L") - lut = [0.5 * float(x) for x in range(256)] - - out = im.point(lut, "F") - - int_lut = [x // 2 for x in range(256)] - assert_image_equal(out.convert("L"), im.point(int_lut, "L")) - - -def test_f_mode() -> None: - im = hopper("F") - with pytest.raises(ValueError): - im.point(None) +from __future__ import annotations + +import pytest + +from .helper import assert_image_equal, hopper + + +def test_sanity() -> None: + im = hopper() + + with pytest.raises(ValueError): + im.point(list(range(256))) + im.point(list(range(256)) * 3) + im.point(lambda x: x) + im.point(lambda x: x * 1.2) + + im = im.convert("I") + with pytest.raises(ValueError): + im.point(list(range(256))) + im.point(lambda x: x * 1) + im.point(lambda x: x + 1) + im.point(lambda x: x - 1) + im.point(lambda x: x * 1 + 1) + im.point(lambda x: 0.1 + 0.2 * x) + im.point(lambda x: -x) + im.point(lambda x: x - 0.5) + im.point(lambda x: 1 - x / 2) + im.point(lambda x: (2 + x) / 3) + im.point(lambda x: 0.5) + im.point(lambda x: x / 1) + im.point(lambda x: x + x) + with pytest.raises(TypeError): + im.point(lambda x: x * x) + with pytest.raises(TypeError): + im.point(lambda x: x / x) + with pytest.raises(TypeError): + im.point(lambda x: 1 / x) + with pytest.raises(TypeError): + im.point(lambda x: x // 2) + + +def test_16bit_lut() -> None: + """Tests for 16 bit -> 8 bit lut for converting I->L images + see https://github.com/python-pillow/Pillow/issues/440 + """ + im = hopper("I") + im.point(list(range(256)) * 256, "L") + + +def test_f_lut() -> None: + """Tests for floating point lut of 8bit gray image""" + im = hopper("L") + lut = [0.5 * float(x) for x in range(256)] + + out = im.point(lut, "F") + + int_lut = [x // 2 for x in range(256)] + assert_image_equal(out.convert("L"), im.point(int_lut, "L")) + + +def test_f_mode() -> None: + im = hopper("F") + with pytest.raises(ValueError): + im.point(None) diff --git a/Tests/test_image_putalpha.py b/Tests/test_image_putalpha.py index 2c92911d1..08af017b2 100644 --- a/Tests/test_image_putalpha.py +++ b/Tests/test_image_putalpha.py @@ -1,50 +1,50 @@ -from __future__ import annotations - -from PIL import Image - - -def test_interface() -> None: - im = Image.new("RGBA", (1, 1), (1, 2, 3, 0)) - assert im.getpixel((0, 0)) == (1, 2, 3, 0) - - im = Image.new("RGBA", (1, 1), (1, 2, 3)) - assert im.getpixel((0, 0)) == (1, 2, 3, 255) - - im.putalpha(Image.new("L", im.size, 4)) - assert im.getpixel((0, 0)) == (1, 2, 3, 4) - - im.putalpha(5) - assert im.getpixel((0, 0)) == (1, 2, 3, 5) - - -def test_promote() -> None: - im = Image.new("L", (1, 1), 1) - assert im.getpixel((0, 0)) == 1 - - im.putalpha(2) - assert im.mode == "LA" - assert im.getpixel((0, 0)) == (1, 2) - - im = Image.new("P", (1, 1), 1) - assert im.getpixel((0, 0)) == 1 - - im.putalpha(2) - assert im.mode == "PA" - assert im.getpixel((0, 0)) == (1, 2) - - im = Image.new("RGB", (1, 1), (1, 2, 3)) - assert im.getpixel((0, 0)) == (1, 2, 3) - - im.putalpha(4) - assert im.mode == "RGBA" - assert im.getpixel((0, 0)) == (1, 2, 3, 4) - - -def test_readonly() -> None: - im = Image.new("RGB", (1, 1), (1, 2, 3)) - im.readonly = 1 - - im.putalpha(4) - assert not im.readonly - assert im.mode == "RGBA" - assert im.getpixel((0, 0)) == (1, 2, 3, 4) +from __future__ import annotations + +from PIL import Image + + +def test_interface() -> None: + im = Image.new("RGBA", (1, 1), (1, 2, 3, 0)) + assert im.getpixel((0, 0)) == (1, 2, 3, 0) + + im = Image.new("RGBA", (1, 1), (1, 2, 3)) + assert im.getpixel((0, 0)) == (1, 2, 3, 255) + + im.putalpha(Image.new("L", im.size, 4)) + assert im.getpixel((0, 0)) == (1, 2, 3, 4) + + im.putalpha(5) + assert im.getpixel((0, 0)) == (1, 2, 3, 5) + + +def test_promote() -> None: + im = Image.new("L", (1, 1), 1) + assert im.getpixel((0, 0)) == 1 + + im.putalpha(2) + assert im.mode == "LA" + assert im.getpixel((0, 0)) == (1, 2) + + im = Image.new("P", (1, 1), 1) + assert im.getpixel((0, 0)) == 1 + + im.putalpha(2) + assert im.mode == "PA" + assert im.getpixel((0, 0)) == (1, 2) + + im = Image.new("RGB", (1, 1), (1, 2, 3)) + assert im.getpixel((0, 0)) == (1, 2, 3) + + im.putalpha(4) + assert im.mode == "RGBA" + assert im.getpixel((0, 0)) == (1, 2, 3, 4) + + +def test_readonly() -> None: + im = Image.new("RGB", (1, 1), (1, 2, 3)) + im.readonly = 1 + + im.putalpha(4) + assert not im.readonly + assert im.mode == "RGBA" + assert im.getpixel((0, 0)) == (1, 2, 3, 4) diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py index dad26ef14..f5dad6d54 100644 --- a/Tests/test_image_putdata.py +++ b/Tests/test_image_putdata.py @@ -1,125 +1,125 @@ -from __future__ import annotations - -import sys -from array import array - -import pytest - -from PIL import Image - -from .helper import assert_image_equal, hopper - - -def test_sanity() -> None: - im1 = hopper() - - data = list(im1.getdata()) - - im2 = Image.new(im1.mode, im1.size, 0) - im2.putdata(data) - - assert_image_equal(im1, im2) - - # readonly - im2 = Image.new(im1.mode, im2.size, 0) - im2.readonly = 1 - im2.putdata(data) - - assert not im2.readonly - assert_image_equal(im1, im2) - - -def test_long_integers() -> None: - # see bug-200802-systemerror - def put(value: int) -> tuple[int, int, int, int]: - im = Image.new("RGBA", (1, 1)) - im.putdata([value]) - return im.getpixel((0, 0)) - - assert put(0xFFFFFFFF) == (255, 255, 255, 255) - assert put(0xFFFFFFFF) == (255, 255, 255, 255) - assert put(-1) == (255, 255, 255, 255) - assert put(-1) == (255, 255, 255, 255) - if sys.maxsize > 2**32: - assert put(sys.maxsize) == (255, 255, 255, 255) - else: - assert put(sys.maxsize) == (255, 255, 255, 127) - - -def test_pypy_performance() -> None: - im = Image.new("L", (256, 256)) - im.putdata(list(range(256)) * 256) - - -def test_mode_with_L_with_float() -> None: - im = Image.new("L", (1, 1), 0) - im.putdata([2.0]) - assert im.getpixel((0, 0)) == 2 - - -@pytest.mark.parametrize("mode", ("I", "I;16", "I;16L", "I;16B")) -def test_mode_i(mode: str) -> None: - src = hopper("L") - data = list(src.getdata()) - im = Image.new(mode, src.size, 0) - im.putdata(data, 2, 256) - - target = [2 * elt + 256 for elt in data] - assert list(im.getdata()) == target - - -def test_mode_F() -> None: - src = hopper("L") - data = list(src.getdata()) - im = Image.new("F", src.size, 0) - im.putdata(data, 2.0, 256.0) - - target = [2.0 * float(elt) + 256.0 for elt in data] - assert list(im.getdata()) == target - - -@pytest.mark.parametrize("mode", ("BGR;15", "BGR;16", "BGR;24")) -def test_mode_BGR(mode: str) -> None: - data = [(16, 32, 49), (32, 32, 98)] - with pytest.warns(DeprecationWarning): - im = Image.new(mode, (1, 2)) - im.putdata(data) - - assert list(im.getdata()) == data - - -def test_array_B() -> None: - # shouldn't segfault - # see https://github.com/python-pillow/Pillow/issues/1008 - - arr = array("B", [0]) * 15000 - im = Image.new("L", (150, 100)) - im.putdata(arr) - - assert len(im.getdata()) == len(arr) - - -def test_array_F() -> None: - # shouldn't segfault - # see https://github.com/python-pillow/Pillow/issues/1008 - - im = Image.new("F", (150, 100)) - arr = array("f", [0.0]) * 15000 - im.putdata(arr) - - assert len(im.getdata()) == len(arr) - - -def test_not_flattened() -> None: - im = Image.new("L", (1, 1)) - with pytest.raises(TypeError): - im.putdata([[0]]) - with pytest.raises(TypeError): - im.putdata([[0]], 2) - - with pytest.raises(TypeError): - im = Image.new("I", (1, 1)) - im.putdata([[0]]) - with pytest.raises(TypeError): - im = Image.new("F", (1, 1)) - im.putdata([[0]]) +from __future__ import annotations + +import sys +from array import array + +import pytest + +from PIL import Image + +from .helper import assert_image_equal, hopper + + +def test_sanity() -> None: + im1 = hopper() + + data = list(im1.getdata()) + + im2 = Image.new(im1.mode, im1.size, 0) + im2.putdata(data) + + assert_image_equal(im1, im2) + + # readonly + im2 = Image.new(im1.mode, im2.size, 0) + im2.readonly = 1 + im2.putdata(data) + + assert not im2.readonly + assert_image_equal(im1, im2) + + +def test_long_integers() -> None: + # see bug-200802-systemerror + def put(value: int) -> tuple[int, int, int, int]: + im = Image.new("RGBA", (1, 1)) + im.putdata([value]) + return im.getpixel((0, 0)) + + assert put(0xFFFFFFFF) == (255, 255, 255, 255) + assert put(0xFFFFFFFF) == (255, 255, 255, 255) + assert put(-1) == (255, 255, 255, 255) + assert put(-1) == (255, 255, 255, 255) + if sys.maxsize > 2**32: + assert put(sys.maxsize) == (255, 255, 255, 255) + else: + assert put(sys.maxsize) == (255, 255, 255, 127) + + +def test_pypy_performance() -> None: + im = Image.new("L", (256, 256)) + im.putdata(list(range(256)) * 256) + + +def test_mode_with_L_with_float() -> None: + im = Image.new("L", (1, 1), 0) + im.putdata([2.0]) + assert im.getpixel((0, 0)) == 2 + + +@pytest.mark.parametrize("mode", ("I", "I;16", "I;16L", "I;16B")) +def test_mode_i(mode: str) -> None: + src = hopper("L") + data = list(src.getdata()) + im = Image.new(mode, src.size, 0) + im.putdata(data, 2, 256) + + target = [2 * elt + 256 for elt in data] + assert list(im.getdata()) == target + + +def test_mode_F() -> None: + src = hopper("L") + data = list(src.getdata()) + im = Image.new("F", src.size, 0) + im.putdata(data, 2.0, 256.0) + + target = [2.0 * float(elt) + 256.0 for elt in data] + assert list(im.getdata()) == target + + +@pytest.mark.parametrize("mode", ("BGR;15", "BGR;16", "BGR;24")) +def test_mode_BGR(mode: str) -> None: + data = [(16, 32, 49), (32, 32, 98)] + with pytest.warns(DeprecationWarning): + im = Image.new(mode, (1, 2)) + im.putdata(data) + + assert list(im.getdata()) == data + + +def test_array_B() -> None: + # shouldn't segfault + # see https://github.com/python-pillow/Pillow/issues/1008 + + arr = array("B", [0]) * 15000 + im = Image.new("L", (150, 100)) + im.putdata(arr) + + assert len(im.getdata()) == len(arr) + + +def test_array_F() -> None: + # shouldn't segfault + # see https://github.com/python-pillow/Pillow/issues/1008 + + im = Image.new("F", (150, 100)) + arr = array("f", [0.0]) * 15000 + im.putdata(arr) + + assert len(im.getdata()) == len(arr) + + +def test_not_flattened() -> None: + im = Image.new("L", (1, 1)) + with pytest.raises(TypeError): + im.putdata([[0]]) + with pytest.raises(TypeError): + im.putdata([[0]], 2) + + with pytest.raises(TypeError): + im = Image.new("I", (1, 1)) + im.putdata([[0]]) + with pytest.raises(TypeError): + im = Image.new("F", (1, 1)) + im.putdata([[0]]) diff --git a/Tests/test_image_putpalette.py b/Tests/test_image_putpalette.py index cc7cf58f0..c6b4f2796 100644 --- a/Tests/test_image_putpalette.py +++ b/Tests/test_image_putpalette.py @@ -1,99 +1,99 @@ -from __future__ import annotations - -import pytest - -from PIL import Image, ImagePalette - -from .helper import assert_image_equal, assert_image_equal_tofile, hopper - - -def test_putpalette() -> None: - def palette(mode: str) -> str | tuple[str, list[int]]: - im = hopper(mode).copy() - im.putpalette(list(range(256)) * 3) - p = im.getpalette() - if p: - return im.mode, p[:10] - return im.mode - - with pytest.raises(ValueError): - palette("1") - for mode in ["L", "LA", "P", "PA"]: - assert palette(mode) == ( - "PA" if "A" in mode else "P", - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], - ) - with pytest.raises(ValueError): - palette("I") - with pytest.raises(ValueError): - palette("F") - with pytest.raises(ValueError): - palette("RGB") - with pytest.raises(ValueError): - palette("RGBA") - with pytest.raises(ValueError): - palette("YCbCr") - - with Image.open("Tests/images/hopper_gray.jpg") as im: - assert im.mode == "L" - im.putpalette(list(range(256)) * 3) - - with Image.open("Tests/images/la.tga") as im: - assert im.mode == "LA" - im.putpalette(list(range(256)) * 3) - - -def test_imagepalette() -> None: - im = hopper("P") - im.putpalette(ImagePalette.negative()) - assert_image_equal_tofile(im.convert("RGB"), "Tests/images/palette_negative.png") - - im.putpalette(ImagePalette.random()) - - im.putpalette(ImagePalette.sepia()) - assert_image_equal_tofile(im.convert("RGB"), "Tests/images/palette_sepia.png") - - im.putpalette(ImagePalette.wedge()) - assert_image_equal_tofile(im.convert("RGB"), "Tests/images/palette_wedge.png") - - -def test_putpalette_with_alpha_values() -> None: - with Image.open("Tests/images/transparent.gif") as im: - expected = im.convert("RGBA") - - palette = im.getpalette() - transparency = im.info.pop("transparency") - - palette_with_alpha_values = [] - for i in range(256): - color = palette[i * 3 : i * 3 + 3] - alpha = 0 if i == transparency else 255 - palette_with_alpha_values += color + [alpha] - im.putpalette(palette_with_alpha_values, "RGBA") - - assert_image_equal(im.convert("RGBA"), expected) - - -@pytest.mark.parametrize( - "mode, palette", - ( - ("RGBA", (1, 2, 3, 4)), - ("RGBAX", (1, 2, 3, 4, 0)), - ), -) -def test_rgba_palette(mode: str, palette: tuple[int, ...]) -> None: - im = Image.new("P", (1, 1)) - im.putpalette(palette, mode) - assert im.getpalette() == [1, 2, 3] - assert im.palette.colors == {(1, 2, 3, 4): 0} - - -def test_empty_palette() -> None: - im = Image.new("P", (1, 1)) - assert im.getpalette() == [] - - -def test_undefined_palette_index() -> None: - im = Image.new("P", (1, 1), 3) - im.putpalette((1, 2, 3)) - assert im.convert("RGB").getpixel((0, 0)) == (0, 0, 0) +from __future__ import annotations + +import pytest + +from PIL import Image, ImagePalette + +from .helper import assert_image_equal, assert_image_equal_tofile, hopper + + +def test_putpalette() -> None: + def palette(mode: str) -> str | tuple[str, list[int]]: + im = hopper(mode).copy() + im.putpalette(list(range(256)) * 3) + p = im.getpalette() + if p: + return im.mode, p[:10] + return im.mode + + with pytest.raises(ValueError): + palette("1") + for mode in ["L", "LA", "P", "PA"]: + assert palette(mode) == ( + "PA" if "A" in mode else "P", + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + ) + with pytest.raises(ValueError): + palette("I") + with pytest.raises(ValueError): + palette("F") + with pytest.raises(ValueError): + palette("RGB") + with pytest.raises(ValueError): + palette("RGBA") + with pytest.raises(ValueError): + palette("YCbCr") + + with Image.open("Tests/images/hopper_gray.jpg") as im: + assert im.mode == "L" + im.putpalette(list(range(256)) * 3) + + with Image.open("Tests/images/la.tga") as im: + assert im.mode == "LA" + im.putpalette(list(range(256)) * 3) + + +def test_imagepalette() -> None: + im = hopper("P") + im.putpalette(ImagePalette.negative()) + assert_image_equal_tofile(im.convert("RGB"), "Tests/images/palette_negative.png") + + im.putpalette(ImagePalette.random()) + + im.putpalette(ImagePalette.sepia()) + assert_image_equal_tofile(im.convert("RGB"), "Tests/images/palette_sepia.png") + + im.putpalette(ImagePalette.wedge()) + assert_image_equal_tofile(im.convert("RGB"), "Tests/images/palette_wedge.png") + + +def test_putpalette_with_alpha_values() -> None: + with Image.open("Tests/images/transparent.gif") as im: + expected = im.convert("RGBA") + + palette = im.getpalette() + transparency = im.info.pop("transparency") + + palette_with_alpha_values = [] + for i in range(256): + color = palette[i * 3 : i * 3 + 3] + alpha = 0 if i == transparency else 255 + palette_with_alpha_values += color + [alpha] + im.putpalette(palette_with_alpha_values, "RGBA") + + assert_image_equal(im.convert("RGBA"), expected) + + +@pytest.mark.parametrize( + "mode, palette", + ( + ("RGBA", (1, 2, 3, 4)), + ("RGBAX", (1, 2, 3, 4, 0)), + ), +) +def test_rgba_palette(mode: str, palette: tuple[int, ...]) -> None: + im = Image.new("P", (1, 1)) + im.putpalette(palette, mode) + assert im.getpalette() == [1, 2, 3] + assert im.palette.colors == {(1, 2, 3, 4): 0} + + +def test_empty_palette() -> None: + im = Image.new("P", (1, 1)) + assert im.getpalette() == [] + + +def test_undefined_palette_index() -> None: + im = Image.new("P", (1, 1), 3) + im.putpalette((1, 2, 3)) + assert im.convert("RGB").getpixel((0, 0)) == (0, 0, 0) diff --git a/Tests/test_image_quantize.py b/Tests/test_image_quantize.py index 2daaf5c3c..13ac35d92 100644 --- a/Tests/test_image_quantize.py +++ b/Tests/test_image_quantize.py @@ -1,157 +1,157 @@ -from __future__ import annotations - -import pytest -from packaging.version import parse as parse_version - -from PIL import Image, features - -from .helper import assert_image_similar, hopper, is_ppc64le, skip_unless_feature - - -def test_sanity() -> None: - image = hopper() - converted = image.quantize() - assert converted.mode == "P" - assert_image_similar(converted.convert("RGB"), image, 10) - - image = hopper() - converted = image.quantize(palette=hopper("P")) - assert converted.mode == "P" - assert_image_similar(converted.convert("RGB"), image, 60) - - -@skip_unless_feature("libimagequant") -def test_libimagequant_quantize() -> None: - image = hopper() - if is_ppc64le(): - version = features.version_feature("libimagequant") - assert version is not None - if parse_version(version) < parse_version("4"): - pytest.skip("Fails with libimagequant earlier than 4.0.0 on ppc64le") - converted = image.quantize(100, Image.Quantize.LIBIMAGEQUANT) - assert converted.mode == "P" - assert_image_similar(converted.convert("RGB"), image, 15) - assert len(converted.getcolors()) == 100 - - -def test_octree_quantize() -> None: - image = hopper() - converted = image.quantize(100, Image.Quantize.FASTOCTREE) - assert converted.mode == "P" - assert_image_similar(converted.convert("RGB"), image, 20) - assert len(converted.getcolors()) == 100 - - -def test_rgba_quantize() -> None: - image = hopper("RGBA") - with pytest.raises(ValueError): - image.quantize(method=0) - - assert image.quantize().convert().mode == "RGBA" - - -def test_quantize() -> None: - with Image.open("Tests/images/caption_6_33_22.png") as image: - image = image.convert("RGB") - converted = image.quantize() - assert converted.mode == "P" - assert_image_similar(converted.convert("RGB"), image, 1) - - -def test_quantize_no_dither() -> None: - image = hopper() - with Image.open("Tests/images/caption_6_33_22.png") as palette: - palette = palette.convert("P") - - converted = image.quantize(dither=Image.Dither.NONE, palette=palette) - assert converted.mode == "P" - assert converted.palette.palette == palette.palette.palette - - -def test_quantize_no_dither2() -> None: - im = Image.new("RGB", (9, 1)) - im.putdata([(p,) * 3 for p in range(0, 36, 4)]) - - palette = Image.new("P", (1, 1)) - data = (0, 0, 0, 32, 32, 32) - palette.putpalette(data) - quantized = im.quantize(dither=Image.Dither.NONE, palette=palette) - - assert tuple(quantized.palette.palette) == data - - px = quantized.load() - for x in range(9): - assert px[x, 0] == (0 if x < 5 else 1) - - -def test_quantize_dither_diff() -> None: - image = hopper() - with Image.open("Tests/images/caption_6_33_22.png") as palette: - palette = palette.convert("P") - - dither = image.quantize(dither=Image.Dither.FLOYDSTEINBERG, palette=palette) - nodither = image.quantize(dither=Image.Dither.NONE, palette=palette) - - assert dither.tobytes() != nodither.tobytes() - - -@pytest.mark.parametrize( - "method", (Image.Quantize.MEDIANCUT, Image.Quantize.MAXCOVERAGE) -) -def test_quantize_kmeans(method) -> None: - im = hopper() - no_kmeans = im.quantize(kmeans=0, method=method) - kmeans = im.quantize(kmeans=1, method=method) - assert kmeans.tobytes() != no_kmeans.tobytes() - - with pytest.raises(ValueError): - im.quantize(kmeans=-1, method=method) - - -def test_colors() -> None: - im = hopper() - colors = 2 - converted = im.quantize(colors) - assert len(converted.palette.palette) == colors * len("RGB") - - -def test_transparent_colors_equal() -> None: - im = Image.new("RGBA", (1, 2), (0, 0, 0, 0)) - px = im.load() - px[0, 1] = (255, 255, 255, 0) - - converted = im.quantize() - converted_px = converted.load() - assert converted_px[0, 0] == converted_px[0, 1] - - -@pytest.mark.parametrize( - "method, color", - ( - (Image.Quantize.MEDIANCUT, (0, 0, 0)), - (Image.Quantize.MAXCOVERAGE, (0, 0, 0)), - (Image.Quantize.FASTOCTREE, (0, 0, 0)), - (Image.Quantize.FASTOCTREE, (0, 0, 0, 0)), - ), -) -def test_palette(method: Image.Quantize, color: tuple[int, ...]) -> None: - im = Image.new("RGBA" if len(color) == 4 else "RGB", (1, 1), color) - - converted = im.quantize(method=method) - converted_px = converted.load() - assert converted_px[0, 0] == converted.palette.colors[color] - - -def test_small_palette() -> None: - # Arrange - im = hopper() - - colors = (255, 0, 0, 0, 0, 255) - p = Image.new("P", (1, 1)) - p.putpalette(colors) - - # Act - im = im.quantize(palette=p) - - # Assert - assert len(im.getcolors()) == 2 +from __future__ import annotations + +import pytest +from packaging.version import parse as parse_version + +from PIL import Image, features + +from .helper import assert_image_similar, hopper, is_ppc64le, skip_unless_feature + + +def test_sanity() -> None: + image = hopper() + converted = image.quantize() + assert converted.mode == "P" + assert_image_similar(converted.convert("RGB"), image, 10) + + image = hopper() + converted = image.quantize(palette=hopper("P")) + assert converted.mode == "P" + assert_image_similar(converted.convert("RGB"), image, 60) + + +@skip_unless_feature("libimagequant") +def test_libimagequant_quantize() -> None: + image = hopper() + if is_ppc64le(): + version = features.version_feature("libimagequant") + assert version is not None + if parse_version(version) < parse_version("4"): + pytest.skip("Fails with libimagequant earlier than 4.0.0 on ppc64le") + converted = image.quantize(100, Image.Quantize.LIBIMAGEQUANT) + assert converted.mode == "P" + assert_image_similar(converted.convert("RGB"), image, 15) + assert len(converted.getcolors()) == 100 + + +def test_octree_quantize() -> None: + image = hopper() + converted = image.quantize(100, Image.Quantize.FASTOCTREE) + assert converted.mode == "P" + assert_image_similar(converted.convert("RGB"), image, 20) + assert len(converted.getcolors()) == 100 + + +def test_rgba_quantize() -> None: + image = hopper("RGBA") + with pytest.raises(ValueError): + image.quantize(method=0) + + assert image.quantize().convert().mode == "RGBA" + + +def test_quantize() -> None: + with Image.open("Tests/images/caption_6_33_22.png") as image: + image = image.convert("RGB") + converted = image.quantize() + assert converted.mode == "P" + assert_image_similar(converted.convert("RGB"), image, 1) + + +def test_quantize_no_dither() -> None: + image = hopper() + with Image.open("Tests/images/caption_6_33_22.png") as palette: + palette = palette.convert("P") + + converted = image.quantize(dither=Image.Dither.NONE, palette=palette) + assert converted.mode == "P" + assert converted.palette.palette == palette.palette.palette + + +def test_quantize_no_dither2() -> None: + im = Image.new("RGB", (9, 1)) + im.putdata([(p,) * 3 for p in range(0, 36, 4)]) + + palette = Image.new("P", (1, 1)) + data = (0, 0, 0, 32, 32, 32) + palette.putpalette(data) + quantized = im.quantize(dither=Image.Dither.NONE, palette=palette) + + assert tuple(quantized.palette.palette) == data + + px = quantized.load() + for x in range(9): + assert px[x, 0] == (0 if x < 5 else 1) + + +def test_quantize_dither_diff() -> None: + image = hopper() + with Image.open("Tests/images/caption_6_33_22.png") as palette: + palette = palette.convert("P") + + dither = image.quantize(dither=Image.Dither.FLOYDSTEINBERG, palette=palette) + nodither = image.quantize(dither=Image.Dither.NONE, palette=palette) + + assert dither.tobytes() != nodither.tobytes() + + +@pytest.mark.parametrize( + "method", (Image.Quantize.MEDIANCUT, Image.Quantize.MAXCOVERAGE) +) +def test_quantize_kmeans(method) -> None: + im = hopper() + no_kmeans = im.quantize(kmeans=0, method=method) + kmeans = im.quantize(kmeans=1, method=method) + assert kmeans.tobytes() != no_kmeans.tobytes() + + with pytest.raises(ValueError): + im.quantize(kmeans=-1, method=method) + + +def test_colors() -> None: + im = hopper() + colors = 2 + converted = im.quantize(colors) + assert len(converted.palette.palette) == colors * len("RGB") + + +def test_transparent_colors_equal() -> None: + im = Image.new("RGBA", (1, 2), (0, 0, 0, 0)) + px = im.load() + px[0, 1] = (255, 255, 255, 0) + + converted = im.quantize() + converted_px = converted.load() + assert converted_px[0, 0] == converted_px[0, 1] + + +@pytest.mark.parametrize( + "method, color", + ( + (Image.Quantize.MEDIANCUT, (0, 0, 0)), + (Image.Quantize.MAXCOVERAGE, (0, 0, 0)), + (Image.Quantize.FASTOCTREE, (0, 0, 0)), + (Image.Quantize.FASTOCTREE, (0, 0, 0, 0)), + ), +) +def test_palette(method: Image.Quantize, color: tuple[int, ...]) -> None: + im = Image.new("RGBA" if len(color) == 4 else "RGB", (1, 1), color) + + converted = im.quantize(method=method) + converted_px = converted.load() + assert converted_px[0, 0] == converted.palette.colors[color] + + +def test_small_palette() -> None: + # Arrange + im = hopper() + + colors = (255, 0, 0, 0, 0, 255) + p = Image.new("P", (1, 1)) + p.putpalette(colors) + + # Act + im = im.quantize(palette=p) + + # Assert + assert len(im.getcolors()) == 2 diff --git a/Tests/test_image_reduce.py b/Tests/test_image_reduce.py index f6609a1a0..6b4cd0d5b 100644 --- a/Tests/test_image_reduce.py +++ b/Tests/test_image_reduce.py @@ -1,285 +1,285 @@ -from __future__ import annotations - -import pytest - -from PIL import Image, ImageMath, ImageMode - -from .helper import convert_to_comparable, skip_unless_feature - -codecs = dir(Image.core) - - -# There are several internal implementations -remarkable_factors = [ - # special implementations - 1, - 2, - 3, - 4, - 5, - 6, - # 1xN implementation - (1, 2), - (1, 3), - (1, 4), - (1, 7), - # Nx1 implementation - (2, 1), - (3, 1), - (4, 1), - (7, 1), - # general implementation with different paths - (4, 6), - (5, 6), - (4, 7), - (5, 7), - (19, 17), -] - -gradients_image = Image.open("Tests/images/radial_gradients.png") -gradients_image.load() - - -@pytest.mark.parametrize( - "size, expected", - ( - (3, (4, 4)), - ((3, 1), (4, 10)), - ((1, 3), (10, 4)), - ), -) -def test_args_factor(size: int | tuple[int, int], expected: tuple[int, int]) -> None: - im = Image.new("L", (10, 10)) - assert expected == im.reduce(size).size - - -@pytest.mark.parametrize( - "size, expected_error", ((0, ValueError), (2.0, TypeError), ((0, 10), ValueError)) -) -def test_args_factor_error(size: float | tuple[int, int], expected_error) -> None: - im = Image.new("L", (10, 10)) - with pytest.raises(expected_error): - im.reduce(size) - - -@pytest.mark.parametrize( - "size, expected", - ( - ((0, 0, 10, 10), (5, 5)), - ((5, 5, 6, 6), (1, 1)), - ), -) -def test_args_box(size: tuple[int, int, int, int], expected: tuple[int, int]) -> None: - im = Image.new("L", (10, 10)) - assert expected == im.reduce(2, size).size - - -@pytest.mark.parametrize( - "size, expected_error", - ( - ("stri", TypeError), - ((0, 0, 11, 10), ValueError), - ((0, 0, 10, 11), ValueError), - ((-1, 0, 10, 10), ValueError), - ((0, -1, 10, 10), ValueError), - ((0, 5, 10, 5), ValueError), - ((5, 0, 5, 10), ValueError), - ), -) -def test_args_box_error(size: str | tuple[int, int, int, int], expected_error) -> None: - im = Image.new("L", (10, 10)) - with pytest.raises(expected_error): - im.reduce(2, size).size - - -@pytest.mark.parametrize("mode", ("P", "1", "I;16")) -def test_unsupported_modes(mode: str) -> None: - im = Image.new("P", (10, 10)) - with pytest.raises(ValueError): - im.reduce(3) - - -def get_image(mode: str) -> Image.Image: - mode_info = ImageMode.getmode(mode) - if mode_info.basetype == "L": - bands: list[Image.Image] = [gradients_image] - for _ in mode_info.bands[1:]: - # rotate previous image - band = bands[-1].transpose(Image.Transpose.ROTATE_90) - bands.append(band) - # Correct alpha channel by transforming completely transparent pixels. - # Low alpha values also emphasize error after alpha multiplication. - if mode.endswith("A"): - bands[-1] = bands[-1].point(lambda x: int(85 + x / 1.5)) - im = Image.merge(mode, bands) - else: - assert len(mode_info.bands) == 1 - im = gradients_image.convert(mode) - # change the height to make a not-square image - return im.crop((0, 0, im.width, im.height - 5)) - - -def compare_reduce_with_box(im: Image.Image, factor: int | tuple[int, int]) -> None: - box = (11, 13, 146, 164) - reduced = im.reduce(factor, box=box) - reference = im.crop(box).reduce(factor) - assert reduced == reference - - -def compare_reduce_with_reference( - im: Image.Image, - factor: int | tuple[int, int], - average_diff: float = 0.4, - max_diff: int = 1, -) -> None: - """Image.reduce() should look very similar to Image.resize(BOX). - - A reference image is compiled from a large source area - and possible last column and last row. - +-----------+ - |..........c| - |..........c| - |..........c| - |rrrrrrrrrrp| - +-----------+ - """ - reduced = im.reduce(factor) - - if not isinstance(factor, (list, tuple)): - factor = (factor, factor) - - reference = Image.new(im.mode, reduced.size) - area_size = (im.size[0] // factor[0], im.size[1] // factor[1]) - area_box = (0, 0, area_size[0] * factor[0], area_size[1] * factor[1]) - area = im.resize(area_size, Image.Resampling.BOX, area_box) - reference.paste(area, (0, 0)) - - if area_size[0] < reduced.size[0]: - assert reduced.size[0] - area_size[0] == 1 - last_column_box = (area_box[2], 0, im.size[0], area_box[3]) - last_column = im.resize( - (1, area_size[1]), Image.Resampling.BOX, last_column_box - ) - reference.paste(last_column, (area_size[0], 0)) - - if area_size[1] < reduced.size[1]: - assert reduced.size[1] - area_size[1] == 1 - last_row_box = (0, area_box[3], area_box[2], im.size[1]) - last_row = im.resize((area_size[0], 1), Image.Resampling.BOX, last_row_box) - reference.paste(last_row, (0, area_size[1])) - - if area_size[0] < reduced.size[0] and area_size[1] < reduced.size[1]: - last_pixel_box = (area_box[2], area_box[3], im.size[0], im.size[1]) - last_pixel = im.resize((1, 1), Image.Resampling.BOX, last_pixel_box) - reference.paste(last_pixel, area_size) - - assert_compare_images(reduced, reference, average_diff, max_diff) - - -def assert_compare_images( - a: Image.Image, b: Image.Image, max_average_diff: float, max_diff: int = 255 -) -> None: - assert a.mode == b.mode, f"got mode {repr(a.mode)}, expected {repr(b.mode)}" - assert a.size == b.size, f"got size {repr(a.size)}, expected {repr(b.size)}" - - a, b = convert_to_comparable(a, b) - - bands = ImageMode.getmode(a.mode).bands - for band, ach, bch in zip(bands, a.split(), b.split()): - ch_diff = ImageMath.lambda_eval( - lambda args: args["convert"](abs(args["a"] - args["b"]), "L"), a=ach, b=bch - ) - ch_hist = ch_diff.histogram() - - average_diff = sum(i * num for i, num in enumerate(ch_hist)) / ( - a.size[0] * a.size[1] - ) - msg = ( - f"average pixel value difference {average_diff:.4f} > " - f"expected {max_average_diff:.4f} for '{band}' band" - ) - assert max_average_diff >= average_diff, msg - - last_diff = [i for i, num in enumerate(ch_hist) if num > 0][-1] - assert max_diff >= last_diff, ( - f"max pixel value difference {last_diff} > expected {max_diff} " - f"for '{band}' band" - ) - - -@pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_L(factor: int | tuple[int, int]) -> None: - im = get_image("L") - compare_reduce_with_reference(im, factor) - compare_reduce_with_box(im, factor) - - -@pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_LA(factor: int | tuple[int, int]) -> None: - im = get_image("LA") - compare_reduce_with_reference(im, factor, 0.8, 5) - - -@pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_LA_opaque(factor: int | tuple[int, int]) -> None: - im = get_image("LA") - # With opaque alpha, an error should be way smaller. - im.putalpha(Image.new("L", im.size, 255)) - compare_reduce_with_reference(im, factor) - compare_reduce_with_box(im, factor) - - -@pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_La(factor: int | tuple[int, int]) -> None: - im = get_image("La") - compare_reduce_with_reference(im, factor) - compare_reduce_with_box(im, factor) - - -@pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_RGB(factor: int | tuple[int, int]) -> None: - im = get_image("RGB") - compare_reduce_with_reference(im, factor) - compare_reduce_with_box(im, factor) - - -@pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_RGBA(factor: int | tuple[int, int]) -> None: - im = get_image("RGBA") - compare_reduce_with_reference(im, factor, 0.8, 5) - - -@pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_RGBA_opaque(factor: int | tuple[int, int]) -> None: - im = get_image("RGBA") - # With opaque alpha, an error should be way smaller. - im.putalpha(Image.new("L", im.size, 255)) - compare_reduce_with_reference(im, factor) - compare_reduce_with_box(im, factor) - - -@pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_RGBa(factor: int | tuple[int, int]) -> None: - im = get_image("RGBa") - compare_reduce_with_reference(im, factor) - compare_reduce_with_box(im, factor) - - -@pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_I(factor: int | tuple[int, int]) -> None: - im = get_image("I") - compare_reduce_with_reference(im, factor) - compare_reduce_with_box(im, factor) - - -@pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_F(factor: int | tuple[int, int]) -> None: - im = get_image("F") - compare_reduce_with_reference(im, factor, 0, 0) - compare_reduce_with_box(im, factor) - - -@skip_unless_feature("jpg_2000") -def test_jpeg2k() -> None: - with Image.open("Tests/images/test-card-lossless.jp2") as im: - assert im.reduce(2).size == (320, 240) +from __future__ import annotations + +import pytest + +from PIL import Image, ImageMath, ImageMode + +from .helper import convert_to_comparable, skip_unless_feature + +codecs = dir(Image.core) + + +# There are several internal implementations +remarkable_factors = [ + # special implementations + 1, + 2, + 3, + 4, + 5, + 6, + # 1xN implementation + (1, 2), + (1, 3), + (1, 4), + (1, 7), + # Nx1 implementation + (2, 1), + (3, 1), + (4, 1), + (7, 1), + # general implementation with different paths + (4, 6), + (5, 6), + (4, 7), + (5, 7), + (19, 17), +] + +gradients_image = Image.open("Tests/images/radial_gradients.png") +gradients_image.load() + + +@pytest.mark.parametrize( + "size, expected", + ( + (3, (4, 4)), + ((3, 1), (4, 10)), + ((1, 3), (10, 4)), + ), +) +def test_args_factor(size: int | tuple[int, int], expected: tuple[int, int]) -> None: + im = Image.new("L", (10, 10)) + assert expected == im.reduce(size).size + + +@pytest.mark.parametrize( + "size, expected_error", ((0, ValueError), (2.0, TypeError), ((0, 10), ValueError)) +) +def test_args_factor_error(size: float | tuple[int, int], expected_error) -> None: + im = Image.new("L", (10, 10)) + with pytest.raises(expected_error): + im.reduce(size) + + +@pytest.mark.parametrize( + "size, expected", + ( + ((0, 0, 10, 10), (5, 5)), + ((5, 5, 6, 6), (1, 1)), + ), +) +def test_args_box(size: tuple[int, int, int, int], expected: tuple[int, int]) -> None: + im = Image.new("L", (10, 10)) + assert expected == im.reduce(2, size).size + + +@pytest.mark.parametrize( + "size, expected_error", + ( + ("stri", TypeError), + ((0, 0, 11, 10), ValueError), + ((0, 0, 10, 11), ValueError), + ((-1, 0, 10, 10), ValueError), + ((0, -1, 10, 10), ValueError), + ((0, 5, 10, 5), ValueError), + ((5, 0, 5, 10), ValueError), + ), +) +def test_args_box_error(size: str | tuple[int, int, int, int], expected_error) -> None: + im = Image.new("L", (10, 10)) + with pytest.raises(expected_error): + im.reduce(2, size).size + + +@pytest.mark.parametrize("mode", ("P", "1", "I;16")) +def test_unsupported_modes(mode: str) -> None: + im = Image.new("P", (10, 10)) + with pytest.raises(ValueError): + im.reduce(3) + + +def get_image(mode: str) -> Image.Image: + mode_info = ImageMode.getmode(mode) + if mode_info.basetype == "L": + bands: list[Image.Image] = [gradients_image] + for _ in mode_info.bands[1:]: + # rotate previous image + band = bands[-1].transpose(Image.Transpose.ROTATE_90) + bands.append(band) + # Correct alpha channel by transforming completely transparent pixels. + # Low alpha values also emphasize error after alpha multiplication. + if mode.endswith("A"): + bands[-1] = bands[-1].point(lambda x: int(85 + x / 1.5)) + im = Image.merge(mode, bands) + else: + assert len(mode_info.bands) == 1 + im = gradients_image.convert(mode) + # change the height to make a not-square image + return im.crop((0, 0, im.width, im.height - 5)) + + +def compare_reduce_with_box(im: Image.Image, factor: int | tuple[int, int]) -> None: + box = (11, 13, 146, 164) + reduced = im.reduce(factor, box=box) + reference = im.crop(box).reduce(factor) + assert reduced == reference + + +def compare_reduce_with_reference( + im: Image.Image, + factor: int | tuple[int, int], + average_diff: float = 0.4, + max_diff: int = 1, +) -> None: + """Image.reduce() should look very similar to Image.resize(BOX). + + A reference image is compiled from a large source area + and possible last column and last row. + +-----------+ + |..........c| + |..........c| + |..........c| + |rrrrrrrrrrp| + +-----------+ + """ + reduced = im.reduce(factor) + + if not isinstance(factor, (list, tuple)): + factor = (factor, factor) + + reference = Image.new(im.mode, reduced.size) + area_size = (im.size[0] // factor[0], im.size[1] // factor[1]) + area_box = (0, 0, area_size[0] * factor[0], area_size[1] * factor[1]) + area = im.resize(area_size, Image.Resampling.BOX, area_box) + reference.paste(area, (0, 0)) + + if area_size[0] < reduced.size[0]: + assert reduced.size[0] - area_size[0] == 1 + last_column_box = (area_box[2], 0, im.size[0], area_box[3]) + last_column = im.resize( + (1, area_size[1]), Image.Resampling.BOX, last_column_box + ) + reference.paste(last_column, (area_size[0], 0)) + + if area_size[1] < reduced.size[1]: + assert reduced.size[1] - area_size[1] == 1 + last_row_box = (0, area_box[3], area_box[2], im.size[1]) + last_row = im.resize((area_size[0], 1), Image.Resampling.BOX, last_row_box) + reference.paste(last_row, (0, area_size[1])) + + if area_size[0] < reduced.size[0] and area_size[1] < reduced.size[1]: + last_pixel_box = (area_box[2], area_box[3], im.size[0], im.size[1]) + last_pixel = im.resize((1, 1), Image.Resampling.BOX, last_pixel_box) + reference.paste(last_pixel, area_size) + + assert_compare_images(reduced, reference, average_diff, max_diff) + + +def assert_compare_images( + a: Image.Image, b: Image.Image, max_average_diff: float, max_diff: int = 255 +) -> None: + assert a.mode == b.mode, f"got mode {repr(a.mode)}, expected {repr(b.mode)}" + assert a.size == b.size, f"got size {repr(a.size)}, expected {repr(b.size)}" + + a, b = convert_to_comparable(a, b) + + bands = ImageMode.getmode(a.mode).bands + for band, ach, bch in zip(bands, a.split(), b.split()): + ch_diff = ImageMath.lambda_eval( + lambda args: args["convert"](abs(args["a"] - args["b"]), "L"), a=ach, b=bch + ) + ch_hist = ch_diff.histogram() + + average_diff = sum(i * num for i, num in enumerate(ch_hist)) / ( + a.size[0] * a.size[1] + ) + msg = ( + f"average pixel value difference {average_diff:.4f} > " + f"expected {max_average_diff:.4f} for '{band}' band" + ) + assert max_average_diff >= average_diff, msg + + last_diff = [i for i, num in enumerate(ch_hist) if num > 0][-1] + assert max_diff >= last_diff, ( + f"max pixel value difference {last_diff} > expected {max_diff} " + f"for '{band}' band" + ) + + +@pytest.mark.parametrize("factor", remarkable_factors) +def test_mode_L(factor: int | tuple[int, int]) -> None: + im = get_image("L") + compare_reduce_with_reference(im, factor) + compare_reduce_with_box(im, factor) + + +@pytest.mark.parametrize("factor", remarkable_factors) +def test_mode_LA(factor: int | tuple[int, int]) -> None: + im = get_image("LA") + compare_reduce_with_reference(im, factor, 0.8, 5) + + +@pytest.mark.parametrize("factor", remarkable_factors) +def test_mode_LA_opaque(factor: int | tuple[int, int]) -> None: + im = get_image("LA") + # With opaque alpha, an error should be way smaller. + im.putalpha(Image.new("L", im.size, 255)) + compare_reduce_with_reference(im, factor) + compare_reduce_with_box(im, factor) + + +@pytest.mark.parametrize("factor", remarkable_factors) +def test_mode_La(factor: int | tuple[int, int]) -> None: + im = get_image("La") + compare_reduce_with_reference(im, factor) + compare_reduce_with_box(im, factor) + + +@pytest.mark.parametrize("factor", remarkable_factors) +def test_mode_RGB(factor: int | tuple[int, int]) -> None: + im = get_image("RGB") + compare_reduce_with_reference(im, factor) + compare_reduce_with_box(im, factor) + + +@pytest.mark.parametrize("factor", remarkable_factors) +def test_mode_RGBA(factor: int | tuple[int, int]) -> None: + im = get_image("RGBA") + compare_reduce_with_reference(im, factor, 0.8, 5) + + +@pytest.mark.parametrize("factor", remarkable_factors) +def test_mode_RGBA_opaque(factor: int | tuple[int, int]) -> None: + im = get_image("RGBA") + # With opaque alpha, an error should be way smaller. + im.putalpha(Image.new("L", im.size, 255)) + compare_reduce_with_reference(im, factor) + compare_reduce_with_box(im, factor) + + +@pytest.mark.parametrize("factor", remarkable_factors) +def test_mode_RGBa(factor: int | tuple[int, int]) -> None: + im = get_image("RGBa") + compare_reduce_with_reference(im, factor) + compare_reduce_with_box(im, factor) + + +@pytest.mark.parametrize("factor", remarkable_factors) +def test_mode_I(factor: int | tuple[int, int]) -> None: + im = get_image("I") + compare_reduce_with_reference(im, factor) + compare_reduce_with_box(im, factor) + + +@pytest.mark.parametrize("factor", remarkable_factors) +def test_mode_F(factor: int | tuple[int, int]) -> None: + im = get_image("F") + compare_reduce_with_reference(im, factor, 0, 0) + compare_reduce_with_box(im, factor) + + +@skip_unless_feature("jpg_2000") +def test_jpeg2k() -> None: + with Image.open("Tests/images/test-card-lossless.jp2") as im: + assert im.reduce(2).size == (320, 240) diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index 9b3bdf330..309dc97c7 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -1,611 +1,611 @@ -from __future__ import annotations - -from contextlib import contextmanager -from typing import Generator - -import pytest - -from PIL import Image, ImageDraw - -from .helper import ( - assert_image_equal, - assert_image_similar, - hopper, - mark_if_feature_version, -) - - -class TestImagingResampleVulnerability: - # see https://github.com/python-pillow/Pillow/issues/1710 - def test_overflow(self) -> None: - im = hopper("L") - size_too_large = 0x100000008 // 4 - size_normal = 1000 # unimportant - for xsize, ysize in ( - (size_too_large, size_normal), - (size_normal, size_too_large), - ): - with pytest.raises(MemoryError): - # any resampling filter will do here - im.im.resize((xsize, ysize), Image.Resampling.BILINEAR) - - def test_invalid_size(self) -> None: - im = hopper() - - # Should not crash - im.resize((100, 100)) - - with pytest.raises(ValueError): - im.resize((-100, 100)) - - with pytest.raises(ValueError): - im.resize((100, -100)) - - def test_modify_after_resizing(self) -> None: - im = hopper("RGB") - # get copy with same size - copy = im.resize(im.size) - # some in-place operation - copy.paste("black", (0, 0, im.width // 2, im.height // 2)) - # image should be different - assert im.tobytes() != copy.tobytes() - - -class TestImagingCoreResampleAccuracy: - def make_case(self, mode: str, size: tuple[int, int], color: int) -> Image.Image: - """Makes a sample image with two dark and two bright squares. - For example: - e0 e0 1f 1f - e0 e0 1f 1f - 1f 1f e0 e0 - 1f 1f e0 e0 - """ - case = Image.new("L", size, 255 - color) - rectangle = ImageDraw.Draw(case).rectangle - rectangle((0, 0, size[0] // 2 - 1, size[1] // 2 - 1), color) - rectangle((size[0] // 2, size[1] // 2, size[0], size[1]), color) - - return Image.merge(mode, [case] * len(mode)) - - def make_sample(self, data: str, size: tuple[int, int]) -> Image.Image: - """Restores a sample image from given data string which contains - hex-encoded pixels from the top left fourth of a sample. - """ - data = data.replace(" ", "") - sample = Image.new("L", size) - s_px = sample.load() - w, h = size[0] // 2, size[1] // 2 - for y in range(h): - for x in range(w): - val = int(data[(y * w + x) * 2 : (y * w + x + 1) * 2], 16) - s_px[x, y] = val - s_px[size[0] - x - 1, size[1] - y - 1] = val - s_px[x, size[1] - y - 1] = 255 - val - s_px[size[0] - x - 1, y] = 255 - val - return sample - - def check_case(self, case: Image.Image, sample: Image.Image) -> None: - s_px = sample.load() - c_px = case.load() - for y in range(case.size[1]): - for x in range(case.size[0]): - if c_px[x, y] != s_px[x, y]: - message = ( - f"\nHave: \n{self.serialize_image(case)}\n" - f"\nExpected: \n{self.serialize_image(sample)}" - ) - assert s_px[x, y] == c_px[x, y], message - - def serialize_image(self, image: Image.Image) -> str: - s_px = image.load() - return "\n".join( - " ".join(f"{s_px[x, y]:02x}" for x in range(image.size[0])) - for y in range(image.size[1]) - ) - - @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_reduce_box(self, mode: str) -> None: - case = self.make_case(mode, (8, 8), 0xE1) - case = case.resize((4, 4), Image.Resampling.BOX) - # fmt: off - data = ("e1 e1" - "e1 e1") - # fmt: on - for channel in case.split(): - self.check_case(channel, self.make_sample(data, (4, 4))) - - @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_reduce_bilinear(self, mode: str) -> None: - case = self.make_case(mode, (8, 8), 0xE1) - case = case.resize((4, 4), Image.Resampling.BILINEAR) - # fmt: off - data = ("e1 c9" - "c9 b7") - # fmt: on - for channel in case.split(): - self.check_case(channel, self.make_sample(data, (4, 4))) - - @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_reduce_hamming(self, mode: str) -> None: - case = self.make_case(mode, (8, 8), 0xE1) - case = case.resize((4, 4), Image.Resampling.HAMMING) - # fmt: off - data = ("e1 da" - "da d3") - # fmt: on - for channel in case.split(): - self.check_case(channel, self.make_sample(data, (4, 4))) - - @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_reduce_bicubic(self, mode: str) -> None: - case = self.make_case(mode, (12, 12), 0xE1) - case = case.resize((6, 6), Image.Resampling.BICUBIC) - # fmt: off - data = ("e1 e3 d4" - "e3 e5 d6" - "d4 d6 c9") - # fmt: on - for channel in case.split(): - self.check_case(channel, self.make_sample(data, (6, 6))) - - @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_reduce_lanczos(self, mode: str) -> None: - case = self.make_case(mode, (16, 16), 0xE1) - case = case.resize((8, 8), Image.Resampling.LANCZOS) - # fmt: off - data = ("e1 e0 e4 d7" - "e0 df e3 d6" - "e4 e3 e7 da" - "d7 d6 d9 ce") - # fmt: on - for channel in case.split(): - self.check_case(channel, self.make_sample(data, (8, 8))) - - @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_enlarge_box(self, mode: str) -> None: - case = self.make_case(mode, (2, 2), 0xE1) - case = case.resize((4, 4), Image.Resampling.BOX) - # fmt: off - data = ("e1 e1" - "e1 e1") - # fmt: on - for channel in case.split(): - self.check_case(channel, self.make_sample(data, (4, 4))) - - @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_enlarge_bilinear(self, mode: str) -> None: - case = self.make_case(mode, (2, 2), 0xE1) - case = case.resize((4, 4), Image.Resampling.BILINEAR) - # fmt: off - data = ("e1 b0" - "b0 98") - # fmt: on - for channel in case.split(): - self.check_case(channel, self.make_sample(data, (4, 4))) - - @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_enlarge_hamming(self, mode: str) -> None: - case = self.make_case(mode, (2, 2), 0xE1) - case = case.resize((4, 4), Image.Resampling.HAMMING) - # fmt: off - data = ("e1 d2" - "d2 c5") - # fmt: on - for channel in case.split(): - self.check_case(channel, self.make_sample(data, (4, 4))) - - @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_enlarge_bicubic(self, mode: str) -> None: - case = self.make_case(mode, (4, 4), 0xE1) - case = case.resize((8, 8), Image.Resampling.BICUBIC) - # fmt: off - data = ("e1 e5 ee b9" - "e5 e9 f3 bc" - "ee f3 fd c1" - "b9 bc c1 a2") - # fmt: on - for channel in case.split(): - self.check_case(channel, self.make_sample(data, (8, 8))) - - @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_enlarge_lanczos(self, mode: str) -> None: - case = self.make_case(mode, (6, 6), 0xE1) - case = case.resize((12, 12), Image.Resampling.LANCZOS) - data = ( - "e1 e0 db ed f5 b8" - "e0 df da ec f3 b7" - "db db d6 e7 ee b5" - "ed ec e6 fb ff bf" - "f5 f4 ee ff ff c4" - "b8 b7 b4 bf c4 a0" - ) - for channel in case.split(): - self.check_case(channel, self.make_sample(data, (12, 12))) - - def test_box_filter_correct_range(self) -> None: - im = Image.new("RGB", (8, 8), "#1688ff").resize( - (100, 100), Image.Resampling.BOX - ) - ref = Image.new("RGB", (100, 100), "#1688ff") - assert_image_equal(im, ref) - - -class TestCoreResampleConsistency: - def make_case( - self, mode: str, fill: tuple[int, int, int] | float - ) -> tuple[Image.Image, tuple[int, ...]]: - im = Image.new(mode, (512, 9), fill) - return im.resize((9, 512), Image.Resampling.LANCZOS), im.load()[0, 0] - - def run_case(self, case: tuple[Image.Image, int | tuple[int, ...]]) -> None: - channel, color = case - px = channel.load() - for x in range(channel.size[0]): - for y in range(channel.size[1]): - if px[x, y] != color: - message = f"{px[x, y]} != {color} for pixel {(x, y)}" - assert px[x, y] == color, message - - def test_8u(self) -> None: - im, color = self.make_case("RGB", (0, 64, 255)) - r, g, b = im.split() - self.run_case((r, color[0])) - self.run_case((g, color[1])) - self.run_case((b, color[2])) - self.run_case(self.make_case("L", 12)) - - def test_32i(self) -> None: - self.run_case(self.make_case("I", 12)) - self.run_case(self.make_case("I", 0x7FFFFFFF)) - self.run_case(self.make_case("I", -12)) - self.run_case(self.make_case("I", -1 << 31)) - - def test_32f(self) -> None: - self.run_case(self.make_case("F", 1)) - self.run_case(self.make_case("F", 3.40282306074e38)) - self.run_case(self.make_case("F", 1.175494e-38)) - self.run_case(self.make_case("F", 1.192093e-07)) - - -class TestCoreResampleAlphaCorrect: - def make_levels_case(self, mode: str) -> Image.Image: - i = Image.new(mode, (256, 16)) - px = i.load() - for y in range(i.size[1]): - for x in range(i.size[0]): - pix = [x] * len(mode) - pix[-1] = 255 - y * 16 - px[x, y] = tuple(pix) - return i - - def run_levels_case(self, i: Image.Image) -> None: - px = i.load() - for y in range(i.size[1]): - used_colors = {px[x, y][0] for x in range(i.size[0])} - assert 256 == len(used_colors), ( - "All colors should be present in resized image. " - f"Only {len(used_colors)} on line {y}." - ) - - @pytest.mark.xfail(reason="Current implementation isn't precise enough") - def test_levels_rgba(self) -> None: - case = self.make_levels_case("RGBA") - self.run_levels_case(case.resize((512, 32), Image.Resampling.BOX)) - self.run_levels_case(case.resize((512, 32), Image.Resampling.BILINEAR)) - self.run_levels_case(case.resize((512, 32), Image.Resampling.HAMMING)) - self.run_levels_case(case.resize((512, 32), Image.Resampling.BICUBIC)) - self.run_levels_case(case.resize((512, 32), Image.Resampling.LANCZOS)) - - @pytest.mark.xfail(reason="Current implementation isn't precise enough") - def test_levels_la(self) -> None: - case = self.make_levels_case("LA") - self.run_levels_case(case.resize((512, 32), Image.Resampling.BOX)) - self.run_levels_case(case.resize((512, 32), Image.Resampling.BILINEAR)) - self.run_levels_case(case.resize((512, 32), Image.Resampling.HAMMING)) - self.run_levels_case(case.resize((512, 32), Image.Resampling.BICUBIC)) - self.run_levels_case(case.resize((512, 32), Image.Resampling.LANCZOS)) - - def make_dirty_case( - self, mode: str, clean_pixel: tuple[int, ...], dirty_pixel: tuple[int, ...] - ) -> Image.Image: - i = Image.new(mode, (64, 64), dirty_pixel) - px = i.load() - xdiv4 = i.size[0] // 4 - ydiv4 = i.size[1] // 4 - for y in range(ydiv4 * 2): - for x in range(xdiv4 * 2): - px[x + xdiv4, y + ydiv4] = clean_pixel - return i - - def run_dirty_case(self, i: Image.Image, clean_pixel: tuple[int, ...]) -> None: - px = i.load() - for y in range(i.size[1]): - for x in range(i.size[0]): - if px[x, y][-1] != 0 and px[x, y][:-1] != clean_pixel: - message = ( - f"pixel at ({x}, {y}) is different:\n" - f"{px[x, y]}\n{clean_pixel}" - ) - assert px[x, y][:3] == clean_pixel, message - - def test_dirty_pixels_rgba(self) -> None: - case = self.make_dirty_case("RGBA", (255, 255, 0, 128), (0, 0, 255, 0)) - self.run_dirty_case(case.resize((20, 20), Image.Resampling.BOX), (255, 255, 0)) - self.run_dirty_case( - case.resize((20, 20), Image.Resampling.BILINEAR), (255, 255, 0) - ) - self.run_dirty_case( - case.resize((20, 20), Image.Resampling.HAMMING), (255, 255, 0) - ) - self.run_dirty_case( - case.resize((20, 20), Image.Resampling.BICUBIC), (255, 255, 0) - ) - self.run_dirty_case( - case.resize((20, 20), Image.Resampling.LANCZOS), (255, 255, 0) - ) - - def test_dirty_pixels_la(self) -> None: - case = self.make_dirty_case("LA", (255, 128), (0, 0)) - self.run_dirty_case(case.resize((20, 20), Image.Resampling.BOX), (255,)) - self.run_dirty_case(case.resize((20, 20), Image.Resampling.BILINEAR), (255,)) - self.run_dirty_case(case.resize((20, 20), Image.Resampling.HAMMING), (255,)) - self.run_dirty_case(case.resize((20, 20), Image.Resampling.BICUBIC), (255,)) - self.run_dirty_case(case.resize((20, 20), Image.Resampling.LANCZOS), (255,)) - - -class TestCoreResamplePasses: - @contextmanager - def count(self, diff: int) -> Generator[None, None, None]: - count = Image.core.get_stats()["new_count"] - yield - assert Image.core.get_stats()["new_count"] - count == diff - - def test_horizontal(self) -> None: - im = hopper("L") - with self.count(1): - im.resize((im.size[0] - 10, im.size[1]), Image.Resampling.BILINEAR) - - def test_vertical(self) -> None: - im = hopper("L") - with self.count(1): - im.resize((im.size[0], im.size[1] - 10), Image.Resampling.BILINEAR) - - def test_both(self) -> None: - im = hopper("L") - with self.count(2): - im.resize((im.size[0] - 10, im.size[1] - 10), Image.Resampling.BILINEAR) - - def test_box_horizontal(self) -> None: - im = hopper("L") - box = (20, 0, im.size[0] - 20, im.size[1]) - with self.count(1): - # the same size, but different box - with_box = im.resize(im.size, Image.Resampling.BILINEAR, box) - with self.count(2): - cropped = im.crop(box).resize(im.size, Image.Resampling.BILINEAR) - assert_image_similar(with_box, cropped, 0.1) - - def test_box_vertical(self) -> None: - im = hopper("L") - box = (0, 20, im.size[0], im.size[1] - 20) - with self.count(1): - # the same size, but different box - with_box = im.resize(im.size, Image.Resampling.BILINEAR, box) - with self.count(2): - cropped = im.crop(box).resize(im.size, Image.Resampling.BILINEAR) - assert_image_similar(with_box, cropped, 0.1) - - -class TestCoreResampleCoefficients: - def test_reduce(self) -> None: - test_color = 254 - - for size in range(400000, 400010, 2): - i = Image.new("L", (size, 1), 0) - draw = ImageDraw.Draw(i) - draw.rectangle((0, 0, i.size[0] // 2 - 1, 0), test_color) - - px = i.resize((5, i.size[1]), Image.Resampling.BICUBIC).load() - if px[2, 0] != test_color // 2: - assert test_color // 2 == px[2, 0] - - def test_non_zero_coefficients(self) -> None: - # regression test for the wrong coefficients calculation - # due to bug https://github.com/python-pillow/Pillow/issues/2161 - im = Image.new("RGBA", (1280, 1280), (0x20, 0x40, 0x60, 0xFF)) - histogram = im.resize((256, 256), Image.Resampling.BICUBIC).histogram() - - # first channel - assert histogram[0x100 * 0 + 0x20] == 0x10000 - # second channel - assert histogram[0x100 * 1 + 0x40] == 0x10000 - # third channel - assert histogram[0x100 * 2 + 0x60] == 0x10000 - # fourth channel - assert histogram[0x100 * 3 + 0xFF] == 0x10000 - - -class TestCoreResampleBox: - @pytest.mark.parametrize( - "resample", - ( - Image.Resampling.NEAREST, - Image.Resampling.BOX, - Image.Resampling.BILINEAR, - Image.Resampling.HAMMING, - Image.Resampling.BICUBIC, - Image.Resampling.LANCZOS, - ), - ) - def test_wrong_arguments(self, resample: Image.Resampling) -> None: - im = hopper() - im.resize((32, 32), resample, (0, 0, im.width, im.height)) - im.resize((32, 32), resample, (20, 20, im.width, im.height)) - im.resize((32, 32), resample, (20, 20, 20, 100)) - im.resize((32, 32), resample, (20, 20, 100, 20)) - - with pytest.raises(TypeError, match="must be sequence of length 4"): - im.resize((32, 32), resample, (im.width, im.height)) - - with pytest.raises(ValueError, match="can't be negative"): - im.resize((32, 32), resample, (-20, 20, 100, 100)) - with pytest.raises(ValueError, match="can't be negative"): - im.resize((32, 32), resample, (20, -20, 100, 100)) - - with pytest.raises(ValueError, match="can't be empty"): - im.resize((32, 32), resample, (20.1, 20, 20, 100)) - with pytest.raises(ValueError, match="can't be empty"): - im.resize((32, 32), resample, (20, 20.1, 100, 20)) - with pytest.raises(ValueError, match="can't be empty"): - im.resize((32, 32), resample, (20.1, 20.1, 20, 20)) - - with pytest.raises(ValueError, match="can't exceed"): - im.resize((32, 32), resample, (0, 0, im.width + 1, im.height)) - with pytest.raises(ValueError, match="can't exceed"): - im.resize((32, 32), resample, (0, 0, im.width, im.height + 1)) - - def resize_tiled( - self, im: Image.Image, dst_size: tuple[int, int], xtiles: int, ytiles: int - ) -> Image.Image: - def split_range( - size: int, tiles: int - ) -> Generator[tuple[int, int], None, None]: - scale = size / tiles - for i in range(tiles): - yield int(round(scale * i)), int(round(scale * (i + 1))) - - tiled = Image.new(im.mode, dst_size) - scale = (im.size[0] / tiled.size[0], im.size[1] / tiled.size[1]) - - for y0, y1 in split_range(dst_size[1], ytiles): - for x0, x1 in split_range(dst_size[0], xtiles): - box = (x0 * scale[0], y0 * scale[1], x1 * scale[0], y1 * scale[1]) - tile = im.resize((x1 - x0, y1 - y0), Image.Resampling.BICUBIC, box) - tiled.paste(tile, (x0, y0)) - return tiled - - @mark_if_feature_version( - pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" - ) - def test_tiles(self) -> None: - with Image.open("Tests/images/flower.jpg") as im: - assert im.size == (480, 360) - dst_size = (251, 188) - reference = im.resize(dst_size, Image.Resampling.BICUBIC) - - for tiles in [(1, 1), (3, 3), (9, 7), (100, 100)]: - tiled = self.resize_tiled(im, dst_size, *tiles) - assert_image_similar(reference, tiled, 0.01) - - @mark_if_feature_version( - pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" - ) - def test_subsample(self) -> None: - # This test shows advantages of the subpixel resizing - # after supersampling (e.g. during JPEG decoding). - with Image.open("Tests/images/flower.jpg") as im: - assert im.size == (480, 360) - dst_size = (48, 36) - # Reference is cropped image resized to destination - reference = im.crop((0, 0, 473, 353)).resize( - dst_size, Image.Resampling.BICUBIC - ) - # Image.Resampling.BOX emulates supersampling (480 / 8 = 60, 360 / 8 = 45) - supersampled = im.resize((60, 45), Image.Resampling.BOX) - - with_box = supersampled.resize( - dst_size, Image.Resampling.BICUBIC, (0, 0, 59.125, 44.125) - ) - without_box = supersampled.resize(dst_size, Image.Resampling.BICUBIC) - - # error with box should be much smaller than without - assert_image_similar(reference, with_box, 6) - with pytest.raises(AssertionError, match=r"difference 29\."): - assert_image_similar(reference, without_box, 5) - - @pytest.mark.parametrize("mode", ("RGB", "L", "RGBA", "LA", "I", "")) - @pytest.mark.parametrize( - "resample", (Image.Resampling.NEAREST, Image.Resampling.BILINEAR) - ) - def test_formats(self, mode: str, resample: Image.Resampling) -> None: - im = hopper(mode) - box = (20, 20, im.size[0] - 20, im.size[1] - 20) - with_box = im.resize((32, 32), resample, box) - cropped = im.crop(box).resize((32, 32), resample) - assert_image_similar(cropped, with_box, 0.4) - - def test_passthrough(self) -> None: - # When no resize is required - im = hopper() - - for size, box in [ - ((40, 50), (0, 0, 40, 50)), - ((40, 50), (0, 10, 40, 60)), - ((40, 50), (10, 0, 50, 50)), - ((40, 50), (10, 20, 50, 70)), - ]: - res = im.resize(size, Image.Resampling.LANCZOS, box) - assert res.size == size - assert_image_equal(res, im.crop(box), f">>> {size} {box}") - - def test_no_passthrough(self) -> None: - # When resize is required - im = hopper() - - for size, box in [ - ((40, 50), (0.4, 0.4, 40.4, 50.4)), - ((40, 50), (0.4, 10.4, 40.4, 60.4)), - ((40, 50), (10.4, 0.4, 50.4, 50.4)), - ((40, 50), (10.4, 20.4, 50.4, 70.4)), - ]: - res = im.resize(size, Image.Resampling.LANCZOS, box) - assert res.size == size - with pytest.raises(AssertionError, match=r"difference \d"): - # check that the difference at least that much - assert_image_similar(res, im.crop(box), 20, f">>> {size} {box}") - - @pytest.mark.parametrize( - "flt", (Image.Resampling.NEAREST, Image.Resampling.BICUBIC) - ) - def test_skip_horizontal(self, flt: Image.Resampling) -> None: - # Can skip resize for one dimension - im = hopper() - - for size, box in [ - ((40, 50), (0, 0, 40, 90)), - ((40, 50), (0, 20, 40, 90)), - ((40, 50), (10, 0, 50, 90)), - ((40, 50), (10, 20, 50, 90)), - ]: - res = im.resize(size, flt, box) - assert res.size == size - # Borders should be slightly different - assert_image_similar( - res, - im.crop(box).resize(size, flt), - 0.4, - f">>> {size} {box} {flt}", - ) - - @pytest.mark.parametrize( - "flt", (Image.Resampling.NEAREST, Image.Resampling.BICUBIC) - ) - def test_skip_vertical(self, flt: Image.Resampling) -> None: - # Can skip resize for one dimension - im = hopper() - - for size, box in [ - ((40, 50), (0, 0, 90, 50)), - ((40, 50), (20, 0, 90, 50)), - ((40, 50), (0, 10, 90, 60)), - ((40, 50), (20, 10, 90, 60)), - ]: - res = im.resize(size, flt, box) - assert res.size == size - # Borders should be slightly different - assert_image_similar( - res, - im.crop(box).resize(size, flt), - 0.4, - f">>> {size} {box} {flt}", - ) +from __future__ import annotations + +from contextlib import contextmanager +from typing import Generator + +import pytest + +from PIL import Image, ImageDraw + +from .helper import ( + assert_image_equal, + assert_image_similar, + hopper, + mark_if_feature_version, +) + + +class TestImagingResampleVulnerability: + # see https://github.com/python-pillow/Pillow/issues/1710 + def test_overflow(self) -> None: + im = hopper("L") + size_too_large = 0x100000008 // 4 + size_normal = 1000 # unimportant + for xsize, ysize in ( + (size_too_large, size_normal), + (size_normal, size_too_large), + ): + with pytest.raises(MemoryError): + # any resampling filter will do here + im.im.resize((xsize, ysize), Image.Resampling.BILINEAR) + + def test_invalid_size(self) -> None: + im = hopper() + + # Should not crash + im.resize((100, 100)) + + with pytest.raises(ValueError): + im.resize((-100, 100)) + + with pytest.raises(ValueError): + im.resize((100, -100)) + + def test_modify_after_resizing(self) -> None: + im = hopper("RGB") + # get copy with same size + copy = im.resize(im.size) + # some in-place operation + copy.paste("black", (0, 0, im.width // 2, im.height // 2)) + # image should be different + assert im.tobytes() != copy.tobytes() + + +class TestImagingCoreResampleAccuracy: + def make_case(self, mode: str, size: tuple[int, int], color: int) -> Image.Image: + """Makes a sample image with two dark and two bright squares. + For example: + e0 e0 1f 1f + e0 e0 1f 1f + 1f 1f e0 e0 + 1f 1f e0 e0 + """ + case = Image.new("L", size, 255 - color) + rectangle = ImageDraw.Draw(case).rectangle + rectangle((0, 0, size[0] // 2 - 1, size[1] // 2 - 1), color) + rectangle((size[0] // 2, size[1] // 2, size[0], size[1]), color) + + return Image.merge(mode, [case] * len(mode)) + + def make_sample(self, data: str, size: tuple[int, int]) -> Image.Image: + """Restores a sample image from given data string which contains + hex-encoded pixels from the top left fourth of a sample. + """ + data = data.replace(" ", "") + sample = Image.new("L", size) + s_px = sample.load() + w, h = size[0] // 2, size[1] // 2 + for y in range(h): + for x in range(w): + val = int(data[(y * w + x) * 2 : (y * w + x + 1) * 2], 16) + s_px[x, y] = val + s_px[size[0] - x - 1, size[1] - y - 1] = val + s_px[x, size[1] - y - 1] = 255 - val + s_px[size[0] - x - 1, y] = 255 - val + return sample + + def check_case(self, case: Image.Image, sample: Image.Image) -> None: + s_px = sample.load() + c_px = case.load() + for y in range(case.size[1]): + for x in range(case.size[0]): + if c_px[x, y] != s_px[x, y]: + message = ( + f"\nHave: \n{self.serialize_image(case)}\n" + f"\nExpected: \n{self.serialize_image(sample)}" + ) + assert s_px[x, y] == c_px[x, y], message + + def serialize_image(self, image: Image.Image) -> str: + s_px = image.load() + return "\n".join( + " ".join(f"{s_px[x, y]:02x}" for x in range(image.size[0])) + for y in range(image.size[1]) + ) + + @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) + def test_reduce_box(self, mode: str) -> None: + case = self.make_case(mode, (8, 8), 0xE1) + case = case.resize((4, 4), Image.Resampling.BOX) + # fmt: off + data = ("e1 e1" + "e1 e1") + # fmt: on + for channel in case.split(): + self.check_case(channel, self.make_sample(data, (4, 4))) + + @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) + def test_reduce_bilinear(self, mode: str) -> None: + case = self.make_case(mode, (8, 8), 0xE1) + case = case.resize((4, 4), Image.Resampling.BILINEAR) + # fmt: off + data = ("e1 c9" + "c9 b7") + # fmt: on + for channel in case.split(): + self.check_case(channel, self.make_sample(data, (4, 4))) + + @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) + def test_reduce_hamming(self, mode: str) -> None: + case = self.make_case(mode, (8, 8), 0xE1) + case = case.resize((4, 4), Image.Resampling.HAMMING) + # fmt: off + data = ("e1 da" + "da d3") + # fmt: on + for channel in case.split(): + self.check_case(channel, self.make_sample(data, (4, 4))) + + @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) + def test_reduce_bicubic(self, mode: str) -> None: + case = self.make_case(mode, (12, 12), 0xE1) + case = case.resize((6, 6), Image.Resampling.BICUBIC) + # fmt: off + data = ("e1 e3 d4" + "e3 e5 d6" + "d4 d6 c9") + # fmt: on + for channel in case.split(): + self.check_case(channel, self.make_sample(data, (6, 6))) + + @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) + def test_reduce_lanczos(self, mode: str) -> None: + case = self.make_case(mode, (16, 16), 0xE1) + case = case.resize((8, 8), Image.Resampling.LANCZOS) + # fmt: off + data = ("e1 e0 e4 d7" + "e0 df e3 d6" + "e4 e3 e7 da" + "d7 d6 d9 ce") + # fmt: on + for channel in case.split(): + self.check_case(channel, self.make_sample(data, (8, 8))) + + @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) + def test_enlarge_box(self, mode: str) -> None: + case = self.make_case(mode, (2, 2), 0xE1) + case = case.resize((4, 4), Image.Resampling.BOX) + # fmt: off + data = ("e1 e1" + "e1 e1") + # fmt: on + for channel in case.split(): + self.check_case(channel, self.make_sample(data, (4, 4))) + + @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) + def test_enlarge_bilinear(self, mode: str) -> None: + case = self.make_case(mode, (2, 2), 0xE1) + case = case.resize((4, 4), Image.Resampling.BILINEAR) + # fmt: off + data = ("e1 b0" + "b0 98") + # fmt: on + for channel in case.split(): + self.check_case(channel, self.make_sample(data, (4, 4))) + + @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) + def test_enlarge_hamming(self, mode: str) -> None: + case = self.make_case(mode, (2, 2), 0xE1) + case = case.resize((4, 4), Image.Resampling.HAMMING) + # fmt: off + data = ("e1 d2" + "d2 c5") + # fmt: on + for channel in case.split(): + self.check_case(channel, self.make_sample(data, (4, 4))) + + @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) + def test_enlarge_bicubic(self, mode: str) -> None: + case = self.make_case(mode, (4, 4), 0xE1) + case = case.resize((8, 8), Image.Resampling.BICUBIC) + # fmt: off + data = ("e1 e5 ee b9" + "e5 e9 f3 bc" + "ee f3 fd c1" + "b9 bc c1 a2") + # fmt: on + for channel in case.split(): + self.check_case(channel, self.make_sample(data, (8, 8))) + + @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) + def test_enlarge_lanczos(self, mode: str) -> None: + case = self.make_case(mode, (6, 6), 0xE1) + case = case.resize((12, 12), Image.Resampling.LANCZOS) + data = ( + "e1 e0 db ed f5 b8" + "e0 df da ec f3 b7" + "db db d6 e7 ee b5" + "ed ec e6 fb ff bf" + "f5 f4 ee ff ff c4" + "b8 b7 b4 bf c4 a0" + ) + for channel in case.split(): + self.check_case(channel, self.make_sample(data, (12, 12))) + + def test_box_filter_correct_range(self) -> None: + im = Image.new("RGB", (8, 8), "#1688ff").resize( + (100, 100), Image.Resampling.BOX + ) + ref = Image.new("RGB", (100, 100), "#1688ff") + assert_image_equal(im, ref) + + +class TestCoreResampleConsistency: + def make_case( + self, mode: str, fill: tuple[int, int, int] | float + ) -> tuple[Image.Image, tuple[int, ...]]: + im = Image.new(mode, (512, 9), fill) + return im.resize((9, 512), Image.Resampling.LANCZOS), im.load()[0, 0] + + def run_case(self, case: tuple[Image.Image, int | tuple[int, ...]]) -> None: + channel, color = case + px = channel.load() + for x in range(channel.size[0]): + for y in range(channel.size[1]): + if px[x, y] != color: + message = f"{px[x, y]} != {color} for pixel {(x, y)}" + assert px[x, y] == color, message + + def test_8u(self) -> None: + im, color = self.make_case("RGB", (0, 64, 255)) + r, g, b = im.split() + self.run_case((r, color[0])) + self.run_case((g, color[1])) + self.run_case((b, color[2])) + self.run_case(self.make_case("L", 12)) + + def test_32i(self) -> None: + self.run_case(self.make_case("I", 12)) + self.run_case(self.make_case("I", 0x7FFFFFFF)) + self.run_case(self.make_case("I", -12)) + self.run_case(self.make_case("I", -1 << 31)) + + def test_32f(self) -> None: + self.run_case(self.make_case("F", 1)) + self.run_case(self.make_case("F", 3.40282306074e38)) + self.run_case(self.make_case("F", 1.175494e-38)) + self.run_case(self.make_case("F", 1.192093e-07)) + + +class TestCoreResampleAlphaCorrect: + def make_levels_case(self, mode: str) -> Image.Image: + i = Image.new(mode, (256, 16)) + px = i.load() + for y in range(i.size[1]): + for x in range(i.size[0]): + pix = [x] * len(mode) + pix[-1] = 255 - y * 16 + px[x, y] = tuple(pix) + return i + + def run_levels_case(self, i: Image.Image) -> None: + px = i.load() + for y in range(i.size[1]): + used_colors = {px[x, y][0] for x in range(i.size[0])} + assert 256 == len(used_colors), ( + "All colors should be present in resized image. " + f"Only {len(used_colors)} on line {y}." + ) + + @pytest.mark.xfail(reason="Current implementation isn't precise enough") + def test_levels_rgba(self) -> None: + case = self.make_levels_case("RGBA") + self.run_levels_case(case.resize((512, 32), Image.Resampling.BOX)) + self.run_levels_case(case.resize((512, 32), Image.Resampling.BILINEAR)) + self.run_levels_case(case.resize((512, 32), Image.Resampling.HAMMING)) + self.run_levels_case(case.resize((512, 32), Image.Resampling.BICUBIC)) + self.run_levels_case(case.resize((512, 32), Image.Resampling.LANCZOS)) + + @pytest.mark.xfail(reason="Current implementation isn't precise enough") + def test_levels_la(self) -> None: + case = self.make_levels_case("LA") + self.run_levels_case(case.resize((512, 32), Image.Resampling.BOX)) + self.run_levels_case(case.resize((512, 32), Image.Resampling.BILINEAR)) + self.run_levels_case(case.resize((512, 32), Image.Resampling.HAMMING)) + self.run_levels_case(case.resize((512, 32), Image.Resampling.BICUBIC)) + self.run_levels_case(case.resize((512, 32), Image.Resampling.LANCZOS)) + + def make_dirty_case( + self, mode: str, clean_pixel: tuple[int, ...], dirty_pixel: tuple[int, ...] + ) -> Image.Image: + i = Image.new(mode, (64, 64), dirty_pixel) + px = i.load() + xdiv4 = i.size[0] // 4 + ydiv4 = i.size[1] // 4 + for y in range(ydiv4 * 2): + for x in range(xdiv4 * 2): + px[x + xdiv4, y + ydiv4] = clean_pixel + return i + + def run_dirty_case(self, i: Image.Image, clean_pixel: tuple[int, ...]) -> None: + px = i.load() + for y in range(i.size[1]): + for x in range(i.size[0]): + if px[x, y][-1] != 0 and px[x, y][:-1] != clean_pixel: + message = ( + f"pixel at ({x}, {y}) is different:\n" + f"{px[x, y]}\n{clean_pixel}" + ) + assert px[x, y][:3] == clean_pixel, message + + def test_dirty_pixels_rgba(self) -> None: + case = self.make_dirty_case("RGBA", (255, 255, 0, 128), (0, 0, 255, 0)) + self.run_dirty_case(case.resize((20, 20), Image.Resampling.BOX), (255, 255, 0)) + self.run_dirty_case( + case.resize((20, 20), Image.Resampling.BILINEAR), (255, 255, 0) + ) + self.run_dirty_case( + case.resize((20, 20), Image.Resampling.HAMMING), (255, 255, 0) + ) + self.run_dirty_case( + case.resize((20, 20), Image.Resampling.BICUBIC), (255, 255, 0) + ) + self.run_dirty_case( + case.resize((20, 20), Image.Resampling.LANCZOS), (255, 255, 0) + ) + + def test_dirty_pixels_la(self) -> None: + case = self.make_dirty_case("LA", (255, 128), (0, 0)) + self.run_dirty_case(case.resize((20, 20), Image.Resampling.BOX), (255,)) + self.run_dirty_case(case.resize((20, 20), Image.Resampling.BILINEAR), (255,)) + self.run_dirty_case(case.resize((20, 20), Image.Resampling.HAMMING), (255,)) + self.run_dirty_case(case.resize((20, 20), Image.Resampling.BICUBIC), (255,)) + self.run_dirty_case(case.resize((20, 20), Image.Resampling.LANCZOS), (255,)) + + +class TestCoreResamplePasses: + @contextmanager + def count(self, diff: int) -> Generator[None, None, None]: + count = Image.core.get_stats()["new_count"] + yield + assert Image.core.get_stats()["new_count"] - count == diff + + def test_horizontal(self) -> None: + im = hopper("L") + with self.count(1): + im.resize((im.size[0] - 10, im.size[1]), Image.Resampling.BILINEAR) + + def test_vertical(self) -> None: + im = hopper("L") + with self.count(1): + im.resize((im.size[0], im.size[1] - 10), Image.Resampling.BILINEAR) + + def test_both(self) -> None: + im = hopper("L") + with self.count(2): + im.resize((im.size[0] - 10, im.size[1] - 10), Image.Resampling.BILINEAR) + + def test_box_horizontal(self) -> None: + im = hopper("L") + box = (20, 0, im.size[0] - 20, im.size[1]) + with self.count(1): + # the same size, but different box + with_box = im.resize(im.size, Image.Resampling.BILINEAR, box) + with self.count(2): + cropped = im.crop(box).resize(im.size, Image.Resampling.BILINEAR) + assert_image_similar(with_box, cropped, 0.1) + + def test_box_vertical(self) -> None: + im = hopper("L") + box = (0, 20, im.size[0], im.size[1] - 20) + with self.count(1): + # the same size, but different box + with_box = im.resize(im.size, Image.Resampling.BILINEAR, box) + with self.count(2): + cropped = im.crop(box).resize(im.size, Image.Resampling.BILINEAR) + assert_image_similar(with_box, cropped, 0.1) + + +class TestCoreResampleCoefficients: + def test_reduce(self) -> None: + test_color = 254 + + for size in range(400000, 400010, 2): + i = Image.new("L", (size, 1), 0) + draw = ImageDraw.Draw(i) + draw.rectangle((0, 0, i.size[0] // 2 - 1, 0), test_color) + + px = i.resize((5, i.size[1]), Image.Resampling.BICUBIC).load() + if px[2, 0] != test_color // 2: + assert test_color // 2 == px[2, 0] + + def test_non_zero_coefficients(self) -> None: + # regression test for the wrong coefficients calculation + # due to bug https://github.com/python-pillow/Pillow/issues/2161 + im = Image.new("RGBA", (1280, 1280), (0x20, 0x40, 0x60, 0xFF)) + histogram = im.resize((256, 256), Image.Resampling.BICUBIC).histogram() + + # first channel + assert histogram[0x100 * 0 + 0x20] == 0x10000 + # second channel + assert histogram[0x100 * 1 + 0x40] == 0x10000 + # third channel + assert histogram[0x100 * 2 + 0x60] == 0x10000 + # fourth channel + assert histogram[0x100 * 3 + 0xFF] == 0x10000 + + +class TestCoreResampleBox: + @pytest.mark.parametrize( + "resample", + ( + Image.Resampling.NEAREST, + Image.Resampling.BOX, + Image.Resampling.BILINEAR, + Image.Resampling.HAMMING, + Image.Resampling.BICUBIC, + Image.Resampling.LANCZOS, + ), + ) + def test_wrong_arguments(self, resample: Image.Resampling) -> None: + im = hopper() + im.resize((32, 32), resample, (0, 0, im.width, im.height)) + im.resize((32, 32), resample, (20, 20, im.width, im.height)) + im.resize((32, 32), resample, (20, 20, 20, 100)) + im.resize((32, 32), resample, (20, 20, 100, 20)) + + with pytest.raises(TypeError, match="must be sequence of length 4"): + im.resize((32, 32), resample, (im.width, im.height)) + + with pytest.raises(ValueError, match="can't be negative"): + im.resize((32, 32), resample, (-20, 20, 100, 100)) + with pytest.raises(ValueError, match="can't be negative"): + im.resize((32, 32), resample, (20, -20, 100, 100)) + + with pytest.raises(ValueError, match="can't be empty"): + im.resize((32, 32), resample, (20.1, 20, 20, 100)) + with pytest.raises(ValueError, match="can't be empty"): + im.resize((32, 32), resample, (20, 20.1, 100, 20)) + with pytest.raises(ValueError, match="can't be empty"): + im.resize((32, 32), resample, (20.1, 20.1, 20, 20)) + + with pytest.raises(ValueError, match="can't exceed"): + im.resize((32, 32), resample, (0, 0, im.width + 1, im.height)) + with pytest.raises(ValueError, match="can't exceed"): + im.resize((32, 32), resample, (0, 0, im.width, im.height + 1)) + + def resize_tiled( + self, im: Image.Image, dst_size: tuple[int, int], xtiles: int, ytiles: int + ) -> Image.Image: + def split_range( + size: int, tiles: int + ) -> Generator[tuple[int, int], None, None]: + scale = size / tiles + for i in range(tiles): + yield int(round(scale * i)), int(round(scale * (i + 1))) + + tiled = Image.new(im.mode, dst_size) + scale = (im.size[0] / tiled.size[0], im.size[1] / tiled.size[1]) + + for y0, y1 in split_range(dst_size[1], ytiles): + for x0, x1 in split_range(dst_size[0], xtiles): + box = (x0 * scale[0], y0 * scale[1], x1 * scale[0], y1 * scale[1]) + tile = im.resize((x1 - x0, y1 - y0), Image.Resampling.BICUBIC, box) + tiled.paste(tile, (x0, y0)) + return tiled + + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) + def test_tiles(self) -> None: + with Image.open("Tests/images/flower.jpg") as im: + assert im.size == (480, 360) + dst_size = (251, 188) + reference = im.resize(dst_size, Image.Resampling.BICUBIC) + + for tiles in [(1, 1), (3, 3), (9, 7), (100, 100)]: + tiled = self.resize_tiled(im, dst_size, *tiles) + assert_image_similar(reference, tiled, 0.01) + + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) + def test_subsample(self) -> None: + # This test shows advantages of the subpixel resizing + # after supersampling (e.g. during JPEG decoding). + with Image.open("Tests/images/flower.jpg") as im: + assert im.size == (480, 360) + dst_size = (48, 36) + # Reference is cropped image resized to destination + reference = im.crop((0, 0, 473, 353)).resize( + dst_size, Image.Resampling.BICUBIC + ) + # Image.Resampling.BOX emulates supersampling (480 / 8 = 60, 360 / 8 = 45) + supersampled = im.resize((60, 45), Image.Resampling.BOX) + + with_box = supersampled.resize( + dst_size, Image.Resampling.BICUBIC, (0, 0, 59.125, 44.125) + ) + without_box = supersampled.resize(dst_size, Image.Resampling.BICUBIC) + + # error with box should be much smaller than without + assert_image_similar(reference, with_box, 6) + with pytest.raises(AssertionError, match=r"difference 29\."): + assert_image_similar(reference, without_box, 5) + + @pytest.mark.parametrize("mode", ("RGB", "L", "RGBA", "LA", "I", "")) + @pytest.mark.parametrize( + "resample", (Image.Resampling.NEAREST, Image.Resampling.BILINEAR) + ) + def test_formats(self, mode: str, resample: Image.Resampling) -> None: + im = hopper(mode) + box = (20, 20, im.size[0] - 20, im.size[1] - 20) + with_box = im.resize((32, 32), resample, box) + cropped = im.crop(box).resize((32, 32), resample) + assert_image_similar(cropped, with_box, 0.4) + + def test_passthrough(self) -> None: + # When no resize is required + im = hopper() + + for size, box in [ + ((40, 50), (0, 0, 40, 50)), + ((40, 50), (0, 10, 40, 60)), + ((40, 50), (10, 0, 50, 50)), + ((40, 50), (10, 20, 50, 70)), + ]: + res = im.resize(size, Image.Resampling.LANCZOS, box) + assert res.size == size + assert_image_equal(res, im.crop(box), f">>> {size} {box}") + + def test_no_passthrough(self) -> None: + # When resize is required + im = hopper() + + for size, box in [ + ((40, 50), (0.4, 0.4, 40.4, 50.4)), + ((40, 50), (0.4, 10.4, 40.4, 60.4)), + ((40, 50), (10.4, 0.4, 50.4, 50.4)), + ((40, 50), (10.4, 20.4, 50.4, 70.4)), + ]: + res = im.resize(size, Image.Resampling.LANCZOS, box) + assert res.size == size + with pytest.raises(AssertionError, match=r"difference \d"): + # check that the difference at least that much + assert_image_similar(res, im.crop(box), 20, f">>> {size} {box}") + + @pytest.mark.parametrize( + "flt", (Image.Resampling.NEAREST, Image.Resampling.BICUBIC) + ) + def test_skip_horizontal(self, flt: Image.Resampling) -> None: + # Can skip resize for one dimension + im = hopper() + + for size, box in [ + ((40, 50), (0, 0, 40, 90)), + ((40, 50), (0, 20, 40, 90)), + ((40, 50), (10, 0, 50, 90)), + ((40, 50), (10, 20, 50, 90)), + ]: + res = im.resize(size, flt, box) + assert res.size == size + # Borders should be slightly different + assert_image_similar( + res, + im.crop(box).resize(size, flt), + 0.4, + f">>> {size} {box} {flt}", + ) + + @pytest.mark.parametrize( + "flt", (Image.Resampling.NEAREST, Image.Resampling.BICUBIC) + ) + def test_skip_vertical(self, flt: Image.Resampling) -> None: + # Can skip resize for one dimension + im = hopper() + + for size, box in [ + ((40, 50), (0, 0, 90, 50)), + ((40, 50), (20, 0, 90, 50)), + ((40, 50), (0, 10, 90, 60)), + ((40, 50), (20, 10, 90, 60)), + ]: + res = im.resize(size, flt, box) + assert res.size == size + # Borders should be slightly different + assert_image_similar( + res, + im.crop(box).resize(size, flt), + 0.4, + f">>> {size} {box} {flt}", + ) diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index 64098f80f..6257b091a 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -1,320 +1,320 @@ -""" -Tests for resize functionality. -""" - -from __future__ import annotations - -from itertools import permutations -from pathlib import Path -from typing import Generator - -import pytest - -from PIL import Image - -from .helper import ( - assert_image_equal, - assert_image_equal_tofile, - assert_image_similar, - hopper, - skip_unless_feature, -) - - -class TestImagingCoreResize: - def resize( - self, im: Image.Image, size: tuple[int, int], f: Image.Resampling - ) -> Image.Image: - # Image class independent version of resize. - im.load() - return im._new(im.im.resize(size, f)) - - @pytest.mark.parametrize( - "mode", ("1", "P", "L", "I", "F", "RGB", "RGBA", "CMYK", "YCbCr", "I;16") - ) - def test_nearest_mode(self, mode: str) -> None: - im = hopper(mode) - r = self.resize(im, (15, 12), Image.Resampling.NEAREST) - assert r.mode == mode - assert r.size == (15, 12) - assert r.im.bands == im.im.bands - - def test_convolution_modes(self) -> None: - with pytest.raises(ValueError): - self.resize(hopper("1"), (15, 12), Image.Resampling.BILINEAR) - with pytest.raises(ValueError): - self.resize(hopper("P"), (15, 12), Image.Resampling.BILINEAR) - with pytest.raises(ValueError): - self.resize(hopper("I;16"), (15, 12), Image.Resampling.BILINEAR) - for mode in ["L", "I", "F", "RGB", "RGBA", "CMYK", "YCbCr"]: - im = hopper(mode) - r = self.resize(im, (15, 12), Image.Resampling.BILINEAR) - assert r.mode == mode - assert r.size == (15, 12) - assert r.im.bands == im.im.bands - - @pytest.mark.parametrize( - "resample", - ( - Image.Resampling.NEAREST, - Image.Resampling.BOX, - Image.Resampling.BILINEAR, - Image.Resampling.HAMMING, - Image.Resampling.BICUBIC, - Image.Resampling.LANCZOS, - ), - ) - def test_reduce_filters(self, resample: Image.Resampling) -> None: - r = self.resize(hopper("RGB"), (15, 12), resample) - assert r.mode == "RGB" - assert r.size == (15, 12) - - @pytest.mark.parametrize( - "resample", - ( - Image.Resampling.NEAREST, - Image.Resampling.BOX, - Image.Resampling.BILINEAR, - Image.Resampling.HAMMING, - Image.Resampling.BICUBIC, - Image.Resampling.LANCZOS, - ), - ) - def test_enlarge_filters(self, resample: Image.Resampling) -> None: - r = self.resize(hopper("RGB"), (212, 195), resample) - assert r.mode == "RGB" - assert r.size == (212, 195) - - @pytest.mark.parametrize( - "resample", - ( - Image.Resampling.NEAREST, - Image.Resampling.BOX, - Image.Resampling.BILINEAR, - Image.Resampling.HAMMING, - Image.Resampling.BICUBIC, - Image.Resampling.LANCZOS, - ), - ) - @pytest.mark.parametrize( - "mode, channels_set", - ( - ("RGB", ("blank", "filled", "dirty")), - ("RGBA", ("blank", "blank", "filled", "dirty")), - ("LA", ("filled", "dirty")), - ), - ) - def test_endianness( - self, resample: Image.Resampling, mode: str, channels_set: tuple[str, ...] - ) -> None: - # Make an image with one colored pixel, in one channel. - # When resized, that channel should be the same as a GS image. - # Other channels should be unaffected. - # The R and A channels should not swap, which is indicative of - # an endianness issues. - - samples = { - "blank": Image.new("L", (2, 2), 0), - "filled": Image.new("L", (2, 2), 255), - "dirty": Image.new("L", (2, 2), 0), - } - samples["dirty"].putpixel((1, 1), 128) - - # samples resized with current filter - references = { - name: self.resize(ch, (4, 4), resample) for name, ch in samples.items() - } - - for channels in set(permutations(channels_set)): - # compile image from different channels permutations - im = Image.merge(mode, [samples[ch] for ch in channels]) - resized = self.resize(im, (4, 4), resample) - - for i, ch in enumerate(resized.split()): - # check what resized channel in image is the same - # as separately resized channel - assert_image_equal(ch, references[channels[i]]) - - @pytest.mark.parametrize( - "resample", - ( - Image.Resampling.NEAREST, - Image.Resampling.BOX, - Image.Resampling.BILINEAR, - Image.Resampling.HAMMING, - Image.Resampling.BICUBIC, - Image.Resampling.LANCZOS, - ), - ) - def test_enlarge_zero(self, resample: Image.Resampling) -> None: - r = self.resize(Image.new("RGB", (0, 0), "white"), (212, 195), resample) - assert r.mode == "RGB" - assert r.size == (212, 195) - assert r.getdata()[0] == (0, 0, 0) - - def test_unknown_filter(self) -> None: - with pytest.raises(ValueError): - self.resize(hopper(), (10, 10), 9) # type: ignore[arg-type] - - def test_cross_platform(self, tmp_path: Path) -> None: - # This test is intended for only check for consistent behaviour across - # platforms. So if a future Pillow change requires that the test file - # be updated, that is okay. - im = hopper().resize((64, 64)) - temp_file = str(tmp_path / "temp.gif") - im.save(temp_file) - - with Image.open(temp_file) as reloaded: - assert_image_equal_tofile(reloaded, "Tests/images/hopper_resized.gif") - - -@pytest.fixture -def gradients_image() -> Generator[Image.Image, None, None]: - with Image.open("Tests/images/radial_gradients.png") as im: - im.load() - try: - yield im - finally: - im.close() - - -class TestReducingGapResize: - def test_reducing_gap_values(self, gradients_image: Image.Image) -> None: - ref = gradients_image.resize( - (52, 34), Image.Resampling.BICUBIC, reducing_gap=None - ) - im = gradients_image.resize((52, 34), Image.Resampling.BICUBIC) - assert_image_equal(ref, im) - - with pytest.raises(ValueError): - gradients_image.resize((52, 34), Image.Resampling.BICUBIC, reducing_gap=0) - - with pytest.raises(ValueError): - gradients_image.resize( - (52, 34), Image.Resampling.BICUBIC, reducing_gap=0.99 - ) - - @pytest.mark.parametrize( - "box, epsilon", - ((None, 4), ((1.1, 2.2, 510.8, 510.9), 4), ((3, 10, 410, 256), 10)), - ) - def test_reducing_gap_1( - self, - gradients_image: Image.Image, - box: tuple[float, float, float, float], - epsilon: float, - ) -> None: - ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) - im = gradients_image.resize( - (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=1.0 - ) - - with pytest.raises(pytest.fail.Exception): - assert_image_equal(ref, im) - - assert_image_similar(ref, im, epsilon) - - @pytest.mark.parametrize( - "box, epsilon", - ((None, 1.5), ((1.1, 2.2, 510.8, 510.9), 1.5), ((3, 10, 410, 256), 1)), - ) - def test_reducing_gap_2( - self, - gradients_image: Image.Image, - box: tuple[float, float, float, float], - epsilon: float, - ) -> None: - ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) - im = gradients_image.resize( - (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=2.0 - ) - - with pytest.raises(pytest.fail.Exception): - assert_image_equal(ref, im) - - assert_image_similar(ref, im, epsilon) - - @pytest.mark.parametrize( - "box, epsilon", - ((None, 1), ((1.1, 2.2, 510.8, 510.9), 1), ((3, 10, 410, 256), 0.5)), - ) - def test_reducing_gap_3( - self, - gradients_image: Image.Image, - box: tuple[float, float, float, float], - epsilon: float, - ) -> None: - ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) - im = gradients_image.resize( - (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=3.0 - ) - - with pytest.raises(pytest.fail.Exception): - assert_image_equal(ref, im) - - assert_image_similar(ref, im, epsilon) - - @pytest.mark.parametrize("box", (None, (1.1, 2.2, 510.8, 510.9), (3, 10, 410, 256))) - def test_reducing_gap_8( - self, gradients_image: Image.Image, box: tuple[float, float, float, float] - ) -> None: - ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) - im = gradients_image.resize( - (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=8.0 - ) - - assert_image_equal(ref, im) - - @pytest.mark.parametrize( - "box, epsilon", - (((0, 0, 512, 512), 5.5), ((0.9, 1.7, 128, 128), 9.5)), - ) - def test_box_filter( - self, - gradients_image: Image.Image, - box: tuple[float, float, float, float], - epsilon: float, - ) -> None: - ref = gradients_image.resize((52, 34), Image.Resampling.BOX, box=box) - im = gradients_image.resize( - (52, 34), Image.Resampling.BOX, box=box, reducing_gap=1.0 - ) - - assert_image_similar(ref, im, epsilon) - - -class TestImageResize: - def test_resize(self) -> None: - def resize(mode: str, size: tuple[int, int]) -> None: - out = hopper(mode).resize(size) - assert out.mode == mode - assert out.size == size - - for mode in "1", "P", "L", "RGB", "I", "F": - resize(mode, (112, 103)) - resize(mode, (188, 214)) - - # Test unknown resampling filter - with hopper() as im: - with pytest.raises(ValueError): - im.resize((10, 10), "unknown") - - @skip_unless_feature("libtiff") - def test_load_first(self) -> None: - # load() may change the size of the image - # Test that resize() is calling it before getting the size - with Image.open("Tests/images/g4_orientation_5.tif") as im: - im = im.resize((64, 64)) - assert im.size == (64, 64) - - @pytest.mark.parametrize("mode", ("L", "RGB", "I", "F")) - def test_default_filter_bicubic(self, mode: str) -> None: - im = hopper(mode) - assert im.resize((20, 20), Image.Resampling.BICUBIC) == im.resize((20, 20)) - - @pytest.mark.parametrize( - "mode", ("1", "P", "I;16", "I;16L", "I;16B", "BGR;15", "BGR;16") - ) - def test_default_filter_nearest(self, mode: str) -> None: - im = hopper(mode) - assert im.resize((20, 20), Image.Resampling.NEAREST) == im.resize((20, 20)) +""" +Tests for resize functionality. +""" + +from __future__ import annotations + +from itertools import permutations +from pathlib import Path +from typing import Generator + +import pytest + +from PIL import Image + +from .helper import ( + assert_image_equal, + assert_image_equal_tofile, + assert_image_similar, + hopper, + skip_unless_feature, +) + + +class TestImagingCoreResize: + def resize( + self, im: Image.Image, size: tuple[int, int], f: Image.Resampling + ) -> Image.Image: + # Image class independent version of resize. + im.load() + return im._new(im.im.resize(size, f)) + + @pytest.mark.parametrize( + "mode", ("1", "P", "L", "I", "F", "RGB", "RGBA", "CMYK", "YCbCr", "I;16") + ) + def test_nearest_mode(self, mode: str) -> None: + im = hopper(mode) + r = self.resize(im, (15, 12), Image.Resampling.NEAREST) + assert r.mode == mode + assert r.size == (15, 12) + assert r.im.bands == im.im.bands + + def test_convolution_modes(self) -> None: + with pytest.raises(ValueError): + self.resize(hopper("1"), (15, 12), Image.Resampling.BILINEAR) + with pytest.raises(ValueError): + self.resize(hopper("P"), (15, 12), Image.Resampling.BILINEAR) + with pytest.raises(ValueError): + self.resize(hopper("I;16"), (15, 12), Image.Resampling.BILINEAR) + for mode in ["L", "I", "F", "RGB", "RGBA", "CMYK", "YCbCr"]: + im = hopper(mode) + r = self.resize(im, (15, 12), Image.Resampling.BILINEAR) + assert r.mode == mode + assert r.size == (15, 12) + assert r.im.bands == im.im.bands + + @pytest.mark.parametrize( + "resample", + ( + Image.Resampling.NEAREST, + Image.Resampling.BOX, + Image.Resampling.BILINEAR, + Image.Resampling.HAMMING, + Image.Resampling.BICUBIC, + Image.Resampling.LANCZOS, + ), + ) + def test_reduce_filters(self, resample: Image.Resampling) -> None: + r = self.resize(hopper("RGB"), (15, 12), resample) + assert r.mode == "RGB" + assert r.size == (15, 12) + + @pytest.mark.parametrize( + "resample", + ( + Image.Resampling.NEAREST, + Image.Resampling.BOX, + Image.Resampling.BILINEAR, + Image.Resampling.HAMMING, + Image.Resampling.BICUBIC, + Image.Resampling.LANCZOS, + ), + ) + def test_enlarge_filters(self, resample: Image.Resampling) -> None: + r = self.resize(hopper("RGB"), (212, 195), resample) + assert r.mode == "RGB" + assert r.size == (212, 195) + + @pytest.mark.parametrize( + "resample", + ( + Image.Resampling.NEAREST, + Image.Resampling.BOX, + Image.Resampling.BILINEAR, + Image.Resampling.HAMMING, + Image.Resampling.BICUBIC, + Image.Resampling.LANCZOS, + ), + ) + @pytest.mark.parametrize( + "mode, channels_set", + ( + ("RGB", ("blank", "filled", "dirty")), + ("RGBA", ("blank", "blank", "filled", "dirty")), + ("LA", ("filled", "dirty")), + ), + ) + def test_endianness( + self, resample: Image.Resampling, mode: str, channels_set: tuple[str, ...] + ) -> None: + # Make an image with one colored pixel, in one channel. + # When resized, that channel should be the same as a GS image. + # Other channels should be unaffected. + # The R and A channels should not swap, which is indicative of + # an endianness issues. + + samples = { + "blank": Image.new("L", (2, 2), 0), + "filled": Image.new("L", (2, 2), 255), + "dirty": Image.new("L", (2, 2), 0), + } + samples["dirty"].putpixel((1, 1), 128) + + # samples resized with current filter + references = { + name: self.resize(ch, (4, 4), resample) for name, ch in samples.items() + } + + for channels in set(permutations(channels_set)): + # compile image from different channels permutations + im = Image.merge(mode, [samples[ch] for ch in channels]) + resized = self.resize(im, (4, 4), resample) + + for i, ch in enumerate(resized.split()): + # check what resized channel in image is the same + # as separately resized channel + assert_image_equal(ch, references[channels[i]]) + + @pytest.mark.parametrize( + "resample", + ( + Image.Resampling.NEAREST, + Image.Resampling.BOX, + Image.Resampling.BILINEAR, + Image.Resampling.HAMMING, + Image.Resampling.BICUBIC, + Image.Resampling.LANCZOS, + ), + ) + def test_enlarge_zero(self, resample: Image.Resampling) -> None: + r = self.resize(Image.new("RGB", (0, 0), "white"), (212, 195), resample) + assert r.mode == "RGB" + assert r.size == (212, 195) + assert r.getdata()[0] == (0, 0, 0) + + def test_unknown_filter(self) -> None: + with pytest.raises(ValueError): + self.resize(hopper(), (10, 10), 9) # type: ignore[arg-type] + + def test_cross_platform(self, tmp_path: Path) -> None: + # This test is intended for only check for consistent behaviour across + # platforms. So if a future Pillow change requires that the test file + # be updated, that is okay. + im = hopper().resize((64, 64)) + temp_file = str(tmp_path / "temp.gif") + im.save(temp_file) + + with Image.open(temp_file) as reloaded: + assert_image_equal_tofile(reloaded, "Tests/images/hopper_resized.gif") + + +@pytest.fixture +def gradients_image() -> Generator[Image.Image, None, None]: + with Image.open("Tests/images/radial_gradients.png") as im: + im.load() + try: + yield im + finally: + im.close() + + +class TestReducingGapResize: + def test_reducing_gap_values(self, gradients_image: Image.Image) -> None: + ref = gradients_image.resize( + (52, 34), Image.Resampling.BICUBIC, reducing_gap=None + ) + im = gradients_image.resize((52, 34), Image.Resampling.BICUBIC) + assert_image_equal(ref, im) + + with pytest.raises(ValueError): + gradients_image.resize((52, 34), Image.Resampling.BICUBIC, reducing_gap=0) + + with pytest.raises(ValueError): + gradients_image.resize( + (52, 34), Image.Resampling.BICUBIC, reducing_gap=0.99 + ) + + @pytest.mark.parametrize( + "box, epsilon", + ((None, 4), ((1.1, 2.2, 510.8, 510.9), 4), ((3, 10, 410, 256), 10)), + ) + def test_reducing_gap_1( + self, + gradients_image: Image.Image, + box: tuple[float, float, float, float], + epsilon: float, + ) -> None: + ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) + im = gradients_image.resize( + (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=1.0 + ) + + with pytest.raises(pytest.fail.Exception): + assert_image_equal(ref, im) + + assert_image_similar(ref, im, epsilon) + + @pytest.mark.parametrize( + "box, epsilon", + ((None, 1.5), ((1.1, 2.2, 510.8, 510.9), 1.5), ((3, 10, 410, 256), 1)), + ) + def test_reducing_gap_2( + self, + gradients_image: Image.Image, + box: tuple[float, float, float, float], + epsilon: float, + ) -> None: + ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) + im = gradients_image.resize( + (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=2.0 + ) + + with pytest.raises(pytest.fail.Exception): + assert_image_equal(ref, im) + + assert_image_similar(ref, im, epsilon) + + @pytest.mark.parametrize( + "box, epsilon", + ((None, 1), ((1.1, 2.2, 510.8, 510.9), 1), ((3, 10, 410, 256), 0.5)), + ) + def test_reducing_gap_3( + self, + gradients_image: Image.Image, + box: tuple[float, float, float, float], + epsilon: float, + ) -> None: + ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) + im = gradients_image.resize( + (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=3.0 + ) + + with pytest.raises(pytest.fail.Exception): + assert_image_equal(ref, im) + + assert_image_similar(ref, im, epsilon) + + @pytest.mark.parametrize("box", (None, (1.1, 2.2, 510.8, 510.9), (3, 10, 410, 256))) + def test_reducing_gap_8( + self, gradients_image: Image.Image, box: tuple[float, float, float, float] + ) -> None: + ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) + im = gradients_image.resize( + (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=8.0 + ) + + assert_image_equal(ref, im) + + @pytest.mark.parametrize( + "box, epsilon", + (((0, 0, 512, 512), 5.5), ((0.9, 1.7, 128, 128), 9.5)), + ) + def test_box_filter( + self, + gradients_image: Image.Image, + box: tuple[float, float, float, float], + epsilon: float, + ) -> None: + ref = gradients_image.resize((52, 34), Image.Resampling.BOX, box=box) + im = gradients_image.resize( + (52, 34), Image.Resampling.BOX, box=box, reducing_gap=1.0 + ) + + assert_image_similar(ref, im, epsilon) + + +class TestImageResize: + def test_resize(self) -> None: + def resize(mode: str, size: tuple[int, int]) -> None: + out = hopper(mode).resize(size) + assert out.mode == mode + assert out.size == size + + for mode in "1", "P", "L", "RGB", "I", "F": + resize(mode, (112, 103)) + resize(mode, (188, 214)) + + # Test unknown resampling filter + with hopper() as im: + with pytest.raises(ValueError): + im.resize((10, 10), "unknown") + + @skip_unless_feature("libtiff") + def test_load_first(self) -> None: + # load() may change the size of the image + # Test that resize() is calling it before getting the size + with Image.open("Tests/images/g4_orientation_5.tif") as im: + im = im.resize((64, 64)) + assert im.size == (64, 64) + + @pytest.mark.parametrize("mode", ("L", "RGB", "I", "F")) + def test_default_filter_bicubic(self, mode: str) -> None: + im = hopper(mode) + assert im.resize((20, 20), Image.Resampling.BICUBIC) == im.resize((20, 20)) + + @pytest.mark.parametrize( + "mode", ("1", "P", "I;16", "I;16L", "I;16B", "BGR;15", "BGR;16") + ) + def test_default_filter_nearest(self, mode: str) -> None: + im = hopper(mode) + assert im.resize((20, 20), Image.Resampling.NEAREST) == im.resize((20, 20)) diff --git a/Tests/test_image_rotate.py b/Tests/test_image_rotate.py index 252a15db7..0d476ade5 100644 --- a/Tests/test_image_rotate.py +++ b/Tests/test_image_rotate.py @@ -1,156 +1,156 @@ -from __future__ import annotations - -import pytest - -from PIL import Image - -from .helper import ( - assert_image_equal, - assert_image_equal_tofile, - assert_image_similar, - hopper, -) - - -def rotate( - im: Image.Image, - mode: str, - angle: int, - center: tuple[int, int] | None = None, - translate: tuple[int, int] | None = None, -) -> None: - out = im.rotate(angle, center=center, translate=translate) - assert out.mode == mode - assert out.size == im.size # default rotate clips output - out = im.rotate(angle, center=center, translate=translate, expand=1) - assert out.mode == mode - if angle % 180 == 0: - assert out.size == im.size - elif im.size == (0, 0): - assert out.size == im.size - else: - assert out.size != im.size - - -@pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F")) -def test_mode(mode: str) -> None: - im = hopper(mode) - rotate(im, mode, 45) - - -@pytest.mark.parametrize("angle", (0, 90, 180, 270)) -def test_angle(angle: int) -> None: - with Image.open("Tests/images/test-card.png") as im: - rotate(im, im.mode, angle) - - im = hopper() - assert_image_equal(im.rotate(angle), im.rotate(angle, expand=1)) - - -@pytest.mark.parametrize("angle", (0, 45, 90, 180, 270)) -def test_zero(angle: int) -> None: - im = Image.new("RGB", (0, 0)) - rotate(im, im.mode, angle) - - -def test_resample() -> None: - # Target image creation, inspected by eye. - # >>> im = Image.open('Tests/images/hopper.ppm') - # >>> im = im.rotate(45, resample=Image.Resampling.BICUBIC, expand=True) - # >>> im.save('Tests/images/hopper_45.png') - - with Image.open("Tests/images/hopper_45.png") as target: - for resample, epsilon in ( - (Image.Resampling.NEAREST, 10), - (Image.Resampling.BILINEAR, 5), - (Image.Resampling.BICUBIC, 0), - ): - im = hopper() - im = im.rotate(45, resample=resample, expand=True) - assert_image_similar(im, target, epsilon) - - -def test_center_0() -> None: - im = hopper() - im = im.rotate(45, center=(0, 0), resample=Image.Resampling.BICUBIC) - - with Image.open("Tests/images/hopper_45.png") as target: - target_origin = target.size[1] / 2 - target = target.crop((0, target_origin, 128, target_origin + 128)) - - assert_image_similar(im, target, 15) - - -def test_center_14() -> None: - im = hopper() - im = im.rotate(45, center=(14, 14), resample=Image.Resampling.BICUBIC) - - with Image.open("Tests/images/hopper_45.png") as target: - target_origin = target.size[1] / 2 - 14 - target = target.crop((6, target_origin, 128 + 6, target_origin + 128)) - - assert_image_similar(im, target, 10) - - -def test_translate() -> None: - im = hopper() - with Image.open("Tests/images/hopper_45.png") as target: - target_origin = (target.size[1] / 2 - 64) - 5 - target = target.crop( - (target_origin, target_origin, target_origin + 128, target_origin + 128) - ) - - im = im.rotate(45, translate=(5, 5), resample=Image.Resampling.BICUBIC) - - assert_image_similar(im, target, 1) - - -def test_fastpath_center() -> None: - # if the center is -1,-1 and we rotate by 90<=x<=270 the - # resulting image should be black - for angle in (90, 180, 270): - im = hopper().rotate(angle, center=(-1, -1)) - assert_image_equal(im, Image.new("RGB", im.size, "black")) - - -def test_fastpath_translate() -> None: - # if we post-translate by -128 - # resulting image should be black - for angle in (0, 90, 180, 270): - im = hopper().rotate(angle, translate=(-128, -128)) - assert_image_equal(im, Image.new("RGB", im.size, "black")) - - -def test_center() -> None: - im = hopper() - rotate(im, im.mode, 45, center=(0, 0)) - rotate(im, im.mode, 45, translate=(im.size[0] // 2, 0)) - rotate(im, im.mode, 45, center=(0, 0), translate=(im.size[0] // 2, 0)) - - -def test_rotate_no_fill() -> None: - im = Image.new("RGB", (100, 100), "green") - im = im.rotate(45) - assert_image_equal_tofile(im, "Tests/images/rotate_45_no_fill.png") - - -def test_rotate_with_fill() -> None: - im = Image.new("RGB", (100, 100), "green") - im = im.rotate(45, fillcolor="white") - assert_image_equal_tofile(im, "Tests/images/rotate_45_with_fill.png") - - -def test_alpha_rotate_no_fill() -> None: - # Alpha images are handled differently internally - im = Image.new("RGBA", (10, 10), "green") - im = im.rotate(45, expand=1) - corner = im.getpixel((0, 0)) - assert corner == (0, 0, 0, 0) - - -def test_alpha_rotate_with_fill() -> None: - # Alpha images are handled differently internally - im = Image.new("RGBA", (10, 10), "green") - im = im.rotate(45, expand=1, fillcolor=(255, 0, 0, 255)) - corner = im.getpixel((0, 0)) - assert corner == (255, 0, 0, 255) +from __future__ import annotations + +import pytest + +from PIL import Image + +from .helper import ( + assert_image_equal, + assert_image_equal_tofile, + assert_image_similar, + hopper, +) + + +def rotate( + im: Image.Image, + mode: str, + angle: int, + center: tuple[int, int] | None = None, + translate: tuple[int, int] | None = None, +) -> None: + out = im.rotate(angle, center=center, translate=translate) + assert out.mode == mode + assert out.size == im.size # default rotate clips output + out = im.rotate(angle, center=center, translate=translate, expand=1) + assert out.mode == mode + if angle % 180 == 0: + assert out.size == im.size + elif im.size == (0, 0): + assert out.size == im.size + else: + assert out.size != im.size + + +@pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F")) +def test_mode(mode: str) -> None: + im = hopper(mode) + rotate(im, mode, 45) + + +@pytest.mark.parametrize("angle", (0, 90, 180, 270)) +def test_angle(angle: int) -> None: + with Image.open("Tests/images/test-card.png") as im: + rotate(im, im.mode, angle) + + im = hopper() + assert_image_equal(im.rotate(angle), im.rotate(angle, expand=1)) + + +@pytest.mark.parametrize("angle", (0, 45, 90, 180, 270)) +def test_zero(angle: int) -> None: + im = Image.new("RGB", (0, 0)) + rotate(im, im.mode, angle) + + +def test_resample() -> None: + # Target image creation, inspected by eye. + # >>> im = Image.open('Tests/images/hopper.ppm') + # >>> im = im.rotate(45, resample=Image.Resampling.BICUBIC, expand=True) + # >>> im.save('Tests/images/hopper_45.png') + + with Image.open("Tests/images/hopper_45.png") as target: + for resample, epsilon in ( + (Image.Resampling.NEAREST, 10), + (Image.Resampling.BILINEAR, 5), + (Image.Resampling.BICUBIC, 0), + ): + im = hopper() + im = im.rotate(45, resample=resample, expand=True) + assert_image_similar(im, target, epsilon) + + +def test_center_0() -> None: + im = hopper() + im = im.rotate(45, center=(0, 0), resample=Image.Resampling.BICUBIC) + + with Image.open("Tests/images/hopper_45.png") as target: + target_origin = target.size[1] / 2 + target = target.crop((0, target_origin, 128, target_origin + 128)) + + assert_image_similar(im, target, 15) + + +def test_center_14() -> None: + im = hopper() + im = im.rotate(45, center=(14, 14), resample=Image.Resampling.BICUBIC) + + with Image.open("Tests/images/hopper_45.png") as target: + target_origin = target.size[1] / 2 - 14 + target = target.crop((6, target_origin, 128 + 6, target_origin + 128)) + + assert_image_similar(im, target, 10) + + +def test_translate() -> None: + im = hopper() + with Image.open("Tests/images/hopper_45.png") as target: + target_origin = (target.size[1] / 2 - 64) - 5 + target = target.crop( + (target_origin, target_origin, target_origin + 128, target_origin + 128) + ) + + im = im.rotate(45, translate=(5, 5), resample=Image.Resampling.BICUBIC) + + assert_image_similar(im, target, 1) + + +def test_fastpath_center() -> None: + # if the center is -1,-1 and we rotate by 90<=x<=270 the + # resulting image should be black + for angle in (90, 180, 270): + im = hopper().rotate(angle, center=(-1, -1)) + assert_image_equal(im, Image.new("RGB", im.size, "black")) + + +def test_fastpath_translate() -> None: + # if we post-translate by -128 + # resulting image should be black + for angle in (0, 90, 180, 270): + im = hopper().rotate(angle, translate=(-128, -128)) + assert_image_equal(im, Image.new("RGB", im.size, "black")) + + +def test_center() -> None: + im = hopper() + rotate(im, im.mode, 45, center=(0, 0)) + rotate(im, im.mode, 45, translate=(im.size[0] // 2, 0)) + rotate(im, im.mode, 45, center=(0, 0), translate=(im.size[0] // 2, 0)) + + +def test_rotate_no_fill() -> None: + im = Image.new("RGB", (100, 100), "green") + im = im.rotate(45) + assert_image_equal_tofile(im, "Tests/images/rotate_45_no_fill.png") + + +def test_rotate_with_fill() -> None: + im = Image.new("RGB", (100, 100), "green") + im = im.rotate(45, fillcolor="white") + assert_image_equal_tofile(im, "Tests/images/rotate_45_with_fill.png") + + +def test_alpha_rotate_no_fill() -> None: + # Alpha images are handled differently internally + im = Image.new("RGBA", (10, 10), "green") + im = im.rotate(45, expand=1) + corner = im.getpixel((0, 0)) + assert corner == (0, 0, 0, 0) + + +def test_alpha_rotate_with_fill() -> None: + # Alpha images are handled differently internally + im = Image.new("RGBA", (10, 10), "green") + im = im.rotate(45, expand=1, fillcolor=(255, 0, 0, 255)) + corner = im.getpixel((0, 0)) + assert corner == (255, 0, 0, 255) diff --git a/Tests/test_image_split.py b/Tests/test_image_split.py index 3385f81f5..162426e2c 100644 --- a/Tests/test_image_split.py +++ b/Tests/test_image_split.py @@ -1,62 +1,62 @@ -from __future__ import annotations - -from pathlib import Path - -import pytest - -from PIL import Image, features - -from .helper import assert_image_equal, hopper - - -def test_split() -> None: - def split(mode: str) -> list[tuple[str, int, int]]: - layers = hopper(mode).split() - return [(i.mode, i.size[0], i.size[1]) for i in layers] - - assert split("1") == [("1", 128, 128)] - assert split("L") == [("L", 128, 128)] - assert split("I") == [("I", 128, 128)] - assert split("F") == [("F", 128, 128)] - assert split("P") == [("P", 128, 128)] - assert split("RGB") == [("L", 128, 128), ("L", 128, 128), ("L", 128, 128)] - assert split("RGBA") == [ - ("L", 128, 128), - ("L", 128, 128), - ("L", 128, 128), - ("L", 128, 128), - ] - assert split("CMYK") == [ - ("L", 128, 128), - ("L", 128, 128), - ("L", 128, 128), - ("L", 128, 128), - ] - assert split("YCbCr") == [("L", 128, 128), ("L", 128, 128), ("L", 128, 128)] - - -@pytest.mark.parametrize( - "mode", ("1", "L", "I", "F", "P", "RGB", "RGBA", "CMYK", "YCbCr") -) -def test_split_merge(mode: str) -> None: - expected = Image.merge(mode, hopper(mode).split()) - assert_image_equal(hopper(mode), expected) - - -def test_split_open(tmp_path: Path) -> None: - if features.check("zlib"): - test_file = str(tmp_path / "temp.png") - else: - test_file = str(tmp_path / "temp.pcx") - - def split_open(mode: str) -> int: - hopper(mode).save(test_file) - with Image.open(test_file) as im: - return len(im.split()) - - assert split_open("1") == 1 - assert split_open("L") == 1 - assert split_open("P") == 1 - assert split_open("RGB") == 3 - if features.check("zlib"): - assert split_open("RGBA") == 4 +from __future__ import annotations + +from pathlib import Path + +import pytest + +from PIL import Image, features + +from .helper import assert_image_equal, hopper + + +def test_split() -> None: + def split(mode: str) -> list[tuple[str, int, int]]: + layers = hopper(mode).split() + return [(i.mode, i.size[0], i.size[1]) for i in layers] + + assert split("1") == [("1", 128, 128)] + assert split("L") == [("L", 128, 128)] + assert split("I") == [("I", 128, 128)] + assert split("F") == [("F", 128, 128)] + assert split("P") == [("P", 128, 128)] + assert split("RGB") == [("L", 128, 128), ("L", 128, 128), ("L", 128, 128)] + assert split("RGBA") == [ + ("L", 128, 128), + ("L", 128, 128), + ("L", 128, 128), + ("L", 128, 128), + ] + assert split("CMYK") == [ + ("L", 128, 128), + ("L", 128, 128), + ("L", 128, 128), + ("L", 128, 128), + ] + assert split("YCbCr") == [("L", 128, 128), ("L", 128, 128), ("L", 128, 128)] + + +@pytest.mark.parametrize( + "mode", ("1", "L", "I", "F", "P", "RGB", "RGBA", "CMYK", "YCbCr") +) +def test_split_merge(mode: str) -> None: + expected = Image.merge(mode, hopper(mode).split()) + assert_image_equal(hopper(mode), expected) + + +def test_split_open(tmp_path: Path) -> None: + if features.check("zlib"): + test_file = str(tmp_path / "temp.png") + else: + test_file = str(tmp_path / "temp.pcx") + + def split_open(mode: str) -> int: + hopper(mode).save(test_file) + with Image.open(test_file) as im: + return len(im.split()) + + assert split_open("1") == 1 + assert split_open("L") == 1 + assert split_open("P") == 1 + assert split_open("RGB") == 3 + if features.check("zlib"): + assert split_open("RGBA") == 4 diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py index 1593eaaf7..6e1d156f4 100644 --- a/Tests/test_image_thumbnail.py +++ b/Tests/test_image_thumbnail.py @@ -1,169 +1,169 @@ -from __future__ import annotations - -import pytest - -from PIL import Image - -from .helper import ( - assert_image_equal, - assert_image_similar, - fromstring, - hopper, - skip_unless_feature, - tostring, -) - - -def test_sanity() -> None: - im = hopper() - assert im.thumbnail((100, 100)) is None - - assert im.size == (100, 100) - - -def test_aspect() -> None: - im = Image.new("L", (128, 128)) - im.thumbnail((100, 100)) - assert im.size == (100, 100) - - im = Image.new("L", (128, 256)) - im.thumbnail((100, 100)) - assert im.size == (50, 100) - - im = Image.new("L", (128, 256)) - im.thumbnail((50, 100)) - assert im.size == (50, 100) - - im = Image.new("L", (256, 128)) - im.thumbnail((100, 100)) - assert im.size == (100, 50) - - im = Image.new("L", (256, 128)) - im.thumbnail((100, 50)) - assert im.size == (100, 50) - - im = Image.new("L", (64, 64)) - im.thumbnail((100, 100)) - assert im.size == (64, 64) - - im = Image.new("L", (256, 162)) # ratio is 1.5802469136 - im.thumbnail((33, 33)) - assert im.size == (33, 21) # ratio is 1.5714285714 - - im = Image.new("L", (162, 256)) # ratio is 0.6328125 - im.thumbnail((33, 33)) - assert im.size == (21, 33) # ratio is 0.6363636364 - - im = Image.new("L", (145, 100)) # ratio is 1.45 - im.thumbnail((50, 50)) - assert im.size == (50, 34) # ratio is 1.47058823529 - - im = Image.new("L", (100, 145)) # ratio is 0.689655172414 - im.thumbnail((50, 50)) - assert im.size == (34, 50) # ratio is 0.68 - - im = Image.new("L", (100, 30)) # ratio is 3.333333333333 - im.thumbnail((75, 75)) - assert im.size == (75, 23) # ratio is 3.260869565217 - - -def test_division_by_zero() -> None: - im = Image.new("L", (200, 2)) - im.thumbnail((75, 75)) - assert im.size == (75, 1) - - -def test_float() -> None: - im = Image.new("L", (128, 128)) - im.thumbnail((99.9, 99.9)) - assert im.size == (99, 99) - - -def test_no_resize() -> None: - # Check that draft() can resize the image to the destination size - with Image.open("Tests/images/hopper.jpg") as im: - im.draft(None, (64, 64)) - assert im.size == (64, 64) - - # Test thumbnail(), where only draft() is necessary to resize the image - with Image.open("Tests/images/hopper.jpg") as im: - im.thumbnail((64, 64)) - assert im.size == (64, 64) - - -@skip_unless_feature("libtiff") -def test_load_first() -> None: - # load() may change the size of the image - # Test that thumbnail() is calling it before performing size calculations - with Image.open("Tests/images/g4_orientation_5.tif") as im: - im.thumbnail((64, 64)) - assert im.size == (64, 10) - - # Test thumbnail(), without draft(), - # on an image that is large enough once load() has changed the size - with Image.open("Tests/images/g4_orientation_5.tif") as im: - im.thumbnail((590, 88), reducing_gap=None) - assert im.size == (590, 88) - - -def test_load_first_unless_jpeg() -> None: - # Test that thumbnail() still uses draft() for JPEG - with Image.open("Tests/images/hopper.jpg") as im: - draft = im.draft - - def im_draft( - mode: str, size: tuple[int, int] - ) -> tuple[str, tuple[int, int, float, float]] | None: - result = draft(mode, size) - assert result is not None - - return result - - im.draft = im_draft - - im.thumbnail((64, 64)) - - -# valgrind test is failing with memory allocated in libjpeg -@pytest.mark.valgrind_known_error(reason="Known Failing") -def test_DCT_scaling_edges() -> None: - # Make an image with red borders and size (N * 8) + 1 to cross DCT grid - im = Image.new("RGB", (257, 257), "red") - im.paste(Image.new("RGB", (235, 235)), (11, 11)) - - thumb = fromstring(tostring(im, "JPEG", quality=99, subsampling=0)) - # small reducing_gap to amplify the effect - thumb.thumbnail((32, 32), Image.Resampling.BICUBIC, reducing_gap=1.0) - - ref = im.resize((32, 32), Image.Resampling.BICUBIC) - # This is still JPEG, some error is present. Without the fix it is 11.5 - assert_image_similar(thumb, ref, 1.5) - - -def test_reducing_gap_values() -> None: - im = hopper() - im.thumbnail((18, 18), Image.Resampling.BICUBIC) - - ref = hopper() - ref.thumbnail((18, 18), Image.Resampling.BICUBIC, reducing_gap=2.0) - # reducing_gap=2.0 should be the default - assert_image_equal(ref, im) - - ref = hopper() - ref.thumbnail((18, 18), Image.Resampling.BICUBIC, reducing_gap=None) - with pytest.raises(pytest.fail.Exception): - assert_image_equal(ref, im) - - assert_image_similar(ref, im, 3.5) - - -def test_reducing_gap_for_DCT_scaling() -> None: - with Image.open("Tests/images/hopper.jpg") as ref: - # thumbnail should call draft with reducing_gap scale - ref.draft(None, (18 * 3, 18 * 3)) - ref = ref.resize((18, 18), Image.Resampling.BICUBIC) - - with Image.open("Tests/images/hopper.jpg") as im: - im.thumbnail((18, 18), Image.Resampling.BICUBIC, reducing_gap=3.0) - - assert_image_similar(ref, im, 1.4) +from __future__ import annotations + +import pytest + +from PIL import Image + +from .helper import ( + assert_image_equal, + assert_image_similar, + fromstring, + hopper, + skip_unless_feature, + tostring, +) + + +def test_sanity() -> None: + im = hopper() + assert im.thumbnail((100, 100)) is None + + assert im.size == (100, 100) + + +def test_aspect() -> None: + im = Image.new("L", (128, 128)) + im.thumbnail((100, 100)) + assert im.size == (100, 100) + + im = Image.new("L", (128, 256)) + im.thumbnail((100, 100)) + assert im.size == (50, 100) + + im = Image.new("L", (128, 256)) + im.thumbnail((50, 100)) + assert im.size == (50, 100) + + im = Image.new("L", (256, 128)) + im.thumbnail((100, 100)) + assert im.size == (100, 50) + + im = Image.new("L", (256, 128)) + im.thumbnail((100, 50)) + assert im.size == (100, 50) + + im = Image.new("L", (64, 64)) + im.thumbnail((100, 100)) + assert im.size == (64, 64) + + im = Image.new("L", (256, 162)) # ratio is 1.5802469136 + im.thumbnail((33, 33)) + assert im.size == (33, 21) # ratio is 1.5714285714 + + im = Image.new("L", (162, 256)) # ratio is 0.6328125 + im.thumbnail((33, 33)) + assert im.size == (21, 33) # ratio is 0.6363636364 + + im = Image.new("L", (145, 100)) # ratio is 1.45 + im.thumbnail((50, 50)) + assert im.size == (50, 34) # ratio is 1.47058823529 + + im = Image.new("L", (100, 145)) # ratio is 0.689655172414 + im.thumbnail((50, 50)) + assert im.size == (34, 50) # ratio is 0.68 + + im = Image.new("L", (100, 30)) # ratio is 3.333333333333 + im.thumbnail((75, 75)) + assert im.size == (75, 23) # ratio is 3.260869565217 + + +def test_division_by_zero() -> None: + im = Image.new("L", (200, 2)) + im.thumbnail((75, 75)) + assert im.size == (75, 1) + + +def test_float() -> None: + im = Image.new("L", (128, 128)) + im.thumbnail((99.9, 99.9)) + assert im.size == (99, 99) + + +def test_no_resize() -> None: + # Check that draft() can resize the image to the destination size + with Image.open("Tests/images/hopper.jpg") as im: + im.draft(None, (64, 64)) + assert im.size == (64, 64) + + # Test thumbnail(), where only draft() is necessary to resize the image + with Image.open("Tests/images/hopper.jpg") as im: + im.thumbnail((64, 64)) + assert im.size == (64, 64) + + +@skip_unless_feature("libtiff") +def test_load_first() -> None: + # load() may change the size of the image + # Test that thumbnail() is calling it before performing size calculations + with Image.open("Tests/images/g4_orientation_5.tif") as im: + im.thumbnail((64, 64)) + assert im.size == (64, 10) + + # Test thumbnail(), without draft(), + # on an image that is large enough once load() has changed the size + with Image.open("Tests/images/g4_orientation_5.tif") as im: + im.thumbnail((590, 88), reducing_gap=None) + assert im.size == (590, 88) + + +def test_load_first_unless_jpeg() -> None: + # Test that thumbnail() still uses draft() for JPEG + with Image.open("Tests/images/hopper.jpg") as im: + draft = im.draft + + def im_draft( + mode: str, size: tuple[int, int] + ) -> tuple[str, tuple[int, int, float, float]] | None: + result = draft(mode, size) + assert result is not None + + return result + + im.draft = im_draft + + im.thumbnail((64, 64)) + + +# valgrind test is failing with memory allocated in libjpeg +@pytest.mark.valgrind_known_error(reason="Known Failing") +def test_DCT_scaling_edges() -> None: + # Make an image with red borders and size (N * 8) + 1 to cross DCT grid + im = Image.new("RGB", (257, 257), "red") + im.paste(Image.new("RGB", (235, 235)), (11, 11)) + + thumb = fromstring(tostring(im, "JPEG", quality=99, subsampling=0)) + # small reducing_gap to amplify the effect + thumb.thumbnail((32, 32), Image.Resampling.BICUBIC, reducing_gap=1.0) + + ref = im.resize((32, 32), Image.Resampling.BICUBIC) + # This is still JPEG, some error is present. Without the fix it is 11.5 + assert_image_similar(thumb, ref, 1.5) + + +def test_reducing_gap_values() -> None: + im = hopper() + im.thumbnail((18, 18), Image.Resampling.BICUBIC) + + ref = hopper() + ref.thumbnail((18, 18), Image.Resampling.BICUBIC, reducing_gap=2.0) + # reducing_gap=2.0 should be the default + assert_image_equal(ref, im) + + ref = hopper() + ref.thumbnail((18, 18), Image.Resampling.BICUBIC, reducing_gap=None) + with pytest.raises(pytest.fail.Exception): + assert_image_equal(ref, im) + + assert_image_similar(ref, im, 3.5) + + +def test_reducing_gap_for_DCT_scaling() -> None: + with Image.open("Tests/images/hopper.jpg") as ref: + # thumbnail should call draft with reducing_gap scale + ref.draft(None, (18 * 3, 18 * 3)) + ref = ref.resize((18, 18), Image.Resampling.BICUBIC) + + with Image.open("Tests/images/hopper.jpg") as im: + im.thumbnail((18, 18), Image.Resampling.BICUBIC, reducing_gap=3.0) + + assert_image_similar(ref, im, 1.4) diff --git a/Tests/test_image_tobitmap.py b/Tests/test_image_tobitmap.py index f7a3cc41d..03f2ce21d 100644 --- a/Tests/test_image_tobitmap.py +++ b/Tests/test_image_tobitmap.py @@ -1,17 +1,17 @@ -from __future__ import annotations - -import pytest - -from .helper import assert_image_equal, fromstring, hopper - - -def test_sanity() -> None: - with pytest.raises(ValueError): - hopper().tobitmap() - - im1 = hopper().convert("1") - - bitmap = im1.tobitmap() - - assert isinstance(bitmap, bytes) - assert_image_equal(im1, fromstring(bitmap)) +from __future__ import annotations + +import pytest + +from .helper import assert_image_equal, fromstring, hopper + + +def test_sanity() -> None: + with pytest.raises(ValueError): + hopper().tobitmap() + + im1 = hopper().convert("1") + + bitmap = im1.tobitmap() + + assert isinstance(bitmap, bytes) + assert_image_equal(im1, fromstring(bitmap)) diff --git a/Tests/test_image_tobytes.py b/Tests/test_image_tobytes.py index d32b6c09b..56aac6dcf 100644 --- a/Tests/test_image_tobytes.py +++ b/Tests/test_image_tobytes.py @@ -1,8 +1,8 @@ -from __future__ import annotations - -from .helper import hopper - - -def test_sanity() -> None: - data = hopper().tobytes() - assert isinstance(data, bytes) +from __future__ import annotations + +from .helper import hopper + + +def test_sanity() -> None: + data = hopper().tobytes() + assert isinstance(data, bytes) diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index 638d12247..7459efc9d 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -1,381 +1,381 @@ -from __future__ import annotations - -import math -from typing import Callable - -import pytest - -from PIL import Image, ImageTransform - -from .helper import assert_image_equal, assert_image_similar, hopper - - -class TestImageTransform: - def test_sanity(self) -> None: - im = hopper() - - for transform in ( - ImageTransform.AffineTransform((1, 0, 0, 0, 1, 0)), - ImageTransform.PerspectiveTransform((1, 0, 0, 0, 1, 0, 0, 0)), - ImageTransform.ExtentTransform((0, 0) + im.size), - ImageTransform.QuadTransform( - (0, 0, 0, im.height, im.width, im.height, im.width, 0) - ), - ImageTransform.MeshTransform( - [ - ( - (0, 0) + im.size, - (0, 0, 0, im.height, im.width, im.height, im.width, 0), - ) - ] - ), - ): - assert_image_equal(im, im.transform(im.size, transform)) - - def test_info(self) -> None: - comment = b"File written by Adobe Photoshop\xa8 4.0" - - with Image.open("Tests/images/hopper.gif") as im: - assert im.info["comment"] == comment - - transform = ImageTransform.ExtentTransform((0, 0, 0, 0)) - new_im = im.transform((100, 100), transform) - assert new_im.info["comment"] == comment - - def test_palette(self) -> None: - with Image.open("Tests/images/hopper.gif") as im: - transformed = im.transform( - im.size, Image.Transform.AFFINE, [1, 0, 0, 0, 1, 0] - ) - assert im.palette.palette == transformed.palette.palette - - def test_extent(self) -> None: - im = hopper("RGB") - (w, h) = im.size - transformed = im.transform( - im.size, - Image.Transform.EXTENT, - (0, 0, w // 2, h // 2), # ul -> lr - Image.Resampling.BILINEAR, - ) - - scaled = im.resize((w * 2, h * 2), Image.Resampling.BILINEAR).crop((0, 0, w, h)) - - # undone -- precision? - assert_image_similar(transformed, scaled, 23) - - def test_quad(self) -> None: - # one simple quad transform, equivalent to scale & crop upper left quad - im = hopper("RGB") - (w, h) = im.size - transformed = im.transform( - im.size, - Image.Transform.QUAD, - (0, 0, 0, h // 2, w // 2, h // 2, w // 2, 0), # ul -> ccw around quad - Image.Resampling.BILINEAR, - ) - - scaled = im.transform( - (w, h), - Image.Transform.AFFINE, - (0.5, 0, 0, 0, 0.5, 0), - Image.Resampling.BILINEAR, - ) - - assert_image_equal(transformed, scaled) - - @pytest.mark.parametrize( - "mode, expected_pixel", - ( - ("RGB", (255, 0, 0)), - ("RGBA", (255, 0, 0, 255)), - ("LA", (76, 0)), - ), - ) - def test_fill(self, mode: str, expected_pixel: tuple[int, ...]) -> None: - im = hopper(mode) - (w, h) = im.size - transformed = im.transform( - im.size, - Image.Transform.EXTENT, - (0, 0, w * 2, h * 2), - Image.Resampling.BILINEAR, - fillcolor="red", - ) - assert transformed.getpixel((w - 1, h - 1)) == expected_pixel - - def test_mesh(self) -> None: - # this should be a checkerboard of halfsized hoppers in ul, lr - im = hopper("RGBA") - (w, h) = im.size - transformed = im.transform( - im.size, - Image.Transform.MESH, - ( - ( - (0, 0, w // 2, h // 2), # box - (0, 0, 0, h, w, h, w, 0), # ul -> ccw around quad - ), - ( - (w // 2, h // 2, w, h), # box - (0, 0, 0, h, w, h, w, 0), # ul -> ccw around quad - ), - ), - Image.Resampling.BILINEAR, - ) - - scaled = im.transform( - (w // 2, h // 2), - Image.Transform.AFFINE, - (2, 0, 0, 0, 2, 0), - Image.Resampling.BILINEAR, - ) - - checker = Image.new("RGBA", im.size) - checker.paste(scaled, (0, 0)) - checker.paste(scaled, (w // 2, h // 2)) - - assert_image_equal(transformed, checker) - - # now, check to see that the extra area is (0, 0, 0, 0) - blank = Image.new("RGBA", (w // 2, h // 2), (0, 0, 0, 0)) - - assert_image_equal(blank, transformed.crop((w // 2, 0, w, h // 2))) - assert_image_equal(blank, transformed.crop((0, h // 2, w // 2, h))) - - def _test_alpha_premult( - self, op: Callable[[Image.Image, tuple[int, int]], Image.Image] - ) -> None: - # create image with half white, half black, - # with the black half transparent. - # do op, - # there should be no darkness in the white section. - im = Image.new("RGBA", (10, 10), (0, 0, 0, 0)) - im2 = Image.new("RGBA", (5, 10), (255, 255, 255, 255)) - im.paste(im2, (0, 0)) - - im = op(im, (40, 10)) - im_background = Image.new("RGB", (40, 10), (255, 255, 255)) - im_background.paste(im, (0, 0), im) - - hist = im_background.histogram() - assert 40 * 10 == hist[-1] - - def test_alpha_premult_resize(self) -> None: - def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image: - return im.resize(sz, Image.Resampling.BILINEAR) - - self._test_alpha_premult(op) - - def test_alpha_premult_transform(self) -> None: - def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image: - (w, h) = im.size - return im.transform( - sz, Image.Transform.EXTENT, (0, 0, w, h), Image.Resampling.BILINEAR - ) - - self._test_alpha_premult(op) - - def _test_nearest( - self, op: Callable[[Image.Image, tuple[int, int]], Image.Image], mode: str - ) -> None: - # create white image with half transparent, - # do op, - # the image should remain white with half transparent - transparent, opaque = { - "RGBA": ((255, 255, 255, 0), (255, 255, 255, 255)), - "LA": ((255, 0), (255, 255)), - }[mode] - im = Image.new(mode, (10, 10), transparent) - im2 = Image.new(mode, (5, 10), opaque) - im.paste(im2, (0, 0)) - - im = op(im, (40, 10)) - - colors = sorted(im.getcolors()) - assert colors == sorted( - ( - (20 * 10, opaque), - (20 * 10, transparent), - ) - ) - - @pytest.mark.parametrize("mode", ("RGBA", "LA")) - def test_nearest_resize(self, mode: str) -> None: - def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image: - return im.resize(sz, Image.Resampling.NEAREST) - - self._test_nearest(op, mode) - - @pytest.mark.parametrize("mode", ("RGBA", "LA")) - def test_nearest_transform(self, mode: str) -> None: - def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image: - (w, h) = im.size - return im.transform( - sz, Image.Transform.EXTENT, (0, 0, w, h), Image.Resampling.NEAREST - ) - - self._test_nearest(op, mode) - - def test_blank_fill(self) -> None: - # attempting to hit - # https://github.com/python-pillow/Pillow/issues/254 reported - # - # issue is that transforms with transparent overflow area - # contained junk from previous images, especially on systems with - # constrained memory. So, attempt to fill up memory with a - # pattern, free it, and then run the mesh test again. Using a 1Mp - # image with 4 bands, for 4 megs of data allocated, x 64. OMM (64 - # bit 12.04 VM with 512 megs available, this fails with Pillow < - # a0eaf06cc5f62a6fb6de556989ac1014ff3348ea - # - # Running by default, but I'd totally understand not doing it in - # the future - - pattern: list[Image.Image] | None = [ - Image.new("RGBA", (1024, 1024), (a, a, a, a)) for a in range(1, 65) - ] - - # Yeah. Watch some JIT optimize this out. - pattern = None # noqa: F841 - - self.test_mesh() - - def test_missing_method_data(self) -> None: - with hopper() as im: - with pytest.raises(ValueError): - im.transform((100, 100), None) - - @pytest.mark.parametrize("resample", (Image.Resampling.BOX, "unknown")) - def test_unknown_resampling_filter(self, resample: Image.Resampling | str) -> None: - with hopper() as im: - (w, h) = im.size - with pytest.raises(ValueError): - im.transform((100, 100), Image.Transform.EXTENT, (0, 0, w, h), resample) - - -class TestImageTransformAffine: - transform = Image.Transform.AFFINE - - def _test_image(self) -> Image.Image: - im = hopper("RGB") - return im.crop((10, 20, im.width - 10, im.height - 20)) - - @pytest.mark.parametrize( - "deg, transpose", - ( - (0, None), - (90, Image.Transpose.ROTATE_90), - (180, Image.Transpose.ROTATE_180), - (270, Image.Transpose.ROTATE_270), - ), - ) - def test_rotate(self, deg: int, transpose: Image.Transpose | None) -> None: - im = self._test_image() - - angle = -math.radians(deg) - matrix = [ - round(math.cos(angle), 15), - round(math.sin(angle), 15), - 0.0, - round(-math.sin(angle), 15), - round(math.cos(angle), 15), - 0.0, - 0, - 0, - ] - matrix[2] = (1 - matrix[0] - matrix[1]) * im.width / 2 - matrix[5] = (1 - matrix[3] - matrix[4]) * im.height / 2 - - if transpose is not None: - transposed = im.transpose(transpose) - else: - transposed = im - - for resample in [ - Image.Resampling.NEAREST, - Image.Resampling.BILINEAR, - Image.Resampling.BICUBIC, - ]: - transformed = im.transform( - transposed.size, self.transform, matrix, resample - ) - assert_image_equal(transposed, transformed) - - @pytest.mark.parametrize( - "scale, epsilon_scale", - ( - (1.1, 6.9), - (1.5, 5.5), - (2.0, 5.5), - (2.3, 3.7), - (2.5, 3.7), - ), - ) - @pytest.mark.parametrize( - "resample,epsilon", - ( - (Image.Resampling.NEAREST, 0), - (Image.Resampling.BILINEAR, 2), - (Image.Resampling.BICUBIC, 1), - ), - ) - def test_resize( - self, - scale: float, - epsilon_scale: float, - resample: Image.Resampling, - epsilon: int, - ) -> None: - im = self._test_image() - - size_up = int(round(im.width * scale)), int(round(im.height * scale)) - matrix_up = [1 / scale, 0, 0, 0, 1 / scale, 0, 0, 0] - matrix_down = [scale, 0, 0, 0, scale, 0, 0, 0] - - transformed = im.transform(size_up, self.transform, matrix_up, resample) - transformed = transformed.transform( - im.size, self.transform, matrix_down, resample - ) - assert_image_similar(transformed, im, epsilon * epsilon_scale) - - @pytest.mark.parametrize( - "x, y, epsilon_scale", - ( - (0.1, 0, 3.7), - (0.6, 0, 9.1), - (50, 50, 0), - ), - ) - @pytest.mark.parametrize( - "resample, epsilon", - ( - (Image.Resampling.NEAREST, 0), - (Image.Resampling.BILINEAR, 1.5), - (Image.Resampling.BICUBIC, 1), - ), - ) - def test_translate( - self, - x: float, - y: float, - epsilon_scale: float, - resample: Image.Resampling, - epsilon: float, - ) -> None: - im = self._test_image() - - size_up = int(round(im.width + x)), int(round(im.height + y)) - matrix_up = [1, 0, -x, 0, 1, -y, 0, 0] - matrix_down = [1, 0, x, 0, 1, y, 0, 0] - - transformed = im.transform(size_up, self.transform, matrix_up, resample) - transformed = transformed.transform( - im.size, self.transform, matrix_down, resample - ) - assert_image_similar(transformed, im, epsilon * epsilon_scale) - - -class TestImageTransformPerspective(TestImageTransformAffine): - # Repeat all tests for AFFINE transformations with PERSPECTIVE - transform = Image.Transform.PERSPECTIVE +from __future__ import annotations + +import math +from typing import Callable + +import pytest + +from PIL import Image, ImageTransform + +from .helper import assert_image_equal, assert_image_similar, hopper + + +class TestImageTransform: + def test_sanity(self) -> None: + im = hopper() + + for transform in ( + ImageTransform.AffineTransform((1, 0, 0, 0, 1, 0)), + ImageTransform.PerspectiveTransform((1, 0, 0, 0, 1, 0, 0, 0)), + ImageTransform.ExtentTransform((0, 0) + im.size), + ImageTransform.QuadTransform( + (0, 0, 0, im.height, im.width, im.height, im.width, 0) + ), + ImageTransform.MeshTransform( + [ + ( + (0, 0) + im.size, + (0, 0, 0, im.height, im.width, im.height, im.width, 0), + ) + ] + ), + ): + assert_image_equal(im, im.transform(im.size, transform)) + + def test_info(self) -> None: + comment = b"File written by Adobe Photoshop\xa8 4.0" + + with Image.open("Tests/images/hopper.gif") as im: + assert im.info["comment"] == comment + + transform = ImageTransform.ExtentTransform((0, 0, 0, 0)) + new_im = im.transform((100, 100), transform) + assert new_im.info["comment"] == comment + + def test_palette(self) -> None: + with Image.open("Tests/images/hopper.gif") as im: + transformed = im.transform( + im.size, Image.Transform.AFFINE, [1, 0, 0, 0, 1, 0] + ) + assert im.palette.palette == transformed.palette.palette + + def test_extent(self) -> None: + im = hopper("RGB") + (w, h) = im.size + transformed = im.transform( + im.size, + Image.Transform.EXTENT, + (0, 0, w // 2, h // 2), # ul -> lr + Image.Resampling.BILINEAR, + ) + + scaled = im.resize((w * 2, h * 2), Image.Resampling.BILINEAR).crop((0, 0, w, h)) + + # undone -- precision? + assert_image_similar(transformed, scaled, 23) + + def test_quad(self) -> None: + # one simple quad transform, equivalent to scale & crop upper left quad + im = hopper("RGB") + (w, h) = im.size + transformed = im.transform( + im.size, + Image.Transform.QUAD, + (0, 0, 0, h // 2, w // 2, h // 2, w // 2, 0), # ul -> ccw around quad + Image.Resampling.BILINEAR, + ) + + scaled = im.transform( + (w, h), + Image.Transform.AFFINE, + (0.5, 0, 0, 0, 0.5, 0), + Image.Resampling.BILINEAR, + ) + + assert_image_equal(transformed, scaled) + + @pytest.mark.parametrize( + "mode, expected_pixel", + ( + ("RGB", (255, 0, 0)), + ("RGBA", (255, 0, 0, 255)), + ("LA", (76, 0)), + ), + ) + def test_fill(self, mode: str, expected_pixel: tuple[int, ...]) -> None: + im = hopper(mode) + (w, h) = im.size + transformed = im.transform( + im.size, + Image.Transform.EXTENT, + (0, 0, w * 2, h * 2), + Image.Resampling.BILINEAR, + fillcolor="red", + ) + assert transformed.getpixel((w - 1, h - 1)) == expected_pixel + + def test_mesh(self) -> None: + # this should be a checkerboard of halfsized hoppers in ul, lr + im = hopper("RGBA") + (w, h) = im.size + transformed = im.transform( + im.size, + Image.Transform.MESH, + ( + ( + (0, 0, w // 2, h // 2), # box + (0, 0, 0, h, w, h, w, 0), # ul -> ccw around quad + ), + ( + (w // 2, h // 2, w, h), # box + (0, 0, 0, h, w, h, w, 0), # ul -> ccw around quad + ), + ), + Image.Resampling.BILINEAR, + ) + + scaled = im.transform( + (w // 2, h // 2), + Image.Transform.AFFINE, + (2, 0, 0, 0, 2, 0), + Image.Resampling.BILINEAR, + ) + + checker = Image.new("RGBA", im.size) + checker.paste(scaled, (0, 0)) + checker.paste(scaled, (w // 2, h // 2)) + + assert_image_equal(transformed, checker) + + # now, check to see that the extra area is (0, 0, 0, 0) + blank = Image.new("RGBA", (w // 2, h // 2), (0, 0, 0, 0)) + + assert_image_equal(blank, transformed.crop((w // 2, 0, w, h // 2))) + assert_image_equal(blank, transformed.crop((0, h // 2, w // 2, h))) + + def _test_alpha_premult( + self, op: Callable[[Image.Image, tuple[int, int]], Image.Image] + ) -> None: + # create image with half white, half black, + # with the black half transparent. + # do op, + # there should be no darkness in the white section. + im = Image.new("RGBA", (10, 10), (0, 0, 0, 0)) + im2 = Image.new("RGBA", (5, 10), (255, 255, 255, 255)) + im.paste(im2, (0, 0)) + + im = op(im, (40, 10)) + im_background = Image.new("RGB", (40, 10), (255, 255, 255)) + im_background.paste(im, (0, 0), im) + + hist = im_background.histogram() + assert 40 * 10 == hist[-1] + + def test_alpha_premult_resize(self) -> None: + def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image: + return im.resize(sz, Image.Resampling.BILINEAR) + + self._test_alpha_premult(op) + + def test_alpha_premult_transform(self) -> None: + def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image: + (w, h) = im.size + return im.transform( + sz, Image.Transform.EXTENT, (0, 0, w, h), Image.Resampling.BILINEAR + ) + + self._test_alpha_premult(op) + + def _test_nearest( + self, op: Callable[[Image.Image, tuple[int, int]], Image.Image], mode: str + ) -> None: + # create white image with half transparent, + # do op, + # the image should remain white with half transparent + transparent, opaque = { + "RGBA": ((255, 255, 255, 0), (255, 255, 255, 255)), + "LA": ((255, 0), (255, 255)), + }[mode] + im = Image.new(mode, (10, 10), transparent) + im2 = Image.new(mode, (5, 10), opaque) + im.paste(im2, (0, 0)) + + im = op(im, (40, 10)) + + colors = sorted(im.getcolors()) + assert colors == sorted( + ( + (20 * 10, opaque), + (20 * 10, transparent), + ) + ) + + @pytest.mark.parametrize("mode", ("RGBA", "LA")) + def test_nearest_resize(self, mode: str) -> None: + def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image: + return im.resize(sz, Image.Resampling.NEAREST) + + self._test_nearest(op, mode) + + @pytest.mark.parametrize("mode", ("RGBA", "LA")) + def test_nearest_transform(self, mode: str) -> None: + def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image: + (w, h) = im.size + return im.transform( + sz, Image.Transform.EXTENT, (0, 0, w, h), Image.Resampling.NEAREST + ) + + self._test_nearest(op, mode) + + def test_blank_fill(self) -> None: + # attempting to hit + # https://github.com/python-pillow/Pillow/issues/254 reported + # + # issue is that transforms with transparent overflow area + # contained junk from previous images, especially on systems with + # constrained memory. So, attempt to fill up memory with a + # pattern, free it, and then run the mesh test again. Using a 1Mp + # image with 4 bands, for 4 megs of data allocated, x 64. OMM (64 + # bit 12.04 VM with 512 megs available, this fails with Pillow < + # a0eaf06cc5f62a6fb6de556989ac1014ff3348ea + # + # Running by default, but I'd totally understand not doing it in + # the future + + pattern: list[Image.Image] | None = [ + Image.new("RGBA", (1024, 1024), (a, a, a, a)) for a in range(1, 65) + ] + + # Yeah. Watch some JIT optimize this out. + pattern = None # noqa: F841 + + self.test_mesh() + + def test_missing_method_data(self) -> None: + with hopper() as im: + with pytest.raises(ValueError): + im.transform((100, 100), None) + + @pytest.mark.parametrize("resample", (Image.Resampling.BOX, "unknown")) + def test_unknown_resampling_filter(self, resample: Image.Resampling | str) -> None: + with hopper() as im: + (w, h) = im.size + with pytest.raises(ValueError): + im.transform((100, 100), Image.Transform.EXTENT, (0, 0, w, h), resample) + + +class TestImageTransformAffine: + transform = Image.Transform.AFFINE + + def _test_image(self) -> Image.Image: + im = hopper("RGB") + return im.crop((10, 20, im.width - 10, im.height - 20)) + + @pytest.mark.parametrize( + "deg, transpose", + ( + (0, None), + (90, Image.Transpose.ROTATE_90), + (180, Image.Transpose.ROTATE_180), + (270, Image.Transpose.ROTATE_270), + ), + ) + def test_rotate(self, deg: int, transpose: Image.Transpose | None) -> None: + im = self._test_image() + + angle = -math.radians(deg) + matrix = [ + round(math.cos(angle), 15), + round(math.sin(angle), 15), + 0.0, + round(-math.sin(angle), 15), + round(math.cos(angle), 15), + 0.0, + 0, + 0, + ] + matrix[2] = (1 - matrix[0] - matrix[1]) * im.width / 2 + matrix[5] = (1 - matrix[3] - matrix[4]) * im.height / 2 + + if transpose is not None: + transposed = im.transpose(transpose) + else: + transposed = im + + for resample in [ + Image.Resampling.NEAREST, + Image.Resampling.BILINEAR, + Image.Resampling.BICUBIC, + ]: + transformed = im.transform( + transposed.size, self.transform, matrix, resample + ) + assert_image_equal(transposed, transformed) + + @pytest.mark.parametrize( + "scale, epsilon_scale", + ( + (1.1, 6.9), + (1.5, 5.5), + (2.0, 5.5), + (2.3, 3.7), + (2.5, 3.7), + ), + ) + @pytest.mark.parametrize( + "resample,epsilon", + ( + (Image.Resampling.NEAREST, 0), + (Image.Resampling.BILINEAR, 2), + (Image.Resampling.BICUBIC, 1), + ), + ) + def test_resize( + self, + scale: float, + epsilon_scale: float, + resample: Image.Resampling, + epsilon: int, + ) -> None: + im = self._test_image() + + size_up = int(round(im.width * scale)), int(round(im.height * scale)) + matrix_up = [1 / scale, 0, 0, 0, 1 / scale, 0, 0, 0] + matrix_down = [scale, 0, 0, 0, scale, 0, 0, 0] + + transformed = im.transform(size_up, self.transform, matrix_up, resample) + transformed = transformed.transform( + im.size, self.transform, matrix_down, resample + ) + assert_image_similar(transformed, im, epsilon * epsilon_scale) + + @pytest.mark.parametrize( + "x, y, epsilon_scale", + ( + (0.1, 0, 3.7), + (0.6, 0, 9.1), + (50, 50, 0), + ), + ) + @pytest.mark.parametrize( + "resample, epsilon", + ( + (Image.Resampling.NEAREST, 0), + (Image.Resampling.BILINEAR, 1.5), + (Image.Resampling.BICUBIC, 1), + ), + ) + def test_translate( + self, + x: float, + y: float, + epsilon_scale: float, + resample: Image.Resampling, + epsilon: float, + ) -> None: + im = self._test_image() + + size_up = int(round(im.width + x)), int(round(im.height + y)) + matrix_up = [1, 0, -x, 0, 1, -y, 0, 0] + matrix_down = [1, 0, x, 0, 1, y, 0, 0] + + transformed = im.transform(size_up, self.transform, matrix_up, resample) + transformed = transformed.transform( + im.size, self.transform, matrix_down, resample + ) + assert_image_similar(transformed, im, epsilon * epsilon_scale) + + +class TestImageTransformPerspective(TestImageTransformAffine): + # Repeat all tests for AFFINE transformations with PERSPECTIVE + transform = Image.Transform.PERSPECTIVE diff --git a/Tests/test_image_transpose.py b/Tests/test_image_transpose.py index d384d8141..7e3b98942 100644 --- a/Tests/test_image_transpose.py +++ b/Tests/test_image_transpose.py @@ -1,149 +1,149 @@ -from __future__ import annotations - -import pytest - -from PIL import Image -from PIL.Image import Transpose - -from . import helper -from .helper import assert_image_equal - -HOPPER = { - mode: helper.hopper(mode).crop((0, 0, 121, 127)).copy() - for mode in ["L", "RGB", "I;16", "I;16L", "I;16B"] -} - - -@pytest.mark.parametrize("mode", HOPPER) -def test_flip_left_right(mode: str) -> None: - im = HOPPER[mode] - out = im.transpose(Transpose.FLIP_LEFT_RIGHT) - assert out.mode == mode - assert out.size == im.size - - x, y = im.size - assert im.getpixel((1, 1)) == out.getpixel((x - 2, 1)) - assert im.getpixel((x - 2, 1)) == out.getpixel((1, 1)) - assert im.getpixel((1, y - 2)) == out.getpixel((x - 2, y - 2)) - assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, y - 2)) - - -@pytest.mark.parametrize("mode", HOPPER) -def test_flip_top_bottom(mode: str) -> None: - im = HOPPER[mode] - out = im.transpose(Transpose.FLIP_TOP_BOTTOM) - assert out.mode == mode - assert out.size == im.size - - x, y = im.size - assert im.getpixel((1, 1)) == out.getpixel((1, y - 2)) - assert im.getpixel((x - 2, 1)) == out.getpixel((x - 2, y - 2)) - assert im.getpixel((1, y - 2)) == out.getpixel((1, 1)) - assert im.getpixel((x - 2, y - 2)) == out.getpixel((x - 2, 1)) - - -@pytest.mark.parametrize("mode", HOPPER) -def test_rotate_90(mode: str) -> None: - im = HOPPER[mode] - out = im.transpose(Transpose.ROTATE_90) - assert out.mode == mode - assert out.size == im.size[::-1] - - x, y = im.size - assert im.getpixel((1, 1)) == out.getpixel((1, x - 2)) - assert im.getpixel((x - 2, 1)) == out.getpixel((1, 1)) - assert im.getpixel((1, y - 2)) == out.getpixel((y - 2, x - 2)) - assert im.getpixel((x - 2, y - 2)) == out.getpixel((y - 2, 1)) - - -@pytest.mark.parametrize("mode", HOPPER) -def test_rotate_180(mode: str) -> None: - im = HOPPER[mode] - out = im.transpose(Transpose.ROTATE_180) - assert out.mode == mode - assert out.size == im.size - - x, y = im.size - assert im.getpixel((1, 1)) == out.getpixel((x - 2, y - 2)) - assert im.getpixel((x - 2, 1)) == out.getpixel((1, y - 2)) - assert im.getpixel((1, y - 2)) == out.getpixel((x - 2, 1)) - assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, 1)) - - -@pytest.mark.parametrize("mode", HOPPER) -def test_rotate_270(mode: str) -> None: - im = HOPPER[mode] - out = im.transpose(Transpose.ROTATE_270) - assert out.mode == mode - assert out.size == im.size[::-1] - - x, y = im.size - assert im.getpixel((1, 1)) == out.getpixel((y - 2, 1)) - assert im.getpixel((x - 2, 1)) == out.getpixel((y - 2, x - 2)) - assert im.getpixel((1, y - 2)) == out.getpixel((1, 1)) - assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, x - 2)) - - -@pytest.mark.parametrize("mode", HOPPER) -def test_transpose(mode: str) -> None: - im = HOPPER[mode] - out = im.transpose(Transpose.TRANSPOSE) - assert out.mode == mode - assert out.size == im.size[::-1] - - x, y = im.size - assert im.getpixel((1, 1)) == out.getpixel((1, 1)) - assert im.getpixel((x - 2, 1)) == out.getpixel((1, x - 2)) - assert im.getpixel((1, y - 2)) == out.getpixel((y - 2, 1)) - assert im.getpixel((x - 2, y - 2)) == out.getpixel((y - 2, x - 2)) - - -@pytest.mark.parametrize("mode", HOPPER) -def test_tranverse(mode: str) -> None: - im = HOPPER[mode] - out = im.transpose(Transpose.TRANSVERSE) - assert out.mode == mode - assert out.size == im.size[::-1] - - x, y = im.size - assert im.getpixel((1, 1)) == out.getpixel((y - 2, x - 2)) - assert im.getpixel((x - 2, 1)) == out.getpixel((y - 2, 1)) - assert im.getpixel((1, y - 2)) == out.getpixel((1, x - 2)) - assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, 1)) - - -@pytest.mark.parametrize("mode", HOPPER) -def test_roundtrip(mode: str) -> None: - im = HOPPER[mode] - - def transpose(first: Transpose, second: Transpose) -> Image.Image: - return im.transpose(first).transpose(second) - - assert_image_equal( - im, transpose(Transpose.FLIP_LEFT_RIGHT, Transpose.FLIP_LEFT_RIGHT) - ) - assert_image_equal( - im, transpose(Transpose.FLIP_TOP_BOTTOM, Transpose.FLIP_TOP_BOTTOM) - ) - assert_image_equal(im, transpose(Transpose.ROTATE_90, Transpose.ROTATE_270)) - assert_image_equal(im, transpose(Transpose.ROTATE_180, Transpose.ROTATE_180)) - assert_image_equal( - im.transpose(Transpose.TRANSPOSE), - transpose(Transpose.ROTATE_90, Transpose.FLIP_TOP_BOTTOM), - ) - assert_image_equal( - im.transpose(Transpose.TRANSPOSE), - transpose(Transpose.ROTATE_270, Transpose.FLIP_LEFT_RIGHT), - ) - assert_image_equal( - im.transpose(Transpose.TRANSVERSE), - transpose(Transpose.ROTATE_90, Transpose.FLIP_LEFT_RIGHT), - ) - assert_image_equal( - im.transpose(Transpose.TRANSVERSE), - transpose(Transpose.ROTATE_270, Transpose.FLIP_TOP_BOTTOM), - ) - assert_image_equal( - im.transpose(Transpose.TRANSVERSE), - transpose(Transpose.ROTATE_180, Transpose.TRANSPOSE), - ) +from __future__ import annotations + +import pytest + +from PIL import Image +from PIL.Image import Transpose + +from . import helper +from .helper import assert_image_equal + +HOPPER = { + mode: helper.hopper(mode).crop((0, 0, 121, 127)).copy() + for mode in ["L", "RGB", "I;16", "I;16L", "I;16B"] +} + + +@pytest.mark.parametrize("mode", HOPPER) +def test_flip_left_right(mode: str) -> None: + im = HOPPER[mode] + out = im.transpose(Transpose.FLIP_LEFT_RIGHT) + assert out.mode == mode + assert out.size == im.size + + x, y = im.size + assert im.getpixel((1, 1)) == out.getpixel((x - 2, 1)) + assert im.getpixel((x - 2, 1)) == out.getpixel((1, 1)) + assert im.getpixel((1, y - 2)) == out.getpixel((x - 2, y - 2)) + assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, y - 2)) + + +@pytest.mark.parametrize("mode", HOPPER) +def test_flip_top_bottom(mode: str) -> None: + im = HOPPER[mode] + out = im.transpose(Transpose.FLIP_TOP_BOTTOM) + assert out.mode == mode + assert out.size == im.size + + x, y = im.size + assert im.getpixel((1, 1)) == out.getpixel((1, y - 2)) + assert im.getpixel((x - 2, 1)) == out.getpixel((x - 2, y - 2)) + assert im.getpixel((1, y - 2)) == out.getpixel((1, 1)) + assert im.getpixel((x - 2, y - 2)) == out.getpixel((x - 2, 1)) + + +@pytest.mark.parametrize("mode", HOPPER) +def test_rotate_90(mode: str) -> None: + im = HOPPER[mode] + out = im.transpose(Transpose.ROTATE_90) + assert out.mode == mode + assert out.size == im.size[::-1] + + x, y = im.size + assert im.getpixel((1, 1)) == out.getpixel((1, x - 2)) + assert im.getpixel((x - 2, 1)) == out.getpixel((1, 1)) + assert im.getpixel((1, y - 2)) == out.getpixel((y - 2, x - 2)) + assert im.getpixel((x - 2, y - 2)) == out.getpixel((y - 2, 1)) + + +@pytest.mark.parametrize("mode", HOPPER) +def test_rotate_180(mode: str) -> None: + im = HOPPER[mode] + out = im.transpose(Transpose.ROTATE_180) + assert out.mode == mode + assert out.size == im.size + + x, y = im.size + assert im.getpixel((1, 1)) == out.getpixel((x - 2, y - 2)) + assert im.getpixel((x - 2, 1)) == out.getpixel((1, y - 2)) + assert im.getpixel((1, y - 2)) == out.getpixel((x - 2, 1)) + assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, 1)) + + +@pytest.mark.parametrize("mode", HOPPER) +def test_rotate_270(mode: str) -> None: + im = HOPPER[mode] + out = im.transpose(Transpose.ROTATE_270) + assert out.mode == mode + assert out.size == im.size[::-1] + + x, y = im.size + assert im.getpixel((1, 1)) == out.getpixel((y - 2, 1)) + assert im.getpixel((x - 2, 1)) == out.getpixel((y - 2, x - 2)) + assert im.getpixel((1, y - 2)) == out.getpixel((1, 1)) + assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, x - 2)) + + +@pytest.mark.parametrize("mode", HOPPER) +def test_transpose(mode: str) -> None: + im = HOPPER[mode] + out = im.transpose(Transpose.TRANSPOSE) + assert out.mode == mode + assert out.size == im.size[::-1] + + x, y = im.size + assert im.getpixel((1, 1)) == out.getpixel((1, 1)) + assert im.getpixel((x - 2, 1)) == out.getpixel((1, x - 2)) + assert im.getpixel((1, y - 2)) == out.getpixel((y - 2, 1)) + assert im.getpixel((x - 2, y - 2)) == out.getpixel((y - 2, x - 2)) + + +@pytest.mark.parametrize("mode", HOPPER) +def test_tranverse(mode: str) -> None: + im = HOPPER[mode] + out = im.transpose(Transpose.TRANSVERSE) + assert out.mode == mode + assert out.size == im.size[::-1] + + x, y = im.size + assert im.getpixel((1, 1)) == out.getpixel((y - 2, x - 2)) + assert im.getpixel((x - 2, 1)) == out.getpixel((y - 2, 1)) + assert im.getpixel((1, y - 2)) == out.getpixel((1, x - 2)) + assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, 1)) + + +@pytest.mark.parametrize("mode", HOPPER) +def test_roundtrip(mode: str) -> None: + im = HOPPER[mode] + + def transpose(first: Transpose, second: Transpose) -> Image.Image: + return im.transpose(first).transpose(second) + + assert_image_equal( + im, transpose(Transpose.FLIP_LEFT_RIGHT, Transpose.FLIP_LEFT_RIGHT) + ) + assert_image_equal( + im, transpose(Transpose.FLIP_TOP_BOTTOM, Transpose.FLIP_TOP_BOTTOM) + ) + assert_image_equal(im, transpose(Transpose.ROTATE_90, Transpose.ROTATE_270)) + assert_image_equal(im, transpose(Transpose.ROTATE_180, Transpose.ROTATE_180)) + assert_image_equal( + im.transpose(Transpose.TRANSPOSE), + transpose(Transpose.ROTATE_90, Transpose.FLIP_TOP_BOTTOM), + ) + assert_image_equal( + im.transpose(Transpose.TRANSPOSE), + transpose(Transpose.ROTATE_270, Transpose.FLIP_LEFT_RIGHT), + ) + assert_image_equal( + im.transpose(Transpose.TRANSVERSE), + transpose(Transpose.ROTATE_90, Transpose.FLIP_LEFT_RIGHT), + ) + assert_image_equal( + im.transpose(Transpose.TRANSVERSE), + transpose(Transpose.ROTATE_270, Transpose.FLIP_TOP_BOTTOM), + ) + assert_image_equal( + im.transpose(Transpose.TRANSVERSE), + transpose(Transpose.ROTATE_180, Transpose.TRANSPOSE), + ) diff --git a/Tests/test_imagechops.py b/Tests/test_imagechops.py index 7e2290c15..ad6f1fce5 100644 --- a/Tests/test_imagechops.py +++ b/Tests/test_imagechops.py @@ -1,413 +1,413 @@ -from __future__ import annotations - -from typing import Callable - -from PIL import Image, ImageChops - -from .helper import assert_image_equal, hopper - -BLACK = (0, 0, 0) -BROWN = (127, 64, 0) -CYAN = (0, 255, 255) -DARK_GREEN = (0, 128, 0) -GREEN = (0, 255, 0) -ORANGE = (255, 128, 0) -WHITE = (255, 255, 255) - -GRAY = 128 - - -def test_sanity() -> None: - im = hopper("L") - - ImageChops.constant(im, 128) - ImageChops.duplicate(im) - ImageChops.invert(im) - ImageChops.lighter(im, im) - ImageChops.darker(im, im) - ImageChops.difference(im, im) - ImageChops.multiply(im, im) - ImageChops.screen(im, im) - - ImageChops.add(im, im) - ImageChops.add(im, im, 2.0) - ImageChops.add(im, im, 2.0, 128) - ImageChops.subtract(im, im) - ImageChops.subtract(im, im, 2.0) - ImageChops.subtract(im, im, 2.0, 128) - - ImageChops.add_modulo(im, im) - ImageChops.subtract_modulo(im, im) - - ImageChops.blend(im, im, 0.5) - ImageChops.composite(im, im, im) - - ImageChops.soft_light(im, im) - ImageChops.hard_light(im, im) - ImageChops.overlay(im, im) - - ImageChops.offset(im, 10) - ImageChops.offset(im, 10, 20) - - -def test_add() -> None: - # Arrange - with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: - with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: - # Act - new = ImageChops.add(im1, im2) - - # Assert - assert new.getbbox() == (25, 25, 76, 76) - assert new.getpixel((50, 50)) == ORANGE - - -def test_add_scale_offset() -> None: - # Arrange - with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: - with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: - # Act - new = ImageChops.add(im1, im2, scale=2.5, offset=100) - - # Assert - assert new.getbbox() == (0, 0, 100, 100) - assert new.getpixel((50, 50)) == (202, 151, 100) - - -def test_add_clip() -> None: - # Arrange - im = hopper() - - # Act - new = ImageChops.add(im, im) - - # Assert - assert new.getpixel((50, 50)) == (255, 255, 254) - - -def test_add_modulo() -> None: - # Arrange - with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: - with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: - # Act - new = ImageChops.add_modulo(im1, im2) - - # Assert - assert new.getbbox() == (25, 25, 76, 76) - assert new.getpixel((50, 50)) == ORANGE - - -def test_add_modulo_no_clip() -> None: - # Arrange - im = hopper() - - # Act - new = ImageChops.add_modulo(im, im) - - # Assert - assert new.getpixel((50, 50)) == (224, 76, 254) - - -def test_blend() -> None: - # Arrange - with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: - with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: - # Act - new = ImageChops.blend(im1, im2, 0.5) - - # Assert - assert new.getbbox() == (25, 25, 76, 76) - assert new.getpixel((50, 50)) == BROWN - - -def test_constant() -> None: - # Arrange - im = Image.new("RGB", (20, 10)) - - # Act - new = ImageChops.constant(im, GRAY) - - # Assert - assert new.size == im.size - assert new.getpixel((0, 0)) == GRAY - assert new.getpixel((19, 9)) == GRAY - - -def test_darker_image() -> None: - # Arrange - with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: - with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: - # Act - new = ImageChops.darker(im1, im2) - - # Assert - assert_image_equal(new, im2) - - -def test_darker_pixel() -> None: - # Arrange - im1 = hopper() - with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: - # Act - new = ImageChops.darker(im1, im2) - - # Assert - assert new.getpixel((50, 50)) == (240, 166, 0) - - -def test_difference() -> None: - # Arrange - with Image.open("Tests/images/imagedraw_arc_end_le_start.png") as im1: - with Image.open("Tests/images/imagedraw_arc_no_loops.png") as im2: - # Act - new = ImageChops.difference(im1, im2) - - # Assert - assert new.getbbox() == (25, 25, 76, 76) - - -def test_difference_pixel() -> None: - # Arrange - im1 = hopper() - with Image.open("Tests/images/imagedraw_polygon_kite_RGB.png") as im2: - # Act - new = ImageChops.difference(im1, im2) - - # Assert - assert new.getpixel((50, 50)) == (240, 166, 128) - - -def test_duplicate() -> None: - # Arrange - im = hopper() - - # Act - new = ImageChops.duplicate(im) - - # Assert - assert_image_equal(new, im) - - -def test_invert() -> None: - # Arrange - with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im: - # Act - new = ImageChops.invert(im) - - # Assert - assert new.getbbox() == (0, 0, 100, 100) - assert new.getpixel((0, 0)) == WHITE - assert new.getpixel((50, 50)) == CYAN - - -def test_lighter_image() -> None: - # Arrange - with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: - with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: - # Act - new = ImageChops.lighter(im1, im2) - - # Assert - assert_image_equal(new, im1) - - -def test_lighter_pixel() -> None: - # Arrange - im1 = hopper() - with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: - # Act - new = ImageChops.lighter(im1, im2) - - # Assert - assert new.getpixel((50, 50)) == (255, 255, 127) - - -def test_multiply_black() -> None: - """If you multiply an image with a solid black image, - the result is black.""" - # Arrange - im1 = hopper() - black = Image.new("RGB", im1.size, "black") - - # Act - new = ImageChops.multiply(im1, black) - - # Assert - assert_image_equal(new, black) - - -def test_multiply_green() -> None: - # Arrange - with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im: - green = Image.new("RGB", im.size, "green") - - # Act - new = ImageChops.multiply(im, green) - - # Assert - assert new.getbbox() == (25, 25, 76, 76) - assert new.getpixel((25, 25)) == DARK_GREEN - assert new.getpixel((50, 50)) == BLACK - - -def test_multiply_white() -> None: - """If you multiply with a solid white image, the image is unaffected.""" - # Arrange - im1 = hopper() - white = Image.new("RGB", im1.size, "white") - - # Act - new = ImageChops.multiply(im1, white) - - # Assert - assert_image_equal(new, im1) - - -def test_offset() -> None: - # Arrange - xoffset = 45 - yoffset = 20 - with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im: - # Act - new = ImageChops.offset(im, xoffset, yoffset) - - # Assert - assert new.getbbox() == (0, 45, 100, 96) - assert new.getpixel((50, 50)) == BLACK - assert new.getpixel((50 + xoffset, 50 + yoffset)) == DARK_GREEN - - # Test no yoffset - assert ImageChops.offset(im, xoffset) == ImageChops.offset(im, xoffset, xoffset) - - -def test_screen() -> None: - # Arrange - with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: - with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: - # Act - new = ImageChops.screen(im1, im2) - - # Assert - assert new.getbbox() == (25, 25, 76, 76) - assert new.getpixel((50, 50)) == ORANGE - - -def test_subtract() -> None: - # Arrange - with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: - with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: - # Act - new = ImageChops.subtract(im1, im2) - - # Assert - assert new.getbbox() == (25, 50, 76, 76) - assert new.getpixel((50, 51)) == GREEN - assert new.getpixel((50, 52)) == BLACK - - -def test_subtract_scale_offset() -> None: - # Arrange - with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: - with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: - # Act - new = ImageChops.subtract(im1, im2, scale=2.5, offset=100) - - # Assert - assert new.getbbox() == (0, 0, 100, 100) - assert new.getpixel((50, 50)) == (100, 202, 100) - - -def test_subtract_clip() -> None: - # Arrange - im1 = hopper() - with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: - # Act - new = ImageChops.subtract(im1, im2) - - # Assert - assert new.getpixel((50, 50)) == (0, 0, 127) - - -def test_subtract_modulo() -> None: - # Arrange - with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: - with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: - # Act - new = ImageChops.subtract_modulo(im1, im2) - - # Assert - assert new.getbbox() == (25, 50, 76, 76) - assert new.getpixel((50, 51)) == GREEN - assert new.getpixel((50, 52)) == BLACK - - -def test_subtract_modulo_no_clip() -> None: - # Arrange - im1 = hopper() - with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: - # Act - new = ImageChops.subtract_modulo(im1, im2) - - # Assert - assert new.getpixel((50, 50)) == (241, 167, 127) - - -def test_soft_light() -> None: - # Arrange - with Image.open("Tests/images/hopper.png") as im1: - with Image.open("Tests/images/hopper-XYZ.png") as im2: - # Act - new = ImageChops.soft_light(im1, im2) - - # Assert - assert new.getpixel((64, 64)) == (163, 54, 32) - assert new.getpixel((15, 100)) == (1, 1, 3) - - -def test_hard_light() -> None: - # Arrange - with Image.open("Tests/images/hopper.png") as im1: - with Image.open("Tests/images/hopper-XYZ.png") as im2: - # Act - new = ImageChops.hard_light(im1, im2) - - # Assert - assert new.getpixel((64, 64)) == (144, 50, 27) - assert new.getpixel((15, 100)) == (1, 1, 2) - - -def test_overlay() -> None: - # Arrange - with Image.open("Tests/images/hopper.png") as im1: - with Image.open("Tests/images/hopper-XYZ.png") as im2: - # Act - new = ImageChops.overlay(im1, im2) - - # Assert - assert new.getpixel((64, 64)) == (159, 50, 27) - assert new.getpixel((15, 100)) == (1, 1, 2) - - -def test_logical() -> None: - def table( - op: Callable[[Image.Image, Image.Image], Image.Image], a: int, b: int - ) -> tuple[int, int, int, int]: - out = [] - for x in (a, b): - imx = Image.new("1", (1, 1), x) - for y in (a, b): - imy = Image.new("1", (1, 1), y) - out.append(op(imx, imy).getpixel((0, 0))) - return tuple(out) - - assert table(ImageChops.logical_and, 0, 1) == (0, 0, 0, 255) - assert table(ImageChops.logical_or, 0, 1) == (0, 255, 255, 255) - assert table(ImageChops.logical_xor, 0, 1) == (0, 255, 255, 0) - - assert table(ImageChops.logical_and, 0, 128) == (0, 0, 0, 255) - assert table(ImageChops.logical_or, 0, 128) == (0, 255, 255, 255) - assert table(ImageChops.logical_xor, 0, 128) == (0, 255, 255, 0) - - assert table(ImageChops.logical_and, 0, 255) == (0, 0, 0, 255) - assert table(ImageChops.logical_or, 0, 255) == (0, 255, 255, 255) - assert table(ImageChops.logical_xor, 0, 255) == (0, 255, 255, 0) +from __future__ import annotations + +from typing import Callable + +from PIL import Image, ImageChops + +from .helper import assert_image_equal, hopper + +BLACK = (0, 0, 0) +BROWN = (127, 64, 0) +CYAN = (0, 255, 255) +DARK_GREEN = (0, 128, 0) +GREEN = (0, 255, 0) +ORANGE = (255, 128, 0) +WHITE = (255, 255, 255) + +GRAY = 128 + + +def test_sanity() -> None: + im = hopper("L") + + ImageChops.constant(im, 128) + ImageChops.duplicate(im) + ImageChops.invert(im) + ImageChops.lighter(im, im) + ImageChops.darker(im, im) + ImageChops.difference(im, im) + ImageChops.multiply(im, im) + ImageChops.screen(im, im) + + ImageChops.add(im, im) + ImageChops.add(im, im, 2.0) + ImageChops.add(im, im, 2.0, 128) + ImageChops.subtract(im, im) + ImageChops.subtract(im, im, 2.0) + ImageChops.subtract(im, im, 2.0, 128) + + ImageChops.add_modulo(im, im) + ImageChops.subtract_modulo(im, im) + + ImageChops.blend(im, im, 0.5) + ImageChops.composite(im, im, im) + + ImageChops.soft_light(im, im) + ImageChops.hard_light(im, im) + ImageChops.overlay(im, im) + + ImageChops.offset(im, 10) + ImageChops.offset(im, 10, 20) + + +def test_add() -> None: + # Arrange + with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: + with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: + # Act + new = ImageChops.add(im1, im2) + + # Assert + assert new.getbbox() == (25, 25, 76, 76) + assert new.getpixel((50, 50)) == ORANGE + + +def test_add_scale_offset() -> None: + # Arrange + with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: + with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: + # Act + new = ImageChops.add(im1, im2, scale=2.5, offset=100) + + # Assert + assert new.getbbox() == (0, 0, 100, 100) + assert new.getpixel((50, 50)) == (202, 151, 100) + + +def test_add_clip() -> None: + # Arrange + im = hopper() + + # Act + new = ImageChops.add(im, im) + + # Assert + assert new.getpixel((50, 50)) == (255, 255, 254) + + +def test_add_modulo() -> None: + # Arrange + with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: + with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: + # Act + new = ImageChops.add_modulo(im1, im2) + + # Assert + assert new.getbbox() == (25, 25, 76, 76) + assert new.getpixel((50, 50)) == ORANGE + + +def test_add_modulo_no_clip() -> None: + # Arrange + im = hopper() + + # Act + new = ImageChops.add_modulo(im, im) + + # Assert + assert new.getpixel((50, 50)) == (224, 76, 254) + + +def test_blend() -> None: + # Arrange + with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: + with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: + # Act + new = ImageChops.blend(im1, im2, 0.5) + + # Assert + assert new.getbbox() == (25, 25, 76, 76) + assert new.getpixel((50, 50)) == BROWN + + +def test_constant() -> None: + # Arrange + im = Image.new("RGB", (20, 10)) + + # Act + new = ImageChops.constant(im, GRAY) + + # Assert + assert new.size == im.size + assert new.getpixel((0, 0)) == GRAY + assert new.getpixel((19, 9)) == GRAY + + +def test_darker_image() -> None: + # Arrange + with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: + with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: + # Act + new = ImageChops.darker(im1, im2) + + # Assert + assert_image_equal(new, im2) + + +def test_darker_pixel() -> None: + # Arrange + im1 = hopper() + with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: + # Act + new = ImageChops.darker(im1, im2) + + # Assert + assert new.getpixel((50, 50)) == (240, 166, 0) + + +def test_difference() -> None: + # Arrange + with Image.open("Tests/images/imagedraw_arc_end_le_start.png") as im1: + with Image.open("Tests/images/imagedraw_arc_no_loops.png") as im2: + # Act + new = ImageChops.difference(im1, im2) + + # Assert + assert new.getbbox() == (25, 25, 76, 76) + + +def test_difference_pixel() -> None: + # Arrange + im1 = hopper() + with Image.open("Tests/images/imagedraw_polygon_kite_RGB.png") as im2: + # Act + new = ImageChops.difference(im1, im2) + + # Assert + assert new.getpixel((50, 50)) == (240, 166, 128) + + +def test_duplicate() -> None: + # Arrange + im = hopper() + + # Act + new = ImageChops.duplicate(im) + + # Assert + assert_image_equal(new, im) + + +def test_invert() -> None: + # Arrange + with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im: + # Act + new = ImageChops.invert(im) + + # Assert + assert new.getbbox() == (0, 0, 100, 100) + assert new.getpixel((0, 0)) == WHITE + assert new.getpixel((50, 50)) == CYAN + + +def test_lighter_image() -> None: + # Arrange + with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: + with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: + # Act + new = ImageChops.lighter(im1, im2) + + # Assert + assert_image_equal(new, im1) + + +def test_lighter_pixel() -> None: + # Arrange + im1 = hopper() + with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: + # Act + new = ImageChops.lighter(im1, im2) + + # Assert + assert new.getpixel((50, 50)) == (255, 255, 127) + + +def test_multiply_black() -> None: + """If you multiply an image with a solid black image, + the result is black.""" + # Arrange + im1 = hopper() + black = Image.new("RGB", im1.size, "black") + + # Act + new = ImageChops.multiply(im1, black) + + # Assert + assert_image_equal(new, black) + + +def test_multiply_green() -> None: + # Arrange + with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im: + green = Image.new("RGB", im.size, "green") + + # Act + new = ImageChops.multiply(im, green) + + # Assert + assert new.getbbox() == (25, 25, 76, 76) + assert new.getpixel((25, 25)) == DARK_GREEN + assert new.getpixel((50, 50)) == BLACK + + +def test_multiply_white() -> None: + """If you multiply with a solid white image, the image is unaffected.""" + # Arrange + im1 = hopper() + white = Image.new("RGB", im1.size, "white") + + # Act + new = ImageChops.multiply(im1, white) + + # Assert + assert_image_equal(new, im1) + + +def test_offset() -> None: + # Arrange + xoffset = 45 + yoffset = 20 + with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im: + # Act + new = ImageChops.offset(im, xoffset, yoffset) + + # Assert + assert new.getbbox() == (0, 45, 100, 96) + assert new.getpixel((50, 50)) == BLACK + assert new.getpixel((50 + xoffset, 50 + yoffset)) == DARK_GREEN + + # Test no yoffset + assert ImageChops.offset(im, xoffset) == ImageChops.offset(im, xoffset, xoffset) + + +def test_screen() -> None: + # Arrange + with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: + with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: + # Act + new = ImageChops.screen(im1, im2) + + # Assert + assert new.getbbox() == (25, 25, 76, 76) + assert new.getpixel((50, 50)) == ORANGE + + +def test_subtract() -> None: + # Arrange + with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: + with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: + # Act + new = ImageChops.subtract(im1, im2) + + # Assert + assert new.getbbox() == (25, 50, 76, 76) + assert new.getpixel((50, 51)) == GREEN + assert new.getpixel((50, 52)) == BLACK + + +def test_subtract_scale_offset() -> None: + # Arrange + with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: + with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: + # Act + new = ImageChops.subtract(im1, im2, scale=2.5, offset=100) + + # Assert + assert new.getbbox() == (0, 0, 100, 100) + assert new.getpixel((50, 50)) == (100, 202, 100) + + +def test_subtract_clip() -> None: + # Arrange + im1 = hopper() + with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: + # Act + new = ImageChops.subtract(im1, im2) + + # Assert + assert new.getpixel((50, 50)) == (0, 0, 127) + + +def test_subtract_modulo() -> None: + # Arrange + with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: + with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: + # Act + new = ImageChops.subtract_modulo(im1, im2) + + # Assert + assert new.getbbox() == (25, 50, 76, 76) + assert new.getpixel((50, 51)) == GREEN + assert new.getpixel((50, 52)) == BLACK + + +def test_subtract_modulo_no_clip() -> None: + # Arrange + im1 = hopper() + with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: + # Act + new = ImageChops.subtract_modulo(im1, im2) + + # Assert + assert new.getpixel((50, 50)) == (241, 167, 127) + + +def test_soft_light() -> None: + # Arrange + with Image.open("Tests/images/hopper.png") as im1: + with Image.open("Tests/images/hopper-XYZ.png") as im2: + # Act + new = ImageChops.soft_light(im1, im2) + + # Assert + assert new.getpixel((64, 64)) == (163, 54, 32) + assert new.getpixel((15, 100)) == (1, 1, 3) + + +def test_hard_light() -> None: + # Arrange + with Image.open("Tests/images/hopper.png") as im1: + with Image.open("Tests/images/hopper-XYZ.png") as im2: + # Act + new = ImageChops.hard_light(im1, im2) + + # Assert + assert new.getpixel((64, 64)) == (144, 50, 27) + assert new.getpixel((15, 100)) == (1, 1, 2) + + +def test_overlay() -> None: + # Arrange + with Image.open("Tests/images/hopper.png") as im1: + with Image.open("Tests/images/hopper-XYZ.png") as im2: + # Act + new = ImageChops.overlay(im1, im2) + + # Assert + assert new.getpixel((64, 64)) == (159, 50, 27) + assert new.getpixel((15, 100)) == (1, 1, 2) + + +def test_logical() -> None: + def table( + op: Callable[[Image.Image, Image.Image], Image.Image], a: int, b: int + ) -> tuple[int, int, int, int]: + out = [] + for x in (a, b): + imx = Image.new("1", (1, 1), x) + for y in (a, b): + imy = Image.new("1", (1, 1), y) + out.append(op(imx, imy).getpixel((0, 0))) + return tuple(out) + + assert table(ImageChops.logical_and, 0, 1) == (0, 0, 0, 255) + assert table(ImageChops.logical_or, 0, 1) == (0, 255, 255, 255) + assert table(ImageChops.logical_xor, 0, 1) == (0, 255, 255, 0) + + assert table(ImageChops.logical_and, 0, 128) == (0, 0, 0, 255) + assert table(ImageChops.logical_or, 0, 128) == (0, 255, 255, 255) + assert table(ImageChops.logical_xor, 0, 128) == (0, 255, 255, 0) + + assert table(ImageChops.logical_and, 0, 255) == (0, 0, 0, 255) + assert table(ImageChops.logical_or, 0, 255) == (0, 255, 255, 255) + assert table(ImageChops.logical_xor, 0, 255) == (0, 255, 255, 0) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 55f72c3b9..968667892 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -1,701 +1,701 @@ -from __future__ import annotations - -import datetime -import os -import re -import shutil -import sys -from io import BytesIO -from pathlib import Path -from typing import Any, Literal, cast - -import pytest - -from PIL import Image, ImageMode, ImageWin, features - -from .helper import ( - assert_image, - assert_image_equal, - assert_image_similar, - assert_image_similar_tofile, - hopper, - is_pypy, -) - -try: - from PIL import ImageCms - from PIL.ImageCms import ImageCmsProfile - - ImageCms.core.profile_open -except ImportError: - # Skipped via setup_module() - pass - - -SRGB = "Tests/icc/sRGB_IEC61966-2-1_black_scaled.icc" -HAVE_PROFILE = os.path.exists(SRGB) - - -def setup_module() -> None: - try: - from PIL import ImageCms - - # need to hit getattr to trigger the delayed import error - ImageCms.core.profile_open - except ImportError as v: - pytest.skip(str(v)) - - -def skip_missing() -> None: - if not HAVE_PROFILE: - pytest.skip("SRGB profile not available") - - -def test_sanity() -> None: - # basic smoke test. - # this mostly follows the cms_test outline. - with pytest.warns(DeprecationWarning): - v = ImageCms.versions() # should return four strings - assert v[0] == "1.0.0 pil" - assert list(map(type, v)) == [str, str, str, str] - - # internal version number - version = features.version_module("littlecms2") - assert version is not None - assert re.search(r"\d+\.\d+(\.\d+)?$", version) - - skip_missing() - i = ImageCms.profileToProfile(hopper(), SRGB, SRGB) - assert i is not None - assert_image(i, "RGB", (128, 128)) - - i = hopper() - ImageCms.profileToProfile(i, SRGB, SRGB, inPlace=True) - assert_image(i, "RGB", (128, 128)) - - t = ImageCms.buildTransform(SRGB, SRGB, "RGB", "RGB") - i = ImageCms.applyTransform(hopper(), t) - assert i is not None - assert_image(i, "RGB", (128, 128)) - - with hopper() as i: - t = ImageCms.buildTransform(SRGB, SRGB, "RGB", "RGB") - ImageCms.applyTransform(hopper(), t, inPlace=True) - assert i is not None - assert_image(i, "RGB", (128, 128)) - - p = ImageCms.createProfile("sRGB") - o = ImageCms.getOpenProfile(SRGB) - t = ImageCms.buildTransformFromOpenProfiles(p, o, "RGB", "RGB") - i = ImageCms.applyTransform(hopper(), t) - assert i is not None - assert_image(i, "RGB", (128, 128)) - - t = ImageCms.buildProofTransform(SRGB, SRGB, SRGB, "RGB", "RGB") - assert t.inputMode == "RGB" - assert t.outputMode == "RGB" - i = ImageCms.applyTransform(hopper(), t) - assert i is not None - assert_image(i, "RGB", (128, 128)) - - # test PointTransform convenience API - hopper().point(t) - - -def test_flags() -> None: - assert ImageCms.Flags.NONE == 0 - assert ImageCms.Flags.GRIDPOINTS(0) == ImageCms.Flags.NONE - assert ImageCms.Flags.GRIDPOINTS(256) == ImageCms.Flags.NONE - - assert ImageCms.Flags.GRIDPOINTS(255) == (255 << 16) - assert ImageCms.Flags.GRIDPOINTS(-1) == ImageCms.Flags.GRIDPOINTS(255) - assert ImageCms.Flags.GRIDPOINTS(511) == ImageCms.Flags.GRIDPOINTS(255) - - -def test_name() -> None: - skip_missing() - # get profile information for file - assert ( - ImageCms.getProfileName(SRGB).strip() - == "IEC 61966-2-1 Default RGB Colour Space - sRGB" - ) - - -def test_info() -> None: - skip_missing() - assert ImageCms.getProfileInfo(SRGB).splitlines() == [ - "sRGB IEC61966-2-1 black scaled", - "", - "Copyright International Color Consortium, 2009", - "", - ] - - -def test_copyright() -> None: - skip_missing() - assert ( - ImageCms.getProfileCopyright(SRGB).strip() - == "Copyright International Color Consortium, 2009" - ) - - -def test_manufacturer() -> None: - skip_missing() - assert ImageCms.getProfileManufacturer(SRGB).strip() == "" - - -def test_model() -> None: - skip_missing() - assert ( - ImageCms.getProfileModel(SRGB).strip() - == "IEC 61966-2-1 Default RGB Colour Space - sRGB" - ) - - -def test_description() -> None: - skip_missing() - assert ( - ImageCms.getProfileDescription(SRGB).strip() == "sRGB IEC61966-2-1 black scaled" - ) - - -def test_intent() -> None: - skip_missing() - assert ImageCms.getDefaultIntent(SRGB) == 0 - support = ImageCms.isIntentSupported( - SRGB, ImageCms.Intent.ABSOLUTE_COLORIMETRIC, ImageCms.Direction.INPUT - ) - assert support == 1 - - -def test_profile_object() -> None: - # same, using profile object - p = ImageCms.createProfile("sRGB") - # assert ImageCms.getProfileName(p).strip() == "sRGB built-in - (lcms internal)" - # assert ImageCms.getProfileInfo(p).splitlines() == - # ["sRGB built-in", "", "WhitePoint : D65 (daylight)", "", ""] - assert ImageCms.getDefaultIntent(p) == 0 - support = ImageCms.isIntentSupported( - p, ImageCms.Intent.ABSOLUTE_COLORIMETRIC, ImageCms.Direction.INPUT - ) - assert support == 1 - - -def test_extensions() -> None: - # extensions - - with Image.open("Tests/images/rgb.jpg") as i: - p = ImageCms.getOpenProfile(BytesIO(i.info["icc_profile"])) - assert ( - ImageCms.getProfileName(p).strip() - == "IEC 61966-2.1 Default RGB colour space - sRGB" - ) - - -def test_exceptions() -> None: - # Test mode mismatch - psRGB = ImageCms.createProfile("sRGB") - pLab = ImageCms.createProfile("LAB") - t = ImageCms.buildTransform(pLab, psRGB, "LAB", "RGB") - with pytest.raises(ValueError, match="mode mismatch"): - t.apply_in_place(hopper("RGBA")) - - # the procedural pyCMS API uses PyCMSError for all sorts of errors - with hopper() as im: - with pytest.raises(ImageCms.PyCMSError, match="cannot open profile file"): - ImageCms.profileToProfile(im, "foo", "bar") - - with pytest.raises(ImageCms.PyCMSError, match="cannot open profile file"): - ImageCms.buildTransform("foo", "bar", "RGB", "RGB") - - with pytest.raises(ImageCms.PyCMSError, match="Invalid type for Profile"): - ImageCms.getProfileName(None) # type: ignore[arg-type] - skip_missing() - - # Python <= 3.9: "an integer is required (got type NoneType)" - # Python > 3.9: "'NoneType' object cannot be interpreted as an integer" - with pytest.raises(ImageCms.PyCMSError, match="integer"): - ImageCms.isIntentSupported(SRGB, None, None) # type: ignore[arg-type] - - -def test_display_profile() -> None: - # try fetching the profile for the current display device - ImageCms.get_display_profile() - - if sys.platform == "win32": - ImageCms.get_display_profile(ImageWin.HDC(0)) - ImageCms.get_display_profile(ImageWin.HWND(0)) - - -def test_lab_color_profile() -> None: - ImageCms.createProfile("LAB", 5000) - ImageCms.createProfile("LAB", 6500) - - -def test_unsupported_color_space() -> None: - with pytest.raises( - ImageCms.PyCMSError, - match=re.escape( - "Color space not supported for on-the-fly profile creation (unsupported)" - ), - ): - ImageCms.createProfile("unsupported") # type: ignore[arg-type] - - -def test_invalid_color_temperature() -> None: - with pytest.raises( - ImageCms.PyCMSError, - match='Color temperature must be numeric, "invalid" not valid', - ): - ImageCms.createProfile("LAB", "invalid") # type: ignore[arg-type] - - -@pytest.mark.parametrize("flag", ("my string", -1)) -def test_invalid_flag(flag: str | int) -> None: - with hopper() as im: - with pytest.raises( - ImageCms.PyCMSError, match="flags must be an integer between 0 and " - ): - ImageCms.profileToProfile(im, "foo", "bar", flags=flag) # type: ignore[arg-type] - - -def test_simple_lab() -> None: - i = Image.new("RGB", (10, 10), (128, 128, 128)) - - psRGB = ImageCms.createProfile("sRGB") - pLab = ImageCms.createProfile("LAB") - t = ImageCms.buildTransform(psRGB, pLab, "RGB", "LAB") - - i_lab = ImageCms.applyTransform(i, t) - assert i_lab is not None - assert i_lab.mode == "LAB" - - k = i_lab.getpixel((0, 0)) - # not a linear luminance map. so L != 128: - assert k == (137, 128, 128) - - l_data = i_lab.getdata(0) - a_data = i_lab.getdata(1) - b_data = i_lab.getdata(2) - - assert list(l_data) == [137] * 100 - assert list(a_data) == [128] * 100 - assert list(b_data) == [128] * 100 - - -def test_lab_color() -> None: - psRGB = ImageCms.createProfile("sRGB") - pLab = ImageCms.createProfile("LAB") - t = ImageCms.buildTransform(psRGB, pLab, "RGB", "LAB") - - # Need to add a type mapping for some PIL type to TYPE_Lab_8 in findLCMSType, and - # have that mapping work back to a PIL mode (likely RGB). - i = ImageCms.applyTransform(hopper(), t) - assert i is not None - assert_image(i, "LAB", (128, 128)) - - # i.save('temp.lab.tif') # visually verified vs PS. - - assert_image_similar_tofile(i, "Tests/images/hopper.Lab.tif", 3.5) - - -def test_lab_srgb() -> None: - psRGB = ImageCms.createProfile("sRGB") - pLab = ImageCms.createProfile("LAB") - t = ImageCms.buildTransform(pLab, psRGB, "LAB", "RGB") - - with Image.open("Tests/images/hopper.Lab.tif") as img: - img_srgb = ImageCms.applyTransform(img, t) - assert img_srgb is not None - - # img_srgb.save('temp.srgb.tif') # visually verified vs ps. - - assert_image_similar(hopper(), img_srgb, 30) - assert img_srgb.info["icc_profile"] - - profile = ImageCmsProfile(BytesIO(img_srgb.info["icc_profile"])) - assert "sRGB" in ImageCms.getProfileDescription(profile) - - -def test_lab_roundtrip() -> None: - # check to see if we're at least internally consistent. - psRGB = ImageCms.createProfile("sRGB") - pLab = ImageCms.createProfile("LAB") - t = ImageCms.buildTransform(psRGB, pLab, "RGB", "LAB") - - t2 = ImageCms.buildTransform(pLab, psRGB, "LAB", "RGB") - - i = ImageCms.applyTransform(hopper(), t) - assert i is not None - assert i.info["icc_profile"] == ImageCmsProfile(pLab).tobytes() - - out = ImageCms.applyTransform(i, t2) - assert out is not None - assert_image_similar(hopper(), out, 2) - - -def test_profile_tobytes() -> None: - with Image.open("Tests/images/rgb.jpg") as i: - p = ImageCms.getOpenProfile(BytesIO(i.info["icc_profile"])) - - p2 = ImageCms.getOpenProfile(BytesIO(p.tobytes())) - - # not the same bytes as the original icc_profile, but it does roundtrip - assert p.tobytes() == p2.tobytes() - assert ImageCms.getProfileName(p) == ImageCms.getProfileName(p2) - assert ImageCms.getProfileDescription(p) == ImageCms.getProfileDescription(p2) - - -def test_extended_information() -> None: - skip_missing() - o = ImageCms.getOpenProfile(SRGB) - p = o.profile - - def assert_truncated_tuple_equal( - tup1: tuple[Any, ...] | None, tup2: tuple[Any, ...], digits: int = 10 - ) -> None: - # Helper function to reduce precision of tuples of floats - # recursively and then check equality. - power = 10**digits - - def truncate_tuple(tuple_value: tuple[Any, ...]) -> tuple[Any, ...]: - return tuple( - ( - truncate_tuple(val) - if isinstance(val, tuple) - else int(val * power) / power - ) - for val in tuple_value - ) - - assert tup1 is not None - assert truncate_tuple(tup1) == truncate_tuple(tup2) - - assert p.attributes == 4294967296 - assert_truncated_tuple_equal( - p.blue_colorant, - ( - (0.14306640625, 0.06060791015625, 0.7140960693359375), - (0.1558847490315394, 0.06603820639433387, 0.06060791015625), - ), - ) - assert_truncated_tuple_equal( - p.blue_primary, - ( - (0.14306641366715667, 0.06060790921083026, 0.7140960805782015), - (0.15588475410450106, 0.06603820408959558, 0.06060790921083026), - ), - ) - assert_truncated_tuple_equal( - p.chromatic_adaptation, - ( - ( - (1.04791259765625, 0.0229339599609375, -0.050201416015625), - (0.02960205078125, 0.9904632568359375, -0.0170745849609375), - (-0.009246826171875, 0.0150604248046875, 0.7517852783203125), - ), - ( - (1.0267159024652783, 0.022470062342089134, 0.0229339599609375), - (0.02951378324103937, 0.9875098886387147, 0.9904632568359375), - (-0.012205438066465256, 0.01987915407854985, 0.0150604248046875), - ), - ), - ) - assert p.chromaticity is None - assert p.clut == { - 0: (False, False, True), - 1: (False, False, True), - 2: (False, False, True), - 3: (False, False, True), - } - - assert p.colorant_table is None - assert p.colorant_table_out is None - assert p.colorimetric_intent is None - assert p.connection_space == "XYZ " - assert p.copyright == "Copyright International Color Consortium, 2009" - assert p.creation_date == datetime.datetime(2009, 2, 27, 21, 36, 31) - assert p.device_class == "mntr" - assert_truncated_tuple_equal( - p.green_colorant, - ( - (0.3851470947265625, 0.7168731689453125, 0.097076416015625), - (0.32119769927720654, 0.5978443449048152, 0.7168731689453125), - ), - ) - assert_truncated_tuple_equal( - p.green_primary, - ( - (0.3851470888162112, 0.7168731974161346, 0.09707641738998518), - (0.32119768793686687, 0.5978443567149709, 0.7168731974161346), - ), - ) - assert p.header_flags == 0 - assert p.header_manufacturer == "\x00\x00\x00\x00" - assert p.header_model == "\x00\x00\x00\x00" - assert p.icc_measurement_condition == { - "backing": (0.0, 0.0, 0.0), - "flare": 0.0, - "geo": "unknown", - "observer": 1, - "illuminant_type": "D65", - } - assert p.icc_version == 33554432 - assert p.icc_viewing_condition is None - assert p.intent_supported == { - 0: (True, True, True), - 1: (True, True, True), - 2: (True, True, True), - 3: (True, True, True), - } - assert p.is_matrix_shaper - assert p.luminance == ((0.0, 80.0, 0.0), (0.0, 1.0, 80.0)) - assert p.manufacturer is None - assert_truncated_tuple_equal( - p.media_black_point, - ( - (0.012054443359375, 0.0124969482421875, 0.01031494140625), - (0.34573304157549234, 0.35842450765864337, 0.0124969482421875), - ), - ) - assert_truncated_tuple_equal( - p.media_white_point, - ( - (0.964202880859375, 1.0, 0.8249053955078125), - (0.3457029219802284, 0.3585375327567059, 1.0), - ), - ) - assert_truncated_tuple_equal( - (p.media_white_point_temperature,), (5000.722328847392,) - ) - assert p.model == "IEC 61966-2-1 Default RGB Colour Space - sRGB" - - assert p.perceptual_rendering_intent_gamut is None - - assert p.profile_description == "sRGB IEC61966-2-1 black scaled" - assert p.profile_id == b")\xf8=\xde\xaf\xf2U\xaexB\xfa\xe4\xca\x839\r" - assert_truncated_tuple_equal( - p.red_colorant, - ( - (0.436065673828125, 0.2224884033203125, 0.013916015625), - (0.6484536316398539, 0.3308524880306778, 0.2224884033203125), - ), - ) - assert_truncated_tuple_equal( - p.red_primary, - ( - (0.43606566581047446, 0.22248840582960838, 0.013916015621759925), - (0.6484536250319214, 0.3308524944738204, 0.22248840582960838), - ), - ) - assert p.rendering_intent == 0 - assert p.saturation_rendering_intent_gamut is None - assert p.screening_description is None - assert p.target is None - assert p.technology == "CRT " - assert p.version == 2.0 - assert p.viewing_condition == "Reference Viewing Condition in IEC 61966-2-1" - assert p.xcolor_space == "RGB " - - -def test_non_ascii_path(tmp_path: Path) -> None: - skip_missing() - tempfile = str(tmp_path / ("temp_" + chr(128) + ".icc")) - try: - shutil.copy(SRGB, tempfile) - except UnicodeEncodeError: - pytest.skip("Non-ASCII path could not be created") - - o = ImageCms.getOpenProfile(tempfile) - p = o.profile - assert p.model == "IEC 61966-2-1 Default RGB Colour Space - sRGB" - - -def test_profile_typesafety() -> None: - # does not segfault - with pytest.raises(TypeError, match="Invalid type for Profile"): - ImageCms.ImageCmsProfile(0) # type: ignore[arg-type] - with pytest.raises(TypeError, match="Invalid type for Profile"): - ImageCms.ImageCmsProfile(1) # type: ignore[arg-type] - - # also check core function - with pytest.raises(TypeError): - ImageCms.core.profile_tobytes(0) # type: ignore[arg-type] - with pytest.raises(TypeError): - ImageCms.core.profile_tobytes(1) # type: ignore[arg-type] - - if not is_pypy(): - # core profile should not be directly instantiable - with pytest.raises(TypeError): - ImageCms.core.CmsProfile() - with pytest.raises(TypeError): - ImageCms.core.CmsProfile(0) # type: ignore[call-arg] - - -@pytest.mark.skipif(is_pypy(), reason="fails on PyPy") -def test_transform_typesafety() -> None: - # core transform should not be directly instantiable - with pytest.raises(TypeError): - ImageCms.core.CmsTransform() - with pytest.raises(TypeError): - ImageCms.core.CmsTransform(0) # type: ignore[call-arg] - - -def assert_aux_channel_preserved( - mode: str, transform_in_place: bool, preserved_channel: str -) -> None: - def create_test_image() -> Image.Image: - # set up test image with something interesting in the tested aux channel. - # fmt: off - nine_grid_deltas = [ - (-1, -1), (-1, 0), (-1, 1), - (0, -1), (0, 0), (0, 1), - (1, -1), (1, 0), (1, 1), - ] - # fmt: on - chans = [] - bands = ImageMode.getmode(mode).bands - for band_ndx in range(len(bands)): - channel_type = "L" # 8-bit unorm - channel_pattern = hopper(channel_type) - - # paste pattern with varying offsets to avoid correlation - # potentially hiding some bugs (like channels getting mixed). - paste_offset = ( - int(band_ndx / len(bands) * channel_pattern.size[0]), - int(band_ndx / (len(bands) * 2) * channel_pattern.size[1]), - ) - channel_data = Image.new(channel_type, channel_pattern.size) - for delta in nine_grid_deltas: - channel_data.paste( - channel_pattern, - tuple( - paste_offset[c] + delta[c] * channel_pattern.size[c] - for c in range(2) - ), - ) - chans.append(channel_data) - return Image.merge(mode, chans) - - source_image = create_test_image() - source_image_aux = source_image.getchannel(preserved_channel) - - # create some transform, it doesn't matter which one - source_profile = ImageCms.createProfile("sRGB") - destination_profile = ImageCms.createProfile("sRGB") - t = ImageCms.buildTransform( - source_profile, destination_profile, inMode=mode, outMode=mode - ) - - # apply transform - result_image: Image.Image | None - if transform_in_place: - ImageCms.applyTransform(source_image, t, inPlace=True) - result_image = source_image - else: - result_image = ImageCms.applyTransform(source_image, t, inPlace=False) - assert result_image is not None - result_image_aux = result_image.getchannel(preserved_channel) - - assert_image_equal(source_image_aux, result_image_aux) - - -def test_preserve_auxiliary_channels_rgba() -> None: - assert_aux_channel_preserved( - mode="RGBA", transform_in_place=False, preserved_channel="A" - ) - - -def test_preserve_auxiliary_channels_rgba_in_place() -> None: - assert_aux_channel_preserved( - mode="RGBA", transform_in_place=True, preserved_channel="A" - ) - - -def test_preserve_auxiliary_channels_rgbx() -> None: - assert_aux_channel_preserved( - mode="RGBX", transform_in_place=False, preserved_channel="X" - ) - - -def test_preserve_auxiliary_channels_rgbx_in_place() -> None: - assert_aux_channel_preserved( - mode="RGBX", transform_in_place=True, preserved_channel="X" - ) - - -def test_auxiliary_channels_isolated() -> None: - # test data in aux channels does not affect non-aux channels - aux_channel_formats = [ - # format, profile, color-only format, source test image - ("RGBA", "sRGB", "RGB", hopper("RGBA")), - ("RGBX", "sRGB", "RGB", hopper("RGBX")), - ("LAB", "LAB", "LAB", Image.open("Tests/images/hopper.Lab.tif")), - ] - for src_format in aux_channel_formats: - for dst_format in aux_channel_formats: - for transform_in_place in [True, False]: - # inplace only if format doesn't change - if transform_in_place and src_format[0] != dst_format[0]: - continue - - # convert with and without AUX data, test colors are equal - src_colorSpace = cast(Literal["LAB", "XYZ", "sRGB"], src_format[1]) - source_profile = ImageCms.createProfile(src_colorSpace) - destination_profile = ImageCms.createProfile(dst_format[1]) - source_image = src_format[3] - test_transform = ImageCms.buildTransform( - source_profile, - destination_profile, - inMode=src_format[0], - outMode=dst_format[0], - ) - - # test conversion from aux-ful source - test_image: Image.Image | None - if transform_in_place: - test_image = source_image.copy() - ImageCms.applyTransform(test_image, test_transform, inPlace=True) - else: - test_image = ImageCms.applyTransform( - source_image, test_transform, inPlace=False - ) - assert test_image is not None - - # reference conversion from aux-less source - reference_transform = ImageCms.buildTransform( - source_profile, - destination_profile, - inMode=src_format[2], - outMode=dst_format[2], - ) - reference_image = ImageCms.applyTransform( - source_image.convert(src_format[2]), reference_transform - ) - assert reference_image is not None - assert_image_equal(test_image.convert(dst_format[2]), reference_image) - - -def test_long_modes() -> None: - p = ImageCms.getOpenProfile("Tests/icc/sGrey-v2-nano.icc") - ImageCms.buildTransform(p, p, "ABCDEFGHI", "ABCDEFGHI") - - -@pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX")) -def test_rgb_lab(mode: str) -> None: - im = Image.new(mode, (1, 1)) - converted_im = im.convert("LAB") - assert converted_im.getpixel((0, 0)) == (0, 128, 128) - - im = Image.new("LAB", (1, 1), (255, 0, 0)) - converted_im = im.convert(mode) - assert converted_im.getpixel((0, 0))[:3] == (0, 255, 255) - - -def test_deprecation() -> None: - with pytest.warns(DeprecationWarning): - assert ImageCms.DESCRIPTION.strip().startswith("pyCMS") - with pytest.warns(DeprecationWarning): - assert ImageCms.VERSION == "1.0.0 pil" - with pytest.warns(DeprecationWarning): - assert isinstance(ImageCms.FLAGS, dict) +from __future__ import annotations + +import datetime +import os +import re +import shutil +import sys +from io import BytesIO +from pathlib import Path +from typing import Any, Literal, cast + +import pytest + +from PIL import Image, ImageMode, ImageWin, features + +from .helper import ( + assert_image, + assert_image_equal, + assert_image_similar, + assert_image_similar_tofile, + hopper, + is_pypy, +) + +try: + from PIL import ImageCms + from PIL.ImageCms import ImageCmsProfile + + ImageCms.core.profile_open +except ImportError: + # Skipped via setup_module() + pass + + +SRGB = "Tests/icc/sRGB_IEC61966-2-1_black_scaled.icc" +HAVE_PROFILE = os.path.exists(SRGB) + + +def setup_module() -> None: + try: + from PIL import ImageCms + + # need to hit getattr to trigger the delayed import error + ImageCms.core.profile_open + except ImportError as v: + pytest.skip(str(v)) + + +def skip_missing() -> None: + if not HAVE_PROFILE: + pytest.skip("SRGB profile not available") + + +def test_sanity() -> None: + # basic smoke test. + # this mostly follows the cms_test outline. + with pytest.warns(DeprecationWarning): + v = ImageCms.versions() # should return four strings + assert v[0] == "1.0.0 pil" + assert list(map(type, v)) == [str, str, str, str] + + # internal version number + version = features.version_module("littlecms2") + assert version is not None + assert re.search(r"\d+\.\d+(\.\d+)?$", version) + + skip_missing() + i = ImageCms.profileToProfile(hopper(), SRGB, SRGB) + assert i is not None + assert_image(i, "RGB", (128, 128)) + + i = hopper() + ImageCms.profileToProfile(i, SRGB, SRGB, inPlace=True) + assert_image(i, "RGB", (128, 128)) + + t = ImageCms.buildTransform(SRGB, SRGB, "RGB", "RGB") + i = ImageCms.applyTransform(hopper(), t) + assert i is not None + assert_image(i, "RGB", (128, 128)) + + with hopper() as i: + t = ImageCms.buildTransform(SRGB, SRGB, "RGB", "RGB") + ImageCms.applyTransform(hopper(), t, inPlace=True) + assert i is not None + assert_image(i, "RGB", (128, 128)) + + p = ImageCms.createProfile("sRGB") + o = ImageCms.getOpenProfile(SRGB) + t = ImageCms.buildTransformFromOpenProfiles(p, o, "RGB", "RGB") + i = ImageCms.applyTransform(hopper(), t) + assert i is not None + assert_image(i, "RGB", (128, 128)) + + t = ImageCms.buildProofTransform(SRGB, SRGB, SRGB, "RGB", "RGB") + assert t.inputMode == "RGB" + assert t.outputMode == "RGB" + i = ImageCms.applyTransform(hopper(), t) + assert i is not None + assert_image(i, "RGB", (128, 128)) + + # test PointTransform convenience API + hopper().point(t) + + +def test_flags() -> None: + assert ImageCms.Flags.NONE == 0 + assert ImageCms.Flags.GRIDPOINTS(0) == ImageCms.Flags.NONE + assert ImageCms.Flags.GRIDPOINTS(256) == ImageCms.Flags.NONE + + assert ImageCms.Flags.GRIDPOINTS(255) == (255 << 16) + assert ImageCms.Flags.GRIDPOINTS(-1) == ImageCms.Flags.GRIDPOINTS(255) + assert ImageCms.Flags.GRIDPOINTS(511) == ImageCms.Flags.GRIDPOINTS(255) + + +def test_name() -> None: + skip_missing() + # get profile information for file + assert ( + ImageCms.getProfileName(SRGB).strip() + == "IEC 61966-2-1 Default RGB Colour Space - sRGB" + ) + + +def test_info() -> None: + skip_missing() + assert ImageCms.getProfileInfo(SRGB).splitlines() == [ + "sRGB IEC61966-2-1 black scaled", + "", + "Copyright International Color Consortium, 2009", + "", + ] + + +def test_copyright() -> None: + skip_missing() + assert ( + ImageCms.getProfileCopyright(SRGB).strip() + == "Copyright International Color Consortium, 2009" + ) + + +def test_manufacturer() -> None: + skip_missing() + assert ImageCms.getProfileManufacturer(SRGB).strip() == "" + + +def test_model() -> None: + skip_missing() + assert ( + ImageCms.getProfileModel(SRGB).strip() + == "IEC 61966-2-1 Default RGB Colour Space - sRGB" + ) + + +def test_description() -> None: + skip_missing() + assert ( + ImageCms.getProfileDescription(SRGB).strip() == "sRGB IEC61966-2-1 black scaled" + ) + + +def test_intent() -> None: + skip_missing() + assert ImageCms.getDefaultIntent(SRGB) == 0 + support = ImageCms.isIntentSupported( + SRGB, ImageCms.Intent.ABSOLUTE_COLORIMETRIC, ImageCms.Direction.INPUT + ) + assert support == 1 + + +def test_profile_object() -> None: + # same, using profile object + p = ImageCms.createProfile("sRGB") + # assert ImageCms.getProfileName(p).strip() == "sRGB built-in - (lcms internal)" + # assert ImageCms.getProfileInfo(p).splitlines() == + # ["sRGB built-in", "", "WhitePoint : D65 (daylight)", "", ""] + assert ImageCms.getDefaultIntent(p) == 0 + support = ImageCms.isIntentSupported( + p, ImageCms.Intent.ABSOLUTE_COLORIMETRIC, ImageCms.Direction.INPUT + ) + assert support == 1 + + +def test_extensions() -> None: + # extensions + + with Image.open("Tests/images/rgb.jpg") as i: + p = ImageCms.getOpenProfile(BytesIO(i.info["icc_profile"])) + assert ( + ImageCms.getProfileName(p).strip() + == "IEC 61966-2.1 Default RGB colour space - sRGB" + ) + + +def test_exceptions() -> None: + # Test mode mismatch + psRGB = ImageCms.createProfile("sRGB") + pLab = ImageCms.createProfile("LAB") + t = ImageCms.buildTransform(pLab, psRGB, "LAB", "RGB") + with pytest.raises(ValueError, match="mode mismatch"): + t.apply_in_place(hopper("RGBA")) + + # the procedural pyCMS API uses PyCMSError for all sorts of errors + with hopper() as im: + with pytest.raises(ImageCms.PyCMSError, match="cannot open profile file"): + ImageCms.profileToProfile(im, "foo", "bar") + + with pytest.raises(ImageCms.PyCMSError, match="cannot open profile file"): + ImageCms.buildTransform("foo", "bar", "RGB", "RGB") + + with pytest.raises(ImageCms.PyCMSError, match="Invalid type for Profile"): + ImageCms.getProfileName(None) # type: ignore[arg-type] + skip_missing() + + # Python <= 3.9: "an integer is required (got type NoneType)" + # Python > 3.9: "'NoneType' object cannot be interpreted as an integer" + with pytest.raises(ImageCms.PyCMSError, match="integer"): + ImageCms.isIntentSupported(SRGB, None, None) # type: ignore[arg-type] + + +def test_display_profile() -> None: + # try fetching the profile for the current display device + ImageCms.get_display_profile() + + if sys.platform == "win32": + ImageCms.get_display_profile(ImageWin.HDC(0)) + ImageCms.get_display_profile(ImageWin.HWND(0)) + + +def test_lab_color_profile() -> None: + ImageCms.createProfile("LAB", 5000) + ImageCms.createProfile("LAB", 6500) + + +def test_unsupported_color_space() -> None: + with pytest.raises( + ImageCms.PyCMSError, + match=re.escape( + "Color space not supported for on-the-fly profile creation (unsupported)" + ), + ): + ImageCms.createProfile("unsupported") # type: ignore[arg-type] + + +def test_invalid_color_temperature() -> None: + with pytest.raises( + ImageCms.PyCMSError, + match='Color temperature must be numeric, "invalid" not valid', + ): + ImageCms.createProfile("LAB", "invalid") # type: ignore[arg-type] + + +@pytest.mark.parametrize("flag", ("my string", -1)) +def test_invalid_flag(flag: str | int) -> None: + with hopper() as im: + with pytest.raises( + ImageCms.PyCMSError, match="flags must be an integer between 0 and " + ): + ImageCms.profileToProfile(im, "foo", "bar", flags=flag) # type: ignore[arg-type] + + +def test_simple_lab() -> None: + i = Image.new("RGB", (10, 10), (128, 128, 128)) + + psRGB = ImageCms.createProfile("sRGB") + pLab = ImageCms.createProfile("LAB") + t = ImageCms.buildTransform(psRGB, pLab, "RGB", "LAB") + + i_lab = ImageCms.applyTransform(i, t) + assert i_lab is not None + assert i_lab.mode == "LAB" + + k = i_lab.getpixel((0, 0)) + # not a linear luminance map. so L != 128: + assert k == (137, 128, 128) + + l_data = i_lab.getdata(0) + a_data = i_lab.getdata(1) + b_data = i_lab.getdata(2) + + assert list(l_data) == [137] * 100 + assert list(a_data) == [128] * 100 + assert list(b_data) == [128] * 100 + + +def test_lab_color() -> None: + psRGB = ImageCms.createProfile("sRGB") + pLab = ImageCms.createProfile("LAB") + t = ImageCms.buildTransform(psRGB, pLab, "RGB", "LAB") + + # Need to add a type mapping for some PIL type to TYPE_Lab_8 in findLCMSType, and + # have that mapping work back to a PIL mode (likely RGB). + i = ImageCms.applyTransform(hopper(), t) + assert i is not None + assert_image(i, "LAB", (128, 128)) + + # i.save('temp.lab.tif') # visually verified vs PS. + + assert_image_similar_tofile(i, "Tests/images/hopper.Lab.tif", 3.5) + + +def test_lab_srgb() -> None: + psRGB = ImageCms.createProfile("sRGB") + pLab = ImageCms.createProfile("LAB") + t = ImageCms.buildTransform(pLab, psRGB, "LAB", "RGB") + + with Image.open("Tests/images/hopper.Lab.tif") as img: + img_srgb = ImageCms.applyTransform(img, t) + assert img_srgb is not None + + # img_srgb.save('temp.srgb.tif') # visually verified vs ps. + + assert_image_similar(hopper(), img_srgb, 30) + assert img_srgb.info["icc_profile"] + + profile = ImageCmsProfile(BytesIO(img_srgb.info["icc_profile"])) + assert "sRGB" in ImageCms.getProfileDescription(profile) + + +def test_lab_roundtrip() -> None: + # check to see if we're at least internally consistent. + psRGB = ImageCms.createProfile("sRGB") + pLab = ImageCms.createProfile("LAB") + t = ImageCms.buildTransform(psRGB, pLab, "RGB", "LAB") + + t2 = ImageCms.buildTransform(pLab, psRGB, "LAB", "RGB") + + i = ImageCms.applyTransform(hopper(), t) + assert i is not None + assert i.info["icc_profile"] == ImageCmsProfile(pLab).tobytes() + + out = ImageCms.applyTransform(i, t2) + assert out is not None + assert_image_similar(hopper(), out, 2) + + +def test_profile_tobytes() -> None: + with Image.open("Tests/images/rgb.jpg") as i: + p = ImageCms.getOpenProfile(BytesIO(i.info["icc_profile"])) + + p2 = ImageCms.getOpenProfile(BytesIO(p.tobytes())) + + # not the same bytes as the original icc_profile, but it does roundtrip + assert p.tobytes() == p2.tobytes() + assert ImageCms.getProfileName(p) == ImageCms.getProfileName(p2) + assert ImageCms.getProfileDescription(p) == ImageCms.getProfileDescription(p2) + + +def test_extended_information() -> None: + skip_missing() + o = ImageCms.getOpenProfile(SRGB) + p = o.profile + + def assert_truncated_tuple_equal( + tup1: tuple[Any, ...] | None, tup2: tuple[Any, ...], digits: int = 10 + ) -> None: + # Helper function to reduce precision of tuples of floats + # recursively and then check equality. + power = 10**digits + + def truncate_tuple(tuple_value: tuple[Any, ...]) -> tuple[Any, ...]: + return tuple( + ( + truncate_tuple(val) + if isinstance(val, tuple) + else int(val * power) / power + ) + for val in tuple_value + ) + + assert tup1 is not None + assert truncate_tuple(tup1) == truncate_tuple(tup2) + + assert p.attributes == 4294967296 + assert_truncated_tuple_equal( + p.blue_colorant, + ( + (0.14306640625, 0.06060791015625, 0.7140960693359375), + (0.1558847490315394, 0.06603820639433387, 0.06060791015625), + ), + ) + assert_truncated_tuple_equal( + p.blue_primary, + ( + (0.14306641366715667, 0.06060790921083026, 0.7140960805782015), + (0.15588475410450106, 0.06603820408959558, 0.06060790921083026), + ), + ) + assert_truncated_tuple_equal( + p.chromatic_adaptation, + ( + ( + (1.04791259765625, 0.0229339599609375, -0.050201416015625), + (0.02960205078125, 0.9904632568359375, -0.0170745849609375), + (-0.009246826171875, 0.0150604248046875, 0.7517852783203125), + ), + ( + (1.0267159024652783, 0.022470062342089134, 0.0229339599609375), + (0.02951378324103937, 0.9875098886387147, 0.9904632568359375), + (-0.012205438066465256, 0.01987915407854985, 0.0150604248046875), + ), + ), + ) + assert p.chromaticity is None + assert p.clut == { + 0: (False, False, True), + 1: (False, False, True), + 2: (False, False, True), + 3: (False, False, True), + } + + assert p.colorant_table is None + assert p.colorant_table_out is None + assert p.colorimetric_intent is None + assert p.connection_space == "XYZ " + assert p.copyright == "Copyright International Color Consortium, 2009" + assert p.creation_date == datetime.datetime(2009, 2, 27, 21, 36, 31) + assert p.device_class == "mntr" + assert_truncated_tuple_equal( + p.green_colorant, + ( + (0.3851470947265625, 0.7168731689453125, 0.097076416015625), + (0.32119769927720654, 0.5978443449048152, 0.7168731689453125), + ), + ) + assert_truncated_tuple_equal( + p.green_primary, + ( + (0.3851470888162112, 0.7168731974161346, 0.09707641738998518), + (0.32119768793686687, 0.5978443567149709, 0.7168731974161346), + ), + ) + assert p.header_flags == 0 + assert p.header_manufacturer == "\x00\x00\x00\x00" + assert p.header_model == "\x00\x00\x00\x00" + assert p.icc_measurement_condition == { + "backing": (0.0, 0.0, 0.0), + "flare": 0.0, + "geo": "unknown", + "observer": 1, + "illuminant_type": "D65", + } + assert p.icc_version == 33554432 + assert p.icc_viewing_condition is None + assert p.intent_supported == { + 0: (True, True, True), + 1: (True, True, True), + 2: (True, True, True), + 3: (True, True, True), + } + assert p.is_matrix_shaper + assert p.luminance == ((0.0, 80.0, 0.0), (0.0, 1.0, 80.0)) + assert p.manufacturer is None + assert_truncated_tuple_equal( + p.media_black_point, + ( + (0.012054443359375, 0.0124969482421875, 0.01031494140625), + (0.34573304157549234, 0.35842450765864337, 0.0124969482421875), + ), + ) + assert_truncated_tuple_equal( + p.media_white_point, + ( + (0.964202880859375, 1.0, 0.8249053955078125), + (0.3457029219802284, 0.3585375327567059, 1.0), + ), + ) + assert_truncated_tuple_equal( + (p.media_white_point_temperature,), (5000.722328847392,) + ) + assert p.model == "IEC 61966-2-1 Default RGB Colour Space - sRGB" + + assert p.perceptual_rendering_intent_gamut is None + + assert p.profile_description == "sRGB IEC61966-2-1 black scaled" + assert p.profile_id == b")\xf8=\xde\xaf\xf2U\xaexB\xfa\xe4\xca\x839\r" + assert_truncated_tuple_equal( + p.red_colorant, + ( + (0.436065673828125, 0.2224884033203125, 0.013916015625), + (0.6484536316398539, 0.3308524880306778, 0.2224884033203125), + ), + ) + assert_truncated_tuple_equal( + p.red_primary, + ( + (0.43606566581047446, 0.22248840582960838, 0.013916015621759925), + (0.6484536250319214, 0.3308524944738204, 0.22248840582960838), + ), + ) + assert p.rendering_intent == 0 + assert p.saturation_rendering_intent_gamut is None + assert p.screening_description is None + assert p.target is None + assert p.technology == "CRT " + assert p.version == 2.0 + assert p.viewing_condition == "Reference Viewing Condition in IEC 61966-2-1" + assert p.xcolor_space == "RGB " + + +def test_non_ascii_path(tmp_path: Path) -> None: + skip_missing() + tempfile = str(tmp_path / ("temp_" + chr(128) + ".icc")) + try: + shutil.copy(SRGB, tempfile) + except UnicodeEncodeError: + pytest.skip("Non-ASCII path could not be created") + + o = ImageCms.getOpenProfile(tempfile) + p = o.profile + assert p.model == "IEC 61966-2-1 Default RGB Colour Space - sRGB" + + +def test_profile_typesafety() -> None: + # does not segfault + with pytest.raises(TypeError, match="Invalid type for Profile"): + ImageCms.ImageCmsProfile(0) # type: ignore[arg-type] + with pytest.raises(TypeError, match="Invalid type for Profile"): + ImageCms.ImageCmsProfile(1) # type: ignore[arg-type] + + # also check core function + with pytest.raises(TypeError): + ImageCms.core.profile_tobytes(0) # type: ignore[arg-type] + with pytest.raises(TypeError): + ImageCms.core.profile_tobytes(1) # type: ignore[arg-type] + + if not is_pypy(): + # core profile should not be directly instantiable + with pytest.raises(TypeError): + ImageCms.core.CmsProfile() + with pytest.raises(TypeError): + ImageCms.core.CmsProfile(0) # type: ignore[call-arg] + + +@pytest.mark.skipif(is_pypy(), reason="fails on PyPy") +def test_transform_typesafety() -> None: + # core transform should not be directly instantiable + with pytest.raises(TypeError): + ImageCms.core.CmsTransform() + with pytest.raises(TypeError): + ImageCms.core.CmsTransform(0) # type: ignore[call-arg] + + +def assert_aux_channel_preserved( + mode: str, transform_in_place: bool, preserved_channel: str +) -> None: + def create_test_image() -> Image.Image: + # set up test image with something interesting in the tested aux channel. + # fmt: off + nine_grid_deltas = [ + (-1, -1), (-1, 0), (-1, 1), + (0, -1), (0, 0), (0, 1), + (1, -1), (1, 0), (1, 1), + ] + # fmt: on + chans = [] + bands = ImageMode.getmode(mode).bands + for band_ndx in range(len(bands)): + channel_type = "L" # 8-bit unorm + channel_pattern = hopper(channel_type) + + # paste pattern with varying offsets to avoid correlation + # potentially hiding some bugs (like channels getting mixed). + paste_offset = ( + int(band_ndx / len(bands) * channel_pattern.size[0]), + int(band_ndx / (len(bands) * 2) * channel_pattern.size[1]), + ) + channel_data = Image.new(channel_type, channel_pattern.size) + for delta in nine_grid_deltas: + channel_data.paste( + channel_pattern, + tuple( + paste_offset[c] + delta[c] * channel_pattern.size[c] + for c in range(2) + ), + ) + chans.append(channel_data) + return Image.merge(mode, chans) + + source_image = create_test_image() + source_image_aux = source_image.getchannel(preserved_channel) + + # create some transform, it doesn't matter which one + source_profile = ImageCms.createProfile("sRGB") + destination_profile = ImageCms.createProfile("sRGB") + t = ImageCms.buildTransform( + source_profile, destination_profile, inMode=mode, outMode=mode + ) + + # apply transform + result_image: Image.Image | None + if transform_in_place: + ImageCms.applyTransform(source_image, t, inPlace=True) + result_image = source_image + else: + result_image = ImageCms.applyTransform(source_image, t, inPlace=False) + assert result_image is not None + result_image_aux = result_image.getchannel(preserved_channel) + + assert_image_equal(source_image_aux, result_image_aux) + + +def test_preserve_auxiliary_channels_rgba() -> None: + assert_aux_channel_preserved( + mode="RGBA", transform_in_place=False, preserved_channel="A" + ) + + +def test_preserve_auxiliary_channels_rgba_in_place() -> None: + assert_aux_channel_preserved( + mode="RGBA", transform_in_place=True, preserved_channel="A" + ) + + +def test_preserve_auxiliary_channels_rgbx() -> None: + assert_aux_channel_preserved( + mode="RGBX", transform_in_place=False, preserved_channel="X" + ) + + +def test_preserve_auxiliary_channels_rgbx_in_place() -> None: + assert_aux_channel_preserved( + mode="RGBX", transform_in_place=True, preserved_channel="X" + ) + + +def test_auxiliary_channels_isolated() -> None: + # test data in aux channels does not affect non-aux channels + aux_channel_formats = [ + # format, profile, color-only format, source test image + ("RGBA", "sRGB", "RGB", hopper("RGBA")), + ("RGBX", "sRGB", "RGB", hopper("RGBX")), + ("LAB", "LAB", "LAB", Image.open("Tests/images/hopper.Lab.tif")), + ] + for src_format in aux_channel_formats: + for dst_format in aux_channel_formats: + for transform_in_place in [True, False]: + # inplace only if format doesn't change + if transform_in_place and src_format[0] != dst_format[0]: + continue + + # convert with and without AUX data, test colors are equal + src_colorSpace = cast(Literal["LAB", "XYZ", "sRGB"], src_format[1]) + source_profile = ImageCms.createProfile(src_colorSpace) + destination_profile = ImageCms.createProfile(dst_format[1]) + source_image = src_format[3] + test_transform = ImageCms.buildTransform( + source_profile, + destination_profile, + inMode=src_format[0], + outMode=dst_format[0], + ) + + # test conversion from aux-ful source + test_image: Image.Image | None + if transform_in_place: + test_image = source_image.copy() + ImageCms.applyTransform(test_image, test_transform, inPlace=True) + else: + test_image = ImageCms.applyTransform( + source_image, test_transform, inPlace=False + ) + assert test_image is not None + + # reference conversion from aux-less source + reference_transform = ImageCms.buildTransform( + source_profile, + destination_profile, + inMode=src_format[2], + outMode=dst_format[2], + ) + reference_image = ImageCms.applyTransform( + source_image.convert(src_format[2]), reference_transform + ) + assert reference_image is not None + assert_image_equal(test_image.convert(dst_format[2]), reference_image) + + +def test_long_modes() -> None: + p = ImageCms.getOpenProfile("Tests/icc/sGrey-v2-nano.icc") + ImageCms.buildTransform(p, p, "ABCDEFGHI", "ABCDEFGHI") + + +@pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX")) +def test_rgb_lab(mode: str) -> None: + im = Image.new(mode, (1, 1)) + converted_im = im.convert("LAB") + assert converted_im.getpixel((0, 0)) == (0, 128, 128) + + im = Image.new("LAB", (1, 1), (255, 0, 0)) + converted_im = im.convert(mode) + assert converted_im.getpixel((0, 0))[:3] == (0, 255, 255) + + +def test_deprecation() -> None: + with pytest.warns(DeprecationWarning): + assert ImageCms.DESCRIPTION.strip().startswith("pyCMS") + with pytest.warns(DeprecationWarning): + assert ImageCms.VERSION == "1.0.0 pil" + with pytest.warns(DeprecationWarning): + assert isinstance(ImageCms.FLAGS, dict) diff --git a/Tests/test_imagecolor.py b/Tests/test_imagecolor.py index 6eea7886d..15efffc08 100644 --- a/Tests/test_imagecolor.py +++ b/Tests/test_imagecolor.py @@ -1,208 +1,208 @@ -from __future__ import annotations - -import pytest - -from PIL import Image, ImageColor - - -def test_hash() -> None: - # short 3 components - assert (255, 0, 0) == ImageColor.getrgb("#f00") - assert (0, 255, 0) == ImageColor.getrgb("#0f0") - assert (0, 0, 255) == ImageColor.getrgb("#00f") - - # short 4 components - assert (255, 0, 0, 0) == ImageColor.getrgb("#f000") - assert (0, 255, 0, 0) == ImageColor.getrgb("#0f00") - assert (0, 0, 255, 0) == ImageColor.getrgb("#00f0") - assert (0, 0, 0, 255) == ImageColor.getrgb("#000f") - - # long 3 components - assert (222, 0, 0) == ImageColor.getrgb("#de0000") - assert (0, 222, 0) == ImageColor.getrgb("#00de00") - assert (0, 0, 222) == ImageColor.getrgb("#0000de") - - # long 4 components - assert (222, 0, 0, 0) == ImageColor.getrgb("#de000000") - assert (0, 222, 0, 0) == ImageColor.getrgb("#00de0000") - assert (0, 0, 222, 0) == ImageColor.getrgb("#0000de00") - assert (0, 0, 0, 222) == ImageColor.getrgb("#000000de") - - # case insensitivity - assert ImageColor.getrgb("#DEF") == ImageColor.getrgb("#def") - assert ImageColor.getrgb("#CDEF") == ImageColor.getrgb("#cdef") - assert ImageColor.getrgb("#DEFDEF") == ImageColor.getrgb("#defdef") - assert ImageColor.getrgb("#CDEFCDEF") == ImageColor.getrgb("#cdefcdef") - - # not a number - with pytest.raises(ValueError): - ImageColor.getrgb("#fo0") - with pytest.raises(ValueError): - ImageColor.getrgb("#fo00") - with pytest.raises(ValueError): - ImageColor.getrgb("#fo0000") - with pytest.raises(ValueError): - ImageColor.getrgb("#fo000000") - - # wrong number of components - with pytest.raises(ValueError): - ImageColor.getrgb("#f0000") - with pytest.raises(ValueError): - ImageColor.getrgb("#f000000") - with pytest.raises(ValueError): - ImageColor.getrgb("#f00000000") - with pytest.raises(ValueError): - ImageColor.getrgb("#f000000000") - with pytest.raises(ValueError): - ImageColor.getrgb("#f00000 ") - - -def test_colormap() -> None: - assert (0, 0, 0) == ImageColor.getrgb("black") - assert (255, 255, 255) == ImageColor.getrgb("white") - assert (255, 255, 255) == ImageColor.getrgb("WHITE") - - with pytest.raises(ValueError): - ImageColor.getrgb("black ") - - -def test_functions() -> None: - # rgb numbers - assert (255, 0, 0) == ImageColor.getrgb("rgb(255,0,0)") - assert (0, 255, 0) == ImageColor.getrgb("rgb(0,255,0)") - assert (0, 0, 255) == ImageColor.getrgb("rgb(0,0,255)") - - # percents - assert (255, 0, 0) == ImageColor.getrgb("rgb(100%,0%,0%)") - assert (0, 255, 0) == ImageColor.getrgb("rgb(0%,100%,0%)") - assert (0, 0, 255) == ImageColor.getrgb("rgb(0%,0%,100%)") - - # rgba numbers - assert (255, 0, 0, 0) == ImageColor.getrgb("rgba(255,0,0,0)") - assert (0, 255, 0, 0) == ImageColor.getrgb("rgba(0,255,0,0)") - assert (0, 0, 255, 0) == ImageColor.getrgb("rgba(0,0,255,0)") - assert (0, 0, 0, 255) == ImageColor.getrgb("rgba(0,0,0,255)") - - assert (255, 0, 0) == ImageColor.getrgb("hsl(0,100%,50%)") - assert (255, 0, 0) == ImageColor.getrgb("hsl(360,100%,50%)") - assert (0, 255, 255) == ImageColor.getrgb("hsl(180,100%,50%)") - - assert (255, 0, 0) == ImageColor.getrgb("hsv(0,100%,100%)") - assert (255, 0, 0) == ImageColor.getrgb("hsv(360,100%,100%)") - assert (0, 255, 255) == ImageColor.getrgb("hsv(180,100%,100%)") - - # alternate format - assert ImageColor.getrgb("hsb(0,100%,50%)") == ImageColor.getrgb("hsv(0,100%,50%)") - - # floats - assert (254, 3, 3) == ImageColor.getrgb("hsl(0.1,99.2%,50.3%)") - assert (255, 0, 0) == ImageColor.getrgb("hsl(360.,100.0%,50%)") - - assert (253, 2, 2) == ImageColor.getrgb("hsv(0.1,99.2%,99.3%)") - assert (255, 0, 0) == ImageColor.getrgb("hsv(360.,100.0%,100%)") - - # case insensitivity - assert ImageColor.getrgb("RGB(255,0,0)") == ImageColor.getrgb("rgb(255,0,0)") - assert ImageColor.getrgb("RGB(100%,0%,0%)") == ImageColor.getrgb("rgb(100%,0%,0%)") - assert ImageColor.getrgb("RGBA(255,0,0,0)") == ImageColor.getrgb("rgba(255,0,0,0)") - assert ImageColor.getrgb("HSL(0,100%,50%)") == ImageColor.getrgb("hsl(0,100%,50%)") - assert ImageColor.getrgb("HSV(0,100%,50%)") == ImageColor.getrgb("hsv(0,100%,50%)") - assert ImageColor.getrgb("HSB(0,100%,50%)") == ImageColor.getrgb("hsb(0,100%,50%)") - - # space agnosticism - assert (255, 0, 0) == ImageColor.getrgb("rgb( 255 , 0 , 0 )") - assert (255, 0, 0) == ImageColor.getrgb("rgb( 100% , 0% , 0% )") - assert (255, 0, 0, 0) == ImageColor.getrgb("rgba( 255 , 0 , 0 , 0 )") - assert (255, 0, 0) == ImageColor.getrgb("hsl( 0 , 100% , 50% )") - assert (255, 0, 0) == ImageColor.getrgb("hsv( 0 , 100% , 100% )") - - # wrong number of components - with pytest.raises(ValueError): - ImageColor.getrgb("rgb(255,0)") - with pytest.raises(ValueError): - ImageColor.getrgb("rgb(255,0,0,0)") - - with pytest.raises(ValueError): - ImageColor.getrgb("rgb(100%,0%)") - with pytest.raises(ValueError): - ImageColor.getrgb("rgb(100%,0%,0)") - with pytest.raises(ValueError): - ImageColor.getrgb("rgb(100%,0%,0 %)") - with pytest.raises(ValueError): - ImageColor.getrgb("rgb(100%,0%,0%,0%)") - - with pytest.raises(ValueError): - ImageColor.getrgb("rgba(255,0,0)") - with pytest.raises(ValueError): - ImageColor.getrgb("rgba(255,0,0,0,0)") - - with pytest.raises(ValueError): - ImageColor.getrgb("hsl(0,100%)") - with pytest.raises(ValueError): - ImageColor.getrgb("hsl(0,100%,0%,0%)") - with pytest.raises(ValueError): - ImageColor.getrgb("hsl(0%,100%,50%)") - with pytest.raises(ValueError): - ImageColor.getrgb("hsl(0,100,50%)") - with pytest.raises(ValueError): - ImageColor.getrgb("hsl(0,100%,50)") - - with pytest.raises(ValueError): - ImageColor.getrgb("hsv(0,100%)") - with pytest.raises(ValueError): - ImageColor.getrgb("hsv(0,100%,0%,0%)") - with pytest.raises(ValueError): - ImageColor.getrgb("hsv(0%,100%,50%)") - with pytest.raises(ValueError): - ImageColor.getrgb("hsv(0,100,50%)") - with pytest.raises(ValueError): - ImageColor.getrgb("hsv(0,100%,50)") - - -# look for rounding errors (based on code by Tim Hatch) -def test_rounding_errors() -> None: - for color in ImageColor.colormap: - expected = Image.new("RGB", (1, 1), color).convert("L").getpixel((0, 0)) - actual = ImageColor.getcolor(color, "L") - assert expected == actual - - assert (0, 255, 115) == ImageColor.getcolor("rgba(0, 255, 115, 33)", "RGB") - Image.new("RGB", (1, 1), "white") - - assert (0, 0, 0, 255) == ImageColor.getcolor("black", "RGBA") - assert (255, 255, 255, 255) == ImageColor.getcolor("white", "RGBA") - assert (0, 255, 115, 33) == ImageColor.getcolor("rgba(0, 255, 115, 33)", "RGBA") - Image.new("RGBA", (1, 1), "white") - - assert 0 == ImageColor.getcolor("black", "L") - assert 255 == ImageColor.getcolor("white", "L") - assert 163 == ImageColor.getcolor("rgba(0, 255, 115, 33)", "L") - Image.new("L", (1, 1), "white") - - assert 0 == ImageColor.getcolor("black", "1") - assert 255 == ImageColor.getcolor("white", "1") - # The following test is wrong, but is current behavior - # The correct result should be 255 due to the mode 1 - assert 163 == ImageColor.getcolor("rgba(0, 255, 115, 33)", "1") - # Correct behavior - # assert - # 255, ImageColor.getcolor("rgba(0, 255, 115, 33)", "1")) - Image.new("1", (1, 1), "white") - - assert (0, 255) == ImageColor.getcolor("black", "LA") - assert (255, 255) == ImageColor.getcolor("white", "LA") - assert (163, 33) == ImageColor.getcolor("rgba(0, 255, 115, 33)", "LA") - Image.new("LA", (1, 1), "white") - - -def test_color_hsv() -> None: - assert (170, 255, 255) == ImageColor.getcolor("hsv(240, 100%, 100%)", "HSV") - - -def test_color_too_long() -> None: - # Arrange - color_too_long = "hsl(" + "1" * 40 + "," + "1" * 40 + "%," + "1" * 40 + "%)" - - # Act / Assert - with pytest.raises(ValueError): - ImageColor.getrgb(color_too_long) +from __future__ import annotations + +import pytest + +from PIL import Image, ImageColor + + +def test_hash() -> None: + # short 3 components + assert (255, 0, 0) == ImageColor.getrgb("#f00") + assert (0, 255, 0) == ImageColor.getrgb("#0f0") + assert (0, 0, 255) == ImageColor.getrgb("#00f") + + # short 4 components + assert (255, 0, 0, 0) == ImageColor.getrgb("#f000") + assert (0, 255, 0, 0) == ImageColor.getrgb("#0f00") + assert (0, 0, 255, 0) == ImageColor.getrgb("#00f0") + assert (0, 0, 0, 255) == ImageColor.getrgb("#000f") + + # long 3 components + assert (222, 0, 0) == ImageColor.getrgb("#de0000") + assert (0, 222, 0) == ImageColor.getrgb("#00de00") + assert (0, 0, 222) == ImageColor.getrgb("#0000de") + + # long 4 components + assert (222, 0, 0, 0) == ImageColor.getrgb("#de000000") + assert (0, 222, 0, 0) == ImageColor.getrgb("#00de0000") + assert (0, 0, 222, 0) == ImageColor.getrgb("#0000de00") + assert (0, 0, 0, 222) == ImageColor.getrgb("#000000de") + + # case insensitivity + assert ImageColor.getrgb("#DEF") == ImageColor.getrgb("#def") + assert ImageColor.getrgb("#CDEF") == ImageColor.getrgb("#cdef") + assert ImageColor.getrgb("#DEFDEF") == ImageColor.getrgb("#defdef") + assert ImageColor.getrgb("#CDEFCDEF") == ImageColor.getrgb("#cdefcdef") + + # not a number + with pytest.raises(ValueError): + ImageColor.getrgb("#fo0") + with pytest.raises(ValueError): + ImageColor.getrgb("#fo00") + with pytest.raises(ValueError): + ImageColor.getrgb("#fo0000") + with pytest.raises(ValueError): + ImageColor.getrgb("#fo000000") + + # wrong number of components + with pytest.raises(ValueError): + ImageColor.getrgb("#f0000") + with pytest.raises(ValueError): + ImageColor.getrgb("#f000000") + with pytest.raises(ValueError): + ImageColor.getrgb("#f00000000") + with pytest.raises(ValueError): + ImageColor.getrgb("#f000000000") + with pytest.raises(ValueError): + ImageColor.getrgb("#f00000 ") + + +def test_colormap() -> None: + assert (0, 0, 0) == ImageColor.getrgb("black") + assert (255, 255, 255) == ImageColor.getrgb("white") + assert (255, 255, 255) == ImageColor.getrgb("WHITE") + + with pytest.raises(ValueError): + ImageColor.getrgb("black ") + + +def test_functions() -> None: + # rgb numbers + assert (255, 0, 0) == ImageColor.getrgb("rgb(255,0,0)") + assert (0, 255, 0) == ImageColor.getrgb("rgb(0,255,0)") + assert (0, 0, 255) == ImageColor.getrgb("rgb(0,0,255)") + + # percents + assert (255, 0, 0) == ImageColor.getrgb("rgb(100%,0%,0%)") + assert (0, 255, 0) == ImageColor.getrgb("rgb(0%,100%,0%)") + assert (0, 0, 255) == ImageColor.getrgb("rgb(0%,0%,100%)") + + # rgba numbers + assert (255, 0, 0, 0) == ImageColor.getrgb("rgba(255,0,0,0)") + assert (0, 255, 0, 0) == ImageColor.getrgb("rgba(0,255,0,0)") + assert (0, 0, 255, 0) == ImageColor.getrgb("rgba(0,0,255,0)") + assert (0, 0, 0, 255) == ImageColor.getrgb("rgba(0,0,0,255)") + + assert (255, 0, 0) == ImageColor.getrgb("hsl(0,100%,50%)") + assert (255, 0, 0) == ImageColor.getrgb("hsl(360,100%,50%)") + assert (0, 255, 255) == ImageColor.getrgb("hsl(180,100%,50%)") + + assert (255, 0, 0) == ImageColor.getrgb("hsv(0,100%,100%)") + assert (255, 0, 0) == ImageColor.getrgb("hsv(360,100%,100%)") + assert (0, 255, 255) == ImageColor.getrgb("hsv(180,100%,100%)") + + # alternate format + assert ImageColor.getrgb("hsb(0,100%,50%)") == ImageColor.getrgb("hsv(0,100%,50%)") + + # floats + assert (254, 3, 3) == ImageColor.getrgb("hsl(0.1,99.2%,50.3%)") + assert (255, 0, 0) == ImageColor.getrgb("hsl(360.,100.0%,50%)") + + assert (253, 2, 2) == ImageColor.getrgb("hsv(0.1,99.2%,99.3%)") + assert (255, 0, 0) == ImageColor.getrgb("hsv(360.,100.0%,100%)") + + # case insensitivity + assert ImageColor.getrgb("RGB(255,0,0)") == ImageColor.getrgb("rgb(255,0,0)") + assert ImageColor.getrgb("RGB(100%,0%,0%)") == ImageColor.getrgb("rgb(100%,0%,0%)") + assert ImageColor.getrgb("RGBA(255,0,0,0)") == ImageColor.getrgb("rgba(255,0,0,0)") + assert ImageColor.getrgb("HSL(0,100%,50%)") == ImageColor.getrgb("hsl(0,100%,50%)") + assert ImageColor.getrgb("HSV(0,100%,50%)") == ImageColor.getrgb("hsv(0,100%,50%)") + assert ImageColor.getrgb("HSB(0,100%,50%)") == ImageColor.getrgb("hsb(0,100%,50%)") + + # space agnosticism + assert (255, 0, 0) == ImageColor.getrgb("rgb( 255 , 0 , 0 )") + assert (255, 0, 0) == ImageColor.getrgb("rgb( 100% , 0% , 0% )") + assert (255, 0, 0, 0) == ImageColor.getrgb("rgba( 255 , 0 , 0 , 0 )") + assert (255, 0, 0) == ImageColor.getrgb("hsl( 0 , 100% , 50% )") + assert (255, 0, 0) == ImageColor.getrgb("hsv( 0 , 100% , 100% )") + + # wrong number of components + with pytest.raises(ValueError): + ImageColor.getrgb("rgb(255,0)") + with pytest.raises(ValueError): + ImageColor.getrgb("rgb(255,0,0,0)") + + with pytest.raises(ValueError): + ImageColor.getrgb("rgb(100%,0%)") + with pytest.raises(ValueError): + ImageColor.getrgb("rgb(100%,0%,0)") + with pytest.raises(ValueError): + ImageColor.getrgb("rgb(100%,0%,0 %)") + with pytest.raises(ValueError): + ImageColor.getrgb("rgb(100%,0%,0%,0%)") + + with pytest.raises(ValueError): + ImageColor.getrgb("rgba(255,0,0)") + with pytest.raises(ValueError): + ImageColor.getrgb("rgba(255,0,0,0,0)") + + with pytest.raises(ValueError): + ImageColor.getrgb("hsl(0,100%)") + with pytest.raises(ValueError): + ImageColor.getrgb("hsl(0,100%,0%,0%)") + with pytest.raises(ValueError): + ImageColor.getrgb("hsl(0%,100%,50%)") + with pytest.raises(ValueError): + ImageColor.getrgb("hsl(0,100,50%)") + with pytest.raises(ValueError): + ImageColor.getrgb("hsl(0,100%,50)") + + with pytest.raises(ValueError): + ImageColor.getrgb("hsv(0,100%)") + with pytest.raises(ValueError): + ImageColor.getrgb("hsv(0,100%,0%,0%)") + with pytest.raises(ValueError): + ImageColor.getrgb("hsv(0%,100%,50%)") + with pytest.raises(ValueError): + ImageColor.getrgb("hsv(0,100,50%)") + with pytest.raises(ValueError): + ImageColor.getrgb("hsv(0,100%,50)") + + +# look for rounding errors (based on code by Tim Hatch) +def test_rounding_errors() -> None: + for color in ImageColor.colormap: + expected = Image.new("RGB", (1, 1), color).convert("L").getpixel((0, 0)) + actual = ImageColor.getcolor(color, "L") + assert expected == actual + + assert (0, 255, 115) == ImageColor.getcolor("rgba(0, 255, 115, 33)", "RGB") + Image.new("RGB", (1, 1), "white") + + assert (0, 0, 0, 255) == ImageColor.getcolor("black", "RGBA") + assert (255, 255, 255, 255) == ImageColor.getcolor("white", "RGBA") + assert (0, 255, 115, 33) == ImageColor.getcolor("rgba(0, 255, 115, 33)", "RGBA") + Image.new("RGBA", (1, 1), "white") + + assert 0 == ImageColor.getcolor("black", "L") + assert 255 == ImageColor.getcolor("white", "L") + assert 163 == ImageColor.getcolor("rgba(0, 255, 115, 33)", "L") + Image.new("L", (1, 1), "white") + + assert 0 == ImageColor.getcolor("black", "1") + assert 255 == ImageColor.getcolor("white", "1") + # The following test is wrong, but is current behavior + # The correct result should be 255 due to the mode 1 + assert 163 == ImageColor.getcolor("rgba(0, 255, 115, 33)", "1") + # Correct behavior + # assert + # 255, ImageColor.getcolor("rgba(0, 255, 115, 33)", "1")) + Image.new("1", (1, 1), "white") + + assert (0, 255) == ImageColor.getcolor("black", "LA") + assert (255, 255) == ImageColor.getcolor("white", "LA") + assert (163, 33) == ImageColor.getcolor("rgba(0, 255, 115, 33)", "LA") + Image.new("LA", (1, 1), "white") + + +def test_color_hsv() -> None: + assert (170, 255, 255) == ImageColor.getcolor("hsv(240, 100%, 100%)", "HSV") + + +def test_color_too_long() -> None: + # Arrange + color_too_long = "hsl(" + "1" * 40 + "," + "1" * 40 + "%," + "1" * 40 + "%)" + + # Act / Assert + with pytest.raises(ValueError): + ImageColor.getrgb(color_too_long) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 61d7b5c6a..202e5b18f 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1,1631 +1,1631 @@ -from __future__ import annotations - -import contextlib -import os.path -from typing import Sequence - -import pytest - -from PIL import Image, ImageColor, ImageDraw, ImageFont, features -from PIL._typing import Coords - -from .helper import ( - assert_image_equal, - assert_image_equal_tofile, - assert_image_similar_tofile, - hopper, - skip_unless_feature, -) - -BLACK = (0, 0, 0) -WHITE = (255, 255, 255) -GRAY = (190, 190, 190) -DEFAULT_MODE = "RGB" -IMAGES_PATH = os.path.join("Tests", "images", "imagedraw") - -# Image size -W, H = 100, 100 - -# Bounding box points -X0 = int(W / 4) -X1 = int(X0 * 3) -Y0 = int(H / 4) -Y1 = int(X0 * 3) - -# Bounding boxes -BBOX = (((X0, Y0), (X1, Y1)), [(X0, Y0), (X1, Y1)], (X0, Y0, X1, Y1), [X0, Y0, X1, Y1]) - -# Coordinate sequences -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], -) - -KITE_POINTS = ( - ((10, 50), (70, 10), (90, 50), (70, 90), (10, 50)), - [(10, 50), (70, 10), (90, 50), (70, 90), (10, 50)], -) - - -def test_sanity() -> None: - im = hopper("RGB").copy() - - draw = ImageDraw.ImageDraw(im) - draw = ImageDraw.Draw(im) - - draw.ellipse(list(range(4))) - draw.line(list(range(10))) - draw.polygon(list(range(100))) - draw.rectangle(list(range(4))) - - -def test_valueerror() -> None: - with Image.open("Tests/images/chi.gif") as im: - draw = ImageDraw.Draw(im) - draw.line((0, 0), fill=(0, 0, 0)) - - -def test_mode_mismatch() -> None: - im = hopper("RGB").copy() - - with pytest.raises(ValueError): - ImageDraw.ImageDraw(im, mode="L") - - -@pytest.mark.parametrize("bbox", BBOX) -@pytest.mark.parametrize("start, end", ((0, 180), (0.5, 180.4))) -def test_arc(bbox: Coords, start: float, end: float) -> None: - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - - # Act - draw.arc(bbox, start, end) - - # Assert - assert_image_similar_tofile(im, "Tests/images/imagedraw_arc.png", 1) - - -@pytest.mark.parametrize("bbox", BBOX) -def test_arc_end_le_start(bbox: Coords) -> None: - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - start = 270.5 - end = 0 - - # Act - draw.arc(bbox, start=start, end=end) - - # Assert - assert_image_equal_tofile(im, "Tests/images/imagedraw_arc_end_le_start.png") - - -@pytest.mark.parametrize("bbox", BBOX) -def test_arc_no_loops(bbox: Coords) -> None: - # No need to go in loops - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - start = 5 - end = 370 - - # Act - draw.arc(bbox, start=start, end=end) - - # Assert - assert_image_similar_tofile(im, "Tests/images/imagedraw_arc_no_loops.png", 1) - - -@pytest.mark.parametrize("bbox", BBOX) -def test_arc_width(bbox: Coords) -> None: - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - - # Act - draw.arc(bbox, 10, 260, width=5) - - # Assert - assert_image_similar_tofile(im, "Tests/images/imagedraw_arc_width.png", 1) - - -@pytest.mark.parametrize("bbox", BBOX) -def test_arc_width_pieslice_large(bbox: Coords) -> None: - # Tests an arc with a large enough width that it is a pieslice - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - - # Act - draw.arc(bbox, 10, 260, fill="yellow", width=100) - - # Assert - assert_image_similar_tofile(im, "Tests/images/imagedraw_arc_width_pieslice.png", 1) - - -@pytest.mark.parametrize("bbox", BBOX) -def test_arc_width_fill(bbox: Coords) -> None: - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - - # Act - draw.arc(bbox, 10, 260, fill="yellow", width=5) - - # Assert - assert_image_similar_tofile(im, "Tests/images/imagedraw_arc_width_fill.png", 1) - - -@pytest.mark.parametrize("bbox", BBOX) -def test_arc_width_non_whole_angle(bbox: Coords) -> None: - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_arc_width_non_whole_angle.png" - - # Act - draw.arc(bbox, 10, 259.5, width=5) - - # Assert - assert_image_similar_tofile(im, expected, 1) - - -def test_arc_high() -> None: - # Arrange - im = Image.new("RGB", (200, 200)) - draw = ImageDraw.Draw(im) - - # Act - draw.arc([10, 10, 89, 189], 20, 330, width=20, fill="white") - draw.arc([110, 10, 189, 189], 20, 150, width=20, fill="white") - - # Assert - assert_image_equal_tofile(im, "Tests/images/imagedraw_arc_high.png") - - -def test_bitmap() -> None: - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - with Image.open("Tests/images/pil123rgba.png") as small: - small = small.resize((50, 50), Image.Resampling.NEAREST) - - # Act - draw.bitmap((10, 10), small) - - # Assert - assert_image_equal_tofile(im, "Tests/images/imagedraw_bitmap.png") - - -@pytest.mark.parametrize("mode", ("RGB", "L")) -@pytest.mark.parametrize("bbox", BBOX) -def test_chord(mode: str, bbox: Coords) -> None: - # Arrange - im = Image.new(mode, (W, H)) - draw = ImageDraw.Draw(im) - expected = f"Tests/images/imagedraw_chord_{mode}.png" - - # Act - draw.chord(bbox, 0, 180, fill="red", outline="yellow") - - # Assert - assert_image_similar_tofile(im, expected, 1) - - -@pytest.mark.parametrize("bbox", BBOX) -def test_chord_width(bbox: Coords) -> None: - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - - # Act - draw.chord(bbox, 10, 260, outline="yellow", width=5) - - # Assert - assert_image_similar_tofile(im, "Tests/images/imagedraw_chord_width.png", 1) - - -@pytest.mark.parametrize("bbox", BBOX) -def test_chord_width_fill(bbox: Coords) -> None: - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - - # Act - draw.chord(bbox, 10, 260, fill="red", outline="yellow", width=5) - - # Assert - assert_image_similar_tofile(im, "Tests/images/imagedraw_chord_width_fill.png", 1) - - -@pytest.mark.parametrize("bbox", BBOX) -def test_chord_zero_width(bbox: Coords) -> None: - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - - # Act - draw.chord(bbox, 10, 260, fill="red", outline="yellow", width=0) - - # Assert - assert_image_equal_tofile(im, "Tests/images/imagedraw_chord_zero_width.png") - - -def test_chord_too_fat() -> None: - # Arrange - im = Image.new("RGB", (100, 100)) - draw = ImageDraw.Draw(im) - - # Act - draw.chord([-150, -150, 99, 99], 15, 60, width=10, fill="white", outline="red") - - # Assert - assert_image_equal_tofile(im, "Tests/images/imagedraw_chord_too_fat.png") - - -@pytest.mark.parametrize("mode", ("RGB", "L")) -@pytest.mark.parametrize("xy", ((W / 2, H / 2), [W / 2, H / 2])) -def test_circle(mode: str, xy: Sequence[float]) -> None: - # Arrange - im = Image.new(mode, (W, H)) - draw = ImageDraw.Draw(im) - expected = f"Tests/images/imagedraw_ellipse_{mode}.png" - - # Act - draw.circle(xy, 25, fill="green", outline="blue") - - # Assert - assert_image_similar_tofile(im, expected, 1) - - -@pytest.mark.parametrize("mode", ("RGB", "L")) -@pytest.mark.parametrize("bbox", BBOX) -def test_ellipse(mode: str, bbox: Coords) -> None: - # Arrange - im = Image.new(mode, (W, H)) - draw = ImageDraw.Draw(im) - expected = f"Tests/images/imagedraw_ellipse_{mode}.png" - - # Act - draw.ellipse(bbox, fill="green", outline="blue") - - # Assert - assert_image_similar_tofile(im, expected, 1) - - -@pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_translucent(bbox: Coords) -> None: - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im, "RGBA") - - # Act - draw.ellipse(bbox, fill=(0, 255, 0, 127)) - - # Assert - expected = "Tests/images/imagedraw_ellipse_translucent.png" - assert_image_similar_tofile(im, expected, 1) - - -def test_ellipse_edge() -> None: - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - - # Act - draw.ellipse(((0, 0), (W - 1, H - 1)), fill="white") - - # Assert - assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_edge.png", 1) - - -def test_ellipse_symmetric() -> None: - for width, bbox in ( - (100, (24, 24, 75, 75)), - (101, (25, 25, 75, 75)), - ): - im = Image.new("RGB", (width, 100)) - draw = ImageDraw.Draw(im) - draw.ellipse(bbox, fill="green", outline="blue") - assert_image_equal(im, im.transpose(Image.Transpose.FLIP_LEFT_RIGHT)) - - -@pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_width(bbox: Coords) -> None: - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - - # Act - draw.ellipse(bbox, outline="blue", width=5) - - # Assert - assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_width.png", 1) - - -def test_ellipse_width_large() -> None: - # Arrange - im = Image.new("RGB", (500, 500)) - draw = ImageDraw.Draw(im) - - # Act - draw.ellipse((25, 25, 475, 475), outline="blue", width=75) - - # Assert - assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_width_large.png", 1) - - -@pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_width_fill(bbox: Coords) -> None: - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - - # Act - draw.ellipse(bbox, fill="green", outline="blue", width=5) - - # Assert - assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_width_fill.png", 1) - - -@pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_zero_width(bbox: Coords) -> None: - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - - # Act - draw.ellipse(bbox, fill="green", outline="blue", width=0) - - # Assert - assert_image_equal_tofile(im, "Tests/images/imagedraw_ellipse_zero_width.png") - - -def ellipse_various_sizes_helper(filled: bool) -> Image.Image: - ellipse_sizes = range(32) - image_size = sum(ellipse_sizes) + len(ellipse_sizes) + 1 - im = Image.new("RGB", (image_size, image_size)) - draw = ImageDraw.Draw(im) - - x = 1 - for w in ellipse_sizes: - y = 1 - for h in ellipse_sizes: - x1 = x + w - if w: - x1 -= 1 - y1 = y + h - if h: - y1 -= 1 - border = [x, y, x1, y1] - if filled: - draw.ellipse(border, fill="white") - else: - draw.ellipse(border, outline="white") - y += h + 1 - x += w + 1 - - return im - - -def test_ellipse_various_sizes() -> None: - im = ellipse_various_sizes_helper(False) - - assert_image_equal_tofile(im, "Tests/images/imagedraw_ellipse_various_sizes.png") - - -def test_ellipse_various_sizes_filled() -> None: - im = ellipse_various_sizes_helper(True) - - assert_image_equal_tofile( - im, "Tests/images/imagedraw_ellipse_various_sizes_filled.png" - ) - - -@pytest.mark.parametrize("points", POINTS) -def test_line(points: Coords) -> None: - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - - # Act - draw.line(points, fill="yellow", width=2) - - # Assert - assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png") - - -def test_shape1() -> None: - # Arrange - im = Image.new("RGB", (100, 100), "white") - draw = ImageDraw.Draw(im) - x0, y0 = 5, 5 - x1, y1 = 5, 50 - x2, y2 = 95, 50 - x3, y3 = 95, 5 - - # Act - s = ImageDraw.Outline() - s.move(x0, y0) - s.curve(x1, y1, x2, y2, x3, y3) - s.line(x0, y0) - - draw.shape(s, fill=1) - - # Assert - assert_image_equal_tofile(im, "Tests/images/imagedraw_shape1.png") - - -def test_shape2() -> None: - # Arrange - im = Image.new("RGB", (100, 100), "white") - draw = ImageDraw.Draw(im) - x0, y0 = 95, 95 - x1, y1 = 95, 50 - x2, y2 = 5, 50 - x3, y3 = 5, 95 - - # Act - s = ImageDraw.Outline() - s.move(x0, y0) - s.curve(x1, y1, x2, y2, x3, y3) - s.line(x0, y0) - - draw.shape(s, outline="blue") - - # Assert - assert_image_equal_tofile(im, "Tests/images/imagedraw_shape2.png") - - -def test_transform() -> None: - # Arrange - im = Image.new("RGB", (100, 100), "white") - expected = im.copy() - draw = ImageDraw.Draw(im) - - # Act - s = ImageDraw.Outline() - s.line(0, 0) - s.transform((0, 0, 0, 0, 0, 0)) - - draw.shape(s, fill=1) - - # Assert - assert_image_equal(im, expected) - - -@pytest.mark.parametrize("bbox", BBOX) -@pytest.mark.parametrize("start, end", ((-92, 46), (-92.2, 46.2))) -def test_pieslice(bbox: Coords, start: float, end: float) -> None: - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - - # Act - draw.pieslice(bbox, start, end, fill="white", outline="blue") - - # Assert - assert_image_similar_tofile(im, "Tests/images/imagedraw_pieslice.png", 1) - - -@pytest.mark.parametrize("bbox", BBOX) -def test_pieslice_width(bbox: Coords) -> None: - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - - # Act - draw.pieslice(bbox, 10, 260, outline="blue", width=5) - - # Assert - assert_image_similar_tofile(im, "Tests/images/imagedraw_pieslice_width.png", 1) - - -@pytest.mark.parametrize("bbox", BBOX) -def test_pieslice_width_fill(bbox: Coords) -> None: - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_pieslice_width_fill.png" - - # Act - draw.pieslice(bbox, 10, 260, fill="white", outline="blue", width=5) - - # Assert - assert_image_similar_tofile(im, expected, 1) - - -@pytest.mark.parametrize("bbox", BBOX) -def test_pieslice_zero_width(bbox: Coords) -> None: - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - - # Act - draw.pieslice(bbox, 10, 260, fill="white", outline="blue", width=0) - - # Assert - assert_image_equal_tofile(im, "Tests/images/imagedraw_pieslice_zero_width.png") - - -def test_pieslice_wide() -> None: - # Arrange - im = Image.new("RGB", (200, 100)) - draw = ImageDraw.Draw(im) - - # Act - draw.pieslice([0, 0, 199, 99], 190, 170, width=10, fill="white", outline="red") - - # Assert - assert_image_equal_tofile(im, "Tests/images/imagedraw_pieslice_wide.png") - - -def test_pieslice_no_spikes() -> None: - im = Image.new("RGB", (161, 161), "white") - draw = ImageDraw.Draw(im) - cxs = ( - [140] * 3 - + list(range(140, 19, -20)) - + [20] * 5 - + list(range(20, 141, 20)) - + [140] * 2 - ) - cys = ( - list(range(80, 141, 20)) - + [140] * 5 - + list(range(140, 19, -20)) - + [20] * 5 - + list(range(20, 80, 20)) - ) - - for cx, cy, angle in zip(cxs, cys, range(0, 360, 15)): - draw.pieslice( - [cx - 100, cy - 100, cx + 100, cy + 100], angle, angle + 1, fill="black" - ) - draw.point([cx, cy], fill="red") - - im_pre_erase = im.copy() - draw.rectangle([21, 21, 139, 139], fill="white") - - assert_image_equal(im, im_pre_erase) - - -@pytest.mark.parametrize("points", POINTS) -def test_point(points: Coords) -> None: - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - - # Act - draw.point(points, fill="yellow") - - # Assert - assert_image_equal_tofile(im, "Tests/images/imagedraw_point.png") - - -def test_point_I16() -> None: - # Arrange - im = Image.new("I;16", (1, 1)) - draw = ImageDraw.Draw(im) - - # Act - draw.point((0, 0), fill=0x1234) - - # Assert - assert im.getpixel((0, 0)) == 0x1234 - - -@pytest.mark.parametrize("points", POINTS) -def test_polygon(points: Coords) -> None: - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - - # Act - draw.polygon(points, fill="red", outline="blue") - - # Assert - assert_image_equal_tofile(im, "Tests/images/imagedraw_polygon.png") - - -@pytest.mark.parametrize("mode", ("RGB", "L")) -@pytest.mark.parametrize("kite_points", KITE_POINTS) -def test_polygon_kite( - mode: str, kite_points: tuple[tuple[int, int], ...] | list[tuple[int, int]] -) -> None: - # Test drawing lines of different gradients (dx>dy, dy>dx) and - # vertical (dx==0) and horizontal (dy==0) lines - # Arrange - im = Image.new(mode, (W, H)) - draw = ImageDraw.Draw(im) - expected = f"Tests/images/imagedraw_polygon_kite_{mode}.png" - - # Act - draw.polygon(kite_points, fill="blue", outline="yellow") - - # Assert - assert_image_equal_tofile(im, expected) - - -def test_polygon_1px_high() -> None: - # Test drawing a 1px high polygon - # Arrange - im = Image.new("RGB", (3, 3)) - draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_polygon_1px_high.png" - - # Act - draw.polygon([(0, 1), (0, 1), (2, 1), (2, 1)], "#f00") - - # Assert - assert_image_equal_tofile(im, expected) - - -def test_polygon_1px_high_translucent() -> None: - # Test drawing a translucent 1px high polygon - # Arrange - im = Image.new("RGB", (4, 3)) - draw = ImageDraw.Draw(im, "RGBA") - expected = "Tests/images/imagedraw_polygon_1px_high_translucent.png" - - # Act - draw.polygon([(1, 1), (1, 1), (3, 1), (3, 1)], (255, 0, 0, 127)) - - # Assert - assert_image_equal_tofile(im, expected) - - -def test_polygon_translucent() -> None: - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im, "RGBA") - - # Act - draw.polygon([(20, 80), (80, 80), (80, 20)], fill=(0, 255, 0, 127)) - - # Assert - expected = "Tests/images/imagedraw_polygon_translucent.png" - assert_image_equal_tofile(im, expected) - - -@pytest.mark.parametrize("bbox", BBOX) -def test_rectangle(bbox: Coords) -> None: - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - - # Act - draw.rectangle(bbox, fill="black", outline="green") - - # Assert - assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle.png") - - -def test_big_rectangle() -> None: - # Test drawing a rectangle bigger than the image - # Arrange - im = Image.new("RGB", (W, H)) - bbox = [(-1, -1), (W + 1, H + 1)] - draw = ImageDraw.Draw(im) - - # Act - draw.rectangle(bbox, fill="orange") - - # Assert - assert_image_similar_tofile(im, "Tests/images/imagedraw_big_rectangle.png", 1) - - -@pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_width(bbox: Coords) -> None: - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_rectangle_width.png" - - # Act - draw.rectangle(bbox, outline="green", width=5) - - # Assert - assert_image_equal_tofile(im, expected) - - -@pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_width_fill(bbox: Coords) -> None: - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_rectangle_width_fill.png" - - # Act - draw.rectangle(bbox, fill="blue", outline="green", width=5) - - # Assert - assert_image_equal_tofile(im, expected) - - -@pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_zero_width(bbox: Coords) -> None: - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - - # Act - draw.rectangle(bbox, fill="blue", outline="green", width=0) - - # Assert - assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle_zero_width.png") - - -@pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_I16(bbox: Coords) -> None: - # Arrange - im = Image.new("I;16", (W, H)) - draw = ImageDraw.Draw(im) - - # Act - draw.rectangle(bbox, outline=0xFFFF) - - # Assert - assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle_I.tiff") - - -@pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_translucent_outline(bbox: Coords) -> None: - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im, "RGBA") - - # Act - draw.rectangle(bbox, fill="black", outline=(0, 255, 0, 127), width=5) - - # Assert - assert_image_equal_tofile( - im, "Tests/images/imagedraw_rectangle_translucent_outline.png" - ) - - -@pytest.mark.parametrize( - "xy", - [(10, 20, 190, 180), ([10, 20], [190, 180]), ((10, 20), (190, 180))], -) -def test_rounded_rectangle( - xy: ( - tuple[int, int, int, int] - | tuple[list[int]] - | tuple[tuple[int, int], tuple[int, int]] - ) -) -> None: - # Arrange - im = Image.new("RGB", (200, 200)) - draw = ImageDraw.Draw(im) - - # Act - draw.rounded_rectangle(xy, 30, fill="red", outline="green", width=5) - - # Assert - assert_image_equal_tofile(im, "Tests/images/imagedraw_rounded_rectangle.png") - - -@pytest.mark.parametrize("top_left", (True, False)) -@pytest.mark.parametrize("top_right", (True, False)) -@pytest.mark.parametrize("bottom_right", (True, False)) -@pytest.mark.parametrize("bottom_left", (True, False)) -def test_rounded_rectangle_corners( - top_left: bool, top_right: bool, bottom_right: bool, bottom_left: bool -) -> None: - corners = (top_left, top_right, bottom_right, bottom_left) - - # Arrange - im = Image.new("RGB", (200, 200)) - draw = ImageDraw.Draw(im) - - # Act - draw.rounded_rectangle( - (10, 20, 190, 180), 30, fill="red", outline="green", width=5, corners=corners - ) - - # Assert - suffix = "".join( - ( - ("y" if top_left else "n"), - ("y" if top_right else "n"), - ("y" if bottom_right else "n"), - ("y" if bottom_left else "n"), - ) - ) - assert_image_equal_tofile( - im, "Tests/images/imagedraw_rounded_rectangle_corners_" + suffix + ".png" - ) - - -@pytest.mark.parametrize( - "xy, radius, type", - [ - ((10, 20, 190, 180), 30.5, "given"), - ((10, 10, 181, 190), 90, "width"), - ((10, 20, 190, 181), 85, "height"), - ], -) -def test_rounded_rectangle_non_integer_radius( - xy: tuple[int, int, int, int], radius: float, type: str -) -> None: - # Arrange - im = Image.new("RGB", (200, 200)) - draw = ImageDraw.Draw(im) - - # Act - draw.rounded_rectangle(xy, radius, fill="red", outline="green", width=5) - - # Assert - assert_image_equal_tofile( - im, - "Tests/images/imagedraw_rounded_rectangle_non_integer_radius_" + type + ".png", - ) - - -@pytest.mark.parametrize("bbox", BBOX) -def test_rounded_rectangle_zero_radius(bbox: Coords) -> None: - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - - # Act - draw.rounded_rectangle(bbox, 0, fill="blue", outline="green", width=5) - - # Assert - assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle_width_fill.png") - - -@pytest.mark.parametrize( - "xy, suffix", - [ - ((20, 10, 80, 90), "x"), - ((20, 10, 81, 90), "x_odd"), - ((20, 10, 81.1, 90), "x_odd"), - ((10, 20, 90, 80), "y"), - ((10, 20, 90, 81), "y_odd"), - ((10, 20, 90, 81.1), "y_odd"), - ((20, 20, 80, 80), "both"), - ], -) -def test_rounded_rectangle_translucent( - xy: tuple[int, int, int, int], suffix: str -) -> None: - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im, "RGBA") - - # Act - draw.rounded_rectangle( - xy, 30, fill=(255, 0, 0, 127), outline=(0, 255, 0, 127), width=5 - ) - - # Assert - assert_image_equal_tofile( - im, "Tests/images/imagedraw_rounded_rectangle_" + suffix + ".png" - ) - - -@pytest.mark.parametrize("bbox", BBOX) -def test_floodfill(bbox: Coords) -> None: - red = ImageColor.getrgb("red") - - for mode, value in [("L", 1), ("RGBA", (255, 0, 0, 0)), ("RGB", red)]: - # Arrange - im = Image.new(mode, (W, H)) - draw = ImageDraw.Draw(im) - draw.rectangle(bbox, outline="yellow", fill="green") - centre_point = (int(W / 2), int(H / 2)) - - # Act - ImageDraw.floodfill(im, centre_point, value) - - # Assert - expected = "Tests/images/imagedraw_floodfill_" + mode + ".png" - with Image.open(expected) as im_floodfill: - assert_image_equal(im, im_floodfill) - - # Test that using the same colour does not change the image - ImageDraw.floodfill(im, centre_point, red) - assert_image_equal(im, im_floodfill) - - # Test that filling outside the image does not change the image - ImageDraw.floodfill(im, (W, H), red) - assert_image_equal(im, im_floodfill) - - # Test filling at the edge of an image - im = Image.new("RGB", (1, 1)) - ImageDraw.floodfill(im, (0, 0), red) - assert_image_equal(im, Image.new("RGB", (1, 1), red)) - - -@pytest.mark.parametrize("bbox", BBOX) -def test_floodfill_border(bbox: Coords) -> None: - # floodfill() is experimental - - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - draw.rectangle(bbox, outline="yellow", fill="green") - centre_point = (int(W / 2), int(H / 2)) - - # Act - ImageDraw.floodfill( - im, - centre_point, - ImageColor.getrgb("red"), - border=ImageColor.getrgb("black"), - ) - - # Assert - assert_image_equal_tofile(im, "Tests/images/imagedraw_floodfill2.png") - - -@pytest.mark.parametrize("bbox", BBOX) -def test_floodfill_thresh(bbox: Coords) -> None: - # floodfill() is experimental - - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - draw.rectangle(bbox, outline="darkgreen", fill="green") - centre_point = (int(W / 2), int(H / 2)) - - # Act - ImageDraw.floodfill(im, centre_point, ImageColor.getrgb("red"), thresh=30) - - # Assert - assert_image_equal_tofile(im, "Tests/images/imagedraw_floodfill2.png") - - -def test_floodfill_not_negative() -> None: - # floodfill() is experimental - # Test that floodfill does not extend into negative coordinates - - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - draw.line((W / 2, 0, W / 2, H / 2), fill="green") - draw.line((0, H / 2, W / 2, H / 2), fill="green") - - # Act - ImageDraw.floodfill(im, (int(W / 4), int(H / 4)), ImageColor.getrgb("red")) - - # Assert - assert_image_equal_tofile(im, "Tests/images/imagedraw_floodfill_not_negative.png") - - -def create_base_image_draw( - size: tuple[int, int], - mode: str = DEFAULT_MODE, - background1: tuple[int, int, int] = WHITE, - 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]): - if (x + y) % 2 == 0: - img.putpixel((x, y), background2) - return img, ImageDraw.Draw(img) - - -def test_square() -> None: - expected = os.path.join(IMAGES_PATH, "square.png") - img, draw = create_base_image_draw((10, 10)) - draw.polygon([(2, 2), (2, 7), (7, 7), (7, 2)], BLACK) - assert_image_equal_tofile(img, expected, "square as normal polygon failed") - img, draw = create_base_image_draw((10, 10)) - draw.polygon([(7, 7), (7, 2), (2, 2), (2, 7)], BLACK) - assert_image_equal_tofile(img, expected, "square as inverted polygon failed") - img, draw = create_base_image_draw((10, 10)) - draw.rectangle((2, 2, 7, 7), BLACK) - assert_image_equal_tofile(img, expected, "square as normal rectangle failed") - - -def test_triangle_right() -> None: - img, draw = create_base_image_draw((20, 20)) - draw.polygon([(3, 5), (17, 5), (10, 12)], BLACK) - assert_image_equal_tofile( - img, os.path.join(IMAGES_PATH, "triangle_right.png"), "triangle right failed" - ) - - -@pytest.mark.parametrize( - "fill, suffix", - ((BLACK, "width"), (None, "width_no_fill")), -) -def test_triangle_right_width(fill: tuple[int, int, int] | None, suffix: str) -> None: - img, draw = create_base_image_draw((100, 100)) - draw.polygon([(15, 25), (85, 25), (50, 60)], fill, WHITE, width=5) - assert_image_equal_tofile( - img, os.path.join(IMAGES_PATH, "triangle_right_" + suffix + ".png") - ) - - -def test_line_horizontal() -> None: - img, draw = create_base_image_draw((20, 20)) - draw.line((5, 5, 14, 5), BLACK, 2) - assert_image_equal_tofile( - img, - os.path.join(IMAGES_PATH, "line_horizontal_w2px_normal.png"), - "line straight horizontal normal 2px wide failed", - ) - - img, draw = create_base_image_draw((20, 20)) - draw.line((14, 5, 5, 5), BLACK, 2) - assert_image_equal_tofile( - img, - os.path.join(IMAGES_PATH, "line_horizontal_w2px_inverted.png"), - "line straight horizontal inverted 2px wide failed", - ) - - expected = os.path.join(IMAGES_PATH, "line_horizontal_w3px.png") - img, draw = create_base_image_draw((20, 20)) - draw.line((5, 5, 14, 5), BLACK, 3) - assert_image_equal_tofile( - img, expected, "line straight horizontal normal 3px wide failed" - ) - img, draw = create_base_image_draw((20, 20)) - draw.line((14, 5, 5, 5), BLACK, 3) - assert_image_equal_tofile( - img, expected, "line straight horizontal inverted 3px wide failed" - ) - - img, draw = create_base_image_draw((200, 110)) - draw.line((5, 55, 195, 55), BLACK, 101) - assert_image_equal_tofile( - img, - os.path.join(IMAGES_PATH, "line_horizontal_w101px.png"), - "line straight horizontal 101px wide failed", - ) - - -@pytest.mark.xfail(reason="failing test") -def test_line_h_s1_w2() -> None: - img, draw = create_base_image_draw((20, 20)) - draw.line((5, 5, 14, 6), BLACK, 2) - assert_image_equal_tofile( - img, - os.path.join(IMAGES_PATH, "line_horizontal_slope1px_w2px.png"), - "line horizontal 1px slope 2px wide failed", - ) - - -def test_line_vertical() -> None: - img, draw = create_base_image_draw((20, 20)) - draw.line((5, 5, 5, 14), BLACK, 2) - assert_image_equal_tofile( - img, - os.path.join(IMAGES_PATH, "line_vertical_w2px_normal.png"), - "line straight vertical normal 2px wide failed", - ) - - img, draw = create_base_image_draw((20, 20)) - draw.line((5, 14, 5, 5), BLACK, 2) - assert_image_equal_tofile( - img, - os.path.join(IMAGES_PATH, "line_vertical_w2px_inverted.png"), - "line straight vertical inverted 2px wide failed", - ) - - expected = os.path.join(IMAGES_PATH, "line_vertical_w3px.png") - img, draw = create_base_image_draw((20, 20)) - draw.line((5, 5, 5, 14), BLACK, 3) - assert_image_equal_tofile( - img, expected, "line straight vertical normal 3px wide failed" - ) - img, draw = create_base_image_draw((20, 20)) - draw.line((5, 14, 5, 5), BLACK, 3) - assert_image_equal_tofile( - img, expected, "line straight vertical inverted 3px wide failed" - ) - - img, draw = create_base_image_draw((110, 200)) - draw.line((55, 5, 55, 195), BLACK, 101) - assert_image_equal_tofile( - img, - os.path.join(IMAGES_PATH, "line_vertical_w101px.png"), - "line straight vertical 101px wide failed", - ) - - img, draw = create_base_image_draw((20, 20)) - draw.line((5, 5, 6, 14), BLACK, 2) - assert_image_equal_tofile( - img, - os.path.join(IMAGES_PATH, "line_vertical_slope1px_w2px.png"), - "line vertical 1px slope 2px wide failed", - ) - - -def test_line_oblique_45() -> None: - expected = os.path.join(IMAGES_PATH, "line_oblique_45_w3px_a.png") - img, draw = create_base_image_draw((20, 20)) - draw.line((5, 5, 14, 14), BLACK, 3) - assert_image_equal_tofile(img, expected, "line oblique 45 normal 3px wide A failed") - img, draw = create_base_image_draw((20, 20)) - draw.line((14, 14, 5, 5), BLACK, 3) - assert_image_equal_tofile( - img, expected, "line oblique 45 inverted 3px wide A failed" - ) - - expected = os.path.join(IMAGES_PATH, "line_oblique_45_w3px_b.png") - img, draw = create_base_image_draw((20, 20)) - draw.line((14, 5, 5, 14), BLACK, 3) - assert_image_equal_tofile(img, expected, "line oblique 45 normal 3px wide B failed") - img, draw = create_base_image_draw((20, 20)) - draw.line((5, 14, 14, 5), BLACK, 3) - assert_image_equal_tofile( - img, expected, "line oblique 45 inverted 3px wide B failed" - ) - - -def test_wide_line_dot() -> None: - # Test drawing a wide "line" from one point to another just draws a single point - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - - # Act - draw.line([(50, 50), (50, 50)], width=3) - - # Assert - assert_image_similar_tofile(im, "Tests/images/imagedraw_wide_line_dot.png", 1) - - -def test_wide_line_larger_than_int() -> None: - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_wide_line_larger_than_int.png" - - # Act - draw.line([(0, 0), (32768, 32768)], width=3) - - # Assert - assert_image_similar_tofile(im, expected, 1) - - -@pytest.mark.parametrize( - "xy", - [ - [ - (400, 280), - (380, 280), - (450, 280), - (440, 120), - (350, 200), - (310, 280), - (300, 280), - (250, 280), - (250, 200), - (150, 200), - (150, 260), - (50, 200), - (150, 50), - (250, 100), - ], - ( - 400, - 280, - 380, - 280, - 450, - 280, - 440, - 120, - 350, - 200, - 310, - 280, - 300, - 280, - 250, - 280, - 250, - 200, - 150, - 200, - 150, - 260, - 50, - 200, - 150, - 50, - 250, - 100, - ), - [ - 400, - 280, - 380, - 280, - 450, - 280, - 440, - 120, - 350, - 200, - 310, - 280, - 300, - 280, - 250, - 280, - 250, - 200, - 150, - 200, - 150, - 260, - 50, - 200, - 150, - 50, - 250, - 100, - ], - ], -) -def test_line_joint(xy: list[tuple[int, int]] | tuple[int, ...] | list[int]) -> None: - im = Image.new("RGB", (500, 325)) - draw = ImageDraw.Draw(im) - - # Act - draw.line(xy, GRAY, 50, "curve") - - # Assert - assert_image_similar_tofile(im, "Tests/images/imagedraw_line_joint_curve.png", 3) - - -def test_textsize_empty_string() -> None: - # https://github.com/python-pillow/Pillow/issues/2783 - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - - # Act - # Should not cause 'SystemError: returned NULL without setting an error' - draw.textbbox((0, 0), "") - draw.textbbox((0, 0), "\n") - draw.textbbox((0, 0), "test\n") - draw.textlength("") - - -@skip_unless_feature("freetype2") -def test_textbbox_stroke() -> None: - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 20) - - # Act / Assert - assert draw.textbbox((2, 2), "A", font, stroke_width=2) == (0, 4, 16, 20) - assert draw.textbbox((2, 2), "A", font, stroke_width=4) == (-2, 2, 18, 22) - assert draw.textbbox((2, 2), "ABC\nAaaa", font, stroke_width=2) == (0, 4, 52, 44) - assert draw.textbbox((2, 2), "ABC\nAaaa", font, stroke_width=4) == (-2, 2, 54, 50) - - -@skip_unless_feature("freetype2") -def test_stroke() -> None: - for suffix, stroke_fill in {"same": None, "different": "#0f0"}.items(): - # Arrange - im = Image.new("RGB", (120, 130)) - draw = ImageDraw.Draw(im) - font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120) - - # Act - draw.text((12, 12), "A", "#f00", font, stroke_width=2, stroke_fill=stroke_fill) - - # Assert - assert_image_similar_tofile( - im, "Tests/images/imagedraw_stroke_" + suffix + ".png", 3.1 - ) - - -@skip_unless_feature("freetype2") -def test_stroke_descender() -> None: - # Arrange - im = Image.new("RGB", (120, 130)) - draw = ImageDraw.Draw(im) - font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120) - - # Act - draw.text((12, 2), "y", "#f00", font, stroke_width=2, stroke_fill="#0f0") - - # Assert - assert_image_similar_tofile(im, "Tests/images/imagedraw_stroke_descender.png", 6.76) - - -@skip_unless_feature("freetype2") -def test_split_word() -> None: - # Arrange - im = Image.new("RGB", (230, 55)) - expected = im.copy() - expected_draw = ImageDraw.Draw(expected) - font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 48) - expected_draw.text((0, 0), "paradise", font=font) - - draw = ImageDraw.Draw(im) - - # Act - draw.text((0, 0), "par", font=font) - - length = draw.textlength("par", font=font) - draw.text((length, 0), "adise", font=font) - - # Assert - assert_image_equal(im, expected) - - -@skip_unless_feature("freetype2") -def test_stroke_multiline() -> None: - # Arrange - im = Image.new("RGB", (100, 250)) - draw = ImageDraw.Draw(im) - font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120) - - # Act - draw.multiline_text( - (12, 12), "A\nB", "#f00", font, stroke_width=2, stroke_fill="#0f0" - ) - - # Assert - assert_image_similar_tofile(im, "Tests/images/imagedraw_stroke_multiline.png", 3.3) - - -@skip_unless_feature("freetype2") -def test_setting_default_font() -> None: - # Arrange - im = Image.new("RGB", (100, 250)) - draw = ImageDraw.Draw(im) - font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120) - - # Act - ImageDraw.ImageDraw.font = font - - # Assert - try: - assert draw.getfont() == font - finally: - ImageDraw.ImageDraw.font = None - assert isinstance(draw.getfont(), ImageFont.load_default().__class__) - - -def test_default_font_size() -> None: - freetype_support = features.check_module("freetype2") - text = "Default font at a specific size." - - im = Image.new("RGB", (220, 25)) - draw = ImageDraw.Draw(im) - with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError): - draw.text((0, 0), text, font_size=16) - assert_image_equal_tofile(im, "Tests/images/imagedraw_default_font_size.png") - - with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError): - assert draw.textlength(text, font_size=16) == 216 - - with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError): - assert draw.textbbox((0, 0), text, font_size=16) == (0, 3, 216, 19) - - im = Image.new("RGB", (220, 25)) - draw = ImageDraw.Draw(im) - with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError): - draw.multiline_text((0, 0), text, font_size=16) - assert_image_equal_tofile(im, "Tests/images/imagedraw_default_font_size.png") - - with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError): - assert draw.multiline_textbbox((0, 0), text, font_size=16) == (0, 3, 216, 19) - - -@pytest.mark.parametrize("bbox", BBOX) -def test_same_color_outline(bbox: Coords) -> None: - # Prepare shape - x0, y0 = 5, 5 - x1, y1 = 5, 50 - x2, y2 = 95, 50 - x3, y3 = 95, 5 - - s = ImageDraw.Outline() - s.move(x0, y0) - s.curve(x1, y1, x2, y2, x3, y3) - s.line(x0, y0) - - # Begin - for mode in ["RGB", "L"]: - fill = "red" - for outline in [None, "red", "#f00"]: - for operation, args in { - "chord": [bbox, 0, 180], - "ellipse": [bbox], - "shape": [s], - "pieslice": [bbox, -90, 45], - "polygon": [[(18, 30), (85, 30), (60, 72)]], - "rectangle": [bbox], - }.items(): - # Arrange - im = Image.new(mode, (W, H)) - draw = ImageDraw.Draw(im) - - # Act - draw_method = getattr(draw, operation) - assert isinstance(args, list) - args += [fill, outline] - draw_method(*args) - - # Assert - expected = f"Tests/images/imagedraw_outline_{operation}_{mode}.png" - assert_image_similar_tofile(im, expected, 1) - - -@pytest.mark.parametrize( - "n_sides, polygon_name, args", - [ - (4, "square", {}), - (8, "regular_octagon", {}), - (4, "square_rotate_45", {"rotation": 45}), - (3, "triangle_width", {"width": 5, "outline": "yellow"}), - ], -) -def test_draw_regular_polygon( - n_sides: int, polygon_name: str, args: dict[str, int | str] -) -> None: - im = Image.new("RGBA", size=(W, H), color=(255, 0, 0, 0)) - filename = f"Tests/images/imagedraw_{polygon_name}.png" - draw = ImageDraw.Draw(im) - bounding_circle = ((W // 2, H // 2), 25) - draw.regular_polygon(bounding_circle, n_sides, fill="red", **args) - assert_image_equal_tofile(im, filename) - - -@pytest.mark.parametrize( - "n_sides, expected_vertices", - [ - (3, [(28.35, 62.5), (71.65, 62.5), (50.0, 25.0)]), - (4, [(32.32, 67.68), (67.68, 67.68), (67.68, 32.32), (32.32, 32.32)]), - ( - 5, - [ - (35.31, 70.23), - (64.69, 70.23), - (73.78, 42.27), - (50.0, 25.0), - (26.22, 42.27), - ], - ), - ( - 6, - [ - (37.5, 71.65), - (62.5, 71.65), - (75.0, 50.0), - (62.5, 28.35), - (37.5, 28.35), - (25.0, 50.0), - ], - ), - ], -) -def test_compute_regular_polygon_vertices( - n_sides: int, expected_vertices: list[tuple[float, float]] -) -> None: - bounding_circle = (W // 2, H // 2, 25) - vertices = ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, 0) - assert vertices == expected_vertices - - -@pytest.mark.parametrize( - "n_sides, bounding_circle, rotation, expected_error, error_message", - [ - (None, (50, 50, 25), 0, TypeError, "n_sides should be an int"), - (1, (50, 50, 25), 0, ValueError, "n_sides should be an int > 2"), - (3, 50, 0, TypeError, "bounding_circle should be a sequence"), - ( - 3, - (50, 50, 100, 100), - 0, - ValueError, - "bounding_circle should contain 2D coordinates " - "and a radius (e.g. (x, y, r) or ((x, y), r) )", - ), - ( - 3, - (50, 50, "25"), - 0, - ValueError, - "bounding_circle should only contain numeric data", - ), - ( - 3, - ((50, 50, 50), 25), - 0, - ValueError, - "bounding_circle centre should contain 2D coordinates (e.g. (x, y))", - ), - ( - 3, - (50, 50, 0), - 0, - ValueError, - "bounding_circle radius should be > 0", - ), - ( - 3, - (50, 50, 25), - "0", - ValueError, - "rotation should be an int or float", - ), - ], -) -def test_compute_regular_polygon_vertices_input_error_handling( - n_sides, bounding_circle, rotation, expected_error, error_message -) -> None: - with pytest.raises(expected_error) as e: - ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) - assert str(e.value) == error_message - - -def test_continuous_horizontal_edges_polygon() -> None: - xy = [ - (2, 6), - (6, 6), - (12, 6), - (12, 12), - (8, 12), - (8, 8), - (4, 8), - (2, 8), - ] - img, draw = create_base_image_draw((16, 16)) - draw.polygon(xy, BLACK) - expected = os.path.join(IMAGES_PATH, "continuous_horizontal_edges_polygon.png") - assert_image_equal_tofile( - img, expected, "continuous horizontal edges polygon failed" - ) - - -def test_discontiguous_corners_polygon() -> None: - img, draw = create_base_image_draw((84, 68)) - draw.polygon(((1, 21), (34, 4), (71, 1), (38, 18)), BLACK) - draw.polygon(((71, 44), (38, 27), (1, 24)), BLACK) - draw.polygon( - ((38, 66), (5, 49), (77, 49), (47, 66), (82, 63), (82, 47), (1, 47), (1, 63)), - BLACK, - ) - expected = os.path.join(IMAGES_PATH, "discontiguous_corners_polygon.png") - assert_image_similar_tofile(img, expected, 1) - - -def test_polygon2() -> None: - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - draw.polygon([(18, 30), (19, 31), (18, 30), (85, 30), (60, 72)], "red") - expected = "Tests/images/imagedraw_outline_polygon_RGB.png" - assert_image_similar_tofile(im, expected, 1) - - -@pytest.mark.parametrize("xy", ((1, 1, 0, 1), (1, 1, 1, 0))) -def test_incorrectly_ordered_coordinates(xy: tuple[int, int, int, int]) -> None: - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - with pytest.raises(ValueError): - draw.arc(xy, 10, 260) - with pytest.raises(ValueError): - draw.chord(xy, 10, 260) - with pytest.raises(ValueError): - draw.ellipse(xy) - with pytest.raises(ValueError): - draw.pieslice(xy, 10, 260) - with pytest.raises(ValueError): - draw.rectangle(xy) - with pytest.raises(ValueError): - draw.rounded_rectangle(xy) - - -def test_getdraw(): - with pytest.warns(DeprecationWarning): - ImageDraw.getdraw(None, []) +from __future__ import annotations + +import contextlib +import os.path +from typing import Sequence + +import pytest + +from PIL import Image, ImageColor, ImageDraw, ImageFont, features +from PIL._typing import Coords + +from .helper import ( + assert_image_equal, + assert_image_equal_tofile, + assert_image_similar_tofile, + hopper, + skip_unless_feature, +) + +BLACK = (0, 0, 0) +WHITE = (255, 255, 255) +GRAY = (190, 190, 190) +DEFAULT_MODE = "RGB" +IMAGES_PATH = os.path.join("Tests", "images", "imagedraw") + +# Image size +W, H = 100, 100 + +# Bounding box points +X0 = int(W / 4) +X1 = int(X0 * 3) +Y0 = int(H / 4) +Y1 = int(X0 * 3) + +# Bounding boxes +BBOX = (((X0, Y0), (X1, Y1)), [(X0, Y0), (X1, Y1)], (X0, Y0, X1, Y1), [X0, Y0, X1, Y1]) + +# Coordinate sequences +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], +) + +KITE_POINTS = ( + ((10, 50), (70, 10), (90, 50), (70, 90), (10, 50)), + [(10, 50), (70, 10), (90, 50), (70, 90), (10, 50)], +) + + +def test_sanity() -> None: + im = hopper("RGB").copy() + + draw = ImageDraw.ImageDraw(im) + draw = ImageDraw.Draw(im) + + draw.ellipse(list(range(4))) + draw.line(list(range(10))) + draw.polygon(list(range(100))) + draw.rectangle(list(range(4))) + + +def test_valueerror() -> None: + with Image.open("Tests/images/chi.gif") as im: + draw = ImageDraw.Draw(im) + draw.line((0, 0), fill=(0, 0, 0)) + + +def test_mode_mismatch() -> None: + im = hopper("RGB").copy() + + with pytest.raises(ValueError): + ImageDraw.ImageDraw(im, mode="L") + + +@pytest.mark.parametrize("bbox", BBOX) +@pytest.mark.parametrize("start, end", ((0, 180), (0.5, 180.4))) +def test_arc(bbox: Coords, start: float, end: float) -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.arc(bbox, start, end) + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_arc.png", 1) + + +@pytest.mark.parametrize("bbox", BBOX) +def test_arc_end_le_start(bbox: Coords) -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + start = 270.5 + end = 0 + + # Act + draw.arc(bbox, start=start, end=end) + + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_arc_end_le_start.png") + + +@pytest.mark.parametrize("bbox", BBOX) +def test_arc_no_loops(bbox: Coords) -> None: + # No need to go in loops + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + start = 5 + end = 370 + + # Act + draw.arc(bbox, start=start, end=end) + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_arc_no_loops.png", 1) + + +@pytest.mark.parametrize("bbox", BBOX) +def test_arc_width(bbox: Coords) -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.arc(bbox, 10, 260, width=5) + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_arc_width.png", 1) + + +@pytest.mark.parametrize("bbox", BBOX) +def test_arc_width_pieslice_large(bbox: Coords) -> None: + # Tests an arc with a large enough width that it is a pieslice + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.arc(bbox, 10, 260, fill="yellow", width=100) + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_arc_width_pieslice.png", 1) + + +@pytest.mark.parametrize("bbox", BBOX) +def test_arc_width_fill(bbox: Coords) -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.arc(bbox, 10, 260, fill="yellow", width=5) + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_arc_width_fill.png", 1) + + +@pytest.mark.parametrize("bbox", BBOX) +def test_arc_width_non_whole_angle(bbox: Coords) -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + expected = "Tests/images/imagedraw_arc_width_non_whole_angle.png" + + # Act + draw.arc(bbox, 10, 259.5, width=5) + + # Assert + assert_image_similar_tofile(im, expected, 1) + + +def test_arc_high() -> None: + # Arrange + im = Image.new("RGB", (200, 200)) + draw = ImageDraw.Draw(im) + + # Act + draw.arc([10, 10, 89, 189], 20, 330, width=20, fill="white") + draw.arc([110, 10, 189, 189], 20, 150, width=20, fill="white") + + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_arc_high.png") + + +def test_bitmap() -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + with Image.open("Tests/images/pil123rgba.png") as small: + small = small.resize((50, 50), Image.Resampling.NEAREST) + + # Act + draw.bitmap((10, 10), small) + + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_bitmap.png") + + +@pytest.mark.parametrize("mode", ("RGB", "L")) +@pytest.mark.parametrize("bbox", BBOX) +def test_chord(mode: str, bbox: Coords) -> None: + # Arrange + im = Image.new(mode, (W, H)) + draw = ImageDraw.Draw(im) + expected = f"Tests/images/imagedraw_chord_{mode}.png" + + # Act + draw.chord(bbox, 0, 180, fill="red", outline="yellow") + + # Assert + assert_image_similar_tofile(im, expected, 1) + + +@pytest.mark.parametrize("bbox", BBOX) +def test_chord_width(bbox: Coords) -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.chord(bbox, 10, 260, outline="yellow", width=5) + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_chord_width.png", 1) + + +@pytest.mark.parametrize("bbox", BBOX) +def test_chord_width_fill(bbox: Coords) -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.chord(bbox, 10, 260, fill="red", outline="yellow", width=5) + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_chord_width_fill.png", 1) + + +@pytest.mark.parametrize("bbox", BBOX) +def test_chord_zero_width(bbox: Coords) -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.chord(bbox, 10, 260, fill="red", outline="yellow", width=0) + + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_chord_zero_width.png") + + +def test_chord_too_fat() -> None: + # Arrange + im = Image.new("RGB", (100, 100)) + draw = ImageDraw.Draw(im) + + # Act + draw.chord([-150, -150, 99, 99], 15, 60, width=10, fill="white", outline="red") + + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_chord_too_fat.png") + + +@pytest.mark.parametrize("mode", ("RGB", "L")) +@pytest.mark.parametrize("xy", ((W / 2, H / 2), [W / 2, H / 2])) +def test_circle(mode: str, xy: Sequence[float]) -> None: + # Arrange + im = Image.new(mode, (W, H)) + draw = ImageDraw.Draw(im) + expected = f"Tests/images/imagedraw_ellipse_{mode}.png" + + # Act + draw.circle(xy, 25, fill="green", outline="blue") + + # Assert + assert_image_similar_tofile(im, expected, 1) + + +@pytest.mark.parametrize("mode", ("RGB", "L")) +@pytest.mark.parametrize("bbox", BBOX) +def test_ellipse(mode: str, bbox: Coords) -> None: + # Arrange + im = Image.new(mode, (W, H)) + draw = ImageDraw.Draw(im) + expected = f"Tests/images/imagedraw_ellipse_{mode}.png" + + # Act + draw.ellipse(bbox, fill="green", outline="blue") + + # Assert + assert_image_similar_tofile(im, expected, 1) + + +@pytest.mark.parametrize("bbox", BBOX) +def test_ellipse_translucent(bbox: Coords) -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im, "RGBA") + + # Act + draw.ellipse(bbox, fill=(0, 255, 0, 127)) + + # Assert + expected = "Tests/images/imagedraw_ellipse_translucent.png" + assert_image_similar_tofile(im, expected, 1) + + +def test_ellipse_edge() -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.ellipse(((0, 0), (W - 1, H - 1)), fill="white") + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_edge.png", 1) + + +def test_ellipse_symmetric() -> None: + for width, bbox in ( + (100, (24, 24, 75, 75)), + (101, (25, 25, 75, 75)), + ): + im = Image.new("RGB", (width, 100)) + draw = ImageDraw.Draw(im) + draw.ellipse(bbox, fill="green", outline="blue") + assert_image_equal(im, im.transpose(Image.Transpose.FLIP_LEFT_RIGHT)) + + +@pytest.mark.parametrize("bbox", BBOX) +def test_ellipse_width(bbox: Coords) -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.ellipse(bbox, outline="blue", width=5) + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_width.png", 1) + + +def test_ellipse_width_large() -> None: + # Arrange + im = Image.new("RGB", (500, 500)) + draw = ImageDraw.Draw(im) + + # Act + draw.ellipse((25, 25, 475, 475), outline="blue", width=75) + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_width_large.png", 1) + + +@pytest.mark.parametrize("bbox", BBOX) +def test_ellipse_width_fill(bbox: Coords) -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.ellipse(bbox, fill="green", outline="blue", width=5) + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_width_fill.png", 1) + + +@pytest.mark.parametrize("bbox", BBOX) +def test_ellipse_zero_width(bbox: Coords) -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.ellipse(bbox, fill="green", outline="blue", width=0) + + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_ellipse_zero_width.png") + + +def ellipse_various_sizes_helper(filled: bool) -> Image.Image: + ellipse_sizes = range(32) + image_size = sum(ellipse_sizes) + len(ellipse_sizes) + 1 + im = Image.new("RGB", (image_size, image_size)) + draw = ImageDraw.Draw(im) + + x = 1 + for w in ellipse_sizes: + y = 1 + for h in ellipse_sizes: + x1 = x + w + if w: + x1 -= 1 + y1 = y + h + if h: + y1 -= 1 + border = [x, y, x1, y1] + if filled: + draw.ellipse(border, fill="white") + else: + draw.ellipse(border, outline="white") + y += h + 1 + x += w + 1 + + return im + + +def test_ellipse_various_sizes() -> None: + im = ellipse_various_sizes_helper(False) + + assert_image_equal_tofile(im, "Tests/images/imagedraw_ellipse_various_sizes.png") + + +def test_ellipse_various_sizes_filled() -> None: + im = ellipse_various_sizes_helper(True) + + assert_image_equal_tofile( + im, "Tests/images/imagedraw_ellipse_various_sizes_filled.png" + ) + + +@pytest.mark.parametrize("points", POINTS) +def test_line(points: Coords) -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.line(points, fill="yellow", width=2) + + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png") + + +def test_shape1() -> None: + # Arrange + im = Image.new("RGB", (100, 100), "white") + draw = ImageDraw.Draw(im) + x0, y0 = 5, 5 + x1, y1 = 5, 50 + x2, y2 = 95, 50 + x3, y3 = 95, 5 + + # Act + s = ImageDraw.Outline() + s.move(x0, y0) + s.curve(x1, y1, x2, y2, x3, y3) + s.line(x0, y0) + + draw.shape(s, fill=1) + + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_shape1.png") + + +def test_shape2() -> None: + # Arrange + im = Image.new("RGB", (100, 100), "white") + draw = ImageDraw.Draw(im) + x0, y0 = 95, 95 + x1, y1 = 95, 50 + x2, y2 = 5, 50 + x3, y3 = 5, 95 + + # Act + s = ImageDraw.Outline() + s.move(x0, y0) + s.curve(x1, y1, x2, y2, x3, y3) + s.line(x0, y0) + + draw.shape(s, outline="blue") + + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_shape2.png") + + +def test_transform() -> None: + # Arrange + im = Image.new("RGB", (100, 100), "white") + expected = im.copy() + draw = ImageDraw.Draw(im) + + # Act + s = ImageDraw.Outline() + s.line(0, 0) + s.transform((0, 0, 0, 0, 0, 0)) + + draw.shape(s, fill=1) + + # Assert + assert_image_equal(im, expected) + + +@pytest.mark.parametrize("bbox", BBOX) +@pytest.mark.parametrize("start, end", ((-92, 46), (-92.2, 46.2))) +def test_pieslice(bbox: Coords, start: float, end: float) -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.pieslice(bbox, start, end, fill="white", outline="blue") + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_pieslice.png", 1) + + +@pytest.mark.parametrize("bbox", BBOX) +def test_pieslice_width(bbox: Coords) -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.pieslice(bbox, 10, 260, outline="blue", width=5) + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_pieslice_width.png", 1) + + +@pytest.mark.parametrize("bbox", BBOX) +def test_pieslice_width_fill(bbox: Coords) -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + expected = "Tests/images/imagedraw_pieslice_width_fill.png" + + # Act + draw.pieslice(bbox, 10, 260, fill="white", outline="blue", width=5) + + # Assert + assert_image_similar_tofile(im, expected, 1) + + +@pytest.mark.parametrize("bbox", BBOX) +def test_pieslice_zero_width(bbox: Coords) -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.pieslice(bbox, 10, 260, fill="white", outline="blue", width=0) + + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_pieslice_zero_width.png") + + +def test_pieslice_wide() -> None: + # Arrange + im = Image.new("RGB", (200, 100)) + draw = ImageDraw.Draw(im) + + # Act + draw.pieslice([0, 0, 199, 99], 190, 170, width=10, fill="white", outline="red") + + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_pieslice_wide.png") + + +def test_pieslice_no_spikes() -> None: + im = Image.new("RGB", (161, 161), "white") + draw = ImageDraw.Draw(im) + cxs = ( + [140] * 3 + + list(range(140, 19, -20)) + + [20] * 5 + + list(range(20, 141, 20)) + + [140] * 2 + ) + cys = ( + list(range(80, 141, 20)) + + [140] * 5 + + list(range(140, 19, -20)) + + [20] * 5 + + list(range(20, 80, 20)) + ) + + for cx, cy, angle in zip(cxs, cys, range(0, 360, 15)): + draw.pieslice( + [cx - 100, cy - 100, cx + 100, cy + 100], angle, angle + 1, fill="black" + ) + draw.point([cx, cy], fill="red") + + im_pre_erase = im.copy() + draw.rectangle([21, 21, 139, 139], fill="white") + + assert_image_equal(im, im_pre_erase) + + +@pytest.mark.parametrize("points", POINTS) +def test_point(points: Coords) -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.point(points, fill="yellow") + + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_point.png") + + +def test_point_I16() -> None: + # Arrange + im = Image.new("I;16", (1, 1)) + draw = ImageDraw.Draw(im) + + # Act + draw.point((0, 0), fill=0x1234) + + # Assert + assert im.getpixel((0, 0)) == 0x1234 + + +@pytest.mark.parametrize("points", POINTS) +def test_polygon(points: Coords) -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.polygon(points, fill="red", outline="blue") + + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_polygon.png") + + +@pytest.mark.parametrize("mode", ("RGB", "L")) +@pytest.mark.parametrize("kite_points", KITE_POINTS) +def test_polygon_kite( + mode: str, kite_points: tuple[tuple[int, int], ...] | list[tuple[int, int]] +) -> None: + # Test drawing lines of different gradients (dx>dy, dy>dx) and + # vertical (dx==0) and horizontal (dy==0) lines + # Arrange + im = Image.new(mode, (W, H)) + draw = ImageDraw.Draw(im) + expected = f"Tests/images/imagedraw_polygon_kite_{mode}.png" + + # Act + draw.polygon(kite_points, fill="blue", outline="yellow") + + # Assert + assert_image_equal_tofile(im, expected) + + +def test_polygon_1px_high() -> None: + # Test drawing a 1px high polygon + # Arrange + im = Image.new("RGB", (3, 3)) + draw = ImageDraw.Draw(im) + expected = "Tests/images/imagedraw_polygon_1px_high.png" + + # Act + draw.polygon([(0, 1), (0, 1), (2, 1), (2, 1)], "#f00") + + # Assert + assert_image_equal_tofile(im, expected) + + +def test_polygon_1px_high_translucent() -> None: + # Test drawing a translucent 1px high polygon + # Arrange + im = Image.new("RGB", (4, 3)) + draw = ImageDraw.Draw(im, "RGBA") + expected = "Tests/images/imagedraw_polygon_1px_high_translucent.png" + + # Act + draw.polygon([(1, 1), (1, 1), (3, 1), (3, 1)], (255, 0, 0, 127)) + + # Assert + assert_image_equal_tofile(im, expected) + + +def test_polygon_translucent() -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im, "RGBA") + + # Act + draw.polygon([(20, 80), (80, 80), (80, 20)], fill=(0, 255, 0, 127)) + + # Assert + expected = "Tests/images/imagedraw_polygon_translucent.png" + assert_image_equal_tofile(im, expected) + + +@pytest.mark.parametrize("bbox", BBOX) +def test_rectangle(bbox: Coords) -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.rectangle(bbox, fill="black", outline="green") + + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle.png") + + +def test_big_rectangle() -> None: + # Test drawing a rectangle bigger than the image + # Arrange + im = Image.new("RGB", (W, H)) + bbox = [(-1, -1), (W + 1, H + 1)] + draw = ImageDraw.Draw(im) + + # Act + draw.rectangle(bbox, fill="orange") + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_big_rectangle.png", 1) + + +@pytest.mark.parametrize("bbox", BBOX) +def test_rectangle_width(bbox: Coords) -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + expected = "Tests/images/imagedraw_rectangle_width.png" + + # Act + draw.rectangle(bbox, outline="green", width=5) + + # Assert + assert_image_equal_tofile(im, expected) + + +@pytest.mark.parametrize("bbox", BBOX) +def test_rectangle_width_fill(bbox: Coords) -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + expected = "Tests/images/imagedraw_rectangle_width_fill.png" + + # Act + draw.rectangle(bbox, fill="blue", outline="green", width=5) + + # Assert + assert_image_equal_tofile(im, expected) + + +@pytest.mark.parametrize("bbox", BBOX) +def test_rectangle_zero_width(bbox: Coords) -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.rectangle(bbox, fill="blue", outline="green", width=0) + + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle_zero_width.png") + + +@pytest.mark.parametrize("bbox", BBOX) +def test_rectangle_I16(bbox: Coords) -> None: + # Arrange + im = Image.new("I;16", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.rectangle(bbox, outline=0xFFFF) + + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle_I.tiff") + + +@pytest.mark.parametrize("bbox", BBOX) +def test_rectangle_translucent_outline(bbox: Coords) -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im, "RGBA") + + # Act + draw.rectangle(bbox, fill="black", outline=(0, 255, 0, 127), width=5) + + # Assert + assert_image_equal_tofile( + im, "Tests/images/imagedraw_rectangle_translucent_outline.png" + ) + + +@pytest.mark.parametrize( + "xy", + [(10, 20, 190, 180), ([10, 20], [190, 180]), ((10, 20), (190, 180))], +) +def test_rounded_rectangle( + xy: ( + tuple[int, int, int, int] + | tuple[list[int]] + | tuple[tuple[int, int], tuple[int, int]] + ) +) -> None: + # Arrange + im = Image.new("RGB", (200, 200)) + draw = ImageDraw.Draw(im) + + # Act + draw.rounded_rectangle(xy, 30, fill="red", outline="green", width=5) + + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_rounded_rectangle.png") + + +@pytest.mark.parametrize("top_left", (True, False)) +@pytest.mark.parametrize("top_right", (True, False)) +@pytest.mark.parametrize("bottom_right", (True, False)) +@pytest.mark.parametrize("bottom_left", (True, False)) +def test_rounded_rectangle_corners( + top_left: bool, top_right: bool, bottom_right: bool, bottom_left: bool +) -> None: + corners = (top_left, top_right, bottom_right, bottom_left) + + # Arrange + im = Image.new("RGB", (200, 200)) + draw = ImageDraw.Draw(im) + + # Act + draw.rounded_rectangle( + (10, 20, 190, 180), 30, fill="red", outline="green", width=5, corners=corners + ) + + # Assert + suffix = "".join( + ( + ("y" if top_left else "n"), + ("y" if top_right else "n"), + ("y" if bottom_right else "n"), + ("y" if bottom_left else "n"), + ) + ) + assert_image_equal_tofile( + im, "Tests/images/imagedraw_rounded_rectangle_corners_" + suffix + ".png" + ) + + +@pytest.mark.parametrize( + "xy, radius, type", + [ + ((10, 20, 190, 180), 30.5, "given"), + ((10, 10, 181, 190), 90, "width"), + ((10, 20, 190, 181), 85, "height"), + ], +) +def test_rounded_rectangle_non_integer_radius( + xy: tuple[int, int, int, int], radius: float, type: str +) -> None: + # Arrange + im = Image.new("RGB", (200, 200)) + draw = ImageDraw.Draw(im) + + # Act + draw.rounded_rectangle(xy, radius, fill="red", outline="green", width=5) + + # Assert + assert_image_equal_tofile( + im, + "Tests/images/imagedraw_rounded_rectangle_non_integer_radius_" + type + ".png", + ) + + +@pytest.mark.parametrize("bbox", BBOX) +def test_rounded_rectangle_zero_radius(bbox: Coords) -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.rounded_rectangle(bbox, 0, fill="blue", outline="green", width=5) + + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle_width_fill.png") + + +@pytest.mark.parametrize( + "xy, suffix", + [ + ((20, 10, 80, 90), "x"), + ((20, 10, 81, 90), "x_odd"), + ((20, 10, 81.1, 90), "x_odd"), + ((10, 20, 90, 80), "y"), + ((10, 20, 90, 81), "y_odd"), + ((10, 20, 90, 81.1), "y_odd"), + ((20, 20, 80, 80), "both"), + ], +) +def test_rounded_rectangle_translucent( + xy: tuple[int, int, int, int], suffix: str +) -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im, "RGBA") + + # Act + draw.rounded_rectangle( + xy, 30, fill=(255, 0, 0, 127), outline=(0, 255, 0, 127), width=5 + ) + + # Assert + assert_image_equal_tofile( + im, "Tests/images/imagedraw_rounded_rectangle_" + suffix + ".png" + ) + + +@pytest.mark.parametrize("bbox", BBOX) +def test_floodfill(bbox: Coords) -> None: + red = ImageColor.getrgb("red") + + for mode, value in [("L", 1), ("RGBA", (255, 0, 0, 0)), ("RGB", red)]: + # Arrange + im = Image.new(mode, (W, H)) + draw = ImageDraw.Draw(im) + draw.rectangle(bbox, outline="yellow", fill="green") + centre_point = (int(W / 2), int(H / 2)) + + # Act + ImageDraw.floodfill(im, centre_point, value) + + # Assert + expected = "Tests/images/imagedraw_floodfill_" + mode + ".png" + with Image.open(expected) as im_floodfill: + assert_image_equal(im, im_floodfill) + + # Test that using the same colour does not change the image + ImageDraw.floodfill(im, centre_point, red) + assert_image_equal(im, im_floodfill) + + # Test that filling outside the image does not change the image + ImageDraw.floodfill(im, (W, H), red) + assert_image_equal(im, im_floodfill) + + # Test filling at the edge of an image + im = Image.new("RGB", (1, 1)) + ImageDraw.floodfill(im, (0, 0), red) + assert_image_equal(im, Image.new("RGB", (1, 1), red)) + + +@pytest.mark.parametrize("bbox", BBOX) +def test_floodfill_border(bbox: Coords) -> None: + # floodfill() is experimental + + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + draw.rectangle(bbox, outline="yellow", fill="green") + centre_point = (int(W / 2), int(H / 2)) + + # Act + ImageDraw.floodfill( + im, + centre_point, + ImageColor.getrgb("red"), + border=ImageColor.getrgb("black"), + ) + + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_floodfill2.png") + + +@pytest.mark.parametrize("bbox", BBOX) +def test_floodfill_thresh(bbox: Coords) -> None: + # floodfill() is experimental + + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + draw.rectangle(bbox, outline="darkgreen", fill="green") + centre_point = (int(W / 2), int(H / 2)) + + # Act + ImageDraw.floodfill(im, centre_point, ImageColor.getrgb("red"), thresh=30) + + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_floodfill2.png") + + +def test_floodfill_not_negative() -> None: + # floodfill() is experimental + # Test that floodfill does not extend into negative coordinates + + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + draw.line((W / 2, 0, W / 2, H / 2), fill="green") + draw.line((0, H / 2, W / 2, H / 2), fill="green") + + # Act + ImageDraw.floodfill(im, (int(W / 4), int(H / 4)), ImageColor.getrgb("red")) + + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_floodfill_not_negative.png") + + +def create_base_image_draw( + size: tuple[int, int], + mode: str = DEFAULT_MODE, + background1: tuple[int, int, int] = WHITE, + 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]): + if (x + y) % 2 == 0: + img.putpixel((x, y), background2) + return img, ImageDraw.Draw(img) + + +def test_square() -> None: + expected = os.path.join(IMAGES_PATH, "square.png") + img, draw = create_base_image_draw((10, 10)) + draw.polygon([(2, 2), (2, 7), (7, 7), (7, 2)], BLACK) + assert_image_equal_tofile(img, expected, "square as normal polygon failed") + img, draw = create_base_image_draw((10, 10)) + draw.polygon([(7, 7), (7, 2), (2, 2), (2, 7)], BLACK) + assert_image_equal_tofile(img, expected, "square as inverted polygon failed") + img, draw = create_base_image_draw((10, 10)) + draw.rectangle((2, 2, 7, 7), BLACK) + assert_image_equal_tofile(img, expected, "square as normal rectangle failed") + + +def test_triangle_right() -> None: + img, draw = create_base_image_draw((20, 20)) + draw.polygon([(3, 5), (17, 5), (10, 12)], BLACK) + assert_image_equal_tofile( + img, os.path.join(IMAGES_PATH, "triangle_right.png"), "triangle right failed" + ) + + +@pytest.mark.parametrize( + "fill, suffix", + ((BLACK, "width"), (None, "width_no_fill")), +) +def test_triangle_right_width(fill: tuple[int, int, int] | None, suffix: str) -> None: + img, draw = create_base_image_draw((100, 100)) + draw.polygon([(15, 25), (85, 25), (50, 60)], fill, WHITE, width=5) + assert_image_equal_tofile( + img, os.path.join(IMAGES_PATH, "triangle_right_" + suffix + ".png") + ) + + +def test_line_horizontal() -> None: + img, draw = create_base_image_draw((20, 20)) + draw.line((5, 5, 14, 5), BLACK, 2) + assert_image_equal_tofile( + img, + os.path.join(IMAGES_PATH, "line_horizontal_w2px_normal.png"), + "line straight horizontal normal 2px wide failed", + ) + + img, draw = create_base_image_draw((20, 20)) + draw.line((14, 5, 5, 5), BLACK, 2) + assert_image_equal_tofile( + img, + os.path.join(IMAGES_PATH, "line_horizontal_w2px_inverted.png"), + "line straight horizontal inverted 2px wide failed", + ) + + expected = os.path.join(IMAGES_PATH, "line_horizontal_w3px.png") + img, draw = create_base_image_draw((20, 20)) + draw.line((5, 5, 14, 5), BLACK, 3) + assert_image_equal_tofile( + img, expected, "line straight horizontal normal 3px wide failed" + ) + img, draw = create_base_image_draw((20, 20)) + draw.line((14, 5, 5, 5), BLACK, 3) + assert_image_equal_tofile( + img, expected, "line straight horizontal inverted 3px wide failed" + ) + + img, draw = create_base_image_draw((200, 110)) + draw.line((5, 55, 195, 55), BLACK, 101) + assert_image_equal_tofile( + img, + os.path.join(IMAGES_PATH, "line_horizontal_w101px.png"), + "line straight horizontal 101px wide failed", + ) + + +@pytest.mark.xfail(reason="failing test") +def test_line_h_s1_w2() -> None: + img, draw = create_base_image_draw((20, 20)) + draw.line((5, 5, 14, 6), BLACK, 2) + assert_image_equal_tofile( + img, + os.path.join(IMAGES_PATH, "line_horizontal_slope1px_w2px.png"), + "line horizontal 1px slope 2px wide failed", + ) + + +def test_line_vertical() -> None: + img, draw = create_base_image_draw((20, 20)) + draw.line((5, 5, 5, 14), BLACK, 2) + assert_image_equal_tofile( + img, + os.path.join(IMAGES_PATH, "line_vertical_w2px_normal.png"), + "line straight vertical normal 2px wide failed", + ) + + img, draw = create_base_image_draw((20, 20)) + draw.line((5, 14, 5, 5), BLACK, 2) + assert_image_equal_tofile( + img, + os.path.join(IMAGES_PATH, "line_vertical_w2px_inverted.png"), + "line straight vertical inverted 2px wide failed", + ) + + expected = os.path.join(IMAGES_PATH, "line_vertical_w3px.png") + img, draw = create_base_image_draw((20, 20)) + draw.line((5, 5, 5, 14), BLACK, 3) + assert_image_equal_tofile( + img, expected, "line straight vertical normal 3px wide failed" + ) + img, draw = create_base_image_draw((20, 20)) + draw.line((5, 14, 5, 5), BLACK, 3) + assert_image_equal_tofile( + img, expected, "line straight vertical inverted 3px wide failed" + ) + + img, draw = create_base_image_draw((110, 200)) + draw.line((55, 5, 55, 195), BLACK, 101) + assert_image_equal_tofile( + img, + os.path.join(IMAGES_PATH, "line_vertical_w101px.png"), + "line straight vertical 101px wide failed", + ) + + img, draw = create_base_image_draw((20, 20)) + draw.line((5, 5, 6, 14), BLACK, 2) + assert_image_equal_tofile( + img, + os.path.join(IMAGES_PATH, "line_vertical_slope1px_w2px.png"), + "line vertical 1px slope 2px wide failed", + ) + + +def test_line_oblique_45() -> None: + expected = os.path.join(IMAGES_PATH, "line_oblique_45_w3px_a.png") + img, draw = create_base_image_draw((20, 20)) + draw.line((5, 5, 14, 14), BLACK, 3) + assert_image_equal_tofile(img, expected, "line oblique 45 normal 3px wide A failed") + img, draw = create_base_image_draw((20, 20)) + draw.line((14, 14, 5, 5), BLACK, 3) + assert_image_equal_tofile( + img, expected, "line oblique 45 inverted 3px wide A failed" + ) + + expected = os.path.join(IMAGES_PATH, "line_oblique_45_w3px_b.png") + img, draw = create_base_image_draw((20, 20)) + draw.line((14, 5, 5, 14), BLACK, 3) + assert_image_equal_tofile(img, expected, "line oblique 45 normal 3px wide B failed") + img, draw = create_base_image_draw((20, 20)) + draw.line((5, 14, 14, 5), BLACK, 3) + assert_image_equal_tofile( + img, expected, "line oblique 45 inverted 3px wide B failed" + ) + + +def test_wide_line_dot() -> None: + # Test drawing a wide "line" from one point to another just draws a single point + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.line([(50, 50), (50, 50)], width=3) + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_wide_line_dot.png", 1) + + +def test_wide_line_larger_than_int() -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + expected = "Tests/images/imagedraw_wide_line_larger_than_int.png" + + # Act + draw.line([(0, 0), (32768, 32768)], width=3) + + # Assert + assert_image_similar_tofile(im, expected, 1) + + +@pytest.mark.parametrize( + "xy", + [ + [ + (400, 280), + (380, 280), + (450, 280), + (440, 120), + (350, 200), + (310, 280), + (300, 280), + (250, 280), + (250, 200), + (150, 200), + (150, 260), + (50, 200), + (150, 50), + (250, 100), + ], + ( + 400, + 280, + 380, + 280, + 450, + 280, + 440, + 120, + 350, + 200, + 310, + 280, + 300, + 280, + 250, + 280, + 250, + 200, + 150, + 200, + 150, + 260, + 50, + 200, + 150, + 50, + 250, + 100, + ), + [ + 400, + 280, + 380, + 280, + 450, + 280, + 440, + 120, + 350, + 200, + 310, + 280, + 300, + 280, + 250, + 280, + 250, + 200, + 150, + 200, + 150, + 260, + 50, + 200, + 150, + 50, + 250, + 100, + ], + ], +) +def test_line_joint(xy: list[tuple[int, int]] | tuple[int, ...] | list[int]) -> None: + im = Image.new("RGB", (500, 325)) + draw = ImageDraw.Draw(im) + + # Act + draw.line(xy, GRAY, 50, "curve") + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_line_joint_curve.png", 3) + + +def test_textsize_empty_string() -> None: + # https://github.com/python-pillow/Pillow/issues/2783 + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + # Should not cause 'SystemError: returned NULL without setting an error' + draw.textbbox((0, 0), "") + draw.textbbox((0, 0), "\n") + draw.textbbox((0, 0), "test\n") + draw.textlength("") + + +@skip_unless_feature("freetype2") +def test_textbbox_stroke() -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 20) + + # Act / Assert + assert draw.textbbox((2, 2), "A", font, stroke_width=2) == (0, 4, 16, 20) + assert draw.textbbox((2, 2), "A", font, stroke_width=4) == (-2, 2, 18, 22) + assert draw.textbbox((2, 2), "ABC\nAaaa", font, stroke_width=2) == (0, 4, 52, 44) + assert draw.textbbox((2, 2), "ABC\nAaaa", font, stroke_width=4) == (-2, 2, 54, 50) + + +@skip_unless_feature("freetype2") +def test_stroke() -> None: + for suffix, stroke_fill in {"same": None, "different": "#0f0"}.items(): + # Arrange + im = Image.new("RGB", (120, 130)) + draw = ImageDraw.Draw(im) + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120) + + # Act + draw.text((12, 12), "A", "#f00", font, stroke_width=2, stroke_fill=stroke_fill) + + # Assert + assert_image_similar_tofile( + im, "Tests/images/imagedraw_stroke_" + suffix + ".png", 3.1 + ) + + +@skip_unless_feature("freetype2") +def test_stroke_descender() -> None: + # Arrange + im = Image.new("RGB", (120, 130)) + draw = ImageDraw.Draw(im) + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120) + + # Act + draw.text((12, 2), "y", "#f00", font, stroke_width=2, stroke_fill="#0f0") + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_stroke_descender.png", 6.76) + + +@skip_unless_feature("freetype2") +def test_split_word() -> None: + # Arrange + im = Image.new("RGB", (230, 55)) + expected = im.copy() + expected_draw = ImageDraw.Draw(expected) + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 48) + expected_draw.text((0, 0), "paradise", font=font) + + draw = ImageDraw.Draw(im) + + # Act + draw.text((0, 0), "par", font=font) + + length = draw.textlength("par", font=font) + draw.text((length, 0), "adise", font=font) + + # Assert + assert_image_equal(im, expected) + + +@skip_unless_feature("freetype2") +def test_stroke_multiline() -> None: + # Arrange + im = Image.new("RGB", (100, 250)) + draw = ImageDraw.Draw(im) + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120) + + # Act + draw.multiline_text( + (12, 12), "A\nB", "#f00", font, stroke_width=2, stroke_fill="#0f0" + ) + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_stroke_multiline.png", 3.3) + + +@skip_unless_feature("freetype2") +def test_setting_default_font() -> None: + # Arrange + im = Image.new("RGB", (100, 250)) + draw = ImageDraw.Draw(im) + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120) + + # Act + ImageDraw.ImageDraw.font = font + + # Assert + try: + assert draw.getfont() == font + finally: + ImageDraw.ImageDraw.font = None + assert isinstance(draw.getfont(), ImageFont.load_default().__class__) + + +def test_default_font_size() -> None: + freetype_support = features.check_module("freetype2") + text = "Default font at a specific size." + + im = Image.new("RGB", (220, 25)) + draw = ImageDraw.Draw(im) + with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError): + draw.text((0, 0), text, font_size=16) + assert_image_equal_tofile(im, "Tests/images/imagedraw_default_font_size.png") + + with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError): + assert draw.textlength(text, font_size=16) == 216 + + with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError): + assert draw.textbbox((0, 0), text, font_size=16) == (0, 3, 216, 19) + + im = Image.new("RGB", (220, 25)) + draw = ImageDraw.Draw(im) + with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError): + draw.multiline_text((0, 0), text, font_size=16) + assert_image_equal_tofile(im, "Tests/images/imagedraw_default_font_size.png") + + with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError): + assert draw.multiline_textbbox((0, 0), text, font_size=16) == (0, 3, 216, 19) + + +@pytest.mark.parametrize("bbox", BBOX) +def test_same_color_outline(bbox: Coords) -> None: + # Prepare shape + x0, y0 = 5, 5 + x1, y1 = 5, 50 + x2, y2 = 95, 50 + x3, y3 = 95, 5 + + s = ImageDraw.Outline() + s.move(x0, y0) + s.curve(x1, y1, x2, y2, x3, y3) + s.line(x0, y0) + + # Begin + for mode in ["RGB", "L"]: + fill = "red" + for outline in [None, "red", "#f00"]: + for operation, args in { + "chord": [bbox, 0, 180], + "ellipse": [bbox], + "shape": [s], + "pieslice": [bbox, -90, 45], + "polygon": [[(18, 30), (85, 30), (60, 72)]], + "rectangle": [bbox], + }.items(): + # Arrange + im = Image.new(mode, (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw_method = getattr(draw, operation) + assert isinstance(args, list) + args += [fill, outline] + draw_method(*args) + + # Assert + expected = f"Tests/images/imagedraw_outline_{operation}_{mode}.png" + assert_image_similar_tofile(im, expected, 1) + + +@pytest.mark.parametrize( + "n_sides, polygon_name, args", + [ + (4, "square", {}), + (8, "regular_octagon", {}), + (4, "square_rotate_45", {"rotation": 45}), + (3, "triangle_width", {"width": 5, "outline": "yellow"}), + ], +) +def test_draw_regular_polygon( + n_sides: int, polygon_name: str, args: dict[str, int | str] +) -> None: + im = Image.new("RGBA", size=(W, H), color=(255, 0, 0, 0)) + filename = f"Tests/images/imagedraw_{polygon_name}.png" + draw = ImageDraw.Draw(im) + bounding_circle = ((W // 2, H // 2), 25) + draw.regular_polygon(bounding_circle, n_sides, fill="red", **args) + assert_image_equal_tofile(im, filename) + + +@pytest.mark.parametrize( + "n_sides, expected_vertices", + [ + (3, [(28.35, 62.5), (71.65, 62.5), (50.0, 25.0)]), + (4, [(32.32, 67.68), (67.68, 67.68), (67.68, 32.32), (32.32, 32.32)]), + ( + 5, + [ + (35.31, 70.23), + (64.69, 70.23), + (73.78, 42.27), + (50.0, 25.0), + (26.22, 42.27), + ], + ), + ( + 6, + [ + (37.5, 71.65), + (62.5, 71.65), + (75.0, 50.0), + (62.5, 28.35), + (37.5, 28.35), + (25.0, 50.0), + ], + ), + ], +) +def test_compute_regular_polygon_vertices( + n_sides: int, expected_vertices: list[tuple[float, float]] +) -> None: + bounding_circle = (W // 2, H // 2, 25) + vertices = ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, 0) + assert vertices == expected_vertices + + +@pytest.mark.parametrize( + "n_sides, bounding_circle, rotation, expected_error, error_message", + [ + (None, (50, 50, 25), 0, TypeError, "n_sides should be an int"), + (1, (50, 50, 25), 0, ValueError, "n_sides should be an int > 2"), + (3, 50, 0, TypeError, "bounding_circle should be a sequence"), + ( + 3, + (50, 50, 100, 100), + 0, + ValueError, + "bounding_circle should contain 2D coordinates " + "and a radius (e.g. (x, y, r) or ((x, y), r) )", + ), + ( + 3, + (50, 50, "25"), + 0, + ValueError, + "bounding_circle should only contain numeric data", + ), + ( + 3, + ((50, 50, 50), 25), + 0, + ValueError, + "bounding_circle centre should contain 2D coordinates (e.g. (x, y))", + ), + ( + 3, + (50, 50, 0), + 0, + ValueError, + "bounding_circle radius should be > 0", + ), + ( + 3, + (50, 50, 25), + "0", + ValueError, + "rotation should be an int or float", + ), + ], +) +def test_compute_regular_polygon_vertices_input_error_handling( + n_sides, bounding_circle, rotation, expected_error, error_message +) -> None: + with pytest.raises(expected_error) as e: + ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) + assert str(e.value) == error_message + + +def test_continuous_horizontal_edges_polygon() -> None: + xy = [ + (2, 6), + (6, 6), + (12, 6), + (12, 12), + (8, 12), + (8, 8), + (4, 8), + (2, 8), + ] + img, draw = create_base_image_draw((16, 16)) + draw.polygon(xy, BLACK) + expected = os.path.join(IMAGES_PATH, "continuous_horizontal_edges_polygon.png") + assert_image_equal_tofile( + img, expected, "continuous horizontal edges polygon failed" + ) + + +def test_discontiguous_corners_polygon() -> None: + img, draw = create_base_image_draw((84, 68)) + draw.polygon(((1, 21), (34, 4), (71, 1), (38, 18)), BLACK) + draw.polygon(((71, 44), (38, 27), (1, 24)), BLACK) + draw.polygon( + ((38, 66), (5, 49), (77, 49), (47, 66), (82, 63), (82, 47), (1, 47), (1, 63)), + BLACK, + ) + expected = os.path.join(IMAGES_PATH, "discontiguous_corners_polygon.png") + assert_image_similar_tofile(img, expected, 1) + + +def test_polygon2() -> None: + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + draw.polygon([(18, 30), (19, 31), (18, 30), (85, 30), (60, 72)], "red") + expected = "Tests/images/imagedraw_outline_polygon_RGB.png" + assert_image_similar_tofile(im, expected, 1) + + +@pytest.mark.parametrize("xy", ((1, 1, 0, 1), (1, 1, 1, 0))) +def test_incorrectly_ordered_coordinates(xy: tuple[int, int, int, int]) -> None: + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + with pytest.raises(ValueError): + draw.arc(xy, 10, 260) + with pytest.raises(ValueError): + draw.chord(xy, 10, 260) + with pytest.raises(ValueError): + draw.ellipse(xy) + with pytest.raises(ValueError): + draw.pieslice(xy, 10, 260) + with pytest.raises(ValueError): + draw.rectangle(xy) + with pytest.raises(ValueError): + draw.rounded_rectangle(xy) + + +def test_getdraw(): + with pytest.warns(DeprecationWarning): + ImageDraw.getdraw(None, []) diff --git a/Tests/test_imagedraw2.py b/Tests/test_imagedraw2.py index 3171eb9ae..93a0175a4 100644 --- a/Tests/test_imagedraw2.py +++ b/Tests/test_imagedraw2.py @@ -1,221 +1,221 @@ -from __future__ import annotations - -import os.path - -import pytest - -from PIL import Image, ImageDraw, ImageDraw2, features -from PIL._typing import Coords - -from .helper import ( - assert_image_equal, - assert_image_equal_tofile, - assert_image_similar_tofile, - hopper, - skip_unless_feature, -) - -BLACK = (0, 0, 0) -WHITE = (255, 255, 255) -GRAY = (190, 190, 190) -DEFAULT_MODE = "RGB" -IMAGES_PATH = os.path.join("Tests", "images", "imagedraw") - -# Image size -W, H = 100, 100 - -# Bounding box points -X0 = int(W / 4) -X1 = int(X0 * 3) -Y0 = int(H / 4) -Y1 = int(X0 * 3) - -# Bounding boxes -BBOX = (((X0, Y0), (X1, Y1)), [(X0, Y0), (X1, Y1)], (X0, Y0, X1, Y1), [X0, Y0, X1, Y1]) - -# Coordinate sequences -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], -) - -FONT_PATH = "Tests/fonts/FreeMono.ttf" - - -def test_sanity() -> None: - im = hopper("RGB").copy() - - draw = ImageDraw2.Draw(im) - pen = ImageDraw2.Pen("blue", width=7) - draw.line(list(range(10)), pen) - - draw, handler = ImageDraw.getdraw(im) - pen = ImageDraw2.Pen("blue", width=7) - draw.line(list(range(10)), pen) - - -@pytest.mark.parametrize("bbox", BBOX) -def test_ellipse(bbox: Coords) -> None: - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw2.Draw(im) - pen = ImageDraw2.Pen("blue", width=2) - brush = ImageDraw2.Brush("green") - - # Act - draw.ellipse(bbox, pen, brush) - - # Assert - assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_RGB.png", 1) - - -def test_ellipse_edge() -> None: - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw2.Draw(im) - brush = ImageDraw2.Brush("white") - - # Act - draw.ellipse(((0, 0), (W - 1, H - 1)), brush) - - # Assert - assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_edge.png", 1) - - -@pytest.mark.parametrize("points", POINTS) -def test_line(points: Coords) -> None: - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw2.Draw(im) - pen = ImageDraw2.Pen("yellow", width=2) - - # Act - draw.line(points, pen) - - # Assert - assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png") - - -@pytest.mark.parametrize("points", POINTS) -def test_line_pen_as_brush(points: Coords) -> None: - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw2.Draw(im) - pen = None - brush = ImageDraw2.Pen("yellow", width=2) - - # Act - # Pass in the pen as the brush parameter - draw.line(points, pen, brush) - - # Assert - assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png") - - -@pytest.mark.parametrize("points", POINTS) -def test_polygon(points: Coords) -> None: - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw2.Draw(im) - pen = ImageDraw2.Pen("blue", width=2) - brush = ImageDraw2.Brush("red") - - # Act - draw.polygon(points, pen, brush) - - # Assert - assert_image_equal_tofile(im, "Tests/images/imagedraw_polygon.png") - - -@pytest.mark.parametrize("bbox", BBOX) -def test_rectangle(bbox: Coords) -> None: - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw2.Draw(im) - pen = ImageDraw2.Pen("green", width=2) - brush = ImageDraw2.Brush("black") - - # Act - draw.rectangle(bbox, pen, brush) - - # Assert - assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle.png") - - -def test_big_rectangle() -> None: - # Test drawing a rectangle bigger than the image - # Arrange - im = Image.new("RGB", (W, H)) - bbox = [(-1, -1), (W + 1, H + 1)] - brush = ImageDraw2.Brush("orange") - draw = ImageDraw2.Draw(im) - expected = "Tests/images/imagedraw_big_rectangle.png" - - # Act - draw.rectangle(bbox, brush) - - # Assert - assert_image_similar_tofile(im, expected, 1) - - -@skip_unless_feature("freetype2") -def test_text() -> None: - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw2.Draw(im) - font = ImageDraw2.Font("white", FONT_PATH) - expected = "Tests/images/imagedraw2_text.png" - - # Act - draw.text((5, 5), "ImageDraw2", font) - - # Assert - assert_image_similar_tofile(im, expected, 13) - - -@skip_unless_feature("freetype2") -def test_textbbox() -> None: - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw2.Draw(im) - font = ImageDraw2.Font("white", FONT_PATH) - - # Act - bbox = draw.textbbox((0, 0), "ImageDraw2", font) - - # Assert - right = 72 if features.check_feature("raqm") else 70 - assert bbox == (0, 2, right, 12) - - -@skip_unless_feature("freetype2") -def test_textsize_empty_string() -> None: - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw2.Draw(im) - font = ImageDraw2.Font("white", FONT_PATH) - - # Act - # Should not cause 'SystemError: returned NULL without setting an error' - draw.textbbox((0, 0), "", font) - draw.textbbox((0, 0), "\n", font) - draw.textbbox((0, 0), "test\n", font) - draw.textlength("", font) - - -@skip_unless_feature("freetype2") -def test_flush() -> None: - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw2.Draw(im) - font = ImageDraw2.Font("white", FONT_PATH) - - # Act - draw.text((5, 5), "ImageDraw2", font) - im2 = draw.flush() - - # Assert - assert_image_equal(im, im2) +from __future__ import annotations + +import os.path + +import pytest + +from PIL import Image, ImageDraw, ImageDraw2, features +from PIL._typing import Coords + +from .helper import ( + assert_image_equal, + assert_image_equal_tofile, + assert_image_similar_tofile, + hopper, + skip_unless_feature, +) + +BLACK = (0, 0, 0) +WHITE = (255, 255, 255) +GRAY = (190, 190, 190) +DEFAULT_MODE = "RGB" +IMAGES_PATH = os.path.join("Tests", "images", "imagedraw") + +# Image size +W, H = 100, 100 + +# Bounding box points +X0 = int(W / 4) +X1 = int(X0 * 3) +Y0 = int(H / 4) +Y1 = int(X0 * 3) + +# Bounding boxes +BBOX = (((X0, Y0), (X1, Y1)), [(X0, Y0), (X1, Y1)], (X0, Y0, X1, Y1), [X0, Y0, X1, Y1]) + +# Coordinate sequences +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], +) + +FONT_PATH = "Tests/fonts/FreeMono.ttf" + + +def test_sanity() -> None: + im = hopper("RGB").copy() + + draw = ImageDraw2.Draw(im) + pen = ImageDraw2.Pen("blue", width=7) + draw.line(list(range(10)), pen) + + draw, handler = ImageDraw.getdraw(im) + pen = ImageDraw2.Pen("blue", width=7) + draw.line(list(range(10)), pen) + + +@pytest.mark.parametrize("bbox", BBOX) +def test_ellipse(bbox: Coords) -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + pen = ImageDraw2.Pen("blue", width=2) + brush = ImageDraw2.Brush("green") + + # Act + draw.ellipse(bbox, pen, brush) + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_RGB.png", 1) + + +def test_ellipse_edge() -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + brush = ImageDraw2.Brush("white") + + # Act + draw.ellipse(((0, 0), (W - 1, H - 1)), brush) + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_edge.png", 1) + + +@pytest.mark.parametrize("points", POINTS) +def test_line(points: Coords) -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + pen = ImageDraw2.Pen("yellow", width=2) + + # Act + draw.line(points, pen) + + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png") + + +@pytest.mark.parametrize("points", POINTS) +def test_line_pen_as_brush(points: Coords) -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + pen = None + brush = ImageDraw2.Pen("yellow", width=2) + + # Act + # Pass in the pen as the brush parameter + draw.line(points, pen, brush) + + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png") + + +@pytest.mark.parametrize("points", POINTS) +def test_polygon(points: Coords) -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + pen = ImageDraw2.Pen("blue", width=2) + brush = ImageDraw2.Brush("red") + + # Act + draw.polygon(points, pen, brush) + + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_polygon.png") + + +@pytest.mark.parametrize("bbox", BBOX) +def test_rectangle(bbox: Coords) -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + pen = ImageDraw2.Pen("green", width=2) + brush = ImageDraw2.Brush("black") + + # Act + draw.rectangle(bbox, pen, brush) + + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle.png") + + +def test_big_rectangle() -> None: + # Test drawing a rectangle bigger than the image + # Arrange + im = Image.new("RGB", (W, H)) + bbox = [(-1, -1), (W + 1, H + 1)] + brush = ImageDraw2.Brush("orange") + draw = ImageDraw2.Draw(im) + expected = "Tests/images/imagedraw_big_rectangle.png" + + # Act + draw.rectangle(bbox, brush) + + # Assert + assert_image_similar_tofile(im, expected, 1) + + +@skip_unless_feature("freetype2") +def test_text() -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + font = ImageDraw2.Font("white", FONT_PATH) + expected = "Tests/images/imagedraw2_text.png" + + # Act + draw.text((5, 5), "ImageDraw2", font) + + # Assert + assert_image_similar_tofile(im, expected, 13) + + +@skip_unless_feature("freetype2") +def test_textbbox() -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + font = ImageDraw2.Font("white", FONT_PATH) + + # Act + bbox = draw.textbbox((0, 0), "ImageDraw2", font) + + # Assert + right = 72 if features.check_feature("raqm") else 70 + assert bbox == (0, 2, right, 12) + + +@skip_unless_feature("freetype2") +def test_textsize_empty_string() -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + font = ImageDraw2.Font("white", FONT_PATH) + + # Act + # Should not cause 'SystemError: returned NULL without setting an error' + draw.textbbox((0, 0), "", font) + draw.textbbox((0, 0), "\n", font) + draw.textbbox((0, 0), "test\n", font) + draw.textlength("", font) + + +@skip_unless_feature("freetype2") +def test_flush() -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + font = ImageDraw2.Font("white", FONT_PATH) + + # Act + draw.text((5, 5), "ImageDraw2", font) + im2 = draw.flush() + + # Assert + assert_image_equal(im, im2) diff --git a/Tests/test_imageenhance.py b/Tests/test_imageenhance.py index 6ebc61e1b..329381b8d 100644 --- a/Tests/test_imageenhance.py +++ b/Tests/test_imageenhance.py @@ -1,61 +1,61 @@ -from __future__ import annotations - -import pytest - -from PIL import Image, ImageEnhance - -from .helper import assert_image_equal, hopper - - -def test_sanity() -> None: - # FIXME: assert_image - # Implicit asserts no exception: - ImageEnhance.Color(hopper()).enhance(0.5) - ImageEnhance.Contrast(hopper()).enhance(0.5) - ImageEnhance.Brightness(hopper()).enhance(0.5) - ImageEnhance.Sharpness(hopper()).enhance(0.5) - - -def test_crash() -> None: - # crashes on small images - im = Image.new("RGB", (1, 1)) - ImageEnhance.Sharpness(im).enhance(0.5) - - -def _half_transparent_image() -> Image.Image: - # returns an image, half transparent, half solid - im = hopper("RGB") - - transparent = Image.new("L", im.size, 0) - solid = Image.new("L", (im.size[0] // 2, im.size[1]), 255) - transparent.paste(solid, (0, 0)) - im.putalpha(transparent) - - return im - - -def _check_alpha( - im: Image.Image, original: Image.Image, op: str, amount: float -) -> None: - assert im.getbands() == original.getbands() - assert_image_equal( - im.getchannel("A"), - original.getchannel("A"), - f"Diff on {op}: {amount}", - ) - - -@pytest.mark.parametrize("op", ("Color", "Brightness", "Contrast", "Sharpness")) -def test_alpha(op: str) -> None: - # Issue https://github.com/python-pillow/Pillow/issues/899 - # Is alpha preserved through image enhancement? - - original = _half_transparent_image() - - for amount in [0, 0.5, 1.0]: - _check_alpha( - getattr(ImageEnhance, op)(original).enhance(amount), - original, - op, - amount, - ) +from __future__ import annotations + +import pytest + +from PIL import Image, ImageEnhance + +from .helper import assert_image_equal, hopper + + +def test_sanity() -> None: + # FIXME: assert_image + # Implicit asserts no exception: + ImageEnhance.Color(hopper()).enhance(0.5) + ImageEnhance.Contrast(hopper()).enhance(0.5) + ImageEnhance.Brightness(hopper()).enhance(0.5) + ImageEnhance.Sharpness(hopper()).enhance(0.5) + + +def test_crash() -> None: + # crashes on small images + im = Image.new("RGB", (1, 1)) + ImageEnhance.Sharpness(im).enhance(0.5) + + +def _half_transparent_image() -> Image.Image: + # returns an image, half transparent, half solid + im = hopper("RGB") + + transparent = Image.new("L", im.size, 0) + solid = Image.new("L", (im.size[0] // 2, im.size[1]), 255) + transparent.paste(solid, (0, 0)) + im.putalpha(transparent) + + return im + + +def _check_alpha( + im: Image.Image, original: Image.Image, op: str, amount: float +) -> None: + assert im.getbands() == original.getbands() + assert_image_equal( + im.getchannel("A"), + original.getchannel("A"), + f"Diff on {op}: {amount}", + ) + + +@pytest.mark.parametrize("op", ("Color", "Brightness", "Contrast", "Sharpness")) +def test_alpha(op: str) -> None: + # Issue https://github.com/python-pillow/Pillow/issues/899 + # Is alpha preserved through image enhancement? + + original = _half_transparent_image() + + for amount in [0, 0.5, 1.0]: + _check_alpha( + getattr(ImageEnhance, op)(original).enhance(amount), + original, + op, + amount, + ) diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index c9dba2943..1863c20a9 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -1,399 +1,399 @@ -from __future__ import annotations - -from io import BytesIO -from typing import Any - -import pytest - -from PIL import ( - BmpImagePlugin, - EpsImagePlugin, - Image, - ImageFile, - UnidentifiedImageError, - _binary, - features, -) - -from .helper import ( - assert_image, - assert_image_equal, - assert_image_similar, - fromstring, - hopper, - skip_unless_feature, - tostring, -) - -# save original block sizes -MAXBLOCK = ImageFile.MAXBLOCK -SAFEBLOCK = ImageFile.SAFEBLOCK - - -class TestImageFile: - def test_parser(self) -> None: - def roundtrip(format: str) -> tuple[Image.Image, Image.Image]: - im = hopper("L").resize((1000, 1000), Image.Resampling.NEAREST) - if format in ("MSP", "XBM"): - im = im.convert("1") - - test_file = BytesIO() - - im.copy().save(test_file, format) - - data = test_file.getvalue() - - parser = ImageFile.Parser() - parser.feed(data) - im_out = parser.close() - - return im, im_out - - assert_image_equal(*roundtrip("BMP")) - im1, im2 = roundtrip("GIF") - assert_image_similar(im1.convert("P"), im2, 1) - assert_image_equal(*roundtrip("IM")) - assert_image_equal(*roundtrip("MSP")) - if features.check("zlib"): - try: - # force multiple blocks in PNG driver - ImageFile.MAXBLOCK = 8192 - assert_image_equal(*roundtrip("PNG")) - finally: - ImageFile.MAXBLOCK = MAXBLOCK - assert_image_equal(*roundtrip("PPM")) - assert_image_equal(*roundtrip("TIFF")) - assert_image_equal(*roundtrip("XBM")) - assert_image_equal(*roundtrip("TGA")) - assert_image_equal(*roundtrip("PCX")) - - if EpsImagePlugin.has_ghostscript(): - im1, im2 = roundtrip("EPS") - # This test fails on Ubuntu 12.04, PPC (Bigendian) It - # appears to be a ghostscript 9.05 bug, since the - # ghostscript rendering is wonky and the file is identical - # to that written on ubuntu 12.04 x64 - # md5sum: ba974835ff2d6f3f2fd0053a23521d4a - - # EPS comes back in RGB: - assert_image_similar(im1, im2.convert("L"), 20) - - if features.check("jpg"): - im1, im2 = roundtrip("JPEG") # lossy compression - assert_image(im1, im2.mode, im2.size) - - with pytest.raises(OSError): - roundtrip("PDF") - - def test_ico(self) -> None: - with open("Tests/images/python.ico", "rb") as f: - data = f.read() - with ImageFile.Parser() as p: - p.feed(data) - assert (48, 48) == p.image.size - - @skip_unless_feature("webp") - @skip_unless_feature("webp_anim") - def test_incremental_webp(self) -> None: - with ImageFile.Parser() as p: - with open("Tests/images/hopper.webp", "rb") as f: - p.feed(f.read(1024)) - - # Check that insufficient data was given in the first feed - assert not p.image - - p.feed(f.read()) - assert (128, 128) == p.image.size - - @skip_unless_feature("zlib") - def test_safeblock(self) -> None: - im1 = hopper() - - try: - ImageFile.SAFEBLOCK = 1 - im2 = fromstring(tostring(im1, "PNG")) - finally: - ImageFile.SAFEBLOCK = SAFEBLOCK - - assert_image_equal(im1, im2) - - def test_raise_oserror(self) -> None: - with pytest.warns(DeprecationWarning): - with pytest.raises(OSError): - ImageFile.raise_oserror(1) - - def test_raise_typeerror(self) -> None: - with pytest.raises(TypeError): - parser = ImageFile.Parser() - parser.feed(1) - - def test_negative_stride(self) -> None: - with open("Tests/images/raw_negative_stride.bin", "rb") as f: - input = f.read() - p = ImageFile.Parser() - p.feed(input) - with pytest.raises(OSError): - p.close() - - def test_no_format(self) -> None: - buf = BytesIO(b"\x00" * 255) - - class DummyImageFile(ImageFile.ImageFile): - def _open(self) -> None: - self._mode = "RGB" - self._size = (1, 1) - - im = DummyImageFile(buf) - assert im.format is None - assert im.get_format_mimetype() is None - - def test_oserror(self) -> None: - im = Image.new("RGB", (1, 1)) - with pytest.raises(OSError): - im.save(BytesIO(), "JPEG2000", num_resolutions=2) - - def test_truncated(self) -> None: - b = BytesIO( - b"BM000000000000" # head_data - + _binary.o32le( - ImageFile.SAFEBLOCK + 1 + 4 - ) # header_size, so BmpImagePlugin will try to read SAFEBLOCK + 1 bytes - + ( - b"0" * ImageFile.SAFEBLOCK - ) # only SAFEBLOCK bytes, so that the header is truncated - ) - with pytest.raises(OSError) as e: - BmpImagePlugin.BmpImageFile(b) - assert str(e.value) == "Truncated File Read" - - @skip_unless_feature("zlib") - def test_truncated_with_errors(self) -> None: - with Image.open("Tests/images/truncated_image.png") as im: - with pytest.raises(OSError): - im.load() - - # Test that the error is raised if loaded a second time - with pytest.raises(OSError): - im.load() - - @skip_unless_feature("zlib") - def test_truncated_without_errors(self) -> 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 - - @skip_unless_feature("zlib") - def test_broken_datastream_with_errors(self) -> None: - with Image.open("Tests/images/broken_data_stream.png") as im: - with pytest.raises(OSError): - im.load() - - @skip_unless_feature("zlib") - def test_broken_datastream_without_errors(self) -> 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 - - -class MockPyDecoder(ImageFile.PyDecoder): - last: MockPyDecoder - - def __init__(self, mode: str, *args: Any) -> None: - MockPyDecoder.last = self - - super().__init__(mode, *args) - - def decode(self, buffer): - # eof - return -1, 0 - - -class MockPyEncoder(ImageFile.PyEncoder): - last: MockPyEncoder | None - - def __init__(self, mode: str, *args: Any) -> None: - MockPyEncoder.last = self - - super().__init__(mode, *args) - - def encode(self, buffer): - return 1, 1, b"" - - def cleanup(self) -> None: - self.cleanup_called = True - - -xoff, yoff, xsize, ysize = 10, 20, 100, 100 - - -class MockImageFile(ImageFile.ImageFile): - def _open(self) -> None: - self.rawmode = "RGBA" - self._mode = "RGBA" - self._size = (200, 200) - self.tile = [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 32, None)] - - -class CodecsTest: - @classmethod - def setup_class(cls) -> None: - Image.register_decoder("MOCK", MockPyDecoder) - Image.register_encoder("MOCK", MockPyEncoder) - - -class TestPyDecoder(CodecsTest): - def test_setimage(self) -> None: - buf = BytesIO(b"\x00" * 255) - - im = MockImageFile(buf) - - im.load() - - assert MockPyDecoder.last.state.xoff == xoff - assert MockPyDecoder.last.state.yoff == yoff - assert MockPyDecoder.last.state.xsize == xsize - assert MockPyDecoder.last.state.ysize == ysize - - with pytest.raises(ValueError): - MockPyDecoder.last.set_as_raw(b"\x00") - - def test_extents_none(self) -> None: - buf = BytesIO(b"\x00" * 255) - - im = MockImageFile(buf) - im.tile = [("MOCK", None, 32, None)] - - im.load() - - assert MockPyDecoder.last.state.xoff == 0 - assert MockPyDecoder.last.state.yoff == 0 - assert MockPyDecoder.last.state.xsize == 200 - assert MockPyDecoder.last.state.ysize == 200 - - def test_negsize(self) -> None: - buf = BytesIO(b"\x00" * 255) - - im = MockImageFile(buf) - im.tile = [("MOCK", (xoff, yoff, -10, yoff + ysize), 32, None)] - - with pytest.raises(ValueError): - im.load() - - im.tile = [("MOCK", (xoff, yoff, xoff + xsize, -10), 32, None)] - with pytest.raises(ValueError): - im.load() - - def test_oversize(self) -> None: - buf = BytesIO(b"\x00" * 255) - - im = MockImageFile(buf) - im.tile = [("MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 32, None)] - - with pytest.raises(ValueError): - im.load() - - im.tile = [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 32, None)] - with pytest.raises(ValueError): - im.load() - - def test_decode(self) -> None: - decoder = ImageFile.PyDecoder(None) - with pytest.raises(NotImplementedError): - decoder.decode(None) - - -class TestPyEncoder(CodecsTest): - def test_setimage(self) -> None: - buf = BytesIO(b"\x00" * 255) - - im = MockImageFile(buf) - - fp = BytesIO() - ImageFile._save( - im, fp, [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 0, "RGB")] - ) - - assert MockPyEncoder.last - assert MockPyEncoder.last.state.xoff == xoff - assert MockPyEncoder.last.state.yoff == yoff - assert MockPyEncoder.last.state.xsize == xsize - assert MockPyEncoder.last.state.ysize == ysize - - def test_extents_none(self) -> None: - buf = BytesIO(b"\x00" * 255) - - im = MockImageFile(buf) - im.tile = [("MOCK", None, 32, None)] - - fp = BytesIO() - ImageFile._save(im, fp, [("MOCK", None, 0, "RGB")]) - - assert MockPyEncoder.last - assert MockPyEncoder.last.state.xoff == 0 - assert MockPyEncoder.last.state.yoff == 0 - assert MockPyEncoder.last.state.xsize == 200 - assert MockPyEncoder.last.state.ysize == 200 - - def test_negsize(self) -> None: - buf = BytesIO(b"\x00" * 255) - - im = MockImageFile(buf) - - fp = BytesIO() - MockPyEncoder.last = None - with pytest.raises(ValueError): - ImageFile._save( - im, fp, [("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")] - ) - assert MockPyEncoder.last.cleanup_called - - with pytest.raises(ValueError): - ImageFile._save( - im, fp, [("MOCK", (xoff, yoff, xoff + xsize, -10), 0, "RGB")] - ) - - def test_oversize(self) -> None: - buf = BytesIO(b"\x00" * 255) - - im = MockImageFile(buf) - - fp = BytesIO() - with pytest.raises(ValueError): - ImageFile._save( - im, - fp, - [("MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 0, "RGB")], - ) - - with pytest.raises(ValueError): - ImageFile._save( - im, - fp, - [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 0, "RGB")], - ) - - def test_encode(self) -> None: - encoder = ImageFile.PyEncoder(None) - with pytest.raises(NotImplementedError): - encoder.encode(None) - - bytes_consumed, errcode = encoder.encode_to_pyfd() - assert bytes_consumed == 0 - assert ImageFile.ERRORS[errcode] == "bad configuration" - - encoder._pushes_fd = True - with pytest.raises(NotImplementedError): - encoder.encode_to_pyfd() - - with pytest.raises(NotImplementedError): - encoder.encode_to_file(None, None) - - def test_zero_height(self) -> None: - with pytest.raises(UnidentifiedImageError): - Image.open("Tests/images/zero_height.j2k") +from __future__ import annotations + +from io import BytesIO +from typing import Any + +import pytest + +from PIL import ( + BmpImagePlugin, + EpsImagePlugin, + Image, + ImageFile, + UnidentifiedImageError, + _binary, + features, +) + +from .helper import ( + assert_image, + assert_image_equal, + assert_image_similar, + fromstring, + hopper, + skip_unless_feature, + tostring, +) + +# save original block sizes +MAXBLOCK = ImageFile.MAXBLOCK +SAFEBLOCK = ImageFile.SAFEBLOCK + + +class TestImageFile: + def test_parser(self) -> None: + def roundtrip(format: str) -> tuple[Image.Image, Image.Image]: + im = hopper("L").resize((1000, 1000), Image.Resampling.NEAREST) + if format in ("MSP", "XBM"): + im = im.convert("1") + + test_file = BytesIO() + + im.copy().save(test_file, format) + + data = test_file.getvalue() + + parser = ImageFile.Parser() + parser.feed(data) + im_out = parser.close() + + return im, im_out + + assert_image_equal(*roundtrip("BMP")) + im1, im2 = roundtrip("GIF") + assert_image_similar(im1.convert("P"), im2, 1) + assert_image_equal(*roundtrip("IM")) + assert_image_equal(*roundtrip("MSP")) + if features.check("zlib"): + try: + # force multiple blocks in PNG driver + ImageFile.MAXBLOCK = 8192 + assert_image_equal(*roundtrip("PNG")) + finally: + ImageFile.MAXBLOCK = MAXBLOCK + assert_image_equal(*roundtrip("PPM")) + assert_image_equal(*roundtrip("TIFF")) + assert_image_equal(*roundtrip("XBM")) + assert_image_equal(*roundtrip("TGA")) + assert_image_equal(*roundtrip("PCX")) + + if EpsImagePlugin.has_ghostscript(): + im1, im2 = roundtrip("EPS") + # This test fails on Ubuntu 12.04, PPC (Bigendian) It + # appears to be a ghostscript 9.05 bug, since the + # ghostscript rendering is wonky and the file is identical + # to that written on ubuntu 12.04 x64 + # md5sum: ba974835ff2d6f3f2fd0053a23521d4a + + # EPS comes back in RGB: + assert_image_similar(im1, im2.convert("L"), 20) + + if features.check("jpg"): + im1, im2 = roundtrip("JPEG") # lossy compression + assert_image(im1, im2.mode, im2.size) + + with pytest.raises(OSError): + roundtrip("PDF") + + def test_ico(self) -> None: + with open("Tests/images/python.ico", "rb") as f: + data = f.read() + with ImageFile.Parser() as p: + p.feed(data) + assert (48, 48) == p.image.size + + @skip_unless_feature("webp") + @skip_unless_feature("webp_anim") + def test_incremental_webp(self) -> None: + with ImageFile.Parser() as p: + with open("Tests/images/hopper.webp", "rb") as f: + p.feed(f.read(1024)) + + # Check that insufficient data was given in the first feed + assert not p.image + + p.feed(f.read()) + assert (128, 128) == p.image.size + + @skip_unless_feature("zlib") + def test_safeblock(self) -> None: + im1 = hopper() + + try: + ImageFile.SAFEBLOCK = 1 + im2 = fromstring(tostring(im1, "PNG")) + finally: + ImageFile.SAFEBLOCK = SAFEBLOCK + + assert_image_equal(im1, im2) + + def test_raise_oserror(self) -> None: + with pytest.warns(DeprecationWarning): + with pytest.raises(OSError): + ImageFile.raise_oserror(1) + + def test_raise_typeerror(self) -> None: + with pytest.raises(TypeError): + parser = ImageFile.Parser() + parser.feed(1) + + def test_negative_stride(self) -> None: + with open("Tests/images/raw_negative_stride.bin", "rb") as f: + input = f.read() + p = ImageFile.Parser() + p.feed(input) + with pytest.raises(OSError): + p.close() + + def test_no_format(self) -> None: + buf = BytesIO(b"\x00" * 255) + + class DummyImageFile(ImageFile.ImageFile): + def _open(self) -> None: + self._mode = "RGB" + self._size = (1, 1) + + im = DummyImageFile(buf) + assert im.format is None + assert im.get_format_mimetype() is None + + def test_oserror(self) -> None: + im = Image.new("RGB", (1, 1)) + with pytest.raises(OSError): + im.save(BytesIO(), "JPEG2000", num_resolutions=2) + + def test_truncated(self) -> None: + b = BytesIO( + b"BM000000000000" # head_data + + _binary.o32le( + ImageFile.SAFEBLOCK + 1 + 4 + ) # header_size, so BmpImagePlugin will try to read SAFEBLOCK + 1 bytes + + ( + b"0" * ImageFile.SAFEBLOCK + ) # only SAFEBLOCK bytes, so that the header is truncated + ) + with pytest.raises(OSError) as e: + BmpImagePlugin.BmpImageFile(b) + assert str(e.value) == "Truncated File Read" + + @skip_unless_feature("zlib") + def test_truncated_with_errors(self) -> None: + with Image.open("Tests/images/truncated_image.png") as im: + with pytest.raises(OSError): + im.load() + + # Test that the error is raised if loaded a second time + with pytest.raises(OSError): + im.load() + + @skip_unless_feature("zlib") + def test_truncated_without_errors(self) -> 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 + + @skip_unless_feature("zlib") + def test_broken_datastream_with_errors(self) -> None: + with Image.open("Tests/images/broken_data_stream.png") as im: + with pytest.raises(OSError): + im.load() + + @skip_unless_feature("zlib") + def test_broken_datastream_without_errors(self) -> 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 + + +class MockPyDecoder(ImageFile.PyDecoder): + last: MockPyDecoder + + def __init__(self, mode: str, *args: Any) -> None: + MockPyDecoder.last = self + + super().__init__(mode, *args) + + def decode(self, buffer): + # eof + return -1, 0 + + +class MockPyEncoder(ImageFile.PyEncoder): + last: MockPyEncoder | None + + def __init__(self, mode: str, *args: Any) -> None: + MockPyEncoder.last = self + + super().__init__(mode, *args) + + def encode(self, buffer): + return 1, 1, b"" + + def cleanup(self) -> None: + self.cleanup_called = True + + +xoff, yoff, xsize, ysize = 10, 20, 100, 100 + + +class MockImageFile(ImageFile.ImageFile): + def _open(self) -> None: + self.rawmode = "RGBA" + self._mode = "RGBA" + self._size = (200, 200) + self.tile = [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 32, None)] + + +class CodecsTest: + @classmethod + def setup_class(cls) -> None: + Image.register_decoder("MOCK", MockPyDecoder) + Image.register_encoder("MOCK", MockPyEncoder) + + +class TestPyDecoder(CodecsTest): + def test_setimage(self) -> None: + buf = BytesIO(b"\x00" * 255) + + im = MockImageFile(buf) + + im.load() + + assert MockPyDecoder.last.state.xoff == xoff + assert MockPyDecoder.last.state.yoff == yoff + assert MockPyDecoder.last.state.xsize == xsize + assert MockPyDecoder.last.state.ysize == ysize + + with pytest.raises(ValueError): + MockPyDecoder.last.set_as_raw(b"\x00") + + def test_extents_none(self) -> None: + buf = BytesIO(b"\x00" * 255) + + im = MockImageFile(buf) + im.tile = [("MOCK", None, 32, None)] + + im.load() + + assert MockPyDecoder.last.state.xoff == 0 + assert MockPyDecoder.last.state.yoff == 0 + assert MockPyDecoder.last.state.xsize == 200 + assert MockPyDecoder.last.state.ysize == 200 + + def test_negsize(self) -> None: + buf = BytesIO(b"\x00" * 255) + + im = MockImageFile(buf) + im.tile = [("MOCK", (xoff, yoff, -10, yoff + ysize), 32, None)] + + with pytest.raises(ValueError): + im.load() + + im.tile = [("MOCK", (xoff, yoff, xoff + xsize, -10), 32, None)] + with pytest.raises(ValueError): + im.load() + + def test_oversize(self) -> None: + buf = BytesIO(b"\x00" * 255) + + im = MockImageFile(buf) + im.tile = [("MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 32, None)] + + with pytest.raises(ValueError): + im.load() + + im.tile = [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 32, None)] + with pytest.raises(ValueError): + im.load() + + def test_decode(self) -> None: + decoder = ImageFile.PyDecoder(None) + with pytest.raises(NotImplementedError): + decoder.decode(None) + + +class TestPyEncoder(CodecsTest): + def test_setimage(self) -> None: + buf = BytesIO(b"\x00" * 255) + + im = MockImageFile(buf) + + fp = BytesIO() + ImageFile._save( + im, fp, [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 0, "RGB")] + ) + + assert MockPyEncoder.last + assert MockPyEncoder.last.state.xoff == xoff + assert MockPyEncoder.last.state.yoff == yoff + assert MockPyEncoder.last.state.xsize == xsize + assert MockPyEncoder.last.state.ysize == ysize + + def test_extents_none(self) -> None: + buf = BytesIO(b"\x00" * 255) + + im = MockImageFile(buf) + im.tile = [("MOCK", None, 32, None)] + + fp = BytesIO() + ImageFile._save(im, fp, [("MOCK", None, 0, "RGB")]) + + assert MockPyEncoder.last + assert MockPyEncoder.last.state.xoff == 0 + assert MockPyEncoder.last.state.yoff == 0 + assert MockPyEncoder.last.state.xsize == 200 + assert MockPyEncoder.last.state.ysize == 200 + + def test_negsize(self) -> None: + buf = BytesIO(b"\x00" * 255) + + im = MockImageFile(buf) + + fp = BytesIO() + MockPyEncoder.last = None + with pytest.raises(ValueError): + ImageFile._save( + im, fp, [("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")] + ) + assert MockPyEncoder.last.cleanup_called + + with pytest.raises(ValueError): + ImageFile._save( + im, fp, [("MOCK", (xoff, yoff, xoff + xsize, -10), 0, "RGB")] + ) + + def test_oversize(self) -> None: + buf = BytesIO(b"\x00" * 255) + + im = MockImageFile(buf) + + fp = BytesIO() + with pytest.raises(ValueError): + ImageFile._save( + im, + fp, + [("MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 0, "RGB")], + ) + + with pytest.raises(ValueError): + ImageFile._save( + im, + fp, + [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 0, "RGB")], + ) + + def test_encode(self) -> None: + encoder = ImageFile.PyEncoder(None) + with pytest.raises(NotImplementedError): + encoder.encode(None) + + bytes_consumed, errcode = encoder.encode_to_pyfd() + assert bytes_consumed == 0 + assert ImageFile.ERRORS[errcode] == "bad configuration" + + encoder._pushes_fd = True + with pytest.raises(NotImplementedError): + encoder.encode_to_pyfd() + + with pytest.raises(NotImplementedError): + encoder.encode_to_file(None, None) + + def test_zero_height(self) -> None: + with pytest.raises(UnidentifiedImageError): + Image.open("Tests/images/zero_height.j2k") diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 4398f8a30..721cbce54 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -1,1131 +1,1131 @@ -from __future__ import annotations - -import copy -import os -import re -import shutil -import sys -from io import BytesIO -from pathlib import Path -from typing import Any, BinaryIO - -import pytest -from packaging.version import parse as parse_version - -from PIL import Image, ImageDraw, ImageFont, features -from PIL._typing import StrOrBytesPath - -from .helper import ( - assert_image_equal, - assert_image_equal_tofile, - assert_image_similar_tofile, - is_win32, - skip_unless_feature, - skip_unless_feature_version, -) - -FONT_PATH = "Tests/fonts/FreeMono.ttf" -FONT_SIZE = 20 - -TEST_TEXT = "hey you\nyou are awesome\nthis looks awkward" - - -pytestmark = skip_unless_feature("freetype2") - - -def test_sanity() -> None: - version = features.version_module("freetype2") - assert version is not None - assert re.search(r"\d+\.\d+\.\d+$", version) - - -@pytest.fixture( - scope="module", - params=[ - pytest.param(ImageFont.Layout.BASIC), - pytest.param(ImageFont.Layout.RAQM, marks=skip_unless_feature("raqm")), - ], -) -def layout_engine(request: pytest.FixtureRequest) -> ImageFont.Layout: - return request.param - - -@pytest.fixture(scope="module") -def font(layout_engine: ImageFont.Layout) -> ImageFont.FreeTypeFont: - return ImageFont.truetype(FONT_PATH, FONT_SIZE, layout_engine=layout_engine) - - -def test_font_properties(font: ImageFont.FreeTypeFont) -> None: - assert font.path == FONT_PATH - assert font.size == FONT_SIZE - - font_copy = font.font_variant() - assert font_copy.path == FONT_PATH - assert font_copy.size == FONT_SIZE - - font_copy = font.font_variant(size=FONT_SIZE + 1) - assert font_copy.size == FONT_SIZE + 1 - - second_font_path = "Tests/fonts/DejaVuSans/DejaVuSans.ttf" - font_copy = font.font_variant(font=second_font_path) - assert font_copy.path == second_font_path - - -def _render( - font: StrOrBytesPath | BinaryIO, layout_engine: ImageFont.Layout -) -> Image.Image: - txt = "Hello World!" - ttf = ImageFont.truetype(font, FONT_SIZE, layout_engine=layout_engine) - ttf.getbbox(txt) - - img = Image.new("RGB", (256, 64), "white") - d = ImageDraw.Draw(img) - d.text((10, 10), txt, font=ttf, fill="black") - - return img - - -@pytest.mark.parametrize("font", (FONT_PATH, Path(FONT_PATH))) -def test_font_with_name(layout_engine: ImageFont.Layout, font: str | Path) -> None: - _render(font, layout_engine) - - -def test_font_with_filelike(layout_engine: ImageFont.Layout) -> None: - def _font_as_bytes() -> BytesIO: - with open(FONT_PATH, "rb") as f: - font_bytes = BytesIO(f.read()) - return font_bytes - - ttf = ImageFont.truetype(_font_as_bytes(), FONT_SIZE, layout_engine=layout_engine) - ttf_copy = ttf.font_variant() - assert ttf_copy.font_bytes == ttf.font_bytes - - _render(_font_as_bytes(), layout_engine) - # Usage note: making two fonts from the same buffer fails. - # shared_bytes = _font_as_bytes() - # _render(shared_bytes) - # with pytest.raises(Exception): - # _render(shared_bytes) - - -def test_font_with_open_file(layout_engine: ImageFont.Layout) -> None: - with open(FONT_PATH, "rb") as f: - _render(f, layout_engine) - - -def test_render_equal(layout_engine: ImageFont.Layout) -> None: - img_path = _render(FONT_PATH, layout_engine) - with open(FONT_PATH, "rb") as f: - font_filelike = BytesIO(f.read()) - img_filelike = _render(font_filelike, layout_engine) - - assert_image_equal(img_path, img_filelike) - - -def test_non_ascii_path(tmp_path: Path, layout_engine: ImageFont.Layout) -> None: - tempfile = str(tmp_path / ("temp_" + chr(128) + ".ttf")) - try: - shutil.copy(FONT_PATH, tempfile) - except UnicodeEncodeError: - pytest.skip("Non-ASCII path could not be created") - - ImageFont.truetype(tempfile, FONT_SIZE, layout_engine=layout_engine) - - -def test_transparent_background(font: ImageFont.FreeTypeFont) -> None: - im = Image.new(mode="RGBA", size=(300, 100)) - draw = ImageDraw.Draw(im) - - txt = "Hello World!" - draw.text((10, 10), txt, font=font) - - target = "Tests/images/transparent_background_text.png" - assert_image_similar_tofile(im, target, 4.09) - - target = "Tests/images/transparent_background_text_L.png" - assert_image_similar_tofile(im.convert("L"), target, 0.01) - - -def test_I16(font: ImageFont.FreeTypeFont) -> None: - im = Image.new(mode="I;16", size=(300, 100)) - draw = ImageDraw.Draw(im) - - txt = "Hello World!" - draw.text((10, 10), txt, fill=0xFFFE, font=font) - - assert im.getpixel((12, 14)) == 0xFFFE - - target = "Tests/images/transparent_background_text_L.png" - assert_image_similar_tofile(im.convert("L"), target, 0.01) - - -def test_textbbox_equal(font: ImageFont.FreeTypeFont) -> None: - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - - txt = "Hello World!" - bbox = draw.textbbox((10, 10), txt, font) - draw.text((10, 10), txt, font=font) - draw.rectangle(bbox) - - assert_image_similar_tofile(im, "Tests/images/rectangle_surrounding_text.png", 2.5) - - -@pytest.mark.parametrize( - "text, mode, fontname, size, length_basic, length_raqm", - ( - # basic test - ("text", "L", "FreeMono.ttf", 15, 36, 36), - ("text", "1", "FreeMono.ttf", 15, 36, 36), - # issue 4177 - ("rrr", "L", "DejaVuSans/DejaVuSans.ttf", 18, 21, 22.21875), - ("rrr", "1", "DejaVuSans/DejaVuSans.ttf", 18, 24, 22.21875), - # test 'l' not including extra margin - # using exact value 2047 / 64 for raqm, checked with debugger - ("ill", "L", "OpenSansCondensed-LightItalic.ttf", 63, 33, 31.984375), - ("ill", "1", "OpenSansCondensed-LightItalic.ttf", 63, 33, 31.984375), - ), -) -def test_getlength( - text: str, - mode: str, - fontname: str, - size: int, - layout_engine: ImageFont.Layout, - length_basic: int, - length_raqm: float, -) -> None: - f = ImageFont.truetype("Tests/fonts/" + fontname, size, layout_engine=layout_engine) - - im = Image.new(mode, (1, 1), 0) - d = ImageDraw.Draw(im) - - if layout_engine == ImageFont.Layout.BASIC: - length = d.textlength(text, f) - assert length == length_basic - else: - # disable kerning, kerning metrics changed - length = d.textlength(text, f, features=["-kern"]) - assert length == length_raqm - - -def test_float_size() -> None: - lengths = [] - for size in (48, 48.5, 49): - f = ImageFont.truetype( - "Tests/fonts/NotoSans-Regular.ttf", size, layout_engine=layout_engine - ) - lengths.append(f.getlength("text")) - assert lengths[0] != lengths[1] != lengths[2] - - -def test_render_multiline(font: ImageFont.FreeTypeFont) -> None: - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - line_spacing = font.getbbox("A")[3] + 4 - lines = TEST_TEXT.split("\n") - y = 0 - for line in lines: - draw.text((0, y), line, font=font) - y += line_spacing - - # some versions of freetype have different horizontal spacing. - # setting a tight epsilon, I'm showing the original test failure - # at epsilon = ~38. - assert_image_similar_tofile(im, "Tests/images/multiline_text.png", 6.2) - - -def test_render_multiline_text(font: ImageFont.FreeTypeFont) -> None: - # Test that text() correctly connects to multiline_text() - # and that align defaults to left - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.text((0, 0), TEST_TEXT, font=font) - - assert_image_similar_tofile(im, "Tests/images/multiline_text.png", 0.01) - - # Test that text() can pass on additional arguments - # to multiline_text() - draw.text( - (0, 0), TEST_TEXT, fill=None, font=font, anchor=None, spacing=4, align="left" - ) - draw.text((0, 0), TEST_TEXT, None, font, None, 4, "left") - - -@pytest.mark.parametrize( - "align, ext", (("left", ""), ("center", "_center"), ("right", "_right")) -) -def test_render_multiline_text_align( - font: ImageFont.FreeTypeFont, align: str, ext: str -) -> None: - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.multiline_text((0, 0), TEST_TEXT, font=font, align=align) - - assert_image_similar_tofile(im, f"Tests/images/multiline_text{ext}.png", 0.01) - - -def test_unknown_align(font: ImageFont.FreeTypeFont) -> None: - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - - # Act/Assert - with pytest.raises(ValueError): - draw.multiline_text((0, 0), TEST_TEXT, font=font, align="unknown") - - -def test_draw_align(font: ImageFont.FreeTypeFont) -> None: - im = Image.new("RGB", (300, 100), "white") - draw = ImageDraw.Draw(im) - line = "some text" - draw.text((100, 40), line, (0, 0, 0), font=font, align="left") - - -def test_multiline_bbox(font: ImageFont.FreeTypeFont) -> None: - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - - # Test that textbbox() correctly connects to multiline_textbbox() - assert draw.textbbox((0, 0), TEST_TEXT, font=font) == draw.multiline_textbbox( - (0, 0), TEST_TEXT, font=font - ) - - # Test that multiline_textbbox corresponds to ImageFont.textbbox() - # for single line text - assert font.getbbox("A") == draw.multiline_textbbox((0, 0), "A", font=font) - - # Test that textbbox() can pass on additional arguments - # to multiline_textbbox() - draw.textbbox((0, 0), TEST_TEXT, font=font, spacing=4) - - -def test_multiline_width(font: ImageFont.FreeTypeFont) -> None: - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - - assert ( - draw.textbbox((0, 0), "longest line", font=font)[2] - == draw.multiline_textbbox((0, 0), "longest line\nline", font=font)[2] - ) - - -def test_multiline_spacing(font: ImageFont.FreeTypeFont) -> None: - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.multiline_text((0, 0), TEST_TEXT, font=font, spacing=10) - - assert_image_similar_tofile(im, "Tests/images/multiline_text_spacing.png", 2.5) - - -@pytest.mark.parametrize( - "orientation", (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270) -) -def test_rotated_transposed_font( - font: ImageFont.FreeTypeFont, orientation: Image.Transpose -) -> None: - img_gray = Image.new("L", (100, 100)) - draw = ImageDraw.Draw(img_gray) - word = "testing" - - transposed_font = ImageFont.TransposedFont(font, orientation=orientation) - - # Original font - draw.font = font - bbox_a = draw.textbbox((10, 10), word) - - # Rotated font - draw.font = transposed_font - bbox_b = draw.textbbox((20, 20), word) - - # Check (w, h) of box a is (h, w) of box b - assert ( - bbox_a[2] - bbox_a[0], - bbox_a[3] - bbox_a[1], - ) == ( - bbox_b[3] - bbox_b[1], - bbox_b[2] - bbox_b[0], - ) - - # Check top left co-ordinates are correct - assert bbox_b[:2] == (20, 20) - - # text length is undefined for vertical text - with pytest.raises(ValueError): - draw.textlength(word) - - -@pytest.mark.parametrize( - "orientation", - ( - None, - Image.Transpose.ROTATE_180, - Image.Transpose.FLIP_LEFT_RIGHT, - Image.Transpose.FLIP_TOP_BOTTOM, - ), -) -def test_unrotated_transposed_font( - font: ImageFont.FreeTypeFont, orientation: Image.Transpose -) -> None: - img_gray = Image.new("L", (100, 100)) - draw = ImageDraw.Draw(img_gray) - word = "testing" - - transposed_font = ImageFont.TransposedFont(font, orientation=orientation) - - # Original font - draw.font = font - bbox_a = draw.textbbox((10, 10), word) - length_a = draw.textlength(word) - - # Rotated font - draw.font = transposed_font - bbox_b = draw.textbbox((20, 20), word) - length_b = draw.textlength(word) - - # Check boxes a and b are same size - assert ( - bbox_a[2] - bbox_a[0], - bbox_a[3] - bbox_a[1], - ) == ( - bbox_b[2] - bbox_b[0], - bbox_b[3] - bbox_b[1], - ) - - # Check top left co-ordinates are correct - assert bbox_b[:2] == (20, 20) - - assert length_a == length_b - - -@pytest.mark.parametrize( - "orientation", (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270) -) -def test_rotated_transposed_font_get_mask( - font: ImageFont.FreeTypeFont, orientation: Image.Transpose -) -> None: - # Arrange - text = "mask this" - transposed_font = ImageFont.TransposedFont(font, orientation=orientation) - - # Act - mask = transposed_font.getmask(text) - - # Assert - assert mask.size == (13, 108) - - -@pytest.mark.parametrize( - "orientation", - ( - None, - Image.Transpose.ROTATE_180, - Image.Transpose.FLIP_LEFT_RIGHT, - Image.Transpose.FLIP_TOP_BOTTOM, - ), -) -def test_unrotated_transposed_font_get_mask( - font: ImageFont.FreeTypeFont, orientation: Image.Transpose -) -> None: - # Arrange - text = "mask this" - transposed_font = ImageFont.TransposedFont(font, orientation=orientation) - - # Act - mask = transposed_font.getmask(text) - - # Assert - assert mask.size == (108, 13) - - -def test_free_type_font_get_name(font: ImageFont.FreeTypeFont) -> None: - assert ("FreeMono", "Regular") == font.getname() - - -def test_free_type_font_get_metrics(font: ImageFont.FreeTypeFont) -> None: - ascent, descent = font.getmetrics() - - assert isinstance(ascent, int) - assert isinstance(descent, int) - assert (ascent, descent) == (16, 4) - - -def test_free_type_font_get_mask(font: ImageFont.FreeTypeFont) -> None: - # Arrange - text = "mask this" - - # Act - mask = font.getmask(text) - - # Assert - assert mask.size == (108, 13) - - -def test_load_path_not_found() -> None: - # Arrange - filename = "somefilenamethatdoesntexist.ttf" - - # Act/Assert - with pytest.raises(OSError): - ImageFont.load_path(filename) - with pytest.raises(OSError): - ImageFont.truetype(filename) - - -def test_load_non_font_bytes() -> None: - with open("Tests/images/hopper.jpg", "rb") as f: - with pytest.raises(OSError): - ImageFont.truetype(f) - - -def test_default_font() -> None: - # Arrange - txt = "This is a default font using FreeType support." - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - - # Act - default_font = ImageFont.load_default() - draw.text((10, 10), txt, font=default_font) - - larger_default_font = ImageFont.load_default(size=14) - draw.text((10, 60), txt, font=larger_default_font) - - # Assert - assert_image_equal_tofile(im, "Tests/images/default_font_freetype.png") - - -@pytest.mark.parametrize("mode", (None, "1", "RGBA")) -def test_getbbox(font: ImageFont.FreeTypeFont, mode: str | None) -> None: - assert (0, 4, 12, 16) == font.getbbox("A", mode) - - -def test_getbbox_empty(font: ImageFont.FreeTypeFont) -> None: - # issue #2614, should not crash. - assert (0, 0, 0, 0) == font.getbbox("") - - -def test_render_empty(font: ImageFont.FreeTypeFont) -> None: - # issue 2666 - im = Image.new(mode="RGB", size=(300, 100)) - target = im.copy() - draw = ImageDraw.Draw(im) - # should not crash here. - draw.text((10, 10), "", font=font) - assert_image_equal(im, target) - - -def test_unicode_extended(layout_engine: ImageFont.Layout) -> None: - # issue #3777 - text = "A\u278A\U0001F12B" - target = "Tests/images/unicode_extended.png" - - ttf = ImageFont.truetype( - "Tests/fonts/NotoSansSymbols-Regular.ttf", - FONT_SIZE, - layout_engine=layout_engine, - ) - img = Image.new("RGB", (100, 60)) - d = ImageDraw.Draw(img) - d.text((10, 10), text, font=ttf) - - # fails with 14.7 - assert_image_similar_tofile(img, target, 6.2) - - -@pytest.mark.parametrize( - "platform, font_directory", - (("linux", "/usr/local/share/fonts"), ("darwin", "/System/Library/Fonts")), -) -@pytest.mark.skipif(is_win32(), reason="requires Unix or macOS") -def test_find_font( - monkeypatch: pytest.MonkeyPatch, platform: str, font_directory: str -) -> None: - def _test_fake_loading_font(path_to_fake: str, fontname: str) -> None: - # Make a copy of FreeTypeFont so we can patch the original - free_type_font = copy.deepcopy(ImageFont.FreeTypeFont) - with monkeypatch.context() as m: - m.setattr(ImageFont, "_FreeTypeFont", free_type_font, raising=False) - - def loadable_font( - filepath: str, size: int, index: int, encoding: str, *args: Any - ): - _freeTypeFont = getattr(ImageFont, "_FreeTypeFont") - if filepath == path_to_fake: - return _freeTypeFont(FONT_PATH, size, index, encoding, *args) - return _freeTypeFont(filepath, size, index, encoding, *args) - - m.setattr(ImageFont, "FreeTypeFont", loadable_font) - font = ImageFont.truetype(fontname) - # Make sure it's loaded - name = font.getname() - assert ("FreeMono", "Regular") == name - - # A lot of mocking here - this is more for hitting code and - # catching syntax like errors - monkeypatch.setattr(sys, "platform", platform) - if platform == "linux": - monkeypatch.setenv("XDG_DATA_DIRS", "/usr/share/:/usr/local/share/") - - def fake_walker(path: str) -> list[tuple[str, list[str], list[str]]]: - if path == font_directory: - return [ - ( - path, - [], - ["Arial.ttf", "Single.otf", "Duplicate.otf", "Duplicate.ttf"], - ) - ] - return [(path, [], ["some_random_font.ttf"])] - - monkeypatch.setattr(os, "walk", fake_walker) - - # Test that the font loads both with and without the extension - _test_fake_loading_font(font_directory + "/Arial.ttf", "Arial.ttf") - _test_fake_loading_font(font_directory + "/Arial.ttf", "Arial") - - # Test that non-ttf fonts can be found without the extension - _test_fake_loading_font(font_directory + "/Single.otf", "Single") - - # Test that ttf fonts are preferred if the extension is not specified - _test_fake_loading_font(font_directory + "/Duplicate.ttf", "Duplicate") - - -def test_imagefont_getters(font: ImageFont.FreeTypeFont) -> None: - assert font.getmetrics() == (16, 4) - assert font.font.ascent == 16 - assert font.font.descent == 4 - assert font.font.height == 20 - assert font.font.x_ppem == 20 - assert font.font.y_ppem == 20 - assert font.font.glyphs == 4177 - assert font.getbbox("A") == (0, 4, 12, 16) - assert font.getbbox("AB") == (0, 4, 24, 16) - assert font.getbbox("M") == (0, 4, 12, 16) - assert font.getbbox("y") == (0, 7, 12, 20) - assert font.getbbox("a") == (0, 7, 12, 16) - assert font.getlength("A") == 12 - assert font.getlength("AB") == 24 - assert font.getlength("M") == 12 - assert font.getlength("y") == 12 - assert font.getlength("a") == 12 - - -@pytest.mark.parametrize("stroke_width", (0, 2)) -def test_getsize_stroke(font: ImageFont.FreeTypeFont, stroke_width: int) -> None: - assert font.getbbox("A", stroke_width=stroke_width) == ( - 0 - stroke_width, - 4 - stroke_width, - 12 + stroke_width, - 16 + stroke_width, - ) - - -def test_complex_font_settings() -> None: - t = ImageFont.truetype(FONT_PATH, FONT_SIZE, layout_engine=ImageFont.Layout.BASIC) - with pytest.raises(KeyError): - t.getmask("абвг", direction="rtl") - with pytest.raises(KeyError): - t.getmask("абвг", features=["-kern"]) - with pytest.raises(KeyError): - t.getmask("абвг", language="sr") - - -def test_variation_get(font: ImageFont.FreeTypeFont) -> None: - version = features.version_module("freetype2") - assert version is not None - freetype = parse_version(version) - if freetype < parse_version("2.9.1"): - with pytest.raises(NotImplementedError): - font.get_variation_names() - with pytest.raises(NotImplementedError): - font.get_variation_axes() - return - - with pytest.raises(OSError): - font.get_variation_names() - with pytest.raises(OSError): - font.get_variation_axes() - - font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf") - assert font.get_variation_names(), [ - b"ExtraLight", - b"Light", - b"Regular", - b"Semibold", - b"Bold", - b"Black", - b"Black Medium Contrast", - b"Black High Contrast", - b"Default", - ] - assert font.get_variation_axes() == [ - {"name": b"Weight", "minimum": 200, "maximum": 900, "default": 389}, - {"name": b"Contrast", "minimum": 0, "maximum": 100, "default": 0}, - ] - - font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf") - assert font.get_variation_names() == [ - b"20", - b"40", - b"60", - b"80", - b"100", - b"120", - b"140", - b"160", - b"180", - b"200", - b"220", - b"240", - b"260", - b"280", - b"300", - b"Regular", - ] - assert font.get_variation_axes() == [ - {"name": b"Size", "minimum": 0, "maximum": 300, "default": 0} - ] - - -def _check_text(font: ImageFont.FreeTypeFont, path: str, epsilon: float) -> None: - im = Image.new("RGB", (100, 75), "white") - d = ImageDraw.Draw(im) - d.text((10, 10), "Text", font=font, fill="black") - - try: - assert_image_similar_tofile(im, path, epsilon) - except AssertionError: - if "_adobe" in path: - path = path.replace("_adobe", "_adobe_older_harfbuzz") - assert_image_similar_tofile(im, path, epsilon) - else: - raise - - -def test_variation_set_by_name(font: ImageFont.FreeTypeFont) -> None: - version = features.version_module("freetype2") - assert version is not None - freetype = parse_version(version) - if freetype < parse_version("2.9.1"): - with pytest.raises(NotImplementedError): - font.set_variation_by_name("Bold") - return - - with pytest.raises(OSError): - font.set_variation_by_name("Bold") - - font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf", 36) - _check_text(font, "Tests/images/variation_adobe.png", 11) - for name in ["Bold", b"Bold"]: - font.set_variation_by_name(name) - assert font.getname()[1] == "Bold" - _check_text(font, "Tests/images/variation_adobe_name.png", 16) - - font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf", 36) - _check_text(font, "Tests/images/variation_tiny.png", 40) - for name in ["200", b"200"]: - font.set_variation_by_name(name) - assert font.getname()[1] == "200" - _check_text(font, "Tests/images/variation_tiny_name.png", 40) - - -def test_variation_set_by_axes(font: ImageFont.FreeTypeFont) -> None: - version = features.version_module("freetype2") - assert version is not None - freetype = parse_version(version) - if freetype < parse_version("2.9.1"): - with pytest.raises(NotImplementedError): - font.set_variation_by_axes([100]) - return - - with pytest.raises(OSError): - font.set_variation_by_axes([500, 50]) - - font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf", 36) - font.set_variation_by_axes([500, 50]) - _check_text(font, "Tests/images/variation_adobe_axes.png", 11.05) - - font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf", 36) - font.set_variation_by_axes([100]) - _check_text(font, "Tests/images/variation_tiny_axes.png", 32.5) - - -@pytest.mark.parametrize( - "anchor, left, top", - ( - # test horizontal anchors - ("ls", 0, -36), - ("ms", -64, -36), - ("rs", -128, -36), - # test vertical anchors - ("ma", -64, 16), - ("mt", -64, 0), - ("mm", -64, -17), - ("mb", -64, -44), - ("md", -64, -51), - ), - ids=("ls", "ms", "rs", "ma", "mt", "mm", "mb", "md"), -) -def test_anchor( - layout_engine: ImageFont.Layout, anchor: str, left: int, top: int -) -> None: - name, text = "quick", "Quick" - path = f"Tests/images/test_anchor_{name}_{anchor}.png" - - if layout_engine == ImageFont.Layout.RAQM: - width, height = (129, 44) - else: - width, height = (128, 44) - - bbox_expected = (left, top, left + width, top + height) - - f = ImageFont.truetype( - "Tests/fonts/NotoSans-Regular.ttf", 48, layout_engine=layout_engine - ) - - im = Image.new("RGB", (200, 200), "white") - d = ImageDraw.Draw(im) - d.line(((0, 100), (200, 100)), "gray") - d.line(((100, 0), (100, 200)), "gray") - d.text((100, 100), text, fill="black", anchor=anchor, font=f) - - assert d.textbbox((0, 0), text, f, anchor=anchor) == bbox_expected - - assert_image_similar_tofile(im, path, 7) - - -@pytest.mark.parametrize( - "anchor, align", - ( - # test horizontal anchors - ("lm", "left"), - ("lm", "center"), - ("lm", "right"), - ("mm", "left"), - ("mm", "center"), - ("mm", "right"), - ("rm", "left"), - ("rm", "center"), - ("rm", "right"), - # test vertical anchors - ("ma", "center"), - # ("mm", "center"), # duplicate - ("md", "center"), - ), -) -def test_anchor_multiline( - layout_engine: ImageFont.Layout, anchor: str, align: str -) -> None: - target = f"Tests/images/test_anchor_multiline_{anchor}_{align}.png" - text = "a\nlong\ntext sample" - - f = ImageFont.truetype( - "Tests/fonts/NotoSans-Regular.ttf", 48, layout_engine=layout_engine - ) - - # test render - im = Image.new("RGB", (600, 400), "white") - d = ImageDraw.Draw(im) - d.line(((0, 200), (600, 200)), "gray") - d.line(((300, 0), (300, 400)), "gray") - d.multiline_text((300, 200), text, fill="black", anchor=anchor, font=f, align=align) - - assert_image_similar_tofile(im, target, 4) - - -def test_anchor_invalid(font: ImageFont.FreeTypeFont) -> None: - im = Image.new("RGB", (100, 100), "white") - d = ImageDraw.Draw(im) - d.font = font - - for anchor in ["", "l", "a", "lax", "sa", "xa", "lx"]: - with pytest.raises(ValueError): - font.getmask2("hello", anchor=anchor) - with pytest.raises(ValueError): - font.getbbox("hello", anchor=anchor) - with pytest.raises(ValueError): - d.text((0, 0), "hello", anchor=anchor) - with pytest.raises(ValueError): - d.textbbox((0, 0), "hello", anchor=anchor) - with pytest.raises(ValueError): - d.multiline_text((0, 0), "foo\nbar", anchor=anchor) - with pytest.raises(ValueError): - d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor) - for anchor in ["lt", "lb"]: - with pytest.raises(ValueError): - d.multiline_text((0, 0), "foo\nbar", anchor=anchor) - with pytest.raises(ValueError): - d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor) - - -@pytest.mark.parametrize("bpp", (1, 2, 4, 8)) -def test_bitmap_font(layout_engine: ImageFont.Layout, bpp: int) -> None: - text = "Bitmap Font" - layout_name = ["basic", "raqm"][layout_engine] - target = f"Tests/images/bitmap_font_{bpp}_{layout_name}.png" - font = ImageFont.truetype( - f"Tests/fonts/DejaVuSans/DejaVuSans-24-{bpp}-stripped.ttf", - 24, - layout_engine=layout_engine, - ) - - im = Image.new("RGB", (160, 35), "white") - draw = ImageDraw.Draw(im) - draw.text((2, 2), text, "black", font) - - assert_image_equal_tofile(im, target) - - -def test_bitmap_font_stroke(layout_engine: ImageFont.Layout) -> None: - text = "Bitmap Font" - layout_name = ["basic", "raqm"][layout_engine] - target = f"Tests/images/bitmap_font_stroke_{layout_name}.png" - font = ImageFont.truetype( - "Tests/fonts/DejaVuSans/DejaVuSans-24-8-stripped.ttf", - 24, - layout_engine=layout_engine, - ) - - im = Image.new("RGB", (160, 35), "white") - draw = ImageDraw.Draw(im) - draw.text((2, 2), text, "black", font, stroke_width=2, stroke_fill="red") - - assert_image_similar_tofile(im, target, 0.03) - - -@pytest.mark.parametrize("embedded_color", (False, True)) -def test_bitmap_blend(layout_engine: ImageFont.Layout, embedded_color: bool) -> None: - font = ImageFont.truetype( - "Tests/fonts/EBDTTestFont.ttf", size=64, layout_engine=layout_engine - ) - - im = Image.new("RGBA", (128, 96), "white") - d = ImageDraw.Draw(im) - d.text((16, 16), "AA", font=font, fill="#8E2F52", embedded_color=embedded_color) - - assert_image_equal_tofile(im, "Tests/images/bitmap_font_blend.png") - - -def test_standard_embedded_color(layout_engine: ImageFont.Layout) -> None: - txt = "Hello World!" - ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine) - ttf.getbbox(txt) - - im = Image.new("RGB", (300, 64), "white") - d = ImageDraw.Draw(im) - d.text((10, 10), txt, font=ttf, fill="#fa6", embedded_color=True) - - assert_image_similar_tofile(im, "Tests/images/standard_embedded.png", 3.1) - - -@pytest.mark.parametrize("fontmode", ("1", "L", "RGBA")) -def test_float_coord(layout_engine: ImageFont.Layout, fontmode: str) -> None: - txt = "Hello World!" - ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine) - - im = Image.new("RGB", (300, 64), "white") - d = ImageDraw.Draw(im) - if fontmode == "1": - d.fontmode = "1" - - embedded_color = fontmode == "RGBA" - d.text((9.5, 9.5), txt, font=ttf, fill="#fa6", embedded_color=embedded_color) - try: - assert_image_similar_tofile(im, "Tests/images/text_float_coord.png", 3.9) - except AssertionError: - if fontmode == "1" and layout_engine == ImageFont.Layout.BASIC: - assert_image_similar_tofile( - im, "Tests/images/text_float_coord_1_alt.png", 1 - ) - else: - raise - - -def test_cbdt(layout_engine: ImageFont.Layout) -> None: - try: - font = ImageFont.truetype( - "Tests/fonts/CBDTTestFont.ttf", size=64, layout_engine=layout_engine - ) - - im = Image.new("RGB", (128, 96), "white") - d = ImageDraw.Draw(im) - - d.text((16, 16), "AB", font=font, embedded_color=True) - - assert_image_equal_tofile(im, "Tests/images/cbdt.png") - except OSError as e: # pragma: no cover - assert str(e) in ("unimplemented feature", "unknown file format") - pytest.skip("freetype compiled without libpng or CBDT support") - - -def test_cbdt_mask(layout_engine: ImageFont.Layout) -> None: - try: - font = ImageFont.truetype( - "Tests/fonts/CBDTTestFont.ttf", size=64, layout_engine=layout_engine - ) - - im = Image.new("RGB", (128, 96), "white") - d = ImageDraw.Draw(im) - - d.text((16, 16), "AB", "green", font=font) - - assert_image_equal_tofile(im, "Tests/images/cbdt_mask.png") - except OSError as e: # pragma: no cover - assert str(e) in ("unimplemented feature", "unknown file format") - pytest.skip("freetype compiled without libpng or CBDT support") - - -def test_sbix(layout_engine: ImageFont.Layout) -> None: - try: - font = ImageFont.truetype( - "Tests/fonts/chromacheck-sbix.woff", size=300, layout_engine=layout_engine - ) - - im = Image.new("RGB", (400, 400), "white") - d = ImageDraw.Draw(im) - - d.text((50, 50), "\uE901", font=font, embedded_color=True) - - assert_image_similar_tofile(im, "Tests/images/chromacheck-sbix.png", 1) - except OSError as e: # pragma: no cover - assert str(e) in ("unimplemented feature", "unknown file format") - pytest.skip("freetype compiled without libpng or SBIX support") - - -def test_sbix_mask(layout_engine: ImageFont.Layout) -> None: - try: - font = ImageFont.truetype( - "Tests/fonts/chromacheck-sbix.woff", size=300, layout_engine=layout_engine - ) - - im = Image.new("RGB", (400, 400), "white") - d = ImageDraw.Draw(im) - - d.text((50, 50), "\uE901", (100, 0, 0), font=font) - - assert_image_similar_tofile(im, "Tests/images/chromacheck-sbix_mask.png", 1) - except OSError as e: # pragma: no cover - assert str(e) in ("unimplemented feature", "unknown file format") - pytest.skip("freetype compiled without libpng or SBIX support") - - -@skip_unless_feature_version("freetype2", "2.10.0") -def test_colr(layout_engine: ImageFont.Layout) -> None: - font = ImageFont.truetype( - "Tests/fonts/BungeeColor-Regular_colr_Windows.ttf", - size=64, - layout_engine=layout_engine, - ) - - im = Image.new("RGB", (300, 75), "white") - d = ImageDraw.Draw(im) - - d.text((15, 5), "Bungee", font=font, embedded_color=True) - - assert_image_similar_tofile(im, "Tests/images/colr_bungee.png", 21) - - -@skip_unless_feature_version("freetype2", "2.10.0") -def test_colr_mask(layout_engine: ImageFont.Layout) -> None: - font = ImageFont.truetype( - "Tests/fonts/BungeeColor-Regular_colr_Windows.ttf", - size=64, - layout_engine=layout_engine, - ) - - im = Image.new("RGB", (300, 75), "white") - d = ImageDraw.Draw(im) - - d.text((15, 5), "Bungee", "black", font=font) - - assert_image_similar_tofile(im, "Tests/images/colr_bungee_mask.png", 22) - - -def test_woff2(layout_engine: ImageFont.Layout) -> None: - try: - font = ImageFont.truetype( - "Tests/fonts/OpenSans.woff2", - size=64, - layout_engine=layout_engine, - ) - except OSError as e: - assert str(e) in ("unimplemented feature", "unknown file format") - pytest.skip("FreeType compiled without brotli or WOFF2 support") - - im = Image.new("RGB", (350, 100), "white") - d = ImageDraw.Draw(im) - - d.text((15, 5), "OpenSans", "black", font=font) - - assert_image_similar_tofile(im, "Tests/images/test_woff2.png", 5) - - -def test_render_mono_size() -> None: - # issue 4177 - - im = Image.new("P", (100, 30), "white") - draw = ImageDraw.Draw(im) - ttf = ImageFont.truetype( - "Tests/fonts/DejaVuSans/DejaVuSans.ttf", - 18, - layout_engine=ImageFont.Layout.BASIC, - ) - - draw.text((10, 10), "r" * 10, "black", ttf) - assert_image_equal_tofile(im, "Tests/images/text_mono.gif") - - -def test_too_many_characters(font: ImageFont.FreeTypeFont) -> None: - with pytest.raises(ValueError): - font.getlength("A" * 1_000_001) - with pytest.raises(ValueError): - font.getbbox("A" * 1_000_001) - with pytest.raises(ValueError): - font.getmask2("A" * 1_000_001) - - transposed_font = ImageFont.TransposedFont(font) - with pytest.raises(ValueError): - transposed_font.getlength("A" * 1_000_001) - - imagefont = ImageFont.ImageFont() - with pytest.raises(ValueError): - imagefont.getlength("A" * 1_000_001) - with pytest.raises(ValueError): - imagefont.getbbox("A" * 1_000_001) - with pytest.raises(ValueError): - imagefont.getmask("A" * 1_000_001) - - -@pytest.mark.parametrize( - "test_file", - [ - "Tests/fonts/oom-e8e927ba6c0d38274a37c1567560eb33baf74627.ttf", - "Tests/fonts/oom-4da0210eb7081b0bf15bf16cc4c52ce02c1e1bbc.ttf", - ], -) -def test_oom(test_file: str) -> None: - with open(test_file, "rb") as f: - font = ImageFont.truetype(BytesIO(f.read())) - with pytest.raises(Image.DecompressionBombError): - font.getmask("Test Text") - - -def test_raqm_missing_warning(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setattr(ImageFont.core, "HAVE_RAQM", False) - with pytest.warns(UserWarning) as record: - font = ImageFont.truetype( - FONT_PATH, FONT_SIZE, layout_engine=ImageFont.Layout.RAQM - ) - assert font.layout_engine == ImageFont.Layout.BASIC - assert str(record[-1].message) == ( - "Raqm layout was requested, but Raqm is not available. " - "Falling back to basic layout." - ) - - -@pytest.mark.parametrize("size", [-1, 0]) -def test_invalid_truetype_sizes_raise_valueerror( - layout_engine: ImageFont.Layout, size: int -) -> None: - with pytest.raises(ValueError): - ImageFont.truetype(FONT_PATH, size, layout_engine=layout_engine) +from __future__ import annotations + +import copy +import os +import re +import shutil +import sys +from io import BytesIO +from pathlib import Path +from typing import Any, BinaryIO + +import pytest +from packaging.version import parse as parse_version + +from PIL import Image, ImageDraw, ImageFont, features +from PIL._typing import StrOrBytesPath + +from .helper import ( + assert_image_equal, + assert_image_equal_tofile, + assert_image_similar_tofile, + is_win32, + skip_unless_feature, + skip_unless_feature_version, +) + +FONT_PATH = "Tests/fonts/FreeMono.ttf" +FONT_SIZE = 20 + +TEST_TEXT = "hey you\nyou are awesome\nthis looks awkward" + + +pytestmark = skip_unless_feature("freetype2") + + +def test_sanity() -> None: + version = features.version_module("freetype2") + assert version is not None + assert re.search(r"\d+\.\d+\.\d+$", version) + + +@pytest.fixture( + scope="module", + params=[ + pytest.param(ImageFont.Layout.BASIC), + pytest.param(ImageFont.Layout.RAQM, marks=skip_unless_feature("raqm")), + ], +) +def layout_engine(request: pytest.FixtureRequest) -> ImageFont.Layout: + return request.param + + +@pytest.fixture(scope="module") +def font(layout_engine: ImageFont.Layout) -> ImageFont.FreeTypeFont: + return ImageFont.truetype(FONT_PATH, FONT_SIZE, layout_engine=layout_engine) + + +def test_font_properties(font: ImageFont.FreeTypeFont) -> None: + assert font.path == FONT_PATH + assert font.size == FONT_SIZE + + font_copy = font.font_variant() + assert font_copy.path == FONT_PATH + assert font_copy.size == FONT_SIZE + + font_copy = font.font_variant(size=FONT_SIZE + 1) + assert font_copy.size == FONT_SIZE + 1 + + second_font_path = "Tests/fonts/DejaVuSans/DejaVuSans.ttf" + font_copy = font.font_variant(font=second_font_path) + assert font_copy.path == second_font_path + + +def _render( + font: StrOrBytesPath | BinaryIO, layout_engine: ImageFont.Layout +) -> Image.Image: + txt = "Hello World!" + ttf = ImageFont.truetype(font, FONT_SIZE, layout_engine=layout_engine) + ttf.getbbox(txt) + + img = Image.new("RGB", (256, 64), "white") + d = ImageDraw.Draw(img) + d.text((10, 10), txt, font=ttf, fill="black") + + return img + + +@pytest.mark.parametrize("font", (FONT_PATH, Path(FONT_PATH))) +def test_font_with_name(layout_engine: ImageFont.Layout, font: str | Path) -> None: + _render(font, layout_engine) + + +def test_font_with_filelike(layout_engine: ImageFont.Layout) -> None: + def _font_as_bytes() -> BytesIO: + with open(FONT_PATH, "rb") as f: + font_bytes = BytesIO(f.read()) + return font_bytes + + ttf = ImageFont.truetype(_font_as_bytes(), FONT_SIZE, layout_engine=layout_engine) + ttf_copy = ttf.font_variant() + assert ttf_copy.font_bytes == ttf.font_bytes + + _render(_font_as_bytes(), layout_engine) + # Usage note: making two fonts from the same buffer fails. + # shared_bytes = _font_as_bytes() + # _render(shared_bytes) + # with pytest.raises(Exception): + # _render(shared_bytes) + + +def test_font_with_open_file(layout_engine: ImageFont.Layout) -> None: + with open(FONT_PATH, "rb") as f: + _render(f, layout_engine) + + +def test_render_equal(layout_engine: ImageFont.Layout) -> None: + img_path = _render(FONT_PATH, layout_engine) + with open(FONT_PATH, "rb") as f: + font_filelike = BytesIO(f.read()) + img_filelike = _render(font_filelike, layout_engine) + + assert_image_equal(img_path, img_filelike) + + +def test_non_ascii_path(tmp_path: Path, layout_engine: ImageFont.Layout) -> None: + tempfile = str(tmp_path / ("temp_" + chr(128) + ".ttf")) + try: + shutil.copy(FONT_PATH, tempfile) + except UnicodeEncodeError: + pytest.skip("Non-ASCII path could not be created") + + ImageFont.truetype(tempfile, FONT_SIZE, layout_engine=layout_engine) + + +def test_transparent_background(font: ImageFont.FreeTypeFont) -> None: + im = Image.new(mode="RGBA", size=(300, 100)) + draw = ImageDraw.Draw(im) + + txt = "Hello World!" + draw.text((10, 10), txt, font=font) + + target = "Tests/images/transparent_background_text.png" + assert_image_similar_tofile(im, target, 4.09) + + target = "Tests/images/transparent_background_text_L.png" + assert_image_similar_tofile(im.convert("L"), target, 0.01) + + +def test_I16(font: ImageFont.FreeTypeFont) -> None: + im = Image.new(mode="I;16", size=(300, 100)) + draw = ImageDraw.Draw(im) + + txt = "Hello World!" + draw.text((10, 10), txt, fill=0xFFFE, font=font) + + assert im.getpixel((12, 14)) == 0xFFFE + + target = "Tests/images/transparent_background_text_L.png" + assert_image_similar_tofile(im.convert("L"), target, 0.01) + + +def test_textbbox_equal(font: ImageFont.FreeTypeFont) -> None: + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + + txt = "Hello World!" + bbox = draw.textbbox((10, 10), txt, font) + draw.text((10, 10), txt, font=font) + draw.rectangle(bbox) + + assert_image_similar_tofile(im, "Tests/images/rectangle_surrounding_text.png", 2.5) + + +@pytest.mark.parametrize( + "text, mode, fontname, size, length_basic, length_raqm", + ( + # basic test + ("text", "L", "FreeMono.ttf", 15, 36, 36), + ("text", "1", "FreeMono.ttf", 15, 36, 36), + # issue 4177 + ("rrr", "L", "DejaVuSans/DejaVuSans.ttf", 18, 21, 22.21875), + ("rrr", "1", "DejaVuSans/DejaVuSans.ttf", 18, 24, 22.21875), + # test 'l' not including extra margin + # using exact value 2047 / 64 for raqm, checked with debugger + ("ill", "L", "OpenSansCondensed-LightItalic.ttf", 63, 33, 31.984375), + ("ill", "1", "OpenSansCondensed-LightItalic.ttf", 63, 33, 31.984375), + ), +) +def test_getlength( + text: str, + mode: str, + fontname: str, + size: int, + layout_engine: ImageFont.Layout, + length_basic: int, + length_raqm: float, +) -> None: + f = ImageFont.truetype("Tests/fonts/" + fontname, size, layout_engine=layout_engine) + + im = Image.new(mode, (1, 1), 0) + d = ImageDraw.Draw(im) + + if layout_engine == ImageFont.Layout.BASIC: + length = d.textlength(text, f) + assert length == length_basic + else: + # disable kerning, kerning metrics changed + length = d.textlength(text, f, features=["-kern"]) + assert length == length_raqm + + +def test_float_size() -> None: + lengths = [] + for size in (48, 48.5, 49): + f = ImageFont.truetype( + "Tests/fonts/NotoSans-Regular.ttf", size, layout_engine=layout_engine + ) + lengths.append(f.getlength("text")) + assert lengths[0] != lengths[1] != lengths[2] + + +def test_render_multiline(font: ImageFont.FreeTypeFont) -> None: + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + line_spacing = font.getbbox("A")[3] + 4 + lines = TEST_TEXT.split("\n") + y = 0 + for line in lines: + draw.text((0, y), line, font=font) + y += line_spacing + + # some versions of freetype have different horizontal spacing. + # setting a tight epsilon, I'm showing the original test failure + # at epsilon = ~38. + assert_image_similar_tofile(im, "Tests/images/multiline_text.png", 6.2) + + +def test_render_multiline_text(font: ImageFont.FreeTypeFont) -> None: + # Test that text() correctly connects to multiline_text() + # and that align defaults to left + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), TEST_TEXT, font=font) + + assert_image_similar_tofile(im, "Tests/images/multiline_text.png", 0.01) + + # Test that text() can pass on additional arguments + # to multiline_text() + draw.text( + (0, 0), TEST_TEXT, fill=None, font=font, anchor=None, spacing=4, align="left" + ) + draw.text((0, 0), TEST_TEXT, None, font, None, 4, "left") + + +@pytest.mark.parametrize( + "align, ext", (("left", ""), ("center", "_center"), ("right", "_right")) +) +def test_render_multiline_text_align( + font: ImageFont.FreeTypeFont, align: str, ext: str +) -> None: + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.multiline_text((0, 0), TEST_TEXT, font=font, align=align) + + assert_image_similar_tofile(im, f"Tests/images/multiline_text{ext}.png", 0.01) + + +def test_unknown_align(font: ImageFont.FreeTypeFont) -> None: + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + + # Act/Assert + with pytest.raises(ValueError): + draw.multiline_text((0, 0), TEST_TEXT, font=font, align="unknown") + + +def test_draw_align(font: ImageFont.FreeTypeFont) -> None: + im = Image.new("RGB", (300, 100), "white") + draw = ImageDraw.Draw(im) + line = "some text" + draw.text((100, 40), line, (0, 0, 0), font=font, align="left") + + +def test_multiline_bbox(font: ImageFont.FreeTypeFont) -> None: + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + + # Test that textbbox() correctly connects to multiline_textbbox() + assert draw.textbbox((0, 0), TEST_TEXT, font=font) == draw.multiline_textbbox( + (0, 0), TEST_TEXT, font=font + ) + + # Test that multiline_textbbox corresponds to ImageFont.textbbox() + # for single line text + assert font.getbbox("A") == draw.multiline_textbbox((0, 0), "A", font=font) + + # Test that textbbox() can pass on additional arguments + # to multiline_textbbox() + draw.textbbox((0, 0), TEST_TEXT, font=font, spacing=4) + + +def test_multiline_width(font: ImageFont.FreeTypeFont) -> None: + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + + assert ( + draw.textbbox((0, 0), "longest line", font=font)[2] + == draw.multiline_textbbox((0, 0), "longest line\nline", font=font)[2] + ) + + +def test_multiline_spacing(font: ImageFont.FreeTypeFont) -> None: + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.multiline_text((0, 0), TEST_TEXT, font=font, spacing=10) + + assert_image_similar_tofile(im, "Tests/images/multiline_text_spacing.png", 2.5) + + +@pytest.mark.parametrize( + "orientation", (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270) +) +def test_rotated_transposed_font( + font: ImageFont.FreeTypeFont, orientation: Image.Transpose +) -> None: + img_gray = Image.new("L", (100, 100)) + draw = ImageDraw.Draw(img_gray) + word = "testing" + + transposed_font = ImageFont.TransposedFont(font, orientation=orientation) + + # Original font + draw.font = font + bbox_a = draw.textbbox((10, 10), word) + + # Rotated font + draw.font = transposed_font + bbox_b = draw.textbbox((20, 20), word) + + # Check (w, h) of box a is (h, w) of box b + assert ( + bbox_a[2] - bbox_a[0], + bbox_a[3] - bbox_a[1], + ) == ( + bbox_b[3] - bbox_b[1], + bbox_b[2] - bbox_b[0], + ) + + # Check top left co-ordinates are correct + assert bbox_b[:2] == (20, 20) + + # text length is undefined for vertical text + with pytest.raises(ValueError): + draw.textlength(word) + + +@pytest.mark.parametrize( + "orientation", + ( + None, + Image.Transpose.ROTATE_180, + Image.Transpose.FLIP_LEFT_RIGHT, + Image.Transpose.FLIP_TOP_BOTTOM, + ), +) +def test_unrotated_transposed_font( + font: ImageFont.FreeTypeFont, orientation: Image.Transpose +) -> None: + img_gray = Image.new("L", (100, 100)) + draw = ImageDraw.Draw(img_gray) + word = "testing" + + transposed_font = ImageFont.TransposedFont(font, orientation=orientation) + + # Original font + draw.font = font + bbox_a = draw.textbbox((10, 10), word) + length_a = draw.textlength(word) + + # Rotated font + draw.font = transposed_font + bbox_b = draw.textbbox((20, 20), word) + length_b = draw.textlength(word) + + # Check boxes a and b are same size + assert ( + bbox_a[2] - bbox_a[0], + bbox_a[3] - bbox_a[1], + ) == ( + bbox_b[2] - bbox_b[0], + bbox_b[3] - bbox_b[1], + ) + + # Check top left co-ordinates are correct + assert bbox_b[:2] == (20, 20) + + assert length_a == length_b + + +@pytest.mark.parametrize( + "orientation", (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270) +) +def test_rotated_transposed_font_get_mask( + font: ImageFont.FreeTypeFont, orientation: Image.Transpose +) -> None: + # Arrange + text = "mask this" + transposed_font = ImageFont.TransposedFont(font, orientation=orientation) + + # Act + mask = transposed_font.getmask(text) + + # Assert + assert mask.size == (13, 108) + + +@pytest.mark.parametrize( + "orientation", + ( + None, + Image.Transpose.ROTATE_180, + Image.Transpose.FLIP_LEFT_RIGHT, + Image.Transpose.FLIP_TOP_BOTTOM, + ), +) +def test_unrotated_transposed_font_get_mask( + font: ImageFont.FreeTypeFont, orientation: Image.Transpose +) -> None: + # Arrange + text = "mask this" + transposed_font = ImageFont.TransposedFont(font, orientation=orientation) + + # Act + mask = transposed_font.getmask(text) + + # Assert + assert mask.size == (108, 13) + + +def test_free_type_font_get_name(font: ImageFont.FreeTypeFont) -> None: + assert ("FreeMono", "Regular") == font.getname() + + +def test_free_type_font_get_metrics(font: ImageFont.FreeTypeFont) -> None: + ascent, descent = font.getmetrics() + + assert isinstance(ascent, int) + assert isinstance(descent, int) + assert (ascent, descent) == (16, 4) + + +def test_free_type_font_get_mask(font: ImageFont.FreeTypeFont) -> None: + # Arrange + text = "mask this" + + # Act + mask = font.getmask(text) + + # Assert + assert mask.size == (108, 13) + + +def test_load_path_not_found() -> None: + # Arrange + filename = "somefilenamethatdoesntexist.ttf" + + # Act/Assert + with pytest.raises(OSError): + ImageFont.load_path(filename) + with pytest.raises(OSError): + ImageFont.truetype(filename) + + +def test_load_non_font_bytes() -> None: + with open("Tests/images/hopper.jpg", "rb") as f: + with pytest.raises(OSError): + ImageFont.truetype(f) + + +def test_default_font() -> None: + # Arrange + txt = "This is a default font using FreeType support." + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + + # Act + default_font = ImageFont.load_default() + draw.text((10, 10), txt, font=default_font) + + larger_default_font = ImageFont.load_default(size=14) + draw.text((10, 60), txt, font=larger_default_font) + + # Assert + assert_image_equal_tofile(im, "Tests/images/default_font_freetype.png") + + +@pytest.mark.parametrize("mode", (None, "1", "RGBA")) +def test_getbbox(font: ImageFont.FreeTypeFont, mode: str | None) -> None: + assert (0, 4, 12, 16) == font.getbbox("A", mode) + + +def test_getbbox_empty(font: ImageFont.FreeTypeFont) -> None: + # issue #2614, should not crash. + assert (0, 0, 0, 0) == font.getbbox("") + + +def test_render_empty(font: ImageFont.FreeTypeFont) -> None: + # issue 2666 + im = Image.new(mode="RGB", size=(300, 100)) + target = im.copy() + draw = ImageDraw.Draw(im) + # should not crash here. + draw.text((10, 10), "", font=font) + assert_image_equal(im, target) + + +def test_unicode_extended(layout_engine: ImageFont.Layout) -> None: + # issue #3777 + text = "A\u278A\U0001F12B" + target = "Tests/images/unicode_extended.png" + + ttf = ImageFont.truetype( + "Tests/fonts/NotoSansSymbols-Regular.ttf", + FONT_SIZE, + layout_engine=layout_engine, + ) + img = Image.new("RGB", (100, 60)) + d = ImageDraw.Draw(img) + d.text((10, 10), text, font=ttf) + + # fails with 14.7 + assert_image_similar_tofile(img, target, 6.2) + + +@pytest.mark.parametrize( + "platform, font_directory", + (("linux", "/usr/local/share/fonts"), ("darwin", "/System/Library/Fonts")), +) +@pytest.mark.skipif(is_win32(), reason="requires Unix or macOS") +def test_find_font( + monkeypatch: pytest.MonkeyPatch, platform: str, font_directory: str +) -> None: + def _test_fake_loading_font(path_to_fake: str, fontname: str) -> None: + # Make a copy of FreeTypeFont so we can patch the original + free_type_font = copy.deepcopy(ImageFont.FreeTypeFont) + with monkeypatch.context() as m: + m.setattr(ImageFont, "_FreeTypeFont", free_type_font, raising=False) + + def loadable_font( + filepath: str, size: int, index: int, encoding: str, *args: Any + ): + _freeTypeFont = getattr(ImageFont, "_FreeTypeFont") + if filepath == path_to_fake: + return _freeTypeFont(FONT_PATH, size, index, encoding, *args) + return _freeTypeFont(filepath, size, index, encoding, *args) + + m.setattr(ImageFont, "FreeTypeFont", loadable_font) + font = ImageFont.truetype(fontname) + # Make sure it's loaded + name = font.getname() + assert ("FreeMono", "Regular") == name + + # A lot of mocking here - this is more for hitting code and + # catching syntax like errors + monkeypatch.setattr(sys, "platform", platform) + if platform == "linux": + monkeypatch.setenv("XDG_DATA_DIRS", "/usr/share/:/usr/local/share/") + + def fake_walker(path: str) -> list[tuple[str, list[str], list[str]]]: + if path == font_directory: + return [ + ( + path, + [], + ["Arial.ttf", "Single.otf", "Duplicate.otf", "Duplicate.ttf"], + ) + ] + return [(path, [], ["some_random_font.ttf"])] + + monkeypatch.setattr(os, "walk", fake_walker) + + # Test that the font loads both with and without the extension + _test_fake_loading_font(font_directory + "/Arial.ttf", "Arial.ttf") + _test_fake_loading_font(font_directory + "/Arial.ttf", "Arial") + + # Test that non-ttf fonts can be found without the extension + _test_fake_loading_font(font_directory + "/Single.otf", "Single") + + # Test that ttf fonts are preferred if the extension is not specified + _test_fake_loading_font(font_directory + "/Duplicate.ttf", "Duplicate") + + +def test_imagefont_getters(font: ImageFont.FreeTypeFont) -> None: + assert font.getmetrics() == (16, 4) + assert font.font.ascent == 16 + assert font.font.descent == 4 + assert font.font.height == 20 + assert font.font.x_ppem == 20 + assert font.font.y_ppem == 20 + assert font.font.glyphs == 4177 + assert font.getbbox("A") == (0, 4, 12, 16) + assert font.getbbox("AB") == (0, 4, 24, 16) + assert font.getbbox("M") == (0, 4, 12, 16) + assert font.getbbox("y") == (0, 7, 12, 20) + assert font.getbbox("a") == (0, 7, 12, 16) + assert font.getlength("A") == 12 + assert font.getlength("AB") == 24 + assert font.getlength("M") == 12 + assert font.getlength("y") == 12 + assert font.getlength("a") == 12 + + +@pytest.mark.parametrize("stroke_width", (0, 2)) +def test_getsize_stroke(font: ImageFont.FreeTypeFont, stroke_width: int) -> None: + assert font.getbbox("A", stroke_width=stroke_width) == ( + 0 - stroke_width, + 4 - stroke_width, + 12 + stroke_width, + 16 + stroke_width, + ) + + +def test_complex_font_settings() -> None: + t = ImageFont.truetype(FONT_PATH, FONT_SIZE, layout_engine=ImageFont.Layout.BASIC) + with pytest.raises(KeyError): + t.getmask("абвг", direction="rtl") + with pytest.raises(KeyError): + t.getmask("абвг", features=["-kern"]) + with pytest.raises(KeyError): + t.getmask("абвг", language="sr") + + +def test_variation_get(font: ImageFont.FreeTypeFont) -> None: + version = features.version_module("freetype2") + assert version is not None + freetype = parse_version(version) + if freetype < parse_version("2.9.1"): + with pytest.raises(NotImplementedError): + font.get_variation_names() + with pytest.raises(NotImplementedError): + font.get_variation_axes() + return + + with pytest.raises(OSError): + font.get_variation_names() + with pytest.raises(OSError): + font.get_variation_axes() + + font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf") + assert font.get_variation_names(), [ + b"ExtraLight", + b"Light", + b"Regular", + b"Semibold", + b"Bold", + b"Black", + b"Black Medium Contrast", + b"Black High Contrast", + b"Default", + ] + assert font.get_variation_axes() == [ + {"name": b"Weight", "minimum": 200, "maximum": 900, "default": 389}, + {"name": b"Contrast", "minimum": 0, "maximum": 100, "default": 0}, + ] + + font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf") + assert font.get_variation_names() == [ + b"20", + b"40", + b"60", + b"80", + b"100", + b"120", + b"140", + b"160", + b"180", + b"200", + b"220", + b"240", + b"260", + b"280", + b"300", + b"Regular", + ] + assert font.get_variation_axes() == [ + {"name": b"Size", "minimum": 0, "maximum": 300, "default": 0} + ] + + +def _check_text(font: ImageFont.FreeTypeFont, path: str, epsilon: float) -> None: + im = Image.new("RGB", (100, 75), "white") + d = ImageDraw.Draw(im) + d.text((10, 10), "Text", font=font, fill="black") + + try: + assert_image_similar_tofile(im, path, epsilon) + except AssertionError: + if "_adobe" in path: + path = path.replace("_adobe", "_adobe_older_harfbuzz") + assert_image_similar_tofile(im, path, epsilon) + else: + raise + + +def test_variation_set_by_name(font: ImageFont.FreeTypeFont) -> None: + version = features.version_module("freetype2") + assert version is not None + freetype = parse_version(version) + if freetype < parse_version("2.9.1"): + with pytest.raises(NotImplementedError): + font.set_variation_by_name("Bold") + return + + with pytest.raises(OSError): + font.set_variation_by_name("Bold") + + font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf", 36) + _check_text(font, "Tests/images/variation_adobe.png", 11) + for name in ["Bold", b"Bold"]: + font.set_variation_by_name(name) + assert font.getname()[1] == "Bold" + _check_text(font, "Tests/images/variation_adobe_name.png", 16) + + font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf", 36) + _check_text(font, "Tests/images/variation_tiny.png", 40) + for name in ["200", b"200"]: + font.set_variation_by_name(name) + assert font.getname()[1] == "200" + _check_text(font, "Tests/images/variation_tiny_name.png", 40) + + +def test_variation_set_by_axes(font: ImageFont.FreeTypeFont) -> None: + version = features.version_module("freetype2") + assert version is not None + freetype = parse_version(version) + if freetype < parse_version("2.9.1"): + with pytest.raises(NotImplementedError): + font.set_variation_by_axes([100]) + return + + with pytest.raises(OSError): + font.set_variation_by_axes([500, 50]) + + font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf", 36) + font.set_variation_by_axes([500, 50]) + _check_text(font, "Tests/images/variation_adobe_axes.png", 11.05) + + font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf", 36) + font.set_variation_by_axes([100]) + _check_text(font, "Tests/images/variation_tiny_axes.png", 32.5) + + +@pytest.mark.parametrize( + "anchor, left, top", + ( + # test horizontal anchors + ("ls", 0, -36), + ("ms", -64, -36), + ("rs", -128, -36), + # test vertical anchors + ("ma", -64, 16), + ("mt", -64, 0), + ("mm", -64, -17), + ("mb", -64, -44), + ("md", -64, -51), + ), + ids=("ls", "ms", "rs", "ma", "mt", "mm", "mb", "md"), +) +def test_anchor( + layout_engine: ImageFont.Layout, anchor: str, left: int, top: int +) -> None: + name, text = "quick", "Quick" + path = f"Tests/images/test_anchor_{name}_{anchor}.png" + + if layout_engine == ImageFont.Layout.RAQM: + width, height = (129, 44) + else: + width, height = (128, 44) + + bbox_expected = (left, top, left + width, top + height) + + f = ImageFont.truetype( + "Tests/fonts/NotoSans-Regular.ttf", 48, layout_engine=layout_engine + ) + + im = Image.new("RGB", (200, 200), "white") + d = ImageDraw.Draw(im) + d.line(((0, 100), (200, 100)), "gray") + d.line(((100, 0), (100, 200)), "gray") + d.text((100, 100), text, fill="black", anchor=anchor, font=f) + + assert d.textbbox((0, 0), text, f, anchor=anchor) == bbox_expected + + assert_image_similar_tofile(im, path, 7) + + +@pytest.mark.parametrize( + "anchor, align", + ( + # test horizontal anchors + ("lm", "left"), + ("lm", "center"), + ("lm", "right"), + ("mm", "left"), + ("mm", "center"), + ("mm", "right"), + ("rm", "left"), + ("rm", "center"), + ("rm", "right"), + # test vertical anchors + ("ma", "center"), + # ("mm", "center"), # duplicate + ("md", "center"), + ), +) +def test_anchor_multiline( + layout_engine: ImageFont.Layout, anchor: str, align: str +) -> None: + target = f"Tests/images/test_anchor_multiline_{anchor}_{align}.png" + text = "a\nlong\ntext sample" + + f = ImageFont.truetype( + "Tests/fonts/NotoSans-Regular.ttf", 48, layout_engine=layout_engine + ) + + # test render + im = Image.new("RGB", (600, 400), "white") + d = ImageDraw.Draw(im) + d.line(((0, 200), (600, 200)), "gray") + d.line(((300, 0), (300, 400)), "gray") + d.multiline_text((300, 200), text, fill="black", anchor=anchor, font=f, align=align) + + assert_image_similar_tofile(im, target, 4) + + +def test_anchor_invalid(font: ImageFont.FreeTypeFont) -> None: + im = Image.new("RGB", (100, 100), "white") + d = ImageDraw.Draw(im) + d.font = font + + for anchor in ["", "l", "a", "lax", "sa", "xa", "lx"]: + with pytest.raises(ValueError): + font.getmask2("hello", anchor=anchor) + with pytest.raises(ValueError): + font.getbbox("hello", anchor=anchor) + with pytest.raises(ValueError): + d.text((0, 0), "hello", anchor=anchor) + with pytest.raises(ValueError): + d.textbbox((0, 0), "hello", anchor=anchor) + with pytest.raises(ValueError): + d.multiline_text((0, 0), "foo\nbar", anchor=anchor) + with pytest.raises(ValueError): + d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor) + for anchor in ["lt", "lb"]: + with pytest.raises(ValueError): + d.multiline_text((0, 0), "foo\nbar", anchor=anchor) + with pytest.raises(ValueError): + d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor) + + +@pytest.mark.parametrize("bpp", (1, 2, 4, 8)) +def test_bitmap_font(layout_engine: ImageFont.Layout, bpp: int) -> None: + text = "Bitmap Font" + layout_name = ["basic", "raqm"][layout_engine] + target = f"Tests/images/bitmap_font_{bpp}_{layout_name}.png" + font = ImageFont.truetype( + f"Tests/fonts/DejaVuSans/DejaVuSans-24-{bpp}-stripped.ttf", + 24, + layout_engine=layout_engine, + ) + + im = Image.new("RGB", (160, 35), "white") + draw = ImageDraw.Draw(im) + draw.text((2, 2), text, "black", font) + + assert_image_equal_tofile(im, target) + + +def test_bitmap_font_stroke(layout_engine: ImageFont.Layout) -> None: + text = "Bitmap Font" + layout_name = ["basic", "raqm"][layout_engine] + target = f"Tests/images/bitmap_font_stroke_{layout_name}.png" + font = ImageFont.truetype( + "Tests/fonts/DejaVuSans/DejaVuSans-24-8-stripped.ttf", + 24, + layout_engine=layout_engine, + ) + + im = Image.new("RGB", (160, 35), "white") + draw = ImageDraw.Draw(im) + draw.text((2, 2), text, "black", font, stroke_width=2, stroke_fill="red") + + assert_image_similar_tofile(im, target, 0.03) + + +@pytest.mark.parametrize("embedded_color", (False, True)) +def test_bitmap_blend(layout_engine: ImageFont.Layout, embedded_color: bool) -> None: + font = ImageFont.truetype( + "Tests/fonts/EBDTTestFont.ttf", size=64, layout_engine=layout_engine + ) + + im = Image.new("RGBA", (128, 96), "white") + d = ImageDraw.Draw(im) + d.text((16, 16), "AA", font=font, fill="#8E2F52", embedded_color=embedded_color) + + assert_image_equal_tofile(im, "Tests/images/bitmap_font_blend.png") + + +def test_standard_embedded_color(layout_engine: ImageFont.Layout) -> None: + txt = "Hello World!" + ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine) + ttf.getbbox(txt) + + im = Image.new("RGB", (300, 64), "white") + d = ImageDraw.Draw(im) + d.text((10, 10), txt, font=ttf, fill="#fa6", embedded_color=True) + + assert_image_similar_tofile(im, "Tests/images/standard_embedded.png", 3.1) + + +@pytest.mark.parametrize("fontmode", ("1", "L", "RGBA")) +def test_float_coord(layout_engine: ImageFont.Layout, fontmode: str) -> None: + txt = "Hello World!" + ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine) + + im = Image.new("RGB", (300, 64), "white") + d = ImageDraw.Draw(im) + if fontmode == "1": + d.fontmode = "1" + + embedded_color = fontmode == "RGBA" + d.text((9.5, 9.5), txt, font=ttf, fill="#fa6", embedded_color=embedded_color) + try: + assert_image_similar_tofile(im, "Tests/images/text_float_coord.png", 3.9) + except AssertionError: + if fontmode == "1" and layout_engine == ImageFont.Layout.BASIC: + assert_image_similar_tofile( + im, "Tests/images/text_float_coord_1_alt.png", 1 + ) + else: + raise + + +def test_cbdt(layout_engine: ImageFont.Layout) -> None: + try: + font = ImageFont.truetype( + "Tests/fonts/CBDTTestFont.ttf", size=64, layout_engine=layout_engine + ) + + im = Image.new("RGB", (128, 96), "white") + d = ImageDraw.Draw(im) + + d.text((16, 16), "AB", font=font, embedded_color=True) + + assert_image_equal_tofile(im, "Tests/images/cbdt.png") + except OSError as e: # pragma: no cover + assert str(e) in ("unimplemented feature", "unknown file format") + pytest.skip("freetype compiled without libpng or CBDT support") + + +def test_cbdt_mask(layout_engine: ImageFont.Layout) -> None: + try: + font = ImageFont.truetype( + "Tests/fonts/CBDTTestFont.ttf", size=64, layout_engine=layout_engine + ) + + im = Image.new("RGB", (128, 96), "white") + d = ImageDraw.Draw(im) + + d.text((16, 16), "AB", "green", font=font) + + assert_image_equal_tofile(im, "Tests/images/cbdt_mask.png") + except OSError as e: # pragma: no cover + assert str(e) in ("unimplemented feature", "unknown file format") + pytest.skip("freetype compiled without libpng or CBDT support") + + +def test_sbix(layout_engine: ImageFont.Layout) -> None: + try: + font = ImageFont.truetype( + "Tests/fonts/chromacheck-sbix.woff", size=300, layout_engine=layout_engine + ) + + im = Image.new("RGB", (400, 400), "white") + d = ImageDraw.Draw(im) + + d.text((50, 50), "\uE901", font=font, embedded_color=True) + + assert_image_similar_tofile(im, "Tests/images/chromacheck-sbix.png", 1) + except OSError as e: # pragma: no cover + assert str(e) in ("unimplemented feature", "unknown file format") + pytest.skip("freetype compiled without libpng or SBIX support") + + +def test_sbix_mask(layout_engine: ImageFont.Layout) -> None: + try: + font = ImageFont.truetype( + "Tests/fonts/chromacheck-sbix.woff", size=300, layout_engine=layout_engine + ) + + im = Image.new("RGB", (400, 400), "white") + d = ImageDraw.Draw(im) + + d.text((50, 50), "\uE901", (100, 0, 0), font=font) + + assert_image_similar_tofile(im, "Tests/images/chromacheck-sbix_mask.png", 1) + except OSError as e: # pragma: no cover + assert str(e) in ("unimplemented feature", "unknown file format") + pytest.skip("freetype compiled without libpng or SBIX support") + + +@skip_unless_feature_version("freetype2", "2.10.0") +def test_colr(layout_engine: ImageFont.Layout) -> None: + font = ImageFont.truetype( + "Tests/fonts/BungeeColor-Regular_colr_Windows.ttf", + size=64, + layout_engine=layout_engine, + ) + + im = Image.new("RGB", (300, 75), "white") + d = ImageDraw.Draw(im) + + d.text((15, 5), "Bungee", font=font, embedded_color=True) + + assert_image_similar_tofile(im, "Tests/images/colr_bungee.png", 21) + + +@skip_unless_feature_version("freetype2", "2.10.0") +def test_colr_mask(layout_engine: ImageFont.Layout) -> None: + font = ImageFont.truetype( + "Tests/fonts/BungeeColor-Regular_colr_Windows.ttf", + size=64, + layout_engine=layout_engine, + ) + + im = Image.new("RGB", (300, 75), "white") + d = ImageDraw.Draw(im) + + d.text((15, 5), "Bungee", "black", font=font) + + assert_image_similar_tofile(im, "Tests/images/colr_bungee_mask.png", 22) + + +def test_woff2(layout_engine: ImageFont.Layout) -> None: + try: + font = ImageFont.truetype( + "Tests/fonts/OpenSans.woff2", + size=64, + layout_engine=layout_engine, + ) + except OSError as e: + assert str(e) in ("unimplemented feature", "unknown file format") + pytest.skip("FreeType compiled without brotli or WOFF2 support") + + im = Image.new("RGB", (350, 100), "white") + d = ImageDraw.Draw(im) + + d.text((15, 5), "OpenSans", "black", font=font) + + assert_image_similar_tofile(im, "Tests/images/test_woff2.png", 5) + + +def test_render_mono_size() -> None: + # issue 4177 + + im = Image.new("P", (100, 30), "white") + draw = ImageDraw.Draw(im) + ttf = ImageFont.truetype( + "Tests/fonts/DejaVuSans/DejaVuSans.ttf", + 18, + layout_engine=ImageFont.Layout.BASIC, + ) + + draw.text((10, 10), "r" * 10, "black", ttf) + assert_image_equal_tofile(im, "Tests/images/text_mono.gif") + + +def test_too_many_characters(font: ImageFont.FreeTypeFont) -> None: + with pytest.raises(ValueError): + font.getlength("A" * 1_000_001) + with pytest.raises(ValueError): + font.getbbox("A" * 1_000_001) + with pytest.raises(ValueError): + font.getmask2("A" * 1_000_001) + + transposed_font = ImageFont.TransposedFont(font) + with pytest.raises(ValueError): + transposed_font.getlength("A" * 1_000_001) + + imagefont = ImageFont.ImageFont() + with pytest.raises(ValueError): + imagefont.getlength("A" * 1_000_001) + with pytest.raises(ValueError): + imagefont.getbbox("A" * 1_000_001) + with pytest.raises(ValueError): + imagefont.getmask("A" * 1_000_001) + + +@pytest.mark.parametrize( + "test_file", + [ + "Tests/fonts/oom-e8e927ba6c0d38274a37c1567560eb33baf74627.ttf", + "Tests/fonts/oom-4da0210eb7081b0bf15bf16cc4c52ce02c1e1bbc.ttf", + ], +) +def test_oom(test_file: str) -> None: + with open(test_file, "rb") as f: + font = ImageFont.truetype(BytesIO(f.read())) + with pytest.raises(Image.DecompressionBombError): + font.getmask("Test Text") + + +def test_raqm_missing_warning(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(ImageFont.core, "HAVE_RAQM", False) + with pytest.warns(UserWarning) as record: + font = ImageFont.truetype( + FONT_PATH, FONT_SIZE, layout_engine=ImageFont.Layout.RAQM + ) + assert font.layout_engine == ImageFont.Layout.BASIC + assert str(record[-1].message) == ( + "Raqm layout was requested, but Raqm is not available. " + "Falling back to basic layout." + ) + + +@pytest.mark.parametrize("size", [-1, 0]) +def test_invalid_truetype_sizes_raise_valueerror( + layout_engine: ImageFont.Layout, size: int +) -> None: + with pytest.raises(ValueError): + ImageFont.truetype(FONT_PATH, size, layout_engine=layout_engine) diff --git a/Tests/test_imagefontctl.py b/Tests/test_imagefontctl.py index 24c7b871a..75d035842 100644 --- a/Tests/test_imagefontctl.py +++ b/Tests/test_imagefontctl.py @@ -1,385 +1,385 @@ -from __future__ import annotations - -import pytest - -from PIL import Image, ImageDraw, ImageFont - -from .helper import assert_image_similar_tofile, skip_unless_feature - -FONT_SIZE = 20 -FONT_PATH = "Tests/fonts/DejaVuSans/DejaVuSans.ttf" - -pytestmark = skip_unless_feature("raqm") - - -def test_english() -> None: - # smoke test, this should not fail - ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.text((0, 0), "TEST", font=ttf, fill=500, direction="ltr") - - -def test_complex_text() -> None: - ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.text((0, 0), "اهلا عمان", font=ttf, fill=500) - - target = "Tests/images/test_text.png" - assert_image_similar_tofile(im, target, 0.5) - - -def test_y_offset() -> None: - ttf = ImageFont.truetype("Tests/fonts/NotoNastaliqUrdu-Regular.ttf", FONT_SIZE) - - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.text((0, 0), "العالم العربي", font=ttf, fill=500) - - target = "Tests/images/test_y_offset.png" - assert_image_similar_tofile(im, target, 1.7) - - -def test_complex_unicode_text() -> None: - ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.text((0, 0), "السلام عليكم", font=ttf, fill=500) - - target = "Tests/images/test_complex_unicode_text.png" - assert_image_similar_tofile(im, target, 0.5) - - ttf = ImageFont.truetype("Tests/fonts/KhmerOSBattambang-Regular.ttf", FONT_SIZE) - - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.text((0, 0), "លោកុប្បážáŸ’ážáž·", font=ttf, fill=500) - - target = "Tests/images/test_complex_unicode_text2.png" - assert_image_similar_tofile(im, target, 2.33) - - -def test_text_direction_rtl() -> None: - ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.text((0, 0), "English عربي", font=ttf, fill=500, direction="rtl") - - target = "Tests/images/test_direction_rtl.png" - assert_image_similar_tofile(im, target, 0.5) - - -def test_text_direction_ltr() -> None: - ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.text((0, 0), "سلطنة عمان Oman", font=ttf, fill=500, direction="ltr") - - target = "Tests/images/test_direction_ltr.png" - assert_image_similar_tofile(im, target, 0.5) - - -def test_text_direction_rtl2() -> None: - ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.text((0, 0), "Oman سلطنة عمان", font=ttf, fill=500, direction="rtl") - - target = "Tests/images/test_direction_ltr.png" - assert_image_similar_tofile(im, target, 0.5) - - -def test_text_direction_ttb() -> None: - ttf = ImageFont.truetype("Tests/fonts/NotoSansJP-Regular.otf", FONT_SIZE) - - im = Image.new(mode="RGB", size=(100, 300)) - draw = ImageDraw.Draw(im) - try: - draw.text((0, 0), "English ã‚ã„", font=ttf, fill=500, direction="ttb") - except ValueError as ex: - if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction": - pytest.skip("libraqm 0.7 or greater not available") - - target = "Tests/images/test_direction_ttb.png" - assert_image_similar_tofile(im, target, 2.8) - - -def test_text_direction_ttb_stroke() -> None: - ttf = ImageFont.truetype("Tests/fonts/NotoSansJP-Regular.otf", 50) - - im = Image.new(mode="RGB", size=(100, 300)) - draw = ImageDraw.Draw(im) - try: - draw.text( - (27, 27), - "ã‚ã„", - font=ttf, - fill=500, - direction="ttb", - stroke_width=2, - stroke_fill="#0f0", - ) - except ValueError as ex: - if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction": - pytest.skip("libraqm 0.7 or greater not available") - - target = "Tests/images/test_direction_ttb_stroke.png" - assert_image_similar_tofile(im, target, 19.4) - - -def test_ligature_features() -> None: - ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.text((0, 0), "filling", font=ttf, fill=500, features=["-liga"]) - target = "Tests/images/test_ligature_features.png" - assert_image_similar_tofile(im, target, 0.5) - - liga_bbox = ttf.getbbox("fi", features=["-liga"]) - assert liga_bbox == (0, 4, 13, 19) - - -def test_kerning_features() -> None: - ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.text((0, 0), "TeToAV", font=ttf, fill=500, features=["-kern"]) - - target = "Tests/images/test_kerning_features.png" - assert_image_similar_tofile(im, target, 0.5) - - -def test_arabictext_features() -> None: - ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.text( - (0, 0), - "اللغة العربية", - font=ttf, - fill=500, - features=["-fina", "-init", "-medi"], - ) - - target = "Tests/images/test_arabictext_features.png" - assert_image_similar_tofile(im, target, 0.5) - - -def test_x_max_and_y_offset() -> None: - ttf = ImageFont.truetype("Tests/fonts/ArefRuqaa-Regular.ttf", 40) - - im = Image.new(mode="RGB", size=(50, 100)) - draw = ImageDraw.Draw(im) - draw.text((0, 0), "لح", font=ttf, fill=500) - - target = "Tests/images/test_x_max_and_y_offset.png" - assert_image_similar_tofile(im, target, 0.5) - - -def test_language() -> None: - ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.text((0, 0), "абвг", font=ttf, fill=500, language="sr") - - target = "Tests/images/test_language.png" - assert_image_similar_tofile(im, target, 0.5) - - -@pytest.mark.parametrize("mode", ("L", "1")) -@pytest.mark.parametrize( - "text, direction, expected", - ( - ("سلطنة عمان Oman", None, 173.703125), - ("سلطنة عمان Oman", "ltr", 173.703125), - ("Oman سلطنة عمان", "rtl", 173.703125), - ("English عربي", "rtl", 123.796875), - ("test", "ttb", 80.0), - ), - ids=("None", "ltr", "rtl2", "rtl", "ttb"), -) -def test_getlength( - mode: str, text: str, direction: str | None, expected: float -) -> None: - ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - im = Image.new(mode, (1, 1), 0) - d = ImageDraw.Draw(im) - - try: - assert d.textlength(text, ttf, direction) == expected - except ValueError as ex: - if ( - direction == "ttb" - and str(ex) == "libraqm 0.7 or greater required for 'ttb' direction" - ): - pytest.skip("libraqm 0.7 or greater not available") - - -@pytest.mark.parametrize("mode", ("L", "1")) -@pytest.mark.parametrize("direction", ("ltr", "ttb")) -@pytest.mark.parametrize( - "text", - ("i" + ("\u030C" * 15) + "i", "i" + "\u032C" * 15 + "i", "\u035Cii", "i\u0305i"), - ids=("caron-above", "caron-below", "double-breve", "overline"), -) -def test_getlength_combine(mode: str, direction: str, text: str) -> None: - if text == "i\u0305i" and direction == "ttb": - pytest.skip("fails with this font") - - ttf = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48) - - try: - target = ttf.getlength("ii", mode, direction) - actual = ttf.getlength(text, mode, direction) - - assert actual == target - except ValueError as ex: - if ( - direction == "ttb" - and str(ex) == "libraqm 0.7 or greater required for 'ttb' direction" - ): - pytest.skip("libraqm 0.7 or greater not available") - - -@pytest.mark.parametrize("anchor", ("lt", "mm", "rb", "sm")) -def test_anchor_ttb(anchor: str) -> None: - text = "f" - path = f"Tests/images/test_anchor_ttb_{text}_{anchor}.png" - f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 120) - - im = Image.new("RGB", (200, 400), "white") - d = ImageDraw.Draw(im) - d.line(((0, 200), (200, 200)), "gray") - d.line(((100, 0), (100, 400)), "gray") - try: - d.text((100, 200), text, fill="black", anchor=anchor, direction="ttb", font=f) - except ValueError as ex: - if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction": - pytest.skip("libraqm 0.7 or greater not available") - - assert_image_similar_tofile(im, path, 1) # fails at 5 - - -combine_tests = ( - # extends above (e.g. issue #4553) - ("caron", "a\u030C\u030C\u030C\u030C\u030Cb", None, None, 0.08), - ("caron_la", "a\u030C\u030C\u030C\u030C\u030Cb", "la", None, 0.08), - ("caron_lt", "a\u030C\u030C\u030C\u030C\u030Cb", "lt", None, 0.08), - ("caron_ls", "a\u030C\u030C\u030C\u030C\u030Cb", "ls", None, 0.08), - ("caron_ttb", "ca" + ("\u030C" * 15) + "b", None, "ttb", 0.3), - ("caron_ttb_lt", "ca" + ("\u030C" * 15) + "b", "lt", "ttb", 0.3), - # extends below - ("caron_below", "a\u032C\u032C\u032C\u032C\u032Cb", None, None, 0.02), - ("caron_below_ld", "a\u032C\u032C\u032C\u032C\u032Cb", "ld", None, 0.02), - ("caron_below_lb", "a\u032C\u032C\u032C\u032C\u032Cb", "lb", None, 0.02), - ("caron_below_ls", "a\u032C\u032C\u032C\u032C\u032Cb", "ls", None, 0.02), - ("caron_below_ttb", "a" + ("\u032C" * 15) + "b", None, "ttb", 0.03), - ("caron_below_ttb_lb", "a" + ("\u032C" * 15) + "b", "lb", "ttb", 0.03), - # extends to the right (e.g. issue #3745) - ("double_breve_below", "a\u035Ci", None, None, 0.02), - ("double_breve_below_ma", "a\u035Ci", "ma", None, 0.02), - ("double_breve_below_ra", "a\u035Ci", "ra", None, 0.02), - ("double_breve_below_ttb", "a\u035Cb", None, "ttb", 0.02), - ("double_breve_below_ttb_rt", "a\u035Cb", "rt", "ttb", 0.02), - ("double_breve_below_ttb_mt", "a\u035Cb", "mt", "ttb", 0.02), - ("double_breve_below_ttb_st", "a\u035Cb", "st", "ttb", 0.02), - # extends to the left (fail=0.064) - ("overline", "i\u0305", None, None, 0.02), - ("overline_la", "i\u0305", "la", None, 0.02), - ("overline_ra", "i\u0305", "ra", None, 0.02), - ("overline_ttb", "i\u0305", None, "ttb", 0.02), - ("overline_ttb_rt", "i\u0305", "rt", "ttb", 0.02), - ("overline_ttb_mt", "i\u0305", "mt", "ttb", 0.02), - ("overline_ttb_st", "i\u0305", "st", "ttb", 0.02), -) - - -# this tests various combining characters for anchor alignment and clipping -@pytest.mark.parametrize( - "name, text, anchor, dir, epsilon", combine_tests, ids=[r[0] for r in combine_tests] -) -def test_combine( - name: str, text: str, dir: str | None, anchor: str | None, epsilon: float -) -> None: - path = f"Tests/images/test_combine_{name}.png" - f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48) - - im = Image.new("RGB", (400, 400), "white") - d = ImageDraw.Draw(im) - d.line(((0, 200), (400, 200)), "gray") - d.line(((200, 0), (200, 400)), "gray") - try: - d.text((200, 200), text, fill="black", anchor=anchor, direction=dir, font=f) - except ValueError as ex: - if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction": - pytest.skip("libraqm 0.7 or greater not available") - - assert_image_similar_tofile(im, path, epsilon) - - -@pytest.mark.parametrize( - "anchor, align", - ( - ("lm", "left"), # pass with getsize - ("lm", "center"), # fail at 2.12 - ("lm", "right"), # fail at 2.57 - ("mm", "left"), # fail at 2.12 - ("mm", "center"), # pass with getsize - ("mm", "right"), # fail at 2.12 - ("rm", "left"), # fail at 2.57 - ("rm", "center"), # fail at 2.12 - ("rm", "right"), # pass with getsize - ), -) -def test_combine_multiline(anchor: str, align: str) -> None: - # test that multiline text uses getlength, not getsize or getbbox - - path = f"Tests/images/test_combine_multiline_{anchor}_{align}.png" - f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48) - text = "i\u0305\u035C\ntext" # i with overline and double breve, and a word - - im = Image.new("RGB", (400, 400), "white") - d = ImageDraw.Draw(im) - d.line(((0, 200), (400, 200)), "gray") - d.line(((200, 0), (200, 400)), "gray") - bbox = d.multiline_textbbox((200, 200), text, anchor=anchor, font=f, align=align) - d.rectangle(bbox, outline="red") - d.multiline_text((200, 200), text, fill="black", anchor=anchor, font=f, align=align) - - assert_image_similar_tofile(im, path, 0.015) - - -def test_anchor_invalid_ttb() -> None: - font = ImageFont.truetype(FONT_PATH, FONT_SIZE) - im = Image.new("RGB", (100, 100), "white") - d = ImageDraw.Draw(im) - d.font = font - - for anchor in ["", "l", "a", "lax", "xa", "la", "ls", "ld", "lx"]: - with pytest.raises(ValueError): - font.getmask2("hello", anchor=anchor, direction="ttb") - with pytest.raises(ValueError): - font.getbbox("hello", anchor=anchor, direction="ttb") - with pytest.raises(ValueError): - d.text((0, 0), "hello", anchor=anchor, direction="ttb") - with pytest.raises(ValueError): - d.textbbox((0, 0), "hello", anchor=anchor, direction="ttb") - with pytest.raises(ValueError): - d.multiline_text((0, 0), "foo\nbar", anchor=anchor, direction="ttb") - with pytest.raises(ValueError): - d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor, direction="ttb") - # ttb multiline text does not support anchors at all - with pytest.raises(ValueError): - d.multiline_text((0, 0), "foo\nbar", anchor="mm", direction="ttb") - with pytest.raises(ValueError): - d.multiline_textbbox((0, 0), "foo\nbar", anchor="mm", direction="ttb") +from __future__ import annotations + +import pytest + +from PIL import Image, ImageDraw, ImageFont + +from .helper import assert_image_similar_tofile, skip_unless_feature + +FONT_SIZE = 20 +FONT_PATH = "Tests/fonts/DejaVuSans/DejaVuSans.ttf" + +pytestmark = skip_unless_feature("raqm") + + +def test_english() -> None: + # smoke test, this should not fail + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "TEST", font=ttf, fill=500, direction="ltr") + + +def test_complex_text() -> None: + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "اهلا عمان", font=ttf, fill=500) + + target = "Tests/images/test_text.png" + assert_image_similar_tofile(im, target, 0.5) + + +def test_y_offset() -> None: + ttf = ImageFont.truetype("Tests/fonts/NotoNastaliqUrdu-Regular.ttf", FONT_SIZE) + + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "العالم العربي", font=ttf, fill=500) + + target = "Tests/images/test_y_offset.png" + assert_image_similar_tofile(im, target, 1.7) + + +def test_complex_unicode_text() -> None: + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "السلام عليكم", font=ttf, fill=500) + + target = "Tests/images/test_complex_unicode_text.png" + assert_image_similar_tofile(im, target, 0.5) + + ttf = ImageFont.truetype("Tests/fonts/KhmerOSBattambang-Regular.ttf", FONT_SIZE) + + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "លោកុប្បážáŸ’ážáž·", font=ttf, fill=500) + + target = "Tests/images/test_complex_unicode_text2.png" + assert_image_similar_tofile(im, target, 2.33) + + +def test_text_direction_rtl() -> None: + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "English عربي", font=ttf, fill=500, direction="rtl") + + target = "Tests/images/test_direction_rtl.png" + assert_image_similar_tofile(im, target, 0.5) + + +def test_text_direction_ltr() -> None: + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "سلطنة عمان Oman", font=ttf, fill=500, direction="ltr") + + target = "Tests/images/test_direction_ltr.png" + assert_image_similar_tofile(im, target, 0.5) + + +def test_text_direction_rtl2() -> None: + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "Oman سلطنة عمان", font=ttf, fill=500, direction="rtl") + + target = "Tests/images/test_direction_ltr.png" + assert_image_similar_tofile(im, target, 0.5) + + +def test_text_direction_ttb() -> None: + ttf = ImageFont.truetype("Tests/fonts/NotoSansJP-Regular.otf", FONT_SIZE) + + im = Image.new(mode="RGB", size=(100, 300)) + draw = ImageDraw.Draw(im) + try: + draw.text((0, 0), "English ã‚ã„", font=ttf, fill=500, direction="ttb") + except ValueError as ex: + if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction": + pytest.skip("libraqm 0.7 or greater not available") + + target = "Tests/images/test_direction_ttb.png" + assert_image_similar_tofile(im, target, 2.8) + + +def test_text_direction_ttb_stroke() -> None: + ttf = ImageFont.truetype("Tests/fonts/NotoSansJP-Regular.otf", 50) + + im = Image.new(mode="RGB", size=(100, 300)) + draw = ImageDraw.Draw(im) + try: + draw.text( + (27, 27), + "ã‚ã„", + font=ttf, + fill=500, + direction="ttb", + stroke_width=2, + stroke_fill="#0f0", + ) + except ValueError as ex: + if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction": + pytest.skip("libraqm 0.7 or greater not available") + + target = "Tests/images/test_direction_ttb_stroke.png" + assert_image_similar_tofile(im, target, 19.4) + + +def test_ligature_features() -> None: + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "filling", font=ttf, fill=500, features=["-liga"]) + target = "Tests/images/test_ligature_features.png" + assert_image_similar_tofile(im, target, 0.5) + + liga_bbox = ttf.getbbox("fi", features=["-liga"]) + assert liga_bbox == (0, 4, 13, 19) + + +def test_kerning_features() -> None: + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "TeToAV", font=ttf, fill=500, features=["-kern"]) + + target = "Tests/images/test_kerning_features.png" + assert_image_similar_tofile(im, target, 0.5) + + +def test_arabictext_features() -> None: + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text( + (0, 0), + "اللغة العربية", + font=ttf, + fill=500, + features=["-fina", "-init", "-medi"], + ) + + target = "Tests/images/test_arabictext_features.png" + assert_image_similar_tofile(im, target, 0.5) + + +def test_x_max_and_y_offset() -> None: + ttf = ImageFont.truetype("Tests/fonts/ArefRuqaa-Regular.ttf", 40) + + im = Image.new(mode="RGB", size=(50, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "لح", font=ttf, fill=500) + + target = "Tests/images/test_x_max_and_y_offset.png" + assert_image_similar_tofile(im, target, 0.5) + + +def test_language() -> None: + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "абвг", font=ttf, fill=500, language="sr") + + target = "Tests/images/test_language.png" + assert_image_similar_tofile(im, target, 0.5) + + +@pytest.mark.parametrize("mode", ("L", "1")) +@pytest.mark.parametrize( + "text, direction, expected", + ( + ("سلطنة عمان Oman", None, 173.703125), + ("سلطنة عمان Oman", "ltr", 173.703125), + ("Oman سلطنة عمان", "rtl", 173.703125), + ("English عربي", "rtl", 123.796875), + ("test", "ttb", 80.0), + ), + ids=("None", "ltr", "rtl2", "rtl", "ttb"), +) +def test_getlength( + mode: str, text: str, direction: str | None, expected: float +) -> None: + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + im = Image.new(mode, (1, 1), 0) + d = ImageDraw.Draw(im) + + try: + assert d.textlength(text, ttf, direction) == expected + except ValueError as ex: + if ( + direction == "ttb" + and str(ex) == "libraqm 0.7 or greater required for 'ttb' direction" + ): + pytest.skip("libraqm 0.7 or greater not available") + + +@pytest.mark.parametrize("mode", ("L", "1")) +@pytest.mark.parametrize("direction", ("ltr", "ttb")) +@pytest.mark.parametrize( + "text", + ("i" + ("\u030C" * 15) + "i", "i" + "\u032C" * 15 + "i", "\u035Cii", "i\u0305i"), + ids=("caron-above", "caron-below", "double-breve", "overline"), +) +def test_getlength_combine(mode: str, direction: str, text: str) -> None: + if text == "i\u0305i" and direction == "ttb": + pytest.skip("fails with this font") + + ttf = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48) + + try: + target = ttf.getlength("ii", mode, direction) + actual = ttf.getlength(text, mode, direction) + + assert actual == target + except ValueError as ex: + if ( + direction == "ttb" + and str(ex) == "libraqm 0.7 or greater required for 'ttb' direction" + ): + pytest.skip("libraqm 0.7 or greater not available") + + +@pytest.mark.parametrize("anchor", ("lt", "mm", "rb", "sm")) +def test_anchor_ttb(anchor: str) -> None: + text = "f" + path = f"Tests/images/test_anchor_ttb_{text}_{anchor}.png" + f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 120) + + im = Image.new("RGB", (200, 400), "white") + d = ImageDraw.Draw(im) + d.line(((0, 200), (200, 200)), "gray") + d.line(((100, 0), (100, 400)), "gray") + try: + d.text((100, 200), text, fill="black", anchor=anchor, direction="ttb", font=f) + except ValueError as ex: + if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction": + pytest.skip("libraqm 0.7 or greater not available") + + assert_image_similar_tofile(im, path, 1) # fails at 5 + + +combine_tests = ( + # extends above (e.g. issue #4553) + ("caron", "a\u030C\u030C\u030C\u030C\u030Cb", None, None, 0.08), + ("caron_la", "a\u030C\u030C\u030C\u030C\u030Cb", "la", None, 0.08), + ("caron_lt", "a\u030C\u030C\u030C\u030C\u030Cb", "lt", None, 0.08), + ("caron_ls", "a\u030C\u030C\u030C\u030C\u030Cb", "ls", None, 0.08), + ("caron_ttb", "ca" + ("\u030C" * 15) + "b", None, "ttb", 0.3), + ("caron_ttb_lt", "ca" + ("\u030C" * 15) + "b", "lt", "ttb", 0.3), + # extends below + ("caron_below", "a\u032C\u032C\u032C\u032C\u032Cb", None, None, 0.02), + ("caron_below_ld", "a\u032C\u032C\u032C\u032C\u032Cb", "ld", None, 0.02), + ("caron_below_lb", "a\u032C\u032C\u032C\u032C\u032Cb", "lb", None, 0.02), + ("caron_below_ls", "a\u032C\u032C\u032C\u032C\u032Cb", "ls", None, 0.02), + ("caron_below_ttb", "a" + ("\u032C" * 15) + "b", None, "ttb", 0.03), + ("caron_below_ttb_lb", "a" + ("\u032C" * 15) + "b", "lb", "ttb", 0.03), + # extends to the right (e.g. issue #3745) + ("double_breve_below", "a\u035Ci", None, None, 0.02), + ("double_breve_below_ma", "a\u035Ci", "ma", None, 0.02), + ("double_breve_below_ra", "a\u035Ci", "ra", None, 0.02), + ("double_breve_below_ttb", "a\u035Cb", None, "ttb", 0.02), + ("double_breve_below_ttb_rt", "a\u035Cb", "rt", "ttb", 0.02), + ("double_breve_below_ttb_mt", "a\u035Cb", "mt", "ttb", 0.02), + ("double_breve_below_ttb_st", "a\u035Cb", "st", "ttb", 0.02), + # extends to the left (fail=0.064) + ("overline", "i\u0305", None, None, 0.02), + ("overline_la", "i\u0305", "la", None, 0.02), + ("overline_ra", "i\u0305", "ra", None, 0.02), + ("overline_ttb", "i\u0305", None, "ttb", 0.02), + ("overline_ttb_rt", "i\u0305", "rt", "ttb", 0.02), + ("overline_ttb_mt", "i\u0305", "mt", "ttb", 0.02), + ("overline_ttb_st", "i\u0305", "st", "ttb", 0.02), +) + + +# this tests various combining characters for anchor alignment and clipping +@pytest.mark.parametrize( + "name, text, anchor, dir, epsilon", combine_tests, ids=[r[0] for r in combine_tests] +) +def test_combine( + name: str, text: str, dir: str | None, anchor: str | None, epsilon: float +) -> None: + path = f"Tests/images/test_combine_{name}.png" + f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48) + + im = Image.new("RGB", (400, 400), "white") + d = ImageDraw.Draw(im) + d.line(((0, 200), (400, 200)), "gray") + d.line(((200, 0), (200, 400)), "gray") + try: + d.text((200, 200), text, fill="black", anchor=anchor, direction=dir, font=f) + except ValueError as ex: + if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction": + pytest.skip("libraqm 0.7 or greater not available") + + assert_image_similar_tofile(im, path, epsilon) + + +@pytest.mark.parametrize( + "anchor, align", + ( + ("lm", "left"), # pass with getsize + ("lm", "center"), # fail at 2.12 + ("lm", "right"), # fail at 2.57 + ("mm", "left"), # fail at 2.12 + ("mm", "center"), # pass with getsize + ("mm", "right"), # fail at 2.12 + ("rm", "left"), # fail at 2.57 + ("rm", "center"), # fail at 2.12 + ("rm", "right"), # pass with getsize + ), +) +def test_combine_multiline(anchor: str, align: str) -> None: + # test that multiline text uses getlength, not getsize or getbbox + + path = f"Tests/images/test_combine_multiline_{anchor}_{align}.png" + f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48) + text = "i\u0305\u035C\ntext" # i with overline and double breve, and a word + + im = Image.new("RGB", (400, 400), "white") + d = ImageDraw.Draw(im) + d.line(((0, 200), (400, 200)), "gray") + d.line(((200, 0), (200, 400)), "gray") + bbox = d.multiline_textbbox((200, 200), text, anchor=anchor, font=f, align=align) + d.rectangle(bbox, outline="red") + d.multiline_text((200, 200), text, fill="black", anchor=anchor, font=f, align=align) + + assert_image_similar_tofile(im, path, 0.015) + + +def test_anchor_invalid_ttb() -> None: + font = ImageFont.truetype(FONT_PATH, FONT_SIZE) + im = Image.new("RGB", (100, 100), "white") + d = ImageDraw.Draw(im) + d.font = font + + for anchor in ["", "l", "a", "lax", "xa", "la", "ls", "ld", "lx"]: + with pytest.raises(ValueError): + font.getmask2("hello", anchor=anchor, direction="ttb") + with pytest.raises(ValueError): + font.getbbox("hello", anchor=anchor, direction="ttb") + with pytest.raises(ValueError): + d.text((0, 0), "hello", anchor=anchor, direction="ttb") + with pytest.raises(ValueError): + d.textbbox((0, 0), "hello", anchor=anchor, direction="ttb") + with pytest.raises(ValueError): + d.multiline_text((0, 0), "foo\nbar", anchor=anchor, direction="ttb") + with pytest.raises(ValueError): + d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor, direction="ttb") + # ttb multiline text does not support anchors at all + with pytest.raises(ValueError): + d.multiline_text((0, 0), "foo\nbar", anchor="mm", direction="ttb") + with pytest.raises(ValueError): + d.multiline_textbbox((0, 0), "foo\nbar", anchor="mm", direction="ttb") diff --git a/Tests/test_imagefontpil.py b/Tests/test_imagefontpil.py index 3b1c14b4e..0e7ee19a7 100644 --- a/Tests/test_imagefontpil.py +++ b/Tests/test_imagefontpil.py @@ -1,78 +1,78 @@ -from __future__ import annotations - -import struct -from io import BytesIO - -import pytest - -from PIL import Image, ImageDraw, ImageFont, _util, features - -from .helper import assert_image_equal_tofile - -original_core = ImageFont.core - - -def setup_module() -> None: - if features.check_module("freetype2"): - ImageFont.core = _util.DeferredError(ImportError) - - -def teardown_module() -> None: - ImageFont.core = original_core - - -def test_default_font() -> None: - # Arrange - txt = 'This is a "better than nothing" default font.' - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - - # Act - default_font = ImageFont.load_default() - draw.text((10, 10), txt, font=default_font) - - # Assert - assert_image_equal_tofile(im, "Tests/images/default_font.png") - - -def test_size_without_freetype() -> None: - with pytest.raises(ImportError): - ImageFont.load_default(size=14) - - -def test_unicode() -> None: - # should not segfault, should return UnicodeDecodeError - # issue #2826 - font = ImageFont.load_default() - with pytest.raises(UnicodeEncodeError): - font.getbbox("’") - - -def test_textbbox() -> None: - im = Image.new("RGB", (200, 200)) - d = ImageDraw.Draw(im) - default_font = ImageFont.load_default() - assert d.textlength("test", font=default_font) == 24 - assert d.textbbox((0, 0), "test", font=default_font) == (0, 0, 24, 11) - - -def test_decompression_bomb() -> None: - glyph = struct.pack(">hhhhhhhhhh", 1, 0, 0, 0, 256, 256, 0, 0, 256, 256) - fp = BytesIO(b"PILfont\n\nDATA\n" + glyph * 256) - - font = ImageFont.ImageFont() - font._load_pilfont_data(fp, Image.new("L", (256, 256))) - with pytest.raises(Image.DecompressionBombError): - font.getmask("A" * 1_000_000) - - -@pytest.mark.timeout(4) -def test_oom() -> None: - glyph = struct.pack( - ">hhhhhhhhhh", 1, 0, -32767, -32767, 32767, 32767, -32767, -32767, 32767, 32767 - ) - fp = BytesIO(b"PILfont\n\nDATA\n" + glyph * 256) - - font = ImageFont.ImageFont() - font._load_pilfont_data(fp, Image.new("L", (1, 1))) - font.getmask("A" * 1_000_000) +from __future__ import annotations + +import struct +from io import BytesIO + +import pytest + +from PIL import Image, ImageDraw, ImageFont, _util, features + +from .helper import assert_image_equal_tofile + +original_core = ImageFont.core + + +def setup_module() -> None: + if features.check_module("freetype2"): + ImageFont.core = _util.DeferredError(ImportError) + + +def teardown_module() -> None: + ImageFont.core = original_core + + +def test_default_font() -> None: + # Arrange + txt = 'This is a "better than nothing" default font.' + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + + # Act + default_font = ImageFont.load_default() + draw.text((10, 10), txt, font=default_font) + + # Assert + assert_image_equal_tofile(im, "Tests/images/default_font.png") + + +def test_size_without_freetype() -> None: + with pytest.raises(ImportError): + ImageFont.load_default(size=14) + + +def test_unicode() -> None: + # should not segfault, should return UnicodeDecodeError + # issue #2826 + font = ImageFont.load_default() + with pytest.raises(UnicodeEncodeError): + font.getbbox("’") + + +def test_textbbox() -> None: + im = Image.new("RGB", (200, 200)) + d = ImageDraw.Draw(im) + default_font = ImageFont.load_default() + assert d.textlength("test", font=default_font) == 24 + assert d.textbbox((0, 0), "test", font=default_font) == (0, 0, 24, 11) + + +def test_decompression_bomb() -> None: + glyph = struct.pack(">hhhhhhhhhh", 1, 0, 0, 0, 256, 256, 0, 0, 256, 256) + fp = BytesIO(b"PILfont\n\nDATA\n" + glyph * 256) + + font = ImageFont.ImageFont() + font._load_pilfont_data(fp, Image.new("L", (256, 256))) + with pytest.raises(Image.DecompressionBombError): + font.getmask("A" * 1_000_000) + + +@pytest.mark.timeout(4) +def test_oom() -> None: + glyph = struct.pack( + ">hhhhhhhhhh", 1, 0, -32767, -32767, 32767, 32767, -32767, -32767, 32767, 32767 + ) + fp = BytesIO(b"PILfont\n\nDATA\n" + glyph * 256) + + font = ImageFont.ImageFont() + font._load_pilfont_data(fp, Image.new("L", (1, 1))) + font.getmask("A" * 1_000_000) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index e23adeb70..dbfdd9b59 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -1,135 +1,135 @@ -from __future__ import annotations - -import os -import shutil -import subprocess -import sys - -import pytest - -from PIL import Image, ImageGrab - -from .helper import assert_image_equal_tofile, skip_unless_feature - - -class TestImageGrab: - @pytest.mark.skipif( - os.environ.get("USERNAME") == "ContainerAdministrator", - reason="can't grab screen when running in Docker", - ) - @pytest.mark.skipif( - sys.platform not in ("win32", "darwin"), reason="requires Windows or macOS" - ) - def test_grab(self) -> None: - ImageGrab.grab() - ImageGrab.grab(include_layered_windows=True) - ImageGrab.grab(all_screens=True) - - im = ImageGrab.grab(bbox=(10, 20, 50, 80)) - assert im.size == (40, 60) - - @skip_unless_feature("xcb") - def test_grab_x11(self) -> None: - try: - if sys.platform not in ("win32", "darwin"): - ImageGrab.grab() - - ImageGrab.grab(xdisplay="") - except OSError as e: - pytest.skip(str(e)) - - @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" - ): - with pytest.raises(OSError) as e: - ImageGrab.grab() - assert str(e.value).startswith("Pillow was built without XCB support") - - with pytest.raises(OSError) as e: - ImageGrab.grab(xdisplay="") - assert str(e.value).startswith("Pillow was built without XCB support") - - @skip_unless_feature("xcb") - def test_grab_invalid_xdisplay(self) -> None: - with pytest.raises(OSError) as e: - ImageGrab.grab(xdisplay="error.test:0.0") - assert str(e.value).startswith("X connection failed") - - def test_grabclipboard(self) -> None: - if sys.platform == "darwin": - subprocess.call(["screencapture", "-cx"]) - elif sys.platform == "win32": - p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE) - p.stdin.write( - b"""[Reflection.Assembly]::LoadWithPartialName("System.Drawing") -[Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") -$bmp = New-Object Drawing.Bitmap 200, 200 -[Windows.Forms.Clipboard]::SetImage($bmp)""" - ) - p.communicate() - else: - if not shutil.which("wl-paste") and not shutil.which("xclip"): - with pytest.raises( - NotImplementedError, - match="wl-paste or xclip is required for" - r" ImageGrab.grabclipboard\(\) on Linux", - ): - ImageGrab.grabclipboard() - return - - ImageGrab.grabclipboard() - - @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") - def test_grabclipboard_file(self) -> None: - p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE) - assert p.stdin is not None - p.stdin.write(rb'Set-Clipboard -Path "Tests\images\hopper.gif"') - p.communicate() - - im = ImageGrab.grabclipboard() - assert len(im) == 1 - assert os.path.samefile(im[0], "Tests/images/hopper.gif") - - @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") - def test_grabclipboard_png(self) -> None: - p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE) - assert p.stdin is not None - p.stdin.write( - rb"""$bytes = [System.IO.File]::ReadAllBytes("Tests\images\hopper.png") -$ms = new-object System.IO.MemoryStream(, $bytes) -[Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") -[Windows.Forms.Clipboard]::SetData("PNG", $ms)""" - ) - p.communicate() - - im = ImageGrab.grabclipboard() - assert_image_equal_tofile(im, "Tests/images/hopper.png") - - @pytest.mark.skipif( - ( - sys.platform != "linux" - or not all(shutil.which(cmd) for cmd in ("wl-paste", "wl-copy")) - ), - reason="Linux with wl-clipboard only", - ) - @pytest.mark.parametrize("ext", ("gif", "png", "ico")) - def test_grabclipboard_wl_clipboard(self, ext: str) -> None: - image_path = "Tests/images/hopper." + ext - with open(image_path, "rb") as fp: - subprocess.call(["wl-copy"], stdin=fp) - im = ImageGrab.grabclipboard() - assert_image_equal_tofile(im, image_path) - - @pytest.mark.skipif( - ( - sys.platform != "linux" - or not all(shutil.which(cmd) for cmd in ("wl-paste", "wl-copy")) - ), - reason="Linux with wl-clipboard only", - ) - @pytest.mark.parametrize("arg", ("text", "--clear")) - def test_grabclipboard_wl_clipboard_errors(self, arg: str) -> None: - subprocess.call(["wl-copy", arg]) - assert ImageGrab.grabclipboard() is None +from __future__ import annotations + +import os +import shutil +import subprocess +import sys + +import pytest + +from PIL import Image, ImageGrab + +from .helper import assert_image_equal_tofile, skip_unless_feature + + +class TestImageGrab: + @pytest.mark.skipif( + os.environ.get("USERNAME") == "ContainerAdministrator", + reason="can't grab screen when running in Docker", + ) + @pytest.mark.skipif( + sys.platform not in ("win32", "darwin"), reason="requires Windows or macOS" + ) + def test_grab(self) -> None: + ImageGrab.grab() + ImageGrab.grab(include_layered_windows=True) + ImageGrab.grab(all_screens=True) + + im = ImageGrab.grab(bbox=(10, 20, 50, 80)) + assert im.size == (40, 60) + + @skip_unless_feature("xcb") + def test_grab_x11(self) -> None: + try: + if sys.platform not in ("win32", "darwin"): + ImageGrab.grab() + + ImageGrab.grab(xdisplay="") + except OSError as e: + pytest.skip(str(e)) + + @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" + ): + with pytest.raises(OSError) as e: + ImageGrab.grab() + assert str(e.value).startswith("Pillow was built without XCB support") + + with pytest.raises(OSError) as e: + ImageGrab.grab(xdisplay="") + assert str(e.value).startswith("Pillow was built without XCB support") + + @skip_unless_feature("xcb") + def test_grab_invalid_xdisplay(self) -> None: + with pytest.raises(OSError) as e: + ImageGrab.grab(xdisplay="error.test:0.0") + assert str(e.value).startswith("X connection failed") + + def test_grabclipboard(self) -> None: + if sys.platform == "darwin": + subprocess.call(["screencapture", "-cx"]) + elif sys.platform == "win32": + p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE) + p.stdin.write( + b"""[Reflection.Assembly]::LoadWithPartialName("System.Drawing") +[Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") +$bmp = New-Object Drawing.Bitmap 200, 200 +[Windows.Forms.Clipboard]::SetImage($bmp)""" + ) + p.communicate() + else: + if not shutil.which("wl-paste") and not shutil.which("xclip"): + with pytest.raises( + NotImplementedError, + match="wl-paste or xclip is required for" + r" ImageGrab.grabclipboard\(\) on Linux", + ): + ImageGrab.grabclipboard() + return + + ImageGrab.grabclipboard() + + @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") + def test_grabclipboard_file(self) -> None: + p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE) + assert p.stdin is not None + p.stdin.write(rb'Set-Clipboard -Path "Tests\images\hopper.gif"') + p.communicate() + + im = ImageGrab.grabclipboard() + assert len(im) == 1 + assert os.path.samefile(im[0], "Tests/images/hopper.gif") + + @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") + def test_grabclipboard_png(self) -> None: + p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE) + assert p.stdin is not None + p.stdin.write( + rb"""$bytes = [System.IO.File]::ReadAllBytes("Tests\images\hopper.png") +$ms = new-object System.IO.MemoryStream(, $bytes) +[Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") +[Windows.Forms.Clipboard]::SetData("PNG", $ms)""" + ) + p.communicate() + + im = ImageGrab.grabclipboard() + assert_image_equal_tofile(im, "Tests/images/hopper.png") + + @pytest.mark.skipif( + ( + sys.platform != "linux" + or not all(shutil.which(cmd) for cmd in ("wl-paste", "wl-copy")) + ), + reason="Linux with wl-clipboard only", + ) + @pytest.mark.parametrize("ext", ("gif", "png", "ico")) + def test_grabclipboard_wl_clipboard(self, ext: str) -> None: + image_path = "Tests/images/hopper." + ext + with open(image_path, "rb") as fp: + subprocess.call(["wl-copy"], stdin=fp) + im = ImageGrab.grabclipboard() + assert_image_equal_tofile(im, image_path) + + @pytest.mark.skipif( + ( + sys.platform != "linux" + or not all(shutil.which(cmd) for cmd in ("wl-paste", "wl-copy")) + ), + reason="Linux with wl-clipboard only", + ) + @pytest.mark.parametrize("arg", ("text", "--clear")) + def test_grabclipboard_wl_clipboard_errors(self, arg: str) -> None: + subprocess.call(["wl-copy", arg]) + assert ImageGrab.grabclipboard() is None diff --git a/Tests/test_imagemath_lambda_eval.py b/Tests/test_imagemath_lambda_eval.py index 5769c903e..03e0516d1 100644 --- a/Tests/test_imagemath_lambda_eval.py +++ b/Tests/test_imagemath_lambda_eval.py @@ -1,496 +1,496 @@ -from __future__ import annotations - -from PIL import Image, ImageMath - - -def pixel(im: Image.Image | int) -> str | int: - if isinstance(im, int): - return int(im) # hack to deal with booleans - - return f"{im.mode} {repr(im.getpixel((0, 0)))}" - - -A = Image.new("L", (1, 1), 1) -B = Image.new("L", (1, 1), 2) -Z = Image.new("L", (1, 1), 0) # Z for zero -F = Image.new("F", (1, 1), 3) -I = Image.new("I", (1, 1), 4) # noqa: E741 - -A2 = A.resize((2, 2)) -B2 = B.resize((2, 2)) - -images = {"A": A, "B": B, "F": F, "I": I} - - -def test_sanity() -> None: - assert ImageMath.lambda_eval(lambda args: 1) == 1 - assert ImageMath.lambda_eval(lambda args: 1 + args["A"], A=2) == 3 - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["A"] + args["B"], A=A, B=B)) - == "I 3" - ) - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["A"] + args["B"], images)) - == "I 3" - ) - assert ( - pixel( - ImageMath.lambda_eval( - lambda args: args["float"](args["A"]) + args["B"], images - ) - ) - == "F 3.0" - ) - assert ( - pixel( - ImageMath.lambda_eval( - lambda args: args["int"](args["float"](args["A"]) + args["B"]), images - ) - ) - == "I 3" - ) - - -def test_ops() -> None: - assert pixel(ImageMath.lambda_eval(lambda args: args["A"] * -1, images)) == "I -1" - - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["A"] + args["B"], images)) - == "I 3" - ) - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["A"] - args["B"], images)) - == "I -1" - ) - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["A"] * args["B"], images)) - == "I 2" - ) - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["A"] / args["B"], images)) - == "I 0" - ) - assert pixel(ImageMath.lambda_eval(lambda args: args["B"] ** 2, images)) == "I 4" - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["B"] ** 33, images)) - == "I 2147483647" - ) - - assert ( - pixel( - ImageMath.lambda_eval( - lambda args: args["float"](args["A"]) + args["B"], images - ) - ) - == "F 3.0" - ) - assert ( - pixel( - ImageMath.lambda_eval( - lambda args: args["float"](args["A"]) - args["B"], images - ) - ) - == "F -1.0" - ) - assert ( - pixel( - ImageMath.lambda_eval( - lambda args: args["float"](args["A"]) * args["B"], images - ) - ) - == "F 2.0" - ) - assert ( - pixel( - ImageMath.lambda_eval( - lambda args: args["float"](args["A"]) / args["B"], images - ) - ) - == "F 0.5" - ) - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["float"](args["B"]) ** 2, images)) - == "F 4.0" - ) - assert ( - pixel( - ImageMath.lambda_eval(lambda args: args["float"](args["B"]) ** 33, images) - ) - == "F 8589934592.0" - ) - - -def test_logical() -> None: - assert pixel(ImageMath.lambda_eval(lambda args: not args["A"], images)) == 0 - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["A"] and args["B"], images)) - == "L 2" - ) - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["A"] or args["B"], images)) - == "L 1" - ) - - -def test_convert() -> None: - assert ( - pixel( - ImageMath.lambda_eval( - lambda args: args["convert"](args["A"] + args["B"], "L"), images - ) - ) - == "L 3" - ) - assert ( - pixel( - ImageMath.lambda_eval( - lambda args: args["convert"](args["A"] + args["B"], "1"), images - ) - ) - == "1 0" - ) - assert ( - pixel( - ImageMath.lambda_eval( - lambda args: args["convert"](args["A"] + args["B"], "RGB"), images - ) - ) - == "RGB (3, 3, 3)" - ) - - -def test_compare() -> None: - assert ( - pixel( - ImageMath.lambda_eval( - lambda args: args["min"](args["A"], args["B"]), images - ) - ) - == "I 1" - ) - assert ( - pixel( - ImageMath.lambda_eval( - lambda args: args["max"](args["A"], args["B"]), images - ) - ) - == "I 2" - ) - assert pixel(ImageMath.lambda_eval(lambda args: args["A"] == 1, images)) == "I 1" - assert pixel(ImageMath.lambda_eval(lambda args: args["A"] == 2, images)) == "I 0" - - -def test_one_image_larger() -> None: - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["A"] + args["B"], A=A2, B=B)) - == "I 3" - ) - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["A"] + args["B"], A=A, B=B2)) - == "I 3" - ) - - -def test_abs() -> None: - assert pixel(ImageMath.lambda_eval(lambda args: abs(args["A"]), A=A)) == "I 1" - assert pixel(ImageMath.lambda_eval(lambda args: abs(args["B"]), B=B)) == "I 2" - - -def test_binary_mod() -> None: - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["A"] % args["A"], A=A)) == "I 0" - ) - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["B"] % args["B"], B=B)) == "I 0" - ) - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["A"] % args["B"], A=A, B=B)) - == "I 1" - ) - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["B"] % args["A"], A=A, B=B)) - == "I 0" - ) - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["Z"] % args["A"], A=A, Z=Z)) - == "I 0" - ) - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["Z"] % args["B"], B=B, Z=Z)) - == "I 0" - ) - - -def test_bitwise_invert() -> None: - assert pixel(ImageMath.lambda_eval(lambda args: ~args["Z"], Z=Z)) == "I -1" - assert pixel(ImageMath.lambda_eval(lambda args: ~args["A"], A=A)) == "I -2" - assert pixel(ImageMath.lambda_eval(lambda args: ~args["B"], B=B)) == "I -3" - - -def test_bitwise_and() -> None: - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["Z"] & args["Z"], A=A, Z=Z)) - == "I 0" - ) - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["Z"] & args["A"], A=A, Z=Z)) - == "I 0" - ) - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["A"] & args["Z"], A=A, Z=Z)) - == "I 0" - ) - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["A"] & args["A"], A=A, Z=Z)) - == "I 1" - ) - - -def test_bitwise_or() -> None: - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["Z"] | args["Z"], A=A, Z=Z)) - == "I 0" - ) - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["Z"] | args["A"], A=A, Z=Z)) - == "I 1" - ) - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["A"] | args["Z"], A=A, Z=Z)) - == "I 1" - ) - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["A"] | args["A"], A=A, Z=Z)) - == "I 1" - ) - - -def test_bitwise_xor() -> None: - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["Z"] ^ args["Z"], A=A, Z=Z)) - == "I 0" - ) - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["Z"] ^ args["A"], A=A, Z=Z)) - == "I 1" - ) - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["A"] ^ args["Z"], A=A, Z=Z)) - == "I 1" - ) - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["A"] ^ args["A"], A=A, Z=Z)) - == "I 0" - ) - - -def test_bitwise_leftshift() -> None: - assert pixel(ImageMath.lambda_eval(lambda args: args["Z"] << 0, Z=Z)) == "I 0" - assert pixel(ImageMath.lambda_eval(lambda args: args["Z"] << 1, Z=Z)) == "I 0" - assert pixel(ImageMath.lambda_eval(lambda args: args["A"] << 0, A=A)) == "I 1" - assert pixel(ImageMath.lambda_eval(lambda args: args["A"] << 1, A=A)) == "I 2" - - -def test_bitwise_rightshift() -> None: - assert pixel(ImageMath.lambda_eval(lambda args: args["Z"] >> 0, Z=Z)) == "I 0" - assert pixel(ImageMath.lambda_eval(lambda args: args["Z"] >> 1, Z=Z)) == "I 0" - assert pixel(ImageMath.lambda_eval(lambda args: args["A"] >> 0, A=A)) == "I 1" - assert pixel(ImageMath.lambda_eval(lambda args: args["A"] >> 1, A=A)) == "I 0" - - -def test_logical_eq() -> None: - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["A"] == args["A"], A=A)) == "I 1" - ) - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["B"] == args["B"], B=B)) == "I 1" - ) - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["A"] == args["B"], A=A, B=B)) - == "I 0" - ) - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["B"] == args["A"], A=A, B=B)) - == "I 0" - ) - - -def test_logical_ne() -> None: - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["A"] != args["A"], A=A)) == "I 0" - ) - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["B"] != args["B"], B=B)) == "I 0" - ) - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["A"] != args["B"], A=A, B=B)) - == "I 1" - ) - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["B"] != args["A"], A=A, B=B)) - == "I 1" - ) - - -def test_logical_lt() -> None: - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["A"] < args["A"], A=A)) == "I 0" - ) - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["B"] < args["B"], B=B)) == "I 0" - ) - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["A"] < args["B"], A=A, B=B)) - == "I 1" - ) - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["B"] < args["A"], A=A, B=B)) - == "I 0" - ) - - -def test_logical_le() -> None: - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["A"] <= args["A"], A=A)) == "I 1" - ) - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["B"] <= args["B"], B=B)) == "I 1" - ) - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["A"] <= args["B"], A=A, B=B)) - == "I 1" - ) - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["B"] <= args["A"], A=A, B=B)) - == "I 0" - ) - - -def test_logical_gt() -> None: - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["A"] > args["A"], A=A)) == "I 0" - ) - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["B"] > args["B"], B=B)) == "I 0" - ) - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["A"] > args["B"], A=A, B=B)) - == "I 0" - ) - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["B"] > args["A"], A=A, B=B)) - == "I 1" - ) - - -def test_logical_ge() -> None: - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["A"] >= args["A"], A=A)) == "I 1" - ) - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["B"] >= args["B"], B=B)) == "I 1" - ) - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["A"] >= args["B"], A=A, B=B)) - == "I 0" - ) - assert ( - pixel(ImageMath.lambda_eval(lambda args: args["B"] >= args["A"], A=A, B=B)) - == "I 1" - ) - - -def test_logical_equal() -> None: - assert ( - pixel( - ImageMath.lambda_eval(lambda args: args["equal"](args["A"], args["A"]), A=A) - ) - == "I 1" - ) - assert ( - pixel( - ImageMath.lambda_eval(lambda args: args["equal"](args["B"], args["B"]), B=B) - ) - == "I 1" - ) - assert ( - pixel( - ImageMath.lambda_eval(lambda args: args["equal"](args["Z"], args["Z"]), Z=Z) - ) - == "I 1" - ) - assert ( - pixel( - ImageMath.lambda_eval( - lambda args: args["equal"](args["A"], args["B"]), A=A, B=B - ) - ) - == "I 0" - ) - assert ( - pixel( - ImageMath.lambda_eval( - lambda args: args["equal"](args["B"], args["A"]), A=A, B=B - ) - ) - == "I 0" - ) - assert ( - pixel( - ImageMath.lambda_eval( - lambda args: args["equal"](args["A"], args["Z"]), A=A, Z=Z - ) - ) - == "I 0" - ) - - -def test_logical_not_equal() -> None: - assert ( - pixel( - ImageMath.lambda_eval( - lambda args: args["notequal"](args["A"], args["A"]), A=A - ) - ) - == "I 0" - ) - assert ( - pixel( - ImageMath.lambda_eval( - lambda args: args["notequal"](args["B"], args["B"]), B=B - ) - ) - == "I 0" - ) - assert ( - pixel( - ImageMath.lambda_eval( - lambda args: args["notequal"](args["Z"], args["Z"]), Z=Z - ) - ) - == "I 0" - ) - assert ( - pixel( - ImageMath.lambda_eval( - lambda args: args["notequal"](args["A"], args["B"]), A=A, B=B - ) - ) - == "I 1" - ) - assert ( - pixel( - ImageMath.lambda_eval( - lambda args: args["notequal"](args["B"], args["A"]), A=A, B=B - ) - ) - == "I 1" - ) - assert ( - pixel( - ImageMath.lambda_eval( - lambda args: args["notequal"](args["A"], args["Z"]), A=A, Z=Z - ) - ) - == "I 1" - ) +from __future__ import annotations + +from PIL import Image, ImageMath + + +def pixel(im: Image.Image | int) -> str | int: + if isinstance(im, int): + return int(im) # hack to deal with booleans + + return f"{im.mode} {repr(im.getpixel((0, 0)))}" + + +A = Image.new("L", (1, 1), 1) +B = Image.new("L", (1, 1), 2) +Z = Image.new("L", (1, 1), 0) # Z for zero +F = Image.new("F", (1, 1), 3) +I = Image.new("I", (1, 1), 4) # noqa: E741 + +A2 = A.resize((2, 2)) +B2 = B.resize((2, 2)) + +images = {"A": A, "B": B, "F": F, "I": I} + + +def test_sanity() -> None: + assert ImageMath.lambda_eval(lambda args: 1) == 1 + assert ImageMath.lambda_eval(lambda args: 1 + args["A"], A=2) == 3 + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] + args["B"], A=A, B=B)) + == "I 3" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] + args["B"], images)) + == "I 3" + ) + assert ( + pixel( + ImageMath.lambda_eval( + lambda args: args["float"](args["A"]) + args["B"], images + ) + ) + == "F 3.0" + ) + assert ( + pixel( + ImageMath.lambda_eval( + lambda args: args["int"](args["float"](args["A"]) + args["B"]), images + ) + ) + == "I 3" + ) + + +def test_ops() -> None: + assert pixel(ImageMath.lambda_eval(lambda args: args["A"] * -1, images)) == "I -1" + + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] + args["B"], images)) + == "I 3" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] - args["B"], images)) + == "I -1" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] * args["B"], images)) + == "I 2" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] / args["B"], images)) + == "I 0" + ) + assert pixel(ImageMath.lambda_eval(lambda args: args["B"] ** 2, images)) == "I 4" + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["B"] ** 33, images)) + == "I 2147483647" + ) + + assert ( + pixel( + ImageMath.lambda_eval( + lambda args: args["float"](args["A"]) + args["B"], images + ) + ) + == "F 3.0" + ) + assert ( + pixel( + ImageMath.lambda_eval( + lambda args: args["float"](args["A"]) - args["B"], images + ) + ) + == "F -1.0" + ) + assert ( + pixel( + ImageMath.lambda_eval( + lambda args: args["float"](args["A"]) * args["B"], images + ) + ) + == "F 2.0" + ) + assert ( + pixel( + ImageMath.lambda_eval( + lambda args: args["float"](args["A"]) / args["B"], images + ) + ) + == "F 0.5" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["float"](args["B"]) ** 2, images)) + == "F 4.0" + ) + assert ( + pixel( + ImageMath.lambda_eval(lambda args: args["float"](args["B"]) ** 33, images) + ) + == "F 8589934592.0" + ) + + +def test_logical() -> None: + assert pixel(ImageMath.lambda_eval(lambda args: not args["A"], images)) == 0 + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] and args["B"], images)) + == "L 2" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] or args["B"], images)) + == "L 1" + ) + + +def test_convert() -> None: + assert ( + pixel( + ImageMath.lambda_eval( + lambda args: args["convert"](args["A"] + args["B"], "L"), images + ) + ) + == "L 3" + ) + assert ( + pixel( + ImageMath.lambda_eval( + lambda args: args["convert"](args["A"] + args["B"], "1"), images + ) + ) + == "1 0" + ) + assert ( + pixel( + ImageMath.lambda_eval( + lambda args: args["convert"](args["A"] + args["B"], "RGB"), images + ) + ) + == "RGB (3, 3, 3)" + ) + + +def test_compare() -> None: + assert ( + pixel( + ImageMath.lambda_eval( + lambda args: args["min"](args["A"], args["B"]), images + ) + ) + == "I 1" + ) + assert ( + pixel( + ImageMath.lambda_eval( + lambda args: args["max"](args["A"], args["B"]), images + ) + ) + == "I 2" + ) + assert pixel(ImageMath.lambda_eval(lambda args: args["A"] == 1, images)) == "I 1" + assert pixel(ImageMath.lambda_eval(lambda args: args["A"] == 2, images)) == "I 0" + + +def test_one_image_larger() -> None: + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] + args["B"], A=A2, B=B)) + == "I 3" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] + args["B"], A=A, B=B2)) + == "I 3" + ) + + +def test_abs() -> None: + assert pixel(ImageMath.lambda_eval(lambda args: abs(args["A"]), A=A)) == "I 1" + assert pixel(ImageMath.lambda_eval(lambda args: abs(args["B"]), B=B)) == "I 2" + + +def test_binary_mod() -> None: + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] % args["A"], A=A)) == "I 0" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["B"] % args["B"], B=B)) == "I 0" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] % args["B"], A=A, B=B)) + == "I 1" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["B"] % args["A"], A=A, B=B)) + == "I 0" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["Z"] % args["A"], A=A, Z=Z)) + == "I 0" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["Z"] % args["B"], B=B, Z=Z)) + == "I 0" + ) + + +def test_bitwise_invert() -> None: + assert pixel(ImageMath.lambda_eval(lambda args: ~args["Z"], Z=Z)) == "I -1" + assert pixel(ImageMath.lambda_eval(lambda args: ~args["A"], A=A)) == "I -2" + assert pixel(ImageMath.lambda_eval(lambda args: ~args["B"], B=B)) == "I -3" + + +def test_bitwise_and() -> None: + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["Z"] & args["Z"], A=A, Z=Z)) + == "I 0" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["Z"] & args["A"], A=A, Z=Z)) + == "I 0" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] & args["Z"], A=A, Z=Z)) + == "I 0" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] & args["A"], A=A, Z=Z)) + == "I 1" + ) + + +def test_bitwise_or() -> None: + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["Z"] | args["Z"], A=A, Z=Z)) + == "I 0" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["Z"] | args["A"], A=A, Z=Z)) + == "I 1" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] | args["Z"], A=A, Z=Z)) + == "I 1" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] | args["A"], A=A, Z=Z)) + == "I 1" + ) + + +def test_bitwise_xor() -> None: + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["Z"] ^ args["Z"], A=A, Z=Z)) + == "I 0" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["Z"] ^ args["A"], A=A, Z=Z)) + == "I 1" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] ^ args["Z"], A=A, Z=Z)) + == "I 1" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] ^ args["A"], A=A, Z=Z)) + == "I 0" + ) + + +def test_bitwise_leftshift() -> None: + assert pixel(ImageMath.lambda_eval(lambda args: args["Z"] << 0, Z=Z)) == "I 0" + assert pixel(ImageMath.lambda_eval(lambda args: args["Z"] << 1, Z=Z)) == "I 0" + assert pixel(ImageMath.lambda_eval(lambda args: args["A"] << 0, A=A)) == "I 1" + assert pixel(ImageMath.lambda_eval(lambda args: args["A"] << 1, A=A)) == "I 2" + + +def test_bitwise_rightshift() -> None: + assert pixel(ImageMath.lambda_eval(lambda args: args["Z"] >> 0, Z=Z)) == "I 0" + assert pixel(ImageMath.lambda_eval(lambda args: args["Z"] >> 1, Z=Z)) == "I 0" + assert pixel(ImageMath.lambda_eval(lambda args: args["A"] >> 0, A=A)) == "I 1" + assert pixel(ImageMath.lambda_eval(lambda args: args["A"] >> 1, A=A)) == "I 0" + + +def test_logical_eq() -> None: + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] == args["A"], A=A)) == "I 1" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["B"] == args["B"], B=B)) == "I 1" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] == args["B"], A=A, B=B)) + == "I 0" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["B"] == args["A"], A=A, B=B)) + == "I 0" + ) + + +def test_logical_ne() -> None: + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] != args["A"], A=A)) == "I 0" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["B"] != args["B"], B=B)) == "I 0" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] != args["B"], A=A, B=B)) + == "I 1" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["B"] != args["A"], A=A, B=B)) + == "I 1" + ) + + +def test_logical_lt() -> None: + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] < args["A"], A=A)) == "I 0" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["B"] < args["B"], B=B)) == "I 0" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] < args["B"], A=A, B=B)) + == "I 1" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["B"] < args["A"], A=A, B=B)) + == "I 0" + ) + + +def test_logical_le() -> None: + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] <= args["A"], A=A)) == "I 1" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["B"] <= args["B"], B=B)) == "I 1" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] <= args["B"], A=A, B=B)) + == "I 1" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["B"] <= args["A"], A=A, B=B)) + == "I 0" + ) + + +def test_logical_gt() -> None: + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] > args["A"], A=A)) == "I 0" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["B"] > args["B"], B=B)) == "I 0" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] > args["B"], A=A, B=B)) + == "I 0" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["B"] > args["A"], A=A, B=B)) + == "I 1" + ) + + +def test_logical_ge() -> None: + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] >= args["A"], A=A)) == "I 1" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["B"] >= args["B"], B=B)) == "I 1" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] >= args["B"], A=A, B=B)) + == "I 0" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["B"] >= args["A"], A=A, B=B)) + == "I 1" + ) + + +def test_logical_equal() -> None: + assert ( + pixel( + ImageMath.lambda_eval(lambda args: args["equal"](args["A"], args["A"]), A=A) + ) + == "I 1" + ) + assert ( + pixel( + ImageMath.lambda_eval(lambda args: args["equal"](args["B"], args["B"]), B=B) + ) + == "I 1" + ) + assert ( + pixel( + ImageMath.lambda_eval(lambda args: args["equal"](args["Z"], args["Z"]), Z=Z) + ) + == "I 1" + ) + assert ( + pixel( + ImageMath.lambda_eval( + lambda args: args["equal"](args["A"], args["B"]), A=A, B=B + ) + ) + == "I 0" + ) + assert ( + pixel( + ImageMath.lambda_eval( + lambda args: args["equal"](args["B"], args["A"]), A=A, B=B + ) + ) + == "I 0" + ) + assert ( + pixel( + ImageMath.lambda_eval( + lambda args: args["equal"](args["A"], args["Z"]), A=A, Z=Z + ) + ) + == "I 0" + ) + + +def test_logical_not_equal() -> None: + assert ( + pixel( + ImageMath.lambda_eval( + lambda args: args["notequal"](args["A"], args["A"]), A=A + ) + ) + == "I 0" + ) + assert ( + pixel( + ImageMath.lambda_eval( + lambda args: args["notequal"](args["B"], args["B"]), B=B + ) + ) + == "I 0" + ) + assert ( + pixel( + ImageMath.lambda_eval( + lambda args: args["notequal"](args["Z"], args["Z"]), Z=Z + ) + ) + == "I 0" + ) + assert ( + pixel( + ImageMath.lambda_eval( + lambda args: args["notequal"](args["A"], args["B"]), A=A, B=B + ) + ) + == "I 1" + ) + assert ( + pixel( + ImageMath.lambda_eval( + lambda args: args["notequal"](args["B"], args["A"]), A=A, B=B + ) + ) + == "I 1" + ) + assert ( + pixel( + ImageMath.lambda_eval( + lambda args: args["notequal"](args["A"], args["Z"]), A=A, Z=Z + ) + ) + == "I 1" + ) diff --git a/Tests/test_imagemath_unsafe_eval.py b/Tests/test_imagemath_unsafe_eval.py index 7b8a562d7..464309928 100644 --- a/Tests/test_imagemath_unsafe_eval.py +++ b/Tests/test_imagemath_unsafe_eval.py @@ -1,221 +1,221 @@ -from __future__ import annotations - -import pytest - -from PIL import Image, ImageMath - - -def pixel(im: Image.Image | int) -> str | int: - if isinstance(im, int): - return int(im) # hack to deal with booleans - - return f"{im.mode} {repr(im.getpixel((0, 0)))}" - - -A = Image.new("L", (1, 1), 1) -B = Image.new("L", (1, 1), 2) -Z = Image.new("L", (1, 1), 0) # Z for zero -F = Image.new("F", (1, 1), 3) -I = Image.new("I", (1, 1), 4) # noqa: E741 - -A2 = A.resize((2, 2)) -B2 = B.resize((2, 2)) - -images = {"A": A, "B": B, "F": F, "I": I} - - -def test_sanity() -> None: - assert ImageMath.unsafe_eval("1") == 1 - assert ImageMath.unsafe_eval("1+A", A=2) == 3 - assert pixel(ImageMath.unsafe_eval("A+B", A=A, B=B)) == "I 3" - assert pixel(ImageMath.unsafe_eval("A+B", images)) == "I 3" - assert pixel(ImageMath.unsafe_eval("float(A)+B", images)) == "F 3.0" - assert pixel(ImageMath.unsafe_eval("int(float(A)+B)", images)) == "I 3" - - -def test_eval_deprecated() -> None: - with pytest.warns(DeprecationWarning): - assert ImageMath.eval("1") == 1 - - -def test_ops() -> None: - assert pixel(ImageMath.unsafe_eval("-A", images)) == "I -1" - assert pixel(ImageMath.unsafe_eval("+B", images)) == "L 2" - - assert pixel(ImageMath.unsafe_eval("A+B", images)) == "I 3" - assert pixel(ImageMath.unsafe_eval("A-B", images)) == "I -1" - assert pixel(ImageMath.unsafe_eval("A*B", images)) == "I 2" - assert pixel(ImageMath.unsafe_eval("A/B", images)) == "I 0" - assert pixel(ImageMath.unsafe_eval("B**2", images)) == "I 4" - assert pixel(ImageMath.unsafe_eval("B**33", images)) == "I 2147483647" - - assert pixel(ImageMath.unsafe_eval("float(A)+B", images)) == "F 3.0" - assert pixel(ImageMath.unsafe_eval("float(A)-B", images)) == "F -1.0" - assert pixel(ImageMath.unsafe_eval("float(A)*B", images)) == "F 2.0" - assert pixel(ImageMath.unsafe_eval("float(A)/B", images)) == "F 0.5" - assert pixel(ImageMath.unsafe_eval("float(B)**2", images)) == "F 4.0" - assert pixel(ImageMath.unsafe_eval("float(B)**33", images)) == "F 8589934592.0" - - -@pytest.mark.parametrize( - "expression", - ( - "exec('pass')", - "(lambda: exec('pass'))()", - "(lambda: (lambda: exec('pass'))())()", - ), -) -def test_prevent_exec(expression: str) -> None: - with pytest.raises(ValueError): - ImageMath.unsafe_eval(expression) - - -def test_prevent_double_underscores() -> None: - with pytest.raises(ValueError): - ImageMath.unsafe_eval("1", {"__": None}) - - -def test_prevent_builtins() -> None: - with pytest.raises(ValueError): - ImageMath.unsafe_eval("(lambda: exec('exit()'))()", {"exec": None}) - - -def test_logical() -> None: - assert pixel(ImageMath.unsafe_eval("not A", images)) == 0 - assert pixel(ImageMath.unsafe_eval("A and B", images)) == "L 2" - assert pixel(ImageMath.unsafe_eval("A or B", images)) == "L 1" - - -def test_convert() -> None: - assert pixel(ImageMath.unsafe_eval("convert(A+B, 'L')", images)) == "L 3" - assert pixel(ImageMath.unsafe_eval("convert(A+B, '1')", images)) == "1 0" - assert ( - pixel(ImageMath.unsafe_eval("convert(A+B, 'RGB')", images)) == "RGB (3, 3, 3)" - ) - - -def test_compare() -> None: - assert pixel(ImageMath.unsafe_eval("min(A, B)", images)) == "I 1" - assert pixel(ImageMath.unsafe_eval("max(A, B)", images)) == "I 2" - assert pixel(ImageMath.unsafe_eval("A == 1", images)) == "I 1" - assert pixel(ImageMath.unsafe_eval("A == 2", images)) == "I 0" - - -def test_one_image_larger() -> None: - assert pixel(ImageMath.unsafe_eval("A+B", A=A2, B=B)) == "I 3" - assert pixel(ImageMath.unsafe_eval("A+B", A=A, B=B2)) == "I 3" - - -def test_abs() -> None: - assert pixel(ImageMath.unsafe_eval("abs(A)", A=A)) == "I 1" - assert pixel(ImageMath.unsafe_eval("abs(B)", B=B)) == "I 2" - - -def test_binary_mod() -> None: - assert pixel(ImageMath.unsafe_eval("A%A", A=A)) == "I 0" - assert pixel(ImageMath.unsafe_eval("B%B", B=B)) == "I 0" - assert pixel(ImageMath.unsafe_eval("A%B", A=A, B=B)) == "I 1" - assert pixel(ImageMath.unsafe_eval("B%A", A=A, B=B)) == "I 0" - assert pixel(ImageMath.unsafe_eval("Z%A", A=A, Z=Z)) == "I 0" - assert pixel(ImageMath.unsafe_eval("Z%B", B=B, Z=Z)) == "I 0" - - -def test_bitwise_invert() -> None: - assert pixel(ImageMath.unsafe_eval("~Z", Z=Z)) == "I -1" - assert pixel(ImageMath.unsafe_eval("~A", A=A)) == "I -2" - assert pixel(ImageMath.unsafe_eval("~B", B=B)) == "I -3" - - -def test_bitwise_and() -> None: - assert pixel(ImageMath.unsafe_eval("Z&Z", A=A, Z=Z)) == "I 0" - assert pixel(ImageMath.unsafe_eval("Z&A", A=A, Z=Z)) == "I 0" - assert pixel(ImageMath.unsafe_eval("A&Z", A=A, Z=Z)) == "I 0" - assert pixel(ImageMath.unsafe_eval("A&A", A=A, Z=Z)) == "I 1" - - -def test_bitwise_or() -> None: - assert pixel(ImageMath.unsafe_eval("Z|Z", A=A, Z=Z)) == "I 0" - assert pixel(ImageMath.unsafe_eval("Z|A", A=A, Z=Z)) == "I 1" - assert pixel(ImageMath.unsafe_eval("A|Z", A=A, Z=Z)) == "I 1" - assert pixel(ImageMath.unsafe_eval("A|A", A=A, Z=Z)) == "I 1" - - -def test_bitwise_xor() -> None: - assert pixel(ImageMath.unsafe_eval("Z^Z", A=A, Z=Z)) == "I 0" - assert pixel(ImageMath.unsafe_eval("Z^A", A=A, Z=Z)) == "I 1" - assert pixel(ImageMath.unsafe_eval("A^Z", A=A, Z=Z)) == "I 1" - assert pixel(ImageMath.unsafe_eval("A^A", A=A, Z=Z)) == "I 0" - - -def test_bitwise_leftshift() -> None: - assert pixel(ImageMath.unsafe_eval("Z<<0", Z=Z)) == "I 0" - assert pixel(ImageMath.unsafe_eval("Z<<1", Z=Z)) == "I 0" - assert pixel(ImageMath.unsafe_eval("A<<0", A=A)) == "I 1" - assert pixel(ImageMath.unsafe_eval("A<<1", A=A)) == "I 2" - - -def test_bitwise_rightshift() -> None: - assert pixel(ImageMath.unsafe_eval("Z>>0", Z=Z)) == "I 0" - assert pixel(ImageMath.unsafe_eval("Z>>1", Z=Z)) == "I 0" - assert pixel(ImageMath.unsafe_eval("A>>0", A=A)) == "I 1" - assert pixel(ImageMath.unsafe_eval("A>>1", A=A)) == "I 0" - - -def test_logical_eq() -> None: - assert pixel(ImageMath.unsafe_eval("A==A", A=A)) == "I 1" - assert pixel(ImageMath.unsafe_eval("B==B", B=B)) == "I 1" - assert pixel(ImageMath.unsafe_eval("A==B", A=A, B=B)) == "I 0" - assert pixel(ImageMath.unsafe_eval("B==A", A=A, B=B)) == "I 0" - - -def test_logical_ne() -> None: - assert pixel(ImageMath.unsafe_eval("A!=A", A=A)) == "I 0" - assert pixel(ImageMath.unsafe_eval("B!=B", B=B)) == "I 0" - assert pixel(ImageMath.unsafe_eval("A!=B", A=A, B=B)) == "I 1" - assert pixel(ImageMath.unsafe_eval("B!=A", A=A, B=B)) == "I 1" - - -def test_logical_lt() -> None: - assert pixel(ImageMath.unsafe_eval("A None: - assert pixel(ImageMath.unsafe_eval("A<=A", A=A)) == "I 1" - assert pixel(ImageMath.unsafe_eval("B<=B", B=B)) == "I 1" - assert pixel(ImageMath.unsafe_eval("A<=B", A=A, B=B)) == "I 1" - assert pixel(ImageMath.unsafe_eval("B<=A", A=A, B=B)) == "I 0" - - -def test_logical_gt() -> None: - assert pixel(ImageMath.unsafe_eval("A>A", A=A)) == "I 0" - assert pixel(ImageMath.unsafe_eval("B>B", B=B)) == "I 0" - assert pixel(ImageMath.unsafe_eval("A>B", A=A, B=B)) == "I 0" - assert pixel(ImageMath.unsafe_eval("B>A", A=A, B=B)) == "I 1" - - -def test_logical_ge() -> None: - assert pixel(ImageMath.unsafe_eval("A>=A", A=A)) == "I 1" - assert pixel(ImageMath.unsafe_eval("B>=B", B=B)) == "I 1" - assert pixel(ImageMath.unsafe_eval("A>=B", A=A, B=B)) == "I 0" - assert pixel(ImageMath.unsafe_eval("B>=A", A=A, B=B)) == "I 1" - - -def test_logical_equal() -> None: - assert pixel(ImageMath.unsafe_eval("equal(A, A)", A=A)) == "I 1" - assert pixel(ImageMath.unsafe_eval("equal(B, B)", B=B)) == "I 1" - assert pixel(ImageMath.unsafe_eval("equal(Z, Z)", Z=Z)) == "I 1" - assert pixel(ImageMath.unsafe_eval("equal(A, B)", A=A, B=B)) == "I 0" - assert pixel(ImageMath.unsafe_eval("equal(B, A)", A=A, B=B)) == "I 0" - assert pixel(ImageMath.unsafe_eval("equal(A, Z)", A=A, Z=Z)) == "I 0" - - -def test_logical_not_equal() -> None: - assert pixel(ImageMath.unsafe_eval("notequal(A, A)", A=A)) == "I 0" - assert pixel(ImageMath.unsafe_eval("notequal(B, B)", B=B)) == "I 0" - assert pixel(ImageMath.unsafe_eval("notequal(Z, Z)", Z=Z)) == "I 0" - assert pixel(ImageMath.unsafe_eval("notequal(A, B)", A=A, B=B)) == "I 1" - assert pixel(ImageMath.unsafe_eval("notequal(B, A)", A=A, B=B)) == "I 1" - assert pixel(ImageMath.unsafe_eval("notequal(A, Z)", A=A, Z=Z)) == "I 1" +from __future__ import annotations + +import pytest + +from PIL import Image, ImageMath + + +def pixel(im: Image.Image | int) -> str | int: + if isinstance(im, int): + return int(im) # hack to deal with booleans + + return f"{im.mode} {repr(im.getpixel((0, 0)))}" + + +A = Image.new("L", (1, 1), 1) +B = Image.new("L", (1, 1), 2) +Z = Image.new("L", (1, 1), 0) # Z for zero +F = Image.new("F", (1, 1), 3) +I = Image.new("I", (1, 1), 4) # noqa: E741 + +A2 = A.resize((2, 2)) +B2 = B.resize((2, 2)) + +images = {"A": A, "B": B, "F": F, "I": I} + + +def test_sanity() -> None: + assert ImageMath.unsafe_eval("1") == 1 + assert ImageMath.unsafe_eval("1+A", A=2) == 3 + assert pixel(ImageMath.unsafe_eval("A+B", A=A, B=B)) == "I 3" + assert pixel(ImageMath.unsafe_eval("A+B", images)) == "I 3" + assert pixel(ImageMath.unsafe_eval("float(A)+B", images)) == "F 3.0" + assert pixel(ImageMath.unsafe_eval("int(float(A)+B)", images)) == "I 3" + + +def test_eval_deprecated() -> None: + with pytest.warns(DeprecationWarning): + assert ImageMath.eval("1") == 1 + + +def test_ops() -> None: + assert pixel(ImageMath.unsafe_eval("-A", images)) == "I -1" + assert pixel(ImageMath.unsafe_eval("+B", images)) == "L 2" + + assert pixel(ImageMath.unsafe_eval("A+B", images)) == "I 3" + assert pixel(ImageMath.unsafe_eval("A-B", images)) == "I -1" + assert pixel(ImageMath.unsafe_eval("A*B", images)) == "I 2" + assert pixel(ImageMath.unsafe_eval("A/B", images)) == "I 0" + assert pixel(ImageMath.unsafe_eval("B**2", images)) == "I 4" + assert pixel(ImageMath.unsafe_eval("B**33", images)) == "I 2147483647" + + assert pixel(ImageMath.unsafe_eval("float(A)+B", images)) == "F 3.0" + assert pixel(ImageMath.unsafe_eval("float(A)-B", images)) == "F -1.0" + assert pixel(ImageMath.unsafe_eval("float(A)*B", images)) == "F 2.0" + assert pixel(ImageMath.unsafe_eval("float(A)/B", images)) == "F 0.5" + assert pixel(ImageMath.unsafe_eval("float(B)**2", images)) == "F 4.0" + assert pixel(ImageMath.unsafe_eval("float(B)**33", images)) == "F 8589934592.0" + + +@pytest.mark.parametrize( + "expression", + ( + "exec('pass')", + "(lambda: exec('pass'))()", + "(lambda: (lambda: exec('pass'))())()", + ), +) +def test_prevent_exec(expression: str) -> None: + with pytest.raises(ValueError): + ImageMath.unsafe_eval(expression) + + +def test_prevent_double_underscores() -> None: + with pytest.raises(ValueError): + ImageMath.unsafe_eval("1", {"__": None}) + + +def test_prevent_builtins() -> None: + with pytest.raises(ValueError): + ImageMath.unsafe_eval("(lambda: exec('exit()'))()", {"exec": None}) + + +def test_logical() -> None: + assert pixel(ImageMath.unsafe_eval("not A", images)) == 0 + assert pixel(ImageMath.unsafe_eval("A and B", images)) == "L 2" + assert pixel(ImageMath.unsafe_eval("A or B", images)) == "L 1" + + +def test_convert() -> None: + assert pixel(ImageMath.unsafe_eval("convert(A+B, 'L')", images)) == "L 3" + assert pixel(ImageMath.unsafe_eval("convert(A+B, '1')", images)) == "1 0" + assert ( + pixel(ImageMath.unsafe_eval("convert(A+B, 'RGB')", images)) == "RGB (3, 3, 3)" + ) + + +def test_compare() -> None: + assert pixel(ImageMath.unsafe_eval("min(A, B)", images)) == "I 1" + assert pixel(ImageMath.unsafe_eval("max(A, B)", images)) == "I 2" + assert pixel(ImageMath.unsafe_eval("A == 1", images)) == "I 1" + assert pixel(ImageMath.unsafe_eval("A == 2", images)) == "I 0" + + +def test_one_image_larger() -> None: + assert pixel(ImageMath.unsafe_eval("A+B", A=A2, B=B)) == "I 3" + assert pixel(ImageMath.unsafe_eval("A+B", A=A, B=B2)) == "I 3" + + +def test_abs() -> None: + assert pixel(ImageMath.unsafe_eval("abs(A)", A=A)) == "I 1" + assert pixel(ImageMath.unsafe_eval("abs(B)", B=B)) == "I 2" + + +def test_binary_mod() -> None: + assert pixel(ImageMath.unsafe_eval("A%A", A=A)) == "I 0" + assert pixel(ImageMath.unsafe_eval("B%B", B=B)) == "I 0" + assert pixel(ImageMath.unsafe_eval("A%B", A=A, B=B)) == "I 1" + assert pixel(ImageMath.unsafe_eval("B%A", A=A, B=B)) == "I 0" + assert pixel(ImageMath.unsafe_eval("Z%A", A=A, Z=Z)) == "I 0" + assert pixel(ImageMath.unsafe_eval("Z%B", B=B, Z=Z)) == "I 0" + + +def test_bitwise_invert() -> None: + assert pixel(ImageMath.unsafe_eval("~Z", Z=Z)) == "I -1" + assert pixel(ImageMath.unsafe_eval("~A", A=A)) == "I -2" + assert pixel(ImageMath.unsafe_eval("~B", B=B)) == "I -3" + + +def test_bitwise_and() -> None: + assert pixel(ImageMath.unsafe_eval("Z&Z", A=A, Z=Z)) == "I 0" + assert pixel(ImageMath.unsafe_eval("Z&A", A=A, Z=Z)) == "I 0" + assert pixel(ImageMath.unsafe_eval("A&Z", A=A, Z=Z)) == "I 0" + assert pixel(ImageMath.unsafe_eval("A&A", A=A, Z=Z)) == "I 1" + + +def test_bitwise_or() -> None: + assert pixel(ImageMath.unsafe_eval("Z|Z", A=A, Z=Z)) == "I 0" + assert pixel(ImageMath.unsafe_eval("Z|A", A=A, Z=Z)) == "I 1" + assert pixel(ImageMath.unsafe_eval("A|Z", A=A, Z=Z)) == "I 1" + assert pixel(ImageMath.unsafe_eval("A|A", A=A, Z=Z)) == "I 1" + + +def test_bitwise_xor() -> None: + assert pixel(ImageMath.unsafe_eval("Z^Z", A=A, Z=Z)) == "I 0" + assert pixel(ImageMath.unsafe_eval("Z^A", A=A, Z=Z)) == "I 1" + assert pixel(ImageMath.unsafe_eval("A^Z", A=A, Z=Z)) == "I 1" + assert pixel(ImageMath.unsafe_eval("A^A", A=A, Z=Z)) == "I 0" + + +def test_bitwise_leftshift() -> None: + assert pixel(ImageMath.unsafe_eval("Z<<0", Z=Z)) == "I 0" + assert pixel(ImageMath.unsafe_eval("Z<<1", Z=Z)) == "I 0" + assert pixel(ImageMath.unsafe_eval("A<<0", A=A)) == "I 1" + assert pixel(ImageMath.unsafe_eval("A<<1", A=A)) == "I 2" + + +def test_bitwise_rightshift() -> None: + assert pixel(ImageMath.unsafe_eval("Z>>0", Z=Z)) == "I 0" + assert pixel(ImageMath.unsafe_eval("Z>>1", Z=Z)) == "I 0" + assert pixel(ImageMath.unsafe_eval("A>>0", A=A)) == "I 1" + assert pixel(ImageMath.unsafe_eval("A>>1", A=A)) == "I 0" + + +def test_logical_eq() -> None: + assert pixel(ImageMath.unsafe_eval("A==A", A=A)) == "I 1" + assert pixel(ImageMath.unsafe_eval("B==B", B=B)) == "I 1" + assert pixel(ImageMath.unsafe_eval("A==B", A=A, B=B)) == "I 0" + assert pixel(ImageMath.unsafe_eval("B==A", A=A, B=B)) == "I 0" + + +def test_logical_ne() -> None: + assert pixel(ImageMath.unsafe_eval("A!=A", A=A)) == "I 0" + assert pixel(ImageMath.unsafe_eval("B!=B", B=B)) == "I 0" + assert pixel(ImageMath.unsafe_eval("A!=B", A=A, B=B)) == "I 1" + assert pixel(ImageMath.unsafe_eval("B!=A", A=A, B=B)) == "I 1" + + +def test_logical_lt() -> None: + assert pixel(ImageMath.unsafe_eval("A None: + assert pixel(ImageMath.unsafe_eval("A<=A", A=A)) == "I 1" + assert pixel(ImageMath.unsafe_eval("B<=B", B=B)) == "I 1" + assert pixel(ImageMath.unsafe_eval("A<=B", A=A, B=B)) == "I 1" + assert pixel(ImageMath.unsafe_eval("B<=A", A=A, B=B)) == "I 0" + + +def test_logical_gt() -> None: + assert pixel(ImageMath.unsafe_eval("A>A", A=A)) == "I 0" + assert pixel(ImageMath.unsafe_eval("B>B", B=B)) == "I 0" + assert pixel(ImageMath.unsafe_eval("A>B", A=A, B=B)) == "I 0" + assert pixel(ImageMath.unsafe_eval("B>A", A=A, B=B)) == "I 1" + + +def test_logical_ge() -> None: + assert pixel(ImageMath.unsafe_eval("A>=A", A=A)) == "I 1" + assert pixel(ImageMath.unsafe_eval("B>=B", B=B)) == "I 1" + assert pixel(ImageMath.unsafe_eval("A>=B", A=A, B=B)) == "I 0" + assert pixel(ImageMath.unsafe_eval("B>=A", A=A, B=B)) == "I 1" + + +def test_logical_equal() -> None: + assert pixel(ImageMath.unsafe_eval("equal(A, A)", A=A)) == "I 1" + assert pixel(ImageMath.unsafe_eval("equal(B, B)", B=B)) == "I 1" + assert pixel(ImageMath.unsafe_eval("equal(Z, Z)", Z=Z)) == "I 1" + assert pixel(ImageMath.unsafe_eval("equal(A, B)", A=A, B=B)) == "I 0" + assert pixel(ImageMath.unsafe_eval("equal(B, A)", A=A, B=B)) == "I 0" + assert pixel(ImageMath.unsafe_eval("equal(A, Z)", A=A, Z=Z)) == "I 0" + + +def test_logical_not_equal() -> None: + assert pixel(ImageMath.unsafe_eval("notequal(A, A)", A=A)) == "I 0" + assert pixel(ImageMath.unsafe_eval("notequal(B, B)", B=B)) == "I 0" + assert pixel(ImageMath.unsafe_eval("notequal(Z, Z)", Z=Z)) == "I 0" + assert pixel(ImageMath.unsafe_eval("notequal(A, B)", A=A, B=B)) == "I 1" + assert pixel(ImageMath.unsafe_eval("notequal(B, A)", A=A, B=B)) == "I 1" + assert pixel(ImageMath.unsafe_eval("notequal(A, Z)", A=A, Z=Z)) == "I 1" diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py index 32615cf0e..8b632b772 100644 --- a/Tests/test_imagemorph.py +++ b/Tests/test_imagemorph.py @@ -1,335 +1,335 @@ -# Test the ImageMorphology functionality -from __future__ import annotations - -from pathlib import Path - -import pytest - -from PIL import Image, ImageMorph, _imagingmorph - -from .helper import assert_image_equal_tofile, hopper - - -def string_to_img(image_string: str) -> Image.Image: - """Turn a string image representation into a binary image""" - rows = [s for s in image_string.replace(" ", "").split("\n") if len(s)] - height = len(rows) - width = len(rows[0]) - im = Image.new("L", (width, height)) - for i in range(width): - for j in range(height): - c = rows[j][i] - v = c in "X1" - im.putpixel((i, j), v) - - return im - - -A = string_to_img( - """ - ....... - ....... - ..111.. - ..111.. - ..111.. - ....... - ....... - """ -) - - -def img_to_string(im: Image.Image) -> str: - """Turn a (small) binary image into a string representation""" - chars = ".1" - width, height = im.size - return "\n".join( - "".join(chars[im.getpixel((c, r)) > 0] for c in range(width)) - for r in range(height) - ) - - -def img_string_normalize(im: str) -> str: - return img_to_string(string_to_img(im)) - - -def assert_img_equal_img_string(a: Image.Image, b_string: str) -> None: - assert img_to_string(a) == img_string_normalize(b_string) - - -def test_str_to_img() -> None: - assert_image_equal_tofile(A, "Tests/images/morph_a.png") - - -@pytest.mark.parametrize( - "op", ("corner", "dilation4", "dilation8", "erosion4", "erosion8", "edge") -) -def test_lut(op: str) -> None: - lb = ImageMorph.LutBuilder(op_name=op) - assert lb.get_lut() is None - - lut = lb.build_lut() - with open(f"Tests/images/{op}.lut", "rb") as f: - assert lut == bytearray(f.read()) - - -def test_no_operator_loaded() -> None: - im = Image.new("L", (1, 1)) - mop = ImageMorph.MorphOp() - with pytest.raises(Exception) as e: - mop.apply(im) - assert str(e.value) == "No operator loaded" - with pytest.raises(Exception) as e: - mop.match(im) - assert str(e.value) == "No operator loaded" - with pytest.raises(Exception) as e: - mop.save_lut("") - assert str(e.value) == "No operator loaded" - - -# Test the named patterns -def test_erosion8() -> None: - # erosion8 - mop = ImageMorph.MorphOp(op_name="erosion8") - count, Aout = mop.apply(A) - assert count == 8 - assert_img_equal_img_string( - Aout, - """ - ....... - ....... - ....... - ...1... - ....... - ....... - ....... - """, - ) - - -def test_dialation8() -> None: - # dialation8 - mop = ImageMorph.MorphOp(op_name="dilation8") - count, Aout = mop.apply(A) - assert count == 16 - assert_img_equal_img_string( - Aout, - """ - ....... - .11111. - .11111. - .11111. - .11111. - .11111. - ....... - """, - ) - - -def test_erosion4() -> None: - # erosion4 - mop = ImageMorph.MorphOp(op_name="dilation4") - count, Aout = mop.apply(A) - assert count == 12 - assert_img_equal_img_string( - Aout, - """ - ....... - ..111.. - .11111. - .11111. - .11111. - ..111.. - ....... - """, - ) - - -def test_edge() -> None: - # edge - mop = ImageMorph.MorphOp(op_name="edge") - count, Aout = mop.apply(A) - assert count == 1 - assert_img_equal_img_string( - Aout, - """ - ....... - ....... - ..111.. - ..1.1.. - ..111.. - ....... - ....... - """, - ) - - -def test_corner() -> None: - # Create a corner detector pattern - mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "4:(00. 01. ...)->1"]) - count, Aout = mop.apply(A) - assert count == 5 - assert_img_equal_img_string( - Aout, - """ - ....... - ....... - ..1.1.. - ....... - ..1.1.. - ....... - ....... - """, - ) - - # Test the coordinate counting with the same operator - coords = mop.match(A) - assert len(coords) == 4 - assert tuple(coords) == ((2, 2), (4, 2), (2, 4), (4, 4)) - - coords = mop.get_on_pixels(Aout) - assert len(coords) == 4 - assert tuple(coords) == ((2, 2), (4, 2), (2, 4), (4, 4)) - - -def test_mirroring() -> None: - # Test 'M' for mirroring - mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "M:(00. 01. ...)->1"]) - count, Aout = mop.apply(A) - assert count == 7 - assert_img_equal_img_string( - Aout, - """ - ....... - ....... - ..1.1.. - ....... - ....... - ....... - ....... - """, - ) - - -def test_negate() -> None: - # Test 'N' for negate - mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "N:(00. 01. ...)->1"]) - count, Aout = mop.apply(A) - assert count == 8 - assert_img_equal_img_string( - Aout, - """ - ....... - ....... - ..1.... - ....... - ....... - ....... - ....... - """, - ) - - -def test_incorrect_mode() -> None: - im = hopper("RGB") - mop = ImageMorph.MorphOp(op_name="erosion8") - - with pytest.raises(ValueError) as e: - mop.apply(im) - assert str(e.value) == "Image mode must be L" - with pytest.raises(ValueError) as e: - mop.match(im) - assert str(e.value) == "Image mode must be L" - with pytest.raises(ValueError) as e: - mop.get_on_pixels(im) - assert str(e.value) == "Image mode must be L" - - -def test_add_patterns() -> None: - # Arrange - lb = ImageMorph.LutBuilder(op_name="corner") - assert lb.patterns == ["1:(... ... ...)->0", "4:(00. 01. ...)->1"] - new_patterns = ["M:(00. 01. ...)->1", "N:(00. 01. ...)->1"] - - # Act - lb.add_patterns(new_patterns) - - # Assert - assert lb.patterns == [ - "1:(... ... ...)->0", - "4:(00. 01. ...)->1", - "M:(00. 01. ...)->1", - "N:(00. 01. ...)->1", - ] - - -def test_unknown_pattern() -> None: - with pytest.raises(Exception): - ImageMorph.LutBuilder(op_name="unknown") - - -def test_pattern_syntax_error() -> None: - # Arrange - lb = ImageMorph.LutBuilder(op_name="corner") - new_patterns = ["a pattern with a syntax error"] - lb.add_patterns(new_patterns) - - # Act / Assert - with pytest.raises(Exception) as e: - lb.build_lut() - assert str(e.value) == 'Syntax error in pattern "a pattern with a syntax error"' - - -def test_load_invalid_mrl() -> None: - # Arrange - invalid_mrl = "Tests/images/hopper.png" - mop = ImageMorph.MorphOp() - - # Act / Assert - with pytest.raises(Exception) as e: - mop.load_lut(invalid_mrl) - assert str(e.value) == "Wrong size operator file!" - - -def test_roundtrip_mrl(tmp_path: Path) -> None: - # Arrange - tempfile = str(tmp_path / "temp.mrl") - mop = ImageMorph.MorphOp(op_name="corner") - initial_lut = mop.lut - - # Act - mop.save_lut(tempfile) - mop.load_lut(tempfile) - - # Act / Assert - assert mop.lut == initial_lut - - -def test_set_lut() -> None: - # Arrange - lb = ImageMorph.LutBuilder(op_name="corner") - lut = lb.build_lut() - mop = ImageMorph.MorphOp() - - # Act - mop.set_lut(lut) - - # Assert - assert mop.lut == lut - - -def test_wrong_mode() -> None: - lut = ImageMorph.LutBuilder(op_name="corner").build_lut() - imrgb = Image.new("RGB", (10, 10)) - iml = Image.new("L", (10, 10)) - - with pytest.raises(RuntimeError): - _imagingmorph.apply(bytes(lut), imrgb.im.id, iml.im.id) - - with pytest.raises(RuntimeError): - _imagingmorph.apply(bytes(lut), iml.im.id, imrgb.im.id) - - with pytest.raises(RuntimeError): - _imagingmorph.match(bytes(lut), imrgb.im.id) - - # Should not raise - _imagingmorph.match(bytes(lut), iml.im.id) +# Test the ImageMorphology functionality +from __future__ import annotations + +from pathlib import Path + +import pytest + +from PIL import Image, ImageMorph, _imagingmorph + +from .helper import assert_image_equal_tofile, hopper + + +def string_to_img(image_string: str) -> Image.Image: + """Turn a string image representation into a binary image""" + rows = [s for s in image_string.replace(" ", "").split("\n") if len(s)] + height = len(rows) + width = len(rows[0]) + im = Image.new("L", (width, height)) + for i in range(width): + for j in range(height): + c = rows[j][i] + v = c in "X1" + im.putpixel((i, j), v) + + return im + + +A = string_to_img( + """ + ....... + ....... + ..111.. + ..111.. + ..111.. + ....... + ....... + """ +) + + +def img_to_string(im: Image.Image) -> str: + """Turn a (small) binary image into a string representation""" + chars = ".1" + width, height = im.size + return "\n".join( + "".join(chars[im.getpixel((c, r)) > 0] for c in range(width)) + for r in range(height) + ) + + +def img_string_normalize(im: str) -> str: + return img_to_string(string_to_img(im)) + + +def assert_img_equal_img_string(a: Image.Image, b_string: str) -> None: + assert img_to_string(a) == img_string_normalize(b_string) + + +def test_str_to_img() -> None: + assert_image_equal_tofile(A, "Tests/images/morph_a.png") + + +@pytest.mark.parametrize( + "op", ("corner", "dilation4", "dilation8", "erosion4", "erosion8", "edge") +) +def test_lut(op: str) -> None: + lb = ImageMorph.LutBuilder(op_name=op) + assert lb.get_lut() is None + + lut = lb.build_lut() + with open(f"Tests/images/{op}.lut", "rb") as f: + assert lut == bytearray(f.read()) + + +def test_no_operator_loaded() -> None: + im = Image.new("L", (1, 1)) + mop = ImageMorph.MorphOp() + with pytest.raises(Exception) as e: + mop.apply(im) + assert str(e.value) == "No operator loaded" + with pytest.raises(Exception) as e: + mop.match(im) + assert str(e.value) == "No operator loaded" + with pytest.raises(Exception) as e: + mop.save_lut("") + assert str(e.value) == "No operator loaded" + + +# Test the named patterns +def test_erosion8() -> None: + # erosion8 + mop = ImageMorph.MorphOp(op_name="erosion8") + count, Aout = mop.apply(A) + assert count == 8 + assert_img_equal_img_string( + Aout, + """ + ....... + ....... + ....... + ...1... + ....... + ....... + ....... + """, + ) + + +def test_dialation8() -> None: + # dialation8 + mop = ImageMorph.MorphOp(op_name="dilation8") + count, Aout = mop.apply(A) + assert count == 16 + assert_img_equal_img_string( + Aout, + """ + ....... + .11111. + .11111. + .11111. + .11111. + .11111. + ....... + """, + ) + + +def test_erosion4() -> None: + # erosion4 + mop = ImageMorph.MorphOp(op_name="dilation4") + count, Aout = mop.apply(A) + assert count == 12 + assert_img_equal_img_string( + Aout, + """ + ....... + ..111.. + .11111. + .11111. + .11111. + ..111.. + ....... + """, + ) + + +def test_edge() -> None: + # edge + mop = ImageMorph.MorphOp(op_name="edge") + count, Aout = mop.apply(A) + assert count == 1 + assert_img_equal_img_string( + Aout, + """ + ....... + ....... + ..111.. + ..1.1.. + ..111.. + ....... + ....... + """, + ) + + +def test_corner() -> None: + # Create a corner detector pattern + mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "4:(00. 01. ...)->1"]) + count, Aout = mop.apply(A) + assert count == 5 + assert_img_equal_img_string( + Aout, + """ + ....... + ....... + ..1.1.. + ....... + ..1.1.. + ....... + ....... + """, + ) + + # Test the coordinate counting with the same operator + coords = mop.match(A) + assert len(coords) == 4 + assert tuple(coords) == ((2, 2), (4, 2), (2, 4), (4, 4)) + + coords = mop.get_on_pixels(Aout) + assert len(coords) == 4 + assert tuple(coords) == ((2, 2), (4, 2), (2, 4), (4, 4)) + + +def test_mirroring() -> None: + # Test 'M' for mirroring + mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "M:(00. 01. ...)->1"]) + count, Aout = mop.apply(A) + assert count == 7 + assert_img_equal_img_string( + Aout, + """ + ....... + ....... + ..1.1.. + ....... + ....... + ....... + ....... + """, + ) + + +def test_negate() -> None: + # Test 'N' for negate + mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "N:(00. 01. ...)->1"]) + count, Aout = mop.apply(A) + assert count == 8 + assert_img_equal_img_string( + Aout, + """ + ....... + ....... + ..1.... + ....... + ....... + ....... + ....... + """, + ) + + +def test_incorrect_mode() -> None: + im = hopper("RGB") + mop = ImageMorph.MorphOp(op_name="erosion8") + + with pytest.raises(ValueError) as e: + mop.apply(im) + assert str(e.value) == "Image mode must be L" + with pytest.raises(ValueError) as e: + mop.match(im) + assert str(e.value) == "Image mode must be L" + with pytest.raises(ValueError) as e: + mop.get_on_pixels(im) + assert str(e.value) == "Image mode must be L" + + +def test_add_patterns() -> None: + # Arrange + lb = ImageMorph.LutBuilder(op_name="corner") + assert lb.patterns == ["1:(... ... ...)->0", "4:(00. 01. ...)->1"] + new_patterns = ["M:(00. 01. ...)->1", "N:(00. 01. ...)->1"] + + # Act + lb.add_patterns(new_patterns) + + # Assert + assert lb.patterns == [ + "1:(... ... ...)->0", + "4:(00. 01. ...)->1", + "M:(00. 01. ...)->1", + "N:(00. 01. ...)->1", + ] + + +def test_unknown_pattern() -> None: + with pytest.raises(Exception): + ImageMorph.LutBuilder(op_name="unknown") + + +def test_pattern_syntax_error() -> None: + # Arrange + lb = ImageMorph.LutBuilder(op_name="corner") + new_patterns = ["a pattern with a syntax error"] + lb.add_patterns(new_patterns) + + # Act / Assert + with pytest.raises(Exception) as e: + lb.build_lut() + assert str(e.value) == 'Syntax error in pattern "a pattern with a syntax error"' + + +def test_load_invalid_mrl() -> None: + # Arrange + invalid_mrl = "Tests/images/hopper.png" + mop = ImageMorph.MorphOp() + + # Act / Assert + with pytest.raises(Exception) as e: + mop.load_lut(invalid_mrl) + assert str(e.value) == "Wrong size operator file!" + + +def test_roundtrip_mrl(tmp_path: Path) -> None: + # Arrange + tempfile = str(tmp_path / "temp.mrl") + mop = ImageMorph.MorphOp(op_name="corner") + initial_lut = mop.lut + + # Act + mop.save_lut(tempfile) + mop.load_lut(tempfile) + + # Act / Assert + assert mop.lut == initial_lut + + +def test_set_lut() -> None: + # Arrange + lb = ImageMorph.LutBuilder(op_name="corner") + lut = lb.build_lut() + mop = ImageMorph.MorphOp() + + # Act + mop.set_lut(lut) + + # Assert + assert mop.lut == lut + + +def test_wrong_mode() -> None: + lut = ImageMorph.LutBuilder(op_name="corner").build_lut() + imrgb = Image.new("RGB", (10, 10)) + iml = Image.new("L", (10, 10)) + + with pytest.raises(RuntimeError): + _imagingmorph.apply(bytes(lut), imrgb.im.id, iml.im.id) + + with pytest.raises(RuntimeError): + _imagingmorph.apply(bytes(lut), iml.im.id, imrgb.im.id) + + with pytest.raises(RuntimeError): + _imagingmorph.match(bytes(lut), imrgb.im.id) + + # Should not raise + _imagingmorph.match(bytes(lut), iml.im.id) diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index d6bdaf450..d2876b8a2 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -1,556 +1,556 @@ -from __future__ import annotations - -import pytest - -from PIL import Image, ImageDraw, ImageOps, ImageStat, features - -from .helper import ( - assert_image_equal, - assert_image_similar, - assert_image_similar_tofile, - assert_tuple_approx_equal, - hopper, -) - - -class Deformer(ImageOps.SupportsGetMesh): - def getmesh( - self, im: Image.Image - ) -> list[ - tuple[tuple[int, int, int, int], tuple[int, int, int, int, int, int, int, int]] - ]: - x, y = im.size - return [((0, 0, x, y), (0, 0, x, 0, x, y, y, 0))] - - -deformer = Deformer() - - -def test_sanity() -> None: - ImageOps.autocontrast(hopper("L")) - ImageOps.autocontrast(hopper("RGB")) - - ImageOps.autocontrast(hopper("L"), cutoff=10) - ImageOps.autocontrast(hopper("L"), cutoff=(2, 10)) - ImageOps.autocontrast(hopper("L"), ignore=[0, 255]) - ImageOps.autocontrast(hopper("L"), mask=hopper("L")) - ImageOps.autocontrast(hopper("L"), preserve_tone=True) - - ImageOps.colorize(hopper("L"), (0, 0, 0), (255, 255, 255)) - ImageOps.colorize(hopper("L"), "black", "white") - - ImageOps.pad(hopper("L"), (128, 128)) - ImageOps.pad(hopper("RGB"), (128, 128)) - - ImageOps.contain(hopper("L"), (128, 128)) - ImageOps.contain(hopper("RGB"), (128, 128)) - - ImageOps.cover(hopper("L"), (128, 128)) - ImageOps.cover(hopper("RGB"), (128, 128)) - - ImageOps.crop(hopper("L"), 1) - ImageOps.crop(hopper("RGB"), 1) - - ImageOps.deform(hopper("L"), deformer) - ImageOps.deform(hopper("RGB"), deformer) - - ImageOps.equalize(hopper("L")) - ImageOps.equalize(hopper("RGB")) - - ImageOps.expand(hopper("L"), 1) - ImageOps.expand(hopper("RGB"), 1) - ImageOps.expand(hopper("L"), 2, "blue") - ImageOps.expand(hopper("RGB"), 2, "blue") - - ImageOps.fit(hopper("L"), (128, 128)) - ImageOps.fit(hopper("RGB"), (128, 128)) - - ImageOps.flip(hopper("L")) - ImageOps.flip(hopper("RGB")) - - ImageOps.grayscale(hopper("L")) - ImageOps.grayscale(hopper("RGB")) - - ImageOps.invert(hopper("1")) - ImageOps.invert(hopper("L")) - ImageOps.invert(hopper("RGB")) - - ImageOps.mirror(hopper("L")) - ImageOps.mirror(hopper("RGB")) - - ImageOps.posterize(hopper("L"), 4) - ImageOps.posterize(hopper("RGB"), 4) - - ImageOps.solarize(hopper("L")) - ImageOps.solarize(hopper("RGB")) - - ImageOps.exif_transpose(hopper("L")) - ImageOps.exif_transpose(hopper("RGB")) - - -def test_1pxfit() -> None: - # Division by zero in equalize if image is 1 pixel high - newimg = ImageOps.fit(hopper("RGB").resize((1, 1)), (35, 35)) - assert newimg.size == (35, 35) - - newimg = ImageOps.fit(hopper("RGB").resize((1, 100)), (35, 35)) - assert newimg.size == (35, 35) - - newimg = ImageOps.fit(hopper("RGB").resize((100, 1)), (35, 35)) - assert newimg.size == (35, 35) - - -def test_fit_same_ratio() -> None: - # The ratio for this image is 1000.0 / 755 = 1.3245033112582782 - # If the ratios are not acknowledged to be the same, - # and Pillow attempts to adjust the width to - # 1.3245033112582782 * 755 = 1000.0000000000001 - # then centering this greater width causes a negative x offset when cropping - with Image.new("RGB", (1000, 755)) as im: - new_im = ImageOps.fit(im, (1000, 755)) - assert new_im.size == (1000, 755) - - -@pytest.mark.parametrize("new_size", ((256, 256), (512, 256), (256, 512))) -def test_contain(new_size: tuple[int, int]) -> None: - im = hopper() - new_im = ImageOps.contain(im, new_size) - assert new_im.size == (256, 256) - - -def test_contain_round() -> None: - im = Image.new("1", (43, 63), 1) - new_im = ImageOps.contain(im, (5, 7)) - assert new_im.width == 5 - - im = Image.new("1", (63, 43), 1) - new_im = ImageOps.contain(im, (7, 5)) - assert new_im.height == 5 - - -@pytest.mark.parametrize( - "image_name, expected_size", - ( - ("colr_bungee.png", (1024, 256)), # landscape - ("imagedraw_stroke_multiline.png", (256, 640)), # portrait - ("hopper.png", (256, 256)), # square - ), -) -def test_cover(image_name: str, expected_size: tuple[int, int]) -> None: - with Image.open("Tests/images/" + image_name) as im: - new_im = ImageOps.cover(im, (256, 256)) - assert new_im.size == expected_size - - -def test_pad() -> None: - # Same ratio - im = hopper() - new_size = (im.width * 2, im.height * 2) - new_im = ImageOps.pad(im, new_size) - assert new_im.size == new_size - - for label, color, new_size in [ - ("h", None, (im.width * 4, im.height * 2)), - ("v", "#f00", (im.width * 2, im.height * 4)), - ]: - for i, centering in enumerate([(0, 0), (0.5, 0.5), (1, 1)]): - new_im = ImageOps.pad(im, new_size, color=color, centering=centering) - assert new_im.size == new_size - - assert_image_similar_tofile( - new_im, "Tests/images/imageops_pad_" + label + "_" + str(i) + ".jpg", 6 - ) - - -def test_pad_round() -> None: - im = Image.new("1", (1, 1), 1) - new_im = ImageOps.pad(im, (4, 1)) - assert new_im.load()[2, 0] == 1 - - new_im = ImageOps.pad(im, (1, 4)) - assert new_im.load()[0, 2] == 1 - - -@pytest.mark.parametrize("mode", ("P", "PA")) -def test_palette(mode: str) -> None: - im = hopper(mode) - - # Expand - expanded_im = ImageOps.expand(im) - assert_image_equal(im.convert("RGB"), expanded_im.convert("RGB")) - - # Pad - padded_im = ImageOps.pad(im, (256, 128), centering=(0, 0)) - assert_image_equal( - im.convert("RGB"), padded_im.convert("RGB").crop((0, 0, 128, 128)) - ) - - -def test_pil163() -> None: - # Division by zero in equalize if < 255 pixels in image (@PIL163) - - i = hopper("RGB").resize((15, 16)) - - ImageOps.equalize(i.convert("L")) - ImageOps.equalize(i.convert("P")) - ImageOps.equalize(i.convert("RGB")) - - -def test_scale() -> None: - # Test the scaling function - i = hopper("L").resize((50, 50)) - - with pytest.raises(ValueError): - ImageOps.scale(i, -1) - - newimg = ImageOps.scale(i, 1) - assert newimg.size == (50, 50) - - newimg = ImageOps.scale(i, 2) - assert newimg.size == (100, 100) - - newimg = ImageOps.scale(i, 0.5) - assert newimg.size == (25, 25) - - -@pytest.mark.parametrize("border", (10, (1, 2, 3, 4))) -def test_expand_palette(border: int | tuple[int, int, int, int]) -> None: - with Image.open("Tests/images/p_16.tga") as im: - im_expanded = ImageOps.expand(im, border, (255, 0, 0)) - - if isinstance(border, int): - left = top = right = bottom = border - else: - left, top, right, bottom = border - px = im_expanded.convert("RGB").load() - for x in range(im_expanded.width): - for b in range(top): - assert px[x, b] == (255, 0, 0) - for b in range(bottom): - assert px[x, im_expanded.height - 1 - b] == (255, 0, 0) - for y in range(im_expanded.height): - for b in range(left): - assert px[b, y] == (255, 0, 0) - for b in range(right): - assert px[im_expanded.width - 1 - b, y] == (255, 0, 0) - - im_cropped = im_expanded.crop( - (left, top, im_expanded.width - right, im_expanded.height - bottom) - ) - assert_image_equal(im_cropped, im) - - -def test_colorize_2color() -> None: - # Test the colorizing function with 2-color functionality - - # Open test image (256px by 10px, black to white) - with Image.open("Tests/images/bw_gradient.png") as im: - im = im.convert("L") - - # Create image with original 2-color functionality - im_test = ImageOps.colorize(im, "red", "green") - - # Test output image (2-color) - left = (0, 1) - middle = (127, 1) - right = (255, 1) - assert_tuple_approx_equal( - im_test.getpixel(left), - (255, 0, 0), - threshold=1, - msg="black test pixel incorrect", - ) - assert_tuple_approx_equal( - im_test.getpixel(middle), - (127, 63, 0), - threshold=1, - msg="mid test pixel incorrect", - ) - assert_tuple_approx_equal( - im_test.getpixel(right), - (0, 127, 0), - threshold=1, - msg="white test pixel incorrect", - ) - - -def test_colorize_2color_offset() -> None: - # Test the colorizing function with 2-color functionality and offset - - # Open test image (256px by 10px, black to white) - with Image.open("Tests/images/bw_gradient.png") as im: - im = im.convert("L") - - # Create image with original 2-color functionality with offsets - im_test = ImageOps.colorize( - im, black="red", white="green", blackpoint=50, whitepoint=100 - ) - - # Test output image (2-color) with offsets - left = (25, 1) - middle = (75, 1) - right = (125, 1) - assert_tuple_approx_equal( - im_test.getpixel(left), - (255, 0, 0), - threshold=1, - msg="black test pixel incorrect", - ) - assert_tuple_approx_equal( - im_test.getpixel(middle), - (127, 63, 0), - threshold=1, - msg="mid test pixel incorrect", - ) - assert_tuple_approx_equal( - im_test.getpixel(right), - (0, 127, 0), - threshold=1, - msg="white test pixel incorrect", - ) - - -def test_colorize_3color_offset() -> None: - # Test the colorizing function with 3-color functionality and offset - - # Open test image (256px by 10px, black to white) - with Image.open("Tests/images/bw_gradient.png") as im: - im = im.convert("L") - - # Create image with new three color functionality with offsets - im_test = ImageOps.colorize( - im, - black="red", - white="green", - mid="blue", - blackpoint=50, - whitepoint=200, - midpoint=100, - ) - - # Test output image (3-color) with offsets - left = (25, 1) - left_middle = (75, 1) - middle = (100, 1) - right_middle = (150, 1) - right = (225, 1) - assert_tuple_approx_equal( - im_test.getpixel(left), - (255, 0, 0), - threshold=1, - msg="black test pixel incorrect", - ) - assert_tuple_approx_equal( - im_test.getpixel(left_middle), - (127, 0, 127), - threshold=1, - msg="low-mid test pixel incorrect", - ) - assert_tuple_approx_equal( - im_test.getpixel(middle), (0, 0, 255), threshold=1, msg="mid incorrect" - ) - assert_tuple_approx_equal( - im_test.getpixel(right_middle), - (0, 63, 127), - threshold=1, - msg="high-mid test pixel incorrect", - ) - assert_tuple_approx_equal( - im_test.getpixel(right), - (0, 127, 0), - threshold=1, - msg="white test pixel incorrect", - ) - - -def test_exif_transpose() -> None: - exts = [".jpg"] - if features.check("webp") and features.check("webp_anim"): - exts.append(".webp") - for ext in exts: - with Image.open("Tests/images/hopper" + ext) as base_im: - - def check(orientation_im: Image.Image) -> None: - for im in [ - orientation_im, - orientation_im.copy(), - ]: # ImageFile # Image - if orientation_im is base_im: - assert "exif" not in im.info - else: - original_exif = im.info["exif"] - transposed_im = ImageOps.exif_transpose(im) - assert 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 - else: - assert transposed_im.info["exif"] != original_exif - - assert 0x0112 in im.getexif() - assert 0x0112 not in transposed_im.getexif() - - # Repeat the operation to test that it does not keep transposing - transposed_im2 = ImageOps.exif_transpose(transposed_im) - assert transposed_im2 is not None - assert_image_equal(transposed_im2, transposed_im) - - check(base_im) - for i in range(2, 9): - with Image.open( - "Tests/images/hopper_orientation_" + str(i) + ext - ) as orientation_im: - check(orientation_im) - - # Orientation from "XML:com.adobe.xmp" info key - for suffix in ("", "_exiftool"): - with Image.open("Tests/images/xmp_tags_orientation" + suffix + ".png") as im: - assert im.getexif()[0x0112] == 3 - - transposed_im = ImageOps.exif_transpose(im) - assert transposed_im is not None - assert 0x0112 not in transposed_im.getexif() - - transposed_im._reload_exif() - assert 0x0112 not in transposed_im.getexif() - - # Orientation from "Raw profile type exif" info key - # This test image has been manually hexedited from exif_imagemagick.png - # to have a different orientation - with Image.open("Tests/images/exif_imagemagick_orientation.png") as im: - assert im.getexif()[0x0112] == 3 - - transposed_im = ImageOps.exif_transpose(im) - assert 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_in_place() -> None: - with Image.open("Tests/images/orientation_rectangle.jpg") as im: - assert im.size == (2, 1) - assert im.getexif()[0x0112] == 8 - expected = im.rotate(90, expand=True) - - ImageOps.exif_transpose(im, in_place=True) - assert im.size == (1, 2) - assert 0x0112 not in im.getexif() - assert_image_equal(im, expected) - - -def test_autocontrast_unsupported_mode() -> None: - im = Image.new("RGBA", (1, 1)) - with pytest.raises(OSError): - ImageOps.autocontrast(im) - - -def test_autocontrast_cutoff() -> None: - # Test the cutoff argument of autocontrast - with Image.open("Tests/images/bw_gradient.png") as img: - - def autocontrast(cutoff: int | tuple[int, int]): - return ImageOps.autocontrast(img, cutoff).histogram() - - assert autocontrast(10) == autocontrast((10, 10)) - assert autocontrast(10) != autocontrast((1, 10)) - - -def test_autocontrast_mask_toy_input() -> None: - # Test the mask argument of autocontrast - with Image.open("Tests/images/bw_gradient.png") as img: - rect_mask = Image.new("L", img.size, 0) - draw = ImageDraw.Draw(rect_mask) - x0 = img.size[0] // 4 - y0 = img.size[1] // 4 - x1 = 3 * img.size[0] // 4 - y1 = 3 * img.size[1] // 4 - draw.rectangle((x0, y0, x1, y1), fill=255) - - result = ImageOps.autocontrast(img, mask=rect_mask) - result_nomask = ImageOps.autocontrast(img) - - assert result != result_nomask - assert ImageStat.Stat(result, mask=rect_mask).median == [127] - assert ImageStat.Stat(result_nomask).median == [128] - - -def test_autocontrast_mask_real_input() -> None: - # Test the autocontrast with a rectangular mask - with Image.open("Tests/images/iptc.jpg") as img: - rect_mask = Image.new("L", img.size, 0) - draw = ImageDraw.Draw(rect_mask) - x0, y0 = img.size[0] // 2, img.size[1] // 2 - x1, y1 = img.size[0] - 40, img.size[1] - draw.rectangle((x0, y0, x1, y1), fill=255) - - result = ImageOps.autocontrast(img, mask=rect_mask) - result_nomask = ImageOps.autocontrast(img) - - assert result_nomask != result - assert_tuple_approx_equal( - ImageStat.Stat(result, mask=rect_mask).median, - (195, 202, 184), - threshold=2, - msg="autocontrast with mask pixel incorrect", - ) - assert_tuple_approx_equal( - ImageStat.Stat(result_nomask).median, - (119, 106, 79), - threshold=2, - msg="autocontrast without mask pixel incorrect", - ) - - -def test_autocontrast_preserve_tone() -> None: - def autocontrast(mode: str, preserve_tone: bool) -> list[int]: - im = hopper(mode) - return ImageOps.autocontrast(im, preserve_tone=preserve_tone).histogram() - - assert autocontrast("RGB", True) != autocontrast("RGB", False) - assert autocontrast("L", True) == autocontrast("L", False) - - -def test_autocontrast_preserve_gradient() -> None: - gradient = Image.linear_gradient("L") - - # test with a grayscale gradient that extends to 0,255. - # Should be a noop. - out = ImageOps.autocontrast(gradient, cutoff=0, preserve_tone=True) - - assert_image_equal(gradient, out) - - # cutoff the top and bottom - # autocontrast should make the first and last histogram entries equal - # and, with rounding, should be 10% of the image pixels - out = ImageOps.autocontrast(gradient, cutoff=10, preserve_tone=True) - hist = out.histogram() - assert hist[0] == hist[-1] - assert hist[-1] == 256 * round(256 * 0.10) - - # in rgb - img = gradient.convert("RGB") - out = ImageOps.autocontrast(img, cutoff=0, preserve_tone=True) - assert_image_equal(img, out) - - -@pytest.mark.parametrize( - "color", ((255, 255, 255), (127, 255, 0), (127, 127, 127), (0, 0, 0)) -) -def test_autocontrast_preserve_one_color(color: tuple[int, int, int]) -> None: - img = Image.new("RGB", (10, 10), color) - - # single color images shouldn't change - out = ImageOps.autocontrast(img, cutoff=0, preserve_tone=True) - assert_image_equal(img, out) # single color, no cutoff - - # even if there is a cutoff - out = ImageOps.autocontrast( - img, cutoff=10, preserve_tone=True - ) # single color 10 cutoff - assert_image_equal(img, out) +from __future__ import annotations + +import pytest + +from PIL import Image, ImageDraw, ImageOps, ImageStat, features + +from .helper import ( + assert_image_equal, + assert_image_similar, + assert_image_similar_tofile, + assert_tuple_approx_equal, + hopper, +) + + +class Deformer(ImageOps.SupportsGetMesh): + def getmesh( + self, im: Image.Image + ) -> list[ + tuple[tuple[int, int, int, int], tuple[int, int, int, int, int, int, int, int]] + ]: + x, y = im.size + return [((0, 0, x, y), (0, 0, x, 0, x, y, y, 0))] + + +deformer = Deformer() + + +def test_sanity() -> None: + ImageOps.autocontrast(hopper("L")) + ImageOps.autocontrast(hopper("RGB")) + + ImageOps.autocontrast(hopper("L"), cutoff=10) + ImageOps.autocontrast(hopper("L"), cutoff=(2, 10)) + ImageOps.autocontrast(hopper("L"), ignore=[0, 255]) + ImageOps.autocontrast(hopper("L"), mask=hopper("L")) + ImageOps.autocontrast(hopper("L"), preserve_tone=True) + + ImageOps.colorize(hopper("L"), (0, 0, 0), (255, 255, 255)) + ImageOps.colorize(hopper("L"), "black", "white") + + ImageOps.pad(hopper("L"), (128, 128)) + ImageOps.pad(hopper("RGB"), (128, 128)) + + ImageOps.contain(hopper("L"), (128, 128)) + ImageOps.contain(hopper("RGB"), (128, 128)) + + ImageOps.cover(hopper("L"), (128, 128)) + ImageOps.cover(hopper("RGB"), (128, 128)) + + ImageOps.crop(hopper("L"), 1) + ImageOps.crop(hopper("RGB"), 1) + + ImageOps.deform(hopper("L"), deformer) + ImageOps.deform(hopper("RGB"), deformer) + + ImageOps.equalize(hopper("L")) + ImageOps.equalize(hopper("RGB")) + + ImageOps.expand(hopper("L"), 1) + ImageOps.expand(hopper("RGB"), 1) + ImageOps.expand(hopper("L"), 2, "blue") + ImageOps.expand(hopper("RGB"), 2, "blue") + + ImageOps.fit(hopper("L"), (128, 128)) + ImageOps.fit(hopper("RGB"), (128, 128)) + + ImageOps.flip(hopper("L")) + ImageOps.flip(hopper("RGB")) + + ImageOps.grayscale(hopper("L")) + ImageOps.grayscale(hopper("RGB")) + + ImageOps.invert(hopper("1")) + ImageOps.invert(hopper("L")) + ImageOps.invert(hopper("RGB")) + + ImageOps.mirror(hopper("L")) + ImageOps.mirror(hopper("RGB")) + + ImageOps.posterize(hopper("L"), 4) + ImageOps.posterize(hopper("RGB"), 4) + + ImageOps.solarize(hopper("L")) + ImageOps.solarize(hopper("RGB")) + + ImageOps.exif_transpose(hopper("L")) + ImageOps.exif_transpose(hopper("RGB")) + + +def test_1pxfit() -> None: + # Division by zero in equalize if image is 1 pixel high + newimg = ImageOps.fit(hopper("RGB").resize((1, 1)), (35, 35)) + assert newimg.size == (35, 35) + + newimg = ImageOps.fit(hopper("RGB").resize((1, 100)), (35, 35)) + assert newimg.size == (35, 35) + + newimg = ImageOps.fit(hopper("RGB").resize((100, 1)), (35, 35)) + assert newimg.size == (35, 35) + + +def test_fit_same_ratio() -> None: + # The ratio for this image is 1000.0 / 755 = 1.3245033112582782 + # If the ratios are not acknowledged to be the same, + # and Pillow attempts to adjust the width to + # 1.3245033112582782 * 755 = 1000.0000000000001 + # then centering this greater width causes a negative x offset when cropping + with Image.new("RGB", (1000, 755)) as im: + new_im = ImageOps.fit(im, (1000, 755)) + assert new_im.size == (1000, 755) + + +@pytest.mark.parametrize("new_size", ((256, 256), (512, 256), (256, 512))) +def test_contain(new_size: tuple[int, int]) -> None: + im = hopper() + new_im = ImageOps.contain(im, new_size) + assert new_im.size == (256, 256) + + +def test_contain_round() -> None: + im = Image.new("1", (43, 63), 1) + new_im = ImageOps.contain(im, (5, 7)) + assert new_im.width == 5 + + im = Image.new("1", (63, 43), 1) + new_im = ImageOps.contain(im, (7, 5)) + assert new_im.height == 5 + + +@pytest.mark.parametrize( + "image_name, expected_size", + ( + ("colr_bungee.png", (1024, 256)), # landscape + ("imagedraw_stroke_multiline.png", (256, 640)), # portrait + ("hopper.png", (256, 256)), # square + ), +) +def test_cover(image_name: str, expected_size: tuple[int, int]) -> None: + with Image.open("Tests/images/" + image_name) as im: + new_im = ImageOps.cover(im, (256, 256)) + assert new_im.size == expected_size + + +def test_pad() -> None: + # Same ratio + im = hopper() + new_size = (im.width * 2, im.height * 2) + new_im = ImageOps.pad(im, new_size) + assert new_im.size == new_size + + for label, color, new_size in [ + ("h", None, (im.width * 4, im.height * 2)), + ("v", "#f00", (im.width * 2, im.height * 4)), + ]: + for i, centering in enumerate([(0, 0), (0.5, 0.5), (1, 1)]): + new_im = ImageOps.pad(im, new_size, color=color, centering=centering) + assert new_im.size == new_size + + assert_image_similar_tofile( + new_im, "Tests/images/imageops_pad_" + label + "_" + str(i) + ".jpg", 6 + ) + + +def test_pad_round() -> None: + im = Image.new("1", (1, 1), 1) + new_im = ImageOps.pad(im, (4, 1)) + assert new_im.load()[2, 0] == 1 + + new_im = ImageOps.pad(im, (1, 4)) + assert new_im.load()[0, 2] == 1 + + +@pytest.mark.parametrize("mode", ("P", "PA")) +def test_palette(mode: str) -> None: + im = hopper(mode) + + # Expand + expanded_im = ImageOps.expand(im) + assert_image_equal(im.convert("RGB"), expanded_im.convert("RGB")) + + # Pad + padded_im = ImageOps.pad(im, (256, 128), centering=(0, 0)) + assert_image_equal( + im.convert("RGB"), padded_im.convert("RGB").crop((0, 0, 128, 128)) + ) + + +def test_pil163() -> None: + # Division by zero in equalize if < 255 pixels in image (@PIL163) + + i = hopper("RGB").resize((15, 16)) + + ImageOps.equalize(i.convert("L")) + ImageOps.equalize(i.convert("P")) + ImageOps.equalize(i.convert("RGB")) + + +def test_scale() -> None: + # Test the scaling function + i = hopper("L").resize((50, 50)) + + with pytest.raises(ValueError): + ImageOps.scale(i, -1) + + newimg = ImageOps.scale(i, 1) + assert newimg.size == (50, 50) + + newimg = ImageOps.scale(i, 2) + assert newimg.size == (100, 100) + + newimg = ImageOps.scale(i, 0.5) + assert newimg.size == (25, 25) + + +@pytest.mark.parametrize("border", (10, (1, 2, 3, 4))) +def test_expand_palette(border: int | tuple[int, int, int, int]) -> None: + with Image.open("Tests/images/p_16.tga") as im: + im_expanded = ImageOps.expand(im, border, (255, 0, 0)) + + if isinstance(border, int): + left = top = right = bottom = border + else: + left, top, right, bottom = border + px = im_expanded.convert("RGB").load() + for x in range(im_expanded.width): + for b in range(top): + assert px[x, b] == (255, 0, 0) + for b in range(bottom): + assert px[x, im_expanded.height - 1 - b] == (255, 0, 0) + for y in range(im_expanded.height): + for b in range(left): + assert px[b, y] == (255, 0, 0) + for b in range(right): + assert px[im_expanded.width - 1 - b, y] == (255, 0, 0) + + im_cropped = im_expanded.crop( + (left, top, im_expanded.width - right, im_expanded.height - bottom) + ) + assert_image_equal(im_cropped, im) + + +def test_colorize_2color() -> None: + # Test the colorizing function with 2-color functionality + + # Open test image (256px by 10px, black to white) + with Image.open("Tests/images/bw_gradient.png") as im: + im = im.convert("L") + + # Create image with original 2-color functionality + im_test = ImageOps.colorize(im, "red", "green") + + # Test output image (2-color) + left = (0, 1) + middle = (127, 1) + right = (255, 1) + assert_tuple_approx_equal( + im_test.getpixel(left), + (255, 0, 0), + threshold=1, + msg="black test pixel incorrect", + ) + assert_tuple_approx_equal( + im_test.getpixel(middle), + (127, 63, 0), + threshold=1, + msg="mid test pixel incorrect", + ) + assert_tuple_approx_equal( + im_test.getpixel(right), + (0, 127, 0), + threshold=1, + msg="white test pixel incorrect", + ) + + +def test_colorize_2color_offset() -> None: + # Test the colorizing function with 2-color functionality and offset + + # Open test image (256px by 10px, black to white) + with Image.open("Tests/images/bw_gradient.png") as im: + im = im.convert("L") + + # Create image with original 2-color functionality with offsets + im_test = ImageOps.colorize( + im, black="red", white="green", blackpoint=50, whitepoint=100 + ) + + # Test output image (2-color) with offsets + left = (25, 1) + middle = (75, 1) + right = (125, 1) + assert_tuple_approx_equal( + im_test.getpixel(left), + (255, 0, 0), + threshold=1, + msg="black test pixel incorrect", + ) + assert_tuple_approx_equal( + im_test.getpixel(middle), + (127, 63, 0), + threshold=1, + msg="mid test pixel incorrect", + ) + assert_tuple_approx_equal( + im_test.getpixel(right), + (0, 127, 0), + threshold=1, + msg="white test pixel incorrect", + ) + + +def test_colorize_3color_offset() -> None: + # Test the colorizing function with 3-color functionality and offset + + # Open test image (256px by 10px, black to white) + with Image.open("Tests/images/bw_gradient.png") as im: + im = im.convert("L") + + # Create image with new three color functionality with offsets + im_test = ImageOps.colorize( + im, + black="red", + white="green", + mid="blue", + blackpoint=50, + whitepoint=200, + midpoint=100, + ) + + # Test output image (3-color) with offsets + left = (25, 1) + left_middle = (75, 1) + middle = (100, 1) + right_middle = (150, 1) + right = (225, 1) + assert_tuple_approx_equal( + im_test.getpixel(left), + (255, 0, 0), + threshold=1, + msg="black test pixel incorrect", + ) + assert_tuple_approx_equal( + im_test.getpixel(left_middle), + (127, 0, 127), + threshold=1, + msg="low-mid test pixel incorrect", + ) + assert_tuple_approx_equal( + im_test.getpixel(middle), (0, 0, 255), threshold=1, msg="mid incorrect" + ) + assert_tuple_approx_equal( + im_test.getpixel(right_middle), + (0, 63, 127), + threshold=1, + msg="high-mid test pixel incorrect", + ) + assert_tuple_approx_equal( + im_test.getpixel(right), + (0, 127, 0), + threshold=1, + msg="white test pixel incorrect", + ) + + +def test_exif_transpose() -> None: + exts = [".jpg"] + if features.check("webp") and features.check("webp_anim"): + exts.append(".webp") + for ext in exts: + with Image.open("Tests/images/hopper" + ext) as base_im: + + def check(orientation_im: Image.Image) -> None: + for im in [ + orientation_im, + orientation_im.copy(), + ]: # ImageFile # Image + if orientation_im is base_im: + assert "exif" not in im.info + else: + original_exif = im.info["exif"] + transposed_im = ImageOps.exif_transpose(im) + assert 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 + else: + assert transposed_im.info["exif"] != original_exif + + assert 0x0112 in im.getexif() + assert 0x0112 not in transposed_im.getexif() + + # Repeat the operation to test that it does not keep transposing + transposed_im2 = ImageOps.exif_transpose(transposed_im) + assert transposed_im2 is not None + assert_image_equal(transposed_im2, transposed_im) + + check(base_im) + for i in range(2, 9): + with Image.open( + "Tests/images/hopper_orientation_" + str(i) + ext + ) as orientation_im: + check(orientation_im) + + # Orientation from "XML:com.adobe.xmp" info key + for suffix in ("", "_exiftool"): + with Image.open("Tests/images/xmp_tags_orientation" + suffix + ".png") as im: + assert im.getexif()[0x0112] == 3 + + transposed_im = ImageOps.exif_transpose(im) + assert transposed_im is not None + assert 0x0112 not in transposed_im.getexif() + + transposed_im._reload_exif() + assert 0x0112 not in transposed_im.getexif() + + # Orientation from "Raw profile type exif" info key + # This test image has been manually hexedited from exif_imagemagick.png + # to have a different orientation + with Image.open("Tests/images/exif_imagemagick_orientation.png") as im: + assert im.getexif()[0x0112] == 3 + + transposed_im = ImageOps.exif_transpose(im) + assert 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_in_place() -> None: + with Image.open("Tests/images/orientation_rectangle.jpg") as im: + assert im.size == (2, 1) + assert im.getexif()[0x0112] == 8 + expected = im.rotate(90, expand=True) + + ImageOps.exif_transpose(im, in_place=True) + assert im.size == (1, 2) + assert 0x0112 not in im.getexif() + assert_image_equal(im, expected) + + +def test_autocontrast_unsupported_mode() -> None: + im = Image.new("RGBA", (1, 1)) + with pytest.raises(OSError): + ImageOps.autocontrast(im) + + +def test_autocontrast_cutoff() -> None: + # Test the cutoff argument of autocontrast + with Image.open("Tests/images/bw_gradient.png") as img: + + def autocontrast(cutoff: int | tuple[int, int]): + return ImageOps.autocontrast(img, cutoff).histogram() + + assert autocontrast(10) == autocontrast((10, 10)) + assert autocontrast(10) != autocontrast((1, 10)) + + +def test_autocontrast_mask_toy_input() -> None: + # Test the mask argument of autocontrast + with Image.open("Tests/images/bw_gradient.png") as img: + rect_mask = Image.new("L", img.size, 0) + draw = ImageDraw.Draw(rect_mask) + x0 = img.size[0] // 4 + y0 = img.size[1] // 4 + x1 = 3 * img.size[0] // 4 + y1 = 3 * img.size[1] // 4 + draw.rectangle((x0, y0, x1, y1), fill=255) + + result = ImageOps.autocontrast(img, mask=rect_mask) + result_nomask = ImageOps.autocontrast(img) + + assert result != result_nomask + assert ImageStat.Stat(result, mask=rect_mask).median == [127] + assert ImageStat.Stat(result_nomask).median == [128] + + +def test_autocontrast_mask_real_input() -> None: + # Test the autocontrast with a rectangular mask + with Image.open("Tests/images/iptc.jpg") as img: + rect_mask = Image.new("L", img.size, 0) + draw = ImageDraw.Draw(rect_mask) + x0, y0 = img.size[0] // 2, img.size[1] // 2 + x1, y1 = img.size[0] - 40, img.size[1] + draw.rectangle((x0, y0, x1, y1), fill=255) + + result = ImageOps.autocontrast(img, mask=rect_mask) + result_nomask = ImageOps.autocontrast(img) + + assert result_nomask != result + assert_tuple_approx_equal( + ImageStat.Stat(result, mask=rect_mask).median, + (195, 202, 184), + threshold=2, + msg="autocontrast with mask pixel incorrect", + ) + assert_tuple_approx_equal( + ImageStat.Stat(result_nomask).median, + (119, 106, 79), + threshold=2, + msg="autocontrast without mask pixel incorrect", + ) + + +def test_autocontrast_preserve_tone() -> None: + def autocontrast(mode: str, preserve_tone: bool) -> list[int]: + im = hopper(mode) + return ImageOps.autocontrast(im, preserve_tone=preserve_tone).histogram() + + assert autocontrast("RGB", True) != autocontrast("RGB", False) + assert autocontrast("L", True) == autocontrast("L", False) + + +def test_autocontrast_preserve_gradient() -> None: + gradient = Image.linear_gradient("L") + + # test with a grayscale gradient that extends to 0,255. + # Should be a noop. + out = ImageOps.autocontrast(gradient, cutoff=0, preserve_tone=True) + + assert_image_equal(gradient, out) + + # cutoff the top and bottom + # autocontrast should make the first and last histogram entries equal + # and, with rounding, should be 10% of the image pixels + out = ImageOps.autocontrast(gradient, cutoff=10, preserve_tone=True) + hist = out.histogram() + assert hist[0] == hist[-1] + assert hist[-1] == 256 * round(256 * 0.10) + + # in rgb + img = gradient.convert("RGB") + out = ImageOps.autocontrast(img, cutoff=0, preserve_tone=True) + assert_image_equal(img, out) + + +@pytest.mark.parametrize( + "color", ((255, 255, 255), (127, 255, 0), (127, 127, 127), (0, 0, 0)) +) +def test_autocontrast_preserve_one_color(color: tuple[int, int, int]) -> None: + img = Image.new("RGB", (10, 10), color) + + # single color images shouldn't change + out = ImageOps.autocontrast(img, cutoff=0, preserve_tone=True) + assert_image_equal(img, out) # single color, no cutoff + + # even if there is a cutoff + out = ImageOps.autocontrast( + img, cutoff=10, preserve_tone=True + ) # single color 10 cutoff + assert_image_equal(img, out) diff --git a/Tests/test_imageops_usm.py b/Tests/test_imageops_usm.py index dbdd5b317..26a056c07 100644 --- a/Tests/test_imageops_usm.py +++ b/Tests/test_imageops_usm.py @@ -1,114 +1,114 @@ -from __future__ import annotations - -from typing import Generator - -import pytest - -from PIL import Image, ImageFile, ImageFilter - - -@pytest.fixture -def test_images() -> Generator[dict[str, ImageFile.ImageFile], None, None]: - ims = { - "im": Image.open("Tests/images/hopper.ppm"), - "snakes": Image.open("Tests/images/color_snakes.png"), - } - try: - yield ims - finally: - for im in ims.values(): - im.close() - - -def test_filter_api(test_images: dict[str, ImageFile.ImageFile]) -> None: - im = test_images["im"] - - test_filter = ImageFilter.GaussianBlur(2.0) - i = im.filter(test_filter) - assert i.mode == "RGB" - assert i.size == (128, 128) - - test_filter2 = ImageFilter.UnsharpMask(2.0, 125, 8) - i = im.filter(test_filter2) - assert i.mode == "RGB" - assert i.size == (128, 128) - - -def test_usm_formats(test_images: dict[str, ImageFile.ImageFile]) -> None: - im = test_images["im"] - - usm = ImageFilter.UnsharpMask - with pytest.raises(ValueError): - im.convert("1").filter(usm) - im.convert("L").filter(usm) - with pytest.raises(ValueError): - im.convert("I").filter(usm) - with pytest.raises(ValueError): - im.convert("F").filter(usm) - im.convert("RGB").filter(usm) - im.convert("RGBA").filter(usm) - im.convert("CMYK").filter(usm) - with pytest.raises(ValueError): - im.convert("YCbCr").filter(usm) - - -def test_blur_formats(test_images: dict[str, ImageFile.ImageFile]) -> None: - im = test_images["im"] - - blur = ImageFilter.GaussianBlur - with pytest.raises(ValueError): - im.convert("1").filter(blur) - with pytest.raises(ValueError): - im.convert("I").filter(blur) - with pytest.raises(ValueError): - im.convert("F").filter(blur) - im.convert("RGB").filter(blur) - im.convert("RGBA").filter(blur) - im.convert("CMYK").filter(blur) - with pytest.raises(ValueError): - im.convert("YCbCr").filter(blur) - - -def test_usm_accuracy(test_images: dict[str, ImageFile.ImageFile]) -> None: - snakes = test_images["snakes"] - - src = snakes.convert("RGB") - i = src.filter(ImageFilter.UnsharpMask(5, 1024, 0)) - # Image should not be changed because it have only 0 and 255 levels. - assert i.tobytes() == src.tobytes() - - -def test_blur_accuracy(test_images: dict[str, ImageFile.ImageFile]) -> None: - snakes = test_images["snakes"] - - i = snakes.filter(ImageFilter.GaussianBlur(0.4)) - # These pixels surrounded with pixels with 255 intensity. - # They must be very close to 255. - for x, y, c in [ - (1, 0, 1), - (2, 0, 1), - (7, 8, 1), - (8, 8, 1), - (2, 9, 1), - (7, 3, 0), - (8, 3, 0), - (5, 8, 0), - (5, 9, 0), - (1, 3, 0), - (4, 3, 2), - (4, 2, 2), - ]: - assert i.im.getpixel((x, y))[c] >= 250 - # Fuzzy match. - - def gp(x, y): - return i.im.getpixel((x, y)) - - assert 236 <= gp(7, 4)[0] <= 239 - assert 236 <= gp(7, 5)[2] <= 239 - assert 236 <= gp(7, 6)[2] <= 239 - assert 236 <= gp(7, 7)[1] <= 239 - assert 236 <= gp(8, 4)[0] <= 239 - assert 236 <= gp(8, 5)[2] <= 239 - assert 236 <= gp(8, 6)[2] <= 239 - assert 236 <= gp(8, 7)[1] <= 239 +from __future__ import annotations + +from typing import Generator + +import pytest + +from PIL import Image, ImageFile, ImageFilter + + +@pytest.fixture +def test_images() -> Generator[dict[str, ImageFile.ImageFile], None, None]: + ims = { + "im": Image.open("Tests/images/hopper.ppm"), + "snakes": Image.open("Tests/images/color_snakes.png"), + } + try: + yield ims + finally: + for im in ims.values(): + im.close() + + +def test_filter_api(test_images: dict[str, ImageFile.ImageFile]) -> None: + im = test_images["im"] + + test_filter = ImageFilter.GaussianBlur(2.0) + i = im.filter(test_filter) + assert i.mode == "RGB" + assert i.size == (128, 128) + + test_filter2 = ImageFilter.UnsharpMask(2.0, 125, 8) + i = im.filter(test_filter2) + assert i.mode == "RGB" + assert i.size == (128, 128) + + +def test_usm_formats(test_images: dict[str, ImageFile.ImageFile]) -> None: + im = test_images["im"] + + usm = ImageFilter.UnsharpMask + with pytest.raises(ValueError): + im.convert("1").filter(usm) + im.convert("L").filter(usm) + with pytest.raises(ValueError): + im.convert("I").filter(usm) + with pytest.raises(ValueError): + im.convert("F").filter(usm) + im.convert("RGB").filter(usm) + im.convert("RGBA").filter(usm) + im.convert("CMYK").filter(usm) + with pytest.raises(ValueError): + im.convert("YCbCr").filter(usm) + + +def test_blur_formats(test_images: dict[str, ImageFile.ImageFile]) -> None: + im = test_images["im"] + + blur = ImageFilter.GaussianBlur + with pytest.raises(ValueError): + im.convert("1").filter(blur) + with pytest.raises(ValueError): + im.convert("I").filter(blur) + with pytest.raises(ValueError): + im.convert("F").filter(blur) + im.convert("RGB").filter(blur) + im.convert("RGBA").filter(blur) + im.convert("CMYK").filter(blur) + with pytest.raises(ValueError): + im.convert("YCbCr").filter(blur) + + +def test_usm_accuracy(test_images: dict[str, ImageFile.ImageFile]) -> None: + snakes = test_images["snakes"] + + src = snakes.convert("RGB") + i = src.filter(ImageFilter.UnsharpMask(5, 1024, 0)) + # Image should not be changed because it have only 0 and 255 levels. + assert i.tobytes() == src.tobytes() + + +def test_blur_accuracy(test_images: dict[str, ImageFile.ImageFile]) -> None: + snakes = test_images["snakes"] + + i = snakes.filter(ImageFilter.GaussianBlur(0.4)) + # These pixels surrounded with pixels with 255 intensity. + # They must be very close to 255. + for x, y, c in [ + (1, 0, 1), + (2, 0, 1), + (7, 8, 1), + (8, 8, 1), + (2, 9, 1), + (7, 3, 0), + (8, 3, 0), + (5, 8, 0), + (5, 9, 0), + (1, 3, 0), + (4, 3, 2), + (4, 2, 2), + ]: + assert i.im.getpixel((x, y))[c] >= 250 + # Fuzzy match. + + def gp(x, y): + return i.im.getpixel((x, y)) + + assert 236 <= gp(7, 4)[0] <= 239 + assert 236 <= gp(7, 5)[2] <= 239 + assert 236 <= gp(7, 6)[2] <= 239 + assert 236 <= gp(7, 7)[1] <= 239 + assert 236 <= gp(8, 4)[0] <= 239 + assert 236 <= gp(8, 5)[2] <= 239 + assert 236 <= gp(8, 6)[2] <= 239 + assert 236 <= gp(8, 7)[1] <= 239 diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py index 8e2db15aa..d991f5574 100644 --- a/Tests/test_imagepalette.py +++ b/Tests/test_imagepalette.py @@ -1,200 +1,200 @@ -from __future__ import annotations - -from pathlib import Path - -import pytest - -from PIL import Image, ImagePalette - -from .helper import assert_image_equal, assert_image_equal_tofile - - -def test_sanity() -> None: - palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) - assert len(palette.colors) == 256 - - -def test_reload() -> None: - with Image.open("Tests/images/hopper.gif") as im: - original = im.copy() - im.palette.dirty = 1 - assert_image_equal(im.convert("RGB"), original.convert("RGB")) - - -def test_getcolor() -> None: - palette = ImagePalette.ImagePalette() - assert len(palette.palette) == 0 - assert len(palette.colors) == 0 - - test_map = {} - for i in range(256): - test_map[palette.getcolor((i, i, i))] = i - assert len(test_map) == 256 - - # Colors can be converted between RGB and RGBA - rgba_palette = ImagePalette.ImagePalette("RGBA") - assert rgba_palette.getcolor((0, 0, 0)) == rgba_palette.getcolor((0, 0, 0, 255)) - - assert palette.getcolor((0, 0, 0)) == palette.getcolor((0, 0, 0, 255)) - - # An error is raised when the palette is full - with pytest.raises(ValueError): - palette.getcolor((1, 2, 3)) - # But not if the image is not using one of the palette entries - palette.getcolor((1, 2, 3), image=Image.new("P", (1, 1))) - - # Test unknown color specifier - with pytest.raises(ValueError): - palette.getcolor("unknown") - - -def test_getcolor_rgba_color_rgb_palette() -> None: - palette = ImagePalette.ImagePalette("RGB") - - # Opaque RGBA colors are converted - assert palette.getcolor((0, 0, 0, 255)) == palette.getcolor((0, 0, 0)) - - with pytest.raises(ValueError): - palette.getcolor((0, 0, 0, 128)) - - -@pytest.mark.parametrize( - "index, palette", - [ - # Test when the palette is not full - (0, ImagePalette.ImagePalette()), - # Test when the palette is full - (255, ImagePalette.ImagePalette("RGB", list(range(256)) * 3)), - ], -) -def test_getcolor_not_special(index: int, palette: ImagePalette.ImagePalette) -> None: - im = Image.new("P", (1, 1)) - - # Do not use transparency index as a new color - im.info["transparency"] = index - index1 = palette.getcolor((0, 0, 0), im) - assert index1 != index - - # Do not use background index as a new color - im.info["background"] = index1 - index2 = palette.getcolor((0, 0, 1), im) - assert index2 not in (index, index1) - - -def test_file(tmp_path: Path) -> None: - palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) - - f = str(tmp_path / "temp.lut") - - palette.save(f) - - p = ImagePalette.load(f) - - # load returns raw palette information - assert len(p[0]) == 768 - assert p[1] == "RGB" - - p = ImagePalette.raw(p[1], p[0]) - assert isinstance(p, ImagePalette.ImagePalette) - assert p.palette == palette.tobytes() - - -def test_make_linear_lut() -> None: - # Arrange - black = 0 - white = 255 - - # Act - lut = ImagePalette.make_linear_lut(black, white) - - # Assert - assert isinstance(lut, list) - assert len(lut) == 256 - # Check values - for i in range(0, len(lut)): - assert lut[i] == i - - -def test_make_linear_lut_not_yet_implemented() -> None: - # Update after FIXME - # Arrange - black = 1 - white = 255 - - # Act - with pytest.raises(NotImplementedError): - ImagePalette.make_linear_lut(black, white) - - -def test_make_gamma_lut() -> None: - # Arrange - exp = 5 - - # Act - lut = ImagePalette.make_gamma_lut(exp) - - # Assert - assert isinstance(lut, list) - assert len(lut) == 256 - # Check a few values - assert lut[0] == 0 - assert lut[63] == 0 - assert lut[127] == 8 - assert lut[191] == 60 - assert lut[255] == 255 - - -def test_rawmode_valueerrors(tmp_path: Path) -> None: - # Arrange - palette = ImagePalette.raw("RGB", list(range(256)) * 3) - - # Act / Assert - with pytest.raises(ValueError): - palette.tobytes() - with pytest.raises(ValueError): - palette.getcolor((1, 2, 3)) - f = str(tmp_path / "temp.lut") - with pytest.raises(ValueError): - palette.save(f) - - -def test_getdata() -> None: - # Arrange - data_in = list(range(256)) * 3 - palette = ImagePalette.ImagePalette("RGB", data_in) - - # Act - mode, data_out = palette.getdata() - - # Assert - assert mode == "RGB" - - -def test_rawmode_getdata() -> None: - # Arrange - data_in = list(range(256)) * 3 - palette = ImagePalette.raw("RGB", data_in) - - # Act - rawmode, data_out = palette.getdata() - - # Assert - assert rawmode == "RGB" - assert data_in == data_out - - -def test_2bit_palette(tmp_path: Path) -> None: - # issue #2258, 2 bit palettes are corrupted. - outfile = str(tmp_path / "temp.png") - - rgb = b"\x00" * 2 + b"\x01" * 2 + b"\x02" * 2 - img = Image.frombytes("P", (6, 1), rgb) - img.putpalette(b"\xFF\x00\x00\x00\xFF\x00\x00\x00\xFF") # RGB - img.save(outfile, format="PNG") - - assert_image_equal_tofile(img, outfile) - - -def test_invalid_palette() -> None: - with pytest.raises(OSError): - ImagePalette.load("Tests/images/hopper.jpg") +from __future__ import annotations + +from pathlib import Path + +import pytest + +from PIL import Image, ImagePalette + +from .helper import assert_image_equal, assert_image_equal_tofile + + +def test_sanity() -> None: + palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) + assert len(palette.colors) == 256 + + +def test_reload() -> None: + with Image.open("Tests/images/hopper.gif") as im: + original = im.copy() + im.palette.dirty = 1 + assert_image_equal(im.convert("RGB"), original.convert("RGB")) + + +def test_getcolor() -> None: + palette = ImagePalette.ImagePalette() + assert len(palette.palette) == 0 + assert len(palette.colors) == 0 + + test_map = {} + for i in range(256): + test_map[palette.getcolor((i, i, i))] = i + assert len(test_map) == 256 + + # Colors can be converted between RGB and RGBA + rgba_palette = ImagePalette.ImagePalette("RGBA") + assert rgba_palette.getcolor((0, 0, 0)) == rgba_palette.getcolor((0, 0, 0, 255)) + + assert palette.getcolor((0, 0, 0)) == palette.getcolor((0, 0, 0, 255)) + + # An error is raised when the palette is full + with pytest.raises(ValueError): + palette.getcolor((1, 2, 3)) + # But not if the image is not using one of the palette entries + palette.getcolor((1, 2, 3), image=Image.new("P", (1, 1))) + + # Test unknown color specifier + with pytest.raises(ValueError): + palette.getcolor("unknown") + + +def test_getcolor_rgba_color_rgb_palette() -> None: + palette = ImagePalette.ImagePalette("RGB") + + # Opaque RGBA colors are converted + assert palette.getcolor((0, 0, 0, 255)) == palette.getcolor((0, 0, 0)) + + with pytest.raises(ValueError): + palette.getcolor((0, 0, 0, 128)) + + +@pytest.mark.parametrize( + "index, palette", + [ + # Test when the palette is not full + (0, ImagePalette.ImagePalette()), + # Test when the palette is full + (255, ImagePalette.ImagePalette("RGB", list(range(256)) * 3)), + ], +) +def test_getcolor_not_special(index: int, palette: ImagePalette.ImagePalette) -> None: + im = Image.new("P", (1, 1)) + + # Do not use transparency index as a new color + im.info["transparency"] = index + index1 = palette.getcolor((0, 0, 0), im) + assert index1 != index + + # Do not use background index as a new color + im.info["background"] = index1 + index2 = palette.getcolor((0, 0, 1), im) + assert index2 not in (index, index1) + + +def test_file(tmp_path: Path) -> None: + palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) + + f = str(tmp_path / "temp.lut") + + palette.save(f) + + p = ImagePalette.load(f) + + # load returns raw palette information + assert len(p[0]) == 768 + assert p[1] == "RGB" + + p = ImagePalette.raw(p[1], p[0]) + assert isinstance(p, ImagePalette.ImagePalette) + assert p.palette == palette.tobytes() + + +def test_make_linear_lut() -> None: + # Arrange + black = 0 + white = 255 + + # Act + lut = ImagePalette.make_linear_lut(black, white) + + # Assert + assert isinstance(lut, list) + assert len(lut) == 256 + # Check values + for i in range(0, len(lut)): + assert lut[i] == i + + +def test_make_linear_lut_not_yet_implemented() -> None: + # Update after FIXME + # Arrange + black = 1 + white = 255 + + # Act + with pytest.raises(NotImplementedError): + ImagePalette.make_linear_lut(black, white) + + +def test_make_gamma_lut() -> None: + # Arrange + exp = 5 + + # Act + lut = ImagePalette.make_gamma_lut(exp) + + # Assert + assert isinstance(lut, list) + assert len(lut) == 256 + # Check a few values + assert lut[0] == 0 + assert lut[63] == 0 + assert lut[127] == 8 + assert lut[191] == 60 + assert lut[255] == 255 + + +def test_rawmode_valueerrors(tmp_path: Path) -> None: + # Arrange + palette = ImagePalette.raw("RGB", list(range(256)) * 3) + + # Act / Assert + with pytest.raises(ValueError): + palette.tobytes() + with pytest.raises(ValueError): + palette.getcolor((1, 2, 3)) + f = str(tmp_path / "temp.lut") + with pytest.raises(ValueError): + palette.save(f) + + +def test_getdata() -> None: + # Arrange + data_in = list(range(256)) * 3 + palette = ImagePalette.ImagePalette("RGB", data_in) + + # Act + mode, data_out = palette.getdata() + + # Assert + assert mode == "RGB" + + +def test_rawmode_getdata() -> None: + # Arrange + data_in = list(range(256)) * 3 + palette = ImagePalette.raw("RGB", data_in) + + # Act + rawmode, data_out = palette.getdata() + + # Assert + assert rawmode == "RGB" + assert data_in == data_out + + +def test_2bit_palette(tmp_path: Path) -> None: + # issue #2258, 2 bit palettes are corrupted. + outfile = str(tmp_path / "temp.png") + + rgb = b"\x00" * 2 + b"\x01" * 2 + b"\x02" * 2 + img = Image.frombytes("P", (6, 1), rgb) + img.putpalette(b"\xFF\x00\x00\x00\xFF\x00\x00\x00\xFF") # RGB + img.save(outfile, format="PNG") + + assert_image_equal_tofile(img, outfile) + + +def test_invalid_palette() -> None: + with pytest.raises(OSError): + ImagePalette.load("Tests/images/hopper.jpg") diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index 9487560af..ce8369049 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -1,216 +1,216 @@ -from __future__ import annotations - -import array -import math -import struct -from typing import Sequence - -import pytest - -from PIL import Image, ImagePath - - -def test_path() -> None: - p = ImagePath.Path(list(range(10))) - - # sequence interface - assert len(p) == 5 - assert p[0] == (0.0, 1.0) - assert p[-1] == (8.0, 9.0) - assert list(p[:1]) == [(0.0, 1.0)] - with pytest.raises(TypeError) as cm: - p["foo"] - assert str(cm.value) == "Path indices must be integers, not str" - assert list(p) == [(0.0, 1.0), (2.0, 3.0), (4.0, 5.0), (6.0, 7.0), (8.0, 9.0)] - - # method sanity check - assert p.tolist() == [ - (0.0, 1.0), - (2.0, 3.0), - (4.0, 5.0), - (6.0, 7.0), - (8.0, 9.0), - ] - assert p.tolist(True) == [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0] - - assert p.getbbox() == (0.0, 1.0, 8.0, 9.0) - - assert p.compact(5) == 2 - assert list(p) == [(0.0, 1.0), (4.0, 5.0), (8.0, 9.0)] - - p.transform((1, 0, 1, 0, 1, 1)) - assert list(p) == [(1.0, 2.0), (5.0, 6.0), (9.0, 10.0)] - - -@pytest.mark.parametrize( - "coords", - ( - (0, 1), - [0, 1], - (0.0, 1.0), - [0.0, 1.0], - ((0, 1),), - [(0, 1)], - ((0.0, 1.0),), - [(0.0, 1.0)], - array.array("f", [0, 1]), - array.array("f", [0, 1]).tobytes(), - ImagePath.Path((0, 1)), - ), -) -def test_path_constructors( - coords: Sequence[float] | array.array[float] | ImagePath.Path, -) -> None: - # Arrange / Act - p = ImagePath.Path(coords) - - # Assert - 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" - - -@pytest.mark.parametrize( - "coords", - ( - (0,), - [0], - (0, 1, 2), - [0, 1, 2], - ), -) -def test_path_odd_number_of_coordinates(coords: Sequence[int]) -> None: - # Act - with pytest.raises(ValueError) as e: - ImagePath.Path(coords) - - # Assert - assert str(e.value) == "wrong number of coordinates" - - -@pytest.mark.parametrize( - "coords, expected", - [ - ([0, 1, 2, 3], (0.0, 1.0, 2.0, 3.0)), - ([3, 2, 1, 0], (1.0, 0.0, 3.0, 2.0)), - (0, (0.0, 0.0, 0.0, 0.0)), - (1, (0.0, 0.0, 0.0, 0.0)), - ], -) -def test_getbbox( - coords: int | list[int], expected: tuple[float, float, float, float] -) -> None: - # Arrange - p = ImagePath.Path(coords) - - # Act / Assert - assert p.getbbox() == expected - - -def test_getbbox_no_args() -> None: - # Arrange - p = ImagePath.Path([0, 1, 2, 3]) - - # Act / Assert - with pytest.raises(TypeError): - p.getbbox(1) - - -@pytest.mark.parametrize( - "coords, expected", - [ - (0, []), - (list(range(6)), [(0.0, 3.0), (4.0, 9.0), (8.0, 15.0)]), - ], -) -def test_map(coords: int | list[int], expected: list[tuple[float, float]]) -> None: - # Arrange - p = ImagePath.Path(coords) - - # Act - # Modifies the path in-place - p.map(lambda x, y: (x * 2, y * 3)) - - # Assert - assert list(p) == expected - - -def test_transform() -> None: - # Arrange - p = ImagePath.Path([0, 1, 2, 3]) - theta = math.pi / 15 - - # Act - # Affine transform, in-place - p.transform( - (math.cos(theta), math.sin(theta), 20, -math.sin(theta), math.cos(theta), 20), - ) - - # Assert - assert p.tolist() == [ - (20.20791169081776, 20.978147600733806), - (22.58003027392089, 22.518619420565898), - ] - - -def test_transform_with_wrap() -> None: - # Arrange - p = ImagePath.Path([0, 1, 2, 3]) - theta = math.pi / 15 - - # Act - # Affine transform, in-place, with wrap parameter - p.transform( - (math.cos(theta), math.sin(theta), 20, -math.sin(theta), math.cos(theta), 20), - 1.0, - ) - - # Assert - assert p.tolist() == [ - (0.20791169081775962, 20.978147600733806), - (0.5800302739208902, 22.518619420565898), - ] - - -def test_overflow_segfault() -> None: - # Some Pythons fail getting the argument as an integer, and it falls - # through to the sequence. Seeing this on 32-bit Windows. - with pytest.raises((TypeError, MemoryError)): - # post patch, this fails with a memory error - x = Evil() - - # This fails due to the invalid malloc above, - # and segfaults - for i in range(200000): - x[i] = b"0" * 16 - - -class Evil: - def __init__(self) -> None: - self.corrupt = Image.core.path(0x4000000000000000) - - def __getitem__(self, i: int) -> bytes: - x = self.corrupt[i] - return struct.pack("dd", x[0], x[1]) - - def __setitem__(self, i: int, x: bytes) -> None: - self.corrupt[i] = struct.unpack("dd", x) +from __future__ import annotations + +import array +import math +import struct +from typing import Sequence + +import pytest + +from PIL import Image, ImagePath + + +def test_path() -> None: + p = ImagePath.Path(list(range(10))) + + # sequence interface + assert len(p) == 5 + assert p[0] == (0.0, 1.0) + assert p[-1] == (8.0, 9.0) + assert list(p[:1]) == [(0.0, 1.0)] + with pytest.raises(TypeError) as cm: + p["foo"] + assert str(cm.value) == "Path indices must be integers, not str" + assert list(p) == [(0.0, 1.0), (2.0, 3.0), (4.0, 5.0), (6.0, 7.0), (8.0, 9.0)] + + # method sanity check + assert p.tolist() == [ + (0.0, 1.0), + (2.0, 3.0), + (4.0, 5.0), + (6.0, 7.0), + (8.0, 9.0), + ] + assert p.tolist(True) == [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0] + + assert p.getbbox() == (0.0, 1.0, 8.0, 9.0) + + assert p.compact(5) == 2 + assert list(p) == [(0.0, 1.0), (4.0, 5.0), (8.0, 9.0)] + + p.transform((1, 0, 1, 0, 1, 1)) + assert list(p) == [(1.0, 2.0), (5.0, 6.0), (9.0, 10.0)] + + +@pytest.mark.parametrize( + "coords", + ( + (0, 1), + [0, 1], + (0.0, 1.0), + [0.0, 1.0], + ((0, 1),), + [(0, 1)], + ((0.0, 1.0),), + [(0.0, 1.0)], + array.array("f", [0, 1]), + array.array("f", [0, 1]).tobytes(), + ImagePath.Path((0, 1)), + ), +) +def test_path_constructors( + coords: Sequence[float] | array.array[float] | ImagePath.Path, +) -> None: + # Arrange / Act + p = ImagePath.Path(coords) + + # Assert + 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" + + +@pytest.mark.parametrize( + "coords", + ( + (0,), + [0], + (0, 1, 2), + [0, 1, 2], + ), +) +def test_path_odd_number_of_coordinates(coords: Sequence[int]) -> None: + # Act + with pytest.raises(ValueError) as e: + ImagePath.Path(coords) + + # Assert + assert str(e.value) == "wrong number of coordinates" + + +@pytest.mark.parametrize( + "coords, expected", + [ + ([0, 1, 2, 3], (0.0, 1.0, 2.0, 3.0)), + ([3, 2, 1, 0], (1.0, 0.0, 3.0, 2.0)), + (0, (0.0, 0.0, 0.0, 0.0)), + (1, (0.0, 0.0, 0.0, 0.0)), + ], +) +def test_getbbox( + coords: int | list[int], expected: tuple[float, float, float, float] +) -> None: + # Arrange + p = ImagePath.Path(coords) + + # Act / Assert + assert p.getbbox() == expected + + +def test_getbbox_no_args() -> None: + # Arrange + p = ImagePath.Path([0, 1, 2, 3]) + + # Act / Assert + with pytest.raises(TypeError): + p.getbbox(1) + + +@pytest.mark.parametrize( + "coords, expected", + [ + (0, []), + (list(range(6)), [(0.0, 3.0), (4.0, 9.0), (8.0, 15.0)]), + ], +) +def test_map(coords: int | list[int], expected: list[tuple[float, float]]) -> None: + # Arrange + p = ImagePath.Path(coords) + + # Act + # Modifies the path in-place + p.map(lambda x, y: (x * 2, y * 3)) + + # Assert + assert list(p) == expected + + +def test_transform() -> None: + # Arrange + p = ImagePath.Path([0, 1, 2, 3]) + theta = math.pi / 15 + + # Act + # Affine transform, in-place + p.transform( + (math.cos(theta), math.sin(theta), 20, -math.sin(theta), math.cos(theta), 20), + ) + + # Assert + assert p.tolist() == [ + (20.20791169081776, 20.978147600733806), + (22.58003027392089, 22.518619420565898), + ] + + +def test_transform_with_wrap() -> None: + # Arrange + p = ImagePath.Path([0, 1, 2, 3]) + theta = math.pi / 15 + + # Act + # Affine transform, in-place, with wrap parameter + p.transform( + (math.cos(theta), math.sin(theta), 20, -math.sin(theta), math.cos(theta), 20), + 1.0, + ) + + # Assert + assert p.tolist() == [ + (0.20791169081775962, 20.978147600733806), + (0.5800302739208902, 22.518619420565898), + ] + + +def test_overflow_segfault() -> None: + # Some Pythons fail getting the argument as an integer, and it falls + # through to the sequence. Seeing this on 32-bit Windows. + with pytest.raises((TypeError, MemoryError)): + # post patch, this fails with a memory error + x = Evil() + + # This fails due to the invalid malloc above, + # and segfaults + for i in range(200000): + x[i] = b"0" * 16 + + +class Evil: + def __init__(self) -> None: + self.corrupt = Image.core.path(0x4000000000000000) + + def __getitem__(self, i: int) -> bytes: + x = self.corrupt[i] + return struct.pack("dd", x[0], x[1]) + + def __setitem__(self, i: int, x: bytes) -> None: + self.corrupt[i] = struct.unpack("dd", x) diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py index 88ad1f9ee..b5bb334d3 100644 --- a/Tests/test_imageqt.py +++ b/Tests/test_imageqt.py @@ -1,60 +1,60 @@ -from __future__ import annotations - -import warnings - -import pytest - -from PIL import ImageQt - -from .helper import assert_image_similar, hopper - -pytestmark = pytest.mark.skipif( - not ImageQt.qt_is_installed, reason="Qt bindings are not installed" -) - -if ImageQt.qt_is_installed: - from PIL.ImageQt import qRgba - - -def test_rgb() -> None: - # from https://doc.qt.io/archives/qt-4.8/qcolor.html - # typedef QRgb - # An ARGB quadruplet on the format #AARRGGBB, - # equivalent to an unsigned int. - if ImageQt.qt_version == "6": - from PyQt6.QtGui import qRgb - elif ImageQt.qt_version == "side6": - from PySide6.QtGui import qRgb - - assert qRgb(0, 0, 0) == qRgba(0, 0, 0, 255) - - def checkrgb(r: int, g: int, b: int) -> None: - val = ImageQt.rgb(r, g, b) - val = val % 2**24 # drop the alpha - assert val >> 16 == r - assert ((val >> 8) % 2**8) == g - assert val % 2**8 == b - - checkrgb(0, 0, 0) - checkrgb(255, 0, 0) - checkrgb(0, 255, 0) - checkrgb(0, 0, 255) - - -def test_image() -> None: - modes = ["1", "RGB", "RGBA", "L", "P"] - qt_format = ImageQt.QImage.Format if ImageQt.qt_version == "6" else ImageQt.QImage - if hasattr(qt_format, "Format_Grayscale16"): # Qt 5.13+ - modes.append("I;16") - - for mode in modes: - im = hopper(mode) - roundtripped_im = ImageQt.fromqimage(ImageQt.ImageQt(im)) - if mode not in ("RGB", "RGBA"): - im = im.convert("RGB") - assert_image_similar(roundtripped_im, im, 1) - - -def test_closed_file() -> None: - with warnings.catch_warnings(): - ImageQt.ImageQt("Tests/images/hopper.gif") +from __future__ import annotations + +import warnings + +import pytest + +from PIL import ImageQt + +from .helper import assert_image_similar, hopper + +pytestmark = pytest.mark.skipif( + not ImageQt.qt_is_installed, reason="Qt bindings are not installed" +) + +if ImageQt.qt_is_installed: + from PIL.ImageQt import qRgba + + +def test_rgb() -> None: + # from https://doc.qt.io/archives/qt-4.8/qcolor.html + # typedef QRgb + # An ARGB quadruplet on the format #AARRGGBB, + # equivalent to an unsigned int. + if ImageQt.qt_version == "6": + from PyQt6.QtGui import qRgb + elif ImageQt.qt_version == "side6": + from PySide6.QtGui import qRgb + + assert qRgb(0, 0, 0) == qRgba(0, 0, 0, 255) + + def checkrgb(r: int, g: int, b: int) -> None: + val = ImageQt.rgb(r, g, b) + val = val % 2**24 # drop the alpha + assert val >> 16 == r + assert ((val >> 8) % 2**8) == g + assert val % 2**8 == b + + checkrgb(0, 0, 0) + checkrgb(255, 0, 0) + checkrgb(0, 255, 0) + checkrgb(0, 0, 255) + + +def test_image() -> None: + modes = ["1", "RGB", "RGBA", "L", "P"] + qt_format = ImageQt.QImage.Format if ImageQt.qt_version == "6" else ImageQt.QImage + if hasattr(qt_format, "Format_Grayscale16"): # Qt 5.13+ + modes.append("I;16") + + for mode in modes: + im = hopper(mode) + roundtripped_im = ImageQt.fromqimage(ImageQt.ImageQt(im)) + if mode not in ("RGB", "RGBA"): + im = im.convert("RGB") + assert_image_similar(roundtripped_im, im, 1) + + +def test_closed_file() -> None: + with warnings.catch_warnings(): + ImageQt.ImageQt("Tests/images/hopper.gif") diff --git a/Tests/test_imagesequence.py b/Tests/test_imagesequence.py index 9b37435eb..d205b591b 100644 --- a/Tests/test_imagesequence.py +++ b/Tests/test_imagesequence.py @@ -1,103 +1,103 @@ -from __future__ import annotations - -from pathlib import Path - -import pytest - -from PIL import Image, ImageSequence, 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") - - im = hopper("RGB") - im.save(test_file) - - seq = ImageSequence.Iterator(im) - - index = 0 - for frame in seq: - assert_image_equal(im, frame) - assert im.tell() == index - index += 1 - - assert index == 1 - - with pytest.raises(AttributeError): - ImageSequence.Iterator(0) # type: ignore[arg-type] - - -def test_iterator() -> None: - with Image.open("Tests/images/multipage.tiff") as im: - i = ImageSequence.Iterator(im) - for index in range(0, im.n_frames): - assert i[index] == next(i) - with pytest.raises(IndexError): - i[index + 1] - with pytest.raises(StopIteration): - next(i) - - -def test_iterator_min_frame() -> None: - with Image.open("Tests/images/hopper.psd") as im: - i = ImageSequence.Iterator(im) - for index in range(1, im.n_frames): - assert i[index] == next(i) - - -@pytest.mark.parametrize( - "libtiff", (pytest.param(True, marks=skip_unless_feature("libtiff")), False) -) -def test_multipage_tiff(monkeypatch: pytest.MonkeyPatch, libtiff: bool) -> None: - monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", libtiff) - with Image.open("Tests/images/multipage.tiff") as im: - for index, frame in enumerate(ImageSequence.Iterator(im)): - frame.load() - assert index == im.tell() - frame.convert("RGB") - - -def test_consecutive() -> None: - with Image.open("Tests/images/multipage.tiff") as im: - first_frame = None - for frame in ImageSequence.Iterator(im): - if first_frame is None: - first_frame = frame.copy() - assert first_frame is not None - for frame in ImageSequence.Iterator(im): - assert_image_equal(frame, first_frame) - break - - -def test_palette_mmap() -> None: - # Using mmap in ImageFile can require to reload the palette. - with Image.open("Tests/images/multipage-mmap.tiff") as im: - color1 = im.getpalette()[:3] - im.seek(0) - color2 = im.getpalette()[:3] - assert color1 == color2 - - -def test_all_frames() -> None: - # Test a single image - with Image.open("Tests/images/iss634.gif") as im: - ims = ImageSequence.all_frames(im) - - assert len(ims) == 42 - for i, im_frame in enumerate(ims): - assert im_frame is not im - - im.seek(i) - assert_image_equal(im, im_frame) - - # Test a series of images - ims = ImageSequence.all_frames([im, hopper(), im]) - assert len(ims) == 85 - - # Test an operation - ims = ImageSequence.all_frames(im, lambda im_frame: im_frame.rotate(90)) - for i, im_frame in enumerate(ims): - im.seek(i) - assert_image_equal(im.rotate(90), im_frame) +from __future__ import annotations + +from pathlib import Path + +import pytest + +from PIL import Image, ImageSequence, 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") + + im = hopper("RGB") + im.save(test_file) + + seq = ImageSequence.Iterator(im) + + index = 0 + for frame in seq: + assert_image_equal(im, frame) + assert im.tell() == index + index += 1 + + assert index == 1 + + with pytest.raises(AttributeError): + ImageSequence.Iterator(0) # type: ignore[arg-type] + + +def test_iterator() -> None: + with Image.open("Tests/images/multipage.tiff") as im: + i = ImageSequence.Iterator(im) + for index in range(0, im.n_frames): + assert i[index] == next(i) + with pytest.raises(IndexError): + i[index + 1] + with pytest.raises(StopIteration): + next(i) + + +def test_iterator_min_frame() -> None: + with Image.open("Tests/images/hopper.psd") as im: + i = ImageSequence.Iterator(im) + for index in range(1, im.n_frames): + assert i[index] == next(i) + + +@pytest.mark.parametrize( + "libtiff", (pytest.param(True, marks=skip_unless_feature("libtiff")), False) +) +def test_multipage_tiff(monkeypatch: pytest.MonkeyPatch, libtiff: bool) -> None: + monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", libtiff) + with Image.open("Tests/images/multipage.tiff") as im: + for index, frame in enumerate(ImageSequence.Iterator(im)): + frame.load() + assert index == im.tell() + frame.convert("RGB") + + +def test_consecutive() -> None: + with Image.open("Tests/images/multipage.tiff") as im: + first_frame = None + for frame in ImageSequence.Iterator(im): + if first_frame is None: + first_frame = frame.copy() + assert first_frame is not None + for frame in ImageSequence.Iterator(im): + assert_image_equal(frame, first_frame) + break + + +def test_palette_mmap() -> None: + # Using mmap in ImageFile can require to reload the palette. + with Image.open("Tests/images/multipage-mmap.tiff") as im: + color1 = im.getpalette()[:3] + im.seek(0) + color2 = im.getpalette()[:3] + assert color1 == color2 + + +def test_all_frames() -> None: + # Test a single image + with Image.open("Tests/images/iss634.gif") as im: + ims = ImageSequence.all_frames(im) + + assert len(ims) == 42 + for i, im_frame in enumerate(ims): + assert im_frame is not im + + im.seek(i) + assert_image_equal(im, im_frame) + + # Test a series of images + ims = ImageSequence.all_frames([im, hopper(), im]) + assert len(ims) == 85 + + # Test an operation + ims = ImageSequence.all_frames(im, lambda im_frame: im_frame.rotate(90)) + for i, im_frame in enumerate(ims): + im.seek(i) + assert_image_equal(im.rotate(90), im_frame) diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py index 4e9291fbb..733b19999 100644 --- a/Tests/test_imageshow.py +++ b/Tests/test_imageshow.py @@ -1,96 +1,96 @@ -from __future__ import annotations - -from typing import Any - -import pytest - -from PIL import Image, ImageShow - -from .helper import hopper, is_win32, on_ci - - -def test_sanity() -> None: - dir(Image) - dir(ImageShow) - - -def test_register() -> None: - # Test registering a viewer that is not a class - ImageShow.register("not a class") - - # Restore original state - ImageShow._viewers.pop() - - -@pytest.mark.parametrize( - "order", - [-1, 0], -) -def test_viewer_show(order: int) -> None: - class TestViewer(ImageShow.Viewer): - def show_image(self, image: Image.Image, **options: Any) -> bool: - self.methodCalled = True - return True - - viewer = TestViewer() - ImageShow.register(viewer, order) - - for mode in ("1", "I;16", "LA", "RGB", "RGBA"): - viewer.methodCalled = False - with hopper(mode) as im: - assert ImageShow.show(im) - assert viewer.methodCalled - - # Restore original state - ImageShow._viewers.pop(0) - - -@pytest.mark.skipif( - not on_ci() or is_win32(), - reason="Only run on CIs; hangs on Windows CIs", -) -@pytest.mark.parametrize("mode", ("1", "I;16", "LA", "RGB", "RGBA")) -def test_show(mode: str) -> None: - im = hopper(mode) - assert ImageShow.show(im) - - -def test_show_without_viewers() -> None: - viewers = ImageShow._viewers - ImageShow._viewers = [] - - with hopper() as im: - assert not ImageShow.show(im) - - ImageShow._viewers = viewers - - -def test_viewer() -> None: - viewer = ImageShow.Viewer() - - im = Image.new("L", (1, 1)) - assert viewer.get_format(im) is None - - with pytest.raises(NotImplementedError): - viewer.get_command("") - - -@pytest.mark.parametrize("viewer", ImageShow._viewers) -def test_viewers(viewer: ImageShow.Viewer) -> None: - try: - viewer.get_command("test.jpg") - except NotImplementedError: - pass - - -def test_ipythonviewer() -> None: - pytest.importorskip("IPython", reason="IPython not installed") - for viewer in ImageShow._viewers: - if isinstance(viewer, ImageShow.IPythonViewer): - test_viewer = viewer - break - else: - pytest.fail() - - im = hopper() - assert test_viewer.show(im) == 1 +from __future__ import annotations + +from typing import Any + +import pytest + +from PIL import Image, ImageShow + +from .helper import hopper, is_win32, on_ci + + +def test_sanity() -> None: + dir(Image) + dir(ImageShow) + + +def test_register() -> None: + # Test registering a viewer that is not a class + ImageShow.register("not a class") + + # Restore original state + ImageShow._viewers.pop() + + +@pytest.mark.parametrize( + "order", + [-1, 0], +) +def test_viewer_show(order: int) -> None: + class TestViewer(ImageShow.Viewer): + def show_image(self, image: Image.Image, **options: Any) -> bool: + self.methodCalled = True + return True + + viewer = TestViewer() + ImageShow.register(viewer, order) + + for mode in ("1", "I;16", "LA", "RGB", "RGBA"): + viewer.methodCalled = False + with hopper(mode) as im: + assert ImageShow.show(im) + assert viewer.methodCalled + + # Restore original state + ImageShow._viewers.pop(0) + + +@pytest.mark.skipif( + not on_ci() or is_win32(), + reason="Only run on CIs; hangs on Windows CIs", +) +@pytest.mark.parametrize("mode", ("1", "I;16", "LA", "RGB", "RGBA")) +def test_show(mode: str) -> None: + im = hopper(mode) + assert ImageShow.show(im) + + +def test_show_without_viewers() -> None: + viewers = ImageShow._viewers + ImageShow._viewers = [] + + with hopper() as im: + assert not ImageShow.show(im) + + ImageShow._viewers = viewers + + +def test_viewer() -> None: + viewer = ImageShow.Viewer() + + im = Image.new("L", (1, 1)) + assert viewer.get_format(im) is None + + with pytest.raises(NotImplementedError): + viewer.get_command("") + + +@pytest.mark.parametrize("viewer", ImageShow._viewers) +def test_viewers(viewer: ImageShow.Viewer) -> None: + try: + viewer.get_command("test.jpg") + except NotImplementedError: + pass + + +def test_ipythonviewer() -> None: + pytest.importorskip("IPython", reason="IPython not installed") + for viewer in ImageShow._viewers: + if isinstance(viewer, ImageShow.IPythonViewer): + test_viewer = viewer + break + else: + pytest.fail() + + im = hopper() + assert test_viewer.show(im) == 1 diff --git a/Tests/test_imagestat.py b/Tests/test_imagestat.py index 0dfbc5a2a..c696c8c12 100644 --- a/Tests/test_imagestat.py +++ b/Tests/test_imagestat.py @@ -1,59 +1,59 @@ -from __future__ import annotations - -import pytest - -from PIL import Image, ImageStat - -from .helper import hopper - - -def test_sanity() -> None: - im = hopper() - - st = ImageStat.Stat(im) - st = ImageStat.Stat(im.histogram()) - st = ImageStat.Stat(im, Image.new("1", im.size, 1)) - - # Check these run. Exceptions will cause failures. - st.extrema - st.sum - st.mean - st.median - st.rms - st.sum2 - st.var - st.stddev - - with pytest.raises(AttributeError): - st.spam() # type: ignore[attr-defined] - - with pytest.raises(TypeError): - ImageStat.Stat(1) # type: ignore[arg-type] - - -def test_hopper() -> None: - im = hopper() - - st = ImageStat.Stat(im) - - # verify a few values - assert st.extrema[0] == (0, 255) - assert st.median[0] == 72 - assert st.sum[0] == 1470218 - assert st.sum[1] == 1311896 - assert st.sum[2] == 1563008 - - -def test_constant() -> None: - im = Image.new("L", (128, 128), 128) - - st = ImageStat.Stat(im) - - assert st.extrema[0] == (128, 128) - assert st.sum[0] == 128**3 - assert st.sum2[0] == 128**4 - assert st.mean[0] == 128 - assert st.median[0] == 128 - assert st.rms[0] == 128 - assert st.var[0] == 0 - assert st.stddev[0] == 0 +from __future__ import annotations + +import pytest + +from PIL import Image, ImageStat + +from .helper import hopper + + +def test_sanity() -> None: + im = hopper() + + st = ImageStat.Stat(im) + st = ImageStat.Stat(im.histogram()) + st = ImageStat.Stat(im, Image.new("1", im.size, 1)) + + # Check these run. Exceptions will cause failures. + st.extrema + st.sum + st.mean + st.median + st.rms + st.sum2 + st.var + st.stddev + + with pytest.raises(AttributeError): + st.spam() # type: ignore[attr-defined] + + with pytest.raises(TypeError): + ImageStat.Stat(1) # type: ignore[arg-type] + + +def test_hopper() -> None: + im = hopper() + + st = ImageStat.Stat(im) + + # verify a few values + assert st.extrema[0] == (0, 255) + assert st.median[0] == 72 + assert st.sum[0] == 1470218 + assert st.sum[1] == 1311896 + assert st.sum[2] == 1563008 + + +def test_constant() -> None: + im = Image.new("L", (128, 128), 128) + + st = ImageStat.Stat(im) + + assert st.extrema[0] == (128, 128) + assert st.sum[0] == 128**3 + assert st.sum2[0] == 128**4 + assert st.mean[0] == 128 + assert st.median[0] == 128 + assert st.rms[0] == 128 + assert st.var[0] == 0 + assert st.stddev[0] == 0 diff --git a/Tests/test_imagetk.py b/Tests/test_imagetk.py index b607b8c43..027ab74aa 100644 --- a/Tests/test_imagetk.py +++ b/Tests/test_imagetk.py @@ -1,104 +1,104 @@ -from __future__ import annotations - -import pytest - -from PIL import Image - -from .helper import assert_image_equal, hopper - -try: - import tkinter as tk - - from PIL import ImageTk - - dir(ImageTk) - HAS_TK = True -except (OSError, ImportError): - # Skipped via pytestmark - HAS_TK = False - -TK_MODES = ("1", "L", "P", "RGB", "RGBA") - - -pytestmark = pytest.mark.skipif(not HAS_TK, reason="Tk not installed") - - -def setup_module() -> None: - try: - # setup tk - tk.Frame() - # root = tk.Tk() - except RuntimeError as v: - pytest.skip(f"RuntimeError: {v}") - except tk.TclError as v: - pytest.skip(f"TCL Error: {v}") - - -def test_kw() -> None: - TEST_JPG = "Tests/images/hopper.jpg" - TEST_PNG = "Tests/images/hopper.png" - with Image.open(TEST_JPG) as im1: - with Image.open(TEST_PNG) as im2: - with open(TEST_PNG, "rb") as fp: - data = fp.read() - kw = {"file": TEST_JPG, "data": data} - - # Test "file" - im = ImageTk._get_image_from_kw(kw) - assert_image_equal(im, im1) - - # Test "data" - im = ImageTk._get_image_from_kw(kw) - assert_image_equal(im, im2) - - # Test no relevant entry - im = ImageTk._get_image_from_kw(kw) - assert im is None - - -@pytest.mark.parametrize("mode", TK_MODES) -def test_photoimage(mode: str) -> None: - # test as image: - im = hopper(mode) - - # this should not crash - im_tk = ImageTk.PhotoImage(im) - - assert im_tk.width() == im.width - assert im_tk.height() == im.height - - reloaded = ImageTk.getimage(im_tk) - assert_image_equal(reloaded, im.convert("RGBA")) - - -def test_photoimage_apply_transparency() -> None: - with Image.open("Tests/images/pil123p.png") as im: - im_tk = ImageTk.PhotoImage(im) - reloaded = ImageTk.getimage(im_tk) - assert_image_equal(reloaded, im.convert("RGBA")) - - -@pytest.mark.parametrize("mode", TK_MODES) -def test_photoimage_blank(mode: str) -> None: - # test a image using mode/size: - im_tk = ImageTk.PhotoImage(mode, (100, 100)) - - assert im_tk.width() == 100 - assert im_tk.height() == 100 - - im = Image.new(mode, (100, 100)) - reloaded = ImageTk.getimage(im_tk) - assert_image_equal(reloaded.convert(mode), im) - - -def test_bitmapimage() -> None: - im = hopper("1") - - # this should not crash - im_tk = ImageTk.BitmapImage(im) - - assert im_tk.width() == im.width - assert im_tk.height() == im.height - - # reloaded = ImageTk.getimage(im_tk) - # assert_image_equal(reloaded, im) +from __future__ import annotations + +import pytest + +from PIL import Image + +from .helper import assert_image_equal, hopper + +try: + import tkinter as tk + + from PIL import ImageTk + + dir(ImageTk) + HAS_TK = True +except (OSError, ImportError): + # Skipped via pytestmark + HAS_TK = False + +TK_MODES = ("1", "L", "P", "RGB", "RGBA") + + +pytestmark = pytest.mark.skipif(not HAS_TK, reason="Tk not installed") + + +def setup_module() -> None: + try: + # setup tk + tk.Frame() + # root = tk.Tk() + except RuntimeError as v: + pytest.skip(f"RuntimeError: {v}") + except tk.TclError as v: + pytest.skip(f"TCL Error: {v}") + + +def test_kw() -> None: + TEST_JPG = "Tests/images/hopper.jpg" + TEST_PNG = "Tests/images/hopper.png" + with Image.open(TEST_JPG) as im1: + with Image.open(TEST_PNG) as im2: + with open(TEST_PNG, "rb") as fp: + data = fp.read() + kw = {"file": TEST_JPG, "data": data} + + # Test "file" + im = ImageTk._get_image_from_kw(kw) + assert_image_equal(im, im1) + + # Test "data" + im = ImageTk._get_image_from_kw(kw) + assert_image_equal(im, im2) + + # Test no relevant entry + im = ImageTk._get_image_from_kw(kw) + assert im is None + + +@pytest.mark.parametrize("mode", TK_MODES) +def test_photoimage(mode: str) -> None: + # test as image: + im = hopper(mode) + + # this should not crash + im_tk = ImageTk.PhotoImage(im) + + assert im_tk.width() == im.width + assert im_tk.height() == im.height + + reloaded = ImageTk.getimage(im_tk) + assert_image_equal(reloaded, im.convert("RGBA")) + + +def test_photoimage_apply_transparency() -> None: + with Image.open("Tests/images/pil123p.png") as im: + im_tk = ImageTk.PhotoImage(im) + reloaded = ImageTk.getimage(im_tk) + assert_image_equal(reloaded, im.convert("RGBA")) + + +@pytest.mark.parametrize("mode", TK_MODES) +def test_photoimage_blank(mode: str) -> None: + # test a image using mode/size: + im_tk = ImageTk.PhotoImage(mode, (100, 100)) + + assert im_tk.width() == 100 + assert im_tk.height() == 100 + + im = Image.new(mode, (100, 100)) + reloaded = ImageTk.getimage(im_tk) + assert_image_equal(reloaded.convert(mode), im) + + +def test_bitmapimage() -> None: + im = hopper("1") + + # this should not crash + im_tk = ImageTk.BitmapImage(im) + + assert im_tk.width() == im.width + assert im_tk.height() == im.height + + # reloaded = ImageTk.getimage(im_tk) + # assert_image_equal(reloaded, im) diff --git a/Tests/test_imagewin.py b/Tests/test_imagewin.py index b43c31b52..7defc5a1b 100644 --- a/Tests/test_imagewin.py +++ b/Tests/test_imagewin.py @@ -1,112 +1,112 @@ -from __future__ import annotations - -import pytest - -from PIL import ImageWin - -from .helper import hopper, is_win32 - - -class TestImageWin: - def test_sanity(self) -> None: - dir(ImageWin) - - def test_hdc(self) -> None: - # Arrange - dc = 50 - - # Act - hdc = ImageWin.HDC(dc) - dc2 = int(hdc) - - # Assert - assert dc2 == 50 - - def test_hwnd(self) -> None: - # Arrange - wnd = 50 - - # Act - hwnd = ImageWin.HWND(wnd) - wnd2 = int(hwnd) - - # Assert - assert wnd2 == 50 - - -@pytest.mark.skipif(not is_win32(), reason="Windows only") -class TestImageWinDib: - def test_dib_image(self) -> None: - # Arrange - im = hopper() - - # Act - dib = ImageWin.Dib(im) - - # Assert - assert dib.size == im.size - - def test_dib_mode_string(self) -> None: - # Arrange - mode = "RGBA" - size = (128, 128) - - # Act - dib = ImageWin.Dib(mode, size) - - # Assert - assert dib.size == (128, 128) - - def test_dib_paste(self) -> None: - # Arrange - im = hopper() - - mode = "RGBA" - size = (128, 128) - dib = ImageWin.Dib(mode, size) - - # Act - dib.paste(im) - - # Assert - assert dib.size == (128, 128) - - def test_dib_paste_bbox(self) -> None: - # Arrange - im = hopper() - bbox = (0, 0, 10, 10) - - mode = "RGBA" - size = (128, 128) - dib = ImageWin.Dib(mode, size) - - # Act - dib.paste(im, bbox) - - # Assert - assert dib.size == (128, 128) - - def test_dib_frombytes_tobytes_roundtrip(self) -> None: - # Arrange - # Make two different DIB images - im = hopper() - dib1 = ImageWin.Dib(im) - - mode = "RGB" - size = (128, 128) - dib2 = ImageWin.Dib(mode, size) - - # Confirm they're different - assert dib1.tobytes() != dib2.tobytes() - - # Act - # Make one the same as the using tobytes()/frombytes() - test_buffer = dib1.tobytes() - for datatype in ("bytes", "memoryview"): - if datatype == "memoryview": - test_buffer = memoryview(test_buffer) - dib2.frombytes(test_buffer) - - # Assert - # Confirm they're the same - assert dib1.tobytes() == dib2.tobytes() +from __future__ import annotations + +import pytest + +from PIL import ImageWin + +from .helper import hopper, is_win32 + + +class TestImageWin: + def test_sanity(self) -> None: + dir(ImageWin) + + def test_hdc(self) -> None: + # Arrange + dc = 50 + + # Act + hdc = ImageWin.HDC(dc) + dc2 = int(hdc) + + # Assert + assert dc2 == 50 + + def test_hwnd(self) -> None: + # Arrange + wnd = 50 + + # Act + hwnd = ImageWin.HWND(wnd) + wnd2 = int(hwnd) + + # Assert + assert wnd2 == 50 + + +@pytest.mark.skipif(not is_win32(), reason="Windows only") +class TestImageWinDib: + def test_dib_image(self) -> None: + # Arrange + im = hopper() + + # Act + dib = ImageWin.Dib(im) + + # Assert + assert dib.size == im.size + + def test_dib_mode_string(self) -> None: + # Arrange + mode = "RGBA" + size = (128, 128) + + # Act + dib = ImageWin.Dib(mode, size) + + # Assert + assert dib.size == (128, 128) + + def test_dib_paste(self) -> None: + # Arrange + im = hopper() + + mode = "RGBA" + size = (128, 128) + dib = ImageWin.Dib(mode, size) + + # Act + dib.paste(im) + + # Assert + assert dib.size == (128, 128) + + def test_dib_paste_bbox(self) -> None: + # Arrange + im = hopper() + bbox = (0, 0, 10, 10) + + mode = "RGBA" + size = (128, 128) + dib = ImageWin.Dib(mode, size) + + # Act + dib.paste(im, bbox) + + # Assert + assert dib.size == (128, 128) + + def test_dib_frombytes_tobytes_roundtrip(self) -> None: + # Arrange + # Make two different DIB images + im = hopper() + dib1 = ImageWin.Dib(im) + + mode = "RGB" + size = (128, 128) + dib2 = ImageWin.Dib(mode, size) + + # Confirm they're different + assert dib1.tobytes() != dib2.tobytes() + + # Act + # Make one the same as the using tobytes()/frombytes() + test_buffer = dib1.tobytes() + for datatype in ("bytes", "memoryview"): + if datatype == "memoryview": + test_buffer = memoryview(test_buffer) + dib2.frombytes(test_buffer) + + # Assert + # Confirm they're the same + assert dib1.tobytes() == dib2.tobytes() diff --git a/Tests/test_imagewin_pointers.py b/Tests/test_imagewin_pointers.py index f59ee7284..a9f2caa48 100644 --- a/Tests/test_imagewin_pointers.py +++ b/Tests/test_imagewin_pointers.py @@ -1,117 +1,117 @@ -from __future__ import annotations - -from io import BytesIO -from pathlib import Path - -from PIL import Image, ImageWin - -from .helper import hopper, is_win32 - -# see https://github.com/python-pillow/Pillow/pull/1431#issuecomment-144692652 - -if is_win32(): - import ctypes - import ctypes.wintypes - - class BITMAPFILEHEADER(ctypes.Structure): - _pack_ = 2 - _fields_ = [ - ("bfType", ctypes.wintypes.WORD), - ("bfSize", ctypes.wintypes.DWORD), - ("bfReserved1", ctypes.wintypes.WORD), - ("bfReserved2", ctypes.wintypes.WORD), - ("bfOffBits", ctypes.wintypes.DWORD), - ] - - class BITMAPINFOHEADER(ctypes.Structure): - _pack_ = 2 - _fields_ = [ - ("biSize", ctypes.wintypes.DWORD), - ("biWidth", ctypes.wintypes.LONG), - ("biHeight", ctypes.wintypes.LONG), - ("biPlanes", ctypes.wintypes.WORD), - ("biBitCount", ctypes.wintypes.WORD), - ("biCompression", ctypes.wintypes.DWORD), - ("biSizeImage", ctypes.wintypes.DWORD), - ("biXPelsPerMeter", ctypes.wintypes.LONG), - ("biYPelsPerMeter", ctypes.wintypes.LONG), - ("biClrUsed", ctypes.wintypes.DWORD), - ("biClrImportant", ctypes.wintypes.DWORD), - ] - - BI_RGB = 0 - DIB_RGB_COLORS = 0 - - memcpy = ctypes.cdll.msvcrt.memcpy - memcpy.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t] - - CreateCompatibleDC = ctypes.windll.gdi32.CreateCompatibleDC - CreateCompatibleDC.argtypes = [ctypes.wintypes.HDC] - CreateCompatibleDC.restype = ctypes.wintypes.HDC - - DeleteDC = ctypes.windll.gdi32.DeleteDC - DeleteDC.argtypes = [ctypes.wintypes.HDC] - - SelectObject = ctypes.windll.gdi32.SelectObject - SelectObject.argtypes = [ctypes.wintypes.HDC, ctypes.wintypes.HGDIOBJ] - SelectObject.restype = ctypes.wintypes.HGDIOBJ - - DeleteObject = ctypes.windll.gdi32.DeleteObject - DeleteObject.argtypes = [ctypes.wintypes.HGDIOBJ] - - CreateDIBSection = ctypes.windll.gdi32.CreateDIBSection - CreateDIBSection.argtypes = [ - ctypes.wintypes.HDC, - ctypes.c_void_p, - ctypes.c_uint, - ctypes.POINTER(ctypes.c_void_p), - ctypes.wintypes.HANDLE, - ctypes.wintypes.DWORD, - ] - CreateDIBSection.restype = ctypes.wintypes.HBITMAP - - def serialize_dib(bi, pixels) -> bytearray: - bf = BITMAPFILEHEADER() - bf.bfType = 0x4D42 - bf.bfOffBits = ctypes.sizeof(bf) + bi.biSize - bf.bfSize = bf.bfOffBits + bi.biSizeImage - bf.bfReserved1 = bf.bfReserved2 = 0 - - buf = (ctypes.c_byte * bf.bfSize)() - bp = ctypes.addressof(buf) - memcpy(bp, ctypes.byref(bf), ctypes.sizeof(bf)) - memcpy(bp + ctypes.sizeof(bf), ctypes.byref(bi), bi.biSize) - memcpy(bp + bf.bfOffBits, pixels, bi.biSizeImage) - return bytearray(buf) - - def test_pointer(tmp_path: Path) -> None: - im = hopper() - (width, height) = im.size - opath = str(tmp_path / "temp.png") - imdib = ImageWin.Dib(im) - - hdr = BITMAPINFOHEADER() - hdr.biSize = ctypes.sizeof(hdr) - hdr.biWidth = width - hdr.biHeight = height - hdr.biPlanes = 1 - hdr.biBitCount = 32 - hdr.biCompression = BI_RGB - hdr.biSizeImage = width * height * 4 - hdr.biClrUsed = 0 - hdr.biClrImportant = 0 - - hdc = CreateCompatibleDC(None) - pixels = ctypes.c_void_p() - dib = CreateDIBSection( - hdc, ctypes.byref(hdr), DIB_RGB_COLORS, ctypes.byref(pixels), None, 0 - ) - SelectObject(hdc, dib) - - imdib.expose(hdc) - bitmap = serialize_dib(hdr, pixels) - DeleteObject(dib) - DeleteDC(hdc) - - with Image.open(BytesIO(bitmap)) as im: - im.save(opath) +from __future__ import annotations + +from io import BytesIO +from pathlib import Path + +from PIL import Image, ImageWin + +from .helper import hopper, is_win32 + +# see https://github.com/python-pillow/Pillow/pull/1431#issuecomment-144692652 + +if is_win32(): + import ctypes + import ctypes.wintypes + + class BITMAPFILEHEADER(ctypes.Structure): + _pack_ = 2 + _fields_ = [ + ("bfType", ctypes.wintypes.WORD), + ("bfSize", ctypes.wintypes.DWORD), + ("bfReserved1", ctypes.wintypes.WORD), + ("bfReserved2", ctypes.wintypes.WORD), + ("bfOffBits", ctypes.wintypes.DWORD), + ] + + class BITMAPINFOHEADER(ctypes.Structure): + _pack_ = 2 + _fields_ = [ + ("biSize", ctypes.wintypes.DWORD), + ("biWidth", ctypes.wintypes.LONG), + ("biHeight", ctypes.wintypes.LONG), + ("biPlanes", ctypes.wintypes.WORD), + ("biBitCount", ctypes.wintypes.WORD), + ("biCompression", ctypes.wintypes.DWORD), + ("biSizeImage", ctypes.wintypes.DWORD), + ("biXPelsPerMeter", ctypes.wintypes.LONG), + ("biYPelsPerMeter", ctypes.wintypes.LONG), + ("biClrUsed", ctypes.wintypes.DWORD), + ("biClrImportant", ctypes.wintypes.DWORD), + ] + + BI_RGB = 0 + DIB_RGB_COLORS = 0 + + memcpy = ctypes.cdll.msvcrt.memcpy + memcpy.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t] + + CreateCompatibleDC = ctypes.windll.gdi32.CreateCompatibleDC + CreateCompatibleDC.argtypes = [ctypes.wintypes.HDC] + CreateCompatibleDC.restype = ctypes.wintypes.HDC + + DeleteDC = ctypes.windll.gdi32.DeleteDC + DeleteDC.argtypes = [ctypes.wintypes.HDC] + + SelectObject = ctypes.windll.gdi32.SelectObject + SelectObject.argtypes = [ctypes.wintypes.HDC, ctypes.wintypes.HGDIOBJ] + SelectObject.restype = ctypes.wintypes.HGDIOBJ + + DeleteObject = ctypes.windll.gdi32.DeleteObject + DeleteObject.argtypes = [ctypes.wintypes.HGDIOBJ] + + CreateDIBSection = ctypes.windll.gdi32.CreateDIBSection + CreateDIBSection.argtypes = [ + ctypes.wintypes.HDC, + ctypes.c_void_p, + ctypes.c_uint, + ctypes.POINTER(ctypes.c_void_p), + ctypes.wintypes.HANDLE, + ctypes.wintypes.DWORD, + ] + CreateDIBSection.restype = ctypes.wintypes.HBITMAP + + def serialize_dib(bi, pixels) -> bytearray: + bf = BITMAPFILEHEADER() + bf.bfType = 0x4D42 + bf.bfOffBits = ctypes.sizeof(bf) + bi.biSize + bf.bfSize = bf.bfOffBits + bi.biSizeImage + bf.bfReserved1 = bf.bfReserved2 = 0 + + buf = (ctypes.c_byte * bf.bfSize)() + bp = ctypes.addressof(buf) + memcpy(bp, ctypes.byref(bf), ctypes.sizeof(bf)) + memcpy(bp + ctypes.sizeof(bf), ctypes.byref(bi), bi.biSize) + memcpy(bp + bf.bfOffBits, pixels, bi.biSizeImage) + return bytearray(buf) + + def test_pointer(tmp_path: Path) -> None: + im = hopper() + (width, height) = im.size + opath = str(tmp_path / "temp.png") + imdib = ImageWin.Dib(im) + + hdr = BITMAPINFOHEADER() + hdr.biSize = ctypes.sizeof(hdr) + hdr.biWidth = width + hdr.biHeight = height + hdr.biPlanes = 1 + hdr.biBitCount = 32 + hdr.biCompression = BI_RGB + hdr.biSizeImage = width * height * 4 + hdr.biClrUsed = 0 + hdr.biClrImportant = 0 + + hdc = CreateCompatibleDC(None) + pixels = ctypes.c_void_p() + dib = CreateDIBSection( + hdc, ctypes.byref(hdr), DIB_RGB_COLORS, ctypes.byref(pixels), None, 0 + ) + SelectObject(hdc, dib) + + imdib.expose(hdc) + bitmap = serialize_dib(hdr, pixels) + DeleteObject(dib) + DeleteDC(hdc) + + with Image.open(BytesIO(bitmap)) as im: + im.save(opath) diff --git a/Tests/test_lib_image.py b/Tests/test_lib_image.py index 31548bbc9..47c6be7db 100644 --- a/Tests/test_lib_image.py +++ b/Tests/test_lib_image.py @@ -1,34 +1,34 @@ -from __future__ import annotations - -import pytest - -from PIL import Image - - -def test_setmode() -> None: - im = Image.new("L", (1, 1), 255) - im.im.setmode("1") - assert im.im.getpixel((0, 0)) == 255 - im.im.setmode("L") - assert im.im.getpixel((0, 0)) == 255 - - im = Image.new("1", (1, 1), 1) - im.im.setmode("L") - assert im.im.getpixel((0, 0)) == 255 - im.im.setmode("1") - assert im.im.getpixel((0, 0)) == 255 - - im = Image.new("RGB", (1, 1), (1, 2, 3)) - im.im.setmode("RGB") - assert im.im.getpixel((0, 0)) == (1, 2, 3) - im.im.setmode("RGBA") - assert im.im.getpixel((0, 0)) == (1, 2, 3, 255) - im.im.setmode("RGBX") - assert im.im.getpixel((0, 0)) == (1, 2, 3, 255) - im.im.setmode("RGB") - assert im.im.getpixel((0, 0)) == (1, 2, 3) - - with pytest.raises(ValueError): - im.im.setmode("L") - with pytest.raises(ValueError): - im.im.setmode("RGBABCDE") +from __future__ import annotations + +import pytest + +from PIL import Image + + +def test_setmode() -> None: + im = Image.new("L", (1, 1), 255) + im.im.setmode("1") + assert im.im.getpixel((0, 0)) == 255 + im.im.setmode("L") + assert im.im.getpixel((0, 0)) == 255 + + im = Image.new("1", (1, 1), 1) + im.im.setmode("L") + assert im.im.getpixel((0, 0)) == 255 + im.im.setmode("1") + assert im.im.getpixel((0, 0)) == 255 + + im = Image.new("RGB", (1, 1), (1, 2, 3)) + im.im.setmode("RGB") + assert im.im.getpixel((0, 0)) == (1, 2, 3) + im.im.setmode("RGBA") + assert im.im.getpixel((0, 0)) == (1, 2, 3, 255) + im.im.setmode("RGBX") + assert im.im.getpixel((0, 0)) == (1, 2, 3, 255) + im.im.setmode("RGB") + assert im.im.getpixel((0, 0)) == (1, 2, 3) + + with pytest.raises(ValueError): + im.im.setmode("L") + with pytest.raises(ValueError): + im.im.setmode("RGBABCDE") diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py index b4a300d0c..b4e8a6a18 100644 --- a/Tests/test_lib_pack.py +++ b/Tests/test_lib_pack.py @@ -1,820 +1,820 @@ -from __future__ import annotations - -import sys - -import pytest - -from PIL import Image - -X = 255 - - -class TestLibPack: - def assert_pack( - self, - mode: str, - rawmode: str, - data: int | bytes, - *pixels: float | tuple[int, ...], - ) -> None: - """ - data - either raw bytes with data or just number of bytes in rawmode. - """ - im = Image.new(mode, (len(pixels), 1)) - for x, pixel in enumerate(pixels): - im.putpixel((x, 0), pixel) - - if isinstance(data, int): - data_len = data * len(pixels) - data = bytes(range(1, data_len + 1)) - - assert data == im.tobytes("raw", rawmode) - - def test_1(self) -> None: - self.assert_pack("1", "1", b"\x01", 0, 0, 0, 0, 0, 0, 0, X) - self.assert_pack("1", "1;I", b"\x01", X, X, X, X, X, X, X, 0) - self.assert_pack("1", "1;R", b"\x01", X, 0, 0, 0, 0, 0, 0, 0) - self.assert_pack("1", "1;IR", b"\x01", 0, X, X, X, X, X, X, X) - - self.assert_pack("1", "1", b"\xaa", X, 0, X, 0, X, 0, X, 0) - self.assert_pack("1", "1;I", b"\xaa", 0, X, 0, X, 0, X, 0, X) - self.assert_pack("1", "1;R", b"\xaa", 0, X, 0, X, 0, X, 0, X) - self.assert_pack("1", "1;IR", b"\xaa", X, 0, X, 0, X, 0, X, 0) - - self.assert_pack("1", "L", b"\xff\x00\x00\xff\x00\x00", X, 0, 0, X, 0, 0) - - def test_L(self) -> None: - self.assert_pack("L", "L", 1, 1, 2, 3, 4) - self.assert_pack("L", "L;16", b"\x00\xc6\x00\xaf", 198, 175) - self.assert_pack("L", "L;16B", b"\xc6\x00\xaf\x00", 198, 175) - - def test_LA(self) -> None: - self.assert_pack("LA", "LA", 2, (1, 2), (3, 4), (5, 6)) - self.assert_pack("LA", "LA;L", 2, (1, 4), (2, 5), (3, 6)) - - def test_La(self) -> None: - self.assert_pack("La", "La", 2, (1, 2), (3, 4), (5, 6)) - - def test_P(self) -> None: - self.assert_pack("P", "P;1", b"\xe4", 1, 1, 1, 0, 0, 255, 0, 0) - self.assert_pack("P", "P;2", b"\xe4", 3, 2, 1, 0) - self.assert_pack("P", "P;4", b"\x02\xef", 0, 2, 14, 15) - self.assert_pack("P", "P", 1, 1, 2, 3, 4) - - def test_PA(self) -> None: - self.assert_pack("PA", "PA", 2, (1, 2), (3, 4), (5, 6)) - self.assert_pack("PA", "PA;L", 2, (1, 4), (2, 5), (3, 6)) - - def test_RGB(self) -> None: - self.assert_pack("RGB", "RGB", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) - self.assert_pack( - "RGB", "RGBX", b"\x01\x02\x03\xff\x05\x06\x07\xff", (1, 2, 3), (5, 6, 7) - ) - self.assert_pack( - "RGB", "XRGB", b"\x00\x02\x03\x04\x00\x06\x07\x08", (2, 3, 4), (6, 7, 8) - ) - self.assert_pack("RGB", "BGR", 3, (3, 2, 1), (6, 5, 4), (9, 8, 7)) - self.assert_pack( - "RGB", "BGRX", b"\x01\x02\x03\x00\x05\x06\x07\x00", (3, 2, 1), (7, 6, 5) - ) - self.assert_pack( - "RGB", "XBGR", b"\x00\x02\x03\x04\x00\x06\x07\x08", (4, 3, 2), (8, 7, 6) - ) - self.assert_pack("RGB", "RGB;L", 3, (1, 4, 7), (2, 5, 8), (3, 6, 9)) - self.assert_pack("RGB", "R", 1, (1, 9, 9), (2, 9, 9), (3, 9, 9)) - self.assert_pack("RGB", "G", 1, (9, 1, 9), (9, 2, 9), (9, 3, 9)) - self.assert_pack("RGB", "B", 1, (9, 9, 1), (9, 9, 2), (9, 9, 3)) - - def test_RGBA(self) -> None: - self.assert_pack("RGBA", "RGBA", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) - self.assert_pack( - "RGBA", "RGBA;L", 4, (1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12) - ) - self.assert_pack("RGBA", "RGB", 3, (1, 2, 3, 14), (4, 5, 6, 15), (7, 8, 9, 16)) - self.assert_pack("RGBA", "BGR", 3, (3, 2, 1, 14), (6, 5, 4, 15), (9, 8, 7, 16)) - self.assert_pack("RGBA", "BGRA", 4, (3, 2, 1, 4), (7, 6, 5, 8), (11, 10, 9, 12)) - self.assert_pack("RGBA", "ABGR", 4, (4, 3, 2, 1), (8, 7, 6, 5), (12, 11, 10, 9)) - self.assert_pack( - "RGBA", - "BGRa", - 4, - (191, 127, 63, 4), - (223, 191, 159, 8), - (233, 212, 191, 12), - ) - self.assert_pack("RGBA", "R", 1, (1, 0, 8, 9), (2, 0, 8, 9), (3, 0, 8, 0)) - self.assert_pack("RGBA", "G", 1, (6, 1, 8, 9), (6, 2, 8, 9), (6, 3, 8, 9)) - self.assert_pack("RGBA", "B", 1, (6, 7, 1, 9), (6, 7, 2, 0), (6, 7, 3, 9)) - self.assert_pack("RGBA", "A", 1, (6, 7, 0, 1), (6, 7, 0, 2), (0, 7, 0, 3)) - - def test_RGBa(self) -> None: - self.assert_pack("RGBa", "RGBa", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) - self.assert_pack("RGBa", "BGRa", 4, (3, 2, 1, 4), (7, 6, 5, 8), (11, 10, 9, 12)) - self.assert_pack("RGBa", "aBGR", 4, (4, 3, 2, 1), (8, 7, 6, 5), (12, 11, 10, 9)) - - def test_RGBX(self) -> None: - self.assert_pack("RGBX", "RGBX", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) - self.assert_pack( - "RGBX", "RGBX;L", 4, (1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12) - ) - self.assert_pack("RGBX", "RGB", 3, (1, 2, 3, X), (4, 5, 6, X), (7, 8, 9, X)) - self.assert_pack("RGBX", "BGR", 3, (3, 2, 1, X), (6, 5, 4, X), (9, 8, 7, X)) - self.assert_pack( - "RGBX", - "BGRX", - b"\x01\x02\x03\x00\x05\x06\x07\x00\t\n\x0b\x00", - (3, 2, 1, X), - (7, 6, 5, X), - (11, 10, 9, X), - ) - self.assert_pack( - "RGBX", - "XBGR", - b"\x00\x02\x03\x04\x00\x06\x07\x08\x00\n\x0b\x0c", - (4, 3, 2, X), - (8, 7, 6, X), - (12, 11, 10, X), - ) - self.assert_pack("RGBX", "R", 1, (1, 0, 8, 9), (2, 0, 8, 9), (3, 0, 8, 0)) - self.assert_pack("RGBX", "G", 1, (6, 1, 8, 9), (6, 2, 8, 9), (6, 3, 8, 9)) - self.assert_pack("RGBX", "B", 1, (6, 7, 1, 9), (6, 7, 2, 0), (6, 7, 3, 9)) - self.assert_pack("RGBX", "X", 1, (6, 7, 0, 1), (6, 7, 0, 2), (0, 7, 0, 3)) - - def test_CMYK(self) -> None: - self.assert_pack("CMYK", "CMYK", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) - self.assert_pack( - "CMYK", - "CMYK;I", - 4, - (254, 253, 252, 251), - (250, 249, 248, 247), - (246, 245, 244, 243), - ) - self.assert_pack( - "CMYK", "CMYK;L", 4, (1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12) - ) - self.assert_pack("CMYK", "K", 1, (6, 7, 0, 1), (6, 7, 0, 2), (0, 7, 0, 3)) - - def test_YCbCr(self) -> None: - self.assert_pack("YCbCr", "YCbCr", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) - self.assert_pack("YCbCr", "YCbCr;L", 3, (1, 4, 7), (2, 5, 8), (3, 6, 9)) - self.assert_pack( - "YCbCr", - "YCbCrX", - b"\x01\x02\x03\xff\x05\x06\x07\xff\t\n\x0b\xff", - (1, 2, 3), - (5, 6, 7), - (9, 10, 11), - ) - self.assert_pack( - "YCbCr", - "YCbCrK", - b"\x01\x02\x03\xff\x05\x06\x07\xff\t\n\x0b\xff", - (1, 2, 3), - (5, 6, 7), - (9, 10, 11), - ) - self.assert_pack("YCbCr", "Y", 1, (1, 0, 8, 9), (2, 0, 8, 9), (3, 0, 8, 0)) - self.assert_pack("YCbCr", "Cb", 1, (6, 1, 8, 9), (6, 2, 8, 9), (6, 3, 8, 9)) - self.assert_pack("YCbCr", "Cr", 1, (6, 7, 1, 9), (6, 7, 2, 0), (6, 7, 3, 9)) - - def test_LAB(self) -> None: - self.assert_pack("LAB", "LAB", 3, (1, 130, 131), (4, 133, 134), (7, 136, 137)) - self.assert_pack("LAB", "L", 1, (1, 9, 9), (2, 9, 9), (3, 9, 9)) - self.assert_pack("LAB", "A", 1, (9, 1, 9), (9, 2, 9), (9, 3, 9)) - self.assert_pack("LAB", "B", 1, (9, 9, 1), (9, 9, 2), (9, 9, 3)) - - def test_HSV(self) -> None: - self.assert_pack("HSV", "HSV", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) - self.assert_pack("HSV", "H", 1, (1, 9, 9), (2, 9, 9), (3, 9, 9)) - self.assert_pack("HSV", "S", 1, (9, 1, 9), (9, 2, 9), (9, 3, 9)) - self.assert_pack("HSV", "V", 1, (9, 9, 1), (9, 9, 2), (9, 9, 3)) - - def test_I(self) -> None: - self.assert_pack("I", "I;16B", 2, 0x0102, 0x0304) - self.assert_pack( - "I", "I;32S", b"\x83\x00\x00\x01\x01\x00\x00\x83", 0x01000083, -2097151999 - ) - - if sys.byteorder == "little": - self.assert_pack("I", "I", 4, 0x04030201, 0x08070605) - self.assert_pack( - "I", - "I;32NS", - b"\x83\x00\x00\x01\x01\x00\x00\x83", - 0x01000083, - -2097151999, - ) - else: - self.assert_pack("I", "I", 4, 0x01020304, 0x05060708) - self.assert_pack( - "I", - "I;32NS", - b"\x83\x00\x00\x01\x01\x00\x00\x83", - -2097151999, - 0x01000083, - ) - - def test_I16(self) -> None: - if sys.byteorder == "little": - self.assert_pack("I;16N", "I;16N", 2, 0x0201, 0x0403, 0x0605) - else: - self.assert_pack("I;16N", "I;16N", 2, 0x0102, 0x0304, 0x0506) - - def test_F_float(self) -> None: - self.assert_pack("F", "F;32F", 4, 1.539989614439558e-36, 4.063216068939723e-34) - - if sys.byteorder == "little": - self.assert_pack("F", "F", 4, 1.539989614439558e-36, 4.063216068939723e-34) - self.assert_pack( - "F", "F;32NF", 4, 1.539989614439558e-36, 4.063216068939723e-34 - ) - else: - self.assert_pack("F", "F", 4, 2.387939260590663e-38, 6.301941157072183e-36) - self.assert_pack( - "F", "F;32NF", 4, 2.387939260590663e-38, 6.301941157072183e-36 - ) - - -class TestLibUnpack: - def assert_unpack( - self, - mode: str, - rawmode: str, - data: int | bytes, - *pixels: float | tuple[int, ...], - ) -> None: - """ - data - either raw bytes with data or just number of bytes in rawmode. - """ - if isinstance(data, int): - data_len = data * len(pixels) - data = bytes(range(1, data_len + 1)) - - im = Image.frombytes(mode, (len(pixels), 1), data, "raw", rawmode, 0, 1) - - for x, pixel in enumerate(pixels): - assert pixel == im.getpixel((x, 0)) - - def test_1(self) -> None: - self.assert_unpack("1", "1", b"\x01", 0, 0, 0, 0, 0, 0, 0, X) - self.assert_unpack("1", "1;I", b"\x01", X, X, X, X, X, X, X, 0) - self.assert_unpack("1", "1;R", b"\x01", X, 0, 0, 0, 0, 0, 0, 0) - self.assert_unpack("1", "1;IR", b"\x01", 0, X, X, X, X, X, X, X) - - self.assert_unpack("1", "1", b"\xaa", X, 0, X, 0, X, 0, X, 0) - self.assert_unpack("1", "1;I", b"\xaa", 0, X, 0, X, 0, X, 0, X) - self.assert_unpack("1", "1;R", b"\xaa", 0, X, 0, X, 0, X, 0, X) - self.assert_unpack("1", "1;IR", b"\xaa", X, 0, X, 0, X, 0, X, 0) - - self.assert_unpack("1", "1;8", b"\x00\x01\x02\xff", 0, X, X, X) - - def test_L(self) -> None: - self.assert_unpack("L", "L;2", b"\xe4", 255, 170, 85, 0) - self.assert_unpack("L", "L;2I", b"\xe4", 0, 85, 170, 255) - self.assert_unpack("L", "L;2R", b"\xe4", 0, 170, 85, 255) - self.assert_unpack("L", "L;2IR", b"\xe4", 255, 85, 170, 0) - - self.assert_unpack("L", "L;4", b"\x02\xef", 0, 34, 238, 255) - self.assert_unpack("L", "L;4I", b"\x02\xef", 255, 221, 17, 0) - self.assert_unpack("L", "L;4R", b"\x02\xef", 68, 0, 255, 119) - self.assert_unpack("L", "L;4IR", b"\x02\xef", 187, 255, 0, 136) - - self.assert_unpack("L", "L", 1, 1, 2, 3, 4) - self.assert_unpack("L", "L;I", 1, 254, 253, 252, 251) - self.assert_unpack("L", "L;R", 1, 128, 64, 192, 32) - self.assert_unpack("L", "L;16", 2, 2, 4, 6, 8) - self.assert_unpack("L", "L;16B", 2, 1, 3, 5, 7) - self.assert_unpack("L", "L;16", b"\x00\xc6\x00\xaf", 198, 175) - self.assert_unpack("L", "L;16B", b"\xc6\x00\xaf\x00", 198, 175) - - def test_LA(self) -> None: - self.assert_unpack("LA", "LA", 2, (1, 2), (3, 4), (5, 6)) - self.assert_unpack("LA", "LA;L", 2, (1, 4), (2, 5), (3, 6)) - - def test_La(self) -> None: - self.assert_unpack("La", "La", 2, (1, 2), (3, 4), (5, 6)) - - def test_P(self) -> None: - self.assert_unpack("P", "P;1", b"\xe4", 1, 1, 1, 0, 0, 1, 0, 0) - self.assert_unpack("P", "P;2", b"\xe4", 3, 2, 1, 0) - # erroneous? - # self.assert_unpack("P", "P;2L", b'\xe4', 1, 1, 1, 0) - self.assert_unpack("P", "P;4", b"\x02\xef", 0, 2, 14, 15) - # erroneous? - # self.assert_unpack("P", "P;4L", b'\x02\xef', 2, 10, 10, 0) - self.assert_unpack("P", "P", 1, 1, 2, 3, 4) - self.assert_unpack("P", "P;R", 1, 128, 64, 192, 32) - - def test_PA(self) -> None: - self.assert_unpack("PA", "PA", 2, (1, 2), (3, 4), (5, 6)) - self.assert_unpack("PA", "PA;L", 2, (1, 4), (2, 5), (3, 6)) - - def test_RGB(self) -> None: - self.assert_unpack("RGB", "RGB", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) - self.assert_unpack("RGB", "RGB;L", 3, (1, 4, 7), (2, 5, 8), (3, 6, 9)) - self.assert_unpack("RGB", "RGB;R", 3, (128, 64, 192), (32, 160, 96)) - self.assert_unpack("RGB", "RGB;16L", 6, (2, 4, 6), (8, 10, 12)) - self.assert_unpack("RGB", "RGB;16B", 6, (1, 3, 5), (7, 9, 11)) - self.assert_unpack("RGB", "BGR", 3, (3, 2, 1), (6, 5, 4), (9, 8, 7)) - self.assert_unpack("RGB", "RGB;15", 2, (8, 131, 0), (24, 0, 8)) - self.assert_unpack("RGB", "BGR;15", 2, (0, 131, 8), (8, 0, 24)) - self.assert_unpack("RGB", "RGB;16", 2, (8, 64, 0), (24, 129, 0)) - self.assert_unpack("RGB", "BGR;16", 2, (0, 64, 8), (0, 129, 24)) - self.assert_unpack("RGB", "RGB;4B", 2, (17, 0, 34), (51, 0, 68)) - self.assert_unpack("RGB", "RGBX", 4, (1, 2, 3), (5, 6, 7), (9, 10, 11)) - self.assert_unpack("RGB", "RGBX;L", 4, (1, 4, 7), (2, 5, 8), (3, 6, 9)) - self.assert_unpack("RGB", "BGRX", 4, (3, 2, 1), (7, 6, 5), (11, 10, 9)) - self.assert_unpack("RGB", "XRGB", 4, (2, 3, 4), (6, 7, 8), (10, 11, 12)) - self.assert_unpack("RGB", "XBGR", 4, (4, 3, 2), (8, 7, 6), (12, 11, 10)) - self.assert_unpack( - "RGB", - "YCC;P", - b"D]\x9c\x82\x1a\x91\xfaOC\xe7J\x12", # random data - (127, 102, 0), - (192, 227, 0), - (213, 255, 170), - (98, 255, 133), - ) - self.assert_unpack("RGB", "R", 1, (1, 0, 0), (2, 0, 0), (3, 0, 0)) - self.assert_unpack("RGB", "G", 1, (0, 1, 0), (0, 2, 0), (0, 3, 0)) - self.assert_unpack("RGB", "B", 1, (0, 0, 1), (0, 0, 2), (0, 0, 3)) - - self.assert_unpack("RGB", "R;16B", 2, (1, 0, 0), (3, 0, 0), (5, 0, 0)) - self.assert_unpack("RGB", "G;16B", 2, (0, 1, 0), (0, 3, 0), (0, 5, 0)) - self.assert_unpack("RGB", "B;16B", 2, (0, 0, 1), (0, 0, 3), (0, 0, 5)) - - self.assert_unpack("RGB", "R;16L", 2, (2, 0, 0), (4, 0, 0), (6, 0, 0)) - self.assert_unpack("RGB", "G;16L", 2, (0, 2, 0), (0, 4, 0), (0, 6, 0)) - self.assert_unpack("RGB", "B;16L", 2, (0, 0, 2), (0, 0, 4), (0, 0, 6)) - - if sys.byteorder == "little": - self.assert_unpack("RGB", "R;16N", 2, (2, 0, 0), (4, 0, 0), (6, 0, 0)) - self.assert_unpack("RGB", "G;16N", 2, (0, 2, 0), (0, 4, 0), (0, 6, 0)) - self.assert_unpack("RGB", "B;16N", 2, (0, 0, 2), (0, 0, 4), (0, 0, 6)) - else: - self.assert_unpack("RGB", "R;16N", 2, (1, 0, 0), (3, 0, 0), (5, 0, 0)) - self.assert_unpack("RGB", "G;16N", 2, (0, 1, 0), (0, 3, 0), (0, 5, 0)) - self.assert_unpack("RGB", "B;16N", 2, (0, 0, 1), (0, 0, 3), (0, 0, 5)) - - self.assert_unpack( - "RGB", "CMYK", 4, (250, 249, 248), (242, 241, 240), (234, 233, 233) - ) - - def test_BGR(self) -> None: - with pytest.warns(DeprecationWarning): - self.assert_unpack( - "BGR;15", "BGR;15", 3, (8, 131, 0), (24, 0, 8), (41, 131, 8) - ) - self.assert_unpack( - "BGR;16", "BGR;16", 3, (8, 64, 0), (24, 129, 0), (41, 194, 0) - ) - self.assert_unpack("BGR;24", "BGR;24", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) - - def test_RGBA(self) -> None: - self.assert_unpack("RGBA", "LA", 2, (1, 1, 1, 2), (3, 3, 3, 4), (5, 5, 5, 6)) - self.assert_unpack( - "RGBA", "LA;16B", 4, (1, 1, 1, 3), (5, 5, 5, 7), (9, 9, 9, 11) - ) - self.assert_unpack( - "RGBA", "RGBA", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12) - ) - self.assert_unpack( - "RGBA", "RGBAX", 5, (1, 2, 3, 4), (6, 7, 8, 9), (11, 12, 13, 14) - ) - self.assert_unpack( - "RGBA", "RGBAXX", 6, (1, 2, 3, 4), (7, 8, 9, 10), (13, 14, 15, 16) - ) - self.assert_unpack( - "RGBA", - "RGBa", - 4, - (63, 127, 191, 4), - (159, 191, 223, 8), - (191, 212, 233, 12), - ) - self.assert_unpack( - "RGBA", - "RGBa", - b"\x01\x02\x03\x00\x10\x20\x30\x7f\x10\x20\x30\xff", - (0, 0, 0, 0), - (32, 64, 96, 127), - (16, 32, 48, 255), - ) - self.assert_unpack( - "RGBA", - "RGBaX", - b"\x01\x02\x03\x00-\x10\x20\x30\x7f-\x10\x20\x30\xff-", - (0, 0, 0, 0), - (32, 64, 96, 127), - (16, 32, 48, 255), - ) - self.assert_unpack( - "RGBA", - "RGBaXX", - b"\x01\x02\x03\x00==\x10\x20\x30\x7f!!\x10\x20\x30\xff??", - (0, 0, 0, 0), - (32, 64, 96, 127), - (16, 32, 48, 255), - ) - self.assert_unpack( - "RGBA", - "RGBa;16L", - 8, - (63, 127, 191, 8), - (159, 191, 223, 16), - (191, 212, 233, 24), - ) - self.assert_unpack( - "RGBA", - "RGBa;16L", - b"\x88\x01\x88\x02\x88\x03\x88\x00\x88\x10\x88\x20\x88\x30\x88\xff", - (0, 0, 0, 0), - (16, 32, 48, 255), - ) - self.assert_unpack( - "RGBA", - "RGBa;16B", - 8, - (36, 109, 182, 7), - (153, 187, 221, 15), - (188, 210, 232, 23), - ) - self.assert_unpack( - "RGBA", - "RGBa;16B", - b"\x01\x88\x02\x88\x03\x88\x00\x88\x10\x88\x20\x88\x30\x88\xff\x88", - (0, 0, 0, 0), - (16, 32, 48, 255), - ) - self.assert_unpack( - "RGBA", - "BGRa", - 4, - (191, 127, 63, 4), - (223, 191, 159, 8), - (233, 212, 191, 12), - ) - self.assert_unpack( - "RGBA", - "BGRa", - b"\x01\x02\x03\x00\x10\x20\x30\xff", - (0, 0, 0, 0), - (48, 32, 16, 255), - ) - self.assert_unpack( - "RGBA", - "RGBA;I", - 4, - (254, 253, 252, 4), - (250, 249, 248, 8), - (246, 245, 244, 12), - ) - self.assert_unpack( - "RGBA", "RGBA;L", 4, (1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12) - ) - self.assert_unpack("RGBA", "RGBA;15", 2, (8, 131, 0, 0), (24, 0, 8, 0)) - self.assert_unpack("RGBA", "BGRA;15", 2, (0, 131, 8, 0), (8, 0, 24, 0)) - self.assert_unpack("RGBA", "RGBA;4B", 2, (17, 0, 34, 0), (51, 0, 68, 0)) - self.assert_unpack("RGBA", "RGBA;16L", 8, (2, 4, 6, 8), (10, 12, 14, 16)) - self.assert_unpack("RGBA", "RGBA;16B", 8, (1, 3, 5, 7), (9, 11, 13, 15)) - self.assert_unpack("RGBA", "BGRA;16L", 8, (6, 4, 2, 8), (14, 12, 10, 16)) - self.assert_unpack("RGBA", "BGRA;16B", 8, (5, 3, 1, 7), (13, 11, 9, 15)) - self.assert_unpack( - "RGBA", "BGRA", 4, (3, 2, 1, 4), (7, 6, 5, 8), (11, 10, 9, 12) - ) - self.assert_unpack( - "RGBA", "ARGB", 4, (2, 3, 4, 1), (6, 7, 8, 5), (10, 11, 12, 9) - ) - self.assert_unpack( - "RGBA", "ABGR", 4, (4, 3, 2, 1), (8, 7, 6, 5), (12, 11, 10, 9) - ) - self.assert_unpack( - "RGBA", - "YCCA;P", - b"]bE\x04\xdd\xbej\xed57T\xce\xac\xce:\x11", # random data - (0, 161, 0, 4), - (255, 255, 255, 237), - (27, 158, 0, 206), - (0, 118, 0, 17), - ) - self.assert_unpack("RGBA", "R", 1, (1, 0, 0, 0), (2, 0, 0, 0), (3, 0, 0, 0)) - self.assert_unpack("RGBA", "G", 1, (0, 1, 0, 0), (0, 2, 0, 0), (0, 3, 0, 0)) - self.assert_unpack("RGBA", "B", 1, (0, 0, 1, 0), (0, 0, 2, 0), (0, 0, 3, 0)) - self.assert_unpack("RGBA", "A", 1, (0, 0, 0, 1), (0, 0, 0, 2), (0, 0, 0, 3)) - - self.assert_unpack("RGBA", "R;16B", 2, (1, 0, 0, 0), (3, 0, 0, 0), (5, 0, 0, 0)) - self.assert_unpack("RGBA", "G;16B", 2, (0, 1, 0, 0), (0, 3, 0, 0), (0, 5, 0, 0)) - self.assert_unpack("RGBA", "B;16B", 2, (0, 0, 1, 0), (0, 0, 3, 0), (0, 0, 5, 0)) - self.assert_unpack("RGBA", "A;16B", 2, (0, 0, 0, 1), (0, 0, 0, 3), (0, 0, 0, 5)) - - self.assert_unpack("RGBA", "R;16L", 2, (2, 0, 0, 0), (4, 0, 0, 0), (6, 0, 0, 0)) - self.assert_unpack("RGBA", "G;16L", 2, (0, 2, 0, 0), (0, 4, 0, 0), (0, 6, 0, 0)) - self.assert_unpack("RGBA", "B;16L", 2, (0, 0, 2, 0), (0, 0, 4, 0), (0, 0, 6, 0)) - self.assert_unpack("RGBA", "A;16L", 2, (0, 0, 0, 2), (0, 0, 0, 4), (0, 0, 0, 6)) - - if sys.byteorder == "little": - self.assert_unpack( - "RGBA", "R;16N", 2, (2, 0, 0, 0), (4, 0, 0, 0), (6, 0, 0, 0) - ) - self.assert_unpack( - "RGBA", "G;16N", 2, (0, 2, 0, 0), (0, 4, 0, 0), (0, 6, 0, 0) - ) - self.assert_unpack( - "RGBA", "B;16N", 2, (0, 0, 2, 0), (0, 0, 4, 0), (0, 0, 6, 0) - ) - self.assert_unpack( - "RGBA", "A;16N", 2, (0, 0, 0, 2), (0, 0, 0, 4), (0, 0, 0, 6) - ) - else: - self.assert_unpack( - "RGBA", "R;16N", 2, (1, 0, 0, 0), (3, 0, 0, 0), (5, 0, 0, 0) - ) - self.assert_unpack( - "RGBA", "G;16N", 2, (0, 1, 0, 0), (0, 3, 0, 0), (0, 5, 0, 0) - ) - self.assert_unpack( - "RGBA", "B;16N", 2, (0, 0, 1, 0), (0, 0, 3, 0), (0, 0, 5, 0) - ) - self.assert_unpack( - "RGBA", "A;16N", 2, (0, 0, 0, 1), (0, 0, 0, 3), (0, 0, 0, 5) - ) - - def test_RGBa(self) -> None: - self.assert_unpack( - "RGBa", "RGBa", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12) - ) - self.assert_unpack( - "RGBa", "BGRa", 4, (3, 2, 1, 4), (7, 6, 5, 8), (11, 10, 9, 12) - ) - self.assert_unpack( - "RGBa", "aRGB", 4, (2, 3, 4, 1), (6, 7, 8, 5), (10, 11, 12, 9) - ) - self.assert_unpack( - "RGBa", "aBGR", 4, (4, 3, 2, 1), (8, 7, 6, 5), (12, 11, 10, 9) - ) - - def test_RGBX(self) -> None: - self.assert_unpack("RGBX", "RGB", 3, (1, 2, 3, X), (4, 5, 6, X), (7, 8, 9, X)) - self.assert_unpack("RGBX", "RGB;L", 3, (1, 4, 7, X), (2, 5, 8, X), (3, 6, 9, X)) - self.assert_unpack("RGBX", "RGB;16B", 6, (1, 3, 5, X), (7, 9, 11, X)) - self.assert_unpack("RGBX", "BGR", 3, (3, 2, 1, X), (6, 5, 4, X), (9, 8, 7, X)) - self.assert_unpack("RGBX", "RGB;15", 2, (8, 131, 0, X), (24, 0, 8, X)) - self.assert_unpack("RGBX", "BGR;15", 2, (0, 131, 8, X), (8, 0, 24, X)) - self.assert_unpack("RGBX", "RGB;4B", 2, (17, 0, 34, X), (51, 0, 68, X)) - self.assert_unpack( - "RGBX", "RGBX", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12) - ) - self.assert_unpack( - "RGBX", "RGBXX", 5, (1, 2, 3, 4), (6, 7, 8, 9), (11, 12, 13, 14) - ) - self.assert_unpack( - "RGBX", "RGBXXX", 6, (1, 2, 3, 4), (7, 8, 9, 10), (13, 14, 15, 16) - ) - self.assert_unpack( - "RGBX", "RGBX;L", 4, (1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12) - ) - self.assert_unpack("RGBX", "RGBX;16L", 8, (2, 4, 6, 8), (10, 12, 14, 16)) - self.assert_unpack("RGBX", "RGBX;16B", 8, (1, 3, 5, 7), (9, 11, 13, 15)) - self.assert_unpack( - "RGBX", "BGRX", 4, (3, 2, 1, X), (7, 6, 5, X), (11, 10, 9, X) - ) - self.assert_unpack( - "RGBX", "XRGB", 4, (2, 3, 4, X), (6, 7, 8, X), (10, 11, 12, X) - ) - self.assert_unpack( - "RGBX", "XBGR", 4, (4, 3, 2, X), (8, 7, 6, X), (12, 11, 10, X) - ) - self.assert_unpack( - "RGBX", - "YCC;P", - b"D]\x9c\x82\x1a\x91\xfaOC\xe7J\x12", # random data - (127, 102, 0, X), - (192, 227, 0, X), - (213, 255, 170, X), - (98, 255, 133, X), - ) - self.assert_unpack("RGBX", "R", 1, (1, 0, 0, 0), (2, 0, 0, 0), (3, 0, 0, 0)) - self.assert_unpack("RGBX", "G", 1, (0, 1, 0, 0), (0, 2, 0, 0), (0, 3, 0, 0)) - self.assert_unpack("RGBX", "B", 1, (0, 0, 1, 0), (0, 0, 2, 0), (0, 0, 3, 0)) - self.assert_unpack("RGBX", "X", 1, (0, 0, 0, 1), (0, 0, 0, 2), (0, 0, 0, 3)) - - def test_CMYK(self) -> None: - self.assert_unpack( - "CMYK", "CMYK", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12) - ) - self.assert_unpack( - "CMYK", "CMYKX", 5, (1, 2, 3, 4), (6, 7, 8, 9), (11, 12, 13, 14) - ) - self.assert_unpack( - "CMYK", "CMYKXX", 6, (1, 2, 3, 4), (7, 8, 9, 10), (13, 14, 15, 16) - ) - self.assert_unpack( - "CMYK", - "CMYK;I", - 4, - (254, 253, 252, 251), - (250, 249, 248, 247), - (246, 245, 244, 243), - ) - self.assert_unpack( - "CMYK", "CMYK;L", 4, (1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12) - ) - self.assert_unpack("CMYK", "C", 1, (1, 0, 0, 0), (2, 0, 0, 0), (3, 0, 0, 0)) - self.assert_unpack("CMYK", "M", 1, (0, 1, 0, 0), (0, 2, 0, 0), (0, 3, 0, 0)) - self.assert_unpack("CMYK", "Y", 1, (0, 0, 1, 0), (0, 0, 2, 0), (0, 0, 3, 0)) - self.assert_unpack("CMYK", "K", 1, (0, 0, 0, 1), (0, 0, 0, 2), (0, 0, 0, 3)) - self.assert_unpack( - "CMYK", "C;I", 1, (254, 0, 0, 0), (253, 0, 0, 0), (252, 0, 0, 0) - ) - self.assert_unpack( - "CMYK", "M;I", 1, (0, 254, 0, 0), (0, 253, 0, 0), (0, 252, 0, 0) - ) - self.assert_unpack( - "CMYK", "Y;I", 1, (0, 0, 254, 0), (0, 0, 253, 0), (0, 0, 252, 0) - ) - self.assert_unpack( - "CMYK", "K;I", 1, (0, 0, 0, 254), (0, 0, 0, 253), (0, 0, 0, 252) - ) - - def test_YCbCr(self) -> None: - self.assert_unpack("YCbCr", "YCbCr", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) - self.assert_unpack("YCbCr", "YCbCr;L", 3, (1, 4, 7), (2, 5, 8), (3, 6, 9)) - self.assert_unpack("YCbCr", "YCbCrK", 4, (1, 2, 3), (5, 6, 7), (9, 10, 11)) - self.assert_unpack("YCbCr", "YCbCrX", 4, (1, 2, 3), (5, 6, 7), (9, 10, 11)) - - def test_LAB(self) -> None: - self.assert_unpack("LAB", "LAB", 3, (1, 130, 131), (4, 133, 134), (7, 136, 137)) - self.assert_unpack("LAB", "L", 1, (1, 0, 0), (2, 0, 0), (3, 0, 0)) - self.assert_unpack("LAB", "A", 1, (0, 1, 0), (0, 2, 0), (0, 3, 0)) - self.assert_unpack("LAB", "B", 1, (0, 0, 1), (0, 0, 2), (0, 0, 3)) - - def test_HSV(self) -> None: - self.assert_unpack("HSV", "HSV", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) - self.assert_unpack("HSV", "H", 1, (1, 0, 0), (2, 0, 0), (3, 0, 0)) - self.assert_unpack("HSV", "S", 1, (0, 1, 0), (0, 2, 0), (0, 3, 0)) - self.assert_unpack("HSV", "V", 1, (0, 0, 1), (0, 0, 2), (0, 0, 3)) - - def test_I(self) -> None: - self.assert_unpack("I", "I;8", 1, 0x01, 0x02, 0x03, 0x04) - self.assert_unpack("I", "I;8S", b"\x01\x83", 1, -125) - self.assert_unpack("I", "I;16", 2, 0x0201, 0x0403) - self.assert_unpack("I", "I;16S", b"\x83\x01\x01\x83", 0x0183, -31999) - self.assert_unpack("I", "I;16B", 2, 0x0102, 0x0304) - self.assert_unpack("I", "I;16BS", b"\x83\x01\x01\x83", -31999, 0x0183) - self.assert_unpack("I", "I;32", 4, 0x04030201, 0x08070605) - self.assert_unpack( - "I", "I;32S", b"\x83\x00\x00\x01\x01\x00\x00\x83", 0x01000083, -2097151999 - ) - self.assert_unpack("I", "I;32B", 4, 0x01020304, 0x05060708) - self.assert_unpack( - "I", "I;32BS", b"\x83\x00\x00\x01\x01\x00\x00\x83", -2097151999, 0x01000083 - ) - - if sys.byteorder == "little": - self.assert_unpack("I", "I", 4, 0x04030201, 0x08070605) - self.assert_unpack("I", "I;16N", 2, 0x0201, 0x0403) - self.assert_unpack("I", "I;16NS", b"\x83\x01\x01\x83", 0x0183, -31999) - self.assert_unpack("I", "I;32N", 4, 0x04030201, 0x08070605) - self.assert_unpack( - "I", - "I;32NS", - b"\x83\x00\x00\x01\x01\x00\x00\x83", - 0x01000083, - -2097151999, - ) - else: - self.assert_unpack("I", "I", 4, 0x01020304, 0x05060708) - self.assert_unpack("I", "I;16N", 2, 0x0102, 0x0304) - self.assert_unpack("I", "I;16NS", b"\x83\x01\x01\x83", -31999, 0x0183) - self.assert_unpack("I", "I;32N", 4, 0x01020304, 0x05060708) - self.assert_unpack( - "I", - "I;32NS", - b"\x83\x00\x00\x01\x01\x00\x00\x83", - -2097151999, - 0x01000083, - ) - - def test_F_int(self) -> None: - self.assert_unpack("F", "F;8", 1, 0x01, 0x02, 0x03, 0x04) - self.assert_unpack("F", "F;8S", b"\x01\x83", 1, -125) - self.assert_unpack("F", "F;16", 2, 0x0201, 0x0403) - self.assert_unpack("F", "F;16S", b"\x83\x01\x01\x83", 0x0183, -31999) - self.assert_unpack("F", "F;16B", 2, 0x0102, 0x0304) - self.assert_unpack("F", "F;16BS", b"\x83\x01\x01\x83", -31999, 0x0183) - self.assert_unpack("F", "F;32", 4, 67305984, 134678016) - self.assert_unpack( - "F", "F;32S", b"\x83\x00\x00\x01\x01\x00\x00\x83", 16777348, -2097152000 - ) - self.assert_unpack("F", "F;32B", 4, 0x01020304, 0x05060708) - self.assert_unpack( - "F", "F;32BS", b"\x83\x00\x00\x01\x01\x00\x00\x83", -2097152000, 16777348 - ) - - if sys.byteorder == "little": - self.assert_unpack("F", "F;16N", 2, 0x0201, 0x0403) - self.assert_unpack("F", "F;16NS", b"\x83\x01\x01\x83", 0x0183, -31999) - self.assert_unpack("F", "F;32N", 4, 67305984, 134678016) - self.assert_unpack( - "F", - "F;32NS", - b"\x83\x00\x00\x01\x01\x00\x00\x83", - 16777348, - -2097152000, - ) - else: - self.assert_unpack("F", "F;16N", 2, 0x0102, 0x0304) - self.assert_unpack("F", "F;16NS", b"\x83\x01\x01\x83", -31999, 0x0183) - self.assert_unpack("F", "F;32N", 4, 0x01020304, 0x05060708) - self.assert_unpack( - "F", - "F;32NS", - b"\x83\x00\x00\x01\x01\x00\x00\x83", - -2097152000, - 16777348, - ) - - def test_F_float(self) -> None: - self.assert_unpack( - "F", "F;32F", 4, 1.539989614439558e-36, 4.063216068939723e-34 - ) - self.assert_unpack( - "F", "F;32BF", 4, 2.387939260590663e-38, 6.301941157072183e-36 - ) - self.assert_unpack( - "F", - "F;64F", - b"333333\xc3?\x00\x00\x00\x00\x00J\x93\xc0", # by struct.pack - 0.15000000596046448, - -1234.5, - ) - self.assert_unpack( - "F", - "F;64BF", - b"?\xc3333333\xc0\x93J\x00\x00\x00\x00\x00", # by struct.pack - 0.15000000596046448, - -1234.5, - ) - - if sys.byteorder == "little": - self.assert_unpack( - "F", "F", 4, 1.539989614439558e-36, 4.063216068939723e-34 - ) - self.assert_unpack( - "F", "F;32NF", 4, 1.539989614439558e-36, 4.063216068939723e-34 - ) - self.assert_unpack( - "F", - "F;64NF", - b"333333\xc3?\x00\x00\x00\x00\x00J\x93\xc0", - 0.15000000596046448, - -1234.5, - ) - else: - self.assert_unpack( - "F", "F", 4, 2.387939260590663e-38, 6.301941157072183e-36 - ) - self.assert_unpack( - "F", "F;32NF", 4, 2.387939260590663e-38, 6.301941157072183e-36 - ) - self.assert_unpack( - "F", - "F;64NF", - b"?\xc3333333\xc0\x93J\x00\x00\x00\x00\x00", - 0.15000000596046448, - -1234.5, - ) - - def test_I16(self) -> None: - self.assert_unpack("I;16", "I;16", 2, 0x0201, 0x0403, 0x0605) - self.assert_unpack("I;16", "I;16B", 2, 0x0102, 0x0304, 0x0506) - self.assert_unpack("I;16B", "I;16B", 2, 0x0102, 0x0304, 0x0506) - self.assert_unpack("I;16L", "I;16L", 2, 0x0201, 0x0403, 0x0605) - self.assert_unpack("I;16", "I;12", 2, 0x0010, 0x0203, 0x0040) - if sys.byteorder == "little": - self.assert_unpack("I;16", "I;16N", 2, 0x0201, 0x0403, 0x0605) - self.assert_unpack("I;16B", "I;16N", 2, 0x0201, 0x0403, 0x0605) - self.assert_unpack("I;16L", "I;16N", 2, 0x0201, 0x0403, 0x0605) - self.assert_unpack("I;16N", "I;16N", 2, 0x0201, 0x0403, 0x0605) - else: - self.assert_unpack("I;16", "I;16N", 2, 0x0102, 0x0304, 0x0506) - self.assert_unpack("I;16B", "I;16N", 2, 0x0102, 0x0304, 0x0506) - self.assert_unpack("I;16L", "I;16N", 2, 0x0102, 0x0304, 0x0506) - self.assert_unpack("I;16N", "I;16N", 2, 0x0102, 0x0304, 0x0506) - - def test_CMYK16(self) -> None: - self.assert_unpack("CMYK", "CMYK;16L", 8, (2, 4, 6, 8), (10, 12, 14, 16)) - self.assert_unpack("CMYK", "CMYK;16B", 8, (1, 3, 5, 7), (9, 11, 13, 15)) - if sys.byteorder == "little": - self.assert_unpack("CMYK", "CMYK;16N", 8, (2, 4, 6, 8), (10, 12, 14, 16)) - else: - self.assert_unpack("CMYK", "CMYK;16N", 8, (1, 3, 5, 7), (9, 11, 13, 15)) - - def test_value_error(self) -> None: - with pytest.raises(ValueError): - self.assert_unpack("L", "L", 0, 0) - with pytest.raises(ValueError): - self.assert_unpack("RGB", "RGB", 2, 0) - with pytest.raises(ValueError): - self.assert_unpack("CMYK", "CMYK", 2, 0) +from __future__ import annotations + +import sys + +import pytest + +from PIL import Image + +X = 255 + + +class TestLibPack: + def assert_pack( + self, + mode: str, + rawmode: str, + data: int | bytes, + *pixels: float | tuple[int, ...], + ) -> None: + """ + data - either raw bytes with data or just number of bytes in rawmode. + """ + im = Image.new(mode, (len(pixels), 1)) + for x, pixel in enumerate(pixels): + im.putpixel((x, 0), pixel) + + if isinstance(data, int): + data_len = data * len(pixels) + data = bytes(range(1, data_len + 1)) + + assert data == im.tobytes("raw", rawmode) + + def test_1(self) -> None: + self.assert_pack("1", "1", b"\x01", 0, 0, 0, 0, 0, 0, 0, X) + self.assert_pack("1", "1;I", b"\x01", X, X, X, X, X, X, X, 0) + self.assert_pack("1", "1;R", b"\x01", X, 0, 0, 0, 0, 0, 0, 0) + self.assert_pack("1", "1;IR", b"\x01", 0, X, X, X, X, X, X, X) + + self.assert_pack("1", "1", b"\xaa", X, 0, X, 0, X, 0, X, 0) + self.assert_pack("1", "1;I", b"\xaa", 0, X, 0, X, 0, X, 0, X) + self.assert_pack("1", "1;R", b"\xaa", 0, X, 0, X, 0, X, 0, X) + self.assert_pack("1", "1;IR", b"\xaa", X, 0, X, 0, X, 0, X, 0) + + self.assert_pack("1", "L", b"\xff\x00\x00\xff\x00\x00", X, 0, 0, X, 0, 0) + + def test_L(self) -> None: + self.assert_pack("L", "L", 1, 1, 2, 3, 4) + self.assert_pack("L", "L;16", b"\x00\xc6\x00\xaf", 198, 175) + self.assert_pack("L", "L;16B", b"\xc6\x00\xaf\x00", 198, 175) + + def test_LA(self) -> None: + self.assert_pack("LA", "LA", 2, (1, 2), (3, 4), (5, 6)) + self.assert_pack("LA", "LA;L", 2, (1, 4), (2, 5), (3, 6)) + + def test_La(self) -> None: + self.assert_pack("La", "La", 2, (1, 2), (3, 4), (5, 6)) + + def test_P(self) -> None: + self.assert_pack("P", "P;1", b"\xe4", 1, 1, 1, 0, 0, 255, 0, 0) + self.assert_pack("P", "P;2", b"\xe4", 3, 2, 1, 0) + self.assert_pack("P", "P;4", b"\x02\xef", 0, 2, 14, 15) + self.assert_pack("P", "P", 1, 1, 2, 3, 4) + + def test_PA(self) -> None: + self.assert_pack("PA", "PA", 2, (1, 2), (3, 4), (5, 6)) + self.assert_pack("PA", "PA;L", 2, (1, 4), (2, 5), (3, 6)) + + def test_RGB(self) -> None: + self.assert_pack("RGB", "RGB", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) + self.assert_pack( + "RGB", "RGBX", b"\x01\x02\x03\xff\x05\x06\x07\xff", (1, 2, 3), (5, 6, 7) + ) + self.assert_pack( + "RGB", "XRGB", b"\x00\x02\x03\x04\x00\x06\x07\x08", (2, 3, 4), (6, 7, 8) + ) + self.assert_pack("RGB", "BGR", 3, (3, 2, 1), (6, 5, 4), (9, 8, 7)) + self.assert_pack( + "RGB", "BGRX", b"\x01\x02\x03\x00\x05\x06\x07\x00", (3, 2, 1), (7, 6, 5) + ) + self.assert_pack( + "RGB", "XBGR", b"\x00\x02\x03\x04\x00\x06\x07\x08", (4, 3, 2), (8, 7, 6) + ) + self.assert_pack("RGB", "RGB;L", 3, (1, 4, 7), (2, 5, 8), (3, 6, 9)) + self.assert_pack("RGB", "R", 1, (1, 9, 9), (2, 9, 9), (3, 9, 9)) + self.assert_pack("RGB", "G", 1, (9, 1, 9), (9, 2, 9), (9, 3, 9)) + self.assert_pack("RGB", "B", 1, (9, 9, 1), (9, 9, 2), (9, 9, 3)) + + def test_RGBA(self) -> None: + self.assert_pack("RGBA", "RGBA", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) + self.assert_pack( + "RGBA", "RGBA;L", 4, (1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12) + ) + self.assert_pack("RGBA", "RGB", 3, (1, 2, 3, 14), (4, 5, 6, 15), (7, 8, 9, 16)) + self.assert_pack("RGBA", "BGR", 3, (3, 2, 1, 14), (6, 5, 4, 15), (9, 8, 7, 16)) + self.assert_pack("RGBA", "BGRA", 4, (3, 2, 1, 4), (7, 6, 5, 8), (11, 10, 9, 12)) + self.assert_pack("RGBA", "ABGR", 4, (4, 3, 2, 1), (8, 7, 6, 5), (12, 11, 10, 9)) + self.assert_pack( + "RGBA", + "BGRa", + 4, + (191, 127, 63, 4), + (223, 191, 159, 8), + (233, 212, 191, 12), + ) + self.assert_pack("RGBA", "R", 1, (1, 0, 8, 9), (2, 0, 8, 9), (3, 0, 8, 0)) + self.assert_pack("RGBA", "G", 1, (6, 1, 8, 9), (6, 2, 8, 9), (6, 3, 8, 9)) + self.assert_pack("RGBA", "B", 1, (6, 7, 1, 9), (6, 7, 2, 0), (6, 7, 3, 9)) + self.assert_pack("RGBA", "A", 1, (6, 7, 0, 1), (6, 7, 0, 2), (0, 7, 0, 3)) + + def test_RGBa(self) -> None: + self.assert_pack("RGBa", "RGBa", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) + self.assert_pack("RGBa", "BGRa", 4, (3, 2, 1, 4), (7, 6, 5, 8), (11, 10, 9, 12)) + self.assert_pack("RGBa", "aBGR", 4, (4, 3, 2, 1), (8, 7, 6, 5), (12, 11, 10, 9)) + + def test_RGBX(self) -> None: + self.assert_pack("RGBX", "RGBX", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) + self.assert_pack( + "RGBX", "RGBX;L", 4, (1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12) + ) + self.assert_pack("RGBX", "RGB", 3, (1, 2, 3, X), (4, 5, 6, X), (7, 8, 9, X)) + self.assert_pack("RGBX", "BGR", 3, (3, 2, 1, X), (6, 5, 4, X), (9, 8, 7, X)) + self.assert_pack( + "RGBX", + "BGRX", + b"\x01\x02\x03\x00\x05\x06\x07\x00\t\n\x0b\x00", + (3, 2, 1, X), + (7, 6, 5, X), + (11, 10, 9, X), + ) + self.assert_pack( + "RGBX", + "XBGR", + b"\x00\x02\x03\x04\x00\x06\x07\x08\x00\n\x0b\x0c", + (4, 3, 2, X), + (8, 7, 6, X), + (12, 11, 10, X), + ) + self.assert_pack("RGBX", "R", 1, (1, 0, 8, 9), (2, 0, 8, 9), (3, 0, 8, 0)) + self.assert_pack("RGBX", "G", 1, (6, 1, 8, 9), (6, 2, 8, 9), (6, 3, 8, 9)) + self.assert_pack("RGBX", "B", 1, (6, 7, 1, 9), (6, 7, 2, 0), (6, 7, 3, 9)) + self.assert_pack("RGBX", "X", 1, (6, 7, 0, 1), (6, 7, 0, 2), (0, 7, 0, 3)) + + def test_CMYK(self) -> None: + self.assert_pack("CMYK", "CMYK", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) + self.assert_pack( + "CMYK", + "CMYK;I", + 4, + (254, 253, 252, 251), + (250, 249, 248, 247), + (246, 245, 244, 243), + ) + self.assert_pack( + "CMYK", "CMYK;L", 4, (1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12) + ) + self.assert_pack("CMYK", "K", 1, (6, 7, 0, 1), (6, 7, 0, 2), (0, 7, 0, 3)) + + def test_YCbCr(self) -> None: + self.assert_pack("YCbCr", "YCbCr", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) + self.assert_pack("YCbCr", "YCbCr;L", 3, (1, 4, 7), (2, 5, 8), (3, 6, 9)) + self.assert_pack( + "YCbCr", + "YCbCrX", + b"\x01\x02\x03\xff\x05\x06\x07\xff\t\n\x0b\xff", + (1, 2, 3), + (5, 6, 7), + (9, 10, 11), + ) + self.assert_pack( + "YCbCr", + "YCbCrK", + b"\x01\x02\x03\xff\x05\x06\x07\xff\t\n\x0b\xff", + (1, 2, 3), + (5, 6, 7), + (9, 10, 11), + ) + self.assert_pack("YCbCr", "Y", 1, (1, 0, 8, 9), (2, 0, 8, 9), (3, 0, 8, 0)) + self.assert_pack("YCbCr", "Cb", 1, (6, 1, 8, 9), (6, 2, 8, 9), (6, 3, 8, 9)) + self.assert_pack("YCbCr", "Cr", 1, (6, 7, 1, 9), (6, 7, 2, 0), (6, 7, 3, 9)) + + def test_LAB(self) -> None: + self.assert_pack("LAB", "LAB", 3, (1, 130, 131), (4, 133, 134), (7, 136, 137)) + self.assert_pack("LAB", "L", 1, (1, 9, 9), (2, 9, 9), (3, 9, 9)) + self.assert_pack("LAB", "A", 1, (9, 1, 9), (9, 2, 9), (9, 3, 9)) + self.assert_pack("LAB", "B", 1, (9, 9, 1), (9, 9, 2), (9, 9, 3)) + + def test_HSV(self) -> None: + self.assert_pack("HSV", "HSV", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) + self.assert_pack("HSV", "H", 1, (1, 9, 9), (2, 9, 9), (3, 9, 9)) + self.assert_pack("HSV", "S", 1, (9, 1, 9), (9, 2, 9), (9, 3, 9)) + self.assert_pack("HSV", "V", 1, (9, 9, 1), (9, 9, 2), (9, 9, 3)) + + def test_I(self) -> None: + self.assert_pack("I", "I;16B", 2, 0x0102, 0x0304) + self.assert_pack( + "I", "I;32S", b"\x83\x00\x00\x01\x01\x00\x00\x83", 0x01000083, -2097151999 + ) + + if sys.byteorder == "little": + self.assert_pack("I", "I", 4, 0x04030201, 0x08070605) + self.assert_pack( + "I", + "I;32NS", + b"\x83\x00\x00\x01\x01\x00\x00\x83", + 0x01000083, + -2097151999, + ) + else: + self.assert_pack("I", "I", 4, 0x01020304, 0x05060708) + self.assert_pack( + "I", + "I;32NS", + b"\x83\x00\x00\x01\x01\x00\x00\x83", + -2097151999, + 0x01000083, + ) + + def test_I16(self) -> None: + if sys.byteorder == "little": + self.assert_pack("I;16N", "I;16N", 2, 0x0201, 0x0403, 0x0605) + else: + self.assert_pack("I;16N", "I;16N", 2, 0x0102, 0x0304, 0x0506) + + def test_F_float(self) -> None: + self.assert_pack("F", "F;32F", 4, 1.539989614439558e-36, 4.063216068939723e-34) + + if sys.byteorder == "little": + self.assert_pack("F", "F", 4, 1.539989614439558e-36, 4.063216068939723e-34) + self.assert_pack( + "F", "F;32NF", 4, 1.539989614439558e-36, 4.063216068939723e-34 + ) + else: + self.assert_pack("F", "F", 4, 2.387939260590663e-38, 6.301941157072183e-36) + self.assert_pack( + "F", "F;32NF", 4, 2.387939260590663e-38, 6.301941157072183e-36 + ) + + +class TestLibUnpack: + def assert_unpack( + self, + mode: str, + rawmode: str, + data: int | bytes, + *pixels: float | tuple[int, ...], + ) -> None: + """ + data - either raw bytes with data or just number of bytes in rawmode. + """ + if isinstance(data, int): + data_len = data * len(pixels) + data = bytes(range(1, data_len + 1)) + + im = Image.frombytes(mode, (len(pixels), 1), data, "raw", rawmode, 0, 1) + + for x, pixel in enumerate(pixels): + assert pixel == im.getpixel((x, 0)) + + def test_1(self) -> None: + self.assert_unpack("1", "1", b"\x01", 0, 0, 0, 0, 0, 0, 0, X) + self.assert_unpack("1", "1;I", b"\x01", X, X, X, X, X, X, X, 0) + self.assert_unpack("1", "1;R", b"\x01", X, 0, 0, 0, 0, 0, 0, 0) + self.assert_unpack("1", "1;IR", b"\x01", 0, X, X, X, X, X, X, X) + + self.assert_unpack("1", "1", b"\xaa", X, 0, X, 0, X, 0, X, 0) + self.assert_unpack("1", "1;I", b"\xaa", 0, X, 0, X, 0, X, 0, X) + self.assert_unpack("1", "1;R", b"\xaa", 0, X, 0, X, 0, X, 0, X) + self.assert_unpack("1", "1;IR", b"\xaa", X, 0, X, 0, X, 0, X, 0) + + self.assert_unpack("1", "1;8", b"\x00\x01\x02\xff", 0, X, X, X) + + def test_L(self) -> None: + self.assert_unpack("L", "L;2", b"\xe4", 255, 170, 85, 0) + self.assert_unpack("L", "L;2I", b"\xe4", 0, 85, 170, 255) + self.assert_unpack("L", "L;2R", b"\xe4", 0, 170, 85, 255) + self.assert_unpack("L", "L;2IR", b"\xe4", 255, 85, 170, 0) + + self.assert_unpack("L", "L;4", b"\x02\xef", 0, 34, 238, 255) + self.assert_unpack("L", "L;4I", b"\x02\xef", 255, 221, 17, 0) + self.assert_unpack("L", "L;4R", b"\x02\xef", 68, 0, 255, 119) + self.assert_unpack("L", "L;4IR", b"\x02\xef", 187, 255, 0, 136) + + self.assert_unpack("L", "L", 1, 1, 2, 3, 4) + self.assert_unpack("L", "L;I", 1, 254, 253, 252, 251) + self.assert_unpack("L", "L;R", 1, 128, 64, 192, 32) + self.assert_unpack("L", "L;16", 2, 2, 4, 6, 8) + self.assert_unpack("L", "L;16B", 2, 1, 3, 5, 7) + self.assert_unpack("L", "L;16", b"\x00\xc6\x00\xaf", 198, 175) + self.assert_unpack("L", "L;16B", b"\xc6\x00\xaf\x00", 198, 175) + + def test_LA(self) -> None: + self.assert_unpack("LA", "LA", 2, (1, 2), (3, 4), (5, 6)) + self.assert_unpack("LA", "LA;L", 2, (1, 4), (2, 5), (3, 6)) + + def test_La(self) -> None: + self.assert_unpack("La", "La", 2, (1, 2), (3, 4), (5, 6)) + + def test_P(self) -> None: + self.assert_unpack("P", "P;1", b"\xe4", 1, 1, 1, 0, 0, 1, 0, 0) + self.assert_unpack("P", "P;2", b"\xe4", 3, 2, 1, 0) + # erroneous? + # self.assert_unpack("P", "P;2L", b'\xe4', 1, 1, 1, 0) + self.assert_unpack("P", "P;4", b"\x02\xef", 0, 2, 14, 15) + # erroneous? + # self.assert_unpack("P", "P;4L", b'\x02\xef', 2, 10, 10, 0) + self.assert_unpack("P", "P", 1, 1, 2, 3, 4) + self.assert_unpack("P", "P;R", 1, 128, 64, 192, 32) + + def test_PA(self) -> None: + self.assert_unpack("PA", "PA", 2, (1, 2), (3, 4), (5, 6)) + self.assert_unpack("PA", "PA;L", 2, (1, 4), (2, 5), (3, 6)) + + def test_RGB(self) -> None: + self.assert_unpack("RGB", "RGB", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) + self.assert_unpack("RGB", "RGB;L", 3, (1, 4, 7), (2, 5, 8), (3, 6, 9)) + self.assert_unpack("RGB", "RGB;R", 3, (128, 64, 192), (32, 160, 96)) + self.assert_unpack("RGB", "RGB;16L", 6, (2, 4, 6), (8, 10, 12)) + self.assert_unpack("RGB", "RGB;16B", 6, (1, 3, 5), (7, 9, 11)) + self.assert_unpack("RGB", "BGR", 3, (3, 2, 1), (6, 5, 4), (9, 8, 7)) + self.assert_unpack("RGB", "RGB;15", 2, (8, 131, 0), (24, 0, 8)) + self.assert_unpack("RGB", "BGR;15", 2, (0, 131, 8), (8, 0, 24)) + self.assert_unpack("RGB", "RGB;16", 2, (8, 64, 0), (24, 129, 0)) + self.assert_unpack("RGB", "BGR;16", 2, (0, 64, 8), (0, 129, 24)) + self.assert_unpack("RGB", "RGB;4B", 2, (17, 0, 34), (51, 0, 68)) + self.assert_unpack("RGB", "RGBX", 4, (1, 2, 3), (5, 6, 7), (9, 10, 11)) + self.assert_unpack("RGB", "RGBX;L", 4, (1, 4, 7), (2, 5, 8), (3, 6, 9)) + self.assert_unpack("RGB", "BGRX", 4, (3, 2, 1), (7, 6, 5), (11, 10, 9)) + self.assert_unpack("RGB", "XRGB", 4, (2, 3, 4), (6, 7, 8), (10, 11, 12)) + self.assert_unpack("RGB", "XBGR", 4, (4, 3, 2), (8, 7, 6), (12, 11, 10)) + self.assert_unpack( + "RGB", + "YCC;P", + b"D]\x9c\x82\x1a\x91\xfaOC\xe7J\x12", # random data + (127, 102, 0), + (192, 227, 0), + (213, 255, 170), + (98, 255, 133), + ) + self.assert_unpack("RGB", "R", 1, (1, 0, 0), (2, 0, 0), (3, 0, 0)) + self.assert_unpack("RGB", "G", 1, (0, 1, 0), (0, 2, 0), (0, 3, 0)) + self.assert_unpack("RGB", "B", 1, (0, 0, 1), (0, 0, 2), (0, 0, 3)) + + self.assert_unpack("RGB", "R;16B", 2, (1, 0, 0), (3, 0, 0), (5, 0, 0)) + self.assert_unpack("RGB", "G;16B", 2, (0, 1, 0), (0, 3, 0), (0, 5, 0)) + self.assert_unpack("RGB", "B;16B", 2, (0, 0, 1), (0, 0, 3), (0, 0, 5)) + + self.assert_unpack("RGB", "R;16L", 2, (2, 0, 0), (4, 0, 0), (6, 0, 0)) + self.assert_unpack("RGB", "G;16L", 2, (0, 2, 0), (0, 4, 0), (0, 6, 0)) + self.assert_unpack("RGB", "B;16L", 2, (0, 0, 2), (0, 0, 4), (0, 0, 6)) + + if sys.byteorder == "little": + self.assert_unpack("RGB", "R;16N", 2, (2, 0, 0), (4, 0, 0), (6, 0, 0)) + self.assert_unpack("RGB", "G;16N", 2, (0, 2, 0), (0, 4, 0), (0, 6, 0)) + self.assert_unpack("RGB", "B;16N", 2, (0, 0, 2), (0, 0, 4), (0, 0, 6)) + else: + self.assert_unpack("RGB", "R;16N", 2, (1, 0, 0), (3, 0, 0), (5, 0, 0)) + self.assert_unpack("RGB", "G;16N", 2, (0, 1, 0), (0, 3, 0), (0, 5, 0)) + self.assert_unpack("RGB", "B;16N", 2, (0, 0, 1), (0, 0, 3), (0, 0, 5)) + + self.assert_unpack( + "RGB", "CMYK", 4, (250, 249, 248), (242, 241, 240), (234, 233, 233) + ) + + def test_BGR(self) -> None: + with pytest.warns(DeprecationWarning): + self.assert_unpack( + "BGR;15", "BGR;15", 3, (8, 131, 0), (24, 0, 8), (41, 131, 8) + ) + self.assert_unpack( + "BGR;16", "BGR;16", 3, (8, 64, 0), (24, 129, 0), (41, 194, 0) + ) + self.assert_unpack("BGR;24", "BGR;24", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) + + def test_RGBA(self) -> None: + self.assert_unpack("RGBA", "LA", 2, (1, 1, 1, 2), (3, 3, 3, 4), (5, 5, 5, 6)) + self.assert_unpack( + "RGBA", "LA;16B", 4, (1, 1, 1, 3), (5, 5, 5, 7), (9, 9, 9, 11) + ) + self.assert_unpack( + "RGBA", "RGBA", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12) + ) + self.assert_unpack( + "RGBA", "RGBAX", 5, (1, 2, 3, 4), (6, 7, 8, 9), (11, 12, 13, 14) + ) + self.assert_unpack( + "RGBA", "RGBAXX", 6, (1, 2, 3, 4), (7, 8, 9, 10), (13, 14, 15, 16) + ) + self.assert_unpack( + "RGBA", + "RGBa", + 4, + (63, 127, 191, 4), + (159, 191, 223, 8), + (191, 212, 233, 12), + ) + self.assert_unpack( + "RGBA", + "RGBa", + b"\x01\x02\x03\x00\x10\x20\x30\x7f\x10\x20\x30\xff", + (0, 0, 0, 0), + (32, 64, 96, 127), + (16, 32, 48, 255), + ) + self.assert_unpack( + "RGBA", + "RGBaX", + b"\x01\x02\x03\x00-\x10\x20\x30\x7f-\x10\x20\x30\xff-", + (0, 0, 0, 0), + (32, 64, 96, 127), + (16, 32, 48, 255), + ) + self.assert_unpack( + "RGBA", + "RGBaXX", + b"\x01\x02\x03\x00==\x10\x20\x30\x7f!!\x10\x20\x30\xff??", + (0, 0, 0, 0), + (32, 64, 96, 127), + (16, 32, 48, 255), + ) + self.assert_unpack( + "RGBA", + "RGBa;16L", + 8, + (63, 127, 191, 8), + (159, 191, 223, 16), + (191, 212, 233, 24), + ) + self.assert_unpack( + "RGBA", + "RGBa;16L", + b"\x88\x01\x88\x02\x88\x03\x88\x00\x88\x10\x88\x20\x88\x30\x88\xff", + (0, 0, 0, 0), + (16, 32, 48, 255), + ) + self.assert_unpack( + "RGBA", + "RGBa;16B", + 8, + (36, 109, 182, 7), + (153, 187, 221, 15), + (188, 210, 232, 23), + ) + self.assert_unpack( + "RGBA", + "RGBa;16B", + b"\x01\x88\x02\x88\x03\x88\x00\x88\x10\x88\x20\x88\x30\x88\xff\x88", + (0, 0, 0, 0), + (16, 32, 48, 255), + ) + self.assert_unpack( + "RGBA", + "BGRa", + 4, + (191, 127, 63, 4), + (223, 191, 159, 8), + (233, 212, 191, 12), + ) + self.assert_unpack( + "RGBA", + "BGRa", + b"\x01\x02\x03\x00\x10\x20\x30\xff", + (0, 0, 0, 0), + (48, 32, 16, 255), + ) + self.assert_unpack( + "RGBA", + "RGBA;I", + 4, + (254, 253, 252, 4), + (250, 249, 248, 8), + (246, 245, 244, 12), + ) + self.assert_unpack( + "RGBA", "RGBA;L", 4, (1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12) + ) + self.assert_unpack("RGBA", "RGBA;15", 2, (8, 131, 0, 0), (24, 0, 8, 0)) + self.assert_unpack("RGBA", "BGRA;15", 2, (0, 131, 8, 0), (8, 0, 24, 0)) + self.assert_unpack("RGBA", "RGBA;4B", 2, (17, 0, 34, 0), (51, 0, 68, 0)) + self.assert_unpack("RGBA", "RGBA;16L", 8, (2, 4, 6, 8), (10, 12, 14, 16)) + self.assert_unpack("RGBA", "RGBA;16B", 8, (1, 3, 5, 7), (9, 11, 13, 15)) + self.assert_unpack("RGBA", "BGRA;16L", 8, (6, 4, 2, 8), (14, 12, 10, 16)) + self.assert_unpack("RGBA", "BGRA;16B", 8, (5, 3, 1, 7), (13, 11, 9, 15)) + self.assert_unpack( + "RGBA", "BGRA", 4, (3, 2, 1, 4), (7, 6, 5, 8), (11, 10, 9, 12) + ) + self.assert_unpack( + "RGBA", "ARGB", 4, (2, 3, 4, 1), (6, 7, 8, 5), (10, 11, 12, 9) + ) + self.assert_unpack( + "RGBA", "ABGR", 4, (4, 3, 2, 1), (8, 7, 6, 5), (12, 11, 10, 9) + ) + self.assert_unpack( + "RGBA", + "YCCA;P", + b"]bE\x04\xdd\xbej\xed57T\xce\xac\xce:\x11", # random data + (0, 161, 0, 4), + (255, 255, 255, 237), + (27, 158, 0, 206), + (0, 118, 0, 17), + ) + self.assert_unpack("RGBA", "R", 1, (1, 0, 0, 0), (2, 0, 0, 0), (3, 0, 0, 0)) + self.assert_unpack("RGBA", "G", 1, (0, 1, 0, 0), (0, 2, 0, 0), (0, 3, 0, 0)) + self.assert_unpack("RGBA", "B", 1, (0, 0, 1, 0), (0, 0, 2, 0), (0, 0, 3, 0)) + self.assert_unpack("RGBA", "A", 1, (0, 0, 0, 1), (0, 0, 0, 2), (0, 0, 0, 3)) + + self.assert_unpack("RGBA", "R;16B", 2, (1, 0, 0, 0), (3, 0, 0, 0), (5, 0, 0, 0)) + self.assert_unpack("RGBA", "G;16B", 2, (0, 1, 0, 0), (0, 3, 0, 0), (0, 5, 0, 0)) + self.assert_unpack("RGBA", "B;16B", 2, (0, 0, 1, 0), (0, 0, 3, 0), (0, 0, 5, 0)) + self.assert_unpack("RGBA", "A;16B", 2, (0, 0, 0, 1), (0, 0, 0, 3), (0, 0, 0, 5)) + + self.assert_unpack("RGBA", "R;16L", 2, (2, 0, 0, 0), (4, 0, 0, 0), (6, 0, 0, 0)) + self.assert_unpack("RGBA", "G;16L", 2, (0, 2, 0, 0), (0, 4, 0, 0), (0, 6, 0, 0)) + self.assert_unpack("RGBA", "B;16L", 2, (0, 0, 2, 0), (0, 0, 4, 0), (0, 0, 6, 0)) + self.assert_unpack("RGBA", "A;16L", 2, (0, 0, 0, 2), (0, 0, 0, 4), (0, 0, 0, 6)) + + if sys.byteorder == "little": + self.assert_unpack( + "RGBA", "R;16N", 2, (2, 0, 0, 0), (4, 0, 0, 0), (6, 0, 0, 0) + ) + self.assert_unpack( + "RGBA", "G;16N", 2, (0, 2, 0, 0), (0, 4, 0, 0), (0, 6, 0, 0) + ) + self.assert_unpack( + "RGBA", "B;16N", 2, (0, 0, 2, 0), (0, 0, 4, 0), (0, 0, 6, 0) + ) + self.assert_unpack( + "RGBA", "A;16N", 2, (0, 0, 0, 2), (0, 0, 0, 4), (0, 0, 0, 6) + ) + else: + self.assert_unpack( + "RGBA", "R;16N", 2, (1, 0, 0, 0), (3, 0, 0, 0), (5, 0, 0, 0) + ) + self.assert_unpack( + "RGBA", "G;16N", 2, (0, 1, 0, 0), (0, 3, 0, 0), (0, 5, 0, 0) + ) + self.assert_unpack( + "RGBA", "B;16N", 2, (0, 0, 1, 0), (0, 0, 3, 0), (0, 0, 5, 0) + ) + self.assert_unpack( + "RGBA", "A;16N", 2, (0, 0, 0, 1), (0, 0, 0, 3), (0, 0, 0, 5) + ) + + def test_RGBa(self) -> None: + self.assert_unpack( + "RGBa", "RGBa", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12) + ) + self.assert_unpack( + "RGBa", "BGRa", 4, (3, 2, 1, 4), (7, 6, 5, 8), (11, 10, 9, 12) + ) + self.assert_unpack( + "RGBa", "aRGB", 4, (2, 3, 4, 1), (6, 7, 8, 5), (10, 11, 12, 9) + ) + self.assert_unpack( + "RGBa", "aBGR", 4, (4, 3, 2, 1), (8, 7, 6, 5), (12, 11, 10, 9) + ) + + def test_RGBX(self) -> None: + self.assert_unpack("RGBX", "RGB", 3, (1, 2, 3, X), (4, 5, 6, X), (7, 8, 9, X)) + self.assert_unpack("RGBX", "RGB;L", 3, (1, 4, 7, X), (2, 5, 8, X), (3, 6, 9, X)) + self.assert_unpack("RGBX", "RGB;16B", 6, (1, 3, 5, X), (7, 9, 11, X)) + self.assert_unpack("RGBX", "BGR", 3, (3, 2, 1, X), (6, 5, 4, X), (9, 8, 7, X)) + self.assert_unpack("RGBX", "RGB;15", 2, (8, 131, 0, X), (24, 0, 8, X)) + self.assert_unpack("RGBX", "BGR;15", 2, (0, 131, 8, X), (8, 0, 24, X)) + self.assert_unpack("RGBX", "RGB;4B", 2, (17, 0, 34, X), (51, 0, 68, X)) + self.assert_unpack( + "RGBX", "RGBX", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12) + ) + self.assert_unpack( + "RGBX", "RGBXX", 5, (1, 2, 3, 4), (6, 7, 8, 9), (11, 12, 13, 14) + ) + self.assert_unpack( + "RGBX", "RGBXXX", 6, (1, 2, 3, 4), (7, 8, 9, 10), (13, 14, 15, 16) + ) + self.assert_unpack( + "RGBX", "RGBX;L", 4, (1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12) + ) + self.assert_unpack("RGBX", "RGBX;16L", 8, (2, 4, 6, 8), (10, 12, 14, 16)) + self.assert_unpack("RGBX", "RGBX;16B", 8, (1, 3, 5, 7), (9, 11, 13, 15)) + self.assert_unpack( + "RGBX", "BGRX", 4, (3, 2, 1, X), (7, 6, 5, X), (11, 10, 9, X) + ) + self.assert_unpack( + "RGBX", "XRGB", 4, (2, 3, 4, X), (6, 7, 8, X), (10, 11, 12, X) + ) + self.assert_unpack( + "RGBX", "XBGR", 4, (4, 3, 2, X), (8, 7, 6, X), (12, 11, 10, X) + ) + self.assert_unpack( + "RGBX", + "YCC;P", + b"D]\x9c\x82\x1a\x91\xfaOC\xe7J\x12", # random data + (127, 102, 0, X), + (192, 227, 0, X), + (213, 255, 170, X), + (98, 255, 133, X), + ) + self.assert_unpack("RGBX", "R", 1, (1, 0, 0, 0), (2, 0, 0, 0), (3, 0, 0, 0)) + self.assert_unpack("RGBX", "G", 1, (0, 1, 0, 0), (0, 2, 0, 0), (0, 3, 0, 0)) + self.assert_unpack("RGBX", "B", 1, (0, 0, 1, 0), (0, 0, 2, 0), (0, 0, 3, 0)) + self.assert_unpack("RGBX", "X", 1, (0, 0, 0, 1), (0, 0, 0, 2), (0, 0, 0, 3)) + + def test_CMYK(self) -> None: + self.assert_unpack( + "CMYK", "CMYK", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12) + ) + self.assert_unpack( + "CMYK", "CMYKX", 5, (1, 2, 3, 4), (6, 7, 8, 9), (11, 12, 13, 14) + ) + self.assert_unpack( + "CMYK", "CMYKXX", 6, (1, 2, 3, 4), (7, 8, 9, 10), (13, 14, 15, 16) + ) + self.assert_unpack( + "CMYK", + "CMYK;I", + 4, + (254, 253, 252, 251), + (250, 249, 248, 247), + (246, 245, 244, 243), + ) + self.assert_unpack( + "CMYK", "CMYK;L", 4, (1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12) + ) + self.assert_unpack("CMYK", "C", 1, (1, 0, 0, 0), (2, 0, 0, 0), (3, 0, 0, 0)) + self.assert_unpack("CMYK", "M", 1, (0, 1, 0, 0), (0, 2, 0, 0), (0, 3, 0, 0)) + self.assert_unpack("CMYK", "Y", 1, (0, 0, 1, 0), (0, 0, 2, 0), (0, 0, 3, 0)) + self.assert_unpack("CMYK", "K", 1, (0, 0, 0, 1), (0, 0, 0, 2), (0, 0, 0, 3)) + self.assert_unpack( + "CMYK", "C;I", 1, (254, 0, 0, 0), (253, 0, 0, 0), (252, 0, 0, 0) + ) + self.assert_unpack( + "CMYK", "M;I", 1, (0, 254, 0, 0), (0, 253, 0, 0), (0, 252, 0, 0) + ) + self.assert_unpack( + "CMYK", "Y;I", 1, (0, 0, 254, 0), (0, 0, 253, 0), (0, 0, 252, 0) + ) + self.assert_unpack( + "CMYK", "K;I", 1, (0, 0, 0, 254), (0, 0, 0, 253), (0, 0, 0, 252) + ) + + def test_YCbCr(self) -> None: + self.assert_unpack("YCbCr", "YCbCr", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) + self.assert_unpack("YCbCr", "YCbCr;L", 3, (1, 4, 7), (2, 5, 8), (3, 6, 9)) + self.assert_unpack("YCbCr", "YCbCrK", 4, (1, 2, 3), (5, 6, 7), (9, 10, 11)) + self.assert_unpack("YCbCr", "YCbCrX", 4, (1, 2, 3), (5, 6, 7), (9, 10, 11)) + + def test_LAB(self) -> None: + self.assert_unpack("LAB", "LAB", 3, (1, 130, 131), (4, 133, 134), (7, 136, 137)) + self.assert_unpack("LAB", "L", 1, (1, 0, 0), (2, 0, 0), (3, 0, 0)) + self.assert_unpack("LAB", "A", 1, (0, 1, 0), (0, 2, 0), (0, 3, 0)) + self.assert_unpack("LAB", "B", 1, (0, 0, 1), (0, 0, 2), (0, 0, 3)) + + def test_HSV(self) -> None: + self.assert_unpack("HSV", "HSV", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) + self.assert_unpack("HSV", "H", 1, (1, 0, 0), (2, 0, 0), (3, 0, 0)) + self.assert_unpack("HSV", "S", 1, (0, 1, 0), (0, 2, 0), (0, 3, 0)) + self.assert_unpack("HSV", "V", 1, (0, 0, 1), (0, 0, 2), (0, 0, 3)) + + def test_I(self) -> None: + self.assert_unpack("I", "I;8", 1, 0x01, 0x02, 0x03, 0x04) + self.assert_unpack("I", "I;8S", b"\x01\x83", 1, -125) + self.assert_unpack("I", "I;16", 2, 0x0201, 0x0403) + self.assert_unpack("I", "I;16S", b"\x83\x01\x01\x83", 0x0183, -31999) + self.assert_unpack("I", "I;16B", 2, 0x0102, 0x0304) + self.assert_unpack("I", "I;16BS", b"\x83\x01\x01\x83", -31999, 0x0183) + self.assert_unpack("I", "I;32", 4, 0x04030201, 0x08070605) + self.assert_unpack( + "I", "I;32S", b"\x83\x00\x00\x01\x01\x00\x00\x83", 0x01000083, -2097151999 + ) + self.assert_unpack("I", "I;32B", 4, 0x01020304, 0x05060708) + self.assert_unpack( + "I", "I;32BS", b"\x83\x00\x00\x01\x01\x00\x00\x83", -2097151999, 0x01000083 + ) + + if sys.byteorder == "little": + self.assert_unpack("I", "I", 4, 0x04030201, 0x08070605) + self.assert_unpack("I", "I;16N", 2, 0x0201, 0x0403) + self.assert_unpack("I", "I;16NS", b"\x83\x01\x01\x83", 0x0183, -31999) + self.assert_unpack("I", "I;32N", 4, 0x04030201, 0x08070605) + self.assert_unpack( + "I", + "I;32NS", + b"\x83\x00\x00\x01\x01\x00\x00\x83", + 0x01000083, + -2097151999, + ) + else: + self.assert_unpack("I", "I", 4, 0x01020304, 0x05060708) + self.assert_unpack("I", "I;16N", 2, 0x0102, 0x0304) + self.assert_unpack("I", "I;16NS", b"\x83\x01\x01\x83", -31999, 0x0183) + self.assert_unpack("I", "I;32N", 4, 0x01020304, 0x05060708) + self.assert_unpack( + "I", + "I;32NS", + b"\x83\x00\x00\x01\x01\x00\x00\x83", + -2097151999, + 0x01000083, + ) + + def test_F_int(self) -> None: + self.assert_unpack("F", "F;8", 1, 0x01, 0x02, 0x03, 0x04) + self.assert_unpack("F", "F;8S", b"\x01\x83", 1, -125) + self.assert_unpack("F", "F;16", 2, 0x0201, 0x0403) + self.assert_unpack("F", "F;16S", b"\x83\x01\x01\x83", 0x0183, -31999) + self.assert_unpack("F", "F;16B", 2, 0x0102, 0x0304) + self.assert_unpack("F", "F;16BS", b"\x83\x01\x01\x83", -31999, 0x0183) + self.assert_unpack("F", "F;32", 4, 67305984, 134678016) + self.assert_unpack( + "F", "F;32S", b"\x83\x00\x00\x01\x01\x00\x00\x83", 16777348, -2097152000 + ) + self.assert_unpack("F", "F;32B", 4, 0x01020304, 0x05060708) + self.assert_unpack( + "F", "F;32BS", b"\x83\x00\x00\x01\x01\x00\x00\x83", -2097152000, 16777348 + ) + + if sys.byteorder == "little": + self.assert_unpack("F", "F;16N", 2, 0x0201, 0x0403) + self.assert_unpack("F", "F;16NS", b"\x83\x01\x01\x83", 0x0183, -31999) + self.assert_unpack("F", "F;32N", 4, 67305984, 134678016) + self.assert_unpack( + "F", + "F;32NS", + b"\x83\x00\x00\x01\x01\x00\x00\x83", + 16777348, + -2097152000, + ) + else: + self.assert_unpack("F", "F;16N", 2, 0x0102, 0x0304) + self.assert_unpack("F", "F;16NS", b"\x83\x01\x01\x83", -31999, 0x0183) + self.assert_unpack("F", "F;32N", 4, 0x01020304, 0x05060708) + self.assert_unpack( + "F", + "F;32NS", + b"\x83\x00\x00\x01\x01\x00\x00\x83", + -2097152000, + 16777348, + ) + + def test_F_float(self) -> None: + self.assert_unpack( + "F", "F;32F", 4, 1.539989614439558e-36, 4.063216068939723e-34 + ) + self.assert_unpack( + "F", "F;32BF", 4, 2.387939260590663e-38, 6.301941157072183e-36 + ) + self.assert_unpack( + "F", + "F;64F", + b"333333\xc3?\x00\x00\x00\x00\x00J\x93\xc0", # by struct.pack + 0.15000000596046448, + -1234.5, + ) + self.assert_unpack( + "F", + "F;64BF", + b"?\xc3333333\xc0\x93J\x00\x00\x00\x00\x00", # by struct.pack + 0.15000000596046448, + -1234.5, + ) + + if sys.byteorder == "little": + self.assert_unpack( + "F", "F", 4, 1.539989614439558e-36, 4.063216068939723e-34 + ) + self.assert_unpack( + "F", "F;32NF", 4, 1.539989614439558e-36, 4.063216068939723e-34 + ) + self.assert_unpack( + "F", + "F;64NF", + b"333333\xc3?\x00\x00\x00\x00\x00J\x93\xc0", + 0.15000000596046448, + -1234.5, + ) + else: + self.assert_unpack( + "F", "F", 4, 2.387939260590663e-38, 6.301941157072183e-36 + ) + self.assert_unpack( + "F", "F;32NF", 4, 2.387939260590663e-38, 6.301941157072183e-36 + ) + self.assert_unpack( + "F", + "F;64NF", + b"?\xc3333333\xc0\x93J\x00\x00\x00\x00\x00", + 0.15000000596046448, + -1234.5, + ) + + def test_I16(self) -> None: + self.assert_unpack("I;16", "I;16", 2, 0x0201, 0x0403, 0x0605) + self.assert_unpack("I;16", "I;16B", 2, 0x0102, 0x0304, 0x0506) + self.assert_unpack("I;16B", "I;16B", 2, 0x0102, 0x0304, 0x0506) + self.assert_unpack("I;16L", "I;16L", 2, 0x0201, 0x0403, 0x0605) + self.assert_unpack("I;16", "I;12", 2, 0x0010, 0x0203, 0x0040) + if sys.byteorder == "little": + self.assert_unpack("I;16", "I;16N", 2, 0x0201, 0x0403, 0x0605) + self.assert_unpack("I;16B", "I;16N", 2, 0x0201, 0x0403, 0x0605) + self.assert_unpack("I;16L", "I;16N", 2, 0x0201, 0x0403, 0x0605) + self.assert_unpack("I;16N", "I;16N", 2, 0x0201, 0x0403, 0x0605) + else: + self.assert_unpack("I;16", "I;16N", 2, 0x0102, 0x0304, 0x0506) + self.assert_unpack("I;16B", "I;16N", 2, 0x0102, 0x0304, 0x0506) + self.assert_unpack("I;16L", "I;16N", 2, 0x0102, 0x0304, 0x0506) + self.assert_unpack("I;16N", "I;16N", 2, 0x0102, 0x0304, 0x0506) + + def test_CMYK16(self) -> None: + self.assert_unpack("CMYK", "CMYK;16L", 8, (2, 4, 6, 8), (10, 12, 14, 16)) + self.assert_unpack("CMYK", "CMYK;16B", 8, (1, 3, 5, 7), (9, 11, 13, 15)) + if sys.byteorder == "little": + self.assert_unpack("CMYK", "CMYK;16N", 8, (2, 4, 6, 8), (10, 12, 14, 16)) + else: + self.assert_unpack("CMYK", "CMYK;16N", 8, (1, 3, 5, 7), (9, 11, 13, 15)) + + def test_value_error(self) -> None: + with pytest.raises(ValueError): + self.assert_unpack("L", "L", 0, 0) + with pytest.raises(ValueError): + self.assert_unpack("RGB", "RGB", 2, 0) + with pytest.raises(ValueError): + self.assert_unpack("CMYK", "CMYK", 2, 0) diff --git a/Tests/test_locale.py b/Tests/test_locale.py index 386c57cc0..3eebbe8a0 100644 --- a/Tests/test_locale.py +++ b/Tests/test_locale.py @@ -1,39 +1,39 @@ -from __future__ import annotations - -import locale - -import pytest - -from PIL import Image - -# ref https://github.com/python-pillow/Pillow/issues/272 -# on windows, in polish locale: - -# import locale -# print(locale.setlocale(locale.LC_ALL, 'polish')) -# import string -# print(len(string.whitespace)) -# print(ord(string.whitespace[6])) - -# Polish_Poland.1250 -# 7 -# 160 - -# one of string.whitespace is not freely convertible into ascii. - -path = "Tests/images/hopper.jpg" - - -def test_sanity() -> None: - with Image.open(path): - pass - try: - locale.setlocale(locale.LC_ALL, "polish") - except locale.Error: - pytest.skip("Polish locale not available") - - try: - with Image.open(path): - pass - finally: - locale.setlocale(locale.LC_ALL, (None, None)) +from __future__ import annotations + +import locale + +import pytest + +from PIL import Image + +# ref https://github.com/python-pillow/Pillow/issues/272 +# on windows, in polish locale: + +# import locale +# print(locale.setlocale(locale.LC_ALL, 'polish')) +# import string +# print(len(string.whitespace)) +# print(ord(string.whitespace[6])) + +# Polish_Poland.1250 +# 7 +# 160 + +# one of string.whitespace is not freely convertible into ascii. + +path = "Tests/images/hopper.jpg" + + +def test_sanity() -> None: + with Image.open(path): + pass + try: + locale.setlocale(locale.LC_ALL, "polish") + except locale.Error: + pytest.skip("Polish locale not available") + + try: + with Image.open(path): + pass + finally: + locale.setlocale(locale.LC_ALL, (None, None)) diff --git a/Tests/test_main.py b/Tests/test_main.py index e9e12b24a..32a68e754 100644 --- a/Tests/test_main.py +++ b/Tests/test_main.py @@ -1,47 +1,47 @@ -from __future__ import annotations - -import os -import subprocess -import sys - -import pytest - - -@pytest.mark.parametrize( - "args, report", - ((["PIL"], False), (["PIL", "--report"], True), (["PIL.report"], True)), -) -def test_main(args, report) -> None: - args = [sys.executable, "-m"] + args - out = subprocess.check_output(args).decode("utf-8") - lines = out.splitlines() - assert lines[0] == "-" * 68 - assert lines[1].startswith("Pillow ") - assert lines[2].startswith("Python ") - lines = lines[3:] - while lines[0].startswith(" "): - lines = lines[1:] - assert lines[0] == "-" * 68 - assert lines[1].startswith("Python executable is") - lines = lines[2:] - if lines[0].startswith("Environment Python files loaded from"): - lines = lines[1:] - assert lines[0].startswith("System Python files loaded from") - assert lines[1] == "-" * 68 - assert lines[2].startswith("Python Pillow modules loaded from ") - assert lines[3].startswith("Binary Pillow modules loaded from ") - assert lines[4] == "-" * 68 - jpeg = ( - os.linesep - + "-" * 68 - + os.linesep - + "JPEG image/jpeg" - + os.linesep - + "Extensions: .jfif, .jpe, .jpeg, .jpg" - + os.linesep - + "Features: open, save" - + os.linesep - + "-" * 68 - + os.linesep - ) - assert report == (jpeg not in out) +from __future__ import annotations + +import os +import subprocess +import sys + +import pytest + + +@pytest.mark.parametrize( + "args, report", + ((["PIL"], False), (["PIL", "--report"], True), (["PIL.report"], True)), +) +def test_main(args, report) -> None: + args = [sys.executable, "-m"] + args + out = subprocess.check_output(args).decode("utf-8") + lines = out.splitlines() + assert lines[0] == "-" * 68 + assert lines[1].startswith("Pillow ") + assert lines[2].startswith("Python ") + lines = lines[3:] + while lines[0].startswith(" "): + lines = lines[1:] + assert lines[0] == "-" * 68 + assert lines[1].startswith("Python executable is") + lines = lines[2:] + if lines[0].startswith("Environment Python files loaded from"): + lines = lines[1:] + assert lines[0].startswith("System Python files loaded from") + assert lines[1] == "-" * 68 + assert lines[2].startswith("Python Pillow modules loaded from ") + assert lines[3].startswith("Binary Pillow modules loaded from ") + assert lines[4] == "-" * 68 + jpeg = ( + os.linesep + + "-" * 68 + + os.linesep + + "JPEG image/jpeg" + + os.linesep + + "Extensions: .jfif, .jpe, .jpeg, .jpg" + + os.linesep + + "Features: open, save" + + os.linesep + + "-" * 68 + + os.linesep + ) + assert report == (jpeg not in out) diff --git a/Tests/test_map.py b/Tests/test_map.py index 93140f6e5..f509e18fa 100644 --- a/Tests/test_map.py +++ b/Tests/test_map.py @@ -1,47 +1,47 @@ -from __future__ import annotations - -import sys - -import pytest - -from PIL import Image - - -def test_overflow() -> 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 - - # 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: - # Note that this image triggers the decompression bomb warning: - max_pixels = Image.MAX_IMAGE_PIXELS - Image.MAX_IMAGE_PIXELS = None - - # Previously raised an access violation on Windows - with Image.open("Tests/images/l2rgb_read.bmp") as im: - with pytest.raises((ValueError, MemoryError, OSError)): - im.tobytes() - - Image.MAX_IMAGE_PIXELS = max_pixels - - -@pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") -def test_ysize() -> None: - numpy = pytest.importorskip("numpy", reason="NumPy not installed") - - # Should not raise 'Integer overflow in ysize' - arr = numpy.zeros((46341, 46341), dtype=numpy.uint8) - Image.fromarray(arr) +from __future__ import annotations + +import sys + +import pytest + +from PIL import Image + + +def test_overflow() -> 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 + + # 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: + # Note that this image triggers the decompression bomb warning: + max_pixels = Image.MAX_IMAGE_PIXELS + Image.MAX_IMAGE_PIXELS = None + + # Previously raised an access violation on Windows + with Image.open("Tests/images/l2rgb_read.bmp") as im: + with pytest.raises((ValueError, MemoryError, OSError)): + im.tobytes() + + Image.MAX_IMAGE_PIXELS = max_pixels + + +@pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") +def test_ysize() -> None: + numpy = pytest.importorskip("numpy", reason="NumPy not installed") + + # Should not raise 'Integer overflow in ysize' + arr = numpy.zeros((46341, 46341), dtype=numpy.uint8) + Image.fromarray(arr) diff --git a/Tests/test_mode_i16.py b/Tests/test_mode_i16.py index 1b01f95ce..95935cb06 100644 --- a/Tests/test_mode_i16.py +++ b/Tests/test_mode_i16.py @@ -1,98 +1,98 @@ -from __future__ import annotations - -from pathlib import Path - -import pytest - -from PIL import Image - -from .helper import hopper - -original = hopper().resize((32, 32)).convert("I") - - -def verify(im1: Image.Image) -> None: - im2 = original.copy() - assert im1.size == im2.size - pix1 = im1.load() - pix2 = im2.load() - for y in range(im1.size[1]): - for x in range(im1.size[0]): - xy = x, y - p1 = pix1[xy] - p2 = pix2[xy] - assert ( - p1 == p2 - ), f"got {repr(p1)} from mode {im1.mode} at {xy}, expected {repr(p2)}" - - -@pytest.mark.parametrize("mode", ("L", "I;16", "I;16B", "I;16L", "I")) -def test_basic(tmp_path: Path, mode: str) -> None: - # PIL 1.1 has limited support for 16-bit image data. Check that - # create/copy/transform and save works as expected. - - im_in = original.convert(mode) - verify(im_in) - - w, h = im_in.size - - im_out = im_in.copy() - verify(im_out) # copy - - im_out = im_in.transform((w, h), Image.Transform.EXTENT, (0, 0, w, h)) - verify(im_out) # transform - - filename = str(tmp_path / "temp.im") - im_in.save(filename) - - with Image.open(filename) as im_out: - verify(im_in) - verify(im_out) - - im_out = im_in.crop((0, 0, w, h)) - verify(im_out) - - im_out = Image.new(mode, (w, h), None) - im_out.paste(im_in.crop((0, 0, w // 2, h)), (0, 0)) - im_out.paste(im_in.crop((w // 2, 0, w, h)), (w // 2, 0)) - - verify(im_in) - verify(im_out) - - im_in = Image.new(mode, (1, 1), 1) - assert im_in.getpixel((0, 0)) == 1 - - im_in.putpixel((0, 0), 2) - assert im_in.getpixel((0, 0)) == 2 - - if mode == "L": - maximum = 255 - else: - maximum = 32767 - - im_in = Image.new(mode, (1, 1), 256) - assert im_in.getpixel((0, 0)) == min(256, maximum) - - im_in.putpixel((0, 0), 512) - assert im_in.getpixel((0, 0)) == min(512, maximum) - - -def test_tobytes() -> None: - def tobytes(mode: str) -> bytes: - return Image.new(mode, (1, 1), 1).tobytes() - - order = 1 if Image._ENDIAN == "<" else -1 - - assert tobytes("L") == b"\x01" - assert tobytes("I;16") == b"\x01\x00" - assert tobytes("I;16B") == b"\x00\x01" - assert tobytes("I") == b"\x01\x00\x00\x00"[::order] - - -def test_convert() -> None: - im = original.copy() - - for mode in ("I;16", "I;16B", "I;16N"): - verify(im.convert(mode)) - verify(im.convert(mode).convert("L")) - verify(im.convert(mode).convert("I")) +from __future__ import annotations + +from pathlib import Path + +import pytest + +from PIL import Image + +from .helper import hopper + +original = hopper().resize((32, 32)).convert("I") + + +def verify(im1: Image.Image) -> None: + im2 = original.copy() + assert im1.size == im2.size + pix1 = im1.load() + pix2 = im2.load() + for y in range(im1.size[1]): + for x in range(im1.size[0]): + xy = x, y + p1 = pix1[xy] + p2 = pix2[xy] + assert ( + p1 == p2 + ), f"got {repr(p1)} from mode {im1.mode} at {xy}, expected {repr(p2)}" + + +@pytest.mark.parametrize("mode", ("L", "I;16", "I;16B", "I;16L", "I")) +def test_basic(tmp_path: Path, mode: str) -> None: + # PIL 1.1 has limited support for 16-bit image data. Check that + # create/copy/transform and save works as expected. + + im_in = original.convert(mode) + verify(im_in) + + w, h = im_in.size + + im_out = im_in.copy() + verify(im_out) # copy + + im_out = im_in.transform((w, h), Image.Transform.EXTENT, (0, 0, w, h)) + verify(im_out) # transform + + filename = str(tmp_path / "temp.im") + im_in.save(filename) + + with Image.open(filename) as im_out: + verify(im_in) + verify(im_out) + + im_out = im_in.crop((0, 0, w, h)) + verify(im_out) + + im_out = Image.new(mode, (w, h), None) + im_out.paste(im_in.crop((0, 0, w // 2, h)), (0, 0)) + im_out.paste(im_in.crop((w // 2, 0, w, h)), (w // 2, 0)) + + verify(im_in) + verify(im_out) + + im_in = Image.new(mode, (1, 1), 1) + assert im_in.getpixel((0, 0)) == 1 + + im_in.putpixel((0, 0), 2) + assert im_in.getpixel((0, 0)) == 2 + + if mode == "L": + maximum = 255 + else: + maximum = 32767 + + im_in = Image.new(mode, (1, 1), 256) + assert im_in.getpixel((0, 0)) == min(256, maximum) + + im_in.putpixel((0, 0), 512) + assert im_in.getpixel((0, 0)) == min(512, maximum) + + +def test_tobytes() -> None: + def tobytes(mode: str) -> bytes: + return Image.new(mode, (1, 1), 1).tobytes() + + order = 1 if Image._ENDIAN == "<" else -1 + + assert tobytes("L") == b"\x01" + assert tobytes("I;16") == b"\x01\x00" + assert tobytes("I;16B") == b"\x00\x01" + assert tobytes("I") == b"\x01\x00\x00\x00"[::order] + + +def test_convert() -> None: + im = original.copy() + + for mode in ("I;16", "I;16B", "I;16N"): + verify(im.convert(mode)) + verify(im.convert(mode).convert("L")) + verify(im.convert(mode).convert("I")) diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index 9f4e6534e..0e16505cc 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -1,249 +1,249 @@ -from __future__ import annotations - -import warnings - -import pytest - -from PIL import Image - -from .helper import assert_deep_equal, assert_image, hopper, skip_unless_feature - -numpy = pytest.importorskip("numpy", reason="NumPy not installed") - -TEST_IMAGE_SIZE = (10, 10) - - -def test_numpy_to_image() -> None: - def to_image(dtype, bands: int = 1, boolean: int = 0) -> Image.Image: - if bands == 1: - if boolean: - data = [0, 255] * 50 - else: - data = list(range(100)) - a = numpy.array(data, dtype=dtype) - a.shape = TEST_IMAGE_SIZE - i = Image.fromarray(a) - if list(i.getdata()) != data: - print("data mismatch for", dtype) - else: - data = list(range(100)) - a = numpy.array([[x] * bands for x in data], dtype=dtype) - a.shape = TEST_IMAGE_SIZE[0], TEST_IMAGE_SIZE[1], bands - i = Image.fromarray(a) - if list(i.getchannel(0).getdata()) != list(range(100)): - print("data mismatch for", dtype) - return i - - # Check supported 1-bit integer formats - assert_image(to_image(bool, 1, 1), "1", TEST_IMAGE_SIZE) - assert_image(to_image(numpy.bool_, 1, 1), "1", TEST_IMAGE_SIZE) - - # Check supported 8-bit integer formats - assert_image(to_image(numpy.uint8), "L", TEST_IMAGE_SIZE) - assert_image(to_image(numpy.uint8, 3), "RGB", TEST_IMAGE_SIZE) - assert_image(to_image(numpy.uint8, 4), "RGBA", TEST_IMAGE_SIZE) - assert_image(to_image(numpy.int8), "I", TEST_IMAGE_SIZE) - - # Check non-fixed-size integer types - # These may fail, depending on the platform, since we have no native - # 64-bit int image types. - # assert_image(to_image(numpy.uint), "I", TEST_IMAGE_SIZE) - # assert_image(to_image(numpy.int), "I", TEST_IMAGE_SIZE) - - # Check 16-bit integer formats - if Image._ENDIAN == "<": - assert_image(to_image(numpy.uint16), "I;16", TEST_IMAGE_SIZE) - else: - assert_image(to_image(numpy.uint16), "I;16B", TEST_IMAGE_SIZE) - - assert_image(to_image(numpy.int16), "I", TEST_IMAGE_SIZE) - - # Check 32-bit integer formats - assert_image(to_image(numpy.uint32), "I", TEST_IMAGE_SIZE) - assert_image(to_image(numpy.int32), "I", TEST_IMAGE_SIZE) - - # Check 64-bit integer formats - with pytest.raises(TypeError): - to_image(numpy.uint64) - with pytest.raises(TypeError): - to_image(numpy.int64) - - # Check floating-point formats - assert_image(to_image(float), "F", TEST_IMAGE_SIZE) - with pytest.raises(TypeError): - to_image(numpy.float16) - assert_image(to_image(numpy.float32), "F", TEST_IMAGE_SIZE) - assert_image(to_image(numpy.float64), "F", TEST_IMAGE_SIZE) - - assert_image(to_image(numpy.uint8, 2), "LA", (10, 10)) - assert_image(to_image(numpy.uint8, 3), "RGB", (10, 10)) - assert_image(to_image(numpy.uint8, 4), "RGBA", (10, 10)) - - -# Based on an erring example at -# https://stackoverflow.com/questions/10854903/what-is-causing-dimension-dependent-attributeerror-in-pil-fromarray-function -def test_3d_array() -> None: - size = (5, TEST_IMAGE_SIZE[0], TEST_IMAGE_SIZE[1]) - a = numpy.ones(size, dtype=numpy.uint8) - assert_image(Image.fromarray(a[1, :, :]), "L", TEST_IMAGE_SIZE) - size = (TEST_IMAGE_SIZE[0], 5, TEST_IMAGE_SIZE[1]) - a = numpy.ones(size, dtype=numpy.uint8) - assert_image(Image.fromarray(a[:, 1, :]), "L", TEST_IMAGE_SIZE) - size = (TEST_IMAGE_SIZE[0], TEST_IMAGE_SIZE[1], 5) - a = numpy.ones(size, dtype=numpy.uint8) - assert_image(Image.fromarray(a[:, :, 1]), "L", TEST_IMAGE_SIZE) - - -def test_1d_array() -> None: - a = numpy.ones(5, dtype=numpy.uint8) - assert_image(Image.fromarray(a), "L", (1, 5)) - - -def _test_img_equals_nparray(img: Image.Image, np) -> None: - assert len(np.shape) >= 2 - np_size = np.shape[1], np.shape[0] - assert img.size == np_size - px = img.load() - for x in range(0, img.size[0], int(img.size[0] / 10)): - for y in range(0, img.size[1], int(img.size[1] / 10)): - assert_deep_equal(px[x, y], np[y, x]) - - -def test_16bit() -> None: - with Image.open("Tests/images/16bit.cropped.tif") as img: - np_img = numpy.array(img) - _test_img_equals_nparray(img, np_img) - assert np_img.dtype == numpy.dtype(" None: - # Test that 1-bit arrays convert to numpy and back - # See: https://github.com/python-pillow/Pillow/issues/350 - arr = numpy.array([[1, 0, 0, 1, 0], [0, 1, 0, 0, 0]], "u1") - img = Image.fromarray(arr * 255).convert("1") - assert img.mode == "1" - arr_back = numpy.array(img) - numpy.testing.assert_array_equal(arr, arr_back) - - -def test_save_tiff_uint16() -> None: - # Tests that we're getting the pixel value in the right byte order. - pixel_value = 0x1234 - a = numpy.array( - [pixel_value] * TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1], dtype=numpy.uint16 - ) - a.shape = TEST_IMAGE_SIZE - img = Image.fromarray(a) - - img_px = img.load() - assert img_px[0, 0] == pixel_value - - -@pytest.mark.parametrize( - "mode, dtype", - ( - ("L", numpy.uint8), - ("I", numpy.int32), - ("F", numpy.float32), - ("LA", numpy.uint8), - ("RGB", numpy.uint8), - ("RGBA", numpy.uint8), - ("RGBX", numpy.uint8), - ("CMYK", numpy.uint8), - ("YCbCr", numpy.uint8), - ("I;16", "u2"), - ("I;16L", " None: - img = hopper(mode) - - # Resize to non-square - img = img.crop((3, 0, 124, 127)) - assert img.size == (121, 127) - - np_img = numpy.array(img) - _test_img_equals_nparray(img, np_img) - assert np_img.dtype == dtype - - -def test_point_lut() -> None: - # See https://github.com/python-pillow/Pillow/issues/439 - - data = list(range(256)) * 3 - lut = numpy.array(data, dtype=numpy.uint8) - - im = hopper() - - im.point(lut) - - -def test_putdata() -> None: - # Shouldn't segfault - # See https://github.com/python-pillow/Pillow/issues/1008 - - im = Image.new("F", (150, 100)) - arr = numpy.zeros((15000,), numpy.float32) - im.putdata(arr) - - assert len(im.getdata()) == len(arr) - - -@pytest.mark.parametrize( - "dtype", - ( - bool, - numpy.bool_, - numpy.int8, - numpy.int16, - numpy.int32, - numpy.uint8, - numpy.uint16, - numpy.uint32, - float, - numpy.float32, - numpy.float64, - ), -) -def test_roundtrip_eye(dtype) -> None: - arr = numpy.eye(10, dtype=dtype) - numpy.testing.assert_array_equal(arr, numpy.array(Image.fromarray(arr))) - - -def test_zero_size() -> None: - # Shouldn't cause floating point exception - # See https://github.com/python-pillow/Pillow/issues/2259 - - im = Image.fromarray(numpy.empty((0, 0), dtype=numpy.uint8)) - - assert im.size == (0, 0) - - -@skip_unless_feature("libtiff") -def test_load_first() -> None: - with Image.open("Tests/images/g4_orientation_5.tif") as im: - a = numpy.array(im) - assert a.shape == (88, 590) - - -def test_bool() -> None: - # https://github.com/python-pillow/Pillow/issues/2044 - a = numpy.zeros((10, 2), dtype=bool) - a[0][0] = True - - im2 = Image.fromarray(a) - assert im2.getdata()[0] == 255 - - -def test_no_resource_warning_for_numpy_array() -> None: - # https://github.com/python-pillow/Pillow/issues/835 - # Arrange - from numpy import array - - test_file = "Tests/images/hopper.png" - with Image.open(test_file) as im: - # Act/Assert - with warnings.catch_warnings(): - array(im) +from __future__ import annotations + +import warnings + +import pytest + +from PIL import Image + +from .helper import assert_deep_equal, assert_image, hopper, skip_unless_feature + +numpy = pytest.importorskip("numpy", reason="NumPy not installed") + +TEST_IMAGE_SIZE = (10, 10) + + +def test_numpy_to_image() -> None: + def to_image(dtype, bands: int = 1, boolean: int = 0) -> Image.Image: + if bands == 1: + if boolean: + data = [0, 255] * 50 + else: + data = list(range(100)) + a = numpy.array(data, dtype=dtype) + a.shape = TEST_IMAGE_SIZE + i = Image.fromarray(a) + if list(i.getdata()) != data: + print("data mismatch for", dtype) + else: + data = list(range(100)) + a = numpy.array([[x] * bands for x in data], dtype=dtype) + a.shape = TEST_IMAGE_SIZE[0], TEST_IMAGE_SIZE[1], bands + i = Image.fromarray(a) + if list(i.getchannel(0).getdata()) != list(range(100)): + print("data mismatch for", dtype) + return i + + # Check supported 1-bit integer formats + assert_image(to_image(bool, 1, 1), "1", TEST_IMAGE_SIZE) + assert_image(to_image(numpy.bool_, 1, 1), "1", TEST_IMAGE_SIZE) + + # Check supported 8-bit integer formats + assert_image(to_image(numpy.uint8), "L", TEST_IMAGE_SIZE) + assert_image(to_image(numpy.uint8, 3), "RGB", TEST_IMAGE_SIZE) + assert_image(to_image(numpy.uint8, 4), "RGBA", TEST_IMAGE_SIZE) + assert_image(to_image(numpy.int8), "I", TEST_IMAGE_SIZE) + + # Check non-fixed-size integer types + # These may fail, depending on the platform, since we have no native + # 64-bit int image types. + # assert_image(to_image(numpy.uint), "I", TEST_IMAGE_SIZE) + # assert_image(to_image(numpy.int), "I", TEST_IMAGE_SIZE) + + # Check 16-bit integer formats + if Image._ENDIAN == "<": + assert_image(to_image(numpy.uint16), "I;16", TEST_IMAGE_SIZE) + else: + assert_image(to_image(numpy.uint16), "I;16B", TEST_IMAGE_SIZE) + + assert_image(to_image(numpy.int16), "I", TEST_IMAGE_SIZE) + + # Check 32-bit integer formats + assert_image(to_image(numpy.uint32), "I", TEST_IMAGE_SIZE) + assert_image(to_image(numpy.int32), "I", TEST_IMAGE_SIZE) + + # Check 64-bit integer formats + with pytest.raises(TypeError): + to_image(numpy.uint64) + with pytest.raises(TypeError): + to_image(numpy.int64) + + # Check floating-point formats + assert_image(to_image(float), "F", TEST_IMAGE_SIZE) + with pytest.raises(TypeError): + to_image(numpy.float16) + assert_image(to_image(numpy.float32), "F", TEST_IMAGE_SIZE) + assert_image(to_image(numpy.float64), "F", TEST_IMAGE_SIZE) + + assert_image(to_image(numpy.uint8, 2), "LA", (10, 10)) + assert_image(to_image(numpy.uint8, 3), "RGB", (10, 10)) + assert_image(to_image(numpy.uint8, 4), "RGBA", (10, 10)) + + +# Based on an erring example at +# https://stackoverflow.com/questions/10854903/what-is-causing-dimension-dependent-attributeerror-in-pil-fromarray-function +def test_3d_array() -> None: + size = (5, TEST_IMAGE_SIZE[0], TEST_IMAGE_SIZE[1]) + a = numpy.ones(size, dtype=numpy.uint8) + assert_image(Image.fromarray(a[1, :, :]), "L", TEST_IMAGE_SIZE) + size = (TEST_IMAGE_SIZE[0], 5, TEST_IMAGE_SIZE[1]) + a = numpy.ones(size, dtype=numpy.uint8) + assert_image(Image.fromarray(a[:, 1, :]), "L", TEST_IMAGE_SIZE) + size = (TEST_IMAGE_SIZE[0], TEST_IMAGE_SIZE[1], 5) + a = numpy.ones(size, dtype=numpy.uint8) + assert_image(Image.fromarray(a[:, :, 1]), "L", TEST_IMAGE_SIZE) + + +def test_1d_array() -> None: + a = numpy.ones(5, dtype=numpy.uint8) + assert_image(Image.fromarray(a), "L", (1, 5)) + + +def _test_img_equals_nparray(img: Image.Image, np) -> None: + assert len(np.shape) >= 2 + np_size = np.shape[1], np.shape[0] + assert img.size == np_size + px = img.load() + for x in range(0, img.size[0], int(img.size[0] / 10)): + for y in range(0, img.size[1], int(img.size[1] / 10)): + assert_deep_equal(px[x, y], np[y, x]) + + +def test_16bit() -> None: + with Image.open("Tests/images/16bit.cropped.tif") as img: + np_img = numpy.array(img) + _test_img_equals_nparray(img, np_img) + assert np_img.dtype == numpy.dtype(" None: + # Test that 1-bit arrays convert to numpy and back + # See: https://github.com/python-pillow/Pillow/issues/350 + arr = numpy.array([[1, 0, 0, 1, 0], [0, 1, 0, 0, 0]], "u1") + img = Image.fromarray(arr * 255).convert("1") + assert img.mode == "1" + arr_back = numpy.array(img) + numpy.testing.assert_array_equal(arr, arr_back) + + +def test_save_tiff_uint16() -> None: + # Tests that we're getting the pixel value in the right byte order. + pixel_value = 0x1234 + a = numpy.array( + [pixel_value] * TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1], dtype=numpy.uint16 + ) + a.shape = TEST_IMAGE_SIZE + img = Image.fromarray(a) + + img_px = img.load() + assert img_px[0, 0] == pixel_value + + +@pytest.mark.parametrize( + "mode, dtype", + ( + ("L", numpy.uint8), + ("I", numpy.int32), + ("F", numpy.float32), + ("LA", numpy.uint8), + ("RGB", numpy.uint8), + ("RGBA", numpy.uint8), + ("RGBX", numpy.uint8), + ("CMYK", numpy.uint8), + ("YCbCr", numpy.uint8), + ("I;16", "u2"), + ("I;16L", " None: + img = hopper(mode) + + # Resize to non-square + img = img.crop((3, 0, 124, 127)) + assert img.size == (121, 127) + + np_img = numpy.array(img) + _test_img_equals_nparray(img, np_img) + assert np_img.dtype == dtype + + +def test_point_lut() -> None: + # See https://github.com/python-pillow/Pillow/issues/439 + + data = list(range(256)) * 3 + lut = numpy.array(data, dtype=numpy.uint8) + + im = hopper() + + im.point(lut) + + +def test_putdata() -> None: + # Shouldn't segfault + # See https://github.com/python-pillow/Pillow/issues/1008 + + im = Image.new("F", (150, 100)) + arr = numpy.zeros((15000,), numpy.float32) + im.putdata(arr) + + assert len(im.getdata()) == len(arr) + + +@pytest.mark.parametrize( + "dtype", + ( + bool, + numpy.bool_, + numpy.int8, + numpy.int16, + numpy.int32, + numpy.uint8, + numpy.uint16, + numpy.uint32, + float, + numpy.float32, + numpy.float64, + ), +) +def test_roundtrip_eye(dtype) -> None: + arr = numpy.eye(10, dtype=dtype) + numpy.testing.assert_array_equal(arr, numpy.array(Image.fromarray(arr))) + + +def test_zero_size() -> None: + # Shouldn't cause floating point exception + # See https://github.com/python-pillow/Pillow/issues/2259 + + im = Image.fromarray(numpy.empty((0, 0), dtype=numpy.uint8)) + + assert im.size == (0, 0) + + +@skip_unless_feature("libtiff") +def test_load_first() -> None: + with Image.open("Tests/images/g4_orientation_5.tif") as im: + a = numpy.array(im) + assert a.shape == (88, 590) + + +def test_bool() -> None: + # https://github.com/python-pillow/Pillow/issues/2044 + a = numpy.zeros((10, 2), dtype=bool) + a[0][0] = True + + im2 = Image.fromarray(a) + assert im2.getdata()[0] == 255 + + +def test_no_resource_warning_for_numpy_array() -> None: + # https://github.com/python-pillow/Pillow/issues/835 + # Arrange + from numpy import array + + test_file = "Tests/images/hopper.png" + with Image.open(test_file) as im: + # Act/Assert + with warnings.catch_warnings(): + array(im) diff --git a/Tests/test_pdfparser.py b/Tests/test_pdfparser.py index f6b12cb20..95896f25c 100644 --- a/Tests/test_pdfparser.py +++ b/Tests/test_pdfparser.py @@ -1,127 +1,127 @@ -from __future__ import annotations - -import time - -import pytest - -from PIL.PdfParser import ( - IndirectObjectDef, - IndirectReference, - PdfBinary, - PdfDict, - PdfFormatError, - PdfName, - PdfParser, - PdfStream, - decode_text, - encode_text, - pdf_repr, -) - - -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 decode_text(b"abc") == "abc" - assert decode_text(b"\x1B a \x1C") == "\u02D9 a \u02DD" - - -def test_indirect_refs() -> None: - assert IndirectReference(1, 2) == IndirectReference(1, 2) - assert IndirectReference(1, 2) != IndirectReference(1, 3) - assert IndirectReference(1, 2) != IndirectObjectDef(1, 2) - assert IndirectReference(1, 2) != (1, 2) - assert IndirectObjectDef(1, 2) == IndirectObjectDef(1, 2) - assert IndirectObjectDef(1, 2) != IndirectObjectDef(1, 3) - assert IndirectObjectDef(1, 2) != IndirectReference(1, 2) - assert IndirectObjectDef(1, 2) != (1, 2) - - -def test_parsing() -> None: - assert PdfParser.interpret_name(b"Name#23Hash") == b"Name#Hash" - assert PdfParser.interpret_name(b"Name#23Hash", as_text=True) == "Name#Hash" - assert PdfParser.get_value(b"1 2 R ", 0) == (IndirectReference(1, 2), 5) - assert PdfParser.get_value(b"true[", 0) == (True, 4) - 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"(asd)", 0) == (b"asd", 5) - assert PdfParser.get_value(b"(asd(qwe)zxc)zzz(aaa)", 0) == (b"asd(qwe)zxc", 13) - assert PdfParser.get_value(b"(Two \\\nwords.)", 0) == (b"Two words.", 14) - assert PdfParser.get_value(b"(Two\nlines.)", 0) == (b"Two\nlines.", 12) - assert PdfParser.get_value(b"(Two\r\nlines.)", 0) == (b"Two\nlines.", 13) - assert PdfParser.get_value(b"(Two\\nlines.)", 0) == (b"Two\nlines.", 13) - assert PdfParser.get_value(b"(One\\(paren).", 0) == (b"One(paren", 12) - assert PdfParser.get_value(b"(One\\)paren).", 0) == (b"One)paren", 12) - assert PdfParser.get_value(b"(\\0053)", 0) == (b"\x053", 7) - assert PdfParser.get_value(b"(\\053)", 0) == (b"\x2B", 6) - assert PdfParser.get_value(b"(\\53)", 0) == (b"\x2B", 5) - assert PdfParser.get_value(b"(\\53a)", 0) == (b"\x2Ba", 6) - assert PdfParser.get_value(b"(\\1111)", 0) == (b"\x491", 7) - assert PdfParser.get_value(b" 123 (", 0) == (123, 4) - assert round(abs(PdfParser.get_value(b" 123.4 %", 0)[0] - 123.4), 7) == 0 - assert PdfParser.get_value(b" 123.4 %", 0)[1] == 6 - with pytest.raises(PdfFormatError): - PdfParser.get_value(b"]", 0) - d = PdfParser.get_value(b"<>", 0)[0] - assert isinstance(d, PdfDict) - assert len(d) == 2 - assert d.Name == "value" - assert d[b"Name"] == b"value" - assert d.N == PdfName("V") - a = PdfParser.get_value(b"[/Name (value) /N /V]", 0)[0] - assert isinstance(a, list) - assert len(a) == 4 - assert a[0] == PdfName("Name") - s = PdfParser.get_value( - b"<>\nstream\nabcde\nendstream<<...", 0 - )[0] - assert isinstance(s, PdfStream) - assert s.dictionary.Name == "value" - assert s.decode() == b"abcde" - for name in ["CreationDate", "ModDate"]: - for date, value in { - b"20180729214124": "20180729214124", - b"D:20180729214124": "20180729214124", - b"D:2018072921": "20180729210000", - b"D:20180729214124Z": "20180729214124", - b"D:20180729214124+08'00'": "20180729134124", - b"D:20180729214124-05'00'": "20180730024124", - }.items(): - b = b"<>" - d = PdfParser.get_value(b, 0)[0] - assert time.strftime("%Y%m%d%H%M%S", getattr(d, name)) == value - - -def test_pdf_repr() -> None: - assert bytes(IndirectReference(1, 2)) == b"1 2 R" - assert bytes(IndirectObjectDef(*IndirectReference(1, 2))) == b"1 2 obj" - assert bytes(PdfName(b"Name#Hash")) == b"/Name#23Hash" - assert bytes(PdfName("Name#Hash")) == b"/Name#23Hash" - assert bytes(PdfDict({b"Name": IndirectReference(1, 2)})) == b"<<\n/Name 1 2 R\n>>" - assert bytes(PdfDict({"Name": IndirectReference(1, 2)})) == b"<<\n/Name 1 2 R\n>>" - assert pdf_repr(IndirectReference(1, 2)) == b"1 2 R" - assert pdf_repr(IndirectObjectDef(*IndirectReference(1, 2))) == b"1 2 obj" - assert pdf_repr(PdfName(b"Name#Hash")) == b"/Name#23Hash" - assert pdf_repr(PdfName("Name#Hash")) == b"/Name#23Hash" - assert ( - pdf_repr(PdfDict({b"Name": IndirectReference(1, 2)})) == b"<<\n/Name 1 2 R\n>>" - ) - assert ( - pdf_repr(PdfDict({"Name": IndirectReference(1, 2)})) == b"<<\n/Name 1 2 R\n>>" - ) - assert pdf_repr(123) == b"123" - assert pdf_repr(True) == b"true" - assert pdf_repr(False) == b"false" - assert pdf_repr(None) == b"null" - assert pdf_repr(b"a)/b\\(c") == rb"(a\)/b\\\(c)" - assert pdf_repr([123, True, {"a": PdfName(b"b")}]) == b"[ 123 true <<\n/a /b\n>> ]" - assert pdf_repr(PdfBinary(b"\x90\x1F\xA0")) == b"<901FA0>" - - -def test_duplicate_xref_entry() -> None: - pdf = PdfParser("Tests/images/duplicate_xref_entry.pdf") - assert pdf.xref_table.existing_entries[6][0] == 1197 - pdf.close() +from __future__ import annotations + +import time + +import pytest + +from PIL.PdfParser import ( + IndirectObjectDef, + IndirectReference, + PdfBinary, + PdfDict, + PdfFormatError, + PdfName, + PdfParser, + PdfStream, + decode_text, + encode_text, + pdf_repr, +) + + +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 decode_text(b"abc") == "abc" + assert decode_text(b"\x1B a \x1C") == "\u02D9 a \u02DD" + + +def test_indirect_refs() -> None: + assert IndirectReference(1, 2) == IndirectReference(1, 2) + assert IndirectReference(1, 2) != IndirectReference(1, 3) + assert IndirectReference(1, 2) != IndirectObjectDef(1, 2) + assert IndirectReference(1, 2) != (1, 2) + assert IndirectObjectDef(1, 2) == IndirectObjectDef(1, 2) + assert IndirectObjectDef(1, 2) != IndirectObjectDef(1, 3) + assert IndirectObjectDef(1, 2) != IndirectReference(1, 2) + assert IndirectObjectDef(1, 2) != (1, 2) + + +def test_parsing() -> None: + assert PdfParser.interpret_name(b"Name#23Hash") == b"Name#Hash" + assert PdfParser.interpret_name(b"Name#23Hash", as_text=True) == "Name#Hash" + assert PdfParser.get_value(b"1 2 R ", 0) == (IndirectReference(1, 2), 5) + assert PdfParser.get_value(b"true[", 0) == (True, 4) + 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"(asd)", 0) == (b"asd", 5) + assert PdfParser.get_value(b"(asd(qwe)zxc)zzz(aaa)", 0) == (b"asd(qwe)zxc", 13) + assert PdfParser.get_value(b"(Two \\\nwords.)", 0) == (b"Two words.", 14) + assert PdfParser.get_value(b"(Two\nlines.)", 0) == (b"Two\nlines.", 12) + assert PdfParser.get_value(b"(Two\r\nlines.)", 0) == (b"Two\nlines.", 13) + assert PdfParser.get_value(b"(Two\\nlines.)", 0) == (b"Two\nlines.", 13) + assert PdfParser.get_value(b"(One\\(paren).", 0) == (b"One(paren", 12) + assert PdfParser.get_value(b"(One\\)paren).", 0) == (b"One)paren", 12) + assert PdfParser.get_value(b"(\\0053)", 0) == (b"\x053", 7) + assert PdfParser.get_value(b"(\\053)", 0) == (b"\x2B", 6) + assert PdfParser.get_value(b"(\\53)", 0) == (b"\x2B", 5) + assert PdfParser.get_value(b"(\\53a)", 0) == (b"\x2Ba", 6) + assert PdfParser.get_value(b"(\\1111)", 0) == (b"\x491", 7) + assert PdfParser.get_value(b" 123 (", 0) == (123, 4) + assert round(abs(PdfParser.get_value(b" 123.4 %", 0)[0] - 123.4), 7) == 0 + assert PdfParser.get_value(b" 123.4 %", 0)[1] == 6 + with pytest.raises(PdfFormatError): + PdfParser.get_value(b"]", 0) + d = PdfParser.get_value(b"<>", 0)[0] + assert isinstance(d, PdfDict) + assert len(d) == 2 + assert d.Name == "value" + assert d[b"Name"] == b"value" + assert d.N == PdfName("V") + a = PdfParser.get_value(b"[/Name (value) /N /V]", 0)[0] + assert isinstance(a, list) + assert len(a) == 4 + assert a[0] == PdfName("Name") + s = PdfParser.get_value( + b"<>\nstream\nabcde\nendstream<<...", 0 + )[0] + assert isinstance(s, PdfStream) + assert s.dictionary.Name == "value" + assert s.decode() == b"abcde" + for name in ["CreationDate", "ModDate"]: + for date, value in { + b"20180729214124": "20180729214124", + b"D:20180729214124": "20180729214124", + b"D:2018072921": "20180729210000", + b"D:20180729214124Z": "20180729214124", + b"D:20180729214124+08'00'": "20180729134124", + b"D:20180729214124-05'00'": "20180730024124", + }.items(): + b = b"<>" + d = PdfParser.get_value(b, 0)[0] + assert time.strftime("%Y%m%d%H%M%S", getattr(d, name)) == value + + +def test_pdf_repr() -> None: + assert bytes(IndirectReference(1, 2)) == b"1 2 R" + assert bytes(IndirectObjectDef(*IndirectReference(1, 2))) == b"1 2 obj" + assert bytes(PdfName(b"Name#Hash")) == b"/Name#23Hash" + assert bytes(PdfName("Name#Hash")) == b"/Name#23Hash" + assert bytes(PdfDict({b"Name": IndirectReference(1, 2)})) == b"<<\n/Name 1 2 R\n>>" + assert bytes(PdfDict({"Name": IndirectReference(1, 2)})) == b"<<\n/Name 1 2 R\n>>" + assert pdf_repr(IndirectReference(1, 2)) == b"1 2 R" + assert pdf_repr(IndirectObjectDef(*IndirectReference(1, 2))) == b"1 2 obj" + assert pdf_repr(PdfName(b"Name#Hash")) == b"/Name#23Hash" + assert pdf_repr(PdfName("Name#Hash")) == b"/Name#23Hash" + assert ( + pdf_repr(PdfDict({b"Name": IndirectReference(1, 2)})) == b"<<\n/Name 1 2 R\n>>" + ) + assert ( + pdf_repr(PdfDict({"Name": IndirectReference(1, 2)})) == b"<<\n/Name 1 2 R\n>>" + ) + assert pdf_repr(123) == b"123" + assert pdf_repr(True) == b"true" + assert pdf_repr(False) == b"false" + assert pdf_repr(None) == b"null" + assert pdf_repr(b"a)/b\\(c") == rb"(a\)/b\\\(c)" + assert pdf_repr([123, True, {"a": PdfName(b"b")}]) == b"[ 123 true <<\n/a /b\n>> ]" + assert pdf_repr(PdfBinary(b"\x90\x1F\xA0")) == b"<901FA0>" + + +def test_duplicate_xref_entry() -> None: + pdf = PdfParser("Tests/images/duplicate_xref_entry.pdf") + assert pdf.xref_table.existing_entries[6][0] == 1197 + pdf.close() diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index ed415953f..5d9f7484d 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -1,152 +1,152 @@ -from __future__ import annotations - -import pickle -from pathlib import Path - -import pytest - -from PIL import Image, ImageDraw, ImageFont - -from .helper import assert_image_equal, skip_unless_feature - -FONT_SIZE = 20 -FONT_PATH = "Tests/fonts/DejaVuSans/DejaVuSans.ttf" - - -def helper_pickle_file( - tmp_path: Path, protocol: int, test_file: str, mode: str | None -) -> None: - # Arrange - with Image.open(test_file) as im: - filename = str(tmp_path / "temp.pkl") - if mode: - im = im.convert(mode) - - # Act - with open(filename, "wb") as f: - pickle.dump(im, f, protocol) - with open(filename, "rb") as f: - loaded_im = pickle.load(f) - - # Assert - assert im == loaded_im - - -def helper_pickle_string(protocol: int, test_file: str, mode: str | None) -> None: - with Image.open(test_file) as im: - if mode: - im = im.convert(mode) - - # Act - dumped_string = pickle.dumps(im, protocol) - loaded_im = pickle.loads(dumped_string) - - # Assert - assert im == loaded_im - - -@pytest.mark.parametrize( - ("test_file", "test_mode"), - [ - ("Tests/images/hopper.jpg", None), - ("Tests/images/hopper.jpg", "L"), - ("Tests/images/hopper.jpg", "PA"), - pytest.param( - "Tests/images/hopper.webp", None, marks=skip_unless_feature("webp") - ), - ("Tests/images/hopper.tif", None), - ("Tests/images/test-card.png", None), - ("Tests/images/zero_bb.png", None), - ("Tests/images/zero_bb_scale2.png", None), - ("Tests/images/non_zero_bb.png", None), - ("Tests/images/non_zero_bb_scale2.png", None), - ("Tests/images/p_trns_single.png", None), - ("Tests/images/pil123p.png", None), - ("Tests/images/itxt_chunks.png", None), - ], -) -@pytest.mark.parametrize("protocol", range(0, pickle.HIGHEST_PROTOCOL + 1)) -def test_pickle_image( - tmp_path: Path, test_file: str, test_mode: str | None, protocol: int -) -> None: - # Act / Assert - helper_pickle_string(protocol, test_file, test_mode) - helper_pickle_file(tmp_path, protocol, test_file, test_mode) - - -def test_pickle_la_mode_with_palette(tmp_path: Path) -> None: - # Arrange - filename = str(tmp_path / "temp.pkl") - with Image.open("Tests/images/hopper.jpg") as im: - im = im.convert("PA") - - # Act / Assert - for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1): - im._mode = "LA" - with open(filename, "wb") as f: - pickle.dump(im, f, protocol) - with open(filename, "rb") as f: - loaded_im = pickle.load(f) - - im._mode = "PA" - assert im == loaded_im - - -@skip_unless_feature("webp") -def test_pickle_tell() -> None: - # Arrange - with Image.open("Tests/images/hopper.webp") as image: - # Act: roundtrip - unpickled_image = pickle.loads(pickle.dumps(image)) - - # Assert - assert unpickled_image.tell() == 0 - - -def helper_assert_pickled_font_images( - font1: ImageFont.FreeTypeFont, font2: ImageFont.FreeTypeFont -) -> None: - # Arrange - im1 = Image.new(mode="RGBA", size=(300, 100)) - im2 = Image.new(mode="RGBA", size=(300, 100)) - draw1 = ImageDraw.Draw(im1) - draw2 = ImageDraw.Draw(im2) - txt = "Hello World!" - - # Act - draw1.text((10, 10), txt, font=font1) - draw2.text((10, 10), txt, font=font2) - - # Assert - assert_image_equal(im1, im2) - - -@skip_unless_feature("freetype2") -@pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1))) -def test_pickle_font_string(protocol: int) -> None: - # Arrange - font = ImageFont.truetype(FONT_PATH, FONT_SIZE) - - # Act: roundtrip - pickled_font = pickle.dumps(font, protocol) - unpickled_font = pickle.loads(pickled_font) - - # Assert - helper_assert_pickled_font_images(font, unpickled_font) - - -@skip_unless_feature("freetype2") -@pytest.mark.parametrize("protocol", list(range(0, 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") - - # Act: roundtrip - with open(filename, "wb") as f: - pickle.dump(font, f, protocol) - with open(filename, "rb") as f: - unpickled_font = pickle.load(f) - - # Assert - helper_assert_pickled_font_images(font, unpickled_font) +from __future__ import annotations + +import pickle +from pathlib import Path + +import pytest + +from PIL import Image, ImageDraw, ImageFont + +from .helper import assert_image_equal, skip_unless_feature + +FONT_SIZE = 20 +FONT_PATH = "Tests/fonts/DejaVuSans/DejaVuSans.ttf" + + +def helper_pickle_file( + tmp_path: Path, protocol: int, test_file: str, mode: str | None +) -> None: + # Arrange + with Image.open(test_file) as im: + filename = str(tmp_path / "temp.pkl") + if mode: + im = im.convert(mode) + + # Act + with open(filename, "wb") as f: + pickle.dump(im, f, protocol) + with open(filename, "rb") as f: + loaded_im = pickle.load(f) + + # Assert + assert im == loaded_im + + +def helper_pickle_string(protocol: int, test_file: str, mode: str | None) -> None: + with Image.open(test_file) as im: + if mode: + im = im.convert(mode) + + # Act + dumped_string = pickle.dumps(im, protocol) + loaded_im = pickle.loads(dumped_string) + + # Assert + assert im == loaded_im + + +@pytest.mark.parametrize( + ("test_file", "test_mode"), + [ + ("Tests/images/hopper.jpg", None), + ("Tests/images/hopper.jpg", "L"), + ("Tests/images/hopper.jpg", "PA"), + pytest.param( + "Tests/images/hopper.webp", None, marks=skip_unless_feature("webp") + ), + ("Tests/images/hopper.tif", None), + ("Tests/images/test-card.png", None), + ("Tests/images/zero_bb.png", None), + ("Tests/images/zero_bb_scale2.png", None), + ("Tests/images/non_zero_bb.png", None), + ("Tests/images/non_zero_bb_scale2.png", None), + ("Tests/images/p_trns_single.png", None), + ("Tests/images/pil123p.png", None), + ("Tests/images/itxt_chunks.png", None), + ], +) +@pytest.mark.parametrize("protocol", range(0, pickle.HIGHEST_PROTOCOL + 1)) +def test_pickle_image( + tmp_path: Path, test_file: str, test_mode: str | None, protocol: int +) -> None: + # Act / Assert + helper_pickle_string(protocol, test_file, test_mode) + helper_pickle_file(tmp_path, protocol, test_file, test_mode) + + +def test_pickle_la_mode_with_palette(tmp_path: Path) -> None: + # Arrange + filename = str(tmp_path / "temp.pkl") + with Image.open("Tests/images/hopper.jpg") as im: + im = im.convert("PA") + + # Act / Assert + for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1): + im._mode = "LA" + with open(filename, "wb") as f: + pickle.dump(im, f, protocol) + with open(filename, "rb") as f: + loaded_im = pickle.load(f) + + im._mode = "PA" + assert im == loaded_im + + +@skip_unless_feature("webp") +def test_pickle_tell() -> None: + # Arrange + with Image.open("Tests/images/hopper.webp") as image: + # Act: roundtrip + unpickled_image = pickle.loads(pickle.dumps(image)) + + # Assert + assert unpickled_image.tell() == 0 + + +def helper_assert_pickled_font_images( + font1: ImageFont.FreeTypeFont, font2: ImageFont.FreeTypeFont +) -> None: + # Arrange + im1 = Image.new(mode="RGBA", size=(300, 100)) + im2 = Image.new(mode="RGBA", size=(300, 100)) + draw1 = ImageDraw.Draw(im1) + draw2 = ImageDraw.Draw(im2) + txt = "Hello World!" + + # Act + draw1.text((10, 10), txt, font=font1) + draw2.text((10, 10), txt, font=font2) + + # Assert + assert_image_equal(im1, im2) + + +@skip_unless_feature("freetype2") +@pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1))) +def test_pickle_font_string(protocol: int) -> None: + # Arrange + font = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + # Act: roundtrip + pickled_font = pickle.dumps(font, protocol) + unpickled_font = pickle.loads(pickled_font) + + # Assert + helper_assert_pickled_font_images(font, unpickled_font) + + +@skip_unless_feature("freetype2") +@pytest.mark.parametrize("protocol", list(range(0, 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") + + # Act: roundtrip + with open(filename, "wb") as f: + pickle.dump(font, f, protocol) + with open(filename, "rb") as f: + unpickled_font = pickle.load(f) + + # Assert + helper_assert_pickled_font_images(font, unpickled_font) diff --git a/Tests/test_psdraw.py b/Tests/test_psdraw.py index 64dfb2c95..d75c8bf85 100644 --- a/Tests/test_psdraw.py +++ b/Tests/test_psdraw.py @@ -1,76 +1,76 @@ -from __future__ import annotations - -import os -import sys -from io import BytesIO -from pathlib import Path - -import pytest - -from PIL import Image, PSDraw - - -def _create_document(ps: PSDraw.PSDraw) -> None: - title = "hopper" - box = (1 * 72, 2 * 72, 7 * 72, 10 * 72) # in points - - ps.begin_document(title) - - # draw diagonal lines in a cross - ps.line((1 * 72, 2 * 72), (7 * 72, 10 * 72)) - ps.line((7 * 72, 2 * 72), (1 * 72, 10 * 72)) - - # draw the image (75 dpi) - with Image.open("Tests/images/hopper.ppm") as im: - ps.image(box, im, 75) - ps.rectangle(box) - - # draw title - ps.setfont("Courier", 36) - ps.text((3 * 72, 4 * 72), title) - - ps.end_document() - - -def test_draw_postscript(tmp_path: Path) -> None: - # Based on Pillow tutorial, but there is no textsize: - # https://pillow.readthedocs.io/en/latest/handbook/tutorial.html#drawing-postscript - - # Arrange - tempfile = str(tmp_path / "temp.ps") - with open(tempfile, "wb") as fp: - # Act - ps = PSDraw.PSDraw(fp) - _create_document(ps) - - # Assert - # Check non-zero file was created - assert os.path.isfile(tempfile) - assert os.path.getsize(tempfile) > 0 - - -@pytest.mark.parametrize("buffer", (True, False)) -def test_stdout(buffer: bool) -> None: - # Temporarily redirect stdout - old_stdout = sys.stdout - - if buffer: - - class MyStdOut: - buffer = BytesIO() - - mystdout = MyStdOut() - else: - mystdout = BytesIO() - - sys.stdout = mystdout - - ps = PSDraw.PSDraw() - _create_document(ps) - - # Reset stdout - sys.stdout = old_stdout - - if buffer: - mystdout = mystdout.buffer - assert mystdout.getvalue() != b"" +from __future__ import annotations + +import os +import sys +from io import BytesIO +from pathlib import Path + +import pytest + +from PIL import Image, PSDraw + + +def _create_document(ps: PSDraw.PSDraw) -> None: + title = "hopper" + box = (1 * 72, 2 * 72, 7 * 72, 10 * 72) # in points + + ps.begin_document(title) + + # draw diagonal lines in a cross + ps.line((1 * 72, 2 * 72), (7 * 72, 10 * 72)) + ps.line((7 * 72, 2 * 72), (1 * 72, 10 * 72)) + + # draw the image (75 dpi) + with Image.open("Tests/images/hopper.ppm") as im: + ps.image(box, im, 75) + ps.rectangle(box) + + # draw title + ps.setfont("Courier", 36) + ps.text((3 * 72, 4 * 72), title) + + ps.end_document() + + +def test_draw_postscript(tmp_path: Path) -> None: + # Based on Pillow tutorial, but there is no textsize: + # https://pillow.readthedocs.io/en/latest/handbook/tutorial.html#drawing-postscript + + # Arrange + tempfile = str(tmp_path / "temp.ps") + with open(tempfile, "wb") as fp: + # Act + ps = PSDraw.PSDraw(fp) + _create_document(ps) + + # Assert + # Check non-zero file was created + assert os.path.isfile(tempfile) + assert os.path.getsize(tempfile) > 0 + + +@pytest.mark.parametrize("buffer", (True, False)) +def test_stdout(buffer: bool) -> None: + # Temporarily redirect stdout + old_stdout = sys.stdout + + if buffer: + + class MyStdOut: + buffer = BytesIO() + + mystdout = MyStdOut() + else: + mystdout = BytesIO() + + sys.stdout = mystdout + + ps = PSDraw.PSDraw() + _create_document(ps) + + # Reset stdout + sys.stdout = old_stdout + + if buffer: + mystdout = mystdout.buffer + assert mystdout.getvalue() != b"" diff --git a/Tests/test_pyroma.py b/Tests/test_pyroma.py index c2f7fe22e..307eb029e 100644 --- a/Tests/test_pyroma.py +++ b/Tests/test_pyroma.py @@ -1,27 +1,27 @@ -from __future__ import annotations - -import pytest - -from PIL import __version__ - -pyroma = pytest.importorskip("pyroma", reason="Pyroma not installed") - - -def test_pyroma() -> None: - # Arrange - data = pyroma.projectdata.get_data(".") - - # Act - rating = pyroma.ratings.rate(data) - - # Assert - if "rc" in __version__: - # Pyroma needs to chill about RC versions and not kill all our tests. - assert rating == ( - 9, - ["The package's version number does not comply with PEP-386."], - ) - - else: - # Should have a perfect score - assert rating == (10, []) +from __future__ import annotations + +import pytest + +from PIL import __version__ + +pyroma = pytest.importorskip("pyroma", reason="Pyroma not installed") + + +def test_pyroma() -> None: + # Arrange + data = pyroma.projectdata.get_data(".") + + # Act + rating = pyroma.ratings.rate(data) + + # Assert + if "rc" in __version__: + # Pyroma needs to chill about RC versions and not kill all our tests. + assert rating == ( + 9, + ["The package's version number does not comply with PEP-386."], + ) + + else: + # Should have a perfect score + assert rating == (10, []) diff --git a/Tests/test_qt_image_qapplication.py b/Tests/test_qt_image_qapplication.py index 28f66891c..0288a5025 100644 --- a/Tests/test_qt_image_qapplication.py +++ b/Tests/test_qt_image_qapplication.py @@ -1,84 +1,84 @@ -from __future__ import annotations - -from pathlib import Path - -import pytest - -from PIL import Image, ImageQt - -from .helper import assert_image_equal_tofile, assert_image_similar, hopper - -if ImageQt.qt_is_installed: - from PIL.ImageQt import QPixmap - - if ImageQt.qt_version == "6": - from PyQt6.QtCore import QPoint - from PyQt6.QtGui import QImage, QPainter, QRegion - from PyQt6.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget - elif ImageQt.qt_version == "side6": - from PySide6.QtCore import QPoint - from PySide6.QtGui import QImage, QPainter, QRegion - from PySide6.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget - - class Example(QWidget): - def __init__(self) -> None: - super().__init__() - - img = hopper().resize((1000, 1000)) - - qimage = ImageQt.ImageQt(img) - - pixmap1 = ImageQt.QPixmap.fromImage(qimage) - - QHBoxLayout(self) # hbox - - lbl = QLabel(self) - # Segfault in the problem - lbl.setPixmap(pixmap1.copy()) - - -def roundtrip(expected: Image.Image) -> None: - result = ImageQt.fromqpixmap(ImageQt.toqpixmap(expected)) - # Qt saves all pixmaps as rgb - assert_image_similar(result, expected.convert("RGB"), 1) - - -@pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed") -def test_sanity(tmp_path: Path) -> None: - # Segfault test - app: QApplication | None = QApplication([]) - ex = Example() - assert app # Silence warning - assert ex # Silence warning - - for mode in ("1", "RGB", "RGBA", "L", "P"): - # to QPixmap - im = hopper(mode) - data = ImageQt.toqpixmap(im) - - assert isinstance(data, QPixmap) - assert not data.isNull() - - # Test saving the file - tempfile = str(tmp_path / f"temp_{mode}.png") - data.save(tempfile) - - # Render the image - qimage = ImageQt.ImageQt(im) - data = QPixmap.fromImage(qimage) - qt_format = QImage.Format if ImageQt.qt_version == "6" else QImage - qimage = QImage(128, 128, qt_format.Format_ARGB32) - painter = QPainter(qimage) - image_label = QLabel() - image_label.setPixmap(data) - image_label.render(painter, QPoint(0, 0), QRegion(0, 0, 128, 128)) - painter.end() - rendered_tempfile = str(tmp_path / f"temp_rendered_{mode}.png") - qimage.save(rendered_tempfile) - assert_image_equal_tofile(im.convert("RGBA"), rendered_tempfile) - - # from QPixmap - roundtrip(hopper(mode)) - - app.quit() - app = None +from __future__ import annotations + +from pathlib import Path + +import pytest + +from PIL import Image, ImageQt + +from .helper import assert_image_equal_tofile, assert_image_similar, hopper + +if ImageQt.qt_is_installed: + from PIL.ImageQt import QPixmap + + if ImageQt.qt_version == "6": + from PyQt6.QtCore import QPoint + from PyQt6.QtGui import QImage, QPainter, QRegion + from PyQt6.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget + elif ImageQt.qt_version == "side6": + from PySide6.QtCore import QPoint + from PySide6.QtGui import QImage, QPainter, QRegion + from PySide6.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget + + class Example(QWidget): + def __init__(self) -> None: + super().__init__() + + img = hopper().resize((1000, 1000)) + + qimage = ImageQt.ImageQt(img) + + pixmap1 = ImageQt.QPixmap.fromImage(qimage) + + QHBoxLayout(self) # hbox + + lbl = QLabel(self) + # Segfault in the problem + lbl.setPixmap(pixmap1.copy()) + + +def roundtrip(expected: Image.Image) -> None: + result = ImageQt.fromqpixmap(ImageQt.toqpixmap(expected)) + # Qt saves all pixmaps as rgb + assert_image_similar(result, expected.convert("RGB"), 1) + + +@pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed") +def test_sanity(tmp_path: Path) -> None: + # Segfault test + app: QApplication | None = QApplication([]) + ex = Example() + assert app # Silence warning + assert ex # Silence warning + + for mode in ("1", "RGB", "RGBA", "L", "P"): + # to QPixmap + im = hopper(mode) + data = ImageQt.toqpixmap(im) + + assert isinstance(data, QPixmap) + assert not data.isNull() + + # Test saving the file + tempfile = str(tmp_path / f"temp_{mode}.png") + data.save(tempfile) + + # Render the image + qimage = ImageQt.ImageQt(im) + data = QPixmap.fromImage(qimage) + qt_format = QImage.Format if ImageQt.qt_version == "6" else QImage + qimage = QImage(128, 128, qt_format.Format_ARGB32) + painter = QPainter(qimage) + image_label = QLabel() + image_label.setPixmap(data) + image_label.render(painter, QPoint(0, 0), QRegion(0, 0, 128, 128)) + painter.end() + rendered_tempfile = str(tmp_path / f"temp_rendered_{mode}.png") + qimage.save(rendered_tempfile) + assert_image_equal_tofile(im.convert("RGBA"), rendered_tempfile) + + # from QPixmap + roundtrip(hopper(mode)) + + app.quit() + app = None diff --git a/Tests/test_qt_image_toqimage.py b/Tests/test_qt_image_toqimage.py index 6110be707..03f479c23 100644 --- a/Tests/test_qt_image_toqimage.py +++ b/Tests/test_qt_image_toqimage.py @@ -1,47 +1,47 @@ -from __future__ import annotations - -from pathlib import Path - -import pytest - -from PIL import ImageQt - -from .helper import assert_image_equal, assert_image_equal_tofile, hopper - -pytestmark = pytest.mark.skipif( - not ImageQt.qt_is_installed, reason="Qt bindings are not installed" -) - -if ImageQt.qt_is_installed: - from PIL.ImageQt import QImage - - -@pytest.mark.parametrize("mode", ("RGB", "RGBA", "L", "P", "1")) -def test_sanity(mode: str, tmp_path: Path) -> None: - src = hopper(mode) - data = ImageQt.toqimage(src) - - assert isinstance(data, QImage) - assert not data.isNull() - - # reload directly from the qimage - rt = ImageQt.fromqimage(data) - if mode in ("L", "P", "1"): - assert_image_equal(rt, src.convert("RGB")) - else: - assert_image_equal(rt, src) - - if mode == "1": - # BW appears to not save correctly on Qt - # kicks out errors on console: - # libpng warning: Invalid color type/bit depth combination - # in IHDR - # libpng error: Invalid IHDR data - return - - # Test saving the file - tempfile = str(tmp_path / f"temp_{mode}.png") - data.save(tempfile) - - # Check that it actually worked. - assert_image_equal_tofile(src, tempfile) +from __future__ import annotations + +from pathlib import Path + +import pytest + +from PIL import ImageQt + +from .helper import assert_image_equal, assert_image_equal_tofile, hopper + +pytestmark = pytest.mark.skipif( + not ImageQt.qt_is_installed, reason="Qt bindings are not installed" +) + +if ImageQt.qt_is_installed: + from PIL.ImageQt import QImage + + +@pytest.mark.parametrize("mode", ("RGB", "RGBA", "L", "P", "1")) +def test_sanity(mode: str, tmp_path: Path) -> None: + src = hopper(mode) + data = ImageQt.toqimage(src) + + assert isinstance(data, QImage) + assert not data.isNull() + + # reload directly from the qimage + rt = ImageQt.fromqimage(data) + if mode in ("L", "P", "1"): + assert_image_equal(rt, src.convert("RGB")) + else: + assert_image_equal(rt, src) + + if mode == "1": + # BW appears to not save correctly on Qt + # kicks out errors on console: + # libpng warning: Invalid color type/bit depth combination + # in IHDR + # libpng error: Invalid IHDR data + return + + # Test saving the file + tempfile = str(tmp_path / f"temp_{mode}.png") + data.save(tempfile) + + # Check that it actually worked. + assert_image_equal_tofile(src, tempfile) diff --git a/Tests/test_sgi_crash.py b/Tests/test_sgi_crash.py index 3ce31cd2d..7b6fba32c 100644 --- a/Tests/test_sgi_crash.py +++ b/Tests/test_sgi_crash.py @@ -1,28 +1,28 @@ -from __future__ import annotations - -import pytest - -from PIL import Image - - -@pytest.mark.parametrize( - "test_file", - [ - "Tests/images/sgi_overrun_expandrowF04.bin", - "Tests/images/sgi_crash.bin", - "Tests/images/crash-6b7f2244da6d0ae297ee0754a424213444e92778.sgi", - "Tests/images/ossfuzz-5730089102868480.sgi", - "Tests/images/crash-754d9c7ec485ffb76a90eeaab191ef69a2a3a3cd.sgi", - "Tests/images/crash-465703f71a0f0094873a3e0e82c9f798161171b8.sgi", - "Tests/images/crash-64834657ee604b8797bf99eac6a194c124a9a8ba.sgi", - "Tests/images/crash-abcf1c97b8fe42a6c68f1fb0b978530c98d57ced.sgi", - "Tests/images/crash-b82e64d4f3f76d7465b6af535283029eda211259.sgi", - "Tests/images/crash-c1b2595b8b0b92cc5f38b6635e98e3a119ade807.sgi", - "Tests/images/crash-db8bfa78b19721225425530c5946217720d7df4e.sgi", - ], -) -def test_crashes(test_file: str) -> None: - with open(test_file, "rb") as f: - with Image.open(f) as im: - with pytest.raises(OSError): - im.load() +from __future__ import annotations + +import pytest + +from PIL import Image + + +@pytest.mark.parametrize( + "test_file", + [ + "Tests/images/sgi_overrun_expandrowF04.bin", + "Tests/images/sgi_crash.bin", + "Tests/images/crash-6b7f2244da6d0ae297ee0754a424213444e92778.sgi", + "Tests/images/ossfuzz-5730089102868480.sgi", + "Tests/images/crash-754d9c7ec485ffb76a90eeaab191ef69a2a3a3cd.sgi", + "Tests/images/crash-465703f71a0f0094873a3e0e82c9f798161171b8.sgi", + "Tests/images/crash-64834657ee604b8797bf99eac6a194c124a9a8ba.sgi", + "Tests/images/crash-abcf1c97b8fe42a6c68f1fb0b978530c98d57ced.sgi", + "Tests/images/crash-b82e64d4f3f76d7465b6af535283029eda211259.sgi", + "Tests/images/crash-c1b2595b8b0b92cc5f38b6635e98e3a119ade807.sgi", + "Tests/images/crash-db8bfa78b19721225425530c5946217720d7df4e.sgi", + ], +) +def test_crashes(test_file: str) -> None: + with open(test_file, "rb") as f: + with Image.open(f) as im: + with pytest.raises(OSError): + im.load() diff --git a/Tests/test_shell_injection.py b/Tests/test_shell_injection.py index 2a072fd44..943a1eea8 100644 --- a/Tests/test_shell_injection.py +++ b/Tests/test_shell_injection.py @@ -1,58 +1,58 @@ -from __future__ import annotations - -import shutil -from pathlib import Path -from typing import Callable - -import pytest - -from PIL import GifImagePlugin, Image, JpegImagePlugin - -from .helper import cjpeg_available, djpeg_available, is_win32, netpbm_available - -TEST_JPG = "Tests/images/hopper.jpg" -TEST_GIF = "Tests/images/hopper.gif" - -test_filenames = ("temp_';", 'temp_";', "temp_'\"|", "temp_'\"||", "temp_'\"&&") - - -@pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS") -class TestShellInjection: - def assert_save_filename_check( - self, - tmp_path: Path, - src_img: Image.Image, - save_func: Callable[[Image.Image, int, str], None], - ) -> None: - for filename in test_filenames: - dest_file = str(tmp_path / filename) - save_func(src_img, 0, dest_file) - # If file can't be opened, shell injection probably occurred - with Image.open(dest_file) as im: - im.load() - - @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) - shutil.copy(TEST_JPG, src_file) - - with Image.open(src_file) as im: - im.load_djpeg() - - @pytest.mark.skipif(not cjpeg_available(), reason="cjpeg not available") - def test_save_cjpeg_filename(self, tmp_path: Path) -> None: - with Image.open(TEST_JPG) as im: - self.assert_save_filename_check(tmp_path, im, JpegImagePlugin._save_cjpeg) - - @pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") - def test_save_netpbm_filename_bmp_mode(self, tmp_path: Path) -> None: - with Image.open(TEST_GIF) as im: - im = im.convert("RGB") - self.assert_save_filename_check(tmp_path, im, GifImagePlugin._save_netpbm) - - @pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") - def test_save_netpbm_filename_l_mode(self, tmp_path: Path) -> None: - with Image.open(TEST_GIF) as im: - im = im.convert("L") - self.assert_save_filename_check(tmp_path, im, GifImagePlugin._save_netpbm) +from __future__ import annotations + +import shutil +from pathlib import Path +from typing import Callable + +import pytest + +from PIL import GifImagePlugin, Image, JpegImagePlugin + +from .helper import cjpeg_available, djpeg_available, is_win32, netpbm_available + +TEST_JPG = "Tests/images/hopper.jpg" +TEST_GIF = "Tests/images/hopper.gif" + +test_filenames = ("temp_';", 'temp_";', "temp_'\"|", "temp_'\"||", "temp_'\"&&") + + +@pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS") +class TestShellInjection: + def assert_save_filename_check( + self, + tmp_path: Path, + src_img: Image.Image, + save_func: Callable[[Image.Image, int, str], None], + ) -> None: + for filename in test_filenames: + dest_file = str(tmp_path / filename) + save_func(src_img, 0, dest_file) + # If file can't be opened, shell injection probably occurred + with Image.open(dest_file) as im: + im.load() + + @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) + shutil.copy(TEST_JPG, src_file) + + with Image.open(src_file) as im: + im.load_djpeg() + + @pytest.mark.skipif(not cjpeg_available(), reason="cjpeg not available") + def test_save_cjpeg_filename(self, tmp_path: Path) -> None: + with Image.open(TEST_JPG) as im: + self.assert_save_filename_check(tmp_path, im, JpegImagePlugin._save_cjpeg) + + @pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") + def test_save_netpbm_filename_bmp_mode(self, tmp_path: Path) -> None: + with Image.open(TEST_GIF) as im: + im = im.convert("RGB") + self.assert_save_filename_check(tmp_path, im, GifImagePlugin._save_netpbm) + + @pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") + def test_save_netpbm_filename_l_mode(self, tmp_path: Path) -> None: + with Image.open(TEST_GIF) as im: + im = im.convert("L") + self.assert_save_filename_check(tmp_path, im, GifImagePlugin._save_netpbm) diff --git a/Tests/test_tiff_crashes.py b/Tests/test_tiff_crashes.py index 073e5415c..c623afa9c 100644 --- a/Tests/test_tiff_crashes.py +++ b/Tests/test_tiff_crashes.py @@ -1,54 +1,54 @@ -# Reproductions/tests for crashes/read errors in TiffDecode.c - -# When run in Python, all of these images should fail for -# one reason or another, either as a buffer overrun, -# unrecognized datastream, or truncated image file. -# There shouldn't be any segfaults. -# -# if run like -# `valgrind --tool=memcheck pytest test_tiff_crashes.py 2>&1 | grep TiffDecode.c` -# the output should be empty. There may be Python issues -# in the valgrind especially if run in a debug Python -# version. -from __future__ import annotations - -import pytest - -from PIL import Image - -from .helper import on_ci - - -@pytest.mark.parametrize( - "test_file", - [ - "Tests/images/crash_1.tif", - "Tests/images/crash_2.tif", - "Tests/images/crash-2020-10-test.tif", - "Tests/images/crash-0c7e0e8e11ce787078f00b5b0ca409a167f070e0.tif", - "Tests/images/crash-0e16d3bfb83be87356d026d66919deaefca44dac.tif", - "Tests/images/crash-1152ec2d1a1a71395b6f2ce6721c38924d025bf3.tif", - "Tests/images/crash-1185209cf7655b5aed8ae5e77784dfdd18ab59e9.tif", - "Tests/images/crash-338516dbd2f0e83caddb8ce256c22db3bd6dc40f.tif", - "Tests/images/crash-4f085cc12ece8cde18758d42608bed6a2a2cfb1c.tif", - "Tests/images/crash-86214e58da443d2b80820cff9677a38a33dcbbca.tif", - "Tests/images/crash-f46f5b2f43c370fe65706c11449f567ecc345e74.tif", - "Tests/images/crash-63b1dffefc8c075ddc606c0a2f5fdc15ece78863.tif", - "Tests/images/crash-74d2a78403a5a59db1fb0a2b8735ac068a75f6e3.tif", - "Tests/images/crash-81154a65438ba5aaeca73fd502fa4850fbde60f8.tif", - "Tests/images/crash-0da013a13571cc8eb457a39fee8db18f8a3c7127.tif", - ], -) -@pytest.mark.filterwarnings("ignore:Possibly corrupt EXIF data") -@pytest.mark.filterwarnings("ignore:Metadata warning") -@pytest.mark.filterwarnings("ignore:Truncated File Read") -def test_tiff_crashes(test_file: str) -> None: - try: - with Image.open(test_file) as im: - im.load() - except FileNotFoundError: - if on_ci(): - raise - pytest.skip("test image not found") - except OSError: - pass +# Reproductions/tests for crashes/read errors in TiffDecode.c + +# When run in Python, all of these images should fail for +# one reason or another, either as a buffer overrun, +# unrecognized datastream, or truncated image file. +# There shouldn't be any segfaults. +# +# if run like +# `valgrind --tool=memcheck pytest test_tiff_crashes.py 2>&1 | grep TiffDecode.c` +# the output should be empty. There may be Python issues +# in the valgrind especially if run in a debug Python +# version. +from __future__ import annotations + +import pytest + +from PIL import Image + +from .helper import on_ci + + +@pytest.mark.parametrize( + "test_file", + [ + "Tests/images/crash_1.tif", + "Tests/images/crash_2.tif", + "Tests/images/crash-2020-10-test.tif", + "Tests/images/crash-0c7e0e8e11ce787078f00b5b0ca409a167f070e0.tif", + "Tests/images/crash-0e16d3bfb83be87356d026d66919deaefca44dac.tif", + "Tests/images/crash-1152ec2d1a1a71395b6f2ce6721c38924d025bf3.tif", + "Tests/images/crash-1185209cf7655b5aed8ae5e77784dfdd18ab59e9.tif", + "Tests/images/crash-338516dbd2f0e83caddb8ce256c22db3bd6dc40f.tif", + "Tests/images/crash-4f085cc12ece8cde18758d42608bed6a2a2cfb1c.tif", + "Tests/images/crash-86214e58da443d2b80820cff9677a38a33dcbbca.tif", + "Tests/images/crash-f46f5b2f43c370fe65706c11449f567ecc345e74.tif", + "Tests/images/crash-63b1dffefc8c075ddc606c0a2f5fdc15ece78863.tif", + "Tests/images/crash-74d2a78403a5a59db1fb0a2b8735ac068a75f6e3.tif", + "Tests/images/crash-81154a65438ba5aaeca73fd502fa4850fbde60f8.tif", + "Tests/images/crash-0da013a13571cc8eb457a39fee8db18f8a3c7127.tif", + ], +) +@pytest.mark.filterwarnings("ignore:Possibly corrupt EXIF data") +@pytest.mark.filterwarnings("ignore:Metadata warning") +@pytest.mark.filterwarnings("ignore:Truncated File Read") +def test_tiff_crashes(test_file: str) -> None: + try: + with Image.open(test_file) as im: + im.load() + except FileNotFoundError: + if on_ci(): + raise + pytest.skip("test image not found") + except OSError: + pass diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py index ae80b98b8..428582c97 100644 --- a/Tests/test_tiff_ifdrational.py +++ b/Tests/test_tiff_ifdrational.py @@ -1,71 +1,71 @@ -from __future__ import annotations - -from fractions import Fraction -from pathlib import Path - -import pytest - -from PIL import Image, TiffImagePlugin -from PIL.TiffImagePlugin import IFDRational - -from .helper import hopper, skip_unless_feature - - -def _test_equal(num, denom, target) -> None: - t = IFDRational(num, denom) - - assert target == t - assert t == target - - -def test_sanity() -> None: - _test_equal(1, 1, 1) - _test_equal(1, 1, Fraction(1, 1)) - - _test_equal(2, 2, 1) - _test_equal(1.0, 1, Fraction(1, 1)) - - _test_equal(Fraction(1, 1), 1, Fraction(1, 1)) - _test_equal(IFDRational(1, 1), 1, 1) - - _test_equal(1, 2, Fraction(1, 2)) - _test_equal(1, 2, IFDRational(1, 2)) - - _test_equal(7, 5, 1.4) - - -def test_ranges() -> None: - for num in range(1, 10): - for denom in range(1, 10): - assert IFDRational(num, denom) == IFDRational(num, denom) - - -def test_nonetype() -> None: - # Fails if the _delegate function doesn't return a valid function - - xres = IFDRational(72) - yres = IFDRational(72) - assert xres._val is not None - assert xres.numerator is not None - assert xres.denominator is not None - assert yres._val is not None - - assert xres and 1 - assert xres and yres - - -@pytest.mark.parametrize( - "libtiff", (pytest.param(True, marks=skip_unless_feature("libtiff")), False) -) -def test_ifd_rational_save( - monkeypatch: pytest.MonkeyPatch, tmp_path: Path, libtiff: bool -) -> None: - im = hopper() - out = str(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 float(IFDRational(301, 1)) == float(reloaded.tag_v2[282]) +from __future__ import annotations + +from fractions import Fraction +from pathlib import Path + +import pytest + +from PIL import Image, TiffImagePlugin +from PIL.TiffImagePlugin import IFDRational + +from .helper import hopper, skip_unless_feature + + +def _test_equal(num, denom, target) -> None: + t = IFDRational(num, denom) + + assert target == t + assert t == target + + +def test_sanity() -> None: + _test_equal(1, 1, 1) + _test_equal(1, 1, Fraction(1, 1)) + + _test_equal(2, 2, 1) + _test_equal(1.0, 1, Fraction(1, 1)) + + _test_equal(Fraction(1, 1), 1, Fraction(1, 1)) + _test_equal(IFDRational(1, 1), 1, 1) + + _test_equal(1, 2, Fraction(1, 2)) + _test_equal(1, 2, IFDRational(1, 2)) + + _test_equal(7, 5, 1.4) + + +def test_ranges() -> None: + for num in range(1, 10): + for denom in range(1, 10): + assert IFDRational(num, denom) == IFDRational(num, denom) + + +def test_nonetype() -> None: + # Fails if the _delegate function doesn't return a valid function + + xres = IFDRational(72) + yres = IFDRational(72) + assert xres._val is not None + assert xres.numerator is not None + assert xres.denominator is not None + assert yres._val is not None + + assert xres and 1 + assert xres and yres + + +@pytest.mark.parametrize( + "libtiff", (pytest.param(True, marks=skip_unless_feature("libtiff")), False) +) +def test_ifd_rational_save( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path, libtiff: bool +) -> None: + im = hopper() + out = str(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 float(IFDRational(301, 1)) == float(reloaded.tag_v2[282]) diff --git a/Tests/test_uploader.py b/Tests/test_uploader.py index d55ceb4be..84446a957 100644 --- a/Tests/test_uploader.py +++ b/Tests/test_uploader.py @@ -1,15 +1,15 @@ -from __future__ import annotations - -from .helper import assert_image_equal, assert_image_similar, hopper - - -def check_upload_equal() -> None: - result = hopper("P").convert("RGB") - target = hopper("RGB") - assert_image_equal(result, target) - - -def check_upload_similar() -> None: - result = hopper("P").convert("RGB") - target = hopper("RGB") - assert_image_similar(result, target, 0) +from __future__ import annotations + +from .helper import assert_image_equal, assert_image_similar, hopper + + +def check_upload_equal() -> None: + result = hopper("P").convert("RGB") + target = hopper("RGB") + assert_image_equal(result, target) + + +def check_upload_similar() -> None: + result = hopper("P").convert("RGB") + target = hopper("RGB") + assert_image_similar(result, target, 0) diff --git a/Tests/test_util.py b/Tests/test_util.py index 197ef79ee..488f4db2a 100644 --- a/Tests/test_util.py +++ b/Tests/test_util.py @@ -1,63 +1,63 @@ -from __future__ import annotations - -from pathlib import Path, PurePath - -import pytest - -from PIL import _util - - -@pytest.mark.parametrize( - "test_path", ["filename.ext", Path("filename.ext"), PurePath("filename.ext")] -) -def test_is_path(test_path: str | Path | PurePath) -> None: - # Act - it_is = _util.is_path(test_path) - - # Assert - assert it_is - - -def test_is_not_path(tmp_path: Path) -> None: - # Arrange - with (tmp_path / "temp.ext").open("w") as fp: - pass - - # Act - it_is_not = _util.is_path(fp) - - # Assert - assert not it_is_not - - -def test_is_directory() -> None: - # Arrange - directory = "Tests" - - # Act - it_is = _util.is_directory(directory) - - # Assert - assert it_is - - -def test_is_not_directory() -> None: - # Arrange - text = "abc" - - # Act - it_is_not = _util.is_directory(text) - - # Assert - assert not it_is_not - - -def test_deferred_error() -> None: - # Arrange - - # Act - thing = _util.DeferredError.new(ValueError("Some error text")) - - # Assert - with pytest.raises(ValueError): - thing.some_attr +from __future__ import annotations + +from pathlib import Path, PurePath + +import pytest + +from PIL import _util + + +@pytest.mark.parametrize( + "test_path", ["filename.ext", Path("filename.ext"), PurePath("filename.ext")] +) +def test_is_path(test_path: str | Path | PurePath) -> None: + # Act + it_is = _util.is_path(test_path) + + # Assert + assert it_is + + +def test_is_not_path(tmp_path: Path) -> None: + # Arrange + with (tmp_path / "temp.ext").open("w") as fp: + pass + + # Act + it_is_not = _util.is_path(fp) + + # Assert + assert not it_is_not + + +def test_is_directory() -> None: + # Arrange + directory = "Tests" + + # Act + it_is = _util.is_directory(directory) + + # Assert + assert it_is + + +def test_is_not_directory() -> None: + # Arrange + text = "abc" + + # Act + it_is_not = _util.is_directory(text) + + # Assert + assert not it_is_not + + +def test_deferred_error() -> None: + # Arrange + + # Act + thing = _util.DeferredError.new(ValueError("Some error text")) + + # Assert + with pytest.raises(ValueError): + thing.some_attr diff --git a/Tests/test_webp_leaks.py b/Tests/test_webp_leaks.py index 626fe427c..6fa3932fb 100644 --- a/Tests/test_webp_leaks.py +++ b/Tests/test_webp_leaks.py @@ -1,25 +1,25 @@ -from __future__ import annotations - -from io import BytesIO - -from PIL import Image - -from .helper import PillowLeakTestCase, skip_unless_feature - -test_file = "Tests/images/hopper.webp" - - -@skip_unless_feature("webp") -class TestWebPLeaks(PillowLeakTestCase): - mem_limit = 3 * 1024 # kb - iterations = 100 - - def test_leak_load(self) -> None: - with open(test_file, "rb") as f: - im_data = f.read() - - def core() -> None: - with Image.open(BytesIO(im_data)) as im: - im.load() - - self._test_leak(core) +from __future__ import annotations + +from io import BytesIO + +from PIL import Image + +from .helper import PillowLeakTestCase, skip_unless_feature + +test_file = "Tests/images/hopper.webp" + + +@skip_unless_feature("webp") +class TestWebPLeaks(PillowLeakTestCase): + mem_limit = 3 * 1024 # kb + iterations = 100 + + def test_leak_load(self) -> None: + with open(test_file, "rb") as f: + im_data = f.read() + + def core() -> None: + with Image.open(BytesIO(im_data)) as im: + im.load() + + self._test_leak(core) diff --git a/_custom_build/backend.py b/_custom_build/backend.py index 44ec6efe5..b2bf99ab8 100644 --- a/_custom_build/backend.py +++ b/_custom_build/backend.py @@ -1,39 +1,39 @@ -from __future__ import annotations - -import sys - -from setuptools.build_meta import * # noqa: F403 -from setuptools.build_meta import build_wheel - -backend_class = build_wheel.__self__.__class__ - - -class _CustomBuildMetaBackend(backend_class): - def run_setup(self, setup_script="setup.py"): - if self.config_settings: - for key, values in self.config_settings.items(): - if not isinstance(values, list): - values = [values] - for value in values: - sys.argv.append(f"--pillow-configuration={key}={value}") - - return super().run_setup(setup_script) - - def build_wheel( - self, wheel_directory, config_settings=None, metadata_directory=None - ): - self.config_settings = config_settings - return super().build_wheel(wheel_directory, config_settings, metadata_directory) - - def build_editable( - self, wheel_directory, config_settings=None, metadata_directory=None - ): - self.config_settings = config_settings - return super().build_editable( - wheel_directory, config_settings, metadata_directory - ) - - -_backend = _CustomBuildMetaBackend() -build_wheel = _backend.build_wheel -build_editable = _backend.build_editable +from __future__ import annotations + +import sys + +from setuptools.build_meta import * # noqa: F403 +from setuptools.build_meta import build_wheel + +backend_class = build_wheel.__self__.__class__ + + +class _CustomBuildMetaBackend(backend_class): + def run_setup(self, setup_script="setup.py"): + if self.config_settings: + for key, values in self.config_settings.items(): + if not isinstance(values, list): + values = [values] + for value in values: + sys.argv.append(f"--pillow-configuration={key}={value}") + + return super().run_setup(setup_script) + + def build_wheel( + self, wheel_directory, config_settings=None, metadata_directory=None + ): + self.config_settings = config_settings + return super().build_wheel(wheel_directory, config_settings, metadata_directory) + + def build_editable( + self, wheel_directory, config_settings=None, metadata_directory=None + ): + self.config_settings = config_settings + return super().build_editable( + wheel_directory, config_settings, metadata_directory + ) + + +_backend = _CustomBuildMetaBackend() +build_wheel = _backend.build_wheel +build_editable = _backend.build_editable diff --git a/codecov.yml b/codecov.yml index 1ea7974eb..01b68b72b 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,22 +1,22 @@ -# Documentation: https://docs.codecov.com/docs/codecov-yaml - -codecov: - # Avoid "Missing base report" due to committing CHANGES.rst with "[CI skip]" - # https://github.com/codecov/support/issues/363 - # https://docs.codecov.com/docs/comparing-commits - allow_coverage_offsets: true - -comment: false - -coverage: - status: - project: - default: - threshold: 0.1% - -# Matches 'omit:' in .coveragerc -ignore: - - "Tests/32bit_segfault_check.py" - - "Tests/bench_cffi_access.py" - - "Tests/check_*.py" - - "Tests/createfontdatachunk.py" +# Documentation: https://docs.codecov.com/docs/codecov-yaml + +codecov: + # Avoid "Missing base report" due to committing CHANGES.rst with "[CI skip]" + # https://github.com/codecov/support/issues/363 + # https://docs.codecov.com/docs/comparing-commits + allow_coverage_offsets: true + +comment: false + +coverage: + status: + project: + default: + threshold: 0.1% + +# Matches 'omit:' in .coveragerc +ignore: + - "Tests/32bit_segfault_check.py" + - "Tests/bench_cffi_access.py" + - "Tests/check_*.py" + - "Tests/createfontdatachunk.py" diff --git a/conftest.py b/conftest.py index 4dcd5e053..5fda4f477 100644 --- a/conftest.py +++ b/conftest.py @@ -1,3 +1,3 @@ -from __future__ import annotations - -pytest_plugins = ["Tests.helper"] +from __future__ import annotations + +pytest_plugins = ["Tests.helper"] diff --git a/coveragetool.py b/coveragetool.py new file mode 100644 index 000000000..60b74e26e --- /dev/null +++ b/coveragetool.py @@ -0,0 +1,22 @@ +from PIL import Image +from PIL.Image import branches + +# Define a function to calculate and print branch coverage + + +def calculate_branch_coverage(): + num_branches = len(branches) + branch_covered = {key: value for key, value in branches.items() if value is True} + sum_branches = len(branch_covered) + coverage = (sum_branches/num_branches) * 100 + print(f"Branches covered: {sum_branches}") + print(f"Total branches: {num_branches}") + print("\nBRANCH COVERAGE:", coverage, "%\n") + +R = Image.new('L', (100, 100), color=255) +G = Image.new('L', (100, 100), color=128) +B = Image.new('L', (100, 100), color=0) +merged_image = Image.merge('RGB', (R, G, B)) +merged_image.save('merged_image.png') + +calculate_branch_coverage() diff --git a/depends/README.rst b/depends/README.rst index b69c9dcbf..43662eb21 100644 --- a/depends/README.rst +++ b/depends/README.rst @@ -1,9 +1,9 @@ -Depends -======= - -``install_openjpeg.sh``, ``install_webp.sh``, ``install_imagequant.sh``, -``install_raqm.sh`` and ``install_raqm_cmake.sh`` can be used to download, -build & install non-packaged dependencies; useful for testing on CI. - -``install_extra_test_images.sh`` can be used to install additional test images -that are used by CI. +Depends +======= + +``install_openjpeg.sh``, ``install_webp.sh``, ``install_imagequant.sh``, +``install_raqm.sh`` and ``install_raqm_cmake.sh`` can be used to download, +build & install non-packaged dependencies; useful for testing on CI. + +``install_extra_test_images.sh`` can be used to install additional test images +that are used by CI. diff --git a/depends/download-and-extract.sh b/depends/download-and-extract.sh index 04bfbc755..c4a8800f3 100755 --- a/depends/download-and-extract.sh +++ b/depends/download-and-extract.sh @@ -1,12 +1,12 @@ -#!/bin/sh -# Usage: ./download-and-extract.sh something https://example.com/something.tar.gz - -archive=$1 -url=$2 - -if [ ! -f $archive.tar.gz ]; then - wget --no-verbose -O $archive.tar.gz $url -fi - -rmdir $archive -tar -xvzf $archive.tar.gz +#!/bin/sh +# Usage: ./download-and-extract.sh something https://example.com/something.tar.gz + +archive=$1 +url=$2 + +if [ ! -f $archive.tar.gz ]; then + wget --no-verbose -O $archive.tar.gz $url +fi + +rmdir $archive +tar -xvzf $archive.tar.gz diff --git a/depends/install_extra_test_images.sh b/depends/install_extra_test_images.sh index 1ef6f4e97..6bd20aa56 100755 --- a/depends/install_extra_test_images.sh +++ b/depends/install_extra_test_images.sh @@ -1,12 +1,12 @@ -#!/usr/bin/env bash -# install extra test images - -archive=test-images-main - -./download-and-extract.sh $archive https://github.com/python-pillow/test-images/archive/main.tar.gz - -mv $archive/* ../Tests/images/ - -# Cleanup old tarball and empty directory -rm $archive.tar.gz -rmdir $archive +#!/usr/bin/env bash +# install extra test images + +archive=test-images-main + +./download-and-extract.sh $archive https://github.com/python-pillow/test-images/archive/main.tar.gz + +mv $archive/* ../Tests/images/ + +# Cleanup old tarball and empty directory +rm $archive.tar.gz +rmdir $archive diff --git a/depends/install_imagequant.sh b/depends/install_imagequant.sh index 9dd7742ed..d9d46cb46 100755 --- a/depends/install_imagequant.sh +++ b/depends/install_imagequant.sh @@ -1,39 +1,39 @@ -#!/bin/bash -# install libimagequant - -archive_name=libimagequant -archive_version=4.3.1 - -archive=$archive_name-$archive_version - -if [[ "$GHA_LIBIMAGEQUANT_CACHE_HIT" == "true" ]]; then - - # Copy cached files into place - sudo cp ~/cache-$archive_name/libimagequant.so* /usr/lib/ - sudo cp ~/cache-$archive_name/libimagequant.h /usr/include/ - -else - - # Build from source - ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz - - pushd $archive/imagequant-sys - - cargo install cargo-c - cargo cinstall --prefix=/usr --destdir=. - - # Copy into place - sudo cp usr/lib/libimagequant.so* /usr/lib/ - sudo cp usr/include/libimagequant.h /usr/include/ - - if [ -n "$GITHUB_ACTIONS" ]; then - # Copy to cache - rm -rf ~/cache-$archive_name - mkdir ~/cache-$archive_name - cp usr/lib/libimagequant.so* ~/cache-$archive_name/ - cp usr/include/libimagequant.h ~/cache-$archive_name/ - fi - - popd - -fi +#!/bin/bash +# install libimagequant + +archive_name=libimagequant +archive_version=4.3.1 + +archive=$archive_name-$archive_version + +if [[ "$GHA_LIBIMAGEQUANT_CACHE_HIT" == "true" ]]; then + + # Copy cached files into place + sudo cp ~/cache-$archive_name/libimagequant.so* /usr/lib/ + sudo cp ~/cache-$archive_name/libimagequant.h /usr/include/ + +else + + # Build from source + ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz + + pushd $archive/imagequant-sys + + cargo install cargo-c + cargo cinstall --prefix=/usr --destdir=. + + # Copy into place + sudo cp usr/lib/libimagequant.so* /usr/lib/ + sudo cp usr/include/libimagequant.h /usr/include/ + + if [ -n "$GITHUB_ACTIONS" ]; then + # Copy to cache + rm -rf ~/cache-$archive_name + mkdir ~/cache-$archive_name + cp usr/lib/libimagequant.so* ~/cache-$archive_name/ + cp usr/include/libimagequant.h ~/cache-$archive_name/ + fi + + popd + +fi diff --git a/depends/install_openjpeg.sh b/depends/install_openjpeg.sh index 8c2967bc2..9d24a75ce 100755 --- a/depends/install_openjpeg.sh +++ b/depends/install_openjpeg.sh @@ -1,12 +1,12 @@ -#!/bin/bash -# install openjpeg - -archive=openjpeg-2.5.2 - -./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz - -pushd $archive - -cmake -DCMAKE_INSTALL_PREFIX=/usr . && make -j4 && sudo make -j4 install - -popd +#!/bin/bash +# install openjpeg + +archive=openjpeg-2.5.2 + +./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz + +pushd $archive + +cmake -DCMAKE_INSTALL_PREFIX=/usr . && make -j4 && sudo make -j4 install + +popd diff --git a/depends/install_raqm.sh b/depends/install_raqm.sh index 070ba23a1..3c4e17b44 100755 --- a/depends/install_raqm.sh +++ b/depends/install_raqm.sh @@ -1,13 +1,13 @@ -#!/usr/bin/env bash -# install raqm - - -archive=libraqm-0.10.1 - -./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz - -pushd $archive - -meson build --prefix=/usr && sudo ninja -C build install - -popd +#!/usr/bin/env bash +# install raqm + + +archive=libraqm-0.10.1 + +./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz + +pushd $archive + +meson build --prefix=/usr && sudo ninja -C build install + +popd diff --git a/depends/install_raqm_cmake.sh b/depends/install_raqm_cmake.sh index 37d9d1160..12b9047d9 100755 --- a/depends/install_raqm_cmake.sh +++ b/depends/install_raqm_cmake.sh @@ -1,17 +1,17 @@ -#!/usr/bin/env bash -# install raqm - - -archive=raqm-cmake-99300ff3 - -./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz - -pushd $archive - -mkdir build -cd build -cmake .. -make && sudo make install -cd .. - -popd +#!/usr/bin/env bash +# install raqm + + +archive=raqm-cmake-99300ff3 + +./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz + +pushd $archive + +mkdir build +cd build +cmake .. +make && sudo make install +cd .. + +popd diff --git a/depends/install_webp.sh b/depends/install_webp.sh index c47fb35f1..f6a6e5366 100755 --- a/depends/install_webp.sh +++ b/depends/install_webp.sh @@ -1,12 +1,12 @@ -#!/bin/bash -# install webp - -archive=libwebp-1.4.0 - -./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz - -pushd $archive - -./configure --prefix=/usr --enable-libwebpmux --enable-libwebpdemux && make -j4 && sudo make -j4 install - -popd +#!/bin/bash +# install webp + +archive=libwebp-1.4.0 + +./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz + +pushd $archive + +./configure --prefix=/usr --enable-libwebpmux --enable-libwebpdemux && make -j4 && sudo make -j4 install + +popd diff --git a/depends/termux.sh b/depends/termux.sh index d437029fd..e4e4508e4 100755 --- a/depends/termux.sh +++ b/depends/termux.sh @@ -1,4 +1,4 @@ -#!/bin/sh - -pkg install -y python ndk-sysroot clang make \ - libjpeg-turbo +#!/bin/sh + +pkg install -y python ndk-sysroot clang make \ + libjpeg-turbo diff --git a/docs/COPYING b/docs/COPYING index d5ee19f81..2c07b9433 100644 --- a/docs/COPYING +++ b/docs/COPYING @@ -1,32 +1,32 @@ -The Python Imaging Library (PIL) is - - Copyright © 1997-2011 by Secret Labs AB - Copyright © 1995-2011 by Fredrik Lundh and contributors - -Pillow is the friendly PIL fork. It is - - Copyright © 2010-2024 by Jeffrey A. Clark and contributors - -Like PIL, Pillow is licensed under the open source PIL -Software License: - -By obtaining, using, and/or copying this software and/or its -associated documentation, you agree that you have read, understood, -and will comply with the following terms and conditions: - -Permission to use, copy, modify, and distribute this software and its -associated documentation for any purpose and without fee is hereby -granted, provided that the above copyright notice appears in all -copies, and that both that copyright notice and this permission notice -appear in supporting documentation, and that the name of Secret Labs -AB or the author not be used in advertising or publicity pertaining to -distribution of the software without specific, written prior -permission. - -SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO -THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND -FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR BE LIABLE FOR -ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT -OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +The Python Imaging Library (PIL) is + + Copyright © 1997-2011 by Secret Labs AB + Copyright © 1995-2011 by Fredrik Lundh and contributors + +Pillow is the friendly PIL fork. It is + + Copyright © 2010-2024 by Jeffrey A. Clark and contributors + +Like PIL, Pillow is licensed under the open source PIL +Software License: + +By obtaining, using, and/or copying this software and/or its +associated documentation, you agree that you have read, understood, +and will comply with the following terms and conditions: + +Permission to use, copy, modify, and distribute this software and its +associated documentation for any purpose and without fee is hereby +granted, provided that the above copyright notice appears in all +copies, and that both that copyright notice and this permission notice +appear in supporting documentation, and that the name of Secret Labs +AB or the author not be used in advertising or publicity pertaining to +distribution of the software without specific, written prior +permission. + +SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO +THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR BE LIABLE FOR +ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/docs/Guardfile b/docs/Guardfile index 16a891a73..ad6a4921f 100755 --- a/docs/Guardfile +++ b/docs/Guardfile @@ -1,10 +1,10 @@ -#!/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")) +#!/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 8f13f1aea..b129370bc 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,210 +1,210 @@ -# Makefile for Sphinx documentation -# - -# You can set these variables from the command line. -PYTHON = python3 -SPHINXOPTS = -SPHINXBUILD = $(PYTHON) -m sphinx.cmd.build -PAPER = -BUILDDIR = _build - -# 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) . - -.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 " serve to start a local server for viewing docs" - @echo " livehtml to start a local server for viewing docs and auto-reload on change" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " texinfo to make Texinfo files" - @echo " info to make Texinfo files and run them through makeinfo" - @echo " gettext to make PO message catalogs" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - -.PHONY: clean -clean: - -rm -rf $(BUILDDIR)/* - -install-sphinx: - $(PYTHON) -m pip install --quiet furo olefile sphinx sphinx-copybutton sphinx-inline-tabs sphinxext-opengraph - -.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." - -.PHONY: dirhtml -dirhtml: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -.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." - -.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." - -.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: serve -serve: - cd $(BUILDDIR)/html; $(PYTHON) -m http.server +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +PYTHON = python3 +SPHINXOPTS = +SPHINXBUILD = $(PYTHON) -m sphinx.cmd.build +PAPER = +BUILDDIR = _build + +# 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) . + +.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 " serve to start a local server for viewing docs" + @echo " livehtml to start a local server for viewing docs and auto-reload on change" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +.PHONY: clean +clean: + -rm -rf $(BUILDDIR)/* + +install-sphinx: + $(PYTHON) -m pip install --quiet furo olefile sphinx sphinx-copybutton sphinx-inline-tabs sphinxext-opengraph + +.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." + +.PHONY: dirhtml +dirhtml: + $(MAKE) install-sphinx + $(SPHINXBUILD) --builder dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +.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." + +.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." + +.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: serve +serve: + cd $(BUILDDIR)/html; $(PYTHON) -m http.server diff --git a/docs/PIL.rst b/docs/PIL.rst index bdbf1373d..98488ad28 100644 --- a/docs/PIL.rst +++ b/docs/PIL.rst @@ -1,133 +1,133 @@ -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 ------------------ - -.. py:module:: PIL - -.. autoexception:: UnidentifiedImageError - :show-inheritance: - -:mod:`~PIL.BdfFontFile` Module ------------------------------- - -.. automodule:: PIL.BdfFontFile - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL.ContainerIO` Module ------------------------------- - -.. automodule:: PIL.ContainerIO - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL.FontFile` Module ---------------------------- - -.. automodule:: PIL.FontFile - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL.GdImageFile` Module ------------------------------- - -.. automodule:: PIL.GdImageFile - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL.GimpGradientFile` Module ------------------------------------ - -.. automodule:: PIL.GimpGradientFile - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL.GimpPaletteFile` Module ----------------------------------- - -.. automodule:: PIL.GimpPaletteFile - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL.ImageDraw2` Module ------------------------------ - -.. automodule:: PIL.ImageDraw2 - :members: - :member-order: bysource - :undoc-members: - :show-inheritance: - -:mod:`~PIL.ImageMode` Module ----------------------------- - -.. automodule:: PIL.ImageMode - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL.PaletteFile` Module ------------------------------- - -.. automodule:: PIL.PaletteFile - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL.PcfFontFile` Module ------------------------------- - -.. automodule:: PIL.PcfFontFile - :members: - :undoc-members: - :show-inheritance: - -:class:`.PngImagePlugin.iTXt` Class ------------------------------------ - -.. autoclass:: PIL.PngImagePlugin.iTXt - :members: - :undoc-members: - :show-inheritance: - - .. method:: __new__(cls, text, lang, tkey) - - :param value: value for this key - :param lang: language code - :param tkey: UTF-8 version of the key name - -:class:`.PngImagePlugin.PngInfo` Class --------------------------------------- - -.. autoclass:: PIL.PngImagePlugin.PngInfo - :members: - :undoc-members: - :show-inheritance: - - -:mod:`~PIL.TarIO` Module ------------------------- - -.. automodule:: PIL.TarIO - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL.WalImageFile` Module -------------------------------- - -.. automodule:: PIL.WalImageFile - :members: - :undoc-members: - :show-inheritance: +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 +----------------- + +.. py:module:: PIL + +.. autoexception:: UnidentifiedImageError + :show-inheritance: + +:mod:`~PIL.BdfFontFile` Module +------------------------------ + +.. automodule:: PIL.BdfFontFile + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL.ContainerIO` Module +------------------------------ + +.. automodule:: PIL.ContainerIO + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL.FontFile` Module +--------------------------- + +.. automodule:: PIL.FontFile + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL.GdImageFile` Module +------------------------------ + +.. automodule:: PIL.GdImageFile + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL.GimpGradientFile` Module +----------------------------------- + +.. automodule:: PIL.GimpGradientFile + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL.GimpPaletteFile` Module +---------------------------------- + +.. automodule:: PIL.GimpPaletteFile + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL.ImageDraw2` Module +----------------------------- + +.. automodule:: PIL.ImageDraw2 + :members: + :member-order: bysource + :undoc-members: + :show-inheritance: + +:mod:`~PIL.ImageMode` Module +---------------------------- + +.. automodule:: PIL.ImageMode + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL.PaletteFile` Module +------------------------------ + +.. automodule:: PIL.PaletteFile + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL.PcfFontFile` Module +------------------------------ + +.. automodule:: PIL.PcfFontFile + :members: + :undoc-members: + :show-inheritance: + +:class:`.PngImagePlugin.iTXt` Class +----------------------------------- + +.. autoclass:: PIL.PngImagePlugin.iTXt + :members: + :undoc-members: + :show-inheritance: + + .. method:: __new__(cls, text, lang, tkey) + + :param value: value for this key + :param lang: language code + :param tkey: UTF-8 version of the key name + +:class:`.PngImagePlugin.PngInfo` Class +-------------------------------------- + +.. autoclass:: PIL.PngImagePlugin.PngInfo + :members: + :undoc-members: + :show-inheritance: + + +:mod:`~PIL.TarIO` Module +------------------------ + +.. automodule:: PIL.TarIO + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL.WalImageFile` Module +------------------------------- + +.. automodule:: PIL.WalImageFile + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/about.rst b/docs/about.rst index 98cdd8e5a..9bc81dbdf 100644 --- a/docs/about.rst +++ b/docs/about.rst @@ -1,38 +1,38 @@ -About -===== - -Goals ------ - -The fork author's goal is to foster and support active development of PIL through: - -- Continuous integration testing via `GitHub Actions`_ and `AppVeyor`_ -- 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 `_ - -Why a fork? ------------ - -PIL is not setuptools compatible. Please see `this Image-SIG post`_ for a more detailed explanation. Also, PIL's bi-yearly (or greater) release schedule was too infrequent to accommodate the large number and frequency of issues reported. - -.. _this Image-SIG post: https://mail.python.org/pipermail/image-sig/2010-August/006480.html - -What about PIL? ---------------- - -.. note:: - - Prior to Pillow 2.0.0, very few image code changes were made. Pillow 2.0.0 - added Python 3 support and includes many bug fixes from many contributors. - -The last PIL release was in 2009 (1.1.7) and `no future releases are expected `_. In January 2020, `the PyPI moderators exhausted the PEP 541 process for contacting the PIL project owner `_ and the `PIL project on PyPI `_ was transferred to the `Pillow team `_. The Pillow team has no plans to update the PIL project on PyPI. +About +===== + +Goals +----- + +The fork author's goal is to foster and support active development of PIL through: + +- Continuous integration testing via `GitHub Actions`_ and `AppVeyor`_ +- 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 `_ + +Why a fork? +----------- + +PIL is not setuptools compatible. Please see `this Image-SIG post`_ for a more detailed explanation. Also, PIL's bi-yearly (or greater) release schedule was too infrequent to accommodate the large number and frequency of issues reported. + +.. _this Image-SIG post: https://mail.python.org/pipermail/image-sig/2010-August/006480.html + +What about PIL? +--------------- + +.. note:: + + Prior to Pillow 2.0.0, very few image code changes were made. Pillow 2.0.0 + added Python 3 support and includes many bug fixes from many contributors. + +The last PIL release was in 2009 (1.1.7) and `no future releases are expected `_. In January 2020, `the PyPI moderators exhausted the PEP 541 process for contacting the PIL project owner `_ and the `PIL project on PyPI `_ was transferred to the `Pillow team `_. The Pillow team has no plans to update the PIL project on PyPI. diff --git a/docs/conf.py b/docs/conf.py index f12b30e65..c49f10281 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,353 +1,353 @@ -# -# Pillow (PIL Fork) documentation build configuration file, created by -# sphinx-quickstart on Sat Apr 4 07:54:11 2015. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# sys.path.insert(0, os.path.abspath('.')) -from __future__ import annotations - -import PIL - -# -- General configuration ------------------------------------------------ - -# If your documentation needs a minimal Sphinx version, state it here. -needs_sphinx = "7.3" - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [ - "dater", - "sphinx.ext.autodoc", - "sphinx.ext.extlinks", - "sphinx.ext.intersphinx", - "sphinx.ext.viewcode", - "sphinx_copybutton", - "sphinx_inline_tabs", - "sphinxext.opengraph", -] - -intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} - -# The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string: -# source_suffix = ['.rst', '.md'] -source_suffix = ".rst" - -# The encoding of source files. -# source_encoding = 'utf-8-sig' - -# The master toctree document. -master_doc = "index" - -# General information about the project. -project = "Pillow (PIL Fork)" -copyright = ( - "1995-2011 Fredrik Lundh and contributors, " - "2010-2024 Jeffrey A. Clark and contributors." -) -author = "Fredrik Lundh (PIL), Jeffrey A. Clark (Pillow)" - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = PIL.__version__ -# The full version, including alpha/beta/rc tags. -release = PIL.__version__ - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = "en" - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -# today = '' -# Else, today_fmt is used as the format for a strftime call. -# today_fmt = '%B %d, %Y' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -exclude_patterns = ["_build", "releasenotes/template.rst"] - -# The reST default role (used for this markup: `text`) to use for all -# documents. -# default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -# add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -# add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -# show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = "sphinx" - -# A list of ignored prefixes for module index sorting. -# modindex_common_prefix = [] - -# If true, keep warnings as "system message" paragraphs in the built documents. -# keep_warnings = False - -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = False - -# If true, Sphinx will warn about all references where the target cannot be found. -# Default is False. You can activate this mode temporarily using the -n command-line -# switch. -nitpicky = True - -# A list of (type, target) tuples (by default empty) that should be ignored when -# generating warnings in “nitpicky modeâ€. Note that type should include the domain name -# if present. Example entries would be ('py:func', 'int') or -# ('envvar', 'LD_LIBRARY_PATH'). -# nitpick_ignore = [] - - -# -- Options for HTML output ---------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. - -html_theme = "furo" - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -html_theme_options = { - "light_logo": "pillow-logo-dark-text.png", - "dark_logo": "pillow-logo.png", -} - -# Add any paths that contain custom themes here, relative to this directory. -# html_theme_path = [] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -# html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -# html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -# html_logo = "resources/pillow-logo.png" - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -html_favicon = "resources/favicon.ico" - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ["resources"] - -# Add any extra paths that contain custom files (such as robots.txt or -# .htaccess) here, relative to this directory. These files are copied -# directly to the root of the documentation. -# html_extra_path = [] - -html_css_files = ["css/dark.css"] - -html_js_files = [ - "js/activate_tab.js", -] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -# html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -# html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -# html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -# html_additional_pages = {} - -# If false, no module index is generated. -# html_domain_indices = True - -# If false, no index is generated. -# html_use_index = True - -# If true, the index is split into individual pages for each letter. -# html_split_index = False - -# If true, links to the reST sources are added to the pages. -# html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -# html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -# html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -# html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -# html_file_suffix = None - -# Language to be used for generating the HTML full-text search index. -# Sphinx supports the following languages: -# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' -# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' -# html_search_language = 'en' - -# A dictionary with options for the search language support, empty by default. -# Now only 'ja' uses this config value -# html_search_options = {'type': 'default'} - -# The name of a javascript file (relative to the configuration directory) that -# implements a search results scorer. If empty, the default will be used. -# html_search_scorer = 'scorer.js' - -# 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/.*", - r"https://badges.gitter.im/python-pillow/Pillow.svg": r"https://badges.gitter.im/repo.svg", - r"https://gitter.im/python-pillow/Pillow?.*": r"https://app.gitter.im/#/room/#python-pillow_Pillow:gitter.im?.*", - r"https://pillow.readthedocs.io/?badge=latest": r"https://pillow.readthedocs.io/en/stable/?badge=latest", - r"https://pillow.readthedocs.io": r"https://pillow.readthedocs.io/en/stable/", - r"https://tidelift.com/badges/package/pypi/pillow?.*": r"https://img.shields.io/badge/.*", - r"https://zenodo.org/badge/17549/python-pillow/Pillow.svg": r"https://zenodo.org/badge/doi/[\.0-9]+/zenodo.[0-9]+.svg", - r"https://zenodo.org/badge/latestdoi/17549/python-pillow/Pillow": r"https://zenodo.org/record/[0-9]+", -} - -# sphinx.ext.extlinks -# This config is a dictionary of external sites, -# mapping unique short aliases to a base URL and a prefix. -# https://www.sphinx-doc.org/en/master/usage/extensions/extlinks.html -_repo = "https://github.com/python-pillow/Pillow/" -extlinks = { - "cve": ("https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-%s", "CVE-%s"), - "cwe": ("https://cwe.mitre.org/data/definitions/%s.html", "CWE-%s"), - "issue": (_repo + "issues/%s", "#%s"), - "pr": (_repo + "pull/%s", "#%s"), - "pypi": ("https://pypi.org/project/%s/", "%s"), -} - -# sphinxext.opengraph -ogp_image = ( - "https://raw.githubusercontent.com/python-pillow/pillow-logo/main/" - "pillow-logo-dark-text-1280x640.png" -) -ogp_image_alt = "Pillow" +# +# Pillow (PIL Fork) documentation build configuration file, created by +# sphinx-quickstart on Sat Apr 4 07:54:11 2015. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# sys.path.insert(0, os.path.abspath('.')) +from __future__ import annotations + +import PIL + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +needs_sphinx = "7.3" + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + "dater", + "sphinx.ext.autodoc", + "sphinx.ext.extlinks", + "sphinx.ext.intersphinx", + "sphinx.ext.viewcode", + "sphinx_copybutton", + "sphinx_inline_tabs", + "sphinxext.opengraph", +] + +intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# source_suffix = ['.rst', '.md'] +source_suffix = ".rst" + +# The encoding of source files. +# source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = "index" + +# General information about the project. +project = "Pillow (PIL Fork)" +copyright = ( + "1995-2011 Fredrik Lundh and contributors, " + "2010-2024 Jeffrey A. Clark and contributors." +) +author = "Fredrik Lundh (PIL), Jeffrey A. Clark (Pillow)" + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = PIL.__version__ +# The full version, including alpha/beta/rc tags. +release = PIL.__version__ + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = "en" + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +# today = '' +# Else, today_fmt is used as the format for a strftime call. +# today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ["_build", "releasenotes/template.rst"] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +# default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +# add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +# add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +# show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = "sphinx" + +# A list of ignored prefixes for module index sorting. +# modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +# keep_warnings = False + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + +# If true, Sphinx will warn about all references where the target cannot be found. +# Default is False. You can activate this mode temporarily using the -n command-line +# switch. +nitpicky = True + +# A list of (type, target) tuples (by default empty) that should be ignored when +# generating warnings in “nitpicky modeâ€. Note that type should include the domain name +# if present. Example entries would be ('py:func', 'int') or +# ('envvar', 'LD_LIBRARY_PATH'). +# nitpick_ignore = [] + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. + +html_theme = "furo" + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +html_theme_options = { + "light_logo": "pillow-logo-dark-text.png", + "dark_logo": "pillow-logo.png", +} + +# Add any paths that contain custom themes here, relative to this directory. +# html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +# html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +# html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +# html_logo = "resources/pillow-logo.png" + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +html_favicon = "resources/favicon.ico" + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ["resources"] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +# html_extra_path = [] + +html_css_files = ["css/dark.css"] + +html_js_files = [ + "js/activate_tab.js", +] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +# html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +# html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +# html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +# html_additional_pages = {} + +# If false, no module index is generated. +# html_domain_indices = True + +# If false, no index is generated. +# html_use_index = True + +# If true, the index is split into individual pages for each letter. +# html_split_index = False + +# If true, links to the reST sources are added to the pages. +# html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +# html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +# html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +# html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +# html_file_suffix = None + +# Language to be used for generating the HTML full-text search index. +# Sphinx supports the following languages: +# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' +# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' +# html_search_language = 'en' + +# A dictionary with options for the search language support, empty by default. +# Now only 'ja' uses this config value +# html_search_options = {'type': 'default'} + +# The name of a javascript file (relative to the configuration directory) that +# implements a search results scorer. If empty, the default will be used. +# html_search_scorer = 'scorer.js' + +# 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/.*", + r"https://badges.gitter.im/python-pillow/Pillow.svg": r"https://badges.gitter.im/repo.svg", + r"https://gitter.im/python-pillow/Pillow?.*": r"https://app.gitter.im/#/room/#python-pillow_Pillow:gitter.im?.*", + r"https://pillow.readthedocs.io/?badge=latest": r"https://pillow.readthedocs.io/en/stable/?badge=latest", + r"https://pillow.readthedocs.io": r"https://pillow.readthedocs.io/en/stable/", + r"https://tidelift.com/badges/package/pypi/pillow?.*": r"https://img.shields.io/badge/.*", + r"https://zenodo.org/badge/17549/python-pillow/Pillow.svg": r"https://zenodo.org/badge/doi/[\.0-9]+/zenodo.[0-9]+.svg", + r"https://zenodo.org/badge/latestdoi/17549/python-pillow/Pillow": r"https://zenodo.org/record/[0-9]+", +} + +# sphinx.ext.extlinks +# This config is a dictionary of external sites, +# mapping unique short aliases to a base URL and a prefix. +# https://www.sphinx-doc.org/en/master/usage/extensions/extlinks.html +_repo = "https://github.com/python-pillow/Pillow/" +extlinks = { + "cve": ("https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-%s", "CVE-%s"), + "cwe": ("https://cwe.mitre.org/data/definitions/%s.html", "CWE-%s"), + "issue": (_repo + "issues/%s", "#%s"), + "pr": (_repo + "pull/%s", "#%s"), + "pypi": ("https://pypi.org/project/%s/", "%s"), +} + +# sphinxext.opengraph +ogp_image = ( + "https://raw.githubusercontent.com/python-pillow/pillow-logo/main/" + "pillow-logo-dark-text-1280x640.png" +) +ogp_image_alt = "Pillow" diff --git a/docs/dater.py b/docs/dater.py index f9fb0c1da..06667319d 100644 --- a/docs/dater.py +++ b/docs/dater.py @@ -1,48 +1,48 @@ -""" -Sphinx extension to add timestamps to release notes based on Git versions. - -Based on https://github.com/jaraco/rst.linker, with thanks to Jason R. Coombs. -""" - -from __future__ import annotations - -import re -import subprocess -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from sphinx.application import Sphinx - -DOC_NAME_REGEX = re.compile(r"releasenotes/\d+\.\d+\.\d+") -VERSION_TITLE_REGEX = re.compile(r"^(\d+\.\d+\.\d+)\n-+\n") - - -def get_date_for(git_version: str) -> str | None: - cmd = ["git", "log", "-1", "--format=%ai", git_version] - try: - out = subprocess.check_output( - cmd, stderr=subprocess.DEVNULL, text=True, encoding="utf-8" - ) - except subprocess.CalledProcessError: - return None - return out.split()[0] - - -def add_date(app: Sphinx, doc_name: str, source: list[str]) -> None: - if DOC_NAME_REGEX.match(doc_name) and (m := VERSION_TITLE_REGEX.match(source[0])): - old_title = m.group(1) - - if tag_date := get_date_for(old_title): - new_title = f"{old_title} ({tag_date})" - else: - new_title = f"{old_title} (unreleased)" - - new_underline = "-" * len(new_title) - - result = source[0].replace(m.group(0), f"{new_title}\n{new_underline}\n", 1) - source[0] = result - - -def setup(app: Sphinx) -> dict[str, bool]: - app.connect("source-read", add_date) - return {"parallel_read_safe": True} +""" +Sphinx extension to add timestamps to release notes based on Git versions. + +Based on https://github.com/jaraco/rst.linker, with thanks to Jason R. Coombs. +""" + +from __future__ import annotations + +import re +import subprocess +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from sphinx.application import Sphinx + +DOC_NAME_REGEX = re.compile(r"releasenotes/\d+\.\d+\.\d+") +VERSION_TITLE_REGEX = re.compile(r"^(\d+\.\d+\.\d+)\n-+\n") + + +def get_date_for(git_version: str) -> str | None: + cmd = ["git", "log", "-1", "--format=%ai", git_version] + try: + out = subprocess.check_output( + cmd, stderr=subprocess.DEVNULL, text=True, encoding="utf-8" + ) + except subprocess.CalledProcessError: + return None + return out.split()[0] + + +def add_date(app: Sphinx, doc_name: str, source: list[str]) -> None: + if DOC_NAME_REGEX.match(doc_name) and (m := VERSION_TITLE_REGEX.match(source[0])): + old_title = m.group(1) + + if tag_date := get_date_for(old_title): + new_title = f"{old_title} ({tag_date})" + else: + new_title = f"{old_title} (unreleased)" + + new_underline = "-" * len(new_title) + + result = source[0].replace(m.group(0), f"{new_title}\n{new_underline}\n", 1) + source[0] = result + + +def setup(app: Sphinx) -> dict[str, bool]: + app.connect("source-read", add_date) + return {"parallel_read_safe": True} diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 627672e1f..8d08f28b2 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -1,592 +1,592 @@ -.. _deprecations: - -Deprecations and removals -========================= - -This page lists Pillow features that are deprecated, or have been removed in -past major releases, and gives the alternatives to use instead. - -Deprecated features -------------------- - -Below are features which are considered deprecated. Where appropriate, -a :py:exc:`DeprecationWarning` is issued. - -PSFile -~~~~~~ - -.. deprecated:: 9.5.0 - -The :py:class:`~PIL.EpsImagePlugin.PSFile` class has been deprecated and will -be removed in Pillow 11 (2024-10-15). This class was only made as a helper to -be used internally, so there is no replacement. If you need this functionality -though, it is a very short class that can easily be recreated in your own code. - -PyAccess and Image.USE_CFFI_ACCESS -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 10.0.0 - -Since Pillow's C API is now faster than PyAccess on PyPy, -:py:mod:`~PIL.PyAccess` has been deprecated and will be removed in Pillow -11.0.0 (2024-10-15). Pillow's C API will now be used by default on PyPy instead. - -``Image.USE_CFFI_ACCESS``, for switching from the C API to PyAccess, is -similarly deprecated. - -ImageFile.raise_oserror -~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 10.2.0 - -``ImageFile.raise_oserror()`` has been deprecated and will be removed in Pillow -12.0.0 (2025-10-15). The function is undocumented and is only useful for translating -error codes returned by a codec's ``decode()`` method, which ImageFile already does -automatically. - -IptcImageFile helper functions -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 10.2.0 - -The functions ``IptcImageFile.dump`` and ``IptcImageFile.i``, and the constant -``IptcImageFile.PAD`` have been deprecated and will be removed in Pillow -12.0.0 (2025-10-15). These are undocumented helper functions intended -for internal use, so there is no replacement. They can each be replaced -by a single line of code using builtin functions in Python. - -ImageCms constants and versions() function -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 10.3.0 - -A number of constants and a function in :py:mod:`.ImageCms` have been deprecated. -This includes a table of flags based on LittleCMS version 1 which has been -replaced with a new class :py:class:`.ImageCms.Flags` based on LittleCMS 2 flags. - -============================================ ==================================================== -Deprecated Use instead -============================================ ==================================================== -``ImageCms.DESCRIPTION`` No replacement -``ImageCms.VERSION`` ``PIL.__version__`` -``ImageCms.FLAGS["MATRIXINPUT"]`` :py:attr:`.ImageCms.Flags.CLUT_POST_LINEARIZATION` -``ImageCms.FLAGS["MATRIXOUTPUT"]`` :py:attr:`.ImageCms.Flags.FORCE_CLUT` -``ImageCms.FLAGS["MATRIXONLY"]`` No replacement -``ImageCms.FLAGS["NOWHITEONWHITEFIXUP"]`` :py:attr:`.ImageCms.Flags.NOWHITEONWHITEFIXUP` -``ImageCms.FLAGS["NOPRELINEARIZATION"]`` :py:attr:`.ImageCms.Flags.CLUT_PRE_LINEARIZATION` -``ImageCms.FLAGS["GUESSDEVICECLASS"]`` :py:attr:`.ImageCms.Flags.GUESSDEVICECLASS` -``ImageCms.FLAGS["NOTCACHE"]`` :py:attr:`.ImageCms.Flags.NOCACHE` -``ImageCms.FLAGS["NOTPRECALC"]`` :py:attr:`.ImageCms.Flags.NOOPTIMIZE` -``ImageCms.FLAGS["NULLTRANSFORM"]`` :py:attr:`.ImageCms.Flags.NULLTRANSFORM` -``ImageCms.FLAGS["HIGHRESPRECALC"]`` :py:attr:`.ImageCms.Flags.HIGHRESPRECALC` -``ImageCms.FLAGS["LOWRESPRECALC"]`` :py:attr:`.ImageCms.Flags.LOWRESPRECALC` -``ImageCms.FLAGS["GAMUTCHECK"]`` :py:attr:`.ImageCms.Flags.GAMUTCHECK` -``ImageCms.FLAGS["WHITEBLACKCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` -``ImageCms.FLAGS["BLACKPOINTCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` -``ImageCms.FLAGS["SOFTPROOFING"]`` :py:attr:`.ImageCms.Flags.SOFTPROOFING` -``ImageCms.FLAGS["PRESERVEBLACK"]`` :py:attr:`.ImageCms.Flags.NONEGATIVES` -``ImageCms.FLAGS["NODEFAULTRESOURCEDEF"]`` :py:attr:`.ImageCms.Flags.NODEFAULTRESOURCEDEF` -``ImageCms.FLAGS["GRIDPOINTS"]`` :py:attr:`.ImageCms.Flags.GRIDPOINTS()` -``ImageCms.versions()`` :py:func:`PIL.features.version_module` with - ``feature="littlecms2"``, :py:data:`sys.version` or - :py:data:`sys.version_info`, and ``PIL.__version__`` -============================================ ==================================================== - -ImageMath eval() -^^^^^^^^^^^^^^^^ - -.. deprecated:: 10.3.0 - -``ImageMath.eval()`` has been deprecated. Use :py:meth:`~PIL.ImageMath.lambda_eval` or -:py:meth:`~PIL.ImageMath.unsafe_eval` instead. - -BGR;15, BGR 16 and BGR;24 -^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. deprecated:: 10.4.0 - -The experimental BGR;15, BGR;16 and BGR;24 modes have been deprecated. - -Support for LibTIFF earlier than 4 -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. deprecated:: 10.4.0 - -Support for LibTIFF earlier than version 4 has been deprecated. -Upgrade to a newer version of LibTIFF instead. - -ImageDraw.getdraw hints parameter -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. deprecated:: 10.4.0 - -The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been deprecated. - -Removed features ----------------- - -Deprecated features are only removed in major releases after an appropriate -period of deprecation has passed. - -Tk/Tcl 8.4 -~~~~~~~~~~ - -.. deprecated:: 8.2.0 -.. versionremoved:: 10.0.0 - -Support for Tk/Tcl 8.4 was removed in Pillow 10.0.0 (2023-07-01). - -Categories -~~~~~~~~~~ - -.. deprecated:: 8.2.0 -.. versionremoved:: 10.0.0 - -``im.category`` was removed along with the related ``Image.NORMAL``, -``Image.SEQUENCE`` and ``Image.CONTAINER`` attributes. - -To determine if an image has multiple frames or not, -``getattr(im, "is_animated", False)`` can be used instead. - -JpegImagePlugin.convert_dict_qtables -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 8.3.0 -.. versionremoved:: 10.0.0 - -Since deprecation in Pillow 8.3.0, the ``convert_dict_qtables`` method no longer -performed any operations on the data given to it, and has been removed. - -ImagePalette size parameter -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 8.4.0 -.. versionremoved:: 10.0.0 - -Before Pillow 8.3.0, ``ImagePalette`` required palette data of particular lengths by -default, and the ``size`` parameter could be used to override that. Pillow 8.3.0 -removed the default required length, also removing the need for the ``size`` parameter. - -ImageShow.Viewer.show_file file argument -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 9.1.0 -.. versionremoved:: 10.0.0 - -The ``file`` argument in :py:meth:`~PIL.ImageShow.Viewer.show_file()` has been -removed and replaced by ``path``. - -In effect, ``viewer.show_file("test.jpg")`` will continue to work unchanged. - -Constants -~~~~~~~~~ - -.. deprecated:: 9.1.0 -.. versionremoved:: 10.0.0 - -A number of constants have been removed. -Instead, :py:class:`enum.IntEnum` classes have been added. - -.. note:: - - Additional ``Image`` constants were deprecated in Pillow 9.1.0, but that - was reversed in Pillow 9.4.0 and those constants will now remain available. - See :ref:`restored-image-constants` - -===================================================== ============================================================ -Removed Use instead -===================================================== ============================================================ -``Image.LINEAR`` ``Image.BILINEAR`` or ``Image.Resampling.BILINEAR`` -``Image.CUBIC`` ``Image.BICUBIC`` or ``Image.Resampling.BICUBIC`` -``Image.ANTIALIAS`` ``Image.LANCZOS`` or ``Image.Resampling.LANCZOS`` -``ImageCms.INTENT_PERCEPTUAL`` ``ImageCms.Intent.PERCEPTUAL`` -``ImageCms.INTENT_RELATIVE_COLORMETRIC`` ``ImageCms.Intent.RELATIVE_COLORMETRIC`` -``ImageCms.INTENT_SATURATION`` ``ImageCms.Intent.SATURATION`` -``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC`` ``ImageCms.Intent.ABSOLUTE_COLORIMETRIC`` -``ImageCms.DIRECTION_INPUT`` ``ImageCms.Direction.INPUT`` -``ImageCms.DIRECTION_OUTPUT`` ``ImageCms.Direction.OUTPUT`` -``ImageCms.DIRECTION_PROOF`` ``ImageCms.Direction.PROOF`` -``ImageFont.LAYOUT_BASIC`` ``ImageFont.Layout.BASIC`` -``ImageFont.LAYOUT_RAQM`` ``ImageFont.Layout.RAQM`` -``BlpImagePlugin.BLP_FORMAT_JPEG`` ``BlpImagePlugin.Format.JPEG`` -``BlpImagePlugin.BLP_ENCODING_UNCOMPRESSED`` ``BlpImagePlugin.Encoding.UNCOMPRESSED`` -``BlpImagePlugin.BLP_ENCODING_DXT`` ``BlpImagePlugin.Encoding.DXT`` -``BlpImagePlugin.BLP_ENCODING_UNCOMPRESSED_RAW_RGBA`` ``BlpImagePlugin.Encoding.UNCOMPRESSED_RAW_RGBA`` -``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT1`` ``BlpImagePlugin.AlphaEncoding.DXT1`` -``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT3`` ``BlpImagePlugin.AlphaEncoding.DXT3`` -``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT5`` ``BlpImagePlugin.AlphaEncoding.DXT5`` -``FtexImagePlugin.FORMAT_DXT1`` ``FtexImagePlugin.Format.DXT1`` -``FtexImagePlugin.FORMAT_UNCOMPRESSED`` ``FtexImagePlugin.Format.UNCOMPRESSED`` -``PngImagePlugin.APNG_DISPOSE_OP_NONE`` ``PngImagePlugin.Disposal.OP_NONE`` -``PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND`` ``PngImagePlugin.Disposal.OP_BACKGROUND`` -``PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS`` ``PngImagePlugin.Disposal.OP_PREVIOUS`` -``PngImagePlugin.APNG_BLEND_OP_SOURCE`` ``PngImagePlugin.Blend.OP_SOURCE`` -``PngImagePlugin.APNG_BLEND_OP_OVER`` ``PngImagePlugin.Blend.OP_OVER`` -===================================================== ============================================================ - -FitsStubImagePlugin -~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 9.1.0 -.. versionremoved:: 10.0.0 - -The stub image plugin ``FitsStubImagePlugin`` has been removed. -FITS images can be read without a handler through :mod:`~PIL.FitsImagePlugin` instead. - -Font size and offset methods -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 9.2.0 -.. versionremoved:: 10.0.0 - -Several functions for computing the size and offset of rendered text have been removed: - -=============================================================== ============================================================================================================= -Removed Use instead -=============================================================== ============================================================================================================= -``FreeTypeFont.getsize()`` and ``FreeTypeFont.getoffset()`` :py:meth:`.FreeTypeFont.getbbox` and :py:meth:`.FreeTypeFont.getlength` -``FreeTypeFont.getsize_multiline()`` :py:meth:`.ImageDraw.multiline_textbbox` -``ImageFont.getsize()`` :py:meth:`.ImageFont.getbbox` and :py:meth:`.ImageFont.getlength` -``TransposedFont.getsize()`` :py:meth:`.TransposedFont.getbbox` and :py:meth:`.TransposedFont.getlength` -``ImageDraw.textsize()`` and ``ImageDraw.multiline_textsize()`` :py:meth:`.ImageDraw.textbbox`, :py:meth:`.ImageDraw.textlength` and :py:meth:`.ImageDraw.multiline_textbbox` -``ImageDraw2.Draw.textsize()`` :py:meth:`.ImageDraw2.Draw.textbbox` and :py:meth:`.ImageDraw2.Draw.textlength` -=============================================================== ============================================================================================================= - -Previous code:: - - from PIL import Image, ImageDraw, ImageFont - - font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") - width, height = font.getsize("Hello world") - left, top = font.getoffset("Hello world") - - im = Image.new("RGB", (100, 100)) - draw = ImageDraw.Draw(im) - width, height = draw.textsize("Hello world", font) - - width, height = font.getsize_multiline("Hello\nworld") - width, height = draw.multiline_textsize("Hello\nworld", font) - -Use instead:: - - from PIL import Image, ImageDraw, ImageFont - - font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") - left, top, right, bottom = font.getbbox("Hello world") - width, height = right - left, bottom - top - - im = Image.new("RGB", (100, 100)) - draw = ImageDraw.Draw(im) - width = draw.textlength("Hello world", font) - - left, top, right, bottom = draw.multiline_textbbox((0, 0), "Hello\nworld", font) - width, height = right - left, bottom - top - -Previously, the ``size`` methods returned a ``height`` that included the vertical -offset of the text, while the new ``bbox`` methods distinguish this as a ``top`` -offset. - -.. image:: ./example/size_vs_bbox.png - :alt: In bbox methods, top measures the vertical distance above the text, while bottom measures that plus the vertical distance of the text itself. In size methods, height also measures the vertical distance above the text plus the vertical distance of the text itself. - :align: center - -If you are using these methods for aligning text, consider using :ref:`text-anchors` instead -which avoid issues that can occur with non-English text or unusual fonts. -For example, instead of the following code:: - - from PIL import Image, ImageDraw, ImageFont - - font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") - - im = Image.new("RGB", (100, 100)) - draw = ImageDraw.Draw(im) - width, height = draw.textsize("Hello world", font) - x, y = (100 - width) / 2, (100 - height) / 2 - draw.text((x, y), "Hello world", font=font) - -Use instead:: - - from PIL import Image, ImageDraw, ImageFont - - font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") - - im = Image.new("RGB", (100, 100)) - draw = ImageDraw.Draw(im) - draw.text((100 / 2, 100 / 2), "Hello world", font=font, anchor="mm") - -FreeTypeFont.getmask2 fill parameter -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 9.2.0 -.. versionremoved:: 10.0.0 - -The undocumented ``fill`` parameter of :py:meth:`.FreeTypeFont.getmask2` has been -removed. - -PhotoImage.paste box parameter -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 9.2.0 -.. versionremoved:: 10.0.0 - -The ``box`` parameter was unused and has been removed. - -PyQt5 and PySide2 -~~~~~~~~~~~~~~~~~ - -.. deprecated:: 9.2.0 -.. versionremoved:: 10.0.0 - -`Qt 5 reached end-of-life `_ on 2020-12-08 for -open-source users (and will reach EOL on 2023-12-08 for commercial licence holders). - -Support for PyQt5 and PySide2 has been removed from ``ImageQt``. Upgrade to -`PyQt6 `_ or -`PySide6 `_ instead. - -Image.coerce_e -~~~~~~~~~~~~~~ - -.. deprecated:: 9.2.0 -.. versionremoved:: 10.0.0 - -This undocumented method has been removed. - -PILLOW_VERSION constant -~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 5.2.0 -.. versionremoved:: 9.0.0 - -Use ``__version__`` instead. - -It was initially removed in Pillow 7.0.0, but temporarily brought back in 7.1.0 -to give projects more time to upgrade. - -Image.show command parameter -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 7.2.0 -.. versionremoved:: 9.0.0 - -The ``command`` parameter has been removed. Use a subclass of -:py:class:`.ImageShow.Viewer` instead. - -Image._showxv -~~~~~~~~~~~~~ - -.. deprecated:: 7.2.0 -.. versionremoved:: 9.0.0 - -Use :py:meth:`.Image.Image.show` instead. If custom behaviour is required, use -:py:func:`.ImageShow.register` to add a custom :py:class:`.ImageShow.Viewer` class. - -ImageFile.raise_ioerror -~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 7.2.0 -.. versionremoved:: 9.0.0 - -:py:exc:`IOError` was merged into :py:exc:`OSError` in Python 3.3. -So, ``ImageFile.raise_ioerror`` has been removed. -Use ``ImageFile.raise_oserror`` instead. - -FreeType 2.7 -~~~~~~~~~~~~ - -.. deprecated:: 8.1.0 -.. versionremoved:: 9.0.0 - -Support for FreeType 2.7 has been removed. - -We recommend upgrading to at least `FreeType`_ 2.10.4, which fixed a severe -vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`). - -.. _FreeType: https://freetype.org/ - -im.offset -~~~~~~~~~ - -.. deprecated:: 1.1.2 -.. versionremoved:: 8.0.0 - -``im.offset()`` has been removed, call :py:func:`.ImageChops.offset()` instead. - -It was documented as deprecated in PIL 1.1.2, -raised a :py:exc:`DeprecationWarning` since 1.1.5, -an :py:exc:`Exception` since Pillow 3.0.0 -and :py:exc:`NotImplementedError` since 3.3.0. - -Image.fromstring, im.fromstring and im.tostring -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 2.0.0 -.. versionremoved:: 8.0.0 - -* ``Image.fromstring()`` has been removed, call :py:func:`.Image.frombytes()` instead. -* ``im.fromstring()`` has been removed, call :py:meth:`~PIL.Image.Image.frombytes()` instead. -* ``im.tostring()`` has been removed, call :py:meth:`~PIL.Image.Image.tobytes()` instead. - -They issued a :py:exc:`DeprecationWarning` since 2.0.0, -an :py:exc:`Exception` since 3.0.0 -and :py:exc:`NotImplementedError` since 3.3.0. - -ImageCms.CmsProfile attributes -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 3.2.0 -.. versionremoved:: 8.0.0 - -Some attributes in :py:class:`PIL.ImageCms.core.CmsProfile` have been removed. -From 6.0.0, they issued a :py:exc:`DeprecationWarning`: - -======================== =================================================== -Removed Use instead -======================== =================================================== -``color_space`` Padded :py:attr:`~.CmsProfile.xcolor_space` -``pcs`` Padded :py:attr:`~.CmsProfile.connection_space` -``product_copyright`` Unicode :py:attr:`~.CmsProfile.copyright` -``product_desc`` Unicode :py:attr:`~.CmsProfile.profile_description` -``product_description`` Unicode :py:attr:`~.CmsProfile.profile_description` -``product_manufacturer`` Unicode :py:attr:`~.CmsProfile.manufacturer` -``product_model`` Unicode :py:attr:`~.CmsProfile.model` -======================== =================================================== - -Python 2.7 -~~~~~~~~~~ - -.. deprecated:: 6.0.0 -.. versionremoved:: 7.0.0 - -Python 2.7 reached end-of-life on 2020-01-01. Pillow 6.x was the last series to -support Python 2. - -Image.__del__ -~~~~~~~~~~~~~ - -.. deprecated:: 6.1.0 -.. versionremoved:: 7.0.0 - -Implicitly closing the image's underlying file in ``Image.__del__`` has been removed. -Use a context manager or call ``Image.close()`` instead to close the file in a -deterministic way. - -Previous method:: - - im = Image.open("hopper.png") - im.save("out.jpg") - -Use instead:: - - with Image.open("hopper.png") as im: - im.save("out.jpg") - -PIL.*ImagePlugin.__version__ attributes -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 6.0.0 -.. versionremoved:: 7.0.0 - -The version constants of individual plugins have been removed. Use ``PIL.__version__`` -instead. - -=============================== ================================= ================================== -Removed Removed Removed -=============================== ================================= ================================== -``BmpImagePlugin.__version__`` ``Jpeg2KImagePlugin.__version__`` ``PngImagePlugin.__version__`` -``CurImagePlugin.__version__`` ``JpegImagePlugin.__version__`` ``PpmImagePlugin.__version__`` -``DcxImagePlugin.__version__`` ``McIdasImagePlugin.__version__`` ``PsdImagePlugin.__version__`` -``EpsImagePlugin.__version__`` ``MicImagePlugin.__version__`` ``SgiImagePlugin.__version__`` -``FliImagePlugin.__version__`` ``MpegImagePlugin.__version__`` ``SunImagePlugin.__version__`` -``FpxImagePlugin.__version__`` ``MpoImagePlugin.__version__`` ``TgaImagePlugin.__version__`` -``GdImageFile.__version__`` ``MspImagePlugin.__version__`` ``TiffImagePlugin.__version__`` -``GifImagePlugin.__version__`` ``PalmImagePlugin.__version__`` ``WmfImagePlugin.__version__`` -``IcoImagePlugin.__version__`` ``PcdImagePlugin.__version__`` ``XbmImagePlugin.__version__`` -``ImImagePlugin.__version__`` ``PcxImagePlugin.__version__`` ``XpmImagePlugin.__version__`` -``ImtImagePlugin.__version__`` ``PdfImagePlugin.__version__`` ``XVThumbImagePlugin.__version__`` -``IptcImagePlugin.__version__`` ``PixarImagePlugin.__version__`` -=============================== ================================= ================================== - -PyQt4 and PySide -~~~~~~~~~~~~~~~~ - -.. deprecated:: 6.0.0 -.. versionremoved:: 7.0.0 - -Qt 4 reached end-of-life on 2015-12-19. Its Python bindings are also EOL: PyQt4 since -2018-08-31 and PySide since 2015-10-14. - -Support for PyQt4 and PySide has been removed from ``ImageQt``. Please upgrade to PyQt5 -or PySide2. - -Setting the size of TIFF images -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 5.3.0 -.. versionremoved:: 7.0.0 - -Setting the size of a TIFF image directly (eg. ``im.size = (256, 256)``) throws -an error. Use ``Image.resize`` instead. - -VERSION constant -~~~~~~~~~~~~~~~~ - -.. deprecated:: 5.2.0 -.. versionremoved:: 6.0.0 - -``VERSION`` (the old PIL version, always 1.1.7) has been removed. Use -``__version__`` instead. - -Undocumented ImageOps functions -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 4.3.0 -.. versionremoved:: 6.0.0 - -Several undocumented functions in ``ImageOps`` have been removed. Use the equivalents -in ``ImageFilter`` instead: - -========================== ============================ -Removed Use instead -========================== ============================ -``ImageOps.box_blur`` ``ImageFilter.BoxBlur`` -``ImageOps.gaussian_blur`` ``ImageFilter.GaussianBlur`` -``ImageOps.gblur`` ``ImageFilter.GaussianBlur`` -``ImageOps.usm`` ``ImageFilter.UnsharpMask`` -``ImageOps.unsharp_mask`` ``ImageFilter.UnsharpMask`` -========================== ============================ - -PIL.OleFileIO -~~~~~~~~~~~~~ - -.. deprecated:: 4.0.0 -.. versionremoved:: 6.0.0 - -``PIL.OleFileIO`` was removed as a vendored file in Pillow 4.0.0 (2017-01) in favour of -the upstream :pypi:`olefile` Python package, and replaced with an :py:exc:`ImportError` in 5.0.0 -(2018-01). The deprecated file has now been removed from Pillow. If needed, install from -PyPI (eg. ``python3 -m pip install olefile``). - -import _imaging -~~~~~~~~~~~~~~~ - -.. versionremoved:: 2.1.0 - -Pillow >= 2.1.0 no longer supports ``import _imaging``. -Please use ``from PIL.Image import core as _imaging`` instead. - -Pillow and PIL -~~~~~~~~~~~~~~ - -.. versionremoved:: 1.0.0 - -Pillow and PIL cannot co-exist in the same environment. -Before installing Pillow, please uninstall PIL. - -import Image -~~~~~~~~~~~~ - -.. versionremoved:: 1.0.0 - -Pillow >= 1.0 no longer supports ``import Image``. -Please use ``from PIL import Image`` instead. +.. _deprecations: + +Deprecations and removals +========================= + +This page lists Pillow features that are deprecated, or have been removed in +past major releases, and gives the alternatives to use instead. + +Deprecated features +------------------- + +Below are features which are considered deprecated. Where appropriate, +a :py:exc:`DeprecationWarning` is issued. + +PSFile +~~~~~~ + +.. deprecated:: 9.5.0 + +The :py:class:`~PIL.EpsImagePlugin.PSFile` class has been deprecated and will +be removed in Pillow 11 (2024-10-15). This class was only made as a helper to +be used internally, so there is no replacement. If you need this functionality +though, it is a very short class that can easily be recreated in your own code. + +PyAccess and Image.USE_CFFI_ACCESS +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 10.0.0 + +Since Pillow's C API is now faster than PyAccess on PyPy, +:py:mod:`~PIL.PyAccess` has been deprecated and will be removed in Pillow +11.0.0 (2024-10-15). Pillow's C API will now be used by default on PyPy instead. + +``Image.USE_CFFI_ACCESS``, for switching from the C API to PyAccess, is +similarly deprecated. + +ImageFile.raise_oserror +~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 10.2.0 + +``ImageFile.raise_oserror()`` has been deprecated and will be removed in Pillow +12.0.0 (2025-10-15). The function is undocumented and is only useful for translating +error codes returned by a codec's ``decode()`` method, which ImageFile already does +automatically. + +IptcImageFile helper functions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 10.2.0 + +The functions ``IptcImageFile.dump`` and ``IptcImageFile.i``, and the constant +``IptcImageFile.PAD`` have been deprecated and will be removed in Pillow +12.0.0 (2025-10-15). These are undocumented helper functions intended +for internal use, so there is no replacement. They can each be replaced +by a single line of code using builtin functions in Python. + +ImageCms constants and versions() function +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 10.3.0 + +A number of constants and a function in :py:mod:`.ImageCms` have been deprecated. +This includes a table of flags based on LittleCMS version 1 which has been +replaced with a new class :py:class:`.ImageCms.Flags` based on LittleCMS 2 flags. + +============================================ ==================================================== +Deprecated Use instead +============================================ ==================================================== +``ImageCms.DESCRIPTION`` No replacement +``ImageCms.VERSION`` ``PIL.__version__`` +``ImageCms.FLAGS["MATRIXINPUT"]`` :py:attr:`.ImageCms.Flags.CLUT_POST_LINEARIZATION` +``ImageCms.FLAGS["MATRIXOUTPUT"]`` :py:attr:`.ImageCms.Flags.FORCE_CLUT` +``ImageCms.FLAGS["MATRIXONLY"]`` No replacement +``ImageCms.FLAGS["NOWHITEONWHITEFIXUP"]`` :py:attr:`.ImageCms.Flags.NOWHITEONWHITEFIXUP` +``ImageCms.FLAGS["NOPRELINEARIZATION"]`` :py:attr:`.ImageCms.Flags.CLUT_PRE_LINEARIZATION` +``ImageCms.FLAGS["GUESSDEVICECLASS"]`` :py:attr:`.ImageCms.Flags.GUESSDEVICECLASS` +``ImageCms.FLAGS["NOTCACHE"]`` :py:attr:`.ImageCms.Flags.NOCACHE` +``ImageCms.FLAGS["NOTPRECALC"]`` :py:attr:`.ImageCms.Flags.NOOPTIMIZE` +``ImageCms.FLAGS["NULLTRANSFORM"]`` :py:attr:`.ImageCms.Flags.NULLTRANSFORM` +``ImageCms.FLAGS["HIGHRESPRECALC"]`` :py:attr:`.ImageCms.Flags.HIGHRESPRECALC` +``ImageCms.FLAGS["LOWRESPRECALC"]`` :py:attr:`.ImageCms.Flags.LOWRESPRECALC` +``ImageCms.FLAGS["GAMUTCHECK"]`` :py:attr:`.ImageCms.Flags.GAMUTCHECK` +``ImageCms.FLAGS["WHITEBLACKCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` +``ImageCms.FLAGS["BLACKPOINTCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` +``ImageCms.FLAGS["SOFTPROOFING"]`` :py:attr:`.ImageCms.Flags.SOFTPROOFING` +``ImageCms.FLAGS["PRESERVEBLACK"]`` :py:attr:`.ImageCms.Flags.NONEGATIVES` +``ImageCms.FLAGS["NODEFAULTRESOURCEDEF"]`` :py:attr:`.ImageCms.Flags.NODEFAULTRESOURCEDEF` +``ImageCms.FLAGS["GRIDPOINTS"]`` :py:attr:`.ImageCms.Flags.GRIDPOINTS()` +``ImageCms.versions()`` :py:func:`PIL.features.version_module` with + ``feature="littlecms2"``, :py:data:`sys.version` or + :py:data:`sys.version_info`, and ``PIL.__version__`` +============================================ ==================================================== + +ImageMath eval() +^^^^^^^^^^^^^^^^ + +.. deprecated:: 10.3.0 + +``ImageMath.eval()`` has been deprecated. Use :py:meth:`~PIL.ImageMath.lambda_eval` or +:py:meth:`~PIL.ImageMath.unsafe_eval` instead. + +BGR;15, BGR 16 and BGR;24 +^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 10.4.0 + +The experimental BGR;15, BGR;16 and BGR;24 modes have been deprecated. + +Support for LibTIFF earlier than 4 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 10.4.0 + +Support for LibTIFF earlier than version 4 has been deprecated. +Upgrade to a newer version of LibTIFF instead. + +ImageDraw.getdraw hints parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 10.4.0 + +The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been deprecated. + +Removed features +---------------- + +Deprecated features are only removed in major releases after an appropriate +period of deprecation has passed. + +Tk/Tcl 8.4 +~~~~~~~~~~ + +.. deprecated:: 8.2.0 +.. versionremoved:: 10.0.0 + +Support for Tk/Tcl 8.4 was removed in Pillow 10.0.0 (2023-07-01). + +Categories +~~~~~~~~~~ + +.. deprecated:: 8.2.0 +.. versionremoved:: 10.0.0 + +``im.category`` was removed along with the related ``Image.NORMAL``, +``Image.SEQUENCE`` and ``Image.CONTAINER`` attributes. + +To determine if an image has multiple frames or not, +``getattr(im, "is_animated", False)`` can be used instead. + +JpegImagePlugin.convert_dict_qtables +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 8.3.0 +.. versionremoved:: 10.0.0 + +Since deprecation in Pillow 8.3.0, the ``convert_dict_qtables`` method no longer +performed any operations on the data given to it, and has been removed. + +ImagePalette size parameter +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 8.4.0 +.. versionremoved:: 10.0.0 + +Before Pillow 8.3.0, ``ImagePalette`` required palette data of particular lengths by +default, and the ``size`` parameter could be used to override that. Pillow 8.3.0 +removed the default required length, also removing the need for the ``size`` parameter. + +ImageShow.Viewer.show_file file argument +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 9.1.0 +.. versionremoved:: 10.0.0 + +The ``file`` argument in :py:meth:`~PIL.ImageShow.Viewer.show_file()` has been +removed and replaced by ``path``. + +In effect, ``viewer.show_file("test.jpg")`` will continue to work unchanged. + +Constants +~~~~~~~~~ + +.. deprecated:: 9.1.0 +.. versionremoved:: 10.0.0 + +A number of constants have been removed. +Instead, :py:class:`enum.IntEnum` classes have been added. + +.. note:: + + Additional ``Image`` constants were deprecated in Pillow 9.1.0, but that + was reversed in Pillow 9.4.0 and those constants will now remain available. + See :ref:`restored-image-constants` + +===================================================== ============================================================ +Removed Use instead +===================================================== ============================================================ +``Image.LINEAR`` ``Image.BILINEAR`` or ``Image.Resampling.BILINEAR`` +``Image.CUBIC`` ``Image.BICUBIC`` or ``Image.Resampling.BICUBIC`` +``Image.ANTIALIAS`` ``Image.LANCZOS`` or ``Image.Resampling.LANCZOS`` +``ImageCms.INTENT_PERCEPTUAL`` ``ImageCms.Intent.PERCEPTUAL`` +``ImageCms.INTENT_RELATIVE_COLORMETRIC`` ``ImageCms.Intent.RELATIVE_COLORMETRIC`` +``ImageCms.INTENT_SATURATION`` ``ImageCms.Intent.SATURATION`` +``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC`` ``ImageCms.Intent.ABSOLUTE_COLORIMETRIC`` +``ImageCms.DIRECTION_INPUT`` ``ImageCms.Direction.INPUT`` +``ImageCms.DIRECTION_OUTPUT`` ``ImageCms.Direction.OUTPUT`` +``ImageCms.DIRECTION_PROOF`` ``ImageCms.Direction.PROOF`` +``ImageFont.LAYOUT_BASIC`` ``ImageFont.Layout.BASIC`` +``ImageFont.LAYOUT_RAQM`` ``ImageFont.Layout.RAQM`` +``BlpImagePlugin.BLP_FORMAT_JPEG`` ``BlpImagePlugin.Format.JPEG`` +``BlpImagePlugin.BLP_ENCODING_UNCOMPRESSED`` ``BlpImagePlugin.Encoding.UNCOMPRESSED`` +``BlpImagePlugin.BLP_ENCODING_DXT`` ``BlpImagePlugin.Encoding.DXT`` +``BlpImagePlugin.BLP_ENCODING_UNCOMPRESSED_RAW_RGBA`` ``BlpImagePlugin.Encoding.UNCOMPRESSED_RAW_RGBA`` +``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT1`` ``BlpImagePlugin.AlphaEncoding.DXT1`` +``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT3`` ``BlpImagePlugin.AlphaEncoding.DXT3`` +``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT5`` ``BlpImagePlugin.AlphaEncoding.DXT5`` +``FtexImagePlugin.FORMAT_DXT1`` ``FtexImagePlugin.Format.DXT1`` +``FtexImagePlugin.FORMAT_UNCOMPRESSED`` ``FtexImagePlugin.Format.UNCOMPRESSED`` +``PngImagePlugin.APNG_DISPOSE_OP_NONE`` ``PngImagePlugin.Disposal.OP_NONE`` +``PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND`` ``PngImagePlugin.Disposal.OP_BACKGROUND`` +``PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS`` ``PngImagePlugin.Disposal.OP_PREVIOUS`` +``PngImagePlugin.APNG_BLEND_OP_SOURCE`` ``PngImagePlugin.Blend.OP_SOURCE`` +``PngImagePlugin.APNG_BLEND_OP_OVER`` ``PngImagePlugin.Blend.OP_OVER`` +===================================================== ============================================================ + +FitsStubImagePlugin +~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 9.1.0 +.. versionremoved:: 10.0.0 + +The stub image plugin ``FitsStubImagePlugin`` has been removed. +FITS images can be read without a handler through :mod:`~PIL.FitsImagePlugin` instead. + +Font size and offset methods +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 9.2.0 +.. versionremoved:: 10.0.0 + +Several functions for computing the size and offset of rendered text have been removed: + +=============================================================== ============================================================================================================= +Removed Use instead +=============================================================== ============================================================================================================= +``FreeTypeFont.getsize()`` and ``FreeTypeFont.getoffset()`` :py:meth:`.FreeTypeFont.getbbox` and :py:meth:`.FreeTypeFont.getlength` +``FreeTypeFont.getsize_multiline()`` :py:meth:`.ImageDraw.multiline_textbbox` +``ImageFont.getsize()`` :py:meth:`.ImageFont.getbbox` and :py:meth:`.ImageFont.getlength` +``TransposedFont.getsize()`` :py:meth:`.TransposedFont.getbbox` and :py:meth:`.TransposedFont.getlength` +``ImageDraw.textsize()`` and ``ImageDraw.multiline_textsize()`` :py:meth:`.ImageDraw.textbbox`, :py:meth:`.ImageDraw.textlength` and :py:meth:`.ImageDraw.multiline_textbbox` +``ImageDraw2.Draw.textsize()`` :py:meth:`.ImageDraw2.Draw.textbbox` and :py:meth:`.ImageDraw2.Draw.textlength` +=============================================================== ============================================================================================================= + +Previous code:: + + from PIL import Image, ImageDraw, ImageFont + + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + width, height = font.getsize("Hello world") + left, top = font.getoffset("Hello world") + + im = Image.new("RGB", (100, 100)) + draw = ImageDraw.Draw(im) + width, height = draw.textsize("Hello world", font) + + width, height = font.getsize_multiline("Hello\nworld") + width, height = draw.multiline_textsize("Hello\nworld", font) + +Use instead:: + + from PIL import Image, ImageDraw, ImageFont + + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + left, top, right, bottom = font.getbbox("Hello world") + width, height = right - left, bottom - top + + im = Image.new("RGB", (100, 100)) + draw = ImageDraw.Draw(im) + width = draw.textlength("Hello world", font) + + left, top, right, bottom = draw.multiline_textbbox((0, 0), "Hello\nworld", font) + width, height = right - left, bottom - top + +Previously, the ``size`` methods returned a ``height`` that included the vertical +offset of the text, while the new ``bbox`` methods distinguish this as a ``top`` +offset. + +.. image:: ./example/size_vs_bbox.png + :alt: In bbox methods, top measures the vertical distance above the text, while bottom measures that plus the vertical distance of the text itself. In size methods, height also measures the vertical distance above the text plus the vertical distance of the text itself. + :align: center + +If you are using these methods for aligning text, consider using :ref:`text-anchors` instead +which avoid issues that can occur with non-English text or unusual fonts. +For example, instead of the following code:: + + from PIL import Image, ImageDraw, ImageFont + + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + + im = Image.new("RGB", (100, 100)) + draw = ImageDraw.Draw(im) + width, height = draw.textsize("Hello world", font) + x, y = (100 - width) / 2, (100 - height) / 2 + draw.text((x, y), "Hello world", font=font) + +Use instead:: + + from PIL import Image, ImageDraw, ImageFont + + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + + im = Image.new("RGB", (100, 100)) + draw = ImageDraw.Draw(im) + draw.text((100 / 2, 100 / 2), "Hello world", font=font, anchor="mm") + +FreeTypeFont.getmask2 fill parameter +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 9.2.0 +.. versionremoved:: 10.0.0 + +The undocumented ``fill`` parameter of :py:meth:`.FreeTypeFont.getmask2` has been +removed. + +PhotoImage.paste box parameter +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 9.2.0 +.. versionremoved:: 10.0.0 + +The ``box`` parameter was unused and has been removed. + +PyQt5 and PySide2 +~~~~~~~~~~~~~~~~~ + +.. deprecated:: 9.2.0 +.. versionremoved:: 10.0.0 + +`Qt 5 reached end-of-life `_ on 2020-12-08 for +open-source users (and will reach EOL on 2023-12-08 for commercial licence holders). + +Support for PyQt5 and PySide2 has been removed from ``ImageQt``. Upgrade to +`PyQt6 `_ or +`PySide6 `_ instead. + +Image.coerce_e +~~~~~~~~~~~~~~ + +.. deprecated:: 9.2.0 +.. versionremoved:: 10.0.0 + +This undocumented method has been removed. + +PILLOW_VERSION constant +~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 5.2.0 +.. versionremoved:: 9.0.0 + +Use ``__version__`` instead. + +It was initially removed in Pillow 7.0.0, but temporarily brought back in 7.1.0 +to give projects more time to upgrade. + +Image.show command parameter +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 7.2.0 +.. versionremoved:: 9.0.0 + +The ``command`` parameter has been removed. Use a subclass of +:py:class:`.ImageShow.Viewer` instead. + +Image._showxv +~~~~~~~~~~~~~ + +.. deprecated:: 7.2.0 +.. versionremoved:: 9.0.0 + +Use :py:meth:`.Image.Image.show` instead. If custom behaviour is required, use +:py:func:`.ImageShow.register` to add a custom :py:class:`.ImageShow.Viewer` class. + +ImageFile.raise_ioerror +~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 7.2.0 +.. versionremoved:: 9.0.0 + +:py:exc:`IOError` was merged into :py:exc:`OSError` in Python 3.3. +So, ``ImageFile.raise_ioerror`` has been removed. +Use ``ImageFile.raise_oserror`` instead. + +FreeType 2.7 +~~~~~~~~~~~~ + +.. deprecated:: 8.1.0 +.. versionremoved:: 9.0.0 + +Support for FreeType 2.7 has been removed. + +We recommend upgrading to at least `FreeType`_ 2.10.4, which fixed a severe +vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`). + +.. _FreeType: https://freetype.org/ + +im.offset +~~~~~~~~~ + +.. deprecated:: 1.1.2 +.. versionremoved:: 8.0.0 + +``im.offset()`` has been removed, call :py:func:`.ImageChops.offset()` instead. + +It was documented as deprecated in PIL 1.1.2, +raised a :py:exc:`DeprecationWarning` since 1.1.5, +an :py:exc:`Exception` since Pillow 3.0.0 +and :py:exc:`NotImplementedError` since 3.3.0. + +Image.fromstring, im.fromstring and im.tostring +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 2.0.0 +.. versionremoved:: 8.0.0 + +* ``Image.fromstring()`` has been removed, call :py:func:`.Image.frombytes()` instead. +* ``im.fromstring()`` has been removed, call :py:meth:`~PIL.Image.Image.frombytes()` instead. +* ``im.tostring()`` has been removed, call :py:meth:`~PIL.Image.Image.tobytes()` instead. + +They issued a :py:exc:`DeprecationWarning` since 2.0.0, +an :py:exc:`Exception` since 3.0.0 +and :py:exc:`NotImplementedError` since 3.3.0. + +ImageCms.CmsProfile attributes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 3.2.0 +.. versionremoved:: 8.0.0 + +Some attributes in :py:class:`PIL.ImageCms.core.CmsProfile` have been removed. +From 6.0.0, they issued a :py:exc:`DeprecationWarning`: + +======================== =================================================== +Removed Use instead +======================== =================================================== +``color_space`` Padded :py:attr:`~.CmsProfile.xcolor_space` +``pcs`` Padded :py:attr:`~.CmsProfile.connection_space` +``product_copyright`` Unicode :py:attr:`~.CmsProfile.copyright` +``product_desc`` Unicode :py:attr:`~.CmsProfile.profile_description` +``product_description`` Unicode :py:attr:`~.CmsProfile.profile_description` +``product_manufacturer`` Unicode :py:attr:`~.CmsProfile.manufacturer` +``product_model`` Unicode :py:attr:`~.CmsProfile.model` +======================== =================================================== + +Python 2.7 +~~~~~~~~~~ + +.. deprecated:: 6.0.0 +.. versionremoved:: 7.0.0 + +Python 2.7 reached end-of-life on 2020-01-01. Pillow 6.x was the last series to +support Python 2. + +Image.__del__ +~~~~~~~~~~~~~ + +.. deprecated:: 6.1.0 +.. versionremoved:: 7.0.0 + +Implicitly closing the image's underlying file in ``Image.__del__`` has been removed. +Use a context manager or call ``Image.close()`` instead to close the file in a +deterministic way. + +Previous method:: + + im = Image.open("hopper.png") + im.save("out.jpg") + +Use instead:: + + with Image.open("hopper.png") as im: + im.save("out.jpg") + +PIL.*ImagePlugin.__version__ attributes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 6.0.0 +.. versionremoved:: 7.0.0 + +The version constants of individual plugins have been removed. Use ``PIL.__version__`` +instead. + +=============================== ================================= ================================== +Removed Removed Removed +=============================== ================================= ================================== +``BmpImagePlugin.__version__`` ``Jpeg2KImagePlugin.__version__`` ``PngImagePlugin.__version__`` +``CurImagePlugin.__version__`` ``JpegImagePlugin.__version__`` ``PpmImagePlugin.__version__`` +``DcxImagePlugin.__version__`` ``McIdasImagePlugin.__version__`` ``PsdImagePlugin.__version__`` +``EpsImagePlugin.__version__`` ``MicImagePlugin.__version__`` ``SgiImagePlugin.__version__`` +``FliImagePlugin.__version__`` ``MpegImagePlugin.__version__`` ``SunImagePlugin.__version__`` +``FpxImagePlugin.__version__`` ``MpoImagePlugin.__version__`` ``TgaImagePlugin.__version__`` +``GdImageFile.__version__`` ``MspImagePlugin.__version__`` ``TiffImagePlugin.__version__`` +``GifImagePlugin.__version__`` ``PalmImagePlugin.__version__`` ``WmfImagePlugin.__version__`` +``IcoImagePlugin.__version__`` ``PcdImagePlugin.__version__`` ``XbmImagePlugin.__version__`` +``ImImagePlugin.__version__`` ``PcxImagePlugin.__version__`` ``XpmImagePlugin.__version__`` +``ImtImagePlugin.__version__`` ``PdfImagePlugin.__version__`` ``XVThumbImagePlugin.__version__`` +``IptcImagePlugin.__version__`` ``PixarImagePlugin.__version__`` +=============================== ================================= ================================== + +PyQt4 and PySide +~~~~~~~~~~~~~~~~ + +.. deprecated:: 6.0.0 +.. versionremoved:: 7.0.0 + +Qt 4 reached end-of-life on 2015-12-19. Its Python bindings are also EOL: PyQt4 since +2018-08-31 and PySide since 2015-10-14. + +Support for PyQt4 and PySide has been removed from ``ImageQt``. Please upgrade to PyQt5 +or PySide2. + +Setting the size of TIFF images +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 5.3.0 +.. versionremoved:: 7.0.0 + +Setting the size of a TIFF image directly (eg. ``im.size = (256, 256)``) throws +an error. Use ``Image.resize`` instead. + +VERSION constant +~~~~~~~~~~~~~~~~ + +.. deprecated:: 5.2.0 +.. versionremoved:: 6.0.0 + +``VERSION`` (the old PIL version, always 1.1.7) has been removed. Use +``__version__`` instead. + +Undocumented ImageOps functions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 4.3.0 +.. versionremoved:: 6.0.0 + +Several undocumented functions in ``ImageOps`` have been removed. Use the equivalents +in ``ImageFilter`` instead: + +========================== ============================ +Removed Use instead +========================== ============================ +``ImageOps.box_blur`` ``ImageFilter.BoxBlur`` +``ImageOps.gaussian_blur`` ``ImageFilter.GaussianBlur`` +``ImageOps.gblur`` ``ImageFilter.GaussianBlur`` +``ImageOps.usm`` ``ImageFilter.UnsharpMask`` +``ImageOps.unsharp_mask`` ``ImageFilter.UnsharpMask`` +========================== ============================ + +PIL.OleFileIO +~~~~~~~~~~~~~ + +.. deprecated:: 4.0.0 +.. versionremoved:: 6.0.0 + +``PIL.OleFileIO`` was removed as a vendored file in Pillow 4.0.0 (2017-01) in favour of +the upstream :pypi:`olefile` Python package, and replaced with an :py:exc:`ImportError` in 5.0.0 +(2018-01). The deprecated file has now been removed from Pillow. If needed, install from +PyPI (eg. ``python3 -m pip install olefile``). + +import _imaging +~~~~~~~~~~~~~~~ + +.. versionremoved:: 2.1.0 + +Pillow >= 2.1.0 no longer supports ``import _imaging``. +Please use ``from PIL.Image import core as _imaging`` instead. + +Pillow and PIL +~~~~~~~~~~~~~~ + +.. versionremoved:: 1.0.0 + +Pillow and PIL cannot co-exist in the same environment. +Before installing Pillow, please uninstall PIL. + +import Image +~~~~~~~~~~~~ + +.. versionremoved:: 1.0.0 + +Pillow >= 1.0 no longer supports ``import Image``. +Please use ``from PIL import Image`` instead. diff --git a/docs/example/DdsImagePlugin.py b/docs/example/DdsImagePlugin.py index 2a2a0ba29..78e86a169 100644 --- a/docs/example/DdsImagePlugin.py +++ b/docs/example/DdsImagePlugin.py @@ -1,287 +1,287 @@ -""" -A Pillow loader for .dds files (S3TC-compressed aka DXTC) -Jerome Leclanche - -Documentation: - https://web.archive.org/web/20170802060935/http://oss.sgi.com/projects/ogl-sample/registry/EXT/texture_compression_s3tc.txt - -The contents of this file are hereby released in the public domain (CC0) -Full text of the CC0 license: - https://creativecommons.org/publicdomain/zero/1.0/ -""" - -from __future__ import annotations - -import struct -from io import BytesIO - -from PIL import Image, ImageFile - -# Magic ("DDS ") -DDS_MAGIC = 0x20534444 - -# DDS flags -DDSD_CAPS = 0x1 -DDSD_HEIGHT = 0x2 -DDSD_WIDTH = 0x4 -DDSD_PITCH = 0x8 -DDSD_PIXELFORMAT = 0x1000 -DDSD_MIPMAPCOUNT = 0x20000 -DDSD_LINEARSIZE = 0x80000 -DDSD_DEPTH = 0x800000 - -# DDS caps -DDSCAPS_COMPLEX = 0x8 -DDSCAPS_TEXTURE = 0x1000 -DDSCAPS_MIPMAP = 0x400000 - -DDSCAPS2_CUBEMAP = 0x200 -DDSCAPS2_CUBEMAP_POSITIVEX = 0x400 -DDSCAPS2_CUBEMAP_NEGATIVEX = 0x800 -DDSCAPS2_CUBEMAP_POSITIVEY = 0x1000 -DDSCAPS2_CUBEMAP_NEGATIVEY = 0x2000 -DDSCAPS2_CUBEMAP_POSITIVEZ = 0x4000 -DDSCAPS2_CUBEMAP_NEGATIVEZ = 0x8000 -DDSCAPS2_VOLUME = 0x200000 - -# Pixel Format -DDPF_ALPHAPIXELS = 0x1 -DDPF_ALPHA = 0x2 -DDPF_FOURCC = 0x4 -DDPF_PALETTEINDEXED8 = 0x20 -DDPF_RGB = 0x40 -DDPF_LUMINANCE = 0x20000 - - -# dds.h - -DDS_FOURCC = DDPF_FOURCC -DDS_RGB = DDPF_RGB -DDS_RGBA = DDPF_RGB | DDPF_ALPHAPIXELS -DDS_LUMINANCE = DDPF_LUMINANCE -DDS_LUMINANCEA = DDPF_LUMINANCE | DDPF_ALPHAPIXELS -DDS_ALPHA = DDPF_ALPHA -DDS_PAL8 = DDPF_PALETTEINDEXED8 - -DDS_HEADER_FLAGS_TEXTURE = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PIXELFORMAT -DDS_HEADER_FLAGS_MIPMAP = DDSD_MIPMAPCOUNT -DDS_HEADER_FLAGS_VOLUME = DDSD_DEPTH -DDS_HEADER_FLAGS_PITCH = DDSD_PITCH -DDS_HEADER_FLAGS_LINEARSIZE = DDSD_LINEARSIZE - -DDS_HEIGHT = DDSD_HEIGHT -DDS_WIDTH = DDSD_WIDTH - -DDS_SURFACE_FLAGS_TEXTURE = DDSCAPS_TEXTURE -DDS_SURFACE_FLAGS_MIPMAP = DDSCAPS_COMPLEX | DDSCAPS_MIPMAP -DDS_SURFACE_FLAGS_CUBEMAP = DDSCAPS_COMPLEX - -DDS_CUBEMAP_POSITIVEX = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_POSITIVEX -DDS_CUBEMAP_NEGATIVEX = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_NEGATIVEX -DDS_CUBEMAP_POSITIVEY = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_POSITIVEY -DDS_CUBEMAP_NEGATIVEY = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_NEGATIVEY -DDS_CUBEMAP_POSITIVEZ = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_POSITIVEZ -DDS_CUBEMAP_NEGATIVEZ = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_NEGATIVEZ - - -# DXT1 -DXT1_FOURCC = 0x31545844 - -# DXT3 -DXT3_FOURCC = 0x33545844 - -# DXT5 -DXT5_FOURCC = 0x35545844 - - -def _decode565(bits): - a = ((bits >> 11) & 0x1F) << 3 - b = ((bits >> 5) & 0x3F) << 2 - c = (bits & 0x1F) << 3 - return a, b, c - - -def _c2a(a, b): - return (2 * a + b) // 3 - - -def _c2b(a, b): - return (a + b) // 2 - - -def _c3(a, b): - return (2 * b + a) // 3 - - -def _dxt1(data, width, height): - # TODO implement this function as pixel format in decode.c - ret = bytearray(4 * width * height) - - for y in range(0, height, 4): - for x in range(0, width, 4): - color0, color1, bits = struct.unpack("> 2 - if control == 0: - r, g, b = r0, g0, b0 - elif control == 1: - r, g, b = r1, g1, b1 - elif control == 2: - if color0 > color1: - r, g, b = _c2a(r0, r1), _c2a(g0, g1), _c2a(b0, b1) - else: - r, g, b = _c2b(r0, r1), _c2b(g0, g1), _c2b(b0, b1) - elif control == 3: - if color0 > color1: - r, g, b = _c3(r0, r1), _c3(g0, g1), _c3(b0, b1) - else: - r, g, b = 0, 0, 0 - - idx = 4 * ((y + j) * width + x + i) - ret[idx : idx + 4] = struct.pack("4B", r, g, b, 255) - - return bytes(ret) - - -def _dxtc_alpha(a0, a1, ac0, ac1, ai): - if ai <= 12: - ac = (ac0 >> ai) & 7 - elif ai == 15: - ac = (ac0 >> 15) | ((ac1 << 1) & 6) - else: - ac = (ac1 >> (ai - 16)) & 7 - - if ac == 0: - alpha = a0 - elif ac == 1: - alpha = a1 - elif a0 > a1: - alpha = ((8 - ac) * a0 + (ac - 1) * a1) // 7 - elif ac == 6: - alpha = 0 - elif ac == 7: - alpha = 0xFF - else: - alpha = ((6 - ac) * a0 + (ac - 1) * a1) // 5 - - return alpha - - -def _dxt5(data, width, height): - # TODO implement this function as pixel format in decode.c - ret = bytearray(4 * width * height) - - for y in range(0, height, 4): - for x in range(0, width, 4): - a0, a1, ac0, ac1, c0, c1, code = struct.unpack("<2BHI2HI", data.read(16)) - - r0, g0, b0 = _decode565(c0) - r1, g1, b1 = _decode565(c1) - - for j in range(4): - for i in range(4): - ai = 3 * (4 * j + i) - alpha = _dxtc_alpha(a0, a1, ac0, ac1, ai) - - cc = (code >> 2 * (4 * j + i)) & 3 - if cc == 0: - r, g, b = r0, g0, b0 - elif cc == 1: - r, g, b = r1, g1, b1 - elif cc == 2: - r, g, b = _c2a(r0, r1), _c2a(g0, g1), _c2a(b0, b1) - elif cc == 3: - r, g, b = _c3(r0, r1), _c3(g0, g1), _c3(b0, b1) - - idx = 4 * ((y + j) * width + x + i) - ret[idx : idx + 4] = struct.pack("4B", r, g, b, alpha) - - return bytes(ret) - - -class DdsImageFile(ImageFile.ImageFile): - format = "DDS" - format_description = "DirectDraw Surface" - - def _open(self): - if not _accept(self.fp.read(4)): - msg = "not a DDS file" - raise SyntaxError(msg) - (header_size,) = struct.unpack(" + +Documentation: + https://web.archive.org/web/20170802060935/http://oss.sgi.com/projects/ogl-sample/registry/EXT/texture_compression_s3tc.txt + +The contents of this file are hereby released in the public domain (CC0) +Full text of the CC0 license: + https://creativecommons.org/publicdomain/zero/1.0/ +""" + +from __future__ import annotations + +import struct +from io import BytesIO + +from PIL import Image, ImageFile + +# Magic ("DDS ") +DDS_MAGIC = 0x20534444 + +# DDS flags +DDSD_CAPS = 0x1 +DDSD_HEIGHT = 0x2 +DDSD_WIDTH = 0x4 +DDSD_PITCH = 0x8 +DDSD_PIXELFORMAT = 0x1000 +DDSD_MIPMAPCOUNT = 0x20000 +DDSD_LINEARSIZE = 0x80000 +DDSD_DEPTH = 0x800000 + +# DDS caps +DDSCAPS_COMPLEX = 0x8 +DDSCAPS_TEXTURE = 0x1000 +DDSCAPS_MIPMAP = 0x400000 + +DDSCAPS2_CUBEMAP = 0x200 +DDSCAPS2_CUBEMAP_POSITIVEX = 0x400 +DDSCAPS2_CUBEMAP_NEGATIVEX = 0x800 +DDSCAPS2_CUBEMAP_POSITIVEY = 0x1000 +DDSCAPS2_CUBEMAP_NEGATIVEY = 0x2000 +DDSCAPS2_CUBEMAP_POSITIVEZ = 0x4000 +DDSCAPS2_CUBEMAP_NEGATIVEZ = 0x8000 +DDSCAPS2_VOLUME = 0x200000 + +# Pixel Format +DDPF_ALPHAPIXELS = 0x1 +DDPF_ALPHA = 0x2 +DDPF_FOURCC = 0x4 +DDPF_PALETTEINDEXED8 = 0x20 +DDPF_RGB = 0x40 +DDPF_LUMINANCE = 0x20000 + + +# dds.h + +DDS_FOURCC = DDPF_FOURCC +DDS_RGB = DDPF_RGB +DDS_RGBA = DDPF_RGB | DDPF_ALPHAPIXELS +DDS_LUMINANCE = DDPF_LUMINANCE +DDS_LUMINANCEA = DDPF_LUMINANCE | DDPF_ALPHAPIXELS +DDS_ALPHA = DDPF_ALPHA +DDS_PAL8 = DDPF_PALETTEINDEXED8 + +DDS_HEADER_FLAGS_TEXTURE = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PIXELFORMAT +DDS_HEADER_FLAGS_MIPMAP = DDSD_MIPMAPCOUNT +DDS_HEADER_FLAGS_VOLUME = DDSD_DEPTH +DDS_HEADER_FLAGS_PITCH = DDSD_PITCH +DDS_HEADER_FLAGS_LINEARSIZE = DDSD_LINEARSIZE + +DDS_HEIGHT = DDSD_HEIGHT +DDS_WIDTH = DDSD_WIDTH + +DDS_SURFACE_FLAGS_TEXTURE = DDSCAPS_TEXTURE +DDS_SURFACE_FLAGS_MIPMAP = DDSCAPS_COMPLEX | DDSCAPS_MIPMAP +DDS_SURFACE_FLAGS_CUBEMAP = DDSCAPS_COMPLEX + +DDS_CUBEMAP_POSITIVEX = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_POSITIVEX +DDS_CUBEMAP_NEGATIVEX = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_NEGATIVEX +DDS_CUBEMAP_POSITIVEY = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_POSITIVEY +DDS_CUBEMAP_NEGATIVEY = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_NEGATIVEY +DDS_CUBEMAP_POSITIVEZ = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_POSITIVEZ +DDS_CUBEMAP_NEGATIVEZ = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_NEGATIVEZ + + +# DXT1 +DXT1_FOURCC = 0x31545844 + +# DXT3 +DXT3_FOURCC = 0x33545844 + +# DXT5 +DXT5_FOURCC = 0x35545844 + + +def _decode565(bits): + a = ((bits >> 11) & 0x1F) << 3 + b = ((bits >> 5) & 0x3F) << 2 + c = (bits & 0x1F) << 3 + return a, b, c + + +def _c2a(a, b): + return (2 * a + b) // 3 + + +def _c2b(a, b): + return (a + b) // 2 + + +def _c3(a, b): + return (2 * b + a) // 3 + + +def _dxt1(data, width, height): + # TODO implement this function as pixel format in decode.c + ret = bytearray(4 * width * height) + + for y in range(0, height, 4): + for x in range(0, width, 4): + color0, color1, bits = struct.unpack("> 2 + if control == 0: + r, g, b = r0, g0, b0 + elif control == 1: + r, g, b = r1, g1, b1 + elif control == 2: + if color0 > color1: + r, g, b = _c2a(r0, r1), _c2a(g0, g1), _c2a(b0, b1) + else: + r, g, b = _c2b(r0, r1), _c2b(g0, g1), _c2b(b0, b1) + elif control == 3: + if color0 > color1: + r, g, b = _c3(r0, r1), _c3(g0, g1), _c3(b0, b1) + else: + r, g, b = 0, 0, 0 + + idx = 4 * ((y + j) * width + x + i) + ret[idx : idx + 4] = struct.pack("4B", r, g, b, 255) + + return bytes(ret) + + +def _dxtc_alpha(a0, a1, ac0, ac1, ai): + if ai <= 12: + ac = (ac0 >> ai) & 7 + elif ai == 15: + ac = (ac0 >> 15) | ((ac1 << 1) & 6) + else: + ac = (ac1 >> (ai - 16)) & 7 + + if ac == 0: + alpha = a0 + elif ac == 1: + alpha = a1 + elif a0 > a1: + alpha = ((8 - ac) * a0 + (ac - 1) * a1) // 7 + elif ac == 6: + alpha = 0 + elif ac == 7: + alpha = 0xFF + else: + alpha = ((6 - ac) * a0 + (ac - 1) * a1) // 5 + + return alpha + + +def _dxt5(data, width, height): + # TODO implement this function as pixel format in decode.c + ret = bytearray(4 * width * height) + + for y in range(0, height, 4): + for x in range(0, width, 4): + a0, a1, ac0, ac1, c0, c1, code = struct.unpack("<2BHI2HI", data.read(16)) + + r0, g0, b0 = _decode565(c0) + r1, g1, b1 = _decode565(c1) + + for j in range(4): + for i in range(4): + ai = 3 * (4 * j + i) + alpha = _dxtc_alpha(a0, a1, ac0, ac1, ai) + + cc = (code >> 2 * (4 * j + i)) & 3 + if cc == 0: + r, g, b = r0, g0, b0 + elif cc == 1: + r, g, b = r1, g1, b1 + elif cc == 2: + r, g, b = _c2a(r0, r1), _c2a(g0, g1), _c2a(b0, b1) + elif cc == 3: + r, g, b = _c3(r0, r1), _c3(g0, g1), _c3(b0, b1) + + idx = 4 * ((y + j) * width + x + i) + ret[idx : idx + 4] = struct.pack("4B", r, g, b, alpha) + + return bytes(ret) + + +class DdsImageFile(ImageFile.ImageFile): + format = "DDS" + format_description = "DirectDraw Surface" + + def _open(self): + if not _accept(self.fp.read(4)): + msg = "not a DDS file" + raise SyntaxError(msg) + (header_size,) = struct.unpack(" Image.Image: - im = Image.new("RGBA", (200, 100), "white") - d = ImageDraw.Draw(im) - d.line(((100, 0), (100, 100)), "gray") - d.line(((0, 50), (200, 50)), "gray") - d.text((100, 50), f"{anchor} example", "black", font, anchor) - return im - - -if __name__ == "__main__": - im = Image.new("RGBA", (600, 300), "white") - d = ImageDraw.Draw(im) - for y, row in enumerate( - (("ma", "mt", "mm"), ("ms", "mb", "md"), ("ls", "ms", "rs")) - ): - for x, anchor in enumerate(row): - im.paste(test(anchor), (x * 200, y * 100)) - if x != 0: - d.line(((x * 200, y * 100), (x * 200, (y + 1) * 100)), "black", 3) - if y != 0: - d.line(((x * 200, y * 100), ((x + 1) * 200, y * 100)), "black", 3) - im.save("docs/example/anchors.png") - im.show() +from __future__ import annotations + +from PIL import Image, ImageDraw, ImageFont + +font = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 16) + + +def test(anchor: str) -> Image.Image: + im = Image.new("RGBA", (200, 100), "white") + d = ImageDraw.Draw(im) + d.line(((100, 0), (100, 100)), "gray") + d.line(((0, 50), (200, 50)), "gray") + d.text((100, 50), f"{anchor} example", "black", font, anchor) + return im + + +if __name__ == "__main__": + im = Image.new("RGBA", (600, 300), "white") + d = ImageDraw.Draw(im) + for y, row in enumerate( + (("ma", "mt", "mm"), ("ms", "mb", "md"), ("ls", "ms", "rs")) + ): + for x, anchor in enumerate(row): + im.paste(test(anchor), (x * 200, y * 100)) + if x != 0: + d.line(((x * 200, y * 100), (x * 200, (y + 1) * 100)), "black", 3) + if y != 0: + d.line(((x * 200, y * 100), ((x + 1) * 200, y * 100)), "black", 3) + im.save("docs/example/anchors.png") + im.show() diff --git a/docs/handbook/appendices.rst b/docs/handbook/appendices.rst index 347a8848b..01b12685c 100644 --- a/docs/handbook/appendices.rst +++ b/docs/handbook/appendices.rst @@ -1,11 +1,11 @@ -Appendices -========== - -.. note:: Contributors please include appendices as needed or appropriate with your bug fixes, feature additions and tests. - -.. toctree:: - :maxdepth: 2 - - image-file-formats - text-anchors - writing-your-own-image-plugin +Appendices +========== + +.. note:: Contributors please include appendices as needed or appropriate with your bug fixes, feature additions and tests. + +.. toctree:: + :maxdepth: 2 + + image-file-formats + text-anchors + writing-your-own-image-plugin diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index 7da1078c1..083a3509e 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -1,217 +1,217 @@ -Concepts -======== - -The Python Imaging Library handles *raster images*; that is, rectangles of -pixel data. - -.. _concept-bands: - -Bands ------ - -An image can consist of one or more bands of data. The Python Imaging Library -allows you to store several bands in a single image, provided they all have the -same dimensions and depth. For example, a PNG image might have 'R', 'G', 'B', -and 'A' bands for the red, green, blue, and alpha transparency values. Many -operations act on each band separately, e.g., histograms. It is often useful to -think of each pixel as having one value per band. - -To get the number and names of bands in an image, use the -:py:meth:`~PIL.Image.Image.getbands` method. - -.. _concept-modes: - -Modes ------ - -The ``mode`` of an image is a string which defines the type and depth of a pixel in the -image. Each pixel uses the full range of the bit depth. So a 1-bit pixel has a range of -0-1, an 8-bit pixel has a range of 0-255, a 32-signed integer pixel has the range of -INT32 and a 32-bit floating point pixel has the range of FLOAT32. The current release -supports the following standard modes: - - * ``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 - - * ``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 - - * ``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) - -Premultiplied alpha is where the values for each other channel have been -multiplied by the alpha. For example, an RGBA pixel of ``(10, 20, 30, 127)`` -would convert to an RGBa pixel of ``(5, 10, 15, 127)``. The values of the R, -G and B channels are halved as a result of the half transparency in the alpha -channel. - -Apart from these additional modes, Pillow doesn't yet support multichannel -images with a depth of more than 8 bits per channel. - -Pillow also doesn’t support user-defined modes; if you need to handle band -combinations that are not listed above, use a sequence of Image objects. - -You can read the mode of an image through the :py:attr:`~PIL.Image.Image.mode` -attribute. This is a string containing one of the above values. - -Size ----- - -You can read the image size through the :py:attr:`~PIL.Image.Image.size` -attribute. This is a 2-tuple, containing the horizontal and vertical size in -pixels. - -.. _coordinate-system: - -Coordinate System ------------------ - -The Python Imaging Library uses a Cartesian pixel coordinate system, with (0,0) -in the upper left corner. Note that the coordinates refer to the implied pixel -corners; the centre of a pixel addressed as (0, 0) actually lies at (0.5, 0.5). - -Coordinates are usually passed to the library as 2-tuples (x, y). Rectangles -are represented as 4-tuples, (x1, y1, x2, y2), with the upper left corner given -first. - -Palette -------- - -The palette mode (``P``) uses a color palette to define the actual color for -each pixel. - -Info ----- - -You can attach auxiliary information to an image using the -:py:attr:`~PIL.Image.Image.info` attribute. This is a dictionary object. - -How such information is handled when loading and saving image files is up to -the file format handler (see the chapter on :ref:`image-file-formats`). Most -handlers add properties to the :py:attr:`~PIL.Image.Image.info` attribute when -loading an image, but ignore it when saving images. - -Transparency ------------- - -If an image does not have an alpha band, transparency may be specified in the -:py:attr:`~PIL.Image.Image.info` attribute with a "transparency" key. - -Most of the time, the "transparency" value is a single integer, describing -which pixel value is transparent in a "1", "L", "I" or "P" mode image. -However, PNG images may have three values, one for each channel in an "RGB" -mode image, or can have a byte string for a "P" mode image, to specify the -alpha value for each palette entry. - -Orientation ------------ - -A common element of the :py:attr:`~PIL.Image.Image.info` attribute for JPG and -TIFF images is the EXIF orientation tag. This is an instruction for how the -image data should be oriented. For example, it may instruct an image to be -rotated by 90 degrees, or to be mirrored. To apply this information to an -image, :py:meth:`~PIL.ImageOps.exif_transpose` can be used. - -.. _concept-filters: - -Filters -------- - -For geometry operations that may map multiple input pixels to a single output -pixel, the Python Imaging Library provides different resampling *filters*. - -.. py:currentmodule:: PIL.Image - -.. data:: Resampling.NEAREST - :noindex: - - Pick one nearest pixel from the input image. Ignore all other input pixels. - -.. data:: Resampling.BOX - :noindex: - - Each pixel of source image contributes to one pixel of the - destination image with identical weights. - For upscaling is equivalent of :data:`Resampling.NEAREST`. - This filter can only be used with the :py:meth:`~PIL.Image.Image.resize` - and :py:meth:`~PIL.Image.Image.thumbnail` methods. - - .. versionadded:: 3.4.0 - -.. data:: Resampling.BILINEAR - :noindex: - - For resize calculate the output pixel value using linear interpolation - on all pixels that may contribute to the output value. - For other transformations linear interpolation over a 2x2 environment - in the input image is used. - -.. data:: Resampling.HAMMING - :noindex: - - Produces a sharper image than :data:`Resampling.BILINEAR`, doesn't have - dislocations on local level like with :data:`Resampling.BOX`. - This filter can only be used with the :py:meth:`~PIL.Image.Image.resize` - and :py:meth:`~PIL.Image.Image.thumbnail` methods. - - .. versionadded:: 3.4.0 - -.. data:: Resampling.BICUBIC - :noindex: - - For resize calculate the output pixel value using cubic interpolation - on all pixels that may contribute to the output value. - For other transformations cubic interpolation over a 4x4 environment - in the input image is used. - -.. data:: Resampling.LANCZOS - :noindex: - - Calculate the output pixel value using a high-quality Lanczos filter (a - truncated sinc) on all pixels that may contribute to the output value. - This filter can only be used with the :py:meth:`~PIL.Image.Image.resize` - and :py:meth:`~PIL.Image.Image.thumbnail` methods. - - .. versionadded:: 1.1.3 - - -Filters comparison table -~~~~~~~~~~~~~~~~~~~~~~~~ - -+---------------------------+-------------+-----------+-------------+ -| Filter | Downscaling | Upscaling | Performance | -| | quality | quality | | -+===========================+=============+===========+=============+ -|:data:`Resampling.NEAREST` | | | â­â­â­â­â­ | -+---------------------------+-------------+-----------+-------------+ -|:data:`Resampling.BOX` | â­ | | â­â­â­â­ | -+---------------------------+-------------+-----------+-------------+ -|:data:`Resampling.BILINEAR`| â­ | â­ | â­â­â­ | -+---------------------------+-------------+-----------+-------------+ -|:data:`Resampling.HAMMING` | â­â­ | | â­â­â­ | -+---------------------------+-------------+-----------+-------------+ -|:data:`Resampling.BICUBIC` | â­â­â­ | â­â­â­ | â­â­ | -+---------------------------+-------------+-----------+-------------+ -|:data:`Resampling.LANCZOS` | â­â­â­â­ | â­â­â­â­ | â­ | -+---------------------------+-------------+-----------+-------------+ +Concepts +======== + +The Python Imaging Library handles *raster images*; that is, rectangles of +pixel data. + +.. _concept-bands: + +Bands +----- + +An image can consist of one or more bands of data. The Python Imaging Library +allows you to store several bands in a single image, provided they all have the +same dimensions and depth. For example, a PNG image might have 'R', 'G', 'B', +and 'A' bands for the red, green, blue, and alpha transparency values. Many +operations act on each band separately, e.g., histograms. It is often useful to +think of each pixel as having one value per band. + +To get the number and names of bands in an image, use the +:py:meth:`~PIL.Image.Image.getbands` method. + +.. _concept-modes: + +Modes +----- + +The ``mode`` of an image is a string which defines the type and depth of a pixel in the +image. Each pixel uses the full range of the bit depth. So a 1-bit pixel has a range of +0-1, an 8-bit pixel has a range of 0-255, a 32-signed integer pixel has the range of +INT32 and a 32-bit floating point pixel has the range of FLOAT32. The current release +supports the following standard modes: + + * ``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 + + * ``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 + + * ``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) + +Premultiplied alpha is where the values for each other channel have been +multiplied by the alpha. For example, an RGBA pixel of ``(10, 20, 30, 127)`` +would convert to an RGBa pixel of ``(5, 10, 15, 127)``. The values of the R, +G and B channels are halved as a result of the half transparency in the alpha +channel. + +Apart from these additional modes, Pillow doesn't yet support multichannel +images with a depth of more than 8 bits per channel. + +Pillow also doesn’t support user-defined modes; if you need to handle band +combinations that are not listed above, use a sequence of Image objects. + +You can read the mode of an image through the :py:attr:`~PIL.Image.Image.mode` +attribute. This is a string containing one of the above values. + +Size +---- + +You can read the image size through the :py:attr:`~PIL.Image.Image.size` +attribute. This is a 2-tuple, containing the horizontal and vertical size in +pixels. + +.. _coordinate-system: + +Coordinate System +----------------- + +The Python Imaging Library uses a Cartesian pixel coordinate system, with (0,0) +in the upper left corner. Note that the coordinates refer to the implied pixel +corners; the centre of a pixel addressed as (0, 0) actually lies at (0.5, 0.5). + +Coordinates are usually passed to the library as 2-tuples (x, y). Rectangles +are represented as 4-tuples, (x1, y1, x2, y2), with the upper left corner given +first. + +Palette +------- + +The palette mode (``P``) uses a color palette to define the actual color for +each pixel. + +Info +---- + +You can attach auxiliary information to an image using the +:py:attr:`~PIL.Image.Image.info` attribute. This is a dictionary object. + +How such information is handled when loading and saving image files is up to +the file format handler (see the chapter on :ref:`image-file-formats`). Most +handlers add properties to the :py:attr:`~PIL.Image.Image.info` attribute when +loading an image, but ignore it when saving images. + +Transparency +------------ + +If an image does not have an alpha band, transparency may be specified in the +:py:attr:`~PIL.Image.Image.info` attribute with a "transparency" key. + +Most of the time, the "transparency" value is a single integer, describing +which pixel value is transparent in a "1", "L", "I" or "P" mode image. +However, PNG images may have three values, one for each channel in an "RGB" +mode image, or can have a byte string for a "P" mode image, to specify the +alpha value for each palette entry. + +Orientation +----------- + +A common element of the :py:attr:`~PIL.Image.Image.info` attribute for JPG and +TIFF images is the EXIF orientation tag. This is an instruction for how the +image data should be oriented. For example, it may instruct an image to be +rotated by 90 degrees, or to be mirrored. To apply this information to an +image, :py:meth:`~PIL.ImageOps.exif_transpose` can be used. + +.. _concept-filters: + +Filters +------- + +For geometry operations that may map multiple input pixels to a single output +pixel, the Python Imaging Library provides different resampling *filters*. + +.. py:currentmodule:: PIL.Image + +.. data:: Resampling.NEAREST + :noindex: + + Pick one nearest pixel from the input image. Ignore all other input pixels. + +.. data:: Resampling.BOX + :noindex: + + Each pixel of source image contributes to one pixel of the + destination image with identical weights. + For upscaling is equivalent of :data:`Resampling.NEAREST`. + This filter can only be used with the :py:meth:`~PIL.Image.Image.resize` + and :py:meth:`~PIL.Image.Image.thumbnail` methods. + + .. versionadded:: 3.4.0 + +.. data:: Resampling.BILINEAR + :noindex: + + For resize calculate the output pixel value using linear interpolation + on all pixels that may contribute to the output value. + For other transformations linear interpolation over a 2x2 environment + in the input image is used. + +.. data:: Resampling.HAMMING + :noindex: + + Produces a sharper image than :data:`Resampling.BILINEAR`, doesn't have + dislocations on local level like with :data:`Resampling.BOX`. + This filter can only be used with the :py:meth:`~PIL.Image.Image.resize` + and :py:meth:`~PIL.Image.Image.thumbnail` methods. + + .. versionadded:: 3.4.0 + +.. data:: Resampling.BICUBIC + :noindex: + + For resize calculate the output pixel value using cubic interpolation + on all pixels that may contribute to the output value. + For other transformations cubic interpolation over a 4x4 environment + in the input image is used. + +.. data:: Resampling.LANCZOS + :noindex: + + Calculate the output pixel value using a high-quality Lanczos filter (a + truncated sinc) on all pixels that may contribute to the output value. + This filter can only be used with the :py:meth:`~PIL.Image.Image.resize` + and :py:meth:`~PIL.Image.Image.thumbnail` methods. + + .. versionadded:: 1.1.3 + + +Filters comparison table +~~~~~~~~~~~~~~~~~~~~~~~~ + ++---------------------------+-------------+-----------+-------------+ +| Filter | Downscaling | Upscaling | Performance | +| | quality | quality | | ++===========================+=============+===========+=============+ +|:data:`Resampling.NEAREST` | | | â­â­â­â­â­ | ++---------------------------+-------------+-----------+-------------+ +|:data:`Resampling.BOX` | â­ | | â­â­â­â­ | ++---------------------------+-------------+-----------+-------------+ +|:data:`Resampling.BILINEAR`| â­ | â­ | â­â­â­ | ++---------------------------+-------------+-----------+-------------+ +|:data:`Resampling.HAMMING` | â­â­ | | â­â­â­ | ++---------------------------+-------------+-----------+-------------+ +|:data:`Resampling.BICUBIC` | â­â­â­ | â­â­â­ | â­â­ | ++---------------------------+-------------+-----------+-------------+ +|:data:`Resampling.LANCZOS` | â­â­â­â­ | â­â­â­â­ | â­ | ++---------------------------+-------------+-----------+-------------+ diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 1ec972149..cd7abe5e9 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1,1721 +1,1721 @@ -.. _image-file-formats: - -Image file formats -================== - -The Python Imaging Library supports a wide variety of raster file formats. -Over 30 different file formats can be identified and read by the library. -Write support is less extensive, but most common interchange and presentation -formats are supported. - -The :py:meth:`~PIL.Image.open` function identifies files from their -contents, not their names, but the :py:meth:`~PIL.Image.Image.save` method -looks at the name to determine which format to use, unless the format is given -explicitly. - -When an image is opened from a file, only that instance of the image is considered to -have the format. Copies of the image will contain data loaded from the file, but not -the file itself, meaning that it can no longer be considered to be in the original -format. So if :py:meth:`~PIL.Image.Image.copy` is called on an image, or another method -internally creates a copy of the image, then any methods or attributes specific to the -format will no longer be present. The ``fp`` (file pointer) attribute will no longer be -present, and the :py:attr:`~PIL.Image.Image.format` attribute will be ``None``. - -Fully supported formats ------------------------ - -BLP -^^^ - -BLP is the Blizzard Mipmap Format, a texture format used in World of -Warcraft. Pillow supports reading ``JPEG`` Compressed or raw ``BLP1`` -images, and all types of ``BLP2`` images. - -Saving -~~~~~~ - -Pillow supports writing BLP images. The :py:meth:`~PIL.Image.Image.save` method -can take the following keyword arguments: - -**blp_version** - If present and set to "BLP1", images will be saved as BLP1. Otherwise, images - will be saved as BLP2. - -BMP -^^^ - -Pillow reads and writes Windows and OS/2 BMP files containing ``1``, ``L``, ``P``, -or ``RGB`` data. 16-colour images are read as ``P`` images. -Support for reading 8-bit run-length encoding was added in Pillow 9.1.0. -Support for reading 4-bit run-length encoding was added in Pillow 9.3.0. - -Opening -~~~~~~~ - -The :py:meth:`~PIL.Image.open` method sets the following -:py:attr:`~PIL.Image.Image.info` properties: - -**compression** - Set to 1 if the file is a 256-color run-length encoded image. - Set to 2 if the file is a 16-color run-length encoded image. - -DDS -^^^ - -DDS is a popular container texture format used in video games and natively supported -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 - ``RGB`` and ``RGBA`` mode. - -.. versionadded:: 6.0.0 - Uncompressed ``RGBA`` images can be read. - - -.. versionadded:: 8.3.0 - BC5S images can be opened in ``RGB`` mode, and uncompressed ``RGB`` images - can be read. Uncompressed data can also be saved to image files. - - -.. versionadded:: 9.3.0 - ATI1 images can be opened in ``L`` mode and ATI2 images can be opened in - ``RGB`` mode. - -.. versionadded:: 9.4.0 - Uncompressed ``L`` ("luminance") and ``LA`` images can be opened and saved. - - -.. versionadded:: 10.1.0 - BC5U can be read in ``RGB`` mode, and 8-bit color indexed images can be read - in ``P`` mode. - - -DIB -^^^ - -Pillow reads and writes DIB files. DIB files are similar to BMP files, so see -above for more information. - - .. versionadded:: 6.0.0 - -EPS -^^^ - -Pillow identifies EPS files containing image data, and can read files that -contain embedded raster images (ImageData descriptors). If Ghostscript is -available, other EPS files can be read as well. The EPS driver can also write -EPS images. The EPS driver can read EPS images in ``L``, ``LAB``, ``RGB`` and -``CMYK`` mode, but Ghostscript may convert the images to ``RGB`` mode rather -than leaving them in the original color space. The EPS driver can write images -in ``L``, ``RGB`` and ``CMYK`` modes. - -Loading -~~~~~~~ - -To use Ghostscript, Pillow searches for the "gs" executable. On Windows, it -also searches for "gswin32c" and "gswin64c". To customise this behaviour, -``EpsImagePlugin.gs_binary = "gswin64"`` will set the name of the executable to -use. ``EpsImagePlugin.gs_binary = False`` will prevent Ghostscript use. - -If Ghostscript is available, you can call the :py:meth:`~PIL.Image.Image.load` -method with the following parameters to affect how Ghostscript renders the EPS. - -**scale** - Affects the scale of the resultant rasterized image. If the EPS suggests - that the image be rendered at 100px x 100px, setting this parameter to - 2 will make the Ghostscript render a 200px x 200px image instead. The - relative position of the bounding box is maintained:: - - im = Image.open(...) - im.size # (100,100) - im.load(scale=2) - im.size # (200,200) - -**transparency** - If true, generates an RGBA image with a transparent background, instead of - the default behaviour of an RGB image with a white background. - - -GIF -^^^ - -Pillow reads GIF87a and GIF89a versions of the GIF file format. The library -writes files in GIF87a by default, unless GIF89a features are used or GIF89a is -already in use. Files are written with LZW encoding. - -GIF files are initially read as grayscale (``L``) or palette mode (``P``) -images. Seeking to later frames in a ``P`` image will change the image to -``RGB`` (or ``RGBA`` if the first frame had transparency). - -``P`` mode images are changed to ``RGB`` because each frame of a GIF may contain -its own individual palette of up to 256 colors. When a new frame is placed onto a -previous frame, those colors may combine to exceed the ``P`` mode limit of 256 -colors. Instead, the image is converted to ``RGB`` handle this. - -If you would prefer the first ``P`` image frame to be ``RGB`` as well, so that -every ``P`` frame is converted to ``RGB`` or ``RGBA`` mode, there is a setting -available:: - - from PIL import GifImagePlugin - GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS - -GIF frames do not always contain individual palettes however. If there is only -a global palette, then all of the colors can fit within ``P`` mode. If you would -prefer the frames to be kept as ``P`` in that case, there is also a setting -available:: - - from PIL import GifImagePlugin - GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY - -To restore the default behavior, where ``P`` mode images are only converted to -``RGB`` or ``RGBA`` after the first frame:: - - from PIL import GifImagePlugin - GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST - -.. _gif-opening: - -Opening -~~~~~~~ - -The :py:meth:`~PIL.Image.open` method sets the following -:py:attr:`~PIL.Image.Image.info` properties: - -**background** - Default background color (a palette color index). - -**transparency** - Transparency color index. This key is omitted if the image is not - transparent. - -**version** - Version (either ``GIF87a`` or ``GIF89a``). - -**duration** - May not be present. The time to display the current frame - of the GIF, in milliseconds. - -**loop** - May not be present. The number of times the GIF should loop. 0 means that - it will loop forever. - -**comment** - May not be present. A comment about the image. This is the last comment found - before the current frame's image. - -**extension** - May not be present. Contains application specific information. - -Reading sequences -~~~~~~~~~~~~~~~~~ - -The GIF loader supports the :py:meth:`~PIL.Image.Image.seek` and -:py:meth:`~PIL.Image.Image.tell` methods. You can combine these methods -to seek to the next frame (``im.seek(im.tell() + 1)``). - -``im.seek()`` raises an :py:exc:`EOFError` if you try to seek after the last frame. - -.. _gif-saving: - -Saving -~~~~~~ - -When calling :py:meth:`~PIL.Image.Image.save` to write a GIF file, the -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. - -**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. - - 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. - -**include_color_table** - Whether or not to include local color table. - -**interlace** - Whether or not the image is interlaced. By default, it is, unless the image - is less than 16 pixels in width or height. - -**disposal** - Indicates the way in which the graphic is to be treated after being displayed. - - * 0 - No disposal specified. - * 1 - Do not dispose. - * 2 - Restore to background color. - * 3 - Restore to previous content. - - Pass a single integer for a constant disposal, or a list or tuple - to set the disposal for each frame separately. - -**palette** - Use the specified palette for the saved image. The palette should - be a bytes or bytearray object containing the palette entries in - RGBRGB... form. It should be no more than 768 bytes. Alternately, - the palette can be passed in as an - :py:class:`PIL.ImagePalette.ImagePalette` object. - -**optimize** - Whether to attempt to compress the palette by eliminating unused colors - (this is only useful if the palette can be compressed to the next smaller - power of 2 elements) and whether to mark all pixels that are not new in the - next frame as transparent. - - This is attempted by default, unless a palette is specified as an option or - as part of the first image's :py:attr:`~PIL.Image.Image.info` dictionary. - -Note that if the image you are saving comes from an existing GIF, it may have -the following properties in its :py:attr:`~PIL.Image.Image.info` dictionary. -For these options, if you do not pass them in, they will default to -their :py:attr:`~PIL.Image.Image.info` values. - -**transparency** - Transparency color index. - -**duration** - The display duration of each frame of the multiframe gif, in - milliseconds. Pass a single integer for a constant duration, or a - list or tuple to set the duration for each frame separately. - -**loop** - Integer number of times the GIF should loop. 0 means that it will loop - forever. If omitted or ``None``, the image will not loop. - -**comment** - A comment about the image. - -Reading local images -~~~~~~~~~~~~~~~~~~~~ - -The GIF loader creates an image memory the same size as the GIF file’s *logical -screen size*, and pastes the actual pixel data (the *local image*) into this -image. If you only want the actual pixel rectangle, you can crop the image:: - - im = Image.open(...) - - if im.tile[0][0] == "gif": - # only read the first "local image" from this GIF file - box = im.tile[0][1] - im = im.crop(box) - -ICNS -^^^^ - -Pillow reads and writes macOS ``.icns`` files. By default, the -largest available icon is read, though you can override this by setting the -:py:attr:`~PIL.Image.Image.size` property before calling -:py:meth:`~PIL.Image.Image.load`. The :py:meth:`~PIL.Image.open` method -sets the following :py:attr:`~PIL.Image.Image.info` property: - -.. note:: - - Prior to version 8.3.0, Pillow could only write ICNS files on macOS. - -**sizes** - A list of supported sizes found in this icon file; these are a - 3-tuple, ``(width, height, scale)``, where ``scale`` is 2 for a retina - icon and 1 for a standard icon. You *are* permitted to use this 3-tuple - format for the :py:attr:`~PIL.Image.Image.size` property if you set it - before calling :py:meth:`~PIL.Image.Image.load`; after loading, the size - will be reset to a 2-tuple containing pixel dimensions (so, e.g. if you - ask for ``(512, 512, 2)``, the final value of - :py:attr:`~PIL.Image.Image.size` will be ``(1024, 1024)``). - -.. _icns-saving: - -Saving -~~~~~~ - -The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments: - -**append_images** - A list of images to replace the scaled down versions of the image. - The order of the images does not matter, as their use is determined by - the size of each image. - - .. versionadded:: 5.1.0 - -ICO -^^^ - -ICO is used to store icons on Windows. The largest available icon is read. - -.. _ico-saving: - -Saving -~~~~~~ - -The :py:meth:`~PIL.Image.Image.save` method supports the following options: - -**sizes** - A list of sizes including in this ico file; these are a 2-tuple, - ``(width, height)``; Default to ``[(16, 16), (24, 24), (32, 32), (48, 48), - (64, 64), (128, 128), (256, 256)]``. Any sizes bigger than the original - size or 256 will be ignored. - -The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments: - -**append_images** - A list of images to replace the scaled down versions of the image. - The order of the images does not matter, as their use is determined by - the size of each image. - - .. versionadded:: 8.1.0 - -**bitmap_format** - By default, the image data will be saved in PNG format. With a bitmap format of - "bmp", image data will be saved in BMP format instead. - - .. versionadded:: 8.3.0 - -IM -^^ - -IM is a format used by LabEye and other applications based on the IFUNC image -processing library. The library reads and writes most uncompressed interchange -versions of this format. - -IM is the only format that can store all internal Pillow formats. - -JPEG -^^^^ - -Pillow reads JPEG, JFIF, and Adobe JPEG files containing ``L``, ``RGB``, or -``CMYK`` data. It writes standard and progressive JFIF files. - -Using the :py:meth:`~PIL.Image.Image.draft` method, you can speed things up by -converting ``RGB`` images to ``L``, and resize images to 1/2, 1/4 or 1/8 of -their original size while loading them. - -By default Pillow doesn't allow loading of truncated JPEG files, set -:data:`.ImageFile.LOAD_TRUNCATED_IMAGES` to override this. - -.. _jpeg-opening: - -Opening -~~~~~~~ - -The :py:meth:`~PIL.Image.open` method may set the following -:py:attr:`~PIL.Image.Image.info` properties if available: - -**jfif** - JFIF application marker found. If the file is not a JFIF file, this key is - not present. - -**jfif_version** - A tuple representing the jfif version, (major version, minor version). - -**jfif_density** - A tuple representing the pixel density of the image, in units specified - by jfif_unit. - -**jfif_unit** - Units for the jfif_density: - - * 0 - No Units - * 1 - Pixels per Inch - * 2 - Pixels per Centimeter - -**dpi** - A tuple representing the reported pixel density in pixels per inch, if - the file is a jfif file and the units are in inches. - -**adobe** - Adobe application marker found. If the file is not an Adobe JPEG file, this - key is not present. - -**adobe_transform** - Vendor Specific Tag. - -**progression** - Indicates that this is a progressive JPEG file. - -**icc_profile** - The ICC color profile for the image. - -**exif** - Raw EXIF data from the image. - -**comment** - A comment about the image. - - .. versionadded:: 7.1.0 - -.. _jpeg-saving: - -Saving -~~~~~~ - -The :py:meth:`~PIL.Image.Image.save` method supports the following options: - -**quality** - The image quality, on a scale from 0 (worst) to 95 (best), or the string - ``keep``. The default is 75. Values above 95 should be avoided; 100 disables - portions of the JPEG compression algorithm, and results in large files with - hardly any gain in image quality. The value ``keep`` is only valid for JPEG - files and will retain the original image quality level, subsampling, and - qtables. - -**optimize** - If present and true, indicates that the encoder should make an extra pass - over the image in order to select optimal encoder settings. - -**progressive** - If present and true, indicates that this image should be stored as a - progressive JPEG file. - -**dpi** - A tuple of integers representing the pixel density, ``(x,y)``. - -**icc_profile** - If present and true, the image is stored with the provided ICC profile. - If this parameter is not provided, the image will be saved with no profile - attached. To preserve the existing profile:: - - im.save(filename, 'jpeg', icc_profile=im.info.get('icc_profile')) - -**exif** - If present, the image will be stored with the provided raw EXIF data. - -**keep_rgb** - By default, libjpeg converts images with an RGB color space to YCbCr. - If this option is present and true, those images will be stored as RGB - instead. - - When this option is enabled, attempting to chroma-subsample RGB images - with the ``subsampling`` option will raise an :py:exc:`OSError`. - - .. versionadded:: 10.2.0 - -**subsampling** - If present, sets the subsampling for the encoder. - - * ``keep``: Only valid for JPEG files, will retain the original image setting. - * ``4:4:4``, ``4:2:2``, ``4:2:0``: Specific sampling values - * ``0``: equivalent to ``4:4:4`` - * ``1``: equivalent to ``4:2:2`` - * ``2``: equivalent to ``4:2:0`` - - If absent, the setting will be determined by libjpeg or libjpeg-turbo. - -**restart_marker_blocks** - If present, emit a restart marker whenever the specified number of MCU - blocks has been produced. - - .. versionadded:: 10.2.0 - -**restart_marker_rows** - If present, emit a restart marker whenever the specified number of MCU - rows has been produced. - - .. versionadded:: 10.2.0 - -**qtables** - If present, sets the qtables for the encoder. This is listed as an - advanced option for wizards in the JPEG documentation. Use with - caution. ``qtables`` can be one of several types of values: - - * a string, naming a preset, e.g. ``keep``, ``web_low``, or ``web_high`` - * a list, tuple, or dictionary (with integer keys = - range(len(keys))) of lists of 64 integers. There must be - between 2 and 4 tables. - - .. versionadded:: 2.5.0 - -**streamtype** - Allows storing images without quantization and Huffman tables, or with - these tables but without image data. This is useful for container formats - or network protocols that handle tables separately and share them between - images. - - * ``0`` (default): interchange datastream, with tables and image data - * ``1``: abbreviated table specification (tables-only) datastream - - .. versionadded:: 10.2.0 - - * ``2``: abbreviated image (image-only) datastream - -**comment** - A comment about the image. - - .. versionadded:: 9.4.0 - - -.. note:: - - To enable JPEG support, you need to build and install the IJG JPEG library - before building the Python Imaging Library. See the distribution README for - details. - -JPEG 2000 -^^^^^^^^^ - -.. versionadded:: 2.4.0 - -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). - -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 -convert the image to either ``RGB`` or ``RGBA`` rather than choosing for -itself. It is also possible to set ``reduce`` to the number of resolutions to -discard (each one reduces the size of the resulting image by a factor of 2), -and ``layers`` to specify the number of quality layers to load. - -.. _jpeg-2000-saving: - -Saving -~~~~~~ - -The :py:meth:`~PIL.Image.Image.save` method supports the following options: - -**offset** - The image offset, as a tuple of integers, e.g. (16, 16) - -**tile_offset** - The tile offset, again as a 2-tuple of integers. - -**tile_size** - The tile size as a 2-tuple. If not specified, or if set to None, the - image will be saved without tiling. - -**quality_mode** - Either ``"rates"`` or ``"dB"`` depending on the units you want to use to - specify image quality. - -**quality_layers** - A sequence of numbers, each of which represents either an approximate size - reduction (if quality mode is ``"rates"``) or a signal to noise ratio value - in decibels. If not specified, defaults to a single layer of full quality. - -**num_resolutions** - The number of different image resolutions to be stored (which corresponds - to the number of Discrete Wavelet Transform decompositions plus one). - -**codeblock_size** - The code-block size as a 2-tuple. Minimum size is 4 x 4, maximum is 1024 x - 1024, with the additional restriction that no code-block may have more - than 4096 coefficients (i.e. the product of the two numbers must be no - greater than 4096). - -**precinct_size** - The precinct size as a 2-tuple. Must be a power of two along both axes, - and must be greater than the code-block size. - -**irreversible** - If ``True``, use the lossy discrete waveform transformation DWT 9-7. - Defaults to ``False``, which uses the lossless DWT 5-3. - -**mct** - If ``1`` then enable multiple component transformation when encoding, - otherwise use ``0`` for no component transformation (default). If MCT is - enabled and ``irreversible`` is ``True`` then the Irreversible Color - Transformation will be applied, otherwise encoding will use the - Reversible Color Transformation. MCT works best with a ``mode`` of - ``RGB`` and is only applicable when the image data has 3 components. - - .. versionadded:: 9.1.0 - -**progression** - Controls the progression order; must be one of ``"LRCP"``, ``"RLCP"``, - ``"RPCL"``, ``"PCRL"``, ``"CPRL"``. The letters stand for Component, - Position, Resolution and Layer respectively and control the order of - encoding, the idea being that e.g. an image encoded using LRCP mode can - have its quality layers decoded as they arrive at the decoder, while one - encoded using RLCP mode will have increasing resolutions decoded as they - arrive, and so on. - -**signed** - If true, then tell the encoder to save the image as signed. - - .. versionadded:: 9.4.0 - -**cinema_mode** - Set the encoder to produce output compliant with the digital cinema - specifications. The options here are ``"no"`` (the default), - ``"cinema2k-24"`` for 24fps 2K, ``"cinema2k-48"`` for 48fps 2K, and - ``"cinema4k-24"`` for 24fps 4K. Note that for compliant 2K files, - *at least one* of your image dimensions must match 2048 x 1080, while - for compliant 4K files, *at least one* of the dimensions must match - 4096 x 2160. - -**no_jp2** - If ``True`` then don't wrap the raw codestream in the JP2 file format when - saving, otherwise the extension of the filename will be used to determine - the format (default). - - .. versionadded:: 9.1.0 - -**comment** - Adds a custom comment to the file, replacing the default - "Created by OpenJPEG version" comment. - - .. versionadded:: 9.5.0 - -**plt** - If ``True`` and OpenJPEG 2.4.0 or later is available, then include a PLT - (packet length, tile-part header) marker in the produced file. - Defaults to ``False``. - - .. versionadded:: 9.5.0 - -.. note:: - - To enable JPEG 2000 support, you need to build and install the OpenJPEG - library, version 2.0.0 or higher, before building the Python Imaging - Library. - - Windows users can install the OpenJPEG binaries available on the - OpenJPEG website, but must add them to their PATH in order to use Pillow (if - you fail to do this, you will get errors about not being able to load the - ``_imaging`` DLL). - -MSP -^^^ - -Pillow identifies and reads MSP files from Windows 1 and 2. The library writes -uncompressed (Windows 1) versions of this format. - -PCX -^^^ - -Pillow reads and writes PCX files containing ``1``, ``L``, ``P``, or ``RGB`` data. - -PFM -^^^ - -.. versionadded:: 10.3.0 - -Pillow reads and writes grayscale (Pf format) Portable FloatMap (PFM) files -containing ``F`` data. - -Color (PF format) PFM files are not supported. - -Opening -~~~~~~~ - -The :py:func:`~PIL.Image.open` function sets the following -:py:attr:`~PIL.Image.Image.info` properties: - -**scale** - The absolute value of the number stored in the *Scale Factor / Endianness* line. - -PNG -^^^ - -Pillow identifies, reads, and writes PNG files containing ``1``, ``L``, ``LA``, -``I``, ``P``, ``RGB`` or ``RGBA`` data. Interlaced files are supported as of -v1.1.7. - -As of Pillow 6.0, EXIF data can be read from PNG images. However, unlike other -image formats, EXIF data is not guaranteed to be present in -:py:attr:`~PIL.Image.Image.info` until :py:meth:`~PIL.Image.Image.load` has been -called. - -By default Pillow doesn't allow loading of truncated PNG files, set -:data:`.ImageFile.LOAD_TRUNCATED_IMAGES` to override this. - -.. _png-opening: - -Opening -~~~~~~~ - -The :py:func:`~PIL.Image.open` function sets the following -:py:attr:`~PIL.Image.Image.info` properties, when appropriate: - -**chromaticity** - The chromaticity points, as an 8 tuple of floats. (``White Point - X``, ``White Point Y``, ``Red X``, ``Red Y``, ``Green X``, ``Green - Y``, ``Blue X``, ``Blue Y``) - -**gamma** - Gamma, given as a floating point number. - -**srgb** - The sRGB rendering intent as an integer. - - * 0 Perceptual - * 1 Relative Colorimetric - * 2 Saturation - * 3 Absolute Colorimetric - -**transparency** - For ``P`` images: Either the palette index for full transparent pixels, - or a byte string with alpha values for each palette entry. - - For ``1``, ``L``, ``I`` and ``RGB`` images, the color that represents - full transparent pixels in this image. - - This key is omitted if the image is not a transparent palette image. - -.. _png-text: - -``open`` also sets ``Image.text`` to a dictionary of the values of the -``tEXt``, ``zTXt``, and ``iTXt`` chunks of the PNG image. Individual -compressed chunks are limited to a decompressed size of -:data:`.PngImagePlugin.MAX_TEXT_CHUNK`, by default 1MB, to prevent -decompression bombs. Additionally, the total size of all of the text -chunks is limited to :data:`.PngImagePlugin.MAX_TEXT_MEMORY`, defaulting to -64MB. - -.. _png-saving: - -Saving -~~~~~~ - -The :py:meth:`~PIL.Image.Image.save` method supports the following options: - -**optimize** - If present and true, instructs the PNG writer to make the output file as - small as possible. This includes extra processing in order to find optimal - encoder settings. - -**transparency** - For ``P``, ``1``, ``L``, ``I``, and ``RGB`` images, this option controls - what color from the image to mark as transparent. - - For ``P`` images, this can be a either the palette index, - or a byte string with alpha values for each palette entry. - -**dpi** - A tuple of two numbers corresponding to the desired dpi in each direction. - -**pnginfo** - A :py:class:`PIL.PngImagePlugin.PngInfo` instance containing chunks. - -**compress_level** - ZLIB compression level, a number between 0 and 9: 1 gives best speed, - 9 gives best compression, 0 gives no compression at all. Default is 6. - When ``optimize`` option is True ``compress_level`` has no effect - (it is set to 9 regardless of a value passed). - -**icc_profile** - The ICC Profile to include in the saved file. - -**exif** - The exif data to include in the saved file. - - .. versionadded:: 6.0.0 - -**bits (experimental)** - For ``P`` images, this option controls how many bits to store. If omitted, - the PNG writer uses 8 bits (256 colors). - -**dictionary (experimental)** - Set the ZLIB encoder dictionary. - -.. note:: - - To enable PNG support, you need to build and install the ZLIB compression - library before building the Python Imaging Library. See the - :doc:`installation documentation <../installation>` for details. - -.. _apng-sequences: - -APNG sequences -~~~~~~~~~~~~~~ - -The PNG loader includes limited support for reading and writing Animated Portable -Network Graphics (APNG) files. -When an APNG file is loaded, :py:meth:`~PIL.ImageFile.ImageFile.get_format_mimetype` -will return ``"image/apng"``. The value of the :py:attr:`~PIL.Image.Image.is_animated` -property will be ``True`` when the :py:attr:`~PIL.Image.Image.n_frames` property is -greater than 1. For APNG files, the ``n_frames`` property depends on both the animation -frame count as well as the presence or absence of a default image. See the -``default_image`` property documentation below for more details. -The :py:meth:`~PIL.Image.Image.seek` and :py:meth:`~PIL.Image.Image.tell` methods -are supported. - -``im.seek()`` raises an :py:exc:`EOFError` if you try to seek after the last frame. - -These :py:attr:`~PIL.Image.Image.info` properties will be set for APNG frames, -where applicable: - -**default_image** - Specifies whether or not this APNG file contains a separate default image, - which is not a part of the actual APNG animation. - - When an APNG file contains a default image, the initially loaded image (i.e. - the result of ``seek(0)``) will be the default image. - To account for the presence of the default image, the - :py:attr:`~PIL.Image.Image.n_frames` property will be set to ``frame_count + 1``, - where ``frame_count`` is the actual APNG animation frame count. - To load the first APNG animation frame, ``seek(1)`` must be called. - - * ``True`` - The APNG contains default image, which is not an animation frame. - * ``False`` - The APNG does not contain a default image. The ``n_frames`` property - will be set to the actual APNG animation frame count. - The initially loaded image (i.e. ``seek(0)``) will be the first APNG animation - frame. - -**loop** - The number of times to loop this APNG, 0 indicates infinite looping. - -**duration** - The time to display this APNG frame (in milliseconds). - -.. note:: - - The APNG loader returns images the same size as the APNG file's logical screen size. - The returned image contains the pixel data for a given frame, after applying - any APNG frame disposal and frame blend operations (i.e. it contains what a web - browser would render for this frame - the composite of all previous frames and this - frame). - - Any APNG file containing sequence errors is treated as an invalid image. The APNG - loader will not attempt to repair and reorder files containing sequence errors. - -.. _apng-saving: - -Saving -~~~~~~ - -When calling :py:meth:`~PIL.Image.Image.save`, by default only a single frame PNG file -will be saved. To save an APNG file (including a single frame APNG), the ``save_all`` -parameter must be set to ``True``. The following parameters can also be set: - -**default_image** - Boolean value, specifying whether or not the base image is a default image. - If ``True``, the base image will be used as the default image, and the first image - from the ``append_images`` sequence will be the first APNG animation frame. - If ``False``, the base image will be used as the first APNG animation frame. - Defaults to ``False``. - -**append_images** - A list or tuple of images to append as additional frames. Each of the - images in the list can be single or multiframe images. The size of each frame - should match the size of the base image. Also note that if a frame's mode does - not match that of the base image, the frame will be converted to the base image - mode. - -**loop** - Integer number of times to loop this APNG, 0 indicates infinite looping. - Defaults to 0. - -**duration** - Integer (or list or tuple of integers) length of time to display this APNG frame - (in milliseconds). - Defaults to 0. - -**disposal** - An integer (or list or tuple of integers) specifying the APNG disposal - operation to be used for this frame before rendering the next frame. - Defaults to 0. - - * 0 (:py:data:`~PIL.PngImagePlugin.Disposal.OP_NONE`, default) - - No disposal is done on this frame before rendering the next frame. - * 1 (:py:data:`PIL.PngImagePlugin.Disposal.OP_BACKGROUND`) - - This frame's modified region is cleared to fully transparent black before - rendering the next frame. - * 2 (:py:data:`~PIL.PngImagePlugin.Disposal.OP_PREVIOUS`) - - This frame's modified region is reverted to the previous frame's contents before - rendering the next frame. - -**blend** - An integer (or list or tuple of integers) specifying the APNG blend - operation to be used for this frame before rendering the next frame. - Defaults to 0. - - * 0 (:py:data:`~PIL.PngImagePlugin.Blend.OP_SOURCE`) - - All color components of this frame, including alpha, overwrite the previous output - image contents. - * 1 (:py:data:`~PIL.PngImagePlugin.Blend.OP_OVER`) - - This frame should be alpha composited with the previous output image contents. - -.. note:: - - The ``duration``, ``disposal`` and ``blend`` parameters can be set to lists or tuples to - specify values for each individual frame in the animation. The length of the list or tuple - must be identical to the total number of actual frames in the APNG animation. - If the APNG contains a default image (i.e. ``default_image`` is set to ``True``), - these list or tuple parameters should not include an entry for the default image. - - -PPM -^^^ - -Pillow reads and writes PBM, PGM, PPM and PNM files containing ``1``, ``L``, ``I`` or -``RGB`` data. - -"Raw" (P4 to P6) formats can be read, and are used when writing. - -Since Pillow 9.2.0, "plain" (P1 to P3) formats can be read as well. - -SGI -^^^ - -Pillow reads and writes uncompressed ``L``, ``RGB``, and ``RGBA`` files. - - -SPIDER -^^^^^^ - -Pillow reads and writes SPIDER image files of 32-bit floating point data -("F;32F"). - -Pillow also reads SPIDER stack files containing sequences of SPIDER images. The -:py:meth:`~PIL.Image.Image.seek` and :py:meth:`~PIL.Image.Image.tell` methods are supported, and -random access is allowed. - -.. _spider-opening: - -Opening -~~~~~~~ - -The :py:meth:`~PIL.Image.open` method sets the following attributes: - -**format** - Set to ``SPIDER`` - -**istack** - Set to 1 if the file is an image stack, else 0. - -**n_frames** - Set to the number of images in the stack. - -A convenience method, :py:meth:`~PIL.SpiderImagePlugin.SpiderImageFile.convert2byte`, -is provided for converting floating point data to byte data (mode ``L``):: - - im = Image.open("image001.spi").convert2byte() - -.. _spider-saving: - -Saving -~~~~~~ - -The extension of SPIDER files may be any 3 alphanumeric characters. Therefore -the output format must be specified explicitly:: - - im.save('newimage.spi', format='SPIDER') - -For more information about the SPIDER image processing package, see -https://github.com/spider-em/SPIDER - -TGA -^^^ - -Pillow reads and writes TGA images containing ``L``, ``LA``, ``P``, -``RGB``, and ``RGBA`` data. Pillow can read and write both uncompressed and -run-length encoded TGAs. - -.. _tga-saving: - -Saving -~~~~~~ - -The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments: - -**compression** - If set to "tga_rle", the file will be run-length encoded. - - .. versionadded:: 5.3.0 - -**id_section** - The identification field. - - .. versionadded:: 5.3.0 - -**orientation** - If present and a positive number, the first pixel is for the top left corner, - rather than the bottom left corner. - - .. versionadded:: 5.3.0 - -TIFF -^^^^ - -Pillow reads and writes TIFF files. It can read both striped and tiled -images, pixel and plane interleaved multi-band images. If you have -libtiff and its headers installed, Pillow can read and write many kinds -of compressed TIFF files. If not, Pillow will only read and write -uncompressed files. - -.. note:: - - Beginning in version 5.0.0, Pillow requires libtiff to read or - write compressed files. Prior to that release, Pillow had buggy - support for reading Packbits, LZW and JPEG compressed TIFFs - without using libtiff. - -.. _tiff-opening: - -Opening -~~~~~~~ - -The :py:meth:`~PIL.Image.open` method sets the following -:py:attr:`~PIL.Image.Image.info` properties: - -**compression** - Compression mode. - - .. versionadded:: 2.0.0 - -**dpi** - Image resolution as an ``(xdpi, ydpi)`` tuple, where applicable. You can use - the :py:attr:`~PIL.TiffImagePlugin.TiffImageFile.tag` attribute to get more - detailed information about the image resolution. - - .. versionadded:: 1.1.5 - -**resolution** - Image resolution as an ``(xres, yres)`` tuple, where applicable. This is a - measurement in whichever unit is specified by the file. - - .. versionadded:: 1.1.5 - - -The :py:attr:`~PIL.TiffImagePlugin.TiffImageFile.tag_v2` attribute contains a -dictionary of TIFF metadata. The keys are numerical indexes from -:py:data:`.TiffTags.TAGS_V2`. Values are strings or numbers for single -items, multiple values are returned in a tuple of values. Rational -numbers are returned as a :py:class:`~PIL.TiffImagePlugin.IFDRational` -object. - - .. versionadded:: 3.0.0 - -For compatibility with legacy code, the -:py:attr:`~PIL.TiffImagePlugin.TiffImageFile.tag` attribute contains a dictionary -of decoded TIFF fields as returned prior to version 3.0.0. Values are -returned as either strings or tuples of numeric values. Rational -numbers are returned as a tuple of ``(numerator, denominator)``. - - .. deprecated:: 3.0.0 - -Reading Multi-frame TIFF Images -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The TIFF loader supports the :py:meth:`~PIL.Image.Image.seek` and -:py:meth:`~PIL.Image.Image.tell` methods, taking and returning frame numbers -within the image file. You can combine these methods to seek to the next frame -(``im.seek(im.tell() + 1)``). Frames are numbered from 0 to ``im.n_frames - 1``, -and can be accessed in any order. - -``im.seek()`` raises an :py:exc:`EOFError` if you try to seek after the -last frame. - -.. _tiff-saving: - -Saving -~~~~~~ - -The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments: - -**save_all** - If true, 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. - - .. versionadded:: 4.2.0 - -**tiffinfo** - A :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` object or dict - object containing tiff tags and values. The TIFF field type is - autodetected for Numeric and string values, any other types - require using an :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` - object and setting the type in - :py:attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v2.tagtype` with - the appropriate numerical value from - :py:data:`.TiffTags.TYPES`. - - .. versionadded:: 2.3.0 - - Metadata values that are of the rational type should be passed in - using a :py:class:`~PIL.TiffImagePlugin.IFDRational` object. - - .. versionadded:: 3.1.0 - - For compatibility with legacy code, a - :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` object may - be passed in this field. However, this is deprecated. - - .. versionadded:: 5.4.0 - - Previous versions only supported some tags when writing using - libtiff. The supported list is found in - :py:data:`.TiffTags.LIBTIFF_CORE`. - - .. versionadded:: 6.1.0 - - Added support for signed types (e.g. ``TIFF_SIGNED_LONG``) and multiple values. - Multiple values for a single tag must be to - :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` as a tuple and - require a matching type in - :py:attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v2.tagtype` tagtype. - -**exif** - Alternate keyword to "tiffinfo", for consistency with other formats. - - .. versionadded:: 8.4.0 - -**compression** - A string containing the desired compression method for the - file. (valid only with libtiff installed) Valid compression - methods are: :data:`None`, ``"group3"``, ``"group4"``, ``"jpeg"``, ``"lzma"``, - ``"packbits"``, ``"tiff_adobe_deflate"``, ``"tiff_ccitt"``, ``"tiff_lzw"``, - ``"tiff_raw_16"``, ``"tiff_sgilog"``, ``"tiff_sgilog24"``, ``"tiff_thunderscan"``, - ``"webp"``, ``"zstd"`` - -**quality** - The image quality for JPEG compression, on a scale from 0 (worst) to 100 - (best). The default is 75. - - .. versionadded:: 6.1.0 - -These arguments to set the tiff header fields are an alternative to -using the general tags available through tiffinfo. - -**description** - -**software** - -**date_time** - -**artist** - -**copyright** - Strings - -**icc_profile** - The ICC Profile to include in the saved file. - -**resolution_unit** - An integer. 1 for no unit, 2 for inches and 3 for centimeters. - -**resolution** - Either an integer or a float, used for both the x and y resolution. - -**x_resolution** - Either an integer or a float. - -**y_resolution** - Either an integer or a float. - -**dpi** - A tuple of ``(x_resolution, y_resolution)``, with inches as the resolution - unit. For consistency with other image formats, the x and y resolutions - of the dpi will be rounded to the nearest integer. - - -WebP -^^^^ - -Pillow reads and writes WebP files. The specifics of Pillow's capabilities with -this format are currently undocumented. - -.. _webp-saving: - -Saving -~~~~~~ - -The :py:meth:`~PIL.Image.Image.save` method supports the following options: - -**lossless** - If present and true, instructs the WebP writer to use lossless compression. - -**quality** - Integer, 0-100, defaults to 80. For lossy, 0 gives the smallest - size and 100 the largest. For lossless, this parameter is the amount - of effort put into the compression: 0 is the fastest, but gives larger - files compared to the slowest, but best, 100. - -**alpha_quality** - Integer, 0-100, defaults to 100. For lossy compression only. 0 gives the - smallest size and 100 is lossless. - -**method** - Quality/speed trade-off (0=fast, 6=slower-better). Defaults to 4. - -**exact** - If true, preserve the transparent RGB values. Otherwise, discard - invisible RGB values for better compression. Defaults to false. - Requires libwebp 0.5.0 or later. - -**icc_profile** - The ICC Profile to include in the saved file. Only supported if - the system WebP library was built with webpmux support. - -**exif** - The exif data to include in the saved file. Only supported if - the system WebP library was built with webpmux support. - -**xmp** - The XMP data to include in the saved file. Only supported if - the system WebP library was built with webpmux support. - -Saving sequences -~~~~~~~~~~~~~~~~ - -.. note:: - - Support for animated WebP files will only be enabled if the system WebP - library is v0.5.0 or later. You can check webp animation support at - runtime by calling ``features.check("webp_anim")``. - -When calling :py:meth:`~PIL.Image.Image.save` to write a WebP file, by default -only the first frame of a multiframe image will be saved. If the ``save_all`` -argument is present and true, then all frames will be saved, and the following -options will also be available. - -**append_images** - A list of images to append as additional frames. Each of the - 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. - -**loop** - Number of times to repeat the animation. Defaults to [0 = infinite]. - -**background** - Background color of the canvas, as an RGBA tuple with values in - the range of (0-255). - -**minimize_size** - If true, minimize the output size (slow). Implicitly disables - key-frame insertion. - -**kmin, kmax** - Minimum and maximum distance between consecutive key frames in - the output. The library may insert some key frames as needed - to satisfy this criteria. Note that these conditions should - hold: kmax > kmin and kmin >= kmax / 2 + 1. Also, if kmax <= 0, - then key-frame insertion is disabled; and if kmax == 1, then all - frames will be key-frames (kmin value does not matter for these - special cases). - -**allow_mixed** - If true, use mixed compression mode; the encoder heuristically - chooses between lossy and lossless for each frame. - -XBM -^^^ - -Pillow reads and writes X bitmap files (mode ``1``). - -Read-only formats ------------------ - -CUR -^^^ - -CUR is used to store cursors on Windows. The CUR decoder reads the largest -available cursor. Animated cursors are not supported. - -DCX -^^^ - -DCX is a container file format for PCX files, defined by Intel. The DCX format -is commonly used in fax applications. The DCX decoder can read files containing -``1``, ``L``, ``P``, or ``RGB`` data. - -When the file is opened, only the first image is read. You can use -:py:meth:`~PIL.Image.Image.seek` or :py:mod:`~PIL.ImageSequence` to read other images. - -FITS -^^^^ - -.. versionadded:: 9.1.0 - -Pillow identifies and reads FITS files, commonly used for astronomy. Uncompressed and -GZIP_1 compressed images can be read. - -FLI, FLC -^^^^^^^^ - -Pillow reads Autodesk FLI and FLC animations. - -The :py:meth:`~PIL.Image.open` method sets the following -:py:attr:`~PIL.Image.Image.info` properties: - -**duration** - The delay (in milliseconds) between each frame. - -FPX -^^^ - -Pillow reads Kodak FlashPix files. Only the highest resolution image is read from the -file, and the viewing transform is not taken into account. - -To enable FPX support, you must install :pypi:`olefile`. - -.. note:: - - To enable full FlashPix support, you need to build and install the IJG JPEG - library before building the Python Imaging Library. See the distribution - README for details. - -FTEX -^^^^ - -.. versionadded:: 3.2.0 - -The FTEX decoder reads textures used for 3D objects in -Independence War 2: Edge Of Chaos. The plugin reads a single texture -per file, in the compressed and uncompressed formats. - -GBR -^^^ - -The GBR decoder reads GIMP brush files, version 1 and 2. - -.. _gbr-opening: - -Opening -~~~~~~~ - -The :py:meth:`~PIL.Image.open` method sets the following -:py:attr:`~PIL.Image.Image.info` properties: - -**comment** - The brush name. - -**spacing** - The spacing between the brushes, in pixels. Version 2 only. - -GD -^^ - -Pillow reads uncompressed GD2 files. Note that you must use -:py:func:`PIL.GdImageFile.open` to read such a file. - -.. _gd-opening: - -Opening -~~~~~~~ - -The :py:meth:`~PIL.Image.open` method sets the following -:py:attr:`~PIL.Image.Image.info` properties: - -**transparency** - Transparency color index. This key is omitted if the image is not - transparent. - -IMT -^^^ - -Pillow reads Image Tools images containing ``L`` data. - -IPTC/NAA -^^^^^^^^ - -Pillow provides limited read support for IPTC/NAA newsphoto files. - -MCIDAS -^^^^^^ - -Pillow identifies and reads 8-bit McIdas area files. - -MIC -^^^ - -Pillow identifies and reads Microsoft Image Composer (MIC) files. When opened, -the first sprite in the file is loaded. You can use :py:meth:`~PIL.Image.Image.seek` and -:py:meth:`~PIL.Image.Image.tell` to read other sprites from the file. - -Note that there may be an embedded gamma of 2.2 in MIC files. - -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 -^^^ - -Pillow reads PhotoCD files containing ``RGB`` data. This only reads the 768x512 -resolution image from the file. Higher resolutions are encoded in a proprietary -encoding. - -PIXAR -^^^^^ - -Pillow provides limited support for PIXAR raster files. The library can -identify and read “dumped†RGB files. - -The format code is ``PIXAR``. - -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 -^^^ - -Pillow identifies and reads Sun raster files. - -WAL -^^^ - -.. versionadded:: 1.1.4 - -Pillow reads Quake2 WAL texture files. - -Note that this file format cannot be automatically identified, so you must use -the open function in the :py:mod:`~PIL.WalImageFile` module to read files in -this format. - -By default, a Quake2 standard palette is attached to the texture. To override -the palette, use the :py:func:`PIL.Image.Image.putpalette()` method. - -WMF, EMF -^^^^^^^^ - -Pillow can identify WMF and EMF files. - -On Windows, it can read WMF and EMF files. By default, it will load the image -at 72 dpi. To load it at another resolution:: - - from PIL import Image - - with Image.open("drawing.wmf") as im: - im.load(dpi=144) - -To add other read or write support, use -:py:func:`PIL.WmfImagePlugin.register_handler` to register a WMF and EMF -handler. :: - - from PIL import Image - from PIL import WmfImagePlugin - - - class WmfHandler: - def open(self, im): - ... - - def load(self, im): - ... - return image - - def save(self, im, fp, filename): - ... - - - wmf_handler = WmfHandler() - - WmfImagePlugin.register_handler(wmf_handler) - - im = Image.open("sample.wmf") - -XPM -^^^ - -Pillow reads X pixmap files (mode ``P``) with 256 colors or less. - -.. _xpm-opening: - -Opening -~~~~~~~ - -The :py:meth:`~PIL.Image.open` method sets the following -:py:attr:`~PIL.Image.Image.info` properties: - -**transparency** - Transparency color index. This key is omitted if the image is not - transparent. - -Write-only formats ------------------- - -PALM -^^^^ - -Pillow provides write-only support for PALM pixmap files. - -The format code is ``Palm``, the extension is ``.palm``. - -PDF -^^^ - -Pillow can write PDF (Acrobat) images. Such images are written as binary PDF 1.4 -files. Different encoding methods are used, depending on the image mode. - -* 1 mode images are saved using TIFF encoding, or JPEG encoding if libtiff support is - unavailable -* L, RGB and CMYK mode images use JPEG encoding -* P mode images use HEX encoding -* LA and RGBA mode images use JPEG2000 encoding - -.. _pdf-saving: - -Saving -~~~~~~ - -The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments: - -**save_all** - If a multiframe image is used, by default, only the first image will be saved. - To save all frames, each frame to a separate page of the PDF, the ``save_all`` - parameter must be present and set to ``True``. - - .. versionadded:: 3.0.0 - -**append_images** - A list of :py:class:`PIL.Image.Image` objects to append as additional pages. Each - of the images in the list can be single or multiframe images. The ``save_all`` - parameter must be present and set to ``True`` in conjunction with - ``append_images``. - - .. versionadded:: 4.2.0 - -**append** - Set to True to append pages to an existing PDF file. If the file doesn't - exist, an :py:exc:`OSError` will be raised. - - .. versionadded:: 5.1.0 - -**resolution** - Image resolution in DPI. This, together with the number of pixels in the - image, will determine the physical dimensions of the page that will be - saved in the PDF. - -**dpi** - A tuple of ``(x_resolution, y_resolution)``, with inches as the resolution - unit. If both the ``resolution`` parameter and the ``dpi`` parameter are - present, ``resolution`` will be ignored. - -**title** - The document’s title. If not appending to an existing PDF file, this will - default to the filename. - - .. versionadded:: 5.1.0 - -**author** - The name of the person who created the document. - - .. versionadded:: 5.1.0 - -**subject** - The subject of the document. - - .. versionadded:: 5.1.0 - -**keywords** - Keywords associated with the document. - - .. versionadded:: 5.1.0 - -**creator** - If the document was converted to PDF from another format, the name of the - conforming product that created the original document from which it was - converted. - - .. versionadded:: 5.1.0 - -**producer** - If the document was converted to PDF from another format, the name of the - conforming product that converted it to PDF. - - .. versionadded:: 5.1.0 - -**creationDate** - The creation date of the document. If not appending to an existing PDF - file, this will default to the current time. - - .. versionadded:: 5.3.0 - -**modDate** - The modification date of the document. If not appending to an existing PDF - file, this will default to the current time. - - .. versionadded:: 5.3.0 - -XV Thumbnails -^^^^^^^^^^^^^ - -Pillow can read XV thumbnail files. - -Identify-only formats ---------------------- - -BUFR -^^^^ - -.. versionadded:: 1.1.3 - -Pillow provides a stub driver for BUFR files. - -To add read or write support to your application, use -:py:func:`PIL.BufrStubImagePlugin.register_handler`. - -GRIB -^^^^ - -.. versionadded:: 1.1.5 - -Pillow provides a stub driver for GRIB files. - -The driver requires the file to start with a GRIB header. If you have files -with embedded GRIB data, or files with multiple GRIB fields, your application -has to seek to the header before passing the file handle to Pillow. - -To add read or write support to your application, use -:py:func:`PIL.GribStubImagePlugin.register_handler`. - -HDF5 -^^^^ - -.. versionadded:: 1.1.5 - -Pillow provides a stub driver for HDF5 files. - -To add read or write support to your application, use -:py:func:`PIL.Hdf5StubImagePlugin.register_handler`. - -MPEG -^^^^ - -Pillow identifies MPEG files. +.. _image-file-formats: + +Image file formats +================== + +The Python Imaging Library supports a wide variety of raster file formats. +Over 30 different file formats can be identified and read by the library. +Write support is less extensive, but most common interchange and presentation +formats are supported. + +The :py:meth:`~PIL.Image.open` function identifies files from their +contents, not their names, but the :py:meth:`~PIL.Image.Image.save` method +looks at the name to determine which format to use, unless the format is given +explicitly. + +When an image is opened from a file, only that instance of the image is considered to +have the format. Copies of the image will contain data loaded from the file, but not +the file itself, meaning that it can no longer be considered to be in the original +format. So if :py:meth:`~PIL.Image.Image.copy` is called on an image, or another method +internally creates a copy of the image, then any methods or attributes specific to the +format will no longer be present. The ``fp`` (file pointer) attribute will no longer be +present, and the :py:attr:`~PIL.Image.Image.format` attribute will be ``None``. + +Fully supported formats +----------------------- + +BLP +^^^ + +BLP is the Blizzard Mipmap Format, a texture format used in World of +Warcraft. Pillow supports reading ``JPEG`` Compressed or raw ``BLP1`` +images, and all types of ``BLP2`` images. + +Saving +~~~~~~ + +Pillow supports writing BLP images. The :py:meth:`~PIL.Image.Image.save` method +can take the following keyword arguments: + +**blp_version** + If present and set to "BLP1", images will be saved as BLP1. Otherwise, images + will be saved as BLP2. + +BMP +^^^ + +Pillow reads and writes Windows and OS/2 BMP files containing ``1``, ``L``, ``P``, +or ``RGB`` data. 16-colour images are read as ``P`` images. +Support for reading 8-bit run-length encoding was added in Pillow 9.1.0. +Support for reading 4-bit run-length encoding was added in Pillow 9.3.0. + +Opening +~~~~~~~ + +The :py:meth:`~PIL.Image.open` method sets the following +:py:attr:`~PIL.Image.Image.info` properties: + +**compression** + Set to 1 if the file is a 256-color run-length encoded image. + Set to 2 if the file is a 16-color run-length encoded image. + +DDS +^^^ + +DDS is a popular container texture format used in video games and natively supported +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 + ``RGB`` and ``RGBA`` mode. + +.. versionadded:: 6.0.0 + Uncompressed ``RGBA`` images can be read. + + +.. versionadded:: 8.3.0 + BC5S images can be opened in ``RGB`` mode, and uncompressed ``RGB`` images + can be read. Uncompressed data can also be saved to image files. + + +.. versionadded:: 9.3.0 + ATI1 images can be opened in ``L`` mode and ATI2 images can be opened in + ``RGB`` mode. + +.. versionadded:: 9.4.0 + Uncompressed ``L`` ("luminance") and ``LA`` images can be opened and saved. + + +.. versionadded:: 10.1.0 + BC5U can be read in ``RGB`` mode, and 8-bit color indexed images can be read + in ``P`` mode. + + +DIB +^^^ + +Pillow reads and writes DIB files. DIB files are similar to BMP files, so see +above for more information. + + .. versionadded:: 6.0.0 + +EPS +^^^ + +Pillow identifies EPS files containing image data, and can read files that +contain embedded raster images (ImageData descriptors). If Ghostscript is +available, other EPS files can be read as well. The EPS driver can also write +EPS images. The EPS driver can read EPS images in ``L``, ``LAB``, ``RGB`` and +``CMYK`` mode, but Ghostscript may convert the images to ``RGB`` mode rather +than leaving them in the original color space. The EPS driver can write images +in ``L``, ``RGB`` and ``CMYK`` modes. + +Loading +~~~~~~~ + +To use Ghostscript, Pillow searches for the "gs" executable. On Windows, it +also searches for "gswin32c" and "gswin64c". To customise this behaviour, +``EpsImagePlugin.gs_binary = "gswin64"`` will set the name of the executable to +use. ``EpsImagePlugin.gs_binary = False`` will prevent Ghostscript use. + +If Ghostscript is available, you can call the :py:meth:`~PIL.Image.Image.load` +method with the following parameters to affect how Ghostscript renders the EPS. + +**scale** + Affects the scale of the resultant rasterized image. If the EPS suggests + that the image be rendered at 100px x 100px, setting this parameter to + 2 will make the Ghostscript render a 200px x 200px image instead. The + relative position of the bounding box is maintained:: + + im = Image.open(...) + im.size # (100,100) + im.load(scale=2) + im.size # (200,200) + +**transparency** + If true, generates an RGBA image with a transparent background, instead of + the default behaviour of an RGB image with a white background. + + +GIF +^^^ + +Pillow reads GIF87a and GIF89a versions of the GIF file format. The library +writes files in GIF87a by default, unless GIF89a features are used or GIF89a is +already in use. Files are written with LZW encoding. + +GIF files are initially read as grayscale (``L``) or palette mode (``P``) +images. Seeking to later frames in a ``P`` image will change the image to +``RGB`` (or ``RGBA`` if the first frame had transparency). + +``P`` mode images are changed to ``RGB`` because each frame of a GIF may contain +its own individual palette of up to 256 colors. When a new frame is placed onto a +previous frame, those colors may combine to exceed the ``P`` mode limit of 256 +colors. Instead, the image is converted to ``RGB`` handle this. + +If you would prefer the first ``P`` image frame to be ``RGB`` as well, so that +every ``P`` frame is converted to ``RGB`` or ``RGBA`` mode, there is a setting +available:: + + from PIL import GifImagePlugin + GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS + +GIF frames do not always contain individual palettes however. If there is only +a global palette, then all of the colors can fit within ``P`` mode. If you would +prefer the frames to be kept as ``P`` in that case, there is also a setting +available:: + + from PIL import GifImagePlugin + GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY + +To restore the default behavior, where ``P`` mode images are only converted to +``RGB`` or ``RGBA`` after the first frame:: + + from PIL import GifImagePlugin + GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST + +.. _gif-opening: + +Opening +~~~~~~~ + +The :py:meth:`~PIL.Image.open` method sets the following +:py:attr:`~PIL.Image.Image.info` properties: + +**background** + Default background color (a palette color index). + +**transparency** + Transparency color index. This key is omitted if the image is not + transparent. + +**version** + Version (either ``GIF87a`` or ``GIF89a``). + +**duration** + May not be present. The time to display the current frame + of the GIF, in milliseconds. + +**loop** + May not be present. The number of times the GIF should loop. 0 means that + it will loop forever. + +**comment** + May not be present. A comment about the image. This is the last comment found + before the current frame's image. + +**extension** + May not be present. Contains application specific information. + +Reading sequences +~~~~~~~~~~~~~~~~~ + +The GIF loader supports the :py:meth:`~PIL.Image.Image.seek` and +:py:meth:`~PIL.Image.Image.tell` methods. You can combine these methods +to seek to the next frame (``im.seek(im.tell() + 1)``). + +``im.seek()`` raises an :py:exc:`EOFError` if you try to seek after the last frame. + +.. _gif-saving: + +Saving +~~~~~~ + +When calling :py:meth:`~PIL.Image.Image.save` to write a GIF file, the +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. + +**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. + + 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. + +**include_color_table** + Whether or not to include local color table. + +**interlace** + Whether or not the image is interlaced. By default, it is, unless the image + is less than 16 pixels in width or height. + +**disposal** + Indicates the way in which the graphic is to be treated after being displayed. + + * 0 - No disposal specified. + * 1 - Do not dispose. + * 2 - Restore to background color. + * 3 - Restore to previous content. + + Pass a single integer for a constant disposal, or a list or tuple + to set the disposal for each frame separately. + +**palette** + Use the specified palette for the saved image. The palette should + be a bytes or bytearray object containing the palette entries in + RGBRGB... form. It should be no more than 768 bytes. Alternately, + the palette can be passed in as an + :py:class:`PIL.ImagePalette.ImagePalette` object. + +**optimize** + Whether to attempt to compress the palette by eliminating unused colors + (this is only useful if the palette can be compressed to the next smaller + power of 2 elements) and whether to mark all pixels that are not new in the + next frame as transparent. + + This is attempted by default, unless a palette is specified as an option or + as part of the first image's :py:attr:`~PIL.Image.Image.info` dictionary. + +Note that if the image you are saving comes from an existing GIF, it may have +the following properties in its :py:attr:`~PIL.Image.Image.info` dictionary. +For these options, if you do not pass them in, they will default to +their :py:attr:`~PIL.Image.Image.info` values. + +**transparency** + Transparency color index. + +**duration** + The display duration of each frame of the multiframe gif, in + milliseconds. Pass a single integer for a constant duration, or a + list or tuple to set the duration for each frame separately. + +**loop** + Integer number of times the GIF should loop. 0 means that it will loop + forever. If omitted or ``None``, the image will not loop. + +**comment** + A comment about the image. + +Reading local images +~~~~~~~~~~~~~~~~~~~~ + +The GIF loader creates an image memory the same size as the GIF file’s *logical +screen size*, and pastes the actual pixel data (the *local image*) into this +image. If you only want the actual pixel rectangle, you can crop the image:: + + im = Image.open(...) + + if im.tile[0][0] == "gif": + # only read the first "local image" from this GIF file + box = im.tile[0][1] + im = im.crop(box) + +ICNS +^^^^ + +Pillow reads and writes macOS ``.icns`` files. By default, the +largest available icon is read, though you can override this by setting the +:py:attr:`~PIL.Image.Image.size` property before calling +:py:meth:`~PIL.Image.Image.load`. The :py:meth:`~PIL.Image.open` method +sets the following :py:attr:`~PIL.Image.Image.info` property: + +.. note:: + + Prior to version 8.3.0, Pillow could only write ICNS files on macOS. + +**sizes** + A list of supported sizes found in this icon file; these are a + 3-tuple, ``(width, height, scale)``, where ``scale`` is 2 for a retina + icon and 1 for a standard icon. You *are* permitted to use this 3-tuple + format for the :py:attr:`~PIL.Image.Image.size` property if you set it + before calling :py:meth:`~PIL.Image.Image.load`; after loading, the size + will be reset to a 2-tuple containing pixel dimensions (so, e.g. if you + ask for ``(512, 512, 2)``, the final value of + :py:attr:`~PIL.Image.Image.size` will be ``(1024, 1024)``). + +.. _icns-saving: + +Saving +~~~~~~ + +The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments: + +**append_images** + A list of images to replace the scaled down versions of the image. + The order of the images does not matter, as their use is determined by + the size of each image. + + .. versionadded:: 5.1.0 + +ICO +^^^ + +ICO is used to store icons on Windows. The largest available icon is read. + +.. _ico-saving: + +Saving +~~~~~~ + +The :py:meth:`~PIL.Image.Image.save` method supports the following options: + +**sizes** + A list of sizes including in this ico file; these are a 2-tuple, + ``(width, height)``; Default to ``[(16, 16), (24, 24), (32, 32), (48, 48), + (64, 64), (128, 128), (256, 256)]``. Any sizes bigger than the original + size or 256 will be ignored. + +The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments: + +**append_images** + A list of images to replace the scaled down versions of the image. + The order of the images does not matter, as their use is determined by + the size of each image. + + .. versionadded:: 8.1.0 + +**bitmap_format** + By default, the image data will be saved in PNG format. With a bitmap format of + "bmp", image data will be saved in BMP format instead. + + .. versionadded:: 8.3.0 + +IM +^^ + +IM is a format used by LabEye and other applications based on the IFUNC image +processing library. The library reads and writes most uncompressed interchange +versions of this format. + +IM is the only format that can store all internal Pillow formats. + +JPEG +^^^^ + +Pillow reads JPEG, JFIF, and Adobe JPEG files containing ``L``, ``RGB``, or +``CMYK`` data. It writes standard and progressive JFIF files. + +Using the :py:meth:`~PIL.Image.Image.draft` method, you can speed things up by +converting ``RGB`` images to ``L``, and resize images to 1/2, 1/4 or 1/8 of +their original size while loading them. + +By default Pillow doesn't allow loading of truncated JPEG files, set +:data:`.ImageFile.LOAD_TRUNCATED_IMAGES` to override this. + +.. _jpeg-opening: + +Opening +~~~~~~~ + +The :py:meth:`~PIL.Image.open` method may set the following +:py:attr:`~PIL.Image.Image.info` properties if available: + +**jfif** + JFIF application marker found. If the file is not a JFIF file, this key is + not present. + +**jfif_version** + A tuple representing the jfif version, (major version, minor version). + +**jfif_density** + A tuple representing the pixel density of the image, in units specified + by jfif_unit. + +**jfif_unit** + Units for the jfif_density: + + * 0 - No Units + * 1 - Pixels per Inch + * 2 - Pixels per Centimeter + +**dpi** + A tuple representing the reported pixel density in pixels per inch, if + the file is a jfif file and the units are in inches. + +**adobe** + Adobe application marker found. If the file is not an Adobe JPEG file, this + key is not present. + +**adobe_transform** + Vendor Specific Tag. + +**progression** + Indicates that this is a progressive JPEG file. + +**icc_profile** + The ICC color profile for the image. + +**exif** + Raw EXIF data from the image. + +**comment** + A comment about the image. + + .. versionadded:: 7.1.0 + +.. _jpeg-saving: + +Saving +~~~~~~ + +The :py:meth:`~PIL.Image.Image.save` method supports the following options: + +**quality** + The image quality, on a scale from 0 (worst) to 95 (best), or the string + ``keep``. The default is 75. Values above 95 should be avoided; 100 disables + portions of the JPEG compression algorithm, and results in large files with + hardly any gain in image quality. The value ``keep`` is only valid for JPEG + files and will retain the original image quality level, subsampling, and + qtables. + +**optimize** + If present and true, indicates that the encoder should make an extra pass + over the image in order to select optimal encoder settings. + +**progressive** + If present and true, indicates that this image should be stored as a + progressive JPEG file. + +**dpi** + A tuple of integers representing the pixel density, ``(x,y)``. + +**icc_profile** + If present and true, the image is stored with the provided ICC profile. + If this parameter is not provided, the image will be saved with no profile + attached. To preserve the existing profile:: + + im.save(filename, 'jpeg', icc_profile=im.info.get('icc_profile')) + +**exif** + If present, the image will be stored with the provided raw EXIF data. + +**keep_rgb** + By default, libjpeg converts images with an RGB color space to YCbCr. + If this option is present and true, those images will be stored as RGB + instead. + + When this option is enabled, attempting to chroma-subsample RGB images + with the ``subsampling`` option will raise an :py:exc:`OSError`. + + .. versionadded:: 10.2.0 + +**subsampling** + If present, sets the subsampling for the encoder. + + * ``keep``: Only valid for JPEG files, will retain the original image setting. + * ``4:4:4``, ``4:2:2``, ``4:2:0``: Specific sampling values + * ``0``: equivalent to ``4:4:4`` + * ``1``: equivalent to ``4:2:2`` + * ``2``: equivalent to ``4:2:0`` + + If absent, the setting will be determined by libjpeg or libjpeg-turbo. + +**restart_marker_blocks** + If present, emit a restart marker whenever the specified number of MCU + blocks has been produced. + + .. versionadded:: 10.2.0 + +**restart_marker_rows** + If present, emit a restart marker whenever the specified number of MCU + rows has been produced. + + .. versionadded:: 10.2.0 + +**qtables** + If present, sets the qtables for the encoder. This is listed as an + advanced option for wizards in the JPEG documentation. Use with + caution. ``qtables`` can be one of several types of values: + + * a string, naming a preset, e.g. ``keep``, ``web_low``, or ``web_high`` + * a list, tuple, or dictionary (with integer keys = + range(len(keys))) of lists of 64 integers. There must be + between 2 and 4 tables. + + .. versionadded:: 2.5.0 + +**streamtype** + Allows storing images without quantization and Huffman tables, or with + these tables but without image data. This is useful for container formats + or network protocols that handle tables separately and share them between + images. + + * ``0`` (default): interchange datastream, with tables and image data + * ``1``: abbreviated table specification (tables-only) datastream + + .. versionadded:: 10.2.0 + + * ``2``: abbreviated image (image-only) datastream + +**comment** + A comment about the image. + + .. versionadded:: 9.4.0 + + +.. note:: + + To enable JPEG support, you need to build and install the IJG JPEG library + before building the Python Imaging Library. See the distribution README for + details. + +JPEG 2000 +^^^^^^^^^ + +.. versionadded:: 2.4.0 + +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). + +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 +convert the image to either ``RGB`` or ``RGBA`` rather than choosing for +itself. It is also possible to set ``reduce`` to the number of resolutions to +discard (each one reduces the size of the resulting image by a factor of 2), +and ``layers`` to specify the number of quality layers to load. + +.. _jpeg-2000-saving: + +Saving +~~~~~~ + +The :py:meth:`~PIL.Image.Image.save` method supports the following options: + +**offset** + The image offset, as a tuple of integers, e.g. (16, 16) + +**tile_offset** + The tile offset, again as a 2-tuple of integers. + +**tile_size** + The tile size as a 2-tuple. If not specified, or if set to None, the + image will be saved without tiling. + +**quality_mode** + Either ``"rates"`` or ``"dB"`` depending on the units you want to use to + specify image quality. + +**quality_layers** + A sequence of numbers, each of which represents either an approximate size + reduction (if quality mode is ``"rates"``) or a signal to noise ratio value + in decibels. If not specified, defaults to a single layer of full quality. + +**num_resolutions** + The number of different image resolutions to be stored (which corresponds + to the number of Discrete Wavelet Transform decompositions plus one). + +**codeblock_size** + The code-block size as a 2-tuple. Minimum size is 4 x 4, maximum is 1024 x + 1024, with the additional restriction that no code-block may have more + than 4096 coefficients (i.e. the product of the two numbers must be no + greater than 4096). + +**precinct_size** + The precinct size as a 2-tuple. Must be a power of two along both axes, + and must be greater than the code-block size. + +**irreversible** + If ``True``, use the lossy discrete waveform transformation DWT 9-7. + Defaults to ``False``, which uses the lossless DWT 5-3. + +**mct** + If ``1`` then enable multiple component transformation when encoding, + otherwise use ``0`` for no component transformation (default). If MCT is + enabled and ``irreversible`` is ``True`` then the Irreversible Color + Transformation will be applied, otherwise encoding will use the + Reversible Color Transformation. MCT works best with a ``mode`` of + ``RGB`` and is only applicable when the image data has 3 components. + + .. versionadded:: 9.1.0 + +**progression** + Controls the progression order; must be one of ``"LRCP"``, ``"RLCP"``, + ``"RPCL"``, ``"PCRL"``, ``"CPRL"``. The letters stand for Component, + Position, Resolution and Layer respectively and control the order of + encoding, the idea being that e.g. an image encoded using LRCP mode can + have its quality layers decoded as they arrive at the decoder, while one + encoded using RLCP mode will have increasing resolutions decoded as they + arrive, and so on. + +**signed** + If true, then tell the encoder to save the image as signed. + + .. versionadded:: 9.4.0 + +**cinema_mode** + Set the encoder to produce output compliant with the digital cinema + specifications. The options here are ``"no"`` (the default), + ``"cinema2k-24"`` for 24fps 2K, ``"cinema2k-48"`` for 48fps 2K, and + ``"cinema4k-24"`` for 24fps 4K. Note that for compliant 2K files, + *at least one* of your image dimensions must match 2048 x 1080, while + for compliant 4K files, *at least one* of the dimensions must match + 4096 x 2160. + +**no_jp2** + If ``True`` then don't wrap the raw codestream in the JP2 file format when + saving, otherwise the extension of the filename will be used to determine + the format (default). + + .. versionadded:: 9.1.0 + +**comment** + Adds a custom comment to the file, replacing the default + "Created by OpenJPEG version" comment. + + .. versionadded:: 9.5.0 + +**plt** + If ``True`` and OpenJPEG 2.4.0 or later is available, then include a PLT + (packet length, tile-part header) marker in the produced file. + Defaults to ``False``. + + .. versionadded:: 9.5.0 + +.. note:: + + To enable JPEG 2000 support, you need to build and install the OpenJPEG + library, version 2.0.0 or higher, before building the Python Imaging + Library. + + Windows users can install the OpenJPEG binaries available on the + OpenJPEG website, but must add them to their PATH in order to use Pillow (if + you fail to do this, you will get errors about not being able to load the + ``_imaging`` DLL). + +MSP +^^^ + +Pillow identifies and reads MSP files from Windows 1 and 2. The library writes +uncompressed (Windows 1) versions of this format. + +PCX +^^^ + +Pillow reads and writes PCX files containing ``1``, ``L``, ``P``, or ``RGB`` data. + +PFM +^^^ + +.. versionadded:: 10.3.0 + +Pillow reads and writes grayscale (Pf format) Portable FloatMap (PFM) files +containing ``F`` data. + +Color (PF format) PFM files are not supported. + +Opening +~~~~~~~ + +The :py:func:`~PIL.Image.open` function sets the following +:py:attr:`~PIL.Image.Image.info` properties: + +**scale** + The absolute value of the number stored in the *Scale Factor / Endianness* line. + +PNG +^^^ + +Pillow identifies, reads, and writes PNG files containing ``1``, ``L``, ``LA``, +``I``, ``P``, ``RGB`` or ``RGBA`` data. Interlaced files are supported as of +v1.1.7. + +As of Pillow 6.0, EXIF data can be read from PNG images. However, unlike other +image formats, EXIF data is not guaranteed to be present in +:py:attr:`~PIL.Image.Image.info` until :py:meth:`~PIL.Image.Image.load` has been +called. + +By default Pillow doesn't allow loading of truncated PNG files, set +:data:`.ImageFile.LOAD_TRUNCATED_IMAGES` to override this. + +.. _png-opening: + +Opening +~~~~~~~ + +The :py:func:`~PIL.Image.open` function sets the following +:py:attr:`~PIL.Image.Image.info` properties, when appropriate: + +**chromaticity** + The chromaticity points, as an 8 tuple of floats. (``White Point + X``, ``White Point Y``, ``Red X``, ``Red Y``, ``Green X``, ``Green + Y``, ``Blue X``, ``Blue Y``) + +**gamma** + Gamma, given as a floating point number. + +**srgb** + The sRGB rendering intent as an integer. + + * 0 Perceptual + * 1 Relative Colorimetric + * 2 Saturation + * 3 Absolute Colorimetric + +**transparency** + For ``P`` images: Either the palette index for full transparent pixels, + or a byte string with alpha values for each palette entry. + + For ``1``, ``L``, ``I`` and ``RGB`` images, the color that represents + full transparent pixels in this image. + + This key is omitted if the image is not a transparent palette image. + +.. _png-text: + +``open`` also sets ``Image.text`` to a dictionary of the values of the +``tEXt``, ``zTXt``, and ``iTXt`` chunks of the PNG image. Individual +compressed chunks are limited to a decompressed size of +:data:`.PngImagePlugin.MAX_TEXT_CHUNK`, by default 1MB, to prevent +decompression bombs. Additionally, the total size of all of the text +chunks is limited to :data:`.PngImagePlugin.MAX_TEXT_MEMORY`, defaulting to +64MB. + +.. _png-saving: + +Saving +~~~~~~ + +The :py:meth:`~PIL.Image.Image.save` method supports the following options: + +**optimize** + If present and true, instructs the PNG writer to make the output file as + small as possible. This includes extra processing in order to find optimal + encoder settings. + +**transparency** + For ``P``, ``1``, ``L``, ``I``, and ``RGB`` images, this option controls + what color from the image to mark as transparent. + + For ``P`` images, this can be a either the palette index, + or a byte string with alpha values for each palette entry. + +**dpi** + A tuple of two numbers corresponding to the desired dpi in each direction. + +**pnginfo** + A :py:class:`PIL.PngImagePlugin.PngInfo` instance containing chunks. + +**compress_level** + ZLIB compression level, a number between 0 and 9: 1 gives best speed, + 9 gives best compression, 0 gives no compression at all. Default is 6. + When ``optimize`` option is True ``compress_level`` has no effect + (it is set to 9 regardless of a value passed). + +**icc_profile** + The ICC Profile to include in the saved file. + +**exif** + The exif data to include in the saved file. + + .. versionadded:: 6.0.0 + +**bits (experimental)** + For ``P`` images, this option controls how many bits to store. If omitted, + the PNG writer uses 8 bits (256 colors). + +**dictionary (experimental)** + Set the ZLIB encoder dictionary. + +.. note:: + + To enable PNG support, you need to build and install the ZLIB compression + library before building the Python Imaging Library. See the + :doc:`installation documentation <../installation>` for details. + +.. _apng-sequences: + +APNG sequences +~~~~~~~~~~~~~~ + +The PNG loader includes limited support for reading and writing Animated Portable +Network Graphics (APNG) files. +When an APNG file is loaded, :py:meth:`~PIL.ImageFile.ImageFile.get_format_mimetype` +will return ``"image/apng"``. The value of the :py:attr:`~PIL.Image.Image.is_animated` +property will be ``True`` when the :py:attr:`~PIL.Image.Image.n_frames` property is +greater than 1. For APNG files, the ``n_frames`` property depends on both the animation +frame count as well as the presence or absence of a default image. See the +``default_image`` property documentation below for more details. +The :py:meth:`~PIL.Image.Image.seek` and :py:meth:`~PIL.Image.Image.tell` methods +are supported. + +``im.seek()`` raises an :py:exc:`EOFError` if you try to seek after the last frame. + +These :py:attr:`~PIL.Image.Image.info` properties will be set for APNG frames, +where applicable: + +**default_image** + Specifies whether or not this APNG file contains a separate default image, + which is not a part of the actual APNG animation. + + When an APNG file contains a default image, the initially loaded image (i.e. + the result of ``seek(0)``) will be the default image. + To account for the presence of the default image, the + :py:attr:`~PIL.Image.Image.n_frames` property will be set to ``frame_count + 1``, + where ``frame_count`` is the actual APNG animation frame count. + To load the first APNG animation frame, ``seek(1)`` must be called. + + * ``True`` - The APNG contains default image, which is not an animation frame. + * ``False`` - The APNG does not contain a default image. The ``n_frames`` property + will be set to the actual APNG animation frame count. + The initially loaded image (i.e. ``seek(0)``) will be the first APNG animation + frame. + +**loop** + The number of times to loop this APNG, 0 indicates infinite looping. + +**duration** + The time to display this APNG frame (in milliseconds). + +.. note:: + + The APNG loader returns images the same size as the APNG file's logical screen size. + The returned image contains the pixel data for a given frame, after applying + any APNG frame disposal and frame blend operations (i.e. it contains what a web + browser would render for this frame - the composite of all previous frames and this + frame). + + Any APNG file containing sequence errors is treated as an invalid image. The APNG + loader will not attempt to repair and reorder files containing sequence errors. + +.. _apng-saving: + +Saving +~~~~~~ + +When calling :py:meth:`~PIL.Image.Image.save`, by default only a single frame PNG file +will be saved. To save an APNG file (including a single frame APNG), the ``save_all`` +parameter must be set to ``True``. The following parameters can also be set: + +**default_image** + Boolean value, specifying whether or not the base image is a default image. + If ``True``, the base image will be used as the default image, and the first image + from the ``append_images`` sequence will be the first APNG animation frame. + If ``False``, the base image will be used as the first APNG animation frame. + Defaults to ``False``. + +**append_images** + A list or tuple of images to append as additional frames. Each of the + images in the list can be single or multiframe images. The size of each frame + should match the size of the base image. Also note that if a frame's mode does + not match that of the base image, the frame will be converted to the base image + mode. + +**loop** + Integer number of times to loop this APNG, 0 indicates infinite looping. + Defaults to 0. + +**duration** + Integer (or list or tuple of integers) length of time to display this APNG frame + (in milliseconds). + Defaults to 0. + +**disposal** + An integer (or list or tuple of integers) specifying the APNG disposal + operation to be used for this frame before rendering the next frame. + Defaults to 0. + + * 0 (:py:data:`~PIL.PngImagePlugin.Disposal.OP_NONE`, default) - + No disposal is done on this frame before rendering the next frame. + * 1 (:py:data:`PIL.PngImagePlugin.Disposal.OP_BACKGROUND`) - + This frame's modified region is cleared to fully transparent black before + rendering the next frame. + * 2 (:py:data:`~PIL.PngImagePlugin.Disposal.OP_PREVIOUS`) - + This frame's modified region is reverted to the previous frame's contents before + rendering the next frame. + +**blend** + An integer (or list or tuple of integers) specifying the APNG blend + operation to be used for this frame before rendering the next frame. + Defaults to 0. + + * 0 (:py:data:`~PIL.PngImagePlugin.Blend.OP_SOURCE`) - + All color components of this frame, including alpha, overwrite the previous output + image contents. + * 1 (:py:data:`~PIL.PngImagePlugin.Blend.OP_OVER`) - + This frame should be alpha composited with the previous output image contents. + +.. note:: + + The ``duration``, ``disposal`` and ``blend`` parameters can be set to lists or tuples to + specify values for each individual frame in the animation. The length of the list or tuple + must be identical to the total number of actual frames in the APNG animation. + If the APNG contains a default image (i.e. ``default_image`` is set to ``True``), + these list or tuple parameters should not include an entry for the default image. + + +PPM +^^^ + +Pillow reads and writes PBM, PGM, PPM and PNM files containing ``1``, ``L``, ``I`` or +``RGB`` data. + +"Raw" (P4 to P6) formats can be read, and are used when writing. + +Since Pillow 9.2.0, "plain" (P1 to P3) formats can be read as well. + +SGI +^^^ + +Pillow reads and writes uncompressed ``L``, ``RGB``, and ``RGBA`` files. + + +SPIDER +^^^^^^ + +Pillow reads and writes SPIDER image files of 32-bit floating point data +("F;32F"). + +Pillow also reads SPIDER stack files containing sequences of SPIDER images. The +:py:meth:`~PIL.Image.Image.seek` and :py:meth:`~PIL.Image.Image.tell` methods are supported, and +random access is allowed. + +.. _spider-opening: + +Opening +~~~~~~~ + +The :py:meth:`~PIL.Image.open` method sets the following attributes: + +**format** + Set to ``SPIDER`` + +**istack** + Set to 1 if the file is an image stack, else 0. + +**n_frames** + Set to the number of images in the stack. + +A convenience method, :py:meth:`~PIL.SpiderImagePlugin.SpiderImageFile.convert2byte`, +is provided for converting floating point data to byte data (mode ``L``):: + + im = Image.open("image001.spi").convert2byte() + +.. _spider-saving: + +Saving +~~~~~~ + +The extension of SPIDER files may be any 3 alphanumeric characters. Therefore +the output format must be specified explicitly:: + + im.save('newimage.spi', format='SPIDER') + +For more information about the SPIDER image processing package, see +https://github.com/spider-em/SPIDER + +TGA +^^^ + +Pillow reads and writes TGA images containing ``L``, ``LA``, ``P``, +``RGB``, and ``RGBA`` data. Pillow can read and write both uncompressed and +run-length encoded TGAs. + +.. _tga-saving: + +Saving +~~~~~~ + +The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments: + +**compression** + If set to "tga_rle", the file will be run-length encoded. + + .. versionadded:: 5.3.0 + +**id_section** + The identification field. + + .. versionadded:: 5.3.0 + +**orientation** + If present and a positive number, the first pixel is for the top left corner, + rather than the bottom left corner. + + .. versionadded:: 5.3.0 + +TIFF +^^^^ + +Pillow reads and writes TIFF files. It can read both striped and tiled +images, pixel and plane interleaved multi-band images. If you have +libtiff and its headers installed, Pillow can read and write many kinds +of compressed TIFF files. If not, Pillow will only read and write +uncompressed files. + +.. note:: + + Beginning in version 5.0.0, Pillow requires libtiff to read or + write compressed files. Prior to that release, Pillow had buggy + support for reading Packbits, LZW and JPEG compressed TIFFs + without using libtiff. + +.. _tiff-opening: + +Opening +~~~~~~~ + +The :py:meth:`~PIL.Image.open` method sets the following +:py:attr:`~PIL.Image.Image.info` properties: + +**compression** + Compression mode. + + .. versionadded:: 2.0.0 + +**dpi** + Image resolution as an ``(xdpi, ydpi)`` tuple, where applicable. You can use + the :py:attr:`~PIL.TiffImagePlugin.TiffImageFile.tag` attribute to get more + detailed information about the image resolution. + + .. versionadded:: 1.1.5 + +**resolution** + Image resolution as an ``(xres, yres)`` tuple, where applicable. This is a + measurement in whichever unit is specified by the file. + + .. versionadded:: 1.1.5 + + +The :py:attr:`~PIL.TiffImagePlugin.TiffImageFile.tag_v2` attribute contains a +dictionary of TIFF metadata. The keys are numerical indexes from +:py:data:`.TiffTags.TAGS_V2`. Values are strings or numbers for single +items, multiple values are returned in a tuple of values. Rational +numbers are returned as a :py:class:`~PIL.TiffImagePlugin.IFDRational` +object. + + .. versionadded:: 3.0.0 + +For compatibility with legacy code, the +:py:attr:`~PIL.TiffImagePlugin.TiffImageFile.tag` attribute contains a dictionary +of decoded TIFF fields as returned prior to version 3.0.0. Values are +returned as either strings or tuples of numeric values. Rational +numbers are returned as a tuple of ``(numerator, denominator)``. + + .. deprecated:: 3.0.0 + +Reading Multi-frame TIFF Images +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The TIFF loader supports the :py:meth:`~PIL.Image.Image.seek` and +:py:meth:`~PIL.Image.Image.tell` methods, taking and returning frame numbers +within the image file. You can combine these methods to seek to the next frame +(``im.seek(im.tell() + 1)``). Frames are numbered from 0 to ``im.n_frames - 1``, +and can be accessed in any order. + +``im.seek()`` raises an :py:exc:`EOFError` if you try to seek after the +last frame. + +.. _tiff-saving: + +Saving +~~~~~~ + +The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments: + +**save_all** + If true, 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. + + .. versionadded:: 4.2.0 + +**tiffinfo** + A :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` object or dict + object containing tiff tags and values. The TIFF field type is + autodetected for Numeric and string values, any other types + require using an :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` + object and setting the type in + :py:attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v2.tagtype` with + the appropriate numerical value from + :py:data:`.TiffTags.TYPES`. + + .. versionadded:: 2.3.0 + + Metadata values that are of the rational type should be passed in + using a :py:class:`~PIL.TiffImagePlugin.IFDRational` object. + + .. versionadded:: 3.1.0 + + For compatibility with legacy code, a + :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` object may + be passed in this field. However, this is deprecated. + + .. versionadded:: 5.4.0 + + Previous versions only supported some tags when writing using + libtiff. The supported list is found in + :py:data:`.TiffTags.LIBTIFF_CORE`. + + .. versionadded:: 6.1.0 + + Added support for signed types (e.g. ``TIFF_SIGNED_LONG``) and multiple values. + Multiple values for a single tag must be to + :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` as a tuple and + require a matching type in + :py:attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v2.tagtype` tagtype. + +**exif** + Alternate keyword to "tiffinfo", for consistency with other formats. + + .. versionadded:: 8.4.0 + +**compression** + A string containing the desired compression method for the + file. (valid only with libtiff installed) Valid compression + methods are: :data:`None`, ``"group3"``, ``"group4"``, ``"jpeg"``, ``"lzma"``, + ``"packbits"``, ``"tiff_adobe_deflate"``, ``"tiff_ccitt"``, ``"tiff_lzw"``, + ``"tiff_raw_16"``, ``"tiff_sgilog"``, ``"tiff_sgilog24"``, ``"tiff_thunderscan"``, + ``"webp"``, ``"zstd"`` + +**quality** + The image quality for JPEG compression, on a scale from 0 (worst) to 100 + (best). The default is 75. + + .. versionadded:: 6.1.0 + +These arguments to set the tiff header fields are an alternative to +using the general tags available through tiffinfo. + +**description** + +**software** + +**date_time** + +**artist** + +**copyright** + Strings + +**icc_profile** + The ICC Profile to include in the saved file. + +**resolution_unit** + An integer. 1 for no unit, 2 for inches and 3 for centimeters. + +**resolution** + Either an integer or a float, used for both the x and y resolution. + +**x_resolution** + Either an integer or a float. + +**y_resolution** + Either an integer or a float. + +**dpi** + A tuple of ``(x_resolution, y_resolution)``, with inches as the resolution + unit. For consistency with other image formats, the x and y resolutions + of the dpi will be rounded to the nearest integer. + + +WebP +^^^^ + +Pillow reads and writes WebP files. The specifics of Pillow's capabilities with +this format are currently undocumented. + +.. _webp-saving: + +Saving +~~~~~~ + +The :py:meth:`~PIL.Image.Image.save` method supports the following options: + +**lossless** + If present and true, instructs the WebP writer to use lossless compression. + +**quality** + Integer, 0-100, defaults to 80. For lossy, 0 gives the smallest + size and 100 the largest. For lossless, this parameter is the amount + of effort put into the compression: 0 is the fastest, but gives larger + files compared to the slowest, but best, 100. + +**alpha_quality** + Integer, 0-100, defaults to 100. For lossy compression only. 0 gives the + smallest size and 100 is lossless. + +**method** + Quality/speed trade-off (0=fast, 6=slower-better). Defaults to 4. + +**exact** + If true, preserve the transparent RGB values. Otherwise, discard + invisible RGB values for better compression. Defaults to false. + Requires libwebp 0.5.0 or later. + +**icc_profile** + The ICC Profile to include in the saved file. Only supported if + the system WebP library was built with webpmux support. + +**exif** + The exif data to include in the saved file. Only supported if + the system WebP library was built with webpmux support. + +**xmp** + The XMP data to include in the saved file. Only supported if + the system WebP library was built with webpmux support. + +Saving sequences +~~~~~~~~~~~~~~~~ + +.. note:: + + Support for animated WebP files will only be enabled if the system WebP + library is v0.5.0 or later. You can check webp animation support at + runtime by calling ``features.check("webp_anim")``. + +When calling :py:meth:`~PIL.Image.Image.save` to write a WebP file, by default +only the first frame of a multiframe image will be saved. If the ``save_all`` +argument is present and true, then all frames will be saved, and the following +options will also be available. + +**append_images** + A list of images to append as additional frames. Each of the + 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. + +**loop** + Number of times to repeat the animation. Defaults to [0 = infinite]. + +**background** + Background color of the canvas, as an RGBA tuple with values in + the range of (0-255). + +**minimize_size** + If true, minimize the output size (slow). Implicitly disables + key-frame insertion. + +**kmin, kmax** + Minimum and maximum distance between consecutive key frames in + the output. The library may insert some key frames as needed + to satisfy this criteria. Note that these conditions should + hold: kmax > kmin and kmin >= kmax / 2 + 1. Also, if kmax <= 0, + then key-frame insertion is disabled; and if kmax == 1, then all + frames will be key-frames (kmin value does not matter for these + special cases). + +**allow_mixed** + If true, use mixed compression mode; the encoder heuristically + chooses between lossy and lossless for each frame. + +XBM +^^^ + +Pillow reads and writes X bitmap files (mode ``1``). + +Read-only formats +----------------- + +CUR +^^^ + +CUR is used to store cursors on Windows. The CUR decoder reads the largest +available cursor. Animated cursors are not supported. + +DCX +^^^ + +DCX is a container file format for PCX files, defined by Intel. The DCX format +is commonly used in fax applications. The DCX decoder can read files containing +``1``, ``L``, ``P``, or ``RGB`` data. + +When the file is opened, only the first image is read. You can use +:py:meth:`~PIL.Image.Image.seek` or :py:mod:`~PIL.ImageSequence` to read other images. + +FITS +^^^^ + +.. versionadded:: 9.1.0 + +Pillow identifies and reads FITS files, commonly used for astronomy. Uncompressed and +GZIP_1 compressed images can be read. + +FLI, FLC +^^^^^^^^ + +Pillow reads Autodesk FLI and FLC animations. + +The :py:meth:`~PIL.Image.open` method sets the following +:py:attr:`~PIL.Image.Image.info` properties: + +**duration** + The delay (in milliseconds) between each frame. + +FPX +^^^ + +Pillow reads Kodak FlashPix files. Only the highest resolution image is read from the +file, and the viewing transform is not taken into account. + +To enable FPX support, you must install :pypi:`olefile`. + +.. note:: + + To enable full FlashPix support, you need to build and install the IJG JPEG + library before building the Python Imaging Library. See the distribution + README for details. + +FTEX +^^^^ + +.. versionadded:: 3.2.0 + +The FTEX decoder reads textures used for 3D objects in +Independence War 2: Edge Of Chaos. The plugin reads a single texture +per file, in the compressed and uncompressed formats. + +GBR +^^^ + +The GBR decoder reads GIMP brush files, version 1 and 2. + +.. _gbr-opening: + +Opening +~~~~~~~ + +The :py:meth:`~PIL.Image.open` method sets the following +:py:attr:`~PIL.Image.Image.info` properties: + +**comment** + The brush name. + +**spacing** + The spacing between the brushes, in pixels. Version 2 only. + +GD +^^ + +Pillow reads uncompressed GD2 files. Note that you must use +:py:func:`PIL.GdImageFile.open` to read such a file. + +.. _gd-opening: + +Opening +~~~~~~~ + +The :py:meth:`~PIL.Image.open` method sets the following +:py:attr:`~PIL.Image.Image.info` properties: + +**transparency** + Transparency color index. This key is omitted if the image is not + transparent. + +IMT +^^^ + +Pillow reads Image Tools images containing ``L`` data. + +IPTC/NAA +^^^^^^^^ + +Pillow provides limited read support for IPTC/NAA newsphoto files. + +MCIDAS +^^^^^^ + +Pillow identifies and reads 8-bit McIdas area files. + +MIC +^^^ + +Pillow identifies and reads Microsoft Image Composer (MIC) files. When opened, +the first sprite in the file is loaded. You can use :py:meth:`~PIL.Image.Image.seek` and +:py:meth:`~PIL.Image.Image.tell` to read other sprites from the file. + +Note that there may be an embedded gamma of 2.2 in MIC files. + +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 +^^^ + +Pillow reads PhotoCD files containing ``RGB`` data. This only reads the 768x512 +resolution image from the file. Higher resolutions are encoded in a proprietary +encoding. + +PIXAR +^^^^^ + +Pillow provides limited support for PIXAR raster files. The library can +identify and read “dumped†RGB files. + +The format code is ``PIXAR``. + +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 +^^^ + +Pillow identifies and reads Sun raster files. + +WAL +^^^ + +.. versionadded:: 1.1.4 + +Pillow reads Quake2 WAL texture files. + +Note that this file format cannot be automatically identified, so you must use +the open function in the :py:mod:`~PIL.WalImageFile` module to read files in +this format. + +By default, a Quake2 standard palette is attached to the texture. To override +the palette, use the :py:func:`PIL.Image.Image.putpalette()` method. + +WMF, EMF +^^^^^^^^ + +Pillow can identify WMF and EMF files. + +On Windows, it can read WMF and EMF files. By default, it will load the image +at 72 dpi. To load it at another resolution:: + + from PIL import Image + + with Image.open("drawing.wmf") as im: + im.load(dpi=144) + +To add other read or write support, use +:py:func:`PIL.WmfImagePlugin.register_handler` to register a WMF and EMF +handler. :: + + from PIL import Image + from PIL import WmfImagePlugin + + + class WmfHandler: + def open(self, im): + ... + + def load(self, im): + ... + return image + + def save(self, im, fp, filename): + ... + + + wmf_handler = WmfHandler() + + WmfImagePlugin.register_handler(wmf_handler) + + im = Image.open("sample.wmf") + +XPM +^^^ + +Pillow reads X pixmap files (mode ``P``) with 256 colors or less. + +.. _xpm-opening: + +Opening +~~~~~~~ + +The :py:meth:`~PIL.Image.open` method sets the following +:py:attr:`~PIL.Image.Image.info` properties: + +**transparency** + Transparency color index. This key is omitted if the image is not + transparent. + +Write-only formats +------------------ + +PALM +^^^^ + +Pillow provides write-only support for PALM pixmap files. + +The format code is ``Palm``, the extension is ``.palm``. + +PDF +^^^ + +Pillow can write PDF (Acrobat) images. Such images are written as binary PDF 1.4 +files. Different encoding methods are used, depending on the image mode. + +* 1 mode images are saved using TIFF encoding, or JPEG encoding if libtiff support is + unavailable +* L, RGB and CMYK mode images use JPEG encoding +* P mode images use HEX encoding +* LA and RGBA mode images use JPEG2000 encoding + +.. _pdf-saving: + +Saving +~~~~~~ + +The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments: + +**save_all** + If a multiframe image is used, by default, only the first image will be saved. + To save all frames, each frame to a separate page of the PDF, the ``save_all`` + parameter must be present and set to ``True``. + + .. versionadded:: 3.0.0 + +**append_images** + A list of :py:class:`PIL.Image.Image` objects to append as additional pages. Each + of the images in the list can be single or multiframe images. The ``save_all`` + parameter must be present and set to ``True`` in conjunction with + ``append_images``. + + .. versionadded:: 4.2.0 + +**append** + Set to True to append pages to an existing PDF file. If the file doesn't + exist, an :py:exc:`OSError` will be raised. + + .. versionadded:: 5.1.0 + +**resolution** + Image resolution in DPI. This, together with the number of pixels in the + image, will determine the physical dimensions of the page that will be + saved in the PDF. + +**dpi** + A tuple of ``(x_resolution, y_resolution)``, with inches as the resolution + unit. If both the ``resolution`` parameter and the ``dpi`` parameter are + present, ``resolution`` will be ignored. + +**title** + The document’s title. If not appending to an existing PDF file, this will + default to the filename. + + .. versionadded:: 5.1.0 + +**author** + The name of the person who created the document. + + .. versionadded:: 5.1.0 + +**subject** + The subject of the document. + + .. versionadded:: 5.1.0 + +**keywords** + Keywords associated with the document. + + .. versionadded:: 5.1.0 + +**creator** + If the document was converted to PDF from another format, the name of the + conforming product that created the original document from which it was + converted. + + .. versionadded:: 5.1.0 + +**producer** + If the document was converted to PDF from another format, the name of the + conforming product that converted it to PDF. + + .. versionadded:: 5.1.0 + +**creationDate** + The creation date of the document. If not appending to an existing PDF + file, this will default to the current time. + + .. versionadded:: 5.3.0 + +**modDate** + The modification date of the document. If not appending to an existing PDF + file, this will default to the current time. + + .. versionadded:: 5.3.0 + +XV Thumbnails +^^^^^^^^^^^^^ + +Pillow can read XV thumbnail files. + +Identify-only formats +--------------------- + +BUFR +^^^^ + +.. versionadded:: 1.1.3 + +Pillow provides a stub driver for BUFR files. + +To add read or write support to your application, use +:py:func:`PIL.BufrStubImagePlugin.register_handler`. + +GRIB +^^^^ + +.. versionadded:: 1.1.5 + +Pillow provides a stub driver for GRIB files. + +The driver requires the file to start with a GRIB header. If you have files +with embedded GRIB data, or files with multiple GRIB fields, your application +has to seek to the header before passing the file handle to Pillow. + +To add read or write support to your application, use +:py:func:`PIL.GribStubImagePlugin.register_handler`. + +HDF5 +^^^^ + +.. versionadded:: 1.1.5 + +Pillow provides a stub driver for HDF5 files. + +To add read or write support to your application, use +:py:func:`PIL.Hdf5StubImagePlugin.register_handler`. + +MPEG +^^^^ + +Pillow identifies MPEG files. diff --git a/docs/handbook/index.rst b/docs/handbook/index.rst index acdeff7db..7cfa9fcc7 100644 --- a/docs/handbook/index.rst +++ b/docs/handbook/index.rst @@ -1,10 +1,10 @@ -Handbook -======== - -.. toctree:: - :maxdepth: 2 - - overview - tutorial - concepts - appendices +Handbook +======== + +.. toctree:: + :maxdepth: 2 + + overview + tutorial + concepts + appendices diff --git a/docs/handbook/overview.rst b/docs/handbook/overview.rst index 17964d1c5..29a874d7e 100644 --- a/docs/handbook/overview.rst +++ b/docs/handbook/overview.rst @@ -1,46 +1,46 @@ -Overview -======== - -The **Python Imaging Library** adds image processing capabilities to your -Python interpreter. - -This library provides extensive file format support, an efficient internal -representation, and fairly powerful image processing capabilities. - -The core image library is designed for fast access to data stored in a few -basic pixel formats. It should provide a solid foundation for a general image -processing tool. - -Let’s look at a few possible uses of this library. - -Image Archives --------------- - -The Python Imaging Library is ideal for image archival and batch processing -applications. You can use the library to create thumbnails, convert between -file formats, print images, etc. - -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 -------------- - -The current release includes Tk :py:class:`~PIL.ImageTk.PhotoImage` and -:py:class:`~PIL.ImageTk.BitmapImage` interfaces, as well as a :py:mod:`Windows -DIB interface ` that can be used with PythonWin and other -Windows-based toolkits. Many other GUI toolkits come with some kind of PIL -support. - -For debugging, there’s also a :py:meth:`~PIL.Image.Image.show` method which saves an image to -disk, and calls an external display utility. - -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. - -The library also supports image resizing, rotation and arbitrary affine transforms. - -There’s a histogram method allowing you to pull some statistics out of an image. This can be used for automatic contrast enhancement, and for global statistical analysis. +Overview +======== + +The **Python Imaging Library** adds image processing capabilities to your +Python interpreter. + +This library provides extensive file format support, an efficient internal +representation, and fairly powerful image processing capabilities. + +The core image library is designed for fast access to data stored in a few +basic pixel formats. It should provide a solid foundation for a general image +processing tool. + +Let’s look at a few possible uses of this library. + +Image Archives +-------------- + +The Python Imaging Library is ideal for image archival and batch processing +applications. You can use the library to create thumbnails, convert between +file formats, print images, etc. + +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 +------------- + +The current release includes Tk :py:class:`~PIL.ImageTk.PhotoImage` and +:py:class:`~PIL.ImageTk.BitmapImage` interfaces, as well as a :py:mod:`Windows +DIB interface ` that can be used with PythonWin and other +Windows-based toolkits. Many other GUI toolkits come with some kind of PIL +support. + +For debugging, there’s also a :py:meth:`~PIL.Image.Image.show` method which saves an image to +disk, and calls an external display utility. + +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. + +The library also supports image resizing, rotation and arbitrary affine transforms. + +There’s a histogram method allowing you to pull some statistics out of an image. This can be used for automatic contrast enhancement, and for global statistical analysis. diff --git a/docs/handbook/text-anchors.rst b/docs/handbook/text-anchors.rst index 3a9572ab2..91d92e38f 100644 --- a/docs/handbook/text-anchors.rst +++ b/docs/handbook/text-anchors.rst @@ -1,140 +1,140 @@ - -.. _text-anchors: - -Text anchors -============ - -The ``anchor`` parameter determines the alignment of drawn text relative to the ``xy`` parameter. -The default alignment is top left, specifically ``la`` (left-ascender) for horizontal text -and ``lt`` (left-top) for vertical text. - -This parameter is only supported by OpenType/TrueType fonts. -Other fonts may ignore the parameter and use the default (top left) alignment. - -Specifying an anchor -^^^^^^^^^^^^^^^^^^^^ - -An anchor is specified with a two-character string. The first character is the -horizontal alignment, the second character is the vertical alignment. -For example, the default value of ``la`` for horizontal text means left-ascender -aligned text. - -When drawing text with :py:meth:`PIL.ImageDraw.ImageDraw.text` with a specific anchor, -the text will be placed such that the specified anchor point is at the ``xy`` coordinates. - -For example, in the following image, the text is ``ms`` (middle-baseline) aligned, with -``xy`` at the intersection of the two lines: - -.. image:: ../../Tests/images/test_anchor_quick_ms.png - :alt: ms (middle-baseline) aligned text. - :align: left - -:: - - from PIL import Image, ImageDraw, ImageFont - - font = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48) - im = Image.new("RGB", (200, 200), "white") - d = ImageDraw.Draw(im) - d.line(((0, 100), (200, 100)), "gray") - d.line(((100, 0), (100, 200)), "gray") - d.text((100, 100), "Quick", fill="black", anchor="ms", font=font) - -.. container:: clearer - - | - -.. only: comment - The container above prevents the image alignment from affecting the following text. - -Quick reference -^^^^^^^^^^^^^^^ - -.. image:: ../resources/anchor_horizontal.svg - :alt: Horizontal text - :align: center - -.. image:: ../resources/anchor_vertical.svg - :alt: Vertical text - :align: center - -Horizontal anchor alignment -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -``l`` --- left - Anchor is to the left of the text. - - For *horizontal* text this is the origin of the first glyph, as shown in the `FreeType tutorial`_. - -``m`` --- middle - Anchor is horizontally centered with the text. - - For *vertical* text it is recommended to use ``s`` (baseline) alignment instead, - as it does not change based on the specific glyphs of the given text. - -``r`` --- right - Anchor is to the right of the text. - - For *horizontal* text this is the advanced origin of the last glyph, as shown in the `FreeType tutorial`_. - -``s`` --- baseline *(vertical text only)* - Anchor is at the baseline (middle) of the text. The exact alignment depends on the font. - - For *vertical* text this is the recommended alignment, - as it does not change based on the specific glyphs of the given text - (see image for vertical text above). - -Vertical anchor alignment -^^^^^^^^^^^^^^^^^^^^^^^^^ - -``a`` --- ascender / top *(horizontal text only)* - Anchor is at the ascender line (top) of the first line of text, as defined by the font. - - See `Font metrics on Wikipedia`_ for more information. - -``t`` --- top *(single-line text only)* - Anchor is at the top of the text. - - For *vertical* text this is the origin of the first glyph, as shown in the `FreeType tutorial`_. - - For *horizontal* text it is recommended to use ``a`` (ascender) alignment instead, - as it does not change based on the specific glyphs of the given text. - -``m`` --- middle - Anchor is vertically centered with the text. - - For *horizontal* text this is the midpoint of the first ascender line and the last descender line. - -``s`` --- baseline *(horizontal text only)* - Anchor is at the baseline (bottom) of the first line of text, only descenders extend below the anchor. - - See `Font metrics on Wikipedia`_ for more information. - -``b`` --- bottom *(single-line text only)* - Anchor is at the bottom of the text. - - For *vertical* text this is the advanced origin of the last glyph, as shown in the `FreeType tutorial`_. - - For *horizontal* text it is recommended to use ``d`` (descender) alignment instead, - as it does not change based on the specific glyphs of the given text. - -``d`` --- descender / bottom *(horizontal text only)* - Anchor is at the descender line (bottom) of the last line of text, as defined by the font. - - See `Font metrics on Wikipedia`_ for more information. - -Examples -^^^^^^^^ - -The following image shows several examples of anchors for horizontal text. -In each section the ``xy`` parameter was set to the center shown by the intersection -of the two lines. - -.. comment: Image generated with ../example/anchors.py - -.. image:: ../example/anchors.png - :alt: Text anchor examples - :align: center - -.. _Font metrics on Wikipedia: https://en.wikipedia.org/wiki/Typeface#Font_metrics -.. _FreeType tutorial: https://freetype.org/freetype2/docs/tutorial/step2.html + +.. _text-anchors: + +Text anchors +============ + +The ``anchor`` parameter determines the alignment of drawn text relative to the ``xy`` parameter. +The default alignment is top left, specifically ``la`` (left-ascender) for horizontal text +and ``lt`` (left-top) for vertical text. + +This parameter is only supported by OpenType/TrueType fonts. +Other fonts may ignore the parameter and use the default (top left) alignment. + +Specifying an anchor +^^^^^^^^^^^^^^^^^^^^ + +An anchor is specified with a two-character string. The first character is the +horizontal alignment, the second character is the vertical alignment. +For example, the default value of ``la`` for horizontal text means left-ascender +aligned text. + +When drawing text with :py:meth:`PIL.ImageDraw.ImageDraw.text` with a specific anchor, +the text will be placed such that the specified anchor point is at the ``xy`` coordinates. + +For example, in the following image, the text is ``ms`` (middle-baseline) aligned, with +``xy`` at the intersection of the two lines: + +.. image:: ../../Tests/images/test_anchor_quick_ms.png + :alt: ms (middle-baseline) aligned text. + :align: left + +:: + + from PIL import Image, ImageDraw, ImageFont + + font = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48) + im = Image.new("RGB", (200, 200), "white") + d = ImageDraw.Draw(im) + d.line(((0, 100), (200, 100)), "gray") + d.line(((100, 0), (100, 200)), "gray") + d.text((100, 100), "Quick", fill="black", anchor="ms", font=font) + +.. container:: clearer + + | + +.. only: comment + The container above prevents the image alignment from affecting the following text. + +Quick reference +^^^^^^^^^^^^^^^ + +.. image:: ../resources/anchor_horizontal.svg + :alt: Horizontal text + :align: center + +.. image:: ../resources/anchor_vertical.svg + :alt: Vertical text + :align: center + +Horizontal anchor alignment +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``l`` --- left + Anchor is to the left of the text. + + For *horizontal* text this is the origin of the first glyph, as shown in the `FreeType tutorial`_. + +``m`` --- middle + Anchor is horizontally centered with the text. + + For *vertical* text it is recommended to use ``s`` (baseline) alignment instead, + as it does not change based on the specific glyphs of the given text. + +``r`` --- right + Anchor is to the right of the text. + + For *horizontal* text this is the advanced origin of the last glyph, as shown in the `FreeType tutorial`_. + +``s`` --- baseline *(vertical text only)* + Anchor is at the baseline (middle) of the text. The exact alignment depends on the font. + + For *vertical* text this is the recommended alignment, + as it does not change based on the specific glyphs of the given text + (see image for vertical text above). + +Vertical anchor alignment +^^^^^^^^^^^^^^^^^^^^^^^^^ + +``a`` --- ascender / top *(horizontal text only)* + Anchor is at the ascender line (top) of the first line of text, as defined by the font. + + See `Font metrics on Wikipedia`_ for more information. + +``t`` --- top *(single-line text only)* + Anchor is at the top of the text. + + For *vertical* text this is the origin of the first glyph, as shown in the `FreeType tutorial`_. + + For *horizontal* text it is recommended to use ``a`` (ascender) alignment instead, + as it does not change based on the specific glyphs of the given text. + +``m`` --- middle + Anchor is vertically centered with the text. + + For *horizontal* text this is the midpoint of the first ascender line and the last descender line. + +``s`` --- baseline *(horizontal text only)* + Anchor is at the baseline (bottom) of the first line of text, only descenders extend below the anchor. + + See `Font metrics on Wikipedia`_ for more information. + +``b`` --- bottom *(single-line text only)* + Anchor is at the bottom of the text. + + For *vertical* text this is the advanced origin of the last glyph, as shown in the `FreeType tutorial`_. + + For *horizontal* text it is recommended to use ``d`` (descender) alignment instead, + as it does not change based on the specific glyphs of the given text. + +``d`` --- descender / bottom *(horizontal text only)* + Anchor is at the descender line (bottom) of the last line of text, as defined by the font. + + See `Font metrics on Wikipedia`_ for more information. + +Examples +^^^^^^^^ + +The following image shows several examples of anchors for horizontal text. +In each section the ``xy`` parameter was set to the center shown by the intersection +of the two lines. + +.. comment: Image generated with ../example/anchors.py + +.. image:: ../example/anchors.png + :alt: Text anchor examples + :align: center + +.. _Font metrics on Wikipedia: https://en.wikipedia.org/wiki/Typeface#Font_metrics +.. _FreeType tutorial: https://freetype.org/freetype2/docs/tutorial/step2.html diff --git a/docs/handbook/tutorial.rst b/docs/handbook/tutorial.rst index 523e2ad74..84ad0163f 100644 --- a/docs/handbook/tutorial.rst +++ b/docs/handbook/tutorial.rst @@ -1,630 +1,630 @@ -Tutorial -======== - -Using the Image class ---------------------- - -The most important class in the Python Imaging Library is the -:py:class:`~PIL.Image.Image` class, defined in the module with the same name. -You can create instances of this class in several ways; either by loading -images from files, processing other images, or creating images from scratch. - -To load an image from a file, use the :py:func:`~PIL.Image.open` function -in the :py:mod:`~PIL.Image` module:: - - >>> from PIL import Image - >>> im = Image.open("hopper.ppm") - -If successful, this function returns an :py:class:`~PIL.Image.Image` object. -You can now use instance attributes to examine the file contents:: - - >>> print(im.format, im.size, im.mode) - PPM (512, 512) RGB - -The :py:attr:`~PIL.Image.Image.format` attribute identifies the source of an -image. If the image was not read from a file, it is set to None. The size -attribute is a 2-tuple containing width and height (in pixels). The -:py:attr:`~PIL.Image.Image.mode` attribute defines the number and names of the -bands in the image, and also the pixel type and depth. Common modes are “L†-(luminance) for grayscale images, “RGB†for true color images, and “CMYK†for -pre-press images. - -If the file cannot be opened, an :py:exc:`OSError` exception is raised. - -Once you have an instance of the :py:class:`~PIL.Image.Image` class, you can use -the methods defined by this class to process and manipulate the image. For -example, let’s display the image we just loaded:: - - >>> im.show() - -.. note:: - - The standard version of :py:meth:`~PIL.Image.Image.show` is not very - efficient, since it saves the image to a temporary file and calls a utility - to display the image. If you don’t have an appropriate utility installed, - it won’t even work. When it does work though, it is very handy for - debugging and tests. - -The following sections provide an overview of the different functions provided in this library. - -Reading and writing images --------------------------- - -The Python Imaging Library supports a wide variety of image file formats. To -read files from disk, use the :py:func:`~PIL.Image.open` function in the -:py:mod:`~PIL.Image` module. You don’t have to know the file format to open a -file. The library automatically determines the format based on the contents of -the file. - -To save a file, use the :py:meth:`~PIL.Image.Image.save` method of the -:py:class:`~PIL.Image.Image` class. When saving files, the name becomes -important. Unless you specify the format, the library uses the filename -extension to discover which file storage format to use. - -Convert files to JPEG -^^^^^^^^^^^^^^^^^^^^^ - -:: - - import os, sys - from PIL import Image - - for infile in sys.argv[1:]: - f, e = os.path.splitext(infile) - outfile = f + ".jpg" - if infile != outfile: - try: - with Image.open(infile) as im: - im.save(outfile) - except OSError: - print("cannot convert", infile) - -A second argument can be supplied to the :py:meth:`~PIL.Image.Image.save` -method which explicitly specifies a file format. If you use a non-standard -extension, you must always specify the format this way: - -Create JPEG thumbnails -^^^^^^^^^^^^^^^^^^^^^^ - -:: - - import os, sys - from PIL import Image - - size = (128, 128) - - for infile in sys.argv[1:]: - outfile = os.path.splitext(infile)[0] + ".thumbnail" - if infile != outfile: - try: - with Image.open(infile) as im: - im.thumbnail(size) - im.save(outfile, "JPEG") - except OSError: - print("cannot create thumbnail for", infile) - -It is important to note that the library doesn’t decode or load the raster data -unless it really has to. When you open a file, the file header is read to -determine the file format and extract things like mode, size, and other -properties required to decode the file, but the rest of the file is not -processed until later. - -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 -^^^^^^^^^^^^^^^^^^^^ - -:: - - import sys - from PIL import Image - - for infile in sys.argv[1:]: - try: - with Image.open(infile) as im: - print(infile, im.format, f"{im.size}x{im.mode}") - except OSError: - pass - -Cutting, pasting, and merging images ------------------------------------- - -The :py:class:`~PIL.Image.Image` class contains methods allowing you to -manipulate regions within an image. To extract a sub-rectangle from an image, -use the :py:meth:`~PIL.Image.Image.crop` method. - -Copying a subrectangle from an image -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -:: - - box = (100, 100, 400, 400) - region = im.crop(box) - -The region is defined by a 4-tuple, where coordinates are (left, upper, right, -lower). The Python Imaging Library uses a coordinate system with (0, 0) in the -upper left corner. Also note that coordinates refer to positions between the -pixels, so the region in the above example is exactly 300x300 pixels. - -The region could now be processed in a certain manner and pasted back. - -Processing a subrectangle, and pasting it back -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -:: - - region = region.transpose(Image.Transpose.ROTATE_180) - im.paste(region, box) - -When pasting regions back, the size of the region must match the given region -exactly. In addition, the region cannot extend outside the image. However, the -modes of the original image and the region do not need to match. If they don’t, -the region is automatically converted before being pasted (see the section on -:ref:`color-transforms` below for details). - -Here’s an additional example: - -Rolling an image -^^^^^^^^^^^^^^^^ - -:: - - def roll(im, delta): - """Roll an image sideways.""" - xsize, ysize = im.size - - delta = delta % xsize - if delta == 0: - return im - - part1 = im.crop((0, 0, delta, ysize)) - part2 = im.crop((delta, 0, xsize, ysize)) - im.paste(part1, (xsize - delta, 0, xsize, ysize)) - im.paste(part2, (0, 0, xsize - delta, ysize)) - - return im - -Or if you would like to merge two images into a wider image: - -Merging images -^^^^^^^^^^^^^^ - -:: - - def merge(im1, im2): - w = im1.size[0] + im2.size[0] - h = max(im1.size[1], im2.size[1]) - im = Image.new("RGBA", (w, h)) - - im.paste(im1) - im.paste(im2, (im1.size[0], 0)) - - return im - -For more advanced tricks, the paste method can also take a transparency mask as -an optional argument. In this mask, the value 255 indicates that the pasted -image is opaque in that position (that is, the pasted image should be used as -is). The value 0 means that the pasted image is completely transparent. Values -in-between indicate different levels of transparency. For example, pasting an -RGBA image and also using it as the mask would paste the opaque portion -of the image but not its transparent background. - -The Python Imaging Library also allows you to work with the individual bands of -an multi-band image, such as an RGB image. The split method creates a set of -new images, each containing one band from the original multi-band image. The -merge function takes a mode and a tuple of images, and combines them into a new -image. The following sample swaps the three bands of an RGB image: - -Splitting and merging bands -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -:: - - r, g, b = im.split() - im = Image.merge("RGB", (b, g, r)) - -Note that for a single-band image, :py:meth:`~PIL.Image.Image.split` returns -the image itself. To work with individual color bands, you may want to convert -the image to “RGB†first. - -Geometrical transforms ----------------------- - -The :py:class:`PIL.Image.Image` class contains methods to -:py:meth:`~PIL.Image.Image.resize` and :py:meth:`~PIL.Image.Image.rotate` an -image. The former takes a tuple giving the new size, the latter the angle in -degrees counter-clockwise. - -Simple geometry transforms -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -:: - - out = im.resize((128, 128)) - out = im.rotate(45) # degrees counter-clockwise - -To rotate the image in 90 degree steps, you can either use the -:py:meth:`~PIL.Image.Image.rotate` method or the -:py:meth:`~PIL.Image.Image.transpose` method. The latter can also be used to -flip an image around its horizontal or vertical axis. - -Transposing an image -^^^^^^^^^^^^^^^^^^^^ - -:: - - out = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) - out = im.transpose(Image.Transpose.FLIP_TOP_BOTTOM) - out = im.transpose(Image.Transpose.ROTATE_90) - out = im.transpose(Image.Transpose.ROTATE_180) - out = im.transpose(Image.Transpose.ROTATE_270) - -``transpose(ROTATE)`` operations can also be performed identically with -:py:meth:`~PIL.Image.Image.rotate` operations, provided the ``expand`` flag is -true, to provide for the same changes to the image's size. - -A more general form of image transformations can be carried out via the -:py:meth:`~PIL.Image.Image.transform` method. - -Relative resizing -^^^^^^^^^^^^^^^^^ - -Instead of calculating the size of the new image when resizing, you can also -choose to resize relative to a given size. - -:: - - from PIL import Image, ImageOps - size = (100, 150) - with Image.open("Tests/images/hopper.png") as im: - ImageOps.contain(im, size).save("imageops_contain.png") - ImageOps.cover(im, size).save("imageops_cover.png") - ImageOps.fit(im, size).save("imageops_fit.png") - ImageOps.pad(im, size, color="#f00").save("imageops_pad.png") - - # thumbnail() can also be used, - # but will modify the image object in place - im.thumbnail(size) - im.save("imageops_thumbnail.png") - -+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+ -| | :py:meth:`~PIL.Image.Image.thumbnail` | :py:meth:`~PIL.ImageOps.contain` | :py:meth:`~PIL.ImageOps.cover` | :py:meth:`~PIL.ImageOps.fit` | :py:meth:`~PIL.ImageOps.pad` | -+================+===========================================+============================================+==========================================+========================================+========================================+ -|Given size | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` | -+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+ -|Resulting image | .. image:: ../example/image_thumbnail.png | .. image:: ../example/imageops_contain.png | .. image:: ../example/imageops_cover.png | .. image:: ../example/imageops_fit.png | .. image:: ../example/imageops_pad.png | -+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+ -|Resulting size | ``100×100`` | ``100×100`` | ``150×150`` | ``100×150`` | ``100×150`` | -+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+ - -.. _color-transforms: - -Color transforms ----------------- - -The Python Imaging Library allows you to convert images between different pixel -representations using the :py:meth:`~PIL.Image.Image.convert` method. - -Converting between modes -^^^^^^^^^^^^^^^^^^^^^^^^ - -:: - - from PIL import Image - - with Image.open("hopper.ppm") as im: - im = im.convert("L") - -The library supports transformations between each supported mode and the “L†-and “RGB†modes. To convert between other modes, you may have to use an -intermediate image (typically an “RGB†image). - -Image enhancement ------------------ - -The Python Imaging Library provides a number of methods and modules that can be -used to enhance images. - -Filters -^^^^^^^ - -The :py:mod:`~PIL.ImageFilter` module contains a number of pre-defined -enhancement filters that can be used with the -:py:meth:`~PIL.Image.Image.filter` method. - -Applying filters -~~~~~~~~~~~~~~~~ - -:: - - from PIL import ImageFilter - out = im.filter(ImageFilter.DETAIL) - -Point Operations -^^^^^^^^^^^^^^^^ - -The :py:meth:`~PIL.Image.Image.point` method can be used to translate the pixel -values of an image (e.g. image contrast manipulation). In most cases, a -function object expecting one argument can be passed to this method. Each -pixel is processed according to that function: - -Applying point transforms -~~~~~~~~~~~~~~~~~~~~~~~~~ - -:: - - # multiply each pixel by 1.2 - out = im.point(lambda i: i * 1.2) - -Using the above technique, you can quickly apply any simple expression to an -image. You can also combine the :py:meth:`~PIL.Image.Image.point` and -:py:meth:`~PIL.Image.Image.paste` methods to selectively modify an image: - -Processing individual bands -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -:: - - # split the image into individual bands - source = im.split() - - R, G, B = 0, 1, 2 - - # select regions where red is less than 100 - mask = source[R].point(lambda i: i < 100 and 255) - - # process the green band - out = source[G].point(lambda i: i * 0.7) - - # paste the processed band back, but only where red was < 100 - source[G].paste(out, None, mask) - - # build a new multiband image - im = Image.merge(im.mode, source) - -Note the syntax used to create the mask:: - - imout = im.point(lambda i: expression and 255) - -Python only evaluates the portion of a logical expression as is necessary to -determine the outcome, and returns the last value examined as the result of the -expression. So if the expression above is false (0), Python does not look at -the second operand, and thus returns 0. Otherwise, it returns 255. - -Enhancement -^^^^^^^^^^^ - -For more advanced image enhancement, you can use the classes in the -:py:mod:`~PIL.ImageEnhance` module. Once created from an image, an enhancement -object can be used to quickly try out different settings. - -You can adjust contrast, brightness, color balance and sharpness in this way. - -Enhancing images -~~~~~~~~~~~~~~~~ - -:: - - from PIL import ImageEnhance - - enh = ImageEnhance.Contrast(im) - enh.enhance(1.3).show("30% more contrast") - -Image sequences ---------------- - -The Python Imaging Library contains some basic support for image sequences -(also called animation formats). Supported sequence formats include FLI/FLC, -GIF, and a few experimental formats. TIFF files can also contain more than one -frame. - -When you open a sequence file, PIL automatically loads the first frame in the -sequence. You can use the seek and tell methods to move between different -frames: - -Reading sequences -^^^^^^^^^^^^^^^^^ - -:: - - from PIL import Image - - with Image.open("animation.gif") as im: - im.seek(1) # skip to the second frame - - try: - while 1: - im.seek(im.tell() + 1) - # do something to im - except EOFError: - pass # end of sequence - -As seen in this example, you’ll get an :py:exc:`EOFError` exception when the -sequence ends. - -The following class lets you use the for-statement to loop over the sequence: - -Using the ImageSequence Iterator class -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -:: - - from PIL import ImageSequence - for frame in ImageSequence.Iterator(im): - # ...do something to frame... - - -PostScript printing -------------------- - -The Python Imaging Library includes functions to print images, text and -graphics on PostScript printers. Here’s a simple example: - -Drawing PostScript -^^^^^^^^^^^^^^^^^^ - -:: - - from PIL import Image - from PIL import PSDraw - - with Image.open("hopper.ppm") as im: - title = "hopper" - box = (1 * 72, 2 * 72, 7 * 72, 10 * 72) # in points - - ps = PSDraw.PSDraw() # default is sys.stdout or sys.stdout.buffer - ps.begin_document(title) - - # draw the image (75 dpi) - ps.image(box, im, 75) - ps.rectangle(box) - - # draw title - ps.setfont("HelveticaNarrow-Bold", 36) - ps.text((3 * 72, 4 * 72), title) - - ps.end_document() - -More on reading images ----------------------- - -As described earlier, the :py:func:`~PIL.Image.open` function of the -:py:mod:`~PIL.Image` module is used to open an image file. In most cases, you -simply pass it the filename as an argument. ``Image.open()`` can be used as a -context manager:: - - from PIL import Image - with Image.open("hopper.ppm") as im: - ... - -If everything goes well, the result is an :py:class:`PIL.Image.Image` object. -Otherwise, an :exc:`OSError` exception is raised. - -You can use a file-like object instead of the filename. The object must -implement ``file.read``, ``file.seek`` and ``file.tell`` methods, -and be opened in binary mode. - -Reading from an open file -^^^^^^^^^^^^^^^^^^^^^^^^^ - -:: - - from PIL import Image - - with open("hopper.ppm", "rb") as fp: - im = Image.open(fp) - -To read an image from binary data, use the :py:class:`~io.BytesIO` -class: - -Reading from binary data -^^^^^^^^^^^^^^^^^^^^^^^^ - -:: - - from PIL import Image - import io - - im = Image.open(io.BytesIO(buffer)) - -Note that the library rewinds the file (using ``seek(0)``) before reading the -image header. In addition, seek will also be used when the image data is read -(by the load method). If the image file is embedded in a larger file, such as a -tar file, you can use the :py:class:`~PIL.ContainerIO` or -:py:class:`~PIL.TarIO` modules to access it. - -Reading from URL -^^^^^^^^^^^^^^^^ - -:: - - from PIL import Image - from urllib.request import urlopen - url = "https://python-pillow.org/assets/images/pillow-logo.png" - img = Image.open(urlopen(url)) - - -Reading from a tar archive -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -:: - - from PIL import Image, TarIO - - fp = TarIO.TarIO("Tests/images/hopper.tar", "hopper.jpg") - im = Image.open(fp) - - -Batch processing -^^^^^^^^^^^^^^^^ - -Operations can be applied to multiple image files. For example, all PNG images -in the current directory can be saved as JPEGs at reduced quality. - -:: - - import glob - from PIL import Image - - - def compress_image(source_path, dest_path): - with Image.open(source_path) as img: - if img.mode != "RGB": - img = img.convert("RGB") - img.save(dest_path, "JPEG", optimize=True, quality=80) - - - paths = glob.glob("*.png") - for path in paths: - compress_image(path, path[:-4] + ".jpg") - -Since images can also be opened from a ``Path`` from the ``pathlib`` module, -the example could be modified to use ``pathlib`` instead of the ``glob`` -module. - -:: - - from pathlib import Path - - paths = Path(".").glob("*.png") - for path in paths: - compress_image(path, path.stem + ".jpg") - - -Controlling the decoder ------------------------ - -Some decoders allow you to manipulate the image while reading it from a file. -This can often be used to speed up decoding when creating thumbnails (when -speed is usually more important than quality) and printing to a monochrome -laser printer (when only a grayscale version of the image is needed). - -The :py:meth:`~PIL.Image.Image.draft` method manipulates an opened but not yet -loaded image so it as closely as possible matches the given mode and size. This -is done by reconfiguring the image decoder. - -Reading in draft mode -^^^^^^^^^^^^^^^^^^^^^ - -This is only available for JPEG and MPO files. - -:: - - from PIL import Image - - with Image.open(file) as im: - print("original =", im.mode, im.size) - - im.draft("L", (100, 100)) - print("draft =", im.mode, im.size) - -This prints something like:: - - original = RGB (512, 512) - draft = L (128, 128) - -Note that the resulting image may not exactly match the requested mode and -size. To make sure that the image is not larger than the given size, use the -thumbnail method instead. +Tutorial +======== + +Using the Image class +--------------------- + +The most important class in the Python Imaging Library is the +:py:class:`~PIL.Image.Image` class, defined in the module with the same name. +You can create instances of this class in several ways; either by loading +images from files, processing other images, or creating images from scratch. + +To load an image from a file, use the :py:func:`~PIL.Image.open` function +in the :py:mod:`~PIL.Image` module:: + + >>> from PIL import Image + >>> im = Image.open("hopper.ppm") + +If successful, this function returns an :py:class:`~PIL.Image.Image` object. +You can now use instance attributes to examine the file contents:: + + >>> print(im.format, im.size, im.mode) + PPM (512, 512) RGB + +The :py:attr:`~PIL.Image.Image.format` attribute identifies the source of an +image. If the image was not read from a file, it is set to None. The size +attribute is a 2-tuple containing width and height (in pixels). The +:py:attr:`~PIL.Image.Image.mode` attribute defines the number and names of the +bands in the image, and also the pixel type and depth. Common modes are “L†+(luminance) for grayscale images, “RGB†for true color images, and “CMYK†for +pre-press images. + +If the file cannot be opened, an :py:exc:`OSError` exception is raised. + +Once you have an instance of the :py:class:`~PIL.Image.Image` class, you can use +the methods defined by this class to process and manipulate the image. For +example, let’s display the image we just loaded:: + + >>> im.show() + +.. note:: + + The standard version of :py:meth:`~PIL.Image.Image.show` is not very + efficient, since it saves the image to a temporary file and calls a utility + to display the image. If you don’t have an appropriate utility installed, + it won’t even work. When it does work though, it is very handy for + debugging and tests. + +The following sections provide an overview of the different functions provided in this library. + +Reading and writing images +-------------------------- + +The Python Imaging Library supports a wide variety of image file formats. To +read files from disk, use the :py:func:`~PIL.Image.open` function in the +:py:mod:`~PIL.Image` module. You don’t have to know the file format to open a +file. The library automatically determines the format based on the contents of +the file. + +To save a file, use the :py:meth:`~PIL.Image.Image.save` method of the +:py:class:`~PIL.Image.Image` class. When saving files, the name becomes +important. Unless you specify the format, the library uses the filename +extension to discover which file storage format to use. + +Convert files to JPEG +^^^^^^^^^^^^^^^^^^^^^ + +:: + + import os, sys + from PIL import Image + + for infile in sys.argv[1:]: + f, e = os.path.splitext(infile) + outfile = f + ".jpg" + if infile != outfile: + try: + with Image.open(infile) as im: + im.save(outfile) + except OSError: + print("cannot convert", infile) + +A second argument can be supplied to the :py:meth:`~PIL.Image.Image.save` +method which explicitly specifies a file format. If you use a non-standard +extension, you must always specify the format this way: + +Create JPEG thumbnails +^^^^^^^^^^^^^^^^^^^^^^ + +:: + + import os, sys + from PIL import Image + + size = (128, 128) + + for infile in sys.argv[1:]: + outfile = os.path.splitext(infile)[0] + ".thumbnail" + if infile != outfile: + try: + with Image.open(infile) as im: + im.thumbnail(size) + im.save(outfile, "JPEG") + except OSError: + print("cannot create thumbnail for", infile) + +It is important to note that the library doesn’t decode or load the raster data +unless it really has to. When you open a file, the file header is read to +determine the file format and extract things like mode, size, and other +properties required to decode the file, but the rest of the file is not +processed until later. + +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 +^^^^^^^^^^^^^^^^^^^^ + +:: + + import sys + from PIL import Image + + for infile in sys.argv[1:]: + try: + with Image.open(infile) as im: + print(infile, im.format, f"{im.size}x{im.mode}") + except OSError: + pass + +Cutting, pasting, and merging images +------------------------------------ + +The :py:class:`~PIL.Image.Image` class contains methods allowing you to +manipulate regions within an image. To extract a sub-rectangle from an image, +use the :py:meth:`~PIL.Image.Image.crop` method. + +Copying a subrectangle from an image +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:: + + box = (100, 100, 400, 400) + region = im.crop(box) + +The region is defined by a 4-tuple, where coordinates are (left, upper, right, +lower). The Python Imaging Library uses a coordinate system with (0, 0) in the +upper left corner. Also note that coordinates refer to positions between the +pixels, so the region in the above example is exactly 300x300 pixels. + +The region could now be processed in a certain manner and pasted back. + +Processing a subrectangle, and pasting it back +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:: + + region = region.transpose(Image.Transpose.ROTATE_180) + im.paste(region, box) + +When pasting regions back, the size of the region must match the given region +exactly. In addition, the region cannot extend outside the image. However, the +modes of the original image and the region do not need to match. If they don’t, +the region is automatically converted before being pasted (see the section on +:ref:`color-transforms` below for details). + +Here’s an additional example: + +Rolling an image +^^^^^^^^^^^^^^^^ + +:: + + def roll(im, delta): + """Roll an image sideways.""" + xsize, ysize = im.size + + delta = delta % xsize + if delta == 0: + return im + + part1 = im.crop((0, 0, delta, ysize)) + part2 = im.crop((delta, 0, xsize, ysize)) + im.paste(part1, (xsize - delta, 0, xsize, ysize)) + im.paste(part2, (0, 0, xsize - delta, ysize)) + + return im + +Or if you would like to merge two images into a wider image: + +Merging images +^^^^^^^^^^^^^^ + +:: + + def merge(im1, im2): + w = im1.size[0] + im2.size[0] + h = max(im1.size[1], im2.size[1]) + im = Image.new("RGBA", (w, h)) + + im.paste(im1) + im.paste(im2, (im1.size[0], 0)) + + return im + +For more advanced tricks, the paste method can also take a transparency mask as +an optional argument. In this mask, the value 255 indicates that the pasted +image is opaque in that position (that is, the pasted image should be used as +is). The value 0 means that the pasted image is completely transparent. Values +in-between indicate different levels of transparency. For example, pasting an +RGBA image and also using it as the mask would paste the opaque portion +of the image but not its transparent background. + +The Python Imaging Library also allows you to work with the individual bands of +an multi-band image, such as an RGB image. The split method creates a set of +new images, each containing one band from the original multi-band image. The +merge function takes a mode and a tuple of images, and combines them into a new +image. The following sample swaps the three bands of an RGB image: + +Splitting and merging bands +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:: + + r, g, b = im.split() + im = Image.merge("RGB", (b, g, r)) + +Note that for a single-band image, :py:meth:`~PIL.Image.Image.split` returns +the image itself. To work with individual color bands, you may want to convert +the image to “RGB†first. + +Geometrical transforms +---------------------- + +The :py:class:`PIL.Image.Image` class contains methods to +:py:meth:`~PIL.Image.Image.resize` and :py:meth:`~PIL.Image.Image.rotate` an +image. The former takes a tuple giving the new size, the latter the angle in +degrees counter-clockwise. + +Simple geometry transforms +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:: + + out = im.resize((128, 128)) + out = im.rotate(45) # degrees counter-clockwise + +To rotate the image in 90 degree steps, you can either use the +:py:meth:`~PIL.Image.Image.rotate` method or the +:py:meth:`~PIL.Image.Image.transpose` method. The latter can also be used to +flip an image around its horizontal or vertical axis. + +Transposing an image +^^^^^^^^^^^^^^^^^^^^ + +:: + + out = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) + out = im.transpose(Image.Transpose.FLIP_TOP_BOTTOM) + out = im.transpose(Image.Transpose.ROTATE_90) + out = im.transpose(Image.Transpose.ROTATE_180) + out = im.transpose(Image.Transpose.ROTATE_270) + +``transpose(ROTATE)`` operations can also be performed identically with +:py:meth:`~PIL.Image.Image.rotate` operations, provided the ``expand`` flag is +true, to provide for the same changes to the image's size. + +A more general form of image transformations can be carried out via the +:py:meth:`~PIL.Image.Image.transform` method. + +Relative resizing +^^^^^^^^^^^^^^^^^ + +Instead of calculating the size of the new image when resizing, you can also +choose to resize relative to a given size. + +:: + + from PIL import Image, ImageOps + size = (100, 150) + with Image.open("Tests/images/hopper.png") as im: + ImageOps.contain(im, size).save("imageops_contain.png") + ImageOps.cover(im, size).save("imageops_cover.png") + ImageOps.fit(im, size).save("imageops_fit.png") + ImageOps.pad(im, size, color="#f00").save("imageops_pad.png") + + # thumbnail() can also be used, + # but will modify the image object in place + im.thumbnail(size) + im.save("imageops_thumbnail.png") + ++----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+ +| | :py:meth:`~PIL.Image.Image.thumbnail` | :py:meth:`~PIL.ImageOps.contain` | :py:meth:`~PIL.ImageOps.cover` | :py:meth:`~PIL.ImageOps.fit` | :py:meth:`~PIL.ImageOps.pad` | ++================+===========================================+============================================+==========================================+========================================+========================================+ +|Given size | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` | ++----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+ +|Resulting image | .. image:: ../example/image_thumbnail.png | .. image:: ../example/imageops_contain.png | .. image:: ../example/imageops_cover.png | .. image:: ../example/imageops_fit.png | .. image:: ../example/imageops_pad.png | ++----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+ +|Resulting size | ``100×100`` | ``100×100`` | ``150×150`` | ``100×150`` | ``100×150`` | ++----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+ + +.. _color-transforms: + +Color transforms +---------------- + +The Python Imaging Library allows you to convert images between different pixel +representations using the :py:meth:`~PIL.Image.Image.convert` method. + +Converting between modes +^^^^^^^^^^^^^^^^^^^^^^^^ + +:: + + from PIL import Image + + with Image.open("hopper.ppm") as im: + im = im.convert("L") + +The library supports transformations between each supported mode and the “L†+and “RGB†modes. To convert between other modes, you may have to use an +intermediate image (typically an “RGB†image). + +Image enhancement +----------------- + +The Python Imaging Library provides a number of methods and modules that can be +used to enhance images. + +Filters +^^^^^^^ + +The :py:mod:`~PIL.ImageFilter` module contains a number of pre-defined +enhancement filters that can be used with the +:py:meth:`~PIL.Image.Image.filter` method. + +Applying filters +~~~~~~~~~~~~~~~~ + +:: + + from PIL import ImageFilter + out = im.filter(ImageFilter.DETAIL) + +Point Operations +^^^^^^^^^^^^^^^^ + +The :py:meth:`~PIL.Image.Image.point` method can be used to translate the pixel +values of an image (e.g. image contrast manipulation). In most cases, a +function object expecting one argument can be passed to this method. Each +pixel is processed according to that function: + +Applying point transforms +~~~~~~~~~~~~~~~~~~~~~~~~~ + +:: + + # multiply each pixel by 1.2 + out = im.point(lambda i: i * 1.2) + +Using the above technique, you can quickly apply any simple expression to an +image. You can also combine the :py:meth:`~PIL.Image.Image.point` and +:py:meth:`~PIL.Image.Image.paste` methods to selectively modify an image: + +Processing individual bands +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:: + + # split the image into individual bands + source = im.split() + + R, G, B = 0, 1, 2 + + # select regions where red is less than 100 + mask = source[R].point(lambda i: i < 100 and 255) + + # process the green band + out = source[G].point(lambda i: i * 0.7) + + # paste the processed band back, but only where red was < 100 + source[G].paste(out, None, mask) + + # build a new multiband image + im = Image.merge(im.mode, source) + +Note the syntax used to create the mask:: + + imout = im.point(lambda i: expression and 255) + +Python only evaluates the portion of a logical expression as is necessary to +determine the outcome, and returns the last value examined as the result of the +expression. So if the expression above is false (0), Python does not look at +the second operand, and thus returns 0. Otherwise, it returns 255. + +Enhancement +^^^^^^^^^^^ + +For more advanced image enhancement, you can use the classes in the +:py:mod:`~PIL.ImageEnhance` module. Once created from an image, an enhancement +object can be used to quickly try out different settings. + +You can adjust contrast, brightness, color balance and sharpness in this way. + +Enhancing images +~~~~~~~~~~~~~~~~ + +:: + + from PIL import ImageEnhance + + enh = ImageEnhance.Contrast(im) + enh.enhance(1.3).show("30% more contrast") + +Image sequences +--------------- + +The Python Imaging Library contains some basic support for image sequences +(also called animation formats). Supported sequence formats include FLI/FLC, +GIF, and a few experimental formats. TIFF files can also contain more than one +frame. + +When you open a sequence file, PIL automatically loads the first frame in the +sequence. You can use the seek and tell methods to move between different +frames: + +Reading sequences +^^^^^^^^^^^^^^^^^ + +:: + + from PIL import Image + + with Image.open("animation.gif") as im: + im.seek(1) # skip to the second frame + + try: + while 1: + im.seek(im.tell() + 1) + # do something to im + except EOFError: + pass # end of sequence + +As seen in this example, you’ll get an :py:exc:`EOFError` exception when the +sequence ends. + +The following class lets you use the for-statement to loop over the sequence: + +Using the ImageSequence Iterator class +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:: + + from PIL import ImageSequence + for frame in ImageSequence.Iterator(im): + # ...do something to frame... + + +PostScript printing +------------------- + +The Python Imaging Library includes functions to print images, text and +graphics on PostScript printers. Here’s a simple example: + +Drawing PostScript +^^^^^^^^^^^^^^^^^^ + +:: + + from PIL import Image + from PIL import PSDraw + + with Image.open("hopper.ppm") as im: + title = "hopper" + box = (1 * 72, 2 * 72, 7 * 72, 10 * 72) # in points + + ps = PSDraw.PSDraw() # default is sys.stdout or sys.stdout.buffer + ps.begin_document(title) + + # draw the image (75 dpi) + ps.image(box, im, 75) + ps.rectangle(box) + + # draw title + ps.setfont("HelveticaNarrow-Bold", 36) + ps.text((3 * 72, 4 * 72), title) + + ps.end_document() + +More on reading images +---------------------- + +As described earlier, the :py:func:`~PIL.Image.open` function of the +:py:mod:`~PIL.Image` module is used to open an image file. In most cases, you +simply pass it the filename as an argument. ``Image.open()`` can be used as a +context manager:: + + from PIL import Image + with Image.open("hopper.ppm") as im: + ... + +If everything goes well, the result is an :py:class:`PIL.Image.Image` object. +Otherwise, an :exc:`OSError` exception is raised. + +You can use a file-like object instead of the filename. The object must +implement ``file.read``, ``file.seek`` and ``file.tell`` methods, +and be opened in binary mode. + +Reading from an open file +^^^^^^^^^^^^^^^^^^^^^^^^^ + +:: + + from PIL import Image + + with open("hopper.ppm", "rb") as fp: + im = Image.open(fp) + +To read an image from binary data, use the :py:class:`~io.BytesIO` +class: + +Reading from binary data +^^^^^^^^^^^^^^^^^^^^^^^^ + +:: + + from PIL import Image + import io + + im = Image.open(io.BytesIO(buffer)) + +Note that the library rewinds the file (using ``seek(0)``) before reading the +image header. In addition, seek will also be used when the image data is read +(by the load method). If the image file is embedded in a larger file, such as a +tar file, you can use the :py:class:`~PIL.ContainerIO` or +:py:class:`~PIL.TarIO` modules to access it. + +Reading from URL +^^^^^^^^^^^^^^^^ + +:: + + from PIL import Image + from urllib.request import urlopen + url = "https://python-pillow.org/assets/images/pillow-logo.png" + img = Image.open(urlopen(url)) + + +Reading from a tar archive +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:: + + from PIL import Image, TarIO + + fp = TarIO.TarIO("Tests/images/hopper.tar", "hopper.jpg") + im = Image.open(fp) + + +Batch processing +^^^^^^^^^^^^^^^^ + +Operations can be applied to multiple image files. For example, all PNG images +in the current directory can be saved as JPEGs at reduced quality. + +:: + + import glob + from PIL import Image + + + def compress_image(source_path, dest_path): + with Image.open(source_path) as img: + if img.mode != "RGB": + img = img.convert("RGB") + img.save(dest_path, "JPEG", optimize=True, quality=80) + + + paths = glob.glob("*.png") + for path in paths: + compress_image(path, path[:-4] + ".jpg") + +Since images can also be opened from a ``Path`` from the ``pathlib`` module, +the example could be modified to use ``pathlib`` instead of the ``glob`` +module. + +:: + + from pathlib import Path + + paths = Path(".").glob("*.png") + for path in paths: + compress_image(path, path.stem + ".jpg") + + +Controlling the decoder +----------------------- + +Some decoders allow you to manipulate the image while reading it from a file. +This can often be used to speed up decoding when creating thumbnails (when +speed is usually more important than quality) and printing to a monochrome +laser printer (when only a grayscale version of the image is needed). + +The :py:meth:`~PIL.Image.Image.draft` method manipulates an opened but not yet +loaded image so it as closely as possible matches the given mode and size. This +is done by reconfiguring the image decoder. + +Reading in draft mode +^^^^^^^^^^^^^^^^^^^^^ + +This is only available for JPEG and MPO files. + +:: + + from PIL import Image + + with Image.open(file) as im: + print("original =", im.mode, im.size) + + im.draft("L", (100, 100)) + print("draft =", im.mode, im.size) + +This prints something like:: + + original = RGB (512, 512) + draft = L (128, 128) + +Note that the resulting image may not exactly match the requested mode and +size. To make sure that the image is not larger than the given size, use the +thumbnail method instead. diff --git a/docs/handbook/writing-your-own-image-plugin.rst b/docs/handbook/writing-your-own-image-plugin.rst index 956d63aa7..5743c4a14 100644 --- a/docs/handbook/writing-your-own-image-plugin.rst +++ b/docs/handbook/writing-your-own-image-plugin.rst @@ -1,456 +1,456 @@ -.. _image-plugins: - -Writing Your Own Image Plugin -============================= - -Pillow uses a plugin model which allows you to add your own -decoders and encoders to the library, without any changes to the library -itself. Such plugins usually have names like :file:`XxxImagePlugin.py`, -where ``Xxx`` is a unique format name (usually an abbreviation). - -.. warning:: Pillow >= 2.1.0 no longer automatically imports any file - in the Python path with a name ending in - :file:`ImagePlugin.py`. You will need to import your - image plugin manually. - -Pillow decodes files in two stages: - -1. It loops over the available image plugins in the loaded order, and - calls the plugin's ``_accept`` function with the first 16 bytes of - the file. If the ``_accept`` function returns true, the plugin's - ``_open`` method is called to set up the image metadata and image - tiles. The ``_open`` method is not for decoding the actual image - data. -2. When the image data is requested, the ``ImageFile.load`` method is - called, which sets up a decoder for each tile and feeds the data to - it. - -An image plugin should contain a format handler derived from the -:py:class:`PIL.ImageFile.ImageFile` base class. This class should provide an -``_open`` method, which reads the file header and set at least the internal -``_size`` and ``_mode`` attributes so that :py:attr:`~PIL.Image.Image.mode` and -:py:attr:`~PIL.Image.Image.size` are populated. To be able to load the file, -the method must also create a list of ``tile`` descriptors, which contain a -decoder name, extents of the tile, and any decoder-specific data. The format -handler class must be explicitly registered, via a call to the -:py:mod:`~PIL.Image` module. - -.. note:: For performance reasons, it is important that the - ``_open`` method quickly rejects files that do not have the - appropriate contents. - -Example -------- - -The following plugin supports a simple format, which has a 128-byte header -consisting of the words “SPAM†followed by the width, height, and pixel size in -bits. The header fields are separated by spaces. The image data follows -directly after the header, and can be either bi-level, grayscale, or 24-bit -true color. - -**SpamImagePlugin.py**:: - - from PIL import Image, ImageFile - - - def _accept(prefix): - return prefix[:4] == b"SPAM" - - - class SpamImageFile(ImageFile.ImageFile): - - format = "SPAM" - format_description = "Spam raster image" - - def _open(self): - - header = self.fp.read(128).split() - - # size in pixels (width, height) - self._size = int(header[1]), int(header[2]) - - # mode setting - bits = int(header[3]) - if bits == 1: - self._mode = "1" - elif bits == 8: - self._mode = "L" - elif bits == 24: - self._mode = "RGB" - else: - msg = "unknown number of bits" - raise SyntaxError(msg) - - # data descriptor - self.tile = [("raw", (0, 0) + self.size, 128, (self.mode, 0, 1))] - - - Image.register_open(SpamImageFile.format, SpamImageFile, _accept) - - Image.register_extensions( - SpamImageFile.format, - [ - ".spam", - ".spa", # DOS version - ], - ) - - -The format handler must always set the internal ``_size`` and ``_mode`` -attributes so that :py:attr:`~PIL.Image.Image.size` and -:py:attr:`~PIL.Image.Image.mode` are populated. If these are not set, the file -cannot be opened. To simplify the plugin, the calling code considers exceptions -like :py:exc:`SyntaxError`, :py:exc:`KeyError`, :py:exc:`IndexError`, -:py:exc:`EOFError` and :py:exc:`struct.error` as a failure to identify the -file. - -Note that the image plugin must be explicitly registered using -:py:func:`PIL.Image.register_open`. Although not required, it is also a good -idea to register any extensions used by this format. - -Once the plugin has been imported, it can be used:: - - from PIL import Image - import SpamImagePlugin - - with Image.open("hopper.spam") as im: - pass - -The ``tile`` attribute ----------------------- - -To be able to read the file as well as just identifying it, the ``tile`` -attribute must also be set. This attribute consists of a list of tile -descriptors, where each descriptor specifies how data should be loaded to a -given region in the image. - -In most cases, only a single descriptor is used, covering the full image. -:py:class:`.PsdImagePlugin.PsdImageFile` uses multiple tiles to combine -channels within a single layer, given that the channels are stored separately, -one after the other. - -The tile descriptor is a 4-tuple with the following contents:: - - (decoder, region, offset, parameters) - -The fields are used as follows: - -**decoder** - Specifies which decoder to use. The ``raw`` decoder used here supports - uncompressed data, in a variety of pixel formats. For more information on - this decoder, see the description below. - - A list of C decoders can be seen under codecs section of the function array - in :file:`_imaging.c`. Python decoders are registered within the relevant - plugins. - -**region** - A 4-tuple specifying where to store data in the image. - -**offset** - Byte offset from the beginning of the file to image data. - -**parameters** - Parameters to the decoder. The contents of this field depends on the - decoder specified by the first field in the tile descriptor tuple. If the - decoder doesn’t need any parameters, use :data:`None` for this field. - -Note that the ``tile`` attribute contains a list of tile descriptors, -not just a single descriptor. - -Decoders -======== - -The raw decoder ---------------- - -The ``raw`` decoder is used to read uncompressed data from an image file. It -can be used with most uncompressed file formats, such as PPM, BMP, uncompressed -TIFF, and many others. To use the raw decoder with the -:py:func:`PIL.Image.frombytes` function, use the following syntax:: - - image = Image.frombytes( - mode, size, data, "raw", - raw_mode, stride, orientation - ) - -When used in a tile descriptor, the parameter field should look like:: - - (raw_mode, stride, orientation) - -The fields are used as follows: - -**raw_mode** - The pixel layout used in the file, and is used to properly convert data to - PIL’s internal layout. For a summary of the available formats, see the - table below. - -**stride** - The distance in bytes between two consecutive lines in the image. If 0, the - image is assumed to be packed (no padding between lines). If omitted, the - stride defaults to 0. - -**orientation** - Whether the first line in the image is the top line on the screen (1), or - the bottom line (-1). If omitted, the orientation defaults to 1. - -The **raw mode** field is used to determine how the data should be unpacked to -match PIL’s internal pixel layout. PIL supports a large set of raw modes; for a -complete list, see the table in the :file:`Unpack.c` module. The following -table describes some commonly used **raw modes**: - -+-----------+-------------------------------------------------------------------+ -| mode | description | -+===========+===================================================================+ -| ``1`` | | 1-bit bilevel, stored with the leftmost pixel in the most | -| | | significant bit. 0 means black, 1 means white. | -+-----------+-------------------------------------------------------------------+ -| ``1;I`` | | 1-bit inverted bilevel, stored with the leftmost pixel in the | -| | | most significant bit. 0 means white, 1 means black. | -+-----------+-------------------------------------------------------------------+ -| ``1;R`` | | 1-bit reversed bilevel, stored with the leftmost pixel in the | -| | | least significant bit. 0 means black, 1 means white. | -+-----------+-------------------------------------------------------------------+ -| ``L`` | 8-bit grayscale. 0 means black, 255 means white. | -+-----------+-------------------------------------------------------------------+ -| ``L;I`` | 8-bit inverted grayscale. 0 means white, 255 means black. | -+-----------+-------------------------------------------------------------------+ -| ``P`` | 8-bit palette-mapped image. | -+-----------+-------------------------------------------------------------------+ -| ``RGB`` | 24-bit true colour, stored as (red, green, blue). | -+-----------+-------------------------------------------------------------------+ -| ``BGR`` | 24-bit true colour, stored as (blue, green, red). | -+-----------+-------------------------------------------------------------------+ -| ``RGBX`` | | 24-bit true colour, stored as (red, green, blue, pad). The pad | -| | | pixels may vary. | -+-----------+-------------------------------------------------------------------+ -| ``RGB;L`` | | 24-bit true colour, line interleaved (first all red pixels, then| -| | | all green pixels, finally all blue pixels). | -+-----------+-------------------------------------------------------------------+ - -Note that for the most common cases, the raw mode is simply the same as the mode. - -The Python Imaging Library supports many other decoders, including JPEG, PNG, -and PackBits. For details, see the :file:`decode.c` source file, and the -standard plugin implementations provided with the library. - -Decoding floating point data ----------------------------- - -PIL provides some special mechanisms to allow you to load a wide variety of -formats into a mode ``F`` (floating point) image memory. - -You can use the ``raw`` decoder to read images where data is packed in any -standard machine data type, using one of the following raw modes: - -============ ======================================= -mode description -============ ======================================= -``F`` 32-bit native floating point. -``F;8`` 8-bit unsigned integer. -``F;8S`` 8-bit signed integer. -``F;16`` 16-bit little endian unsigned integer. -``F;16S`` 16-bit little endian signed integer. -``F;16B`` 16-bit big endian unsigned integer. -``F;16BS`` 16-bit big endian signed integer. -``F;16N`` 16-bit native unsigned integer. -``F;16NS`` 16-bit native signed integer. -``F;32`` 32-bit little endian unsigned integer. -``F;32S`` 32-bit little endian signed integer. -``F;32B`` 32-bit big endian unsigned integer. -``F;32BS`` 32-bit big endian signed integer. -``F;32N`` 32-bit native unsigned integer. -``F;32NS`` 32-bit native signed integer. -``F;32F`` 32-bit little endian floating point. -``F;32BF`` 32-bit big endian floating point. -``F;32NF`` 32-bit native floating point. -``F;64F`` 64-bit little endian floating point. -``F;64BF`` 64-bit big endian floating point. -``F;64NF`` 64-bit native floating point. -============ ======================================= - -The bit decoder ---------------- - -If the raw decoder cannot handle your format, PIL also provides a special “bit†-decoder that can be used to read various packed formats into a floating point -image memory. - -To use the bit decoder with the :py:func:`PIL.Image.frombytes` function, use -the following syntax:: - - image = Image.frombytes( - mode, size, data, "bit", - bits, pad, fill, sign, orientation - ) - -When used in a tile descriptor, the parameter field should look like:: - - (bits, pad, fill, sign, orientation) - -The fields are used as follows: - -**bits** - Number of bits per pixel (2-32). No default. - -**pad** - Padding between lines, in bits. This is either 0 if there is no padding, or - 8 if lines are padded to full bytes. If omitted, the pad value defaults to - 8. - -**fill** - Controls how data are added to, and stored from, the decoder bit buffer. - -**fill=0** - Add bytes to the LSB end of the decoder buffer; store pixels from the MSB - end. - -**fill=1** - Add bytes to the MSB end of the decoder buffer; store pixels from the MSB - end. - -**fill=2** - Add bytes to the LSB end of the decoder buffer; store pixels from the LSB - end. - -**fill=3** - Add bytes to the MSB end of the decoder buffer; store pixels from the LSB - end. - - If omitted, the fill order defaults to 0. - -**sign** - If non-zero, bit fields are sign extended. If zero or omitted, bit fields - are unsigned. - -**orientation** - Whether the first line in the image is the top line on the screen (1), or - the bottom line (-1). If omitted, the orientation defaults to 1. - -.. _file-codecs: - -Writing Your Own File Codec in C -================================ - -There are 3 stages in a file codec's lifetime: - -1. Setup: Pillow looks for a function in the decoder or encoder registry, - falling back to a function named ``[codecname]_decoder`` or - ``[codecname]_encoder`` on the internal core image object. That function is - called with the ``args`` tuple from the ``tile``. - -2. Transforming: The codec's ``decode`` or ``encode`` function is repeatedly - called with chunks of image data. - -3. Cleanup: If the codec has registered a cleanup function, it will - be called at the end of the transformation process, even if there was an - exception raised. - - -Setup ------ - -The current conventions are that the codec setup function is named -``PyImaging_[codecname]DecoderNew`` or ``PyImaging_[codecname]EncoderNew`` -and defined in ``decode.c`` or ``encode.c``. The Python binding for it is -named ``[codecname]_decoder`` or ``[codecname]_encoder`` and is set up from -within the ``_imaging.c`` file in the codecs section of the function array. - -The setup function needs to call ``PyImaging_DecoderNew`` or -``PyImaging_EncoderNew`` and at the very least, set the ``decode`` or -``encode`` function pointer. The fields of interest in this object are: - -**decode**/**encode** - Function pointer to the decode or encode function, which has access to - ``im``, ``state``, and the buffer of data to be transformed. - -**cleanup** - Function pointer to the cleanup function, has access to ``state``. - -**im** - The target image, will be set by Pillow. - -**state** - An ImagingCodecStateInstance, will be set by Pillow. The ``context`` - member is an opaque struct that can be used by the codec to store - any format specific state or options. - -**pulls_fd**/**pushes_fd** - If the decoder has ``pulls_fd`` or the encoder has ``pushes_fd`` set to 1, - ``state->fd`` will be a pointer to the Python file like object. The codec may - use the functions in ``codec_fd.c`` to read or write directly with the file - like object rather than have the data pushed through a buffer. - - .. versionadded:: 3.3.0 - - -Transforming ------------- - -The decode or encode function is called with the target (core) image, the codec -state structure, and a buffer of data to be transformed. - -It is the codec's responsibility to pull as much data as possible out of the -buffer and return the number of bytes consumed. The next call to the codec will -include the previous unconsumed tail. The codec function will be called -multiple times as the data processed. - -Alternatively, if ``pulls_fd`` or ``pushes_fd`` is set, then the decode or -encode function is called once, with an empty buffer. It is the codec's -responsibility to transform the entire tile in that one call. Using this will -provide a codec with more freedom, but that freedom may mean increased memory -usage if the entire tile is held in memory at once by the codec. - -If an error occurs, set ``state->errcode`` and return -1. - -Return -1 on success, without setting the errcode. - -Cleanup -------- - -The cleanup function is called after the codec returns a negative -value, or if there is an error. This function should free any allocated -memory and release any resources from external libraries. - -.. _file-codecs-py: - -Writing Your Own File Codec in Python -===================================== - -Python file decoders and encoders should derive from -:py:class:`PIL.ImageFile.PyDecoder` and :py:class:`PIL.ImageFile.PyEncoder` -respectively, and should at least override the decode or encode method. -They should be registered using :py:meth:`PIL.Image.register_decoder` and -:py:meth:`PIL.Image.register_encoder`. As in the C implementation of -the file codecs, there are three stages in the lifetime of a -Python-based file codec: - -1. Setup: Pillow looks for the codec in the decoder or encoder registry, then - instantiates the class. - -2. Transforming: The instance's ``decode`` method is repeatedly called with - a buffer of data to be interpreted, or the ``encode`` method is repeatedly - called with the size of data to be output. - - Alternatively, if the decoder's ``_pulls_fd`` property (or the encoder's - ``_pushes_fd`` property) is set to ``True``, then ``decode`` and ``encode`` - will only be called once. In the decoder, ``self.fd`` can be used to access - the file-like object. Using this will provide a codec with more freedom, but - that freedom may mean increased memory usage if entire file is held in - memory at once by the codec. - - In ``decode``, once the data has been interpreted, ``set_as_raw`` can be - used to populate the image. - -3. Cleanup: The instance's ``cleanup`` method is called once the transformation - is complete. This can be used to clean up any resources used by the codec. - - If you set ``_pulls_fd`` or ``_pushes_fd`` to ``True`` however, then you - probably chose to perform any cleanup tasks at the end of ``decode`` or - ``encode``. - -For an example :py:class:`PIL.ImageFile.PyDecoder`, see `DdsImagePlugin -`_. -For a plugin that uses both :py:class:`PIL.ImageFile.PyDecoder` and -:py:class:`PIL.ImageFile.PyEncoder`, see `BlpImagePlugin -`_ +.. _image-plugins: + +Writing Your Own Image Plugin +============================= + +Pillow uses a plugin model which allows you to add your own +decoders and encoders to the library, without any changes to the library +itself. Such plugins usually have names like :file:`XxxImagePlugin.py`, +where ``Xxx`` is a unique format name (usually an abbreviation). + +.. warning:: Pillow >= 2.1.0 no longer automatically imports any file + in the Python path with a name ending in + :file:`ImagePlugin.py`. You will need to import your + image plugin manually. + +Pillow decodes files in two stages: + +1. It loops over the available image plugins in the loaded order, and + calls the plugin's ``_accept`` function with the first 16 bytes of + the file. If the ``_accept`` function returns true, the plugin's + ``_open`` method is called to set up the image metadata and image + tiles. The ``_open`` method is not for decoding the actual image + data. +2. When the image data is requested, the ``ImageFile.load`` method is + called, which sets up a decoder for each tile and feeds the data to + it. + +An image plugin should contain a format handler derived from the +:py:class:`PIL.ImageFile.ImageFile` base class. This class should provide an +``_open`` method, which reads the file header and set at least the internal +``_size`` and ``_mode`` attributes so that :py:attr:`~PIL.Image.Image.mode` and +:py:attr:`~PIL.Image.Image.size` are populated. To be able to load the file, +the method must also create a list of ``tile`` descriptors, which contain a +decoder name, extents of the tile, and any decoder-specific data. The format +handler class must be explicitly registered, via a call to the +:py:mod:`~PIL.Image` module. + +.. note:: For performance reasons, it is important that the + ``_open`` method quickly rejects files that do not have the + appropriate contents. + +Example +------- + +The following plugin supports a simple format, which has a 128-byte header +consisting of the words “SPAM†followed by the width, height, and pixel size in +bits. The header fields are separated by spaces. The image data follows +directly after the header, and can be either bi-level, grayscale, or 24-bit +true color. + +**SpamImagePlugin.py**:: + + from PIL import Image, ImageFile + + + def _accept(prefix): + return prefix[:4] == b"SPAM" + + + class SpamImageFile(ImageFile.ImageFile): + + format = "SPAM" + format_description = "Spam raster image" + + def _open(self): + + header = self.fp.read(128).split() + + # size in pixels (width, height) + self._size = int(header[1]), int(header[2]) + + # mode setting + bits = int(header[3]) + if bits == 1: + self._mode = "1" + elif bits == 8: + self._mode = "L" + elif bits == 24: + self._mode = "RGB" + else: + msg = "unknown number of bits" + raise SyntaxError(msg) + + # data descriptor + self.tile = [("raw", (0, 0) + self.size, 128, (self.mode, 0, 1))] + + + Image.register_open(SpamImageFile.format, SpamImageFile, _accept) + + Image.register_extensions( + SpamImageFile.format, + [ + ".spam", + ".spa", # DOS version + ], + ) + + +The format handler must always set the internal ``_size`` and ``_mode`` +attributes so that :py:attr:`~PIL.Image.Image.size` and +:py:attr:`~PIL.Image.Image.mode` are populated. If these are not set, the file +cannot be opened. To simplify the plugin, the calling code considers exceptions +like :py:exc:`SyntaxError`, :py:exc:`KeyError`, :py:exc:`IndexError`, +:py:exc:`EOFError` and :py:exc:`struct.error` as a failure to identify the +file. + +Note that the image plugin must be explicitly registered using +:py:func:`PIL.Image.register_open`. Although not required, it is also a good +idea to register any extensions used by this format. + +Once the plugin has been imported, it can be used:: + + from PIL import Image + import SpamImagePlugin + + with Image.open("hopper.spam") as im: + pass + +The ``tile`` attribute +---------------------- + +To be able to read the file as well as just identifying it, the ``tile`` +attribute must also be set. This attribute consists of a list of tile +descriptors, where each descriptor specifies how data should be loaded to a +given region in the image. + +In most cases, only a single descriptor is used, covering the full image. +:py:class:`.PsdImagePlugin.PsdImageFile` uses multiple tiles to combine +channels within a single layer, given that the channels are stored separately, +one after the other. + +The tile descriptor is a 4-tuple with the following contents:: + + (decoder, region, offset, parameters) + +The fields are used as follows: + +**decoder** + Specifies which decoder to use. The ``raw`` decoder used here supports + uncompressed data, in a variety of pixel formats. For more information on + this decoder, see the description below. + + A list of C decoders can be seen under codecs section of the function array + in :file:`_imaging.c`. Python decoders are registered within the relevant + plugins. + +**region** + A 4-tuple specifying where to store data in the image. + +**offset** + Byte offset from the beginning of the file to image data. + +**parameters** + Parameters to the decoder. The contents of this field depends on the + decoder specified by the first field in the tile descriptor tuple. If the + decoder doesn’t need any parameters, use :data:`None` for this field. + +Note that the ``tile`` attribute contains a list of tile descriptors, +not just a single descriptor. + +Decoders +======== + +The raw decoder +--------------- + +The ``raw`` decoder is used to read uncompressed data from an image file. It +can be used with most uncompressed file formats, such as PPM, BMP, uncompressed +TIFF, and many others. To use the raw decoder with the +:py:func:`PIL.Image.frombytes` function, use the following syntax:: + + image = Image.frombytes( + mode, size, data, "raw", + raw_mode, stride, orientation + ) + +When used in a tile descriptor, the parameter field should look like:: + + (raw_mode, stride, orientation) + +The fields are used as follows: + +**raw_mode** + The pixel layout used in the file, and is used to properly convert data to + PIL’s internal layout. For a summary of the available formats, see the + table below. + +**stride** + The distance in bytes between two consecutive lines in the image. If 0, the + image is assumed to be packed (no padding between lines). If omitted, the + stride defaults to 0. + +**orientation** + Whether the first line in the image is the top line on the screen (1), or + the bottom line (-1). If omitted, the orientation defaults to 1. + +The **raw mode** field is used to determine how the data should be unpacked to +match PIL’s internal pixel layout. PIL supports a large set of raw modes; for a +complete list, see the table in the :file:`Unpack.c` module. The following +table describes some commonly used **raw modes**: + ++-----------+-------------------------------------------------------------------+ +| mode | description | ++===========+===================================================================+ +| ``1`` | | 1-bit bilevel, stored with the leftmost pixel in the most | +| | | significant bit. 0 means black, 1 means white. | ++-----------+-------------------------------------------------------------------+ +| ``1;I`` | | 1-bit inverted bilevel, stored with the leftmost pixel in the | +| | | most significant bit. 0 means white, 1 means black. | ++-----------+-------------------------------------------------------------------+ +| ``1;R`` | | 1-bit reversed bilevel, stored with the leftmost pixel in the | +| | | least significant bit. 0 means black, 1 means white. | ++-----------+-------------------------------------------------------------------+ +| ``L`` | 8-bit grayscale. 0 means black, 255 means white. | ++-----------+-------------------------------------------------------------------+ +| ``L;I`` | 8-bit inverted grayscale. 0 means white, 255 means black. | ++-----------+-------------------------------------------------------------------+ +| ``P`` | 8-bit palette-mapped image. | ++-----------+-------------------------------------------------------------------+ +| ``RGB`` | 24-bit true colour, stored as (red, green, blue). | ++-----------+-------------------------------------------------------------------+ +| ``BGR`` | 24-bit true colour, stored as (blue, green, red). | ++-----------+-------------------------------------------------------------------+ +| ``RGBX`` | | 24-bit true colour, stored as (red, green, blue, pad). The pad | +| | | pixels may vary. | ++-----------+-------------------------------------------------------------------+ +| ``RGB;L`` | | 24-bit true colour, line interleaved (first all red pixels, then| +| | | all green pixels, finally all blue pixels). | ++-----------+-------------------------------------------------------------------+ + +Note that for the most common cases, the raw mode is simply the same as the mode. + +The Python Imaging Library supports many other decoders, including JPEG, PNG, +and PackBits. For details, see the :file:`decode.c` source file, and the +standard plugin implementations provided with the library. + +Decoding floating point data +---------------------------- + +PIL provides some special mechanisms to allow you to load a wide variety of +formats into a mode ``F`` (floating point) image memory. + +You can use the ``raw`` decoder to read images where data is packed in any +standard machine data type, using one of the following raw modes: + +============ ======================================= +mode description +============ ======================================= +``F`` 32-bit native floating point. +``F;8`` 8-bit unsigned integer. +``F;8S`` 8-bit signed integer. +``F;16`` 16-bit little endian unsigned integer. +``F;16S`` 16-bit little endian signed integer. +``F;16B`` 16-bit big endian unsigned integer. +``F;16BS`` 16-bit big endian signed integer. +``F;16N`` 16-bit native unsigned integer. +``F;16NS`` 16-bit native signed integer. +``F;32`` 32-bit little endian unsigned integer. +``F;32S`` 32-bit little endian signed integer. +``F;32B`` 32-bit big endian unsigned integer. +``F;32BS`` 32-bit big endian signed integer. +``F;32N`` 32-bit native unsigned integer. +``F;32NS`` 32-bit native signed integer. +``F;32F`` 32-bit little endian floating point. +``F;32BF`` 32-bit big endian floating point. +``F;32NF`` 32-bit native floating point. +``F;64F`` 64-bit little endian floating point. +``F;64BF`` 64-bit big endian floating point. +``F;64NF`` 64-bit native floating point. +============ ======================================= + +The bit decoder +--------------- + +If the raw decoder cannot handle your format, PIL also provides a special “bit†+decoder that can be used to read various packed formats into a floating point +image memory. + +To use the bit decoder with the :py:func:`PIL.Image.frombytes` function, use +the following syntax:: + + image = Image.frombytes( + mode, size, data, "bit", + bits, pad, fill, sign, orientation + ) + +When used in a tile descriptor, the parameter field should look like:: + + (bits, pad, fill, sign, orientation) + +The fields are used as follows: + +**bits** + Number of bits per pixel (2-32). No default. + +**pad** + Padding between lines, in bits. This is either 0 if there is no padding, or + 8 if lines are padded to full bytes. If omitted, the pad value defaults to + 8. + +**fill** + Controls how data are added to, and stored from, the decoder bit buffer. + +**fill=0** + Add bytes to the LSB end of the decoder buffer; store pixels from the MSB + end. + +**fill=1** + Add bytes to the MSB end of the decoder buffer; store pixels from the MSB + end. + +**fill=2** + Add bytes to the LSB end of the decoder buffer; store pixels from the LSB + end. + +**fill=3** + Add bytes to the MSB end of the decoder buffer; store pixels from the LSB + end. + + If omitted, the fill order defaults to 0. + +**sign** + If non-zero, bit fields are sign extended. If zero or omitted, bit fields + are unsigned. + +**orientation** + Whether the first line in the image is the top line on the screen (1), or + the bottom line (-1). If omitted, the orientation defaults to 1. + +.. _file-codecs: + +Writing Your Own File Codec in C +================================ + +There are 3 stages in a file codec's lifetime: + +1. Setup: Pillow looks for a function in the decoder or encoder registry, + falling back to a function named ``[codecname]_decoder`` or + ``[codecname]_encoder`` on the internal core image object. That function is + called with the ``args`` tuple from the ``tile``. + +2. Transforming: The codec's ``decode`` or ``encode`` function is repeatedly + called with chunks of image data. + +3. Cleanup: If the codec has registered a cleanup function, it will + be called at the end of the transformation process, even if there was an + exception raised. + + +Setup +----- + +The current conventions are that the codec setup function is named +``PyImaging_[codecname]DecoderNew`` or ``PyImaging_[codecname]EncoderNew`` +and defined in ``decode.c`` or ``encode.c``. The Python binding for it is +named ``[codecname]_decoder`` or ``[codecname]_encoder`` and is set up from +within the ``_imaging.c`` file in the codecs section of the function array. + +The setup function needs to call ``PyImaging_DecoderNew`` or +``PyImaging_EncoderNew`` and at the very least, set the ``decode`` or +``encode`` function pointer. The fields of interest in this object are: + +**decode**/**encode** + Function pointer to the decode or encode function, which has access to + ``im``, ``state``, and the buffer of data to be transformed. + +**cleanup** + Function pointer to the cleanup function, has access to ``state``. + +**im** + The target image, will be set by Pillow. + +**state** + An ImagingCodecStateInstance, will be set by Pillow. The ``context`` + member is an opaque struct that can be used by the codec to store + any format specific state or options. + +**pulls_fd**/**pushes_fd** + If the decoder has ``pulls_fd`` or the encoder has ``pushes_fd`` set to 1, + ``state->fd`` will be a pointer to the Python file like object. The codec may + use the functions in ``codec_fd.c`` to read or write directly with the file + like object rather than have the data pushed through a buffer. + + .. versionadded:: 3.3.0 + + +Transforming +------------ + +The decode or encode function is called with the target (core) image, the codec +state structure, and a buffer of data to be transformed. + +It is the codec's responsibility to pull as much data as possible out of the +buffer and return the number of bytes consumed. The next call to the codec will +include the previous unconsumed tail. The codec function will be called +multiple times as the data processed. + +Alternatively, if ``pulls_fd`` or ``pushes_fd`` is set, then the decode or +encode function is called once, with an empty buffer. It is the codec's +responsibility to transform the entire tile in that one call. Using this will +provide a codec with more freedom, but that freedom may mean increased memory +usage if the entire tile is held in memory at once by the codec. + +If an error occurs, set ``state->errcode`` and return -1. + +Return -1 on success, without setting the errcode. + +Cleanup +------- + +The cleanup function is called after the codec returns a negative +value, or if there is an error. This function should free any allocated +memory and release any resources from external libraries. + +.. _file-codecs-py: + +Writing Your Own File Codec in Python +===================================== + +Python file decoders and encoders should derive from +:py:class:`PIL.ImageFile.PyDecoder` and :py:class:`PIL.ImageFile.PyEncoder` +respectively, and should at least override the decode or encode method. +They should be registered using :py:meth:`PIL.Image.register_decoder` and +:py:meth:`PIL.Image.register_encoder`. As in the C implementation of +the file codecs, there are three stages in the lifetime of a +Python-based file codec: + +1. Setup: Pillow looks for the codec in the decoder or encoder registry, then + instantiates the class. + +2. Transforming: The instance's ``decode`` method is repeatedly called with + a buffer of data to be interpreted, or the ``encode`` method is repeatedly + called with the size of data to be output. + + Alternatively, if the decoder's ``_pulls_fd`` property (or the encoder's + ``_pushes_fd`` property) is set to ``True``, then ``decode`` and ``encode`` + will only be called once. In the decoder, ``self.fd`` can be used to access + the file-like object. Using this will provide a codec with more freedom, but + that freedom may mean increased memory usage if entire file is held in + memory at once by the codec. + + In ``decode``, once the data has been interpreted, ``set_as_raw`` can be + used to populate the image. + +3. Cleanup: The instance's ``cleanup`` method is called once the transformation + is complete. This can be used to clean up any resources used by the codec. + + If you set ``_pulls_fd`` or ``_pushes_fd`` to ``True`` however, then you + probably chose to perform any cleanup tasks at the end of ``decode`` or + ``encode``. + +For an example :py:class:`PIL.ImageFile.PyDecoder`, see `DdsImagePlugin +`_. +For a plugin that uses both :py:class:`PIL.ImageFile.PyDecoder` and +:py:class:`PIL.ImageFile.PyEncoder`, see `BlpImagePlugin +`_ diff --git a/docs/index.rst b/docs/index.rst index 3a12953f0..a27290fe9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,108 +1,108 @@ -Pillow -====== - -Pillow is the friendly PIL fork by `Jeffrey A. Clark and contributors `_. PIL is the Python Imaging Library by Fredrik Lundh and contributors. - -Pillow for enterprise is available via the Tidelift Subscription. `Learn more `_. - -.. image:: https://readthedocs.org/projects/pillow/badge/?version=latest - :target: https://pillow.readthedocs.io/?badge=latest - :alt: Documentation Status - -.. image:: https://github.com/python-pillow/Pillow/workflows/Lint/badge.svg - :target: https://github.com/python-pillow/Pillow/actions/workflows/lint.yml - :alt: GitHub Actions build status (Lint) - -.. image:: https://github.com/python-pillow/Pillow/workflows/Test%20Docker/badge.svg - :target: https://github.com/python-pillow/Pillow/actions/workflows/test-docker.yml - :alt: GitHub Actions build status (Test Docker) - -.. image:: https://github.com/python-pillow/Pillow/workflows/Test/badge.svg - :target: https://github.com/python-pillow/Pillow/actions/workflows/test.yml - :alt: GitHub Actions build status (Test Linux and macOS) - -.. image:: https://github.com/python-pillow/Pillow/workflows/Test%20Windows/badge.svg - :target: https://github.com/python-pillow/Pillow/actions/workflows/test-windows.yml - :alt: GitHub Actions build status (Test Windows) - -.. image:: https://github.com/python-pillow/Pillow/workflows/Test%20MinGW/badge.svg - :target: https://github.com/python-pillow/Pillow/actions/workflows/test-mingw.yml - :alt: GitHub Actions build status (Test MinGW) - -.. image:: https://github.com/python-pillow/Pillow/workflows/Test%20Cygwin/badge.svg - :target: https://github.com/python-pillow/Pillow/actions/workflows/test-cygwin.yml - :alt: GitHub Actions build status (Test Cygwin) - -.. image:: https://img.shields.io/appveyor/build/python-pillow/Pillow/main.svg?label=Windows%20build - :target: https://ci.appveyor.com/project/python-pillow/Pillow - :alt: AppVeyor CI build status (Windows) - -.. image:: https://github.com/python-pillow/Pillow/workflows/Wheels/badge.svg - :target: https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml - :alt: GitHub Actions build status (Wheels) - -.. image:: https://codecov.io/gh/python-pillow/Pillow/branch/main/graph/badge.svg - :target: https://app.codecov.io/gh/python-pillow/Pillow - :alt: Code coverage - -.. image:: https://zenodo.org/badge/17549/python-pillow/Pillow.svg - :target: https://zenodo.org/badge/latestdoi/17549/python-pillow/Pillow - :alt: Zenodo - -.. image:: https://tidelift.com/badges/package/pypi/pillow?style=flat - :target: https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=badge - :alt: Tidelift - -.. image:: https://oss-fuzz-build-logs.storage.googleapis.com/badges/pillow.svg - :target: https://bugs.chromium.org/p/oss-fuzz/issues/list?sort=-opened&can=1&q=proj:pillow - :alt: Fuzzing Status - -.. image:: https://img.shields.io/pypi/v/pillow.svg - :target: https://pypi.org/project/pillow/ - :alt: Latest PyPI version - -.. image:: https://img.shields.io/pypi/dm/pillow.svg - :target: https://pypi.org/project/pillow/ - :alt: Number of PyPI downloads - -.. image:: https://www.bestpractices.dev/projects/6331/badge - :target: https://www.bestpractices.dev/projects/6331 - :alt: OpenSSF Best Practices - -.. image:: https://badges.gitter.im/python-pillow/Pillow.svg - :target: https://gitter.im/python-pillow/Pillow?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge - :alt: Join the chat at https://gitter.im/python-pillow/Pillow - -.. image:: https://img.shields.io/badge/publish-on%20Mastodon-595aff.svg - :target: https://fosstodon.org/@pillow - :alt: Follow on https://fosstodon.org/@pillow - -.. raw:: html - - - -Overview -======== - -The Python Imaging Library adds image processing capabilities to your Python interpreter. - -This library provides extensive file format support, an efficient internal representation, and fairly powerful image processing capabilities. - -The core image library is designed for fast access to data stored in a few basic pixel formats. It should provide a solid foundation for a general image processing tool. - -.. toctree:: - :maxdepth: 2 - - installation/index.rst - handbook/index.rst - reference/index.rst - porting.rst - about.rst - releasenotes/index.rst - deprecations.rst - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` +Pillow +====== + +Pillow is the friendly PIL fork by `Jeffrey A. Clark and contributors `_. PIL is the Python Imaging Library by Fredrik Lundh and contributors. + +Pillow for enterprise is available via the Tidelift Subscription. `Learn more `_. + +.. image:: https://readthedocs.org/projects/pillow/badge/?version=latest + :target: https://pillow.readthedocs.io/?badge=latest + :alt: Documentation Status + +.. image:: https://github.com/python-pillow/Pillow/workflows/Lint/badge.svg + :target: https://github.com/python-pillow/Pillow/actions/workflows/lint.yml + :alt: GitHub Actions build status (Lint) + +.. image:: https://github.com/python-pillow/Pillow/workflows/Test%20Docker/badge.svg + :target: https://github.com/python-pillow/Pillow/actions/workflows/test-docker.yml + :alt: GitHub Actions build status (Test Docker) + +.. image:: https://github.com/python-pillow/Pillow/workflows/Test/badge.svg + :target: https://github.com/python-pillow/Pillow/actions/workflows/test.yml + :alt: GitHub Actions build status (Test Linux and macOS) + +.. image:: https://github.com/python-pillow/Pillow/workflows/Test%20Windows/badge.svg + :target: https://github.com/python-pillow/Pillow/actions/workflows/test-windows.yml + :alt: GitHub Actions build status (Test Windows) + +.. image:: https://github.com/python-pillow/Pillow/workflows/Test%20MinGW/badge.svg + :target: https://github.com/python-pillow/Pillow/actions/workflows/test-mingw.yml + :alt: GitHub Actions build status (Test MinGW) + +.. image:: https://github.com/python-pillow/Pillow/workflows/Test%20Cygwin/badge.svg + :target: https://github.com/python-pillow/Pillow/actions/workflows/test-cygwin.yml + :alt: GitHub Actions build status (Test Cygwin) + +.. image:: https://img.shields.io/appveyor/build/python-pillow/Pillow/main.svg?label=Windows%20build + :target: https://ci.appveyor.com/project/python-pillow/Pillow + :alt: AppVeyor CI build status (Windows) + +.. image:: https://github.com/python-pillow/Pillow/workflows/Wheels/badge.svg + :target: https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml + :alt: GitHub Actions build status (Wheels) + +.. image:: https://codecov.io/gh/python-pillow/Pillow/branch/main/graph/badge.svg + :target: https://app.codecov.io/gh/python-pillow/Pillow + :alt: Code coverage + +.. image:: https://zenodo.org/badge/17549/python-pillow/Pillow.svg + :target: https://zenodo.org/badge/latestdoi/17549/python-pillow/Pillow + :alt: Zenodo + +.. image:: https://tidelift.com/badges/package/pypi/pillow?style=flat + :target: https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=badge + :alt: Tidelift + +.. image:: https://oss-fuzz-build-logs.storage.googleapis.com/badges/pillow.svg + :target: https://bugs.chromium.org/p/oss-fuzz/issues/list?sort=-opened&can=1&q=proj:pillow + :alt: Fuzzing Status + +.. image:: https://img.shields.io/pypi/v/pillow.svg + :target: https://pypi.org/project/pillow/ + :alt: Latest PyPI version + +.. image:: https://img.shields.io/pypi/dm/pillow.svg + :target: https://pypi.org/project/pillow/ + :alt: Number of PyPI downloads + +.. image:: https://www.bestpractices.dev/projects/6331/badge + :target: https://www.bestpractices.dev/projects/6331 + :alt: OpenSSF Best Practices + +.. image:: https://badges.gitter.im/python-pillow/Pillow.svg + :target: https://gitter.im/python-pillow/Pillow?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge + :alt: Join the chat at https://gitter.im/python-pillow/Pillow + +.. image:: https://img.shields.io/badge/publish-on%20Mastodon-595aff.svg + :target: https://fosstodon.org/@pillow + :alt: Follow on https://fosstodon.org/@pillow + +.. raw:: html + + + +Overview +======== + +The Python Imaging Library adds image processing capabilities to your Python interpreter. + +This library provides extensive file format support, an efficient internal representation, and fairly powerful image processing capabilities. + +The core image library is designed for fast access to data stored in a few basic pixel formats. It should provide a solid foundation for a general image processing tool. + +.. toctree:: + :maxdepth: 2 + + installation/index.rst + handbook/index.rst + reference/index.rst + porting.rst + about.rst + releasenotes/index.rst + deprecations.rst + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` diff --git a/docs/installation.rst b/docs/installation.rst index b4bf2fa00..d0fdec7b9 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -1,29 +1,29 @@ -:orphan: - -Installation -============ - -Basic Installation ------------------- - -.. Note:: This section has moved to :ref:`basic-installation`. Please update references accordingly. - -Python Support --------------- - -.. Note:: This section has moved to :ref:`python-support`. Please update references accordingly. - -Platform Support ----------------- - -.. Note:: This section has moved to :ref:`platform-support`. Please update references accordingly. - -Building From Source --------------------- - -.. Note:: This section has moved to :ref:`building-from-source`. Please update references accordingly. - -Old Versions ------------- - -.. Note:: This section has moved to :ref:`old-versions`. Please update references accordingly. +:orphan: + +Installation +============ + +Basic Installation +------------------ + +.. Note:: This section has moved to :ref:`basic-installation`. Please update references accordingly. + +Python Support +-------------- + +.. Note:: This section has moved to :ref:`python-support`. Please update references accordingly. + +Platform Support +---------------- + +.. Note:: This section has moved to :ref:`platform-support`. Please update references accordingly. + +Building From Source +-------------------- + +.. Note:: This section has moved to :ref:`building-from-source`. Please update references accordingly. + +Old Versions +------------ + +.. Note:: This section has moved to :ref:`old-versions`. Please update references accordingly. diff --git a/docs/installation/basic-installation.rst b/docs/installation/basic-installation.rst index 01981aa4f..3bffc1a59 100644 --- a/docs/installation/basic-installation.rst +++ b/docs/installation/basic-installation.rst @@ -1,97 +1,97 @@ -.. raw:: html - - - -.. _basic-installation: - -Basic Installation -================== - -.. note:: - - The following instructions will install Pillow with support for - most common image formats. See :ref:`external-libraries` for a - full list of external libraries supported. - -Install Pillow with :command:`pip`:: - - python3 -m pip install --upgrade pip - python3 -m pip install --upgrade Pillow - -Optionally, install :pypi:`defusedxml` for Pillow to read XMP data, -and :pypi:`olefile` for Pillow to read FPX and MIC images:: - - python3 -m pip install --upgrade defusedxml olefile - - -.. tab:: Linux - - We provide binaries for Linux for each of the supported Python - versions in the manylinux wheel format. These include support for all - optional libraries except libimagequant. Raqm support requires - FriBiDi to be installed separately:: - - python3 -m pip install --upgrade pip - python3 -m pip install --upgrade Pillow - - Most major Linux distributions, including Fedora, Ubuntu and ArchLinux - also include Pillow in packages that previously contained PIL e.g. - ``python-imaging``. Debian splits it into two packages, ``python3-pil`` - and ``python3-pil.imagetk``. - -.. tab:: macOS - - We provide binaries for macOS for each of the supported Python - versions in the wheel format. These include support for all optional - libraries except libimagequant. Raqm support requires - FriBiDi to be installed separately:: - - python3 -m pip install --upgrade pip - python3 -m pip install --upgrade Pillow - - While we provide binaries for both x86-64 and arm64, we do not provide universal2 - binaries. However, it is simple to combine our current binaries to create one:: - - python3 -m pip download --only-binary=:all: --platform macosx_10_10_x86_64 Pillow - python3 -m pip download --only-binary=:all: --platform macosx_11_0_arm64 Pillow - python3 -m pip install delocate - - Then, with the names of the downloaded wheels, use Python to combine them:: - - from delocate.fuse import fuse_wheels - fuse_wheels('Pillow-9.4.0-2-cp39-cp39-macosx_10_10_x86_64.whl', 'Pillow-9.4.0-cp39-cp39-macosx_11_0_arm64.whl', 'Pillow-9.4.0-cp39-cp39-macosx_11_0_universal2.whl') - -.. tab:: Windows - - We provide Pillow binaries for Windows compiled for the matrix of supported - Pythons in the wheel format. These include x86, x86-64 and arm64 versions - (with the exception of Python 3.8 on arm64). These binaries include support - for all optional libraries except libimagequant and libxcb. Raqm support - requires FriBiDi to be installed separately:: - - python3 -m pip install --upgrade pip - python3 -m pip install --upgrade Pillow - - To install Pillow in MSYS2, see :ref:`building-from-source`. - -.. tab:: FreeBSD - - Pillow can be installed on FreeBSD via the official Ports or Packages systems: - - **Ports**:: - - cd /usr/ports/graphics/py-pillow && make install clean - - **Packages**:: - - pkg install py38-pillow - - .. note:: - - The `Pillow FreeBSD port - `_ and packages - are tested by the ports team with all supported FreeBSD versions. +.. raw:: html + + + +.. _basic-installation: + +Basic Installation +================== + +.. note:: + + The following instructions will install Pillow with support for + most common image formats. See :ref:`external-libraries` for a + full list of external libraries supported. + +Install Pillow with :command:`pip`:: + + python3 -m pip install --upgrade pip + python3 -m pip install --upgrade Pillow + +Optionally, install :pypi:`defusedxml` for Pillow to read XMP data, +and :pypi:`olefile` for Pillow to read FPX and MIC images:: + + python3 -m pip install --upgrade defusedxml olefile + + +.. tab:: Linux + + We provide binaries for Linux for each of the supported Python + versions in the manylinux wheel format. These include support for all + optional libraries except libimagequant. Raqm support requires + FriBiDi to be installed separately:: + + python3 -m pip install --upgrade pip + python3 -m pip install --upgrade Pillow + + Most major Linux distributions, including Fedora, Ubuntu and ArchLinux + also include Pillow in packages that previously contained PIL e.g. + ``python-imaging``. Debian splits it into two packages, ``python3-pil`` + and ``python3-pil.imagetk``. + +.. tab:: macOS + + We provide binaries for macOS for each of the supported Python + versions in the wheel format. These include support for all optional + libraries except libimagequant. Raqm support requires + FriBiDi to be installed separately:: + + python3 -m pip install --upgrade pip + python3 -m pip install --upgrade Pillow + + While we provide binaries for both x86-64 and arm64, we do not provide universal2 + binaries. However, it is simple to combine our current binaries to create one:: + + python3 -m pip download --only-binary=:all: --platform macosx_10_10_x86_64 Pillow + python3 -m pip download --only-binary=:all: --platform macosx_11_0_arm64 Pillow + python3 -m pip install delocate + + Then, with the names of the downloaded wheels, use Python to combine them:: + + from delocate.fuse import fuse_wheels + fuse_wheels('Pillow-9.4.0-2-cp39-cp39-macosx_10_10_x86_64.whl', 'Pillow-9.4.0-cp39-cp39-macosx_11_0_arm64.whl', 'Pillow-9.4.0-cp39-cp39-macosx_11_0_universal2.whl') + +.. tab:: Windows + + We provide Pillow binaries for Windows compiled for the matrix of supported + Pythons in the wheel format. These include x86, x86-64 and arm64 versions + (with the exception of Python 3.8 on arm64). These binaries include support + for all optional libraries except libimagequant and libxcb. Raqm support + requires FriBiDi to be installed separately:: + + python3 -m pip install --upgrade pip + python3 -m pip install --upgrade Pillow + + To install Pillow in MSYS2, see :ref:`building-from-source`. + +.. tab:: FreeBSD + + Pillow can be installed on FreeBSD via the official Ports or Packages systems: + + **Ports**:: + + cd /usr/ports/graphics/py-pillow && make install clean + + **Packages**:: + + pkg install py38-pillow + + .. note:: + + The `Pillow FreeBSD port + `_ and packages + are tested by the ports team with all supported FreeBSD versions. diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index 7f7dfa6ff..97e090993 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -1,317 +1,317 @@ -.. raw:: html - - - -.. _building-from-source: - -Building From Source -==================== - -.. _external-libraries: - -External Libraries ------------------- - -.. note:: - - You **do not need to install all supported external libraries** to - use Pillow's basic features. **Zlib** and **libjpeg** are required - by default. - -.. note:: - - There are Dockerfiles in our `Docker images repo - `_ to install the - dependencies for some operating systems. - -Many of Pillow's features require external libraries: - -* **libjpeg** provides JPEG functionality. - - * Pillow has been tested with libjpeg versions **6b**, **8**, **9-9d** and - libjpeg-turbo version **8**. - * Starting with Pillow 3.0.0, libjpeg is required by default. It can be - disabled with the ``-C jpeg=disable`` flag. - -* **zlib** provides access to compressed PNGs - - * Starting with Pillow 3.0.0, zlib is required by default. It can be - disabled with the ``-C zlib=disable`` flag. - -* **libtiff** provides compressed TIFF functionality - - * Pillow has been tested with libtiff versions **3.x** and **4.0-4.6.0** - -* **libfreetype** provides type related services - -* **littlecms** provides color management - - * Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and - above uses liblcms2. Tested with **1.19** and **2.7-2.16**. - -* **libwebp** provides the WebP format. - - * Pillow has been tested with version **0.1.3**, which does not read - transparent WebP files. Versions **0.3.0** and above support - transparency. - -* **openjpeg** provides JPEG 2000 functionality. - - * Pillow has been tested with openjpeg **2.0.0**, **2.1.0**, **2.3.1**, - **2.4.0**, **2.5.0** and **2.5.2**. - * Pillow does **not** support the earlier **1.5** series which ships - with Debian Jessie. - -* **libimagequant** provides improved color quantization - - * Pillow has been tested with libimagequant **2.6-4.3.1** - * Libimagequant is licensed GPLv3, which is more restrictive than - the Pillow license, therefore we will not be distributing binaries - with libimagequant support enabled. - -* **libraqm** provides complex text layout support. - - * libraqm provides bidirectional text support (using FriBiDi), - shaping (using HarfBuzz), and proper script itemization. As a - result, Raqm can support most writing systems covered by Unicode. - * libraqm depends on the following libraries: FreeType, HarfBuzz, - FriBiDi, make sure that you install them before installing libraqm - if not available as package in your system. - * Setting text direction or font features is not supported without libraqm. - * Pillow wheels since version 8.2.0 include a modified version of libraqm that - loads libfribidi at runtime if it is installed. - On Windows this requires compiling FriBiDi and installing ``fribidi.dll`` - into a directory listed in the `Dynamic-link library search order (Microsoft Learn) - `_ - (``fribidi-0.dll`` or ``libfribidi-0.dll`` are also detected). - See `Build Options`_ to see how to build this version. - * Previous versions of Pillow (5.0.0 to 8.1.2) linked libraqm dynamically at runtime. - -* **libxcb** provides X11 screengrab support. - -.. tab:: Linux - - If you didn't build Python from source, make sure you have Python's - development libraries installed. - - In Debian or Ubuntu:: - - sudo apt-get install python3-dev python3-setuptools - - In Fedora, the command is:: - - sudo dnf install python3-devel redhat-rpm-config - - In Alpine, the command is:: - - sudo apk add python3-dev py3-setuptools - - .. Note:: ``redhat-rpm-config`` is required on Fedora 23, but not earlier versions. - - Prerequisites for **Ubuntu 16.04 LTS - 22.04 LTS** are installed with:: - - sudo apt-get install libtiff5-dev libjpeg8-dev libopenjp2-7-dev zlib1g-dev \ - libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python3-tk \ - libharfbuzz-dev libfribidi-dev libxcb1-dev - - To install libraqm, ``sudo apt-get install meson`` and then see - ``depends/install_raqm.sh``. - - Prerequisites are installed on recent **Red Hat**, **CentOS** or **Fedora** with:: - - sudo dnf install libtiff-devel libjpeg-devel openjpeg2-devel zlib-devel \ - freetype-devel lcms2-devel libwebp-devel tcl-devel tk-devel \ - harfbuzz-devel fribidi-devel libraqm-devel libimagequant-devel libxcb-devel - - Note that the package manager may be yum or DNF, depending on the - exact distribution. - - Prerequisites are installed for **Alpine** with:: - - sudo apk add tiff-dev jpeg-dev openjpeg-dev zlib-dev freetype-dev lcms2-dev \ - libwebp-dev tcl-dev tk-dev harfbuzz-dev fribidi-dev libimagequant-dev \ - libxcb-dev libpng-dev - - See also the ``Dockerfile``\s in the Test Infrastructure repo - (https://github.com/python-pillow/docker-images) for a known working - install process for other tested distros. - -.. tab:: macOS - - The Xcode command line tools are required to compile portions of - Pillow. The tools are installed by running ``xcode-select --install`` - from the command line. The command line tools are required even if you - have the full Xcode package installed. It may be necessary to run - ``sudo xcodebuild -license`` to accept the license prior to using the - tools. - - The easiest way to install external libraries is via `Homebrew - `_. After you install Homebrew, run:: - - brew install libjpeg libtiff little-cms2 openjpeg webp - - To install libraqm on macOS use Homebrew to install its dependencies:: - - brew install freetype harfbuzz fribidi - - Then see ``depends/install_raqm_cmake.sh`` to install libraqm. - -.. tab:: Windows - - We recommend you use prebuilt wheels from PyPI. - If you wish to compile Pillow manually, you can use the build scripts - in the ``winbuild`` directory used for CI testing and development. - These scripts require Visual Studio 2017 or newer and NASM. - - The scripts also install Pillow from the local copy of the source code, so the - `Installing`_ instructions will not be necessary afterwards. - -.. tab:: Windows using MSYS2/MinGW - - To build Pillow using MSYS2, make sure you run the **MSYS2 MinGW 32-bit** or - **MSYS2 MinGW 64-bit** console, *not* **MSYS2** directly. - - The following instructions target the 64-bit build, for 32-bit - replace all occurrences of ``mingw-w64-x86_64-`` with ``mingw-w64-i686-``. - - Make sure you have Python and GCC installed:: - - pacman -S \ - mingw-w64-x86_64-gcc \ - mingw-w64-x86_64-python3 \ - mingw-w64-x86_64-python3-pip \ - mingw-w64-x86_64-python3-setuptools - - Prerequisites are installed on **MSYS2 MinGW 64-bit** with:: - - pacman -S \ - mingw-w64-x86_64-libjpeg-turbo \ - mingw-w64-x86_64-zlib \ - mingw-w64-x86_64-libtiff \ - mingw-w64-x86_64-freetype \ - mingw-w64-x86_64-lcms2 \ - mingw-w64-x86_64-libwebp \ - mingw-w64-x86_64-openjpeg2 \ - mingw-w64-x86_64-libimagequant \ - mingw-w64-x86_64-libraqm - - 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 - -.. tab:: FreeBSD - - .. Note:: Only FreeBSD 10 and 11 tested - - Make sure you have Python's development libraries installed:: - - sudo pkg install python3 - - Prerequisites are installed on **FreeBSD 10 or 11** with:: - - sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi libxcb - - Then see ``depends/install_raqm_cmake.sh`` to install libraqm. - -.. tab:: Android - - Basic Android support has been added for compilation within the Termux - environment. The dependencies can be installed by:: - - pkg install -y python ndk-sysroot clang make \ - libjpeg-turbo - - This has been tested within the Termux app on ChromeOS, on x86. - -Installing ----------- - -Once you have installed the prerequisites, to install Pillow from the source -code on PyPI, run:: - - python3 -m pip install --upgrade pip - python3 -m pip install --upgrade Pillow --no-binary :all: - -If the prerequisites are installed in the standard library locations -for your machine (e.g. :file:`/usr` or :file:`/usr/local`), no -additional configuration should be required. If they are installed in -a non-standard location, you may need to configure setuptools to use -those locations by editing :file:`setup.py` or -:file:`pyproject.toml`, or by adding environment variables on the command -line:: - - CFLAGS="-I/usr/pkg/include" python3 -m pip install --upgrade Pillow --no-binary :all: - -If Pillow has been previously built without the required -prerequisites, it may be necessary to manually clear the pip cache or -build without cache using the ``--no-cache-dir`` option to force a -build with newly installed external libraries. - -If you would like to install from a local copy of the source code instead, you -can clone from GitHub with ``git clone https://github.com/python-pillow/Pillow`` -or download and extract the `compressed archive from PyPI`_. - -After navigating to the Pillow directory, run:: - - python3 -m pip install --upgrade pip - python3 -m pip install . - -.. _compressed archive from PyPI: https://pypi.org/project/pillow/#files - -Build Options -^^^^^^^^^^^^^ - -* Config setting: ``-C parallel=n``. Can also be given - with environment variable: ``MAX_CONCURRENCY=n``. Pillow can use - multiprocessing to build the extension. Setting ``-C parallel=n`` - sets the number of CPUs to use to ``n``, or can disable parallel building by - using a setting of 1. By default, it uses 4 CPUs, or if 4 are not - available, as many as are present. - -* Config settings: ``-C zlib=disable``, ``-C jpeg=disable``, - ``-C tiff=disable``, ``-C freetype=disable``, ``-C raqm=disable``, - ``-C lcms=disable``, ``-C webp=disable``, ``-C webpmux=disable``, - ``-C jpeg2000=disable``, ``-C imagequant=disable``, ``-C xcb=disable``. - Disable building the corresponding feature even if the development - libraries are present on the building machine. - -* Config settings: ``-C zlib=enable``, ``-C jpeg=enable``, - ``-C tiff=enable``, ``-C freetype=enable``, ``-C raqm=enable``, - ``-C lcms=enable``, ``-C webp=enable``, ``-C webpmux=enable``, - ``-C jpeg2000=enable``, ``-C imagequant=enable``, ``-C xcb=enable``. - Require that the corresponding feature is built. The build will raise - an exception if the libraries are not found. Webpmux (WebP metadata) - relies on WebP support. Tcl and Tk also must be used together. - -* Config settings: ``-C raqm=vendor``, ``-C fribidi=vendor``. - These flags are used to compile a modified version of libraqm and - a shim that dynamically loads libfribidi at runtime. These are - used to compile the standard Pillow wheels. Compiling libraqm requires - a C99-compliant compiler. - -* Config setting: ``-C platform-guessing=disable``. Skips all of the - platform dependent guessing of include and library directories for - automated build systems that configure the proper paths in the - environment variables (e.g. Buildroot). - -* Config setting: ``-C debug=true``. Adds a debugging flag to the include and - library search process to dump all paths searched for and found to stdout. - - -Sample usage:: - - python3 -m pip install --upgrade Pillow -C [feature]=enable - -.. _old-versions: - -Old Versions -============ - -You can download old distributions from the `release history at PyPI -`_ and by direct URL access -eg. https://pypi.org/project/pillow/1.0/. +.. raw:: html + + + +.. _building-from-source: + +Building From Source +==================== + +.. _external-libraries: + +External Libraries +------------------ + +.. note:: + + You **do not need to install all supported external libraries** to + use Pillow's basic features. **Zlib** and **libjpeg** are required + by default. + +.. note:: + + There are Dockerfiles in our `Docker images repo + `_ to install the + dependencies for some operating systems. + +Many of Pillow's features require external libraries: + +* **libjpeg** provides JPEG functionality. + + * Pillow has been tested with libjpeg versions **6b**, **8**, **9-9d** and + libjpeg-turbo version **8**. + * Starting with Pillow 3.0.0, libjpeg is required by default. It can be + disabled with the ``-C jpeg=disable`` flag. + +* **zlib** provides access to compressed PNGs + + * Starting with Pillow 3.0.0, zlib is required by default. It can be + disabled with the ``-C zlib=disable`` flag. + +* **libtiff** provides compressed TIFF functionality + + * Pillow has been tested with libtiff versions **3.x** and **4.0-4.6.0** + +* **libfreetype** provides type related services + +* **littlecms** provides color management + + * Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and + above uses liblcms2. Tested with **1.19** and **2.7-2.16**. + +* **libwebp** provides the WebP format. + + * Pillow has been tested with version **0.1.3**, which does not read + transparent WebP files. Versions **0.3.0** and above support + transparency. + +* **openjpeg** provides JPEG 2000 functionality. + + * Pillow has been tested with openjpeg **2.0.0**, **2.1.0**, **2.3.1**, + **2.4.0**, **2.5.0** and **2.5.2**. + * Pillow does **not** support the earlier **1.5** series which ships + with Debian Jessie. + +* **libimagequant** provides improved color quantization + + * Pillow has been tested with libimagequant **2.6-4.3.1** + * Libimagequant is licensed GPLv3, which is more restrictive than + the Pillow license, therefore we will not be distributing binaries + with libimagequant support enabled. + +* **libraqm** provides complex text layout support. + + * libraqm provides bidirectional text support (using FriBiDi), + shaping (using HarfBuzz), and proper script itemization. As a + result, Raqm can support most writing systems covered by Unicode. + * libraqm depends on the following libraries: FreeType, HarfBuzz, + FriBiDi, make sure that you install them before installing libraqm + if not available as package in your system. + * Setting text direction or font features is not supported without libraqm. + * Pillow wheels since version 8.2.0 include a modified version of libraqm that + loads libfribidi at runtime if it is installed. + On Windows this requires compiling FriBiDi and installing ``fribidi.dll`` + into a directory listed in the `Dynamic-link library search order (Microsoft Learn) + `_ + (``fribidi-0.dll`` or ``libfribidi-0.dll`` are also detected). + See `Build Options`_ to see how to build this version. + * Previous versions of Pillow (5.0.0 to 8.1.2) linked libraqm dynamically at runtime. + +* **libxcb** provides X11 screengrab support. + +.. tab:: Linux + + If you didn't build Python from source, make sure you have Python's + development libraries installed. + + In Debian or Ubuntu:: + + sudo apt-get install python3-dev python3-setuptools + + In Fedora, the command is:: + + sudo dnf install python3-devel redhat-rpm-config + + In Alpine, the command is:: + + sudo apk add python3-dev py3-setuptools + + .. Note:: ``redhat-rpm-config`` is required on Fedora 23, but not earlier versions. + + Prerequisites for **Ubuntu 16.04 LTS - 22.04 LTS** are installed with:: + + sudo apt-get install libtiff5-dev libjpeg8-dev libopenjp2-7-dev zlib1g-dev \ + libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python3-tk \ + libharfbuzz-dev libfribidi-dev libxcb1-dev + + To install libraqm, ``sudo apt-get install meson`` and then see + ``depends/install_raqm.sh``. + + Prerequisites are installed on recent **Red Hat**, **CentOS** or **Fedora** with:: + + sudo dnf install libtiff-devel libjpeg-devel openjpeg2-devel zlib-devel \ + freetype-devel lcms2-devel libwebp-devel tcl-devel tk-devel \ + harfbuzz-devel fribidi-devel libraqm-devel libimagequant-devel libxcb-devel + + Note that the package manager may be yum or DNF, depending on the + exact distribution. + + Prerequisites are installed for **Alpine** with:: + + sudo apk add tiff-dev jpeg-dev openjpeg-dev zlib-dev freetype-dev lcms2-dev \ + libwebp-dev tcl-dev tk-dev harfbuzz-dev fribidi-dev libimagequant-dev \ + libxcb-dev libpng-dev + + See also the ``Dockerfile``\s in the Test Infrastructure repo + (https://github.com/python-pillow/docker-images) for a known working + install process for other tested distros. + +.. tab:: macOS + + The Xcode command line tools are required to compile portions of + Pillow. The tools are installed by running ``xcode-select --install`` + from the command line. The command line tools are required even if you + have the full Xcode package installed. It may be necessary to run + ``sudo xcodebuild -license`` to accept the license prior to using the + tools. + + The easiest way to install external libraries is via `Homebrew + `_. After you install Homebrew, run:: + + brew install libjpeg libtiff little-cms2 openjpeg webp + + To install libraqm on macOS use Homebrew to install its dependencies:: + + brew install freetype harfbuzz fribidi + + Then see ``depends/install_raqm_cmake.sh`` to install libraqm. + +.. tab:: Windows + + We recommend you use prebuilt wheels from PyPI. + If you wish to compile Pillow manually, you can use the build scripts + in the ``winbuild`` directory used for CI testing and development. + These scripts require Visual Studio 2017 or newer and NASM. + + The scripts also install Pillow from the local copy of the source code, so the + `Installing`_ instructions will not be necessary afterwards. + +.. tab:: Windows using MSYS2/MinGW + + To build Pillow using MSYS2, make sure you run the **MSYS2 MinGW 32-bit** or + **MSYS2 MinGW 64-bit** console, *not* **MSYS2** directly. + + The following instructions target the 64-bit build, for 32-bit + replace all occurrences of ``mingw-w64-x86_64-`` with ``mingw-w64-i686-``. + + Make sure you have Python and GCC installed:: + + pacman -S \ + mingw-w64-x86_64-gcc \ + mingw-w64-x86_64-python3 \ + mingw-w64-x86_64-python3-pip \ + mingw-w64-x86_64-python3-setuptools + + Prerequisites are installed on **MSYS2 MinGW 64-bit** with:: + + pacman -S \ + mingw-w64-x86_64-libjpeg-turbo \ + mingw-w64-x86_64-zlib \ + mingw-w64-x86_64-libtiff \ + mingw-w64-x86_64-freetype \ + mingw-w64-x86_64-lcms2 \ + mingw-w64-x86_64-libwebp \ + mingw-w64-x86_64-openjpeg2 \ + mingw-w64-x86_64-libimagequant \ + mingw-w64-x86_64-libraqm + + 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 + +.. tab:: FreeBSD + + .. Note:: Only FreeBSD 10 and 11 tested + + Make sure you have Python's development libraries installed:: + + sudo pkg install python3 + + Prerequisites are installed on **FreeBSD 10 or 11** with:: + + sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi libxcb + + Then see ``depends/install_raqm_cmake.sh`` to install libraqm. + +.. tab:: Android + + Basic Android support has been added for compilation within the Termux + environment. The dependencies can be installed by:: + + pkg install -y python ndk-sysroot clang make \ + libjpeg-turbo + + This has been tested within the Termux app on ChromeOS, on x86. + +Installing +---------- + +Once you have installed the prerequisites, to install Pillow from the source +code on PyPI, run:: + + python3 -m pip install --upgrade pip + python3 -m pip install --upgrade Pillow --no-binary :all: + +If the prerequisites are installed in the standard library locations +for your machine (e.g. :file:`/usr` or :file:`/usr/local`), no +additional configuration should be required. If they are installed in +a non-standard location, you may need to configure setuptools to use +those locations by editing :file:`setup.py` or +:file:`pyproject.toml`, or by adding environment variables on the command +line:: + + CFLAGS="-I/usr/pkg/include" python3 -m pip install --upgrade Pillow --no-binary :all: + +If Pillow has been previously built without the required +prerequisites, it may be necessary to manually clear the pip cache or +build without cache using the ``--no-cache-dir`` option to force a +build with newly installed external libraries. + +If you would like to install from a local copy of the source code instead, you +can clone from GitHub with ``git clone https://github.com/python-pillow/Pillow`` +or download and extract the `compressed archive from PyPI`_. + +After navigating to the Pillow directory, run:: + + python3 -m pip install --upgrade pip + python3 -m pip install . + +.. _compressed archive from PyPI: https://pypi.org/project/pillow/#files + +Build Options +^^^^^^^^^^^^^ + +* Config setting: ``-C parallel=n``. Can also be given + with environment variable: ``MAX_CONCURRENCY=n``. Pillow can use + multiprocessing to build the extension. Setting ``-C parallel=n`` + sets the number of CPUs to use to ``n``, or can disable parallel building by + using a setting of 1. By default, it uses 4 CPUs, or if 4 are not + available, as many as are present. + +* Config settings: ``-C zlib=disable``, ``-C jpeg=disable``, + ``-C tiff=disable``, ``-C freetype=disable``, ``-C raqm=disable``, + ``-C lcms=disable``, ``-C webp=disable``, ``-C webpmux=disable``, + ``-C jpeg2000=disable``, ``-C imagequant=disable``, ``-C xcb=disable``. + Disable building the corresponding feature even if the development + libraries are present on the building machine. + +* Config settings: ``-C zlib=enable``, ``-C jpeg=enable``, + ``-C tiff=enable``, ``-C freetype=enable``, ``-C raqm=enable``, + ``-C lcms=enable``, ``-C webp=enable``, ``-C webpmux=enable``, + ``-C jpeg2000=enable``, ``-C imagequant=enable``, ``-C xcb=enable``. + Require that the corresponding feature is built. The build will raise + an exception if the libraries are not found. Webpmux (WebP metadata) + relies on WebP support. Tcl and Tk also must be used together. + +* Config settings: ``-C raqm=vendor``, ``-C fribidi=vendor``. + These flags are used to compile a modified version of libraqm and + a shim that dynamically loads libfribidi at runtime. These are + used to compile the standard Pillow wheels. Compiling libraqm requires + a C99-compliant compiler. + +* Config setting: ``-C platform-guessing=disable``. Skips all of the + platform dependent guessing of include and library directories for + automated build systems that configure the proper paths in the + environment variables (e.g. Buildroot). + +* Config setting: ``-C debug=true``. Adds a debugging flag to the include and + library search process to dump all paths searched for and found to stdout. + + +Sample usage:: + + python3 -m pip install --upgrade Pillow -C [feature]=enable + +.. _old-versions: + +Old Versions +============ + +You can download old distributions from the `release history at PyPI +`_ and by direct URL access +eg. https://pypi.org/project/pillow/1.0/. diff --git a/docs/installation/index.rst b/docs/installation/index.rst index a94204b6b..d60a3ecc2 100644 --- a/docs/installation/index.rst +++ b/docs/installation/index.rst @@ -1,10 +1,10 @@ -Installation -============ - -.. toctree:: - :maxdepth: 2 - - basic-installation - python-support - platform-support - building-from-source +Installation +============ + +.. toctree:: + :maxdepth: 2 + + basic-installation + python-support + platform-support + building-from-source diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index ed25d33a6..d4fdbc2a5 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -1,168 +1,168 @@ -.. _platform-support: - -Platform Support -================ - -Current platform support for Pillow. Binary distributions are -contributed for each release on a volunteer basis, but the source -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 ------------------------------- - -These platforms are built and tested for every change. - -+----------------------------------+----------------------------+---------------------+ -| Operating system | Tested Python versions | Tested architecture | -+==================================+============================+=====================+ -| Alpine | 3.9 | x86-64 | -+----------------------------------+----------------------------+---------------------+ -| Amazon Linux 2 | 3.9 | x86-64 | -+----------------------------------+----------------------------+---------------------+ -| Amazon Linux 2023 | 3.9 | x86-64 | -+----------------------------------+----------------------------+---------------------+ -| Arch | 3.9 | x86-64 | -+----------------------------------+----------------------------+---------------------+ -| CentOS Stream 9 | 3.9 | x86-64 | -+----------------------------------+----------------------------+---------------------+ -| Debian 11 Bullseye | 3.9 | x86-64 | -+----------------------------------+----------------------------+---------------------+ -| Debian 12 Bookworm | 3.11 | x86, x86-64 | -+----------------------------------+----------------------------+---------------------+ -| Fedora 39 | 3.12 | x86-64 | -+----------------------------------+----------------------------+---------------------+ -| Fedora 40 | 3.12 | x86-64 | -+----------------------------------+----------------------------+---------------------+ -| Gentoo | 3.9 | x86-64 | -+----------------------------------+----------------------------+---------------------+ -| macOS 13 Ventura | 3.8, 3.9 | x86-64 | -+----------------------------------+----------------------------+---------------------+ -| macOS 14 Sonoma | 3.10, 3.11, 3.12, 3.13, | arm64 | -| | PyPy3 | | -+----------------------------------+----------------------------+---------------------+ -| Ubuntu Linux 20.04 LTS (Focal) | 3.8 | x86-64 | -+----------------------------------+----------------------------+---------------------+ -| Ubuntu Linux 22.04 LTS (Jammy) | 3.8, 3.9, 3.10, 3.11, | x86-64 | -| | 3.12, 3.13, PyPy3 | | -| +----------------------------+---------------------+ -| | 3.10 | arm64v8 | -+----------------------------------+----------------------------+---------------------+ -| Ubuntu Linux 24.04 LTS (Noble) | 3.12 | x86-64, ppc64le, | -| | | s390x | -+----------------------------------+----------------------------+---------------------+ -| Windows Server 2016 | 3.8 | x86-64 | -+----------------------------------+----------------------------+---------------------+ -| Windows Server 2022 | 3.8, 3.9, 3.10, 3.11, | x86-64 | -| | 3.12, 3.13, PyPy3 | | -| +----------------------------+---------------------+ -| | 3.12 | x86 | -| +----------------------------+---------------------+ -| | 3.9 (MinGW) | x86-64 | -| +----------------------------+---------------------+ -| | 3.8, 3.9 (Cygwin) | x86-64 | -+----------------------------------+----------------------------+---------------------+ - - -Other Platforms ---------------- - -These platforms have been reported to work at the versions mentioned. - -.. note:: - - Contributors please test Pillow on your platform then update this - document and send a pull request. - -+----------------------------------+----------------------------+------------------+--------------+ -| Operating system | | Tested Python | | Latest tested | | Tested | -| | | versions | | Pillow version | | processors | -+==================================+============================+==================+==============+ -| macOS 14 Sonoma | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.3.0 |arm | -+----------------------------------+----------------------------+------------------+--------------+ -| macOS 13 Ventura | 3.8, 3.9, 3.10, 3.11 | 10.0.1 |arm | -| +----------------------------+------------------+ | -| | 3.7 | 9.5.0 | | -+----------------------------------+----------------------------+------------------+--------------+ -| macOS 12 Monterey | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |arm | -+----------------------------------+----------------------------+------------------+--------------+ -| macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10 | 8.4.0 |arm | -| +----------------------------+------------------+--------------+ -| | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.4.0 |x86-64 | -| +----------------------------+------------------+ | -| | 3.6 | 8.4.0 | | -+----------------------------------+----------------------------+------------------+--------------+ -| macOS 10.15 Catalina | 3.6, 3.7, 3.8, 3.9 | 8.3.2 |x86-64 | -| +----------------------------+------------------+ | -| | 3.5 | 7.2.0 | | -+----------------------------------+----------------------------+------------------+--------------+ -| macOS 10.14 Mojave | 3.5, 3.6, 3.7, 3.8 | 7.2.0 |x86-64 | -| +----------------------------+------------------+ | -| | 2.7 | 6.0.0 | | -| +----------------------------+------------------+ | -| | 3.4 | 5.4.1 | | -+----------------------------------+----------------------------+------------------+--------------+ -| macOS 10.13 High Sierra | 2.7, 3.4, 3.5, 3.6 | 4.2.1 |x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| macOS 10.12 Sierra | 2.7, 3.4, 3.5, 3.6 | 4.1.1 |x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| Mac OS X 10.11 El Capitan | 2.7, 3.4, 3.5, 3.6, 3.7 | 5.4.1 |x86-64 | -| +----------------------------+------------------+ | -| | 3.3 | 4.1.0 | | -+----------------------------------+----------------------------+------------------+--------------+ -| Mac OS X 10.9 Mavericks | 2.7, 3.2, 3.3, 3.4 | 3.0.0 |x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| Mac OS X 10.8 Mountain Lion | 2.6, 2.7, 3.2, 3.3 | |x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| Redhat Linux 6 | 2.6 | |x86 | -+----------------------------------+----------------------------+------------------+--------------+ -| CentOS 6.3 | 2.7, 3.3 | |x86 | -+----------------------------------+----------------------------+------------------+--------------+ -| CentOS 8 | 3.9 | 9.0.0 |x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| Fedora 23 | 2.7, 3.4 | 3.1.0 |x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| Ubuntu Linux 12.04 LTS (Precise) | | 2.6, 3.2, 3.3, 3.4, 3.5 | 3.4.1 |x86,x86-64 | -| | | PyPy5.3.1, PyPy3 v2.4.0 | | | -| +----------------------------+------------------+--------------+ -| | 2.7 | 4.3.0 |x86-64 | -| +----------------------------+------------------+--------------+ -| | 2.7, 3.2 | 3.4.1 |ppc | -+----------------------------------+----------------------------+------------------+--------------+ -| Ubuntu Linux 10.04 LTS (Lucid) | 2.6 | 2.3.0 |x86,x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| Debian 8.2 Jessie | 2.7, 3.4 | 3.1.0 |x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| Raspbian Jessie | 2.7, 3.4 | 3.1.0 |arm | -+----------------------------------+----------------------------+------------------+--------------+ -| Raspbian Stretch | 2.7, 3.5 | 4.0.0 |arm | -+----------------------------------+----------------------------+------------------+--------------+ -| Raspberry Pi OS | 3.6, 3.7, 3.8, 3.9 | 8.2.0 |arm | -| +----------------------------+------------------+ | -| | 2.7 | 6.2.2 | | -+----------------------------------+----------------------------+------------------+--------------+ -| Gentoo Linux | 2.7, 3.2 | 2.1.0 |x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| FreeBSD 11.1 | 2.7, 3.4, 3.5, 3.6 | 4.3.0 |x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| FreeBSD 10.3 | 2.7, 3.4, 3.5 | 4.2.0 |x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| FreeBSD 10.2 | 2.7, 3.4 | 3.1.0 |x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| Windows 11 | 3.9, 3.10, 3.11, 3.12 | 10.2.0 |arm64 | -+----------------------------------+----------------------------+------------------+--------------+ -| Windows 11 Pro | 3.11, 3.12 | 10.2.0 |x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| Windows 10 | 3.7 | 7.1.0 |x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| Windows 10/Cygwin 3.3 | 3.6, 3.7, 3.8, 3.9 | 8.4.0 |x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| Windows 8.1 Pro | 2.6, 2.7, 3.2, 3.3, 3.4 | 2.4.0 |x86,x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| Windows 8 Pro | 2.6, 2.7, 3.2, 3.3, 3.4a3 | 2.2.0 |x86,x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| Windows 7 Professional | 3.7 | 7.0.0 |x86,x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| Windows Server 2008 R2 Enterprise| 3.3 | |x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ +.. _platform-support: + +Platform Support +================ + +Current platform support for Pillow. Binary distributions are +contributed for each release on a volunteer basis, but the source +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 +------------------------------ + +These platforms are built and tested for every change. + ++----------------------------------+----------------------------+---------------------+ +| Operating system | Tested Python versions | Tested architecture | ++==================================+============================+=====================+ +| Alpine | 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Amazon Linux 2 | 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Amazon Linux 2023 | 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Arch | 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| CentOS Stream 9 | 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Debian 11 Bullseye | 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Debian 12 Bookworm | 3.11 | x86, x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Fedora 39 | 3.12 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Fedora 40 | 3.12 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Gentoo | 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| macOS 13 Ventura | 3.8, 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| macOS 14 Sonoma | 3.10, 3.11, 3.12, 3.13, | arm64 | +| | PyPy3 | | ++----------------------------------+----------------------------+---------------------+ +| Ubuntu Linux 20.04 LTS (Focal) | 3.8 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Ubuntu Linux 22.04 LTS (Jammy) | 3.8, 3.9, 3.10, 3.11, | x86-64 | +| | 3.12, 3.13, PyPy3 | | +| +----------------------------+---------------------+ +| | 3.10 | arm64v8 | ++----------------------------------+----------------------------+---------------------+ +| Ubuntu Linux 24.04 LTS (Noble) | 3.12 | x86-64, ppc64le, | +| | | s390x | ++----------------------------------+----------------------------+---------------------+ +| Windows Server 2016 | 3.8 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Windows Server 2022 | 3.8, 3.9, 3.10, 3.11, | x86-64 | +| | 3.12, 3.13, PyPy3 | | +| +----------------------------+---------------------+ +| | 3.12 | x86 | +| +----------------------------+---------------------+ +| | 3.9 (MinGW) | x86-64 | +| +----------------------------+---------------------+ +| | 3.8, 3.9 (Cygwin) | x86-64 | ++----------------------------------+----------------------------+---------------------+ + + +Other Platforms +--------------- + +These platforms have been reported to work at the versions mentioned. + +.. note:: + + Contributors please test Pillow on your platform then update this + document and send a pull request. + ++----------------------------------+----------------------------+------------------+--------------+ +| Operating system | | Tested Python | | Latest tested | | Tested | +| | | versions | | Pillow version | | processors | ++==================================+============================+==================+==============+ +| macOS 14 Sonoma | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.3.0 |arm | ++----------------------------------+----------------------------+------------------+--------------+ +| macOS 13 Ventura | 3.8, 3.9, 3.10, 3.11 | 10.0.1 |arm | +| +----------------------------+------------------+ | +| | 3.7 | 9.5.0 | | ++----------------------------------+----------------------------+------------------+--------------+ +| macOS 12 Monterey | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |arm | ++----------------------------------+----------------------------+------------------+--------------+ +| macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10 | 8.4.0 |arm | +| +----------------------------+------------------+--------------+ +| | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.4.0 |x86-64 | +| +----------------------------+------------------+ | +| | 3.6 | 8.4.0 | | ++----------------------------------+----------------------------+------------------+--------------+ +| macOS 10.15 Catalina | 3.6, 3.7, 3.8, 3.9 | 8.3.2 |x86-64 | +| +----------------------------+------------------+ | +| | 3.5 | 7.2.0 | | ++----------------------------------+----------------------------+------------------+--------------+ +| macOS 10.14 Mojave | 3.5, 3.6, 3.7, 3.8 | 7.2.0 |x86-64 | +| +----------------------------+------------------+ | +| | 2.7 | 6.0.0 | | +| +----------------------------+------------------+ | +| | 3.4 | 5.4.1 | | ++----------------------------------+----------------------------+------------------+--------------+ +| macOS 10.13 High Sierra | 2.7, 3.4, 3.5, 3.6 | 4.2.1 |x86-64 | ++----------------------------------+----------------------------+------------------+--------------+ +| macOS 10.12 Sierra | 2.7, 3.4, 3.5, 3.6 | 4.1.1 |x86-64 | ++----------------------------------+----------------------------+------------------+--------------+ +| Mac OS X 10.11 El Capitan | 2.7, 3.4, 3.5, 3.6, 3.7 | 5.4.1 |x86-64 | +| +----------------------------+------------------+ | +| | 3.3 | 4.1.0 | | ++----------------------------------+----------------------------+------------------+--------------+ +| Mac OS X 10.9 Mavericks | 2.7, 3.2, 3.3, 3.4 | 3.0.0 |x86-64 | ++----------------------------------+----------------------------+------------------+--------------+ +| Mac OS X 10.8 Mountain Lion | 2.6, 2.7, 3.2, 3.3 | |x86-64 | ++----------------------------------+----------------------------+------------------+--------------+ +| Redhat Linux 6 | 2.6 | |x86 | ++----------------------------------+----------------------------+------------------+--------------+ +| CentOS 6.3 | 2.7, 3.3 | |x86 | ++----------------------------------+----------------------------+------------------+--------------+ +| CentOS 8 | 3.9 | 9.0.0 |x86-64 | ++----------------------------------+----------------------------+------------------+--------------+ +| Fedora 23 | 2.7, 3.4 | 3.1.0 |x86-64 | ++----------------------------------+----------------------------+------------------+--------------+ +| Ubuntu Linux 12.04 LTS (Precise) | | 2.6, 3.2, 3.3, 3.4, 3.5 | 3.4.1 |x86,x86-64 | +| | | PyPy5.3.1, PyPy3 v2.4.0 | | | +| +----------------------------+------------------+--------------+ +| | 2.7 | 4.3.0 |x86-64 | +| +----------------------------+------------------+--------------+ +| | 2.7, 3.2 | 3.4.1 |ppc | ++----------------------------------+----------------------------+------------------+--------------+ +| Ubuntu Linux 10.04 LTS (Lucid) | 2.6 | 2.3.0 |x86,x86-64 | ++----------------------------------+----------------------------+------------------+--------------+ +| Debian 8.2 Jessie | 2.7, 3.4 | 3.1.0 |x86-64 | ++----------------------------------+----------------------------+------------------+--------------+ +| Raspbian Jessie | 2.7, 3.4 | 3.1.0 |arm | ++----------------------------------+----------------------------+------------------+--------------+ +| Raspbian Stretch | 2.7, 3.5 | 4.0.0 |arm | ++----------------------------------+----------------------------+------------------+--------------+ +| Raspberry Pi OS | 3.6, 3.7, 3.8, 3.9 | 8.2.0 |arm | +| +----------------------------+------------------+ | +| | 2.7 | 6.2.2 | | ++----------------------------------+----------------------------+------------------+--------------+ +| Gentoo Linux | 2.7, 3.2 | 2.1.0 |x86-64 | ++----------------------------------+----------------------------+------------------+--------------+ +| FreeBSD 11.1 | 2.7, 3.4, 3.5, 3.6 | 4.3.0 |x86-64 | ++----------------------------------+----------------------------+------------------+--------------+ +| FreeBSD 10.3 | 2.7, 3.4, 3.5 | 4.2.0 |x86-64 | ++----------------------------------+----------------------------+------------------+--------------+ +| FreeBSD 10.2 | 2.7, 3.4 | 3.1.0 |x86-64 | ++----------------------------------+----------------------------+------------------+--------------+ +| Windows 11 | 3.9, 3.10, 3.11, 3.12 | 10.2.0 |arm64 | ++----------------------------------+----------------------------+------------------+--------------+ +| Windows 11 Pro | 3.11, 3.12 | 10.2.0 |x86-64 | ++----------------------------------+----------------------------+------------------+--------------+ +| Windows 10 | 3.7 | 7.1.0 |x86-64 | ++----------------------------------+----------------------------+------------------+--------------+ +| Windows 10/Cygwin 3.3 | 3.6, 3.7, 3.8, 3.9 | 8.4.0 |x86-64 | ++----------------------------------+----------------------------+------------------+--------------+ +| Windows 8.1 Pro | 2.6, 2.7, 3.2, 3.3, 3.4 | 2.4.0 |x86,x86-64 | ++----------------------------------+----------------------------+------------------+--------------+ +| Windows 8 Pro | 2.6, 2.7, 3.2, 3.3, 3.4a3 | 2.2.0 |x86,x86-64 | ++----------------------------------+----------------------------+------------------+--------------+ +| Windows 7 Professional | 3.7 | 7.0.0 |x86,x86-64 | ++----------------------------------+----------------------------+------------------+--------------+ +| Windows Server 2008 R2 Enterprise| 3.3 | |x86-64 | ++----------------------------------+----------------------------+------------------+--------------+ diff --git a/docs/installation/python-support.rst b/docs/installation/python-support.rst index dd5765b6b..f683e1dc2 100644 --- a/docs/installation/python-support.rst +++ b/docs/installation/python-support.rst @@ -1,14 +1,14 @@ -.. _python-support: - -Python Support -============== - -Pillow supports these Python versions. - -.. csv-table:: Newer versions - :file: newer-versions.csv - :header-rows: 1 - -.. csv-table:: Older versions - :file: older-versions.csv - :header-rows: 1 +.. _python-support: + +Python Support +============== + +Pillow supports these Python versions. + +.. csv-table:: Newer versions + :file: newer-versions.csv + :header-rows: 1 + +.. csv-table:: Older versions + :file: older-versions.csv + :header-rows: 1 diff --git a/docs/make.bat b/docs/make.bat index 0ed5ee1a5..db416dd7d 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -1,202 +1,202 @@ -@ECHO OFF - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -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 - -if "%1" == "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. 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 -) - -if "%1" == "clean" ( - for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i - del /q /s %BUILDDIR%\* - goto end -) - -set html=false -if "%1%" == "html" set html=true -if "%1%" == "htmlview" set html=true -if "%html%" == "true" ( - %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/html. - - if "%1" == "htmlview" ( - if EXIST "%BUILDDIR%\html\index.html" ( - echo.Opening "%BUILDDIR%\html\index.html" in the default web browser... - start "" "%BUILDDIR%\html\index.html" - ) - ) - - goto end -) - -if "%1" == "dirhtml" ( - %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. - goto end -) - -if "%1" == "singlehtml" ( - %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/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 - echo. - echo.Link check complete; look for any errors in the above output ^ -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 +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +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 + +if "%1" == "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. 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 +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + +set html=false +if "%1%" == "html" set html=true +if "%1%" == "htmlview" set html=true +if "%html%" == "true" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + + if "%1" == "htmlview" ( + if EXIST "%BUILDDIR%\html\index.html" ( + echo.Opening "%BUILDDIR%\html\index.html" in the default web browser... + start "" "%BUILDDIR%\html\index.html" + ) + ) + + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/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 + echo. + echo.Link check complete; look for any errors in the above output ^ +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/porting.rst b/docs/porting.rst index 2943d72fd..ea8ea5801 100644 --- a/docs/porting.rst +++ b/docs/porting.rst @@ -1,35 +1,35 @@ -Porting -======= - -**Porting existing PIL-based code to Pillow** - -Pillow is a functional drop-in replacement for the Python Imaging Library. - -PIL is Python 2 only. Pillow dropped support for Python 2 in Pillow -7.0. So if you would like to run the latest version of Pillow, you will first -and foremost need to port your code from Python 2 to 3. - -To run your existing PIL-compatible code with Pillow, it needs to be modified -to import the ``Image`` module from the ``PIL`` namespace *instead* of the -global namespace. Change this:: - - import Image - -to this:: - - from PIL import Image - -The :py:mod:`PIL._imaging` module has been moved to :py:mod:`PIL.Image.core`. -You can now import it like this:: - - from PIL.Image import core as _imaging - -The image plugin loading mechanism has changed. Pillow no longer -automatically imports any file in the Python path with a name ending -in :file:`ImagePlugin.py`. You will need to import your image plugin -manually. - -Pillow will raise an exception if the core extension can't be loaded -for any reason, including a version mismatch between the Python and -extension code. Previously PIL allowed Python only code to run if the -core extension was not available. +Porting +======= + +**Porting existing PIL-based code to Pillow** + +Pillow is a functional drop-in replacement for the Python Imaging Library. + +PIL is Python 2 only. Pillow dropped support for Python 2 in Pillow +7.0. So if you would like to run the latest version of Pillow, you will first +and foremost need to port your code from Python 2 to 3. + +To run your existing PIL-compatible code with Pillow, it needs to be modified +to import the ``Image`` module from the ``PIL`` namespace *instead* of the +global namespace. Change this:: + + import Image + +to this:: + + from PIL import Image + +The :py:mod:`PIL._imaging` module has been moved to :py:mod:`PIL.Image.core`. +You can now import it like this:: + + from PIL.Image import core as _imaging + +The image plugin loading mechanism has changed. Pillow no longer +automatically imports any file in the Python path with a name ending +in :file:`ImagePlugin.py`. You will need to import your image plugin +manually. + +Pillow will raise an exception if the core extension can't be loaded +for any reason, including a version mismatch between the Python and +extension code. Previously PIL allowed Python only code to run if the +core extension was not available. diff --git a/docs/reference/ExifTags.rst b/docs/reference/ExifTags.rst index 06965ead3..12a5e0ac2 100644 --- a/docs/reference/ExifTags.rst +++ b/docs/reference/ExifTags.rst @@ -1,71 +1,71 @@ -.. py:module:: PIL.ExifTags -.. py:currentmodule:: PIL.ExifTags - -:py:mod:`~PIL.ExifTags` Module -============================== - -The :py:mod:`~PIL.ExifTags` module exposes several :py:class:`enum.IntEnum` -classes which provide constants and clear-text names for various well-known -EXIF tags. - -.. py:data:: Base - - >>> from PIL.ExifTags import Base - >>> Base.ImageDescription.value - 270 - >>> Base(270).name - 'ImageDescription' - -.. py:data:: GPS - - >>> from PIL.ExifTags import GPS - >>> GPS.GPSDestLatitude.value - 20 - >>> GPS(20).name - 'GPSDestLatitude' - -.. py:data:: Interop - - >>> from PIL.ExifTags import Interop - >>> Interop.RelatedImageFileFormat.value - 4096 - >>> Interop(4096).name - 'RelatedImageFileFormat' - -.. py:data:: IFD - - >>> from PIL.ExifTags import IFD - >>> IFD.Exif.value - 34665 - >>> IFD(34665).name - 'Exif - -.. py:data:: LightSource - - >>> from PIL.ExifTags import LightSource - >>> LightSource.Unknown.value - 0 - >>> LightSource(0).name - 'Unknown' - -Two of these values are also exposed as dictionaries. - -.. py:data:: TAGS - :type: dict - - The TAGS dictionary maps 16-bit integer EXIF tag enumerations to - descriptive string names. For instance: - - >>> from PIL.ExifTags import TAGS - >>> TAGS[0x010e] - 'ImageDescription' - -.. py:data:: GPSTAGS - :type: dict - - The GPSTAGS dictionary maps 8-bit integer EXIF GPS enumerations to - descriptive string names. For instance: - - >>> from PIL.ExifTags import GPSTAGS - >>> GPSTAGS[20] - 'GPSDestLatitude' +.. py:module:: PIL.ExifTags +.. py:currentmodule:: PIL.ExifTags + +:py:mod:`~PIL.ExifTags` Module +============================== + +The :py:mod:`~PIL.ExifTags` module exposes several :py:class:`enum.IntEnum` +classes which provide constants and clear-text names for various well-known +EXIF tags. + +.. py:data:: Base + + >>> from PIL.ExifTags import Base + >>> Base.ImageDescription.value + 270 + >>> Base(270).name + 'ImageDescription' + +.. py:data:: GPS + + >>> from PIL.ExifTags import GPS + >>> GPS.GPSDestLatitude.value + 20 + >>> GPS(20).name + 'GPSDestLatitude' + +.. py:data:: Interop + + >>> from PIL.ExifTags import Interop + >>> Interop.RelatedImageFileFormat.value + 4096 + >>> Interop(4096).name + 'RelatedImageFileFormat' + +.. py:data:: IFD + + >>> from PIL.ExifTags import IFD + >>> IFD.Exif.value + 34665 + >>> IFD(34665).name + 'Exif + +.. py:data:: LightSource + + >>> from PIL.ExifTags import LightSource + >>> LightSource.Unknown.value + 0 + >>> LightSource(0).name + 'Unknown' + +Two of these values are also exposed as dictionaries. + +.. py:data:: TAGS + :type: dict + + The TAGS dictionary maps 16-bit integer EXIF tag enumerations to + descriptive string names. For instance: + + >>> from PIL.ExifTags import TAGS + >>> TAGS[0x010e] + 'ImageDescription' + +.. py:data:: GPSTAGS + :type: dict + + The GPSTAGS dictionary maps 8-bit integer EXIF GPS enumerations to + descriptive string names. For instance: + + >>> from PIL.ExifTags import GPSTAGS + >>> GPSTAGS[20] + 'GPSDestLatitude' diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index 1c095a114..61dd5a966 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -1,486 +1,486 @@ -.. py:module:: PIL.Image -.. py:currentmodule:: PIL.Image - -:py:mod:`~PIL.Image` Module -=========================== - -The :py:mod:`~PIL.Image` module provides a class with the same name which is -used to represent a PIL image. The module also provides a number of factory -functions, including functions to load images from files, and to create new -images. - -Examples --------- - -Open, rotate, and display an image (using the default viewer) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The following script loads an image, rotates it 45 degrees, and displays it -using an external viewer (usually xv on Unix, and the Paint program on -Windows). :: - - from PIL import Image - with Image.open("hopper.jpg") as im: - im.rotate(45).show() - -Create thumbnails -^^^^^^^^^^^^^^^^^ - -The following script creates nice thumbnails of all JPEG images in the -current directory preserving aspect ratios with 128x128 max resolution. :: - - from PIL import Image - import glob, os - - size = 128, 128 - - for infile in glob.glob("*.jpg"): - file, ext = os.path.splitext(infile) - with Image.open(infile) as im: - im.thumbnail(size) - im.save(file + ".thumbnail", "JPEG") - -Functions ---------- - -.. autofunction:: open - - .. warning:: - To protect against potential DOS attacks caused by "`decompression bombs`_" (i.e. malicious files - which decompress into a huge amount of data and are designed to crash or cause disruption by using up - a lot of memory), Pillow will issue a ``DecompressionBombWarning`` if the number of pixels in an - image is over a certain limit, :py:data:`MAX_IMAGE_PIXELS`. - - This threshold can be changed by setting :py:data:`MAX_IMAGE_PIXELS`. It can be disabled - by setting ``Image.MAX_IMAGE_PIXELS = None``. - - If desired, the warning can be turned into an error with - ``warnings.simplefilter('error', Image.DecompressionBombWarning)`` or suppressed entirely with - ``warnings.simplefilter('ignore', Image.DecompressionBombWarning)``. See also - `the logging documentation`_ to have warnings output to the logging facility instead of stderr. - - If the number of pixels is greater than twice :py:data:`MAX_IMAGE_PIXELS`, then a - ``DecompressionBombError`` will be raised instead. - - .. _decompression bombs: https://en.wikipedia.org/wiki/Zip_bomb - .. _the logging documentation: https://docs.python.org/3/library/logging.html#integration-with-the-warnings-module - -Image processing -^^^^^^^^^^^^^^^^ - -.. autofunction:: alpha_composite -.. autofunction:: blend -.. autofunction:: composite -.. autofunction:: eval -.. autofunction:: merge - -Constructing images -^^^^^^^^^^^^^^^^^^^ - -.. autofunction:: new -.. autofunction:: fromarray -.. autofunction:: frombytes -.. autofunction:: frombuffer - -Generating images -^^^^^^^^^^^^^^^^^ - -.. autofunction:: effect_mandelbrot -.. autofunction:: effect_noise -.. autofunction:: linear_gradient -.. autofunction:: radial_gradient - -Registering plugins -^^^^^^^^^^^^^^^^^^^ - -.. autofunction:: preinit -.. autofunction:: init - -.. note:: - - These functions are for use by plugin authors. They are called when a - plugin is loaded as part of :py:meth:`~preinit()` or :py:meth:`~init()`. - Application authors can ignore them. - -.. autofunction:: register_open -.. autofunction:: register_mime -.. autofunction:: register_save -.. autofunction:: register_save_all -.. autofunction:: register_extension -.. autofunction:: register_extensions -.. autofunction:: registered_extensions -.. autofunction:: register_decoder -.. autofunction:: register_encoder - -The Image Class ---------------- - -.. autoclass:: PIL.Image.Image - -An instance of the :py:class:`~PIL.Image.Image` class has the following -methods. Unless otherwise stated, all methods return a new instance of the -:py:class:`~PIL.Image.Image` class, holding the resulting image. - - -.. automethod:: PIL.Image.Image.alpha_composite -.. automethod:: PIL.Image.Image.apply_transparency -.. automethod:: PIL.Image.Image.convert - -The following example converts an RGB image (linearly calibrated according to -ITU-R 709, using the D65 luminant) to the CIE XYZ color space:: - - rgb2xyz = ( - 0.412453, 0.357580, 0.180423, 0, - 0.212671, 0.715160, 0.072169, 0, - 0.019334, 0.119193, 0.950227, 0) - out = im.convert("RGB", rgb2xyz) - -.. automethod:: PIL.Image.Image.copy -.. automethod:: PIL.Image.Image.crop - -This crops the input image with the provided coordinates:: - - from PIL import Image - - with Image.open("hopper.jpg") as im: - - # The crop method from the Image module takes four coordinates as input. - # The right can also be represented as (left+width) - # and lower can be represented as (upper+height). - (left, upper, right, lower) = (20, 20, 100, 100) - - # Here the image "im" is cropped and assigned to new variable im_crop - im_crop = im.crop((left, upper, right, lower)) - - -.. automethod:: PIL.Image.Image.draft -.. automethod:: PIL.Image.Image.effect_spread -.. automethod:: PIL.Image.Image.entropy -.. automethod:: PIL.Image.Image.filter - -This blurs the input image using a filter from the ``ImageFilter`` module:: - - from PIL import Image, ImageFilter - - with Image.open("hopper.jpg") as im: - - # Blur the input image using the filter ImageFilter.BLUR - im_blurred = im.filter(filter=ImageFilter.BLUR) - -.. automethod:: PIL.Image.Image.frombytes -.. automethod:: PIL.Image.Image.getbands - -This helps to get the bands of the input image:: - - from PIL import Image - - with Image.open("hopper.jpg") as im: - print(im.getbands()) # Returns ('R', 'G', 'B') - -.. automethod:: PIL.Image.Image.getbbox - -This helps to get the bounding box coordinates of the input image:: - - from PIL import Image - - with Image.open("hopper.jpg") as im: - print(im.getbbox()) - # Returns four coordinates in the format (left, upper, right, lower) - -.. automethod:: PIL.Image.Image.getchannel -.. automethod:: PIL.Image.Image.getcolors -.. automethod:: PIL.Image.Image.getdata -.. automethod:: PIL.Image.Image.getexif -.. automethod:: PIL.Image.Image.getextrema -.. automethod:: PIL.Image.Image.getpalette -.. automethod:: PIL.Image.Image.getpixel -.. automethod:: PIL.Image.Image.getprojection -.. automethod:: PIL.Image.Image.histogram -.. automethod:: PIL.Image.Image.paste -.. automethod:: PIL.Image.Image.point -.. automethod:: PIL.Image.Image.putalpha -.. automethod:: PIL.Image.Image.putdata -.. automethod:: PIL.Image.Image.putpalette -.. automethod:: PIL.Image.Image.putpixel -.. automethod:: PIL.Image.Image.quantize -.. automethod:: PIL.Image.Image.reduce -.. automethod:: PIL.Image.Image.remap_palette -.. automethod:: PIL.Image.Image.resize - -This resizes the given image from ``(width, height)`` to ``(width/2, height/2)``:: - - from PIL import Image - - with Image.open("hopper.jpg") as im: - - # Provide the target width and height of the image - (width, height) = (im.width // 2, im.height // 2) - im_resized = im.resize((width, height)) - -.. automethod:: PIL.Image.Image.rotate - -This rotates the input image by ``theta`` degrees counter clockwise:: - - from PIL import Image - - with Image.open("hopper.jpg") as im: - - # Rotate the image by 60 degrees counter clockwise - theta = 60 - # Angle is in degrees counter clockwise - im_rotated = im.rotate(angle=theta) - -.. automethod:: PIL.Image.Image.save -.. automethod:: PIL.Image.Image.seek -.. automethod:: PIL.Image.Image.show -.. automethod:: PIL.Image.Image.split -.. automethod:: PIL.Image.Image.tell -.. automethod:: PIL.Image.Image.thumbnail -.. automethod:: PIL.Image.Image.tobitmap -.. automethod:: PIL.Image.Image.tobytes -.. automethod:: PIL.Image.Image.transform -.. automethod:: PIL.Image.Image.transpose - -This flips the input image by using the :data:`Transpose.FLIP_LEFT_RIGHT` -method. :: - - from PIL import Image - - with Image.open("hopper.jpg") as im: - - # Flip the image from left to right - im_flipped = im.transpose(method=Image.Transpose.FLIP_LEFT_RIGHT) - # To flip the image from top to bottom, - # use the method "Image.Transpose.FLIP_TOP_BOTTOM" - - -.. automethod:: PIL.Image.Image.verify - -.. automethod:: PIL.Image.Image.load -.. automethod:: PIL.Image.Image.close - -Image Attributes ----------------- - -Instances of the :py:class:`Image` class have the following attributes: - -.. py:attribute:: Image.filename - :type: str - - The filename or path of the source file. Only images created with the - factory function ``open`` have a filename attribute. If the input is a - file like object, the filename attribute is set to an empty string. - -.. py:attribute:: Image.format - :type: Optional[str] - - The file format of the source file. For images created by the library - itself (via a factory function, or by running a method on an existing - image), this attribute is set to :data:`None`. - -.. py:attribute:: Image.mode - :type: str - - Image mode. This is a string specifying the pixel format used by the image. - Typical values are “1â€, “Lâ€, “RGBâ€, or “CMYK.†See - :ref:`concept-modes` for a full list. - -.. py:attribute:: Image.size - :type: tuple[int] - - Image size, in pixels. The size is given as a 2-tuple (width, height). - -.. py:attribute:: Image.width - :type: int - - Image width, in pixels. - -.. py:attribute:: Image.height - :type: int - - Image height, in pixels. - -.. py:attribute:: Image.palette - :type: Optional[PIL.ImagePalette.ImagePalette] - - Colour palette table, if any. If mode is "P" or "PA", this should be an - instance of the :py:class:`~PIL.ImagePalette.ImagePalette` class. - Otherwise, it should be set to :data:`None`. - -.. py:attribute:: Image.info - :type: dict - - A dictionary holding data associated with the image. This dictionary is - used by file handlers to pass on various non-image information read from - the file. See documentation for the various file handlers for details. - - Most methods ignore the dictionary when returning new images; since the - keys are not standardized, it’s not possible for a method to know if the - operation affects the dictionary. If you need the information later on, - keep a reference to the info dictionary returned from the open method. - - Unless noted elsewhere, this dictionary does not affect saving files. - -.. py:attribute:: Image.is_animated - :type: bool - - ``True`` if this image has more than one frame, or ``False`` otherwise. - - This attribute is only defined by image plugins that support animated images. - Plugins may leave this attribute undefined if they don't support loading - animated images, even if the given format supports animated images. - - Given that this attribute is not present for all images use - ``getattr(image, "is_animated", False)`` to check if Pillow is aware of multiple - frames in an image regardless of its format. - - .. seealso:: :attr:`~Image.n_frames`, :func:`~Image.seek` and :func:`~Image.tell` - -.. py:attribute:: Image.n_frames - :type: int - - The number of frames in this image. - - This attribute is only defined by image plugins that support animated images. - Plugins may leave this attribute undefined if they don't support loading - animated images, even if the given format supports animated images. - - Given that this attribute is not present for all images use - ``getattr(image, "n_frames", 1)`` to check the number of frames that Pillow is - aware of in an image regardless of its format. - - .. seealso:: :attr:`~Image.is_animated`, :func:`~Image.seek` and :func:`~Image.tell` - -.. autoattribute:: PIL.Image.Image.has_transparency_data - -Classes -------- - -.. autoclass:: PIL.Image.Exif - :members: - :undoc-members: - :show-inheritance: -.. autoclass:: PIL.Image.ImagePointHandler -.. autoclass:: PIL.Image.ImageTransformHandler - -Protocols ---------- - -.. autoclass:: SupportsArrayInterface - :show-inheritance: -.. autoclass:: SupportsGetData - :show-inheritance: - -Constants ---------- - -.. data:: NONE -.. data:: MAX_IMAGE_PIXELS - - Set to 89,478,485, approximately 0.25GB for a 24-bit (3 bpp) image. - See :py:meth:`~PIL.Image.open` for more information about how this is used. - -Transpose methods -^^^^^^^^^^^^^^^^^ - -Used to specify the :meth:`Image.transpose` method to use. - -.. autoclass:: Transpose - :members: - :undoc-members: - -Transform methods -^^^^^^^^^^^^^^^^^ - -Used to specify the :meth:`Image.transform` method to use. - -.. py:class:: Transform - - .. py:attribute:: AFFINE - - Affine transform - - .. py:attribute:: EXTENT - - Cut out a rectangular subregion - - .. py:attribute:: PERSPECTIVE - - Perspective transform - - .. py:attribute:: QUAD - - Map a quadrilateral to a rectangle - - .. py:attribute:: MESH - - Map a number of source quadrilaterals in one operation - -Resampling filters -^^^^^^^^^^^^^^^^^^ - -See :ref:`concept-filters` for details. - -.. autoclass:: Resampling - :members: - :undoc-members: - -Dither modes -^^^^^^^^^^^^ - -Used to specify the dithering method to use for the -:meth:`~Image.convert` and :meth:`~Image.quantize` methods. - -.. py:class:: Dither - - .. py:attribute:: NONE - - No dither - - .. py:attribute:: ORDERED - - Not implemented - - .. py:attribute:: RASTERIZE - - Not implemented - - .. py:attribute:: FLOYDSTEINBERG - - Floyd-Steinberg dither - -Palettes -^^^^^^^^ - -Used to specify the palette to use for the :meth:`~Image.convert` method. - -.. autoclass:: Palette - :members: - :undoc-members: - -Quantization methods -^^^^^^^^^^^^^^^^^^^^ - -Used to specify the quantization method to use for the :meth:`~Image.quantize` method. - -.. py:class:: Quantize - - .. py:attribute:: MEDIANCUT - - Median cut. Default method, except for RGBA images. This method does not support - RGBA images. - - .. py:attribute:: MAXCOVERAGE - - Maximum coverage. This method does not support RGBA images. - - .. py:attribute:: FASTOCTREE - - Fast octree. Default method for RGBA images. - - .. py:attribute:: LIBIMAGEQUANT - - libimagequant - - Check support using :py:func:`PIL.features.check_feature` with - ``feature="libimagequant"``. +.. py:module:: PIL.Image +.. py:currentmodule:: PIL.Image + +:py:mod:`~PIL.Image` Module +=========================== + +The :py:mod:`~PIL.Image` module provides a class with the same name which is +used to represent a PIL image. The module also provides a number of factory +functions, including functions to load images from files, and to create new +images. + +Examples +-------- + +Open, rotate, and display an image (using the default viewer) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The following script loads an image, rotates it 45 degrees, and displays it +using an external viewer (usually xv on Unix, and the Paint program on +Windows). :: + + from PIL import Image + with Image.open("hopper.jpg") as im: + im.rotate(45).show() + +Create thumbnails +^^^^^^^^^^^^^^^^^ + +The following script creates nice thumbnails of all JPEG images in the +current directory preserving aspect ratios with 128x128 max resolution. :: + + from PIL import Image + import glob, os + + size = 128, 128 + + for infile in glob.glob("*.jpg"): + file, ext = os.path.splitext(infile) + with Image.open(infile) as im: + im.thumbnail(size) + im.save(file + ".thumbnail", "JPEG") + +Functions +--------- + +.. autofunction:: open + + .. warning:: + To protect against potential DOS attacks caused by "`decompression bombs`_" (i.e. malicious files + which decompress into a huge amount of data and are designed to crash or cause disruption by using up + a lot of memory), Pillow will issue a ``DecompressionBombWarning`` if the number of pixels in an + image is over a certain limit, :py:data:`MAX_IMAGE_PIXELS`. + + This threshold can be changed by setting :py:data:`MAX_IMAGE_PIXELS`. It can be disabled + by setting ``Image.MAX_IMAGE_PIXELS = None``. + + If desired, the warning can be turned into an error with + ``warnings.simplefilter('error', Image.DecompressionBombWarning)`` or suppressed entirely with + ``warnings.simplefilter('ignore', Image.DecompressionBombWarning)``. See also + `the logging documentation`_ to have warnings output to the logging facility instead of stderr. + + If the number of pixels is greater than twice :py:data:`MAX_IMAGE_PIXELS`, then a + ``DecompressionBombError`` will be raised instead. + + .. _decompression bombs: https://en.wikipedia.org/wiki/Zip_bomb + .. _the logging documentation: https://docs.python.org/3/library/logging.html#integration-with-the-warnings-module + +Image processing +^^^^^^^^^^^^^^^^ + +.. autofunction:: alpha_composite +.. autofunction:: blend +.. autofunction:: composite +.. autofunction:: eval +.. autofunction:: merge + +Constructing images +^^^^^^^^^^^^^^^^^^^ + +.. autofunction:: new +.. autofunction:: fromarray +.. autofunction:: frombytes +.. autofunction:: frombuffer + +Generating images +^^^^^^^^^^^^^^^^^ + +.. autofunction:: effect_mandelbrot +.. autofunction:: effect_noise +.. autofunction:: linear_gradient +.. autofunction:: radial_gradient + +Registering plugins +^^^^^^^^^^^^^^^^^^^ + +.. autofunction:: preinit +.. autofunction:: init + +.. note:: + + These functions are for use by plugin authors. They are called when a + plugin is loaded as part of :py:meth:`~preinit()` or :py:meth:`~init()`. + Application authors can ignore them. + +.. autofunction:: register_open +.. autofunction:: register_mime +.. autofunction:: register_save +.. autofunction:: register_save_all +.. autofunction:: register_extension +.. autofunction:: register_extensions +.. autofunction:: registered_extensions +.. autofunction:: register_decoder +.. autofunction:: register_encoder + +The Image Class +--------------- + +.. autoclass:: PIL.Image.Image + +An instance of the :py:class:`~PIL.Image.Image` class has the following +methods. Unless otherwise stated, all methods return a new instance of the +:py:class:`~PIL.Image.Image` class, holding the resulting image. + + +.. automethod:: PIL.Image.Image.alpha_composite +.. automethod:: PIL.Image.Image.apply_transparency +.. automethod:: PIL.Image.Image.convert + +The following example converts an RGB image (linearly calibrated according to +ITU-R 709, using the D65 luminant) to the CIE XYZ color space:: + + rgb2xyz = ( + 0.412453, 0.357580, 0.180423, 0, + 0.212671, 0.715160, 0.072169, 0, + 0.019334, 0.119193, 0.950227, 0) + out = im.convert("RGB", rgb2xyz) + +.. automethod:: PIL.Image.Image.copy +.. automethod:: PIL.Image.Image.crop + +This crops the input image with the provided coordinates:: + + from PIL import Image + + with Image.open("hopper.jpg") as im: + + # The crop method from the Image module takes four coordinates as input. + # The right can also be represented as (left+width) + # and lower can be represented as (upper+height). + (left, upper, right, lower) = (20, 20, 100, 100) + + # Here the image "im" is cropped and assigned to new variable im_crop + im_crop = im.crop((left, upper, right, lower)) + + +.. automethod:: PIL.Image.Image.draft +.. automethod:: PIL.Image.Image.effect_spread +.. automethod:: PIL.Image.Image.entropy +.. automethod:: PIL.Image.Image.filter + +This blurs the input image using a filter from the ``ImageFilter`` module:: + + from PIL import Image, ImageFilter + + with Image.open("hopper.jpg") as im: + + # Blur the input image using the filter ImageFilter.BLUR + im_blurred = im.filter(filter=ImageFilter.BLUR) + +.. automethod:: PIL.Image.Image.frombytes +.. automethod:: PIL.Image.Image.getbands + +This helps to get the bands of the input image:: + + from PIL import Image + + with Image.open("hopper.jpg") as im: + print(im.getbands()) # Returns ('R', 'G', 'B') + +.. automethod:: PIL.Image.Image.getbbox + +This helps to get the bounding box coordinates of the input image:: + + from PIL import Image + + with Image.open("hopper.jpg") as im: + print(im.getbbox()) + # Returns four coordinates in the format (left, upper, right, lower) + +.. automethod:: PIL.Image.Image.getchannel +.. automethod:: PIL.Image.Image.getcolors +.. automethod:: PIL.Image.Image.getdata +.. automethod:: PIL.Image.Image.getexif +.. automethod:: PIL.Image.Image.getextrema +.. automethod:: PIL.Image.Image.getpalette +.. automethod:: PIL.Image.Image.getpixel +.. automethod:: PIL.Image.Image.getprojection +.. automethod:: PIL.Image.Image.histogram +.. automethod:: PIL.Image.Image.paste +.. automethod:: PIL.Image.Image.point +.. automethod:: PIL.Image.Image.putalpha +.. automethod:: PIL.Image.Image.putdata +.. automethod:: PIL.Image.Image.putpalette +.. automethod:: PIL.Image.Image.putpixel +.. automethod:: PIL.Image.Image.quantize +.. automethod:: PIL.Image.Image.reduce +.. automethod:: PIL.Image.Image.remap_palette +.. automethod:: PIL.Image.Image.resize + +This resizes the given image from ``(width, height)`` to ``(width/2, height/2)``:: + + from PIL import Image + + with Image.open("hopper.jpg") as im: + + # Provide the target width and height of the image + (width, height) = (im.width // 2, im.height // 2) + im_resized = im.resize((width, height)) + +.. automethod:: PIL.Image.Image.rotate + +This rotates the input image by ``theta`` degrees counter clockwise:: + + from PIL import Image + + with Image.open("hopper.jpg") as im: + + # Rotate the image by 60 degrees counter clockwise + theta = 60 + # Angle is in degrees counter clockwise + im_rotated = im.rotate(angle=theta) + +.. automethod:: PIL.Image.Image.save +.. automethod:: PIL.Image.Image.seek +.. automethod:: PIL.Image.Image.show +.. automethod:: PIL.Image.Image.split +.. automethod:: PIL.Image.Image.tell +.. automethod:: PIL.Image.Image.thumbnail +.. automethod:: PIL.Image.Image.tobitmap +.. automethod:: PIL.Image.Image.tobytes +.. automethod:: PIL.Image.Image.transform +.. automethod:: PIL.Image.Image.transpose + +This flips the input image by using the :data:`Transpose.FLIP_LEFT_RIGHT` +method. :: + + from PIL import Image + + with Image.open("hopper.jpg") as im: + + # Flip the image from left to right + im_flipped = im.transpose(method=Image.Transpose.FLIP_LEFT_RIGHT) + # To flip the image from top to bottom, + # use the method "Image.Transpose.FLIP_TOP_BOTTOM" + + +.. automethod:: PIL.Image.Image.verify + +.. automethod:: PIL.Image.Image.load +.. automethod:: PIL.Image.Image.close + +Image Attributes +---------------- + +Instances of the :py:class:`Image` class have the following attributes: + +.. py:attribute:: Image.filename + :type: str + + The filename or path of the source file. Only images created with the + factory function ``open`` have a filename attribute. If the input is a + file like object, the filename attribute is set to an empty string. + +.. py:attribute:: Image.format + :type: Optional[str] + + The file format of the source file. For images created by the library + itself (via a factory function, or by running a method on an existing + image), this attribute is set to :data:`None`. + +.. py:attribute:: Image.mode + :type: str + + Image mode. This is a string specifying the pixel format used by the image. + Typical values are “1â€, “Lâ€, “RGBâ€, or “CMYK.†See + :ref:`concept-modes` for a full list. + +.. py:attribute:: Image.size + :type: tuple[int] + + Image size, in pixels. The size is given as a 2-tuple (width, height). + +.. py:attribute:: Image.width + :type: int + + Image width, in pixels. + +.. py:attribute:: Image.height + :type: int + + Image height, in pixels. + +.. py:attribute:: Image.palette + :type: Optional[PIL.ImagePalette.ImagePalette] + + Colour palette table, if any. If mode is "P" or "PA", this should be an + instance of the :py:class:`~PIL.ImagePalette.ImagePalette` class. + Otherwise, it should be set to :data:`None`. + +.. py:attribute:: Image.info + :type: dict + + A dictionary holding data associated with the image. This dictionary is + used by file handlers to pass on various non-image information read from + the file. See documentation for the various file handlers for details. + + Most methods ignore the dictionary when returning new images; since the + keys are not standardized, it’s not possible for a method to know if the + operation affects the dictionary. If you need the information later on, + keep a reference to the info dictionary returned from the open method. + + Unless noted elsewhere, this dictionary does not affect saving files. + +.. py:attribute:: Image.is_animated + :type: bool + + ``True`` if this image has more than one frame, or ``False`` otherwise. + + This attribute is only defined by image plugins that support animated images. + Plugins may leave this attribute undefined if they don't support loading + animated images, even if the given format supports animated images. + + Given that this attribute is not present for all images use + ``getattr(image, "is_animated", False)`` to check if Pillow is aware of multiple + frames in an image regardless of its format. + + .. seealso:: :attr:`~Image.n_frames`, :func:`~Image.seek` and :func:`~Image.tell` + +.. py:attribute:: Image.n_frames + :type: int + + The number of frames in this image. + + This attribute is only defined by image plugins that support animated images. + Plugins may leave this attribute undefined if they don't support loading + animated images, even if the given format supports animated images. + + Given that this attribute is not present for all images use + ``getattr(image, "n_frames", 1)`` to check the number of frames that Pillow is + aware of in an image regardless of its format. + + .. seealso:: :attr:`~Image.is_animated`, :func:`~Image.seek` and :func:`~Image.tell` + +.. autoattribute:: PIL.Image.Image.has_transparency_data + +Classes +------- + +.. autoclass:: PIL.Image.Exif + :members: + :undoc-members: + :show-inheritance: +.. autoclass:: PIL.Image.ImagePointHandler +.. autoclass:: PIL.Image.ImageTransformHandler + +Protocols +--------- + +.. autoclass:: SupportsArrayInterface + :show-inheritance: +.. autoclass:: SupportsGetData + :show-inheritance: + +Constants +--------- + +.. data:: NONE +.. data:: MAX_IMAGE_PIXELS + + Set to 89,478,485, approximately 0.25GB for a 24-bit (3 bpp) image. + See :py:meth:`~PIL.Image.open` for more information about how this is used. + +Transpose methods +^^^^^^^^^^^^^^^^^ + +Used to specify the :meth:`Image.transpose` method to use. + +.. autoclass:: Transpose + :members: + :undoc-members: + +Transform methods +^^^^^^^^^^^^^^^^^ + +Used to specify the :meth:`Image.transform` method to use. + +.. py:class:: Transform + + .. py:attribute:: AFFINE + + Affine transform + + .. py:attribute:: EXTENT + + Cut out a rectangular subregion + + .. py:attribute:: PERSPECTIVE + + Perspective transform + + .. py:attribute:: QUAD + + Map a quadrilateral to a rectangle + + .. py:attribute:: MESH + + Map a number of source quadrilaterals in one operation + +Resampling filters +^^^^^^^^^^^^^^^^^^ + +See :ref:`concept-filters` for details. + +.. autoclass:: Resampling + :members: + :undoc-members: + +Dither modes +^^^^^^^^^^^^ + +Used to specify the dithering method to use for the +:meth:`~Image.convert` and :meth:`~Image.quantize` methods. + +.. py:class:: Dither + + .. py:attribute:: NONE + + No dither + + .. py:attribute:: ORDERED + + Not implemented + + .. py:attribute:: RASTERIZE + + Not implemented + + .. py:attribute:: FLOYDSTEINBERG + + Floyd-Steinberg dither + +Palettes +^^^^^^^^ + +Used to specify the palette to use for the :meth:`~Image.convert` method. + +.. autoclass:: Palette + :members: + :undoc-members: + +Quantization methods +^^^^^^^^^^^^^^^^^^^^ + +Used to specify the quantization method to use for the :meth:`~Image.quantize` method. + +.. py:class:: Quantize + + .. py:attribute:: MEDIANCUT + + Median cut. Default method, except for RGBA images. This method does not support + RGBA images. + + .. py:attribute:: MAXCOVERAGE + + Maximum coverage. This method does not support RGBA images. + + .. py:attribute:: FASTOCTREE + + Fast octree. Default method for RGBA images. + + .. py:attribute:: LIBIMAGEQUANT + + libimagequant + + Check support using :py:func:`PIL.features.check_feature` with + ``feature="libimagequant"``. diff --git a/docs/reference/ImageChops.rst b/docs/reference/ImageChops.rst index 9519361a7..6e5c928c7 100644 --- a/docs/reference/ImageChops.rst +++ b/docs/reference/ImageChops.rst @@ -1,45 +1,45 @@ -.. py:module:: PIL.ImageChops -.. py:currentmodule:: PIL.ImageChops - -:py:mod:`~PIL.ImageChops` ("Channel Operations") Module -======================================================= - -The :py:mod:`~PIL.ImageChops` module contains a number of arithmetical image -operations, called channel operations (“chopsâ€). These can be used for various -purposes, including special effects, image compositions, algorithmic painting, -and more. - -For more pre-made operations, see :py:mod:`~PIL.ImageOps`. - -At this time, most channel operations are only implemented for 8-bit images -(e.g. “L†and “RGBâ€). - -Functions ---------- - -Most channel operations take one or two image arguments and returns a new -image. Unless otherwise noted, the result of a channel operation is always -clipped to the range 0 to MAX (which is 255 for all modes supported by the -operations in this module). - -.. autofunction:: PIL.ImageChops.add -.. autofunction:: PIL.ImageChops.add_modulo -.. autofunction:: PIL.ImageChops.blend -.. autofunction:: PIL.ImageChops.composite -.. autofunction:: PIL.ImageChops.constant -.. autofunction:: PIL.ImageChops.darker -.. autofunction:: PIL.ImageChops.difference -.. autofunction:: PIL.ImageChops.duplicate -.. autofunction:: PIL.ImageChops.invert -.. autofunction:: PIL.ImageChops.lighter -.. autofunction:: PIL.ImageChops.logical_and -.. autofunction:: PIL.ImageChops.logical_or -.. autofunction:: PIL.ImageChops.logical_xor -.. autofunction:: PIL.ImageChops.multiply -.. autofunction:: PIL.ImageChops.soft_light -.. autofunction:: PIL.ImageChops.hard_light -.. autofunction:: PIL.ImageChops.overlay -.. autofunction:: PIL.ImageChops.offset -.. autofunction:: PIL.ImageChops.screen -.. autofunction:: PIL.ImageChops.subtract -.. autofunction:: PIL.ImageChops.subtract_modulo +.. py:module:: PIL.ImageChops +.. py:currentmodule:: PIL.ImageChops + +:py:mod:`~PIL.ImageChops` ("Channel Operations") Module +======================================================= + +The :py:mod:`~PIL.ImageChops` module contains a number of arithmetical image +operations, called channel operations (“chopsâ€). These can be used for various +purposes, including special effects, image compositions, algorithmic painting, +and more. + +For more pre-made operations, see :py:mod:`~PIL.ImageOps`. + +At this time, most channel operations are only implemented for 8-bit images +(e.g. “L†and “RGBâ€). + +Functions +--------- + +Most channel operations take one or two image arguments and returns a new +image. Unless otherwise noted, the result of a channel operation is always +clipped to the range 0 to MAX (which is 255 for all modes supported by the +operations in this module). + +.. autofunction:: PIL.ImageChops.add +.. autofunction:: PIL.ImageChops.add_modulo +.. autofunction:: PIL.ImageChops.blend +.. autofunction:: PIL.ImageChops.composite +.. autofunction:: PIL.ImageChops.constant +.. autofunction:: PIL.ImageChops.darker +.. autofunction:: PIL.ImageChops.difference +.. autofunction:: PIL.ImageChops.duplicate +.. autofunction:: PIL.ImageChops.invert +.. autofunction:: PIL.ImageChops.lighter +.. autofunction:: PIL.ImageChops.logical_and +.. autofunction:: PIL.ImageChops.logical_or +.. autofunction:: PIL.ImageChops.logical_xor +.. autofunction:: PIL.ImageChops.multiply +.. autofunction:: PIL.ImageChops.soft_light +.. autofunction:: PIL.ImageChops.hard_light +.. autofunction:: PIL.ImageChops.overlay +.. autofunction:: PIL.ImageChops.offset +.. autofunction:: PIL.ImageChops.screen +.. autofunction:: PIL.ImageChops.subtract +.. autofunction:: PIL.ImageChops.subtract_modulo diff --git a/docs/reference/ImageCms.rst b/docs/reference/ImageCms.rst index 96bd14dd3..5db2b410b 100644 --- a/docs/reference/ImageCms.rst +++ b/docs/reference/ImageCms.rst @@ -1,389 +1,389 @@ -.. py:module:: PIL.ImageCms -.. py:currentmodule:: PIL.ImageCms - -:py:mod:`~PIL.ImageCms` Module -============================== - -The :py:mod:`~PIL.ImageCms` module provides color profile management -support using the LittleCMS2 color management engine, based on Kevin -Cazabon's PyCMS library. - -.. autoclass:: ImageCmsProfile - :members: - :special-members: __init__ -.. autoclass:: ImageCmsTransform - :members: - :undoc-members: - :show-inheritance: -.. autoexception:: PyCMSError - -Constants ---------- - -.. autoclass:: Intent - :members: - :member-order: bysource - :undoc-members: - :show-inheritance: -.. autoclass:: Direction - :members: - :member-order: bysource - :undoc-members: - :show-inheritance: -.. autoclass:: Flags - :members: - :member-order: bysource - :undoc-members: - :show-inheritance: - -Functions ---------- - -.. autofunction:: applyTransform -.. autofunction:: buildProofTransform -.. autofunction:: buildProofTransformFromOpenProfiles -.. autofunction:: buildTransform -.. autofunction:: buildTransformFromOpenProfiles -.. autofunction:: createProfile -.. autofunction:: getDefaultIntent -.. autofunction:: getOpenProfile -.. autofunction:: getProfileCopyright -.. autofunction:: getProfileDescription -.. autofunction:: getProfileInfo -.. autofunction:: getProfileManufacturer -.. autofunction:: getProfileModel -.. autofunction:: getProfileName -.. autofunction:: get_display_profile -.. autofunction:: isIntentSupported -.. autofunction:: profileToProfile -.. autofunction:: versions - -CmsProfile ----------- - -The ICC color profiles are wrapped in an instance of the class -:py:class:`~core.CmsProfile`. The specification ICC.1:2010 contains more -information about the meaning of the values in ICC profiles. - -For convenience, all XYZ-values are also given as xyY-values (so they -can be easily displayed in a chromaticity diagram, for example). - -.. py:currentmodule:: PIL.ImageCms.core -.. py:class:: CmsProfile - :canonical: PIL._imagingcms.CmsProfile - - .. py:attribute:: creation_date - :type: datetime.datetime | None - - Date and time this profile was first created (see 7.2.1 of ICC.1:2010). - - .. py:attribute:: version - :type: float - - The version number of the ICC standard that this profile follows - (e.g. ``2.0``). - - .. py:attribute:: icc_version - :type: int - - Same as ``version``, but in encoded format (see 7.2.4 of ICC.1:2010). - - .. py:attribute:: device_class - :type: str - - 4-character string identifying the profile class. One of - ``scnr``, ``mntr``, ``prtr``, ``link``, ``spac``, ``abst``, - ``nmcl`` (see 7.2.5 of ICC.1:2010 for details). - - .. py:attribute:: xcolor_space - :type: str - - 4-character string (padded with whitespace) identifying the color - space, e.g. ``XYZâ£``, ``RGBâ£`` or ``CMYK`` (see 7.2.6 of - ICC.1:2010 for details). - - .. py:attribute:: connection_space - :type: str - - 4-character string (padded with whitespace) identifying the color - space on the B-side of the transform (see 7.2.7 of ICC.1:2010 for - details). - - .. py:attribute:: header_flags - :type: int - - The encoded header flags of the profile (see 7.2.11 of ICC.1:2010 - for details). - - .. py:attribute:: header_manufacturer - :type: str - - 4-character string (padded with whitespace) identifying the device - manufacturer, which shall match the signature contained in the - appropriate section of the ICC signature registry found at - www.color.org (see 7.2.12 of ICC.1:2010). - - .. py:attribute:: header_model - :type: str - - 4-character string (padded with whitespace) identifying the device - model, which shall match the signature contained in the - appropriate section of the ICC signature registry found at - www.color.org (see 7.2.13 of ICC.1:2010). - - .. py:attribute:: attributes - :type: int - - Flags used to identify attributes unique to the particular device - setup for which the profile is applicable (see 7.2.14 of - ICC.1:2010 for details). - - .. py:attribute:: rendering_intent - :type: int - - The rendering intent to use when combining this profile with - another profile (usually overridden at run-time, but provided here - for DeviceLink and embedded source profiles, see 7.2.15 of ICC.1:2010). - - One of ``ImageCms.Intent.ABSOLUTE_COLORIMETRIC``, ``ImageCms.Intent.PERCEPTUAL``, - ``ImageCms.Intent.RELATIVE_COLORIMETRIC`` and ``ImageCms.Intent.SATURATION``. - - .. py:attribute:: profile_id - :type: bytes - - A sequence of 16 bytes identifying the profile (via a specially - constructed MD5 sum), or 16 binary zeroes if the profile ID has - not been calculated (see 7.2.18 of ICC.1:2010). - - .. py:attribute:: copyright - :type: str | None - - The text copyright information for the profile (see 9.2.21 of ICC.1:2010). - - .. py:attribute:: manufacturer - :type: str | None - - The (English) display string for the device manufacturer (see - 9.2.22 of ICC.1:2010). - - .. py:attribute:: model - :type: str | None - - The (English) display string for the device model of the device - for which this profile is created (see 9.2.23 of ICC.1:2010). - - .. py:attribute:: profile_description - :type: str | None - - The (English) display string for the profile description (see - 9.2.41 of ICC.1:2010). - - .. py:attribute:: target - :type: str | None - - The name of the registered characterization data set, or the - measurement data for a characterization target (see 9.2.14 of - ICC.1:2010). - - .. py:attribute:: red_colorant - :type: tuple[tuple[float, float, float], tuple[float, float, float]] | None - - The first column in the matrix used in matrix/TRC transforms (see 9.2.44 of ICC.1:2010). - - The value is in the format ``((X, Y, Z), (x, y, Y))``, if available. - - .. py:attribute:: green_colorant - :type: tuple[tuple[float, float, float], tuple[float, float, float]] | None - - The second column in the matrix used in matrix/TRC transforms (see 9.2.30 of ICC.1:2010). - - The value is in the format ``((X, Y, Z), (x, y, Y))``, if available. - - .. py:attribute:: blue_colorant - :type: tuple[tuple[float, float, float], tuple[float, float, float]] | None - - The third column in the matrix used in matrix/TRC transforms (see 9.2.4 of ICC.1:2010). - - The value is in the format ``((X, Y, Z), (x, y, Y))``, if available. - - .. py:attribute:: luminance - :type: tuple[tuple[float, float, float], tuple[float, float, float]] | None - - The absolute luminance of emissive devices in candelas per square - metre as described by the Y channel (see 9.2.32 of ICC.1:2010). - - The value is in the format ``((X, Y, Z), (x, y, Y))``, if available. - - .. py:attribute:: chromaticity - :type: tuple[tuple[float, float, float], tuple[float, float, float], tuple[float, float, float]] | None - - The data of the phosphor/colorant chromaticity set used (red, - green and blue channels, see 9.2.16 of ICC.1:2010). - - The value is in the format ``((x, y, Y), (x, y, Y), (x, y, Y))``, if available. - - .. py:attribute:: chromatic_adaption - :type: tuple[tuple[tuple[float, float, float], tuple[float, float, float], tuple[float, float, float]], tuple[tuple[float, float, float], tuple[float, float, float], tuple[float, float, float]]] | None - - The chromatic adaption matrix converts a color measured using the - actual illumination conditions and relative to the actual adopted - white, to a color relative to the PCS adopted white, with - complete adaptation from the actual adopted white chromaticity to - the PCS adopted white chromaticity (see 9.2.15 of ICC.1:2010). - - Two 3-tuples of floats are returned in a 2-tuple, - one in (X, Y, Z) space and one in (x, y, Y) space. - - .. py:attribute:: colorant_table - :type: list[str] - - This tag identifies the colorants used in the profile by a unique - name and set of PCSXYZ or PCSLAB values (see 9.2.19 of - ICC.1:2010). - - .. py:attribute:: colorant_table_out - :type: list[str] - - This tag identifies the colorants used in the profile by a unique - name and set of PCSLAB values (for DeviceLink profiles only, see - 9.2.19 of ICC.1:2010). - - .. py:attribute:: colorimetric_intent - :type: str | None - - 4-character string (padded with whitespace) identifying the image - state of PCS colorimetry produced using the colorimetric intent - transforms (see 9.2.20 of ICC.1:2010 for details). - - .. py:attribute:: perceptual_rendering_intent_gamut - :type: str | None - - 4-character string (padded with whitespace) identifying the (one) - standard reference medium gamut (see 9.2.37 of ICC.1:2010 for - details). - - .. py:attribute:: saturation_rendering_intent_gamut - :type: str | None - - 4-character string (padded with whitespace) identifying the (one) - standard reference medium gamut (see 9.2.37 of ICC.1:2010 for - details). - - .. py:attribute:: technology - :type: str | None - - 4-character string (padded with whitespace) identifying the device - technology (see 9.2.47 of ICC.1:2010 for details). - - .. py:attribute:: media_black_point - :type: tuple[tuple[float, float, float], tuple[float, float, float]] | None - - This tag specifies the media black point and is used for - generating absolute colorimetry. - - This tag was available in ICC 3.2, but it is removed from - version 4. - - The value is in the format ``((X, Y, Z), (x, y, Y))``, if available. - - .. py:attribute:: media_white_point_temperature - :type: float | None - - Calculates the white point temperature (see the LCMS documentation - for more information). - - .. py:attribute:: viewing_condition - :type: str | None - - The (English) display string for the viewing conditions (see - 9.2.48 of ICC.1:2010). - - .. py:attribute:: screening_description - :type: str | None - - The (English) display string for the screening conditions. - - This tag was available in ICC 3.2, but it is removed from - version 4. - - .. py:attribute:: red_primary - :type: tuple[tuple[float, float, float], tuple[float, float, float]] | None - - The XYZ-transformed of the RGB primary color red (1, 0, 0). - - The value is in the format ``((X, Y, Z), (x, y, Y))``, if available. - - .. py:attribute:: green_primary - :type: tuple[tuple[float, float, float], tuple[float, float, float]] | None - - The XYZ-transformed of the RGB primary color green (0, 1, 0). - - The value is in the format ``((X, Y, Z), (x, y, Y))``, if available. - - .. py:attribute:: blue_primary - :type: tuple[tuple[float, float, float], tuple[float, float, float]] | None - - The XYZ-transformed of the RGB primary color blue (0, 0, 1). - - The value is in the format ``((X, Y, Z), (x, y, Y))``, if available. - - .. py:attribute:: is_matrix_shaper - :type: bool - - True if this profile is implemented as a matrix shaper (see - documentation on LCMS). - - .. py:attribute:: clut - :type: dict[int, tuple[bool, bool, bool]] | None - - Returns a dictionary of all supported intents and directions for - the CLUT model. - - The dictionary is indexed by intents - (``ImageCms.Intent.ABSOLUTE_COLORIMETRIC``, - ``ImageCms.Intent.PERCEPTUAL``, - ``ImageCms.Intent.RELATIVE_COLORIMETRIC`` and - ``ImageCms.Intent.SATURATION``). - - The values are 3-tuples indexed by directions - (``ImageCms.Direction.INPUT``, ``ImageCms.Direction.OUTPUT``, - ``ImageCms.Direction.PROOF``). - - The elements of the tuple are booleans. If the value is ``True``, - that intent is supported for that direction. - - .. py:attribute:: intent_supported - :type: dict[int, tuple[bool, bool, bool]] | None - - Returns a dictionary of all supported intents and directions. - - The dictionary is indexed by intents - (``ImageCms.Intent.ABSOLUTE_COLORIMETRIC``, - ``ImageCms.Intent.PERCEPTUAL``, - ``ImageCms.Intent.RELATIVE_COLORIMETRIC`` and - ``ImageCms.Intent.SATURATION``). - - The values are 3-tuples indexed by directions - (``ImageCms.Direction.INPUT``, ``ImageCms.Direction.OUTPUT``, - ``ImageCms.Direction.PROOF``). - - The elements of the tuple are booleans. If the value is ``True``, - that intent is supported for that direction. - - There is one function defined on the class: - - .. py:method:: is_intent_supported(intent: int, direction: int, /) - - Returns if the intent is supported for the given direction. - - Note that you can also get this information for all intents and directions - with :py:attr:`.intent_supported`. - - :param intent: One of ``ImageCms.Intent.ABSOLUTE_COLORIMETRIC``, - ``ImageCms.Intent.PERCEPTUAL``, - ``ImageCms.Intent.RELATIVE_COLORIMETRIC`` - and ``ImageCms.Intent.SATURATION``. - :param direction: One of ``ImageCms.Direction.INPUT``, - ``ImageCms.Direction.OUTPUT`` - and ``ImageCms.Direction.PROOF`` - :return: Boolean if the intent and direction is supported. +.. py:module:: PIL.ImageCms +.. py:currentmodule:: PIL.ImageCms + +:py:mod:`~PIL.ImageCms` Module +============================== + +The :py:mod:`~PIL.ImageCms` module provides color profile management +support using the LittleCMS2 color management engine, based on Kevin +Cazabon's PyCMS library. + +.. autoclass:: ImageCmsProfile + :members: + :special-members: __init__ +.. autoclass:: ImageCmsTransform + :members: + :undoc-members: + :show-inheritance: +.. autoexception:: PyCMSError + +Constants +--------- + +.. autoclass:: Intent + :members: + :member-order: bysource + :undoc-members: + :show-inheritance: +.. autoclass:: Direction + :members: + :member-order: bysource + :undoc-members: + :show-inheritance: +.. autoclass:: Flags + :members: + :member-order: bysource + :undoc-members: + :show-inheritance: + +Functions +--------- + +.. autofunction:: applyTransform +.. autofunction:: buildProofTransform +.. autofunction:: buildProofTransformFromOpenProfiles +.. autofunction:: buildTransform +.. autofunction:: buildTransformFromOpenProfiles +.. autofunction:: createProfile +.. autofunction:: getDefaultIntent +.. autofunction:: getOpenProfile +.. autofunction:: getProfileCopyright +.. autofunction:: getProfileDescription +.. autofunction:: getProfileInfo +.. autofunction:: getProfileManufacturer +.. autofunction:: getProfileModel +.. autofunction:: getProfileName +.. autofunction:: get_display_profile +.. autofunction:: isIntentSupported +.. autofunction:: profileToProfile +.. autofunction:: versions + +CmsProfile +---------- + +The ICC color profiles are wrapped in an instance of the class +:py:class:`~core.CmsProfile`. The specification ICC.1:2010 contains more +information about the meaning of the values in ICC profiles. + +For convenience, all XYZ-values are also given as xyY-values (so they +can be easily displayed in a chromaticity diagram, for example). + +.. py:currentmodule:: PIL.ImageCms.core +.. py:class:: CmsProfile + :canonical: PIL._imagingcms.CmsProfile + + .. py:attribute:: creation_date + :type: datetime.datetime | None + + Date and time this profile was first created (see 7.2.1 of ICC.1:2010). + + .. py:attribute:: version + :type: float + + The version number of the ICC standard that this profile follows + (e.g. ``2.0``). + + .. py:attribute:: icc_version + :type: int + + Same as ``version``, but in encoded format (see 7.2.4 of ICC.1:2010). + + .. py:attribute:: device_class + :type: str + + 4-character string identifying the profile class. One of + ``scnr``, ``mntr``, ``prtr``, ``link``, ``spac``, ``abst``, + ``nmcl`` (see 7.2.5 of ICC.1:2010 for details). + + .. py:attribute:: xcolor_space + :type: str + + 4-character string (padded with whitespace) identifying the color + space, e.g. ``XYZâ£``, ``RGBâ£`` or ``CMYK`` (see 7.2.6 of + ICC.1:2010 for details). + + .. py:attribute:: connection_space + :type: str + + 4-character string (padded with whitespace) identifying the color + space on the B-side of the transform (see 7.2.7 of ICC.1:2010 for + details). + + .. py:attribute:: header_flags + :type: int + + The encoded header flags of the profile (see 7.2.11 of ICC.1:2010 + for details). + + .. py:attribute:: header_manufacturer + :type: str + + 4-character string (padded with whitespace) identifying the device + manufacturer, which shall match the signature contained in the + appropriate section of the ICC signature registry found at + www.color.org (see 7.2.12 of ICC.1:2010). + + .. py:attribute:: header_model + :type: str + + 4-character string (padded with whitespace) identifying the device + model, which shall match the signature contained in the + appropriate section of the ICC signature registry found at + www.color.org (see 7.2.13 of ICC.1:2010). + + .. py:attribute:: attributes + :type: int + + Flags used to identify attributes unique to the particular device + setup for which the profile is applicable (see 7.2.14 of + ICC.1:2010 for details). + + .. py:attribute:: rendering_intent + :type: int + + The rendering intent to use when combining this profile with + another profile (usually overridden at run-time, but provided here + for DeviceLink and embedded source profiles, see 7.2.15 of ICC.1:2010). + + One of ``ImageCms.Intent.ABSOLUTE_COLORIMETRIC``, ``ImageCms.Intent.PERCEPTUAL``, + ``ImageCms.Intent.RELATIVE_COLORIMETRIC`` and ``ImageCms.Intent.SATURATION``. + + .. py:attribute:: profile_id + :type: bytes + + A sequence of 16 bytes identifying the profile (via a specially + constructed MD5 sum), or 16 binary zeroes if the profile ID has + not been calculated (see 7.2.18 of ICC.1:2010). + + .. py:attribute:: copyright + :type: str | None + + The text copyright information for the profile (see 9.2.21 of ICC.1:2010). + + .. py:attribute:: manufacturer + :type: str | None + + The (English) display string for the device manufacturer (see + 9.2.22 of ICC.1:2010). + + .. py:attribute:: model + :type: str | None + + The (English) display string for the device model of the device + for which this profile is created (see 9.2.23 of ICC.1:2010). + + .. py:attribute:: profile_description + :type: str | None + + The (English) display string for the profile description (see + 9.2.41 of ICC.1:2010). + + .. py:attribute:: target + :type: str | None + + The name of the registered characterization data set, or the + measurement data for a characterization target (see 9.2.14 of + ICC.1:2010). + + .. py:attribute:: red_colorant + :type: tuple[tuple[float, float, float], tuple[float, float, float]] | None + + The first column in the matrix used in matrix/TRC transforms (see 9.2.44 of ICC.1:2010). + + The value is in the format ``((X, Y, Z), (x, y, Y))``, if available. + + .. py:attribute:: green_colorant + :type: tuple[tuple[float, float, float], tuple[float, float, float]] | None + + The second column in the matrix used in matrix/TRC transforms (see 9.2.30 of ICC.1:2010). + + The value is in the format ``((X, Y, Z), (x, y, Y))``, if available. + + .. py:attribute:: blue_colorant + :type: tuple[tuple[float, float, float], tuple[float, float, float]] | None + + The third column in the matrix used in matrix/TRC transforms (see 9.2.4 of ICC.1:2010). + + The value is in the format ``((X, Y, Z), (x, y, Y))``, if available. + + .. py:attribute:: luminance + :type: tuple[tuple[float, float, float], tuple[float, float, float]] | None + + The absolute luminance of emissive devices in candelas per square + metre as described by the Y channel (see 9.2.32 of ICC.1:2010). + + The value is in the format ``((X, Y, Z), (x, y, Y))``, if available. + + .. py:attribute:: chromaticity + :type: tuple[tuple[float, float, float], tuple[float, float, float], tuple[float, float, float]] | None + + The data of the phosphor/colorant chromaticity set used (red, + green and blue channels, see 9.2.16 of ICC.1:2010). + + The value is in the format ``((x, y, Y), (x, y, Y), (x, y, Y))``, if available. + + .. py:attribute:: chromatic_adaption + :type: tuple[tuple[tuple[float, float, float], tuple[float, float, float], tuple[float, float, float]], tuple[tuple[float, float, float], tuple[float, float, float], tuple[float, float, float]]] | None + + The chromatic adaption matrix converts a color measured using the + actual illumination conditions and relative to the actual adopted + white, to a color relative to the PCS adopted white, with + complete adaptation from the actual adopted white chromaticity to + the PCS adopted white chromaticity (see 9.2.15 of ICC.1:2010). + + Two 3-tuples of floats are returned in a 2-tuple, + one in (X, Y, Z) space and one in (x, y, Y) space. + + .. py:attribute:: colorant_table + :type: list[str] + + This tag identifies the colorants used in the profile by a unique + name and set of PCSXYZ or PCSLAB values (see 9.2.19 of + ICC.1:2010). + + .. py:attribute:: colorant_table_out + :type: list[str] + + This tag identifies the colorants used in the profile by a unique + name and set of PCSLAB values (for DeviceLink profiles only, see + 9.2.19 of ICC.1:2010). + + .. py:attribute:: colorimetric_intent + :type: str | None + + 4-character string (padded with whitespace) identifying the image + state of PCS colorimetry produced using the colorimetric intent + transforms (see 9.2.20 of ICC.1:2010 for details). + + .. py:attribute:: perceptual_rendering_intent_gamut + :type: str | None + + 4-character string (padded with whitespace) identifying the (one) + standard reference medium gamut (see 9.2.37 of ICC.1:2010 for + details). + + .. py:attribute:: saturation_rendering_intent_gamut + :type: str | None + + 4-character string (padded with whitespace) identifying the (one) + standard reference medium gamut (see 9.2.37 of ICC.1:2010 for + details). + + .. py:attribute:: technology + :type: str | None + + 4-character string (padded with whitespace) identifying the device + technology (see 9.2.47 of ICC.1:2010 for details). + + .. py:attribute:: media_black_point + :type: tuple[tuple[float, float, float], tuple[float, float, float]] | None + + This tag specifies the media black point and is used for + generating absolute colorimetry. + + This tag was available in ICC 3.2, but it is removed from + version 4. + + The value is in the format ``((X, Y, Z), (x, y, Y))``, if available. + + .. py:attribute:: media_white_point_temperature + :type: float | None + + Calculates the white point temperature (see the LCMS documentation + for more information). + + .. py:attribute:: viewing_condition + :type: str | None + + The (English) display string for the viewing conditions (see + 9.2.48 of ICC.1:2010). + + .. py:attribute:: screening_description + :type: str | None + + The (English) display string for the screening conditions. + + This tag was available in ICC 3.2, but it is removed from + version 4. + + .. py:attribute:: red_primary + :type: tuple[tuple[float, float, float], tuple[float, float, float]] | None + + The XYZ-transformed of the RGB primary color red (1, 0, 0). + + The value is in the format ``((X, Y, Z), (x, y, Y))``, if available. + + .. py:attribute:: green_primary + :type: tuple[tuple[float, float, float], tuple[float, float, float]] | None + + The XYZ-transformed of the RGB primary color green (0, 1, 0). + + The value is in the format ``((X, Y, Z), (x, y, Y))``, if available. + + .. py:attribute:: blue_primary + :type: tuple[tuple[float, float, float], tuple[float, float, float]] | None + + The XYZ-transformed of the RGB primary color blue (0, 0, 1). + + The value is in the format ``((X, Y, Z), (x, y, Y))``, if available. + + .. py:attribute:: is_matrix_shaper + :type: bool + + True if this profile is implemented as a matrix shaper (see + documentation on LCMS). + + .. py:attribute:: clut + :type: dict[int, tuple[bool, bool, bool]] | None + + Returns a dictionary of all supported intents and directions for + the CLUT model. + + The dictionary is indexed by intents + (``ImageCms.Intent.ABSOLUTE_COLORIMETRIC``, + ``ImageCms.Intent.PERCEPTUAL``, + ``ImageCms.Intent.RELATIVE_COLORIMETRIC`` and + ``ImageCms.Intent.SATURATION``). + + The values are 3-tuples indexed by directions + (``ImageCms.Direction.INPUT``, ``ImageCms.Direction.OUTPUT``, + ``ImageCms.Direction.PROOF``). + + The elements of the tuple are booleans. If the value is ``True``, + that intent is supported for that direction. + + .. py:attribute:: intent_supported + :type: dict[int, tuple[bool, bool, bool]] | None + + Returns a dictionary of all supported intents and directions. + + The dictionary is indexed by intents + (``ImageCms.Intent.ABSOLUTE_COLORIMETRIC``, + ``ImageCms.Intent.PERCEPTUAL``, + ``ImageCms.Intent.RELATIVE_COLORIMETRIC`` and + ``ImageCms.Intent.SATURATION``). + + The values are 3-tuples indexed by directions + (``ImageCms.Direction.INPUT``, ``ImageCms.Direction.OUTPUT``, + ``ImageCms.Direction.PROOF``). + + The elements of the tuple are booleans. If the value is ``True``, + that intent is supported for that direction. + + There is one function defined on the class: + + .. py:method:: is_intent_supported(intent: int, direction: int, /) + + Returns if the intent is supported for the given direction. + + Note that you can also get this information for all intents and directions + with :py:attr:`.intent_supported`. + + :param intent: One of ``ImageCms.Intent.ABSOLUTE_COLORIMETRIC``, + ``ImageCms.Intent.PERCEPTUAL``, + ``ImageCms.Intent.RELATIVE_COLORIMETRIC`` + and ``ImageCms.Intent.SATURATION``. + :param direction: One of ``ImageCms.Direction.INPUT``, + ``ImageCms.Direction.OUTPUT`` + and ``ImageCms.Direction.PROOF`` + :return: Boolean if the intent and direction is supported. diff --git a/docs/reference/ImageColor.rst b/docs/reference/ImageColor.rst index 31faeac78..2ccb910ae 100644 --- a/docs/reference/ImageColor.rst +++ b/docs/reference/ImageColor.rst @@ -1,65 +1,65 @@ -.. py:module:: PIL.ImageColor -.. py:currentmodule:: PIL.ImageColor - -:py:mod:`~PIL.ImageColor` Module -================================ - -The :py:mod:`~PIL.ImageColor` module contains color tables and converters from -CSS3-style color specifiers to RGB tuples. This module is used by -:py:meth:`PIL.Image.new` and the :py:mod:`~PIL.ImageDraw` module, among -others. - -.. _color-names: - -Color Names ------------ - -The ImageColor module supports the following string formats: - -* Hexadecimal color specifiers, given as ``#rgb``, ``#rgba``, ``#rrggbb`` or - ``#rrggbbaa``, where ``r`` is red, ``g`` is green, ``b`` is blue and ``a`` is - alpha (also called 'opacity'). For example, ``#ff0000`` specifies pure red, - and ``#ff0000cc`` specifies red with 80% opacity (``cc`` is 204 in decimal - form, and 204 / 255 = 0.8). - -* RGB functions, given as ``rgb(red, green, blue)`` where the color values are - integers in the range 0 to 255. Alternatively, the color values can be given - as three percentages (0% to 100%). For example, ``rgb(255,0,0)`` and - ``rgb(100%,0%,0%)`` both specify pure red. - -* Hue-Saturation-Lightness (HSL) functions, given as ``hsl(hue, saturation%, - lightness%)`` where hue is the color given as an angle between 0 and 360 - (red=0, green=120, blue=240), saturation is a value between 0% and 100% - (gray=0%, full color=100%), and lightness is a value between 0% and 100% - (black=0%, normal=50%, white=100%). For example, ``hsl(0,100%,50%)`` is pure - red. - -* Hue-Saturation-Value (HSV) functions, given as ``hsv(hue, saturation%, - value%)`` where hue and saturation are the same as HSL, and value is between - 0% and 100% (black=0%, normal=100%). For example, ``hsv(0,100%,100%)`` is - pure red. This format is also known as Hue-Saturation-Brightness (HSB), and - can be given as ``hsb(hue, saturation%, brightness%)``, where each of the - values are used as they are in HSV. - -* Common HTML color names. The :py:mod:`~PIL.ImageColor` module provides some - 140 standard color names, based on the colors supported by the X Window - system and most web browsers. color names are case insensitive. For example, - ``red`` and ``Red`` both specify pure red. - -Functions ---------- - -.. py:method:: getrgb(color) - - Convert a color string to an RGB tuple. If the string cannot be parsed, - this function raises a :py:exc:`ValueError` exception. - - .. versionadded:: 1.1.4 - -.. py:method:: getcolor(color, mode) - - Same as :py:func:`~PIL.ImageColor.getrgb`, but converts the RGB value to a - grayscale value if the mode is not color or a palette image. If the string - cannot be parsed, this function raises a :py:exc:`ValueError` exception. - - .. versionadded:: 1.1.4 +.. py:module:: PIL.ImageColor +.. py:currentmodule:: PIL.ImageColor + +:py:mod:`~PIL.ImageColor` Module +================================ + +The :py:mod:`~PIL.ImageColor` module contains color tables and converters from +CSS3-style color specifiers to RGB tuples. This module is used by +:py:meth:`PIL.Image.new` and the :py:mod:`~PIL.ImageDraw` module, among +others. + +.. _color-names: + +Color Names +----------- + +The ImageColor module supports the following string formats: + +* Hexadecimal color specifiers, given as ``#rgb``, ``#rgba``, ``#rrggbb`` or + ``#rrggbbaa``, where ``r`` is red, ``g`` is green, ``b`` is blue and ``a`` is + alpha (also called 'opacity'). For example, ``#ff0000`` specifies pure red, + and ``#ff0000cc`` specifies red with 80% opacity (``cc`` is 204 in decimal + form, and 204 / 255 = 0.8). + +* RGB functions, given as ``rgb(red, green, blue)`` where the color values are + integers in the range 0 to 255. Alternatively, the color values can be given + as three percentages (0% to 100%). For example, ``rgb(255,0,0)`` and + ``rgb(100%,0%,0%)`` both specify pure red. + +* Hue-Saturation-Lightness (HSL) functions, given as ``hsl(hue, saturation%, + lightness%)`` where hue is the color given as an angle between 0 and 360 + (red=0, green=120, blue=240), saturation is a value between 0% and 100% + (gray=0%, full color=100%), and lightness is a value between 0% and 100% + (black=0%, normal=50%, white=100%). For example, ``hsl(0,100%,50%)`` is pure + red. + +* Hue-Saturation-Value (HSV) functions, given as ``hsv(hue, saturation%, + value%)`` where hue and saturation are the same as HSL, and value is between + 0% and 100% (black=0%, normal=100%). For example, ``hsv(0,100%,100%)`` is + pure red. This format is also known as Hue-Saturation-Brightness (HSB), and + can be given as ``hsb(hue, saturation%, brightness%)``, where each of the + values are used as they are in HSV. + +* Common HTML color names. The :py:mod:`~PIL.ImageColor` module provides some + 140 standard color names, based on the colors supported by the X Window + system and most web browsers. color names are case insensitive. For example, + ``red`` and ``Red`` both specify pure red. + +Functions +--------- + +.. py:method:: getrgb(color) + + Convert a color string to an RGB tuple. If the string cannot be parsed, + this function raises a :py:exc:`ValueError` exception. + + .. versionadded:: 1.1.4 + +.. py:method:: getcolor(color, mode) + + Same as :py:func:`~PIL.ImageColor.getrgb`, but converts the RGB value to a + grayscale value if the mode is not color or a palette image. If the string + cannot be parsed, this function raises a :py:exc:`ValueError` exception. + + .. versionadded:: 1.1.4 diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 1404869ca..fd046de72 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -1,713 +1,713 @@ -.. py:module:: PIL.ImageDraw -.. py:currentmodule:: PIL.ImageDraw - -:py:mod:`~PIL.ImageDraw` Module -=============================== - -The :py:mod:`~PIL.ImageDraw` module provides simple 2D graphics for -:py:class:`~PIL.Image.Image` objects. You can use this module to create new -images, annotate or retouch existing images, and to generate graphics on the -fly for web use. - -For a more advanced drawing library for PIL, see the `aggdraw module`_. - -.. _aggdraw module: https://github.com/pytroll/aggdraw - -Example: Draw a gray cross over an image ----------------------------------------- - -:: - - import sys - from PIL import Image, ImageDraw - - with Image.open("hopper.jpg") as im: - - draw = ImageDraw.Draw(im) - draw.line((0, 0) + im.size, fill=128) - draw.line((0, im.size[1], im.size[0], 0), fill=128) - - # write to stdout - im.save(sys.stdout, "PNG") - - -Concepts --------- - -Coordinates -^^^^^^^^^^^ - -The graphics interface uses the same coordinate system as PIL itself, with (0, -0) in the upper left corner. Any pixels drawn outside of the image bounds will -be discarded. - -Colors -^^^^^^ - -To specify colors, you can use numbers or tuples just as you would use with -:py:meth:`PIL.Image.new` or :py:meth:`PIL.Image.Image.putpixel`. For “1â€, -“Lâ€, and “I†images, use integers. For “RGB†images, use a 3-tuple containing -integer values. For “F†images, use integer or floating point values. - -For palette images (mode “Pâ€), use integers as color indexes. In 1.1.4 and -later, you can also use RGB 3-tuples or color names (see below). The drawing -layer will automatically assign color indexes, as long as you don’t draw with -more than 256 colors. - -Color Names -^^^^^^^^^^^ - -See :ref:`color-names` for the color names supported by Pillow. - -Fonts -^^^^^ - -PIL can use bitmap fonts or OpenType/TrueType fonts. - -Bitmap fonts are stored in PIL's own format, where each font typically consists -of two files, one named .pil and the other usually named .pbm. The former -contains font metrics, the latter raster data. - -To load a bitmap font, use the load functions in the :py:mod:`~PIL.ImageFont` -module. - -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 ----------------------------------- - -:: - - from PIL import Image, ImageDraw, ImageFont - - # get an image - with Image.open("Pillow/Tests/images/hopper.png").convert("RGBA") as base: - - # make a blank image for the text, initialized to transparent text color - txt = Image.new("RGBA", base.size, (255, 255, 255, 0)) - - # get a font - fnt = ImageFont.truetype("Pillow/Tests/fonts/FreeMono.ttf", 40) - # get a drawing context - d = ImageDraw.Draw(txt) - - # draw text, half opacity - d.text((10, 10), "Hello", font=fnt, fill=(255, 255, 255, 128)) - # draw text, full opacity - d.text((10, 60), "World", font=fnt, fill=(255, 255, 255, 255)) - - out = Image.alpha_composite(base, txt) - - out.show() - -Example: Draw Multiline Text ----------------------------- - -:: - - from PIL import Image, ImageDraw, ImageFont - - # create an image - out = Image.new("RGB", (150, 100), (255, 255, 255)) - - # get a font - fnt = ImageFont.truetype("Pillow/Tests/fonts/FreeMono.ttf", 40) - # get a drawing context - d = ImageDraw.Draw(out) - - # draw multiline text - d.multiline_text((10, 10), "Hello\nWorld", font=fnt, fill=(0, 0, 0)) - - out.show() - - -Functions ---------- - -.. py:method:: Draw(im, mode=None) - - Creates an object that can be used to draw in the given image. - - Note that the image will be modified in place. - - :param im: The image to draw in. - :param mode: Optional mode to use for color values. For RGB - images, this argument can be RGB or RGBA (to blend the - drawing into the image). For all other modes, this argument - must be the same as the image mode. If omitted, the mode - defaults to the mode of the image. - -Attributes ----------- - -.. py:attribute:: ImageDraw.fill - :type: bool - :value: False - - Selects whether :py:attr:`ImageDraw.ink` should be used as a fill or outline color. - -.. py:attribute:: ImageDraw.font - - The current default font. - - Can be set per instance:: - - from PIL import ImageDraw, ImageFont - draw = ImageDraw.Draw(image) - draw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") - - Or globally for all future ImageDraw instances:: - - from PIL import ImageDraw, ImageFont - ImageDraw.ImageDraw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") - -.. py:attribute:: ImageDraw.fontmode - - The current font drawing mode. - - Set to ``"1"`` to disable antialiasing or ``"L"`` to enable it. - -.. py:attribute:: ImageDraw.ink - :type: int - - The internal representation of the current default color. - -Methods -------- - -.. py:method:: ImageDraw.getfont() - - Get the current default font, :py:attr:`ImageDraw.font`. - - If the current default font is ``None``, - it is initialized with :py:func:`.ImageFont.load_default`. - - :returns: An image font. - -.. py:method:: ImageDraw.arc(xy, start, end, fill=None, width=0) - - Draws an arc (a portion of a circle outline) between the start and end - angles, inside the given bounding box. - - :param xy: Two points to define the bounding box. Sequence of ``[(x0, y0), - (x1, y1)]`` or ``[x0, y0, x1, y1]``, where ``x1 >= x0`` and ``y1 >= - y0``. - :param start: Starting angle, in degrees. Angles are measured from 3 - o'clock, increasing clockwise. - :param end: Ending angle, in degrees. - :param fill: Color to use for the arc. - :param width: The line width, in pixels. - - .. versionadded:: 5.3.0 - -.. py:method:: ImageDraw.bitmap(xy, bitmap, fill=None) - - Draws a bitmap (mask) at the given position, using the current fill color - for the non-zero portions. The bitmap should be a valid transparency mask - (mode “1â€) or matte (mode “L†or “RGBAâ€). - - This is equivalent to doing ``image.paste(xy, color, bitmap)``. - - To paste pixel data into an image, use the - :py:meth:`~PIL.Image.Image.paste` method on the image itself. - -.. py:method:: ImageDraw.chord(xy, start, end, fill=None, outline=None, width=1) - - Same as :py:meth:`~PIL.ImageDraw.ImageDraw.arc`, but connects the end points - with a straight line. - - :param xy: Two points to define the bounding box. Sequence of ``[(x0, y0), - (x1, y1)]`` or ``[x0, y0, x1, y1]``, where ``x1 >= x0`` and ``y1 >= - y0``. - :param outline: Color to use for the outline. - :param fill: Color to use for the fill. - :param width: The line width, in pixels. - - .. versionadded:: 5.3.0 - -.. py:method:: ImageDraw.circle(xy, radius, fill=None, outline=None, width=1) - - Draws a circle with a given radius centering on a point. - - .. versionadded:: 10.4.0 - - :param xy: The point for the center of the circle, e.g. ``(x, y)``. - :param radius: Radius of the circle. - :param outline: Color to use for the outline. - :param fill: Color to use for the fill. - :param width: The line width, in pixels. - -.. py:method:: ImageDraw.ellipse(xy, fill=None, outline=None, width=1) - - Draws an ellipse inside the given bounding box. - - :param xy: Two points to define the bounding box. Sequence of either - ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``, where ``x1 >= x0`` - and ``y1 >= y0``. - :param outline: Color to use for the outline. - :param fill: Color to use for the fill. - :param width: The line width, in pixels. - - .. versionadded:: 5.3.0 - -.. py:method:: ImageDraw.line(xy, fill=None, width=0, joint=None) - - Draws a line between the coordinates in the ``xy`` list. - The coordinate pixels are included in the drawn line. - - :param xy: Sequence of either 2-tuples like ``[(x, y), (x, y), ...]`` or - numeric values like ``[x, y, x, y, ...]``. - :param fill: Color to use for the line. - :param width: The line width, in pixels. - - .. versionadded:: 1.1.5 - - .. note:: This option was broken until version 1.1.6. - :param joint: Joint type between a sequence of lines. It can be ``"curve"``, for rounded edges, or :data:`None`. - - .. versionadded:: 5.3.0 - -.. py:method:: ImageDraw.pieslice(xy, start, end, fill=None, outline=None, width=1) - - Same as arc, but also draws straight lines between the end points and the - center of the bounding box. - - :param xy: Two points to define the bounding box. Sequence of ``[(x0, y0), - (x1, y1)]`` or ``[x0, y0, x1, y1]``, where ``x1 >= x0`` and ``y1 >= - y0``. - :param start: Starting angle, in degrees. Angles are measured from 3 - o'clock, increasing clockwise. - :param end: Ending angle, in degrees. - :param fill: Color to use for the fill. - :param outline: Color to use for the outline. - :param width: The line width, in pixels. - - .. versionadded:: 5.3.0 - -.. py:method:: ImageDraw.point(xy, fill=None) - - Draws points (individual pixels) at the given coordinates. - - :param xy: Sequence of either 2-tuples like ``[(x, y), (x, y), ...]`` or - numeric values like ``[x, y, x, y, ...]``. - :param fill: Color to use for the point. - -.. py:method:: ImageDraw.polygon(xy, fill=None, outline=None, width=1) - - Draws a polygon. - - The polygon outline consists of straight lines between the given - coordinates, plus a straight line between the last and the first - coordinate. The coordinate pixels are included in the drawn polygon. - - :param xy: Sequence of either 2-tuples like ``[(x, y), (x, y), ...]`` or - numeric values like ``[x, y, x, y, ...]``. - :param fill: Color to use for the fill. - :param outline: Color to use for the outline. - :param width: The line width, in pixels. - - -.. py:method:: ImageDraw.regular_polygon(bounding_circle, n_sides, rotation=0, fill=None, outline=None, width=1) - - Draws a regular polygon inscribed in ``bounding_circle``, - with ``n_sides``, and rotation of ``rotation`` degrees. - - :param bounding_circle: The bounding circle is a tuple defined - by a point and radius. - (e.g. ``bounding_circle=(x, y, r)`` or ``((x, y), r)``). - The polygon is inscribed in this circle. - :param n_sides: Number of sides - (e.g. ``n_sides=3`` for a triangle, ``6`` for a hexagon). - :param rotation: Apply an arbitrary rotation to the polygon - (e.g. ``rotation=90``, applies a 90 degree rotation). - :param fill: Color to use for the fill. - :param outline: Color to use for the outline. - :param width: The line width, in pixels. - - -.. py:method:: ImageDraw.rectangle(xy, fill=None, outline=None, width=1) - - Draws a rectangle. - - :param xy: Two points to define the bounding box. Sequence of either - ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``, where ``x1 >= x0`` and - ``y1 >= y0``. The bounding box is inclusive of both endpoints. - :param fill: Color to use for the fill. - :param outline: Color to use for the outline. - :param width: The line width, in pixels. - - .. versionadded:: 5.3.0 - -.. py:method:: ImageDraw.rounded_rectangle(xy, radius=0, fill=None, outline=None, width=1, corners=None) - - Draws a rounded rectangle. - - :param xy: Two points to define the bounding box. Sequence of either - ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``, where ``x1 >= x0`` and - ``y1 >= y0``. The bounding box is inclusive of both endpoints. - :param radius: Radius of the corners. - :param fill: Color to use for the fill. - :param outline: Color to use for the outline. - :param width: The line width, in pixels. - :param corners: A tuple of whether to round each corner, - ``(top_left, top_right, bottom_right, bottom_left)``. - Keyword-only argument. - - .. versionadded:: 8.2.0 - -.. py:method:: ImageDraw.shape(shape, fill=None, outline=None) - - .. warning:: This method is experimental. - - Draw a shape. - -.. py:method:: ImageDraw.text(xy, text, fill=None, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, stroke_fill=None, embedded_color=False, font_size=None) - - Draws the string at the given position. - - :param xy: The anchor coordinates of the text. - :param text: String to be drawn. If it contains any newline characters, - the text is passed on to - :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_text`. - :param fill: Color to use for the text. - :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. - :param anchor: The text anchor alignment. Determines the relative location of - the anchor to the text. The default alignment is top left, - specifically ``la`` for horizontal text and ``lt`` for - vertical text. See :ref:`text-anchors` for details. - This parameter is ignored for non-TrueType fonts. - - .. note:: This parameter was present in earlier versions - of Pillow, but implemented only in version 8.0.0. - - :param spacing: If the text is passed on to - :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_text`, - the number of pixels between lines. - :param align: If the text is passed on to - :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_text`, - ``"left"``, ``"center"`` or ``"right"``. Determines the relative alignment of lines. - Use the ``anchor`` parameter to specify the alignment to ``xy``. - :param direction: Direction of the text. It can be ``"rtl"`` (right to - left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). - Requires libraqm. - - .. versionadded:: 4.2.0 - - :param features: A list of OpenType font features to be used during text - layout. This is usually used to turn on optional - font features that are not enabled by default, - for example ``"dlig"`` or ``"ss01"``, but can be also - used to turn off default font features, for - example ``"-liga"`` to disable ligatures or ``"-kern"`` - to disable kerning. To get all supported - features, see `OpenType docs`_. - Requires libraqm. - - .. versionadded:: 4.2.0 - - :param language: Language of the text. Different languages may use - different glyph shapes or ligatures. This parameter tells - the font which language the text is in, and to apply the - correct substitutions as appropriate, if available. - It should be a `BCP 47 language code`_. - Requires libraqm. - - .. versionadded:: 6.0.0 - - :param stroke_width: The width of the text stroke. - - .. versionadded:: 6.2.0 - - :param stroke_fill: Color to use for the text stroke. If not given, will default to - the ``fill`` parameter. - - .. versionadded:: 6.2.0 - - :param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX). - - .. versionadded:: 8.0.0 - - :param font_size: If ``font`` is not provided, then the size to use for the default - font. - Keyword-only argument. - - .. versionadded:: 10.1.0 - - -.. py:method:: ImageDraw.multiline_text(xy, text, fill=None, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, stroke_fill=None, embedded_color=False, font_size=None) - - Draws the string at the given position. - - :param xy: The anchor coordinates of the text. - :param text: String to be drawn. - :param fill: Color to use for the text. - :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. - - :param anchor: The text anchor alignment. Determines the relative location of - the anchor to the text. The default alignment is top left, - specifically ``la`` for horizontal text and ``lt`` for - vertical text. See :ref:`text-anchors` for details. - This parameter is ignored for non-TrueType fonts. - - .. note:: This parameter was present in earlier versions - of Pillow, but implemented only in version 8.0.0. - - :param spacing: The number of pixels between lines. - :param align: ``"left"``, ``"center"`` or ``"right"``. Determines the relative alignment of lines. - Use the ``anchor`` parameter to specify the alignment to ``xy``. - :param direction: Direction of the text. It can be ``"rtl"`` (right to - left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). - Requires libraqm. - - .. versionadded:: 4.2.0 - - :param features: A list of OpenType font features to be used during text - layout. This is usually used to turn on optional - font features that are not enabled by default, - for example ``"dlig"`` or ``"ss01"``, but can be also - used to turn off default font features, for - example ``"-liga"`` to disable ligatures or ``"-kern"`` - to disable kerning. To get all supported - features, see `OpenType docs`_. - Requires libraqm. - - .. versionadded:: 4.2.0 - - :param language: Language of the text. Different languages may use - different glyph shapes or ligatures. This parameter tells - the font which language the text is in, and to apply the - correct substitutions as appropriate, if available. - It should be a `BCP 47 language code`_. - Requires libraqm. - - .. versionadded:: 6.0.0 - - :param stroke_width: The width of the text stroke. - - .. versionadded:: 6.2.0 - - :param stroke_fill: Color to use for the text stroke. If not given, will default to - the ``fill`` parameter. - - .. versionadded:: 6.2.0 - - :param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX). - - .. versionadded:: 8.0.0 - - :param font_size: If ``font`` is not provided, then the size to use for the default - font. - Keyword-only argument. - - .. versionadded:: 10.1.0 - -.. py:method:: ImageDraw.textlength(text, font=None, direction=None, features=None, language=None, embedded_color=False, font_size=None) - - Returns length (in pixels with 1/64 precision) of given text when rendered - in font with provided direction, features, and language. - - This is the amount by which following text should be offset. - Text bounding box may extend past the length in some fonts, - e.g. when using italics or accents. - - The result is returned as a float; it is a whole number if using basic layout. - - Note that the sum of two lengths may not equal the length of a concatenated - string due to kerning. If you need to adjust for kerning, include the following - character and subtract its length. - - For example, instead of :: - - hello = draw.textlength("Hello", font) - world = draw.textlength("World", font) - hello_world = hello + world # not adjusted for kerning - assert hello_world == draw.textlength("HelloWorld", font) # may fail - - use :: - - hello = draw.textlength("HelloW", font) - draw.textlength( - "W", font - ) # adjusted for kerning - world = draw.textlength("World", font) - hello_world = hello + world # adjusted for kerning - assert hello_world == draw.textlength("HelloWorld", font) # True - - or disable kerning with (requires libraqm) :: - - hello = draw.textlength("Hello", font, features=["-kern"]) - world = draw.textlength("World", font, features=["-kern"]) - hello_world = hello + world # kerning is disabled, no need to adjust - assert hello_world == draw.textlength("HelloWorld", font, features=["-kern"]) # True - - .. versionadded:: 8.0.0 - - :param text: Text to be measured. May not contain any newline characters. - :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. - :param direction: Direction of the text. It can be ``"rtl"`` (right to - left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). - Requires libraqm. - :param features: A list of OpenType font features to be used during text - layout. This is usually used to turn on optional - font features that are not enabled by default, - for example ``"dlig"`` or ``"ss01"``, but can be also - used to turn off default font features, for - example ``"-liga"`` to disable ligatures or ``"-kern"`` - to disable kerning. To get all supported - features, see `OpenType docs`_. - Requires libraqm. - :param language: Language of the text. Different languages may use - different glyph shapes or ligatures. This parameter tells - the font which language the text is in, and to apply the - correct substitutions as appropriate, if available. - It should be a `BCP 47 language code`_. - Requires libraqm. - :param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX). - :param font_size: If ``font`` is not provided, then the size to use for the default - font. - Keyword-only argument. - - .. versionadded:: 10.1.0 - - :return: Either width for horizontal text, or height for vertical text. - -.. py:method:: ImageDraw.textbbox(xy, text, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, embedded_color=False, font_size=None) - - Returns bounding box (in pixels) of given text relative to given anchor - when rendered in font with provided direction, features, and language. - Only supported for TrueType fonts. - - Use :py:meth:`textlength` to get the offset of following text with - 1/64 pixel precision. The bounding box includes extra margins for - some fonts, e.g. italics or accents. - - .. versionadded:: 8.0.0 - - :param xy: The anchor coordinates of the text. - :param text: Text to be measured. If it contains any newline characters, - the text is passed on to - :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_textbbox`. - :param font: A :py:class:`~PIL.ImageFont.FreeTypeFont` instance. - :param anchor: The text anchor alignment. Determines the relative location of - the anchor to the text. The default alignment is top left, - specifically ``la`` for horizontal text and ``lt`` for - vertical text. See :ref:`text-anchors` for details. - This parameter is ignored for non-TrueType fonts. - :param spacing: If the text is passed on to - :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_textbbox`, - the number of pixels between lines. - :param align: If the text is passed on to - :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_textbbox`, - ``"left"``, ``"center"`` or ``"right"``. Determines the relative alignment of lines. - Use the ``anchor`` parameter to specify the alignment to ``xy``. - :param direction: Direction of the text. It can be ``"rtl"`` (right to - left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). - Requires libraqm. - :param features: A list of OpenType font features to be used during text - layout. This is usually used to turn on optional - font features that are not enabled by default, - for example ``"dlig"`` or ``"ss01"``, but can be also - used to turn off default font features, for - example ``"-liga"`` to disable ligatures or ``"-kern"`` - to disable kerning. To get all supported - features, see `OpenType docs`_. - Requires libraqm. - :param language: Language of the text. Different languages may use - different glyph shapes or ligatures. This parameter tells - the font which language the text is in, and to apply the - correct substitutions as appropriate, if available. - It should be a `BCP 47 language code`_. - Requires libraqm. - :param stroke_width: The width of the text stroke. - :param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX). - :param font_size: If ``font`` is not provided, then the size to use for the default - font. - Keyword-only argument. - - .. versionadded:: 10.1.0 - - :return: ``(left, top, right, bottom)`` bounding box - -.. py:method:: ImageDraw.multiline_textbbox(xy, text, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, embedded_color=False, font_size=None) - - Returns bounding box (in pixels) of given text relative to given anchor - when rendered in font with provided direction, features, and language. - Only supported for TrueType fonts. - - Use :py:meth:`textlength` to get the offset of following text with - 1/64 pixel precision. The bounding box includes extra margins for - some fonts, e.g. italics or accents. - - .. versionadded:: 8.0.0 - - :param xy: The anchor coordinates of the text. - :param text: Text to be measured. - :param font: A :py:class:`~PIL.ImageFont.FreeTypeFont` instance. - :param anchor: The text anchor alignment. Determines the relative location of - the anchor to the text. The default alignment is top left, - specifically ``la`` for horizontal text and ``lt`` for - 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 direction: Direction of the text. It can be ``"rtl"`` (right to - left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). - Requires libraqm. - :param features: A list of OpenType font features to be used during text - layout. This is usually used to turn on optional - font features that are not enabled by default, - for example ``"dlig"`` or ``"ss01"``, but can be also - used to turn off default font features, for - example ``"-liga"`` to disable ligatures or ``"-kern"`` - to disable kerning. To get all supported - features, see `OpenType docs`_. - Requires libraqm. - :param language: Language of the text. Different languages may use - different glyph shapes or ligatures. This parameter tells - the font which language the text is in, and to apply the - correct substitutions as appropriate, if available. - It should be a `BCP 47 language code`_. - Requires libraqm. - :param stroke_width: The width of the text stroke. - :param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX). - :param font_size: If ``font`` is not provided, then the size to use for the default - font. - Keyword-only argument. - - .. versionadded:: 10.1.0 - - :return: ``(left, top, right, bottom)`` bounding box - -.. py:method:: getdraw(im=None, hints=None) - - .. warning:: This method is experimental. - - A more advanced 2D drawing interface for PIL images, - based on the WCK interface. - - :param im: The image to draw in. - :param hints: An optional list of hints. - :returns: A (drawing context, drawing resource factory) tuple. - -.. py:method:: floodfill(image, xy, value, border=None, thresh=0) - - .. warning:: This method is experimental. - - Fills a bounded region with a given color. - - :param image: Target image. - :param xy: Seed position (a 2-item coordinate tuple). - :param value: Fill color. - :param border: Optional border value. If given, the region consists of - pixels with a color different from the border color. If not given, - the region consists of pixels having the same color as the seed - pixel. - :param thresh: Optional threshold value which specifies a maximum - tolerable difference of a pixel value from the 'background' in - order for it to be replaced. Useful for filling regions of non- - homogeneous, but similar, colors. - -.. _BCP 47 language code: https://www.w3.org/International/articles/language-tags/ -.. _OpenType docs: https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist +.. py:module:: PIL.ImageDraw +.. py:currentmodule:: PIL.ImageDraw + +:py:mod:`~PIL.ImageDraw` Module +=============================== + +The :py:mod:`~PIL.ImageDraw` module provides simple 2D graphics for +:py:class:`~PIL.Image.Image` objects. You can use this module to create new +images, annotate or retouch existing images, and to generate graphics on the +fly for web use. + +For a more advanced drawing library for PIL, see the `aggdraw module`_. + +.. _aggdraw module: https://github.com/pytroll/aggdraw + +Example: Draw a gray cross over an image +---------------------------------------- + +:: + + import sys + from PIL import Image, ImageDraw + + with Image.open("hopper.jpg") as im: + + draw = ImageDraw.Draw(im) + draw.line((0, 0) + im.size, fill=128) + draw.line((0, im.size[1], im.size[0], 0), fill=128) + + # write to stdout + im.save(sys.stdout, "PNG") + + +Concepts +-------- + +Coordinates +^^^^^^^^^^^ + +The graphics interface uses the same coordinate system as PIL itself, with (0, +0) in the upper left corner. Any pixels drawn outside of the image bounds will +be discarded. + +Colors +^^^^^^ + +To specify colors, you can use numbers or tuples just as you would use with +:py:meth:`PIL.Image.new` or :py:meth:`PIL.Image.Image.putpixel`. For “1â€, +“Lâ€, and “I†images, use integers. For “RGB†images, use a 3-tuple containing +integer values. For “F†images, use integer or floating point values. + +For palette images (mode “Pâ€), use integers as color indexes. In 1.1.4 and +later, you can also use RGB 3-tuples or color names (see below). The drawing +layer will automatically assign color indexes, as long as you don’t draw with +more than 256 colors. + +Color Names +^^^^^^^^^^^ + +See :ref:`color-names` for the color names supported by Pillow. + +Fonts +^^^^^ + +PIL can use bitmap fonts or OpenType/TrueType fonts. + +Bitmap fonts are stored in PIL's own format, where each font typically consists +of two files, one named .pil and the other usually named .pbm. The former +contains font metrics, the latter raster data. + +To load a bitmap font, use the load functions in the :py:mod:`~PIL.ImageFont` +module. + +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 +---------------------------------- + +:: + + from PIL import Image, ImageDraw, ImageFont + + # get an image + with Image.open("Pillow/Tests/images/hopper.png").convert("RGBA") as base: + + # make a blank image for the text, initialized to transparent text color + txt = Image.new("RGBA", base.size, (255, 255, 255, 0)) + + # get a font + fnt = ImageFont.truetype("Pillow/Tests/fonts/FreeMono.ttf", 40) + # get a drawing context + d = ImageDraw.Draw(txt) + + # draw text, half opacity + d.text((10, 10), "Hello", font=fnt, fill=(255, 255, 255, 128)) + # draw text, full opacity + d.text((10, 60), "World", font=fnt, fill=(255, 255, 255, 255)) + + out = Image.alpha_composite(base, txt) + + out.show() + +Example: Draw Multiline Text +---------------------------- + +:: + + from PIL import Image, ImageDraw, ImageFont + + # create an image + out = Image.new("RGB", (150, 100), (255, 255, 255)) + + # get a font + fnt = ImageFont.truetype("Pillow/Tests/fonts/FreeMono.ttf", 40) + # get a drawing context + d = ImageDraw.Draw(out) + + # draw multiline text + d.multiline_text((10, 10), "Hello\nWorld", font=fnt, fill=(0, 0, 0)) + + out.show() + + +Functions +--------- + +.. py:method:: Draw(im, mode=None) + + Creates an object that can be used to draw in the given image. + + Note that the image will be modified in place. + + :param im: The image to draw in. + :param mode: Optional mode to use for color values. For RGB + images, this argument can be RGB or RGBA (to blend the + drawing into the image). For all other modes, this argument + must be the same as the image mode. If omitted, the mode + defaults to the mode of the image. + +Attributes +---------- + +.. py:attribute:: ImageDraw.fill + :type: bool + :value: False + + Selects whether :py:attr:`ImageDraw.ink` should be used as a fill or outline color. + +.. py:attribute:: ImageDraw.font + + The current default font. + + Can be set per instance:: + + from PIL import ImageDraw, ImageFont + draw = ImageDraw.Draw(image) + draw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + + Or globally for all future ImageDraw instances:: + + from PIL import ImageDraw, ImageFont + ImageDraw.ImageDraw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + +.. py:attribute:: ImageDraw.fontmode + + The current font drawing mode. + + Set to ``"1"`` to disable antialiasing or ``"L"`` to enable it. + +.. py:attribute:: ImageDraw.ink + :type: int + + The internal representation of the current default color. + +Methods +------- + +.. py:method:: ImageDraw.getfont() + + Get the current default font, :py:attr:`ImageDraw.font`. + + If the current default font is ``None``, + it is initialized with :py:func:`.ImageFont.load_default`. + + :returns: An image font. + +.. py:method:: ImageDraw.arc(xy, start, end, fill=None, width=0) + + Draws an arc (a portion of a circle outline) between the start and end + angles, inside the given bounding box. + + :param xy: Two points to define the bounding box. Sequence of ``[(x0, y0), + (x1, y1)]`` or ``[x0, y0, x1, y1]``, where ``x1 >= x0`` and ``y1 >= + y0``. + :param start: Starting angle, in degrees. Angles are measured from 3 + o'clock, increasing clockwise. + :param end: Ending angle, in degrees. + :param fill: Color to use for the arc. + :param width: The line width, in pixels. + + .. versionadded:: 5.3.0 + +.. py:method:: ImageDraw.bitmap(xy, bitmap, fill=None) + + Draws a bitmap (mask) at the given position, using the current fill color + for the non-zero portions. The bitmap should be a valid transparency mask + (mode “1â€) or matte (mode “L†or “RGBAâ€). + + This is equivalent to doing ``image.paste(xy, color, bitmap)``. + + To paste pixel data into an image, use the + :py:meth:`~PIL.Image.Image.paste` method on the image itself. + +.. py:method:: ImageDraw.chord(xy, start, end, fill=None, outline=None, width=1) + + Same as :py:meth:`~PIL.ImageDraw.ImageDraw.arc`, but connects the end points + with a straight line. + + :param xy: Two points to define the bounding box. Sequence of ``[(x0, y0), + (x1, y1)]`` or ``[x0, y0, x1, y1]``, where ``x1 >= x0`` and ``y1 >= + y0``. + :param outline: Color to use for the outline. + :param fill: Color to use for the fill. + :param width: The line width, in pixels. + + .. versionadded:: 5.3.0 + +.. py:method:: ImageDraw.circle(xy, radius, fill=None, outline=None, width=1) + + Draws a circle with a given radius centering on a point. + + .. versionadded:: 10.4.0 + + :param xy: The point for the center of the circle, e.g. ``(x, y)``. + :param radius: Radius of the circle. + :param outline: Color to use for the outline. + :param fill: Color to use for the fill. + :param width: The line width, in pixels. + +.. py:method:: ImageDraw.ellipse(xy, fill=None, outline=None, width=1) + + Draws an ellipse inside the given bounding box. + + :param xy: Two points to define the bounding box. Sequence of either + ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``, where ``x1 >= x0`` + and ``y1 >= y0``. + :param outline: Color to use for the outline. + :param fill: Color to use for the fill. + :param width: The line width, in pixels. + + .. versionadded:: 5.3.0 + +.. py:method:: ImageDraw.line(xy, fill=None, width=0, joint=None) + + Draws a line between the coordinates in the ``xy`` list. + The coordinate pixels are included in the drawn line. + + :param xy: Sequence of either 2-tuples like ``[(x, y), (x, y), ...]`` or + numeric values like ``[x, y, x, y, ...]``. + :param fill: Color to use for the line. + :param width: The line width, in pixels. + + .. versionadded:: 1.1.5 + + .. note:: This option was broken until version 1.1.6. + :param joint: Joint type between a sequence of lines. It can be ``"curve"``, for rounded edges, or :data:`None`. + + .. versionadded:: 5.3.0 + +.. py:method:: ImageDraw.pieslice(xy, start, end, fill=None, outline=None, width=1) + + Same as arc, but also draws straight lines between the end points and the + center of the bounding box. + + :param xy: Two points to define the bounding box. Sequence of ``[(x0, y0), + (x1, y1)]`` or ``[x0, y0, x1, y1]``, where ``x1 >= x0`` and ``y1 >= + y0``. + :param start: Starting angle, in degrees. Angles are measured from 3 + o'clock, increasing clockwise. + :param end: Ending angle, in degrees. + :param fill: Color to use for the fill. + :param outline: Color to use for the outline. + :param width: The line width, in pixels. + + .. versionadded:: 5.3.0 + +.. py:method:: ImageDraw.point(xy, fill=None) + + Draws points (individual pixels) at the given coordinates. + + :param xy: Sequence of either 2-tuples like ``[(x, y), (x, y), ...]`` or + numeric values like ``[x, y, x, y, ...]``. + :param fill: Color to use for the point. + +.. py:method:: ImageDraw.polygon(xy, fill=None, outline=None, width=1) + + Draws a polygon. + + The polygon outline consists of straight lines between the given + coordinates, plus a straight line between the last and the first + coordinate. The coordinate pixels are included in the drawn polygon. + + :param xy: Sequence of either 2-tuples like ``[(x, y), (x, y), ...]`` or + numeric values like ``[x, y, x, y, ...]``. + :param fill: Color to use for the fill. + :param outline: Color to use for the outline. + :param width: The line width, in pixels. + + +.. py:method:: ImageDraw.regular_polygon(bounding_circle, n_sides, rotation=0, fill=None, outline=None, width=1) + + Draws a regular polygon inscribed in ``bounding_circle``, + with ``n_sides``, and rotation of ``rotation`` degrees. + + :param bounding_circle: The bounding circle is a tuple defined + by a point and radius. + (e.g. ``bounding_circle=(x, y, r)`` or ``((x, y), r)``). + The polygon is inscribed in this circle. + :param n_sides: Number of sides + (e.g. ``n_sides=3`` for a triangle, ``6`` for a hexagon). + :param rotation: Apply an arbitrary rotation to the polygon + (e.g. ``rotation=90``, applies a 90 degree rotation). + :param fill: Color to use for the fill. + :param outline: Color to use for the outline. + :param width: The line width, in pixels. + + +.. py:method:: ImageDraw.rectangle(xy, fill=None, outline=None, width=1) + + Draws a rectangle. + + :param xy: Two points to define the bounding box. Sequence of either + ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``, where ``x1 >= x0`` and + ``y1 >= y0``. The bounding box is inclusive of both endpoints. + :param fill: Color to use for the fill. + :param outline: Color to use for the outline. + :param width: The line width, in pixels. + + .. versionadded:: 5.3.0 + +.. py:method:: ImageDraw.rounded_rectangle(xy, radius=0, fill=None, outline=None, width=1, corners=None) + + Draws a rounded rectangle. + + :param xy: Two points to define the bounding box. Sequence of either + ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``, where ``x1 >= x0`` and + ``y1 >= y0``. The bounding box is inclusive of both endpoints. + :param radius: Radius of the corners. + :param fill: Color to use for the fill. + :param outline: Color to use for the outline. + :param width: The line width, in pixels. + :param corners: A tuple of whether to round each corner, + ``(top_left, top_right, bottom_right, bottom_left)``. + Keyword-only argument. + + .. versionadded:: 8.2.0 + +.. py:method:: ImageDraw.shape(shape, fill=None, outline=None) + + .. warning:: This method is experimental. + + Draw a shape. + +.. py:method:: ImageDraw.text(xy, text, fill=None, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, stroke_fill=None, embedded_color=False, font_size=None) + + Draws the string at the given position. + + :param xy: The anchor coordinates of the text. + :param text: String to be drawn. If it contains any newline characters, + the text is passed on to + :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_text`. + :param fill: Color to use for the text. + :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. + :param anchor: The text anchor alignment. Determines the relative location of + the anchor to the text. The default alignment is top left, + specifically ``la`` for horizontal text and ``lt`` for + vertical text. See :ref:`text-anchors` for details. + This parameter is ignored for non-TrueType fonts. + + .. note:: This parameter was present in earlier versions + of Pillow, but implemented only in version 8.0.0. + + :param spacing: If the text is passed on to + :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_text`, + the number of pixels between lines. + :param align: If the text is passed on to + :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_text`, + ``"left"``, ``"center"`` or ``"right"``. Determines the relative alignment of lines. + Use the ``anchor`` parameter to specify the alignment to ``xy``. + :param direction: Direction of the text. It can be ``"rtl"`` (right to + left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). + Requires libraqm. + + .. versionadded:: 4.2.0 + + :param features: A list of OpenType font features to be used during text + layout. This is usually used to turn on optional + font features that are not enabled by default, + for example ``"dlig"`` or ``"ss01"``, but can be also + used to turn off default font features, for + example ``"-liga"`` to disable ligatures or ``"-kern"`` + to disable kerning. To get all supported + features, see `OpenType docs`_. + Requires libraqm. + + .. versionadded:: 4.2.0 + + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP 47 language code`_. + Requires libraqm. + + .. versionadded:: 6.0.0 + + :param stroke_width: The width of the text stroke. + + .. versionadded:: 6.2.0 + + :param stroke_fill: Color to use for the text stroke. If not given, will default to + the ``fill`` parameter. + + .. versionadded:: 6.2.0 + + :param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX). + + .. versionadded:: 8.0.0 + + :param font_size: If ``font`` is not provided, then the size to use for the default + font. + Keyword-only argument. + + .. versionadded:: 10.1.0 + + +.. py:method:: ImageDraw.multiline_text(xy, text, fill=None, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, stroke_fill=None, embedded_color=False, font_size=None) + + Draws the string at the given position. + + :param xy: The anchor coordinates of the text. + :param text: String to be drawn. + :param fill: Color to use for the text. + :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. + + :param anchor: The text anchor alignment. Determines the relative location of + the anchor to the text. The default alignment is top left, + specifically ``la`` for horizontal text and ``lt`` for + vertical text. See :ref:`text-anchors` for details. + This parameter is ignored for non-TrueType fonts. + + .. note:: This parameter was present in earlier versions + of Pillow, but implemented only in version 8.0.0. + + :param spacing: The number of pixels between lines. + :param align: ``"left"``, ``"center"`` or ``"right"``. Determines the relative alignment of lines. + Use the ``anchor`` parameter to specify the alignment to ``xy``. + :param direction: Direction of the text. It can be ``"rtl"`` (right to + left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). + Requires libraqm. + + .. versionadded:: 4.2.0 + + :param features: A list of OpenType font features to be used during text + layout. This is usually used to turn on optional + font features that are not enabled by default, + for example ``"dlig"`` or ``"ss01"``, but can be also + used to turn off default font features, for + example ``"-liga"`` to disable ligatures or ``"-kern"`` + to disable kerning. To get all supported + features, see `OpenType docs`_. + Requires libraqm. + + .. versionadded:: 4.2.0 + + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP 47 language code`_. + Requires libraqm. + + .. versionadded:: 6.0.0 + + :param stroke_width: The width of the text stroke. + + .. versionadded:: 6.2.0 + + :param stroke_fill: Color to use for the text stroke. If not given, will default to + the ``fill`` parameter. + + .. versionadded:: 6.2.0 + + :param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX). + + .. versionadded:: 8.0.0 + + :param font_size: If ``font`` is not provided, then the size to use for the default + font. + Keyword-only argument. + + .. versionadded:: 10.1.0 + +.. py:method:: ImageDraw.textlength(text, font=None, direction=None, features=None, language=None, embedded_color=False, font_size=None) + + Returns length (in pixels with 1/64 precision) of given text when rendered + in font with provided direction, features, and language. + + This is the amount by which following text should be offset. + Text bounding box may extend past the length in some fonts, + e.g. when using italics or accents. + + The result is returned as a float; it is a whole number if using basic layout. + + Note that the sum of two lengths may not equal the length of a concatenated + string due to kerning. If you need to adjust for kerning, include the following + character and subtract its length. + + For example, instead of :: + + hello = draw.textlength("Hello", font) + world = draw.textlength("World", font) + hello_world = hello + world # not adjusted for kerning + assert hello_world == draw.textlength("HelloWorld", font) # may fail + + use :: + + hello = draw.textlength("HelloW", font) - draw.textlength( + "W", font + ) # adjusted for kerning + world = draw.textlength("World", font) + hello_world = hello + world # adjusted for kerning + assert hello_world == draw.textlength("HelloWorld", font) # True + + or disable kerning with (requires libraqm) :: + + hello = draw.textlength("Hello", font, features=["-kern"]) + world = draw.textlength("World", font, features=["-kern"]) + hello_world = hello + world # kerning is disabled, no need to adjust + assert hello_world == draw.textlength("HelloWorld", font, features=["-kern"]) # True + + .. versionadded:: 8.0.0 + + :param text: Text to be measured. May not contain any newline characters. + :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. + :param direction: Direction of the text. It can be ``"rtl"`` (right to + left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). + Requires libraqm. + :param features: A list of OpenType font features to be used during text + layout. This is usually used to turn on optional + font features that are not enabled by default, + for example ``"dlig"`` or ``"ss01"``, but can be also + used to turn off default font features, for + example ``"-liga"`` to disable ligatures or ``"-kern"`` + to disable kerning. To get all supported + features, see `OpenType docs`_. + Requires libraqm. + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP 47 language code`_. + Requires libraqm. + :param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX). + :param font_size: If ``font`` is not provided, then the size to use for the default + font. + Keyword-only argument. + + .. versionadded:: 10.1.0 + + :return: Either width for horizontal text, or height for vertical text. + +.. py:method:: ImageDraw.textbbox(xy, text, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, embedded_color=False, font_size=None) + + Returns bounding box (in pixels) of given text relative to given anchor + when rendered in font with provided direction, features, and language. + Only supported for TrueType fonts. + + Use :py:meth:`textlength` to get the offset of following text with + 1/64 pixel precision. The bounding box includes extra margins for + some fonts, e.g. italics or accents. + + .. versionadded:: 8.0.0 + + :param xy: The anchor coordinates of the text. + :param text: Text to be measured. If it contains any newline characters, + the text is passed on to + :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_textbbox`. + :param font: A :py:class:`~PIL.ImageFont.FreeTypeFont` instance. + :param anchor: The text anchor alignment. Determines the relative location of + the anchor to the text. The default alignment is top left, + specifically ``la`` for horizontal text and ``lt`` for + vertical text. See :ref:`text-anchors` for details. + This parameter is ignored for non-TrueType fonts. + :param spacing: If the text is passed on to + :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_textbbox`, + the number of pixels between lines. + :param align: If the text is passed on to + :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_textbbox`, + ``"left"``, ``"center"`` or ``"right"``. Determines the relative alignment of lines. + Use the ``anchor`` parameter to specify the alignment to ``xy``. + :param direction: Direction of the text. It can be ``"rtl"`` (right to + left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). + Requires libraqm. + :param features: A list of OpenType font features to be used during text + layout. This is usually used to turn on optional + font features that are not enabled by default, + for example ``"dlig"`` or ``"ss01"``, but can be also + used to turn off default font features, for + example ``"-liga"`` to disable ligatures or ``"-kern"`` + to disable kerning. To get all supported + features, see `OpenType docs`_. + Requires libraqm. + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP 47 language code`_. + Requires libraqm. + :param stroke_width: The width of the text stroke. + :param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX). + :param font_size: If ``font`` is not provided, then the size to use for the default + font. + Keyword-only argument. + + .. versionadded:: 10.1.0 + + :return: ``(left, top, right, bottom)`` bounding box + +.. py:method:: ImageDraw.multiline_textbbox(xy, text, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, embedded_color=False, font_size=None) + + Returns bounding box (in pixels) of given text relative to given anchor + when rendered in font with provided direction, features, and language. + Only supported for TrueType fonts. + + Use :py:meth:`textlength` to get the offset of following text with + 1/64 pixel precision. The bounding box includes extra margins for + some fonts, e.g. italics or accents. + + .. versionadded:: 8.0.0 + + :param xy: The anchor coordinates of the text. + :param text: Text to be measured. + :param font: A :py:class:`~PIL.ImageFont.FreeTypeFont` instance. + :param anchor: The text anchor alignment. Determines the relative location of + the anchor to the text. The default alignment is top left, + specifically ``la`` for horizontal text and ``lt`` for + 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 direction: Direction of the text. It can be ``"rtl"`` (right to + left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). + Requires libraqm. + :param features: A list of OpenType font features to be used during text + layout. This is usually used to turn on optional + font features that are not enabled by default, + for example ``"dlig"`` or ``"ss01"``, but can be also + used to turn off default font features, for + example ``"-liga"`` to disable ligatures or ``"-kern"`` + to disable kerning. To get all supported + features, see `OpenType docs`_. + Requires libraqm. + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP 47 language code`_. + Requires libraqm. + :param stroke_width: The width of the text stroke. + :param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX). + :param font_size: If ``font`` is not provided, then the size to use for the default + font. + Keyword-only argument. + + .. versionadded:: 10.1.0 + + :return: ``(left, top, right, bottom)`` bounding box + +.. py:method:: getdraw(im=None, hints=None) + + .. warning:: This method is experimental. + + A more advanced 2D drawing interface for PIL images, + based on the WCK interface. + + :param im: The image to draw in. + :param hints: An optional list of hints. + :returns: A (drawing context, drawing resource factory) tuple. + +.. py:method:: floodfill(image, xy, value, border=None, thresh=0) + + .. warning:: This method is experimental. + + Fills a bounded region with a given color. + + :param image: Target image. + :param xy: Seed position (a 2-item coordinate tuple). + :param value: Fill color. + :param border: Optional border value. If given, the region consists of + pixels with a color different from the border color. If not given, + the region consists of pixels having the same color as the seed + pixel. + :param thresh: Optional threshold value which specifies a maximum + tolerable difference of a pixel value from the 'background' in + order for it to be replaced. Useful for filling regions of non- + homogeneous, but similar, colors. + +.. _BCP 47 language code: https://www.w3.org/International/articles/language-tags/ +.. _OpenType docs: https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist diff --git a/docs/reference/ImageEnhance.rst b/docs/reference/ImageEnhance.rst index 529acad4a..47f89f27c 100644 --- a/docs/reference/ImageEnhance.rst +++ b/docs/reference/ImageEnhance.rst @@ -1,81 +1,81 @@ -.. py:module:: PIL.ImageEnhance -.. py:currentmodule:: PIL.ImageEnhance - -:py:mod:`~PIL.ImageEnhance` Module -================================== - -The :py:mod:`~PIL.ImageEnhance` module contains a number of classes that can be used -for image enhancement. - -Example: Vary the sharpness of an image ---------------------------------------- - -:: - - from PIL import ImageEnhance - - enhancer = ImageEnhance.Sharpness(image) - - for i in range(8): - factor = i / 4.0 - enhancer.enhance(factor).show(f"Sharpness {factor:f}") - -Also see the :file:`enhancer.py` demo program in the :file:`Scripts/` -directory. - -Classes -------- - -All enhancement classes implement a common interface, containing a single -method: - -.. _enhancement-factor: - -.. py:class:: _Enhance - - .. py:method:: enhance(factor) - - Returns an enhanced image. - - :param factor: A floating point value controlling the enhancement. - Factor 1.0 always returns a copy of the original image, - lower factors mean less color (brightness, contrast, - etc), and higher values more. There are no restrictions - on this value. - -.. py:class:: Color(image) - - Adjust image color balance. - - This class can be used to adjust the colour balance of an image, in a - manner similar to the controls on a colour TV set. An - :ref:`enhancement factor ` of 0.0 gives a black and - white image. A factor of 1.0 gives the original image. - -.. py:class:: Contrast(image) - - Adjust image contrast. - - This class can be used to control the contrast of an image, similar to the - contrast control on a TV set. An - :ref:`enhancement factor ` of 0.0 gives a solid gray - image, a factor of 1.0 gives the original image, and greater values - increase the contrast of the image. - -.. py:class:: Brightness(image) - - Adjust image brightness. - - This class can be used to control the brightness of an image. An - :ref:`enhancement factor ` of 0.0 gives a black image, - a factor of 1.0 gives the original image, and greater values increase the - brightness of the image. - -.. py:class:: Sharpness(image) - - Adjust image sharpness. - - This class can be used to adjust the sharpness of an image. An - :ref:`enhancement factor ` of 0.0 gives a blurred - image, a factor of 1.0 gives the original image, and a factor of 2.0 gives - a sharpened image. +.. py:module:: PIL.ImageEnhance +.. py:currentmodule:: PIL.ImageEnhance + +:py:mod:`~PIL.ImageEnhance` Module +================================== + +The :py:mod:`~PIL.ImageEnhance` module contains a number of classes that can be used +for image enhancement. + +Example: Vary the sharpness of an image +--------------------------------------- + +:: + + from PIL import ImageEnhance + + enhancer = ImageEnhance.Sharpness(image) + + for i in range(8): + factor = i / 4.0 + enhancer.enhance(factor).show(f"Sharpness {factor:f}") + +Also see the :file:`enhancer.py` demo program in the :file:`Scripts/` +directory. + +Classes +------- + +All enhancement classes implement a common interface, containing a single +method: + +.. _enhancement-factor: + +.. py:class:: _Enhance + + .. py:method:: enhance(factor) + + Returns an enhanced image. + + :param factor: A floating point value controlling the enhancement. + Factor 1.0 always returns a copy of the original image, + lower factors mean less color (brightness, contrast, + etc), and higher values more. There are no restrictions + on this value. + +.. py:class:: Color(image) + + Adjust image color balance. + + This class can be used to adjust the colour balance of an image, in a + manner similar to the controls on a colour TV set. An + :ref:`enhancement factor ` of 0.0 gives a black and + white image. A factor of 1.0 gives the original image. + +.. py:class:: Contrast(image) + + Adjust image contrast. + + This class can be used to control the contrast of an image, similar to the + contrast control on a TV set. An + :ref:`enhancement factor ` of 0.0 gives a solid gray + image, a factor of 1.0 gives the original image, and greater values + increase the contrast of the image. + +.. py:class:: Brightness(image) + + Adjust image brightness. + + This class can be used to control the brightness of an image. An + :ref:`enhancement factor ` of 0.0 gives a black image, + a factor of 1.0 gives the original image, and greater values increase the + brightness of the image. + +.. py:class:: Sharpness(image) + + Adjust image sharpness. + + This class can be used to adjust the sharpness of an image. An + :ref:`enhancement factor ` of 0.0 gives a blurred + image, a factor of 1.0 gives the original image, and a factor of 2.0 gives + a sharpened image. diff --git a/docs/reference/ImageFile.rst b/docs/reference/ImageFile.rst index e59c7311a..60d66de93 100644 --- a/docs/reference/ImageFile.rst +++ b/docs/reference/ImageFile.rst @@ -1,73 +1,73 @@ -.. py:module:: PIL.ImageFile -.. py:currentmodule:: PIL.ImageFile - -:py:mod:`~PIL.ImageFile` Module -=============================== - -The :py:mod:`~PIL.ImageFile` module provides support functions for the image open -and save functions. - -In addition, it provides a :py:class:`Parser` class which can be used to decode -an image piece by piece (e.g. while receiving it over a network connection). -This class implements the same consumer interface as the standard **sgmllib** -and **xmllib** modules. - -Example: Parse an image ------------------------ - -:: - - from PIL import ImageFile - - fp = open("hopper.pgm", "rb") - - p = ImageFile.Parser() - - while 1: - s = fp.read(1024) - if not s: - break - p.feed(s) - - im = p.close() - - im.save("copy.jpg") - - -Classes -------- - -.. autoclass:: PIL.ImageFile.Parser() - :members: - -.. autoclass:: PIL.ImageFile.PyCodec() - :members: - -.. autoclass:: PIL.ImageFile.PyDecoder() - :members: - :show-inheritance: - -.. autoclass:: PIL.ImageFile.PyEncoder() - :members: - :show-inheritance: - -.. autoclass:: PIL.ImageFile.ImageFile() - :member-order: bysource - :members: - :undoc-members: - :show-inheritance: - -.. autoclass:: PIL.ImageFile.StubHandler() - :members: - :show-inheritance: - -.. autoclass:: PIL.ImageFile.StubImageFile() - :members: - :show-inheritance: - -Constants ---------- - -.. autodata:: PIL.ImageFile.LOAD_TRUNCATED_IMAGES -.. autodata:: PIL.ImageFile.ERRORS - :annotation: +.. py:module:: PIL.ImageFile +.. py:currentmodule:: PIL.ImageFile + +:py:mod:`~PIL.ImageFile` Module +=============================== + +The :py:mod:`~PIL.ImageFile` module provides support functions for the image open +and save functions. + +In addition, it provides a :py:class:`Parser` class which can be used to decode +an image piece by piece (e.g. while receiving it over a network connection). +This class implements the same consumer interface as the standard **sgmllib** +and **xmllib** modules. + +Example: Parse an image +----------------------- + +:: + + from PIL import ImageFile + + fp = open("hopper.pgm", "rb") + + p = ImageFile.Parser() + + while 1: + s = fp.read(1024) + if not s: + break + p.feed(s) + + im = p.close() + + im.save("copy.jpg") + + +Classes +------- + +.. autoclass:: PIL.ImageFile.Parser() + :members: + +.. autoclass:: PIL.ImageFile.PyCodec() + :members: + +.. autoclass:: PIL.ImageFile.PyDecoder() + :members: + :show-inheritance: + +.. autoclass:: PIL.ImageFile.PyEncoder() + :members: + :show-inheritance: + +.. autoclass:: PIL.ImageFile.ImageFile() + :member-order: bysource + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: PIL.ImageFile.StubHandler() + :members: + :show-inheritance: + +.. autoclass:: PIL.ImageFile.StubImageFile() + :members: + :show-inheritance: + +Constants +--------- + +.. autodata:: PIL.ImageFile.LOAD_TRUNCATED_IMAGES +.. autodata:: PIL.ImageFile.ERRORS + :annotation: diff --git a/docs/reference/ImageFilter.rst b/docs/reference/ImageFilter.rst index 5f2b6af7c..e2b330853 100644 --- a/docs/reference/ImageFilter.rst +++ b/docs/reference/ImageFilter.rst @@ -1,93 +1,93 @@ -.. py:module:: PIL.ImageFilter -.. py:currentmodule:: PIL.ImageFilter - -:py:mod:`~PIL.ImageFilter` Module -================================= - -The :py:mod:`~PIL.ImageFilter` module contains definitions for a pre-defined set of -filters, which can be be used with the :py:meth:`Image.filter() -` method. - -Example: Filter an image ------------------------- - -:: - - from PIL import ImageFilter - - im1 = im.filter(ImageFilter.BLUR) - - im2 = im.filter(ImageFilter.MinFilter(3)) - im3 = im.filter(ImageFilter.MinFilter) # same as MinFilter(3) - -Filters -------- - -Pillow provides the following set of predefined image enhancement filters: - -* **BLUR** -* **CONTOUR** -* **DETAIL** -* **EDGE_ENHANCE** -* **EDGE_ENHANCE_MORE** -* **EMBOSS** -* **FIND_EDGES** -* **SHARPEN** -* **SMOOTH** -* **SMOOTH_MORE** - -.. autoclass:: PIL.ImageFilter.Color3DLUT - :members: - -.. autoclass:: PIL.ImageFilter.BoxBlur - :members: - -.. autoclass:: PIL.ImageFilter.GaussianBlur - :members: - -.. autoclass:: PIL.ImageFilter.UnsharpMask - :members: - -.. autoclass:: PIL.ImageFilter.Kernel - :members: - -.. autoclass:: PIL.ImageFilter.RankFilter - :members: - -.. autoclass:: PIL.ImageFilter.MedianFilter - :members: - -.. autoclass:: PIL.ImageFilter.MinFilter - :members: - -.. autoclass:: PIL.ImageFilter.MaxFilter - :members: - -.. autoclass:: PIL.ImageFilter.ModeFilter - :members: - -.. class:: Filter - - An abstract mixin used for filtering images - (for use with :py:meth:`~PIL.Image.Image.filter`). - - Implementors must provide the following method: - - .. method:: filter(self, image) - - Applies a filter to a single-band image, or a single band of an image. - - :returns: A filtered copy of the image. - -.. class:: MultibandFilter - - An abstract mixin used for filtering multi-band images - (for use with :py:meth:`~PIL.Image.Image.filter`). - - Implementors must provide the following method: - - .. method:: filter(self, image) - - Applies a filter to a multi-band image. - - :returns: A filtered copy of the image. +.. py:module:: PIL.ImageFilter +.. py:currentmodule:: PIL.ImageFilter + +:py:mod:`~PIL.ImageFilter` Module +================================= + +The :py:mod:`~PIL.ImageFilter` module contains definitions for a pre-defined set of +filters, which can be be used with the :py:meth:`Image.filter() +` method. + +Example: Filter an image +------------------------ + +:: + + from PIL import ImageFilter + + im1 = im.filter(ImageFilter.BLUR) + + im2 = im.filter(ImageFilter.MinFilter(3)) + im3 = im.filter(ImageFilter.MinFilter) # same as MinFilter(3) + +Filters +------- + +Pillow provides the following set of predefined image enhancement filters: + +* **BLUR** +* **CONTOUR** +* **DETAIL** +* **EDGE_ENHANCE** +* **EDGE_ENHANCE_MORE** +* **EMBOSS** +* **FIND_EDGES** +* **SHARPEN** +* **SMOOTH** +* **SMOOTH_MORE** + +.. autoclass:: PIL.ImageFilter.Color3DLUT + :members: + +.. autoclass:: PIL.ImageFilter.BoxBlur + :members: + +.. autoclass:: PIL.ImageFilter.GaussianBlur + :members: + +.. autoclass:: PIL.ImageFilter.UnsharpMask + :members: + +.. autoclass:: PIL.ImageFilter.Kernel + :members: + +.. autoclass:: PIL.ImageFilter.RankFilter + :members: + +.. autoclass:: PIL.ImageFilter.MedianFilter + :members: + +.. autoclass:: PIL.ImageFilter.MinFilter + :members: + +.. autoclass:: PIL.ImageFilter.MaxFilter + :members: + +.. autoclass:: PIL.ImageFilter.ModeFilter + :members: + +.. class:: Filter + + An abstract mixin used for filtering images + (for use with :py:meth:`~PIL.Image.Image.filter`). + + Implementors must provide the following method: + + .. method:: filter(self, image) + + Applies a filter to a single-band image, or a single band of an image. + + :returns: A filtered copy of the image. + +.. class:: MultibandFilter + + An abstract mixin used for filtering multi-band images + (for use with :py:meth:`~PIL.Image.Image.filter`). + + Implementors must provide the following method: + + .. method:: filter(self, image) + + Applies a filter to a multi-band image. + + :returns: A filtered copy of the image. diff --git a/docs/reference/ImageFont.rst b/docs/reference/ImageFont.rst index 6edf4b05c..3e1f0483a 100644 --- a/docs/reference/ImageFont.rst +++ b/docs/reference/ImageFont.rst @@ -1,92 +1,92 @@ -.. py:module:: PIL.ImageFont -.. py:currentmodule:: PIL.ImageFont - -:py:mod:`~PIL.ImageFont` Module -=============================== - -The :py:mod:`~PIL.ImageFont` module defines a class with the same name. Instances of -this class store bitmap fonts, and are used with the -:py:meth:`PIL.ImageDraw.ImageDraw.text` method. - -PIL uses its own font file format to store bitmap fonts, limited to 256 characters. You can use -`pilfont.py `_ -from :pypi:`pillow-scripts` to convert BDF and -PCF font descriptors (X window font formats) to this format. - -Starting with version 1.1.4, PIL can be configured to support TrueType and -OpenType fonts (as well as other font formats supported by the FreeType -library). For earlier versions, TrueType support is only available as part of -the imToolkit package. - -.. 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 - is over a certain limit, :py:data:`MAX_STRING_LENGTH`. - - This threshold can be changed by setting - :py:data:`MAX_STRING_LENGTH`. It can be disabled by setting - ``ImageFont.MAX_STRING_LENGTH = None``. - -Example -------- - -:: - - from PIL import ImageFont, ImageDraw - - draw = ImageDraw.Draw(image) - - # use a bitmap font - font = ImageFont.load("arial.pil") - - draw.text((10, 10), "hello", font=font) - - # use a truetype font - font = ImageFont.truetype("arial.ttf", 15) - - draw.text((10, 25), "world", font=font) - -Functions ---------- - -.. autofunction:: PIL.ImageFont.load -.. autofunction:: PIL.ImageFont.load_path -.. autofunction:: PIL.ImageFont.truetype -.. autofunction:: PIL.ImageFont.load_default - -Methods -------- - -.. autoclass:: PIL.ImageFont.ImageFont - :members: - -.. autoclass:: PIL.ImageFont.FreeTypeFont - :members: - -.. autoclass:: PIL.ImageFont.TransposedFont - :members: - :undoc-members: - -Constants ---------- - -.. class:: Layout - - .. py:attribute:: BASIC - - Use basic text layout for TrueType font. - Advanced features such as text direction are not supported. - - .. py:attribute:: RAQM - - Use Raqm text layout for TrueType font. - Advanced features are supported. - - Requires Raqm, you can check support using - :py:func:`PIL.features.check_feature` with ``feature="raqm"``. - -.. data:: MAX_STRING_LENGTH - - Set to 1,000,000, to protect against potential DOS attacks. Pillow will - raise a :py:exc:`ValueError` if the number of characters is over this limit. The - check can be disabled by setting ``ImageFont.MAX_STRING_LENGTH = None``. +.. py:module:: PIL.ImageFont +.. py:currentmodule:: PIL.ImageFont + +:py:mod:`~PIL.ImageFont` Module +=============================== + +The :py:mod:`~PIL.ImageFont` module defines a class with the same name. Instances of +this class store bitmap fonts, and are used with the +:py:meth:`PIL.ImageDraw.ImageDraw.text` method. + +PIL uses its own font file format to store bitmap fonts, limited to 256 characters. You can use +`pilfont.py `_ +from :pypi:`pillow-scripts` to convert BDF and +PCF font descriptors (X window font formats) to this format. + +Starting with version 1.1.4, PIL can be configured to support TrueType and +OpenType fonts (as well as other font formats supported by the FreeType +library). For earlier versions, TrueType support is only available as part of +the imToolkit package. + +.. 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 + is over a certain limit, :py:data:`MAX_STRING_LENGTH`. + + This threshold can be changed by setting + :py:data:`MAX_STRING_LENGTH`. It can be disabled by setting + ``ImageFont.MAX_STRING_LENGTH = None``. + +Example +------- + +:: + + from PIL import ImageFont, ImageDraw + + draw = ImageDraw.Draw(image) + + # use a bitmap font + font = ImageFont.load("arial.pil") + + draw.text((10, 10), "hello", font=font) + + # use a truetype font + font = ImageFont.truetype("arial.ttf", 15) + + draw.text((10, 25), "world", font=font) + +Functions +--------- + +.. autofunction:: PIL.ImageFont.load +.. autofunction:: PIL.ImageFont.load_path +.. autofunction:: PIL.ImageFont.truetype +.. autofunction:: PIL.ImageFont.load_default + +Methods +------- + +.. autoclass:: PIL.ImageFont.ImageFont + :members: + +.. autoclass:: PIL.ImageFont.FreeTypeFont + :members: + +.. autoclass:: PIL.ImageFont.TransposedFont + :members: + :undoc-members: + +Constants +--------- + +.. class:: Layout + + .. py:attribute:: BASIC + + Use basic text layout for TrueType font. + Advanced features such as text direction are not supported. + + .. py:attribute:: RAQM + + Use Raqm text layout for TrueType font. + Advanced features are supported. + + Requires Raqm, you can check support using + :py:func:`PIL.features.check_feature` with ``feature="raqm"``. + +.. data:: MAX_STRING_LENGTH + + Set to 1,000,000, to protect against potential DOS attacks. Pillow will + raise a :py:exc:`ValueError` if the number of characters is over this limit. The + check can be disabled by setting ``ImageFont.MAX_STRING_LENGTH = None``. diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst index db2987eb0..1fbc230f4 100644 --- a/docs/reference/ImageGrab.rst +++ b/docs/reference/ImageGrab.rst @@ -1,59 +1,59 @@ -.. py:module:: PIL.ImageGrab -.. py:currentmodule:: PIL.ImageGrab - -:py:mod:`~PIL.ImageGrab` Module -=============================== - -The :py:mod:`~PIL.ImageGrab` module can be used to copy the contents of the screen -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) - - 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. - - .. versionadded:: 1.1.3 (Windows), 3.0.0 (macOS), 7.1.0 (Linux) - - :param bbox: What region to copy. Default is the entire screen. - On macOS, this is not increased to 2x for Retina screens, so the full - width of a Retina screen would be 1440, not 2880. - On Windows, the top-left point may be negative if ``all_screens=True`` - is used. - :param include_layered_windows: Includes layered windows. Windows OS only. - - .. versionadded:: 6.1.0 - :param all_screens: Capture all monitors. Windows OS only. - - .. versionadded:: 6.2.0 - - :param xdisplay: - X11 Display address. Pass :data:`None` to grab the default system screen. Pass ``""`` to grab the default X11 screen on Windows or macOS. - - You can check X11 support using :py:func:`PIL.features.check_feature` with ``feature="xcb"``. - - .. versionadded:: 7.1.0 - :return: An image - -.. py:function:: grabclipboard() - - Take a snapshot of the clipboard image, if any. - - On Linux, ``wl-paste`` or ``xclip`` is required. - - .. versionadded:: 1.1.4 (Windows), 3.3.0 (macOS), 9.4.0 (Linux) - - :return: On Windows, an image, a list of filenames, - or None if the clipboard does not contain image data or filenames. - Note that if a list is returned, the filenames may not represent image files. - - On Mac, an image, - or None if the clipboard does not contain image data. - - On Linux, an image. +.. py:module:: PIL.ImageGrab +.. py:currentmodule:: PIL.ImageGrab + +:py:mod:`~PIL.ImageGrab` Module +=============================== + +The :py:mod:`~PIL.ImageGrab` module can be used to copy the contents of the screen +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) + + 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. + + .. versionadded:: 1.1.3 (Windows), 3.0.0 (macOS), 7.1.0 (Linux) + + :param bbox: What region to copy. Default is the entire screen. + On macOS, this is not increased to 2x for Retina screens, so the full + width of a Retina screen would be 1440, not 2880. + On Windows, the top-left point may be negative if ``all_screens=True`` + is used. + :param include_layered_windows: Includes layered windows. Windows OS only. + + .. versionadded:: 6.1.0 + :param all_screens: Capture all monitors. Windows OS only. + + .. versionadded:: 6.2.0 + + :param xdisplay: + X11 Display address. Pass :data:`None` to grab the default system screen. Pass ``""`` to grab the default X11 screen on Windows or macOS. + + You can check X11 support using :py:func:`PIL.features.check_feature` with ``feature="xcb"``. + + .. versionadded:: 7.1.0 + :return: An image + +.. py:function:: grabclipboard() + + Take a snapshot of the clipboard image, if any. + + On Linux, ``wl-paste`` or ``xclip`` is required. + + .. versionadded:: 1.1.4 (Windows), 3.3.0 (macOS), 9.4.0 (Linux) + + :return: On Windows, an image, a list of filenames, + or None if the clipboard does not contain image data or filenames. + Note that if a list is returned, the filenames may not represent image files. + + On Mac, an image, + or None if the clipboard does not contain image data. + + On Linux, an image. diff --git a/docs/reference/ImageMath.rst b/docs/reference/ImageMath.rst index 2535db711..4836e8d9e 100644 --- a/docs/reference/ImageMath.rst +++ b/docs/reference/ImageMath.rst @@ -1,169 +1,169 @@ -.. py:module:: PIL.ImageMath -.. py:currentmodule:: PIL.ImageMath - -:py:mod:`~PIL.ImageMath` Module -=============================== - -The :py:mod:`~PIL.ImageMath` module can be used to evaluate “image expressionsâ€, that -can take a number of images and generate a result. - -:py:mod:`~PIL.ImageMath` only supports single-layer images. To process multi-band -images, use the :py:meth:`~PIL.Image.Image.split` method or :py:func:`~PIL.Image.merge` -function. - -Example: Using the :py:mod:`~PIL.ImageMath` module --------------------------------------------------- - -:: - - from PIL import Image, ImageMath - - with Image.open("image1.jpg") as im1: - with Image.open("image2.jpg") as im2: - out = ImageMath.lambda_eval( - lambda args: args["convert"](args["min"](args["a"], args["b"]), 'L'), - a=im1, - b=im2 - ) - out = ImageMath.unsafe_eval( - "convert(min(a, b), 'L')", - a=im1, - b=im2 - ) - -.. py:function:: lambda_eval(expression, options) - - Returns the result of an image function. - - :param expression: A function that receives a dictionary. - :param options: Values to add to the function's dictionary, mapping image - names to Image instances. You can use one or more keyword - arguments instead of a dictionary, as shown in the above - example. Note that the names must be valid Python - identifiers. - :return: An image, an integer value, a floating point value, - or a pixel tuple, depending on the expression. - -.. py:function:: unsafe_eval(expression, options) - - Evaluates an image expression. - - .. danger:: - This uses Python's ``eval()`` function to process the expression string, - and carries the security risks of doing so. It is not - recommended to process expressions without considering this. - :py:meth:`lambda_eval` is a more secure alternative. - - :py:mod:`~PIL.ImageMath` only supports single-layer images. To process multi-band - images, use the :py:meth:`~PIL.Image.Image.split` method or - :py:func:`~PIL.Image.merge` function. - - :param expression: A string which uses the standard Python expression - syntax. In addition to the standard operators, you can - also use the functions described below. - :param options: Values to add to the function's dictionary, mapping image - names to Image instances. You can use one or more keyword - arguments instead of a dictionary, as shown in the above - example. Note that the names must be valid Python - identifiers. - :return: An image, an integer value, a floating point value, - or a pixel tuple, depending on the expression. - -Expression syntax ------------------ - -* :py:meth:`lambda_eval` expressions are functions that receive a dictionary - containing images and operators. - -* :py:meth:`unsafe_eval` expressions are standard Python expressions, - but they’re evaluated in a non-standard environment. - -.. danger:: - :py:meth:`unsafe_eval` uses Python's ``eval()`` function to process the - expression string, and carries the security risks of doing so. - It is not recommended to process expressions without considering this. - :py:meth:`lambda_eval` is a more secure alternative. - -Standard Operators -^^^^^^^^^^^^^^^^^^ - -You can use standard arithmetical operators for addition (+), subtraction (-), -multiplication (*), and division (/). - -The module also supports unary minus (-), modulo (%), and power (**) operators. - -Note that all operations are done with 32-bit integers or 32-bit floating -point values, as necessary. For example, if you add two 8-bit images, the -result will be a 32-bit integer image. If you add a floating point constant to -an 8-bit image, the result will be a 32-bit floating point image. - -You can force conversion using the ``convert()``, ``float()``, and ``int()`` -functions described below. - -Bitwise Operators -^^^^^^^^^^^^^^^^^ - -The module also provides operations that operate on individual bits. This -includes and (&), or (|), and exclusive or (^). You can also invert (~) all -pixel bits. - -Note that the operands are converted to 32-bit signed integers before the -bitwise operation is applied. This means that you’ll get negative values if -you invert an ordinary grayscale image. You can use the and (&) operator to -mask off unwanted bits. - -Bitwise operators don’t work on floating point images. - -Logical Operators -^^^^^^^^^^^^^^^^^ - -Logical operators like ``and``, ``or``, and ``not`` work -on entire images, rather than individual pixels. - -An empty image (all pixels zero) is treated as false. All other images are -treated as true. - -Note that ``and`` and ``or`` return the last evaluated operand, -while not always returns a boolean value. - -Built-in Functions -^^^^^^^^^^^^^^^^^^ - -These functions are applied to each individual pixel. - -.. py:currentmodule:: None - -.. py:function:: abs(image) - :noindex: - - Absolute value. - -.. py:function:: convert(image, mode) - :noindex: - - Convert image to the given mode. The mode must be given as a string - constant. - -.. py:function:: float(image) - :noindex: - - Convert image to 32-bit floating point. This is equivalent to - convert(image, “Fâ€). - -.. py:function:: int(image) - :noindex: - - Convert image to 32-bit integer. This is equivalent to convert(image, “Iâ€). - - Note that 1-bit and 8-bit images are automatically converted to 32-bit - integers if necessary to get a correct result. - -.. py:function:: max(image1, image2) - :noindex: - - Maximum value. - -.. py:function:: min(image1, image2) - :noindex: - - Minimum value. +.. py:module:: PIL.ImageMath +.. py:currentmodule:: PIL.ImageMath + +:py:mod:`~PIL.ImageMath` Module +=============================== + +The :py:mod:`~PIL.ImageMath` module can be used to evaluate “image expressionsâ€, that +can take a number of images and generate a result. + +:py:mod:`~PIL.ImageMath` only supports single-layer images. To process multi-band +images, use the :py:meth:`~PIL.Image.Image.split` method or :py:func:`~PIL.Image.merge` +function. + +Example: Using the :py:mod:`~PIL.ImageMath` module +-------------------------------------------------- + +:: + + from PIL import Image, ImageMath + + with Image.open("image1.jpg") as im1: + with Image.open("image2.jpg") as im2: + out = ImageMath.lambda_eval( + lambda args: args["convert"](args["min"](args["a"], args["b"]), 'L'), + a=im1, + b=im2 + ) + out = ImageMath.unsafe_eval( + "convert(min(a, b), 'L')", + a=im1, + b=im2 + ) + +.. py:function:: lambda_eval(expression, options) + + Returns the result of an image function. + + :param expression: A function that receives a dictionary. + :param options: Values to add to the function's dictionary, mapping image + names to Image instances. You can use one or more keyword + arguments instead of a dictionary, as shown in the above + example. Note that the names must be valid Python + identifiers. + :return: An image, an integer value, a floating point value, + or a pixel tuple, depending on the expression. + +.. py:function:: unsafe_eval(expression, options) + + Evaluates an image expression. + + .. danger:: + This uses Python's ``eval()`` function to process the expression string, + and carries the security risks of doing so. It is not + recommended to process expressions without considering this. + :py:meth:`lambda_eval` is a more secure alternative. + + :py:mod:`~PIL.ImageMath` only supports single-layer images. To process multi-band + images, use the :py:meth:`~PIL.Image.Image.split` method or + :py:func:`~PIL.Image.merge` function. + + :param expression: A string which uses the standard Python expression + syntax. In addition to the standard operators, you can + also use the functions described below. + :param options: Values to add to the function's dictionary, mapping image + names to Image instances. You can use one or more keyword + arguments instead of a dictionary, as shown in the above + example. Note that the names must be valid Python + identifiers. + :return: An image, an integer value, a floating point value, + or a pixel tuple, depending on the expression. + +Expression syntax +----------------- + +* :py:meth:`lambda_eval` expressions are functions that receive a dictionary + containing images and operators. + +* :py:meth:`unsafe_eval` expressions are standard Python expressions, + but they’re evaluated in a non-standard environment. + +.. danger:: + :py:meth:`unsafe_eval` uses Python's ``eval()`` function to process the + expression string, and carries the security risks of doing so. + It is not recommended to process expressions without considering this. + :py:meth:`lambda_eval` is a more secure alternative. + +Standard Operators +^^^^^^^^^^^^^^^^^^ + +You can use standard arithmetical operators for addition (+), subtraction (-), +multiplication (*), and division (/). + +The module also supports unary minus (-), modulo (%), and power (**) operators. + +Note that all operations are done with 32-bit integers or 32-bit floating +point values, as necessary. For example, if you add two 8-bit images, the +result will be a 32-bit integer image. If you add a floating point constant to +an 8-bit image, the result will be a 32-bit floating point image. + +You can force conversion using the ``convert()``, ``float()``, and ``int()`` +functions described below. + +Bitwise Operators +^^^^^^^^^^^^^^^^^ + +The module also provides operations that operate on individual bits. This +includes and (&), or (|), and exclusive or (^). You can also invert (~) all +pixel bits. + +Note that the operands are converted to 32-bit signed integers before the +bitwise operation is applied. This means that you’ll get negative values if +you invert an ordinary grayscale image. You can use the and (&) operator to +mask off unwanted bits. + +Bitwise operators don’t work on floating point images. + +Logical Operators +^^^^^^^^^^^^^^^^^ + +Logical operators like ``and``, ``or``, and ``not`` work +on entire images, rather than individual pixels. + +An empty image (all pixels zero) is treated as false. All other images are +treated as true. + +Note that ``and`` and ``or`` return the last evaluated operand, +while not always returns a boolean value. + +Built-in Functions +^^^^^^^^^^^^^^^^^^ + +These functions are applied to each individual pixel. + +.. py:currentmodule:: None + +.. py:function:: abs(image) + :noindex: + + Absolute value. + +.. py:function:: convert(image, mode) + :noindex: + + Convert image to the given mode. The mode must be given as a string + constant. + +.. py:function:: float(image) + :noindex: + + Convert image to 32-bit floating point. This is equivalent to + convert(image, “Fâ€). + +.. py:function:: int(image) + :noindex: + + Convert image to 32-bit integer. This is equivalent to convert(image, “Iâ€). + + Note that 1-bit and 8-bit images are automatically converted to 32-bit + integers if necessary to get a correct result. + +.. py:function:: max(image1, image2) + :noindex: + + Maximum value. + +.. py:function:: min(image1, image2) + :noindex: + + Minimum value. diff --git a/docs/reference/ImageMorph.rst b/docs/reference/ImageMorph.rst index d4522a06a..05f9a2fd3 100644 --- a/docs/reference/ImageMorph.rst +++ b/docs/reference/ImageMorph.rst @@ -1,13 +1,13 @@ -.. py:module:: PIL.ImageMorph -.. py:currentmodule:: PIL.ImageMorph - -:py:mod:`~PIL.ImageMorph` Module -================================ - -The :py:mod:`~PIL.ImageMorph` module provides morphology operations on images. - -.. automodule:: PIL.ImageMorph - :members: - :undoc-members: - :show-inheritance: - :noindex: +.. py:module:: PIL.ImageMorph +.. py:currentmodule:: PIL.ImageMorph + +:py:mod:`~PIL.ImageMorph` Module +================================ + +The :py:mod:`~PIL.ImageMorph` module provides morphology operations on images. + +.. automodule:: PIL.ImageMorph + :members: + :undoc-members: + :show-inheritance: + :noindex: diff --git a/docs/reference/ImageOps.rst b/docs/reference/ImageOps.rst index 051fdcfc9..b3e7ccd04 100644 --- a/docs/reference/ImageOps.rst +++ b/docs/reference/ImageOps.rst @@ -1,63 +1,63 @@ -.. py:module:: PIL.ImageOps -.. py:currentmodule:: PIL.ImageOps - -:py:mod:`~PIL.ImageOps` Module -============================== - -The :py:mod:`~PIL.ImageOps` module contains a number of ‘ready-made’ image -processing operations. This module is somewhat experimental, and most operators -only work on L and RGB images. - -.. versionadded:: 1.1.3 - -.. autofunction:: autocontrast -.. autofunction:: colorize -.. autofunction:: crop -.. autofunction:: scale -.. autoclass:: SupportsGetMesh - :show-inheritance: -.. autofunction:: deform -.. autofunction:: equalize -.. autofunction:: expand -.. autofunction:: flip -.. autofunction:: grayscale -.. autofunction:: invert -.. autofunction:: mirror -.. autofunction:: posterize -.. autofunction:: solarize -.. autofunction:: exif_transpose - -.. _relative-resize: - -Resize relative to a given size -------------------------------- - -:: - - from PIL import Image, ImageOps - size = (100, 150) - with Image.open("Tests/images/hopper.png") as im: - ImageOps.contain(im, size).save("imageops_contain.png") - ImageOps.cover(im, size).save("imageops_cover.png") - ImageOps.fit(im, size).save("imageops_fit.png") - ImageOps.pad(im, size, color="#f00").save("imageops_pad.png") - - # thumbnail() can also be used, - # but will modify the image object in place - im.thumbnail(size) - im.save("imageops_thumbnail.png") - -+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+ -| | :py:meth:`~PIL.Image.Image.thumbnail` | :py:meth:`~PIL.ImageOps.contain` | :py:meth:`~PIL.ImageOps.cover` | :py:meth:`~PIL.ImageOps.fit` | :py:meth:`~PIL.ImageOps.pad` | -+================+===========================================+============================================+==========================================+========================================+========================================+ -|Given size | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` | -+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+ -|Resulting image | .. image:: ../example/image_thumbnail.png | .. image:: ../example/imageops_contain.png | .. image:: ../example/imageops_cover.png | .. image:: ../example/imageops_fit.png | .. image:: ../example/imageops_pad.png | -+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+ -|Resulting size | ``100×100`` | ``100×100`` | ``150×150`` | ``100×150`` | ``100×150`` | -+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+ - -.. autofunction:: contain -.. autofunction:: cover -.. autofunction:: fit -.. autofunction:: pad +.. py:module:: PIL.ImageOps +.. py:currentmodule:: PIL.ImageOps + +:py:mod:`~PIL.ImageOps` Module +============================== + +The :py:mod:`~PIL.ImageOps` module contains a number of ‘ready-made’ image +processing operations. This module is somewhat experimental, and most operators +only work on L and RGB images. + +.. versionadded:: 1.1.3 + +.. autofunction:: autocontrast +.. autofunction:: colorize +.. autofunction:: crop +.. autofunction:: scale +.. autoclass:: SupportsGetMesh + :show-inheritance: +.. autofunction:: deform +.. autofunction:: equalize +.. autofunction:: expand +.. autofunction:: flip +.. autofunction:: grayscale +.. autofunction:: invert +.. autofunction:: mirror +.. autofunction:: posterize +.. autofunction:: solarize +.. autofunction:: exif_transpose + +.. _relative-resize: + +Resize relative to a given size +------------------------------- + +:: + + from PIL import Image, ImageOps + size = (100, 150) + with Image.open("Tests/images/hopper.png") as im: + ImageOps.contain(im, size).save("imageops_contain.png") + ImageOps.cover(im, size).save("imageops_cover.png") + ImageOps.fit(im, size).save("imageops_fit.png") + ImageOps.pad(im, size, color="#f00").save("imageops_pad.png") + + # thumbnail() can also be used, + # but will modify the image object in place + im.thumbnail(size) + im.save("imageops_thumbnail.png") + ++----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+ +| | :py:meth:`~PIL.Image.Image.thumbnail` | :py:meth:`~PIL.ImageOps.contain` | :py:meth:`~PIL.ImageOps.cover` | :py:meth:`~PIL.ImageOps.fit` | :py:meth:`~PIL.ImageOps.pad` | ++================+===========================================+============================================+==========================================+========================================+========================================+ +|Given size | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` | ++----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+ +|Resulting image | .. image:: ../example/image_thumbnail.png | .. image:: ../example/imageops_contain.png | .. image:: ../example/imageops_cover.png | .. image:: ../example/imageops_fit.png | .. image:: ../example/imageops_pad.png | ++----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+ +|Resulting size | ``100×100`` | ``100×100`` | ``150×150`` | ``100×150`` | ``100×150`` | ++----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+ + +.. autofunction:: contain +.. autofunction:: cover +.. autofunction:: fit +.. autofunction:: pad diff --git a/docs/reference/ImagePalette.rst b/docs/reference/ImagePalette.rst index 72ccfac7d..fb0b368dc 100644 --- a/docs/reference/ImagePalette.rst +++ b/docs/reference/ImagePalette.rst @@ -1,17 +1,17 @@ -.. py:module:: PIL.ImagePalette -.. py:currentmodule:: PIL.ImagePalette - -:py:mod:`~PIL.ImagePalette` Module -================================== - -The :py:mod:`~PIL.ImagePalette` module contains a class of the same name to -represent the color palette of palette mapped images. - -.. note:: - - The :py:class:`~PIL.ImagePalette.ImagePalette` class has several methods, - but they are all marked as "experimental." Read that as you will. The - ``[source]`` link is there for a reason. - -.. autoclass:: PIL.ImagePalette.ImagePalette - :members: +.. py:module:: PIL.ImagePalette +.. py:currentmodule:: PIL.ImagePalette + +:py:mod:`~PIL.ImagePalette` Module +================================== + +The :py:mod:`~PIL.ImagePalette` module contains a class of the same name to +represent the color palette of palette mapped images. + +.. note:: + + The :py:class:`~PIL.ImagePalette.ImagePalette` class has several methods, + but they are all marked as "experimental." Read that as you will. The + ``[source]`` link is there for a reason. + +.. autoclass:: PIL.ImagePalette.ImagePalette + :members: diff --git a/docs/reference/ImagePath.rst b/docs/reference/ImagePath.rst index 23544b613..6312806a8 100644 --- a/docs/reference/ImagePath.rst +++ b/docs/reference/ImagePath.rst @@ -1,66 +1,66 @@ -.. py:module:: PIL.ImagePath -.. py:currentmodule:: PIL.ImagePath - -:py:mod:`~PIL.ImagePath` Module -=============================== - -The :py:mod:`~PIL.ImagePath` module is used to store and manipulate 2-dimensional -vector data. Path objects can be passed to the methods on the -:py:mod:`~PIL.ImageDraw` module. - -.. py:class:: PIL.ImagePath.Path - - A path object. The coordinate list can be any sequence object containing - either 2-tuples [(x, y), …] or numeric values [x, y, …]. - - You can also create a path object from another path object. - - In 1.1.6 and later, you can also pass in any object that implements - Python’s buffer API. The buffer should provide read access, and contain C - floats in machine byte order. - - The path object implements most parts of the Python sequence interface, and - behaves like a list of (x, y) pairs. You can use len(), item access, and - slicing as usual. However, this does not support slice assignment, or item - and slice deletion. - - :param xy: A sequence. The sequence can contain 2-tuples [(x, y), ...] - or a flat list of numbers [x, y, ...]. - -.. py:method:: PIL.ImagePath.Path.compact(distance=2) - - Compacts the path, by removing points that are close to each other. This - method modifies the path in place, and returns the number of points left in - the path. - - ``distance`` is measured as `Manhattan distance`_ and defaults to two - pixels. - -.. _Manhattan distance: https://en.wikipedia.org/wiki/Manhattan_distance - -.. py:method:: PIL.ImagePath.Path.getbbox() - - Gets the bounding box of the path. - - :return: ``(x0, y0, x1, y1)`` - -.. py:method:: PIL.ImagePath.Path.map(function) - - Maps the path through a function. - -.. py:method:: PIL.ImagePath.Path.tolist(flat=False) - - Converts the path to a Python list [(x, y), …]. - - :param flat: By default, this function returns a list of 2-tuples - [(x, y), ...]. If this argument is ``True``, it - returns a flat list [x, y, ...] instead. - :return: A list of coordinates. See ``flat``. - -.. py:method:: PIL.ImagePath.Path.transform(matrix) - - Transforms the path in place, using an affine transform. The matrix is a - 6-tuple (a, b, c, d, e, f), and each point is mapped as follows:: - - xOut = xIn * a + yIn * b + c - yOut = xIn * d + yIn * e + f +.. py:module:: PIL.ImagePath +.. py:currentmodule:: PIL.ImagePath + +:py:mod:`~PIL.ImagePath` Module +=============================== + +The :py:mod:`~PIL.ImagePath` module is used to store and manipulate 2-dimensional +vector data. Path objects can be passed to the methods on the +:py:mod:`~PIL.ImageDraw` module. + +.. py:class:: PIL.ImagePath.Path + + A path object. The coordinate list can be any sequence object containing + either 2-tuples [(x, y), …] or numeric values [x, y, …]. + + You can also create a path object from another path object. + + In 1.1.6 and later, you can also pass in any object that implements + Python’s buffer API. The buffer should provide read access, and contain C + floats in machine byte order. + + The path object implements most parts of the Python sequence interface, and + behaves like a list of (x, y) pairs. You can use len(), item access, and + slicing as usual. However, this does not support slice assignment, or item + and slice deletion. + + :param xy: A sequence. The sequence can contain 2-tuples [(x, y), ...] + or a flat list of numbers [x, y, ...]. + +.. py:method:: PIL.ImagePath.Path.compact(distance=2) + + Compacts the path, by removing points that are close to each other. This + method modifies the path in place, and returns the number of points left in + the path. + + ``distance`` is measured as `Manhattan distance`_ and defaults to two + pixels. + +.. _Manhattan distance: https://en.wikipedia.org/wiki/Manhattan_distance + +.. py:method:: PIL.ImagePath.Path.getbbox() + + Gets the bounding box of the path. + + :return: ``(x0, y0, x1, y1)`` + +.. py:method:: PIL.ImagePath.Path.map(function) + + Maps the path through a function. + +.. py:method:: PIL.ImagePath.Path.tolist(flat=False) + + Converts the path to a Python list [(x, y), …]. + + :param flat: By default, this function returns a list of 2-tuples + [(x, y), ...]. If this argument is ``True``, it + returns a flat list [x, y, ...] instead. + :return: A list of coordinates. See ``flat``. + +.. py:method:: PIL.ImagePath.Path.transform(matrix) + + Transforms the path in place, using an affine transform. The matrix is a + 6-tuple (a, b, c, d, e, f), and each point is mapped as follows:: + + xOut = xIn * a + yIn * b + c + yOut = xIn * d + yIn * e + f diff --git a/docs/reference/ImageQt.rst b/docs/reference/ImageQt.rst index 7e67a44d3..f1ebcf3b6 100644 --- a/docs/reference/ImageQt.rst +++ b/docs/reference/ImageQt.rst @@ -1,20 +1,20 @@ -.. py:module:: PIL.ImageQt -.. py:currentmodule:: PIL.ImageQt - -:py:mod:`~PIL.ImageQt` Module -============================= - -The :py:mod:`~PIL.ImageQt` module contains support for creating PyQt6 or PySide6 -QImage objects from PIL images. - -.. versionadded:: 1.1.6 - -.. py:class:: ImageQt(image) - - Creates an :py:class:`~PIL.ImageQt.ImageQt` object from a PIL - :py:class:`~PIL.Image.Image` object. This class is a subclass of - QtGui.QImage, which means that you can pass the resulting objects directly - to PyQt6/PySide6 API functions and methods. - - This operation is currently supported for mode 1, L, P, RGB, and RGBA - images. To handle other modes, you need to convert the image first. +.. py:module:: PIL.ImageQt +.. py:currentmodule:: PIL.ImageQt + +:py:mod:`~PIL.ImageQt` Module +============================= + +The :py:mod:`~PIL.ImageQt` module contains support for creating PyQt6 or PySide6 +QImage objects from PIL images. + +.. versionadded:: 1.1.6 + +.. py:class:: ImageQt(image) + + Creates an :py:class:`~PIL.ImageQt.ImageQt` object from a PIL + :py:class:`~PIL.Image.Image` object. This class is a subclass of + QtGui.QImage, which means that you can pass the resulting objects directly + to PyQt6/PySide6 API functions and methods. + + This operation is currently supported for mode 1, L, P, RGB, and RGBA + images. To handle other modes, you need to convert the image first. diff --git a/docs/reference/ImageSequence.rst b/docs/reference/ImageSequence.rst index a27b2fb4e..90387d54b 100644 --- a/docs/reference/ImageSequence.rst +++ b/docs/reference/ImageSequence.rst @@ -1,32 +1,32 @@ -.. py:module:: PIL.ImageSequence -.. py:currentmodule:: PIL.ImageSequence - -:py:mod:`~PIL.ImageSequence` Module -=================================== - -The :py:mod:`~PIL.ImageSequence` module contains a wrapper class that lets you -iterate over the frames of an image sequence. - -Extracting frames from an animation ------------------------------------ - -:: - - from PIL import Image, ImageSequence - - with Image.open("animation.fli") as im: - index = 1 - for frame in ImageSequence.Iterator(im): - frame.save(f"frame{index}.png") - index += 1 - -The :py:class:`~PIL.ImageSequence.Iterator` class -------------------------------------------------- - -.. autoclass:: PIL.ImageSequence.Iterator - :members: - -Functions ---------- - -.. autofunction:: PIL.ImageSequence.all_frames +.. py:module:: PIL.ImageSequence +.. py:currentmodule:: PIL.ImageSequence + +:py:mod:`~PIL.ImageSequence` Module +=================================== + +The :py:mod:`~PIL.ImageSequence` module contains a wrapper class that lets you +iterate over the frames of an image sequence. + +Extracting frames from an animation +----------------------------------- + +:: + + from PIL import Image, ImageSequence + + with Image.open("animation.fli") as im: + index = 1 + for frame in ImageSequence.Iterator(im): + frame.save(f"frame{index}.png") + index += 1 + +The :py:class:`~PIL.ImageSequence.Iterator` class +------------------------------------------------- + +.. autoclass:: PIL.ImageSequence.Iterator + :members: + +Functions +--------- + +.. autofunction:: PIL.ImageSequence.all_frames diff --git a/docs/reference/ImageShow.rst b/docs/reference/ImageShow.rst index 5cedede69..a3bdef4ad 100644 --- a/docs/reference/ImageShow.rst +++ b/docs/reference/ImageShow.rst @@ -1,33 +1,33 @@ -.. py:module:: PIL.ImageShow -.. py:currentmodule:: PIL.ImageShow - -:py:mod:`~PIL.ImageShow` Module -=============================== - -The :py:mod:`~PIL.ImageShow` Module is used to display images. -All default viewers convert the image to be shown to PNG format. - -.. autofunction:: PIL.ImageShow.show - -.. autoclass:: IPythonViewer -.. autoclass:: WindowsViewer -.. autoclass:: MacViewer - -.. class:: UnixViewer - - The following viewers may be registered on Unix-based systems, if the given command is found: - - .. autoclass:: PIL.ImageShow.XDGViewer - .. autoclass:: PIL.ImageShow.DisplayViewer - .. autoclass:: PIL.ImageShow.GmDisplayViewer - .. autoclass:: PIL.ImageShow.EogViewer - .. autoclass:: PIL.ImageShow.XVViewer - - To provide maximum functionality on Unix-based systems, temporary files created - from images will not be automatically removed by Pillow. - -.. autofunction:: PIL.ImageShow.register -.. autoclass:: PIL.ImageShow.Viewer - :member-order: bysource - :members: - :undoc-members: +.. py:module:: PIL.ImageShow +.. py:currentmodule:: PIL.ImageShow + +:py:mod:`~PIL.ImageShow` Module +=============================== + +The :py:mod:`~PIL.ImageShow` Module is used to display images. +All default viewers convert the image to be shown to PNG format. + +.. autofunction:: PIL.ImageShow.show + +.. autoclass:: IPythonViewer +.. autoclass:: WindowsViewer +.. autoclass:: MacViewer + +.. class:: UnixViewer + + The following viewers may be registered on Unix-based systems, if the given command is found: + + .. autoclass:: PIL.ImageShow.XDGViewer + .. autoclass:: PIL.ImageShow.DisplayViewer + .. autoclass:: PIL.ImageShow.GmDisplayViewer + .. autoclass:: PIL.ImageShow.EogViewer + .. autoclass:: PIL.ImageShow.XVViewer + + To provide maximum functionality on Unix-based systems, temporary files created + from images will not be automatically removed by Pillow. + +.. autofunction:: PIL.ImageShow.register +.. autoclass:: PIL.ImageShow.Viewer + :member-order: bysource + :members: + :undoc-members: diff --git a/docs/reference/ImageStat.rst b/docs/reference/ImageStat.rst index f69466382..0eda2efb8 100644 --- a/docs/reference/ImageStat.rst +++ b/docs/reference/ImageStat.rst @@ -1,12 +1,12 @@ -.. py:module:: PIL.ImageStat -.. py:currentmodule:: PIL.ImageStat - -:py:mod:`~PIL.ImageStat` Module -=============================== - -The :py:mod:`~PIL.ImageStat` module calculates global statistics for an image, or -for a region of an image. - -.. autoclass:: Stat - :members: - :special-members: __init__ +.. py:module:: PIL.ImageStat +.. py:currentmodule:: PIL.ImageStat + +:py:mod:`~PIL.ImageStat` Module +=============================== + +The :py:mod:`~PIL.ImageStat` module calculates global statistics for an image, or +for a region of an image. + +.. autoclass:: Stat + :members: + :special-members: __init__ diff --git a/docs/reference/ImageTk.rst b/docs/reference/ImageTk.rst index 134ef5651..e4ff5fcc0 100644 --- a/docs/reference/ImageTk.rst +++ b/docs/reference/ImageTk.rst @@ -1,16 +1,16 @@ -.. py:module:: PIL.ImageTk -.. py:currentmodule:: PIL.ImageTk - -:py:mod:`~PIL.ImageTk` Module -============================= - -The :py:mod:`~PIL.ImageTk` module contains support to create and modify Tkinter -BitmapImage and PhotoImage objects from PIL images. - -For examples, see the demo programs in the Scripts directory. - -.. autoclass:: PIL.ImageTk.BitmapImage - :members: - -.. autoclass:: PIL.ImageTk.PhotoImage - :members: +.. py:module:: PIL.ImageTk +.. py:currentmodule:: PIL.ImageTk + +:py:mod:`~PIL.ImageTk` Module +============================= + +The :py:mod:`~PIL.ImageTk` module contains support to create and modify Tkinter +BitmapImage and PhotoImage objects from PIL images. + +For examples, see the demo programs in the Scripts directory. + +.. autoclass:: PIL.ImageTk.BitmapImage + :members: + +.. autoclass:: PIL.ImageTk.PhotoImage + :members: diff --git a/docs/reference/ImageTransform.rst b/docs/reference/ImageTransform.rst index 5b0a5ce49..aab660763 100644 --- a/docs/reference/ImageTransform.rst +++ b/docs/reference/ImageTransform.rst @@ -1,40 +1,40 @@ - -.. py:module:: PIL.ImageTransform -.. py:currentmodule:: PIL.ImageTransform - -:py:mod:`~PIL.ImageTransform` Module -==================================== - -The :py:mod:`~PIL.ImageTransform` module contains implementations of -:py:class:`~PIL.Image.ImageTransformHandler` for some of the builtin -:py:class:`.Image.Transform` methods. - -.. autoclass:: Transform - :members: - :undoc-members: - :show-inheritance: - -.. autoclass:: AffineTransform - :members: - :undoc-members: - :show-inheritance: - -.. autoclass:: PerspectiveTransform - :members: - :undoc-members: - :show-inheritance: - -.. autoclass:: ExtentTransform - :members: - :undoc-members: - :show-inheritance: - -.. autoclass:: QuadTransform - :members: - :undoc-members: - :show-inheritance: - -.. autoclass:: MeshTransform - :members: - :undoc-members: - :show-inheritance: + +.. py:module:: PIL.ImageTransform +.. py:currentmodule:: PIL.ImageTransform + +:py:mod:`~PIL.ImageTransform` Module +==================================== + +The :py:mod:`~PIL.ImageTransform` module contains implementations of +:py:class:`~PIL.Image.ImageTransformHandler` for some of the builtin +:py:class:`.Image.Transform` methods. + +.. autoclass:: Transform + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: AffineTransform + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: PerspectiveTransform + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: ExtentTransform + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: QuadTransform + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: MeshTransform + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/reference/ImageWin.rst b/docs/reference/ImageWin.rst index 4151be4a7..f5e84a577 100644 --- a/docs/reference/ImageWin.rst +++ b/docs/reference/ImageWin.rst @@ -1,29 +1,29 @@ -.. py:module:: PIL.ImageWin -.. py:currentmodule:: PIL.ImageWin - -:py:mod:`~PIL.ImageWin` Module (Windows-only) -============================================= - -The :py:mod:`~PIL.ImageWin` module contains support to create and display images on -Windows. - -ImageWin can be used with PythonWin and other user interface toolkits that -provide access to Windows device contexts or window handles. For example, -Tkinter makes the window handle available via the winfo_id method:: - - from PIL import ImageWin - - dib = ImageWin.Dib(...) - - hwnd = ImageWin.HWND(widget.winfo_id()) - dib.draw(hwnd, xy) - - -.. autoclass:: PIL.ImageWin.Dib - :members: - -.. autoclass:: PIL.ImageWin.HDC - :members: - -.. autoclass:: PIL.ImageWin.HWND - :members: +.. py:module:: PIL.ImageWin +.. py:currentmodule:: PIL.ImageWin + +:py:mod:`~PIL.ImageWin` Module (Windows-only) +============================================= + +The :py:mod:`~PIL.ImageWin` module contains support to create and display images on +Windows. + +ImageWin can be used with PythonWin and other user interface toolkits that +provide access to Windows device contexts or window handles. For example, +Tkinter makes the window handle available via the winfo_id method:: + + from PIL import ImageWin + + dib = ImageWin.Dib(...) + + hwnd = ImageWin.HWND(widget.winfo_id()) + dib.draw(hwnd, xy) + + +.. autoclass:: PIL.ImageWin.Dib + :members: + +.. autoclass:: PIL.ImageWin.HDC + :members: + +.. autoclass:: PIL.ImageWin.HWND + :members: diff --git a/docs/reference/JpegPresets.rst b/docs/reference/JpegPresets.rst index aafae44cf..4a7b335e9 100644 --- a/docs/reference/JpegPresets.rst +++ b/docs/reference/JpegPresets.rst @@ -1,11 +1,11 @@ -.. py:currentmodule:: PIL.JpegPresets - -:py:mod:`~PIL.JpegPresets` Module -================================= - -.. automodule:: PIL.JpegPresets - - .. data:: presets - :type: dict - - A dictionary of all supported presets. +.. py:currentmodule:: PIL.JpegPresets + +:py:mod:`~PIL.JpegPresets` Module +================================= + +.. automodule:: PIL.JpegPresets + + .. data:: presets + :type: dict + + A dictionary of all supported presets. diff --git a/docs/reference/PSDraw.rst b/docs/reference/PSDraw.rst index 3e8512e7a..85f1d1a69 100644 --- a/docs/reference/PSDraw.rst +++ b/docs/reference/PSDraw.rst @@ -1,11 +1,11 @@ -.. py:module:: PIL.PSDraw -.. py:currentmodule:: PIL.PSDraw - -:py:mod:`~PIL.PSDraw` Module -============================ - -The :py:mod:`~PIL.PSDraw` module provides simple print support for PostScript -printers. You can print text, graphics and images through this module. - -.. autoclass:: PIL.PSDraw.PSDraw - :members: +.. py:module:: PIL.PSDraw +.. py:currentmodule:: PIL.PSDraw + +:py:mod:`~PIL.PSDraw` Module +============================ + +The :py:mod:`~PIL.PSDraw` module provides simple print support for PostScript +printers. You can print text, graphics and images through this module. + +.. autoclass:: PIL.PSDraw.PSDraw + :members: diff --git a/docs/reference/PixelAccess.rst b/docs/reference/PixelAccess.rst index 04d6f5dcd..7305baa1c 100644 --- a/docs/reference/PixelAccess.rst +++ b/docs/reference/PixelAccess.rst @@ -1,85 +1,85 @@ -.. _PixelAccess: - -:py:class:`PixelAccess` Class -============================= - -The PixelAccess class provides read and write access to -:py:class:`PIL.Image` data at a pixel level. - -.. note:: Accessing individual pixels is fairly slow. If you are - looping over all of the pixels in an image, there is likely - a faster way using other parts of the Pillow API. - - :mod:`~PIL.Image`, :mod:`~PIL.ImageChops` and :mod:`~PIL.ImageOps` - have methods for many standard operations. If you wish to perform - a custom mapping, check out :py:meth:`~PIL.Image.Image.point`. - -Example -------- - -The following script loads an image, accesses one pixel from it, then -changes it. :: - - from PIL import Image - - with Image.open("hopper.jpg") as im: - px = im.load() - print(px[4, 4]) - px[4, 4] = (0, 0, 0) - print(px[4, 4]) - -Results in the following:: - - (23, 24, 68) - (0, 0, 0) - -Access using negative indexes is also possible. :: - - px[-1, -1] = (0, 0, 0) - print(px[-1, -1]) - - - -:py:class:`PixelAccess` Class ------------------------------ - -.. class:: PixelAccess - - .. method:: __setitem__(self, xy, color): - - Modifies the pixel at x,y. The color is given as a single - numerical value for single band images, and a tuple for - multi-band images - - :param xy: The pixel coordinate, given as (x, y). - :param color: The pixel value according to its mode. e.g. tuple (r, g, b) for RGB mode) - - .. method:: __getitem__(self, xy): - - Returns the pixel at x,y. The pixel is returned as a single - value for single band images or a tuple for multiple band - images - - :param xy: The pixel coordinate, given as (x, y). - :returns: a pixel value for single band images, a tuple of - pixel values for multiband images. - - .. method:: putpixel(self, xy, color): - - Modifies the pixel at x,y. The color is given as a single - numerical value for single band images, and a tuple for - multi-band images. In addition to this, RGB and RGBA tuples - are accepted for P and PA images. - - :param xy: The pixel coordinate, given as (x, y). - :param color: The pixel value according to its mode. e.g. tuple (r, g, b) for RGB mode) - - .. method:: getpixel(self, xy): - - Returns the pixel at x,y. The pixel is returned as a single - value for single band images or a tuple for multiple band - images - - :param xy: The pixel coordinate, given as (x, y). - :returns: a pixel value for single band images, a tuple of - pixel values for multiband images. +.. _PixelAccess: + +:py:class:`PixelAccess` Class +============================= + +The PixelAccess class provides read and write access to +:py:class:`PIL.Image` data at a pixel level. + +.. note:: Accessing individual pixels is fairly slow. If you are + looping over all of the pixels in an image, there is likely + a faster way using other parts of the Pillow API. + + :mod:`~PIL.Image`, :mod:`~PIL.ImageChops` and :mod:`~PIL.ImageOps` + have methods for many standard operations. If you wish to perform + a custom mapping, check out :py:meth:`~PIL.Image.Image.point`. + +Example +------- + +The following script loads an image, accesses one pixel from it, then +changes it. :: + + from PIL import Image + + with Image.open("hopper.jpg") as im: + px = im.load() + print(px[4, 4]) + px[4, 4] = (0, 0, 0) + print(px[4, 4]) + +Results in the following:: + + (23, 24, 68) + (0, 0, 0) + +Access using negative indexes is also possible. :: + + px[-1, -1] = (0, 0, 0) + print(px[-1, -1]) + + + +:py:class:`PixelAccess` Class +----------------------------- + +.. class:: PixelAccess + + .. method:: __setitem__(self, xy, color): + + Modifies the pixel at x,y. The color is given as a single + numerical value for single band images, and a tuple for + multi-band images + + :param xy: The pixel coordinate, given as (x, y). + :param color: The pixel value according to its mode. e.g. tuple (r, g, b) for RGB mode) + + .. method:: __getitem__(self, xy): + + Returns the pixel at x,y. The pixel is returned as a single + value for single band images or a tuple for multiple band + images + + :param xy: The pixel coordinate, given as (x, y). + :returns: a pixel value for single band images, a tuple of + pixel values for multiband images. + + .. method:: putpixel(self, xy, color): + + Modifies the pixel at x,y. The color is given as a single + numerical value for single band images, and a tuple for + multi-band images. In addition to this, RGB and RGBA tuples + are accepted for P and PA images. + + :param xy: The pixel coordinate, given as (x, y). + :param color: The pixel value according to its mode. e.g. tuple (r, g, b) for RGB mode) + + .. method:: getpixel(self, xy): + + Returns the pixel at x,y. The pixel is returned as a single + value for single band images or a tuple for multiple band + images + + :param xy: The pixel coordinate, given as (x, y). + :returns: a pixel value for single band images, a tuple of + pixel values for multiband images. diff --git a/docs/reference/PyAccess.rst b/docs/reference/PyAccess.rst index ed58ca3a5..cdd93e996 100644 --- a/docs/reference/PyAccess.rst +++ b/docs/reference/PyAccess.rst @@ -1,46 +1,46 @@ -.. py:module:: PIL.PyAccess -.. py:currentmodule:: PIL.PyAccess - -:py:mod:`~PIL.PyAccess` Module -============================== - -The :py:mod:`~PIL.PyAccess` module provides a CFFI/Python implementation of the :ref:`PixelAccess`. This implementation is far faster on PyPy than the PixelAccess version. - -.. note:: Accessing individual pixels is fairly slow. If you are - looping over all of the pixels in an image, there is likely - a faster way using other parts of the Pillow API. - - :mod:`~PIL.Image`, :mod:`~PIL.ImageChops` and :mod:`~PIL.ImageOps` - have methods for many standard operations. If you wish to perform - a custom mapping, check out :py:meth:`~PIL.Image.Image.point`. - -Example -------- - -The following script loads an image, accesses one pixel from it, then changes it. :: - - from PIL import Image - - with Image.open("hopper.jpg") as im: - px = im.load() - print(px[4, 4]) - px[4, 4] = (0, 0, 0) - print(px[4, 4]) - -Results in the following:: - - (23, 24, 68) - (0, 0, 0) - -Access using negative indexes is also possible. :: - - px[-1, -1] = (0, 0, 0) - print(px[-1, -1]) - - - -:py:class:`PyAccess` Class --------------------------- - -.. autoclass:: PIL.PyAccess.PyAccess() - :members: +.. py:module:: PIL.PyAccess +.. py:currentmodule:: PIL.PyAccess + +:py:mod:`~PIL.PyAccess` Module +============================== + +The :py:mod:`~PIL.PyAccess` module provides a CFFI/Python implementation of the :ref:`PixelAccess`. This implementation is far faster on PyPy than the PixelAccess version. + +.. note:: Accessing individual pixels is fairly slow. If you are + looping over all of the pixels in an image, there is likely + a faster way using other parts of the Pillow API. + + :mod:`~PIL.Image`, :mod:`~PIL.ImageChops` and :mod:`~PIL.ImageOps` + have methods for many standard operations. If you wish to perform + a custom mapping, check out :py:meth:`~PIL.Image.Image.point`. + +Example +------- + +The following script loads an image, accesses one pixel from it, then changes it. :: + + from PIL import Image + + with Image.open("hopper.jpg") as im: + px = im.load() + print(px[4, 4]) + px[4, 4] = (0, 0, 0) + print(px[4, 4]) + +Results in the following:: + + (23, 24, 68) + (0, 0, 0) + +Access using negative indexes is also possible. :: + + px[-1, -1] = (0, 0, 0) + print(px[-1, -1]) + + + +:py:class:`PyAccess` Class +-------------------------- + +.. autoclass:: PIL.PyAccess.PyAccess() + :members: diff --git a/docs/reference/TiffTags.rst b/docs/reference/TiffTags.rst index 7cb7d16ae..6794aab19 100644 --- a/docs/reference/TiffTags.rst +++ b/docs/reference/TiffTags.rst @@ -1,81 +1,81 @@ -.. py:module:: PIL.TiffTags -.. py:currentmodule:: PIL.TiffTags - -:py:mod:`~PIL.TiffTags` Module -============================== - -The :py:mod:`~PIL.TiffTags` module exposes many of the standard TIFF -metadata tag numbers, names, and type information. - -.. method:: lookup(tag) - - :param tag: Integer tag number - :param group: Which :py:data:`~PIL.TiffTags.TAGS_V2_GROUPS` to look in - - .. versionadded:: 8.3.0 - - :returns: Taginfo namedtuple, From the :py:data:`~PIL.TiffTags.TAGS_V2` info if possible, - otherwise just populating the value and name from :py:data:`~PIL.TiffTags.TAGS`. - If the tag is not recognized, "unknown" is returned for the name - -.. versionadded:: 3.1.0 - -.. class:: TagInfo - - .. method:: __init__(self, value=None, name="unknown", type=None, length=0, enum=None) - - :param value: Integer Tag Number - :param name: Tag Name - :param type: Integer type from :py:data:`PIL.TiffTags.TYPES` - :param length: Array length: 0 == variable, 1 == single value, n = fixed - :param enum: Dict of name:integer value options for an enumeration - - .. method:: cvt_enum(self, value) - - :param value: The enumerated value name - :returns: The integer corresponding to the name. - -.. versionadded:: 3.0.0 - -.. py:data:: PIL.TiffTags.TAGS_V2 - :type: dict - - The ``TAGS_V2`` dictionary maps 16-bit integer tag numbers to - :py:class:`PIL.TiffTags.TagInfo` tuples for metadata fields defined in the TIFF - spec. - -.. versionadded:: 3.0.0 - -.. py:data:: PIL.TiffTags.TAGS_V2_GROUPS - :type: dict - - :py:data:`~PIL.TiffTags.TAGS_V2` is one dimensional and - doesn't account for the fact that tags actually exist in - `different groups `_. - This dictionary is used when the tag in question is part of a group. - -.. versionadded:: 8.3.0 - -.. py:data:: PIL.TiffTags.TAGS - :type: dict - - The ``TAGS`` dictionary maps 16-bit integer TIFF tag number to - descriptive string names. For instance: - - >>> from PIL.TiffTags import TAGS - >>> TAGS[0x010e] - 'ImageDescription' - - This dictionary contains a superset of the tags in :py:data:`~PIL.TiffTags.TAGS_V2`, common - EXIF tags, and other well known metadata tags. - -.. py:data:: PIL.TiffTags.TYPES - :type: dict - - The ``TYPES`` dictionary maps the TIFF type short integer to a - human readable type name. - -.. py:data:: PIL.TiffTags.LIBTIFF_CORE - :type: list - - A list of supported tag IDs when writing using LibTIFF. +.. py:module:: PIL.TiffTags +.. py:currentmodule:: PIL.TiffTags + +:py:mod:`~PIL.TiffTags` Module +============================== + +The :py:mod:`~PIL.TiffTags` module exposes many of the standard TIFF +metadata tag numbers, names, and type information. + +.. method:: lookup(tag) + + :param tag: Integer tag number + :param group: Which :py:data:`~PIL.TiffTags.TAGS_V2_GROUPS` to look in + + .. versionadded:: 8.3.0 + + :returns: Taginfo namedtuple, From the :py:data:`~PIL.TiffTags.TAGS_V2` info if possible, + otherwise just populating the value and name from :py:data:`~PIL.TiffTags.TAGS`. + If the tag is not recognized, "unknown" is returned for the name + +.. versionadded:: 3.1.0 + +.. class:: TagInfo + + .. method:: __init__(self, value=None, name="unknown", type=None, length=0, enum=None) + + :param value: Integer Tag Number + :param name: Tag Name + :param type: Integer type from :py:data:`PIL.TiffTags.TYPES` + :param length: Array length: 0 == variable, 1 == single value, n = fixed + :param enum: Dict of name:integer value options for an enumeration + + .. method:: cvt_enum(self, value) + + :param value: The enumerated value name + :returns: The integer corresponding to the name. + +.. versionadded:: 3.0.0 + +.. py:data:: PIL.TiffTags.TAGS_V2 + :type: dict + + The ``TAGS_V2`` dictionary maps 16-bit integer tag numbers to + :py:class:`PIL.TiffTags.TagInfo` tuples for metadata fields defined in the TIFF + spec. + +.. versionadded:: 3.0.0 + +.. py:data:: PIL.TiffTags.TAGS_V2_GROUPS + :type: dict + + :py:data:`~PIL.TiffTags.TAGS_V2` is one dimensional and + doesn't account for the fact that tags actually exist in + `different groups `_. + This dictionary is used when the tag in question is part of a group. + +.. versionadded:: 8.3.0 + +.. py:data:: PIL.TiffTags.TAGS + :type: dict + + The ``TAGS`` dictionary maps 16-bit integer TIFF tag number to + descriptive string names. For instance: + + >>> from PIL.TiffTags import TAGS + >>> TAGS[0x010e] + 'ImageDescription' + + This dictionary contains a superset of the tags in :py:data:`~PIL.TiffTags.TAGS_V2`, common + EXIF tags, and other well known metadata tags. + +.. py:data:: PIL.TiffTags.TYPES + :type: dict + + The ``TYPES`` dictionary maps the TIFF type short integer to a + human readable type name. + +.. py:data:: PIL.TiffTags.LIBTIFF_CORE + :type: list + + A list of supported tag IDs when writing using LibTIFF. diff --git a/docs/reference/block_allocator.rst b/docs/reference/block_allocator.rst index 1abe5280f..aca664449 100644 --- a/docs/reference/block_allocator.rst +++ b/docs/reference/block_allocator.rst @@ -1,47 +1,47 @@ -Block Allocator -=============== - -Previous Design ---------------- - -Historically there have been two image allocators in Pillow: -``ImagingAllocateBlock`` and ``ImagingAllocateArray``. The first works -for images smaller than 16MB of data and allocates one large chunk of -memory of ``im->linesize * im->ysize`` bytes. The second works for -large images and makes one allocation for each scan line of size -``im->linesize`` bytes. This makes for a very sharp transition -between one allocation and potentially thousands of small allocations, -leading to unpredictable performance penalties around the transition. - -New Design ----------- - -``ImagingAllocateArray`` now allocates space for images as a chain of -blocks with a maximum size of 16MB. If there is a memory allocation -error, it falls back to allocating a 4KB block, or at least one scan -line. This is now the default for all internal allocations. - -``ImagingAllocateBlock`` is now only used for those cases when we are -specifically requesting a single segment of memory for sharing with -other code. - -Memory Pools ------------- - -There is now a memory pool to contain a supply of recently freed -blocks, which can then be reused without going back to the OS for a -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_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. +Block Allocator +=============== + +Previous Design +--------------- + +Historically there have been two image allocators in Pillow: +``ImagingAllocateBlock`` and ``ImagingAllocateArray``. The first works +for images smaller than 16MB of data and allocates one large chunk of +memory of ``im->linesize * im->ysize`` bytes. The second works for +large images and makes one allocation for each scan line of size +``im->linesize`` bytes. This makes for a very sharp transition +between one allocation and potentially thousands of small allocations, +leading to unpredictable performance penalties around the transition. + +New Design +---------- + +``ImagingAllocateArray`` now allocates space for images as a chain of +blocks with a maximum size of 16MB. If there is a memory allocation +error, it falls back to allocating a 4KB block, or at least one scan +line. This is now the default for all internal allocations. + +``ImagingAllocateBlock`` is now only used for those cases when we are +specifically requesting a single segment of memory for sharing with +other code. + +Memory Pools +------------ + +There is now a memory pool to contain a supply of recently freed +blocks, which can then be reused without going back to the OS for a +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_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. diff --git a/docs/reference/c_extension_debugging.rst b/docs/reference/c_extension_debugging.rst index 5e8586905..8bd2479ed 100644 --- a/docs/reference/c_extension_debugging.rst +++ b/docs/reference/c_extension_debugging.rst @@ -1,464 +1,464 @@ -C Extension debugging on Linux, with gbd/valgrind. -================================================== - -Install the tools ------------------ - -You need some basics in addition to the basic tools to build -pillow. These are what's required on Ubuntu, YMMV for other -distributions. - -- ``python3-dbg`` package for the gdb extensions and python symbols -- ``gdb`` and ``valgrind`` -- Potentially debug symbols for libraries. On Ubuntu you can follow those - instructions to install the corresponding packages: `Debug Symbol Packages `_ - -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: - -:: - - cd /usr/share/gdb/auto-load/usr/bin - ln -s python3.8m-gdb.py python3.8d-gdb.py - -- In Ubuntu 18.04, it's actually including the path to the virtualenv - in the search for the ``python3.*-gdb.py`` file, but you can - helpfully put in the same directory as the binary. - -- I also find that history is really useful for gdb, so I added this to - my ``~/.gdbinit`` file: - -:: - - set history filename ~/.gdb_history - set history save on - -- If the python stack isn't working in gdb, then - ``set debug auto-load`` can also be helpful in ``.gdbinit``. - -- Make a virtualenv with the debug python and activate it, then install - whatever dependencies are required and build. You want to build with - the debug python so you get symbols for your extension. - -:: - - virtualenv -p python3.8-dbg ~/vpy38-dbg - source ~/vpy38-dbg/bin/activate - cd ~/Pillow && make install - -Test Case ---------- - -Take your test image, and make a really simple harness. - -:: - - from PIL import Image - - with Image.open(path) as im: - im.load() - -- Run this through valgrind, but note that python triggers some issues - on its own, so you're looking for items within the Pillow hierarchy - that don't look like they're solely in the python call chain. In this - example, the ones we're interested are after the warnings, and have - ``decode.c`` and ``TiffDecode.c`` in the call stack: - -:: - - (vpy38-dbg) ubuntu@primary:~/Home/tests$ valgrind python test_tiff.py - ==51890== Memcheck, a memory error detector - ==51890== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al. - ==51890== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info - ==51890== Command: python test_tiff.py - ==51890== - ==51890== Invalid read of size 4 - ==51890== at 0x472E3D: address_in_range (obmalloc.c:1401) - ==51890== by 0x472EEA: pymalloc_free (obmalloc.c:1677) - ==51890== by 0x474960: _PyObject_Free (obmalloc.c:1896) - ==51890== by 0x473BAC: _PyMem_DebugRawFree (obmalloc.c:2187) - ==51890== by 0x473BD4: _PyMem_DebugFree (obmalloc.c:2318) - ==51890== by 0x474C08: PyObject_Free (obmalloc.c:709) - ==51890== by 0x45DD60: dictresize (dictobject.c:1259) - ==51890== by 0x45DD76: insertion_resize (dictobject.c:1019) - ==51890== by 0x464F30: PyDict_SetDefault (dictobject.c:2924) - ==51890== by 0x4D03BE: PyUnicode_InternInPlace (unicodeobject.c:15289) - ==51890== by 0x4D0700: PyUnicode_InternFromString (unicodeobject.c:15322) - ==51890== by 0x64D2FC: descr_new (descrobject.c:857) - ==51890== Address 0x4c1b020 is 384 bytes inside a block of size 1,160 free'd - ==51890== at 0x483CA3F: free (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so) - ==51890== by 0x4735D3: _PyMem_RawFree (obmalloc.c:127) - ==51890== by 0x473BAC: _PyMem_DebugRawFree (obmalloc.c:2187) - ==51890== by 0x474941: PyMem_RawFree (obmalloc.c:595) - ==51890== by 0x47496E: _PyObject_Free (obmalloc.c:1898) - ==51890== by 0x473BAC: _PyMem_DebugRawFree (obmalloc.c:2187) - ==51890== by 0x473BD4: _PyMem_DebugFree (obmalloc.c:2318) - ==51890== by 0x474C08: PyObject_Free (obmalloc.c:709) - ==51890== by 0x45DD60: dictresize (dictobject.c:1259) - ==51890== by 0x45DD76: insertion_resize (dictobject.c:1019) - ==51890== by 0x464F30: PyDict_SetDefault (dictobject.c:2924) - ==51890== by 0x4D03BE: PyUnicode_InternInPlace (unicodeobject.c:15289) - ==51890== Block was alloc'd at - ==51890== at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so) - ==51890== by 0x473646: _PyMem_RawMalloc (obmalloc.c:99) - ==51890== by 0x473529: _PyMem_DebugRawAlloc (obmalloc.c:2120) - ==51890== by 0x473565: _PyMem_DebugRawMalloc (obmalloc.c:2153) - ==51890== by 0x4748B1: PyMem_RawMalloc (obmalloc.c:572) - ==51890== by 0x475909: _PyObject_Malloc (obmalloc.c:1628) - ==51890== by 0x473529: _PyMem_DebugRawAlloc (obmalloc.c:2120) - ==51890== by 0x473565: _PyMem_DebugRawMalloc (obmalloc.c:2153) - ==51890== by 0x4736B0: _PyMem_DebugMalloc (obmalloc.c:2303) - ==51890== by 0x474B78: PyObject_Malloc (obmalloc.c:685) - ==51890== by 0x45C435: new_keys_object (dictobject.c:558) - ==51890== by 0x45DA95: dictresize (dictobject.c:1202) - ==51890== - ==51890== Invalid read of size 4 - ==51890== at 0x472E3D: address_in_range (obmalloc.c:1401) - ==51890== by 0x47594A: pymalloc_realloc (obmalloc.c:1929) - ==51890== by 0x475A02: _PyObject_Realloc (obmalloc.c:1982) - ==51890== by 0x473DCA: _PyMem_DebugRawRealloc (obmalloc.c:2240) - ==51890== by 0x473FF8: _PyMem_DebugRealloc (obmalloc.c:2326) - ==51890== by 0x4749FB: PyMem_Realloc (obmalloc.c:623) - ==51890== by 0x44A6FC: list_resize (listobject.c:70) - ==51890== by 0x44A872: app1 (listobject.c:340) - ==51890== by 0x44FD65: PyList_Append (listobject.c:352) - ==51890== by 0x514315: r_ref (marshal.c:945) - ==51890== by 0x516034: r_object (marshal.c:1139) - ==51890== by 0x516C70: r_object (marshal.c:1389) - ==51890== Address 0x4c41020 is 32 bytes before a block of size 1,600 in arena "client" - ==51890== - ==51890== Conditional jump or move depends on uninitialised value(s) - ==51890== at 0x472E46: address_in_range (obmalloc.c:1403) - ==51890== by 0x47594A: pymalloc_realloc (obmalloc.c:1929) - ==51890== by 0x475A02: _PyObject_Realloc (obmalloc.c:1982) - ==51890== by 0x473DCA: _PyMem_DebugRawRealloc (obmalloc.c:2240) - ==51890== by 0x473FF8: _PyMem_DebugRealloc (obmalloc.c:2326) - ==51890== by 0x4749FB: PyMem_Realloc (obmalloc.c:623) - ==51890== by 0x44A6FC: list_resize (listobject.c:70) - ==51890== by 0x44A872: app1 (listobject.c:340) - ==51890== by 0x44FD65: PyList_Append (listobject.c:352) - ==51890== by 0x5E3321: _posix_listdir (posixmodule.c:3823) - ==51890== by 0x5E33A8: os_listdir_impl (posixmodule.c:3879) - ==51890== by 0x5E4D77: os_listdir (posixmodule.c.h:1197) - ==51890== - ==51890== Use of uninitialised value of size 8 - ==51890== at 0x472E59: address_in_range (obmalloc.c:1403) - ==51890== by 0x47594A: pymalloc_realloc (obmalloc.c:1929) - ==51890== by 0x475A02: _PyObject_Realloc (obmalloc.c:1982) - ==51890== by 0x473DCA: _PyMem_DebugRawRealloc (obmalloc.c:2240) - ==51890== by 0x473FF8: _PyMem_DebugRealloc (obmalloc.c:2326) - ==51890== by 0x4749FB: PyMem_Realloc (obmalloc.c:623) - ==51890== by 0x44A6FC: list_resize (listobject.c:70) - ==51890== by 0x44A872: app1 (listobject.c:340) - ==51890== by 0x44FD65: PyList_Append (listobject.c:352) - ==51890== by 0x5E3321: _posix_listdir (posixmodule.c:3823) - ==51890== by 0x5E33A8: os_listdir_impl (posixmodule.c:3879) - ==51890== by 0x5E4D77: os_listdir (posixmodule.c.h:1197) - ==51890== - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 16908288 bytes but only got 0. Skipping tag 0 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 67895296 bytes but only got 0. Skipping tag 0 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 1572864 bytes but only got 0. Skipping tag 42 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 116647 bytes but only got 4867. Skipping tag 42738 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 3468830728 bytes but only got 4851. Skipping tag 279 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 2198732800 bytes but only got 0. Skipping tag 0 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 67239937 bytes but only got 4125. Skipping tag 0 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 33947764 bytes but only got 0. Skipping tag 139 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 17170432 bytes but only got 0. Skipping tag 0 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 80478208 bytes but only got 0. Skipping tag 1 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 787460 bytes but only got 4882. Skipping tag 20 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 1075 bytes but only got 0. Skipping tag 256 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 120586240 bytes but only got 0. Skipping tag 194 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 65536 bytes but only got 0. Skipping tag 3 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 198656 bytes but only got 0. Skipping tag 279 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 206848 bytes but only got 0. Skipping tag 64512 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 130968 bytes but only got 4882. Skipping tag 256 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 77848 bytes but only got 4689. Skipping tag 64270 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 262156 bytes but only got 0. Skipping tag 257 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 33624064 bytes but only got 0. Skipping tag 49152 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 67178752 bytes but only got 4627. Skipping tag 50688 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 33632768 bytes but only got 0. Skipping tag 56320 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 134386688 bytes but only got 4115. Skipping tag 2048 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 33912832 bytes but only got 0. Skipping tag 7168 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 151966208 bytes but only got 4627. Skipping tag 10240 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 119032832 bytes but only got 3859. Skipping tag 256 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 46535680 bytes but only got 0. Skipping tag 256 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 35651584 bytes but only got 0. Skipping tag 42 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 524288 bytes but only got 0. Skipping tag 0 - warnings.warn( - _TIFFVSetField: tempfile.tif: Null count for "Tag 769" (type 1, writecount -3, passcount 1). - _TIFFVSetField: tempfile.tif: Null count for "Tag 42754" (type 1, writecount -3, passcount 1). - _TIFFVSetField: tempfile.tif: Null count for "Tag 769" (type 1, writecount -3, passcount 1). - _TIFFVSetField: tempfile.tif: Null count for "Tag 42754" (type 1, writecount -3, passcount 1). - ZIPDecode: Decoding error at scanline 0, incorrect header check. - ==51890== Invalid write of size 4 - ==51890== at 0x61C39E6: putcontig8bitYCbCr22tile (tif_getimage.c:2146) - ==51890== by 0x61C5865: gtStripContig (tif_getimage.c:977) - ==51890== by 0x6094317: ReadStrip (TiffDecode.c:269) - ==51890== by 0x6094749: ImagingLibTiffDecode (TiffDecode.c:479) - ==51890== by 0x60615D1: _decode (decode.c:136) - ==51890== by 0x64BF47: method_vectorcall_VARARGS (descrobject.c:300) - ==51890== by 0x4EB73C: _PyObject_Vectorcall (abstract.h:127) - ==51890== by 0x4EB73C: call_function (ceval.c:4963) - ==51890== by 0x4EB73C: _PyEval_EvalFrameDefault (ceval.c:3486) - ==51890== by 0x4DF2EE: PyEval_EvalFrameEx (ceval.c:741) - ==51890== by 0x43627B: function_code_fastcall (call.c:283) - ==51890== by 0x436D21: _PyFunction_Vectorcall (call.c:410) - ==51890== by 0x4EB73C: _PyObject_Vectorcall (abstract.h:127) - ==51890== by 0x4EB73C: call_function (ceval.c:4963) - ==51890== by 0x4EB73C: _PyEval_EvalFrameDefault (ceval.c:3486) - ==51890== by 0x4DF2EE: PyEval_EvalFrameEx (ceval.c:741) - ==51890== Address 0x6f456d4 is 0 bytes after a block of size 68 alloc'd - ==51890== at 0x483DFAF: realloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so) - ==51890== by 0x60946D0: ImagingLibTiffDecode (TiffDecode.c:469) - ==51890== by 0x60615D1: _decode (decode.c:136) - ==51890== by 0x64BF47: method_vectorcall_VARARGS (descrobject.c:300) - ==51890== by 0x4EB73C: _PyObject_Vectorcall (abstract.h:127) - ==51890== by 0x4EB73C: call_function (ceval.c:4963) - ==51890== by 0x4EB73C: _PyEval_EvalFrameDefault (ceval.c:3486) - ==51890== by 0x4DF2EE: PyEval_EvalFrameEx (ceval.c:741) - ==51890== by 0x43627B: function_code_fastcall (call.c:283) - ==51890== by 0x436D21: _PyFunction_Vectorcall (call.c:410) - ==51890== by 0x4EB73C: _PyObject_Vectorcall (abstract.h:127) - ==51890== by 0x4EB73C: call_function (ceval.c:4963) - ==51890== by 0x4EB73C: _PyEval_EvalFrameDefault (ceval.c:3486) - ==51890== by 0x4DF2EE: PyEval_EvalFrameEx (ceval.c:741) - ==51890== by 0x4DFDFB: _PyEval_EvalCodeWithName (ceval.c:4298) - ==51890== by 0x436C40: _PyFunction_Vectorcall (call.c:435) - ==51890== - ==51890== Invalid write of size 4 - ==51890== at 0x61C39B5: putcontig8bitYCbCr22tile (tif_getimage.c:2145) - ==51890== by 0x61C5865: gtStripContig (tif_getimage.c:977) - ==51890== by 0x6094317: ReadStrip (TiffDecode.c:269) - ==51890== by 0x6094749: ImagingLibTiffDecode (TiffDecode.c:479) - ==51890== by 0x60615D1: _decode (decode.c:136) - ==51890== by 0x64BF47: method_vectorcall_VARARGS (descrobject.c:300) - ==51890== by 0x4EB73C: _PyObject_Vectorcall (abstract.h:127) - ==51890== by 0x4EB73C: call_function (ceval.c:4963) - ==51890== by 0x4EB73C: _PyEval_EvalFrameDefault (ceval.c:3486) - ==51890== by 0x4DF2EE: PyEval_EvalFrameEx (ceval.c:741) - ==51890== by 0x43627B: function_code_fastcall (call.c:283) - ==51890== by 0x436D21: _PyFunction_Vectorcall (call.c:410) - ==51890== by 0x4EB73C: _PyObject_Vectorcall (abstract.h:127) - ==51890== by 0x4EB73C: call_function (ceval.c:4963) - ==51890== by 0x4EB73C: _PyEval_EvalFrameDefault (ceval.c:3486) - ==51890== by 0x4DF2EE: PyEval_EvalFrameEx (ceval.c:741) - ==51890== Address 0x6f456d8 is 4 bytes after a block of size 68 alloc'd - ==51890== at 0x483DFAF: realloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so) - ==51890== by 0x60946D0: ImagingLibTiffDecode (TiffDecode.c:469) - ==51890== by 0x60615D1: _decode (decode.c:136) - ==51890== by 0x64BF47: method_vectorcall_VARARGS (descrobject.c:300) - ==51890== by 0x4EB73C: _PyObject_Vectorcall (abstract.h:127) - ==51890== by 0x4EB73C: call_function (ceval.c:4963) - ==51890== by 0x4EB73C: _PyEval_EvalFrameDefault (ceval.c:3486) - ==51890== by 0x4DF2EE: PyEval_EvalFrameEx (ceval.c:741) - ==51890== by 0x43627B: function_code_fastcall (call.c:283) - ==51890== by 0x436D21: _PyFunction_Vectorcall (call.c:410) - ==51890== by 0x4EB73C: _PyObject_Vectorcall (abstract.h:127) - ==51890== by 0x4EB73C: call_function (ceval.c:4963) - ==51890== by 0x4EB73C: _PyEval_EvalFrameDefault (ceval.c:3486) - ==51890== by 0x4DF2EE: PyEval_EvalFrameEx (ceval.c:741) - ==51890== by 0x4DFDFB: _PyEval_EvalCodeWithName (ceval.c:4298) - ==51890== by 0x436C40: _PyFunction_Vectorcall (call.c:435) - ==51890== - TIFFFillStrip: Invalid strip byte count 0, strip 1. - Traceback (most recent call last): - File "test_tiff.py", line 8, in - im.load() - File "/home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py", line 1087, in load - return self._load_libtiff() - File "/home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py", line 1191, in _load_libtiff - raise OSError(err) - OSError: -2 - sys:1: ResourceWarning: unclosed file <_io.BufferedReader name='crash-2020-10-test.tiff'> - ==51890== - ==51890== HEAP SUMMARY: - ==51890== in use at exit: 748,734 bytes in 444 blocks - ==51890== total heap usage: 6,320 allocs, 5,876 frees, 69,142,969 bytes allocated - ==51890== - ==51890== LEAK SUMMARY: - ==51890== definitely lost: 0 bytes in 0 blocks - ==51890== indirectly lost: 0 bytes in 0 blocks - ==51890== possibly lost: 721,538 bytes in 372 blocks - ==51890== still reachable: 27,196 bytes in 72 blocks - ==51890== suppressed: 0 bytes in 0 blocks - ==51890== Rerun with --leak-check=full to see details of leaked memory - ==51890== - ==51890== Use --track-origins=yes to see where uninitialised values come from - ==51890== For lists of detected and suppressed errors, rerun with: -s - ==51890== ERROR SUMMARY: 2556 errors from 6 contexts (suppressed: 0 from 0) - (vpy38-dbg) ubuntu@primary:~/Home/tests$ - -- Now that we've confirmed that there's something odd/bad going on, - it's time to gdb. -- Start with ``gdb python`` -- Set a break point starting with the valgrind stack trace. - ``b TiffDecode.c:269`` -- Run the script with ``r test_tiff.py`` -- When the break point is hit, explore the state with ``info locals``, - ``bt``, ``py-bt``, or ``p [variable]``. For pointers, - ``p *[variable]`` is useful. - -:: - - (vpy38-dbg) ubuntu@primary:~/Home/tests$ gdb python - GNU gdb (Ubuntu 9.2-0ubuntu1~20.04) 9.2 - Copyright (C) 2020 Free Software Foundation, Inc. - License GPLv3+: GNU GPL version 3 or later - This is free software: you are free to change and redistribute it. - There is NO WARRANTY, to the extent permitted by law. - Type "show copying" and "show warranty" for details. - This GDB was configured as "x86_64-linux-gnu". - Type "show configuration" for configuration details. - For bug reporting instructions, please see: - . - Find the GDB manual and other documentation resources online at: - . - - For help, type "help". - Type "apropos word" to search for commands related to "word"... - Reading symbols from python... - (gdb) b TiffDecode.c:269 - No source file named TiffDecode.c. - Make breakpoint pending on future shared library load? (y or [n]) y - Breakpoint 1 (TiffDecode.c:269) pending. - (gdb) r test_tiff.py - Starting program: /home/ubuntu/vpy38-dbg/bin/python test_tiff.py - [Thread debugging using libthread_db enabled] - Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 16908288 bytes but only got 0. Skipping tag 0 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 67895296 bytes but only got 0. Skipping tag 0 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 1572864 bytes but only got 0. Skipping tag 42 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 116647 bytes but only got 4867. Skipping tag 42738 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 3468830728 bytes but only got 4851. Skipping tag 279 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 2198732800 bytes but only got 0. Skipping tag 0 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 67239937 bytes but only got 4125. Skipping tag 0 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 33947764 bytes but only got 0. Skipping tag 139 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 17170432 bytes but only got 0. Skipping tag 0 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 80478208 bytes but only got 0. Skipping tag 1 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 787460 bytes but only got 4882. Skipping tag 20 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 1075 bytes but only got 0. Skipping tag 256 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 120586240 bytes but only got 0. Skipping tag 194 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 65536 bytes but only got 0. Skipping tag 3 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 198656 bytes but only got 0. Skipping tag 279 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 206848 bytes but only got 0. Skipping tag 64512 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 130968 bytes but only got 4882. Skipping tag 256 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 77848 bytes but only got 4689. Skipping tag 64270 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 262156 bytes but only got 0. Skipping tag 257 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 33624064 bytes but only got 0. Skipping tag 49152 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 67178752 bytes but only got 4627. Skipping tag 50688 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 33632768 bytes but only got 0. Skipping tag 56320 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 134386688 bytes but only got 4115. Skipping tag 2048 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 33912832 bytes but only got 0. Skipping tag 7168 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 151966208 bytes but only got 4627. Skipping tag 10240 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 119032832 bytes but only got 3859. Skipping tag 256 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 46535680 bytes but only got 0. Skipping tag 256 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 35651584 bytes but only got 0. Skipping tag 42 - warnings.warn( - /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 524288 bytes but only got 0. Skipping tag 0 - warnings.warn( - _TIFFVSetField: tempfile.tif: Null count for "Tag 769" (type 1, writecount -3, passcount 1). - _TIFFVSetField: tempfile.tif: Null count for "Tag 42754" (type 1, writecount -3, passcount 1). - _TIFFVSetField: tempfile.tif: Null count for "Tag 769" (type 1, writecount -3, passcount 1). - _TIFFVSetField: tempfile.tif: Null count for "Tag 42754" (type 1, writecount -3, passcount 1). - - Breakpoint 1, ReadStrip (tiff=tiff@entry=0xae9b90, row=0, buffer=0xac2eb0) at src/libImaging/TiffDecode.c:269 - 269 ok = TIFFRGBAImageGet(&img, buffer, img.width, rows_to_read); - (gdb) p img - $1 = {tif = 0xae9b90, stoponerr = 0, isContig = 1, alpha = 0, width = 20, height = 1536, bitspersample = 8, samplesperpixel = 3, - orientation = 1, req_orientation = 1, photometric = 6, redcmap = 0x0, greencmap = 0x0, bluecmap = 0x0, get = - 0x7ffff71d0710 , put = {any = 0x7ffff71ce550 , - contig = 0x7ffff71ce550 , separate = 0x7ffff71ce550 }, Map = 0x0, - BWmap = 0x0, PALmap = 0x0, ycbcr = 0xaf24b0, cielab = 0x0, UaToAa = 0x0, Bitdepth16To8 = 0x0, row_offset = 0, col_offset = 0} - (gdb) up - #1 0x00007ffff736174a in ImagingLibTiffDecode (im=0xac1f90, state=0x7ffff76767e0, buffer=, bytes=) - at src/libImaging/TiffDecode.c:479 - 479 if (ReadStrip(tiff, state->y, (UINT32 *)state->buffer) == -1) { - (gdb) p *state - $2 = {count = 0, state = 0, errcode = 0, x = 0, y = 0, ystep = 0, xsize = 17, ysize = 108, xoff = 0, yoff = 0, - shuffle = 0x7ffff735f411 , bits = 32, bytes = 68, buffer = 0xac2eb0 "P\354\336\367\377\177", context = 0xa75440, fd = 0x0} - (gdb) py-bt - Traceback (most recent call first): - File "/home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py", line 1428, in _load_libtiff - - File "/home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py", line 1087, in load - return self._load_libtiff() - File "test_tiff.py", line 8, in - im.load() - -- Poke around till you understand what's going on. In this case, - state->xsize and img.width are different, which led to an out of - bounds write, as the receiving buffer was sized for the smaller of - the two. - -Caveats -------- - -- If your program is running/hung in a docker container and your host - has the appropriate tools, you can run gdb as the superuser in the - host and you may be able to get a trace of where the process is hung. - You probably won't have the capability to do that from within the - docker container, as the trace capacity isn't allowed by default. - -- Variations of this are possible on the mac/windows, but the details - are going to be different. - -- IIRC, Fedora has the gdb bits working by default. Ubuntu has always - been a bit of a battle to make it work. +C Extension debugging on Linux, with gbd/valgrind. +================================================== + +Install the tools +----------------- + +You need some basics in addition to the basic tools to build +pillow. These are what's required on Ubuntu, YMMV for other +distributions. + +- ``python3-dbg`` package for the gdb extensions and python symbols +- ``gdb`` and ``valgrind`` +- Potentially debug symbols for libraries. On Ubuntu you can follow those + instructions to install the corresponding packages: `Debug Symbol Packages `_ + +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: + +:: + + cd /usr/share/gdb/auto-load/usr/bin + ln -s python3.8m-gdb.py python3.8d-gdb.py + +- In Ubuntu 18.04, it's actually including the path to the virtualenv + in the search for the ``python3.*-gdb.py`` file, but you can + helpfully put in the same directory as the binary. + +- I also find that history is really useful for gdb, so I added this to + my ``~/.gdbinit`` file: + +:: + + set history filename ~/.gdb_history + set history save on + +- If the python stack isn't working in gdb, then + ``set debug auto-load`` can also be helpful in ``.gdbinit``. + +- Make a virtualenv with the debug python and activate it, then install + whatever dependencies are required and build. You want to build with + the debug python so you get symbols for your extension. + +:: + + virtualenv -p python3.8-dbg ~/vpy38-dbg + source ~/vpy38-dbg/bin/activate + cd ~/Pillow && make install + +Test Case +--------- + +Take your test image, and make a really simple harness. + +:: + + from PIL import Image + + with Image.open(path) as im: + im.load() + +- Run this through valgrind, but note that python triggers some issues + on its own, so you're looking for items within the Pillow hierarchy + that don't look like they're solely in the python call chain. In this + example, the ones we're interested are after the warnings, and have + ``decode.c`` and ``TiffDecode.c`` in the call stack: + +:: + + (vpy38-dbg) ubuntu@primary:~/Home/tests$ valgrind python test_tiff.py + ==51890== Memcheck, a memory error detector + ==51890== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al. + ==51890== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info + ==51890== Command: python test_tiff.py + ==51890== + ==51890== Invalid read of size 4 + ==51890== at 0x472E3D: address_in_range (obmalloc.c:1401) + ==51890== by 0x472EEA: pymalloc_free (obmalloc.c:1677) + ==51890== by 0x474960: _PyObject_Free (obmalloc.c:1896) + ==51890== by 0x473BAC: _PyMem_DebugRawFree (obmalloc.c:2187) + ==51890== by 0x473BD4: _PyMem_DebugFree (obmalloc.c:2318) + ==51890== by 0x474C08: PyObject_Free (obmalloc.c:709) + ==51890== by 0x45DD60: dictresize (dictobject.c:1259) + ==51890== by 0x45DD76: insertion_resize (dictobject.c:1019) + ==51890== by 0x464F30: PyDict_SetDefault (dictobject.c:2924) + ==51890== by 0x4D03BE: PyUnicode_InternInPlace (unicodeobject.c:15289) + ==51890== by 0x4D0700: PyUnicode_InternFromString (unicodeobject.c:15322) + ==51890== by 0x64D2FC: descr_new (descrobject.c:857) + ==51890== Address 0x4c1b020 is 384 bytes inside a block of size 1,160 free'd + ==51890== at 0x483CA3F: free (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so) + ==51890== by 0x4735D3: _PyMem_RawFree (obmalloc.c:127) + ==51890== by 0x473BAC: _PyMem_DebugRawFree (obmalloc.c:2187) + ==51890== by 0x474941: PyMem_RawFree (obmalloc.c:595) + ==51890== by 0x47496E: _PyObject_Free (obmalloc.c:1898) + ==51890== by 0x473BAC: _PyMem_DebugRawFree (obmalloc.c:2187) + ==51890== by 0x473BD4: _PyMem_DebugFree (obmalloc.c:2318) + ==51890== by 0x474C08: PyObject_Free (obmalloc.c:709) + ==51890== by 0x45DD60: dictresize (dictobject.c:1259) + ==51890== by 0x45DD76: insertion_resize (dictobject.c:1019) + ==51890== by 0x464F30: PyDict_SetDefault (dictobject.c:2924) + ==51890== by 0x4D03BE: PyUnicode_InternInPlace (unicodeobject.c:15289) + ==51890== Block was alloc'd at + ==51890== at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so) + ==51890== by 0x473646: _PyMem_RawMalloc (obmalloc.c:99) + ==51890== by 0x473529: _PyMem_DebugRawAlloc (obmalloc.c:2120) + ==51890== by 0x473565: _PyMem_DebugRawMalloc (obmalloc.c:2153) + ==51890== by 0x4748B1: PyMem_RawMalloc (obmalloc.c:572) + ==51890== by 0x475909: _PyObject_Malloc (obmalloc.c:1628) + ==51890== by 0x473529: _PyMem_DebugRawAlloc (obmalloc.c:2120) + ==51890== by 0x473565: _PyMem_DebugRawMalloc (obmalloc.c:2153) + ==51890== by 0x4736B0: _PyMem_DebugMalloc (obmalloc.c:2303) + ==51890== by 0x474B78: PyObject_Malloc (obmalloc.c:685) + ==51890== by 0x45C435: new_keys_object (dictobject.c:558) + ==51890== by 0x45DA95: dictresize (dictobject.c:1202) + ==51890== + ==51890== Invalid read of size 4 + ==51890== at 0x472E3D: address_in_range (obmalloc.c:1401) + ==51890== by 0x47594A: pymalloc_realloc (obmalloc.c:1929) + ==51890== by 0x475A02: _PyObject_Realloc (obmalloc.c:1982) + ==51890== by 0x473DCA: _PyMem_DebugRawRealloc (obmalloc.c:2240) + ==51890== by 0x473FF8: _PyMem_DebugRealloc (obmalloc.c:2326) + ==51890== by 0x4749FB: PyMem_Realloc (obmalloc.c:623) + ==51890== by 0x44A6FC: list_resize (listobject.c:70) + ==51890== by 0x44A872: app1 (listobject.c:340) + ==51890== by 0x44FD65: PyList_Append (listobject.c:352) + ==51890== by 0x514315: r_ref (marshal.c:945) + ==51890== by 0x516034: r_object (marshal.c:1139) + ==51890== by 0x516C70: r_object (marshal.c:1389) + ==51890== Address 0x4c41020 is 32 bytes before a block of size 1,600 in arena "client" + ==51890== + ==51890== Conditional jump or move depends on uninitialised value(s) + ==51890== at 0x472E46: address_in_range (obmalloc.c:1403) + ==51890== by 0x47594A: pymalloc_realloc (obmalloc.c:1929) + ==51890== by 0x475A02: _PyObject_Realloc (obmalloc.c:1982) + ==51890== by 0x473DCA: _PyMem_DebugRawRealloc (obmalloc.c:2240) + ==51890== by 0x473FF8: _PyMem_DebugRealloc (obmalloc.c:2326) + ==51890== by 0x4749FB: PyMem_Realloc (obmalloc.c:623) + ==51890== by 0x44A6FC: list_resize (listobject.c:70) + ==51890== by 0x44A872: app1 (listobject.c:340) + ==51890== by 0x44FD65: PyList_Append (listobject.c:352) + ==51890== by 0x5E3321: _posix_listdir (posixmodule.c:3823) + ==51890== by 0x5E33A8: os_listdir_impl (posixmodule.c:3879) + ==51890== by 0x5E4D77: os_listdir (posixmodule.c.h:1197) + ==51890== + ==51890== Use of uninitialised value of size 8 + ==51890== at 0x472E59: address_in_range (obmalloc.c:1403) + ==51890== by 0x47594A: pymalloc_realloc (obmalloc.c:1929) + ==51890== by 0x475A02: _PyObject_Realloc (obmalloc.c:1982) + ==51890== by 0x473DCA: _PyMem_DebugRawRealloc (obmalloc.c:2240) + ==51890== by 0x473FF8: _PyMem_DebugRealloc (obmalloc.c:2326) + ==51890== by 0x4749FB: PyMem_Realloc (obmalloc.c:623) + ==51890== by 0x44A6FC: list_resize (listobject.c:70) + ==51890== by 0x44A872: app1 (listobject.c:340) + ==51890== by 0x44FD65: PyList_Append (listobject.c:352) + ==51890== by 0x5E3321: _posix_listdir (posixmodule.c:3823) + ==51890== by 0x5E33A8: os_listdir_impl (posixmodule.c:3879) + ==51890== by 0x5E4D77: os_listdir (posixmodule.c.h:1197) + ==51890== + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 16908288 bytes but only got 0. Skipping tag 0 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 67895296 bytes but only got 0. Skipping tag 0 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 1572864 bytes but only got 0. Skipping tag 42 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 116647 bytes but only got 4867. Skipping tag 42738 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 3468830728 bytes but only got 4851. Skipping tag 279 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 2198732800 bytes but only got 0. Skipping tag 0 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 67239937 bytes but only got 4125. Skipping tag 0 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 33947764 bytes but only got 0. Skipping tag 139 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 17170432 bytes but only got 0. Skipping tag 0 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 80478208 bytes but only got 0. Skipping tag 1 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 787460 bytes but only got 4882. Skipping tag 20 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 1075 bytes but only got 0. Skipping tag 256 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 120586240 bytes but only got 0. Skipping tag 194 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 65536 bytes but only got 0. Skipping tag 3 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 198656 bytes but only got 0. Skipping tag 279 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 206848 bytes but only got 0. Skipping tag 64512 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 130968 bytes but only got 4882. Skipping tag 256 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 77848 bytes but only got 4689. Skipping tag 64270 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 262156 bytes but only got 0. Skipping tag 257 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 33624064 bytes but only got 0. Skipping tag 49152 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 67178752 bytes but only got 4627. Skipping tag 50688 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 33632768 bytes but only got 0. Skipping tag 56320 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 134386688 bytes but only got 4115. Skipping tag 2048 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 33912832 bytes but only got 0. Skipping tag 7168 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 151966208 bytes but only got 4627. Skipping tag 10240 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 119032832 bytes but only got 3859. Skipping tag 256 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 46535680 bytes but only got 0. Skipping tag 256 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 35651584 bytes but only got 0. Skipping tag 42 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 524288 bytes but only got 0. Skipping tag 0 + warnings.warn( + _TIFFVSetField: tempfile.tif: Null count for "Tag 769" (type 1, writecount -3, passcount 1). + _TIFFVSetField: tempfile.tif: Null count for "Tag 42754" (type 1, writecount -3, passcount 1). + _TIFFVSetField: tempfile.tif: Null count for "Tag 769" (type 1, writecount -3, passcount 1). + _TIFFVSetField: tempfile.tif: Null count for "Tag 42754" (type 1, writecount -3, passcount 1). + ZIPDecode: Decoding error at scanline 0, incorrect header check. + ==51890== Invalid write of size 4 + ==51890== at 0x61C39E6: putcontig8bitYCbCr22tile (tif_getimage.c:2146) + ==51890== by 0x61C5865: gtStripContig (tif_getimage.c:977) + ==51890== by 0x6094317: ReadStrip (TiffDecode.c:269) + ==51890== by 0x6094749: ImagingLibTiffDecode (TiffDecode.c:479) + ==51890== by 0x60615D1: _decode (decode.c:136) + ==51890== by 0x64BF47: method_vectorcall_VARARGS (descrobject.c:300) + ==51890== by 0x4EB73C: _PyObject_Vectorcall (abstract.h:127) + ==51890== by 0x4EB73C: call_function (ceval.c:4963) + ==51890== by 0x4EB73C: _PyEval_EvalFrameDefault (ceval.c:3486) + ==51890== by 0x4DF2EE: PyEval_EvalFrameEx (ceval.c:741) + ==51890== by 0x43627B: function_code_fastcall (call.c:283) + ==51890== by 0x436D21: _PyFunction_Vectorcall (call.c:410) + ==51890== by 0x4EB73C: _PyObject_Vectorcall (abstract.h:127) + ==51890== by 0x4EB73C: call_function (ceval.c:4963) + ==51890== by 0x4EB73C: _PyEval_EvalFrameDefault (ceval.c:3486) + ==51890== by 0x4DF2EE: PyEval_EvalFrameEx (ceval.c:741) + ==51890== Address 0x6f456d4 is 0 bytes after a block of size 68 alloc'd + ==51890== at 0x483DFAF: realloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so) + ==51890== by 0x60946D0: ImagingLibTiffDecode (TiffDecode.c:469) + ==51890== by 0x60615D1: _decode (decode.c:136) + ==51890== by 0x64BF47: method_vectorcall_VARARGS (descrobject.c:300) + ==51890== by 0x4EB73C: _PyObject_Vectorcall (abstract.h:127) + ==51890== by 0x4EB73C: call_function (ceval.c:4963) + ==51890== by 0x4EB73C: _PyEval_EvalFrameDefault (ceval.c:3486) + ==51890== by 0x4DF2EE: PyEval_EvalFrameEx (ceval.c:741) + ==51890== by 0x43627B: function_code_fastcall (call.c:283) + ==51890== by 0x436D21: _PyFunction_Vectorcall (call.c:410) + ==51890== by 0x4EB73C: _PyObject_Vectorcall (abstract.h:127) + ==51890== by 0x4EB73C: call_function (ceval.c:4963) + ==51890== by 0x4EB73C: _PyEval_EvalFrameDefault (ceval.c:3486) + ==51890== by 0x4DF2EE: PyEval_EvalFrameEx (ceval.c:741) + ==51890== by 0x4DFDFB: _PyEval_EvalCodeWithName (ceval.c:4298) + ==51890== by 0x436C40: _PyFunction_Vectorcall (call.c:435) + ==51890== + ==51890== Invalid write of size 4 + ==51890== at 0x61C39B5: putcontig8bitYCbCr22tile (tif_getimage.c:2145) + ==51890== by 0x61C5865: gtStripContig (tif_getimage.c:977) + ==51890== by 0x6094317: ReadStrip (TiffDecode.c:269) + ==51890== by 0x6094749: ImagingLibTiffDecode (TiffDecode.c:479) + ==51890== by 0x60615D1: _decode (decode.c:136) + ==51890== by 0x64BF47: method_vectorcall_VARARGS (descrobject.c:300) + ==51890== by 0x4EB73C: _PyObject_Vectorcall (abstract.h:127) + ==51890== by 0x4EB73C: call_function (ceval.c:4963) + ==51890== by 0x4EB73C: _PyEval_EvalFrameDefault (ceval.c:3486) + ==51890== by 0x4DF2EE: PyEval_EvalFrameEx (ceval.c:741) + ==51890== by 0x43627B: function_code_fastcall (call.c:283) + ==51890== by 0x436D21: _PyFunction_Vectorcall (call.c:410) + ==51890== by 0x4EB73C: _PyObject_Vectorcall (abstract.h:127) + ==51890== by 0x4EB73C: call_function (ceval.c:4963) + ==51890== by 0x4EB73C: _PyEval_EvalFrameDefault (ceval.c:3486) + ==51890== by 0x4DF2EE: PyEval_EvalFrameEx (ceval.c:741) + ==51890== Address 0x6f456d8 is 4 bytes after a block of size 68 alloc'd + ==51890== at 0x483DFAF: realloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so) + ==51890== by 0x60946D0: ImagingLibTiffDecode (TiffDecode.c:469) + ==51890== by 0x60615D1: _decode (decode.c:136) + ==51890== by 0x64BF47: method_vectorcall_VARARGS (descrobject.c:300) + ==51890== by 0x4EB73C: _PyObject_Vectorcall (abstract.h:127) + ==51890== by 0x4EB73C: call_function (ceval.c:4963) + ==51890== by 0x4EB73C: _PyEval_EvalFrameDefault (ceval.c:3486) + ==51890== by 0x4DF2EE: PyEval_EvalFrameEx (ceval.c:741) + ==51890== by 0x43627B: function_code_fastcall (call.c:283) + ==51890== by 0x436D21: _PyFunction_Vectorcall (call.c:410) + ==51890== by 0x4EB73C: _PyObject_Vectorcall (abstract.h:127) + ==51890== by 0x4EB73C: call_function (ceval.c:4963) + ==51890== by 0x4EB73C: _PyEval_EvalFrameDefault (ceval.c:3486) + ==51890== by 0x4DF2EE: PyEval_EvalFrameEx (ceval.c:741) + ==51890== by 0x4DFDFB: _PyEval_EvalCodeWithName (ceval.c:4298) + ==51890== by 0x436C40: _PyFunction_Vectorcall (call.c:435) + ==51890== + TIFFFillStrip: Invalid strip byte count 0, strip 1. + Traceback (most recent call last): + File "test_tiff.py", line 8, in + im.load() + File "/home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py", line 1087, in load + return self._load_libtiff() + File "/home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py", line 1191, in _load_libtiff + raise OSError(err) + OSError: -2 + sys:1: ResourceWarning: unclosed file <_io.BufferedReader name='crash-2020-10-test.tiff'> + ==51890== + ==51890== HEAP SUMMARY: + ==51890== in use at exit: 748,734 bytes in 444 blocks + ==51890== total heap usage: 6,320 allocs, 5,876 frees, 69,142,969 bytes allocated + ==51890== + ==51890== LEAK SUMMARY: + ==51890== definitely lost: 0 bytes in 0 blocks + ==51890== indirectly lost: 0 bytes in 0 blocks + ==51890== possibly lost: 721,538 bytes in 372 blocks + ==51890== still reachable: 27,196 bytes in 72 blocks + ==51890== suppressed: 0 bytes in 0 blocks + ==51890== Rerun with --leak-check=full to see details of leaked memory + ==51890== + ==51890== Use --track-origins=yes to see where uninitialised values come from + ==51890== For lists of detected and suppressed errors, rerun with: -s + ==51890== ERROR SUMMARY: 2556 errors from 6 contexts (suppressed: 0 from 0) + (vpy38-dbg) ubuntu@primary:~/Home/tests$ + +- Now that we've confirmed that there's something odd/bad going on, + it's time to gdb. +- Start with ``gdb python`` +- Set a break point starting with the valgrind stack trace. + ``b TiffDecode.c:269`` +- Run the script with ``r test_tiff.py`` +- When the break point is hit, explore the state with ``info locals``, + ``bt``, ``py-bt``, or ``p [variable]``. For pointers, + ``p *[variable]`` is useful. + +:: + + (vpy38-dbg) ubuntu@primary:~/Home/tests$ gdb python + GNU gdb (Ubuntu 9.2-0ubuntu1~20.04) 9.2 + Copyright (C) 2020 Free Software Foundation, Inc. + License GPLv3+: GNU GPL version 3 or later + This is free software: you are free to change and redistribute it. + There is NO WARRANTY, to the extent permitted by law. + Type "show copying" and "show warranty" for details. + This GDB was configured as "x86_64-linux-gnu". + Type "show configuration" for configuration details. + For bug reporting instructions, please see: + . + Find the GDB manual and other documentation resources online at: + . + + For help, type "help". + Type "apropos word" to search for commands related to "word"... + Reading symbols from python... + (gdb) b TiffDecode.c:269 + No source file named TiffDecode.c. + Make breakpoint pending on future shared library load? (y or [n]) y + Breakpoint 1 (TiffDecode.c:269) pending. + (gdb) r test_tiff.py + Starting program: /home/ubuntu/vpy38-dbg/bin/python test_tiff.py + [Thread debugging using libthread_db enabled] + Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 16908288 bytes but only got 0. Skipping tag 0 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 67895296 bytes but only got 0. Skipping tag 0 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 1572864 bytes but only got 0. Skipping tag 42 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 116647 bytes but only got 4867. Skipping tag 42738 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 3468830728 bytes but only got 4851. Skipping tag 279 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 2198732800 bytes but only got 0. Skipping tag 0 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 67239937 bytes but only got 4125. Skipping tag 0 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 33947764 bytes but only got 0. Skipping tag 139 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 17170432 bytes but only got 0. Skipping tag 0 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 80478208 bytes but only got 0. Skipping tag 1 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 787460 bytes but only got 4882. Skipping tag 20 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 1075 bytes but only got 0. Skipping tag 256 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 120586240 bytes but only got 0. Skipping tag 194 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 65536 bytes but only got 0. Skipping tag 3 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 198656 bytes but only got 0. Skipping tag 279 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 206848 bytes but only got 0. Skipping tag 64512 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 130968 bytes but only got 4882. Skipping tag 256 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 77848 bytes but only got 4689. Skipping tag 64270 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 262156 bytes but only got 0. Skipping tag 257 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 33624064 bytes but only got 0. Skipping tag 49152 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 67178752 bytes but only got 4627. Skipping tag 50688 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 33632768 bytes but only got 0. Skipping tag 56320 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 134386688 bytes but only got 4115. Skipping tag 2048 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 33912832 bytes but only got 0. Skipping tag 7168 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 151966208 bytes but only got 4627. Skipping tag 10240 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 119032832 bytes but only got 3859. Skipping tag 256 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 46535680 bytes but only got 0. Skipping tag 256 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 35651584 bytes but only got 0. Skipping tag 42 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 524288 bytes but only got 0. Skipping tag 0 + warnings.warn( + _TIFFVSetField: tempfile.tif: Null count for "Tag 769" (type 1, writecount -3, passcount 1). + _TIFFVSetField: tempfile.tif: Null count for "Tag 42754" (type 1, writecount -3, passcount 1). + _TIFFVSetField: tempfile.tif: Null count for "Tag 769" (type 1, writecount -3, passcount 1). + _TIFFVSetField: tempfile.tif: Null count for "Tag 42754" (type 1, writecount -3, passcount 1). + + Breakpoint 1, ReadStrip (tiff=tiff@entry=0xae9b90, row=0, buffer=0xac2eb0) at src/libImaging/TiffDecode.c:269 + 269 ok = TIFFRGBAImageGet(&img, buffer, img.width, rows_to_read); + (gdb) p img + $1 = {tif = 0xae9b90, stoponerr = 0, isContig = 1, alpha = 0, width = 20, height = 1536, bitspersample = 8, samplesperpixel = 3, + orientation = 1, req_orientation = 1, photometric = 6, redcmap = 0x0, greencmap = 0x0, bluecmap = 0x0, get = + 0x7ffff71d0710 , put = {any = 0x7ffff71ce550 , + contig = 0x7ffff71ce550 , separate = 0x7ffff71ce550 }, Map = 0x0, + BWmap = 0x0, PALmap = 0x0, ycbcr = 0xaf24b0, cielab = 0x0, UaToAa = 0x0, Bitdepth16To8 = 0x0, row_offset = 0, col_offset = 0} + (gdb) up + #1 0x00007ffff736174a in ImagingLibTiffDecode (im=0xac1f90, state=0x7ffff76767e0, buffer=, bytes=) + at src/libImaging/TiffDecode.c:479 + 479 if (ReadStrip(tiff, state->y, (UINT32 *)state->buffer) == -1) { + (gdb) p *state + $2 = {count = 0, state = 0, errcode = 0, x = 0, y = 0, ystep = 0, xsize = 17, ysize = 108, xoff = 0, yoff = 0, + shuffle = 0x7ffff735f411 , bits = 32, bytes = 68, buffer = 0xac2eb0 "P\354\336\367\377\177", context = 0xa75440, fd = 0x0} + (gdb) py-bt + Traceback (most recent call first): + File "/home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py", line 1428, in _load_libtiff + + File "/home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py", line 1087, in load + return self._load_libtiff() + File "test_tiff.py", line 8, in + im.load() + +- Poke around till you understand what's going on. In this case, + state->xsize and img.width are different, which led to an out of + bounds write, as the receiving buffer was sized for the smaller of + the two. + +Caveats +------- + +- If your program is running/hung in a docker container and your host + has the appropriate tools, you can run gdb as the superuser in the + host and you may be able to get a trace of where the process is hung. + You probably won't have the capability to do that from within the + docker container, as the trace capacity isn't allowed by default. + +- Variations of this are possible on the mac/windows, but the details + are going to be different. + +- IIRC, Fedora has the gdb bits working by default. Ubuntu has always + been a bit of a battle to make it work. diff --git a/docs/reference/features.rst b/docs/reference/features.rst index c66193061..bb2719694 100644 --- a/docs/reference/features.rst +++ b/docs/reference/features.rst @@ -1,66 +1,66 @@ -.. py:module:: PIL.features -.. py:currentmodule:: PIL.features - -:py:mod:`~PIL.features` Module -============================== - -The :py:mod:`PIL.features` module can be used to detect which Pillow features are available on your system. - -.. autofunction:: PIL.features.pilinfo -.. autofunction:: PIL.features.check -.. autofunction:: PIL.features.version -.. autofunction:: PIL.features.get_supported - -Modules -------- - -Support for the following modules can be checked: - -* ``pil``: The Pillow core module, required for all functionality. -* ``tkinter``: Tkinter support. -* ``freetype2``: FreeType font support via :py:func:`PIL.ImageFont.truetype`. -* ``littlecms2``: LittleCMS 2 support via :py:mod:`PIL.ImageCms`. -* ``webp``: WebP image support. - -.. autofunction:: PIL.features.check_module -.. autofunction:: PIL.features.version_module -.. autofunction:: PIL.features.get_supported_modules - -Codecs ------- - -Support for these is only checked during Pillow compilation. -If the required library was uninstalled from the system, the ``pil`` core module may fail to load instead. -Except for ``jpg``, the version number is checked at run-time. - -Support for the following codecs can be checked: - -* ``jpg``: (compile time) Libjpeg support, required for JPEG based image formats. Only compile time version number is available. -* ``jpg_2000``: (compile time) OpenJPEG support, required for JPEG 2000 image formats. -* ``zlib``: (compile time) Zlib support, required for zlib compressed formats, such as PNG. -* ``libtiff``: (compile time) LibTIFF support, required for TIFF based image formats. - -.. autofunction:: PIL.features.check_codec -.. autofunction:: PIL.features.version_codec -.. autofunction:: PIL.features.get_supported_codecs - -Features --------- - -Some of these are only checked during Pillow compilation. -If the required library was uninstalled from the system, the relevant module may fail to load instead. -Feature version numbers are available only where stated. - -Support for the following features can be checked: - -* ``libjpeg_turbo``: (compile time) Whether Pillow was compiled against the libjpeg-turbo version of libjpeg. Compile-time version number is available. -* ``transp_webp``: Support for transparency in WebP images. -* ``webp_mux``: (compile time) Support for EXIF data in WebP images. -* ``webp_anim``: (compile time) Support for animated WebP images. -* ``raqm``: Raqm library, required for ``ImageFont.Layout.RAQM`` in :py:func:`PIL.ImageFont.truetype`. Run-time version number is available for Raqm 0.7.0 or newer. -* ``libimagequant``: (compile time) ImageQuant quantization support in :py:func:`PIL.Image.Image.quantize`. Run-time version number is available. -* ``xcb``: (compile time) Support for X11 in :py:func:`PIL.ImageGrab.grab` via the XCB library. - -.. autofunction:: PIL.features.check_feature -.. autofunction:: PIL.features.version_feature -.. autofunction:: PIL.features.get_supported_features +.. py:module:: PIL.features +.. py:currentmodule:: PIL.features + +:py:mod:`~PIL.features` Module +============================== + +The :py:mod:`PIL.features` module can be used to detect which Pillow features are available on your system. + +.. autofunction:: PIL.features.pilinfo +.. autofunction:: PIL.features.check +.. autofunction:: PIL.features.version +.. autofunction:: PIL.features.get_supported + +Modules +------- + +Support for the following modules can be checked: + +* ``pil``: The Pillow core module, required for all functionality. +* ``tkinter``: Tkinter support. +* ``freetype2``: FreeType font support via :py:func:`PIL.ImageFont.truetype`. +* ``littlecms2``: LittleCMS 2 support via :py:mod:`PIL.ImageCms`. +* ``webp``: WebP image support. + +.. autofunction:: PIL.features.check_module +.. autofunction:: PIL.features.version_module +.. autofunction:: PIL.features.get_supported_modules + +Codecs +------ + +Support for these is only checked during Pillow compilation. +If the required library was uninstalled from the system, the ``pil`` core module may fail to load instead. +Except for ``jpg``, the version number is checked at run-time. + +Support for the following codecs can be checked: + +* ``jpg``: (compile time) Libjpeg support, required for JPEG based image formats. Only compile time version number is available. +* ``jpg_2000``: (compile time) OpenJPEG support, required for JPEG 2000 image formats. +* ``zlib``: (compile time) Zlib support, required for zlib compressed formats, such as PNG. +* ``libtiff``: (compile time) LibTIFF support, required for TIFF based image formats. + +.. autofunction:: PIL.features.check_codec +.. autofunction:: PIL.features.version_codec +.. autofunction:: PIL.features.get_supported_codecs + +Features +-------- + +Some of these are only checked during Pillow compilation. +If the required library was uninstalled from the system, the relevant module may fail to load instead. +Feature version numbers are available only where stated. + +Support for the following features can be checked: + +* ``libjpeg_turbo``: (compile time) Whether Pillow was compiled against the libjpeg-turbo version of libjpeg. Compile-time version number is available. +* ``transp_webp``: Support for transparency in WebP images. +* ``webp_mux``: (compile time) Support for EXIF data in WebP images. +* ``webp_anim``: (compile time) Support for animated WebP images. +* ``raqm``: Raqm library, required for ``ImageFont.Layout.RAQM`` in :py:func:`PIL.ImageFont.truetype`. Run-time version number is available for Raqm 0.7.0 or newer. +* ``libimagequant``: (compile time) ImageQuant quantization support in :py:func:`PIL.Image.Image.quantize`. Run-time version number is available. +* ``xcb``: (compile time) Support for X11 in :py:func:`PIL.ImageGrab.grab` via the XCB library. + +.. autofunction:: PIL.features.check_feature +.. autofunction:: PIL.features.version_feature +.. autofunction:: PIL.features.get_supported_features diff --git a/docs/reference/index.rst b/docs/reference/index.rst index 82c75e373..220aa01c8 100644 --- a/docs/reference/index.rst +++ b/docs/reference/index.rst @@ -1,39 +1,39 @@ -Reference -========= - -.. toctree:: - :maxdepth: 2 - - - Image - ImageChops - ImageCms - ImageColor - ImageDraw - ImageEnhance - ImageFile - ImageFilter - ImageFont - ImageGrab - ImageMath - ImageMorph - ImageOps - ImagePalette - ImagePath - ImageQt - ImageSequence - ImageShow - ImageStat - ImageTk - ImageTransform - ImageWin - ExifTags - TiffTags - JpegPresets - PSDraw - PixelAccess - PyAccess - features - ../PIL - plugins - internal_design +Reference +========= + +.. toctree:: + :maxdepth: 2 + + + Image + ImageChops + ImageCms + ImageColor + ImageDraw + ImageEnhance + ImageFile + ImageFilter + ImageFont + ImageGrab + ImageMath + ImageMorph + ImageOps + ImagePalette + ImagePath + ImageQt + ImageSequence + ImageShow + ImageStat + ImageTk + ImageTransform + ImageWin + ExifTags + TiffTags + JpegPresets + PSDraw + PixelAccess + PyAccess + features + ../PIL + plugins + internal_design diff --git a/docs/reference/internal_design.rst b/docs/reference/internal_design.rst index 99a18e9ea..b5567bea2 100644 --- a/docs/reference/internal_design.rst +++ b/docs/reference/internal_design.rst @@ -1,11 +1,11 @@ -Internal Reference -================== - -.. toctree:: - :maxdepth: 2 - - open_files - limits - block_allocator - internal_modules - c_extension_debugging +Internal Reference +================== + +.. toctree:: + :maxdepth: 2 + + open_files + limits + block_allocator + internal_modules + c_extension_debugging diff --git a/docs/reference/internal_modules.rst b/docs/reference/internal_modules.rst index 899e4966f..474b6f589 100644 --- a/docs/reference/internal_modules.rst +++ b/docs/reference/internal_modules.rst @@ -1,76 +1,76 @@ -Internal Modules -================ - -:mod:`~PIL._binary` Module --------------------------- - -.. automodule:: PIL._binary - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL._deprecate` Module ------------------------------ - -.. automodule:: PIL._deprecate - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL._tkinter_finder` Module ----------------------------------- - -.. automodule:: PIL._tkinter_finder - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL._typing` Module --------------------------- - -.. module:: PIL._typing - -Provides a convenient way to import type hints that are not available -on some Python versions. - -.. py:class:: StrOrBytesPath - - Typing alias. - -.. py:class:: SupportsRead - - An object that supports the read method. - -.. py:data:: TypeGuard - :value: typing.TypeGuard - - See :py:obj:`typing.TypeGuard`. - -:mod:`~PIL._util` Module ------------------------- - -.. automodule:: PIL._util - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL._version` Module ---------------------------- - -.. module:: PIL._version - -.. data:: __version__ - :annotation: - :type: str - - This is the master version number for Pillow, - all other uses reference this module. - -:mod:`PIL.Image.core` Module ----------------------------- - -.. module:: PIL._imaging -.. module:: PIL.Image.core - -An internal interface module previously known as :mod:`~PIL._imaging`, -implemented in :file:`_imaging.c`. +Internal Modules +================ + +:mod:`~PIL._binary` Module +-------------------------- + +.. automodule:: PIL._binary + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL._deprecate` Module +----------------------------- + +.. automodule:: PIL._deprecate + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL._tkinter_finder` Module +---------------------------------- + +.. automodule:: PIL._tkinter_finder + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL._typing` Module +-------------------------- + +.. module:: PIL._typing + +Provides a convenient way to import type hints that are not available +on some Python versions. + +.. py:class:: StrOrBytesPath + + Typing alias. + +.. py:class:: SupportsRead + + An object that supports the read method. + +.. py:data:: TypeGuard + :value: typing.TypeGuard + + See :py:obj:`typing.TypeGuard`. + +:mod:`~PIL._util` Module +------------------------ + +.. automodule:: PIL._util + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL._version` Module +--------------------------- + +.. module:: PIL._version + +.. data:: __version__ + :annotation: + :type: str + + This is the master version number for Pillow, + all other uses reference this module. + +:mod:`PIL.Image.core` Module +---------------------------- + +.. module:: PIL._imaging +.. module:: PIL.Image.core + +An internal interface module previously known as :mod:`~PIL._imaging`, +implemented in :file:`_imaging.c`. diff --git a/docs/reference/limits.rst b/docs/reference/limits.rst index a71b514b5..717c8d33d 100644 --- a/docs/reference/limits.rst +++ b/docs/reference/limits.rst @@ -1,34 +1,34 @@ -Limits ------- - -This page is documentation to the various fundamental size limits in -the Pillow implementation. - -Internal Limits -=============== - -* Image sizes cannot be negative. These are checked both in - ``Storage.c`` and ``Image.py`` - -* Image sizes may be 0. (Although not in 3.4) - -* Maximum pixel dimensions are limited to INT32, or 2^31 by the sizes - in the image header. - -* Individual allocations are limited to 2GB in ``Storage.c`` - -* The 2GB allocation puts an upper limit to the xsize of the image of - either 2^31 for 'L' or 2^29 for 'RGB' - -* Individual memory mapped segments are limited to 2GB in map.c based - on the overflow checks. This requires that any memory mapped image - is smaller than 2GB, as calculated by ``y*stride`` (so 2Gpx for 'L' - images, and .5Gpx for 'RGB' - -Format Size Limits -================== - -* ICO: Max size is 256x256 - -* Webp: 16383x16383 (underlying library size limit: - https://developers.google.com/speed/webp/docs/api) +Limits +------ + +This page is documentation to the various fundamental size limits in +the Pillow implementation. + +Internal Limits +=============== + +* Image sizes cannot be negative. These are checked both in + ``Storage.c`` and ``Image.py`` + +* Image sizes may be 0. (Although not in 3.4) + +* Maximum pixel dimensions are limited to INT32, or 2^31 by the sizes + in the image header. + +* Individual allocations are limited to 2GB in ``Storage.c`` + +* The 2GB allocation puts an upper limit to the xsize of the image of + either 2^31 for 'L' or 2^29 for 'RGB' + +* Individual memory mapped segments are limited to 2GB in map.c based + on the overflow checks. This requires that any memory mapped image + is smaller than 2GB, as calculated by ``y*stride`` (so 2Gpx for 'L' + images, and .5Gpx for 'RGB' + +Format Size Limits +================== + +* ICO: Max size is 256x256 + +* 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..cb0603855 100644 --- a/docs/reference/open_files.rst +++ b/docs/reference/open_files.rst @@ -1,110 +1,110 @@ -.. _file-handling: - -File Handling in Pillow -======================= - -When opening a file as an image, Pillow requires a filename, ``os.PathLike`` -object, or a file-like object. Pillow uses the filename or ``Path`` to open a -file, so for the rest of this article, they will all be treated as a file-like -object. - -The following are all equivalent:: - - from PIL import Image - import io - import pathlib - - with Image.open("test.jpg") as im: - ... - - with Image.open(pathlib.Path("test.jpg")) as im2: - ... - - with open("test.jpg", "rb") as f: - im3 = Image.open(f) - ... - - with open("test.jpg", "rb") as f: - im4 = Image.open(io.BytesIO(f.read())) - ... - -If a filename or a path-like object is passed to Pillow, then the resulting -file object opened by Pillow may also be closed by Pillow after the -``Image.Image.load()`` method is called, provided the associated image does not -have multiple frames. - -Pillow cannot in general close and reopen a file, so any access to -that file needs to be prior to the close. - -Image Lifecycle ---------------- - -* ``Image.open()`` Filenames and ``Path`` objects are opened as a file. - Metadata is read from the open file. The file is left open for further usage. - -* ``Image.Image.load()`` When the pixel data from the image is - required, ``load()`` is called. The current frame is read into - memory. The image can now be used independently of the underlying - image file. - - Any Pillow method that creates a new image instance based on another will - internally call ``load()`` on the original image and then read the data. - The new image instance will not be associated with the original image file. - - If a filename or a ``Path`` object was passed to ``Image.open()``, then the - file object was opened by Pillow and is considered to be used exclusively by - Pillow. So if the image is a single-frame image, the file will be closed in - this method after the frame is read. If the image is a multi-frame image, - (e.g. multipage TIFF and animated GIF) the image file is left open so that - ``Image.Image.seek()`` can load the appropriate frame. - -* ``Image.Image.close()`` Closes the file and destroys the core image object. - - The Pillow context manager will also close the file, but will not destroy - the core image object. e.g.:: - - with Image.open("test.jpg") as img: - img.load() - assert img.fp is None - img.save("test.png") - - -The lifecycle of a single-frame image is relatively simple. The file must -remain open until the ``load()`` or ``close()`` function is called or the -context manager exits. - -Multi-frame images are more complicated. The ``load()`` method is not -a terminal method, so it should not close the underlying file. In general, -Pillow does not know if there are going to be any requests for additional -data until the caller has explicitly closed the image. - - -Complications -------------- - -* ``TiffImagePlugin`` has some code to pass the underlying file descriptor into - libtiff (if working on an actual file). Since libtiff closes the file - descriptor internally, it is duplicated prior to passing it into libtiff. - -* After a file has been closed, operations that require file access will fail:: - - with open("test.jpg", "rb") as f: - im5 = Image.open(f) - im5.load() # FAILS, closed file - - with Image.open("test.jpg") as im6: - pass - im6.load() # FAILS, closed file - - -Proposed File Handling ----------------------- - -* ``Image.Image.load()`` should close the image file, unless there are - multiple frames. - -* ``Image.Image.seek()`` should never close the image file. - -* Users of the library should use a context manager or call - ``Image.Image.close()`` on any image opened with a filename or ``Path`` - object to ensure that the underlying file is closed. +.. _file-handling: + +File Handling in Pillow +======================= + +When opening a file as an image, Pillow requires a filename, ``os.PathLike`` +object, or a file-like object. Pillow uses the filename or ``Path`` to open a +file, so for the rest of this article, they will all be treated as a file-like +object. + +The following are all equivalent:: + + from PIL import Image + import io + import pathlib + + with Image.open("test.jpg") as im: + ... + + with Image.open(pathlib.Path("test.jpg")) as im2: + ... + + with open("test.jpg", "rb") as f: + im3 = Image.open(f) + ... + + with open("test.jpg", "rb") as f: + im4 = Image.open(io.BytesIO(f.read())) + ... + +If a filename or a path-like object is passed to Pillow, then the resulting +file object opened by Pillow may also be closed by Pillow after the +``Image.Image.load()`` method is called, provided the associated image does not +have multiple frames. + +Pillow cannot in general close and reopen a file, so any access to +that file needs to be prior to the close. + +Image Lifecycle +--------------- + +* ``Image.open()`` Filenames and ``Path`` objects are opened as a file. + Metadata is read from the open file. The file is left open for further usage. + +* ``Image.Image.load()`` When the pixel data from the image is + required, ``load()`` is called. The current frame is read into + memory. The image can now be used independently of the underlying + image file. + + Any Pillow method that creates a new image instance based on another will + internally call ``load()`` on the original image and then read the data. + The new image instance will not be associated with the original image file. + + If a filename or a ``Path`` object was passed to ``Image.open()``, then the + file object was opened by Pillow and is considered to be used exclusively by + Pillow. So if the image is a single-frame image, the file will be closed in + this method after the frame is read. If the image is a multi-frame image, + (e.g. multipage TIFF and animated GIF) the image file is left open so that + ``Image.Image.seek()`` can load the appropriate frame. + +* ``Image.Image.close()`` Closes the file and destroys the core image object. + + The Pillow context manager will also close the file, but will not destroy + the core image object. e.g.:: + + with Image.open("test.jpg") as img: + img.load() + assert img.fp is None + img.save("test.png") + + +The lifecycle of a single-frame image is relatively simple. The file must +remain open until the ``load()`` or ``close()`` function is called or the +context manager exits. + +Multi-frame images are more complicated. The ``load()`` method is not +a terminal method, so it should not close the underlying file. In general, +Pillow does not know if there are going to be any requests for additional +data until the caller has explicitly closed the image. + + +Complications +------------- + +* ``TiffImagePlugin`` has some code to pass the underlying file descriptor into + libtiff (if working on an actual file). Since libtiff closes the file + descriptor internally, it is duplicated prior to passing it into libtiff. + +* After a file has been closed, operations that require file access will fail:: + + with open("test.jpg", "rb") as f: + im5 = Image.open(f) + im5.load() # FAILS, closed file + + with Image.open("test.jpg") as im6: + pass + im6.load() # FAILS, closed file + + +Proposed File Handling +---------------------- + +* ``Image.Image.load()`` should close the image file, unless there are + multiple frames. + +* ``Image.Image.seek()`` should never close the image file. + +* Users of the library should use a context manager or call + ``Image.Image.close()`` on any image opened with a filename or ``Path`` + object to ensure that the underlying file is closed. diff --git a/docs/reference/plugins.rst b/docs/reference/plugins.rst index 18cd99cf3..c9dfb1a2a 100644 --- a/docs/reference/plugins.rst +++ b/docs/reference/plugins.rst @@ -1,341 +1,341 @@ -Plugin reference -================ - -:mod:`~PIL.BmpImagePlugin` Module ---------------------------------- - -.. automodule:: PIL.BmpImagePlugin - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL.BufrStubImagePlugin` Module --------------------------------------- - -.. automodule:: PIL.BufrStubImagePlugin - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL.CurImagePlugin` Module ---------------------------------- - -.. automodule:: PIL.CurImagePlugin - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL.DcxImagePlugin` Module ---------------------------------- - -.. automodule:: PIL.DcxImagePlugin - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL.DdsImagePlugin` Module ---------------------------------- - -.. automodule:: PIL.DdsImagePlugin - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL.EpsImagePlugin` Module ---------------------------------- - -.. automodule:: PIL.EpsImagePlugin - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL.FitsImagePlugin` Module --------------------------------------- - -.. automodule:: PIL.FitsImagePlugin - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL.FliImagePlugin` Module ---------------------------------- - -.. automodule:: PIL.FliImagePlugin - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL.FpxImagePlugin` Module ---------------------------------- - -.. automodule:: PIL.FpxImagePlugin - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL.GbrImagePlugin` Module ---------------------------------- - -.. automodule:: PIL.GbrImagePlugin - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL.GifImagePlugin` Module ---------------------------------- - -.. automodule:: PIL.GifImagePlugin - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL.GribStubImagePlugin` Module --------------------------------------- - -.. automodule:: PIL.GribStubImagePlugin - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL.Hdf5StubImagePlugin` Module --------------------------------------- - -.. automodule:: PIL.Hdf5StubImagePlugin - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL.IcnsImagePlugin` Module ----------------------------------- - -.. automodule:: PIL.IcnsImagePlugin - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL.IcoImagePlugin` Module ---------------------------------- - -.. automodule:: PIL.IcoImagePlugin - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL.ImImagePlugin` Module --------------------------------- - -.. automodule:: PIL.ImImagePlugin - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL.ImtImagePlugin` Module ---------------------------------- - -.. automodule:: PIL.ImtImagePlugin - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL.IptcImagePlugin` Module ----------------------------------- - -.. automodule:: PIL.IptcImagePlugin - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL.JpegImagePlugin` Module ----------------------------------- - -.. automodule:: PIL.JpegImagePlugin - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL.Jpeg2KImagePlugin` Module ------------------------------------- - -.. automodule:: PIL.Jpeg2KImagePlugin - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL.McIdasImagePlugin` Module ------------------------------------- - -.. automodule:: PIL.McIdasImagePlugin - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL.MicImagePlugin` Module ---------------------------------- - -.. automodule:: PIL.MicImagePlugin - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL.MpegImagePlugin` Module ----------------------------------- - -.. automodule:: PIL.MpegImagePlugin - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL.MspImagePlugin` Module ---------------------------------- - -.. automodule:: PIL.MspImagePlugin - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL.PalmImagePlugin` Module ----------------------------------- - -.. automodule:: PIL.PalmImagePlugin - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL.PcdImagePlugin` Module ---------------------------------- - -.. automodule:: PIL.PcdImagePlugin - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL.PcxImagePlugin` Module ---------------------------------- - -.. automodule:: PIL.PcxImagePlugin - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL.PdfImagePlugin` Module ---------------------------------- - -.. automodule:: PIL.PdfImagePlugin - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL.PixarImagePlugin` Module ------------------------------------ - -.. automodule:: PIL.PixarImagePlugin - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL.PngImagePlugin` Module ---------------------------------- - -.. automodule:: PIL.PngImagePlugin - :members: ChunkStream, PngImageFile, PngStream, getchunks, is_cid, putchunk, - Blend, Disposal, MAX_TEXT_CHUNK, MAX_TEXT_MEMORY - :undoc-members: - :show-inheritance: - :member-order: groupwise - - -:mod:`~PIL.PpmImagePlugin` Module ---------------------------------- - -.. automodule:: PIL.PpmImagePlugin - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL.PsdImagePlugin` Module ---------------------------------- - -.. automodule:: PIL.PsdImagePlugin - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL.SgiImagePlugin` Module ---------------------------------- - -.. automodule:: PIL.SgiImagePlugin - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL.SpiderImagePlugin` Module ------------------------------------- - -.. automodule:: PIL.SpiderImagePlugin - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL.SunImagePlugin` Module ---------------------------------- - -.. automodule:: PIL.SunImagePlugin - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL.TgaImagePlugin` Module ---------------------------------- - -.. automodule:: PIL.TgaImagePlugin - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL.TiffImagePlugin` Module ----------------------------------- - -.. automodule:: PIL.TiffImagePlugin - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL.WebPImagePlugin` Module ----------------------------------- - -.. automodule:: PIL.WebPImagePlugin - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL.WmfImagePlugin` Module ---------------------------------- - -.. automodule:: PIL.WmfImagePlugin - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL.XVThumbImagePlugin` Module -------------------------------------- - -.. automodule:: PIL.XVThumbImagePlugin - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL.XbmImagePlugin` Module ---------------------------------- - -.. automodule:: PIL.XbmImagePlugin - :members: - :undoc-members: - :show-inheritance: - -:mod:`~PIL.XpmImagePlugin` Module ---------------------------------- - -.. automodule:: PIL.XpmImagePlugin - :members: - :undoc-members: - :show-inheritance: +Plugin reference +================ + +:mod:`~PIL.BmpImagePlugin` Module +--------------------------------- + +.. automodule:: PIL.BmpImagePlugin + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL.BufrStubImagePlugin` Module +-------------------------------------- + +.. automodule:: PIL.BufrStubImagePlugin + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL.CurImagePlugin` Module +--------------------------------- + +.. automodule:: PIL.CurImagePlugin + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL.DcxImagePlugin` Module +--------------------------------- + +.. automodule:: PIL.DcxImagePlugin + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL.DdsImagePlugin` Module +--------------------------------- + +.. automodule:: PIL.DdsImagePlugin + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL.EpsImagePlugin` Module +--------------------------------- + +.. automodule:: PIL.EpsImagePlugin + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL.FitsImagePlugin` Module +-------------------------------------- + +.. automodule:: PIL.FitsImagePlugin + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL.FliImagePlugin` Module +--------------------------------- + +.. automodule:: PIL.FliImagePlugin + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL.FpxImagePlugin` Module +--------------------------------- + +.. automodule:: PIL.FpxImagePlugin + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL.GbrImagePlugin` Module +--------------------------------- + +.. automodule:: PIL.GbrImagePlugin + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL.GifImagePlugin` Module +--------------------------------- + +.. automodule:: PIL.GifImagePlugin + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL.GribStubImagePlugin` Module +-------------------------------------- + +.. automodule:: PIL.GribStubImagePlugin + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL.Hdf5StubImagePlugin` Module +-------------------------------------- + +.. automodule:: PIL.Hdf5StubImagePlugin + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL.IcnsImagePlugin` Module +---------------------------------- + +.. automodule:: PIL.IcnsImagePlugin + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL.IcoImagePlugin` Module +--------------------------------- + +.. automodule:: PIL.IcoImagePlugin + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL.ImImagePlugin` Module +-------------------------------- + +.. automodule:: PIL.ImImagePlugin + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL.ImtImagePlugin` Module +--------------------------------- + +.. automodule:: PIL.ImtImagePlugin + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL.IptcImagePlugin` Module +---------------------------------- + +.. automodule:: PIL.IptcImagePlugin + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL.JpegImagePlugin` Module +---------------------------------- + +.. automodule:: PIL.JpegImagePlugin + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL.Jpeg2KImagePlugin` Module +------------------------------------ + +.. automodule:: PIL.Jpeg2KImagePlugin + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL.McIdasImagePlugin` Module +------------------------------------ + +.. automodule:: PIL.McIdasImagePlugin + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL.MicImagePlugin` Module +--------------------------------- + +.. automodule:: PIL.MicImagePlugin + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL.MpegImagePlugin` Module +---------------------------------- + +.. automodule:: PIL.MpegImagePlugin + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL.MspImagePlugin` Module +--------------------------------- + +.. automodule:: PIL.MspImagePlugin + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL.PalmImagePlugin` Module +---------------------------------- + +.. automodule:: PIL.PalmImagePlugin + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL.PcdImagePlugin` Module +--------------------------------- + +.. automodule:: PIL.PcdImagePlugin + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL.PcxImagePlugin` Module +--------------------------------- + +.. automodule:: PIL.PcxImagePlugin + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL.PdfImagePlugin` Module +--------------------------------- + +.. automodule:: PIL.PdfImagePlugin + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL.PixarImagePlugin` Module +----------------------------------- + +.. automodule:: PIL.PixarImagePlugin + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL.PngImagePlugin` Module +--------------------------------- + +.. automodule:: PIL.PngImagePlugin + :members: ChunkStream, PngImageFile, PngStream, getchunks, is_cid, putchunk, + Blend, Disposal, MAX_TEXT_CHUNK, MAX_TEXT_MEMORY + :undoc-members: + :show-inheritance: + :member-order: groupwise + + +:mod:`~PIL.PpmImagePlugin` Module +--------------------------------- + +.. automodule:: PIL.PpmImagePlugin + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL.PsdImagePlugin` Module +--------------------------------- + +.. automodule:: PIL.PsdImagePlugin + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL.SgiImagePlugin` Module +--------------------------------- + +.. automodule:: PIL.SgiImagePlugin + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL.SpiderImagePlugin` Module +------------------------------------ + +.. automodule:: PIL.SpiderImagePlugin + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL.SunImagePlugin` Module +--------------------------------- + +.. automodule:: PIL.SunImagePlugin + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL.TgaImagePlugin` Module +--------------------------------- + +.. automodule:: PIL.TgaImagePlugin + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL.TiffImagePlugin` Module +---------------------------------- + +.. automodule:: PIL.TiffImagePlugin + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL.WebPImagePlugin` Module +---------------------------------- + +.. automodule:: PIL.WebPImagePlugin + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL.WmfImagePlugin` Module +--------------------------------- + +.. automodule:: PIL.WmfImagePlugin + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL.XVThumbImagePlugin` Module +------------------------------------- + +.. automodule:: PIL.XVThumbImagePlugin + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL.XbmImagePlugin` Module +--------------------------------- + +.. automodule:: PIL.XbmImagePlugin + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL.XpmImagePlugin` Module +--------------------------------- + +.. automodule:: PIL.XpmImagePlugin + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst index adada6e01..ef2076412 100644 --- a/docs/releasenotes/10.0.0.rst +++ b/docs/releasenotes/10.0.0.rst @@ -1,211 +1,211 @@ -10.0.0 ------- - -Security -======== - -Limit size even if one dimension is zero -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -When performing decompression bomb checks, Pillow did not reject images with -excessive width and zero height, or zero width and excessive height. That has -now been fixed. - -This effectively dates to the PIL fork, since problem images would still have -been processed before Pillow started checking for decompression bombs. - -.. _Added ImageFont.MAX_STRING_LENGTH: - -:cve:`2023-44271`: Added ImageFont.MAX_STRING_LENGTH -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -To protect against potential DOS attacks when using arbitrary strings as text -input, Pillow will now raise a :py:exc:`ValueError` if the number of characters -passed into ImageFont methods is over a certain limit, -:py:data:`PIL.ImageFont.MAX_STRING_LENGTH`. - -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 -============================== - -Categories -^^^^^^^^^^ - -``im.category`` has been removed, along with the related ``Image.NORMAL``, -``Image.SEQUENCE`` and ``Image.CONTAINER`` attributes. - -To determine if an image has multiple frames or not, -``getattr(im, "is_animated", False)`` can be used instead. - -Tk/Tcl 8.4 -^^^^^^^^^^ - -Support for Tk/Tcl 8.4 has been removed. - -JpegImagePlugin.convert_dict_qtables -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Since deprecation in Pillow 8.3.0, the ``convert_dict_qtables`` method no longer -performed any operations on the data given to it, and has been removed. - -ImagePalette size parameter -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Before Pillow 8.3.0, ``ImagePalette`` required palette data of particular lengths by -default, and the ``size`` parameter could be used to override that. Pillow 8.3.0 -removed the default required length, also removing the need for the ``size`` parameter. - -ImageShow.Viewer.show_file file argument -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The ``file`` argument in :py:meth:`~PIL.ImageShow.Viewer.show_file()` has been -removed and replaced by ``path``. - -In effect, ``viewer.show_file("test.jpg")`` will continue to work unchanged. - -Constants -^^^^^^^^^ - -A number of constants have been removed. -Instead, :py:class:`enum.IntEnum` classes have been added. - -===================================================== ============================================================ -Removed Use instead -===================================================== ============================================================ -``Image.LINEAR`` ``Image.BILINEAR`` or ``Image.Resampling.BILINEAR`` -``Image.CUBIC`` ``Image.BICUBIC`` or ``Image.Resampling.BICUBIC`` -``Image.ANTIALIAS`` ``Image.LANCZOS`` or ``Image.Resampling.LANCZOS`` -``ImageCms.INTENT_PERCEPTUAL`` ``ImageCms.Intent.PERCEPTUAL`` -``ImageCms.INTENT_RELATIVE_COLORMETRIC`` ``ImageCms.Intent.RELATIVE_COLORMETRIC`` -``ImageCms.INTENT_SATURATION`` ``ImageCms.Intent.SATURATION`` -``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC`` ``ImageCms.Intent.ABSOLUTE_COLORIMETRIC`` -``ImageCms.DIRECTION_INPUT`` ``ImageCms.Direction.INPUT`` -``ImageCms.DIRECTION_OUTPUT`` ``ImageCms.Direction.OUTPUT`` -``ImageCms.DIRECTION_PROOF`` ``ImageCms.Direction.PROOF`` -``ImageFont.LAYOUT_BASIC`` ``ImageFont.Layout.BASIC`` -``ImageFont.LAYOUT_RAQM`` ``ImageFont.Layout.RAQM`` -``BlpImagePlugin.BLP_FORMAT_JPEG`` ``BlpImagePlugin.Format.JPEG`` -``BlpImagePlugin.BLP_ENCODING_UNCOMPRESSED`` ``BlpImagePlugin.Encoding.UNCOMPRESSED`` -``BlpImagePlugin.BLP_ENCODING_DXT`` ``BlpImagePlugin.Encoding.DXT`` -``BlpImagePlugin.BLP_ENCODING_UNCOMPRESSED_RAW_RGBA`` ``BlpImagePlugin.Encoding.UNCOMPRESSED_RAW_RGBA`` -``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT1`` ``BlpImagePlugin.AlphaEncoding.DXT1`` -``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT3`` ``BlpImagePlugin.AlphaEncoding.DXT3`` -``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT5`` ``BlpImagePlugin.AlphaEncoding.DXT5`` -``FtexImagePlugin.FORMAT_DXT1`` ``FtexImagePlugin.Format.DXT1`` -``FtexImagePlugin.FORMAT_UNCOMPRESSED`` ``FtexImagePlugin.Format.UNCOMPRESSED`` -``PngImagePlugin.APNG_DISPOSE_OP_NONE`` ``PngImagePlugin.Disposal.OP_NONE`` -``PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND`` ``PngImagePlugin.Disposal.OP_BACKGROUND`` -``PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS`` ``PngImagePlugin.Disposal.OP_PREVIOUS`` -``PngImagePlugin.APNG_BLEND_OP_SOURCE`` ``PngImagePlugin.Blend.OP_SOURCE`` -``PngImagePlugin.APNG_BLEND_OP_OVER`` ``PngImagePlugin.Blend.OP_OVER`` -===================================================== ============================================================ - -FitsStubImagePlugin -^^^^^^^^^^^^^^^^^^^ - -The stub image plugin ``FitsStubImagePlugin`` has been removed. -FITS images can be read without a handler through :mod:`~PIL.FitsImagePlugin` instead. - -Font size and offset methods -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Several functions for computing the size and offset of rendered text have been removed: - -=============================================================== ============================================================================================================= -Removed Use instead -=============================================================== ============================================================================================================= -``FreeTypeFont.getsize()`` and ``FreeTypeFont.getoffset()`` :py:meth:`.FreeTypeFont.getbbox` and :py:meth:`.FreeTypeFont.getlength` -``FreeTypeFont.getsize_multiline()`` :py:meth:`.ImageDraw.multiline_textbbox` -``ImageFont.getsize()`` :py:meth:`.ImageFont.getbbox` and :py:meth:`.ImageFont.getlength` -``TransposedFont.getsize()`` :py:meth:`.TransposedFont.getbbox` and :py:meth:`.TransposedFont.getlength` -``ImageDraw.textsize()`` and ``ImageDraw.multiline_textsize()`` :py:meth:`.ImageDraw.textbbox`, :py:meth:`.ImageDraw.textlength` and :py:meth:`.ImageDraw.multiline_textbbox` -``ImageDraw2.Draw.textsize()`` :py:meth:`.ImageDraw2.Draw.textbbox` and :py:meth:`.ImageDraw2.Draw.textlength` -=============================================================== ============================================================================================================= - -FreeTypeFont.getmask2 fill parameter -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The undocumented ``fill`` parameter of :py:meth:`.FreeTypeFont.getmask2` has been -removed. - -PhotoImage.paste box parameter -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The ``box`` parameter was unused and has been removed. - -PyQt5 and PySide2 -^^^^^^^^^^^^^^^^^ - -`Qt 5 reached end-of-life `_ on 2020-12-08 for -open-source users (and will reach EOL on 2023-12-08 for commercial licence holders). - -Support for PyQt5 and PySide2 has been removed from ``ImageQt``. Upgrade to -`PyQt6 `_ or -`PySide6 `_ instead. - -Image.coerce_e -^^^^^^^^^^^^^^ - -This undocumented method has been removed. - -Deprecations -============ - -PyAccess and Image.USE_CFFI_ACCESS -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Since Pillow's C API is now faster than PyAccess on PyPy, -:py:mod:`~PIL.PyAccess` has been deprecated and will be removed in Pillow -11.0.0 (2024-10-15). Pillow's C API will now be used by default on PyPy instead. - -``Image.USE_CFFI_ACCESS``, for switching from the C API to PyAccess, is -similarly deprecated. - -API Changes -=========== - -Added line width parameter to ImageDraw regular_polygon -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -An optional line ``width`` parameter has been added to -``ImageDraw.Draw.regular_polygon``. - -API Additions -============= - -Added ``alpha_only`` argument to ``getbbox()`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -:py:meth:`~PIL.Image.Image.getbbox` now accepts a keyword argument of -``alpha_only``. This is an optional flag, defaulting to ``True``. If ``True`` -and the image has an alpha channel, trim transparent pixels. Otherwise, trim -pixels when all channels are zero. - -Other Changes -============= - -32-bit wheels -^^^^^^^^^^^^^ - -32-bit wheels are no longer provided. - -Support display_jpeg() in IPython -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -In addition to ``display()`` and ``display_png``, ``display_jpeg()`` can now -also be used to display images in IPython:: - - from PIL import Image - from IPython.display import display_jpeg - - im = Image.new("RGB", (100, 100), (255, 0, 0)) - display_jpeg(im) - -Support reading signed 8-bit TIFF images -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -TIFF images with signed integer data, 8 bits per sample and a photometric -interpretation of BlackIsZero can now be read. +10.0.0 +------ + +Security +======== + +Limit size even if one dimension is zero +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When performing decompression bomb checks, Pillow did not reject images with +excessive width and zero height, or zero width and excessive height. That has +now been fixed. + +This effectively dates to the PIL fork, since problem images would still have +been processed before Pillow started checking for decompression bombs. + +.. _Added ImageFont.MAX_STRING_LENGTH: + +:cve:`2023-44271`: Added ImageFont.MAX_STRING_LENGTH +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To protect against potential DOS attacks when using arbitrary strings as text +input, Pillow will now raise a :py:exc:`ValueError` if the number of characters +passed into ImageFont methods is over a certain limit, +:py:data:`PIL.ImageFont.MAX_STRING_LENGTH`. + +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 +============================== + +Categories +^^^^^^^^^^ + +``im.category`` has been removed, along with the related ``Image.NORMAL``, +``Image.SEQUENCE`` and ``Image.CONTAINER`` attributes. + +To determine if an image has multiple frames or not, +``getattr(im, "is_animated", False)`` can be used instead. + +Tk/Tcl 8.4 +^^^^^^^^^^ + +Support for Tk/Tcl 8.4 has been removed. + +JpegImagePlugin.convert_dict_qtables +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Since deprecation in Pillow 8.3.0, the ``convert_dict_qtables`` method no longer +performed any operations on the data given to it, and has been removed. + +ImagePalette size parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Before Pillow 8.3.0, ``ImagePalette`` required palette data of particular lengths by +default, and the ``size`` parameter could be used to override that. Pillow 8.3.0 +removed the default required length, also removing the need for the ``size`` parameter. + +ImageShow.Viewer.show_file file argument +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``file`` argument in :py:meth:`~PIL.ImageShow.Viewer.show_file()` has been +removed and replaced by ``path``. + +In effect, ``viewer.show_file("test.jpg")`` will continue to work unchanged. + +Constants +^^^^^^^^^ + +A number of constants have been removed. +Instead, :py:class:`enum.IntEnum` classes have been added. + +===================================================== ============================================================ +Removed Use instead +===================================================== ============================================================ +``Image.LINEAR`` ``Image.BILINEAR`` or ``Image.Resampling.BILINEAR`` +``Image.CUBIC`` ``Image.BICUBIC`` or ``Image.Resampling.BICUBIC`` +``Image.ANTIALIAS`` ``Image.LANCZOS`` or ``Image.Resampling.LANCZOS`` +``ImageCms.INTENT_PERCEPTUAL`` ``ImageCms.Intent.PERCEPTUAL`` +``ImageCms.INTENT_RELATIVE_COLORMETRIC`` ``ImageCms.Intent.RELATIVE_COLORMETRIC`` +``ImageCms.INTENT_SATURATION`` ``ImageCms.Intent.SATURATION`` +``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC`` ``ImageCms.Intent.ABSOLUTE_COLORIMETRIC`` +``ImageCms.DIRECTION_INPUT`` ``ImageCms.Direction.INPUT`` +``ImageCms.DIRECTION_OUTPUT`` ``ImageCms.Direction.OUTPUT`` +``ImageCms.DIRECTION_PROOF`` ``ImageCms.Direction.PROOF`` +``ImageFont.LAYOUT_BASIC`` ``ImageFont.Layout.BASIC`` +``ImageFont.LAYOUT_RAQM`` ``ImageFont.Layout.RAQM`` +``BlpImagePlugin.BLP_FORMAT_JPEG`` ``BlpImagePlugin.Format.JPEG`` +``BlpImagePlugin.BLP_ENCODING_UNCOMPRESSED`` ``BlpImagePlugin.Encoding.UNCOMPRESSED`` +``BlpImagePlugin.BLP_ENCODING_DXT`` ``BlpImagePlugin.Encoding.DXT`` +``BlpImagePlugin.BLP_ENCODING_UNCOMPRESSED_RAW_RGBA`` ``BlpImagePlugin.Encoding.UNCOMPRESSED_RAW_RGBA`` +``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT1`` ``BlpImagePlugin.AlphaEncoding.DXT1`` +``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT3`` ``BlpImagePlugin.AlphaEncoding.DXT3`` +``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT5`` ``BlpImagePlugin.AlphaEncoding.DXT5`` +``FtexImagePlugin.FORMAT_DXT1`` ``FtexImagePlugin.Format.DXT1`` +``FtexImagePlugin.FORMAT_UNCOMPRESSED`` ``FtexImagePlugin.Format.UNCOMPRESSED`` +``PngImagePlugin.APNG_DISPOSE_OP_NONE`` ``PngImagePlugin.Disposal.OP_NONE`` +``PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND`` ``PngImagePlugin.Disposal.OP_BACKGROUND`` +``PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS`` ``PngImagePlugin.Disposal.OP_PREVIOUS`` +``PngImagePlugin.APNG_BLEND_OP_SOURCE`` ``PngImagePlugin.Blend.OP_SOURCE`` +``PngImagePlugin.APNG_BLEND_OP_OVER`` ``PngImagePlugin.Blend.OP_OVER`` +===================================================== ============================================================ + +FitsStubImagePlugin +^^^^^^^^^^^^^^^^^^^ + +The stub image plugin ``FitsStubImagePlugin`` has been removed. +FITS images can be read without a handler through :mod:`~PIL.FitsImagePlugin` instead. + +Font size and offset methods +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Several functions for computing the size and offset of rendered text have been removed: + +=============================================================== ============================================================================================================= +Removed Use instead +=============================================================== ============================================================================================================= +``FreeTypeFont.getsize()`` and ``FreeTypeFont.getoffset()`` :py:meth:`.FreeTypeFont.getbbox` and :py:meth:`.FreeTypeFont.getlength` +``FreeTypeFont.getsize_multiline()`` :py:meth:`.ImageDraw.multiline_textbbox` +``ImageFont.getsize()`` :py:meth:`.ImageFont.getbbox` and :py:meth:`.ImageFont.getlength` +``TransposedFont.getsize()`` :py:meth:`.TransposedFont.getbbox` and :py:meth:`.TransposedFont.getlength` +``ImageDraw.textsize()`` and ``ImageDraw.multiline_textsize()`` :py:meth:`.ImageDraw.textbbox`, :py:meth:`.ImageDraw.textlength` and :py:meth:`.ImageDraw.multiline_textbbox` +``ImageDraw2.Draw.textsize()`` :py:meth:`.ImageDraw2.Draw.textbbox` and :py:meth:`.ImageDraw2.Draw.textlength` +=============================================================== ============================================================================================================= + +FreeTypeFont.getmask2 fill parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The undocumented ``fill`` parameter of :py:meth:`.FreeTypeFont.getmask2` has been +removed. + +PhotoImage.paste box parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``box`` parameter was unused and has been removed. + +PyQt5 and PySide2 +^^^^^^^^^^^^^^^^^ + +`Qt 5 reached end-of-life `_ on 2020-12-08 for +open-source users (and will reach EOL on 2023-12-08 for commercial licence holders). + +Support for PyQt5 and PySide2 has been removed from ``ImageQt``. Upgrade to +`PyQt6 `_ or +`PySide6 `_ instead. + +Image.coerce_e +^^^^^^^^^^^^^^ + +This undocumented method has been removed. + +Deprecations +============ + +PyAccess and Image.USE_CFFI_ACCESS +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Since Pillow's C API is now faster than PyAccess on PyPy, +:py:mod:`~PIL.PyAccess` has been deprecated and will be removed in Pillow +11.0.0 (2024-10-15). Pillow's C API will now be used by default on PyPy instead. + +``Image.USE_CFFI_ACCESS``, for switching from the C API to PyAccess, is +similarly deprecated. + +API Changes +=========== + +Added line width parameter to ImageDraw regular_polygon +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +An optional line ``width`` parameter has been added to +``ImageDraw.Draw.regular_polygon``. + +API Additions +============= + +Added ``alpha_only`` argument to ``getbbox()`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:py:meth:`~PIL.Image.Image.getbbox` now accepts a keyword argument of +``alpha_only``. This is an optional flag, defaulting to ``True``. If ``True`` +and the image has an alpha channel, trim transparent pixels. Otherwise, trim +pixels when all channels are zero. + +Other Changes +============= + +32-bit wheels +^^^^^^^^^^^^^ + +32-bit wheels are no longer provided. + +Support display_jpeg() in IPython +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In addition to ``display()`` and ``display_png``, ``display_jpeg()`` can now +also be used to display images in IPython:: + + from PIL import Image + from IPython.display import display_jpeg + + im = Image.new("RGB", (100, 100), (255, 0, 0)) + display_jpeg(im) + +Support reading signed 8-bit TIFF images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +TIFF images with signed integer data, 8 bits per sample and a photometric +interpretation of BlackIsZero can now be read. diff --git a/docs/releasenotes/10.0.1.rst b/docs/releasenotes/10.0.1.rst index 02189d514..e5384deef 100644 --- a/docs/releasenotes/10.0.1.rst +++ b/docs/releasenotes/10.0.1.rst @@ -1,20 +1,20 @@ -10.0.1 ------- - -Security -======== - -:cve:`2023-4863`: Updated install script and updated wheels -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -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 -============= - -Updated tests to pass with latest zlib version -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The release of zlib 1.3 caused one of the tests in the Pillow test suite to fail. +10.0.1 +------ + +Security +======== + +:cve:`2023-4863`: Updated install script and updated wheels +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +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 +============= + +Updated tests to pass with latest zlib version +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The release of zlib 1.3 caused one of the tests in the Pillow test suite to fail. diff --git a/docs/releasenotes/10.1.0.rst b/docs/releasenotes/10.1.0.rst index fd556bdf1..9691198bc 100644 --- a/docs/releasenotes/10.1.0.rst +++ b/docs/releasenotes/10.1.0.rst @@ -1,109 +1,109 @@ -10.1.0 ------- - -API Changes -=========== - -Setting image mode -^^^^^^^^^^^^^^^^^^ - -If you attempt to set the mode of an image directly, e.g. -``im.mode = "RGBA"``, you will now receive an :py:exc:`AttributeError`. This is -not about removing existing functionality, but instead about raising an -explicit error to prevent later consequences. The ``convert`` method is the -correct way to change an image's mode. - -Accept a list in getpixel() -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -:py:meth:`~PIL.Image.Image.getpixel` now accepts a list of coordinates, as well -as a tuple. :: - - from PIL import Image - im = Image.new("RGB", (1, 1)) - im.getpixel((0, 0)) - im.getpixel([0, 0]) - -BoxBlur and GaussianBlur allow for different x and y radii -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -:py:class:`~PIL.ImageFilter.BoxBlur` and -:py:class:`~PIL.ImageFilter.GaussianBlur` now allow a sequence of x and y radii -to be specified, rather than a single number for both dimensions. :: - - from PIL import ImageFilter - ImageFilter.BoxBlur((2, 5)) - ImageFilter.GaussianBlur((2, 5)) - -API Additions -============= - -EpsImagePlugin.gs_binary -^^^^^^^^^^^^^^^^^^^^^^^^ - -``EpsImagePlugin.gs_windows_binary`` stores the name of the Ghostscript -executable on Windows. ``EpsImagePlugin.gs_binary`` has now been added for all -platforms, and can be used to customise the name of the executable, or disable -use entirely through ``EpsImagePlugin.gs_binary = False``. - -has_transparency_data -^^^^^^^^^^^^^^^^^^^^^ - -Images now have :py:attr:`~PIL.Image.Image.has_transparency_data` to indicate -whether the image has transparency data, whether in the form of an alpha -channel, a palette with an alpha channel, or a "transparency" key in the -:py:attr:`~PIL.Image.Image.info` dictionary. - -Even if this attribute is true, the image might still appear solid, if all of -the values shown within are opaque. - -ImageOps.cover -^^^^^^^^^^^^^^ - -Returns a resized version of the image, so that the requested size is covered, -while maintaining the original aspect ratio. - -See :ref:`relative-resize` for a comparison between this and similar ``ImageOps`` -methods. - -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 -drawn at chosen font sizes. - -The following ``size`` and ``font_size`` arguments can now be used to specify a -font size for this new builtin font:: - - ImageFont.load_default(size=24) - draw.text((0, 0), "test", font_size=24) - draw.textlength((0, 0), "test", font_size=24) - draw.textbbox((0, 0), "test", font_size=24) - draw.multiline_text((0, 0), "test", font_size=24) - draw.multiline_textbbox((0, 0), "test", font_size=24) - -Other Changes -============= - -Python 3.12 -^^^^^^^^^^^ - -Pillow 10.0.0 had wheels built against Python 3.12 beta, available as a preview to help -others prepare for 3.12, and to ensure Pillow could be used immediately at the release -of 3.12.0 final (2023-10-02, :pep:`693`). - -Pillow 10.1.0 now officially supports Python 3.12. - -Added support for DDS BC5U and 8-bit color indexed images -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Support has been added to read BC5U DDS files as RGB images, and -PALETTEINDEXED8 DDS files as P mode images. - -Support reading signed 8-bit YCbCr TIFF images -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -TIFF images with unsigned integer data, 8 bits per sample and a photometric -interpretation of YCbCr can now be read. +10.1.0 +------ + +API Changes +=========== + +Setting image mode +^^^^^^^^^^^^^^^^^^ + +If you attempt to set the mode of an image directly, e.g. +``im.mode = "RGBA"``, you will now receive an :py:exc:`AttributeError`. This is +not about removing existing functionality, but instead about raising an +explicit error to prevent later consequences. The ``convert`` method is the +correct way to change an image's mode. + +Accept a list in getpixel() +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:py:meth:`~PIL.Image.Image.getpixel` now accepts a list of coordinates, as well +as a tuple. :: + + from PIL import Image + im = Image.new("RGB", (1, 1)) + im.getpixel((0, 0)) + im.getpixel([0, 0]) + +BoxBlur and GaussianBlur allow for different x and y radii +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:py:class:`~PIL.ImageFilter.BoxBlur` and +:py:class:`~PIL.ImageFilter.GaussianBlur` now allow a sequence of x and y radii +to be specified, rather than a single number for both dimensions. :: + + from PIL import ImageFilter + ImageFilter.BoxBlur((2, 5)) + ImageFilter.GaussianBlur((2, 5)) + +API Additions +============= + +EpsImagePlugin.gs_binary +^^^^^^^^^^^^^^^^^^^^^^^^ + +``EpsImagePlugin.gs_windows_binary`` stores the name of the Ghostscript +executable on Windows. ``EpsImagePlugin.gs_binary`` has now been added for all +platforms, and can be used to customise the name of the executable, or disable +use entirely through ``EpsImagePlugin.gs_binary = False``. + +has_transparency_data +^^^^^^^^^^^^^^^^^^^^^ + +Images now have :py:attr:`~PIL.Image.Image.has_transparency_data` to indicate +whether the image has transparency data, whether in the form of an alpha +channel, a palette with an alpha channel, or a "transparency" key in the +:py:attr:`~PIL.Image.Image.info` dictionary. + +Even if this attribute is true, the image might still appear solid, if all of +the values shown within are opaque. + +ImageOps.cover +^^^^^^^^^^^^^^ + +Returns a resized version of the image, so that the requested size is covered, +while maintaining the original aspect ratio. + +See :ref:`relative-resize` for a comparison between this and similar ``ImageOps`` +methods. + +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 +drawn at chosen font sizes. + +The following ``size`` and ``font_size`` arguments can now be used to specify a +font size for this new builtin font:: + + ImageFont.load_default(size=24) + draw.text((0, 0), "test", font_size=24) + draw.textlength((0, 0), "test", font_size=24) + draw.textbbox((0, 0), "test", font_size=24) + draw.multiline_text((0, 0), "test", font_size=24) + draw.multiline_textbbox((0, 0), "test", font_size=24) + +Other Changes +============= + +Python 3.12 +^^^^^^^^^^^ + +Pillow 10.0.0 had wheels built against Python 3.12 beta, available as a preview to help +others prepare for 3.12, and to ensure Pillow could be used immediately at the release +of 3.12.0 final (2023-10-02, :pep:`693`). + +Pillow 10.1.0 now officially supports Python 3.12. + +Added support for DDS BC5U and 8-bit color indexed images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Support has been added to read BC5U DDS files as RGB images, and +PALETTEINDEXED8 DDS files as P mode images. + +Support reading signed 8-bit YCbCr TIFF images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +TIFF images with unsigned integer data, 8 bits per sample and a photometric +interpretation of YCbCr can now be read. diff --git a/docs/releasenotes/10.2.0.rst b/docs/releasenotes/10.2.0.rst index 1c6b78b08..ea0c73c28 100644 --- a/docs/releasenotes/10.2.0.rst +++ b/docs/releasenotes/10.2.0.rst @@ -1,161 +1,161 @@ -10.2.0 ------- - -Security -======== - -ImageFont.getmask: Applied ImageFont.MAX_STRING_LENGTH -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -To protect against potential DOS attacks when using arbitrary strings as text input, -Pillow will now raise a :py:exc:`ValueError` if the number of characters passed into -:py:meth:`PIL.ImageFont.ImageFont.getmask` is over a certain limit, -:py:data:`PIL.ImageFont.MAX_STRING_LENGTH`. - -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``. - -A decompression bomb check has also been added to -:py:meth:`PIL.ImageFont.ImageFont.getmask`. - -ImageFont.getmask: Trim glyph size -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -To protect against potential DOS attacks when using PIL fonts, -:py:class:`PIL.ImageFont.ImageFont` now trims the size of individual glyphs so that -they do not extend beyond the bitmap image. - -:cve:`2023-50447`: ImageMath.eval: Restricted environment keys -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -If an attacker has control over the keys passed to the -``environment`` argument of :py:meth:`!PIL.ImageMath.eval`, they may be able to execute -arbitrary code. To prevent this, keys matching the names of builtins and keys -containing double underscores will now raise a :py:exc:`ValueError`. - -Deprecations -============ - -ImageFile.raise_oserror -^^^^^^^^^^^^^^^^^^^^^^^ - -``ImageFile.raise_oserror()`` has been deprecated and will be removed in Pillow -12.0.0 (2025-10-15). The function is undocumented and is only useful for translating -error codes returned by a codec's ``decode()`` method, which ImageFile already does -automatically. - -IptcImageFile helper functions -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The functions ``IptcImageFile.dump`` and ``IptcImageFile.i``, and the constant -``IptcImageFile.PAD`` have been deprecated and will be removed in Pillow -12.0.0 (2025-10-15). These are undocumented helper functions intended -for internal use, so there is no replacement. They can each be replaced -by a single line of code using builtin functions in Python. - -API Changes -=========== - -Zero or negative font size error -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -When creating a :py:class:`~PIL.ImageFont.FreeTypeFont` instance, either directly or -through :py:func:`~PIL.ImageFont.truetype`, if the font size is zero or less, a -:py:exc:`ValueError` will now be raised. - -API Additions -============= - -Added DdsImagePlugin enums -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -:py:class:`~PIL.DdsImagePlugin.DDSD`, :py:class:`~PIL.DdsImagePlugin.DDSCAPS`, -:py:class:`~PIL.DdsImagePlugin.DDSCAPS2`, :py:class:`~PIL.DdsImagePlugin.DDPF`, -:py:class:`~PIL.DdsImagePlugin.DXGI_FORMAT` and :py:class:`~PIL.DdsImagePlugin.D3DFMT` -enums have been added to :py:class:`PIL.DdsImagePlugin`. - -JPEG RGB color space -^^^^^^^^^^^^^^^^^^^^ - -When saving JPEG files, ``keep_rgb`` can now be set to ``True``. This will store RGB -images in the RGB color space instead of being converted to YCbCr automatically by -libjpeg. When this option is enabled, attempting to chroma-subsample RGB images with -the ``subsampling`` option will raise an :py:exc:`OSError`. - -JPEG restart marker interval -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -When saving JPEG files, ``restart_marker_blocks`` and ``restart_marker_rows`` can now -be used to emit restart markers whenever the specified number of MCU blocks or rows -have been produced. - -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 -============= - -Added DDS BC4U and DX10 BC1 and BC4 reading -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Support has been added to read the BC4U format of DDS images. - -Support has also been added to read DX10 BC1 and BC4, whether UNORM or -TYPELESS. - -Support arbitrary masks for uncompressed RGB DDS images -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -All masks are now supported when reading DDS images with uncompressed RGB data, -allowing for bit counts other than 24 and 32. - -Saving TIFF tag RowsPerStrip -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -When saving TIFF images, the TIFF tag RowsPerStrip can now be one of the tags set by -the user, rather than always being calculated by Pillow. - -Optimized ImageColor.getrgb and getcolor -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The color calculations of :py:attr:`~PIL.ImageColor.getrgb` and -:py:attr:`~PIL.ImageColor.getcolor` are now cached using -:py:func:`functools.lru_cache`. Cached calls of ``getrgb`` are 3.1 - 91.4 times -as fast and ``getcolor`` are 5.1 - 19.6 times as fast. - -Optimized ImageMode.getmode -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The lookups made by :py:attr:`~PIL.ImageMode.getmode` are now cached using -:py:func:`functools.lru_cache` instead of a custom cache. Cached calls are 1.2 times as -fast. - -Optimized ImageStat.Stat count and extrema -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Calculating the :py:attr:`~PIL.ImageStat.Stat.count` and -:py:attr:`~PIL.ImageStat.Stat.extrema` statistics is now faster. After the -histogram is created in ``st = ImageStat.Stat(im)``, ``st.count`` is 3 times as fast on -average and ``st.extrema`` is 12 times as fast on average. - -Encoder errors now report error detail as string -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -:py:exc:`OSError` exceptions from image encoders now include a textual description of -the error instead of a numeric error code. - -Type hints -^^^^^^^^^^ - -Work has begun to add type annotations to Pillow, including: - -* :py:mod:`~PIL.ContainerIO` -* :py:mod:`~PIL.FontFile`, :py:mod:`~PIL.BdfFontFile` and :py:mod:`~PIL.PcfFontFile` -* :py:mod:`~PIL.ImageChops` -* :py:mod:`~PIL.ImageMode` -* :py:mod:`~PIL.ImageSequence` -* :py:mod:`~PIL.ImageTransform` -* :py:mod:`~PIL.TarIO` +10.2.0 +------ + +Security +======== + +ImageFont.getmask: Applied ImageFont.MAX_STRING_LENGTH +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To protect against potential DOS attacks when using arbitrary strings as text input, +Pillow will now raise a :py:exc:`ValueError` if the number of characters passed into +:py:meth:`PIL.ImageFont.ImageFont.getmask` is over a certain limit, +:py:data:`PIL.ImageFont.MAX_STRING_LENGTH`. + +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``. + +A decompression bomb check has also been added to +:py:meth:`PIL.ImageFont.ImageFont.getmask`. + +ImageFont.getmask: Trim glyph size +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To protect against potential DOS attacks when using PIL fonts, +:py:class:`PIL.ImageFont.ImageFont` now trims the size of individual glyphs so that +they do not extend beyond the bitmap image. + +:cve:`2023-50447`: ImageMath.eval: Restricted environment keys +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If an attacker has control over the keys passed to the +``environment`` argument of :py:meth:`!PIL.ImageMath.eval`, they may be able to execute +arbitrary code. To prevent this, keys matching the names of builtins and keys +containing double underscores will now raise a :py:exc:`ValueError`. + +Deprecations +============ + +ImageFile.raise_oserror +^^^^^^^^^^^^^^^^^^^^^^^ + +``ImageFile.raise_oserror()`` has been deprecated and will be removed in Pillow +12.0.0 (2025-10-15). The function is undocumented and is only useful for translating +error codes returned by a codec's ``decode()`` method, which ImageFile already does +automatically. + +IptcImageFile helper functions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The functions ``IptcImageFile.dump`` and ``IptcImageFile.i``, and the constant +``IptcImageFile.PAD`` have been deprecated and will be removed in Pillow +12.0.0 (2025-10-15). These are undocumented helper functions intended +for internal use, so there is no replacement. They can each be replaced +by a single line of code using builtin functions in Python. + +API Changes +=========== + +Zero or negative font size error +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When creating a :py:class:`~PIL.ImageFont.FreeTypeFont` instance, either directly or +through :py:func:`~PIL.ImageFont.truetype`, if the font size is zero or less, a +:py:exc:`ValueError` will now be raised. + +API Additions +============= + +Added DdsImagePlugin enums +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:py:class:`~PIL.DdsImagePlugin.DDSD`, :py:class:`~PIL.DdsImagePlugin.DDSCAPS`, +:py:class:`~PIL.DdsImagePlugin.DDSCAPS2`, :py:class:`~PIL.DdsImagePlugin.DDPF`, +:py:class:`~PIL.DdsImagePlugin.DXGI_FORMAT` and :py:class:`~PIL.DdsImagePlugin.D3DFMT` +enums have been added to :py:class:`PIL.DdsImagePlugin`. + +JPEG RGB color space +^^^^^^^^^^^^^^^^^^^^ + +When saving JPEG files, ``keep_rgb`` can now be set to ``True``. This will store RGB +images in the RGB color space instead of being converted to YCbCr automatically by +libjpeg. When this option is enabled, attempting to chroma-subsample RGB images with +the ``subsampling`` option will raise an :py:exc:`OSError`. + +JPEG restart marker interval +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When saving JPEG files, ``restart_marker_blocks`` and ``restart_marker_rows`` can now +be used to emit restart markers whenever the specified number of MCU blocks or rows +have been produced. + +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 +============= + +Added DDS BC4U and DX10 BC1 and BC4 reading +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Support has been added to read the BC4U format of DDS images. + +Support has also been added to read DX10 BC1 and BC4, whether UNORM or +TYPELESS. + +Support arbitrary masks for uncompressed RGB DDS images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +All masks are now supported when reading DDS images with uncompressed RGB data, +allowing for bit counts other than 24 and 32. + +Saving TIFF tag RowsPerStrip +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When saving TIFF images, the TIFF tag RowsPerStrip can now be one of the tags set by +the user, rather than always being calculated by Pillow. + +Optimized ImageColor.getrgb and getcolor +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The color calculations of :py:attr:`~PIL.ImageColor.getrgb` and +:py:attr:`~PIL.ImageColor.getcolor` are now cached using +:py:func:`functools.lru_cache`. Cached calls of ``getrgb`` are 3.1 - 91.4 times +as fast and ``getcolor`` are 5.1 - 19.6 times as fast. + +Optimized ImageMode.getmode +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The lookups made by :py:attr:`~PIL.ImageMode.getmode` are now cached using +:py:func:`functools.lru_cache` instead of a custom cache. Cached calls are 1.2 times as +fast. + +Optimized ImageStat.Stat count and extrema +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Calculating the :py:attr:`~PIL.ImageStat.Stat.count` and +:py:attr:`~PIL.ImageStat.Stat.extrema` statistics is now faster. After the +histogram is created in ``st = ImageStat.Stat(im)``, ``st.count`` is 3 times as fast on +average and ``st.extrema`` is 12 times as fast on average. + +Encoder errors now report error detail as string +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:py:exc:`OSError` exceptions from image encoders now include a textual description of +the error instead of a numeric error code. + +Type hints +^^^^^^^^^^ + +Work has begun to add type annotations to Pillow, including: + +* :py:mod:`~PIL.ContainerIO` +* :py:mod:`~PIL.FontFile`, :py:mod:`~PIL.BdfFontFile` and :py:mod:`~PIL.PcfFontFile` +* :py:mod:`~PIL.ImageChops` +* :py:mod:`~PIL.ImageMode` +* :py:mod:`~PIL.ImageSequence` +* :py:mod:`~PIL.ImageTransform` +* :py:mod:`~PIL.TarIO` diff --git a/docs/releasenotes/10.3.0.rst b/docs/releasenotes/10.3.0.rst index 2f0437d94..edac0ae76 100644 --- a/docs/releasenotes/10.3.0.rst +++ b/docs/releasenotes/10.3.0.rst @@ -1,119 +1,119 @@ -10.3.0 ------- - -Security -======== - -ImageMath eval() -^^^^^^^^^^^^^^^^ - -.. danger:: - ``ImageMath.eval()`` uses Python's ``eval()`` function to process the expression - string, and carries the security risks of doing so. A direct replacement for this is - the new :py:meth:`~PIL.ImageMath.unsafe_eval`, but that carries the same risks. It is - not recommended to process expressions without considering this. - :py:meth:`~PIL.ImageMath.lambda_eval` is a more secure alternative. - -:cve:`2024-28219`: Fix buffer overflow in ``_imagingcms.c`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -In ``_imagingcms.c``, two ``strcpy`` calls were able to copy too much data into fixed -length strings. This has been fixed by using ``strncpy`` instead. - -Deprecations -============ - -ImageCms constants and versions() function -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -A number of constants and a function in :py:mod:`.ImageCms` have been deprecated. -This includes a table of flags based on LittleCMS version 1 which has been replaced -with a new class :py:class:`.ImageCms.Flags` based on LittleCMS 2 flags. - -============================================ ==================================================== -Deprecated Use instead -============================================ ==================================================== -``ImageCms.DESCRIPTION`` No replacement -``ImageCms.VERSION`` ``PIL.__version__`` -``ImageCms.FLAGS["MATRIXINPUT"]`` :py:attr:`.ImageCms.Flags.CLUT_POST_LINEARIZATION` -``ImageCms.FLAGS["MATRIXOUTPUT"]`` :py:attr:`.ImageCms.Flags.FORCE_CLUT` -``ImageCms.FLAGS["MATRIXONLY"]`` No replacement -``ImageCms.FLAGS["NOWHITEONWHITEFIXUP"]`` :py:attr:`.ImageCms.Flags.NOWHITEONWHITEFIXUP` -``ImageCms.FLAGS["NOPRELINEARIZATION"]`` :py:attr:`.ImageCms.Flags.CLUT_PRE_LINEARIZATION` -``ImageCms.FLAGS["GUESSDEVICECLASS"]`` :py:attr:`.ImageCms.Flags.GUESSDEVICECLASS` -``ImageCms.FLAGS["NOTCACHE"]`` :py:attr:`.ImageCms.Flags.NOCACHE` -``ImageCms.FLAGS["NOTPRECALC"]`` :py:attr:`.ImageCms.Flags.NOOPTIMIZE` -``ImageCms.FLAGS["NULLTRANSFORM"]`` :py:attr:`.ImageCms.Flags.NULLTRANSFORM` -``ImageCms.FLAGS["HIGHRESPRECALC"]`` :py:attr:`.ImageCms.Flags.HIGHRESPRECALC` -``ImageCms.FLAGS["LOWRESPRECALC"]`` :py:attr:`.ImageCms.Flags.LOWRESPRECALC` -``ImageCms.FLAGS["GAMUTCHECK"]`` :py:attr:`.ImageCms.Flags.GAMUTCHECK` -``ImageCms.FLAGS["WHITEBLACKCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` -``ImageCms.FLAGS["BLACKPOINTCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` -``ImageCms.FLAGS["SOFTPROOFING"]`` :py:attr:`.ImageCms.Flags.SOFTPROOFING` -``ImageCms.FLAGS["PRESERVEBLACK"]`` :py:attr:`.ImageCms.Flags.NONEGATIVES` -``ImageCms.FLAGS["NODEFAULTRESOURCEDEF"]`` :py:attr:`.ImageCms.Flags.NODEFAULTRESOURCEDEF` -``ImageCms.FLAGS["GRIDPOINTS"]`` :py:attr:`.ImageCms.Flags.GRIDPOINTS()` -``ImageCms.versions()`` :py:func:`PIL.features.version_module` with - ``feature="littlecms2"``, :py:data:`sys.version` or - :py:data:`sys.version_info`, and ``PIL.__version__`` -============================================ ==================================================== - -ImageMath.eval() -^^^^^^^^^^^^^^^^ - -``ImageMath.eval()`` has been deprecated. Use :py:meth:`~PIL.ImageMath.lambda_eval` or -:py:meth:`~PIL.ImageMath.unsafe_eval` instead. See earlier security notes for more -information. - -API Changes -=========== - -Added alpha_quality argument when saving WebP images -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -When saving WebP images, an ``alpha_quality`` argument can be passed to the encoder. It -is an integer value between 0 to 100, where values other than 100 will provide lossy -compression. - -Negative kmeans error -^^^^^^^^^^^^^^^^^^^^^ - -When calling :py:meth:`~PIL.Image.Image.quantize`, a negative ``kmeans`` will now -raise a :py:exc:`ValueError`, unless a palette is supplied to make the value redundant. - -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 -============= - -Added PerspectiveTransform -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -:py:class:`~PIL.ImageTransform.PerspectiveTransform` has been added, meaning -that all of the :py:data:`~PIL.Image.Transform` values now have a corresponding -subclass of :py:class:`~PIL.ImageTransform.Transform`. - -Other Changes -============= - -Portable FloatMap (PFM) images -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Support has been added for reading and writing grayscale (Pf format) -Portable FloatMap (PFM) files containing ``F`` data. - -Release GIL when fetching WebP frames -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Python's Global Interpreter Lock is now released when fetching WebP frames from -the libwebp decoder. - -Type hints -^^^^^^^^^^ - -Pillow now has type hints for a large part of its modules, and the package -includes a ``py.typed`` file and the ``Typing :: Typed`` Trove classifier. +10.3.0 +------ + +Security +======== + +ImageMath eval() +^^^^^^^^^^^^^^^^ + +.. danger:: + ``ImageMath.eval()`` uses Python's ``eval()`` function to process the expression + string, and carries the security risks of doing so. A direct replacement for this is + the new :py:meth:`~PIL.ImageMath.unsafe_eval`, but that carries the same risks. It is + not recommended to process expressions without considering this. + :py:meth:`~PIL.ImageMath.lambda_eval` is a more secure alternative. + +:cve:`2024-28219`: Fix buffer overflow in ``_imagingcms.c`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In ``_imagingcms.c``, two ``strcpy`` calls were able to copy too much data into fixed +length strings. This has been fixed by using ``strncpy`` instead. + +Deprecations +============ + +ImageCms constants and versions() function +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +A number of constants and a function in :py:mod:`.ImageCms` have been deprecated. +This includes a table of flags based on LittleCMS version 1 which has been replaced +with a new class :py:class:`.ImageCms.Flags` based on LittleCMS 2 flags. + +============================================ ==================================================== +Deprecated Use instead +============================================ ==================================================== +``ImageCms.DESCRIPTION`` No replacement +``ImageCms.VERSION`` ``PIL.__version__`` +``ImageCms.FLAGS["MATRIXINPUT"]`` :py:attr:`.ImageCms.Flags.CLUT_POST_LINEARIZATION` +``ImageCms.FLAGS["MATRIXOUTPUT"]`` :py:attr:`.ImageCms.Flags.FORCE_CLUT` +``ImageCms.FLAGS["MATRIXONLY"]`` No replacement +``ImageCms.FLAGS["NOWHITEONWHITEFIXUP"]`` :py:attr:`.ImageCms.Flags.NOWHITEONWHITEFIXUP` +``ImageCms.FLAGS["NOPRELINEARIZATION"]`` :py:attr:`.ImageCms.Flags.CLUT_PRE_LINEARIZATION` +``ImageCms.FLAGS["GUESSDEVICECLASS"]`` :py:attr:`.ImageCms.Flags.GUESSDEVICECLASS` +``ImageCms.FLAGS["NOTCACHE"]`` :py:attr:`.ImageCms.Flags.NOCACHE` +``ImageCms.FLAGS["NOTPRECALC"]`` :py:attr:`.ImageCms.Flags.NOOPTIMIZE` +``ImageCms.FLAGS["NULLTRANSFORM"]`` :py:attr:`.ImageCms.Flags.NULLTRANSFORM` +``ImageCms.FLAGS["HIGHRESPRECALC"]`` :py:attr:`.ImageCms.Flags.HIGHRESPRECALC` +``ImageCms.FLAGS["LOWRESPRECALC"]`` :py:attr:`.ImageCms.Flags.LOWRESPRECALC` +``ImageCms.FLAGS["GAMUTCHECK"]`` :py:attr:`.ImageCms.Flags.GAMUTCHECK` +``ImageCms.FLAGS["WHITEBLACKCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` +``ImageCms.FLAGS["BLACKPOINTCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` +``ImageCms.FLAGS["SOFTPROOFING"]`` :py:attr:`.ImageCms.Flags.SOFTPROOFING` +``ImageCms.FLAGS["PRESERVEBLACK"]`` :py:attr:`.ImageCms.Flags.NONEGATIVES` +``ImageCms.FLAGS["NODEFAULTRESOURCEDEF"]`` :py:attr:`.ImageCms.Flags.NODEFAULTRESOURCEDEF` +``ImageCms.FLAGS["GRIDPOINTS"]`` :py:attr:`.ImageCms.Flags.GRIDPOINTS()` +``ImageCms.versions()`` :py:func:`PIL.features.version_module` with + ``feature="littlecms2"``, :py:data:`sys.version` or + :py:data:`sys.version_info`, and ``PIL.__version__`` +============================================ ==================================================== + +ImageMath.eval() +^^^^^^^^^^^^^^^^ + +``ImageMath.eval()`` has been deprecated. Use :py:meth:`~PIL.ImageMath.lambda_eval` or +:py:meth:`~PIL.ImageMath.unsafe_eval` instead. See earlier security notes for more +information. + +API Changes +=========== + +Added alpha_quality argument when saving WebP images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When saving WebP images, an ``alpha_quality`` argument can be passed to the encoder. It +is an integer value between 0 to 100, where values other than 100 will provide lossy +compression. + +Negative kmeans error +^^^^^^^^^^^^^^^^^^^^^ + +When calling :py:meth:`~PIL.Image.Image.quantize`, a negative ``kmeans`` will now +raise a :py:exc:`ValueError`, unless a palette is supplied to make the value redundant. + +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 +============= + +Added PerspectiveTransform +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:py:class:`~PIL.ImageTransform.PerspectiveTransform` has been added, meaning +that all of the :py:data:`~PIL.Image.Transform` values now have a corresponding +subclass of :py:class:`~PIL.ImageTransform.Transform`. + +Other Changes +============= + +Portable FloatMap (PFM) images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Support has been added for reading and writing grayscale (Pf format) +Portable FloatMap (PFM) files containing ``F`` data. + +Release GIL when fetching WebP frames +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Python's Global Interpreter Lock is now released when fetching WebP frames from +the libwebp decoder. + +Type hints +^^^^^^^^^^ + +Pillow now has type hints for a large part of its modules, and the package +includes a ``py.typed`` file and the ``Typing :: Typed`` Trove classifier. diff --git a/docs/releasenotes/10.4.0.rst b/docs/releasenotes/10.4.0.rst index 44727efd4..02b010788 100644 --- a/docs/releasenotes/10.4.0.rst +++ b/docs/releasenotes/10.4.0.rst @@ -1,73 +1,73 @@ -10.4.0 ------- - -Security -======== - -TODO -^^^^ - -TODO - -:cve:`YYYY-XXXXX`: TODO -^^^^^^^^^^^^^^^^^^^^^^^ - -TODO - -Backwards Incompatible Changes -============================== - -TODO -^^^^ - -Deprecations -============ - -BGR;15, BGR 16 and BGR;24 -^^^^^^^^^^^^^^^^^^^^^^^^^ - -The experimental BGR;15, BGR;16 and BGR;24 modes have been deprecated. - -Support for LibTIFF earlier than 4 -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Support for LibTIFF earlier than version 4 has been deprecated. -Upgrade to a newer version of LibTIFF instead. - -ImageDraw.getdraw hints parameter -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been deprecated. - -API Changes -=========== - -TODO -^^^^ - -TODO - -API Additions -============= - -ImageDraw.circle -^^^^^^^^^^^^^^^^ - -Added :py:meth:`~PIL.ImageDraw.ImageDraw.circle`. It provides the same functionality as -:py:meth:`~PIL.ImageDraw.ImageDraw.ellipse`, but instead of taking a bounding box, it -takes a center point and radius. - -TODO -^^^^ - -TODO - -Other Changes -============= - -Python 3.13 beta -^^^^^^^^^^^^^^^^ - -To help others prepare for Python 3.13, wheels have been built against the 3.13 beta as -a preview. This is not official support for Python 3.13, but simply an opportunity for -users to test how Pillow works with the beta and report any problems. +10.4.0 +------ + +Security +======== + +TODO +^^^^ + +TODO + +:cve:`YYYY-XXXXX`: TODO +^^^^^^^^^^^^^^^^^^^^^^^ + +TODO + +Backwards Incompatible Changes +============================== + +TODO +^^^^ + +Deprecations +============ + +BGR;15, BGR 16 and BGR;24 +^^^^^^^^^^^^^^^^^^^^^^^^^ + +The experimental BGR;15, BGR;16 and BGR;24 modes have been deprecated. + +Support for LibTIFF earlier than 4 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Support for LibTIFF earlier than version 4 has been deprecated. +Upgrade to a newer version of LibTIFF instead. + +ImageDraw.getdraw hints parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been deprecated. + +API Changes +=========== + +TODO +^^^^ + +TODO + +API Additions +============= + +ImageDraw.circle +^^^^^^^^^^^^^^^^ + +Added :py:meth:`~PIL.ImageDraw.ImageDraw.circle`. It provides the same functionality as +:py:meth:`~PIL.ImageDraw.ImageDraw.ellipse`, but instead of taking a bounding box, it +takes a center point and radius. + +TODO +^^^^ + +TODO + +Other Changes +============= + +Python 3.13 beta +^^^^^^^^^^^^^^^^ + +To help others prepare for Python 3.13, wheels have been built against the 3.13 beta as +a preview. This is not official support for Python 3.13, but simply an opportunity for +users to test how Pillow works with the beta and report any problems. diff --git a/docs/releasenotes/2.3.1.rst b/docs/releasenotes/2.3.1.rst index e54065a0b..aead699d6 100644 --- a/docs/releasenotes/2.3.1.rst +++ b/docs/releasenotes/2.3.1.rst @@ -1,26 +1,26 @@ -2.3.1 ------ - -Security -======== - -These issues were reported in -`Debian bug #737059 `_. - -:cve:`2014-1932`: Fix insecure use of :py:func:`tempfile.mktemp` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The (1) ``load_djpeg`` function in ``JpegImagePlugin.py``, (2) Ghostscript function -in ``EpsImagePlugin.py``, (3) ``load`` function in ``IptcImagePlugin.py``, and (4) -``_copy`` function in ``Image.py`` in -Pillow before 2.3.1 do not properly create temporary files, which allow -local users to overwrite arbitrary files and obtain sensitive information via a -symlink attack on the temporary file. - -:cve:`2014-1933`: Fix insecure use of :py:func:`tempfile.mktemp` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The (1) ``JpegImagePlugin.py`` and (2) ``EpsImagePlugin.py`` scripts in -Pillow before 2.3.1 uses the names of -temporary files on the command line, which makes it easier for local users to -conduct symlink attacks by listing the processes. +2.3.1 +----- + +Security +======== + +These issues were reported in +`Debian bug #737059 `_. + +:cve:`2014-1932`: Fix insecure use of :py:func:`tempfile.mktemp` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The (1) ``load_djpeg`` function in ``JpegImagePlugin.py``, (2) Ghostscript function +in ``EpsImagePlugin.py``, (3) ``load`` function in ``IptcImagePlugin.py``, and (4) +``_copy`` function in ``Image.py`` in +Pillow before 2.3.1 do not properly create temporary files, which allow +local users to overwrite arbitrary files and obtain sensitive information via a +symlink attack on the temporary file. + +:cve:`2014-1933`: Fix insecure use of :py:func:`tempfile.mktemp` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The (1) ``JpegImagePlugin.py`` and (2) ``EpsImagePlugin.py`` scripts in +Pillow before 2.3.1 uses the names of +temporary files on the command line, which makes it easier for local users to +conduct symlink attacks by listing the processes. diff --git a/docs/releasenotes/2.3.2.rst b/docs/releasenotes/2.3.2.rst index c4504ee33..086ef100f 100644 --- a/docs/releasenotes/2.3.2.rst +++ b/docs/releasenotes/2.3.2.rst @@ -1,14 +1,14 @@ -2.3.2 ------ - -Security -======== - -:cve:`2014-3589`: Fix DOS attack -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -``PIL/IcnsImagePlugin.py`` in Pillow before 2.3.2 and -2.5.x before 2.5.2 allows remote attackers to cause a denial of service via a crafted -block size. - -Found and reported by Andrew Drake of `Dropbox `__. +2.3.2 +----- + +Security +======== + +:cve:`2014-3589`: Fix DOS attack +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``PIL/IcnsImagePlugin.py`` in Pillow before 2.3.2 and +2.5.x before 2.5.2 allows remote attackers to cause a denial of service via a crafted +block size. + +Found and reported by Andrew Drake of `Dropbox `__. diff --git a/docs/releasenotes/2.5.2.rst b/docs/releasenotes/2.5.2.rst index a80b460a8..7cdd4e060 100644 --- a/docs/releasenotes/2.5.2.rst +++ b/docs/releasenotes/2.5.2.rst @@ -1,14 +1,14 @@ -2.5.2 ------ - -Security -======== - -:cve:`2014-3589`: Fix DOS attack -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -``PIL/IcnsImagePlugin.py`` in Pillow before 2.3.2 and -2.5.x before 2.5.2 allows remote attackers to cause a denial of service via a crafted -block size. - -Found and reported by Andrew Drake of `Dropbox `__. +2.5.2 +----- + +Security +======== + +:cve:`2014-3589`: Fix DOS attack +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``PIL/IcnsImagePlugin.py`` in Pillow before 2.3.2 and +2.5.x before 2.5.2 allows remote attackers to cause a denial of service via a crafted +block size. + +Found and reported by Andrew Drake of `Dropbox `__. diff --git a/docs/releasenotes/2.6.0.rst b/docs/releasenotes/2.6.0.rst index 84b0016d2..62582b32c 100644 --- a/docs/releasenotes/2.6.0.rst +++ b/docs/releasenotes/2.6.0.rst @@ -1,14 +1,14 @@ -2.6.0 ------ - -Security -======== - -:cve:`2014-3589`: Fix DOS attack -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -``PIL/IcnsImagePlugin.py`` in Pillow before 2.3.2 and -2.5.x before 2.5.2 allows remote attackers to cause a denial of service via a crafted -block size. - -Found and reported by Andrew Drake of `Dropbox `__. +2.6.0 +----- + +Security +======== + +:cve:`2014-3589`: Fix DOS attack +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``PIL/IcnsImagePlugin.py`` in Pillow before 2.3.2 and +2.5.x before 2.5.2 allows remote attackers to cause a denial of service via a crafted +block size. + +Found and reported by Andrew Drake of `Dropbox `__. diff --git a/docs/releasenotes/2.7.0.rst b/docs/releasenotes/2.7.0.rst index e9b0995bb..5ad53f824 100644 --- a/docs/releasenotes/2.7.0.rst +++ b/docs/releasenotes/2.7.0.rst @@ -1,165 +1,165 @@ -2.7.0 ------ - -Sane Plugin -^^^^^^^^^^^ - -The Sane plugin has now been split into its own repo: -https://github.com/python-pillow/Sane . - -Png text chunk size limits -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -To prevent potential denial of service attacks using compressed text -chunks, there are now limits to the decompressed size of text chunks -decoded from PNG images. If the limits are exceeded when opening a PNG -image a :py:exc:`ValueError` will be raised. - -Individual text chunks are limited to -:py:attr:`PIL.PngImagePlugin.MAX_TEXT_CHUNK`, set to 1MB by -default. The total decompressed size of all text chunks is limited to -:py:attr:`PIL.PngImagePlugin.MAX_TEXT_MEMORY`, which defaults to -64MB. These values can be changed prior to opening PNG images if you -know that there are large text blocks that are desired. - -Image resizing filters -^^^^^^^^^^^^^^^^^^^^^^ - -Image resizing methods :py:meth:`~PIL.Image.Image.resize` and -:py:meth:`~PIL.Image.Image.thumbnail` take a ``resample`` argument, which tells -which filter should be used for resampling. Possible values are: -``NEAREST``, ``BILINEAR``, ``BICUBIC`` and ``ANTIALIAS``. Almost all of them -were changed in this version. - -Bicubic and bilinear downscaling -++++++++++++++++++++++++++++++++ - -From the beginning ``BILINEAR`` and ``BICUBIC`` filters were based on affine -transformations and used a fixed number of pixels from the source image for -every destination pixel (2x2 pixels for ``BILINEAR`` and 4x4 for ``BICUBIC``). -This gave an unsatisfactory result for downscaling. At the same time, a high -quality convolutions-based algorithm with flexible kernel was used for -``ANTIALIAS`` filter. - -Starting from Pillow 2.7.0, a high quality convolutions-based algorithm is used -for all of these three filters. - -If you have previously used any tricks to maintain quality when downscaling with -``BILINEAR`` and ``BICUBIC`` filters (for example, reducing within several -steps), they are unnecessary now. - -Antialias renamed to Lanczos -++++++++++++++++++++++++++++ - -A new ``LANCZOS`` constant was added instead of ``ANTIALIAS``. - -When ``ANTIALIAS`` was initially added, it was the only high-quality filter -based on convolutions. It's name was supposed to reflect this. Starting from -Pillow 2.7.0 all resize method are based on convolutions. All of them are -antialias from now on. And the real name of the ``ANTIALIAS`` filter is Lanczos -filter. - -The ``ANTIALIAS`` constant is left for backward compatibility and is an alias -for ``LANCZOS``. - -Lanczos upscaling quality -+++++++++++++++++++++++++ - -The image upscaling quality with ``LANCZOS`` filter was almost the same as -``BILINEAR`` due to a bug. This has been fixed. - -Bicubic upscaling quality -+++++++++++++++++++++++++ - -The ``BICUBIC`` filter for affine transformations produced sharp, slightly -pixelated image for upscaling. Bicubic for convolutions is more soft. - -Resize performance -++++++++++++++++++ - -In most cases, convolution is more a expensive algorithm for downscaling -because it takes into account all the pixels of source image. Therefore -``BILINEAR`` and ``BICUBIC`` filters' performance can be lower than before. -On the other hand the quality of ``BILINEAR`` and ``BICUBIC`` was close to -``NEAREST``. So if such quality is suitable for your tasks you can switch to -``NEAREST`` filter for downscaling, which will give a huge improvement in -performance. - -At the same time performance of convolution resampling for downscaling has been -improved by around a factor of two compared to the previous version. -The upscaling performance of the ``LANCZOS`` filter has remained the same. For -``BILINEAR`` filter it has improved by 1.5 times and for ``BICUBIC`` by four -times. - -Default filter for thumbnails -+++++++++++++++++++++++++++++ - -In Pillow 2.5 the default filter for :py:meth:`~PIL.Image.Image.thumbnail` was -changed from ``NEAREST`` to ``ANTIALIAS``. Antialias was chosen because all the -other filters gave poor quality for reduction. Starting from Pillow 2.7.0, -``ANTIALIAS`` has been replaced with ``BICUBIC``, because it's faster and -``ANTIALIAS`` doesn't give any advantages after downscaling with libjpeg, which -uses supersampling internally, not convolutions. - -Image transposition -+++++++++++++++++++ - -A new method ``TRANSPOSE`` has been added for the -:py:meth:`~PIL.Image.Image.transpose` operation in addition to -``FLIP_LEFT_RIGHT``, ``FLIP_TOP_BOTTOM``, ``ROTATE_90``, ``ROTATE_180``, -``ROTATE_270``. ``TRANSPOSE`` is an algebra transpose, with an image reflected -across its main diagonal. - -The speed of ``ROTATE_90``, ``ROTATE_270`` and ``TRANSPOSE`` has been significantly -improved for large images which don't fit in the processor cache. - -Gaussian blur and unsharp mask -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The :py:meth:`~PIL.ImageFilter.GaussianBlur` implementation has been replaced -with a sequential application of box filters. The new implementation is based on -"Theoretical foundations of Gaussian convolution by extended box filtering" from -the Mathematical Image Analysis Group. As :py:meth:`~PIL.ImageFilter.UnsharpMask` -implementations use Gaussian blur internally, all changes from this chapter -are also applicable to it. - -Blur radius -+++++++++++ - -There was an error in the previous version of Pillow, where blur radius (the -standard deviation of Gaussian) actually meant blur diameter. For example, to -blur an image with actual radius 5 you were forced to use value 10. This has -been fixed. Now the meaning of the radius is the same as in other software. - -If you used a Gaussian blur with some radius value, you need to divide this -value by two. - -Blur performance -++++++++++++++++ - -Box filter computation time is constant relative to the radius and depends -on source image size only. Because the new Gaussian blur implementation -is based on box filter, its computation time also doesn't depend on the blur -radius. - -For example, previously, if the execution time for a given test image was 1 -second for radius 1, 3.6 seconds for radius 10 and 17 seconds for 50, now blur -with any radius on same image is executed for 0.2 seconds. - -Blur quality -++++++++++++ - -The previous implementation takes into account only source pixels within -2 * standard deviation radius for every destination pixel. This was not enough, -so the quality was worse compared to other Gaussian blur software. - -The new implementation does not have this drawback. - -TIFF Parameter Changes -^^^^^^^^^^^^^^^^^^^^^^ - -Several kwarg parameters for saving TIFF images were previously -specified as strings with included spaces (e.g. 'x resolution'). This -was difficult to use as kwargs without constructing and passing a -dictionary. These parameters now use the underscore character instead -of space. (e.g. 'x_resolution') +2.7.0 +----- + +Sane Plugin +^^^^^^^^^^^ + +The Sane plugin has now been split into its own repo: +https://github.com/python-pillow/Sane . + +Png text chunk size limits +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To prevent potential denial of service attacks using compressed text +chunks, there are now limits to the decompressed size of text chunks +decoded from PNG images. If the limits are exceeded when opening a PNG +image a :py:exc:`ValueError` will be raised. + +Individual text chunks are limited to +:py:attr:`PIL.PngImagePlugin.MAX_TEXT_CHUNK`, set to 1MB by +default. The total decompressed size of all text chunks is limited to +:py:attr:`PIL.PngImagePlugin.MAX_TEXT_MEMORY`, which defaults to +64MB. These values can be changed prior to opening PNG images if you +know that there are large text blocks that are desired. + +Image resizing filters +^^^^^^^^^^^^^^^^^^^^^^ + +Image resizing methods :py:meth:`~PIL.Image.Image.resize` and +:py:meth:`~PIL.Image.Image.thumbnail` take a ``resample`` argument, which tells +which filter should be used for resampling. Possible values are: +``NEAREST``, ``BILINEAR``, ``BICUBIC`` and ``ANTIALIAS``. Almost all of them +were changed in this version. + +Bicubic and bilinear downscaling +++++++++++++++++++++++++++++++++ + +From the beginning ``BILINEAR`` and ``BICUBIC`` filters were based on affine +transformations and used a fixed number of pixels from the source image for +every destination pixel (2x2 pixels for ``BILINEAR`` and 4x4 for ``BICUBIC``). +This gave an unsatisfactory result for downscaling. At the same time, a high +quality convolutions-based algorithm with flexible kernel was used for +``ANTIALIAS`` filter. + +Starting from Pillow 2.7.0, a high quality convolutions-based algorithm is used +for all of these three filters. + +If you have previously used any tricks to maintain quality when downscaling with +``BILINEAR`` and ``BICUBIC`` filters (for example, reducing within several +steps), they are unnecessary now. + +Antialias renamed to Lanczos +++++++++++++++++++++++++++++ + +A new ``LANCZOS`` constant was added instead of ``ANTIALIAS``. + +When ``ANTIALIAS`` was initially added, it was the only high-quality filter +based on convolutions. It's name was supposed to reflect this. Starting from +Pillow 2.7.0 all resize method are based on convolutions. All of them are +antialias from now on. And the real name of the ``ANTIALIAS`` filter is Lanczos +filter. + +The ``ANTIALIAS`` constant is left for backward compatibility and is an alias +for ``LANCZOS``. + +Lanczos upscaling quality ++++++++++++++++++++++++++ + +The image upscaling quality with ``LANCZOS`` filter was almost the same as +``BILINEAR`` due to a bug. This has been fixed. + +Bicubic upscaling quality ++++++++++++++++++++++++++ + +The ``BICUBIC`` filter for affine transformations produced sharp, slightly +pixelated image for upscaling. Bicubic for convolutions is more soft. + +Resize performance +++++++++++++++++++ + +In most cases, convolution is more a expensive algorithm for downscaling +because it takes into account all the pixels of source image. Therefore +``BILINEAR`` and ``BICUBIC`` filters' performance can be lower than before. +On the other hand the quality of ``BILINEAR`` and ``BICUBIC`` was close to +``NEAREST``. So if such quality is suitable for your tasks you can switch to +``NEAREST`` filter for downscaling, which will give a huge improvement in +performance. + +At the same time performance of convolution resampling for downscaling has been +improved by around a factor of two compared to the previous version. +The upscaling performance of the ``LANCZOS`` filter has remained the same. For +``BILINEAR`` filter it has improved by 1.5 times and for ``BICUBIC`` by four +times. + +Default filter for thumbnails ++++++++++++++++++++++++++++++ + +In Pillow 2.5 the default filter for :py:meth:`~PIL.Image.Image.thumbnail` was +changed from ``NEAREST`` to ``ANTIALIAS``. Antialias was chosen because all the +other filters gave poor quality for reduction. Starting from Pillow 2.7.0, +``ANTIALIAS`` has been replaced with ``BICUBIC``, because it's faster and +``ANTIALIAS`` doesn't give any advantages after downscaling with libjpeg, which +uses supersampling internally, not convolutions. + +Image transposition ++++++++++++++++++++ + +A new method ``TRANSPOSE`` has been added for the +:py:meth:`~PIL.Image.Image.transpose` operation in addition to +``FLIP_LEFT_RIGHT``, ``FLIP_TOP_BOTTOM``, ``ROTATE_90``, ``ROTATE_180``, +``ROTATE_270``. ``TRANSPOSE`` is an algebra transpose, with an image reflected +across its main diagonal. + +The speed of ``ROTATE_90``, ``ROTATE_270`` and ``TRANSPOSE`` has been significantly +improved for large images which don't fit in the processor cache. + +Gaussian blur and unsharp mask +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The :py:meth:`~PIL.ImageFilter.GaussianBlur` implementation has been replaced +with a sequential application of box filters. The new implementation is based on +"Theoretical foundations of Gaussian convolution by extended box filtering" from +the Mathematical Image Analysis Group. As :py:meth:`~PIL.ImageFilter.UnsharpMask` +implementations use Gaussian blur internally, all changes from this chapter +are also applicable to it. + +Blur radius ++++++++++++ + +There was an error in the previous version of Pillow, where blur radius (the +standard deviation of Gaussian) actually meant blur diameter. For example, to +blur an image with actual radius 5 you were forced to use value 10. This has +been fixed. Now the meaning of the radius is the same as in other software. + +If you used a Gaussian blur with some radius value, you need to divide this +value by two. + +Blur performance +++++++++++++++++ + +Box filter computation time is constant relative to the radius and depends +on source image size only. Because the new Gaussian blur implementation +is based on box filter, its computation time also doesn't depend on the blur +radius. + +For example, previously, if the execution time for a given test image was 1 +second for radius 1, 3.6 seconds for radius 10 and 17 seconds for 50, now blur +with any radius on same image is executed for 0.2 seconds. + +Blur quality +++++++++++++ + +The previous implementation takes into account only source pixels within +2 * standard deviation radius for every destination pixel. This was not enough, +so the quality was worse compared to other Gaussian blur software. + +The new implementation does not have this drawback. + +TIFF Parameter Changes +^^^^^^^^^^^^^^^^^^^^^^ + +Several kwarg parameters for saving TIFF images were previously +specified as strings with included spaces (e.g. 'x resolution'). This +was difficult to use as kwargs without constructing and passing a +dictionary. These parameters now use the underscore character instead +of space. (e.g. 'x_resolution') diff --git a/docs/releasenotes/2.8.0.rst b/docs/releasenotes/2.8.0.rst index 2b9eed524..424ec5239 100644 --- a/docs/releasenotes/2.8.0.rst +++ b/docs/releasenotes/2.8.0.rst @@ -1,32 +1,32 @@ -2.8.0 ------ - -Open HTTP response objects with Image.open -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -HTTP response objects returned from ``urllib2.urlopen(url)`` or -``requests.get(url, stream=True).raw`` are 'file-like' but do not support ``.seek()`` -operations. As a result PIL was unable to open them as images, requiring a wrap in -``cStringIO`` or ``BytesIO``. - -Now new functionality has been added to ``Image.open()`` by way of an ``.seek(0)`` check and -catch on exception :py:exc:`AttributeError` or :py:exc:`io.UnsupportedOperation`. If this is caught we -attempt to wrap the object using ``io.BytesIO`` (which will only work on buffer-file-like -objects). - -This allows opening of files using both ``urllib2`` and ``requests``, e.g.:: - - Image.open(urllib2.urlopen(url)) - Image.open(requests.get(url, stream=True).raw) - -If the response uses content-encoding (compression, either gzip or deflate) then this -will fail as both the urllib2 and requests raw file object will produce compressed data -in that case. Using Content-Encoding on images is rather non-sensical as most images are -already compressed, but it can still happen. - -For requests the work-around is to set the decode_content attribute on the raw object to -True:: - - response = requests.get(url, stream=True) - response.raw.decode_content = True - image = Image.open(response.raw) +2.8.0 +----- + +Open HTTP response objects with Image.open +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +HTTP response objects returned from ``urllib2.urlopen(url)`` or +``requests.get(url, stream=True).raw`` are 'file-like' but do not support ``.seek()`` +operations. As a result PIL was unable to open them as images, requiring a wrap in +``cStringIO`` or ``BytesIO``. + +Now new functionality has been added to ``Image.open()`` by way of an ``.seek(0)`` check and +catch on exception :py:exc:`AttributeError` or :py:exc:`io.UnsupportedOperation`. If this is caught we +attempt to wrap the object using ``io.BytesIO`` (which will only work on buffer-file-like +objects). + +This allows opening of files using both ``urllib2`` and ``requests``, e.g.:: + + Image.open(urllib2.urlopen(url)) + Image.open(requests.get(url, stream=True).raw) + +If the response uses content-encoding (compression, either gzip or deflate) then this +will fail as both the urllib2 and requests raw file object will produce compressed data +in that case. Using Content-Encoding on images is rather non-sensical as most images are +already compressed, but it can still happen. + +For requests the work-around is to set the decode_content attribute on the raw object to +True:: + + response = requests.get(url, stream=True) + response.raw.decode_content = True + image = Image.open(response.raw) diff --git a/docs/releasenotes/3.0.0.rst b/docs/releasenotes/3.0.0.rst index 8bc477f70..f189bc24d 100644 --- a/docs/releasenotes/3.0.0.rst +++ b/docs/releasenotes/3.0.0.rst @@ -1,53 +1,53 @@ -3.0.0 ------ - -Backwards Incompatible Changes -============================== - -Several methods that have been marked as deprecated for many releases -have been removed in this release: - -* ``Image.tostring()`` -* ``Image.fromstring()`` -* ``Image.offset()`` -* ``ImageDraw.setink()`` -* ``ImageDraw.setfill()`` -* The ``ImageFileIO`` module -* The ``ImageFont.FreeTypeFont`` and ``ImageFont.truetype`` ``file`` keyword arg -* The ``ImagePalette`` private ``_make`` functions -* ``ImageWin.fromstring()`` -* ``ImageWin.tostring()`` - -Other Changes -============= - -Saving Multipage Images -^^^^^^^^^^^^^^^^^^^^^^^ - -There is now support for saving multipage images in the ``GIF`` and -``PDF`` formats. To enable this functionality, pass in ``save_all=True`` -as a keyword argument to the save:: - - im.save('test.pdf', save_all=True) - -Tiff ImageFileDirectory Rewrite -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -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. - -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 -TiffImagePlugin.ImageFileDirectory_v2 class. - -LibJpeg and Zlib are required by default -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The external dependencies on libjpeg and zlib are now required by default. -If the headers or libraries are not found, then installation will abort -with an error. This behaviour can be disabled with the ``--disable-libjpeg`` -and ``--disable-zlib`` flags. +3.0.0 +----- + +Backwards Incompatible Changes +============================== + +Several methods that have been marked as deprecated for many releases +have been removed in this release: + +* ``Image.tostring()`` +* ``Image.fromstring()`` +* ``Image.offset()`` +* ``ImageDraw.setink()`` +* ``ImageDraw.setfill()`` +* The ``ImageFileIO`` module +* The ``ImageFont.FreeTypeFont`` and ``ImageFont.truetype`` ``file`` keyword arg +* The ``ImagePalette`` private ``_make`` functions +* ``ImageWin.fromstring()`` +* ``ImageWin.tostring()`` + +Other Changes +============= + +Saving Multipage Images +^^^^^^^^^^^^^^^^^^^^^^^ + +There is now support for saving multipage images in the ``GIF`` and +``PDF`` formats. To enable this functionality, pass in ``save_all=True`` +as a keyword argument to the save:: + + im.save('test.pdf', save_all=True) + +Tiff ImageFileDirectory Rewrite +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +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. + +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 +TiffImagePlugin.ImageFileDirectory_v2 class. + +LibJpeg and Zlib are required by default +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The external dependencies on libjpeg and zlib are now required by default. +If the headers or libraries are not found, then installation will abort +with an error. This behaviour can be disabled with the ``--disable-libjpeg`` +and ``--disable-zlib`` flags. diff --git a/docs/releasenotes/3.1.0.rst b/docs/releasenotes/3.1.0.rst index 951819f19..541077489 100644 --- a/docs/releasenotes/3.1.0.rst +++ b/docs/releasenotes/3.1.0.rst @@ -1,74 +1,74 @@ -3.1.0 ------ - -ImageDraw arc, chord and pieslice can now use floats -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -There is no longer a need to ensure that the start and end arguments for ``arc``, -``chord`` and ``pieslice`` are integers. - -Note that these numbers are not simply rounded internally, but are actually -utilised in the drawing process. - -Consistent multiline text spacing -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -When using the ``ImageDraw`` multiline methods, the spacing between -lines was inconsistent, based on the combination on ascenders and -descenders. - -This has now been fixed, so that lines are offset by their baselines, -not the absolute height of each line. - -There is also now a default spacing of 4px between lines. - -Exif, Jpeg and Tiff Metadata -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -There were major changes in the TIFF ImageFileDirectory support in -Pillow 3.0 that led to a number of regressions. Some of them have been -fixed in Pillow 3.1, and some of them have been extended to have -different behavior. - -TiffImagePlugin.IFDRational -+++++++++++++++++++++++++++ - -Pillow 3.0 changed rational metadata to use a float. In Pillow 3.1, -this has changed to allow the expression of 0/0 as a valid piece of -rational metadata to reflect usage in the wild. - -Rational metadata is now encapsulated in an ``IFDRational`` -instance. This class extends the Rational class to allow a denominator -of 0. It compares as a float or a number, but does allow access to the -raw numerator and denominator values through attributes. - -When used in a ``ImageFileDirectory_v1``, a 2 item tuple is returned -of the numerator and denominator, as was done previously. - -This class should be used when adding a rational value to an -ImageFileDirectory for saving to image metadata. - -JpegImagePlugin._getexif -++++++++++++++++++++++++ - -In Pillow 3.0, the dictionary returned from the private, experimental, -but generally widely used ``_getexif`` function changed to reflect the -ImageFileDirectory_v2 format, without a fallback to the previous format. - -In Pillow 3.1, ``_getexif`` now returns a dictionary compatible with -Pillow 2.9 and earlier, built with -``ImageFileDirectory_v1`` instances. Additionally, any -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 -++++++++++++++++++++ - -In Pillow 3.0 and 3.1, images that contain metadata that is internally -consistent, but not in agreement with the TIFF spec, may cause an -exception when reading the metadata. This can happen when a tag that -is specified to have a single value is stored with an array of values. - -It is anticipated that this behavior will change in future releases. +3.1.0 +----- + +ImageDraw arc, chord and pieslice can now use floats +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +There is no longer a need to ensure that the start and end arguments for ``arc``, +``chord`` and ``pieslice`` are integers. + +Note that these numbers are not simply rounded internally, but are actually +utilised in the drawing process. + +Consistent multiline text spacing +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When using the ``ImageDraw`` multiline methods, the spacing between +lines was inconsistent, based on the combination on ascenders and +descenders. + +This has now been fixed, so that lines are offset by their baselines, +not the absolute height of each line. + +There is also now a default spacing of 4px between lines. + +Exif, Jpeg and Tiff Metadata +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +There were major changes in the TIFF ImageFileDirectory support in +Pillow 3.0 that led to a number of regressions. Some of them have been +fixed in Pillow 3.1, and some of them have been extended to have +different behavior. + +TiffImagePlugin.IFDRational ++++++++++++++++++++++++++++ + +Pillow 3.0 changed rational metadata to use a float. In Pillow 3.1, +this has changed to allow the expression of 0/0 as a valid piece of +rational metadata to reflect usage in the wild. + +Rational metadata is now encapsulated in an ``IFDRational`` +instance. This class extends the Rational class to allow a denominator +of 0. It compares as a float or a number, but does allow access to the +raw numerator and denominator values through attributes. + +When used in a ``ImageFileDirectory_v1``, a 2 item tuple is returned +of the numerator and denominator, as was done previously. + +This class should be used when adding a rational value to an +ImageFileDirectory for saving to image metadata. + +JpegImagePlugin._getexif +++++++++++++++++++++++++ + +In Pillow 3.0, the dictionary returned from the private, experimental, +but generally widely used ``_getexif`` function changed to reflect the +ImageFileDirectory_v2 format, without a fallback to the previous format. + +In Pillow 3.1, ``_getexif`` now returns a dictionary compatible with +Pillow 2.9 and earlier, built with +``ImageFileDirectory_v1`` instances. Additionally, any +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 +++++++++++++++++++++ + +In Pillow 3.0 and 3.1, images that contain metadata that is internally +consistent, but not in agreement with the TIFF spec, may cause an +exception when reading the metadata. This can happen when a tag that +is specified to have a single value is stored with an array of values. + +It is anticipated that this behavior will change in future releases. diff --git a/docs/releasenotes/3.1.1.rst b/docs/releasenotes/3.1.1.rst index 4eabd1944..46872b86e 100644 --- a/docs/releasenotes/3.1.1.rst +++ b/docs/releasenotes/3.1.1.rst @@ -1,83 +1,83 @@ -3.1.1 ------ - -Security -======== - -:cve:`2016-0740`: Buffer overflow in ``TiffDecode.c`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Pillow 3.1.0 and earlier when linked against libtiff >= 4.0.0 on x64 -may overflow a buffer when reading a specially crafted tiff file. - -Specifically, libtiff >= 4.0.0 changed the return type of -``TIFFScanlineSize`` from ``int32`` to machine dependent -``int32|64``. If the scanline is sized so that it overflows an -``int32``, it may be interpreted as a negative number, which will then -pass the size check in ``TiffDecode.c`` line 236. To do this, the -logical scanline size has to be > 2gb, and for the test file, the -allocated buffer size is 64k against a roughly 4gb scan line size. Any -image data over 64k is written over the heap, causing a segfault. - -This issue was found by security researcher FourOne. - -:cve:`2016-0775`: Buffer overflow in ``FliDecode.c`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -In all versions of Pillow, dating back at least to the last PIL 1.1.7 -release, FliDecode.c has a buffer overflow error (:cve:`2016-0775`). - -Around line 192: - -.. code-block:: c - - case 16: - /* COPY chunk */ - for (y = 0; y < state->ysize; y++) { - UINT8* buf = (UINT8*) im->image[y]; - memcpy(buf+x, data, state->xsize); - data += state->xsize; - } - break; - - -The memcpy has error where ``x`` is added to the target buffer -address. ``X`` is used in several internal temporary variable roles, -but can take a value up to the width of the image. ``Im->image[y]`` -is a set of row pointers to segments of memory that are the size of -the row. At the max ``y``, this will write the contents of the line -off the end of the memory buffer, causing a segfault. - -This issue was found by Alyssa Besseling at Atlassian. - -:cve:`2016-2533`: Buffer overflow in ``PcdDecode.c`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -In all versions of Pillow, dating back at least to the last PIL 1.1.7 -release, ``PcdDecode.c`` has a buffer overflow error (:cve:`2016-2533`). - -The ``state.buffer`` for ``PcdDecode.c`` is allocated based on a 3 -bytes per pixel sizing, where ``PcdDecode.c`` wrote into the buffer -assuming 4 bytes per pixel. This writes 768 bytes beyond the end of -the buffer into other Python object storage. In some cases, this -causes a segfault, in others an internal Python malloc error. - -Integer overflow in ``Resample.c`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -If a large value was passed into the new size for an image, it is -possible to overflow an ``int32`` value passed into malloc. - -.. code-block:: c - - kk = malloc(xsize * kmax * sizeof(float)); - ... - xbounds = malloc(xsize * 2 * sizeof(int)); - -``xsize`` is trusted user input. These multiplications can overflow, -leading the ``malloc``'d buffer to be undersized. These allocations are -followed by a loop that writes out of bounds. This can lead to -corruption on the heap of the Python process with attacker controlled -float data. - -This issue was found by Ned Williamson. +3.1.1 +----- + +Security +======== + +:cve:`2016-0740`: Buffer overflow in ``TiffDecode.c`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Pillow 3.1.0 and earlier when linked against libtiff >= 4.0.0 on x64 +may overflow a buffer when reading a specially crafted tiff file. + +Specifically, libtiff >= 4.0.0 changed the return type of +``TIFFScanlineSize`` from ``int32`` to machine dependent +``int32|64``. If the scanline is sized so that it overflows an +``int32``, it may be interpreted as a negative number, which will then +pass the size check in ``TiffDecode.c`` line 236. To do this, the +logical scanline size has to be > 2gb, and for the test file, the +allocated buffer size is 64k against a roughly 4gb scan line size. Any +image data over 64k is written over the heap, causing a segfault. + +This issue was found by security researcher FourOne. + +:cve:`2016-0775`: Buffer overflow in ``FliDecode.c`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In all versions of Pillow, dating back at least to the last PIL 1.1.7 +release, FliDecode.c has a buffer overflow error (:cve:`2016-0775`). + +Around line 192: + +.. code-block:: c + + case 16: + /* COPY chunk */ + for (y = 0; y < state->ysize; y++) { + UINT8* buf = (UINT8*) im->image[y]; + memcpy(buf+x, data, state->xsize); + data += state->xsize; + } + break; + + +The memcpy has error where ``x`` is added to the target buffer +address. ``X`` is used in several internal temporary variable roles, +but can take a value up to the width of the image. ``Im->image[y]`` +is a set of row pointers to segments of memory that are the size of +the row. At the max ``y``, this will write the contents of the line +off the end of the memory buffer, causing a segfault. + +This issue was found by Alyssa Besseling at Atlassian. + +:cve:`2016-2533`: Buffer overflow in ``PcdDecode.c`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In all versions of Pillow, dating back at least to the last PIL 1.1.7 +release, ``PcdDecode.c`` has a buffer overflow error (:cve:`2016-2533`). + +The ``state.buffer`` for ``PcdDecode.c`` is allocated based on a 3 +bytes per pixel sizing, where ``PcdDecode.c`` wrote into the buffer +assuming 4 bytes per pixel. This writes 768 bytes beyond the end of +the buffer into other Python object storage. In some cases, this +causes a segfault, in others an internal Python malloc error. + +Integer overflow in ``Resample.c`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If a large value was passed into the new size for an image, it is +possible to overflow an ``int32`` value passed into malloc. + +.. code-block:: c + + kk = malloc(xsize * kmax * sizeof(float)); + ... + xbounds = malloc(xsize * 2 * sizeof(int)); + +``xsize`` is trusted user input. These multiplications can overflow, +leading the ``malloc``'d buffer to be undersized. These allocations are +followed by a loop that writes out of bounds. This can lead to +corruption on the heap of the Python process with attacker controlled +float data. + +This issue was found by Ned Williamson. diff --git a/docs/releasenotes/3.1.2.rst b/docs/releasenotes/3.1.2.rst index a96154977..637445aea 100644 --- a/docs/releasenotes/3.1.2.rst +++ b/docs/releasenotes/3.1.2.rst @@ -1,47 +1,47 @@ -3.1.2 ------ - -Security -======== - -:cve:`2016-3076`: Buffer overflow in Jpeg2KEncode.c -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Pillow between 2.5.0 and 3.1.1 may overflow a buffer -when writing large Jpeg2000 files, allowing for code execution or other -memory corruption. - -This occurs specifically in the function ``j2k_encode_entry``, at the line: - -.. code-block:: c - - state->buffer = malloc (tile_width * tile_height * components * prec / 8); - - -This vulnerability requires a particular value for ``height * width`` -such that ``height * width * components * precision`` overflows, at -which point the malloc will be for a smaller value than expected. The -buffer that is allocated will be ``((height * width * components * -precision) mod (2^31) / 8)``, where components is 1-4 and precision is -either 8 or -16. Common values would be 4 components at precision 8 for a standard -``RGBA`` image. - -The unpackers then split an image that is laid out:: - - RGBARGBARGBA.... - -into:: - - - RRR. - GGG. - BBB. - AAA. - - -If this buffer is smaller than expected, the jpeg2k unpacker functions -will write outside the allocation and onto the heap, corrupting -memory. - -This issue was found by Alyssa Besseling at Atlassian. +3.1.2 +----- + +Security +======== + +:cve:`2016-3076`: Buffer overflow in Jpeg2KEncode.c +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Pillow between 2.5.0 and 3.1.1 may overflow a buffer +when writing large Jpeg2000 files, allowing for code execution or other +memory corruption. + +This occurs specifically in the function ``j2k_encode_entry``, at the line: + +.. code-block:: c + + state->buffer = malloc (tile_width * tile_height * components * prec / 8); + + +This vulnerability requires a particular value for ``height * width`` +such that ``height * width * components * precision`` overflows, at +which point the malloc will be for a smaller value than expected. The +buffer that is allocated will be ``((height * width * components * +precision) mod (2^31) / 8)``, where components is 1-4 and precision is +either 8 or +16. Common values would be 4 components at precision 8 for a standard +``RGBA`` image. + +The unpackers then split an image that is laid out:: + + RGBARGBARGBA.... + +into:: + + + RRR. + GGG. + BBB. + AAA. + + +If this buffer is smaller than expected, the jpeg2k unpacker functions +will write outside the allocation and onto the heap, corrupting +memory. + +This issue was found by Alyssa Besseling at Atlassian. diff --git a/docs/releasenotes/3.2.0.rst b/docs/releasenotes/3.2.0.rst index 3ed8fae57..2148f9d10 100644 --- a/docs/releasenotes/3.2.0.rst +++ b/docs/releasenotes/3.2.0.rst @@ -1,35 +1,35 @@ -3.2.0 ------ - -New DDS and FTEX Image Plugins -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The ``DdsImagePlugin`` reading DXT1 and DXT5 encoded ``.dds`` images was -added. DXT3 images are not currently supported. - -The ``FtexImagePlugin`` reads textures used for 3D objects in -Independence War 2: Edge Of Chaos. The plugin reads a single texture -per file, in the ``.ftc`` (compressed) and ``.ftu`` (uncompressed) -formats. - -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 -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -``ImageDraw.multiline_text`` and ``ImageDraw.multiline_size`` take extra -spacing parameters above what are used in ``ImageDraw.text`` and -``ImageDraw.size``. These parameters can now be passed into -``ImageDraw.text`` and ``ImageDraw.size`` and they will be passed through -to the corresponding multiline functions. - -ImageSequence.Iterator changes -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -``ImageSequence.Iterator`` is now an actual iterator implementing the -Iterator protocol. It is also now possible to seek to the first image -of the file when using direct indexing. +3.2.0 +----- + +New DDS and FTEX Image Plugins +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``DdsImagePlugin`` reading DXT1 and DXT5 encoded ``.dds`` images was +added. DXT3 images are not currently supported. + +The ``FtexImagePlugin`` reads textures used for 3D objects in +Independence War 2: Edge Of Chaos. The plugin reads a single texture +per file, in the ``.ftc`` (compressed) and ``.ftu`` (uncompressed) +formats. + +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 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``ImageDraw.multiline_text`` and ``ImageDraw.multiline_size`` take extra +spacing parameters above what are used in ``ImageDraw.text`` and +``ImageDraw.size``. These parameters can now be passed into +``ImageDraw.text`` and ``ImageDraw.size`` and they will be passed through +to the corresponding multiline functions. + +ImageSequence.Iterator changes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``ImageSequence.Iterator`` is now an actual iterator implementing the +Iterator protocol. It is also now possible to seek to the first image +of the file when using direct indexing. diff --git a/docs/releasenotes/3.3.0.rst b/docs/releasenotes/3.3.0.rst index cd6f7e2f9..daa7a872d 100644 --- a/docs/releasenotes/3.3.0.rst +++ b/docs/releasenotes/3.3.0.rst @@ -1,51 +1,51 @@ -3.3.0 ------ - -Libimagequant support -^^^^^^^^^^^^^^^^^^^^^ - -There is now support for using libimagequant as a higher quality -quantization option in ``Image.quantize()`` on Unix-like -platforms. This support requires building Pillow from source against -libimagequant. We cannot distribute binaries due to licensing -differences. - - -New Setup.py options -^^^^^^^^^^^^^^^^^^^^ - -There are two new options to control the ``build_ext`` task in ``setup.py``: - -* ``--debug`` dumps all of the directories and files that are - checked when searching for libraries or headers when building the - extensions. -* ``--disable-platform-guessing`` removes many of the directories - that are checked for libraries and headers for build systems or - cross compilers that specify that information in via environment - variables. - -Resizing -^^^^^^^^ - -Image resampling for 8-bit per channel images was rewritten using only integer -computings. This is faster on most platforms and doesn't introduce precision -errors on the wide range of scales. With other performance improvements, this -makes resampling 60% faster on average. - -Color calculation for images in the ``LA`` mode on semitransparent pixels -was fixed. - -Rotation -^^^^^^^^ - -Rotation for angles divisible by 90 degrees now always uses transposition. -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 -^^^^^^^^^^^^^^ - -The return type for binary data in version 2 Exif and Tiff metadata -has been changed from a tuple of integers to bytes. This is a change -from the behavior since 3.0.0. +3.3.0 +----- + +Libimagequant support +^^^^^^^^^^^^^^^^^^^^^ + +There is now support for using libimagequant as a higher quality +quantization option in ``Image.quantize()`` on Unix-like +platforms. This support requires building Pillow from source against +libimagequant. We cannot distribute binaries due to licensing +differences. + + +New Setup.py options +^^^^^^^^^^^^^^^^^^^^ + +There are two new options to control the ``build_ext`` task in ``setup.py``: + +* ``--debug`` dumps all of the directories and files that are + checked when searching for libraries or headers when building the + extensions. +* ``--disable-platform-guessing`` removes many of the directories + that are checked for libraries and headers for build systems or + cross compilers that specify that information in via environment + variables. + +Resizing +^^^^^^^^ + +Image resampling for 8-bit per channel images was rewritten using only integer +computings. This is faster on most platforms and doesn't introduce precision +errors on the wide range of scales. With other performance improvements, this +makes resampling 60% faster on average. + +Color calculation for images in the ``LA`` mode on semitransparent pixels +was fixed. + +Rotation +^^^^^^^^ + +Rotation for angles divisible by 90 degrees now always uses transposition. +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 +^^^^^^^^^^^^^^ + +The return type for binary data in version 2 Exif and Tiff metadata +has been changed from a tuple of integers to bytes. This is a change +from the behavior since 3.0.0. diff --git a/docs/releasenotes/3.3.2.rst b/docs/releasenotes/3.3.2.rst index 73156a65d..1888807d7 100644 --- a/docs/releasenotes/3.3.2.rst +++ b/docs/releasenotes/3.3.2.rst @@ -1,38 +1,38 @@ -3.3.2 ------ - -Security -======== - -Integer overflow in Map.c -^^^^^^^^^^^^^^^^^^^^^^^^^ - -Pillow prior to 3.3.2 may experience integer overflow errors in map.c -when reading specially crafted image files. This may lead to memory -disclosure or corruption. - -Specifically, when parameters from the image are passed into -``Image.core.map_buffer``, the size of the image was calculated with -``xsize`` * ``ysize`` * ``bytes_per_pixel``. This will overflow if the -result is larger than SIZE_MAX. This is possible on a 32-bit system. - -Furthermore this ``size`` value was added to a potentially attacker -provided ``offset`` value and compared to the size of the buffer -without checking for overflow or negative values. - -These values were then used for creating pointers, at which point -Pillow could read the memory and include it in other images. The image -was marked readonly, so Pillow would not ordinarily write to that -memory without duplicating the image first. - -This issue was found by Cris Neckar at Divergent Security. - -Sign Extension in Storage.c -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Pillow prior to 3.3.2 and PIL 1.1.7 (at least) do not check for -negative image sizes in ``ImagingNew`` in ``Storage.c``. A negative -image size can lead to a smaller allocation than expected, leading to -arbitrary writes. - -This issue was found by Cris Neckar at Divergent Security. +3.3.2 +----- + +Security +======== + +Integer overflow in Map.c +^^^^^^^^^^^^^^^^^^^^^^^^^ + +Pillow prior to 3.3.2 may experience integer overflow errors in map.c +when reading specially crafted image files. This may lead to memory +disclosure or corruption. + +Specifically, when parameters from the image are passed into +``Image.core.map_buffer``, the size of the image was calculated with +``xsize`` * ``ysize`` * ``bytes_per_pixel``. This will overflow if the +result is larger than SIZE_MAX. This is possible on a 32-bit system. + +Furthermore this ``size`` value was added to a potentially attacker +provided ``offset`` value and compared to the size of the buffer +without checking for overflow or negative values. + +These values were then used for creating pointers, at which point +Pillow could read the memory and include it in other images. The image +was marked readonly, so Pillow would not ordinarily write to that +memory without duplicating the image first. + +This issue was found by Cris Neckar at Divergent Security. + +Sign Extension in Storage.c +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Pillow prior to 3.3.2 and PIL 1.1.7 (at least) do not check for +negative image sizes in ``ImagingNew`` in ``Storage.c``. A negative +image size can lead to a smaller allocation than expected, leading to +arbitrary writes. + +This issue was found by Cris Neckar at Divergent Security. diff --git a/docs/releasenotes/3.4.0.rst b/docs/releasenotes/3.4.0.rst index 8a5a7efe3..e13e7ff8c 100644 --- a/docs/releasenotes/3.4.0.rst +++ b/docs/releasenotes/3.4.0.rst @@ -1,65 +1,65 @@ -3.4.0 ------ - -Backwards Incompatible Changes -============================== - -Image.core.open_ppm removed -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The nominally private/debugging function ``Image.core.open_ppm`` has -been removed. If you were using this function, please use -``Image.open`` instead. - -Deprecations -============ - -Deprecation Warning when Saving JPEGs -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -JPEG images cannot contain an alpha channel. Pillow prior to 3.4.0 -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 -============= - -New resizing filters -^^^^^^^^^^^^^^^^^^^^ - -Two new filters available for ``Image.resize()`` and ``Image.thumbnail()`` -functions: ``BOX`` and ``HAMMING``. ``BOX`` is the high-performance filter with -two times shorter window than ``BILINEAR``. It can be used for image reduction -3 and more times and produces a sharper result than ``BILINEAR``. - -``HAMMING`` filter has the same performance as ``BILINEAR`` filter while -providing the image downscaling quality comparable to ``BICUBIC``. -Both new filters don't show good quality for the image upscaling. - -New DDS Decoders -^^^^^^^^^^^^^^^^ - -Pillow can now decode DXT3 images, as well as the previously supported -DXT1 and DXT5 formats. All three formats are now decoded in C code for -better performance. - -Append images to GIF -^^^^^^^^^^^^^^^^^^^^ - -Additional frames can now be appended when saving a GIF file, through the -``append_images`` argument. The new frames are passed in as a list of images, -which may be have multiple frames themselves. - -Note that the ``append_images`` argument is only used if ``save_all`` is also -in effect, e.g.:: - - im.save(out, save_all=True, append_images=[im1, im2, ...]) - -Save multiple frame TIFF -^^^^^^^^^^^^^^^^^^^^^^^^ - -Multiple frames can now be saved in a TIFF file by using the ``save_all`` option. -e.g.:: - - im.save("filename.tiff", format="TIFF", save_all=True) +3.4.0 +----- + +Backwards Incompatible Changes +============================== + +Image.core.open_ppm removed +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The nominally private/debugging function ``Image.core.open_ppm`` has +been removed. If you were using this function, please use +``Image.open`` instead. + +Deprecations +============ + +Deprecation Warning when Saving JPEGs +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +JPEG images cannot contain an alpha channel. Pillow prior to 3.4.0 +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 +============= + +New resizing filters +^^^^^^^^^^^^^^^^^^^^ + +Two new filters available for ``Image.resize()`` and ``Image.thumbnail()`` +functions: ``BOX`` and ``HAMMING``. ``BOX`` is the high-performance filter with +two times shorter window than ``BILINEAR``. It can be used for image reduction +3 and more times and produces a sharper result than ``BILINEAR``. + +``HAMMING`` filter has the same performance as ``BILINEAR`` filter while +providing the image downscaling quality comparable to ``BICUBIC``. +Both new filters don't show good quality for the image upscaling. + +New DDS Decoders +^^^^^^^^^^^^^^^^ + +Pillow can now decode DXT3 images, as well as the previously supported +DXT1 and DXT5 formats. All three formats are now decoded in C code for +better performance. + +Append images to GIF +^^^^^^^^^^^^^^^^^^^^ + +Additional frames can now be appended when saving a GIF file, through the +``append_images`` argument. The new frames are passed in as a list of images, +which may be have multiple frames themselves. + +Note that the ``append_images`` argument is only used if ``save_all`` is also +in effect, e.g.:: + + im.save(out, save_all=True, append_images=[im1, im2, ...]) + +Save multiple frame TIFF +^^^^^^^^^^^^^^^^^^^^^^^^ + +Multiple frames can now be saved in a TIFF file by using the ``save_all`` option. +e.g.:: + + im.save("filename.tiff", format="TIFF", save_all=True) diff --git a/docs/releasenotes/4.0.0.rst b/docs/releasenotes/4.0.0.rst index 625f237e8..0fee12960 100644 --- a/docs/releasenotes/4.0.0.rst +++ b/docs/releasenotes/4.0.0.rst @@ -1,51 +1,51 @@ -4.0.0 ------ - -Python 2.6 and 3.2 Dropped -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Pillow 4.0 no longer supports Python 2.6 and 3.2. We will not be -creating binaries, testing, or retaining compatibility with these -releases. This release removes some workarounds for those Python -releases, so the final working version of Pillow on 2.6 or 3.2 is 3.4.2. - -Support added for Python 3.6 -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Pillow 4.0 supports Python 3.6. - -OleFileIO.py -^^^^^^^^^^^^ - -``OleFileIO.py`` has been removed as a vendored file and is now installed -from the upstream :pypi:`olefile` PyPI package. All internal dependencies are -redirected to the olefile package. Direct accesses to -``PIL.OlefileIO`` raises a deprecation warning, then patches the -upstream olefile into ``sys.modules`` in its place. - -SGI image save -^^^^^^^^^^^^^^ - -It is now possible to save images in modes ``L``, ``RGB``, and -``RGBA`` to the uncompressed SGI image format. - -Zero sized images -^^^^^^^^^^^^^^^^^ - -Pillow 3.4.0 removed support for creating images with (0,0) size. This -has been reenabled, restoring pre 3.4 behavior. - -Internal handles_eof flag -^^^^^^^^^^^^^^^^^^^^^^^^^ - -The ``handles_eof flag`` for decoding images has been removed, as there -were no internal users of the flag. Anyone maintaining image decoders -outside of the Pillow source tree should consider using the cleanup -function pointers instead. - -Image.core.stretch removed -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The stretch function on the core image object has been removed. This -used to be for enlarging the image, but has been aliased to resize -recently. +4.0.0 +----- + +Python 2.6 and 3.2 Dropped +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Pillow 4.0 no longer supports Python 2.6 and 3.2. We will not be +creating binaries, testing, or retaining compatibility with these +releases. This release removes some workarounds for those Python +releases, so the final working version of Pillow on 2.6 or 3.2 is 3.4.2. + +Support added for Python 3.6 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Pillow 4.0 supports Python 3.6. + +OleFileIO.py +^^^^^^^^^^^^ + +``OleFileIO.py`` has been removed as a vendored file and is now installed +from the upstream :pypi:`olefile` PyPI package. All internal dependencies are +redirected to the olefile package. Direct accesses to +``PIL.OlefileIO`` raises a deprecation warning, then patches the +upstream olefile into ``sys.modules`` in its place. + +SGI image save +^^^^^^^^^^^^^^ + +It is now possible to save images in modes ``L``, ``RGB``, and +``RGBA`` to the uncompressed SGI image format. + +Zero sized images +^^^^^^^^^^^^^^^^^ + +Pillow 3.4.0 removed support for creating images with (0,0) size. This +has been reenabled, restoring pre 3.4 behavior. + +Internal handles_eof flag +^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``handles_eof flag`` for decoding images has been removed, as there +were no internal users of the flag. Anyone maintaining image decoders +outside of the Pillow source tree should consider using the cleanup +function pointers instead. + +Image.core.stretch removed +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The stretch function on the core image object has been removed. This +used to be for enlarging the image, but has been aliased to resize +recently. diff --git a/docs/releasenotes/4.1.0.rst b/docs/releasenotes/4.1.0.rst index 80ad9b9fb..1867acd79 100644 --- a/docs/releasenotes/4.1.0.rst +++ b/docs/releasenotes/4.1.0.rst @@ -1,88 +1,88 @@ -4.1.0 ------ - -Deprecations -============ - -Several deprecated items have been removed. - -* Support for spaces in tiff kwargs in the parameters for 'x resolution', 'y - resolution', 'resolution unit', and 'date time' has been - removed. Underscores should be used instead. - -* The methods ``PIL.ImageDraw.ImageDraw.setink``, - ``PIL.ImageDraw.ImageDraw.setfill``, and - ``PIL.ImageDraw.ImageDraw.setfont`` have been removed. - - -Other Changes -============= - -Closing Files When Opening Images -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The file handling when opening images has been overhauled. Previously, -Pillow would attempt to close some, but not all image formats -after loading the image data. Now, the following behavior -is specified: - -* For images where an open file is passed in, it is the - responsibility of the calling code to close the file. - -* For images where Pillow opens the file and the file is known to have - only one frame, the file is closed after loading. - -* If the file has more than one frame, or if it can't be determined, - then the file is left open to permit seeking to subsequent - frames. It will be closed, eventually, in the ``close`` or - ``__del__`` methods. - -* If the image is memory mapped, then we can't close the mapping to - the underlying file until we are done with the image. The mapping - will be closed in the ``close`` or ``__del__`` method. - -Changes to GIF Handling When Saving -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The :py:class:`PIL.GifImagePlugin` code has been refactored to fix the flow when -saving images. There are two external changes that arise from this: - -* An :py:class:`PIL.ImagePalette.ImagePalette` object is now accepted - as a specified palette argument in :py:meth:`PIL.Image.Image.save()`. - -* The image to be saved is no longer modified in place by any of the - operations of the save function. Previously it was modified when - optimizing the image palette. - -This refactor fixed some bugs with palette handling when saving -multiple frame GIFs. - -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 -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -There is now a decoder registry similar to the image plugin -registries. Image plugins can register a decoder, and it will be -called when the decoding is requested. This allows for the creation of -pure Python decoders. While the Python decoders will not be as fast as -their C based counterparts, they may be easier and quicker to develop -or safer to run. - -Tests -^^^^^ - -Many tests have been added, including correctness tests for image -formats that have been previously untested. - -We are now running automated tests in Docker containers against more -Linux versions than are provided on Travis CI, which is currently -Ubuntu 14.04 x64. This Pillow release is tested on 64-bit Alpine, -Arch, Ubuntu 12.04 and 16.04, and 32-bit Debian Stretch and Ubuntu -14.04. This also covers a wider range of dependency versions than are -provided on Travis natively. +4.1.0 +----- + +Deprecations +============ + +Several deprecated items have been removed. + +* Support for spaces in tiff kwargs in the parameters for 'x resolution', 'y + resolution', 'resolution unit', and 'date time' has been + removed. Underscores should be used instead. + +* The methods ``PIL.ImageDraw.ImageDraw.setink``, + ``PIL.ImageDraw.ImageDraw.setfill``, and + ``PIL.ImageDraw.ImageDraw.setfont`` have been removed. + + +Other Changes +============= + +Closing Files When Opening Images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The file handling when opening images has been overhauled. Previously, +Pillow would attempt to close some, but not all image formats +after loading the image data. Now, the following behavior +is specified: + +* For images where an open file is passed in, it is the + responsibility of the calling code to close the file. + +* For images where Pillow opens the file and the file is known to have + only one frame, the file is closed after loading. + +* If the file has more than one frame, or if it can't be determined, + then the file is left open to permit seeking to subsequent + frames. It will be closed, eventually, in the ``close`` or + ``__del__`` methods. + +* If the image is memory mapped, then we can't close the mapping to + the underlying file until we are done with the image. The mapping + will be closed in the ``close`` or ``__del__`` method. + +Changes to GIF Handling When Saving +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The :py:class:`PIL.GifImagePlugin` code has been refactored to fix the flow when +saving images. There are two external changes that arise from this: + +* An :py:class:`PIL.ImagePalette.ImagePalette` object is now accepted + as a specified palette argument in :py:meth:`PIL.Image.Image.save()`. + +* The image to be saved is no longer modified in place by any of the + operations of the save function. Previously it was modified when + optimizing the image palette. + +This refactor fixed some bugs with palette handling when saving +multiple frame GIFs. + +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 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +There is now a decoder registry similar to the image plugin +registries. Image plugins can register a decoder, and it will be +called when the decoding is requested. This allows for the creation of +pure Python decoders. While the Python decoders will not be as fast as +their C based counterparts, they may be easier and quicker to develop +or safer to run. + +Tests +^^^^^ + +Many tests have been added, including correctness tests for image +formats that have been previously untested. + +We are now running automated tests in Docker containers against more +Linux versions than are provided on Travis CI, which is currently +Ubuntu 14.04 x64. This Pillow release is tested on 64-bit Alpine, +Arch, Ubuntu 12.04 and 16.04, and 32-bit Debian Stretch and Ubuntu +14.04. This also covers a wider range of dependency versions than are +provided on Travis natively. diff --git a/docs/releasenotes/4.1.1.rst b/docs/releasenotes/4.1.1.rst index 8c8055bfa..6dd30445e 100644 --- a/docs/releasenotes/4.1.1.rst +++ b/docs/releasenotes/4.1.1.rst @@ -1,21 +1,21 @@ -4.1.1 ------ - -Fix Regression with reading DPI from EXIF data -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Some JPEG images don't contain DPI information in the image metadata, -but do contain it in the EXIF data. A patch was added in 4.1.0 to read -from the EXIF data, but it did not accept all possible types that -could be included there. This fix adds the ability to read ints as -well as rational values. - -Incompatibility between 3.6.0 and 3.6.1 -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -CPython 3.6.1 added a new symbol, PySlice_GetIndicesEx, which was not -present in 3.6.0. This had the effect of causing binaries compiled on -CPython 3.6.1 to not work on installations of C-Python 3.6.0. This fix -undefines PySlice_GetIndicesEx if it exists to restore compatibility -with both 3.6.0 and 3.6.1. See https://bugs.python.org/issue29943 for -more details. +4.1.1 +----- + +Fix Regression with reading DPI from EXIF data +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Some JPEG images don't contain DPI information in the image metadata, +but do contain it in the EXIF data. A patch was added in 4.1.0 to read +from the EXIF data, but it did not accept all possible types that +could be included there. This fix adds the ability to read ints as +well as rational values. + +Incompatibility between 3.6.0 and 3.6.1 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +CPython 3.6.1 added a new symbol, PySlice_GetIndicesEx, which was not +present in 3.6.0. This had the effect of causing binaries compiled on +CPython 3.6.1 to not work on installations of C-Python 3.6.0. This fix +undefines PySlice_GetIndicesEx if it exists to restore compatibility +with both 3.6.0 and 3.6.1. See https://bugs.python.org/issue29943 for +more details. diff --git a/docs/releasenotes/4.2.0.rst b/docs/releasenotes/4.2.0.rst index bc2a45f02..ecc23bb50 100644 --- a/docs/releasenotes/4.2.0.rst +++ b/docs/releasenotes/4.2.0.rst @@ -1,55 +1,55 @@ -4.2.0 ------ - -Backwards Incompatible Changes -============================== - -Several deprecated items have been removed -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -* The methods ``PIL.ImageWin.Dib.fromstring``, - ``PIL.ImageWin.Dib.tostring`` and - ``PIL.TiffImagePlugin.ImageFileDirectory_v2.as_dict`` have - been removed. - -* Before Pillow 4.2.0, attempting to save an RGBA image as JPEG would - discard the alpha channel. From Pillow 3.4.0, a deprecation warning - was shown. From Pillow 4.2.0, the deprecation warning is removed and - an :py:exc:`IOError` is raised. - -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 -============= - -Added Complex Text Rendering -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Pillow now supports complex text rendering for scripts requiring glyph -composition and bidirectional flow. This optional feature adds three -dependencies: harfbuzz, fribidi, and raqm. See the :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 -^^^^^^^^^^^^^^^^^^^^^^^ - -* :py:meth:`PIL.ImageDraw.floodfill` has a new optional parameter: - threshold. This specifies a tolerance for the color to replace with - the flood fill. - -* The TIFF and PDF image writers now support the ``append_images`` - optional parameter for specifying additional images to create - multipage outputs. - -New DecompressionBomb Warning -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -:py:meth:`PIL.Image.Image.crop` now may raise a DecompressionBomb -warning if the crop region enlarges the image over the threshold -specified by :py:data:`PIL.Image.MAX_IMAGE_PIXELS`. +4.2.0 +----- + +Backwards Incompatible Changes +============================== + +Several deprecated items have been removed +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* The methods ``PIL.ImageWin.Dib.fromstring``, + ``PIL.ImageWin.Dib.tostring`` and + ``PIL.TiffImagePlugin.ImageFileDirectory_v2.as_dict`` have + been removed. + +* Before Pillow 4.2.0, attempting to save an RGBA image as JPEG would + discard the alpha channel. From Pillow 3.4.0, a deprecation warning + was shown. From Pillow 4.2.0, the deprecation warning is removed and + an :py:exc:`IOError` is raised. + +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 +============= + +Added Complex Text Rendering +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Pillow now supports complex text rendering for scripts requiring glyph +composition and bidirectional flow. This optional feature adds three +dependencies: harfbuzz, fribidi, and raqm. See the :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 +^^^^^^^^^^^^^^^^^^^^^^^ + +* :py:meth:`PIL.ImageDraw.floodfill` has a new optional parameter: + threshold. This specifies a tolerance for the color to replace with + the flood fill. + +* The TIFF and PDF image writers now support the ``append_images`` + optional parameter for specifying additional images to create + multipage outputs. + +New DecompressionBomb Warning +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:py:meth:`PIL.Image.Image.crop` now may raise a DecompressionBomb +warning if the crop region enlarges the image over the threshold +specified by :py:data:`PIL.Image.MAX_IMAGE_PIXELS`. diff --git a/docs/releasenotes/4.2.1.rst b/docs/releasenotes/4.2.1.rst index 2061f6467..ee6e7180d 100644 --- a/docs/releasenotes/4.2.1.rst +++ b/docs/releasenotes/4.2.1.rst @@ -1,10 +1,10 @@ -4.2.1 ------ - -There are no functional changes in this release. - -Fixed Windows PyPy Build -^^^^^^^^^^^^^^^^^^^^^^^^ - -A change in the 4.2.0 cycle broke the Windows PyPy build. This has -been fixed, and PyPy is now part of the Windows CI matrix. +4.2.1 +----- + +There are no functional changes in this release. + +Fixed Windows PyPy Build +^^^^^^^^^^^^^^^^^^^^^^^^ + +A change in the 4.2.0 cycle broke the Windows PyPy build. This has +been fixed, and PyPy is now part of the Windows CI matrix. diff --git a/docs/releasenotes/4.3.0.rst b/docs/releasenotes/4.3.0.rst index ea81fc45e..4dd8503d0 100644 --- a/docs/releasenotes/4.3.0.rst +++ b/docs/releasenotes/4.3.0.rst @@ -1,138 +1,138 @@ -4.3.0 ------ - -API Changes -=========== - -Deprecations -^^^^^^^^^^^^ - -Several undocumented functions in ImageOps have been deprecated: -``gaussian_blur``, ``gblur``, ``unsharp_mask``, ``usm`` and -``box_blur``. Use the equivalent operations in ``ImageFilter`` -instead. These functions will be removed in a future release. - -TIFF Metadata Changes -^^^^^^^^^^^^^^^^^^^^^ - -* TIFF tags with unknown type/quantity now default to being bare - values if they are 1 element, where previously they would be a - single element tuple. This is only with the new api, not the legacy - api. This normalizes the handling of fields, so that the metadata - with inferred or image specified counts are handled the same as - metadata with count specified in the TIFF spec. -* The ``PhotoshopInfo``, ``XMP``, and ``JPEGTables`` tags now have a - defined type (bytes) and a count of 1. -* The ``ImageJMetaDataByteCounts`` tag now has an arbitrary number of - items, as there can be multiple items, one for UTF-8, and one for - UTF-16. - -Core Image API Changes -^^^^^^^^^^^^^^^^^^^^^^ - -These are internal functions that should not have been used by user -code, but they were accessible from the python layer. - -Debugging code within ``Image.core.grabclipboard`` was removed. It had been -marked as ``will be removed in future versions`` since PIL. When enabled, it -identified the format of the clipboard data. - -The ``PIL.Image.core.copy`` and ``PIL.Image.Image.im.copy2`` methods -have been removed. - -The ``PIL.Image.core.getcount`` methods have been removed, use -``PIL.Image.core.get_stats()['new_count']`` property instead. - - -API Additions -============= - -Get One Channel From Image -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -A new method :py:meth:`PIL.Image.Image.getchannel` has been added to -return a single channel by index or name. For example, -``image.getchannel("A")`` will return alpha channel as separate image. -``getchannel`` should work up to 6 times faster than -``image.split()[0]`` in previous Pillow versions. - -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 -^^^^^^^^^^^^^^^^^^ - -Added new argument ``box`` for :py:meth:`PIL.Image.Image.resize`. This -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 -^^^^^^^^^^^^^^^^^^^^^^^ - -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 -^^^^^^^^^^^^^^^^^ - -There is a new :py:class:`PIL.ImageFilter.MultibandFilter` base class -for image filters that can run on all channels of an image in one -operation. The original :py:class:`PIL.ImageFilter.Filter` class -remains for image filters that can process only single band images, or -require splitting of channels prior to filtering. - -Other Changes -============= - -Loading 16-bit TIFF Images -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Pillow now can read 16-bit multichannel TIFF files including files -with alpha transparency. The image data is truncated to 8-bit -precision. - -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 -^^^^^^^^^^ - -Pillow can now read and write uncompressed 16-bit multichannel SGI -images to and from RGB and RGBA formats. The image data is truncated -to 8-bit precision. - -Pillow can now read RLE encoded SGI images in both 8 and 16-bit -precision. - -Performance -^^^^^^^^^^^ - -This release contains several performance improvements: - -* Many memory bandwidth-bounded operations such as crop, image allocation, - conversion, split into bands and merging from bands are up to 2x faster. -* Upscaling of multichannel images (such as RGB) is accelerated by 5-10% -* JPEG loading is accelerated up to 15% and JPEG saving up to 20% when - using a recent version of libjpeg-turbo. -* ``Image.transpose`` has been accelerated 15% or more by using a cache - friendly algorithm. -* ImageFilters based on Kernel convolution are significantly faster - due to the new :py:class:`~PIL.ImageFilter.MultibandFilter` feature. -* All memory allocation for images is now done in blocks, rather than - falling back to an allocation for each scan line for images larger - than the block size. - -CMYK Conversion -^^^^^^^^^^^^^^^ - -The basic CMYK->RGB conversion has been tweaked to match the formula -from Google Chrome. This produces an image that is generally lighter -than the previous formula, and more in line with what color managed -applications produce. +4.3.0 +----- + +API Changes +=========== + +Deprecations +^^^^^^^^^^^^ + +Several undocumented functions in ImageOps have been deprecated: +``gaussian_blur``, ``gblur``, ``unsharp_mask``, ``usm`` and +``box_blur``. Use the equivalent operations in ``ImageFilter`` +instead. These functions will be removed in a future release. + +TIFF Metadata Changes +^^^^^^^^^^^^^^^^^^^^^ + +* TIFF tags with unknown type/quantity now default to being bare + values if they are 1 element, where previously they would be a + single element tuple. This is only with the new api, not the legacy + api. This normalizes the handling of fields, so that the metadata + with inferred or image specified counts are handled the same as + metadata with count specified in the TIFF spec. +* The ``PhotoshopInfo``, ``XMP``, and ``JPEGTables`` tags now have a + defined type (bytes) and a count of 1. +* The ``ImageJMetaDataByteCounts`` tag now has an arbitrary number of + items, as there can be multiple items, one for UTF-8, and one for + UTF-16. + +Core Image API Changes +^^^^^^^^^^^^^^^^^^^^^^ + +These are internal functions that should not have been used by user +code, but they were accessible from the python layer. + +Debugging code within ``Image.core.grabclipboard`` was removed. It had been +marked as ``will be removed in future versions`` since PIL. When enabled, it +identified the format of the clipboard data. + +The ``PIL.Image.core.copy`` and ``PIL.Image.Image.im.copy2`` methods +have been removed. + +The ``PIL.Image.core.getcount`` methods have been removed, use +``PIL.Image.core.get_stats()['new_count']`` property instead. + + +API Additions +============= + +Get One Channel From Image +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +A new method :py:meth:`PIL.Image.Image.getchannel` has been added to +return a single channel by index or name. For example, +``image.getchannel("A")`` will return alpha channel as separate image. +``getchannel`` should work up to 6 times faster than +``image.split()[0]`` in previous Pillow versions. + +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 +^^^^^^^^^^^^^^^^^^ + +Added new argument ``box`` for :py:meth:`PIL.Image.Image.resize`. This +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 +^^^^^^^^^^^^^^^^^^^^^^^ + +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 +^^^^^^^^^^^^^^^^^ + +There is a new :py:class:`PIL.ImageFilter.MultibandFilter` base class +for image filters that can run on all channels of an image in one +operation. The original :py:class:`PIL.ImageFilter.Filter` class +remains for image filters that can process only single band images, or +require splitting of channels prior to filtering. + +Other Changes +============= + +Loading 16-bit TIFF Images +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Pillow now can read 16-bit multichannel TIFF files including files +with alpha transparency. The image data is truncated to 8-bit +precision. + +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 +^^^^^^^^^^ + +Pillow can now read and write uncompressed 16-bit multichannel SGI +images to and from RGB and RGBA formats. The image data is truncated +to 8-bit precision. + +Pillow can now read RLE encoded SGI images in both 8 and 16-bit +precision. + +Performance +^^^^^^^^^^^ + +This release contains several performance improvements: + +* Many memory bandwidth-bounded operations such as crop, image allocation, + conversion, split into bands and merging from bands are up to 2x faster. +* Upscaling of multichannel images (such as RGB) is accelerated by 5-10% +* JPEG loading is accelerated up to 15% and JPEG saving up to 20% when + using a recent version of libjpeg-turbo. +* ``Image.transpose`` has been accelerated 15% or more by using a cache + friendly algorithm. +* ImageFilters based on Kernel convolution are significantly faster + due to the new :py:class:`~PIL.ImageFilter.MultibandFilter` feature. +* All memory allocation for images is now done in blocks, rather than + falling back to an allocation for each scan line for images larger + than the block size. + +CMYK Conversion +^^^^^^^^^^^^^^^ + +The basic CMYK->RGB conversion has been tweaked to match the formula +from Google Chrome. This produces an image that is generally lighter +than the previous formula, and more in line with what color managed +applications produce. diff --git a/docs/releasenotes/5.0.0.rst b/docs/releasenotes/5.0.0.rst index be00a45cd..d5715770b 100644 --- a/docs/releasenotes/5.0.0.rst +++ b/docs/releasenotes/5.0.0.rst @@ -1,106 +1,106 @@ -5.0.0 ------ - -Backwards Incompatible Changes -============================== - -Python 3.3 Dropped -^^^^^^^^^^^^^^^^^^ - -Python 3.3 is EOL and no longer supported due to moving testing from nose, -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 -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Pillow has previously emitted warnings for images that are -unexpectedly large and may be a denial of service. These warnings are -now upgraded to ``DecompressionBombError``\s for images that are twice -the size of images that trigger the ``DecompressionBombWarning``. The -default threshold is 128Mpx, or 0.5GB for an ``RGB`` or ``RGBA`` -image. This can be disabled or changed by setting -``Image.MAX_IMAGE_PIXELS = None``. - -Scripts -^^^^^^^ - -The scripts formerly installed by Pillow have been split into a -separate package, pillow-scripts, living at -https://github.com/python-pillow/pillow-scripts. - - -API Changes -=========== - -OleFileIO.py -^^^^^^^^^^^^ - -The :pypi:`olefile` module is no longer a required dependency when installing Pillow. -Support for plugins requiring olefile will not be loaded if it is not -installed. This allows library consumers to avoid installing this dependency -if they choose. Some library consumers have little interest in the format -support and would like to keep dependencies to a minimum. - -Further, the vendored version was removed in Pillow 4.0.0 and replaced with a -deprecation warning that PIL.OleFileIO would be removed in a future version. -This warning has been upgraded to an import error pending future removal. - -Check parameter on _save -^^^^^^^^^^^^^^^^^^^^^^^^ - -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 -============= - -Image.transform -^^^^^^^^^^^^^^^ - -A new named parameter, ``fillcolor``, has been added to -``Image.transform``. This color specifies the background color to use in -the area outside the transformed area in the output image. This -parameter takes the same color specifications as used in ``Image.new``. - -GIF Disposal -^^^^^^^^^^^^ - -Multiframe GIF images now take an optional disposal parameter to -specify the disposal option for changed pixels. - -Other Changes -============= - -Compressed TIFF Images -^^^^^^^^^^^^^^^^^^^^^^ - -Previously, there were some compression modes (JPEG, Packbits, and -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 -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The libraqm dependency for complex text scripts is now linked -dynamically at runtime rather than at packaging time. This allows us -to release binaries with support for libraqm if it is installed on the -user's machine. - -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 -^^^^^^^^^^^^^^^^ - -Multiarch support on Linux should be more robust, especially on Debian -derivatives on ARM platforms. Debian's multiarch platform -configuration is run in preference to the sniffing of machine platform -and architecture. +5.0.0 +----- + +Backwards Incompatible Changes +============================== + +Python 3.3 Dropped +^^^^^^^^^^^^^^^^^^ + +Python 3.3 is EOL and no longer supported due to moving testing from nose, +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 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Pillow has previously emitted warnings for images that are +unexpectedly large and may be a denial of service. These warnings are +now upgraded to ``DecompressionBombError``\s for images that are twice +the size of images that trigger the ``DecompressionBombWarning``. The +default threshold is 128Mpx, or 0.5GB for an ``RGB`` or ``RGBA`` +image. This can be disabled or changed by setting +``Image.MAX_IMAGE_PIXELS = None``. + +Scripts +^^^^^^^ + +The scripts formerly installed by Pillow have been split into a +separate package, pillow-scripts, living at +https://github.com/python-pillow/pillow-scripts. + + +API Changes +=========== + +OleFileIO.py +^^^^^^^^^^^^ + +The :pypi:`olefile` module is no longer a required dependency when installing Pillow. +Support for plugins requiring olefile will not be loaded if it is not +installed. This allows library consumers to avoid installing this dependency +if they choose. Some library consumers have little interest in the format +support and would like to keep dependencies to a minimum. + +Further, the vendored version was removed in Pillow 4.0.0 and replaced with a +deprecation warning that PIL.OleFileIO would be removed in a future version. +This warning has been upgraded to an import error pending future removal. + +Check parameter on _save +^^^^^^^^^^^^^^^^^^^^^^^^ + +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 +============= + +Image.transform +^^^^^^^^^^^^^^^ + +A new named parameter, ``fillcolor``, has been added to +``Image.transform``. This color specifies the background color to use in +the area outside the transformed area in the output image. This +parameter takes the same color specifications as used in ``Image.new``. + +GIF Disposal +^^^^^^^^^^^^ + +Multiframe GIF images now take an optional disposal parameter to +specify the disposal option for changed pixels. + +Other Changes +============= + +Compressed TIFF Images +^^^^^^^^^^^^^^^^^^^^^^ + +Previously, there were some compression modes (JPEG, Packbits, and +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 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The libraqm dependency for complex text scripts is now linked +dynamically at runtime rather than at packaging time. This allows us +to release binaries with support for libraqm if it is installed on the +user's machine. + +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 +^^^^^^^^^^^^^^^^ + +Multiarch support on Linux should be more robust, especially on Debian +derivatives on ARM platforms. Debian's multiarch platform +configuration is run in preference to the sniffing of machine platform +and architecture. diff --git a/docs/releasenotes/5.1.0.rst b/docs/releasenotes/5.1.0.rst index 4e3d10ac5..daeb49d60 100644 --- a/docs/releasenotes/5.1.0.rst +++ b/docs/releasenotes/5.1.0.rst @@ -1,36 +1,36 @@ -5.1.0 ------ - -API Changes -=========== - -Optional channels for TIFF files -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Pillow can now open TIFF files with base modes of ``RGB``, ``YCbCr``, -and ``CMYK`` with up to 6 8-bit channels, discarding any extra -channels if the content is tagged as UNSPECIFIED. Pillow still does -not store more than 4 8-bit channels of image data. - -API Additions -============= - -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 -^^^^^^^^^^^^^^^^^^^ - -Pillow now supports reading the BLP "Blizzard Mipmap" file format used -for tiles in Blizzard's engine. - -Other Changes -============= - -WebP memory leak -^^^^^^^^^^^^^^^^ - -A memory leak when opening ``WebP`` files has been fixed. +5.1.0 +----- + +API Changes +=========== + +Optional channels for TIFF files +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Pillow can now open TIFF files with base modes of ``RGB``, ``YCbCr``, +and ``CMYK`` with up to 6 8-bit channels, discarding any extra +channels if the content is tagged as UNSPECIFIED. Pillow still does +not store more than 4 8-bit channels of image data. + +API Additions +============= + +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 +^^^^^^^^^^^^^^^^^^^ + +Pillow now supports reading the BLP "Blizzard Mipmap" file format used +for tiles in Blizzard's engine. + +Other Changes +============= + +WebP memory leak +^^^^^^^^^^^^^^^^ + +A memory leak when opening ``WebP`` files has been fixed. diff --git a/docs/releasenotes/5.2.0.rst b/docs/releasenotes/5.2.0.rst index d9b8f0fb7..ebfd81641 100644 --- a/docs/releasenotes/5.2.0.rst +++ b/docs/releasenotes/5.2.0.rst @@ -1,113 +1,113 @@ -5.2.0 ------ - -API Changes -=========== - -Deprecations -^^^^^^^^^^^^ - -These version constants have been deprecated. ``VERSION`` will be removed in -Pillow 6.0.0, and ``PILLOW_VERSION`` will be removed after that. - -* ``PIL.VERSION`` (old PIL version 1.1.7) -* ``PIL.PILLOW_VERSION`` -* ``PIL.Image.VERSION`` -* ``PIL.Image.PILLOW_VERSION`` - -Use ``PIL.__version__`` instead. - -API Additions -============= - -3D color lookup tables -^^^^^^^^^^^^^^^^^^^^^^ - -Support for 3D color lookup table transformations has been added. - -* https://en.wikipedia.org/wiki/3D_lookup_table - -``Color3DLUT.generate`` transforms 3-channel pixels using the values of the -channels as coordinates in the 3D lookup table and interpolating the nearest -elements. - -It allows you to apply almost any color transformation in constant time by -using pre-calculated decimated tables. - -``Color3DLUT.transform()`` allows altering table values with a callback. - -If NumPy is installed, the performance of argument conversion is dramatically -improved when a source table supports buffer interface (NumPy && arrays in -Python >= 3). - -ImageColor.getrgb -^^^^^^^^^^^^^^^^^ - -Previously ``Image.rotate`` only supported HSL color strings. Now HSB and HSV -strings are also supported, as well as float values. For example, -``ImageColor.getrgb("hsv(180,100%,99.5%)")``. - -ImageFile.get_format_mimetype -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -``ImageFile.get_format_mimetype`` has been added to return the MIME type of an -image file, where available. For example, -``Image.open("hopper.jpg").get_format_mimetype()`` returns ``"image/jpeg"``. - -ImageFont.getsize_multiline -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -A new method to return the size of multiline text, for example -``font.getsize_multiline("ABC\nAaaa")`` - -Image.rotate -^^^^^^^^^^^^ - -A new named parameter, ``fillcolor``, has been added to ``Image.rotate``. This -color specifies the background color to use in the area outside the rotated -image. This parameter takes the same color specifications as used in -``Image.new``. - - -TGA file format -^^^^^^^^^^^^^^^ - -Pillow can now read and write LA data (in addition to L, P, RGB and RGBA), and -write RLE data (in addition to uncompressed). - -Other Changes -============= - -Support added for Python 3.7 -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Pillow 5.2 supports Python 3.7. - -Build macOS wheels with Xcode 6.4, supporting older macOS versions -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The macOS wheels for Pillow 5.1.0 were built with Xcode 9.2, meaning 10.12 -Sierra was the lowest supported version. - -Prior to Pillow 5.1.0, Xcode 8 was used, supporting El Capitan 10.11. - -Instead, Pillow 5.2.0 is built with the oldest available Xcode 6.4 to support -at least 10.10 Yosemite. - -Fix _i2f compilation with some GCC versions -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -For example, this allows compilation with GCC 4.8 on NetBSD. - -Resolve confusion getting PIL / Pillow version string -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Re: "version constants deprecated" listed above, as user gnbl notes in #3082: - -- it's confusing that PIL.VERSION returns the version string of the former PIL instead of Pillow's -- ReadTheDocs documentation is missing for some version branches (why is this, will it ever change, ...) -- it's confusing that PIL.version is a module and does not return the version information directly or hints on how to get it -- the package information header is essentially useless (placeholder, does not even mention Pillow, nor the version) -- PIL._version module documentation comment could explain how to access the version information - -We have attempted to resolve these issues in #3083, #3090 and #3218. +5.2.0 +----- + +API Changes +=========== + +Deprecations +^^^^^^^^^^^^ + +These version constants have been deprecated. ``VERSION`` will be removed in +Pillow 6.0.0, and ``PILLOW_VERSION`` will be removed after that. + +* ``PIL.VERSION`` (old PIL version 1.1.7) +* ``PIL.PILLOW_VERSION`` +* ``PIL.Image.VERSION`` +* ``PIL.Image.PILLOW_VERSION`` + +Use ``PIL.__version__`` instead. + +API Additions +============= + +3D color lookup tables +^^^^^^^^^^^^^^^^^^^^^^ + +Support for 3D color lookup table transformations has been added. + +* https://en.wikipedia.org/wiki/3D_lookup_table + +``Color3DLUT.generate`` transforms 3-channel pixels using the values of the +channels as coordinates in the 3D lookup table and interpolating the nearest +elements. + +It allows you to apply almost any color transformation in constant time by +using pre-calculated decimated tables. + +``Color3DLUT.transform()`` allows altering table values with a callback. + +If NumPy is installed, the performance of argument conversion is dramatically +improved when a source table supports buffer interface (NumPy && arrays in +Python >= 3). + +ImageColor.getrgb +^^^^^^^^^^^^^^^^^ + +Previously ``Image.rotate`` only supported HSL color strings. Now HSB and HSV +strings are also supported, as well as float values. For example, +``ImageColor.getrgb("hsv(180,100%,99.5%)")``. + +ImageFile.get_format_mimetype +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``ImageFile.get_format_mimetype`` has been added to return the MIME type of an +image file, where available. For example, +``Image.open("hopper.jpg").get_format_mimetype()`` returns ``"image/jpeg"``. + +ImageFont.getsize_multiline +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +A new method to return the size of multiline text, for example +``font.getsize_multiline("ABC\nAaaa")`` + +Image.rotate +^^^^^^^^^^^^ + +A new named parameter, ``fillcolor``, has been added to ``Image.rotate``. This +color specifies the background color to use in the area outside the rotated +image. This parameter takes the same color specifications as used in +``Image.new``. + + +TGA file format +^^^^^^^^^^^^^^^ + +Pillow can now read and write LA data (in addition to L, P, RGB and RGBA), and +write RLE data (in addition to uncompressed). + +Other Changes +============= + +Support added for Python 3.7 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Pillow 5.2 supports Python 3.7. + +Build macOS wheels with Xcode 6.4, supporting older macOS versions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The macOS wheels for Pillow 5.1.0 were built with Xcode 9.2, meaning 10.12 +Sierra was the lowest supported version. + +Prior to Pillow 5.1.0, Xcode 8 was used, supporting El Capitan 10.11. + +Instead, Pillow 5.2.0 is built with the oldest available Xcode 6.4 to support +at least 10.10 Yosemite. + +Fix _i2f compilation with some GCC versions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +For example, this allows compilation with GCC 4.8 on NetBSD. + +Resolve confusion getting PIL / Pillow version string +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Re: "version constants deprecated" listed above, as user gnbl notes in #3082: + +- it's confusing that PIL.VERSION returns the version string of the former PIL instead of Pillow's +- ReadTheDocs documentation is missing for some version branches (why is this, will it ever change, ...) +- it's confusing that PIL.version is a module and does not return the version information directly or hints on how to get it +- the package information header is essentially useless (placeholder, does not even mention Pillow, nor the version) +- PIL._version module documentation comment could explain how to access the version information + +We have attempted to resolve these issues in #3083, #3090 and #3218. diff --git a/docs/releasenotes/5.3.0.rst b/docs/releasenotes/5.3.0.rst index 8f276da24..29f1f9a85 100644 --- a/docs/releasenotes/5.3.0.rst +++ b/docs/releasenotes/5.3.0.rst @@ -1,68 +1,68 @@ -5.3.0 ------ - -API Changes -=========== - -Image size -^^^^^^^^^^ - -If you attempt to set the size of an image directly, e.g. -``im.size = (100, 100)``, you will now receive an :py:exc:`AttributeError`. This is -not about removing existing functionality, but instead about raising an -explicit error to prevent later consequences. The ``resize`` method is the -correct way to change an image's size. - -The exceptions to this are: - -* The ICO and ICNS image formats, which use ``im.size = (100, 100)`` to select a subimage. -* The TIFF image format, which now has a :py:exc:`DeprecationWarning` for this action, - as direct image size setting was previously necessary to work around an issue with tile extents. - - -API Additions -============= - -Added line width parameter to rectangle and ellipse-based shapes -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -An optional line ``width`` parameter has been added to ``ImageDraw.Draw.arc``, -``chord``, ``ellipse``, ``pieslice`` and ``rectangle``. - -Curved joints for line sequences -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -``ImageDraw.Draw.line`` draws a line, or lines, between points. Previously, -when multiple points are given, for a larger ``width``, the joints between -these lines looked unsightly. There is now an additional optional argument, -``joint``, defaulting to :data:`None`. When it is set to ``curved``, the joints -between the lines will become rounded. - -ImageOps.colorize -^^^^^^^^^^^^^^^^^ - -Previously ``ImageOps.colorize`` only supported two-color mapping with -``black`` and ``white`` arguments being mapped to 0 and 255 respectively. -Now it supports three-color mapping with the optional ``mid`` parameter, and -the positions for all three color arguments can each be optionally specified -(``blackpoint``, ``whitepoint`` and ``midpoint``). -For example, with all optional arguments:: - - ImageOps.colorize(im, black=(32, 37, 79), white='white', mid=(59, 101, 175), - blackpoint=15, whitepoint=240, midpoint=100) - -ImageOps.pad -^^^^^^^^^^^^ - -While ``ImageOps.fit`` allows users to crop images to a requested aspect ratio -and size, new method ``ImageOps.pad`` pads images to fill a requested aspect -ratio and size, filling new space with a provided ``color`` and positioning the -image within the new area through a ``centering`` argument. - -Other Changes -============= - -Added support for reading tiled TIFF images through LibTIFF. Compressed TIFF -images are now read through LibTIFF. - -RGB WebP images are now read as RGB mode, rather than RGBX. +5.3.0 +----- + +API Changes +=========== + +Image size +^^^^^^^^^^ + +If you attempt to set the size of an image directly, e.g. +``im.size = (100, 100)``, you will now receive an :py:exc:`AttributeError`. This is +not about removing existing functionality, but instead about raising an +explicit error to prevent later consequences. The ``resize`` method is the +correct way to change an image's size. + +The exceptions to this are: + +* The ICO and ICNS image formats, which use ``im.size = (100, 100)`` to select a subimage. +* The TIFF image format, which now has a :py:exc:`DeprecationWarning` for this action, + as direct image size setting was previously necessary to work around an issue with tile extents. + + +API Additions +============= + +Added line width parameter to rectangle and ellipse-based shapes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +An optional line ``width`` parameter has been added to ``ImageDraw.Draw.arc``, +``chord``, ``ellipse``, ``pieslice`` and ``rectangle``. + +Curved joints for line sequences +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``ImageDraw.Draw.line`` draws a line, or lines, between points. Previously, +when multiple points are given, for a larger ``width``, the joints between +these lines looked unsightly. There is now an additional optional argument, +``joint``, defaulting to :data:`None`. When it is set to ``curved``, the joints +between the lines will become rounded. + +ImageOps.colorize +^^^^^^^^^^^^^^^^^ + +Previously ``ImageOps.colorize`` only supported two-color mapping with +``black`` and ``white`` arguments being mapped to 0 and 255 respectively. +Now it supports three-color mapping with the optional ``mid`` parameter, and +the positions for all three color arguments can each be optionally specified +(``blackpoint``, ``whitepoint`` and ``midpoint``). +For example, with all optional arguments:: + + ImageOps.colorize(im, black=(32, 37, 79), white='white', mid=(59, 101, 175), + blackpoint=15, whitepoint=240, midpoint=100) + +ImageOps.pad +^^^^^^^^^^^^ + +While ``ImageOps.fit`` allows users to crop images to a requested aspect ratio +and size, new method ``ImageOps.pad`` pads images to fill a requested aspect +ratio and size, filling new space with a provided ``color`` and positioning the +image within the new area through a ``centering`` argument. + +Other Changes +============= + +Added support for reading tiled TIFF images through LibTIFF. Compressed TIFF +images are now read through LibTIFF. + +RGB WebP images are now read as RGB mode, rather than RGBX. diff --git a/docs/releasenotes/5.4.0.rst b/docs/releasenotes/5.4.0.rst index 6d7277c70..6980a155f 100644 --- a/docs/releasenotes/5.4.0.rst +++ b/docs/releasenotes/5.4.0.rst @@ -1,67 +1,67 @@ -5.4.0 ------ - -API Changes -=========== - -APNG extension to PNG plugin -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Animated Portable Network Graphics (APNG) images are not fully supported but -can be opened via the PNG plugin to get some basic info:: - - im = Image.open("image.apng") - print(im.mode) # "RGBA" - print(im.size) # (245, 245) - im.show() # Shows a single frame - -Check for libjpeg-turbo -^^^^^^^^^^^^^^^^^^^^^^^ - -You can check if Pillow has been built against the libjpeg-turbo version of the -libjpeg library:: - - from PIL import features - features.check_feature("libjpeg_turbo") # True or False - -Negative indexes in pixel access -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -When accessing individual image pixels, negative indexes are now also accepted. -For example, to get or set the farthest pixel in the lower right of an image:: - - px = im.load() - print(px[-1, -1]) - px[-1, -1] = (0, 0, 0) - - -New custom TIFF tags -^^^^^^^^^^^^^^^^^^^^ - -TIFF images can now be saved with custom integer, float and string TIFF tags:: - - im = Image.new("RGB", (200, 100)) - custom = { - 37000: 4, - 37001: 4.2, - 37002: "custom tag value", - 37003: u"custom tag value", - 37004: b"custom tag value", - } - im.save("output.tif", tiffinfo=custom) - - im2 = Image.open("output.tif") - print(im2.tag_v2[37000]) # 4 - print(im2.tag_v2[37002]) # "custom tag value" - print(im2.tag_v2[37004]) # b"custom tag value" - -Other Changes -============= - -ImageOps.fit -^^^^^^^^^^^^ - -Now uses one resize operation with ``box`` parameter internally -instead of a crop and scale operations sequence. -This improves the performance and accuracy of cropping since -the ``box`` parameter accepts float values. +5.4.0 +----- + +API Changes +=========== + +APNG extension to PNG plugin +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Animated Portable Network Graphics (APNG) images are not fully supported but +can be opened via the PNG plugin to get some basic info:: + + im = Image.open("image.apng") + print(im.mode) # "RGBA" + print(im.size) # (245, 245) + im.show() # Shows a single frame + +Check for libjpeg-turbo +^^^^^^^^^^^^^^^^^^^^^^^ + +You can check if Pillow has been built against the libjpeg-turbo version of the +libjpeg library:: + + from PIL import features + features.check_feature("libjpeg_turbo") # True or False + +Negative indexes in pixel access +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When accessing individual image pixels, negative indexes are now also accepted. +For example, to get or set the farthest pixel in the lower right of an image:: + + px = im.load() + print(px[-1, -1]) + px[-1, -1] = (0, 0, 0) + + +New custom TIFF tags +^^^^^^^^^^^^^^^^^^^^ + +TIFF images can now be saved with custom integer, float and string TIFF tags:: + + im = Image.new("RGB", (200, 100)) + custom = { + 37000: 4, + 37001: 4.2, + 37002: "custom tag value", + 37003: u"custom tag value", + 37004: b"custom tag value", + } + im.save("output.tif", tiffinfo=custom) + + im2 = Image.open("output.tif") + print(im2.tag_v2[37000]) # 4 + print(im2.tag_v2[37002]) # "custom tag value" + print(im2.tag_v2[37004]) # b"custom tag value" + +Other Changes +============= + +ImageOps.fit +^^^^^^^^^^^^ + +Now uses one resize operation with ``box`` parameter internally +instead of a crop and scale operations sequence. +This improves the performance and accuracy of cropping since +the ``box`` parameter accepts float values. diff --git a/docs/releasenotes/5.4.1.rst b/docs/releasenotes/5.4.1.rst index bbabd6520..d532dc839 100644 --- a/docs/releasenotes/5.4.1.rst +++ b/docs/releasenotes/5.4.1.rst @@ -1,36 +1,36 @@ -5.4.1 ------ - -This release fixes regressions in 5.4.0. - -Installation on Termux -^^^^^^^^^^^^^^^^^^^^^^ - -A change to the way Pillow detects libraries during installed prevented -installation on Termux, which does not have ``/sbin/ldconfig``. This is now -fixed. - -PNG: Handle IDAT chunks after image end -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Some PNG images have multiple IDAT chunks. In some cases, Pillow will stop -reading image data before the IDAT chunks finish. A regression caused an -:py:exc:`EOFError` exception when previously there was none. This is now fixed, and -file reading continues in case there are subsequent text chunks. - -PNG: MIME type -^^^^^^^^^^^^^^ - -The addition of limited APNG support to the PNG plugin also overwrote the MIME -type for PNG files, causing "image/apng" to be returned as the MIME type of -both APNG and PNG files. This has been fixed so the MIME type of PNG files is -"image/png". - -File closing -^^^^^^^^^^^^ - -A regression caused an unsupported image file to report a -``ValueError: seek of closed file`` exception instead of an :py:exc:`OSError`. This -has been fixed by ensuring that image plugins only close their internal ``__fp`` -if they are not the same as ``ImageFile``'s ``fp``, allowing each to manage their own -file pointers. +5.4.1 +----- + +This release fixes regressions in 5.4.0. + +Installation on Termux +^^^^^^^^^^^^^^^^^^^^^^ + +A change to the way Pillow detects libraries during installed prevented +installation on Termux, which does not have ``/sbin/ldconfig``. This is now +fixed. + +PNG: Handle IDAT chunks after image end +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Some PNG images have multiple IDAT chunks. In some cases, Pillow will stop +reading image data before the IDAT chunks finish. A regression caused an +:py:exc:`EOFError` exception when previously there was none. This is now fixed, and +file reading continues in case there are subsequent text chunks. + +PNG: MIME type +^^^^^^^^^^^^^^ + +The addition of limited APNG support to the PNG plugin also overwrote the MIME +type for PNG files, causing "image/apng" to be returned as the MIME type of +both APNG and PNG files. This has been fixed so the MIME type of PNG files is +"image/png". + +File closing +^^^^^^^^^^^^ + +A regression caused an unsupported image file to report a +``ValueError: seek of closed file`` exception instead of an :py:exc:`OSError`. This +has been fixed by ensuring that image plugins only close their internal ``__fp`` +if they are not the same as ``ImageFile``'s ``fp``, allowing each to manage their own +file pointers. diff --git a/docs/releasenotes/6.0.0.rst b/docs/releasenotes/6.0.0.rst index 5e69f0b6b..1670b0988 100644 --- a/docs/releasenotes/6.0.0.rst +++ b/docs/releasenotes/6.0.0.rst @@ -1,212 +1,212 @@ -6.0.0 ------ - -Backwards Incompatible Changes -============================== - -Python 3.4 dropped -^^^^^^^^^^^^^^^^^^ - -Python 3.4 is EOL since 2019-03-16 and no longer supported. We will not be creating -binaries, testing, or retaining compatibility with this version. The final version of -Pillow for Python 3.4 is 5.4.1. - -Removed deprecated PIL.OleFileIO -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -``PIL.OleFileIO`` was removed as a vendored file and in Pillow 4.0.0 (2017-01) in favour of -the upstream :pypi:`olefile` Python package, and replaced with an :py:exc:`ImportError`. The -deprecated file has now been removed from Pillow. If needed, install from PyPI (eg. -``python3 -m pip install olefile``). - -Removed deprecated ImageOps functions -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Several undocumented functions in ``ImageOps`` were deprecated in Pillow 4.3.0 (2017-10) -and have now been removed: ``gaussian_blur``, ``gblur``, ``unsharp_mask``, ``usm`` and -``box_blur``. Use the equivalent operations in ``ImageFilter`` instead. - -Removed deprecated VERSION -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -``VERSION`` (the old PIL version, always 1.1.7) has been removed. Use ``__version__`` -instead. - -API Changes -=========== - -Deprecations -^^^^^^^^^^^^ - -Python 2.7 -~~~~~~~~~~ - -Python 2.7 reaches end-of-life on 2020-01-01. - -Pillow 7.0.0 will be released on 2020-01-01 and will drop support for Python 2.7, making -Pillow 6.x the last series to support Python 2. - -PyQt4 and PySide -~~~~~~~~~~~~~~~~ - -Qt 4 reached end-of-life on 2015-12-19. Its Python bindings are also EOL: PyQt4 since -2018-08-31 and PySide since 2015-10-14. - -Support for PyQt4 and PySide has been deprecated from ``ImageQt`` and will be removed in -a future version. Please upgrade to PyQt5 or PySide2. - -PIL.*ImagePlugin.__version__ attributes -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -These version constants have been deprecated and will be removed in a future -version. - -* ``BmpImagePlugin.__version__`` -* ``CurImagePlugin.__version__`` -* ``DcxImagePlugin.__version__`` -* ``EpsImagePlugin.__version__`` -* ``FliImagePlugin.__version__`` -* ``FpxImagePlugin.__version__`` -* ``GdImageFile.__version__`` -* ``GifImagePlugin.__version__`` -* ``IcoImagePlugin.__version__`` -* ``ImImagePlugin.__version__`` -* ``ImtImagePlugin.__version__`` -* ``IptcImagePlugin.__version__`` -* ``Jpeg2KImagePlugin.__version__`` -* ``JpegImagePlugin.__version__`` -* ``McIdasImagePlugin.__version__`` -* ``MicImagePlugin.__version__`` -* ``MpegImagePlugin.__version__`` -* ``MpoImagePlugin.__version__`` -* ``MspImagePlugin.__version__`` -* ``PalmImagePlugin.__version__`` -* ``PcdImagePlugin.__version__`` -* ``PcxImagePlugin.__version__`` -* ``PdfImagePlugin.__version__`` -* ``PixarImagePlugin.__version__`` -* ``PngImagePlugin.__version__`` -* ``PpmImagePlugin.__version__`` -* ``PsdImagePlugin.__version__`` -* ``SgiImagePlugin.__version__`` -* ``SunImagePlugin.__version__`` -* ``TgaImagePlugin.__version__`` -* ``TiffImagePlugin.__version__`` -* ``WmfImagePlugin.__version__`` -* ``XbmImagePlugin.__version__`` -* ``XpmImagePlugin.__version__`` -* ``XVThumbImagePlugin.__version__`` - -Use ``PIL.__version__`` instead. - -ImageCms.CmsProfile attributes -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Some attributes in ``ImageCms.CmsProfile`` have been deprecated since Pillow 3.2.0. From -6.0.0, they issue a :py:exc:`DeprecationWarning`: - -======================== =============================== -Deprecated Use instead -======================== =============================== -``color_space`` Padded ``xcolor_space`` -``pcs`` Padded ``connection_space`` -``product_copyright`` Unicode ``copyright`` -``product_desc`` Unicode ``profile_description`` -``product_description`` Unicode ``profile_description`` -``product_manufacturer`` Unicode ``manufacturer`` -``product_model`` Unicode ``model`` -======================== =============================== - -MIME type improvements -^^^^^^^^^^^^^^^^^^^^^^ - -Previously, all JPEG2000 images had the MIME type "image/jpx". This has now been -corrected. After the file format drivers have been loaded, ``Image.MIME["JPEG2000"]`` -will return "image/jp2". ``ImageFile.get_format_mimetype`` will return "image/jpx" if -a JPX profile is present, or "image/jp2" otherwise. - -Previously, all SGI images had the MIME type "image/rgb". This has now been -corrected. After the file format drivers have been loaded, ``Image.MIME["SGI"]`` -will return "image/sgi". ``ImageFile.get_format_mimetype`` will return "image/rgb" if -RGB image data is present, or "image/sgi" otherwise. - -MIME types have been added to the PPM format. After the file format drivers have been -loaded, ``Image.MIME["PPM"]`` will now return the generic "image/x-portable-anymap". -``ImageFile.get_format_mimetype`` will return a MIME type specific to the color type. - -The TGA, PCX and ICO formats also now have MIME types: "image/x-tga", "image/x-pcx" and -"image/x-icon" respectively. - -API Additions -============= - -DIB file format -^^^^^^^^^^^^^^^ - -Pillow now supports reading and writing the Device Independent Bitmap file format. - -Image.quantize -^^^^^^^^^^^^^^ - -The ``dither`` option is now a customisable parameter (was previously hardcoded to ``1``). -This parameter takes the same values used in :py:meth:`~PIL.Image.Image.convert`. - -New language parameter -^^^^^^^^^^^^^^^^^^^^^^ - -These text-rendering functions now accept a ``language`` parameter to request -language-specific glyphs and ligatures from the font: - -* ``ImageDraw.ImageDraw.multiline_text()`` -* ``ImageDraw.ImageDraw.multiline_textsize()`` -* ``ImageDraw.ImageDraw.text()`` -* ``ImageDraw.ImageDraw.textsize()`` -* ``ImageFont.ImageFont.getmask()`` -* ``ImageFont.ImageFont.getsize_multiline()`` -* ``ImageFont.ImageFont.getsize()`` - -Added EXIF class -^^^^^^^^^^^^^^^^ - -:py:meth:`~PIL.Image.Image.getexif` has been added, which returns an -:py:class:`~PIL.Image.Exif` instance. Values can be retrieved and set like a -dictionary. When saving JPEG, PNG or WEBP, the instance can be passed as an -``exif`` argument to include any changes in the output image. - -Added ImageOps.exif_transpose -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -:py:meth:`~PIL.ImageOps.exif_transpose` returns a copy of an image, transposed -according to its EXIF Orientation tag. - -PNG EXIF data -^^^^^^^^^^^^^ - -EXIF data can now be read from and saved to PNG images. However, unlike other image -formats, EXIF data is not guaranteed to be present in :py:attr:`~PIL.Image.Image.info` -until :py:meth:`~PIL.Image.Image.load` has been called. - -Other Changes -============= - -Reading new DDS image format -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Pillow can now read uncompressed RGB data from DDS images. - -Reading TIFF with old-style JPEG compression -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Added support reading TIFF files with old-style JPEG compression through LibTIFF. All -YCbCr TIFF images are now always read as RGB. - -TIFF compression codecs -^^^^^^^^^^^^^^^^^^^^^^^ - -Support has been added for the LZMA, Zstd and WebP TIFF compression codecs. - -Improved support for transposing I;16 images -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -I;16, I;16L and I;16B are now supported image modes for all -:py:meth:`~PIL.Image.Image.transpose` operations. +6.0.0 +----- + +Backwards Incompatible Changes +============================== + +Python 3.4 dropped +^^^^^^^^^^^^^^^^^^ + +Python 3.4 is EOL since 2019-03-16 and no longer supported. We will not be creating +binaries, testing, or retaining compatibility with this version. The final version of +Pillow for Python 3.4 is 5.4.1. + +Removed deprecated PIL.OleFileIO +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``PIL.OleFileIO`` was removed as a vendored file and in Pillow 4.0.0 (2017-01) in favour of +the upstream :pypi:`olefile` Python package, and replaced with an :py:exc:`ImportError`. The +deprecated file has now been removed from Pillow. If needed, install from PyPI (eg. +``python3 -m pip install olefile``). + +Removed deprecated ImageOps functions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Several undocumented functions in ``ImageOps`` were deprecated in Pillow 4.3.0 (2017-10) +and have now been removed: ``gaussian_blur``, ``gblur``, ``unsharp_mask``, ``usm`` and +``box_blur``. Use the equivalent operations in ``ImageFilter`` instead. + +Removed deprecated VERSION +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``VERSION`` (the old PIL version, always 1.1.7) has been removed. Use ``__version__`` +instead. + +API Changes +=========== + +Deprecations +^^^^^^^^^^^^ + +Python 2.7 +~~~~~~~~~~ + +Python 2.7 reaches end-of-life on 2020-01-01. + +Pillow 7.0.0 will be released on 2020-01-01 and will drop support for Python 2.7, making +Pillow 6.x the last series to support Python 2. + +PyQt4 and PySide +~~~~~~~~~~~~~~~~ + +Qt 4 reached end-of-life on 2015-12-19. Its Python bindings are also EOL: PyQt4 since +2018-08-31 and PySide since 2015-10-14. + +Support for PyQt4 and PySide has been deprecated from ``ImageQt`` and will be removed in +a future version. Please upgrade to PyQt5 or PySide2. + +PIL.*ImagePlugin.__version__ attributes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +These version constants have been deprecated and will be removed in a future +version. + +* ``BmpImagePlugin.__version__`` +* ``CurImagePlugin.__version__`` +* ``DcxImagePlugin.__version__`` +* ``EpsImagePlugin.__version__`` +* ``FliImagePlugin.__version__`` +* ``FpxImagePlugin.__version__`` +* ``GdImageFile.__version__`` +* ``GifImagePlugin.__version__`` +* ``IcoImagePlugin.__version__`` +* ``ImImagePlugin.__version__`` +* ``ImtImagePlugin.__version__`` +* ``IptcImagePlugin.__version__`` +* ``Jpeg2KImagePlugin.__version__`` +* ``JpegImagePlugin.__version__`` +* ``McIdasImagePlugin.__version__`` +* ``MicImagePlugin.__version__`` +* ``MpegImagePlugin.__version__`` +* ``MpoImagePlugin.__version__`` +* ``MspImagePlugin.__version__`` +* ``PalmImagePlugin.__version__`` +* ``PcdImagePlugin.__version__`` +* ``PcxImagePlugin.__version__`` +* ``PdfImagePlugin.__version__`` +* ``PixarImagePlugin.__version__`` +* ``PngImagePlugin.__version__`` +* ``PpmImagePlugin.__version__`` +* ``PsdImagePlugin.__version__`` +* ``SgiImagePlugin.__version__`` +* ``SunImagePlugin.__version__`` +* ``TgaImagePlugin.__version__`` +* ``TiffImagePlugin.__version__`` +* ``WmfImagePlugin.__version__`` +* ``XbmImagePlugin.__version__`` +* ``XpmImagePlugin.__version__`` +* ``XVThumbImagePlugin.__version__`` + +Use ``PIL.__version__`` instead. + +ImageCms.CmsProfile attributes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Some attributes in ``ImageCms.CmsProfile`` have been deprecated since Pillow 3.2.0. From +6.0.0, they issue a :py:exc:`DeprecationWarning`: + +======================== =============================== +Deprecated Use instead +======================== =============================== +``color_space`` Padded ``xcolor_space`` +``pcs`` Padded ``connection_space`` +``product_copyright`` Unicode ``copyright`` +``product_desc`` Unicode ``profile_description`` +``product_description`` Unicode ``profile_description`` +``product_manufacturer`` Unicode ``manufacturer`` +``product_model`` Unicode ``model`` +======================== =============================== + +MIME type improvements +^^^^^^^^^^^^^^^^^^^^^^ + +Previously, all JPEG2000 images had the MIME type "image/jpx". This has now been +corrected. After the file format drivers have been loaded, ``Image.MIME["JPEG2000"]`` +will return "image/jp2". ``ImageFile.get_format_mimetype`` will return "image/jpx" if +a JPX profile is present, or "image/jp2" otherwise. + +Previously, all SGI images had the MIME type "image/rgb". This has now been +corrected. After the file format drivers have been loaded, ``Image.MIME["SGI"]`` +will return "image/sgi". ``ImageFile.get_format_mimetype`` will return "image/rgb" if +RGB image data is present, or "image/sgi" otherwise. + +MIME types have been added to the PPM format. After the file format drivers have been +loaded, ``Image.MIME["PPM"]`` will now return the generic "image/x-portable-anymap". +``ImageFile.get_format_mimetype`` will return a MIME type specific to the color type. + +The TGA, PCX and ICO formats also now have MIME types: "image/x-tga", "image/x-pcx" and +"image/x-icon" respectively. + +API Additions +============= + +DIB file format +^^^^^^^^^^^^^^^ + +Pillow now supports reading and writing the Device Independent Bitmap file format. + +Image.quantize +^^^^^^^^^^^^^^ + +The ``dither`` option is now a customisable parameter (was previously hardcoded to ``1``). +This parameter takes the same values used in :py:meth:`~PIL.Image.Image.convert`. + +New language parameter +^^^^^^^^^^^^^^^^^^^^^^ + +These text-rendering functions now accept a ``language`` parameter to request +language-specific glyphs and ligatures from the font: + +* ``ImageDraw.ImageDraw.multiline_text()`` +* ``ImageDraw.ImageDraw.multiline_textsize()`` +* ``ImageDraw.ImageDraw.text()`` +* ``ImageDraw.ImageDraw.textsize()`` +* ``ImageFont.ImageFont.getmask()`` +* ``ImageFont.ImageFont.getsize_multiline()`` +* ``ImageFont.ImageFont.getsize()`` + +Added EXIF class +^^^^^^^^^^^^^^^^ + +:py:meth:`~PIL.Image.Image.getexif` has been added, which returns an +:py:class:`~PIL.Image.Exif` instance. Values can be retrieved and set like a +dictionary. When saving JPEG, PNG or WEBP, the instance can be passed as an +``exif`` argument to include any changes in the output image. + +Added ImageOps.exif_transpose +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:py:meth:`~PIL.ImageOps.exif_transpose` returns a copy of an image, transposed +according to its EXIF Orientation tag. + +PNG EXIF data +^^^^^^^^^^^^^ + +EXIF data can now be read from and saved to PNG images. However, unlike other image +formats, EXIF data is not guaranteed to be present in :py:attr:`~PIL.Image.Image.info` +until :py:meth:`~PIL.Image.Image.load` has been called. + +Other Changes +============= + +Reading new DDS image format +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Pillow can now read uncompressed RGB data from DDS images. + +Reading TIFF with old-style JPEG compression +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Added support reading TIFF files with old-style JPEG compression through LibTIFF. All +YCbCr TIFF images are now always read as RGB. + +TIFF compression codecs +^^^^^^^^^^^^^^^^^^^^^^^ + +Support has been added for the LZMA, Zstd and WebP TIFF compression codecs. + +Improved support for transposing I;16 images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +I;16, I;16L and I;16B are now supported image modes for all +:py:meth:`~PIL.Image.Image.transpose` operations. diff --git a/docs/releasenotes/6.1.0.rst b/docs/releasenotes/6.1.0.rst index ce3edc5fa..28d32ce83 100644 --- a/docs/releasenotes/6.1.0.rst +++ b/docs/releasenotes/6.1.0.rst @@ -1,105 +1,105 @@ -6.1.0 ------ - -Deprecations -============ - -Image.__del__ -^^^^^^^^^^^^^ - -.. deprecated:: 6.1.0 - -Implicitly closing the image's underlying file in ``Image.__del__`` has been deprecated. -Use a context manager or call ``Image.close()`` instead to close the file in a -deterministic way. - -Deprecated:: - - im = Image.open("hopper.png") - im.save("out.jpg") - -Use instead:: - - with Image.open("hopper.png") as im: - im.save("out.jpg") - -API Additions -============= - -Image.entropy -^^^^^^^^^^^^^ -Calculates and returns the entropy for the image. A bilevel image (mode "1") is treated -as a grayscale ("L") image by this method. If a mask is provided, the method employs -the histogram for those parts of the image where the mask image is non-zero. The mask -image must have the same size as the image, and be either a bi-level image (mode "1") or -a grayscale image ("L"). - -ImageGrab.grab -^^^^^^^^^^^^^^ - -An optional ``include_layered_windows`` parameter has been added to ``ImageGrab.grab``, -defaulting to ``False``. If true, layered windows will be included in the resulting -image on Windows. - -ImageSequence.all_frames -^^^^^^^^^^^^^^^^^^^^^^^^ - -A new method to facilitate applying a given function to all frames in an image, or to -all frames in a list of images. The frames are returned as a list of separate images. -For example, ``ImageSequence.all_frames(im, lambda im_frame: im_frame.rotate(90))`` -could be used to return all frames from an image, each rotated 90 degrees. - -Variation fonts -^^^^^^^^^^^^^^^ - -Variation fonts are now supported, allowing for different styles from the same font -file. ``ImageFont.FreeTypeFont`` has four new methods, -:py:meth:`PIL.ImageFont.FreeTypeFont.get_variation_names` and -:py:meth:`PIL.ImageFont.FreeTypeFont.set_variation_by_name` for using named styles, and -:py:meth:`PIL.ImageFont.FreeTypeFont.get_variation_axes` and -:py:meth:`PIL.ImageFont.FreeTypeFont.set_variation_by_axes` for using font axes -instead. An :py:exc:`IOError` will be raised if the font is not a variation font. FreeType -2.9.1 or greater is required. - -Other Changes -============= - -ImageTk.getimage -^^^^^^^^^^^^^^^^ - -This function is now supported. It returns the contents of an ``ImageTk.PhotoImage`` as -an RGBA ``Image.Image`` instance. - -Image quality for JPEG compressed TIFF -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The TIFF encoder accepts a ``quality`` parameter for ``jpeg`` compressed TIFF files. A -value from 0 (worst) to 100 (best) controls the image quality, similar to the JPEG -encoder. The default is 75. For example:: - - im.save("out.tif", compression="jpeg", quality=85) - -Improve encoding of TIFF tags -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The TIFF encoder supports more types, especially arrays. This is required for the -GeoTIFF format which encodes geospatial information. - -* Pass ``tagtype`` from v2 directory to libtiff encoder, instead of autodetecting type. -* Use explicit types eg. ``uint32_t`` for ``TIFF_LONG`` to fix issues on platforms with - 64-bit longs. -* Add support for multiple values (arrays). Requires type in v2 directory and values - must be passed as a tuple. -* Add support for signed types eg. ``TIFFTypes.TIFF_SIGNED_SHORT``. - -Respect PKG_CONFIG environment variable when building -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -This variable is commonly used by other build systems and using it can help with -cross-compiling. Falls back to ``pkg-config`` as before. - -Top-to-bottom complex text rendering -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Drawing text in the 'ttb' direction with ``ImageFont`` has been significantly improved -and requires Raqm 0.7 or greater. +6.1.0 +----- + +Deprecations +============ + +Image.__del__ +^^^^^^^^^^^^^ + +.. deprecated:: 6.1.0 + +Implicitly closing the image's underlying file in ``Image.__del__`` has been deprecated. +Use a context manager or call ``Image.close()`` instead to close the file in a +deterministic way. + +Deprecated:: + + im = Image.open("hopper.png") + im.save("out.jpg") + +Use instead:: + + with Image.open("hopper.png") as im: + im.save("out.jpg") + +API Additions +============= + +Image.entropy +^^^^^^^^^^^^^ +Calculates and returns the entropy for the image. A bilevel image (mode "1") is treated +as a grayscale ("L") image by this method. If a mask is provided, the method employs +the histogram for those parts of the image where the mask image is non-zero. The mask +image must have the same size as the image, and be either a bi-level image (mode "1") or +a grayscale image ("L"). + +ImageGrab.grab +^^^^^^^^^^^^^^ + +An optional ``include_layered_windows`` parameter has been added to ``ImageGrab.grab``, +defaulting to ``False``. If true, layered windows will be included in the resulting +image on Windows. + +ImageSequence.all_frames +^^^^^^^^^^^^^^^^^^^^^^^^ + +A new method to facilitate applying a given function to all frames in an image, or to +all frames in a list of images. The frames are returned as a list of separate images. +For example, ``ImageSequence.all_frames(im, lambda im_frame: im_frame.rotate(90))`` +could be used to return all frames from an image, each rotated 90 degrees. + +Variation fonts +^^^^^^^^^^^^^^^ + +Variation fonts are now supported, allowing for different styles from the same font +file. ``ImageFont.FreeTypeFont`` has four new methods, +:py:meth:`PIL.ImageFont.FreeTypeFont.get_variation_names` and +:py:meth:`PIL.ImageFont.FreeTypeFont.set_variation_by_name` for using named styles, and +:py:meth:`PIL.ImageFont.FreeTypeFont.get_variation_axes` and +:py:meth:`PIL.ImageFont.FreeTypeFont.set_variation_by_axes` for using font axes +instead. An :py:exc:`IOError` will be raised if the font is not a variation font. FreeType +2.9.1 or greater is required. + +Other Changes +============= + +ImageTk.getimage +^^^^^^^^^^^^^^^^ + +This function is now supported. It returns the contents of an ``ImageTk.PhotoImage`` as +an RGBA ``Image.Image`` instance. + +Image quality for JPEG compressed TIFF +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The TIFF encoder accepts a ``quality`` parameter for ``jpeg`` compressed TIFF files. A +value from 0 (worst) to 100 (best) controls the image quality, similar to the JPEG +encoder. The default is 75. For example:: + + im.save("out.tif", compression="jpeg", quality=85) + +Improve encoding of TIFF tags +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The TIFF encoder supports more types, especially arrays. This is required for the +GeoTIFF format which encodes geospatial information. + +* Pass ``tagtype`` from v2 directory to libtiff encoder, instead of autodetecting type. +* Use explicit types eg. ``uint32_t`` for ``TIFF_LONG`` to fix issues on platforms with + 64-bit longs. +* Add support for multiple values (arrays). Requires type in v2 directory and values + must be passed as a tuple. +* Add support for signed types eg. ``TIFFTypes.TIFF_SIGNED_SHORT``. + +Respect PKG_CONFIG environment variable when building +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This variable is commonly used by other build systems and using it can help with +cross-compiling. Falls back to ``pkg-config`` as before. + +Top-to-bottom complex text rendering +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Drawing text in the 'ttb' direction with ``ImageFont`` has been significantly improved +and requires Raqm 0.7 or greater. diff --git a/docs/releasenotes/6.2.0.rst b/docs/releasenotes/6.2.0.rst index b851c56fc..53f2f432d 100644 --- a/docs/releasenotes/6.2.0.rst +++ b/docs/releasenotes/6.2.0.rst @@ -1,112 +1,112 @@ -6.2.0 ------ - -Security -======== - -This release catches several buffer overruns and fixes :cve:`2019-16865`. - -Buffer overruns -^^^^^^^^^^^^^^^ - -In ``RawDecode.c``, an error is now thrown if skip is calculated to be less than -zero. It is intended to skip padding between lines, not to go backwards. - -In ``PsdImagePlugin``, if the combined sizes of the individual parts is larger than -the declared size of the extra data field, then it looked for the next layer by -seeking backwards. This is now corrected by seeking to (the start of the layer -+ the size of the extra data field) instead of (the read parts of the layer + -the rest of the layer). - -Decompression bomb checks have been added to GIF and ICO formats. - -An error is now raised if a TIFF dimension is a string, rather than trying to -perform operations on it. - -:cve:`2019-16865`: Fix DOS attack -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -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 -=========== - -Image.getexif -^^^^^^^^^^^^^ - -To allow for lazy loading of Exif data, ``Image.getexif()`` now returns a -shared instance of ``Image.Exif``. - -Deprecations -^^^^^^^^^^^^ - -Image.frombuffer -~~~~~~~~~~~~~~~~ - -There has been a longstanding warning that the defaults of ``Image.frombuffer`` -may change in the future for the "raw" decoder. The change will now take place -in Pillow 7.0. - -API Additions -============= - -Text stroking -^^^^^^^^^^^^^ - -``stroke_width`` and ``stroke_fill`` arguments have been added to text drawing -operations. They allow text to be outlined, setting the width of the stroke and -and the color respectively. If not provided, ``stroke_fill`` will default to -the ``fill`` parameter. :: - - from PIL import Image, ImageDraw, ImageFont - - font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 40) - font.getsize_multiline("A", stroke_width=2) - font.getsize("ABC\nAaaa", stroke_width=2) - - im = Image.new("RGB", (100, 100)) - draw = ImageDraw.Draw(im) - draw.textsize("A", font, stroke_width=2) - draw.multiline_textsize("ABC\nAaaa", font, stroke_width=2) - draw.text((10, 10), "A", "#f00", font, stroke_width=2, stroke_fill="#0f0") - draw.multiline_text((10, 10), "A\nB", "#f00", font, - stroke_width=2, stroke_fill="#0f0") - -For example, :: - - from PIL import Image, ImageDraw, ImageFont - - im = Image.new("RGB", (120, 130)) - draw = ImageDraw.Draw(im) - font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120) - draw.text((10, 10), "A", "#f00", font, stroke_width=2, stroke_fill="#0f0") - - -creates the following image: - -.. image:: ../../Tests/images/imagedraw_stroke_different.png - -ImageGrab on multi-monitor Windows -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -An ``all_screens`` argument has been added to ``ImageGrab.grab``. If ``True``, -all monitors will be included in the created image. - -Other Changes -============= - -Removed bdist_wininst .exe installers -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.exe installers fell out of favour with :pep:`527`, and will be deprecated in -Python 3.8. Pillow will no longer be distributing them. Wheels should be used -instead. - -Flags for libwebp in wheels -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -When building libwebp for inclusion in wheels, Pillow now adds the ``-O3`` and -``-DNDEBUG`` CFLAGS. These flags would be used by default if building libwebp -without debugging, and using them fixes a significant decrease in speed when -a wheel-installed copy of Pillow performs libwebp operations. +6.2.0 +----- + +Security +======== + +This release catches several buffer overruns and fixes :cve:`2019-16865`. + +Buffer overruns +^^^^^^^^^^^^^^^ + +In ``RawDecode.c``, an error is now thrown if skip is calculated to be less than +zero. It is intended to skip padding between lines, not to go backwards. + +In ``PsdImagePlugin``, if the combined sizes of the individual parts is larger than +the declared size of the extra data field, then it looked for the next layer by +seeking backwards. This is now corrected by seeking to (the start of the layer ++ the size of the extra data field) instead of (the read parts of the layer + +the rest of the layer). + +Decompression bomb checks have been added to GIF and ICO formats. + +An error is now raised if a TIFF dimension is a string, rather than trying to +perform operations on it. + +:cve:`2019-16865`: Fix DOS attack +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +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 +=========== + +Image.getexif +^^^^^^^^^^^^^ + +To allow for lazy loading of Exif data, ``Image.getexif()`` now returns a +shared instance of ``Image.Exif``. + +Deprecations +^^^^^^^^^^^^ + +Image.frombuffer +~~~~~~~~~~~~~~~~ + +There has been a longstanding warning that the defaults of ``Image.frombuffer`` +may change in the future for the "raw" decoder. The change will now take place +in Pillow 7.0. + +API Additions +============= + +Text stroking +^^^^^^^^^^^^^ + +``stroke_width`` and ``stroke_fill`` arguments have been added to text drawing +operations. They allow text to be outlined, setting the width of the stroke and +and the color respectively. If not provided, ``stroke_fill`` will default to +the ``fill`` parameter. :: + + from PIL import Image, ImageDraw, ImageFont + + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 40) + font.getsize_multiline("A", stroke_width=2) + font.getsize("ABC\nAaaa", stroke_width=2) + + im = Image.new("RGB", (100, 100)) + draw = ImageDraw.Draw(im) + draw.textsize("A", font, stroke_width=2) + draw.multiline_textsize("ABC\nAaaa", font, stroke_width=2) + draw.text((10, 10), "A", "#f00", font, stroke_width=2, stroke_fill="#0f0") + draw.multiline_text((10, 10), "A\nB", "#f00", font, + stroke_width=2, stroke_fill="#0f0") + +For example, :: + + from PIL import Image, ImageDraw, ImageFont + + im = Image.new("RGB", (120, 130)) + draw = ImageDraw.Draw(im) + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120) + draw.text((10, 10), "A", "#f00", font, stroke_width=2, stroke_fill="#0f0") + + +creates the following image: + +.. image:: ../../Tests/images/imagedraw_stroke_different.png + +ImageGrab on multi-monitor Windows +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +An ``all_screens`` argument has been added to ``ImageGrab.grab``. If ``True``, +all monitors will be included in the created image. + +Other Changes +============= + +Removed bdist_wininst .exe installers +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.exe installers fell out of favour with :pep:`527`, and will be deprecated in +Python 3.8. Pillow will no longer be distributing them. Wheels should be used +instead. + +Flags for libwebp in wheels +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When building libwebp for inclusion in wheels, Pillow now adds the ``-O3`` and +``-DNDEBUG`` CFLAGS. These flags would be used by default if building libwebp +without debugging, and using them fixes a significant decrease in speed when +a wheel-installed copy of Pillow performs libwebp operations. diff --git a/docs/releasenotes/6.2.1.rst b/docs/releasenotes/6.2.1.rst index 372298fbc..4ea71b78b 100644 --- a/docs/releasenotes/6.2.1.rst +++ b/docs/releasenotes/6.2.1.rst @@ -1,24 +1,24 @@ -6.2.1 ------ - -API Changes -=========== - -Deprecations -^^^^^^^^^^^^ - -Python 2.7 -~~~~~~~~~~ - -Python 2.7 reaches end-of-life on 2020-01-01. - -Pillow 7.0.0 will be released on 2020-01-01 and will drop support for Python -2.7, making Pillow 6.2.x the last release series to support Python 2. - -Other Changes -============= - -Support added for Python 3.8 -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Pillow 6.2.1 supports Python 3.8. +6.2.1 +----- + +API Changes +=========== + +Deprecations +^^^^^^^^^^^^ + +Python 2.7 +~~~~~~~~~~ + +Python 2.7 reaches end-of-life on 2020-01-01. + +Pillow 7.0.0 will be released on 2020-01-01 and will drop support for Python +2.7, making Pillow 6.2.x the last release series to support Python 2. + +Other Changes +============= + +Support added for Python 3.8 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Pillow 6.2.1 supports Python 3.8. diff --git a/docs/releasenotes/6.2.2.rst b/docs/releasenotes/6.2.2.rst index 85b0d0ba9..d3cba8b30 100644 --- a/docs/releasenotes/6.2.2.rst +++ b/docs/releasenotes/6.2.2.rst @@ -1,20 +1,20 @@ -6.2.2 ------ - -Security -======== - -This release fixes several buffer overflow issues and a DOS attack vulnerability. - -:cve:`2020-5310`, :cve:`2020-5311`, :cve:`2020-5312`, :cve:`2020-5313`: Overflow checks added -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Overflow checks have been added when calculating the size of a memory block to be reallocated -in the processing of TIFF, SGI, PCX and FLI images. - -:cve:`2019-19911`: DOS attack vulnerability -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -If an FPX image reports that it has a large number of bands, a large amount of -resources will be used when trying to process the image. This is fixed by -limiting the number of bands to those usable by Pillow. +6.2.2 +----- + +Security +======== + +This release fixes several buffer overflow issues and a DOS attack vulnerability. + +:cve:`2020-5310`, :cve:`2020-5311`, :cve:`2020-5312`, :cve:`2020-5313`: Overflow checks added +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Overflow checks have been added when calculating the size of a memory block to be reallocated +in the processing of TIFF, SGI, PCX and FLI images. + +:cve:`2019-19911`: DOS attack vulnerability +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If an FPX image reports that it has a large number of bands, a large amount of +resources will be used when trying to process the image. This is fixed by +limiting the number of bands to those usable by Pillow. diff --git a/docs/releasenotes/7.0.0.rst b/docs/releasenotes/7.0.0.rst index ed6026593..5810e8bee 100644 --- a/docs/releasenotes/7.0.0.rst +++ b/docs/releasenotes/7.0.0.rst @@ -1,155 +1,155 @@ -7.0.0 ------ - -Backwards Incompatible Changes -============================== - -Python 2.7 -^^^^^^^^^^ - -Pillow has dropped support for Python 2.7, which reached end-of-life on 2020-01-01. - -PILLOW_VERSION constant -^^^^^^^^^^^^^^^^^^^^^^^ - -``PILLOW_VERSION`` has been removed. Use ``__version__`` instead. - -PIL.*ImagePlugin.__version__ attributes -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The version constants of individual plugins have been removed. Use ``PIL.__version__`` -instead. - -=============================== ================================= ================================== -Removed Removed Removed -=============================== ================================= ================================== -``BmpImagePlugin.__version__`` ``Jpeg2KImagePlugin.__version__`` ``PngImagePlugin.__version__`` -``CurImagePlugin.__version__`` ``JpegImagePlugin.__version__`` ``PpmImagePlugin.__version__`` -``DcxImagePlugin.__version__`` ``McIdasImagePlugin.__version__`` ``PsdImagePlugin.__version__`` -``EpsImagePlugin.__version__`` ``MicImagePlugin.__version__`` ``SgiImagePlugin.__version__`` -``FliImagePlugin.__version__`` ``MpegImagePlugin.__version__`` ``SunImagePlugin.__version__`` -``FpxImagePlugin.__version__`` ``MpoImagePlugin.__version__`` ``TgaImagePlugin.__version__`` -``GdImageFile.__version__`` ``MspImagePlugin.__version__`` ``TiffImagePlugin.__version__`` -``GifImagePlugin.__version__`` ``PalmImagePlugin.__version__`` ``WmfImagePlugin.__version__`` -``IcoImagePlugin.__version__`` ``PcdImagePlugin.__version__`` ``XbmImagePlugin.__version__`` -``ImImagePlugin.__version__`` ``PcxImagePlugin.__version__`` ``XpmImagePlugin.__version__`` -``ImtImagePlugin.__version__`` ``PdfImagePlugin.__version__`` ``XVThumbImagePlugin.__version__`` -``IptcImagePlugin.__version__`` ``PixarImagePlugin.__version__`` -=============================== ================================= ================================== - -PyQt4 and PySide -^^^^^^^^^^^^^^^^ - -Qt 4 reached end-of-life on 2015-12-19. Its Python bindings are also EOL: PyQt4 since -2018-08-31 and PySide since 2015-10-14. - -Support for PyQt4 and PySide has been removed from ``ImageQt``. Please upgrade to PyQt5 -or PySide2. - -Setting the size of TIFF images -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Setting the size of a TIFF image directly (eg. ``im.size = (256, 256)``) throws -an error. Use ``Image.resize`` instead. - -Default resampling filter -^^^^^^^^^^^^^^^^^^^^^^^^^ - -The default resampling filter has been changed to the high-quality convolution -``Image.BICUBIC`` instead of ``Image.NEAREST``, for the :py:meth:`~PIL.Image.Image.resize` -method and the :py:meth:`~PIL.ImageOps.pad`, :py:meth:`~PIL.ImageOps.scale` -and :py:meth:`~PIL.ImageOps.fit` functions. -``Image.NEAREST`` is still always used for images in "P" and "1" modes. -See :ref:`concept-filters` to learn the difference. In short, -``Image.NEAREST`` is a very fast filter, but simple and low-quality. - -Image.draft() return value -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -If the :py:meth:`~PIL.Image.Image.draft` method has no effect, it returns :data:`None`. -If it does have an effect, then it previously returned the image itself. -However, unlike other `chain methods`_, :py:meth:`~PIL.Image.Image.draft` does not -return a modified version of the image, but modifies it in-place. So instead, if -:py:meth:`~PIL.Image.Image.draft` has an effect, Pillow will now return a tuple -of the image mode and a co-ordinate box. The box is the original coordinates in the -bounds of resulting image. This may be useful in a subsequent -:py:meth:`~PIL.Image.Image.resize` call. - -.. _chain methods: https://en.wikipedia.org/wiki/Method_chaining - - -API Additions -============= - -Custom unidentified image error -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Pillow will now throw a custom ``UnidentifiedImageError`` when an image cannot be -identified. For backwards compatibility, this will inherit from :py:exc:`OSError`. - -New argument ``reducing_gap`` for Image.resize() and Image.thumbnail() methods -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Speeds up resizing by resizing the image in two steps. The bigger ``reducing_gap``, -the closer the result to the fair resampling. The smaller ``reducing_gap``, -the faster resizing. With ``reducing_gap`` greater or equal to 3.0, -the result is indistinguishable from fair resampling. - -The default value for :py:meth:`~PIL.Image.Image.resize` is :data:`None`, -which means that the optimization is turned off by default. - -The default value for :py:meth:`~PIL.Image.Image.thumbnail` is 2.0, -which is very close to fair resampling while still being faster in many cases. -In addition, the same gap is applied when :py:meth:`~PIL.Image.Image.thumbnail` -calls :py:meth:`~PIL.Image.Image.draft`, which may greatly improve the quality -of JPEG thumbnails. As a result, :py:meth:`~PIL.Image.Image.thumbnail` -in the new version provides equally high speed and high quality from any -source (JPEG or arbitrary images). - -New Image.reduce() method -^^^^^^^^^^^^^^^^^^^^^^^^^ - -:py:meth:`~PIL.Image.Image.reduce` is a highly efficient operation -to reduce an image by integer times. Normally, it shouldn't be used directly. -Used internally by :py:meth:`~PIL.Image.Image.resize` and :py:meth:`~PIL.Image.Image.thumbnail` -methods to speed up resize when a new argument ``reducing_gap`` is set. - -Loading WMF images at a given DPI -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -On Windows, Pillow can read WMF files, with a default DPI of 72. An image can -now also be loaded at another resolution:: - - from PIL import Image - with Image.open("drawing.wmf") as im: - im.load(dpi=144) - -Other Changes -============= - -Image.__del__ -^^^^^^^^^^^^^ - -Implicitly closing the image's underlying file in ``Image.__del__`` has been removed. -Use a context manager or call :py:meth:`~PIL.Image.Image.close` instead to close -the file in a deterministic way. - -Previous method:: - - im = Image.open("hopper.png") - im.save("out.jpg") - -Use instead:: - - with Image.open("hopper.png") as im: - im.save("out.jpg") - -Better thumbnail geometry -^^^^^^^^^^^^^^^^^^^^^^^^^ - -When calculating the new dimensions in :py:meth:`~PIL.Image.Image.thumbnail`, -round to the nearest integer, instead of always rounding down. -This better preserves the original aspect ratio. - -When the image width or height is not divisible by 8 the last row and column -in the image get the correct weight after JPEG DCT scaling. +7.0.0 +----- + +Backwards Incompatible Changes +============================== + +Python 2.7 +^^^^^^^^^^ + +Pillow has dropped support for Python 2.7, which reached end-of-life on 2020-01-01. + +PILLOW_VERSION constant +^^^^^^^^^^^^^^^^^^^^^^^ + +``PILLOW_VERSION`` has been removed. Use ``__version__`` instead. + +PIL.*ImagePlugin.__version__ attributes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The version constants of individual plugins have been removed. Use ``PIL.__version__`` +instead. + +=============================== ================================= ================================== +Removed Removed Removed +=============================== ================================= ================================== +``BmpImagePlugin.__version__`` ``Jpeg2KImagePlugin.__version__`` ``PngImagePlugin.__version__`` +``CurImagePlugin.__version__`` ``JpegImagePlugin.__version__`` ``PpmImagePlugin.__version__`` +``DcxImagePlugin.__version__`` ``McIdasImagePlugin.__version__`` ``PsdImagePlugin.__version__`` +``EpsImagePlugin.__version__`` ``MicImagePlugin.__version__`` ``SgiImagePlugin.__version__`` +``FliImagePlugin.__version__`` ``MpegImagePlugin.__version__`` ``SunImagePlugin.__version__`` +``FpxImagePlugin.__version__`` ``MpoImagePlugin.__version__`` ``TgaImagePlugin.__version__`` +``GdImageFile.__version__`` ``MspImagePlugin.__version__`` ``TiffImagePlugin.__version__`` +``GifImagePlugin.__version__`` ``PalmImagePlugin.__version__`` ``WmfImagePlugin.__version__`` +``IcoImagePlugin.__version__`` ``PcdImagePlugin.__version__`` ``XbmImagePlugin.__version__`` +``ImImagePlugin.__version__`` ``PcxImagePlugin.__version__`` ``XpmImagePlugin.__version__`` +``ImtImagePlugin.__version__`` ``PdfImagePlugin.__version__`` ``XVThumbImagePlugin.__version__`` +``IptcImagePlugin.__version__`` ``PixarImagePlugin.__version__`` +=============================== ================================= ================================== + +PyQt4 and PySide +^^^^^^^^^^^^^^^^ + +Qt 4 reached end-of-life on 2015-12-19. Its Python bindings are also EOL: PyQt4 since +2018-08-31 and PySide since 2015-10-14. + +Support for PyQt4 and PySide has been removed from ``ImageQt``. Please upgrade to PyQt5 +or PySide2. + +Setting the size of TIFF images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Setting the size of a TIFF image directly (eg. ``im.size = (256, 256)``) throws +an error. Use ``Image.resize`` instead. + +Default resampling filter +^^^^^^^^^^^^^^^^^^^^^^^^^ + +The default resampling filter has been changed to the high-quality convolution +``Image.BICUBIC`` instead of ``Image.NEAREST``, for the :py:meth:`~PIL.Image.Image.resize` +method and the :py:meth:`~PIL.ImageOps.pad`, :py:meth:`~PIL.ImageOps.scale` +and :py:meth:`~PIL.ImageOps.fit` functions. +``Image.NEAREST`` is still always used for images in "P" and "1" modes. +See :ref:`concept-filters` to learn the difference. In short, +``Image.NEAREST`` is a very fast filter, but simple and low-quality. + +Image.draft() return value +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If the :py:meth:`~PIL.Image.Image.draft` method has no effect, it returns :data:`None`. +If it does have an effect, then it previously returned the image itself. +However, unlike other `chain methods`_, :py:meth:`~PIL.Image.Image.draft` does not +return a modified version of the image, but modifies it in-place. So instead, if +:py:meth:`~PIL.Image.Image.draft` has an effect, Pillow will now return a tuple +of the image mode and a co-ordinate box. The box is the original coordinates in the +bounds of resulting image. This may be useful in a subsequent +:py:meth:`~PIL.Image.Image.resize` call. + +.. _chain methods: https://en.wikipedia.org/wiki/Method_chaining + + +API Additions +============= + +Custom unidentified image error +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Pillow will now throw a custom ``UnidentifiedImageError`` when an image cannot be +identified. For backwards compatibility, this will inherit from :py:exc:`OSError`. + +New argument ``reducing_gap`` for Image.resize() and Image.thumbnail() methods +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Speeds up resizing by resizing the image in two steps. The bigger ``reducing_gap``, +the closer the result to the fair resampling. The smaller ``reducing_gap``, +the faster resizing. With ``reducing_gap`` greater or equal to 3.0, +the result is indistinguishable from fair resampling. + +The default value for :py:meth:`~PIL.Image.Image.resize` is :data:`None`, +which means that the optimization is turned off by default. + +The default value for :py:meth:`~PIL.Image.Image.thumbnail` is 2.0, +which is very close to fair resampling while still being faster in many cases. +In addition, the same gap is applied when :py:meth:`~PIL.Image.Image.thumbnail` +calls :py:meth:`~PIL.Image.Image.draft`, which may greatly improve the quality +of JPEG thumbnails. As a result, :py:meth:`~PIL.Image.Image.thumbnail` +in the new version provides equally high speed and high quality from any +source (JPEG or arbitrary images). + +New Image.reduce() method +^^^^^^^^^^^^^^^^^^^^^^^^^ + +:py:meth:`~PIL.Image.Image.reduce` is a highly efficient operation +to reduce an image by integer times. Normally, it shouldn't be used directly. +Used internally by :py:meth:`~PIL.Image.Image.resize` and :py:meth:`~PIL.Image.Image.thumbnail` +methods to speed up resize when a new argument ``reducing_gap`` is set. + +Loading WMF images at a given DPI +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +On Windows, Pillow can read WMF files, with a default DPI of 72. An image can +now also be loaded at another resolution:: + + from PIL import Image + with Image.open("drawing.wmf") as im: + im.load(dpi=144) + +Other Changes +============= + +Image.__del__ +^^^^^^^^^^^^^ + +Implicitly closing the image's underlying file in ``Image.__del__`` has been removed. +Use a context manager or call :py:meth:`~PIL.Image.Image.close` instead to close +the file in a deterministic way. + +Previous method:: + + im = Image.open("hopper.png") + im.save("out.jpg") + +Use instead:: + + with Image.open("hopper.png") as im: + im.save("out.jpg") + +Better thumbnail geometry +^^^^^^^^^^^^^^^^^^^^^^^^^ + +When calculating the new dimensions in :py:meth:`~PIL.Image.Image.thumbnail`, +round to the nearest integer, instead of always rounding down. +This better preserves the original aspect ratio. + +When the image width or height is not divisible by 8 the last row and column +in the image get the correct weight after JPEG DCT scaling. diff --git a/docs/releasenotes/7.1.0.rst b/docs/releasenotes/7.1.0.rst index 0dd8669a5..287eebf05 100644 --- a/docs/releasenotes/7.1.0.rst +++ b/docs/releasenotes/7.1.0.rst @@ -1,123 +1,123 @@ -7.1.0 ------ - -Security -======== - -This release includes many security fixes. - -:cve:`2020-10177`: Multiple out-of-bounds reads in FLI decoding -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Pillow before 7.1.0 has multiple out-of-bounds reads in ``libImaging/FliDecode.c``. - -:cve:`2020-10378`: Bounds overflow in PCX decoding -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -In ``libImaging/PcxDecode.c`` in Pillow before 7.1.0, an out-of-bounds read can occur -when reading PCX files where ``state->shuffle`` is instructed to read beyond -``state->buffer``. - -:cve:`2020-10379`: Two buffer overflows in TIFF decoding -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -In Pillow before 7.1.0, there are two buffer overflows in ``libImaging/TiffDecode.c``. - -:cve:`2020-10994`: Bounds overflow in JPEG 2000 decoding -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -In ``libImaging/Jpeg2KDecode.c`` in Pillow before 7.1.0, there are multiple -out-of-bounds reads via a crafted JP2 file. - -:cve:`2020-11538`: Buffer overflow in SGI-RLE decoding -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -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 -=========== - -Allow saving of zero quality JPEG images -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -If no quality was specified when saving a JPEG, Pillow internally used a value -of zero to indicate that the default quality should be used. However, this -removed the ability to actually save a JPEG with zero quality. This has now -been resolved. :: - - from PIL import Image - im = Image.open("hopper.jpg") - im.save("out.jpg", quality=0) - -API Additions -============= - -New channel operations -^^^^^^^^^^^^^^^^^^^^^^ - -Three new channel operations have been added: :py:meth:`~PIL.ImageChops.soft_light`, -:py:meth:`~PIL.ImageChops.hard_light` and :py:meth:`~PIL.ImageChops.overlay`. - -PILLOW_VERSION constant -^^^^^^^^^^^^^^^^^^^^^^^ - -``PILLOW_VERSION`` has been re-added but is deprecated and will be removed in a future -release. Use ``__version__`` instead. - -It was initially removed in Pillow 7.0.0, but brought back in 7.1.0 to give projects -more time to upgrade. - -Reading JPEG comments -^^^^^^^^^^^^^^^^^^^^^ - -When opening a JPEG image, the comment may now be read into -:py:attr:`~PIL.Image.Image.info`. - -Support for different charset encodings in PcfFontFile -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Previously ``PcfFontFile`` output only bitmap PIL fonts with ISO 8859-1 encoding, even -though the PCF format supports Unicode, making it hard to work with Pillow with bitmap -fonts in languages which use different character sets. - -Now it's possible to set a different charset encoding in ``PcfFontFile``'s class -constructor. By default, it generates a PIL font file with ISO 8859-1 as before. The -generated PIL font file still contains up to 256 characters, but the character set is -different depending on the selected encoding. - -To use such a font with ``ImageDraw.text``, call it with a bytes object with the same -encoding as the font file. - -X11 ImageGrab.grab() -^^^^^^^^^^^^^^^^^^^^ -Support has been added for ``ImageGrab.grab()`` on Linux using the X server -with the XCB library. - -An optional ``xdisplay`` parameter has been added to select the X server, -with the default value of :data:`None` using the default X server. - -Passing a different value on Windows or macOS will force taking a snapshot -using the selected X server; pass an empty string to use the default X server. -XCB support is not included in pre-compiled wheels for Windows and macOS. - -Other Changes -============= - -If present, only use alpha channel for bounding box -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -When the :py:meth:`~PIL.Image.Image.getbbox` method calculates the bounding -box, for an RGB image it trims black pixels. Similarly, for an RGBA image it -would trim black transparent pixels. This is now changed so that if an image -has an alpha channel (RGBA, RGBa, PA, LA, La), any transparent pixels are -trimmed. - -Improved APNG support -^^^^^^^^^^^^^^^^^^^^^ - -Added support for reading and writing Animated Portable Network Graphics (APNG) images. -The PNG plugin now supports using the :py:meth:`~PIL.Image.Image.seek` method and the -:py:class:`~PIL.ImageSequence.Iterator` class to read APNG frame sequences. -The PNG plugin also now supports using the ``append_images`` argument to write APNG frame -sequences. See :ref:`apng-sequences` for further details. +7.1.0 +----- + +Security +======== + +This release includes many security fixes. + +:cve:`2020-10177`: Multiple out-of-bounds reads in FLI decoding +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Pillow before 7.1.0 has multiple out-of-bounds reads in ``libImaging/FliDecode.c``. + +:cve:`2020-10378`: Bounds overflow in PCX decoding +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In ``libImaging/PcxDecode.c`` in Pillow before 7.1.0, an out-of-bounds read can occur +when reading PCX files where ``state->shuffle`` is instructed to read beyond +``state->buffer``. + +:cve:`2020-10379`: Two buffer overflows in TIFF decoding +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In Pillow before 7.1.0, there are two buffer overflows in ``libImaging/TiffDecode.c``. + +:cve:`2020-10994`: Bounds overflow in JPEG 2000 decoding +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In ``libImaging/Jpeg2KDecode.c`` in Pillow before 7.1.0, there are multiple +out-of-bounds reads via a crafted JP2 file. + +:cve:`2020-11538`: Buffer overflow in SGI-RLE decoding +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +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 +=========== + +Allow saving of zero quality JPEG images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If no quality was specified when saving a JPEG, Pillow internally used a value +of zero to indicate that the default quality should be used. However, this +removed the ability to actually save a JPEG with zero quality. This has now +been resolved. :: + + from PIL import Image + im = Image.open("hopper.jpg") + im.save("out.jpg", quality=0) + +API Additions +============= + +New channel operations +^^^^^^^^^^^^^^^^^^^^^^ + +Three new channel operations have been added: :py:meth:`~PIL.ImageChops.soft_light`, +:py:meth:`~PIL.ImageChops.hard_light` and :py:meth:`~PIL.ImageChops.overlay`. + +PILLOW_VERSION constant +^^^^^^^^^^^^^^^^^^^^^^^ + +``PILLOW_VERSION`` has been re-added but is deprecated and will be removed in a future +release. Use ``__version__`` instead. + +It was initially removed in Pillow 7.0.0, but brought back in 7.1.0 to give projects +more time to upgrade. + +Reading JPEG comments +^^^^^^^^^^^^^^^^^^^^^ + +When opening a JPEG image, the comment may now be read into +:py:attr:`~PIL.Image.Image.info`. + +Support for different charset encodings in PcfFontFile +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Previously ``PcfFontFile`` output only bitmap PIL fonts with ISO 8859-1 encoding, even +though the PCF format supports Unicode, making it hard to work with Pillow with bitmap +fonts in languages which use different character sets. + +Now it's possible to set a different charset encoding in ``PcfFontFile``'s class +constructor. By default, it generates a PIL font file with ISO 8859-1 as before. The +generated PIL font file still contains up to 256 characters, but the character set is +different depending on the selected encoding. + +To use such a font with ``ImageDraw.text``, call it with a bytes object with the same +encoding as the font file. + +X11 ImageGrab.grab() +^^^^^^^^^^^^^^^^^^^^ +Support has been added for ``ImageGrab.grab()`` on Linux using the X server +with the XCB library. + +An optional ``xdisplay`` parameter has been added to select the X server, +with the default value of :data:`None` using the default X server. + +Passing a different value on Windows or macOS will force taking a snapshot +using the selected X server; pass an empty string to use the default X server. +XCB support is not included in pre-compiled wheels for Windows and macOS. + +Other Changes +============= + +If present, only use alpha channel for bounding box +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When the :py:meth:`~PIL.Image.Image.getbbox` method calculates the bounding +box, for an RGB image it trims black pixels. Similarly, for an RGBA image it +would trim black transparent pixels. This is now changed so that if an image +has an alpha channel (RGBA, RGBa, PA, LA, La), any transparent pixels are +trimmed. + +Improved APNG support +^^^^^^^^^^^^^^^^^^^^^ + +Added support for reading and writing Animated Portable Network Graphics (APNG) images. +The PNG plugin now supports using the :py:meth:`~PIL.Image.Image.seek` method and the +:py:class:`~PIL.ImageSequence.Iterator` class to read APNG frame sequences. +The PNG plugin also now supports using the ``append_images`` argument to write APNG frame +sequences. See :ref:`apng-sequences` for further details. diff --git a/docs/releasenotes/7.1.1.rst b/docs/releasenotes/7.1.1.rst index 4afdb6645..fda824a83 100644 --- a/docs/releasenotes/7.1.1.rst +++ b/docs/releasenotes/7.1.1.rst @@ -1,25 +1,25 @@ -7.1.1 ------ - -Fix regression seeking PNG files -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -This fixes a regression introduced in 7.1.0 when adding support for APNG files when calling -``seek`` and ``tell``: - -.. code-block:: pycon - - >>> from PIL import Image - >>> with Image.open("Tests/images/hopper.png") as im: - ... im.seek(0) - ... - Traceback (most recent call last): - File "", line 2, in - File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/PIL/PngImagePlugin.py", line 739, in seek - if not self._seek_check(frame): - File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/PIL/ImageFile.py", line 306, in _seek_check - return self.tell() != frame - File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/PIL/PngImagePlugin.py", line 827, in tell - return self.__frame - AttributeError: 'PngImageFile' object has no attribute '_PngImageFile__frame' - >>> +7.1.1 +----- + +Fix regression seeking PNG files +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This fixes a regression introduced in 7.1.0 when adding support for APNG files when calling +``seek`` and ``tell``: + +.. code-block:: pycon + + >>> from PIL import Image + >>> with Image.open("Tests/images/hopper.png") as im: + ... im.seek(0) + ... + Traceback (most recent call last): + File "", line 2, in + File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/PIL/PngImagePlugin.py", line 739, in seek + if not self._seek_check(frame): + File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/PIL/ImageFile.py", line 306, in _seek_check + return self.tell() != frame + File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/PIL/PngImagePlugin.py", line 827, in tell + return self.__frame + AttributeError: 'PngImageFile' object has no attribute '_PngImageFile__frame' + >>> diff --git a/docs/releasenotes/7.1.2.rst b/docs/releasenotes/7.1.2.rst index 63a4b7aad..36cc52afc 100644 --- a/docs/releasenotes/7.1.2.rst +++ b/docs/releasenotes/7.1.2.rst @@ -1,16 +1,16 @@ -7.1.2 ------ - -Fix another regression seeking PNG files -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -This fixes a regression introduced in 7.1.0 when adding support for APNG files. - -When calling ``seek(n)`` on a regular PNG where ``n > 0``, it failed to raise an -:py:exc:`EOFError` as it should have done, resulting in: - -.. code-block:: pycon - - AttributeError: 'NoneType' object has no attribute 'read' - -Pillow 7.1.2 now raises the correct exception. +7.1.2 +----- + +Fix another regression seeking PNG files +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This fixes a regression introduced in 7.1.0 when adding support for APNG files. + +When calling ``seek(n)`` on a regular PNG where ``n > 0``, it failed to raise an +:py:exc:`EOFError` as it should have done, resulting in: + +.. code-block:: pycon + + AttributeError: 'NoneType' object has no attribute 'read' + +Pillow 7.1.2 now raises the correct exception. diff --git a/docs/releasenotes/7.2.0.rst b/docs/releasenotes/7.2.0.rst index 91e54da19..63e75293d 100644 --- a/docs/releasenotes/7.2.0.rst +++ b/docs/releasenotes/7.2.0.rst @@ -1,58 +1,58 @@ -7.2.0 ------ - -API Changes -=========== - -Replaced TiffImagePlugin DEBUG with logging -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -``TiffImagePlugin.DEBUG = True`` has been a way to print various debugging -information when interacting with TIFF images. This has now been removed -in favour of Python's ``logging`` module, already used in other places in the -Pillow source code. - -Corrected default offset when writing EXIF data -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Previously, the default ``offset`` argument for -:py:meth:`~PIL.Image.Exif.tobytes` was 0, which did not include the magic -header. It is now 8. - -Moved to ImageFileDirectory_v2 in Image.Exif -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Moved from the legacy :py:class:`PIL.TiffImagePlugin.ImageFileDirectory_v1` to -:py:class:`PIL.TiffImagePlugin.ImageFileDirectory_v2` in -:py:class:`PIL.Image.Exif`. This means that Exif RATIONALs and SIGNED_RATIONALs -are now read as :py:class:`PIL.TiffImagePlugin.IFDRational`, instead of as a -tuple with a numerator and a denominator. - -TIFF BYTE tags format -^^^^^^^^^^^^^^^^^^^^^ - -TIFF BYTE tags were previously read as a tuple containing a bytestring. They -are now read as just a single bytestring. - -Deprecations -^^^^^^^^^^^^ - -Image.show command parameter -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The ``command`` parameter was deprecated and will be removed in a future release. -Use a subclass of :py:class:`PIL.ImageShow.Viewer` instead. - -Image._showxv -~~~~~~~~~~~~~ - -``Image._showxv`` has been deprecated. Use :py:meth:`~PIL.Image.Image.show` -instead. If custom behaviour is required, use :py:meth:`~PIL.ImageShow.register` to add -a custom :py:class:`~PIL.ImageShow.Viewer` class. - -ImageFile.raise_ioerror -~~~~~~~~~~~~~~~~~~~~~~~ - -:py:exc:`IOError` was merged into :py:exc:`OSError` in Python 3.3. So, ``ImageFile.raise_ioerror`` -is now deprecated and will be removed in a future release. Use -``ImageFile.raise_oserror`` instead. +7.2.0 +----- + +API Changes +=========== + +Replaced TiffImagePlugin DEBUG with logging +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``TiffImagePlugin.DEBUG = True`` has been a way to print various debugging +information when interacting with TIFF images. This has now been removed +in favour of Python's ``logging`` module, already used in other places in the +Pillow source code. + +Corrected default offset when writing EXIF data +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Previously, the default ``offset`` argument for +:py:meth:`~PIL.Image.Exif.tobytes` was 0, which did not include the magic +header. It is now 8. + +Moved to ImageFileDirectory_v2 in Image.Exif +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Moved from the legacy :py:class:`PIL.TiffImagePlugin.ImageFileDirectory_v1` to +:py:class:`PIL.TiffImagePlugin.ImageFileDirectory_v2` in +:py:class:`PIL.Image.Exif`. This means that Exif RATIONALs and SIGNED_RATIONALs +are now read as :py:class:`PIL.TiffImagePlugin.IFDRational`, instead of as a +tuple with a numerator and a denominator. + +TIFF BYTE tags format +^^^^^^^^^^^^^^^^^^^^^ + +TIFF BYTE tags were previously read as a tuple containing a bytestring. They +are now read as just a single bytestring. + +Deprecations +^^^^^^^^^^^^ + +Image.show command parameter +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``command`` parameter was deprecated and will be removed in a future release. +Use a subclass of :py:class:`PIL.ImageShow.Viewer` instead. + +Image._showxv +~~~~~~~~~~~~~ + +``Image._showxv`` has been deprecated. Use :py:meth:`~PIL.Image.Image.show` +instead. If custom behaviour is required, use :py:meth:`~PIL.ImageShow.register` to add +a custom :py:class:`~PIL.ImageShow.Viewer` class. + +ImageFile.raise_ioerror +~~~~~~~~~~~~~~~~~~~~~~~ + +:py:exc:`IOError` was merged into :py:exc:`OSError` in Python 3.3. So, ``ImageFile.raise_ioerror`` +is now deprecated and will be removed in a future release. Use +``ImageFile.raise_oserror`` instead. diff --git a/docs/releasenotes/8.0.0.rst b/docs/releasenotes/8.0.0.rst index 1fc245c9a..8194f8772 100644 --- a/docs/releasenotes/8.0.0.rst +++ b/docs/releasenotes/8.0.0.rst @@ -1,177 +1,177 @@ -8.0.0 ------ - -Backwards Incompatible Changes -============================== - -Python 3.5 -^^^^^^^^^^ - -Pillow has dropped support for Python 3.5, which reached end-of-life on 2020-09-13. - -PyPy 7.1.x -^^^^^^^^^^ - -Pillow has dropped support for PyPy3 7.1.1. -PyPy3 7.2.0, released on 2019-10-14, is now the minimum compatible version. - -im.offset -^^^^^^^^^ - -``im.offset()`` has been removed, call :py:func:`.ImageChops.offset()` instead. - -Image.fromstring, im.fromstring and im.tostring -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -* ``Image.fromstring()`` has been removed, call :py:func:`.Image.frombytes()` instead. -* ``im.fromstring()`` has been removed, call :py:meth:`~PIL.Image.Image.frombytes()` instead. -* ``im.tostring()`` has been removed, call :py:meth:`~PIL.Image.Image.tobytes()` instead. - -ImageCms.CmsProfile attributes -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Some attributes in :py:class:`PIL.ImageCms.core.CmsProfile` have been removed: - -======================== =================================================== -Removed Use instead -======================== =================================================== -``color_space`` Padded :py:attr:`~.CmsProfile.xcolor_space` -``pcs`` Padded :py:attr:`~.CmsProfile.connection_space` -``product_copyright`` Unicode :py:attr:`~.CmsProfile.copyright` -``product_desc`` Unicode :py:attr:`~.CmsProfile.profile_description` -``product_description`` Unicode :py:attr:`~.CmsProfile.profile_description` -``product_manufacturer`` Unicode :py:attr:`~.CmsProfile.manufacturer` -``product_model`` Unicode :py:attr:`~.CmsProfile.model` -======================== =================================================== - -API Changes -=========== - -ImageDraw.text: stroke_width -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Fixed issue where passing ``stroke_width`` with a non-zero value -to :py:meth:`.ImageDraw.text` would cause the text to be offset by that amount. - -ImageDraw.text: anchor -^^^^^^^^^^^^^^^^^^^^^^ - -The ``anchor`` parameter of :py:meth:`.ImageDraw.text` has been implemented. - -Use this parameter to change the position of text relative to the -specified ``xy`` point. See :ref:`text-anchors` for details. - -Add MIME type to PsdImagePlugin -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -"image/vnd.adobe.photoshop" is now registered as the -:py:class:`.PsdImagePlugin.PsdImageFile` MIME type. - -API Additions -============= - -Image.open: add formats parameter -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Added a new ``formats`` parameter to :py:func:`.Image.open`: - -* A list or tuple of formats to attempt to load the file in. - This can be used to restrict the set of formats checked. - Pass ``None`` to try all supported formats. You can print the set of - available formats by running ``python3 -m PIL`` or using - the :py:func:`PIL.features.pilinfo` function. - -ImageOps.autocontrast: add mask parameter -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -:py:func:`.ImageOps.autocontrast` can now take a ``mask`` parameter: - -* Histogram used in contrast operation is computed using pixels within the mask. - If no mask is given the entire image is used for histogram computation. - -ImageOps.autocontrast cutoffs -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Previously, the ``cutoff`` parameter of :py:func:`.ImageOps.autocontrast` could only -be a single number, used as the percent to cut off from the histogram on the low and -high ends. - -Now, it can also be a tuple ``(low, high)``. - -ImageDraw.regular_polygon -^^^^^^^^^^^^^^^^^^^^^^^^^ - -A new method :py:meth:`.ImageDraw.regular_polygon`, draws a regular polygon of ``n_sides``, inscribed in a ``bounding_circle``. - -For example ``draw.regular_polygon(((100, 100), 50), 5)`` -draws a pentagon centered at the point ``(100, 100)`` with a polygon radius of ``50``. - -ImageDraw.text: embedded_color -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The methods :py:meth:`.ImageDraw.text` and :py:meth:`.ImageDraw.multiline_text` -now support fonts with embedded color data. - -To render text with embedded color data, use the parameter ``embedded_color=True``. - -Support for CBDT fonts requires FreeType 2.5 compiled with libpng. -Support for SBIX fonts requires FreeType 2.5.1 compiled with libpng. -Support for COLR fonts requires FreeType 2.10. -SVG fonts are not yet supported. - -ImageDraw.textlength -^^^^^^^^^^^^^^^^^^^^ - -Two new methods :py:meth:`.ImageDraw.textlength` and :py:meth:`.FreeTypeFont.getlength` -were added, returning the exact advance length of text with 1/64 pixel precision. - -These can be used for word-wrapping or rendering text in parts. - -ImageDraw.textbbox -^^^^^^^^^^^^^^^^^^ - -Three new methods :py:meth:`.ImageDraw.textbbox`, :py:meth:`.ImageDraw.multiline_textbbox`, -and :py:meth:`.FreeTypeFont.getbbox` return the bounding box of rendered text. - -These functions accept an ``anchor`` parameter, see :ref:`text-anchors` for details. - -Other Changes -============= - -Improved ellipse-drawing algorithm -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The ellipse-drawing algorithm has been changed from drawing a 360-sided polygon to one -which resembles Bresenham's algorithm for circles. It should be faster and produce -smoother curves, especially for smaller ellipses. - -ImageDraw.text and ImageDraw.multiline_text -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Fixed multiple issues in methods :py:meth:`.ImageDraw.text` and :py:meth:`.ImageDraw.multiline_text` -sometimes causing unexpected text alignment issues. - -The ``align`` parameter of :py:meth:`.ImageDraw.multiline_text` now gives better results in some cases. - -TrueType fonts with embedded bitmaps are now supported. - -Added writing of subIFDs -^^^^^^^^^^^^^^^^^^^^^^^^ - -When saving EXIF data, Pillow is now able to write subIFDs, such as the GPS IFD. This -should happen automatically when saving an image using the EXIF data that it was opened -with, such as in :py:meth:`~PIL.ImageOps.exif_transpose`. - -Previously, the code of the first tag of the subIFD was incorrectly written as the -offset. - -Error for large BMP files -^^^^^^^^^^^^^^^^^^^^^^^^^ - -Previously, if a BMP file was too large, an :py:exc:`OSError` would be raised. Now, -``DecompressionBombError`` is used instead, as Pillow already uses for other formats. - -Dark theme for docs -^^^^^^^^^^^^^^^^^^^ - -The https://pillow.readthedocs.io documentation will use a dark theme if the user has requested the system use one. Uses the ``prefers-color-scheme`` CSS media query. +8.0.0 +----- + +Backwards Incompatible Changes +============================== + +Python 3.5 +^^^^^^^^^^ + +Pillow has dropped support for Python 3.5, which reached end-of-life on 2020-09-13. + +PyPy 7.1.x +^^^^^^^^^^ + +Pillow has dropped support for PyPy3 7.1.1. +PyPy3 7.2.0, released on 2019-10-14, is now the minimum compatible version. + +im.offset +^^^^^^^^^ + +``im.offset()`` has been removed, call :py:func:`.ImageChops.offset()` instead. + +Image.fromstring, im.fromstring and im.tostring +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* ``Image.fromstring()`` has been removed, call :py:func:`.Image.frombytes()` instead. +* ``im.fromstring()`` has been removed, call :py:meth:`~PIL.Image.Image.frombytes()` instead. +* ``im.tostring()`` has been removed, call :py:meth:`~PIL.Image.Image.tobytes()` instead. + +ImageCms.CmsProfile attributes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Some attributes in :py:class:`PIL.ImageCms.core.CmsProfile` have been removed: + +======================== =================================================== +Removed Use instead +======================== =================================================== +``color_space`` Padded :py:attr:`~.CmsProfile.xcolor_space` +``pcs`` Padded :py:attr:`~.CmsProfile.connection_space` +``product_copyright`` Unicode :py:attr:`~.CmsProfile.copyright` +``product_desc`` Unicode :py:attr:`~.CmsProfile.profile_description` +``product_description`` Unicode :py:attr:`~.CmsProfile.profile_description` +``product_manufacturer`` Unicode :py:attr:`~.CmsProfile.manufacturer` +``product_model`` Unicode :py:attr:`~.CmsProfile.model` +======================== =================================================== + +API Changes +=========== + +ImageDraw.text: stroke_width +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Fixed issue where passing ``stroke_width`` with a non-zero value +to :py:meth:`.ImageDraw.text` would cause the text to be offset by that amount. + +ImageDraw.text: anchor +^^^^^^^^^^^^^^^^^^^^^^ + +The ``anchor`` parameter of :py:meth:`.ImageDraw.text` has been implemented. + +Use this parameter to change the position of text relative to the +specified ``xy`` point. See :ref:`text-anchors` for details. + +Add MIME type to PsdImagePlugin +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +"image/vnd.adobe.photoshop" is now registered as the +:py:class:`.PsdImagePlugin.PsdImageFile` MIME type. + +API Additions +============= + +Image.open: add formats parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Added a new ``formats`` parameter to :py:func:`.Image.open`: + +* A list or tuple of formats to attempt to load the file in. + This can be used to restrict the set of formats checked. + Pass ``None`` to try all supported formats. You can print the set of + available formats by running ``python3 -m PIL`` or using + the :py:func:`PIL.features.pilinfo` function. + +ImageOps.autocontrast: add mask parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:py:func:`.ImageOps.autocontrast` can now take a ``mask`` parameter: + +* Histogram used in contrast operation is computed using pixels within the mask. + If no mask is given the entire image is used for histogram computation. + +ImageOps.autocontrast cutoffs +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Previously, the ``cutoff`` parameter of :py:func:`.ImageOps.autocontrast` could only +be a single number, used as the percent to cut off from the histogram on the low and +high ends. + +Now, it can also be a tuple ``(low, high)``. + +ImageDraw.regular_polygon +^^^^^^^^^^^^^^^^^^^^^^^^^ + +A new method :py:meth:`.ImageDraw.regular_polygon`, draws a regular polygon of ``n_sides``, inscribed in a ``bounding_circle``. + +For example ``draw.regular_polygon(((100, 100), 50), 5)`` +draws a pentagon centered at the point ``(100, 100)`` with a polygon radius of ``50``. + +ImageDraw.text: embedded_color +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The methods :py:meth:`.ImageDraw.text` and :py:meth:`.ImageDraw.multiline_text` +now support fonts with embedded color data. + +To render text with embedded color data, use the parameter ``embedded_color=True``. + +Support for CBDT fonts requires FreeType 2.5 compiled with libpng. +Support for SBIX fonts requires FreeType 2.5.1 compiled with libpng. +Support for COLR fonts requires FreeType 2.10. +SVG fonts are not yet supported. + +ImageDraw.textlength +^^^^^^^^^^^^^^^^^^^^ + +Two new methods :py:meth:`.ImageDraw.textlength` and :py:meth:`.FreeTypeFont.getlength` +were added, returning the exact advance length of text with 1/64 pixel precision. + +These can be used for word-wrapping or rendering text in parts. + +ImageDraw.textbbox +^^^^^^^^^^^^^^^^^^ + +Three new methods :py:meth:`.ImageDraw.textbbox`, :py:meth:`.ImageDraw.multiline_textbbox`, +and :py:meth:`.FreeTypeFont.getbbox` return the bounding box of rendered text. + +These functions accept an ``anchor`` parameter, see :ref:`text-anchors` for details. + +Other Changes +============= + +Improved ellipse-drawing algorithm +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ellipse-drawing algorithm has been changed from drawing a 360-sided polygon to one +which resembles Bresenham's algorithm for circles. It should be faster and produce +smoother curves, especially for smaller ellipses. + +ImageDraw.text and ImageDraw.multiline_text +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Fixed multiple issues in methods :py:meth:`.ImageDraw.text` and :py:meth:`.ImageDraw.multiline_text` +sometimes causing unexpected text alignment issues. + +The ``align`` parameter of :py:meth:`.ImageDraw.multiline_text` now gives better results in some cases. + +TrueType fonts with embedded bitmaps are now supported. + +Added writing of subIFDs +^^^^^^^^^^^^^^^^^^^^^^^^ + +When saving EXIF data, Pillow is now able to write subIFDs, such as the GPS IFD. This +should happen automatically when saving an image using the EXIF data that it was opened +with, such as in :py:meth:`~PIL.ImageOps.exif_transpose`. + +Previously, the code of the first tag of the subIFD was incorrectly written as the +offset. + +Error for large BMP files +^^^^^^^^^^^^^^^^^^^^^^^^^ + +Previously, if a BMP file was too large, an :py:exc:`OSError` would be raised. Now, +``DecompressionBombError`` is used instead, as Pillow already uses for other formats. + +Dark theme for docs +^^^^^^^^^^^^^^^^^^^ + +The https://pillow.readthedocs.io documentation will use a dark theme if the user has requested the system use one. Uses the ``prefers-color-scheme`` CSS media query. diff --git a/docs/releasenotes/8.0.1.rst b/docs/releasenotes/8.0.1.rst index 29a28443f..1cd4ad031 100644 --- a/docs/releasenotes/8.0.1.rst +++ b/docs/releasenotes/8.0.1.rst @@ -1,23 +1,23 @@ -8.0.1 ------ - -Security -======== - -:cve:`2020-15999`: Update FreeType in wheels to `2.10.4`_ -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -* A heap buffer overflow has been found in the handling of embedded PNG bitmaps, - introduced in FreeType version 2.6. - -* If you use option ``FT_CONFIG_OPTION_USE_PNG`` you should upgrade immediately. - -We strongly recommend updating to Pillow 8.0.1 if you are using Pillow 8.0.0, which improved support for bitmap fonts. - -In Pillow 7.2.0 and earlier bitmap fonts were disabled with ``FT_LOAD_NO_BITMAP``, but it is not -clear if this prevents the exploit and we recommend updating to Pillow 8.0.1. - -Pillow 8.0.0 and earlier are potentially vulnerable releases, including the last release -to support Python 2.7, namely Pillow 6.2.2. - -.. _2.10.4: https://sourceforge.net/projects/freetype/files/freetype2/2.10.4/ +8.0.1 +----- + +Security +======== + +:cve:`2020-15999`: Update FreeType in wheels to `2.10.4`_ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* A heap buffer overflow has been found in the handling of embedded PNG bitmaps, + introduced in FreeType version 2.6. + +* If you use option ``FT_CONFIG_OPTION_USE_PNG`` you should upgrade immediately. + +We strongly recommend updating to Pillow 8.0.1 if you are using Pillow 8.0.0, which improved support for bitmap fonts. + +In Pillow 7.2.0 and earlier bitmap fonts were disabled with ``FT_LOAD_NO_BITMAP``, but it is not +clear if this prevents the exploit and we recommend updating to Pillow 8.0.1. + +Pillow 8.0.0 and earlier are potentially vulnerable releases, including the last release +to support Python 2.7, namely Pillow 6.2.2. + +.. _2.10.4: https://sourceforge.net/projects/freetype/files/freetype2/2.10.4/ diff --git a/docs/releasenotes/8.1.0.rst b/docs/releasenotes/8.1.0.rst index 5c3993318..45bc8afa3 100644 --- a/docs/releasenotes/8.1.0.rst +++ b/docs/releasenotes/8.1.0.rst @@ -1,97 +1,97 @@ -8.1.0 ------ - -Security -======== - -This release includes security fixes. - -* An out-of-bounds read when saving TIFFs with custom metadata through LibTIFF -* An out-of-bounds read when saving a GIF of 1px width - -:cve:`2020-35653`: Buffer read overrun in PCX decoding -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The PCX image decoder used the reported image stride to calculate -the row buffer, rather than calculating it from the image size. This issue dates back -to the PIL fork. Thanks to Google's `OSS-Fuzz`_ project for finding this. - -:cve:`2020-35654`: TIFF out-of-bounds write error -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Out-of-bounds write in ``TiffDecode.c`` when reading corrupt YCbCr -files in some LibTIFF versions (4.1.0/Ubuntu 20.04, but not 4.0.9/Ubuntu 18.04). -In some cases LibTIFF's interpretation of the file is different when reading in RGBA mode, -leading to an out-of-bounds write in ``TiffDecode.c``. This potentially affects Pillow -versions from 6.0.0 to 8.0.1, depending on the version of LibTIFF. This was reported through -`Tidelift`_. - -:cve:`2020-35655`: SGI Decode buffer overrun -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -4 byte read overflow in ``SgiRleDecode.c``, where the code was not correctly -checking the offsets and length tables. Independently reported through `Tidelift`_ and Google's -`OSS-Fuzz`_. This vulnerability covers Pillow versions 4.3.0->8.0.1. - -.. _Tidelift: https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pillow&utm_medium=referral&utm_campaign=docs -.. _OSS-Fuzz: https://github.com/google/oss-fuzz - -Dependencies -^^^^^^^^^^^^ - -OpenJPEG in the macOS and Linux wheels has been updated from 2.3.1 to 2.4.0, including -security fixes. - -LibTIFF in the macOS and Linux wheels has been updated from 4.1.0 to 4.2.0, including -security fixes discovered by fuzzers. - -Deprecations -============ - -FreeType 2.7 -^^^^^^^^^^^^ - -Support for FreeType 2.7 is deprecated and will be removed in Pillow 9.0.0 (2022-01-02), -when FreeType 2.8 will be the minimum supported. - -We recommend upgrading to at least FreeType `2.10.4`_, which fixed a severe -vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`). - -.. _2.10.4: https://sourceforge.net/projects/freetype/files/freetype2/2.10.4/ - -Makefile -^^^^^^^^ - -The ``install-venv`` target has been deprecated. - -API Additions -============= - -Append images to ICO -^^^^^^^^^^^^^^^^^^^^ - -When saving an ICO image, the file may contain versions of the image at different -sizes. By default, Pillow will scale down the main image to create these copies. - -With this release, a list of images can be provided to the ``append_images`` parameter -when saving, to replace the scaled down versions. This is the same functionality that -already exists for the ICNS format. - -Other Changes -============= - -Makefile -^^^^^^^^ - -The ``co`` target has been removed. - -PyPy wheels -^^^^^^^^^^^ - -Wheels have been added for PyPy 3.7. - -PySide6 -^^^^^^^ - -Support has been added for PySide6. If it is installed, it will be used instead of -PyQt5 or PySide2, since it is based on a newer Qt. +8.1.0 +----- + +Security +======== + +This release includes security fixes. + +* An out-of-bounds read when saving TIFFs with custom metadata through LibTIFF +* An out-of-bounds read when saving a GIF of 1px width + +:cve:`2020-35653`: Buffer read overrun in PCX decoding +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The PCX image decoder used the reported image stride to calculate +the row buffer, rather than calculating it from the image size. This issue dates back +to the PIL fork. Thanks to Google's `OSS-Fuzz`_ project for finding this. + +:cve:`2020-35654`: TIFF out-of-bounds write error +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Out-of-bounds write in ``TiffDecode.c`` when reading corrupt YCbCr +files in some LibTIFF versions (4.1.0/Ubuntu 20.04, but not 4.0.9/Ubuntu 18.04). +In some cases LibTIFF's interpretation of the file is different when reading in RGBA mode, +leading to an out-of-bounds write in ``TiffDecode.c``. This potentially affects Pillow +versions from 6.0.0 to 8.0.1, depending on the version of LibTIFF. This was reported through +`Tidelift`_. + +:cve:`2020-35655`: SGI Decode buffer overrun +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +4 byte read overflow in ``SgiRleDecode.c``, where the code was not correctly +checking the offsets and length tables. Independently reported through `Tidelift`_ and Google's +`OSS-Fuzz`_. This vulnerability covers Pillow versions 4.3.0->8.0.1. + +.. _Tidelift: https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pillow&utm_medium=referral&utm_campaign=docs +.. _OSS-Fuzz: https://github.com/google/oss-fuzz + +Dependencies +^^^^^^^^^^^^ + +OpenJPEG in the macOS and Linux wheels has been updated from 2.3.1 to 2.4.0, including +security fixes. + +LibTIFF in the macOS and Linux wheels has been updated from 4.1.0 to 4.2.0, including +security fixes discovered by fuzzers. + +Deprecations +============ + +FreeType 2.7 +^^^^^^^^^^^^ + +Support for FreeType 2.7 is deprecated and will be removed in Pillow 9.0.0 (2022-01-02), +when FreeType 2.8 will be the minimum supported. + +We recommend upgrading to at least FreeType `2.10.4`_, which fixed a severe +vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`). + +.. _2.10.4: https://sourceforge.net/projects/freetype/files/freetype2/2.10.4/ + +Makefile +^^^^^^^^ + +The ``install-venv`` target has been deprecated. + +API Additions +============= + +Append images to ICO +^^^^^^^^^^^^^^^^^^^^ + +When saving an ICO image, the file may contain versions of the image at different +sizes. By default, Pillow will scale down the main image to create these copies. + +With this release, a list of images can be provided to the ``append_images`` parameter +when saving, to replace the scaled down versions. This is the same functionality that +already exists for the ICNS format. + +Other Changes +============= + +Makefile +^^^^^^^^ + +The ``co`` target has been removed. + +PyPy wheels +^^^^^^^^^^^ + +Wheels have been added for PyPy 3.7. + +PySide6 +^^^^^^^ + +Support has been added for PySide6. If it is installed, it will be used instead of +PyQt5 or PySide2, since it is based on a newer Qt. diff --git a/docs/releasenotes/8.1.1.rst b/docs/releasenotes/8.1.1.rst index 690421c2a..b2b06b92c 100644 --- a/docs/releasenotes/8.1.1.rst +++ b/docs/releasenotes/8.1.1.rst @@ -1,39 +1,39 @@ -8.1.1 ------ - -Security -======== - -:cve:`2021-25289`: Correct the fix for :cve:`2020-35654` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The previous fix for :cve:`2020-35654` was insufficient due to incorrect -error checking in ``TiffDecode.c``. - -:cve:`2021-25290`: Fix buffer overflow in ``TiffDecode.c`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -In ``TiffDecode.c``, there is a negative-offset ``memcpy`` with an invalid size. - -:cve:`2021-25291`: Fix buffer overflow in ``TIFFReadRGBATile`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -In ``TiffDecode.c``, invalid tile boundaries could lead to an out-of-bounds -read in ``TIFFReadRGBATile``. - -:cve:`2021-25292`: Fix DOS attack -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The PDF parser has a catastrophic backtracking regex that could be used as a -DOS attack. - -:cve:`2021-25293`: Fix buffer overflow in ``SgiRleDecode.c`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -There is an out-of-bounds read in ``SgiRleDecode.c`` since Pillow 4.3.0. - -Other Changes -============= - -A crash with the feature flags for libimagequant, libjpeg-turbo, WebP and XCB on -unreleased Python 3.10 has been fixed (:issue:`5193`). +8.1.1 +----- + +Security +======== + +:cve:`2021-25289`: Correct the fix for :cve:`2020-35654` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The previous fix for :cve:`2020-35654` was insufficient due to incorrect +error checking in ``TiffDecode.c``. + +:cve:`2021-25290`: Fix buffer overflow in ``TiffDecode.c`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In ``TiffDecode.c``, there is a negative-offset ``memcpy`` with an invalid size. + +:cve:`2021-25291`: Fix buffer overflow in ``TIFFReadRGBATile`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In ``TiffDecode.c``, invalid tile boundaries could lead to an out-of-bounds +read in ``TIFFReadRGBATile``. + +:cve:`2021-25292`: Fix DOS attack +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The PDF parser has a catastrophic backtracking regex that could be used as a +DOS attack. + +:cve:`2021-25293`: Fix buffer overflow in ``SgiRleDecode.c`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +There is an out-of-bounds read in ``SgiRleDecode.c`` since Pillow 4.3.0. + +Other Changes +============= + +A crash with the feature flags for libimagequant, libjpeg-turbo, WebP and XCB on +unreleased Python 3.10 has been fixed (:issue:`5193`). diff --git a/docs/releasenotes/8.1.2.rst b/docs/releasenotes/8.1.2.rst index de6ba605e..acd860a6f 100644 --- a/docs/releasenotes/8.1.2.rst +++ b/docs/releasenotes/8.1.2.rst @@ -1,15 +1,15 @@ -8.1.2 ------ - -Security -======== - -:cve:`2021-27921`, :cve:`2021-27922`, :cve:`2021-27923`: Fix DOS attacks -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -There is an exhaustion of memory DOS attack in BLP, ICNS, ICO images -where Pillow did not properly check the reported size of the contained image. -These images could cause arbitrarily large memory allocations. - -These issues were reported by Jiayi Lin, Luke Shaffer, Xinran Xie and -Akshay Ajayan of `Arizona State University `_. +8.1.2 +----- + +Security +======== + +:cve:`2021-27921`, :cve:`2021-27922`, :cve:`2021-27923`: Fix DOS attacks +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +There is an exhaustion of memory DOS attack in BLP, ICNS, ICO images +where Pillow did not properly check the reported size of the contained image. +These images could cause arbitrarily large memory allocations. + +These issues were reported by Jiayi Lin, Luke Shaffer, Xinran Xie and +Akshay Ajayan of `Arizona State University `_. diff --git a/docs/releasenotes/8.2.0.rst b/docs/releasenotes/8.2.0.rst index 50fe9aa19..1d8ab1402 100644 --- a/docs/releasenotes/8.2.0.rst +++ b/docs/releasenotes/8.2.0.rst @@ -1,227 +1,227 @@ -8.2.0 ------ - -Security -======== - -These issues were all found with `OSS-Fuzz`_. - -:cve:`2021-25287`, :cve:`2021-25288`: OOB read in Jpeg2KDecode -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -* For J2k images with multiple bands, it's legal to have different widths for each band, - e.g. 1 byte for ``L``, 4 bytes for ``A``. -* This dates to Pillow 2.4.0. - -:cve:`2021-28675`: DOS attack in PsdImagePlugin -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -* :py:class:`.PsdImagePlugin.PsdImageFile` did not sanity check the number of input - layers with regard to the size of the data block, this could lead to a - denial-of-service on :py:meth:`~PIL.Image.open` prior to - :py:meth:`~PIL.Image.Image.load`. -* This dates to the PIL fork. - -:cve:`2021-28676`: FLI image DOS attack -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -* ``FliDecode.c`` did not properly check that the block advance was non-zero, - potentially leading to an infinite loop on load. -* This dates to the PIL fork. - -:cve:`2021-28677`: EPS DOS on _open -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -* The readline used in EPS has to deal with any combination of ``\r`` and ``\n`` as line - endings. It accidentally used a quadratic method of accumulating lines while looking - for a line ending. -* A malicious EPS file could use this to perform a denial-of-service of Pillow in the - open phase, before an image was accepted for opening. -* This dates to the PIL fork. - -:cve:`2021-28678`: BLP DOS attack -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -* ``BlpImagePlugin`` did not properly check that reads after jumping to file offsets - returned data. This could lead to a denial-of-service where the decoder could be run a - large number of times on empty data. -* This dates to Pillow 5.1.0. - -Fix memory DOS in ImageFont -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -* A corrupt or specially crafted TTF font could have font metrics that lead to - unreasonably large sizes when rendering text in font. ``ImageFont.py`` did not check - the image size before allocating memory for it. -* This dates to the PIL fork. - -Deprecations -============ - -Categories -^^^^^^^^^^ - -``im.category`` is deprecated and will be removed in Pillow 10.0.0 (2023-07-01), -along with the related ``Image.NORMAL``, ``Image.SEQUENCE`` and -``Image.CONTAINER`` attributes. - -To determine if an image has multiple frames or not, -``getattr(im, "is_animated", False)`` can be used instead. - -Tk/Tcl 8.4 -^^^^^^^^^^ - -Support for Tk/Tcl 8.4 is deprecated and will be removed in Pillow 10.0.0 (2023-07-01), -when Tk/Tcl 8.5 will be the minimum supported. - -API Changes -=========== - -Image.alpha_composite: dest -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -When calling :py:meth:`~PIL.Image.Image.alpha_composite`, the ``dest`` argument now -accepts negative co-ordinates, like the upper left corner of the ``box`` argument of -:py:meth:`~PIL.Image.Image.paste` can be negative. Naturally, this has effect of -cropping the overlaid image. - -Image.getexif: EXIF and GPS IFD -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Previously, :py:meth:`~PIL.Image.Image.getexif` flattened the EXIF IFD into the rest of -the data, losing information. This information is now kept separate, moved under -``im.getexif().get_ifd(0x8769)``. - -Direct access to the GPS IFD dictionary was possible through ``im.getexif()[0x8825]``. -This is now consistent with other IFDs, and must be accessed through -``im.getexif().get_ifd(0x8825)``. - -These changes only affect :py:meth:`~PIL.Image.Image.getexif`, introduced in Pillow -6.0. The older ``_getexif()`` methods are unaffected. - -Image._MODEINFO -^^^^^^^^^^^^^^^ - -This internal dictionary had been deprecated by a comment since PIL, and is now -removed. Instead, ``Image.getmodebase()``, ``Image.getmodetype()``, -``Image.getmodebandnames()``, ``Image.getmodebands()`` or ``ImageMode.getmode()`` -can be used. - -API Additions -============= - -getxmp() for JPEG images -^^^^^^^^^^^^^^^^^^^^^^^^ - -A new method has been added to return -`XMP data `_ for JPEG -images. It reads the XML data into a dictionary of names and values. - -For example:: - - >>> from PIL import Image - >>> with Image.open("Tests/images/xmp_test.jpg") as im: - >>> print(im.getxmp()) - {'RDF': {}, 'Description': {'Version': '10.4', 'ProcessVersion': '10.0', ...}, ...} - -ImageDraw.rounded_rectangle -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Added :py:meth:`~PIL.ImageDraw.ImageDraw.rounded_rectangle`. It works the same as -:py:meth:`~PIL.ImageDraw.ImageDraw.rectangle`, except with an additional ``radius`` -argument. ``radius`` is limited to half of the width or the height, so that users can -create a circle, but not any other ellipse. :: - - from PIL import Image, ImageDraw - im = Image.new("RGB", (200, 200)) - draw = ImageDraw.Draw(im) - draw.rounded_rectangle(xy=(10, 20, 190, 180), radius=30, fill="red") - -ImageOps.autocontrast: preserve_tone -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The default behaviour of :py:meth:`~PIL.ImageOps.autocontrast` is to normalize -separate histograms for each color channel, changing the tone of the image. The new -``preserve_tone`` argument keeps the tone unchanged by using one luminance histogram -for all channels. - -ImageShow.GmDisplayViewer -^^^^^^^^^^^^^^^^^^^^^^^^^ - -If GraphicsMagick is present, this new :py:class:`PIL.ImageShow.Viewer` subclass will -be registered. It uses GraphicsMagick_, an ImageMagick_ fork, to display images. - -The GraphicsMagick based viewer has a lower priority than its ImageMagick -counterpart. Thus, if both ImageMagick and GraphicsMagick are installed, -``im.show()`` and :py:func:`.ImageShow.show()` prefer the viewer based on -ImageMagick, i.e the behaviour stays the same for Pillow users having -ImageMagick installed. - -ImageShow.IPythonViewer -^^^^^^^^^^^^^^^^^^^^^^^ - -If IPython is present, this new :py:class:`PIL.ImageShow.Viewer` subclass will be -registered. It displays images on all IPython frontends. This will be helpful -to users of Google Colab, allowing ``im.show()`` to display images. - -It is lower in priority than the other default :py:class:`PIL.ImageShow.Viewer` -instances, so it will only be used by ``im.show()`` or :py:func:`.ImageShow.show()` -if none of the other viewers are available. This means that the behaviour of -:py:class:`PIL.ImageShow` will stay the same for most Pillow users. - -Saving TIFF with ICC profile -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -As is already possible for JPEG, PNG and WebP, the ICC profile for TIFF files can now -be specified through a keyword argument:: - - im.save("out.tif", icc_profile=...) - -Other Changes -============= - -GIF writer uses LZW encoding -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -GIF files are now written using LZW encoding, which will generate smaller files, -typically about 70% of the size generated by the older encoder. - -The pixel data is encoded using the format specified in the `CompuServe GIF standard -`_. - -The older encoder used a variant of run-length encoding that was compatible but less -efficient. - -GraphicsMagick -^^^^^^^^^^^^^^ - -The test suite can now be run on systems which have GraphicsMagick_ but not -ImageMagick_ installed. If both are installed, the tests prefer ImageMagick. - -Libraqm and FriBiDi linking -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The way the libraqm dependency for complex text scripts is linked has been changed: - -Source builds will now link against the system version of libraqm at build time -rather than at runtime by default. - -Binary wheels now include a statically linked modified version of libraqm that -links against FriBiDi at runtime instead. This change is intended to address -issues with the previous implementation on some platforms. These are created -by building Pillow with the new build flags ``--vendor-raqm --vendor-fribidi``. - -Windows users will now need to install ``fribidi.dll`` (or ``fribidi-0.dll``) only, -``libraqm.dll`` is no longer used. - -See :doc:`installation documentation<../installation>` for more information. - -PyQt6 -^^^^^ - -Support has been added for PyQt6. If it is installed, it will be used instead of -PySide6, PyQt5 or PySide2. - -.. _GraphicsMagick: http://www.graphicsmagick.org/ -.. _ImageMagick: https://imagemagick.org/ -.. _OSS-Fuzz: https://github.com/google/oss-fuzz +8.2.0 +----- + +Security +======== + +These issues were all found with `OSS-Fuzz`_. + +:cve:`2021-25287`, :cve:`2021-25288`: OOB read in Jpeg2KDecode +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* For J2k images with multiple bands, it's legal to have different widths for each band, + e.g. 1 byte for ``L``, 4 bytes for ``A``. +* This dates to Pillow 2.4.0. + +:cve:`2021-28675`: DOS attack in PsdImagePlugin +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* :py:class:`.PsdImagePlugin.PsdImageFile` did not sanity check the number of input + layers with regard to the size of the data block, this could lead to a + denial-of-service on :py:meth:`~PIL.Image.open` prior to + :py:meth:`~PIL.Image.Image.load`. +* This dates to the PIL fork. + +:cve:`2021-28676`: FLI image DOS attack +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* ``FliDecode.c`` did not properly check that the block advance was non-zero, + potentially leading to an infinite loop on load. +* This dates to the PIL fork. + +:cve:`2021-28677`: EPS DOS on _open +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* The readline used in EPS has to deal with any combination of ``\r`` and ``\n`` as line + endings. It accidentally used a quadratic method of accumulating lines while looking + for a line ending. +* A malicious EPS file could use this to perform a denial-of-service of Pillow in the + open phase, before an image was accepted for opening. +* This dates to the PIL fork. + +:cve:`2021-28678`: BLP DOS attack +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* ``BlpImagePlugin`` did not properly check that reads after jumping to file offsets + returned data. This could lead to a denial-of-service where the decoder could be run a + large number of times on empty data. +* This dates to Pillow 5.1.0. + +Fix memory DOS in ImageFont +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* A corrupt or specially crafted TTF font could have font metrics that lead to + unreasonably large sizes when rendering text in font. ``ImageFont.py`` did not check + the image size before allocating memory for it. +* This dates to the PIL fork. + +Deprecations +============ + +Categories +^^^^^^^^^^ + +``im.category`` is deprecated and will be removed in Pillow 10.0.0 (2023-07-01), +along with the related ``Image.NORMAL``, ``Image.SEQUENCE`` and +``Image.CONTAINER`` attributes. + +To determine if an image has multiple frames or not, +``getattr(im, "is_animated", False)`` can be used instead. + +Tk/Tcl 8.4 +^^^^^^^^^^ + +Support for Tk/Tcl 8.4 is deprecated and will be removed in Pillow 10.0.0 (2023-07-01), +when Tk/Tcl 8.5 will be the minimum supported. + +API Changes +=========== + +Image.alpha_composite: dest +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When calling :py:meth:`~PIL.Image.Image.alpha_composite`, the ``dest`` argument now +accepts negative co-ordinates, like the upper left corner of the ``box`` argument of +:py:meth:`~PIL.Image.Image.paste` can be negative. Naturally, this has effect of +cropping the overlaid image. + +Image.getexif: EXIF and GPS IFD +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Previously, :py:meth:`~PIL.Image.Image.getexif` flattened the EXIF IFD into the rest of +the data, losing information. This information is now kept separate, moved under +``im.getexif().get_ifd(0x8769)``. + +Direct access to the GPS IFD dictionary was possible through ``im.getexif()[0x8825]``. +This is now consistent with other IFDs, and must be accessed through +``im.getexif().get_ifd(0x8825)``. + +These changes only affect :py:meth:`~PIL.Image.Image.getexif`, introduced in Pillow +6.0. The older ``_getexif()`` methods are unaffected. + +Image._MODEINFO +^^^^^^^^^^^^^^^ + +This internal dictionary had been deprecated by a comment since PIL, and is now +removed. Instead, ``Image.getmodebase()``, ``Image.getmodetype()``, +``Image.getmodebandnames()``, ``Image.getmodebands()`` or ``ImageMode.getmode()`` +can be used. + +API Additions +============= + +getxmp() for JPEG images +^^^^^^^^^^^^^^^^^^^^^^^^ + +A new method has been added to return +`XMP data `_ for JPEG +images. It reads the XML data into a dictionary of names and values. + +For example:: + + >>> from PIL import Image + >>> with Image.open("Tests/images/xmp_test.jpg") as im: + >>> print(im.getxmp()) + {'RDF': {}, 'Description': {'Version': '10.4', 'ProcessVersion': '10.0', ...}, ...} + +ImageDraw.rounded_rectangle +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Added :py:meth:`~PIL.ImageDraw.ImageDraw.rounded_rectangle`. It works the same as +:py:meth:`~PIL.ImageDraw.ImageDraw.rectangle`, except with an additional ``radius`` +argument. ``radius`` is limited to half of the width or the height, so that users can +create a circle, but not any other ellipse. :: + + from PIL import Image, ImageDraw + im = Image.new("RGB", (200, 200)) + draw = ImageDraw.Draw(im) + draw.rounded_rectangle(xy=(10, 20, 190, 180), radius=30, fill="red") + +ImageOps.autocontrast: preserve_tone +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The default behaviour of :py:meth:`~PIL.ImageOps.autocontrast` is to normalize +separate histograms for each color channel, changing the tone of the image. The new +``preserve_tone`` argument keeps the tone unchanged by using one luminance histogram +for all channels. + +ImageShow.GmDisplayViewer +^^^^^^^^^^^^^^^^^^^^^^^^^ + +If GraphicsMagick is present, this new :py:class:`PIL.ImageShow.Viewer` subclass will +be registered. It uses GraphicsMagick_, an ImageMagick_ fork, to display images. + +The GraphicsMagick based viewer has a lower priority than its ImageMagick +counterpart. Thus, if both ImageMagick and GraphicsMagick are installed, +``im.show()`` and :py:func:`.ImageShow.show()` prefer the viewer based on +ImageMagick, i.e the behaviour stays the same for Pillow users having +ImageMagick installed. + +ImageShow.IPythonViewer +^^^^^^^^^^^^^^^^^^^^^^^ + +If IPython is present, this new :py:class:`PIL.ImageShow.Viewer` subclass will be +registered. It displays images on all IPython frontends. This will be helpful +to users of Google Colab, allowing ``im.show()`` to display images. + +It is lower in priority than the other default :py:class:`PIL.ImageShow.Viewer` +instances, so it will only be used by ``im.show()`` or :py:func:`.ImageShow.show()` +if none of the other viewers are available. This means that the behaviour of +:py:class:`PIL.ImageShow` will stay the same for most Pillow users. + +Saving TIFF with ICC profile +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +As is already possible for JPEG, PNG and WebP, the ICC profile for TIFF files can now +be specified through a keyword argument:: + + im.save("out.tif", icc_profile=...) + +Other Changes +============= + +GIF writer uses LZW encoding +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +GIF files are now written using LZW encoding, which will generate smaller files, +typically about 70% of the size generated by the older encoder. + +The pixel data is encoded using the format specified in the `CompuServe GIF standard +`_. + +The older encoder used a variant of run-length encoding that was compatible but less +efficient. + +GraphicsMagick +^^^^^^^^^^^^^^ + +The test suite can now be run on systems which have GraphicsMagick_ but not +ImageMagick_ installed. If both are installed, the tests prefer ImageMagick. + +Libraqm and FriBiDi linking +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The way the libraqm dependency for complex text scripts is linked has been changed: + +Source builds will now link against the system version of libraqm at build time +rather than at runtime by default. + +Binary wheels now include a statically linked modified version of libraqm that +links against FriBiDi at runtime instead. This change is intended to address +issues with the previous implementation on some platforms. These are created +by building Pillow with the new build flags ``--vendor-raqm --vendor-fribidi``. + +Windows users will now need to install ``fribidi.dll`` (or ``fribidi-0.dll``) only, +``libraqm.dll`` is no longer used. + +See :doc:`installation documentation<../installation>` for more information. + +PyQt6 +^^^^^ + +Support has been added for PyQt6. If it is installed, it will be used instead of +PySide6, PyQt5 or PySide2. + +.. _GraphicsMagick: http://www.graphicsmagick.org/ +.. _ImageMagick: https://imagemagick.org/ +.. _OSS-Fuzz: https://github.com/google/oss-fuzz diff --git a/docs/releasenotes/8.3.0.rst b/docs/releasenotes/8.3.0.rst index 9f46cc1e9..397627ced 100644 --- a/docs/releasenotes/8.3.0.rst +++ b/docs/releasenotes/8.3.0.rst @@ -1,112 +1,112 @@ -8.3.0 ------ - -Security -======== - -:cve:`2021-34552`: Fix buffer overflow -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -PIL since 1.1.4 and Pillow since 1.0 allowed parameters passed into a convert -function to trigger buffer overflow in ``Convert.c``. - -Parsing XML -^^^^^^^^^^^ - -Pillow previously parsed XMP data using Python's ``xml`` module. However, this module -is not secure. - -- :py:meth:`~PIL.Image.Image.getexif` has used ``xml`` to potentially retrieve - orientation data since Pillow 7.2.0. It has been refactored to use ``re`` instead. -- :py:meth:`~PIL.JpegImagePlugin.JpegImageFile.getxmp` was added in Pillow 8.2.0. It - will now use ``defusedxml`` instead. If the dependency is not present, an empty - dictionary will be returned and a warning raised. - -Deprecations -============ - -JpegImagePlugin.convert_dict_qtables -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -JPEG ``quantization`` is now automatically converted, but still returned as a -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 -=========== - -Changed WebP default "method" value when saving -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Previously, it was 0, for the best speed. The default has now been changed to 4, to -match WebP's default, for higher quality with still some speed optimisation. - -Default resampling filter for special image modes -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Pillow 7.0 changed the default resampling filter to ``Image.BICUBIC``. However, as this -is not supported yet for images with a custom number of bits, the default filter for -those modes has been reverted to ``Image.NEAREST``. - -ImageMorph incorrect mode errors -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -For ``apply()``, ``match()`` and ``get_on_pixels()``, if the image mode is not L, an -:py:exc:`Exception` was thrown. This has now been changed to a :py:exc:`ValueError`. - -getxmp() -^^^^^^^^ - -`XMP data `_ can now be -returned for PNG and TIFF images, through ``getxmp()`` for each format. - -The returned dictionary will start from the base of the XML, meaning that the top level -should contain an "xmpmeta" key. JPEG's ``getxmp()`` method has also been updated to -this structure. - -TIFF getexif() -^^^^^^^^^^^^^^ - -TIFF :py:attr:`~PIL.TiffImagePlugin.TiffImageFile.tag_v2` data can now be accessed -through :py:meth:`~PIL.Image.Image.getexif`. This also provides access to the GPS and -EXIF IFDs, through ``im.getexif().get_ifd(0x8825)`` and -``im.getexif().get_ifd(0x8769)`` respectively. - -API Additions -============= - -ImageOps.contain -^^^^^^^^^^^^^^^^ - -Returns a resized version of the image, set to the maximum width and height within -``size``, while maintaining the original aspect ratio. - -To compare it to other ImageOps methods: - -- :py:meth:`~PIL.ImageOps.fit` expands an image until is fills ``size``, cropping the - parts of the image that do not fit. -- :py:meth:`~PIL.ImageOps.pad` expands an image to fill ``size``, without cropping, but - instead filling the extra space with ``color``. -- :py:meth:`~PIL.ImageOps.contain` is similar to :py:meth:`~PIL.ImageOps.pad`, but it - does not fill the extra space. Instead, the original aspect ratio is maintained. So - unlike the other two methods, it is not guaranteed to return an image of ``size``. - -ICO saving: bitmap_format argument -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -By default, Pillow saves ICO files in the PNG format. They can now also be saved in BMP -format, through the new ``bitmap_format`` argument:: - - im.save("out.ico", bitmap_format="bmp") - -Other Changes -============= - -Added DDS BC5 reading and uncompressed saving -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Support has been added to read the BC5 format of DDS images, whether UNORM, SNORM or -TYPELESS. - -Support has also been added to write the uncompressed format of DDS images. +8.3.0 +----- + +Security +======== + +:cve:`2021-34552`: Fix buffer overflow +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +PIL since 1.1.4 and Pillow since 1.0 allowed parameters passed into a convert +function to trigger buffer overflow in ``Convert.c``. + +Parsing XML +^^^^^^^^^^^ + +Pillow previously parsed XMP data using Python's ``xml`` module. However, this module +is not secure. + +- :py:meth:`~PIL.Image.Image.getexif` has used ``xml`` to potentially retrieve + orientation data since Pillow 7.2.0. It has been refactored to use ``re`` instead. +- :py:meth:`~PIL.JpegImagePlugin.JpegImageFile.getxmp` was added in Pillow 8.2.0. It + will now use ``defusedxml`` instead. If the dependency is not present, an empty + dictionary will be returned and a warning raised. + +Deprecations +============ + +JpegImagePlugin.convert_dict_qtables +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +JPEG ``quantization`` is now automatically converted, but still returned as a +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 +=========== + +Changed WebP default "method" value when saving +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Previously, it was 0, for the best speed. The default has now been changed to 4, to +match WebP's default, for higher quality with still some speed optimisation. + +Default resampling filter for special image modes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Pillow 7.0 changed the default resampling filter to ``Image.BICUBIC``. However, as this +is not supported yet for images with a custom number of bits, the default filter for +those modes has been reverted to ``Image.NEAREST``. + +ImageMorph incorrect mode errors +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +For ``apply()``, ``match()`` and ``get_on_pixels()``, if the image mode is not L, an +:py:exc:`Exception` was thrown. This has now been changed to a :py:exc:`ValueError`. + +getxmp() +^^^^^^^^ + +`XMP data `_ can now be +returned for PNG and TIFF images, through ``getxmp()`` for each format. + +The returned dictionary will start from the base of the XML, meaning that the top level +should contain an "xmpmeta" key. JPEG's ``getxmp()`` method has also been updated to +this structure. + +TIFF getexif() +^^^^^^^^^^^^^^ + +TIFF :py:attr:`~PIL.TiffImagePlugin.TiffImageFile.tag_v2` data can now be accessed +through :py:meth:`~PIL.Image.Image.getexif`. This also provides access to the GPS and +EXIF IFDs, through ``im.getexif().get_ifd(0x8825)`` and +``im.getexif().get_ifd(0x8769)`` respectively. + +API Additions +============= + +ImageOps.contain +^^^^^^^^^^^^^^^^ + +Returns a resized version of the image, set to the maximum width and height within +``size``, while maintaining the original aspect ratio. + +To compare it to other ImageOps methods: + +- :py:meth:`~PIL.ImageOps.fit` expands an image until is fills ``size``, cropping the + parts of the image that do not fit. +- :py:meth:`~PIL.ImageOps.pad` expands an image to fill ``size``, without cropping, but + instead filling the extra space with ``color``. +- :py:meth:`~PIL.ImageOps.contain` is similar to :py:meth:`~PIL.ImageOps.pad`, but it + does not fill the extra space. Instead, the original aspect ratio is maintained. So + unlike the other two methods, it is not guaranteed to return an image of ``size``. + +ICO saving: bitmap_format argument +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +By default, Pillow saves ICO files in the PNG format. They can now also be saved in BMP +format, through the new ``bitmap_format`` argument:: + + im.save("out.ico", bitmap_format="bmp") + +Other Changes +============= + +Added DDS BC5 reading and uncompressed saving +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Support has been added to read the BC5 format of DDS images, whether UNORM, SNORM or +TYPELESS. + +Support has also been added to write the uncompressed format of DDS images. diff --git a/docs/releasenotes/8.3.1.rst b/docs/releasenotes/8.3.1.rst index edcda3d61..a146c656e 100644 --- a/docs/releasenotes/8.3.1.rst +++ b/docs/releasenotes/8.3.1.rst @@ -1,41 +1,41 @@ -8.3.1 ------ - -Fixed regression converting to NumPy arrays -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -This fixes a regression introduced in 8.3.0 when converting an image to a NumPy array -with a ``dtype`` argument. - -.. code-block:: pycon - - >>> from PIL import Image - >>> import numpy - >>> im = Image.new("RGB", (100, 100)) - >>> numpy.array(im, dtype=numpy.float64) - Traceback (most recent call last): - File "", line 1, in - TypeError: __array__() takes 1 positional argument but 2 were given - >>> - -Catch OSError when checking if destination is sys.stdout -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -In 8.3.0, a check to see if the destination was ``sys.stdout`` when saving an image was -updated. This lead to an :py:exc:`OSError` being raised if the environment restricted -access. - -The :py:exc:`OSError` is now silently caught. - -Fixed removing orientation in ImageOps.exif_transpose -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -In 8.3.0, :py:meth:`~PIL.ImageOps.exif_transpose` was changed to ensure that the -original image EXIF data was not modified, and the orientation was only removed from -the modified copy. - -However, for certain images the orientation was already missing from the modified -image, leading to a :py:exc:`KeyError`. - -This error has been resolved, and the copying of metadata to the modified image -improved. +8.3.1 +----- + +Fixed regression converting to NumPy arrays +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This fixes a regression introduced in 8.3.0 when converting an image to a NumPy array +with a ``dtype`` argument. + +.. code-block:: pycon + + >>> from PIL import Image + >>> import numpy + >>> im = Image.new("RGB", (100, 100)) + >>> numpy.array(im, dtype=numpy.float64) + Traceback (most recent call last): + File "", line 1, in + TypeError: __array__() takes 1 positional argument but 2 were given + >>> + +Catch OSError when checking if destination is sys.stdout +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In 8.3.0, a check to see if the destination was ``sys.stdout`` when saving an image was +updated. This lead to an :py:exc:`OSError` being raised if the environment restricted +access. + +The :py:exc:`OSError` is now silently caught. + +Fixed removing orientation in ImageOps.exif_transpose +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In 8.3.0, :py:meth:`~PIL.ImageOps.exif_transpose` was changed to ensure that the +original image EXIF data was not modified, and the orientation was only removed from +the modified copy. + +However, for certain images the orientation was already missing from the modified +image, leading to a :py:exc:`KeyError`. + +This error has been resolved, and the copying of metadata to the modified image +improved. diff --git a/docs/releasenotes/8.3.2.rst b/docs/releasenotes/8.3.2.rst index 34ba703f7..cfe753447 100644 --- a/docs/releasenotes/8.3.2.rst +++ b/docs/releasenotes/8.3.2.rst @@ -1,48 +1,48 @@ -8.3.2 ------ - -Security -======== - -:cve:`2021-23437`: Avoid potential ReDoS (regular expression denial of service) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Avoid a potential ReDoS (regular expression denial of service) in :py:class:`~PIL.ImageColor`'s -:py:meth:`~PIL.ImageColor.getrgb` by raising :py:exc:`ValueError` if the color specifier is -too long. Present since Pillow 5.2.0. - -Fix 6-byte out-of-bounds (OOB) read -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Fix 6-byte out-of-bounds (OOB) read. The previous bounds check in ``FliDecode.c`` incorrectly -calculated the required read buffer size when copying a chunk, potentially reading six extra -bytes off the end of the allocated buffer from the heap. Present since Pillow 7.1.0. - -This bug was found by Google's `OSS-Fuzz`_ `CIFuzz`_ runs. - -Other Changes -============= - -Python 3.10 wheels -^^^^^^^^^^^^^^^^^^ - -Pillow now includes binary wheels for Python 3.10. - -The Python 3.10 release candidate was released on 2021-08-03 with the final release due -2021-10-04 (:pep:`619`). The CPython core team strongly encourages maintainers of -third-party Python projects to prepare for 3.10 compatibility. And as there are `no ABI -changes`_ planned we are releasing wheels to help others prepare for 3.10, and ensure -Pillow can be used immediately on release day of 3.10.0 final. - -Fixed regressions -^^^^^^^^^^^^^^^^^ - -* Ensure TIFF ``RowsPerStrip`` is multiple of 8 for JPEG compression (:pr:`5588`). - -* Updates for :py:class:`~PIL.ImagePalette` channel order (:pr:`5599`). - -* Hide FriBiDi shim symbols to avoid conflict with real FriBiDi library (:pr:`5651`). - -.. _OSS-Fuzz: https://github.com/google/oss-fuzz -.. _CIFuzz: https://google.github.io/oss-fuzz/getting-started/continuous-integration/ -.. _no ABI changes: https://www.python.org/downloads/release/python-3100rc1/ +8.3.2 +----- + +Security +======== + +:cve:`2021-23437`: Avoid potential ReDoS (regular expression denial of service) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Avoid a potential ReDoS (regular expression denial of service) in :py:class:`~PIL.ImageColor`'s +:py:meth:`~PIL.ImageColor.getrgb` by raising :py:exc:`ValueError` if the color specifier is +too long. Present since Pillow 5.2.0. + +Fix 6-byte out-of-bounds (OOB) read +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Fix 6-byte out-of-bounds (OOB) read. The previous bounds check in ``FliDecode.c`` incorrectly +calculated the required read buffer size when copying a chunk, potentially reading six extra +bytes off the end of the allocated buffer from the heap. Present since Pillow 7.1.0. + +This bug was found by Google's `OSS-Fuzz`_ `CIFuzz`_ runs. + +Other Changes +============= + +Python 3.10 wheels +^^^^^^^^^^^^^^^^^^ + +Pillow now includes binary wheels for Python 3.10. + +The Python 3.10 release candidate was released on 2021-08-03 with the final release due +2021-10-04 (:pep:`619`). The CPython core team strongly encourages maintainers of +third-party Python projects to prepare for 3.10 compatibility. And as there are `no ABI +changes`_ planned we are releasing wheels to help others prepare for 3.10, and ensure +Pillow can be used immediately on release day of 3.10.0 final. + +Fixed regressions +^^^^^^^^^^^^^^^^^ + +* Ensure TIFF ``RowsPerStrip`` is multiple of 8 for JPEG compression (:pr:`5588`). + +* Updates for :py:class:`~PIL.ImagePalette` channel order (:pr:`5599`). + +* Hide FriBiDi shim symbols to avoid conflict with real FriBiDi library (:pr:`5651`). + +.. _OSS-Fuzz: https://github.com/google/oss-fuzz +.. _CIFuzz: https://google.github.io/oss-fuzz/getting-started/continuous-integration/ +.. _no ABI changes: https://www.python.org/downloads/release/python-3100rc1/ diff --git a/docs/releasenotes/8.4.0.rst b/docs/releasenotes/8.4.0.rst index bdc8e8020..88b472f5a 100644 --- a/docs/releasenotes/8.4.0.rst +++ b/docs/releasenotes/8.4.0.rst @@ -1,48 +1,48 @@ -8.4.0 ------ - -Deprecations -============ - -ImagePalette size parameter -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The ``size`` parameter will be removed in Pillow 10.0.0 (2023-07-01). - -Before Pillow 8.3.0, ``ImagePalette`` required palette data of particular lengths by -default, and the size parameter could be used to override that. Pillow 8.3.0 removed -the default required length, also removing the need for the size parameter. - -API Additions -============= - -Added "transparency" argument for loading EPS images -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -This new argument switches the Ghostscript device from "ppmraw" to "pngalpha", -generating an RGBA image with a transparent background instead of an RGB image with a -white background. :: - - with Image.open("sample.eps") as im: - im.load(transparency=True) - -Added WalImageFile class -^^^^^^^^^^^^^^^^^^^^^^^^ - -:py:func:`PIL.WalImageFile.open()` previously returned a generic -:py:class:`PIL.Image.Image` instance. It now returns a dedicated -:py:class:`PIL.WalImageFile.WalImageFile` class. - -Other Changes -============= - -Speed improvement when rotating square images -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Starting with Pillow 3.3.0, the speed of rotating images by 90 or 270 degrees was -improved by quickly returning :py:meth:`~PIL.Image.Image.transpose` instead, if the -rotate operation allowed for expansion and did not specify a center or post-rotate -translation. - -Since the ``expand`` flag makes no difference for square images though, Pillow now -uses this faster method for square images without the ``expand`` flag as well. +8.4.0 +----- + +Deprecations +============ + +ImagePalette size parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``size`` parameter will be removed in Pillow 10.0.0 (2023-07-01). + +Before Pillow 8.3.0, ``ImagePalette`` required palette data of particular lengths by +default, and the size parameter could be used to override that. Pillow 8.3.0 removed +the default required length, also removing the need for the size parameter. + +API Additions +============= + +Added "transparency" argument for loading EPS images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This new argument switches the Ghostscript device from "ppmraw" to "pngalpha", +generating an RGBA image with a transparent background instead of an RGB image with a +white background. :: + + with Image.open("sample.eps") as im: + im.load(transparency=True) + +Added WalImageFile class +^^^^^^^^^^^^^^^^^^^^^^^^ + +:py:func:`PIL.WalImageFile.open()` previously returned a generic +:py:class:`PIL.Image.Image` instance. It now returns a dedicated +:py:class:`PIL.WalImageFile.WalImageFile` class. + +Other Changes +============= + +Speed improvement when rotating square images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Starting with Pillow 3.3.0, the speed of rotating images by 90 or 270 degrees was +improved by quickly returning :py:meth:`~PIL.Image.Image.transpose` instead, if the +rotate operation allowed for expansion and did not specify a center or post-rotate +translation. + +Since the ``expand`` flag makes no difference for square images though, Pillow now +uses this faster method for square images without the ``expand`` flag as well. diff --git a/docs/releasenotes/9.0.0.rst b/docs/releasenotes/9.0.0.rst index fee66b6d0..7e2bcc471 100644 --- a/docs/releasenotes/9.0.0.rst +++ b/docs/releasenotes/9.0.0.rst @@ -1,172 +1,172 @@ -9.0.0 ------ - -Fredrik Lundh -============= - -This release is dedicated to the memory of Fredrik Lundh, aka Effbot, who died in -November 2021. Fredrik created PIL in 1995 and he was instrumental in the early -success of Python. - -`Guido wrote `_: - - Fredrik was an early Python contributor (e.g. Elementtree and the 're' - module) and his enthusiasm for the language and community were inspiring - for all who encountered him or his work. He spent countless hours on - comp.lang.python answering questions from newbies and advanced users alike. - - He also co-founded an early Python startup, Secret Labs AB, which among - other software released an IDE named PythonWorks. Fredrik also created the - Python Imaging Library (PIL) which is still THE way to interact with images - in Python, now most often through its Pillow fork. His effbot.org site was - a valuable resource for generations of Python users, especially its Tkinter - documentation. - -Thank you, Fredrik. - -Security -======== - -Ensure JpegImagePlugin stops at the end of a truncated file -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -``JpegImagePlugin`` may append an EOF marker to the end of a truncated file, so that -the last segment of the data will still be processed by the decoder. - -If the EOF marker is not detected as such however, this could lead to an infinite -loop where ``JpegImagePlugin`` keeps trying to end the file. - -Remove consecutive duplicate tiles that only differ by their offset -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -To prevent attempts to slow down loading times for images, if an image has consecutive -duplicate tiles that only differ by their offset, only load the last tile. Credit to -Google's `OSS-Fuzz`_ project for finding this issue. - -:cve:`2022-22817`: Restrict builtins available to ImageMath.eval -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -To limit :py:class:`PIL.ImageMath` to working with images, Pillow -will now restrict the builtins available to :py:meth:`!PIL.ImageMath.eval`. This will -help prevent problems arising if users evaluate arbitrary expressions, such as -``ImageMath.eval("exec(exit())")``. - -:cve:`2022-22815`, :cve:`2022-22816`: ImagePath.Path array handling -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -:cve:`2022-22815` (:cwe:`126`) and :cve:`2022-22816` (:cwe:`665`) were found when -initializing ``ImagePath.Path``. - -.. _OSS-Fuzz: https://github.com/google/oss-fuzz - -Backwards Incompatible Changes -============================== - -Python 3.6 -^^^^^^^^^^ - -Pillow has dropped support for Python 3.6, which reached end-of-life on 2021-12-23. - -PILLOW_VERSION constant -^^^^^^^^^^^^^^^^^^^^^^^ - -``PILLOW_VERSION`` has been removed. Use ``__version__`` instead. - -FreeType 2.7 -^^^^^^^^^^^^ - -Support for FreeType 2.7 has been removed; FreeType 2.8 is the minimum supported. - -We recommend upgrading to at least `FreeType`_ 2.10.4, which fixed a severe -vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`). - -.. _FreeType: https://freetype.org/ - -Image.show command parameter -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The ``command`` parameter has been removed. Use a subclass of -:py:class:`PIL.ImageShow.Viewer` instead. - -Image._showxv -^^^^^^^^^^^^^ - -``Image._showxv`` has been removed. Use :py:meth:`~PIL.Image.Image.show` -instead. If custom behaviour is required, use :py:meth:`~PIL.ImageShow.register` to add -a custom :py:class:`~PIL.ImageShow.Viewer` class. - -ImageFile.raise_ioerror -^^^^^^^^^^^^^^^^^^^^^^^ - -:py:exc:`IOError` was merged into :py:exc:`OSError` in Python 3.3. So, ``ImageFile.raise_ioerror`` -has been removed. Use ``ImageFile.raise_oserror`` instead. - - -API Changes -=========== - -Added line width parameter to ImageDraw polygon -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -An optional line ``width`` parameter has been added to ``ImageDraw.Draw.polygon``. - - -API Additions -============= - -ImageShow.XDGViewer -^^^^^^^^^^^^^^^^^^^ - -If ``xdg-open`` is present on Linux, this new :py:class:`PIL.ImageShow.Viewer` subclass -will be registered. It displays images using the application selected by the system. - -It is higher in priority than the other default :py:class:`PIL.ImageShow.Viewer` -instances, so it will be preferred by ``im.show()`` or :py:func:`.ImageShow.show()`. - -Added support for "title" argument to DisplayViewer -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Support has been added for the "title" argument in -:py:class:`~PIL.ImageShow.UnixViewer.DisplayViewer`, so that when ``im.show()`` or -:py:func:`.ImageShow.show()` use the ``display`` command line tool, the "title" -argument will also now be supported, e.g. ``im.show(title="My Image")`` and -``ImageShow.show(im, title="My Image")``. - -Other Changes -============= - -Convert subsequent GIF frames to RGB or RGBA -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Since each frame of a GIF can have up to 256 colors, after the first frame it is -possible for there to be too many colors to fit in a P mode image. To allow for this, -seeking to any subsequent GIF frame will now convert the image to RGB or RGBA, -depending on whether or not the first frame had transparency. - -Switched to libjpeg-turbo in macOS and Linux wheels -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The Pillow wheels from PyPI for macOS and Linux have switched from libjpeg to -libjpeg-turbo. It is a fork of libjpeg, popular for its speed. - -Because different JPEG decoders load images differently, JPEG pixels may be -altered slightly with this change. - -Added support for pickling TrueType fonts -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -TrueType fonts may now be pickled and unpickled. For example:: - - import pickle - from PIL import ImageFont - - font = ImageFont.truetype("arial.ttf", size=30) - pickled_font = pickle.dumps(font, protocol=pickle.HIGHEST_PROTOCOL) - - # Later... - unpickled_font = pickle.loads(pickled_font) - -Added support for additional TGA orientations -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -TGA images with top right or bottom right orientations are now supported. +9.0.0 +----- + +Fredrik Lundh +============= + +This release is dedicated to the memory of Fredrik Lundh, aka Effbot, who died in +November 2021. Fredrik created PIL in 1995 and he was instrumental in the early +success of Python. + +`Guido wrote `_: + + Fredrik was an early Python contributor (e.g. Elementtree and the 're' + module) and his enthusiasm for the language and community were inspiring + for all who encountered him or his work. He spent countless hours on + comp.lang.python answering questions from newbies and advanced users alike. + + He also co-founded an early Python startup, Secret Labs AB, which among + other software released an IDE named PythonWorks. Fredrik also created the + Python Imaging Library (PIL) which is still THE way to interact with images + in Python, now most often through its Pillow fork. His effbot.org site was + a valuable resource for generations of Python users, especially its Tkinter + documentation. + +Thank you, Fredrik. + +Security +======== + +Ensure JpegImagePlugin stops at the end of a truncated file +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``JpegImagePlugin`` may append an EOF marker to the end of a truncated file, so that +the last segment of the data will still be processed by the decoder. + +If the EOF marker is not detected as such however, this could lead to an infinite +loop where ``JpegImagePlugin`` keeps trying to end the file. + +Remove consecutive duplicate tiles that only differ by their offset +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To prevent attempts to slow down loading times for images, if an image has consecutive +duplicate tiles that only differ by their offset, only load the last tile. Credit to +Google's `OSS-Fuzz`_ project for finding this issue. + +:cve:`2022-22817`: Restrict builtins available to ImageMath.eval +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To limit :py:class:`PIL.ImageMath` to working with images, Pillow +will now restrict the builtins available to :py:meth:`!PIL.ImageMath.eval`. This will +help prevent problems arising if users evaluate arbitrary expressions, such as +``ImageMath.eval("exec(exit())")``. + +:cve:`2022-22815`, :cve:`2022-22816`: ImagePath.Path array handling +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:cve:`2022-22815` (:cwe:`126`) and :cve:`2022-22816` (:cwe:`665`) were found when +initializing ``ImagePath.Path``. + +.. _OSS-Fuzz: https://github.com/google/oss-fuzz + +Backwards Incompatible Changes +============================== + +Python 3.6 +^^^^^^^^^^ + +Pillow has dropped support for Python 3.6, which reached end-of-life on 2021-12-23. + +PILLOW_VERSION constant +^^^^^^^^^^^^^^^^^^^^^^^ + +``PILLOW_VERSION`` has been removed. Use ``__version__`` instead. + +FreeType 2.7 +^^^^^^^^^^^^ + +Support for FreeType 2.7 has been removed; FreeType 2.8 is the minimum supported. + +We recommend upgrading to at least `FreeType`_ 2.10.4, which fixed a severe +vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`). + +.. _FreeType: https://freetype.org/ + +Image.show command parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``command`` parameter has been removed. Use a subclass of +:py:class:`PIL.ImageShow.Viewer` instead. + +Image._showxv +^^^^^^^^^^^^^ + +``Image._showxv`` has been removed. Use :py:meth:`~PIL.Image.Image.show` +instead. If custom behaviour is required, use :py:meth:`~PIL.ImageShow.register` to add +a custom :py:class:`~PIL.ImageShow.Viewer` class. + +ImageFile.raise_ioerror +^^^^^^^^^^^^^^^^^^^^^^^ + +:py:exc:`IOError` was merged into :py:exc:`OSError` in Python 3.3. So, ``ImageFile.raise_ioerror`` +has been removed. Use ``ImageFile.raise_oserror`` instead. + + +API Changes +=========== + +Added line width parameter to ImageDraw polygon +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +An optional line ``width`` parameter has been added to ``ImageDraw.Draw.polygon``. + + +API Additions +============= + +ImageShow.XDGViewer +^^^^^^^^^^^^^^^^^^^ + +If ``xdg-open`` is present on Linux, this new :py:class:`PIL.ImageShow.Viewer` subclass +will be registered. It displays images using the application selected by the system. + +It is higher in priority than the other default :py:class:`PIL.ImageShow.Viewer` +instances, so it will be preferred by ``im.show()`` or :py:func:`.ImageShow.show()`. + +Added support for "title" argument to DisplayViewer +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Support has been added for the "title" argument in +:py:class:`~PIL.ImageShow.UnixViewer.DisplayViewer`, so that when ``im.show()`` or +:py:func:`.ImageShow.show()` use the ``display`` command line tool, the "title" +argument will also now be supported, e.g. ``im.show(title="My Image")`` and +``ImageShow.show(im, title="My Image")``. + +Other Changes +============= + +Convert subsequent GIF frames to RGB or RGBA +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Since each frame of a GIF can have up to 256 colors, after the first frame it is +possible for there to be too many colors to fit in a P mode image. To allow for this, +seeking to any subsequent GIF frame will now convert the image to RGB or RGBA, +depending on whether or not the first frame had transparency. + +Switched to libjpeg-turbo in macOS and Linux wheels +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The Pillow wheels from PyPI for macOS and Linux have switched from libjpeg to +libjpeg-turbo. It is a fork of libjpeg, popular for its speed. + +Because different JPEG decoders load images differently, JPEG pixels may be +altered slightly with this change. + +Added support for pickling TrueType fonts +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +TrueType fonts may now be pickled and unpickled. For example:: + + import pickle + from PIL import ImageFont + + font = ImageFont.truetype("arial.ttf", size=30) + pickled_font = pickle.dumps(font, protocol=pickle.HIGHEST_PROTOCOL) + + # Later... + unpickled_font = pickle.loads(pickled_font) + +Added support for additional TGA orientations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +TGA images with top right or bottom right orientations are now supported. diff --git a/docs/releasenotes/9.0.1.rst b/docs/releasenotes/9.0.1.rst index f65e3bcc2..dbc1cbee3 100644 --- a/docs/releasenotes/9.0.1.rst +++ b/docs/releasenotes/9.0.1.rst @@ -1,29 +1,29 @@ -9.0.1 ------ - -Security -======== - -This release addresses several security problems. - -:cve:`2022-24303`: Temp image removal -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -If the path to the temporary directory on Linux or macOS -contained a space, this would break removal of the temporary image file after -``im.show()`` (and related actions), and potentially remove an unrelated file. This -has been present since PIL. - -:cve:`2022-22817`: Restrict lambda expressions -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -While Pillow 9.0 restricted top-level builtins available to -:py:meth:`!PIL.ImageMath.eval`, it did not prevent builtins -available to lambda expressions. These are now also restricted. - -Other Changes -============= - -Pillow 9.0 added support for ``xdg-open`` as an image viewer, but there have been -reports that the temporary image file was removed too quickly to be loaded into the -final application. A delay has been added. +9.0.1 +----- + +Security +======== + +This release addresses several security problems. + +:cve:`2022-24303`: Temp image removal +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If the path to the temporary directory on Linux or macOS +contained a space, this would break removal of the temporary image file after +``im.show()`` (and related actions), and potentially remove an unrelated file. This +has been present since PIL. + +:cve:`2022-22817`: Restrict lambda expressions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +While Pillow 9.0 restricted top-level builtins available to +:py:meth:`!PIL.ImageMath.eval`, it did not prevent builtins +available to lambda expressions. These are now also restricted. + +Other Changes +============= + +Pillow 9.0 added support for ``xdg-open`` as an image viewer, but there have been +reports that the temporary image file was removed too quickly to be loaded into the +final application. A delay has been added. diff --git a/docs/releasenotes/9.1.0.rst b/docs/releasenotes/9.1.0.rst index 5b83d1e9c..04164833b 100644 --- a/docs/releasenotes/9.1.0.rst +++ b/docs/releasenotes/9.1.0.rst @@ -1,229 +1,229 @@ -9.1.0 ------ - -Deprecations -============ - -Constants -^^^^^^^^^ - -A number of constants have been deprecated and will be removed in Pillow 10.0.0 -(2023-07-01). Instead, :py:class:`enum.IntEnum` classes have been added. - -.. note:: - - Some of these deprecations were restored in Pillow 9.4.0. See - :ref:`restored-image-constants` - -===================================================== ============================================================ -Deprecated Use instead -===================================================== ============================================================ -``Image.NONE`` Either ``Image.Dither.NONE`` or ``Image.Resampling.NEAREST`` -``Image.NEAREST`` Either ``Image.Dither.NONE`` or ``Image.Resampling.NEAREST`` -``Image.ORDERED`` ``Image.Dither.ORDERED`` -``Image.RASTERIZE`` ``Image.Dither.RASTERIZE`` -``Image.FLOYDSTEINBERG`` ``Image.Dither.FLOYDSTEINBERG`` -``Image.WEB`` ``Image.Palette.WEB`` -``Image.ADAPTIVE`` ``Image.Palette.ADAPTIVE`` -``Image.AFFINE`` ``Image.Transform.AFFINE`` -``Image.EXTENT`` ``Image.Transform.EXTENT`` -``Image.PERSPECTIVE`` ``Image.Transform.PERSPECTIVE`` -``Image.QUAD`` ``Image.Transform.QUAD`` -``Image.MESH`` ``Image.Transform.MESH`` -``Image.FLIP_LEFT_RIGHT`` ``Image.Transpose.FLIP_LEFT_RIGHT`` -``Image.FLIP_TOP_BOTTOM`` ``Image.Transpose.FLIP_TOP_BOTTOM`` -``Image.ROTATE_90`` ``Image.Transpose.ROTATE_90`` -``Image.ROTATE_180`` ``Image.Transpose.ROTATE_180`` -``Image.ROTATE_270`` ``Image.Transpose.ROTATE_270`` -``Image.TRANSPOSE`` ``Image.Transpose.TRANSPOSE`` -``Image.TRANSVERSE`` ``Image.Transpose.TRANSVERSE`` -``Image.BOX`` ``Image.Resampling.BOX`` -``Image.BILINEAR`` ``Image.Resampling.BILINEAR`` -``Image.LINEAR`` ``Image.Resampling.BILINEAR`` -``Image.HAMMING`` ``Image.Resampling.HAMMING`` -``Image.BICUBIC`` ``Image.Resampling.BICUBIC`` -``Image.CUBIC`` ``Image.Resampling.BICUBIC`` -``Image.LANCZOS`` ``Image.Resampling.LANCZOS`` -``Image.ANTIALIAS`` ``Image.Resampling.LANCZOS`` -``Image.MEDIANCUT`` ``Image.Quantize.MEDIANCUT`` -``Image.MAXCOVERAGE`` ``Image.Quantize.MAXCOVERAGE`` -``Image.FASTOCTREE`` ``Image.Quantize.FASTOCTREE`` -``Image.LIBIMAGEQUANT`` ``Image.Quantize.LIBIMAGEQUANT`` -``ImageCms.INTENT_PERCEPTUAL`` ``ImageCms.Intent.PERCEPTUAL`` -``ImageCms.INTENT_RELATIVE_COLORMETRIC`` ``ImageCms.Intent.RELATIVE_COLORMETRIC`` -``ImageCms.INTENT_SATURATION`` ``ImageCms.Intent.SATURATION`` -``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC`` ``ImageCms.Intent.ABSOLUTE_COLORIMETRIC`` -``ImageCms.DIRECTION_INPUT`` ``ImageCms.Direction.INPUT`` -``ImageCms.DIRECTION_OUTPUT`` ``ImageCms.Direction.OUTPUT`` -``ImageCms.DIRECTION_PROOF`` ``ImageCms.Direction.PROOF`` -``ImageFont.LAYOUT_BASIC`` ``ImageFont.Layout.BASIC`` -``ImageFont.LAYOUT_RAQM`` ``ImageFont.Layout.RAQM`` -``BlpImagePlugin.BLP_FORMAT_JPEG`` ``BlpImagePlugin.Format.JPEG`` -``BlpImagePlugin.BLP_ENCODING_UNCOMPRESSED`` ``BlpImagePlugin.Encoding.UNCOMPRESSED`` -``BlpImagePlugin.BLP_ENCODING_DXT`` ``BlpImagePlugin.Encoding.DXT`` -``BlpImagePlugin.BLP_ENCODING_UNCOMPRESSED_RAW_RGBA`` ``BlpImagePlugin.Encoding.UNCOMPRESSED_RAW_RGBA`` -``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT1`` ``BlpImagePlugin.AlphaEncoding.DXT1`` -``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT3`` ``BlpImagePlugin.AlphaEncoding.DXT3`` -``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT5`` ``BlpImagePlugin.AlphaEncoding.DXT5`` -``FtexImagePlugin.FORMAT_DXT1`` ``FtexImagePlugin.Format.DXT1`` -``FtexImagePlugin.FORMAT_UNCOMPRESSED`` ``FtexImagePlugin.Format.UNCOMPRESSED`` -``PngImagePlugin.APNG_DISPOSE_OP_NONE`` ``PngImagePlugin.Disposal.OP_NONE`` -``PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND`` ``PngImagePlugin.Disposal.OP_BACKGROUND`` -``PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS`` ``PngImagePlugin.Disposal.OP_PREVIOUS`` -``PngImagePlugin.APNG_BLEND_OP_SOURCE`` ``PngImagePlugin.Blend.OP_SOURCE`` -``PngImagePlugin.APNG_BLEND_OP_OVER`` ``PngImagePlugin.Blend.OP_OVER`` -===================================================== ============================================================ - -ImageShow.Viewer.show_file file argument -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The ``file`` argument in :py:meth:`~PIL.ImageShow.Viewer.show_file()` has been -deprecated and will be removed in Pillow 10.0.0 (2023-07-01). It has been replaced by -``path``. - -In effect, ``viewer.show_file("test.jpg")`` will continue to work unchanged. -``viewer.show_file(file="test.jpg")`` will raise a deprecation warning, and suggest -``viewer.show_file(path="test.jpg")`` instead. - -FitsStubImagePlugin -^^^^^^^^^^^^^^^^^^^ - -.. deprecated:: 9.1.0 - -The stub image plugin ``FitsStubImagePlugin`` has been deprecated and will be removed in -Pillow 10.0.0 (2023-07-01). FITS images can be read without a handler through -:mod:`~PIL.FitsImagePlugin` instead. - -API Changes -=========== - -Raise an error when performing a negative crop -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Performing a negative crop on an image previously just returned a ``(0, 0)`` image. Now -it will raise a :py:exc:`ValueError`, to help reduce confusion if a user has unintentionally -provided the wrong arguments. - -Added specific error if path coordinate type is incorrect -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Rather than returning a :py:exc:`SystemError`, passing the incorrect types of coordinates into -a path will now raise a more specific :py:exc:`ValueError`, with the message "incorrect -coordinate type". - -Replace requirements.txt with extras -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Rather than installing all dependencies for docs and tests via ``requirements.txt``, -``extras_require`` is used instead. This installs only those needed and at the same -time as installing Pillow. - -For example: - -.. code-block:: bash - - # Install with dependencies for tests: - python3 -m pip install .[tests] - - # Or for building docs: - python3 -m pip install .[docs] - - # Or for all: - python3 -m pip install .[docs,tests] - -On macOS, the last argument may need to be wrapped in quotes, e.g. -``python3 -m pip install ".[tests]"`` - -Therefore ``requirements.txt`` has been removed along with the ``make install-req`` -command for installing its contents. - -API Additions -============= - -Added get_photoshop_blocks() to parse Photoshop TIFF tag -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -:py:meth:`~PIL.TiffImagePlugin.TiffImageFile.get_photoshop_blocks` has been added, to -allow users to determine what Photoshop "Image Resource Blocks" are contained within an -image. The keys of the returned dictionary are the image resource IDs. - -At present, the information within each block is merely returned as a dictionary with a -"data" entry. This will allow more useful information to be added in the future without -breaking backwards compatibility. - -Added mct and no_jp2 options for saving JPEG 2000 -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The :py:meth:`PIL.Image.Image.save` method now supports the following options for -JPEG 2000: - -**mct** - If ``1`` then enable multiple component transformation when encoding, - otherwise use ``0`` for no component transformation (default). If MCT is - enabled and ``irreversible`` is ``True`` then the Irreversible Color - Transformation will be applied, otherwise encoding will use the - Reversible Color Transformation. MCT works best with a ``mode`` of - ``RGB`` and is only applicable when the image data has 3 components. - -**no_jp2** - If ``True`` then don't wrap the raw codestream in the JP2 file format when - saving, otherwise the extension of the filename will be used to determine - the format (default). - -Added PyEncoder -^^^^^^^^^^^^^^^ - -:py:class:`~PIL.ImageFile.PyEncoder` has been added, allowing for file encoders to be -written in Python. See :ref:`Writing Your Own File Codec in Python` for -more information. - -GifImagePlugin loading strategy -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Pillow 9.0.0 introduced the conversion of subsequent GIF frames to ``RGB`` or ``RGBA``. This -behaviour can now be changed so that the first ``P`` frame is converted to ``RGB`` as -well. :: - - from PIL import GifImagePlugin - GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS - -Or subsequent frames can be kept in ``P`` mode as long as there is only a single -palette. :: - - from PIL import GifImagePlugin - GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY - -Other Changes -============= - -musllinux wheels -^^^^^^^^^^^^^^^^ - -Pillow now builds binary wheels for musllinux, suitable for Linux distributions based on the musl C standard library such as Alpine -(rather than the glibc library used by manylinux wheels). See :pep:`656`. - -ImageShow temporary files on Unix -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -When calling :py:meth:`~PIL.Image.Image.show` or using :py:mod:`~PIL.ImageShow`, -a temporary file is created from the image. On Unix, Pillow will no longer delete these -files, and instead leave it to the operating system to do so. - -Image._repr_pretty_ -^^^^^^^^^^^^^^^^^^^ - -``im._repr_pretty_`` has been added to provide a representation of an image without the -identity of the object. This allows Jupyter to describe an image and have that -description stay the same on subsequent executions of the same code. - -Added BigTIFF reading -^^^^^^^^^^^^^^^^^^^^^ - -Support has been added for reading BigTIFF images. - -Added BLP saving -^^^^^^^^^^^^^^^^ - -Support has been added for saving BLP images. ``blp_version`` can be used to specify -whether the image should be saved as BLP1 or BLP2, e.g. -``im.save("out.blp", blp_version="BLP1")``. By default, BLP2 will be used. +9.1.0 +----- + +Deprecations +============ + +Constants +^^^^^^^^^ + +A number of constants have been deprecated and will be removed in Pillow 10.0.0 +(2023-07-01). Instead, :py:class:`enum.IntEnum` classes have been added. + +.. note:: + + Some of these deprecations were restored in Pillow 9.4.0. See + :ref:`restored-image-constants` + +===================================================== ============================================================ +Deprecated Use instead +===================================================== ============================================================ +``Image.NONE`` Either ``Image.Dither.NONE`` or ``Image.Resampling.NEAREST`` +``Image.NEAREST`` Either ``Image.Dither.NONE`` or ``Image.Resampling.NEAREST`` +``Image.ORDERED`` ``Image.Dither.ORDERED`` +``Image.RASTERIZE`` ``Image.Dither.RASTERIZE`` +``Image.FLOYDSTEINBERG`` ``Image.Dither.FLOYDSTEINBERG`` +``Image.WEB`` ``Image.Palette.WEB`` +``Image.ADAPTIVE`` ``Image.Palette.ADAPTIVE`` +``Image.AFFINE`` ``Image.Transform.AFFINE`` +``Image.EXTENT`` ``Image.Transform.EXTENT`` +``Image.PERSPECTIVE`` ``Image.Transform.PERSPECTIVE`` +``Image.QUAD`` ``Image.Transform.QUAD`` +``Image.MESH`` ``Image.Transform.MESH`` +``Image.FLIP_LEFT_RIGHT`` ``Image.Transpose.FLIP_LEFT_RIGHT`` +``Image.FLIP_TOP_BOTTOM`` ``Image.Transpose.FLIP_TOP_BOTTOM`` +``Image.ROTATE_90`` ``Image.Transpose.ROTATE_90`` +``Image.ROTATE_180`` ``Image.Transpose.ROTATE_180`` +``Image.ROTATE_270`` ``Image.Transpose.ROTATE_270`` +``Image.TRANSPOSE`` ``Image.Transpose.TRANSPOSE`` +``Image.TRANSVERSE`` ``Image.Transpose.TRANSVERSE`` +``Image.BOX`` ``Image.Resampling.BOX`` +``Image.BILINEAR`` ``Image.Resampling.BILINEAR`` +``Image.LINEAR`` ``Image.Resampling.BILINEAR`` +``Image.HAMMING`` ``Image.Resampling.HAMMING`` +``Image.BICUBIC`` ``Image.Resampling.BICUBIC`` +``Image.CUBIC`` ``Image.Resampling.BICUBIC`` +``Image.LANCZOS`` ``Image.Resampling.LANCZOS`` +``Image.ANTIALIAS`` ``Image.Resampling.LANCZOS`` +``Image.MEDIANCUT`` ``Image.Quantize.MEDIANCUT`` +``Image.MAXCOVERAGE`` ``Image.Quantize.MAXCOVERAGE`` +``Image.FASTOCTREE`` ``Image.Quantize.FASTOCTREE`` +``Image.LIBIMAGEQUANT`` ``Image.Quantize.LIBIMAGEQUANT`` +``ImageCms.INTENT_PERCEPTUAL`` ``ImageCms.Intent.PERCEPTUAL`` +``ImageCms.INTENT_RELATIVE_COLORMETRIC`` ``ImageCms.Intent.RELATIVE_COLORMETRIC`` +``ImageCms.INTENT_SATURATION`` ``ImageCms.Intent.SATURATION`` +``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC`` ``ImageCms.Intent.ABSOLUTE_COLORIMETRIC`` +``ImageCms.DIRECTION_INPUT`` ``ImageCms.Direction.INPUT`` +``ImageCms.DIRECTION_OUTPUT`` ``ImageCms.Direction.OUTPUT`` +``ImageCms.DIRECTION_PROOF`` ``ImageCms.Direction.PROOF`` +``ImageFont.LAYOUT_BASIC`` ``ImageFont.Layout.BASIC`` +``ImageFont.LAYOUT_RAQM`` ``ImageFont.Layout.RAQM`` +``BlpImagePlugin.BLP_FORMAT_JPEG`` ``BlpImagePlugin.Format.JPEG`` +``BlpImagePlugin.BLP_ENCODING_UNCOMPRESSED`` ``BlpImagePlugin.Encoding.UNCOMPRESSED`` +``BlpImagePlugin.BLP_ENCODING_DXT`` ``BlpImagePlugin.Encoding.DXT`` +``BlpImagePlugin.BLP_ENCODING_UNCOMPRESSED_RAW_RGBA`` ``BlpImagePlugin.Encoding.UNCOMPRESSED_RAW_RGBA`` +``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT1`` ``BlpImagePlugin.AlphaEncoding.DXT1`` +``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT3`` ``BlpImagePlugin.AlphaEncoding.DXT3`` +``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT5`` ``BlpImagePlugin.AlphaEncoding.DXT5`` +``FtexImagePlugin.FORMAT_DXT1`` ``FtexImagePlugin.Format.DXT1`` +``FtexImagePlugin.FORMAT_UNCOMPRESSED`` ``FtexImagePlugin.Format.UNCOMPRESSED`` +``PngImagePlugin.APNG_DISPOSE_OP_NONE`` ``PngImagePlugin.Disposal.OP_NONE`` +``PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND`` ``PngImagePlugin.Disposal.OP_BACKGROUND`` +``PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS`` ``PngImagePlugin.Disposal.OP_PREVIOUS`` +``PngImagePlugin.APNG_BLEND_OP_SOURCE`` ``PngImagePlugin.Blend.OP_SOURCE`` +``PngImagePlugin.APNG_BLEND_OP_OVER`` ``PngImagePlugin.Blend.OP_OVER`` +===================================================== ============================================================ + +ImageShow.Viewer.show_file file argument +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``file`` argument in :py:meth:`~PIL.ImageShow.Viewer.show_file()` has been +deprecated and will be removed in Pillow 10.0.0 (2023-07-01). It has been replaced by +``path``. + +In effect, ``viewer.show_file("test.jpg")`` will continue to work unchanged. +``viewer.show_file(file="test.jpg")`` will raise a deprecation warning, and suggest +``viewer.show_file(path="test.jpg")`` instead. + +FitsStubImagePlugin +^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 9.1.0 + +The stub image plugin ``FitsStubImagePlugin`` has been deprecated and will be removed in +Pillow 10.0.0 (2023-07-01). FITS images can be read without a handler through +:mod:`~PIL.FitsImagePlugin` instead. + +API Changes +=========== + +Raise an error when performing a negative crop +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Performing a negative crop on an image previously just returned a ``(0, 0)`` image. Now +it will raise a :py:exc:`ValueError`, to help reduce confusion if a user has unintentionally +provided the wrong arguments. + +Added specific error if path coordinate type is incorrect +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Rather than returning a :py:exc:`SystemError`, passing the incorrect types of coordinates into +a path will now raise a more specific :py:exc:`ValueError`, with the message "incorrect +coordinate type". + +Replace requirements.txt with extras +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Rather than installing all dependencies for docs and tests via ``requirements.txt``, +``extras_require`` is used instead. This installs only those needed and at the same +time as installing Pillow. + +For example: + +.. code-block:: bash + + # Install with dependencies for tests: + python3 -m pip install .[tests] + + # Or for building docs: + python3 -m pip install .[docs] + + # Or for all: + python3 -m pip install .[docs,tests] + +On macOS, the last argument may need to be wrapped in quotes, e.g. +``python3 -m pip install ".[tests]"`` + +Therefore ``requirements.txt`` has been removed along with the ``make install-req`` +command for installing its contents. + +API Additions +============= + +Added get_photoshop_blocks() to parse Photoshop TIFF tag +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:py:meth:`~PIL.TiffImagePlugin.TiffImageFile.get_photoshop_blocks` has been added, to +allow users to determine what Photoshop "Image Resource Blocks" are contained within an +image. The keys of the returned dictionary are the image resource IDs. + +At present, the information within each block is merely returned as a dictionary with a +"data" entry. This will allow more useful information to be added in the future without +breaking backwards compatibility. + +Added mct and no_jp2 options for saving JPEG 2000 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The :py:meth:`PIL.Image.Image.save` method now supports the following options for +JPEG 2000: + +**mct** + If ``1`` then enable multiple component transformation when encoding, + otherwise use ``0`` for no component transformation (default). If MCT is + enabled and ``irreversible`` is ``True`` then the Irreversible Color + Transformation will be applied, otherwise encoding will use the + Reversible Color Transformation. MCT works best with a ``mode`` of + ``RGB`` and is only applicable when the image data has 3 components. + +**no_jp2** + If ``True`` then don't wrap the raw codestream in the JP2 file format when + saving, otherwise the extension of the filename will be used to determine + the format (default). + +Added PyEncoder +^^^^^^^^^^^^^^^ + +:py:class:`~PIL.ImageFile.PyEncoder` has been added, allowing for file encoders to be +written in Python. See :ref:`Writing Your Own File Codec in Python` for +more information. + +GifImagePlugin loading strategy +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Pillow 9.0.0 introduced the conversion of subsequent GIF frames to ``RGB`` or ``RGBA``. This +behaviour can now be changed so that the first ``P`` frame is converted to ``RGB`` as +well. :: + + from PIL import GifImagePlugin + GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS + +Or subsequent frames can be kept in ``P`` mode as long as there is only a single +palette. :: + + from PIL import GifImagePlugin + GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY + +Other Changes +============= + +musllinux wheels +^^^^^^^^^^^^^^^^ + +Pillow now builds binary wheels for musllinux, suitable for Linux distributions based on the musl C standard library such as Alpine +(rather than the glibc library used by manylinux wheels). See :pep:`656`. + +ImageShow temporary files on Unix +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When calling :py:meth:`~PIL.Image.Image.show` or using :py:mod:`~PIL.ImageShow`, +a temporary file is created from the image. On Unix, Pillow will no longer delete these +files, and instead leave it to the operating system to do so. + +Image._repr_pretty_ +^^^^^^^^^^^^^^^^^^^ + +``im._repr_pretty_`` has been added to provide a representation of an image without the +identity of the object. This allows Jupyter to describe an image and have that +description stay the same on subsequent executions of the same code. + +Added BigTIFF reading +^^^^^^^^^^^^^^^^^^^^^ + +Support has been added for reading BigTIFF images. + +Added BLP saving +^^^^^^^^^^^^^^^^ + +Support has been added for saving BLP images. ``blp_version`` can be used to specify +whether the image should be saved as BLP1 or BLP2, e.g. +``im.save("out.blp", blp_version="BLP1")``. By default, BLP2 will be used. diff --git a/docs/releasenotes/9.1.1.rst b/docs/releasenotes/9.1.1.rst index 746bec4d4..c323e3554 100644 --- a/docs/releasenotes/9.1.1.rst +++ b/docs/releasenotes/9.1.1.rst @@ -1,22 +1,22 @@ -9.1.1 ------ - -Security -======== - -This release addresses several security issues. - -:cve:`2022-30595`: Heap buffer overflow -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -When reading a TGA file with RLE packets that cross scan lines, -Pillow reads the information past the end of the first line without deducting that -from the length of the remaining file data. This vulnerability was introduced in Pillow -9.1.0, and can cause a heap buffer overflow. - -Decompression bomb check fix -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Opening an image with a zero or negative height has been found to bypass a -decompression bomb check. This will now raise a :py:exc:`SyntaxError` instead, in turn -raising a ``PIL.UnidentifiedImageError``. +9.1.1 +----- + +Security +======== + +This release addresses several security issues. + +:cve:`2022-30595`: Heap buffer overflow +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When reading a TGA file with RLE packets that cross scan lines, +Pillow reads the information past the end of the first line without deducting that +from the length of the remaining file data. This vulnerability was introduced in Pillow +9.1.0, and can cause a heap buffer overflow. + +Decompression bomb check fix +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Opening an image with a zero or negative height has been found to bypass a +decompression bomb check. This will now raise a :py:exc:`SyntaxError` instead, in turn +raising a ``PIL.UnidentifiedImageError``. diff --git a/docs/releasenotes/9.2.0.rst b/docs/releasenotes/9.2.0.rst index fe29f2e4f..4fb0afe55 100644 --- a/docs/releasenotes/9.2.0.rst +++ b/docs/releasenotes/9.2.0.rst @@ -1,148 +1,148 @@ -9.2.0 ------ - -Security -======== - -An additional decompression bomb check has been added for the GIF format. - -Deprecations -============ - -PyQt5 and PySide2 -^^^^^^^^^^^^^^^^^ - -.. deprecated:: 9.2.0 - -`Qt 5 reached end-of-life `_ on 2020-12-08 for -open-source users (and will reach EOL on 2023-12-08 for commercial licence holders). - -Support for PyQt5 and PySide2 has been deprecated from ``ImageQt`` and will be removed -in Pillow 10 (2023-07-01). Upgrade to -`PyQt6 `_ or -`PySide6 `_ instead. - -FreeTypeFont.getmask2 fill parameter -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. deprecated:: 9.2.0 - -The undocumented ``fill`` parameter of :py:meth:`.FreeTypeFont.getmask2` -has been deprecated and will be removed in Pillow 10 (2023-07-01). - -PhotoImage.paste box parameter -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. deprecated:: 9.2.0 - -The ``box`` parameter is unused. It will be removed in Pillow 10.0.0 (2023-07-01). - -Image.coerce_e -^^^^^^^^^^^^^^ - -.. deprecated:: 9.2.0 - -This undocumented method has been deprecated and will be removed in Pillow 10 -(2023-07-01). - -Font size and offset methods -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. deprecated:: 9.2.0 - -Several functions for computing the size and offset of rendered text -have been deprecated and will be removed in Pillow 10 (2023-07-01): - -=============================================================== ============================================================================================================= -Deprecated Use instead -=============================================================== ============================================================================================================= -``FreeTypeFont.getsize()`` and ``FreeTypeFont.getoffset()`` :py:meth:`.FreeTypeFont.getbbox` and :py:meth:`.FreeTypeFont.getlength` -``FreeTypeFont.getsize_multiline()`` :py:meth:`.ImageDraw.multiline_textbbox` -``ImageFont.getsize()`` :py:meth:`.ImageFont.getbbox` and :py:meth:`.ImageFont.getlength` -``TransposedFont.getsize()`` :py:meth:`.TransposedFont.getbbox` and :py:meth:`.TransposedFont.getlength` -``ImageDraw.textsize()`` and ``ImageDraw.multiline_textsize()`` :py:meth:`.ImageDraw.textbbox`, :py:meth:`.ImageDraw.textlength` and :py:meth:`.ImageDraw.multiline_textbbox` -``ImageDraw2.Draw.textsize()`` :py:meth:`.ImageDraw2.Draw.textbbox` and :py:meth:`.ImageDraw2.Draw.textlength` -=============================================================== ============================================================================================================= - -Previous code:: - - from PIL import Image, ImageDraw, ImageFont - - font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") - width, height = font.getsize("Hello world") - left, top = font.getoffset("Hello world") - - im = Image.new("RGB", (100, 100)) - draw = ImageDraw.Draw(im) - width, height = draw.textsize("Hello world", font) - - width, height = font.getsize_multiline("Hello\nworld") - width, height = draw.multiline_textsize("Hello\nworld", font) - -Use instead:: - - from PIL import Image, ImageDraw, ImageFont - - font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") - left, top, right, bottom = font.getbbox("Hello world") - width, height = right - left, bottom - top - - im = Image.new("RGB", (100, 100)) - draw = ImageDraw.Draw(im) - width = draw.textlength("Hello world", font) - - left, top, right, bottom = draw.multiline_textbbox((0, 0), "Hello\nworld", font) - width, height = right - left, bottom - top - -Previously, the ``size`` methods returned a ``height`` that included the vertical -offset of the text, while the new ``bbox`` methods distinguish this as a ``top`` -offset. - -.. image:: ../example/size_vs_bbox.png - :alt: In bbox methods, top measures the vertical distance above the text, while bottom measures that plus the vertical distance of the text itself. In size methods, height also measures the vertical distance above the text plus the vertical distance of the text itself. - :align: center - -If you are using these methods for aligning text, consider using :ref:`text-anchors` instead -which avoid issues that can occur with non-English text or unusual fonts. -For example, instead of the following code:: - - from PIL import Image, ImageDraw, ImageFont - - font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") - - im = Image.new("RGB", (100, 100)) - draw = ImageDraw.Draw(im) - width, height = draw.textsize("Hello world", font) - x, y = (100 - width) / 2, (100 - height) / 2 - draw.text((x, y), "Hello world", font=font) - -Use instead:: - - from PIL import Image, ImageDraw, ImageFont - - font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") - - im = Image.new("RGB", (100, 100)) - draw = ImageDraw.Draw(im) - draw.text((100 / 2, 100 / 2), "Hello world", font=font, anchor="mm") - -API Additions -============= - -Image.apply_transparency -^^^^^^^^^^^^^^^^^^^^^^^^ - -Added :py:meth:`~PIL.Image.Image.apply_transparency`, a method to take a P mode image -with "transparency" in ``im.info``, and apply the transparency to the palette instead. -The image's palette mode will become "RGBA", and "transparency" will be removed from -``im.info``. - -Other Changes -============= - -Using gnome-screenshot on Linux -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -In :py:meth:`~PIL.ImageGrab.grab` on Linux, if ``xdisplay`` is ``None`` then -``gnome-screenshot`` will be used to capture the display if it is installed. To capture -the default X11 display instead, pass ``xdisplay=""``. +9.2.0 +----- + +Security +======== + +An additional decompression bomb check has been added for the GIF format. + +Deprecations +============ + +PyQt5 and PySide2 +^^^^^^^^^^^^^^^^^ + +.. deprecated:: 9.2.0 + +`Qt 5 reached end-of-life `_ on 2020-12-08 for +open-source users (and will reach EOL on 2023-12-08 for commercial licence holders). + +Support for PyQt5 and PySide2 has been deprecated from ``ImageQt`` and will be removed +in Pillow 10 (2023-07-01). Upgrade to +`PyQt6 `_ or +`PySide6 `_ instead. + +FreeTypeFont.getmask2 fill parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 9.2.0 + +The undocumented ``fill`` parameter of :py:meth:`.FreeTypeFont.getmask2` +has been deprecated and will be removed in Pillow 10 (2023-07-01). + +PhotoImage.paste box parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 9.2.0 + +The ``box`` parameter is unused. It will be removed in Pillow 10.0.0 (2023-07-01). + +Image.coerce_e +^^^^^^^^^^^^^^ + +.. deprecated:: 9.2.0 + +This undocumented method has been deprecated and will be removed in Pillow 10 +(2023-07-01). + +Font size and offset methods +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 9.2.0 + +Several functions for computing the size and offset of rendered text +have been deprecated and will be removed in Pillow 10 (2023-07-01): + +=============================================================== ============================================================================================================= +Deprecated Use instead +=============================================================== ============================================================================================================= +``FreeTypeFont.getsize()`` and ``FreeTypeFont.getoffset()`` :py:meth:`.FreeTypeFont.getbbox` and :py:meth:`.FreeTypeFont.getlength` +``FreeTypeFont.getsize_multiline()`` :py:meth:`.ImageDraw.multiline_textbbox` +``ImageFont.getsize()`` :py:meth:`.ImageFont.getbbox` and :py:meth:`.ImageFont.getlength` +``TransposedFont.getsize()`` :py:meth:`.TransposedFont.getbbox` and :py:meth:`.TransposedFont.getlength` +``ImageDraw.textsize()`` and ``ImageDraw.multiline_textsize()`` :py:meth:`.ImageDraw.textbbox`, :py:meth:`.ImageDraw.textlength` and :py:meth:`.ImageDraw.multiline_textbbox` +``ImageDraw2.Draw.textsize()`` :py:meth:`.ImageDraw2.Draw.textbbox` and :py:meth:`.ImageDraw2.Draw.textlength` +=============================================================== ============================================================================================================= + +Previous code:: + + from PIL import Image, ImageDraw, ImageFont + + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + width, height = font.getsize("Hello world") + left, top = font.getoffset("Hello world") + + im = Image.new("RGB", (100, 100)) + draw = ImageDraw.Draw(im) + width, height = draw.textsize("Hello world", font) + + width, height = font.getsize_multiline("Hello\nworld") + width, height = draw.multiline_textsize("Hello\nworld", font) + +Use instead:: + + from PIL import Image, ImageDraw, ImageFont + + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + left, top, right, bottom = font.getbbox("Hello world") + width, height = right - left, bottom - top + + im = Image.new("RGB", (100, 100)) + draw = ImageDraw.Draw(im) + width = draw.textlength("Hello world", font) + + left, top, right, bottom = draw.multiline_textbbox((0, 0), "Hello\nworld", font) + width, height = right - left, bottom - top + +Previously, the ``size`` methods returned a ``height`` that included the vertical +offset of the text, while the new ``bbox`` methods distinguish this as a ``top`` +offset. + +.. image:: ../example/size_vs_bbox.png + :alt: In bbox methods, top measures the vertical distance above the text, while bottom measures that plus the vertical distance of the text itself. In size methods, height also measures the vertical distance above the text plus the vertical distance of the text itself. + :align: center + +If you are using these methods for aligning text, consider using :ref:`text-anchors` instead +which avoid issues that can occur with non-English text or unusual fonts. +For example, instead of the following code:: + + from PIL import Image, ImageDraw, ImageFont + + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + + im = Image.new("RGB", (100, 100)) + draw = ImageDraw.Draw(im) + width, height = draw.textsize("Hello world", font) + x, y = (100 - width) / 2, (100 - height) / 2 + draw.text((x, y), "Hello world", font=font) + +Use instead:: + + from PIL import Image, ImageDraw, ImageFont + + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + + im = Image.new("RGB", (100, 100)) + draw = ImageDraw.Draw(im) + draw.text((100 / 2, 100 / 2), "Hello world", font=font, anchor="mm") + +API Additions +============= + +Image.apply_transparency +^^^^^^^^^^^^^^^^^^^^^^^^ + +Added :py:meth:`~PIL.Image.Image.apply_transparency`, a method to take a P mode image +with "transparency" in ``im.info``, and apply the transparency to the palette instead. +The image's palette mode will become "RGBA", and "transparency" will be removed from +``im.info``. + +Other Changes +============= + +Using gnome-screenshot on Linux +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In :py:meth:`~PIL.ImageGrab.grab` on Linux, if ``xdisplay`` is ``None`` then +``gnome-screenshot`` will be used to capture the display if it is installed. To capture +the default X11 display instead, pass ``xdisplay=""``. diff --git a/docs/releasenotes/9.3.0.rst b/docs/releasenotes/9.3.0.rst index e5987ce08..d00ba3968 100644 --- a/docs/releasenotes/9.3.0.rst +++ b/docs/releasenotes/9.3.0.rst @@ -1,108 +1,108 @@ -9.3.0 ------ - -Security -======== - -Initialize libtiff buffer when saving -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -When saving a TIFF image to a file object using libtiff, the buffer was not -initialized. This behaviour introduced in Pillow 2.0.0, and has now been fixed. - -Decode JPEG compressed BLP1 data in original mode -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Within the BLP image format, BLP1 data may use JPEG compression. Instead of -telling the JPEG library that this data is in BGRX mode, Pillow will now -decode the data in its natural CMYK mode, then convert it to RGB and rearrange -the channels afterwards. Trying to load the data in an incorrect mode could -result in a segmentation fault. This issue was introduced in Pillow 9.1.0. - -Limit SAMPLESPERPIXEL to avoid runtime DOS -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -A large value in the ``SAMPLESPERPIXEL`` tag could lead to a memory and runtime DOS in -``TiffImagePlugin.py`` when setting up the context for image decoding. -This was introduced in Pillow 9.2.0, found with `OSS-Fuzz`_ and fixed by limiting -``SAMPLESPERPIXEL`` to the number of planes that we can decode. - - -API Additions -============= - -Allow default ImageDraw font to be set -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Rather than specifying a font when calling text-related ImageDraw methods, or -setting a font on each ImageDraw instance, the default font can now be set for -all future ImageDraw operations:: - - from PIL import ImageDraw, ImageFont - ImageDraw.ImageDraw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") - -Saving multiple MPO frames -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Multiple MPO frames can now be saved. Using the ``save_all`` argument, all of -an image's frames will be saved to file:: - - from PIL import Image - im = Image.open("frozenpond.mpo") - im.save(out, save_all=True) - -Additional images can also be appended when saving, by combining the -``save_all`` argument with the ``append_images`` argument:: - - im.save(out, save_all=True, append_images=[im1, im2, ...]) - -Added ExifTags enums -^^^^^^^^^^^^^^^^^^^^ - -The data from :py:data:`~PIL.ExifTags.TAGS` and -:py:data:`~PIL.ExifTags.GPSTAGS` is now also exposed as -:py:class:`enum.IntEnum` classes: :py:data:`~PIL.ExifTags.Base` and -:py:data:`~PIL.ExifTags.GPS`. - - -Other Changes -============= - -Python 3.11 wheels -^^^^^^^^^^^^^^^^^^ - -Pillow 9.2.0 had wheels built against Python 3.11 beta, available as a preview to help -others prepare for 3.11, and ensure Pillow can be used immediately on release day of -3.11.0 final (2022-10-24, :pep:`664`). - -Pillow 9.3.0 now officially includes binary wheels for Python 3.11 final. - -Windows wheels -^^^^^^^^^^^^^^ - -This release contains wheels for Windows built using GitHub Actions. - -Previously they were built by `Christoph Gohlke `_. - -A huge thanks to Christoph for building Windows binaries for us for around a decade, -plus testing, and fixing over a hundred bug fixes along the way, in addition to building -and hosting unofficial Windows binaries for hundreds of Python projects! - -Added DDS ATI1, ATI2 and BC6H reading -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Support has been added to read the ATI1, ATI2 and BC6H formats of DDS images. - -Release GIL when converting images using matrix operations -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Python's Global Interpreter Lock is now released when converting images using matrix -operations. - -Show all frames with ImageShow -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -When calling :py:meth:`~PIL.Image.Image.show` or using -:py:mod:`~PIL.ImageShow`, all frames will now be shown. - -.. _OSS-Fuzz: https://github.com/google/oss-fuzz +9.3.0 +----- + +Security +======== + +Initialize libtiff buffer when saving +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When saving a TIFF image to a file object using libtiff, the buffer was not +initialized. This behaviour introduced in Pillow 2.0.0, and has now been fixed. + +Decode JPEG compressed BLP1 data in original mode +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Within the BLP image format, BLP1 data may use JPEG compression. Instead of +telling the JPEG library that this data is in BGRX mode, Pillow will now +decode the data in its natural CMYK mode, then convert it to RGB and rearrange +the channels afterwards. Trying to load the data in an incorrect mode could +result in a segmentation fault. This issue was introduced in Pillow 9.1.0. + +Limit SAMPLESPERPIXEL to avoid runtime DOS +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +A large value in the ``SAMPLESPERPIXEL`` tag could lead to a memory and runtime DOS in +``TiffImagePlugin.py`` when setting up the context for image decoding. +This was introduced in Pillow 9.2.0, found with `OSS-Fuzz`_ and fixed by limiting +``SAMPLESPERPIXEL`` to the number of planes that we can decode. + + +API Additions +============= + +Allow default ImageDraw font to be set +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Rather than specifying a font when calling text-related ImageDraw methods, or +setting a font on each ImageDraw instance, the default font can now be set for +all future ImageDraw operations:: + + from PIL import ImageDraw, ImageFont + ImageDraw.ImageDraw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + +Saving multiple MPO frames +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Multiple MPO frames can now be saved. Using the ``save_all`` argument, all of +an image's frames will be saved to file:: + + from PIL import Image + im = Image.open("frozenpond.mpo") + im.save(out, save_all=True) + +Additional images can also be appended when saving, by combining the +``save_all`` argument with the ``append_images`` argument:: + + im.save(out, save_all=True, append_images=[im1, im2, ...]) + +Added ExifTags enums +^^^^^^^^^^^^^^^^^^^^ + +The data from :py:data:`~PIL.ExifTags.TAGS` and +:py:data:`~PIL.ExifTags.GPSTAGS` is now also exposed as +:py:class:`enum.IntEnum` classes: :py:data:`~PIL.ExifTags.Base` and +:py:data:`~PIL.ExifTags.GPS`. + + +Other Changes +============= + +Python 3.11 wheels +^^^^^^^^^^^^^^^^^^ + +Pillow 9.2.0 had wheels built against Python 3.11 beta, available as a preview to help +others prepare for 3.11, and ensure Pillow can be used immediately on release day of +3.11.0 final (2022-10-24, :pep:`664`). + +Pillow 9.3.0 now officially includes binary wheels for Python 3.11 final. + +Windows wheels +^^^^^^^^^^^^^^ + +This release contains wheels for Windows built using GitHub Actions. + +Previously they were built by `Christoph Gohlke `_. + +A huge thanks to Christoph for building Windows binaries for us for around a decade, +plus testing, and fixing over a hundred bug fixes along the way, in addition to building +and hosting unofficial Windows binaries for hundreds of Python projects! + +Added DDS ATI1, ATI2 and BC6H reading +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Support has been added to read the ATI1, ATI2 and BC6H formats of DDS images. + +Release GIL when converting images using matrix operations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Python's Global Interpreter Lock is now released when converting images using matrix +operations. + +Show all frames with ImageShow +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When calling :py:meth:`~PIL.Image.Image.show` or using +:py:mod:`~PIL.ImageShow`, all frames will now be shown. + +.. _OSS-Fuzz: https://github.com/google/oss-fuzz diff --git a/docs/releasenotes/9.4.0.rst b/docs/releasenotes/9.4.0.rst index 37f26a22c..f4e67ccfd 100644 --- a/docs/releasenotes/9.4.0.rst +++ b/docs/releasenotes/9.4.0.rst @@ -1,135 +1,135 @@ -9.4.0 ------ - -Security -======== - -Fix memory DOS in ImageFont -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -A corrupt or specially crafted TTF font could have font metrics that lead to -unreasonably large sizes when rendering text in font. ``ImageFont.py`` did not -check the image size before allocating memory for it. This dates to the PIL -fork. Pillow 8.2.0 added a check for large sizes, but did not consider the -case where one dimension is zero. - -Null pointer dereference crash in ImageFont -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -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 -============= - -Added start position for getmask and getmask2 -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Text may render differently when starting at fractional coordinates, so -:py:meth:`.FreeTypeFont.getmask` and :py:meth:`.FreeTypeFont.getmask2` now -support a ``start`` argument. This tuple of horizontal and vertical offset -will be used internally by :py:meth:`.ImageDraw.text` to more accurately place -text at the ``xy`` coordinates. - -Added the ``exact`` encoding option for WebP -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The ``exact`` encoding option for WebP is now supported. The WebP encoder -removes the hidden RGB values for better compression by default in libwebp 0.5 -or later. By setting this option to ``True``, the encoder will keep the hidden -RGB values. - -Added ``signed`` option when saving JPEG2000 -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -If the ``signed`` keyword argument is present and true when saving JPEG2000 -images, then tell the encoder to save the image as signed. - -Added IFD, Interop and LightSource ExifTags enums -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -:py:data:`~PIL.ExifTags.IFD` has been added, allowing enums to be used with -:py:meth:`~PIL.Image.Exif.get_ifd`:: - - from PIL import Image, ExifTags - im = Image.open("Tests/images/flower.jpg") - print(im.getexif().get_ifd(ExifTags.IFD.Exif)) - -``IFD1`` can also be used with :py:meth:`~PIL.Image.Exif.get_ifd`, but it should -not be used in other contexts, as the enum value is only internally meaningful. - -:py:data:`~PIL.ExifTags.Interop` has been added for tags within the Interop IFD:: - - from PIL import Image, ExifTags - im = Image.open("Tests/images/flower.jpg") - interop_ifd = im.getexif().get_ifd(ExifTags.IFD.Interop) - print(interop_ifd.get(ExifTags.Interop.InteropIndex)) # R98 - -:py:data:`~PIL.ExifTags.LightSource` has been added for values within the LightSource -tag:: - - from PIL import Image, ExifTags - im = Image.open("Tests/images/iptc.jpg") - exif_ifd = im.getexif().get_ifd(ExifTags.IFD.Exif) - print(ExifTags.LightSource(exif_ifd[0x9208])) # LightSource.Unknown - -getxmp() -^^^^^^^^ - -`XMP data `_ can now be -decoded for WEBP images through ``getxmp()``. - -Writing JPEG comments -^^^^^^^^^^^^^^^^^^^^^ - -When saving a JPEG image, a comment can now be written from -:py:attr:`~PIL.Image.Image.info`, or by using an argument when saving:: - - im.save(out, comment="Test comment") - -Other Changes -============= - -Added support for DDS L and LA images -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Support has been added to read and write L and LA DDS images in the uncompressed -format, known as "luminance" textures. - -.. _restored-image-constants: - -Constants -^^^^^^^^^ - -In Pillow 9.1.0, the following constants were deprecated. That has been reversed and -these constants will now remain available. - -- ``Image.NONE`` -- ``Image.NEAREST`` -- ``Image.ORDERED`` -- ``Image.RASTERIZE`` -- ``Image.FLOYDSTEINBERG`` -- ``Image.WEB`` -- ``Image.ADAPTIVE`` -- ``Image.AFFINE`` -- ``Image.EXTENT`` -- ``Image.PERSPECTIVE`` -- ``Image.QUAD`` -- ``Image.MESH`` -- ``Image.FLIP_LEFT_RIGHT`` -- ``Image.FLIP_TOP_BOTTOM`` -- ``Image.ROTATE_90`` -- ``Image.ROTATE_180`` -- ``Image.ROTATE_270`` -- ``Image.TRANSPOSE`` -- ``Image.TRANSVERSE`` -- ``Image.BOX`` -- ``Image.BILINEAR`` -- ``Image.HAMMING`` -- ``Image.BICUBIC`` -- ``Image.LANCZOS`` -- ``Image.MEDIANCUT`` -- ``Image.MAXCOVERAGE`` -- ``Image.FASTOCTREE`` -- ``Image.LIBIMAGEQUANT`` +9.4.0 +----- + +Security +======== + +Fix memory DOS in ImageFont +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +A corrupt or specially crafted TTF font could have font metrics that lead to +unreasonably large sizes when rendering text in font. ``ImageFont.py`` did not +check the image size before allocating memory for it. This dates to the PIL +fork. Pillow 8.2.0 added a check for large sizes, but did not consider the +case where one dimension is zero. + +Null pointer dereference crash in ImageFont +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +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 +============= + +Added start position for getmask and getmask2 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Text may render differently when starting at fractional coordinates, so +:py:meth:`.FreeTypeFont.getmask` and :py:meth:`.FreeTypeFont.getmask2` now +support a ``start`` argument. This tuple of horizontal and vertical offset +will be used internally by :py:meth:`.ImageDraw.text` to more accurately place +text at the ``xy`` coordinates. + +Added the ``exact`` encoding option for WebP +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``exact`` encoding option for WebP is now supported. The WebP encoder +removes the hidden RGB values for better compression by default in libwebp 0.5 +or later. By setting this option to ``True``, the encoder will keep the hidden +RGB values. + +Added ``signed`` option when saving JPEG2000 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If the ``signed`` keyword argument is present and true when saving JPEG2000 +images, then tell the encoder to save the image as signed. + +Added IFD, Interop and LightSource ExifTags enums +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:py:data:`~PIL.ExifTags.IFD` has been added, allowing enums to be used with +:py:meth:`~PIL.Image.Exif.get_ifd`:: + + from PIL import Image, ExifTags + im = Image.open("Tests/images/flower.jpg") + print(im.getexif().get_ifd(ExifTags.IFD.Exif)) + +``IFD1`` can also be used with :py:meth:`~PIL.Image.Exif.get_ifd`, but it should +not be used in other contexts, as the enum value is only internally meaningful. + +:py:data:`~PIL.ExifTags.Interop` has been added for tags within the Interop IFD:: + + from PIL import Image, ExifTags + im = Image.open("Tests/images/flower.jpg") + interop_ifd = im.getexif().get_ifd(ExifTags.IFD.Interop) + print(interop_ifd.get(ExifTags.Interop.InteropIndex)) # R98 + +:py:data:`~PIL.ExifTags.LightSource` has been added for values within the LightSource +tag:: + + from PIL import Image, ExifTags + im = Image.open("Tests/images/iptc.jpg") + exif_ifd = im.getexif().get_ifd(ExifTags.IFD.Exif) + print(ExifTags.LightSource(exif_ifd[0x9208])) # LightSource.Unknown + +getxmp() +^^^^^^^^ + +`XMP data `_ can now be +decoded for WEBP images through ``getxmp()``. + +Writing JPEG comments +^^^^^^^^^^^^^^^^^^^^^ + +When saving a JPEG image, a comment can now be written from +:py:attr:`~PIL.Image.Image.info`, or by using an argument when saving:: + + im.save(out, comment="Test comment") + +Other Changes +============= + +Added support for DDS L and LA images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Support has been added to read and write L and LA DDS images in the uncompressed +format, known as "luminance" textures. + +.. _restored-image-constants: + +Constants +^^^^^^^^^ + +In Pillow 9.1.0, the following constants were deprecated. That has been reversed and +these constants will now remain available. + +- ``Image.NONE`` +- ``Image.NEAREST`` +- ``Image.ORDERED`` +- ``Image.RASTERIZE`` +- ``Image.FLOYDSTEINBERG`` +- ``Image.WEB`` +- ``Image.ADAPTIVE`` +- ``Image.AFFINE`` +- ``Image.EXTENT`` +- ``Image.PERSPECTIVE`` +- ``Image.QUAD`` +- ``Image.MESH`` +- ``Image.FLIP_LEFT_RIGHT`` +- ``Image.FLIP_TOP_BOTTOM`` +- ``Image.ROTATE_90`` +- ``Image.ROTATE_180`` +- ``Image.ROTATE_270`` +- ``Image.TRANSPOSE`` +- ``Image.TRANSVERSE`` +- ``Image.BOX`` +- ``Image.BILINEAR`` +- ``Image.HAMMING`` +- ``Image.BICUBIC`` +- ``Image.LANCZOS`` +- ``Image.MEDIANCUT`` +- ``Image.MAXCOVERAGE`` +- ``Image.FASTOCTREE`` +- ``Image.LIBIMAGEQUANT`` diff --git a/docs/releasenotes/9.5.0.rst b/docs/releasenotes/9.5.0.rst index 08e9ec2a4..1fbda6b5c 100644 --- a/docs/releasenotes/9.5.0.rst +++ b/docs/releasenotes/9.5.0.rst @@ -1,96 +1,96 @@ -9.5.0 ------ - -Security -======== - -Clear PPM half token after use -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Image files that are small on disk are often prevented from expanding to be -big images consuming a large amount of resources simply because they lack the -data to populate those resources. - -PpmImagePlugin might hold onto the last data read for a pixel value in case the -pixel value has not been finished yet. However, that data was not being cleared -afterwards, meaning that infinite data could be available to fill any image -size. This has been present since Pillow 9.2.0. - -That data is now cleared after use. - -Saving TIFF tag ImageSourceData -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -If Pillow incorrectly saved the TIFF tag ImageSourceData as ASCII instead of -UNDEFINED, a segmentation fault was triggered. - -The correct tag type will now be used by default instead. - -Deprecations -============ - -PSFile -^^^^^^ - -The :py:class:`~PIL.EpsImagePlugin.PSFile` class has been deprecated and will -be removed in Pillow 11 (2024-10-15). This class was only made as a helper to -be used internally, so there is no replacement. If you need this functionality -though, it is a very short class that can easily be recreated in your own code. - -API Additions -============= - -QOI file format -^^^^^^^^^^^^^^^ - -Pillow can now read images in Quite OK Image format. - -Added ``dpi`` argument when saving PDFs -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -When saving a PDF, resolution could already be specified using the -``resolution`` argument. Now, a tuple of ``(x_resolution, y_resolution)`` can -be provided as ``dpi``. If both are provided, ``dpi`` will override -``resolution``. - -Added ``corners`` argument to ``ImageDraw.rounded_rectangle()`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -:py:meth:`.ImageDraw.rounded_rectangle` now accepts a keyword argument of -``corners``. This a tuple of Booleans, specifying whether to round each corner, -``(top_left, top_right, bottom_right, bottom_left)``. - -JPEG2000 comments and PLT marker -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -When opening a JPEG2000 image, the comment may now be read into -:py:attr:`~PIL.Image.Image.info`. The ``comment`` keyword argument can be used -to save it back again. - -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 -============= - -Added support for saving PDFs in RGBA mode -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Using the JPXDecode filter, PDFs can now be saved in RGBA mode. - -Improved I;16N support -^^^^^^^^^^^^^^^^^^^^^^ - -Support has been added for I;16N access, packing and unpacking. Conversion to -and from L mode has also been added. - -BGR;* modes -^^^^^^^^^^^ - -It is now possible to create new BGR;15, BGR;16 and BGR;24 images. Conversely, BGR;32 -has been removed from ImageMode and its associated methods, dropping the little support -Pillow had for the mode. - -With that, all modes listed under :ref:`concept-modes` can now be used to create a new -image. +9.5.0 +----- + +Security +======== + +Clear PPM half token after use +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Image files that are small on disk are often prevented from expanding to be +big images consuming a large amount of resources simply because they lack the +data to populate those resources. + +PpmImagePlugin might hold onto the last data read for a pixel value in case the +pixel value has not been finished yet. However, that data was not being cleared +afterwards, meaning that infinite data could be available to fill any image +size. This has been present since Pillow 9.2.0. + +That data is now cleared after use. + +Saving TIFF tag ImageSourceData +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If Pillow incorrectly saved the TIFF tag ImageSourceData as ASCII instead of +UNDEFINED, a segmentation fault was triggered. + +The correct tag type will now be used by default instead. + +Deprecations +============ + +PSFile +^^^^^^ + +The :py:class:`~PIL.EpsImagePlugin.PSFile` class has been deprecated and will +be removed in Pillow 11 (2024-10-15). This class was only made as a helper to +be used internally, so there is no replacement. If you need this functionality +though, it is a very short class that can easily be recreated in your own code. + +API Additions +============= + +QOI file format +^^^^^^^^^^^^^^^ + +Pillow can now read images in Quite OK Image format. + +Added ``dpi`` argument when saving PDFs +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When saving a PDF, resolution could already be specified using the +``resolution`` argument. Now, a tuple of ``(x_resolution, y_resolution)`` can +be provided as ``dpi``. If both are provided, ``dpi`` will override +``resolution``. + +Added ``corners`` argument to ``ImageDraw.rounded_rectangle()`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:py:meth:`.ImageDraw.rounded_rectangle` now accepts a keyword argument of +``corners``. This a tuple of Booleans, specifying whether to round each corner, +``(top_left, top_right, bottom_right, bottom_left)``. + +JPEG2000 comments and PLT marker +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When opening a JPEG2000 image, the comment may now be read into +:py:attr:`~PIL.Image.Image.info`. The ``comment`` keyword argument can be used +to save it back again. + +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 +============= + +Added support for saving PDFs in RGBA mode +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Using the JPXDecode filter, PDFs can now be saved in RGBA mode. + +Improved I;16N support +^^^^^^^^^^^^^^^^^^^^^^ + +Support has been added for I;16N access, packing and unpacking. Conversion to +and from L mode has also been added. + +BGR;* modes +^^^^^^^^^^^ + +It is now possible to create new BGR;15, BGR;16 and BGR;24 images. Conversely, BGR;32 +has been removed from ImageMode and its associated methods, dropping the little support +Pillow had for the mode. + +With that, all modes listed under :ref:`concept-modes` can now be used to create a new +image. diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index 6ee5fb6c8..2d1e95a45 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -1,77 +1,77 @@ -Release Notes -============= - -Pillow is released quarterly on January 2nd, April 1st, July 1st and October 15th. -Patch releases are created if the latest release contains severe bugs, or if security -fixes are put together before a scheduled release. See :ref:`versioning` for more -information. - -Please use the latest version of Pillow. Functionality and security fixes should not be -expected to be backported to earlier versions. - -.. note:: Contributors please include release notes as needed or appropriate with your bug fixes, feature additions and tests. - -.. toctree:: - :maxdepth: 2 - - 10.4.0 - 10.3.0 - 10.2.0 - 10.1.0 - 10.0.1 - 10.0.0 - 9.5.0 - 9.4.0 - 9.3.0 - 9.2.0 - 9.1.1 - 9.1.0 - 9.0.1 - 9.0.0 - 8.4.0 - 8.3.2 - 8.3.1 - 8.3.0 - 8.2.0 - 8.1.2 - 8.1.1 - 8.1.0 - 8.0.1 - 8.0.0 - 7.2.0 - 7.1.2 - 7.1.1 - 7.1.0 - 7.0.0 - 6.2.2 - 6.2.1 - 6.2.0 - 6.1.0 - 6.0.0 - 5.4.1 - 5.4.0 - 5.3.0 - 5.2.0 - 5.1.0 - 5.0.0 - 4.3.0 - 4.2.1 - 4.2.0 - 4.1.1 - 4.1.0 - 4.0.0 - 3.4.0 - 3.3.2 - 3.3.0 - 3.2.0 - 3.1.2 - 3.1.1 - 3.1.0 - 3.0.0 - 2.8.0 - 2.7.0 - 2.6.0 - 2.5.2 - 2.3.2 - 2.3.1 - versioning +Release Notes +============= + +Pillow is released quarterly on January 2nd, April 1st, July 1st and October 15th. +Patch releases are created if the latest release contains severe bugs, or if security +fixes are put together before a scheduled release. See :ref:`versioning` for more +information. + +Please use the latest version of Pillow. Functionality and security fixes should not be +expected to be backported to earlier versions. + +.. note:: Contributors please include release notes as needed or appropriate with your bug fixes, feature additions and tests. + +.. toctree:: + :maxdepth: 2 + + 10.4.0 + 10.3.0 + 10.2.0 + 10.1.0 + 10.0.1 + 10.0.0 + 9.5.0 + 9.4.0 + 9.3.0 + 9.2.0 + 9.1.1 + 9.1.0 + 9.0.1 + 9.0.0 + 8.4.0 + 8.3.2 + 8.3.1 + 8.3.0 + 8.2.0 + 8.1.2 + 8.1.1 + 8.1.0 + 8.0.1 + 8.0.0 + 7.2.0 + 7.1.2 + 7.1.1 + 7.1.0 + 7.0.0 + 6.2.2 + 6.2.1 + 6.2.0 + 6.1.0 + 6.0.0 + 5.4.1 + 5.4.0 + 5.3.0 + 5.2.0 + 5.1.0 + 5.0.0 + 4.3.0 + 4.2.1 + 4.2.0 + 4.1.1 + 4.1.0 + 4.0.0 + 3.4.0 + 3.3.2 + 3.3.0 + 3.2.0 + 3.1.2 + 3.1.1 + 3.1.0 + 3.0.0 + 2.8.0 + 2.7.0 + 2.6.0 + 2.5.2 + 2.3.2 + 2.3.1 + versioning diff --git a/docs/releasenotes/template.rst b/docs/releasenotes/template.rst index cfc7221a3..739c5026b 100644 --- a/docs/releasenotes/template.rst +++ b/docs/releasenotes/template.rst @@ -1,53 +1,53 @@ -xx.y.z ------- - -Security -======== - -TODO -^^^^ - -TODO - -:cve:`YYYY-XXXXX`: TODO -^^^^^^^^^^^^^^^^^^^^^^^ - -TODO - -Backwards Incompatible Changes -============================== - -TODO -^^^^ - -Deprecations -============ - -TODO -^^^^ - -TODO - -API Changes -=========== - -TODO -^^^^ - -TODO - -API Additions -============= - -TODO -^^^^ - -TODO - -Other Changes -============= - -TODO -^^^^ - -TODO +xx.y.z +------ + +Security +======== + +TODO +^^^^ + +TODO + +:cve:`YYYY-XXXXX`: TODO +^^^^^^^^^^^^^^^^^^^^^^^ + +TODO + +Backwards Incompatible Changes +============================== + +TODO +^^^^ + +Deprecations +============ + +TODO +^^^^ + +TODO + +API Changes +=========== + +TODO +^^^^ + +TODO + +API Additions +============= + +TODO +^^^^ + +TODO + +Other Changes +============= + +TODO +^^^^ + +TODO diff --git a/docs/releasenotes/versioning.rst b/docs/releasenotes/versioning.rst index 2a0af9e59..cfb4fe9d2 100644 --- a/docs/releasenotes/versioning.rst +++ b/docs/releasenotes/versioning.rst @@ -1,30 +1,30 @@ -.. _versioning: - -Versioning -========== - -Pillow follows `Semantic Versioning `_: - - Given a version number MAJOR.MINOR.PATCH, increment the: - - 1. MAJOR version when you make incompatible API changes, - 2. MINOR version when you add functionality in a backwards compatible manner, and - 3. PATCH version when you make backwards compatible bug fixes. - -Quarterly releases ("`Main Release `_") -bump at least the MINOR version, as new functionality has likely been added in the -prior three months. - -A quarterly release bumps the MAJOR version when incompatible API changes are -made, such as removing deprecated APIs or dropping an EOL Python version. In practice, -these occur every 12-18 months, guided by -`Python's EOL schedule `_, and -any APIs that have been deprecated for at least a year are removed at the same time. - -PATCH versions ("`Point Release `_" -or "`Embargoed Release `_") -are for security, installation or critical bug fixes. These are less common as it is -preferred to stick to quarterly releases. - -Between quarterly releases, ``.dev0`` is appended to the ``main`` branch, indicating that -this is not a formally released copy. +.. _versioning: + +Versioning +========== + +Pillow follows `Semantic Versioning `_: + + Given a version number MAJOR.MINOR.PATCH, increment the: + + 1. MAJOR version when you make incompatible API changes, + 2. MINOR version when you add functionality in a backwards compatible manner, and + 3. PATCH version when you make backwards compatible bug fixes. + +Quarterly releases ("`Main Release `_") +bump at least the MINOR version, as new functionality has likely been added in the +prior three months. + +A quarterly release bumps the MAJOR version when incompatible API changes are +made, such as removing deprecated APIs or dropping an EOL Python version. In practice, +these occur every 12-18 months, guided by +`Python's EOL schedule `_, and +any APIs that have been deprecated for at least a year are removed at the same time. + +PATCH versions ("`Point Release `_" +or "`Embargoed Release `_") +are for security, installation or critical bug fixes. These are less common as it is +preferred to stick to quarterly releases. + +Between quarterly releases, ``.dev0`` is appended to the ``main`` branch, indicating that +this is not a formally released copy. diff --git a/docs/resources/anchor_horizontal.svg b/docs/resources/anchor_horizontal.svg index a0648a10c..300d30ffd 100644 --- a/docs/resources/anchor_horizontal.svg +++ b/docs/resources/anchor_horizontal.svg @@ -1,467 +1,467 @@ - - - - - Pillow horizontal text anchors - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - Pillow horizontal text anchors - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - (d) descender - (s) baseline - (a) ascender - (m) middle - (t) top - (b) bottom - (l) left - (r) right - (m) middle - - - Horizontal text - - + + + + + Pillow horizontal text anchors + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Pillow horizontal text anchors + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + (d) descender + (s) baseline + (a) ascender + (m) middle + (t) top + (b) bottom + (l) left + (r) right + (m) middle + + + Horizontal text + + diff --git a/docs/resources/anchor_vertical.svg b/docs/resources/anchor_vertical.svg index 95da30ffd..ca14fa271 100644 --- a/docs/resources/anchor_vertical.svg +++ b/docs/resources/anchor_vertical.svg @@ -1,841 +1,841 @@ - - - - - Pillow vertical text anchors - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - Pillow vertical text anchors - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - (l)left - (s) baseline - (r)right - (t) top - (m) middle - (b) bottom - (m)middle - (l)left - (s) baseline - (r)right - (t) top - (m) middle - (b) bottom - (m)middle - - - Verticaltext - - + + + + + Pillow vertical text anchors + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Pillow vertical text anchors + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + (l)left + (s) baseline + (r)right + (t) top + (m) middle + (b) bottom + (m)middle + (l)left + (s) baseline + (r)right + (t) top + (m) middle + (b) bottom + (m)middle + + + Verticaltext + + diff --git a/docs/resources/css/dark.css b/docs/resources/css/dark.css index 1571cbc4e..c7a678fe2 100644 --- a/docs/resources/css/dark.css +++ b/docs/resources/css/dark.css @@ -1,9 +1,9 @@ -/* For black-on-white/transparent images at handbook/text-anchors.html */ -body[data-theme="dark"] #text-anchors img { - filter: invert(1) brightness(0.85) hue-rotate(-60deg); -} -@media (prefers-color-scheme: dark) { - body[data-theme="auto"] #text-anchors img { - filter: invert(1) brightness(0.85) hue-rotate(-60deg); - } -} +/* For black-on-white/transparent images at handbook/text-anchors.html */ +body[data-theme="dark"] #text-anchors img { + filter: invert(1) brightness(0.85) hue-rotate(-60deg); +} +@media (prefers-color-scheme: dark) { + body[data-theme="auto"] #text-anchors img { + filter: invert(1) brightness(0.85) hue-rotate(-60deg); + } +} diff --git a/docs/resources/js/activate_tab.js b/docs/resources/js/activate_tab.js index 92522b5ce..8e088df70 100644 --- a/docs/resources/js/activate_tab.js +++ b/docs/resources/js/activate_tab.js @@ -1,36 +1,36 @@ -// Based on https://stackoverflow.com/a/38241481/724176 -function getOS() { - const userAgent = window.navigator.userAgent, - platform = window.navigator.userAgentData?.platform || window.navigator.platform, - macosPlatforms = ["macOS", "Macintosh", "MacIntel", "MacPPC", "Mac68K"], - windowsPlatforms = ["Win32", "Win64", "Windows", "WinCE"]; - - if (macosPlatforms.includes(platform)) { - return "macOS"; - } else if (windowsPlatforms.includes(platform)) { - return "Windows"; - } else if (/Android/.test(userAgent)) { - return "Android"; - } else if (/Linux/.test(platform)) { - return "Linux"; - } -} - -function activateTab(tabName) { - // Find all label elements with the specified tab name - const labels = document.querySelectorAll(".tab-label"); - - labels.forEach((label) => { - if (label.textContent == tabName) { - // Find the associated input element using the "for" attribute - const tabInputId = label.getAttribute("for"); - const tabInput = document.getElementById(tabInputId); - - // Check if the input element exists before attempting to set the "checked" attribute - if (tabInput) { - // Activate the tab by setting its "checked" attribute to true - tabInput.checked = true; - } - } - }); -} +// Based on https://stackoverflow.com/a/38241481/724176 +function getOS() { + const userAgent = window.navigator.userAgent, + platform = window.navigator.userAgentData?.platform || window.navigator.platform, + macosPlatforms = ["macOS", "Macintosh", "MacIntel", "MacPPC", "Mac68K"], + windowsPlatforms = ["Win32", "Win64", "Windows", "WinCE"]; + + if (macosPlatforms.includes(platform)) { + return "macOS"; + } else if (windowsPlatforms.includes(platform)) { + return "Windows"; + } else if (/Android/.test(userAgent)) { + return "Android"; + } else if (/Linux/.test(platform)) { + return "Linux"; + } +} + +function activateTab(tabName) { + // Find all label elements with the specified tab name + const labels = document.querySelectorAll(".tab-label"); + + labels.forEach((label) => { + if (label.textContent == tabName) { + // Find the associated input element using the "for" attribute + const tabInputId = label.getAttribute("for"); + const tabInput = document.getElementById(tabInputId); + + // Check if the input element exists before attempting to set the "checked" attribute + if (tabInput) { + // Activate the tab by setting its "checked" attribute to true + tabInput.checked = true; + } + } + }); +} diff --git a/merged_image.png b/merged_image.png new file mode 100644 index 000000000..2302e7d35 Binary files /dev/null and b/merged_image.png differ diff --git a/pyproject.toml b/pyproject.toml index 20e87ad32..bd7b72568 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,147 +1,147 @@ -[build-system] -build-backend = "backend" -requires = [ - "setuptools>=67.8", -] -backend-path = [ - "_custom_build", -] - -[project] -name = "pillow" -description = "Python Imaging Library (Fork)" -readme = "README.md" -keywords = [ - "Imaging", -] -license = {text = "HPND"} -authors = [{name = "Jeffrey A. Clark", email = "aclark@aclark.net"}] -requires-python = ">=3.8" -classifiers = [ - "Development Status :: 6 - Mature", - "License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND)", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: Implementation :: CPython", - "Programming Language :: Python :: Implementation :: PyPy", - "Topic :: Multimedia :: Graphics", - "Topic :: Multimedia :: Graphics :: Capture :: Digital Camera", - "Topic :: Multimedia :: Graphics :: Capture :: Screen Capture", - "Topic :: Multimedia :: Graphics :: Graphics Conversion", - "Topic :: Multimedia :: Graphics :: Viewers", - "Typing :: Typed", -] -dynamic = [ - "version", -] -[project.optional-dependencies] -docs = [ - "furo", - "olefile", - "sphinx>=7.3", - "sphinx-copybutton", - "sphinx-inline-tabs", - "sphinxext-opengraph", -] -fpx = [ - "olefile", -] -mic = [ - "olefile", -] -tests = [ - "check-manifest", - "coverage", - "defusedxml", - "markdown2", - "olefile", - "packaging", - "pyroma", - "pytest", - "pytest-cov", - "pytest-timeout", -] -typing = [ - 'typing-extensions; python_version < "3.10"', -] -xmp = [ - "defusedxml", -] -[project.urls] -Changelog = "https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst" -Documentation = "https://pillow.readthedocs.io" -Funding = "https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=pypi" -Homepage = "https://python-pillow.org" -Mastodon = "https://fosstodon.org/@pillow" -"Release notes" = "https://pillow.readthedocs.io/en/stable/releasenotes/index.html" -Source = "https://github.com/python-pillow/Pillow" - -[tool.setuptools] -packages = ["PIL"] -include-package-data = true -package-dir = {"" = "src"} - -[tool.setuptools.dynamic] -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" -test-command = "cd {project} && .github/workflows/wheels-test.sh" -test-extras = "tests" - -[tool.ruff] -fix = true - -[tool.ruff.lint] -select = [ - "C4", # flake8-comprehensions - "E", # pycodestyle errors - "EM", # flake8-errmsg - "F", # pyflakes errors - "I", # isort - "ISC", # flake8-implicit-str-concat - "LOG", # flake8-logging - "PGH", # pygrep-hooks - "PYI", # flake8-pyi - "RUF100", # unused noqa (yesqa) - "UP", # pyupgrade - "W", # pycodestyle warnings - "YTT", # flake8-2020 -] -ignore = [ - "E203", # Whitespace before ':' - "E221", # Multiple spaces before operator - "E226", # Missing whitespace around arithmetic operator - "E241", # Multiple spaces after ',' - "PYI026", # flake8-pyi: typing.TypeAlias added in Python 3.10 - "PYI034", # flake8-pyi: typing.Self added in Python 3.11 -] - -[tool.ruff.lint.per-file-ignores] -"Tests/oss-fuzz/fuzz_font.py" = ["I002"] -"Tests/oss-fuzz/fuzz_pillow.py" = ["I002"] - -[tool.ruff.lint.isort] -known-first-party = ["PIL"] -required-imports = ["from __future__ import annotations"] - -[tool.pytest.ini_options] -addopts = "-ra --color=yes" -testpaths = ["Tests"] - -[tool.mypy] -python_version = "3.8" -pretty = true -disallow_any_generics = true -enable_error_code = "ignore-without-code" -extra_checks = true -follow_imports = "silent" -warn_redundant_casts = true -warn_unreachable = true -warn_unused_ignores = true +[build-system] +build-backend = "backend" +requires = [ + "setuptools>=67.8", +] +backend-path = [ + "_custom_build", +] + +[project] +name = "pillow" +description = "Python Imaging Library (Fork)" +readme = "README.md" +keywords = [ + "Imaging", +] +license = {text = "HPND"} +authors = [{name = "Jeffrey A. Clark", email = "aclark@aclark.net"}] +requires-python = ">=3.8" +classifiers = [ + "Development Status :: 6 - Mature", + "License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND)", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Topic :: Multimedia :: Graphics", + "Topic :: Multimedia :: Graphics :: Capture :: Digital Camera", + "Topic :: Multimedia :: Graphics :: Capture :: Screen Capture", + "Topic :: Multimedia :: Graphics :: Graphics Conversion", + "Topic :: Multimedia :: Graphics :: Viewers", + "Typing :: Typed", +] +dynamic = [ + "version", +] +[project.optional-dependencies] +docs = [ + "furo", + "olefile", + "sphinx>=7.3", + "sphinx-copybutton", + "sphinx-inline-tabs", + "sphinxext-opengraph", +] +fpx = [ + "olefile", +] +mic = [ + "olefile", +] +tests = [ + "check-manifest", + "coverage", + "defusedxml", + "markdown2", + "olefile", + "packaging", + "pyroma", + "pytest", + "pytest-cov", + "pytest-timeout", +] +typing = [ + 'typing-extensions; python_version < "3.10"', +] +xmp = [ + "defusedxml", +] +[project.urls] +Changelog = "https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst" +Documentation = "https://pillow.readthedocs.io" +Funding = "https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=pypi" +Homepage = "https://python-pillow.org" +Mastodon = "https://fosstodon.org/@pillow" +"Release notes" = "https://pillow.readthedocs.io/en/stable/releasenotes/index.html" +Source = "https://github.com/python-pillow/Pillow" + +[tool.setuptools] +packages = ["PIL"] +include-package-data = true +package-dir = {"" = "src"} + +[tool.setuptools.dynamic] +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" +test-command = "cd {project} && .github/workflows/wheels-test.sh" +test-extras = "tests" + +[tool.ruff] +fix = true + +[tool.ruff.lint] +select = [ + "C4", # flake8-comprehensions + "E", # pycodestyle errors + "EM", # flake8-errmsg + "F", # pyflakes errors + "I", # isort + "ISC", # flake8-implicit-str-concat + "LOG", # flake8-logging + "PGH", # pygrep-hooks + "PYI", # flake8-pyi + "RUF100", # unused noqa (yesqa) + "UP", # pyupgrade + "W", # pycodestyle warnings + "YTT", # flake8-2020 +] +ignore = [ + "E203", # Whitespace before ':' + "E221", # Multiple spaces before operator + "E226", # Missing whitespace around arithmetic operator + "E241", # Multiple spaces after ',' + "PYI026", # flake8-pyi: typing.TypeAlias added in Python 3.10 + "PYI034", # flake8-pyi: typing.Self added in Python 3.11 +] + +[tool.ruff.lint.per-file-ignores] +"Tests/oss-fuzz/fuzz_font.py" = ["I002"] +"Tests/oss-fuzz/fuzz_pillow.py" = ["I002"] + +[tool.ruff.lint.isort] +known-first-party = ["PIL"] +required-imports = ["from __future__ import annotations"] + +[tool.pytest.ini_options] +addopts = "-ra --color=yes" +testpaths = ["Tests"] + +[tool.mypy] +python_version = "3.8" +pretty = true +disallow_any_generics = true +enable_error_code = "ignore-without-code" +extra_checks = true +follow_imports = "silent" +warn_redundant_casts = true +warn_unreachable = true +warn_unused_ignores = true diff --git a/selftest.py b/selftest.py index 9e049367e..bd748ea67 100755 --- a/selftest.py +++ b/selftest.py @@ -1,173 +1,173 @@ -#!/usr/bin/env python3 -# minimal sanity check -from __future__ import annotations - -import sys - -from PIL import Image, features - -try: - Image.core.ping -except ImportError as v: - print("***", v) - sys.exit() -except AttributeError: - pass - - -def testimage() -> None: - """ - PIL lets you create in-memory images with various pixel types: - - >>> from PIL import Image, ImageDraw, ImageFilter, ImageMath - >>> im = Image.new("1", (128, 128)) # monochrome - >>> def _info(im): return im.format, im.mode, im.size - >>> _info(im) - (None, '1', (128, 128)) - >>> _info(Image.new("L", (128, 128))) # grayscale (luminance) - (None, 'L', (128, 128)) - >>> _info(Image.new("P", (128, 128))) # palette - (None, 'P', (128, 128)) - >>> _info(Image.new("RGB", (128, 128))) # truecolor - (None, 'RGB', (128, 128)) - >>> _info(Image.new("I", (128, 128))) # 32-bit integer - (None, 'I', (128, 128)) - >>> _info(Image.new("F", (128, 128))) # 32-bit floating point - (None, 'F', (128, 128)) - - Or open existing files: - - >>> with Image.open("Tests/images/hopper.gif") as im: - ... _info(im) - ('GIF', 'P', (128, 128)) - >>> _info(Image.open("Tests/images/hopper.ppm")) - ('PPM', 'RGB', (128, 128)) - >>> try: - ... _info(Image.open("Tests/images/hopper.jpg")) - ... except OSError as v: - ... print(v) - ('JPEG', 'RGB', (128, 128)) - - PIL doesn't actually load the image data until it's needed, - or you call the "load" method: - - >>> im = Image.open("Tests/images/hopper.ppm") - >>> print(im.im) # internal image attribute - None - >>> a = im.load() - >>> type(im.im) # doctest: +ELLIPSIS - <... '...ImagingCore'> - - You can apply many different operations on images. Most - operations return a new image: - - >>> im = Image.open("Tests/images/hopper.ppm") - >>> _info(im.convert("L")) - (None, 'L', (128, 128)) - >>> _info(im.copy()) - (None, 'RGB', (128, 128)) - >>> _info(im.crop((32, 32, 96, 96))) - (None, 'RGB', (64, 64)) - >>> _info(im.filter(ImageFilter.BLUR)) - (None, 'RGB', (128, 128)) - >>> im.getbands() - ('R', 'G', 'B') - >>> im.getbbox() - (0, 0, 128, 128) - >>> len(im.getdata()) - 16384 - >>> im.getextrema() - ((0, 255), (0, 255), (0, 255)) - >>> im.getpixel((0, 0)) - (20, 20, 70) - >>> len(im.getprojection()) - 2 - >>> len(im.histogram()) - 768 - >>> '%.7f' % im.entropy() - '8.8212866' - >>> _info(im.point(list(range(256))*3)) - (None, 'RGB', (128, 128)) - >>> _info(im.resize((64, 64))) - (None, 'RGB', (64, 64)) - >>> _info(im.rotate(45)) - (None, 'RGB', (128, 128)) - >>> [_info(ch) for ch in im.split()] - [(None, 'L', (128, 128)), (None, 'L', (128, 128)), (None, 'L', (128, 128))] - >>> len(im.convert("1").tobitmap()) - 10456 - >>> len(im.tobytes()) - 49152 - >>> _info(im.transform((512, 512), Image.Transform.AFFINE, (1,0,0,0,1,0))) - (None, 'RGB', (512, 512)) - >>> _info(im.transform((512, 512), Image.Transform.EXTENT, (32,32,96,96))) - (None, 'RGB', (512, 512)) - - The ImageDraw module lets you draw stuff in raster images: - - >>> im = Image.new("L", (128, 128), 64) - >>> d = ImageDraw.ImageDraw(im) - >>> d.line((0, 0, 128, 128), fill=128) - >>> d.line((0, 128, 128, 0), fill=128) - >>> im.getextrema() - (64, 128) - - In 1.1.4, you can specify colors in a number of ways: - - >>> xy = 0, 0, 128, 128 - >>> im = Image.new("RGB", (128, 128), 0) - >>> d = ImageDraw.ImageDraw(im) - >>> d.rectangle(xy, "#f00") - >>> im.getpixel((0, 0)) - (255, 0, 0) - >>> d.rectangle(xy, "#ff0000") - >>> im.getpixel((0, 0)) - (255, 0, 0) - >>> d.rectangle(xy, "rgb(255,0,0)") - >>> im.getpixel((0, 0)) - (255, 0, 0) - >>> d.rectangle(xy, "rgb(100%,0%,0%)") - >>> im.getpixel((0, 0)) - (255, 0, 0) - >>> d.rectangle(xy, "hsl(0, 100%, 50%)") - >>> im.getpixel((0, 0)) - (255, 0, 0) - >>> d.rectangle(xy, "red") - >>> im.getpixel((0, 0)) - (255, 0, 0) - - In 1.1.6, you can use the ImageMath module to do image - calculations. - - >>> im = ImageMath.lambda_eval( \ - lambda args: args["float"](args["im"] + 20), im=im.convert("L") \ - ) - >>> im.mode, im.size - ('F', (128, 128)) - - PIL can do many other things, but I'll leave that for another - day. - - Cheers /F - """ - - -if __name__ == "__main__": - # check build sanity - - exit_status = 0 - - features.pilinfo(sys.stdout, False) - - # use doctest to make sure the test program behaves as documented! - import doctest - - print("Running selftest:") - status = doctest.testmod(sys.modules[__name__]) - if status[0]: - print(f"*** {status[0]} tests of {status[1]} failed.") - exit_status = 1 - else: - print(f"--- {status[1]} tests passed.") - - sys.exit(exit_status) +#!/usr/bin/env python3 +# minimal sanity check +from __future__ import annotations + +import sys + +from PIL import Image, features + +try: + Image.core.ping +except ImportError as v: + print("***", v) + sys.exit() +except AttributeError: + pass + + +def testimage() -> None: + """ + PIL lets you create in-memory images with various pixel types: + + >>> from PIL import Image, ImageDraw, ImageFilter, ImageMath + >>> im = Image.new("1", (128, 128)) # monochrome + >>> def _info(im): return im.format, im.mode, im.size + >>> _info(im) + (None, '1', (128, 128)) + >>> _info(Image.new("L", (128, 128))) # grayscale (luminance) + (None, 'L', (128, 128)) + >>> _info(Image.new("P", (128, 128))) # palette + (None, 'P', (128, 128)) + >>> _info(Image.new("RGB", (128, 128))) # truecolor + (None, 'RGB', (128, 128)) + >>> _info(Image.new("I", (128, 128))) # 32-bit integer + (None, 'I', (128, 128)) + >>> _info(Image.new("F", (128, 128))) # 32-bit floating point + (None, 'F', (128, 128)) + + Or open existing files: + + >>> with Image.open("Tests/images/hopper.gif") as im: + ... _info(im) + ('GIF', 'P', (128, 128)) + >>> _info(Image.open("Tests/images/hopper.ppm")) + ('PPM', 'RGB', (128, 128)) + >>> try: + ... _info(Image.open("Tests/images/hopper.jpg")) + ... except OSError as v: + ... print(v) + ('JPEG', 'RGB', (128, 128)) + + PIL doesn't actually load the image data until it's needed, + or you call the "load" method: + + >>> im = Image.open("Tests/images/hopper.ppm") + >>> print(im.im) # internal image attribute + None + >>> a = im.load() + >>> type(im.im) # doctest: +ELLIPSIS + <... '...ImagingCore'> + + You can apply many different operations on images. Most + operations return a new image: + + >>> im = Image.open("Tests/images/hopper.ppm") + >>> _info(im.convert("L")) + (None, 'L', (128, 128)) + >>> _info(im.copy()) + (None, 'RGB', (128, 128)) + >>> _info(im.crop((32, 32, 96, 96))) + (None, 'RGB', (64, 64)) + >>> _info(im.filter(ImageFilter.BLUR)) + (None, 'RGB', (128, 128)) + >>> im.getbands() + ('R', 'G', 'B') + >>> im.getbbox() + (0, 0, 128, 128) + >>> len(im.getdata()) + 16384 + >>> im.getextrema() + ((0, 255), (0, 255), (0, 255)) + >>> im.getpixel((0, 0)) + (20, 20, 70) + >>> len(im.getprojection()) + 2 + >>> len(im.histogram()) + 768 + >>> '%.7f' % im.entropy() + '8.8212866' + >>> _info(im.point(list(range(256))*3)) + (None, 'RGB', (128, 128)) + >>> _info(im.resize((64, 64))) + (None, 'RGB', (64, 64)) + >>> _info(im.rotate(45)) + (None, 'RGB', (128, 128)) + >>> [_info(ch) for ch in im.split()] + [(None, 'L', (128, 128)), (None, 'L', (128, 128)), (None, 'L', (128, 128))] + >>> len(im.convert("1").tobitmap()) + 10456 + >>> len(im.tobytes()) + 49152 + >>> _info(im.transform((512, 512), Image.Transform.AFFINE, (1,0,0,0,1,0))) + (None, 'RGB', (512, 512)) + >>> _info(im.transform((512, 512), Image.Transform.EXTENT, (32,32,96,96))) + (None, 'RGB', (512, 512)) + + The ImageDraw module lets you draw stuff in raster images: + + >>> im = Image.new("L", (128, 128), 64) + >>> d = ImageDraw.ImageDraw(im) + >>> d.line((0, 0, 128, 128), fill=128) + >>> d.line((0, 128, 128, 0), fill=128) + >>> im.getextrema() + (64, 128) + + In 1.1.4, you can specify colors in a number of ways: + + >>> xy = 0, 0, 128, 128 + >>> im = Image.new("RGB", (128, 128), 0) + >>> d = ImageDraw.ImageDraw(im) + >>> d.rectangle(xy, "#f00") + >>> im.getpixel((0, 0)) + (255, 0, 0) + >>> d.rectangle(xy, "#ff0000") + >>> im.getpixel((0, 0)) + (255, 0, 0) + >>> d.rectangle(xy, "rgb(255,0,0)") + >>> im.getpixel((0, 0)) + (255, 0, 0) + >>> d.rectangle(xy, "rgb(100%,0%,0%)") + >>> im.getpixel((0, 0)) + (255, 0, 0) + >>> d.rectangle(xy, "hsl(0, 100%, 50%)") + >>> im.getpixel((0, 0)) + (255, 0, 0) + >>> d.rectangle(xy, "red") + >>> im.getpixel((0, 0)) + (255, 0, 0) + + In 1.1.6, you can use the ImageMath module to do image + calculations. + + >>> im = ImageMath.lambda_eval( \ + lambda args: args["float"](args["im"] + 20), im=im.convert("L") \ + ) + >>> im.mode, im.size + ('F', (128, 128)) + + PIL can do many other things, but I'll leave that for another + day. + + Cheers /F + """ + + +if __name__ == "__main__": + # check build sanity + + exit_status = 0 + + features.pilinfo(sys.stdout, False) + + # use doctest to make sure the test program behaves as documented! + import doctest + + print("Running selftest:") + status = doctest.testmod(sys.modules[__name__]) + if status[0]: + print(f"*** {status[0]} tests of {status[1]} failed.") + exit_status = 1 + else: + print(f"--- {status[1]} tests passed.") + + sys.exit(exit_status) diff --git a/setup.py b/setup.py index 0abfaaddc..ba34467f4 100644 --- a/setup.py +++ b/setup.py @@ -1,1037 +1,1037 @@ -# > pyroma . -# ------------------------------ -# Checking . -# Found Pillow -# ------------------------------ -# Final rating: 10/10 -# Your cheese is so fresh most people think it's a cream: Mascarpone -# ------------------------------ -from __future__ import annotations - -import os -import re -import shutil -import struct -import subprocess -import sys -import warnings - -from setuptools import Extension, setup -from setuptools.command.build_ext import build_ext - - -def get_version(): - version_file = "src/PIL/_version.py" - with open(version_file, encoding="utf-8") as f: - return f.read().split('"')[1] - - -configuration = {} - - -PILLOW_VERSION = get_version() -FREETYPE_ROOT = None -HARFBUZZ_ROOT = None -FRIBIDI_ROOT = None -IMAGEQUANT_ROOT = None -JPEG2K_ROOT = None -JPEG_ROOT = None -LCMS_ROOT = None -RAQM_ROOT = None -TIFF_ROOT = None -WEBP_ROOT = None -ZLIB_ROOT = None -FUZZING_BUILD = "LIB_FUZZING_ENGINE" in os.environ - -if sys.platform == "win32" and sys.version_info >= (3, 13): - import atexit - - atexit.register( - lambda: warnings.warn( - f"Pillow {PILLOW_VERSION} does not support Python " - f"{sys.version_info.major}.{sys.version_info.minor} and does not provide " - "prebuilt Windows binaries. We do not recommend building from source on " - "Windows.", - RuntimeWarning, - ) - ) - - -_IMAGING = ("decode", "encode", "map", "display", "outline", "path") - -_LIB_IMAGING = ( - "Access", - "AlphaComposite", - "Resample", - "Reduce", - "Bands", - "BcnDecode", - "BitDecode", - "Blend", - "Chops", - "ColorLUT", - "Convert", - "ConvertYCbCr", - "Copy", - "Crop", - "Dib", - "Draw", - "Effects", - "EpsEncode", - "File", - "Fill", - "Filter", - "FliDecode", - "Geometry", - "GetBBox", - "GifDecode", - "GifEncode", - "HexDecode", - "Histo", - "JpegDecode", - "JpegEncode", - "Matrix", - "ModeFilter", - "Negative", - "Offset", - "Pack", - "PackDecode", - "Palette", - "Paste", - "Quant", - "QuantOctree", - "QuantHash", - "QuantHeap", - "PcdDecode", - "PcxDecode", - "PcxEncode", - "Point", - "RankFilter", - "RawDecode", - "RawEncode", - "Storage", - "SgiRleDecode", - "SunRleDecode", - "TgaRleDecode", - "TgaRleEncode", - "Unpack", - "UnpackYCC", - "UnsharpMask", - "XbmDecode", - "XbmEncode", - "ZipDecode", - "ZipEncode", - "TiffDecode", - "Jpeg2KDecode", - "Jpeg2KEncode", - "BoxBlur", - "QuantPngQuant", - "codec_fd", -) - -DEBUG = False - - -class DependencyException(Exception): - pass - - -class RequiredDependencyException(Exception): - pass - - -PLATFORM_MINGW = os.name == "nt" and "GCC" in sys.version - - -def _dbg(s, tp=None): - if DEBUG: - if tp: - print(s % tp) - return - print(s) - - -def _find_library_dirs_ldconfig(): - # Based on ctypes.util from Python 2 - - ldconfig = "ldconfig" if shutil.which("ldconfig") else "/sbin/ldconfig" - if sys.platform.startswith("linux") or sys.platform.startswith("gnu"): - if struct.calcsize("l") == 4: - machine = os.uname()[4] + "-32" - else: - machine = os.uname()[4] + "-64" - mach_map = { - "x86_64-64": "libc6,x86-64", - "ppc64-64": "libc6,64bit", - "sparc64-64": "libc6,64bit", - "s390x-64": "libc6,64bit", - "ia64-64": "libc6,IA-64", - } - abi_type = mach_map.get(machine, "libc6") - - # Assuming GLIBC's ldconfig (with option -p) - # Alpine Linux uses musl that can't print cache - args = [ldconfig, "-p"] - expr = rf".*\({abi_type}.*\) => (.*)" - env = dict(os.environ) - env["LC_ALL"] = "C" - env["LANG"] = "C" - - elif sys.platform.startswith("freebsd"): - args = [ldconfig, "-r"] - expr = r".* => (.*)" - env = {} - - try: - p = subprocess.Popen( - args, stderr=subprocess.DEVNULL, stdout=subprocess.PIPE, env=env - ) - except OSError: # E.g. command not found - return [] - [data, _] = p.communicate() - if isinstance(data, bytes): - data = data.decode("latin1") - - dirs = [] - for dll in re.findall(expr, data): - dir = os.path.dirname(dll) - if dir not in dirs: - dirs.append(dir) - return dirs - - -def _add_directory(path, subdir, where=None): - if subdir is None: - return - subdir = os.path.realpath(subdir) - if os.path.isdir(subdir) and subdir not in path: - if where is None: - _dbg("Appending path %s", subdir) - path.append(subdir) - else: - _dbg("Inserting path %s", subdir) - path.insert(where, subdir) - elif subdir in path and where is not None: - path.remove(subdir) - path.insert(where, subdir) - - -def _find_include_file(self, include): - for directory in self.compiler.include_dirs: - _dbg("Checking for include file %s in %s", (include, directory)) - if os.path.isfile(os.path.join(directory, include)): - _dbg("Found %s", include) - return 1 - return 0 - - -def _find_library_file(self, library): - ret = self.compiler.find_library_file(self.compiler.library_dirs, library) - if ret: - _dbg("Found library %s at %s", (library, ret)) - else: - _dbg("Couldn't find library %s in %s", (library, self.compiler.library_dirs)) - return ret - - -def _find_include_dir(self, dirname, include): - for directory in self.compiler.include_dirs: - _dbg("Checking for include file %s in %s", (include, directory)) - if os.path.isfile(os.path.join(directory, include)): - _dbg("Found %s in %s", (include, directory)) - return True - subdir = os.path.join(directory, dirname) - _dbg("Checking for include file %s in %s", (include, subdir)) - if os.path.isfile(os.path.join(subdir, include)): - _dbg("Found %s in %s", (include, subdir)) - return subdir - - -def _cmd_exists(cmd: str) -> bool: - if "PATH" not in os.environ: - return False - return any( - os.access(os.path.join(path, cmd), os.X_OK) - for path in os.environ["PATH"].split(os.pathsep) - ) - - -def _pkg_config(name): - command = os.environ.get("PKG_CONFIG", "pkg-config") - for keep_system in (True, False): - try: - command_libs = [command, "--libs-only-L", name] - command_cflags = [command, "--cflags-only-I", name] - stderr = None - if keep_system: - command_libs.append("--keep-system-libs") - command_cflags.append("--keep-system-cflags") - stderr = subprocess.DEVNULL - if not DEBUG: - command_libs.append("--silence-errors") - command_cflags.append("--silence-errors") - libs = re.split( - r"(^|\s+)-L", - subprocess.check_output(command_libs, stderr=stderr) - .decode("utf8") - .strip(), - )[::2][1:] - cflags = re.split( - r"(^|\s+)-I", - subprocess.check_output(command_cflags).decode("utf8").strip(), - )[::2][1:] - return libs, cflags - except Exception: - pass - - -class pil_build_ext(build_ext): - class feature: - features = [ - "zlib", - "jpeg", - "tiff", - "freetype", - "raqm", - "lcms", - "webp", - "webpmux", - "jpeg2000", - "imagequant", - "xcb", - ] - - required = {"jpeg", "zlib"} - vendor = set() - - def __init__(self): - for f in self.features: - setattr(self, f, None) - - def require(self, feat): - return feat in self.required - - def want(self, feat): - return getattr(self, feat) is None - - def want_vendor(self, feat): - return feat in self.vendor - - def __iter__(self): - yield from self.features - - feature = feature() - - user_options = ( - build_ext.user_options - + [(f"disable-{x}", None, f"Disable support for {x}") for x in feature] - + [(f"enable-{x}", None, f"Enable support for {x}") for x in feature] - + [ - (f"vendor-{x}", None, f"Use vendored version of {x}") - for x in ("raqm", "fribidi") - ] - + [ - ("disable-platform-guessing", None, "Disable platform guessing on Linux"), - ("debug", None, "Debug logging"), - ] - + [("add-imaging-libs=", None, "Add libs to _imaging build")] - ) - - @staticmethod - def check_configuration(option, value): - return True if value in configuration.get(option, []) else None - - def initialize_options(self): - self.disable_platform_guessing = self.check_configuration( - "platform-guessing", "disable" - ) - self.add_imaging_libs = "" - build_ext.initialize_options(self) - for x in self.feature: - setattr(self, f"disable_{x}", self.check_configuration(x, "disable")) - setattr(self, f"enable_{x}", self.check_configuration(x, "enable")) - for x in ("raqm", "fribidi"): - setattr(self, f"vendor_{x}", self.check_configuration(x, "vendor")) - if self.check_configuration("debug", "true"): - self.debug = True - self.parallel = configuration.get("parallel", [None])[-1] - - def finalize_options(self): - build_ext.finalize_options(self) - if self.debug: - global DEBUG - DEBUG = True - if not self.parallel: - # If --parallel (or -j) wasn't specified, we want to reproduce the same - # behavior as before, that is, auto-detect the number of jobs. - try: - self.parallel = int( - os.environ.get("MAX_CONCURRENCY", min(4, os.cpu_count())) - ) - except TypeError: - self.parallel = None - for x in self.feature: - if getattr(self, f"disable_{x}"): - setattr(self.feature, 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}" - raise ValueError(msg) - if x == "freetype": - _dbg("--disable-freetype implies --disable-raqm") - if getattr(self, "enable_raqm"): - msg = ( - "Conflicting options: --enable-raqm and --disable-freetype" - ) - raise ValueError(msg) - setattr(self, "disable_raqm", True) - if getattr(self, f"enable_{x}"): - _dbg("Requiring %s", x) - self.feature.required.add(x) - if x == "raqm": - _dbg("--enable-raqm implies --enable-freetype") - self.feature.required.add("freetype") - for x in ("raqm", "fribidi"): - if getattr(self, f"vendor_{x}"): - if getattr(self, "disable_raqm"): - msg = f"Conflicting options: --vendor-{x} and --disable-raqm" - raise ValueError(msg) - if x == "fribidi" and not getattr(self, "vendor_raqm"): - msg = f"Conflicting options: --vendor-{x} and not --vendor-raqm" - raise ValueError(msg) - _dbg("Using vendored version of %s", x) - self.feature.vendor.add(x) - - def _update_extension(self, name, libraries, define_macros=None, sources=None): - for extension in self.extensions: - if extension.name == name: - extension.libraries += libraries - if define_macros is not None: - extension.define_macros += define_macros - if sources is not None: - extension.sources += sources - if FUZZING_BUILD: - extension.language = "c++" - extension.extra_link_args = ["--stdlib=libc++"] - break - - def _remove_extension(self, name): - for extension in self.extensions: - if extension.name == name: - self.extensions.remove(extension) - break - - def get_macos_sdk_path(self): - try: - sdk_path = ( - subprocess.check_output(["xcrun", "--show-sdk-path"]) - .strip() - .decode("latin1") - ) - except Exception: - sdk_path = None - if ( - not sdk_path - or sdk_path == "/Applications/Xcode.app/Contents/Developer" - "/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk" - ): - commandlinetools_sdk_path = ( - "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk" - ) - if os.path.exists(commandlinetools_sdk_path): - sdk_path = commandlinetools_sdk_path - return sdk_path - - def build_extensions(self): - library_dirs = [] - include_dirs = [] - - pkg_config = None - if _cmd_exists(os.environ.get("PKG_CONFIG", "pkg-config")): - pkg_config = _pkg_config - - # - # add configured kits - for root_name, lib_name in { - "JPEG_ROOT": "libjpeg", - "JPEG2K_ROOT": "libopenjp2", - "TIFF_ROOT": ("libtiff-5", "libtiff-4"), - "ZLIB_ROOT": "zlib", - "FREETYPE_ROOT": "freetype2", - "HARFBUZZ_ROOT": "harfbuzz", - "FRIBIDI_ROOT": "fribidi", - "RAQM_ROOT": "raqm", - "WEBP_ROOT": "libwebp", - "LCMS_ROOT": "lcms2", - "IMAGEQUANT_ROOT": "libimagequant", - }.items(): - root = globals()[root_name] - - if root is None and root_name in os.environ: - prefix = os.environ[root_name] - root = (os.path.join(prefix, "lib"), os.path.join(prefix, "include")) - - if root is None and pkg_config: - if isinstance(lib_name, tuple): - for lib_name2 in lib_name: - _dbg(f"Looking for `{lib_name2}` using pkg-config.") - root = pkg_config(lib_name2) - if root: - break - else: - _dbg(f"Looking for `{lib_name}` using pkg-config.") - root = pkg_config(lib_name) - - if isinstance(root, tuple): - lib_root, include_root = root - else: - lib_root = include_root = root - - if lib_root is not None: - if not isinstance(lib_root, (tuple, list)): - lib_root = (lib_root,) - for lib_dir in lib_root: - _add_directory(library_dirs, lib_dir) - if include_root is not None: - if not isinstance(include_root, (tuple, list)): - include_root = (include_root,) - for include_dir in include_root: - _add_directory(include_dirs, include_dir) - - # respect CFLAGS/CPPFLAGS/LDFLAGS - for k in ("CFLAGS", "CPPFLAGS", "LDFLAGS"): - if k in os.environ: - for match in re.finditer(r"-I([^\s]+)", os.environ[k]): - _add_directory(include_dirs, match.group(1)) - for match in re.finditer(r"-L([^\s]+)", os.environ[k]): - _add_directory(library_dirs, match.group(1)) - - # include, rpath, if set as environment variables: - for k in ("C_INCLUDE_PATH", "CPATH", "INCLUDE"): - if k in os.environ: - for d in os.environ[k].split(os.path.pathsep): - _add_directory(include_dirs, d) - - for k in ("LD_RUN_PATH", "LIBRARY_PATH", "LIB"): - if k in os.environ: - for d in os.environ[k].split(os.path.pathsep): - _add_directory(library_dirs, d) - - _add_directory(library_dirs, os.path.join(sys.prefix, "lib")) - _add_directory(include_dirs, os.path.join(sys.prefix, "include")) - - # - # add platform directories - - if self.disable_platform_guessing: - pass - - elif sys.platform == "cygwin": - # pythonX.Y.dll.a is in the /usr/lib/pythonX.Y/config directory - self.compiler.shared_lib_extension = ".dll.a" - _add_directory( - library_dirs, - os.path.join( - "/usr/lib", "python{}.{}".format(*sys.version_info), "config" - ), - ) - - elif sys.platform == "darwin": - # attempt to make sure we pick freetype2 over other versions - _add_directory(include_dirs, "/sw/include/freetype2") - _add_directory(include_dirs, "/sw/lib/freetype2/include") - # fink installation directories - _add_directory(library_dirs, "/sw/lib") - _add_directory(include_dirs, "/sw/include") - # darwin ports installation directories - _add_directory(library_dirs, "/opt/local/lib") - _add_directory(include_dirs, "/opt/local/include") - - # if Homebrew is installed, use its lib and include directories - try: - prefix = ( - subprocess.check_output(["brew", "--prefix"]) - .strip() - .decode("latin1") - ) - except Exception: - # Homebrew not installed - prefix = None - - ft_prefix = None - - if prefix: - # add Homebrew's include and lib directories - _add_directory(library_dirs, os.path.join(prefix, "lib")) - _add_directory(include_dirs, os.path.join(prefix, "include")) - _add_directory( - include_dirs, os.path.join(prefix, "opt", "zlib", "include") - ) - ft_prefix = os.path.join(prefix, "opt", "freetype") - - if ft_prefix and os.path.isdir(ft_prefix): - # freetype might not be linked into Homebrew's prefix - _add_directory(library_dirs, os.path.join(ft_prefix, "lib")) - _add_directory(include_dirs, os.path.join(ft_prefix, "include")) - else: - # fall back to freetype from XQuartz if - # Homebrew's freetype is missing - _add_directory(library_dirs, "/usr/X11/lib") - _add_directory(include_dirs, "/usr/X11/include") - - sdk_path = self.get_macos_sdk_path() - if sdk_path: - _add_directory(library_dirs, os.path.join(sdk_path, "usr", "lib")) - _add_directory(include_dirs, os.path.join(sdk_path, "usr", "include")) - - 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") - ): - for dirname in _find_library_dirs_ldconfig(): - _add_directory(library_dirs, dirname) - if sys.platform.startswith("linux") and os.environ.get("ANDROID_ROOT"): - # termux support for android. - # system libraries (zlib) are installed in /system/lib - # headers are at $PREFIX/include - # user libs are at $PREFIX/lib - _add_directory( - library_dirs, - os.path.join( - os.environ["ANDROID_ROOT"], - "lib" if struct.calcsize("l") == 4 else "lib64", - ), - ) - - elif sys.platform.startswith("netbsd"): - _add_directory(library_dirs, "/usr/pkg/lib") - _add_directory(include_dirs, "/usr/pkg/include") - - elif sys.platform.startswith("sunos5"): - _add_directory(library_dirs, "/opt/local/lib") - _add_directory(include_dirs, "/opt/local/include") - - # FIXME: check /opt/stuff directories here? - - # standard locations - if not self.disable_platform_guessing: - _add_directory(library_dirs, "/usr/local/lib") - _add_directory(include_dirs, "/usr/local/include") - - _add_directory(library_dirs, "/usr/lib") - _add_directory(include_dirs, "/usr/include") - # alpine, at least - _add_directory(library_dirs, "/lib") - - if sys.platform == "win32": - # on Windows, look for the OpenJPEG libraries in the location that - # the official installer puts them - program_files = os.environ.get("ProgramFiles", "") - best_version = (0, 0) - best_path = None - for name in os.listdir(program_files): - if name.startswith("OpenJPEG "): - version = tuple(int(x) for x in name[9:].strip().split(".")) - if version > best_version: - best_version = version - best_path = os.path.join(program_files, name) - - if best_path: - _dbg("Adding %s to search list", best_path) - _add_directory(library_dirs, os.path.join(best_path, "lib")) - _add_directory(include_dirs, os.path.join(best_path, "include")) - - # - # insert new dirs *before* default libs, to avoid conflicts - # between Python PYD stub libs and real libraries - - self.compiler.library_dirs = library_dirs + self.compiler.library_dirs - self.compiler.include_dirs = include_dirs + self.compiler.include_dirs - - # - # look for available libraries - - feature = self.feature - - if feature.want("zlib"): - _dbg("Looking for zlib") - if _find_include_file(self, "zlib.h"): - if _find_library_file(self, "z"): - feature.zlib = "z" - elif sys.platform == "win32" and _find_library_file(self, "zlib"): - feature.zlib = "zlib" # alternative name - - if feature.want("jpeg"): - _dbg("Looking for jpeg") - if _find_include_file(self, "jpeglib.h"): - if _find_library_file(self, "jpeg"): - feature.jpeg = "jpeg" - elif sys.platform == "win32" and _find_library_file(self, "libjpeg"): - feature.jpeg = "libjpeg" # alternative name - - feature.openjpeg_version = None - if feature.want("jpeg2000"): - _dbg("Looking for jpeg2000") - best_version = None - best_path = None - - # Find the best version - for directory in self.compiler.include_dirs: - _dbg("Checking for openjpeg-#.# in %s", directory) - try: - listdir = os.listdir(directory) - except Exception: - # OSError, FileNotFoundError - continue - for name in listdir: - if name.startswith("openjpeg-") and os.path.isfile( - os.path.join(directory, name, "openjpeg.h") - ): - _dbg("Found openjpeg.h in %s/%s", (directory, name)) - version = tuple(int(x) for x in name[9:].split(".")) - if best_version is None or version > best_version: - best_version = version - best_path = os.path.join(directory, name) - _dbg( - "Best openjpeg version %s so far in %s", - (best_version, best_path), - ) - - if best_version and _find_library_file(self, "openjp2"): - # Add the directory to the include path so we can include - # rather than having to cope with the versioned - # include path - _add_directory(self.compiler.include_dirs, best_path, 0) - feature.jpeg2000 = "openjp2" - feature.openjpeg_version = ".".join(str(x) for x in best_version) - - if feature.want("imagequant"): - _dbg("Looking for imagequant") - if _find_include_file(self, "libimagequant.h"): - if _find_library_file(self, "imagequant"): - feature.imagequant = "imagequant" - elif _find_library_file(self, "libimagequant"): - feature.imagequant = "libimagequant" - - if feature.want("tiff"): - _dbg("Looking for tiff") - if _find_include_file(self, "tiff.h"): - if _find_library_file(self, "tiff"): - feature.tiff = "tiff" - if sys.platform in ["win32", "darwin"] and _find_library_file( - self, "libtiff" - ): - feature.tiff = "libtiff" - - if feature.want("freetype"): - _dbg("Looking for freetype") - if _find_library_file(self, "freetype"): - # look for freetype2 include files - freetype_version = 0 - for subdir in self.compiler.include_dirs: - _dbg("Checking for include file %s in %s", ("ft2build.h", subdir)) - if os.path.isfile(os.path.join(subdir, "ft2build.h")): - _dbg("Found %s in %s", ("ft2build.h", subdir)) - freetype_version = 21 - subdir = os.path.join(subdir, "freetype2") - break - subdir = os.path.join(subdir, "freetype2") - _dbg("Checking for include file %s in %s", ("ft2build.h", subdir)) - if os.path.isfile(os.path.join(subdir, "ft2build.h")): - _dbg("Found %s in %s", ("ft2build.h", subdir)) - freetype_version = 21 - break - if freetype_version: - feature.freetype = "freetype" - if subdir: - _add_directory(self.compiler.include_dirs, subdir, 0) - - if feature.freetype and feature.want("raqm"): - if not feature.want_vendor("raqm"): # want system Raqm - _dbg("Looking for Raqm") - if _find_include_file(self, "raqm.h"): - if _find_library_file(self, "raqm"): - feature.raqm = "raqm" - elif _find_library_file(self, "libraqm"): - feature.raqm = "libraqm" - else: # want to build Raqm from src/thirdparty - _dbg("Looking for HarfBuzz") - feature.harfbuzz = None - hb_dir = _find_include_dir(self, "harfbuzz", "hb.h") - if hb_dir: - if isinstance(hb_dir, str): - _add_directory(self.compiler.include_dirs, hb_dir, 0) - if _find_library_file(self, "harfbuzz"): - feature.harfbuzz = "harfbuzz" - if feature.harfbuzz: - if not feature.want_vendor("fribidi"): # want system FriBiDi - _dbg("Looking for FriBiDi") - feature.fribidi = None - fribidi_dir = _find_include_dir(self, "fribidi", "fribidi.h") - if fribidi_dir: - if isinstance(fribidi_dir, str): - _add_directory( - self.compiler.include_dirs, fribidi_dir, 0 - ) - if _find_library_file(self, "fribidi"): - feature.fribidi = "fribidi" - feature.raqm = True - else: # want to build FriBiDi shim from src/thirdparty - feature.raqm = True - - if feature.want("lcms"): - _dbg("Looking for lcms") - if _find_include_file(self, "lcms2.h"): - if _find_library_file(self, "lcms2"): - feature.lcms = "lcms2" - elif _find_library_file(self, "lcms2_static"): - # alternate Windows name. - feature.lcms = "lcms2_static" - - if feature.want("webp"): - _dbg("Looking for webp") - if _find_include_file(self, "webp/encode.h") and _find_include_file( - self, "webp/decode.h" - ): - # In Google's precompiled zip it is call "libwebp": - if _find_library_file(self, "webp"): - feature.webp = "webp" - elif _find_library_file(self, "libwebp"): - feature.webp = "libwebp" - - if feature.want("webpmux"): - _dbg("Looking for webpmux") - if _find_include_file(self, "webp/mux.h") and _find_include_file( - self, "webp/demux.h" - ): - if _find_library_file(self, "webpmux") and _find_library_file( - self, "webpdemux" - ): - feature.webpmux = "webpmux" - if _find_library_file(self, "libwebpmux") and _find_library_file( - self, "libwebpdemux" - ): - feature.webpmux = "libwebpmux" - - if feature.want("xcb"): - _dbg("Looking for xcb") - if _find_include_file(self, "xcb/xcb.h"): - if _find_library_file(self, "xcb"): - feature.xcb = "xcb" - - for f in feature: - if not getattr(feature, f) and feature.require(f): - if f in ("jpeg", "zlib"): - raise RequiredDependencyException(f) - raise DependencyException(f) - - # - # core library - - libs = self.add_imaging_libs.split() - defs = [] - if feature.tiff: - libs.append(feature.tiff) - defs.append(("HAVE_LIBTIFF", None)) - if sys.platform == "win32": - # This define needs to be defined if-and-only-if it was defined - # when compiling LibTIFF. LibTIFF doesn't expose it in `tiffconf.h`, - # so we have to guess; by default it is defined in all Windows builds. - # See #4237, #5243, #5359 for more information. - defs.append(("USE_WIN32_FILEIO", None)) - if feature.jpeg: - libs.append(feature.jpeg) - defs.append(("HAVE_LIBJPEG", None)) - if feature.jpeg2000: - libs.append(feature.jpeg2000) - defs.append(("HAVE_OPENJPEG", None)) - if sys.platform == "win32" and not PLATFORM_MINGW: - defs.append(("OPJ_STATIC", None)) - if feature.zlib: - libs.append(feature.zlib) - defs.append(("HAVE_LIBZ", None)) - if feature.imagequant: - libs.append(feature.imagequant) - defs.append(("HAVE_LIBIMAGEQUANT", None)) - if feature.xcb: - libs.append(feature.xcb) - defs.append(("HAVE_XCB", None)) - if sys.platform == "win32": - libs.extend(["kernel32", "user32", "gdi32"]) - if struct.unpack("h", b"\0\1")[0] == 1: - defs.append(("WORDS_BIGENDIAN", None)) - - defs.append(("PILLOW_VERSION", f'"{PILLOW_VERSION}"')) - - self._update_extension("PIL._imaging", libs, defs) - - # - # additional libraries - - if feature.freetype: - srcs = [] - libs = ["freetype"] - defs = [] - if feature.raqm: - if not feature.want_vendor("raqm"): # using system Raqm - defs.append(("HAVE_RAQM", None)) - defs.append(("HAVE_RAQM_SYSTEM", None)) - libs.append(feature.raqm) - else: # building Raqm from src/thirdparty - defs.append(("HAVE_RAQM", None)) - srcs.append("src/thirdparty/raqm/raqm.c") - libs.append(feature.harfbuzz) - if not feature.want_vendor("fribidi"): # using system FriBiDi - defs.append(("HAVE_FRIBIDI_SYSTEM", None)) - libs.append(feature.fribidi) - else: # building FriBiDi shim from src/thirdparty - srcs.append("src/thirdparty/fribidi-shim/fribidi.c") - self._update_extension("PIL._imagingft", libs, defs, srcs) - - else: - self._remove_extension("PIL._imagingft") - - if feature.lcms: - extra = [] - if sys.platform == "win32": - extra.extend(["user32", "gdi32"]) - self._update_extension("PIL._imagingcms", [feature.lcms] + extra) - else: - self._remove_extension("PIL._imagingcms") - - if feature.webp: - libs = [feature.webp] - defs = [] - - if feature.webpmux: - defs.append(("HAVE_WEBPMUX", None)) - libs.append(feature.webpmux) - libs.append(feature.webpmux.replace("pmux", "pdemux")) - - self._update_extension("PIL._webp", libs, defs) - else: - self._remove_extension("PIL._webp") - - tk_libs = ["psapi"] if sys.platform in ("win32", "cygwin") else [] - self._update_extension("PIL._imagingtk", tk_libs) - - build_ext.build_extensions(self) - - # - # sanity checks - - self.summary_report(feature) - - def summary_report(self, feature): - print("-" * 68) - print("PIL SETUP SUMMARY") - print("-" * 68) - print(f"version Pillow {PILLOW_VERSION}") - v = sys.version.split("[") - print(f"platform {sys.platform} {v[0].strip()}") - for v in v[1:]: - print(f" [{v.strip()}") - print("-" * 68) - - raqm_extra_info = "" - if feature.want_vendor("raqm"): - raqm_extra_info += "bundled" - if feature.want_vendor("fribidi"): - raqm_extra_info += ", FriBiDi shim" - - options = [ - (feature.jpeg, "JPEG"), - (feature.jpeg2000, "OPENJPEG (JPEG2000)", feature.openjpeg_version), - (feature.zlib, "ZLIB (PNG/ZIP)"), - (feature.imagequant, "LIBIMAGEQUANT"), - (feature.tiff, "LIBTIFF"), - (feature.freetype, "FREETYPE2"), - (feature.raqm, "RAQM (Text shaping)", raqm_extra_info), - (feature.lcms, "LITTLECMS2"), - (feature.webp, "WEBP"), - (feature.webpmux, "WEBPMUX"), - (feature.xcb, "XCB (X protocol)"), - ] - - all = 1 - for option in options: - if option[0]: - extra_info = "" - if len(option) >= 3 and option[2]: - extra_info = f" ({option[2]})" - print(f"--- {option[1]} support available{extra_info}") - else: - print(f"*** {option[1]} support not available") - all = 0 - - print("-" * 68) - - if not all: - print("To add a missing option, make sure you have the required") - print("library and headers.") - print( - "See https://pillow.readthedocs.io/en/latest/installation." - "html#building-from-source" - ) - print("") - - print("To check the build, run the selftest.py script.") - print("") - - -def debug_build(): - return hasattr(sys, "gettotalrefcount") or FUZZING_BUILD - - -files = ["src/_imaging.c"] -for src_file in _IMAGING: - files.append("src/" + src_file + ".c") -for src_file in _LIB_IMAGING: - files.append(os.path.join("src/libImaging", src_file + ".c")) -ext_modules = [ - Extension("PIL._imaging", files), - Extension("PIL._imagingft", ["src/_imagingft.c"]), - Extension("PIL._imagingcms", ["src/_imagingcms.c"]), - Extension("PIL._webp", ["src/_webp.c"]), - Extension("PIL._imagingtk", ["src/_imagingtk.c", "src/Tk/tkImaging.c"]), - Extension("PIL._imagingmath", ["src/_imagingmath.c"]), - Extension("PIL._imagingmorph", ["src/_imagingmorph.c"]), -] - - -# parse configuration from _custom_build/backend.py -while sys.argv[-1].startswith("--pillow-configuration="): - _, key, value = sys.argv.pop().split("=", 2) - configuration.setdefault(key, []).append(value) - -try: - setup( - cmdclass={"build_ext": pil_build_ext}, - ext_modules=ext_modules, - zip_safe=not (debug_build() or PLATFORM_MINGW), - ) -except RequiredDependencyException as err: - msg = f""" - -The headers or library files could not be found for {str(err)}, -a required dependency when compiling Pillow from source. - -Please see the install instructions at: - https://pillow.readthedocs.io/en/latest/installation/basic-installation.html - -""" - sys.stderr.write(msg) - raise RequiredDependencyException(msg) -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)} - -""" - sys.stderr.write(msg) - raise DependencyException(msg) +# > pyroma . +# ------------------------------ +# Checking . +# Found Pillow +# ------------------------------ +# Final rating: 10/10 +# Your cheese is so fresh most people think it's a cream: Mascarpone +# ------------------------------ +from __future__ import annotations + +import os +import re +import shutil +import struct +import subprocess +import sys +import warnings + +from setuptools import Extension, setup +from setuptools.command.build_ext import build_ext + + +def get_version(): + version_file = "src/PIL/_version.py" + with open(version_file, encoding="utf-8") as f: + return f.read().split('"')[1] + + +configuration = {} + + +PILLOW_VERSION = get_version() +FREETYPE_ROOT = None +HARFBUZZ_ROOT = None +FRIBIDI_ROOT = None +IMAGEQUANT_ROOT = None +JPEG2K_ROOT = None +JPEG_ROOT = None +LCMS_ROOT = None +RAQM_ROOT = None +TIFF_ROOT = None +WEBP_ROOT = None +ZLIB_ROOT = None +FUZZING_BUILD = "LIB_FUZZING_ENGINE" in os.environ + +if sys.platform == "win32" and sys.version_info >= (3, 13): + import atexit + + atexit.register( + lambda: warnings.warn( + f"Pillow {PILLOW_VERSION} does not support Python " + f"{sys.version_info.major}.{sys.version_info.minor} and does not provide " + "prebuilt Windows binaries. We do not recommend building from source on " + "Windows.", + RuntimeWarning, + ) + ) + + +_IMAGING = ("decode", "encode", "map", "display", "outline", "path") + +_LIB_IMAGING = ( + "Access", + "AlphaComposite", + "Resample", + "Reduce", + "Bands", + "BcnDecode", + "BitDecode", + "Blend", + "Chops", + "ColorLUT", + "Convert", + "ConvertYCbCr", + "Copy", + "Crop", + "Dib", + "Draw", + "Effects", + "EpsEncode", + "File", + "Fill", + "Filter", + "FliDecode", + "Geometry", + "GetBBox", + "GifDecode", + "GifEncode", + "HexDecode", + "Histo", + "JpegDecode", + "JpegEncode", + "Matrix", + "ModeFilter", + "Negative", + "Offset", + "Pack", + "PackDecode", + "Palette", + "Paste", + "Quant", + "QuantOctree", + "QuantHash", + "QuantHeap", + "PcdDecode", + "PcxDecode", + "PcxEncode", + "Point", + "RankFilter", + "RawDecode", + "RawEncode", + "Storage", + "SgiRleDecode", + "SunRleDecode", + "TgaRleDecode", + "TgaRleEncode", + "Unpack", + "UnpackYCC", + "UnsharpMask", + "XbmDecode", + "XbmEncode", + "ZipDecode", + "ZipEncode", + "TiffDecode", + "Jpeg2KDecode", + "Jpeg2KEncode", + "BoxBlur", + "QuantPngQuant", + "codec_fd", +) + +DEBUG = False + + +class DependencyException(Exception): + pass + + +class RequiredDependencyException(Exception): + pass + + +PLATFORM_MINGW = os.name == "nt" and "GCC" in sys.version + + +def _dbg(s, tp=None): + if DEBUG: + if tp: + print(s % tp) + return + print(s) + + +def _find_library_dirs_ldconfig(): + # Based on ctypes.util from Python 2 + + ldconfig = "ldconfig" if shutil.which("ldconfig") else "/sbin/ldconfig" + if sys.platform.startswith("linux") or sys.platform.startswith("gnu"): + if struct.calcsize("l") == 4: + machine = os.uname()[4] + "-32" + else: + machine = os.uname()[4] + "-64" + mach_map = { + "x86_64-64": "libc6,x86-64", + "ppc64-64": "libc6,64bit", + "sparc64-64": "libc6,64bit", + "s390x-64": "libc6,64bit", + "ia64-64": "libc6,IA-64", + } + abi_type = mach_map.get(machine, "libc6") + + # Assuming GLIBC's ldconfig (with option -p) + # Alpine Linux uses musl that can't print cache + args = [ldconfig, "-p"] + expr = rf".*\({abi_type}.*\) => (.*)" + env = dict(os.environ) + env["LC_ALL"] = "C" + env["LANG"] = "C" + + elif sys.platform.startswith("freebsd"): + args = [ldconfig, "-r"] + expr = r".* => (.*)" + env = {} + + try: + p = subprocess.Popen( + args, stderr=subprocess.DEVNULL, stdout=subprocess.PIPE, env=env + ) + except OSError: # E.g. command not found + return [] + [data, _] = p.communicate() + if isinstance(data, bytes): + data = data.decode("latin1") + + dirs = [] + for dll in re.findall(expr, data): + dir = os.path.dirname(dll) + if dir not in dirs: + dirs.append(dir) + return dirs + + +def _add_directory(path, subdir, where=None): + if subdir is None: + return + subdir = os.path.realpath(subdir) + if os.path.isdir(subdir) and subdir not in path: + if where is None: + _dbg("Appending path %s", subdir) + path.append(subdir) + else: + _dbg("Inserting path %s", subdir) + path.insert(where, subdir) + elif subdir in path and where is not None: + path.remove(subdir) + path.insert(where, subdir) + + +def _find_include_file(self, include): + for directory in self.compiler.include_dirs: + _dbg("Checking for include file %s in %s", (include, directory)) + if os.path.isfile(os.path.join(directory, include)): + _dbg("Found %s", include) + return 1 + return 0 + + +def _find_library_file(self, library): + ret = self.compiler.find_library_file(self.compiler.library_dirs, library) + if ret: + _dbg("Found library %s at %s", (library, ret)) + else: + _dbg("Couldn't find library %s in %s", (library, self.compiler.library_dirs)) + return ret + + +def _find_include_dir(self, dirname, include): + for directory in self.compiler.include_dirs: + _dbg("Checking for include file %s in %s", (include, directory)) + if os.path.isfile(os.path.join(directory, include)): + _dbg("Found %s in %s", (include, directory)) + return True + subdir = os.path.join(directory, dirname) + _dbg("Checking for include file %s in %s", (include, subdir)) + if os.path.isfile(os.path.join(subdir, include)): + _dbg("Found %s in %s", (include, subdir)) + return subdir + + +def _cmd_exists(cmd: str) -> bool: + if "PATH" not in os.environ: + return False + return any( + os.access(os.path.join(path, cmd), os.X_OK) + for path in os.environ["PATH"].split(os.pathsep) + ) + + +def _pkg_config(name): + command = os.environ.get("PKG_CONFIG", "pkg-config") + for keep_system in (True, False): + try: + command_libs = [command, "--libs-only-L", name] + command_cflags = [command, "--cflags-only-I", name] + stderr = None + if keep_system: + command_libs.append("--keep-system-libs") + command_cflags.append("--keep-system-cflags") + stderr = subprocess.DEVNULL + if not DEBUG: + command_libs.append("--silence-errors") + command_cflags.append("--silence-errors") + libs = re.split( + r"(^|\s+)-L", + subprocess.check_output(command_libs, stderr=stderr) + .decode("utf8") + .strip(), + )[::2][1:] + cflags = re.split( + r"(^|\s+)-I", + subprocess.check_output(command_cflags).decode("utf8").strip(), + )[::2][1:] + return libs, cflags + except Exception: + pass + + +class pil_build_ext(build_ext): + class feature: + features = [ + "zlib", + "jpeg", + "tiff", + "freetype", + "raqm", + "lcms", + "webp", + "webpmux", + "jpeg2000", + "imagequant", + "xcb", + ] + + required = {"jpeg", "zlib"} + vendor = set() + + def __init__(self): + for f in self.features: + setattr(self, f, None) + + def require(self, feat): + return feat in self.required + + def want(self, feat): + return getattr(self, feat) is None + + def want_vendor(self, feat): + return feat in self.vendor + + def __iter__(self): + yield from self.features + + feature = feature() + + user_options = ( + build_ext.user_options + + [(f"disable-{x}", None, f"Disable support for {x}") for x in feature] + + [(f"enable-{x}", None, f"Enable support for {x}") for x in feature] + + [ + (f"vendor-{x}", None, f"Use vendored version of {x}") + for x in ("raqm", "fribidi") + ] + + [ + ("disable-platform-guessing", None, "Disable platform guessing on Linux"), + ("debug", None, "Debug logging"), + ] + + [("add-imaging-libs=", None, "Add libs to _imaging build")] + ) + + @staticmethod + def check_configuration(option, value): + return True if value in configuration.get(option, []) else None + + def initialize_options(self): + self.disable_platform_guessing = self.check_configuration( + "platform-guessing", "disable" + ) + self.add_imaging_libs = "" + build_ext.initialize_options(self) + for x in self.feature: + setattr(self, f"disable_{x}", self.check_configuration(x, "disable")) + setattr(self, f"enable_{x}", self.check_configuration(x, "enable")) + for x in ("raqm", "fribidi"): + setattr(self, f"vendor_{x}", self.check_configuration(x, "vendor")) + if self.check_configuration("debug", "true"): + self.debug = True + self.parallel = configuration.get("parallel", [None])[-1] + + def finalize_options(self): + build_ext.finalize_options(self) + if self.debug: + global DEBUG + DEBUG = True + if not self.parallel: + # If --parallel (or -j) wasn't specified, we want to reproduce the same + # behavior as before, that is, auto-detect the number of jobs. + try: + self.parallel = int( + os.environ.get("MAX_CONCURRENCY", min(4, os.cpu_count())) + ) + except TypeError: + self.parallel = None + for x in self.feature: + if getattr(self, f"disable_{x}"): + setattr(self.feature, 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}" + raise ValueError(msg) + if x == "freetype": + _dbg("--disable-freetype implies --disable-raqm") + if getattr(self, "enable_raqm"): + msg = ( + "Conflicting options: --enable-raqm and --disable-freetype" + ) + raise ValueError(msg) + setattr(self, "disable_raqm", True) + if getattr(self, f"enable_{x}"): + _dbg("Requiring %s", x) + self.feature.required.add(x) + if x == "raqm": + _dbg("--enable-raqm implies --enable-freetype") + self.feature.required.add("freetype") + for x in ("raqm", "fribidi"): + if getattr(self, f"vendor_{x}"): + if getattr(self, "disable_raqm"): + msg = f"Conflicting options: --vendor-{x} and --disable-raqm" + raise ValueError(msg) + if x == "fribidi" and not getattr(self, "vendor_raqm"): + msg = f"Conflicting options: --vendor-{x} and not --vendor-raqm" + raise ValueError(msg) + _dbg("Using vendored version of %s", x) + self.feature.vendor.add(x) + + def _update_extension(self, name, libraries, define_macros=None, sources=None): + for extension in self.extensions: + if extension.name == name: + extension.libraries += libraries + if define_macros is not None: + extension.define_macros += define_macros + if sources is not None: + extension.sources += sources + if FUZZING_BUILD: + extension.language = "c++" + extension.extra_link_args = ["--stdlib=libc++"] + break + + def _remove_extension(self, name): + for extension in self.extensions: + if extension.name == name: + self.extensions.remove(extension) + break + + def get_macos_sdk_path(self): + try: + sdk_path = ( + subprocess.check_output(["xcrun", "--show-sdk-path"]) + .strip() + .decode("latin1") + ) + except Exception: + sdk_path = None + if ( + not sdk_path + or sdk_path == "/Applications/Xcode.app/Contents/Developer" + "/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk" + ): + commandlinetools_sdk_path = ( + "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk" + ) + if os.path.exists(commandlinetools_sdk_path): + sdk_path = commandlinetools_sdk_path + return sdk_path + + def build_extensions(self): + library_dirs = [] + include_dirs = [] + + pkg_config = None + if _cmd_exists(os.environ.get("PKG_CONFIG", "pkg-config")): + pkg_config = _pkg_config + + # + # add configured kits + for root_name, lib_name in { + "JPEG_ROOT": "libjpeg", + "JPEG2K_ROOT": "libopenjp2", + "TIFF_ROOT": ("libtiff-5", "libtiff-4"), + "ZLIB_ROOT": "zlib", + "FREETYPE_ROOT": "freetype2", + "HARFBUZZ_ROOT": "harfbuzz", + "FRIBIDI_ROOT": "fribidi", + "RAQM_ROOT": "raqm", + "WEBP_ROOT": "libwebp", + "LCMS_ROOT": "lcms2", + "IMAGEQUANT_ROOT": "libimagequant", + }.items(): + root = globals()[root_name] + + if root is None and root_name in os.environ: + prefix = os.environ[root_name] + root = (os.path.join(prefix, "lib"), os.path.join(prefix, "include")) + + if root is None and pkg_config: + if isinstance(lib_name, tuple): + for lib_name2 in lib_name: + _dbg(f"Looking for `{lib_name2}` using pkg-config.") + root = pkg_config(lib_name2) + if root: + break + else: + _dbg(f"Looking for `{lib_name}` using pkg-config.") + root = pkg_config(lib_name) + + if isinstance(root, tuple): + lib_root, include_root = root + else: + lib_root = include_root = root + + if lib_root is not None: + if not isinstance(lib_root, (tuple, list)): + lib_root = (lib_root,) + for lib_dir in lib_root: + _add_directory(library_dirs, lib_dir) + if include_root is not None: + if not isinstance(include_root, (tuple, list)): + include_root = (include_root,) + for include_dir in include_root: + _add_directory(include_dirs, include_dir) + + # respect CFLAGS/CPPFLAGS/LDFLAGS + for k in ("CFLAGS", "CPPFLAGS", "LDFLAGS"): + if k in os.environ: + for match in re.finditer(r"-I([^\s]+)", os.environ[k]): + _add_directory(include_dirs, match.group(1)) + for match in re.finditer(r"-L([^\s]+)", os.environ[k]): + _add_directory(library_dirs, match.group(1)) + + # include, rpath, if set as environment variables: + for k in ("C_INCLUDE_PATH", "CPATH", "INCLUDE"): + if k in os.environ: + for d in os.environ[k].split(os.path.pathsep): + _add_directory(include_dirs, d) + + for k in ("LD_RUN_PATH", "LIBRARY_PATH", "LIB"): + if k in os.environ: + for d in os.environ[k].split(os.path.pathsep): + _add_directory(library_dirs, d) + + _add_directory(library_dirs, os.path.join(sys.prefix, "lib")) + _add_directory(include_dirs, os.path.join(sys.prefix, "include")) + + # + # add platform directories + + if self.disable_platform_guessing: + pass + + elif sys.platform == "cygwin": + # pythonX.Y.dll.a is in the /usr/lib/pythonX.Y/config directory + self.compiler.shared_lib_extension = ".dll.a" + _add_directory( + library_dirs, + os.path.join( + "/usr/lib", "python{}.{}".format(*sys.version_info), "config" + ), + ) + + elif sys.platform == "darwin": + # attempt to make sure we pick freetype2 over other versions + _add_directory(include_dirs, "/sw/include/freetype2") + _add_directory(include_dirs, "/sw/lib/freetype2/include") + # fink installation directories + _add_directory(library_dirs, "/sw/lib") + _add_directory(include_dirs, "/sw/include") + # darwin ports installation directories + _add_directory(library_dirs, "/opt/local/lib") + _add_directory(include_dirs, "/opt/local/include") + + # if Homebrew is installed, use its lib and include directories + try: + prefix = ( + subprocess.check_output(["brew", "--prefix"]) + .strip() + .decode("latin1") + ) + except Exception: + # Homebrew not installed + prefix = None + + ft_prefix = None + + if prefix: + # add Homebrew's include and lib directories + _add_directory(library_dirs, os.path.join(prefix, "lib")) + _add_directory(include_dirs, os.path.join(prefix, "include")) + _add_directory( + include_dirs, os.path.join(prefix, "opt", "zlib", "include") + ) + ft_prefix = os.path.join(prefix, "opt", "freetype") + + if ft_prefix and os.path.isdir(ft_prefix): + # freetype might not be linked into Homebrew's prefix + _add_directory(library_dirs, os.path.join(ft_prefix, "lib")) + _add_directory(include_dirs, os.path.join(ft_prefix, "include")) + else: + # fall back to freetype from XQuartz if + # Homebrew's freetype is missing + _add_directory(library_dirs, "/usr/X11/lib") + _add_directory(include_dirs, "/usr/X11/include") + + sdk_path = self.get_macos_sdk_path() + if sdk_path: + _add_directory(library_dirs, os.path.join(sdk_path, "usr", "lib")) + _add_directory(include_dirs, os.path.join(sdk_path, "usr", "include")) + + 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") + ): + for dirname in _find_library_dirs_ldconfig(): + _add_directory(library_dirs, dirname) + if sys.platform.startswith("linux") and os.environ.get("ANDROID_ROOT"): + # termux support for android. + # system libraries (zlib) are installed in /system/lib + # headers are at $PREFIX/include + # user libs are at $PREFIX/lib + _add_directory( + library_dirs, + os.path.join( + os.environ["ANDROID_ROOT"], + "lib" if struct.calcsize("l") == 4 else "lib64", + ), + ) + + elif sys.platform.startswith("netbsd"): + _add_directory(library_dirs, "/usr/pkg/lib") + _add_directory(include_dirs, "/usr/pkg/include") + + elif sys.platform.startswith("sunos5"): + _add_directory(library_dirs, "/opt/local/lib") + _add_directory(include_dirs, "/opt/local/include") + + # FIXME: check /opt/stuff directories here? + + # standard locations + if not self.disable_platform_guessing: + _add_directory(library_dirs, "/usr/local/lib") + _add_directory(include_dirs, "/usr/local/include") + + _add_directory(library_dirs, "/usr/lib") + _add_directory(include_dirs, "/usr/include") + # alpine, at least + _add_directory(library_dirs, "/lib") + + if sys.platform == "win32": + # on Windows, look for the OpenJPEG libraries in the location that + # the official installer puts them + program_files = os.environ.get("ProgramFiles", "") + best_version = (0, 0) + best_path = None + for name in os.listdir(program_files): + if name.startswith("OpenJPEG "): + version = tuple(int(x) for x in name[9:].strip().split(".")) + if version > best_version: + best_version = version + best_path = os.path.join(program_files, name) + + if best_path: + _dbg("Adding %s to search list", best_path) + _add_directory(library_dirs, os.path.join(best_path, "lib")) + _add_directory(include_dirs, os.path.join(best_path, "include")) + + # + # insert new dirs *before* default libs, to avoid conflicts + # between Python PYD stub libs and real libraries + + self.compiler.library_dirs = library_dirs + self.compiler.library_dirs + self.compiler.include_dirs = include_dirs + self.compiler.include_dirs + + # + # look for available libraries + + feature = self.feature + + if feature.want("zlib"): + _dbg("Looking for zlib") + if _find_include_file(self, "zlib.h"): + if _find_library_file(self, "z"): + feature.zlib = "z" + elif sys.platform == "win32" and _find_library_file(self, "zlib"): + feature.zlib = "zlib" # alternative name + + if feature.want("jpeg"): + _dbg("Looking for jpeg") + if _find_include_file(self, "jpeglib.h"): + if _find_library_file(self, "jpeg"): + feature.jpeg = "jpeg" + elif sys.platform == "win32" and _find_library_file(self, "libjpeg"): + feature.jpeg = "libjpeg" # alternative name + + feature.openjpeg_version = None + if feature.want("jpeg2000"): + _dbg("Looking for jpeg2000") + best_version = None + best_path = None + + # Find the best version + for directory in self.compiler.include_dirs: + _dbg("Checking for openjpeg-#.# in %s", directory) + try: + listdir = os.listdir(directory) + except Exception: + # OSError, FileNotFoundError + continue + for name in listdir: + if name.startswith("openjpeg-") and os.path.isfile( + os.path.join(directory, name, "openjpeg.h") + ): + _dbg("Found openjpeg.h in %s/%s", (directory, name)) + version = tuple(int(x) for x in name[9:].split(".")) + if best_version is None or version > best_version: + best_version = version + best_path = os.path.join(directory, name) + _dbg( + "Best openjpeg version %s so far in %s", + (best_version, best_path), + ) + + if best_version and _find_library_file(self, "openjp2"): + # Add the directory to the include path so we can include + # rather than having to cope with the versioned + # include path + _add_directory(self.compiler.include_dirs, best_path, 0) + feature.jpeg2000 = "openjp2" + feature.openjpeg_version = ".".join(str(x) for x in best_version) + + if feature.want("imagequant"): + _dbg("Looking for imagequant") + if _find_include_file(self, "libimagequant.h"): + if _find_library_file(self, "imagequant"): + feature.imagequant = "imagequant" + elif _find_library_file(self, "libimagequant"): + feature.imagequant = "libimagequant" + + if feature.want("tiff"): + _dbg("Looking for tiff") + if _find_include_file(self, "tiff.h"): + if _find_library_file(self, "tiff"): + feature.tiff = "tiff" + if sys.platform in ["win32", "darwin"] and _find_library_file( + self, "libtiff" + ): + feature.tiff = "libtiff" + + if feature.want("freetype"): + _dbg("Looking for freetype") + if _find_library_file(self, "freetype"): + # look for freetype2 include files + freetype_version = 0 + for subdir in self.compiler.include_dirs: + _dbg("Checking for include file %s in %s", ("ft2build.h", subdir)) + if os.path.isfile(os.path.join(subdir, "ft2build.h")): + _dbg("Found %s in %s", ("ft2build.h", subdir)) + freetype_version = 21 + subdir = os.path.join(subdir, "freetype2") + break + subdir = os.path.join(subdir, "freetype2") + _dbg("Checking for include file %s in %s", ("ft2build.h", subdir)) + if os.path.isfile(os.path.join(subdir, "ft2build.h")): + _dbg("Found %s in %s", ("ft2build.h", subdir)) + freetype_version = 21 + break + if freetype_version: + feature.freetype = "freetype" + if subdir: + _add_directory(self.compiler.include_dirs, subdir, 0) + + if feature.freetype and feature.want("raqm"): + if not feature.want_vendor("raqm"): # want system Raqm + _dbg("Looking for Raqm") + if _find_include_file(self, "raqm.h"): + if _find_library_file(self, "raqm"): + feature.raqm = "raqm" + elif _find_library_file(self, "libraqm"): + feature.raqm = "libraqm" + else: # want to build Raqm from src/thirdparty + _dbg("Looking for HarfBuzz") + feature.harfbuzz = None + hb_dir = _find_include_dir(self, "harfbuzz", "hb.h") + if hb_dir: + if isinstance(hb_dir, str): + _add_directory(self.compiler.include_dirs, hb_dir, 0) + if _find_library_file(self, "harfbuzz"): + feature.harfbuzz = "harfbuzz" + if feature.harfbuzz: + if not feature.want_vendor("fribidi"): # want system FriBiDi + _dbg("Looking for FriBiDi") + feature.fribidi = None + fribidi_dir = _find_include_dir(self, "fribidi", "fribidi.h") + if fribidi_dir: + if isinstance(fribidi_dir, str): + _add_directory( + self.compiler.include_dirs, fribidi_dir, 0 + ) + if _find_library_file(self, "fribidi"): + feature.fribidi = "fribidi" + feature.raqm = True + else: # want to build FriBiDi shim from src/thirdparty + feature.raqm = True + + if feature.want("lcms"): + _dbg("Looking for lcms") + if _find_include_file(self, "lcms2.h"): + if _find_library_file(self, "lcms2"): + feature.lcms = "lcms2" + elif _find_library_file(self, "lcms2_static"): + # alternate Windows name. + feature.lcms = "lcms2_static" + + if feature.want("webp"): + _dbg("Looking for webp") + if _find_include_file(self, "webp/encode.h") and _find_include_file( + self, "webp/decode.h" + ): + # In Google's precompiled zip it is call "libwebp": + if _find_library_file(self, "webp"): + feature.webp = "webp" + elif _find_library_file(self, "libwebp"): + feature.webp = "libwebp" + + if feature.want("webpmux"): + _dbg("Looking for webpmux") + if _find_include_file(self, "webp/mux.h") and _find_include_file( + self, "webp/demux.h" + ): + if _find_library_file(self, "webpmux") and _find_library_file( + self, "webpdemux" + ): + feature.webpmux = "webpmux" + if _find_library_file(self, "libwebpmux") and _find_library_file( + self, "libwebpdemux" + ): + feature.webpmux = "libwebpmux" + + if feature.want("xcb"): + _dbg("Looking for xcb") + if _find_include_file(self, "xcb/xcb.h"): + if _find_library_file(self, "xcb"): + feature.xcb = "xcb" + + for f in feature: + if not getattr(feature, f) and feature.require(f): + if f in ("jpeg", "zlib"): + raise RequiredDependencyException(f) + raise DependencyException(f) + + # + # core library + + libs = self.add_imaging_libs.split() + defs = [] + if feature.tiff: + libs.append(feature.tiff) + defs.append(("HAVE_LIBTIFF", None)) + if sys.platform == "win32": + # This define needs to be defined if-and-only-if it was defined + # when compiling LibTIFF. LibTIFF doesn't expose it in `tiffconf.h`, + # so we have to guess; by default it is defined in all Windows builds. + # See #4237, #5243, #5359 for more information. + defs.append(("USE_WIN32_FILEIO", None)) + if feature.jpeg: + libs.append(feature.jpeg) + defs.append(("HAVE_LIBJPEG", None)) + if feature.jpeg2000: + libs.append(feature.jpeg2000) + defs.append(("HAVE_OPENJPEG", None)) + if sys.platform == "win32" and not PLATFORM_MINGW: + defs.append(("OPJ_STATIC", None)) + if feature.zlib: + libs.append(feature.zlib) + defs.append(("HAVE_LIBZ", None)) + if feature.imagequant: + libs.append(feature.imagequant) + defs.append(("HAVE_LIBIMAGEQUANT", None)) + if feature.xcb: + libs.append(feature.xcb) + defs.append(("HAVE_XCB", None)) + if sys.platform == "win32": + libs.extend(["kernel32", "user32", "gdi32"]) + if struct.unpack("h", b"\0\1")[0] == 1: + defs.append(("WORDS_BIGENDIAN", None)) + + defs.append(("PILLOW_VERSION", f'"{PILLOW_VERSION}"')) + + self._update_extension("PIL._imaging", libs, defs) + + # + # additional libraries + + if feature.freetype: + srcs = [] + libs = ["freetype"] + defs = [] + if feature.raqm: + if not feature.want_vendor("raqm"): # using system Raqm + defs.append(("HAVE_RAQM", None)) + defs.append(("HAVE_RAQM_SYSTEM", None)) + libs.append(feature.raqm) + else: # building Raqm from src/thirdparty + defs.append(("HAVE_RAQM", None)) + srcs.append("src/thirdparty/raqm/raqm.c") + libs.append(feature.harfbuzz) + if not feature.want_vendor("fribidi"): # using system FriBiDi + defs.append(("HAVE_FRIBIDI_SYSTEM", None)) + libs.append(feature.fribidi) + else: # building FriBiDi shim from src/thirdparty + srcs.append("src/thirdparty/fribidi-shim/fribidi.c") + self._update_extension("PIL._imagingft", libs, defs, srcs) + + else: + self._remove_extension("PIL._imagingft") + + if feature.lcms: + extra = [] + if sys.platform == "win32": + extra.extend(["user32", "gdi32"]) + self._update_extension("PIL._imagingcms", [feature.lcms] + extra) + else: + self._remove_extension("PIL._imagingcms") + + if feature.webp: + libs = [feature.webp] + defs = [] + + if feature.webpmux: + defs.append(("HAVE_WEBPMUX", None)) + libs.append(feature.webpmux) + libs.append(feature.webpmux.replace("pmux", "pdemux")) + + self._update_extension("PIL._webp", libs, defs) + else: + self._remove_extension("PIL._webp") + + tk_libs = ["psapi"] if sys.platform in ("win32", "cygwin") else [] + self._update_extension("PIL._imagingtk", tk_libs) + + build_ext.build_extensions(self) + + # + # sanity checks + + self.summary_report(feature) + + def summary_report(self, feature): + print("-" * 68) + print("PIL SETUP SUMMARY") + print("-" * 68) + print(f"version Pillow {PILLOW_VERSION}") + v = sys.version.split("[") + print(f"platform {sys.platform} {v[0].strip()}") + for v in v[1:]: + print(f" [{v.strip()}") + print("-" * 68) + + raqm_extra_info = "" + if feature.want_vendor("raqm"): + raqm_extra_info += "bundled" + if feature.want_vendor("fribidi"): + raqm_extra_info += ", FriBiDi shim" + + options = [ + (feature.jpeg, "JPEG"), + (feature.jpeg2000, "OPENJPEG (JPEG2000)", feature.openjpeg_version), + (feature.zlib, "ZLIB (PNG/ZIP)"), + (feature.imagequant, "LIBIMAGEQUANT"), + (feature.tiff, "LIBTIFF"), + (feature.freetype, "FREETYPE2"), + (feature.raqm, "RAQM (Text shaping)", raqm_extra_info), + (feature.lcms, "LITTLECMS2"), + (feature.webp, "WEBP"), + (feature.webpmux, "WEBPMUX"), + (feature.xcb, "XCB (X protocol)"), + ] + + all = 1 + for option in options: + if option[0]: + extra_info = "" + if len(option) >= 3 and option[2]: + extra_info = f" ({option[2]})" + print(f"--- {option[1]} support available{extra_info}") + else: + print(f"*** {option[1]} support not available") + all = 0 + + print("-" * 68) + + if not all: + print("To add a missing option, make sure you have the required") + print("library and headers.") + print( + "See https://pillow.readthedocs.io/en/latest/installation." + "html#building-from-source" + ) + print("") + + print("To check the build, run the selftest.py script.") + print("") + + +def debug_build(): + return hasattr(sys, "gettotalrefcount") or FUZZING_BUILD + + +files = ["src/_imaging.c"] +for src_file in _IMAGING: + files.append("src/" + src_file + ".c") +for src_file in _LIB_IMAGING: + files.append(os.path.join("src/libImaging", src_file + ".c")) +ext_modules = [ + Extension("PIL._imaging", files), + Extension("PIL._imagingft", ["src/_imagingft.c"]), + Extension("PIL._imagingcms", ["src/_imagingcms.c"]), + Extension("PIL._webp", ["src/_webp.c"]), + Extension("PIL._imagingtk", ["src/_imagingtk.c", "src/Tk/tkImaging.c"]), + Extension("PIL._imagingmath", ["src/_imagingmath.c"]), + Extension("PIL._imagingmorph", ["src/_imagingmorph.c"]), +] + + +# parse configuration from _custom_build/backend.py +while sys.argv[-1].startswith("--pillow-configuration="): + _, key, value = sys.argv.pop().split("=", 2) + configuration.setdefault(key, []).append(value) + +try: + setup( + cmdclass={"build_ext": pil_build_ext}, + ext_modules=ext_modules, + zip_safe=not (debug_build() or PLATFORM_MINGW), + ) +except RequiredDependencyException as err: + msg = f""" + +The headers or library files could not be found for {str(err)}, +a required dependency when compiling Pillow from source. + +Please see the install instructions at: + https://pillow.readthedocs.io/en/latest/installation/basic-installation.html + +""" + sys.stderr.write(msg) + raise RequiredDependencyException(msg) +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)} + +""" + sys.stderr.write(msg) + raise DependencyException(msg) diff --git a/src/PIL/BdfFontFile.py b/src/PIL/BdfFontFile.py index e3eda4fe9..cf2bd8dac 100644 --- a/src/PIL/BdfFontFile.py +++ b/src/PIL/BdfFontFile.py @@ -1,133 +1,133 @@ -# -# The Python Imaging Library -# $Id$ -# -# bitmap distribution font (bdf) file parser -# -# history: -# 1996-05-16 fl created (as bdf2pil) -# 1997-08-25 fl converted to FontFile driver -# 2001-05-25 fl removed bogus __init__ call -# 2002-11-20 fl robustification (from Kevin Cazabon, Dmitry Vasiliev) -# 2003-04-22 fl more robustification (from Graham Dumpleton) -# -# Copyright (c) 1997-2003 by Secret Labs AB. -# Copyright (c) 1997-2003 by Fredrik Lundh. -# -# See the README file for information on usage and redistribution. -# - -""" -Parse X Bitmap Distribution Format (BDF) -""" -from __future__ import annotations - -from typing import BinaryIO - -from . import FontFile, Image - -bdf_slant = { - "R": "Roman", - "I": "Italic", - "O": "Oblique", - "RI": "Reverse Italic", - "RO": "Reverse Oblique", - "OT": "Other", -} - -bdf_spacing = {"P": "Proportional", "M": "Monospaced", "C": "Cell"} - - -def bdf_char( - f: BinaryIO, -) -> ( - tuple[ - str, - int, - tuple[tuple[int, int], tuple[int, int, int, int], tuple[int, int, int, int]], - Image.Image, - ] - | None -): - # skip to STARTCHAR - while True: - s = f.readline() - if not s: - return None - if s[:9] == b"STARTCHAR": - break - id = s[9:].strip().decode("ascii") - - # load symbol properties - props = {} - while True: - s = f.readline() - if not s or s[:6] == b"BITMAP": - break - i = s.find(b" ") - props[s[:i].decode("ascii")] = s[i + 1 : -1].decode("ascii") - - # load bitmap - bitmap = bytearray() - while True: - s = f.readline() - if not s or s[:7] == b"ENDCHAR": - break - bitmap += s[:-1] - - # The word BBX - # followed by the width in x (BBw), height in y (BBh), - # and x and y displacement (BBxoff0, BByoff0) - # of the lower left corner from the origin of the character. - width, height, x_disp, y_disp = (int(p) for p in props["BBX"].split()) - - # The word DWIDTH - # followed by the width in x and y of the character in device pixels. - dwx, dwy = (int(p) for p in props["DWIDTH"].split()) - - bbox = ( - (dwx, dwy), - (x_disp, -y_disp - height, width + x_disp, -y_disp), - (0, 0, width, height), - ) - - try: - im = Image.frombytes("1", (width, height), bitmap, "hex", "1") - except ValueError: - # deal with zero-width characters - im = Image.new("1", (width, height)) - - return id, int(props["ENCODING"]), bbox, im - - -class BdfFontFile(FontFile.FontFile): - """Font file plugin for the X11 BDF format.""" - - def __init__(self, fp: BinaryIO): - super().__init__() - - s = fp.readline() - if s[:13] != b"STARTFONT 2.1": - msg = "not a valid BDF file" - raise SyntaxError(msg) - - props = {} - comments = [] - - while True: - s = fp.readline() - if not s or s[:13] == b"ENDPROPERTIES": - break - i = s.find(b" ") - props[s[:i].decode("ascii")] = s[i + 1 : -1].decode("ascii") - if s[:i] in [b"COMMENT", b"COPYRIGHT"]: - if s.find(b"LogicalFontDescription") < 0: - comments.append(s[i + 1 : -1].decode("ascii")) - - while True: - c = bdf_char(fp) - if not c: - break - id, ch, (xy, dst, src), im = c - if 0 <= ch < len(self.glyph): - self.glyph[ch] = xy, dst, src, im +# +# The Python Imaging Library +# $Id$ +# +# bitmap distribution font (bdf) file parser +# +# history: +# 1996-05-16 fl created (as bdf2pil) +# 1997-08-25 fl converted to FontFile driver +# 2001-05-25 fl removed bogus __init__ call +# 2002-11-20 fl robustification (from Kevin Cazabon, Dmitry Vasiliev) +# 2003-04-22 fl more robustification (from Graham Dumpleton) +# +# Copyright (c) 1997-2003 by Secret Labs AB. +# Copyright (c) 1997-2003 by Fredrik Lundh. +# +# See the README file for information on usage and redistribution. +# + +""" +Parse X Bitmap Distribution Format (BDF) +""" +from __future__ import annotations + +from typing import BinaryIO + +from . import FontFile, Image + +bdf_slant = { + "R": "Roman", + "I": "Italic", + "O": "Oblique", + "RI": "Reverse Italic", + "RO": "Reverse Oblique", + "OT": "Other", +} + +bdf_spacing = {"P": "Proportional", "M": "Monospaced", "C": "Cell"} + + +def bdf_char( + f: BinaryIO, +) -> ( + tuple[ + str, + int, + tuple[tuple[int, int], tuple[int, int, int, int], tuple[int, int, int, int]], + Image.Image, + ] + | None +): + # skip to STARTCHAR + while True: + s = f.readline() + if not s: + return None + if s[:9] == b"STARTCHAR": + break + id = s[9:].strip().decode("ascii") + + # load symbol properties + props = {} + while True: + s = f.readline() + if not s or s[:6] == b"BITMAP": + break + i = s.find(b" ") + props[s[:i].decode("ascii")] = s[i + 1 : -1].decode("ascii") + + # load bitmap + bitmap = bytearray() + while True: + s = f.readline() + if not s or s[:7] == b"ENDCHAR": + break + bitmap += s[:-1] + + # The word BBX + # followed by the width in x (BBw), height in y (BBh), + # and x and y displacement (BBxoff0, BByoff0) + # of the lower left corner from the origin of the character. + width, height, x_disp, y_disp = (int(p) for p in props["BBX"].split()) + + # The word DWIDTH + # followed by the width in x and y of the character in device pixels. + dwx, dwy = (int(p) for p in props["DWIDTH"].split()) + + bbox = ( + (dwx, dwy), + (x_disp, -y_disp - height, width + x_disp, -y_disp), + (0, 0, width, height), + ) + + try: + im = Image.frombytes("1", (width, height), bitmap, "hex", "1") + except ValueError: + # deal with zero-width characters + im = Image.new("1", (width, height)) + + return id, int(props["ENCODING"]), bbox, im + + +class BdfFontFile(FontFile.FontFile): + """Font file plugin for the X11 BDF format.""" + + def __init__(self, fp: BinaryIO): + super().__init__() + + s = fp.readline() + if s[:13] != b"STARTFONT 2.1": + msg = "not a valid BDF file" + raise SyntaxError(msg) + + props = {} + comments = [] + + while True: + s = fp.readline() + if not s or s[:13] == b"ENDPROPERTIES": + break + i = s.find(b" ") + props[s[:i].decode("ascii")] = s[i + 1 : -1].decode("ascii") + if s[:i] in [b"COMMENT", b"COPYRIGHT"]: + if s.find(b"LogicalFontDescription") < 0: + comments.append(s[i + 1 : -1].decode("ascii")) + + while True: + c = bdf_char(fp) + if not c: + break + id, ch, (xy, dst, src), im = c + if 0 <= ch < len(self.glyph): + self.glyph[ch] = xy, dst, src, im diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index 59246c6e2..5c9fba993 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -1,486 +1,486 @@ -""" -Blizzard Mipmap Format (.blp) -Jerome Leclanche - -The contents of this file are hereby released in the public domain (CC0) -Full text of the CC0 license: - https://creativecommons.org/publicdomain/zero/1.0/ - -BLP1 files, used mostly in Warcraft III, are not fully supported. -All types of BLP2 files used in World of Warcraft are supported. - -The BLP file structure consists of a header, up to 16 mipmaps of the -texture - -Texture sizes must be powers of two, though the two dimensions do -not have to be equal; 512x256 is valid, but 512x200 is not. -The first mipmap (mipmap #0) is the full size image; each subsequent -mipmap halves both dimensions. The final mipmap should be 1x1. - -BLP files come in many different flavours: -* JPEG-compressed (type == 0) - only supported for BLP1. -* RAW images (type == 1, encoding == 1). Each mipmap is stored as an - array of 8-bit values, one per pixel, left to right, top to bottom. - Each value is an index to the palette. -* DXT-compressed (type == 1, encoding == 2): -- DXT1 compression is used if alpha_encoding == 0. - - An additional alpha bit is used if alpha_depth == 1. - - DXT3 compression is used if alpha_encoding == 1. - - DXT5 compression is used if alpha_encoding == 7. -""" - -from __future__ import annotations - -import abc -import os -import struct -from enum import IntEnum -from io import BytesIO -from typing import IO - -from . import Image, ImageFile - - -class Format(IntEnum): - JPEG = 0 - - -class Encoding(IntEnum): - UNCOMPRESSED = 1 - DXT = 2 - UNCOMPRESSED_RAW_BGRA = 3 - - -class AlphaEncoding(IntEnum): - DXT1 = 0 - DXT3 = 1 - DXT5 = 7 - - -def unpack_565(i: int) -> tuple[int, int, int]: - return ((i >> 11) & 0x1F) << 3, ((i >> 5) & 0x3F) << 2, (i & 0x1F) << 3 - - -def decode_dxt1( - data: bytes, alpha: bool = False -) -> tuple[bytearray, bytearray, bytearray, bytearray]: - """ - input: one "row" of data (i.e. will produce 4*width pixels) - """ - - blocks = len(data) // 8 # number of blocks in row - ret = (bytearray(), bytearray(), bytearray(), bytearray()) - - for block_index in range(blocks): - # Decode next 8-byte block. - idx = block_index * 8 - color0, color1, bits = struct.unpack_from("> 2 - - a = 0xFF - if control == 0: - r, g, b = r0, g0, b0 - elif control == 1: - r, g, b = r1, g1, b1 - elif control == 2: - if color0 > color1: - r = (2 * r0 + r1) // 3 - g = (2 * g0 + g1) // 3 - b = (2 * b0 + b1) // 3 - else: - r = (r0 + r1) // 2 - g = (g0 + g1) // 2 - b = (b0 + b1) // 2 - elif control == 3: - if color0 > color1: - r = (2 * r1 + r0) // 3 - g = (2 * g1 + g0) // 3 - b = (2 * b1 + b0) // 3 - else: - r, g, b, a = 0, 0, 0, 0 - - if alpha: - ret[j].extend([r, g, b, a]) - else: - ret[j].extend([r, g, b]) - - return ret - - -def decode_dxt3(data: bytes) -> tuple[bytearray, bytearray, bytearray, bytearray]: - """ - input: one "row" of data (i.e. will produce 4*width pixels) - """ - - blocks = len(data) // 16 # number of blocks in row - ret = (bytearray(), bytearray(), bytearray(), bytearray()) - - for block_index in range(blocks): - idx = block_index * 16 - block = data[idx : idx + 16] - # Decode next 16-byte block. - bits = struct.unpack_from("<8B", block) - color0, color1 = struct.unpack_from(">= 4 - else: - high = True - a &= 0xF - a *= 17 # We get a value between 0 and 15 - - color_code = (code >> 2 * (4 * j + i)) & 0x03 - - if color_code == 0: - r, g, b = r0, g0, b0 - elif color_code == 1: - r, g, b = r1, g1, b1 - elif color_code == 2: - r = (2 * r0 + r1) // 3 - g = (2 * g0 + g1) // 3 - b = (2 * b0 + b1) // 3 - elif color_code == 3: - r = (2 * r1 + r0) // 3 - g = (2 * g1 + g0) // 3 - b = (2 * b1 + b0) // 3 - - ret[j].extend([r, g, b, a]) - - return ret - - -def decode_dxt5(data: bytes) -> tuple[bytearray, bytearray, bytearray, bytearray]: - """ - input: one "row" of data (i.e. will produce 4 * width pixels) - """ - - blocks = len(data) // 16 # number of blocks in row - ret = (bytearray(), bytearray(), bytearray(), bytearray()) - - for block_index in range(blocks): - idx = block_index * 16 - block = data[idx : idx + 16] - # Decode next 16-byte block. - a0, a1 = struct.unpack_from("> alphacode_index) & 0x07 - elif alphacode_index == 15: - alphacode = (alphacode2 >> 15) | ((alphacode1 << 1) & 0x06) - else: # alphacode_index >= 18 and alphacode_index <= 45 - alphacode = (alphacode1 >> (alphacode_index - 16)) & 0x07 - - if alphacode == 0: - a = a0 - elif alphacode == 1: - a = a1 - elif a0 > a1: - a = ((8 - alphacode) * a0 + (alphacode - 1) * a1) // 7 - elif alphacode == 6: - a = 0 - elif alphacode == 7: - a = 255 - else: - a = ((6 - alphacode) * a0 + (alphacode - 1) * a1) // 5 - - color_code = (code >> 2 * (4 * j + i)) & 0x03 - - if color_code == 0: - r, g, b = r0, g0, b0 - elif color_code == 1: - r, g, b = r1, g1, b1 - elif color_code == 2: - r = (2 * r0 + r1) // 3 - g = (2 * g0 + g1) // 3 - b = (2 * b0 + b1) // 3 - elif color_code == 3: - r = (2 * r1 + r0) // 3 - g = (2 * g1 + g0) // 3 - b = (2 * b1 + b0) // 3 - - ret[j].extend([r, g, b, a]) - - return ret - - -class BLPFormatError(NotImplementedError): - pass - - -def _accept(prefix: bytes) -> bool: - return prefix[:4] in (b"BLP1", b"BLP2") - - -class BlpImageFile(ImageFile.ImageFile): - """ - Blizzard Mipmap Format - """ - - format = "BLP" - format_description = "Blizzard Mipmap Format" - - def _open(self) -> 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._load() - except struct.error as e: - msg = "Truncated BLP file" - raise OSError(msg) from e - return -1, 0 - - @abc.abstractmethod - 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(" bytes: - return ImageFile._safe_read(self.fd, length) - - def _read_palette(self) -> list[tuple[int, int, int, int]]: - ret = [] - for i in range(256): - try: - b, g, r, a = struct.unpack("<4B", self._safe_read(4)) - except struct.error: - break - ret.append((b, g, r, a)) - return ret - - def _read_bgra(self, palette: list[tuple[int, int, int, int]]) -> bytearray: - data = bytearray() - _data = BytesIO(self._safe_read(self._blp_lengths[0])) - while True: - try: - (offset,) = struct.unpack(" None: - if self._blp_compression == Format.JPEG: - self._decode_jpeg_stream() - - elif self._blp_compression == 1: - if self._blp_encoding in (4, 5): - palette = self._read_palette() - data = self._read_bgra(palette) - self.set_as_raw(data) - else: - msg = f"Unsupported BLP encoding {repr(self._blp_encoding)}" - raise BLPFormatError(msg) - else: - msg = f"Unsupported BLP compression {repr(self._blp_encoding)}" - raise BLPFormatError(msg) - - def _decode_jpeg_stream(self) -> None: - from .JpegImagePlugin import JpegImageFile - - (jpeg_header_size,) = struct.unpack(" None: - palette = self._read_palette() - - assert self.fd is not None - self.fd.seek(self._blp_offsets[0]) - - if self._blp_compression == 1: - # Uncompressed or DirectX compression - - if self._blp_encoding == Encoding.UNCOMPRESSED: - data = self._read_bgra(palette) - - elif self._blp_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) - ): - 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): - 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): - for d in decode_dxt5(self._safe_read(linesize)): - data += d - else: - msg = f"Unsupported alpha encoding {repr(self._blp_alpha_encoding)}" - raise BLPFormatError(msg) - else: - msg = f"Unknown BLP encoding {repr(self._blp_encoding)}" - raise BLPFormatError(msg) - - else: - msg = f"Unknown BLP compression {repr(self._blp_compression)}" - raise BLPFormatError(msg) - - self.set_as_raw(data) - - -class BLPEncoder(ImageFile.PyEncoder): - _pushes_fd = True - - def _write_palette(self) -> bytes: - data = b"" - palette = self.im.getpalette("RGBA", "RGBA") - for i in range(len(palette) // 4): - r, g, b, a = palette[i * 4 : (i + 1) * 4] - data += struct.pack("<4B", b, g, r, a) - while len(data) < 256 * 4: - data += b"\x00" * 4 - return data - - def encode(self, bufsize: int) -> tuple[int, int, bytes]: - palette_data = self._write_palette() - - offset = 20 + 16 * 4 * 2 + len(palette_data) - data = struct.pack("<16I", offset, *((0,) * 15)) - - w, h = self.im.size - data += struct.pack("<16I", w * h, *((0,) * 15)) - - data += palette_data - - for y in range(h): - for x in range(w): - data += struct.pack(" None: - if im.mode != "P": - msg = "Unsupported BLP image mode" - raise ValueError(msg) - - magic = b"BLP1" if im.encoderinfo.get("blp_version") == "BLP1" else b"BLP2" - fp.write(magic) - - fp.write(struct.pack(" + +The contents of this file are hereby released in the public domain (CC0) +Full text of the CC0 license: + https://creativecommons.org/publicdomain/zero/1.0/ + +BLP1 files, used mostly in Warcraft III, are not fully supported. +All types of BLP2 files used in World of Warcraft are supported. + +The BLP file structure consists of a header, up to 16 mipmaps of the +texture + +Texture sizes must be powers of two, though the two dimensions do +not have to be equal; 512x256 is valid, but 512x200 is not. +The first mipmap (mipmap #0) is the full size image; each subsequent +mipmap halves both dimensions. The final mipmap should be 1x1. + +BLP files come in many different flavours: +* JPEG-compressed (type == 0) - only supported for BLP1. +* RAW images (type == 1, encoding == 1). Each mipmap is stored as an + array of 8-bit values, one per pixel, left to right, top to bottom. + Each value is an index to the palette. +* DXT-compressed (type == 1, encoding == 2): +- DXT1 compression is used if alpha_encoding == 0. + - An additional alpha bit is used if alpha_depth == 1. + - DXT3 compression is used if alpha_encoding == 1. + - DXT5 compression is used if alpha_encoding == 7. +""" + +from __future__ import annotations + +import abc +import os +import struct +from enum import IntEnum +from io import BytesIO +from typing import IO + +from . import Image, ImageFile + + +class Format(IntEnum): + JPEG = 0 + + +class Encoding(IntEnum): + UNCOMPRESSED = 1 + DXT = 2 + UNCOMPRESSED_RAW_BGRA = 3 + + +class AlphaEncoding(IntEnum): + DXT1 = 0 + DXT3 = 1 + DXT5 = 7 + + +def unpack_565(i: int) -> tuple[int, int, int]: + return ((i >> 11) & 0x1F) << 3, ((i >> 5) & 0x3F) << 2, (i & 0x1F) << 3 + + +def decode_dxt1( + data: bytes, alpha: bool = False +) -> tuple[bytearray, bytearray, bytearray, bytearray]: + """ + input: one "row" of data (i.e. will produce 4*width pixels) + """ + + blocks = len(data) // 8 # number of blocks in row + ret = (bytearray(), bytearray(), bytearray(), bytearray()) + + for block_index in range(blocks): + # Decode next 8-byte block. + idx = block_index * 8 + color0, color1, bits = struct.unpack_from("> 2 + + a = 0xFF + if control == 0: + r, g, b = r0, g0, b0 + elif control == 1: + r, g, b = r1, g1, b1 + elif control == 2: + if color0 > color1: + r = (2 * r0 + r1) // 3 + g = (2 * g0 + g1) // 3 + b = (2 * b0 + b1) // 3 + else: + r = (r0 + r1) // 2 + g = (g0 + g1) // 2 + b = (b0 + b1) // 2 + elif control == 3: + if color0 > color1: + r = (2 * r1 + r0) // 3 + g = (2 * g1 + g0) // 3 + b = (2 * b1 + b0) // 3 + else: + r, g, b, a = 0, 0, 0, 0 + + if alpha: + ret[j].extend([r, g, b, a]) + else: + ret[j].extend([r, g, b]) + + return ret + + +def decode_dxt3(data: bytes) -> tuple[bytearray, bytearray, bytearray, bytearray]: + """ + input: one "row" of data (i.e. will produce 4*width pixels) + """ + + blocks = len(data) // 16 # number of blocks in row + ret = (bytearray(), bytearray(), bytearray(), bytearray()) + + for block_index in range(blocks): + idx = block_index * 16 + block = data[idx : idx + 16] + # Decode next 16-byte block. + bits = struct.unpack_from("<8B", block) + color0, color1 = struct.unpack_from(">= 4 + else: + high = True + a &= 0xF + a *= 17 # We get a value between 0 and 15 + + color_code = (code >> 2 * (4 * j + i)) & 0x03 + + if color_code == 0: + r, g, b = r0, g0, b0 + elif color_code == 1: + r, g, b = r1, g1, b1 + elif color_code == 2: + r = (2 * r0 + r1) // 3 + g = (2 * g0 + g1) // 3 + b = (2 * b0 + b1) // 3 + elif color_code == 3: + r = (2 * r1 + r0) // 3 + g = (2 * g1 + g0) // 3 + b = (2 * b1 + b0) // 3 + + ret[j].extend([r, g, b, a]) + + return ret + + +def decode_dxt5(data: bytes) -> tuple[bytearray, bytearray, bytearray, bytearray]: + """ + input: one "row" of data (i.e. will produce 4 * width pixels) + """ + + blocks = len(data) // 16 # number of blocks in row + ret = (bytearray(), bytearray(), bytearray(), bytearray()) + + for block_index in range(blocks): + idx = block_index * 16 + block = data[idx : idx + 16] + # Decode next 16-byte block. + a0, a1 = struct.unpack_from("> alphacode_index) & 0x07 + elif alphacode_index == 15: + alphacode = (alphacode2 >> 15) | ((alphacode1 << 1) & 0x06) + else: # alphacode_index >= 18 and alphacode_index <= 45 + alphacode = (alphacode1 >> (alphacode_index - 16)) & 0x07 + + if alphacode == 0: + a = a0 + elif alphacode == 1: + a = a1 + elif a0 > a1: + a = ((8 - alphacode) * a0 + (alphacode - 1) * a1) // 7 + elif alphacode == 6: + a = 0 + elif alphacode == 7: + a = 255 + else: + a = ((6 - alphacode) * a0 + (alphacode - 1) * a1) // 5 + + color_code = (code >> 2 * (4 * j + i)) & 0x03 + + if color_code == 0: + r, g, b = r0, g0, b0 + elif color_code == 1: + r, g, b = r1, g1, b1 + elif color_code == 2: + r = (2 * r0 + r1) // 3 + g = (2 * g0 + g1) // 3 + b = (2 * b0 + b1) // 3 + elif color_code == 3: + r = (2 * r1 + r0) // 3 + g = (2 * g1 + g0) // 3 + b = (2 * b1 + b0) // 3 + + ret[j].extend([r, g, b, a]) + + return ret + + +class BLPFormatError(NotImplementedError): + pass + + +def _accept(prefix: bytes) -> bool: + return prefix[:4] in (b"BLP1", b"BLP2") + + +class BlpImageFile(ImageFile.ImageFile): + """ + Blizzard Mipmap Format + """ + + format = "BLP" + format_description = "Blizzard Mipmap Format" + + def _open(self) -> 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._load() + except struct.error as e: + msg = "Truncated BLP file" + raise OSError(msg) from e + return -1, 0 + + @abc.abstractmethod + 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(" bytes: + return ImageFile._safe_read(self.fd, length) + + def _read_palette(self) -> list[tuple[int, int, int, int]]: + ret = [] + for i in range(256): + try: + b, g, r, a = struct.unpack("<4B", self._safe_read(4)) + except struct.error: + break + ret.append((b, g, r, a)) + return ret + + def _read_bgra(self, palette: list[tuple[int, int, int, int]]) -> bytearray: + data = bytearray() + _data = BytesIO(self._safe_read(self._blp_lengths[0])) + while True: + try: + (offset,) = struct.unpack(" None: + if self._blp_compression == Format.JPEG: + self._decode_jpeg_stream() + + elif self._blp_compression == 1: + if self._blp_encoding in (4, 5): + palette = self._read_palette() + data = self._read_bgra(palette) + self.set_as_raw(data) + else: + msg = f"Unsupported BLP encoding {repr(self._blp_encoding)}" + raise BLPFormatError(msg) + else: + msg = f"Unsupported BLP compression {repr(self._blp_encoding)}" + raise BLPFormatError(msg) + + def _decode_jpeg_stream(self) -> None: + from .JpegImagePlugin import JpegImageFile + + (jpeg_header_size,) = struct.unpack(" None: + palette = self._read_palette() + + assert self.fd is not None + self.fd.seek(self._blp_offsets[0]) + + if self._blp_compression == 1: + # Uncompressed or DirectX compression + + if self._blp_encoding == Encoding.UNCOMPRESSED: + data = self._read_bgra(palette) + + elif self._blp_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) + ): + 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): + 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): + for d in decode_dxt5(self._safe_read(linesize)): + data += d + else: + msg = f"Unsupported alpha encoding {repr(self._blp_alpha_encoding)}" + raise BLPFormatError(msg) + else: + msg = f"Unknown BLP encoding {repr(self._blp_encoding)}" + raise BLPFormatError(msg) + + else: + msg = f"Unknown BLP compression {repr(self._blp_compression)}" + raise BLPFormatError(msg) + + self.set_as_raw(data) + + +class BLPEncoder(ImageFile.PyEncoder): + _pushes_fd = True + + def _write_palette(self) -> bytes: + data = b"" + palette = self.im.getpalette("RGBA", "RGBA") + for i in range(len(palette) // 4): + r, g, b, a = palette[i * 4 : (i + 1) * 4] + data += struct.pack("<4B", b, g, r, a) + while len(data) < 256 * 4: + data += b"\x00" * 4 + return data + + def encode(self, bufsize: int) -> tuple[int, int, bytes]: + palette_data = self._write_palette() + + offset = 20 + 16 * 4 * 2 + len(palette_data) + data = struct.pack("<16I", offset, *((0,) * 15)) + + w, h = self.im.size + data += struct.pack("<16I", w * h, *((0,) * 15)) + + data += palette_data + + for y in range(h): + for x in range(w): + data += struct.pack(" None: + if im.mode != "P": + msg = "Unsupported BLP image mode" + raise ValueError(msg) + + magic = b"BLP1" if im.encoderinfo.get("blp_version") == "BLP1" else b"BLP2" + fp.write(magic) + + fp.write(struct.pack(" mode, rawmode - 1: ("P", "P;1"), - 4: ("P", "P;4"), - 8: ("P", "P"), - 16: ("RGB", "BGR;15"), - 24: ("RGB", "BGR"), - 32: ("RGB", "BGRX"), -} - - -def _accept(prefix: bytes) -> bool: - return prefix[:2] == b"BM" - - -def _dib_accept(prefix: bytes) -> bool: - return i32(prefix) in [12, 40, 52, 56, 64, 108, 124] - - -# ============================================================================= -# Image plugin for the Windows BMP format. -# ============================================================================= -class BmpImageFile(ImageFile.ImageFile): - """Image plugin for the Windows Bitmap format (BMP)""" - - # ------------------------------------------------------------- Description - format_description = "Windows Bitmap" - format = "BMP" - - # -------------------------------------------------- BMP Compression values - COMPRESSIONS = {"RAW": 0, "RLE8": 1, "RLE4": 2, "BITFIELDS": 3, "JPEG": 4, "PNG": 5} - for k, v in COMPRESSIONS.items(): - vars()[k] = v - - def _bitmap(self, header=0, offset=0): - """Read relevant info about the BMP""" - read, seek = self.fp.read, self.fp.seek - if header: - seek(header) - # read bmp header size @offset 14 (this is part of the header size) - file_info = {"header_size": i32(read(4)), "direction": -1} - - # -------------------- If requested, read header at a specific position - # read the rest of the bmp header, without its size - header_data = ImageFile._safe_read(self.fp, file_info["header_size"] - 4) - - # ------------------------------- Windows Bitmap v2, IBM OS/2 Bitmap v1 - # ----- This format has different offsets because of width/height types - # 12: BITMAPCOREHEADER/OS21XBITMAPHEADER - if file_info["header_size"] == 12: - file_info["width"] = i16(header_data, 0) - file_info["height"] = i16(header_data, 2) - file_info["planes"] = i16(header_data, 4) - file_info["bits"] = i16(header_data, 6) - file_info["compression"] = self.RAW - file_info["palette_padding"] = 3 - - # --------------------------------------------- Windows Bitmap v3 to v5 - # 40: BITMAPINFOHEADER - # 52: BITMAPV2HEADER - # 56: BITMAPV3HEADER - # 64: BITMAPCOREHEADER2/OS22XBITMAPHEADER - # 108: BITMAPV4HEADER - # 124: BITMAPV5HEADER - elif file_info["header_size"] in (40, 52, 56, 64, 108, 124): - file_info["y_flip"] = header_data[7] == 0xFF - file_info["direction"] = 1 if file_info["y_flip"] else -1 - file_info["width"] = i32(header_data, 0) - file_info["height"] = ( - i32(header_data, 4) - if not file_info["y_flip"] - else 2**32 - i32(header_data, 4) - ) - file_info["planes"] = i16(header_data, 8) - file_info["bits"] = i16(header_data, 10) - file_info["compression"] = i32(header_data, 12) - # byte size of pixel data - file_info["data_size"] = i32(header_data, 16) - file_info["pixels_per_meter"] = ( - i32(header_data, 20), - i32(header_data, 24), - ) - file_info["colors"] = i32(header_data, 28) - file_info["palette_padding"] = 4 - self.info["dpi"] = tuple(x / 39.3701 for x in file_info["pixels_per_meter"]) - if file_info["compression"] == self.BITFIELDS: - masks = ["r_mask", "g_mask", "b_mask"] - if len(header_data) >= 48: - if len(header_data) >= 52: - masks.append("a_mask") - else: - file_info["a_mask"] = 0x0 - for idx, mask in enumerate(masks): - file_info[mask] = i32(header_data, 36 + idx * 4) - else: - # 40 byte headers only have the three components in the - # bitfields masks, ref: - # https://msdn.microsoft.com/en-us/library/windows/desktop/dd183376(v=vs.85).aspx - # See also - # https://github.com/python-pillow/Pillow/issues/1293 - # There is a 4th component in the RGBQuad, in the alpha - # location, but it is listed as a reserved component, - # and it is not generally an alpha channel - file_info["a_mask"] = 0x0 - for mask in masks: - file_info[mask] = i32(read(4)) - file_info["rgb_mask"] = ( - file_info["r_mask"], - file_info["g_mask"], - file_info["b_mask"], - ) - file_info["rgba_mask"] = ( - file_info["r_mask"], - file_info["g_mask"], - file_info["b_mask"], - file_info["a_mask"], - ) - else: - msg = f"Unsupported BMP header type ({file_info['header_size']})" - raise OSError(msg) - - # ------------------ Special case : header is reported 40, which - # ---------------------- is shorter than real size for bpp >= 16 - self._size = file_info["width"], file_info["height"] - - # ------- If color count was not found in the header, compute from bits - file_info["colors"] = ( - file_info["colors"] - if file_info.get("colors", 0) - else (1 << file_info["bits"]) - ) - if offset == 14 + file_info["header_size"] and file_info["bits"] <= 8: - offset += 4 * file_info["colors"] - - # ---------------------- Check bit depth for unusual unsupported values - self._mode, raw_mode = BIT2MODE.get(file_info["bits"], (None, None)) - if self.mode is None: - msg = f"Unsupported BMP pixel depth ({file_info['bits']})" - raise OSError(msg) - - # ---------------- Process BMP with Bitfields compression (not palette) - decoder_name = "raw" - if file_info["compression"] == self.BITFIELDS: - SUPPORTED = { - 32: [ - (0xFF0000, 0xFF00, 0xFF, 0x0), - (0xFF000000, 0xFF0000, 0xFF00, 0x0), - (0xFF000000, 0xFF00, 0xFF, 0x0), - (0xFF000000, 0xFF0000, 0xFF00, 0xFF), - (0xFF, 0xFF00, 0xFF0000, 0xFF000000), - (0xFF0000, 0xFF00, 0xFF, 0xFF000000), - (0xFF000000, 0xFF00, 0xFF, 0xFF0000), - (0x0, 0x0, 0x0, 0x0), - ], - 24: [(0xFF0000, 0xFF00, 0xFF)], - 16: [(0xF800, 0x7E0, 0x1F), (0x7C00, 0x3E0, 0x1F)], - } - MASK_MODES = { - (32, (0xFF0000, 0xFF00, 0xFF, 0x0)): "BGRX", - (32, (0xFF000000, 0xFF0000, 0xFF00, 0x0)): "XBGR", - (32, (0xFF000000, 0xFF00, 0xFF, 0x0)): "BGXR", - (32, (0xFF000000, 0xFF0000, 0xFF00, 0xFF)): "ABGR", - (32, (0xFF, 0xFF00, 0xFF0000, 0xFF000000)): "RGBA", - (32, (0xFF0000, 0xFF00, 0xFF, 0xFF000000)): "BGRA", - (32, (0xFF000000, 0xFF00, 0xFF, 0xFF0000)): "BGAR", - (32, (0x0, 0x0, 0x0, 0x0)): "BGRA", - (24, (0xFF0000, 0xFF00, 0xFF)): "BGR", - (16, (0xF800, 0x7E0, 0x1F)): "BGR;16", - (16, (0x7C00, 0x3E0, 0x1F)): "BGR;15", - } - if file_info["bits"] in SUPPORTED: - if ( - file_info["bits"] == 32 - and file_info["rgba_mask"] in SUPPORTED[file_info["bits"]] - ): - raw_mode = MASK_MODES[(file_info["bits"], file_info["rgba_mask"])] - self._mode = "RGBA" if "A" in raw_mode else self.mode - elif ( - file_info["bits"] in (24, 16) - and file_info["rgb_mask"] in SUPPORTED[file_info["bits"]] - ): - raw_mode = MASK_MODES[(file_info["bits"], file_info["rgb_mask"])] - else: - msg = "Unsupported BMP bitfields layout" - raise OSError(msg) - else: - msg = "Unsupported BMP bitfields layout" - raise OSError(msg) - elif file_info["compression"] == self.RAW: - if file_info["bits"] == 32 and header == 22: # 32-bit .cur offset - raw_mode, self._mode = "BGRA", "RGBA" - elif file_info["compression"] in (self.RLE8, self.RLE4): - decoder_name = "bmp_rle" - else: - msg = f"Unsupported BMP compression ({file_info['compression']})" - raise OSError(msg) - - # --------------- Once the header is processed, process the palette/LUT - if self.mode == "P": # Paletted for 1, 4 and 8 bit images - # ---------------------------------------------------- 1-bit images - if not (0 < file_info["colors"] <= 65536): - msg = f"Unsupported BMP Palette size ({file_info['colors']})" - raise OSError(msg) - else: - padding = file_info["palette_padding"] - palette = read(padding * file_info["colors"]) - grayscale = True - indices = ( - (0, 255) - if file_info["colors"] == 2 - else list(range(file_info["colors"])) - ) - - # ----------------- Check if grayscale and ignore palette if so - for ind, val in enumerate(indices): - rgb = palette[ind * padding : ind * padding + 3] - if rgb != o8(val) * 3: - grayscale = False - - # ------- If all colors are gray, white or black, ditch palette - if grayscale: - self._mode = "1" if file_info["colors"] == 2 else "L" - raw_mode = self.mode - else: - self._mode = "P" - self.palette = ImagePalette.raw( - "BGRX" if padding == 4 else "BGR", palette - ) - - # ---------------------------- Finally set the tile data for the plugin - self.info["compression"] = file_info["compression"] - args = [raw_mode] - if decoder_name == "bmp_rle": - args.append(file_info["compression"] == self.RLE4) - else: - args.append(((file_info["width"] * file_info["bits"] + 31) >> 3) & (~3)) - args.append(file_info["direction"]) - self.tile = [ - ( - decoder_name, - (0, 0, file_info["width"], file_info["height"]), - offset or self.fp.tell(), - tuple(args), - ) - ] - - def _open(self) -> None: - """Open file, check magic number and read header""" - # read 14 bytes: magic number, filesize, reserved, header final offset - head_data = self.fp.read(14) - # choke if the file does not have the required magic bytes - if not _accept(head_data): - msg = "Not a BMP file" - raise SyntaxError(msg) - # read the start position of the BMP image data (u32) - offset = i32(head_data, 10) - # load bitmap information (offset=raster info) - self._bitmap(offset=offset) - - -class BmpRleDecoder(ImageFile.PyDecoder): - _pulls_fd = True - - def decode(self, buffer: bytes) -> tuple[int, int]: - assert self.fd is not None - rle4 = self.args[1] - data = bytearray() - x = 0 - dest_length = self.state.xsize * self.state.ysize - while len(data) < dest_length: - pixels = self.fd.read(1) - byte = self.fd.read(1) - if not pixels or not byte: - break - num_pixels = pixels[0] - if num_pixels: - # encoded mode - if x + num_pixels > self.state.xsize: - # Too much data for row - num_pixels = max(0, self.state.xsize - x) - if rle4: - first_pixel = o8(byte[0] >> 4) - second_pixel = o8(byte[0] & 0x0F) - for index in range(num_pixels): - if index % 2 == 0: - data += first_pixel - else: - data += second_pixel - else: - data += byte * num_pixels - x += num_pixels - else: - if byte[0] == 0: - # end of line - while len(data) % self.state.xsize != 0: - data += b"\x00" - x = 0 - elif byte[0] == 1: - # end of bitmap - break - elif byte[0] == 2: - # delta - bytes_read = self.fd.read(2) - if len(bytes_read) < 2: - break - right, up = self.fd.read(2) - data += b"\x00" * (right + up * self.state.xsize) - x = len(data) % self.state.xsize - else: - # absolute mode - if rle4: - # 2 pixels per byte - byte_count = byte[0] // 2 - bytes_read = self.fd.read(byte_count) - for byte_read in bytes_read: - data += o8(byte_read >> 4) - data += o8(byte_read & 0x0F) - else: - byte_count = byte[0] - bytes_read = self.fd.read(byte_count) - data += bytes_read - if len(bytes_read) < byte_count: - break - x += byte[0] - - # align to 16-bit word boundary - if self.fd.tell() % 2 != 0: - self.fd.seek(1, os.SEEK_CUR) - rawmode = "L" if self.mode == "L" else "P" - self.set_as_raw(bytes(data), (rawmode, 0, self.args[-1])) - return -1, 0 - - -# ============================================================================= -# Image plugin for the DIB format (BMP alias) -# ============================================================================= -class DibImageFile(BmpImageFile): - format = "DIB" - format_description = "Windows Bitmap" - - def _open(self) -> None: - self._bitmap() - - -# -# -------------------------------------------------------------------- -# Write BMP file - - -SAVE = { - "1": ("1", 1, 2), - "L": ("L", 8, 256), - "P": ("P", 8, 256), - "RGB": ("BGR", 24, 0), - "RGBA": ("BGRA", 32, 0), -} - - -def _dib_save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - _save(im, fp, filename, False) - - -def _save( - im: Image.Image, fp: IO[bytes], filename: str | bytes, bitmap_header: bool = True -) -> None: - try: - rawmode, bits, colors = SAVE[im.mode] - except KeyError as e: - msg = f"cannot write mode {im.mode} as BMP" - raise OSError(msg) from e - - info = im.encoderinfo - - dpi = info.get("dpi", (96, 96)) - - # 1 meter == 39.3701 inches - ppm = tuple(int(x * 39.3701 + 0.5) for x in dpi) - - stride = ((im.size[0] * bits + 7) // 8 + 3) & (~3) - header = 40 # or 64 for OS/2 version 2 - image = stride * im.size[1] - - if im.mode == "1": - palette = b"".join(o8(i) * 4 for i in (0, 255)) - elif im.mode == "L": - palette = b"".join(o8(i) * 4 for i in range(256)) - elif im.mode == "P": - palette = im.im.getpalette("RGB", "BGRX") - colors = len(palette) // 4 - else: - palette = None - - # bitmap header - if bitmap_header: - offset = 14 + header + colors * 4 - file_size = offset + image - if file_size > 2**32 - 1: - msg = "File size is too large for the BMP format" - raise ValueError(msg) - fp.write( - b"BM" # file type (magic) - + o32(file_size) # file size - + o32(0) # reserved - + o32(offset) # image data offset - ) - - # bitmap info header - fp.write( - o32(header) # info header size - + o32(im.size[0]) # width - + o32(im.size[1]) # height - + o16(1) # planes - + o16(bits) # depth - + o32(0) # compression (0=uncompressed) - + o32(image) # size of bitmap - + o32(ppm[0]) # resolution - + o32(ppm[1]) # resolution - + o32(colors) # colors used - + o32(colors) # colors important - ) - - fp.write(b"\0" * (header - 40)) # padding (for OS/2 format) - - if palette: - fp.write(palette) - - ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, stride, -1))]) - - -# -# -------------------------------------------------------------------- -# Registry - - -Image.register_open(BmpImageFile.format, BmpImageFile, _accept) -Image.register_save(BmpImageFile.format, _save) - -Image.register_extension(BmpImageFile.format, ".bmp") - -Image.register_mime(BmpImageFile.format, "image/bmp") - -Image.register_decoder("bmp_rle", BmpRleDecoder) - -Image.register_open(DibImageFile.format, DibImageFile, _dib_accept) -Image.register_save(DibImageFile.format, _dib_save) - -Image.register_extension(DibImageFile.format, ".dib") - -Image.register_mime(DibImageFile.format, "image/bmp") +# +# The Python Imaging Library. +# $Id$ +# +# BMP file handler +# +# Windows (and OS/2) native bitmap storage format. +# +# history: +# 1995-09-01 fl Created +# 1996-04-30 fl Added save +# 1997-08-27 fl Fixed save of 1-bit images +# 1998-03-06 fl Load P images as L where possible +# 1998-07-03 fl Load P images as 1 where possible +# 1998-12-29 fl Handle small palettes +# 2002-12-30 fl Fixed load of 1-bit palette images +# 2003-04-21 fl Fixed load of 1-bit monochrome images +# 2003-04-23 fl Added limited support for BI_BITFIELDS compression +# +# 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. +# +from __future__ import annotations + +import os +from typing import IO + +from . import Image, ImageFile, ImagePalette +from ._binary import i16le as i16 +from ._binary import i32le as i32 +from ._binary import o8 +from ._binary import o16le as o16 +from ._binary import o32le as o32 + +# +# -------------------------------------------------------------------- +# Read BMP file + +BIT2MODE = { + # bits => mode, rawmode + 1: ("P", "P;1"), + 4: ("P", "P;4"), + 8: ("P", "P"), + 16: ("RGB", "BGR;15"), + 24: ("RGB", "BGR"), + 32: ("RGB", "BGRX"), +} + + +def _accept(prefix: bytes) -> bool: + return prefix[:2] == b"BM" + + +def _dib_accept(prefix: bytes) -> bool: + return i32(prefix) in [12, 40, 52, 56, 64, 108, 124] + + +# ============================================================================= +# Image plugin for the Windows BMP format. +# ============================================================================= +class BmpImageFile(ImageFile.ImageFile): + """Image plugin for the Windows Bitmap format (BMP)""" + + # ------------------------------------------------------------- Description + format_description = "Windows Bitmap" + format = "BMP" + + # -------------------------------------------------- BMP Compression values + COMPRESSIONS = {"RAW": 0, "RLE8": 1, "RLE4": 2, "BITFIELDS": 3, "JPEG": 4, "PNG": 5} + for k, v in COMPRESSIONS.items(): + vars()[k] = v + + def _bitmap(self, header=0, offset=0): + """Read relevant info about the BMP""" + read, seek = self.fp.read, self.fp.seek + if header: + seek(header) + # read bmp header size @offset 14 (this is part of the header size) + file_info = {"header_size": i32(read(4)), "direction": -1} + + # -------------------- If requested, read header at a specific position + # read the rest of the bmp header, without its size + header_data = ImageFile._safe_read(self.fp, file_info["header_size"] - 4) + + # ------------------------------- Windows Bitmap v2, IBM OS/2 Bitmap v1 + # ----- This format has different offsets because of width/height types + # 12: BITMAPCOREHEADER/OS21XBITMAPHEADER + if file_info["header_size"] == 12: + file_info["width"] = i16(header_data, 0) + file_info["height"] = i16(header_data, 2) + file_info["planes"] = i16(header_data, 4) + file_info["bits"] = i16(header_data, 6) + file_info["compression"] = self.RAW + file_info["palette_padding"] = 3 + + # --------------------------------------------- Windows Bitmap v3 to v5 + # 40: BITMAPINFOHEADER + # 52: BITMAPV2HEADER + # 56: BITMAPV3HEADER + # 64: BITMAPCOREHEADER2/OS22XBITMAPHEADER + # 108: BITMAPV4HEADER + # 124: BITMAPV5HEADER + elif file_info["header_size"] in (40, 52, 56, 64, 108, 124): + file_info["y_flip"] = header_data[7] == 0xFF + file_info["direction"] = 1 if file_info["y_flip"] else -1 + file_info["width"] = i32(header_data, 0) + file_info["height"] = ( + i32(header_data, 4) + if not file_info["y_flip"] + else 2**32 - i32(header_data, 4) + ) + file_info["planes"] = i16(header_data, 8) + file_info["bits"] = i16(header_data, 10) + file_info["compression"] = i32(header_data, 12) + # byte size of pixel data + file_info["data_size"] = i32(header_data, 16) + file_info["pixels_per_meter"] = ( + i32(header_data, 20), + i32(header_data, 24), + ) + file_info["colors"] = i32(header_data, 28) + file_info["palette_padding"] = 4 + self.info["dpi"] = tuple(x / 39.3701 for x in file_info["pixels_per_meter"]) + if file_info["compression"] == self.BITFIELDS: + masks = ["r_mask", "g_mask", "b_mask"] + if len(header_data) >= 48: + if len(header_data) >= 52: + masks.append("a_mask") + else: + file_info["a_mask"] = 0x0 + for idx, mask in enumerate(masks): + file_info[mask] = i32(header_data, 36 + idx * 4) + else: + # 40 byte headers only have the three components in the + # bitfields masks, ref: + # https://msdn.microsoft.com/en-us/library/windows/desktop/dd183376(v=vs.85).aspx + # See also + # https://github.com/python-pillow/Pillow/issues/1293 + # There is a 4th component in the RGBQuad, in the alpha + # location, but it is listed as a reserved component, + # and it is not generally an alpha channel + file_info["a_mask"] = 0x0 + for mask in masks: + file_info[mask] = i32(read(4)) + file_info["rgb_mask"] = ( + file_info["r_mask"], + file_info["g_mask"], + file_info["b_mask"], + ) + file_info["rgba_mask"] = ( + file_info["r_mask"], + file_info["g_mask"], + file_info["b_mask"], + file_info["a_mask"], + ) + else: + msg = f"Unsupported BMP header type ({file_info['header_size']})" + raise OSError(msg) + + # ------------------ Special case : header is reported 40, which + # ---------------------- is shorter than real size for bpp >= 16 + self._size = file_info["width"], file_info["height"] + + # ------- If color count was not found in the header, compute from bits + file_info["colors"] = ( + file_info["colors"] + if file_info.get("colors", 0) + else (1 << file_info["bits"]) + ) + if offset == 14 + file_info["header_size"] and file_info["bits"] <= 8: + offset += 4 * file_info["colors"] + + # ---------------------- Check bit depth for unusual unsupported values + self._mode, raw_mode = BIT2MODE.get(file_info["bits"], (None, None)) + if self.mode is None: + msg = f"Unsupported BMP pixel depth ({file_info['bits']})" + raise OSError(msg) + + # ---------------- Process BMP with Bitfields compression (not palette) + decoder_name = "raw" + if file_info["compression"] == self.BITFIELDS: + SUPPORTED = { + 32: [ + (0xFF0000, 0xFF00, 0xFF, 0x0), + (0xFF000000, 0xFF0000, 0xFF00, 0x0), + (0xFF000000, 0xFF00, 0xFF, 0x0), + (0xFF000000, 0xFF0000, 0xFF00, 0xFF), + (0xFF, 0xFF00, 0xFF0000, 0xFF000000), + (0xFF0000, 0xFF00, 0xFF, 0xFF000000), + (0xFF000000, 0xFF00, 0xFF, 0xFF0000), + (0x0, 0x0, 0x0, 0x0), + ], + 24: [(0xFF0000, 0xFF00, 0xFF)], + 16: [(0xF800, 0x7E0, 0x1F), (0x7C00, 0x3E0, 0x1F)], + } + MASK_MODES = { + (32, (0xFF0000, 0xFF00, 0xFF, 0x0)): "BGRX", + (32, (0xFF000000, 0xFF0000, 0xFF00, 0x0)): "XBGR", + (32, (0xFF000000, 0xFF00, 0xFF, 0x0)): "BGXR", + (32, (0xFF000000, 0xFF0000, 0xFF00, 0xFF)): "ABGR", + (32, (0xFF, 0xFF00, 0xFF0000, 0xFF000000)): "RGBA", + (32, (0xFF0000, 0xFF00, 0xFF, 0xFF000000)): "BGRA", + (32, (0xFF000000, 0xFF00, 0xFF, 0xFF0000)): "BGAR", + (32, (0x0, 0x0, 0x0, 0x0)): "BGRA", + (24, (0xFF0000, 0xFF00, 0xFF)): "BGR", + (16, (0xF800, 0x7E0, 0x1F)): "BGR;16", + (16, (0x7C00, 0x3E0, 0x1F)): "BGR;15", + } + if file_info["bits"] in SUPPORTED: + if ( + file_info["bits"] == 32 + and file_info["rgba_mask"] in SUPPORTED[file_info["bits"]] + ): + raw_mode = MASK_MODES[(file_info["bits"], file_info["rgba_mask"])] + self._mode = "RGBA" if "A" in raw_mode else self.mode + elif ( + file_info["bits"] in (24, 16) + and file_info["rgb_mask"] in SUPPORTED[file_info["bits"]] + ): + raw_mode = MASK_MODES[(file_info["bits"], file_info["rgb_mask"])] + else: + msg = "Unsupported BMP bitfields layout" + raise OSError(msg) + else: + msg = "Unsupported BMP bitfields layout" + raise OSError(msg) + elif file_info["compression"] == self.RAW: + if file_info["bits"] == 32 and header == 22: # 32-bit .cur offset + raw_mode, self._mode = "BGRA", "RGBA" + elif file_info["compression"] in (self.RLE8, self.RLE4): + decoder_name = "bmp_rle" + else: + msg = f"Unsupported BMP compression ({file_info['compression']})" + raise OSError(msg) + + # --------------- Once the header is processed, process the palette/LUT + if self.mode == "P": # Paletted for 1, 4 and 8 bit images + # ---------------------------------------------------- 1-bit images + if not (0 < file_info["colors"] <= 65536): + msg = f"Unsupported BMP Palette size ({file_info['colors']})" + raise OSError(msg) + else: + padding = file_info["palette_padding"] + palette = read(padding * file_info["colors"]) + grayscale = True + indices = ( + (0, 255) + if file_info["colors"] == 2 + else list(range(file_info["colors"])) + ) + + # ----------------- Check if grayscale and ignore palette if so + for ind, val in enumerate(indices): + rgb = palette[ind * padding : ind * padding + 3] + if rgb != o8(val) * 3: + grayscale = False + + # ------- If all colors are gray, white or black, ditch palette + if grayscale: + self._mode = "1" if file_info["colors"] == 2 else "L" + raw_mode = self.mode + else: + self._mode = "P" + self.palette = ImagePalette.raw( + "BGRX" if padding == 4 else "BGR", palette + ) + + # ---------------------------- Finally set the tile data for the plugin + self.info["compression"] = file_info["compression"] + args = [raw_mode] + if decoder_name == "bmp_rle": + args.append(file_info["compression"] == self.RLE4) + else: + args.append(((file_info["width"] * file_info["bits"] + 31) >> 3) & (~3)) + args.append(file_info["direction"]) + self.tile = [ + ( + decoder_name, + (0, 0, file_info["width"], file_info["height"]), + offset or self.fp.tell(), + tuple(args), + ) + ] + + def _open(self) -> None: + """Open file, check magic number and read header""" + # read 14 bytes: magic number, filesize, reserved, header final offset + head_data = self.fp.read(14) + # choke if the file does not have the required magic bytes + if not _accept(head_data): + msg = "Not a BMP file" + raise SyntaxError(msg) + # read the start position of the BMP image data (u32) + offset = i32(head_data, 10) + # load bitmap information (offset=raster info) + self._bitmap(offset=offset) + + +class BmpRleDecoder(ImageFile.PyDecoder): + _pulls_fd = True + + def decode(self, buffer: bytes) -> tuple[int, int]: + assert self.fd is not None + rle4 = self.args[1] + data = bytearray() + x = 0 + dest_length = self.state.xsize * self.state.ysize + while len(data) < dest_length: + pixels = self.fd.read(1) + byte = self.fd.read(1) + if not pixels or not byte: + break + num_pixels = pixels[0] + if num_pixels: + # encoded mode + if x + num_pixels > self.state.xsize: + # Too much data for row + num_pixels = max(0, self.state.xsize - x) + if rle4: + first_pixel = o8(byte[0] >> 4) + second_pixel = o8(byte[0] & 0x0F) + for index in range(num_pixels): + if index % 2 == 0: + data += first_pixel + else: + data += second_pixel + else: + data += byte * num_pixels + x += num_pixels + else: + if byte[0] == 0: + # end of line + while len(data) % self.state.xsize != 0: + data += b"\x00" + x = 0 + elif byte[0] == 1: + # end of bitmap + break + elif byte[0] == 2: + # delta + bytes_read = self.fd.read(2) + if len(bytes_read) < 2: + break + right, up = self.fd.read(2) + data += b"\x00" * (right + up * self.state.xsize) + x = len(data) % self.state.xsize + else: + # absolute mode + if rle4: + # 2 pixels per byte + byte_count = byte[0] // 2 + bytes_read = self.fd.read(byte_count) + for byte_read in bytes_read: + data += o8(byte_read >> 4) + data += o8(byte_read & 0x0F) + else: + byte_count = byte[0] + bytes_read = self.fd.read(byte_count) + data += bytes_read + if len(bytes_read) < byte_count: + break + x += byte[0] + + # align to 16-bit word boundary + if self.fd.tell() % 2 != 0: + self.fd.seek(1, os.SEEK_CUR) + rawmode = "L" if self.mode == "L" else "P" + self.set_as_raw(bytes(data), (rawmode, 0, self.args[-1])) + return -1, 0 + + +# ============================================================================= +# Image plugin for the DIB format (BMP alias) +# ============================================================================= +class DibImageFile(BmpImageFile): + format = "DIB" + format_description = "Windows Bitmap" + + def _open(self) -> None: + self._bitmap() + + +# +# -------------------------------------------------------------------- +# Write BMP file + + +SAVE = { + "1": ("1", 1, 2), + "L": ("L", 8, 256), + "P": ("P", 8, 256), + "RGB": ("BGR", 24, 0), + "RGBA": ("BGRA", 32, 0), +} + + +def _dib_save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + _save(im, fp, filename, False) + + +def _save( + im: Image.Image, fp: IO[bytes], filename: str | bytes, bitmap_header: bool = True +) -> None: + try: + rawmode, bits, colors = SAVE[im.mode] + except KeyError as e: + msg = f"cannot write mode {im.mode} as BMP" + raise OSError(msg) from e + + info = im.encoderinfo + + dpi = info.get("dpi", (96, 96)) + + # 1 meter == 39.3701 inches + ppm = tuple(int(x * 39.3701 + 0.5) for x in dpi) + + stride = ((im.size[0] * bits + 7) // 8 + 3) & (~3) + header = 40 # or 64 for OS/2 version 2 + image = stride * im.size[1] + + if im.mode == "1": + palette = b"".join(o8(i) * 4 for i in (0, 255)) + elif im.mode == "L": + palette = b"".join(o8(i) * 4 for i in range(256)) + elif im.mode == "P": + palette = im.im.getpalette("RGB", "BGRX") + colors = len(palette) // 4 + else: + palette = None + + # bitmap header + if bitmap_header: + offset = 14 + header + colors * 4 + file_size = offset + image + if file_size > 2**32 - 1: + msg = "File size is too large for the BMP format" + raise ValueError(msg) + fp.write( + b"BM" # file type (magic) + + o32(file_size) # file size + + o32(0) # reserved + + o32(offset) # image data offset + ) + + # bitmap info header + fp.write( + o32(header) # info header size + + o32(im.size[0]) # width + + o32(im.size[1]) # height + + o16(1) # planes + + o16(bits) # depth + + o32(0) # compression (0=uncompressed) + + o32(image) # size of bitmap + + o32(ppm[0]) # resolution + + o32(ppm[1]) # resolution + + o32(colors) # colors used + + o32(colors) # colors important + ) + + fp.write(b"\0" * (header - 40)) # padding (for OS/2 format) + + if palette: + fp.write(palette) + + ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, stride, -1))]) + + +# +# -------------------------------------------------------------------- +# Registry + + +Image.register_open(BmpImageFile.format, BmpImageFile, _accept) +Image.register_save(BmpImageFile.format, _save) + +Image.register_extension(BmpImageFile.format, ".bmp") + +Image.register_mime(BmpImageFile.format, "image/bmp") + +Image.register_decoder("bmp_rle", BmpRleDecoder) + +Image.register_open(DibImageFile.format, DibImageFile, _dib_accept) +Image.register_save(DibImageFile.format, _dib_save) + +Image.register_extension(DibImageFile.format, ".dib") + +Image.register_mime(DibImageFile.format, "image/bmp") diff --git a/src/PIL/BufrStubImagePlugin.py b/src/PIL/BufrStubImagePlugin.py index 0ee2f653b..4d8b8ac8e 100644 --- a/src/PIL/BufrStubImagePlugin.py +++ b/src/PIL/BufrStubImagePlugin.py @@ -1,76 +1,76 @@ -# -# The Python Imaging Library -# $Id$ -# -# BUFR stub adapter -# -# Copyright (c) 1996-2003 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -from typing import IO - -from . import Image, ImageFile - -_handler = None - - -def register_handler(handler: ImageFile.StubHandler | None) -> None: - """ - Install application-specific BUFR image handler. - - :param handler: Handler object. - """ - global _handler - _handler = handler - - -# -------------------------------------------------------------------- -# Image adapter - - -def _accept(prefix: bytes) -> bool: - return prefix[:4] == b"BUFR" or prefix[:4] == b"ZCZC" - - -class BufrStubImageFile(ImageFile.StubImageFile): - format = "BUFR" - 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) - - # make something up - self._mode = "F" - self._size = 1, 1 - - loader = self._load() - if loader: - loader.open(self) - - def _load(self) -> ImageFile.StubHandler | None: - return _handler - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - if _handler is None or not hasattr(_handler, "save"): - msg = "BUFR save handler not installed" - raise OSError(msg) - _handler.save(im, fp, filename) - - -# -------------------------------------------------------------------- -# Registry - -Image.register_open(BufrStubImageFile.format, BufrStubImageFile, _accept) -Image.register_save(BufrStubImageFile.format, _save) - -Image.register_extension(BufrStubImageFile.format, ".bufr") +# +# The Python Imaging Library +# $Id$ +# +# BUFR stub adapter +# +# Copyright (c) 1996-2003 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +from typing import IO + +from . import Image, ImageFile + +_handler = None + + +def register_handler(handler: ImageFile.StubHandler | None) -> None: + """ + Install application-specific BUFR image handler. + + :param handler: Handler object. + """ + global _handler + _handler = handler + + +# -------------------------------------------------------------------- +# Image adapter + + +def _accept(prefix: bytes) -> bool: + return prefix[:4] == b"BUFR" or prefix[:4] == b"ZCZC" + + +class BufrStubImageFile(ImageFile.StubImageFile): + format = "BUFR" + 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) + + # make something up + self._mode = "F" + self._size = 1, 1 + + loader = self._load() + if loader: + loader.open(self) + + def _load(self) -> ImageFile.StubHandler | None: + return _handler + + +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + if _handler is None or not hasattr(_handler, "save"): + msg = "BUFR save handler not installed" + raise OSError(msg) + _handler.save(im, fp, filename) + + +# -------------------------------------------------------------------- +# Registry + +Image.register_open(BufrStubImageFile.format, BufrStubImageFile, _accept) +Image.register_save(BufrStubImageFile.format, _save) + +Image.register_extension(BufrStubImageFile.format, ".bufr") diff --git a/src/PIL/ContainerIO.py b/src/PIL/ContainerIO.py index 0035296a4..b88782f2d 100644 --- a/src/PIL/ContainerIO.py +++ b/src/PIL/ContainerIO.py @@ -1,121 +1,121 @@ -# -# The Python Imaging Library. -# $Id$ -# -# a class to read from a container file -# -# History: -# 1995-06-18 fl Created -# 1995-09-07 fl Added readline(), readlines() -# -# Copyright (c) 1997-2001 by Secret Labs AB -# Copyright (c) 1995 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import io -from typing import IO, AnyStr, Generic, Literal - - -class ContainerIO(Generic[AnyStr]): - """ - A file object that provides read access to a part of an existing - file (for example a TAR file). - """ - - def __init__(self, file: IO[AnyStr], offset: int, length: int) -> None: - """ - Create file object. - - :param file: Existing file. - :param offset: Start of region, in bytes. - :param length: Size of region, in bytes. - """ - self.fh: IO[AnyStr] = file - self.pos = 0 - self.offset = offset - self.length = length - self.fh.seek(offset) - - ## - # Always false. - - def isatty(self) -> bool: - return False - - def seek(self, offset: int, mode: Literal[0, 1, 2] = io.SEEK_SET) -> None: - """ - Move file pointer. - - :param offset: Offset in bytes. - :param mode: Starting position. Use 0 for beginning of region, 1 - for current offset, and 2 for end of region. You cannot move - the pointer outside the defined region. - """ - if mode == 1: - self.pos = self.pos + offset - elif mode == 2: - self.pos = self.length + offset - else: - self.pos = offset - # clamp - self.pos = max(0, min(self.pos, self.length)) - self.fh.seek(self.offset + self.pos) - - def tell(self) -> int: - """ - Get current file pointer. - - :returns: Offset from start of region, in bytes. - """ - return self.pos - - def read(self, n: int = 0) -> AnyStr: - """ - Read data. - - :param n: Number of bytes to read. If omitted or zero, - read until end of region. - :returns: An 8-bit string. - """ - if n: - n = min(n, self.length - self.pos) - else: - n = self.length - self.pos - if not n: # EOF - return b"" if "b" in self.fh.mode else "" # type: ignore[return-value] - self.pos = self.pos + n - return self.fh.read(n) - - def readline(self) -> AnyStr: - """ - Read a line of text. - - :returns: An 8-bit string. - """ - s: AnyStr = b"" if "b" in self.fh.mode else "" # type: ignore[assignment] - newline_character = b"\n" if "b" in self.fh.mode else "\n" - while True: - c = self.read(1) - if not c: - break - s = s + c - if c == newline_character: - break - return s - - def readlines(self) -> list[AnyStr]: - """ - Read multiple lines of text. - - :returns: A list of 8-bit strings. - """ - lines = [] - while True: - s = self.readline() - if not s: - break - lines.append(s) - return lines +# +# The Python Imaging Library. +# $Id$ +# +# a class to read from a container file +# +# History: +# 1995-06-18 fl Created +# 1995-09-07 fl Added readline(), readlines() +# +# Copyright (c) 1997-2001 by Secret Labs AB +# Copyright (c) 1995 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import io +from typing import IO, AnyStr, Generic, Literal + + +class ContainerIO(Generic[AnyStr]): + """ + A file object that provides read access to a part of an existing + file (for example a TAR file). + """ + + def __init__(self, file: IO[AnyStr], offset: int, length: int) -> None: + """ + Create file object. + + :param file: Existing file. + :param offset: Start of region, in bytes. + :param length: Size of region, in bytes. + """ + self.fh: IO[AnyStr] = file + self.pos = 0 + self.offset = offset + self.length = length + self.fh.seek(offset) + + ## + # Always false. + + def isatty(self) -> bool: + return False + + def seek(self, offset: int, mode: Literal[0, 1, 2] = io.SEEK_SET) -> None: + """ + Move file pointer. + + :param offset: Offset in bytes. + :param mode: Starting position. Use 0 for beginning of region, 1 + for current offset, and 2 for end of region. You cannot move + the pointer outside the defined region. + """ + if mode == 1: + self.pos = self.pos + offset + elif mode == 2: + self.pos = self.length + offset + else: + self.pos = offset + # clamp + self.pos = max(0, min(self.pos, self.length)) + self.fh.seek(self.offset + self.pos) + + def tell(self) -> int: + """ + Get current file pointer. + + :returns: Offset from start of region, in bytes. + """ + return self.pos + + def read(self, n: int = 0) -> AnyStr: + """ + Read data. + + :param n: Number of bytes to read. If omitted or zero, + read until end of region. + :returns: An 8-bit string. + """ + if n: + n = min(n, self.length - self.pos) + else: + n = self.length - self.pos + if not n: # EOF + return b"" if "b" in self.fh.mode else "" # type: ignore[return-value] + self.pos = self.pos + n + return self.fh.read(n) + + def readline(self) -> AnyStr: + """ + Read a line of text. + + :returns: An 8-bit string. + """ + s: AnyStr = b"" if "b" in self.fh.mode else "" # type: ignore[assignment] + newline_character = b"\n" if "b" in self.fh.mode else "\n" + while True: + c = self.read(1) + if not c: + break + s = s + c + if c == newline_character: + break + return s + + def readlines(self) -> list[AnyStr]: + """ + Read multiple lines of text. + + :returns: A list of 8-bit strings. + """ + lines = [] + while True: + s = self.readline() + if not s: + break + lines.append(s) + return lines diff --git a/src/PIL/CurImagePlugin.py b/src/PIL/CurImagePlugin.py index 85e2145e7..d192b8b59 100644 --- a/src/PIL/CurImagePlugin.py +++ b/src/PIL/CurImagePlugin.py @@ -1,75 +1,75 @@ -# -# The Python Imaging Library. -# $Id$ -# -# Windows Cursor support for PIL -# -# notes: -# uses BmpImagePlugin.py to read the bitmap data. -# -# history: -# 96-05-27 fl Created -# -# Copyright (c) Secret Labs AB 1997. -# Copyright (c) Fredrik Lundh 1996. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -from . import BmpImagePlugin, Image -from ._binary import i16le as i16 -from ._binary import i32le as i32 - -# -# -------------------------------------------------------------------- - - -def _accept(prefix: bytes) -> bool: - return prefix[:4] == b"\0\0\2\0" - - -## -# Image plugin for Windows Cursor files. - - -class CurImageFile(BmpImagePlugin.BmpImageFile): - format = "CUR" - format_description = "Windows Cursor" - - def _open(self) -> None: - offset = self.fp.tell() - - # check magic - s = self.fp.read(6) - if not _accept(s): - msg = "not a CUR file" - raise SyntaxError(msg) - - # pick the largest cursor in the file - m = b"" - for i in range(i16(s, 4)): - s = self.fp.read(16) - if not m: - m = s - elif s[0] > m[0] and s[1] > m[1]: - m = s - if not m: - msg = "No cursors were found" - raise TypeError(msg) - - # load as bitmap - self._bitmap(i32(m, 12) + offset) - - # patch up the bitmap height - self._size = self.size[0], self.size[1] // 2 - d, e, o, a = self.tile[0] - self.tile[0] = d, (0, 0) + self.size, o, a - - -# -# -------------------------------------------------------------------- - -Image.register_open(CurImageFile.format, CurImageFile, _accept) - -Image.register_extension(CurImageFile.format, ".cur") +# +# The Python Imaging Library. +# $Id$ +# +# Windows Cursor support for PIL +# +# notes: +# uses BmpImagePlugin.py to read the bitmap data. +# +# history: +# 96-05-27 fl Created +# +# Copyright (c) Secret Labs AB 1997. +# Copyright (c) Fredrik Lundh 1996. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +from . import BmpImagePlugin, Image +from ._binary import i16le as i16 +from ._binary import i32le as i32 + +# +# -------------------------------------------------------------------- + + +def _accept(prefix: bytes) -> bool: + return prefix[:4] == b"\0\0\2\0" + + +## +# Image plugin for Windows Cursor files. + + +class CurImageFile(BmpImagePlugin.BmpImageFile): + format = "CUR" + format_description = "Windows Cursor" + + def _open(self) -> None: + offset = self.fp.tell() + + # check magic + s = self.fp.read(6) + if not _accept(s): + msg = "not a CUR file" + raise SyntaxError(msg) + + # pick the largest cursor in the file + m = b"" + for i in range(i16(s, 4)): + s = self.fp.read(16) + if not m: + m = s + elif s[0] > m[0] and s[1] > m[1]: + m = s + if not m: + msg = "No cursors were found" + raise TypeError(msg) + + # load as bitmap + self._bitmap(i32(m, 12) + offset) + + # patch up the bitmap height + self._size = self.size[0], self.size[1] // 2 + d, e, o, a = self.tile[0] + self.tile[0] = d, (0, 0) + self.size, o, a + + +# +# -------------------------------------------------------------------- + +Image.register_open(CurImageFile.format, CurImageFile, _accept) + +Image.register_extension(CurImageFile.format, ".cur") diff --git a/src/PIL/DcxImagePlugin.py b/src/PIL/DcxImagePlugin.py index f67f27d73..5d4184f10 100644 --- a/src/PIL/DcxImagePlugin.py +++ b/src/PIL/DcxImagePlugin.py @@ -1,80 +1,80 @@ -# -# The Python Imaging Library. -# $Id$ -# -# DCX file handling -# -# DCX is a container file format defined by Intel, commonly used -# for fax applications. Each DCX file consists of a directory -# (a list of file offsets) followed by a set of (usually 1-bit) -# PCX files. -# -# History: -# 1995-09-09 fl Created -# 1996-03-20 fl Properly derived from PcxImageFile. -# 1998-07-15 fl Renamed offset attribute to avoid name clash -# 2002-07-30 fl Fixed file handling -# -# Copyright (c) 1997-98 by Secret Labs AB. -# Copyright (c) 1995-96 by Fredrik Lundh. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -from . import Image -from ._binary import i32le as i32 -from .PcxImagePlugin import PcxImageFile - -MAGIC = 0x3ADE68B1 # QUIZ: what's this value, then? - - -def _accept(prefix: bytes) -> bool: - return len(prefix) >= 4 and i32(prefix) == MAGIC - - -## -# Image plugin for the Intel DCX format. - - -class DcxImageFile(PcxImageFile): - format = "DCX" - format_description = "Intel DCX" - _close_exclusive_fp_after_loading = False - - def _open(self) -> None: - # Header - s = self.fp.read(4) - if not _accept(s): - msg = "not a DCX file" - raise SyntaxError(msg) - - # Component directory - self._offset = [] - for i in range(1024): - offset = i32(self.fp.read(4)) - if not offset: - break - self._offset.append(offset) - - self._fp = self.fp - self.frame = -1 - self.n_frames = len(self._offset) - self.is_animated = self.n_frames > 1 - self.seek(0) - - def seek(self, frame: int) -> None: - if not self._seek_check(frame): - return - self.frame = frame - self.fp = self._fp - self.fp.seek(self._offset[frame]) - PcxImageFile._open(self) - - def tell(self) -> int: - return self.frame - - -Image.register_open(DcxImageFile.format, DcxImageFile, _accept) - -Image.register_extension(DcxImageFile.format, ".dcx") +# +# The Python Imaging Library. +# $Id$ +# +# DCX file handling +# +# DCX is a container file format defined by Intel, commonly used +# for fax applications. Each DCX file consists of a directory +# (a list of file offsets) followed by a set of (usually 1-bit) +# PCX files. +# +# History: +# 1995-09-09 fl Created +# 1996-03-20 fl Properly derived from PcxImageFile. +# 1998-07-15 fl Renamed offset attribute to avoid name clash +# 2002-07-30 fl Fixed file handling +# +# Copyright (c) 1997-98 by Secret Labs AB. +# Copyright (c) 1995-96 by Fredrik Lundh. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +from . import Image +from ._binary import i32le as i32 +from .PcxImagePlugin import PcxImageFile + +MAGIC = 0x3ADE68B1 # QUIZ: what's this value, then? + + +def _accept(prefix: bytes) -> bool: + return len(prefix) >= 4 and i32(prefix) == MAGIC + + +## +# Image plugin for the Intel DCX format. + + +class DcxImageFile(PcxImageFile): + format = "DCX" + format_description = "Intel DCX" + _close_exclusive_fp_after_loading = False + + def _open(self) -> None: + # Header + s = self.fp.read(4) + if not _accept(s): + msg = "not a DCX file" + raise SyntaxError(msg) + + # Component directory + self._offset = [] + for i in range(1024): + offset = i32(self.fp.read(4)) + if not offset: + break + self._offset.append(offset) + + self._fp = self.fp + self.frame = -1 + self.n_frames = len(self._offset) + self.is_animated = self.n_frames > 1 + self.seek(0) + + def seek(self, frame: int) -> None: + if not self._seek_check(frame): + return + self.frame = frame + self.fp = self._fp + self.fp.seek(self._offset[frame]) + PcxImageFile._open(self) + + def tell(self) -> int: + return self.frame + + +Image.register_open(DcxImageFile.format, DcxImageFile, _accept) + +Image.register_extension(DcxImageFile.format, ".dcx") diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index e74727007..767c604ce 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -1,574 +1,574 @@ -""" -A Pillow loader for .dds files (S3TC-compressed aka DXTC) -Jerome Leclanche - -Documentation: -https://web.archive.org/web/20170802060935/http://oss.sgi.com/projects/ogl-sample/registry/EXT/texture_compression_s3tc.txt - -The contents of this file are hereby released in the public domain (CC0) -Full text of the CC0 license: -https://creativecommons.org/publicdomain/zero/1.0/ -""" - -from __future__ import annotations - -import io -import struct -import sys -from enum import IntEnum, IntFlag -from typing import IO - -from . import Image, ImageFile, ImagePalette -from ._binary import i32le as i32 -from ._binary import o8 -from ._binary import o32le as o32 - -# Magic ("DDS ") -DDS_MAGIC = 0x20534444 - - -# DDS flags -class DDSD(IntFlag): - CAPS = 0x1 - HEIGHT = 0x2 - WIDTH = 0x4 - PITCH = 0x8 - PIXELFORMAT = 0x1000 - MIPMAPCOUNT = 0x20000 - LINEARSIZE = 0x80000 - DEPTH = 0x800000 - - -# DDS caps -class DDSCAPS(IntFlag): - COMPLEX = 0x8 - TEXTURE = 0x1000 - MIPMAP = 0x400000 - - -class DDSCAPS2(IntFlag): - CUBEMAP = 0x200 - CUBEMAP_POSITIVEX = 0x400 - CUBEMAP_NEGATIVEX = 0x800 - CUBEMAP_POSITIVEY = 0x1000 - CUBEMAP_NEGATIVEY = 0x2000 - CUBEMAP_POSITIVEZ = 0x4000 - CUBEMAP_NEGATIVEZ = 0x8000 - VOLUME = 0x200000 - - -# Pixel Format -class DDPF(IntFlag): - ALPHAPIXELS = 0x1 - ALPHA = 0x2 - FOURCC = 0x4 - PALETTEINDEXED8 = 0x20 - RGB = 0x40 - LUMINANCE = 0x20000 - - -# dxgiformat.h -class DXGI_FORMAT(IntEnum): - UNKNOWN = 0 - R32G32B32A32_TYPELESS = 1 - R32G32B32A32_FLOAT = 2 - R32G32B32A32_UINT = 3 - R32G32B32A32_SINT = 4 - R32G32B32_TYPELESS = 5 - R32G32B32_FLOAT = 6 - R32G32B32_UINT = 7 - R32G32B32_SINT = 8 - R16G16B16A16_TYPELESS = 9 - R16G16B16A16_FLOAT = 10 - R16G16B16A16_UNORM = 11 - R16G16B16A16_UINT = 12 - R16G16B16A16_SNORM = 13 - R16G16B16A16_SINT = 14 - R32G32_TYPELESS = 15 - R32G32_FLOAT = 16 - R32G32_UINT = 17 - R32G32_SINT = 18 - R32G8X24_TYPELESS = 19 - D32_FLOAT_S8X24_UINT = 20 - R32_FLOAT_X8X24_TYPELESS = 21 - X32_TYPELESS_G8X24_UINT = 22 - R10G10B10A2_TYPELESS = 23 - R10G10B10A2_UNORM = 24 - R10G10B10A2_UINT = 25 - R11G11B10_FLOAT = 26 - R8G8B8A8_TYPELESS = 27 - R8G8B8A8_UNORM = 28 - R8G8B8A8_UNORM_SRGB = 29 - R8G8B8A8_UINT = 30 - R8G8B8A8_SNORM = 31 - R8G8B8A8_SINT = 32 - R16G16_TYPELESS = 33 - R16G16_FLOAT = 34 - R16G16_UNORM = 35 - R16G16_UINT = 36 - R16G16_SNORM = 37 - R16G16_SINT = 38 - R32_TYPELESS = 39 - D32_FLOAT = 40 - R32_FLOAT = 41 - R32_UINT = 42 - R32_SINT = 43 - R24G8_TYPELESS = 44 - D24_UNORM_S8_UINT = 45 - R24_UNORM_X8_TYPELESS = 46 - X24_TYPELESS_G8_UINT = 47 - R8G8_TYPELESS = 48 - R8G8_UNORM = 49 - R8G8_UINT = 50 - R8G8_SNORM = 51 - R8G8_SINT = 52 - R16_TYPELESS = 53 - R16_FLOAT = 54 - D16_UNORM = 55 - R16_UNORM = 56 - R16_UINT = 57 - R16_SNORM = 58 - R16_SINT = 59 - R8_TYPELESS = 60 - R8_UNORM = 61 - R8_UINT = 62 - R8_SNORM = 63 - R8_SINT = 64 - A8_UNORM = 65 - R1_UNORM = 66 - R9G9B9E5_SHAREDEXP = 67 - R8G8_B8G8_UNORM = 68 - G8R8_G8B8_UNORM = 69 - BC1_TYPELESS = 70 - BC1_UNORM = 71 - BC1_UNORM_SRGB = 72 - BC2_TYPELESS = 73 - BC2_UNORM = 74 - BC2_UNORM_SRGB = 75 - BC3_TYPELESS = 76 - BC3_UNORM = 77 - BC3_UNORM_SRGB = 78 - BC4_TYPELESS = 79 - BC4_UNORM = 80 - BC4_SNORM = 81 - BC5_TYPELESS = 82 - BC5_UNORM = 83 - BC5_SNORM = 84 - B5G6R5_UNORM = 85 - B5G5R5A1_UNORM = 86 - B8G8R8A8_UNORM = 87 - B8G8R8X8_UNORM = 88 - R10G10B10_XR_BIAS_A2_UNORM = 89 - B8G8R8A8_TYPELESS = 90 - B8G8R8A8_UNORM_SRGB = 91 - B8G8R8X8_TYPELESS = 92 - B8G8R8X8_UNORM_SRGB = 93 - BC6H_TYPELESS = 94 - BC6H_UF16 = 95 - BC6H_SF16 = 96 - BC7_TYPELESS = 97 - BC7_UNORM = 98 - BC7_UNORM_SRGB = 99 - AYUV = 100 - Y410 = 101 - Y416 = 102 - NV12 = 103 - P010 = 104 - P016 = 105 - OPAQUE_420 = 106 - YUY2 = 107 - Y210 = 108 - Y216 = 109 - NV11 = 110 - AI44 = 111 - IA44 = 112 - P8 = 113 - A8P8 = 114 - B4G4R4A4_UNORM = 115 - P208 = 130 - V208 = 131 - V408 = 132 - SAMPLER_FEEDBACK_MIN_MIP_OPAQUE = 189 - SAMPLER_FEEDBACK_MIP_REGION_USED_OPAQUE = 190 - - -class D3DFMT(IntEnum): - UNKNOWN = 0 - R8G8B8 = 20 - A8R8G8B8 = 21 - X8R8G8B8 = 22 - R5G6B5 = 23 - X1R5G5B5 = 24 - A1R5G5B5 = 25 - A4R4G4B4 = 26 - R3G3B2 = 27 - A8 = 28 - A8R3G3B2 = 29 - X4R4G4B4 = 30 - A2B10G10R10 = 31 - A8B8G8R8 = 32 - X8B8G8R8 = 33 - G16R16 = 34 - A2R10G10B10 = 35 - A16B16G16R16 = 36 - A8P8 = 40 - P8 = 41 - L8 = 50 - A8L8 = 51 - A4L4 = 52 - V8U8 = 60 - L6V5U5 = 61 - X8L8V8U8 = 62 - Q8W8V8U8 = 63 - V16U16 = 64 - A2W10V10U10 = 67 - D16_LOCKABLE = 70 - D32 = 71 - D15S1 = 73 - D24S8 = 75 - D24X8 = 77 - D24X4S4 = 79 - D16 = 80 - D32F_LOCKABLE = 82 - D24FS8 = 83 - D32_LOCKABLE = 84 - S8_LOCKABLE = 85 - L16 = 81 - VERTEXDATA = 100 - INDEX16 = 101 - INDEX32 = 102 - Q16W16V16U16 = 110 - R16F = 111 - G16R16F = 112 - A16B16G16R16F = 113 - R32F = 114 - G32R32F = 115 - A32B32G32R32F = 116 - CxV8U8 = 117 - A1 = 118 - A2B10G10R10_XR_BIAS = 119 - BINARYBUFFER = 199 - - UYVY = i32(b"UYVY") - R8G8_B8G8 = i32(b"RGBG") - YUY2 = i32(b"YUY2") - G8R8_G8B8 = i32(b"GRGB") - DXT1 = i32(b"DXT1") - DXT2 = i32(b"DXT2") - DXT3 = i32(b"DXT3") - DXT4 = i32(b"DXT4") - DXT5 = i32(b"DXT5") - DX10 = i32(b"DX10") - BC4S = i32(b"BC4S") - BC4U = i32(b"BC4U") - BC5S = i32(b"BC5S") - BC5U = i32(b"BC5U") - ATI1 = i32(b"ATI1") - ATI2 = i32(b"ATI2") - MULTI2_ARGB8 = i32(b"MET1") - - -# Backward compatibility layer -module = sys.modules[__name__] -for item in DDSD: - assert item.name is not None - setattr(module, f"DDSD_{item.name}", item.value) -for item1 in DDSCAPS: - assert item1.name is not None - setattr(module, f"DDSCAPS_{item1.name}", item1.value) -for item2 in DDSCAPS2: - assert item2.name is not None - setattr(module, f"DDSCAPS2_{item2.name}", item2.value) -for item3 in DDPF: - assert item3.name is not None - setattr(module, f"DDPF_{item3.name}", item3.value) - -DDS_FOURCC = DDPF.FOURCC -DDS_RGB = DDPF.RGB -DDS_RGBA = DDPF.RGB | DDPF.ALPHAPIXELS -DDS_LUMINANCE = DDPF.LUMINANCE -DDS_LUMINANCEA = DDPF.LUMINANCE | DDPF.ALPHAPIXELS -DDS_ALPHA = DDPF.ALPHA -DDS_PAL8 = DDPF.PALETTEINDEXED8 - -DDS_HEADER_FLAGS_TEXTURE = DDSD.CAPS | DDSD.HEIGHT | DDSD.WIDTH | DDSD.PIXELFORMAT -DDS_HEADER_FLAGS_MIPMAP = DDSD.MIPMAPCOUNT -DDS_HEADER_FLAGS_VOLUME = DDSD.DEPTH -DDS_HEADER_FLAGS_PITCH = DDSD.PITCH -DDS_HEADER_FLAGS_LINEARSIZE = DDSD.LINEARSIZE - -DDS_HEIGHT = DDSD.HEIGHT -DDS_WIDTH = DDSD.WIDTH - -DDS_SURFACE_FLAGS_TEXTURE = DDSCAPS.TEXTURE -DDS_SURFACE_FLAGS_MIPMAP = DDSCAPS.COMPLEX | DDSCAPS.MIPMAP -DDS_SURFACE_FLAGS_CUBEMAP = DDSCAPS.COMPLEX - -DDS_CUBEMAP_POSITIVEX = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_POSITIVEX -DDS_CUBEMAP_NEGATIVEX = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_NEGATIVEX -DDS_CUBEMAP_POSITIVEY = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_POSITIVEY -DDS_CUBEMAP_NEGATIVEY = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_NEGATIVEY -DDS_CUBEMAP_POSITIVEZ = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_POSITIVEZ -DDS_CUBEMAP_NEGATIVEZ = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_NEGATIVEZ - -DXT1_FOURCC = D3DFMT.DXT1 -DXT3_FOURCC = D3DFMT.DXT3 -DXT5_FOURCC = D3DFMT.DXT5 - -DXGI_FORMAT_R8G8B8A8_TYPELESS = DXGI_FORMAT.R8G8B8A8_TYPELESS -DXGI_FORMAT_R8G8B8A8_UNORM = DXGI_FORMAT.R8G8B8A8_UNORM -DXGI_FORMAT_R8G8B8A8_UNORM_SRGB = DXGI_FORMAT.R8G8B8A8_UNORM_SRGB -DXGI_FORMAT_BC5_TYPELESS = DXGI_FORMAT.BC5_TYPELESS -DXGI_FORMAT_BC5_UNORM = DXGI_FORMAT.BC5_UNORM -DXGI_FORMAT_BC5_SNORM = DXGI_FORMAT.BC5_SNORM -DXGI_FORMAT_BC6H_UF16 = DXGI_FORMAT.BC6H_UF16 -DXGI_FORMAT_BC6H_SF16 = DXGI_FORMAT.BC6H_SF16 -DXGI_FORMAT_BC7_TYPELESS = DXGI_FORMAT.BC7_TYPELESS -DXGI_FORMAT_BC7_UNORM = DXGI_FORMAT.BC7_UNORM -DXGI_FORMAT_BC7_UNORM_SRGB = DXGI_FORMAT.BC7_UNORM_SRGB - - -class DdsImageFile(ImageFile.ImageFile): - format = "DDS" - format_description = "DirectDraw Surface" - - def _open(self) -> None: - if not _accept(self.fp.read(4)): - msg = "not a DDS file" - raise SyntaxError(msg) - (header_size,) = struct.unpack(" None: - pass - - -class DdsRgbDecoder(ImageFile.PyDecoder): - _pulls_fd = True - - def decode(self, buffer: bytes) -> tuple[int, int]: - assert self.fd is not None - bitcount, masks = self.args - - # Some masks will be padded with zeros, e.g. R 0b11 G 0b1100 - # Calculate how many zeros each mask is padded with - mask_offsets = [] - # And the maximum value of each channel without the padding - mask_totals = [] - for mask in masks: - offset = 0 - if mask != 0: - while mask >> (offset + 1) << (offset + 1) == mask: - offset += 1 - mask_offsets.append(offset) - mask_totals.append(mask >> offset) - - data = bytearray() - bytecount = bitcount // 8 - dest_length = self.state.xsize * self.state.ysize * len(masks) - while len(data) < dest_length: - value = int.from_bytes(self.fd.read(bytecount), "little") - for i, mask in enumerate(masks): - masked_value = value & mask - # Remove the zero padding, and scale it to 8 bits - data += o8( - int(((masked_value >> mask_offsets[i]) / mask_totals[i]) * 255) - ) - self.set_as_raw(data) - return -1, 0 - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - if im.mode not in ("RGB", "RGBA", "L", "LA"): - 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 - bitcount = len(im.getbands()) * 8 - pitch = (im.width * bitcount + 7) // 8 - - fp.write( - o32(DDS_MAGIC) - + struct.pack( - "<7I", - 124, # header size - flags, # flags - im.height, - im.width, - pitch, - 0, # depth - 0, # mipmaps - ) - + struct.pack("11I", *((0,) * 11)) # reserved - # pfsize, pfflags, fourcc, bitcount - + struct.pack("<4I", 32, pixel_flags, 0, 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))] - ) - - -def _accept(prefix: bytes) -> bool: - return prefix[:4] == b"DDS " - - -Image.register_open(DdsImageFile.format, DdsImageFile, _accept) -Image.register_decoder("dds_rgb", DdsRgbDecoder) -Image.register_save(DdsImageFile.format, _save) -Image.register_extension(DdsImageFile.format, ".dds") +""" +A Pillow loader for .dds files (S3TC-compressed aka DXTC) +Jerome Leclanche + +Documentation: +https://web.archive.org/web/20170802060935/http://oss.sgi.com/projects/ogl-sample/registry/EXT/texture_compression_s3tc.txt + +The contents of this file are hereby released in the public domain (CC0) +Full text of the CC0 license: +https://creativecommons.org/publicdomain/zero/1.0/ +""" + +from __future__ import annotations + +import io +import struct +import sys +from enum import IntEnum, IntFlag +from typing import IO + +from . import Image, ImageFile, ImagePalette +from ._binary import i32le as i32 +from ._binary import o8 +from ._binary import o32le as o32 + +# Magic ("DDS ") +DDS_MAGIC = 0x20534444 + + +# DDS flags +class DDSD(IntFlag): + CAPS = 0x1 + HEIGHT = 0x2 + WIDTH = 0x4 + PITCH = 0x8 + PIXELFORMAT = 0x1000 + MIPMAPCOUNT = 0x20000 + LINEARSIZE = 0x80000 + DEPTH = 0x800000 + + +# DDS caps +class DDSCAPS(IntFlag): + COMPLEX = 0x8 + TEXTURE = 0x1000 + MIPMAP = 0x400000 + + +class DDSCAPS2(IntFlag): + CUBEMAP = 0x200 + CUBEMAP_POSITIVEX = 0x400 + CUBEMAP_NEGATIVEX = 0x800 + CUBEMAP_POSITIVEY = 0x1000 + CUBEMAP_NEGATIVEY = 0x2000 + CUBEMAP_POSITIVEZ = 0x4000 + CUBEMAP_NEGATIVEZ = 0x8000 + VOLUME = 0x200000 + + +# Pixel Format +class DDPF(IntFlag): + ALPHAPIXELS = 0x1 + ALPHA = 0x2 + FOURCC = 0x4 + PALETTEINDEXED8 = 0x20 + RGB = 0x40 + LUMINANCE = 0x20000 + + +# dxgiformat.h +class DXGI_FORMAT(IntEnum): + UNKNOWN = 0 + R32G32B32A32_TYPELESS = 1 + R32G32B32A32_FLOAT = 2 + R32G32B32A32_UINT = 3 + R32G32B32A32_SINT = 4 + R32G32B32_TYPELESS = 5 + R32G32B32_FLOAT = 6 + R32G32B32_UINT = 7 + R32G32B32_SINT = 8 + R16G16B16A16_TYPELESS = 9 + R16G16B16A16_FLOAT = 10 + R16G16B16A16_UNORM = 11 + R16G16B16A16_UINT = 12 + R16G16B16A16_SNORM = 13 + R16G16B16A16_SINT = 14 + R32G32_TYPELESS = 15 + R32G32_FLOAT = 16 + R32G32_UINT = 17 + R32G32_SINT = 18 + R32G8X24_TYPELESS = 19 + D32_FLOAT_S8X24_UINT = 20 + R32_FLOAT_X8X24_TYPELESS = 21 + X32_TYPELESS_G8X24_UINT = 22 + R10G10B10A2_TYPELESS = 23 + R10G10B10A2_UNORM = 24 + R10G10B10A2_UINT = 25 + R11G11B10_FLOAT = 26 + R8G8B8A8_TYPELESS = 27 + R8G8B8A8_UNORM = 28 + R8G8B8A8_UNORM_SRGB = 29 + R8G8B8A8_UINT = 30 + R8G8B8A8_SNORM = 31 + R8G8B8A8_SINT = 32 + R16G16_TYPELESS = 33 + R16G16_FLOAT = 34 + R16G16_UNORM = 35 + R16G16_UINT = 36 + R16G16_SNORM = 37 + R16G16_SINT = 38 + R32_TYPELESS = 39 + D32_FLOAT = 40 + R32_FLOAT = 41 + R32_UINT = 42 + R32_SINT = 43 + R24G8_TYPELESS = 44 + D24_UNORM_S8_UINT = 45 + R24_UNORM_X8_TYPELESS = 46 + X24_TYPELESS_G8_UINT = 47 + R8G8_TYPELESS = 48 + R8G8_UNORM = 49 + R8G8_UINT = 50 + R8G8_SNORM = 51 + R8G8_SINT = 52 + R16_TYPELESS = 53 + R16_FLOAT = 54 + D16_UNORM = 55 + R16_UNORM = 56 + R16_UINT = 57 + R16_SNORM = 58 + R16_SINT = 59 + R8_TYPELESS = 60 + R8_UNORM = 61 + R8_UINT = 62 + R8_SNORM = 63 + R8_SINT = 64 + A8_UNORM = 65 + R1_UNORM = 66 + R9G9B9E5_SHAREDEXP = 67 + R8G8_B8G8_UNORM = 68 + G8R8_G8B8_UNORM = 69 + BC1_TYPELESS = 70 + BC1_UNORM = 71 + BC1_UNORM_SRGB = 72 + BC2_TYPELESS = 73 + BC2_UNORM = 74 + BC2_UNORM_SRGB = 75 + BC3_TYPELESS = 76 + BC3_UNORM = 77 + BC3_UNORM_SRGB = 78 + BC4_TYPELESS = 79 + BC4_UNORM = 80 + BC4_SNORM = 81 + BC5_TYPELESS = 82 + BC5_UNORM = 83 + BC5_SNORM = 84 + B5G6R5_UNORM = 85 + B5G5R5A1_UNORM = 86 + B8G8R8A8_UNORM = 87 + B8G8R8X8_UNORM = 88 + R10G10B10_XR_BIAS_A2_UNORM = 89 + B8G8R8A8_TYPELESS = 90 + B8G8R8A8_UNORM_SRGB = 91 + B8G8R8X8_TYPELESS = 92 + B8G8R8X8_UNORM_SRGB = 93 + BC6H_TYPELESS = 94 + BC6H_UF16 = 95 + BC6H_SF16 = 96 + BC7_TYPELESS = 97 + BC7_UNORM = 98 + BC7_UNORM_SRGB = 99 + AYUV = 100 + Y410 = 101 + Y416 = 102 + NV12 = 103 + P010 = 104 + P016 = 105 + OPAQUE_420 = 106 + YUY2 = 107 + Y210 = 108 + Y216 = 109 + NV11 = 110 + AI44 = 111 + IA44 = 112 + P8 = 113 + A8P8 = 114 + B4G4R4A4_UNORM = 115 + P208 = 130 + V208 = 131 + V408 = 132 + SAMPLER_FEEDBACK_MIN_MIP_OPAQUE = 189 + SAMPLER_FEEDBACK_MIP_REGION_USED_OPAQUE = 190 + + +class D3DFMT(IntEnum): + UNKNOWN = 0 + R8G8B8 = 20 + A8R8G8B8 = 21 + X8R8G8B8 = 22 + R5G6B5 = 23 + X1R5G5B5 = 24 + A1R5G5B5 = 25 + A4R4G4B4 = 26 + R3G3B2 = 27 + A8 = 28 + A8R3G3B2 = 29 + X4R4G4B4 = 30 + A2B10G10R10 = 31 + A8B8G8R8 = 32 + X8B8G8R8 = 33 + G16R16 = 34 + A2R10G10B10 = 35 + A16B16G16R16 = 36 + A8P8 = 40 + P8 = 41 + L8 = 50 + A8L8 = 51 + A4L4 = 52 + V8U8 = 60 + L6V5U5 = 61 + X8L8V8U8 = 62 + Q8W8V8U8 = 63 + V16U16 = 64 + A2W10V10U10 = 67 + D16_LOCKABLE = 70 + D32 = 71 + D15S1 = 73 + D24S8 = 75 + D24X8 = 77 + D24X4S4 = 79 + D16 = 80 + D32F_LOCKABLE = 82 + D24FS8 = 83 + D32_LOCKABLE = 84 + S8_LOCKABLE = 85 + L16 = 81 + VERTEXDATA = 100 + INDEX16 = 101 + INDEX32 = 102 + Q16W16V16U16 = 110 + R16F = 111 + G16R16F = 112 + A16B16G16R16F = 113 + R32F = 114 + G32R32F = 115 + A32B32G32R32F = 116 + CxV8U8 = 117 + A1 = 118 + A2B10G10R10_XR_BIAS = 119 + BINARYBUFFER = 199 + + UYVY = i32(b"UYVY") + R8G8_B8G8 = i32(b"RGBG") + YUY2 = i32(b"YUY2") + G8R8_G8B8 = i32(b"GRGB") + DXT1 = i32(b"DXT1") + DXT2 = i32(b"DXT2") + DXT3 = i32(b"DXT3") + DXT4 = i32(b"DXT4") + DXT5 = i32(b"DXT5") + DX10 = i32(b"DX10") + BC4S = i32(b"BC4S") + BC4U = i32(b"BC4U") + BC5S = i32(b"BC5S") + BC5U = i32(b"BC5U") + ATI1 = i32(b"ATI1") + ATI2 = i32(b"ATI2") + MULTI2_ARGB8 = i32(b"MET1") + + +# Backward compatibility layer +module = sys.modules[__name__] +for item in DDSD: + assert item.name is not None + setattr(module, f"DDSD_{item.name}", item.value) +for item1 in DDSCAPS: + assert item1.name is not None + setattr(module, f"DDSCAPS_{item1.name}", item1.value) +for item2 in DDSCAPS2: + assert item2.name is not None + setattr(module, f"DDSCAPS2_{item2.name}", item2.value) +for item3 in DDPF: + assert item3.name is not None + setattr(module, f"DDPF_{item3.name}", item3.value) + +DDS_FOURCC = DDPF.FOURCC +DDS_RGB = DDPF.RGB +DDS_RGBA = DDPF.RGB | DDPF.ALPHAPIXELS +DDS_LUMINANCE = DDPF.LUMINANCE +DDS_LUMINANCEA = DDPF.LUMINANCE | DDPF.ALPHAPIXELS +DDS_ALPHA = DDPF.ALPHA +DDS_PAL8 = DDPF.PALETTEINDEXED8 + +DDS_HEADER_FLAGS_TEXTURE = DDSD.CAPS | DDSD.HEIGHT | DDSD.WIDTH | DDSD.PIXELFORMAT +DDS_HEADER_FLAGS_MIPMAP = DDSD.MIPMAPCOUNT +DDS_HEADER_FLAGS_VOLUME = DDSD.DEPTH +DDS_HEADER_FLAGS_PITCH = DDSD.PITCH +DDS_HEADER_FLAGS_LINEARSIZE = DDSD.LINEARSIZE + +DDS_HEIGHT = DDSD.HEIGHT +DDS_WIDTH = DDSD.WIDTH + +DDS_SURFACE_FLAGS_TEXTURE = DDSCAPS.TEXTURE +DDS_SURFACE_FLAGS_MIPMAP = DDSCAPS.COMPLEX | DDSCAPS.MIPMAP +DDS_SURFACE_FLAGS_CUBEMAP = DDSCAPS.COMPLEX + +DDS_CUBEMAP_POSITIVEX = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_POSITIVEX +DDS_CUBEMAP_NEGATIVEX = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_NEGATIVEX +DDS_CUBEMAP_POSITIVEY = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_POSITIVEY +DDS_CUBEMAP_NEGATIVEY = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_NEGATIVEY +DDS_CUBEMAP_POSITIVEZ = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_POSITIVEZ +DDS_CUBEMAP_NEGATIVEZ = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_NEGATIVEZ + +DXT1_FOURCC = D3DFMT.DXT1 +DXT3_FOURCC = D3DFMT.DXT3 +DXT5_FOURCC = D3DFMT.DXT5 + +DXGI_FORMAT_R8G8B8A8_TYPELESS = DXGI_FORMAT.R8G8B8A8_TYPELESS +DXGI_FORMAT_R8G8B8A8_UNORM = DXGI_FORMAT.R8G8B8A8_UNORM +DXGI_FORMAT_R8G8B8A8_UNORM_SRGB = DXGI_FORMAT.R8G8B8A8_UNORM_SRGB +DXGI_FORMAT_BC5_TYPELESS = DXGI_FORMAT.BC5_TYPELESS +DXGI_FORMAT_BC5_UNORM = DXGI_FORMAT.BC5_UNORM +DXGI_FORMAT_BC5_SNORM = DXGI_FORMAT.BC5_SNORM +DXGI_FORMAT_BC6H_UF16 = DXGI_FORMAT.BC6H_UF16 +DXGI_FORMAT_BC6H_SF16 = DXGI_FORMAT.BC6H_SF16 +DXGI_FORMAT_BC7_TYPELESS = DXGI_FORMAT.BC7_TYPELESS +DXGI_FORMAT_BC7_UNORM = DXGI_FORMAT.BC7_UNORM +DXGI_FORMAT_BC7_UNORM_SRGB = DXGI_FORMAT.BC7_UNORM_SRGB + + +class DdsImageFile(ImageFile.ImageFile): + format = "DDS" + format_description = "DirectDraw Surface" + + def _open(self) -> None: + if not _accept(self.fp.read(4)): + msg = "not a DDS file" + raise SyntaxError(msg) + (header_size,) = struct.unpack(" None: + pass + + +class DdsRgbDecoder(ImageFile.PyDecoder): + _pulls_fd = True + + def decode(self, buffer: bytes) -> tuple[int, int]: + assert self.fd is not None + bitcount, masks = self.args + + # Some masks will be padded with zeros, e.g. R 0b11 G 0b1100 + # Calculate how many zeros each mask is padded with + mask_offsets = [] + # And the maximum value of each channel without the padding + mask_totals = [] + for mask in masks: + offset = 0 + if mask != 0: + while mask >> (offset + 1) << (offset + 1) == mask: + offset += 1 + mask_offsets.append(offset) + mask_totals.append(mask >> offset) + + data = bytearray() + bytecount = bitcount // 8 + dest_length = self.state.xsize * self.state.ysize * len(masks) + while len(data) < dest_length: + value = int.from_bytes(self.fd.read(bytecount), "little") + for i, mask in enumerate(masks): + masked_value = value & mask + # Remove the zero padding, and scale it to 8 bits + data += o8( + int(((masked_value >> mask_offsets[i]) / mask_totals[i]) * 255) + ) + self.set_as_raw(data) + return -1, 0 + + +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + if im.mode not in ("RGB", "RGBA", "L", "LA"): + 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 + bitcount = len(im.getbands()) * 8 + pitch = (im.width * bitcount + 7) // 8 + + fp.write( + o32(DDS_MAGIC) + + struct.pack( + "<7I", + 124, # header size + flags, # flags + im.height, + im.width, + pitch, + 0, # depth + 0, # mipmaps + ) + + struct.pack("11I", *((0,) * 11)) # reserved + # pfsize, pfflags, fourcc, bitcount + + struct.pack("<4I", 32, pixel_flags, 0, 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))] + ) + + +def _accept(prefix: bytes) -> bool: + return prefix[:4] == b"DDS " + + +Image.register_open(DdsImageFile.format, DdsImageFile, _accept) +Image.register_decoder("dds_rgb", DdsRgbDecoder) +Image.register_save(DdsImageFile.format, _save) +Image.register_extension(DdsImageFile.format, ".dds") diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 380b1cf0e..959e1d066 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -1,473 +1,473 @@ -# -# The Python Imaging Library. -# $Id$ -# -# EPS file handling -# -# History: -# 1995-09-01 fl Created (0.1) -# 1996-05-18 fl Don't choke on "atend" fields, Ghostscript interface (0.2) -# 1996-08-22 fl Don't choke on floating point BoundingBox values -# 1996-08-23 fl Handle files from Macintosh (0.3) -# 2001-02-17 fl Use 're' instead of 'regex' (Python 2.1) (0.4) -# 2003-09-07 fl Check gs.close status (from Federico Di Gregorio) (0.5) -# 2014-05-07 e Handling of EPS with binary preview and fixed resolution -# resizing -# -# 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. -# -from __future__ import annotations - -import io -import os -import re -import subprocess -import sys -import tempfile -from typing import IO - -from . import Image, ImageFile -from ._binary import i32le as i32 -from ._deprecate import deprecate - -# -------------------------------------------------------------------- - - -split = re.compile(r"^%%([^:]*):[ \t]*(.*)[ \t]*$") -field = re.compile(r"^%[%!\w]([^:]*)[ \t]*$") - -gs_binary: str | bool | None = None -gs_windows_binary = None - - -def has_ghostscript() -> bool: - global gs_binary, gs_windows_binary - if gs_binary is None: - if sys.platform.startswith("win"): - if gs_windows_binary is None: - import shutil - - for binary in ("gswin32c", "gswin64c", "gs"): - if shutil.which(binary) is not None: - gs_windows_binary = binary - break - else: - gs_windows_binary = False - gs_binary = gs_windows_binary - else: - try: - subprocess.check_call(["gs", "--version"], stdout=subprocess.DEVNULL) - gs_binary = "gs" - except OSError: - gs_binary = False - return gs_binary is not False - - -def Ghostscript(tile, size, fp, scale=1, transparency=False): - """Render an image using Ghostscript""" - global gs_binary - if not has_ghostscript(): - msg = "Unable to locate Ghostscript on paths" - raise OSError(msg) - - # Unpack decoder tile - decoder, tile, offset, data = tile[0] - length, bbox = data - - # Hack to support hi-res rendering - scale = int(scale) or 1 - width = size[0] * scale - height = size[1] * scale - # resolution is dependent on bbox and size - res_x = 72.0 * width / (bbox[2] - bbox[0]) - res_y = 72.0 * height / (bbox[3] - bbox[1]) - - out_fd, outfile = tempfile.mkstemp() - os.close(out_fd) - - infile_temp = None - if hasattr(fp, "name") and os.path.exists(fp.name): - infile = fp.name - else: - in_fd, infile_temp = tempfile.mkstemp() - os.close(in_fd) - infile = infile_temp - - # Ignore length and offset! - # Ghostscript can read it - # Copy whole file to read in Ghostscript - with open(infile_temp, "wb") as f: - # fetch length of fp - fp.seek(0, io.SEEK_END) - fsize = fp.tell() - # ensure start position - # go back - fp.seek(0) - lengthfile = fsize - while lengthfile > 0: - s = fp.read(min(lengthfile, 100 * 1024)) - if not s: - break - lengthfile -= len(s) - f.write(s) - - device = "pngalpha" if transparency else "ppmraw" - - # Build Ghostscript command - command = [ - gs_binary, - "-q", # quiet mode - f"-g{width:d}x{height:d}", # set output geometry (pixels) - f"-r{res_x:f}x{res_y:f}", # set input DPI (dots per inch) - "-dBATCH", # exit after processing - "-dNOPAUSE", # don't pause between pages - "-dSAFER", # safe mode - f"-sDEVICE={device}", - f"-sOutputFile={outfile}", # output file - # adjust for image origin - "-c", - f"{-bbox[0]} {-bbox[1]} translate", - "-f", - infile, # input file - # showpage (see https://bugs.ghostscript.com/show_bug.cgi?id=698272) - "-c", - "showpage", - ] - - # push data through Ghostscript - try: - startupinfo = None - if sys.platform.startswith("win"): - startupinfo = subprocess.STARTUPINFO() - startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW - subprocess.check_call(command, startupinfo=startupinfo) - out_im = Image.open(outfile) - out_im.load() - finally: - try: - os.unlink(outfile) - if infile_temp: - os.unlink(infile_temp) - except OSError: - pass - - im = out_im.im.copy() - out_im.close() - return im - - -class PSFile: - """ - Wrapper for bytesio object that treats either CR or LF as end of line. - This class is no longer used internally, but kept for backwards compatibility. - """ - - def __init__(self, fp): - deprecate( - "PSFile", - 11, - action="If you need the functionality of this class " - "you will need to implement it yourself.", - ) - self.fp = fp - self.char = None - - def seek(self, offset, whence=io.SEEK_SET): - self.char = None - self.fp.seek(offset, whence) - - def readline(self) -> str: - s = [self.char or b""] - self.char = None - - c = self.fp.read(1) - while (c not in b"\r\n") and len(c): - s.append(c) - c = self.fp.read(1) - - self.char = self.fp.read(1) - # line endings can be 1 or 2 of \r \n, in either order - if self.char in b"\r\n": - self.char = None - - return b"".join(s).decode("latin-1") - - -def _accept(prefix: bytes) -> bool: - return prefix[:4] == b"%!PS" or (len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5) - - -## -# Image plugin for Encapsulated PostScript. This plugin supports only -# a few variants of this format. - - -class EpsImageFile(ImageFile.ImageFile): - """EPS File Parser for the Python Imaging Library""" - - format = "EPS" - format_description = "Encapsulated Postscript" - - mode_map = {1: "L", 2: "LAB", 3: "RGB", 4: "CMYK"} - - def _open(self) -> None: - (length, offset) = self._find_offset(self.fp) - - # go to offset - start of "%!PS" - self.fp.seek(offset) - - self._mode = "RGB" - self._size = None - - byte_arr = bytearray(255) - bytes_mv = memoryview(byte_arr) - bytes_read = 0 - reading_header_comments = True - reading_trailer_comments = False - trailer_reached = False - - def check_required_header_comments() -> None: - if "PS-Adobe" not in self.info: - msg = 'EPS header missing "%!PS-Adobe" comment' - raise SyntaxError(msg) - if "BoundingBox" not in self.info: - msg = 'EPS header missing "%%BoundingBox" comment' - raise SyntaxError(msg) - - def _read_comment(s: str) -> bool: - nonlocal reading_trailer_comments - try: - m = split.match(s) - except re.error as e: - msg = "not an EPS file" - raise SyntaxError(msg) from e - - if not m: - return False - - k, v = m.group(1, 2) - self.info[k] = v - if k == "BoundingBox": - if v == "(atend)": - reading_trailer_comments = True - elif not self._size or (trailer_reached and reading_trailer_comments): - try: - # Note: The DSC spec says that BoundingBox - # fields should be integers, but some drivers - # put floating point values there anyway. - box = [int(float(i)) for i in v.split()] - self._size = box[2] - box[0], box[3] - box[1] - self.tile = [("eps", (0, 0) + self.size, offset, (length, box))] - except Exception: - pass - return True - - while True: - byte = self.fp.read(1) - if byte == b"": - # if we didn't read a byte we must be at the end of the file - if bytes_read == 0: - break - elif byte in b"\r\n": - # if we read a line ending character, ignore it and parse what - # we have already read. if we haven't read any other characters, - # continue reading - if bytes_read == 0: - continue - else: - # ASCII/hexadecimal lines in an EPS file must not exceed - # 255 characters, not including line ending characters - if bytes_read >= 255: - # only enforce this for lines starting with a "%", - # otherwise assume it's binary data - if byte_arr[0] == ord("%"): - msg = "not an EPS file" - raise SyntaxError(msg) - else: - if reading_header_comments: - check_required_header_comments() - reading_header_comments = False - # reset bytes_read so we can keep reading - # data until the end of the line - bytes_read = 0 - byte_arr[bytes_read] = byte[0] - bytes_read += 1 - continue - - if reading_header_comments: - # Load EPS header - - # if this line doesn't start with a "%", - # or does start with "%%EndComments", - # then we've reached the end of the header/comments - if byte_arr[0] != ord("%") or bytes_mv[:13] == b"%%EndComments": - check_required_header_comments() - reading_header_comments = False - continue - - s = str(bytes_mv[:bytes_read], "latin-1") - if not _read_comment(s): - m = field.match(s) - if m: - k = m.group(1) - if k[:8] == "PS-Adobe": - self.info["PS-Adobe"] = k[9:] - else: - self.info[k] = "" - elif s[0] == "%": - # handle non-DSC PostScript comments that some - # tools mistakenly put in the Comments section - pass - else: - msg = "bad EPS header" - raise OSError(msg) - elif bytes_mv[:11] == b"%ImageData:": - # Check for an "ImageData" descriptor - # https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577413_pgfId-1035096 - - # Values: - # columns - # rows - # bit depth (1 or 8) - # mode (1: L, 2: LAB, 3: RGB, 4: CMYK) - # number of padding channels - # block size (number of bytes per row per channel) - # binary/ascii (1: binary, 2: ascii) - # data start identifier (the image data follows after a single line - # consisting only of this quoted value) - image_data_values = byte_arr[11:bytes_read].split(None, 7) - columns, rows, bit_depth, mode_id = ( - int(value) for value in image_data_values[:4] - ) - - if bit_depth == 1: - self._mode = "1" - elif bit_depth == 8: - try: - self._mode = self.mode_map[mode_id] - except ValueError: - break - else: - break - - self._size = columns, rows - return - elif bytes_mv[:5] == b"%%EOF": - break - elif trailer_reached and reading_trailer_comments: - # Load EPS trailer - s = str(bytes_mv[:bytes_read], "latin-1") - _read_comment(s) - elif bytes_mv[:9] == b"%%Trailer": - trailer_reached = True - bytes_read = 0 - - check_required_header_comments() - - if not self._size: - msg = "cannot determine EPS bounding box" - raise OSError(msg) - - def _find_offset(self, fp): - s = fp.read(4) - - if s == b"%!PS": - # for HEAD without binary preview - fp.seek(0, io.SEEK_END) - length = fp.tell() - offset = 0 - elif i32(s) == 0xC6D3D0C5: - # FIX for: Some EPS file not handled correctly / issue #302 - # EPS can contain binary data - # or start directly with latin coding - # more info see: - # https://web.archive.org/web/20160528181353/http://partners.adobe.com/public/developer/en/ps/5002.EPSF_Spec.pdf - s = fp.read(8) - offset = i32(s) - length = i32(s, 4) - else: - msg = "not an EPS file" - raise SyntaxError(msg) - - return length, offset - - def load(self, scale=1, transparency=False): - # Load EPS via Ghostscript - if self.tile: - self.im = Ghostscript(self.tile, self.size, self.fp, scale, transparency) - self._mode = self.im.mode - self._size = self.im.size - self.tile = [] - return Image.Image.load(self) - - def load_seek(self, pos: int) -> None: - # we can't incrementally load, so force ImageFile.parser to - # use our custom load method by defining this method. - pass - - -# -------------------------------------------------------------------- - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes, eps: int = 1) -> None: - """EPS Writer for the Python Imaging Library.""" - - # make sure image data is available - im.load() - - # determine PostScript image mode - if im.mode == "L": - operator = (8, 1, b"image") - elif im.mode == "RGB": - operator = (8, 3, b"false 3 colorimage") - elif im.mode == "CMYK": - operator = (8, 4, b"false 4 colorimage") - else: - msg = "image mode is not supported" - raise ValueError(msg) - - if eps: - # write EPS header - fp.write(b"%!PS-Adobe-3.0 EPSF-3.0\n") - fp.write(b"%%Creator: PIL 0.1 EpsEncode\n") - # fp.write("%%CreationDate: %s"...) - fp.write(b"%%%%BoundingBox: 0 0 %d %d\n" % im.size) - fp.write(b"%%Pages: 1\n") - fp.write(b"%%EndComments\n") - fp.write(b"%%Page: 1 1\n") - fp.write(b"%%ImageData: %d %d " % im.size) - fp.write(b'%d %d 0 1 1 "%s"\n' % operator) - - # image header - fp.write(b"gsave\n") - fp.write(b"10 dict begin\n") - fp.write(b"/buf %d string def\n" % (im.size[0] * operator[1])) - fp.write(b"%d %d scale\n" % im.size) - fp.write(b"%d %d 8\n" % im.size) # <= bits - fp.write(b"[%d 0 0 -%d 0 %d]\n" % (im.size[0], im.size[1], im.size[1])) - fp.write(b"{ currentfile buf readhexstring pop } bind\n") - fp.write(operator[2] + b"\n") - if hasattr(fp, "flush"): - fp.flush() - - ImageFile._save(im, fp, [("eps", (0, 0) + im.size, 0, None)]) - - fp.write(b"\n%%%%EndBinary\n") - fp.write(b"grestore end\n") - if hasattr(fp, "flush"): - fp.flush() - - -# -------------------------------------------------------------------- - - -Image.register_open(EpsImageFile.format, EpsImageFile, _accept) - -Image.register_save(EpsImageFile.format, _save) - -Image.register_extensions(EpsImageFile.format, [".ps", ".eps"]) - -Image.register_mime(EpsImageFile.format, "application/postscript") +# +# The Python Imaging Library. +# $Id$ +# +# EPS file handling +# +# History: +# 1995-09-01 fl Created (0.1) +# 1996-05-18 fl Don't choke on "atend" fields, Ghostscript interface (0.2) +# 1996-08-22 fl Don't choke on floating point BoundingBox values +# 1996-08-23 fl Handle files from Macintosh (0.3) +# 2001-02-17 fl Use 're' instead of 'regex' (Python 2.1) (0.4) +# 2003-09-07 fl Check gs.close status (from Federico Di Gregorio) (0.5) +# 2014-05-07 e Handling of EPS with binary preview and fixed resolution +# resizing +# +# 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. +# +from __future__ import annotations + +import io +import os +import re +import subprocess +import sys +import tempfile +from typing import IO + +from . import Image, ImageFile +from ._binary import i32le as i32 +from ._deprecate import deprecate + +# -------------------------------------------------------------------- + + +split = re.compile(r"^%%([^:]*):[ \t]*(.*)[ \t]*$") +field = re.compile(r"^%[%!\w]([^:]*)[ \t]*$") + +gs_binary: str | bool | None = None +gs_windows_binary = None + + +def has_ghostscript() -> bool: + global gs_binary, gs_windows_binary + if gs_binary is None: + if sys.platform.startswith("win"): + if gs_windows_binary is None: + import shutil + + for binary in ("gswin32c", "gswin64c", "gs"): + if shutil.which(binary) is not None: + gs_windows_binary = binary + break + else: + gs_windows_binary = False + gs_binary = gs_windows_binary + else: + try: + subprocess.check_call(["gs", "--version"], stdout=subprocess.DEVNULL) + gs_binary = "gs" + except OSError: + gs_binary = False + return gs_binary is not False + + +def Ghostscript(tile, size, fp, scale=1, transparency=False): + """Render an image using Ghostscript""" + global gs_binary + if not has_ghostscript(): + msg = "Unable to locate Ghostscript on paths" + raise OSError(msg) + + # Unpack decoder tile + decoder, tile, offset, data = tile[0] + length, bbox = data + + # Hack to support hi-res rendering + scale = int(scale) or 1 + width = size[0] * scale + height = size[1] * scale + # resolution is dependent on bbox and size + res_x = 72.0 * width / (bbox[2] - bbox[0]) + res_y = 72.0 * height / (bbox[3] - bbox[1]) + + out_fd, outfile = tempfile.mkstemp() + os.close(out_fd) + + infile_temp = None + if hasattr(fp, "name") and os.path.exists(fp.name): + infile = fp.name + else: + in_fd, infile_temp = tempfile.mkstemp() + os.close(in_fd) + infile = infile_temp + + # Ignore length and offset! + # Ghostscript can read it + # Copy whole file to read in Ghostscript + with open(infile_temp, "wb") as f: + # fetch length of fp + fp.seek(0, io.SEEK_END) + fsize = fp.tell() + # ensure start position + # go back + fp.seek(0) + lengthfile = fsize + while lengthfile > 0: + s = fp.read(min(lengthfile, 100 * 1024)) + if not s: + break + lengthfile -= len(s) + f.write(s) + + device = "pngalpha" if transparency else "ppmraw" + + # Build Ghostscript command + command = [ + gs_binary, + "-q", # quiet mode + f"-g{width:d}x{height:d}", # set output geometry (pixels) + f"-r{res_x:f}x{res_y:f}", # set input DPI (dots per inch) + "-dBATCH", # exit after processing + "-dNOPAUSE", # don't pause between pages + "-dSAFER", # safe mode + f"-sDEVICE={device}", + f"-sOutputFile={outfile}", # output file + # adjust for image origin + "-c", + f"{-bbox[0]} {-bbox[1]} translate", + "-f", + infile, # input file + # showpage (see https://bugs.ghostscript.com/show_bug.cgi?id=698272) + "-c", + "showpage", + ] + + # push data through Ghostscript + try: + startupinfo = None + if sys.platform.startswith("win"): + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + subprocess.check_call(command, startupinfo=startupinfo) + out_im = Image.open(outfile) + out_im.load() + finally: + try: + os.unlink(outfile) + if infile_temp: + os.unlink(infile_temp) + except OSError: + pass + + im = out_im.im.copy() + out_im.close() + return im + + +class PSFile: + """ + Wrapper for bytesio object that treats either CR or LF as end of line. + This class is no longer used internally, but kept for backwards compatibility. + """ + + def __init__(self, fp): + deprecate( + "PSFile", + 11, + action="If you need the functionality of this class " + "you will need to implement it yourself.", + ) + self.fp = fp + self.char = None + + def seek(self, offset, whence=io.SEEK_SET): + self.char = None + self.fp.seek(offset, whence) + + def readline(self) -> str: + s = [self.char or b""] + self.char = None + + c = self.fp.read(1) + while (c not in b"\r\n") and len(c): + s.append(c) + c = self.fp.read(1) + + self.char = self.fp.read(1) + # line endings can be 1 or 2 of \r \n, in either order + if self.char in b"\r\n": + self.char = None + + return b"".join(s).decode("latin-1") + + +def _accept(prefix: bytes) -> bool: + return prefix[:4] == b"%!PS" or (len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5) + + +## +# Image plugin for Encapsulated PostScript. This plugin supports only +# a few variants of this format. + + +class EpsImageFile(ImageFile.ImageFile): + """EPS File Parser for the Python Imaging Library""" + + format = "EPS" + format_description = "Encapsulated Postscript" + + mode_map = {1: "L", 2: "LAB", 3: "RGB", 4: "CMYK"} + + def _open(self) -> None: + (length, offset) = self._find_offset(self.fp) + + # go to offset - start of "%!PS" + self.fp.seek(offset) + + self._mode = "RGB" + self._size = None + + byte_arr = bytearray(255) + bytes_mv = memoryview(byte_arr) + bytes_read = 0 + reading_header_comments = True + reading_trailer_comments = False + trailer_reached = False + + def check_required_header_comments() -> None: + if "PS-Adobe" not in self.info: + msg = 'EPS header missing "%!PS-Adobe" comment' + raise SyntaxError(msg) + if "BoundingBox" not in self.info: + msg = 'EPS header missing "%%BoundingBox" comment' + raise SyntaxError(msg) + + def _read_comment(s: str) -> bool: + nonlocal reading_trailer_comments + try: + m = split.match(s) + except re.error as e: + msg = "not an EPS file" + raise SyntaxError(msg) from e + + if not m: + return False + + k, v = m.group(1, 2) + self.info[k] = v + if k == "BoundingBox": + if v == "(atend)": + reading_trailer_comments = True + elif not self._size or (trailer_reached and reading_trailer_comments): + try: + # Note: The DSC spec says that BoundingBox + # fields should be integers, but some drivers + # put floating point values there anyway. + box = [int(float(i)) for i in v.split()] + self._size = box[2] - box[0], box[3] - box[1] + self.tile = [("eps", (0, 0) + self.size, offset, (length, box))] + except Exception: + pass + return True + + while True: + byte = self.fp.read(1) + if byte == b"": + # if we didn't read a byte we must be at the end of the file + if bytes_read == 0: + break + elif byte in b"\r\n": + # if we read a line ending character, ignore it and parse what + # we have already read. if we haven't read any other characters, + # continue reading + if bytes_read == 0: + continue + else: + # ASCII/hexadecimal lines in an EPS file must not exceed + # 255 characters, not including line ending characters + if bytes_read >= 255: + # only enforce this for lines starting with a "%", + # otherwise assume it's binary data + if byte_arr[0] == ord("%"): + msg = "not an EPS file" + raise SyntaxError(msg) + else: + if reading_header_comments: + check_required_header_comments() + reading_header_comments = False + # reset bytes_read so we can keep reading + # data until the end of the line + bytes_read = 0 + byte_arr[bytes_read] = byte[0] + bytes_read += 1 + continue + + if reading_header_comments: + # Load EPS header + + # if this line doesn't start with a "%", + # or does start with "%%EndComments", + # then we've reached the end of the header/comments + if byte_arr[0] != ord("%") or bytes_mv[:13] == b"%%EndComments": + check_required_header_comments() + reading_header_comments = False + continue + + s = str(bytes_mv[:bytes_read], "latin-1") + if not _read_comment(s): + m = field.match(s) + if m: + k = m.group(1) + if k[:8] == "PS-Adobe": + self.info["PS-Adobe"] = k[9:] + else: + self.info[k] = "" + elif s[0] == "%": + # handle non-DSC PostScript comments that some + # tools mistakenly put in the Comments section + pass + else: + msg = "bad EPS header" + raise OSError(msg) + elif bytes_mv[:11] == b"%ImageData:": + # Check for an "ImageData" descriptor + # https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577413_pgfId-1035096 + + # Values: + # columns + # rows + # bit depth (1 or 8) + # mode (1: L, 2: LAB, 3: RGB, 4: CMYK) + # number of padding channels + # block size (number of bytes per row per channel) + # binary/ascii (1: binary, 2: ascii) + # data start identifier (the image data follows after a single line + # consisting only of this quoted value) + image_data_values = byte_arr[11:bytes_read].split(None, 7) + columns, rows, bit_depth, mode_id = ( + int(value) for value in image_data_values[:4] + ) + + if bit_depth == 1: + self._mode = "1" + elif bit_depth == 8: + try: + self._mode = self.mode_map[mode_id] + except ValueError: + break + else: + break + + self._size = columns, rows + return + elif bytes_mv[:5] == b"%%EOF": + break + elif trailer_reached and reading_trailer_comments: + # Load EPS trailer + s = str(bytes_mv[:bytes_read], "latin-1") + _read_comment(s) + elif bytes_mv[:9] == b"%%Trailer": + trailer_reached = True + bytes_read = 0 + + check_required_header_comments() + + if not self._size: + msg = "cannot determine EPS bounding box" + raise OSError(msg) + + def _find_offset(self, fp): + s = fp.read(4) + + if s == b"%!PS": + # for HEAD without binary preview + fp.seek(0, io.SEEK_END) + length = fp.tell() + offset = 0 + elif i32(s) == 0xC6D3D0C5: + # FIX for: Some EPS file not handled correctly / issue #302 + # EPS can contain binary data + # or start directly with latin coding + # more info see: + # https://web.archive.org/web/20160528181353/http://partners.adobe.com/public/developer/en/ps/5002.EPSF_Spec.pdf + s = fp.read(8) + offset = i32(s) + length = i32(s, 4) + else: + msg = "not an EPS file" + raise SyntaxError(msg) + + return length, offset + + def load(self, scale=1, transparency=False): + # Load EPS via Ghostscript + if self.tile: + self.im = Ghostscript(self.tile, self.size, self.fp, scale, transparency) + self._mode = self.im.mode + self._size = self.im.size + self.tile = [] + return Image.Image.load(self) + + def load_seek(self, pos: int) -> None: + # we can't incrementally load, so force ImageFile.parser to + # use our custom load method by defining this method. + pass + + +# -------------------------------------------------------------------- + + +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes, eps: int = 1) -> None: + """EPS Writer for the Python Imaging Library.""" + + # make sure image data is available + im.load() + + # determine PostScript image mode + if im.mode == "L": + operator = (8, 1, b"image") + elif im.mode == "RGB": + operator = (8, 3, b"false 3 colorimage") + elif im.mode == "CMYK": + operator = (8, 4, b"false 4 colorimage") + else: + msg = "image mode is not supported" + raise ValueError(msg) + + if eps: + # write EPS header + fp.write(b"%!PS-Adobe-3.0 EPSF-3.0\n") + fp.write(b"%%Creator: PIL 0.1 EpsEncode\n") + # fp.write("%%CreationDate: %s"...) + fp.write(b"%%%%BoundingBox: 0 0 %d %d\n" % im.size) + fp.write(b"%%Pages: 1\n") + fp.write(b"%%EndComments\n") + fp.write(b"%%Page: 1 1\n") + fp.write(b"%%ImageData: %d %d " % im.size) + fp.write(b'%d %d 0 1 1 "%s"\n' % operator) + + # image header + fp.write(b"gsave\n") + fp.write(b"10 dict begin\n") + fp.write(b"/buf %d string def\n" % (im.size[0] * operator[1])) + fp.write(b"%d %d scale\n" % im.size) + fp.write(b"%d %d 8\n" % im.size) # <= bits + fp.write(b"[%d 0 0 -%d 0 %d]\n" % (im.size[0], im.size[1], im.size[1])) + fp.write(b"{ currentfile buf readhexstring pop } bind\n") + fp.write(operator[2] + b"\n") + if hasattr(fp, "flush"): + fp.flush() + + ImageFile._save(im, fp, [("eps", (0, 0) + im.size, 0, None)]) + + fp.write(b"\n%%%%EndBinary\n") + fp.write(b"grestore end\n") + if hasattr(fp, "flush"): + fp.flush() + + +# -------------------------------------------------------------------- + + +Image.register_open(EpsImageFile.format, EpsImageFile, _accept) + +Image.register_save(EpsImageFile.format, _save) + +Image.register_extensions(EpsImageFile.format, [".ps", ".eps"]) + +Image.register_mime(EpsImageFile.format, "application/postscript") diff --git a/src/PIL/ExifTags.py b/src/PIL/ExifTags.py index 39b4aa552..a4cbe3c48 100644 --- a/src/PIL/ExifTags.py +++ b/src/PIL/ExifTags.py @@ -1,381 +1,381 @@ -# -# The Python Imaging Library. -# $Id$ -# -# EXIF tags -# -# Copyright (c) 2003 by Secret Labs AB -# -# See the README file for information on usage and redistribution. -# - -""" -This module provides constants and clear-text names for various -well-known EXIF tags. -""" -from __future__ import annotations - -from enum import IntEnum - - -class Base(IntEnum): - # possibly incomplete - InteropIndex = 0x0001 - ProcessingSoftware = 0x000B - NewSubfileType = 0x00FE - SubfileType = 0x00FF - ImageWidth = 0x0100 - ImageLength = 0x0101 - BitsPerSample = 0x0102 - Compression = 0x0103 - PhotometricInterpretation = 0x0106 - Thresholding = 0x0107 - CellWidth = 0x0108 - CellLength = 0x0109 - FillOrder = 0x010A - DocumentName = 0x010D - ImageDescription = 0x010E - Make = 0x010F - Model = 0x0110 - StripOffsets = 0x0111 - Orientation = 0x0112 - SamplesPerPixel = 0x0115 - RowsPerStrip = 0x0116 - StripByteCounts = 0x0117 - MinSampleValue = 0x0118 - MaxSampleValue = 0x0119 - XResolution = 0x011A - YResolution = 0x011B - PlanarConfiguration = 0x011C - PageName = 0x011D - FreeOffsets = 0x0120 - FreeByteCounts = 0x0121 - GrayResponseUnit = 0x0122 - GrayResponseCurve = 0x0123 - T4Options = 0x0124 - T6Options = 0x0125 - ResolutionUnit = 0x0128 - PageNumber = 0x0129 - TransferFunction = 0x012D - Software = 0x0131 - DateTime = 0x0132 - Artist = 0x013B - HostComputer = 0x013C - Predictor = 0x013D - WhitePoint = 0x013E - PrimaryChromaticities = 0x013F - ColorMap = 0x0140 - HalftoneHints = 0x0141 - TileWidth = 0x0142 - TileLength = 0x0143 - TileOffsets = 0x0144 - TileByteCounts = 0x0145 - SubIFDs = 0x014A - InkSet = 0x014C - InkNames = 0x014D - NumberOfInks = 0x014E - DotRange = 0x0150 - TargetPrinter = 0x0151 - ExtraSamples = 0x0152 - SampleFormat = 0x0153 - SMinSampleValue = 0x0154 - SMaxSampleValue = 0x0155 - TransferRange = 0x0156 - ClipPath = 0x0157 - XClipPathUnits = 0x0158 - YClipPathUnits = 0x0159 - Indexed = 0x015A - JPEGTables = 0x015B - OPIProxy = 0x015F - JPEGProc = 0x0200 - JpegIFOffset = 0x0201 - JpegIFByteCount = 0x0202 - JpegRestartInterval = 0x0203 - JpegLosslessPredictors = 0x0205 - JpegPointTransforms = 0x0206 - JpegQTables = 0x0207 - JpegDCTables = 0x0208 - JpegACTables = 0x0209 - YCbCrCoefficients = 0x0211 - YCbCrSubSampling = 0x0212 - YCbCrPositioning = 0x0213 - ReferenceBlackWhite = 0x0214 - XMLPacket = 0x02BC - RelatedImageFileFormat = 0x1000 - RelatedImageWidth = 0x1001 - RelatedImageLength = 0x1002 - Rating = 0x4746 - RatingPercent = 0x4749 - ImageID = 0x800D - CFARepeatPatternDim = 0x828D - BatteryLevel = 0x828F - Copyright = 0x8298 - ExposureTime = 0x829A - FNumber = 0x829D - IPTCNAA = 0x83BB - ImageResources = 0x8649 - ExifOffset = 0x8769 - InterColorProfile = 0x8773 - ExposureProgram = 0x8822 - SpectralSensitivity = 0x8824 - GPSInfo = 0x8825 - ISOSpeedRatings = 0x8827 - OECF = 0x8828 - Interlace = 0x8829 - TimeZoneOffset = 0x882A - SelfTimerMode = 0x882B - SensitivityType = 0x8830 - StandardOutputSensitivity = 0x8831 - RecommendedExposureIndex = 0x8832 - ISOSpeed = 0x8833 - ISOSpeedLatitudeyyy = 0x8834 - ISOSpeedLatitudezzz = 0x8835 - ExifVersion = 0x9000 - DateTimeOriginal = 0x9003 - DateTimeDigitized = 0x9004 - OffsetTime = 0x9010 - OffsetTimeOriginal = 0x9011 - OffsetTimeDigitized = 0x9012 - ComponentsConfiguration = 0x9101 - CompressedBitsPerPixel = 0x9102 - ShutterSpeedValue = 0x9201 - ApertureValue = 0x9202 - BrightnessValue = 0x9203 - ExposureBiasValue = 0x9204 - MaxApertureValue = 0x9205 - SubjectDistance = 0x9206 - MeteringMode = 0x9207 - LightSource = 0x9208 - Flash = 0x9209 - FocalLength = 0x920A - Noise = 0x920D - ImageNumber = 0x9211 - SecurityClassification = 0x9212 - ImageHistory = 0x9213 - TIFFEPStandardID = 0x9216 - MakerNote = 0x927C - UserComment = 0x9286 - SubsecTime = 0x9290 - SubsecTimeOriginal = 0x9291 - SubsecTimeDigitized = 0x9292 - AmbientTemperature = 0x9400 - Humidity = 0x9401 - Pressure = 0x9402 - WaterDepth = 0x9403 - Acceleration = 0x9404 - CameraElevationAngle = 0x9405 - XPTitle = 0x9C9B - XPComment = 0x9C9C - XPAuthor = 0x9C9D - XPKeywords = 0x9C9E - XPSubject = 0x9C9F - FlashPixVersion = 0xA000 - ColorSpace = 0xA001 - ExifImageWidth = 0xA002 - ExifImageHeight = 0xA003 - RelatedSoundFile = 0xA004 - ExifInteroperabilityOffset = 0xA005 - FlashEnergy = 0xA20B - SpatialFrequencyResponse = 0xA20C - FocalPlaneXResolution = 0xA20E - FocalPlaneYResolution = 0xA20F - FocalPlaneResolutionUnit = 0xA210 - SubjectLocation = 0xA214 - ExposureIndex = 0xA215 - SensingMethod = 0xA217 - FileSource = 0xA300 - SceneType = 0xA301 - CFAPattern = 0xA302 - CustomRendered = 0xA401 - ExposureMode = 0xA402 - WhiteBalance = 0xA403 - DigitalZoomRatio = 0xA404 - FocalLengthIn35mmFilm = 0xA405 - SceneCaptureType = 0xA406 - GainControl = 0xA407 - Contrast = 0xA408 - Saturation = 0xA409 - Sharpness = 0xA40A - DeviceSettingDescription = 0xA40B - SubjectDistanceRange = 0xA40C - ImageUniqueID = 0xA420 - CameraOwnerName = 0xA430 - BodySerialNumber = 0xA431 - LensSpecification = 0xA432 - LensMake = 0xA433 - LensModel = 0xA434 - LensSerialNumber = 0xA435 - CompositeImage = 0xA460 - CompositeImageCount = 0xA461 - CompositeImageExposureTimes = 0xA462 - Gamma = 0xA500 - PrintImageMatching = 0xC4A5 - DNGVersion = 0xC612 - DNGBackwardVersion = 0xC613 - UniqueCameraModel = 0xC614 - LocalizedCameraModel = 0xC615 - CFAPlaneColor = 0xC616 - CFALayout = 0xC617 - LinearizationTable = 0xC618 - BlackLevelRepeatDim = 0xC619 - BlackLevel = 0xC61A - BlackLevelDeltaH = 0xC61B - BlackLevelDeltaV = 0xC61C - WhiteLevel = 0xC61D - DefaultScale = 0xC61E - DefaultCropOrigin = 0xC61F - DefaultCropSize = 0xC620 - ColorMatrix1 = 0xC621 - ColorMatrix2 = 0xC622 - CameraCalibration1 = 0xC623 - CameraCalibration2 = 0xC624 - ReductionMatrix1 = 0xC625 - ReductionMatrix2 = 0xC626 - AnalogBalance = 0xC627 - AsShotNeutral = 0xC628 - AsShotWhiteXY = 0xC629 - BaselineExposure = 0xC62A - BaselineNoise = 0xC62B - BaselineSharpness = 0xC62C - BayerGreenSplit = 0xC62D - LinearResponseLimit = 0xC62E - CameraSerialNumber = 0xC62F - LensInfo = 0xC630 - ChromaBlurRadius = 0xC631 - AntiAliasStrength = 0xC632 - ShadowScale = 0xC633 - DNGPrivateData = 0xC634 - MakerNoteSafety = 0xC635 - CalibrationIlluminant1 = 0xC65A - CalibrationIlluminant2 = 0xC65B - BestQualityScale = 0xC65C - RawDataUniqueID = 0xC65D - OriginalRawFileName = 0xC68B - OriginalRawFileData = 0xC68C - ActiveArea = 0xC68D - MaskedAreas = 0xC68E - AsShotICCProfile = 0xC68F - AsShotPreProfileMatrix = 0xC690 - CurrentICCProfile = 0xC691 - CurrentPreProfileMatrix = 0xC692 - ColorimetricReference = 0xC6BF - CameraCalibrationSignature = 0xC6F3 - ProfileCalibrationSignature = 0xC6F4 - AsShotProfileName = 0xC6F6 - NoiseReductionApplied = 0xC6F7 - ProfileName = 0xC6F8 - ProfileHueSatMapDims = 0xC6F9 - ProfileHueSatMapData1 = 0xC6FA - ProfileHueSatMapData2 = 0xC6FB - ProfileToneCurve = 0xC6FC - ProfileEmbedPolicy = 0xC6FD - ProfileCopyright = 0xC6FE - ForwardMatrix1 = 0xC714 - ForwardMatrix2 = 0xC715 - PreviewApplicationName = 0xC716 - PreviewApplicationVersion = 0xC717 - PreviewSettingsName = 0xC718 - PreviewSettingsDigest = 0xC719 - PreviewColorSpace = 0xC71A - PreviewDateTime = 0xC71B - RawImageDigest = 0xC71C - OriginalRawFileDigest = 0xC71D - SubTileBlockSize = 0xC71E - RowInterleaveFactor = 0xC71F - ProfileLookTableDims = 0xC725 - ProfileLookTableData = 0xC726 - OpcodeList1 = 0xC740 - OpcodeList2 = 0xC741 - OpcodeList3 = 0xC74E - NoiseProfile = 0xC761 - - -"""Maps EXIF tags to tag names.""" -TAGS = { - **{i.value: i.name for i in Base}, - 0x920C: "SpatialFrequencyResponse", - 0x9214: "SubjectLocation", - 0x9215: "ExposureIndex", - 0x828E: "CFAPattern", - 0x920B: "FlashEnergy", - 0x9216: "TIFF/EPStandardID", -} - - -class GPS(IntEnum): - GPSVersionID = 0 - GPSLatitudeRef = 1 - GPSLatitude = 2 - GPSLongitudeRef = 3 - GPSLongitude = 4 - GPSAltitudeRef = 5 - GPSAltitude = 6 - GPSTimeStamp = 7 - GPSSatellites = 8 - GPSStatus = 9 - GPSMeasureMode = 10 - GPSDOP = 11 - GPSSpeedRef = 12 - GPSSpeed = 13 - GPSTrackRef = 14 - GPSTrack = 15 - GPSImgDirectionRef = 16 - GPSImgDirection = 17 - GPSMapDatum = 18 - GPSDestLatitudeRef = 19 - GPSDestLatitude = 20 - GPSDestLongitudeRef = 21 - GPSDestLongitude = 22 - GPSDestBearingRef = 23 - GPSDestBearing = 24 - GPSDestDistanceRef = 25 - GPSDestDistance = 26 - GPSProcessingMethod = 27 - GPSAreaInformation = 28 - GPSDateStamp = 29 - GPSDifferential = 30 - GPSHPositioningError = 31 - - -"""Maps EXIF GPS tags to tag names.""" -GPSTAGS = {i.value: i.name for i in GPS} - - -class Interop(IntEnum): - InteropIndex = 1 - InteropVersion = 2 - RelatedImageFileFormat = 4096 - RelatedImageWidth = 4097 - RelatedImageHeight = 4098 - - -class IFD(IntEnum): - Exif = 34665 - GPSInfo = 34853 - Makernote = 37500 - Interop = 40965 - IFD1 = -1 - - -class LightSource(IntEnum): - Unknown = 0 - Daylight = 1 - Fluorescent = 2 - Tungsten = 3 - Flash = 4 - Fine = 9 - Cloudy = 10 - Shade = 11 - DaylightFluorescent = 12 - DayWhiteFluorescent = 13 - CoolWhiteFluorescent = 14 - WhiteFluorescent = 15 - StandardLightA = 17 - StandardLightB = 18 - StandardLightC = 19 - D55 = 20 - D65 = 21 - D75 = 22 - D50 = 23 - ISO = 24 - Other = 255 +# +# The Python Imaging Library. +# $Id$ +# +# EXIF tags +# +# Copyright (c) 2003 by Secret Labs AB +# +# See the README file for information on usage and redistribution. +# + +""" +This module provides constants and clear-text names for various +well-known EXIF tags. +""" +from __future__ import annotations + +from enum import IntEnum + + +class Base(IntEnum): + # possibly incomplete + InteropIndex = 0x0001 + ProcessingSoftware = 0x000B + NewSubfileType = 0x00FE + SubfileType = 0x00FF + ImageWidth = 0x0100 + ImageLength = 0x0101 + BitsPerSample = 0x0102 + Compression = 0x0103 + PhotometricInterpretation = 0x0106 + Thresholding = 0x0107 + CellWidth = 0x0108 + CellLength = 0x0109 + FillOrder = 0x010A + DocumentName = 0x010D + ImageDescription = 0x010E + Make = 0x010F + Model = 0x0110 + StripOffsets = 0x0111 + Orientation = 0x0112 + SamplesPerPixel = 0x0115 + RowsPerStrip = 0x0116 + StripByteCounts = 0x0117 + MinSampleValue = 0x0118 + MaxSampleValue = 0x0119 + XResolution = 0x011A + YResolution = 0x011B + PlanarConfiguration = 0x011C + PageName = 0x011D + FreeOffsets = 0x0120 + FreeByteCounts = 0x0121 + GrayResponseUnit = 0x0122 + GrayResponseCurve = 0x0123 + T4Options = 0x0124 + T6Options = 0x0125 + ResolutionUnit = 0x0128 + PageNumber = 0x0129 + TransferFunction = 0x012D + Software = 0x0131 + DateTime = 0x0132 + Artist = 0x013B + HostComputer = 0x013C + Predictor = 0x013D + WhitePoint = 0x013E + PrimaryChromaticities = 0x013F + ColorMap = 0x0140 + HalftoneHints = 0x0141 + TileWidth = 0x0142 + TileLength = 0x0143 + TileOffsets = 0x0144 + TileByteCounts = 0x0145 + SubIFDs = 0x014A + InkSet = 0x014C + InkNames = 0x014D + NumberOfInks = 0x014E + DotRange = 0x0150 + TargetPrinter = 0x0151 + ExtraSamples = 0x0152 + SampleFormat = 0x0153 + SMinSampleValue = 0x0154 + SMaxSampleValue = 0x0155 + TransferRange = 0x0156 + ClipPath = 0x0157 + XClipPathUnits = 0x0158 + YClipPathUnits = 0x0159 + Indexed = 0x015A + JPEGTables = 0x015B + OPIProxy = 0x015F + JPEGProc = 0x0200 + JpegIFOffset = 0x0201 + JpegIFByteCount = 0x0202 + JpegRestartInterval = 0x0203 + JpegLosslessPredictors = 0x0205 + JpegPointTransforms = 0x0206 + JpegQTables = 0x0207 + JpegDCTables = 0x0208 + JpegACTables = 0x0209 + YCbCrCoefficients = 0x0211 + YCbCrSubSampling = 0x0212 + YCbCrPositioning = 0x0213 + ReferenceBlackWhite = 0x0214 + XMLPacket = 0x02BC + RelatedImageFileFormat = 0x1000 + RelatedImageWidth = 0x1001 + RelatedImageLength = 0x1002 + Rating = 0x4746 + RatingPercent = 0x4749 + ImageID = 0x800D + CFARepeatPatternDim = 0x828D + BatteryLevel = 0x828F + Copyright = 0x8298 + ExposureTime = 0x829A + FNumber = 0x829D + IPTCNAA = 0x83BB + ImageResources = 0x8649 + ExifOffset = 0x8769 + InterColorProfile = 0x8773 + ExposureProgram = 0x8822 + SpectralSensitivity = 0x8824 + GPSInfo = 0x8825 + ISOSpeedRatings = 0x8827 + OECF = 0x8828 + Interlace = 0x8829 + TimeZoneOffset = 0x882A + SelfTimerMode = 0x882B + SensitivityType = 0x8830 + StandardOutputSensitivity = 0x8831 + RecommendedExposureIndex = 0x8832 + ISOSpeed = 0x8833 + ISOSpeedLatitudeyyy = 0x8834 + ISOSpeedLatitudezzz = 0x8835 + ExifVersion = 0x9000 + DateTimeOriginal = 0x9003 + DateTimeDigitized = 0x9004 + OffsetTime = 0x9010 + OffsetTimeOriginal = 0x9011 + OffsetTimeDigitized = 0x9012 + ComponentsConfiguration = 0x9101 + CompressedBitsPerPixel = 0x9102 + ShutterSpeedValue = 0x9201 + ApertureValue = 0x9202 + BrightnessValue = 0x9203 + ExposureBiasValue = 0x9204 + MaxApertureValue = 0x9205 + SubjectDistance = 0x9206 + MeteringMode = 0x9207 + LightSource = 0x9208 + Flash = 0x9209 + FocalLength = 0x920A + Noise = 0x920D + ImageNumber = 0x9211 + SecurityClassification = 0x9212 + ImageHistory = 0x9213 + TIFFEPStandardID = 0x9216 + MakerNote = 0x927C + UserComment = 0x9286 + SubsecTime = 0x9290 + SubsecTimeOriginal = 0x9291 + SubsecTimeDigitized = 0x9292 + AmbientTemperature = 0x9400 + Humidity = 0x9401 + Pressure = 0x9402 + WaterDepth = 0x9403 + Acceleration = 0x9404 + CameraElevationAngle = 0x9405 + XPTitle = 0x9C9B + XPComment = 0x9C9C + XPAuthor = 0x9C9D + XPKeywords = 0x9C9E + XPSubject = 0x9C9F + FlashPixVersion = 0xA000 + ColorSpace = 0xA001 + ExifImageWidth = 0xA002 + ExifImageHeight = 0xA003 + RelatedSoundFile = 0xA004 + ExifInteroperabilityOffset = 0xA005 + FlashEnergy = 0xA20B + SpatialFrequencyResponse = 0xA20C + FocalPlaneXResolution = 0xA20E + FocalPlaneYResolution = 0xA20F + FocalPlaneResolutionUnit = 0xA210 + SubjectLocation = 0xA214 + ExposureIndex = 0xA215 + SensingMethod = 0xA217 + FileSource = 0xA300 + SceneType = 0xA301 + CFAPattern = 0xA302 + CustomRendered = 0xA401 + ExposureMode = 0xA402 + WhiteBalance = 0xA403 + DigitalZoomRatio = 0xA404 + FocalLengthIn35mmFilm = 0xA405 + SceneCaptureType = 0xA406 + GainControl = 0xA407 + Contrast = 0xA408 + Saturation = 0xA409 + Sharpness = 0xA40A + DeviceSettingDescription = 0xA40B + SubjectDistanceRange = 0xA40C + ImageUniqueID = 0xA420 + CameraOwnerName = 0xA430 + BodySerialNumber = 0xA431 + LensSpecification = 0xA432 + LensMake = 0xA433 + LensModel = 0xA434 + LensSerialNumber = 0xA435 + CompositeImage = 0xA460 + CompositeImageCount = 0xA461 + CompositeImageExposureTimes = 0xA462 + Gamma = 0xA500 + PrintImageMatching = 0xC4A5 + DNGVersion = 0xC612 + DNGBackwardVersion = 0xC613 + UniqueCameraModel = 0xC614 + LocalizedCameraModel = 0xC615 + CFAPlaneColor = 0xC616 + CFALayout = 0xC617 + LinearizationTable = 0xC618 + BlackLevelRepeatDim = 0xC619 + BlackLevel = 0xC61A + BlackLevelDeltaH = 0xC61B + BlackLevelDeltaV = 0xC61C + WhiteLevel = 0xC61D + DefaultScale = 0xC61E + DefaultCropOrigin = 0xC61F + DefaultCropSize = 0xC620 + ColorMatrix1 = 0xC621 + ColorMatrix2 = 0xC622 + CameraCalibration1 = 0xC623 + CameraCalibration2 = 0xC624 + ReductionMatrix1 = 0xC625 + ReductionMatrix2 = 0xC626 + AnalogBalance = 0xC627 + AsShotNeutral = 0xC628 + AsShotWhiteXY = 0xC629 + BaselineExposure = 0xC62A + BaselineNoise = 0xC62B + BaselineSharpness = 0xC62C + BayerGreenSplit = 0xC62D + LinearResponseLimit = 0xC62E + CameraSerialNumber = 0xC62F + LensInfo = 0xC630 + ChromaBlurRadius = 0xC631 + AntiAliasStrength = 0xC632 + ShadowScale = 0xC633 + DNGPrivateData = 0xC634 + MakerNoteSafety = 0xC635 + CalibrationIlluminant1 = 0xC65A + CalibrationIlluminant2 = 0xC65B + BestQualityScale = 0xC65C + RawDataUniqueID = 0xC65D + OriginalRawFileName = 0xC68B + OriginalRawFileData = 0xC68C + ActiveArea = 0xC68D + MaskedAreas = 0xC68E + AsShotICCProfile = 0xC68F + AsShotPreProfileMatrix = 0xC690 + CurrentICCProfile = 0xC691 + CurrentPreProfileMatrix = 0xC692 + ColorimetricReference = 0xC6BF + CameraCalibrationSignature = 0xC6F3 + ProfileCalibrationSignature = 0xC6F4 + AsShotProfileName = 0xC6F6 + NoiseReductionApplied = 0xC6F7 + ProfileName = 0xC6F8 + ProfileHueSatMapDims = 0xC6F9 + ProfileHueSatMapData1 = 0xC6FA + ProfileHueSatMapData2 = 0xC6FB + ProfileToneCurve = 0xC6FC + ProfileEmbedPolicy = 0xC6FD + ProfileCopyright = 0xC6FE + ForwardMatrix1 = 0xC714 + ForwardMatrix2 = 0xC715 + PreviewApplicationName = 0xC716 + PreviewApplicationVersion = 0xC717 + PreviewSettingsName = 0xC718 + PreviewSettingsDigest = 0xC719 + PreviewColorSpace = 0xC71A + PreviewDateTime = 0xC71B + RawImageDigest = 0xC71C + OriginalRawFileDigest = 0xC71D + SubTileBlockSize = 0xC71E + RowInterleaveFactor = 0xC71F + ProfileLookTableDims = 0xC725 + ProfileLookTableData = 0xC726 + OpcodeList1 = 0xC740 + OpcodeList2 = 0xC741 + OpcodeList3 = 0xC74E + NoiseProfile = 0xC761 + + +"""Maps EXIF tags to tag names.""" +TAGS = { + **{i.value: i.name for i in Base}, + 0x920C: "SpatialFrequencyResponse", + 0x9214: "SubjectLocation", + 0x9215: "ExposureIndex", + 0x828E: "CFAPattern", + 0x920B: "FlashEnergy", + 0x9216: "TIFF/EPStandardID", +} + + +class GPS(IntEnum): + GPSVersionID = 0 + GPSLatitudeRef = 1 + GPSLatitude = 2 + GPSLongitudeRef = 3 + GPSLongitude = 4 + GPSAltitudeRef = 5 + GPSAltitude = 6 + GPSTimeStamp = 7 + GPSSatellites = 8 + GPSStatus = 9 + GPSMeasureMode = 10 + GPSDOP = 11 + GPSSpeedRef = 12 + GPSSpeed = 13 + GPSTrackRef = 14 + GPSTrack = 15 + GPSImgDirectionRef = 16 + GPSImgDirection = 17 + GPSMapDatum = 18 + GPSDestLatitudeRef = 19 + GPSDestLatitude = 20 + GPSDestLongitudeRef = 21 + GPSDestLongitude = 22 + GPSDestBearingRef = 23 + GPSDestBearing = 24 + GPSDestDistanceRef = 25 + GPSDestDistance = 26 + GPSProcessingMethod = 27 + GPSAreaInformation = 28 + GPSDateStamp = 29 + GPSDifferential = 30 + GPSHPositioningError = 31 + + +"""Maps EXIF GPS tags to tag names.""" +GPSTAGS = {i.value: i.name for i in GPS} + + +class Interop(IntEnum): + InteropIndex = 1 + InteropVersion = 2 + RelatedImageFileFormat = 4096 + RelatedImageWidth = 4097 + RelatedImageHeight = 4098 + + +class IFD(IntEnum): + Exif = 34665 + GPSInfo = 34853 + Makernote = 37500 + Interop = 40965 + IFD1 = -1 + + +class LightSource(IntEnum): + Unknown = 0 + Daylight = 1 + Fluorescent = 2 + Tungsten = 3 + Flash = 4 + Fine = 9 + Cloudy = 10 + Shade = 11 + DaylightFluorescent = 12 + DayWhiteFluorescent = 13 + CoolWhiteFluorescent = 14 + WhiteFluorescent = 15 + StandardLightA = 17 + StandardLightB = 18 + StandardLightC = 19 + D55 = 20 + D65 = 21 + D75 = 22 + D50 = 23 + ISO = 24 + Other = 255 diff --git a/src/PIL/FitsImagePlugin.py b/src/PIL/FitsImagePlugin.py index a169b6083..078b82351 100644 --- a/src/PIL/FitsImagePlugin.py +++ b/src/PIL/FitsImagePlugin.py @@ -1,148 +1,148 @@ -# -# The Python Imaging Library -# $Id$ -# -# FITS file handling -# -# Copyright (c) 1998-2003 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import gzip -import math - -from . import Image, ImageFile - - -def _accept(prefix: bytes) -> bool: - return prefix[:6] == b"SIMPLE" - - -class FitsImageFile(ImageFile.ImageFile): - format = "FITS" - format_description = "FITS" - - def _open(self) -> None: - assert self.fp is not None - - headers: dict[bytes, bytes] = {} - header_in_progress = False - decoder_name = "" - while True: - header = self.fp.read(80) - if not header: - msg = "Truncated FITS file" - raise OSError(msg) - keyword = header[:8].strip() - if keyword in (b"SIMPLE", b"XTENSION"): - header_in_progress = True - elif headers and not header_in_progress: - # This is now a data unit - break - elif keyword == b"END": - # Seek to the end of the header unit - self.fp.seek(math.ceil(self.fp.tell() / 2880) * 2880) - if not decoder_name: - decoder_name, offset, args = self._parse_headers(headers) - - header_in_progress = False - continue - - if decoder_name: - # Keep going to read past the headers - continue - - value = header[8:].split(b"/")[0].strip() - if value.startswith(b"="): - value = value[1:].strip() - if not headers and (not _accept(keyword) or value != b"T"): - msg = "Not a FITS file" - raise SyntaxError(msg) - headers[keyword] = value - - if not decoder_name: - msg = "No image data" - raise ValueError(msg) - - offset += self.fp.tell() - 80 - self.tile = [(decoder_name, (0, 0) + self.size, offset, args)] - - def _get_size( - self, headers: dict[bytes, bytes], prefix: bytes - ) -> tuple[int, int] | None: - naxis = int(headers[prefix + b"NAXIS"]) - if naxis == 0: - return None - - if naxis == 1: - return 1, int(headers[prefix + b"NAXIS1"]) - else: - return int(headers[prefix + b"NAXIS1"]), int(headers[prefix + b"NAXIS2"]) - - def _parse_headers( - self, headers: dict[bytes, bytes] - ) -> tuple[str, int, tuple[str | int, ...]]: - prefix = b"" - decoder_name = "raw" - offset = 0 - if ( - headers.get(b"XTENSION") == b"'BINTABLE'" - and headers.get(b"ZIMAGE") == b"T" - and headers[b"ZCMPTYPE"] == b"'GZIP_1 '" - ): - no_prefix_size = self._get_size(headers, prefix) or (0, 0) - number_of_bits = int(headers[b"BITPIX"]) - offset = no_prefix_size[0] * no_prefix_size[1] * (number_of_bits // 8) - - prefix = b"Z" - decoder_name = "fits_gzip" - - size = self._get_size(headers, prefix) - if not size: - return "", 0, () - - self._size = size - - number_of_bits = int(headers[prefix + b"BITPIX"]) - if number_of_bits == 8: - self._mode = "L" - elif number_of_bits == 16: - self._mode = "I;16" - elif number_of_bits == 32: - self._mode = "I" - elif number_of_bits in (-32, -64): - self._mode = "F" - - args = (self.mode, 0, -1) if decoder_name == "raw" else (number_of_bits,) - return decoder_name, offset, args - - -class FitsGzipDecoder(ImageFile.PyDecoder): - _pulls_fd = True - - def decode(self, buffer: bytes) -> tuple[int, int]: - assert self.fd is not None - value = gzip.decompress(self.fd.read()) - - rows = [] - offset = 0 - number_of_bits = min(self.args[0] // 8, 4) - for y in range(self.state.ysize): - row = bytearray() - for x in range(self.state.xsize): - row += value[offset + (4 - number_of_bits) : offset + 4] - offset += 4 - rows.append(row) - self.set_as_raw(bytes([pixel for row in rows[::-1] for pixel in row])) - return -1, 0 - - -# -------------------------------------------------------------------- -# Registry - -Image.register_open(FitsImageFile.format, FitsImageFile, _accept) -Image.register_decoder("fits_gzip", FitsGzipDecoder) - -Image.register_extensions(FitsImageFile.format, [".fit", ".fits"]) +# +# The Python Imaging Library +# $Id$ +# +# FITS file handling +# +# Copyright (c) 1998-2003 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import gzip +import math + +from . import Image, ImageFile + + +def _accept(prefix: bytes) -> bool: + return prefix[:6] == b"SIMPLE" + + +class FitsImageFile(ImageFile.ImageFile): + format = "FITS" + format_description = "FITS" + + def _open(self) -> None: + assert self.fp is not None + + headers: dict[bytes, bytes] = {} + header_in_progress = False + decoder_name = "" + while True: + header = self.fp.read(80) + if not header: + msg = "Truncated FITS file" + raise OSError(msg) + keyword = header[:8].strip() + if keyword in (b"SIMPLE", b"XTENSION"): + header_in_progress = True + elif headers and not header_in_progress: + # This is now a data unit + break + elif keyword == b"END": + # Seek to the end of the header unit + self.fp.seek(math.ceil(self.fp.tell() / 2880) * 2880) + if not decoder_name: + decoder_name, offset, args = self._parse_headers(headers) + + header_in_progress = False + continue + + if decoder_name: + # Keep going to read past the headers + continue + + value = header[8:].split(b"/")[0].strip() + if value.startswith(b"="): + value = value[1:].strip() + if not headers and (not _accept(keyword) or value != b"T"): + msg = "Not a FITS file" + raise SyntaxError(msg) + headers[keyword] = value + + if not decoder_name: + msg = "No image data" + raise ValueError(msg) + + offset += self.fp.tell() - 80 + self.tile = [(decoder_name, (0, 0) + self.size, offset, args)] + + def _get_size( + self, headers: dict[bytes, bytes], prefix: bytes + ) -> tuple[int, int] | None: + naxis = int(headers[prefix + b"NAXIS"]) + if naxis == 0: + return None + + if naxis == 1: + return 1, int(headers[prefix + b"NAXIS1"]) + else: + return int(headers[prefix + b"NAXIS1"]), int(headers[prefix + b"NAXIS2"]) + + def _parse_headers( + self, headers: dict[bytes, bytes] + ) -> tuple[str, int, tuple[str | int, ...]]: + prefix = b"" + decoder_name = "raw" + offset = 0 + if ( + headers.get(b"XTENSION") == b"'BINTABLE'" + and headers.get(b"ZIMAGE") == b"T" + and headers[b"ZCMPTYPE"] == b"'GZIP_1 '" + ): + no_prefix_size = self._get_size(headers, prefix) or (0, 0) + number_of_bits = int(headers[b"BITPIX"]) + offset = no_prefix_size[0] * no_prefix_size[1] * (number_of_bits // 8) + + prefix = b"Z" + decoder_name = "fits_gzip" + + size = self._get_size(headers, prefix) + if not size: + return "", 0, () + + self._size = size + + number_of_bits = int(headers[prefix + b"BITPIX"]) + if number_of_bits == 8: + self._mode = "L" + elif number_of_bits == 16: + self._mode = "I;16" + elif number_of_bits == 32: + self._mode = "I" + elif number_of_bits in (-32, -64): + self._mode = "F" + + args = (self.mode, 0, -1) if decoder_name == "raw" else (number_of_bits,) + return decoder_name, offset, args + + +class FitsGzipDecoder(ImageFile.PyDecoder): + _pulls_fd = True + + def decode(self, buffer: bytes) -> tuple[int, int]: + assert self.fd is not None + value = gzip.decompress(self.fd.read()) + + rows = [] + offset = 0 + number_of_bits = min(self.args[0] // 8, 4) + for y in range(self.state.ysize): + row = bytearray() + for x in range(self.state.xsize): + row += value[offset + (4 - number_of_bits) : offset + 4] + offset += 4 + rows.append(row) + self.set_as_raw(bytes([pixel for row in rows[::-1] for pixel in row])) + return -1, 0 + + +# -------------------------------------------------------------------- +# Registry + +Image.register_open(FitsImageFile.format, FitsImageFile, _accept) +Image.register_decoder("fits_gzip", FitsGzipDecoder) + +Image.register_extensions(FitsImageFile.format, [".fit", ".fits"]) diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py index dceb83927..5ad7a0bc5 100644 --- a/src/PIL/FliImagePlugin.py +++ b/src/PIL/FliImagePlugin.py @@ -1,174 +1,174 @@ -# -# The Python Imaging Library. -# $Id$ -# -# FLI/FLC file handling. -# -# History: -# 95-09-01 fl Created -# 97-01-03 fl Fixed parser, setup decoder tile -# 98-07-15 fl Renamed offset attribute to avoid name clash -# -# Copyright (c) Secret Labs AB 1997-98. -# Copyright (c) Fredrik Lundh 1995-97. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import os - -from . import Image, ImageFile, ImagePalette -from ._binary import i16le as i16 -from ._binary import i32le as i32 -from ._binary import o8 - -# -# decoder - - -def _accept(prefix: bytes) -> bool: - return ( - len(prefix) >= 6 - and i16(prefix, 4) in [0xAF11, 0xAF12] - and i16(prefix, 14) in [0, 3] # flags - ) - - -## -# Image plugin for the FLI/FLC animation format. Use the seek -# method to load individual frames. - - -class FliImageFile(ImageFile.ImageFile): - format = "FLI" - format_description = "Autodesk FLI/FLC Animation" - _close_exclusive_fp_after_loading = False - - def _open(self): - # HEAD - s = self.fp.read(128) - if not (_accept(s) and s[20:22] == b"\x00\x00"): - msg = "not an FLI/FLC file" - raise SyntaxError(msg) - - # frames - self.n_frames = i16(s, 6) - self.is_animated = self.n_frames > 1 - - # image characteristics - self._mode = "P" - self._size = i16(s, 8), i16(s, 10) - - # animation speed - duration = i32(s, 16) - magic = i16(s, 4) - if magic == 0xAF11: - duration = (duration * 1000) // 70 - self.info["duration"] = duration - - # look for palette - palette = [(a, a, a) for a in range(256)] - - s = self.fp.read(16) - - self.__offset = 128 - - if i16(s, 4) == 0xF100: - # prefix chunk; ignore it - self.__offset = self.__offset + i32(s) - self.fp.seek(self.__offset) - s = self.fp.read(16) - - if i16(s, 4) == 0xF1FA: - # look for palette chunk - number_of_subchunks = i16(s, 6) - chunk_size = None - for _ in range(number_of_subchunks): - if chunk_size is not None: - self.fp.seek(chunk_size - 6, os.SEEK_CUR) - s = self.fp.read(6) - chunk_type = i16(s, 4) - if chunk_type in (4, 11): - self._palette(palette, 2 if chunk_type == 11 else 0) - break - chunk_size = i32(s) - if not chunk_size: - break - - palette = [o8(r) + o8(g) + o8(b) for (r, g, b) in palette] - self.palette = ImagePalette.raw("RGB", b"".join(palette)) - - # set things up to decode first frame - self.__frame = -1 - self._fp = self.fp - self.__rewind = self.fp.tell() - self.seek(0) - - def _palette(self, palette, shift): - # load palette - - i = 0 - for e in range(i16(self.fp.read(2))): - s = self.fp.read(2) - i = i + s[0] - n = s[1] - if n == 0: - n = 256 - s = self.fp.read(n * 3) - for n in range(0, len(s), 3): - r = s[n] << shift - g = s[n + 1] << shift - b = s[n + 2] << shift - palette[i] = (r, g, b) - i += 1 - - def seek(self, frame: int) -> None: - if not self._seek_check(frame): - return - if frame < self.__frame: - self._seek(0) - - for f in range(self.__frame + 1, frame + 1): - self._seek(f) - - def _seek(self, frame: int) -> None: - if frame == 0: - self.__frame = -1 - self._fp.seek(self.__rewind) - self.__offset = 128 - else: - # ensure that the previous frame was loaded - self.load() - - if frame != self.__frame + 1: - msg = f"cannot seek to frame {frame}" - raise ValueError(msg) - self.__frame = frame - - # move to next frame - self.fp = self._fp - self.fp.seek(self.__offset) - - s = self.fp.read(4) - if not s: - msg = "missing frame size" - raise EOFError(msg) - - framesize = i32(s) - - self.decodermaxblock = framesize - self.tile = [("fli", (0, 0) + self.size, self.__offset, None)] - - self.__offset += framesize - - def tell(self) -> int: - return self.__frame - - -# -# registry - -Image.register_open(FliImageFile.format, FliImageFile, _accept) - -Image.register_extensions(FliImageFile.format, [".fli", ".flc"]) +# +# The Python Imaging Library. +# $Id$ +# +# FLI/FLC file handling. +# +# History: +# 95-09-01 fl Created +# 97-01-03 fl Fixed parser, setup decoder tile +# 98-07-15 fl Renamed offset attribute to avoid name clash +# +# Copyright (c) Secret Labs AB 1997-98. +# Copyright (c) Fredrik Lundh 1995-97. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import os + +from . import Image, ImageFile, ImagePalette +from ._binary import i16le as i16 +from ._binary import i32le as i32 +from ._binary import o8 + +# +# decoder + + +def _accept(prefix: bytes) -> bool: + return ( + len(prefix) >= 6 + and i16(prefix, 4) in [0xAF11, 0xAF12] + and i16(prefix, 14) in [0, 3] # flags + ) + + +## +# Image plugin for the FLI/FLC animation format. Use the seek +# method to load individual frames. + + +class FliImageFile(ImageFile.ImageFile): + format = "FLI" + format_description = "Autodesk FLI/FLC Animation" + _close_exclusive_fp_after_loading = False + + def _open(self): + # HEAD + s = self.fp.read(128) + if not (_accept(s) and s[20:22] == b"\x00\x00"): + msg = "not an FLI/FLC file" + raise SyntaxError(msg) + + # frames + self.n_frames = i16(s, 6) + self.is_animated = self.n_frames > 1 + + # image characteristics + self._mode = "P" + self._size = i16(s, 8), i16(s, 10) + + # animation speed + duration = i32(s, 16) + magic = i16(s, 4) + if magic == 0xAF11: + duration = (duration * 1000) // 70 + self.info["duration"] = duration + + # look for palette + palette = [(a, a, a) for a in range(256)] + + s = self.fp.read(16) + + self.__offset = 128 + + if i16(s, 4) == 0xF100: + # prefix chunk; ignore it + self.__offset = self.__offset + i32(s) + self.fp.seek(self.__offset) + s = self.fp.read(16) + + if i16(s, 4) == 0xF1FA: + # look for palette chunk + number_of_subchunks = i16(s, 6) + chunk_size = None + for _ in range(number_of_subchunks): + if chunk_size is not None: + self.fp.seek(chunk_size - 6, os.SEEK_CUR) + s = self.fp.read(6) + chunk_type = i16(s, 4) + if chunk_type in (4, 11): + self._palette(palette, 2 if chunk_type == 11 else 0) + break + chunk_size = i32(s) + if not chunk_size: + break + + palette = [o8(r) + o8(g) + o8(b) for (r, g, b) in palette] + self.palette = ImagePalette.raw("RGB", b"".join(palette)) + + # set things up to decode first frame + self.__frame = -1 + self._fp = self.fp + self.__rewind = self.fp.tell() + self.seek(0) + + def _palette(self, palette, shift): + # load palette + + i = 0 + for e in range(i16(self.fp.read(2))): + s = self.fp.read(2) + i = i + s[0] + n = s[1] + if n == 0: + n = 256 + s = self.fp.read(n * 3) + for n in range(0, len(s), 3): + r = s[n] << shift + g = s[n + 1] << shift + b = s[n + 2] << shift + palette[i] = (r, g, b) + i += 1 + + def seek(self, frame: int) -> None: + if not self._seek_check(frame): + return + if frame < self.__frame: + self._seek(0) + + for f in range(self.__frame + 1, frame + 1): + self._seek(f) + + def _seek(self, frame: int) -> None: + if frame == 0: + self.__frame = -1 + self._fp.seek(self.__rewind) + self.__offset = 128 + else: + # ensure that the previous frame was loaded + self.load() + + if frame != self.__frame + 1: + msg = f"cannot seek to frame {frame}" + raise ValueError(msg) + self.__frame = frame + + # move to next frame + self.fp = self._fp + self.fp.seek(self.__offset) + + s = self.fp.read(4) + if not s: + msg = "missing frame size" + raise EOFError(msg) + + framesize = i32(s) + + self.decodermaxblock = framesize + self.tile = [("fli", (0, 0) + self.size, self.__offset, None)] + + self.__offset += framesize + + def tell(self) -> int: + return self.__frame + + +# +# registry + +Image.register_open(FliImageFile.format, FliImageFile, _accept) + +Image.register_extensions(FliImageFile.format, [".fli", ".flc"]) diff --git a/src/PIL/FontFile.py b/src/PIL/FontFile.py index 1e0c1c166..5a51ea284 100644 --- a/src/PIL/FontFile.py +++ b/src/PIL/FontFile.py @@ -1,134 +1,134 @@ -# -# The Python Imaging Library -# $Id$ -# -# base class for raster font file parsers -# -# history: -# 1997-06-05 fl created -# 1997-08-19 fl restrict image width -# -# Copyright (c) 1997-1998 by Secret Labs AB -# Copyright (c) 1997-1998 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import os -from typing import BinaryIO - -from . import Image, _binary - -WIDTH = 800 - - -def puti16( - fp: BinaryIO, values: tuple[int, int, int, int, int, int, int, int, int, int] -) -> None: - """Write network order (big-endian) 16-bit sequence""" - for v in values: - if v < 0: - v += 65536 - fp.write(_binary.o16be(v)) - - -class FontFile: - """Base class for raster font file handlers.""" - - bitmap: Image.Image | None = None - - def __init__(self) -> None: - self.info: dict[bytes, bytes | int] = {} - self.glyph: list[ - tuple[ - tuple[int, int], - tuple[int, int, int, int], - tuple[int, int, int, int], - Image.Image, - ] - | None - ] = [None] * 256 - - def __getitem__(self, ix: int) -> ( - tuple[ - tuple[int, int], - tuple[int, int, int, int], - tuple[int, int, int, int], - Image.Image, - ] - | None - ): - return self.glyph[ix] - - def compile(self) -> None: - """Create metrics and bitmap""" - - if self.bitmap: - return - - # create bitmap large enough to hold all data - h = w = maxwidth = 0 - lines = 1 - for glyph in self.glyph: - if glyph: - d, dst, src, im = glyph - h = max(h, src[3] - src[1]) - w = w + (src[2] - src[0]) - if w > WIDTH: - lines += 1 - w = src[2] - src[0] - maxwidth = max(maxwidth, w) - - xsize = maxwidth - ysize = lines * h - - if xsize == 0 and ysize == 0: - return - - self.ysize = h - - # paste glyphs into bitmap - self.bitmap = Image.new("1", (xsize, ysize)) - self.metrics: list[ - tuple[tuple[int, int], tuple[int, int, int, int], tuple[int, int, int, int]] - | None - ] = [None] * 256 - x = y = 0 - for i in range(256): - glyph = self[i] - if glyph: - d, dst, src, im = glyph - xx = src[2] - src[0] - x0, y0 = x, y - x = x + xx - if x > WIDTH: - x, y = 0, y + h - x0, y0 = x, y - x = xx - s = src[0] + x0, src[1] + y0, src[2] + x0, src[3] + y0 - self.bitmap.paste(im.crop(src), s) - self.metrics[i] = d, dst, s - - def save(self, filename: str) -> None: - """Save font""" - - self.compile() - - # font data - if not self.bitmap: - msg = "No bitmap created" - raise ValueError(msg) - self.bitmap.save(os.path.splitext(filename)[0] + ".pbm", "PNG") - - # font metrics - with open(os.path.splitext(filename)[0] + ".pil", "wb") as fp: - fp.write(b"PILfont\n") - fp.write(f";;;;;;{self.ysize};\n".encode("ascii")) # HACK!!! - fp.write(b"DATA\n") - for id in range(256): - m = self.metrics[id] - if not m: - puti16(fp, (0,) * 10) - else: - puti16(fp, m[0] + m[1] + m[2]) +# +# The Python Imaging Library +# $Id$ +# +# base class for raster font file parsers +# +# history: +# 1997-06-05 fl created +# 1997-08-19 fl restrict image width +# +# Copyright (c) 1997-1998 by Secret Labs AB +# Copyright (c) 1997-1998 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import os +from typing import BinaryIO + +from . import Image, _binary + +WIDTH = 800 + + +def puti16( + fp: BinaryIO, values: tuple[int, int, int, int, int, int, int, int, int, int] +) -> None: + """Write network order (big-endian) 16-bit sequence""" + for v in values: + if v < 0: + v += 65536 + fp.write(_binary.o16be(v)) + + +class FontFile: + """Base class for raster font file handlers.""" + + bitmap: Image.Image | None = None + + def __init__(self) -> None: + self.info: dict[bytes, bytes | int] = {} + self.glyph: list[ + tuple[ + tuple[int, int], + tuple[int, int, int, int], + tuple[int, int, int, int], + Image.Image, + ] + | None + ] = [None] * 256 + + def __getitem__(self, ix: int) -> ( + tuple[ + tuple[int, int], + tuple[int, int, int, int], + tuple[int, int, int, int], + Image.Image, + ] + | None + ): + return self.glyph[ix] + + def compile(self) -> None: + """Create metrics and bitmap""" + + if self.bitmap: + return + + # create bitmap large enough to hold all data + h = w = maxwidth = 0 + lines = 1 + for glyph in self.glyph: + if glyph: + d, dst, src, im = glyph + h = max(h, src[3] - src[1]) + w = w + (src[2] - src[0]) + if w > WIDTH: + lines += 1 + w = src[2] - src[0] + maxwidth = max(maxwidth, w) + + xsize = maxwidth + ysize = lines * h + + if xsize == 0 and ysize == 0: + return + + self.ysize = h + + # paste glyphs into bitmap + self.bitmap = Image.new("1", (xsize, ysize)) + self.metrics: list[ + tuple[tuple[int, int], tuple[int, int, int, int], tuple[int, int, int, int]] + | None + ] = [None] * 256 + x = y = 0 + for i in range(256): + glyph = self[i] + if glyph: + d, dst, src, im = glyph + xx = src[2] - src[0] + x0, y0 = x, y + x = x + xx + if x > WIDTH: + x, y = 0, y + h + x0, y0 = x, y + x = xx + s = src[0] + x0, src[1] + y0, src[2] + x0, src[3] + y0 + self.bitmap.paste(im.crop(src), s) + self.metrics[i] = d, dst, s + + def save(self, filename: str) -> None: + """Save font""" + + self.compile() + + # font data + if not self.bitmap: + msg = "No bitmap created" + raise ValueError(msg) + self.bitmap.save(os.path.splitext(filename)[0] + ".pbm", "PNG") + + # font metrics + with open(os.path.splitext(filename)[0] + ".pil", "wb") as fp: + fp.write(b"PILfont\n") + fp.write(f";;;;;;{self.ysize};\n".encode("ascii")) # HACK!!! + fp.write(b"DATA\n") + for id in range(256): + m = self.metrics[id] + if not m: + puti16(fp, (0,) * 10) + else: + puti16(fp, m[0] + m[1] + m[2]) diff --git a/src/PIL/FpxImagePlugin.py b/src/PIL/FpxImagePlugin.py index c1927bd26..08790154d 100644 --- a/src/PIL/FpxImagePlugin.py +++ b/src/PIL/FpxImagePlugin.py @@ -1,255 +1,255 @@ -# -# THIS IS WORK IN PROGRESS -# -# The Python Imaging Library. -# $Id$ -# -# FlashPix support for PIL -# -# History: -# 97-01-25 fl Created (reads uncompressed RGB images only) -# -# Copyright (c) Secret Labs AB 1997. -# Copyright (c) Fredrik Lundh 1997. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import olefile - -from . import Image, ImageFile -from ._binary import i32le as i32 - -# we map from colour field tuples to (mode, rawmode) descriptors -MODES = { - # opacity - (0x00007FFE,): ("A", "L"), - # monochrome - (0x00010000,): ("L", "L"), - (0x00018000, 0x00017FFE): ("RGBA", "LA"), - # photo YCC - (0x00020000, 0x00020001, 0x00020002): ("RGB", "YCC;P"), - (0x00028000, 0x00028001, 0x00028002, 0x00027FFE): ("RGBA", "YCCA;P"), - # standard RGB (NIFRGB) - (0x00030000, 0x00030001, 0x00030002): ("RGB", "RGB"), - (0x00038000, 0x00038001, 0x00038002, 0x00037FFE): ("RGBA", "RGBA"), -} - - -# -# -------------------------------------------------------------------- - - -def _accept(prefix: bytes) -> bool: - return prefix[:8] == olefile.MAGIC - - -## -# Image plugin for the FlashPix images. - - -class FpxImageFile(ImageFile.ImageFile): - format = "FPX" - format_description = "FlashPix" - - def _open(self): - # - # read the OLE directory and see if this is a likely - # to be a FlashPix file - - try: - self.ole = olefile.OleFileIO(self.fp) - except OSError as e: - msg = "not an FPX file; invalid OLE file" - raise SyntaxError(msg) from e - - if self.ole.root.clsid != "56616700-C154-11CE-8553-00AA00A1F95B": - msg = "not an FPX file; bad root CLSID" - raise SyntaxError(msg) - - self._open_index(1) - - def _open_index(self, index: int = 1) -> None: - # - # get the Image Contents Property Set - - prop = self.ole.getproperties( - [f"Data Object Store {index:06d}", "\005Image Contents"] - ) - - # size (highest resolution) - - self._size = prop[0x1000002], prop[0x1000003] - - size = max(self.size) - i = 1 - while size > 64: - size = size // 2 - i += 1 - self.maxid = i - 1 - - # mode. instead of using a single field for this, flashpix - # requires you to specify the mode for each channel in each - # resolution subimage, and leaves it to the decoder to make - # sure that they all match. for now, we'll cheat and assume - # that this is always the case. - - id = self.maxid << 16 - - s = prop[0x2000002 | id] - - bands = i32(s, 4) - if bands > 4: - msg = "Invalid number of bands" - raise OSError(msg) - - # note: for now, we ignore the "uncalibrated" flag - colors = tuple(i32(s, 8 + i * 4) & 0x7FFFFFFF for i in range(bands)) - - self._mode, self.rawmode = MODES[colors] - - # load JPEG tables, if any - self.jpeg = {} - for i in range(256): - id = 0x3000001 | (i << 16) - if id in prop: - self.jpeg[i] = prop[id] - - self._open_subimage(1, self.maxid) - - def _open_subimage(self, index: int = 1, subimage: int = 0) -> None: - # - # setup tile descriptors for a given subimage - - stream = [ - f"Data Object Store {index:06d}", - f"Resolution {subimage:04d}", - "Subimage 0000 Header", - ] - - fp = self.ole.openstream(stream) - - # skip prefix - fp.read(28) - - # header stream - s = fp.read(36) - - size = i32(s, 4), i32(s, 8) - # tilecount = i32(s, 12) - tilesize = i32(s, 16), i32(s, 20) - # channels = i32(s, 24) - offset = i32(s, 28) - length = i32(s, 32) - - if size != self.size: - msg = "subimage mismatch" - raise OSError(msg) - - # get tile descriptors - fp.seek(28 + offset) - s = fp.read(i32(s, 12) * length) - - x = y = 0 - xsize, ysize = size - xtile, ytile = tilesize - self.tile = [] - - for i in range(0, len(s), length): - x1 = min(xsize, x + xtile) - y1 = min(ysize, y + ytile) - - compression = i32(s, i + 8) - - if compression == 0: - self.tile.append( - ( - "raw", - (x, y, x1, y1), - i32(s, i) + 28, - (self.rawmode,), - ) - ) - - elif compression == 1: - # FIXME: the fill decoder is not implemented - self.tile.append( - ( - "fill", - (x, y, x1, y1), - i32(s, i) + 28, - (self.rawmode, s[12:16]), - ) - ) - - elif compression == 2: - internal_color_conversion = s[14] - jpeg_tables = s[15] - rawmode = self.rawmode - - if internal_color_conversion: - # The image is stored as usual (usually YCbCr). - if rawmode == "RGBA": - # For "RGBA", data is stored as YCbCrA based on - # negative RGB. The following trick works around - # this problem : - jpegmode, rawmode = "YCbCrK", "CMYK" - else: - jpegmode = None # let the decoder decide - - else: - # The image is stored as defined by rawmode - jpegmode = rawmode - - self.tile.append( - ( - "jpeg", - (x, y, x1, y1), - i32(s, i) + 28, - (rawmode, jpegmode), - ) - ) - - # FIXME: jpeg tables are tile dependent; the prefix - # data must be placed in the tile descriptor itself! - - if jpeg_tables: - self.tile_prefix = self.jpeg[jpeg_tables] - - else: - msg = "unknown/invalid compression" - raise OSError(msg) - - x = x + xtile - if x >= xsize: - x, y = 0, y + ytile - if y >= ysize: - break # isn't really required - - self.stream = stream - self._fp = self.fp - self.fp = None - - def load(self): - if not self.fp: - self.fp = self.ole.openstream(self.stream[:2] + ["Subimage 0000 Data"]) - - return ImageFile.ImageFile.load(self) - - def close(self) -> None: - self.ole.close() - super().close() - - def __exit__(self, *args: object) -> None: - self.ole.close() - super().__exit__() - - -# -# -------------------------------------------------------------------- - - -Image.register_open(FpxImageFile.format, FpxImageFile, _accept) - -Image.register_extension(FpxImageFile.format, ".fpx") +# +# THIS IS WORK IN PROGRESS +# +# The Python Imaging Library. +# $Id$ +# +# FlashPix support for PIL +# +# History: +# 97-01-25 fl Created (reads uncompressed RGB images only) +# +# Copyright (c) Secret Labs AB 1997. +# Copyright (c) Fredrik Lundh 1997. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import olefile + +from . import Image, ImageFile +from ._binary import i32le as i32 + +# we map from colour field tuples to (mode, rawmode) descriptors +MODES = { + # opacity + (0x00007FFE,): ("A", "L"), + # monochrome + (0x00010000,): ("L", "L"), + (0x00018000, 0x00017FFE): ("RGBA", "LA"), + # photo YCC + (0x00020000, 0x00020001, 0x00020002): ("RGB", "YCC;P"), + (0x00028000, 0x00028001, 0x00028002, 0x00027FFE): ("RGBA", "YCCA;P"), + # standard RGB (NIFRGB) + (0x00030000, 0x00030001, 0x00030002): ("RGB", "RGB"), + (0x00038000, 0x00038001, 0x00038002, 0x00037FFE): ("RGBA", "RGBA"), +} + + +# +# -------------------------------------------------------------------- + + +def _accept(prefix: bytes) -> bool: + return prefix[:8] == olefile.MAGIC + + +## +# Image plugin for the FlashPix images. + + +class FpxImageFile(ImageFile.ImageFile): + format = "FPX" + format_description = "FlashPix" + + def _open(self): + # + # read the OLE directory and see if this is a likely + # to be a FlashPix file + + try: + self.ole = olefile.OleFileIO(self.fp) + except OSError as e: + msg = "not an FPX file; invalid OLE file" + raise SyntaxError(msg) from e + + if self.ole.root.clsid != "56616700-C154-11CE-8553-00AA00A1F95B": + msg = "not an FPX file; bad root CLSID" + raise SyntaxError(msg) + + self._open_index(1) + + def _open_index(self, index: int = 1) -> None: + # + # get the Image Contents Property Set + + prop = self.ole.getproperties( + [f"Data Object Store {index:06d}", "\005Image Contents"] + ) + + # size (highest resolution) + + self._size = prop[0x1000002], prop[0x1000003] + + size = max(self.size) + i = 1 + while size > 64: + size = size // 2 + i += 1 + self.maxid = i - 1 + + # mode. instead of using a single field for this, flashpix + # requires you to specify the mode for each channel in each + # resolution subimage, and leaves it to the decoder to make + # sure that they all match. for now, we'll cheat and assume + # that this is always the case. + + id = self.maxid << 16 + + s = prop[0x2000002 | id] + + bands = i32(s, 4) + if bands > 4: + msg = "Invalid number of bands" + raise OSError(msg) + + # note: for now, we ignore the "uncalibrated" flag + colors = tuple(i32(s, 8 + i * 4) & 0x7FFFFFFF for i in range(bands)) + + self._mode, self.rawmode = MODES[colors] + + # load JPEG tables, if any + self.jpeg = {} + for i in range(256): + id = 0x3000001 | (i << 16) + if id in prop: + self.jpeg[i] = prop[id] + + self._open_subimage(1, self.maxid) + + def _open_subimage(self, index: int = 1, subimage: int = 0) -> None: + # + # setup tile descriptors for a given subimage + + stream = [ + f"Data Object Store {index:06d}", + f"Resolution {subimage:04d}", + "Subimage 0000 Header", + ] + + fp = self.ole.openstream(stream) + + # skip prefix + fp.read(28) + + # header stream + s = fp.read(36) + + size = i32(s, 4), i32(s, 8) + # tilecount = i32(s, 12) + tilesize = i32(s, 16), i32(s, 20) + # channels = i32(s, 24) + offset = i32(s, 28) + length = i32(s, 32) + + if size != self.size: + msg = "subimage mismatch" + raise OSError(msg) + + # get tile descriptors + fp.seek(28 + offset) + s = fp.read(i32(s, 12) * length) + + x = y = 0 + xsize, ysize = size + xtile, ytile = tilesize + self.tile = [] + + for i in range(0, len(s), length): + x1 = min(xsize, x + xtile) + y1 = min(ysize, y + ytile) + + compression = i32(s, i + 8) + + if compression == 0: + self.tile.append( + ( + "raw", + (x, y, x1, y1), + i32(s, i) + 28, + (self.rawmode,), + ) + ) + + elif compression == 1: + # FIXME: the fill decoder is not implemented + self.tile.append( + ( + "fill", + (x, y, x1, y1), + i32(s, i) + 28, + (self.rawmode, s[12:16]), + ) + ) + + elif compression == 2: + internal_color_conversion = s[14] + jpeg_tables = s[15] + rawmode = self.rawmode + + if internal_color_conversion: + # The image is stored as usual (usually YCbCr). + if rawmode == "RGBA": + # For "RGBA", data is stored as YCbCrA based on + # negative RGB. The following trick works around + # this problem : + jpegmode, rawmode = "YCbCrK", "CMYK" + else: + jpegmode = None # let the decoder decide + + else: + # The image is stored as defined by rawmode + jpegmode = rawmode + + self.tile.append( + ( + "jpeg", + (x, y, x1, y1), + i32(s, i) + 28, + (rawmode, jpegmode), + ) + ) + + # FIXME: jpeg tables are tile dependent; the prefix + # data must be placed in the tile descriptor itself! + + if jpeg_tables: + self.tile_prefix = self.jpeg[jpeg_tables] + + else: + msg = "unknown/invalid compression" + raise OSError(msg) + + x = x + xtile + if x >= xsize: + x, y = 0, y + ytile + if y >= ysize: + break # isn't really required + + self.stream = stream + self._fp = self.fp + self.fp = None + + def load(self): + if not self.fp: + self.fp = self.ole.openstream(self.stream[:2] + ["Subimage 0000 Data"]) + + return ImageFile.ImageFile.load(self) + + def close(self) -> None: + self.ole.close() + super().close() + + def __exit__(self, *args: object) -> None: + self.ole.close() + super().__exit__() + + +# +# -------------------------------------------------------------------- + + +Image.register_open(FpxImageFile.format, FpxImageFile, _accept) + +Image.register_extension(FpxImageFile.format, ".fpx") diff --git a/src/PIL/FtexImagePlugin.py b/src/PIL/FtexImagePlugin.py index 5acbb4912..80700739f 100644 --- a/src/PIL/FtexImagePlugin.py +++ b/src/PIL/FtexImagePlugin.py @@ -1,115 +1,115 @@ -""" -A Pillow loader for .ftc and .ftu files (FTEX) -Jerome Leclanche - -The contents of this file are hereby released in the public domain (CC0) -Full text of the CC0 license: - https://creativecommons.org/publicdomain/zero/1.0/ - -Independence War 2: Edge Of Chaos - Texture File Format - 16 October 2001 - -The textures used for 3D objects in Independence War 2: Edge Of Chaos are in a -packed custom format called FTEX. This file format uses file extensions FTC -and FTU. -* FTC files are compressed textures (using standard texture compression). -* FTU files are not compressed. -Texture File Format -The FTC and FTU texture files both use the same format. This -has the following structure: -{header} -{format_directory} -{data} -Where: -{header} = { - u32:magic, - u32:version, - u32:width, - u32:height, - u32:mipmap_count, - u32:format_count -} - -* The "magic" number is "FTEX". -* "width" and "height" are the dimensions of the texture. -* "mipmap_count" is the number of mipmaps in the texture. -* "format_count" is the number of texture formats (different versions of the -same texture) in this file. - -{format_directory} = format_count * { u32:format, u32:where } - -The format value is 0 for DXT1 compressed textures and 1 for 24-bit RGB -uncompressed textures. -The texture data for a format starts at the position "where" in the file. - -Each set of texture data in the file has the following structure: -{data} = format_count * { u32:mipmap_size, mipmap_size * { u8 } } -* "mipmap_size" is the number of bytes in that mip level. For compressed -textures this is the size of the texture data compressed with DXT1. For 24 bit -uncompressed textures, this is 3 * width * height. Following this are the image -bytes for that mipmap level. - -Note: All data is stored in little-Endian (Intel) byte order. -""" - -from __future__ import annotations - -import struct -from enum import IntEnum -from io import BytesIO - -from . import Image, ImageFile - -MAGIC = b"FTEX" - - -class Format(IntEnum): - DXT1 = 0 - UNCOMPRESSED = 1 - - -class FtexImageFile(ImageFile.ImageFile): - format = "FTEX" - format_description = "Texture File Format (IW2:EOC)" - - def _open(self) -> None: - if not _accept(self.fp.read(4)): - msg = "not an FTEX file" - raise SyntaxError(msg) - struct.unpack(" None: - pass - - -def _accept(prefix: bytes) -> bool: - return prefix[:4] == MAGIC - - -Image.register_open(FtexImageFile.format, FtexImageFile, _accept) -Image.register_extensions(FtexImageFile.format, [".ftc", ".ftu"]) +""" +A Pillow loader for .ftc and .ftu files (FTEX) +Jerome Leclanche + +The contents of this file are hereby released in the public domain (CC0) +Full text of the CC0 license: + https://creativecommons.org/publicdomain/zero/1.0/ + +Independence War 2: Edge Of Chaos - Texture File Format - 16 October 2001 + +The textures used for 3D objects in Independence War 2: Edge Of Chaos are in a +packed custom format called FTEX. This file format uses file extensions FTC +and FTU. +* FTC files are compressed textures (using standard texture compression). +* FTU files are not compressed. +Texture File Format +The FTC and FTU texture files both use the same format. This +has the following structure: +{header} +{format_directory} +{data} +Where: +{header} = { + u32:magic, + u32:version, + u32:width, + u32:height, + u32:mipmap_count, + u32:format_count +} + +* The "magic" number is "FTEX". +* "width" and "height" are the dimensions of the texture. +* "mipmap_count" is the number of mipmaps in the texture. +* "format_count" is the number of texture formats (different versions of the +same texture) in this file. + +{format_directory} = format_count * { u32:format, u32:where } + +The format value is 0 for DXT1 compressed textures and 1 for 24-bit RGB +uncompressed textures. +The texture data for a format starts at the position "where" in the file. + +Each set of texture data in the file has the following structure: +{data} = format_count * { u32:mipmap_size, mipmap_size * { u8 } } +* "mipmap_size" is the number of bytes in that mip level. For compressed +textures this is the size of the texture data compressed with DXT1. For 24 bit +uncompressed textures, this is 3 * width * height. Following this are the image +bytes for that mipmap level. + +Note: All data is stored in little-Endian (Intel) byte order. +""" + +from __future__ import annotations + +import struct +from enum import IntEnum +from io import BytesIO + +from . import Image, ImageFile + +MAGIC = b"FTEX" + + +class Format(IntEnum): + DXT1 = 0 + UNCOMPRESSED = 1 + + +class FtexImageFile(ImageFile.ImageFile): + format = "FTEX" + format_description = "Texture File Format (IW2:EOC)" + + def _open(self) -> None: + if not _accept(self.fp.read(4)): + msg = "not an FTEX file" + raise SyntaxError(msg) + struct.unpack(" None: + pass + + +def _accept(prefix: bytes) -> bool: + return prefix[:4] == MAGIC + + +Image.register_open(FtexImageFile.format, FtexImageFile, _accept) +Image.register_extensions(FtexImageFile.format, [".ftc", ".ftu"]) diff --git a/src/PIL/GbrImagePlugin.py b/src/PIL/GbrImagePlugin.py index 93e89b1e6..7de53d79b 100644 --- a/src/PIL/GbrImagePlugin.py +++ b/src/PIL/GbrImagePlugin.py @@ -1,103 +1,103 @@ -# -# The Python Imaging Library -# -# load a GIMP brush file -# -# History: -# 96-03-14 fl Created -# 16-01-08 es Version 2 -# -# Copyright (c) Secret Labs AB 1997. -# Copyright (c) Fredrik Lundh 1996. -# Copyright (c) Eric Soroos 2016. -# -# See the README file for information on usage and redistribution. -# -# -# See https://github.com/GNOME/gimp/blob/mainline/devel-docs/gbr.txt for -# format documentation. -# -# This code Interprets version 1 and 2 .gbr files. -# Version 1 files are obsolete, and should not be used for new -# brushes. -# Version 2 files are saved by GIMP v2.8 (at least) -# Version 3 files have a format specifier of 18 for 16bit floats in -# the color depth field. This is currently unsupported by Pillow. -from __future__ import annotations - -from . import Image, ImageFile -from ._binary import i32be as i32 - - -def _accept(prefix: bytes) -> bool: - return len(prefix) >= 8 and i32(prefix, 0) >= 20 and i32(prefix, 4) in (1, 2) - - -## -# Image plugin for the GIMP brush format. - - -class GbrImageFile(ImageFile.ImageFile): - format = "GBR" - format_description = "GIMP brush file" - - def _open(self) -> None: - header_size = i32(self.fp.read(4)) - if header_size < 20: - msg = "not a GIMP brush" - raise SyntaxError(msg) - version = i32(self.fp.read(4)) - if version not in (1, 2): - msg = f"Unsupported GIMP brush version: {version}" - raise SyntaxError(msg) - - width = i32(self.fp.read(4)) - height = i32(self.fp.read(4)) - color_depth = i32(self.fp.read(4)) - if width <= 0 or height <= 0: - msg = "not a GIMP brush" - raise SyntaxError(msg) - if color_depth not in (1, 4): - msg = f"Unsupported GIMP brush color depth: {color_depth}" - raise SyntaxError(msg) - - if version == 1: - comment_length = header_size - 20 - else: - comment_length = header_size - 28 - magic_number = self.fp.read(4) - if magic_number != b"GIMP": - msg = "not a GIMP brush, bad magic number" - raise SyntaxError(msg) - self.info["spacing"] = i32(self.fp.read(4)) - - comment = self.fp.read(comment_length)[:-1] - - if color_depth == 1: - self._mode = "L" - else: - self._mode = "RGBA" - - self._size = width, height - - self.info["comment"] = comment - - # Image might not be small - Image._decompression_bomb_check(self.size) - - # Data is an uncompressed block of w * h * bytes/pixel - self._data_size = width * height * color_depth - - def load(self): - if not self.im: - self.im = Image.core.new(self.mode, self.size) - self.frombytes(self.fp.read(self._data_size)) - return Image.Image.load(self) - - -# -# registry - - -Image.register_open(GbrImageFile.format, GbrImageFile, _accept) -Image.register_extension(GbrImageFile.format, ".gbr") +# +# The Python Imaging Library +# +# load a GIMP brush file +# +# History: +# 96-03-14 fl Created +# 16-01-08 es Version 2 +# +# Copyright (c) Secret Labs AB 1997. +# Copyright (c) Fredrik Lundh 1996. +# Copyright (c) Eric Soroos 2016. +# +# See the README file for information on usage and redistribution. +# +# +# See https://github.com/GNOME/gimp/blob/mainline/devel-docs/gbr.txt for +# format documentation. +# +# This code Interprets version 1 and 2 .gbr files. +# Version 1 files are obsolete, and should not be used for new +# brushes. +# Version 2 files are saved by GIMP v2.8 (at least) +# Version 3 files have a format specifier of 18 for 16bit floats in +# the color depth field. This is currently unsupported by Pillow. +from __future__ import annotations + +from . import Image, ImageFile +from ._binary import i32be as i32 + + +def _accept(prefix: bytes) -> bool: + return len(prefix) >= 8 and i32(prefix, 0) >= 20 and i32(prefix, 4) in (1, 2) + + +## +# Image plugin for the GIMP brush format. + + +class GbrImageFile(ImageFile.ImageFile): + format = "GBR" + format_description = "GIMP brush file" + + def _open(self) -> None: + header_size = i32(self.fp.read(4)) + if header_size < 20: + msg = "not a GIMP brush" + raise SyntaxError(msg) + version = i32(self.fp.read(4)) + if version not in (1, 2): + msg = f"Unsupported GIMP brush version: {version}" + raise SyntaxError(msg) + + width = i32(self.fp.read(4)) + height = i32(self.fp.read(4)) + color_depth = i32(self.fp.read(4)) + if width <= 0 or height <= 0: + msg = "not a GIMP brush" + raise SyntaxError(msg) + if color_depth not in (1, 4): + msg = f"Unsupported GIMP brush color depth: {color_depth}" + raise SyntaxError(msg) + + if version == 1: + comment_length = header_size - 20 + else: + comment_length = header_size - 28 + magic_number = self.fp.read(4) + if magic_number != b"GIMP": + msg = "not a GIMP brush, bad magic number" + raise SyntaxError(msg) + self.info["spacing"] = i32(self.fp.read(4)) + + comment = self.fp.read(comment_length)[:-1] + + if color_depth == 1: + self._mode = "L" + else: + self._mode = "RGBA" + + self._size = width, height + + self.info["comment"] = comment + + # Image might not be small + Image._decompression_bomb_check(self.size) + + # Data is an uncompressed block of w * h * bytes/pixel + self._data_size = width * height * color_depth + + def load(self): + if not self.im: + self.im = Image.core.new(self.mode, self.size) + self.frombytes(self.fp.read(self._data_size)) + return Image.Image.load(self) + + +# +# registry + + +Image.register_open(GbrImageFile.format, GbrImageFile, _accept) +Image.register_extension(GbrImageFile.format, ".gbr") diff --git a/src/PIL/GdImageFile.py b/src/PIL/GdImageFile.py index 88b87a22c..207713f7c 100644 --- a/src/PIL/GdImageFile.py +++ b/src/PIL/GdImageFile.py @@ -1,102 +1,102 @@ -# -# The Python Imaging Library. -# $Id$ -# -# GD file handling -# -# History: -# 1996-04-12 fl Created -# -# Copyright (c) 1997 by Secret Labs AB. -# Copyright (c) 1996 by Fredrik Lundh. -# -# See the README file for information on usage and redistribution. -# - - -""" -.. note:: - This format cannot be automatically recognized, so the - class is not registered for use with :py:func:`PIL.Image.open()`. To open a - gd file, use the :py:func:`PIL.GdImageFile.open()` function instead. - -.. warning:: - THE GD FORMAT IS NOT DESIGNED FOR DATA INTERCHANGE. This - implementation is provided for convenience and demonstrational - purposes only. -""" -from __future__ import annotations - -from typing import IO - -from . import ImageFile, ImagePalette, UnidentifiedImageError -from ._binary import i16be as i16 -from ._binary import i32be as i32 -from ._typing import StrOrBytesPath - - -class GdImageFile(ImageFile.ImageFile): - """ - Image plugin for the GD uncompressed format. Note that this format - is not supported by the standard :py:func:`PIL.Image.open()` function. To use - this plugin, you have to import the :py:mod:`PIL.GdImageFile` module and - use the :py:func:`PIL.GdImageFile.open()` function. - """ - - format = "GD" - format_description = "GD uncompressed images" - - def _open(self) -> None: - # Header - assert self.fp is not None - - s = self.fp.read(1037) - - if i16(s) not in [65534, 65535]: - msg = "Not a valid GD 2.x .gd file" - raise SyntaxError(msg) - - self._mode = "L" # FIXME: "P" - self._size = i16(s, 2), i16(s, 4) - - true_color = s[6] - true_color_offset = 2 if true_color else 0 - - # transparency index - tindex = i32(s, 7 + true_color_offset) - if tindex < 256: - self.info["transparency"] = tindex - - self.palette = ImagePalette.raw( - "XBGR", s[7 + true_color_offset + 4 : 7 + true_color_offset + 4 + 256 * 4] - ) - - self.tile = [ - ( - "raw", - (0, 0) + self.size, - 7 + true_color_offset + 4 + 256 * 4, - ("L", 0, 1), - ) - ] - - -def open(fp: StrOrBytesPath | IO[bytes], mode: str = "r") -> GdImageFile: - """ - Load texture from a GD image file. - - :param fp: GD file name, or an opened file handle. - :param mode: Optional mode. In this version, if the mode argument - is given, it must be "r". - :returns: An image instance. - :raises OSError: If the image could not be read. - """ - if mode != "r": - msg = "bad mode" - raise ValueError(msg) - - try: - return GdImageFile(fp) - except SyntaxError as e: - msg = "cannot identify this image file" - raise UnidentifiedImageError(msg) from e +# +# The Python Imaging Library. +# $Id$ +# +# GD file handling +# +# History: +# 1996-04-12 fl Created +# +# Copyright (c) 1997 by Secret Labs AB. +# Copyright (c) 1996 by Fredrik Lundh. +# +# See the README file for information on usage and redistribution. +# + + +""" +.. note:: + This format cannot be automatically recognized, so the + class is not registered for use with :py:func:`PIL.Image.open()`. To open a + gd file, use the :py:func:`PIL.GdImageFile.open()` function instead. + +.. warning:: + THE GD FORMAT IS NOT DESIGNED FOR DATA INTERCHANGE. This + implementation is provided for convenience and demonstrational + purposes only. +""" +from __future__ import annotations + +from typing import IO + +from . import ImageFile, ImagePalette, UnidentifiedImageError +from ._binary import i16be as i16 +from ._binary import i32be as i32 +from ._typing import StrOrBytesPath + + +class GdImageFile(ImageFile.ImageFile): + """ + Image plugin for the GD uncompressed format. Note that this format + is not supported by the standard :py:func:`PIL.Image.open()` function. To use + this plugin, you have to import the :py:mod:`PIL.GdImageFile` module and + use the :py:func:`PIL.GdImageFile.open()` function. + """ + + format = "GD" + format_description = "GD uncompressed images" + + def _open(self) -> None: + # Header + assert self.fp is not None + + s = self.fp.read(1037) + + if i16(s) not in [65534, 65535]: + msg = "Not a valid GD 2.x .gd file" + raise SyntaxError(msg) + + self._mode = "L" # FIXME: "P" + self._size = i16(s, 2), i16(s, 4) + + true_color = s[6] + true_color_offset = 2 if true_color else 0 + + # transparency index + tindex = i32(s, 7 + true_color_offset) + if tindex < 256: + self.info["transparency"] = tindex + + self.palette = ImagePalette.raw( + "XBGR", s[7 + true_color_offset + 4 : 7 + true_color_offset + 4 + 256 * 4] + ) + + self.tile = [ + ( + "raw", + (0, 0) + self.size, + 7 + true_color_offset + 4 + 256 * 4, + ("L", 0, 1), + ) + ] + + +def open(fp: StrOrBytesPath | IO[bytes], mode: str = "r") -> GdImageFile: + """ + Load texture from a GD image file. + + :param fp: GD file name, or an opened file handle. + :param mode: Optional mode. In this version, if the mode argument + is given, it must be "r". + :returns: An image instance. + :raises OSError: If the image could not be read. + """ + if mode != "r": + msg = "bad mode" + raise ValueError(msg) + + try: + return GdImageFile(fp) + except SyntaxError as e: + msg = "cannot identify this image file" + raise UnidentifiedImageError(msg) from e diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index a305e8de6..2c5a4311b 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -1,1157 +1,1157 @@ -# -# The Python Imaging Library. -# $Id$ -# -# GIF file handling -# -# History: -# 1995-09-01 fl Created -# 1996-12-14 fl Added interlace support -# 1996-12-30 fl Added animation support -# 1997-01-05 fl Added write support, fixed local colour map bug -# 1997-02-23 fl Make sure to load raster data in getdata() -# 1997-07-05 fl Support external decoder (0.4) -# 1998-07-09 fl Handle all modes when saving (0.5) -# 1998-07-15 fl Renamed offset attribute to avoid name clash -# 2001-04-16 fl Added rewind support (seek to frame 0) (0.6) -# 2001-04-17 fl Added palette optimization (0.7) -# 2002-06-06 fl Added transparency support for save (0.8) -# 2004-02-24 fl Disable interlacing for small images -# -# Copyright (c) 1997-2004 by Secret Labs AB -# Copyright (c) 1995-2004 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import itertools -import math -import os -import subprocess -import sys -from enum import IntEnum -from functools import cached_property -from typing import IO, TYPE_CHECKING, Any, List, Literal, NamedTuple, Union - -from . import ( - Image, - ImageChops, - ImageFile, - ImageMath, - ImageOps, - ImagePalette, - ImageSequence, -) -from ._binary import i16le as i16 -from ._binary import o8 -from ._binary import o16le as o16 - -if TYPE_CHECKING: - from . import _imaging - - -class LoadingStrategy(IntEnum): - """.. versionadded:: 9.1.0""" - - RGB_AFTER_FIRST = 0 - RGB_AFTER_DIFFERENT_PALETTE_ONLY = 1 - RGB_ALWAYS = 2 - - -#: .. versionadded:: 9.1.0 -LOADING_STRATEGY = LoadingStrategy.RGB_AFTER_FIRST - -# -------------------------------------------------------------------- -# Identify/read GIF files - - -def _accept(prefix: bytes) -> bool: - return prefix[:6] in [b"GIF87a", b"GIF89a"] - - -## -# Image plugin for GIF images. This plugin supports both GIF87 and -# GIF89 images. - - -class GifImageFile(ImageFile.ImageFile): - format = "GIF" - format_description = "Compuserve GIF" - _close_exclusive_fp_after_loading = False - - global_palette = None - - def data(self) -> bytes | None: - s = self.fp.read(1) - if s and s[0]: - return self.fp.read(s[0]) - return None - - def _is_palette_needed(self, p: bytes) -> bool: - for i in range(0, len(p), 3): - if not (i // 3 == p[i] == p[i + 1] == p[i + 2]): - return True - return False - - def _open(self) -> None: - # Screen - s = self.fp.read(13) - if not _accept(s): - msg = "not a GIF file" - raise SyntaxError(msg) - - self.info["version"] = s[:6] - self._size = i16(s, 6), i16(s, 8) - self.tile = [] - flags = s[10] - bits = (flags & 7) + 1 - - if flags & 128: - # get global palette - self.info["background"] = s[11] - # check if palette contains colour indices - p = self.fp.read(3 << bits) - if self._is_palette_needed(p): - p = ImagePalette.raw("RGB", p) - self.global_palette = self.palette = p - - self._fp = self.fp # FIXME: hack - self.__rewind = self.fp.tell() - self._n_frames: int | None = None - self._seek(0) # get ready to read first frame - - @property - def n_frames(self) -> int: - if self._n_frames is None: - current = self.tell() - try: - while True: - self._seek(self.tell() + 1, False) - except EOFError: - self._n_frames = self.tell() + 1 - self.seek(current) - return self._n_frames - - @cached_property - def is_animated(self) -> bool: - if self._n_frames is not None: - return self._n_frames != 1 - - current = self.tell() - if current: - return True - - try: - self._seek(1, False) - is_animated = True - except EOFError: - is_animated = False - - self.seek(current) - return is_animated - - def seek(self, frame: int) -> None: - if not self._seek_check(frame): - return - if frame < self.__frame: - self.im = None - self._seek(0) - - last_frame = self.__frame - for f in range(self.__frame + 1, frame + 1): - try: - self._seek(f) - except EOFError as e: - self.seek(last_frame) - msg = "no more images in GIF file" - raise EOFError(msg) from e - - def _seek(self, frame: int, update_image: bool = True) -> None: - if frame == 0: - # rewind - self.__offset = 0 - self.dispose: _imaging.ImagingCore | None = None - self.__frame = -1 - self._fp.seek(self.__rewind) - self.disposal_method = 0 - if "comment" in self.info: - del self.info["comment"] - else: - # ensure that the previous frame was loaded - if self.tile and update_image: - self.load() - - if frame != self.__frame + 1: - msg = f"cannot seek to frame {frame}" - raise ValueError(msg) - - self.fp = self._fp - if self.__offset: - # backup to last frame - self.fp.seek(self.__offset) - while self.data(): - pass - self.__offset = 0 - - s = self.fp.read(1) - if not s or s == b";": - msg = "no more images in GIF file" - raise EOFError(msg) - - palette: ImagePalette.ImagePalette | Literal[False] | None = None - - info: dict[str, Any] = {} - frame_transparency = None - interlace = None - frame_dispose_extent = None - while True: - if not s: - s = self.fp.read(1) - if not s or s == b";": - break - - elif s == b"!": - # - # extensions - # - s = self.fp.read(1) - block = self.data() - if s[0] == 249 and block is not None: - # - # graphic control extension - # - flags = block[0] - if flags & 1: - frame_transparency = block[3] - info["duration"] = i16(block, 1) * 10 - - # disposal method - find the value of bits 4 - 6 - dispose_bits = 0b00011100 & flags - dispose_bits = dispose_bits >> 2 - if dispose_bits: - # only set the dispose if it is not - # unspecified. I'm not sure if this is - # correct, but it seems to prevent the last - # frame from looking odd for some animations - self.disposal_method = dispose_bits - elif s[0] == 254: - # - # comment extension - # - comment = b"" - - # Read this comment block - while block: - comment += block - block = self.data() - - if "comment" in info: - # If multiple comment blocks in frame, separate with \n - info["comment"] += b"\n" + comment - else: - info["comment"] = comment - s = None - continue - elif s[0] == 255 and frame == 0 and block is not None: - # - # application extension - # - info["extension"] = block, self.fp.tell() - if block[:11] == b"NETSCAPE2.0": - block = self.data() - if block and len(block) >= 3 and block[0] == 1: - self.info["loop"] = i16(block, 1) - while self.data(): - pass - - elif s == b",": - # - # local image - # - s = self.fp.read(9) - - # extent - x0, y0 = i16(s, 0), i16(s, 2) - x1, y1 = x0 + i16(s, 4), y0 + i16(s, 6) - if (x1 > self.size[0] or y1 > self.size[1]) and update_image: - self._size = max(x1, self.size[0]), max(y1, self.size[1]) - Image._decompression_bomb_check(self._size) - frame_dispose_extent = x0, y0, x1, y1 - flags = s[8] - - interlace = (flags & 64) != 0 - - if flags & 128: - bits = (flags & 7) + 1 - p = self.fp.read(3 << bits) - if self._is_palette_needed(p): - palette = ImagePalette.raw("RGB", p) - else: - palette = False - - # image data - bits = self.fp.read(1)[0] - self.__offset = self.fp.tell() - break - s = None - - if interlace is None: - msg = "image not found in GIF frame" - raise EOFError(msg) - - self.__frame = frame - if not update_image: - return - - self.tile = [] - - if self.dispose: - self.im.paste(self.dispose, self.dispose_extent) - - self._frame_palette = palette if palette is not None else self.global_palette - self._frame_transparency = frame_transparency - if frame == 0: - if self._frame_palette: - if LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS: - self._mode = "RGBA" if frame_transparency is not None else "RGB" - else: - self._mode = "P" - else: - self._mode = "L" - - if not palette and self.global_palette: - from copy import copy - - palette = copy(self.global_palette) - self.palette = palette - else: - if self.mode == "P": - if ( - LOADING_STRATEGY != LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY - or palette - ): - self.pyaccess = None - if "transparency" in self.info: - self.im.putpalettealpha(self.info["transparency"], 0) - self.im = self.im.convert("RGBA", Image.Dither.FLOYDSTEINBERG) - self._mode = "RGBA" - del self.info["transparency"] - else: - self._mode = "RGB" - self.im = self.im.convert("RGB", Image.Dither.FLOYDSTEINBERG) - - def _rgb(color: int) -> tuple[int, int, int]: - if self._frame_palette: - if color * 3 + 3 > len(self._frame_palette.palette): - color = 0 - return tuple(self._frame_palette.palette[color * 3 : color * 3 + 3]) - else: - return (color, color, color) - - self.dispose = None - self.dispose_extent = frame_dispose_extent - if self.dispose_extent and self.disposal_method >= 2: - try: - if self.disposal_method == 2: - # replace with background colour - - # only dispose the extent in this frame - x0, y0, x1, y1 = self.dispose_extent - dispose_size = (x1 - x0, y1 - y0) - - Image._decompression_bomb_check(dispose_size) - - # by convention, attempt to use transparency first - dispose_mode = "P" - color = self.info.get("transparency", frame_transparency) - if color is not None: - if self.mode in ("RGB", "RGBA"): - dispose_mode = "RGBA" - color = _rgb(color) + (0,) - else: - color = self.info.get("background", 0) - if self.mode in ("RGB", "RGBA"): - dispose_mode = "RGB" - color = _rgb(color) - self.dispose = Image.core.fill(dispose_mode, dispose_size, color) - else: - # replace with previous contents - if self.im is not None: - # only dispose the extent in this frame - self.dispose = self._crop(self.im, self.dispose_extent) - elif frame_transparency is not None: - x0, y0, x1, y1 = self.dispose_extent - dispose_size = (x1 - x0, y1 - y0) - - Image._decompression_bomb_check(dispose_size) - dispose_mode = "P" - color = frame_transparency - if self.mode in ("RGB", "RGBA"): - dispose_mode = "RGBA" - color = _rgb(frame_transparency) + (0,) - self.dispose = Image.core.fill( - dispose_mode, dispose_size, color - ) - except AttributeError: - pass - - if interlace is not None: - transparency = -1 - if frame_transparency is not None: - if frame == 0: - if LOADING_STRATEGY != LoadingStrategy.RGB_ALWAYS: - self.info["transparency"] = frame_transparency - elif self.mode not in ("RGB", "RGBA"): - transparency = frame_transparency - self.tile = [ - ( - "gif", - (x0, y0, x1, y1), - self.__offset, - (bits, interlace, transparency), - ) - ] - - if info.get("comment"): - self.info["comment"] = info["comment"] - for k in ["duration", "extension"]: - if k in info: - self.info[k] = info[k] - elif k in self.info: - del self.info[k] - - def load_prepare(self) -> None: - temp_mode = "P" if self._frame_palette else "L" - self._prev_im = None - if self.__frame == 0: - if self._frame_transparency is not None: - self.im = Image.core.fill( - temp_mode, self.size, self._frame_transparency - ) - elif self.mode in ("RGB", "RGBA"): - self._prev_im = self.im - if self._frame_palette: - self.im = Image.core.fill("P", self.size, self._frame_transparency or 0) - self.im.putpalette(*self._frame_palette.getdata()) - else: - self.im = None - self._mode = temp_mode - self._frame_palette = None - - super().load_prepare() - - def load_end(self) -> None: - if self.__frame == 0: - if self.mode == "P" and LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS: - if self._frame_transparency is not None: - self.im.putpalettealpha(self._frame_transparency, 0) - self._mode = "RGBA" - else: - self._mode = "RGB" - self.im = self.im.convert(self.mode, Image.Dither.FLOYDSTEINBERG) - return - if not self._prev_im: - return - if self._frame_transparency is not None: - self.im.putpalettealpha(self._frame_transparency, 0) - frame_im = self.im.convert("RGBA") - else: - frame_im = self.im.convert("RGB") - frame_im = self._crop(frame_im, self.dispose_extent) - - self.im = self._prev_im - self._mode = self.im.mode - if frame_im.mode == "RGBA": - self.im.paste(frame_im, self.dispose_extent, frame_im) - else: - self.im.paste(frame_im, self.dispose_extent) - - def tell(self) -> int: - return self.__frame - - -# -------------------------------------------------------------------- -# Write GIF files - - -RAWMODE = {"1": "L", "L": "L", "P": "P"} - - -def _normalize_mode(im: Image.Image) -> Image.Image: - """ - Takes an image (or frame), returns an image in a mode that is appropriate - for saving in a Gif. - - It may return the original image, or it may return an image converted to - palette or 'L' mode. - - :param im: Image object - :returns: Image object - """ - if im.mode in RAWMODE: - im.load() - return im - if Image.getmodebase(im.mode) == "RGB": - im = im.convert("P", palette=Image.Palette.ADAPTIVE) - if im.palette.mode == "RGBA": - for rgba in im.palette.colors: - if rgba[3] == 0: - im.info["transparency"] = im.palette.colors[rgba] - break - return im - return im.convert("L") - - -_Palette = Union[bytes, bytearray, List[int], ImagePalette.ImagePalette] - - -def _normalize_palette( - im: Image.Image, palette: _Palette | None, info: dict[str, Any] -) -> Image.Image: - """ - Normalizes the palette for image. - - Sets the palette to the incoming palette, if provided. - - Ensures that there's a palette for L mode images - - Optimizes the palette if necessary/desired. - - :param im: Image object - :param palette: bytes object containing the source palette, or .... - :param info: encoderinfo - :returns: Image object - """ - source_palette = None - if palette: - # a bytes palette - if isinstance(palette, (bytes, bytearray, list)): - source_palette = bytearray(palette[:768]) - if isinstance(palette, ImagePalette.ImagePalette): - source_palette = bytearray(palette.palette) - - if im.mode == "P": - if not source_palette: - source_palette = im.im.getpalette("RGB")[:768] - else: # L-mode - if not source_palette: - source_palette = bytearray(i // 3 for i in range(768)) - im.palette = ImagePalette.ImagePalette("RGB", palette=source_palette) - - used_palette_colors: list[int] | None - if palette: - used_palette_colors = [] - assert source_palette is not None - for i in range(0, len(source_palette), 3): - source_color = tuple(source_palette[i : i + 3]) - index = im.palette.colors.get(source_color) - if index in used_palette_colors: - index = None - used_palette_colors.append(index) - for i, index in enumerate(used_palette_colors): - if index is None: - for j in range(len(used_palette_colors)): - if j not in used_palette_colors: - used_palette_colors[i] = j - break - im = im.remap_palette(used_palette_colors) - else: - used_palette_colors = _get_optimize(im, info) - if used_palette_colors is not None: - im = im.remap_palette(used_palette_colors, source_palette) - if "transparency" in info: - try: - info["transparency"] = used_palette_colors.index( - info["transparency"] - ) - except ValueError: - del info["transparency"] - return im - - im.palette.palette = source_palette - return im - - -def _write_single_frame( - im: Image.Image, - fp: IO[bytes], - palette: _Palette | None, -) -> None: - im_out = _normalize_mode(im) - for k, v in im_out.info.items(): - im.encoderinfo.setdefault(k, v) - im_out = _normalize_palette(im_out, palette, im.encoderinfo) - - for s in _get_global_header(im_out, im.encoderinfo): - fp.write(s) - - # local image header - flags = 0 - if get_interlace(im): - flags = flags | 64 - _write_local_header(fp, im, (0, 0), flags) - - im_out.encoderconfig = (8, get_interlace(im)) - ImageFile._save(im_out, fp, [("gif", (0, 0) + im.size, 0, RAWMODE[im_out.mode])]) - - fp.write(b"\0") # end of image data - - -def _getbbox( - base_im: Image.Image, im_frame: Image.Image -) -> tuple[Image.Image, tuple[int, int, int, int] | None]: - if _get_palette_bytes(im_frame) != _get_palette_bytes(base_im): - im_frame = im_frame.convert("RGBA") - base_im = base_im.convert("RGBA") - delta = ImageChops.subtract_modulo(im_frame, base_im) - return delta, delta.getbbox(alpha_only=False) - - -class _Frame(NamedTuple): - im: Image.Image - bbox: tuple[int, int, int, int] | None - encoderinfo: dict[str, Any] - - -def _write_multiple_frames( - im: Image.Image, fp: IO[bytes], palette: _Palette | None -) -> bool: - duration = im.encoderinfo.get("duration") - disposal = im.encoderinfo.get("disposal", im.info.get("disposal")) - - im_frames: list[_Frame] = [] - previous_im: Image.Image | None = None - frame_count = 0 - background_im = None - for imSequence in itertools.chain([im], im.encoderinfo.get("append_images", [])): - for im_frame in ImageSequence.Iterator(imSequence): - # a copy is required here since seek can still mutate the image - im_frame = _normalize_mode(im_frame.copy()) - if frame_count == 0: - for k, v in im_frame.info.items(): - if k == "transparency": - continue - im.encoderinfo.setdefault(k, v) - - encoderinfo = im.encoderinfo.copy() - if "transparency" in im_frame.info: - encoderinfo.setdefault("transparency", im_frame.info["transparency"]) - im_frame = _normalize_palette(im_frame, palette, encoderinfo) - if isinstance(duration, (list, tuple)): - encoderinfo["duration"] = duration[frame_count] - elif duration is None and "duration" in im_frame.info: - encoderinfo["duration"] = im_frame.info["duration"] - if isinstance(disposal, (list, tuple)): - encoderinfo["disposal"] = disposal[frame_count] - frame_count += 1 - - diff_frame = None - if im_frames and previous_im: - # delta frame - delta, bbox = _getbbox(previous_im, im_frame) - if not bbox: - # This frame is identical to the previous frame - if encoderinfo.get("duration"): - im_frames[-1].encoderinfo["duration"] += encoderinfo["duration"] - continue - if im_frames[-1].encoderinfo.get("disposal") == 2: - if background_im is None: - color = im.encoderinfo.get( - "transparency", im.info.get("transparency", (0, 0, 0)) - ) - background = _get_background(im_frame, color) - background_im = Image.new("P", im_frame.size, background) - background_im.putpalette(im_frames[0].im.palette) - bbox = _getbbox(background_im, im_frame)[1] - elif encoderinfo.get("optimize") and im_frame.mode != "1": - if "transparency" not in encoderinfo: - try: - encoderinfo["transparency"] = ( - im_frame.palette._new_color_index(im_frame) - ) - except ValueError: - pass - if "transparency" in encoderinfo: - # When the delta is zero, fill the image with transparency - diff_frame = im_frame.copy() - fill = Image.new("P", delta.size, encoderinfo["transparency"]) - if delta.mode == "RGBA": - r, g, b, a = delta.split() - mask = ImageMath.lambda_eval( - lambda args: args["convert"]( - args["max"]( - args["max"]( - args["max"](args["r"], args["g"]), args["b"] - ), - args["a"], - ) - * 255, - "1", - ), - r=r, - g=g, - b=b, - a=a, - ) - else: - if delta.mode == "P": - # Convert to L without considering palette - delta_l = Image.new("L", delta.size) - delta_l.putdata(delta.getdata()) - delta = delta_l - mask = ImageMath.lambda_eval( - lambda args: args["convert"](args["im"] * 255, "1"), - im=delta, - ) - diff_frame.paste(fill, mask=ImageOps.invert(mask)) - else: - bbox = None - previous_im = im_frame - im_frames.append(_Frame(diff_frame or im_frame, bbox, encoderinfo)) - - if len(im_frames) == 1: - if "duration" in im.encoderinfo: - # Since multiple frames will not be written, use the combined duration - im.encoderinfo["duration"] = im_frames[0].encoderinfo["duration"] - return False - - for frame_data in im_frames: - im_frame = frame_data.im - if not frame_data.bbox: - # global header - for s in _get_global_header(im_frame, frame_data.encoderinfo): - fp.write(s) - offset = (0, 0) - else: - # compress difference - if not palette: - frame_data.encoderinfo["include_color_table"] = True - - im_frame = im_frame.crop(frame_data.bbox) - offset = frame_data.bbox[:2] - _write_frame_data(fp, im_frame, offset, frame_data.encoderinfo) - return True - - -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: - # header - if "palette" in im.encoderinfo or "palette" in im.info: - palette = im.encoderinfo.get("palette", im.info.get("palette")) - else: - palette = None - im.encoderinfo.setdefault("optimize", True) - - if not save_all or not _write_multiple_frames(im, fp, palette): - _write_single_frame(im, fp, palette) - - fp.write(b";") # end of file - - if hasattr(fp, "flush"): - fp.flush() - - -def get_interlace(im: Image.Image) -> int: - interlace = im.encoderinfo.get("interlace", 1) - - # workaround for @PIL153 - if min(im.size) < 16: - interlace = 0 - - return interlace - - -def _write_local_header( - fp: IO[bytes], im: Image.Image, offset: tuple[int, int], flags: int -) -> None: - try: - transparency = im.encoderinfo["transparency"] - except KeyError: - transparency = None - - if "duration" in im.encoderinfo: - duration = int(im.encoderinfo["duration"] / 10) - else: - duration = 0 - - disposal = int(im.encoderinfo.get("disposal", 0)) - - if transparency is not None or duration != 0 or disposal: - packed_flag = 1 if transparency is not None else 0 - packed_flag |= disposal << 2 - - fp.write( - b"!" - + o8(249) # extension intro - + o8(4) # length - + o8(packed_flag) # packed fields - + o16(duration) # duration - + o8(transparency or 0) # transparency index - + o8(0) - ) - - include_color_table = im.encoderinfo.get("include_color_table") - if include_color_table: - palette_bytes = _get_palette_bytes(im) - color_table_size = _get_color_table_size(palette_bytes) - if color_table_size: - flags = flags | 128 # local color table flag - flags = flags | color_table_size - - fp.write( - b"," - + o16(offset[0]) # offset - + o16(offset[1]) - + o16(im.size[0]) # size - + o16(im.size[1]) - + o8(flags) # flags - ) - if include_color_table and color_table_size: - fp.write(_get_header_palette(palette_bytes)) - fp.write(o8(8)) # bits - - -def _save_netpbm(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - # Unused by default. - # To use, uncomment the register_save call at the end of the file. - # - # If you need real GIF compression and/or RGB quantization, you - # can use the external NETPBM/PBMPLUS utilities. See comments - # below for information on how to enable this. - tempfile = im._dump() - - try: - with open(filename, "wb") as f: - if im.mode != "RGB": - subprocess.check_call( - ["ppmtogif", tempfile], stdout=f, stderr=subprocess.DEVNULL - ) - else: - # Pipe ppmquant output into ppmtogif - # "ppmquant 256 %s | ppmtogif > %s" % (tempfile, filename) - quant_cmd = ["ppmquant", "256", tempfile] - togif_cmd = ["ppmtogif"] - quant_proc = subprocess.Popen( - quant_cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL - ) - togif_proc = subprocess.Popen( - togif_cmd, - stdin=quant_proc.stdout, - stdout=f, - stderr=subprocess.DEVNULL, - ) - - # Allow ppmquant to receive SIGPIPE if ppmtogif exits - assert quant_proc.stdout is not None - quant_proc.stdout.close() - - retcode = quant_proc.wait() - if retcode: - raise subprocess.CalledProcessError(retcode, quant_cmd) - - retcode = togif_proc.wait() - if retcode: - raise subprocess.CalledProcessError(retcode, togif_cmd) - finally: - try: - os.unlink(tempfile) - except OSError: - pass - - -# Force optimization so that we can test performance against -# cases where it took lots of memory and time previously. -_FORCE_OPTIMIZE = False - - -def _get_optimize(im: Image.Image, info: dict[str, Any]) -> list[int] | None: - """ - Palette optimization is a potentially expensive operation. - - This function determines if the palette should be optimized using - some heuristics, then returns the list of palette entries in use. - - :param im: Image object - :param info: encoderinfo - :returns: list of indexes of palette entries in use, or None - """ - if im.mode in ("P", "L") and info and info.get("optimize"): - # Potentially expensive operation. - - # The palette saves 3 bytes per color not used, but palette - # lengths are restricted to 3*(2**N) bytes. Max saving would - # be 768 -> 6 bytes if we went all the way down to 2 colors. - # * If we're over 128 colors, we can't save any space. - # * If there aren't any holes, it's not worth collapsing. - # * If we have a 'large' image, the palette is in the noise. - - # create the new palette if not every color is used - optimise = _FORCE_OPTIMIZE or im.mode == "L" - if optimise or im.width * im.height < 512 * 512: - # check which colors are used - used_palette_colors = [] - for i, count in enumerate(im.histogram()): - if count: - used_palette_colors.append(i) - - if optimise or max(used_palette_colors) >= len(used_palette_colors): - return used_palette_colors - - num_palette_colors = len(im.palette.palette) // Image.getmodebands( - im.palette.mode - ) - current_palette_size = 1 << (num_palette_colors - 1).bit_length() - if ( - # check that the palette would become smaller when saved - len(used_palette_colors) <= current_palette_size // 2 - # check that the palette is not already the smallest possible size - and current_palette_size > 2 - ): - return used_palette_colors - return None - - -def _get_color_table_size(palette_bytes: bytes) -> int: - # calculate the palette size for the header - if not palette_bytes: - return 0 - elif len(palette_bytes) < 9: - return 1 - else: - return math.ceil(math.log(len(palette_bytes) // 3, 2)) - 1 - - -def _get_header_palette(palette_bytes: bytes) -> bytes: - """ - Returns the palette, null padded to the next power of 2 (*3) bytes - suitable for direct inclusion in the GIF header - - :param palette_bytes: Unpadded palette bytes, in RGBRGB form - :returns: Null padded palette - """ - color_table_size = _get_color_table_size(palette_bytes) - - # add the missing amount of bytes - # the palette has to be 2< 0: - palette_bytes += o8(0) * 3 * actual_target_size_diff - return palette_bytes - - -def _get_palette_bytes(im: Image.Image) -> bytes: - """ - Gets the palette for inclusion in the gif header - - :param im: Image object - :returns: Bytes, len<=768 suitable for inclusion in gif header - """ - return im.palette.palette if im.palette else b"" - - -def _get_background( - im: Image.Image, - info_background: int | tuple[int, int, int] | tuple[int, int, int, int] | None, -) -> int: - background = 0 - if info_background: - if isinstance(info_background, tuple): - # WebPImagePlugin stores an RGBA value in info["background"] - # So it must be converted to the same format as GifImagePlugin's - # info["background"] - a global color table index - try: - background = im.palette.getcolor(info_background, im) - except ValueError as e: - if str(e) not in ( - # If all 256 colors are in use, - # then there is no need for the background color - "cannot allocate more than 256 colors", - # Ignore non-opaque WebP background - "cannot add non-opaque RGBA color to RGB palette", - ): - raise - else: - background = info_background - return background - - -def _get_global_header(im: Image.Image, info: dict[str, Any]) -> list[bytes]: - """Return a list of strings representing a GIF header""" - - # Header Block - # https://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp - - version = b"87a" - if im.info.get("version") == b"89a" or ( - info - and ( - "transparency" in info - or info.get("loop") is not None - or info.get("duration") - or info.get("comment") - ) - ): - version = b"89a" - - background = _get_background(im, info.get("background")) - - palette_bytes = _get_palette_bytes(im) - color_table_size = _get_color_table_size(palette_bytes) - - header = [ - b"GIF" # signature - + version # version - + o16(im.size[0]) # canvas width - + o16(im.size[1]), # canvas height - # Logical Screen Descriptor - # size of global color table + global color table flag - o8(color_table_size + 128), # packed fields - # background + reserved/aspect - o8(background) + o8(0), - # Global Color Table - _get_header_palette(palette_bytes), - ] - if info.get("loop") is not None: - header.append( - b"!" - + o8(255) # extension intro - + o8(11) - + b"NETSCAPE2.0" - + o8(3) - + o8(1) - + o16(info["loop"]) # number of loops - + o8(0) - ) - if info.get("comment"): - comment_block = b"!" + o8(254) # extension intro - - comment = info["comment"] - if isinstance(comment, str): - comment = comment.encode() - for i in range(0, len(comment), 255): - subblock = comment[i : i + 255] - comment_block += o8(len(subblock)) + subblock - - comment_block += o8(0) - header.append(comment_block) - return header - - -def _write_frame_data( - fp: IO[bytes], - im_frame: Image.Image, - offset: tuple[int, int], - params: dict[str, Any], -) -> None: - try: - im_frame.encoderinfo = params - - # local image header - _write_local_header(fp, im_frame, offset, 0) - - ImageFile._save( - im_frame, fp, [("gif", (0, 0) + im_frame.size, 0, RAWMODE[im_frame.mode])] - ) - - fp.write(b"\0") # end of image data - finally: - del im_frame.encoderinfo - - -# -------------------------------------------------------------------- -# Legacy GIF utilities - - -def getheader( - im: Image.Image, palette: _Palette | None = None, info: dict[str, Any] | None = None -) -> tuple[list[bytes], list[int] | None]: - """ - Legacy Method to get Gif data from image. - - Warning:: May modify image data. - - :param im: Image object - :param palette: bytes object containing the source palette, or .... - :param info: encoderinfo - :returns: tuple of(list of header items, optimized palette) - - """ - if info is None: - info = {} - - used_palette_colors = _get_optimize(im, info) - - if "background" not in info and "background" in im.info: - info["background"] = im.info["background"] - - im_mod = _normalize_palette(im, palette, info) - im.palette = im_mod.palette - im.im = im_mod.im - header = _get_global_header(im, info) - - return header, used_palette_colors - - -def getdata( - im: Image.Image, offset: tuple[int, int] = (0, 0), **params: Any -) -> list[bytes]: - """ - Legacy Method - - Return a list of strings representing this image. - The first string is a local image header, the rest contains - encoded image data. - - To specify duration, add the time in milliseconds, - e.g. ``getdata(im_frame, duration=1000)`` - - :param im: Image object - :param offset: Tuple of (x, y) pixels. Defaults to (0, 0) - :param \\**params: e.g. duration or other encoder info parameters - :returns: List of bytes containing GIF encoded frame data - - """ - from io import BytesIO - - class Collector(BytesIO): - data = [] - - if sys.version_info >= (3, 12): - from collections.abc import Buffer - - def write(self, data: Buffer) -> int: - self.data.append(data) - return len(data) - - else: - - def write(self, data: Any) -> int: - self.data.append(data) - return len(data) - - im.load() # make sure raster data is available - - fp = Collector() - - _write_frame_data(fp, im, offset, params) - - return fp.data - - -# -------------------------------------------------------------------- -# Registry - -Image.register_open(GifImageFile.format, GifImageFile, _accept) -Image.register_save(GifImageFile.format, _save) -Image.register_save_all(GifImageFile.format, _save_all) -Image.register_extension(GifImageFile.format, ".gif") -Image.register_mime(GifImageFile.format, "image/gif") - -# -# Uncomment the following line if you wish to use NETPBM/PBMPLUS -# instead of the built-in "uncompressed" GIF encoder - -# Image.register_save(GifImageFile.format, _save_netpbm) +# +# The Python Imaging Library. +# $Id$ +# +# GIF file handling +# +# History: +# 1995-09-01 fl Created +# 1996-12-14 fl Added interlace support +# 1996-12-30 fl Added animation support +# 1997-01-05 fl Added write support, fixed local colour map bug +# 1997-02-23 fl Make sure to load raster data in getdata() +# 1997-07-05 fl Support external decoder (0.4) +# 1998-07-09 fl Handle all modes when saving (0.5) +# 1998-07-15 fl Renamed offset attribute to avoid name clash +# 2001-04-16 fl Added rewind support (seek to frame 0) (0.6) +# 2001-04-17 fl Added palette optimization (0.7) +# 2002-06-06 fl Added transparency support for save (0.8) +# 2004-02-24 fl Disable interlacing for small images +# +# Copyright (c) 1997-2004 by Secret Labs AB +# Copyright (c) 1995-2004 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import itertools +import math +import os +import subprocess +import sys +from enum import IntEnum +from functools import cached_property +from typing import IO, TYPE_CHECKING, Any, List, Literal, NamedTuple, Union + +from . import ( + Image, + ImageChops, + ImageFile, + ImageMath, + ImageOps, + ImagePalette, + ImageSequence, +) +from ._binary import i16le as i16 +from ._binary import o8 +from ._binary import o16le as o16 + +if TYPE_CHECKING: + from . import _imaging + + +class LoadingStrategy(IntEnum): + """.. versionadded:: 9.1.0""" + + RGB_AFTER_FIRST = 0 + RGB_AFTER_DIFFERENT_PALETTE_ONLY = 1 + RGB_ALWAYS = 2 + + +#: .. versionadded:: 9.1.0 +LOADING_STRATEGY = LoadingStrategy.RGB_AFTER_FIRST + +# -------------------------------------------------------------------- +# Identify/read GIF files + + +def _accept(prefix: bytes) -> bool: + return prefix[:6] in [b"GIF87a", b"GIF89a"] + + +## +# Image plugin for GIF images. This plugin supports both GIF87 and +# GIF89 images. + + +class GifImageFile(ImageFile.ImageFile): + format = "GIF" + format_description = "Compuserve GIF" + _close_exclusive_fp_after_loading = False + + global_palette = None + + def data(self) -> bytes | None: + s = self.fp.read(1) + if s and s[0]: + return self.fp.read(s[0]) + return None + + def _is_palette_needed(self, p: bytes) -> bool: + for i in range(0, len(p), 3): + if not (i // 3 == p[i] == p[i + 1] == p[i + 2]): + return True + return False + + def _open(self) -> None: + # Screen + s = self.fp.read(13) + if not _accept(s): + msg = "not a GIF file" + raise SyntaxError(msg) + + self.info["version"] = s[:6] + self._size = i16(s, 6), i16(s, 8) + self.tile = [] + flags = s[10] + bits = (flags & 7) + 1 + + if flags & 128: + # get global palette + self.info["background"] = s[11] + # check if palette contains colour indices + p = self.fp.read(3 << bits) + if self._is_palette_needed(p): + p = ImagePalette.raw("RGB", p) + self.global_palette = self.palette = p + + self._fp = self.fp # FIXME: hack + self.__rewind = self.fp.tell() + self._n_frames: int | None = None + self._seek(0) # get ready to read first frame + + @property + def n_frames(self) -> int: + if self._n_frames is None: + current = self.tell() + try: + while True: + self._seek(self.tell() + 1, False) + except EOFError: + self._n_frames = self.tell() + 1 + self.seek(current) + return self._n_frames + + @cached_property + def is_animated(self) -> bool: + if self._n_frames is not None: + return self._n_frames != 1 + + current = self.tell() + if current: + return True + + try: + self._seek(1, False) + is_animated = True + except EOFError: + is_animated = False + + self.seek(current) + return is_animated + + def seek(self, frame: int) -> None: + if not self._seek_check(frame): + return + if frame < self.__frame: + self.im = None + self._seek(0) + + last_frame = self.__frame + for f in range(self.__frame + 1, frame + 1): + try: + self._seek(f) + except EOFError as e: + self.seek(last_frame) + msg = "no more images in GIF file" + raise EOFError(msg) from e + + def _seek(self, frame: int, update_image: bool = True) -> None: + if frame == 0: + # rewind + self.__offset = 0 + self.dispose: _imaging.ImagingCore | None = None + self.__frame = -1 + self._fp.seek(self.__rewind) + self.disposal_method = 0 + if "comment" in self.info: + del self.info["comment"] + else: + # ensure that the previous frame was loaded + if self.tile and update_image: + self.load() + + if frame != self.__frame + 1: + msg = f"cannot seek to frame {frame}" + raise ValueError(msg) + + self.fp = self._fp + if self.__offset: + # backup to last frame + self.fp.seek(self.__offset) + while self.data(): + pass + self.__offset = 0 + + s = self.fp.read(1) + if not s or s == b";": + msg = "no more images in GIF file" + raise EOFError(msg) + + palette: ImagePalette.ImagePalette | Literal[False] | None = None + + info: dict[str, Any] = {} + frame_transparency = None + interlace = None + frame_dispose_extent = None + while True: + if not s: + s = self.fp.read(1) + if not s or s == b";": + break + + elif s == b"!": + # + # extensions + # + s = self.fp.read(1) + block = self.data() + if s[0] == 249 and block is not None: + # + # graphic control extension + # + flags = block[0] + if flags & 1: + frame_transparency = block[3] + info["duration"] = i16(block, 1) * 10 + + # disposal method - find the value of bits 4 - 6 + dispose_bits = 0b00011100 & flags + dispose_bits = dispose_bits >> 2 + if dispose_bits: + # only set the dispose if it is not + # unspecified. I'm not sure if this is + # correct, but it seems to prevent the last + # frame from looking odd for some animations + self.disposal_method = dispose_bits + elif s[0] == 254: + # + # comment extension + # + comment = b"" + + # Read this comment block + while block: + comment += block + block = self.data() + + if "comment" in info: + # If multiple comment blocks in frame, separate with \n + info["comment"] += b"\n" + comment + else: + info["comment"] = comment + s = None + continue + elif s[0] == 255 and frame == 0 and block is not None: + # + # application extension + # + info["extension"] = block, self.fp.tell() + if block[:11] == b"NETSCAPE2.0": + block = self.data() + if block and len(block) >= 3 and block[0] == 1: + self.info["loop"] = i16(block, 1) + while self.data(): + pass + + elif s == b",": + # + # local image + # + s = self.fp.read(9) + + # extent + x0, y0 = i16(s, 0), i16(s, 2) + x1, y1 = x0 + i16(s, 4), y0 + i16(s, 6) + if (x1 > self.size[0] or y1 > self.size[1]) and update_image: + self._size = max(x1, self.size[0]), max(y1, self.size[1]) + Image._decompression_bomb_check(self._size) + frame_dispose_extent = x0, y0, x1, y1 + flags = s[8] + + interlace = (flags & 64) != 0 + + if flags & 128: + bits = (flags & 7) + 1 + p = self.fp.read(3 << bits) + if self._is_palette_needed(p): + palette = ImagePalette.raw("RGB", p) + else: + palette = False + + # image data + bits = self.fp.read(1)[0] + self.__offset = self.fp.tell() + break + s = None + + if interlace is None: + msg = "image not found in GIF frame" + raise EOFError(msg) + + self.__frame = frame + if not update_image: + return + + self.tile = [] + + if self.dispose: + self.im.paste(self.dispose, self.dispose_extent) + + self._frame_palette = palette if palette is not None else self.global_palette + self._frame_transparency = frame_transparency + if frame == 0: + if self._frame_palette: + if LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS: + self._mode = "RGBA" if frame_transparency is not None else "RGB" + else: + self._mode = "P" + else: + self._mode = "L" + + if not palette and self.global_palette: + from copy import copy + + palette = copy(self.global_palette) + self.palette = palette + else: + if self.mode == "P": + if ( + LOADING_STRATEGY != LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY + or palette + ): + self.pyaccess = None + if "transparency" in self.info: + self.im.putpalettealpha(self.info["transparency"], 0) + self.im = self.im.convert("RGBA", Image.Dither.FLOYDSTEINBERG) + self._mode = "RGBA" + del self.info["transparency"] + else: + self._mode = "RGB" + self.im = self.im.convert("RGB", Image.Dither.FLOYDSTEINBERG) + + def _rgb(color: int) -> tuple[int, int, int]: + if self._frame_palette: + if color * 3 + 3 > len(self._frame_palette.palette): + color = 0 + return tuple(self._frame_palette.palette[color * 3 : color * 3 + 3]) + else: + return (color, color, color) + + self.dispose = None + self.dispose_extent = frame_dispose_extent + if self.dispose_extent and self.disposal_method >= 2: + try: + if self.disposal_method == 2: + # replace with background colour + + # only dispose the extent in this frame + x0, y0, x1, y1 = self.dispose_extent + dispose_size = (x1 - x0, y1 - y0) + + Image._decompression_bomb_check(dispose_size) + + # by convention, attempt to use transparency first + dispose_mode = "P" + color = self.info.get("transparency", frame_transparency) + if color is not None: + if self.mode in ("RGB", "RGBA"): + dispose_mode = "RGBA" + color = _rgb(color) + (0,) + else: + color = self.info.get("background", 0) + if self.mode in ("RGB", "RGBA"): + dispose_mode = "RGB" + color = _rgb(color) + self.dispose = Image.core.fill(dispose_mode, dispose_size, color) + else: + # replace with previous contents + if self.im is not None: + # only dispose the extent in this frame + self.dispose = self._crop(self.im, self.dispose_extent) + elif frame_transparency is not None: + x0, y0, x1, y1 = self.dispose_extent + dispose_size = (x1 - x0, y1 - y0) + + Image._decompression_bomb_check(dispose_size) + dispose_mode = "P" + color = frame_transparency + if self.mode in ("RGB", "RGBA"): + dispose_mode = "RGBA" + color = _rgb(frame_transparency) + (0,) + self.dispose = Image.core.fill( + dispose_mode, dispose_size, color + ) + except AttributeError: + pass + + if interlace is not None: + transparency = -1 + if frame_transparency is not None: + if frame == 0: + if LOADING_STRATEGY != LoadingStrategy.RGB_ALWAYS: + self.info["transparency"] = frame_transparency + elif self.mode not in ("RGB", "RGBA"): + transparency = frame_transparency + self.tile = [ + ( + "gif", + (x0, y0, x1, y1), + self.__offset, + (bits, interlace, transparency), + ) + ] + + if info.get("comment"): + self.info["comment"] = info["comment"] + for k in ["duration", "extension"]: + if k in info: + self.info[k] = info[k] + elif k in self.info: + del self.info[k] + + def load_prepare(self) -> None: + temp_mode = "P" if self._frame_palette else "L" + self._prev_im = None + if self.__frame == 0: + if self._frame_transparency is not None: + self.im = Image.core.fill( + temp_mode, self.size, self._frame_transparency + ) + elif self.mode in ("RGB", "RGBA"): + self._prev_im = self.im + if self._frame_palette: + self.im = Image.core.fill("P", self.size, self._frame_transparency or 0) + self.im.putpalette(*self._frame_palette.getdata()) + else: + self.im = None + self._mode = temp_mode + self._frame_palette = None + + super().load_prepare() + + def load_end(self) -> None: + if self.__frame == 0: + if self.mode == "P" and LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS: + if self._frame_transparency is not None: + self.im.putpalettealpha(self._frame_transparency, 0) + self._mode = "RGBA" + else: + self._mode = "RGB" + self.im = self.im.convert(self.mode, Image.Dither.FLOYDSTEINBERG) + return + if not self._prev_im: + return + if self._frame_transparency is not None: + self.im.putpalettealpha(self._frame_transparency, 0) + frame_im = self.im.convert("RGBA") + else: + frame_im = self.im.convert("RGB") + frame_im = self._crop(frame_im, self.dispose_extent) + + self.im = self._prev_im + self._mode = self.im.mode + if frame_im.mode == "RGBA": + self.im.paste(frame_im, self.dispose_extent, frame_im) + else: + self.im.paste(frame_im, self.dispose_extent) + + def tell(self) -> int: + return self.__frame + + +# -------------------------------------------------------------------- +# Write GIF files + + +RAWMODE = {"1": "L", "L": "L", "P": "P"} + + +def _normalize_mode(im: Image.Image) -> Image.Image: + """ + Takes an image (or frame), returns an image in a mode that is appropriate + for saving in a Gif. + + It may return the original image, or it may return an image converted to + palette or 'L' mode. + + :param im: Image object + :returns: Image object + """ + if im.mode in RAWMODE: + im.load() + return im + if Image.getmodebase(im.mode) == "RGB": + im = im.convert("P", palette=Image.Palette.ADAPTIVE) + if im.palette.mode == "RGBA": + for rgba in im.palette.colors: + if rgba[3] == 0: + im.info["transparency"] = im.palette.colors[rgba] + break + return im + return im.convert("L") + + +_Palette = Union[bytes, bytearray, List[int], ImagePalette.ImagePalette] + + +def _normalize_palette( + im: Image.Image, palette: _Palette | None, info: dict[str, Any] +) -> Image.Image: + """ + Normalizes the palette for image. + - Sets the palette to the incoming palette, if provided. + - Ensures that there's a palette for L mode images + - Optimizes the palette if necessary/desired. + + :param im: Image object + :param palette: bytes object containing the source palette, or .... + :param info: encoderinfo + :returns: Image object + """ + source_palette = None + if palette: + # a bytes palette + if isinstance(palette, (bytes, bytearray, list)): + source_palette = bytearray(palette[:768]) + if isinstance(palette, ImagePalette.ImagePalette): + source_palette = bytearray(palette.palette) + + if im.mode == "P": + if not source_palette: + source_palette = im.im.getpalette("RGB")[:768] + else: # L-mode + if not source_palette: + source_palette = bytearray(i // 3 for i in range(768)) + im.palette = ImagePalette.ImagePalette("RGB", palette=source_palette) + + used_palette_colors: list[int] | None + if palette: + used_palette_colors = [] + assert source_palette is not None + for i in range(0, len(source_palette), 3): + source_color = tuple(source_palette[i : i + 3]) + index = im.palette.colors.get(source_color) + if index in used_palette_colors: + index = None + used_palette_colors.append(index) + for i, index in enumerate(used_palette_colors): + if index is None: + for j in range(len(used_palette_colors)): + if j not in used_palette_colors: + used_palette_colors[i] = j + break + im = im.remap_palette(used_palette_colors) + else: + used_palette_colors = _get_optimize(im, info) + if used_palette_colors is not None: + im = im.remap_palette(used_palette_colors, source_palette) + if "transparency" in info: + try: + info["transparency"] = used_palette_colors.index( + info["transparency"] + ) + except ValueError: + del info["transparency"] + return im + + im.palette.palette = source_palette + return im + + +def _write_single_frame( + im: Image.Image, + fp: IO[bytes], + palette: _Palette | None, +) -> None: + im_out = _normalize_mode(im) + for k, v in im_out.info.items(): + im.encoderinfo.setdefault(k, v) + im_out = _normalize_palette(im_out, palette, im.encoderinfo) + + for s in _get_global_header(im_out, im.encoderinfo): + fp.write(s) + + # local image header + flags = 0 + if get_interlace(im): + flags = flags | 64 + _write_local_header(fp, im, (0, 0), flags) + + im_out.encoderconfig = (8, get_interlace(im)) + ImageFile._save(im_out, fp, [("gif", (0, 0) + im.size, 0, RAWMODE[im_out.mode])]) + + fp.write(b"\0") # end of image data + + +def _getbbox( + base_im: Image.Image, im_frame: Image.Image +) -> tuple[Image.Image, tuple[int, int, int, int] | None]: + if _get_palette_bytes(im_frame) != _get_palette_bytes(base_im): + im_frame = im_frame.convert("RGBA") + base_im = base_im.convert("RGBA") + delta = ImageChops.subtract_modulo(im_frame, base_im) + return delta, delta.getbbox(alpha_only=False) + + +class _Frame(NamedTuple): + im: Image.Image + bbox: tuple[int, int, int, int] | None + encoderinfo: dict[str, Any] + + +def _write_multiple_frames( + im: Image.Image, fp: IO[bytes], palette: _Palette | None +) -> bool: + duration = im.encoderinfo.get("duration") + disposal = im.encoderinfo.get("disposal", im.info.get("disposal")) + + im_frames: list[_Frame] = [] + previous_im: Image.Image | None = None + frame_count = 0 + background_im = None + for imSequence in itertools.chain([im], im.encoderinfo.get("append_images", [])): + for im_frame in ImageSequence.Iterator(imSequence): + # a copy is required here since seek can still mutate the image + im_frame = _normalize_mode(im_frame.copy()) + if frame_count == 0: + for k, v in im_frame.info.items(): + if k == "transparency": + continue + im.encoderinfo.setdefault(k, v) + + encoderinfo = im.encoderinfo.copy() + if "transparency" in im_frame.info: + encoderinfo.setdefault("transparency", im_frame.info["transparency"]) + im_frame = _normalize_palette(im_frame, palette, encoderinfo) + if isinstance(duration, (list, tuple)): + encoderinfo["duration"] = duration[frame_count] + elif duration is None and "duration" in im_frame.info: + encoderinfo["duration"] = im_frame.info["duration"] + if isinstance(disposal, (list, tuple)): + encoderinfo["disposal"] = disposal[frame_count] + frame_count += 1 + + diff_frame = None + if im_frames and previous_im: + # delta frame + delta, bbox = _getbbox(previous_im, im_frame) + if not bbox: + # This frame is identical to the previous frame + if encoderinfo.get("duration"): + im_frames[-1].encoderinfo["duration"] += encoderinfo["duration"] + continue + if im_frames[-1].encoderinfo.get("disposal") == 2: + if background_im is None: + color = im.encoderinfo.get( + "transparency", im.info.get("transparency", (0, 0, 0)) + ) + background = _get_background(im_frame, color) + background_im = Image.new("P", im_frame.size, background) + background_im.putpalette(im_frames[0].im.palette) + bbox = _getbbox(background_im, im_frame)[1] + elif encoderinfo.get("optimize") and im_frame.mode != "1": + if "transparency" not in encoderinfo: + try: + encoderinfo["transparency"] = ( + im_frame.palette._new_color_index(im_frame) + ) + except ValueError: + pass + if "transparency" in encoderinfo: + # When the delta is zero, fill the image with transparency + diff_frame = im_frame.copy() + fill = Image.new("P", delta.size, encoderinfo["transparency"]) + if delta.mode == "RGBA": + r, g, b, a = delta.split() + mask = ImageMath.lambda_eval( + lambda args: args["convert"]( + args["max"]( + args["max"]( + args["max"](args["r"], args["g"]), args["b"] + ), + args["a"], + ) + * 255, + "1", + ), + r=r, + g=g, + b=b, + a=a, + ) + else: + if delta.mode == "P": + # Convert to L without considering palette + delta_l = Image.new("L", delta.size) + delta_l.putdata(delta.getdata()) + delta = delta_l + mask = ImageMath.lambda_eval( + lambda args: args["convert"](args["im"] * 255, "1"), + im=delta, + ) + diff_frame.paste(fill, mask=ImageOps.invert(mask)) + else: + bbox = None + previous_im = im_frame + im_frames.append(_Frame(diff_frame or im_frame, bbox, encoderinfo)) + + if len(im_frames) == 1: + if "duration" in im.encoderinfo: + # Since multiple frames will not be written, use the combined duration + im.encoderinfo["duration"] = im_frames[0].encoderinfo["duration"] + return False + + for frame_data in im_frames: + im_frame = frame_data.im + if not frame_data.bbox: + # global header + for s in _get_global_header(im_frame, frame_data.encoderinfo): + fp.write(s) + offset = (0, 0) + else: + # compress difference + if not palette: + frame_data.encoderinfo["include_color_table"] = True + + im_frame = im_frame.crop(frame_data.bbox) + offset = frame_data.bbox[:2] + _write_frame_data(fp, im_frame, offset, frame_data.encoderinfo) + return True + + +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: + # header + if "palette" in im.encoderinfo or "palette" in im.info: + palette = im.encoderinfo.get("palette", im.info.get("palette")) + else: + palette = None + im.encoderinfo.setdefault("optimize", True) + + if not save_all or not _write_multiple_frames(im, fp, palette): + _write_single_frame(im, fp, palette) + + fp.write(b";") # end of file + + if hasattr(fp, "flush"): + fp.flush() + + +def get_interlace(im: Image.Image) -> int: + interlace = im.encoderinfo.get("interlace", 1) + + # workaround for @PIL153 + if min(im.size) < 16: + interlace = 0 + + return interlace + + +def _write_local_header( + fp: IO[bytes], im: Image.Image, offset: tuple[int, int], flags: int +) -> None: + try: + transparency = im.encoderinfo["transparency"] + except KeyError: + transparency = None + + if "duration" in im.encoderinfo: + duration = int(im.encoderinfo["duration"] / 10) + else: + duration = 0 + + disposal = int(im.encoderinfo.get("disposal", 0)) + + if transparency is not None or duration != 0 or disposal: + packed_flag = 1 if transparency is not None else 0 + packed_flag |= disposal << 2 + + fp.write( + b"!" + + o8(249) # extension intro + + o8(4) # length + + o8(packed_flag) # packed fields + + o16(duration) # duration + + o8(transparency or 0) # transparency index + + o8(0) + ) + + include_color_table = im.encoderinfo.get("include_color_table") + if include_color_table: + palette_bytes = _get_palette_bytes(im) + color_table_size = _get_color_table_size(palette_bytes) + if color_table_size: + flags = flags | 128 # local color table flag + flags = flags | color_table_size + + fp.write( + b"," + + o16(offset[0]) # offset + + o16(offset[1]) + + o16(im.size[0]) # size + + o16(im.size[1]) + + o8(flags) # flags + ) + if include_color_table and color_table_size: + fp.write(_get_header_palette(palette_bytes)) + fp.write(o8(8)) # bits + + +def _save_netpbm(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + # Unused by default. + # To use, uncomment the register_save call at the end of the file. + # + # If you need real GIF compression and/or RGB quantization, you + # can use the external NETPBM/PBMPLUS utilities. See comments + # below for information on how to enable this. + tempfile = im._dump() + + try: + with open(filename, "wb") as f: + if im.mode != "RGB": + subprocess.check_call( + ["ppmtogif", tempfile], stdout=f, stderr=subprocess.DEVNULL + ) + else: + # Pipe ppmquant output into ppmtogif + # "ppmquant 256 %s | ppmtogif > %s" % (tempfile, filename) + quant_cmd = ["ppmquant", "256", tempfile] + togif_cmd = ["ppmtogif"] + quant_proc = subprocess.Popen( + quant_cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL + ) + togif_proc = subprocess.Popen( + togif_cmd, + stdin=quant_proc.stdout, + stdout=f, + stderr=subprocess.DEVNULL, + ) + + # Allow ppmquant to receive SIGPIPE if ppmtogif exits + assert quant_proc.stdout is not None + quant_proc.stdout.close() + + retcode = quant_proc.wait() + if retcode: + raise subprocess.CalledProcessError(retcode, quant_cmd) + + retcode = togif_proc.wait() + if retcode: + raise subprocess.CalledProcessError(retcode, togif_cmd) + finally: + try: + os.unlink(tempfile) + except OSError: + pass + + +# Force optimization so that we can test performance against +# cases where it took lots of memory and time previously. +_FORCE_OPTIMIZE = False + + +def _get_optimize(im: Image.Image, info: dict[str, Any]) -> list[int] | None: + """ + Palette optimization is a potentially expensive operation. + + This function determines if the palette should be optimized using + some heuristics, then returns the list of palette entries in use. + + :param im: Image object + :param info: encoderinfo + :returns: list of indexes of palette entries in use, or None + """ + if im.mode in ("P", "L") and info and info.get("optimize"): + # Potentially expensive operation. + + # The palette saves 3 bytes per color not used, but palette + # lengths are restricted to 3*(2**N) bytes. Max saving would + # be 768 -> 6 bytes if we went all the way down to 2 colors. + # * If we're over 128 colors, we can't save any space. + # * If there aren't any holes, it's not worth collapsing. + # * If we have a 'large' image, the palette is in the noise. + + # create the new palette if not every color is used + optimise = _FORCE_OPTIMIZE or im.mode == "L" + if optimise or im.width * im.height < 512 * 512: + # check which colors are used + used_palette_colors = [] + for i, count in enumerate(im.histogram()): + if count: + used_palette_colors.append(i) + + if optimise or max(used_palette_colors) >= len(used_palette_colors): + return used_palette_colors + + num_palette_colors = len(im.palette.palette) // Image.getmodebands( + im.palette.mode + ) + current_palette_size = 1 << (num_palette_colors - 1).bit_length() + if ( + # check that the palette would become smaller when saved + len(used_palette_colors) <= current_palette_size // 2 + # check that the palette is not already the smallest possible size + and current_palette_size > 2 + ): + return used_palette_colors + return None + + +def _get_color_table_size(palette_bytes: bytes) -> int: + # calculate the palette size for the header + if not palette_bytes: + return 0 + elif len(palette_bytes) < 9: + return 1 + else: + return math.ceil(math.log(len(palette_bytes) // 3, 2)) - 1 + + +def _get_header_palette(palette_bytes: bytes) -> bytes: + """ + Returns the palette, null padded to the next power of 2 (*3) bytes + suitable for direct inclusion in the GIF header + + :param palette_bytes: Unpadded palette bytes, in RGBRGB form + :returns: Null padded palette + """ + color_table_size = _get_color_table_size(palette_bytes) + + # add the missing amount of bytes + # the palette has to be 2< 0: + palette_bytes += o8(0) * 3 * actual_target_size_diff + return palette_bytes + + +def _get_palette_bytes(im: Image.Image) -> bytes: + """ + Gets the palette for inclusion in the gif header + + :param im: Image object + :returns: Bytes, len<=768 suitable for inclusion in gif header + """ + return im.palette.palette if im.palette else b"" + + +def _get_background( + im: Image.Image, + info_background: int | tuple[int, int, int] | tuple[int, int, int, int] | None, +) -> int: + background = 0 + if info_background: + if isinstance(info_background, tuple): + # WebPImagePlugin stores an RGBA value in info["background"] + # So it must be converted to the same format as GifImagePlugin's + # info["background"] - a global color table index + try: + background = im.palette.getcolor(info_background, im) + except ValueError as e: + if str(e) not in ( + # If all 256 colors are in use, + # then there is no need for the background color + "cannot allocate more than 256 colors", + # Ignore non-opaque WebP background + "cannot add non-opaque RGBA color to RGB palette", + ): + raise + else: + background = info_background + return background + + +def _get_global_header(im: Image.Image, info: dict[str, Any]) -> list[bytes]: + """Return a list of strings representing a GIF header""" + + # Header Block + # https://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp + + version = b"87a" + if im.info.get("version") == b"89a" or ( + info + and ( + "transparency" in info + or info.get("loop") is not None + or info.get("duration") + or info.get("comment") + ) + ): + version = b"89a" + + background = _get_background(im, info.get("background")) + + palette_bytes = _get_palette_bytes(im) + color_table_size = _get_color_table_size(palette_bytes) + + header = [ + b"GIF" # signature + + version # version + + o16(im.size[0]) # canvas width + + o16(im.size[1]), # canvas height + # Logical Screen Descriptor + # size of global color table + global color table flag + o8(color_table_size + 128), # packed fields + # background + reserved/aspect + o8(background) + o8(0), + # Global Color Table + _get_header_palette(palette_bytes), + ] + if info.get("loop") is not None: + header.append( + b"!" + + o8(255) # extension intro + + o8(11) + + b"NETSCAPE2.0" + + o8(3) + + o8(1) + + o16(info["loop"]) # number of loops + + o8(0) + ) + if info.get("comment"): + comment_block = b"!" + o8(254) # extension intro + + comment = info["comment"] + if isinstance(comment, str): + comment = comment.encode() + for i in range(0, len(comment), 255): + subblock = comment[i : i + 255] + comment_block += o8(len(subblock)) + subblock + + comment_block += o8(0) + header.append(comment_block) + return header + + +def _write_frame_data( + fp: IO[bytes], + im_frame: Image.Image, + offset: tuple[int, int], + params: dict[str, Any], +) -> None: + try: + im_frame.encoderinfo = params + + # local image header + _write_local_header(fp, im_frame, offset, 0) + + ImageFile._save( + im_frame, fp, [("gif", (0, 0) + im_frame.size, 0, RAWMODE[im_frame.mode])] + ) + + fp.write(b"\0") # end of image data + finally: + del im_frame.encoderinfo + + +# -------------------------------------------------------------------- +# Legacy GIF utilities + + +def getheader( + im: Image.Image, palette: _Palette | None = None, info: dict[str, Any] | None = None +) -> tuple[list[bytes], list[int] | None]: + """ + Legacy Method to get Gif data from image. + + Warning:: May modify image data. + + :param im: Image object + :param palette: bytes object containing the source palette, or .... + :param info: encoderinfo + :returns: tuple of(list of header items, optimized palette) + + """ + if info is None: + info = {} + + used_palette_colors = _get_optimize(im, info) + + if "background" not in info and "background" in im.info: + info["background"] = im.info["background"] + + im_mod = _normalize_palette(im, palette, info) + im.palette = im_mod.palette + im.im = im_mod.im + header = _get_global_header(im, info) + + return header, used_palette_colors + + +def getdata( + im: Image.Image, offset: tuple[int, int] = (0, 0), **params: Any +) -> list[bytes]: + """ + Legacy Method + + Return a list of strings representing this image. + The first string is a local image header, the rest contains + encoded image data. + + To specify duration, add the time in milliseconds, + e.g. ``getdata(im_frame, duration=1000)`` + + :param im: Image object + :param offset: Tuple of (x, y) pixels. Defaults to (0, 0) + :param \\**params: e.g. duration or other encoder info parameters + :returns: List of bytes containing GIF encoded frame data + + """ + from io import BytesIO + + class Collector(BytesIO): + data = [] + + if sys.version_info >= (3, 12): + from collections.abc import Buffer + + def write(self, data: Buffer) -> int: + self.data.append(data) + return len(data) + + else: + + def write(self, data: Any) -> int: + self.data.append(data) + return len(data) + + im.load() # make sure raster data is available + + fp = Collector() + + _write_frame_data(fp, im, offset, params) + + return fp.data + + +# -------------------------------------------------------------------- +# Registry + +Image.register_open(GifImageFile.format, GifImageFile, _accept) +Image.register_save(GifImageFile.format, _save) +Image.register_save_all(GifImageFile.format, _save_all) +Image.register_extension(GifImageFile.format, ".gif") +Image.register_mime(GifImageFile.format, "image/gif") + +# +# Uncomment the following line if you wish to use NETPBM/PBMPLUS +# instead of the built-in "uncompressed" GIF encoder + +# Image.register_save(GifImageFile.format, _save_netpbm) diff --git a/src/PIL/GimpGradientFile.py b/src/PIL/GimpGradientFile.py index 220eac57e..59e50a9f3 100644 --- a/src/PIL/GimpGradientFile.py +++ b/src/PIL/GimpGradientFile.py @@ -1,149 +1,149 @@ -# -# Python Imaging Library -# $Id$ -# -# stuff to read (and render) GIMP gradient files -# -# History: -# 97-08-23 fl Created -# -# Copyright (c) Secret Labs AB 1997. -# Copyright (c) Fredrik Lundh 1997. -# -# See the README file for information on usage and redistribution. -# - -""" -Stuff to translate curve segments to palette values (derived from -the corresponding code in GIMP, written by Federico Mena Quintero. -See the GIMP distribution for more information.) -""" -from __future__ import annotations - -from math import log, pi, sin, sqrt -from typing import IO, Callable - -from ._binary import o8 - -EPSILON = 1e-10 -"""""" # Enable auto-doc for data member - - -def linear(middle: float, pos: float) -> float: - if pos <= middle: - if middle < EPSILON: - return 0.0 - else: - return 0.5 * pos / middle - else: - pos = pos - middle - middle = 1.0 - middle - if middle < EPSILON: - return 1.0 - else: - return 0.5 + 0.5 * pos / middle - - -def curved(middle: float, pos: float) -> float: - return pos ** (log(0.5) / log(max(middle, EPSILON))) - - -def sine(middle: float, pos: float) -> float: - return (sin((-pi / 2.0) + pi * linear(middle, pos)) + 1.0) / 2.0 - - -def sphere_increasing(middle: float, pos: float) -> float: - return sqrt(1.0 - (linear(middle, pos) - 1.0) ** 2) - - -def sphere_decreasing(middle: float, pos: float) -> float: - return 1.0 - sqrt(1.0 - linear(middle, pos) ** 2) - - -SEGMENTS = [linear, curved, sine, sphere_increasing, sphere_decreasing] -"""""" # Enable auto-doc for data member - - -class GradientFile: - gradient: ( - list[ - tuple[ - float, - float, - float, - list[float], - list[float], - Callable[[float, float], float], - ] - ] - | None - ) = None - - def getpalette(self, entries: int = 256) -> tuple[bytes, str]: - assert self.gradient is not None - palette = [] - - ix = 0 - x0, x1, xm, rgb0, rgb1, segment = self.gradient[ix] - - for i in range(entries): - x = i / (entries - 1) - - while x1 < x: - ix += 1 - x0, x1, xm, rgb0, rgb1, segment = self.gradient[ix] - - w = x1 - x0 - - if w < EPSILON: - scale = segment(0.5, 0.5) - else: - scale = segment((xm - x0) / w, (x - x0) / w) - - # expand to RGBA - r = o8(int(255 * ((rgb1[0] - rgb0[0]) * scale + rgb0[0]) + 0.5)) - g = o8(int(255 * ((rgb1[1] - rgb0[1]) * scale + rgb0[1]) + 0.5)) - b = o8(int(255 * ((rgb1[2] - rgb0[2]) * scale + rgb0[2]) + 0.5)) - a = o8(int(255 * ((rgb1[3] - rgb0[3]) * scale + rgb0[3]) + 0.5)) - - # add to palette - palette.append(r + g + b + a) - - return b"".join(palette), "RGBA" - - -class GimpGradientFile(GradientFile): - """File handler for GIMP's gradient format.""" - - def __init__(self, fp: IO[bytes]) -> None: - if fp.readline()[:13] != b"GIMP Gradient": - msg = "not a GIMP gradient file" - raise SyntaxError(msg) - - line = fp.readline() - - # GIMP 1.2 gradient files don't contain a name, but GIMP 1.3 files do - if line.startswith(b"Name: "): - line = fp.readline().strip() - - count = int(line) - - self.gradient = [] - - for i in range(count): - s = fp.readline().split() - w = [float(x) for x in s[:11]] - - x0, x1 = w[0], w[2] - xm = w[1] - rgb0 = w[3:7] - rgb1 = w[7:11] - - segment = SEGMENTS[int(s[11])] - cspace = int(s[12]) - - if cspace != 0: - msg = "cannot handle HSV colour space" - raise OSError(msg) - - self.gradient.append((x0, x1, xm, rgb0, rgb1, segment)) +# +# Python Imaging Library +# $Id$ +# +# stuff to read (and render) GIMP gradient files +# +# History: +# 97-08-23 fl Created +# +# Copyright (c) Secret Labs AB 1997. +# Copyright (c) Fredrik Lundh 1997. +# +# See the README file for information on usage and redistribution. +# + +""" +Stuff to translate curve segments to palette values (derived from +the corresponding code in GIMP, written by Federico Mena Quintero. +See the GIMP distribution for more information.) +""" +from __future__ import annotations + +from math import log, pi, sin, sqrt +from typing import IO, Callable + +from ._binary import o8 + +EPSILON = 1e-10 +"""""" # Enable auto-doc for data member + + +def linear(middle: float, pos: float) -> float: + if pos <= middle: + if middle < EPSILON: + return 0.0 + else: + return 0.5 * pos / middle + else: + pos = pos - middle + middle = 1.0 - middle + if middle < EPSILON: + return 1.0 + else: + return 0.5 + 0.5 * pos / middle + + +def curved(middle: float, pos: float) -> float: + return pos ** (log(0.5) / log(max(middle, EPSILON))) + + +def sine(middle: float, pos: float) -> float: + return (sin((-pi / 2.0) + pi * linear(middle, pos)) + 1.0) / 2.0 + + +def sphere_increasing(middle: float, pos: float) -> float: + return sqrt(1.0 - (linear(middle, pos) - 1.0) ** 2) + + +def sphere_decreasing(middle: float, pos: float) -> float: + return 1.0 - sqrt(1.0 - linear(middle, pos) ** 2) + + +SEGMENTS = [linear, curved, sine, sphere_increasing, sphere_decreasing] +"""""" # Enable auto-doc for data member + + +class GradientFile: + gradient: ( + list[ + tuple[ + float, + float, + float, + list[float], + list[float], + Callable[[float, float], float], + ] + ] + | None + ) = None + + def getpalette(self, entries: int = 256) -> tuple[bytes, str]: + assert self.gradient is not None + palette = [] + + ix = 0 + x0, x1, xm, rgb0, rgb1, segment = self.gradient[ix] + + for i in range(entries): + x = i / (entries - 1) + + while x1 < x: + ix += 1 + x0, x1, xm, rgb0, rgb1, segment = self.gradient[ix] + + w = x1 - x0 + + if w < EPSILON: + scale = segment(0.5, 0.5) + else: + scale = segment((xm - x0) / w, (x - x0) / w) + + # expand to RGBA + r = o8(int(255 * ((rgb1[0] - rgb0[0]) * scale + rgb0[0]) + 0.5)) + g = o8(int(255 * ((rgb1[1] - rgb0[1]) * scale + rgb0[1]) + 0.5)) + b = o8(int(255 * ((rgb1[2] - rgb0[2]) * scale + rgb0[2]) + 0.5)) + a = o8(int(255 * ((rgb1[3] - rgb0[3]) * scale + rgb0[3]) + 0.5)) + + # add to palette + palette.append(r + g + b + a) + + return b"".join(palette), "RGBA" + + +class GimpGradientFile(GradientFile): + """File handler for GIMP's gradient format.""" + + def __init__(self, fp: IO[bytes]) -> None: + if fp.readline()[:13] != b"GIMP Gradient": + msg = "not a GIMP gradient file" + raise SyntaxError(msg) + + line = fp.readline() + + # GIMP 1.2 gradient files don't contain a name, but GIMP 1.3 files do + if line.startswith(b"Name: "): + line = fp.readline().strip() + + count = int(line) + + self.gradient = [] + + for i in range(count): + s = fp.readline().split() + w = [float(x) for x in s[:11]] + + x0, x1 = w[0], w[2] + xm = w[1] + rgb0 = w[3:7] + rgb1 = w[7:11] + + segment = SEGMENTS[int(s[11])] + cspace = int(s[12]) + + if cspace != 0: + msg = "cannot handle HSV colour space" + raise OSError(msg) + + self.gradient.append((x0, x1, xm, rgb0, rgb1, segment)) diff --git a/src/PIL/GimpPaletteFile.py b/src/PIL/GimpPaletteFile.py index 4cad0ebee..3cc054460 100644 --- a/src/PIL/GimpPaletteFile.py +++ b/src/PIL/GimpPaletteFile.py @@ -1,58 +1,58 @@ -# -# Python Imaging Library -# $Id$ -# -# stuff to read GIMP palette files -# -# History: -# 1997-08-23 fl Created -# 2004-09-07 fl Support GIMP 2.0 palette files. -# -# Copyright (c) Secret Labs AB 1997-2004. All rights reserved. -# Copyright (c) Fredrik Lundh 1997-2004. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import re -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": - msg = "not a GIMP palette file" - raise SyntaxError(msg) - - for i in range(256): - s = fp.readline() - if not s: - break - - # skip fields and comment lines - if re.match(rb"\w+:|#", s): - continue - if len(s) > 100: - msg = "bad palette file" - raise SyntaxError(msg) - - v = tuple(map(int, s.split()[:3])) - if len(v) != 3: - msg = "bad palette entry" - raise ValueError(msg) - - palette[i] = o8(v[0]) + o8(v[1]) + o8(v[2]) - - self.palette = b"".join(palette) - - def getpalette(self) -> tuple[bytes, str]: - return self.palette, self.rawmode +# +# Python Imaging Library +# $Id$ +# +# stuff to read GIMP palette files +# +# History: +# 1997-08-23 fl Created +# 2004-09-07 fl Support GIMP 2.0 palette files. +# +# Copyright (c) Secret Labs AB 1997-2004. All rights reserved. +# Copyright (c) Fredrik Lundh 1997-2004. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import re +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": + msg = "not a GIMP palette file" + raise SyntaxError(msg) + + for i in range(256): + s = fp.readline() + if not s: + break + + # skip fields and comment lines + if re.match(rb"\w+:|#", s): + continue + if len(s) > 100: + msg = "bad palette file" + raise SyntaxError(msg) + + v = tuple(map(int, s.split()[:3])) + if len(v) != 3: + msg = "bad palette entry" + raise ValueError(msg) + + palette[i] = o8(v[0]) + o8(v[1]) + o8(v[2]) + + self.palette = b"".join(palette) + + 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..9cef6786a 100644 --- a/src/PIL/GribStubImagePlugin.py +++ b/src/PIL/GribStubImagePlugin.py @@ -1,76 +1,76 @@ -# -# The Python Imaging Library -# $Id$ -# -# GRIB stub adapter -# -# Copyright (c) 1996-2003 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -from typing import IO - -from . import Image, ImageFile - -_handler = None - - -def register_handler(handler: ImageFile.StubHandler | None) -> None: - """ - Install application-specific GRIB image handler. - - :param handler: Handler object. - """ - global _handler - _handler = handler - - -# -------------------------------------------------------------------- -# Image adapter - - -def _accept(prefix: bytes) -> bool: - return prefix[:4] == b"GRIB" and prefix[7] == 1 - - -class GribStubImageFile(ImageFile.StubImageFile): - format = "GRIB" - 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) - - # make something up - self._mode = "F" - self._size = 1, 1 - - loader = self._load() - if loader: - loader.open(self) - - def _load(self) -> ImageFile.StubHandler | None: - return _handler - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - if _handler is None or not hasattr(_handler, "save"): - msg = "GRIB save handler not installed" - raise OSError(msg) - _handler.save(im, fp, filename) - - -# -------------------------------------------------------------------- -# Registry - -Image.register_open(GribStubImageFile.format, GribStubImageFile, _accept) -Image.register_save(GribStubImageFile.format, _save) - -Image.register_extension(GribStubImageFile.format, ".grib") +# +# The Python Imaging Library +# $Id$ +# +# GRIB stub adapter +# +# Copyright (c) 1996-2003 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +from typing import IO + +from . import Image, ImageFile + +_handler = None + + +def register_handler(handler: ImageFile.StubHandler | None) -> None: + """ + Install application-specific GRIB image handler. + + :param handler: Handler object. + """ + global _handler + _handler = handler + + +# -------------------------------------------------------------------- +# Image adapter + + +def _accept(prefix: bytes) -> bool: + return prefix[:4] == b"GRIB" and prefix[7] == 1 + + +class GribStubImageFile(ImageFile.StubImageFile): + format = "GRIB" + 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) + + # make something up + self._mode = "F" + self._size = 1, 1 + + loader = self._load() + if loader: + loader.open(self) + + def _load(self) -> ImageFile.StubHandler | None: + return _handler + + +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + if _handler is None or not hasattr(_handler, "save"): + msg = "GRIB save handler not installed" + raise OSError(msg) + _handler.save(im, fp, filename) + + +# -------------------------------------------------------------------- +# Registry + +Image.register_open(GribStubImageFile.format, GribStubImageFile, _accept) +Image.register_save(GribStubImageFile.format, _save) + +Image.register_extension(GribStubImageFile.format, ".grib") diff --git a/src/PIL/Hdf5StubImagePlugin.py b/src/PIL/Hdf5StubImagePlugin.py index cc9e73deb..b1a89e931 100644 --- a/src/PIL/Hdf5StubImagePlugin.py +++ b/src/PIL/Hdf5StubImagePlugin.py @@ -1,76 +1,76 @@ -# -# The Python Imaging Library -# $Id$ -# -# HDF5 stub adapter -# -# Copyright (c) 2000-2003 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -from typing import IO - -from . import Image, ImageFile - -_handler = None - - -def register_handler(handler: ImageFile.StubHandler | None) -> None: - """ - Install application-specific HDF5 image handler. - - :param handler: Handler object. - """ - global _handler - _handler = handler - - -# -------------------------------------------------------------------- -# Image adapter - - -def _accept(prefix: bytes) -> bool: - return prefix[:8] == b"\x89HDF\r\n\x1a\n" - - -class HDF5StubImageFile(ImageFile.StubImageFile): - format = "HDF5" - 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) - - # make something up - self._mode = "F" - self._size = 1, 1 - - loader = self._load() - if loader: - loader.open(self) - - def _load(self) -> ImageFile.StubHandler | None: - return _handler - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - if _handler is None or not hasattr(_handler, "save"): - msg = "HDF5 save handler not installed" - raise OSError(msg) - _handler.save(im, fp, filename) - - -# -------------------------------------------------------------------- -# Registry - -Image.register_open(HDF5StubImageFile.format, HDF5StubImageFile, _accept) -Image.register_save(HDF5StubImageFile.format, _save) - -Image.register_extensions(HDF5StubImageFile.format, [".h5", ".hdf"]) +# +# The Python Imaging Library +# $Id$ +# +# HDF5 stub adapter +# +# Copyright (c) 2000-2003 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +from typing import IO + +from . import Image, ImageFile + +_handler = None + + +def register_handler(handler: ImageFile.StubHandler | None) -> None: + """ + Install application-specific HDF5 image handler. + + :param handler: Handler object. + """ + global _handler + _handler = handler + + +# -------------------------------------------------------------------- +# Image adapter + + +def _accept(prefix: bytes) -> bool: + return prefix[:8] == b"\x89HDF\r\n\x1a\n" + + +class HDF5StubImageFile(ImageFile.StubImageFile): + format = "HDF5" + 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) + + # make something up + self._mode = "F" + self._size = 1, 1 + + loader = self._load() + if loader: + loader.open(self) + + def _load(self) -> ImageFile.StubHandler | None: + return _handler + + +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + if _handler is None or not hasattr(_handler, "save"): + msg = "HDF5 save handler not installed" + raise OSError(msg) + _handler.save(im, fp, filename) + + +# -------------------------------------------------------------------- +# Registry + +Image.register_open(HDF5StubImageFile.format, HDF5StubImageFile, _accept) +Image.register_save(HDF5StubImageFile.format, _save) + +Image.register_extensions(HDF5StubImageFile.format, [".h5", ".hdf"]) diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index 2a89d498c..2e7db7040 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -1,399 +1,399 @@ -# -# The Python Imaging Library. -# $Id$ -# -# macOS icns file decoder, based on icns.py by Bob Ippolito. -# -# history: -# 2004-10-09 fl Turned into a PIL plugin; removed 2.3 dependencies. -# 2020-04-04 Allow saving on all operating systems. -# -# Copyright (c) 2004 by Bob Ippolito. -# Copyright (c) 2004 by Secret Labs. -# Copyright (c) 2004 by Fredrik Lundh. -# Copyright (c) 2014 by Alastair Houghton. -# Copyright (c) 2020 by Pan Jing. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import io -import os -import struct -import sys -from typing import IO - -from . import Image, ImageFile, PngImagePlugin, features - -enable_jpeg2k = features.check_codec("jpg_2000") -if enable_jpeg2k: - from . import Jpeg2KImagePlugin - -MAGIC = b"icns" -HEADERSIZE = 8 - - -def nextheader(fobj): - return struct.unpack(">4sI", fobj.read(HEADERSIZE)) - - -def read_32t(fobj, start_length, size): - # The 128x128 icon seems to have an extra header for some reason. - (start, length) = start_length - fobj.seek(start) - sig = fobj.read(4) - if sig != b"\x00\x00\x00\x00": - msg = "Unknown signature, expecting 0x00000000" - raise SyntaxError(msg) - return read_32(fobj, (start + 4, length - 4), size) - - -def read_32(fobj, start_length, size): - """ - Read a 32bit RGB icon resource. Seems to be either uncompressed or - an RLE packbits-like scheme. - """ - (start, length) = start_length - fobj.seek(start) - pixel_size = (size[0] * size[2], size[1] * size[2]) - sizesq = pixel_size[0] * pixel_size[1] - if length == sizesq * 3: - # uncompressed ("RGBRGBGB") - indata = fobj.read(length) - im = Image.frombuffer("RGB", pixel_size, indata, "raw", "RGB", 0, 1) - else: - # decode image - im = Image.new("RGB", pixel_size, None) - for band_ix in range(3): - data = [] - bytesleft = sizesq - while bytesleft > 0: - byte = fobj.read(1) - if not byte: - break - byte = byte[0] - if byte & 0x80: - blocksize = byte - 125 - byte = fobj.read(1) - for i in range(blocksize): - data.append(byte) - else: - blocksize = byte + 1 - data.append(fobj.read(blocksize)) - bytesleft -= blocksize - if bytesleft <= 0: - break - if bytesleft != 0: - msg = f"Error reading channel [{repr(bytesleft)} left]" - raise SyntaxError(msg) - band = Image.frombuffer("L", pixel_size, b"".join(data), "raw", "L", 0, 1) - im.im.putband(band.im, band_ix) - return {"RGB": im} - - -def read_mk(fobj, start_length, size): - # Alpha masks seem to be uncompressed - start = start_length[0] - fobj.seek(start) - pixel_size = (size[0] * size[2], size[1] * size[2]) - sizesq = pixel_size[0] * pixel_size[1] - band = Image.frombuffer("L", pixel_size, fobj.read(sizesq), "raw", "L", 0, 1) - return {"A": band} - - -def read_png_or_jpeg2000(fobj, start_length, size): - (start, length) = start_length - fobj.seek(start) - sig = fobj.read(12) - if sig[:8] == b"\x89PNG\x0d\x0a\x1a\x0a": - fobj.seek(start) - im = PngImagePlugin.PngImageFile(fobj) - Image._decompression_bomb_check(im.size) - return {"RGBA": im} - elif ( - sig[:4] == b"\xff\x4f\xff\x51" - or sig[:4] == b"\x0d\x0a\x87\x0a" - or sig == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a" - ): - if not enable_jpeg2k: - msg = ( - "Unsupported icon subimage format (rebuild PIL " - "with JPEG 2000 support to fix this)" - ) - raise ValueError(msg) - # j2k, jpc or j2c - fobj.seek(start) - jp2kstream = fobj.read(length) - f = io.BytesIO(jp2kstream) - im = Jpeg2KImagePlugin.Jpeg2KImageFile(f) - Image._decompression_bomb_check(im.size) - if im.mode != "RGBA": - im = im.convert("RGBA") - return {"RGBA": im} - else: - msg = "Unsupported icon subimage format" - raise ValueError(msg) - - -class IcnsFile: - SIZES = { - (512, 512, 2): [(b"ic10", read_png_or_jpeg2000)], - (512, 512, 1): [(b"ic09", read_png_or_jpeg2000)], - (256, 256, 2): [(b"ic14", read_png_or_jpeg2000)], - (256, 256, 1): [(b"ic08", read_png_or_jpeg2000)], - (128, 128, 2): [(b"ic13", read_png_or_jpeg2000)], - (128, 128, 1): [ - (b"ic07", read_png_or_jpeg2000), - (b"it32", read_32t), - (b"t8mk", read_mk), - ], - (64, 64, 1): [(b"icp6", read_png_or_jpeg2000)], - (32, 32, 2): [(b"ic12", read_png_or_jpeg2000)], - (48, 48, 1): [(b"ih32", read_32), (b"h8mk", read_mk)], - (32, 32, 1): [ - (b"icp5", read_png_or_jpeg2000), - (b"il32", read_32), - (b"l8mk", read_mk), - ], - (16, 16, 2): [(b"ic11", read_png_or_jpeg2000)], - (16, 16, 1): [ - (b"icp4", read_png_or_jpeg2000), - (b"is32", read_32), - (b"s8mk", read_mk), - ], - } - - def __init__(self, fobj): - """ - fobj is a file-like object as an icns resource - """ - # signature : (start, length) - self.dct = dct = {} - self.fobj = fobj - sig, filesize = nextheader(fobj) - if not _accept(sig): - msg = "not an icns file" - raise SyntaxError(msg) - i = HEADERSIZE - while i < filesize: - sig, blocksize = nextheader(fobj) - if blocksize <= 0: - msg = "invalid block header" - raise SyntaxError(msg) - i += HEADERSIZE - blocksize -= HEADERSIZE - dct[sig] = (i, blocksize) - fobj.seek(blocksize, io.SEEK_CUR) - i += blocksize - - def itersizes(self): - sizes = [] - for size, fmts in self.SIZES.items(): - for fmt, reader in fmts: - if fmt in self.dct: - sizes.append(size) - break - return sizes - - def bestsize(self): - sizes = self.itersizes() - if not sizes: - msg = "No 32bit icon resources found" - raise SyntaxError(msg) - return max(sizes) - - def dataforsize(self, size): - """ - Get an icon resource as {channel: array}. Note that - the arrays are bottom-up like windows bitmaps and will likely - need to be flipped or transposed in some way. - """ - dct = {} - for code, reader in self.SIZES[size]: - desc = self.dct.get(code) - if desc is not None: - dct.update(reader(self.fobj, desc, size)) - return dct - - def getimage(self, size=None): - if size is None: - size = self.bestsize() - if len(size) == 2: - size = (size[0], size[1], 1) - channels = self.dataforsize(size) - - im = channels.get("RGBA", None) - if im: - return im - - im = channels.get("RGB").copy() - try: - im.putalpha(channels["A"]) - except KeyError: - pass - return im - - -## -# Image plugin for Mac OS icons. - - -class IcnsImageFile(ImageFile.ImageFile): - """ - PIL image support for Mac OS .icns files. - Chooses the best resolution, but will possibly load - a different size image if you mutate the size attribute - before calling 'load'. - - The info dictionary has a key 'sizes' that is a list - of sizes that the icns file has. - """ - - format = "ICNS" - format_description = "Mac OS icns resource" - - def _open(self) -> None: - self.icns = IcnsFile(self.fp) - self._mode = "RGBA" - self.info["sizes"] = self.icns.itersizes() - self.best_size = self.icns.bestsize() - self.size = ( - self.best_size[0] * self.best_size[2], - self.best_size[1] * self.best_size[2], - ) - - @property - def size(self): - return self._size - - @size.setter - def size(self, value): - info_size = value - if info_size not in self.info["sizes"] and len(info_size) == 2: - info_size = (info_size[0], info_size[1], 1) - if ( - info_size not in self.info["sizes"] - and len(info_size) == 3 - and info_size[2] == 1 - ): - simple_sizes = [ - (size[0] * size[2], size[1] * size[2]) for size in self.info["sizes"] - ] - if value in simple_sizes: - info_size = self.info["sizes"][simple_sizes.index(value)] - if info_size not in self.info["sizes"]: - msg = "This is not one of the allowed sizes of this image" - raise ValueError(msg) - self._size = value - - def load(self): - if len(self.size) == 3: - self.best_size = self.size - self.size = ( - self.best_size[0] * self.best_size[2], - self.best_size[1] * self.best_size[2], - ) - - px = Image.Image.load(self) - if self.im is not None and self.im.size == self.size: - # Already loaded - return px - self.load_prepare() - # This is likely NOT the best way to do it, but whatever. - im = self.icns.getimage(self.best_size) - - # If this is a PNG or JPEG 2000, it won't be loaded yet - px = im.load() - - self.im = im.im - self._mode = im.mode - self.size = im.size - - return px - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - """ - Saves the image as a series of PNG files, - that are then combined into a .icns file. - """ - if hasattr(fp, "flush"): - fp.flush() - - sizes = { - b"ic07": 128, - b"ic08": 256, - b"ic09": 512, - b"ic10": 1024, - b"ic11": 32, - b"ic12": 64, - b"ic13": 256, - b"ic14": 512, - } - provided_images = {im.width: im for im in im.encoderinfo.get("append_images", [])} - size_streams = {} - for size in set(sizes.values()): - image = ( - provided_images[size] - if size in provided_images - else im.resize((size, size)) - ) - - temp = io.BytesIO() - image.save(temp, "png") - size_streams[size] = temp.getvalue() - - entries = [] - for type, size in sizes.items(): - stream = size_streams[size] - entries.append((type, HEADERSIZE + len(stream), stream)) - - # Header - fp.write(MAGIC) - file_length = HEADERSIZE # Header - file_length += HEADERSIZE + 8 * len(entries) # TOC - file_length += sum(entry[1] for entry in entries) - fp.write(struct.pack(">i", file_length)) - - # TOC - fp.write(b"TOC ") - fp.write(struct.pack(">i", HEADERSIZE + len(entries) * HEADERSIZE)) - for entry in entries: - fp.write(entry[0]) - fp.write(struct.pack(">i", entry[1])) - - # Data - for entry in entries: - fp.write(entry[0]) - fp.write(struct.pack(">i", entry[1])) - fp.write(entry[2]) - - if hasattr(fp, "flush"): - fp.flush() - - -def _accept(prefix: bytes) -> bool: - return prefix[:4] == MAGIC - - -Image.register_open(IcnsImageFile.format, IcnsImageFile, _accept) -Image.register_extension(IcnsImageFile.format, ".icns") - -Image.register_save(IcnsImageFile.format, _save) -Image.register_mime(IcnsImageFile.format, "image/icns") - -if __name__ == "__main__": - if len(sys.argv) < 2: - print("Syntax: python3 IcnsImagePlugin.py [file]") - sys.exit() - - with open(sys.argv[1], "rb") as fp: - imf = IcnsImageFile(fp) - for size in imf.info["sizes"]: - width, height, scale = imf.size = size - imf.save(f"out-{width}-{height}-{scale}.png") - with Image.open(sys.argv[1]) as im: - im.save("out.png") - if sys.platform == "windows": - os.startfile("out.png") +# +# The Python Imaging Library. +# $Id$ +# +# macOS icns file decoder, based on icns.py by Bob Ippolito. +# +# history: +# 2004-10-09 fl Turned into a PIL plugin; removed 2.3 dependencies. +# 2020-04-04 Allow saving on all operating systems. +# +# Copyright (c) 2004 by Bob Ippolito. +# Copyright (c) 2004 by Secret Labs. +# Copyright (c) 2004 by Fredrik Lundh. +# Copyright (c) 2014 by Alastair Houghton. +# Copyright (c) 2020 by Pan Jing. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import io +import os +import struct +import sys +from typing import IO + +from . import Image, ImageFile, PngImagePlugin, features + +enable_jpeg2k = features.check_codec("jpg_2000") +if enable_jpeg2k: + from . import Jpeg2KImagePlugin + +MAGIC = b"icns" +HEADERSIZE = 8 + + +def nextheader(fobj): + return struct.unpack(">4sI", fobj.read(HEADERSIZE)) + + +def read_32t(fobj, start_length, size): + # The 128x128 icon seems to have an extra header for some reason. + (start, length) = start_length + fobj.seek(start) + sig = fobj.read(4) + if sig != b"\x00\x00\x00\x00": + msg = "Unknown signature, expecting 0x00000000" + raise SyntaxError(msg) + return read_32(fobj, (start + 4, length - 4), size) + + +def read_32(fobj, start_length, size): + """ + Read a 32bit RGB icon resource. Seems to be either uncompressed or + an RLE packbits-like scheme. + """ + (start, length) = start_length + fobj.seek(start) + pixel_size = (size[0] * size[2], size[1] * size[2]) + sizesq = pixel_size[0] * pixel_size[1] + if length == sizesq * 3: + # uncompressed ("RGBRGBGB") + indata = fobj.read(length) + im = Image.frombuffer("RGB", pixel_size, indata, "raw", "RGB", 0, 1) + else: + # decode image + im = Image.new("RGB", pixel_size, None) + for band_ix in range(3): + data = [] + bytesleft = sizesq + while bytesleft > 0: + byte = fobj.read(1) + if not byte: + break + byte = byte[0] + if byte & 0x80: + blocksize = byte - 125 + byte = fobj.read(1) + for i in range(blocksize): + data.append(byte) + else: + blocksize = byte + 1 + data.append(fobj.read(blocksize)) + bytesleft -= blocksize + if bytesleft <= 0: + break + if bytesleft != 0: + msg = f"Error reading channel [{repr(bytesleft)} left]" + raise SyntaxError(msg) + band = Image.frombuffer("L", pixel_size, b"".join(data), "raw", "L", 0, 1) + im.im.putband(band.im, band_ix) + return {"RGB": im} + + +def read_mk(fobj, start_length, size): + # Alpha masks seem to be uncompressed + start = start_length[0] + fobj.seek(start) + pixel_size = (size[0] * size[2], size[1] * size[2]) + sizesq = pixel_size[0] * pixel_size[1] + band = Image.frombuffer("L", pixel_size, fobj.read(sizesq), "raw", "L", 0, 1) + return {"A": band} + + +def read_png_or_jpeg2000(fobj, start_length, size): + (start, length) = start_length + fobj.seek(start) + sig = fobj.read(12) + if sig[:8] == b"\x89PNG\x0d\x0a\x1a\x0a": + fobj.seek(start) + im = PngImagePlugin.PngImageFile(fobj) + Image._decompression_bomb_check(im.size) + return {"RGBA": im} + elif ( + sig[:4] == b"\xff\x4f\xff\x51" + or sig[:4] == b"\x0d\x0a\x87\x0a" + or sig == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a" + ): + if not enable_jpeg2k: + msg = ( + "Unsupported icon subimage format (rebuild PIL " + "with JPEG 2000 support to fix this)" + ) + raise ValueError(msg) + # j2k, jpc or j2c + fobj.seek(start) + jp2kstream = fobj.read(length) + f = io.BytesIO(jp2kstream) + im = Jpeg2KImagePlugin.Jpeg2KImageFile(f) + Image._decompression_bomb_check(im.size) + if im.mode != "RGBA": + im = im.convert("RGBA") + return {"RGBA": im} + else: + msg = "Unsupported icon subimage format" + raise ValueError(msg) + + +class IcnsFile: + SIZES = { + (512, 512, 2): [(b"ic10", read_png_or_jpeg2000)], + (512, 512, 1): [(b"ic09", read_png_or_jpeg2000)], + (256, 256, 2): [(b"ic14", read_png_or_jpeg2000)], + (256, 256, 1): [(b"ic08", read_png_or_jpeg2000)], + (128, 128, 2): [(b"ic13", read_png_or_jpeg2000)], + (128, 128, 1): [ + (b"ic07", read_png_or_jpeg2000), + (b"it32", read_32t), + (b"t8mk", read_mk), + ], + (64, 64, 1): [(b"icp6", read_png_or_jpeg2000)], + (32, 32, 2): [(b"ic12", read_png_or_jpeg2000)], + (48, 48, 1): [(b"ih32", read_32), (b"h8mk", read_mk)], + (32, 32, 1): [ + (b"icp5", read_png_or_jpeg2000), + (b"il32", read_32), + (b"l8mk", read_mk), + ], + (16, 16, 2): [(b"ic11", read_png_or_jpeg2000)], + (16, 16, 1): [ + (b"icp4", read_png_or_jpeg2000), + (b"is32", read_32), + (b"s8mk", read_mk), + ], + } + + def __init__(self, fobj): + """ + fobj is a file-like object as an icns resource + """ + # signature : (start, length) + self.dct = dct = {} + self.fobj = fobj + sig, filesize = nextheader(fobj) + if not _accept(sig): + msg = "not an icns file" + raise SyntaxError(msg) + i = HEADERSIZE + while i < filesize: + sig, blocksize = nextheader(fobj) + if blocksize <= 0: + msg = "invalid block header" + raise SyntaxError(msg) + i += HEADERSIZE + blocksize -= HEADERSIZE + dct[sig] = (i, blocksize) + fobj.seek(blocksize, io.SEEK_CUR) + i += blocksize + + def itersizes(self): + sizes = [] + for size, fmts in self.SIZES.items(): + for fmt, reader in fmts: + if fmt in self.dct: + sizes.append(size) + break + return sizes + + def bestsize(self): + sizes = self.itersizes() + if not sizes: + msg = "No 32bit icon resources found" + raise SyntaxError(msg) + return max(sizes) + + def dataforsize(self, size): + """ + Get an icon resource as {channel: array}. Note that + the arrays are bottom-up like windows bitmaps and will likely + need to be flipped or transposed in some way. + """ + dct = {} + for code, reader in self.SIZES[size]: + desc = self.dct.get(code) + if desc is not None: + dct.update(reader(self.fobj, desc, size)) + return dct + + def getimage(self, size=None): + if size is None: + size = self.bestsize() + if len(size) == 2: + size = (size[0], size[1], 1) + channels = self.dataforsize(size) + + im = channels.get("RGBA", None) + if im: + return im + + im = channels.get("RGB").copy() + try: + im.putalpha(channels["A"]) + except KeyError: + pass + return im + + +## +# Image plugin for Mac OS icons. + + +class IcnsImageFile(ImageFile.ImageFile): + """ + PIL image support for Mac OS .icns files. + Chooses the best resolution, but will possibly load + a different size image if you mutate the size attribute + before calling 'load'. + + The info dictionary has a key 'sizes' that is a list + of sizes that the icns file has. + """ + + format = "ICNS" + format_description = "Mac OS icns resource" + + def _open(self) -> None: + self.icns = IcnsFile(self.fp) + self._mode = "RGBA" + self.info["sizes"] = self.icns.itersizes() + self.best_size = self.icns.bestsize() + self.size = ( + self.best_size[0] * self.best_size[2], + self.best_size[1] * self.best_size[2], + ) + + @property + def size(self): + return self._size + + @size.setter + def size(self, value): + info_size = value + if info_size not in self.info["sizes"] and len(info_size) == 2: + info_size = (info_size[0], info_size[1], 1) + if ( + info_size not in self.info["sizes"] + and len(info_size) == 3 + and info_size[2] == 1 + ): + simple_sizes = [ + (size[0] * size[2], size[1] * size[2]) for size in self.info["sizes"] + ] + if value in simple_sizes: + info_size = self.info["sizes"][simple_sizes.index(value)] + if info_size not in self.info["sizes"]: + msg = "This is not one of the allowed sizes of this image" + raise ValueError(msg) + self._size = value + + def load(self): + if len(self.size) == 3: + self.best_size = self.size + self.size = ( + self.best_size[0] * self.best_size[2], + self.best_size[1] * self.best_size[2], + ) + + px = Image.Image.load(self) + if self.im is not None and self.im.size == self.size: + # Already loaded + return px + self.load_prepare() + # This is likely NOT the best way to do it, but whatever. + im = self.icns.getimage(self.best_size) + + # If this is a PNG or JPEG 2000, it won't be loaded yet + px = im.load() + + self.im = im.im + self._mode = im.mode + self.size = im.size + + return px + + +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + """ + Saves the image as a series of PNG files, + that are then combined into a .icns file. + """ + if hasattr(fp, "flush"): + fp.flush() + + sizes = { + b"ic07": 128, + b"ic08": 256, + b"ic09": 512, + b"ic10": 1024, + b"ic11": 32, + b"ic12": 64, + b"ic13": 256, + b"ic14": 512, + } + provided_images = {im.width: im for im in im.encoderinfo.get("append_images", [])} + size_streams = {} + for size in set(sizes.values()): + image = ( + provided_images[size] + if size in provided_images + else im.resize((size, size)) + ) + + temp = io.BytesIO() + image.save(temp, "png") + size_streams[size] = temp.getvalue() + + entries = [] + for type, size in sizes.items(): + stream = size_streams[size] + entries.append((type, HEADERSIZE + len(stream), stream)) + + # Header + fp.write(MAGIC) + file_length = HEADERSIZE # Header + file_length += HEADERSIZE + 8 * len(entries) # TOC + file_length += sum(entry[1] for entry in entries) + fp.write(struct.pack(">i", file_length)) + + # TOC + fp.write(b"TOC ") + fp.write(struct.pack(">i", HEADERSIZE + len(entries) * HEADERSIZE)) + for entry in entries: + fp.write(entry[0]) + fp.write(struct.pack(">i", entry[1])) + + # Data + for entry in entries: + fp.write(entry[0]) + fp.write(struct.pack(">i", entry[1])) + fp.write(entry[2]) + + if hasattr(fp, "flush"): + fp.flush() + + +def _accept(prefix: bytes) -> bool: + return prefix[:4] == MAGIC + + +Image.register_open(IcnsImageFile.format, IcnsImageFile, _accept) +Image.register_extension(IcnsImageFile.format, ".icns") + +Image.register_save(IcnsImageFile.format, _save) +Image.register_mime(IcnsImageFile.format, "image/icns") + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Syntax: python3 IcnsImagePlugin.py [file]") + sys.exit() + + with open(sys.argv[1], "rb") as fp: + imf = IcnsImageFile(fp) + for size in imf.info["sizes"]: + width, height, scale = imf.size = size + imf.save(f"out-{width}-{height}-{scale}.png") + with Image.open(sys.argv[1]) as im: + im.save("out.png") + if sys.platform == "windows": + os.startfile("out.png") diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index 227fcf35c..a284907ba 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -1,360 +1,360 @@ -# -# The Python Imaging Library. -# $Id$ -# -# Windows Icon support for PIL -# -# History: -# 96-05-27 fl Created -# -# Copyright (c) Secret Labs AB 1997. -# Copyright (c) Fredrik Lundh 1996. -# -# See the README file for information on usage and redistribution. -# - -# This plugin is a refactored version of Win32IconImagePlugin by Bryan Davis -# . -# https://code.google.com/archive/p/casadebender/wikis/Win32IconImagePlugin.wiki -# -# Icon format references: -# * https://en.wikipedia.org/wiki/ICO_(file_format) -# * https://msdn.microsoft.com/en-us/library/ms997538.aspx -from __future__ import annotations - -import warnings -from io import BytesIO -from math import ceil, log -from typing import IO - -from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin -from ._binary import i16le as i16 -from ._binary import i32le as i32 -from ._binary import o8 -from ._binary import o16le as o16 -from ._binary import o32le as o32 - -# -# -------------------------------------------------------------------- - -_MAGIC = b"\0\0\1\0" - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - fp.write(_MAGIC) # (2+2) - bmp = im.encoderinfo.get("bitmap_format") == "bmp" - sizes = im.encoderinfo.get( - "sizes", - [(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)], - ) - frames = [] - provided_ims = [im] + im.encoderinfo.get("append_images", []) - width, height = im.size - for size in sorted(set(sizes)): - if size[0] > width or size[1] > height or size[0] > 256 or size[1] > 256: - continue - - for provided_im in provided_ims: - if provided_im.size != size: - continue - frames.append(provided_im) - if bmp: - bits = BmpImagePlugin.SAVE[provided_im.mode][1] - bits_used = [bits] - for other_im in provided_ims: - if other_im.size != size: - continue - bits = BmpImagePlugin.SAVE[other_im.mode][1] - if bits not in bits_used: - # Another image has been supplied for this size - # with a different bit depth - frames.append(other_im) - bits_used.append(bits) - break - else: - # TODO: invent a more convenient method for proportional scalings - frame = provided_im.copy() - frame.thumbnail(size, Image.Resampling.LANCZOS, reducing_gap=None) - frames.append(frame) - fp.write(o16(len(frames))) # idCount(2) - offset = fp.tell() + len(frames) * 16 - for frame in frames: - width, height = frame.size - # 0 means 256 - fp.write(o8(width if width < 256 else 0)) # bWidth(1) - fp.write(o8(height if height < 256 else 0)) # bHeight(1) - - bits, colors = BmpImagePlugin.SAVE[frame.mode][1:] if bmp else (32, 0) - fp.write(o8(colors)) # bColorCount(1) - fp.write(b"\0") # bReserved(1) - fp.write(b"\0\0") # wPlanes(2) - fp.write(o16(bits)) # wBitCount(2) - - image_io = BytesIO() - if bmp: - frame.save(image_io, "dib") - - if bits != 32: - and_mask = Image.new("1", size) - ImageFile._save( - and_mask, image_io, [("raw", (0, 0) + size, 0, ("1", 0, -1))] - ) - else: - frame.save(image_io, "png") - image_io.seek(0) - image_bytes = image_io.read() - if bmp: - image_bytes = image_bytes[:8] + o32(height * 2) + image_bytes[12:] - bytes_len = len(image_bytes) - fp.write(o32(bytes_len)) # dwBytesInRes(4) - fp.write(o32(offset)) # dwImageOffset(4) - current = fp.tell() - fp.seek(offset) - fp.write(image_bytes) - offset = offset + bytes_len - fp.seek(current) - - -def _accept(prefix: bytes) -> bool: - return prefix[:4] == _MAGIC - - -class IcoFile: - def __init__(self, buf): - """ - Parse image from file-like object containing ico file data - """ - - # check magic - s = buf.read(6) - if not _accept(s): - msg = "not an ICO file" - raise SyntaxError(msg) - - self.buf = buf - self.entry = [] - - # Number of items in file - self.nb_items = i16(s, 4) - - # Get headers for each item - for i in range(self.nb_items): - s = buf.read(16) - - icon_header = { - "width": s[0], - "height": s[1], - "nb_color": s[2], # No. of colors in image (0 if >=8bpp) - "reserved": s[3], - "planes": i16(s, 4), - "bpp": i16(s, 6), - "size": i32(s, 8), - "offset": i32(s, 12), - } - - # See Wikipedia - for j in ("width", "height"): - if not icon_header[j]: - icon_header[j] = 256 - - # See Wikipedia notes about color depth. - # We need this just to differ images with equal sizes - icon_header["color_depth"] = ( - icon_header["bpp"] - or ( - icon_header["nb_color"] != 0 - and ceil(log(icon_header["nb_color"], 2)) - ) - or 256 - ) - - icon_header["dim"] = (icon_header["width"], icon_header["height"]) - icon_header["square"] = icon_header["width"] * icon_header["height"] - - self.entry.append(icon_header) - - self.entry = sorted(self.entry, key=lambda x: x["color_depth"]) - # ICO images are usually squares - self.entry = sorted(self.entry, key=lambda x: x["square"], reverse=True) - - def sizes(self): - """ - Get a list of all available icon sizes and color depths. - """ - return {(h["width"], h["height"]) for h in self.entry} - - def getentryindex(self, size, bpp=False): - for i, h in enumerate(self.entry): - if size == h["dim"] and (bpp is False or bpp == h["color_depth"]): - return i - return 0 - - def getimage(self, size, bpp=False): - """ - Get an image from the icon - """ - return self.frame(self.getentryindex(size, bpp)) - - def frame(self, idx: int) -> Image.Image: - """ - Get an image from frame idx - """ - - header = self.entry[idx] - - self.buf.seek(header["offset"]) - data = self.buf.read(8) - self.buf.seek(header["offset"]) - - im: Image.Image - if data[:8] == PngImagePlugin._MAGIC: - # png frame - im = PngImagePlugin.PngImageFile(self.buf) - Image._decompression_bomb_check(im.size) - else: - # XOR + AND mask bmp frame - im = BmpImagePlugin.DibImageFile(self.buf) - Image._decompression_bomb_check(im.size) - - # change tile dimension to only encompass XOR image - im._size = (im.size[0], int(im.size[1] / 2)) - d, e, o, a = im.tile[0] - im.tile[0] = d, (0, 0) + im.size, o, a - - # figure out where AND mask image starts - bpp = header["bpp"] - if 32 == bpp: - # 32-bit color depth icon image allows semitransparent areas - # PIL's DIB format ignores transparency bits, recover them. - # The DIB is packed in BGRX byte order where X is the alpha - # channel. - - # Back up to start of bmp data - self.buf.seek(o) - # extract every 4th byte (eg. 3,7,11,15,...) - alpha_bytes = self.buf.read(im.size[0] * im.size[1] * 4)[3::4] - - # convert to an 8bpp grayscale image - mask = Image.frombuffer( - "L", # 8bpp - im.size, # (w, h) - alpha_bytes, # source chars - "raw", # raw decoder - ("L", 0, -1), # 8bpp inverted, unpadded, reversed - ) - else: - # get AND image from end of bitmap - w = im.size[0] - if (w % 32) > 0: - # bitmap row data is aligned to word boundaries - w += 32 - (im.size[0] % 32) - - # the total mask data is - # padded row size * height / bits per char - - total_bytes = int((w * im.size[1]) / 8) - and_mask_offset = header["offset"] + header["size"] - total_bytes - - self.buf.seek(and_mask_offset) - mask_data = self.buf.read(total_bytes) - - # convert raw data to image - mask = Image.frombuffer( - "1", # 1 bpp - im.size, # (w, h) - mask_data, # source chars - "raw", # raw decoder - ("1;I", int(w / 8), -1), # 1bpp inverted, padded, reversed - ) - - # now we have two images, im is XOR image and mask is AND image - - # apply mask image as alpha channel - im = im.convert("RGBA") - im.putalpha(mask) - - return im - - -## -# Image plugin for Windows Icon files. - - -class IcoImageFile(ImageFile.ImageFile): - """ - PIL read-only image support for Microsoft Windows .ico files. - - By default the largest resolution image in the file will be loaded. This - can be changed by altering the 'size' attribute before calling 'load'. - - The info dictionary has a key 'sizes' that is a list of the sizes available - in the icon file. - - Handles classic, XP and Vista icon formats. - - When saving, PNG compression is used. Support for this was only added in - Windows Vista. If you are unable to view the icon in Windows, convert the - image to "RGBA" mode before saving. - - This plugin is a refactored version of Win32IconImagePlugin by Bryan Davis - . - https://code.google.com/archive/p/casadebender/wikis/Win32IconImagePlugin.wiki - """ - - format = "ICO" - format_description = "Windows Icon" - - def _open(self) -> None: - self.ico = IcoFile(self.fp) - self.info["sizes"] = self.ico.sizes() - self.size = self.ico.entry[0]["dim"] - self.load() - - @property - def size(self): - return self._size - - @size.setter - def size(self, value): - if value not in self.info["sizes"]: - msg = "This is not one of the allowed sizes of this image" - raise ValueError(msg) - self._size = value - - def load(self): - if self.im is not None and self.im.size == self.size: - # Already loaded - return Image.Image.load(self) - im = self.ico.getimage(self.size) - # if tile is PNG, it won't really be loaded yet - im.load() - self.im = im.im - self.pyaccess = None - self._mode = im.mode - if im.palette: - self.palette = im.palette - if im.size != self.size: - warnings.warn("Image was not the expected size") - - index = self.ico.getentryindex(self.size) - sizes = list(self.info["sizes"]) - sizes[index] = im.size - self.info["sizes"] = set(sizes) - - self.size = im.size - - def load_seek(self, pos: int) -> None: - # Flag the ImageFile.Parser so that it - # just does all the decode at the end. - pass - - -# -# -------------------------------------------------------------------- - - -Image.register_open(IcoImageFile.format, IcoImageFile, _accept) -Image.register_save(IcoImageFile.format, _save) -Image.register_extension(IcoImageFile.format, ".ico") - -Image.register_mime(IcoImageFile.format, "image/x-icon") +# +# The Python Imaging Library. +# $Id$ +# +# Windows Icon support for PIL +# +# History: +# 96-05-27 fl Created +# +# Copyright (c) Secret Labs AB 1997. +# Copyright (c) Fredrik Lundh 1996. +# +# See the README file for information on usage and redistribution. +# + +# This plugin is a refactored version of Win32IconImagePlugin by Bryan Davis +# . +# https://code.google.com/archive/p/casadebender/wikis/Win32IconImagePlugin.wiki +# +# Icon format references: +# * https://en.wikipedia.org/wiki/ICO_(file_format) +# * https://msdn.microsoft.com/en-us/library/ms997538.aspx +from __future__ import annotations + +import warnings +from io import BytesIO +from math import ceil, log +from typing import IO + +from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin +from ._binary import i16le as i16 +from ._binary import i32le as i32 +from ._binary import o8 +from ._binary import o16le as o16 +from ._binary import o32le as o32 + +# +# -------------------------------------------------------------------- + +_MAGIC = b"\0\0\1\0" + + +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + fp.write(_MAGIC) # (2+2) + bmp = im.encoderinfo.get("bitmap_format") == "bmp" + sizes = im.encoderinfo.get( + "sizes", + [(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)], + ) + frames = [] + provided_ims = [im] + im.encoderinfo.get("append_images", []) + width, height = im.size + for size in sorted(set(sizes)): + if size[0] > width or size[1] > height or size[0] > 256 or size[1] > 256: + continue + + for provided_im in provided_ims: + if provided_im.size != size: + continue + frames.append(provided_im) + if bmp: + bits = BmpImagePlugin.SAVE[provided_im.mode][1] + bits_used = [bits] + for other_im in provided_ims: + if other_im.size != size: + continue + bits = BmpImagePlugin.SAVE[other_im.mode][1] + if bits not in bits_used: + # Another image has been supplied for this size + # with a different bit depth + frames.append(other_im) + bits_used.append(bits) + break + else: + # TODO: invent a more convenient method for proportional scalings + frame = provided_im.copy() + frame.thumbnail(size, Image.Resampling.LANCZOS, reducing_gap=None) + frames.append(frame) + fp.write(o16(len(frames))) # idCount(2) + offset = fp.tell() + len(frames) * 16 + for frame in frames: + width, height = frame.size + # 0 means 256 + fp.write(o8(width if width < 256 else 0)) # bWidth(1) + fp.write(o8(height if height < 256 else 0)) # bHeight(1) + + bits, colors = BmpImagePlugin.SAVE[frame.mode][1:] if bmp else (32, 0) + fp.write(o8(colors)) # bColorCount(1) + fp.write(b"\0") # bReserved(1) + fp.write(b"\0\0") # wPlanes(2) + fp.write(o16(bits)) # wBitCount(2) + + image_io = BytesIO() + if bmp: + frame.save(image_io, "dib") + + if bits != 32: + and_mask = Image.new("1", size) + ImageFile._save( + and_mask, image_io, [("raw", (0, 0) + size, 0, ("1", 0, -1))] + ) + else: + frame.save(image_io, "png") + image_io.seek(0) + image_bytes = image_io.read() + if bmp: + image_bytes = image_bytes[:8] + o32(height * 2) + image_bytes[12:] + bytes_len = len(image_bytes) + fp.write(o32(bytes_len)) # dwBytesInRes(4) + fp.write(o32(offset)) # dwImageOffset(4) + current = fp.tell() + fp.seek(offset) + fp.write(image_bytes) + offset = offset + bytes_len + fp.seek(current) + + +def _accept(prefix: bytes) -> bool: + return prefix[:4] == _MAGIC + + +class IcoFile: + def __init__(self, buf): + """ + Parse image from file-like object containing ico file data + """ + + # check magic + s = buf.read(6) + if not _accept(s): + msg = "not an ICO file" + raise SyntaxError(msg) + + self.buf = buf + self.entry = [] + + # Number of items in file + self.nb_items = i16(s, 4) + + # Get headers for each item + for i in range(self.nb_items): + s = buf.read(16) + + icon_header = { + "width": s[0], + "height": s[1], + "nb_color": s[2], # No. of colors in image (0 if >=8bpp) + "reserved": s[3], + "planes": i16(s, 4), + "bpp": i16(s, 6), + "size": i32(s, 8), + "offset": i32(s, 12), + } + + # See Wikipedia + for j in ("width", "height"): + if not icon_header[j]: + icon_header[j] = 256 + + # See Wikipedia notes about color depth. + # We need this just to differ images with equal sizes + icon_header["color_depth"] = ( + icon_header["bpp"] + or ( + icon_header["nb_color"] != 0 + and ceil(log(icon_header["nb_color"], 2)) + ) + or 256 + ) + + icon_header["dim"] = (icon_header["width"], icon_header["height"]) + icon_header["square"] = icon_header["width"] * icon_header["height"] + + self.entry.append(icon_header) + + self.entry = sorted(self.entry, key=lambda x: x["color_depth"]) + # ICO images are usually squares + self.entry = sorted(self.entry, key=lambda x: x["square"], reverse=True) + + def sizes(self): + """ + Get a list of all available icon sizes and color depths. + """ + return {(h["width"], h["height"]) for h in self.entry} + + def getentryindex(self, size, bpp=False): + for i, h in enumerate(self.entry): + if size == h["dim"] and (bpp is False or bpp == h["color_depth"]): + return i + return 0 + + def getimage(self, size, bpp=False): + """ + Get an image from the icon + """ + return self.frame(self.getentryindex(size, bpp)) + + def frame(self, idx: int) -> Image.Image: + """ + Get an image from frame idx + """ + + header = self.entry[idx] + + self.buf.seek(header["offset"]) + data = self.buf.read(8) + self.buf.seek(header["offset"]) + + im: Image.Image + if data[:8] == PngImagePlugin._MAGIC: + # png frame + im = PngImagePlugin.PngImageFile(self.buf) + Image._decompression_bomb_check(im.size) + else: + # XOR + AND mask bmp frame + im = BmpImagePlugin.DibImageFile(self.buf) + Image._decompression_bomb_check(im.size) + + # change tile dimension to only encompass XOR image + im._size = (im.size[0], int(im.size[1] / 2)) + d, e, o, a = im.tile[0] + im.tile[0] = d, (0, 0) + im.size, o, a + + # figure out where AND mask image starts + bpp = header["bpp"] + if 32 == bpp: + # 32-bit color depth icon image allows semitransparent areas + # PIL's DIB format ignores transparency bits, recover them. + # The DIB is packed in BGRX byte order where X is the alpha + # channel. + + # Back up to start of bmp data + self.buf.seek(o) + # extract every 4th byte (eg. 3,7,11,15,...) + alpha_bytes = self.buf.read(im.size[0] * im.size[1] * 4)[3::4] + + # convert to an 8bpp grayscale image + mask = Image.frombuffer( + "L", # 8bpp + im.size, # (w, h) + alpha_bytes, # source chars + "raw", # raw decoder + ("L", 0, -1), # 8bpp inverted, unpadded, reversed + ) + else: + # get AND image from end of bitmap + w = im.size[0] + if (w % 32) > 0: + # bitmap row data is aligned to word boundaries + w += 32 - (im.size[0] % 32) + + # the total mask data is + # padded row size * height / bits per char + + total_bytes = int((w * im.size[1]) / 8) + and_mask_offset = header["offset"] + header["size"] - total_bytes + + self.buf.seek(and_mask_offset) + mask_data = self.buf.read(total_bytes) + + # convert raw data to image + mask = Image.frombuffer( + "1", # 1 bpp + im.size, # (w, h) + mask_data, # source chars + "raw", # raw decoder + ("1;I", int(w / 8), -1), # 1bpp inverted, padded, reversed + ) + + # now we have two images, im is XOR image and mask is AND image + + # apply mask image as alpha channel + im = im.convert("RGBA") + im.putalpha(mask) + + return im + + +## +# Image plugin for Windows Icon files. + + +class IcoImageFile(ImageFile.ImageFile): + """ + PIL read-only image support for Microsoft Windows .ico files. + + By default the largest resolution image in the file will be loaded. This + can be changed by altering the 'size' attribute before calling 'load'. + + The info dictionary has a key 'sizes' that is a list of the sizes available + in the icon file. + + Handles classic, XP and Vista icon formats. + + When saving, PNG compression is used. Support for this was only added in + Windows Vista. If you are unable to view the icon in Windows, convert the + image to "RGBA" mode before saving. + + This plugin is a refactored version of Win32IconImagePlugin by Bryan Davis + . + https://code.google.com/archive/p/casadebender/wikis/Win32IconImagePlugin.wiki + """ + + format = "ICO" + format_description = "Windows Icon" + + def _open(self) -> None: + self.ico = IcoFile(self.fp) + self.info["sizes"] = self.ico.sizes() + self.size = self.ico.entry[0]["dim"] + self.load() + + @property + def size(self): + return self._size + + @size.setter + def size(self, value): + if value not in self.info["sizes"]: + msg = "This is not one of the allowed sizes of this image" + raise ValueError(msg) + self._size = value + + def load(self): + if self.im is not None and self.im.size == self.size: + # Already loaded + return Image.Image.load(self) + im = self.ico.getimage(self.size) + # if tile is PNG, it won't really be loaded yet + im.load() + self.im = im.im + self.pyaccess = None + self._mode = im.mode + if im.palette: + self.palette = im.palette + if im.size != self.size: + warnings.warn("Image was not the expected size") + + index = self.ico.getentryindex(self.size) + sizes = list(self.info["sizes"]) + sizes[index] = im.size + self.info["sizes"] = set(sizes) + + self.size = im.size + + def load_seek(self, pos: int) -> None: + # Flag the ImageFile.Parser so that it + # just does all the decode at the end. + pass + + +# +# -------------------------------------------------------------------- + + +Image.register_open(IcoImageFile.format, IcoImageFile, _accept) +Image.register_save(IcoImageFile.format, _save) +Image.register_extension(IcoImageFile.format, ".ico") + +Image.register_mime(IcoImageFile.format, "image/x-icon") diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index 015c2febe..621bbf814 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -1,374 +1,374 @@ -# -# The Python Imaging Library. -# $Id$ -# -# IFUNC IM file handling for PIL -# -# history: -# 1995-09-01 fl Created. -# 1997-01-03 fl Save palette images -# 1997-01-08 fl Added sequence support -# 1997-01-23 fl Added P and RGB save support -# 1997-05-31 fl Read floating point images -# 1997-06-22 fl Save floating point images -# 1997-08-27 fl Read and save 1-bit images -# 1998-06-25 fl Added support for RGB+LUT images -# 1998-07-02 fl Added support for YCC images -# 1998-07-15 fl Renamed offset attribute to avoid name clash -# 1998-12-29 fl Added I;16 support -# 2001-02-17 fl Use 're' instead of 'regex' (Python 2.1) (0.7) -# 2003-09-26 fl Added LA/PA support -# -# Copyright (c) 1997-2003 by Secret Labs AB. -# Copyright (c) 1995-2001 by Fredrik Lundh. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import os -import re -from typing import IO, Any - -from . import Image, ImageFile, ImagePalette - -# -------------------------------------------------------------------- -# Standard tags - -COMMENT = "Comment" -DATE = "Date" -EQUIPMENT = "Digitalization equipment" -FRAMES = "File size (no of images)" -LUT = "Lut" -NAME = "Name" -SCALE = "Scale (x,y)" -SIZE = "Image size (x*y)" -MODE = "Image type" - -TAGS = { - COMMENT: 0, - DATE: 0, - EQUIPMENT: 0, - FRAMES: 0, - LUT: 0, - NAME: 0, - SCALE: 0, - SIZE: 0, - MODE: 0, -} - -OPEN = { - # ifunc93/p3cfunc formats - "0 1 image": ("1", "1"), - "L 1 image": ("1", "1"), - "Greyscale image": ("L", "L"), - "Grayscale image": ("L", "L"), - "RGB image": ("RGB", "RGB;L"), - "RLB image": ("RGB", "RLB"), - "RYB image": ("RGB", "RLB"), - "B1 image": ("1", "1"), - "B2 image": ("P", "P;2"), - "B4 image": ("P", "P;4"), - "X 24 image": ("RGB", "RGB"), - "L 32 S image": ("I", "I;32"), - "L 32 F image": ("F", "F;32"), - # old p3cfunc formats - "RGB3 image": ("RGB", "RGB;T"), - "RYB3 image": ("RGB", "RYB;T"), - # extensions - "LA image": ("LA", "LA;L"), - "PA image": ("LA", "PA;L"), - "RGBA image": ("RGBA", "RGBA;L"), - "RGBX image": ("RGBX", "RGBX;L"), - "CMYK image": ("CMYK", "CMYK;L"), - "YCC image": ("YCbCr", "YCbCr;L"), -} - -# ifunc95 extensions -for i in ["8", "8S", "16", "16S", "32", "32F"]: - OPEN[f"L {i} image"] = ("F", f"F;{i}") - OPEN[f"L*{i} image"] = ("F", f"F;{i}") -for i in ["16", "16L", "16B"]: - OPEN[f"L {i} image"] = (f"I;{i}", f"I;{i}") - OPEN[f"L*{i} image"] = (f"I;{i}", f"I;{i}") -for i in ["32S"]: - OPEN[f"L {i} image"] = ("I", f"I;{i}") - OPEN[f"L*{i} image"] = ("I", f"I;{i}") -for j in range(2, 33): - OPEN[f"L*{j} image"] = ("F", f"F;{j}") - - -# -------------------------------------------------------------------- -# Read IM directory - -split = re.compile(rb"^([A-Za-z][^:]*):[ \t]*(.*)[ \t]*$") - - -def number(s: Any) -> float: - try: - return int(s) - except ValueError: - return float(s) - - -## -# Image plugin for the IFUNC IM file format. - - -class ImImageFile(ImageFile.ImageFile): - format = "IM" - format_description = "IFUNC Image Memory" - _close_exclusive_fp_after_loading = False - - def _open(self) -> None: - # Quick rejection: if there's not an LF among the first - # 100 bytes, this is (probably) not a text header. - - if b"\n" not in self.fp.read(100): - msg = "not an IM file" - raise SyntaxError(msg) - self.fp.seek(0) - - n = 0 - - # Default values - self.info[MODE] = "L" - self.info[SIZE] = (512, 512) - self.info[FRAMES] = 1 - - self.rawmode = "L" - - while True: - s = self.fp.read(1) - - # Some versions of IFUNC uses \n\r instead of \r\n... - if s == b"\r": - continue - - if not s or s == b"\0" or s == b"\x1A": - break - - # FIXME: this may read whole file if not a text file - s = s + self.fp.readline() - - if len(s) > 100: - msg = "not an IM file" - raise SyntaxError(msg) - - if s[-2:] == b"\r\n": - s = s[:-2] - elif s[-1:] == b"\n": - s = s[:-1] - - try: - m = split.match(s) - except re.error as e: - msg = "not an IM file" - raise SyntaxError(msg) from e - - if m: - k, v = m.group(1, 2) - - # Don't know if this is the correct encoding, - # but a decent guess (I guess) - k = k.decode("latin-1", "replace") - v = v.decode("latin-1", "replace") - - # Convert value as appropriate - if k in [FRAMES, SCALE, SIZE]: - v = v.replace("*", ",") - v = tuple(map(number, v.split(","))) - if len(v) == 1: - v = v[0] - elif k == MODE and v in OPEN: - v, self.rawmode = OPEN[v] - - # Add to dictionary. Note that COMMENT tags are - # combined into a list of strings. - if k == COMMENT: - if k in self.info: - self.info[k].append(v) - else: - self.info[k] = [v] - else: - self.info[k] = v - - if k in TAGS: - n += 1 - - else: - msg = f"Syntax error in IM header: {s.decode('ascii', 'replace')}" - raise SyntaxError(msg) - - if not n: - msg = "Not an IM file" - raise SyntaxError(msg) - - # Basic attributes - self._size = self.info[SIZE] - self._mode = self.info[MODE] - - # Skip forward to start of image data - while s and s[:1] != b"\x1A": - s = self.fp.read(1) - if not s: - msg = "File truncated" - raise SyntaxError(msg) - - if LUT in self.info: - # convert lookup table to palette or lut attribute - palette = self.fp.read(768) - greyscale = 1 # greyscale palette - linear = 1 # linear greyscale palette - for i in range(256): - if palette[i] == palette[i + 256] == palette[i + 512]: - if palette[i] != i: - linear = 0 - else: - greyscale = 0 - if self.mode in ["L", "LA", "P", "PA"]: - if greyscale: - if not linear: - self.lut = list(palette[:256]) - else: - if self.mode in ["L", "P"]: - self._mode = self.rawmode = "P" - elif self.mode in ["LA", "PA"]: - self._mode = "PA" - self.rawmode = "PA;L" - self.palette = ImagePalette.raw("RGB;L", palette) - elif self.mode == "RGB": - if not greyscale or not linear: - self.lut = list(palette) - - self.frame = 0 - - self.__offset = offs = self.fp.tell() - - self._fp = self.fp # FIXME: hack - - if self.rawmode[:2] == "F;": - # ifunc95 formats - try: - # use bit decoder (if necessary) - bits = int(self.rawmode[2:]) - if bits not in [8, 16, 32]: - self.tile = [("bit", (0, 0) + self.size, offs, (bits, 8, 3, 0, -1))] - return - except ValueError: - pass - - if self.rawmode in ["RGB;T", "RYB;T"]: - # Old LabEye/3PC files. Would be very surprised if anyone - # ever stumbled upon such a file ;-) - size = self.size[0] * self.size[1] - self.tile = [ - ("raw", (0, 0) + self.size, offs, ("G", 0, -1)), - ("raw", (0, 0) + self.size, offs + size, ("R", 0, -1)), - ("raw", (0, 0) + self.size, offs + 2 * size, ("B", 0, -1)), - ] - else: - # LabEye/IFUNC files - self.tile = [("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))] - - @property - def n_frames(self) -> int: - return self.info[FRAMES] - - @property - def is_animated(self) -> bool: - return self.info[FRAMES] > 1 - - def seek(self, frame: int) -> None: - if not self._seek_check(frame): - return - - self.frame = frame - - if self.mode == "1": - bits = 1 - else: - bits = 8 * len(self.mode) - - size = ((self.size[0] * bits + 7) // 8) * self.size[1] - offs = self.__offset + frame * size - - self.fp = self._fp - - self.tile = [("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))] - - def tell(self) -> int: - return self.frame - - -# -# -------------------------------------------------------------------- -# Save IM files - - -SAVE = { - # mode: (im type, raw mode) - "1": ("0 1", "1"), - "L": ("Greyscale", "L"), - "LA": ("LA", "LA;L"), - "P": ("Greyscale", "P"), - "PA": ("LA", "PA;L"), - "I": ("L 32S", "I;32S"), - "I;16": ("L 16", "I;16"), - "I;16L": ("L 16L", "I;16L"), - "I;16B": ("L 16B", "I;16B"), - "F": ("L 32F", "F;32F"), - "RGB": ("RGB", "RGB;L"), - "RGBA": ("RGBA", "RGBA;L"), - "RGBX": ("RGBX", "RGBX;L"), - "CMYK": ("CMYK", "CMYK;L"), - "YCbCr": ("YCC", "YCbCr;L"), -} - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - try: - image_type, rawmode = SAVE[im.mode] - except KeyError as e: - msg = f"Cannot save {im.mode} images as IM" - raise ValueError(msg) from e - - frames = im.encoderinfo.get("frames", 1) - - fp.write(f"Image type: {image_type} image\r\n".encode("ascii")) - if filename: - # Each line must be 100 characters or less, - # or: SyntaxError("not an IM file") - # 8 characters are used for "Name: " and "\r\n" - # Keep just the filename, ditch the potentially overlong path - if isinstance(filename, bytes): - filename = filename.decode("ascii") - name, ext = os.path.splitext(os.path.basename(filename)) - name = "".join([name[: 92 - len(ext)], ext]) - - fp.write(f"Name: {name}\r\n".encode("ascii")) - fp.write(("Image size (x*y): %d*%d\r\n" % im.size).encode("ascii")) - fp.write(f"File size (no of images): {frames}\r\n".encode("ascii")) - if im.mode in ["P", "PA"]: - fp.write(b"Lut: 1\r\n") - fp.write(b"\000" * (511 - fp.tell()) + b"\032") - if im.mode in ["P", "PA"]: - im_palette = im.im.getpalette("RGB", "RGB;L") - colors = len(im_palette) // 3 - palette = b"" - for i in range(3): - palette += im_palette[colors * i : colors * (i + 1)] - palette += b"\x00" * (256 - colors) - fp.write(palette) # 768 bytes - ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, -1))]) - - -# -# -------------------------------------------------------------------- -# Registry - - -Image.register_open(ImImageFile.format, ImImageFile) -Image.register_save(ImImageFile.format, _save) - -Image.register_extension(ImImageFile.format, ".im") +# +# The Python Imaging Library. +# $Id$ +# +# IFUNC IM file handling for PIL +# +# history: +# 1995-09-01 fl Created. +# 1997-01-03 fl Save palette images +# 1997-01-08 fl Added sequence support +# 1997-01-23 fl Added P and RGB save support +# 1997-05-31 fl Read floating point images +# 1997-06-22 fl Save floating point images +# 1997-08-27 fl Read and save 1-bit images +# 1998-06-25 fl Added support for RGB+LUT images +# 1998-07-02 fl Added support for YCC images +# 1998-07-15 fl Renamed offset attribute to avoid name clash +# 1998-12-29 fl Added I;16 support +# 2001-02-17 fl Use 're' instead of 'regex' (Python 2.1) (0.7) +# 2003-09-26 fl Added LA/PA support +# +# Copyright (c) 1997-2003 by Secret Labs AB. +# Copyright (c) 1995-2001 by Fredrik Lundh. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import os +import re +from typing import IO, Any + +from . import Image, ImageFile, ImagePalette + +# -------------------------------------------------------------------- +# Standard tags + +COMMENT = "Comment" +DATE = "Date" +EQUIPMENT = "Digitalization equipment" +FRAMES = "File size (no of images)" +LUT = "Lut" +NAME = "Name" +SCALE = "Scale (x,y)" +SIZE = "Image size (x*y)" +MODE = "Image type" + +TAGS = { + COMMENT: 0, + DATE: 0, + EQUIPMENT: 0, + FRAMES: 0, + LUT: 0, + NAME: 0, + SCALE: 0, + SIZE: 0, + MODE: 0, +} + +OPEN = { + # ifunc93/p3cfunc formats + "0 1 image": ("1", "1"), + "L 1 image": ("1", "1"), + "Greyscale image": ("L", "L"), + "Grayscale image": ("L", "L"), + "RGB image": ("RGB", "RGB;L"), + "RLB image": ("RGB", "RLB"), + "RYB image": ("RGB", "RLB"), + "B1 image": ("1", "1"), + "B2 image": ("P", "P;2"), + "B4 image": ("P", "P;4"), + "X 24 image": ("RGB", "RGB"), + "L 32 S image": ("I", "I;32"), + "L 32 F image": ("F", "F;32"), + # old p3cfunc formats + "RGB3 image": ("RGB", "RGB;T"), + "RYB3 image": ("RGB", "RYB;T"), + # extensions + "LA image": ("LA", "LA;L"), + "PA image": ("LA", "PA;L"), + "RGBA image": ("RGBA", "RGBA;L"), + "RGBX image": ("RGBX", "RGBX;L"), + "CMYK image": ("CMYK", "CMYK;L"), + "YCC image": ("YCbCr", "YCbCr;L"), +} + +# ifunc95 extensions +for i in ["8", "8S", "16", "16S", "32", "32F"]: + OPEN[f"L {i} image"] = ("F", f"F;{i}") + OPEN[f"L*{i} image"] = ("F", f"F;{i}") +for i in ["16", "16L", "16B"]: + OPEN[f"L {i} image"] = (f"I;{i}", f"I;{i}") + OPEN[f"L*{i} image"] = (f"I;{i}", f"I;{i}") +for i in ["32S"]: + OPEN[f"L {i} image"] = ("I", f"I;{i}") + OPEN[f"L*{i} image"] = ("I", f"I;{i}") +for j in range(2, 33): + OPEN[f"L*{j} image"] = ("F", f"F;{j}") + + +# -------------------------------------------------------------------- +# Read IM directory + +split = re.compile(rb"^([A-Za-z][^:]*):[ \t]*(.*)[ \t]*$") + + +def number(s: Any) -> float: + try: + return int(s) + except ValueError: + return float(s) + + +## +# Image plugin for the IFUNC IM file format. + + +class ImImageFile(ImageFile.ImageFile): + format = "IM" + format_description = "IFUNC Image Memory" + _close_exclusive_fp_after_loading = False + + def _open(self) -> None: + # Quick rejection: if there's not an LF among the first + # 100 bytes, this is (probably) not a text header. + + if b"\n" not in self.fp.read(100): + msg = "not an IM file" + raise SyntaxError(msg) + self.fp.seek(0) + + n = 0 + + # Default values + self.info[MODE] = "L" + self.info[SIZE] = (512, 512) + self.info[FRAMES] = 1 + + self.rawmode = "L" + + while True: + s = self.fp.read(1) + + # Some versions of IFUNC uses \n\r instead of \r\n... + if s == b"\r": + continue + + if not s or s == b"\0" or s == b"\x1A": + break + + # FIXME: this may read whole file if not a text file + s = s + self.fp.readline() + + if len(s) > 100: + msg = "not an IM file" + raise SyntaxError(msg) + + if s[-2:] == b"\r\n": + s = s[:-2] + elif s[-1:] == b"\n": + s = s[:-1] + + try: + m = split.match(s) + except re.error as e: + msg = "not an IM file" + raise SyntaxError(msg) from e + + if m: + k, v = m.group(1, 2) + + # Don't know if this is the correct encoding, + # but a decent guess (I guess) + k = k.decode("latin-1", "replace") + v = v.decode("latin-1", "replace") + + # Convert value as appropriate + if k in [FRAMES, SCALE, SIZE]: + v = v.replace("*", ",") + v = tuple(map(number, v.split(","))) + if len(v) == 1: + v = v[0] + elif k == MODE and v in OPEN: + v, self.rawmode = OPEN[v] + + # Add to dictionary. Note that COMMENT tags are + # combined into a list of strings. + if k == COMMENT: + if k in self.info: + self.info[k].append(v) + else: + self.info[k] = [v] + else: + self.info[k] = v + + if k in TAGS: + n += 1 + + else: + msg = f"Syntax error in IM header: {s.decode('ascii', 'replace')}" + raise SyntaxError(msg) + + if not n: + msg = "Not an IM file" + raise SyntaxError(msg) + + # Basic attributes + self._size = self.info[SIZE] + self._mode = self.info[MODE] + + # Skip forward to start of image data + while s and s[:1] != b"\x1A": + s = self.fp.read(1) + if not s: + msg = "File truncated" + raise SyntaxError(msg) + + if LUT in self.info: + # convert lookup table to palette or lut attribute + palette = self.fp.read(768) + greyscale = 1 # greyscale palette + linear = 1 # linear greyscale palette + for i in range(256): + if palette[i] == palette[i + 256] == palette[i + 512]: + if palette[i] != i: + linear = 0 + else: + greyscale = 0 + if self.mode in ["L", "LA", "P", "PA"]: + if greyscale: + if not linear: + self.lut = list(palette[:256]) + else: + if self.mode in ["L", "P"]: + self._mode = self.rawmode = "P" + elif self.mode in ["LA", "PA"]: + self._mode = "PA" + self.rawmode = "PA;L" + self.palette = ImagePalette.raw("RGB;L", palette) + elif self.mode == "RGB": + if not greyscale or not linear: + self.lut = list(palette) + + self.frame = 0 + + self.__offset = offs = self.fp.tell() + + self._fp = self.fp # FIXME: hack + + if self.rawmode[:2] == "F;": + # ifunc95 formats + try: + # use bit decoder (if necessary) + bits = int(self.rawmode[2:]) + if bits not in [8, 16, 32]: + self.tile = [("bit", (0, 0) + self.size, offs, (bits, 8, 3, 0, -1))] + return + except ValueError: + pass + + if self.rawmode in ["RGB;T", "RYB;T"]: + # Old LabEye/3PC files. Would be very surprised if anyone + # ever stumbled upon such a file ;-) + size = self.size[0] * self.size[1] + self.tile = [ + ("raw", (0, 0) + self.size, offs, ("G", 0, -1)), + ("raw", (0, 0) + self.size, offs + size, ("R", 0, -1)), + ("raw", (0, 0) + self.size, offs + 2 * size, ("B", 0, -1)), + ] + else: + # LabEye/IFUNC files + self.tile = [("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))] + + @property + def n_frames(self) -> int: + return self.info[FRAMES] + + @property + def is_animated(self) -> bool: + return self.info[FRAMES] > 1 + + def seek(self, frame: int) -> None: + if not self._seek_check(frame): + return + + self.frame = frame + + if self.mode == "1": + bits = 1 + else: + bits = 8 * len(self.mode) + + size = ((self.size[0] * bits + 7) // 8) * self.size[1] + offs = self.__offset + frame * size + + self.fp = self._fp + + self.tile = [("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))] + + def tell(self) -> int: + return self.frame + + +# +# -------------------------------------------------------------------- +# Save IM files + + +SAVE = { + # mode: (im type, raw mode) + "1": ("0 1", "1"), + "L": ("Greyscale", "L"), + "LA": ("LA", "LA;L"), + "P": ("Greyscale", "P"), + "PA": ("LA", "PA;L"), + "I": ("L 32S", "I;32S"), + "I;16": ("L 16", "I;16"), + "I;16L": ("L 16L", "I;16L"), + "I;16B": ("L 16B", "I;16B"), + "F": ("L 32F", "F;32F"), + "RGB": ("RGB", "RGB;L"), + "RGBA": ("RGBA", "RGBA;L"), + "RGBX": ("RGBX", "RGBX;L"), + "CMYK": ("CMYK", "CMYK;L"), + "YCbCr": ("YCC", "YCbCr;L"), +} + + +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + try: + image_type, rawmode = SAVE[im.mode] + except KeyError as e: + msg = f"Cannot save {im.mode} images as IM" + raise ValueError(msg) from e + + frames = im.encoderinfo.get("frames", 1) + + fp.write(f"Image type: {image_type} image\r\n".encode("ascii")) + if filename: + # Each line must be 100 characters or less, + # or: SyntaxError("not an IM file") + # 8 characters are used for "Name: " and "\r\n" + # Keep just the filename, ditch the potentially overlong path + if isinstance(filename, bytes): + filename = filename.decode("ascii") + name, ext = os.path.splitext(os.path.basename(filename)) + name = "".join([name[: 92 - len(ext)], ext]) + + fp.write(f"Name: {name}\r\n".encode("ascii")) + fp.write(("Image size (x*y): %d*%d\r\n" % im.size).encode("ascii")) + fp.write(f"File size (no of images): {frames}\r\n".encode("ascii")) + if im.mode in ["P", "PA"]: + fp.write(b"Lut: 1\r\n") + fp.write(b"\000" * (511 - fp.tell()) + b"\032") + if im.mode in ["P", "PA"]: + im_palette = im.im.getpalette("RGB", "RGB;L") + colors = len(im_palette) // 3 + palette = b"" + for i in range(3): + palette += im_palette[colors * i : colors * (i + 1)] + palette += b"\x00" * (256 - colors) + fp.write(palette) # 768 bytes + ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, -1))]) + + +# +# -------------------------------------------------------------------- +# Registry + + +Image.register_open(ImImageFile.format, ImImageFile) +Image.register_save(ImImageFile.format, _save) + +Image.register_extension(ImImageFile.format, ".im") diff --git a/src/PIL/Image.py b/src/PIL/Image.py index bdd869ccc..947deeb78 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1,4083 +1,4096 @@ -# -# The Python Imaging Library. -# $Id$ -# -# the Image class wrapper -# -# partial release history: -# 1995-09-09 fl Created -# 1996-03-11 fl PIL release 0.0 (proof of concept) -# 1996-04-30 fl PIL release 0.1b1 -# 1999-07-28 fl PIL release 1.0 final -# 2000-06-07 fl PIL release 1.1 -# 2000-10-20 fl PIL release 1.1.1 -# 2001-05-07 fl PIL release 1.1.2 -# 2002-03-15 fl PIL release 1.1.3 -# 2003-05-10 fl PIL release 1.1.4 -# 2005-03-28 fl PIL release 1.1.5 -# 2006-12-02 fl PIL release 1.1.6 -# 2009-11-15 fl PIL release 1.1.7 -# -# Copyright (c) 1997-2009 by Secret Labs AB. All rights reserved. -# Copyright (c) 1995-2009 by Fredrik Lundh. -# -# See the README file for information on usage and redistribution. -# - -from __future__ import annotations - -import abc -import atexit -import builtins -import io -import logging -import math -import os -import re -import struct -import sys -import tempfile -import warnings -from collections.abc import Callable, MutableMapping -from enum import IntEnum -from types import ModuleType -from typing import IO, TYPE_CHECKING, Any, Literal, Protocol, Sequence, Tuple, cast - -# VERSION was removed in Pillow 6.0.0. -# PILLOW_VERSION was removed in Pillow 9.0.0. -# Use __version__ instead. -from . import ( - ExifTags, - ImageMode, - TiffTags, - UnidentifiedImageError, - __version__, - _plugins, -) -from ._binary import i32le, o32be, o32le -from ._deprecate import deprecate -from ._typing import StrOrBytesPath, TypeGuard -from ._util import DeferredError, is_path - -ElementTree: ModuleType | None -try: - from defusedxml import ElementTree -except ImportError: - ElementTree = None - -logger = logging.getLogger(__name__) - - -class DecompressionBombWarning(RuntimeWarning): - pass - - -class DecompressionBombError(Exception): - pass - - -# Limit to around a quarter gigabyte for a 24-bit (3 bpp) image -MAX_IMAGE_PIXELS: int | None = int(1024 * 1024 * 1024 // 4 // 3) - - -try: - # If the _imaging C module is not present, Pillow will not load. - # Note that other modules should not refer to _imaging directly; - # import Image and use the Image.core variable instead. - # Also note that Image.core is not a publicly documented interface, - # and should be considered private and subject to change. - from . import _imaging as core - - if __version__ != getattr(core, "PILLOW_VERSION", None): - msg = ( - "The _imaging extension was built for another version of Pillow or PIL:\n" - f"Core version: {getattr(core, 'PILLOW_VERSION', None)}\n" - f"Pillow version: {__version__}" - ) - raise ImportError(msg) - -except ImportError as v: - core = DeferredError.new(ImportError("The _imaging C module is not installed.")) - # Explanations for ways that we know we might have an import error - if str(v).startswith("Module use of python"): - # The _imaging C module is present, but not compiled for - # the right version (windows only). Print a warning, if - # possible. - warnings.warn( - "The _imaging extension was built for another version of Python.", - RuntimeWarning, - ) - elif str(v).startswith("The _imaging extension"): - warnings.warn(str(v), RuntimeWarning) - # Fail here anyway. Don't let people run with a mostly broken Pillow. - # see docs/porting.rst - raise - - -USE_CFFI_ACCESS = False -cffi: ModuleType | None -try: - import cffi -except ImportError: - cffi = None - - -def isImageType(t: Any) -> TypeGuard[Image]: - """ - Checks if an object is an image object. - - .. warning:: - - This function is for internal use only. - - :param t: object to check if it's an image - :returns: True if the object is an image - """ - return hasattr(t, "im") - - -# -# Constants - - -# transpose -class Transpose(IntEnum): - FLIP_LEFT_RIGHT = 0 - FLIP_TOP_BOTTOM = 1 - ROTATE_90 = 2 - ROTATE_180 = 3 - ROTATE_270 = 4 - TRANSPOSE = 5 - TRANSVERSE = 6 - - -# transforms (also defined in Imaging.h) -class Transform(IntEnum): - AFFINE = 0 - EXTENT = 1 - PERSPECTIVE = 2 - QUAD = 3 - MESH = 4 - - -# resampling filters (also defined in Imaging.h) -class Resampling(IntEnum): - NEAREST = 0 - BOX = 4 - BILINEAR = 2 - HAMMING = 5 - BICUBIC = 3 - LANCZOS = 1 - - -_filters_support = { - Resampling.BOX: 0.5, - Resampling.BILINEAR: 1.0, - Resampling.HAMMING: 1.0, - Resampling.BICUBIC: 2.0, - Resampling.LANCZOS: 3.0, -} - - -# dithers -class Dither(IntEnum): - NONE = 0 - ORDERED = 1 # Not yet implemented - RASTERIZE = 2 # Not yet implemented - FLOYDSTEINBERG = 3 # default - - -# palettes/quantizers -class Palette(IntEnum): - WEB = 0 - ADAPTIVE = 1 - - -class Quantize(IntEnum): - MEDIANCUT = 0 - MAXCOVERAGE = 1 - FASTOCTREE = 2 - LIBIMAGEQUANT = 3 - - -module = sys.modules[__name__] -for enum in (Transpose, Transform, Resampling, Dither, Palette, Quantize): - for item in enum: - setattr(module, item.name, item.value) - - -if hasattr(core, "DEFAULT_STRATEGY"): - DEFAULT_STRATEGY = core.DEFAULT_STRATEGY - FILTERED = core.FILTERED - HUFFMAN_ONLY = core.HUFFMAN_ONLY - RLE = core.RLE - FIXED = core.FIXED - - -# -------------------------------------------------------------------- -# Registries - -if TYPE_CHECKING: - from . import ImageFile -ID: list[str] = [] -OPEN: dict[ - str, - tuple[ - Callable[[IO[bytes], str | bytes], ImageFile.ImageFile], - Callable[[bytes], bool | str] | None, - ], -] = {} -MIME: dict[str, str] = {} -SAVE: dict[str, Callable[[Image, IO[bytes], str | bytes], None]] = {} -SAVE_ALL: dict[str, Callable[[Image, IO[bytes], str | bytes], None]] = {} -EXTENSION: dict[str, str] = {} -DECODERS: dict[str, type[ImageFile.PyDecoder]] = {} -ENCODERS: dict[str, type[ImageFile.PyEncoder]] = {} - -# -------------------------------------------------------------------- -# Modes - -_ENDIAN = "<" if sys.byteorder == "little" else ">" - - -def _conv_type_shape(im): - m = ImageMode.getmode(im.mode) - shape = (im.height, im.width) - extra = len(m.bands) - if extra != 1: - shape += (extra,) - return shape, m.typestr - - -MODES = [ - "1", - "CMYK", - "F", - "HSV", - "I", - "I;16", - "I;16B", - "I;16L", - "I;16N", - "L", - "LA", - "La", - "LAB", - "P", - "PA", - "RGB", - "RGBA", - "RGBa", - "RGBX", - "YCbCr", -] - -# raw modes that may be memory mapped. NOTE: if you change this, you -# may have to modify the stride calculation in map.c too! -_MAPMODES = ("L", "P", "RGBX", "RGBA", "CMYK", "I;16", "I;16L", "I;16B") - - -def getmodebase(mode: str) -> str: - """ - Gets the "base" mode for given mode. This function returns "L" for - images that contain grayscale data, and "RGB" for images that - contain color data. - - :param mode: Input mode. - :returns: "L" or "RGB". - :exception KeyError: If the input mode was not a standard mode. - """ - return ImageMode.getmode(mode).basemode - - -def getmodetype(mode: str) -> str: - """ - Gets the storage type mode. Given a mode, this function returns a - single-layer mode suitable for storing individual bands. - - :param mode: Input mode. - :returns: "L", "I", or "F". - :exception KeyError: If the input mode was not a standard mode. - """ - return ImageMode.getmode(mode).basetype - - -def getmodebandnames(mode: str) -> tuple[str, ...]: - """ - Gets a list of individual band names. Given a mode, this function returns - a tuple containing the names of individual bands (use - :py:method:`~PIL.Image.getmodetype` to get the mode used to store each - individual band. - - :param mode: Input mode. - :returns: A tuple containing band names. The length of the tuple - gives the number of bands in an image of the given mode. - :exception KeyError: If the input mode was not a standard mode. - """ - return ImageMode.getmode(mode).bands - - -def getmodebands(mode: str) -> int: - """ - Gets the number of individual bands for this mode. - - :param mode: Input mode. - :returns: The number of bands in this mode. - :exception KeyError: If the input mode was not a standard mode. - """ - return len(ImageMode.getmode(mode).bands) - - -# -------------------------------------------------------------------- -# Helpers - -_initialized = 0 - - -def preinit() -> None: - """ - Explicitly loads BMP, GIF, JPEG, PPM and PPM file format drivers. - - It is called when opening or saving images. - """ - - global _initialized - if _initialized >= 1: - return - - try: - from . import BmpImagePlugin - - assert BmpImagePlugin - except ImportError: - pass - try: - from . import GifImagePlugin - - assert GifImagePlugin - except ImportError: - pass - try: - from . import JpegImagePlugin - - assert JpegImagePlugin - except ImportError: - pass - try: - from . import PpmImagePlugin - - assert PpmImagePlugin - except ImportError: - pass - try: - from . import PngImagePlugin - - assert PngImagePlugin - except ImportError: - pass - - _initialized = 1 - - -def init() -> bool: - """ - Explicitly initializes the Python Imaging Library. This function - loads all available file format drivers. - - It is called when opening or saving images if :py:meth:`~preinit()` is - insufficient, and by :py:meth:`~PIL.features.pilinfo`. - """ - - global _initialized - if _initialized >= 2: - return False - - parent_name = __name__.rpartition(".")[0] - for plugin in _plugins: - try: - logger.debug("Importing %s", plugin) - __import__(f"{parent_name}.{plugin}", globals(), locals(), []) - except ImportError as e: - logger.debug("Image: failed to import %s: %s", plugin, e) - - if OPEN or SAVE: - _initialized = 2 - return True - return False - - -# -------------------------------------------------------------------- -# Codec factories (used by tobytes/frombytes and ImageFile.load) - - -def _getdecoder(mode, decoder_name, args, extra=()): - # tweak arguments - if args is None: - args = () - elif not isinstance(args, tuple): - args = (args,) - - try: - decoder = DECODERS[decoder_name] - except KeyError: - pass - else: - return decoder(mode, *args + extra) - - try: - # get decoder - decoder = getattr(core, f"{decoder_name}_decoder") - except AttributeError as e: - msg = f"decoder {decoder_name} not available" - raise OSError(msg) from e - return decoder(mode, *args + extra) - - -def _getencoder(mode, encoder_name, args, extra=()): - # tweak arguments - if args is None: - args = () - elif not isinstance(args, tuple): - args = (args,) - - try: - encoder = ENCODERS[encoder_name] - except KeyError: - pass - else: - return encoder(mode, *args + extra) - - try: - # get encoder - encoder = getattr(core, f"{encoder_name}_encoder") - except AttributeError as e: - msg = f"encoder {encoder_name} not available" - raise OSError(msg) from e - return encoder(mode, *args + extra) - - -# -------------------------------------------------------------------- -# Simple expression analyzer - - -class _E: - def __init__(self, scale, offset) -> None: - self.scale = scale - self.offset = offset - - def __neg__(self): - return _E(-self.scale, -self.offset) - - def __add__(self, other): - if isinstance(other, _E): - return _E(self.scale + other.scale, self.offset + other.offset) - return _E(self.scale, self.offset + other) - - __radd__ = __add__ - - def __sub__(self, other): - return self + -other - - def __rsub__(self, other): - return other + -self - - def __mul__(self, other): - if isinstance(other, _E): - return NotImplemented - return _E(self.scale * other, self.offset * other) - - __rmul__ = __mul__ - - def __truediv__(self, other): - if isinstance(other, _E): - return NotImplemented - return _E(self.scale / other, self.offset / other) - - -def _getscaleoffset(expr): - a = expr(_E(1, 0)) - return (a.scale, a.offset) if isinstance(a, _E) else (0, a) - - -# -------------------------------------------------------------------- -# Implementation wrapper - - -class SupportsGetData(Protocol): - def getdata( - self, - ) -> tuple[Transform, Sequence[int]]: ... - - -class Image: - """ - This class represents an image object. To create - :py:class:`~PIL.Image.Image` objects, use the appropriate factory - functions. There's hardly ever any reason to call the Image constructor - directly. - - * :py:func:`~PIL.Image.open` - * :py:func:`~PIL.Image.new` - * :py:func:`~PIL.Image.frombytes` - """ - - format: str | None = None - format_description: str | None = None - _close_exclusive_fp_after_loading = True - - def __init__(self): - # FIXME: take "new" parameters / other image? - # FIXME: turn mode and size into delegating properties? - self.im = None - self._mode = "" - self._size = (0, 0) - self.palette = None - self.info = {} - self.readonly = 0 - self.pyaccess = None - self._exif = None - - @property - def width(self) -> int: - return self.size[0] - - @property - def height(self) -> int: - return self.size[1] - - @property - def size(self) -> tuple[int, int]: - return self._size - - @property - def mode(self): - return self._mode - - def _new(self, im) -> Image: - new = Image() - new.im = im - new._mode = im.mode - new._size = im.size - if im.mode in ("P", "PA"): - if self.palette: - new.palette = self.palette.copy() - else: - from . import ImagePalette - - new.palette = ImagePalette.ImagePalette() - new.info = self.info.copy() - return new - - # Context manager support - 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"): - if getattr(self, "_exclusive_fp", False): - self._close_fp() - self.fp = None - - def close(self) -> None: - """ - Closes the file pointer, if possible. - - This operation will destroy the image core and release its memory. - The image data will be unusable afterward. - - This function is required to close images that have multiple frames or - have not had their file read and closed by the - :py:meth:`~PIL.Image.Image.load` method. See :ref:`file-handling` for - more information. - """ - if hasattr(self, "fp"): - try: - self._close_fp() - self.fp = None - except Exception as msg: - logger.debug("Error closing: %s", msg) - - if getattr(self, "map", None): - self.map = None - - # Instead of simply setting to None, we're setting up a - # deferred error that will better explain that the core image - # object is gone. - self.im = DeferredError(ValueError("Operation on closed image")) - - def _copy(self) -> None: - self.load() - self.im = self.im.copy() - self.pyaccess = None - self.readonly = 0 - - def _ensure_mutable(self) -> None: - if self.readonly: - self._copy() - else: - self.load() - - def _dump( - self, file: str | None = None, format: str | None = None, **options: Any - ) -> str: - suffix = "" - if format: - suffix = f".{format}" - - if not file: - f, filename = tempfile.mkstemp(suffix) - os.close(f) - else: - filename = file - if not filename.endswith(suffix): - filename = filename + suffix - - self.load() - - if not format or format == "PPM": - self.im.save_ppm(filename) - else: - self.save(filename, format, **options) - - return filename - - def __eq__(self, other: object) -> bool: - if self.__class__ is not other.__class__: - return False - assert isinstance(other, Image) - return ( - self.mode == other.mode - and self.size == other.size - and self.info == other.info - and self.getpalette() == other.getpalette() - and self.tobytes() == other.tobytes() - ) - - 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), - ) - - def _repr_pretty_(self, p, cycle) -> None: - """IPython plain text display support""" - - # Same as __repr__ but without unpredictable id(self), - # to keep Jupyter notebook `text/plain` output stable. - p.text( - "<%s.%s image mode=%s size=%dx%d>" - % ( - self.__class__.__module__, - self.__class__.__name__, - self.mode, - self.size[0], - self.size[1], - ) - ) - - def _repr_image(self, image_format, **kwargs): - """Helper function for iPython display hook. - - :param image_format: Image format. - :returns: image as bytes, saved into the given format. - """ - b = io.BytesIO() - try: - self.save(b, image_format, **kwargs) - except Exception: - return None - return b.getvalue() - - def _repr_png_(self): - """iPython display hook support for PNG format. - - :returns: PNG version of the image as bytes - """ - return self._repr_image("PNG", compress_level=1) - - def _repr_jpeg_(self): - """iPython display hook support for JPEG format. - - :returns: JPEG version of the image as bytes - """ - return self._repr_image("JPEG") - - @property - def __array_interface__(self): - # numpy array interface support - new = {"version": 3} - try: - if self.mode == "1": - # Binary images need to be extended from bits to bytes - # See: https://github.com/python-pillow/Pillow/issues/350 - new["data"] = self.tobytes("raw", "L") - else: - new["data"] = self.tobytes() - except Exception as e: - if not isinstance(e, (MemoryError, RecursionError)): - try: - import numpy - from packaging.version import parse as parse_version - except ImportError: - pass - else: - if parse_version(numpy.__version__) < parse_version("1.23"): - warnings.warn(str(e)) - raise - new["shape"], new["typestr"] = _conv_type_shape(self) - return new - - def __getstate__(self): - im_data = self.tobytes() # load image first - return [self.info, self.mode, self.size, self.getpalette(), im_data] - - def __setstate__(self, state) -> None: - Image.__init__(self) - info, mode, size, palette, data = state - self.info = info - self._mode = mode - self._size = size - self.im = core.new(mode, size) - if mode in ("L", "LA", "P", "PA") and palette: - self.putpalette(palette) - self.frombytes(data) - - def tobytes(self, encoder_name: str = "raw", *args) -> bytes: - """ - Return image as a bytes object. - - .. 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. - - :param encoder_name: What encoder to use. The default is to - use the standard "raw" encoder. - - A list of C encoders can be seen under - codecs section of the function array in - :file:`_imaging.c`. Python encoders are - registered within the relevant plugins. - :param args: Extra arguments to the encoder. - :returns: A :py:class:`bytes` object. - """ - - # may pass tuple instead of argument list - if len(args) == 1 and isinstance(args[0], tuple): - args = args[0] - - if encoder_name == "raw" and args == (): - args = self.mode - - self.load() - - if self.width == 0 or self.height == 0: - return b"" - - # unpack data - e = _getencoder(self.mode, encoder_name, args) - e.setimage(self.im) - - bufsize = max(65536, self.size[0] * 4) # see RawEncode.c - - output = [] - while True: - bytes_consumed, errcode, data = e.encode(bufsize) - output.append(data) - if errcode: - break - if errcode < 0: - msg = f"encoder error {errcode} in tobytes" - raise RuntimeError(msg) - - return b"".join(output) - - def tobitmap(self, name: str = "image") -> bytes: - """ - Returns the image converted to an X11 bitmap. - - .. note:: This method only works for mode "1" images. - - :param name: The name prefix to use for the bitmap variables. - :returns: A string containing an X11 bitmap. - :raises ValueError: If the mode is not "1" - """ - - self.load() - if self.mode != "1": - msg = "not a bitmap" - raise ValueError(msg) - data = self.tobytes("xbm") - return b"".join( - [ - f"#define {name}_width {self.size[0]}\n".encode("ascii"), - f"#define {name}_height {self.size[1]}\n".encode("ascii"), - f"static char {name}_bits[] = {{\n".encode("ascii"), - data, - b"};", - ] - ) - - def frombytes(self, data: bytes, decoder_name: str = "raw", *args) -> None: - """ - Loads this image with pixel data from a bytes object. - - This method is similar to the :py:func:`~PIL.Image.frombytes` function, - but loads data into this image instead of creating a new image object. - """ - - if self.width == 0 or self.height == 0: - return - - # may pass tuple instead of argument list - if len(args) == 1 and isinstance(args[0], tuple): - args = args[0] - - # default format - if decoder_name == "raw" and args == (): - args = self.mode - - # unpack data - d = _getdecoder(self.mode, decoder_name, args) - d.setimage(self.im) - s = d.decode(data) - - if s[0] >= 0: - msg = "not enough image data" - raise ValueError(msg) - if s[1] != 0: - msg = "cannot decode image data" - raise ValueError(msg) - - def load(self): - """ - Allocates storage for the image and loads the pixel data. In - normal cases, you don't need to call this method, since the - Image class automatically loads an opened image when it is - accessed for the first time. - - If the file associated with the image was opened by Pillow, then this - method will close it. The exception to this is if the image has - multiple frames, in which case the file will be left open for seek - operations. See :ref:`file-handling` for more information. - - :returns: An image access object. - :rtype: :ref:`PixelAccess` or :py:class:`PIL.PyAccess` - """ - if self.im is not None and self.palette and self.palette.dirty: - # realize palette - mode, arr = self.palette.getdata() - self.im.putpalette(mode, arr) - self.palette.dirty = 0 - self.palette.rawmode = None - if "transparency" in self.info and mode in ("LA", "PA"): - if isinstance(self.info["transparency"], int): - self.im.putpalettealpha(self.info["transparency"], 0) - else: - self.im.putpalettealphas(self.info["transparency"]) - self.palette.mode = "RGBA" - else: - palette_mode = "RGBA" if mode.startswith("RGBA") else "RGB" - self.palette.mode = palette_mode - self.palette.palette = self.im.getpalette(palette_mode, palette_mode) - - if self.im is not None: - if cffi and USE_CFFI_ACCESS: - if self.pyaccess: - return self.pyaccess - from . import PyAccess - - self.pyaccess = PyAccess.new(self, self.readonly) - if self.pyaccess: - return self.pyaccess - return self.im.pixel_access(self.readonly) - - def verify(self) -> None: - """ - Verifies the contents of a file. For data read from a file, this - method attempts to determine if the file is broken, without - actually decoding the image data. If this method finds any - problems, it raises suitable exceptions. If you need to load - the image after using this method, you must reopen the image - file. - """ - pass - - def convert( - self, - mode: str | None = None, - matrix: tuple[float, ...] | None = None, - dither: Dither | None = None, - palette: Palette = Palette.WEB, - colors: int = 256, - ) -> Image: - """ - Returns a converted copy of this image. For the "P" mode, this - method translates pixels through the palette. If mode is - omitted, a mode is chosen so that all information in the image - and the palette can be represented without a palette. - - This supports all possible conversions between "L", "RGB" and "CMYK". The - ``matrix`` argument only supports "L" and "RGB". - - When translating a color image to grayscale (mode "L"), - the library uses the ITU-R 601-2 luma transform:: - - L = R * 299/1000 + G * 587/1000 + B * 114/1000 - - The default method of converting a grayscale ("L") or "RGB" - image into a bilevel (mode "1") image uses Floyd-Steinberg - dither to approximate the original image luminosity levels. If - dither is ``None``, all values larger than 127 are set to 255 (white), - all other values to 0 (black). To use other thresholds, use the - :py:meth:`~PIL.Image.Image.point` method. - - When converting from "RGBA" to "P" without a ``matrix`` argument, - this passes the operation to :py:meth:`~PIL.Image.Image.quantize`, - and ``dither`` and ``palette`` are ignored. - - When converting from "PA", if an "RGBA" palette is present, the alpha - channel from the image will be used instead of the values from the palette. - - :param mode: The requested mode. See: :ref:`concept-modes`. - :param matrix: An optional conversion matrix. If given, this - should be 4- or 12-tuple containing floating point values. - :param dither: Dithering method, used when converting from - mode "RGB" to "P" or from "RGB" or "L" to "1". - Available methods are :data:`Dither.NONE` or :data:`Dither.FLOYDSTEINBERG` - (default). Note that this is not used when ``matrix`` is supplied. - :param palette: Palette to use when converting from mode "RGB" - to "P". Available palettes are :data:`Palette.WEB` or - :data:`Palette.ADAPTIVE`. - :param colors: Number of colors to use for the :data:`Palette.ADAPTIVE` - palette. Defaults to 256. - :rtype: :py:class:`~PIL.Image.Image` - :returns: An :py:class:`~PIL.Image.Image` object. - """ - - if mode in ("BGR;15", "BGR;16", "BGR;24"): - deprecate(mode, 12) - - self.load() - - has_transparency = "transparency" in self.info - if not mode and self.mode == "P": - # determine default mode - if self.palette: - mode = self.palette.mode - else: - mode = "RGB" - if mode == "RGB" and has_transparency: - mode = "RGBA" - if not mode or (mode == self.mode and not matrix): - return self.copy() - - if matrix: - # matrix conversion - if mode not in ("L", "RGB"): - msg = "illegal conversion" - raise ValueError(msg) - im = self.im.convert_matrix(mode, matrix) - new_im = self._new(im) - if has_transparency and self.im.bands == 3: - transparency = new_im.info["transparency"] - - def convert_transparency(m, v): - v = m[0] * v[0] + m[1] * v[1] + m[2] * v[2] + m[3] * 0.5 - return max(0, min(255, int(v))) - - if mode == "L": - transparency = convert_transparency(matrix, transparency) - elif len(mode) == 3: - transparency = tuple( - convert_transparency(matrix[i * 4 : i * 4 + 4], transparency) - for i in range(0, len(transparency)) - ) - new_im.info["transparency"] = transparency - return new_im - - if mode == "P" and self.mode == "RGBA": - return self.quantize(colors) - - trns = None - delete_trns = False - # transparency handling - if has_transparency: - if (self.mode in ("1", "L", "I", "I;16") and mode in ("LA", "RGBA")) or ( - self.mode == "RGB" and mode in ("La", "LA", "RGBa", "RGBA") - ): - # Use transparent conversion to promote from transparent - # color to an alpha channel. - new_im = self._new( - self.im.convert_transparent(mode, self.info["transparency"]) - ) - del new_im.info["transparency"] - return new_im - elif self.mode in ("L", "RGB", "P") and mode in ("L", "RGB", "P"): - t = self.info["transparency"] - if isinstance(t, bytes): - # Dragons. This can't be represented by a single color - warnings.warn( - "Palette images with Transparency expressed in bytes should be " - "converted to RGBA images" - ) - delete_trns = True - else: - # get the new transparency color. - # use existing conversions - trns_im = new(self.mode, (1, 1)) - if self.mode == "P": - trns_im.putpalette(self.palette) - if isinstance(t, tuple): - err = "Couldn't allocate a palette color for transparency" - try: - t = trns_im.palette.getcolor(t, self) - except ValueError as e: - if str(e) == "cannot allocate more than 256 colors": - # If all 256 colors are in use, - # then there is no need for transparency - t = None - else: - raise ValueError(err) from e - if t is None: - trns = None - else: - trns_im.putpixel((0, 0), t) - - if mode in ("L", "RGB"): - trns_im = trns_im.convert(mode) - else: - # can't just retrieve the palette number, got to do it - # after quantization. - trns_im = trns_im.convert("RGB") - trns = trns_im.getpixel((0, 0)) - - elif self.mode == "P" and mode in ("LA", "PA", "RGBA"): - t = self.info["transparency"] - delete_trns = True - - if isinstance(t, bytes): - self.im.putpalettealphas(t) - elif isinstance(t, int): - self.im.putpalettealpha(t, 0) - else: - msg = "Transparency for P mode should be bytes or int" - raise ValueError(msg) - - if mode == "P" and palette == Palette.ADAPTIVE: - im = self.im.quantize(colors) - new_im = self._new(im) - from . import ImagePalette - - new_im.palette = ImagePalette.ImagePalette( - "RGB", new_im.im.getpalette("RGB") - ) - if delete_trns: - # This could possibly happen if we requantize to fewer colors. - # The transparency would be totally off in that case. - del new_im.info["transparency"] - if trns is not None: - try: - new_im.info["transparency"] = new_im.palette.getcolor(trns, new_im) - except Exception: - # if we can't make a transparent color, don't leave the old - # transparency hanging around to mess us up. - del new_im.info["transparency"] - warnings.warn("Couldn't allocate palette entry for transparency") - return new_im - - if "LAB" in (self.mode, mode): - other_mode = mode if self.mode == "LAB" else self.mode - if other_mode in ("RGB", "RGBA", "RGBX"): - from . import ImageCms - - srgb = ImageCms.createProfile("sRGB") - lab = ImageCms.createProfile("LAB") - profiles = [lab, srgb] if self.mode == "LAB" else [srgb, lab] - transform = ImageCms.buildTransform( - profiles[0], profiles[1], self.mode, mode - ) - return transform.apply(self) - - # colorspace conversion - if dither is None: - dither = Dither.FLOYDSTEINBERG - - try: - im = self.im.convert(mode, dither) - except ValueError: - try: - # normalize source image and try again - modebase = getmodebase(self.mode) - if modebase == self.mode: - raise - im = self.im.convert(modebase) - im = im.convert(mode, dither) - except KeyError as e: - msg = "illegal conversion" - raise ValueError(msg) from e - - new_im = self._new(im) - if mode == "P" and palette != Palette.ADAPTIVE: - from . import ImagePalette - - new_im.palette = ImagePalette.ImagePalette("RGB", im.getpalette("RGB")) - if delete_trns: - # crash fail if we leave a bytes transparency in an rgb/l mode. - del new_im.info["transparency"] - if trns is not None: - if new_im.mode == "P": - try: - new_im.info["transparency"] = new_im.palette.getcolor(trns, new_im) - except ValueError as e: - del new_im.info["transparency"] - if str(e) != "cannot allocate more than 256 colors": - # If all 256 colors are in use, - # then there is no need for transparency - warnings.warn( - "Couldn't allocate palette entry for transparency" - ) - else: - new_im.info["transparency"] = trns - return new_im - - def quantize( - self, - colors: int = 256, - method: Quantize | None = None, - kmeans: int = 0, - palette=None, - dither: Dither = Dither.FLOYDSTEINBERG, - ) -> Image: - """ - Convert the image to 'P' mode with the specified number - of colors. - - :param colors: The desired number of colors, <= 256 - :param method: :data:`Quantize.MEDIANCUT` (median cut), - :data:`Quantize.MAXCOVERAGE` (maximum coverage), - :data:`Quantize.FASTOCTREE` (fast octree), - :data:`Quantize.LIBIMAGEQUANT` (libimagequant; check support - using :py:func:`PIL.features.check_feature` with - ``feature="libimagequant"``). - - By default, :data:`Quantize.MEDIANCUT` will be used. - - The exception to this is RGBA images. :data:`Quantize.MEDIANCUT` - and :data:`Quantize.MAXCOVERAGE` do not support RGBA images, so - :data:`Quantize.FASTOCTREE` is used by default instead. - :param kmeans: Integer greater than or equal to zero. - :param palette: Quantize to the palette of given - :py:class:`PIL.Image.Image`. - :param dither: Dithering method, used when converting from - mode "RGB" to "P" or from "RGB" or "L" to "1". - Available methods are :data:`Dither.NONE` or :data:`Dither.FLOYDSTEINBERG` - (default). - :returns: A new image - """ - - self.load() - - if method is None: - # defaults: - method = Quantize.MEDIANCUT - if self.mode == "RGBA": - method = Quantize.FASTOCTREE - - if self.mode == "RGBA" and method not in ( - Quantize.FASTOCTREE, - Quantize.LIBIMAGEQUANT, - ): - # Caller specified an invalid mode. - msg = ( - "Fast Octree (method == 2) and libimagequant (method == 3) " - "are the only valid methods for quantizing RGBA images" - ) - raise ValueError(msg) - - if palette: - # use palette from reference image - palette.load() - if palette.mode != "P": - msg = "bad mode for palette image" - raise ValueError(msg) - if self.mode not in {"RGB", "L"}: - msg = "only RGB or L mode images can be quantized to a palette" - raise ValueError(msg) - im = self.im.convert("P", dither, palette.im) - new_im = self._new(im) - new_im.palette = palette.palette.copy() - return new_im - - if kmeans < 0: - msg = "kmeans must not be negative" - raise ValueError(msg) - - im = self._new(self.im.quantize(colors, method, kmeans)) - - from . import ImagePalette - - mode = im.im.getpalettemode() - palette = im.im.getpalette(mode, mode)[: colors * len(mode)] - im.palette = ImagePalette.ImagePalette(mode, palette) - - return im - - def copy(self) -> Image: - """ - Copies this image. Use this method if you wish to paste things - into an image, but still retain the original. - - :rtype: :py:class:`~PIL.Image.Image` - :returns: An :py:class:`~PIL.Image.Image` object. - """ - self.load() - return self._new(self.im.copy()) - - __copy__ = copy - - def crop(self, box: tuple[int, int, int, int] | None = None) -> Image: - """ - Returns a rectangular region from this image. The box is a - 4-tuple defining the left, upper, right, and lower pixel - coordinate. See :ref:`coordinate-system`. - - Note: Prior to Pillow 3.4.0, this was a lazy operation. - - :param box: The crop rectangle, as a (left, upper, right, lower)-tuple. - :rtype: :py:class:`~PIL.Image.Image` - :returns: An :py:class:`~PIL.Image.Image` object. - """ - - if box is None: - return self.copy() - - if box[2] < box[0]: - msg = "Coordinate 'right' is less than 'left'" - raise ValueError(msg) - elif box[3] < box[1]: - msg = "Coordinate 'lower' is less than 'upper'" - raise ValueError(msg) - - self.load() - return self._new(self._crop(self.im, box)) - - def _crop(self, im, box): - """ - Returns a rectangular region from the core image object im. - - This is equivalent to calling im.crop((x0, y0, x1, y1)), but - includes additional sanity checks. - - :param im: a core image object - :param box: The crop rectangle, as a (left, upper, right, lower)-tuple. - :returns: A core image object. - """ - - x0, y0, x1, y1 = map(int, map(round, box)) - - absolute_values = (abs(x1 - x0), abs(y1 - y0)) - - _decompression_bomb_check(absolute_values) - - return im.crop((x0, y0, x1, y1)) - - def draft( - self, mode: str | None, size: tuple[int, int] - ) -> tuple[str, tuple[int, int, float, float]] | None: - """ - Configures the image file loader so it returns a version of the - image that as closely as possible matches the given mode and - size. For example, you can use this method to convert a color - JPEG to grayscale while loading it. - - If any changes are made, returns a tuple with the chosen ``mode`` and - ``box`` with coordinates of the original image within the altered one. - - Note that this method modifies the :py:class:`~PIL.Image.Image` object - in place. If the image has already been loaded, this method has no - effect. - - Note: This method is not implemented for most images. It is - currently implemented only for JPEG and MPO images. - - :param mode: The requested mode. - :param size: The requested size in pixels, as a 2-tuple: - (width, height). - """ - pass - - def _expand(self, xmargin: int, ymargin: int | None = None) -> Image: - if ymargin is None: - ymargin = xmargin - self.load() - return self._new(self.im.expand(xmargin, ymargin)) - - if TYPE_CHECKING: - from . import ImageFilter - - def filter(self, filter: ImageFilter.Filter | type[ImageFilter.Filter]) -> Image: - """ - Filters this image using the given filter. For a list of - available filters, see the :py:mod:`~PIL.ImageFilter` module. - - :param filter: Filter kernel. - :returns: An :py:class:`~PIL.Image.Image` object.""" - - from . import ImageFilter - - self.load() - - if callable(filter): - filter = filter() - if not hasattr(filter, "filter"): - msg = "filter argument should be ImageFilter.Filter instance or class" - raise TypeError(msg) - - multiband = isinstance(filter, ImageFilter.MultibandFilter) - if self.im.bands == 1 or multiband: - return self._new(filter.filter(self.im)) - - ims = [ - self._new(filter.filter(self.im.getband(c))) for c in range(self.im.bands) - ] - return merge(self.mode, ims) - - def getbands(self) -> tuple[str, ...]: - """ - Returns a tuple containing the name of each band in this image. - For example, ``getbands`` on an RGB image returns ("R", "G", "B"). - - :returns: A tuple containing band names. - :rtype: tuple - """ - return ImageMode.getmode(self.mode).bands - - def getbbox(self, *, alpha_only: bool = True) -> tuple[int, int, int, int] | None: - """ - Calculates the bounding box of the non-zero regions in the - image. - - :param alpha_only: Optional flag, defaulting to ``True``. - If ``True`` and the image has an alpha channel, trim transparent pixels. - Otherwise, trim pixels when all channels are zero. - Keyword-only argument. - :returns: The bounding box is returned as a 4-tuple defining the - left, upper, right, and lower pixel coordinate. See - :ref:`coordinate-system`. If the image is completely empty, this - method returns None. - - """ - - self.load() - return self.im.getbbox(alpha_only) - - def getcolors(self, maxcolors: int = 256): - """ - Returns a list of colors used in this image. - - The colors will be in the image's mode. For example, an RGB image will - return a tuple of (red, green, blue) color values, and a P image will - return the index of the color in the palette. - - :param maxcolors: Maximum number of colors. If this number is - exceeded, this method returns None. The default limit is - 256 colors. - :returns: An unsorted list of (count, pixel) values. - """ - - self.load() - if self.mode in ("1", "L", "P"): - h = self.im.histogram() - out = [(h[i], i) for i in range(256) if h[i]] - if len(out) > maxcolors: - return None - return out - return self.im.getcolors(maxcolors) - - def getdata(self, band: int | None = None): - """ - Returns the contents of this image as a sequence object - containing pixel values. The sequence object is flattened, so - that values for line one follow directly after the values of - line zero, and so on. - - Note that the sequence object returned by this method is an - internal PIL data type, which only supports certain sequence - operations. To convert it to an ordinary sequence (e.g. for - printing), use ``list(im.getdata())``. - - :param band: What band to return. The default is to return - all bands. To return a single band, pass in the index - value (e.g. 0 to get the "R" band from an "RGB" image). - :returns: A sequence-like object. - """ - - self.load() - if band is not None: - return self.im.getband(band) - return self.im # could be abused - - def getextrema(self) -> tuple[float, float] | tuple[tuple[int, int], ...]: - """ - Gets the minimum and maximum pixel values for each band in - the image. - - :returns: For a single-band image, a 2-tuple containing the - minimum and maximum pixel value. For a multi-band image, - a tuple containing one 2-tuple for each band. - """ - - self.load() - if self.im.bands > 1: - return tuple(self.im.getband(i).getextrema() for i in range(self.im.bands)) - return self.im.getextrema() - - def _getxmp(self, xmp_tags): - def get_name(tag): - return re.sub("^{[^}]+}", "", tag) - - def get_value(element): - value = {get_name(k): v for k, v in element.attrib.items()} - children = list(element) - if children: - for child in children: - name = get_name(child.tag) - child_value = get_value(child) - if name in value: - if not isinstance(value[name], list): - value[name] = [value[name]] - value[name].append(child_value) - else: - value[name] = child_value - elif value: - if element.text: - value["text"] = element.text - else: - return element.text - return value - - if ElementTree is None: - warnings.warn("XMP data cannot be read without defusedxml dependency") - return {} - else: - root = ElementTree.fromstring(xmp_tags) - return {get_name(root.tag): get_value(root)} - - def getexif(self) -> Exif: - """ - Gets EXIF data from the image. - - :returns: an :py:class:`~PIL.Image.Exif` object. - """ - if self._exif is None: - self._exif = Exif() - elif self._exif._loaded: - return self._exif - self._exif._loaded = True - - exif_info = self.info.get("exif") - if exif_info is None: - if "Raw profile type exif" in self.info: - exif_info = bytes.fromhex( - "".join(self.info["Raw profile type exif"].split("\n")[3:]) - ) - elif hasattr(self, "tag_v2"): - self._exif.bigtiff = self.tag_v2._bigtiff - self._exif.endian = self.tag_v2._endian - self._exif.load_from_fp(self.fp, self.tag_v2._offset) - if exif_info is not None: - self._exif.load(exif_info) - - # XMP tags - if ExifTags.Base.Orientation not in self._exif: - xmp_tags = self.info.get("XML:com.adobe.xmp") - if xmp_tags: - match = re.search(r'tiff:Orientation(="|>)([0-9])', xmp_tags) - if match: - self._exif[ExifTags.Base.Orientation] = int(match[2]) - - return self._exif - - def _reload_exif(self) -> None: - if self._exif is None or not self._exif._loaded: - return - self._exif._loaded = False - self.getexif() - - def get_child_images(self) -> list[ImageFile.ImageFile]: - child_images = [] - exif = self.getexif() - ifds = [] - if ExifTags.Base.SubIFDs in exif: - subifd_offsets = exif[ExifTags.Base.SubIFDs] - if subifd_offsets: - if not isinstance(subifd_offsets, tuple): - subifd_offsets = (subifd_offsets,) - for subifd_offset in subifd_offsets: - ifds.append((exif._get_ifd_dict(subifd_offset), subifd_offset)) - ifd1 = exif.get_ifd(ExifTags.IFD.IFD1) - if ifd1 and ifd1.get(513): - ifds.append((ifd1, exif._info.next)) - - offset = None - for ifd, ifd_offset in ifds: - current_offset = self.fp.tell() - if offset is None: - offset = current_offset - - fp = self.fp - thumbnail_offset = ifd.get(513) - if thumbnail_offset is not None: - thumbnail_offset += getattr(self, "_exif_offset", 0) - self.fp.seek(thumbnail_offset) - data = self.fp.read(ifd.get(514)) - fp = io.BytesIO(data) - - with open(fp) as im: - if thumbnail_offset is None: - im._frame_pos = [ifd_offset] - im._seek(0) - im.load() - child_images.append(im) - - if offset is not None: - self.fp.seek(offset) - return child_images - - def getim(self): - """ - Returns a capsule that points to the internal image memory. - - :returns: A capsule object. - """ - - self.load() - return self.im.ptr - - def getpalette(self, rawmode: str | None = "RGB") -> list[int] | None: - """ - Returns the image palette as a list. - - :param rawmode: The mode in which to return the palette. ``None`` will - return the palette in its current mode. - - .. versionadded:: 9.1.0 - - :returns: A list of color values [r, g, b, ...], or None if the - image has no palette. - """ - - self.load() - try: - mode = self.im.getpalettemode() - except ValueError: - return None # no palette - if rawmode is None: - rawmode = mode - return list(self.im.getpalette(mode, rawmode)) - - @property - def has_transparency_data(self) -> bool: - """ - Determine if an image has transparency data, whether in the form of an - alpha channel, a palette with an alpha channel, or a "transparency" key - in the info dictionary. - - Note the image might still appear solid, if all of the values shown - within are opaque. - - :returns: A boolean. - """ - return ( - self.mode in ("LA", "La", "PA", "RGBA", "RGBa") - or (self.mode == "P" and self.palette.mode.endswith("A")) - or "transparency" in self.info - ) - - def apply_transparency(self) -> None: - """ - If a P mode image has a "transparency" key in the info dictionary, - remove the key and instead apply the transparency to the palette. - Otherwise, the image is unchanged. - """ - if self.mode != "P" or "transparency" not in self.info: - return - - from . import ImagePalette - - palette = self.getpalette("RGBA") - assert palette is not None - transparency = self.info["transparency"] - if isinstance(transparency, bytes): - for i, alpha in enumerate(transparency): - palette[i * 4 + 3] = alpha - else: - palette[transparency * 4 + 3] = 0 - self.palette = ImagePalette.ImagePalette("RGBA", bytes(palette)) - self.palette.dirty = 1 - - del self.info["transparency"] - - def getpixel(self, xy): - """ - Returns the pixel value at a given position. - - :param xy: The coordinate, given as (x, y). See - :ref:`coordinate-system`. - :returns: The pixel value. If the image is a multi-layer image, - this method returns a tuple. - """ - - self.load() - if self.pyaccess: - return self.pyaccess.getpixel(xy) - return self.im.getpixel(tuple(xy)) - - def getprojection(self) -> tuple[list[int], list[int]]: - """ - Get projection to x and y axes - - :returns: Two sequences, indicating where there are non-zero - pixels along the X-axis and the Y-axis, respectively. - """ - - self.load() - x, y = self.im.getprojection() - return list(x), list(y) - - def histogram(self, mask: Image | None = None, extrema=None) -> list[int]: - """ - Returns a histogram for the image. The histogram is returned as a - list of pixel counts, one for each pixel value in the source - image. Counts are grouped into 256 bins for each band, even if - the image has more than 8 bits per band. If the image has more - than one band, the histograms for all bands are concatenated (for - example, the histogram for an "RGB" image contains 768 values). - - A bilevel image (mode "1") is treated as a grayscale ("L") image - by this method. - - If a mask is provided, the method returns a histogram for those - parts of the image where the mask image is non-zero. The mask - image must have the same size as the image, and be either a - bi-level image (mode "1") or a grayscale image ("L"). - - :param mask: An optional mask. - :param extrema: An optional tuple of manually-specified extrema. - :returns: A list containing pixel counts. - """ - self.load() - if mask: - mask.load() - return self.im.histogram((0, 0), mask.im) - if self.mode in ("I", "F"): - if extrema is None: - extrema = self.getextrema() - return self.im.histogram(extrema) - return self.im.histogram() - - def entropy(self, mask=None, extrema=None): - """ - Calculates and returns the entropy for the image. - - A bilevel image (mode "1") is treated as a grayscale ("L") - image by this method. - - If a mask is provided, the method employs the histogram for - those parts of the image where the mask image is non-zero. - The mask image must have the same size as the image, and be - either a bi-level image (mode "1") or a grayscale image ("L"). - - :param mask: An optional mask. - :param extrema: An optional tuple of manually-specified extrema. - :returns: A float value representing the image entropy - """ - self.load() - if mask: - mask.load() - return self.im.entropy((0, 0), mask.im) - if self.mode in ("I", "F"): - if extrema is None: - extrema = self.getextrema() - return self.im.entropy(extrema) - return self.im.entropy() - - def paste( - self, - im: Image | str | float | tuple[float, ...], - box: tuple[int, int, int, int] | tuple[int, int] | None = None, - mask: Image | None = None, - ) -> None: - """ - Pastes another image into this image. The box argument is either - a 2-tuple giving the upper left corner, a 4-tuple defining the - left, upper, right, and lower pixel coordinate, or None (same as - (0, 0)). See :ref:`coordinate-system`. If a 4-tuple is given, the size - of the pasted image must match the size of the region. - - If the modes don't match, the pasted image is converted to the mode of - this image (see the :py:meth:`~PIL.Image.Image.convert` method for - details). - - Instead of an image, the source can be a integer or tuple - containing pixel values. The method then fills the region - with the given color. When creating RGB images, you can - also use color strings as supported by the ImageColor module. - - If a mask is given, this method updates only the regions - indicated by the mask. You can use either "1", "L", "LA", "RGBA" - or "RGBa" images (if present, the alpha band is used as mask). - Where the mask is 255, the given image is copied as is. Where - the mask is 0, the current value is preserved. Intermediate - values will mix the two images together, including their alpha - channels if they have them. - - See :py:meth:`~PIL.Image.Image.alpha_composite` if you want to - combine images with respect to their alpha channels. - - :param im: Source image or pixel value (integer, float or tuple). - :param box: An optional 4-tuple giving the region to paste into. - If a 2-tuple is used instead, it's treated as the upper left - corner. If omitted or None, the source is pasted into the - upper left corner. - - If an image is given as the second argument and there is no - third, the box defaults to (0, 0), and the second argument - is interpreted as a mask image. - :param mask: An optional mask image. - """ - - if isImageType(box) and mask is None: - # abbreviated paste(im, mask) syntax - mask = box - box = None - - if box is None: - box = (0, 0) - - if len(box) == 2: - # upper left corner given; get size from image or mask - if isImageType(im): - size = im.size - elif isImageType(mask): - size = mask.size - else: - # FIXME: use self.size here? - msg = "cannot determine region size; use 4-item box" - raise ValueError(msg) - box += (box[0] + size[0], box[1] + size[1]) - - if isinstance(im, str): - from . import ImageColor - - im = ImageColor.getcolor(im, self.mode) - - elif isImageType(im): - im.load() - if self.mode != im.mode: - if self.mode != "RGB" or im.mode not in ("LA", "RGBA", "RGBa"): - # should use an adapter for this! - im = im.convert(self.mode) - im = im.im - - self._ensure_mutable() - - if mask: - mask.load() - self.im.paste(im, box, mask.im) - else: - self.im.paste(im, box) - - def alpha_composite(self, im, dest=(0, 0), source=(0, 0)): - """'In-place' analog of Image.alpha_composite. Composites an image - onto this image. - - :param im: image to composite over this one - :param dest: Optional 2 tuple (left, top) specifying the upper - left corner in this (destination) image. - :param source: Optional 2 (left, top) tuple for the upper left - corner in the overlay source image, or 4 tuple (left, top, right, - bottom) for the bounds of the source rectangle - - Performance Note: Not currently implemented in-place in the core layer. - """ - - if not isinstance(source, (list, tuple)): - msg = "Source must be a tuple" - raise ValueError(msg) - if not isinstance(dest, (list, tuple)): - msg = "Destination must be a tuple" - raise ValueError(msg) - if len(source) not in (2, 4): - msg = "Source must be a 2 or 4-tuple" - raise ValueError(msg) - if not len(dest) == 2: - msg = "Destination must be a 2-tuple" - raise ValueError(msg) - if min(source) < 0: - msg = "Source must be non-negative" - raise ValueError(msg) - - if len(source) == 2: - source = source + im.size - - # over image, crop if it's not the whole thing. - if source == (0, 0) + im.size: - overlay = im - else: - overlay = im.crop(source) - - # target for the paste - box = dest + (dest[0] + overlay.width, dest[1] + overlay.height) - - # destination image. don't copy if we're using the whole image. - if box == (0, 0) + self.size: - background = self - else: - background = self.crop(box) - - result = alpha_composite(background, overlay) - self.paste(result, box) - - def point(self, lut, mode: str | None = None) -> Image: - """ - Maps this image through a lookup table or function. - - :param lut: A lookup table, containing 256 (or 65536 if - self.mode=="I" and mode == "L") values per band in the - image. A function can be used instead, it should take a - single argument. The function is called once for each - possible pixel value, and the resulting table is applied to - all bands of the image. - - It may also be an :py:class:`~PIL.Image.ImagePointHandler` - object:: - - class Example(Image.ImagePointHandler): - def point(self, data): - # Return result - :param mode: Output mode (default is same as input). This can only be used if - the source image has mode "L" or "P", and the output has mode "1" or the - source image mode is "I" and the output mode is "L". - :returns: An :py:class:`~PIL.Image.Image` object. - """ - - self.load() - - if isinstance(lut, ImagePointHandler): - return lut.point(self) - - if callable(lut): - # if it isn't a list, it should be a function - if self.mode in ("I", "I;16", "F"): - # check if the function can be used with point_transform - # UNDONE wiredfool -- I think this prevents us from ever doing - # a gamma function point transform on > 8bit images. - scale, offset = _getscaleoffset(lut) - return self._new(self.im.point_transform(scale, offset)) - # for other modes, convert the function to a table - lut = [lut(i) for i in range(256)] * self.im.bands - - if self.mode == "F": - # FIXME: _imaging returns a confusing error message for this case - msg = "point operation not supported for this mode" - raise ValueError(msg) - - if mode != "F": - lut = [round(i) for i in lut] - return self._new(self.im.point(lut, mode)) - - def putalpha(self, alpha): - """ - Adds or replaces the alpha layer in this image. If the image - does not have an alpha layer, it's converted to "LA" or "RGBA". - The new layer must be either "L" or "1". - - :param alpha: The new alpha layer. This can either be an "L" or "1" - image having the same size as this image, or an integer or - other color value. - """ - - self._ensure_mutable() - - if self.mode not in ("LA", "PA", "RGBA"): - # attempt to promote self to a matching alpha mode - try: - mode = getmodebase(self.mode) + "A" - try: - self.im.setmode(mode) - except (AttributeError, ValueError) as e: - # do things the hard way - im = self.im.convert(mode) - if im.mode not in ("LA", "PA", "RGBA"): - msg = "alpha channel could not be added" - raise ValueError(msg) from e # sanity check - self.im = im - self.pyaccess = None - self._mode = self.im.mode - except KeyError as e: - msg = "illegal image mode" - raise ValueError(msg) from e - - if self.mode in ("LA", "PA"): - band = 1 - else: - band = 3 - - if isImageType(alpha): - # alpha layer - if alpha.mode not in ("1", "L"): - msg = "illegal image mode" - raise ValueError(msg) - alpha.load() - if alpha.mode == "1": - alpha = alpha.convert("L") - else: - # constant alpha - try: - self.im.fillband(band, alpha) - except (AttributeError, ValueError): - # do things the hard way - alpha = new("L", self.size, alpha) - else: - return - - self.im.putband(alpha.im, band) - - def putdata( - self, data: Sequence[float], scale: float = 1.0, offset: float = 0.0 - ) -> None: - """ - Copies pixel data from a flattened sequence object into the image. The - values should start at the upper left corner (0, 0), continue to the - end of the line, followed directly by the first value of the second - line, and so on. Data will be read until either the image or the - sequence ends. The scale and offset values are used to adjust the - sequence values: **pixel = value*scale + offset**. - - :param data: A flattened sequence object. - :param scale: An optional scale value. The default is 1.0. - :param offset: An optional offset value. The default is 0.0. - """ - - self._ensure_mutable() - - self.im.putdata(data, scale, offset) - - def putpalette(self, data, rawmode="RGB") -> None: - """ - Attaches a palette to this image. The image must be a "P", "PA", "L" - or "LA" image. - - The palette sequence must contain at most 256 colors, made up of one - integer value for each channel in the raw mode. - For example, if the raw mode is "RGB", then it can contain at most 768 - values, made up of red, green and blue values for the corresponding pixel - index in the 256 colors. - If the raw mode is "RGBA", then it can contain at most 1024 values, - containing red, green, blue and alpha values. - - Alternatively, an 8-bit string may be used instead of an integer sequence. - - :param data: A palette sequence (either a list or a string). - :param rawmode: The raw mode of the palette. Either "RGB", "RGBA", or a mode - that can be transformed to "RGB" or "RGBA" (e.g. "R", "BGR;15", "RGBA;L"). - """ - from . import ImagePalette - - if self.mode not in ("L", "LA", "P", "PA"): - msg = "illegal image mode" - raise ValueError(msg) - if isinstance(data, ImagePalette.ImagePalette): - palette = ImagePalette.raw(data.rawmode, data.palette) - else: - if not isinstance(data, bytes): - data = bytes(data) - palette = ImagePalette.raw(rawmode, data) - self._mode = "PA" if "A" in self.mode else "P" - self.palette = palette - self.palette.mode = "RGB" - self.load() # install new palette - - def putpixel(self, xy, value): - """ - Modifies the pixel at the given position. The color is given as - a single numerical value for single-band images, and a tuple for - multi-band images. In addition to this, RGB and RGBA tuples are - accepted for P and PA images. - - Note that this method is relatively slow. For more extensive changes, - use :py:meth:`~PIL.Image.Image.paste` or the :py:mod:`~PIL.ImageDraw` - module instead. - - See: - - * :py:meth:`~PIL.Image.Image.paste` - * :py:meth:`~PIL.Image.Image.putdata` - * :py:mod:`~PIL.ImageDraw` - - :param xy: The pixel coordinate, given as (x, y). See - :ref:`coordinate-system`. - :param value: The pixel value. - """ - - if self.readonly: - self._copy() - self.load() - - if self.pyaccess: - return self.pyaccess.putpixel(xy, value) - - if ( - self.mode in ("P", "PA") - and isinstance(value, (list, tuple)) - and len(value) in [3, 4] - ): - # RGB or RGBA value for a P or PA image - if self.mode == "PA": - alpha = value[3] if len(value) == 4 else 255 - value = value[:3] - value = self.palette.getcolor(value, self) - if self.mode == "PA": - value = (value, alpha) - return self.im.putpixel(xy, value) - - def remap_palette(self, dest_map, source_palette=None): - """ - Rewrites the image to reorder the palette. - - :param dest_map: A list of indexes into the original palette. - e.g. ``[1,0]`` would swap a two item palette, and ``list(range(256))`` - is the identity transform. - :param source_palette: Bytes or None. - :returns: An :py:class:`~PIL.Image.Image` object. - - """ - from . import ImagePalette - - if self.mode not in ("L", "P"): - msg = "illegal image mode" - raise ValueError(msg) - - bands = 3 - palette_mode = "RGB" - if source_palette is None: - if self.mode == "P": - self.load() - palette_mode = self.im.getpalettemode() - if palette_mode == "RGBA": - bands = 4 - source_palette = self.im.getpalette(palette_mode, palette_mode) - else: # L-mode - source_palette = bytearray(i // 3 for i in range(768)) - - palette_bytes = b"" - new_positions = [0] * 256 - - # pick only the used colors from the palette - for i, oldPosition in enumerate(dest_map): - palette_bytes += source_palette[ - oldPosition * bands : oldPosition * bands + bands - ] - new_positions[oldPosition] = i - - # replace the palette color id of all pixel with the new id - - # Palette images are [0..255], mapped through a 1 or 3 - # byte/color map. We need to remap the whole image - # from palette 1 to palette 2. New_positions is - # an array of indexes into palette 1. Palette 2 is - # palette 1 with any holes removed. - - # We're going to leverage the convert mechanism to use the - # C code to remap the image from palette 1 to palette 2, - # by forcing the source image into 'L' mode and adding a - # mapping 'L' mode palette, then converting back to 'L' - # sans palette thus converting the image bytes, then - # assigning the optimized RGB palette. - - # perf reference, 9500x4000 gif, w/~135 colors - # 14 sec prepatch, 1 sec postpatch with optimization forced. - - mapping_palette = bytearray(new_positions) - - m_im = self.copy() - m_im._mode = "P" - - m_im.palette = ImagePalette.ImagePalette( - palette_mode, palette=mapping_palette * bands - ) - # possibly set palette dirty, then - # m_im.putpalette(mapping_palette, 'L') # converts to 'P' - # or just force it. - # UNDONE -- this is part of the general issue with palettes - m_im.im.putpalette(palette_mode + ";L", m_im.palette.tobytes()) - - m_im = m_im.convert("L") - - m_im.putpalette(palette_bytes, palette_mode) - m_im.palette = ImagePalette.ImagePalette(palette_mode, palette=palette_bytes) - - if "transparency" in self.info: - try: - m_im.info["transparency"] = dest_map.index(self.info["transparency"]) - except ValueError: - if "transparency" in m_im.info: - del m_im.info["transparency"] - - return m_im - - def _get_safe_box(self, size, resample, box): - """Expands the box so it includes adjacent pixels - that may be used by resampling with the given resampling filter. - """ - filter_support = _filters_support[resample] - 0.5 - scale_x = (box[2] - box[0]) / size[0] - scale_y = (box[3] - box[1]) / size[1] - support_x = filter_support * scale_x - support_y = filter_support * scale_y - - return ( - max(0, int(box[0] - support_x)), - max(0, int(box[1] - support_y)), - min(self.size[0], math.ceil(box[2] + support_x)), - min(self.size[1], math.ceil(box[3] + support_y)), - ) - - def resize( - self, - size: tuple[int, int], - resample: int | None = None, - box: tuple[float, float, float, float] | None = None, - reducing_gap: float | None = None, - ) -> Image: - """ - Returns a resized copy of this image. - - :param size: The requested size in pixels, as a 2-tuple: - (width, height). - :param resample: An optional resampling filter. This can be - one of :py:data:`Resampling.NEAREST`, :py:data:`Resampling.BOX`, - :py:data:`Resampling.BILINEAR`, :py:data:`Resampling.HAMMING`, - :py:data:`Resampling.BICUBIC` or :py:data:`Resampling.LANCZOS`. - If the image has mode "1" or "P", it is always set to - :py:data:`Resampling.NEAREST`. If the image mode specifies a number - of bits, such as "I;16", then the default filter is - :py:data:`Resampling.NEAREST`. Otherwise, the default filter is - :py:data:`Resampling.BICUBIC`. See: :ref:`concept-filters`. - :param box: An optional 4-tuple of floats providing - the source image region to be scaled. - The values must be within (0, 0, width, height) rectangle. - If omitted or None, the entire source is used. - :param reducing_gap: Apply optimization by resizing the image - in two steps. First, reducing the image by integer times - using :py:meth:`~PIL.Image.Image.reduce`. - Second, resizing using regular resampling. The last step - changes size no less than by ``reducing_gap`` times. - ``reducing_gap`` may be None (no first step is performed) - or should be greater than 1.0. The bigger ``reducing_gap``, - the closer the result to the fair resampling. - The smaller ``reducing_gap``, the faster resizing. - With ``reducing_gap`` greater or equal to 3.0, the result is - indistinguishable from fair resampling in most cases. - The default value is None (no optimization). - :returns: An :py:class:`~PIL.Image.Image` object. - """ - - if resample is None: - type_special = ";" in self.mode - resample = Resampling.NEAREST if type_special else Resampling.BICUBIC - elif resample not in ( - Resampling.NEAREST, - Resampling.BILINEAR, - Resampling.BICUBIC, - Resampling.LANCZOS, - Resampling.BOX, - Resampling.HAMMING, - ): - msg = f"Unknown resampling filter ({resample})." - - filters = [ - f"{filter[1]} ({filter[0]})" - for filter in ( - (Resampling.NEAREST, "Image.Resampling.NEAREST"), - (Resampling.LANCZOS, "Image.Resampling.LANCZOS"), - (Resampling.BILINEAR, "Image.Resampling.BILINEAR"), - (Resampling.BICUBIC, "Image.Resampling.BICUBIC"), - (Resampling.BOX, "Image.Resampling.BOX"), - (Resampling.HAMMING, "Image.Resampling.HAMMING"), - ) - ] - msg += f" Use {', '.join(filters[:-1])} or {filters[-1]}" - raise ValueError(msg) - - if reducing_gap is not None and reducing_gap < 1.0: - msg = "reducing_gap must be 1.0 or greater" - raise ValueError(msg) - - self.load() - if box is None: - box = (0, 0) + self.size - - if self.size == size and box == (0, 0) + self.size: - return self.copy() - - if self.mode in ("1", "P"): - resample = Resampling.NEAREST - - if self.mode in ["LA", "RGBA"] and resample != Resampling.NEAREST: - im = self.convert({"LA": "La", "RGBA": "RGBa"}[self.mode]) - im = im.resize(size, resample, box) - return im.convert(self.mode) - - self.load() - - if reducing_gap is not None and resample != Resampling.NEAREST: - factor_x = int((box[2] - box[0]) / size[0] / reducing_gap) or 1 - factor_y = int((box[3] - box[1]) / size[1] / reducing_gap) or 1 - if factor_x > 1 or factor_y > 1: - reduce_box = self._get_safe_box(size, resample, box) - factor = (factor_x, factor_y) - self = ( - self.reduce(factor, box=reduce_box) - if callable(self.reduce) - else Image.reduce(self, factor, box=reduce_box) - ) - box = ( - (box[0] - reduce_box[0]) / factor_x, - (box[1] - reduce_box[1]) / factor_y, - (box[2] - reduce_box[0]) / factor_x, - (box[3] - reduce_box[1]) / factor_y, - ) - - return self._new(self.im.resize(size, resample, box)) - - def reduce( - self, - factor: int | tuple[int, int], - box: tuple[int, int, int, int] | None = None, - ) -> Image: - """ - Returns a copy of the image reduced ``factor`` times. - If the size of the image is not dividable by ``factor``, - the resulting size will be rounded up. - - :param factor: A greater than 0 integer or tuple of two integers - for width and height separately. - :param box: An optional 4-tuple of ints providing - the source image region to be reduced. - The values must be within ``(0, 0, width, height)`` rectangle. - If omitted or ``None``, the entire source is used. - """ - if not isinstance(factor, (list, tuple)): - factor = (factor, factor) - - if box is None: - box = (0, 0) + self.size - - if factor == (1, 1) and box == (0, 0) + self.size: - return self.copy() - - if self.mode in ["LA", "RGBA"]: - im = self.convert({"LA": "La", "RGBA": "RGBa"}[self.mode]) - im = im.reduce(factor, box) - return im.convert(self.mode) - - self.load() - - return self._new(self.im.reduce(factor, box)) - - def rotate( - self, - angle: float, - resample: Resampling = Resampling.NEAREST, - expand: int | bool = False, - center: tuple[int, int] | None = None, - translate: tuple[int, int] | None = None, - fillcolor: float | tuple[float, ...] | str | None = None, - ) -> Image: - """ - Returns a rotated copy of this image. This method returns a - copy of this image, rotated the given number of degrees counter - clockwise around its centre. - - :param angle: In degrees counter clockwise. - :param resample: An optional resampling filter. This can be - one of :py:data:`Resampling.NEAREST` (use nearest neighbour), - :py:data:`Resampling.BILINEAR` (linear interpolation in a 2x2 - environment), or :py:data:`Resampling.BICUBIC` (cubic spline - interpolation in a 4x4 environment). If omitted, or if the image has - mode "1" or "P", it is set to :py:data:`Resampling.NEAREST`. - See :ref:`concept-filters`. - :param expand: Optional expansion flag. If true, expands the output - image to make it large enough to hold the entire rotated image. - If false or omitted, make the output image the same size as the - input image. Note that the expand flag assumes rotation around - the center and no translation. - :param center: Optional center of rotation (a 2-tuple). Origin is - the upper left corner. Default is the center of the image. - :param translate: An optional post-rotate translation (a 2-tuple). - :param fillcolor: An optional color for area outside the rotated image. - :returns: An :py:class:`~PIL.Image.Image` object. - """ - - angle = angle % 360.0 - - # Fast paths regardless of filter, as long as we're not - # translating or changing the center. - if not (center or translate): - if angle == 0: - return self.copy() - if angle == 180: - return self.transpose(Transpose.ROTATE_180) - if angle in (90, 270) and (expand or self.width == self.height): - return self.transpose( - Transpose.ROTATE_90 if angle == 90 else Transpose.ROTATE_270 - ) - - # Calculate the affine matrix. Note that this is the reverse - # transformation (from destination image to source) because we - # want to interpolate the (discrete) destination pixel from - # the local area around the (floating) source pixel. - - # The matrix we actually want (note that it operates from the right): - # (1, 0, tx) (1, 0, cx) ( cos a, sin a, 0) (1, 0, -cx) - # (0, 1, ty) * (0, 1, cy) * (-sin a, cos a, 0) * (0, 1, -cy) - # (0, 0, 1) (0, 0, 1) ( 0, 0, 1) (0, 0, 1) - - # The reverse matrix is thus: - # (1, 0, cx) ( cos -a, sin -a, 0) (1, 0, -cx) (1, 0, -tx) - # (0, 1, cy) * (-sin -a, cos -a, 0) * (0, 1, -cy) * (0, 1, -ty) - # (0, 0, 1) ( 0, 0, 1) (0, 0, 1) (0, 0, 1) - - # In any case, the final translation may be updated at the end to - # compensate for the expand flag. - - w, h = self.size - - if translate is None: - post_trans = (0, 0) - else: - post_trans = translate - if center is None: - # FIXME These should be rounded to ints? - rotn_center = (w / 2.0, h / 2.0) - else: - rotn_center = center - - angle = -math.radians(angle) - matrix = [ - round(math.cos(angle), 15), - round(math.sin(angle), 15), - 0.0, - round(-math.sin(angle), 15), - round(math.cos(angle), 15), - 0.0, - ] - - def transform(x, y, matrix): - (a, b, c, d, e, f) = matrix - return a * x + b * y + c, d * x + e * y + f - - matrix[2], matrix[5] = transform( - -rotn_center[0] - post_trans[0], -rotn_center[1] - post_trans[1], matrix - ) - matrix[2] += rotn_center[0] - matrix[5] += rotn_center[1] - - if expand: - # calculate output size - xx = [] - yy = [] - for x, y in ((0, 0), (w, 0), (w, h), (0, h)): - x, y = transform(x, y, matrix) - xx.append(x) - yy.append(y) - nw = math.ceil(max(xx)) - math.floor(min(xx)) - nh = math.ceil(max(yy)) - math.floor(min(yy)) - - # We multiply a translation matrix from the right. Because of its - # special form, this is the same as taking the image of the - # translation vector as new translation vector. - matrix[2], matrix[5] = transform(-(nw - w) / 2.0, -(nh - h) / 2.0, matrix) - w, h = nw, nh - - return self.transform( - (w, h), Transform.AFFINE, matrix, resample, fillcolor=fillcolor - ) - - def save( - self, fp: StrOrBytesPath | IO[bytes], format: str | None = None, **params: Any - ) -> None: - """ - Saves this image under the given filename. If no format is - specified, the format to use is determined from the filename - extension, if possible. - - Keyword options can be used to provide additional instructions - to the writer. If a writer doesn't recognise an option, it is - silently ignored. The available options are described in the - :doc:`image format documentation - <../handbook/image-file-formats>` for each writer. - - You can use a file object instead of a filename. In this case, - you must always specify the format. The file object must - implement the ``seek``, ``tell``, and ``write`` - methods, and be opened in binary mode. - - :param fp: A filename (string), os.PathLike object or file object. - :param format: Optional format override. If omitted, the - 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. - :returns: None - :exception ValueError: If the output format could not be determined - from the file name. Use the format option to solve this. - :exception OSError: If the file could not be written. The file - may have been created, and may contain partial data. - """ - - filename: str | bytes = "" - open_fp = False - if is_path(fp): - filename = os.path.realpath(os.fspath(fp)) - open_fp = True - elif fp == sys.stdout: - try: - fp = sys.stdout.buffer - except AttributeError: - pass - if not filename and hasattr(fp, "name") and is_path(fp.name): - # only set the name for metadata purposes - filename = 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, ...] = () - - preinit() - - filename_ext = os.path.splitext(filename)[1].lower() - ext = filename_ext.decode() if isinstance(filename_ext, bytes) else filename_ext - - if not format: - if ext not in EXTENSION: - init() - try: - format = EXTENSION[ext] - except KeyError as e: - msg = f"unknown file extension: {ext}" - raise ValueError(msg) from e - - if format.upper() not in SAVE: - init() - if save_all: - save_handler = SAVE_ALL[format.upper()] - else: - save_handler = SAVE[format.upper()] - - created = False - if open_fp: - created = not os.path.exists(filename) - if params.get("append", False): - # Open also for reading ("+"), because TIFF save_all - # writer needs to go back and edit the written data. - fp = builtins.open(filename, "r+b") - else: - fp = builtins.open(filename, "w+b") - else: - fp = cast(IO[bytes], fp) - - try: - save_handler(self, fp, filename) - except Exception: - if open_fp: - fp.close() - if created: - try: - os.remove(filename) - except PermissionError: - pass - raise - if open_fp: - fp.close() - - def seek(self, frame: int) -> None: - """ - Seeks to the given frame in this sequence file. If you seek - beyond the end of the sequence, the method raises an - ``EOFError`` exception. When a sequence file is opened, the - library automatically seeks to frame 0. - - See :py:meth:`~PIL.Image.Image.tell`. - - If defined, :attr:`~PIL.Image.Image.n_frames` refers to the - number of available frames. - - :param frame: Frame number, starting at 0. - :exception EOFError: If the call attempts to seek beyond the end - of the sequence. - """ - - # overridden by file handlers - if frame != 0: - msg = "no more images in file" - raise EOFError(msg) - - def show(self, title: str | None = None) -> None: - """ - Displays this image. This method is mainly intended for debugging purposes. - - This method calls :py:func:`PIL.ImageShow.show` internally. You can use - :py:func:`PIL.ImageShow.register` to override its default behaviour. - - The image is first saved to a temporary file. By default, it will be in - PNG format. - - On Unix, the image is then opened using the **xdg-open**, **display**, - **gm**, **eog** or **xv** utility, depending on which one can be found. - - On macOS, the image is opened with the native Preview application. - - On Windows, the image is opened with the standard PNG display utility. - - :param title: Optional title to use for the image window, where possible. - """ - - _show(self, title=title) - - def split(self) -> tuple[Image, ...]: - """ - Split this image into individual bands. This method returns a - tuple of individual image bands from an image. For example, - splitting an "RGB" image creates three new images each - containing a copy of one of the original bands (red, green, - blue). - - If you need only one band, :py:meth:`~PIL.Image.Image.getchannel` - method can be more convenient and faster. - - :returns: A tuple containing bands. - """ - - self.load() - if self.im.bands == 1: - return (self.copy(),) - return tuple(map(self._new, self.im.split())) - - def getchannel(self, channel: int | str) -> Image: - """ - Returns an image containing a single channel of the source image. - - :param channel: What channel to return. Could be index - (0 for "R" channel of "RGB") or channel name - ("A" for alpha channel of "RGBA"). - :returns: An image in "L" mode. - - .. versionadded:: 4.3.0 - """ - self.load() - - if isinstance(channel, str): - try: - channel = self.getbands().index(channel) - except ValueError as e: - msg = f'The image has no channel "{channel}"' - raise ValueError(msg) from e - - return self._new(self.im.getband(channel)) - - def tell(self) -> int: - """ - Returns the current frame number. See :py:meth:`~PIL.Image.Image.seek`. - - If defined, :attr:`~PIL.Image.Image.n_frames` refers to the - number of available frames. - - :returns: Frame number, starting with 0. - """ - return 0 - - def thumbnail( - self, - size: tuple[float, float], - resample: Resampling = Resampling.BICUBIC, - reducing_gap: float = 2.0, - ) -> None: - """ - Make this image into a thumbnail. This method modifies the - image to contain a thumbnail version of itself, no larger than - the given size. This method calculates an appropriate thumbnail - size to preserve the aspect of the image, calls the - :py:meth:`~PIL.Image.Image.draft` method to configure the file reader - (where applicable), and finally resizes the image. - - Note that this function modifies the :py:class:`~PIL.Image.Image` - object in place. If you need to use the full resolution image as well, - apply this method to a :py:meth:`~PIL.Image.Image.copy` of the original - image. - - :param size: The requested size in pixels, as a 2-tuple: - (width, height). - :param resample: Optional resampling filter. This can be one - of :py:data:`Resampling.NEAREST`, :py:data:`Resampling.BOX`, - :py:data:`Resampling.BILINEAR`, :py:data:`Resampling.HAMMING`, - :py:data:`Resampling.BICUBIC` or :py:data:`Resampling.LANCZOS`. - If omitted, it defaults to :py:data:`Resampling.BICUBIC`. - (was :py:data:`Resampling.NEAREST` prior to version 2.5.0). - See: :ref:`concept-filters`. - :param reducing_gap: Apply optimization by resizing the image - in two steps. First, reducing the image by integer times - using :py:meth:`~PIL.Image.Image.reduce` or - :py:meth:`~PIL.Image.Image.draft` for JPEG images. - Second, resizing using regular resampling. The last step - changes size no less than by ``reducing_gap`` times. - ``reducing_gap`` may be None (no first step is performed) - or should be greater than 1.0. The bigger ``reducing_gap``, - the closer the result to the fair resampling. - The smaller ``reducing_gap``, the faster resizing. - With ``reducing_gap`` greater or equal to 3.0, the result is - indistinguishable from fair resampling in most cases. - The default value is 2.0 (very close to fair resampling - while still being faster in many cases). - :returns: None - """ - - provided_size = tuple(map(math.floor, size)) - - def preserve_aspect_ratio() -> tuple[int, int] | None: - def round_aspect(number, key): - return max(min(math.floor(number), math.ceil(number), key=key), 1) - - x, y = provided_size - if x >= self.width and y >= self.height: - return None - - aspect = self.width / self.height - if x / y >= aspect: - x = round_aspect(y * aspect, key=lambda n: abs(aspect - n / y)) - else: - y = round_aspect( - x / aspect, key=lambda n: 0 if n == 0 else abs(aspect - x / n) - ) - return x, y - - box = None - if reducing_gap is not None: - preserved_size = preserve_aspect_ratio() - if preserved_size is None: - return - size = preserved_size - - res = self.draft( - None, (int(size[0] * reducing_gap), int(size[1] * reducing_gap)) - ) - if res is not None: - box = res[1] - if box is None: - self.load() - - # load() may have changed the size of the image - preserved_size = preserve_aspect_ratio() - if preserved_size is None: - return - size = preserved_size - - if self.size != size: - im = self.resize(size, resample, box=box, reducing_gap=reducing_gap) - - self.im = im.im - self._size = size - self._mode = self.im.mode - - self.readonly = 0 - self.pyaccess = None - - # FIXME: the different transform methods need further explanation - # instead of bloating the method docs, add a separate chapter. - def transform( - self, - size: tuple[int, int], - method: Transform | ImageTransformHandler | SupportsGetData, - data: Sequence[Any] | None = None, - resample: int = Resampling.NEAREST, - fill: int = 1, - fillcolor: float | tuple[float, ...] | str | None = None, - ) -> Image: - """ - Transforms this image. This method creates a new image with the - given size, and the same mode as the original, and copies data - to the new image using the given transform. - - :param size: The output size in pixels, as a 2-tuple: - (width, height). - :param method: The transformation method. This is one of - :py:data:`Transform.EXTENT` (cut out a rectangular subregion), - :py:data:`Transform.AFFINE` (affine transform), - :py:data:`Transform.PERSPECTIVE` (perspective transform), - :py:data:`Transform.QUAD` (map a quadrilateral to a rectangle), or - :py:data:`Transform.MESH` (map a number of source quadrilaterals - in one operation). - - It may also be an :py:class:`~PIL.Image.ImageTransformHandler` - object:: - - class Example(Image.ImageTransformHandler): - def transform(self, size, data, resample, fill=1): - # Return result - - Implementations of :py:class:`~PIL.Image.ImageTransformHandler` - for some of the :py:class:`Transform` methods are provided - in :py:mod:`~PIL.ImageTransform`. - - It may also be an object with a ``method.getdata`` method - that returns a tuple supplying new ``method`` and ``data`` values:: - - class Example: - def getdata(self): - method = Image.Transform.EXTENT - data = (0, 0, 100, 100) - return method, data - :param data: Extra data to the transformation method. - :param resample: Optional resampling filter. It can be one of - :py:data:`Resampling.NEAREST` (use nearest neighbour), - :py:data:`Resampling.BILINEAR` (linear interpolation in a 2x2 - environment), or :py:data:`Resampling.BICUBIC` (cubic spline - interpolation in a 4x4 environment). If omitted, or if the image - has mode "1" or "P", it is set to :py:data:`Resampling.NEAREST`. - See: :ref:`concept-filters`. - :param fill: If ``method`` is an - :py:class:`~PIL.Image.ImageTransformHandler` object, this is one of - the arguments passed to it. Otherwise, it is unused. - :param fillcolor: Optional fill color for the area outside the - transform in the output image. - :returns: An :py:class:`~PIL.Image.Image` object. - """ - - if self.mode in ("LA", "RGBA") and resample != Resampling.NEAREST: - return ( - self.convert({"LA": "La", "RGBA": "RGBa"}[self.mode]) - .transform(size, method, data, resample, fill, fillcolor) - .convert(self.mode) - ) - - if isinstance(method, ImageTransformHandler): - return method.transform(size, self, resample=resample, fill=fill) - - if hasattr(method, "getdata"): - # compatibility w. old-style transform objects - method, data = method.getdata() - - if data is None: - msg = "missing method data" - raise ValueError(msg) - - im = new(self.mode, size, fillcolor) - if self.mode == "P" and self.palette: - im.palette = self.palette.copy() - im.info = self.info.copy() - if method == Transform.MESH: - # list of quads - for box, quad in data: - im.__transformer( - box, self, Transform.QUAD, quad, resample, fillcolor is None - ) - else: - im.__transformer( - (0, 0) + size, self, method, data, resample, fillcolor is None - ) - - return im - - def __transformer( - self, box, image, method, data, resample=Resampling.NEAREST, fill=1 - ): - w = box[2] - box[0] - h = box[3] - box[1] - - if method == Transform.AFFINE: - data = data[:6] - - elif method == Transform.EXTENT: - # convert extent to an affine transform - x0, y0, x1, y1 = data - xs = (x1 - x0) / w - ys = (y1 - y0) / h - method = Transform.AFFINE - data = (xs, 0, x0, 0, ys, y0) - - elif method == Transform.PERSPECTIVE: - data = data[:8] - - elif method == Transform.QUAD: - # quadrilateral warp. data specifies the four corners - # given as NW, SW, SE, and NE. - nw = data[:2] - sw = data[2:4] - se = data[4:6] - ne = data[6:8] - x0, y0 = nw - As = 1.0 / w - At = 1.0 / h - data = ( - x0, - (ne[0] - x0) * As, - (sw[0] - x0) * At, - (se[0] - sw[0] - ne[0] + x0) * As * At, - y0, - (ne[1] - y0) * As, - (sw[1] - y0) * At, - (se[1] - sw[1] - ne[1] + y0) * As * At, - ) - - else: - msg = "unknown transformation method" - raise ValueError(msg) - - if resample not in ( - Resampling.NEAREST, - Resampling.BILINEAR, - Resampling.BICUBIC, - ): - if resample in (Resampling.BOX, Resampling.HAMMING, Resampling.LANCZOS): - msg = { - Resampling.BOX: "Image.Resampling.BOX", - Resampling.HAMMING: "Image.Resampling.HAMMING", - Resampling.LANCZOS: "Image.Resampling.LANCZOS", - }[resample] + f" ({resample}) cannot be used." - else: - msg = f"Unknown resampling filter ({resample})." - - filters = [ - f"{filter[1]} ({filter[0]})" - for filter in ( - (Resampling.NEAREST, "Image.Resampling.NEAREST"), - (Resampling.BILINEAR, "Image.Resampling.BILINEAR"), - (Resampling.BICUBIC, "Image.Resampling.BICUBIC"), - ) - ] - msg += f" Use {', '.join(filters[:-1])} or {filters[-1]}" - raise ValueError(msg) - - image.load() - - self.load() - - if image.mode in ("1", "P"): - resample = Resampling.NEAREST - - self.im.transform(box, image.im, method, data, resample, fill) - - def transpose(self, method: Transpose) -> Image: - """ - Transpose image (flip or rotate in 90 degree steps) - - :param method: One of :py:data:`Transpose.FLIP_LEFT_RIGHT`, - :py:data:`Transpose.FLIP_TOP_BOTTOM`, :py:data:`Transpose.ROTATE_90`, - :py:data:`Transpose.ROTATE_180`, :py:data:`Transpose.ROTATE_270`, - :py:data:`Transpose.TRANSPOSE` or :py:data:`Transpose.TRANSVERSE`. - :returns: Returns a flipped or rotated copy of this image. - """ - - self.load() - return self._new(self.im.transpose(method)) - - def effect_spread(self, distance: int) -> Image: - """ - Randomly spread pixels in an image. - - :param distance: Distance to spread pixels. - """ - self.load() - return self._new(self.im.effect_spread(distance)) - - def toqimage(self): - """Returns a QImage copy of this image""" - from . import ImageQt - - if not ImageQt.qt_is_installed: - msg = "Qt bindings are not installed" - raise ImportError(msg) - return ImageQt.toqimage(self) - - def toqpixmap(self): - """Returns a QPixmap copy of this image""" - from . import ImageQt - - if not ImageQt.qt_is_installed: - msg = "Qt bindings are not installed" - raise ImportError(msg) - return ImageQt.toqpixmap(self) - - -# -------------------------------------------------------------------- -# Abstract handlers. - - -class ImagePointHandler: - """ - Used as a mixin by point transforms - (for use with :py:meth:`~PIL.Image.Image.point`) - """ - - @abc.abstractmethod - def point(self, im: Image) -> Image: - pass - - -class ImageTransformHandler: - """ - Used as a mixin by geometry transforms - (for use with :py:meth:`~PIL.Image.Image.transform`) - """ - - @abc.abstractmethod - def transform( - self, - size: tuple[int, int], - image: Image, - **options: Any, - ) -> Image: - pass - - -# -------------------------------------------------------------------- -# Factories - -# -# Debugging - - -def _wedge() -> Image: - """Create grayscale wedge (for debugging only)""" - - return Image()._new(core.wedge("L")) - - -def _check_size(size): - """ - Common check to enforce type and sanity check on size tuples - - :param size: Should be a 2 tuple of (width, height) - :returns: True, or raises a ValueError - """ - - if not isinstance(size, (list, tuple)): - msg = "Size must be a tuple" - raise ValueError(msg) - if len(size) != 2: - msg = "Size must be a tuple of length 2" - raise ValueError(msg) - if size[0] < 0 or size[1] < 0: - msg = "Width and height must be >= 0" - raise ValueError(msg) - - return True - - -def new( - mode: str, size: tuple[int, int], color: float | tuple[float, ...] | str | None = 0 -) -> Image: - """ - Creates a new image with the given mode and size. - - :param mode: The mode to use for the new image. See: - :ref:`concept-modes`. - :param size: A 2-tuple, containing (width, height) in pixels. - :param color: What color to use for the image. Default is black. - If given, this should be a single integer or floating point value - for single-band modes, and a tuple for multi-band modes (one value - per band). When creating RGB or HSV images, you can also use color - strings as supported by the ImageColor module. If the color is - None, the image is not initialised. - :returns: An :py:class:`~PIL.Image.Image` object. - """ - - if mode in ("BGR;15", "BGR;16", "BGR;24"): - deprecate(mode, 12) - - _check_size(size) - - if color is None: - # don't initialize - return Image()._new(core.new(mode, size)) - - if isinstance(color, str): - # css3-style specifier - - from . import ImageColor - - color = ImageColor.getcolor(color, mode) - - im = Image() - if ( - mode == "P" - and isinstance(color, (list, tuple)) - and all(isinstance(i, int) for i in color) - ): - color_ints: tuple[int, ...] = cast(Tuple[int, ...], tuple(color)) - if len(color_ints) == 3 or len(color_ints) == 4: - # RGB or RGBA value for a P image - from . import ImagePalette - - im.palette = ImagePalette.ImagePalette() - color = im.palette.getcolor(color_ints) - return im._new(core.fill(mode, size, color)) - - -def frombytes(mode, size, data, decoder_name: str = "raw", *args) -> Image: - """ - Creates a copy of an image memory from pixel data in a buffer. - - In its simplest form, this function takes three arguments - (mode, size, and unpacked pixel data). - - You can also use any pixel decoder supported by PIL. For more - information on available decoders, see the section - :ref:`Writing Your Own File Codec `. - - Note that this function decodes pixel data only, not entire images. - If you have an entire image in a string, wrap it in a - :py:class:`~io.BytesIO` object, and use :py:func:`~PIL.Image.open` to load - it. - - :param mode: The image mode. See: :ref:`concept-modes`. - :param size: The image size. - :param data: A byte buffer containing raw data for the given mode. - :param decoder_name: What decoder to use. - :param args: Additional parameters for the given decoder. - :returns: An :py:class:`~PIL.Image.Image` object. - """ - - _check_size(size) - - im = new(mode, size) - if im.width != 0 and im.height != 0: - # may pass tuple instead of argument list - if len(args) == 1 and isinstance(args[0], tuple): - args = args[0] - - if decoder_name == "raw" and args == (): - args = mode - - im.frombytes(data, decoder_name, args) - return im - - -def frombuffer(mode: str, size, data, decoder_name: str = "raw", *args) -> Image: - """ - Creates an image memory referencing pixel data in a byte buffer. - - This function is similar to :py:func:`~PIL.Image.frombytes`, but uses data - in the byte buffer, where possible. This means that changes to the - original buffer object are reflected in this image). Not all modes can - share memory; supported modes include "L", "RGBX", "RGBA", and "CMYK". - - Note that this function decodes pixel data only, not entire images. - If you have an entire image file in a string, wrap it in a - :py:class:`~io.BytesIO` object, and use :py:func:`~PIL.Image.open` to load it. - - The default parameters used for the "raw" decoder differs from that used for - :py:func:`~PIL.Image.frombytes`. This is a bug, and will probably be fixed in a - future release. The current release issues a warning if you do this; to disable - the warning, you should provide the full set of parameters. See below for details. - - :param mode: The image mode. See: :ref:`concept-modes`. - :param size: The image size. - :param data: A bytes or other buffer object containing raw - data for the given mode. - :param decoder_name: What decoder to use. - :param args: Additional parameters for the given decoder. For the - default encoder ("raw"), it's recommended that you provide the - full set of parameters:: - - frombuffer(mode, size, data, "raw", mode, 0, 1) - - :returns: An :py:class:`~PIL.Image.Image` object. - - .. versionadded:: 1.1.4 - """ - - _check_size(size) - - # may pass tuple instead of argument list - if len(args) == 1 and isinstance(args[0], tuple): - args = args[0] - - if decoder_name == "raw": - if args == (): - args = mode, 0, 1 - if args[0] in _MAPMODES: - im = new(mode, (0, 0)) - im = im._new(core.map_buffer(data, size, decoder_name, 0, args)) - if mode == "P": - from . import ImagePalette - - im.palette = ImagePalette.ImagePalette("RGB", im.im.getpalette("RGB")) - im.readonly = 1 - return im - - return frombytes(mode, size, data, decoder_name, args) - - -class SupportsArrayInterface(Protocol): - """ - An object that has an ``__array_interface__`` dictionary. - """ - - @property - def __array_interface__(self) -> dict[str, Any]: - raise NotImplementedError() - - -def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image: - """ - Creates an image memory from an object exporting the array interface - (using the buffer protocol):: - - from PIL import Image - import numpy as np - a = np.zeros((5, 5)) - im = Image.fromarray(a) - - If ``obj`` is not contiguous, then the ``tobytes`` method is called - and :py:func:`~PIL.Image.frombuffer` is used. - - In the case of NumPy, be aware that Pillow modes do not always correspond - to NumPy dtypes. Pillow modes only offer 1-bit pixels, 8-bit pixels, - 32-bit signed integer pixels, and 32-bit floating point pixels. - - Pillow images can also be converted to arrays:: - - from PIL import Image - import numpy as np - im = Image.open("hopper.jpg") - a = np.asarray(im) - - When converting Pillow images to arrays however, only pixel values are - transferred. This means that P and PA mode images will lose their palette. - - :param obj: Object with array interface - :param mode: Optional mode to use when reading ``obj``. Will be determined from - type if ``None``. - - This will not be used to convert the data after reading, but will be used to - change how the data is read:: - - from PIL import Image - import numpy as np - a = np.full((1, 1), 300) - im = Image.fromarray(a, mode="L") - im.getpixel((0, 0)) # 44 - im = Image.fromarray(a, mode="RGB") - im.getpixel((0, 0)) # (44, 1, 0) - - See: :ref:`concept-modes` for general information about modes. - :returns: An image object. - - .. versionadded:: 1.1.6 - """ - arr = obj.__array_interface__ - shape = arr["shape"] - ndim = len(shape) - strides = arr.get("strides", None) - if mode is None: - try: - typekey = (1, 1) + shape[2:], arr["typestr"] - except KeyError as e: - msg = "Cannot handle this data type" - raise TypeError(msg) from e - try: - mode, rawmode = _fromarray_typemap[typekey] - except KeyError as e: - typekey_shape, typestr = typekey - msg = f"Cannot handle this data type: {typekey_shape}, {typestr}" - raise TypeError(msg) from e - else: - rawmode = mode - if mode in ["1", "L", "I", "P", "F"]: - ndmax = 2 - elif mode == "RGB": - ndmax = 3 - else: - ndmax = 4 - if ndim > ndmax: - msg = f"Too many dimensions: {ndim} > {ndmax}." - raise ValueError(msg) - - size = 1 if ndim == 1 else shape[1], shape[0] - if strides is not None: - if hasattr(obj, "tobytes"): - obj = obj.tobytes() - elif hasattr(obj, "tostring"): - obj = obj.tostring() - else: - msg = "'strides' requires either tobytes() or tostring()" - raise ValueError(msg) - - return frombuffer(mode, size, obj, "raw", rawmode, 0, 1) - - -def fromqimage(im): - """Creates an image instance from a QImage image""" - from . import ImageQt - - if not ImageQt.qt_is_installed: - msg = "Qt bindings are not installed" - raise ImportError(msg) - return ImageQt.fromqimage(im) - - -def fromqpixmap(im): - """Creates an image instance from a QPixmap image""" - from . import ImageQt - - if not ImageQt.qt_is_installed: - msg = "Qt bindings are not installed" - raise ImportError(msg) - return ImageQt.fromqpixmap(im) - - -_fromarray_typemap = { - # (shape, typestr) => mode, rawmode - # first two members of shape are set to one - ((1, 1), "|b1"): ("1", "1;8"), - ((1, 1), "|u1"): ("L", "L"), - ((1, 1), "|i1"): ("I", "I;8"), - ((1, 1), "u2"): ("I", "I;16B"), - ((1, 1), "i2"): ("I", "I;16BS"), - ((1, 1), "u4"): ("I", "I;32B"), - ((1, 1), "i4"): ("I", "I;32BS"), - ((1, 1), "f4"): ("F", "F;32BF"), - ((1, 1), "f8"): ("F", "F;64BF"), - ((1, 1, 2), "|u1"): ("LA", "LA"), - ((1, 1, 3), "|u1"): ("RGB", "RGB"), - ((1, 1, 4), "|u1"): ("RGBA", "RGBA"), - # shortcuts: - ((1, 1), f"{_ENDIAN}i4"): ("I", "I"), - ((1, 1), f"{_ENDIAN}f4"): ("F", "F"), -} - - -def _decompression_bomb_check(size: tuple[int, int]) -> None: - if MAX_IMAGE_PIXELS is None: - return - - pixels = max(1, size[0]) * max(1, size[1]) - - if pixels > 2 * MAX_IMAGE_PIXELS: - msg = ( - f"Image size ({pixels} pixels) exceeds limit of {2 * MAX_IMAGE_PIXELS} " - "pixels, could be decompression bomb DOS attack." - ) - raise DecompressionBombError(msg) - - if pixels > MAX_IMAGE_PIXELS: - warnings.warn( - f"Image size ({pixels} pixels) exceeds limit of {MAX_IMAGE_PIXELS} pixels, " - "could be decompression bomb DOS attack.", - DecompressionBombWarning, - ) - - -def open( - fp: StrOrBytesPath | IO[bytes], - mode: Literal["r"] = "r", - formats: list[str] | tuple[str, ...] | None = None, -) -> ImageFile.ImageFile: - """ - Opens and identifies the given image file. - - This is a lazy operation; this function identifies the file, but - the file remains open and the actual image data is not read from - the file until you try to process the data (or call the - :py:meth:`~PIL.Image.Image.load` method). See - :py:func:`~PIL.Image.new`. See :ref:`file-handling`. - - :param fp: A filename (string), os.PathLike object or a file object. - The file object must implement ``file.read``, - ``file.seek``, and ``file.tell`` methods, - and be opened in binary mode. The file object will also seek to zero - before reading. - :param mode: The mode. If given, this argument must be "r". - :param formats: A list or tuple of formats to attempt to load the file in. - This can be used to restrict the set of formats checked. - Pass ``None`` to try all supported formats. You can print the set of - available formats by running ``python3 -m PIL`` or using - the :py:func:`PIL.features.pilinfo` function. - :returns: An :py:class:`~PIL.Image.Image` object. - :exception FileNotFoundError: If the file cannot be found. - :exception PIL.UnidentifiedImageError: If the image cannot be opened and - identified. - :exception ValueError: If the ``mode`` is not "r", or if a ``StringIO`` - instance is used for ``fp``. - :exception TypeError: If ``formats`` is not ``None``, a list or a tuple. - """ - - if mode != "r": - msg = f"bad mode {repr(mode)}" # type: ignore[unreachable] - raise ValueError(msg) - elif isinstance(fp, io.StringIO): - msg = ( # type: ignore[unreachable] - "StringIO cannot be used to open an image. " - "Binary data must be used instead." - ) - raise ValueError(msg) - - if formats is None: - formats = ID - elif not isinstance(formats, (list, tuple)): - msg = "formats must be a list or tuple" # type: ignore[unreachable] - raise TypeError(msg) - - exclusive_fp = False - filename: str | bytes = "" - if is_path(fp): - filename = os.path.realpath(os.fspath(fp)) - - if filename: - fp = builtins.open(filename, "rb") - exclusive_fp = True - else: - fp = cast(IO[bytes], fp) - - try: - fp.seek(0) - except (AttributeError, io.UnsupportedOperation): - fp = io.BytesIO(fp.read()) - exclusive_fp = True - - prefix = fp.read(16) - - preinit() - - accept_warnings: list[str] = [] - - def _open_core( - fp: IO[bytes], - filename: str | bytes, - prefix: bytes, - formats: list[str] | tuple[str, ...], - ) -> ImageFile.ImageFile | None: - for i in formats: - i = i.upper() - if i not in OPEN: - init() - try: - factory, accept = OPEN[i] - result = not accept or accept(prefix) - if isinstance(result, str): - accept_warnings.append(result) - elif result: - fp.seek(0) - im = factory(fp, filename) - _decompression_bomb_check(im.size) - return im - except (SyntaxError, IndexError, TypeError, struct.error): - # Leave disabled by default, spams the logs with image - # opening failures that are entirely expected. - # logger.debug("", exc_info=True) - continue - except BaseException: - if exclusive_fp: - fp.close() - raise - return None - - im = _open_core(fp, filename, prefix, formats) - - if im is None and formats is ID: - checked_formats = ID.copy() - if init(): - im = _open_core( - fp, - filename, - prefix, - tuple(format for format in formats if format not in checked_formats), - ) - - if im: - im._exclusive_fp = exclusive_fp - return im - - if exclusive_fp: - fp.close() - for message in accept_warnings: - warnings.warn(message) - msg = "cannot identify image file %r" % (filename if filename else fp) - raise UnidentifiedImageError(msg) - - -# -# Image processing. - - -def alpha_composite(im1: Image, im2: Image) -> Image: - """ - Alpha composite im2 over im1. - - :param im1: The first image. Must have mode RGBA. - :param im2: The second image. Must have mode RGBA, and the same size as - the first image. - :returns: An :py:class:`~PIL.Image.Image` object. - """ - - im1.load() - im2.load() - return im1._new(core.alpha_composite(im1.im, im2.im)) - - -def blend(im1: Image, im2: Image, alpha: float) -> Image: - """ - Creates a new image by interpolating between two input images, using - a constant alpha:: - - out = image1 * (1.0 - alpha) + image2 * alpha - - :param im1: The first image. - :param im2: The second image. Must have the same mode and size as - the first image. - :param alpha: The interpolation alpha factor. If alpha is 0.0, a - copy of the first image is returned. If alpha is 1.0, a copy of - the second image is returned. There are no restrictions on the - alpha value. If necessary, the result is clipped to fit into - the allowed output range. - :returns: An :py:class:`~PIL.Image.Image` object. - """ - - im1.load() - im2.load() - return im1._new(core.blend(im1.im, im2.im, alpha)) - - -def composite(image1: Image, image2: Image, mask: Image) -> Image: - """ - Create composite image by blending images using a transparency mask. - - :param image1: The first image. - :param image2: The second image. Must have the same mode and - size as the first image. - :param mask: A mask image. This image can have mode - "1", "L", or "RGBA", and must have the same size as the - other two images. - """ - - image = image2.copy() - image.paste(image1, None, mask) - return image - - -def eval(image, *args): - """ - Applies the function (which should take one argument) to each pixel - in the given image. If the image has more than one band, the same - function is applied to each band. Note that the function is - evaluated once for each possible pixel value, so you cannot use - random components or other generators. - - :param image: The input image. - :param function: A function object, taking one integer argument. - :returns: An :py:class:`~PIL.Image.Image` object. - """ - - return image.point(args[0]) - - -def merge(mode: str, bands: Sequence[Image]) -> Image: - """ - Merge a set of single band images into a new multiband image. - - :param mode: The mode to use for the output image. See: - :ref:`concept-modes`. - :param bands: A sequence containing one single-band image for - each band in the output image. All bands must have the - same size. - :returns: An :py:class:`~PIL.Image.Image` object. - """ - - if getmodebands(mode) != len(bands) or "*" in mode: - msg = "wrong number of bands" - raise ValueError(msg) - for band in bands[1:]: - if band.mode != getmodetype(mode): - msg = "mode mismatch" - raise ValueError(msg) - if band.size != bands[0].size: - msg = "size mismatch" - raise ValueError(msg) - for band in bands: - band.load() - return bands[0]._new(core.merge(mode, *[b.im for b in bands])) - - -# -------------------------------------------------------------------- -# Plugin registry - - -def register_open( - id, - factory: Callable[[IO[bytes], str | bytes], ImageFile.ImageFile], - accept: Callable[[bytes], bool | str] | None = None, -) -> None: - """ - Register an image file plugin. This function should not be used - in application code. - - :param id: An image format identifier. - :param factory: An image file factory method. - :param accept: An optional function that can be used to quickly - reject images having another format. - """ - id = id.upper() - if id not in ID: - ID.append(id) - OPEN[id] = factory, accept - - -def register_mime(id: str, mimetype: str) -> None: - """ - Registers an image MIME type by populating ``Image.MIME``. This function - should not be used in application code. - - ``Image.MIME`` provides a mapping from image format identifiers to mime - formats, but :py:meth:`~PIL.ImageFile.ImageFile.get_format_mimetype` can - provide a different result for specific images. - - :param id: An image format identifier. - :param mimetype: The image MIME type for this format. - """ - MIME[id.upper()] = mimetype - - -def register_save( - id: str, driver: Callable[[Image, IO[bytes], str | bytes], None] -) -> None: - """ - Registers an image save function. This function should not be - used in application code. - - :param id: An image format identifier. - :param driver: A function to save images in this format. - """ - SAVE[id.upper()] = driver - - -def register_save_all( - id: str, driver: Callable[[Image, IO[bytes], str | bytes], None] -) -> None: - """ - Registers an image function to save all the frames - of a multiframe format. This function should not be - used in application code. - - :param id: An image format identifier. - :param driver: A function to save images in this format. - """ - SAVE_ALL[id.upper()] = driver - - -def register_extension(id: str, extension: str) -> None: - """ - Registers an image extension. This function should not be - used in application code. - - :param id: An image format identifier. - :param extension: An extension used for this format. - """ - EXTENSION[extension.lower()] = id.upper() - - -def register_extensions(id: str, extensions: list[str]) -> None: - """ - Registers image extensions. This function should not be - used in application code. - - :param id: An image format identifier. - :param extensions: A list of extensions used for this format. - """ - for extension in extensions: - register_extension(id, extension) - - -def registered_extensions() -> dict[str, str]: - """ - Returns a dictionary containing all file extensions belonging - to registered plugins - """ - init() - return EXTENSION - - -def register_decoder(name: str, decoder: type[ImageFile.PyDecoder]) -> None: - """ - Registers an image decoder. This function should not be - used in application code. - - :param name: The name of the decoder - :param decoder: An ImageFile.PyDecoder object - - .. versionadded:: 4.1.0 - """ - DECODERS[name] = decoder - - -def register_encoder(name: str, encoder: type[ImageFile.PyEncoder]) -> None: - """ - Registers an image encoder. This function should not be - used in application code. - - :param name: The name of the encoder - :param encoder: An ImageFile.PyEncoder object - - .. versionadded:: 4.1.0 - """ - ENCODERS[name] = encoder - - -# -------------------------------------------------------------------- -# Simple display support. - - -def _show(image: Image, **options: Any) -> None: - from . import ImageShow - - ImageShow.show(image, **options) - - -# -------------------------------------------------------------------- -# Effects - - -def effect_mandelbrot( - size: tuple[int, int], extent: tuple[int, int, int, int], quality: int -) -> Image: - """ - Generate a Mandelbrot set covering the given extent. - - :param size: The requested size in pixels, as a 2-tuple: - (width, height). - :param extent: The extent to cover, as a 4-tuple: - (x0, y0, x1, y1). - :param quality: Quality. - """ - return Image()._new(core.effect_mandelbrot(size, extent, quality)) - - -def effect_noise(size: tuple[int, int], sigma: float) -> Image: - """ - Generate Gaussian noise centered around 128. - - :param size: The requested size in pixels, as a 2-tuple: - (width, height). - :param sigma: Standard deviation of noise. - """ - return Image()._new(core.effect_noise(size, sigma)) - - -def linear_gradient(mode: str) -> Image: - """ - Generate 256x256 linear gradient from black to white, top to bottom. - - :param mode: Input mode. - """ - return Image()._new(core.linear_gradient(mode)) - - -def radial_gradient(mode: str) -> Image: - """ - Generate 256x256 radial gradient from black to white, centre to edge. - - :param mode: Input mode. - """ - return Image()._new(core.radial_gradient(mode)) - - -# -------------------------------------------------------------------- -# Resources - - -def _apply_env_variables(env=None) -> None: - if env is None: - env = os.environ - - for var_name, setter in [ - ("PILLOW_ALIGNMENT", core.set_alignment), - ("PILLOW_BLOCK_SIZE", core.set_block_size), - ("PILLOW_BLOCKS_MAX", core.set_blocks_max), - ]: - if var_name not in env: - continue - - var = env[var_name].lower() - - units = 1 - for postfix, mul in [("k", 1024), ("m", 1024 * 1024)]: - if var.endswith(postfix): - units = mul - var = var[: -len(postfix)] - - try: - var = int(var) * units - except ValueError: - warnings.warn(f"{var_name} is not int") - continue - - try: - setter(var) - except ValueError as e: - warnings.warn(f"{var_name}: {e}") - - -_apply_env_variables() -atexit.register(core.clear_cache) - - -if TYPE_CHECKING: - _ExifBase = MutableMapping[int, Any] -else: - _ExifBase = MutableMapping - - -class Exif(_ExifBase): - """ - This class provides read and write access to EXIF image data:: - - from PIL import Image - im = Image.open("exif.png") - exif = im.getexif() # Returns an instance of this class - - Information can be read and written, iterated over or deleted:: - - print(exif[274]) # 1 - exif[274] = 2 - for k, v in exif.items(): - print("Tag", k, "Value", v) # Tag 274 Value 2 - del exif[274] - - To access information beyond IFD0, :py:meth:`~PIL.Image.Exif.get_ifd` - returns a dictionary:: - - from PIL import ExifTags - im = Image.open("exif_gps.jpg") - exif = im.getexif() - gps_ifd = exif.get_ifd(ExifTags.IFD.GPSInfo) - print(gps_ifd) - - 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:: - - print(exif[ExifTags.Base.Software]) # PIL - print(gps_ifd[ExifTags.GPS.GPSDateStamp]) # 1999:99:99 99:99:99 - """ - - endian = None - bigtiff = False - _loaded = False - - def __init__(self): - self._data = {} - self._hidden_data = {} - self._ifds = {} - self._info = None - self._loaded_exif = None - - def _fixup(self, value): - try: - if len(value) == 1 and isinstance(value, tuple): - return value[0] - except Exception: - pass - return value - - def _fixup_dict(self, src_dict): - # Helper function - # returns a dict with any single item tuples/lists as individual values - return {k: self._fixup(v) for k, v in src_dict.items()} - - def _get_ifd_dict(self, offset, group=None): - try: - # an offset pointer to the location of the nested embedded IFD. - # It should be a long, but may be corrupted. - self.fp.seek(offset) - except (KeyError, TypeError): - pass - else: - from . import TiffImagePlugin - - info = TiffImagePlugin.ImageFileDirectory_v2(self.head, group=group) - info.load(self.fp) - return self._fixup_dict(info) - - def _get_head(self): - version = b"\x2B" if self.bigtiff else b"\x2A" - if self.endian == "<": - head = b"II" + version + b"\x00" + o32le(8) - else: - head = b"MM\x00" + version + o32be(8) - if self.bigtiff: - head += o32le(8) if self.endian == "<" else o32be(8) - head += b"\x00\x00\x00\x00" - return head - - def load(self, data): - # Extract EXIF information. This is highly experimental, - # and is likely to be replaced with something better in a future - # version. - - # The EXIF record consists of a TIFF file embedded in a JPEG - # application marker (!). - if data == self._loaded_exif: - return - self._loaded_exif = data - self._data.clear() - self._hidden_data.clear() - self._ifds.clear() - if data and data.startswith(b"Exif\x00\x00"): - data = data[6:] - if not data: - self._info = None - return - - self.fp = io.BytesIO(data) - self.head = self.fp.read(8) - # process dictionary - from . import TiffImagePlugin - - self._info = TiffImagePlugin.ImageFileDirectory_v2(self.head) - self.endian = self._info._endian - self.fp.seek(self._info.next) - self._info.load(self.fp) - - def load_from_fp(self, fp, offset=None): - self._loaded_exif = None - self._data.clear() - self._hidden_data.clear() - self._ifds.clear() - - # process dictionary - from . import TiffImagePlugin - - self.fp = fp - if offset is not None: - self.head = self._get_head() - else: - self.head = self.fp.read(8) - self._info = TiffImagePlugin.ImageFileDirectory_v2(self.head) - if self.endian is None: - self.endian = self._info._endian - if offset is None: - offset = self._info.next - self.fp.tell() - self.fp.seek(offset) - self._info.load(self.fp) - - def _get_merged_dict(self): - merged_dict = dict(self) - - # get EXIF extension - if ExifTags.IFD.Exif in self: - ifd = self._get_ifd_dict(self[ExifTags.IFD.Exif], ExifTags.IFD.Exif) - if ifd: - merged_dict.update(ifd) - - # GPS - if ExifTags.IFD.GPSInfo in self: - merged_dict[ExifTags.IFD.GPSInfo] = self._get_ifd_dict( - self[ExifTags.IFD.GPSInfo], ExifTags.IFD.GPSInfo - ) - - return merged_dict - - def tobytes(self, offset: int = 8) -> bytes: - from . import TiffImagePlugin - - head = self._get_head() - ifd = TiffImagePlugin.ImageFileDirectory_v2(ifh=head) - for tag, value in self.items(): - if tag in [ - ExifTags.IFD.Exif, - ExifTags.IFD.GPSInfo, - ] and not isinstance(value, dict): - value = self.get_ifd(tag) - if ( - tag == ExifTags.IFD.Exif - and ExifTags.IFD.Interop in value - and not isinstance(value[ExifTags.IFD.Interop], dict) - ): - value = value.copy() - value[ExifTags.IFD.Interop] = self.get_ifd(ExifTags.IFD.Interop) - ifd[tag] = value - return b"Exif\x00\x00" + head + ifd.tobytes(offset) - - def get_ifd(self, tag): - if tag not in self._ifds: - if tag == ExifTags.IFD.IFD1: - if self._info is not None and self._info.next != 0: - self._ifds[tag] = self._get_ifd_dict(self._info.next) - elif tag in [ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo]: - offset = self._hidden_data.get(tag, self.get(tag)) - if offset is not None: - self._ifds[tag] = self._get_ifd_dict(offset, tag) - elif tag in [ExifTags.IFD.Interop, ExifTags.IFD.Makernote]: - if ExifTags.IFD.Exif not in self._ifds: - self.get_ifd(ExifTags.IFD.Exif) - tag_data = self._ifds[ExifTags.IFD.Exif][tag] - if tag == ExifTags.IFD.Makernote: - from .TiffImagePlugin import ImageFileDirectory_v2 - - if tag_data[:8] == b"FUJIFILM": - ifd_offset = i32le(tag_data, 8) - ifd_data = tag_data[ifd_offset:] - - makernote = {} - for i in range(0, struct.unpack(" 4: - (offset,) = struct.unpack("H", tag_data[:2])[0]): - ifd_tag, typ, count, data = struct.unpack( - ">HHL4s", tag_data[i * 12 + 2 : (i + 1) * 12 + 2] - ) - if ifd_tag == 0x1101: - # CameraInfo - (offset,) = struct.unpack(">L", data) - self.fp.seek(offset) - - camerainfo = {"ModelID": self.fp.read(4)} - - self.fp.read(4) - # Seconds since 2000 - camerainfo["TimeStamp"] = i32le(self.fp.read(12)) - - self.fp.read(4) - camerainfo["InternalSerialNumber"] = self.fp.read(4) - - self.fp.read(12) - parallax = self.fp.read(4) - handler = ImageFileDirectory_v2._load_dispatch[ - TiffTags.FLOAT - ][1] - camerainfo["Parallax"] = handler( - ImageFileDirectory_v2(), parallax, False - ) - - self.fp.read(4) - camerainfo["Category"] = self.fp.read(2) - - makernote = {0x1101: dict(self._fixup_dict(camerainfo))} - self._ifds[tag] = makernote - else: - # Interop - self._ifds[tag] = self._get_ifd_dict(tag_data, tag) - ifd = self._ifds.get(tag, {}) - if tag == ExifTags.IFD.Exif and self._hidden_data: - ifd = { - k: v - for (k, v) in ifd.items() - if k not in (ExifTags.IFD.Interop, ExifTags.IFD.Makernote) - } - return ifd - - def hide_offsets(self) -> None: - for tag in (ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo): - if tag in self: - self._hidden_data[tag] = self[tag] - del self[tag] - - def __str__(self) -> str: - if self._info is not None: - # Load all keys into self._data - for tag in self._info: - self[tag] - - return str(self._data) - - def __len__(self) -> int: - keys = set(self._data) - if self._info is not None: - keys.update(self._info) - return len(keys) - - def __getitem__(self, tag): - if self._info is not None and tag not in self._data and tag in self._info: - self._data[tag] = self._fixup(self._info[tag]) - del self._info[tag] - return self._data[tag] - - def __contains__(self, tag) -> bool: - return tag in self._data or (self._info is not None and tag in self._info) - - def __setitem__(self, tag, value) -> None: - if self._info is not None and tag in self._info: - del self._info[tag] - self._data[tag] = value - - def __delitem__(self, tag: int) -> None: - if self._info is not None and tag in self._info: - del self._info[tag] - else: - del self._data[tag] - - def __iter__(self): - keys = set(self._data) - if self._info is not None: - keys.update(self._info) - return iter(keys) +# +# The Python Imaging Library. +# $Id$ +# +# the Image class wrapper +# +# partial release history: +# 1995-09-09 fl Created +# 1996-03-11 fl PIL release 0.0 (proof of concept) +# 1996-04-30 fl PIL release 0.1b1 +# 1999-07-28 fl PIL release 1.0 final +# 2000-06-07 fl PIL release 1.1 +# 2000-10-20 fl PIL release 1.1.1 +# 2001-05-07 fl PIL release 1.1.2 +# 2002-03-15 fl PIL release 1.1.3 +# 2003-05-10 fl PIL release 1.1.4 +# 2005-03-28 fl PIL release 1.1.5 +# 2006-12-02 fl PIL release 1.1.6 +# 2009-11-15 fl PIL release 1.1.7 +# +# Copyright (c) 1997-2009 by Secret Labs AB. All rights reserved. +# Copyright (c) 1995-2009 by Fredrik Lundh. +# +# See the README file for information on usage and redistribution. +# + +from __future__ import annotations + +import abc +import atexit +import builtins +import io +import logging +import math +import os +import re +import struct +import sys +import tempfile +import warnings +from collections.abc import Callable, MutableMapping +from enum import IntEnum +from types import ModuleType +from typing import IO, TYPE_CHECKING, Any, Literal, Protocol, Sequence, Tuple, cast + +# VERSION was removed in Pillow 6.0.0. +# PILLOW_VERSION was removed in Pillow 9.0.0. +# Use __version__ instead. +from . import ( + ExifTags, + ImageMode, + TiffTags, + UnidentifiedImageError, + __version__, + _plugins, +) +from ._binary import i32le, o32be, o32le +from ._deprecate import deprecate +from ._typing import StrOrBytesPath, TypeGuard +from ._util import DeferredError, is_path + +ElementTree: ModuleType | None +try: + from defusedxml import ElementTree +except ImportError: + ElementTree = None + +logger = logging.getLogger(__name__) + + +class DecompressionBombWarning(RuntimeWarning): + pass + + +class DecompressionBombError(Exception): + pass + +branches = { + "1": False, + "2": False, + "3": False, + "4": False, + "5": False, +} + + +# Limit to around a quarter gigabyte for a 24-bit (3 bpp) image +MAX_IMAGE_PIXELS: int | None = int(1024 * 1024 * 1024 // 4 // 3) + + +try: + # If the _imaging C module is not present, Pillow will not load. + # Note that other modules should not refer to _imaging directly; + # import Image and use the Image.core variable instead. + # Also note that Image.core is not a publicly documented interface, + # and should be considered private and subject to change. + from . import _imaging as core + + if __version__ != getattr(core, "PILLOW_VERSION", None): + msg = ( + "The _imaging extension was built for another version of Pillow or PIL:\n" + f"Core version: {getattr(core, 'PILLOW_VERSION', None)}\n" + f"Pillow version: {__version__}" + ) + raise ImportError(msg) + +except ImportError as v: + core = DeferredError.new(ImportError("The _imaging C module is not installed.")) + # Explanations for ways that we know we might have an import error + if str(v).startswith("Module use of python"): + # The _imaging C module is present, but not compiled for + # the right version (windows only). Print a warning, if + # possible. + warnings.warn( + "The _imaging extension was built for another version of Python.", + RuntimeWarning, + ) + elif str(v).startswith("The _imaging extension"): + warnings.warn(str(v), RuntimeWarning) + # Fail here anyway. Don't let people run with a mostly broken Pillow. + # see docs/porting.rst + raise + + +USE_CFFI_ACCESS = False +cffi: ModuleType | None +try: + import cffi +except ImportError: + cffi = None + + +def isImageType(t: Any) -> TypeGuard[Image]: + """ + Checks if an object is an image object. + + .. warning:: + + This function is for internal use only. + + :param t: object to check if it's an image + :returns: True if the object is an image + """ + return hasattr(t, "im") + + +# +# Constants + + +# transpose +class Transpose(IntEnum): + FLIP_LEFT_RIGHT = 0 + FLIP_TOP_BOTTOM = 1 + ROTATE_90 = 2 + ROTATE_180 = 3 + ROTATE_270 = 4 + TRANSPOSE = 5 + TRANSVERSE = 6 + + +# transforms (also defined in Imaging.h) +class Transform(IntEnum): + AFFINE = 0 + EXTENT = 1 + PERSPECTIVE = 2 + QUAD = 3 + MESH = 4 + + +# resampling filters (also defined in Imaging.h) +class Resampling(IntEnum): + NEAREST = 0 + BOX = 4 + BILINEAR = 2 + HAMMING = 5 + BICUBIC = 3 + LANCZOS = 1 + + +_filters_support = { + Resampling.BOX: 0.5, + Resampling.BILINEAR: 1.0, + Resampling.HAMMING: 1.0, + Resampling.BICUBIC: 2.0, + Resampling.LANCZOS: 3.0, +} + + +# dithers +class Dither(IntEnum): + NONE = 0 + ORDERED = 1 # Not yet implemented + RASTERIZE = 2 # Not yet implemented + FLOYDSTEINBERG = 3 # default + + +# palettes/quantizers +class Palette(IntEnum): + WEB = 0 + ADAPTIVE = 1 + + +class Quantize(IntEnum): + MEDIANCUT = 0 + MAXCOVERAGE = 1 + FASTOCTREE = 2 + LIBIMAGEQUANT = 3 + + +module = sys.modules[__name__] +for enum in (Transpose, Transform, Resampling, Dither, Palette, Quantize): + for item in enum: + setattr(module, item.name, item.value) + + +if hasattr(core, "DEFAULT_STRATEGY"): + DEFAULT_STRATEGY = core.DEFAULT_STRATEGY + FILTERED = core.FILTERED + HUFFMAN_ONLY = core.HUFFMAN_ONLY + RLE = core.RLE + FIXED = core.FIXED + + +# -------------------------------------------------------------------- +# Registries + +if TYPE_CHECKING: + from . import ImageFile +ID: list[str] = [] +OPEN: dict[ + str, + tuple[ + Callable[[IO[bytes], str | bytes], ImageFile.ImageFile], + Callable[[bytes], bool | str] | None, + ], +] = {} +MIME: dict[str, str] = {} +SAVE: dict[str, Callable[[Image, IO[bytes], str | bytes], None]] = {} +SAVE_ALL: dict[str, Callable[[Image, IO[bytes], str | bytes], None]] = {} +EXTENSION: dict[str, str] = {} +DECODERS: dict[str, type[ImageFile.PyDecoder]] = {} +ENCODERS: dict[str, type[ImageFile.PyEncoder]] = {} + +# -------------------------------------------------------------------- +# Modes + +_ENDIAN = "<" if sys.byteorder == "little" else ">" + + +def _conv_type_shape(im): + m = ImageMode.getmode(im.mode) + shape = (im.height, im.width) + extra = len(m.bands) + if extra != 1: + shape += (extra,) + return shape, m.typestr + + +MODES = [ + "1", + "CMYK", + "F", + "HSV", + "I", + "I;16", + "I;16B", + "I;16L", + "I;16N", + "L", + "LA", + "La", + "LAB", + "P", + "PA", + "RGB", + "RGBA", + "RGBa", + "RGBX", + "YCbCr", +] + +# raw modes that may be memory mapped. NOTE: if you change this, you +# may have to modify the stride calculation in map.c too! +_MAPMODES = ("L", "P", "RGBX", "RGBA", "CMYK", "I;16", "I;16L", "I;16B") + + +def getmodebase(mode: str) -> str: + """ + Gets the "base" mode for given mode. This function returns "L" for + images that contain grayscale data, and "RGB" for images that + contain color data. + + :param mode: Input mode. + :returns: "L" or "RGB". + :exception KeyError: If the input mode was not a standard mode. + """ + return ImageMode.getmode(mode).basemode + + +def getmodetype(mode: str) -> str: + """ + Gets the storage type mode. Given a mode, this function returns a + single-layer mode suitable for storing individual bands. + + :param mode: Input mode. + :returns: "L", "I", or "F". + :exception KeyError: If the input mode was not a standard mode. + """ + return ImageMode.getmode(mode).basetype + + +def getmodebandnames(mode: str) -> tuple[str, ...]: + """ + Gets a list of individual band names. Given a mode, this function returns + a tuple containing the names of individual bands (use + :py:method:`~PIL.Image.getmodetype` to get the mode used to store each + individual band. + + :param mode: Input mode. + :returns: A tuple containing band names. The length of the tuple + gives the number of bands in an image of the given mode. + :exception KeyError: If the input mode was not a standard mode. + """ + return ImageMode.getmode(mode).bands + + +def getmodebands(mode: str) -> int: + """ + Gets the number of individual bands for this mode. + + :param mode: Input mode. + :returns: The number of bands in this mode. + :exception KeyError: If the input mode was not a standard mode. + """ + return len(ImageMode.getmode(mode).bands) + + +# -------------------------------------------------------------------- +# Helpers + +_initialized = 0 + + +def preinit() -> None: + """ + Explicitly loads BMP, GIF, JPEG, PPM and PPM file format drivers. + + It is called when opening or saving images. + """ + + global _initialized + if _initialized >= 1: + return + + try: + from . import BmpImagePlugin + + assert BmpImagePlugin + except ImportError: + pass + try: + from . import GifImagePlugin + + assert GifImagePlugin + except ImportError: + pass + try: + from . import JpegImagePlugin + + assert JpegImagePlugin + except ImportError: + pass + try: + from . import PpmImagePlugin + + assert PpmImagePlugin + except ImportError: + pass + try: + from . import PngImagePlugin + + assert PngImagePlugin + except ImportError: + pass + + _initialized = 1 + + +def init() -> bool: + """ + Explicitly initializes the Python Imaging Library. This function + loads all available file format drivers. + + It is called when opening or saving images if :py:meth:`~preinit()` is + insufficient, and by :py:meth:`~PIL.features.pilinfo`. + """ + + global _initialized + if _initialized >= 2: + return False + + parent_name = __name__.rpartition(".")[0] + for plugin in _plugins: + try: + logger.debug("Importing %s", plugin) + __import__(f"{parent_name}.{plugin}", globals(), locals(), []) + except ImportError as e: + logger.debug("Image: failed to import %s: %s", plugin, e) + + if OPEN or SAVE: + _initialized = 2 + return True + return False + + +# -------------------------------------------------------------------- +# Codec factories (used by tobytes/frombytes and ImageFile.load) + + +def _getdecoder(mode, decoder_name, args, extra=()): + # tweak arguments + if args is None: + args = () + elif not isinstance(args, tuple): + args = (args,) + + try: + decoder = DECODERS[decoder_name] + except KeyError: + pass + else: + return decoder(mode, *args + extra) + + try: + # get decoder + decoder = getattr(core, f"{decoder_name}_decoder") + except AttributeError as e: + msg = f"decoder {decoder_name} not available" + raise OSError(msg) from e + return decoder(mode, *args + extra) + + +def _getencoder(mode, encoder_name, args, extra=()): + # tweak arguments + if args is None: + args = () + elif not isinstance(args, tuple): + args = (args,) + + try: + encoder = ENCODERS[encoder_name] + except KeyError: + pass + else: + return encoder(mode, *args + extra) + + try: + # get encoder + encoder = getattr(core, f"{encoder_name}_encoder") + except AttributeError as e: + msg = f"encoder {encoder_name} not available" + raise OSError(msg) from e + return encoder(mode, *args + extra) + + +# -------------------------------------------------------------------- +# Simple expression analyzer + + +class _E: + def __init__(self, scale, offset) -> None: + self.scale = scale + self.offset = offset + + def __neg__(self): + return _E(-self.scale, -self.offset) + + def __add__(self, other): + if isinstance(other, _E): + return _E(self.scale + other.scale, self.offset + other.offset) + return _E(self.scale, self.offset + other) + + __radd__ = __add__ + + def __sub__(self, other): + return self + -other + + def __rsub__(self, other): + return other + -self + + def __mul__(self, other): + if isinstance(other, _E): + return NotImplemented + return _E(self.scale * other, self.offset * other) + + __rmul__ = __mul__ + + def __truediv__(self, other): + if isinstance(other, _E): + return NotImplemented + return _E(self.scale / other, self.offset / other) + + +def _getscaleoffset(expr): + a = expr(_E(1, 0)) + return (a.scale, a.offset) if isinstance(a, _E) else (0, a) + + +# -------------------------------------------------------------------- +# Implementation wrapper + + +class SupportsGetData(Protocol): + def getdata( + self, + ) -> tuple[Transform, Sequence[int]]: ... + + +class Image: + """ + This class represents an image object. To create + :py:class:`~PIL.Image.Image` objects, use the appropriate factory + functions. There's hardly ever any reason to call the Image constructor + directly. + + * :py:func:`~PIL.Image.open` + * :py:func:`~PIL.Image.new` + * :py:func:`~PIL.Image.frombytes` + """ + + format: str | None = None + format_description: str | None = None + _close_exclusive_fp_after_loading = True + + def __init__(self): + # FIXME: take "new" parameters / other image? + # FIXME: turn mode and size into delegating properties? + self.im = None + self._mode = "" + self._size = (0, 0) + self.palette = None + self.info = {} + self.readonly = 0 + self.pyaccess = None + self._exif = None + + @property + def width(self) -> int: + return self.size[0] + + @property + def height(self) -> int: + return self.size[1] + + @property + def size(self) -> tuple[int, int]: + return self._size + + @property + def mode(self): + return self._mode + + def _new(self, im) -> Image: + new = Image() + new.im = im + new._mode = im.mode + new._size = im.size + if im.mode in ("P", "PA"): + if self.palette: + new.palette = self.palette.copy() + else: + from . import ImagePalette + + new.palette = ImagePalette.ImagePalette() + new.info = self.info.copy() + return new + + # Context manager support + 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"): + if getattr(self, "_exclusive_fp", False): + self._close_fp() + self.fp = None + + def close(self) -> None: + """ + Closes the file pointer, if possible. + + This operation will destroy the image core and release its memory. + The image data will be unusable afterward. + + This function is required to close images that have multiple frames or + have not had their file read and closed by the + :py:meth:`~PIL.Image.Image.load` method. See :ref:`file-handling` for + more information. + """ + if hasattr(self, "fp"): + try: + self._close_fp() + self.fp = None + except Exception as msg: + logger.debug("Error closing: %s", msg) + + if getattr(self, "map", None): + self.map = None + + # Instead of simply setting to None, we're setting up a + # deferred error that will better explain that the core image + # object is gone. + self.im = DeferredError(ValueError("Operation on closed image")) + + def _copy(self) -> None: + self.load() + self.im = self.im.copy() + self.pyaccess = None + self.readonly = 0 + + def _ensure_mutable(self) -> None: + if self.readonly: + self._copy() + else: + self.load() + + def _dump( + self, file: str | None = None, format: str | None = None, **options: Any + ) -> str: + suffix = "" + if format: + suffix = f".{format}" + + if not file: + f, filename = tempfile.mkstemp(suffix) + os.close(f) + else: + filename = file + if not filename.endswith(suffix): + filename = filename + suffix + + self.load() + + if not format or format == "PPM": + self.im.save_ppm(filename) + else: + self.save(filename, format, **options) + + return filename + + def __eq__(self, other: object) -> bool: + if self.__class__ is not other.__class__: + return False + assert isinstance(other, Image) + return ( + self.mode == other.mode + and self.size == other.size + and self.info == other.info + and self.getpalette() == other.getpalette() + and self.tobytes() == other.tobytes() + ) + + 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), + ) + + def _repr_pretty_(self, p, cycle) -> None: + """IPython plain text display support""" + + # Same as __repr__ but without unpredictable id(self), + # to keep Jupyter notebook `text/plain` output stable. + p.text( + "<%s.%s image mode=%s size=%dx%d>" + % ( + self.__class__.__module__, + self.__class__.__name__, + self.mode, + self.size[0], + self.size[1], + ) + ) + + def _repr_image(self, image_format, **kwargs): + """Helper function for iPython display hook. + + :param image_format: Image format. + :returns: image as bytes, saved into the given format. + """ + b = io.BytesIO() + try: + self.save(b, image_format, **kwargs) + except Exception: + return None + return b.getvalue() + + def _repr_png_(self): + """iPython display hook support for PNG format. + + :returns: PNG version of the image as bytes + """ + return self._repr_image("PNG", compress_level=1) + + def _repr_jpeg_(self): + """iPython display hook support for JPEG format. + + :returns: JPEG version of the image as bytes + """ + return self._repr_image("JPEG") + + @property + def __array_interface__(self): + # numpy array interface support + new = {"version": 3} + try: + if self.mode == "1": + # Binary images need to be extended from bits to bytes + # See: https://github.com/python-pillow/Pillow/issues/350 + new["data"] = self.tobytes("raw", "L") + else: + new["data"] = self.tobytes() + except Exception as e: + if not isinstance(e, (MemoryError, RecursionError)): + try: + import numpy + from packaging.version import parse as parse_version + except ImportError: + pass + else: + if parse_version(numpy.__version__) < parse_version("1.23"): + warnings.warn(str(e)) + raise + new["shape"], new["typestr"] = _conv_type_shape(self) + return new + + def __getstate__(self): + im_data = self.tobytes() # load image first + return [self.info, self.mode, self.size, self.getpalette(), im_data] + + def __setstate__(self, state) -> None: + Image.__init__(self) + info, mode, size, palette, data = state + self.info = info + self._mode = mode + self._size = size + self.im = core.new(mode, size) + if mode in ("L", "LA", "P", "PA") and palette: + self.putpalette(palette) + self.frombytes(data) + + def tobytes(self, encoder_name: str = "raw", *args) -> bytes: + """ + Return image as a bytes object. + + .. 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. + + :param encoder_name: What encoder to use. The default is to + use the standard "raw" encoder. + + A list of C encoders can be seen under + codecs section of the function array in + :file:`_imaging.c`. Python encoders are + registered within the relevant plugins. + :param args: Extra arguments to the encoder. + :returns: A :py:class:`bytes` object. + """ + + # may pass tuple instead of argument list + if len(args) == 1 and isinstance(args[0], tuple): + args = args[0] + + if encoder_name == "raw" and args == (): + args = self.mode + + self.load() + + if self.width == 0 or self.height == 0: + return b"" + + # unpack data + e = _getencoder(self.mode, encoder_name, args) + e.setimage(self.im) + + bufsize = max(65536, self.size[0] * 4) # see RawEncode.c + + output = [] + while True: + bytes_consumed, errcode, data = e.encode(bufsize) + output.append(data) + if errcode: + break + if errcode < 0: + msg = f"encoder error {errcode} in tobytes" + raise RuntimeError(msg) + + return b"".join(output) + + def tobitmap(self, name: str = "image") -> bytes: + """ + Returns the image converted to an X11 bitmap. + + .. note:: This method only works for mode "1" images. + + :param name: The name prefix to use for the bitmap variables. + :returns: A string containing an X11 bitmap. + :raises ValueError: If the mode is not "1" + """ + + self.load() + if self.mode != "1": + msg = "not a bitmap" + raise ValueError(msg) + data = self.tobytes("xbm") + return b"".join( + [ + f"#define {name}_width {self.size[0]}\n".encode("ascii"), + f"#define {name}_height {self.size[1]}\n".encode("ascii"), + f"static char {name}_bits[] = {{\n".encode("ascii"), + data, + b"};", + ] + ) + + def frombytes(self, data: bytes, decoder_name: str = "raw", *args) -> None: + """ + Loads this image with pixel data from a bytes object. + + This method is similar to the :py:func:`~PIL.Image.frombytes` function, + but loads data into this image instead of creating a new image object. + """ + + if self.width == 0 or self.height == 0: + return + + # may pass tuple instead of argument list + if len(args) == 1 and isinstance(args[0], tuple): + args = args[0] + + # default format + if decoder_name == "raw" and args == (): + args = self.mode + + # unpack data + d = _getdecoder(self.mode, decoder_name, args) + d.setimage(self.im) + s = d.decode(data) + + if s[0] >= 0: + msg = "not enough image data" + raise ValueError(msg) + if s[1] != 0: + msg = "cannot decode image data" + raise ValueError(msg) + + def load(self): + """ + Allocates storage for the image and loads the pixel data. In + normal cases, you don't need to call this method, since the + Image class automatically loads an opened image when it is + accessed for the first time. + + If the file associated with the image was opened by Pillow, then this + method will close it. The exception to this is if the image has + multiple frames, in which case the file will be left open for seek + operations. See :ref:`file-handling` for more information. + + :returns: An image access object. + :rtype: :ref:`PixelAccess` or :py:class:`PIL.PyAccess` + """ + if self.im is not None and self.palette and self.palette.dirty: + # realize palette + mode, arr = self.palette.getdata() + self.im.putpalette(mode, arr) + self.palette.dirty = 0 + self.palette.rawmode = None + if "transparency" in self.info and mode in ("LA", "PA"): + if isinstance(self.info["transparency"], int): + self.im.putpalettealpha(self.info["transparency"], 0) + else: + self.im.putpalettealphas(self.info["transparency"]) + self.palette.mode = "RGBA" + else: + palette_mode = "RGBA" if mode.startswith("RGBA") else "RGB" + self.palette.mode = palette_mode + self.palette.palette = self.im.getpalette(palette_mode, palette_mode) + + if self.im is not None: + if cffi and USE_CFFI_ACCESS: + if self.pyaccess: + return self.pyaccess + from . import PyAccess + + self.pyaccess = PyAccess.new(self, self.readonly) + if self.pyaccess: + return self.pyaccess + return self.im.pixel_access(self.readonly) + + def verify(self) -> None: + """ + Verifies the contents of a file. For data read from a file, this + method attempts to determine if the file is broken, without + actually decoding the image data. If this method finds any + problems, it raises suitable exceptions. If you need to load + the image after using this method, you must reopen the image + file. + """ + pass + + def convert( + self, + mode: str | None = None, + matrix: tuple[float, ...] | None = None, + dither: Dither | None = None, + palette: Palette = Palette.WEB, + colors: int = 256, + ) -> Image: + """ + Returns a converted copy of this image. For the "P" mode, this + method translates pixels through the palette. If mode is + omitted, a mode is chosen so that all information in the image + and the palette can be represented without a palette. + + This supports all possible conversions between "L", "RGB" and "CMYK". The + ``matrix`` argument only supports "L" and "RGB". + + When translating a color image to grayscale (mode "L"), + the library uses the ITU-R 601-2 luma transform:: + + L = R * 299/1000 + G * 587/1000 + B * 114/1000 + + The default method of converting a grayscale ("L") or "RGB" + image into a bilevel (mode "1") image uses Floyd-Steinberg + dither to approximate the original image luminosity levels. If + dither is ``None``, all values larger than 127 are set to 255 (white), + all other values to 0 (black). To use other thresholds, use the + :py:meth:`~PIL.Image.Image.point` method. + + When converting from "RGBA" to "P" without a ``matrix`` argument, + this passes the operation to :py:meth:`~PIL.Image.Image.quantize`, + and ``dither`` and ``palette`` are ignored. + + When converting from "PA", if an "RGBA" palette is present, the alpha + channel from the image will be used instead of the values from the palette. + + :param mode: The requested mode. See: :ref:`concept-modes`. + :param matrix: An optional conversion matrix. If given, this + should be 4- or 12-tuple containing floating point values. + :param dither: Dithering method, used when converting from + mode "RGB" to "P" or from "RGB" or "L" to "1". + Available methods are :data:`Dither.NONE` or :data:`Dither.FLOYDSTEINBERG` + (default). Note that this is not used when ``matrix`` is supplied. + :param palette: Palette to use when converting from mode "RGB" + to "P". Available palettes are :data:`Palette.WEB` or + :data:`Palette.ADAPTIVE`. + :param colors: Number of colors to use for the :data:`Palette.ADAPTIVE` + palette. Defaults to 256. + :rtype: :py:class:`~PIL.Image.Image` + :returns: An :py:class:`~PIL.Image.Image` object. + """ + + if mode in ("BGR;15", "BGR;16", "BGR;24"): + deprecate(mode, 12) + + self.load() + + has_transparency = "transparency" in self.info + if not mode and self.mode == "P": + # determine default mode + if self.palette: + mode = self.palette.mode + else: + mode = "RGB" + if mode == "RGB" and has_transparency: + mode = "RGBA" + if not mode or (mode == self.mode and not matrix): + return self.copy() + + if matrix: + # matrix conversion + if mode not in ("L", "RGB"): + msg = "illegal conversion" + raise ValueError(msg) + im = self.im.convert_matrix(mode, matrix) + new_im = self._new(im) + if has_transparency and self.im.bands == 3: + transparency = new_im.info["transparency"] + + def convert_transparency(m, v): + v = m[0] * v[0] + m[1] * v[1] + m[2] * v[2] + m[3] * 0.5 + return max(0, min(255, int(v))) + + if mode == "L": + transparency = convert_transparency(matrix, transparency) + elif len(mode) == 3: + transparency = tuple( + convert_transparency(matrix[i * 4 : i * 4 + 4], transparency) + for i in range(0, len(transparency)) + ) + new_im.info["transparency"] = transparency + return new_im + + if mode == "P" and self.mode == "RGBA": + return self.quantize(colors) + + trns = None + delete_trns = False + # transparency handling + if has_transparency: + if (self.mode in ("1", "L", "I", "I;16") and mode in ("LA", "RGBA")) or ( + self.mode == "RGB" and mode in ("La", "LA", "RGBa", "RGBA") + ): + # Use transparent conversion to promote from transparent + # color to an alpha channel. + new_im = self._new( + self.im.convert_transparent(mode, self.info["transparency"]) + ) + del new_im.info["transparency"] + return new_im + elif self.mode in ("L", "RGB", "P") and mode in ("L", "RGB", "P"): + t = self.info["transparency"] + if isinstance(t, bytes): + # Dragons. This can't be represented by a single color + warnings.warn( + "Palette images with Transparency expressed in bytes should be " + "converted to RGBA images" + ) + delete_trns = True + else: + # get the new transparency color. + # use existing conversions + trns_im = new(self.mode, (1, 1)) + if self.mode == "P": + trns_im.putpalette(self.palette) + if isinstance(t, tuple): + err = "Couldn't allocate a palette color for transparency" + try: + t = trns_im.palette.getcolor(t, self) + except ValueError as e: + if str(e) == "cannot allocate more than 256 colors": + # If all 256 colors are in use, + # then there is no need for transparency + t = None + else: + raise ValueError(err) from e + if t is None: + trns = None + else: + trns_im.putpixel((0, 0), t) + + if mode in ("L", "RGB"): + trns_im = trns_im.convert(mode) + else: + # can't just retrieve the palette number, got to do it + # after quantization. + trns_im = trns_im.convert("RGB") + trns = trns_im.getpixel((0, 0)) + + elif self.mode == "P" and mode in ("LA", "PA", "RGBA"): + t = self.info["transparency"] + delete_trns = True + + if isinstance(t, bytes): + self.im.putpalettealphas(t) + elif isinstance(t, int): + self.im.putpalettealpha(t, 0) + else: + msg = "Transparency for P mode should be bytes or int" + raise ValueError(msg) + + if mode == "P" and palette == Palette.ADAPTIVE: + im = self.im.quantize(colors) + new_im = self._new(im) + from . import ImagePalette + + new_im.palette = ImagePalette.ImagePalette( + "RGB", new_im.im.getpalette("RGB") + ) + if delete_trns: + # This could possibly happen if we requantize to fewer colors. + # The transparency would be totally off in that case. + del new_im.info["transparency"] + if trns is not None: + try: + new_im.info["transparency"] = new_im.palette.getcolor(trns, new_im) + except Exception: + # if we can't make a transparent color, don't leave the old + # transparency hanging around to mess us up. + del new_im.info["transparency"] + warnings.warn("Couldn't allocate palette entry for transparency") + return new_im + + if "LAB" in (self.mode, mode): + other_mode = mode if self.mode == "LAB" else self.mode + if other_mode in ("RGB", "RGBA", "RGBX"): + from . import ImageCms + + srgb = ImageCms.createProfile("sRGB") + lab = ImageCms.createProfile("LAB") + profiles = [lab, srgb] if self.mode == "LAB" else [srgb, lab] + transform = ImageCms.buildTransform( + profiles[0], profiles[1], self.mode, mode + ) + return transform.apply(self) + + # colorspace conversion + if dither is None: + dither = Dither.FLOYDSTEINBERG + + try: + im = self.im.convert(mode, dither) + except ValueError: + try: + # normalize source image and try again + modebase = getmodebase(self.mode) + if modebase == self.mode: + raise + im = self.im.convert(modebase) + im = im.convert(mode, dither) + except KeyError as e: + msg = "illegal conversion" + raise ValueError(msg) from e + + new_im = self._new(im) + if mode == "P" and palette != Palette.ADAPTIVE: + from . import ImagePalette + + new_im.palette = ImagePalette.ImagePalette("RGB", im.getpalette("RGB")) + if delete_trns: + # crash fail if we leave a bytes transparency in an rgb/l mode. + del new_im.info["transparency"] + if trns is not None: + if new_im.mode == "P": + try: + new_im.info["transparency"] = new_im.palette.getcolor(trns, new_im) + except ValueError as e: + del new_im.info["transparency"] + if str(e) != "cannot allocate more than 256 colors": + # If all 256 colors are in use, + # then there is no need for transparency + warnings.warn( + "Couldn't allocate palette entry for transparency" + ) + else: + new_im.info["transparency"] = trns + return new_im + + def quantize( + self, + colors: int = 256, + method: Quantize | None = None, + kmeans: int = 0, + palette=None, + dither: Dither = Dither.FLOYDSTEINBERG, + ) -> Image: + """ + Convert the image to 'P' mode with the specified number + of colors. + + :param colors: The desired number of colors, <= 256 + :param method: :data:`Quantize.MEDIANCUT` (median cut), + :data:`Quantize.MAXCOVERAGE` (maximum coverage), + :data:`Quantize.FASTOCTREE` (fast octree), + :data:`Quantize.LIBIMAGEQUANT` (libimagequant; check support + using :py:func:`PIL.features.check_feature` with + ``feature="libimagequant"``). + + By default, :data:`Quantize.MEDIANCUT` will be used. + + The exception to this is RGBA images. :data:`Quantize.MEDIANCUT` + and :data:`Quantize.MAXCOVERAGE` do not support RGBA images, so + :data:`Quantize.FASTOCTREE` is used by default instead. + :param kmeans: Integer greater than or equal to zero. + :param palette: Quantize to the palette of given + :py:class:`PIL.Image.Image`. + :param dither: Dithering method, used when converting from + mode "RGB" to "P" or from "RGB" or "L" to "1". + Available methods are :data:`Dither.NONE` or :data:`Dither.FLOYDSTEINBERG` + (default). + :returns: A new image + """ + + self.load() + + if method is None: + # defaults: + method = Quantize.MEDIANCUT + if self.mode == "RGBA": + method = Quantize.FASTOCTREE + + if self.mode == "RGBA" and method not in ( + Quantize.FASTOCTREE, + Quantize.LIBIMAGEQUANT, + ): + # Caller specified an invalid mode. + msg = ( + "Fast Octree (method == 2) and libimagequant (method == 3) " + "are the only valid methods for quantizing RGBA images" + ) + raise ValueError(msg) + + if palette: + # use palette from reference image + palette.load() + if palette.mode != "P": + msg = "bad mode for palette image" + raise ValueError(msg) + if self.mode not in {"RGB", "L"}: + msg = "only RGB or L mode images can be quantized to a palette" + raise ValueError(msg) + im = self.im.convert("P", dither, palette.im) + new_im = self._new(im) + new_im.palette = palette.palette.copy() + return new_im + + if kmeans < 0: + msg = "kmeans must not be negative" + raise ValueError(msg) + + im = self._new(self.im.quantize(colors, method, kmeans)) + + from . import ImagePalette + + mode = im.im.getpalettemode() + palette = im.im.getpalette(mode, mode)[: colors * len(mode)] + im.palette = ImagePalette.ImagePalette(mode, palette) + + return im + + def copy(self) -> Image: + """ + Copies this image. Use this method if you wish to paste things + into an image, but still retain the original. + + :rtype: :py:class:`~PIL.Image.Image` + :returns: An :py:class:`~PIL.Image.Image` object. + """ + self.load() + return self._new(self.im.copy()) + + __copy__ = copy + + def crop(self, box: tuple[int, int, int, int] | None = None) -> Image: + """ + Returns a rectangular region from this image. The box is a + 4-tuple defining the left, upper, right, and lower pixel + coordinate. See :ref:`coordinate-system`. + + Note: Prior to Pillow 3.4.0, this was a lazy operation. + + :param box: The crop rectangle, as a (left, upper, right, lower)-tuple. + :rtype: :py:class:`~PIL.Image.Image` + :returns: An :py:class:`~PIL.Image.Image` object. + """ + + if box is None: + return self.copy() + + if box[2] < box[0]: + msg = "Coordinate 'right' is less than 'left'" + raise ValueError(msg) + elif box[3] < box[1]: + msg = "Coordinate 'lower' is less than 'upper'" + raise ValueError(msg) + + self.load() + return self._new(self._crop(self.im, box)) + + def _crop(self, im, box): + """ + Returns a rectangular region from the core image object im. + + This is equivalent to calling im.crop((x0, y0, x1, y1)), but + includes additional sanity checks. + + :param im: a core image object + :param box: The crop rectangle, as a (left, upper, right, lower)-tuple. + :returns: A core image object. + """ + + x0, y0, x1, y1 = map(int, map(round, box)) + + absolute_values = (abs(x1 - x0), abs(y1 - y0)) + + _decompression_bomb_check(absolute_values) + + return im.crop((x0, y0, x1, y1)) + + def draft( + self, mode: str | None, size: tuple[int, int] + ) -> tuple[str, tuple[int, int, float, float]] | None: + """ + Configures the image file loader so it returns a version of the + image that as closely as possible matches the given mode and + size. For example, you can use this method to convert a color + JPEG to grayscale while loading it. + + If any changes are made, returns a tuple with the chosen ``mode`` and + ``box`` with coordinates of the original image within the altered one. + + Note that this method modifies the :py:class:`~PIL.Image.Image` object + in place. If the image has already been loaded, this method has no + effect. + + Note: This method is not implemented for most images. It is + currently implemented only for JPEG and MPO images. + + :param mode: The requested mode. + :param size: The requested size in pixels, as a 2-tuple: + (width, height). + """ + pass + + def _expand(self, xmargin: int, ymargin: int | None = None) -> Image: + if ymargin is None: + ymargin = xmargin + self.load() + return self._new(self.im.expand(xmargin, ymargin)) + + if TYPE_CHECKING: + from . import ImageFilter + + def filter(self, filter: ImageFilter.Filter | type[ImageFilter.Filter]) -> Image: + """ + Filters this image using the given filter. For a list of + available filters, see the :py:mod:`~PIL.ImageFilter` module. + + :param filter: Filter kernel. + :returns: An :py:class:`~PIL.Image.Image` object.""" + + from . import ImageFilter + + self.load() + + if callable(filter): + filter = filter() + if not hasattr(filter, "filter"): + msg = "filter argument should be ImageFilter.Filter instance or class" + raise TypeError(msg) + + multiband = isinstance(filter, ImageFilter.MultibandFilter) + if self.im.bands == 1 or multiband: + return self._new(filter.filter(self.im)) + + ims = [ + self._new(filter.filter(self.im.getband(c))) for c in range(self.im.bands) + ] + return merge(self.mode, ims) + + def getbands(self) -> tuple[str, ...]: + """ + Returns a tuple containing the name of each band in this image. + For example, ``getbands`` on an RGB image returns ("R", "G", "B"). + + :returns: A tuple containing band names. + :rtype: tuple + """ + return ImageMode.getmode(self.mode).bands + + def getbbox(self, *, alpha_only: bool = True) -> tuple[int, int, int, int] | None: + """ + Calculates the bounding box of the non-zero regions in the + image. + + :param alpha_only: Optional flag, defaulting to ``True``. + If ``True`` and the image has an alpha channel, trim transparent pixels. + Otherwise, trim pixels when all channels are zero. + Keyword-only argument. + :returns: The bounding box is returned as a 4-tuple defining the + left, upper, right, and lower pixel coordinate. See + :ref:`coordinate-system`. If the image is completely empty, this + method returns None. + + """ + + self.load() + return self.im.getbbox(alpha_only) + + def getcolors(self, maxcolors: int = 256): + """ + Returns a list of colors used in this image. + + The colors will be in the image's mode. For example, an RGB image will + return a tuple of (red, green, blue) color values, and a P image will + return the index of the color in the palette. + + :param maxcolors: Maximum number of colors. If this number is + exceeded, this method returns None. The default limit is + 256 colors. + :returns: An unsorted list of (count, pixel) values. + """ + + self.load() + if self.mode in ("1", "L", "P"): + h = self.im.histogram() + out = [(h[i], i) for i in range(256) if h[i]] + if len(out) > maxcolors: + return None + return out + return self.im.getcolors(maxcolors) + + def getdata(self, band: int | None = None): + """ + Returns the contents of this image as a sequence object + containing pixel values. The sequence object is flattened, so + that values for line one follow directly after the values of + line zero, and so on. + + Note that the sequence object returned by this method is an + internal PIL data type, which only supports certain sequence + operations. To convert it to an ordinary sequence (e.g. for + printing), use ``list(im.getdata())``. + + :param band: What band to return. The default is to return + all bands. To return a single band, pass in the index + value (e.g. 0 to get the "R" band from an "RGB" image). + :returns: A sequence-like object. + """ + + self.load() + if band is not None: + return self.im.getband(band) + return self.im # could be abused + + def getextrema(self) -> tuple[float, float] | tuple[tuple[int, int], ...]: + """ + Gets the minimum and maximum pixel values for each band in + the image. + + :returns: For a single-band image, a 2-tuple containing the + minimum and maximum pixel value. For a multi-band image, + a tuple containing one 2-tuple for each band. + """ + + self.load() + if self.im.bands > 1: + return tuple(self.im.getband(i).getextrema() for i in range(self.im.bands)) + return self.im.getextrema() + + def _getxmp(self, xmp_tags): + def get_name(tag): + return re.sub("^{[^}]+}", "", tag) + + def get_value(element): + value = {get_name(k): v for k, v in element.attrib.items()} + children = list(element) + if children: + for child in children: + name = get_name(child.tag) + child_value = get_value(child) + if name in value: + if not isinstance(value[name], list): + value[name] = [value[name]] + value[name].append(child_value) + else: + value[name] = child_value + elif value: + if element.text: + value["text"] = element.text + else: + return element.text + return value + + if ElementTree is None: + warnings.warn("XMP data cannot be read without defusedxml dependency") + return {} + else: + root = ElementTree.fromstring(xmp_tags) + return {get_name(root.tag): get_value(root)} + + def getexif(self) -> Exif: + """ + Gets EXIF data from the image. + + :returns: an :py:class:`~PIL.Image.Exif` object. + """ + if self._exif is None: + self._exif = Exif() + elif self._exif._loaded: + return self._exif + self._exif._loaded = True + + exif_info = self.info.get("exif") + if exif_info is None: + if "Raw profile type exif" in self.info: + exif_info = bytes.fromhex( + "".join(self.info["Raw profile type exif"].split("\n")[3:]) + ) + elif hasattr(self, "tag_v2"): + self._exif.bigtiff = self.tag_v2._bigtiff + self._exif.endian = self.tag_v2._endian + self._exif.load_from_fp(self.fp, self.tag_v2._offset) + if exif_info is not None: + self._exif.load(exif_info) + + # XMP tags + if ExifTags.Base.Orientation not in self._exif: + xmp_tags = self.info.get("XML:com.adobe.xmp") + if xmp_tags: + match = re.search(r'tiff:Orientation(="|>)([0-9])', xmp_tags) + if match: + self._exif[ExifTags.Base.Orientation] = int(match[2]) + + return self._exif + + def _reload_exif(self) -> None: + if self._exif is None or not self._exif._loaded: + return + self._exif._loaded = False + self.getexif() + + def get_child_images(self) -> list[ImageFile.ImageFile]: + child_images = [] + exif = self.getexif() + ifds = [] + if ExifTags.Base.SubIFDs in exif: + subifd_offsets = exif[ExifTags.Base.SubIFDs] + if subifd_offsets: + if not isinstance(subifd_offsets, tuple): + subifd_offsets = (subifd_offsets,) + for subifd_offset in subifd_offsets: + ifds.append((exif._get_ifd_dict(subifd_offset), subifd_offset)) + ifd1 = exif.get_ifd(ExifTags.IFD.IFD1) + if ifd1 and ifd1.get(513): + ifds.append((ifd1, exif._info.next)) + + offset = None + for ifd, ifd_offset in ifds: + current_offset = self.fp.tell() + if offset is None: + offset = current_offset + + fp = self.fp + thumbnail_offset = ifd.get(513) + if thumbnail_offset is not None: + thumbnail_offset += getattr(self, "_exif_offset", 0) + self.fp.seek(thumbnail_offset) + data = self.fp.read(ifd.get(514)) + fp = io.BytesIO(data) + + with open(fp) as im: + if thumbnail_offset is None: + im._frame_pos = [ifd_offset] + im._seek(0) + im.load() + child_images.append(im) + + if offset is not None: + self.fp.seek(offset) + return child_images + + def getim(self): + """ + Returns a capsule that points to the internal image memory. + + :returns: A capsule object. + """ + + self.load() + return self.im.ptr + + def getpalette(self, rawmode: str | None = "RGB") -> list[int] | None: + """ + Returns the image palette as a list. + + :param rawmode: The mode in which to return the palette. ``None`` will + return the palette in its current mode. + + .. versionadded:: 9.1.0 + + :returns: A list of color values [r, g, b, ...], or None if the + image has no palette. + """ + + self.load() + try: + mode = self.im.getpalettemode() + except ValueError: + return None # no palette + if rawmode is None: + rawmode = mode + return list(self.im.getpalette(mode, rawmode)) + + @property + def has_transparency_data(self) -> bool: + """ + Determine if an image has transparency data, whether in the form of an + alpha channel, a palette with an alpha channel, or a "transparency" key + in the info dictionary. + + Note the image might still appear solid, if all of the values shown + within are opaque. + + :returns: A boolean. + """ + return ( + self.mode in ("LA", "La", "PA", "RGBA", "RGBa") + or (self.mode == "P" and self.palette.mode.endswith("A")) + or "transparency" in self.info + ) + + def apply_transparency(self) -> None: + """ + If a P mode image has a "transparency" key in the info dictionary, + remove the key and instead apply the transparency to the palette. + Otherwise, the image is unchanged. + """ + if self.mode != "P" or "transparency" not in self.info: + return + + from . import ImagePalette + + palette = self.getpalette("RGBA") + assert palette is not None + transparency = self.info["transparency"] + if isinstance(transparency, bytes): + for i, alpha in enumerate(transparency): + palette[i * 4 + 3] = alpha + else: + palette[transparency * 4 + 3] = 0 + self.palette = ImagePalette.ImagePalette("RGBA", bytes(palette)) + self.palette.dirty = 1 + + del self.info["transparency"] + + def getpixel(self, xy): + """ + Returns the pixel value at a given position. + + :param xy: The coordinate, given as (x, y). See + :ref:`coordinate-system`. + :returns: The pixel value. If the image is a multi-layer image, + this method returns a tuple. + """ + + self.load() + if self.pyaccess: + return self.pyaccess.getpixel(xy) + return self.im.getpixel(tuple(xy)) + + def getprojection(self) -> tuple[list[int], list[int]]: + """ + Get projection to x and y axes + + :returns: Two sequences, indicating where there are non-zero + pixels along the X-axis and the Y-axis, respectively. + """ + + self.load() + x, y = self.im.getprojection() + return list(x), list(y) + + def histogram(self, mask: Image | None = None, extrema=None) -> list[int]: + """ + Returns a histogram for the image. The histogram is returned as a + list of pixel counts, one for each pixel value in the source + image. Counts are grouped into 256 bins for each band, even if + the image has more than 8 bits per band. If the image has more + than one band, the histograms for all bands are concatenated (for + example, the histogram for an "RGB" image contains 768 values). + + A bilevel image (mode "1") is treated as a grayscale ("L") image + by this method. + + If a mask is provided, the method returns a histogram for those + parts of the image where the mask image is non-zero. The mask + image must have the same size as the image, and be either a + bi-level image (mode "1") or a grayscale image ("L"). + + :param mask: An optional mask. + :param extrema: An optional tuple of manually-specified extrema. + :returns: A list containing pixel counts. + """ + self.load() + if mask: + mask.load() + return self.im.histogram((0, 0), mask.im) + if self.mode in ("I", "F"): + if extrema is None: + extrema = self.getextrema() + return self.im.histogram(extrema) + return self.im.histogram() + + def entropy(self, mask=None, extrema=None): + """ + Calculates and returns the entropy for the image. + + A bilevel image (mode "1") is treated as a grayscale ("L") + image by this method. + + If a mask is provided, the method employs the histogram for + those parts of the image where the mask image is non-zero. + The mask image must have the same size as the image, and be + either a bi-level image (mode "1") or a grayscale image ("L"). + + :param mask: An optional mask. + :param extrema: An optional tuple of manually-specified extrema. + :returns: A float value representing the image entropy + """ + self.load() + if mask: + mask.load() + return self.im.entropy((0, 0), mask.im) + if self.mode in ("I", "F"): + if extrema is None: + extrema = self.getextrema() + return self.im.entropy(extrema) + return self.im.entropy() + + def paste( + self, + im: Image | str | float | tuple[float, ...], + box: tuple[int, int, int, int] | tuple[int, int] | None = None, + mask: Image | None = None, + ) -> None: + """ + Pastes another image into this image. The box argument is either + a 2-tuple giving the upper left corner, a 4-tuple defining the + left, upper, right, and lower pixel coordinate, or None (same as + (0, 0)). See :ref:`coordinate-system`. If a 4-tuple is given, the size + of the pasted image must match the size of the region. + + If the modes don't match, the pasted image is converted to the mode of + this image (see the :py:meth:`~PIL.Image.Image.convert` method for + details). + + Instead of an image, the source can be a integer or tuple + containing pixel values. The method then fills the region + with the given color. When creating RGB images, you can + also use color strings as supported by the ImageColor module. + + If a mask is given, this method updates only the regions + indicated by the mask. You can use either "1", "L", "LA", "RGBA" + or "RGBa" images (if present, the alpha band is used as mask). + Where the mask is 255, the given image is copied as is. Where + the mask is 0, the current value is preserved. Intermediate + values will mix the two images together, including their alpha + channels if they have them. + + See :py:meth:`~PIL.Image.Image.alpha_composite` if you want to + combine images with respect to their alpha channels. + + :param im: Source image or pixel value (integer, float or tuple). + :param box: An optional 4-tuple giving the region to paste into. + If a 2-tuple is used instead, it's treated as the upper left + corner. If omitted or None, the source is pasted into the + upper left corner. + + If an image is given as the second argument and there is no + third, the box defaults to (0, 0), and the second argument + is interpreted as a mask image. + :param mask: An optional mask image. + """ + + if isImageType(box) and mask is None: + # abbreviated paste(im, mask) syntax + mask = box + box = None + + if box is None: + box = (0, 0) + + if len(box) == 2: + # upper left corner given; get size from image or mask + if isImageType(im): + size = im.size + elif isImageType(mask): + size = mask.size + else: + # FIXME: use self.size here? + msg = "cannot determine region size; use 4-item box" + raise ValueError(msg) + box += (box[0] + size[0], box[1] + size[1]) + + if isinstance(im, str): + from . import ImageColor + + im = ImageColor.getcolor(im, self.mode) + + elif isImageType(im): + im.load() + if self.mode != im.mode: + if self.mode != "RGB" or im.mode not in ("LA", "RGBA", "RGBa"): + # should use an adapter for this! + im = im.convert(self.mode) + im = im.im + + self._ensure_mutable() + + if mask: + mask.load() + self.im.paste(im, box, mask.im) + else: + self.im.paste(im, box) + + def alpha_composite(self, im, dest=(0, 0), source=(0, 0)): + """'In-place' analog of Image.alpha_composite. Composites an image + onto this image. + + :param im: image to composite over this one + :param dest: Optional 2 tuple (left, top) specifying the upper + left corner in this (destination) image. + :param source: Optional 2 (left, top) tuple for the upper left + corner in the overlay source image, or 4 tuple (left, top, right, + bottom) for the bounds of the source rectangle + + Performance Note: Not currently implemented in-place in the core layer. + """ + + if not isinstance(source, (list, tuple)): + msg = "Source must be a tuple" + raise ValueError(msg) + if not isinstance(dest, (list, tuple)): + msg = "Destination must be a tuple" + raise ValueError(msg) + if len(source) not in (2, 4): + msg = "Source must be a 2 or 4-tuple" + raise ValueError(msg) + if not len(dest) == 2: + msg = "Destination must be a 2-tuple" + raise ValueError(msg) + if min(source) < 0: + msg = "Source must be non-negative" + raise ValueError(msg) + + if len(source) == 2: + source = source + im.size + + # over image, crop if it's not the whole thing. + if source == (0, 0) + im.size: + overlay = im + else: + overlay = im.crop(source) + + # target for the paste + box = dest + (dest[0] + overlay.width, dest[1] + overlay.height) + + # destination image. don't copy if we're using the whole image. + if box == (0, 0) + self.size: + background = self + else: + background = self.crop(box) + + result = alpha_composite(background, overlay) + self.paste(result, box) + + def point(self, lut, mode: str | None = None) -> Image: + """ + Maps this image through a lookup table or function. + + :param lut: A lookup table, containing 256 (or 65536 if + self.mode=="I" and mode == "L") values per band in the + image. A function can be used instead, it should take a + single argument. The function is called once for each + possible pixel value, and the resulting table is applied to + all bands of the image. + + It may also be an :py:class:`~PIL.Image.ImagePointHandler` + object:: + + class Example(Image.ImagePointHandler): + def point(self, data): + # Return result + :param mode: Output mode (default is same as input). This can only be used if + the source image has mode "L" or "P", and the output has mode "1" or the + source image mode is "I" and the output mode is "L". + :returns: An :py:class:`~PIL.Image.Image` object. + """ + + self.load() + + if isinstance(lut, ImagePointHandler): + return lut.point(self) + + if callable(lut): + # if it isn't a list, it should be a function + if self.mode in ("I", "I;16", "F"): + # check if the function can be used with point_transform + # UNDONE wiredfool -- I think this prevents us from ever doing + # a gamma function point transform on > 8bit images. + scale, offset = _getscaleoffset(lut) + return self._new(self.im.point_transform(scale, offset)) + # for other modes, convert the function to a table + lut = [lut(i) for i in range(256)] * self.im.bands + + if self.mode == "F": + # FIXME: _imaging returns a confusing error message for this case + msg = "point operation not supported for this mode" + raise ValueError(msg) + + if mode != "F": + lut = [round(i) for i in lut] + return self._new(self.im.point(lut, mode)) + + def putalpha(self, alpha): + """ + Adds or replaces the alpha layer in this image. If the image + does not have an alpha layer, it's converted to "LA" or "RGBA". + The new layer must be either "L" or "1". + + :param alpha: The new alpha layer. This can either be an "L" or "1" + image having the same size as this image, or an integer or + other color value. + """ + + self._ensure_mutable() + + if self.mode not in ("LA", "PA", "RGBA"): + # attempt to promote self to a matching alpha mode + try: + mode = getmodebase(self.mode) + "A" + try: + self.im.setmode(mode) + except (AttributeError, ValueError) as e: + # do things the hard way + im = self.im.convert(mode) + if im.mode not in ("LA", "PA", "RGBA"): + msg = "alpha channel could not be added" + raise ValueError(msg) from e # sanity check + self.im = im + self.pyaccess = None + self._mode = self.im.mode + except KeyError as e: + msg = "illegal image mode" + raise ValueError(msg) from e + + if self.mode in ("LA", "PA"): + band = 1 + else: + band = 3 + + if isImageType(alpha): + # alpha layer + if alpha.mode not in ("1", "L"): + msg = "illegal image mode" + raise ValueError(msg) + alpha.load() + if alpha.mode == "1": + alpha = alpha.convert("L") + else: + # constant alpha + try: + self.im.fillband(band, alpha) + except (AttributeError, ValueError): + # do things the hard way + alpha = new("L", self.size, alpha) + else: + return + + self.im.putband(alpha.im, band) + + def putdata( + self, data: Sequence[float], scale: float = 1.0, offset: float = 0.0 + ) -> None: + """ + Copies pixel data from a flattened sequence object into the image. The + values should start at the upper left corner (0, 0), continue to the + end of the line, followed directly by the first value of the second + line, and so on. Data will be read until either the image or the + sequence ends. The scale and offset values are used to adjust the + sequence values: **pixel = value*scale + offset**. + + :param data: A flattened sequence object. + :param scale: An optional scale value. The default is 1.0. + :param offset: An optional offset value. The default is 0.0. + """ + + self._ensure_mutable() + + self.im.putdata(data, scale, offset) + + def putpalette(self, data, rawmode="RGB") -> None: + """ + Attaches a palette to this image. The image must be a "P", "PA", "L" + or "LA" image. + + The palette sequence must contain at most 256 colors, made up of one + integer value for each channel in the raw mode. + For example, if the raw mode is "RGB", then it can contain at most 768 + values, made up of red, green and blue values for the corresponding pixel + index in the 256 colors. + If the raw mode is "RGBA", then it can contain at most 1024 values, + containing red, green, blue and alpha values. + + Alternatively, an 8-bit string may be used instead of an integer sequence. + + :param data: A palette sequence (either a list or a string). + :param rawmode: The raw mode of the palette. Either "RGB", "RGBA", or a mode + that can be transformed to "RGB" or "RGBA" (e.g. "R", "BGR;15", "RGBA;L"). + """ + from . import ImagePalette + + if self.mode not in ("L", "LA", "P", "PA"): + msg = "illegal image mode" + raise ValueError(msg) + if isinstance(data, ImagePalette.ImagePalette): + palette = ImagePalette.raw(data.rawmode, data.palette) + else: + if not isinstance(data, bytes): + data = bytes(data) + palette = ImagePalette.raw(rawmode, data) + self._mode = "PA" if "A" in self.mode else "P" + self.palette = palette + self.palette.mode = "RGB" + self.load() # install new palette + + def putpixel(self, xy, value): + """ + Modifies the pixel at the given position. The color is given as + a single numerical value for single-band images, and a tuple for + multi-band images. In addition to this, RGB and RGBA tuples are + accepted for P and PA images. + + Note that this method is relatively slow. For more extensive changes, + use :py:meth:`~PIL.Image.Image.paste` or the :py:mod:`~PIL.ImageDraw` + module instead. + + See: + + * :py:meth:`~PIL.Image.Image.paste` + * :py:meth:`~PIL.Image.Image.putdata` + * :py:mod:`~PIL.ImageDraw` + + :param xy: The pixel coordinate, given as (x, y). See + :ref:`coordinate-system`. + :param value: The pixel value. + """ + + if self.readonly: + self._copy() + self.load() + + if self.pyaccess: + return self.pyaccess.putpixel(xy, value) + + if ( + self.mode in ("P", "PA") + and isinstance(value, (list, tuple)) + and len(value) in [3, 4] + ): + # RGB or RGBA value for a P or PA image + if self.mode == "PA": + alpha = value[3] if len(value) == 4 else 255 + value = value[:3] + value = self.palette.getcolor(value, self) + if self.mode == "PA": + value = (value, alpha) + return self.im.putpixel(xy, value) + + def remap_palette(self, dest_map, source_palette=None): + """ + Rewrites the image to reorder the palette. + + :param dest_map: A list of indexes into the original palette. + e.g. ``[1,0]`` would swap a two item palette, and ``list(range(256))`` + is the identity transform. + :param source_palette: Bytes or None. + :returns: An :py:class:`~PIL.Image.Image` object. + + """ + from . import ImagePalette + + if self.mode not in ("L", "P"): + msg = "illegal image mode" + raise ValueError(msg) + + bands = 3 + palette_mode = "RGB" + if source_palette is None: + if self.mode == "P": + self.load() + palette_mode = self.im.getpalettemode() + if palette_mode == "RGBA": + bands = 4 + source_palette = self.im.getpalette(palette_mode, palette_mode) + else: # L-mode + source_palette = bytearray(i // 3 for i in range(768)) + + palette_bytes = b"" + new_positions = [0] * 256 + + # pick only the used colors from the palette + for i, oldPosition in enumerate(dest_map): + palette_bytes += source_palette[ + oldPosition * bands : oldPosition * bands + bands + ] + new_positions[oldPosition] = i + + # replace the palette color id of all pixel with the new id + + # Palette images are [0..255], mapped through a 1 or 3 + # byte/color map. We need to remap the whole image + # from palette 1 to palette 2. New_positions is + # an array of indexes into palette 1. Palette 2 is + # palette 1 with any holes removed. + + # We're going to leverage the convert mechanism to use the + # C code to remap the image from palette 1 to palette 2, + # by forcing the source image into 'L' mode and adding a + # mapping 'L' mode palette, then converting back to 'L' + # sans palette thus converting the image bytes, then + # assigning the optimized RGB palette. + + # perf reference, 9500x4000 gif, w/~135 colors + # 14 sec prepatch, 1 sec postpatch with optimization forced. + + mapping_palette = bytearray(new_positions) + + m_im = self.copy() + m_im._mode = "P" + + m_im.palette = ImagePalette.ImagePalette( + palette_mode, palette=mapping_palette * bands + ) + # possibly set palette dirty, then + # m_im.putpalette(mapping_palette, 'L') # converts to 'P' + # or just force it. + # UNDONE -- this is part of the general issue with palettes + m_im.im.putpalette(palette_mode + ";L", m_im.palette.tobytes()) + + m_im = m_im.convert("L") + + m_im.putpalette(palette_bytes, palette_mode) + m_im.palette = ImagePalette.ImagePalette(palette_mode, palette=palette_bytes) + + if "transparency" in self.info: + try: + m_im.info["transparency"] = dest_map.index(self.info["transparency"]) + except ValueError: + if "transparency" in m_im.info: + del m_im.info["transparency"] + + return m_im + + def _get_safe_box(self, size, resample, box): + """Expands the box so it includes adjacent pixels + that may be used by resampling with the given resampling filter. + """ + filter_support = _filters_support[resample] - 0.5 + scale_x = (box[2] - box[0]) / size[0] + scale_y = (box[3] - box[1]) / size[1] + support_x = filter_support * scale_x + support_y = filter_support * scale_y + + return ( + max(0, int(box[0] - support_x)), + max(0, int(box[1] - support_y)), + min(self.size[0], math.ceil(box[2] + support_x)), + min(self.size[1], math.ceil(box[3] + support_y)), + ) + + def resize( + self, + size: tuple[int, int], + resample: int | None = None, + box: tuple[float, float, float, float] | None = None, + reducing_gap: float | None = None, + ) -> Image: + """ + Returns a resized copy of this image. + + :param size: The requested size in pixels, as a 2-tuple: + (width, height). + :param resample: An optional resampling filter. This can be + one of :py:data:`Resampling.NEAREST`, :py:data:`Resampling.BOX`, + :py:data:`Resampling.BILINEAR`, :py:data:`Resampling.HAMMING`, + :py:data:`Resampling.BICUBIC` or :py:data:`Resampling.LANCZOS`. + If the image has mode "1" or "P", it is always set to + :py:data:`Resampling.NEAREST`. If the image mode specifies a number + of bits, such as "I;16", then the default filter is + :py:data:`Resampling.NEAREST`. Otherwise, the default filter is + :py:data:`Resampling.BICUBIC`. See: :ref:`concept-filters`. + :param box: An optional 4-tuple of floats providing + the source image region to be scaled. + The values must be within (0, 0, width, height) rectangle. + If omitted or None, the entire source is used. + :param reducing_gap: Apply optimization by resizing the image + in two steps. First, reducing the image by integer times + using :py:meth:`~PIL.Image.Image.reduce`. + Second, resizing using regular resampling. The last step + changes size no less than by ``reducing_gap`` times. + ``reducing_gap`` may be None (no first step is performed) + or should be greater than 1.0. The bigger ``reducing_gap``, + the closer the result to the fair resampling. + The smaller ``reducing_gap``, the faster resizing. + With ``reducing_gap`` greater or equal to 3.0, the result is + indistinguishable from fair resampling in most cases. + The default value is None (no optimization). + :returns: An :py:class:`~PIL.Image.Image` object. + """ + + if resample is None: + type_special = ";" in self.mode + resample = Resampling.NEAREST if type_special else Resampling.BICUBIC + elif resample not in ( + Resampling.NEAREST, + Resampling.BILINEAR, + Resampling.BICUBIC, + Resampling.LANCZOS, + Resampling.BOX, + Resampling.HAMMING, + ): + msg = f"Unknown resampling filter ({resample})." + + filters = [ + f"{filter[1]} ({filter[0]})" + for filter in ( + (Resampling.NEAREST, "Image.Resampling.NEAREST"), + (Resampling.LANCZOS, "Image.Resampling.LANCZOS"), + (Resampling.BILINEAR, "Image.Resampling.BILINEAR"), + (Resampling.BICUBIC, "Image.Resampling.BICUBIC"), + (Resampling.BOX, "Image.Resampling.BOX"), + (Resampling.HAMMING, "Image.Resampling.HAMMING"), + ) + ] + msg += f" Use {', '.join(filters[:-1])} or {filters[-1]}" + raise ValueError(msg) + + if reducing_gap is not None and reducing_gap < 1.0: + msg = "reducing_gap must be 1.0 or greater" + raise ValueError(msg) + + self.load() + if box is None: + box = (0, 0) + self.size + + if self.size == size and box == (0, 0) + self.size: + return self.copy() + + if self.mode in ("1", "P"): + resample = Resampling.NEAREST + + if self.mode in ["LA", "RGBA"] and resample != Resampling.NEAREST: + im = self.convert({"LA": "La", "RGBA": "RGBa"}[self.mode]) + im = im.resize(size, resample, box) + return im.convert(self.mode) + + self.load() + + if reducing_gap is not None and resample != Resampling.NEAREST: + factor_x = int((box[2] - box[0]) / size[0] / reducing_gap) or 1 + factor_y = int((box[3] - box[1]) / size[1] / reducing_gap) or 1 + if factor_x > 1 or factor_y > 1: + reduce_box = self._get_safe_box(size, resample, box) + factor = (factor_x, factor_y) + self = ( + self.reduce(factor, box=reduce_box) + if callable(self.reduce) + else Image.reduce(self, factor, box=reduce_box) + ) + box = ( + (box[0] - reduce_box[0]) / factor_x, + (box[1] - reduce_box[1]) / factor_y, + (box[2] - reduce_box[0]) / factor_x, + (box[3] - reduce_box[1]) / factor_y, + ) + + return self._new(self.im.resize(size, resample, box)) + + def reduce( + self, + factor: int | tuple[int, int], + box: tuple[int, int, int, int] | None = None, + ) -> Image: + """ + Returns a copy of the image reduced ``factor`` times. + If the size of the image is not dividable by ``factor``, + the resulting size will be rounded up. + + :param factor: A greater than 0 integer or tuple of two integers + for width and height separately. + :param box: An optional 4-tuple of ints providing + the source image region to be reduced. + The values must be within ``(0, 0, width, height)`` rectangle. + If omitted or ``None``, the entire source is used. + """ + if not isinstance(factor, (list, tuple)): + factor = (factor, factor) + + if box is None: + box = (0, 0) + self.size + + if factor == (1, 1) and box == (0, 0) + self.size: + return self.copy() + + if self.mode in ["LA", "RGBA"]: + im = self.convert({"LA": "La", "RGBA": "RGBa"}[self.mode]) + im = im.reduce(factor, box) + return im.convert(self.mode) + + self.load() + + return self._new(self.im.reduce(factor, box)) + + def rotate( + self, + angle: float, + resample: Resampling = Resampling.NEAREST, + expand: int | bool = False, + center: tuple[int, int] | None = None, + translate: tuple[int, int] | None = None, + fillcolor: float | tuple[float, ...] | str | None = None, + ) -> Image: + """ + Returns a rotated copy of this image. This method returns a + copy of this image, rotated the given number of degrees counter + clockwise around its centre. + + :param angle: In degrees counter clockwise. + :param resample: An optional resampling filter. This can be + one of :py:data:`Resampling.NEAREST` (use nearest neighbour), + :py:data:`Resampling.BILINEAR` (linear interpolation in a 2x2 + environment), or :py:data:`Resampling.BICUBIC` (cubic spline + interpolation in a 4x4 environment). If omitted, or if the image has + mode "1" or "P", it is set to :py:data:`Resampling.NEAREST`. + See :ref:`concept-filters`. + :param expand: Optional expansion flag. If true, expands the output + image to make it large enough to hold the entire rotated image. + If false or omitted, make the output image the same size as the + input image. Note that the expand flag assumes rotation around + the center and no translation. + :param center: Optional center of rotation (a 2-tuple). Origin is + the upper left corner. Default is the center of the image. + :param translate: An optional post-rotate translation (a 2-tuple). + :param fillcolor: An optional color for area outside the rotated image. + :returns: An :py:class:`~PIL.Image.Image` object. + """ + + angle = angle % 360.0 + + # Fast paths regardless of filter, as long as we're not + # translating or changing the center. + if not (center or translate): + if angle == 0: + return self.copy() + if angle == 180: + return self.transpose(Transpose.ROTATE_180) + if angle in (90, 270) and (expand or self.width == self.height): + return self.transpose( + Transpose.ROTATE_90 if angle == 90 else Transpose.ROTATE_270 + ) + + # Calculate the affine matrix. Note that this is the reverse + # transformation (from destination image to source) because we + # want to interpolate the (discrete) destination pixel from + # the local area around the (floating) source pixel. + + # The matrix we actually want (note that it operates from the right): + # (1, 0, tx) (1, 0, cx) ( cos a, sin a, 0) (1, 0, -cx) + # (0, 1, ty) * (0, 1, cy) * (-sin a, cos a, 0) * (0, 1, -cy) + # (0, 0, 1) (0, 0, 1) ( 0, 0, 1) (0, 0, 1) + + # The reverse matrix is thus: + # (1, 0, cx) ( cos -a, sin -a, 0) (1, 0, -cx) (1, 0, -tx) + # (0, 1, cy) * (-sin -a, cos -a, 0) * (0, 1, -cy) * (0, 1, -ty) + # (0, 0, 1) ( 0, 0, 1) (0, 0, 1) (0, 0, 1) + + # In any case, the final translation may be updated at the end to + # compensate for the expand flag. + + w, h = self.size + + if translate is None: + post_trans = (0, 0) + else: + post_trans = translate + if center is None: + # FIXME These should be rounded to ints? + rotn_center = (w / 2.0, h / 2.0) + else: + rotn_center = center + + angle = -math.radians(angle) + matrix = [ + round(math.cos(angle), 15), + round(math.sin(angle), 15), + 0.0, + round(-math.sin(angle), 15), + round(math.cos(angle), 15), + 0.0, + ] + + def transform(x, y, matrix): + (a, b, c, d, e, f) = matrix + return a * x + b * y + c, d * x + e * y + f + + matrix[2], matrix[5] = transform( + -rotn_center[0] - post_trans[0], -rotn_center[1] - post_trans[1], matrix + ) + matrix[2] += rotn_center[0] + matrix[5] += rotn_center[1] + + if expand: + # calculate output size + xx = [] + yy = [] + for x, y in ((0, 0), (w, 0), (w, h), (0, h)): + x, y = transform(x, y, matrix) + xx.append(x) + yy.append(y) + nw = math.ceil(max(xx)) - math.floor(min(xx)) + nh = math.ceil(max(yy)) - math.floor(min(yy)) + + # We multiply a translation matrix from the right. Because of its + # special form, this is the same as taking the image of the + # translation vector as new translation vector. + matrix[2], matrix[5] = transform(-(nw - w) / 2.0, -(nh - h) / 2.0, matrix) + w, h = nw, nh + + return self.transform( + (w, h), Transform.AFFINE, matrix, resample, fillcolor=fillcolor + ) + + def save( + self, fp: StrOrBytesPath | IO[bytes], format: str | None = None, **params: Any + ) -> None: + """ + Saves this image under the given filename. If no format is + specified, the format to use is determined from the filename + extension, if possible. + + Keyword options can be used to provide additional instructions + to the writer. If a writer doesn't recognise an option, it is + silently ignored. The available options are described in the + :doc:`image format documentation + <../handbook/image-file-formats>` for each writer. + + You can use a file object instead of a filename. In this case, + you must always specify the format. The file object must + implement the ``seek``, ``tell``, and ``write`` + methods, and be opened in binary mode. + + :param fp: A filename (string), os.PathLike object or file object. + :param format: Optional format override. If omitted, the + 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. + :returns: None + :exception ValueError: If the output format could not be determined + from the file name. Use the format option to solve this. + :exception OSError: If the file could not be written. The file + may have been created, and may contain partial data. + """ + + filename: str | bytes = "" + open_fp = False + if is_path(fp): + filename = os.path.realpath(os.fspath(fp)) + open_fp = True + elif fp == sys.stdout: + try: + fp = sys.stdout.buffer + except AttributeError: + pass + if not filename and hasattr(fp, "name") and is_path(fp.name): + # only set the name for metadata purposes + filename = 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, ...] = () + + preinit() + + filename_ext = os.path.splitext(filename)[1].lower() + ext = filename_ext.decode() if isinstance(filename_ext, bytes) else filename_ext + + if not format: + if ext not in EXTENSION: + init() + try: + format = EXTENSION[ext] + except KeyError as e: + msg = f"unknown file extension: {ext}" + raise ValueError(msg) from e + + if format.upper() not in SAVE: + init() + if save_all: + save_handler = SAVE_ALL[format.upper()] + else: + save_handler = SAVE[format.upper()] + + created = False + if open_fp: + created = not os.path.exists(filename) + if params.get("append", False): + # Open also for reading ("+"), because TIFF save_all + # writer needs to go back and edit the written data. + fp = builtins.open(filename, "r+b") + else: + fp = builtins.open(filename, "w+b") + else: + fp = cast(IO[bytes], fp) + + try: + save_handler(self, fp, filename) + except Exception: + if open_fp: + fp.close() + if created: + try: + os.remove(filename) + except PermissionError: + pass + raise + if open_fp: + fp.close() + + def seek(self, frame: int) -> None: + """ + Seeks to the given frame in this sequence file. If you seek + beyond the end of the sequence, the method raises an + ``EOFError`` exception. When a sequence file is opened, the + library automatically seeks to frame 0. + + See :py:meth:`~PIL.Image.Image.tell`. + + If defined, :attr:`~PIL.Image.Image.n_frames` refers to the + number of available frames. + + :param frame: Frame number, starting at 0. + :exception EOFError: If the call attempts to seek beyond the end + of the sequence. + """ + + # overridden by file handlers + if frame != 0: + msg = "no more images in file" + raise EOFError(msg) + + def show(self, title: str | None = None) -> None: + """ + Displays this image. This method is mainly intended for debugging purposes. + + This method calls :py:func:`PIL.ImageShow.show` internally. You can use + :py:func:`PIL.ImageShow.register` to override its default behaviour. + + The image is first saved to a temporary file. By default, it will be in + PNG format. + + On Unix, the image is then opened using the **xdg-open**, **display**, + **gm**, **eog** or **xv** utility, depending on which one can be found. + + On macOS, the image is opened with the native Preview application. + + On Windows, the image is opened with the standard PNG display utility. + + :param title: Optional title to use for the image window, where possible. + """ + + _show(self, title=title) + + def split(self) -> tuple[Image, ...]: + """ + Split this image into individual bands. This method returns a + tuple of individual image bands from an image. For example, + splitting an "RGB" image creates three new images each + containing a copy of one of the original bands (red, green, + blue). + + If you need only one band, :py:meth:`~PIL.Image.Image.getchannel` + method can be more convenient and faster. + + :returns: A tuple containing bands. + """ + + self.load() + if self.im.bands == 1: + return (self.copy(),) + return tuple(map(self._new, self.im.split())) + + def getchannel(self, channel: int | str) -> Image: + """ + Returns an image containing a single channel of the source image. + + :param channel: What channel to return. Could be index + (0 for "R" channel of "RGB") or channel name + ("A" for alpha channel of "RGBA"). + :returns: An image in "L" mode. + + .. versionadded:: 4.3.0 + """ + self.load() + + if isinstance(channel, str): + try: + channel = self.getbands().index(channel) + except ValueError as e: + msg = f'The image has no channel "{channel}"' + raise ValueError(msg) from e + + return self._new(self.im.getband(channel)) + + def tell(self) -> int: + """ + Returns the current frame number. See :py:meth:`~PIL.Image.Image.seek`. + + If defined, :attr:`~PIL.Image.Image.n_frames` refers to the + number of available frames. + + :returns: Frame number, starting with 0. + """ + return 0 + + def thumbnail( + self, + size: tuple[float, float], + resample: Resampling = Resampling.BICUBIC, + reducing_gap: float = 2.0, + ) -> None: + """ + Make this image into a thumbnail. This method modifies the + image to contain a thumbnail version of itself, no larger than + the given size. This method calculates an appropriate thumbnail + size to preserve the aspect of the image, calls the + :py:meth:`~PIL.Image.Image.draft` method to configure the file reader + (where applicable), and finally resizes the image. + + Note that this function modifies the :py:class:`~PIL.Image.Image` + object in place. If you need to use the full resolution image as well, + apply this method to a :py:meth:`~PIL.Image.Image.copy` of the original + image. + + :param size: The requested size in pixels, as a 2-tuple: + (width, height). + :param resample: Optional resampling filter. This can be one + of :py:data:`Resampling.NEAREST`, :py:data:`Resampling.BOX`, + :py:data:`Resampling.BILINEAR`, :py:data:`Resampling.HAMMING`, + :py:data:`Resampling.BICUBIC` or :py:data:`Resampling.LANCZOS`. + If omitted, it defaults to :py:data:`Resampling.BICUBIC`. + (was :py:data:`Resampling.NEAREST` prior to version 2.5.0). + See: :ref:`concept-filters`. + :param reducing_gap: Apply optimization by resizing the image + in two steps. First, reducing the image by integer times + using :py:meth:`~PIL.Image.Image.reduce` or + :py:meth:`~PIL.Image.Image.draft` for JPEG images. + Second, resizing using regular resampling. The last step + changes size no less than by ``reducing_gap`` times. + ``reducing_gap`` may be None (no first step is performed) + or should be greater than 1.0. The bigger ``reducing_gap``, + the closer the result to the fair resampling. + The smaller ``reducing_gap``, the faster resizing. + With ``reducing_gap`` greater or equal to 3.0, the result is + indistinguishable from fair resampling in most cases. + The default value is 2.0 (very close to fair resampling + while still being faster in many cases). + :returns: None + """ + + provided_size = tuple(map(math.floor, size)) + + def preserve_aspect_ratio() -> tuple[int, int] | None: + def round_aspect(number, key): + return max(min(math.floor(number), math.ceil(number), key=key), 1) + + x, y = provided_size + if x >= self.width and y >= self.height: + return None + + aspect = self.width / self.height + if x / y >= aspect: + x = round_aspect(y * aspect, key=lambda n: abs(aspect - n / y)) + else: + y = round_aspect( + x / aspect, key=lambda n: 0 if n == 0 else abs(aspect - x / n) + ) + return x, y + + box = None + if reducing_gap is not None: + preserved_size = preserve_aspect_ratio() + if preserved_size is None: + return + size = preserved_size + + res = self.draft( + None, (int(size[0] * reducing_gap), int(size[1] * reducing_gap)) + ) + if res is not None: + box = res[1] + if box is None: + self.load() + + # load() may have changed the size of the image + preserved_size = preserve_aspect_ratio() + if preserved_size is None: + return + size = preserved_size + + if self.size != size: + im = self.resize(size, resample, box=box, reducing_gap=reducing_gap) + + self.im = im.im + self._size = size + self._mode = self.im.mode + + self.readonly = 0 + self.pyaccess = None + + # FIXME: the different transform methods need further explanation + # instead of bloating the method docs, add a separate chapter. + def transform( + self, + size: tuple[int, int], + method: Transform | ImageTransformHandler | SupportsGetData, + data: Sequence[Any] | None = None, + resample: int = Resampling.NEAREST, + fill: int = 1, + fillcolor: float | tuple[float, ...] | str | None = None, + ) -> Image: + """ + Transforms this image. This method creates a new image with the + given size, and the same mode as the original, and copies data + to the new image using the given transform. + + :param size: The output size in pixels, as a 2-tuple: + (width, height). + :param method: The transformation method. This is one of + :py:data:`Transform.EXTENT` (cut out a rectangular subregion), + :py:data:`Transform.AFFINE` (affine transform), + :py:data:`Transform.PERSPECTIVE` (perspective transform), + :py:data:`Transform.QUAD` (map a quadrilateral to a rectangle), or + :py:data:`Transform.MESH` (map a number of source quadrilaterals + in one operation). + + It may also be an :py:class:`~PIL.Image.ImageTransformHandler` + object:: + + class Example(Image.ImageTransformHandler): + def transform(self, size, data, resample, fill=1): + # Return result + + Implementations of :py:class:`~PIL.Image.ImageTransformHandler` + for some of the :py:class:`Transform` methods are provided + in :py:mod:`~PIL.ImageTransform`. + + It may also be an object with a ``method.getdata`` method + that returns a tuple supplying new ``method`` and ``data`` values:: + + class Example: + def getdata(self): + method = Image.Transform.EXTENT + data = (0, 0, 100, 100) + return method, data + :param data: Extra data to the transformation method. + :param resample: Optional resampling filter. It can be one of + :py:data:`Resampling.NEAREST` (use nearest neighbour), + :py:data:`Resampling.BILINEAR` (linear interpolation in a 2x2 + environment), or :py:data:`Resampling.BICUBIC` (cubic spline + interpolation in a 4x4 environment). If omitted, or if the image + has mode "1" or "P", it is set to :py:data:`Resampling.NEAREST`. + See: :ref:`concept-filters`. + :param fill: If ``method`` is an + :py:class:`~PIL.Image.ImageTransformHandler` object, this is one of + the arguments passed to it. Otherwise, it is unused. + :param fillcolor: Optional fill color for the area outside the + transform in the output image. + :returns: An :py:class:`~PIL.Image.Image` object. + """ + + if self.mode in ("LA", "RGBA") and resample != Resampling.NEAREST: + return ( + self.convert({"LA": "La", "RGBA": "RGBa"}[self.mode]) + .transform(size, method, data, resample, fill, fillcolor) + .convert(self.mode) + ) + + if isinstance(method, ImageTransformHandler): + return method.transform(size, self, resample=resample, fill=fill) + + if hasattr(method, "getdata"): + # compatibility w. old-style transform objects + method, data = method.getdata() + + if data is None: + msg = "missing method data" + raise ValueError(msg) + + im = new(self.mode, size, fillcolor) + if self.mode == "P" and self.palette: + im.palette = self.palette.copy() + im.info = self.info.copy() + if method == Transform.MESH: + # list of quads + for box, quad in data: + im.__transformer( + box, self, Transform.QUAD, quad, resample, fillcolor is None + ) + else: + im.__transformer( + (0, 0) + size, self, method, data, resample, fillcolor is None + ) + + return im + + def __transformer( + self, box, image, method, data, resample=Resampling.NEAREST, fill=1 + ): + w = box[2] - box[0] + h = box[3] - box[1] + + if method == Transform.AFFINE: + data = data[:6] + + elif method == Transform.EXTENT: + # convert extent to an affine transform + x0, y0, x1, y1 = data + xs = (x1 - x0) / w + ys = (y1 - y0) / h + method = Transform.AFFINE + data = (xs, 0, x0, 0, ys, y0) + + elif method == Transform.PERSPECTIVE: + data = data[:8] + + elif method == Transform.QUAD: + # quadrilateral warp. data specifies the four corners + # given as NW, SW, SE, and NE. + nw = data[:2] + sw = data[2:4] + se = data[4:6] + ne = data[6:8] + x0, y0 = nw + As = 1.0 / w + At = 1.0 / h + data = ( + x0, + (ne[0] - x0) * As, + (sw[0] - x0) * At, + (se[0] - sw[0] - ne[0] + x0) * As * At, + y0, + (ne[1] - y0) * As, + (sw[1] - y0) * At, + (se[1] - sw[1] - ne[1] + y0) * As * At, + ) + + else: + msg = "unknown transformation method" + raise ValueError(msg) + + if resample not in ( + Resampling.NEAREST, + Resampling.BILINEAR, + Resampling.BICUBIC, + ): + if resample in (Resampling.BOX, Resampling.HAMMING, Resampling.LANCZOS): + msg = { + Resampling.BOX: "Image.Resampling.BOX", + Resampling.HAMMING: "Image.Resampling.HAMMING", + Resampling.LANCZOS: "Image.Resampling.LANCZOS", + }[resample] + f" ({resample}) cannot be used." + else: + msg = f"Unknown resampling filter ({resample})." + + filters = [ + f"{filter[1]} ({filter[0]})" + for filter in ( + (Resampling.NEAREST, "Image.Resampling.NEAREST"), + (Resampling.BILINEAR, "Image.Resampling.BILINEAR"), + (Resampling.BICUBIC, "Image.Resampling.BICUBIC"), + ) + ] + msg += f" Use {', '.join(filters[:-1])} or {filters[-1]}" + raise ValueError(msg) + + image.load() + + self.load() + + if image.mode in ("1", "P"): + resample = Resampling.NEAREST + + self.im.transform(box, image.im, method, data, resample, fill) + + def transpose(self, method: Transpose) -> Image: + """ + Transpose image (flip or rotate in 90 degree steps) + + :param method: One of :py:data:`Transpose.FLIP_LEFT_RIGHT`, + :py:data:`Transpose.FLIP_TOP_BOTTOM`, :py:data:`Transpose.ROTATE_90`, + :py:data:`Transpose.ROTATE_180`, :py:data:`Transpose.ROTATE_270`, + :py:data:`Transpose.TRANSPOSE` or :py:data:`Transpose.TRANSVERSE`. + :returns: Returns a flipped or rotated copy of this image. + """ + + self.load() + return self._new(self.im.transpose(method)) + + def effect_spread(self, distance: int) -> Image: + """ + Randomly spread pixels in an image. + + :param distance: Distance to spread pixels. + """ + self.load() + return self._new(self.im.effect_spread(distance)) + + def toqimage(self): + """Returns a QImage copy of this image""" + from . import ImageQt + + if not ImageQt.qt_is_installed: + msg = "Qt bindings are not installed" + raise ImportError(msg) + return ImageQt.toqimage(self) + + def toqpixmap(self): + """Returns a QPixmap copy of this image""" + from . import ImageQt + + if not ImageQt.qt_is_installed: + msg = "Qt bindings are not installed" + raise ImportError(msg) + return ImageQt.toqpixmap(self) + + +# -------------------------------------------------------------------- +# Abstract handlers. + + +class ImagePointHandler: + """ + Used as a mixin by point transforms + (for use with :py:meth:`~PIL.Image.Image.point`) + """ + + @abc.abstractmethod + def point(self, im: Image) -> Image: + pass + + +class ImageTransformHandler: + """ + Used as a mixin by geometry transforms + (for use with :py:meth:`~PIL.Image.Image.transform`) + """ + + @abc.abstractmethod + def transform( + self, + size: tuple[int, int], + image: Image, + **options: Any, + ) -> Image: + pass + + +# -------------------------------------------------------------------- +# Factories + +# +# Debugging + + +def _wedge() -> Image: + """Create grayscale wedge (for debugging only)""" + + return Image()._new(core.wedge("L")) + + +def _check_size(size): + """ + Common check to enforce type and sanity check on size tuples + + :param size: Should be a 2 tuple of (width, height) + :returns: True, or raises a ValueError + """ + + if not isinstance(size, (list, tuple)): + msg = "Size must be a tuple" + raise ValueError(msg) + if len(size) != 2: + msg = "Size must be a tuple of length 2" + raise ValueError(msg) + if size[0] < 0 or size[1] < 0: + msg = "Width and height must be >= 0" + raise ValueError(msg) + + return True + + +def new( + mode: str, size: tuple[int, int], color: float | tuple[float, ...] | str | None = 0 +) -> Image: + """ + Creates a new image with the given mode and size. + + :param mode: The mode to use for the new image. See: + :ref:`concept-modes`. + :param size: A 2-tuple, containing (width, height) in pixels. + :param color: What color to use for the image. Default is black. + If given, this should be a single integer or floating point value + for single-band modes, and a tuple for multi-band modes (one value + per band). When creating RGB or HSV images, you can also use color + strings as supported by the ImageColor module. If the color is + None, the image is not initialised. + :returns: An :py:class:`~PIL.Image.Image` object. + """ + + if mode in ("BGR;15", "BGR;16", "BGR;24"): + deprecate(mode, 12) + + _check_size(size) + + if color is None: + # don't initialize + return Image()._new(core.new(mode, size)) + + if isinstance(color, str): + # css3-style specifier + + from . import ImageColor + + color = ImageColor.getcolor(color, mode) + + im = Image() + if ( + mode == "P" + and isinstance(color, (list, tuple)) + and all(isinstance(i, int) for i in color) + ): + color_ints: tuple[int, ...] = cast(Tuple[int, ...], tuple(color)) + if len(color_ints) == 3 or len(color_ints) == 4: + # RGB or RGBA value for a P image + from . import ImagePalette + + im.palette = ImagePalette.ImagePalette() + color = im.palette.getcolor(color_ints) + return im._new(core.fill(mode, size, color)) + + +def frombytes(mode, size, data, decoder_name: str = "raw", *args) -> Image: + """ + Creates a copy of an image memory from pixel data in a buffer. + + In its simplest form, this function takes three arguments + (mode, size, and unpacked pixel data). + + You can also use any pixel decoder supported by PIL. For more + information on available decoders, see the section + :ref:`Writing Your Own File Codec `. + + Note that this function decodes pixel data only, not entire images. + If you have an entire image in a string, wrap it in a + :py:class:`~io.BytesIO` object, and use :py:func:`~PIL.Image.open` to load + it. + + :param mode: The image mode. See: :ref:`concept-modes`. + :param size: The image size. + :param data: A byte buffer containing raw data for the given mode. + :param decoder_name: What decoder to use. + :param args: Additional parameters for the given decoder. + :returns: An :py:class:`~PIL.Image.Image` object. + """ + + _check_size(size) + + im = new(mode, size) + if im.width != 0 and im.height != 0: + # may pass tuple instead of argument list + if len(args) == 1 and isinstance(args[0], tuple): + args = args[0] + + if decoder_name == "raw" and args == (): + args = mode + + im.frombytes(data, decoder_name, args) + return im + + +def frombuffer(mode: str, size, data, decoder_name: str = "raw", *args) -> Image: + """ + Creates an image memory referencing pixel data in a byte buffer. + + This function is similar to :py:func:`~PIL.Image.frombytes`, but uses data + in the byte buffer, where possible. This means that changes to the + original buffer object are reflected in this image). Not all modes can + share memory; supported modes include "L", "RGBX", "RGBA", and "CMYK". + + Note that this function decodes pixel data only, not entire images. + If you have an entire image file in a string, wrap it in a + :py:class:`~io.BytesIO` object, and use :py:func:`~PIL.Image.open` to load it. + + The default parameters used for the "raw" decoder differs from that used for + :py:func:`~PIL.Image.frombytes`. This is a bug, and will probably be fixed in a + future release. The current release issues a warning if you do this; to disable + the warning, you should provide the full set of parameters. See below for details. + + :param mode: The image mode. See: :ref:`concept-modes`. + :param size: The image size. + :param data: A bytes or other buffer object containing raw + data for the given mode. + :param decoder_name: What decoder to use. + :param args: Additional parameters for the given decoder. For the + default encoder ("raw"), it's recommended that you provide the + full set of parameters:: + + frombuffer(mode, size, data, "raw", mode, 0, 1) + + :returns: An :py:class:`~PIL.Image.Image` object. + + .. versionadded:: 1.1.4 + """ + + _check_size(size) + + # may pass tuple instead of argument list + if len(args) == 1 and isinstance(args[0], tuple): + args = args[0] + + if decoder_name == "raw": + if args == (): + args = mode, 0, 1 + if args[0] in _MAPMODES: + im = new(mode, (0, 0)) + im = im._new(core.map_buffer(data, size, decoder_name, 0, args)) + if mode == "P": + from . import ImagePalette + + im.palette = ImagePalette.ImagePalette("RGB", im.im.getpalette("RGB")) + im.readonly = 1 + return im + + return frombytes(mode, size, data, decoder_name, args) + + +class SupportsArrayInterface(Protocol): + """ + An object that has an ``__array_interface__`` dictionary. + """ + + @property + def __array_interface__(self) -> dict[str, Any]: + raise NotImplementedError() + + +def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image: + """ + Creates an image memory from an object exporting the array interface + (using the buffer protocol):: + + from PIL import Image + import numpy as np + a = np.zeros((5, 5)) + im = Image.fromarray(a) + + If ``obj`` is not contiguous, then the ``tobytes`` method is called + and :py:func:`~PIL.Image.frombuffer` is used. + + In the case of NumPy, be aware that Pillow modes do not always correspond + to NumPy dtypes. Pillow modes only offer 1-bit pixels, 8-bit pixels, + 32-bit signed integer pixels, and 32-bit floating point pixels. + + Pillow images can also be converted to arrays:: + + from PIL import Image + import numpy as np + im = Image.open("hopper.jpg") + a = np.asarray(im) + + When converting Pillow images to arrays however, only pixel values are + transferred. This means that P and PA mode images will lose their palette. + + :param obj: Object with array interface + :param mode: Optional mode to use when reading ``obj``. Will be determined from + type if ``None``. + + This will not be used to convert the data after reading, but will be used to + change how the data is read:: + + from PIL import Image + import numpy as np + a = np.full((1, 1), 300) + im = Image.fromarray(a, mode="L") + im.getpixel((0, 0)) # 44 + im = Image.fromarray(a, mode="RGB") + im.getpixel((0, 0)) # (44, 1, 0) + + See: :ref:`concept-modes` for general information about modes. + :returns: An image object. + + .. versionadded:: 1.1.6 + """ + arr = obj.__array_interface__ + shape = arr["shape"] + ndim = len(shape) + strides = arr.get("strides", None) + if mode is None: + try: + typekey = (1, 1) + shape[2:], arr["typestr"] + except KeyError as e: + msg = "Cannot handle this data type" + raise TypeError(msg) from e + try: + mode, rawmode = _fromarray_typemap[typekey] + except KeyError as e: + typekey_shape, typestr = typekey + msg = f"Cannot handle this data type: {typekey_shape}, {typestr}" + raise TypeError(msg) from e + else: + rawmode = mode + if mode in ["1", "L", "I", "P", "F"]: + ndmax = 2 + elif mode == "RGB": + ndmax = 3 + else: + ndmax = 4 + if ndim > ndmax: + msg = f"Too many dimensions: {ndim} > {ndmax}." + raise ValueError(msg) + + size = 1 if ndim == 1 else shape[1], shape[0] + if strides is not None: + if hasattr(obj, "tobytes"): + obj = obj.tobytes() + elif hasattr(obj, "tostring"): + obj = obj.tostring() + else: + msg = "'strides' requires either tobytes() or tostring()" + raise ValueError(msg) + + return frombuffer(mode, size, obj, "raw", rawmode, 0, 1) + + +def fromqimage(im): + """Creates an image instance from a QImage image""" + from . import ImageQt + + if not ImageQt.qt_is_installed: + msg = "Qt bindings are not installed" + raise ImportError(msg) + return ImageQt.fromqimage(im) + + +def fromqpixmap(im): + """Creates an image instance from a QPixmap image""" + from . import ImageQt + + if not ImageQt.qt_is_installed: + msg = "Qt bindings are not installed" + raise ImportError(msg) + return ImageQt.fromqpixmap(im) + + +_fromarray_typemap = { + # (shape, typestr) => mode, rawmode + # first two members of shape are set to one + ((1, 1), "|b1"): ("1", "1;8"), + ((1, 1), "|u1"): ("L", "L"), + ((1, 1), "|i1"): ("I", "I;8"), + ((1, 1), "u2"): ("I", "I;16B"), + ((1, 1), "i2"): ("I", "I;16BS"), + ((1, 1), "u4"): ("I", "I;32B"), + ((1, 1), "i4"): ("I", "I;32BS"), + ((1, 1), "f4"): ("F", "F;32BF"), + ((1, 1), "f8"): ("F", "F;64BF"), + ((1, 1, 2), "|u1"): ("LA", "LA"), + ((1, 1, 3), "|u1"): ("RGB", "RGB"), + ((1, 1, 4), "|u1"): ("RGBA", "RGBA"), + # shortcuts: + ((1, 1), f"{_ENDIAN}i4"): ("I", "I"), + ((1, 1), f"{_ENDIAN}f4"): ("F", "F"), +} + + +def _decompression_bomb_check(size: tuple[int, int]) -> None: + if MAX_IMAGE_PIXELS is None: + return + + pixels = max(1, size[0]) * max(1, size[1]) + + if pixels > 2 * MAX_IMAGE_PIXELS: + msg = ( + f"Image size ({pixels} pixels) exceeds limit of {2 * MAX_IMAGE_PIXELS} " + "pixels, could be decompression bomb DOS attack." + ) + raise DecompressionBombError(msg) + + if pixels > MAX_IMAGE_PIXELS: + warnings.warn( + f"Image size ({pixels} pixels) exceeds limit of {MAX_IMAGE_PIXELS} pixels, " + "could be decompression bomb DOS attack.", + DecompressionBombWarning, + ) + + +def open( + fp: StrOrBytesPath | IO[bytes], + mode: Literal["r"] = "r", + formats: list[str] | tuple[str, ...] | None = None, +) -> ImageFile.ImageFile: + """ + Opens and identifies the given image file. + + This is a lazy operation; this function identifies the file, but + the file remains open and the actual image data is not read from + the file until you try to process the data (or call the + :py:meth:`~PIL.Image.Image.load` method). See + :py:func:`~PIL.Image.new`. See :ref:`file-handling`. + + :param fp: A filename (string), os.PathLike object or a file object. + The file object must implement ``file.read``, + ``file.seek``, and ``file.tell`` methods, + and be opened in binary mode. The file object will also seek to zero + before reading. + :param mode: The mode. If given, this argument must be "r". + :param formats: A list or tuple of formats to attempt to load the file in. + This can be used to restrict the set of formats checked. + Pass ``None`` to try all supported formats. You can print the set of + available formats by running ``python3 -m PIL`` or using + the :py:func:`PIL.features.pilinfo` function. + :returns: An :py:class:`~PIL.Image.Image` object. + :exception FileNotFoundError: If the file cannot be found. + :exception PIL.UnidentifiedImageError: If the image cannot be opened and + identified. + :exception ValueError: If the ``mode`` is not "r", or if a ``StringIO`` + instance is used for ``fp``. + :exception TypeError: If ``formats`` is not ``None``, a list or a tuple. + """ + + if mode != "r": + msg = f"bad mode {repr(mode)}" # type: ignore[unreachable] + raise ValueError(msg) + elif isinstance(fp, io.StringIO): + msg = ( # type: ignore[unreachable] + "StringIO cannot be used to open an image. " + "Binary data must be used instead." + ) + raise ValueError(msg) + + if formats is None: + formats = ID + elif not isinstance(formats, (list, tuple)): + msg = "formats must be a list or tuple" # type: ignore[unreachable] + raise TypeError(msg) + + exclusive_fp = False + filename: str | bytes = "" + if is_path(fp): + filename = os.path.realpath(os.fspath(fp)) + + if filename: + fp = builtins.open(filename, "rb") + exclusive_fp = True + else: + fp = cast(IO[bytes], fp) + + try: + fp.seek(0) + except (AttributeError, io.UnsupportedOperation): + fp = io.BytesIO(fp.read()) + exclusive_fp = True + + prefix = fp.read(16) + + preinit() + + accept_warnings: list[str] = [] + + def _open_core( + fp: IO[bytes], + filename: str | bytes, + prefix: bytes, + formats: list[str] | tuple[str, ...], + ) -> ImageFile.ImageFile | None: + for i in formats: + i = i.upper() + if i not in OPEN: + init() + try: + factory, accept = OPEN[i] + result = not accept or accept(prefix) + if isinstance(result, str): + accept_warnings.append(result) + elif result: + fp.seek(0) + im = factory(fp, filename) + _decompression_bomb_check(im.size) + return im + except (SyntaxError, IndexError, TypeError, struct.error): + # Leave disabled by default, spams the logs with image + # opening failures that are entirely expected. + # logger.debug("", exc_info=True) + continue + except BaseException: + if exclusive_fp: + fp.close() + raise + return None + + im = _open_core(fp, filename, prefix, formats) + + if im is None and formats is ID: + checked_formats = ID.copy() + if init(): + im = _open_core( + fp, + filename, + prefix, + tuple(format for format in formats if format not in checked_formats), + ) + + if im: + im._exclusive_fp = exclusive_fp + return im + + if exclusive_fp: + fp.close() + for message in accept_warnings: + warnings.warn(message) + msg = "cannot identify image file %r" % (filename if filename else fp) + raise UnidentifiedImageError(msg) + + +# +# Image processing. + + +def alpha_composite(im1: Image, im2: Image) -> Image: + """ + Alpha composite im2 over im1. + + :param im1: The first image. Must have mode RGBA. + :param im2: The second image. Must have mode RGBA, and the same size as + the first image. + :returns: An :py:class:`~PIL.Image.Image` object. + """ + + im1.load() + im2.load() + return im1._new(core.alpha_composite(im1.im, im2.im)) + + +def blend(im1: Image, im2: Image, alpha: float) -> Image: + """ + Creates a new image by interpolating between two input images, using + a constant alpha:: + + out = image1 * (1.0 - alpha) + image2 * alpha + + :param im1: The first image. + :param im2: The second image. Must have the same mode and size as + the first image. + :param alpha: The interpolation alpha factor. If alpha is 0.0, a + copy of the first image is returned. If alpha is 1.0, a copy of + the second image is returned. There are no restrictions on the + alpha value. If necessary, the result is clipped to fit into + the allowed output range. + :returns: An :py:class:`~PIL.Image.Image` object. + """ + + im1.load() + im2.load() + return im1._new(core.blend(im1.im, im2.im, alpha)) + + +def composite(image1: Image, image2: Image, mask: Image) -> Image: + """ + Create composite image by blending images using a transparency mask. + + :param image1: The first image. + :param image2: The second image. Must have the same mode and + size as the first image. + :param mask: A mask image. This image can have mode + "1", "L", or "RGBA", and must have the same size as the + other two images. + """ + + image = image2.copy() + image.paste(image1, None, mask) + return image + + +def eval(image, *args): + """ + Applies the function (which should take one argument) to each pixel + in the given image. If the image has more than one band, the same + function is applied to each band. Note that the function is + evaluated once for each possible pixel value, so you cannot use + random components or other generators. + + :param image: The input image. + :param function: A function object, taking one integer argument. + :returns: An :py:class:`~PIL.Image.Image` object. + """ + + return image.point(args[0]) + + +def merge(mode: str, bands: Sequence[Image]) -> Image: + """ + Merge a set of single band images into a new multiband image. + + :param mode: The mode to use for the output image. See: + :ref:`concept-modes`. + :param bands: A sequence containing one single-band image for + each band in the output image. All bands must have the + same size. + :returns: An :py:class:`~PIL.Image.Image` object. + """ + + if getmodebands(mode) != len(bands) or "*" in mode: + branches["1"] = True + msg = "wrong number of bands" + raise ValueError(msg) + for band in bands[1:]: + branches["2"] = True + if band.mode != getmodetype(mode): + branches["3"] = True + msg = "mode mismatch" + raise ValueError(msg) + if band.size != bands[0].size: + branches["4"] = True + msg = "size mismatch" + raise ValueError(msg) + for band in bands: + branches["5"] = True + band.load() + return bands[0]._new(core.merge(mode, *[b.im for b in bands])) + + +# -------------------------------------------------------------------- +# Plugin registry + + +def register_open( + id, + factory: Callable[[IO[bytes], str | bytes], ImageFile.ImageFile], + accept: Callable[[bytes], bool | str] | None = None, +) -> None: + """ + Register an image file plugin. This function should not be used + in application code. + + :param id: An image format identifier. + :param factory: An image file factory method. + :param accept: An optional function that can be used to quickly + reject images having another format. + """ + id = id.upper() + if id not in ID: + ID.append(id) + OPEN[id] = factory, accept + + +def register_mime(id: str, mimetype: str) -> None: + """ + Registers an image MIME type by populating ``Image.MIME``. This function + should not be used in application code. + + ``Image.MIME`` provides a mapping from image format identifiers to mime + formats, but :py:meth:`~PIL.ImageFile.ImageFile.get_format_mimetype` can + provide a different result for specific images. + + :param id: An image format identifier. + :param mimetype: The image MIME type for this format. + """ + MIME[id.upper()] = mimetype + + +def register_save( + id: str, driver: Callable[[Image, IO[bytes], str | bytes], None] +) -> None: + """ + Registers an image save function. This function should not be + used in application code. + + :param id: An image format identifier. + :param driver: A function to save images in this format. + """ + SAVE[id.upper()] = driver + + +def register_save_all( + id: str, driver: Callable[[Image, IO[bytes], str | bytes], None] +) -> None: + """ + Registers an image function to save all the frames + of a multiframe format. This function should not be + used in application code. + + :param id: An image format identifier. + :param driver: A function to save images in this format. + """ + SAVE_ALL[id.upper()] = driver + + +def register_extension(id: str, extension: str) -> None: + """ + Registers an image extension. This function should not be + used in application code. + + :param id: An image format identifier. + :param extension: An extension used for this format. + """ + EXTENSION[extension.lower()] = id.upper() + + +def register_extensions(id: str, extensions: list[str]) -> None: + """ + Registers image extensions. This function should not be + used in application code. + + :param id: An image format identifier. + :param extensions: A list of extensions used for this format. + """ + for extension in extensions: + register_extension(id, extension) + + +def registered_extensions() -> dict[str, str]: + """ + Returns a dictionary containing all file extensions belonging + to registered plugins + """ + init() + return EXTENSION + + +def register_decoder(name: str, decoder: type[ImageFile.PyDecoder]) -> None: + """ + Registers an image decoder. This function should not be + used in application code. + + :param name: The name of the decoder + :param decoder: An ImageFile.PyDecoder object + + .. versionadded:: 4.1.0 + """ + DECODERS[name] = decoder + + +def register_encoder(name: str, encoder: type[ImageFile.PyEncoder]) -> None: + """ + Registers an image encoder. This function should not be + used in application code. + + :param name: The name of the encoder + :param encoder: An ImageFile.PyEncoder object + + .. versionadded:: 4.1.0 + """ + ENCODERS[name] = encoder + + +# -------------------------------------------------------------------- +# Simple display support. + + +def _show(image: Image, **options: Any) -> None: + from . import ImageShow + + ImageShow.show(image, **options) + + +# -------------------------------------------------------------------- +# Effects + + +def effect_mandelbrot( + size: tuple[int, int], extent: tuple[int, int, int, int], quality: int +) -> Image: + """ + Generate a Mandelbrot set covering the given extent. + + :param size: The requested size in pixels, as a 2-tuple: + (width, height). + :param extent: The extent to cover, as a 4-tuple: + (x0, y0, x1, y1). + :param quality: Quality. + """ + return Image()._new(core.effect_mandelbrot(size, extent, quality)) + + +def effect_noise(size: tuple[int, int], sigma: float) -> Image: + """ + Generate Gaussian noise centered around 128. + + :param size: The requested size in pixels, as a 2-tuple: + (width, height). + :param sigma: Standard deviation of noise. + """ + return Image()._new(core.effect_noise(size, sigma)) + + +def linear_gradient(mode: str) -> Image: + """ + Generate 256x256 linear gradient from black to white, top to bottom. + + :param mode: Input mode. + """ + return Image()._new(core.linear_gradient(mode)) + + +def radial_gradient(mode: str) -> Image: + """ + Generate 256x256 radial gradient from black to white, centre to edge. + + :param mode: Input mode. + """ + return Image()._new(core.radial_gradient(mode)) + + +# -------------------------------------------------------------------- +# Resources + + +def _apply_env_variables(env=None) -> None: + if env is None: + env = os.environ + + for var_name, setter in [ + ("PILLOW_ALIGNMENT", core.set_alignment), + ("PILLOW_BLOCK_SIZE", core.set_block_size), + ("PILLOW_BLOCKS_MAX", core.set_blocks_max), + ]: + if var_name not in env: + continue + + var = env[var_name].lower() + + units = 1 + for postfix, mul in [("k", 1024), ("m", 1024 * 1024)]: + if var.endswith(postfix): + units = mul + var = var[: -len(postfix)] + + try: + var = int(var) * units + except ValueError: + warnings.warn(f"{var_name} is not int") + continue + + try: + setter(var) + except ValueError as e: + warnings.warn(f"{var_name}: {e}") + + +_apply_env_variables() +atexit.register(core.clear_cache) + + +if TYPE_CHECKING: + _ExifBase = MutableMapping[int, Any] +else: + _ExifBase = MutableMapping + + +class Exif(_ExifBase): + """ + This class provides read and write access to EXIF image data:: + + from PIL import Image + im = Image.open("exif.png") + exif = im.getexif() # Returns an instance of this class + + Information can be read and written, iterated over or deleted:: + + print(exif[274]) # 1 + exif[274] = 2 + for k, v in exif.items(): + print("Tag", k, "Value", v) # Tag 274 Value 2 + del exif[274] + + To access information beyond IFD0, :py:meth:`~PIL.Image.Exif.get_ifd` + returns a dictionary:: + + from PIL import ExifTags + im = Image.open("exif_gps.jpg") + exif = im.getexif() + gps_ifd = exif.get_ifd(ExifTags.IFD.GPSInfo) + print(gps_ifd) + + 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:: + + print(exif[ExifTags.Base.Software]) # PIL + print(gps_ifd[ExifTags.GPS.GPSDateStamp]) # 1999:99:99 99:99:99 + """ + + endian = None + bigtiff = False + _loaded = False + + def __init__(self): + self._data = {} + self._hidden_data = {} + self._ifds = {} + self._info = None + self._loaded_exif = None + + def _fixup(self, value): + try: + if len(value) == 1 and isinstance(value, tuple): + return value[0] + except Exception: + pass + return value + + def _fixup_dict(self, src_dict): + # Helper function + # returns a dict with any single item tuples/lists as individual values + return {k: self._fixup(v) for k, v in src_dict.items()} + + def _get_ifd_dict(self, offset, group=None): + try: + # an offset pointer to the location of the nested embedded IFD. + # It should be a long, but may be corrupted. + self.fp.seek(offset) + except (KeyError, TypeError): + pass + else: + from . import TiffImagePlugin + + info = TiffImagePlugin.ImageFileDirectory_v2(self.head, group=group) + info.load(self.fp) + return self._fixup_dict(info) + + def _get_head(self): + version = b"\x2B" if self.bigtiff else b"\x2A" + if self.endian == "<": + head = b"II" + version + b"\x00" + o32le(8) + else: + head = b"MM\x00" + version + o32be(8) + if self.bigtiff: + head += o32le(8) if self.endian == "<" else o32be(8) + head += b"\x00\x00\x00\x00" + return head + + def load(self, data): + # Extract EXIF information. This is highly experimental, + # and is likely to be replaced with something better in a future + # version. + + # The EXIF record consists of a TIFF file embedded in a JPEG + # application marker (!). + if data == self._loaded_exif: + return + self._loaded_exif = data + self._data.clear() + self._hidden_data.clear() + self._ifds.clear() + if data and data.startswith(b"Exif\x00\x00"): + data = data[6:] + if not data: + self._info = None + return + + self.fp = io.BytesIO(data) + self.head = self.fp.read(8) + # process dictionary + from . import TiffImagePlugin + + self._info = TiffImagePlugin.ImageFileDirectory_v2(self.head) + self.endian = self._info._endian + self.fp.seek(self._info.next) + self._info.load(self.fp) + + def load_from_fp(self, fp, offset=None): + self._loaded_exif = None + self._data.clear() + self._hidden_data.clear() + self._ifds.clear() + + # process dictionary + from . import TiffImagePlugin + + self.fp = fp + if offset is not None: + self.head = self._get_head() + else: + self.head = self.fp.read(8) + self._info = TiffImagePlugin.ImageFileDirectory_v2(self.head) + if self.endian is None: + self.endian = self._info._endian + if offset is None: + offset = self._info.next + self.fp.tell() + self.fp.seek(offset) + self._info.load(self.fp) + + def _get_merged_dict(self): + merged_dict = dict(self) + + # get EXIF extension + if ExifTags.IFD.Exif in self: + ifd = self._get_ifd_dict(self[ExifTags.IFD.Exif], ExifTags.IFD.Exif) + if ifd: + merged_dict.update(ifd) + + # GPS + if ExifTags.IFD.GPSInfo in self: + merged_dict[ExifTags.IFD.GPSInfo] = self._get_ifd_dict( + self[ExifTags.IFD.GPSInfo], ExifTags.IFD.GPSInfo + ) + + return merged_dict + + def tobytes(self, offset: int = 8) -> bytes: + from . import TiffImagePlugin + + head = self._get_head() + ifd = TiffImagePlugin.ImageFileDirectory_v2(ifh=head) + for tag, value in self.items(): + if tag in [ + ExifTags.IFD.Exif, + ExifTags.IFD.GPSInfo, + ] and not isinstance(value, dict): + value = self.get_ifd(tag) + if ( + tag == ExifTags.IFD.Exif + and ExifTags.IFD.Interop in value + and not isinstance(value[ExifTags.IFD.Interop], dict) + ): + value = value.copy() + value[ExifTags.IFD.Interop] = self.get_ifd(ExifTags.IFD.Interop) + ifd[tag] = value + return b"Exif\x00\x00" + head + ifd.tobytes(offset) + + def get_ifd(self, tag): + if tag not in self._ifds: + if tag == ExifTags.IFD.IFD1: + if self._info is not None and self._info.next != 0: + self._ifds[tag] = self._get_ifd_dict(self._info.next) + elif tag in [ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo]: + offset = self._hidden_data.get(tag, self.get(tag)) + if offset is not None: + self._ifds[tag] = self._get_ifd_dict(offset, tag) + elif tag in [ExifTags.IFD.Interop, ExifTags.IFD.Makernote]: + if ExifTags.IFD.Exif not in self._ifds: + self.get_ifd(ExifTags.IFD.Exif) + tag_data = self._ifds[ExifTags.IFD.Exif][tag] + if tag == ExifTags.IFD.Makernote: + from .TiffImagePlugin import ImageFileDirectory_v2 + + if tag_data[:8] == b"FUJIFILM": + ifd_offset = i32le(tag_data, 8) + ifd_data = tag_data[ifd_offset:] + + makernote = {} + for i in range(0, struct.unpack(" 4: + (offset,) = struct.unpack("H", tag_data[:2])[0]): + ifd_tag, typ, count, data = struct.unpack( + ">HHL4s", tag_data[i * 12 + 2 : (i + 1) * 12 + 2] + ) + if ifd_tag == 0x1101: + # CameraInfo + (offset,) = struct.unpack(">L", data) + self.fp.seek(offset) + + camerainfo = {"ModelID": self.fp.read(4)} + + self.fp.read(4) + # Seconds since 2000 + camerainfo["TimeStamp"] = i32le(self.fp.read(12)) + + self.fp.read(4) + camerainfo["InternalSerialNumber"] = self.fp.read(4) + + self.fp.read(12) + parallax = self.fp.read(4) + handler = ImageFileDirectory_v2._load_dispatch[ + TiffTags.FLOAT + ][1] + camerainfo["Parallax"] = handler( + ImageFileDirectory_v2(), parallax, False + ) + + self.fp.read(4) + camerainfo["Category"] = self.fp.read(2) + + makernote = {0x1101: dict(self._fixup_dict(camerainfo))} + self._ifds[tag] = makernote + else: + # Interop + self._ifds[tag] = self._get_ifd_dict(tag_data, tag) + ifd = self._ifds.get(tag, {}) + if tag == ExifTags.IFD.Exif and self._hidden_data: + ifd = { + k: v + for (k, v) in ifd.items() + if k not in (ExifTags.IFD.Interop, ExifTags.IFD.Makernote) + } + return ifd + + def hide_offsets(self) -> None: + for tag in (ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo): + if tag in self: + self._hidden_data[tag] = self[tag] + del self[tag] + + def __str__(self) -> str: + if self._info is not None: + # Load all keys into self._data + for tag in self._info: + self[tag] + + return str(self._data) + + def __len__(self) -> int: + keys = set(self._data) + if self._info is not None: + keys.update(self._info) + return len(keys) + + def __getitem__(self, tag): + if self._info is not None and tag not in self._data and tag in self._info: + self._data[tag] = self._fixup(self._info[tag]) + del self._info[tag] + return self._data[tag] + + def __contains__(self, tag) -> bool: + return tag in self._data or (self._info is not None and tag in self._info) + + def __setitem__(self, tag, value) -> None: + if self._info is not None and tag in self._info: + del self._info[tag] + self._data[tag] = value + + def __delitem__(self, tag: int) -> None: + if self._info is not None and tag in self._info: + del self._info[tag] + else: + del self._data[tag] + + def __iter__(self): + keys = set(self._data) + if self._info is not None: + keys.update(self._info) + return iter(keys) diff --git a/src/PIL/ImageChops.py b/src/PIL/ImageChops.py index 29a5c995f..4ec5fafa8 100644 --- a/src/PIL/ImageChops.py +++ b/src/PIL/ImageChops.py @@ -1,311 +1,311 @@ -# -# The Python Imaging Library. -# $Id$ -# -# standard channel operations -# -# History: -# 1996-03-24 fl Created -# 1996-08-13 fl Added logical operations (for "1" images) -# 2000-10-12 fl Added offset method (from Image.py) -# -# Copyright (c) 1997-2000 by Secret Labs AB -# Copyright (c) 1996-2000 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# - -from __future__ import annotations - -from . import Image - - -def constant(image: Image.Image, value: int) -> Image.Image: - """Fill a channel with a given gray level. - - :rtype: :py:class:`~PIL.Image.Image` - """ - - return Image.new("L", image.size, value) - - -def duplicate(image: Image.Image) -> Image.Image: - """Copy a channel. Alias for :py:meth:`PIL.Image.Image.copy`. - - :rtype: :py:class:`~PIL.Image.Image` - """ - - return image.copy() - - -def invert(image: Image.Image) -> Image.Image: - """ - Invert an image (channel). :: - - out = MAX - image - - :rtype: :py:class:`~PIL.Image.Image` - """ - - image.load() - return image._new(image.im.chop_invert()) - - -def lighter(image1: Image.Image, image2: Image.Image) -> Image.Image: - """ - Compares the two images, pixel by pixel, and returns a new image containing - the lighter values. :: - - out = max(image1, image2) - - :rtype: :py:class:`~PIL.Image.Image` - """ - - image1.load() - image2.load() - return image1._new(image1.im.chop_lighter(image2.im)) - - -def darker(image1: Image.Image, image2: Image.Image) -> Image.Image: - """ - Compares the two images, pixel by pixel, and returns a new image containing - the darker values. :: - - out = min(image1, image2) - - :rtype: :py:class:`~PIL.Image.Image` - """ - - image1.load() - image2.load() - return image1._new(image1.im.chop_darker(image2.im)) - - -def difference(image1: Image.Image, image2: Image.Image) -> Image.Image: - """ - Returns the absolute value of the pixel-by-pixel difference between the two - images. :: - - out = abs(image1 - image2) - - :rtype: :py:class:`~PIL.Image.Image` - """ - - image1.load() - image2.load() - return image1._new(image1.im.chop_difference(image2.im)) - - -def multiply(image1: Image.Image, image2: Image.Image) -> Image.Image: - """ - Superimposes two images on top of each other. - - If you multiply an image with a solid black image, the result is black. If - you multiply with a solid white image, the image is unaffected. :: - - out = image1 * image2 / MAX - - :rtype: :py:class:`~PIL.Image.Image` - """ - - image1.load() - image2.load() - return image1._new(image1.im.chop_multiply(image2.im)) - - -def screen(image1: Image.Image, image2: Image.Image) -> Image.Image: - """ - Superimposes two inverted images on top of each other. :: - - out = MAX - ((MAX - image1) * (MAX - image2) / MAX) - - :rtype: :py:class:`~PIL.Image.Image` - """ - - image1.load() - image2.load() - return image1._new(image1.im.chop_screen(image2.im)) - - -def soft_light(image1: Image.Image, image2: Image.Image) -> Image.Image: - """ - Superimposes two images on top of each other using the Soft Light algorithm - - :rtype: :py:class:`~PIL.Image.Image` - """ - - image1.load() - image2.load() - return image1._new(image1.im.chop_soft_light(image2.im)) - - -def hard_light(image1: Image.Image, image2: Image.Image) -> Image.Image: - """ - Superimposes two images on top of each other using the Hard Light algorithm - - :rtype: :py:class:`~PIL.Image.Image` - """ - - image1.load() - image2.load() - return image1._new(image1.im.chop_hard_light(image2.im)) - - -def overlay(image1: Image.Image, image2: Image.Image) -> Image.Image: - """ - Superimposes two images on top of each other using the Overlay algorithm - - :rtype: :py:class:`~PIL.Image.Image` - """ - - image1.load() - image2.load() - return image1._new(image1.im.chop_overlay(image2.im)) - - -def add( - image1: Image.Image, image2: Image.Image, scale: float = 1.0, offset: float = 0 -) -> Image.Image: - """ - Adds two images, dividing the result by scale and adding the - offset. If omitted, scale defaults to 1.0, and offset to 0.0. :: - - out = ((image1 + image2) / scale + offset) - - :rtype: :py:class:`~PIL.Image.Image` - """ - - image1.load() - image2.load() - return image1._new(image1.im.chop_add(image2.im, scale, offset)) - - -def subtract( - image1: Image.Image, image2: Image.Image, scale: float = 1.0, offset: float = 0 -) -> Image.Image: - """ - Subtracts two images, dividing the result by scale and adding the offset. - If omitted, scale defaults to 1.0, and offset to 0.0. :: - - out = ((image1 - image2) / scale + offset) - - :rtype: :py:class:`~PIL.Image.Image` - """ - - image1.load() - image2.load() - return image1._new(image1.im.chop_subtract(image2.im, scale, offset)) - - -def add_modulo(image1: Image.Image, image2: Image.Image) -> Image.Image: - """Add two images, without clipping the result. :: - - out = ((image1 + image2) % MAX) - - :rtype: :py:class:`~PIL.Image.Image` - """ - - image1.load() - image2.load() - return image1._new(image1.im.chop_add_modulo(image2.im)) - - -def subtract_modulo(image1: Image.Image, image2: Image.Image) -> Image.Image: - """Subtract two images, without clipping the result. :: - - out = ((image1 - image2) % MAX) - - :rtype: :py:class:`~PIL.Image.Image` - """ - - image1.load() - image2.load() - return image1._new(image1.im.chop_subtract_modulo(image2.im)) - - -def logical_and(image1: Image.Image, image2: Image.Image) -> Image.Image: - """Logical AND between two images. - - Both of the images must have mode "1". If you would like to perform a - logical AND on an image with a mode other than "1", try - :py:meth:`~PIL.ImageChops.multiply` instead, using a black-and-white mask - as the second image. :: - - out = ((image1 and image2) % MAX) - - :rtype: :py:class:`~PIL.Image.Image` - """ - - image1.load() - image2.load() - return image1._new(image1.im.chop_and(image2.im)) - - -def logical_or(image1: Image.Image, image2: Image.Image) -> Image.Image: - """Logical OR between two images. - - Both of the images must have mode "1". :: - - out = ((image1 or image2) % MAX) - - :rtype: :py:class:`~PIL.Image.Image` - """ - - image1.load() - image2.load() - return image1._new(image1.im.chop_or(image2.im)) - - -def logical_xor(image1: Image.Image, image2: Image.Image) -> Image.Image: - """Logical XOR between two images. - - Both of the images must have mode "1". :: - - out = ((bool(image1) != bool(image2)) % MAX) - - :rtype: :py:class:`~PIL.Image.Image` - """ - - image1.load() - image2.load() - return image1._new(image1.im.chop_xor(image2.im)) - - -def blend(image1: Image.Image, image2: Image.Image, alpha: float) -> Image.Image: - """Blend images using constant transparency weight. Alias for - :py:func:`PIL.Image.blend`. - - :rtype: :py:class:`~PIL.Image.Image` - """ - - return Image.blend(image1, image2, alpha) - - -def composite( - image1: Image.Image, image2: Image.Image, mask: Image.Image -) -> Image.Image: - """Create composite using transparency mask. Alias for - :py:func:`PIL.Image.composite`. - - :rtype: :py:class:`~PIL.Image.Image` - """ - - return Image.composite(image1, image2, mask) - - -def offset(image: Image.Image, xoffset: int, yoffset: int | None = None) -> Image.Image: - """Returns a copy of the image where data has been offset by the given - distances. Data wraps around the edges. If ``yoffset`` is omitted, it - is assumed to be equal to ``xoffset``. - - :param image: Input image. - :param xoffset: The horizontal distance. - :param yoffset: The vertical distance. If omitted, both - distances are set to the same value. - :rtype: :py:class:`~PIL.Image.Image` - """ - - if yoffset is None: - yoffset = xoffset - image.load() - return image._new(image.im.offset(xoffset, yoffset)) +# +# The Python Imaging Library. +# $Id$ +# +# standard channel operations +# +# History: +# 1996-03-24 fl Created +# 1996-08-13 fl Added logical operations (for "1" images) +# 2000-10-12 fl Added offset method (from Image.py) +# +# Copyright (c) 1997-2000 by Secret Labs AB +# Copyright (c) 1996-2000 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# + +from __future__ import annotations + +from . import Image + + +def constant(image: Image.Image, value: int) -> Image.Image: + """Fill a channel with a given gray level. + + :rtype: :py:class:`~PIL.Image.Image` + """ + + return Image.new("L", image.size, value) + + +def duplicate(image: Image.Image) -> Image.Image: + """Copy a channel. Alias for :py:meth:`PIL.Image.Image.copy`. + + :rtype: :py:class:`~PIL.Image.Image` + """ + + return image.copy() + + +def invert(image: Image.Image) -> Image.Image: + """ + Invert an image (channel). :: + + out = MAX - image + + :rtype: :py:class:`~PIL.Image.Image` + """ + + image.load() + return image._new(image.im.chop_invert()) + + +def lighter(image1: Image.Image, image2: Image.Image) -> Image.Image: + """ + Compares the two images, pixel by pixel, and returns a new image containing + the lighter values. :: + + out = max(image1, image2) + + :rtype: :py:class:`~PIL.Image.Image` + """ + + image1.load() + image2.load() + return image1._new(image1.im.chop_lighter(image2.im)) + + +def darker(image1: Image.Image, image2: Image.Image) -> Image.Image: + """ + Compares the two images, pixel by pixel, and returns a new image containing + the darker values. :: + + out = min(image1, image2) + + :rtype: :py:class:`~PIL.Image.Image` + """ + + image1.load() + image2.load() + return image1._new(image1.im.chop_darker(image2.im)) + + +def difference(image1: Image.Image, image2: Image.Image) -> Image.Image: + """ + Returns the absolute value of the pixel-by-pixel difference between the two + images. :: + + out = abs(image1 - image2) + + :rtype: :py:class:`~PIL.Image.Image` + """ + + image1.load() + image2.load() + return image1._new(image1.im.chop_difference(image2.im)) + + +def multiply(image1: Image.Image, image2: Image.Image) -> Image.Image: + """ + Superimposes two images on top of each other. + + If you multiply an image with a solid black image, the result is black. If + you multiply with a solid white image, the image is unaffected. :: + + out = image1 * image2 / MAX + + :rtype: :py:class:`~PIL.Image.Image` + """ + + image1.load() + image2.load() + return image1._new(image1.im.chop_multiply(image2.im)) + + +def screen(image1: Image.Image, image2: Image.Image) -> Image.Image: + """ + Superimposes two inverted images on top of each other. :: + + out = MAX - ((MAX - image1) * (MAX - image2) / MAX) + + :rtype: :py:class:`~PIL.Image.Image` + """ + + image1.load() + image2.load() + return image1._new(image1.im.chop_screen(image2.im)) + + +def soft_light(image1: Image.Image, image2: Image.Image) -> Image.Image: + """ + Superimposes two images on top of each other using the Soft Light algorithm + + :rtype: :py:class:`~PIL.Image.Image` + """ + + image1.load() + image2.load() + return image1._new(image1.im.chop_soft_light(image2.im)) + + +def hard_light(image1: Image.Image, image2: Image.Image) -> Image.Image: + """ + Superimposes two images on top of each other using the Hard Light algorithm + + :rtype: :py:class:`~PIL.Image.Image` + """ + + image1.load() + image2.load() + return image1._new(image1.im.chop_hard_light(image2.im)) + + +def overlay(image1: Image.Image, image2: Image.Image) -> Image.Image: + """ + Superimposes two images on top of each other using the Overlay algorithm + + :rtype: :py:class:`~PIL.Image.Image` + """ + + image1.load() + image2.load() + return image1._new(image1.im.chop_overlay(image2.im)) + + +def add( + image1: Image.Image, image2: Image.Image, scale: float = 1.0, offset: float = 0 +) -> Image.Image: + """ + Adds two images, dividing the result by scale and adding the + offset. If omitted, scale defaults to 1.0, and offset to 0.0. :: + + out = ((image1 + image2) / scale + offset) + + :rtype: :py:class:`~PIL.Image.Image` + """ + + image1.load() + image2.load() + return image1._new(image1.im.chop_add(image2.im, scale, offset)) + + +def subtract( + image1: Image.Image, image2: Image.Image, scale: float = 1.0, offset: float = 0 +) -> Image.Image: + """ + Subtracts two images, dividing the result by scale and adding the offset. + If omitted, scale defaults to 1.0, and offset to 0.0. :: + + out = ((image1 - image2) / scale + offset) + + :rtype: :py:class:`~PIL.Image.Image` + """ + + image1.load() + image2.load() + return image1._new(image1.im.chop_subtract(image2.im, scale, offset)) + + +def add_modulo(image1: Image.Image, image2: Image.Image) -> Image.Image: + """Add two images, without clipping the result. :: + + out = ((image1 + image2) % MAX) + + :rtype: :py:class:`~PIL.Image.Image` + """ + + image1.load() + image2.load() + return image1._new(image1.im.chop_add_modulo(image2.im)) + + +def subtract_modulo(image1: Image.Image, image2: Image.Image) -> Image.Image: + """Subtract two images, without clipping the result. :: + + out = ((image1 - image2) % MAX) + + :rtype: :py:class:`~PIL.Image.Image` + """ + + image1.load() + image2.load() + return image1._new(image1.im.chop_subtract_modulo(image2.im)) + + +def logical_and(image1: Image.Image, image2: Image.Image) -> Image.Image: + """Logical AND between two images. + + Both of the images must have mode "1". If you would like to perform a + logical AND on an image with a mode other than "1", try + :py:meth:`~PIL.ImageChops.multiply` instead, using a black-and-white mask + as the second image. :: + + out = ((image1 and image2) % MAX) + + :rtype: :py:class:`~PIL.Image.Image` + """ + + image1.load() + image2.load() + return image1._new(image1.im.chop_and(image2.im)) + + +def logical_or(image1: Image.Image, image2: Image.Image) -> Image.Image: + """Logical OR between two images. + + Both of the images must have mode "1". :: + + out = ((image1 or image2) % MAX) + + :rtype: :py:class:`~PIL.Image.Image` + """ + + image1.load() + image2.load() + return image1._new(image1.im.chop_or(image2.im)) + + +def logical_xor(image1: Image.Image, image2: Image.Image) -> Image.Image: + """Logical XOR between two images. + + Both of the images must have mode "1". :: + + out = ((bool(image1) != bool(image2)) % MAX) + + :rtype: :py:class:`~PIL.Image.Image` + """ + + image1.load() + image2.load() + return image1._new(image1.im.chop_xor(image2.im)) + + +def blend(image1: Image.Image, image2: Image.Image, alpha: float) -> Image.Image: + """Blend images using constant transparency weight. Alias for + :py:func:`PIL.Image.blend`. + + :rtype: :py:class:`~PIL.Image.Image` + """ + + return Image.blend(image1, image2, alpha) + + +def composite( + image1: Image.Image, image2: Image.Image, mask: Image.Image +) -> Image.Image: + """Create composite using transparency mask. Alias for + :py:func:`PIL.Image.composite`. + + :rtype: :py:class:`~PIL.Image.Image` + """ + + return Image.composite(image1, image2, mask) + + +def offset(image: Image.Image, xoffset: int, yoffset: int | None = None) -> Image.Image: + """Returns a copy of the image where data has been offset by the given + distances. Data wraps around the edges. If ``yoffset`` is omitted, it + is assumed to be equal to ``xoffset``. + + :param image: Input image. + :param xoffset: The horizontal distance. + :param yoffset: The vertical distance. If omitted, both + distances are set to the same value. + :rtype: :py:class:`~PIL.Image.Image` + """ + + if yoffset is None: + yoffset = xoffset + image.load() + return image._new(image.im.offset(xoffset, yoffset)) diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 19a79facc..2eedf952f 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -1,1102 +1,1102 @@ -# The Python Imaging Library. -# $Id$ - -# Optional color management support, based on Kevin Cazabon's PyCMS -# library. - -# Originally released under LGPL. Graciously donated to PIL in -# March 2009, for distribution under the standard PIL license - -# History: - -# 2009-03-08 fl Added to PIL. - -# Copyright (C) 2002-2003 Kevin Cazabon -# Copyright (c) 2009 by Fredrik Lundh -# Copyright (c) 2013 by Eric Soroos - -# See the README file for information on usage and redistribution. See -# below for the original description. -from __future__ import annotations - -import operator -import sys -from enum import IntEnum, IntFlag -from functools import reduce -from typing import Any, Literal, SupportsFloat, SupportsInt, Union - -from . import Image, __version__ -from ._deprecate import deprecate -from ._typing import SupportsRead - -try: - from . import _imagingcms as core -except ImportError as ex: - # Allow error import for doc purposes, but error out when accessing - # anything in core. - from ._util import DeferredError - - core = DeferredError.new(ex) - -_DESCRIPTION = """ -pyCMS - - a Python / PIL interface to the littleCMS ICC Color Management System - Copyright (C) 2002-2003 Kevin Cazabon - kevin@cazabon.com - https://www.cazabon.com - - pyCMS home page: https://www.cazabon.com/pyCMS - littleCMS home page: https://www.littlecms.com - (littleCMS is Copyright (C) 1998-2001 Marti Maria) - - Originally released under LGPL. Graciously donated to PIL in - March 2009, for distribution under the standard PIL license - - The pyCMS.py module provides a "clean" interface between Python/PIL and - pyCMSdll, taking care of some of the more complex handling of the direct - pyCMSdll functions, as well as error-checking and making sure that all - relevant data is kept together. - - While it is possible to call pyCMSdll functions directly, it's not highly - recommended. - - Version History: - - 1.0.0 pil Oct 2013 Port to LCMS 2. - - 0.1.0 pil mod March 10, 2009 - - Renamed display profile to proof profile. The proof - profile is the profile of the device that is being - simulated, not the profile of the device which is - actually used to display/print the final simulation - (that'd be the output profile) - also see LCMSAPI.txt - input colorspace -> using 'renderingIntent' -> proof - colorspace -> using 'proofRenderingIntent' -> output - colorspace - - Added LCMS FLAGS support. - Added FLAGS["SOFTPROOFING"] as default flag for - buildProofTransform (otherwise the proof profile/intent - would be ignored). - - 0.1.0 pil March 2009 - added to PIL, as PIL.ImageCms - - 0.0.2 alpha Jan 6, 2002 - - Added try/except statements around type() checks of - potential CObjects... Python won't let you use type() - on them, and raises a TypeError (stupid, if you ask - me!) - - Added buildProofTransformFromOpenProfiles() function. - Additional fixes in DLL, see DLL code for details. - - 0.0.1 alpha first public release, Dec. 26, 2002 - - Known to-do list with current version (of Python interface, not pyCMSdll): - - none - -""" - -_VERSION = "1.0.0 pil" - - -def __getattr__(name: str) -> Any: - if name == "DESCRIPTION": - deprecate("PIL.ImageCms.DESCRIPTION", 12) - return _DESCRIPTION - elif name == "VERSION": - deprecate("PIL.ImageCms.VERSION", 12) - return _VERSION - elif name == "FLAGS": - deprecate("PIL.ImageCms.FLAGS", 12, "PIL.ImageCms.Flags") - return _FLAGS - msg = f"module '{__name__}' has no attribute '{name}'" - raise AttributeError(msg) - - -# --------------------------------------------------------------------. - - -# -# intent/direction values - - -class Intent(IntEnum): - PERCEPTUAL = 0 - RELATIVE_COLORIMETRIC = 1 - SATURATION = 2 - ABSOLUTE_COLORIMETRIC = 3 - - -class Direction(IntEnum): - INPUT = 0 - OUTPUT = 1 - PROOF = 2 - - -# -# flags - - -class Flags(IntFlag): - """Flags and documentation are taken from ``lcms2.h``.""" - - NONE = 0 - NOCACHE = 0x0040 - """Inhibit 1-pixel cache""" - NOOPTIMIZE = 0x0100 - """Inhibit optimizations""" - NULLTRANSFORM = 0x0200 - """Don't transform anyway""" - GAMUTCHECK = 0x1000 - """Out of Gamut alarm""" - SOFTPROOFING = 0x4000 - """Do softproofing""" - BLACKPOINTCOMPENSATION = 0x2000 - NOWHITEONWHITEFIXUP = 0x0004 - """Don't fix scum dot""" - HIGHRESPRECALC = 0x0400 - """Use more memory to give better accuracy""" - LOWRESPRECALC = 0x0800 - """Use less memory to minimize resources""" - # this should be 8BITS_DEVICELINK, but that is not a valid name in Python: - USE_8BITS_DEVICELINK = 0x0008 - """Create 8 bits devicelinks""" - GUESSDEVICECLASS = 0x0020 - """Guess device class (for ``transform2devicelink``)""" - KEEP_SEQUENCE = 0x0080 - """Keep profile sequence for devicelink creation""" - FORCE_CLUT = 0x0002 - """Force CLUT optimization""" - CLUT_POST_LINEARIZATION = 0x0001 - """create postlinearization tables if possible""" - CLUT_PRE_LINEARIZATION = 0x0010 - """create prelinearization tables if possible""" - NONEGATIVES = 0x8000 - """Prevent negative numbers in floating point transforms""" - COPY_ALPHA = 0x04000000 - """Alpha channels are copied on ``cmsDoTransform()``""" - NODEFAULTRESOURCEDEF = 0x01000000 - - _GRIDPOINTS_1 = 1 << 16 - _GRIDPOINTS_2 = 2 << 16 - _GRIDPOINTS_4 = 4 << 16 - _GRIDPOINTS_8 = 8 << 16 - _GRIDPOINTS_16 = 16 << 16 - _GRIDPOINTS_32 = 32 << 16 - _GRIDPOINTS_64 = 64 << 16 - _GRIDPOINTS_128 = 128 << 16 - - @staticmethod - def GRIDPOINTS(n: int) -> Flags: - """ - Fine-tune control over number of gridpoints - - :param n: :py:class:`int` in range ``0 <= n <= 255`` - """ - return Flags.NONE | ((n & 0xFF) << 16) - - -_MAX_FLAG = reduce(operator.or_, Flags) - - -_FLAGS = { - "MATRIXINPUT": 1, - "MATRIXOUTPUT": 2, - "MATRIXONLY": (1 | 2), - "NOWHITEONWHITEFIXUP": 4, # Don't hot fix scum dot - # Don't create prelinearization tables on precalculated transforms - # (internal use): - "NOPRELINEARIZATION": 16, - "GUESSDEVICECLASS": 32, # Guess device class (for transform2devicelink) - "NOTCACHE": 64, # Inhibit 1-pixel cache - "NOTPRECALC": 256, - "NULLTRANSFORM": 512, # Don't transform anyway - "HIGHRESPRECALC": 1024, # Use more memory to give better accuracy - "LOWRESPRECALC": 2048, # Use less memory to minimize resources - "WHITEBLACKCOMPENSATION": 8192, - "BLACKPOINTCOMPENSATION": 8192, - "GAMUTCHECK": 4096, # Out of Gamut alarm - "SOFTPROOFING": 16384, # Do softproofing - "PRESERVEBLACK": 32768, # Black preservation - "NODEFAULTRESOURCEDEF": 16777216, # CRD special - "GRIDPOINTS": lambda n: (n & 0xFF) << 16, # Gridpoints -} - - -# --------------------------------------------------------------------. -# Experimental PIL-level API -# --------------------------------------------------------------------. - -## -# Profile. - - -class ImageCmsProfile: - def __init__(self, profile: str | SupportsRead[bytes] | core.CmsProfile) -> None: - """ - :param profile: Either a string representing a filename, - a file like object containing a profile or a - low-level profile object - - """ - - if isinstance(profile, str): - if sys.platform == "win32": - profile_bytes_path = profile.encode() - try: - profile_bytes_path.decode("ascii") - except UnicodeDecodeError: - with open(profile, "rb") as f: - self._set(core.profile_frombytes(f.read())) - return - self._set(core.profile_open(profile), profile) - elif hasattr(profile, "read"): - self._set(core.profile_frombytes(profile.read())) - elif isinstance(profile, core.CmsProfile): - self._set(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 - saved images. - - :returns: a bytes object containing the ICC profile. - """ - - return core.profile_tobytes(self.profile) - - -class ImageCmsTransform(Image.ImagePointHandler): - """ - Transform. This can be used with the procedural API, or with the standard - :py:func:`~PIL.Image.Image.point` method. - - Will return the output profile in the ``output.info['icc_profile']``. - """ - - def __init__( - self, - input: ImageCmsProfile, - output: ImageCmsProfile, - input_mode: str, - output_mode: str, - intent: Intent = Intent.PERCEPTUAL, - proof: ImageCmsProfile | None = None, - proof_intent: Intent = Intent.ABSOLUTE_COLORIMETRIC, - flags: Flags = Flags.NONE, - ): - if proof is None: - self.transform = core.buildTransform( - input.profile, output.profile, input_mode, output_mode, intent, flags - ) - else: - self.transform = core.buildProofTransform( - input.profile, - output.profile, - proof.profile, - input_mode, - output_mode, - intent, - proof_intent, - flags, - ) - # Note: inputMode and outputMode are for pyCMS compatibility only - self.input_mode = self.inputMode = input_mode - self.output_mode = self.outputMode = output_mode - - self.output_profile = output - - def point(self, im: Image.Image) -> Image.Image: - return self.apply(im) - - def apply(self, im: Image.Image, imOut: Image.Image | None = None) -> Image.Image: - im.load() - if imOut is None: - imOut = Image.new(self.output_mode, im.size, None) - self.transform.apply(im.im.id, imOut.im.id) - imOut.info["icc_profile"] = self.output_profile.tobytes() - return imOut - - def apply_in_place(self, im: Image.Image) -> Image.Image: - im.load() - if im.mode != self.output_mode: - msg = "mode mismatch" - raise ValueError(msg) # wrong output mode - self.transform.apply(im.im.id, im.im.id) - im.info["icc_profile"] = self.output_profile.tobytes() - return im - - -def get_display_profile(handle: SupportsInt | None = None) -> ImageCmsProfile | None: - """ - (experimental) Fetches the profile for the current display device. - - :returns: ``None`` if the profile is not known. - """ - - if sys.platform != "win32": - return None - - from . import ImageWin # type: ignore[unused-ignore, unreachable] - - if isinstance(handle, ImageWin.HDC): - profile = core.get_display_profile_win32(int(handle), 1) - else: - profile = core.get_display_profile_win32(int(handle or 0)) - if profile is None: - return None - return ImageCmsProfile(profile) - - -# --------------------------------------------------------------------. -# pyCMS compatible layer -# --------------------------------------------------------------------. - -_CmsProfileCompatible = Union[ - str, SupportsRead[bytes], core.CmsProfile, ImageCmsProfile -] - - -class PyCMSError(Exception): - """(pyCMS) Exception class. - This is used for all errors in the pyCMS API.""" - - pass - - -def profileToProfile( - im: Image.Image, - inputProfile: _CmsProfileCompatible, - outputProfile: _CmsProfileCompatible, - renderingIntent: Intent = Intent.PERCEPTUAL, - outputMode: str | None = None, - inPlace: bool = False, - flags: Flags = Flags.NONE, -) -> Image.Image | None: - """ - (pyCMS) Applies an ICC transformation to a given image, mapping from - ``inputProfile`` to ``outputProfile``. - - If the input or output profiles specified are not valid filenames, a - :exc:`PyCMSError` will be raised. If ``inPlace`` is ``True`` and - ``outputMode != im.mode``, a :exc:`PyCMSError` will be raised. - If an error occurs during application of the profiles, - a :exc:`PyCMSError` will be raised. - If ``outputMode`` is not a mode supported by the ``outputProfile`` (or by pyCMS), - a :exc:`PyCMSError` will be raised. - - This function applies an ICC transformation to im from ``inputProfile``'s - color space to ``outputProfile``'s color space using the specified rendering - intent to decide how to handle out-of-gamut colors. - - ``outputMode`` can be used to specify that a color mode conversion is to - be done using these profiles, but the specified profiles must be able - to handle that mode. I.e., if converting im from RGB to CMYK using - profiles, the input profile must handle RGB data, and the output - profile must handle CMYK data. - - :param im: An open :py:class:`~PIL.Image.Image` object (i.e. Image.new(...) - or Image.open(...), etc.) - :param inputProfile: String, as a valid filename path to the ICC input - profile you wish to use for this image, or a profile object - :param outputProfile: String, as a valid filename path to the ICC output - profile you wish to use for this image, or a profile object - :param renderingIntent: Integer (0-3) specifying the rendering intent you - wish to use for the transform - - ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT) - ImageCms.Intent.RELATIVE_COLORIMETRIC = 1 - ImageCms.Intent.SATURATION = 2 - ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3 - - see the pyCMS documentation for details on rendering intents and what - they do. - :param outputMode: A valid PIL mode for the output image (i.e. "RGB", - "CMYK", etc.). Note: if rendering the image "inPlace", outputMode - MUST be the same mode as the input, or omitted completely. If - omitted, the outputMode will be the same as the mode of the input - image (im.mode) - :param inPlace: Boolean. If ``True``, the original image is modified in-place, - and ``None`` is returned. If ``False`` (default), a new - :py:class:`~PIL.Image.Image` object is returned with the transform applied. - :param flags: Integer (0-...) specifying additional flags - :returns: Either None or a new :py:class:`~PIL.Image.Image` object, depending on - the value of ``inPlace`` - :exception PyCMSError: - """ - - if outputMode is None: - outputMode = im.mode - - if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <= 3): - msg = "renderingIntent must be an integer between 0 and 3" - raise PyCMSError(msg) - - if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG): - msg = f"flags must be an integer between 0 and {_MAX_FLAG}" - raise PyCMSError(msg) - - try: - if not isinstance(inputProfile, ImageCmsProfile): - inputProfile = ImageCmsProfile(inputProfile) - if not isinstance(outputProfile, ImageCmsProfile): - outputProfile = ImageCmsProfile(outputProfile) - transform = ImageCmsTransform( - inputProfile, - outputProfile, - im.mode, - outputMode, - renderingIntent, - flags=flags, - ) - if inPlace: - transform.apply_in_place(im) - imOut = None - else: - imOut = transform.apply(im) - except (OSError, TypeError, ValueError) as v: - raise PyCMSError(v) from v - - return imOut - - -def getOpenProfile( - profileFilename: str | SupportsRead[bytes] | core.CmsProfile, -) -> ImageCmsProfile: - """ - (pyCMS) Opens an ICC profile file. - - The PyCMSProfile object can be passed back into pyCMS for use in creating - transforms and such (as in ImageCms.buildTransformFromOpenProfiles()). - - If ``profileFilename`` is not a valid filename for an ICC profile, - a :exc:`PyCMSError` will be raised. - - :param profileFilename: String, as a valid filename path to the ICC profile - you wish to open, or a file-like object. - :returns: A CmsProfile class object. - :exception PyCMSError: - """ - - try: - return ImageCmsProfile(profileFilename) - except (OSError, TypeError, ValueError) as v: - raise PyCMSError(v) from v - - -def buildTransform( - inputProfile: _CmsProfileCompatible, - outputProfile: _CmsProfileCompatible, - inMode: str, - outMode: str, - renderingIntent: Intent = Intent.PERCEPTUAL, - flags: Flags = Flags.NONE, -) -> ImageCmsTransform: - """ - (pyCMS) Builds an ICC transform mapping from the ``inputProfile`` to the - ``outputProfile``. Use applyTransform to apply the transform to a given - image. - - If the input or output profiles specified are not valid filenames, a - :exc:`PyCMSError` will be raised. If an error occurs during creation - of the transform, a :exc:`PyCMSError` will be raised. - - If ``inMode`` or ``outMode`` are not a mode supported by the ``outputProfile`` - (or by pyCMS), a :exc:`PyCMSError` will be raised. - - This function builds and returns an ICC transform from the ``inputProfile`` - to the ``outputProfile`` using the ``renderingIntent`` to determine what to do - with out-of-gamut colors. It will ONLY work for converting images that - are in ``inMode`` to images that are in ``outMode`` color format (PIL mode, - i.e. "RGB", "RGBA", "CMYK", etc.). - - Building the transform is a fair part of the overhead in - ImageCms.profileToProfile(), so if you're planning on converting multiple - images using the same input/output settings, this can save you time. - Once you have a transform object, it can be used with - ImageCms.applyProfile() to convert images without the need to re-compute - the lookup table for the transform. - - The reason pyCMS returns a class object rather than a handle directly - to the transform is that it needs to keep track of the PIL input/output - modes that the transform is meant for. These attributes are stored in - the ``inMode`` and ``outMode`` attributes of the object (which can be - manually overridden if you really want to, but I don't know of any - time that would be of use, or would even work). - - :param inputProfile: String, as a valid filename path to the ICC input - profile you wish to use for this transform, or a profile object - :param outputProfile: String, as a valid filename path to the ICC output - profile you wish to use for this transform, or a profile object - :param inMode: String, as a valid PIL mode that the appropriate profile - also supports (i.e. "RGB", "RGBA", "CMYK", etc.) - :param outMode: String, as a valid PIL mode that the appropriate profile - also supports (i.e. "RGB", "RGBA", "CMYK", etc.) - :param renderingIntent: Integer (0-3) specifying the rendering intent you - wish to use for the transform - - ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT) - ImageCms.Intent.RELATIVE_COLORIMETRIC = 1 - ImageCms.Intent.SATURATION = 2 - ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3 - - see the pyCMS documentation for details on rendering intents and what - they do. - :param flags: Integer (0-...) specifying additional flags - :returns: A CmsTransform class object. - :exception PyCMSError: - """ - - if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <= 3): - msg = "renderingIntent must be an integer between 0 and 3" - raise PyCMSError(msg) - - if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG): - msg = f"flags must be an integer between 0 and {_MAX_FLAG}" - raise PyCMSError(msg) - - try: - if not isinstance(inputProfile, ImageCmsProfile): - inputProfile = ImageCmsProfile(inputProfile) - if not isinstance(outputProfile, ImageCmsProfile): - outputProfile = ImageCmsProfile(outputProfile) - return ImageCmsTransform( - inputProfile, outputProfile, inMode, outMode, renderingIntent, flags=flags - ) - except (OSError, TypeError, ValueError) as v: - raise PyCMSError(v) from v - - -def buildProofTransform( - inputProfile: _CmsProfileCompatible, - outputProfile: _CmsProfileCompatible, - proofProfile: _CmsProfileCompatible, - inMode: str, - outMode: str, - renderingIntent: Intent = Intent.PERCEPTUAL, - proofRenderingIntent: Intent = Intent.ABSOLUTE_COLORIMETRIC, - flags: Flags = Flags.SOFTPROOFING, -) -> ImageCmsTransform: - """ - (pyCMS) Builds an ICC transform mapping from the ``inputProfile`` to the - ``outputProfile``, but tries to simulate the result that would be - obtained on the ``proofProfile`` device. - - If the input, output, or proof profiles specified are not valid - filenames, a :exc:`PyCMSError` will be raised. - - If an error occurs during creation of the transform, - a :exc:`PyCMSError` will be raised. - - If ``inMode`` or ``outMode`` are not a mode supported by the ``outputProfile`` - (or by pyCMS), a :exc:`PyCMSError` will be raised. - - This function builds and returns an ICC transform from the ``inputProfile`` - to the ``outputProfile``, but tries to simulate the result that would be - obtained on the ``proofProfile`` device using ``renderingIntent`` and - ``proofRenderingIntent`` to determine what to do with out-of-gamut - colors. This is known as "soft-proofing". It will ONLY work for - converting images that are in ``inMode`` to images that are in outMode - color format (PIL mode, i.e. "RGB", "RGBA", "CMYK", etc.). - - Usage of the resulting transform object is exactly the same as with - ImageCms.buildTransform(). - - Proof profiling is generally used when using an output device to get a - good idea of what the final printed/displayed image would look like on - the ``proofProfile`` device when it's quicker and easier to use the - output device for judging color. Generally, this means that the - output device is a monitor, or a dye-sub printer (etc.), and the simulated - device is something more expensive, complicated, or time consuming - (making it difficult to make a real print for color judgement purposes). - - Soft-proofing basically functions by adjusting the colors on the - output device to match the colors of the device being simulated. However, - when the simulated device has a much wider gamut than the output - device, you may obtain marginal results. - - :param inputProfile: String, as a valid filename path to the ICC input - profile you wish to use for this transform, or a profile object - :param outputProfile: String, as a valid filename path to the ICC output - (monitor, usually) profile you wish to use for this transform, or a - profile object - :param proofProfile: String, as a valid filename path to the ICC proof - profile you wish to use for this transform, or a profile object - :param inMode: String, as a valid PIL mode that the appropriate profile - also supports (i.e. "RGB", "RGBA", "CMYK", etc.) - :param outMode: String, as a valid PIL mode that the appropriate profile - also supports (i.e. "RGB", "RGBA", "CMYK", etc.) - :param renderingIntent: Integer (0-3) specifying the rendering intent you - wish to use for the input->proof (simulated) transform - - ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT) - ImageCms.Intent.RELATIVE_COLORIMETRIC = 1 - ImageCms.Intent.SATURATION = 2 - ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3 - - see the pyCMS documentation for details on rendering intents and what - they do. - :param proofRenderingIntent: Integer (0-3) specifying the rendering intent - you wish to use for proof->output transform - - ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT) - ImageCms.Intent.RELATIVE_COLORIMETRIC = 1 - ImageCms.Intent.SATURATION = 2 - ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3 - - see the pyCMS documentation for details on rendering intents and what - they do. - :param flags: Integer (0-...) specifying additional flags - :returns: A CmsTransform class object. - :exception PyCMSError: - """ - - if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <= 3): - msg = "renderingIntent must be an integer between 0 and 3" - raise PyCMSError(msg) - - if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG): - msg = f"flags must be an integer between 0 and {_MAX_FLAG}" - raise PyCMSError(msg) - - try: - if not isinstance(inputProfile, ImageCmsProfile): - inputProfile = ImageCmsProfile(inputProfile) - if not isinstance(outputProfile, ImageCmsProfile): - outputProfile = ImageCmsProfile(outputProfile) - if not isinstance(proofProfile, ImageCmsProfile): - proofProfile = ImageCmsProfile(proofProfile) - return ImageCmsTransform( - inputProfile, - outputProfile, - inMode, - outMode, - renderingIntent, - proofProfile, - proofRenderingIntent, - flags, - ) - except (OSError, TypeError, ValueError) as v: - raise PyCMSError(v) from v - - -buildTransformFromOpenProfiles = buildTransform -buildProofTransformFromOpenProfiles = buildProofTransform - - -def applyTransform( - im: Image.Image, transform: ImageCmsTransform, inPlace: bool = False -) -> Image.Image | None: - """ - (pyCMS) Applies a transform to a given image. - - If ``im.mode != transform.input_mode``, a :exc:`PyCMSError` is raised. - - If ``inPlace`` is ``True`` and ``transform.input_mode != transform.output_mode``, a - :exc:`PyCMSError` is raised. - - If ``im.mode``, ``transform.input_mode`` or ``transform.output_mode`` is not - supported by pyCMSdll or the profiles you used for the transform, a - :exc:`PyCMSError` is raised. - - If an error occurs while the transform is being applied, - a :exc:`PyCMSError` is raised. - - This function applies a pre-calculated transform (from - ImageCms.buildTransform() or ImageCms.buildTransformFromOpenProfiles()) - to an image. The transform can be used for multiple images, saving - considerable calculation time if doing the same conversion multiple times. - - If you want to modify im in-place instead of receiving a new image as - the return value, set ``inPlace`` to ``True``. This can only be done if - ``transform.input_mode`` and ``transform.output_mode`` are the same, because we - can't change the mode in-place (the buffer sizes for some modes are - different). The default behavior is to return a new :py:class:`~PIL.Image.Image` - object of the same dimensions in mode ``transform.output_mode``. - - :param im: An :py:class:`~PIL.Image.Image` object, and ``im.mode`` must be the same - as the ``input_mode`` supported by the transform. - :param transform: A valid CmsTransform class object - :param inPlace: Bool. If ``True``, ``im`` is modified in place and ``None`` is - returned, if ``False``, a new :py:class:`~PIL.Image.Image` object with the - transform applied is returned (and ``im`` is not changed). The default is - ``False``. - :returns: Either ``None``, or a new :py:class:`~PIL.Image.Image` object, - depending on the value of ``inPlace``. The profile will be returned in - the image's ``info['icc_profile']``. - :exception PyCMSError: - """ - - try: - if inPlace: - transform.apply_in_place(im) - imOut = None - else: - imOut = transform.apply(im) - except (TypeError, ValueError) as v: - raise PyCMSError(v) from v - - return imOut - - -def createProfile( - colorSpace: Literal["LAB", "XYZ", "sRGB"], colorTemp: SupportsFloat = 0 -) -> core.CmsProfile: - """ - (pyCMS) Creates a profile. - - If colorSpace not in ``["LAB", "XYZ", "sRGB"]``, - a :exc:`PyCMSError` is raised. - - If using LAB and ``colorTemp`` is not a positive integer, - a :exc:`PyCMSError` is raised. - - If an error occurs while creating the profile, - a :exc:`PyCMSError` is raised. - - Use this function to create common profiles on-the-fly instead of - having to supply a profile on disk and knowing the path to it. It - returns a normal CmsProfile object that can be passed to - ImageCms.buildTransformFromOpenProfiles() to create a transform to apply - to images. - - :param colorSpace: String, the color space of the profile you wish to - create. - Currently only "LAB", "XYZ", and "sRGB" are supported. - :param colorTemp: Positive number for the white point for the profile, in - degrees Kelvin (i.e. 5000, 6500, 9600, etc.). The default is for D50 - illuminant if omitted (5000k). colorTemp is ONLY applied to LAB - profiles, and is ignored for XYZ and sRGB. - :returns: A CmsProfile class object - :exception PyCMSError: - """ - - if colorSpace not in ["LAB", "XYZ", "sRGB"]: - msg = ( - f"Color space not supported for on-the-fly profile creation ({colorSpace})" - ) - raise PyCMSError(msg) - - if colorSpace == "LAB": - try: - colorTemp = float(colorTemp) - except (TypeError, ValueError) as e: - msg = f'Color temperature must be numeric, "{colorTemp}" not valid' - raise PyCMSError(msg) from e - - try: - return core.createProfile(colorSpace, colorTemp) - except (TypeError, ValueError) as v: - raise PyCMSError(v) from v - - -def getProfileName(profile: _CmsProfileCompatible) -> str: - """ - - (pyCMS) Gets the internal product name for the given profile. - - If ``profile`` isn't a valid CmsProfile object or filename to a profile, - a :exc:`PyCMSError` is raised If an error occurs while trying - to obtain the name tag, a :exc:`PyCMSError` is raised. - - Use this function to obtain the INTERNAL name of the profile (stored - in an ICC tag in the profile itself), usually the one used when the - profile was originally created. Sometimes this tag also contains - additional information supplied by the creator. - - :param profile: EITHER a valid CmsProfile object, OR a string of the - filename of an ICC profile. - :returns: A string containing the internal name of the profile as stored - in an ICC tag. - :exception PyCMSError: - """ - - try: - # add an extra newline to preserve pyCMS compatibility - if not isinstance(profile, ImageCmsProfile): - profile = ImageCmsProfile(profile) - # do it in python, not c. - # // name was "%s - %s" (model, manufacturer) || Description , - # // but if the Model and Manufacturer were the same or the model - # // was long, Just the model, in 1.x - model = profile.profile.model - manufacturer = profile.profile.manufacturer - - if not (model or manufacturer): - return (profile.profile.profile_description or "") + "\n" - if not manufacturer or (model and len(model) > 30): - return f"{model}\n" - return f"{model} - {manufacturer}\n" - - except (AttributeError, OSError, TypeError, ValueError) as v: - raise PyCMSError(v) from v - - -def getProfileInfo(profile: _CmsProfileCompatible) -> str: - """ - (pyCMS) Gets the internal product information for the given profile. - - If ``profile`` isn't a valid CmsProfile object or filename to a profile, - a :exc:`PyCMSError` is raised. - - If an error occurs while trying to obtain the info tag, - a :exc:`PyCMSError` is raised. - - Use this function to obtain the information stored in the profile's - info tag. This often contains details about the profile, and how it - was created, as supplied by the creator. - - :param profile: EITHER a valid CmsProfile object, OR a string of the - filename of an ICC profile. - :returns: A string containing the internal profile information stored in - an ICC tag. - :exception PyCMSError: - """ - - try: - if not isinstance(profile, ImageCmsProfile): - profile = ImageCmsProfile(profile) - # add an extra newline to preserve pyCMS compatibility - # Python, not C. the white point bits weren't working well, - # so skipping. - # info was description \r\n\r\n copyright \r\n\r\n K007 tag \r\n\r\n whitepoint - description = profile.profile.profile_description - cpright = profile.profile.copyright - elements = [element for element in (description, cpright) if element] - return "\r\n\r\n".join(elements) + "\r\n\r\n" - - except (AttributeError, OSError, TypeError, ValueError) as v: - raise PyCMSError(v) from v - - -def getProfileCopyright(profile: _CmsProfileCompatible) -> str: - """ - (pyCMS) Gets the copyright for the given profile. - - If ``profile`` isn't a valid CmsProfile object or filename to a profile, a - :exc:`PyCMSError` is raised. - - If an error occurs while trying to obtain the copyright tag, - a :exc:`PyCMSError` is raised. - - Use this function to obtain the information stored in the profile's - copyright tag. - - :param profile: EITHER a valid CmsProfile object, OR a string of the - filename of an ICC profile. - :returns: A string containing the internal profile information stored in - an ICC tag. - :exception PyCMSError: - """ - try: - # add an extra newline to preserve pyCMS compatibility - if not isinstance(profile, ImageCmsProfile): - profile = ImageCmsProfile(profile) - return (profile.profile.copyright or "") + "\n" - except (AttributeError, OSError, TypeError, ValueError) as v: - raise PyCMSError(v) from v - - -def getProfileManufacturer(profile: _CmsProfileCompatible) -> str: - """ - (pyCMS) Gets the manufacturer for the given profile. - - If ``profile`` isn't a valid CmsProfile object or filename to a profile, a - :exc:`PyCMSError` is raised. - - If an error occurs while trying to obtain the manufacturer tag, a - :exc:`PyCMSError` is raised. - - Use this function to obtain the information stored in the profile's - manufacturer tag. - - :param profile: EITHER a valid CmsProfile object, OR a string of the - filename of an ICC profile. - :returns: A string containing the internal profile information stored in - an ICC tag. - :exception PyCMSError: - """ - try: - # add an extra newline to preserve pyCMS compatibility - if not isinstance(profile, ImageCmsProfile): - profile = ImageCmsProfile(profile) - return (profile.profile.manufacturer or "") + "\n" - except (AttributeError, OSError, TypeError, ValueError) as v: - raise PyCMSError(v) from v - - -def getProfileModel(profile: _CmsProfileCompatible) -> str: - """ - (pyCMS) Gets the model for the given profile. - - If ``profile`` isn't a valid CmsProfile object or filename to a profile, a - :exc:`PyCMSError` is raised. - - If an error occurs while trying to obtain the model tag, - a :exc:`PyCMSError` is raised. - - Use this function to obtain the information stored in the profile's - model tag. - - :param profile: EITHER a valid CmsProfile object, OR a string of the - filename of an ICC profile. - :returns: A string containing the internal profile information stored in - an ICC tag. - :exception PyCMSError: - """ - - try: - # add an extra newline to preserve pyCMS compatibility - if not isinstance(profile, ImageCmsProfile): - profile = ImageCmsProfile(profile) - return (profile.profile.model or "") + "\n" - except (AttributeError, OSError, TypeError, ValueError) as v: - raise PyCMSError(v) from v - - -def getProfileDescription(profile: _CmsProfileCompatible) -> str: - """ - (pyCMS) Gets the description for the given profile. - - If ``profile`` isn't a valid CmsProfile object or filename to a profile, a - :exc:`PyCMSError` is raised. - - If an error occurs while trying to obtain the description tag, - a :exc:`PyCMSError` is raised. - - Use this function to obtain the information stored in the profile's - description tag. - - :param profile: EITHER a valid CmsProfile object, OR a string of the - filename of an ICC profile. - :returns: A string containing the internal profile information stored in an - ICC tag. - :exception PyCMSError: - """ - - try: - # add an extra newline to preserve pyCMS compatibility - if not isinstance(profile, ImageCmsProfile): - profile = ImageCmsProfile(profile) - return (profile.profile.profile_description or "") + "\n" - except (AttributeError, OSError, TypeError, ValueError) as v: - raise PyCMSError(v) from v - - -def getDefaultIntent(profile: _CmsProfileCompatible) -> int: - """ - (pyCMS) Gets the default intent name for the given profile. - - If ``profile`` isn't a valid CmsProfile object or filename to a profile, a - :exc:`PyCMSError` is raised. - - If an error occurs while trying to obtain the default intent, a - :exc:`PyCMSError` is raised. - - Use this function to determine the default (and usually best optimized) - rendering intent for this profile. Most profiles support multiple - rendering intents, but are intended mostly for one type of conversion. - If you wish to use a different intent than returned, use - ImageCms.isIntentSupported() to verify it will work first. - - :param profile: EITHER a valid CmsProfile object, OR a string of the - filename of an ICC profile. - :returns: Integer 0-3 specifying the default rendering intent for this - profile. - - ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT) - ImageCms.Intent.RELATIVE_COLORIMETRIC = 1 - ImageCms.Intent.SATURATION = 2 - ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3 - - see the pyCMS documentation for details on rendering intents and what - they do. - :exception PyCMSError: - """ - - try: - if not isinstance(profile, ImageCmsProfile): - profile = ImageCmsProfile(profile) - return profile.profile.rendering_intent - except (AttributeError, OSError, TypeError, ValueError) as v: - raise PyCMSError(v) from v - - -def isIntentSupported( - profile: _CmsProfileCompatible, intent: Intent, direction: Direction -) -> Literal[-1, 1]: - """ - (pyCMS) Checks if a given intent is supported. - - Use this function to verify that you can use your desired - ``intent`` with ``profile``, and that ``profile`` can be used for the - input/output/proof profile as you desire. - - Some profiles are created specifically for one "direction", can cannot - be used for others. Some profiles can only be used for certain - rendering intents, so it's best to either verify this before trying - to create a transform with them (using this function), or catch the - potential :exc:`PyCMSError` that will occur if they don't - support the modes you select. - - :param profile: EITHER a valid CmsProfile object, OR a string of the - filename of an ICC profile. - :param intent: Integer (0-3) specifying the rendering intent you wish to - use with this profile - - ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT) - ImageCms.Intent.RELATIVE_COLORIMETRIC = 1 - ImageCms.Intent.SATURATION = 2 - ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3 - - see the pyCMS documentation for details on rendering intents and what - they do. - :param direction: Integer specifying if the profile is to be used for - input, output, or proof - - INPUT = 0 (or use ImageCms.Direction.INPUT) - OUTPUT = 1 (or use ImageCms.Direction.OUTPUT) - PROOF = 2 (or use ImageCms.Direction.PROOF) - - :returns: 1 if the intent/direction are supported, -1 if they are not. - :exception PyCMSError: - """ - - try: - if not isinstance(profile, ImageCmsProfile): - profile = ImageCmsProfile(profile) - # FIXME: I get different results for the same data w. different - # compilers. Bug in LittleCMS or in the binding? - if profile.profile.is_intent_supported(intent, direction): - return 1 - else: - return -1 - except (AttributeError, OSError, TypeError, ValueError) as v: - raise PyCMSError(v) from v - - -def versions() -> tuple[str, str | None, str, str]: - """ - (pyCMS) Fetches versions. - """ - - deprecate( - "PIL.ImageCms.versions()", - 12, - '(PIL.features.version("littlecms2"), sys.version, PIL.__version__)', - ) - return _VERSION, core.littlecms_version, sys.version.split()[0], __version__ +# The Python Imaging Library. +# $Id$ + +# Optional color management support, based on Kevin Cazabon's PyCMS +# library. + +# Originally released under LGPL. Graciously donated to PIL in +# March 2009, for distribution under the standard PIL license + +# History: + +# 2009-03-08 fl Added to PIL. + +# Copyright (C) 2002-2003 Kevin Cazabon +# Copyright (c) 2009 by Fredrik Lundh +# Copyright (c) 2013 by Eric Soroos + +# See the README file for information on usage and redistribution. See +# below for the original description. +from __future__ import annotations + +import operator +import sys +from enum import IntEnum, IntFlag +from functools import reduce +from typing import Any, Literal, SupportsFloat, SupportsInt, Union + +from . import Image, __version__ +from ._deprecate import deprecate +from ._typing import SupportsRead + +try: + from . import _imagingcms as core +except ImportError as ex: + # Allow error import for doc purposes, but error out when accessing + # anything in core. + from ._util import DeferredError + + core = DeferredError.new(ex) + +_DESCRIPTION = """ +pyCMS + + a Python / PIL interface to the littleCMS ICC Color Management System + Copyright (C) 2002-2003 Kevin Cazabon + kevin@cazabon.com + https://www.cazabon.com + + pyCMS home page: https://www.cazabon.com/pyCMS + littleCMS home page: https://www.littlecms.com + (littleCMS is Copyright (C) 1998-2001 Marti Maria) + + Originally released under LGPL. Graciously donated to PIL in + March 2009, for distribution under the standard PIL license + + The pyCMS.py module provides a "clean" interface between Python/PIL and + pyCMSdll, taking care of some of the more complex handling of the direct + pyCMSdll functions, as well as error-checking and making sure that all + relevant data is kept together. + + While it is possible to call pyCMSdll functions directly, it's not highly + recommended. + + Version History: + + 1.0.0 pil Oct 2013 Port to LCMS 2. + + 0.1.0 pil mod March 10, 2009 + + Renamed display profile to proof profile. The proof + profile is the profile of the device that is being + simulated, not the profile of the device which is + actually used to display/print the final simulation + (that'd be the output profile) - also see LCMSAPI.txt + input colorspace -> using 'renderingIntent' -> proof + colorspace -> using 'proofRenderingIntent' -> output + colorspace + + Added LCMS FLAGS support. + Added FLAGS["SOFTPROOFING"] as default flag for + buildProofTransform (otherwise the proof profile/intent + would be ignored). + + 0.1.0 pil March 2009 - added to PIL, as PIL.ImageCms + + 0.0.2 alpha Jan 6, 2002 + + Added try/except statements around type() checks of + potential CObjects... Python won't let you use type() + on them, and raises a TypeError (stupid, if you ask + me!) + + Added buildProofTransformFromOpenProfiles() function. + Additional fixes in DLL, see DLL code for details. + + 0.0.1 alpha first public release, Dec. 26, 2002 + + Known to-do list with current version (of Python interface, not pyCMSdll): + + none + +""" + +_VERSION = "1.0.0 pil" + + +def __getattr__(name: str) -> Any: + if name == "DESCRIPTION": + deprecate("PIL.ImageCms.DESCRIPTION", 12) + return _DESCRIPTION + elif name == "VERSION": + deprecate("PIL.ImageCms.VERSION", 12) + return _VERSION + elif name == "FLAGS": + deprecate("PIL.ImageCms.FLAGS", 12, "PIL.ImageCms.Flags") + return _FLAGS + msg = f"module '{__name__}' has no attribute '{name}'" + raise AttributeError(msg) + + +# --------------------------------------------------------------------. + + +# +# intent/direction values + + +class Intent(IntEnum): + PERCEPTUAL = 0 + RELATIVE_COLORIMETRIC = 1 + SATURATION = 2 + ABSOLUTE_COLORIMETRIC = 3 + + +class Direction(IntEnum): + INPUT = 0 + OUTPUT = 1 + PROOF = 2 + + +# +# flags + + +class Flags(IntFlag): + """Flags and documentation are taken from ``lcms2.h``.""" + + NONE = 0 + NOCACHE = 0x0040 + """Inhibit 1-pixel cache""" + NOOPTIMIZE = 0x0100 + """Inhibit optimizations""" + NULLTRANSFORM = 0x0200 + """Don't transform anyway""" + GAMUTCHECK = 0x1000 + """Out of Gamut alarm""" + SOFTPROOFING = 0x4000 + """Do softproofing""" + BLACKPOINTCOMPENSATION = 0x2000 + NOWHITEONWHITEFIXUP = 0x0004 + """Don't fix scum dot""" + HIGHRESPRECALC = 0x0400 + """Use more memory to give better accuracy""" + LOWRESPRECALC = 0x0800 + """Use less memory to minimize resources""" + # this should be 8BITS_DEVICELINK, but that is not a valid name in Python: + USE_8BITS_DEVICELINK = 0x0008 + """Create 8 bits devicelinks""" + GUESSDEVICECLASS = 0x0020 + """Guess device class (for ``transform2devicelink``)""" + KEEP_SEQUENCE = 0x0080 + """Keep profile sequence for devicelink creation""" + FORCE_CLUT = 0x0002 + """Force CLUT optimization""" + CLUT_POST_LINEARIZATION = 0x0001 + """create postlinearization tables if possible""" + CLUT_PRE_LINEARIZATION = 0x0010 + """create prelinearization tables if possible""" + NONEGATIVES = 0x8000 + """Prevent negative numbers in floating point transforms""" + COPY_ALPHA = 0x04000000 + """Alpha channels are copied on ``cmsDoTransform()``""" + NODEFAULTRESOURCEDEF = 0x01000000 + + _GRIDPOINTS_1 = 1 << 16 + _GRIDPOINTS_2 = 2 << 16 + _GRIDPOINTS_4 = 4 << 16 + _GRIDPOINTS_8 = 8 << 16 + _GRIDPOINTS_16 = 16 << 16 + _GRIDPOINTS_32 = 32 << 16 + _GRIDPOINTS_64 = 64 << 16 + _GRIDPOINTS_128 = 128 << 16 + + @staticmethod + def GRIDPOINTS(n: int) -> Flags: + """ + Fine-tune control over number of gridpoints + + :param n: :py:class:`int` in range ``0 <= n <= 255`` + """ + return Flags.NONE | ((n & 0xFF) << 16) + + +_MAX_FLAG = reduce(operator.or_, Flags) + + +_FLAGS = { + "MATRIXINPUT": 1, + "MATRIXOUTPUT": 2, + "MATRIXONLY": (1 | 2), + "NOWHITEONWHITEFIXUP": 4, # Don't hot fix scum dot + # Don't create prelinearization tables on precalculated transforms + # (internal use): + "NOPRELINEARIZATION": 16, + "GUESSDEVICECLASS": 32, # Guess device class (for transform2devicelink) + "NOTCACHE": 64, # Inhibit 1-pixel cache + "NOTPRECALC": 256, + "NULLTRANSFORM": 512, # Don't transform anyway + "HIGHRESPRECALC": 1024, # Use more memory to give better accuracy + "LOWRESPRECALC": 2048, # Use less memory to minimize resources + "WHITEBLACKCOMPENSATION": 8192, + "BLACKPOINTCOMPENSATION": 8192, + "GAMUTCHECK": 4096, # Out of Gamut alarm + "SOFTPROOFING": 16384, # Do softproofing + "PRESERVEBLACK": 32768, # Black preservation + "NODEFAULTRESOURCEDEF": 16777216, # CRD special + "GRIDPOINTS": lambda n: (n & 0xFF) << 16, # Gridpoints +} + + +# --------------------------------------------------------------------. +# Experimental PIL-level API +# --------------------------------------------------------------------. + +## +# Profile. + + +class ImageCmsProfile: + def __init__(self, profile: str | SupportsRead[bytes] | core.CmsProfile) -> None: + """ + :param profile: Either a string representing a filename, + a file like object containing a profile or a + low-level profile object + + """ + + if isinstance(profile, str): + if sys.platform == "win32": + profile_bytes_path = profile.encode() + try: + profile_bytes_path.decode("ascii") + except UnicodeDecodeError: + with open(profile, "rb") as f: + self._set(core.profile_frombytes(f.read())) + return + self._set(core.profile_open(profile), profile) + elif hasattr(profile, "read"): + self._set(core.profile_frombytes(profile.read())) + elif isinstance(profile, core.CmsProfile): + self._set(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 + saved images. + + :returns: a bytes object containing the ICC profile. + """ + + return core.profile_tobytes(self.profile) + + +class ImageCmsTransform(Image.ImagePointHandler): + """ + Transform. This can be used with the procedural API, or with the standard + :py:func:`~PIL.Image.Image.point` method. + + Will return the output profile in the ``output.info['icc_profile']``. + """ + + def __init__( + self, + input: ImageCmsProfile, + output: ImageCmsProfile, + input_mode: str, + output_mode: str, + intent: Intent = Intent.PERCEPTUAL, + proof: ImageCmsProfile | None = None, + proof_intent: Intent = Intent.ABSOLUTE_COLORIMETRIC, + flags: Flags = Flags.NONE, + ): + if proof is None: + self.transform = core.buildTransform( + input.profile, output.profile, input_mode, output_mode, intent, flags + ) + else: + self.transform = core.buildProofTransform( + input.profile, + output.profile, + proof.profile, + input_mode, + output_mode, + intent, + proof_intent, + flags, + ) + # Note: inputMode and outputMode are for pyCMS compatibility only + self.input_mode = self.inputMode = input_mode + self.output_mode = self.outputMode = output_mode + + self.output_profile = output + + def point(self, im: Image.Image) -> Image.Image: + return self.apply(im) + + def apply(self, im: Image.Image, imOut: Image.Image | None = None) -> Image.Image: + im.load() + if imOut is None: + imOut = Image.new(self.output_mode, im.size, None) + self.transform.apply(im.im.id, imOut.im.id) + imOut.info["icc_profile"] = self.output_profile.tobytes() + return imOut + + def apply_in_place(self, im: Image.Image) -> Image.Image: + im.load() + if im.mode != self.output_mode: + msg = "mode mismatch" + raise ValueError(msg) # wrong output mode + self.transform.apply(im.im.id, im.im.id) + im.info["icc_profile"] = self.output_profile.tobytes() + return im + + +def get_display_profile(handle: SupportsInt | None = None) -> ImageCmsProfile | None: + """ + (experimental) Fetches the profile for the current display device. + + :returns: ``None`` if the profile is not known. + """ + + if sys.platform != "win32": + return None + + from . import ImageWin # type: ignore[unused-ignore, unreachable] + + if isinstance(handle, ImageWin.HDC): + profile = core.get_display_profile_win32(int(handle), 1) + else: + profile = core.get_display_profile_win32(int(handle or 0)) + if profile is None: + return None + return ImageCmsProfile(profile) + + +# --------------------------------------------------------------------. +# pyCMS compatible layer +# --------------------------------------------------------------------. + +_CmsProfileCompatible = Union[ + str, SupportsRead[bytes], core.CmsProfile, ImageCmsProfile +] + + +class PyCMSError(Exception): + """(pyCMS) Exception class. + This is used for all errors in the pyCMS API.""" + + pass + + +def profileToProfile( + im: Image.Image, + inputProfile: _CmsProfileCompatible, + outputProfile: _CmsProfileCompatible, + renderingIntent: Intent = Intent.PERCEPTUAL, + outputMode: str | None = None, + inPlace: bool = False, + flags: Flags = Flags.NONE, +) -> Image.Image | None: + """ + (pyCMS) Applies an ICC transformation to a given image, mapping from + ``inputProfile`` to ``outputProfile``. + + If the input or output profiles specified are not valid filenames, a + :exc:`PyCMSError` will be raised. If ``inPlace`` is ``True`` and + ``outputMode != im.mode``, a :exc:`PyCMSError` will be raised. + If an error occurs during application of the profiles, + a :exc:`PyCMSError` will be raised. + If ``outputMode`` is not a mode supported by the ``outputProfile`` (or by pyCMS), + a :exc:`PyCMSError` will be raised. + + This function applies an ICC transformation to im from ``inputProfile``'s + color space to ``outputProfile``'s color space using the specified rendering + intent to decide how to handle out-of-gamut colors. + + ``outputMode`` can be used to specify that a color mode conversion is to + be done using these profiles, but the specified profiles must be able + to handle that mode. I.e., if converting im from RGB to CMYK using + profiles, the input profile must handle RGB data, and the output + profile must handle CMYK data. + + :param im: An open :py:class:`~PIL.Image.Image` object (i.e. Image.new(...) + or Image.open(...), etc.) + :param inputProfile: String, as a valid filename path to the ICC input + profile you wish to use for this image, or a profile object + :param outputProfile: String, as a valid filename path to the ICC output + profile you wish to use for this image, or a profile object + :param renderingIntent: Integer (0-3) specifying the rendering intent you + wish to use for the transform + + ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT) + ImageCms.Intent.RELATIVE_COLORIMETRIC = 1 + ImageCms.Intent.SATURATION = 2 + ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3 + + see the pyCMS documentation for details on rendering intents and what + they do. + :param outputMode: A valid PIL mode for the output image (i.e. "RGB", + "CMYK", etc.). Note: if rendering the image "inPlace", outputMode + MUST be the same mode as the input, or omitted completely. If + omitted, the outputMode will be the same as the mode of the input + image (im.mode) + :param inPlace: Boolean. If ``True``, the original image is modified in-place, + and ``None`` is returned. If ``False`` (default), a new + :py:class:`~PIL.Image.Image` object is returned with the transform applied. + :param flags: Integer (0-...) specifying additional flags + :returns: Either None or a new :py:class:`~PIL.Image.Image` object, depending on + the value of ``inPlace`` + :exception PyCMSError: + """ + + if outputMode is None: + outputMode = im.mode + + if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <= 3): + msg = "renderingIntent must be an integer between 0 and 3" + raise PyCMSError(msg) + + if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG): + msg = f"flags must be an integer between 0 and {_MAX_FLAG}" + raise PyCMSError(msg) + + try: + if not isinstance(inputProfile, ImageCmsProfile): + inputProfile = ImageCmsProfile(inputProfile) + if not isinstance(outputProfile, ImageCmsProfile): + outputProfile = ImageCmsProfile(outputProfile) + transform = ImageCmsTransform( + inputProfile, + outputProfile, + im.mode, + outputMode, + renderingIntent, + flags=flags, + ) + if inPlace: + transform.apply_in_place(im) + imOut = None + else: + imOut = transform.apply(im) + except (OSError, TypeError, ValueError) as v: + raise PyCMSError(v) from v + + return imOut + + +def getOpenProfile( + profileFilename: str | SupportsRead[bytes] | core.CmsProfile, +) -> ImageCmsProfile: + """ + (pyCMS) Opens an ICC profile file. + + The PyCMSProfile object can be passed back into pyCMS for use in creating + transforms and such (as in ImageCms.buildTransformFromOpenProfiles()). + + If ``profileFilename`` is not a valid filename for an ICC profile, + a :exc:`PyCMSError` will be raised. + + :param profileFilename: String, as a valid filename path to the ICC profile + you wish to open, or a file-like object. + :returns: A CmsProfile class object. + :exception PyCMSError: + """ + + try: + return ImageCmsProfile(profileFilename) + except (OSError, TypeError, ValueError) as v: + raise PyCMSError(v) from v + + +def buildTransform( + inputProfile: _CmsProfileCompatible, + outputProfile: _CmsProfileCompatible, + inMode: str, + outMode: str, + renderingIntent: Intent = Intent.PERCEPTUAL, + flags: Flags = Flags.NONE, +) -> ImageCmsTransform: + """ + (pyCMS) Builds an ICC transform mapping from the ``inputProfile`` to the + ``outputProfile``. Use applyTransform to apply the transform to a given + image. + + If the input or output profiles specified are not valid filenames, a + :exc:`PyCMSError` will be raised. If an error occurs during creation + of the transform, a :exc:`PyCMSError` will be raised. + + If ``inMode`` or ``outMode`` are not a mode supported by the ``outputProfile`` + (or by pyCMS), a :exc:`PyCMSError` will be raised. + + This function builds and returns an ICC transform from the ``inputProfile`` + to the ``outputProfile`` using the ``renderingIntent`` to determine what to do + with out-of-gamut colors. It will ONLY work for converting images that + are in ``inMode`` to images that are in ``outMode`` color format (PIL mode, + i.e. "RGB", "RGBA", "CMYK", etc.). + + Building the transform is a fair part of the overhead in + ImageCms.profileToProfile(), so if you're planning on converting multiple + images using the same input/output settings, this can save you time. + Once you have a transform object, it can be used with + ImageCms.applyProfile() to convert images without the need to re-compute + the lookup table for the transform. + + The reason pyCMS returns a class object rather than a handle directly + to the transform is that it needs to keep track of the PIL input/output + modes that the transform is meant for. These attributes are stored in + the ``inMode`` and ``outMode`` attributes of the object (which can be + manually overridden if you really want to, but I don't know of any + time that would be of use, or would even work). + + :param inputProfile: String, as a valid filename path to the ICC input + profile you wish to use for this transform, or a profile object + :param outputProfile: String, as a valid filename path to the ICC output + profile you wish to use for this transform, or a profile object + :param inMode: String, as a valid PIL mode that the appropriate profile + also supports (i.e. "RGB", "RGBA", "CMYK", etc.) + :param outMode: String, as a valid PIL mode that the appropriate profile + also supports (i.e. "RGB", "RGBA", "CMYK", etc.) + :param renderingIntent: Integer (0-3) specifying the rendering intent you + wish to use for the transform + + ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT) + ImageCms.Intent.RELATIVE_COLORIMETRIC = 1 + ImageCms.Intent.SATURATION = 2 + ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3 + + see the pyCMS documentation for details on rendering intents and what + they do. + :param flags: Integer (0-...) specifying additional flags + :returns: A CmsTransform class object. + :exception PyCMSError: + """ + + if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <= 3): + msg = "renderingIntent must be an integer between 0 and 3" + raise PyCMSError(msg) + + if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG): + msg = f"flags must be an integer between 0 and {_MAX_FLAG}" + raise PyCMSError(msg) + + try: + if not isinstance(inputProfile, ImageCmsProfile): + inputProfile = ImageCmsProfile(inputProfile) + if not isinstance(outputProfile, ImageCmsProfile): + outputProfile = ImageCmsProfile(outputProfile) + return ImageCmsTransform( + inputProfile, outputProfile, inMode, outMode, renderingIntent, flags=flags + ) + except (OSError, TypeError, ValueError) as v: + raise PyCMSError(v) from v + + +def buildProofTransform( + inputProfile: _CmsProfileCompatible, + outputProfile: _CmsProfileCompatible, + proofProfile: _CmsProfileCompatible, + inMode: str, + outMode: str, + renderingIntent: Intent = Intent.PERCEPTUAL, + proofRenderingIntent: Intent = Intent.ABSOLUTE_COLORIMETRIC, + flags: Flags = Flags.SOFTPROOFING, +) -> ImageCmsTransform: + """ + (pyCMS) Builds an ICC transform mapping from the ``inputProfile`` to the + ``outputProfile``, but tries to simulate the result that would be + obtained on the ``proofProfile`` device. + + If the input, output, or proof profiles specified are not valid + filenames, a :exc:`PyCMSError` will be raised. + + If an error occurs during creation of the transform, + a :exc:`PyCMSError` will be raised. + + If ``inMode`` or ``outMode`` are not a mode supported by the ``outputProfile`` + (or by pyCMS), a :exc:`PyCMSError` will be raised. + + This function builds and returns an ICC transform from the ``inputProfile`` + to the ``outputProfile``, but tries to simulate the result that would be + obtained on the ``proofProfile`` device using ``renderingIntent`` and + ``proofRenderingIntent`` to determine what to do with out-of-gamut + colors. This is known as "soft-proofing". It will ONLY work for + converting images that are in ``inMode`` to images that are in outMode + color format (PIL mode, i.e. "RGB", "RGBA", "CMYK", etc.). + + Usage of the resulting transform object is exactly the same as with + ImageCms.buildTransform(). + + Proof profiling is generally used when using an output device to get a + good idea of what the final printed/displayed image would look like on + the ``proofProfile`` device when it's quicker and easier to use the + output device for judging color. Generally, this means that the + output device is a monitor, or a dye-sub printer (etc.), and the simulated + device is something more expensive, complicated, or time consuming + (making it difficult to make a real print for color judgement purposes). + + Soft-proofing basically functions by adjusting the colors on the + output device to match the colors of the device being simulated. However, + when the simulated device has a much wider gamut than the output + device, you may obtain marginal results. + + :param inputProfile: String, as a valid filename path to the ICC input + profile you wish to use for this transform, or a profile object + :param outputProfile: String, as a valid filename path to the ICC output + (monitor, usually) profile you wish to use for this transform, or a + profile object + :param proofProfile: String, as a valid filename path to the ICC proof + profile you wish to use for this transform, or a profile object + :param inMode: String, as a valid PIL mode that the appropriate profile + also supports (i.e. "RGB", "RGBA", "CMYK", etc.) + :param outMode: String, as a valid PIL mode that the appropriate profile + also supports (i.e. "RGB", "RGBA", "CMYK", etc.) + :param renderingIntent: Integer (0-3) specifying the rendering intent you + wish to use for the input->proof (simulated) transform + + ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT) + ImageCms.Intent.RELATIVE_COLORIMETRIC = 1 + ImageCms.Intent.SATURATION = 2 + ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3 + + see the pyCMS documentation for details on rendering intents and what + they do. + :param proofRenderingIntent: Integer (0-3) specifying the rendering intent + you wish to use for proof->output transform + + ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT) + ImageCms.Intent.RELATIVE_COLORIMETRIC = 1 + ImageCms.Intent.SATURATION = 2 + ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3 + + see the pyCMS documentation for details on rendering intents and what + they do. + :param flags: Integer (0-...) specifying additional flags + :returns: A CmsTransform class object. + :exception PyCMSError: + """ + + if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <= 3): + msg = "renderingIntent must be an integer between 0 and 3" + raise PyCMSError(msg) + + if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG): + msg = f"flags must be an integer between 0 and {_MAX_FLAG}" + raise PyCMSError(msg) + + try: + if not isinstance(inputProfile, ImageCmsProfile): + inputProfile = ImageCmsProfile(inputProfile) + if not isinstance(outputProfile, ImageCmsProfile): + outputProfile = ImageCmsProfile(outputProfile) + if not isinstance(proofProfile, ImageCmsProfile): + proofProfile = ImageCmsProfile(proofProfile) + return ImageCmsTransform( + inputProfile, + outputProfile, + inMode, + outMode, + renderingIntent, + proofProfile, + proofRenderingIntent, + flags, + ) + except (OSError, TypeError, ValueError) as v: + raise PyCMSError(v) from v + + +buildTransformFromOpenProfiles = buildTransform +buildProofTransformFromOpenProfiles = buildProofTransform + + +def applyTransform( + im: Image.Image, transform: ImageCmsTransform, inPlace: bool = False +) -> Image.Image | None: + """ + (pyCMS) Applies a transform to a given image. + + If ``im.mode != transform.input_mode``, a :exc:`PyCMSError` is raised. + + If ``inPlace`` is ``True`` and ``transform.input_mode != transform.output_mode``, a + :exc:`PyCMSError` is raised. + + If ``im.mode``, ``transform.input_mode`` or ``transform.output_mode`` is not + supported by pyCMSdll or the profiles you used for the transform, a + :exc:`PyCMSError` is raised. + + If an error occurs while the transform is being applied, + a :exc:`PyCMSError` is raised. + + This function applies a pre-calculated transform (from + ImageCms.buildTransform() or ImageCms.buildTransformFromOpenProfiles()) + to an image. The transform can be used for multiple images, saving + considerable calculation time if doing the same conversion multiple times. + + If you want to modify im in-place instead of receiving a new image as + the return value, set ``inPlace`` to ``True``. This can only be done if + ``transform.input_mode`` and ``transform.output_mode`` are the same, because we + can't change the mode in-place (the buffer sizes for some modes are + different). The default behavior is to return a new :py:class:`~PIL.Image.Image` + object of the same dimensions in mode ``transform.output_mode``. + + :param im: An :py:class:`~PIL.Image.Image` object, and ``im.mode`` must be the same + as the ``input_mode`` supported by the transform. + :param transform: A valid CmsTransform class object + :param inPlace: Bool. If ``True``, ``im`` is modified in place and ``None`` is + returned, if ``False``, a new :py:class:`~PIL.Image.Image` object with the + transform applied is returned (and ``im`` is not changed). The default is + ``False``. + :returns: Either ``None``, or a new :py:class:`~PIL.Image.Image` object, + depending on the value of ``inPlace``. The profile will be returned in + the image's ``info['icc_profile']``. + :exception PyCMSError: + """ + + try: + if inPlace: + transform.apply_in_place(im) + imOut = None + else: + imOut = transform.apply(im) + except (TypeError, ValueError) as v: + raise PyCMSError(v) from v + + return imOut + + +def createProfile( + colorSpace: Literal["LAB", "XYZ", "sRGB"], colorTemp: SupportsFloat = 0 +) -> core.CmsProfile: + """ + (pyCMS) Creates a profile. + + If colorSpace not in ``["LAB", "XYZ", "sRGB"]``, + a :exc:`PyCMSError` is raised. + + If using LAB and ``colorTemp`` is not a positive integer, + a :exc:`PyCMSError` is raised. + + If an error occurs while creating the profile, + a :exc:`PyCMSError` is raised. + + Use this function to create common profiles on-the-fly instead of + having to supply a profile on disk and knowing the path to it. It + returns a normal CmsProfile object that can be passed to + ImageCms.buildTransformFromOpenProfiles() to create a transform to apply + to images. + + :param colorSpace: String, the color space of the profile you wish to + create. + Currently only "LAB", "XYZ", and "sRGB" are supported. + :param colorTemp: Positive number for the white point for the profile, in + degrees Kelvin (i.e. 5000, 6500, 9600, etc.). The default is for D50 + illuminant if omitted (5000k). colorTemp is ONLY applied to LAB + profiles, and is ignored for XYZ and sRGB. + :returns: A CmsProfile class object + :exception PyCMSError: + """ + + if colorSpace not in ["LAB", "XYZ", "sRGB"]: + msg = ( + f"Color space not supported for on-the-fly profile creation ({colorSpace})" + ) + raise PyCMSError(msg) + + if colorSpace == "LAB": + try: + colorTemp = float(colorTemp) + except (TypeError, ValueError) as e: + msg = f'Color temperature must be numeric, "{colorTemp}" not valid' + raise PyCMSError(msg) from e + + try: + return core.createProfile(colorSpace, colorTemp) + except (TypeError, ValueError) as v: + raise PyCMSError(v) from v + + +def getProfileName(profile: _CmsProfileCompatible) -> str: + """ + + (pyCMS) Gets the internal product name for the given profile. + + If ``profile`` isn't a valid CmsProfile object or filename to a profile, + a :exc:`PyCMSError` is raised If an error occurs while trying + to obtain the name tag, a :exc:`PyCMSError` is raised. + + Use this function to obtain the INTERNAL name of the profile (stored + in an ICC tag in the profile itself), usually the one used when the + profile was originally created. Sometimes this tag also contains + additional information supplied by the creator. + + :param profile: EITHER a valid CmsProfile object, OR a string of the + filename of an ICC profile. + :returns: A string containing the internal name of the profile as stored + in an ICC tag. + :exception PyCMSError: + """ + + try: + # add an extra newline to preserve pyCMS compatibility + if not isinstance(profile, ImageCmsProfile): + profile = ImageCmsProfile(profile) + # do it in python, not c. + # // name was "%s - %s" (model, manufacturer) || Description , + # // but if the Model and Manufacturer were the same or the model + # // was long, Just the model, in 1.x + model = profile.profile.model + manufacturer = profile.profile.manufacturer + + if not (model or manufacturer): + return (profile.profile.profile_description or "") + "\n" + if not manufacturer or (model and len(model) > 30): + return f"{model}\n" + return f"{model} - {manufacturer}\n" + + except (AttributeError, OSError, TypeError, ValueError) as v: + raise PyCMSError(v) from v + + +def getProfileInfo(profile: _CmsProfileCompatible) -> str: + """ + (pyCMS) Gets the internal product information for the given profile. + + If ``profile`` isn't a valid CmsProfile object or filename to a profile, + a :exc:`PyCMSError` is raised. + + If an error occurs while trying to obtain the info tag, + a :exc:`PyCMSError` is raised. + + Use this function to obtain the information stored in the profile's + info tag. This often contains details about the profile, and how it + was created, as supplied by the creator. + + :param profile: EITHER a valid CmsProfile object, OR a string of the + filename of an ICC profile. + :returns: A string containing the internal profile information stored in + an ICC tag. + :exception PyCMSError: + """ + + try: + if not isinstance(profile, ImageCmsProfile): + profile = ImageCmsProfile(profile) + # add an extra newline to preserve pyCMS compatibility + # Python, not C. the white point bits weren't working well, + # so skipping. + # info was description \r\n\r\n copyright \r\n\r\n K007 tag \r\n\r\n whitepoint + description = profile.profile.profile_description + cpright = profile.profile.copyright + elements = [element for element in (description, cpright) if element] + return "\r\n\r\n".join(elements) + "\r\n\r\n" + + except (AttributeError, OSError, TypeError, ValueError) as v: + raise PyCMSError(v) from v + + +def getProfileCopyright(profile: _CmsProfileCompatible) -> str: + """ + (pyCMS) Gets the copyright for the given profile. + + If ``profile`` isn't a valid CmsProfile object or filename to a profile, a + :exc:`PyCMSError` is raised. + + If an error occurs while trying to obtain the copyright tag, + a :exc:`PyCMSError` is raised. + + Use this function to obtain the information stored in the profile's + copyright tag. + + :param profile: EITHER a valid CmsProfile object, OR a string of the + filename of an ICC profile. + :returns: A string containing the internal profile information stored in + an ICC tag. + :exception PyCMSError: + """ + try: + # add an extra newline to preserve pyCMS compatibility + if not isinstance(profile, ImageCmsProfile): + profile = ImageCmsProfile(profile) + return (profile.profile.copyright or "") + "\n" + except (AttributeError, OSError, TypeError, ValueError) as v: + raise PyCMSError(v) from v + + +def getProfileManufacturer(profile: _CmsProfileCompatible) -> str: + """ + (pyCMS) Gets the manufacturer for the given profile. + + If ``profile`` isn't a valid CmsProfile object or filename to a profile, a + :exc:`PyCMSError` is raised. + + If an error occurs while trying to obtain the manufacturer tag, a + :exc:`PyCMSError` is raised. + + Use this function to obtain the information stored in the profile's + manufacturer tag. + + :param profile: EITHER a valid CmsProfile object, OR a string of the + filename of an ICC profile. + :returns: A string containing the internal profile information stored in + an ICC tag. + :exception PyCMSError: + """ + try: + # add an extra newline to preserve pyCMS compatibility + if not isinstance(profile, ImageCmsProfile): + profile = ImageCmsProfile(profile) + return (profile.profile.manufacturer or "") + "\n" + except (AttributeError, OSError, TypeError, ValueError) as v: + raise PyCMSError(v) from v + + +def getProfileModel(profile: _CmsProfileCompatible) -> str: + """ + (pyCMS) Gets the model for the given profile. + + If ``profile`` isn't a valid CmsProfile object or filename to a profile, a + :exc:`PyCMSError` is raised. + + If an error occurs while trying to obtain the model tag, + a :exc:`PyCMSError` is raised. + + Use this function to obtain the information stored in the profile's + model tag. + + :param profile: EITHER a valid CmsProfile object, OR a string of the + filename of an ICC profile. + :returns: A string containing the internal profile information stored in + an ICC tag. + :exception PyCMSError: + """ + + try: + # add an extra newline to preserve pyCMS compatibility + if not isinstance(profile, ImageCmsProfile): + profile = ImageCmsProfile(profile) + return (profile.profile.model or "") + "\n" + except (AttributeError, OSError, TypeError, ValueError) as v: + raise PyCMSError(v) from v + + +def getProfileDescription(profile: _CmsProfileCompatible) -> str: + """ + (pyCMS) Gets the description for the given profile. + + If ``profile`` isn't a valid CmsProfile object or filename to a profile, a + :exc:`PyCMSError` is raised. + + If an error occurs while trying to obtain the description tag, + a :exc:`PyCMSError` is raised. + + Use this function to obtain the information stored in the profile's + description tag. + + :param profile: EITHER a valid CmsProfile object, OR a string of the + filename of an ICC profile. + :returns: A string containing the internal profile information stored in an + ICC tag. + :exception PyCMSError: + """ + + try: + # add an extra newline to preserve pyCMS compatibility + if not isinstance(profile, ImageCmsProfile): + profile = ImageCmsProfile(profile) + return (profile.profile.profile_description or "") + "\n" + except (AttributeError, OSError, TypeError, ValueError) as v: + raise PyCMSError(v) from v + + +def getDefaultIntent(profile: _CmsProfileCompatible) -> int: + """ + (pyCMS) Gets the default intent name for the given profile. + + If ``profile`` isn't a valid CmsProfile object or filename to a profile, a + :exc:`PyCMSError` is raised. + + If an error occurs while trying to obtain the default intent, a + :exc:`PyCMSError` is raised. + + Use this function to determine the default (and usually best optimized) + rendering intent for this profile. Most profiles support multiple + rendering intents, but are intended mostly for one type of conversion. + If you wish to use a different intent than returned, use + ImageCms.isIntentSupported() to verify it will work first. + + :param profile: EITHER a valid CmsProfile object, OR a string of the + filename of an ICC profile. + :returns: Integer 0-3 specifying the default rendering intent for this + profile. + + ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT) + ImageCms.Intent.RELATIVE_COLORIMETRIC = 1 + ImageCms.Intent.SATURATION = 2 + ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3 + + see the pyCMS documentation for details on rendering intents and what + they do. + :exception PyCMSError: + """ + + try: + if not isinstance(profile, ImageCmsProfile): + profile = ImageCmsProfile(profile) + return profile.profile.rendering_intent + except (AttributeError, OSError, TypeError, ValueError) as v: + raise PyCMSError(v) from v + + +def isIntentSupported( + profile: _CmsProfileCompatible, intent: Intent, direction: Direction +) -> Literal[-1, 1]: + """ + (pyCMS) Checks if a given intent is supported. + + Use this function to verify that you can use your desired + ``intent`` with ``profile``, and that ``profile`` can be used for the + input/output/proof profile as you desire. + + Some profiles are created specifically for one "direction", can cannot + be used for others. Some profiles can only be used for certain + rendering intents, so it's best to either verify this before trying + to create a transform with them (using this function), or catch the + potential :exc:`PyCMSError` that will occur if they don't + support the modes you select. + + :param profile: EITHER a valid CmsProfile object, OR a string of the + filename of an ICC profile. + :param intent: Integer (0-3) specifying the rendering intent you wish to + use with this profile + + ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT) + ImageCms.Intent.RELATIVE_COLORIMETRIC = 1 + ImageCms.Intent.SATURATION = 2 + ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3 + + see the pyCMS documentation for details on rendering intents and what + they do. + :param direction: Integer specifying if the profile is to be used for + input, output, or proof + + INPUT = 0 (or use ImageCms.Direction.INPUT) + OUTPUT = 1 (or use ImageCms.Direction.OUTPUT) + PROOF = 2 (or use ImageCms.Direction.PROOF) + + :returns: 1 if the intent/direction are supported, -1 if they are not. + :exception PyCMSError: + """ + + try: + if not isinstance(profile, ImageCmsProfile): + profile = ImageCmsProfile(profile) + # FIXME: I get different results for the same data w. different + # compilers. Bug in LittleCMS or in the binding? + if profile.profile.is_intent_supported(intent, direction): + return 1 + else: + return -1 + except (AttributeError, OSError, TypeError, ValueError) as v: + raise PyCMSError(v) from v + + +def versions() -> tuple[str, str | None, str, str]: + """ + (pyCMS) Fetches versions. + """ + + deprecate( + "PIL.ImageCms.versions()", + 12, + '(PIL.features.version("littlecms2"), sys.version, PIL.__version__)', + ) + return _VERSION, core.littlecms_version, sys.version.split()[0], __version__ diff --git a/src/PIL/ImageColor.py b/src/PIL/ImageColor.py index 9a15a8eb7..4dcc33bf1 100644 --- a/src/PIL/ImageColor.py +++ b/src/PIL/ImageColor.py @@ -1,320 +1,320 @@ -# -# The Python Imaging Library -# $Id$ -# -# map CSS3-style colour description strings to RGB -# -# History: -# 2002-10-24 fl Added support for CSS-style color strings -# 2002-12-15 fl Added RGBA support -# 2004-03-27 fl Fixed remaining int() problems for Python 1.5.2 -# 2004-07-19 fl Fixed gray/grey spelling issues -# 2009-03-05 fl Fixed rounding error in grayscale calculation -# -# Copyright (c) 2002-2004 by Secret Labs AB -# Copyright (c) 2002-2004 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import re -from functools import lru_cache - -from . import Image - - -@lru_cache -def getrgb(color: str) -> tuple[int, int, int] | tuple[int, int, int, int]: - """ - Convert a color string to an RGB or RGBA tuple. If the string cannot be - parsed, this function raises a :py:exc:`ValueError` exception. - - .. versionadded:: 1.1.4 - - :param color: A color string - :return: ``(red, green, blue[, alpha])`` - """ - if len(color) > 100: - msg = "color specifier is too long" - raise ValueError(msg) - color = color.lower() - - rgb = colormap.get(color, None) - if rgb: - if isinstance(rgb, tuple): - return rgb - rgb_tuple = getrgb(rgb) - assert len(rgb_tuple) == 3 - colormap[color] = rgb_tuple - return rgb_tuple - - # check for known string formats - if re.match("#[a-f0-9]{3}$", color): - return int(color[1] * 2, 16), int(color[2] * 2, 16), int(color[3] * 2, 16) - - if re.match("#[a-f0-9]{4}$", color): - return ( - int(color[1] * 2, 16), - int(color[2] * 2, 16), - int(color[3] * 2, 16), - int(color[4] * 2, 16), - ) - - if re.match("#[a-f0-9]{6}$", color): - return int(color[1:3], 16), int(color[3:5], 16), int(color[5:7], 16) - - if re.match("#[a-f0-9]{8}$", color): - return ( - int(color[1:3], 16), - int(color[3:5], 16), - int(color[5:7], 16), - int(color[7:9], 16), - ) - - m = re.match(r"rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$", color) - if m: - return int(m.group(1)), int(m.group(2)), int(m.group(3)) - - m = re.match(r"rgb\(\s*(\d+)%\s*,\s*(\d+)%\s*,\s*(\d+)%\s*\)$", color) - if m: - return ( - int((int(m.group(1)) * 255) / 100.0 + 0.5), - int((int(m.group(2)) * 255) / 100.0 + 0.5), - int((int(m.group(3)) * 255) / 100.0 + 0.5), - ) - - m = re.match( - r"hsl\(\s*(\d+\.?\d*)\s*,\s*(\d+\.?\d*)%\s*,\s*(\d+\.?\d*)%\s*\)$", color - ) - if m: - from colorsys import hls_to_rgb - - rgb_floats = hls_to_rgb( - float(m.group(1)) / 360.0, - float(m.group(3)) / 100.0, - float(m.group(2)) / 100.0, - ) - return ( - int(rgb_floats[0] * 255 + 0.5), - int(rgb_floats[1] * 255 + 0.5), - int(rgb_floats[2] * 255 + 0.5), - ) - - m = re.match( - r"hs[bv]\(\s*(\d+\.?\d*)\s*,\s*(\d+\.?\d*)%\s*,\s*(\d+\.?\d*)%\s*\)$", color - ) - if m: - from colorsys import hsv_to_rgb - - rgb_floats = hsv_to_rgb( - float(m.group(1)) / 360.0, - float(m.group(2)) / 100.0, - float(m.group(3)) / 100.0, - ) - return ( - int(rgb_floats[0] * 255 + 0.5), - int(rgb_floats[1] * 255 + 0.5), - int(rgb_floats[2] * 255 + 0.5), - ) - - m = re.match(r"rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$", color) - if m: - return int(m.group(1)), int(m.group(2)), int(m.group(3)), int(m.group(4)) - msg = f"unknown color specifier: {repr(color)}" - raise ValueError(msg) - - -@lru_cache -def getcolor(color: str, mode: str) -> int | tuple[int, ...]: - """ - Same as :py:func:`~PIL.ImageColor.getrgb` for most modes. However, if - ``mode`` is HSV, converts the RGB value to a HSV value, or if ``mode`` is - not color or a palette image, converts the RGB value to a grayscale value. - If the string cannot be parsed, this function raises a :py:exc:`ValueError` - exception. - - .. versionadded:: 1.1.4 - - :param color: A color string - :param mode: Convert result to this mode - :return: ``graylevel, (graylevel, alpha) or (red, green, blue[, alpha])`` - """ - # same as getrgb, but converts the result to the given mode - rgb, alpha = getrgb(color), 255 - if len(rgb) == 4: - alpha = rgb[3] - rgb = rgb[:3] - - if mode == "HSV": - from colorsys import rgb_to_hsv - - r, g, b = rgb - h, s, v = rgb_to_hsv(r / 255, g / 255, b / 255) - return int(h * 255), int(s * 255), int(v * 255) - elif Image.getmodebase(mode) == "L": - r, g, b = rgb - # ITU-R Recommendation 601-2 for nonlinear RGB - # scaled to 24 bits to match the convert's implementation. - graylevel = (r * 19595 + g * 38470 + b * 7471 + 0x8000) >> 16 - if mode[-1] == "A": - return graylevel, alpha - return graylevel - elif mode[-1] == "A": - return rgb + (alpha,) - return rgb - - -colormap: dict[str, str | tuple[int, int, int]] = { - # X11 colour table from https://drafts.csswg.org/css-color-4/, with - # gray/grey spelling issues fixed. This is a superset of HTML 4.0 - # colour names used in CSS 1. - "aliceblue": "#f0f8ff", - "antiquewhite": "#faebd7", - "aqua": "#00ffff", - "aquamarine": "#7fffd4", - "azure": "#f0ffff", - "beige": "#f5f5dc", - "bisque": "#ffe4c4", - "black": "#000000", - "blanchedalmond": "#ffebcd", - "blue": "#0000ff", - "blueviolet": "#8a2be2", - "brown": "#a52a2a", - "burlywood": "#deb887", - "cadetblue": "#5f9ea0", - "chartreuse": "#7fff00", - "chocolate": "#d2691e", - "coral": "#ff7f50", - "cornflowerblue": "#6495ed", - "cornsilk": "#fff8dc", - "crimson": "#dc143c", - "cyan": "#00ffff", - "darkblue": "#00008b", - "darkcyan": "#008b8b", - "darkgoldenrod": "#b8860b", - "darkgray": "#a9a9a9", - "darkgrey": "#a9a9a9", - "darkgreen": "#006400", - "darkkhaki": "#bdb76b", - "darkmagenta": "#8b008b", - "darkolivegreen": "#556b2f", - "darkorange": "#ff8c00", - "darkorchid": "#9932cc", - "darkred": "#8b0000", - "darksalmon": "#e9967a", - "darkseagreen": "#8fbc8f", - "darkslateblue": "#483d8b", - "darkslategray": "#2f4f4f", - "darkslategrey": "#2f4f4f", - "darkturquoise": "#00ced1", - "darkviolet": "#9400d3", - "deeppink": "#ff1493", - "deepskyblue": "#00bfff", - "dimgray": "#696969", - "dimgrey": "#696969", - "dodgerblue": "#1e90ff", - "firebrick": "#b22222", - "floralwhite": "#fffaf0", - "forestgreen": "#228b22", - "fuchsia": "#ff00ff", - "gainsboro": "#dcdcdc", - "ghostwhite": "#f8f8ff", - "gold": "#ffd700", - "goldenrod": "#daa520", - "gray": "#808080", - "grey": "#808080", - "green": "#008000", - "greenyellow": "#adff2f", - "honeydew": "#f0fff0", - "hotpink": "#ff69b4", - "indianred": "#cd5c5c", - "indigo": "#4b0082", - "ivory": "#fffff0", - "khaki": "#f0e68c", - "lavender": "#e6e6fa", - "lavenderblush": "#fff0f5", - "lawngreen": "#7cfc00", - "lemonchiffon": "#fffacd", - "lightblue": "#add8e6", - "lightcoral": "#f08080", - "lightcyan": "#e0ffff", - "lightgoldenrodyellow": "#fafad2", - "lightgreen": "#90ee90", - "lightgray": "#d3d3d3", - "lightgrey": "#d3d3d3", - "lightpink": "#ffb6c1", - "lightsalmon": "#ffa07a", - "lightseagreen": "#20b2aa", - "lightskyblue": "#87cefa", - "lightslategray": "#778899", - "lightslategrey": "#778899", - "lightsteelblue": "#b0c4de", - "lightyellow": "#ffffe0", - "lime": "#00ff00", - "limegreen": "#32cd32", - "linen": "#faf0e6", - "magenta": "#ff00ff", - "maroon": "#800000", - "mediumaquamarine": "#66cdaa", - "mediumblue": "#0000cd", - "mediumorchid": "#ba55d3", - "mediumpurple": "#9370db", - "mediumseagreen": "#3cb371", - "mediumslateblue": "#7b68ee", - "mediumspringgreen": "#00fa9a", - "mediumturquoise": "#48d1cc", - "mediumvioletred": "#c71585", - "midnightblue": "#191970", - "mintcream": "#f5fffa", - "mistyrose": "#ffe4e1", - "moccasin": "#ffe4b5", - "navajowhite": "#ffdead", - "navy": "#000080", - "oldlace": "#fdf5e6", - "olive": "#808000", - "olivedrab": "#6b8e23", - "orange": "#ffa500", - "orangered": "#ff4500", - "orchid": "#da70d6", - "palegoldenrod": "#eee8aa", - "palegreen": "#98fb98", - "paleturquoise": "#afeeee", - "palevioletred": "#db7093", - "papayawhip": "#ffefd5", - "peachpuff": "#ffdab9", - "peru": "#cd853f", - "pink": "#ffc0cb", - "plum": "#dda0dd", - "powderblue": "#b0e0e6", - "purple": "#800080", - "rebeccapurple": "#663399", - "red": "#ff0000", - "rosybrown": "#bc8f8f", - "royalblue": "#4169e1", - "saddlebrown": "#8b4513", - "salmon": "#fa8072", - "sandybrown": "#f4a460", - "seagreen": "#2e8b57", - "seashell": "#fff5ee", - "sienna": "#a0522d", - "silver": "#c0c0c0", - "skyblue": "#87ceeb", - "slateblue": "#6a5acd", - "slategray": "#708090", - "slategrey": "#708090", - "snow": "#fffafa", - "springgreen": "#00ff7f", - "steelblue": "#4682b4", - "tan": "#d2b48c", - "teal": "#008080", - "thistle": "#d8bfd8", - "tomato": "#ff6347", - "turquoise": "#40e0d0", - "violet": "#ee82ee", - "wheat": "#f5deb3", - "white": "#ffffff", - "whitesmoke": "#f5f5f5", - "yellow": "#ffff00", - "yellowgreen": "#9acd32", -} +# +# The Python Imaging Library +# $Id$ +# +# map CSS3-style colour description strings to RGB +# +# History: +# 2002-10-24 fl Added support for CSS-style color strings +# 2002-12-15 fl Added RGBA support +# 2004-03-27 fl Fixed remaining int() problems for Python 1.5.2 +# 2004-07-19 fl Fixed gray/grey spelling issues +# 2009-03-05 fl Fixed rounding error in grayscale calculation +# +# Copyright (c) 2002-2004 by Secret Labs AB +# Copyright (c) 2002-2004 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import re +from functools import lru_cache + +from . import Image + + +@lru_cache +def getrgb(color: str) -> tuple[int, int, int] | tuple[int, int, int, int]: + """ + Convert a color string to an RGB or RGBA tuple. If the string cannot be + parsed, this function raises a :py:exc:`ValueError` exception. + + .. versionadded:: 1.1.4 + + :param color: A color string + :return: ``(red, green, blue[, alpha])`` + """ + if len(color) > 100: + msg = "color specifier is too long" + raise ValueError(msg) + color = color.lower() + + rgb = colormap.get(color, None) + if rgb: + if isinstance(rgb, tuple): + return rgb + rgb_tuple = getrgb(rgb) + assert len(rgb_tuple) == 3 + colormap[color] = rgb_tuple + return rgb_tuple + + # check for known string formats + if re.match("#[a-f0-9]{3}$", color): + return int(color[1] * 2, 16), int(color[2] * 2, 16), int(color[3] * 2, 16) + + if re.match("#[a-f0-9]{4}$", color): + return ( + int(color[1] * 2, 16), + int(color[2] * 2, 16), + int(color[3] * 2, 16), + int(color[4] * 2, 16), + ) + + if re.match("#[a-f0-9]{6}$", color): + return int(color[1:3], 16), int(color[3:5], 16), int(color[5:7], 16) + + if re.match("#[a-f0-9]{8}$", color): + return ( + int(color[1:3], 16), + int(color[3:5], 16), + int(color[5:7], 16), + int(color[7:9], 16), + ) + + m = re.match(r"rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$", color) + if m: + return int(m.group(1)), int(m.group(2)), int(m.group(3)) + + m = re.match(r"rgb\(\s*(\d+)%\s*,\s*(\d+)%\s*,\s*(\d+)%\s*\)$", color) + if m: + return ( + int((int(m.group(1)) * 255) / 100.0 + 0.5), + int((int(m.group(2)) * 255) / 100.0 + 0.5), + int((int(m.group(3)) * 255) / 100.0 + 0.5), + ) + + m = re.match( + r"hsl\(\s*(\d+\.?\d*)\s*,\s*(\d+\.?\d*)%\s*,\s*(\d+\.?\d*)%\s*\)$", color + ) + if m: + from colorsys import hls_to_rgb + + rgb_floats = hls_to_rgb( + float(m.group(1)) / 360.0, + float(m.group(3)) / 100.0, + float(m.group(2)) / 100.0, + ) + return ( + int(rgb_floats[0] * 255 + 0.5), + int(rgb_floats[1] * 255 + 0.5), + int(rgb_floats[2] * 255 + 0.5), + ) + + m = re.match( + r"hs[bv]\(\s*(\d+\.?\d*)\s*,\s*(\d+\.?\d*)%\s*,\s*(\d+\.?\d*)%\s*\)$", color + ) + if m: + from colorsys import hsv_to_rgb + + rgb_floats = hsv_to_rgb( + float(m.group(1)) / 360.0, + float(m.group(2)) / 100.0, + float(m.group(3)) / 100.0, + ) + return ( + int(rgb_floats[0] * 255 + 0.5), + int(rgb_floats[1] * 255 + 0.5), + int(rgb_floats[2] * 255 + 0.5), + ) + + m = re.match(r"rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$", color) + if m: + return int(m.group(1)), int(m.group(2)), int(m.group(3)), int(m.group(4)) + msg = f"unknown color specifier: {repr(color)}" + raise ValueError(msg) + + +@lru_cache +def getcolor(color: str, mode: str) -> int | tuple[int, ...]: + """ + Same as :py:func:`~PIL.ImageColor.getrgb` for most modes. However, if + ``mode`` is HSV, converts the RGB value to a HSV value, or if ``mode`` is + not color or a palette image, converts the RGB value to a grayscale value. + If the string cannot be parsed, this function raises a :py:exc:`ValueError` + exception. + + .. versionadded:: 1.1.4 + + :param color: A color string + :param mode: Convert result to this mode + :return: ``graylevel, (graylevel, alpha) or (red, green, blue[, alpha])`` + """ + # same as getrgb, but converts the result to the given mode + rgb, alpha = getrgb(color), 255 + if len(rgb) == 4: + alpha = rgb[3] + rgb = rgb[:3] + + if mode == "HSV": + from colorsys import rgb_to_hsv + + r, g, b = rgb + h, s, v = rgb_to_hsv(r / 255, g / 255, b / 255) + return int(h * 255), int(s * 255), int(v * 255) + elif Image.getmodebase(mode) == "L": + r, g, b = rgb + # ITU-R Recommendation 601-2 for nonlinear RGB + # scaled to 24 bits to match the convert's implementation. + graylevel = (r * 19595 + g * 38470 + b * 7471 + 0x8000) >> 16 + if mode[-1] == "A": + return graylevel, alpha + return graylevel + elif mode[-1] == "A": + return rgb + (alpha,) + return rgb + + +colormap: dict[str, str | tuple[int, int, int]] = { + # X11 colour table from https://drafts.csswg.org/css-color-4/, with + # gray/grey spelling issues fixed. This is a superset of HTML 4.0 + # colour names used in CSS 1. + "aliceblue": "#f0f8ff", + "antiquewhite": "#faebd7", + "aqua": "#00ffff", + "aquamarine": "#7fffd4", + "azure": "#f0ffff", + "beige": "#f5f5dc", + "bisque": "#ffe4c4", + "black": "#000000", + "blanchedalmond": "#ffebcd", + "blue": "#0000ff", + "blueviolet": "#8a2be2", + "brown": "#a52a2a", + "burlywood": "#deb887", + "cadetblue": "#5f9ea0", + "chartreuse": "#7fff00", + "chocolate": "#d2691e", + "coral": "#ff7f50", + "cornflowerblue": "#6495ed", + "cornsilk": "#fff8dc", + "crimson": "#dc143c", + "cyan": "#00ffff", + "darkblue": "#00008b", + "darkcyan": "#008b8b", + "darkgoldenrod": "#b8860b", + "darkgray": "#a9a9a9", + "darkgrey": "#a9a9a9", + "darkgreen": "#006400", + "darkkhaki": "#bdb76b", + "darkmagenta": "#8b008b", + "darkolivegreen": "#556b2f", + "darkorange": "#ff8c00", + "darkorchid": "#9932cc", + "darkred": "#8b0000", + "darksalmon": "#e9967a", + "darkseagreen": "#8fbc8f", + "darkslateblue": "#483d8b", + "darkslategray": "#2f4f4f", + "darkslategrey": "#2f4f4f", + "darkturquoise": "#00ced1", + "darkviolet": "#9400d3", + "deeppink": "#ff1493", + "deepskyblue": "#00bfff", + "dimgray": "#696969", + "dimgrey": "#696969", + "dodgerblue": "#1e90ff", + "firebrick": "#b22222", + "floralwhite": "#fffaf0", + "forestgreen": "#228b22", + "fuchsia": "#ff00ff", + "gainsboro": "#dcdcdc", + "ghostwhite": "#f8f8ff", + "gold": "#ffd700", + "goldenrod": "#daa520", + "gray": "#808080", + "grey": "#808080", + "green": "#008000", + "greenyellow": "#adff2f", + "honeydew": "#f0fff0", + "hotpink": "#ff69b4", + "indianred": "#cd5c5c", + "indigo": "#4b0082", + "ivory": "#fffff0", + "khaki": "#f0e68c", + "lavender": "#e6e6fa", + "lavenderblush": "#fff0f5", + "lawngreen": "#7cfc00", + "lemonchiffon": "#fffacd", + "lightblue": "#add8e6", + "lightcoral": "#f08080", + "lightcyan": "#e0ffff", + "lightgoldenrodyellow": "#fafad2", + "lightgreen": "#90ee90", + "lightgray": "#d3d3d3", + "lightgrey": "#d3d3d3", + "lightpink": "#ffb6c1", + "lightsalmon": "#ffa07a", + "lightseagreen": "#20b2aa", + "lightskyblue": "#87cefa", + "lightslategray": "#778899", + "lightslategrey": "#778899", + "lightsteelblue": "#b0c4de", + "lightyellow": "#ffffe0", + "lime": "#00ff00", + "limegreen": "#32cd32", + "linen": "#faf0e6", + "magenta": "#ff00ff", + "maroon": "#800000", + "mediumaquamarine": "#66cdaa", + "mediumblue": "#0000cd", + "mediumorchid": "#ba55d3", + "mediumpurple": "#9370db", + "mediumseagreen": "#3cb371", + "mediumslateblue": "#7b68ee", + "mediumspringgreen": "#00fa9a", + "mediumturquoise": "#48d1cc", + "mediumvioletred": "#c71585", + "midnightblue": "#191970", + "mintcream": "#f5fffa", + "mistyrose": "#ffe4e1", + "moccasin": "#ffe4b5", + "navajowhite": "#ffdead", + "navy": "#000080", + "oldlace": "#fdf5e6", + "olive": "#808000", + "olivedrab": "#6b8e23", + "orange": "#ffa500", + "orangered": "#ff4500", + "orchid": "#da70d6", + "palegoldenrod": "#eee8aa", + "palegreen": "#98fb98", + "paleturquoise": "#afeeee", + "palevioletred": "#db7093", + "papayawhip": "#ffefd5", + "peachpuff": "#ffdab9", + "peru": "#cd853f", + "pink": "#ffc0cb", + "plum": "#dda0dd", + "powderblue": "#b0e0e6", + "purple": "#800080", + "rebeccapurple": "#663399", + "red": "#ff0000", + "rosybrown": "#bc8f8f", + "royalblue": "#4169e1", + "saddlebrown": "#8b4513", + "salmon": "#fa8072", + "sandybrown": "#f4a460", + "seagreen": "#2e8b57", + "seashell": "#fff5ee", + "sienna": "#a0522d", + "silver": "#c0c0c0", + "skyblue": "#87ceeb", + "slateblue": "#6a5acd", + "slategray": "#708090", + "slategrey": "#708090", + "snow": "#fffafa", + "springgreen": "#00ff7f", + "steelblue": "#4682b4", + "tan": "#d2b48c", + "teal": "#008080", + "thistle": "#d8bfd8", + "tomato": "#ff6347", + "turquoise": "#40e0d0", + "violet": "#ee82ee", + "wheat": "#f5deb3", + "white": "#ffffff", + "whitesmoke": "#f5f5f5", + "yellow": "#ffff00", + "yellowgreen": "#9acd32", +} diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index e74fab9fb..f368f993c 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -1,1115 +1,1115 @@ -# -# The Python Imaging Library -# $Id$ -# -# drawing interface operations -# -# History: -# 1996-04-13 fl Created (experimental) -# 1996-08-07 fl Filled polygons, ellipses. -# 1996-08-13 fl Added text support -# 1998-06-28 fl Handle I and F images -# 1998-12-29 fl Added arc; use arc primitive to draw ellipses -# 1999-01-10 fl Added shape stuff (experimental) -# 1999-02-06 fl Added bitmap support -# 1999-02-11 fl Changed all primitives to take options -# 1999-02-20 fl Fixed backwards compatibility -# 2000-10-12 fl Copy on write, when necessary -# 2001-02-18 fl Use default ink for bitmap/text also in fill mode -# 2002-10-24 fl Added support for CSS-style color strings -# 2002-12-10 fl Added experimental support for RGBA-on-RGB drawing -# 2002-12-11 fl Refactored low-level drawing API (work in progress) -# 2004-08-26 fl Made Draw() a factory function, added getdraw() support -# 2004-09-04 fl Added width support to line primitive -# 2004-09-10 fl Added font mode handling -# 2006-06-19 fl Added font bearing support (getmask2) -# -# Copyright (c) 1997-2006 by Secret Labs AB -# Copyright (c) 1996-2006 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import math -import numbers -import struct -from typing import TYPE_CHECKING, AnyStr, Sequence, cast - -from . import Image, ImageColor -from ._deprecate import deprecate -from ._typing import Coords - -""" -A simple 2D drawing interface for PIL images. -

-Application code should use the Draw factory, instead of -directly. -""" - - -class ImageDraw: - font = None - - def __init__(self, im: Image.Image, mode: str | None = None) -> None: - """ - Create a drawing instance. - - :param im: The image to draw in. - :param mode: Optional mode to use for color values. For RGB - images, this argument can be RGB or RGBA (to blend the - drawing into the image). For all other modes, this argument - must be the same as the image mode. If omitted, the mode - defaults to the mode of the image. - """ - im.load() - if im.readonly: - im._copy() # make it writeable - blend = 0 - if mode is None: - mode = im.mode - if mode != im.mode: - if mode == "RGBA" and im.mode == "RGB": - blend = 1 - else: - msg = "mode mismatch" - raise ValueError(msg) - if mode == "P": - self.palette = im.palette - else: - self.palette = None - self._image = im - self.im = im.im - self.draw = Image.core.draw(self.im, blend) - self.mode = mode - if mode in ("I", "F"): - self.ink = self.draw.draw_ink(1) - else: - self.ink = self.draw.draw_ink(-1) - if mode in ("1", "P", "I", "F"): - # FIXME: fix Fill2 to properly support matte for I+F images - self.fontmode = "1" - else: - self.fontmode = "L" # aliasing is okay for other modes - self.fill = False - - if TYPE_CHECKING: - from . import ImageFont - - def getfont( - self, - ) -> ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont: - """ - Get the current default font. - - To set the default font for this ImageDraw instance:: - - from PIL import ImageDraw, ImageFont - draw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") - - To set the default font for all future ImageDraw instances:: - - from PIL import ImageDraw, ImageFont - ImageDraw.ImageDraw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") - - If the current default font is ``None``, - it is initialized with ``ImageFont.load_default()``. - - :returns: An image font.""" - if not self.font: - # FIXME: should add a font repository - from . import ImageFont - - self.font = ImageFont.load_default() - return self.font - - def _getfont( - self, font_size: float | None - ) -> ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont: - if font_size is not None: - from . import ImageFont - - return ImageFont.load_default(font_size) - else: - return self.getfont() - - def _getink(self, ink, fill=None) -> tuple[int | None, int | None]: - if ink is None and fill is None: - if self.fill: - fill = self.ink - else: - ink = self.ink - else: - if ink is not None: - if isinstance(ink, str): - ink = ImageColor.getcolor(ink, self.mode) - if self.palette and not isinstance(ink, numbers.Number): - ink = self.palette.getcolor(ink, self._image) - ink = self.draw.draw_ink(ink) - if fill is not None: - if isinstance(fill, str): - fill = ImageColor.getcolor(fill, self.mode) - if self.palette and not isinstance(fill, numbers.Number): - fill = self.palette.getcolor(fill, self._image) - fill = self.draw.draw_ink(fill) - return ink, fill - - def arc(self, xy: Coords, start, end, fill=None, width=1) -> None: - """Draw an arc.""" - ink, fill = self._getink(fill) - if ink is not None: - self.draw.draw_arc(xy, start, end, ink, width) - - def bitmap(self, xy: Sequence[int], bitmap, fill=None) -> None: - """Draw a bitmap.""" - bitmap.load() - ink, fill = self._getink(fill) - if ink is None: - ink = fill - if ink is not None: - self.draw.draw_bitmap(xy, bitmap.im, ink) - - def chord(self, xy: Coords, start, end, fill=None, outline=None, width=1) -> None: - """Draw a chord.""" - ink, fill = self._getink(outline, fill) - if fill is not None: - self.draw.draw_chord(xy, start, end, fill, 1) - if ink is not None and ink != fill and width != 0: - self.draw.draw_chord(xy, start, end, ink, 0, width) - - def ellipse(self, xy: Coords, fill=None, outline=None, width=1) -> None: - """Draw an ellipse.""" - ink, fill = self._getink(outline, fill) - if fill is not None: - self.draw.draw_ellipse(xy, fill, 1) - if ink is not None and ink != fill and width != 0: - self.draw.draw_ellipse(xy, ink, 0, width) - - def circle( - self, xy: Sequence[float], radius: float, fill=None, outline=None, width=1 - ) -> None: - """Draw a circle given center coordinates and a radius.""" - ellipse_xy = (xy[0] - radius, xy[1] - radius, xy[0] + radius, xy[1] + radius) - self.ellipse(ellipse_xy, fill, outline, width) - - def line(self, xy: Coords, fill=None, width=0, joint=None) -> None: - """Draw a line, or a connected sequence of line segments.""" - ink = self._getink(fill)[0] - if ink is not None: - self.draw.draw_lines(xy, ink, width) - if joint == "curve" and width > 4: - points: Sequence[Sequence[float]] - if isinstance(xy[0], (list, tuple)): - points = cast(Sequence[Sequence[float]], xy) - else: - points = [ - cast(Sequence[float], tuple(xy[i : i + 2])) - for i in range(0, len(xy), 2) - ] - for i in range(1, len(points) - 1): - point = points[i] - angles = [ - math.degrees(math.atan2(end[0] - start[0], start[1] - end[1])) - % 360 - for start, end in ( - (points[i - 1], point), - (point, points[i + 1]), - ) - ] - if angles[0] == angles[1]: - # This is a straight line, so no joint is required - continue - - def coord_at_angle( - coord: Sequence[float], angle: float - ) -> tuple[float, float]: - x, y = coord - angle -= 90 - distance = width / 2 - 1 - return tuple( - p + (math.floor(p_d) if p_d > 0 else math.ceil(p_d)) - for p, p_d in ( - (x, distance * math.cos(math.radians(angle))), - (y, distance * math.sin(math.radians(angle))), - ) - ) - - flipped = ( - angles[1] > angles[0] and angles[1] - 180 > angles[0] - ) or (angles[1] < angles[0] and angles[1] + 180 > angles[0]) - coords = [ - (point[0] - width / 2 + 1, point[1] - width / 2 + 1), - (point[0] + width / 2 - 1, point[1] + width / 2 - 1), - ] - if flipped: - start, end = (angles[1] + 90, angles[0] + 90) - else: - start, end = (angles[0] - 90, angles[1] - 90) - self.pieslice(coords, start - 90, end - 90, fill) - - if width > 8: - # Cover potential gaps between the line and the joint - if flipped: - gap_coords = [ - coord_at_angle(point, angles[0] + 90), - point, - coord_at_angle(point, angles[1] + 90), - ] - else: - gap_coords = [ - coord_at_angle(point, angles[0] - 90), - point, - coord_at_angle(point, angles[1] - 90), - ] - self.line(gap_coords, fill, width=3) - - def shape(self, shape, fill=None, outline=None) -> None: - """(Experimental) Draw a shape.""" - shape.close() - ink, fill = self._getink(outline, fill) - if fill is not None: - self.draw.draw_outline(shape, fill, 1) - if ink is not None and ink != fill: - self.draw.draw_outline(shape, ink, 0) - - def pieslice( - self, xy: Coords, start, end, fill=None, outline=None, width=1 - ) -> None: - """Draw a pieslice.""" - ink, fill = self._getink(outline, fill) - if fill is not None: - self.draw.draw_pieslice(xy, start, end, fill, 1) - if ink is not None and ink != fill and width != 0: - self.draw.draw_pieslice(xy, start, end, ink, 0, width) - - def point(self, xy: Coords, fill=None) -> None: - """Draw one or more individual pixels.""" - ink, fill = self._getink(fill) - if ink is not None: - self.draw.draw_points(xy, ink) - - def polygon(self, xy: Coords, fill=None, outline=None, width=1) -> None: - """Draw a polygon.""" - ink, fill = self._getink(outline, fill) - if fill is not None: - self.draw.draw_polygon(xy, fill, 1) - if ink is not None and ink != fill and width != 0: - if width == 1: - self.draw.draw_polygon(xy, ink, 0, width) - elif self.im is not None: - # To avoid expanding the polygon outwards, - # use the fill as a mask - mask = Image.new("1", self.im.size) - mask_ink = self._getink(1)[0] - - fill_im = mask.copy() - draw = Draw(fill_im) - draw.draw.draw_polygon(xy, mask_ink, 1) - - ink_im = mask.copy() - draw = Draw(ink_im) - width = width * 2 - 1 - draw.draw.draw_polygon(xy, mask_ink, 0, width) - - mask.paste(ink_im, mask=fill_im) - - im = Image.new(self.mode, self.im.size) - draw = Draw(im) - draw.draw.draw_polygon(xy, ink, 0, width) - self.im.paste(im.im, (0, 0) + im.size, mask.im) - - def regular_polygon( - self, bounding_circle, n_sides, rotation=0, fill=None, outline=None, width=1 - ) -> None: - """Draw a regular polygon.""" - xy = _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) - self.polygon(xy, fill, outline, width) - - def rectangle(self, xy: Coords, fill=None, outline=None, width=1) -> None: - """Draw a rectangle.""" - ink, fill = self._getink(outline, fill) - if fill is not None: - self.draw.draw_rectangle(xy, fill, 1) - if ink is not None and ink != fill and width != 0: - self.draw.draw_rectangle(xy, ink, 0, width) - - def rounded_rectangle( - self, xy: Coords, radius=0, fill=None, outline=None, width=1, *, corners=None - ) -> None: - """Draw a rounded rectangle.""" - if isinstance(xy[0], (list, tuple)): - (x0, y0), (x1, y1) = cast(Sequence[Sequence[float]], xy) - else: - x0, y0, x1, y1 = cast(Sequence[float], xy) - if x1 < x0: - msg = "x1 must be greater than or equal to x0" - raise ValueError(msg) - if y1 < y0: - msg = "y1 must be greater than or equal to y0" - raise ValueError(msg) - if corners is None: - corners = (True, True, True, True) - - d = radius * 2 - - x0 = round(x0) - y0 = round(y0) - x1 = round(x1) - y1 = round(y1) - full_x, full_y = False, False - if all(corners): - full_x = d >= x1 - x0 - 1 - if full_x: - # The two left and two right corners are joined - d = x1 - x0 - full_y = d >= y1 - y0 - 1 - if full_y: - # The two top and two bottom corners are joined - d = y1 - y0 - if full_x and full_y: - # If all corners are joined, that is a circle - return self.ellipse(xy, fill, outline, width) - - if d == 0 or not any(corners): - # If the corners have no curve, - # or there are no corners, - # that is a rectangle - return self.rectangle(xy, fill, outline, width) - - r = d // 2 - ink, fill = self._getink(outline, fill) - - def draw_corners(pieslice) -> None: - parts: tuple[tuple[tuple[float, float, float, float], int, int], ...] - if full_x: - # Draw top and bottom halves - parts = ( - ((x0, y0, x0 + d, y0 + d), 180, 360), - ((x0, y1 - d, x0 + d, y1), 0, 180), - ) - elif full_y: - # Draw left and right halves - parts = ( - ((x0, y0, x0 + d, y0 + d), 90, 270), - ((x1 - d, y0, x1, y0 + d), 270, 90), - ) - else: - # Draw four separate corners - parts = tuple( - part - for i, part in enumerate( - ( - ((x0, y0, x0 + d, y0 + d), 180, 270), - ((x1 - d, y0, x1, y0 + d), 270, 360), - ((x1 - d, y1 - d, x1, y1), 0, 90), - ((x0, y1 - d, x0 + d, y1), 90, 180), - ) - ) - if corners[i] - ) - for part in parts: - if pieslice: - self.draw.draw_pieslice(*(part + (fill, 1))) - else: - self.draw.draw_arc(*(part + (ink, width))) - - if fill is not None: - draw_corners(True) - - if full_x: - self.draw.draw_rectangle((x0, y0 + r + 1, x1, y1 - r - 1), fill, 1) - else: - self.draw.draw_rectangle((x0 + r + 1, y0, x1 - r - 1, y1), fill, 1) - if not full_x and not full_y: - left = [x0, y0, x0 + r, y1] - if corners[0]: - left[1] += r + 1 - if corners[3]: - left[3] -= r + 1 - self.draw.draw_rectangle(left, fill, 1) - - right = [x1 - r, y0, x1, y1] - if corners[1]: - right[1] += r + 1 - if corners[2]: - right[3] -= r + 1 - self.draw.draw_rectangle(right, fill, 1) - if ink is not None and ink != fill and width != 0: - draw_corners(False) - - if not full_x: - top = [x0, y0, x1, y0 + width - 1] - if corners[0]: - top[0] += r + 1 - if corners[1]: - top[2] -= r + 1 - self.draw.draw_rectangle(top, ink, 1) - - bottom = [x0, y1 - width + 1, x1, y1] - if corners[3]: - bottom[0] += r + 1 - if corners[2]: - bottom[2] -= r + 1 - self.draw.draw_rectangle(bottom, ink, 1) - if not full_y: - left = [x0, y0, x0 + width - 1, y1] - if corners[0]: - left[1] += r + 1 - if corners[3]: - left[3] -= r + 1 - self.draw.draw_rectangle(left, ink, 1) - - right = [x1 - width + 1, y0, x1, y1] - if corners[1]: - right[1] += r + 1 - if corners[2]: - right[3] -= r + 1 - self.draw.draw_rectangle(right, ink, 1) - - def _multiline_check(self, text: AnyStr) -> bool: - split_character = "\n" if isinstance(text, str) else b"\n" - - return split_character in text - - def _multiline_split(self, text: AnyStr) -> list[AnyStr]: - return text.split("\n" if isinstance(text, str) else b"\n") - - def _multiline_spacing(self, font, spacing, stroke_width): - return ( - self.textbbox((0, 0), "A", font, stroke_width=stroke_width)[3] - + stroke_width - + spacing - ) - - def text( - self, - xy: tuple[float, float], - text: str, - fill=None, - font: ( - ImageFont.ImageFont - | ImageFont.FreeTypeFont - | ImageFont.TransposedFont - | None - ) = None, - anchor=None, - spacing=4, - align="left", - direction=None, - features=None, - language=None, - stroke_width=0, - stroke_fill=None, - embedded_color=False, - *args, - **kwargs, - ) -> None: - """Draw text.""" - if embedded_color and self.mode not in ("RGB", "RGBA"): - msg = "Embedded color supported only in RGB and RGBA modes" - raise ValueError(msg) - - if font is None: - font = self._getfont(kwargs.get("font_size")) - - if self._multiline_check(text): - return self.multiline_text( - xy, - text, - fill, - font, - anchor, - spacing, - align, - direction, - features, - language, - stroke_width, - stroke_fill, - embedded_color, - ) - - def getink(fill): - ink, fill = self._getink(fill) - if ink is None: - return fill - return ink - - def draw_text(ink, stroke_width=0, stroke_offset=None) -> None: - mode = self.fontmode - if stroke_width == 0 and embedded_color: - mode = "RGBA" - coord = [] - start = [] - for i in range(2): - coord.append(int(xy[i])) - start.append(math.modf(xy[i])[0]) - try: - mask, offset = font.getmask2( # type: ignore[union-attr,misc] - text, - mode, - direction=direction, - features=features, - language=language, - stroke_width=stroke_width, - anchor=anchor, - ink=ink, - start=start, - *args, - **kwargs, - ) - coord = [coord[0] + offset[0], coord[1] + offset[1]] - except AttributeError: - try: - mask = font.getmask( # type: ignore[misc] - text, - mode, - direction, - features, - language, - stroke_width, - anchor, - ink, - start=start, - *args, - **kwargs, - ) - except TypeError: - mask = font.getmask(text) - if stroke_offset: - coord = [coord[0] + stroke_offset[0], coord[1] + stroke_offset[1]] - if mode == "RGBA": - # font.getmask2(mode="RGBA") returns color in RGB bands and mask in A - # extract mask and set text alpha - color, mask = mask, mask.getband(3) - ink_alpha = struct.pack("i", ink)[3] - color.fillband(3, ink_alpha) - x, y = coord - if self.im is not None: - self.im.paste( - color, (x, y, x + mask.size[0], y + mask.size[1]), mask - ) - else: - self.draw.draw_bitmap(coord, mask, ink) - - ink = getink(fill) - if ink is not None: - stroke_ink = None - if stroke_width: - stroke_ink = getink(stroke_fill) if stroke_fill is not None else ink - - if stroke_ink is not None: - # Draw stroked text - draw_text(stroke_ink, stroke_width) - - # Draw normal text - draw_text(ink, 0) - else: - # Only draw normal text - draw_text(ink) - - def multiline_text( - self, - xy: tuple[float, float], - text: str, - fill=None, - font: ( - ImageFont.ImageFont - | ImageFont.FreeTypeFont - | ImageFont.TransposedFont - | None - ) = None, - anchor=None, - spacing=4, - align="left", - direction=None, - features=None, - language=None, - stroke_width=0, - stroke_fill=None, - embedded_color=False, - *, - font_size=None, - ) -> None: - if direction == "ttb": - msg = "ttb direction is unsupported for multiline text" - raise ValueError(msg) - - if anchor is None: - anchor = "la" - elif len(anchor) != 2: - msg = "anchor must be a 2 character string" - raise ValueError(msg) - elif anchor[1] in "tb": - msg = "anchor not supported for multiline text" - raise ValueError(msg) - - if font is None: - font = self._getfont(font_size) - - widths = [] - max_width: float = 0 - lines = self._multiline_split(text) - line_spacing = self._multiline_spacing(font, spacing, stroke_width) - for line in lines: - line_width = self.textlength( - line, font, direction=direction, features=features, language=language - ) - widths.append(line_width) - max_width = max(max_width, line_width) - - top = xy[1] - if anchor[1] == "m": - top -= (len(lines) - 1) * line_spacing / 2.0 - elif anchor[1] == "d": - top -= (len(lines) - 1) * line_spacing - - for idx, line in enumerate(lines): - left = xy[0] - width_difference = max_width - widths[idx] - - # first align left by anchor - if anchor[0] == "m": - left -= width_difference / 2.0 - elif anchor[0] == "r": - left -= width_difference - - # then align by align parameter - if align == "left": - pass - elif align == "center": - left += width_difference / 2.0 - elif align == "right": - left += width_difference - else: - msg = 'align must be "left", "center" or "right"' - raise ValueError(msg) - - self.text( - (left, top), - line, - fill, - font, - anchor, - direction=direction, - features=features, - language=language, - stroke_width=stroke_width, - stroke_fill=stroke_fill, - embedded_color=embedded_color, - ) - top += line_spacing - - def textlength( - self, - text: str, - font: ( - ImageFont.ImageFont - | ImageFont.FreeTypeFont - | ImageFont.TransposedFont - | None - ) = None, - direction=None, - features=None, - language=None, - embedded_color=False, - *, - font_size=None, - ) -> float: - """Get the length of a given string, in pixels with 1/64 precision.""" - if self._multiline_check(text): - msg = "can't measure length of multiline text" - raise ValueError(msg) - if embedded_color and self.mode not in ("RGB", "RGBA"): - msg = "Embedded color supported only in RGB and RGBA modes" - raise ValueError(msg) - - if font is None: - font = self._getfont(font_size) - mode = "RGBA" if embedded_color else self.fontmode - return font.getlength(text, mode, direction, features, language) - - def textbbox( - self, - xy, - text, - font=None, - anchor=None, - spacing=4, - align="left", - direction=None, - features=None, - language=None, - stroke_width=0, - embedded_color=False, - *, - font_size=None, - ) -> tuple[int, int, int, int]: - """Get the bounding box of a given string, in pixels.""" - if embedded_color and self.mode not in ("RGB", "RGBA"): - msg = "Embedded color supported only in RGB and RGBA modes" - raise ValueError(msg) - - if font is None: - font = self._getfont(font_size) - - if self._multiline_check(text): - return self.multiline_textbbox( - xy, - text, - font, - anchor, - spacing, - align, - direction, - features, - language, - stroke_width, - embedded_color, - ) - - mode = "RGBA" if embedded_color else self.fontmode - bbox = font.getbbox( - text, mode, direction, features, language, stroke_width, anchor - ) - return bbox[0] + xy[0], bbox[1] + xy[1], bbox[2] + xy[0], bbox[3] + xy[1] - - def multiline_textbbox( - self, - xy, - text, - font=None, - anchor=None, - spacing=4, - align="left", - direction=None, - features=None, - language=None, - stroke_width=0, - embedded_color=False, - *, - font_size=None, - ) -> tuple[int, int, int, int]: - if direction == "ttb": - msg = "ttb direction is unsupported for multiline text" - raise ValueError(msg) - - if anchor is None: - anchor = "la" - elif len(anchor) != 2: - msg = "anchor must be a 2 character string" - raise ValueError(msg) - elif anchor[1] in "tb": - msg = "anchor not supported for multiline text" - raise ValueError(msg) - - if font is None: - font = self._getfont(font_size) - - widths = [] - max_width: float = 0 - lines = self._multiline_split(text) - line_spacing = self._multiline_spacing(font, spacing, stroke_width) - for line in lines: - line_width = self.textlength( - line, - font, - direction=direction, - features=features, - language=language, - embedded_color=embedded_color, - ) - widths.append(line_width) - max_width = max(max_width, line_width) - - top = xy[1] - if anchor[1] == "m": - top -= (len(lines) - 1) * line_spacing / 2.0 - elif anchor[1] == "d": - top -= (len(lines) - 1) * line_spacing - - bbox: tuple[int, int, int, int] | None = None - - for idx, line in enumerate(lines): - left = xy[0] - width_difference = max_width - widths[idx] - - # first align left by anchor - if anchor[0] == "m": - left -= width_difference / 2.0 - elif anchor[0] == "r": - left -= width_difference - - # then align by align parameter - if align == "left": - pass - elif align == "center": - left += width_difference / 2.0 - elif align == "right": - left += width_difference - else: - msg = 'align must be "left", "center" or "right"' - raise ValueError(msg) - - bbox_line = self.textbbox( - (left, top), - line, - font, - anchor, - direction=direction, - features=features, - language=language, - stroke_width=stroke_width, - embedded_color=embedded_color, - ) - if bbox is None: - bbox = bbox_line - else: - bbox = ( - min(bbox[0], bbox_line[0]), - min(bbox[1], bbox_line[1]), - max(bbox[2], bbox_line[2]), - max(bbox[3], bbox_line[3]), - ) - - top += line_spacing - - if bbox is None: - return xy[0], xy[1], xy[0], xy[1] - return bbox - - -def Draw(im, mode: str | None = None) -> ImageDraw: - """ - A simple 2D drawing interface for PIL images. - - :param im: The image to draw in. - :param mode: Optional mode to use for color values. For RGB - images, this argument can be RGB or RGBA (to blend the - drawing into the image). For all other modes, this argument - must be the same as the image mode. If omitted, the mode - defaults to the mode of the image. - """ - try: - return im.getdraw(mode) - except AttributeError: - return ImageDraw(im, mode) - - -# experimental access to the outline API -try: - Outline = Image.core.outline -except AttributeError: - Outline = None - - -def getdraw(im=None, hints=None): - """ - :param im: The image to draw in. - :param hints: An optional list of hints. Deprecated. - :returns: A (drawing context, drawing resource factory) tuple. - """ - if hints is not None: - deprecate("'hints' parameter", 12) - from . import ImageDraw2 - - if im: - im = ImageDraw2.Draw(im) - return im, ImageDraw2 - - -def floodfill( - image: Image.Image, - xy: tuple[int, int], - value: float | tuple[int, ...], - border: float | tuple[int, ...] | None = None, - thresh: float = 0, -) -> None: - """ - (experimental) Fills a bounded region with a given color. - - :param image: Target image. - :param xy: Seed position (a 2-item coordinate tuple). See - :ref:`coordinate-system`. - :param value: Fill color. - :param border: Optional border value. If given, the region consists of - pixels with a color different from the border color. If not given, - the region consists of pixels having the same color as the seed - pixel. - :param thresh: Optional threshold value which specifies a maximum - tolerable difference of a pixel value from the 'background' in - order for it to be replaced. Useful for filling regions of - non-homogeneous, but similar, colors. - """ - # based on an implementation by Eric S. Raymond - # amended by yo1995 @20180806 - pixel = image.load() - x, y = xy - try: - background = pixel[x, y] - if _color_diff(value, background) <= thresh: - return # seed point already has fill color - pixel[x, y] = value - except (ValueError, IndexError): - return # seed point outside image - edge = {(x, y)} - # use a set to keep record of current and previous edge pixels - # to reduce memory consumption - full_edge = set() - while edge: - new_edge = set() - for x, y in edge: # 4 adjacent method - for s, t in ((x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)): - # If already processed, or if a coordinate is negative, skip - if (s, t) in full_edge or s < 0 or t < 0: - continue - try: - p = pixel[s, t] - except (ValueError, IndexError): - pass - else: - full_edge.add((s, t)) - if border is None: - fill = _color_diff(p, background) <= thresh - else: - fill = p not in (value, border) - if fill: - pixel[s, t] = value - new_edge.add((s, t)) - full_edge = edge # discard pixels processed - edge = new_edge - - -def _compute_regular_polygon_vertices( - bounding_circle, n_sides, rotation -) -> list[tuple[float, float]]: - """ - Generate a list of vertices for a 2D regular polygon. - - :param bounding_circle: The bounding circle is a tuple defined - by a point and radius. The polygon is inscribed in this circle. - (e.g. ``bounding_circle=(x, y, r)`` or ``((x, y), r)``) - :param n_sides: Number of sides - (e.g. ``n_sides=3`` for a triangle, ``6`` for a hexagon) - :param rotation: Apply an arbitrary rotation to the polygon - (e.g. ``rotation=90``, applies a 90 degree rotation) - :return: List of regular polygon vertices - (e.g. ``[(25, 50), (50, 50), (50, 25), (25, 25)]``) - - How are the vertices computed? - 1. Compute the following variables - - theta: Angle between the apothem & the nearest polygon vertex - - side_length: Length of each polygon edge - - centroid: Center of bounding circle (1st, 2nd elements of bounding_circle) - - polygon_radius: Polygon radius (last element of bounding_circle) - - angles: Location of each polygon vertex in polar grid - (e.g. A square with 0 degree rotation => [225.0, 315.0, 45.0, 135.0]) - - 2. For each angle in angles, get the polygon vertex at that angle - The vertex is computed using the equation below. - X= xcos(φ) + ysin(φ) - Y= −xsin(φ) + ycos(φ) - - Note: - φ = angle in degrees - x = 0 - y = polygon_radius - - The formula above assumes rotation around the origin. - In our case, we are rotating around the centroid. - To account for this, we use the formula below - X = xcos(φ) + ysin(φ) + centroid_x - Y = −xsin(φ) + ycos(φ) + centroid_y - """ - # 1. Error Handling - # 1.1 Check `n_sides` has an appropriate value - if not isinstance(n_sides, int): - msg = "n_sides should be an int" - raise TypeError(msg) - if n_sides < 3: - msg = "n_sides should be an int > 2" - raise ValueError(msg) - - # 1.2 Check `bounding_circle` has an appropriate value - if not isinstance(bounding_circle, (list, tuple)): - msg = "bounding_circle should be a sequence" - raise TypeError(msg) - - if len(bounding_circle) == 3: - *centroid, polygon_radius = bounding_circle - elif len(bounding_circle) == 2: - centroid, polygon_radius = bounding_circle - else: - msg = ( - "bounding_circle should contain 2D coordinates " - "and a radius (e.g. (x, y, r) or ((x, y), r) )" - ) - raise ValueError(msg) - - if not all(isinstance(i, (int, float)) for i in (*centroid, polygon_radius)): - msg = "bounding_circle should only contain numeric data" - raise ValueError(msg) - - if not len(centroid) == 2: - msg = "bounding_circle centre should contain 2D coordinates (e.g. (x, y))" - raise ValueError(msg) - - if polygon_radius <= 0: - msg = "bounding_circle radius should be > 0" - raise ValueError(msg) - - # 1.3 Check `rotation` has an appropriate value - if not isinstance(rotation, (int, float)): - msg = "rotation should be an int or float" - raise ValueError(msg) - - # 2. Define Helper Functions - def _apply_rotation(point: list[float], degrees: float) -> tuple[int, int]: - return ( - round( - point[0] * math.cos(math.radians(360 - degrees)) - - point[1] * math.sin(math.radians(360 - degrees)) - + centroid[0], - 2, - ), - round( - point[1] * math.cos(math.radians(360 - degrees)) - + point[0] * math.sin(math.radians(360 - degrees)) - + centroid[1], - 2, - ), - ) - - def _compute_polygon_vertex(angle: float) -> tuple[int, int]: - start_point = [polygon_radius, 0] - return _apply_rotation(start_point, angle) - - def _get_angles(n_sides: int, rotation: float) -> list[float]: - angles = [] - degrees = 360 / n_sides - # Start with the bottom left polygon vertex - current_angle = (270 - 0.5 * degrees) + rotation - for _ in range(0, n_sides): - angles.append(current_angle) - current_angle += degrees - if current_angle > 360: - current_angle -= 360 - return angles - - # 3. Variable Declarations - angles = _get_angles(n_sides, rotation) - - # 4. Compute Vertices - return [_compute_polygon_vertex(angle) for angle in angles] - - -def _color_diff( - color1: float | tuple[int, ...], color2: float | tuple[int, ...] -) -> float: - """ - Uses 1-norm distance to calculate difference between two values. - """ - 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))) +# +# The Python Imaging Library +# $Id$ +# +# drawing interface operations +# +# History: +# 1996-04-13 fl Created (experimental) +# 1996-08-07 fl Filled polygons, ellipses. +# 1996-08-13 fl Added text support +# 1998-06-28 fl Handle I and F images +# 1998-12-29 fl Added arc; use arc primitive to draw ellipses +# 1999-01-10 fl Added shape stuff (experimental) +# 1999-02-06 fl Added bitmap support +# 1999-02-11 fl Changed all primitives to take options +# 1999-02-20 fl Fixed backwards compatibility +# 2000-10-12 fl Copy on write, when necessary +# 2001-02-18 fl Use default ink for bitmap/text also in fill mode +# 2002-10-24 fl Added support for CSS-style color strings +# 2002-12-10 fl Added experimental support for RGBA-on-RGB drawing +# 2002-12-11 fl Refactored low-level drawing API (work in progress) +# 2004-08-26 fl Made Draw() a factory function, added getdraw() support +# 2004-09-04 fl Added width support to line primitive +# 2004-09-10 fl Added font mode handling +# 2006-06-19 fl Added font bearing support (getmask2) +# +# Copyright (c) 1997-2006 by Secret Labs AB +# Copyright (c) 1996-2006 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import math +import numbers +import struct +from typing import TYPE_CHECKING, AnyStr, Sequence, cast + +from . import Image, ImageColor +from ._deprecate import deprecate +from ._typing import Coords + +""" +A simple 2D drawing interface for PIL images. +

+Application code should use the Draw factory, instead of +directly. +""" + + +class ImageDraw: + font = None + + def __init__(self, im: Image.Image, mode: str | None = None) -> None: + """ + Create a drawing instance. + + :param im: The image to draw in. + :param mode: Optional mode to use for color values. For RGB + images, this argument can be RGB or RGBA (to blend the + drawing into the image). For all other modes, this argument + must be the same as the image mode. If omitted, the mode + defaults to the mode of the image. + """ + im.load() + if im.readonly: + im._copy() # make it writeable + blend = 0 + if mode is None: + mode = im.mode + if mode != im.mode: + if mode == "RGBA" and im.mode == "RGB": + blend = 1 + else: + msg = "mode mismatch" + raise ValueError(msg) + if mode == "P": + self.palette = im.palette + else: + self.palette = None + self._image = im + self.im = im.im + self.draw = Image.core.draw(self.im, blend) + self.mode = mode + if mode in ("I", "F"): + self.ink = self.draw.draw_ink(1) + else: + self.ink = self.draw.draw_ink(-1) + if mode in ("1", "P", "I", "F"): + # FIXME: fix Fill2 to properly support matte for I+F images + self.fontmode = "1" + else: + self.fontmode = "L" # aliasing is okay for other modes + self.fill = False + + if TYPE_CHECKING: + from . import ImageFont + + def getfont( + self, + ) -> ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont: + """ + Get the current default font. + + To set the default font for this ImageDraw instance:: + + from PIL import ImageDraw, ImageFont + draw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + + To set the default font for all future ImageDraw instances:: + + from PIL import ImageDraw, ImageFont + ImageDraw.ImageDraw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + + If the current default font is ``None``, + it is initialized with ``ImageFont.load_default()``. + + :returns: An image font.""" + if not self.font: + # FIXME: should add a font repository + from . import ImageFont + + self.font = ImageFont.load_default() + return self.font + + def _getfont( + self, font_size: float | None + ) -> ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont: + if font_size is not None: + from . import ImageFont + + return ImageFont.load_default(font_size) + else: + return self.getfont() + + def _getink(self, ink, fill=None) -> tuple[int | None, int | None]: + if ink is None and fill is None: + if self.fill: + fill = self.ink + else: + ink = self.ink + else: + if ink is not None: + if isinstance(ink, str): + ink = ImageColor.getcolor(ink, self.mode) + if self.palette and not isinstance(ink, numbers.Number): + ink = self.palette.getcolor(ink, self._image) + ink = self.draw.draw_ink(ink) + if fill is not None: + if isinstance(fill, str): + fill = ImageColor.getcolor(fill, self.mode) + if self.palette and not isinstance(fill, numbers.Number): + fill = self.palette.getcolor(fill, self._image) + fill = self.draw.draw_ink(fill) + return ink, fill + + def arc(self, xy: Coords, start, end, fill=None, width=1) -> None: + """Draw an arc.""" + ink, fill = self._getink(fill) + if ink is not None: + self.draw.draw_arc(xy, start, end, ink, width) + + def bitmap(self, xy: Sequence[int], bitmap, fill=None) -> None: + """Draw a bitmap.""" + bitmap.load() + ink, fill = self._getink(fill) + if ink is None: + ink = fill + if ink is not None: + self.draw.draw_bitmap(xy, bitmap.im, ink) + + def chord(self, xy: Coords, start, end, fill=None, outline=None, width=1) -> None: + """Draw a chord.""" + ink, fill = self._getink(outline, fill) + if fill is not None: + self.draw.draw_chord(xy, start, end, fill, 1) + if ink is not None and ink != fill and width != 0: + self.draw.draw_chord(xy, start, end, ink, 0, width) + + def ellipse(self, xy: Coords, fill=None, outline=None, width=1) -> None: + """Draw an ellipse.""" + ink, fill = self._getink(outline, fill) + if fill is not None: + self.draw.draw_ellipse(xy, fill, 1) + if ink is not None and ink != fill and width != 0: + self.draw.draw_ellipse(xy, ink, 0, width) + + def circle( + self, xy: Sequence[float], radius: float, fill=None, outline=None, width=1 + ) -> None: + """Draw a circle given center coordinates and a radius.""" + ellipse_xy = (xy[0] - radius, xy[1] - radius, xy[0] + radius, xy[1] + radius) + self.ellipse(ellipse_xy, fill, outline, width) + + def line(self, xy: Coords, fill=None, width=0, joint=None) -> None: + """Draw a line, or a connected sequence of line segments.""" + ink = self._getink(fill)[0] + if ink is not None: + self.draw.draw_lines(xy, ink, width) + if joint == "curve" and width > 4: + points: Sequence[Sequence[float]] + if isinstance(xy[0], (list, tuple)): + points = cast(Sequence[Sequence[float]], xy) + else: + points = [ + cast(Sequence[float], tuple(xy[i : i + 2])) + for i in range(0, len(xy), 2) + ] + for i in range(1, len(points) - 1): + point = points[i] + angles = [ + math.degrees(math.atan2(end[0] - start[0], start[1] - end[1])) + % 360 + for start, end in ( + (points[i - 1], point), + (point, points[i + 1]), + ) + ] + if angles[0] == angles[1]: + # This is a straight line, so no joint is required + continue + + def coord_at_angle( + coord: Sequence[float], angle: float + ) -> tuple[float, float]: + x, y = coord + angle -= 90 + distance = width / 2 - 1 + return tuple( + p + (math.floor(p_d) if p_d > 0 else math.ceil(p_d)) + for p, p_d in ( + (x, distance * math.cos(math.radians(angle))), + (y, distance * math.sin(math.radians(angle))), + ) + ) + + flipped = ( + angles[1] > angles[0] and angles[1] - 180 > angles[0] + ) or (angles[1] < angles[0] and angles[1] + 180 > angles[0]) + coords = [ + (point[0] - width / 2 + 1, point[1] - width / 2 + 1), + (point[0] + width / 2 - 1, point[1] + width / 2 - 1), + ] + if flipped: + start, end = (angles[1] + 90, angles[0] + 90) + else: + start, end = (angles[0] - 90, angles[1] - 90) + self.pieslice(coords, start - 90, end - 90, fill) + + if width > 8: + # Cover potential gaps between the line and the joint + if flipped: + gap_coords = [ + coord_at_angle(point, angles[0] + 90), + point, + coord_at_angle(point, angles[1] + 90), + ] + else: + gap_coords = [ + coord_at_angle(point, angles[0] - 90), + point, + coord_at_angle(point, angles[1] - 90), + ] + self.line(gap_coords, fill, width=3) + + def shape(self, shape, fill=None, outline=None) -> None: + """(Experimental) Draw a shape.""" + shape.close() + ink, fill = self._getink(outline, fill) + if fill is not None: + self.draw.draw_outline(shape, fill, 1) + if ink is not None and ink != fill: + self.draw.draw_outline(shape, ink, 0) + + def pieslice( + self, xy: Coords, start, end, fill=None, outline=None, width=1 + ) -> None: + """Draw a pieslice.""" + ink, fill = self._getink(outline, fill) + if fill is not None: + self.draw.draw_pieslice(xy, start, end, fill, 1) + if ink is not None and ink != fill and width != 0: + self.draw.draw_pieslice(xy, start, end, ink, 0, width) + + def point(self, xy: Coords, fill=None) -> None: + """Draw one or more individual pixels.""" + ink, fill = self._getink(fill) + if ink is not None: + self.draw.draw_points(xy, ink) + + def polygon(self, xy: Coords, fill=None, outline=None, width=1) -> None: + """Draw a polygon.""" + ink, fill = self._getink(outline, fill) + if fill is not None: + self.draw.draw_polygon(xy, fill, 1) + if ink is not None and ink != fill and width != 0: + if width == 1: + self.draw.draw_polygon(xy, ink, 0, width) + elif self.im is not None: + # To avoid expanding the polygon outwards, + # use the fill as a mask + mask = Image.new("1", self.im.size) + mask_ink = self._getink(1)[0] + + fill_im = mask.copy() + draw = Draw(fill_im) + draw.draw.draw_polygon(xy, mask_ink, 1) + + ink_im = mask.copy() + draw = Draw(ink_im) + width = width * 2 - 1 + draw.draw.draw_polygon(xy, mask_ink, 0, width) + + mask.paste(ink_im, mask=fill_im) + + im = Image.new(self.mode, self.im.size) + draw = Draw(im) + draw.draw.draw_polygon(xy, ink, 0, width) + self.im.paste(im.im, (0, 0) + im.size, mask.im) + + def regular_polygon( + self, bounding_circle, n_sides, rotation=0, fill=None, outline=None, width=1 + ) -> None: + """Draw a regular polygon.""" + xy = _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) + self.polygon(xy, fill, outline, width) + + def rectangle(self, xy: Coords, fill=None, outline=None, width=1) -> None: + """Draw a rectangle.""" + ink, fill = self._getink(outline, fill) + if fill is not None: + self.draw.draw_rectangle(xy, fill, 1) + if ink is not None and ink != fill and width != 0: + self.draw.draw_rectangle(xy, ink, 0, width) + + def rounded_rectangle( + self, xy: Coords, radius=0, fill=None, outline=None, width=1, *, corners=None + ) -> None: + """Draw a rounded rectangle.""" + if isinstance(xy[0], (list, tuple)): + (x0, y0), (x1, y1) = cast(Sequence[Sequence[float]], xy) + else: + x0, y0, x1, y1 = cast(Sequence[float], xy) + if x1 < x0: + msg = "x1 must be greater than or equal to x0" + raise ValueError(msg) + if y1 < y0: + msg = "y1 must be greater than or equal to y0" + raise ValueError(msg) + if corners is None: + corners = (True, True, True, True) + + d = radius * 2 + + x0 = round(x0) + y0 = round(y0) + x1 = round(x1) + y1 = round(y1) + full_x, full_y = False, False + if all(corners): + full_x = d >= x1 - x0 - 1 + if full_x: + # The two left and two right corners are joined + d = x1 - x0 + full_y = d >= y1 - y0 - 1 + if full_y: + # The two top and two bottom corners are joined + d = y1 - y0 + if full_x and full_y: + # If all corners are joined, that is a circle + return self.ellipse(xy, fill, outline, width) + + if d == 0 or not any(corners): + # If the corners have no curve, + # or there are no corners, + # that is a rectangle + return self.rectangle(xy, fill, outline, width) + + r = d // 2 + ink, fill = self._getink(outline, fill) + + def draw_corners(pieslice) -> None: + parts: tuple[tuple[tuple[float, float, float, float], int, int], ...] + if full_x: + # Draw top and bottom halves + parts = ( + ((x0, y0, x0 + d, y0 + d), 180, 360), + ((x0, y1 - d, x0 + d, y1), 0, 180), + ) + elif full_y: + # Draw left and right halves + parts = ( + ((x0, y0, x0 + d, y0 + d), 90, 270), + ((x1 - d, y0, x1, y0 + d), 270, 90), + ) + else: + # Draw four separate corners + parts = tuple( + part + for i, part in enumerate( + ( + ((x0, y0, x0 + d, y0 + d), 180, 270), + ((x1 - d, y0, x1, y0 + d), 270, 360), + ((x1 - d, y1 - d, x1, y1), 0, 90), + ((x0, y1 - d, x0 + d, y1), 90, 180), + ) + ) + if corners[i] + ) + for part in parts: + if pieslice: + self.draw.draw_pieslice(*(part + (fill, 1))) + else: + self.draw.draw_arc(*(part + (ink, width))) + + if fill is not None: + draw_corners(True) + + if full_x: + self.draw.draw_rectangle((x0, y0 + r + 1, x1, y1 - r - 1), fill, 1) + else: + self.draw.draw_rectangle((x0 + r + 1, y0, x1 - r - 1, y1), fill, 1) + if not full_x and not full_y: + left = [x0, y0, x0 + r, y1] + if corners[0]: + left[1] += r + 1 + if corners[3]: + left[3] -= r + 1 + self.draw.draw_rectangle(left, fill, 1) + + right = [x1 - r, y0, x1, y1] + if corners[1]: + right[1] += r + 1 + if corners[2]: + right[3] -= r + 1 + self.draw.draw_rectangle(right, fill, 1) + if ink is not None and ink != fill and width != 0: + draw_corners(False) + + if not full_x: + top = [x0, y0, x1, y0 + width - 1] + if corners[0]: + top[0] += r + 1 + if corners[1]: + top[2] -= r + 1 + self.draw.draw_rectangle(top, ink, 1) + + bottom = [x0, y1 - width + 1, x1, y1] + if corners[3]: + bottom[0] += r + 1 + if corners[2]: + bottom[2] -= r + 1 + self.draw.draw_rectangle(bottom, ink, 1) + if not full_y: + left = [x0, y0, x0 + width - 1, y1] + if corners[0]: + left[1] += r + 1 + if corners[3]: + left[3] -= r + 1 + self.draw.draw_rectangle(left, ink, 1) + + right = [x1 - width + 1, y0, x1, y1] + if corners[1]: + right[1] += r + 1 + if corners[2]: + right[3] -= r + 1 + self.draw.draw_rectangle(right, ink, 1) + + def _multiline_check(self, text: AnyStr) -> bool: + split_character = "\n" if isinstance(text, str) else b"\n" + + return split_character in text + + def _multiline_split(self, text: AnyStr) -> list[AnyStr]: + return text.split("\n" if isinstance(text, str) else b"\n") + + def _multiline_spacing(self, font, spacing, stroke_width): + return ( + self.textbbox((0, 0), "A", font, stroke_width=stroke_width)[3] + + stroke_width + + spacing + ) + + def text( + self, + xy: tuple[float, float], + text: str, + fill=None, + font: ( + ImageFont.ImageFont + | ImageFont.FreeTypeFont + | ImageFont.TransposedFont + | None + ) = None, + anchor=None, + spacing=4, + align="left", + direction=None, + features=None, + language=None, + stroke_width=0, + stroke_fill=None, + embedded_color=False, + *args, + **kwargs, + ) -> None: + """Draw text.""" + if embedded_color and self.mode not in ("RGB", "RGBA"): + msg = "Embedded color supported only in RGB and RGBA modes" + raise ValueError(msg) + + if font is None: + font = self._getfont(kwargs.get("font_size")) + + if self._multiline_check(text): + return self.multiline_text( + xy, + text, + fill, + font, + anchor, + spacing, + align, + direction, + features, + language, + stroke_width, + stroke_fill, + embedded_color, + ) + + def getink(fill): + ink, fill = self._getink(fill) + if ink is None: + return fill + return ink + + def draw_text(ink, stroke_width=0, stroke_offset=None) -> None: + mode = self.fontmode + if stroke_width == 0 and embedded_color: + mode = "RGBA" + coord = [] + start = [] + for i in range(2): + coord.append(int(xy[i])) + start.append(math.modf(xy[i])[0]) + try: + mask, offset = font.getmask2( # type: ignore[union-attr,misc] + text, + mode, + direction=direction, + features=features, + language=language, + stroke_width=stroke_width, + anchor=anchor, + ink=ink, + start=start, + *args, + **kwargs, + ) + coord = [coord[0] + offset[0], coord[1] + offset[1]] + except AttributeError: + try: + mask = font.getmask( # type: ignore[misc] + text, + mode, + direction, + features, + language, + stroke_width, + anchor, + ink, + start=start, + *args, + **kwargs, + ) + except TypeError: + mask = font.getmask(text) + if stroke_offset: + coord = [coord[0] + stroke_offset[0], coord[1] + stroke_offset[1]] + if mode == "RGBA": + # font.getmask2(mode="RGBA") returns color in RGB bands and mask in A + # extract mask and set text alpha + color, mask = mask, mask.getband(3) + ink_alpha = struct.pack("i", ink)[3] + color.fillband(3, ink_alpha) + x, y = coord + if self.im is not None: + self.im.paste( + color, (x, y, x + mask.size[0], y + mask.size[1]), mask + ) + else: + self.draw.draw_bitmap(coord, mask, ink) + + ink = getink(fill) + if ink is not None: + stroke_ink = None + if stroke_width: + stroke_ink = getink(stroke_fill) if stroke_fill is not None else ink + + if stroke_ink is not None: + # Draw stroked text + draw_text(stroke_ink, stroke_width) + + # Draw normal text + draw_text(ink, 0) + else: + # Only draw normal text + draw_text(ink) + + def multiline_text( + self, + xy: tuple[float, float], + text: str, + fill=None, + font: ( + ImageFont.ImageFont + | ImageFont.FreeTypeFont + | ImageFont.TransposedFont + | None + ) = None, + anchor=None, + spacing=4, + align="left", + direction=None, + features=None, + language=None, + stroke_width=0, + stroke_fill=None, + embedded_color=False, + *, + font_size=None, + ) -> None: + if direction == "ttb": + msg = "ttb direction is unsupported for multiline text" + raise ValueError(msg) + + if anchor is None: + anchor = "la" + elif len(anchor) != 2: + msg = "anchor must be a 2 character string" + raise ValueError(msg) + elif anchor[1] in "tb": + msg = "anchor not supported for multiline text" + raise ValueError(msg) + + if font is None: + font = self._getfont(font_size) + + widths = [] + max_width: float = 0 + lines = self._multiline_split(text) + line_spacing = self._multiline_spacing(font, spacing, stroke_width) + for line in lines: + line_width = self.textlength( + line, font, direction=direction, features=features, language=language + ) + widths.append(line_width) + max_width = max(max_width, line_width) + + top = xy[1] + if anchor[1] == "m": + top -= (len(lines) - 1) * line_spacing / 2.0 + elif anchor[1] == "d": + top -= (len(lines) - 1) * line_spacing + + for idx, line in enumerate(lines): + left = xy[0] + width_difference = max_width - widths[idx] + + # first align left by anchor + if anchor[0] == "m": + left -= width_difference / 2.0 + elif anchor[0] == "r": + left -= width_difference + + # then align by align parameter + if align == "left": + pass + elif align == "center": + left += width_difference / 2.0 + elif align == "right": + left += width_difference + else: + msg = 'align must be "left", "center" or "right"' + raise ValueError(msg) + + self.text( + (left, top), + line, + fill, + font, + anchor, + direction=direction, + features=features, + language=language, + stroke_width=stroke_width, + stroke_fill=stroke_fill, + embedded_color=embedded_color, + ) + top += line_spacing + + def textlength( + self, + text: str, + font: ( + ImageFont.ImageFont + | ImageFont.FreeTypeFont + | ImageFont.TransposedFont + | None + ) = None, + direction=None, + features=None, + language=None, + embedded_color=False, + *, + font_size=None, + ) -> float: + """Get the length of a given string, in pixels with 1/64 precision.""" + if self._multiline_check(text): + msg = "can't measure length of multiline text" + raise ValueError(msg) + if embedded_color and self.mode not in ("RGB", "RGBA"): + msg = "Embedded color supported only in RGB and RGBA modes" + raise ValueError(msg) + + if font is None: + font = self._getfont(font_size) + mode = "RGBA" if embedded_color else self.fontmode + return font.getlength(text, mode, direction, features, language) + + def textbbox( + self, + xy, + text, + font=None, + anchor=None, + spacing=4, + align="left", + direction=None, + features=None, + language=None, + stroke_width=0, + embedded_color=False, + *, + font_size=None, + ) -> tuple[int, int, int, int]: + """Get the bounding box of a given string, in pixels.""" + if embedded_color and self.mode not in ("RGB", "RGBA"): + msg = "Embedded color supported only in RGB and RGBA modes" + raise ValueError(msg) + + if font is None: + font = self._getfont(font_size) + + if self._multiline_check(text): + return self.multiline_textbbox( + xy, + text, + font, + anchor, + spacing, + align, + direction, + features, + language, + stroke_width, + embedded_color, + ) + + mode = "RGBA" if embedded_color else self.fontmode + bbox = font.getbbox( + text, mode, direction, features, language, stroke_width, anchor + ) + return bbox[0] + xy[0], bbox[1] + xy[1], bbox[2] + xy[0], bbox[3] + xy[1] + + def multiline_textbbox( + self, + xy, + text, + font=None, + anchor=None, + spacing=4, + align="left", + direction=None, + features=None, + language=None, + stroke_width=0, + embedded_color=False, + *, + font_size=None, + ) -> tuple[int, int, int, int]: + if direction == "ttb": + msg = "ttb direction is unsupported for multiline text" + raise ValueError(msg) + + if anchor is None: + anchor = "la" + elif len(anchor) != 2: + msg = "anchor must be a 2 character string" + raise ValueError(msg) + elif anchor[1] in "tb": + msg = "anchor not supported for multiline text" + raise ValueError(msg) + + if font is None: + font = self._getfont(font_size) + + widths = [] + max_width: float = 0 + lines = self._multiline_split(text) + line_spacing = self._multiline_spacing(font, spacing, stroke_width) + for line in lines: + line_width = self.textlength( + line, + font, + direction=direction, + features=features, + language=language, + embedded_color=embedded_color, + ) + widths.append(line_width) + max_width = max(max_width, line_width) + + top = xy[1] + if anchor[1] == "m": + top -= (len(lines) - 1) * line_spacing / 2.0 + elif anchor[1] == "d": + top -= (len(lines) - 1) * line_spacing + + bbox: tuple[int, int, int, int] | None = None + + for idx, line in enumerate(lines): + left = xy[0] + width_difference = max_width - widths[idx] + + # first align left by anchor + if anchor[0] == "m": + left -= width_difference / 2.0 + elif anchor[0] == "r": + left -= width_difference + + # then align by align parameter + if align == "left": + pass + elif align == "center": + left += width_difference / 2.0 + elif align == "right": + left += width_difference + else: + msg = 'align must be "left", "center" or "right"' + raise ValueError(msg) + + bbox_line = self.textbbox( + (left, top), + line, + font, + anchor, + direction=direction, + features=features, + language=language, + stroke_width=stroke_width, + embedded_color=embedded_color, + ) + if bbox is None: + bbox = bbox_line + else: + bbox = ( + min(bbox[0], bbox_line[0]), + min(bbox[1], bbox_line[1]), + max(bbox[2], bbox_line[2]), + max(bbox[3], bbox_line[3]), + ) + + top += line_spacing + + if bbox is None: + return xy[0], xy[1], xy[0], xy[1] + return bbox + + +def Draw(im, mode: str | None = None) -> ImageDraw: + """ + A simple 2D drawing interface for PIL images. + + :param im: The image to draw in. + :param mode: Optional mode to use for color values. For RGB + images, this argument can be RGB or RGBA (to blend the + drawing into the image). For all other modes, this argument + must be the same as the image mode. If omitted, the mode + defaults to the mode of the image. + """ + try: + return im.getdraw(mode) + except AttributeError: + return ImageDraw(im, mode) + + +# experimental access to the outline API +try: + Outline = Image.core.outline +except AttributeError: + Outline = None + + +def getdraw(im=None, hints=None): + """ + :param im: The image to draw in. + :param hints: An optional list of hints. Deprecated. + :returns: A (drawing context, drawing resource factory) tuple. + """ + if hints is not None: + deprecate("'hints' parameter", 12) + from . import ImageDraw2 + + if im: + im = ImageDraw2.Draw(im) + return im, ImageDraw2 + + +def floodfill( + image: Image.Image, + xy: tuple[int, int], + value: float | tuple[int, ...], + border: float | tuple[int, ...] | None = None, + thresh: float = 0, +) -> None: + """ + (experimental) Fills a bounded region with a given color. + + :param image: Target image. + :param xy: Seed position (a 2-item coordinate tuple). See + :ref:`coordinate-system`. + :param value: Fill color. + :param border: Optional border value. If given, the region consists of + pixels with a color different from the border color. If not given, + the region consists of pixels having the same color as the seed + pixel. + :param thresh: Optional threshold value which specifies a maximum + tolerable difference of a pixel value from the 'background' in + order for it to be replaced. Useful for filling regions of + non-homogeneous, but similar, colors. + """ + # based on an implementation by Eric S. Raymond + # amended by yo1995 @20180806 + pixel = image.load() + x, y = xy + try: + background = pixel[x, y] + if _color_diff(value, background) <= thresh: + return # seed point already has fill color + pixel[x, y] = value + except (ValueError, IndexError): + return # seed point outside image + edge = {(x, y)} + # use a set to keep record of current and previous edge pixels + # to reduce memory consumption + full_edge = set() + while edge: + new_edge = set() + for x, y in edge: # 4 adjacent method + for s, t in ((x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)): + # If already processed, or if a coordinate is negative, skip + if (s, t) in full_edge or s < 0 or t < 0: + continue + try: + p = pixel[s, t] + except (ValueError, IndexError): + pass + else: + full_edge.add((s, t)) + if border is None: + fill = _color_diff(p, background) <= thresh + else: + fill = p not in (value, border) + if fill: + pixel[s, t] = value + new_edge.add((s, t)) + full_edge = edge # discard pixels processed + edge = new_edge + + +def _compute_regular_polygon_vertices( + bounding_circle, n_sides, rotation +) -> list[tuple[float, float]]: + """ + Generate a list of vertices for a 2D regular polygon. + + :param bounding_circle: The bounding circle is a tuple defined + by a point and radius. The polygon is inscribed in this circle. + (e.g. ``bounding_circle=(x, y, r)`` or ``((x, y), r)``) + :param n_sides: Number of sides + (e.g. ``n_sides=3`` for a triangle, ``6`` for a hexagon) + :param rotation: Apply an arbitrary rotation to the polygon + (e.g. ``rotation=90``, applies a 90 degree rotation) + :return: List of regular polygon vertices + (e.g. ``[(25, 50), (50, 50), (50, 25), (25, 25)]``) + + How are the vertices computed? + 1. Compute the following variables + - theta: Angle between the apothem & the nearest polygon vertex + - side_length: Length of each polygon edge + - centroid: Center of bounding circle (1st, 2nd elements of bounding_circle) + - polygon_radius: Polygon radius (last element of bounding_circle) + - angles: Location of each polygon vertex in polar grid + (e.g. A square with 0 degree rotation => [225.0, 315.0, 45.0, 135.0]) + + 2. For each angle in angles, get the polygon vertex at that angle + The vertex is computed using the equation below. + X= xcos(φ) + ysin(φ) + Y= −xsin(φ) + ycos(φ) + + Note: + φ = angle in degrees + x = 0 + y = polygon_radius + + The formula above assumes rotation around the origin. + In our case, we are rotating around the centroid. + To account for this, we use the formula below + X = xcos(φ) + ysin(φ) + centroid_x + Y = −xsin(φ) + ycos(φ) + centroid_y + """ + # 1. Error Handling + # 1.1 Check `n_sides` has an appropriate value + if not isinstance(n_sides, int): + msg = "n_sides should be an int" + raise TypeError(msg) + if n_sides < 3: + msg = "n_sides should be an int > 2" + raise ValueError(msg) + + # 1.2 Check `bounding_circle` has an appropriate value + if not isinstance(bounding_circle, (list, tuple)): + msg = "bounding_circle should be a sequence" + raise TypeError(msg) + + if len(bounding_circle) == 3: + *centroid, polygon_radius = bounding_circle + elif len(bounding_circle) == 2: + centroid, polygon_radius = bounding_circle + else: + msg = ( + "bounding_circle should contain 2D coordinates " + "and a radius (e.g. (x, y, r) or ((x, y), r) )" + ) + raise ValueError(msg) + + if not all(isinstance(i, (int, float)) for i in (*centroid, polygon_radius)): + msg = "bounding_circle should only contain numeric data" + raise ValueError(msg) + + if not len(centroid) == 2: + msg = "bounding_circle centre should contain 2D coordinates (e.g. (x, y))" + raise ValueError(msg) + + if polygon_radius <= 0: + msg = "bounding_circle radius should be > 0" + raise ValueError(msg) + + # 1.3 Check `rotation` has an appropriate value + if not isinstance(rotation, (int, float)): + msg = "rotation should be an int or float" + raise ValueError(msg) + + # 2. Define Helper Functions + def _apply_rotation(point: list[float], degrees: float) -> tuple[int, int]: + return ( + round( + point[0] * math.cos(math.radians(360 - degrees)) + - point[1] * math.sin(math.radians(360 - degrees)) + + centroid[0], + 2, + ), + round( + point[1] * math.cos(math.radians(360 - degrees)) + + point[0] * math.sin(math.radians(360 - degrees)) + + centroid[1], + 2, + ), + ) + + def _compute_polygon_vertex(angle: float) -> tuple[int, int]: + start_point = [polygon_radius, 0] + return _apply_rotation(start_point, angle) + + def _get_angles(n_sides: int, rotation: float) -> list[float]: + angles = [] + degrees = 360 / n_sides + # Start with the bottom left polygon vertex + current_angle = (270 - 0.5 * degrees) + rotation + for _ in range(0, n_sides): + angles.append(current_angle) + current_angle += degrees + if current_angle > 360: + current_angle -= 360 + return angles + + # 3. Variable Declarations + angles = _get_angles(n_sides, rotation) + + # 4. Compute Vertices + return [_compute_polygon_vertex(angle) for angle in angles] + + +def _color_diff( + color1: float | tuple[int, ...], color2: float | tuple[int, ...] +) -> float: + """ + Uses 1-norm distance to calculate difference between two values. + """ + 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))) diff --git a/src/PIL/ImageDraw2.py b/src/PIL/ImageDraw2.py index b42f5d9ea..4c2e943f6 100644 --- a/src/PIL/ImageDraw2.py +++ b/src/PIL/ImageDraw2.py @@ -1,193 +1,193 @@ -# -# The Python Imaging Library -# $Id$ -# -# WCK-style drawing interface operations -# -# History: -# 2003-12-07 fl created -# 2005-05-15 fl updated; added to PIL as ImageDraw2 -# 2005-05-15 fl added text support -# 2005-05-20 fl added arc/chord/pieslice support -# -# Copyright (c) 2003-2005 by Secret Labs AB -# Copyright (c) 2003-2005 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# - - -""" -(Experimental) WCK-style drawing interface operations - -.. seealso:: :py:mod:`PIL.ImageDraw` -""" -from __future__ import annotations - -from . import Image, ImageColor, ImageDraw, ImageFont, ImagePath - - -class Pen: - """Stores an outline color and width.""" - - def __init__(self, color: str, width: int = 1, opacity: int = 255) -> None: - self.color = ImageColor.getrgb(color) - self.width = width - - -class Brush: - """Stores a fill color""" - - def __init__(self, color: str, opacity: int = 255) -> None: - self.color = ImageColor.getrgb(color) - - -class Font: - """Stores a TrueType font and color""" - - def __init__(self, color, file, size=12): - # FIXME: add support for bitmap fonts - self.color = ImageColor.getrgb(color) - self.font = ImageFont.truetype(file, size) - - -class Draw: - """ - (Experimental) WCK-style drawing interface - """ - - def __init__(self, image, size=None, color=None): - if not hasattr(image, "im"): - image = Image.new(image, size, color) - self.draw = ImageDraw.Draw(image) - self.image = image - self.transform = None - - def flush(self) -> Image.Image: - return self.image - - def render(self, op, xy, pen, brush=None): - # handle color arguments - outline = fill = None - width = 1 - if isinstance(pen, Pen): - outline = pen.color - width = pen.width - elif isinstance(brush, Pen): - outline = brush.color - width = brush.width - if isinstance(brush, Brush): - fill = brush.color - elif isinstance(pen, Brush): - fill = pen.color - # handle transformation - if self.transform: - xy = ImagePath.Path(xy) - xy.transform(self.transform) - # render the item - if op == "line": - self.draw.line(xy, fill=outline, width=width) - else: - getattr(self.draw, op)(xy, fill=fill, outline=outline) - - def settransform(self, offset): - """Sets a transformation offset.""" - (xoffset, yoffset) = offset - self.transform = (1, 0, xoffset, 0, 1, yoffset) - - def arc(self, xy, start, end, *options): - """ - Draws an arc (a portion of a circle outline) between the start and end - angles, inside the given bounding box. - - .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.arc` - """ - self.render("arc", xy, start, end, *options) - - def chord(self, xy, start, end, *options): - """ - Same as :py:meth:`~PIL.ImageDraw2.Draw.arc`, but connects the end points - with a straight line. - - .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.chord` - """ - self.render("chord", xy, start, end, *options) - - def ellipse(self, xy, *options): - """ - Draws an ellipse inside the given bounding box. - - .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.ellipse` - """ - self.render("ellipse", xy, *options) - - def line(self, xy, *options): - """ - Draws a line between the coordinates in the ``xy`` list. - - .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.line` - """ - self.render("line", xy, *options) - - def pieslice(self, xy, start, end, *options): - """ - Same as arc, but also draws straight lines between the end points and the - center of the bounding box. - - .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.pieslice` - """ - self.render("pieslice", xy, start, end, *options) - - def polygon(self, xy, *options): - """ - Draws a polygon. - - The polygon outline consists of straight lines between the given - coordinates, plus a straight line between the last and the first - coordinate. - - - .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.polygon` - """ - self.render("polygon", xy, *options) - - def rectangle(self, xy, *options): - """ - Draws a rectangle. - - .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.rectangle` - """ - self.render("rectangle", xy, *options) - - def text(self, xy, text, font): - """ - Draws the string at the given position. - - .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.text` - """ - if self.transform: - xy = ImagePath.Path(xy) - xy.transform(self.transform) - self.draw.text(xy, text, font=font.font, fill=font.color) - - def textbbox(self, xy, text, font): - """ - Returns bounding box (in pixels) of given text. - - :return: ``(left, top, right, bottom)`` bounding box - - .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.textbbox` - """ - if self.transform: - xy = ImagePath.Path(xy) - xy.transform(self.transform) - return self.draw.textbbox(xy, text, font=font.font) - - def textlength(self, text, font): - """ - Returns length (in pixels) of given text. - This is the amount by which following text should be offset. - - .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.textlength` - """ - return self.draw.textlength(text, font=font.font) +# +# The Python Imaging Library +# $Id$ +# +# WCK-style drawing interface operations +# +# History: +# 2003-12-07 fl created +# 2005-05-15 fl updated; added to PIL as ImageDraw2 +# 2005-05-15 fl added text support +# 2005-05-20 fl added arc/chord/pieslice support +# +# Copyright (c) 2003-2005 by Secret Labs AB +# Copyright (c) 2003-2005 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# + + +""" +(Experimental) WCK-style drawing interface operations + +.. seealso:: :py:mod:`PIL.ImageDraw` +""" +from __future__ import annotations + +from . import Image, ImageColor, ImageDraw, ImageFont, ImagePath + + +class Pen: + """Stores an outline color and width.""" + + def __init__(self, color: str, width: int = 1, opacity: int = 255) -> None: + self.color = ImageColor.getrgb(color) + self.width = width + + +class Brush: + """Stores a fill color""" + + def __init__(self, color: str, opacity: int = 255) -> None: + self.color = ImageColor.getrgb(color) + + +class Font: + """Stores a TrueType font and color""" + + def __init__(self, color, file, size=12): + # FIXME: add support for bitmap fonts + self.color = ImageColor.getrgb(color) + self.font = ImageFont.truetype(file, size) + + +class Draw: + """ + (Experimental) WCK-style drawing interface + """ + + def __init__(self, image, size=None, color=None): + if not hasattr(image, "im"): + image = Image.new(image, size, color) + self.draw = ImageDraw.Draw(image) + self.image = image + self.transform = None + + def flush(self) -> Image.Image: + return self.image + + def render(self, op, xy, pen, brush=None): + # handle color arguments + outline = fill = None + width = 1 + if isinstance(pen, Pen): + outline = pen.color + width = pen.width + elif isinstance(brush, Pen): + outline = brush.color + width = brush.width + if isinstance(brush, Brush): + fill = brush.color + elif isinstance(pen, Brush): + fill = pen.color + # handle transformation + if self.transform: + xy = ImagePath.Path(xy) + xy.transform(self.transform) + # render the item + if op == "line": + self.draw.line(xy, fill=outline, width=width) + else: + getattr(self.draw, op)(xy, fill=fill, outline=outline) + + def settransform(self, offset): + """Sets a transformation offset.""" + (xoffset, yoffset) = offset + self.transform = (1, 0, xoffset, 0, 1, yoffset) + + def arc(self, xy, start, end, *options): + """ + Draws an arc (a portion of a circle outline) between the start and end + angles, inside the given bounding box. + + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.arc` + """ + self.render("arc", xy, start, end, *options) + + def chord(self, xy, start, end, *options): + """ + Same as :py:meth:`~PIL.ImageDraw2.Draw.arc`, but connects the end points + with a straight line. + + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.chord` + """ + self.render("chord", xy, start, end, *options) + + def ellipse(self, xy, *options): + """ + Draws an ellipse inside the given bounding box. + + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.ellipse` + """ + self.render("ellipse", xy, *options) + + def line(self, xy, *options): + """ + Draws a line between the coordinates in the ``xy`` list. + + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.line` + """ + self.render("line", xy, *options) + + def pieslice(self, xy, start, end, *options): + """ + Same as arc, but also draws straight lines between the end points and the + center of the bounding box. + + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.pieslice` + """ + self.render("pieslice", xy, start, end, *options) + + def polygon(self, xy, *options): + """ + Draws a polygon. + + The polygon outline consists of straight lines between the given + coordinates, plus a straight line between the last and the first + coordinate. + + + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.polygon` + """ + self.render("polygon", xy, *options) + + def rectangle(self, xy, *options): + """ + Draws a rectangle. + + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.rectangle` + """ + self.render("rectangle", xy, *options) + + def text(self, xy, text, font): + """ + Draws the string at the given position. + + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.text` + """ + if self.transform: + xy = ImagePath.Path(xy) + xy.transform(self.transform) + self.draw.text(xy, text, font=font.font, fill=font.color) + + def textbbox(self, xy, text, font): + """ + Returns bounding box (in pixels) of given text. + + :return: ``(left, top, right, bottom)`` bounding box + + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.textbbox` + """ + if self.transform: + xy = ImagePath.Path(xy) + xy.transform(self.transform) + return self.draw.textbbox(xy, text, font=font.font) + + def textlength(self, text, font): + """ + Returns length (in pixels) of given text. + This is the amount by which following text should be offset. + + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.textlength` + """ + return self.draw.textlength(text, font=font.font) diff --git a/src/PIL/ImageEnhance.py b/src/PIL/ImageEnhance.py index d7e99a968..39818dc29 100644 --- a/src/PIL/ImageEnhance.py +++ b/src/PIL/ImageEnhance.py @@ -1,107 +1,107 @@ -# -# The Python Imaging Library. -# $Id$ -# -# image enhancement classes -# -# For a background, see "Image Processing By Interpolation and -# Extrapolation", Paul Haeberli and Douglas Voorhies. Available -# at http://www.graficaobscura.com/interp/index.html -# -# History: -# 1996-03-23 fl Created -# 2009-06-16 fl Fixed mean calculation -# -# Copyright (c) Secret Labs AB 1997. -# Copyright (c) Fredrik Lundh 1996. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -from . import Image, ImageFilter, ImageStat - - -class _Enhance: - image: Image.Image - degenerate: Image.Image - - def enhance(self, factor: float) -> Image.Image: - """ - Returns an enhanced image. - - :param factor: A floating point value controlling the enhancement. - Factor 1.0 always returns a copy of the original image, - lower factors mean less color (brightness, contrast, - etc), and higher values more. There are no restrictions - on this value. - :rtype: :py:class:`~PIL.Image.Image` - """ - return Image.blend(self.degenerate, self.image, factor) - - -class Color(_Enhance): - """Adjust image color balance. - - This class can be used to adjust the colour balance of an image, in - a manner similar to the controls on a colour TV set. An enhancement - factor of 0.0 gives a black and white image. A factor of 1.0 gives - the original image. - """ - - def __init__(self, image: Image.Image) -> None: - self.image = image - self.intermediate_mode = "L" - if "A" in image.getbands(): - self.intermediate_mode = "LA" - - self.degenerate = image.convert(self.intermediate_mode).convert(image.mode) - - -class Contrast(_Enhance): - """Adjust image contrast. - - This class can be used to control the contrast of an image, similar - to the contrast control on a TV set. An enhancement factor of 0.0 - gives a solid gray image. A factor of 1.0 gives the original image. - """ - - def __init__(self, image: Image.Image) -> None: - self.image = image - mean = int(ImageStat.Stat(image.convert("L")).mean[0] + 0.5) - self.degenerate = Image.new("L", image.size, mean).convert(image.mode) - - if "A" in image.getbands(): - self.degenerate.putalpha(image.getchannel("A")) - - -class Brightness(_Enhance): - """Adjust image brightness. - - This class can be used to control the brightness of an image. An - enhancement factor of 0.0 gives a black image. A factor of 1.0 gives the - original image. - """ - - def __init__(self, image: Image.Image) -> None: - self.image = image - self.degenerate = Image.new(image.mode, image.size, 0) - - if "A" in image.getbands(): - self.degenerate.putalpha(image.getchannel("A")) - - -class Sharpness(_Enhance): - """Adjust image sharpness. - - This class can be used to adjust the sharpness of an image. An - enhancement factor of 0.0 gives a blurred image, a factor of 1.0 gives the - original image, and a factor of 2.0 gives a sharpened image. - """ - - def __init__(self, image: Image.Image) -> None: - self.image = image - self.degenerate = image.filter(ImageFilter.SMOOTH) - - if "A" in image.getbands(): - self.degenerate.putalpha(image.getchannel("A")) +# +# The Python Imaging Library. +# $Id$ +# +# image enhancement classes +# +# For a background, see "Image Processing By Interpolation and +# Extrapolation", Paul Haeberli and Douglas Voorhies. Available +# at http://www.graficaobscura.com/interp/index.html +# +# History: +# 1996-03-23 fl Created +# 2009-06-16 fl Fixed mean calculation +# +# Copyright (c) Secret Labs AB 1997. +# Copyright (c) Fredrik Lundh 1996. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +from . import Image, ImageFilter, ImageStat + + +class _Enhance: + image: Image.Image + degenerate: Image.Image + + def enhance(self, factor: float) -> Image.Image: + """ + Returns an enhanced image. + + :param factor: A floating point value controlling the enhancement. + Factor 1.0 always returns a copy of the original image, + lower factors mean less color (brightness, contrast, + etc), and higher values more. There are no restrictions + on this value. + :rtype: :py:class:`~PIL.Image.Image` + """ + return Image.blend(self.degenerate, self.image, factor) + + +class Color(_Enhance): + """Adjust image color balance. + + This class can be used to adjust the colour balance of an image, in + a manner similar to the controls on a colour TV set. An enhancement + factor of 0.0 gives a black and white image. A factor of 1.0 gives + the original image. + """ + + def __init__(self, image: Image.Image) -> None: + self.image = image + self.intermediate_mode = "L" + if "A" in image.getbands(): + self.intermediate_mode = "LA" + + self.degenerate = image.convert(self.intermediate_mode).convert(image.mode) + + +class Contrast(_Enhance): + """Adjust image contrast. + + This class can be used to control the contrast of an image, similar + to the contrast control on a TV set. An enhancement factor of 0.0 + gives a solid gray image. A factor of 1.0 gives the original image. + """ + + def __init__(self, image: Image.Image) -> None: + self.image = image + mean = int(ImageStat.Stat(image.convert("L")).mean[0] + 0.5) + self.degenerate = Image.new("L", image.size, mean).convert(image.mode) + + if "A" in image.getbands(): + self.degenerate.putalpha(image.getchannel("A")) + + +class Brightness(_Enhance): + """Adjust image brightness. + + This class can be used to control the brightness of an image. An + enhancement factor of 0.0 gives a black image. A factor of 1.0 gives the + original image. + """ + + def __init__(self, image: Image.Image) -> None: + self.image = image + self.degenerate = Image.new(image.mode, image.size, 0) + + if "A" in image.getbands(): + self.degenerate.putalpha(image.getchannel("A")) + + +class Sharpness(_Enhance): + """Adjust image sharpness. + + This class can be used to adjust the sharpness of an image. An + enhancement factor of 0.0 gives a blurred image, a factor of 1.0 gives the + original image, and a factor of 2.0 gives a sharpened image. + """ + + def __init__(self, image: Image.Image) -> None: + self.image = image + self.degenerate = image.filter(ImageFilter.SMOOTH) + + if "A" in image.getbands(): + self.degenerate.putalpha(image.getchannel("A")) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 5d67409ea..3b6100e13 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -1,807 +1,807 @@ -# -# The Python Imaging Library. -# $Id$ -# -# base class for image file handlers -# -# history: -# 1995-09-09 fl Created -# 1996-03-11 fl Fixed load mechanism. -# 1996-04-15 fl Added pcx/xbm decoders. -# 1996-04-30 fl Added encoders. -# 1996-12-14 fl Added load helpers -# 1997-01-11 fl Use encode_to_file where possible -# 1997-08-27 fl Flush output in _save -# 1998-03-05 fl Use memory mapping for some modes -# 1999-02-04 fl Use memory mapping also for "I;16" and "I;16B" -# 1999-05-31 fl Added image parser -# 2000-10-12 fl Set readonly flag on memory-mapped images -# 2002-03-20 fl Use better messages for common decoder errors -# 2003-04-21 fl Fall back on mmap/map_buffer if map is not available -# 2003-10-30 fl Added StubImageFile class -# 2004-02-25 fl Made incremental parser more robust -# -# Copyright (c) 1997-2004 by Secret Labs AB -# Copyright (c) 1995-2004 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import abc -import io -import itertools -import struct -import sys -from typing import IO, Any, NamedTuple - -from . import Image -from ._deprecate import deprecate -from ._util import is_path - -MAXBLOCK = 65536 - -SAFEBLOCK = 1024 * 1024 - -LOAD_TRUNCATED_IMAGES = False -"""Whether or not to load truncated image files. User code may change this.""" - -ERRORS = { - -1: "image buffer overrun error", - -2: "decoding error", - -3: "unknown error", - -8: "bad configuration", - -9: "out of memory error", -} -""" -Dict of known error codes returned from :meth:`.PyDecoder.decode`, -:meth:`.PyEncoder.encode` :meth:`.PyEncoder.encode_to_pyfd` and -:meth:`.PyEncoder.encode_to_file`. -""" - - -# -# -------------------------------------------------------------------- -# Helpers - - -def _get_oserror(error, *, encoder): - try: - msg = Image.core.getcodecstatus(error) - except AttributeError: - msg = ERRORS.get(error) - if not msg: - msg = f"{'encoder' if encoder else 'decoder'} error {error}" - msg += f" when {'writing' if encoder else 'reading'} image file" - return OSError(msg) - - -def raise_oserror(error): - deprecate( - "raise_oserror", - 12, - action="It is only useful for translating error codes returned by a codec's " - "decode() method, which ImageFile already does automatically.", - ) - raise _get_oserror(error, encoder=False) - - -def _tilesort(t): - # sort on offset - return t[2] - - -class _Tile(NamedTuple): - codec_name: str - extents: tuple[int, int, int, int] - offset: int - args: tuple[Any, ...] | str | None - - -# -# -------------------------------------------------------------------- -# ImageFile base class - - -class ImageFile(Image.Image): - """Base class for image file format handlers.""" - - def __init__(self, fp=None, filename=None): - super().__init__() - - self._min_frame = 0 - - self.custom_mimetype = None - - self.tile = None - """ A list of tile descriptors, or ``None`` """ - - self.readonly = 1 # until we know better - - self.decoderconfig = () - self.decodermaxblock = MAXBLOCK - - if is_path(fp): - # filename - self.fp = open(fp, "rb") - self.filename = fp - self._exclusive_fp = True - else: - # stream - self.fp = fp - self.filename = filename - # can be overridden - self._exclusive_fp = None - - try: - try: - self._open() - except ( - IndexError, # end of data - TypeError, # end of data (ord) - KeyError, # unsupported mode - EOFError, # got header but not the first frame - struct.error, - ) as v: - raise SyntaxError(v) from v - - if not self.mode or self.size[0] <= 0 or self.size[1] <= 0: - msg = "not identified by this driver" - raise SyntaxError(msg) - except BaseException: - # close the file only if we have opened it this constructor - if self._exclusive_fp: - self.fp.close() - raise - - def get_format_mimetype(self): - if self.custom_mimetype: - return self.custom_mimetype - if self.format is not None: - return Image.MIME.get(self.format.upper()) - - def __setstate__(self, state): - self.tile = [] - super().__setstate__(state) - - def verify(self) -> None: - """Check file integrity""" - - # raise exception if something's wrong. must be called - # directly after open, and closes file when finished. - if self._exclusive_fp: - self.fp.close() - self.fp = None - - def load(self): - """Load image data based on tile list""" - - if self.tile is None: - msg = "cannot load this image" - raise OSError(msg) - - pixel = Image.Image.load(self) - if not self.tile: - return pixel - - self.map = None - use_mmap = self.filename and len(self.tile) == 1 - # As of pypy 2.1.0, memory mapping was failing here. - use_mmap = use_mmap and not hasattr(sys, "pypy_version_info") - - readonly = 0 - - # look for read/seek overrides - try: - read = self.load_read - # don't use mmap if there are custom read/seek functions - use_mmap = False - except AttributeError: - read = self.fp.read - - try: - seek = self.load_seek - use_mmap = False - except AttributeError: - seek = self.fp.seek - - if use_mmap: - # try memory mapping - decoder_name, extents, offset, args = self.tile[0] - if isinstance(args, str): - args = (args, 0, 1) - if ( - decoder_name == "raw" - and len(args) >= 3 - and args[0] == self.mode - and args[0] in Image._MAPMODES - ): - try: - # use mmap, if possible - import mmap - - with open(self.filename) as fp: - self.map = mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ) - if offset + self.size[1] * args[1] > self.map.size(): - msg = "buffer is not large enough" - raise OSError(msg) - self.im = Image.core.map_buffer( - self.map, self.size, decoder_name, offset, args - ) - readonly = 1 - # After trashing self.im, - # we might need to reload the palette data. - if self.palette: - self.palette.dirty = 1 - except (AttributeError, OSError, ImportError): - self.map = None - - self.load_prepare() - err_code = -3 # initialize to unknown error - if not self.map: - # sort tiles in file order - self.tile.sort(key=_tilesort) - - try: - # FIXME: This is a hack to handle TIFF's JpegTables tag. - prefix = self.tile_prefix - except AttributeError: - prefix = b"" - - # Remove consecutive duplicates that only differ by their offset - self.tile = [ - list(tiles)[-1] - for _, tiles in itertools.groupby( - self.tile, lambda tile: (tile[0], tile[1], tile[3]) - ) - ] - for decoder_name, extents, offset, args in self.tile: - seek(offset) - decoder = Image._getdecoder( - self.mode, decoder_name, args, self.decoderconfig - ) - try: - decoder.setimage(self.im, extents) - if decoder.pulls_fd: - decoder.setfd(self.fp) - err_code = decoder.decode(b"")[1] - else: - b = prefix - while True: - try: - s = read(self.decodermaxblock) - except (IndexError, struct.error) as e: - # truncated png/gif - if LOAD_TRUNCATED_IMAGES: - break - else: - msg = "image file is truncated" - raise OSError(msg) from e - - if not s: # truncated jpeg - if LOAD_TRUNCATED_IMAGES: - break - else: - msg = ( - "image file is truncated " - f"({len(b)} bytes not processed)" - ) - raise OSError(msg) - - b = b + s - n, err_code = decoder.decode(b) - if n < 0: - break - b = b[n:] - finally: - # Need to cleanup here to prevent leaks - decoder.cleanup() - - self.tile = [] - self.readonly = readonly - - self.load_end() - - if self._exclusive_fp and self._close_exclusive_fp_after_loading: - self.fp.close() - self.fp = None - - if not self.map and not LOAD_TRUNCATED_IMAGES and err_code < 0: - # still raised if decoder fails to return anything - raise _get_oserror(err_code, encoder=False) - - return Image.Image.load(self) - - def load_prepare(self) -> None: - # create image memory if necessary - if not self.im or self.im.mode != self.mode or self.im.size != self.size: - self.im = Image.core.new(self.mode, self.size) - # create palette (optional) - if self.mode == "P": - Image.Image.load(self) - - def load_end(self) -> None: - # may be overridden - pass - - # may be defined for contained formats - # def load_seek(self, pos: int) -> None: - # pass - - # may be defined for blocked formats (e.g. PNG) - # def load_read(self, read_bytes: int) -> bytes: - # pass - - def _seek_check(self, frame): - if ( - frame < self._min_frame - # Only check upper limit on frames if additional seek operations - # are not required to do so - or ( - not (hasattr(self, "_n_frames") and self._n_frames is None) - and frame >= self.n_frames + self._min_frame - ) - ): - msg = "attempt to seek outside sequence" - raise EOFError(msg) - - return self.tell() != frame - - -class StubHandler: - def open(self, im: StubImageFile) -> None: - pass - - @abc.abstractmethod - def load(self, im: StubImageFile) -> Image.Image: - pass - - -class StubImageFile(ImageFile): - """ - Base class for stub image loaders. - - A stub loader is an image loader that can identify files of a - certain format, but relies on external code to load the file. - """ - - def _open(self): - msg = "StubImageFile subclass must implement _open" - raise NotImplementedError(msg) - - def load(self): - loader = self._load() - if loader is None: - msg = f"cannot find loader for this {self.format} file" - raise OSError(msg) - image = loader.load(self) - assert image is not None - # become the other object (!) - self.__class__ = image.__class__ - self.__dict__ = image.__dict__ - return image.load() - - def _load(self): - """(Hook) Find actual image loader.""" - msg = "StubImageFile subclass must implement _load" - raise NotImplementedError(msg) - - -class Parser: - """ - Incremental image parser. This class implements the standard - feed/close consumer interface. - """ - - incremental = None - image: Image.Image | None = None - data = None - decoder = None - offset = 0 - finished = 0 - - def reset(self) -> None: - """ - (Consumer) Reset the parser. Note that you can only call this - method immediately after you've created a parser; parser - instances cannot be reused. - """ - assert self.data is None, "cannot reuse parsers" - - def feed(self, data): - """ - (Consumer) Feed data to the parser. - - :param data: A string buffer. - :exception OSError: If the parser failed to parse the image file. - """ - # collect data - - if self.finished: - return - - if self.data is None: - self.data = data - else: - self.data = self.data + data - - # parse what we have - if self.decoder: - if self.offset > 0: - # skip header - skip = min(len(self.data), self.offset) - self.data = self.data[skip:] - self.offset = self.offset - skip - if self.offset > 0 or not self.data: - return - - n, e = self.decoder.decode(self.data) - - if n < 0: - # end of stream - self.data = None - self.finished = 1 - if e < 0: - # decoding error - self.image = None - raise _get_oserror(e, encoder=False) - else: - # end of image - return - self.data = self.data[n:] - - elif self.image: - # if we end up here with no decoder, this file cannot - # be incrementally parsed. wait until we've gotten all - # available data - pass - - else: - # attempt to open this file - try: - with io.BytesIO(self.data) as fp: - im = Image.open(fp) - except OSError: - pass # not enough data - else: - flag = hasattr(im, "load_seek") or hasattr(im, "load_read") - if flag or len(im.tile) != 1: - # custom load code, or multiple tiles - self.decode = None - else: - # initialize decoder - im.load_prepare() - d, e, o, a = im.tile[0] - im.tile = [] - self.decoder = Image._getdecoder(im.mode, d, a, im.decoderconfig) - self.decoder.setimage(im.im, e) - - # calculate decoder offset - self.offset = o - if self.offset <= len(self.data): - self.data = self.data[self.offset :] - self.offset = 0 - - self.image = im - - def __enter__(self): - return self - - def __exit__(self, *args: object) -> None: - self.close() - - def close(self): - """ - (Consumer) Close the stream. - - :returns: An image object. - :exception OSError: If the parser failed to parse the image file either - because it cannot be identified or cannot be - decoded. - """ - # finish decoding - if self.decoder: - # get rid of what's left in the buffers - self.feed(b"") - self.data = self.decoder = None - if not self.finished: - msg = "image was incomplete" - raise OSError(msg) - if not self.image: - msg = "cannot parse this image" - raise OSError(msg) - if self.data: - # incremental parsing not possible; reopen the file - # not that we have all data - with io.BytesIO(self.data) as fp: - try: - self.image = Image.open(fp) - finally: - self.image.load() - return self.image - - -# -------------------------------------------------------------------- - - -def _save(im, fp, tile, bufsize=0) -> None: - """Helper to save image based on tile list - - :param im: Image object. - :param fp: File object. - :param tile: Tile list. - :param bufsize: Optional buffer size - """ - - im.load() - if not hasattr(im, "encoderconfig"): - im.encoderconfig = () - tile.sort(key=_tilesort) - # FIXME: make MAXBLOCK a configuration parameter - # It would be great if we could have the encoder specify what it needs - # But, it would need at least the image size in most cases. RawEncode is - # a tricky case. - bufsize = max(MAXBLOCK, bufsize, im.size[0] * 4) # see RawEncode.c - try: - fh = fp.fileno() - fp.flush() - _encode_tile(im, fp, tile, bufsize, fh) - except (AttributeError, io.UnsupportedOperation) as exc: - _encode_tile(im, fp, tile, bufsize, None, exc) - if hasattr(fp, "flush"): - fp.flush() - - -def _encode_tile(im, fp, tile: list[_Tile], bufsize, fh, exc=None): - for encoder_name, extents, offset, args in tile: - if offset > 0: - fp.seek(offset) - encoder = Image._getencoder(im.mode, encoder_name, args, im.encoderconfig) - try: - encoder.setimage(im.im, extents) - if encoder.pushes_fd: - encoder.setfd(fp) - errcode = encoder.encode_to_pyfd()[1] - else: - if exc: - # compress to Python file-compatible object - while True: - errcode, data = encoder.encode(bufsize)[1:] - fp.write(data) - if errcode: - break - else: - # slight speedup: compress to real file object - errcode = encoder.encode_to_file(fh, bufsize) - if errcode < 0: - raise _get_oserror(errcode, encoder=True) from exc - finally: - encoder.cleanup() - - -def _safe_read(fp, size): - """ - Reads large blocks in a safe way. Unlike fp.read(n), this function - doesn't trust the user. If the requested size is larger than - SAFEBLOCK, the file is read block by block. - - :param fp: File handle. Must implement a read method. - :param size: Number of bytes to read. - :returns: A string containing size bytes of data. - - Raises an OSError if the file is truncated and the read cannot be completed - - """ - if size <= 0: - return b"" - if size <= SAFEBLOCK: - data = fp.read(size) - if len(data) < size: - msg = "Truncated File Read" - raise OSError(msg) - return data - data = [] - remaining_size = size - while remaining_size > 0: - block = fp.read(min(remaining_size, SAFEBLOCK)) - if not block: - break - data.append(block) - remaining_size -= len(block) - if sum(len(d) for d in data) < size: - msg = "Truncated File Read" - raise OSError(msg) - return b"".join(data) - - -class PyCodecState: - def __init__(self) -> None: - self.xsize = 0 - self.ysize = 0 - self.xoff = 0 - self.yoff = 0 - - def extents(self): - return self.xoff, self.yoff, self.xoff + self.xsize, self.yoff + self.ysize - - -class PyCodec: - fd: IO[bytes] | None - - def __init__(self, mode, *args): - self.im = None - self.state = PyCodecState() - self.fd = None - self.mode = mode - self.init(args) - - def init(self, args): - """ - Override to perform codec specific initialization - - :param args: Array of args items from the tile entry - :returns: None - """ - self.args = args - - def cleanup(self) -> None: - """ - Override to perform codec specific cleanup - - :returns: None - """ - pass - - def setfd(self, fd): - """ - Called from ImageFile to set the Python file-like object - - :param fd: A Python file-like object - :returns: None - """ - self.fd = fd - - def setimage(self, im, extents=None): - """ - Called from ImageFile to set the core output image for the codec - - :param im: A core image object - :param extents: a 4 tuple of (x0, y0, x1, y1) defining the rectangle - for this tile - :returns: None - """ - - # following c code - self.im = im - - if extents: - (x0, y0, x1, y1) = extents - else: - (x0, y0, x1, y1) = (0, 0, 0, 0) - - if x0 == 0 and x1 == 0: - self.state.xsize, self.state.ysize = self.im.size - else: - self.state.xoff = x0 - self.state.yoff = y0 - self.state.xsize = x1 - x0 - self.state.ysize = y1 - y0 - - if self.state.xsize <= 0 or self.state.ysize <= 0: - msg = "Size cannot be negative" - raise ValueError(msg) - - if ( - self.state.xsize + self.state.xoff > self.im.size[0] - or self.state.ysize + self.state.yoff > self.im.size[1] - ): - msg = "Tile cannot extend outside image" - raise ValueError(msg) - - -class PyDecoder(PyCodec): - """ - Python implementation of a format decoder. Override this class and - add the decoding logic in the :meth:`decode` method. - - See :ref:`Writing Your Own File Codec in Python` - """ - - _pulls_fd = False - - @property - def pulls_fd(self): - return self._pulls_fd - - def decode(self, buffer): - """ - Override to perform the decoding process. - - :param buffer: A bytes object with the data to be decoded. - :returns: A tuple of ``(bytes consumed, errcode)``. - If finished with decoding return -1 for the bytes consumed. - Err codes are from :data:`.ImageFile.ERRORS`. - """ - msg = "unavailable in base decoder" - raise NotImplementedError(msg) - - def set_as_raw(self, data: bytes, rawmode=None) -> None: - """ - Convenience method to set the internal image from a stream of raw data - - :param data: Bytes to be set - :param rawmode: The rawmode to be used for the decoder. - If not specified, it will default to the mode of the image - :returns: None - """ - - if not rawmode: - rawmode = self.mode - d = Image._getdecoder(self.mode, "raw", rawmode) - d.setimage(self.im, self.state.extents()) - s = d.decode(data) - - if s[0] >= 0: - msg = "not enough image data" - raise ValueError(msg) - if s[1] != 0: - msg = "cannot decode image data" - raise ValueError(msg) - - -class PyEncoder(PyCodec): - """ - Python implementation of a format encoder. Override this class and - add the decoding logic in the :meth:`encode` method. - - See :ref:`Writing Your Own File Codec in Python` - """ - - _pushes_fd = False - - @property - def pushes_fd(self): - return self._pushes_fd - - def encode(self, bufsize: int) -> tuple[int, int, bytes]: - """ - Override to perform the encoding process. - - :param bufsize: Buffer size. - :returns: A tuple of ``(bytes encoded, errcode, bytes)``. - If finished with encoding return 1 for the error code. - Err codes are from :data:`.ImageFile.ERRORS`. - """ - msg = "unavailable in base encoder" - raise NotImplementedError(msg) - - def encode_to_pyfd(self): - """ - If ``pushes_fd`` is ``True``, then this method will be used, - and ``encode()`` will only be called once. - - :returns: A tuple of ``(bytes consumed, errcode)``. - Err codes are from :data:`.ImageFile.ERRORS`. - """ - if not self.pushes_fd: - return 0, -8 # bad configuration - bytes_consumed, errcode, data = self.encode(0) - if data: - self.fd.write(data) - return bytes_consumed, errcode - - def encode_to_file(self, fh, bufsize): - """ - :param fh: File handle. - :param bufsize: Buffer size. - - :returns: If finished successfully, return 0. - Otherwise, return an error code. Err codes are from - :data:`.ImageFile.ERRORS`. - """ - errcode = 0 - while errcode == 0: - status, errcode, buf = self.encode(bufsize) - if status > 0: - fh.write(buf[status:]) - return errcode +# +# The Python Imaging Library. +# $Id$ +# +# base class for image file handlers +# +# history: +# 1995-09-09 fl Created +# 1996-03-11 fl Fixed load mechanism. +# 1996-04-15 fl Added pcx/xbm decoders. +# 1996-04-30 fl Added encoders. +# 1996-12-14 fl Added load helpers +# 1997-01-11 fl Use encode_to_file where possible +# 1997-08-27 fl Flush output in _save +# 1998-03-05 fl Use memory mapping for some modes +# 1999-02-04 fl Use memory mapping also for "I;16" and "I;16B" +# 1999-05-31 fl Added image parser +# 2000-10-12 fl Set readonly flag on memory-mapped images +# 2002-03-20 fl Use better messages for common decoder errors +# 2003-04-21 fl Fall back on mmap/map_buffer if map is not available +# 2003-10-30 fl Added StubImageFile class +# 2004-02-25 fl Made incremental parser more robust +# +# Copyright (c) 1997-2004 by Secret Labs AB +# Copyright (c) 1995-2004 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import abc +import io +import itertools +import struct +import sys +from typing import IO, Any, NamedTuple + +from . import Image +from ._deprecate import deprecate +from ._util import is_path + +MAXBLOCK = 65536 + +SAFEBLOCK = 1024 * 1024 + +LOAD_TRUNCATED_IMAGES = False +"""Whether or not to load truncated image files. User code may change this.""" + +ERRORS = { + -1: "image buffer overrun error", + -2: "decoding error", + -3: "unknown error", + -8: "bad configuration", + -9: "out of memory error", +} +""" +Dict of known error codes returned from :meth:`.PyDecoder.decode`, +:meth:`.PyEncoder.encode` :meth:`.PyEncoder.encode_to_pyfd` and +:meth:`.PyEncoder.encode_to_file`. +""" + + +# +# -------------------------------------------------------------------- +# Helpers + + +def _get_oserror(error, *, encoder): + try: + msg = Image.core.getcodecstatus(error) + except AttributeError: + msg = ERRORS.get(error) + if not msg: + msg = f"{'encoder' if encoder else 'decoder'} error {error}" + msg += f" when {'writing' if encoder else 'reading'} image file" + return OSError(msg) + + +def raise_oserror(error): + deprecate( + "raise_oserror", + 12, + action="It is only useful for translating error codes returned by a codec's " + "decode() method, which ImageFile already does automatically.", + ) + raise _get_oserror(error, encoder=False) + + +def _tilesort(t): + # sort on offset + return t[2] + + +class _Tile(NamedTuple): + codec_name: str + extents: tuple[int, int, int, int] + offset: int + args: tuple[Any, ...] | str | None + + +# +# -------------------------------------------------------------------- +# ImageFile base class + + +class ImageFile(Image.Image): + """Base class for image file format handlers.""" + + def __init__(self, fp=None, filename=None): + super().__init__() + + self._min_frame = 0 + + self.custom_mimetype = None + + self.tile = None + """ A list of tile descriptors, or ``None`` """ + + self.readonly = 1 # until we know better + + self.decoderconfig = () + self.decodermaxblock = MAXBLOCK + + if is_path(fp): + # filename + self.fp = open(fp, "rb") + self.filename = fp + self._exclusive_fp = True + else: + # stream + self.fp = fp + self.filename = filename + # can be overridden + self._exclusive_fp = None + + try: + try: + self._open() + except ( + IndexError, # end of data + TypeError, # end of data (ord) + KeyError, # unsupported mode + EOFError, # got header but not the first frame + struct.error, + ) as v: + raise SyntaxError(v) from v + + if not self.mode or self.size[0] <= 0 or self.size[1] <= 0: + msg = "not identified by this driver" + raise SyntaxError(msg) + except BaseException: + # close the file only if we have opened it this constructor + if self._exclusive_fp: + self.fp.close() + raise + + def get_format_mimetype(self): + if self.custom_mimetype: + return self.custom_mimetype + if self.format is not None: + return Image.MIME.get(self.format.upper()) + + def __setstate__(self, state): + self.tile = [] + super().__setstate__(state) + + def verify(self) -> None: + """Check file integrity""" + + # raise exception if something's wrong. must be called + # directly after open, and closes file when finished. + if self._exclusive_fp: + self.fp.close() + self.fp = None + + def load(self): + """Load image data based on tile list""" + + if self.tile is None: + msg = "cannot load this image" + raise OSError(msg) + + pixel = Image.Image.load(self) + if not self.tile: + return pixel + + self.map = None + use_mmap = self.filename and len(self.tile) == 1 + # As of pypy 2.1.0, memory mapping was failing here. + use_mmap = use_mmap and not hasattr(sys, "pypy_version_info") + + readonly = 0 + + # look for read/seek overrides + try: + read = self.load_read + # don't use mmap if there are custom read/seek functions + use_mmap = False + except AttributeError: + read = self.fp.read + + try: + seek = self.load_seek + use_mmap = False + except AttributeError: + seek = self.fp.seek + + if use_mmap: + # try memory mapping + decoder_name, extents, offset, args = self.tile[0] + if isinstance(args, str): + args = (args, 0, 1) + if ( + decoder_name == "raw" + and len(args) >= 3 + and args[0] == self.mode + and args[0] in Image._MAPMODES + ): + try: + # use mmap, if possible + import mmap + + with open(self.filename) as fp: + self.map = mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ) + if offset + self.size[1] * args[1] > self.map.size(): + msg = "buffer is not large enough" + raise OSError(msg) + self.im = Image.core.map_buffer( + self.map, self.size, decoder_name, offset, args + ) + readonly = 1 + # After trashing self.im, + # we might need to reload the palette data. + if self.palette: + self.palette.dirty = 1 + except (AttributeError, OSError, ImportError): + self.map = None + + self.load_prepare() + err_code = -3 # initialize to unknown error + if not self.map: + # sort tiles in file order + self.tile.sort(key=_tilesort) + + try: + # FIXME: This is a hack to handle TIFF's JpegTables tag. + prefix = self.tile_prefix + except AttributeError: + prefix = b"" + + # Remove consecutive duplicates that only differ by their offset + self.tile = [ + list(tiles)[-1] + for _, tiles in itertools.groupby( + self.tile, lambda tile: (tile[0], tile[1], tile[3]) + ) + ] + for decoder_name, extents, offset, args in self.tile: + seek(offset) + decoder = Image._getdecoder( + self.mode, decoder_name, args, self.decoderconfig + ) + try: + decoder.setimage(self.im, extents) + if decoder.pulls_fd: + decoder.setfd(self.fp) + err_code = decoder.decode(b"")[1] + else: + b = prefix + while True: + try: + s = read(self.decodermaxblock) + except (IndexError, struct.error) as e: + # truncated png/gif + if LOAD_TRUNCATED_IMAGES: + break + else: + msg = "image file is truncated" + raise OSError(msg) from e + + if not s: # truncated jpeg + if LOAD_TRUNCATED_IMAGES: + break + else: + msg = ( + "image file is truncated " + f"({len(b)} bytes not processed)" + ) + raise OSError(msg) + + b = b + s + n, err_code = decoder.decode(b) + if n < 0: + break + b = b[n:] + finally: + # Need to cleanup here to prevent leaks + decoder.cleanup() + + self.tile = [] + self.readonly = readonly + + self.load_end() + + if self._exclusive_fp and self._close_exclusive_fp_after_loading: + self.fp.close() + self.fp = None + + if not self.map and not LOAD_TRUNCATED_IMAGES and err_code < 0: + # still raised if decoder fails to return anything + raise _get_oserror(err_code, encoder=False) + + return Image.Image.load(self) + + def load_prepare(self) -> None: + # create image memory if necessary + if not self.im or self.im.mode != self.mode or self.im.size != self.size: + self.im = Image.core.new(self.mode, self.size) + # create palette (optional) + if self.mode == "P": + Image.Image.load(self) + + def load_end(self) -> None: + # may be overridden + pass + + # may be defined for contained formats + # def load_seek(self, pos: int) -> None: + # pass + + # may be defined for blocked formats (e.g. PNG) + # def load_read(self, read_bytes: int) -> bytes: + # pass + + def _seek_check(self, frame): + if ( + frame < self._min_frame + # Only check upper limit on frames if additional seek operations + # are not required to do so + or ( + not (hasattr(self, "_n_frames") and self._n_frames is None) + and frame >= self.n_frames + self._min_frame + ) + ): + msg = "attempt to seek outside sequence" + raise EOFError(msg) + + return self.tell() != frame + + +class StubHandler: + def open(self, im: StubImageFile) -> None: + pass + + @abc.abstractmethod + def load(self, im: StubImageFile) -> Image.Image: + pass + + +class StubImageFile(ImageFile): + """ + Base class for stub image loaders. + + A stub loader is an image loader that can identify files of a + certain format, but relies on external code to load the file. + """ + + def _open(self): + msg = "StubImageFile subclass must implement _open" + raise NotImplementedError(msg) + + def load(self): + loader = self._load() + if loader is None: + msg = f"cannot find loader for this {self.format} file" + raise OSError(msg) + image = loader.load(self) + assert image is not None + # become the other object (!) + self.__class__ = image.__class__ + self.__dict__ = image.__dict__ + return image.load() + + def _load(self): + """(Hook) Find actual image loader.""" + msg = "StubImageFile subclass must implement _load" + raise NotImplementedError(msg) + + +class Parser: + """ + Incremental image parser. This class implements the standard + feed/close consumer interface. + """ + + incremental = None + image: Image.Image | None = None + data = None + decoder = None + offset = 0 + finished = 0 + + def reset(self) -> None: + """ + (Consumer) Reset the parser. Note that you can only call this + method immediately after you've created a parser; parser + instances cannot be reused. + """ + assert self.data is None, "cannot reuse parsers" + + def feed(self, data): + """ + (Consumer) Feed data to the parser. + + :param data: A string buffer. + :exception OSError: If the parser failed to parse the image file. + """ + # collect data + + if self.finished: + return + + if self.data is None: + self.data = data + else: + self.data = self.data + data + + # parse what we have + if self.decoder: + if self.offset > 0: + # skip header + skip = min(len(self.data), self.offset) + self.data = self.data[skip:] + self.offset = self.offset - skip + if self.offset > 0 or not self.data: + return + + n, e = self.decoder.decode(self.data) + + if n < 0: + # end of stream + self.data = None + self.finished = 1 + if e < 0: + # decoding error + self.image = None + raise _get_oserror(e, encoder=False) + else: + # end of image + return + self.data = self.data[n:] + + elif self.image: + # if we end up here with no decoder, this file cannot + # be incrementally parsed. wait until we've gotten all + # available data + pass + + else: + # attempt to open this file + try: + with io.BytesIO(self.data) as fp: + im = Image.open(fp) + except OSError: + pass # not enough data + else: + flag = hasattr(im, "load_seek") or hasattr(im, "load_read") + if flag or len(im.tile) != 1: + # custom load code, or multiple tiles + self.decode = None + else: + # initialize decoder + im.load_prepare() + d, e, o, a = im.tile[0] + im.tile = [] + self.decoder = Image._getdecoder(im.mode, d, a, im.decoderconfig) + self.decoder.setimage(im.im, e) + + # calculate decoder offset + self.offset = o + if self.offset <= len(self.data): + self.data = self.data[self.offset :] + self.offset = 0 + + self.image = im + + def __enter__(self): + return self + + def __exit__(self, *args: object) -> None: + self.close() + + def close(self): + """ + (Consumer) Close the stream. + + :returns: An image object. + :exception OSError: If the parser failed to parse the image file either + because it cannot be identified or cannot be + decoded. + """ + # finish decoding + if self.decoder: + # get rid of what's left in the buffers + self.feed(b"") + self.data = self.decoder = None + if not self.finished: + msg = "image was incomplete" + raise OSError(msg) + if not self.image: + msg = "cannot parse this image" + raise OSError(msg) + if self.data: + # incremental parsing not possible; reopen the file + # not that we have all data + with io.BytesIO(self.data) as fp: + try: + self.image = Image.open(fp) + finally: + self.image.load() + return self.image + + +# -------------------------------------------------------------------- + + +def _save(im, fp, tile, bufsize=0) -> None: + """Helper to save image based on tile list + + :param im: Image object. + :param fp: File object. + :param tile: Tile list. + :param bufsize: Optional buffer size + """ + + im.load() + if not hasattr(im, "encoderconfig"): + im.encoderconfig = () + tile.sort(key=_tilesort) + # FIXME: make MAXBLOCK a configuration parameter + # It would be great if we could have the encoder specify what it needs + # But, it would need at least the image size in most cases. RawEncode is + # a tricky case. + bufsize = max(MAXBLOCK, bufsize, im.size[0] * 4) # see RawEncode.c + try: + fh = fp.fileno() + fp.flush() + _encode_tile(im, fp, tile, bufsize, fh) + except (AttributeError, io.UnsupportedOperation) as exc: + _encode_tile(im, fp, tile, bufsize, None, exc) + if hasattr(fp, "flush"): + fp.flush() + + +def _encode_tile(im, fp, tile: list[_Tile], bufsize, fh, exc=None): + for encoder_name, extents, offset, args in tile: + if offset > 0: + fp.seek(offset) + encoder = Image._getencoder(im.mode, encoder_name, args, im.encoderconfig) + try: + encoder.setimage(im.im, extents) + if encoder.pushes_fd: + encoder.setfd(fp) + errcode = encoder.encode_to_pyfd()[1] + else: + if exc: + # compress to Python file-compatible object + while True: + errcode, data = encoder.encode(bufsize)[1:] + fp.write(data) + if errcode: + break + else: + # slight speedup: compress to real file object + errcode = encoder.encode_to_file(fh, bufsize) + if errcode < 0: + raise _get_oserror(errcode, encoder=True) from exc + finally: + encoder.cleanup() + + +def _safe_read(fp, size): + """ + Reads large blocks in a safe way. Unlike fp.read(n), this function + doesn't trust the user. If the requested size is larger than + SAFEBLOCK, the file is read block by block. + + :param fp: File handle. Must implement a read method. + :param size: Number of bytes to read. + :returns: A string containing size bytes of data. + + Raises an OSError if the file is truncated and the read cannot be completed + + """ + if size <= 0: + return b"" + if size <= SAFEBLOCK: + data = fp.read(size) + if len(data) < size: + msg = "Truncated File Read" + raise OSError(msg) + return data + data = [] + remaining_size = size + while remaining_size > 0: + block = fp.read(min(remaining_size, SAFEBLOCK)) + if not block: + break + data.append(block) + remaining_size -= len(block) + if sum(len(d) for d in data) < size: + msg = "Truncated File Read" + raise OSError(msg) + return b"".join(data) + + +class PyCodecState: + def __init__(self) -> None: + self.xsize = 0 + self.ysize = 0 + self.xoff = 0 + self.yoff = 0 + + def extents(self): + return self.xoff, self.yoff, self.xoff + self.xsize, self.yoff + self.ysize + + +class PyCodec: + fd: IO[bytes] | None + + def __init__(self, mode, *args): + self.im = None + self.state = PyCodecState() + self.fd = None + self.mode = mode + self.init(args) + + def init(self, args): + """ + Override to perform codec specific initialization + + :param args: Array of args items from the tile entry + :returns: None + """ + self.args = args + + def cleanup(self) -> None: + """ + Override to perform codec specific cleanup + + :returns: None + """ + pass + + def setfd(self, fd): + """ + Called from ImageFile to set the Python file-like object + + :param fd: A Python file-like object + :returns: None + """ + self.fd = fd + + def setimage(self, im, extents=None): + """ + Called from ImageFile to set the core output image for the codec + + :param im: A core image object + :param extents: a 4 tuple of (x0, y0, x1, y1) defining the rectangle + for this tile + :returns: None + """ + + # following c code + self.im = im + + if extents: + (x0, y0, x1, y1) = extents + else: + (x0, y0, x1, y1) = (0, 0, 0, 0) + + if x0 == 0 and x1 == 0: + self.state.xsize, self.state.ysize = self.im.size + else: + self.state.xoff = x0 + self.state.yoff = y0 + self.state.xsize = x1 - x0 + self.state.ysize = y1 - y0 + + if self.state.xsize <= 0 or self.state.ysize <= 0: + msg = "Size cannot be negative" + raise ValueError(msg) + + if ( + self.state.xsize + self.state.xoff > self.im.size[0] + or self.state.ysize + self.state.yoff > self.im.size[1] + ): + msg = "Tile cannot extend outside image" + raise ValueError(msg) + + +class PyDecoder(PyCodec): + """ + Python implementation of a format decoder. Override this class and + add the decoding logic in the :meth:`decode` method. + + See :ref:`Writing Your Own File Codec in Python` + """ + + _pulls_fd = False + + @property + def pulls_fd(self): + return self._pulls_fd + + def decode(self, buffer): + """ + Override to perform the decoding process. + + :param buffer: A bytes object with the data to be decoded. + :returns: A tuple of ``(bytes consumed, errcode)``. + If finished with decoding return -1 for the bytes consumed. + Err codes are from :data:`.ImageFile.ERRORS`. + """ + msg = "unavailable in base decoder" + raise NotImplementedError(msg) + + def set_as_raw(self, data: bytes, rawmode=None) -> None: + """ + Convenience method to set the internal image from a stream of raw data + + :param data: Bytes to be set + :param rawmode: The rawmode to be used for the decoder. + If not specified, it will default to the mode of the image + :returns: None + """ + + if not rawmode: + rawmode = self.mode + d = Image._getdecoder(self.mode, "raw", rawmode) + d.setimage(self.im, self.state.extents()) + s = d.decode(data) + + if s[0] >= 0: + msg = "not enough image data" + raise ValueError(msg) + if s[1] != 0: + msg = "cannot decode image data" + raise ValueError(msg) + + +class PyEncoder(PyCodec): + """ + Python implementation of a format encoder. Override this class and + add the decoding logic in the :meth:`encode` method. + + See :ref:`Writing Your Own File Codec in Python` + """ + + _pushes_fd = False + + @property + def pushes_fd(self): + return self._pushes_fd + + def encode(self, bufsize: int) -> tuple[int, int, bytes]: + """ + Override to perform the encoding process. + + :param bufsize: Buffer size. + :returns: A tuple of ``(bytes encoded, errcode, bytes)``. + If finished with encoding return 1 for the error code. + Err codes are from :data:`.ImageFile.ERRORS`. + """ + msg = "unavailable in base encoder" + raise NotImplementedError(msg) + + def encode_to_pyfd(self): + """ + If ``pushes_fd`` is ``True``, then this method will be used, + and ``encode()`` will only be called once. + + :returns: A tuple of ``(bytes consumed, errcode)``. + Err codes are from :data:`.ImageFile.ERRORS`. + """ + if not self.pushes_fd: + return 0, -8 # bad configuration + bytes_consumed, errcode, data = self.encode(0) + if data: + self.fd.write(data) + return bytes_consumed, errcode + + def encode_to_file(self, fh, bufsize): + """ + :param fh: File handle. + :param bufsize: Buffer size. + + :returns: If finished successfully, return 0. + Otherwise, return an error code. Err codes are from + :data:`.ImageFile.ERRORS`. + """ + errcode = 0 + while errcode == 0: + status, errcode, buf = self.encode(bufsize) + if status > 0: + fh.write(buf[status:]) + return errcode diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index 02288e135..275735658 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -1,578 +1,578 @@ -# -# The Python Imaging Library. -# $Id$ -# -# standard filters -# -# History: -# 1995-11-27 fl Created -# 2002-06-08 fl Added rank and mode filters -# 2003-09-15 fl Fixed rank calculation in rank filter; added expand call -# -# Copyright (c) 1997-2003 by Secret Labs AB. -# Copyright (c) 1995-2002 by Fredrik Lundh. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import abc -import functools -from types import ModuleType -from typing import Any, Sequence - - -class Filter: - @abc.abstractmethod - def filter(self, image): - pass - - -class MultibandFilter(Filter): - pass - - -class BuiltinFilter(MultibandFilter): - def filter(self, image): - if image.mode == "P": - msg = "cannot filter palette images" - raise ValueError(msg) - return image.filter(*self.filterargs) - - -class Kernel(BuiltinFilter): - """ - Create a convolution kernel. This only supports 3x3 and 5x5 integer and floating - point kernels. - - Kernels can only be applied to "L" and "RGB" images. - - :param size: Kernel size, given as (width, height). This must be (3,3) or (5,5). - :param kernel: A sequence containing kernel weights. The kernel will be flipped - vertically before being applied to the image. - :param scale: Scale factor. If given, the result for each pixel is divided by this - value. The default is the sum of the kernel weights. - :param offset: Offset. If given, this value is added to the result, after it has - been divided by the scale factor. - """ - - name = "Kernel" - - def __init__( - self, - size: tuple[int, int], - kernel: Sequence[float], - scale: float | None = None, - offset: float = 0, - ) -> None: - if scale is None: - # default scale is sum of kernel - scale = functools.reduce(lambda a, b: a + b, kernel) - if size[0] * size[1] != len(kernel): - msg = "not enough coefficients in kernel" - raise ValueError(msg) - self.filterargs = size, scale, offset, kernel - - -class RankFilter(Filter): - """ - Create a rank filter. The rank filter sorts all pixels in - a window of the given size, and returns the ``rank``'th value. - - :param size: The kernel size, in pixels. - :param rank: What pixel value to pick. Use 0 for a min filter, - ``size * size / 2`` for a median filter, ``size * size - 1`` - for a max filter, etc. - """ - - name = "Rank" - - def __init__(self, size: int, rank: int) -> None: - self.size = size - self.rank = rank - - def filter(self, image): - if image.mode == "P": - msg = "cannot filter palette images" - raise ValueError(msg) - image = image.expand(self.size // 2, self.size // 2) - return image.rankfilter(self.size, self.rank) - - -class MedianFilter(RankFilter): - """ - Create a median filter. Picks the median pixel value in a window with the - given size. - - :param size: The kernel size, in pixels. - """ - - name = "Median" - - def __init__(self, size: int = 3) -> None: - self.size = size - self.rank = size * size // 2 - - -class MinFilter(RankFilter): - """ - Create a min filter. Picks the lowest pixel value in a window with the - given size. - - :param size: The kernel size, in pixels. - """ - - name = "Min" - - def __init__(self, size: int = 3) -> None: - self.size = size - self.rank = 0 - - -class MaxFilter(RankFilter): - """ - Create a max filter. Picks the largest pixel value in a window with the - given size. - - :param size: The kernel size, in pixels. - """ - - name = "Max" - - def __init__(self, size: int = 3) -> None: - self.size = size - self.rank = size * size - 1 - - -class ModeFilter(Filter): - """ - Create a mode filter. Picks the most frequent pixel value in a box with the - given size. Pixel values that occur only once or twice are ignored; if no - pixel value occurs more than twice, the original pixel value is preserved. - - :param size: The kernel size, in pixels. - """ - - name = "Mode" - - def __init__(self, size: int = 3) -> None: - self.size = size - - def filter(self, image): - return image.modefilter(self.size) - - -class GaussianBlur(MultibandFilter): - """Blurs the image with a sequence of extended box filters, which - approximates a Gaussian kernel. For details on accuracy see - - - :param radius: Standard deviation of the Gaussian kernel. Either a sequence of two - numbers for x and y, or a single number for both. - """ - - name = "GaussianBlur" - - def __init__(self, radius: float | Sequence[float] = 2) -> None: - self.radius = radius - - def filter(self, image): - xy = self.radius - if not isinstance(xy, (tuple, list)): - xy = (xy, xy) - if xy == (0, 0): - return image.copy() - return image.gaussian_blur(xy) - - -class BoxBlur(MultibandFilter): - """Blurs the image by setting each pixel to the average value of the pixels - in a square box extending radius pixels in each direction. - Supports float radius of arbitrary size. Uses an optimized implementation - which runs in linear time relative to the size of the image - for any radius value. - - :param radius: Size of the box in a direction. Either a sequence of two numbers for - x and y, or a single number for both. - - Radius 0 does not blur, returns an identical image. - Radius 1 takes 1 pixel in each direction, i.e. 9 pixels in total. - """ - - name = "BoxBlur" - - def __init__(self, radius: float | Sequence[float]) -> None: - xy = radius if isinstance(radius, (tuple, list)) else (radius, radius) - if xy[0] < 0 or xy[1] < 0: - msg = "radius must be >= 0" - raise ValueError(msg) - self.radius = radius - - def filter(self, image): - xy = self.radius - if not isinstance(xy, (tuple, list)): - xy = (xy, xy) - if xy == (0, 0): - return image.copy() - return image.box_blur(xy) - - -class UnsharpMask(MultibandFilter): - """Unsharp mask filter. - - See Wikipedia's entry on `digital unsharp masking`_ for an explanation of - the parameters. - - :param radius: Blur Radius - :param percent: Unsharp strength, in percent - :param threshold: Threshold controls the minimum brightness change that - will be sharpened - - .. _digital unsharp masking: https://en.wikipedia.org/wiki/Unsharp_masking#Digital_unsharp_masking - - """ - - name = "UnsharpMask" - - def __init__( - self, radius: float = 2, percent: int = 150, threshold: int = 3 - ) -> None: - self.radius = radius - self.percent = percent - self.threshold = threshold - - def filter(self, image): - return image.unsharp_mask(self.radius, self.percent, self.threshold) - - -class BLUR(BuiltinFilter): - name = "Blur" - # fmt: off - filterargs = (5, 5), 16, 0, ( - 1, 1, 1, 1, 1, - 1, 0, 0, 0, 1, - 1, 0, 0, 0, 1, - 1, 0, 0, 0, 1, - 1, 1, 1, 1, 1, - ) - # fmt: on - - -class CONTOUR(BuiltinFilter): - name = "Contour" - # fmt: off - filterargs = (3, 3), 1, 255, ( - -1, -1, -1, - -1, 8, -1, - -1, -1, -1, - ) - # fmt: on - - -class DETAIL(BuiltinFilter): - name = "Detail" - # fmt: off - filterargs = (3, 3), 6, 0, ( - 0, -1, 0, - -1, 10, -1, - 0, -1, 0, - ) - # fmt: on - - -class EDGE_ENHANCE(BuiltinFilter): - name = "Edge-enhance" - # fmt: off - filterargs = (3, 3), 2, 0, ( - -1, -1, -1, - -1, 10, -1, - -1, -1, -1, - ) - # fmt: on - - -class EDGE_ENHANCE_MORE(BuiltinFilter): - name = "Edge-enhance More" - # fmt: off - filterargs = (3, 3), 1, 0, ( - -1, -1, -1, - -1, 9, -1, - -1, -1, -1, - ) - # fmt: on - - -class EMBOSS(BuiltinFilter): - name = "Emboss" - # fmt: off - filterargs = (3, 3), 1, 128, ( - -1, 0, 0, - 0, 1, 0, - 0, 0, 0, - ) - # fmt: on - - -class FIND_EDGES(BuiltinFilter): - name = "Find Edges" - # fmt: off - filterargs = (3, 3), 1, 0, ( - -1, -1, -1, - -1, 8, -1, - -1, -1, -1, - ) - # fmt: on - - -class SHARPEN(BuiltinFilter): - name = "Sharpen" - # fmt: off - filterargs = (3, 3), 16, 0, ( - -2, -2, -2, - -2, 32, -2, - -2, -2, -2, - ) - # fmt: on - - -class SMOOTH(BuiltinFilter): - name = "Smooth" - # fmt: off - filterargs = (3, 3), 13, 0, ( - 1, 1, 1, - 1, 5, 1, - 1, 1, 1, - ) - # fmt: on - - -class SMOOTH_MORE(BuiltinFilter): - name = "Smooth More" - # fmt: off - filterargs = (5, 5), 100, 0, ( - 1, 1, 1, 1, 1, - 1, 5, 5, 5, 1, - 1, 5, 44, 5, 1, - 1, 5, 5, 5, 1, - 1, 1, 1, 1, 1, - ) - # fmt: on - - -class Color3DLUT(MultibandFilter): - """Three-dimensional color lookup table. - - Transforms 3-channel pixels using the values of the channels as coordinates - in the 3D lookup table and interpolating the nearest elements. - - This method allows you to apply almost any color transformation - in constant time by using pre-calculated decimated tables. - - .. versionadded:: 5.2.0 - - :param size: Size of the table. One int or tuple of (int, int, int). - Minimal size in any dimension is 2, maximum is 65. - :param table: Flat lookup table. A list of ``channels * size**3`` - float elements or a list of ``size**3`` channels-sized - tuples with floats. Channels are changed first, - then first dimension, then second, then third. - Value 0.0 corresponds lowest value of output, 1.0 highest. - :param channels: Number of channels in the table. Could be 3 or 4. - Default is 3. - :param target_mode: A mode for the result image. Should have not less - than ``channels`` channels. Default is ``None``, - which means that mode wouldn't be changed. - """ - - name = "Color 3D LUT" - - def __init__( - self, size, table, channels: int = 3, target_mode: str | None = None, **kwargs - ): - if channels not in (3, 4): - msg = "Only 3 or 4 output channels are supported" - raise ValueError(msg) - self.size = size = self._check_size(size) - self.channels = channels - self.mode = target_mode - - # Hidden flag `_copy_table=False` could be used to avoid extra copying - # of the table if the table is specially made for the constructor. - copy_table = kwargs.get("_copy_table", True) - items = size[0] * size[1] * size[2] - wrong_size = False - - numpy: ModuleType | None = None - if hasattr(table, "shape"): - try: - import numpy - except ImportError: - pass - - if numpy and isinstance(table, numpy.ndarray): - if copy_table: - table = table.copy() - - if table.shape in [ - (items * channels,), - (items, channels), - (size[2], size[1], size[0], channels), - ]: - table = table.reshape(items * channels) - else: - wrong_size = True - - else: - if copy_table: - table = list(table) - - # Convert to a flat list - if table and isinstance(table[0], (list, tuple)): - table, raw_table = [], table - for pixel in raw_table: - if len(pixel) != channels: - msg = ( - "The elements of the table should " - f"have a length of {channels}." - ) - raise ValueError(msg) - table.extend(pixel) - - if wrong_size or len(table) != items * channels: - msg = ( - "The table should have either channels * size**3 float items " - "or size**3 items of channels-sized tuples with floats. " - f"Table should be: {channels}x{size[0]}x{size[1]}x{size[2]}. " - f"Actual length: {len(table)}" - ) - raise ValueError(msg) - self.table = table - - @staticmethod - def _check_size(size: Any) -> list[int]: - try: - _, _, _ = size - except ValueError as e: - msg = "Size should be either an integer or a tuple of three integers." - raise ValueError(msg) from e - except TypeError: - size = (size, size, size) - size = [int(x) for x in size] - for size_1d in size: - if not 2 <= size_1d <= 65: - msg = "Size should be in [2, 65] range." - raise ValueError(msg) - return size - - @classmethod - def generate(cls, size, callback, channels=3, target_mode=None): - """Generates new LUT using provided callback. - - :param size: Size of the table. Passed to the constructor. - :param callback: Function with three parameters which correspond - three color channels. Will be called ``size**3`` - times with values from 0.0 to 1.0 and should return - a tuple with ``channels`` elements. - :param channels: The number of channels which should return callback. - :param target_mode: Passed to the constructor of the resulting - lookup table. - """ - size_1d, size_2d, size_3d = cls._check_size(size) - if channels not in (3, 4): - msg = "Only 3 or 4 output channels are supported" - raise ValueError(msg) - - table = [0] * (size_1d * size_2d * size_3d * channels) - idx_out = 0 - for b in range(size_3d): - for g in range(size_2d): - for r in range(size_1d): - table[idx_out : idx_out + channels] = callback( - r / (size_1d - 1), g / (size_2d - 1), b / (size_3d - 1) - ) - idx_out += channels - - return cls( - (size_1d, size_2d, size_3d), - table, - channels=channels, - target_mode=target_mode, - _copy_table=False, - ) - - def transform(self, callback, with_normals=False, channels=None, target_mode=None): - """Transforms the table values using provided callback and returns - a new LUT with altered values. - - :param callback: A function which takes old lookup table values - and returns a new set of values. The number - of arguments which function should take is - ``self.channels`` or ``3 + self.channels`` - if ``with_normals`` flag is set. - Should return a tuple of ``self.channels`` or - ``channels`` elements if it is set. - :param with_normals: If true, ``callback`` will be called with - coordinates in the color cube as the first - three arguments. Otherwise, ``callback`` - will be called only with actual color values. - :param channels: The number of channels in the resulting lookup table. - :param target_mode: Passed to the constructor of the resulting - lookup table. - """ - if channels not in (None, 3, 4): - msg = "Only 3 or 4 output channels are supported" - raise ValueError(msg) - ch_in = self.channels - ch_out = channels or ch_in - size_1d, size_2d, size_3d = self.size - - table = [0] * (size_1d * size_2d * size_3d * ch_out) - idx_in = 0 - idx_out = 0 - for b in range(size_3d): - for g in range(size_2d): - for r in range(size_1d): - values = self.table[idx_in : idx_in + ch_in] - if with_normals: - values = callback( - r / (size_1d - 1), - g / (size_2d - 1), - b / (size_3d - 1), - *values, - ) - else: - values = callback(*values) - table[idx_out : idx_out + ch_out] = values - idx_in += ch_in - idx_out += ch_out - - return type(self)( - self.size, - table, - channels=ch_out, - target_mode=target_mode or self.mode, - _copy_table=False, - ) - - def __repr__(self) -> str: - r = [ - f"{self.__class__.__name__} from {self.table.__class__.__name__}", - "size={:d}x{:d}x{:d}".format(*self.size), - f"channels={self.channels:d}", - ] - if self.mode: - r.append(f"target_mode={self.mode}") - return "<{}>".format(" ".join(r)) - - def filter(self, image): - from . import Image - - return image.color_lut_3d( - self.mode or image.mode, - Image.Resampling.BILINEAR, - self.channels, - self.size[0], - self.size[1], - self.size[2], - self.table, - ) +# +# The Python Imaging Library. +# $Id$ +# +# standard filters +# +# History: +# 1995-11-27 fl Created +# 2002-06-08 fl Added rank and mode filters +# 2003-09-15 fl Fixed rank calculation in rank filter; added expand call +# +# Copyright (c) 1997-2003 by Secret Labs AB. +# Copyright (c) 1995-2002 by Fredrik Lundh. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import abc +import functools +from types import ModuleType +from typing import Any, Sequence + + +class Filter: + @abc.abstractmethod + def filter(self, image): + pass + + +class MultibandFilter(Filter): + pass + + +class BuiltinFilter(MultibandFilter): + def filter(self, image): + if image.mode == "P": + msg = "cannot filter palette images" + raise ValueError(msg) + return image.filter(*self.filterargs) + + +class Kernel(BuiltinFilter): + """ + Create a convolution kernel. This only supports 3x3 and 5x5 integer and floating + point kernels. + + Kernels can only be applied to "L" and "RGB" images. + + :param size: Kernel size, given as (width, height). This must be (3,3) or (5,5). + :param kernel: A sequence containing kernel weights. The kernel will be flipped + vertically before being applied to the image. + :param scale: Scale factor. If given, the result for each pixel is divided by this + value. The default is the sum of the kernel weights. + :param offset: Offset. If given, this value is added to the result, after it has + been divided by the scale factor. + """ + + name = "Kernel" + + def __init__( + self, + size: tuple[int, int], + kernel: Sequence[float], + scale: float | None = None, + offset: float = 0, + ) -> None: + if scale is None: + # default scale is sum of kernel + scale = functools.reduce(lambda a, b: a + b, kernel) + if size[0] * size[1] != len(kernel): + msg = "not enough coefficients in kernel" + raise ValueError(msg) + self.filterargs = size, scale, offset, kernel + + +class RankFilter(Filter): + """ + Create a rank filter. The rank filter sorts all pixels in + a window of the given size, and returns the ``rank``'th value. + + :param size: The kernel size, in pixels. + :param rank: What pixel value to pick. Use 0 for a min filter, + ``size * size / 2`` for a median filter, ``size * size - 1`` + for a max filter, etc. + """ + + name = "Rank" + + def __init__(self, size: int, rank: int) -> None: + self.size = size + self.rank = rank + + def filter(self, image): + if image.mode == "P": + msg = "cannot filter palette images" + raise ValueError(msg) + image = image.expand(self.size // 2, self.size // 2) + return image.rankfilter(self.size, self.rank) + + +class MedianFilter(RankFilter): + """ + Create a median filter. Picks the median pixel value in a window with the + given size. + + :param size: The kernel size, in pixels. + """ + + name = "Median" + + def __init__(self, size: int = 3) -> None: + self.size = size + self.rank = size * size // 2 + + +class MinFilter(RankFilter): + """ + Create a min filter. Picks the lowest pixel value in a window with the + given size. + + :param size: The kernel size, in pixels. + """ + + name = "Min" + + def __init__(self, size: int = 3) -> None: + self.size = size + self.rank = 0 + + +class MaxFilter(RankFilter): + """ + Create a max filter. Picks the largest pixel value in a window with the + given size. + + :param size: The kernel size, in pixels. + """ + + name = "Max" + + def __init__(self, size: int = 3) -> None: + self.size = size + self.rank = size * size - 1 + + +class ModeFilter(Filter): + """ + Create a mode filter. Picks the most frequent pixel value in a box with the + given size. Pixel values that occur only once or twice are ignored; if no + pixel value occurs more than twice, the original pixel value is preserved. + + :param size: The kernel size, in pixels. + """ + + name = "Mode" + + def __init__(self, size: int = 3) -> None: + self.size = size + + def filter(self, image): + return image.modefilter(self.size) + + +class GaussianBlur(MultibandFilter): + """Blurs the image with a sequence of extended box filters, which + approximates a Gaussian kernel. For details on accuracy see + + + :param radius: Standard deviation of the Gaussian kernel. Either a sequence of two + numbers for x and y, or a single number for both. + """ + + name = "GaussianBlur" + + def __init__(self, radius: float | Sequence[float] = 2) -> None: + self.radius = radius + + def filter(self, image): + xy = self.radius + if not isinstance(xy, (tuple, list)): + xy = (xy, xy) + if xy == (0, 0): + return image.copy() + return image.gaussian_blur(xy) + + +class BoxBlur(MultibandFilter): + """Blurs the image by setting each pixel to the average value of the pixels + in a square box extending radius pixels in each direction. + Supports float radius of arbitrary size. Uses an optimized implementation + which runs in linear time relative to the size of the image + for any radius value. + + :param radius: Size of the box in a direction. Either a sequence of two numbers for + x and y, or a single number for both. + + Radius 0 does not blur, returns an identical image. + Radius 1 takes 1 pixel in each direction, i.e. 9 pixels in total. + """ + + name = "BoxBlur" + + def __init__(self, radius: float | Sequence[float]) -> None: + xy = radius if isinstance(radius, (tuple, list)) else (radius, radius) + if xy[0] < 0 or xy[1] < 0: + msg = "radius must be >= 0" + raise ValueError(msg) + self.radius = radius + + def filter(self, image): + xy = self.radius + if not isinstance(xy, (tuple, list)): + xy = (xy, xy) + if xy == (0, 0): + return image.copy() + return image.box_blur(xy) + + +class UnsharpMask(MultibandFilter): + """Unsharp mask filter. + + See Wikipedia's entry on `digital unsharp masking`_ for an explanation of + the parameters. + + :param radius: Blur Radius + :param percent: Unsharp strength, in percent + :param threshold: Threshold controls the minimum brightness change that + will be sharpened + + .. _digital unsharp masking: https://en.wikipedia.org/wiki/Unsharp_masking#Digital_unsharp_masking + + """ + + name = "UnsharpMask" + + def __init__( + self, radius: float = 2, percent: int = 150, threshold: int = 3 + ) -> None: + self.radius = radius + self.percent = percent + self.threshold = threshold + + def filter(self, image): + return image.unsharp_mask(self.radius, self.percent, self.threshold) + + +class BLUR(BuiltinFilter): + name = "Blur" + # fmt: off + filterargs = (5, 5), 16, 0, ( + 1, 1, 1, 1, 1, + 1, 0, 0, 0, 1, + 1, 0, 0, 0, 1, + 1, 0, 0, 0, 1, + 1, 1, 1, 1, 1, + ) + # fmt: on + + +class CONTOUR(BuiltinFilter): + name = "Contour" + # fmt: off + filterargs = (3, 3), 1, 255, ( + -1, -1, -1, + -1, 8, -1, + -1, -1, -1, + ) + # fmt: on + + +class DETAIL(BuiltinFilter): + name = "Detail" + # fmt: off + filterargs = (3, 3), 6, 0, ( + 0, -1, 0, + -1, 10, -1, + 0, -1, 0, + ) + # fmt: on + + +class EDGE_ENHANCE(BuiltinFilter): + name = "Edge-enhance" + # fmt: off + filterargs = (3, 3), 2, 0, ( + -1, -1, -1, + -1, 10, -1, + -1, -1, -1, + ) + # fmt: on + + +class EDGE_ENHANCE_MORE(BuiltinFilter): + name = "Edge-enhance More" + # fmt: off + filterargs = (3, 3), 1, 0, ( + -1, -1, -1, + -1, 9, -1, + -1, -1, -1, + ) + # fmt: on + + +class EMBOSS(BuiltinFilter): + name = "Emboss" + # fmt: off + filterargs = (3, 3), 1, 128, ( + -1, 0, 0, + 0, 1, 0, + 0, 0, 0, + ) + # fmt: on + + +class FIND_EDGES(BuiltinFilter): + name = "Find Edges" + # fmt: off + filterargs = (3, 3), 1, 0, ( + -1, -1, -1, + -1, 8, -1, + -1, -1, -1, + ) + # fmt: on + + +class SHARPEN(BuiltinFilter): + name = "Sharpen" + # fmt: off + filterargs = (3, 3), 16, 0, ( + -2, -2, -2, + -2, 32, -2, + -2, -2, -2, + ) + # fmt: on + + +class SMOOTH(BuiltinFilter): + name = "Smooth" + # fmt: off + filterargs = (3, 3), 13, 0, ( + 1, 1, 1, + 1, 5, 1, + 1, 1, 1, + ) + # fmt: on + + +class SMOOTH_MORE(BuiltinFilter): + name = "Smooth More" + # fmt: off + filterargs = (5, 5), 100, 0, ( + 1, 1, 1, 1, 1, + 1, 5, 5, 5, 1, + 1, 5, 44, 5, 1, + 1, 5, 5, 5, 1, + 1, 1, 1, 1, 1, + ) + # fmt: on + + +class Color3DLUT(MultibandFilter): + """Three-dimensional color lookup table. + + Transforms 3-channel pixels using the values of the channels as coordinates + in the 3D lookup table and interpolating the nearest elements. + + This method allows you to apply almost any color transformation + in constant time by using pre-calculated decimated tables. + + .. versionadded:: 5.2.0 + + :param size: Size of the table. One int or tuple of (int, int, int). + Minimal size in any dimension is 2, maximum is 65. + :param table: Flat lookup table. A list of ``channels * size**3`` + float elements or a list of ``size**3`` channels-sized + tuples with floats. Channels are changed first, + then first dimension, then second, then third. + Value 0.0 corresponds lowest value of output, 1.0 highest. + :param channels: Number of channels in the table. Could be 3 or 4. + Default is 3. + :param target_mode: A mode for the result image. Should have not less + than ``channels`` channels. Default is ``None``, + which means that mode wouldn't be changed. + """ + + name = "Color 3D LUT" + + def __init__( + self, size, table, channels: int = 3, target_mode: str | None = None, **kwargs + ): + if channels not in (3, 4): + msg = "Only 3 or 4 output channels are supported" + raise ValueError(msg) + self.size = size = self._check_size(size) + self.channels = channels + self.mode = target_mode + + # Hidden flag `_copy_table=False` could be used to avoid extra copying + # of the table if the table is specially made for the constructor. + copy_table = kwargs.get("_copy_table", True) + items = size[0] * size[1] * size[2] + wrong_size = False + + numpy: ModuleType | None = None + if hasattr(table, "shape"): + try: + import numpy + except ImportError: + pass + + if numpy and isinstance(table, numpy.ndarray): + if copy_table: + table = table.copy() + + if table.shape in [ + (items * channels,), + (items, channels), + (size[2], size[1], size[0], channels), + ]: + table = table.reshape(items * channels) + else: + wrong_size = True + + else: + if copy_table: + table = list(table) + + # Convert to a flat list + if table and isinstance(table[0], (list, tuple)): + table, raw_table = [], table + for pixel in raw_table: + if len(pixel) != channels: + msg = ( + "The elements of the table should " + f"have a length of {channels}." + ) + raise ValueError(msg) + table.extend(pixel) + + if wrong_size or len(table) != items * channels: + msg = ( + "The table should have either channels * size**3 float items " + "or size**3 items of channels-sized tuples with floats. " + f"Table should be: {channels}x{size[0]}x{size[1]}x{size[2]}. " + f"Actual length: {len(table)}" + ) + raise ValueError(msg) + self.table = table + + @staticmethod + def _check_size(size: Any) -> list[int]: + try: + _, _, _ = size + except ValueError as e: + msg = "Size should be either an integer or a tuple of three integers." + raise ValueError(msg) from e + except TypeError: + size = (size, size, size) + size = [int(x) for x in size] + for size_1d in size: + if not 2 <= size_1d <= 65: + msg = "Size should be in [2, 65] range." + raise ValueError(msg) + return size + + @classmethod + def generate(cls, size, callback, channels=3, target_mode=None): + """Generates new LUT using provided callback. + + :param size: Size of the table. Passed to the constructor. + :param callback: Function with three parameters which correspond + three color channels. Will be called ``size**3`` + times with values from 0.0 to 1.0 and should return + a tuple with ``channels`` elements. + :param channels: The number of channels which should return callback. + :param target_mode: Passed to the constructor of the resulting + lookup table. + """ + size_1d, size_2d, size_3d = cls._check_size(size) + if channels not in (3, 4): + msg = "Only 3 or 4 output channels are supported" + raise ValueError(msg) + + table = [0] * (size_1d * size_2d * size_3d * channels) + idx_out = 0 + for b in range(size_3d): + for g in range(size_2d): + for r in range(size_1d): + table[idx_out : idx_out + channels] = callback( + r / (size_1d - 1), g / (size_2d - 1), b / (size_3d - 1) + ) + idx_out += channels + + return cls( + (size_1d, size_2d, size_3d), + table, + channels=channels, + target_mode=target_mode, + _copy_table=False, + ) + + def transform(self, callback, with_normals=False, channels=None, target_mode=None): + """Transforms the table values using provided callback and returns + a new LUT with altered values. + + :param callback: A function which takes old lookup table values + and returns a new set of values. The number + of arguments which function should take is + ``self.channels`` or ``3 + self.channels`` + if ``with_normals`` flag is set. + Should return a tuple of ``self.channels`` or + ``channels`` elements if it is set. + :param with_normals: If true, ``callback`` will be called with + coordinates in the color cube as the first + three arguments. Otherwise, ``callback`` + will be called only with actual color values. + :param channels: The number of channels in the resulting lookup table. + :param target_mode: Passed to the constructor of the resulting + lookup table. + """ + if channels not in (None, 3, 4): + msg = "Only 3 or 4 output channels are supported" + raise ValueError(msg) + ch_in = self.channels + ch_out = channels or ch_in + size_1d, size_2d, size_3d = self.size + + table = [0] * (size_1d * size_2d * size_3d * ch_out) + idx_in = 0 + idx_out = 0 + for b in range(size_3d): + for g in range(size_2d): + for r in range(size_1d): + values = self.table[idx_in : idx_in + ch_in] + if with_normals: + values = callback( + r / (size_1d - 1), + g / (size_2d - 1), + b / (size_3d - 1), + *values, + ) + else: + values = callback(*values) + table[idx_out : idx_out + ch_out] = values + idx_in += ch_in + idx_out += ch_out + + return type(self)( + self.size, + table, + channels=ch_out, + target_mode=target_mode or self.mode, + _copy_table=False, + ) + + def __repr__(self) -> str: + r = [ + f"{self.__class__.__name__} from {self.table.__class__.__name__}", + "size={:d}x{:d}x{:d}".format(*self.size), + f"channels={self.channels:d}", + ] + if self.mode: + r.append(f"target_mode={self.mode}") + return "<{}>".format(" ".join(r)) + + def filter(self, image): + from . import Image + + return image.color_lut_3d( + self.mode or image.mode, + Image.Resampling.BILINEAR, + self.channels, + self.size[0], + self.size[1], + self.size[2], + self.table, + ) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index fa5608e6c..ecdd10f37 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -1,1270 +1,1270 @@ -# -# The Python Imaging Library. -# $Id$ -# -# PIL raster font management -# -# History: -# 1996-08-07 fl created (experimental) -# 1997-08-25 fl minor adjustments to handle fonts from pilfont 0.3 -# 1999-02-06 fl rewrote most font management stuff in C -# 1999-03-17 fl take pth files into account in load_path (from Richard Jones) -# 2001-02-17 fl added freetype support -# 2001-05-09 fl added TransposedFont wrapper class -# 2002-03-04 fl make sure we have a "L" or "1" font -# 2002-12-04 fl skip non-directory entries in the system path -# 2003-04-29 fl add embedded default font -# 2003-09-27 fl added support for truetype charmap encodings -# -# Todo: -# Adapt to PILFONT2 format (16-bit fonts, compressed, single file) -# -# Copyright (c) 1997-2003 by Secret Labs AB -# Copyright (c) 1996-2003 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# - -from __future__ import annotations - -import base64 -import os -import sys -import warnings -from enum import IntEnum -from io import BytesIO -from typing import IO, TYPE_CHECKING, Any, BinaryIO - -from . import Image -from ._typing import StrOrBytesPath -from ._util import is_path - -if TYPE_CHECKING: - from . import ImageFile - from ._imaging import ImagingFont - from ._imagingft import Font - - -class Layout(IntEnum): - BASIC = 0 - RAQM = 1 - - -MAX_STRING_LENGTH = 1_000_000 - - -try: - from . import _imagingft as core -except ImportError as ex: - from ._util import DeferredError - - core = DeferredError.new(ex) - - -def _string_length_check(text: str | bytes | bytearray) -> None: - if MAX_STRING_LENGTH is not None and len(text) > MAX_STRING_LENGTH: - msg = "too many characters in string" - raise ValueError(msg) - - -# FIXME: add support for pilfont2 format (see FontFile.py) - -# -------------------------------------------------------------------- -# Font metrics format: -# "PILfont" LF -# fontdescriptor LF -# (optional) key=value... LF -# "DATA" LF -# binary data: 256*10*2 bytes (dx, dy, dstbox, srcbox) -# -# To place a character, cut out srcbox and paste at dstbox, -# relative to the character position. Then move the character -# position according to dx, dy. -# -------------------------------------------------------------------- - - -class ImageFont: - """PIL font wrapper""" - - font: ImagingFont - - def _load_pilfont(self, filename: str) -> None: - with open(filename, "rb") as fp: - image: ImageFile.ImageFile | None = None - for ext in (".png", ".gif", ".pbm"): - if image: - image.close() - try: - fullname = os.path.splitext(filename)[0] + ext - image = Image.open(fullname) - except Exception: - pass - else: - if image and image.mode in ("1", "L"): - break - else: - if image: - image.close() - msg = "cannot find glyph data file" - raise OSError(msg) - - self.file = fullname - - self._load_pilfont_data(fp, image) - image.close() - - def _load_pilfont_data(self, file: IO[bytes], image: Image.Image) -> None: - # read PILfont header - if file.readline() != b"PILfont\n": - msg = "Not a PILfont file" - raise SyntaxError(msg) - file.readline().split(b";") - self.info = [] # FIXME: should be a dictionary - while True: - s = file.readline() - if not s or s == b"DATA\n": - break - self.info.append(s) - - # read PILfont metrics - data = file.read(256 * 20) - - # check image - if image.mode not in ("1", "L"): - msg = "invalid font image mode" - raise TypeError(msg) - - image.load() - - self.font = Image.core.font(image.im, data) - - def getmask(self, text, mode="", *args, **kwargs): - """ - Create a bitmap for the text. - - If the font uses antialiasing, the bitmap should have mode ``L`` and use a - maximum value of 255. Otherwise, it should have mode ``1``. - - :param text: Text to render. - :param mode: Used by some graphics drivers to indicate what mode the - driver prefers; if empty, the renderer may return either - mode. Note that the mode is always a string, to simplify - C-level implementations. - - .. versionadded:: 1.1.5 - - :return: An internal PIL storage memory instance as defined by the - :py:mod:`PIL.Image.core` interface module. - """ - _string_length_check(text) - Image._decompression_bomb_check(self.font.getsize(text)) - return self.font.getmask(text, mode) - - def getbbox( - self, text: str | bytes | bytearray, *args: Any, **kwargs: Any - ) -> tuple[int, int, int, int]: - """ - Returns bounding box (in pixels) of given text. - - .. versionadded:: 9.2.0 - - :param text: Text to render. - - :return: ``(left, top, right, bottom)`` bounding box - """ - _string_length_check(text) - width, height = self.font.getsize(text) - return 0, 0, width, height - - def getlength( - self, text: str | bytes | bytearray, *args: Any, **kwargs: Any - ) -> int: - """ - Returns length (in pixels) of given text. - This is the amount by which following text should be offset. - - .. versionadded:: 9.2.0 - """ - _string_length_check(text) - width, height = self.font.getsize(text) - return width - - -## -# Wrapper for FreeType fonts. Application code should use the -# truetype factory function to create font objects. - - -class FreeTypeFont: - """FreeType font wrapper (requires _imagingft service)""" - - font: Font - - def __init__( - self, - font: StrOrBytesPath | BinaryIO | None = None, - size: float = 10, - index: int = 0, - encoding: str = "", - layout_engine: Layout | None = None, - ) -> None: - # FIXME: use service provider instead - - if size <= 0: - msg = "font size must be greater than 0" - raise ValueError(msg) - - self.path = font - self.size = size - self.index = index - self.encoding = encoding - - if layout_engine not in (Layout.BASIC, Layout.RAQM): - layout_engine = Layout.BASIC - if core.HAVE_RAQM: - layout_engine = Layout.RAQM - elif layout_engine == Layout.RAQM and not core.HAVE_RAQM: - warnings.warn( - "Raqm layout was requested, but Raqm is not available. " - "Falling back to basic layout." - ) - layout_engine = Layout.BASIC - - self.layout_engine = layout_engine - - def load_from_bytes(f): - self.font_bytes = f.read() - self.font = core.getfont( - "", size, index, encoding, self.font_bytes, layout_engine - ) - - if is_path(font): - font = os.path.realpath(os.fspath(font)) - if sys.platform == "win32": - font_bytes_path = font if isinstance(font, bytes) else font.encode() - try: - font_bytes_path.decode("ascii") - except UnicodeDecodeError: - # FreeType cannot load fonts with non-ASCII characters on Windows - # So load it into memory first - with open(font, "rb") as f: - load_from_bytes(f) - return - self.font = core.getfont( - font, size, index, encoding, layout_engine=layout_engine - ) - else: - load_from_bytes(font) - - def __getstate__(self): - return [self.path, self.size, self.index, self.encoding, self.layout_engine] - - def __setstate__(self, state): - path, size, index, encoding, layout_engine = state - self.__init__(path, size, index, encoding, layout_engine) - - def getname(self) -> tuple[str | None, str | None]: - """ - :return: A tuple of the font family (e.g. Helvetica) and the font style - (e.g. Bold) - """ - return self.font.family, self.font.style - - def getmetrics(self) -> tuple[int, int]: - """ - :return: A tuple of the font ascent (the distance from the baseline to - the highest outline point) and descent (the distance from the - baseline to the lowest outline point, a negative value) - """ - return self.font.ascent, self.font.descent - - def getlength( - self, text: str, mode="", direction=None, features=None, language=None - ) -> float: - """ - Returns length (in pixels with 1/64 precision) of given text when rendered - in font with provided direction, features, and language. - - This is the amount by which following text should be offset. - Text bounding box may extend past the length in some fonts, - e.g. when using italics or accents. - - The result is returned as a float; it is a whole number if using basic layout. - - Note that the sum of two lengths may not equal the length of a concatenated - string due to kerning. If you need to adjust for kerning, include the following - character and subtract its length. - - For example, instead of :: - - hello = font.getlength("Hello") - world = font.getlength("World") - hello_world = hello + world # not adjusted for kerning - assert hello_world == font.getlength("HelloWorld") # may fail - - use :: - - hello = font.getlength("HelloW") - font.getlength("W") # adjusted for kerning - world = font.getlength("World") - hello_world = hello + world # adjusted for kerning - assert hello_world == font.getlength("HelloWorld") # True - - or disable kerning with (requires libraqm) :: - - hello = draw.textlength("Hello", font, features=["-kern"]) - world = draw.textlength("World", font, features=["-kern"]) - hello_world = hello + world # kerning is disabled, no need to adjust - assert hello_world == draw.textlength("HelloWorld", font, features=["-kern"]) - - .. versionadded:: 8.0.0 - - :param text: Text to measure. - :param mode: Used by some graphics drivers to indicate what mode the - driver prefers; if empty, the renderer may return either - mode. Note that the mode is always a string, to simplify - C-level implementations. - - :param direction: Direction of the text. It can be 'rtl' (right to - left), 'ltr' (left to right) or 'ttb' (top to bottom). - Requires libraqm. - - :param features: A list of OpenType font features to be used during text - layout. This is usually used to turn on optional - font features that are not enabled by default, - for example 'dlig' or 'ss01', but can be also - used to turn off default font features for - example '-liga' to disable ligatures or '-kern' - to disable kerning. To get all supported - features, see - https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist - Requires libraqm. - - :param language: Language of the text. Different languages may use - different glyph shapes or ligatures. This parameter tells - the font which language the text is in, and to apply the - correct substitutions as appropriate, if available. - It should be a `BCP 47 language code - `_ - Requires libraqm. - - :return: Either width for horizontal text, or height for vertical text. - """ - _string_length_check(text) - return self.font.getlength(text, mode, direction, features, language) / 64 - - def getbbox( - self, - text: str, - mode: str = "", - direction: str | None = None, - features: list[str] | None = None, - language: str | None = None, - stroke_width: float = 0, - anchor: str | None = None, - ) -> tuple[float, float, float, float]: - """ - Returns bounding box (in pixels) of given text relative to given anchor - when rendered in font with provided direction, features, and language. - - Use :py:meth:`getlength()` to get the offset of following text with - 1/64 pixel precision. The bounding box includes extra margins for - some fonts, e.g. italics or accents. - - .. versionadded:: 8.0.0 - - :param text: Text to render. - :param mode: Used by some graphics drivers to indicate what mode the - driver prefers; if empty, the renderer may return either - mode. Note that the mode is always a string, to simplify - C-level implementations. - - :param direction: Direction of the text. It can be 'rtl' (right to - left), 'ltr' (left to right) or 'ttb' (top to bottom). - Requires libraqm. - - :param features: A list of OpenType font features to be used during text - layout. This is usually used to turn on optional - font features that are not enabled by default, - for example 'dlig' or 'ss01', but can be also - used to turn off default font features for - example '-liga' to disable ligatures or '-kern' - to disable kerning. To get all supported - features, see - https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist - Requires libraqm. - - :param language: Language of the text. Different languages may use - different glyph shapes or ligatures. This parameter tells - the font which language the text is in, and to apply the - correct substitutions as appropriate, if available. - It should be a `BCP 47 language code - `_ - Requires libraqm. - - :param stroke_width: The width of the text stroke. - - :param anchor: The text anchor alignment. Determines the relative location of - the anchor to the text. The default alignment is top left, - specifically ``la`` for horizontal text and ``lt`` for - vertical text. See :ref:`text-anchors` for details. - - :return: ``(left, top, right, bottom)`` bounding box - """ - _string_length_check(text) - size, offset = self.font.getsize( - text, mode, direction, features, language, anchor - ) - left, top = offset[0] - stroke_width, offset[1] - stroke_width - width, height = size[0] + 2 * stroke_width, size[1] + 2 * stroke_width - return left, top, left + width, top + height - - def getmask( - self, - text, - mode="", - direction=None, - features=None, - language=None, - stroke_width=0, - anchor=None, - ink=0, - start=None, - ): - """ - Create a bitmap for the text. - - If the font uses antialiasing, the bitmap should have mode ``L`` and use a - maximum value of 255. If the font has embedded color data, the bitmap - should have mode ``RGBA``. Otherwise, it should have mode ``1``. - - :param text: Text to render. - :param mode: Used by some graphics drivers to indicate what mode the - driver prefers; if empty, the renderer may return either - mode. Note that the mode is always a string, to simplify - C-level implementations. - - .. versionadded:: 1.1.5 - - :param direction: Direction of the text. It can be 'rtl' (right to - left), 'ltr' (left to right) or 'ttb' (top to bottom). - Requires libraqm. - - .. versionadded:: 4.2.0 - - :param features: A list of OpenType font features to be used during text - layout. This is usually used to turn on optional - font features that are not enabled by default, - for example 'dlig' or 'ss01', but can be also - used to turn off default font features for - example '-liga' to disable ligatures or '-kern' - to disable kerning. To get all supported - features, see - https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist - Requires libraqm. - - .. versionadded:: 4.2.0 - - :param language: Language of the text. Different languages may use - different glyph shapes or ligatures. This parameter tells - the font which language the text is in, and to apply the - correct substitutions as appropriate, if available. - It should be a `BCP 47 language code - `_ - Requires libraqm. - - .. versionadded:: 6.0.0 - - :param stroke_width: The width of the text stroke. - - .. versionadded:: 6.2.0 - - :param anchor: The text anchor alignment. Determines the relative location of - the anchor to the text. The default alignment is top left, - specifically ``la`` for horizontal text and ``lt`` for - vertical text. See :ref:`text-anchors` for details. - - .. versionadded:: 8.0.0 - - :param ink: Foreground ink for rendering in RGBA mode. - - .. versionadded:: 8.0.0 - - :param start: Tuple of horizontal and vertical offset, as text may render - differently when starting at fractional coordinates. - - .. versionadded:: 9.4.0 - - :return: An internal PIL storage memory instance as defined by the - :py:mod:`PIL.Image.core` interface module. - """ - return self.getmask2( - text, - mode, - direction=direction, - features=features, - language=language, - stroke_width=stroke_width, - anchor=anchor, - ink=ink, - start=start, - )[0] - - def getmask2( - self, - text: str, - mode="", - direction=None, - features=None, - language=None, - stroke_width=0, - anchor=None, - ink=0, - start=None, - *args, - **kwargs, - ): - """ - Create a bitmap for the text. - - If the font uses antialiasing, the bitmap should have mode ``L`` and use a - maximum value of 255. If the font has embedded color data, the bitmap - should have mode ``RGBA``. Otherwise, it should have mode ``1``. - - :param text: Text to render. - :param mode: Used by some graphics drivers to indicate what mode the - driver prefers; if empty, the renderer may return either - mode. Note that the mode is always a string, to simplify - C-level implementations. - - .. versionadded:: 1.1.5 - - :param direction: Direction of the text. It can be 'rtl' (right to - left), 'ltr' (left to right) or 'ttb' (top to bottom). - Requires libraqm. - - .. versionadded:: 4.2.0 - - :param features: A list of OpenType font features to be used during text - layout. This is usually used to turn on optional - font features that are not enabled by default, - for example 'dlig' or 'ss01', but can be also - used to turn off default font features for - example '-liga' to disable ligatures or '-kern' - to disable kerning. To get all supported - features, see - https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist - Requires libraqm. - - .. versionadded:: 4.2.0 - - :param language: Language of the text. Different languages may use - different glyph shapes or ligatures. This parameter tells - the font which language the text is in, and to apply the - correct substitutions as appropriate, if available. - It should be a `BCP 47 language code - `_ - Requires libraqm. - - .. versionadded:: 6.0.0 - - :param stroke_width: The width of the text stroke. - - .. versionadded:: 6.2.0 - - :param anchor: The text anchor alignment. Determines the relative location of - the anchor to the text. The default alignment is top left, - specifically ``la`` for horizontal text and ``lt`` for - vertical text. See :ref:`text-anchors` for details. - - .. versionadded:: 8.0.0 - - :param ink: Foreground ink for rendering in RGBA mode. - - .. versionadded:: 8.0.0 - - :param start: Tuple of horizontal and vertical offset, as text may render - differently when starting at fractional coordinates. - - .. versionadded:: 9.4.0 - - :return: A tuple of an internal PIL storage memory instance as defined by the - :py:mod:`PIL.Image.core` interface module, and the text offset, the - gap between the starting coordinate and the first marking - """ - _string_length_check(text) - if start is None: - start = (0, 0) - - def fill(width, height): - size = (width, height) - Image._decompression_bomb_check(size) - return Image.core.fill("RGBA" if mode == "RGBA" else "L", size) - - return self.font.render( - text, - fill, - mode, - direction, - features, - language, - stroke_width, - anchor, - ink, - start[0], - start[1], - ) - - def font_variant( - self, font=None, size=None, index=None, encoding=None, layout_engine=None - ): - """ - Create a copy of this FreeTypeFont object, - using any specified arguments to override the settings. - - Parameters are identical to the parameters used to initialize this - object. - - :return: A FreeTypeFont object. - """ - if font is None: - try: - font = BytesIO(self.font_bytes) - except AttributeError: - font = self.path - return FreeTypeFont( - font=font, - size=self.size if size is None else size, - index=self.index if index is None else index, - encoding=self.encoding if encoding is None else encoding, - layout_engine=layout_engine or self.layout_engine, - ) - - def get_variation_names(self) -> list[bytes]: - """ - :returns: A list of the named styles in a variation font. - :exception OSError: If the font is not a variation font. - """ - try: - names = self.font.getvarnames() - except AttributeError as e: - msg = "FreeType 2.9.1 or greater is required" - raise NotImplementedError(msg) from e - return [name.replace(b"\x00", b"") for name in names] - - def set_variation_by_name(self, name): - """ - :param name: The name of the style. - :exception OSError: If the font is not a variation font. - """ - names = self.get_variation_names() - if not isinstance(name, bytes): - name = name.encode() - index = names.index(name) + 1 - - if index == getattr(self, "_last_variation_index", None): - # When the same name is set twice in a row, - # there is an 'unknown freetype error' - # https://savannah.nongnu.org/bugs/?56186 - return - self._last_variation_index = index - - self.font.setvarname(index) - - def get_variation_axes(self): - """ - :returns: A list of the axes in a variation font. - :exception OSError: If the font is not a variation font. - """ - try: - axes = self.font.getvaraxes() - except AttributeError as e: - msg = "FreeType 2.9.1 or greater is required" - raise NotImplementedError(msg) from e - for axis in axes: - if axis["name"]: - axis["name"] = axis["name"].replace(b"\x00", b"") - return axes - - def set_variation_by_axes(self, axes: list[float]) -> None: - """ - :param axes: A list of values for each axis. - :exception OSError: If the font is not a variation font. - """ - try: - self.font.setvaraxes(axes) - except AttributeError as e: - msg = "FreeType 2.9.1 or greater is required" - raise NotImplementedError(msg) from e - - -class TransposedFont: - """Wrapper for writing rotated or mirrored text""" - - def __init__(self, font, orientation=None): - """ - Wrapper that creates a transposed font from any existing font - object. - - :param font: A font object. - :param orientation: An optional orientation. If given, this should - be one of Image.Transpose.FLIP_LEFT_RIGHT, Image.Transpose.FLIP_TOP_BOTTOM, - Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_180, or - Image.Transpose.ROTATE_270. - """ - self.font = font - self.orientation = orientation # any 'transpose' argument, or None - - def getmask(self, text, mode="", *args, **kwargs): - im = self.font.getmask(text, mode, *args, **kwargs) - if self.orientation is not None: - return im.transpose(self.orientation) - return im - - def getbbox(self, text, *args, **kwargs): - # TransposedFont doesn't support getmask2, move top-left point to (0, 0) - # this has no effect on ImageFont and simulates anchor="lt" for FreeTypeFont - left, top, right, bottom = self.font.getbbox(text, *args, **kwargs) - width = right - left - height = bottom - top - if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270): - return 0, 0, height, width - return 0, 0, width, height - - def getlength(self, text: str, *args, **kwargs) -> float: - if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270): - msg = "text length is undefined for text rotated by 90 or 270 degrees" - raise ValueError(msg) - return self.font.getlength(text, *args, **kwargs) - - -def load(filename: str) -> ImageFont: - """ - Load a font file. This function loads a font object from the given - bitmap font file, and returns the corresponding font object. - - :param filename: Name of font file. - :return: A font object. - :exception OSError: If the file could not be read. - """ - f = ImageFont() - f._load_pilfont(filename) - return f - - -def truetype( - font: StrOrBytesPath | BinaryIO | None = None, - size: float = 10, - index: int = 0, - encoding: str = "", - layout_engine: Layout | None = None, -) -> FreeTypeFont: - """ - Load a TrueType or OpenType font from a file or file-like object, - and create a font object. - This function loads a font object from the given file or file-like - object, and creates a font object for a font of the given size. - - Pillow uses FreeType to open font files. On Windows, be aware that FreeType - will keep the file open as long as the FreeTypeFont object exists. Windows - limits the number of files that can be open in C at once to 512, so if many - fonts are opened simultaneously and that limit is approached, an - ``OSError`` may be thrown, reporting that FreeType "cannot open resource". - A workaround would be to copy the file(s) into memory, and open that instead. - - This function requires the _imagingft service. - - :param font: A filename or file-like object containing a TrueType font. - If the file is not found in this filename, the loader may also - search in other directories, such as the :file:`fonts/` - directory on Windows or :file:`/Library/Fonts/`, - :file:`/System/Library/Fonts/` and :file:`~/Library/Fonts/` on - macOS. - - :param size: The requested size, in pixels. - :param index: Which font face to load (default is first available face). - :param encoding: Which font encoding to use (default is Unicode). Possible - encodings include (see the FreeType documentation for more - information): - - * "unic" (Unicode) - * "symb" (Microsoft Symbol) - * "ADOB" (Adobe Standard) - * "ADBE" (Adobe Expert) - * "ADBC" (Adobe Custom) - * "armn" (Apple Roman) - * "sjis" (Shift JIS) - * "gb " (PRC) - * "big5" - * "wans" (Extended Wansung) - * "joha" (Johab) - * "lat1" (Latin-1) - - This specifies the character set to use. It does not alter the - encoding of any text provided in subsequent operations. - :param layout_engine: Which layout engine to use, if available: - :attr:`.ImageFont.Layout.BASIC` or :attr:`.ImageFont.Layout.RAQM`. - If it is available, Raqm layout will be used by default. - Otherwise, basic layout will be used. - - Raqm layout is recommended for all non-English text. If Raqm layout - is not required, basic layout will have better performance. - - You can check support for Raqm layout using - :py:func:`PIL.features.check_feature` with ``feature="raqm"``. - - .. versionadded:: 4.2.0 - :return: A font object. - :exception OSError: If the file could not be read. - :exception ValueError: If the font size is not greater than zero. - """ - - def freetype(font: StrOrBytesPath | BinaryIO | None) -> FreeTypeFont: - return FreeTypeFont(font, size, index, encoding, layout_engine) - - try: - return freetype(font) - except OSError: - if not is_path(font): - raise - ttf_filename = os.path.basename(font) - - dirs = [] - if sys.platform == "win32": - # check the windows font repository - # NOTE: must use uppercase WINDIR, to work around bugs in - # 1.5.2's os.environ.get() - windir = os.environ.get("WINDIR") - if windir: - dirs.append(os.path.join(windir, "fonts")) - elif sys.platform in ("linux", "linux2"): - lindirs = os.environ.get("XDG_DATA_DIRS") - if not lindirs: - # According to the freedesktop spec, XDG_DATA_DIRS should - # default to /usr/share - lindirs = "/usr/share" - dirs += [os.path.join(lindir, "fonts") for lindir in lindirs.split(":")] - elif sys.platform == "darwin": - dirs += [ - "/Library/Fonts", - "/System/Library/Fonts", - os.path.expanduser("~/Library/Fonts"), - ] - - ext = os.path.splitext(ttf_filename)[1] - first_font_with_a_different_extension = None - for directory in dirs: - for walkroot, walkdir, walkfilenames in os.walk(directory): - for walkfilename in walkfilenames: - if ext and walkfilename == ttf_filename: - return freetype(os.path.join(walkroot, walkfilename)) - elif not ext and os.path.splitext(walkfilename)[0] == ttf_filename: - fontpath = os.path.join(walkroot, walkfilename) - if os.path.splitext(fontpath)[1] == ".ttf": - return freetype(fontpath) - if not ext and first_font_with_a_different_extension is None: - first_font_with_a_different_extension = fontpath - if first_font_with_a_different_extension: - return freetype(first_font_with_a_different_extension) - raise - - -def load_path(filename: str | bytes) -> ImageFont: - """ - Load font file. Same as :py:func:`~PIL.ImageFont.load`, but searches for a - bitmap font along the Python path. - - :param filename: Name of font file. - :return: A font object. - :exception OSError: If the file could not be read. - """ - if not isinstance(filename, str): - filename = filename.decode("utf-8") - for directory in sys.path: - try: - return load(os.path.join(directory, filename)) - except OSError: - pass - msg = "cannot find font file" - raise OSError(msg) - - -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. - - Otherwise, load a "better than nothing" font. - - .. versionadded:: 1.1.4 - - :param size: The font size of Aileron Regular. - - .. versionadded:: 10.1.0 - - :return: A font object. - """ - f: FreeTypeFont | ImageFont - if core.__class__.__name__ == "module" or size is not None: - f = truetype( - BytesIO( - base64.b64decode( - b""" -AAEAAAAPAIAAAwBwRkZUTYwDlUAAADFoAAAAHEdERUYAqADnAAAo8AAAACRHUE9ThhmITwAAKfgAA -AduR1NVQnHxefoAACkUAAAA4k9TLzJovoHLAAABeAAAAGBjbWFw5lFQMQAAA6gAAAGqZ2FzcP//AA -MAACjoAAAACGdseWYmRXoPAAAGQAAAHfhoZWFkE18ayQAAAPwAAAA2aGhlYQboArEAAAE0AAAAJGh -tdHjjERZ8AAAB2AAAAdBsb2NhuOexrgAABVQAAADqbWF4cAC7AEYAAAFYAAAAIG5hbWUr+h5lAAAk -OAAAA6Jwb3N0D3oPTQAAJ9wAAAEKAAEAAAABGhxJDqIhXw889QALA+gAAAAA0Bqf2QAAAADhCh2h/ -2r/LgOxAyAAAAAIAAIAAAAAAAAAAQAAA8r/GgAAA7j/av9qA7EAAQAAAAAAAAAAAAAAAAAAAHQAAQ -AAAHQAQwAFAAAAAAACAAAAAQABAAAAQAAAAAAAAAADAfoBkAAFAAgCigJYAAAASwKKAlgAAAFeADI -BPgAAAAAFAAAAAAAAAAAAAAcAAAAAAAAAAAAAAABVS1dOAEAAIPsCAwL/GgDIA8oA5iAAAJMAAAAA -AhICsgAAACAAAwH0AAAAAAAAAU0AAADYAAAA8gA5AVMAVgJEAEYCRAA1AuQAKQKOAEAAsAArATsAZ -AE7AB4CMABVAkQAUADc/+EBEgAgANwAJQEv//sCRAApAkQAggJEADwCRAAtAkQAIQJEADkCRAArAk -QAMgJEACwCRAAxANwAJQDc/+ECRABnAkQAUAJEAEQB8wAjA1QANgJ/AB0CcwBkArsALwLFAGQCSwB -kAjcAZALGAC8C2gBkAQgAZAIgADcCYQBkAj8AZANiAGQCzgBkAuEALwJWAGQC3QAvAmsAZAJJADQC -ZAAiAqoAXgJuACADuAAaAnEAGQJFABMCTwAuATMAYgEv//sBJwAiAkQAUAH0ADIBLAApAhMAJAJjA -EoCEQAeAmcAHgIlAB4BIgAVAmcAHgJRAEoA7gA+AOn/8wIKAEoA9wBGA1cASgJRAEoCSgAeAmMASg -JnAB4BSgBKAcsAGAE5ABQCUABCAgIAAQMRAAEB4v/6AgEAAQHOABQBLwBAAPoAYAEvACECRABNA0Y -AJAItAHgBKgAcAkQAUAEsAHQAygAgAi0AOQD3ADYA9wAWAaEANgGhABYCbAAlAYMAeAGDADkA6/9q -AhsAFAIKABUB/QAVAAAAAwAAAAMAAAAcAAEAAAAAAKQAAwABAAAAHAAEAIgAAAAeABAAAwAOAH4Aq -QCrALEAtAC3ALsgGSAdICYgOiBEISL7Av//AAAAIACpAKsAsAC0ALcAuyAYIBwgJiA5IEQhIvsB// -//4/+5/7j/tP+y/7D/reBR4E/gR+A14CzfTwVxAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAEGAAABAAAAAAAAAAECAAAAAgAAAAAAAAAAAAAAAAAAAAEAAAMEBQYHCAkKCwwNDg8QERIT -FBUWFxgZGhscHR4fICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj9AQUJDREVGR0hJSktMT -U5PUFFSU1RVVldYWVpbXF1eX2BhAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGQAAA -AAAAAAYnFmAAAAAABlAAAAAAAAAAAAAAAAAAAAAAAAAAAAY2htAAAAAAAAAABrbGlqAAAAAHAAbm9 -ycwBnAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAmACYAJgAmAD4AUgCCAMoBCgFO -AVwBcgGIAaYBvAHKAdYB6AH2AgwCIAJKAogCpgLWAw4DIgNkA5wDugPUA+gD/AQQBEYEogS8BPoFJ -gVSBWoFgAWwBcoF1gX6BhQGJAZMBmgGiga0BuIHGgdUB2YHkAeiB8AH3AfyCAoIHAgqCDoITghcCG -oIogjSCPoJKglYCXwJwgnqCgIKKApACl4Klgq8CtwLDAs8C1YLjAuyC9oL7gwMDCYMSAxgDKAMrAz -qDQoNTA1mDYQNoA2uDcAN2g3oDfYODA4iDkoOXA5sDnoOnA7EDvwAAAAFAAAAAAH0ArwAAwAGAAkA -DAAPAAAxESERAxMhExcRASELARETAfT6qv6syKr+jgFUqsiqArz9RAGLAP/+1P8B/v3VAP8BLP4CA -P8AAgA5//IAuQKyAAMACwAANyMDMwIyFhQGIiY0oE4MZk84JCQ4JLQB/v3AJDgkJDgAAgBWAeUBPA -LfAAMABwAAEyMnMxcjJzOmRgpagkYKWgHl+vr6AAAAAAIARgAAAf4CsgAbAB8AAAEHMxUjByM3Iwc -jNyM1MzcjNTM3MwczNzMHMxUrAQczAZgdZXEvOi9bLzovWmYdZXEvOi9bLzovWp9bHlsBn4w429vb -2ziMONvb29s4jAAAAAMANf+mAg4DDAAfACYALAAAJRQGBxUjNS4BJzMeARcRLgE0Njc1MxUeARcjJ -icVHgEBFBYXNQ4BExU+ATU0Ag5xWDpgcgRcBz41Xl9oVTpVYwpcC1ttXP6cLTQuM5szOrVRZwlOTQ -ZqVzZECAEAGlukZAlOTQdrUG8O7iNlAQgxNhDlCDj+8/YGOjReAAAAAAUAKf/yArsCvAAHAAsAFQA -dACcAABIyFhQGIiY0EyMBMwQiBhUUFjI2NTQSMhYUBiImNDYiBhUUFjI2NTR5iFBQiFCVVwHAV/5c -OiMjOiPmiFBQiFCxOiMjOiMCvFaSVlaS/ZoCsjIzMC80NC8w/uNWklZWkhozMC80NC8wAAAAAgBA/ -/ICbgLAACIALgAAARUjEQYjIiY1NDY3LgE1NDYzMhcVJiMiBhUUFhcWOwE1MxUFFBYzMjc1IyIHDg -ECbmBcYYOOVkg7R4hsQjY4Q0RNRD4SLDxW/pJUXzksPCkUUk0BgUb+zBVUZ0BkDw5RO1huCkULQzp -COAMBcHDHRz0J/AIHRQAAAAEAKwHlAIUC3wADAAATIycze0YKWgHl+gAAAAABAGT/sAEXAwwACQAA -EzMGEBcjLgE0Nt06dXU6OUBAAwzG/jDGVePs4wAAAAEAHv+wANEDDAAJAAATMx4BFAYHIzYQHjo5Q -EA5OnUDDFXj7ONVxgHQAAAAAQBVAFIB2wHbAA4AAAE3FwcXBycHJzcnNxcnMwEtmxOfcTJjYzJxnx -ObCj4BKD07KYolmZkliik7PbMAAQBQAFUB9AIlAAsAAAEjFSM1IzUzNTMVMwH0tTq1tTq1AR/Kyjj -OzgAAAAAB/+H/iACMAGQABAAANwcjNzOMWlFOXVrS3AAAAQAgAP8A8gE3AAMAABMjNTPy0tIA/zgA -AQAl//IApQByAAcAADYyFhQGIiY0STgkJDgkciQ4JCQ4AAAAAf/7/+IBNALQAAMAABcjEzM5Pvs+H -gLuAAAAAAIAKf/yAhsCwAADAAcAABIgECA2IBAgKQHy/g5gATL+zgLA/TJEAkYAAAAAAQCCAAABlg -KyAAgAAAERIxEHNTc2MwGWVr6SIygCsv1OAldxW1sWAAEAPAAAAg4CwAAZAAA3IRUhNRM+ATU0JiM -iDwEjNz4BMzIWFRQGB7kBUv4x+kI2QTt+EAFWAQp8aGVtSl5GRjEA/0RVLzlLmAoKa3FsUkNxXQAA -AAEALf/yAhYCwAAqAAABHgEVFAYjIi8BMxceATMyNjU0KwE1MzI2NTQmIyIGDwEjNz4BMzIWFRQGA -YxBSZJo2RUBVgEHV0JBUaQREUBUQzc5TQcBVgEKfGhfcEMBbxJbQl1x0AoKRkZHPn9GSD80QUVCCg -pfbGBPOlgAAAACACEAAAIkArIACgAPAAAlIxUjNSE1ATMRMyMRBg8BAiRXVv6qAVZWV60dHLCurq4 -rAdn+QgFLMibzAAABADn/8gIZArIAHQAAATIWFRQGIyIvATMXFjMyNjU0JiMiByMTIRUhBzc2ATNv -d5Fl1RQBVgIad0VSTkVhL1IwAYj+vh8rMAHHgGdtgcUKCoFXTU5bYgGRRvAuHQAAAAACACv/8gITA -sAAFwAjAAABMhYVFAYjIhE0NjMyFh8BIycmIyIDNzYTMjY1NCYjIgYVFBYBLmp7imr0l3RZdAgBXA -IYZ5wKJzU6QVNJSz5SUAHSgWltiQFGxcNlVQoKdv7sPiz+ZF1LTmJbU0lhAAAAAQAyAAACGgKyAAY -AAAEVASMBITUCGv6oXAFL/oECsij9dgJsRgAAAAMALP/xAhgCwAAWACAALAAAAR4BFRQGIyImNTQ2 -Ny4BNTQ2MhYVFAYmIgYVFBYyNjU0AzI2NTQmIyIGFRQWAZQ5S5BmbIpPOjA7ecp5P2F8Q0J8RIVJS -0pLTEtOAW0TXTxpZ2ZqPF0SE1A3VWVlVTdQ/UU0N0RENzT9/ko+Ok1NOj1LAAIAMf/yAhkCwAAXAC -MAAAEyERQGIyImLwEzFxYzMhMHBiMiJjU0NhMyNjU0JiMiBhUUFgEl9Jd0WXQIAVwCGGecCic1SWp -7imo+UlBAQVNJAsD+usXDZVUKCnYBFD4sgWltif5kW1NJYV1LTmIAAAACACX/8gClAiAABwAPAAAS -MhYUBiImNBIyFhQGIiY0STgkJDgkJDgkJDgkAiAkOCQkOP52JDgkJDgAAAAC/+H/iAClAiAABwAMA -AASMhYUBiImNBMHIzczSTgkJDgkaFpSTl4CICQ4JCQ4/mba5gAAAQBnAB4B+AH0AAYAAAENARUlNS -UB+P6qAVb+bwGRAbCmpkbJRMkAAAIAUAC7AfQBuwADAAcAAAEhNSERITUhAfT+XAGk/lwBpAGDOP8 -AOAABAEQAHgHVAfQABgAAARUFNS0BNQHV/m8BVv6qAStEyUSmpkYAAAAAAgAj//IB1ALAABgAIAAA -ATIWFRQHDgEHIz4BNz4BNTQmIyIGByM+ARIyFhQGIiY0AQRibmktIAJWBSEqNig+NTlHBFoDezQ4J -CQ4JALAZ1BjaS03JS1DMD5LLDQ/SUVgcv2yJDgkJDgAAAAAAgA2/5gDFgKYADYAQgAAAQMGFRQzMj -Y1NCYjIg4CFRQWMzI2NxcGIyImNTQ+AjMyFhUUBiMiJwcGIyImNTQ2MzIfATcHNzYmIyIGFRQzMjY -Cej8EJjJJlnBAfGQ+oHtAhjUYg5OPx0h2k06Os3xRWQsVLjY5VHtdPBwJETcJDyUoOkZEJz8B0f74 -EQ8kZl6EkTFZjVOLlyknMVm1pmCiaTq4lX6CSCknTVRmmR8wPdYnQzxuSWVGAAIAHQAAAncCsgAHA -AoAACUjByMTMxMjATMDAcj+UVz4dO5d/sjPZPT0ArL9TgE6ATQAAAADAGQAAAJMArIAEAAbACcAAA -EeARUUBgcGKwERMzIXFhUUJRUzMjc2NTQnJiMTPgE1NCcmKwEVMzIBvkdHZkwiNt7LOSGq/oeFHBt -hahIlSTM+cB8Yj5UWAW8QT0VYYgwFArIEF5Fv1eMED2NfDAL93AU+N24PBP0AAAAAAQAv//ICjwLA -ABsAAAEyFh8BIycmIyIGFRQWMzI/ATMHDgEjIiY1NDYBdX+PCwFWAiKiaHx5ZaIiAlYBCpWBk6a0A -sCAagoKpqN/gaOmCgplhcicn8sAAAIAZAAAAp8CsgAMABkAAAEeARUUBgcGKwERMzITPgE1NCYnJi -sBETMyAY59lJp8IzXN0jUVWmdjWRs5d3I4Aq4QqJWUug8EArL9mQ+PeHGHDgX92gAAAAABAGQAAAI -vArIACwAAJRUhESEVIRUhFSEVAi/+NQHB/pUBTf6zRkYCskbwRvAAAAABAGQAAAIlArIACQAAExUh -FSERIxEhFboBQ/69VgHBAmzwRv7KArJGAAAAAAEAL//yAo8CwAAfAAABMxEjNQcGIyImNTQ2MzIWH -wEjJyYjIgYVFBYzMjY1IwGP90wfPnWTprSSf48LAVYCIqJofHllVG+hAU3+s3hARsicn8uAagoKpq -N/gaN1XAAAAAEAZAAAAowCsgALAAABESMRIREjETMRIRECjFb+hFZWAXwCsv1OAS7+0gKy/sQBPAA -AAAABAGQAAAC6ArIAAwAAMyMRM7pWVgKyAAABADf/8gHoArIAEwAAAREUBw4BIyImLwEzFxYzMjc2 -NREB6AIFcGpgbQIBVgIHfXQKAQKy/lYxIltob2EpKYyEFD0BpwAAAAABAGQAAAJ0ArIACwAACQEjA -wcVIxEzEQEzATsBJ3ntQlZWAVVlAWH+nwEnR+ACsv6RAW8AAQBkAAACLwKyAAUAACUVIREzEQIv/j -VWRkYCsv2UAAABAGQAAAMUArIAFAAAAREjETQ3BgcDIwMmJxYVESMRMxsBAxRWAiMxemx8NxsCVo7 -MywKy/U4BY7ZLco7+nAFmoFxLtP6dArL9lwJpAAAAAAEAZAAAAoACsgANAAAhIwEWFREjETMBJjUR -MwKAhP67A1aEAUUDVAJeeov+pwKy/aJ5jAFZAAAAAgAv//ICuwLAAAkAEwAAEiAWFRQGICY1NBIyN -jU0JiIGFRTbATSsrP7MrNrYenrYegLAxaKhxsahov47nIeIm5uIhwACAGQAAAJHArIADgAYAAABHg -EVFAYHBisBESMRMzITNjQnJisBETMyAZRUX2VOHzuAVtY7GlxcGDWIiDUCrgtnVlVpCgT+5gKy/rU -V1BUF/vgAAAACAC//zAK9AsAAEgAcAAAlFhcHJiMiBwYjIiY1NDYgFhUUJRQWMjY1NCYiBgI9PUMx -UDcfKh8omqysATSs/dR62Hp62HpICTg7NgkHxqGixcWitbWHnJyHiJubAAIAZAAAAlgCsgAXACMAA -CUWFyMmJyYnJisBESMRMzIXHgEVFAYHFiUzMjc+ATU0JyYrAQIqDCJfGQwNWhAhglbiOx9QXEY1Tv -6bhDATMj1lGSyMtYgtOXR0BwH+1wKyBApbU0BSESRAAgVAOGoQBAABADT/8gIoAsAAJQAAATIWFyM -uASMiBhUUFhceARUUBiMiJiczHgEzMjY1NCYnLgE1NDYBOmd2ClwGS0E6SUNRdW+HZnKKC1wPWkQ9 -Uk1cZGuEAsBwXUJHNjQ3OhIbZVZZbm5kREo+NT5DFRdYUFdrAAAAAAEAIgAAAmQCsgAHAAABIxEjE -SM1IQJk9lb2AkICbP2UAmxGAAEAXv/yAmQCsgAXAAABERQHDgEiJicmNREzERQXHgEyNjc2NRECZA -IIgfCBCAJWAgZYmlgGAgKy/k0qFFxzc1wUKgGz/lUrEkRQUEQSKwGrAAAAAAEAIAAAAnoCsgAGAAA -hIwMzGwEzAYJ07l3N1FwCsv2PAnEAAAEAGgAAA7ECsgAMAAABAyMLASMDMxsBMxsBA7HAcZyicrZi -kaB0nJkCsv1OAlP9rQKy/ZsCW/2kAmYAAAEAGQAAAm8CsgALAAAhCwEjEwMzGwEzAxMCCsrEY/bkY -re+Y/D6AST+3AFcAVb+5gEa/q3+oQAAAQATAAACUQKyAAgAAAERIxEDMxsBMwFdVvRjwLphARD+8A -EQAaL+sQFPAAABAC4AAAI5ArIACQAAJRUhNQEhNSEVAQI5/fUBof57Aen+YUZGQgIqRkX92QAAAAA -BAGL/sAEFAwwABwAAARUjETMVIxEBBWlpowMMOP0UOANcAAAB//v/4gE0AtAAAwAABSMDMwE0Pvs+ -HgLuAAAAAQAi/7AAxQMMAAcAABcjNTMRIzUzxaNpaaNQOALsOAABAFAA1wH0AmgABgAAJQsBIxMzE -wGwjY1GsESw1wFZ/qcBkf5vAAAAAQAy/6oBwv/iAAMAAAUhNSEBwv5wAZBWOAAAAAEAKQJEALYCsg -ADAAATIycztjhVUAJEbgAAAAACACT/8gHQAiAAHQAlAAAhJwcGIyImNTQ2OwE1NCcmIyIHIz4BMzI -XFh0BFBcnMjY9ASYVFAF6CR0wVUtgkJoiAgdgaQlaBm1Zrg4DCuQ9R+5MOSFQR1tbDiwUUXBUXowf -J8c9SjRORzYSgVwAAAAAAgBK//ICRQLfABEAHgAAATIWFRQGIyImLwEVIxEzETc2EzI2NTQmIyIGH -QEUFgFUcYCVbiNJEyNWVigySElcU01JXmECIJd4i5QTEDRJAt/+3jkq/hRuZV55ZWsdX14AAQAe// -IB9wIgABgAAAEyFhcjJiMiBhUUFjMyNjczDgEjIiY1NDYBF152DFocbEJXU0A1Rw1aE3pbaoKQAiB -oWH5qZm1tPDlaXYuLgZcAAAACAB7/8gIZAt8AEQAeAAABESM1BwYjIiY1NDYzMhYfAREDMjY9ATQm -IyIGFRQWAhlWKDJacYCVbiNJEyOnSV5hQUlcUwLf/SFVOSqXeIuUExA0ARb9VWVrHV9ebmVeeQACA -B7/8gH9AiAAFQAbAAABFAchHgEzMjY3Mw4BIyImNTQ2MzIWJyIGByEmAf0C/oAGUkA1SwlaD4FXbI -WObmt45UBVBwEqDQEYFhNjWD84W16Oh3+akU9aU60AAAEAFQAAARoC8gAWAAATBh0BMxUjESMRIzU -zNTQ3PgEzMhcVJqcDbW1WOTkDB0k8Hx5oAngVITRC/jQBzEIsJRs5PwVHEwAAAAIAHv8uAhkCIAAi -AC8AAAERFAcOASMiLwEzFx4BMzI2NzY9AQcGIyImNTQ2MzIWHwE1AzI2PQE0JiMiBhUUFgIZAQSEd -NwRAVcBBU5DTlUDASgyWnGAlW4jSRMjp0leYUFJXFMCEv5wSh1zeq8KCTI8VU0ZIQk5Kpd4i5QTED -RJ/iJlax1fXm5lXnkAAQBKAAACCgLkABcAAAEWFREjETQnLgEHDgEdASMRMxE3NjMyFgIIAlYCBDs -6RVRWViE5UVViAYUbQP7WASQxGzI7AQJyf+kC5P7TPSxUAAACAD4AAACsAsAABwALAAASMhYUBiIm -NBMjETNeLiAgLiBiVlYCwCAuICAu/WACEgAC//P/LgCnAsAABwAVAAASMhYUBiImNBcRFAcGIyInN -RY3NjURWS4gIC4gYgMLcRwNSgYCAsAgLiAgLo79wCUbZAJGBzMOHgJEAAAAAQBKAAACCALfAAsAAC -EnBxUjETMREzMHEwGTwTJWVvdu9/rgN6kC3/4oAQv6/ugAAQBG//wA3gLfAA8AABMRFBceATcVBiM -iJicmNRGcAQIcIxkkKi4CAQLf/bkhERoSBD4EJC8SNAJKAAAAAQBKAAADEAIgACQAAAEWFREjETQn -JiMiFREjETQnJiMiFREjETMVNzYzMhYXNzYzMhYDCwVWBAxedFYEDF50VlYiJko7ThAvJkpEVAGfI -jn+vAEcQyRZ1v76ARxDJFnW/voCEk08HzYtRB9HAAAAAAEASgAAAgoCIAAWAAABFhURIxE0JyYjIg -YdASMRMxU3NjMyFgIIAlYCCXBEVVZWITlRVWIBhRtA/tYBJDEbbHR/6QISWz0sVAAAAAACAB7/8gI -sAiAABwARAAASIBYUBiAmNBIyNjU0JiIGFRSlAQCHh/8Ah7ieWlqeWgIgn/Cfn/D+s3ZfYHV1YF8A -AgBK/zwCRQIgABEAHgAAATIWFRQGIyImLwERIxEzFTc2EzI2NTQmIyIGHQEUFgFUcYCVbiNJEyNWV -igySElcU01JXmECIJd4i5QTEDT+8wLWVTkq/hRuZV55ZWsdX14AAgAe/zwCGQIgABEAHgAAAREjEQ -cGIyImNTQ2MzIWHwE1AzI2PQE0JiMiBhUUFgIZVigyWnGAlW4jSRMjp0leYUFJXFMCEv0qARk5Kpd -4i5QTEDRJ/iJlax1fXm5lXnkAAQBKAAABPgIeAA0AAAEyFxUmBhURIxEzFTc2ARoWDkdXVlYwIwIe -B0EFVlf+0gISU0cYAAEAGP/yAa0CIAAjAAATMhYXIyYjIgYVFBYXHgEVFAYjIiYnMxYzMjY1NCYnL -gE1NDbkV2MJWhNdKy04PF1XbVhWbgxaE2ktOjlEUllkAiBaS2MrJCUoEBlPQkhOVFZoKCUmLhIWSE -BIUwAAAAEAFP/4ARQCiQAXAAATERQXHgE3FQYjIiYnJjURIzUzNTMVMxWxAQMmMx8qMjMEAUdHVmM -BzP7PGw4mFgY/BSwxDjQBNUJ7e0IAAAABAEL/8gICAhIAFwAAAREjNQcGIyImJyY1ETMRFBceATMy -Nj0BAgJWITlRT2EKBVYEBkA1RFECEv3uWj4qTToiOQE+/tIlJC43c4DpAAAAAAEAAQAAAfwCEgAGA -AABAyMDMxsBAfzJaclfop8CEv3uAhL+LQHTAAABAAEAAAMLAhIADAAAAQMjCwEjAzMbATMbAQMLqW -Z2dmapY3t0a3Z7AhL97gG+/kICEv5AAcD+QwG9AAAB//oAAAHWAhIACwAAARMjJwcjEwMzFzczARq -8ZIuKY763ZoWFYwEO/vLV1QEMAQbNzQAAAQAB/y4B+wISABEAAAEDDgEjIic1FjMyNj8BAzMbAQH7 -2iFZQB8NDRIpNhQH02GenQIS/cFVUAJGASozEwIt/i4B0gABABQAAAGxAg4ACQAAJRUhNQEhNSEVA -QGx/mMBNP7iAYL+zkREQgGIREX+ewAAAAABAED/sAEOAwwALAAAASMiBhUUFxYVFAYHHgEVFAcGFR -QWOwEVIyImNTQ3NjU0JzU2NTQnJjU0NjsBAQ4MKiMLDS4pKS4NCyMqDAtERAwLUlILDERECwLUGBk -WTlsgKzUFBTcrIFtOFhkYOC87GFVMIkUIOAhFIkxVGDsvAAAAAAEAYP84AJoDIAADAAAXIxEzmjo6 -yAPoAAEAIf+wAO8DDAAsAAATFQYVFBcWFRQGKwE1MzI2NTQnJjU0NjcuATU0NzY1NCYrATUzMhYVF -AcGFRTvUgsMREQLDCojCw0uKSkuDQsjKgwLREQMCwF6OAhFIkxVGDsvOBgZFk5bICs1BQU3KyBbTh -YZGDgvOxhVTCJFAAABAE0A3wH2AWQAEwAAATMUIyImJyYjIhUjNDMyFhcWMzIBvjhuGywtQR0xOG4 -bLC1BHTEBZIURGCNMhREYIwAAAwAk/94DIgLoAAcAEQApAAAAIBYQBiAmECQgBhUUFiA2NTQlMhYX -IyYjIgYUFjMyNjczDgEjIiY1NDYBAQFE3d3+vN0CB/7wubkBELn+xVBnD1wSWDo+QTcqOQZcEmZWX -HN2Aujg/rbg4AFKpr+Mjb6+jYxbWEldV5ZZNShLVn5na34AAgB4AFIB9AGeAAUACwAAAQcXIyc3Mw -cXIyc3AUqJiUmJifOJiUmJiQGepqampqampqYAAAIAHAHSAQ4CwAAHAA8AABIyFhQGIiY0NiIGFBY -yNjRgakREakSTNCEhNCECwEJqQkJqCiM4IyM4AAAAAAIAUAAAAfQCCwALAA8AAAEzFSMVIzUjNTM1 -MxMhNSEBP7W1OrW1OrX+XAGkAVs4tLQ4sP31OAAAAQB0AkQBAQKyAAMAABMjNzOsOD1QAkRuAAAAA -AEAIADsAKoBdgAHAAASMhYUBiImNEg6KCg6KAF2KDooKDoAAAIAOQBSAbUBngAFAAsAACUHIzcnMw -UHIzcnMwELiUmJiUkBM4lJiYlJ+KampqampqYAAAABADYB5QDhAt8ABAAAEzczByM2Xk1OXQHv8Po -AAQAWAeUAwQLfAAQAABMHIzczwV5NTl0C1fD6AAIANgHlAYsC3wAEAAkAABM3MwcjPwEzByM2Xk1O -XapeTU5dAe/w+grw+gAAAgAWAeUBawLfAAQACQAAEwcjNzMXByM3M8FeTU5dql5NTl0C1fD6CvD6A -AADACX/8gI1AHIABwAPABcAADYyFhQGIiY0NjIWFAYiJjQ2MhYUBiImNEk4JCQ4JOw4JCQ4JOw4JC -Q4JHIkOCQkOCQkOCQkOCQkOCQkOAAAAAEAeABSAUoBngAFAAABBxcjJzcBSomJSYmJAZ6mpqamAAA -AAAEAOQBSAQsBngAFAAAlByM3JzMBC4lJiYlJ+KampgAAAf9qAAABgQKyAAMAACsBATM/VwHAVwKy -AAAAAAIAFAHIAdwClAAHABQAABMVIxUjNSM1BRUjNwcjJxcjNTMXN9pKMkoByDICKzQqATJLKysCl -CmjoykBy46KiY3Lm5sAAQAVAAABvALyABgAAAERIxEjESMRIzUzNTQ3NjMyFxUmBgcGHQEBvFbCVj -k5AxHHHx5iVgcDAg798gHM/jQBzEIOJRuWBUcIJDAVIRYAAAABABX//AHkAvIAJQAAJR4BNxUGIyI -mJyY1ESYjIgcGHQEzFSMRIxEjNTM1NDc2MzIXERQBowIcIxkkKi4CAR4nXgwDbW1WLy8DEbNdOmYa -EQQ/BCQvEjQCFQZWFSEWQv40AcxCDiUblhP9uSEAAAAAAAAWAQ4AAQAAAAAAAAATACgAAQAAAAAAA -QAHAEwAAQAAAAAAAgAHAGQAAQAAAAAAAwAaAKIAAQAAAAAABAAHAM0AAQAAAAAABQA8AU8AAQAAAA -AABgAPAawAAQAAAAAACAALAdQAAQAAAAAACQALAfgAAQAAAAAACwAXAjQAAQAAAAAADAAXAnwAAwA -BBAkAAAAmAAAAAwABBAkAAQAOADwAAwABBAkAAgAOAFQAAwABBAkAAwA0AGwAAwABBAkABAAOAL0A -AwABBAkABQB4ANUAAwABBAkABgAeAYwAAwABBAkACAAWAbwAAwABBAkACQAWAeAAAwABBAkACwAuA -gQAAwABBAkADAAuAkwATgBvACAAUgBpAGcAaAB0AHMAIABSAGUAcwBlAHIAdgBlAGQALgAATm8gUm -lnaHRzIFJlc2VydmVkLgAAQQBpAGwAZQByAG8AbgAAQWlsZXJvbgAAUgBlAGcAdQBsAGEAcgAAUmV -ndWxhcgAAMQAuADEAMAAyADsAVQBLAFcATgA7AEEAaQBsAGUAcgBvAG4ALQBSAGUAZwB1AGwAYQBy -AAAxLjEwMjtVS1dOO0FpbGVyb24tUmVndWxhcgAAQQBpAGwAZQByAG8AbgAAQWlsZXJvbgAAVgBlA -HIAcwBpAG8AbgAgADEALgAxADAAMgA7AFAAUwAgADAAMAAxAC4AMQAwADIAOwBoAG8AdABjAG8Abg -B2ACAAMQAuADAALgA3ADAAOwBtAGEAawBlAG8AdABmAC4AbABpAGIAMgAuADUALgA1ADgAMwAyADk -AAFZlcnNpb24gMS4xMDI7UFMgMDAxLjEwMjtob3Rjb252IDEuMC43MDttYWtlb3RmLmxpYjIuNS41 -ODMyOQAAQQBpAGwAZQByAG8AbgAtAFIAZQBnAHUAbABhAHIAAEFpbGVyb24tUmVndWxhcgAAUwBvA -HIAYQAgAFMAYQBnAGEAbgBvAABTb3JhIFNhZ2FubwAAUwBvAHIAYQAgAFMAYQBnAGEAbgBvAABTb3 -JhIFNhZ2FubwAAaAB0AHQAcAA6AC8ALwB3AHcAdwAuAGQAbwB0AGMAbwBsAG8AbgAuAG4AZQB0AAB -odHRwOi8vd3d3LmRvdGNvbG9uLm5ldAAAaAB0AHQAcAA6AC8ALwB3AHcAdwAuAGQAbwB0AGMAbwBs -AG8AbgAuAG4AZQB0AABodHRwOi8vd3d3LmRvdGNvbG9uLm5ldAAAAAACAAAAAAAA/4MAMgAAAAAAA -AAAAAAAAAAAAAAAAAAAAHQAAAABAAIAAwAEAAUABgAHAAgACQAKAAsADAANAA4ADwAQABEAEgATAB -QAFQAWABcAGAAZABoAGwAcAB0AHgAfACAAIQAiACMAJAAlACYAJwAoACkAKgArACwALQAuAC8AMAA -xADIAMwA0ADUANgA3ADgAOQA6ADsAPAA9AD4APwBAAEEAQgBDAEQARQBGAEcASABJAEoASwBMAE0A -TgBPAFAAUQBSAFMAVABVAFYAVwBYAFkAWgBbAFwAXQBeAF8AYABhAIsAqQCDAJMAjQDDAKoAtgC3A -LQAtQCrAL4AvwC8AIwAwADBAAAAAAAB//8AAgABAAAADAAAABwAAAACAAIAAwBxAAEAcgBzAAIABA -AAAAIAAAABAAAACgBMAGYAAkRGTFQADmxhdG4AGgAEAAAAAP//AAEAAAAWAANDQVQgAB5NT0wgABZ -ST00gABYAAP//AAEAAAAA//8AAgAAAAEAAmxpZ2EADmxvY2wAFAAAAAEAAQAAAAEAAAACAAYAEAAG -AAAAAgASADQABAAAAAEATAADAAAAAgAQABYAAQAcAAAAAQABAE8AAQABAGcAAQABAE8AAwAAAAIAE -AAWAAEAHAAAAAEAAQAvAAEAAQBnAAEAAQAvAAEAGgABAAgAAgAGAAwAcwACAE8AcgACAEwAAQABAE -kAAAABAAAACgBGAGAAAkRGTFQADmxhdG4AHAAEAAAAAP//AAIAAAABABYAA0NBVCAAFk1PTCAAFlJ -PTSAAFgAA//8AAgAAAAEAAmNwc3AADmtlcm4AFAAAAAEAAAAAAAEAAQACAAYADgABAAAAAQASAAIA -AAACAB4ANgABAAoABQAFAAoAAgABACQAPQAAAAEAEgAEAAAAAQAMAAEAOP/nAAEAAQAkAAIGigAEA -AAFJAXKABoAGQAA//gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAD/sv+4/+z/7v/MAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAD/xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/9T/6AAAAAD/8QAA -ABD/vQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/7gAAAAAAAAAAAAAAAAAA//MAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABIAAAAAAAAAAP/5AAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/gAAD/4AAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//L/9AAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAA/+gAAAAAAAkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/zAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/mAAAAAAAAAAAAAAAAAAD -/4gAA//AAAAAA//YAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/+AAAAAAAAP/OAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/zv/qAAAAAP/0AAAACAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/ZAAD/egAA/1kAAAAA/5D/rgAAAAAAAAAAAA -AAAAAAAAAAAAAAAAD/9AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAD/8AAA/7b/8P+wAAD/8P/E/98AAAAA/8P/+P/0//oAAAAAAAAAAAAA//gA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/+AAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/w//C/9MAAP/SAAD/9wAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAD/yAAA/+kAAAAA//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/9wAAAAD//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAP/2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAP/cAAAAAAAAAAAAAAAA/7YAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAP/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/6AAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAkAFAAEAAAAAQACwAAABcA -BgAAAAAAAAAIAA4AAAAAAAsAEgAAAAAAAAATABkAAwANAAAAAQAJAAAAAAAAAAAAAAAAAAAAGAAAA -AAABwAAAAAAAAAAAAAAFQAFAAAAAAAYABgAAAAUAAAACgAAAAwAAgAPABEAFgAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAEAEQBdAAYAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAcAAAAAAAAABwAAAAAACAAAAAAAAAAAAAcAAAAHAAAAEwAJ -ABUADgAPAAAACwAQAAAAAAAAAAAAAAAAAAUAGAACAAIAAgAAAAIAGAAXAAAAGAAAABYAFgACABYAA -gAWAAAAEQADAAoAFAAMAA0ABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASAAAAEgAGAAEAHgAkAC -YAJwApACoALQAuAC8AMgAzADcAOAA5ADoAPAA9AEUASABOAE8AUgBTAFUAVwBZAFoAWwBcAF0AcwA -AAAAAAQAAAADa3tfFAAAAANAan9kAAAAA4QodoQ== -""" - ) - ), - 10 if size is None else size, - layout_engine=Layout.BASIC, - ) - else: - f = ImageFont() - f._load_pilfont_data( - # courB08 - BytesIO( - base64.b64decode( - b""" -UElMZm9udAo7Ozs7OzsxMDsKREFUQQoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAA//8AAQAAAAAAAAABAAEA -BgAAAAH/+gADAAAAAQAAAAMABgAGAAAAAf/6AAT//QADAAAABgADAAYAAAAA//kABQABAAYAAAAL -AAgABgAAAAD/+AAFAAEACwAAABAACQAGAAAAAP/5AAUAAAAQAAAAFQAHAAYAAP////oABQAAABUA -AAAbAAYABgAAAAH/+QAE//wAGwAAAB4AAwAGAAAAAf/5AAQAAQAeAAAAIQAIAAYAAAAB//kABAAB -ACEAAAAkAAgABgAAAAD/+QAE//0AJAAAACgABAAGAAAAAP/6AAX//wAoAAAALQAFAAYAAAAB//8A -BAACAC0AAAAwAAMABgAAAAD//AAF//0AMAAAADUAAQAGAAAAAf//AAMAAAA1AAAANwABAAYAAAAB -//kABQABADcAAAA7AAgABgAAAAD/+QAFAAAAOwAAAEAABwAGAAAAAP/5AAYAAABAAAAARgAHAAYA -AAAA//kABQAAAEYAAABLAAcABgAAAAD/+QAFAAAASwAAAFAABwAGAAAAAP/5AAYAAABQAAAAVgAH -AAYAAAAA//kABQAAAFYAAABbAAcABgAAAAD/+QAFAAAAWwAAAGAABwAGAAAAAP/5AAUAAABgAAAA -ZQAHAAYAAAAA//kABQAAAGUAAABqAAcABgAAAAD/+QAFAAAAagAAAG8ABwAGAAAAAf/8AAMAAABv -AAAAcQAEAAYAAAAA//wAAwACAHEAAAB0AAYABgAAAAD/+gAE//8AdAAAAHgABQAGAAAAAP/7AAT/ -/gB4AAAAfAADAAYAAAAB//oABf//AHwAAACAAAUABgAAAAD/+gAFAAAAgAAAAIUABgAGAAAAAP/5 -AAYAAQCFAAAAiwAIAAYAAP////oABgAAAIsAAACSAAYABgAA////+gAFAAAAkgAAAJgABgAGAAAA -AP/6AAUAAACYAAAAnQAGAAYAAP////oABQAAAJ0AAACjAAYABgAA////+gAFAAAAowAAAKkABgAG -AAD////6AAUAAACpAAAArwAGAAYAAAAA//oABQAAAK8AAAC0AAYABgAA////+gAGAAAAtAAAALsA -BgAGAAAAAP/6AAQAAAC7AAAAvwAGAAYAAP////oABQAAAL8AAADFAAYABgAA////+gAGAAAAxQAA -AMwABgAGAAD////6AAUAAADMAAAA0gAGAAYAAP////oABQAAANIAAADYAAYABgAA////+gAGAAAA -2AAAAN8ABgAGAAAAAP/6AAUAAADfAAAA5AAGAAYAAP////oABQAAAOQAAADqAAYABgAAAAD/+gAF -AAEA6gAAAO8ABwAGAAD////6AAYAAADvAAAA9gAGAAYAAAAA//oABQAAAPYAAAD7AAYABgAA//// -+gAFAAAA+wAAAQEABgAGAAD////6AAYAAAEBAAABCAAGAAYAAP////oABgAAAQgAAAEPAAYABgAA -////+gAGAAABDwAAARYABgAGAAAAAP/6AAYAAAEWAAABHAAGAAYAAP////oABgAAARwAAAEjAAYA -BgAAAAD/+gAFAAABIwAAASgABgAGAAAAAf/5AAQAAQEoAAABKwAIAAYAAAAA//kABAABASsAAAEv -AAgABgAAAAH/+QAEAAEBLwAAATIACAAGAAAAAP/5AAX//AEyAAABNwADAAYAAAAAAAEABgACATcA -AAE9AAEABgAAAAH/+QAE//wBPQAAAUAAAwAGAAAAAP/7AAYAAAFAAAABRgAFAAYAAP////kABQAA -AUYAAAFMAAcABgAAAAD/+wAFAAABTAAAAVEABQAGAAAAAP/5AAYAAAFRAAABVwAHAAYAAAAA//sA -BQAAAVcAAAFcAAUABgAAAAD/+QAFAAABXAAAAWEABwAGAAAAAP/7AAYAAgFhAAABZwAHAAYAAP// -//kABQAAAWcAAAFtAAcABgAAAAD/+QAGAAABbQAAAXMABwAGAAAAAP/5AAQAAgFzAAABdwAJAAYA -AP////kABgAAAXcAAAF+AAcABgAAAAD/+QAGAAABfgAAAYQABwAGAAD////7AAUAAAGEAAABigAF -AAYAAP////sABQAAAYoAAAGQAAUABgAAAAD/+wAFAAABkAAAAZUABQAGAAD////7AAUAAgGVAAAB -mwAHAAYAAAAA//sABgACAZsAAAGhAAcABgAAAAD/+wAGAAABoQAAAacABQAGAAAAAP/7AAYAAAGn -AAABrQAFAAYAAAAA//kABgAAAa0AAAGzAAcABgAA////+wAGAAABswAAAboABQAGAAD////7AAUA -AAG6AAABwAAFAAYAAP////sABgAAAcAAAAHHAAUABgAAAAD/+wAGAAABxwAAAc0ABQAGAAD////7 -AAYAAgHNAAAB1AAHAAYAAAAA//sABQAAAdQAAAHZAAUABgAAAAH/+QAFAAEB2QAAAd0ACAAGAAAA -Av/6AAMAAQHdAAAB3gAHAAYAAAAA//kABAABAd4AAAHiAAgABgAAAAD/+wAF//0B4gAAAecAAgAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAB -//sAAwACAecAAAHpAAcABgAAAAD/+QAFAAEB6QAAAe4ACAAGAAAAAP/5AAYAAAHuAAAB9AAHAAYA -AAAA//oABf//AfQAAAH5AAUABgAAAAD/+QAGAAAB+QAAAf8ABwAGAAAAAv/5AAMAAgH/AAACAAAJ -AAYAAAAA//kABQABAgAAAAIFAAgABgAAAAH/+gAE//sCBQAAAggAAQAGAAAAAP/5AAYAAAIIAAAC -DgAHAAYAAAAB//kABf/+Ag4AAAISAAUABgAA////+wAGAAACEgAAAhkABQAGAAAAAP/7AAX//gIZ -AAACHgADAAYAAAAA//wABf/9Ah4AAAIjAAEABgAAAAD/+QAHAAACIwAAAioABwAGAAAAAP/6AAT/ -+wIqAAACLgABAAYAAAAA//kABP/8Ai4AAAIyAAMABgAAAAD/+gAFAAACMgAAAjcABgAGAAAAAf/5 -AAT//QI3AAACOgAEAAYAAAAB//kABP/9AjoAAAI9AAQABgAAAAL/+QAE//sCPQAAAj8AAgAGAAD/ -///7AAYAAgI/AAACRgAHAAYAAAAA//kABgABAkYAAAJMAAgABgAAAAH//AAD//0CTAAAAk4AAQAG -AAAAAf//AAQAAgJOAAACUQADAAYAAAAB//kABP/9AlEAAAJUAAQABgAAAAH/+QAF//4CVAAAAlgA -BQAGAAD////7AAYAAAJYAAACXwAFAAYAAP////kABgAAAl8AAAJmAAcABgAA////+QAGAAACZgAA -Am0ABwAGAAD////5AAYAAAJtAAACdAAHAAYAAAAA//sABQACAnQAAAJ5AAcABgAA////9wAGAAAC -eQAAAoAACQAGAAD////3AAYAAAKAAAAChwAJAAYAAP////cABgAAAocAAAKOAAkABgAA////9wAG -AAACjgAAApUACQAGAAD////4AAYAAAKVAAACnAAIAAYAAP////cABgAAApwAAAKjAAkABgAA//// -+gAGAAACowAAAqoABgAGAAAAAP/6AAUAAgKqAAACrwAIAAYAAP////cABQAAAq8AAAK1AAkABgAA -////9wAFAAACtQAAArsACQAGAAD////3AAUAAAK7AAACwQAJAAYAAP////gABQAAAsEAAALHAAgA -BgAAAAD/9wAEAAACxwAAAssACQAGAAAAAP/3AAQAAALLAAACzwAJAAYAAAAA//cABAAAAs8AAALT -AAkABgAAAAD/+AAEAAAC0wAAAtcACAAGAAD////6AAUAAALXAAAC3QAGAAYAAP////cABgAAAt0A -AALkAAkABgAAAAD/9wAFAAAC5AAAAukACQAGAAAAAP/3AAUAAALpAAAC7gAJAAYAAAAA//cABQAA -Au4AAALzAAkABgAAAAD/9wAFAAAC8wAAAvgACQAGAAAAAP/4AAUAAAL4AAAC/QAIAAYAAAAA//oA -Bf//Av0AAAMCAAUABgAA////+gAGAAADAgAAAwkABgAGAAD////3AAYAAAMJAAADEAAJAAYAAP// -//cABgAAAxAAAAMXAAkABgAA////9wAGAAADFwAAAx4ACQAGAAD////4AAYAAAAAAAoABwASAAYA -AP////cABgAAAAcACgAOABMABgAA////+gAFAAAADgAKABQAEAAGAAD////6AAYAAAAUAAoAGwAQ -AAYAAAAA//gABgAAABsACgAhABIABgAAAAD/+AAGAAAAIQAKACcAEgAGAAAAAP/4AAYAAAAnAAoA -LQASAAYAAAAA//gABgAAAC0ACgAzABIABgAAAAD/+QAGAAAAMwAKADkAEQAGAAAAAP/3AAYAAAA5 -AAoAPwATAAYAAP////sABQAAAD8ACgBFAA8ABgAAAAD/+wAFAAIARQAKAEoAEQAGAAAAAP/4AAUA -AABKAAoATwASAAYAAAAA//gABQAAAE8ACgBUABIABgAAAAD/+AAFAAAAVAAKAFkAEgAGAAAAAP/5 -AAUAAABZAAoAXgARAAYAAAAA//gABgAAAF4ACgBkABIABgAAAAD/+AAGAAAAZAAKAGoAEgAGAAAA -AP/4AAYAAABqAAoAcAASAAYAAAAA//kABgAAAHAACgB2ABEABgAAAAD/+AAFAAAAdgAKAHsAEgAG -AAD////4AAYAAAB7AAoAggASAAYAAAAA//gABQAAAIIACgCHABIABgAAAAD/+AAFAAAAhwAKAIwA -EgAGAAAAAP/4AAUAAACMAAoAkQASAAYAAAAA//gABQAAAJEACgCWABIABgAAAAD/+QAFAAAAlgAK -AJsAEQAGAAAAAP/6AAX//wCbAAoAoAAPAAYAAAAA//oABQABAKAACgClABEABgAA////+AAGAAAA -pQAKAKwAEgAGAAD////4AAYAAACsAAoAswASAAYAAP////gABgAAALMACgC6ABIABgAA////+QAG -AAAAugAKAMEAEQAGAAD////4AAYAAgDBAAoAyAAUAAYAAP////kABQACAMgACgDOABMABgAA//// -+QAGAAIAzgAKANUAEw== -""" - ) - ), - Image.open( - BytesIO( - base64.b64decode( - b""" -iVBORw0KGgoAAAANSUhEUgAAAx4AAAAUAQAAAAArMtZoAAAEwElEQVR4nABlAJr/AHVE4czCI/4u -Mc4b7vuds/xzjz5/3/7u/n9vMe7vnfH/9++vPn/xyf5zhxzjt8GHw8+2d83u8x27199/nxuQ6Od9 -M43/5z2I+9n9ZtmDBwMQECDRQw/eQIQohJXxpBCNVE6QCCAAAAD//wBlAJr/AgALyj1t/wINwq0g -LeNZUworuN1cjTPIzrTX6ofHWeo3v336qPzfEwRmBnHTtf95/fglZK5N0PDgfRTslpGBvz7LFc4F -IUXBWQGjQ5MGCx34EDFPwXiY4YbYxavpnhHFrk14CDAAAAD//wBlAJr/AgKqRooH2gAgPeggvUAA -Bu2WfgPoAwzRAABAAAAAAACQgLz/3Uv4Gv+gX7BJgDeeGP6AAAD1NMDzKHD7ANWr3loYbxsAD791 -NAADfcoIDyP44K/jv4Y63/Z+t98Ovt+ub4T48LAAAAD//wBlAJr/AuplMlADJAAAAGuAphWpqhMx -in0A/fRvAYBABPgBwBUgABBQ/sYAyv9g0bCHgOLoGAAAAAAAREAAwI7nr0ArYpow7aX8//9LaP/9 -SjdavWA8ePHeBIKB//81/83ndznOaXx379wAAAD//wBlAJr/AqDxW+D3AABAAbUh/QMnbQag/gAY -AYDAAACgtgD/gOqAAAB5IA/8AAAk+n9w0AAA8AAAmFRJuPo27ciC0cD5oeW4E7KA/wD3ECMAn2tt -y8PgwH8AfAxFzC0JzeAMtratAsC/ffwAAAD//wBlAJr/BGKAyCAA4AAAAvgeYTAwHd1kmQF5chkG -ABoMIHcL5xVpTfQbUqzlAAAErwAQBgAAEOClA5D9il08AEh/tUzdCBsXkbgACED+woQg8Si9VeqY -lODCn7lmF6NhnAEYgAAA/NMIAAAAAAD//2JgjLZgVGBg5Pv/Tvpc8hwGBjYGJADjHDrAwPzAjv/H -/Wf3PzCwtzcwHmBgYGcwbZz8wHaCAQMDOwMDQ8MCBgYOC3W7mp+f0w+wHOYxO3OG+e376hsMZjk3 -AAAAAP//YmCMY2A4wMAIN5e5gQETPD6AZisDAwMDgzSDAAPjByiHcQMDAwMDg1nOze1lByRu5/47 -c4859311AYNZzg0AAAAA//9iYGDBYihOIIMuwIjGL39/fwffA8b//xv/P2BPtzzHwCBjUQAAAAD/ -/yLFBrIBAAAA//9i1HhcwdhizX7u8NZNzyLbvT97bfrMf/QHI8evOwcSqGUJAAAA//9iYBB81iSw -pEE170Qrg5MIYydHqwdDQRMrAwcVrQAAAAD//2J4x7j9AAMDn8Q/BgYLBoaiAwwMjPdvMDBYM1Tv -oJodAAAAAP//Yqo/83+dxePWlxl3npsel9lvLfPcqlE9725C+acfVLMEAAAA//9i+s9gwCoaaGMR -evta/58PTEWzr21hufPjA8N+qlnBwAAAAAD//2JiWLci5v1+HmFXDqcnULE/MxgYGBj+f6CaJQAA -AAD//2Ji2FrkY3iYpYC5qDeGgeEMAwPDvwQBBoYvcTwOVLMEAAAA//9isDBgkP///0EOg9z35v// -Gc/eeW7BwPj5+QGZhANUswMAAAD//2JgqGBgYGBgqEMXlvhMPUsAAAAA//8iYDd1AAAAAP//AwDR -w7IkEbzhVQAAAABJRU5ErkJggg== -""" - ) - ) - ), - ) - return f +# +# The Python Imaging Library. +# $Id$ +# +# PIL raster font management +# +# History: +# 1996-08-07 fl created (experimental) +# 1997-08-25 fl minor adjustments to handle fonts from pilfont 0.3 +# 1999-02-06 fl rewrote most font management stuff in C +# 1999-03-17 fl take pth files into account in load_path (from Richard Jones) +# 2001-02-17 fl added freetype support +# 2001-05-09 fl added TransposedFont wrapper class +# 2002-03-04 fl make sure we have a "L" or "1" font +# 2002-12-04 fl skip non-directory entries in the system path +# 2003-04-29 fl add embedded default font +# 2003-09-27 fl added support for truetype charmap encodings +# +# Todo: +# Adapt to PILFONT2 format (16-bit fonts, compressed, single file) +# +# Copyright (c) 1997-2003 by Secret Labs AB +# Copyright (c) 1996-2003 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# + +from __future__ import annotations + +import base64 +import os +import sys +import warnings +from enum import IntEnum +from io import BytesIO +from typing import IO, TYPE_CHECKING, Any, BinaryIO + +from . import Image +from ._typing import StrOrBytesPath +from ._util import is_path + +if TYPE_CHECKING: + from . import ImageFile + from ._imaging import ImagingFont + from ._imagingft import Font + + +class Layout(IntEnum): + BASIC = 0 + RAQM = 1 + + +MAX_STRING_LENGTH = 1_000_000 + + +try: + from . import _imagingft as core +except ImportError as ex: + from ._util import DeferredError + + core = DeferredError.new(ex) + + +def _string_length_check(text: str | bytes | bytearray) -> None: + if MAX_STRING_LENGTH is not None and len(text) > MAX_STRING_LENGTH: + msg = "too many characters in string" + raise ValueError(msg) + + +# FIXME: add support for pilfont2 format (see FontFile.py) + +# -------------------------------------------------------------------- +# Font metrics format: +# "PILfont" LF +# fontdescriptor LF +# (optional) key=value... LF +# "DATA" LF +# binary data: 256*10*2 bytes (dx, dy, dstbox, srcbox) +# +# To place a character, cut out srcbox and paste at dstbox, +# relative to the character position. Then move the character +# position according to dx, dy. +# -------------------------------------------------------------------- + + +class ImageFont: + """PIL font wrapper""" + + font: ImagingFont + + def _load_pilfont(self, filename: str) -> None: + with open(filename, "rb") as fp: + image: ImageFile.ImageFile | None = None + for ext in (".png", ".gif", ".pbm"): + if image: + image.close() + try: + fullname = os.path.splitext(filename)[0] + ext + image = Image.open(fullname) + except Exception: + pass + else: + if image and image.mode in ("1", "L"): + break + else: + if image: + image.close() + msg = "cannot find glyph data file" + raise OSError(msg) + + self.file = fullname + + self._load_pilfont_data(fp, image) + image.close() + + def _load_pilfont_data(self, file: IO[bytes], image: Image.Image) -> None: + # read PILfont header + if file.readline() != b"PILfont\n": + msg = "Not a PILfont file" + raise SyntaxError(msg) + file.readline().split(b";") + self.info = [] # FIXME: should be a dictionary + while True: + s = file.readline() + if not s or s == b"DATA\n": + break + self.info.append(s) + + # read PILfont metrics + data = file.read(256 * 20) + + # check image + if image.mode not in ("1", "L"): + msg = "invalid font image mode" + raise TypeError(msg) + + image.load() + + self.font = Image.core.font(image.im, data) + + def getmask(self, text, mode="", *args, **kwargs): + """ + Create a bitmap for the text. + + If the font uses antialiasing, the bitmap should have mode ``L`` and use a + maximum value of 255. Otherwise, it should have mode ``1``. + + :param text: Text to render. + :param mode: Used by some graphics drivers to indicate what mode the + driver prefers; if empty, the renderer may return either + mode. Note that the mode is always a string, to simplify + C-level implementations. + + .. versionadded:: 1.1.5 + + :return: An internal PIL storage memory instance as defined by the + :py:mod:`PIL.Image.core` interface module. + """ + _string_length_check(text) + Image._decompression_bomb_check(self.font.getsize(text)) + return self.font.getmask(text, mode) + + def getbbox( + self, text: str | bytes | bytearray, *args: Any, **kwargs: Any + ) -> tuple[int, int, int, int]: + """ + Returns bounding box (in pixels) of given text. + + .. versionadded:: 9.2.0 + + :param text: Text to render. + + :return: ``(left, top, right, bottom)`` bounding box + """ + _string_length_check(text) + width, height = self.font.getsize(text) + return 0, 0, width, height + + def getlength( + self, text: str | bytes | bytearray, *args: Any, **kwargs: Any + ) -> int: + """ + Returns length (in pixels) of given text. + This is the amount by which following text should be offset. + + .. versionadded:: 9.2.0 + """ + _string_length_check(text) + width, height = self.font.getsize(text) + return width + + +## +# Wrapper for FreeType fonts. Application code should use the +# truetype factory function to create font objects. + + +class FreeTypeFont: + """FreeType font wrapper (requires _imagingft service)""" + + font: Font + + def __init__( + self, + font: StrOrBytesPath | BinaryIO | None = None, + size: float = 10, + index: int = 0, + encoding: str = "", + layout_engine: Layout | None = None, + ) -> None: + # FIXME: use service provider instead + + if size <= 0: + msg = "font size must be greater than 0" + raise ValueError(msg) + + self.path = font + self.size = size + self.index = index + self.encoding = encoding + + if layout_engine not in (Layout.BASIC, Layout.RAQM): + layout_engine = Layout.BASIC + if core.HAVE_RAQM: + layout_engine = Layout.RAQM + elif layout_engine == Layout.RAQM and not core.HAVE_RAQM: + warnings.warn( + "Raqm layout was requested, but Raqm is not available. " + "Falling back to basic layout." + ) + layout_engine = Layout.BASIC + + self.layout_engine = layout_engine + + def load_from_bytes(f): + self.font_bytes = f.read() + self.font = core.getfont( + "", size, index, encoding, self.font_bytes, layout_engine + ) + + if is_path(font): + font = os.path.realpath(os.fspath(font)) + if sys.platform == "win32": + font_bytes_path = font if isinstance(font, bytes) else font.encode() + try: + font_bytes_path.decode("ascii") + except UnicodeDecodeError: + # FreeType cannot load fonts with non-ASCII characters on Windows + # So load it into memory first + with open(font, "rb") as f: + load_from_bytes(f) + return + self.font = core.getfont( + font, size, index, encoding, layout_engine=layout_engine + ) + else: + load_from_bytes(font) + + def __getstate__(self): + return [self.path, self.size, self.index, self.encoding, self.layout_engine] + + def __setstate__(self, state): + path, size, index, encoding, layout_engine = state + self.__init__(path, size, index, encoding, layout_engine) + + def getname(self) -> tuple[str | None, str | None]: + """ + :return: A tuple of the font family (e.g. Helvetica) and the font style + (e.g. Bold) + """ + return self.font.family, self.font.style + + def getmetrics(self) -> tuple[int, int]: + """ + :return: A tuple of the font ascent (the distance from the baseline to + the highest outline point) and descent (the distance from the + baseline to the lowest outline point, a negative value) + """ + return self.font.ascent, self.font.descent + + def getlength( + self, text: str, mode="", direction=None, features=None, language=None + ) -> float: + """ + Returns length (in pixels with 1/64 precision) of given text when rendered + in font with provided direction, features, and language. + + This is the amount by which following text should be offset. + Text bounding box may extend past the length in some fonts, + e.g. when using italics or accents. + + The result is returned as a float; it is a whole number if using basic layout. + + Note that the sum of two lengths may not equal the length of a concatenated + string due to kerning. If you need to adjust for kerning, include the following + character and subtract its length. + + For example, instead of :: + + hello = font.getlength("Hello") + world = font.getlength("World") + hello_world = hello + world # not adjusted for kerning + assert hello_world == font.getlength("HelloWorld") # may fail + + use :: + + hello = font.getlength("HelloW") - font.getlength("W") # adjusted for kerning + world = font.getlength("World") + hello_world = hello + world # adjusted for kerning + assert hello_world == font.getlength("HelloWorld") # True + + or disable kerning with (requires libraqm) :: + + hello = draw.textlength("Hello", font, features=["-kern"]) + world = draw.textlength("World", font, features=["-kern"]) + hello_world = hello + world # kerning is disabled, no need to adjust + assert hello_world == draw.textlength("HelloWorld", font, features=["-kern"]) + + .. versionadded:: 8.0.0 + + :param text: Text to measure. + :param mode: Used by some graphics drivers to indicate what mode the + driver prefers; if empty, the renderer may return either + mode. Note that the mode is always a string, to simplify + C-level implementations. + + :param direction: Direction of the text. It can be 'rtl' (right to + left), 'ltr' (left to right) or 'ttb' (top to bottom). + Requires libraqm. + + :param features: A list of OpenType font features to be used during text + layout. This is usually used to turn on optional + font features that are not enabled by default, + for example 'dlig' or 'ss01', but can be also + used to turn off default font features for + example '-liga' to disable ligatures or '-kern' + to disable kerning. To get all supported + features, see + https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist + Requires libraqm. + + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP 47 language code + `_ + Requires libraqm. + + :return: Either width for horizontal text, or height for vertical text. + """ + _string_length_check(text) + return self.font.getlength(text, mode, direction, features, language) / 64 + + def getbbox( + self, + text: str, + mode: str = "", + direction: str | None = None, + features: list[str] | None = None, + language: str | None = None, + stroke_width: float = 0, + anchor: str | None = None, + ) -> tuple[float, float, float, float]: + """ + Returns bounding box (in pixels) of given text relative to given anchor + when rendered in font with provided direction, features, and language. + + Use :py:meth:`getlength()` to get the offset of following text with + 1/64 pixel precision. The bounding box includes extra margins for + some fonts, e.g. italics or accents. + + .. versionadded:: 8.0.0 + + :param text: Text to render. + :param mode: Used by some graphics drivers to indicate what mode the + driver prefers; if empty, the renderer may return either + mode. Note that the mode is always a string, to simplify + C-level implementations. + + :param direction: Direction of the text. It can be 'rtl' (right to + left), 'ltr' (left to right) or 'ttb' (top to bottom). + Requires libraqm. + + :param features: A list of OpenType font features to be used during text + layout. This is usually used to turn on optional + font features that are not enabled by default, + for example 'dlig' or 'ss01', but can be also + used to turn off default font features for + example '-liga' to disable ligatures or '-kern' + to disable kerning. To get all supported + features, see + https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist + Requires libraqm. + + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP 47 language code + `_ + Requires libraqm. + + :param stroke_width: The width of the text stroke. + + :param anchor: The text anchor alignment. Determines the relative location of + the anchor to the text. The default alignment is top left, + specifically ``la`` for horizontal text and ``lt`` for + vertical text. See :ref:`text-anchors` for details. + + :return: ``(left, top, right, bottom)`` bounding box + """ + _string_length_check(text) + size, offset = self.font.getsize( + text, mode, direction, features, language, anchor + ) + left, top = offset[0] - stroke_width, offset[1] - stroke_width + width, height = size[0] + 2 * stroke_width, size[1] + 2 * stroke_width + return left, top, left + width, top + height + + def getmask( + self, + text, + mode="", + direction=None, + features=None, + language=None, + stroke_width=0, + anchor=None, + ink=0, + start=None, + ): + """ + Create a bitmap for the text. + + If the font uses antialiasing, the bitmap should have mode ``L`` and use a + maximum value of 255. If the font has embedded color data, the bitmap + should have mode ``RGBA``. Otherwise, it should have mode ``1``. + + :param text: Text to render. + :param mode: Used by some graphics drivers to indicate what mode the + driver prefers; if empty, the renderer may return either + mode. Note that the mode is always a string, to simplify + C-level implementations. + + .. versionadded:: 1.1.5 + + :param direction: Direction of the text. It can be 'rtl' (right to + left), 'ltr' (left to right) or 'ttb' (top to bottom). + Requires libraqm. + + .. versionadded:: 4.2.0 + + :param features: A list of OpenType font features to be used during text + layout. This is usually used to turn on optional + font features that are not enabled by default, + for example 'dlig' or 'ss01', but can be also + used to turn off default font features for + example '-liga' to disable ligatures or '-kern' + to disable kerning. To get all supported + features, see + https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist + Requires libraqm. + + .. versionadded:: 4.2.0 + + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP 47 language code + `_ + Requires libraqm. + + .. versionadded:: 6.0.0 + + :param stroke_width: The width of the text stroke. + + .. versionadded:: 6.2.0 + + :param anchor: The text anchor alignment. Determines the relative location of + the anchor to the text. The default alignment is top left, + specifically ``la`` for horizontal text and ``lt`` for + vertical text. See :ref:`text-anchors` for details. + + .. versionadded:: 8.0.0 + + :param ink: Foreground ink for rendering in RGBA mode. + + .. versionadded:: 8.0.0 + + :param start: Tuple of horizontal and vertical offset, as text may render + differently when starting at fractional coordinates. + + .. versionadded:: 9.4.0 + + :return: An internal PIL storage memory instance as defined by the + :py:mod:`PIL.Image.core` interface module. + """ + return self.getmask2( + text, + mode, + direction=direction, + features=features, + language=language, + stroke_width=stroke_width, + anchor=anchor, + ink=ink, + start=start, + )[0] + + def getmask2( + self, + text: str, + mode="", + direction=None, + features=None, + language=None, + stroke_width=0, + anchor=None, + ink=0, + start=None, + *args, + **kwargs, + ): + """ + Create a bitmap for the text. + + If the font uses antialiasing, the bitmap should have mode ``L`` and use a + maximum value of 255. If the font has embedded color data, the bitmap + should have mode ``RGBA``. Otherwise, it should have mode ``1``. + + :param text: Text to render. + :param mode: Used by some graphics drivers to indicate what mode the + driver prefers; if empty, the renderer may return either + mode. Note that the mode is always a string, to simplify + C-level implementations. + + .. versionadded:: 1.1.5 + + :param direction: Direction of the text. It can be 'rtl' (right to + left), 'ltr' (left to right) or 'ttb' (top to bottom). + Requires libraqm. + + .. versionadded:: 4.2.0 + + :param features: A list of OpenType font features to be used during text + layout. This is usually used to turn on optional + font features that are not enabled by default, + for example 'dlig' or 'ss01', but can be also + used to turn off default font features for + example '-liga' to disable ligatures or '-kern' + to disable kerning. To get all supported + features, see + https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist + Requires libraqm. + + .. versionadded:: 4.2.0 + + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP 47 language code + `_ + Requires libraqm. + + .. versionadded:: 6.0.0 + + :param stroke_width: The width of the text stroke. + + .. versionadded:: 6.2.0 + + :param anchor: The text anchor alignment. Determines the relative location of + the anchor to the text. The default alignment is top left, + specifically ``la`` for horizontal text and ``lt`` for + vertical text. See :ref:`text-anchors` for details. + + .. versionadded:: 8.0.0 + + :param ink: Foreground ink for rendering in RGBA mode. + + .. versionadded:: 8.0.0 + + :param start: Tuple of horizontal and vertical offset, as text may render + differently when starting at fractional coordinates. + + .. versionadded:: 9.4.0 + + :return: A tuple of an internal PIL storage memory instance as defined by the + :py:mod:`PIL.Image.core` interface module, and the text offset, the + gap between the starting coordinate and the first marking + """ + _string_length_check(text) + if start is None: + start = (0, 0) + + def fill(width, height): + size = (width, height) + Image._decompression_bomb_check(size) + return Image.core.fill("RGBA" if mode == "RGBA" else "L", size) + + return self.font.render( + text, + fill, + mode, + direction, + features, + language, + stroke_width, + anchor, + ink, + start[0], + start[1], + ) + + def font_variant( + self, font=None, size=None, index=None, encoding=None, layout_engine=None + ): + """ + Create a copy of this FreeTypeFont object, + using any specified arguments to override the settings. + + Parameters are identical to the parameters used to initialize this + object. + + :return: A FreeTypeFont object. + """ + if font is None: + try: + font = BytesIO(self.font_bytes) + except AttributeError: + font = self.path + return FreeTypeFont( + font=font, + size=self.size if size is None else size, + index=self.index if index is None else index, + encoding=self.encoding if encoding is None else encoding, + layout_engine=layout_engine or self.layout_engine, + ) + + def get_variation_names(self) -> list[bytes]: + """ + :returns: A list of the named styles in a variation font. + :exception OSError: If the font is not a variation font. + """ + try: + names = self.font.getvarnames() + except AttributeError as e: + msg = "FreeType 2.9.1 or greater is required" + raise NotImplementedError(msg) from e + return [name.replace(b"\x00", b"") for name in names] + + def set_variation_by_name(self, name): + """ + :param name: The name of the style. + :exception OSError: If the font is not a variation font. + """ + names = self.get_variation_names() + if not isinstance(name, bytes): + name = name.encode() + index = names.index(name) + 1 + + if index == getattr(self, "_last_variation_index", None): + # When the same name is set twice in a row, + # there is an 'unknown freetype error' + # https://savannah.nongnu.org/bugs/?56186 + return + self._last_variation_index = index + + self.font.setvarname(index) + + def get_variation_axes(self): + """ + :returns: A list of the axes in a variation font. + :exception OSError: If the font is not a variation font. + """ + try: + axes = self.font.getvaraxes() + except AttributeError as e: + msg = "FreeType 2.9.1 or greater is required" + raise NotImplementedError(msg) from e + for axis in axes: + if axis["name"]: + axis["name"] = axis["name"].replace(b"\x00", b"") + return axes + + def set_variation_by_axes(self, axes: list[float]) -> None: + """ + :param axes: A list of values for each axis. + :exception OSError: If the font is not a variation font. + """ + try: + self.font.setvaraxes(axes) + except AttributeError as e: + msg = "FreeType 2.9.1 or greater is required" + raise NotImplementedError(msg) from e + + +class TransposedFont: + """Wrapper for writing rotated or mirrored text""" + + def __init__(self, font, orientation=None): + """ + Wrapper that creates a transposed font from any existing font + object. + + :param font: A font object. + :param orientation: An optional orientation. If given, this should + be one of Image.Transpose.FLIP_LEFT_RIGHT, Image.Transpose.FLIP_TOP_BOTTOM, + Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_180, or + Image.Transpose.ROTATE_270. + """ + self.font = font + self.orientation = orientation # any 'transpose' argument, or None + + def getmask(self, text, mode="", *args, **kwargs): + im = self.font.getmask(text, mode, *args, **kwargs) + if self.orientation is not None: + return im.transpose(self.orientation) + return im + + def getbbox(self, text, *args, **kwargs): + # TransposedFont doesn't support getmask2, move top-left point to (0, 0) + # this has no effect on ImageFont and simulates anchor="lt" for FreeTypeFont + left, top, right, bottom = self.font.getbbox(text, *args, **kwargs) + width = right - left + height = bottom - top + if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270): + return 0, 0, height, width + return 0, 0, width, height + + def getlength(self, text: str, *args, **kwargs) -> float: + if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270): + msg = "text length is undefined for text rotated by 90 or 270 degrees" + raise ValueError(msg) + return self.font.getlength(text, *args, **kwargs) + + +def load(filename: str) -> ImageFont: + """ + Load a font file. This function loads a font object from the given + bitmap font file, and returns the corresponding font object. + + :param filename: Name of font file. + :return: A font object. + :exception OSError: If the file could not be read. + """ + f = ImageFont() + f._load_pilfont(filename) + return f + + +def truetype( + font: StrOrBytesPath | BinaryIO | None = None, + size: float = 10, + index: int = 0, + encoding: str = "", + layout_engine: Layout | None = None, +) -> FreeTypeFont: + """ + Load a TrueType or OpenType font from a file or file-like object, + and create a font object. + This function loads a font object from the given file or file-like + object, and creates a font object for a font of the given size. + + Pillow uses FreeType to open font files. On Windows, be aware that FreeType + will keep the file open as long as the FreeTypeFont object exists. Windows + limits the number of files that can be open in C at once to 512, so if many + fonts are opened simultaneously and that limit is approached, an + ``OSError`` may be thrown, reporting that FreeType "cannot open resource". + A workaround would be to copy the file(s) into memory, and open that instead. + + This function requires the _imagingft service. + + :param font: A filename or file-like object containing a TrueType font. + If the file is not found in this filename, the loader may also + search in other directories, such as the :file:`fonts/` + directory on Windows or :file:`/Library/Fonts/`, + :file:`/System/Library/Fonts/` and :file:`~/Library/Fonts/` on + macOS. + + :param size: The requested size, in pixels. + :param index: Which font face to load (default is first available face). + :param encoding: Which font encoding to use (default is Unicode). Possible + encodings include (see the FreeType documentation for more + information): + + * "unic" (Unicode) + * "symb" (Microsoft Symbol) + * "ADOB" (Adobe Standard) + * "ADBE" (Adobe Expert) + * "ADBC" (Adobe Custom) + * "armn" (Apple Roman) + * "sjis" (Shift JIS) + * "gb " (PRC) + * "big5" + * "wans" (Extended Wansung) + * "joha" (Johab) + * "lat1" (Latin-1) + + This specifies the character set to use. It does not alter the + encoding of any text provided in subsequent operations. + :param layout_engine: Which layout engine to use, if available: + :attr:`.ImageFont.Layout.BASIC` or :attr:`.ImageFont.Layout.RAQM`. + If it is available, Raqm layout will be used by default. + Otherwise, basic layout will be used. + + Raqm layout is recommended for all non-English text. If Raqm layout + is not required, basic layout will have better performance. + + You can check support for Raqm layout using + :py:func:`PIL.features.check_feature` with ``feature="raqm"``. + + .. versionadded:: 4.2.0 + :return: A font object. + :exception OSError: If the file could not be read. + :exception ValueError: If the font size is not greater than zero. + """ + + def freetype(font: StrOrBytesPath | BinaryIO | None) -> FreeTypeFont: + return FreeTypeFont(font, size, index, encoding, layout_engine) + + try: + return freetype(font) + except OSError: + if not is_path(font): + raise + ttf_filename = os.path.basename(font) + + dirs = [] + if sys.platform == "win32": + # check the windows font repository + # NOTE: must use uppercase WINDIR, to work around bugs in + # 1.5.2's os.environ.get() + windir = os.environ.get("WINDIR") + if windir: + dirs.append(os.path.join(windir, "fonts")) + elif sys.platform in ("linux", "linux2"): + lindirs = os.environ.get("XDG_DATA_DIRS") + if not lindirs: + # According to the freedesktop spec, XDG_DATA_DIRS should + # default to /usr/share + lindirs = "/usr/share" + dirs += [os.path.join(lindir, "fonts") for lindir in lindirs.split(":")] + elif sys.platform == "darwin": + dirs += [ + "/Library/Fonts", + "/System/Library/Fonts", + os.path.expanduser("~/Library/Fonts"), + ] + + ext = os.path.splitext(ttf_filename)[1] + first_font_with_a_different_extension = None + for directory in dirs: + for walkroot, walkdir, walkfilenames in os.walk(directory): + for walkfilename in walkfilenames: + if ext and walkfilename == ttf_filename: + return freetype(os.path.join(walkroot, walkfilename)) + elif not ext and os.path.splitext(walkfilename)[0] == ttf_filename: + fontpath = os.path.join(walkroot, walkfilename) + if os.path.splitext(fontpath)[1] == ".ttf": + return freetype(fontpath) + if not ext and first_font_with_a_different_extension is None: + first_font_with_a_different_extension = fontpath + if first_font_with_a_different_extension: + return freetype(first_font_with_a_different_extension) + raise + + +def load_path(filename: str | bytes) -> ImageFont: + """ + Load font file. Same as :py:func:`~PIL.ImageFont.load`, but searches for a + bitmap font along the Python path. + + :param filename: Name of font file. + :return: A font object. + :exception OSError: If the file could not be read. + """ + if not isinstance(filename, str): + filename = filename.decode("utf-8") + for directory in sys.path: + try: + return load(os.path.join(directory, filename)) + except OSError: + pass + msg = "cannot find font file" + raise OSError(msg) + + +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. + + Otherwise, load a "better than nothing" font. + + .. versionadded:: 1.1.4 + + :param size: The font size of Aileron Regular. + + .. versionadded:: 10.1.0 + + :return: A font object. + """ + f: FreeTypeFont | ImageFont + if core.__class__.__name__ == "module" or size is not None: + f = truetype( + BytesIO( + base64.b64decode( + b""" +AAEAAAAPAIAAAwBwRkZUTYwDlUAAADFoAAAAHEdERUYAqADnAAAo8AAAACRHUE9ThhmITwAAKfgAA +AduR1NVQnHxefoAACkUAAAA4k9TLzJovoHLAAABeAAAAGBjbWFw5lFQMQAAA6gAAAGqZ2FzcP//AA +MAACjoAAAACGdseWYmRXoPAAAGQAAAHfhoZWFkE18ayQAAAPwAAAA2aGhlYQboArEAAAE0AAAAJGh +tdHjjERZ8AAAB2AAAAdBsb2NhuOexrgAABVQAAADqbWF4cAC7AEYAAAFYAAAAIG5hbWUr+h5lAAAk +OAAAA6Jwb3N0D3oPTQAAJ9wAAAEKAAEAAAABGhxJDqIhXw889QALA+gAAAAA0Bqf2QAAAADhCh2h/ +2r/LgOxAyAAAAAIAAIAAAAAAAAAAQAAA8r/GgAAA7j/av9qA7EAAQAAAAAAAAAAAAAAAAAAAHQAAQ +AAAHQAQwAFAAAAAAACAAAAAQABAAAAQAAAAAAAAAADAfoBkAAFAAgCigJYAAAASwKKAlgAAAFeADI +BPgAAAAAFAAAAAAAAAAAAAAcAAAAAAAAAAAAAAABVS1dOAEAAIPsCAwL/GgDIA8oA5iAAAJMAAAAA +AhICsgAAACAAAwH0AAAAAAAAAU0AAADYAAAA8gA5AVMAVgJEAEYCRAA1AuQAKQKOAEAAsAArATsAZ +AE7AB4CMABVAkQAUADc/+EBEgAgANwAJQEv//sCRAApAkQAggJEADwCRAAtAkQAIQJEADkCRAArAk +QAMgJEACwCRAAxANwAJQDc/+ECRABnAkQAUAJEAEQB8wAjA1QANgJ/AB0CcwBkArsALwLFAGQCSwB +kAjcAZALGAC8C2gBkAQgAZAIgADcCYQBkAj8AZANiAGQCzgBkAuEALwJWAGQC3QAvAmsAZAJJADQC +ZAAiAqoAXgJuACADuAAaAnEAGQJFABMCTwAuATMAYgEv//sBJwAiAkQAUAH0ADIBLAApAhMAJAJjA +EoCEQAeAmcAHgIlAB4BIgAVAmcAHgJRAEoA7gA+AOn/8wIKAEoA9wBGA1cASgJRAEoCSgAeAmMASg +JnAB4BSgBKAcsAGAE5ABQCUABCAgIAAQMRAAEB4v/6AgEAAQHOABQBLwBAAPoAYAEvACECRABNA0Y +AJAItAHgBKgAcAkQAUAEsAHQAygAgAi0AOQD3ADYA9wAWAaEANgGhABYCbAAlAYMAeAGDADkA6/9q +AhsAFAIKABUB/QAVAAAAAwAAAAMAAAAcAAEAAAAAAKQAAwABAAAAHAAEAIgAAAAeABAAAwAOAH4Aq +QCrALEAtAC3ALsgGSAdICYgOiBEISL7Av//AAAAIACpAKsAsAC0ALcAuyAYIBwgJiA5IEQhIvsB// +//4/+5/7j/tP+y/7D/reBR4E/gR+A14CzfTwVxAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAEGAAABAAAAAAAAAAECAAAAAgAAAAAAAAAAAAAAAAAAAAEAAAMEBQYHCAkKCwwNDg8QERIT +FBUWFxgZGhscHR4fICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj9AQUJDREVGR0hJSktMT +U5PUFFSU1RVVldYWVpbXF1eX2BhAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGQAAA +AAAAAAYnFmAAAAAABlAAAAAAAAAAAAAAAAAAAAAAAAAAAAY2htAAAAAAAAAABrbGlqAAAAAHAAbm9 +ycwBnAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAmACYAJgAmAD4AUgCCAMoBCgFO +AVwBcgGIAaYBvAHKAdYB6AH2AgwCIAJKAogCpgLWAw4DIgNkA5wDugPUA+gD/AQQBEYEogS8BPoFJ +gVSBWoFgAWwBcoF1gX6BhQGJAZMBmgGiga0BuIHGgdUB2YHkAeiB8AH3AfyCAoIHAgqCDoITghcCG +oIogjSCPoJKglYCXwJwgnqCgIKKApACl4Klgq8CtwLDAs8C1YLjAuyC9oL7gwMDCYMSAxgDKAMrAz +qDQoNTA1mDYQNoA2uDcAN2g3oDfYODA4iDkoOXA5sDnoOnA7EDvwAAAAFAAAAAAH0ArwAAwAGAAkA +DAAPAAAxESERAxMhExcRASELARETAfT6qv6syKr+jgFUqsiqArz9RAGLAP/+1P8B/v3VAP8BLP4CA +P8AAgA5//IAuQKyAAMACwAANyMDMwIyFhQGIiY0oE4MZk84JCQ4JLQB/v3AJDgkJDgAAgBWAeUBPA +LfAAMABwAAEyMnMxcjJzOmRgpagkYKWgHl+vr6AAAAAAIARgAAAf4CsgAbAB8AAAEHMxUjByM3Iwc +jNyM1MzcjNTM3MwczNzMHMxUrAQczAZgdZXEvOi9bLzovWmYdZXEvOi9bLzovWp9bHlsBn4w429vb +2ziMONvb29s4jAAAAAMANf+mAg4DDAAfACYALAAAJRQGBxUjNS4BJzMeARcRLgE0Njc1MxUeARcjJ +icVHgEBFBYXNQ4BExU+ATU0Ag5xWDpgcgRcBz41Xl9oVTpVYwpcC1ttXP6cLTQuM5szOrVRZwlOTQ +ZqVzZECAEAGlukZAlOTQdrUG8O7iNlAQgxNhDlCDj+8/YGOjReAAAAAAUAKf/yArsCvAAHAAsAFQA +dACcAABIyFhQGIiY0EyMBMwQiBhUUFjI2NTQSMhYUBiImNDYiBhUUFjI2NTR5iFBQiFCVVwHAV/5c +OiMjOiPmiFBQiFCxOiMjOiMCvFaSVlaS/ZoCsjIzMC80NC8w/uNWklZWkhozMC80NC8wAAAAAgBA/ +/ICbgLAACIALgAAARUjEQYjIiY1NDY3LgE1NDYzMhcVJiMiBhUUFhcWOwE1MxUFFBYzMjc1IyIHDg +ECbmBcYYOOVkg7R4hsQjY4Q0RNRD4SLDxW/pJUXzksPCkUUk0BgUb+zBVUZ0BkDw5RO1huCkULQzp +COAMBcHDHRz0J/AIHRQAAAAEAKwHlAIUC3wADAAATIycze0YKWgHl+gAAAAABAGT/sAEXAwwACQAA +EzMGEBcjLgE0Nt06dXU6OUBAAwzG/jDGVePs4wAAAAEAHv+wANEDDAAJAAATMx4BFAYHIzYQHjo5Q +EA5OnUDDFXj7ONVxgHQAAAAAQBVAFIB2wHbAA4AAAE3FwcXBycHJzcnNxcnMwEtmxOfcTJjYzJxnx +ObCj4BKD07KYolmZkliik7PbMAAQBQAFUB9AIlAAsAAAEjFSM1IzUzNTMVMwH0tTq1tTq1AR/Kyjj +OzgAAAAAB/+H/iACMAGQABAAANwcjNzOMWlFOXVrS3AAAAQAgAP8A8gE3AAMAABMjNTPy0tIA/zgA +AQAl//IApQByAAcAADYyFhQGIiY0STgkJDgkciQ4JCQ4AAAAAf/7/+IBNALQAAMAABcjEzM5Pvs+H +gLuAAAAAAIAKf/yAhsCwAADAAcAABIgECA2IBAgKQHy/g5gATL+zgLA/TJEAkYAAAAAAQCCAAABlg +KyAAgAAAERIxEHNTc2MwGWVr6SIygCsv1OAldxW1sWAAEAPAAAAg4CwAAZAAA3IRUhNRM+ATU0JiM +iDwEjNz4BMzIWFRQGB7kBUv4x+kI2QTt+EAFWAQp8aGVtSl5GRjEA/0RVLzlLmAoKa3FsUkNxXQAA +AAEALf/yAhYCwAAqAAABHgEVFAYjIi8BMxceATMyNjU0KwE1MzI2NTQmIyIGDwEjNz4BMzIWFRQGA +YxBSZJo2RUBVgEHV0JBUaQREUBUQzc5TQcBVgEKfGhfcEMBbxJbQl1x0AoKRkZHPn9GSD80QUVCCg +pfbGBPOlgAAAACACEAAAIkArIACgAPAAAlIxUjNSE1ATMRMyMRBg8BAiRXVv6qAVZWV60dHLCurq4 +rAdn+QgFLMibzAAABADn/8gIZArIAHQAAATIWFRQGIyIvATMXFjMyNjU0JiMiByMTIRUhBzc2ATNv +d5Fl1RQBVgIad0VSTkVhL1IwAYj+vh8rMAHHgGdtgcUKCoFXTU5bYgGRRvAuHQAAAAACACv/8gITA +sAAFwAjAAABMhYVFAYjIhE0NjMyFh8BIycmIyIDNzYTMjY1NCYjIgYVFBYBLmp7imr0l3RZdAgBXA +IYZ5wKJzU6QVNJSz5SUAHSgWltiQFGxcNlVQoKdv7sPiz+ZF1LTmJbU0lhAAAAAQAyAAACGgKyAAY +AAAEVASMBITUCGv6oXAFL/oECsij9dgJsRgAAAAMALP/xAhgCwAAWACAALAAAAR4BFRQGIyImNTQ2 +Ny4BNTQ2MhYVFAYmIgYVFBYyNjU0AzI2NTQmIyIGFRQWAZQ5S5BmbIpPOjA7ecp5P2F8Q0J8RIVJS +0pLTEtOAW0TXTxpZ2ZqPF0SE1A3VWVlVTdQ/UU0N0RENzT9/ko+Ok1NOj1LAAIAMf/yAhkCwAAXAC +MAAAEyERQGIyImLwEzFxYzMhMHBiMiJjU0NhMyNjU0JiMiBhUUFgEl9Jd0WXQIAVwCGGecCic1SWp +7imo+UlBAQVNJAsD+usXDZVUKCnYBFD4sgWltif5kW1NJYV1LTmIAAAACACX/8gClAiAABwAPAAAS +MhYUBiImNBIyFhQGIiY0STgkJDgkJDgkJDgkAiAkOCQkOP52JDgkJDgAAAAC/+H/iAClAiAABwAMA +AASMhYUBiImNBMHIzczSTgkJDgkaFpSTl4CICQ4JCQ4/mba5gAAAQBnAB4B+AH0AAYAAAENARUlNS +UB+P6qAVb+bwGRAbCmpkbJRMkAAAIAUAC7AfQBuwADAAcAAAEhNSERITUhAfT+XAGk/lwBpAGDOP8 +AOAABAEQAHgHVAfQABgAAARUFNS0BNQHV/m8BVv6qAStEyUSmpkYAAAAAAgAj//IB1ALAABgAIAAA +ATIWFRQHDgEHIz4BNz4BNTQmIyIGByM+ARIyFhQGIiY0AQRibmktIAJWBSEqNig+NTlHBFoDezQ4J +CQ4JALAZ1BjaS03JS1DMD5LLDQ/SUVgcv2yJDgkJDgAAAAAAgA2/5gDFgKYADYAQgAAAQMGFRQzMj +Y1NCYjIg4CFRQWMzI2NxcGIyImNTQ+AjMyFhUUBiMiJwcGIyImNTQ2MzIfATcHNzYmIyIGFRQzMjY +Cej8EJjJJlnBAfGQ+oHtAhjUYg5OPx0h2k06Os3xRWQsVLjY5VHtdPBwJETcJDyUoOkZEJz8B0f74 +EQ8kZl6EkTFZjVOLlyknMVm1pmCiaTq4lX6CSCknTVRmmR8wPdYnQzxuSWVGAAIAHQAAAncCsgAHA +AoAACUjByMTMxMjATMDAcj+UVz4dO5d/sjPZPT0ArL9TgE6ATQAAAADAGQAAAJMArIAEAAbACcAAA +EeARUUBgcGKwERMzIXFhUUJRUzMjc2NTQnJiMTPgE1NCcmKwEVMzIBvkdHZkwiNt7LOSGq/oeFHBt +hahIlSTM+cB8Yj5UWAW8QT0VYYgwFArIEF5Fv1eMED2NfDAL93AU+N24PBP0AAAAAAQAv//ICjwLA +ABsAAAEyFh8BIycmIyIGFRQWMzI/ATMHDgEjIiY1NDYBdX+PCwFWAiKiaHx5ZaIiAlYBCpWBk6a0A +sCAagoKpqN/gaOmCgplhcicn8sAAAIAZAAAAp8CsgAMABkAAAEeARUUBgcGKwERMzITPgE1NCYnJi +sBETMyAY59lJp8IzXN0jUVWmdjWRs5d3I4Aq4QqJWUug8EArL9mQ+PeHGHDgX92gAAAAABAGQAAAI +vArIACwAAJRUhESEVIRUhFSEVAi/+NQHB/pUBTf6zRkYCskbwRvAAAAABAGQAAAIlArIACQAAExUh +FSERIxEhFboBQ/69VgHBAmzwRv7KArJGAAAAAAEAL//yAo8CwAAfAAABMxEjNQcGIyImNTQ2MzIWH +wEjJyYjIgYVFBYzMjY1IwGP90wfPnWTprSSf48LAVYCIqJofHllVG+hAU3+s3hARsicn8uAagoKpq +N/gaN1XAAAAAEAZAAAAowCsgALAAABESMRIREjETMRIRECjFb+hFZWAXwCsv1OAS7+0gKy/sQBPAA +AAAABAGQAAAC6ArIAAwAAMyMRM7pWVgKyAAABADf/8gHoArIAEwAAAREUBw4BIyImLwEzFxYzMjc2 +NREB6AIFcGpgbQIBVgIHfXQKAQKy/lYxIltob2EpKYyEFD0BpwAAAAABAGQAAAJ0ArIACwAACQEjA +wcVIxEzEQEzATsBJ3ntQlZWAVVlAWH+nwEnR+ACsv6RAW8AAQBkAAACLwKyAAUAACUVIREzEQIv/j +VWRkYCsv2UAAABAGQAAAMUArIAFAAAAREjETQ3BgcDIwMmJxYVESMRMxsBAxRWAiMxemx8NxsCVo7 +MywKy/U4BY7ZLco7+nAFmoFxLtP6dArL9lwJpAAAAAAEAZAAAAoACsgANAAAhIwEWFREjETMBJjUR +MwKAhP67A1aEAUUDVAJeeov+pwKy/aJ5jAFZAAAAAgAv//ICuwLAAAkAEwAAEiAWFRQGICY1NBIyN +jU0JiIGFRTbATSsrP7MrNrYenrYegLAxaKhxsahov47nIeIm5uIhwACAGQAAAJHArIADgAYAAABHg +EVFAYHBisBESMRMzITNjQnJisBETMyAZRUX2VOHzuAVtY7GlxcGDWIiDUCrgtnVlVpCgT+5gKy/rU +V1BUF/vgAAAACAC//zAK9AsAAEgAcAAAlFhcHJiMiBwYjIiY1NDYgFhUUJRQWMjY1NCYiBgI9PUMx +UDcfKh8omqysATSs/dR62Hp62HpICTg7NgkHxqGixcWitbWHnJyHiJubAAIAZAAAAlgCsgAXACMAA +CUWFyMmJyYnJisBESMRMzIXHgEVFAYHFiUzMjc+ATU0JyYrAQIqDCJfGQwNWhAhglbiOx9QXEY1Tv +6bhDATMj1lGSyMtYgtOXR0BwH+1wKyBApbU0BSESRAAgVAOGoQBAABADT/8gIoAsAAJQAAATIWFyM +uASMiBhUUFhceARUUBiMiJiczHgEzMjY1NCYnLgE1NDYBOmd2ClwGS0E6SUNRdW+HZnKKC1wPWkQ9 +Uk1cZGuEAsBwXUJHNjQ3OhIbZVZZbm5kREo+NT5DFRdYUFdrAAAAAAEAIgAAAmQCsgAHAAABIxEjE +SM1IQJk9lb2AkICbP2UAmxGAAEAXv/yAmQCsgAXAAABERQHDgEiJicmNREzERQXHgEyNjc2NRECZA +IIgfCBCAJWAgZYmlgGAgKy/k0qFFxzc1wUKgGz/lUrEkRQUEQSKwGrAAAAAAEAIAAAAnoCsgAGAAA +hIwMzGwEzAYJ07l3N1FwCsv2PAnEAAAEAGgAAA7ECsgAMAAABAyMLASMDMxsBMxsBA7HAcZyicrZi +kaB0nJkCsv1OAlP9rQKy/ZsCW/2kAmYAAAEAGQAAAm8CsgALAAAhCwEjEwMzGwEzAxMCCsrEY/bkY +re+Y/D6AST+3AFcAVb+5gEa/q3+oQAAAQATAAACUQKyAAgAAAERIxEDMxsBMwFdVvRjwLphARD+8A +EQAaL+sQFPAAABAC4AAAI5ArIACQAAJRUhNQEhNSEVAQI5/fUBof57Aen+YUZGQgIqRkX92QAAAAA +BAGL/sAEFAwwABwAAARUjETMVIxEBBWlpowMMOP0UOANcAAAB//v/4gE0AtAAAwAABSMDMwE0Pvs+ +HgLuAAAAAQAi/7AAxQMMAAcAABcjNTMRIzUzxaNpaaNQOALsOAABAFAA1wH0AmgABgAAJQsBIxMzE +wGwjY1GsESw1wFZ/qcBkf5vAAAAAQAy/6oBwv/iAAMAAAUhNSEBwv5wAZBWOAAAAAEAKQJEALYCsg +ADAAATIycztjhVUAJEbgAAAAACACT/8gHQAiAAHQAlAAAhJwcGIyImNTQ2OwE1NCcmIyIHIz4BMzI +XFh0BFBcnMjY9ASYVFAF6CR0wVUtgkJoiAgdgaQlaBm1Zrg4DCuQ9R+5MOSFQR1tbDiwUUXBUXowf +J8c9SjRORzYSgVwAAAAAAgBK//ICRQLfABEAHgAAATIWFRQGIyImLwEVIxEzETc2EzI2NTQmIyIGH +QEUFgFUcYCVbiNJEyNWVigySElcU01JXmECIJd4i5QTEDRJAt/+3jkq/hRuZV55ZWsdX14AAQAe// +IB9wIgABgAAAEyFhcjJiMiBhUUFjMyNjczDgEjIiY1NDYBF152DFocbEJXU0A1Rw1aE3pbaoKQAiB +oWH5qZm1tPDlaXYuLgZcAAAACAB7/8gIZAt8AEQAeAAABESM1BwYjIiY1NDYzMhYfAREDMjY9ATQm +IyIGFRQWAhlWKDJacYCVbiNJEyOnSV5hQUlcUwLf/SFVOSqXeIuUExA0ARb9VWVrHV9ebmVeeQACA +B7/8gH9AiAAFQAbAAABFAchHgEzMjY3Mw4BIyImNTQ2MzIWJyIGByEmAf0C/oAGUkA1SwlaD4FXbI +WObmt45UBVBwEqDQEYFhNjWD84W16Oh3+akU9aU60AAAEAFQAAARoC8gAWAAATBh0BMxUjESMRIzU +zNTQ3PgEzMhcVJqcDbW1WOTkDB0k8Hx5oAngVITRC/jQBzEIsJRs5PwVHEwAAAAIAHv8uAhkCIAAi +AC8AAAERFAcOASMiLwEzFx4BMzI2NzY9AQcGIyImNTQ2MzIWHwE1AzI2PQE0JiMiBhUUFgIZAQSEd +NwRAVcBBU5DTlUDASgyWnGAlW4jSRMjp0leYUFJXFMCEv5wSh1zeq8KCTI8VU0ZIQk5Kpd4i5QTED +RJ/iJlax1fXm5lXnkAAQBKAAACCgLkABcAAAEWFREjETQnLgEHDgEdASMRMxE3NjMyFgIIAlYCBDs +6RVRWViE5UVViAYUbQP7WASQxGzI7AQJyf+kC5P7TPSxUAAACAD4AAACsAsAABwALAAASMhYUBiIm +NBMjETNeLiAgLiBiVlYCwCAuICAu/WACEgAC//P/LgCnAsAABwAVAAASMhYUBiImNBcRFAcGIyInN +RY3NjURWS4gIC4gYgMLcRwNSgYCAsAgLiAgLo79wCUbZAJGBzMOHgJEAAAAAQBKAAACCALfAAsAAC +EnBxUjETMREzMHEwGTwTJWVvdu9/rgN6kC3/4oAQv6/ugAAQBG//wA3gLfAA8AABMRFBceATcVBiM +iJicmNRGcAQIcIxkkKi4CAQLf/bkhERoSBD4EJC8SNAJKAAAAAQBKAAADEAIgACQAAAEWFREjETQn +JiMiFREjETQnJiMiFREjETMVNzYzMhYXNzYzMhYDCwVWBAxedFYEDF50VlYiJko7ThAvJkpEVAGfI +jn+vAEcQyRZ1v76ARxDJFnW/voCEk08HzYtRB9HAAAAAAEASgAAAgoCIAAWAAABFhURIxE0JyYjIg +YdASMRMxU3NjMyFgIIAlYCCXBEVVZWITlRVWIBhRtA/tYBJDEbbHR/6QISWz0sVAAAAAACAB7/8gI +sAiAABwARAAASIBYUBiAmNBIyNjU0JiIGFRSlAQCHh/8Ah7ieWlqeWgIgn/Cfn/D+s3ZfYHV1YF8A +AgBK/zwCRQIgABEAHgAAATIWFRQGIyImLwERIxEzFTc2EzI2NTQmIyIGHQEUFgFUcYCVbiNJEyNWV +igySElcU01JXmECIJd4i5QTEDT+8wLWVTkq/hRuZV55ZWsdX14AAgAe/zwCGQIgABEAHgAAAREjEQ +cGIyImNTQ2MzIWHwE1AzI2PQE0JiMiBhUUFgIZVigyWnGAlW4jSRMjp0leYUFJXFMCEv0qARk5Kpd +4i5QTEDRJ/iJlax1fXm5lXnkAAQBKAAABPgIeAA0AAAEyFxUmBhURIxEzFTc2ARoWDkdXVlYwIwIe +B0EFVlf+0gISU0cYAAEAGP/yAa0CIAAjAAATMhYXIyYjIgYVFBYXHgEVFAYjIiYnMxYzMjY1NCYnL +gE1NDbkV2MJWhNdKy04PF1XbVhWbgxaE2ktOjlEUllkAiBaS2MrJCUoEBlPQkhOVFZoKCUmLhIWSE +BIUwAAAAEAFP/4ARQCiQAXAAATERQXHgE3FQYjIiYnJjURIzUzNTMVMxWxAQMmMx8qMjMEAUdHVmM +BzP7PGw4mFgY/BSwxDjQBNUJ7e0IAAAABAEL/8gICAhIAFwAAAREjNQcGIyImJyY1ETMRFBceATMy +Nj0BAgJWITlRT2EKBVYEBkA1RFECEv3uWj4qTToiOQE+/tIlJC43c4DpAAAAAAEAAQAAAfwCEgAGA +AABAyMDMxsBAfzJaclfop8CEv3uAhL+LQHTAAABAAEAAAMLAhIADAAAAQMjCwEjAzMbATMbAQMLqW +Z2dmapY3t0a3Z7AhL97gG+/kICEv5AAcD+QwG9AAAB//oAAAHWAhIACwAAARMjJwcjEwMzFzczARq +8ZIuKY763ZoWFYwEO/vLV1QEMAQbNzQAAAQAB/y4B+wISABEAAAEDDgEjIic1FjMyNj8BAzMbAQH7 +2iFZQB8NDRIpNhQH02GenQIS/cFVUAJGASozEwIt/i4B0gABABQAAAGxAg4ACQAAJRUhNQEhNSEVA +QGx/mMBNP7iAYL+zkREQgGIREX+ewAAAAABAED/sAEOAwwALAAAASMiBhUUFxYVFAYHHgEVFAcGFR +QWOwEVIyImNTQ3NjU0JzU2NTQnJjU0NjsBAQ4MKiMLDS4pKS4NCyMqDAtERAwLUlILDERECwLUGBk +WTlsgKzUFBTcrIFtOFhkYOC87GFVMIkUIOAhFIkxVGDsvAAAAAAEAYP84AJoDIAADAAAXIxEzmjo6 +yAPoAAEAIf+wAO8DDAAsAAATFQYVFBcWFRQGKwE1MzI2NTQnJjU0NjcuATU0NzY1NCYrATUzMhYVF +AcGFRTvUgsMREQLDCojCw0uKSkuDQsjKgwLREQMCwF6OAhFIkxVGDsvOBgZFk5bICs1BQU3KyBbTh +YZGDgvOxhVTCJFAAABAE0A3wH2AWQAEwAAATMUIyImJyYjIhUjNDMyFhcWMzIBvjhuGywtQR0xOG4 +bLC1BHTEBZIURGCNMhREYIwAAAwAk/94DIgLoAAcAEQApAAAAIBYQBiAmECQgBhUUFiA2NTQlMhYX +IyYjIgYUFjMyNjczDgEjIiY1NDYBAQFE3d3+vN0CB/7wubkBELn+xVBnD1wSWDo+QTcqOQZcEmZWX +HN2Aujg/rbg4AFKpr+Mjb6+jYxbWEldV5ZZNShLVn5na34AAgB4AFIB9AGeAAUACwAAAQcXIyc3Mw +cXIyc3AUqJiUmJifOJiUmJiQGepqampqampqYAAAIAHAHSAQ4CwAAHAA8AABIyFhQGIiY0NiIGFBY +yNjRgakREakSTNCEhNCECwEJqQkJqCiM4IyM4AAAAAAIAUAAAAfQCCwALAA8AAAEzFSMVIzUjNTM1 +MxMhNSEBP7W1OrW1OrX+XAGkAVs4tLQ4sP31OAAAAQB0AkQBAQKyAAMAABMjNzOsOD1QAkRuAAAAA +AEAIADsAKoBdgAHAAASMhYUBiImNEg6KCg6KAF2KDooKDoAAAIAOQBSAbUBngAFAAsAACUHIzcnMw +UHIzcnMwELiUmJiUkBM4lJiYlJ+KampqampqYAAAABADYB5QDhAt8ABAAAEzczByM2Xk1OXQHv8Po +AAQAWAeUAwQLfAAQAABMHIzczwV5NTl0C1fD6AAIANgHlAYsC3wAEAAkAABM3MwcjPwEzByM2Xk1O +XapeTU5dAe/w+grw+gAAAgAWAeUBawLfAAQACQAAEwcjNzMXByM3M8FeTU5dql5NTl0C1fD6CvD6A +AADACX/8gI1AHIABwAPABcAADYyFhQGIiY0NjIWFAYiJjQ2MhYUBiImNEk4JCQ4JOw4JCQ4JOw4JC +Q4JHIkOCQkOCQkOCQkOCQkOCQkOAAAAAEAeABSAUoBngAFAAABBxcjJzcBSomJSYmJAZ6mpqamAAA +AAAEAOQBSAQsBngAFAAAlByM3JzMBC4lJiYlJ+KampgAAAf9qAAABgQKyAAMAACsBATM/VwHAVwKy +AAAAAAIAFAHIAdwClAAHABQAABMVIxUjNSM1BRUjNwcjJxcjNTMXN9pKMkoByDICKzQqATJLKysCl +CmjoykBy46KiY3Lm5sAAQAVAAABvALyABgAAAERIxEjESMRIzUzNTQ3NjMyFxUmBgcGHQEBvFbCVj +k5AxHHHx5iVgcDAg798gHM/jQBzEIOJRuWBUcIJDAVIRYAAAABABX//AHkAvIAJQAAJR4BNxUGIyI +mJyY1ESYjIgcGHQEzFSMRIxEjNTM1NDc2MzIXERQBowIcIxkkKi4CAR4nXgwDbW1WLy8DEbNdOmYa +EQQ/BCQvEjQCFQZWFSEWQv40AcxCDiUblhP9uSEAAAAAAAAWAQ4AAQAAAAAAAAATACgAAQAAAAAAA +QAHAEwAAQAAAAAAAgAHAGQAAQAAAAAAAwAaAKIAAQAAAAAABAAHAM0AAQAAAAAABQA8AU8AAQAAAA +AABgAPAawAAQAAAAAACAALAdQAAQAAAAAACQALAfgAAQAAAAAACwAXAjQAAQAAAAAADAAXAnwAAwA +BBAkAAAAmAAAAAwABBAkAAQAOADwAAwABBAkAAgAOAFQAAwABBAkAAwA0AGwAAwABBAkABAAOAL0A +AwABBAkABQB4ANUAAwABBAkABgAeAYwAAwABBAkACAAWAbwAAwABBAkACQAWAeAAAwABBAkACwAuA +gQAAwABBAkADAAuAkwATgBvACAAUgBpAGcAaAB0AHMAIABSAGUAcwBlAHIAdgBlAGQALgAATm8gUm +lnaHRzIFJlc2VydmVkLgAAQQBpAGwAZQByAG8AbgAAQWlsZXJvbgAAUgBlAGcAdQBsAGEAcgAAUmV +ndWxhcgAAMQAuADEAMAAyADsAVQBLAFcATgA7AEEAaQBsAGUAcgBvAG4ALQBSAGUAZwB1AGwAYQBy +AAAxLjEwMjtVS1dOO0FpbGVyb24tUmVndWxhcgAAQQBpAGwAZQByAG8AbgAAQWlsZXJvbgAAVgBlA +HIAcwBpAG8AbgAgADEALgAxADAAMgA7AFAAUwAgADAAMAAxAC4AMQAwADIAOwBoAG8AdABjAG8Abg +B2ACAAMQAuADAALgA3ADAAOwBtAGEAawBlAG8AdABmAC4AbABpAGIAMgAuADUALgA1ADgAMwAyADk +AAFZlcnNpb24gMS4xMDI7UFMgMDAxLjEwMjtob3Rjb252IDEuMC43MDttYWtlb3RmLmxpYjIuNS41 +ODMyOQAAQQBpAGwAZQByAG8AbgAtAFIAZQBnAHUAbABhAHIAAEFpbGVyb24tUmVndWxhcgAAUwBvA +HIAYQAgAFMAYQBnAGEAbgBvAABTb3JhIFNhZ2FubwAAUwBvAHIAYQAgAFMAYQBnAGEAbgBvAABTb3 +JhIFNhZ2FubwAAaAB0AHQAcAA6AC8ALwB3AHcAdwAuAGQAbwB0AGMAbwBsAG8AbgAuAG4AZQB0AAB +odHRwOi8vd3d3LmRvdGNvbG9uLm5ldAAAaAB0AHQAcAA6AC8ALwB3AHcAdwAuAGQAbwB0AGMAbwBs +AG8AbgAuAG4AZQB0AABodHRwOi8vd3d3LmRvdGNvbG9uLm5ldAAAAAACAAAAAAAA/4MAMgAAAAAAA +AAAAAAAAAAAAAAAAAAAAHQAAAABAAIAAwAEAAUABgAHAAgACQAKAAsADAANAA4ADwAQABEAEgATAB +QAFQAWABcAGAAZABoAGwAcAB0AHgAfACAAIQAiACMAJAAlACYAJwAoACkAKgArACwALQAuAC8AMAA +xADIAMwA0ADUANgA3ADgAOQA6ADsAPAA9AD4APwBAAEEAQgBDAEQARQBGAEcASABJAEoASwBMAE0A +TgBPAFAAUQBSAFMAVABVAFYAVwBYAFkAWgBbAFwAXQBeAF8AYABhAIsAqQCDAJMAjQDDAKoAtgC3A +LQAtQCrAL4AvwC8AIwAwADBAAAAAAAB//8AAgABAAAADAAAABwAAAACAAIAAwBxAAEAcgBzAAIABA +AAAAIAAAABAAAACgBMAGYAAkRGTFQADmxhdG4AGgAEAAAAAP//AAEAAAAWAANDQVQgAB5NT0wgABZ +ST00gABYAAP//AAEAAAAA//8AAgAAAAEAAmxpZ2EADmxvY2wAFAAAAAEAAQAAAAEAAAACAAYAEAAG +AAAAAgASADQABAAAAAEATAADAAAAAgAQABYAAQAcAAAAAQABAE8AAQABAGcAAQABAE8AAwAAAAIAE +AAWAAEAHAAAAAEAAQAvAAEAAQBnAAEAAQAvAAEAGgABAAgAAgAGAAwAcwACAE8AcgACAEwAAQABAE +kAAAABAAAACgBGAGAAAkRGTFQADmxhdG4AHAAEAAAAAP//AAIAAAABABYAA0NBVCAAFk1PTCAAFlJ +PTSAAFgAA//8AAgAAAAEAAmNwc3AADmtlcm4AFAAAAAEAAAAAAAEAAQACAAYADgABAAAAAQASAAIA +AAACAB4ANgABAAoABQAFAAoAAgABACQAPQAAAAEAEgAEAAAAAQAMAAEAOP/nAAEAAQAkAAIGigAEA +AAFJAXKABoAGQAA//gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAD/sv+4/+z/7v/MAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAD/xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/9T/6AAAAAD/8QAA +ABD/vQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/7gAAAAAAAAAAAAAAAAAA//MAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABIAAAAAAAAAAP/5AAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/gAAD/4AAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//L/9AAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAA/+gAAAAAAAkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/zAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/mAAAAAAAAAAAAAAAAAAD +/4gAA//AAAAAA//YAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/+AAAAAAAAP/OAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/zv/qAAAAAP/0AAAACAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/ZAAD/egAA/1kAAAAA/5D/rgAAAAAAAAAAAA +AAAAAAAAAAAAAAAAD/9AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAD/8AAA/7b/8P+wAAD/8P/E/98AAAAA/8P/+P/0//oAAAAAAAAAAAAA//gA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/+AAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/w//C/9MAAP/SAAD/9wAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAD/yAAA/+kAAAAA//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/9wAAAAD//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAP/2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAP/cAAAAAAAAAAAAAAAA/7YAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAP/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/6AAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAkAFAAEAAAAAQACwAAABcA +BgAAAAAAAAAIAA4AAAAAAAsAEgAAAAAAAAATABkAAwANAAAAAQAJAAAAAAAAAAAAAAAAAAAAGAAAA +AAABwAAAAAAAAAAAAAAFQAFAAAAAAAYABgAAAAUAAAACgAAAAwAAgAPABEAFgAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAEAEQBdAAYAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAcAAAAAAAAABwAAAAAACAAAAAAAAAAAAAcAAAAHAAAAEwAJ +ABUADgAPAAAACwAQAAAAAAAAAAAAAAAAAAUAGAACAAIAAgAAAAIAGAAXAAAAGAAAABYAFgACABYAA +gAWAAAAEQADAAoAFAAMAA0ABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASAAAAEgAGAAEAHgAkAC +YAJwApACoALQAuAC8AMgAzADcAOAA5ADoAPAA9AEUASABOAE8AUgBTAFUAVwBZAFoAWwBcAF0AcwA +AAAAAAQAAAADa3tfFAAAAANAan9kAAAAA4QodoQ== +""" + ) + ), + 10 if size is None else size, + layout_engine=Layout.BASIC, + ) + else: + f = ImageFont() + f._load_pilfont_data( + # courB08 + BytesIO( + base64.b64decode( + b""" +UElMZm9udAo7Ozs7OzsxMDsKREFUQQoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAA//8AAQAAAAAAAAABAAEA +BgAAAAH/+gADAAAAAQAAAAMABgAGAAAAAf/6AAT//QADAAAABgADAAYAAAAA//kABQABAAYAAAAL +AAgABgAAAAD/+AAFAAEACwAAABAACQAGAAAAAP/5AAUAAAAQAAAAFQAHAAYAAP////oABQAAABUA +AAAbAAYABgAAAAH/+QAE//wAGwAAAB4AAwAGAAAAAf/5AAQAAQAeAAAAIQAIAAYAAAAB//kABAAB +ACEAAAAkAAgABgAAAAD/+QAE//0AJAAAACgABAAGAAAAAP/6AAX//wAoAAAALQAFAAYAAAAB//8A +BAACAC0AAAAwAAMABgAAAAD//AAF//0AMAAAADUAAQAGAAAAAf//AAMAAAA1AAAANwABAAYAAAAB +//kABQABADcAAAA7AAgABgAAAAD/+QAFAAAAOwAAAEAABwAGAAAAAP/5AAYAAABAAAAARgAHAAYA +AAAA//kABQAAAEYAAABLAAcABgAAAAD/+QAFAAAASwAAAFAABwAGAAAAAP/5AAYAAABQAAAAVgAH +AAYAAAAA//kABQAAAFYAAABbAAcABgAAAAD/+QAFAAAAWwAAAGAABwAGAAAAAP/5AAUAAABgAAAA +ZQAHAAYAAAAA//kABQAAAGUAAABqAAcABgAAAAD/+QAFAAAAagAAAG8ABwAGAAAAAf/8AAMAAABv +AAAAcQAEAAYAAAAA//wAAwACAHEAAAB0AAYABgAAAAD/+gAE//8AdAAAAHgABQAGAAAAAP/7AAT/ +/gB4AAAAfAADAAYAAAAB//oABf//AHwAAACAAAUABgAAAAD/+gAFAAAAgAAAAIUABgAGAAAAAP/5 +AAYAAQCFAAAAiwAIAAYAAP////oABgAAAIsAAACSAAYABgAA////+gAFAAAAkgAAAJgABgAGAAAA +AP/6AAUAAACYAAAAnQAGAAYAAP////oABQAAAJ0AAACjAAYABgAA////+gAFAAAAowAAAKkABgAG +AAD////6AAUAAACpAAAArwAGAAYAAAAA//oABQAAAK8AAAC0AAYABgAA////+gAGAAAAtAAAALsA +BgAGAAAAAP/6AAQAAAC7AAAAvwAGAAYAAP////oABQAAAL8AAADFAAYABgAA////+gAGAAAAxQAA +AMwABgAGAAD////6AAUAAADMAAAA0gAGAAYAAP////oABQAAANIAAADYAAYABgAA////+gAGAAAA +2AAAAN8ABgAGAAAAAP/6AAUAAADfAAAA5AAGAAYAAP////oABQAAAOQAAADqAAYABgAAAAD/+gAF +AAEA6gAAAO8ABwAGAAD////6AAYAAADvAAAA9gAGAAYAAAAA//oABQAAAPYAAAD7AAYABgAA//// ++gAFAAAA+wAAAQEABgAGAAD////6AAYAAAEBAAABCAAGAAYAAP////oABgAAAQgAAAEPAAYABgAA +////+gAGAAABDwAAARYABgAGAAAAAP/6AAYAAAEWAAABHAAGAAYAAP////oABgAAARwAAAEjAAYA +BgAAAAD/+gAFAAABIwAAASgABgAGAAAAAf/5AAQAAQEoAAABKwAIAAYAAAAA//kABAABASsAAAEv +AAgABgAAAAH/+QAEAAEBLwAAATIACAAGAAAAAP/5AAX//AEyAAABNwADAAYAAAAAAAEABgACATcA +AAE9AAEABgAAAAH/+QAE//wBPQAAAUAAAwAGAAAAAP/7AAYAAAFAAAABRgAFAAYAAP////kABQAA +AUYAAAFMAAcABgAAAAD/+wAFAAABTAAAAVEABQAGAAAAAP/5AAYAAAFRAAABVwAHAAYAAAAA//sA +BQAAAVcAAAFcAAUABgAAAAD/+QAFAAABXAAAAWEABwAGAAAAAP/7AAYAAgFhAAABZwAHAAYAAP// +//kABQAAAWcAAAFtAAcABgAAAAD/+QAGAAABbQAAAXMABwAGAAAAAP/5AAQAAgFzAAABdwAJAAYA +AP////kABgAAAXcAAAF+AAcABgAAAAD/+QAGAAABfgAAAYQABwAGAAD////7AAUAAAGEAAABigAF +AAYAAP////sABQAAAYoAAAGQAAUABgAAAAD/+wAFAAABkAAAAZUABQAGAAD////7AAUAAgGVAAAB +mwAHAAYAAAAA//sABgACAZsAAAGhAAcABgAAAAD/+wAGAAABoQAAAacABQAGAAAAAP/7AAYAAAGn +AAABrQAFAAYAAAAA//kABgAAAa0AAAGzAAcABgAA////+wAGAAABswAAAboABQAGAAD////7AAUA +AAG6AAABwAAFAAYAAP////sABgAAAcAAAAHHAAUABgAAAAD/+wAGAAABxwAAAc0ABQAGAAD////7 +AAYAAgHNAAAB1AAHAAYAAAAA//sABQAAAdQAAAHZAAUABgAAAAH/+QAFAAEB2QAAAd0ACAAGAAAA +Av/6AAMAAQHdAAAB3gAHAAYAAAAA//kABAABAd4AAAHiAAgABgAAAAD/+wAF//0B4gAAAecAAgAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAB +//sAAwACAecAAAHpAAcABgAAAAD/+QAFAAEB6QAAAe4ACAAGAAAAAP/5AAYAAAHuAAAB9AAHAAYA +AAAA//oABf//AfQAAAH5AAUABgAAAAD/+QAGAAAB+QAAAf8ABwAGAAAAAv/5AAMAAgH/AAACAAAJ +AAYAAAAA//kABQABAgAAAAIFAAgABgAAAAH/+gAE//sCBQAAAggAAQAGAAAAAP/5AAYAAAIIAAAC +DgAHAAYAAAAB//kABf/+Ag4AAAISAAUABgAA////+wAGAAACEgAAAhkABQAGAAAAAP/7AAX//gIZ +AAACHgADAAYAAAAA//wABf/9Ah4AAAIjAAEABgAAAAD/+QAHAAACIwAAAioABwAGAAAAAP/6AAT/ ++wIqAAACLgABAAYAAAAA//kABP/8Ai4AAAIyAAMABgAAAAD/+gAFAAACMgAAAjcABgAGAAAAAf/5 +AAT//QI3AAACOgAEAAYAAAAB//kABP/9AjoAAAI9AAQABgAAAAL/+QAE//sCPQAAAj8AAgAGAAD/ +///7AAYAAgI/AAACRgAHAAYAAAAA//kABgABAkYAAAJMAAgABgAAAAH//AAD//0CTAAAAk4AAQAG +AAAAAf//AAQAAgJOAAACUQADAAYAAAAB//kABP/9AlEAAAJUAAQABgAAAAH/+QAF//4CVAAAAlgA +BQAGAAD////7AAYAAAJYAAACXwAFAAYAAP////kABgAAAl8AAAJmAAcABgAA////+QAGAAACZgAA +Am0ABwAGAAD////5AAYAAAJtAAACdAAHAAYAAAAA//sABQACAnQAAAJ5AAcABgAA////9wAGAAAC +eQAAAoAACQAGAAD////3AAYAAAKAAAAChwAJAAYAAP////cABgAAAocAAAKOAAkABgAA////9wAG +AAACjgAAApUACQAGAAD////4AAYAAAKVAAACnAAIAAYAAP////cABgAAApwAAAKjAAkABgAA//// ++gAGAAACowAAAqoABgAGAAAAAP/6AAUAAgKqAAACrwAIAAYAAP////cABQAAAq8AAAK1AAkABgAA +////9wAFAAACtQAAArsACQAGAAD////3AAUAAAK7AAACwQAJAAYAAP////gABQAAAsEAAALHAAgA +BgAAAAD/9wAEAAACxwAAAssACQAGAAAAAP/3AAQAAALLAAACzwAJAAYAAAAA//cABAAAAs8AAALT +AAkABgAAAAD/+AAEAAAC0wAAAtcACAAGAAD////6AAUAAALXAAAC3QAGAAYAAP////cABgAAAt0A +AALkAAkABgAAAAD/9wAFAAAC5AAAAukACQAGAAAAAP/3AAUAAALpAAAC7gAJAAYAAAAA//cABQAA +Au4AAALzAAkABgAAAAD/9wAFAAAC8wAAAvgACQAGAAAAAP/4AAUAAAL4AAAC/QAIAAYAAAAA//oA +Bf//Av0AAAMCAAUABgAA////+gAGAAADAgAAAwkABgAGAAD////3AAYAAAMJAAADEAAJAAYAAP// +//cABgAAAxAAAAMXAAkABgAA////9wAGAAADFwAAAx4ACQAGAAD////4AAYAAAAAAAoABwASAAYA +AP////cABgAAAAcACgAOABMABgAA////+gAFAAAADgAKABQAEAAGAAD////6AAYAAAAUAAoAGwAQ +AAYAAAAA//gABgAAABsACgAhABIABgAAAAD/+AAGAAAAIQAKACcAEgAGAAAAAP/4AAYAAAAnAAoA +LQASAAYAAAAA//gABgAAAC0ACgAzABIABgAAAAD/+QAGAAAAMwAKADkAEQAGAAAAAP/3AAYAAAA5 +AAoAPwATAAYAAP////sABQAAAD8ACgBFAA8ABgAAAAD/+wAFAAIARQAKAEoAEQAGAAAAAP/4AAUA +AABKAAoATwASAAYAAAAA//gABQAAAE8ACgBUABIABgAAAAD/+AAFAAAAVAAKAFkAEgAGAAAAAP/5 +AAUAAABZAAoAXgARAAYAAAAA//gABgAAAF4ACgBkABIABgAAAAD/+AAGAAAAZAAKAGoAEgAGAAAA +AP/4AAYAAABqAAoAcAASAAYAAAAA//kABgAAAHAACgB2ABEABgAAAAD/+AAFAAAAdgAKAHsAEgAG +AAD////4AAYAAAB7AAoAggASAAYAAAAA//gABQAAAIIACgCHABIABgAAAAD/+AAFAAAAhwAKAIwA +EgAGAAAAAP/4AAUAAACMAAoAkQASAAYAAAAA//gABQAAAJEACgCWABIABgAAAAD/+QAFAAAAlgAK +AJsAEQAGAAAAAP/6AAX//wCbAAoAoAAPAAYAAAAA//oABQABAKAACgClABEABgAA////+AAGAAAA +pQAKAKwAEgAGAAD////4AAYAAACsAAoAswASAAYAAP////gABgAAALMACgC6ABIABgAA////+QAG +AAAAugAKAMEAEQAGAAD////4AAYAAgDBAAoAyAAUAAYAAP////kABQACAMgACgDOABMABgAA//// ++QAGAAIAzgAKANUAEw== +""" + ) + ), + Image.open( + BytesIO( + base64.b64decode( + b""" +iVBORw0KGgoAAAANSUhEUgAAAx4AAAAUAQAAAAArMtZoAAAEwElEQVR4nABlAJr/AHVE4czCI/4u +Mc4b7vuds/xzjz5/3/7u/n9vMe7vnfH/9++vPn/xyf5zhxzjt8GHw8+2d83u8x27199/nxuQ6Od9 +M43/5z2I+9n9ZtmDBwMQECDRQw/eQIQohJXxpBCNVE6QCCAAAAD//wBlAJr/AgALyj1t/wINwq0g +LeNZUworuN1cjTPIzrTX6ofHWeo3v336qPzfEwRmBnHTtf95/fglZK5N0PDgfRTslpGBvz7LFc4F +IUXBWQGjQ5MGCx34EDFPwXiY4YbYxavpnhHFrk14CDAAAAD//wBlAJr/AgKqRooH2gAgPeggvUAA +Bu2WfgPoAwzRAABAAAAAAACQgLz/3Uv4Gv+gX7BJgDeeGP6AAAD1NMDzKHD7ANWr3loYbxsAD791 +NAADfcoIDyP44K/jv4Y63/Z+t98Ovt+ub4T48LAAAAD//wBlAJr/AuplMlADJAAAAGuAphWpqhMx +in0A/fRvAYBABPgBwBUgABBQ/sYAyv9g0bCHgOLoGAAAAAAAREAAwI7nr0ArYpow7aX8//9LaP/9 +SjdavWA8ePHeBIKB//81/83ndznOaXx379wAAAD//wBlAJr/AqDxW+D3AABAAbUh/QMnbQag/gAY +AYDAAACgtgD/gOqAAAB5IA/8AAAk+n9w0AAA8AAAmFRJuPo27ciC0cD5oeW4E7KA/wD3ECMAn2tt +y8PgwH8AfAxFzC0JzeAMtratAsC/ffwAAAD//wBlAJr/BGKAyCAA4AAAAvgeYTAwHd1kmQF5chkG +ABoMIHcL5xVpTfQbUqzlAAAErwAQBgAAEOClA5D9il08AEh/tUzdCBsXkbgACED+woQg8Si9VeqY +lODCn7lmF6NhnAEYgAAA/NMIAAAAAAD//2JgjLZgVGBg5Pv/Tvpc8hwGBjYGJADjHDrAwPzAjv/H +/Wf3PzCwtzcwHmBgYGcwbZz8wHaCAQMDOwMDQ8MCBgYOC3W7mp+f0w+wHOYxO3OG+e376hsMZjk3 +AAAAAP//YmCMY2A4wMAIN5e5gQETPD6AZisDAwMDgzSDAAPjByiHcQMDAwMDg1nOze1lByRu5/47 +c4859311AYNZzg0AAAAA//9iYGDBYihOIIMuwIjGL39/fwffA8b//xv/P2BPtzzHwCBjUQAAAAD/ +/yLFBrIBAAAA//9i1HhcwdhizX7u8NZNzyLbvT97bfrMf/QHI8evOwcSqGUJAAAA//9iYBB81iSw +pEE170Qrg5MIYydHqwdDQRMrAwcVrQAAAAD//2J4x7j9AAMDn8Q/BgYLBoaiAwwMjPdvMDBYM1Tv +oJodAAAAAP//Yqo/83+dxePWlxl3npsel9lvLfPcqlE9725C+acfVLMEAAAA//9i+s9gwCoaaGMR +evta/58PTEWzr21hufPjA8N+qlnBwAAAAAD//2JiWLci5v1+HmFXDqcnULE/MxgYGBj+f6CaJQAA +AAD//2Ji2FrkY3iYpYC5qDeGgeEMAwPDvwQBBoYvcTwOVLMEAAAA//9isDBgkP///0EOg9z35v// +Gc/eeW7BwPj5+QGZhANUswMAAAD//2JgqGBgYGBgqEMXlvhMPUsAAAAA//8iYDd1AAAAAP//AwDR +w7IkEbzhVQAAAABJRU5ErkJggg== +""" + ) + ) + ), + ) + return f diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 3f3be706d..253e084d1 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -1,186 +1,186 @@ -# -# The Python Imaging Library -# $Id$ -# -# screen grabber -# -# History: -# 2001-04-26 fl created -# 2001-09-17 fl use builtin driver, if present -# 2002-11-19 fl added grabclipboard support -# -# Copyright (c) 2001-2002 by Secret Labs AB -# Copyright (c) 2001-2002 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import io -import os -import shutil -import subprocess -import sys -import tempfile - -from . import Image - - -def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=None): - if xdisplay is None: - if sys.platform == "darwin": - fh, filepath = tempfile.mkstemp(".png") - os.close(fh) - args = ["screencapture"] - if bbox: - left, top, right, bottom = bbox - args += ["-R", f"{left},{top},{right-left},{bottom-top}"] - subprocess.call(args + ["-x", filepath]) - im = Image.open(filepath) - im.load() - os.unlink(filepath) - if bbox: - im_resized = im.resize((right - left, bottom - top)) - im.close() - return im_resized - return im - elif sys.platform == "win32": - offset, size, data = Image.core.grabscreen_win32( - include_layered_windows, all_screens - ) - im = Image.frombytes( - "RGB", - size, - data, - # RGB, 32-bit line padding, origin lower left corner - "raw", - "BGR", - (size[0] * 3 + 3) & -4, - -1, - ) - if bbox: - x0, y0 = offset - left, top, right, bottom = bbox - im = im.crop((left - x0, top - y0, right - x0, bottom - y0)) - return im - try: - if not Image.core.HAVE_XCB: - msg = "Pillow was built without XCB support" - raise OSError(msg) - size, data = Image.core.grabscreen_x11(xdisplay) - except OSError: - if ( - xdisplay is None - and sys.platform not in ("darwin", "win32") - and shutil.which("gnome-screenshot") - ): - fh, filepath = tempfile.mkstemp(".png") - os.close(fh) - subprocess.call(["gnome-screenshot", "-f", filepath]) - im = Image.open(filepath) - im.load() - os.unlink(filepath) - if bbox: - im_cropped = im.crop(bbox) - im.close() - return im_cropped - return im - else: - raise - else: - im = Image.frombytes("RGB", size, data, "raw", "BGRX", size[0] * 4, 1) - if bbox: - im = im.crop(bbox) - return im - - -def grabclipboard(): - if sys.platform == "darwin": - fh, filepath = tempfile.mkstemp(".png") - os.close(fh) - commands = [ - 'set theFile to (open for access POSIX file "' - + filepath - + '" with write permission)', - "try", - " write (the clipboard as «class PNGf») to theFile", - "end try", - "close access theFile", - ] - script = ["osascript"] - for command in commands: - script += ["-e", command] - subprocess.call(script) - - im = None - if os.stat(filepath).st_size != 0: - im = Image.open(filepath) - im.load() - os.unlink(filepath) - return im - elif sys.platform == "win32": - fmt, data = Image.core.grabclipboard_win32() - if fmt == "file": # CF_HDROP - import struct - - o = struct.unpack_from("I", data)[0] - if data[16] != 0: - files = data[o:].decode("utf-16le").split("\0") - else: - files = data[o:].decode("mbcs").split("\0") - return files[: files.index("")] - if isinstance(data, bytes): - data = io.BytesIO(data) - if fmt == "png": - from . import PngImagePlugin - - return PngImagePlugin.PngImageFile(data) - elif fmt == "DIB": - from . import BmpImagePlugin - - return BmpImagePlugin.DibImageFile(data) - return None - else: - if os.getenv("WAYLAND_DISPLAY"): - session_type = "wayland" - elif os.getenv("DISPLAY"): - session_type = "x11" - else: # Session type check failed - session_type = None - - if shutil.which("wl-paste") and session_type in ("wayland", None): - args = ["wl-paste", "-t", "image"] - elif shutil.which("xclip") and session_type in ("x11", None): - args = ["xclip", "-selection", "clipboard", "-t", "image/png", "-o"] - else: - msg = "wl-paste or xclip is required for ImageGrab.grabclipboard() on Linux" - raise NotImplementedError(msg) - - p = subprocess.run(args, capture_output=True) - if p.returncode != 0: - err = p.stderr - for silent_error in [ - # wl-paste, when the clipboard is empty - b"Nothing is copied", - # Ubuntu/Debian wl-paste, when the clipboard is empty - b"No selection", - # Ubuntu/Debian wl-paste, when an image isn't available - b"No suitable type of content copied", - # wl-paste or Ubuntu/Debian xclip, when an image isn't available - b" not available", - # xclip, when an image isn't available - b"cannot convert ", - # xclip, when the clipboard isn't initialized - b"xclip: Error: There is no owner for the ", - ]: - if silent_error in err: - return None - msg = f"{args[0]} error" - if err: - msg += f": {err.strip().decode()}" - raise ChildProcessError(msg) - - data = io.BytesIO(p.stdout) - im = Image.open(data) - im.load() - return im +# +# The Python Imaging Library +# $Id$ +# +# screen grabber +# +# History: +# 2001-04-26 fl created +# 2001-09-17 fl use builtin driver, if present +# 2002-11-19 fl added grabclipboard support +# +# Copyright (c) 2001-2002 by Secret Labs AB +# Copyright (c) 2001-2002 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import io +import os +import shutil +import subprocess +import sys +import tempfile + +from . import Image + + +def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=None): + if xdisplay is None: + if sys.platform == "darwin": + fh, filepath = tempfile.mkstemp(".png") + os.close(fh) + args = ["screencapture"] + if bbox: + left, top, right, bottom = bbox + args += ["-R", f"{left},{top},{right-left},{bottom-top}"] + subprocess.call(args + ["-x", filepath]) + im = Image.open(filepath) + im.load() + os.unlink(filepath) + if bbox: + im_resized = im.resize((right - left, bottom - top)) + im.close() + return im_resized + return im + elif sys.platform == "win32": + offset, size, data = Image.core.grabscreen_win32( + include_layered_windows, all_screens + ) + im = Image.frombytes( + "RGB", + size, + data, + # RGB, 32-bit line padding, origin lower left corner + "raw", + "BGR", + (size[0] * 3 + 3) & -4, + -1, + ) + if bbox: + x0, y0 = offset + left, top, right, bottom = bbox + im = im.crop((left - x0, top - y0, right - x0, bottom - y0)) + return im + try: + if not Image.core.HAVE_XCB: + msg = "Pillow was built without XCB support" + raise OSError(msg) + size, data = Image.core.grabscreen_x11(xdisplay) + except OSError: + if ( + xdisplay is None + and sys.platform not in ("darwin", "win32") + and shutil.which("gnome-screenshot") + ): + fh, filepath = tempfile.mkstemp(".png") + os.close(fh) + subprocess.call(["gnome-screenshot", "-f", filepath]) + im = Image.open(filepath) + im.load() + os.unlink(filepath) + if bbox: + im_cropped = im.crop(bbox) + im.close() + return im_cropped + return im + else: + raise + else: + im = Image.frombytes("RGB", size, data, "raw", "BGRX", size[0] * 4, 1) + if bbox: + im = im.crop(bbox) + return im + + +def grabclipboard(): + if sys.platform == "darwin": + fh, filepath = tempfile.mkstemp(".png") + os.close(fh) + commands = [ + 'set theFile to (open for access POSIX file "' + + filepath + + '" with write permission)', + "try", + " write (the clipboard as «class PNGf») to theFile", + "end try", + "close access theFile", + ] + script = ["osascript"] + for command in commands: + script += ["-e", command] + subprocess.call(script) + + im = None + if os.stat(filepath).st_size != 0: + im = Image.open(filepath) + im.load() + os.unlink(filepath) + return im + elif sys.platform == "win32": + fmt, data = Image.core.grabclipboard_win32() + if fmt == "file": # CF_HDROP + import struct + + o = struct.unpack_from("I", data)[0] + if data[16] != 0: + files = data[o:].decode("utf-16le").split("\0") + else: + files = data[o:].decode("mbcs").split("\0") + return files[: files.index("")] + if isinstance(data, bytes): + data = io.BytesIO(data) + if fmt == "png": + from . import PngImagePlugin + + return PngImagePlugin.PngImageFile(data) + elif fmt == "DIB": + from . import BmpImagePlugin + + return BmpImagePlugin.DibImageFile(data) + return None + else: + if os.getenv("WAYLAND_DISPLAY"): + session_type = "wayland" + elif os.getenv("DISPLAY"): + session_type = "x11" + else: # Session type check failed + session_type = None + + if shutil.which("wl-paste") and session_type in ("wayland", None): + args = ["wl-paste", "-t", "image"] + elif shutil.which("xclip") and session_type in ("x11", None): + args = ["xclip", "-selection", "clipboard", "-t", "image/png", "-o"] + else: + msg = "wl-paste or xclip is required for ImageGrab.grabclipboard() on Linux" + raise NotImplementedError(msg) + + p = subprocess.run(args, capture_output=True) + if p.returncode != 0: + err = p.stderr + for silent_error in [ + # wl-paste, when the clipboard is empty + b"Nothing is copied", + # Ubuntu/Debian wl-paste, when the clipboard is empty + b"No selection", + # Ubuntu/Debian wl-paste, when an image isn't available + b"No suitable type of content copied", + # wl-paste or Ubuntu/Debian xclip, when an image isn't available + b" not available", + # xclip, when an image isn't available + b"cannot convert ", + # xclip, when the clipboard isn't initialized + b"xclip: Error: There is no owner for the ", + ]: + if silent_error in err: + return None + msg = f"{args[0]} error" + if err: + msg += f": {err.strip().decode()}" + raise ChildProcessError(msg) + + data = io.BytesIO(p.stdout) + im = Image.open(data) + im.load() + return im diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index 6664434ea..75fce77b0 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -1,357 +1,357 @@ -# -# The Python Imaging Library -# $Id$ -# -# a simple math add-on for the Python Imaging Library -# -# History: -# 1999-02-15 fl Original PIL Plus release -# 2005-05-05 fl Simplified and cleaned up for PIL 1.1.6 -# 2005-09-12 fl Fixed int() and float() for Python 2.4.1 -# -# Copyright (c) 1999-2005 by Secret Labs AB -# Copyright (c) 2005 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import builtins -from types import CodeType -from typing import Any, Callable - -from . import Image, _imagingmath -from ._deprecate import deprecate - - -class _Operand: - """Wraps an image operand, providing standard operators""" - - def __init__(self, im: Image.Image): - self.im = im - - def __fixup(self, im1: _Operand | float) -> Image.Image: - # convert image to suitable mode - if isinstance(im1, _Operand): - # argument was an image. - if im1.im.mode in ("1", "L"): - return im1.im.convert("I") - elif im1.im.mode in ("I", "F"): - return im1.im - else: - msg = f"unsupported mode: {im1.im.mode}" - raise ValueError(msg) - else: - # argument was a constant - if isinstance(im1, (int, float)) and self.im.mode in ("1", "L", "I"): - return Image.new("I", self.im.size, im1) - else: - return Image.new("F", self.im.size, im1) - - def apply( - self, - op: str, - im1: _Operand | float, - im2: _Operand | float | None = None, - mode: str | None = None, - ) -> _Operand: - im_1 = self.__fixup(im1) - if im2 is None: - # unary operation - out = Image.new(mode or im_1.mode, im_1.size, None) - im_1.load() - try: - op = getattr(_imagingmath, f"{op}_{im_1.mode}") - except AttributeError as e: - msg = f"bad operand type for '{op}'" - raise TypeError(msg) from e - _imagingmath.unop(op, out.im.id, im_1.im.id) - else: - # binary operation - im_2 = self.__fixup(im2) - if im_1.mode != im_2.mode: - # convert both arguments to floating point - if im_1.mode != "F": - im_1 = im_1.convert("F") - if im_2.mode != "F": - im_2 = im_2.convert("F") - if im_1.size != im_2.size: - # crop both arguments to a common size - size = ( - min(im_1.size[0], im_2.size[0]), - min(im_1.size[1], im_2.size[1]), - ) - if im_1.size != size: - im_1 = im_1.crop((0, 0) + size) - if im_2.size != size: - im_2 = im_2.crop((0, 0) + size) - out = Image.new(mode or im_1.mode, im_1.size, None) - im_1.load() - im_2.load() - try: - op = getattr(_imagingmath, f"{op}_{im_1.mode}") - except AttributeError as e: - msg = f"bad operand type for '{op}'" - raise TypeError(msg) from e - _imagingmath.binop(op, out.im.id, im_1.im.id, im_2.im.id) - return _Operand(out) - - # unary operators - def __bool__(self) -> bool: - # an image is "true" if it contains at least one non-zero pixel - return self.im.getbbox() is not None - - def __abs__(self) -> _Operand: - return self.apply("abs", self) - - def __pos__(self) -> _Operand: - return self - - def __neg__(self) -> _Operand: - return self.apply("neg", self) - - # binary operators - def __add__(self, other: _Operand | float) -> _Operand: - return self.apply("add", self, other) - - def __radd__(self, other: _Operand | float) -> _Operand: - return self.apply("add", other, self) - - def __sub__(self, other: _Operand | float) -> _Operand: - return self.apply("sub", self, other) - - def __rsub__(self, other: _Operand | float) -> _Operand: - return self.apply("sub", other, self) - - def __mul__(self, other: _Operand | float) -> _Operand: - return self.apply("mul", self, other) - - def __rmul__(self, other: _Operand | float) -> _Operand: - return self.apply("mul", other, self) - - def __truediv__(self, other: _Operand | float) -> _Operand: - return self.apply("div", self, other) - - def __rtruediv__(self, other: _Operand | float) -> _Operand: - return self.apply("div", other, self) - - def __mod__(self, other: _Operand | float) -> _Operand: - return self.apply("mod", self, other) - - def __rmod__(self, other: _Operand | float) -> _Operand: - return self.apply("mod", other, self) - - def __pow__(self, other: _Operand | float) -> _Operand: - return self.apply("pow", self, other) - - def __rpow__(self, other: _Operand | float) -> _Operand: - return self.apply("pow", other, self) - - # bitwise - def __invert__(self) -> _Operand: - return self.apply("invert", self) - - def __and__(self, other: _Operand | float) -> _Operand: - return self.apply("and", self, other) - - def __rand__(self, other: _Operand | float) -> _Operand: - return self.apply("and", other, self) - - def __or__(self, other: _Operand | float) -> _Operand: - return self.apply("or", self, other) - - def __ror__(self, other: _Operand | float) -> _Operand: - return self.apply("or", other, self) - - def __xor__(self, other: _Operand | float) -> _Operand: - return self.apply("xor", self, other) - - def __rxor__(self, other: _Operand | float) -> _Operand: - return self.apply("xor", other, self) - - def __lshift__(self, other: _Operand | float) -> _Operand: - return self.apply("lshift", self, other) - - def __rshift__(self, other: _Operand | float) -> _Operand: - return self.apply("rshift", self, other) - - # logical - def __eq__(self, other): - return self.apply("eq", self, other) - - def __ne__(self, other): - return self.apply("ne", self, other) - - def __lt__(self, other: _Operand | float) -> _Operand: - return self.apply("lt", self, other) - - def __le__(self, other: _Operand | float) -> _Operand: - return self.apply("le", self, other) - - def __gt__(self, other: _Operand | float) -> _Operand: - return self.apply("gt", self, other) - - def __ge__(self, other: _Operand | float) -> _Operand: - return self.apply("ge", self, other) - - -# conversions -def imagemath_int(self: _Operand) -> _Operand: - return _Operand(self.im.convert("I")) - - -def imagemath_float(self: _Operand) -> _Operand: - return _Operand(self.im.convert("F")) - - -# logical -def imagemath_equal(self: _Operand, other: _Operand | float | None) -> _Operand: - return self.apply("eq", self, other, mode="I") - - -def imagemath_notequal(self: _Operand, other: _Operand | float | None) -> _Operand: - return self.apply("ne", self, other, mode="I") - - -def imagemath_min(self: _Operand, other: _Operand | float | None) -> _Operand: - return self.apply("min", self, other) - - -def imagemath_max(self: _Operand, other: _Operand | float | None) -> _Operand: - return self.apply("max", self, other) - - -def imagemath_convert(self: _Operand, mode: str) -> _Operand: - return _Operand(self.im.convert(mode)) - - -ops = { - "int": imagemath_int, - "float": imagemath_float, - "equal": imagemath_equal, - "notequal": imagemath_notequal, - "min": imagemath_min, - "max": imagemath_max, - "convert": imagemath_convert, -} - - -def lambda_eval( - expression: Callable[[dict[str, Any]], Any], - options: dict[str, Any] = {}, - **kw: Any, -) -> Any: - """ - Returns the result of an image function. - - :py:mod:`~PIL.ImageMath` only supports single-layer images. To process multi-band - images, use the :py:meth:`~PIL.Image.Image.split` method or - :py:func:`~PIL.Image.merge` function. - - :param expression: A function that receives a dictionary. - :param options: Values to add to the function's dictionary. You - can either use a dictionary, or one or more keyword - arguments. - :return: The expression result. This is usually an image object, but can - also be an integer, a floating point value, or a pixel tuple, - depending on the expression. - """ - - args: dict[str, Any] = ops.copy() - args.update(options) - args.update(kw) - for k, v in args.items(): - if hasattr(v, "im"): - args[k] = _Operand(v) - - out = expression(args) - try: - return out.im - except AttributeError: - return out - - -def unsafe_eval( - expression: str, - options: dict[str, Any] = {}, - **kw: Any, -) -> Any: - """ - Evaluates an image expression. This uses Python's ``eval()`` function to process - the expression string, and carries the security risks of doing so. It is not - recommended to process expressions without considering this. - :py:meth:`~lambda_eval` is a more secure alternative. - - :py:mod:`~PIL.ImageMath` only supports single-layer images. To process multi-band - images, use the :py:meth:`~PIL.Image.Image.split` method or - :py:func:`~PIL.Image.merge` function. - - :param expression: A string containing a Python-style expression. - :param options: Values to add to the evaluation context. You - can either use a dictionary, or one or more keyword - arguments. - :return: The evaluated expression. This is usually an image object, but can - also be an integer, a floating point value, or a pixel tuple, - depending on the expression. - """ - - # build execution namespace - args: dict[str, Any] = ops.copy() - for k in list(options.keys()) + list(kw.keys()): - if "__" in k or hasattr(builtins, k): - msg = f"'{k}' not allowed" - raise ValueError(msg) - - args.update(options) - args.update(kw) - for k, v in args.items(): - if hasattr(v, "im"): - args[k] = _Operand(v) - - compiled_code = compile(expression, "", "eval") - - def scan(code: CodeType) -> None: - for const in code.co_consts: - if type(const) is type(compiled_code): - scan(const) - - for name in code.co_names: - if name not in args and name != "abs": - msg = f"'{name}' not allowed" - raise ValueError(msg) - - scan(compiled_code) - out = builtins.eval(expression, {"__builtins": {"abs": abs}}, args) - try: - return out.im - except AttributeError: - return out - - -def eval( - expression: str, - _dict: dict[str, Any] = {}, - **kw: Any, -) -> Any: - """ - Evaluates an image expression. - - Deprecated. Use lambda_eval() or unsafe_eval() instead. - - :param expression: A string containing a Python-style expression. - :param _dict: Values to add to the evaluation context. You - can either use a dictionary, or one or more keyword - arguments. - :return: The evaluated expression. This is usually an image object, but can - also be an integer, a floating point value, or a pixel tuple, - depending on the expression. - - .. deprecated:: 10.3.0 - """ - - deprecate( - "ImageMath.eval", - 12, - "ImageMath.lambda_eval or ImageMath.unsafe_eval", - ) - return unsafe_eval(expression, _dict, **kw) +# +# The Python Imaging Library +# $Id$ +# +# a simple math add-on for the Python Imaging Library +# +# History: +# 1999-02-15 fl Original PIL Plus release +# 2005-05-05 fl Simplified and cleaned up for PIL 1.1.6 +# 2005-09-12 fl Fixed int() and float() for Python 2.4.1 +# +# Copyright (c) 1999-2005 by Secret Labs AB +# Copyright (c) 2005 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import builtins +from types import CodeType +from typing import Any, Callable + +from . import Image, _imagingmath +from ._deprecate import deprecate + + +class _Operand: + """Wraps an image operand, providing standard operators""" + + def __init__(self, im: Image.Image): + self.im = im + + def __fixup(self, im1: _Operand | float) -> Image.Image: + # convert image to suitable mode + if isinstance(im1, _Operand): + # argument was an image. + if im1.im.mode in ("1", "L"): + return im1.im.convert("I") + elif im1.im.mode in ("I", "F"): + return im1.im + else: + msg = f"unsupported mode: {im1.im.mode}" + raise ValueError(msg) + else: + # argument was a constant + if isinstance(im1, (int, float)) and self.im.mode in ("1", "L", "I"): + return Image.new("I", self.im.size, im1) + else: + return Image.new("F", self.im.size, im1) + + def apply( + self, + op: str, + im1: _Operand | float, + im2: _Operand | float | None = None, + mode: str | None = None, + ) -> _Operand: + im_1 = self.__fixup(im1) + if im2 is None: + # unary operation + out = Image.new(mode or im_1.mode, im_1.size, None) + im_1.load() + try: + op = getattr(_imagingmath, f"{op}_{im_1.mode}") + except AttributeError as e: + msg = f"bad operand type for '{op}'" + raise TypeError(msg) from e + _imagingmath.unop(op, out.im.id, im_1.im.id) + else: + # binary operation + im_2 = self.__fixup(im2) + if im_1.mode != im_2.mode: + # convert both arguments to floating point + if im_1.mode != "F": + im_1 = im_1.convert("F") + if im_2.mode != "F": + im_2 = im_2.convert("F") + if im_1.size != im_2.size: + # crop both arguments to a common size + size = ( + min(im_1.size[0], im_2.size[0]), + min(im_1.size[1], im_2.size[1]), + ) + if im_1.size != size: + im_1 = im_1.crop((0, 0) + size) + if im_2.size != size: + im_2 = im_2.crop((0, 0) + size) + out = Image.new(mode or im_1.mode, im_1.size, None) + im_1.load() + im_2.load() + try: + op = getattr(_imagingmath, f"{op}_{im_1.mode}") + except AttributeError as e: + msg = f"bad operand type for '{op}'" + raise TypeError(msg) from e + _imagingmath.binop(op, out.im.id, im_1.im.id, im_2.im.id) + return _Operand(out) + + # unary operators + def __bool__(self) -> bool: + # an image is "true" if it contains at least one non-zero pixel + return self.im.getbbox() is not None + + def __abs__(self) -> _Operand: + return self.apply("abs", self) + + def __pos__(self) -> _Operand: + return self + + def __neg__(self) -> _Operand: + return self.apply("neg", self) + + # binary operators + def __add__(self, other: _Operand | float) -> _Operand: + return self.apply("add", self, other) + + def __radd__(self, other: _Operand | float) -> _Operand: + return self.apply("add", other, self) + + def __sub__(self, other: _Operand | float) -> _Operand: + return self.apply("sub", self, other) + + def __rsub__(self, other: _Operand | float) -> _Operand: + return self.apply("sub", other, self) + + def __mul__(self, other: _Operand | float) -> _Operand: + return self.apply("mul", self, other) + + def __rmul__(self, other: _Operand | float) -> _Operand: + return self.apply("mul", other, self) + + def __truediv__(self, other: _Operand | float) -> _Operand: + return self.apply("div", self, other) + + def __rtruediv__(self, other: _Operand | float) -> _Operand: + return self.apply("div", other, self) + + def __mod__(self, other: _Operand | float) -> _Operand: + return self.apply("mod", self, other) + + def __rmod__(self, other: _Operand | float) -> _Operand: + return self.apply("mod", other, self) + + def __pow__(self, other: _Operand | float) -> _Operand: + return self.apply("pow", self, other) + + def __rpow__(self, other: _Operand | float) -> _Operand: + return self.apply("pow", other, self) + + # bitwise + def __invert__(self) -> _Operand: + return self.apply("invert", self) + + def __and__(self, other: _Operand | float) -> _Operand: + return self.apply("and", self, other) + + def __rand__(self, other: _Operand | float) -> _Operand: + return self.apply("and", other, self) + + def __or__(self, other: _Operand | float) -> _Operand: + return self.apply("or", self, other) + + def __ror__(self, other: _Operand | float) -> _Operand: + return self.apply("or", other, self) + + def __xor__(self, other: _Operand | float) -> _Operand: + return self.apply("xor", self, other) + + def __rxor__(self, other: _Operand | float) -> _Operand: + return self.apply("xor", other, self) + + def __lshift__(self, other: _Operand | float) -> _Operand: + return self.apply("lshift", self, other) + + def __rshift__(self, other: _Operand | float) -> _Operand: + return self.apply("rshift", self, other) + + # logical + def __eq__(self, other): + return self.apply("eq", self, other) + + def __ne__(self, other): + return self.apply("ne", self, other) + + def __lt__(self, other: _Operand | float) -> _Operand: + return self.apply("lt", self, other) + + def __le__(self, other: _Operand | float) -> _Operand: + return self.apply("le", self, other) + + def __gt__(self, other: _Operand | float) -> _Operand: + return self.apply("gt", self, other) + + def __ge__(self, other: _Operand | float) -> _Operand: + return self.apply("ge", self, other) + + +# conversions +def imagemath_int(self: _Operand) -> _Operand: + return _Operand(self.im.convert("I")) + + +def imagemath_float(self: _Operand) -> _Operand: + return _Operand(self.im.convert("F")) + + +# logical +def imagemath_equal(self: _Operand, other: _Operand | float | None) -> _Operand: + return self.apply("eq", self, other, mode="I") + + +def imagemath_notequal(self: _Operand, other: _Operand | float | None) -> _Operand: + return self.apply("ne", self, other, mode="I") + + +def imagemath_min(self: _Operand, other: _Operand | float | None) -> _Operand: + return self.apply("min", self, other) + + +def imagemath_max(self: _Operand, other: _Operand | float | None) -> _Operand: + return self.apply("max", self, other) + + +def imagemath_convert(self: _Operand, mode: str) -> _Operand: + return _Operand(self.im.convert(mode)) + + +ops = { + "int": imagemath_int, + "float": imagemath_float, + "equal": imagemath_equal, + "notequal": imagemath_notequal, + "min": imagemath_min, + "max": imagemath_max, + "convert": imagemath_convert, +} + + +def lambda_eval( + expression: Callable[[dict[str, Any]], Any], + options: dict[str, Any] = {}, + **kw: Any, +) -> Any: + """ + Returns the result of an image function. + + :py:mod:`~PIL.ImageMath` only supports single-layer images. To process multi-band + images, use the :py:meth:`~PIL.Image.Image.split` method or + :py:func:`~PIL.Image.merge` function. + + :param expression: A function that receives a dictionary. + :param options: Values to add to the function's dictionary. You + can either use a dictionary, or one or more keyword + arguments. + :return: The expression result. This is usually an image object, but can + also be an integer, a floating point value, or a pixel tuple, + depending on the expression. + """ + + args: dict[str, Any] = ops.copy() + args.update(options) + args.update(kw) + for k, v in args.items(): + if hasattr(v, "im"): + args[k] = _Operand(v) + + out = expression(args) + try: + return out.im + except AttributeError: + return out + + +def unsafe_eval( + expression: str, + options: dict[str, Any] = {}, + **kw: Any, +) -> Any: + """ + Evaluates an image expression. This uses Python's ``eval()`` function to process + the expression string, and carries the security risks of doing so. It is not + recommended to process expressions without considering this. + :py:meth:`~lambda_eval` is a more secure alternative. + + :py:mod:`~PIL.ImageMath` only supports single-layer images. To process multi-band + images, use the :py:meth:`~PIL.Image.Image.split` method or + :py:func:`~PIL.Image.merge` function. + + :param expression: A string containing a Python-style expression. + :param options: Values to add to the evaluation context. You + can either use a dictionary, or one or more keyword + arguments. + :return: The evaluated expression. This is usually an image object, but can + also be an integer, a floating point value, or a pixel tuple, + depending on the expression. + """ + + # build execution namespace + args: dict[str, Any] = ops.copy() + for k in list(options.keys()) + list(kw.keys()): + if "__" in k or hasattr(builtins, k): + msg = f"'{k}' not allowed" + raise ValueError(msg) + + args.update(options) + args.update(kw) + for k, v in args.items(): + if hasattr(v, "im"): + args[k] = _Operand(v) + + compiled_code = compile(expression, "", "eval") + + def scan(code: CodeType) -> None: + for const in code.co_consts: + if type(const) is type(compiled_code): + scan(const) + + for name in code.co_names: + if name not in args and name != "abs": + msg = f"'{name}' not allowed" + raise ValueError(msg) + + scan(compiled_code) + out = builtins.eval(expression, {"__builtins": {"abs": abs}}, args) + try: + return out.im + except AttributeError: + return out + + +def eval( + expression: str, + _dict: dict[str, Any] = {}, + **kw: Any, +) -> Any: + """ + Evaluates an image expression. + + Deprecated. Use lambda_eval() or unsafe_eval() instead. + + :param expression: A string containing a Python-style expression. + :param _dict: Values to add to the evaluation context. You + can either use a dictionary, or one or more keyword + arguments. + :return: The evaluated expression. This is usually an image object, but can + also be an integer, a floating point value, or a pixel tuple, + depending on the expression. + + .. deprecated:: 10.3.0 + """ + + deprecate( + "ImageMath.eval", + 12, + "ImageMath.lambda_eval or ImageMath.unsafe_eval", + ) + return unsafe_eval(expression, _dict, **kw) diff --git a/src/PIL/ImageMode.py b/src/PIL/ImageMode.py index 92a08d2cb..21c041bef 100644 --- a/src/PIL/ImageMode.py +++ b/src/PIL/ImageMode.py @@ -1,92 +1,92 @@ -# -# The Python Imaging Library. -# $Id$ -# -# standard mode descriptors -# -# History: -# 2006-03-20 fl Added -# -# Copyright (c) 2006 by Secret Labs AB. -# Copyright (c) 2006 by Fredrik Lundh. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import sys -from functools import lru_cache -from typing import NamedTuple - -from ._deprecate import deprecate - - -class ModeDescriptor(NamedTuple): - """Wrapper for mode strings.""" - - mode: str - bands: tuple[str, ...] - basemode: str - basetype: str - typestr: str - - def __str__(self) -> str: - return self.mode - - -@lru_cache -def getmode(mode: str) -> ModeDescriptor: - """Gets a mode descriptor for the given mode.""" - endian = "<" if sys.byteorder == "little" else ">" - - modes = { - # core modes - # Bits need to be extended to bytes - "1": ("L", "L", ("1",), "|b1"), - "L": ("L", "L", ("L",), "|u1"), - "I": ("L", "I", ("I",), f"{endian}i4"), - "F": ("L", "F", ("F",), f"{endian}f4"), - "P": ("P", "L", ("P",), "|u1"), - "RGB": ("RGB", "L", ("R", "G", "B"), "|u1"), - "RGBX": ("RGB", "L", ("R", "G", "B", "X"), "|u1"), - "RGBA": ("RGB", "L", ("R", "G", "B", "A"), "|u1"), - "CMYK": ("RGB", "L", ("C", "M", "Y", "K"), "|u1"), - "YCbCr": ("RGB", "L", ("Y", "Cb", "Cr"), "|u1"), - # UNDONE - unsigned |u1i1i1 - "LAB": ("RGB", "L", ("L", "A", "B"), "|u1"), - "HSV": ("RGB", "L", ("H", "S", "V"), "|u1"), - # extra experimental modes - "RGBa": ("RGB", "L", ("R", "G", "B", "a"), "|u1"), - "BGR;15": ("RGB", "L", ("B", "G", "R"), "|u1"), - "BGR;16": ("RGB", "L", ("B", "G", "R"), "|u1"), - "BGR;24": ("RGB", "L", ("B", "G", "R"), "|u1"), - "LA": ("L", "L", ("L", "A"), "|u1"), - "La": ("L", "L", ("L", "a"), "|u1"), - "PA": ("RGB", "L", ("P", "A"), "|u1"), - } - if mode in modes: - if mode in ("BGR;15", "BGR;16", "BGR;24"): - deprecate(mode, 12) - base_mode, base_type, bands, type_str = modes[mode] - return ModeDescriptor(mode, bands, base_mode, base_type, type_str) - - mapping_modes = { - # I;16 == I;16L, and I;32 == I;32L - "I;16": "u2", - "I;16BS": ">i2", - "I;16N": f"{endian}u2", - "I;16NS": f"{endian}i2", - "I;32": "u4", - "I;32L": "i4", - "I;32LS": " str: + return self.mode + + +@lru_cache +def getmode(mode: str) -> ModeDescriptor: + """Gets a mode descriptor for the given mode.""" + endian = "<" if sys.byteorder == "little" else ">" + + modes = { + # core modes + # Bits need to be extended to bytes + "1": ("L", "L", ("1",), "|b1"), + "L": ("L", "L", ("L",), "|u1"), + "I": ("L", "I", ("I",), f"{endian}i4"), + "F": ("L", "F", ("F",), f"{endian}f4"), + "P": ("P", "L", ("P",), "|u1"), + "RGB": ("RGB", "L", ("R", "G", "B"), "|u1"), + "RGBX": ("RGB", "L", ("R", "G", "B", "X"), "|u1"), + "RGBA": ("RGB", "L", ("R", "G", "B", "A"), "|u1"), + "CMYK": ("RGB", "L", ("C", "M", "Y", "K"), "|u1"), + "YCbCr": ("RGB", "L", ("Y", "Cb", "Cr"), "|u1"), + # UNDONE - unsigned |u1i1i1 + "LAB": ("RGB", "L", ("L", "A", "B"), "|u1"), + "HSV": ("RGB", "L", ("H", "S", "V"), "|u1"), + # extra experimental modes + "RGBa": ("RGB", "L", ("R", "G", "B", "a"), "|u1"), + "BGR;15": ("RGB", "L", ("B", "G", "R"), "|u1"), + "BGR;16": ("RGB", "L", ("B", "G", "R"), "|u1"), + "BGR;24": ("RGB", "L", ("B", "G", "R"), "|u1"), + "LA": ("L", "L", ("L", "A"), "|u1"), + "La": ("L", "L", ("L", "a"), "|u1"), + "PA": ("RGB", "L", ("P", "A"), "|u1"), + } + if mode in modes: + if mode in ("BGR;15", "BGR;16", "BGR;24"): + deprecate(mode, 12) + base_mode, base_type, bands, type_str = modes[mode] + return ModeDescriptor(mode, bands, base_mode, base_type, type_str) + + mapping_modes = { + # I;16 == I;16L, and I;32 == I;32L + "I;16": "u2", + "I;16BS": ">i2", + "I;16N": f"{endian}u2", + "I;16NS": f"{endian}i2", + "I;32": "u4", + "I;32L": "i4", + "I;32LS": " -from __future__ import annotations - -import re - -from . import Image, _imagingmorph - -LUT_SIZE = 1 << 9 - -# fmt: off -ROTATION_MATRIX = [ - 6, 3, 0, - 7, 4, 1, - 8, 5, 2, -] -MIRROR_MATRIX = [ - 2, 1, 0, - 5, 4, 3, - 8, 7, 6, -] -# fmt: on - - -class LutBuilder: - """A class for building a MorphLut from a descriptive language - - The input patterns is a list of a strings sequences like these:: - - 4:(... - .1. - 111)->1 - - (whitespaces including linebreaks are ignored). The option 4 - describes a series of symmetry operations (in this case a - 4-rotation), the pattern is described by: - - - . or X - Ignore - - 1 - Pixel is on - - 0 - Pixel is off - - The result of the operation is described after "->" string. - - The default is to return the current pixel value, which is - returned if no other match is found. - - Operations: - - - 4 - 4 way rotation - - N - Negate - - 1 - Dummy op for no other operation (an op must always be given) - - M - Mirroring - - Example:: - - lb = LutBuilder(patterns = ["4:(... .1. 111)->1"]) - lut = lb.build_lut() - - """ - - def __init__( - self, patterns: list[str] | None = None, op_name: str | None = None - ) -> None: - if patterns is not None: - self.patterns = patterns - else: - self.patterns = [] - self.lut: bytearray | None = None - if op_name is not None: - known_patterns = { - "corner": ["1:(... ... ...)->0", "4:(00. 01. ...)->1"], - "dilation4": ["4:(... .0. .1.)->1"], - "dilation8": ["4:(... .0. .1.)->1", "4:(... .0. ..1)->1"], - "erosion4": ["4:(... .1. .0.)->0"], - "erosion8": ["4:(... .1. .0.)->0", "4:(... .1. ..0)->0"], - "edge": [ - "1:(... ... ...)->0", - "4:(.0. .1. ...)->1", - "4:(01. .1. ...)->1", - ], - } - if op_name not in known_patterns: - msg = f"Unknown pattern {op_name}!" - raise Exception(msg) - - self.patterns = known_patterns[op_name] - - def add_patterns(self, patterns: list[str]) -> None: - self.patterns += patterns - - def build_default_lut(self) -> None: - symbols = [0, 1] - m = 1 << 4 # pos of current pixel - self.lut = bytearray(symbols[(i & m) > 0] for i in range(LUT_SIZE)) - - def get_lut(self) -> bytearray | None: - return self.lut - - def _string_permute(self, pattern: str, permutation: list[int]) -> str: - """string_permute takes a pattern and a permutation and returns the - string permuted according to the permutation list. - """ - assert len(permutation) == 9 - return "".join(pattern[p] for p in permutation) - - def _pattern_permute( - self, basic_pattern: str, options: str, basic_result: int - ) -> list[tuple[str, int]]: - """pattern_permute takes a basic pattern and its result and clones - the pattern according to the modifications described in the $options - parameter. It returns a list of all cloned patterns.""" - patterns = [(basic_pattern, basic_result)] - - # rotations - if "4" in options: - res = patterns[-1][1] - for i in range(4): - patterns.append( - (self._string_permute(patterns[-1][0], ROTATION_MATRIX), res) - ) - # mirror - if "M" in options: - n = len(patterns) - for pattern, res in patterns[:n]: - patterns.append((self._string_permute(pattern, MIRROR_MATRIX), res)) - - # negate - if "N" in options: - n = len(patterns) - for pattern, res in patterns[:n]: - # Swap 0 and 1 - pattern = pattern.replace("0", "Z").replace("1", "0").replace("Z", "1") - res = 1 - int(res) - patterns.append((pattern, res)) - - return patterns - - def build_lut(self) -> bytearray: - """Compile all patterns into a morphology lut. - - TBD :Build based on (file) morphlut:modify_lut - """ - self.build_default_lut() - assert self.lut is not None - patterns = [] - - # Parse and create symmetries of the patterns strings - for p in self.patterns: - m = re.search(r"(\w*):?\s*\((.+?)\)\s*->\s*(\d)", p.replace("\n", "")) - if not m: - msg = 'Syntax error in pattern "' + p + '"' - raise Exception(msg) - options = m.group(1) - pattern = m.group(2) - result = int(m.group(3)) - - # Get rid of spaces - pattern = pattern.replace(" ", "").replace("\n", "") - - patterns += self._pattern_permute(pattern, options, result) - - # compile the patterns into regular expressions for speed - compiled_patterns = [] - for pattern in patterns: - p = pattern[0].replace(".", "X").replace("X", "[01]") - compiled_patterns.append((re.compile(p), pattern[1])) - - # Step through table and find patterns that match. - # Note that all the patterns are searched. The last one - # caught overrides - for i in range(LUT_SIZE): - # Build the bit pattern - bitpattern = bin(i)[2:] - bitpattern = ("0" * (9 - len(bitpattern)) + bitpattern)[::-1] - - for pattern, r in compiled_patterns: - if pattern.match(bitpattern): - self.lut[i] = [0, 1][r] - - return self.lut - - -class MorphOp: - """A class for binary morphological operators""" - - def __init__( - self, - lut: bytearray | None = None, - op_name: str | None = None, - patterns: list[str] | None = None, - ) -> None: - """Create a binary morphological operator""" - self.lut = lut - if op_name is not None: - self.lut = LutBuilder(op_name=op_name).build_lut() - elif patterns is not None: - self.lut = LutBuilder(patterns=patterns).build_lut() - - def apply(self, image: Image.Image) -> tuple[int, Image.Image]: - """Run a single morphological operation on an image - - Returns a tuple of the number of changed pixels and the - morphed image""" - if self.lut is None: - msg = "No operator loaded" - raise Exception(msg) - - if image.mode != "L": - msg = "Image mode must be L" - raise ValueError(msg) - outimage = Image.new(image.mode, image.size, None) - count = _imagingmorph.apply(bytes(self.lut), image.im.id, outimage.im.id) - return count, outimage - - def match(self, image: Image.Image) -> list[tuple[int, int]]: - """Get a list of coordinates matching the morphological operation on - an image. - - Returns a list of tuples of (x,y) coordinates - of all matching pixels. See :ref:`coordinate-system`.""" - if self.lut is None: - msg = "No operator loaded" - raise Exception(msg) - - if image.mode != "L": - msg = "Image mode must be L" - raise ValueError(msg) - return _imagingmorph.match(bytes(self.lut), image.im.id) - - def get_on_pixels(self, image: Image.Image) -> list[tuple[int, int]]: - """Get a list of all turned on pixels in a binary image - - Returns a list of tuples of (x,y) coordinates - of all matching pixels. See :ref:`coordinate-system`.""" - - if image.mode != "L": - msg = "Image mode must be L" - raise ValueError(msg) - return _imagingmorph.get_on_pixels(image.im.id) - - def load_lut(self, filename: str) -> None: - """Load an operator from an mrl file""" - with open(filename, "rb") as f: - self.lut = bytearray(f.read()) - - if len(self.lut) != LUT_SIZE: - self.lut = None - msg = "Wrong size operator file!" - raise Exception(msg) - - def save_lut(self, filename: str) -> None: - """Save an operator to an mrl file""" - if self.lut is None: - msg = "No operator loaded" - raise Exception(msg) - with open(filename, "wb") as f: - f.write(self.lut) - - def set_lut(self, lut: bytearray | None) -> None: - """Set the lut from an external source""" - self.lut = lut +# A binary morphology add-on for the Python Imaging Library +# +# History: +# 2014-06-04 Initial version. +# +# Copyright (c) 2014 Dov Grobgeld +from __future__ import annotations + +import re + +from . import Image, _imagingmorph + +LUT_SIZE = 1 << 9 + +# fmt: off +ROTATION_MATRIX = [ + 6, 3, 0, + 7, 4, 1, + 8, 5, 2, +] +MIRROR_MATRIX = [ + 2, 1, 0, + 5, 4, 3, + 8, 7, 6, +] +# fmt: on + + +class LutBuilder: + """A class for building a MorphLut from a descriptive language + + The input patterns is a list of a strings sequences like these:: + + 4:(... + .1. + 111)->1 + + (whitespaces including linebreaks are ignored). The option 4 + describes a series of symmetry operations (in this case a + 4-rotation), the pattern is described by: + + - . or X - Ignore + - 1 - Pixel is on + - 0 - Pixel is off + + The result of the operation is described after "->" string. + + The default is to return the current pixel value, which is + returned if no other match is found. + + Operations: + + - 4 - 4 way rotation + - N - Negate + - 1 - Dummy op for no other operation (an op must always be given) + - M - Mirroring + + Example:: + + lb = LutBuilder(patterns = ["4:(... .1. 111)->1"]) + lut = lb.build_lut() + + """ + + def __init__( + self, patterns: list[str] | None = None, op_name: str | None = None + ) -> None: + if patterns is not None: + self.patterns = patterns + else: + self.patterns = [] + self.lut: bytearray | None = None + if op_name is not None: + known_patterns = { + "corner": ["1:(... ... ...)->0", "4:(00. 01. ...)->1"], + "dilation4": ["4:(... .0. .1.)->1"], + "dilation8": ["4:(... .0. .1.)->1", "4:(... .0. ..1)->1"], + "erosion4": ["4:(... .1. .0.)->0"], + "erosion8": ["4:(... .1. .0.)->0", "4:(... .1. ..0)->0"], + "edge": [ + "1:(... ... ...)->0", + "4:(.0. .1. ...)->1", + "4:(01. .1. ...)->1", + ], + } + if op_name not in known_patterns: + msg = f"Unknown pattern {op_name}!" + raise Exception(msg) + + self.patterns = known_patterns[op_name] + + def add_patterns(self, patterns: list[str]) -> None: + self.patterns += patterns + + def build_default_lut(self) -> None: + symbols = [0, 1] + m = 1 << 4 # pos of current pixel + self.lut = bytearray(symbols[(i & m) > 0] for i in range(LUT_SIZE)) + + def get_lut(self) -> bytearray | None: + return self.lut + + def _string_permute(self, pattern: str, permutation: list[int]) -> str: + """string_permute takes a pattern and a permutation and returns the + string permuted according to the permutation list. + """ + assert len(permutation) == 9 + return "".join(pattern[p] for p in permutation) + + def _pattern_permute( + self, basic_pattern: str, options: str, basic_result: int + ) -> list[tuple[str, int]]: + """pattern_permute takes a basic pattern and its result and clones + the pattern according to the modifications described in the $options + parameter. It returns a list of all cloned patterns.""" + patterns = [(basic_pattern, basic_result)] + + # rotations + if "4" in options: + res = patterns[-1][1] + for i in range(4): + patterns.append( + (self._string_permute(patterns[-1][0], ROTATION_MATRIX), res) + ) + # mirror + if "M" in options: + n = len(patterns) + for pattern, res in patterns[:n]: + patterns.append((self._string_permute(pattern, MIRROR_MATRIX), res)) + + # negate + if "N" in options: + n = len(patterns) + for pattern, res in patterns[:n]: + # Swap 0 and 1 + pattern = pattern.replace("0", "Z").replace("1", "0").replace("Z", "1") + res = 1 - int(res) + patterns.append((pattern, res)) + + return patterns + + def build_lut(self) -> bytearray: + """Compile all patterns into a morphology lut. + + TBD :Build based on (file) morphlut:modify_lut + """ + self.build_default_lut() + assert self.lut is not None + patterns = [] + + # Parse and create symmetries of the patterns strings + for p in self.patterns: + m = re.search(r"(\w*):?\s*\((.+?)\)\s*->\s*(\d)", p.replace("\n", "")) + if not m: + msg = 'Syntax error in pattern "' + p + '"' + raise Exception(msg) + options = m.group(1) + pattern = m.group(2) + result = int(m.group(3)) + + # Get rid of spaces + pattern = pattern.replace(" ", "").replace("\n", "") + + patterns += self._pattern_permute(pattern, options, result) + + # compile the patterns into regular expressions for speed + compiled_patterns = [] + for pattern in patterns: + p = pattern[0].replace(".", "X").replace("X", "[01]") + compiled_patterns.append((re.compile(p), pattern[1])) + + # Step through table and find patterns that match. + # Note that all the patterns are searched. The last one + # caught overrides + for i in range(LUT_SIZE): + # Build the bit pattern + bitpattern = bin(i)[2:] + bitpattern = ("0" * (9 - len(bitpattern)) + bitpattern)[::-1] + + for pattern, r in compiled_patterns: + if pattern.match(bitpattern): + self.lut[i] = [0, 1][r] + + return self.lut + + +class MorphOp: + """A class for binary morphological operators""" + + def __init__( + self, + lut: bytearray | None = None, + op_name: str | None = None, + patterns: list[str] | None = None, + ) -> None: + """Create a binary morphological operator""" + self.lut = lut + if op_name is not None: + self.lut = LutBuilder(op_name=op_name).build_lut() + elif patterns is not None: + self.lut = LutBuilder(patterns=patterns).build_lut() + + def apply(self, image: Image.Image) -> tuple[int, Image.Image]: + """Run a single morphological operation on an image + + Returns a tuple of the number of changed pixels and the + morphed image""" + if self.lut is None: + msg = "No operator loaded" + raise Exception(msg) + + if image.mode != "L": + msg = "Image mode must be L" + raise ValueError(msg) + outimage = Image.new(image.mode, image.size, None) + count = _imagingmorph.apply(bytes(self.lut), image.im.id, outimage.im.id) + return count, outimage + + def match(self, image: Image.Image) -> list[tuple[int, int]]: + """Get a list of coordinates matching the morphological operation on + an image. + + Returns a list of tuples of (x,y) coordinates + of all matching pixels. See :ref:`coordinate-system`.""" + if self.lut is None: + msg = "No operator loaded" + raise Exception(msg) + + if image.mode != "L": + msg = "Image mode must be L" + raise ValueError(msg) + return _imagingmorph.match(bytes(self.lut), image.im.id) + + def get_on_pixels(self, image: Image.Image) -> list[tuple[int, int]]: + """Get a list of all turned on pixels in a binary image + + Returns a list of tuples of (x,y) coordinates + of all matching pixels. See :ref:`coordinate-system`.""" + + if image.mode != "L": + msg = "Image mode must be L" + raise ValueError(msg) + return _imagingmorph.get_on_pixels(image.im.id) + + def load_lut(self, filename: str) -> None: + """Load an operator from an mrl file""" + with open(filename, "rb") as f: + self.lut = bytearray(f.read()) + + if len(self.lut) != LUT_SIZE: + self.lut = None + msg = "Wrong size operator file!" + raise Exception(msg) + + def save_lut(self, filename: str) -> None: + """Save an operator to an mrl file""" + if self.lut is None: + msg = "No operator loaded" + raise Exception(msg) + with open(filename, "wb") as f: + f.write(self.lut) + + def set_lut(self, lut: bytearray | None) -> None: + """Set the lut from an external source""" + self.lut = lut diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index cbe189cc9..87e7dce08 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -1,724 +1,724 @@ -# -# The Python Imaging Library. -# $Id$ -# -# standard image operations -# -# History: -# 2001-10-20 fl Created -# 2001-10-23 fl Added autocontrast operator -# 2001-12-18 fl Added Kevin's fit operator -# 2004-03-14 fl Fixed potential division by zero in equalize -# 2005-05-05 fl Fixed equalize for low number of values -# -# Copyright (c) 2001-2004 by Secret Labs AB -# Copyright (c) 2001-2004 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import functools -import operator -import re -from typing import Protocol, Sequence, cast - -from . import ExifTags, Image, ImagePalette - -# -# helpers - - -def _border(border: int | tuple[int, ...]) -> tuple[int, int, int, int]: - if isinstance(border, tuple): - if len(border) == 2: - left, top = right, bottom = border - elif len(border) == 4: - left, top, right, bottom = border - else: - left = top = right = bottom = border - return left, top, right, bottom - - -def _color(color: str | int | tuple[int, ...], mode: str) -> int | tuple[int, ...]: - if isinstance(color, str): - from . import ImageColor - - color = ImageColor.getcolor(color, mode) - return color - - -def _lut(image: Image.Image, lut: list[int]) -> Image.Image: - if image.mode == "P": - # FIXME: apply to lookup table, not image data - msg = "mode P support coming soon" - raise NotImplementedError(msg) - elif image.mode in ("L", "RGB"): - if image.mode == "RGB" and len(lut) == 256: - lut = lut + lut + lut - return image.point(lut) - else: - msg = f"not supported for mode {image.mode}" - raise OSError(msg) - - -# -# actions - - -def autocontrast( - image: Image.Image, - cutoff: float | tuple[float, float] = 0, - ignore: int | Sequence[int] | None = None, - mask: Image.Image | None = None, - preserve_tone: bool = False, -) -> Image.Image: - """ - Maximize (normalize) image contrast. This function calculates a - histogram of the input image (or mask region), removes ``cutoff`` percent of the - lightest and darkest pixels from the histogram, and remaps the image - so that the darkest pixel becomes black (0), and the lightest - becomes white (255). - - :param image: The image to process. - :param cutoff: The percent to cut off from the histogram on the low and - high ends. Either a tuple of (low, high), or a single - number for both. - :param ignore: The background pixel value (use None for no background). - :param mask: Histogram used in contrast operation is computed using pixels - within the mask. If no mask is given the entire image is used - for histogram computation. - :param preserve_tone: Preserve image tone in Photoshop-like style autocontrast. - - .. versionadded:: 8.2.0 - - :return: An image. - """ - if preserve_tone: - histogram = image.convert("L").histogram(mask) - else: - histogram = image.histogram(mask) - - lut = [] - for layer in range(0, len(histogram), 256): - h = histogram[layer : layer + 256] - if ignore is not None: - # get rid of outliers - if isinstance(ignore, int): - h[ignore] = 0 - else: - for ix in ignore: - h[ix] = 0 - if cutoff: - # cut off pixels from both ends of the histogram - if not isinstance(cutoff, tuple): - cutoff = (cutoff, cutoff) - # get number of pixels - n = 0 - for ix in range(256): - n = n + h[ix] - # remove cutoff% pixels from the low end - cut = int(n * cutoff[0] // 100) - for lo in range(256): - if cut > h[lo]: - cut = cut - h[lo] - h[lo] = 0 - else: - h[lo] -= cut - cut = 0 - if cut <= 0: - break - # remove cutoff% samples from the high end - cut = int(n * cutoff[1] // 100) - for hi in range(255, -1, -1): - if cut > h[hi]: - cut = cut - h[hi] - h[hi] = 0 - else: - h[hi] -= cut - cut = 0 - if cut <= 0: - break - # find lowest/highest samples after preprocessing - for lo in range(256): - if h[lo]: - break - for hi in range(255, -1, -1): - if h[hi]: - break - if hi <= lo: - # don't bother - lut.extend(list(range(256))) - else: - scale = 255.0 / (hi - lo) - offset = -lo * scale - for ix in range(256): - ix = int(ix * scale + offset) - if ix < 0: - ix = 0 - elif ix > 255: - ix = 255 - lut.append(ix) - return _lut(image, lut) - - -def colorize( - image: Image.Image, - black: str | tuple[int, ...], - white: str | tuple[int, ...], - mid: str | int | tuple[int, ...] | None = None, - blackpoint: int = 0, - whitepoint: int = 255, - midpoint: int = 127, -) -> Image.Image: - """ - Colorize grayscale image. - This function calculates a color wedge which maps all black pixels in - the source image to the first color and all white pixels to the - second color. If ``mid`` is specified, it uses three-color mapping. - The ``black`` and ``white`` arguments should be RGB tuples or color names; - optionally you can use three-color mapping by also specifying ``mid``. - Mapping positions for any of the colors can be specified - (e.g. ``blackpoint``), where these parameters are the integer - value corresponding to where the corresponding color should be mapped. - These parameters must have logical order, such that - ``blackpoint <= midpoint <= whitepoint`` (if ``mid`` is specified). - - :param image: The image to colorize. - :param black: The color to use for black input pixels. - :param white: The color to use for white input pixels. - :param mid: The color to use for midtone input pixels. - :param blackpoint: an int value [0, 255] for the black mapping. - :param whitepoint: an int value [0, 255] for the white mapping. - :param midpoint: an int value [0, 255] for the midtone mapping. - :return: An image. - """ - - # Initial asserts - assert image.mode == "L" - if mid is None: - assert 0 <= blackpoint <= whitepoint <= 255 - else: - assert 0 <= blackpoint <= midpoint <= whitepoint <= 255 - - # Define colors from arguments - rgb_black = cast(Sequence[int], _color(black, "RGB")) - rgb_white = cast(Sequence[int], _color(white, "RGB")) - rgb_mid = cast(Sequence[int], _color(mid, "RGB")) if mid is not None else None - - # Empty lists for the mapping - red = [] - green = [] - blue = [] - - # Create the low-end values - for i in range(0, 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) - - for i in range_map: - red.append( - rgb_black[0] + i * (rgb_white[0] - rgb_black[0]) // len(range_map) - ) - green.append( - rgb_black[1] + i * (rgb_white[1] - rgb_black[1]) // len(range_map) - ) - blue.append( - rgb_black[2] + i * (rgb_white[2] - rgb_black[2]) // len(range_map) - ) - - # Create the mapping (3-color) - else: - range_map1 = range(0, midpoint - blackpoint) - range_map2 = range(0, whitepoint - midpoint) - - for i in range_map1: - red.append( - rgb_black[0] + i * (rgb_mid[0] - rgb_black[0]) // len(range_map1) - ) - green.append( - rgb_black[1] + i * (rgb_mid[1] - rgb_black[1]) // len(range_map1) - ) - blue.append( - rgb_black[2] + i * (rgb_mid[2] - rgb_black[2]) // len(range_map1) - ) - for i in range_map2: - red.append(rgb_mid[0] + i * (rgb_white[0] - rgb_mid[0]) // len(range_map2)) - green.append( - rgb_mid[1] + i * (rgb_white[1] - rgb_mid[1]) // len(range_map2) - ) - 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): - red.append(rgb_white[0]) - green.append(rgb_white[1]) - blue.append(rgb_white[2]) - - # Return converted image - image = image.convert("RGB") - return _lut(image, red + green + blue) - - -def contain( - image: Image.Image, size: tuple[int, int], method: int = Image.Resampling.BICUBIC -) -> Image.Image: - """ - Returns a resized version of the image, set to the maximum width and height - within the requested size, while maintaining the original aspect ratio. - - :param image: The image to resize. - :param size: The requested output size in pixels, given as a - (width, height) tuple. - :param method: Resampling method to use. Default is - :py:attr:`~PIL.Image.Resampling.BICUBIC`. - See :ref:`concept-filters`. - :return: An image. - """ - - im_ratio = image.width / image.height - dest_ratio = size[0] / size[1] - - if im_ratio != dest_ratio: - if im_ratio > dest_ratio: - new_height = round(image.height / image.width * size[0]) - if new_height != size[1]: - size = (size[0], new_height) - else: - new_width = round(image.width / image.height * size[1]) - if new_width != size[0]: - size = (new_width, size[1]) - return image.resize(size, resample=method) - - -def cover( - image: Image.Image, size: tuple[int, int], method: int = Image.Resampling.BICUBIC -) -> Image.Image: - """ - Returns a resized version of the image, so that the requested size is - covered, while maintaining the original aspect ratio. - - :param image: The image to resize. - :param size: The requested output size in pixels, given as a - (width, height) tuple. - :param method: Resampling method to use. Default is - :py:attr:`~PIL.Image.Resampling.BICUBIC`. - See :ref:`concept-filters`. - :return: An image. - """ - - im_ratio = image.width / image.height - dest_ratio = size[0] / size[1] - - if im_ratio != dest_ratio: - if im_ratio < dest_ratio: - new_height = round(image.height / image.width * size[0]) - if new_height != size[1]: - size = (size[0], new_height) - else: - new_width = round(image.width / image.height * size[1]) - if new_width != size[0]: - size = (new_width, size[1]) - return image.resize(size, resample=method) - - -def pad( - image: Image.Image, - size: tuple[int, int], - method: int = Image.Resampling.BICUBIC, - color: str | int | tuple[int, ...] | None = None, - centering: tuple[float, float] = (0.5, 0.5), -) -> Image.Image: - """ - Returns a resized and padded version of the image, expanded to fill the - requested aspect ratio and size. - - :param image: The image to resize and crop. - :param size: The requested output size in pixels, given as a - (width, height) tuple. - :param method: Resampling method to use. Default is - :py:attr:`~PIL.Image.Resampling.BICUBIC`. - See :ref:`concept-filters`. - :param color: The background color of the padded image. - :param centering: Control the position of the original image within the - padded version. - - (0.5, 0.5) will keep the image centered - (0, 0) will keep the image aligned to the top left - (1, 1) will keep the image aligned to the bottom - right - :return: An image. - """ - - resized = contain(image, size, method) - if resized.size == size: - out = resized - else: - out = Image.new(image.mode, size, color) - if resized.palette: - out.putpalette(resized.getpalette()) - if resized.width != size[0]: - x = round((size[0] - resized.width) * max(0, min(centering[0], 1))) - out.paste(resized, (x, 0)) - else: - y = round((size[1] - resized.height) * max(0, min(centering[1], 1))) - out.paste(resized, (0, y)) - return out - - -def crop(image: Image.Image, border: int = 0) -> Image.Image: - """ - Remove border from image. The same amount of pixels are removed - from all four sides. This function works on all image modes. - - .. seealso:: :py:meth:`~PIL.Image.Image.crop` - - :param image: The image to crop. - :param border: The number of pixels to remove. - :return: An image. - """ - left, top, right, bottom = _border(border) - return image.crop((left, top, image.size[0] - right, image.size[1] - bottom)) - - -def scale( - image: Image.Image, factor: float, resample: int = Image.Resampling.BICUBIC -) -> Image.Image: - """ - Returns a rescaled image by a specific factor given in parameter. - A factor greater than 1 expands the image, between 0 and 1 contracts the - image. - - :param image: The image to rescale. - :param factor: The expansion factor, as a float. - :param resample: Resampling method to use. Default is - :py:attr:`~PIL.Image.Resampling.BICUBIC`. - See :ref:`concept-filters`. - :returns: An :py:class:`~PIL.Image.Image` object. - """ - if factor == 1: - return image.copy() - elif factor <= 0: - msg = "the factor must be greater than 0" - raise ValueError(msg) - else: - size = (round(factor * image.width), round(factor * image.height)) - return image.resize(size, resample) - - -class SupportsGetMesh(Protocol): - """ - An object that supports the ``getmesh`` method, taking an image as an - argument, and returning a list of tuples. Each tuple contains two tuples, - the source box as a tuple of 4 integers, and a tuple of 8 integers for the - final quadrilateral, in order of top left, bottom left, bottom right, top - right. - """ - - def getmesh( - self, image: Image.Image - ) -> list[ - tuple[tuple[int, int, int, int], tuple[int, int, int, int, int, int, int, int]] - ]: ... - - -def deform( - image: Image.Image, - deformer: SupportsGetMesh, - resample: int = Image.Resampling.BILINEAR, -) -> Image.Image: - """ - Deform the image. - - :param image: The image to deform. - :param deformer: A deformer object. Any object that implements a - ``getmesh`` method can be used. - :param resample: An optional resampling filter. Same values possible as - in the PIL.Image.transform function. - :return: An image. - """ - return image.transform( - image.size, Image.Transform.MESH, deformer.getmesh(image), resample - ) - - -def equalize(image: Image.Image, mask: Image.Image | None = None) -> Image.Image: - """ - Equalize the image histogram. This function applies a non-linear - mapping to the input image, in order to create a uniform - distribution of grayscale values in the output image. - - :param image: The image to equalize. - :param mask: An optional mask. If given, only the pixels selected by - the mask are included in the analysis. - :return: An image. - """ - if image.mode == "P": - image = image.convert("RGB") - h = image.histogram(mask) - lut = [] - for b in range(0, len(h), 256): - histo = [_f for _f in h[b : b + 256] if _f] - if len(histo) <= 1: - lut.extend(list(range(256))) - else: - step = (functools.reduce(operator.add, histo) - histo[-1]) // 255 - if not step: - lut.extend(list(range(256))) - else: - n = step // 2 - for i in range(256): - lut.append(n // step) - n = n + h[i + b] - return _lut(image, lut) - - -def expand( - image: Image.Image, - border: int | tuple[int, ...] = 0, - fill: str | int | tuple[int, ...] = 0, -) -> Image.Image: - """ - Add border to the image - - :param image: The image to expand. - :param border: Border width, in pixels. - :param fill: Pixel fill value (a color value). Default is 0 (black). - :return: An image. - """ - left, top, right, bottom = _border(border) - width = left + image.size[0] + right - height = top + image.size[1] + bottom - color = _color(fill, image.mode) - if image.palette: - palette = ImagePalette.ImagePalette(palette=image.getpalette()) - if isinstance(color, tuple) and (len(color) == 3 or len(color) == 4): - color = palette.getcolor(color) - else: - palette = None - out = Image.new(image.mode, (width, height), color) - if palette: - out.putpalette(palette.palette) - out.paste(image, (left, top)) - return out - - -def fit( - image: Image.Image, - size: tuple[int, int], - method: int = Image.Resampling.BICUBIC, - bleed: float = 0.0, - centering: tuple[float, float] = (0.5, 0.5), -) -> Image.Image: - """ - Returns a resized and cropped version of the image, cropped to the - requested aspect ratio and size. - - This function was contributed by Kevin Cazabon. - - :param image: The image to resize and crop. - :param size: The requested output size in pixels, given as a - (width, height) tuple. - :param method: Resampling method to use. Default is - :py:attr:`~PIL.Image.Resampling.BICUBIC`. - See :ref:`concept-filters`. - :param bleed: Remove a border around the outside of the image from all - four edges. The value is a decimal percentage (use 0.01 for - one percent). The default value is 0 (no border). - Cannot be greater than or equal to 0.5. - :param centering: Control the cropping position. Use (0.5, 0.5) for - center cropping (e.g. if cropping the width, take 50% off - of the left side, and therefore 50% off the right side). - (0.0, 0.0) will crop from the top left corner (i.e. if - cropping the width, take all of the crop off of the right - side, and if cropping the height, take all of it off the - bottom). (1.0, 0.0) will crop from the bottom left - corner, etc. (i.e. if cropping the width, take all of the - crop off the left side, and if cropping the height take - none from the top, and therefore all off the bottom). - :return: An image. - """ - - # by Kevin Cazabon, Feb 17/2000 - # kevin@cazabon.com - # https://www.cazabon.com - - centering_x, centering_y = centering - - if not 0.0 <= centering_x <= 1.0: - centering_x = 0.5 - if not 0.0 <= centering_y <= 1.0: - centering_y = 0.5 - - if not 0.0 <= bleed < 0.5: - bleed = 0.0 - - # calculate the area to use for resizing and cropping, subtracting - # the 'bleed' around the edges - - # number of pixels to trim off on Top and Bottom, Left and Right - bleed_pixels = (bleed * image.size[0], bleed * image.size[1]) - - live_size = ( - image.size[0] - bleed_pixels[0] * 2, - image.size[1] - bleed_pixels[1] * 2, - ) - - # calculate the aspect ratio of the live_size - live_size_ratio = live_size[0] / live_size[1] - - # calculate the aspect ratio of the output image - output_ratio = size[0] / size[1] - - # figure out if the sides or top/bottom will be cropped off - if live_size_ratio == output_ratio: - # live_size is already the needed ratio - crop_width = live_size[0] - crop_height = live_size[1] - elif live_size_ratio >= output_ratio: - # live_size is wider than what's needed, crop the sides - crop_width = output_ratio * live_size[1] - crop_height = live_size[1] - else: - # live_size is taller than what's needed, crop the top and bottom - crop_width = live_size[0] - crop_height = live_size[0] / output_ratio - - # make the crop - crop_left = bleed_pixels[0] + (live_size[0] - crop_width) * centering_x - crop_top = bleed_pixels[1] + (live_size[1] - crop_height) * centering_y - - crop = (crop_left, crop_top, crop_left + crop_width, crop_top + crop_height) - - # resize the image and return it - return image.resize(size, method, box=crop) - - -def flip(image: Image.Image) -> Image.Image: - """ - Flip the image vertically (top to bottom). - - :param image: The image to flip. - :return: An image. - """ - return image.transpose(Image.Transpose.FLIP_TOP_BOTTOM) - - -def grayscale(image: Image.Image) -> Image.Image: - """ - Convert the image to grayscale. - - :param image: The image to convert. - :return: An image. - """ - return image.convert("L") - - -def invert(image: Image.Image) -> Image.Image: - """ - Invert (negate) the image. - - :param image: The image to invert. - :return: An image. - """ - lut = list(range(255, -1, -1)) - return image.point(lut) if image.mode == "1" else _lut(image, lut) - - -def mirror(image: Image.Image) -> Image.Image: - """ - Flip image horizontally (left to right). - - :param image: The image to mirror. - :return: An image. - """ - return image.transpose(Image.Transpose.FLIP_LEFT_RIGHT) - - -def posterize(image: Image.Image, bits: int) -> Image.Image: - """ - Reduce the number of bits for each color channel. - - :param image: The image to posterize. - :param bits: The number of bits to keep for each channel (1-8). - :return: An image. - """ - mask = ~(2 ** (8 - bits) - 1) - lut = [i & mask for i in range(256)] - return _lut(image, lut) - - -def solarize(image: Image.Image, threshold: int = 128) -> Image.Image: - """ - Invert all pixel values above a threshold. - - :param image: The image to solarize. - :param threshold: All pixels above this grayscale level are inverted. - :return: An image. - """ - lut = [] - for i in range(256): - if i < threshold: - lut.append(i) - else: - lut.append(255 - i) - return _lut(image, lut) - - -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 - accordingly, and remove the orientation data. - - :param image: The image to transpose. - :param in_place: Boolean. Keyword-only argument. - If ``True``, the original image is modified in-place, and ``None`` is returned. - If ``False`` (default), a new :py:class:`~PIL.Image.Image` object is returned - with the transposition applied. If there is no transposition, a copy of the - image will be returned. - """ - image.load() - image_exif = image.getexif() - orientation = image_exif.get(ExifTags.Base.Orientation, 1) - method = { - 2: Image.Transpose.FLIP_LEFT_RIGHT, - 3: Image.Transpose.ROTATE_180, - 4: Image.Transpose.FLIP_TOP_BOTTOM, - 5: Image.Transpose.TRANSPOSE, - 6: Image.Transpose.ROTATE_270, - 7: Image.Transpose.TRANSVERSE, - 8: Image.Transpose.ROTATE_90, - }.get(orientation) - if method is not None: - transposed_image = image.transpose(method) - if in_place: - image.im = transposed_image.im - image.pyaccess = None - image._size = transposed_image._size - exif_image = image if in_place else transposed_image - - exif = exif_image.getexif() - if ExifTags.Base.Orientation in exif: - del exif[ExifTags.Base.Orientation] - if "exif" in exif_image.info: - exif_image.info["exif"] = exif.tobytes() - elif "Raw profile type exif" in exif_image.info: - exif_image.info["Raw profile type exif"] = exif.tobytes().hex() - elif "XML:com.adobe.xmp" in exif_image.info: - for pattern in ( - r'tiff:Orientation="([0-9])"', - r"([0-9])", - ): - exif_image.info["XML:com.adobe.xmp"] = re.sub( - pattern, "", exif_image.info["XML:com.adobe.xmp"] - ) - if not in_place: - return transposed_image - elif not in_place: - return image.copy() - return None +# +# The Python Imaging Library. +# $Id$ +# +# standard image operations +# +# History: +# 2001-10-20 fl Created +# 2001-10-23 fl Added autocontrast operator +# 2001-12-18 fl Added Kevin's fit operator +# 2004-03-14 fl Fixed potential division by zero in equalize +# 2005-05-05 fl Fixed equalize for low number of values +# +# Copyright (c) 2001-2004 by Secret Labs AB +# Copyright (c) 2001-2004 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import functools +import operator +import re +from typing import Protocol, Sequence, cast + +from . import ExifTags, Image, ImagePalette + +# +# helpers + + +def _border(border: int | tuple[int, ...]) -> tuple[int, int, int, int]: + if isinstance(border, tuple): + if len(border) == 2: + left, top = right, bottom = border + elif len(border) == 4: + left, top, right, bottom = border + else: + left = top = right = bottom = border + return left, top, right, bottom + + +def _color(color: str | int | tuple[int, ...], mode: str) -> int | tuple[int, ...]: + if isinstance(color, str): + from . import ImageColor + + color = ImageColor.getcolor(color, mode) + return color + + +def _lut(image: Image.Image, lut: list[int]) -> Image.Image: + if image.mode == "P": + # FIXME: apply to lookup table, not image data + msg = "mode P support coming soon" + raise NotImplementedError(msg) + elif image.mode in ("L", "RGB"): + if image.mode == "RGB" and len(lut) == 256: + lut = lut + lut + lut + return image.point(lut) + else: + msg = f"not supported for mode {image.mode}" + raise OSError(msg) + + +# +# actions + + +def autocontrast( + image: Image.Image, + cutoff: float | tuple[float, float] = 0, + ignore: int | Sequence[int] | None = None, + mask: Image.Image | None = None, + preserve_tone: bool = False, +) -> Image.Image: + """ + Maximize (normalize) image contrast. This function calculates a + histogram of the input image (or mask region), removes ``cutoff`` percent of the + lightest and darkest pixels from the histogram, and remaps the image + so that the darkest pixel becomes black (0), and the lightest + becomes white (255). + + :param image: The image to process. + :param cutoff: The percent to cut off from the histogram on the low and + high ends. Either a tuple of (low, high), or a single + number for both. + :param ignore: The background pixel value (use None for no background). + :param mask: Histogram used in contrast operation is computed using pixels + within the mask. If no mask is given the entire image is used + for histogram computation. + :param preserve_tone: Preserve image tone in Photoshop-like style autocontrast. + + .. versionadded:: 8.2.0 + + :return: An image. + """ + if preserve_tone: + histogram = image.convert("L").histogram(mask) + else: + histogram = image.histogram(mask) + + lut = [] + for layer in range(0, len(histogram), 256): + h = histogram[layer : layer + 256] + if ignore is not None: + # get rid of outliers + if isinstance(ignore, int): + h[ignore] = 0 + else: + for ix in ignore: + h[ix] = 0 + if cutoff: + # cut off pixels from both ends of the histogram + if not isinstance(cutoff, tuple): + cutoff = (cutoff, cutoff) + # get number of pixels + n = 0 + for ix in range(256): + n = n + h[ix] + # remove cutoff% pixels from the low end + cut = int(n * cutoff[0] // 100) + for lo in range(256): + if cut > h[lo]: + cut = cut - h[lo] + h[lo] = 0 + else: + h[lo] -= cut + cut = 0 + if cut <= 0: + break + # remove cutoff% samples from the high end + cut = int(n * cutoff[1] // 100) + for hi in range(255, -1, -1): + if cut > h[hi]: + cut = cut - h[hi] + h[hi] = 0 + else: + h[hi] -= cut + cut = 0 + if cut <= 0: + break + # find lowest/highest samples after preprocessing + for lo in range(256): + if h[lo]: + break + for hi in range(255, -1, -1): + if h[hi]: + break + if hi <= lo: + # don't bother + lut.extend(list(range(256))) + else: + scale = 255.0 / (hi - lo) + offset = -lo * scale + for ix in range(256): + ix = int(ix * scale + offset) + if ix < 0: + ix = 0 + elif ix > 255: + ix = 255 + lut.append(ix) + return _lut(image, lut) + + +def colorize( + image: Image.Image, + black: str | tuple[int, ...], + white: str | tuple[int, ...], + mid: str | int | tuple[int, ...] | None = None, + blackpoint: int = 0, + whitepoint: int = 255, + midpoint: int = 127, +) -> Image.Image: + """ + Colorize grayscale image. + This function calculates a color wedge which maps all black pixels in + the source image to the first color and all white pixels to the + second color. If ``mid`` is specified, it uses three-color mapping. + The ``black`` and ``white`` arguments should be RGB tuples or color names; + optionally you can use three-color mapping by also specifying ``mid``. + Mapping positions for any of the colors can be specified + (e.g. ``blackpoint``), where these parameters are the integer + value corresponding to where the corresponding color should be mapped. + These parameters must have logical order, such that + ``blackpoint <= midpoint <= whitepoint`` (if ``mid`` is specified). + + :param image: The image to colorize. + :param black: The color to use for black input pixels. + :param white: The color to use for white input pixels. + :param mid: The color to use for midtone input pixels. + :param blackpoint: an int value [0, 255] for the black mapping. + :param whitepoint: an int value [0, 255] for the white mapping. + :param midpoint: an int value [0, 255] for the midtone mapping. + :return: An image. + """ + + # Initial asserts + assert image.mode == "L" + if mid is None: + assert 0 <= blackpoint <= whitepoint <= 255 + else: + assert 0 <= blackpoint <= midpoint <= whitepoint <= 255 + + # Define colors from arguments + rgb_black = cast(Sequence[int], _color(black, "RGB")) + rgb_white = cast(Sequence[int], _color(white, "RGB")) + rgb_mid = cast(Sequence[int], _color(mid, "RGB")) if mid is not None else None + + # Empty lists for the mapping + red = [] + green = [] + blue = [] + + # Create the low-end values + for i in range(0, 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) + + for i in range_map: + red.append( + rgb_black[0] + i * (rgb_white[0] - rgb_black[0]) // len(range_map) + ) + green.append( + rgb_black[1] + i * (rgb_white[1] - rgb_black[1]) // len(range_map) + ) + blue.append( + rgb_black[2] + i * (rgb_white[2] - rgb_black[2]) // len(range_map) + ) + + # Create the mapping (3-color) + else: + range_map1 = range(0, midpoint - blackpoint) + range_map2 = range(0, whitepoint - midpoint) + + for i in range_map1: + red.append( + rgb_black[0] + i * (rgb_mid[0] - rgb_black[0]) // len(range_map1) + ) + green.append( + rgb_black[1] + i * (rgb_mid[1] - rgb_black[1]) // len(range_map1) + ) + blue.append( + rgb_black[2] + i * (rgb_mid[2] - rgb_black[2]) // len(range_map1) + ) + for i in range_map2: + red.append(rgb_mid[0] + i * (rgb_white[0] - rgb_mid[0]) // len(range_map2)) + green.append( + rgb_mid[1] + i * (rgb_white[1] - rgb_mid[1]) // len(range_map2) + ) + 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): + red.append(rgb_white[0]) + green.append(rgb_white[1]) + blue.append(rgb_white[2]) + + # Return converted image + image = image.convert("RGB") + return _lut(image, red + green + blue) + + +def contain( + image: Image.Image, size: tuple[int, int], method: int = Image.Resampling.BICUBIC +) -> Image.Image: + """ + Returns a resized version of the image, set to the maximum width and height + within the requested size, while maintaining the original aspect ratio. + + :param image: The image to resize. + :param size: The requested output size in pixels, given as a + (width, height) tuple. + :param method: Resampling method to use. Default is + :py:attr:`~PIL.Image.Resampling.BICUBIC`. + See :ref:`concept-filters`. + :return: An image. + """ + + im_ratio = image.width / image.height + dest_ratio = size[0] / size[1] + + if im_ratio != dest_ratio: + if im_ratio > dest_ratio: + new_height = round(image.height / image.width * size[0]) + if new_height != size[1]: + size = (size[0], new_height) + else: + new_width = round(image.width / image.height * size[1]) + if new_width != size[0]: + size = (new_width, size[1]) + return image.resize(size, resample=method) + + +def cover( + image: Image.Image, size: tuple[int, int], method: int = Image.Resampling.BICUBIC +) -> Image.Image: + """ + Returns a resized version of the image, so that the requested size is + covered, while maintaining the original aspect ratio. + + :param image: The image to resize. + :param size: The requested output size in pixels, given as a + (width, height) tuple. + :param method: Resampling method to use. Default is + :py:attr:`~PIL.Image.Resampling.BICUBIC`. + See :ref:`concept-filters`. + :return: An image. + """ + + im_ratio = image.width / image.height + dest_ratio = size[0] / size[1] + + if im_ratio != dest_ratio: + if im_ratio < dest_ratio: + new_height = round(image.height / image.width * size[0]) + if new_height != size[1]: + size = (size[0], new_height) + else: + new_width = round(image.width / image.height * size[1]) + if new_width != size[0]: + size = (new_width, size[1]) + return image.resize(size, resample=method) + + +def pad( + image: Image.Image, + size: tuple[int, int], + method: int = Image.Resampling.BICUBIC, + color: str | int | tuple[int, ...] | None = None, + centering: tuple[float, float] = (0.5, 0.5), +) -> Image.Image: + """ + Returns a resized and padded version of the image, expanded to fill the + requested aspect ratio and size. + + :param image: The image to resize and crop. + :param size: The requested output size in pixels, given as a + (width, height) tuple. + :param method: Resampling method to use. Default is + :py:attr:`~PIL.Image.Resampling.BICUBIC`. + See :ref:`concept-filters`. + :param color: The background color of the padded image. + :param centering: Control the position of the original image within the + padded version. + + (0.5, 0.5) will keep the image centered + (0, 0) will keep the image aligned to the top left + (1, 1) will keep the image aligned to the bottom + right + :return: An image. + """ + + resized = contain(image, size, method) + if resized.size == size: + out = resized + else: + out = Image.new(image.mode, size, color) + if resized.palette: + out.putpalette(resized.getpalette()) + if resized.width != size[0]: + x = round((size[0] - resized.width) * max(0, min(centering[0], 1))) + out.paste(resized, (x, 0)) + else: + y = round((size[1] - resized.height) * max(0, min(centering[1], 1))) + out.paste(resized, (0, y)) + return out + + +def crop(image: Image.Image, border: int = 0) -> Image.Image: + """ + Remove border from image. The same amount of pixels are removed + from all four sides. This function works on all image modes. + + .. seealso:: :py:meth:`~PIL.Image.Image.crop` + + :param image: The image to crop. + :param border: The number of pixels to remove. + :return: An image. + """ + left, top, right, bottom = _border(border) + return image.crop((left, top, image.size[0] - right, image.size[1] - bottom)) + + +def scale( + image: Image.Image, factor: float, resample: int = Image.Resampling.BICUBIC +) -> Image.Image: + """ + Returns a rescaled image by a specific factor given in parameter. + A factor greater than 1 expands the image, between 0 and 1 contracts the + image. + + :param image: The image to rescale. + :param factor: The expansion factor, as a float. + :param resample: Resampling method to use. Default is + :py:attr:`~PIL.Image.Resampling.BICUBIC`. + See :ref:`concept-filters`. + :returns: An :py:class:`~PIL.Image.Image` object. + """ + if factor == 1: + return image.copy() + elif factor <= 0: + msg = "the factor must be greater than 0" + raise ValueError(msg) + else: + size = (round(factor * image.width), round(factor * image.height)) + return image.resize(size, resample) + + +class SupportsGetMesh(Protocol): + """ + An object that supports the ``getmesh`` method, taking an image as an + argument, and returning a list of tuples. Each tuple contains two tuples, + the source box as a tuple of 4 integers, and a tuple of 8 integers for the + final quadrilateral, in order of top left, bottom left, bottom right, top + right. + """ + + def getmesh( + self, image: Image.Image + ) -> list[ + tuple[tuple[int, int, int, int], tuple[int, int, int, int, int, int, int, int]] + ]: ... + + +def deform( + image: Image.Image, + deformer: SupportsGetMesh, + resample: int = Image.Resampling.BILINEAR, +) -> Image.Image: + """ + Deform the image. + + :param image: The image to deform. + :param deformer: A deformer object. Any object that implements a + ``getmesh`` method can be used. + :param resample: An optional resampling filter. Same values possible as + in the PIL.Image.transform function. + :return: An image. + """ + return image.transform( + image.size, Image.Transform.MESH, deformer.getmesh(image), resample + ) + + +def equalize(image: Image.Image, mask: Image.Image | None = None) -> Image.Image: + """ + Equalize the image histogram. This function applies a non-linear + mapping to the input image, in order to create a uniform + distribution of grayscale values in the output image. + + :param image: The image to equalize. + :param mask: An optional mask. If given, only the pixels selected by + the mask are included in the analysis. + :return: An image. + """ + if image.mode == "P": + image = image.convert("RGB") + h = image.histogram(mask) + lut = [] + for b in range(0, len(h), 256): + histo = [_f for _f in h[b : b + 256] if _f] + if len(histo) <= 1: + lut.extend(list(range(256))) + else: + step = (functools.reduce(operator.add, histo) - histo[-1]) // 255 + if not step: + lut.extend(list(range(256))) + else: + n = step // 2 + for i in range(256): + lut.append(n // step) + n = n + h[i + b] + return _lut(image, lut) + + +def expand( + image: Image.Image, + border: int | tuple[int, ...] = 0, + fill: str | int | tuple[int, ...] = 0, +) -> Image.Image: + """ + Add border to the image + + :param image: The image to expand. + :param border: Border width, in pixels. + :param fill: Pixel fill value (a color value). Default is 0 (black). + :return: An image. + """ + left, top, right, bottom = _border(border) + width = left + image.size[0] + right + height = top + image.size[1] + bottom + color = _color(fill, image.mode) + if image.palette: + palette = ImagePalette.ImagePalette(palette=image.getpalette()) + if isinstance(color, tuple) and (len(color) == 3 or len(color) == 4): + color = palette.getcolor(color) + else: + palette = None + out = Image.new(image.mode, (width, height), color) + if palette: + out.putpalette(palette.palette) + out.paste(image, (left, top)) + return out + + +def fit( + image: Image.Image, + size: tuple[int, int], + method: int = Image.Resampling.BICUBIC, + bleed: float = 0.0, + centering: tuple[float, float] = (0.5, 0.5), +) -> Image.Image: + """ + Returns a resized and cropped version of the image, cropped to the + requested aspect ratio and size. + + This function was contributed by Kevin Cazabon. + + :param image: The image to resize and crop. + :param size: The requested output size in pixels, given as a + (width, height) tuple. + :param method: Resampling method to use. Default is + :py:attr:`~PIL.Image.Resampling.BICUBIC`. + See :ref:`concept-filters`. + :param bleed: Remove a border around the outside of the image from all + four edges. The value is a decimal percentage (use 0.01 for + one percent). The default value is 0 (no border). + Cannot be greater than or equal to 0.5. + :param centering: Control the cropping position. Use (0.5, 0.5) for + center cropping (e.g. if cropping the width, take 50% off + of the left side, and therefore 50% off the right side). + (0.0, 0.0) will crop from the top left corner (i.e. if + cropping the width, take all of the crop off of the right + side, and if cropping the height, take all of it off the + bottom). (1.0, 0.0) will crop from the bottom left + corner, etc. (i.e. if cropping the width, take all of the + crop off the left side, and if cropping the height take + none from the top, and therefore all off the bottom). + :return: An image. + """ + + # by Kevin Cazabon, Feb 17/2000 + # kevin@cazabon.com + # https://www.cazabon.com + + centering_x, centering_y = centering + + if not 0.0 <= centering_x <= 1.0: + centering_x = 0.5 + if not 0.0 <= centering_y <= 1.0: + centering_y = 0.5 + + if not 0.0 <= bleed < 0.5: + bleed = 0.0 + + # calculate the area to use for resizing and cropping, subtracting + # the 'bleed' around the edges + + # number of pixels to trim off on Top and Bottom, Left and Right + bleed_pixels = (bleed * image.size[0], bleed * image.size[1]) + + live_size = ( + image.size[0] - bleed_pixels[0] * 2, + image.size[1] - bleed_pixels[1] * 2, + ) + + # calculate the aspect ratio of the live_size + live_size_ratio = live_size[0] / live_size[1] + + # calculate the aspect ratio of the output image + output_ratio = size[0] / size[1] + + # figure out if the sides or top/bottom will be cropped off + if live_size_ratio == output_ratio: + # live_size is already the needed ratio + crop_width = live_size[0] + crop_height = live_size[1] + elif live_size_ratio >= output_ratio: + # live_size is wider than what's needed, crop the sides + crop_width = output_ratio * live_size[1] + crop_height = live_size[1] + else: + # live_size is taller than what's needed, crop the top and bottom + crop_width = live_size[0] + crop_height = live_size[0] / output_ratio + + # make the crop + crop_left = bleed_pixels[0] + (live_size[0] - crop_width) * centering_x + crop_top = bleed_pixels[1] + (live_size[1] - crop_height) * centering_y + + crop = (crop_left, crop_top, crop_left + crop_width, crop_top + crop_height) + + # resize the image and return it + return image.resize(size, method, box=crop) + + +def flip(image: Image.Image) -> Image.Image: + """ + Flip the image vertically (top to bottom). + + :param image: The image to flip. + :return: An image. + """ + return image.transpose(Image.Transpose.FLIP_TOP_BOTTOM) + + +def grayscale(image: Image.Image) -> Image.Image: + """ + Convert the image to grayscale. + + :param image: The image to convert. + :return: An image. + """ + return image.convert("L") + + +def invert(image: Image.Image) -> Image.Image: + """ + Invert (negate) the image. + + :param image: The image to invert. + :return: An image. + """ + lut = list(range(255, -1, -1)) + return image.point(lut) if image.mode == "1" else _lut(image, lut) + + +def mirror(image: Image.Image) -> Image.Image: + """ + Flip image horizontally (left to right). + + :param image: The image to mirror. + :return: An image. + """ + return image.transpose(Image.Transpose.FLIP_LEFT_RIGHT) + + +def posterize(image: Image.Image, bits: int) -> Image.Image: + """ + Reduce the number of bits for each color channel. + + :param image: The image to posterize. + :param bits: The number of bits to keep for each channel (1-8). + :return: An image. + """ + mask = ~(2 ** (8 - bits) - 1) + lut = [i & mask for i in range(256)] + return _lut(image, lut) + + +def solarize(image: Image.Image, threshold: int = 128) -> Image.Image: + """ + Invert all pixel values above a threshold. + + :param image: The image to solarize. + :param threshold: All pixels above this grayscale level are inverted. + :return: An image. + """ + lut = [] + for i in range(256): + if i < threshold: + lut.append(i) + else: + lut.append(255 - i) + return _lut(image, lut) + + +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 + accordingly, and remove the orientation data. + + :param image: The image to transpose. + :param in_place: Boolean. Keyword-only argument. + If ``True``, the original image is modified in-place, and ``None`` is returned. + If ``False`` (default), a new :py:class:`~PIL.Image.Image` object is returned + with the transposition applied. If there is no transposition, a copy of the + image will be returned. + """ + image.load() + image_exif = image.getexif() + orientation = image_exif.get(ExifTags.Base.Orientation, 1) + method = { + 2: Image.Transpose.FLIP_LEFT_RIGHT, + 3: Image.Transpose.ROTATE_180, + 4: Image.Transpose.FLIP_TOP_BOTTOM, + 5: Image.Transpose.TRANSPOSE, + 6: Image.Transpose.ROTATE_270, + 7: Image.Transpose.TRANSVERSE, + 8: Image.Transpose.ROTATE_90, + }.get(orientation) + if method is not None: + transposed_image = image.transpose(method) + if in_place: + image.im = transposed_image.im + image.pyaccess = None + image._size = transposed_image._size + exif_image = image if in_place else transposed_image + + exif = exif_image.getexif() + if ExifTags.Base.Orientation in exif: + del exif[ExifTags.Base.Orientation] + if "exif" in exif_image.info: + exif_image.info["exif"] = exif.tobytes() + elif "Raw profile type exif" in exif_image.info: + exif_image.info["Raw profile type exif"] = exif.tobytes().hex() + elif "XML:com.adobe.xmp" in exif_image.info: + for pattern in ( + r'tiff:Orientation="([0-9])"', + r"([0-9])", + ): + exif_image.info["XML:com.adobe.xmp"] = re.sub( + pattern, "", exif_image.info["XML:com.adobe.xmp"] + ) + if not in_place: + return transposed_image + elif not in_place: + return image.copy() + return None diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py index 1ff05a3ef..4bb6c13b3 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -1,270 +1,270 @@ -# -# The Python Imaging Library. -# $Id$ -# -# image palette object -# -# History: -# 1996-03-11 fl Rewritten. -# 1997-01-03 fl Up and running. -# 1997-08-23 fl Added load hack -# 2001-04-16 fl Fixed randint shadow bug in random() -# -# Copyright (c) 1997-2001 by Secret Labs AB -# Copyright (c) 1996-1997 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import array -from typing import IO, TYPE_CHECKING, Sequence - -from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile - -if TYPE_CHECKING: - from . import Image - - -class ImagePalette: - """ - Color palette for palette mapped images - - :param mode: The mode to use for the palette. See: - :ref:`concept-modes`. Defaults to "RGB" - :param palette: An optional palette. If given, it must be a bytearray, - an array or a list of ints between 0-255. The list must consist of - all channels for one color followed by the next color (e.g. RGBRGBRGB). - Defaults to an empty palette. - """ - - def __init__(self, mode: str = "RGB", palette: Sequence[int] | None = None) -> None: - self.mode = mode - self.rawmode = None # if set, palette contains raw data - self.palette = palette or bytearray() - self.dirty: int | None = None - - @property - def palette(self): - return self._palette - - @palette.setter - def palette(self, palette): - self._colors = None - self._palette = palette - - @property - def colors(self): - if self._colors is None: - mode_len = len(self.mode) - self._colors = {} - for i in range(0, len(self.palette), mode_len): - color = tuple(self.palette[i : i + mode_len]) - if color in self._colors: - continue - self._colors[color] = i // mode_len - return self._colors - - @colors.setter - def colors(self, colors): - self._colors = colors - - def copy(self) -> ImagePalette: - new = ImagePalette() - - new.mode = self.mode - new.rawmode = self.rawmode - if self.palette is not None: - new.palette = self.palette[:] - new.dirty = self.dirty - - return new - - def getdata(self) -> tuple[str, bytes]: - """ - Get palette contents in format suitable for the low-level - ``im.putpalette`` primitive. - - .. warning:: This method is experimental. - """ - if self.rawmode: - return self.rawmode, self.palette - return self.mode, self.tobytes() - - def tobytes(self) -> bytes: - """Convert palette to bytes. - - .. warning:: This method is experimental. - """ - if self.rawmode: - msg = "palette contains raw palette data" - raise ValueError(msg) - if isinstance(self.palette, bytes): - return self.palette - arr = array.array("B", self.palette) - return arr.tobytes() - - # Declare tostring as an alias for tobytes - tostring = tobytes - - def _new_color_index(self, image=None, e=None): - if not isinstance(self.palette, bytearray): - self._palette = bytearray(self.palette) - index = len(self.palette) // 3 - special_colors = () - if image: - special_colors = ( - image.info.get("background"), - image.info.get("transparency"), - ) - while index in special_colors: - index += 1 - if index >= 256: - if image: - # Search for an unused index - for i, count in reversed(list(enumerate(image.histogram()))): - if count == 0 and i not in special_colors: - index = i - break - if index >= 256: - msg = "cannot allocate more than 256 colors" - raise ValueError(msg) from e - return index - - def getcolor( - self, - color: tuple[int, int, int] | tuple[int, int, int, int], - image: Image.Image | None = None, - ) -> int: - """Given an rgb tuple, allocate palette entry. - - .. warning:: This method is experimental. - """ - if self.rawmode: - msg = "palette contains raw palette data" - raise ValueError(msg) - if isinstance(color, tuple): - if self.mode == "RGB": - if len(color) == 4: - if color[3] != 255: - msg = "cannot add non-opaque RGBA color to RGB palette" - raise ValueError(msg) - color = color[:3] - elif self.mode == "RGBA": - if len(color) == 3: - color += (255,) - try: - return self.colors[color] - except KeyError as e: - # allocate new color slot - index = self._new_color_index(image, e) - self.colors[color] = index - if index * 3 < len(self.palette): - self._palette = ( - self.palette[: index * 3] - + bytes(color) - + self.palette[index * 3 + 3 :] - ) - else: - self._palette += bytes(color) - self.dirty = 1 - return index - else: - msg = f"unknown color specifier: {repr(color)}" # type: ignore[unreachable] - raise ValueError(msg) - - def save(self, fp: str | IO[str]) -> None: - """Save palette to text file. - - .. warning:: This method is experimental. - """ - if self.rawmode: - msg = "palette contains raw palette data" - raise ValueError(msg) - if isinstance(fp, str): - fp = open(fp, "w") - fp.write("# Palette\n") - fp.write(f"# Mode: {self.mode}\n") - for i in range(256): - fp.write(f"{i}") - for j in range(i * len(self.mode), (i + 1) * len(self.mode)): - try: - fp.write(f" {self.palette[j]}") - except IndexError: - fp.write(" 0") - fp.write("\n") - fp.close() - - -# -------------------------------------------------------------------- -# Internal - - -def raw(rawmode, data) -> ImagePalette: - palette = ImagePalette() - palette.rawmode = rawmode - palette.palette = data - palette.dirty = 1 - return palette - - -# -------------------------------------------------------------------- -# Factories - - -def make_linear_lut(black, white): - if black == 0: - return [white * i // 255 for i in range(256)] - - msg = "unavailable when black is non-zero" - raise NotImplementedError(msg) # FIXME - - -def make_gamma_lut(exp: float) -> list[int]: - return [int(((i / 255.0) ** exp) * 255.0 + 0.5) for i in range(256)] - - -def negative(mode: str = "RGB") -> ImagePalette: - palette = list(range(256 * len(mode))) - palette.reverse() - return ImagePalette(mode, [i // len(mode) for i in palette]) - - -def random(mode: str = "RGB") -> ImagePalette: - from random import randint - - palette = [randint(0, 255) for _ in range(256 * len(mode))] - return ImagePalette(mode, palette) - - -def sepia(white: str = "#fff0c0") -> ImagePalette: - bands = [make_linear_lut(0, band) for band in ImageColor.getrgb(white)] - return ImagePalette("RGB", [bands[i % 3][i // 3] for i in range(256 * 3)]) - - -def wedge(mode: str = "RGB") -> ImagePalette: - palette = list(range(256 * len(mode))) - return ImagePalette(mode, [i // len(mode) for i in palette]) - - -def load(filename): - # FIXME: supports GIMP gradients only - - with open(filename, "rb") as fp: - for paletteHandler in [ - GimpPaletteFile.GimpPaletteFile, - GimpGradientFile.GimpGradientFile, - PaletteFile.PaletteFile, - ]: - try: - fp.seek(0) - lut = paletteHandler(fp).getpalette() - if lut: - break - except (SyntaxError, ValueError): - pass - else: - msg = "cannot load palette" - raise OSError(msg) - - return lut # data, rawmode +# +# The Python Imaging Library. +# $Id$ +# +# image palette object +# +# History: +# 1996-03-11 fl Rewritten. +# 1997-01-03 fl Up and running. +# 1997-08-23 fl Added load hack +# 2001-04-16 fl Fixed randint shadow bug in random() +# +# Copyright (c) 1997-2001 by Secret Labs AB +# Copyright (c) 1996-1997 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import array +from typing import IO, TYPE_CHECKING, Sequence + +from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile + +if TYPE_CHECKING: + from . import Image + + +class ImagePalette: + """ + Color palette for palette mapped images + + :param mode: The mode to use for the palette. See: + :ref:`concept-modes`. Defaults to "RGB" + :param palette: An optional palette. If given, it must be a bytearray, + an array or a list of ints between 0-255. The list must consist of + all channels for one color followed by the next color (e.g. RGBRGBRGB). + Defaults to an empty palette. + """ + + def __init__(self, mode: str = "RGB", palette: Sequence[int] | None = None) -> None: + self.mode = mode + self.rawmode = None # if set, palette contains raw data + self.palette = palette or bytearray() + self.dirty: int | None = None + + @property + def palette(self): + return self._palette + + @palette.setter + def palette(self, palette): + self._colors = None + self._palette = palette + + @property + def colors(self): + if self._colors is None: + mode_len = len(self.mode) + self._colors = {} + for i in range(0, len(self.palette), mode_len): + color = tuple(self.palette[i : i + mode_len]) + if color in self._colors: + continue + self._colors[color] = i // mode_len + return self._colors + + @colors.setter + def colors(self, colors): + self._colors = colors + + def copy(self) -> ImagePalette: + new = ImagePalette() + + new.mode = self.mode + new.rawmode = self.rawmode + if self.palette is not None: + new.palette = self.palette[:] + new.dirty = self.dirty + + return new + + def getdata(self) -> tuple[str, bytes]: + """ + Get palette contents in format suitable for the low-level + ``im.putpalette`` primitive. + + .. warning:: This method is experimental. + """ + if self.rawmode: + return self.rawmode, self.palette + return self.mode, self.tobytes() + + def tobytes(self) -> bytes: + """Convert palette to bytes. + + .. warning:: This method is experimental. + """ + if self.rawmode: + msg = "palette contains raw palette data" + raise ValueError(msg) + if isinstance(self.palette, bytes): + return self.palette + arr = array.array("B", self.palette) + return arr.tobytes() + + # Declare tostring as an alias for tobytes + tostring = tobytes + + def _new_color_index(self, image=None, e=None): + if not isinstance(self.palette, bytearray): + self._palette = bytearray(self.palette) + index = len(self.palette) // 3 + special_colors = () + if image: + special_colors = ( + image.info.get("background"), + image.info.get("transparency"), + ) + while index in special_colors: + index += 1 + if index >= 256: + if image: + # Search for an unused index + for i, count in reversed(list(enumerate(image.histogram()))): + if count == 0 and i not in special_colors: + index = i + break + if index >= 256: + msg = "cannot allocate more than 256 colors" + raise ValueError(msg) from e + return index + + def getcolor( + self, + color: tuple[int, int, int] | tuple[int, int, int, int], + image: Image.Image | None = None, + ) -> int: + """Given an rgb tuple, allocate palette entry. + + .. warning:: This method is experimental. + """ + if self.rawmode: + msg = "palette contains raw palette data" + raise ValueError(msg) + if isinstance(color, tuple): + if self.mode == "RGB": + if len(color) == 4: + if color[3] != 255: + msg = "cannot add non-opaque RGBA color to RGB palette" + raise ValueError(msg) + color = color[:3] + elif self.mode == "RGBA": + if len(color) == 3: + color += (255,) + try: + return self.colors[color] + except KeyError as e: + # allocate new color slot + index = self._new_color_index(image, e) + self.colors[color] = index + if index * 3 < len(self.palette): + self._palette = ( + self.palette[: index * 3] + + bytes(color) + + self.palette[index * 3 + 3 :] + ) + else: + self._palette += bytes(color) + self.dirty = 1 + return index + else: + msg = f"unknown color specifier: {repr(color)}" # type: ignore[unreachable] + raise ValueError(msg) + + def save(self, fp: str | IO[str]) -> None: + """Save palette to text file. + + .. warning:: This method is experimental. + """ + if self.rawmode: + msg = "palette contains raw palette data" + raise ValueError(msg) + if isinstance(fp, str): + fp = open(fp, "w") + fp.write("# Palette\n") + fp.write(f"# Mode: {self.mode}\n") + for i in range(256): + fp.write(f"{i}") + for j in range(i * len(self.mode), (i + 1) * len(self.mode)): + try: + fp.write(f" {self.palette[j]}") + except IndexError: + fp.write(" 0") + fp.write("\n") + fp.close() + + +# -------------------------------------------------------------------- +# Internal + + +def raw(rawmode, data) -> ImagePalette: + palette = ImagePalette() + palette.rawmode = rawmode + palette.palette = data + palette.dirty = 1 + return palette + + +# -------------------------------------------------------------------- +# Factories + + +def make_linear_lut(black, white): + if black == 0: + return [white * i // 255 for i in range(256)] + + msg = "unavailable when black is non-zero" + raise NotImplementedError(msg) # FIXME + + +def make_gamma_lut(exp: float) -> list[int]: + return [int(((i / 255.0) ** exp) * 255.0 + 0.5) for i in range(256)] + + +def negative(mode: str = "RGB") -> ImagePalette: + palette = list(range(256 * len(mode))) + palette.reverse() + return ImagePalette(mode, [i // len(mode) for i in palette]) + + +def random(mode: str = "RGB") -> ImagePalette: + from random import randint + + palette = [randint(0, 255) for _ in range(256 * len(mode))] + return ImagePalette(mode, palette) + + +def sepia(white: str = "#fff0c0") -> ImagePalette: + bands = [make_linear_lut(0, band) for band in ImageColor.getrgb(white)] + return ImagePalette("RGB", [bands[i % 3][i // 3] for i in range(256 * 3)]) + + +def wedge(mode: str = "RGB") -> ImagePalette: + palette = list(range(256 * len(mode))) + return ImagePalette(mode, [i // len(mode) for i in palette]) + + +def load(filename): + # FIXME: supports GIMP gradients only + + with open(filename, "rb") as fp: + for paletteHandler in [ + GimpPaletteFile.GimpPaletteFile, + GimpGradientFile.GimpGradientFile, + PaletteFile.PaletteFile, + ]: + try: + fp.seek(0) + lut = paletteHandler(fp).getpalette() + if lut: + break + except (SyntaxError, ValueError): + pass + else: + msg = "cannot load palette" + raise OSError(msg) + + return lut # data, rawmode diff --git a/src/PIL/ImagePath.py b/src/PIL/ImagePath.py index 77e8a609a..c986e9095 100644 --- a/src/PIL/ImagePath.py +++ b/src/PIL/ImagePath.py @@ -1,20 +1,20 @@ -# -# The Python Imaging Library -# $Id$ -# -# path interface -# -# History: -# 1996-11-04 fl Created -# 2002-04-14 fl Added documentation stub class -# -# Copyright (c) Secret Labs AB 1997. -# Copyright (c) Fredrik Lundh 1996. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -from . import Image - -Path = Image.core.path +# +# The Python Imaging Library +# $Id$ +# +# path interface +# +# History: +# 1996-11-04 fl Created +# 2002-04-14 fl Added documentation stub class +# +# Copyright (c) Secret Labs AB 1997. +# Copyright (c) Fredrik Lundh 1996. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +from . import Image + +Path = Image.core.path diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py index 293ba4941..95670c7f7 100644 --- a/src/PIL/ImageQt.py +++ b/src/PIL/ImageQt.py @@ -1,205 +1,205 @@ -# -# The Python Imaging Library. -# $Id$ -# -# a simple Qt image interface. -# -# history: -# 2006-06-03 fl: created -# 2006-06-04 fl: inherit from QImage instead of wrapping it -# 2006-06-05 fl: removed toimage helper; move string support to ImageQt -# 2013-11-13 fl: add support for Qt5 (aurelien.ballier@cyclonit.com) -# -# Copyright (c) 2006 by Secret Labs AB -# Copyright (c) 2006 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import sys -from io import BytesIO -from typing import Callable - -from . import Image -from ._util import is_path - -qt_version: str | None -qt_versions = [ - ["6", "PyQt6"], - ["side6", "PySide6"], -] - -# If a version has already been imported, attempt it first -qt_versions.sort(key=lambda version: version[1] in sys.modules, reverse=True) -for version, qt_module in qt_versions: - try: - QBuffer: type - QIODevice: type - QImage: type - QPixmap: type - qRgba: Callable[[int, int, int, int], int] - if qt_module == "PyQt6": - from PyQt6.QtCore import QBuffer, QIODevice - from PyQt6.QtGui import QImage, QPixmap, qRgba - elif qt_module == "PySide6": - from PySide6.QtCore import QBuffer, QIODevice - from PySide6.QtGui import QImage, QPixmap, qRgba - except (ImportError, RuntimeError): - continue - qt_is_installed = True - qt_version = version - break -else: - qt_is_installed = False - qt_version = None - - -def rgb(r, g, b, a=255): - """(Internal) Turns an RGB color into a Qt compatible color integer.""" - # use qRgb to pack the colors, and then turn the resulting long - # into a negative integer with the same bitpattern. - return qRgba(r, g, b, a) & 0xFFFFFFFF - - -def fromqimage(im): - """ - :param im: QImage or PIL ImageQt object - """ - buffer = QBuffer() - if qt_version == "6": - try: - qt_openmode = QIODevice.OpenModeFlag - except AttributeError: - qt_openmode = QIODevice.OpenMode - else: - qt_openmode = QIODevice - buffer.open(qt_openmode.ReadWrite) - # preserve alpha channel with png - # otherwise ppm is more friendly with Image.open - if im.hasAlphaChannel(): - im.save(buffer, "png") - else: - im.save(buffer, "ppm") - - b = BytesIO() - b.write(buffer.data()) - buffer.close() - b.seek(0) - - return Image.open(b) - - -def fromqpixmap(im): - return fromqimage(im) - - -def align8to32(bytes, width, mode): - """ - converts each scanline of data from 8 bit to 32 bit aligned - """ - - bits_per_pixel = {"1": 1, "L": 8, "P": 8, "I;16": 16}[mode] - - # calculate bytes per line and the extra padding if needed - bits_per_line = bits_per_pixel * width - full_bytes_per_line, remaining_bits_per_line = divmod(bits_per_line, 8) - bytes_per_line = full_bytes_per_line + (1 if remaining_bits_per_line else 0) - - extra_padding = -bytes_per_line % 4 - - # already 32 bit aligned by luck - if not extra_padding: - return bytes - - new_data = [ - bytes[i * bytes_per_line : (i + 1) * bytes_per_line] + b"\x00" * extra_padding - for i in range(len(bytes) // bytes_per_line) - ] - - return b"".join(new_data) - - -def _toqclass_helper(im): - data = None - colortable = None - exclusive_fp = False - - # handle filename, if given instead of image name - if hasattr(im, "toUtf8"): - # FIXME - is this really the best way to do this? - im = str(im.toUtf8(), "utf-8") - if is_path(im): - im = Image.open(im) - exclusive_fp = True - - qt_format = QImage.Format if qt_version == "6" else QImage - if im.mode == "1": - format = qt_format.Format_Mono - elif im.mode == "L": - format = qt_format.Format_Indexed8 - colortable = [rgb(i, i, i) for i in range(256)] - elif im.mode == "P": - format = qt_format.Format_Indexed8 - palette = im.getpalette() - colortable = [rgb(*palette[i : i + 3]) for i in range(0, len(palette), 3)] - elif im.mode == "RGB": - # Populate the 4th channel with 255 - im = im.convert("RGBA") - - data = im.tobytes("raw", "BGRA") - format = qt_format.Format_RGB32 - elif im.mode == "RGBA": - data = im.tobytes("raw", "BGRA") - format = qt_format.Format_ARGB32 - elif im.mode == "I;16" and hasattr(qt_format, "Format_Grayscale16"): # Qt 5.13+ - im = im.point(lambda i: i * 256) - - format = qt_format.Format_Grayscale16 - else: - if exclusive_fp: - im.close() - msg = f"unsupported image mode {repr(im.mode)}" - raise ValueError(msg) - - size = im.size - __data = data or align8to32(im.tobytes(), size[0], im.mode) - if exclusive_fp: - im.close() - return {"data": __data, "size": size, "format": format, "colortable": colortable} - - -if qt_is_installed: - - class ImageQt(QImage): - def __init__(self, im): - """ - An PIL image wrapper for Qt. This is a subclass of PyQt's QImage - class. - - :param im: A PIL Image object, or a file name (given either as - Python string or a PyQt string object). - """ - im_data = _toqclass_helper(im) - # must keep a reference, or Qt will crash! - # All QImage constructors that take data operate on an existing - # buffer, so this buffer has to hang on for the life of the image. - # Fixes https://github.com/python-pillow/Pillow/issues/1370 - self.__data = im_data["data"] - super().__init__( - self.__data, - im_data["size"][0], - im_data["size"][1], - im_data["format"], - ) - if im_data["colortable"]: - self.setColorTable(im_data["colortable"]) - - -def toqimage(im): - return ImageQt(im) - - -def toqpixmap(im): - qimage = toqimage(im) - return QPixmap.fromImage(qimage) +# +# The Python Imaging Library. +# $Id$ +# +# a simple Qt image interface. +# +# history: +# 2006-06-03 fl: created +# 2006-06-04 fl: inherit from QImage instead of wrapping it +# 2006-06-05 fl: removed toimage helper; move string support to ImageQt +# 2013-11-13 fl: add support for Qt5 (aurelien.ballier@cyclonit.com) +# +# Copyright (c) 2006 by Secret Labs AB +# Copyright (c) 2006 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import sys +from io import BytesIO +from typing import Callable + +from . import Image +from ._util import is_path + +qt_version: str | None +qt_versions = [ + ["6", "PyQt6"], + ["side6", "PySide6"], +] + +# If a version has already been imported, attempt it first +qt_versions.sort(key=lambda version: version[1] in sys.modules, reverse=True) +for version, qt_module in qt_versions: + try: + QBuffer: type + QIODevice: type + QImage: type + QPixmap: type + qRgba: Callable[[int, int, int, int], int] + if qt_module == "PyQt6": + from PyQt6.QtCore import QBuffer, QIODevice + from PyQt6.QtGui import QImage, QPixmap, qRgba + elif qt_module == "PySide6": + from PySide6.QtCore import QBuffer, QIODevice + from PySide6.QtGui import QImage, QPixmap, qRgba + except (ImportError, RuntimeError): + continue + qt_is_installed = True + qt_version = version + break +else: + qt_is_installed = False + qt_version = None + + +def rgb(r, g, b, a=255): + """(Internal) Turns an RGB color into a Qt compatible color integer.""" + # use qRgb to pack the colors, and then turn the resulting long + # into a negative integer with the same bitpattern. + return qRgba(r, g, b, a) & 0xFFFFFFFF + + +def fromqimage(im): + """ + :param im: QImage or PIL ImageQt object + """ + buffer = QBuffer() + if qt_version == "6": + try: + qt_openmode = QIODevice.OpenModeFlag + except AttributeError: + qt_openmode = QIODevice.OpenMode + else: + qt_openmode = QIODevice + buffer.open(qt_openmode.ReadWrite) + # preserve alpha channel with png + # otherwise ppm is more friendly with Image.open + if im.hasAlphaChannel(): + im.save(buffer, "png") + else: + im.save(buffer, "ppm") + + b = BytesIO() + b.write(buffer.data()) + buffer.close() + b.seek(0) + + return Image.open(b) + + +def fromqpixmap(im): + return fromqimage(im) + + +def align8to32(bytes, width, mode): + """ + converts each scanline of data from 8 bit to 32 bit aligned + """ + + bits_per_pixel = {"1": 1, "L": 8, "P": 8, "I;16": 16}[mode] + + # calculate bytes per line and the extra padding if needed + bits_per_line = bits_per_pixel * width + full_bytes_per_line, remaining_bits_per_line = divmod(bits_per_line, 8) + bytes_per_line = full_bytes_per_line + (1 if remaining_bits_per_line else 0) + + extra_padding = -bytes_per_line % 4 + + # already 32 bit aligned by luck + if not extra_padding: + return bytes + + new_data = [ + bytes[i * bytes_per_line : (i + 1) * bytes_per_line] + b"\x00" * extra_padding + for i in range(len(bytes) // bytes_per_line) + ] + + return b"".join(new_data) + + +def _toqclass_helper(im): + data = None + colortable = None + exclusive_fp = False + + # handle filename, if given instead of image name + if hasattr(im, "toUtf8"): + # FIXME - is this really the best way to do this? + im = str(im.toUtf8(), "utf-8") + if is_path(im): + im = Image.open(im) + exclusive_fp = True + + qt_format = QImage.Format if qt_version == "6" else QImage + if im.mode == "1": + format = qt_format.Format_Mono + elif im.mode == "L": + format = qt_format.Format_Indexed8 + colortable = [rgb(i, i, i) for i in range(256)] + elif im.mode == "P": + format = qt_format.Format_Indexed8 + palette = im.getpalette() + colortable = [rgb(*palette[i : i + 3]) for i in range(0, len(palette), 3)] + elif im.mode == "RGB": + # Populate the 4th channel with 255 + im = im.convert("RGBA") + + data = im.tobytes("raw", "BGRA") + format = qt_format.Format_RGB32 + elif im.mode == "RGBA": + data = im.tobytes("raw", "BGRA") + format = qt_format.Format_ARGB32 + elif im.mode == "I;16" and hasattr(qt_format, "Format_Grayscale16"): # Qt 5.13+ + im = im.point(lambda i: i * 256) + + format = qt_format.Format_Grayscale16 + else: + if exclusive_fp: + im.close() + msg = f"unsupported image mode {repr(im.mode)}" + raise ValueError(msg) + + size = im.size + __data = data or align8to32(im.tobytes(), size[0], im.mode) + if exclusive_fp: + im.close() + return {"data": __data, "size": size, "format": format, "colortable": colortable} + + +if qt_is_installed: + + class ImageQt(QImage): + def __init__(self, im): + """ + An PIL image wrapper for Qt. This is a subclass of PyQt's QImage + class. + + :param im: A PIL Image object, or a file name (given either as + Python string or a PyQt string object). + """ + im_data = _toqclass_helper(im) + # must keep a reference, or Qt will crash! + # All QImage constructors that take data operate on an existing + # buffer, so this buffer has to hang on for the life of the image. + # Fixes https://github.com/python-pillow/Pillow/issues/1370 + self.__data = im_data["data"] + super().__init__( + self.__data, + im_data["size"][0], + im_data["size"][1], + im_data["format"], + ) + if im_data["colortable"]: + self.setColorTable(im_data["colortable"]) + + +def toqimage(im): + return ImageQt(im) + + +def toqpixmap(im): + qimage = toqimage(im) + return QPixmap.fromImage(qimage) diff --git a/src/PIL/ImageSequence.py b/src/PIL/ImageSequence.py index 2c1850276..11bd04be4 100644 --- a/src/PIL/ImageSequence.py +++ b/src/PIL/ImageSequence.py @@ -1,86 +1,86 @@ -# -# The Python Imaging Library. -# $Id$ -# -# sequence support classes -# -# history: -# 1997-02-20 fl Created -# -# Copyright (c) 1997 by Secret Labs AB. -# Copyright (c) 1997 by Fredrik Lundh. -# -# See the README file for information on usage and redistribution. -# - -## -from __future__ import annotations - -from typing import Callable - -from . import Image - - -class Iterator: - """ - This class implements an iterator object that can be used to loop - over an image sequence. - - You can use the ``[]`` operator to access elements by index. This operator - will raise an :py:exc:`IndexError` if you try to access a nonexistent - frame. - - :param im: An image object. - """ - - def __init__(self, im: Image.Image): - if not hasattr(im, "seek"): - msg = "im must have seek method" - raise AttributeError(msg) - self.im = im - self.position = getattr(self.im, "_min_frame", 0) - - def __getitem__(self, ix: int) -> Image.Image: - try: - self.im.seek(ix) - return self.im - except EOFError as e: - msg = "end of sequence" - raise IndexError(msg) from e - - def __iter__(self) -> Iterator: - return self - - def __next__(self) -> Image.Image: - try: - self.im.seek(self.position) - self.position += 1 - return self.im - except EOFError as e: - msg = "end of sequence" - raise StopIteration(msg) from e - - -def all_frames( - im: Image.Image | list[Image.Image], - func: Callable[[Image.Image], Image.Image] | None = None, -) -> list[Image.Image]: - """ - Applies a given function to all frames in an image or a list of images. - The frames are returned as a list of separate images. - - :param im: An image, or a list of images. - :param func: The function to apply to all of the image frames. - :returns: A list of images. - """ - if not isinstance(im, list): - im = [im] - - ims = [] - for imSequence in im: - current = imSequence.tell() - - ims += [im_frame.copy() for im_frame in Iterator(imSequence)] - - imSequence.seek(current) - return [func(im) for im in ims] if func else ims +# +# The Python Imaging Library. +# $Id$ +# +# sequence support classes +# +# history: +# 1997-02-20 fl Created +# +# Copyright (c) 1997 by Secret Labs AB. +# Copyright (c) 1997 by Fredrik Lundh. +# +# See the README file for information on usage and redistribution. +# + +## +from __future__ import annotations + +from typing import Callable + +from . import Image + + +class Iterator: + """ + This class implements an iterator object that can be used to loop + over an image sequence. + + You can use the ``[]`` operator to access elements by index. This operator + will raise an :py:exc:`IndexError` if you try to access a nonexistent + frame. + + :param im: An image object. + """ + + def __init__(self, im: Image.Image): + if not hasattr(im, "seek"): + msg = "im must have seek method" + raise AttributeError(msg) + self.im = im + self.position = getattr(self.im, "_min_frame", 0) + + def __getitem__(self, ix: int) -> Image.Image: + try: + self.im.seek(ix) + return self.im + except EOFError as e: + msg = "end of sequence" + raise IndexError(msg) from e + + def __iter__(self) -> Iterator: + return self + + def __next__(self) -> Image.Image: + try: + self.im.seek(self.position) + self.position += 1 + return self.im + except EOFError as e: + msg = "end of sequence" + raise StopIteration(msg) from e + + +def all_frames( + im: Image.Image | list[Image.Image], + func: Callable[[Image.Image], Image.Image] | None = None, +) -> list[Image.Image]: + """ + Applies a given function to all frames in an image or a list of images. + The frames are returned as a list of separate images. + + :param im: An image, or a list of images. + :param func: The function to apply to all of the image frames. + :returns: A list of images. + """ + if not isinstance(im, list): + im = [im] + + ims = [] + for imSequence in im: + current = imSequence.tell() + + ims += [im_frame.copy() for im_frame in Iterator(imSequence)] + + imSequence.seek(current) + return [func(im) for im in ims] if func else ims diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index f60b1e11e..1c2c7be84 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -1,347 +1,347 @@ -# -# The Python Imaging Library. -# $Id$ -# -# im.show() drivers -# -# History: -# 2008-04-06 fl Created -# -# Copyright (c) Secret Labs AB 2008. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import abc -import os -import shutil -import subprocess -import sys -from shlex import quote -from typing import Any - -from . import Image - -_viewers = [] - - -def register(viewer, order: int = 1) -> None: - """ - The :py:func:`register` function is used to register additional viewers:: - - from PIL import ImageShow - ImageShow.register(MyViewer()) # MyViewer will be used as a last resort - ImageShow.register(MySecondViewer(), 0) # MySecondViewer will be prioritised - ImageShow.register(ImageShow.XVViewer(), 0) # XVViewer will be prioritised - - :param viewer: The viewer to be registered. - :param order: - Zero or a negative integer to prepend this viewer to the list, - a positive integer to append it. - """ - try: - if issubclass(viewer, Viewer): - viewer = viewer() - except TypeError: - pass # raised if viewer wasn't a class - if order > 0: - _viewers.append(viewer) - else: - _viewers.insert(0, viewer) - - -def show(image: Image.Image, title: str | None = None, **options: Any) -> bool: - r""" - Display a given image. - - :param image: An image object. - :param title: Optional title. Not all viewers can display the title. - :param \**options: Additional viewer options. - :returns: ``True`` if a suitable viewer was found, ``False`` otherwise. - """ - for viewer in _viewers: - if viewer.show(image, title=title, **options): - return True - return False - - -class Viewer: - """Base class for viewers.""" - - # main api - - def show(self, image: Image.Image, **options: Any) -> int: - """ - The main function for displaying an image. - Converts the given image to the target format and displays it. - """ - - if not ( - image.mode in ("1", "RGBA") - or (self.format == "PNG" and image.mode in ("I;16", "LA")) - ): - base = Image.getmodebase(image.mode) - if image.mode != base: - image = image.convert(base) - - return self.show_image(image, **options) - - # hook methods - - format: str | None = None - """The format to convert the image into.""" - options: dict[str, Any] = {} - """Additional options used to convert the image.""" - - def get_format(self, image: Image.Image) -> str | None: - """Return format name, or ``None`` to save as PGM/PPM.""" - return self.format - - def get_command(self, file: str, **options: Any) -> str: - """ - Returns the command used to display the file. - Not implemented in the base class. - """ - msg = "unavailable in base viewer" - raise NotImplementedError(msg) - - def save_image(self, image: Image.Image) -> str: - """Save to temporary file and return filename.""" - return image._dump(format=self.get_format(image), **self.options) - - def show_image(self, image: Image.Image, **options: Any) -> int: - """Display the given image.""" - return self.show_file(self.save_image(image), **options) - - def show_file(self, path: str, **options: Any) -> int: - """ - Display given file. - """ - os.system(self.get_command(path, **options)) # nosec - return 1 - - -# -------------------------------------------------------------------- - - -class WindowsViewer(Viewer): - """The default viewer on Windows is the default system application for PNG files.""" - - format = "PNG" - options = {"compress_level": 1, "save_all": True} - - def get_command(self, file: str, **options: Any) -> str: - return ( - f'start "Pillow" /WAIT "{file}" ' - "&& ping -n 4 127.0.0.1 >NUL " - f'&& del /f "{file}"' - ) - - def show_file(self, path: str, **options: Any) -> int: - """ - Display given file. - """ - subprocess.Popen( - self.get_command(path, **options), - shell=True, - creationflags=getattr(subprocess, "CREATE_NO_WINDOW"), - ) # nosec - return 1 - - -if sys.platform == "win32": - register(WindowsViewer) - - -class MacViewer(Viewer): - """The default viewer on macOS using ``Preview.app``.""" - - format = "PNG" - options = {"compress_level": 1, "save_all": True} - - def get_command(self, file: str, **options: Any) -> str: - # on darwin open returns immediately resulting in the temp - # file removal while app is opening - command = "open -a Preview.app" - command = f"({command} {quote(file)}; sleep 20; rm -f {quote(file)})&" - return command - - def show_file(self, path: str, **options: Any) -> int: - """ - Display given file. - """ - subprocess.call(["open", "-a", "Preview.app", path]) - executable = sys.executable or shutil.which("python3") - if executable: - subprocess.Popen( - [ - executable, - "-c", - "import os, sys, time; time.sleep(20); os.remove(sys.argv[1])", - path, - ] - ) - return 1 - - -if sys.platform == "darwin": - register(MacViewer) - - -class UnixViewer(Viewer): - format = "PNG" - options = {"compress_level": 1, "save_all": True} - - @abc.abstractmethod - def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]: - pass - - def get_command(self, file: str, **options: Any) -> str: - command = self.get_command_ex(file, **options)[0] - return f"{command} {quote(file)}" - - -class XDGViewer(UnixViewer): - """ - The freedesktop.org ``xdg-open`` command. - """ - - def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]: - command = executable = "xdg-open" - return command, executable - - def show_file(self, path: str, **options: Any) -> int: - """ - Display given file. - """ - subprocess.Popen(["xdg-open", path]) - return 1 - - -class DisplayViewer(UnixViewer): - """ - The ImageMagick ``display`` command. - This viewer supports the ``title`` parameter. - """ - - def get_command_ex( - self, file: str, title: str | None = None, **options: Any - ) -> tuple[str, str]: - command = executable = "display" - if title: - command += f" -title {quote(title)}" - return command, executable - - def show_file(self, path: str, **options: Any) -> int: - """ - Display given file. - """ - args = ["display"] - title = options.get("title") - if title: - args += ["-title", title] - args.append(path) - - subprocess.Popen(args) - return 1 - - -class GmDisplayViewer(UnixViewer): - """The GraphicsMagick ``gm display`` command.""" - - def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]: - executable = "gm" - command = "gm display" - return command, executable - - def show_file(self, path: str, **options: Any) -> int: - """ - Display given file. - """ - subprocess.Popen(["gm", "display", path]) - return 1 - - -class EogViewer(UnixViewer): - """The GNOME Image Viewer ``eog`` command.""" - - def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]: - executable = "eog" - command = "eog -n" - return command, executable - - def show_file(self, path: str, **options: Any) -> int: - """ - Display given file. - """ - subprocess.Popen(["eog", "-n", path]) - return 1 - - -class XVViewer(UnixViewer): - """ - The X Viewer ``xv`` command. - This viewer supports the ``title`` parameter. - """ - - def get_command_ex( - self, file: str, title: str | None = None, **options: Any - ) -> tuple[str, str]: - # note: xv is pretty outdated. most modern systems have - # imagemagick's display command instead. - command = executable = "xv" - if title: - command += f" -name {quote(title)}" - return command, executable - - def show_file(self, path: str, **options: Any) -> int: - """ - Display given file. - """ - args = ["xv"] - title = options.get("title") - if title: - args += ["-name", title] - args.append(path) - - subprocess.Popen(args) - return 1 - - -if sys.platform not in ("win32", "darwin"): # unixoids - if shutil.which("xdg-open"): - register(XDGViewer) - if shutil.which("display"): - register(DisplayViewer) - if shutil.which("gm"): - register(GmDisplayViewer) - if shutil.which("eog"): - register(EogViewer) - if shutil.which("xv"): - register(XVViewer) - - -class IPythonViewer(Viewer): - """The viewer for IPython frontends.""" - - def show_image(self, image: Image.Image, **options: Any) -> int: - ipython_display(image) - return 1 - - -try: - from IPython.display import display as ipython_display -except ImportError: - pass -else: - register(IPythonViewer) - - -if __name__ == "__main__": - if len(sys.argv) < 2: - print("Syntax: python3 ImageShow.py imagefile [title]") - sys.exit() - - with Image.open(sys.argv[1]) as im: - print(show(im, *sys.argv[2:])) +# +# The Python Imaging Library. +# $Id$ +# +# im.show() drivers +# +# History: +# 2008-04-06 fl Created +# +# Copyright (c) Secret Labs AB 2008. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import abc +import os +import shutil +import subprocess +import sys +from shlex import quote +from typing import Any + +from . import Image + +_viewers = [] + + +def register(viewer, order: int = 1) -> None: + """ + The :py:func:`register` function is used to register additional viewers:: + + from PIL import ImageShow + ImageShow.register(MyViewer()) # MyViewer will be used as a last resort + ImageShow.register(MySecondViewer(), 0) # MySecondViewer will be prioritised + ImageShow.register(ImageShow.XVViewer(), 0) # XVViewer will be prioritised + + :param viewer: The viewer to be registered. + :param order: + Zero or a negative integer to prepend this viewer to the list, + a positive integer to append it. + """ + try: + if issubclass(viewer, Viewer): + viewer = viewer() + except TypeError: + pass # raised if viewer wasn't a class + if order > 0: + _viewers.append(viewer) + else: + _viewers.insert(0, viewer) + + +def show(image: Image.Image, title: str | None = None, **options: Any) -> bool: + r""" + Display a given image. + + :param image: An image object. + :param title: Optional title. Not all viewers can display the title. + :param \**options: Additional viewer options. + :returns: ``True`` if a suitable viewer was found, ``False`` otherwise. + """ + for viewer in _viewers: + if viewer.show(image, title=title, **options): + return True + return False + + +class Viewer: + """Base class for viewers.""" + + # main api + + def show(self, image: Image.Image, **options: Any) -> int: + """ + The main function for displaying an image. + Converts the given image to the target format and displays it. + """ + + if not ( + image.mode in ("1", "RGBA") + or (self.format == "PNG" and image.mode in ("I;16", "LA")) + ): + base = Image.getmodebase(image.mode) + if image.mode != base: + image = image.convert(base) + + return self.show_image(image, **options) + + # hook methods + + format: str | None = None + """The format to convert the image into.""" + options: dict[str, Any] = {} + """Additional options used to convert the image.""" + + def get_format(self, image: Image.Image) -> str | None: + """Return format name, or ``None`` to save as PGM/PPM.""" + return self.format + + def get_command(self, file: str, **options: Any) -> str: + """ + Returns the command used to display the file. + Not implemented in the base class. + """ + msg = "unavailable in base viewer" + raise NotImplementedError(msg) + + def save_image(self, image: Image.Image) -> str: + """Save to temporary file and return filename.""" + return image._dump(format=self.get_format(image), **self.options) + + def show_image(self, image: Image.Image, **options: Any) -> int: + """Display the given image.""" + return self.show_file(self.save_image(image), **options) + + def show_file(self, path: str, **options: Any) -> int: + """ + Display given file. + """ + os.system(self.get_command(path, **options)) # nosec + return 1 + + +# -------------------------------------------------------------------- + + +class WindowsViewer(Viewer): + """The default viewer on Windows is the default system application for PNG files.""" + + format = "PNG" + options = {"compress_level": 1, "save_all": True} + + def get_command(self, file: str, **options: Any) -> str: + return ( + f'start "Pillow" /WAIT "{file}" ' + "&& ping -n 4 127.0.0.1 >NUL " + f'&& del /f "{file}"' + ) + + def show_file(self, path: str, **options: Any) -> int: + """ + Display given file. + """ + subprocess.Popen( + self.get_command(path, **options), + shell=True, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW"), + ) # nosec + return 1 + + +if sys.platform == "win32": + register(WindowsViewer) + + +class MacViewer(Viewer): + """The default viewer on macOS using ``Preview.app``.""" + + format = "PNG" + options = {"compress_level": 1, "save_all": True} + + def get_command(self, file: str, **options: Any) -> str: + # on darwin open returns immediately resulting in the temp + # file removal while app is opening + command = "open -a Preview.app" + command = f"({command} {quote(file)}; sleep 20; rm -f {quote(file)})&" + return command + + def show_file(self, path: str, **options: Any) -> int: + """ + Display given file. + """ + subprocess.call(["open", "-a", "Preview.app", path]) + executable = sys.executable or shutil.which("python3") + if executable: + subprocess.Popen( + [ + executable, + "-c", + "import os, sys, time; time.sleep(20); os.remove(sys.argv[1])", + path, + ] + ) + return 1 + + +if sys.platform == "darwin": + register(MacViewer) + + +class UnixViewer(Viewer): + format = "PNG" + options = {"compress_level": 1, "save_all": True} + + @abc.abstractmethod + def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]: + pass + + def get_command(self, file: str, **options: Any) -> str: + command = self.get_command_ex(file, **options)[0] + return f"{command} {quote(file)}" + + +class XDGViewer(UnixViewer): + """ + The freedesktop.org ``xdg-open`` command. + """ + + def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]: + command = executable = "xdg-open" + return command, executable + + def show_file(self, path: str, **options: Any) -> int: + """ + Display given file. + """ + subprocess.Popen(["xdg-open", path]) + return 1 + + +class DisplayViewer(UnixViewer): + """ + The ImageMagick ``display`` command. + This viewer supports the ``title`` parameter. + """ + + def get_command_ex( + self, file: str, title: str | None = None, **options: Any + ) -> tuple[str, str]: + command = executable = "display" + if title: + command += f" -title {quote(title)}" + return command, executable + + def show_file(self, path: str, **options: Any) -> int: + """ + Display given file. + """ + args = ["display"] + title = options.get("title") + if title: + args += ["-title", title] + args.append(path) + + subprocess.Popen(args) + return 1 + + +class GmDisplayViewer(UnixViewer): + """The GraphicsMagick ``gm display`` command.""" + + def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]: + executable = "gm" + command = "gm display" + return command, executable + + def show_file(self, path: str, **options: Any) -> int: + """ + Display given file. + """ + subprocess.Popen(["gm", "display", path]) + return 1 + + +class EogViewer(UnixViewer): + """The GNOME Image Viewer ``eog`` command.""" + + def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]: + executable = "eog" + command = "eog -n" + return command, executable + + def show_file(self, path: str, **options: Any) -> int: + """ + Display given file. + """ + subprocess.Popen(["eog", "-n", path]) + return 1 + + +class XVViewer(UnixViewer): + """ + The X Viewer ``xv`` command. + This viewer supports the ``title`` parameter. + """ + + def get_command_ex( + self, file: str, title: str | None = None, **options: Any + ) -> tuple[str, str]: + # note: xv is pretty outdated. most modern systems have + # imagemagick's display command instead. + command = executable = "xv" + if title: + command += f" -name {quote(title)}" + return command, executable + + def show_file(self, path: str, **options: Any) -> int: + """ + Display given file. + """ + args = ["xv"] + title = options.get("title") + if title: + args += ["-name", title] + args.append(path) + + subprocess.Popen(args) + return 1 + + +if sys.platform not in ("win32", "darwin"): # unixoids + if shutil.which("xdg-open"): + register(XDGViewer) + if shutil.which("display"): + register(DisplayViewer) + if shutil.which("gm"): + register(GmDisplayViewer) + if shutil.which("eog"): + register(EogViewer) + if shutil.which("xv"): + register(XVViewer) + + +class IPythonViewer(Viewer): + """The viewer for IPython frontends.""" + + def show_image(self, image: Image.Image, **options: Any) -> int: + ipython_display(image) + return 1 + + +try: + from IPython.display import display as ipython_display +except ImportError: + pass +else: + register(IPythonViewer) + + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Syntax: python3 ImageShow.py imagefile [title]") + sys.exit() + + with Image.open(sys.argv[1]) as im: + print(show(im, *sys.argv[2:])) diff --git a/src/PIL/ImageStat.py b/src/PIL/ImageStat.py index 8bc504526..eb4162f6b 100644 --- a/src/PIL/ImageStat.py +++ b/src/PIL/ImageStat.py @@ -1,160 +1,160 @@ -# -# The Python Imaging Library. -# $Id$ -# -# global image statistics -# -# History: -# 1996-04-05 fl Created -# 1997-05-21 fl Added mask; added rms, var, stddev attributes -# 1997-08-05 fl Added median -# 1998-07-05 hk Fixed integer overflow error -# -# Notes: -# This class shows how to implement delayed evaluation of attributes. -# To get a certain value, simply access the corresponding attribute. -# The __getattr__ dispatcher takes care of the rest. -# -# Copyright (c) Secret Labs AB 1997. -# Copyright (c) Fredrik Lundh 1996-97. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import math -from functools import cached_property - -from . import Image - - -class Stat: - def __init__( - self, image_or_list: Image.Image | list[int], mask: Image.Image | None = None - ) -> None: - """ - Calculate statistics for the given image. If a mask is included, - only the regions covered by that mask are included in the - statistics. You can also pass in a previously calculated histogram. - - :param image: A PIL image, or a precalculated histogram. - - .. note:: - - For a PIL image, calculations rely on the - :py:meth:`~PIL.Image.Image.histogram` method. The pixel counts are - grouped into 256 bins, even if the image has more than 8 bits per - channel. So ``I`` and ``F`` mode images have a maximum ``mean``, - ``median`` and ``rms`` of 255, and cannot have an ``extrema`` maximum - of more than 255. - - :param mask: An optional mask. - """ - if isinstance(image_or_list, Image.Image): - self.h = image_or_list.histogram(mask) - elif isinstance(image_or_list, list): - self.h = image_or_list - else: - msg = "first argument must be image or list" # type: ignore[unreachable] - raise TypeError(msg) - self.bands = list(range(len(self.h) // 256)) - - @cached_property - def extrema(self) -> list[tuple[int, int]]: - """ - Min/max values for each band in the image. - - .. note:: - This relies on the :py:meth:`~PIL.Image.Image.histogram` method, and - simply returns the low and high bins used. This is correct for - images with 8 bits per channel, but fails for other modes such as - ``I`` or ``F``. Instead, use :py:meth:`~PIL.Image.Image.getextrema` to - return per-band extrema for the image. This is more correct and - efficient because, for non-8-bit modes, the histogram method uses - :py:meth:`~PIL.Image.Image.getextrema` to determine the bins used. - """ - - def minmax(histogram: list[int]) -> tuple[int, int]: - res_min, res_max = 255, 0 - for i in range(256): - if histogram[i]: - res_min = i - break - for i in range(255, -1, -1): - if histogram[i]: - res_max = i - break - return res_min, res_max - - return [minmax(self.h[i:]) for i in range(0, len(self.h), 256)] - - @cached_property - def count(self) -> list[int]: - """Total number of pixels for each band in the image.""" - return [sum(self.h[i : i + 256]) for i in range(0, len(self.h), 256)] - - @cached_property - def sum(self) -> list[float]: - """Sum of all pixels for each band in the image.""" - - v = [] - for i in range(0, len(self.h), 256): - layer_sum = 0.0 - for j in range(256): - layer_sum += j * self.h[i + j] - v.append(layer_sum) - return v - - @cached_property - def sum2(self) -> list[float]: - """Squared sum of all pixels for each band in the image.""" - - v = [] - for i in range(0, len(self.h), 256): - sum2 = 0.0 - for j in range(256): - sum2 += (j**2) * float(self.h[i + j]) - v.append(sum2) - return v - - @cached_property - def mean(self) -> list[float]: - """Average (arithmetic mean) pixel level for each band in the image.""" - return [self.sum[i] / self.count[i] for i in self.bands] - - @cached_property - def median(self) -> list[int]: - """Median pixel level for each band in the image.""" - - v = [] - for i in self.bands: - s = 0 - half = self.count[i] // 2 - b = i * 256 - for j in range(256): - s = s + self.h[b + j] - if s > half: - break - v.append(j) - return v - - @cached_property - def rms(self) -> list[float]: - """RMS (root-mean-square) for each band in the image.""" - return [math.sqrt(self.sum2[i] / self.count[i]) for i in self.bands] - - @cached_property - def var(self) -> list[float]: - """Variance for each band in the image.""" - return [ - (self.sum2[i] - (self.sum[i] ** 2.0) / self.count[i]) / self.count[i] - for i in self.bands - ] - - @cached_property - def stddev(self) -> list[float]: - """Standard deviation for each band in the image.""" - return [math.sqrt(self.var[i]) for i in self.bands] - - -Global = Stat # compatibility +# +# The Python Imaging Library. +# $Id$ +# +# global image statistics +# +# History: +# 1996-04-05 fl Created +# 1997-05-21 fl Added mask; added rms, var, stddev attributes +# 1997-08-05 fl Added median +# 1998-07-05 hk Fixed integer overflow error +# +# Notes: +# This class shows how to implement delayed evaluation of attributes. +# To get a certain value, simply access the corresponding attribute. +# The __getattr__ dispatcher takes care of the rest. +# +# Copyright (c) Secret Labs AB 1997. +# Copyright (c) Fredrik Lundh 1996-97. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import math +from functools import cached_property + +from . import Image + + +class Stat: + def __init__( + self, image_or_list: Image.Image | list[int], mask: Image.Image | None = None + ) -> None: + """ + Calculate statistics for the given image. If a mask is included, + only the regions covered by that mask are included in the + statistics. You can also pass in a previously calculated histogram. + + :param image: A PIL image, or a precalculated histogram. + + .. note:: + + For a PIL image, calculations rely on the + :py:meth:`~PIL.Image.Image.histogram` method. The pixel counts are + grouped into 256 bins, even if the image has more than 8 bits per + channel. So ``I`` and ``F`` mode images have a maximum ``mean``, + ``median`` and ``rms`` of 255, and cannot have an ``extrema`` maximum + of more than 255. + + :param mask: An optional mask. + """ + if isinstance(image_or_list, Image.Image): + self.h = image_or_list.histogram(mask) + elif isinstance(image_or_list, list): + self.h = image_or_list + else: + msg = "first argument must be image or list" # type: ignore[unreachable] + raise TypeError(msg) + self.bands = list(range(len(self.h) // 256)) + + @cached_property + def extrema(self) -> list[tuple[int, int]]: + """ + Min/max values for each band in the image. + + .. note:: + This relies on the :py:meth:`~PIL.Image.Image.histogram` method, and + simply returns the low and high bins used. This is correct for + images with 8 bits per channel, but fails for other modes such as + ``I`` or ``F``. Instead, use :py:meth:`~PIL.Image.Image.getextrema` to + return per-band extrema for the image. This is more correct and + efficient because, for non-8-bit modes, the histogram method uses + :py:meth:`~PIL.Image.Image.getextrema` to determine the bins used. + """ + + def minmax(histogram: list[int]) -> tuple[int, int]: + res_min, res_max = 255, 0 + for i in range(256): + if histogram[i]: + res_min = i + break + for i in range(255, -1, -1): + if histogram[i]: + res_max = i + break + return res_min, res_max + + return [minmax(self.h[i:]) for i in range(0, len(self.h), 256)] + + @cached_property + def count(self) -> list[int]: + """Total number of pixels for each band in the image.""" + return [sum(self.h[i : i + 256]) for i in range(0, len(self.h), 256)] + + @cached_property + def sum(self) -> list[float]: + """Sum of all pixels for each band in the image.""" + + v = [] + for i in range(0, len(self.h), 256): + layer_sum = 0.0 + for j in range(256): + layer_sum += j * self.h[i + j] + v.append(layer_sum) + return v + + @cached_property + def sum2(self) -> list[float]: + """Squared sum of all pixels for each band in the image.""" + + v = [] + for i in range(0, len(self.h), 256): + sum2 = 0.0 + for j in range(256): + sum2 += (j**2) * float(self.h[i + j]) + v.append(sum2) + return v + + @cached_property + def mean(self) -> list[float]: + """Average (arithmetic mean) pixel level for each band in the image.""" + return [self.sum[i] / self.count[i] for i in self.bands] + + @cached_property + def median(self) -> list[int]: + """Median pixel level for each band in the image.""" + + v = [] + for i in self.bands: + s = 0 + half = self.count[i] // 2 + b = i * 256 + for j in range(256): + s = s + self.h[b + j] + if s > half: + break + v.append(j) + return v + + @cached_property + def rms(self) -> list[float]: + """RMS (root-mean-square) for each band in the image.""" + return [math.sqrt(self.sum2[i] / self.count[i]) for i in self.bands] + + @cached_property + def var(self) -> list[float]: + """Variance for each band in the image.""" + return [ + (self.sum2[i] - (self.sum[i] ** 2.0) / self.count[i]) / self.count[i] + for i in self.bands + ] + + @cached_property + def stddev(self) -> list[float]: + """Standard deviation for each band in the image.""" + return [math.sqrt(self.var[i]) for i in self.bands] + + +Global = Stat # compatibility diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py index 90defdbbc..87176ae54 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -1,284 +1,284 @@ -# -# The Python Imaging Library. -# $Id$ -# -# a Tk display interface -# -# History: -# 96-04-08 fl Created -# 96-09-06 fl Added getimage method -# 96-11-01 fl Rewritten, removed image attribute and crop method -# 97-05-09 fl Use PyImagingPaste method instead of image type -# 97-05-12 fl Minor tweaks to match the IFUNC95 interface -# 97-05-17 fl Support the "pilbitmap" booster patch -# 97-06-05 fl Added file= and data= argument to image constructors -# 98-03-09 fl Added width and height methods to Image classes -# 98-07-02 fl Use default mode for "P" images without palette attribute -# 98-07-02 fl Explicitly destroy Tkinter image objects -# 99-07-24 fl Support multiple Tk interpreters (from Greg Couch) -# 99-07-26 fl Automatically hook into Tkinter (if possible) -# 99-08-15 fl Hook uses _imagingtk instead of _imaging -# -# Copyright (c) 1997-1999 by Secret Labs AB -# Copyright (c) 1996-1997 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import tkinter -from io import BytesIO - -from . import Image - -# -------------------------------------------------------------------- -# Check for Tkinter interface hooks - -_pilbitmap_ok = None - - -def _pilbitmap_check() -> int: - global _pilbitmap_ok - if _pilbitmap_ok is None: - try: - im = Image.new("1", (1, 1)) - tkinter.BitmapImage(data=f"PIL:{im.im.id}") - _pilbitmap_ok = 1 - except tkinter.TclError: - _pilbitmap_ok = 0 - return _pilbitmap_ok - - -def _get_image_from_kw(kw): - source = None - if "file" in kw: - source = kw.pop("file") - elif "data" in kw: - source = BytesIO(kw.pop("data")) - if source: - return Image.open(source) - - -def _pyimagingtkcall(command, photo, id): - tk = photo.tk - try: - tk.call(command, photo, id) - except tkinter.TclError: - # activate Tkinter hook - # may raise an error if it cannot attach to Tkinter - from . import _imagingtk - - _imagingtk.tkinit(tk.interpaddr()) - tk.call(command, photo, id) - - -# -------------------------------------------------------------------- -# PhotoImage - - -class PhotoImage: - """ - A Tkinter-compatible photo image. This can be used - everywhere Tkinter expects an image object. If the image is an RGBA - image, pixels having alpha 0 are treated as transparent. - - The constructor takes either a PIL image, or a mode and a size. - Alternatively, you can use the ``file`` or ``data`` options to initialize - the photo image object. - - :param image: Either a PIL image, or a mode string. If a mode string is - used, a size must also be given. - :param size: If the first argument is a mode string, this defines the size - of the image. - :keyword file: A filename to load the image from (using - ``Image.open(file)``). - :keyword data: An 8-bit string containing image data (as loaded from an - image file). - """ - - def __init__(self, image=None, size=None, **kw): - # Tk compatibility: file or data - if image is None: - image = _get_image_from_kw(kw) - - if hasattr(image, "mode") and hasattr(image, "size"): - # got an image instead of a mode - mode = image.mode - if mode == "P": - # palette mapped data - image.apply_transparency() - image.load() - try: - mode = image.palette.mode - except AttributeError: - mode = "RGB" # default - size = image.size - kw["width"], kw["height"] = size - else: - mode = image - image = None - - if mode not in ["1", "L", "RGB", "RGBA"]: - mode = Image.getmodebase(mode) - - self.__mode = mode - self.__size = size - self.__photo = tkinter.PhotoImage(**kw) - self.tk = self.__photo.tk - if image: - self.paste(image) - - def __del__(self) -> None: - name = self.__photo.name - self.__photo.name = None - try: - self.__photo.tk.call("image", "delete", name) - except Exception: - pass # ignore internal errors - - def __str__(self) -> str: - """ - Get the Tkinter photo image identifier. This method is automatically - called by Tkinter whenever a PhotoImage object is passed to a Tkinter - method. - - :return: A Tkinter photo image identifier (a string). - """ - return str(self.__photo) - - def width(self) -> int: - """ - Get the width of the image. - - :return: The width, in pixels. - """ - return self.__size[0] - - def height(self) -> int: - """ - Get the height of the image. - - :return: The height, in pixels. - """ - return self.__size[1] - - def paste(self, im: Image.Image) -> None: - """ - Paste a PIL image into the photo image. Note that this can - be very slow if the photo image is displayed. - - :param im: A PIL image. The size must match the target region. If the - mode does not match, the image is converted to the mode of - the bitmap image. - """ - # convert to blittable - im.load() - image = im.im - if image.isblock() and im.mode == self.__mode: - block = image - else: - block = image.new_block(self.__mode, im.size) - image.convert2(block, image) # convert directly between buffers - - _pyimagingtkcall("PyImagingPhoto", self.__photo, block.id) - - -# -------------------------------------------------------------------- -# BitmapImage - - -class BitmapImage: - """ - A Tkinter-compatible bitmap image. This can be used everywhere Tkinter - expects an image object. - - The given image must have mode "1". Pixels having value 0 are treated as - transparent. Options, if any, are passed on to Tkinter. The most commonly - used option is ``foreground``, which is used to specify the color for the - non-transparent parts. See the Tkinter documentation for information on - how to specify colours. - - :param image: A PIL image. - """ - - def __init__(self, image=None, **kw): - # Tk compatibility: file or data - if image is None: - image = _get_image_from_kw(kw) - - self.__mode = image.mode - self.__size = image.size - - if _pilbitmap_check(): - # fast way (requires the pilbitmap booster patch) - image.load() - kw["data"] = f"PIL:{image.im.id}" - self.__im = image # must keep a reference - else: - # slow but safe way - kw["data"] = image.tobitmap() - self.__photo = tkinter.BitmapImage(**kw) - - def __del__(self) -> None: - name = self.__photo.name - self.__photo.name = None - try: - self.__photo.tk.call("image", "delete", name) - except Exception: - pass # ignore internal errors - - def width(self) -> int: - """ - Get the width of the image. - - :return: The width, in pixels. - """ - return self.__size[0] - - def height(self) -> int: - """ - Get the height of the image. - - :return: The height, in pixels. - """ - return self.__size[1] - - def __str__(self) -> str: - """ - Get the Tkinter bitmap image identifier. This method is automatically - called by Tkinter whenever a BitmapImage object is passed to a Tkinter - method. - - :return: A Tkinter bitmap image identifier (a string). - """ - return str(self.__photo) - - -def getimage(photo: PhotoImage) -> Image.Image: - """Copies the contents of a PhotoImage to a PIL image memory.""" - im = Image.new("RGBA", (photo.width(), photo.height())) - block = im.im - - _pyimagingtkcall("PyImagingPhotoGet", photo, block.id) - - return im - - -def _show(image, title): - """Helper for the Image.show method.""" - - class UI(tkinter.Label): - def __init__(self, master, im): - if im.mode == "1": - self.image = BitmapImage(im, foreground="white", master=master) - else: - self.image = PhotoImage(im, master=master) - super().__init__(master, image=self.image, bg="black", bd=0) - - if not tkinter._default_root: - msg = "tkinter not initialized" - raise OSError(msg) - top = tkinter.Toplevel() - if title: - top.title(title) - UI(top, image).pack() +# +# The Python Imaging Library. +# $Id$ +# +# a Tk display interface +# +# History: +# 96-04-08 fl Created +# 96-09-06 fl Added getimage method +# 96-11-01 fl Rewritten, removed image attribute and crop method +# 97-05-09 fl Use PyImagingPaste method instead of image type +# 97-05-12 fl Minor tweaks to match the IFUNC95 interface +# 97-05-17 fl Support the "pilbitmap" booster patch +# 97-06-05 fl Added file= and data= argument to image constructors +# 98-03-09 fl Added width and height methods to Image classes +# 98-07-02 fl Use default mode for "P" images without palette attribute +# 98-07-02 fl Explicitly destroy Tkinter image objects +# 99-07-24 fl Support multiple Tk interpreters (from Greg Couch) +# 99-07-26 fl Automatically hook into Tkinter (if possible) +# 99-08-15 fl Hook uses _imagingtk instead of _imaging +# +# Copyright (c) 1997-1999 by Secret Labs AB +# Copyright (c) 1996-1997 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import tkinter +from io import BytesIO + +from . import Image + +# -------------------------------------------------------------------- +# Check for Tkinter interface hooks + +_pilbitmap_ok = None + + +def _pilbitmap_check() -> int: + global _pilbitmap_ok + if _pilbitmap_ok is None: + try: + im = Image.new("1", (1, 1)) + tkinter.BitmapImage(data=f"PIL:{im.im.id}") + _pilbitmap_ok = 1 + except tkinter.TclError: + _pilbitmap_ok = 0 + return _pilbitmap_ok + + +def _get_image_from_kw(kw): + source = None + if "file" in kw: + source = kw.pop("file") + elif "data" in kw: + source = BytesIO(kw.pop("data")) + if source: + return Image.open(source) + + +def _pyimagingtkcall(command, photo, id): + tk = photo.tk + try: + tk.call(command, photo, id) + except tkinter.TclError: + # activate Tkinter hook + # may raise an error if it cannot attach to Tkinter + from . import _imagingtk + + _imagingtk.tkinit(tk.interpaddr()) + tk.call(command, photo, id) + + +# -------------------------------------------------------------------- +# PhotoImage + + +class PhotoImage: + """ + A Tkinter-compatible photo image. This can be used + everywhere Tkinter expects an image object. If the image is an RGBA + image, pixels having alpha 0 are treated as transparent. + + The constructor takes either a PIL image, or a mode and a size. + Alternatively, you can use the ``file`` or ``data`` options to initialize + the photo image object. + + :param image: Either a PIL image, or a mode string. If a mode string is + used, a size must also be given. + :param size: If the first argument is a mode string, this defines the size + of the image. + :keyword file: A filename to load the image from (using + ``Image.open(file)``). + :keyword data: An 8-bit string containing image data (as loaded from an + image file). + """ + + def __init__(self, image=None, size=None, **kw): + # Tk compatibility: file or data + if image is None: + image = _get_image_from_kw(kw) + + if hasattr(image, "mode") and hasattr(image, "size"): + # got an image instead of a mode + mode = image.mode + if mode == "P": + # palette mapped data + image.apply_transparency() + image.load() + try: + mode = image.palette.mode + except AttributeError: + mode = "RGB" # default + size = image.size + kw["width"], kw["height"] = size + else: + mode = image + image = None + + if mode not in ["1", "L", "RGB", "RGBA"]: + mode = Image.getmodebase(mode) + + self.__mode = mode + self.__size = size + self.__photo = tkinter.PhotoImage(**kw) + self.tk = self.__photo.tk + if image: + self.paste(image) + + def __del__(self) -> None: + name = self.__photo.name + self.__photo.name = None + try: + self.__photo.tk.call("image", "delete", name) + except Exception: + pass # ignore internal errors + + def __str__(self) -> str: + """ + Get the Tkinter photo image identifier. This method is automatically + called by Tkinter whenever a PhotoImage object is passed to a Tkinter + method. + + :return: A Tkinter photo image identifier (a string). + """ + return str(self.__photo) + + def width(self) -> int: + """ + Get the width of the image. + + :return: The width, in pixels. + """ + return self.__size[0] + + def height(self) -> int: + """ + Get the height of the image. + + :return: The height, in pixels. + """ + return self.__size[1] + + def paste(self, im: Image.Image) -> None: + """ + Paste a PIL image into the photo image. Note that this can + be very slow if the photo image is displayed. + + :param im: A PIL image. The size must match the target region. If the + mode does not match, the image is converted to the mode of + the bitmap image. + """ + # convert to blittable + im.load() + image = im.im + if image.isblock() and im.mode == self.__mode: + block = image + else: + block = image.new_block(self.__mode, im.size) + image.convert2(block, image) # convert directly between buffers + + _pyimagingtkcall("PyImagingPhoto", self.__photo, block.id) + + +# -------------------------------------------------------------------- +# BitmapImage + + +class BitmapImage: + """ + A Tkinter-compatible bitmap image. This can be used everywhere Tkinter + expects an image object. + + The given image must have mode "1". Pixels having value 0 are treated as + transparent. Options, if any, are passed on to Tkinter. The most commonly + used option is ``foreground``, which is used to specify the color for the + non-transparent parts. See the Tkinter documentation for information on + how to specify colours. + + :param image: A PIL image. + """ + + def __init__(self, image=None, **kw): + # Tk compatibility: file or data + if image is None: + image = _get_image_from_kw(kw) + + self.__mode = image.mode + self.__size = image.size + + if _pilbitmap_check(): + # fast way (requires the pilbitmap booster patch) + image.load() + kw["data"] = f"PIL:{image.im.id}" + self.__im = image # must keep a reference + else: + # slow but safe way + kw["data"] = image.tobitmap() + self.__photo = tkinter.BitmapImage(**kw) + + def __del__(self) -> None: + name = self.__photo.name + self.__photo.name = None + try: + self.__photo.tk.call("image", "delete", name) + except Exception: + pass # ignore internal errors + + def width(self) -> int: + """ + Get the width of the image. + + :return: The width, in pixels. + """ + return self.__size[0] + + def height(self) -> int: + """ + Get the height of the image. + + :return: The height, in pixels. + """ + return self.__size[1] + + def __str__(self) -> str: + """ + Get the Tkinter bitmap image identifier. This method is automatically + called by Tkinter whenever a BitmapImage object is passed to a Tkinter + method. + + :return: A Tkinter bitmap image identifier (a string). + """ + return str(self.__photo) + + +def getimage(photo: PhotoImage) -> Image.Image: + """Copies the contents of a PhotoImage to a PIL image memory.""" + im = Image.new("RGBA", (photo.width(), photo.height())) + block = im.im + + _pyimagingtkcall("PyImagingPhotoGet", photo, block.id) + + return im + + +def _show(image, title): + """Helper for the Image.show method.""" + + class UI(tkinter.Label): + def __init__(self, master, im): + if im.mode == "1": + self.image = BitmapImage(im, foreground="white", master=master) + else: + self.image = PhotoImage(im, master=master) + super().__init__(master, image=self.image, bg="black", bd=0) + + if not tkinter._default_root: + msg = "tkinter not initialized" + raise OSError(msg) + top = tkinter.Toplevel() + if title: + top.title(title) + UI(top, image).pack() diff --git a/src/PIL/ImageTransform.py b/src/PIL/ImageTransform.py index 80a6116b7..ee97a71ad 100644 --- a/src/PIL/ImageTransform.py +++ b/src/PIL/ImageTransform.py @@ -1,135 +1,135 @@ -# -# The Python Imaging Library. -# $Id$ -# -# transform wrappers -# -# History: -# 2002-04-08 fl Created -# -# Copyright (c) 2002 by Secret Labs AB -# Copyright (c) 2002 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -from typing import Any, Sequence - -from . import Image - - -class Transform(Image.ImageTransformHandler): - """Base class for other transforms defined in :py:mod:`~PIL.ImageTransform`.""" - - method: Image.Transform - - def __init__(self, data: Sequence[int]) -> None: - self.data = data - - def getdata(self) -> tuple[Image.Transform, Sequence[int]]: - return self.method, self.data - - def transform( - self, - size: tuple[int, int], - image: Image.Image, - **options: Any, - ) -> Image.Image: - """Perform the transform. Called from :py:meth:`.Image.transform`.""" - # can be overridden - method, data = self.getdata() - return image.transform(size, method, data, **options) - - -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. - - This function can be used to scale, translate, rotate, and shear the - original image. - - 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. - """ - - method = Image.Transform.AFFINE - - -class PerspectiveTransform(Transform): - """ - Define a perspective image transform. - - This function takes an 8-tuple (a, b, c, d, e, f, g, h). For each pixel - (x, y) in the output image, the new value is taken from a position - ((a x + b y + c) / (g x + h y + 1), (d x + e y + f) / (g x + h y + 1)) in - the input image, rounded to nearest pixel. - - This function can be used to scale, translate, rotate, and shear the - original image. - - See :py:meth:`.Image.transform` - - :param matrix: An 8-tuple (a, b, c, d, e, f, g, h). - """ - - method = Image.Transform.PERSPECTIVE - - -class ExtentTransform(Transform): - """ - Define a transform to extract a subregion from an image. - - Maps a rectangle (defined by two corners) from the image to a rectangle of - the given size. The resulting image will contain data sampled from between - the corners, such that (x0, y0) in the input image will end up at (0,0) in - the output image, and (x1, y1) at size. - - This method can be used to crop, stretch, shrink, or mirror an arbitrary - rectangle in the current image. It is slightly slower than crop, but about - as fast as a corresponding resize operation. - - See :py:meth:`.Image.transform` - - :param bbox: A 4-tuple (x0, y0, x1, y1) which specifies two points in the - input image's coordinate system. See :ref:`coordinate-system`. - """ - - method = Image.Transform.EXTENT - - -class QuadTransform(Transform): - """ - Define a quad image transform. - - Maps a quadrilateral (a region defined by four corners) from the image to a - rectangle of the given size. - - See :py:meth:`.Image.transform` - - :param xy: An 8-tuple (x0, y0, x1, y1, x2, y2, x3, y3) which contain the - upper left, lower left, lower right, and upper right corner of the - source quadrilateral. - """ - - method = Image.Transform.QUAD - - -class MeshTransform(Transform): - """ - Define a mesh image transform. A mesh transform consists of one or more - individual quad transforms. - - See :py:meth:`.Image.transform` - - :param data: A list of (bbox, quad) tuples. - """ - - method = Image.Transform.MESH +# +# The Python Imaging Library. +# $Id$ +# +# transform wrappers +# +# History: +# 2002-04-08 fl Created +# +# Copyright (c) 2002 by Secret Labs AB +# Copyright (c) 2002 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +from typing import Any, Sequence + +from . import Image + + +class Transform(Image.ImageTransformHandler): + """Base class for other transforms defined in :py:mod:`~PIL.ImageTransform`.""" + + method: Image.Transform + + def __init__(self, data: Sequence[int]) -> None: + self.data = data + + def getdata(self) -> tuple[Image.Transform, Sequence[int]]: + return self.method, self.data + + def transform( + self, + size: tuple[int, int], + image: Image.Image, + **options: Any, + ) -> Image.Image: + """Perform the transform. Called from :py:meth:`.Image.transform`.""" + # can be overridden + method, data = self.getdata() + return image.transform(size, method, data, **options) + + +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. + + This function can be used to scale, translate, rotate, and shear the + original image. + + 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. + """ + + method = Image.Transform.AFFINE + + +class PerspectiveTransform(Transform): + """ + Define a perspective image transform. + + This function takes an 8-tuple (a, b, c, d, e, f, g, h). For each pixel + (x, y) in the output image, the new value is taken from a position + ((a x + b y + c) / (g x + h y + 1), (d x + e y + f) / (g x + h y + 1)) in + the input image, rounded to nearest pixel. + + This function can be used to scale, translate, rotate, and shear the + original image. + + See :py:meth:`.Image.transform` + + :param matrix: An 8-tuple (a, b, c, d, e, f, g, h). + """ + + method = Image.Transform.PERSPECTIVE + + +class ExtentTransform(Transform): + """ + Define a transform to extract a subregion from an image. + + Maps a rectangle (defined by two corners) from the image to a rectangle of + the given size. The resulting image will contain data sampled from between + the corners, such that (x0, y0) in the input image will end up at (0,0) in + the output image, and (x1, y1) at size. + + This method can be used to crop, stretch, shrink, or mirror an arbitrary + rectangle in the current image. It is slightly slower than crop, but about + as fast as a corresponding resize operation. + + See :py:meth:`.Image.transform` + + :param bbox: A 4-tuple (x0, y0, x1, y1) which specifies two points in the + input image's coordinate system. See :ref:`coordinate-system`. + """ + + method = Image.Transform.EXTENT + + +class QuadTransform(Transform): + """ + Define a quad image transform. + + Maps a quadrilateral (a region defined by four corners) from the image to a + rectangle of the given size. + + See :py:meth:`.Image.transform` + + :param xy: An 8-tuple (x0, y0, x1, y1, x2, y2, x3, y3) which contain the + upper left, lower left, lower right, and upper right corner of the + source quadrilateral. + """ + + method = Image.Transform.QUAD + + +class MeshTransform(Transform): + """ + Define a mesh image transform. A mesh transform consists of one or more + individual quad transforms. + + See :py:meth:`.Image.transform` + + :param data: A list of (bbox, quad) tuples. + """ + + method = Image.Transform.MESH diff --git a/src/PIL/ImageWin.py b/src/PIL/ImageWin.py index 6c29e2590..96d6b49ab 100644 --- a/src/PIL/ImageWin.py +++ b/src/PIL/ImageWin.py @@ -1,235 +1,235 @@ -# -# The Python Imaging Library. -# $Id$ -# -# a Windows DIB display interface -# -# History: -# 1996-05-20 fl Created -# 1996-09-20 fl Fixed subregion exposure -# 1997-09-21 fl Added draw primitive (for tzPrint) -# 2003-05-21 fl Added experimental Window/ImageWindow classes -# 2003-09-05 fl Added fromstring/tostring methods -# -# Copyright (c) Secret Labs AB 1997-2003. -# Copyright (c) Fredrik Lundh 1996-2003. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -from . import Image - - -class HDC: - """ - Wraps an HDC integer. The resulting object can be passed to the - :py:meth:`~PIL.ImageWin.Dib.draw` and :py:meth:`~PIL.ImageWin.Dib.expose` - methods. - """ - - def __init__(self, dc: int) -> None: - self.dc = dc - - def __int__(self) -> int: - return self.dc - - -class HWND: - """ - Wraps an HWND integer. The resulting object can be passed to the - :py:meth:`~PIL.ImageWin.Dib.draw` and :py:meth:`~PIL.ImageWin.Dib.expose` - methods, instead of a DC. - """ - - def __init__(self, wnd: int) -> None: - self.wnd = wnd - - def __int__(self) -> int: - return self.wnd - - -class Dib: - """ - A Windows bitmap with the given mode and size. The mode can be one of "1", - "L", "P", or "RGB". - - If the display requires a palette, this constructor creates a suitable - palette and associates it with the image. For an "L" image, 128 graylevels - are allocated. For an "RGB" image, a 6x6x6 colour cube is used, together - with 20 graylevels. - - To make sure that palettes work properly under Windows, you must call the - ``palette`` method upon certain events from Windows. - - :param image: Either a PIL image, or a mode string. If a mode string is - used, a size must also be given. The mode can be one of "1", - "L", "P", or "RGB". - :param size: If the first argument is a mode string, this - defines the size of the image. - """ - - def __init__(self, image, size=None): - if hasattr(image, "mode") and hasattr(image, "size"): - mode = image.mode - size = image.size - else: - mode = image - image = None - if mode not in ["1", "L", "P", "RGB"]: - mode = Image.getmodebase(mode) - self.image = Image.core.display(mode, size) - self.mode = mode - self.size = size - if image: - self.paste(image) - - def expose(self, handle): - """ - Copy the bitmap contents to a device context. - - :param handle: Device context (HDC), cast to a Python integer, or an - HDC or HWND instance. In PythonWin, you can use - ``CDC.GetHandleAttrib()`` to get a suitable handle. - """ - if isinstance(handle, HWND): - dc = self.image.getdc(handle) - try: - result = self.image.expose(dc) - finally: - self.image.releasedc(handle, dc) - else: - result = self.image.expose(handle) - return result - - def draw(self, handle, dst, src=None): - """ - Same as expose, but allows you to specify where to draw the image, and - what part of it to draw. - - The destination and source areas are given as 4-tuple rectangles. If - the source is omitted, the entire image is copied. If the source and - the destination have different sizes, the image is resized as - necessary. - """ - if not src: - src = (0, 0) + self.size - if isinstance(handle, HWND): - dc = self.image.getdc(handle) - try: - result = self.image.draw(dc, dst, src) - finally: - self.image.releasedc(handle, dc) - else: - result = self.image.draw(handle, dst, src) - return result - - def query_palette(self, handle): - """ - Installs the palette associated with the image in the given device - context. - - This method should be called upon **QUERYNEWPALETTE** and - **PALETTECHANGED** events from Windows. If this method returns a - non-zero value, one or more display palette entries were changed, and - the image should be redrawn. - - :param handle: Device context (HDC), cast to a Python integer, or an - HDC or HWND instance. - :return: A true value if one or more entries were changed (this - indicates that the image should be redrawn). - """ - if isinstance(handle, HWND): - handle = self.image.getdc(handle) - try: - result = self.image.query_palette(handle) - finally: - self.image.releasedc(handle, handle) - else: - result = self.image.query_palette(handle) - return result - - def paste( - self, im: Image.Image, box: tuple[int, int, int, int] | None = None - ) -> None: - """ - Paste a PIL image into the bitmap image. - - :param im: A PIL image. The size must match the target region. - If the mode does not match, the image is converted to the - mode of the bitmap image. - :param box: A 4-tuple defining the left, upper, right, and - lower pixel coordinate. See :ref:`coordinate-system`. If - None is given instead of a tuple, all of the image is - assumed. - """ - im.load() - if self.mode != im.mode: - im = im.convert(self.mode) - if box: - self.image.paste(im.im, box) - else: - self.image.paste(im.im) - - def frombytes(self, buffer: bytes) -> None: - """ - Load display memory contents from byte data. - - :param buffer: A buffer containing display data (usually - data returned from :py:func:`~PIL.ImageWin.Dib.tobytes`) - """ - self.image.frombytes(buffer) - - def tobytes(self) -> bytes: - """ - Copy display memory contents to bytes object. - - :return: A bytes object containing display data. - """ - return self.image.tobytes() - - -class Window: - """Create a Window with the given title size.""" - - def __init__( - self, title: str = "PIL", width: int | None = None, height: int | None = None - ) -> None: - self.hwnd = Image.core.createwindow( - title, self.__dispatcher, width or 0, height or 0 - ) - - def __dispatcher(self, action, *args): - return getattr(self, f"ui_handle_{action}")(*args) - - def ui_handle_clear(self, dc, x0, y0, x1, y1): - pass - - def ui_handle_damage(self, x0, y0, x1, y1): - pass - - def ui_handle_destroy(self) -> None: - pass - - def ui_handle_repair(self, dc, x0, y0, x1, y1): - pass - - def ui_handle_resize(self, width, height): - pass - - def mainloop(self) -> None: - Image.core.eventloop() - - -class ImageWindow(Window): - """Create an image window which displays the given image.""" - - def __init__(self, image, title="PIL"): - if not isinstance(image, Dib): - image = Dib(image) - self.image = image - width, height = image.size - super().__init__(title, width=width, height=height) - - def ui_handle_repair(self, dc, x0, y0, x1, y1): - self.image.draw(dc, (x0, y0, x1, y1)) +# +# The Python Imaging Library. +# $Id$ +# +# a Windows DIB display interface +# +# History: +# 1996-05-20 fl Created +# 1996-09-20 fl Fixed subregion exposure +# 1997-09-21 fl Added draw primitive (for tzPrint) +# 2003-05-21 fl Added experimental Window/ImageWindow classes +# 2003-09-05 fl Added fromstring/tostring methods +# +# Copyright (c) Secret Labs AB 1997-2003. +# Copyright (c) Fredrik Lundh 1996-2003. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +from . import Image + + +class HDC: + """ + Wraps an HDC integer. The resulting object can be passed to the + :py:meth:`~PIL.ImageWin.Dib.draw` and :py:meth:`~PIL.ImageWin.Dib.expose` + methods. + """ + + def __init__(self, dc: int) -> None: + self.dc = dc + + def __int__(self) -> int: + return self.dc + + +class HWND: + """ + Wraps an HWND integer. The resulting object can be passed to the + :py:meth:`~PIL.ImageWin.Dib.draw` and :py:meth:`~PIL.ImageWin.Dib.expose` + methods, instead of a DC. + """ + + def __init__(self, wnd: int) -> None: + self.wnd = wnd + + def __int__(self) -> int: + return self.wnd + + +class Dib: + """ + A Windows bitmap with the given mode and size. The mode can be one of "1", + "L", "P", or "RGB". + + If the display requires a palette, this constructor creates a suitable + palette and associates it with the image. For an "L" image, 128 graylevels + are allocated. For an "RGB" image, a 6x6x6 colour cube is used, together + with 20 graylevels. + + To make sure that palettes work properly under Windows, you must call the + ``palette`` method upon certain events from Windows. + + :param image: Either a PIL image, or a mode string. If a mode string is + used, a size must also be given. The mode can be one of "1", + "L", "P", or "RGB". + :param size: If the first argument is a mode string, this + defines the size of the image. + """ + + def __init__(self, image, size=None): + if hasattr(image, "mode") and hasattr(image, "size"): + mode = image.mode + size = image.size + else: + mode = image + image = None + if mode not in ["1", "L", "P", "RGB"]: + mode = Image.getmodebase(mode) + self.image = Image.core.display(mode, size) + self.mode = mode + self.size = size + if image: + self.paste(image) + + def expose(self, handle): + """ + Copy the bitmap contents to a device context. + + :param handle: Device context (HDC), cast to a Python integer, or an + HDC or HWND instance. In PythonWin, you can use + ``CDC.GetHandleAttrib()`` to get a suitable handle. + """ + if isinstance(handle, HWND): + dc = self.image.getdc(handle) + try: + result = self.image.expose(dc) + finally: + self.image.releasedc(handle, dc) + else: + result = self.image.expose(handle) + return result + + def draw(self, handle, dst, src=None): + """ + Same as expose, but allows you to specify where to draw the image, and + what part of it to draw. + + The destination and source areas are given as 4-tuple rectangles. If + the source is omitted, the entire image is copied. If the source and + the destination have different sizes, the image is resized as + necessary. + """ + if not src: + src = (0, 0) + self.size + if isinstance(handle, HWND): + dc = self.image.getdc(handle) + try: + result = self.image.draw(dc, dst, src) + finally: + self.image.releasedc(handle, dc) + else: + result = self.image.draw(handle, dst, src) + return result + + def query_palette(self, handle): + """ + Installs the palette associated with the image in the given device + context. + + This method should be called upon **QUERYNEWPALETTE** and + **PALETTECHANGED** events from Windows. If this method returns a + non-zero value, one or more display palette entries were changed, and + the image should be redrawn. + + :param handle: Device context (HDC), cast to a Python integer, or an + HDC or HWND instance. + :return: A true value if one or more entries were changed (this + indicates that the image should be redrawn). + """ + if isinstance(handle, HWND): + handle = self.image.getdc(handle) + try: + result = self.image.query_palette(handle) + finally: + self.image.releasedc(handle, handle) + else: + result = self.image.query_palette(handle) + return result + + def paste( + self, im: Image.Image, box: tuple[int, int, int, int] | None = None + ) -> None: + """ + Paste a PIL image into the bitmap image. + + :param im: A PIL image. The size must match the target region. + If the mode does not match, the image is converted to the + mode of the bitmap image. + :param box: A 4-tuple defining the left, upper, right, and + lower pixel coordinate. See :ref:`coordinate-system`. If + None is given instead of a tuple, all of the image is + assumed. + """ + im.load() + if self.mode != im.mode: + im = im.convert(self.mode) + if box: + self.image.paste(im.im, box) + else: + self.image.paste(im.im) + + def frombytes(self, buffer: bytes) -> None: + """ + Load display memory contents from byte data. + + :param buffer: A buffer containing display data (usually + data returned from :py:func:`~PIL.ImageWin.Dib.tobytes`) + """ + self.image.frombytes(buffer) + + def tobytes(self) -> bytes: + """ + Copy display memory contents to bytes object. + + :return: A bytes object containing display data. + """ + return self.image.tobytes() + + +class Window: + """Create a Window with the given title size.""" + + def __init__( + self, title: str = "PIL", width: int | None = None, height: int | None = None + ) -> None: + self.hwnd = Image.core.createwindow( + title, self.__dispatcher, width or 0, height or 0 + ) + + def __dispatcher(self, action, *args): + return getattr(self, f"ui_handle_{action}")(*args) + + def ui_handle_clear(self, dc, x0, y0, x1, y1): + pass + + def ui_handle_damage(self, x0, y0, x1, y1): + pass + + def ui_handle_destroy(self) -> None: + pass + + def ui_handle_repair(self, dc, x0, y0, x1, y1): + pass + + def ui_handle_resize(self, width, height): + pass + + def mainloop(self) -> None: + Image.core.eventloop() + + +class ImageWindow(Window): + """Create an image window which displays the given image.""" + + def __init__(self, image, title="PIL"): + if not isinstance(image, Dib): + image = Dib(image) + self.image = image + width, height = image.size + super().__init__(title, width=width, height=height) + + def ui_handle_repair(self, dc, x0, y0, x1, y1): + self.image.draw(dc, (x0, y0, x1, y1)) diff --git a/src/PIL/ImtImagePlugin.py b/src/PIL/ImtImagePlugin.py index abb3fb762..11bc17c56 100644 --- a/src/PIL/ImtImagePlugin.py +++ b/src/PIL/ImtImagePlugin.py @@ -1,103 +1,103 @@ -# -# The Python Imaging Library. -# $Id$ -# -# IM Tools support for PIL -# -# history: -# 1996-05-27 fl Created (read 8-bit images only) -# 2001-02-17 fl Use 're' instead of 'regex' (Python 2.1) (0.2) -# -# Copyright (c) Secret Labs AB 1997-2001. -# Copyright (c) Fredrik Lundh 1996-2001. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import re - -from . import Image, ImageFile - -# -# -------------------------------------------------------------------- - -field = re.compile(rb"([a-z]*) ([^ \r\n]*)") - - -## -# Image plugin for IM Tools images. - - -class ImtImageFile(ImageFile.ImageFile): - format = "IMT" - format_description = "IM Tools" - - def _open(self) -> None: - # Quick rejection: if there's not a LF among the first - # 100 bytes, this is (probably) not a text header. - - assert self.fp is not None - - buffer = self.fp.read(100) - if b"\n" not in buffer: - msg = "not an IM file" - raise SyntaxError(msg) - - xsize = ysize = 0 - - while True: - if buffer: - s = buffer[:1] - buffer = buffer[1:] - else: - s = self.fp.read(1) - if not s: - break - - if s == b"\x0C": - # image data begins - self.tile = [ - ( - "raw", - (0, 0) + self.size, - self.fp.tell() - len(buffer), - (self.mode, 0, 1), - ) - ] - - break - - else: - # read key/value pair - if b"\n" not in buffer: - buffer += self.fp.read(100) - lines = buffer.split(b"\n") - s += lines.pop(0) - buffer = b"\n".join(lines) - if len(s) == 1 or len(s) > 100: - break - if s[0] == ord(b"*"): - continue # comment - - m = field.match(s) - if not m: - break - k, v = m.group(1, 2) - if k == b"width": - xsize = int(v) - self._size = xsize, ysize - elif k == b"height": - ysize = int(v) - self._size = xsize, ysize - elif k == b"pixel" and v == b"n8": - self._mode = "L" - - -# -# -------------------------------------------------------------------- - -Image.register_open(ImtImageFile.format, ImtImageFile) - -# -# no extension registered (".im" is simply too common) +# +# The Python Imaging Library. +# $Id$ +# +# IM Tools support for PIL +# +# history: +# 1996-05-27 fl Created (read 8-bit images only) +# 2001-02-17 fl Use 're' instead of 'regex' (Python 2.1) (0.2) +# +# Copyright (c) Secret Labs AB 1997-2001. +# Copyright (c) Fredrik Lundh 1996-2001. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import re + +from . import Image, ImageFile + +# +# -------------------------------------------------------------------- + +field = re.compile(rb"([a-z]*) ([^ \r\n]*)") + + +## +# Image plugin for IM Tools images. + + +class ImtImageFile(ImageFile.ImageFile): + format = "IMT" + format_description = "IM Tools" + + def _open(self) -> None: + # Quick rejection: if there's not a LF among the first + # 100 bytes, this is (probably) not a text header. + + assert self.fp is not None + + buffer = self.fp.read(100) + if b"\n" not in buffer: + msg = "not an IM file" + raise SyntaxError(msg) + + xsize = ysize = 0 + + while True: + if buffer: + s = buffer[:1] + buffer = buffer[1:] + else: + s = self.fp.read(1) + if not s: + break + + if s == b"\x0C": + # image data begins + self.tile = [ + ( + "raw", + (0, 0) + self.size, + self.fp.tell() - len(buffer), + (self.mode, 0, 1), + ) + ] + + break + + else: + # read key/value pair + if b"\n" not in buffer: + buffer += self.fp.read(100) + lines = buffer.split(b"\n") + s += lines.pop(0) + buffer = b"\n".join(lines) + if len(s) == 1 or len(s) > 100: + break + if s[0] == ord(b"*"): + continue # comment + + m = field.match(s) + if not m: + break + k, v = m.group(1, 2) + if k == b"width": + xsize = int(v) + self._size = xsize, ysize + elif k == b"height": + ysize = int(v) + self._size = xsize, ysize + elif k == b"pixel" and v == b"n8": + self._mode = "L" + + +# +# -------------------------------------------------------------------- + +Image.register_open(ImtImageFile.format, ImtImageFile) + +# +# no extension registered (".im" is simply too common) diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index 73df83bfb..6ad09e050 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -1,235 +1,235 @@ -# -# The Python Imaging Library. -# $Id$ -# -# IPTC/NAA file handling -# -# history: -# 1995-10-01 fl Created -# 1998-03-09 fl Cleaned up and added to PIL -# 2002-06-18 fl Added getiptcinfo helper -# -# Copyright (c) Secret Labs AB 1997-2002. -# Copyright (c) Fredrik Lundh 1995. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -from io import BytesIO -from typing import Sequence - -from . import Image, ImageFile -from ._binary import i16be as i16 -from ._binary import i32be as i32 -from ._deprecate import deprecate - -COMPRESSION = {1: "raw", 5: "jpeg"} - - -def __getattr__(name: str) -> bytes: - if name == "PAD": - deprecate("IptcImagePlugin.PAD", 12) - return b"\0\0\0\0" - msg = f"module '{__name__}' has no attribute '{name}'" - raise AttributeError(msg) - - -# -# Helpers - - -def _i(c: bytes) -> int: - return i32((b"\0\0\0\0" + c)[-4:]) - - -def _i8(c: int | bytes) -> int: - return c if isinstance(c, int) else c[0] - - -def i(c: bytes) -> int: - """.. deprecated:: 10.2.0""" - deprecate("IptcImagePlugin.i", 12) - return _i(c) - - -def dump(c: Sequence[int | bytes]) -> None: - """.. deprecated:: 10.2.0""" - deprecate("IptcImagePlugin.dump", 12) - for i in c: - print(f"{_i8(i):02x}", end=" ") - print() - - -## -# Image plugin for IPTC/NAA datastreams. To read IPTC/NAA fields -# from TIFF and JPEG files, use the getiptcinfo function. - - -class IptcImageFile(ImageFile.ImageFile): - format = "IPTC" - format_description = "IPTC/NAA" - - def getint(self, key: tuple[int, int]) -> int: - return _i(self.info[key]) - - def field(self) -> tuple[tuple[int, int] | None, int]: - # - # get a IPTC field header - s = self.fp.read(5) - if not s.strip(b"\x00"): - return None, 0 - - tag = s[1], s[2] - - # syntax - if s[0] != 0x1C or tag[0] not in [1, 2, 3, 4, 5, 6, 7, 8, 9, 240]: - msg = "invalid IPTC/NAA file" - raise SyntaxError(msg) - - # field size - size = s[3] - if size > 132: - msg = "illegal field length in IPTC/NAA file" - raise OSError(msg) - elif size == 128: - size = 0 - elif size > 128: - size = _i(self.fp.read(size - 128)) - else: - size = i16(s, 3) - - return tag, size - - def _open(self) -> None: - # load descriptive fields - while True: - offset = self.fp.tell() - tag, size = self.field() - if not tag or tag == (8, 10): - break - if size: - tagdata = self.fp.read(size) - else: - tagdata = None - if tag in self.info: - if isinstance(self.info[tag], list): - self.info[tag].append(tagdata) - else: - self.info[tag] = [self.info[tag], tagdata] - else: - self.info[tag] = tagdata - - # mode - layers = self.info[(3, 60)][0] - component = self.info[(3, 60)][1] - if (3, 65) in self.info: - id = self.info[(3, 65)][0] - 1 - else: - id = 0 - if layers == 1 and not component: - self._mode = "L" - elif layers == 3 and component: - self._mode = "RGB"[id] - elif layers == 4 and component: - self._mode = "CMYK"[id] - - # size - self._size = self.getint((3, 20)), self.getint((3, 30)) - - # compression - try: - compression = COMPRESSION[self.getint((3, 120))] - except KeyError as e: - msg = "Unknown IPTC image compression" - raise OSError(msg) from e - - # tile - if tag == (8, 10): - self.tile = [("iptc", (0, 0) + self.size, offset, compression)] - - def load(self): - if len(self.tile) != 1 or self.tile[0][0] != "iptc": - return ImageFile.ImageFile.load(self) - - offset, compression = self.tile[0][2:] - - self.fp.seek(offset) - - # Copy image data to temporary file - o = BytesIO() - if compression == "raw": - # To simplify access to the extracted file, - # prepend a PPM header - o.write(b"P5\n%d %d\n255\n" % self.size) - while True: - type, size = self.field() - if type != (8, 10): - break - while size > 0: - s = self.fp.read(min(size, 8192)) - if not s: - break - o.write(s) - size -= len(s) - - with Image.open(o) as _im: - _im.load() - self.im = _im.im - - -Image.register_open(IptcImageFile.format, IptcImageFile) - -Image.register_extension(IptcImageFile.format, ".iim") - - -def getiptcinfo(im): - """ - Get IPTC information from TIFF, JPEG, or IPTC file. - - :param im: An image containing IPTC data. - :returns: A dictionary containing IPTC information, or None if - no IPTC information block was found. - """ - from . import JpegImagePlugin, TiffImagePlugin - - data = None - - if isinstance(im, IptcImageFile): - # return info dictionary right away - return im.info - - elif isinstance(im, JpegImagePlugin.JpegImageFile): - # extract the IPTC/NAA resource - photoshop = im.info.get("photoshop") - if photoshop: - data = photoshop.get(0x0404) - - elif isinstance(im, TiffImagePlugin.TiffImageFile): - # get raw data from the IPTC/NAA tag (PhotoShop tags the data - # as 4-byte integers, so we cannot use the get method...) - try: - data = im.tag.tagdata[TiffImagePlugin.IPTC_NAA_CHUNK] - except (AttributeError, KeyError): - pass - - if data is None: - return None # no properties - - # create an IptcImagePlugin object without initializing it - class FakeImage: - pass - - im = FakeImage() - im.__class__ = IptcImageFile - - # parse the IPTC information chunk - im.info = {} - im.fp = BytesIO(data) - - try: - im._open() - except (IndexError, KeyError): - pass # expected failure - - return im.info +# +# The Python Imaging Library. +# $Id$ +# +# IPTC/NAA file handling +# +# history: +# 1995-10-01 fl Created +# 1998-03-09 fl Cleaned up and added to PIL +# 2002-06-18 fl Added getiptcinfo helper +# +# Copyright (c) Secret Labs AB 1997-2002. +# Copyright (c) Fredrik Lundh 1995. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +from io import BytesIO +from typing import Sequence + +from . import Image, ImageFile +from ._binary import i16be as i16 +from ._binary import i32be as i32 +from ._deprecate import deprecate + +COMPRESSION = {1: "raw", 5: "jpeg"} + + +def __getattr__(name: str) -> bytes: + if name == "PAD": + deprecate("IptcImagePlugin.PAD", 12) + return b"\0\0\0\0" + msg = f"module '{__name__}' has no attribute '{name}'" + raise AttributeError(msg) + + +# +# Helpers + + +def _i(c: bytes) -> int: + return i32((b"\0\0\0\0" + c)[-4:]) + + +def _i8(c: int | bytes) -> int: + return c if isinstance(c, int) else c[0] + + +def i(c: bytes) -> int: + """.. deprecated:: 10.2.0""" + deprecate("IptcImagePlugin.i", 12) + return _i(c) + + +def dump(c: Sequence[int | bytes]) -> None: + """.. deprecated:: 10.2.0""" + deprecate("IptcImagePlugin.dump", 12) + for i in c: + print(f"{_i8(i):02x}", end=" ") + print() + + +## +# Image plugin for IPTC/NAA datastreams. To read IPTC/NAA fields +# from TIFF and JPEG files, use the getiptcinfo function. + + +class IptcImageFile(ImageFile.ImageFile): + format = "IPTC" + format_description = "IPTC/NAA" + + def getint(self, key: tuple[int, int]) -> int: + return _i(self.info[key]) + + def field(self) -> tuple[tuple[int, int] | None, int]: + # + # get a IPTC field header + s = self.fp.read(5) + if not s.strip(b"\x00"): + return None, 0 + + tag = s[1], s[2] + + # syntax + if s[0] != 0x1C or tag[0] not in [1, 2, 3, 4, 5, 6, 7, 8, 9, 240]: + msg = "invalid IPTC/NAA file" + raise SyntaxError(msg) + + # field size + size = s[3] + if size > 132: + msg = "illegal field length in IPTC/NAA file" + raise OSError(msg) + elif size == 128: + size = 0 + elif size > 128: + size = _i(self.fp.read(size - 128)) + else: + size = i16(s, 3) + + return tag, size + + def _open(self) -> None: + # load descriptive fields + while True: + offset = self.fp.tell() + tag, size = self.field() + if not tag or tag == (8, 10): + break + if size: + tagdata = self.fp.read(size) + else: + tagdata = None + if tag in self.info: + if isinstance(self.info[tag], list): + self.info[tag].append(tagdata) + else: + self.info[tag] = [self.info[tag], tagdata] + else: + self.info[tag] = tagdata + + # mode + layers = self.info[(3, 60)][0] + component = self.info[(3, 60)][1] + if (3, 65) in self.info: + id = self.info[(3, 65)][0] - 1 + else: + id = 0 + if layers == 1 and not component: + self._mode = "L" + elif layers == 3 and component: + self._mode = "RGB"[id] + elif layers == 4 and component: + self._mode = "CMYK"[id] + + # size + self._size = self.getint((3, 20)), self.getint((3, 30)) + + # compression + try: + compression = COMPRESSION[self.getint((3, 120))] + except KeyError as e: + msg = "Unknown IPTC image compression" + raise OSError(msg) from e + + # tile + if tag == (8, 10): + self.tile = [("iptc", (0, 0) + self.size, offset, compression)] + + def load(self): + if len(self.tile) != 1 or self.tile[0][0] != "iptc": + return ImageFile.ImageFile.load(self) + + offset, compression = self.tile[0][2:] + + self.fp.seek(offset) + + # Copy image data to temporary file + o = BytesIO() + if compression == "raw": + # To simplify access to the extracted file, + # prepend a PPM header + o.write(b"P5\n%d %d\n255\n" % self.size) + while True: + type, size = self.field() + if type != (8, 10): + break + while size > 0: + s = self.fp.read(min(size, 8192)) + if not s: + break + o.write(s) + size -= len(s) + + with Image.open(o) as _im: + _im.load() + self.im = _im.im + + +Image.register_open(IptcImageFile.format, IptcImageFile) + +Image.register_extension(IptcImageFile.format, ".iim") + + +def getiptcinfo(im): + """ + Get IPTC information from TIFF, JPEG, or IPTC file. + + :param im: An image containing IPTC data. + :returns: A dictionary containing IPTC information, or None if + no IPTC information block was found. + """ + from . import JpegImagePlugin, TiffImagePlugin + + data = None + + if isinstance(im, IptcImageFile): + # return info dictionary right away + return im.info + + elif isinstance(im, JpegImagePlugin.JpegImageFile): + # extract the IPTC/NAA resource + photoshop = im.info.get("photoshop") + if photoshop: + data = photoshop.get(0x0404) + + elif isinstance(im, TiffImagePlugin.TiffImageFile): + # get raw data from the IPTC/NAA tag (PhotoShop tags the data + # as 4-byte integers, so we cannot use the get method...) + try: + data = im.tag.tagdata[TiffImagePlugin.IPTC_NAA_CHUNK] + except (AttributeError, KeyError): + pass + + if data is None: + return None # no properties + + # create an IptcImagePlugin object without initializing it + class FakeImage: + pass + + im = FakeImage() + im.__class__ = IptcImageFile + + # parse the IPTC information chunk + im.info = {} + im.fp = BytesIO(data) + + try: + im._open() + except (IndexError, KeyError): + pass # expected failure + + return im.info diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index 60f3bff0a..df927dc4b 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -1,411 +1,411 @@ -# -# The Python Imaging Library -# $Id$ -# -# JPEG2000 file handling -# -# History: -# 2014-03-12 ajh Created -# 2021-06-30 rogermb Extract dpi information from the 'resc' header box -# -# Copyright (c) 2014 Coriolis Systems Limited -# Copyright (c) 2014 Alastair Houghton -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import io -import os -import struct -from typing import IO, Tuple, cast - -from . import Image, ImageFile, ImagePalette, _binary - - -class BoxReader: - """ - A small helper class to read fields stored in JPEG2000 header boxes - and to easily step into and read sub-boxes. - """ - - def __init__(self, fp, length=-1): - self.fp = fp - self.has_length = length >= 0 - self.length = length - self.remaining_in_box = -1 - - def _can_read(self, num_bytes: int) -> bool: - if self.has_length and self.fp.tell() + num_bytes > self.length: - # Outside box: ensure we don't read past the known file length - return False - if self.remaining_in_box >= 0: - # Inside box contents: ensure read does not go past box boundaries - return num_bytes <= self.remaining_in_box - else: - return True # No length known, just read - - def _read_bytes(self, num_bytes: int) -> bytes: - if not self._can_read(num_bytes): - msg = "Not enough data in header" - raise SyntaxError(msg) - - data = self.fp.read(num_bytes) - if len(data) < num_bytes: - msg = f"Expected to read {num_bytes} bytes but only got {len(data)}." - raise OSError(msg) - - if self.remaining_in_box > 0: - self.remaining_in_box -= num_bytes - return data - - def read_fields(self, field_format: str) -> tuple[int | bytes, ...]: - size = struct.calcsize(field_format) - data = self._read_bytes(size) - return struct.unpack(field_format, data) - - def read_boxes(self) -> BoxReader: - size = self.remaining_in_box - data = self._read_bytes(size) - return BoxReader(io.BytesIO(data), size) - - def has_next_box(self) -> bool: - if self.has_length: - return self.fp.tell() + self.remaining_in_box < self.length - else: - return True - - def next_box_type(self) -> bytes: - # Skip the rest of the box if it has not been read - if self.remaining_in_box > 0: - self.fp.seek(self.remaining_in_box, os.SEEK_CUR) - self.remaining_in_box = -1 - - # Read the length and type of the next box - lbox, tbox = cast(Tuple[int, bytes], self.read_fields(">I4s")) - if lbox == 1: - lbox = cast(int, self.read_fields(">Q")[0]) - hlen = 16 - else: - hlen = 8 - - if lbox < hlen or not self._can_read(lbox - hlen): - msg = "Invalid header length" - raise SyntaxError(msg) - - self.remaining_in_box = lbox - hlen - return tbox - - -def _parse_codestream(fp): - """Parse the JPEG 2000 codestream to extract the size and component - count from the SIZ marker segment, returning a PIL (size, mode) tuple.""" - - hdr = fp.read(2) - lsiz = _binary.i16be(hdr) - siz = hdr + fp.read(lsiz - 2) - lsiz, rsiz, xsiz, ysiz, xosiz, yosiz, _, _, _, _, csiz = struct.unpack_from( - ">HHIIIIIIIIH", siz - ) - - size = (xsiz - xosiz, ysiz - yosiz) - if csiz == 1: - ssiz = struct.unpack_from(">B", siz, 38) - if (ssiz[0] & 0x7F) + 1 > 8: - mode = "I;16" - else: - mode = "L" - elif csiz == 2: - mode = "LA" - elif csiz == 3: - mode = "RGB" - elif csiz == 4: - mode = "RGBA" - else: - mode = None - - return size, mode - - -def _res_to_dpi(num: int, denom: int, exp: int) -> float | None: - """Convert JPEG2000's (numerator, denominator, exponent-base-10) resolution, - calculated as (num / denom) * 10^exp and stored in dots per meter, - to floating-point dots per inch.""" - if denom == 0: - return None - return (254 * num * (10**exp)) / (10000 * denom) - - -def _parse_jp2_header(fp): - """Parse the JP2 header box to extract size, component count, - color space information, and optionally DPI information, - returning a (size, mode, mimetype, dpi) tuple.""" - - # Find the JP2 header box - reader = BoxReader(fp) - header = None - mimetype = None - while reader.has_next_box(): - tbox = reader.next_box_type() - - if tbox == b"jp2h": - header = reader.read_boxes() - break - elif tbox == b"ftyp": - if reader.read_fields(">4s")[0] == b"jpx ": - mimetype = "image/jpx" - - size = None - mode = None - bpc = None - nc = None - dpi = None # 2-tuple of DPI info, or None - palette = None - - while header.has_next_box(): - tbox = header.next_box_type() - - if tbox == b"ihdr": - height, width, nc, bpc = header.read_fields(">IIHB") - size = (width, height) - if nc == 1 and (bpc & 0x7F) > 8: - mode = "I;16" - elif nc == 1: - mode = "L" - elif nc == 2: - mode = "LA" - elif nc == 3: - mode = "RGB" - elif nc == 4: - mode = "RGBA" - elif tbox == b"colr" and nc == 4: - meth, _, _, enumcs = header.read_fields(">BBBI") - if meth == 1 and enumcs == 12: - mode = "CMYK" - elif tbox == b"pclr" and mode in ("L", "LA"): - ne, npc = header.read_fields(">HB") - bitdepths = header.read_fields(">" + ("B" * npc)) - if max(bitdepths) <= 8: - palette = ImagePalette.ImagePalette() - for i in range(ne): - palette.getcolor(header.read_fields(">" + ("B" * npc))) - mode = "P" if mode == "L" else "PA" - elif tbox == b"res ": - res = header.read_boxes() - while res.has_next_box(): - tres = res.next_box_type() - if tres == b"resc": - vrcn, vrcd, hrcn, hrcd, vrce, hrce = res.read_fields(">HHHHBB") - hres = _res_to_dpi(hrcn, hrcd, hrce) - vres = _res_to_dpi(vrcn, vrcd, vrce) - if hres is not None and vres is not None: - dpi = (hres, vres) - break - - if size is None or mode is None: - msg = "Malformed JP2 header" - raise SyntaxError(msg) - - return size, mode, mimetype, dpi, palette - - -## -# Image plugin for JPEG2000 images. - - -class Jpeg2KImageFile(ImageFile.ImageFile): - format = "JPEG2000" - format_description = "JPEG 2000 (ISO 15444)" - - def _open(self) -> None: - sig = self.fp.read(4) - if sig == b"\xff\x4f\xff\x51": - self.codec = "j2k" - self._size, self._mode = _parse_codestream(self.fp) - else: - sig = sig + self.fp.read(8) - - if sig == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a": - self.codec = "jp2" - header = _parse_jp2_header(self.fp) - self._size, self._mode, self.custom_mimetype, dpi, self.palette = header - if dpi is not None: - self.info["dpi"] = dpi - if self.fp.read(12).endswith(b"jp2c\xff\x4f\xff\x51"): - self._parse_comment() - else: - msg = "not a JPEG 2000 file" - raise SyntaxError(msg) - - if self.size is None or self.mode is None: - msg = "unable to determine size/mode" - raise SyntaxError(msg) - - self._reduce = 0 - self.layers = 0 - - fd = -1 - length = -1 - - try: - fd = self.fp.fileno() - length = os.fstat(fd).st_size - except Exception: - fd = -1 - try: - pos = self.fp.tell() - self.fp.seek(0, io.SEEK_END) - length = self.fp.tell() - self.fp.seek(pos) - except Exception: - length = -1 - - self.tile = [ - ( - "jpeg2k", - (0, 0) + self.size, - 0, - (self.codec, self._reduce, self.layers, fd, length), - ) - ] - - 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: - break - typ = marker[1] - if typ in (0x90, 0xD9): - # Start of tile or end of codestream - break - hdr = self.fp.read(2) - length = _binary.i16be(hdr) - if typ == 0x64: - # Comment - self.info["comment"] = self.fp.read(length - 2)[2:] - break - else: - self.fp.seek(length - 2, os.SEEK_CUR) - - @property - def reduce(self): - # https://github.com/python-pillow/Pillow/issues/4343 found that the - # new Image 'reduce' method was shadowed by this plugin's 'reduce' - # property. This attempts to allow for both scenarios - return self._reduce or super().reduce - - @reduce.setter - def reduce(self, value): - self._reduce = value - - def load(self): - if self.tile and self._reduce: - power = 1 << self._reduce - adjust = power >> 1 - self._size = ( - int((self.size[0] + adjust) / power), - int((self.size[1] + adjust) / power), - ) - - # Update the reduce and layers settings - t = self.tile[0] - t3 = (t[3][0], self._reduce, self.layers, t[3][3], t[3][4]) - self.tile = [(t[0], (0, 0) + self.size, t[2], t3)] - - return ImageFile.ImageFile.load(self) - - -def _accept(prefix: bytes) -> bool: - return ( - prefix[:4] == b"\xff\x4f\xff\x51" - or prefix[:12] == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a" - ) - - -# ------------------------------------------------------------ -# Save support - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - # Get the keyword arguments - info = im.encoderinfo - - if isinstance(filename, str): - filename = filename.encode() - if filename.endswith(b".j2k") or info.get("no_jp2", False): - kind = "j2k" - else: - kind = "jp2" - - offset = info.get("offset", None) - tile_offset = info.get("tile_offset", None) - tile_size = info.get("tile_size", None) - quality_mode = info.get("quality_mode", "rates") - quality_layers = info.get("quality_layers", None) - if quality_layers is not None and not ( - isinstance(quality_layers, (list, tuple)) - and all( - isinstance(quality_layer, (int, float)) for quality_layer in quality_layers - ) - ): - msg = "quality_layers must be a sequence of numbers" - raise ValueError(msg) - - num_resolutions = info.get("num_resolutions", 0) - cblk_size = info.get("codeblock_size", None) - precinct_size = info.get("precinct_size", None) - irreversible = info.get("irreversible", False) - progression = info.get("progression", "LRCP") - cinema_mode = info.get("cinema_mode", "no") - mct = info.get("mct", 0) - signed = info.get("signed", False) - comment = info.get("comment") - if isinstance(comment, str): - comment = comment.encode() - plt = info.get("plt", False) - - fd = -1 - if hasattr(fp, "fileno"): - try: - fd = fp.fileno() - except Exception: - fd = -1 - - im.encoderconfig = ( - offset, - tile_offset, - tile_size, - quality_mode, - quality_layers, - num_resolutions, - cblk_size, - precinct_size, - irreversible, - progression, - cinema_mode, - mct, - signed, - fd, - comment, - plt, - ) - - ImageFile._save(im, fp, [("jpeg2k", (0, 0) + im.size, 0, kind)]) - - -# ------------------------------------------------------------ -# Registry stuff - - -Image.register_open(Jpeg2KImageFile.format, Jpeg2KImageFile, _accept) -Image.register_save(Jpeg2KImageFile.format, _save) - -Image.register_extensions( - Jpeg2KImageFile.format, [".jp2", ".j2k", ".jpc", ".jpf", ".jpx", ".j2c"] -) - -Image.register_mime(Jpeg2KImageFile.format, "image/jp2") +# +# The Python Imaging Library +# $Id$ +# +# JPEG2000 file handling +# +# History: +# 2014-03-12 ajh Created +# 2021-06-30 rogermb Extract dpi information from the 'resc' header box +# +# Copyright (c) 2014 Coriolis Systems Limited +# Copyright (c) 2014 Alastair Houghton +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import io +import os +import struct +from typing import IO, Tuple, cast + +from . import Image, ImageFile, ImagePalette, _binary + + +class BoxReader: + """ + A small helper class to read fields stored in JPEG2000 header boxes + and to easily step into and read sub-boxes. + """ + + def __init__(self, fp, length=-1): + self.fp = fp + self.has_length = length >= 0 + self.length = length + self.remaining_in_box = -1 + + def _can_read(self, num_bytes: int) -> bool: + if self.has_length and self.fp.tell() + num_bytes > self.length: + # Outside box: ensure we don't read past the known file length + return False + if self.remaining_in_box >= 0: + # Inside box contents: ensure read does not go past box boundaries + return num_bytes <= self.remaining_in_box + else: + return True # No length known, just read + + def _read_bytes(self, num_bytes: int) -> bytes: + if not self._can_read(num_bytes): + msg = "Not enough data in header" + raise SyntaxError(msg) + + data = self.fp.read(num_bytes) + if len(data) < num_bytes: + msg = f"Expected to read {num_bytes} bytes but only got {len(data)}." + raise OSError(msg) + + if self.remaining_in_box > 0: + self.remaining_in_box -= num_bytes + return data + + def read_fields(self, field_format: str) -> tuple[int | bytes, ...]: + size = struct.calcsize(field_format) + data = self._read_bytes(size) + return struct.unpack(field_format, data) + + def read_boxes(self) -> BoxReader: + size = self.remaining_in_box + data = self._read_bytes(size) + return BoxReader(io.BytesIO(data), size) + + def has_next_box(self) -> bool: + if self.has_length: + return self.fp.tell() + self.remaining_in_box < self.length + else: + return True + + def next_box_type(self) -> bytes: + # Skip the rest of the box if it has not been read + if self.remaining_in_box > 0: + self.fp.seek(self.remaining_in_box, os.SEEK_CUR) + self.remaining_in_box = -1 + + # Read the length and type of the next box + lbox, tbox = cast(Tuple[int, bytes], self.read_fields(">I4s")) + if lbox == 1: + lbox = cast(int, self.read_fields(">Q")[0]) + hlen = 16 + else: + hlen = 8 + + if lbox < hlen or not self._can_read(lbox - hlen): + msg = "Invalid header length" + raise SyntaxError(msg) + + self.remaining_in_box = lbox - hlen + return tbox + + +def _parse_codestream(fp): + """Parse the JPEG 2000 codestream to extract the size and component + count from the SIZ marker segment, returning a PIL (size, mode) tuple.""" + + hdr = fp.read(2) + lsiz = _binary.i16be(hdr) + siz = hdr + fp.read(lsiz - 2) + lsiz, rsiz, xsiz, ysiz, xosiz, yosiz, _, _, _, _, csiz = struct.unpack_from( + ">HHIIIIIIIIH", siz + ) + + size = (xsiz - xosiz, ysiz - yosiz) + if csiz == 1: + ssiz = struct.unpack_from(">B", siz, 38) + if (ssiz[0] & 0x7F) + 1 > 8: + mode = "I;16" + else: + mode = "L" + elif csiz == 2: + mode = "LA" + elif csiz == 3: + mode = "RGB" + elif csiz == 4: + mode = "RGBA" + else: + mode = None + + return size, mode + + +def _res_to_dpi(num: int, denom: int, exp: int) -> float | None: + """Convert JPEG2000's (numerator, denominator, exponent-base-10) resolution, + calculated as (num / denom) * 10^exp and stored in dots per meter, + to floating-point dots per inch.""" + if denom == 0: + return None + return (254 * num * (10**exp)) / (10000 * denom) + + +def _parse_jp2_header(fp): + """Parse the JP2 header box to extract size, component count, + color space information, and optionally DPI information, + returning a (size, mode, mimetype, dpi) tuple.""" + + # Find the JP2 header box + reader = BoxReader(fp) + header = None + mimetype = None + while reader.has_next_box(): + tbox = reader.next_box_type() + + if tbox == b"jp2h": + header = reader.read_boxes() + break + elif tbox == b"ftyp": + if reader.read_fields(">4s")[0] == b"jpx ": + mimetype = "image/jpx" + + size = None + mode = None + bpc = None + nc = None + dpi = None # 2-tuple of DPI info, or None + palette = None + + while header.has_next_box(): + tbox = header.next_box_type() + + if tbox == b"ihdr": + height, width, nc, bpc = header.read_fields(">IIHB") + size = (width, height) + if nc == 1 and (bpc & 0x7F) > 8: + mode = "I;16" + elif nc == 1: + mode = "L" + elif nc == 2: + mode = "LA" + elif nc == 3: + mode = "RGB" + elif nc == 4: + mode = "RGBA" + elif tbox == b"colr" and nc == 4: + meth, _, _, enumcs = header.read_fields(">BBBI") + if meth == 1 and enumcs == 12: + mode = "CMYK" + elif tbox == b"pclr" and mode in ("L", "LA"): + ne, npc = header.read_fields(">HB") + bitdepths = header.read_fields(">" + ("B" * npc)) + if max(bitdepths) <= 8: + palette = ImagePalette.ImagePalette() + for i in range(ne): + palette.getcolor(header.read_fields(">" + ("B" * npc))) + mode = "P" if mode == "L" else "PA" + elif tbox == b"res ": + res = header.read_boxes() + while res.has_next_box(): + tres = res.next_box_type() + if tres == b"resc": + vrcn, vrcd, hrcn, hrcd, vrce, hrce = res.read_fields(">HHHHBB") + hres = _res_to_dpi(hrcn, hrcd, hrce) + vres = _res_to_dpi(vrcn, vrcd, vrce) + if hres is not None and vres is not None: + dpi = (hres, vres) + break + + if size is None or mode is None: + msg = "Malformed JP2 header" + raise SyntaxError(msg) + + return size, mode, mimetype, dpi, palette + + +## +# Image plugin for JPEG2000 images. + + +class Jpeg2KImageFile(ImageFile.ImageFile): + format = "JPEG2000" + format_description = "JPEG 2000 (ISO 15444)" + + def _open(self) -> None: + sig = self.fp.read(4) + if sig == b"\xff\x4f\xff\x51": + self.codec = "j2k" + self._size, self._mode = _parse_codestream(self.fp) + else: + sig = sig + self.fp.read(8) + + if sig == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a": + self.codec = "jp2" + header = _parse_jp2_header(self.fp) + self._size, self._mode, self.custom_mimetype, dpi, self.palette = header + if dpi is not None: + self.info["dpi"] = dpi + if self.fp.read(12).endswith(b"jp2c\xff\x4f\xff\x51"): + self._parse_comment() + else: + msg = "not a JPEG 2000 file" + raise SyntaxError(msg) + + if self.size is None or self.mode is None: + msg = "unable to determine size/mode" + raise SyntaxError(msg) + + self._reduce = 0 + self.layers = 0 + + fd = -1 + length = -1 + + try: + fd = self.fp.fileno() + length = os.fstat(fd).st_size + except Exception: + fd = -1 + try: + pos = self.fp.tell() + self.fp.seek(0, io.SEEK_END) + length = self.fp.tell() + self.fp.seek(pos) + except Exception: + length = -1 + + self.tile = [ + ( + "jpeg2k", + (0, 0) + self.size, + 0, + (self.codec, self._reduce, self.layers, fd, length), + ) + ] + + 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: + break + typ = marker[1] + if typ in (0x90, 0xD9): + # Start of tile or end of codestream + break + hdr = self.fp.read(2) + length = _binary.i16be(hdr) + if typ == 0x64: + # Comment + self.info["comment"] = self.fp.read(length - 2)[2:] + break + else: + self.fp.seek(length - 2, os.SEEK_CUR) + + @property + def reduce(self): + # https://github.com/python-pillow/Pillow/issues/4343 found that the + # new Image 'reduce' method was shadowed by this plugin's 'reduce' + # property. This attempts to allow for both scenarios + return self._reduce or super().reduce + + @reduce.setter + def reduce(self, value): + self._reduce = value + + def load(self): + if self.tile and self._reduce: + power = 1 << self._reduce + adjust = power >> 1 + self._size = ( + int((self.size[0] + adjust) / power), + int((self.size[1] + adjust) / power), + ) + + # Update the reduce and layers settings + t = self.tile[0] + t3 = (t[3][0], self._reduce, self.layers, t[3][3], t[3][4]) + self.tile = [(t[0], (0, 0) + self.size, t[2], t3)] + + return ImageFile.ImageFile.load(self) + + +def _accept(prefix: bytes) -> bool: + return ( + prefix[:4] == b"\xff\x4f\xff\x51" + or prefix[:12] == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a" + ) + + +# ------------------------------------------------------------ +# Save support + + +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + # Get the keyword arguments + info = im.encoderinfo + + if isinstance(filename, str): + filename = filename.encode() + if filename.endswith(b".j2k") or info.get("no_jp2", False): + kind = "j2k" + else: + kind = "jp2" + + offset = info.get("offset", None) + tile_offset = info.get("tile_offset", None) + tile_size = info.get("tile_size", None) + quality_mode = info.get("quality_mode", "rates") + quality_layers = info.get("quality_layers", None) + if quality_layers is not None and not ( + isinstance(quality_layers, (list, tuple)) + and all( + isinstance(quality_layer, (int, float)) for quality_layer in quality_layers + ) + ): + msg = "quality_layers must be a sequence of numbers" + raise ValueError(msg) + + num_resolutions = info.get("num_resolutions", 0) + cblk_size = info.get("codeblock_size", None) + precinct_size = info.get("precinct_size", None) + irreversible = info.get("irreversible", False) + progression = info.get("progression", "LRCP") + cinema_mode = info.get("cinema_mode", "no") + mct = info.get("mct", 0) + signed = info.get("signed", False) + comment = info.get("comment") + if isinstance(comment, str): + comment = comment.encode() + plt = info.get("plt", False) + + fd = -1 + if hasattr(fp, "fileno"): + try: + fd = fp.fileno() + except Exception: + fd = -1 + + im.encoderconfig = ( + offset, + tile_offset, + tile_size, + quality_mode, + quality_layers, + num_resolutions, + cblk_size, + precinct_size, + irreversible, + progression, + cinema_mode, + mct, + signed, + fd, + comment, + plt, + ) + + ImageFile._save(im, fp, [("jpeg2k", (0, 0) + im.size, 0, kind)]) + + +# ------------------------------------------------------------ +# Registry stuff + + +Image.register_open(Jpeg2KImageFile.format, Jpeg2KImageFile, _accept) +Image.register_save(Jpeg2KImageFile.format, _save) + +Image.register_extensions( + Jpeg2KImageFile.format, [".jp2", ".j2k", ".jpc", ".jpf", ".jpx", ".j2c"] +) + +Image.register_mime(Jpeg2KImageFile.format, "image/jp2") diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 0c8a67888..921022719 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -1,871 +1,871 @@ -# -# The Python Imaging Library. -# $Id$ -# -# JPEG (JFIF) file handling -# -# See "Digital Compression and Coding of Continuous-Tone Still Images, -# Part 1, Requirements and Guidelines" (CCITT T.81 / ISO 10918-1) -# -# History: -# 1995-09-09 fl Created -# 1995-09-13 fl Added full parser -# 1996-03-25 fl Added hack to use the IJG command line utilities -# 1996-05-05 fl Workaround Photoshop 2.5 CMYK polarity bug -# 1996-05-28 fl Added draft support, JFIF version (0.1) -# 1996-12-30 fl Added encoder options, added progression property (0.2) -# 1997-08-27 fl Save mode 1 images as BW (0.3) -# 1998-07-12 fl Added YCbCr to draft and save methods (0.4) -# 1998-10-19 fl Don't hang on files using 16-bit DQT's (0.4.1) -# 2001-04-16 fl Extract DPI settings from JFIF files (0.4.2) -# 2002-07-01 fl Skip pad bytes before markers; identify Exif files (0.4.3) -# 2003-04-25 fl Added experimental EXIF decoder (0.5) -# 2003-06-06 fl Added experimental EXIF GPSinfo decoder -# 2003-09-13 fl Extract COM markers -# 2009-09-06 fl Added icc_profile support (from Florian Hoech) -# 2009-03-06 fl Changed CMYK handling; always use Adobe polarity (0.6) -# 2009-03-08 fl Added subsampling support (from Justin Huff). -# -# Copyright (c) 1997-2003 by Secret Labs AB. -# Copyright (c) 1995-1996 by Fredrik Lundh. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import array -import io -import math -import os -import struct -import subprocess -import sys -import tempfile -import warnings -from typing import IO, Any - -from . import Image, ImageFile -from ._binary import i16be as i16 -from ._binary import i32be as i32 -from ._binary import o8 -from ._binary import o16be as o16 -from .JpegPresets import presets - -# -# Parser - - -def Skip(self: JpegImageFile, marker: int) -> None: - n = i16(self.fp.read(2)) - 2 - ImageFile._safe_read(self.fp, n) - - -def APP(self, marker): - # - # Application marker. Store these in the APP dictionary. - # Also look for well-known application markers. - - n = i16(self.fp.read(2)) - 2 - s = ImageFile._safe_read(self.fp, n) - - app = "APP%d" % (marker & 15) - - self.app[app] = s # compatibility - self.applist.append((app, s)) - - if marker == 0xFFE0 and s[:4] == b"JFIF": - # extract JFIF information - self.info["jfif"] = version = i16(s, 5) # version - self.info["jfif_version"] = divmod(version, 256) - # extract JFIF properties - try: - jfif_unit = s[7] - jfif_density = i16(s, 8), i16(s, 10) - except Exception: - pass - else: - if jfif_unit == 1: - self.info["dpi"] = jfif_density - self.info["jfif_unit"] = jfif_unit - self.info["jfif_density"] = jfif_density - elif marker == 0xFFE1 and s[:6] == 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 == 0xFFE2 and s[:5] == 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": - # 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 - # one or more APP2 markers containing the following data: - # Identifying string ASCII "ICC_PROFILE\0" (12 bytes) - # Marker sequence number 1, 2, etc (1 byte) - # Number of markers Total of APP2's used (1 byte) - # Profile data (remainder of APP2 data) - # Decoders should use the marker sequence numbers to - # reassemble the profile, rather than assuming that the APP2 - # markers appear in the correct sequence. - self.icclist.append(s) - elif marker == 0xFFED and s[:14] == b"Photoshop 3.0\x00": - # parse the image resource block - offset = 14 - photoshop = self.info.setdefault("photoshop", {}) - while s[offset : offset + 4] == b"8BIM": - try: - offset += 4 - # resource code - code = i16(s, offset) - offset += 2 - # resource name (usually empty) - name_len = s[offset] - # name = s[offset+1:offset+1+name_len] - offset += 1 + name_len - offset += offset & 1 # align - # resource data block - size = i32(s, offset) - offset += 4 - data = s[offset : offset + size] - if code == 0x03ED: # ResolutionInfo - data = { - "XResolution": i32(data, 0) / 65536, - "DisplayedUnitsX": i16(data, 4), - "YResolution": i32(data, 8) / 65536, - "DisplayedUnitsY": i16(data, 12), - } - photoshop[code] = data - offset += size - offset += offset & 1 # align - except struct.error: - break # insufficient data - - elif marker == 0xFFEE and s[:5] == b"Adobe": - self.info["adobe"] = i16(s, 5) - # extract Adobe custom properties - try: - adobe_transform = s[11] - except IndexError: - pass - else: - self.info["adobe_transform"] = adobe_transform - elif marker == 0xFFE2 and s[:4] == b"MPF\0": - # extract MPO information - self.info["mp"] = s[4:] - # offset is current location minus buffer size - # plus constant header size - self.info["mpoffset"] = self.fp.tell() - n + 4 - - # If DPI isn't in JPEG header, fetch from EXIF - if "dpi" not in self.info and "exif" in self.info: - try: - exif = self.getexif() - resolution_unit = exif[0x0128] - x_resolution = exif[0x011A] - try: - dpi = float(x_resolution[0]) / x_resolution[1] - except TypeError: - dpi = x_resolution - if math.isnan(dpi): - msg = "DPI is not a number" - raise ValueError(msg) - if resolution_unit == 3: # cm - # 1 dpcm = 2.54 dpi - dpi *= 2.54 - self.info["dpi"] = dpi, dpi - except ( - struct.error, - KeyError, - SyntaxError, - TypeError, - ValueError, - ZeroDivisionError, - ): - # struct.error for truncated EXIF - # KeyError for dpi not included - # SyntaxError for invalid/unreadable EXIF - # ValueError or TypeError for dpi being an invalid float - # ZeroDivisionError for invalid dpi rational value - self.info["dpi"] = 72, 72 - - -def COM(self: JpegImageFile, marker: int) -> None: - # - # Comment marker. Store these in the APP dictionary. - n = i16(self.fp.read(2)) - 2 - s = ImageFile._safe_read(self.fp, n) - - self.info["comment"] = s - self.app["COM"] = s # compatibility - self.applist.append(("COM", s)) - - -def SOF(self: JpegImageFile, marker: int) -> None: - # - # Start of frame marker. Defines the size and mode of the - # image. JPEG is colour blind, so we use some simple - # heuristics to map the number of layers to an appropriate - # mode. Note that this could be made a bit brighter, by - # looking for JFIF and Adobe APP markers. - - n = i16(self.fp.read(2)) - 2 - s = ImageFile._safe_read(self.fp, n) - self._size = i16(s, 3), i16(s, 1) - - self.bits = s[0] - if self.bits != 8: - msg = f"cannot handle {self.bits}-bit layers" - raise SyntaxError(msg) - - self.layers = s[5] - if self.layers == 1: - self._mode = "L" - elif self.layers == 3: - self._mode = "RGB" - elif self.layers == 4: - self._mode = "CMYK" - else: - msg = f"cannot handle {self.layers}-layer images" - raise SyntaxError(msg) - - if marker in [0xFFC2, 0xFFC6, 0xFFCA, 0xFFCE]: - self.info["progressive"] = self.info["progression"] = 1 - - if self.icclist: - # fixup icc profile - self.icclist.sort() # sort by sequence number - if self.icclist[0][13] == len(self.icclist): - profile = [p[14:] for p in self.icclist] - icc_profile = b"".join(profile) - else: - icc_profile = None # wrong number of fragments - self.info["icc_profile"] = icc_profile - self.icclist = [] - - for i in range(6, len(s), 3): - t = s[i : i + 3] - # 4-tuples: id, vsamp, hsamp, qtable - self.layer.append((t[0], t[1] // 16, t[1] & 15, t[2])) - - -def DQT(self: JpegImageFile, marker: int) -> None: - # - # Define quantization table. Note that there might be more - # than one table in each marker. - - # FIXME: The quantization tables can be used to estimate the - # compression quality. - - n = i16(self.fp.read(2)) - 2 - s = ImageFile._safe_read(self.fp, n) - while len(s): - v = s[0] - precision = 1 if (v // 16 == 0) else 2 # in bytes - qt_length = 1 + precision * 64 - if len(s) < qt_length: - msg = "bad quantization table marker" - raise SyntaxError(msg) - data = array.array("B" if precision == 1 else "H", s[1:qt_length]) - if sys.byteorder == "little" and precision > 1: - data.byteswap() # the values are always big-endian - self.quantization[v & 15] = [data[i] for i in zigzag_index] - s = s[qt_length:] - - -# -# JPEG marker table - -MARKER = { - 0xFFC0: ("SOF0", "Baseline DCT", SOF), - 0xFFC1: ("SOF1", "Extended Sequential DCT", SOF), - 0xFFC2: ("SOF2", "Progressive DCT", SOF), - 0xFFC3: ("SOF3", "Spatial lossless", SOF), - 0xFFC4: ("DHT", "Define Huffman table", Skip), - 0xFFC5: ("SOF5", "Differential sequential DCT", SOF), - 0xFFC6: ("SOF6", "Differential progressive DCT", SOF), - 0xFFC7: ("SOF7", "Differential spatial", SOF), - 0xFFC8: ("JPG", "Extension", None), - 0xFFC9: ("SOF9", "Extended sequential DCT (AC)", SOF), - 0xFFCA: ("SOF10", "Progressive DCT (AC)", SOF), - 0xFFCB: ("SOF11", "Spatial lossless DCT (AC)", SOF), - 0xFFCC: ("DAC", "Define arithmetic coding conditioning", Skip), - 0xFFCD: ("SOF13", "Differential sequential DCT (AC)", SOF), - 0xFFCE: ("SOF14", "Differential progressive DCT (AC)", SOF), - 0xFFCF: ("SOF15", "Differential spatial (AC)", SOF), - 0xFFD0: ("RST0", "Restart 0", None), - 0xFFD1: ("RST1", "Restart 1", None), - 0xFFD2: ("RST2", "Restart 2", None), - 0xFFD3: ("RST3", "Restart 3", None), - 0xFFD4: ("RST4", "Restart 4", None), - 0xFFD5: ("RST5", "Restart 5", None), - 0xFFD6: ("RST6", "Restart 6", None), - 0xFFD7: ("RST7", "Restart 7", None), - 0xFFD8: ("SOI", "Start of image", None), - 0xFFD9: ("EOI", "End of image", None), - 0xFFDA: ("SOS", "Start of scan", Skip), - 0xFFDB: ("DQT", "Define quantization table", DQT), - 0xFFDC: ("DNL", "Define number of lines", Skip), - 0xFFDD: ("DRI", "Define restart interval", Skip), - 0xFFDE: ("DHP", "Define hierarchical progression", SOF), - 0xFFDF: ("EXP", "Expand reference component", Skip), - 0xFFE0: ("APP0", "Application segment 0", APP), - 0xFFE1: ("APP1", "Application segment 1", APP), - 0xFFE2: ("APP2", "Application segment 2", APP), - 0xFFE3: ("APP3", "Application segment 3", APP), - 0xFFE4: ("APP4", "Application segment 4", APP), - 0xFFE5: ("APP5", "Application segment 5", APP), - 0xFFE6: ("APP6", "Application segment 6", APP), - 0xFFE7: ("APP7", "Application segment 7", APP), - 0xFFE8: ("APP8", "Application segment 8", APP), - 0xFFE9: ("APP9", "Application segment 9", APP), - 0xFFEA: ("APP10", "Application segment 10", APP), - 0xFFEB: ("APP11", "Application segment 11", APP), - 0xFFEC: ("APP12", "Application segment 12", APP), - 0xFFED: ("APP13", "Application segment 13", APP), - 0xFFEE: ("APP14", "Application segment 14", APP), - 0xFFEF: ("APP15", "Application segment 15", APP), - 0xFFF0: ("JPG0", "Extension 0", None), - 0xFFF1: ("JPG1", "Extension 1", None), - 0xFFF2: ("JPG2", "Extension 2", None), - 0xFFF3: ("JPG3", "Extension 3", None), - 0xFFF4: ("JPG4", "Extension 4", None), - 0xFFF5: ("JPG5", "Extension 5", None), - 0xFFF6: ("JPG6", "Extension 6", None), - 0xFFF7: ("JPG7", "Extension 7", None), - 0xFFF8: ("JPG8", "Extension 8", None), - 0xFFF9: ("JPG9", "Extension 9", None), - 0xFFFA: ("JPG10", "Extension 10", None), - 0xFFFB: ("JPG11", "Extension 11", None), - 0xFFFC: ("JPG12", "Extension 12", None), - 0xFFFD: ("JPG13", "Extension 13", None), - 0xFFFE: ("COM", "Comment", COM), -} - - -def _accept(prefix: bytes) -> bool: - # Magic number was taken from https://en.wikipedia.org/wiki/JPEG - return prefix[:3] == b"\xFF\xD8\xFF" - - -## -# Image plugin for JPEG and JFIF images. - - -class JpegImageFile(ImageFile.ImageFile): - format = "JPEG" - format_description = "JPEG (ISO 10918)" - - def _open(self): - s = self.fp.read(3) - - if not _accept(s): - msg = "not a JPEG file" - raise SyntaxError(msg) - s = b"\xFF" - - # Create attributes - self.bits = self.layers = 0 - - # JPEG specifics (internal) - self.layer = [] - self.huffman_dc = {} - self.huffman_ac = {} - self.quantization = {} - self.app = {} # compatibility - self.applist = [] - self.icclist = [] - - while True: - i = s[0] - if i == 0xFF: - s = s + self.fp.read(1) - i = i16(s) - else: - # Skip non-0xFF junk - s = self.fp.read(1) - continue - - if i in MARKER: - name, description, handler = MARKER[i] - if handler is not None: - handler(self, i) - if i == 0xFFDA: # start of scan - rawmode = self.mode - if self.mode == "CMYK": - rawmode = "CMYK;I" # assume adobe conventions - self.tile = [("jpeg", (0, 0) + self.size, 0, (rawmode, ""))] - # self.__offset = self.fp.tell() - break - s = self.fp.read(1) - elif i in {0, 0xFFFF}: - # padded marker or junk; move on - s = b"\xff" - elif i == 0xFF00: # Skip extraneous data (escaped 0xFF) - s = self.fp.read(1) - else: - msg = "no marker found" - raise SyntaxError(msg) - - def load_read(self, read_bytes: int) -> bytes: - """ - internal: read more image data - For premature EOF and LOAD_TRUNCATED_IMAGES adds EOI marker - so libjpeg can finish decoding - """ - s = self.fp.read(read_bytes) - - if not s and ImageFile.LOAD_TRUNCATED_IMAGES and not hasattr(self, "_ended"): - # Premature EOF. - # Pretend file is finished adding EOI marker - self._ended = True - return b"\xFF\xD9" - - return s - - def draft( - self, mode: str | None, size: tuple[int, int] - ) -> tuple[str, tuple[int, int, float, float]] | None: - if len(self.tile) != 1: - return None - - # Protect from second call - if self.decoderconfig: - return None - - d, e, o, a = self.tile[0] - scale = 1 - original_size = self.size - - if a[0] == "RGB" and mode in ["L", "YCbCr"]: - self._mode = mode - a = mode, "" - - if size: - scale = min(self.size[0] // size[0], self.size[1] // size[1]) - for s in [8, 4, 2, 1]: - if scale >= s: - break - e = ( - e[0], - e[1], - (e[2] - e[0] + s - 1) // s + e[0], - (e[3] - e[1] + s - 1) // s + e[1], - ) - self._size = ((self.size[0] + s - 1) // s, (self.size[1] + s - 1) // s) - scale = s - - self.tile = [(d, e, o, a)] - self.decoderconfig = (scale, 0) - - box = (0, 0, original_size[0] / scale, original_size[1] / scale) - return self.mode, box - - def load_djpeg(self) -> None: - # ALTERNATIVE: handle JPEGs via the IJG command line utilities - - f, path = tempfile.mkstemp() - os.close(f) - if os.path.exists(self.filename): - subprocess.check_call(["djpeg", "-outfile", path, self.filename]) - else: - try: - os.unlink(path) - except OSError: - pass - - msg = "Invalid Filename" - raise ValueError(msg) - - try: - with Image.open(path) as _im: - _im.load() - self.im = _im.im - finally: - try: - os.unlink(path) - except OSError: - pass - - self._mode = self.im.mode - self._size = self.im.size - - self.tile = [] - - def _getexif(self) -> dict[str, Any] | None: - return _getexif(self) - - def _getmp(self): - return _getmp(self) - - def getxmp(self) -> dict[str, Any]: - """ - Returns a dictionary containing the XMP tags. - Requires defusedxml to be installed. - - :returns: XMP tags in a dictionary. - """ - - for segment, content in self.applist: - if segment == "APP1": - marker, xmp_tags = content.split(b"\x00")[:2] - if marker == b"http://ns.adobe.com/xap/1.0/": - return self._getxmp(xmp_tags) - return {} - - -def _getexif(self) -> dict[str, Any] | None: - if "exif" not in self.info: - return None - return self.getexif()._get_merged_dict() - - -def _getmp(self): - # Extract MP information. This method was inspired by the "highly - # experimental" _getexif version that's been in use for years now, - # itself based on the ImageFileDirectory class in the TIFF plugin. - - # The MP record essentially consists of a TIFF file embedded in a JPEG - # application marker. - try: - data = self.info["mp"] - except KeyError: - return None - file_contents = io.BytesIO(data) - head = file_contents.read(8) - endianness = ">" if head[:4] == b"\x4d\x4d\x00\x2a" else "<" - # process dictionary - from . import TiffImagePlugin - - try: - info = TiffImagePlugin.ImageFileDirectory_v2(head) - file_contents.seek(info.next) - info.load(file_contents) - mp = dict(info) - except Exception as e: - msg = "malformed MP Index (unreadable directory)" - raise SyntaxError(msg) from e - # it's an error not to have a number of images - try: - quant = mp[0xB001] - except KeyError as e: - msg = "malformed MP Index (no number of images)" - raise SyntaxError(msg) from e - # get MP entries - mpentries = [] - try: - rawmpentries = mp[0xB002] - for entrynum in range(0, quant): - unpackedentry = struct.unpack_from( - f"{endianness}LLLHH", rawmpentries, entrynum * 16 - ) - labels = ("Attribute", "Size", "DataOffset", "EntryNo1", "EntryNo2") - mpentry = dict(zip(labels, unpackedentry)) - mpentryattr = { - "DependentParentImageFlag": bool(mpentry["Attribute"] & (1 << 31)), - "DependentChildImageFlag": bool(mpentry["Attribute"] & (1 << 30)), - "RepresentativeImageFlag": bool(mpentry["Attribute"] & (1 << 29)), - "Reserved": (mpentry["Attribute"] & (3 << 27)) >> 27, - "ImageDataFormat": (mpentry["Attribute"] & (7 << 24)) >> 24, - "MPType": mpentry["Attribute"] & 0x00FFFFFF, - } - if mpentryattr["ImageDataFormat"] == 0: - mpentryattr["ImageDataFormat"] = "JPEG" - else: - msg = "unsupported picture format in MPO" - raise SyntaxError(msg) - mptypemap = { - 0x000000: "Undefined", - 0x010001: "Large Thumbnail (VGA Equivalent)", - 0x010002: "Large Thumbnail (Full HD Equivalent)", - 0x020001: "Multi-Frame Image (Panorama)", - 0x020002: "Multi-Frame Image: (Disparity)", - 0x020003: "Multi-Frame Image: (Multi-Angle)", - 0x030000: "Baseline MP Primary Image", - } - mpentryattr["MPType"] = mptypemap.get(mpentryattr["MPType"], "Unknown") - mpentry["Attribute"] = mpentryattr - mpentries.append(mpentry) - mp[0xB002] = mpentries - except KeyError as e: - msg = "malformed MP Index (bad MP Entry)" - raise SyntaxError(msg) from e - # Next we should try and parse the individual image unique ID list; - # we don't because I've never seen this actually used in a real MPO - # file and so can't test it. - return mp - - -# -------------------------------------------------------------------- -# stuff to save JPEG files - -RAWMODE = { - "1": "L", - "L": "L", - "RGB": "RGB", - "RGBX": "RGB", - "CMYK": "CMYK;I", # assume adobe conventions - "YCbCr": "YCbCr", -} - -# fmt: off -zigzag_index = ( - 0, 1, 5, 6, 14, 15, 27, 28, - 2, 4, 7, 13, 16, 26, 29, 42, - 3, 8, 12, 17, 25, 30, 41, 43, - 9, 11, 18, 24, 31, 40, 44, 53, - 10, 19, 23, 32, 39, 45, 52, 54, - 20, 22, 33, 38, 46, 51, 55, 60, - 21, 34, 37, 47, 50, 56, 59, 61, - 35, 36, 48, 49, 57, 58, 62, 63, -) - -samplings = { - (1, 1, 1, 1, 1, 1): 0, - (2, 1, 1, 1, 1, 1): 1, - (2, 2, 1, 1, 1, 1): 2, -} -# fmt: on - - -def get_sampling(im): - # There's no subsampling when images have only 1 layer - # (grayscale images) or when they are CMYK (4 layers), - # so set subsampling to the default value. - # - # NOTE: currently Pillow can't encode JPEG to YCCK format. - # If YCCK support is added in the future, subsampling code will have - # to be updated (here and in JpegEncode.c) to deal with 4 layers. - if not hasattr(im, "layers") or im.layers in (1, 4): - return -1 - sampling = im.layer[0][1:3] + im.layer[1][1:3] + im.layer[2][1:3] - return samplings.get(sampling, -1) - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - if im.width == 0 or im.height == 0: - msg = "cannot write empty image as JPEG" - raise ValueError(msg) - - try: - rawmode = RAWMODE[im.mode] - except KeyError as e: - msg = f"cannot write mode {im.mode} as JPEG" - raise OSError(msg) from e - - info = im.encoderinfo - - dpi = [round(x) for x in info.get("dpi", (0, 0))] - - quality = info.get("quality", -1) - subsampling = info.get("subsampling", -1) - qtables = info.get("qtables") - - if quality == "keep": - quality = -1 - subsampling = "keep" - qtables = "keep" - elif quality in presets: - preset = presets[quality] - quality = -1 - subsampling = preset.get("subsampling", -1) - qtables = preset.get("quantization") - elif not isinstance(quality, int): - msg = "Invalid quality setting" - raise ValueError(msg) - else: - if subsampling in presets: - subsampling = presets[subsampling].get("subsampling", -1) - if isinstance(qtables, str) and qtables in presets: - qtables = presets[qtables].get("quantization") - - if subsampling == "4:4:4": - subsampling = 0 - elif subsampling == "4:2:2": - subsampling = 1 - elif subsampling == "4:2:0": - subsampling = 2 - elif subsampling == "4:1:1": - # For compatibility. Before Pillow 4.3, 4:1:1 actually meant 4:2:0. - # Set 4:2:0 if someone is still using that value. - subsampling = 2 - elif subsampling == "keep": - if im.format != "JPEG": - msg = "Cannot use 'keep' when original image is not a JPEG" - raise ValueError(msg) - subsampling = get_sampling(im) - - def validate_qtables(qtables): - if qtables is None: - return qtables - if isinstance(qtables, str): - try: - lines = [ - int(num) - for line in qtables.splitlines() - for num in line.split("#", 1)[0].split() - ] - except ValueError as e: - msg = "Invalid quantization table" - raise ValueError(msg) from e - else: - qtables = [lines[s : s + 64] for s in range(0, len(lines), 64)] - if isinstance(qtables, (tuple, list, dict)): - if isinstance(qtables, dict): - qtables = [ - qtables[key] for key in range(len(qtables)) if key in qtables - ] - elif isinstance(qtables, tuple): - qtables = list(qtables) - if not (0 < len(qtables) < 5): - msg = "None or too many quantization tables" - raise ValueError(msg) - for idx, table in enumerate(qtables): - try: - if len(table) != 64: - msg = "Invalid quantization table" - raise TypeError(msg) - table = array.array("H", table) - except TypeError as e: - msg = "Invalid quantization table" - raise ValueError(msg) from e - else: - qtables[idx] = list(table) - return qtables - - if qtables == "keep": - if im.format != "JPEG": - msg = "Cannot use 'keep' when original image is not a JPEG" - raise ValueError(msg) - qtables = getattr(im, "quantization", None) - qtables = validate_qtables(qtables) - - extra = info.get("extra", b"") - - MAX_BYTES_IN_MARKER = 65533 - icc_profile = info.get("icc_profile") - if icc_profile: - ICC_OVERHEAD_LEN = 14 - MAX_DATA_BYTES_IN_MARKER = MAX_BYTES_IN_MARKER - ICC_OVERHEAD_LEN - markers = [] - while icc_profile: - markers.append(icc_profile[:MAX_DATA_BYTES_IN_MARKER]) - icc_profile = icc_profile[MAX_DATA_BYTES_IN_MARKER:] - i = 1 - for marker in markers: - size = o16(2 + ICC_OVERHEAD_LEN + len(marker)) - extra += ( - b"\xFF\xE2" - + size - + b"ICC_PROFILE\0" - + o8(i) - + o8(len(markers)) - + marker - ) - i += 1 - - comment = info.get("comment", im.info.get("comment")) - - # "progressive" is the official name, but older documentation - # says "progression" - # FIXME: issue a warning if the wrong form is used (post-1.1.7) - progressive = info.get("progressive", False) or info.get("progression", False) - - optimize = info.get("optimize", False) - - exif = info.get("exif", b"") - if isinstance(exif, Image.Exif): - exif = exif.tobytes() - if len(exif) > MAX_BYTES_IN_MARKER: - msg = "EXIF data is too long" - raise ValueError(msg) - - # get keyword arguments - im.encoderconfig = ( - quality, - progressive, - info.get("smooth", 0), - optimize, - info.get("keep_rgb", False), - info.get("streamtype", 0), - dpi[0], - dpi[1], - subsampling, - info.get("restart_marker_blocks", 0), - info.get("restart_marker_rows", 0), - qtables, - comment, - extra, - exif, - ) - - # if we optimize, libjpeg needs a buffer big enough to hold the whole image - # in a shot. Guessing on the size, at im.size bytes. (raw 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": - bufsize = 4 * im.size[0] * im.size[1] - # keep sets quality to -1, but the actual value may be high. - elif quality >= 95 or quality == -1: - bufsize = 2 * im.size[0] * im.size[1] - else: - bufsize = im.size[0] * im.size[1] - if exif: - bufsize += len(exif) + 5 - if extra: - bufsize += len(extra) + 1 - 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) - - ImageFile._save(im, fp, [("jpeg", (0, 0) + im.size, 0, rawmode)], bufsize) - - -def _save_cjpeg(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - # ALTERNATIVE: handle JPEGs via the IJG command line utilities. - tempfile = im._dump() - subprocess.check_call(["cjpeg", "-outfile", filename, tempfile]) - try: - os.unlink(tempfile) - except OSError: - pass - - -## -# Factory for making JPEG and MPO instances -def jpeg_factory(fp=None, filename=None): - im = JpegImageFile(fp, filename) - try: - mpheader = im._getmp() - if mpheader[45057] > 1: - # It's actually an MPO - from .MpoImagePlugin import MpoImageFile - - # Don't reload everything, just convert it. - im = MpoImageFile.adopt(im, mpheader) - except (TypeError, IndexError): - # It is really a JPEG - pass - except SyntaxError: - warnings.warn( - "Image appears to be a malformed MPO file, it will be " - "interpreted as a base JPEG file" - ) - return im - - -# --------------------------------------------------------------------- -# Registry stuff - -Image.register_open(JpegImageFile.format, jpeg_factory, _accept) -Image.register_save(JpegImageFile.format, _save) - -Image.register_extensions(JpegImageFile.format, [".jfif", ".jpe", ".jpg", ".jpeg"]) - -Image.register_mime(JpegImageFile.format, "image/jpeg") +# +# The Python Imaging Library. +# $Id$ +# +# JPEG (JFIF) file handling +# +# See "Digital Compression and Coding of Continuous-Tone Still Images, +# Part 1, Requirements and Guidelines" (CCITT T.81 / ISO 10918-1) +# +# History: +# 1995-09-09 fl Created +# 1995-09-13 fl Added full parser +# 1996-03-25 fl Added hack to use the IJG command line utilities +# 1996-05-05 fl Workaround Photoshop 2.5 CMYK polarity bug +# 1996-05-28 fl Added draft support, JFIF version (0.1) +# 1996-12-30 fl Added encoder options, added progression property (0.2) +# 1997-08-27 fl Save mode 1 images as BW (0.3) +# 1998-07-12 fl Added YCbCr to draft and save methods (0.4) +# 1998-10-19 fl Don't hang on files using 16-bit DQT's (0.4.1) +# 2001-04-16 fl Extract DPI settings from JFIF files (0.4.2) +# 2002-07-01 fl Skip pad bytes before markers; identify Exif files (0.4.3) +# 2003-04-25 fl Added experimental EXIF decoder (0.5) +# 2003-06-06 fl Added experimental EXIF GPSinfo decoder +# 2003-09-13 fl Extract COM markers +# 2009-09-06 fl Added icc_profile support (from Florian Hoech) +# 2009-03-06 fl Changed CMYK handling; always use Adobe polarity (0.6) +# 2009-03-08 fl Added subsampling support (from Justin Huff). +# +# Copyright (c) 1997-2003 by Secret Labs AB. +# Copyright (c) 1995-1996 by Fredrik Lundh. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import array +import io +import math +import os +import struct +import subprocess +import sys +import tempfile +import warnings +from typing import IO, Any + +from . import Image, ImageFile +from ._binary import i16be as i16 +from ._binary import i32be as i32 +from ._binary import o8 +from ._binary import o16be as o16 +from .JpegPresets import presets + +# +# Parser + + +def Skip(self: JpegImageFile, marker: int) -> None: + n = i16(self.fp.read(2)) - 2 + ImageFile._safe_read(self.fp, n) + + +def APP(self, marker): + # + # Application marker. Store these in the APP dictionary. + # Also look for well-known application markers. + + n = i16(self.fp.read(2)) - 2 + s = ImageFile._safe_read(self.fp, n) + + app = "APP%d" % (marker & 15) + + self.app[app] = s # compatibility + self.applist.append((app, s)) + + if marker == 0xFFE0 and s[:4] == b"JFIF": + # extract JFIF information + self.info["jfif"] = version = i16(s, 5) # version + self.info["jfif_version"] = divmod(version, 256) + # extract JFIF properties + try: + jfif_unit = s[7] + jfif_density = i16(s, 8), i16(s, 10) + except Exception: + pass + else: + if jfif_unit == 1: + self.info["dpi"] = jfif_density + self.info["jfif_unit"] = jfif_unit + self.info["jfif_density"] = jfif_density + elif marker == 0xFFE1 and s[:6] == 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 == 0xFFE2 and s[:5] == 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": + # 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 + # one or more APP2 markers containing the following data: + # Identifying string ASCII "ICC_PROFILE\0" (12 bytes) + # Marker sequence number 1, 2, etc (1 byte) + # Number of markers Total of APP2's used (1 byte) + # Profile data (remainder of APP2 data) + # Decoders should use the marker sequence numbers to + # reassemble the profile, rather than assuming that the APP2 + # markers appear in the correct sequence. + self.icclist.append(s) + elif marker == 0xFFED and s[:14] == b"Photoshop 3.0\x00": + # parse the image resource block + offset = 14 + photoshop = self.info.setdefault("photoshop", {}) + while s[offset : offset + 4] == b"8BIM": + try: + offset += 4 + # resource code + code = i16(s, offset) + offset += 2 + # resource name (usually empty) + name_len = s[offset] + # name = s[offset+1:offset+1+name_len] + offset += 1 + name_len + offset += offset & 1 # align + # resource data block + size = i32(s, offset) + offset += 4 + data = s[offset : offset + size] + if code == 0x03ED: # ResolutionInfo + data = { + "XResolution": i32(data, 0) / 65536, + "DisplayedUnitsX": i16(data, 4), + "YResolution": i32(data, 8) / 65536, + "DisplayedUnitsY": i16(data, 12), + } + photoshop[code] = data + offset += size + offset += offset & 1 # align + except struct.error: + break # insufficient data + + elif marker == 0xFFEE and s[:5] == b"Adobe": + self.info["adobe"] = i16(s, 5) + # extract Adobe custom properties + try: + adobe_transform = s[11] + except IndexError: + pass + else: + self.info["adobe_transform"] = adobe_transform + elif marker == 0xFFE2 and s[:4] == b"MPF\0": + # extract MPO information + self.info["mp"] = s[4:] + # offset is current location minus buffer size + # plus constant header size + self.info["mpoffset"] = self.fp.tell() - n + 4 + + # If DPI isn't in JPEG header, fetch from EXIF + if "dpi" not in self.info and "exif" in self.info: + try: + exif = self.getexif() + resolution_unit = exif[0x0128] + x_resolution = exif[0x011A] + try: + dpi = float(x_resolution[0]) / x_resolution[1] + except TypeError: + dpi = x_resolution + if math.isnan(dpi): + msg = "DPI is not a number" + raise ValueError(msg) + if resolution_unit == 3: # cm + # 1 dpcm = 2.54 dpi + dpi *= 2.54 + self.info["dpi"] = dpi, dpi + except ( + struct.error, + KeyError, + SyntaxError, + TypeError, + ValueError, + ZeroDivisionError, + ): + # struct.error for truncated EXIF + # KeyError for dpi not included + # SyntaxError for invalid/unreadable EXIF + # ValueError or TypeError for dpi being an invalid float + # ZeroDivisionError for invalid dpi rational value + self.info["dpi"] = 72, 72 + + +def COM(self: JpegImageFile, marker: int) -> None: + # + # Comment marker. Store these in the APP dictionary. + n = i16(self.fp.read(2)) - 2 + s = ImageFile._safe_read(self.fp, n) + + self.info["comment"] = s + self.app["COM"] = s # compatibility + self.applist.append(("COM", s)) + + +def SOF(self: JpegImageFile, marker: int) -> None: + # + # Start of frame marker. Defines the size and mode of the + # image. JPEG is colour blind, so we use some simple + # heuristics to map the number of layers to an appropriate + # mode. Note that this could be made a bit brighter, by + # looking for JFIF and Adobe APP markers. + + n = i16(self.fp.read(2)) - 2 + s = ImageFile._safe_read(self.fp, n) + self._size = i16(s, 3), i16(s, 1) + + self.bits = s[0] + if self.bits != 8: + msg = f"cannot handle {self.bits}-bit layers" + raise SyntaxError(msg) + + self.layers = s[5] + if self.layers == 1: + self._mode = "L" + elif self.layers == 3: + self._mode = "RGB" + elif self.layers == 4: + self._mode = "CMYK" + else: + msg = f"cannot handle {self.layers}-layer images" + raise SyntaxError(msg) + + if marker in [0xFFC2, 0xFFC6, 0xFFCA, 0xFFCE]: + self.info["progressive"] = self.info["progression"] = 1 + + if self.icclist: + # fixup icc profile + self.icclist.sort() # sort by sequence number + if self.icclist[0][13] == len(self.icclist): + profile = [p[14:] for p in self.icclist] + icc_profile = b"".join(profile) + else: + icc_profile = None # wrong number of fragments + self.info["icc_profile"] = icc_profile + self.icclist = [] + + for i in range(6, len(s), 3): + t = s[i : i + 3] + # 4-tuples: id, vsamp, hsamp, qtable + self.layer.append((t[0], t[1] // 16, t[1] & 15, t[2])) + + +def DQT(self: JpegImageFile, marker: int) -> None: + # + # Define quantization table. Note that there might be more + # than one table in each marker. + + # FIXME: The quantization tables can be used to estimate the + # compression quality. + + n = i16(self.fp.read(2)) - 2 + s = ImageFile._safe_read(self.fp, n) + while len(s): + v = s[0] + precision = 1 if (v // 16 == 0) else 2 # in bytes + qt_length = 1 + precision * 64 + if len(s) < qt_length: + msg = "bad quantization table marker" + raise SyntaxError(msg) + data = array.array("B" if precision == 1 else "H", s[1:qt_length]) + if sys.byteorder == "little" and precision > 1: + data.byteswap() # the values are always big-endian + self.quantization[v & 15] = [data[i] for i in zigzag_index] + s = s[qt_length:] + + +# +# JPEG marker table + +MARKER = { + 0xFFC0: ("SOF0", "Baseline DCT", SOF), + 0xFFC1: ("SOF1", "Extended Sequential DCT", SOF), + 0xFFC2: ("SOF2", "Progressive DCT", SOF), + 0xFFC3: ("SOF3", "Spatial lossless", SOF), + 0xFFC4: ("DHT", "Define Huffman table", Skip), + 0xFFC5: ("SOF5", "Differential sequential DCT", SOF), + 0xFFC6: ("SOF6", "Differential progressive DCT", SOF), + 0xFFC7: ("SOF7", "Differential spatial", SOF), + 0xFFC8: ("JPG", "Extension", None), + 0xFFC9: ("SOF9", "Extended sequential DCT (AC)", SOF), + 0xFFCA: ("SOF10", "Progressive DCT (AC)", SOF), + 0xFFCB: ("SOF11", "Spatial lossless DCT (AC)", SOF), + 0xFFCC: ("DAC", "Define arithmetic coding conditioning", Skip), + 0xFFCD: ("SOF13", "Differential sequential DCT (AC)", SOF), + 0xFFCE: ("SOF14", "Differential progressive DCT (AC)", SOF), + 0xFFCF: ("SOF15", "Differential spatial (AC)", SOF), + 0xFFD0: ("RST0", "Restart 0", None), + 0xFFD1: ("RST1", "Restart 1", None), + 0xFFD2: ("RST2", "Restart 2", None), + 0xFFD3: ("RST3", "Restart 3", None), + 0xFFD4: ("RST4", "Restart 4", None), + 0xFFD5: ("RST5", "Restart 5", None), + 0xFFD6: ("RST6", "Restart 6", None), + 0xFFD7: ("RST7", "Restart 7", None), + 0xFFD8: ("SOI", "Start of image", None), + 0xFFD9: ("EOI", "End of image", None), + 0xFFDA: ("SOS", "Start of scan", Skip), + 0xFFDB: ("DQT", "Define quantization table", DQT), + 0xFFDC: ("DNL", "Define number of lines", Skip), + 0xFFDD: ("DRI", "Define restart interval", Skip), + 0xFFDE: ("DHP", "Define hierarchical progression", SOF), + 0xFFDF: ("EXP", "Expand reference component", Skip), + 0xFFE0: ("APP0", "Application segment 0", APP), + 0xFFE1: ("APP1", "Application segment 1", APP), + 0xFFE2: ("APP2", "Application segment 2", APP), + 0xFFE3: ("APP3", "Application segment 3", APP), + 0xFFE4: ("APP4", "Application segment 4", APP), + 0xFFE5: ("APP5", "Application segment 5", APP), + 0xFFE6: ("APP6", "Application segment 6", APP), + 0xFFE7: ("APP7", "Application segment 7", APP), + 0xFFE8: ("APP8", "Application segment 8", APP), + 0xFFE9: ("APP9", "Application segment 9", APP), + 0xFFEA: ("APP10", "Application segment 10", APP), + 0xFFEB: ("APP11", "Application segment 11", APP), + 0xFFEC: ("APP12", "Application segment 12", APP), + 0xFFED: ("APP13", "Application segment 13", APP), + 0xFFEE: ("APP14", "Application segment 14", APP), + 0xFFEF: ("APP15", "Application segment 15", APP), + 0xFFF0: ("JPG0", "Extension 0", None), + 0xFFF1: ("JPG1", "Extension 1", None), + 0xFFF2: ("JPG2", "Extension 2", None), + 0xFFF3: ("JPG3", "Extension 3", None), + 0xFFF4: ("JPG4", "Extension 4", None), + 0xFFF5: ("JPG5", "Extension 5", None), + 0xFFF6: ("JPG6", "Extension 6", None), + 0xFFF7: ("JPG7", "Extension 7", None), + 0xFFF8: ("JPG8", "Extension 8", None), + 0xFFF9: ("JPG9", "Extension 9", None), + 0xFFFA: ("JPG10", "Extension 10", None), + 0xFFFB: ("JPG11", "Extension 11", None), + 0xFFFC: ("JPG12", "Extension 12", None), + 0xFFFD: ("JPG13", "Extension 13", None), + 0xFFFE: ("COM", "Comment", COM), +} + + +def _accept(prefix: bytes) -> bool: + # Magic number was taken from https://en.wikipedia.org/wiki/JPEG + return prefix[:3] == b"\xFF\xD8\xFF" + + +## +# Image plugin for JPEG and JFIF images. + + +class JpegImageFile(ImageFile.ImageFile): + format = "JPEG" + format_description = "JPEG (ISO 10918)" + + def _open(self): + s = self.fp.read(3) + + if not _accept(s): + msg = "not a JPEG file" + raise SyntaxError(msg) + s = b"\xFF" + + # Create attributes + self.bits = self.layers = 0 + + # JPEG specifics (internal) + self.layer = [] + self.huffman_dc = {} + self.huffman_ac = {} + self.quantization = {} + self.app = {} # compatibility + self.applist = [] + self.icclist = [] + + while True: + i = s[0] + if i == 0xFF: + s = s + self.fp.read(1) + i = i16(s) + else: + # Skip non-0xFF junk + s = self.fp.read(1) + continue + + if i in MARKER: + name, description, handler = MARKER[i] + if handler is not None: + handler(self, i) + if i == 0xFFDA: # start of scan + rawmode = self.mode + if self.mode == "CMYK": + rawmode = "CMYK;I" # assume adobe conventions + self.tile = [("jpeg", (0, 0) + self.size, 0, (rawmode, ""))] + # self.__offset = self.fp.tell() + break + s = self.fp.read(1) + elif i in {0, 0xFFFF}: + # padded marker or junk; move on + s = b"\xff" + elif i == 0xFF00: # Skip extraneous data (escaped 0xFF) + s = self.fp.read(1) + else: + msg = "no marker found" + raise SyntaxError(msg) + + def load_read(self, read_bytes: int) -> bytes: + """ + internal: read more image data + For premature EOF and LOAD_TRUNCATED_IMAGES adds EOI marker + so libjpeg can finish decoding + """ + s = self.fp.read(read_bytes) + + if not s and ImageFile.LOAD_TRUNCATED_IMAGES and not hasattr(self, "_ended"): + # Premature EOF. + # Pretend file is finished adding EOI marker + self._ended = True + return b"\xFF\xD9" + + return s + + def draft( + self, mode: str | None, size: tuple[int, int] + ) -> tuple[str, tuple[int, int, float, float]] | None: + if len(self.tile) != 1: + return None + + # Protect from second call + if self.decoderconfig: + return None + + d, e, o, a = self.tile[0] + scale = 1 + original_size = self.size + + if a[0] == "RGB" and mode in ["L", "YCbCr"]: + self._mode = mode + a = mode, "" + + if size: + scale = min(self.size[0] // size[0], self.size[1] // size[1]) + for s in [8, 4, 2, 1]: + if scale >= s: + break + e = ( + e[0], + e[1], + (e[2] - e[0] + s - 1) // s + e[0], + (e[3] - e[1] + s - 1) // s + e[1], + ) + self._size = ((self.size[0] + s - 1) // s, (self.size[1] + s - 1) // s) + scale = s + + self.tile = [(d, e, o, a)] + self.decoderconfig = (scale, 0) + + box = (0, 0, original_size[0] / scale, original_size[1] / scale) + return self.mode, box + + def load_djpeg(self) -> None: + # ALTERNATIVE: handle JPEGs via the IJG command line utilities + + f, path = tempfile.mkstemp() + os.close(f) + if os.path.exists(self.filename): + subprocess.check_call(["djpeg", "-outfile", path, self.filename]) + else: + try: + os.unlink(path) + except OSError: + pass + + msg = "Invalid Filename" + raise ValueError(msg) + + try: + with Image.open(path) as _im: + _im.load() + self.im = _im.im + finally: + try: + os.unlink(path) + except OSError: + pass + + self._mode = self.im.mode + self._size = self.im.size + + self.tile = [] + + def _getexif(self) -> dict[str, Any] | None: + return _getexif(self) + + def _getmp(self): + return _getmp(self) + + def getxmp(self) -> dict[str, Any]: + """ + Returns a dictionary containing the XMP tags. + Requires defusedxml to be installed. + + :returns: XMP tags in a dictionary. + """ + + for segment, content in self.applist: + if segment == "APP1": + marker, xmp_tags = content.split(b"\x00")[:2] + if marker == b"http://ns.adobe.com/xap/1.0/": + return self._getxmp(xmp_tags) + return {} + + +def _getexif(self) -> dict[str, Any] | None: + if "exif" not in self.info: + return None + return self.getexif()._get_merged_dict() + + +def _getmp(self): + # Extract MP information. This method was inspired by the "highly + # experimental" _getexif version that's been in use for years now, + # itself based on the ImageFileDirectory class in the TIFF plugin. + + # The MP record essentially consists of a TIFF file embedded in a JPEG + # application marker. + try: + data = self.info["mp"] + except KeyError: + return None + file_contents = io.BytesIO(data) + head = file_contents.read(8) + endianness = ">" if head[:4] == b"\x4d\x4d\x00\x2a" else "<" + # process dictionary + from . import TiffImagePlugin + + try: + info = TiffImagePlugin.ImageFileDirectory_v2(head) + file_contents.seek(info.next) + info.load(file_contents) + mp = dict(info) + except Exception as e: + msg = "malformed MP Index (unreadable directory)" + raise SyntaxError(msg) from e + # it's an error not to have a number of images + try: + quant = mp[0xB001] + except KeyError as e: + msg = "malformed MP Index (no number of images)" + raise SyntaxError(msg) from e + # get MP entries + mpentries = [] + try: + rawmpentries = mp[0xB002] + for entrynum in range(0, quant): + unpackedentry = struct.unpack_from( + f"{endianness}LLLHH", rawmpentries, entrynum * 16 + ) + labels = ("Attribute", "Size", "DataOffset", "EntryNo1", "EntryNo2") + mpentry = dict(zip(labels, unpackedentry)) + mpentryattr = { + "DependentParentImageFlag": bool(mpentry["Attribute"] & (1 << 31)), + "DependentChildImageFlag": bool(mpentry["Attribute"] & (1 << 30)), + "RepresentativeImageFlag": bool(mpentry["Attribute"] & (1 << 29)), + "Reserved": (mpentry["Attribute"] & (3 << 27)) >> 27, + "ImageDataFormat": (mpentry["Attribute"] & (7 << 24)) >> 24, + "MPType": mpentry["Attribute"] & 0x00FFFFFF, + } + if mpentryattr["ImageDataFormat"] == 0: + mpentryattr["ImageDataFormat"] = "JPEG" + else: + msg = "unsupported picture format in MPO" + raise SyntaxError(msg) + mptypemap = { + 0x000000: "Undefined", + 0x010001: "Large Thumbnail (VGA Equivalent)", + 0x010002: "Large Thumbnail (Full HD Equivalent)", + 0x020001: "Multi-Frame Image (Panorama)", + 0x020002: "Multi-Frame Image: (Disparity)", + 0x020003: "Multi-Frame Image: (Multi-Angle)", + 0x030000: "Baseline MP Primary Image", + } + mpentryattr["MPType"] = mptypemap.get(mpentryattr["MPType"], "Unknown") + mpentry["Attribute"] = mpentryattr + mpentries.append(mpentry) + mp[0xB002] = mpentries + except KeyError as e: + msg = "malformed MP Index (bad MP Entry)" + raise SyntaxError(msg) from e + # Next we should try and parse the individual image unique ID list; + # we don't because I've never seen this actually used in a real MPO + # file and so can't test it. + return mp + + +# -------------------------------------------------------------------- +# stuff to save JPEG files + +RAWMODE = { + "1": "L", + "L": "L", + "RGB": "RGB", + "RGBX": "RGB", + "CMYK": "CMYK;I", # assume adobe conventions + "YCbCr": "YCbCr", +} + +# fmt: off +zigzag_index = ( + 0, 1, 5, 6, 14, 15, 27, 28, + 2, 4, 7, 13, 16, 26, 29, 42, + 3, 8, 12, 17, 25, 30, 41, 43, + 9, 11, 18, 24, 31, 40, 44, 53, + 10, 19, 23, 32, 39, 45, 52, 54, + 20, 22, 33, 38, 46, 51, 55, 60, + 21, 34, 37, 47, 50, 56, 59, 61, + 35, 36, 48, 49, 57, 58, 62, 63, +) + +samplings = { + (1, 1, 1, 1, 1, 1): 0, + (2, 1, 1, 1, 1, 1): 1, + (2, 2, 1, 1, 1, 1): 2, +} +# fmt: on + + +def get_sampling(im): + # There's no subsampling when images have only 1 layer + # (grayscale images) or when they are CMYK (4 layers), + # so set subsampling to the default value. + # + # NOTE: currently Pillow can't encode JPEG to YCCK format. + # If YCCK support is added in the future, subsampling code will have + # to be updated (here and in JpegEncode.c) to deal with 4 layers. + if not hasattr(im, "layers") or im.layers in (1, 4): + return -1 + sampling = im.layer[0][1:3] + im.layer[1][1:3] + im.layer[2][1:3] + return samplings.get(sampling, -1) + + +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + if im.width == 0 or im.height == 0: + msg = "cannot write empty image as JPEG" + raise ValueError(msg) + + try: + rawmode = RAWMODE[im.mode] + except KeyError as e: + msg = f"cannot write mode {im.mode} as JPEG" + raise OSError(msg) from e + + info = im.encoderinfo + + dpi = [round(x) for x in info.get("dpi", (0, 0))] + + quality = info.get("quality", -1) + subsampling = info.get("subsampling", -1) + qtables = info.get("qtables") + + if quality == "keep": + quality = -1 + subsampling = "keep" + qtables = "keep" + elif quality in presets: + preset = presets[quality] + quality = -1 + subsampling = preset.get("subsampling", -1) + qtables = preset.get("quantization") + elif not isinstance(quality, int): + msg = "Invalid quality setting" + raise ValueError(msg) + else: + if subsampling in presets: + subsampling = presets[subsampling].get("subsampling", -1) + if isinstance(qtables, str) and qtables in presets: + qtables = presets[qtables].get("quantization") + + if subsampling == "4:4:4": + subsampling = 0 + elif subsampling == "4:2:2": + subsampling = 1 + elif subsampling == "4:2:0": + subsampling = 2 + elif subsampling == "4:1:1": + # For compatibility. Before Pillow 4.3, 4:1:1 actually meant 4:2:0. + # Set 4:2:0 if someone is still using that value. + subsampling = 2 + elif subsampling == "keep": + if im.format != "JPEG": + msg = "Cannot use 'keep' when original image is not a JPEG" + raise ValueError(msg) + subsampling = get_sampling(im) + + def validate_qtables(qtables): + if qtables is None: + return qtables + if isinstance(qtables, str): + try: + lines = [ + int(num) + for line in qtables.splitlines() + for num in line.split("#", 1)[0].split() + ] + except ValueError as e: + msg = "Invalid quantization table" + raise ValueError(msg) from e + else: + qtables = [lines[s : s + 64] for s in range(0, len(lines), 64)] + if isinstance(qtables, (tuple, list, dict)): + if isinstance(qtables, dict): + qtables = [ + qtables[key] for key in range(len(qtables)) if key in qtables + ] + elif isinstance(qtables, tuple): + qtables = list(qtables) + if not (0 < len(qtables) < 5): + msg = "None or too many quantization tables" + raise ValueError(msg) + for idx, table in enumerate(qtables): + try: + if len(table) != 64: + msg = "Invalid quantization table" + raise TypeError(msg) + table = array.array("H", table) + except TypeError as e: + msg = "Invalid quantization table" + raise ValueError(msg) from e + else: + qtables[idx] = list(table) + return qtables + + if qtables == "keep": + if im.format != "JPEG": + msg = "Cannot use 'keep' when original image is not a JPEG" + raise ValueError(msg) + qtables = getattr(im, "quantization", None) + qtables = validate_qtables(qtables) + + extra = info.get("extra", b"") + + MAX_BYTES_IN_MARKER = 65533 + icc_profile = info.get("icc_profile") + if icc_profile: + ICC_OVERHEAD_LEN = 14 + MAX_DATA_BYTES_IN_MARKER = MAX_BYTES_IN_MARKER - ICC_OVERHEAD_LEN + markers = [] + while icc_profile: + markers.append(icc_profile[:MAX_DATA_BYTES_IN_MARKER]) + icc_profile = icc_profile[MAX_DATA_BYTES_IN_MARKER:] + i = 1 + for marker in markers: + size = o16(2 + ICC_OVERHEAD_LEN + len(marker)) + extra += ( + b"\xFF\xE2" + + size + + b"ICC_PROFILE\0" + + o8(i) + + o8(len(markers)) + + marker + ) + i += 1 + + comment = info.get("comment", im.info.get("comment")) + + # "progressive" is the official name, but older documentation + # says "progression" + # FIXME: issue a warning if the wrong form is used (post-1.1.7) + progressive = info.get("progressive", False) or info.get("progression", False) + + optimize = info.get("optimize", False) + + exif = info.get("exif", b"") + if isinstance(exif, Image.Exif): + exif = exif.tobytes() + if len(exif) > MAX_BYTES_IN_MARKER: + msg = "EXIF data is too long" + raise ValueError(msg) + + # get keyword arguments + im.encoderconfig = ( + quality, + progressive, + info.get("smooth", 0), + optimize, + info.get("keep_rgb", False), + info.get("streamtype", 0), + dpi[0], + dpi[1], + subsampling, + info.get("restart_marker_blocks", 0), + info.get("restart_marker_rows", 0), + qtables, + comment, + extra, + exif, + ) + + # if we optimize, libjpeg needs a buffer big enough to hold the whole image + # in a shot. Guessing on the size, at im.size bytes. (raw 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": + bufsize = 4 * im.size[0] * im.size[1] + # keep sets quality to -1, but the actual value may be high. + elif quality >= 95 or quality == -1: + bufsize = 2 * im.size[0] * im.size[1] + else: + bufsize = im.size[0] * im.size[1] + if exif: + bufsize += len(exif) + 5 + if extra: + bufsize += len(extra) + 1 + 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) + + ImageFile._save(im, fp, [("jpeg", (0, 0) + im.size, 0, rawmode)], bufsize) + + +def _save_cjpeg(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + # ALTERNATIVE: handle JPEGs via the IJG command line utilities. + tempfile = im._dump() + subprocess.check_call(["cjpeg", "-outfile", filename, tempfile]) + try: + os.unlink(tempfile) + except OSError: + pass + + +## +# Factory for making JPEG and MPO instances +def jpeg_factory(fp=None, filename=None): + im = JpegImageFile(fp, filename) + try: + mpheader = im._getmp() + if mpheader[45057] > 1: + # It's actually an MPO + from .MpoImagePlugin import MpoImageFile + + # Don't reload everything, just convert it. + im = MpoImageFile.adopt(im, mpheader) + except (TypeError, IndexError): + # It is really a JPEG + pass + except SyntaxError: + warnings.warn( + "Image appears to be a malformed MPO file, it will be " + "interpreted as a base JPEG file" + ) + return im + + +# --------------------------------------------------------------------- +# Registry stuff + +Image.register_open(JpegImageFile.format, jpeg_factory, _accept) +Image.register_save(JpegImageFile.format, _save) + +Image.register_extensions(JpegImageFile.format, [".jfif", ".jpe", ".jpg", ".jpeg"]) + +Image.register_mime(JpegImageFile.format, "image/jpeg") diff --git a/src/PIL/JpegPresets.py b/src/PIL/JpegPresets.py index 3aefa073c..47346257d 100644 --- a/src/PIL/JpegPresets.py +++ b/src/PIL/JpegPresets.py @@ -1,242 +1,242 @@ -""" -JPEG quality settings equivalent to the Photoshop settings. -Can be used when saving JPEG files. - -The following presets are available by default: -``web_low``, ``web_medium``, ``web_high``, ``web_very_high``, ``web_maximum``, -``low``, ``medium``, ``high``, ``maximum``. -More presets can be added to the :py:data:`presets` dict if needed. - -To apply the preset, specify:: - - quality="preset_name" - -To apply only the quantization table:: - - qtables="preset_name" - -To apply only the subsampling setting:: - - subsampling="preset_name" - -Example:: - - im.save("image_name.jpg", quality="web_high") - -Subsampling ------------ - -Subsampling is the practice of encoding images by implementing less resolution -for chroma information than for luma information. -(ref.: https://en.wikipedia.org/wiki/Chroma_subsampling) - -Possible subsampling values are 0, 1 and 2 that correspond to 4:4:4, 4:2:2 and -4:2:0. - -You can get the subsampling of a JPEG with the -:func:`.JpegImagePlugin.get_sampling` function. - -In JPEG compressed data a JPEG marker is used instead of an EXIF tag. -(ref.: https://web.archive.org/web/20240227115053/https://exiv2.org/tags.html) - - -Quantization tables -------------------- - -They are values use by the DCT (Discrete cosine transform) to remove -*unnecessary* information from the image (the lossy part of the compression). -(ref.: https://en.wikipedia.org/wiki/Quantization_matrix#Quantization_matrices, -https://en.wikipedia.org/wiki/JPEG#Quantization) - -You can get the quantization tables of a JPEG with:: - - im.quantization - -This will return a dict with a number of lists. You can pass this dict -directly as the qtables argument when saving a JPEG. - -The quantization table format in presets is a list with sublists. These formats -are interchangeable. - -Libjpeg ref.: -https://web.archive.org/web/20120328125543/http://www.jpegcameras.com/libjpeg/libjpeg-3.html - -""" - -from __future__ import annotations - -# fmt: off -presets = { - 'web_low': {'subsampling': 2, # "4:2:0" - 'quantization': [ - [20, 16, 25, 39, 50, 46, 62, 68, - 16, 18, 23, 38, 38, 53, 65, 68, - 25, 23, 31, 38, 53, 65, 68, 68, - 39, 38, 38, 53, 65, 68, 68, 68, - 50, 38, 53, 65, 68, 68, 68, 68, - 46, 53, 65, 68, 68, 68, 68, 68, - 62, 65, 68, 68, 68, 68, 68, 68, - 68, 68, 68, 68, 68, 68, 68, 68], - [21, 25, 32, 38, 54, 68, 68, 68, - 25, 28, 24, 38, 54, 68, 68, 68, - 32, 24, 32, 43, 66, 68, 68, 68, - 38, 38, 43, 53, 68, 68, 68, 68, - 54, 54, 66, 68, 68, 68, 68, 68, - 68, 68, 68, 68, 68, 68, 68, 68, - 68, 68, 68, 68, 68, 68, 68, 68, - 68, 68, 68, 68, 68, 68, 68, 68] - ]}, - 'web_medium': {'subsampling': 2, # "4:2:0" - 'quantization': [ - [16, 11, 11, 16, 23, 27, 31, 30, - 11, 12, 12, 15, 20, 23, 23, 30, - 11, 12, 13, 16, 23, 26, 35, 47, - 16, 15, 16, 23, 26, 37, 47, 64, - 23, 20, 23, 26, 39, 51, 64, 64, - 27, 23, 26, 37, 51, 64, 64, 64, - 31, 23, 35, 47, 64, 64, 64, 64, - 30, 30, 47, 64, 64, 64, 64, 64], - [17, 15, 17, 21, 20, 26, 38, 48, - 15, 19, 18, 17, 20, 26, 35, 43, - 17, 18, 20, 22, 26, 30, 46, 53, - 21, 17, 22, 28, 30, 39, 53, 64, - 20, 20, 26, 30, 39, 48, 64, 64, - 26, 26, 30, 39, 48, 63, 64, 64, - 38, 35, 46, 53, 64, 64, 64, 64, - 48, 43, 53, 64, 64, 64, 64, 64] - ]}, - 'web_high': {'subsampling': 0, # "4:4:4" - 'quantization': [ - [6, 4, 4, 6, 9, 11, 12, 16, - 4, 5, 5, 6, 8, 10, 12, 12, - 4, 5, 5, 6, 10, 12, 14, 19, - 6, 6, 6, 11, 12, 15, 19, 28, - 9, 8, 10, 12, 16, 20, 27, 31, - 11, 10, 12, 15, 20, 27, 31, 31, - 12, 12, 14, 19, 27, 31, 31, 31, - 16, 12, 19, 28, 31, 31, 31, 31], - [7, 7, 13, 24, 26, 31, 31, 31, - 7, 12, 16, 21, 31, 31, 31, 31, - 13, 16, 17, 31, 31, 31, 31, 31, - 24, 21, 31, 31, 31, 31, 31, 31, - 26, 31, 31, 31, 31, 31, 31, 31, - 31, 31, 31, 31, 31, 31, 31, 31, - 31, 31, 31, 31, 31, 31, 31, 31, - 31, 31, 31, 31, 31, 31, 31, 31] - ]}, - 'web_very_high': {'subsampling': 0, # "4:4:4" - 'quantization': [ - [2, 2, 2, 2, 3, 4, 5, 6, - 2, 2, 2, 2, 3, 4, 5, 6, - 2, 2, 2, 2, 4, 5, 7, 9, - 2, 2, 2, 4, 5, 7, 9, 12, - 3, 3, 4, 5, 8, 10, 12, 12, - 4, 4, 5, 7, 10, 12, 12, 12, - 5, 5, 7, 9, 12, 12, 12, 12, - 6, 6, 9, 12, 12, 12, 12, 12], - [3, 3, 5, 9, 13, 15, 15, 15, - 3, 4, 6, 11, 14, 12, 12, 12, - 5, 6, 9, 14, 12, 12, 12, 12, - 9, 11, 14, 12, 12, 12, 12, 12, - 13, 14, 12, 12, 12, 12, 12, 12, - 15, 12, 12, 12, 12, 12, 12, 12, - 15, 12, 12, 12, 12, 12, 12, 12, - 15, 12, 12, 12, 12, 12, 12, 12] - ]}, - 'web_maximum': {'subsampling': 0, # "4:4:4" - 'quantization': [ - [1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 2, - 1, 1, 1, 1, 1, 1, 2, 2, - 1, 1, 1, 1, 1, 2, 2, 3, - 1, 1, 1, 1, 2, 2, 3, 3, - 1, 1, 1, 2, 2, 3, 3, 3, - 1, 1, 2, 2, 3, 3, 3, 3], - [1, 1, 1, 2, 2, 3, 3, 3, - 1, 1, 1, 2, 3, 3, 3, 3, - 1, 1, 1, 3, 3, 3, 3, 3, - 2, 2, 3, 3, 3, 3, 3, 3, - 2, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3] - ]}, - 'low': {'subsampling': 2, # "4:2:0" - 'quantization': [ - [18, 14, 14, 21, 30, 35, 34, 17, - 14, 16, 16, 19, 26, 23, 12, 12, - 14, 16, 17, 21, 23, 12, 12, 12, - 21, 19, 21, 23, 12, 12, 12, 12, - 30, 26, 23, 12, 12, 12, 12, 12, - 35, 23, 12, 12, 12, 12, 12, 12, - 34, 12, 12, 12, 12, 12, 12, 12, - 17, 12, 12, 12, 12, 12, 12, 12], - [20, 19, 22, 27, 20, 20, 17, 17, - 19, 25, 23, 14, 14, 12, 12, 12, - 22, 23, 14, 14, 12, 12, 12, 12, - 27, 14, 14, 12, 12, 12, 12, 12, - 20, 14, 12, 12, 12, 12, 12, 12, - 20, 12, 12, 12, 12, 12, 12, 12, - 17, 12, 12, 12, 12, 12, 12, 12, - 17, 12, 12, 12, 12, 12, 12, 12] - ]}, - 'medium': {'subsampling': 2, # "4:2:0" - 'quantization': [ - [12, 8, 8, 12, 17, 21, 24, 17, - 8, 9, 9, 11, 15, 19, 12, 12, - 8, 9, 10, 12, 19, 12, 12, 12, - 12, 11, 12, 21, 12, 12, 12, 12, - 17, 15, 19, 12, 12, 12, 12, 12, - 21, 19, 12, 12, 12, 12, 12, 12, - 24, 12, 12, 12, 12, 12, 12, 12, - 17, 12, 12, 12, 12, 12, 12, 12], - [13, 11, 13, 16, 20, 20, 17, 17, - 11, 14, 14, 14, 14, 12, 12, 12, - 13, 14, 14, 14, 12, 12, 12, 12, - 16, 14, 14, 12, 12, 12, 12, 12, - 20, 14, 12, 12, 12, 12, 12, 12, - 20, 12, 12, 12, 12, 12, 12, 12, - 17, 12, 12, 12, 12, 12, 12, 12, - 17, 12, 12, 12, 12, 12, 12, 12] - ]}, - 'high': {'subsampling': 0, # "4:4:4" - 'quantization': [ - [6, 4, 4, 6, 9, 11, 12, 16, - 4, 5, 5, 6, 8, 10, 12, 12, - 4, 5, 5, 6, 10, 12, 12, 12, - 6, 6, 6, 11, 12, 12, 12, 12, - 9, 8, 10, 12, 12, 12, 12, 12, - 11, 10, 12, 12, 12, 12, 12, 12, - 12, 12, 12, 12, 12, 12, 12, 12, - 16, 12, 12, 12, 12, 12, 12, 12], - [7, 7, 13, 24, 20, 20, 17, 17, - 7, 12, 16, 14, 14, 12, 12, 12, - 13, 16, 14, 14, 12, 12, 12, 12, - 24, 14, 14, 12, 12, 12, 12, 12, - 20, 14, 12, 12, 12, 12, 12, 12, - 20, 12, 12, 12, 12, 12, 12, 12, - 17, 12, 12, 12, 12, 12, 12, 12, - 17, 12, 12, 12, 12, 12, 12, 12] - ]}, - 'maximum': {'subsampling': 0, # "4:4:4" - 'quantization': [ - [2, 2, 2, 2, 3, 4, 5, 6, - 2, 2, 2, 2, 3, 4, 5, 6, - 2, 2, 2, 2, 4, 5, 7, 9, - 2, 2, 2, 4, 5, 7, 9, 12, - 3, 3, 4, 5, 8, 10, 12, 12, - 4, 4, 5, 7, 10, 12, 12, 12, - 5, 5, 7, 9, 12, 12, 12, 12, - 6, 6, 9, 12, 12, 12, 12, 12], - [3, 3, 5, 9, 13, 15, 15, 15, - 3, 4, 6, 10, 14, 12, 12, 12, - 5, 6, 9, 14, 12, 12, 12, 12, - 9, 10, 14, 12, 12, 12, 12, 12, - 13, 14, 12, 12, 12, 12, 12, 12, - 15, 12, 12, 12, 12, 12, 12, 12, - 15, 12, 12, 12, 12, 12, 12, 12, - 15, 12, 12, 12, 12, 12, 12, 12] - ]}, -} -# fmt: on +""" +JPEG quality settings equivalent to the Photoshop settings. +Can be used when saving JPEG files. + +The following presets are available by default: +``web_low``, ``web_medium``, ``web_high``, ``web_very_high``, ``web_maximum``, +``low``, ``medium``, ``high``, ``maximum``. +More presets can be added to the :py:data:`presets` dict if needed. + +To apply the preset, specify:: + + quality="preset_name" + +To apply only the quantization table:: + + qtables="preset_name" + +To apply only the subsampling setting:: + + subsampling="preset_name" + +Example:: + + im.save("image_name.jpg", quality="web_high") + +Subsampling +----------- + +Subsampling is the practice of encoding images by implementing less resolution +for chroma information than for luma information. +(ref.: https://en.wikipedia.org/wiki/Chroma_subsampling) + +Possible subsampling values are 0, 1 and 2 that correspond to 4:4:4, 4:2:2 and +4:2:0. + +You can get the subsampling of a JPEG with the +:func:`.JpegImagePlugin.get_sampling` function. + +In JPEG compressed data a JPEG marker is used instead of an EXIF tag. +(ref.: https://web.archive.org/web/20240227115053/https://exiv2.org/tags.html) + + +Quantization tables +------------------- + +They are values use by the DCT (Discrete cosine transform) to remove +*unnecessary* information from the image (the lossy part of the compression). +(ref.: https://en.wikipedia.org/wiki/Quantization_matrix#Quantization_matrices, +https://en.wikipedia.org/wiki/JPEG#Quantization) + +You can get the quantization tables of a JPEG with:: + + im.quantization + +This will return a dict with a number of lists. You can pass this dict +directly as the qtables argument when saving a JPEG. + +The quantization table format in presets is a list with sublists. These formats +are interchangeable. + +Libjpeg ref.: +https://web.archive.org/web/20120328125543/http://www.jpegcameras.com/libjpeg/libjpeg-3.html + +""" + +from __future__ import annotations + +# fmt: off +presets = { + 'web_low': {'subsampling': 2, # "4:2:0" + 'quantization': [ + [20, 16, 25, 39, 50, 46, 62, 68, + 16, 18, 23, 38, 38, 53, 65, 68, + 25, 23, 31, 38, 53, 65, 68, 68, + 39, 38, 38, 53, 65, 68, 68, 68, + 50, 38, 53, 65, 68, 68, 68, 68, + 46, 53, 65, 68, 68, 68, 68, 68, + 62, 65, 68, 68, 68, 68, 68, 68, + 68, 68, 68, 68, 68, 68, 68, 68], + [21, 25, 32, 38, 54, 68, 68, 68, + 25, 28, 24, 38, 54, 68, 68, 68, + 32, 24, 32, 43, 66, 68, 68, 68, + 38, 38, 43, 53, 68, 68, 68, 68, + 54, 54, 66, 68, 68, 68, 68, 68, + 68, 68, 68, 68, 68, 68, 68, 68, + 68, 68, 68, 68, 68, 68, 68, 68, + 68, 68, 68, 68, 68, 68, 68, 68] + ]}, + 'web_medium': {'subsampling': 2, # "4:2:0" + 'quantization': [ + [16, 11, 11, 16, 23, 27, 31, 30, + 11, 12, 12, 15, 20, 23, 23, 30, + 11, 12, 13, 16, 23, 26, 35, 47, + 16, 15, 16, 23, 26, 37, 47, 64, + 23, 20, 23, 26, 39, 51, 64, 64, + 27, 23, 26, 37, 51, 64, 64, 64, + 31, 23, 35, 47, 64, 64, 64, 64, + 30, 30, 47, 64, 64, 64, 64, 64], + [17, 15, 17, 21, 20, 26, 38, 48, + 15, 19, 18, 17, 20, 26, 35, 43, + 17, 18, 20, 22, 26, 30, 46, 53, + 21, 17, 22, 28, 30, 39, 53, 64, + 20, 20, 26, 30, 39, 48, 64, 64, + 26, 26, 30, 39, 48, 63, 64, 64, + 38, 35, 46, 53, 64, 64, 64, 64, + 48, 43, 53, 64, 64, 64, 64, 64] + ]}, + 'web_high': {'subsampling': 0, # "4:4:4" + 'quantization': [ + [6, 4, 4, 6, 9, 11, 12, 16, + 4, 5, 5, 6, 8, 10, 12, 12, + 4, 5, 5, 6, 10, 12, 14, 19, + 6, 6, 6, 11, 12, 15, 19, 28, + 9, 8, 10, 12, 16, 20, 27, 31, + 11, 10, 12, 15, 20, 27, 31, 31, + 12, 12, 14, 19, 27, 31, 31, 31, + 16, 12, 19, 28, 31, 31, 31, 31], + [7, 7, 13, 24, 26, 31, 31, 31, + 7, 12, 16, 21, 31, 31, 31, 31, + 13, 16, 17, 31, 31, 31, 31, 31, + 24, 21, 31, 31, 31, 31, 31, 31, + 26, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31] + ]}, + 'web_very_high': {'subsampling': 0, # "4:4:4" + 'quantization': [ + [2, 2, 2, 2, 3, 4, 5, 6, + 2, 2, 2, 2, 3, 4, 5, 6, + 2, 2, 2, 2, 4, 5, 7, 9, + 2, 2, 2, 4, 5, 7, 9, 12, + 3, 3, 4, 5, 8, 10, 12, 12, + 4, 4, 5, 7, 10, 12, 12, 12, + 5, 5, 7, 9, 12, 12, 12, 12, + 6, 6, 9, 12, 12, 12, 12, 12], + [3, 3, 5, 9, 13, 15, 15, 15, + 3, 4, 6, 11, 14, 12, 12, 12, + 5, 6, 9, 14, 12, 12, 12, 12, + 9, 11, 14, 12, 12, 12, 12, 12, + 13, 14, 12, 12, 12, 12, 12, 12, + 15, 12, 12, 12, 12, 12, 12, 12, + 15, 12, 12, 12, 12, 12, 12, 12, + 15, 12, 12, 12, 12, 12, 12, 12] + ]}, + 'web_maximum': {'subsampling': 0, # "4:4:4" + 'quantization': [ + [1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 2, + 1, 1, 1, 1, 1, 1, 2, 2, + 1, 1, 1, 1, 1, 2, 2, 3, + 1, 1, 1, 1, 2, 2, 3, 3, + 1, 1, 1, 2, 2, 3, 3, 3, + 1, 1, 2, 2, 3, 3, 3, 3], + [1, 1, 1, 2, 2, 3, 3, 3, + 1, 1, 1, 2, 3, 3, 3, 3, + 1, 1, 1, 3, 3, 3, 3, 3, + 2, 2, 3, 3, 3, 3, 3, 3, + 2, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3] + ]}, + 'low': {'subsampling': 2, # "4:2:0" + 'quantization': [ + [18, 14, 14, 21, 30, 35, 34, 17, + 14, 16, 16, 19, 26, 23, 12, 12, + 14, 16, 17, 21, 23, 12, 12, 12, + 21, 19, 21, 23, 12, 12, 12, 12, + 30, 26, 23, 12, 12, 12, 12, 12, + 35, 23, 12, 12, 12, 12, 12, 12, + 34, 12, 12, 12, 12, 12, 12, 12, + 17, 12, 12, 12, 12, 12, 12, 12], + [20, 19, 22, 27, 20, 20, 17, 17, + 19, 25, 23, 14, 14, 12, 12, 12, + 22, 23, 14, 14, 12, 12, 12, 12, + 27, 14, 14, 12, 12, 12, 12, 12, + 20, 14, 12, 12, 12, 12, 12, 12, + 20, 12, 12, 12, 12, 12, 12, 12, + 17, 12, 12, 12, 12, 12, 12, 12, + 17, 12, 12, 12, 12, 12, 12, 12] + ]}, + 'medium': {'subsampling': 2, # "4:2:0" + 'quantization': [ + [12, 8, 8, 12, 17, 21, 24, 17, + 8, 9, 9, 11, 15, 19, 12, 12, + 8, 9, 10, 12, 19, 12, 12, 12, + 12, 11, 12, 21, 12, 12, 12, 12, + 17, 15, 19, 12, 12, 12, 12, 12, + 21, 19, 12, 12, 12, 12, 12, 12, + 24, 12, 12, 12, 12, 12, 12, 12, + 17, 12, 12, 12, 12, 12, 12, 12], + [13, 11, 13, 16, 20, 20, 17, 17, + 11, 14, 14, 14, 14, 12, 12, 12, + 13, 14, 14, 14, 12, 12, 12, 12, + 16, 14, 14, 12, 12, 12, 12, 12, + 20, 14, 12, 12, 12, 12, 12, 12, + 20, 12, 12, 12, 12, 12, 12, 12, + 17, 12, 12, 12, 12, 12, 12, 12, + 17, 12, 12, 12, 12, 12, 12, 12] + ]}, + 'high': {'subsampling': 0, # "4:4:4" + 'quantization': [ + [6, 4, 4, 6, 9, 11, 12, 16, + 4, 5, 5, 6, 8, 10, 12, 12, + 4, 5, 5, 6, 10, 12, 12, 12, + 6, 6, 6, 11, 12, 12, 12, 12, + 9, 8, 10, 12, 12, 12, 12, 12, + 11, 10, 12, 12, 12, 12, 12, 12, + 12, 12, 12, 12, 12, 12, 12, 12, + 16, 12, 12, 12, 12, 12, 12, 12], + [7, 7, 13, 24, 20, 20, 17, 17, + 7, 12, 16, 14, 14, 12, 12, 12, + 13, 16, 14, 14, 12, 12, 12, 12, + 24, 14, 14, 12, 12, 12, 12, 12, + 20, 14, 12, 12, 12, 12, 12, 12, + 20, 12, 12, 12, 12, 12, 12, 12, + 17, 12, 12, 12, 12, 12, 12, 12, + 17, 12, 12, 12, 12, 12, 12, 12] + ]}, + 'maximum': {'subsampling': 0, # "4:4:4" + 'quantization': [ + [2, 2, 2, 2, 3, 4, 5, 6, + 2, 2, 2, 2, 3, 4, 5, 6, + 2, 2, 2, 2, 4, 5, 7, 9, + 2, 2, 2, 4, 5, 7, 9, 12, + 3, 3, 4, 5, 8, 10, 12, 12, + 4, 4, 5, 7, 10, 12, 12, 12, + 5, 5, 7, 9, 12, 12, 12, 12, + 6, 6, 9, 12, 12, 12, 12, 12], + [3, 3, 5, 9, 13, 15, 15, 15, + 3, 4, 6, 10, 14, 12, 12, 12, + 5, 6, 9, 14, 12, 12, 12, 12, + 9, 10, 14, 12, 12, 12, 12, 12, + 13, 14, 12, 12, 12, 12, 12, 12, + 15, 12, 12, 12, 12, 12, 12, 12, + 15, 12, 12, 12, 12, 12, 12, 12, + 15, 12, 12, 12, 12, 12, 12, 12] + ]}, +} +# fmt: on diff --git a/src/PIL/McIdasImagePlugin.py b/src/PIL/McIdasImagePlugin.py index 27972236c..d2a8f32e7 100644 --- a/src/PIL/McIdasImagePlugin.py +++ b/src/PIL/McIdasImagePlugin.py @@ -1,78 +1,78 @@ -# -# The Python Imaging Library. -# $Id$ -# -# Basic McIdas support for PIL -# -# History: -# 1997-05-05 fl Created (8-bit images only) -# 2009-03-08 fl Added 16/32-bit support. -# -# Thanks to Richard Jones and Craig Swank for specs and samples. -# -# Copyright (c) Secret Labs AB 1997. -# Copyright (c) Fredrik Lundh 1997. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import struct - -from . import Image, ImageFile - - -def _accept(prefix: bytes) -> bool: - return prefix[:8] == b"\x00\x00\x00\x00\x00\x00\x00\x04" - - -## -# Image plugin for McIdas area images. - - -class McIdasImageFile(ImageFile.ImageFile): - format = "MCIDAS" - format_description = "McIdas area file" - - def _open(self) -> None: - # parse area file directory - assert self.fp is not None - - s = self.fp.read(256) - if not _accept(s) or len(s) != 256: - msg = "not an McIdas area file" - raise SyntaxError(msg) - - self.area_descriptor_raw = s - self.area_descriptor = w = [0] + list(struct.unpack("!64i", s)) - - # get mode - if w[11] == 1: - mode = rawmode = "L" - elif w[11] == 2: - # FIXME: add memory map support - mode = "I" - rawmode = "I;16B" - elif w[11] == 4: - # FIXME: add memory map support - mode = "I" - rawmode = "I;32B" - else: - msg = "unsupported McIdas format" - raise SyntaxError(msg) - - self._mode = mode - self._size = w[10], w[9] - - offset = w[34] + w[15] - stride = w[15] + w[10] * w[11] * w[14] - - self.tile = [("raw", (0, 0) + self.size, offset, (rawmode, stride, 1))] - - -# -------------------------------------------------------------------- -# registry - -Image.register_open(McIdasImageFile.format, McIdasImageFile, _accept) - -# no default extension +# +# The Python Imaging Library. +# $Id$ +# +# Basic McIdas support for PIL +# +# History: +# 1997-05-05 fl Created (8-bit images only) +# 2009-03-08 fl Added 16/32-bit support. +# +# Thanks to Richard Jones and Craig Swank for specs and samples. +# +# Copyright (c) Secret Labs AB 1997. +# Copyright (c) Fredrik Lundh 1997. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import struct + +from . import Image, ImageFile + + +def _accept(prefix: bytes) -> bool: + return prefix[:8] == b"\x00\x00\x00\x00\x00\x00\x00\x04" + + +## +# Image plugin for McIdas area images. + + +class McIdasImageFile(ImageFile.ImageFile): + format = "MCIDAS" + format_description = "McIdas area file" + + def _open(self) -> None: + # parse area file directory + assert self.fp is not None + + s = self.fp.read(256) + if not _accept(s) or len(s) != 256: + msg = "not an McIdas area file" + raise SyntaxError(msg) + + self.area_descriptor_raw = s + self.area_descriptor = w = [0] + list(struct.unpack("!64i", s)) + + # get mode + if w[11] == 1: + mode = rawmode = "L" + elif w[11] == 2: + # FIXME: add memory map support + mode = "I" + rawmode = "I;16B" + elif w[11] == 4: + # FIXME: add memory map support + mode = "I" + rawmode = "I;32B" + else: + msg = "unsupported McIdas format" + raise SyntaxError(msg) + + self._mode = mode + self._size = w[10], w[9] + + offset = w[34] + w[15] + stride = w[15] + w[10] * w[11] * w[14] + + self.tile = [("raw", (0, 0) + self.size, offset, (rawmode, stride, 1))] + + +# -------------------------------------------------------------------- +# registry + +Image.register_open(McIdasImageFile.format, McIdasImageFile, _accept) + +# no default extension diff --git a/src/PIL/MicImagePlugin.py b/src/PIL/MicImagePlugin.py index ed2ea2849..33fcb6db8 100644 --- a/src/PIL/MicImagePlugin.py +++ b/src/PIL/MicImagePlugin.py @@ -1,107 +1,107 @@ -# -# The Python Imaging Library. -# $Id$ -# -# Microsoft Image Composer support for PIL -# -# Notes: -# uses TiffImagePlugin.py to read the actual image streams -# -# History: -# 97-01-20 fl Created -# -# Copyright (c) Secret Labs AB 1997. -# Copyright (c) Fredrik Lundh 1997. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import olefile - -from . import Image, TiffImagePlugin - -# -# -------------------------------------------------------------------- - - -def _accept(prefix: bytes) -> bool: - return prefix[:8] == olefile.MAGIC - - -## -# Image plugin for Microsoft's Image Composer file format. - - -class MicImageFile(TiffImagePlugin.TiffImageFile): - format = "MIC" - format_description = "Microsoft Image Composer" - _close_exclusive_fp_after_loading = False - - def _open(self) -> None: - # read the OLE directory and see if this is a likely - # to be a Microsoft Image Composer file - - try: - self.ole = olefile.OleFileIO(self.fp) - except OSError as e: - msg = "not an MIC file; invalid OLE file" - raise SyntaxError(msg) from e - - # find ACI subfiles with Image members (maybe not the - # best way to identify MIC files, but what the... ;-) - - self.images = [ - path - for path in self.ole.listdir() - if path[1:] and path[0][-4:] == ".ACI" and path[1] == "Image" - ] - - # if we didn't find any images, this is probably not - # an MIC file. - if not self.images: - msg = "not an MIC file; no image entries" - raise SyntaxError(msg) - - self.frame = None - self._n_frames = len(self.images) - self.is_animated = self._n_frames > 1 - - self.__fp = self.fp - self.seek(0) - - def seek(self, frame): - 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 - - self.fp = self.ole.openstream(filename) - - TiffImagePlugin.TiffImageFile._open(self) - - self.frame = frame - - def tell(self): - return self.frame - - def close(self) -> None: - self.__fp.close() - self.ole.close() - super().close() - - def __exit__(self, *args: object) -> None: - self.__fp.close() - self.ole.close() - super().__exit__() - - -# -# -------------------------------------------------------------------- - -Image.register_open(MicImageFile.format, MicImageFile, _accept) - -Image.register_extension(MicImageFile.format, ".mic") +# +# The Python Imaging Library. +# $Id$ +# +# Microsoft Image Composer support for PIL +# +# Notes: +# uses TiffImagePlugin.py to read the actual image streams +# +# History: +# 97-01-20 fl Created +# +# Copyright (c) Secret Labs AB 1997. +# Copyright (c) Fredrik Lundh 1997. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import olefile + +from . import Image, TiffImagePlugin + +# +# -------------------------------------------------------------------- + + +def _accept(prefix: bytes) -> bool: + return prefix[:8] == olefile.MAGIC + + +## +# Image plugin for Microsoft's Image Composer file format. + + +class MicImageFile(TiffImagePlugin.TiffImageFile): + format = "MIC" + format_description = "Microsoft Image Composer" + _close_exclusive_fp_after_loading = False + + def _open(self) -> None: + # read the OLE directory and see if this is a likely + # to be a Microsoft Image Composer file + + try: + self.ole = olefile.OleFileIO(self.fp) + except OSError as e: + msg = "not an MIC file; invalid OLE file" + raise SyntaxError(msg) from e + + # find ACI subfiles with Image members (maybe not the + # best way to identify MIC files, but what the... ;-) + + self.images = [ + path + for path in self.ole.listdir() + if path[1:] and path[0][-4:] == ".ACI" and path[1] == "Image" + ] + + # if we didn't find any images, this is probably not + # an MIC file. + if not self.images: + msg = "not an MIC file; no image entries" + raise SyntaxError(msg) + + self.frame = None + self._n_frames = len(self.images) + self.is_animated = self._n_frames > 1 + + self.__fp = self.fp + self.seek(0) + + def seek(self, frame): + 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 + + self.fp = self.ole.openstream(filename) + + TiffImagePlugin.TiffImageFile._open(self) + + self.frame = frame + + def tell(self): + return self.frame + + def close(self) -> None: + self.__fp.close() + self.ole.close() + super().close() + + def __exit__(self, *args: object) -> None: + self.__fp.close() + self.ole.close() + super().__exit__() + + +# +# -------------------------------------------------------------------- + +Image.register_open(MicImageFile.format, MicImageFile, _accept) + +Image.register_extension(MicImageFile.format, ".mic") diff --git a/src/PIL/MpegImagePlugin.py b/src/PIL/MpegImagePlugin.py index ad4d3e937..cf62cd87d 100644 --- a/src/PIL/MpegImagePlugin.py +++ b/src/PIL/MpegImagePlugin.py @@ -1,88 +1,88 @@ -# -# The Python Imaging Library. -# $Id$ -# -# MPEG file handling -# -# History: -# 95-09-09 fl Created -# -# Copyright (c) Secret Labs AB 1997. -# Copyright (c) Fredrik Lundh 1995. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -from . import Image, ImageFile -from ._binary import i8 -from ._typing import SupportsRead - -# -# Bitstream parser - - -class BitStream: - def __init__(self, fp: SupportsRead[bytes]) -> None: - self.fp = fp - self.bits = 0 - self.bitbuffer = 0 - - def next(self) -> int: - return i8(self.fp.read(1)) - - 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.bits += 8 - return self.bitbuffer >> (self.bits - bits) & (1 << bits) - 1 - - def skip(self, bits: int) -> None: - while self.bits < bits: - self.bitbuffer = (self.bitbuffer << 8) + i8(self.fp.read(1)) - self.bits += 8 - self.bits = self.bits - bits - - def read(self, bits: int) -> int: - v = self.peek(bits) - self.bits = self.bits - bits - return v - - -def _accept(prefix: bytes) -> bool: - return prefix[:4] == b"\x00\x00\x01\xb3" - - -## -# Image plugin for MPEG streams. This plugin can identify a stream, -# but it cannot read it. - - -class MpegImageFile(ImageFile.ImageFile): - format = "MPEG" - format_description = "MPEG" - - def _open(self) -> None: - assert self.fp is not None - - s = BitStream(self.fp) - if s.read(32) != 0x1B3: - msg = "not an MPEG file" - raise SyntaxError(msg) - - self._mode = "RGB" - self._size = s.read(12), s.read(12) - - -# -------------------------------------------------------------------- -# Registry stuff - -Image.register_open(MpegImageFile.format, MpegImageFile, _accept) - -Image.register_extensions(MpegImageFile.format, [".mpg", ".mpeg"]) - -Image.register_mime(MpegImageFile.format, "video/mpeg") +# +# The Python Imaging Library. +# $Id$ +# +# MPEG file handling +# +# History: +# 95-09-09 fl Created +# +# Copyright (c) Secret Labs AB 1997. +# Copyright (c) Fredrik Lundh 1995. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +from . import Image, ImageFile +from ._binary import i8 +from ._typing import SupportsRead + +# +# Bitstream parser + + +class BitStream: + def __init__(self, fp: SupportsRead[bytes]) -> None: + self.fp = fp + self.bits = 0 + self.bitbuffer = 0 + + def next(self) -> int: + return i8(self.fp.read(1)) + + 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.bits += 8 + return self.bitbuffer >> (self.bits - bits) & (1 << bits) - 1 + + def skip(self, bits: int) -> None: + while self.bits < bits: + self.bitbuffer = (self.bitbuffer << 8) + i8(self.fp.read(1)) + self.bits += 8 + self.bits = self.bits - bits + + def read(self, bits: int) -> int: + v = self.peek(bits) + self.bits = self.bits - bits + return v + + +def _accept(prefix: bytes) -> bool: + return prefix[:4] == b"\x00\x00\x01\xb3" + + +## +# Image plugin for MPEG streams. This plugin can identify a stream, +# but it cannot read it. + + +class MpegImageFile(ImageFile.ImageFile): + format = "MPEG" + format_description = "MPEG" + + def _open(self) -> None: + assert self.fp is not None + + s = BitStream(self.fp) + if s.read(32) != 0x1B3: + msg = "not an MPEG file" + raise SyntaxError(msg) + + self._mode = "RGB" + self._size = s.read(12), s.read(12) + + +# -------------------------------------------------------------------- +# Registry stuff + +Image.register_open(MpegImageFile.format, MpegImageFile, _accept) + +Image.register_extensions(MpegImageFile.format, [".mpg", ".mpeg"]) + +Image.register_mime(MpegImageFile.format, "video/mpeg") diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index f21570661..0a3a0e4c0 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -1,180 +1,180 @@ -# -# The Python Imaging Library. -# $Id$ -# -# MPO file handling -# -# See "Multi-Picture Format" (CIPA DC-007-Translation 2009, Standard of the -# Camera & Imaging Products Association) -# -# The multi-picture object combines multiple JPEG images (with a modified EXIF -# data format) into a single file. While it can theoretically be used much like -# a GIF animation, it is commonly used to represent 3D photographs and is (as -# of this writing) the most commonly used format by 3D cameras. -# -# History: -# 2014-03-13 Feneric Created -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import itertools -import os -import struct -from typing import IO - -from . import ( - Image, - ImageSequence, - JpegImagePlugin, - TiffImagePlugin, -) -from ._binary import o32le - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - JpegImagePlugin._save(im, fp, filename) - - -def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - append_images = im.encoderinfo.get("append_images", []) - if not append_images and not getattr(im, "is_animated", False): - _save(im, fp, filename) - return - - mpf_offset = 28 - offsets: list[int] = [] - for imSequence in itertools.chain([im], append_images): - for im_frame in ImageSequence.Iterator(imSequence): - if not offsets: - # APP2 marker - im_frame.encoderinfo["extra"] = ( - b"\xFF\xE2" + struct.pack(">H", 6 + 82) + b"MPF\0" + b" " * 82 - ) - exif = im_frame.encoderinfo.get("exif") - if isinstance(exif, Image.Exif): - exif = exif.tobytes() - im_frame.encoderinfo["exif"] = exif - if exif: - mpf_offset += 4 + len(exif) - - JpegImagePlugin._save(im_frame, fp, filename) - offsets.append(fp.tell()) - else: - im_frame.save(fp, "JPEG") - offsets.append(fp.tell() - offsets[-1]) - - ifd = TiffImagePlugin.ImageFileDirectory_v2() - ifd[0xB000] = b"0100" - ifd[0xB001] = len(offsets) - - mpentries = b"" - data_offset = 0 - for i, size in enumerate(offsets): - if i == 0: - mptype = 0x030000 # Baseline MP Primary Image - else: - mptype = 0x000000 # Undefined - mpentries += struct.pack(" None: - self.fp.seek(0) # prep the fp in order to pass the JPEG test - JpegImagePlugin.JpegImageFile._open(self) - self._after_jpeg_open() - - def _after_jpeg_open(self, mpheader=None): - self.mpinfo = mpheader if mpheader is not None else self._getmp() - self.n_frames = self.mpinfo[0xB001] - self.__mpoffsets = [ - mpent["DataOffset"] + self.info["mpoffset"] for mpent in self.mpinfo[0xB002] - ] - self.__mpoffsets[0] = 0 - # Note that the following assertion will only be invalid if something - # gets broken within JpegImagePlugin. - assert self.n_frames == len(self.__mpoffsets) - del self.info["mpoffset"] # no longer needed - self.is_animated = self.n_frames > 1 - self._fp = self.fp # FIXME: hack - self._fp.seek(self.__mpoffsets[0]) # get ready to read first frame - self.__frame = 0 - self.offset = 0 - # for now we can only handle reading and individual frame extraction - self.readonly = 1 - - def load_seek(self, pos: int) -> None: - self._fp.seek(pos) - - def seek(self, frame: int) -> None: - if not self._seek_check(frame): - return - self.fp = self._fp - self.offset = self.__mpoffsets[frame] - - original_exif = self.info.get("exif") - if "exif" in self.info: - del self.info["exif"] - - self.fp.seek(self.offset + 2) # skip SOI marker - if not self.fp.read(2): - msg = "No data found for frame" - raise ValueError(msg) - self.fp.seek(self.offset) - JpegImagePlugin.JpegImageFile._open(self) - if self.info.get("exif") != original_exif: - self._reload_exif() - - self.tile = [("jpeg", (0, 0) + self.size, self.offset, self.tile[0][-1])] - self.__frame = frame - - def tell(self) -> int: - return self.__frame - - @staticmethod - def adopt(jpeg_instance, mpheader=None): - """ - Transform the instance of JpegImageFile into - an instance of MpoImageFile. - After the call, the JpegImageFile is extended - to be an MpoImageFile. - - This is essentially useful when opening a JPEG - file that reveals itself as an MPO, to avoid - double call to _open. - """ - jpeg_instance.__class__ = MpoImageFile - jpeg_instance._after_jpeg_open(mpheader) - return jpeg_instance - - -# --------------------------------------------------------------------- -# Registry stuff - -# Note that since MPO shares a factory with JPEG, we do not need to do a -# separate registration for it here. -# Image.register_open(MpoImageFile.format, -# JpegImagePlugin.jpeg_factory, _accept) -Image.register_save(MpoImageFile.format, _save) -Image.register_save_all(MpoImageFile.format, _save_all) - -Image.register_extension(MpoImageFile.format, ".mpo") - -Image.register_mime(MpoImageFile.format, "image/mpo") +# +# The Python Imaging Library. +# $Id$ +# +# MPO file handling +# +# See "Multi-Picture Format" (CIPA DC-007-Translation 2009, Standard of the +# Camera & Imaging Products Association) +# +# The multi-picture object combines multiple JPEG images (with a modified EXIF +# data format) into a single file. While it can theoretically be used much like +# a GIF animation, it is commonly used to represent 3D photographs and is (as +# of this writing) the most commonly used format by 3D cameras. +# +# History: +# 2014-03-13 Feneric Created +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import itertools +import os +import struct +from typing import IO + +from . import ( + Image, + ImageSequence, + JpegImagePlugin, + TiffImagePlugin, +) +from ._binary import o32le + + +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + JpegImagePlugin._save(im, fp, filename) + + +def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + append_images = im.encoderinfo.get("append_images", []) + if not append_images and not getattr(im, "is_animated", False): + _save(im, fp, filename) + return + + mpf_offset = 28 + offsets: list[int] = [] + for imSequence in itertools.chain([im], append_images): + for im_frame in ImageSequence.Iterator(imSequence): + if not offsets: + # APP2 marker + im_frame.encoderinfo["extra"] = ( + b"\xFF\xE2" + struct.pack(">H", 6 + 82) + b"MPF\0" + b" " * 82 + ) + exif = im_frame.encoderinfo.get("exif") + if isinstance(exif, Image.Exif): + exif = exif.tobytes() + im_frame.encoderinfo["exif"] = exif + if exif: + mpf_offset += 4 + len(exif) + + JpegImagePlugin._save(im_frame, fp, filename) + offsets.append(fp.tell()) + else: + im_frame.save(fp, "JPEG") + offsets.append(fp.tell() - offsets[-1]) + + ifd = TiffImagePlugin.ImageFileDirectory_v2() + ifd[0xB000] = b"0100" + ifd[0xB001] = len(offsets) + + mpentries = b"" + data_offset = 0 + for i, size in enumerate(offsets): + if i == 0: + mptype = 0x030000 # Baseline MP Primary Image + else: + mptype = 0x000000 # Undefined + mpentries += struct.pack(" None: + self.fp.seek(0) # prep the fp in order to pass the JPEG test + JpegImagePlugin.JpegImageFile._open(self) + self._after_jpeg_open() + + def _after_jpeg_open(self, mpheader=None): + self.mpinfo = mpheader if mpheader is not None else self._getmp() + self.n_frames = self.mpinfo[0xB001] + self.__mpoffsets = [ + mpent["DataOffset"] + self.info["mpoffset"] for mpent in self.mpinfo[0xB002] + ] + self.__mpoffsets[0] = 0 + # Note that the following assertion will only be invalid if something + # gets broken within JpegImagePlugin. + assert self.n_frames == len(self.__mpoffsets) + del self.info["mpoffset"] # no longer needed + self.is_animated = self.n_frames > 1 + self._fp = self.fp # FIXME: hack + self._fp.seek(self.__mpoffsets[0]) # get ready to read first frame + self.__frame = 0 + self.offset = 0 + # for now we can only handle reading and individual frame extraction + self.readonly = 1 + + def load_seek(self, pos: int) -> None: + self._fp.seek(pos) + + def seek(self, frame: int) -> None: + if not self._seek_check(frame): + return + self.fp = self._fp + self.offset = self.__mpoffsets[frame] + + original_exif = self.info.get("exif") + if "exif" in self.info: + del self.info["exif"] + + self.fp.seek(self.offset + 2) # skip SOI marker + if not self.fp.read(2): + msg = "No data found for frame" + raise ValueError(msg) + self.fp.seek(self.offset) + JpegImagePlugin.JpegImageFile._open(self) + if self.info.get("exif") != original_exif: + self._reload_exif() + + self.tile = [("jpeg", (0, 0) + self.size, self.offset, self.tile[0][-1])] + self.__frame = frame + + def tell(self) -> int: + return self.__frame + + @staticmethod + def adopt(jpeg_instance, mpheader=None): + """ + Transform the instance of JpegImageFile into + an instance of MpoImageFile. + After the call, the JpegImageFile is extended + to be an MpoImageFile. + + This is essentially useful when opening a JPEG + file that reveals itself as an MPO, to avoid + double call to _open. + """ + jpeg_instance.__class__ = MpoImageFile + jpeg_instance._after_jpeg_open(mpheader) + return jpeg_instance + + +# --------------------------------------------------------------------- +# Registry stuff + +# Note that since MPO shares a factory with JPEG, we do not need to do a +# separate registration for it here. +# Image.register_open(MpoImageFile.format, +# JpegImagePlugin.jpeg_factory, _accept) +Image.register_save(MpoImageFile.format, _save) +Image.register_save_all(MpoImageFile.format, _save_all) + +Image.register_extension(MpoImageFile.format, ".mpo") + +Image.register_mime(MpoImageFile.format, "image/mpo") diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index 0a75c868b..fea52dca8 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -1,200 +1,200 @@ -# -# The Python Imaging Library. -# -# MSP file handling -# -# This is the format used by the Paint program in Windows 1 and 2. -# -# History: -# 95-09-05 fl Created -# 97-01-03 fl Read/write MSP images -# 17-02-21 es Fixed RLE interpretation -# -# Copyright (c) Secret Labs AB 1997. -# Copyright (c) Fredrik Lundh 1995-97. -# Copyright (c) Eric Soroos 2017. -# -# See the README file for information on usage and redistribution. -# -# More info on this format: https://archive.org/details/gg243631 -# Page 313: -# Figure 205. Windows Paint Version 1: "DanM" Format -# Figure 206. Windows Paint Version 2: "LinS" Format. Used in Windows V2.03 -# -# See also: https://www.fileformat.info/format/mspaint/egff.htm -from __future__ import annotations - -import io -import struct -from typing import IO - -from . import Image, ImageFile -from ._binary import i16le as i16 -from ._binary import o16le as o16 - -# -# read MSP files - - -def _accept(prefix: bytes) -> bool: - return prefix[:4] in [b"DanM", b"LinS"] - - -## -# Image plugin for Windows MSP images. This plugin supports both -# uncompressed (Windows 1.0). - - -class MspImageFile(ImageFile.ImageFile): - format = "MSP" - format_description = "Windows Paint" - - def _open(self) -> None: - # Header - assert self.fp is not None - - s = self.fp.read(32) - if not _accept(s): - msg = "not an MSP file" - raise SyntaxError(msg) - - # Header checksum - checksum = 0 - for i in range(0, 32, 2): - checksum = checksum ^ i16(s, i) - if checksum != 0: - msg = "bad MSP checksum" - raise SyntaxError(msg) - - self._mode = "1" - self._size = i16(s, 4), i16(s, 6) - - if s[:4] == b"DanM": - self.tile = [("raw", (0, 0) + self.size, 32, ("1", 0, 1))] - else: - self.tile = [("MSP", (0, 0) + self.size, 32, None)] - - -class MspDecoder(ImageFile.PyDecoder): - # The algo for the MSP decoder is from - # https://www.fileformat.info/format/mspaint/egff.htm - # cc-by-attribution -- That page references is taken from the - # Encyclopedia of Graphics File Formats and is licensed by - # O'Reilly under the Creative Common/Attribution license - # - # For RLE encoded files, the 32byte header is followed by a scan - # line map, encoded as one 16bit word of encoded byte length per - # line. - # - # NOTE: the encoded length of the line can be 0. This was not - # handled in the previous version of this encoder, and there's no - # mention of how to handle it in the documentation. From the few - # examples I've seen, I've assumed that it is a fill of the - # background color, in this case, white. - # - # - # Pseudocode of the decoder: - # Read a BYTE value as the RunType - # If the RunType value is zero - # Read next byte as the RunCount - # Read the next byte as the RunValue - # Write the RunValue byte RunCount times - # If the RunType value is non-zero - # Use this value as the RunCount - # Read and write the next RunCount bytes literally - # - # e.g.: - # 0x00 03 ff 05 00 01 02 03 04 - # would yield the bytes: - # 0xff ff ff 00 01 02 03 04 - # - # which are then interpreted as a bit packed mode '1' image - - _pulls_fd = True - - def decode(self, buffer: bytes) -> tuple[int, int]: - assert self.fd is not None - - img = io.BytesIO() - blank_line = bytearray((0xFF,) * ((self.state.xsize + 7) // 8)) - try: - self.fd.seek(32) - rowmap = struct.unpack_from( - f"<{self.state.ysize}H", self.fd.read(self.state.ysize * 2) - ) - except struct.error as e: - msg = "Truncated MSP file in row map" - raise OSError(msg) from e - - for x, rowlen in enumerate(rowmap): - try: - if rowlen == 0: - img.write(blank_line) - continue - row = self.fd.read(rowlen) - if len(row) != rowlen: - msg = f"Truncated MSP file, expected {rowlen} bytes on row {x}" - raise OSError(msg) - idx = 0 - while idx < rowlen: - runtype = row[idx] - idx += 1 - if runtype == 0: - (runcount, runval) = struct.unpack_from("Bc", row, idx) - img.write(runval * runcount) - idx += 2 - else: - runcount = runtype - img.write(row[idx : idx + runcount]) - idx += runcount - - except struct.error as e: - msg = f"Corrupted MSP file in row {x}" - raise OSError(msg) from e - - self.set_as_raw(img.getvalue(), ("1", 0, 1)) - - return -1, 0 - - -Image.register_decoder("MSP", MspDecoder) - - -# -# write MSP files (uncompressed only) - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - if im.mode != "1": - msg = f"cannot write mode {im.mode} as MSP" - raise OSError(msg) - - # create MSP header - header = [0] * 16 - - header[0], header[1] = i16(b"Da"), i16(b"nM") # version 1 - header[2], header[3] = im.size - header[4], header[5] = 1, 1 - header[6], header[7] = 1, 1 - header[8], header[9] = im.size - - checksum = 0 - for h in header: - checksum = checksum ^ h - header[12] = checksum # FIXME: is this the right field? - - # header - for h in header: - fp.write(o16(h)) - - # image body - ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 32, ("1", 0, 1))]) - - -# -# registry - -Image.register_open(MspImageFile.format, MspImageFile, _accept) -Image.register_save(MspImageFile.format, _save) - -Image.register_extension(MspImageFile.format, ".msp") +# +# The Python Imaging Library. +# +# MSP file handling +# +# This is the format used by the Paint program in Windows 1 and 2. +# +# History: +# 95-09-05 fl Created +# 97-01-03 fl Read/write MSP images +# 17-02-21 es Fixed RLE interpretation +# +# Copyright (c) Secret Labs AB 1997. +# Copyright (c) Fredrik Lundh 1995-97. +# Copyright (c) Eric Soroos 2017. +# +# See the README file for information on usage and redistribution. +# +# More info on this format: https://archive.org/details/gg243631 +# Page 313: +# Figure 205. Windows Paint Version 1: "DanM" Format +# Figure 206. Windows Paint Version 2: "LinS" Format. Used in Windows V2.03 +# +# See also: https://www.fileformat.info/format/mspaint/egff.htm +from __future__ import annotations + +import io +import struct +from typing import IO + +from . import Image, ImageFile +from ._binary import i16le as i16 +from ._binary import o16le as o16 + +# +# read MSP files + + +def _accept(prefix: bytes) -> bool: + return prefix[:4] in [b"DanM", b"LinS"] + + +## +# Image plugin for Windows MSP images. This plugin supports both +# uncompressed (Windows 1.0). + + +class MspImageFile(ImageFile.ImageFile): + format = "MSP" + format_description = "Windows Paint" + + def _open(self) -> None: + # Header + assert self.fp is not None + + s = self.fp.read(32) + if not _accept(s): + msg = "not an MSP file" + raise SyntaxError(msg) + + # Header checksum + checksum = 0 + for i in range(0, 32, 2): + checksum = checksum ^ i16(s, i) + if checksum != 0: + msg = "bad MSP checksum" + raise SyntaxError(msg) + + self._mode = "1" + self._size = i16(s, 4), i16(s, 6) + + if s[:4] == b"DanM": + self.tile = [("raw", (0, 0) + self.size, 32, ("1", 0, 1))] + else: + self.tile = [("MSP", (0, 0) + self.size, 32, None)] + + +class MspDecoder(ImageFile.PyDecoder): + # The algo for the MSP decoder is from + # https://www.fileformat.info/format/mspaint/egff.htm + # cc-by-attribution -- That page references is taken from the + # Encyclopedia of Graphics File Formats and is licensed by + # O'Reilly under the Creative Common/Attribution license + # + # For RLE encoded files, the 32byte header is followed by a scan + # line map, encoded as one 16bit word of encoded byte length per + # line. + # + # NOTE: the encoded length of the line can be 0. This was not + # handled in the previous version of this encoder, and there's no + # mention of how to handle it in the documentation. From the few + # examples I've seen, I've assumed that it is a fill of the + # background color, in this case, white. + # + # + # Pseudocode of the decoder: + # Read a BYTE value as the RunType + # If the RunType value is zero + # Read next byte as the RunCount + # Read the next byte as the RunValue + # Write the RunValue byte RunCount times + # If the RunType value is non-zero + # Use this value as the RunCount + # Read and write the next RunCount bytes literally + # + # e.g.: + # 0x00 03 ff 05 00 01 02 03 04 + # would yield the bytes: + # 0xff ff ff 00 01 02 03 04 + # + # which are then interpreted as a bit packed mode '1' image + + _pulls_fd = True + + def decode(self, buffer: bytes) -> tuple[int, int]: + assert self.fd is not None + + img = io.BytesIO() + blank_line = bytearray((0xFF,) * ((self.state.xsize + 7) // 8)) + try: + self.fd.seek(32) + rowmap = struct.unpack_from( + f"<{self.state.ysize}H", self.fd.read(self.state.ysize * 2) + ) + except struct.error as e: + msg = "Truncated MSP file in row map" + raise OSError(msg) from e + + for x, rowlen in enumerate(rowmap): + try: + if rowlen == 0: + img.write(blank_line) + continue + row = self.fd.read(rowlen) + if len(row) != rowlen: + msg = f"Truncated MSP file, expected {rowlen} bytes on row {x}" + raise OSError(msg) + idx = 0 + while idx < rowlen: + runtype = row[idx] + idx += 1 + if runtype == 0: + (runcount, runval) = struct.unpack_from("Bc", row, idx) + img.write(runval * runcount) + idx += 2 + else: + runcount = runtype + img.write(row[idx : idx + runcount]) + idx += runcount + + except struct.error as e: + msg = f"Corrupted MSP file in row {x}" + raise OSError(msg) from e + + self.set_as_raw(img.getvalue(), ("1", 0, 1)) + + return -1, 0 + + +Image.register_decoder("MSP", MspDecoder) + + +# +# write MSP files (uncompressed only) + + +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + if im.mode != "1": + msg = f"cannot write mode {im.mode} as MSP" + raise OSError(msg) + + # create MSP header + header = [0] * 16 + + header[0], header[1] = i16(b"Da"), i16(b"nM") # version 1 + header[2], header[3] = im.size + header[4], header[5] = 1, 1 + header[6], header[7] = 1, 1 + header[8], header[9] = im.size + + checksum = 0 + for h in header: + checksum = checksum ^ h + header[12] = checksum # FIXME: is this the right field? + + # header + for h in header: + fp.write(o16(h)) + + # image body + ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 32, ("1", 0, 1))]) + + +# +# registry + +Image.register_open(MspImageFile.format, MspImageFile, _accept) +Image.register_save(MspImageFile.format, _save) + +Image.register_extension(MspImageFile.format, ".msp") diff --git a/src/PIL/PSDraw.py b/src/PIL/PSDraw.py index 673eae1d1..3d3059d5e 100644 --- a/src/PIL/PSDraw.py +++ b/src/PIL/PSDraw.py @@ -1,237 +1,237 @@ -# -# The Python Imaging Library -# $Id$ -# -# Simple PostScript graphics interface -# -# History: -# 1996-04-20 fl Created -# 1999-01-10 fl Added gsave/grestore to image method -# 2005-05-04 fl Fixed floating point issue in image (from Eric Etheridge) -# -# Copyright (c) 1997-2005 by Secret Labs AB. All rights reserved. -# Copyright (c) 1996 by Fredrik Lundh. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import sys -from typing import TYPE_CHECKING - -from . import EpsImagePlugin - -## -# Simple PostScript graphics interface. - - -class PSDraw: - """ - Sets up printing to the given file. If ``fp`` is omitted, - ``sys.stdout.buffer`` or ``sys.stdout`` is assumed. - """ - - def __init__(self, fp=None): - if not fp: - try: - fp = sys.stdout.buffer - except AttributeError: - fp = sys.stdout - self.fp = fp - - def begin_document(self, id: str | None = None) -> None: - """Set up printing of a document. (Write PostScript DSC header.)""" - # FIXME: incomplete - self.fp.write( - b"%!PS-Adobe-3.0\n" - b"save\n" - b"/showpage { } def\n" - b"%%EndComments\n" - b"%%BeginDocument\n" - ) - # self.fp.write(ERROR_PS) # debugging! - self.fp.write(EDROFF_PS) - self.fp.write(VDI_PS) - self.fp.write(b"%%EndProlog\n") - self.isofont: dict[bytes, int] = {} - - def end_document(self) -> None: - """Ends printing. (Write PostScript DSC footer.)""" - self.fp.write(b"%%EndDocument\nrestore showpage\n%%End\n") - if hasattr(self.fp, "flush"): - self.fp.flush() - - def setfont(self, font: str, size: int) -> None: - """ - Selects which font to use. - - :param font: A PostScript font name - :param size: Size in points. - """ - font_bytes = bytes(font, "UTF-8") - if font_bytes not in self.isofont: - # reencode font - self.fp.write( - b"/PSDraw-%s ISOLatin1Encoding /%s E\n" % (font_bytes, font_bytes) - ) - self.isofont[font_bytes] = 1 - # rough - self.fp.write(b"/F0 %d /PSDraw-%s F\n" % (size, font_bytes)) - - def line(self, xy0: tuple[int, int], xy1: tuple[int, int]) -> None: - """ - Draws a line between the two points. Coordinates are given in - PostScript point coordinates (72 points per inch, (0, 0) is the lower - left corner of the page). - """ - self.fp.write(b"%d %d %d %d Vl\n" % (*xy0, *xy1)) - - def rectangle(self, box: tuple[int, int, int, int]) -> None: - """ - Draws a rectangle. - - :param box: A tuple of four integers, specifying left, bottom, width and - height. - """ - self.fp.write(b"%d %d M 0 %d %d Vr\n" % box) - - def text(self, xy: tuple[int, int], text: str) -> None: - """ - Draws text at the given position. You must use - :py:meth:`~PIL.PSDraw.PSDraw.setfont` before calling this method. - """ - text_bytes = bytes(text, "UTF-8") - text_bytes = b"\\(".join(text_bytes.split(b"(")) - text_bytes = b"\\)".join(text_bytes.split(b")")) - self.fp.write(b"%d %d M (%s) S\n" % (xy + (text_bytes,))) - - if TYPE_CHECKING: - from . import Image - - def image( - self, box: tuple[int, int, int, int], im: Image.Image, dpi: int | None = None - ) -> None: - """Draw a PIL image, centered in the given box.""" - # default resolution depends on mode - if not dpi: - if im.mode == "1": - dpi = 200 # fax - else: - dpi = 100 # grayscale - # image size (on paper) - x = im.size[0] * 72 / dpi - y = im.size[1] * 72 / dpi - # max allowed size - xmax = float(box[2] - box[0]) - ymax = float(box[3] - box[1]) - if x > xmax: - y = y * xmax / x - x = xmax - if y > ymax: - x = x * ymax / y - y = ymax - dx = (xmax - x) / 2 + box[0] - dy = (ymax - y) / 2 + box[1] - self.fp.write(b"gsave\n%f %f translate\n" % (dx, dy)) - if (x, y) != im.size: - # EpsImagePlugin._save prints the image at (0,0,xsize,ysize) - sx = x / im.size[0] - sy = y / im.size[1] - self.fp.write(b"%f %f scale\n" % (sx, sy)) - EpsImagePlugin._save(im, self.fp, "", 0) - self.fp.write(b"\ngrestore\n") - - -# -------------------------------------------------------------------- -# PostScript driver - -# -# EDROFF.PS -- PostScript driver for Edroff 2 -# -# History: -# 94-01-25 fl: created (edroff 2.04) -# -# Copyright (c) Fredrik Lundh 1994. -# - - -EDROFF_PS = b"""\ -/S { show } bind def -/P { moveto show } bind def -/M { moveto } bind def -/X { 0 rmoveto } bind def -/Y { 0 exch rmoveto } bind def -/E { findfont - dup maxlength dict begin - { - 1 index /FID ne { def } { pop pop } ifelse - } forall - /Encoding exch def - dup /FontName exch def - currentdict end definefont pop -} bind def -/F { findfont exch scalefont dup setfont - [ exch /setfont cvx ] cvx bind def -} bind def -""" - -# -# VDI.PS -- PostScript driver for VDI meta commands -# -# History: -# 94-01-25 fl: created (edroff 2.04) -# -# Copyright (c) Fredrik Lundh 1994. -# - -VDI_PS = b"""\ -/Vm { moveto } bind def -/Va { newpath arcn stroke } bind def -/Vl { moveto lineto stroke } bind def -/Vc { newpath 0 360 arc closepath } bind def -/Vr { exch dup 0 rlineto - exch dup 0 exch rlineto - exch neg 0 rlineto - 0 exch neg rlineto - setgray fill } bind def -/Tm matrix def -/Ve { Tm currentmatrix pop - translate scale newpath 0 0 .5 0 360 arc closepath - Tm setmatrix -} bind def -/Vf { currentgray exch setgray fill setgray } bind def -""" - -# -# ERROR.PS -- Error handler -# -# History: -# 89-11-21 fl: created (pslist 1.10) -# - -ERROR_PS = b"""\ -/landscape false def -/errorBUF 200 string def -/errorNL { currentpoint 10 sub exch pop 72 exch moveto } def -errordict begin /handleerror { - initmatrix /Courier findfont 10 scalefont setfont - newpath 72 720 moveto $error begin /newerror false def - (PostScript Error) show errorNL errorNL - (Error: ) show - /errorname load errorBUF cvs show errorNL errorNL - (Command: ) show - /command load dup type /stringtype ne { errorBUF cvs } if show - errorNL errorNL - (VMstatus: ) show - vmstatus errorBUF cvs show ( bytes available, ) show - errorBUF cvs show ( bytes used at level ) show - errorBUF cvs show errorNL errorNL - (Operand stargck: ) show errorNL /ostargck load { - dup type /stringtype ne { errorBUF cvs } if 72 0 rmoveto show errorNL - } forall errorNL - (Execution stargck: ) show errorNL /estargck load { - dup type /stringtype ne { errorBUF cvs } if 72 0 rmoveto show errorNL - } forall - end showpage -} def end -""" +# +# The Python Imaging Library +# $Id$ +# +# Simple PostScript graphics interface +# +# History: +# 1996-04-20 fl Created +# 1999-01-10 fl Added gsave/grestore to image method +# 2005-05-04 fl Fixed floating point issue in image (from Eric Etheridge) +# +# Copyright (c) 1997-2005 by Secret Labs AB. All rights reserved. +# Copyright (c) 1996 by Fredrik Lundh. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import sys +from typing import TYPE_CHECKING + +from . import EpsImagePlugin + +## +# Simple PostScript graphics interface. + + +class PSDraw: + """ + Sets up printing to the given file. If ``fp`` is omitted, + ``sys.stdout.buffer`` or ``sys.stdout`` is assumed. + """ + + def __init__(self, fp=None): + if not fp: + try: + fp = sys.stdout.buffer + except AttributeError: + fp = sys.stdout + self.fp = fp + + def begin_document(self, id: str | None = None) -> None: + """Set up printing of a document. (Write PostScript DSC header.)""" + # FIXME: incomplete + self.fp.write( + b"%!PS-Adobe-3.0\n" + b"save\n" + b"/showpage { } def\n" + b"%%EndComments\n" + b"%%BeginDocument\n" + ) + # self.fp.write(ERROR_PS) # debugging! + self.fp.write(EDROFF_PS) + self.fp.write(VDI_PS) + self.fp.write(b"%%EndProlog\n") + self.isofont: dict[bytes, int] = {} + + def end_document(self) -> None: + """Ends printing. (Write PostScript DSC footer.)""" + self.fp.write(b"%%EndDocument\nrestore showpage\n%%End\n") + if hasattr(self.fp, "flush"): + self.fp.flush() + + def setfont(self, font: str, size: int) -> None: + """ + Selects which font to use. + + :param font: A PostScript font name + :param size: Size in points. + """ + font_bytes = bytes(font, "UTF-8") + if font_bytes not in self.isofont: + # reencode font + self.fp.write( + b"/PSDraw-%s ISOLatin1Encoding /%s E\n" % (font_bytes, font_bytes) + ) + self.isofont[font_bytes] = 1 + # rough + self.fp.write(b"/F0 %d /PSDraw-%s F\n" % (size, font_bytes)) + + def line(self, xy0: tuple[int, int], xy1: tuple[int, int]) -> None: + """ + Draws a line between the two points. Coordinates are given in + PostScript point coordinates (72 points per inch, (0, 0) is the lower + left corner of the page). + """ + self.fp.write(b"%d %d %d %d Vl\n" % (*xy0, *xy1)) + + def rectangle(self, box: tuple[int, int, int, int]) -> None: + """ + Draws a rectangle. + + :param box: A tuple of four integers, specifying left, bottom, width and + height. + """ + self.fp.write(b"%d %d M 0 %d %d Vr\n" % box) + + def text(self, xy: tuple[int, int], text: str) -> None: + """ + Draws text at the given position. You must use + :py:meth:`~PIL.PSDraw.PSDraw.setfont` before calling this method. + """ + text_bytes = bytes(text, "UTF-8") + text_bytes = b"\\(".join(text_bytes.split(b"(")) + text_bytes = b"\\)".join(text_bytes.split(b")")) + self.fp.write(b"%d %d M (%s) S\n" % (xy + (text_bytes,))) + + if TYPE_CHECKING: + from . import Image + + def image( + self, box: tuple[int, int, int, int], im: Image.Image, dpi: int | None = None + ) -> None: + """Draw a PIL image, centered in the given box.""" + # default resolution depends on mode + if not dpi: + if im.mode == "1": + dpi = 200 # fax + else: + dpi = 100 # grayscale + # image size (on paper) + x = im.size[0] * 72 / dpi + y = im.size[1] * 72 / dpi + # max allowed size + xmax = float(box[2] - box[0]) + ymax = float(box[3] - box[1]) + if x > xmax: + y = y * xmax / x + x = xmax + if y > ymax: + x = x * ymax / y + y = ymax + dx = (xmax - x) / 2 + box[0] + dy = (ymax - y) / 2 + box[1] + self.fp.write(b"gsave\n%f %f translate\n" % (dx, dy)) + if (x, y) != im.size: + # EpsImagePlugin._save prints the image at (0,0,xsize,ysize) + sx = x / im.size[0] + sy = y / im.size[1] + self.fp.write(b"%f %f scale\n" % (sx, sy)) + EpsImagePlugin._save(im, self.fp, "", 0) + self.fp.write(b"\ngrestore\n") + + +# -------------------------------------------------------------------- +# PostScript driver + +# +# EDROFF.PS -- PostScript driver for Edroff 2 +# +# History: +# 94-01-25 fl: created (edroff 2.04) +# +# Copyright (c) Fredrik Lundh 1994. +# + + +EDROFF_PS = b"""\ +/S { show } bind def +/P { moveto show } bind def +/M { moveto } bind def +/X { 0 rmoveto } bind def +/Y { 0 exch rmoveto } bind def +/E { findfont + dup maxlength dict begin + { + 1 index /FID ne { def } { pop pop } ifelse + } forall + /Encoding exch def + dup /FontName exch def + currentdict end definefont pop +} bind def +/F { findfont exch scalefont dup setfont + [ exch /setfont cvx ] cvx bind def +} bind def +""" + +# +# VDI.PS -- PostScript driver for VDI meta commands +# +# History: +# 94-01-25 fl: created (edroff 2.04) +# +# Copyright (c) Fredrik Lundh 1994. +# + +VDI_PS = b"""\ +/Vm { moveto } bind def +/Va { newpath arcn stroke } bind def +/Vl { moveto lineto stroke } bind def +/Vc { newpath 0 360 arc closepath } bind def +/Vr { exch dup 0 rlineto + exch dup 0 exch rlineto + exch neg 0 rlineto + 0 exch neg rlineto + setgray fill } bind def +/Tm matrix def +/Ve { Tm currentmatrix pop + translate scale newpath 0 0 .5 0 360 arc closepath + Tm setmatrix +} bind def +/Vf { currentgray exch setgray fill setgray } bind def +""" + +# +# ERROR.PS -- Error handler +# +# History: +# 89-11-21 fl: created (pslist 1.10) +# + +ERROR_PS = b"""\ +/landscape false def +/errorBUF 200 string def +/errorNL { currentpoint 10 sub exch pop 72 exch moveto } def +errordict begin /handleerror { + initmatrix /Courier findfont 10 scalefont setfont + newpath 72 720 moveto $error begin /newerror false def + (PostScript Error) show errorNL errorNL + (Error: ) show + /errorname load errorBUF cvs show errorNL errorNL + (Command: ) show + /command load dup type /stringtype ne { errorBUF cvs } if show + errorNL errorNL + (VMstatus: ) show + vmstatus errorBUF cvs show ( bytes available, ) show + errorBUF cvs show ( bytes used at level ) show + errorBUF cvs show errorNL errorNL + (Operand stargck: ) show errorNL /ostargck load { + dup type /stringtype ne { errorBUF cvs } if 72 0 rmoveto show errorNL + } forall errorNL + (Execution stargck: ) show errorNL /estargck load { + dup type /stringtype ne { errorBUF cvs } if 72 0 rmoveto show errorNL + } forall + end showpage +} def end +""" diff --git a/src/PIL/PaletteFile.py b/src/PIL/PaletteFile.py index 81652e5ee..70b22e601 100644 --- a/src/PIL/PaletteFile.py +++ b/src/PIL/PaletteFile.py @@ -1,54 +1,54 @@ -# -# Python Imaging Library -# $Id$ -# -# stuff to read simple, teragon-style palette files -# -# History: -# 97-08-23 fl Created -# -# Copyright (c) Secret Labs AB 1997. -# Copyright (c) Fredrik Lundh 1997. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -from typing import IO - -from ._binary import o8 - - -class PaletteFile: - """File handler for Teragon-style palette files.""" - - rawmode = "RGB" - - def __init__(self, fp: IO[bytes]) -> None: - palette = [o8(i) * 3 for i in range(256)] - - while True: - s = fp.readline() - - if not s: - break - if s[:1] == b"#": - continue - if len(s) > 100: - msg = "bad palette file" - raise SyntaxError(msg) - - v = [int(x) for x in s.split()] - try: - [i, r, g, b] = v - except ValueError: - [i, r] = v - g = b = r - - if 0 <= i <= 255: - palette[i] = o8(r) + o8(g) + o8(b) - - self.palette = b"".join(palette) - - def getpalette(self) -> tuple[bytes, str]: - return self.palette, self.rawmode +# +# Python Imaging Library +# $Id$ +# +# stuff to read simple, teragon-style palette files +# +# History: +# 97-08-23 fl Created +# +# Copyright (c) Secret Labs AB 1997. +# Copyright (c) Fredrik Lundh 1997. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +from typing import IO + +from ._binary import o8 + + +class PaletteFile: + """File handler for Teragon-style palette files.""" + + rawmode = "RGB" + + def __init__(self, fp: IO[bytes]) -> None: + palette = [o8(i) * 3 for i in range(256)] + + while True: + s = fp.readline() + + if not s: + break + if s[:1] == b"#": + continue + if len(s) > 100: + msg = "bad palette file" + raise SyntaxError(msg) + + v = [int(x) for x in s.split()] + try: + [i, r, g, b] = v + except ValueError: + [i, r] = v + g = b = r + + if 0 <= i <= 255: + palette[i] = o8(r) + o8(g) + o8(b) + + self.palette = b"".join(palette) + + def getpalette(self) -> tuple[bytes, str]: + return self.palette, self.rawmode diff --git a/src/PIL/PalmImagePlugin.py b/src/PIL/PalmImagePlugin.py index fc83918b5..3c20d2eef 100644 --- a/src/PIL/PalmImagePlugin.py +++ b/src/PIL/PalmImagePlugin.py @@ -1,228 +1,228 @@ -# -# The Python Imaging Library. -# $Id$ -# - -## -# Image plugin for Palm pixmap images (output only). -## -from __future__ import annotations - -from typing import IO - -from . import Image, ImageFile -from ._binary import o8 -from ._binary import o16be as o16b - -# fmt: off -_Palm8BitColormapValues = ( - (255, 255, 255), (255, 204, 255), (255, 153, 255), (255, 102, 255), - (255, 51, 255), (255, 0, 255), (255, 255, 204), (255, 204, 204), - (255, 153, 204), (255, 102, 204), (255, 51, 204), (255, 0, 204), - (255, 255, 153), (255, 204, 153), (255, 153, 153), (255, 102, 153), - (255, 51, 153), (255, 0, 153), (204, 255, 255), (204, 204, 255), - (204, 153, 255), (204, 102, 255), (204, 51, 255), (204, 0, 255), - (204, 255, 204), (204, 204, 204), (204, 153, 204), (204, 102, 204), - (204, 51, 204), (204, 0, 204), (204, 255, 153), (204, 204, 153), - (204, 153, 153), (204, 102, 153), (204, 51, 153), (204, 0, 153), - (153, 255, 255), (153, 204, 255), (153, 153, 255), (153, 102, 255), - (153, 51, 255), (153, 0, 255), (153, 255, 204), (153, 204, 204), - (153, 153, 204), (153, 102, 204), (153, 51, 204), (153, 0, 204), - (153, 255, 153), (153, 204, 153), (153, 153, 153), (153, 102, 153), - (153, 51, 153), (153, 0, 153), (102, 255, 255), (102, 204, 255), - (102, 153, 255), (102, 102, 255), (102, 51, 255), (102, 0, 255), - (102, 255, 204), (102, 204, 204), (102, 153, 204), (102, 102, 204), - (102, 51, 204), (102, 0, 204), (102, 255, 153), (102, 204, 153), - (102, 153, 153), (102, 102, 153), (102, 51, 153), (102, 0, 153), - (51, 255, 255), (51, 204, 255), (51, 153, 255), (51, 102, 255), - (51, 51, 255), (51, 0, 255), (51, 255, 204), (51, 204, 204), - (51, 153, 204), (51, 102, 204), (51, 51, 204), (51, 0, 204), - (51, 255, 153), (51, 204, 153), (51, 153, 153), (51, 102, 153), - (51, 51, 153), (51, 0, 153), (0, 255, 255), (0, 204, 255), - (0, 153, 255), (0, 102, 255), (0, 51, 255), (0, 0, 255), - (0, 255, 204), (0, 204, 204), (0, 153, 204), (0, 102, 204), - (0, 51, 204), (0, 0, 204), (0, 255, 153), (0, 204, 153), - (0, 153, 153), (0, 102, 153), (0, 51, 153), (0, 0, 153), - (255, 255, 102), (255, 204, 102), (255, 153, 102), (255, 102, 102), - (255, 51, 102), (255, 0, 102), (255, 255, 51), (255, 204, 51), - (255, 153, 51), (255, 102, 51), (255, 51, 51), (255, 0, 51), - (255, 255, 0), (255, 204, 0), (255, 153, 0), (255, 102, 0), - (255, 51, 0), (255, 0, 0), (204, 255, 102), (204, 204, 102), - (204, 153, 102), (204, 102, 102), (204, 51, 102), (204, 0, 102), - (204, 255, 51), (204, 204, 51), (204, 153, 51), (204, 102, 51), - (204, 51, 51), (204, 0, 51), (204, 255, 0), (204, 204, 0), - (204, 153, 0), (204, 102, 0), (204, 51, 0), (204, 0, 0), - (153, 255, 102), (153, 204, 102), (153, 153, 102), (153, 102, 102), - (153, 51, 102), (153, 0, 102), (153, 255, 51), (153, 204, 51), - (153, 153, 51), (153, 102, 51), (153, 51, 51), (153, 0, 51), - (153, 255, 0), (153, 204, 0), (153, 153, 0), (153, 102, 0), - (153, 51, 0), (153, 0, 0), (102, 255, 102), (102, 204, 102), - (102, 153, 102), (102, 102, 102), (102, 51, 102), (102, 0, 102), - (102, 255, 51), (102, 204, 51), (102, 153, 51), (102, 102, 51), - (102, 51, 51), (102, 0, 51), (102, 255, 0), (102, 204, 0), - (102, 153, 0), (102, 102, 0), (102, 51, 0), (102, 0, 0), - (51, 255, 102), (51, 204, 102), (51, 153, 102), (51, 102, 102), - (51, 51, 102), (51, 0, 102), (51, 255, 51), (51, 204, 51), - (51, 153, 51), (51, 102, 51), (51, 51, 51), (51, 0, 51), - (51, 255, 0), (51, 204, 0), (51, 153, 0), (51, 102, 0), - (51, 51, 0), (51, 0, 0), (0, 255, 102), (0, 204, 102), - (0, 153, 102), (0, 102, 102), (0, 51, 102), (0, 0, 102), - (0, 255, 51), (0, 204, 51), (0, 153, 51), (0, 102, 51), - (0, 51, 51), (0, 0, 51), (0, 255, 0), (0, 204, 0), - (0, 153, 0), (0, 102, 0), (0, 51, 0), (17, 17, 17), - (34, 34, 34), (68, 68, 68), (85, 85, 85), (119, 119, 119), - (136, 136, 136), (170, 170, 170), (187, 187, 187), (221, 221, 221), - (238, 238, 238), (192, 192, 192), (128, 0, 0), (128, 0, 128), - (0, 128, 0), (0, 128, 128), (0, 0, 0), (0, 0, 0), - (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), - (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), - (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), - (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), - (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), - (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0)) -# fmt: on - - -# so build a prototype image to be used for palette resampling -def build_prototype_image() -> Image.Image: - image = Image.new("L", (1, len(_Palm8BitColormapValues))) - image.putdata(list(range(len(_Palm8BitColormapValues)))) - palettedata: tuple[int, ...] = () - for colormapValue in _Palm8BitColormapValues: - palettedata += colormapValue - palettedata += (0, 0, 0) * (256 - len(_Palm8BitColormapValues)) - image.putpalette(palettedata) - return image - - -Palm8BitColormapImage = build_prototype_image() - -# OK, we now have in Palm8BitColormapImage, -# a "P"-mode image with the right palette -# -# -------------------------------------------------------------------- - -_FLAGS = {"custom-colormap": 0x4000, "is-compressed": 0x8000, "has-transparent": 0x2000} - -_COMPRESSION_TYPES = {"none": 0xFF, "rle": 0x01, "scanline": 0x00} - - -# -# -------------------------------------------------------------------- - -## -# (Internal) Image save plugin for the Palm format. - - -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 - - elif im.mode == "L": - if im.encoderinfo.get("bpp") in (1, 2, 4): - # this is 8-bit grayscale, so we shift it to get the high-order bits, - # and invert it because - # Palm does grayscale from white (0) to black (1) - bpp = im.encoderinfo["bpp"] - im = im.point( - lambda x, shift=8 - bpp, maxval=(1 << bpp) - 1: maxval - (x >> shift) - ) - elif im.info.get("bpp") in (1, 2, 4): - # here we assume that even though the inherent mode is 8-bit grayscale, - # only the lower bpp bits are significant. - # We invert them to match the Palm. - bpp = im.info["bpp"] - im = im.point(lambda x, maxval=(1 << bpp) - 1: maxval - (x & maxval)) - else: - msg = f"cannot write mode {im.mode} as Palm" - raise OSError(msg) - - # we ignore the palette here - im._mode = "P" - rawmode = f"P;{bpp}" - version = 1 - - elif im.mode == "1": - # monochrome -- write it inverted, as is the Palm standard - rawmode = "1;I" - bpp = 1 - version = 0 - - else: - msg = f"cannot write mode {im.mode} as Palm" - raise OSError(msg) - - # - # make sure image data is available - im.load() - - # write header - - cols = im.size[0] - rows = im.size[1] - - rowbytes = int((cols + (16 // bpp - 1)) / (16 // bpp)) * 2 - transparent_index = 0 - compression_type = _COMPRESSION_TYPES["none"] - - flags = 0 - if im.mode == "P" and "custom-colormap" in im.info: - flags = flags & _FLAGS["custom-colormap"] - colormapsize = 4 * 256 + 2 - colormapmode = im.palette.mode - colormap = im.getdata().getpalette() - else: - colormapsize = 0 - - if "offset" in im.info: - offset = (rowbytes * rows + 16 + 3 + colormapsize) // 4 - else: - offset = 0 - - fp.write(o16b(cols) + o16b(rows) + o16b(rowbytes) + o16b(flags)) - fp.write(o8(bpp)) - fp.write(o8(version)) - fp.write(o16b(offset)) - fp.write(o8(transparent_index)) - fp.write(o8(compression_type)) - fp.write(o16b(0)) # reserved by Palm - - # now write colormap if necessary - - if colormapsize > 0: - fp.write(o16b(256)) - for i in range(256): - 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]) - ) - - # now convert data to raw form - ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, rowbytes, 1))]) - - if hasattr(fp, "flush"): - fp.flush() - - -# -# -------------------------------------------------------------------- - -Image.register_save("Palm", _save) - -Image.register_extension("Palm", ".palm") - -Image.register_mime("Palm", "image/palm") +# +# The Python Imaging Library. +# $Id$ +# + +## +# Image plugin for Palm pixmap images (output only). +## +from __future__ import annotations + +from typing import IO + +from . import Image, ImageFile +from ._binary import o8 +from ._binary import o16be as o16b + +# fmt: off +_Palm8BitColormapValues = ( + (255, 255, 255), (255, 204, 255), (255, 153, 255), (255, 102, 255), + (255, 51, 255), (255, 0, 255), (255, 255, 204), (255, 204, 204), + (255, 153, 204), (255, 102, 204), (255, 51, 204), (255, 0, 204), + (255, 255, 153), (255, 204, 153), (255, 153, 153), (255, 102, 153), + (255, 51, 153), (255, 0, 153), (204, 255, 255), (204, 204, 255), + (204, 153, 255), (204, 102, 255), (204, 51, 255), (204, 0, 255), + (204, 255, 204), (204, 204, 204), (204, 153, 204), (204, 102, 204), + (204, 51, 204), (204, 0, 204), (204, 255, 153), (204, 204, 153), + (204, 153, 153), (204, 102, 153), (204, 51, 153), (204, 0, 153), + (153, 255, 255), (153, 204, 255), (153, 153, 255), (153, 102, 255), + (153, 51, 255), (153, 0, 255), (153, 255, 204), (153, 204, 204), + (153, 153, 204), (153, 102, 204), (153, 51, 204), (153, 0, 204), + (153, 255, 153), (153, 204, 153), (153, 153, 153), (153, 102, 153), + (153, 51, 153), (153, 0, 153), (102, 255, 255), (102, 204, 255), + (102, 153, 255), (102, 102, 255), (102, 51, 255), (102, 0, 255), + (102, 255, 204), (102, 204, 204), (102, 153, 204), (102, 102, 204), + (102, 51, 204), (102, 0, 204), (102, 255, 153), (102, 204, 153), + (102, 153, 153), (102, 102, 153), (102, 51, 153), (102, 0, 153), + (51, 255, 255), (51, 204, 255), (51, 153, 255), (51, 102, 255), + (51, 51, 255), (51, 0, 255), (51, 255, 204), (51, 204, 204), + (51, 153, 204), (51, 102, 204), (51, 51, 204), (51, 0, 204), + (51, 255, 153), (51, 204, 153), (51, 153, 153), (51, 102, 153), + (51, 51, 153), (51, 0, 153), (0, 255, 255), (0, 204, 255), + (0, 153, 255), (0, 102, 255), (0, 51, 255), (0, 0, 255), + (0, 255, 204), (0, 204, 204), (0, 153, 204), (0, 102, 204), + (0, 51, 204), (0, 0, 204), (0, 255, 153), (0, 204, 153), + (0, 153, 153), (0, 102, 153), (0, 51, 153), (0, 0, 153), + (255, 255, 102), (255, 204, 102), (255, 153, 102), (255, 102, 102), + (255, 51, 102), (255, 0, 102), (255, 255, 51), (255, 204, 51), + (255, 153, 51), (255, 102, 51), (255, 51, 51), (255, 0, 51), + (255, 255, 0), (255, 204, 0), (255, 153, 0), (255, 102, 0), + (255, 51, 0), (255, 0, 0), (204, 255, 102), (204, 204, 102), + (204, 153, 102), (204, 102, 102), (204, 51, 102), (204, 0, 102), + (204, 255, 51), (204, 204, 51), (204, 153, 51), (204, 102, 51), + (204, 51, 51), (204, 0, 51), (204, 255, 0), (204, 204, 0), + (204, 153, 0), (204, 102, 0), (204, 51, 0), (204, 0, 0), + (153, 255, 102), (153, 204, 102), (153, 153, 102), (153, 102, 102), + (153, 51, 102), (153, 0, 102), (153, 255, 51), (153, 204, 51), + (153, 153, 51), (153, 102, 51), (153, 51, 51), (153, 0, 51), + (153, 255, 0), (153, 204, 0), (153, 153, 0), (153, 102, 0), + (153, 51, 0), (153, 0, 0), (102, 255, 102), (102, 204, 102), + (102, 153, 102), (102, 102, 102), (102, 51, 102), (102, 0, 102), + (102, 255, 51), (102, 204, 51), (102, 153, 51), (102, 102, 51), + (102, 51, 51), (102, 0, 51), (102, 255, 0), (102, 204, 0), + (102, 153, 0), (102, 102, 0), (102, 51, 0), (102, 0, 0), + (51, 255, 102), (51, 204, 102), (51, 153, 102), (51, 102, 102), + (51, 51, 102), (51, 0, 102), (51, 255, 51), (51, 204, 51), + (51, 153, 51), (51, 102, 51), (51, 51, 51), (51, 0, 51), + (51, 255, 0), (51, 204, 0), (51, 153, 0), (51, 102, 0), + (51, 51, 0), (51, 0, 0), (0, 255, 102), (0, 204, 102), + (0, 153, 102), (0, 102, 102), (0, 51, 102), (0, 0, 102), + (0, 255, 51), (0, 204, 51), (0, 153, 51), (0, 102, 51), + (0, 51, 51), (0, 0, 51), (0, 255, 0), (0, 204, 0), + (0, 153, 0), (0, 102, 0), (0, 51, 0), (17, 17, 17), + (34, 34, 34), (68, 68, 68), (85, 85, 85), (119, 119, 119), + (136, 136, 136), (170, 170, 170), (187, 187, 187), (221, 221, 221), + (238, 238, 238), (192, 192, 192), (128, 0, 0), (128, 0, 128), + (0, 128, 0), (0, 128, 128), (0, 0, 0), (0, 0, 0), + (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), + (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), + (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), + (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), + (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), + (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0)) +# fmt: on + + +# so build a prototype image to be used for palette resampling +def build_prototype_image() -> Image.Image: + image = Image.new("L", (1, len(_Palm8BitColormapValues))) + image.putdata(list(range(len(_Palm8BitColormapValues)))) + palettedata: tuple[int, ...] = () + for colormapValue in _Palm8BitColormapValues: + palettedata += colormapValue + palettedata += (0, 0, 0) * (256 - len(_Palm8BitColormapValues)) + image.putpalette(palettedata) + return image + + +Palm8BitColormapImage = build_prototype_image() + +# OK, we now have in Palm8BitColormapImage, +# a "P"-mode image with the right palette +# +# -------------------------------------------------------------------- + +_FLAGS = {"custom-colormap": 0x4000, "is-compressed": 0x8000, "has-transparent": 0x2000} + +_COMPRESSION_TYPES = {"none": 0xFF, "rle": 0x01, "scanline": 0x00} + + +# +# -------------------------------------------------------------------- + +## +# (Internal) Image save plugin for the Palm format. + + +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 + + elif im.mode == "L": + if im.encoderinfo.get("bpp") in (1, 2, 4): + # this is 8-bit grayscale, so we shift it to get the high-order bits, + # and invert it because + # Palm does grayscale from white (0) to black (1) + bpp = im.encoderinfo["bpp"] + im = im.point( + lambda x, shift=8 - bpp, maxval=(1 << bpp) - 1: maxval - (x >> shift) + ) + elif im.info.get("bpp") in (1, 2, 4): + # here we assume that even though the inherent mode is 8-bit grayscale, + # only the lower bpp bits are significant. + # We invert them to match the Palm. + bpp = im.info["bpp"] + im = im.point(lambda x, maxval=(1 << bpp) - 1: maxval - (x & maxval)) + else: + msg = f"cannot write mode {im.mode} as Palm" + raise OSError(msg) + + # we ignore the palette here + im._mode = "P" + rawmode = f"P;{bpp}" + version = 1 + + elif im.mode == "1": + # monochrome -- write it inverted, as is the Palm standard + rawmode = "1;I" + bpp = 1 + version = 0 + + else: + msg = f"cannot write mode {im.mode} as Palm" + raise OSError(msg) + + # + # make sure image data is available + im.load() + + # write header + + cols = im.size[0] + rows = im.size[1] + + rowbytes = int((cols + (16 // bpp - 1)) / (16 // bpp)) * 2 + transparent_index = 0 + compression_type = _COMPRESSION_TYPES["none"] + + flags = 0 + if im.mode == "P" and "custom-colormap" in im.info: + flags = flags & _FLAGS["custom-colormap"] + colormapsize = 4 * 256 + 2 + colormapmode = im.palette.mode + colormap = im.getdata().getpalette() + else: + colormapsize = 0 + + if "offset" in im.info: + offset = (rowbytes * rows + 16 + 3 + colormapsize) // 4 + else: + offset = 0 + + fp.write(o16b(cols) + o16b(rows) + o16b(rowbytes) + o16b(flags)) + fp.write(o8(bpp)) + fp.write(o8(version)) + fp.write(o16b(offset)) + fp.write(o8(transparent_index)) + fp.write(o8(compression_type)) + fp.write(o16b(0)) # reserved by Palm + + # now write colormap if necessary + + if colormapsize > 0: + fp.write(o16b(256)) + for i in range(256): + 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]) + ) + + # now convert data to raw form + ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, rowbytes, 1))]) + + if hasattr(fp, "flush"): + fp.flush() + + +# +# -------------------------------------------------------------------- + +Image.register_save("Palm", _save) + +Image.register_extension("Palm", ".palm") + +Image.register_mime("Palm", "image/palm") diff --git a/src/PIL/PcdImagePlugin.py b/src/PIL/PcdImagePlugin.py index 1cd5c4a9d..421d1871e 100644 --- a/src/PIL/PcdImagePlugin.py +++ b/src/PIL/PcdImagePlugin.py @@ -1,66 +1,66 @@ -# -# The Python Imaging Library. -# $Id$ -# -# PCD file handling -# -# History: -# 96-05-10 fl Created -# 96-05-27 fl Added draft mode (128x192, 256x384) -# -# Copyright (c) Secret Labs AB 1997. -# Copyright (c) Fredrik Lundh 1996. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -from . import Image, ImageFile - -## -# Image plugin for PhotoCD images. This plugin only reads the 768x512 -# image from the file; higher resolutions are encoded in a proprietary -# encoding. - - -class PcdImageFile(ImageFile.ImageFile): - format = "PCD" - format_description = "Kodak PhotoCD" - - def _open(self) -> None: - # rough - assert self.fp is not None - - self.fp.seek(2048) - s = self.fp.read(2048) - - if s[:4] != b"PCD_": - msg = "not a PCD file" - raise SyntaxError(msg) - - orientation = s[1538] & 3 - self.tile_post_rotate = None - if orientation == 1: - self.tile_post_rotate = 90 - elif orientation == 3: - self.tile_post_rotate = -90 - - self._mode = "RGB" - self._size = 768, 512 # FIXME: not correct for rotated images! - self.tile = [("pcd", (0, 0) + self.size, 96 * 2048, None)] - - def load_end(self) -> None: - if self.tile_post_rotate: - # Handle rotated PCDs - assert self.im is not None - - self.im = self.im.rotate(self.tile_post_rotate) - self._size = self.im.size - - -# -# registry - -Image.register_open(PcdImageFile.format, PcdImageFile) - -Image.register_extension(PcdImageFile.format, ".pcd") +# +# The Python Imaging Library. +# $Id$ +# +# PCD file handling +# +# History: +# 96-05-10 fl Created +# 96-05-27 fl Added draft mode (128x192, 256x384) +# +# Copyright (c) Secret Labs AB 1997. +# Copyright (c) Fredrik Lundh 1996. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +from . import Image, ImageFile + +## +# Image plugin for PhotoCD images. This plugin only reads the 768x512 +# image from the file; higher resolutions are encoded in a proprietary +# encoding. + + +class PcdImageFile(ImageFile.ImageFile): + format = "PCD" + format_description = "Kodak PhotoCD" + + def _open(self) -> None: + # rough + assert self.fp is not None + + self.fp.seek(2048) + s = self.fp.read(2048) + + if s[:4] != b"PCD_": + msg = "not a PCD file" + raise SyntaxError(msg) + + orientation = s[1538] & 3 + self.tile_post_rotate = None + if orientation == 1: + self.tile_post_rotate = 90 + elif orientation == 3: + self.tile_post_rotate = -90 + + self._mode = "RGB" + self._size = 768, 512 # FIXME: not correct for rotated images! + self.tile = [("pcd", (0, 0) + self.size, 96 * 2048, None)] + + def load_end(self) -> None: + if self.tile_post_rotate: + # Handle rotated PCDs + assert self.im is not None + + self.im = self.im.rotate(self.tile_post_rotate) + self._size = self.im.size + + +# +# registry + +Image.register_open(PcdImageFile.format, PcdImageFile) + +Image.register_extension(PcdImageFile.format, ".pcd") diff --git a/src/PIL/PcfFontFile.py b/src/PIL/PcfFontFile.py index 0d1968b14..24b3f26ca 100644 --- a/src/PIL/PcfFontFile.py +++ b/src/PIL/PcfFontFile.py @@ -1,254 +1,254 @@ -# -# THIS IS WORK IN PROGRESS -# -# The Python Imaging Library -# $Id$ -# -# portable compiled font file parser -# -# history: -# 1997-08-19 fl created -# 2003-09-13 fl fixed loading of unicode fonts -# -# Copyright (c) 1997-2003 by Secret Labs AB. -# Copyright (c) 1997-2003 by Fredrik Lundh. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import io -from typing import BinaryIO, Callable - -from . import FontFile, Image -from ._binary import i8 -from ._binary import i16be as b16 -from ._binary import i16le as l16 -from ._binary import i32be as b32 -from ._binary import i32le as l32 - -# -------------------------------------------------------------------- -# declarations - -PCF_MAGIC = 0x70636601 # "\x01fcp" - -PCF_PROPERTIES = 1 << 0 -PCF_ACCELERATORS = 1 << 1 -PCF_METRICS = 1 << 2 -PCF_BITMAPS = 1 << 3 -PCF_INK_METRICS = 1 << 4 -PCF_BDF_ENCODINGS = 1 << 5 -PCF_SWIDTHS = 1 << 6 -PCF_GLYPH_NAMES = 1 << 7 -PCF_BDF_ACCELERATORS = 1 << 8 - -BYTES_PER_ROW: list[Callable[[int], int]] = [ - lambda bits: ((bits + 7) >> 3), - lambda bits: ((bits + 15) >> 3) & ~1, - lambda bits: ((bits + 31) >> 3) & ~3, - lambda bits: ((bits + 63) >> 3) & ~7, -] - - -def sz(s: bytes, o: int) -> bytes: - return s[o : s.index(b"\0", o)] - - -class PcfFontFile(FontFile.FontFile): - """Font file plugin for the X11 PCF format.""" - - name = "name" - - def __init__(self, fp: BinaryIO, charset_encoding: str = "iso8859-1"): - self.charset_encoding = charset_encoding - - magic = l32(fp.read(4)) - if magic != PCF_MAGIC: - msg = "not a PCF file" - raise SyntaxError(msg) - - super().__init__() - - count = l32(fp.read(4)) - self.toc = {} - for i in range(count): - type = l32(fp.read(4)) - self.toc[type] = l32(fp.read(4)), l32(fp.read(4)), l32(fp.read(4)) - - self.fp = fp - - self.info = self._load_properties() - - metrics = self._load_metrics() - bitmaps = self._load_bitmaps(metrics) - encoding = self._load_encoding() - - # - # create glyph structure - - for ch, ix in enumerate(encoding): - if ix is not None: - ( - xsize, - ysize, - left, - right, - width, - ascent, - descent, - attributes, - ) = metrics[ix] - self.glyph[ch] = ( - (width, 0), - (left, descent - ysize, xsize + left, descent), - (0, 0, xsize, ysize), - bitmaps[ix], - ) - - def _getformat( - self, tag: int - ) -> tuple[BinaryIO, int, Callable[[bytes], int], Callable[[bytes], int]]: - format, size, offset = self.toc[tag] - - fp = self.fp - fp.seek(offset) - - format = l32(fp.read(4)) - - if format & 4: - i16, i32 = b16, b32 - else: - i16, i32 = l16, l32 - - return fp, format, i16, i32 - - def _load_properties(self) -> dict[bytes, bytes | int]: - # - # font properties - - properties = {} - - fp, format, i16, i32 = self._getformat(PCF_PROPERTIES) - - nprops = i32(fp.read(4)) - - # read property description - p = [(i32(fp.read(4)), i8(fp.read(1)), i32(fp.read(4))) for _ in range(nprops)] - - if nprops & 3: - fp.seek(4 - (nprops & 3), io.SEEK_CUR) # pad - - data = fp.read(i32(fp.read(4))) - - for k, s, v in p: - property_value: bytes | int = sz(data, v) if s else v - properties[sz(data, k)] = property_value - - return properties - - def _load_metrics(self) -> list[tuple[int, int, int, int, int, int, int, int]]: - # - # font metrics - - metrics: list[tuple[int, int, int, int, int, int, int, int]] = [] - - fp, format, i16, i32 = self._getformat(PCF_METRICS) - - append = metrics.append - - if (format & 0xFF00) == 0x100: - # "compressed" metrics - for i in range(i16(fp.read(2))): - left = i8(fp.read(1)) - 128 - right = i8(fp.read(1)) - 128 - width = i8(fp.read(1)) - 128 - ascent = i8(fp.read(1)) - 128 - descent = i8(fp.read(1)) - 128 - xsize = right - left - ysize = ascent + descent - append((xsize, ysize, left, right, width, ascent, descent, 0)) - - else: - # "jumbo" metrics - for i in range(i32(fp.read(4))): - left = i16(fp.read(2)) - right = i16(fp.read(2)) - width = i16(fp.read(2)) - ascent = i16(fp.read(2)) - descent = i16(fp.read(2)) - attributes = i16(fp.read(2)) - xsize = right - left - ysize = ascent + descent - append((xsize, ysize, left, right, width, ascent, descent, attributes)) - - return metrics - - def _load_bitmaps( - self, metrics: list[tuple[int, int, int, int, int, int, int, int]] - ) -> list[Image.Image]: - # - # bitmap data - - fp, format, i16, i32 = self._getformat(PCF_BITMAPS) - - nbitmaps = i32(fp.read(4)) - - if nbitmaps != len(metrics): - msg = "Wrong number of bitmaps" - raise OSError(msg) - - offsets = [i32(fp.read(4)) for _ in range(nbitmaps)] - - bitmap_sizes = [i32(fp.read(4)) for _ in range(4)] - - # byteorder = format & 4 # non-zero => MSB - bitorder = format & 8 # non-zero => MSB - padindex = format & 3 - - bitmapsize = bitmap_sizes[padindex] - offsets.append(bitmapsize) - - data = fp.read(bitmapsize) - - pad = BYTES_PER_ROW[padindex] - mode = "1;R" - if bitorder: - mode = "1" - - bitmaps = [] - for i in range(nbitmaps): - xsize, ysize = metrics[i][:2] - b, e = offsets[i : i + 2] - bitmaps.append( - Image.frombytes("1", (xsize, ysize), data[b:e], "raw", mode, pad(xsize)) - ) - - return bitmaps - - def _load_encoding(self) -> list[int | None]: - fp, format, i16, i32 = self._getformat(PCF_BDF_ENCODINGS) - - first_col, last_col = i16(fp.read(2)), i16(fp.read(2)) - first_row, last_row = i16(fp.read(2)), i16(fp.read(2)) - - i16(fp.read(2)) # default - - nencoding = (last_col - first_col + 1) * (last_row - first_row + 1) - - # map character code to bitmap index - encoding: list[int | None] = [None] * min(256, nencoding) - - encoding_offsets = [i16(fp.read(2)) for _ in range(nencoding)] - - for i in range(first_col, len(encoding)): - try: - encoding_offset = encoding_offsets[ - ord(bytearray([i]).decode(self.charset_encoding)) - ] - if encoding_offset != 0xFFFF: - encoding[i] = encoding_offset - except UnicodeDecodeError: - # character is not supported in selected encoding - pass - - return encoding +# +# THIS IS WORK IN PROGRESS +# +# The Python Imaging Library +# $Id$ +# +# portable compiled font file parser +# +# history: +# 1997-08-19 fl created +# 2003-09-13 fl fixed loading of unicode fonts +# +# Copyright (c) 1997-2003 by Secret Labs AB. +# Copyright (c) 1997-2003 by Fredrik Lundh. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import io +from typing import BinaryIO, Callable + +from . import FontFile, Image +from ._binary import i8 +from ._binary import i16be as b16 +from ._binary import i16le as l16 +from ._binary import i32be as b32 +from ._binary import i32le as l32 + +# -------------------------------------------------------------------- +# declarations + +PCF_MAGIC = 0x70636601 # "\x01fcp" + +PCF_PROPERTIES = 1 << 0 +PCF_ACCELERATORS = 1 << 1 +PCF_METRICS = 1 << 2 +PCF_BITMAPS = 1 << 3 +PCF_INK_METRICS = 1 << 4 +PCF_BDF_ENCODINGS = 1 << 5 +PCF_SWIDTHS = 1 << 6 +PCF_GLYPH_NAMES = 1 << 7 +PCF_BDF_ACCELERATORS = 1 << 8 + +BYTES_PER_ROW: list[Callable[[int], int]] = [ + lambda bits: ((bits + 7) >> 3), + lambda bits: ((bits + 15) >> 3) & ~1, + lambda bits: ((bits + 31) >> 3) & ~3, + lambda bits: ((bits + 63) >> 3) & ~7, +] + + +def sz(s: bytes, o: int) -> bytes: + return s[o : s.index(b"\0", o)] + + +class PcfFontFile(FontFile.FontFile): + """Font file plugin for the X11 PCF format.""" + + name = "name" + + def __init__(self, fp: BinaryIO, charset_encoding: str = "iso8859-1"): + self.charset_encoding = charset_encoding + + magic = l32(fp.read(4)) + if magic != PCF_MAGIC: + msg = "not a PCF file" + raise SyntaxError(msg) + + super().__init__() + + count = l32(fp.read(4)) + self.toc = {} + for i in range(count): + type = l32(fp.read(4)) + self.toc[type] = l32(fp.read(4)), l32(fp.read(4)), l32(fp.read(4)) + + self.fp = fp + + self.info = self._load_properties() + + metrics = self._load_metrics() + bitmaps = self._load_bitmaps(metrics) + encoding = self._load_encoding() + + # + # create glyph structure + + for ch, ix in enumerate(encoding): + if ix is not None: + ( + xsize, + ysize, + left, + right, + width, + ascent, + descent, + attributes, + ) = metrics[ix] + self.glyph[ch] = ( + (width, 0), + (left, descent - ysize, xsize + left, descent), + (0, 0, xsize, ysize), + bitmaps[ix], + ) + + def _getformat( + self, tag: int + ) -> tuple[BinaryIO, int, Callable[[bytes], int], Callable[[bytes], int]]: + format, size, offset = self.toc[tag] + + fp = self.fp + fp.seek(offset) + + format = l32(fp.read(4)) + + if format & 4: + i16, i32 = b16, b32 + else: + i16, i32 = l16, l32 + + return fp, format, i16, i32 + + def _load_properties(self) -> dict[bytes, bytes | int]: + # + # font properties + + properties = {} + + fp, format, i16, i32 = self._getformat(PCF_PROPERTIES) + + nprops = i32(fp.read(4)) + + # read property description + p = [(i32(fp.read(4)), i8(fp.read(1)), i32(fp.read(4))) for _ in range(nprops)] + + if nprops & 3: + fp.seek(4 - (nprops & 3), io.SEEK_CUR) # pad + + data = fp.read(i32(fp.read(4))) + + for k, s, v in p: + property_value: bytes | int = sz(data, v) if s else v + properties[sz(data, k)] = property_value + + return properties + + def _load_metrics(self) -> list[tuple[int, int, int, int, int, int, int, int]]: + # + # font metrics + + metrics: list[tuple[int, int, int, int, int, int, int, int]] = [] + + fp, format, i16, i32 = self._getformat(PCF_METRICS) + + append = metrics.append + + if (format & 0xFF00) == 0x100: + # "compressed" metrics + for i in range(i16(fp.read(2))): + left = i8(fp.read(1)) - 128 + right = i8(fp.read(1)) - 128 + width = i8(fp.read(1)) - 128 + ascent = i8(fp.read(1)) - 128 + descent = i8(fp.read(1)) - 128 + xsize = right - left + ysize = ascent + descent + append((xsize, ysize, left, right, width, ascent, descent, 0)) + + else: + # "jumbo" metrics + for i in range(i32(fp.read(4))): + left = i16(fp.read(2)) + right = i16(fp.read(2)) + width = i16(fp.read(2)) + ascent = i16(fp.read(2)) + descent = i16(fp.read(2)) + attributes = i16(fp.read(2)) + xsize = right - left + ysize = ascent + descent + append((xsize, ysize, left, right, width, ascent, descent, attributes)) + + return metrics + + def _load_bitmaps( + self, metrics: list[tuple[int, int, int, int, int, int, int, int]] + ) -> list[Image.Image]: + # + # bitmap data + + fp, format, i16, i32 = self._getformat(PCF_BITMAPS) + + nbitmaps = i32(fp.read(4)) + + if nbitmaps != len(metrics): + msg = "Wrong number of bitmaps" + raise OSError(msg) + + offsets = [i32(fp.read(4)) for _ in range(nbitmaps)] + + bitmap_sizes = [i32(fp.read(4)) for _ in range(4)] + + # byteorder = format & 4 # non-zero => MSB + bitorder = format & 8 # non-zero => MSB + padindex = format & 3 + + bitmapsize = bitmap_sizes[padindex] + offsets.append(bitmapsize) + + data = fp.read(bitmapsize) + + pad = BYTES_PER_ROW[padindex] + mode = "1;R" + if bitorder: + mode = "1" + + bitmaps = [] + for i in range(nbitmaps): + xsize, ysize = metrics[i][:2] + b, e = offsets[i : i + 2] + bitmaps.append( + Image.frombytes("1", (xsize, ysize), data[b:e], "raw", mode, pad(xsize)) + ) + + return bitmaps + + def _load_encoding(self) -> list[int | None]: + fp, format, i16, i32 = self._getformat(PCF_BDF_ENCODINGS) + + first_col, last_col = i16(fp.read(2)), i16(fp.read(2)) + first_row, last_row = i16(fp.read(2)), i16(fp.read(2)) + + i16(fp.read(2)) # default + + nencoding = (last_col - first_col + 1) * (last_row - first_row + 1) + + # map character code to bitmap index + encoding: list[int | None] = [None] * min(256, nencoding) + + encoding_offsets = [i16(fp.read(2)) for _ in range(nencoding)] + + for i in range(first_col, len(encoding)): + try: + encoding_offset = encoding_offsets[ + ord(bytearray([i]).decode(self.charset_encoding)) + ] + if encoding_offset != 0xFFFF: + encoding[i] = encoding_offset + except UnicodeDecodeError: + # character is not supported in selected encoding + pass + + return encoding diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index dd42003b5..f5d121e0d 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -1,227 +1,227 @@ -# -# The Python Imaging Library. -# $Id$ -# -# PCX file handling -# -# This format was originally used by ZSoft's popular PaintBrush -# program for the IBM PC. It is also supported by many MS-DOS and -# Windows applications, including the Windows PaintBrush program in -# Windows 3. -# -# history: -# 1995-09-01 fl Created -# 1996-05-20 fl Fixed RGB support -# 1997-01-03 fl Fixed 2-bit and 4-bit support -# 1999-02-03 fl Fixed 8-bit support (broken in 1.0b1) -# 1999-02-07 fl Added write support -# 2002-06-09 fl Made 2-bit and 4-bit support a bit more robust -# 2002-07-30 fl Seek from to current position, not beginning of file -# 2003-06-03 fl Extract DPI settings (info["dpi"]) -# -# 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. -# -from __future__ import annotations - -import io -import logging -from typing import IO - -from . import Image, ImageFile, ImagePalette -from ._binary import i16le as i16 -from ._binary import o8 -from ._binary import o16le as o16 - -logger = logging.getLogger(__name__) - - -def _accept(prefix: bytes) -> bool: - return prefix[0] == 10 and prefix[1] in [0, 2, 3, 5] - - -## -# Image plugin for Paintbrush images. - - -class PcxImageFile(ImageFile.ImageFile): - format = "PCX" - format_description = "Paintbrush" - - def _open(self) -> None: - # header - assert self.fp is not None - - s = self.fp.read(128) - if not _accept(s): - msg = "not a PCX file" - raise SyntaxError(msg) - - # image - bbox = i16(s, 4), i16(s, 6), i16(s, 8) + 1, i16(s, 10) + 1 - if bbox[2] <= bbox[0] or bbox[3] <= bbox[1]: - msg = "bad PCX image size" - raise SyntaxError(msg) - logger.debug("BBox: %s %s %s %s", *bbox) - - # format - version = s[1] - bits = s[3] - planes = s[65] - provided_stride = i16(s, 66) - logger.debug( - "PCX version %s, bits %s, planes %s, stride %s", - version, - bits, - planes, - provided_stride, - ) - - self.info["dpi"] = i16(s, 12), i16(s, 14) - - if bits == 1 and planes == 1: - mode = rawmode = "1" - - elif bits == 1 and planes in (2, 4): - mode = "P" - rawmode = "P;%dL" % planes - self.palette = ImagePalette.raw("RGB", s[16:64]) - - elif version == 5 and bits == 8 and planes == 1: - mode = rawmode = "L" - # FIXME: hey, this doesn't work with the incremental loader !!! - self.fp.seek(-769, io.SEEK_END) - s = self.fp.read(769) - if len(s) == 769 and s[0] == 12: - # check if the palette is linear grayscale - for i in range(256): - if s[i * 3 + 1 : i * 3 + 4] != o8(i) * 3: - mode = rawmode = "P" - 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" - rawmode = "RGB;L" - - else: - msg = "unknown PCX mode" - raise OSError(msg) - - self._mode = mode - self._size = bbox[2] - bbox[0], bbox[3] - bbox[1] - - # Don't trust the passed in stride. - # Calculate the approximate position for ourselves. - # CVE-2020-35653 - stride = (self._size[0] * bits + 7) // 8 - - # While the specification states that this must be even, - # not all images follow this - if provided_stride != stride: - stride += stride % 2 - - bbox = (0, 0) + self.size - logger.debug("size: %sx%s", *self.size) - - self.tile = [("pcx", bbox, self.fp.tell(), (rawmode, planes * stride))] - - -# -------------------------------------------------------------------- -# save PCX files - - -SAVE = { - # mode: (version, bits, planes, raw mode) - "1": (2, 1, 1, "1"), - "L": (5, 8, 1, "L"), - "P": (5, 8, 1, "P"), - "RGB": (5, 8, 3, "RGB;L"), -} - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - try: - version, bits, planes, rawmode = SAVE[im.mode] - except KeyError as e: - msg = f"Cannot save {im.mode} images as PCX" - raise ValueError(msg) from e - - # bytes per plane - stride = (im.size[0] * bits + 7) // 8 - # stride should be even - stride += stride % 2 - # Stride needs to be kept in sync with the PcxEncode.c version. - # Ideally it should be passed in in the state, but the bytes value - # gets overwritten. - - logger.debug( - "PcxImagePlugin._save: xwidth: %d, bits: %d, stride: %d", - im.size[0], - bits, - stride, - ) - - # under windows, we could determine the current screen size with - # "Image.core.display_mode()[1]", but I think that's overkill... - - screen = im.size - - dpi = 100, 100 - - # PCX header - fp.write( - o8(10) - + o8(version) - + o8(1) - + o8(bits) - + o16(0) - + o16(0) - + o16(im.size[0] - 1) - + o16(im.size[1] - 1) - + o16(dpi[0]) - + o16(dpi[1]) - + b"\0" * 24 - + b"\xFF" * 24 - + b"\0" - + o8(planes) - + o16(stride) - + o16(1) - + o16(screen[0]) - + o16(screen[1]) - + b"\0" * 54 - ) - - assert fp.tell() == 128 - - ImageFile._save(im, fp, [("pcx", (0, 0) + im.size, 0, (rawmode, bits * planes))]) - - if im.mode == "P": - # colour palette - assert im.im is not None - - fp.write(o8(12)) - palette = im.im.getpalette("RGB", "RGB") - palette += b"\x00" * (768 - len(palette)) - fp.write(palette) # 768 bytes - elif im.mode == "L": - # grayscale palette - fp.write(o8(12)) - for i in range(256): - fp.write(o8(i) * 3) - - -# -------------------------------------------------------------------- -# registry - - -Image.register_open(PcxImageFile.format, PcxImageFile, _accept) -Image.register_save(PcxImageFile.format, _save) - -Image.register_extension(PcxImageFile.format, ".pcx") - -Image.register_mime(PcxImageFile.format, "image/x-pcx") +# +# The Python Imaging Library. +# $Id$ +# +# PCX file handling +# +# This format was originally used by ZSoft's popular PaintBrush +# program for the IBM PC. It is also supported by many MS-DOS and +# Windows applications, including the Windows PaintBrush program in +# Windows 3. +# +# history: +# 1995-09-01 fl Created +# 1996-05-20 fl Fixed RGB support +# 1997-01-03 fl Fixed 2-bit and 4-bit support +# 1999-02-03 fl Fixed 8-bit support (broken in 1.0b1) +# 1999-02-07 fl Added write support +# 2002-06-09 fl Made 2-bit and 4-bit support a bit more robust +# 2002-07-30 fl Seek from to current position, not beginning of file +# 2003-06-03 fl Extract DPI settings (info["dpi"]) +# +# 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. +# +from __future__ import annotations + +import io +import logging +from typing import IO + +from . import Image, ImageFile, ImagePalette +from ._binary import i16le as i16 +from ._binary import o8 +from ._binary import o16le as o16 + +logger = logging.getLogger(__name__) + + +def _accept(prefix: bytes) -> bool: + return prefix[0] == 10 and prefix[1] in [0, 2, 3, 5] + + +## +# Image plugin for Paintbrush images. + + +class PcxImageFile(ImageFile.ImageFile): + format = "PCX" + format_description = "Paintbrush" + + def _open(self) -> None: + # header + assert self.fp is not None + + s = self.fp.read(128) + if not _accept(s): + msg = "not a PCX file" + raise SyntaxError(msg) + + # image + bbox = i16(s, 4), i16(s, 6), i16(s, 8) + 1, i16(s, 10) + 1 + if bbox[2] <= bbox[0] or bbox[3] <= bbox[1]: + msg = "bad PCX image size" + raise SyntaxError(msg) + logger.debug("BBox: %s %s %s %s", *bbox) + + # format + version = s[1] + bits = s[3] + planes = s[65] + provided_stride = i16(s, 66) + logger.debug( + "PCX version %s, bits %s, planes %s, stride %s", + version, + bits, + planes, + provided_stride, + ) + + self.info["dpi"] = i16(s, 12), i16(s, 14) + + if bits == 1 and planes == 1: + mode = rawmode = "1" + + elif bits == 1 and planes in (2, 4): + mode = "P" + rawmode = "P;%dL" % planes + self.palette = ImagePalette.raw("RGB", s[16:64]) + + elif version == 5 and bits == 8 and planes == 1: + mode = rawmode = "L" + # FIXME: hey, this doesn't work with the incremental loader !!! + self.fp.seek(-769, io.SEEK_END) + s = self.fp.read(769) + if len(s) == 769 and s[0] == 12: + # check if the palette is linear grayscale + for i in range(256): + if s[i * 3 + 1 : i * 3 + 4] != o8(i) * 3: + mode = rawmode = "P" + 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" + rawmode = "RGB;L" + + else: + msg = "unknown PCX mode" + raise OSError(msg) + + self._mode = mode + self._size = bbox[2] - bbox[0], bbox[3] - bbox[1] + + # Don't trust the passed in stride. + # Calculate the approximate position for ourselves. + # CVE-2020-35653 + stride = (self._size[0] * bits + 7) // 8 + + # While the specification states that this must be even, + # not all images follow this + if provided_stride != stride: + stride += stride % 2 + + bbox = (0, 0) + self.size + logger.debug("size: %sx%s", *self.size) + + self.tile = [("pcx", bbox, self.fp.tell(), (rawmode, planes * stride))] + + +# -------------------------------------------------------------------- +# save PCX files + + +SAVE = { + # mode: (version, bits, planes, raw mode) + "1": (2, 1, 1, "1"), + "L": (5, 8, 1, "L"), + "P": (5, 8, 1, "P"), + "RGB": (5, 8, 3, "RGB;L"), +} + + +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + try: + version, bits, planes, rawmode = SAVE[im.mode] + except KeyError as e: + msg = f"Cannot save {im.mode} images as PCX" + raise ValueError(msg) from e + + # bytes per plane + stride = (im.size[0] * bits + 7) // 8 + # stride should be even + stride += stride % 2 + # Stride needs to be kept in sync with the PcxEncode.c version. + # Ideally it should be passed in in the state, but the bytes value + # gets overwritten. + + logger.debug( + "PcxImagePlugin._save: xwidth: %d, bits: %d, stride: %d", + im.size[0], + bits, + stride, + ) + + # under windows, we could determine the current screen size with + # "Image.core.display_mode()[1]", but I think that's overkill... + + screen = im.size + + dpi = 100, 100 + + # PCX header + fp.write( + o8(10) + + o8(version) + + o8(1) + + o8(bits) + + o16(0) + + o16(0) + + o16(im.size[0] - 1) + + o16(im.size[1] - 1) + + o16(dpi[0]) + + o16(dpi[1]) + + b"\0" * 24 + + b"\xFF" * 24 + + b"\0" + + o8(planes) + + o16(stride) + + o16(1) + + o16(screen[0]) + + o16(screen[1]) + + b"\0" * 54 + ) + + assert fp.tell() == 128 + + ImageFile._save(im, fp, [("pcx", (0, 0) + im.size, 0, (rawmode, bits * planes))]) + + if im.mode == "P": + # colour palette + assert im.im is not None + + fp.write(o8(12)) + palette = im.im.getpalette("RGB", "RGB") + palette += b"\x00" * (768 - len(palette)) + fp.write(palette) # 768 bytes + elif im.mode == "L": + # grayscale palette + fp.write(o8(12)) + for i in range(256): + fp.write(o8(i) * 3) + + +# -------------------------------------------------------------------- +# registry + + +Image.register_open(PcxImageFile.format, PcxImageFile, _accept) +Image.register_save(PcxImageFile.format, _save) + +Image.register_extension(PcxImageFile.format, ".pcx") + +Image.register_mime(PcxImageFile.format, "image/x-pcx") diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index f0da1e047..9f9021c45 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -1,304 +1,304 @@ -# -# The Python Imaging Library. -# $Id$ -# -# PDF (Acrobat) file handling -# -# History: -# 1996-07-16 fl Created -# 1997-01-18 fl Fixed header -# 2004-02-21 fl Fixes for 1/L/CMYK images, etc. -# 2004-02-24 fl Fixes for 1 and P images. -# -# Copyright (c) 1997-2004 by Secret Labs AB. All rights reserved. -# Copyright (c) 1996-1997 by Fredrik Lundh. -# -# See the README file for information on usage and redistribution. -# - -## -# Image plugin for PDF images (output only). -## -from __future__ import annotations - -import io -import math -import os -import time -from typing import IO - -from . import Image, ImageFile, ImageSequence, PdfParser, __version__, features - -# -# -------------------------------------------------------------------- - -# object ids: -# 1. catalogue -# 2. pages -# 3. image -# 4. page -# 5. page contents - - -def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - _save(im, fp, filename, save_all=True) - - -## -# (Internal) Image save plugin for the PDF format. - - -def _write_image(im, filename, existing_pdf, image_refs): - # FIXME: Should replace ASCIIHexDecode with RunLengthDecode - # (packbits) or LZWDecode (tiff/lzw compression). Note that - # PDF 1.2 also supports Flatedecode (zip compression). - - params = None - decode = None - - # - # Get image characteristics - - width, height = im.size - - dict_obj = {"BitsPerComponent": 8} - if im.mode == "1": - if features.check("libtiff"): - filter = "CCITTFaxDecode" - dict_obj["BitsPerComponent"] = 1 - params = PdfParser.PdfArray( - [ - PdfParser.PdfDict( - { - "K": -1, - "BlackIs1": True, - "Columns": width, - "Rows": height, - } - ) - ] - ) - else: - filter = "DCTDecode" - dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceGray") - procset = "ImageB" # grayscale - elif im.mode == "L": - filter = "DCTDecode" - # params = f"<< /Predictor 15 /Columns {width-2} >>" - dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceGray") - procset = "ImageB" # grayscale - elif im.mode == "LA": - filter = "JPXDecode" - # params = f"<< /Predictor 15 /Columns {width-2} >>" - procset = "ImageB" # grayscale - dict_obj["SMaskInData"] = 1 - elif im.mode == "P": - filter = "ASCIIHexDecode" - palette = im.getpalette() - dict_obj["ColorSpace"] = [ - PdfParser.PdfName("Indexed"), - PdfParser.PdfName("DeviceRGB"), - len(palette) // 3 - 1, - PdfParser.PdfBinary(palette), - ] - procset = "ImageI" # indexed color - - if "transparency" in im.info: - smask = im.convert("LA").getchannel("A") - smask.encoderinfo = {} - - image_ref = _write_image(smask, filename, existing_pdf, image_refs)[0] - dict_obj["SMask"] = image_ref - elif im.mode == "RGB": - filter = "DCTDecode" - dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceRGB") - procset = "ImageC" # color images - elif im.mode == "RGBA": - filter = "JPXDecode" - procset = "ImageC" # color images - dict_obj["SMaskInData"] = 1 - elif im.mode == "CMYK": - filter = "DCTDecode" - dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceCMYK") - procset = "ImageC" # color images - decode = [1, 0, 1, 0, 1, 0, 1, 0] - else: - msg = f"cannot save mode {im.mode}" - raise ValueError(msg) - - # - # image - - op = io.BytesIO() - - if filter == "ASCIIHexDecode": - ImageFile._save(im, op, [("hex", (0, 0) + im.size, 0, im.mode)]) - elif filter == "CCITTFaxDecode": - im.save( - op, - "TIFF", - compression="group4", - # use a single strip - strip_size=math.ceil(width / 8) * height, - ) - elif filter == "DCTDecode": - Image.SAVE["JPEG"](im, op, filename) - elif filter == "JPXDecode": - del dict_obj["BitsPerComponent"] - Image.SAVE["JPEG2000"](im, op, filename) - else: - msg = f"unsupported PDF filter ({filter})" - raise ValueError(msg) - - stream = op.getvalue() - if filter == "CCITTFaxDecode": - stream = stream[8:] - filter = PdfParser.PdfArray([PdfParser.PdfName(filter)]) - else: - filter = PdfParser.PdfName(filter) - - image_ref = image_refs.pop(0) - existing_pdf.write_obj( - image_ref, - stream=stream, - Type=PdfParser.PdfName("XObject"), - Subtype=PdfParser.PdfName("Image"), - Width=width, # * 72.0 / x_resolution, - Height=height, # * 72.0 / y_resolution, - Filter=filter, - Decode=decode, - DecodeParms=params, - **dict_obj, - ) - - return image_ref, procset - - -def _save(im, fp, filename, save_all=False): - is_appending = im.encoderinfo.get("append", False) - if is_appending: - existing_pdf = PdfParser.PdfParser(f=fp, filename=filename, mode="r+b") - else: - existing_pdf = PdfParser.PdfParser(f=fp, filename=filename, mode="w+b") - - dpi = im.encoderinfo.get("dpi") - if dpi: - x_resolution = dpi[0] - y_resolution = dpi[1] - else: - x_resolution = y_resolution = im.encoderinfo.get("resolution", 72.0) - - info = { - "title": ( - None if is_appending else os.path.splitext(os.path.basename(filename))[0] - ), - "author": None, - "subject": None, - "keywords": None, - "creator": None, - "producer": None, - "creationDate": None if is_appending else time.gmtime(), - "modDate": None if is_appending else time.gmtime(), - } - for k, default in info.items(): - v = im.encoderinfo.get(k) if k in im.encoderinfo else default - if v: - existing_pdf.info[k[0].upper() + k[1:]] = v - - # - # make sure image data is available - im.load() - - existing_pdf.start_writing() - existing_pdf.write_header() - existing_pdf.write_comment(f"created by Pillow {__version__} PDF driver") - - # - # pages - ims = [im] - if save_all: - append_images = im.encoderinfo.get("append_images", []) - for append_im in append_images: - append_im.encoderinfo = im.encoderinfo.copy() - ims.append(append_im) - number_of_pages = 0 - image_refs = [] - page_refs = [] - contents_refs = [] - for im in ims: - im_number_of_pages = 1 - if save_all: - try: - im_number_of_pages = im.n_frames - except AttributeError: - # Image format does not have n_frames. - # It is a single frame image - pass - number_of_pages += im_number_of_pages - for i in range(im_number_of_pages): - image_refs.append(existing_pdf.next_object_id(0)) - if im.mode == "P" and "transparency" in im.info: - image_refs.append(existing_pdf.next_object_id(0)) - - page_refs.append(existing_pdf.next_object_id(0)) - contents_refs.append(existing_pdf.next_object_id(0)) - existing_pdf.pages.append(page_refs[-1]) - - # - # catalog and list of pages - existing_pdf.write_catalog() - - page_number = 0 - for im_sequence in ims: - im_pages = ImageSequence.Iterator(im_sequence) if save_all else [im_sequence] - for im in im_pages: - image_ref, procset = _write_image(im, filename, existing_pdf, image_refs) - - # - # page - - existing_pdf.write_page( - page_refs[page_number], - Resources=PdfParser.PdfDict( - ProcSet=[PdfParser.PdfName("PDF"), PdfParser.PdfName(procset)], - XObject=PdfParser.PdfDict(image=image_ref), - ), - MediaBox=[ - 0, - 0, - im.width * 72.0 / x_resolution, - im.height * 72.0 / y_resolution, - ], - Contents=contents_refs[page_number], - ) - - # - # page contents - - page_contents = b"q %f 0 0 %f 0 0 cm /image Do Q\n" % ( - im.width * 72.0 / x_resolution, - im.height * 72.0 / y_resolution, - ) - - existing_pdf.write_obj(contents_refs[page_number], stream=page_contents) - - page_number += 1 - - # - # trailer - existing_pdf.write_xref_and_trailer() - if hasattr(fp, "flush"): - fp.flush() - existing_pdf.close() - - -# -# -------------------------------------------------------------------- - - -Image.register_save("PDF", _save) -Image.register_save_all("PDF", _save_all) - -Image.register_extension("PDF", ".pdf") - -Image.register_mime("PDF", "application/pdf") +# +# The Python Imaging Library. +# $Id$ +# +# PDF (Acrobat) file handling +# +# History: +# 1996-07-16 fl Created +# 1997-01-18 fl Fixed header +# 2004-02-21 fl Fixes for 1/L/CMYK images, etc. +# 2004-02-24 fl Fixes for 1 and P images. +# +# Copyright (c) 1997-2004 by Secret Labs AB. All rights reserved. +# Copyright (c) 1996-1997 by Fredrik Lundh. +# +# See the README file for information on usage and redistribution. +# + +## +# Image plugin for PDF images (output only). +## +from __future__ import annotations + +import io +import math +import os +import time +from typing import IO + +from . import Image, ImageFile, ImageSequence, PdfParser, __version__, features + +# +# -------------------------------------------------------------------- + +# object ids: +# 1. catalogue +# 2. pages +# 3. image +# 4. page +# 5. page contents + + +def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + _save(im, fp, filename, save_all=True) + + +## +# (Internal) Image save plugin for the PDF format. + + +def _write_image(im, filename, existing_pdf, image_refs): + # FIXME: Should replace ASCIIHexDecode with RunLengthDecode + # (packbits) or LZWDecode (tiff/lzw compression). Note that + # PDF 1.2 also supports Flatedecode (zip compression). + + params = None + decode = None + + # + # Get image characteristics + + width, height = im.size + + dict_obj = {"BitsPerComponent": 8} + if im.mode == "1": + if features.check("libtiff"): + filter = "CCITTFaxDecode" + dict_obj["BitsPerComponent"] = 1 + params = PdfParser.PdfArray( + [ + PdfParser.PdfDict( + { + "K": -1, + "BlackIs1": True, + "Columns": width, + "Rows": height, + } + ) + ] + ) + else: + filter = "DCTDecode" + dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceGray") + procset = "ImageB" # grayscale + elif im.mode == "L": + filter = "DCTDecode" + # params = f"<< /Predictor 15 /Columns {width-2} >>" + dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceGray") + procset = "ImageB" # grayscale + elif im.mode == "LA": + filter = "JPXDecode" + # params = f"<< /Predictor 15 /Columns {width-2} >>" + procset = "ImageB" # grayscale + dict_obj["SMaskInData"] = 1 + elif im.mode == "P": + filter = "ASCIIHexDecode" + palette = im.getpalette() + dict_obj["ColorSpace"] = [ + PdfParser.PdfName("Indexed"), + PdfParser.PdfName("DeviceRGB"), + len(palette) // 3 - 1, + PdfParser.PdfBinary(palette), + ] + procset = "ImageI" # indexed color + + if "transparency" in im.info: + smask = im.convert("LA").getchannel("A") + smask.encoderinfo = {} + + image_ref = _write_image(smask, filename, existing_pdf, image_refs)[0] + dict_obj["SMask"] = image_ref + elif im.mode == "RGB": + filter = "DCTDecode" + dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceRGB") + procset = "ImageC" # color images + elif im.mode == "RGBA": + filter = "JPXDecode" + procset = "ImageC" # color images + dict_obj["SMaskInData"] = 1 + elif im.mode == "CMYK": + filter = "DCTDecode" + dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceCMYK") + procset = "ImageC" # color images + decode = [1, 0, 1, 0, 1, 0, 1, 0] + else: + msg = f"cannot save mode {im.mode}" + raise ValueError(msg) + + # + # image + + op = io.BytesIO() + + if filter == "ASCIIHexDecode": + ImageFile._save(im, op, [("hex", (0, 0) + im.size, 0, im.mode)]) + elif filter == "CCITTFaxDecode": + im.save( + op, + "TIFF", + compression="group4", + # use a single strip + strip_size=math.ceil(width / 8) * height, + ) + elif filter == "DCTDecode": + Image.SAVE["JPEG"](im, op, filename) + elif filter == "JPXDecode": + del dict_obj["BitsPerComponent"] + Image.SAVE["JPEG2000"](im, op, filename) + else: + msg = f"unsupported PDF filter ({filter})" + raise ValueError(msg) + + stream = op.getvalue() + if filter == "CCITTFaxDecode": + stream = stream[8:] + filter = PdfParser.PdfArray([PdfParser.PdfName(filter)]) + else: + filter = PdfParser.PdfName(filter) + + image_ref = image_refs.pop(0) + existing_pdf.write_obj( + image_ref, + stream=stream, + Type=PdfParser.PdfName("XObject"), + Subtype=PdfParser.PdfName("Image"), + Width=width, # * 72.0 / x_resolution, + Height=height, # * 72.0 / y_resolution, + Filter=filter, + Decode=decode, + DecodeParms=params, + **dict_obj, + ) + + return image_ref, procset + + +def _save(im, fp, filename, save_all=False): + is_appending = im.encoderinfo.get("append", False) + if is_appending: + existing_pdf = PdfParser.PdfParser(f=fp, filename=filename, mode="r+b") + else: + existing_pdf = PdfParser.PdfParser(f=fp, filename=filename, mode="w+b") + + dpi = im.encoderinfo.get("dpi") + if dpi: + x_resolution = dpi[0] + y_resolution = dpi[1] + else: + x_resolution = y_resolution = im.encoderinfo.get("resolution", 72.0) + + info = { + "title": ( + None if is_appending else os.path.splitext(os.path.basename(filename))[0] + ), + "author": None, + "subject": None, + "keywords": None, + "creator": None, + "producer": None, + "creationDate": None if is_appending else time.gmtime(), + "modDate": None if is_appending else time.gmtime(), + } + for k, default in info.items(): + v = im.encoderinfo.get(k) if k in im.encoderinfo else default + if v: + existing_pdf.info[k[0].upper() + k[1:]] = v + + # + # make sure image data is available + im.load() + + existing_pdf.start_writing() + existing_pdf.write_header() + existing_pdf.write_comment(f"created by Pillow {__version__} PDF driver") + + # + # pages + ims = [im] + if save_all: + append_images = im.encoderinfo.get("append_images", []) + for append_im in append_images: + append_im.encoderinfo = im.encoderinfo.copy() + ims.append(append_im) + number_of_pages = 0 + image_refs = [] + page_refs = [] + contents_refs = [] + for im in ims: + im_number_of_pages = 1 + if save_all: + try: + im_number_of_pages = im.n_frames + except AttributeError: + # Image format does not have n_frames. + # It is a single frame image + pass + number_of_pages += im_number_of_pages + for i in range(im_number_of_pages): + image_refs.append(existing_pdf.next_object_id(0)) + if im.mode == "P" and "transparency" in im.info: + image_refs.append(existing_pdf.next_object_id(0)) + + page_refs.append(existing_pdf.next_object_id(0)) + contents_refs.append(existing_pdf.next_object_id(0)) + existing_pdf.pages.append(page_refs[-1]) + + # + # catalog and list of pages + existing_pdf.write_catalog() + + page_number = 0 + for im_sequence in ims: + im_pages = ImageSequence.Iterator(im_sequence) if save_all else [im_sequence] + for im in im_pages: + image_ref, procset = _write_image(im, filename, existing_pdf, image_refs) + + # + # page + + existing_pdf.write_page( + page_refs[page_number], + Resources=PdfParser.PdfDict( + ProcSet=[PdfParser.PdfName("PDF"), PdfParser.PdfName(procset)], + XObject=PdfParser.PdfDict(image=image_ref), + ), + MediaBox=[ + 0, + 0, + im.width * 72.0 / x_resolution, + im.height * 72.0 / y_resolution, + ], + Contents=contents_refs[page_number], + ) + + # + # page contents + + page_contents = b"q %f 0 0 %f 0 0 cm /image Do Q\n" % ( + im.width * 72.0 / x_resolution, + im.height * 72.0 / y_resolution, + ) + + existing_pdf.write_obj(contents_refs[page_number], stream=page_contents) + + page_number += 1 + + # + # trailer + existing_pdf.write_xref_and_trailer() + if hasattr(fp, "flush"): + fp.flush() + existing_pdf.close() + + +# +# -------------------------------------------------------------------- + + +Image.register_save("PDF", _save) +Image.register_save_all("PDF", _save_all) + +Image.register_extension("PDF", ".pdf") + +Image.register_mime("PDF", "application/pdf") diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index 9e2231347..93d677e89 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -1,1003 +1,1003 @@ -from __future__ import annotations - -import calendar -import codecs -import collections -import mmap -import os -import re -import time -import zlib -from typing import TYPE_CHECKING, Any, List, NamedTuple, Union - - -# see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set -# on page 656 -def encode_text(s: str) -> bytes: - return codecs.BOM_UTF16_BE + s.encode("utf_16_be") - - -PDFDocEncoding = { - 0x16: "\u0017", - 0x18: "\u02D8", - 0x19: "\u02C7", - 0x1A: "\u02C6", - 0x1B: "\u02D9", - 0x1C: "\u02DD", - 0x1D: "\u02DB", - 0x1E: "\u02DA", - 0x1F: "\u02DC", - 0x80: "\u2022", - 0x81: "\u2020", - 0x82: "\u2021", - 0x83: "\u2026", - 0x84: "\u2014", - 0x85: "\u2013", - 0x86: "\u0192", - 0x87: "\u2044", - 0x88: "\u2039", - 0x89: "\u203A", - 0x8A: "\u2212", - 0x8B: "\u2030", - 0x8C: "\u201E", - 0x8D: "\u201C", - 0x8E: "\u201D", - 0x8F: "\u2018", - 0x90: "\u2019", - 0x91: "\u201A", - 0x92: "\u2122", - 0x93: "\uFB01", - 0x94: "\uFB02", - 0x95: "\u0141", - 0x96: "\u0152", - 0x97: "\u0160", - 0x98: "\u0178", - 0x99: "\u017D", - 0x9A: "\u0131", - 0x9B: "\u0142", - 0x9C: "\u0153", - 0x9D: "\u0161", - 0x9E: "\u017E", - 0xA0: "\u20AC", -} - - -def decode_text(b): - if b[: len(codecs.BOM_UTF16_BE)] == codecs.BOM_UTF16_BE: - return b[len(codecs.BOM_UTF16_BE) :].decode("utf_16_be") - else: - return "".join(PDFDocEncoding.get(byte, chr(byte)) for byte in b) - - -class PdfFormatError(RuntimeError): - """An error that probably indicates a syntactic or semantic error in the - PDF file structure""" - - pass - - -def check_format_condition(condition: bool, error_message: str) -> None: - if not condition: - raise PdfFormatError(error_message) - - -class IndirectReferenceTuple(NamedTuple): - object_id: int - generation: int - - -class IndirectReference(IndirectReferenceTuple): - def __str__(self) -> str: - return f"{self.object_id} {self.generation} R" - - def __bytes__(self) -> bytes: - return self.__str__().encode("us-ascii") - - def __eq__(self, other: object) -> bool: - if self.__class__ is not other.__class__: - return False - assert isinstance(other, IndirectReference) - return other.object_id == self.object_id and other.generation == self.generation - - def __ne__(self, other): - return not (self == other) - - def __hash__(self) -> int: - return hash((self.object_id, self.generation)) - - -class IndirectObjectDef(IndirectReference): - def __str__(self) -> str: - return f"{self.object_id} {self.generation} obj" - - -class XrefTable: - def __init__(self): - self.existing_entries = {} # object ID => (offset, generation) - self.new_entries = {} # object ID => (offset, generation) - self.deleted_entries = {0: 65536} # object ID => generation - self.reading_finished = False - - def __setitem__(self, key, value): - if self.reading_finished: - self.new_entries[key] = value - else: - self.existing_entries[key] = value - if key in self.deleted_entries: - del self.deleted_entries[key] - - def __getitem__(self, key): - try: - return self.new_entries[key] - except KeyError: - return self.existing_entries[key] - - def __delitem__(self, key): - if key in self.new_entries: - generation = self.new_entries[key][1] + 1 - del self.new_entries[key] - self.deleted_entries[key] = generation - elif key in self.existing_entries: - generation = self.existing_entries[key][1] + 1 - self.deleted_entries[key] = generation - elif key in self.deleted_entries: - generation = self.deleted_entries[key] - else: - msg = f"object ID {key} cannot be deleted because it doesn't exist" - raise IndexError(msg) - - def __contains__(self, key): - return key in self.existing_entries or key in self.new_entries - - def __len__(self) -> int: - return len( - set(self.existing_entries.keys()) - | set(self.new_entries.keys()) - | set(self.deleted_entries.keys()) - ) - - def keys(self): - return ( - set(self.existing_entries.keys()) - set(self.deleted_entries.keys()) - ) | set(self.new_entries.keys()) - - def write(self, f): - keys = sorted(set(self.new_entries.keys()) | set(self.deleted_entries.keys())) - deleted_keys = sorted(set(self.deleted_entries.keys())) - startxref = f.tell() - f.write(b"xref\n") - while keys: - # find a contiguous sequence of object IDs - prev = None - for index, key in enumerate(keys): - if prev is None or prev + 1 == key: - prev = key - else: - contiguous_keys = keys[:index] - keys = keys[index:] - break - else: - contiguous_keys = keys - keys = None - f.write(b"%d %d\n" % (contiguous_keys[0], len(contiguous_keys))) - for object_id in contiguous_keys: - if object_id in self.new_entries: - f.write(b"%010d %05d n \n" % self.new_entries[object_id]) - else: - this_deleted_object_id = deleted_keys.pop(0) - check_format_condition( - object_id == this_deleted_object_id, - f"expected the next deleted object ID to be {object_id}, " - f"instead found {this_deleted_object_id}", - ) - try: - next_in_linked_list = deleted_keys[0] - except IndexError: - next_in_linked_list = 0 - f.write( - b"%010d %05d f \n" - % (next_in_linked_list, self.deleted_entries[object_id]) - ) - return startxref - - -class PdfName: - def __init__(self, name): - if isinstance(name, PdfName): - self.name = name.name - elif isinstance(name, bytes): - self.name = name - else: - self.name = name.encode("us-ascii") - - def name_as_str(self) -> str: - return self.name.decode("us-ascii") - - def __eq__(self, other): - return ( - isinstance(other, PdfName) and other.name == self.name - ) or other == self.name - - def __hash__(self) -> int: - return hash(self.name) - - def __repr__(self) -> str: - return f"{self.__class__.__name__}({repr(self.name)})" - - @classmethod - def from_pdf_stream(cls, data): - return cls(PdfParser.interpret_name(data)) - - allowed_chars = set(range(33, 127)) - {ord(c) for c in "#%/()<>[]{}"} - - def __bytes__(self) -> bytes: - result = bytearray(b"/") - for b in self.name: - if b in self.allowed_chars: - result.append(b) - else: - result.extend(b"#%02X" % b) - return bytes(result) - - -class PdfArray(List[Any]): - def __bytes__(self) -> bytes: - return b"[ " + b" ".join(pdf_repr(x) for x in self) + b" ]" - - -if TYPE_CHECKING: - _DictBase = collections.UserDict[Union[str, bytes], Any] -else: - _DictBase = collections.UserDict - - -class PdfDict(_DictBase): - def __setattr__(self, key, value): - if key == "data": - collections.UserDict.__setattr__(self, key, value) - else: - self[key.encode("us-ascii")] = value - - def __getattr__(self, key): - try: - value = self[key.encode("us-ascii")] - except KeyError as e: - raise AttributeError(key) from e - if isinstance(value, bytes): - value = decode_text(value) - if key.endswith("Date"): - if value.startswith("D:"): - value = value[2:] - - relationship = "Z" - if len(value) > 17: - relationship = value[14] - offset = int(value[15:17]) * 60 - if len(value) > 20: - offset += int(value[18:20]) - - format = "%Y%m%d%H%M%S"[: len(value) - 2] - value = time.strptime(value[: len(format) + 2], format) - if relationship in ["+", "-"]: - offset *= 60 - if relationship == "+": - offset *= -1 - value = time.gmtime(calendar.timegm(value) + offset) - return value - - def __bytes__(self) -> bytes: - out = bytearray(b"<<") - for key, value in self.items(): - if value is None: - continue - value = pdf_repr(value) - out.extend(b"\n") - out.extend(bytes(PdfName(key))) - out.extend(b" ") - out.extend(value) - out.extend(b"\n>>") - return bytes(out) - - -class PdfBinary: - def __init__(self, data): - self.data = data - - def __bytes__(self) -> bytes: - return b"<%s>" % b"".join(b"%02X" % b for b in self.data) - - -class PdfStream: - def __init__(self, dictionary, buf): - self.dictionary = dictionary - self.buf = buf - - def decode(self): - try: - filter = self.dictionary.Filter - except AttributeError: - return self.buf - if filter == b"FlateDecode": - try: - expected_length = self.dictionary.DL - except AttributeError: - expected_length = self.dictionary.Length - return zlib.decompress(self.buf, bufsize=int(expected_length)) - else: - msg = f"stream filter {repr(self.dictionary.Filter)} unknown/unsupported" - raise NotImplementedError(msg) - - -def pdf_repr(x): - if x is True: - return b"true" - elif x is False: - return b"false" - elif x is None: - return b"null" - elif isinstance(x, (PdfName, PdfDict, PdfArray, PdfBinary)): - return bytes(x) - elif isinstance(x, (int, float)): - return str(x).encode("us-ascii") - elif isinstance(x, time.struct_time): - return b"(D:" + time.strftime("%Y%m%d%H%M%SZ", x).encode("us-ascii") + b")" - elif isinstance(x, dict): - return bytes(PdfDict(x)) - elif isinstance(x, list): - return bytes(PdfArray(x)) - elif isinstance(x, str): - return pdf_repr(encode_text(x)) - elif isinstance(x, bytes): - # XXX escape more chars? handle binary garbage - x = x.replace(b"\\", b"\\\\") - x = x.replace(b"(", b"\\(") - x = x.replace(b")", b"\\)") - return b"(" + x + b")" - else: - return bytes(x) - - -class PdfParser: - """Based on - https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/PDF32000_2008.pdf - Supports PDF up to 1.4 - """ - - def __init__(self, filename=None, f=None, buf=None, start_offset=0, mode="rb"): - if buf and f: - msg = "specify buf or f or filename, but not both buf and f" - raise RuntimeError(msg) - self.filename = filename - self.buf = buf - self.f = f - self.start_offset = start_offset - self.should_close_buf = False - self.should_close_file = False - if filename is not None and f is None: - self.f = f = open(filename, mode) - self.should_close_file = True - if f is not None: - self.buf = buf = self.get_buf_from_file(f) - self.should_close_buf = True - if not filename and hasattr(f, "name"): - self.filename = f.name - self.cached_objects = {} - if buf: - self.read_pdf_info() - else: - self.file_size_total = self.file_size_this = 0 - self.root = PdfDict() - self.root_ref = None - self.info = PdfDict() - self.info_ref = None - self.page_tree_root = {} - self.pages = [] - self.orig_pages = [] - self.pages_ref = None - self.last_xref_section_offset = None - self.trailer_dict = {} - self.xref_table = XrefTable() - self.xref_table.reading_finished = True - if f: - self.seek_end() - - def __enter__(self) -> PdfParser: - return self - - def __exit__(self, *args: object) -> None: - self.close() - - def start_writing(self) -> None: - self.close_buf() - self.seek_end() - - def close_buf(self) -> None: - try: - self.buf.close() - except AttributeError: - pass - self.buf = None - - def close(self) -> None: - if self.should_close_buf: - self.close_buf() - if self.f is not None and self.should_close_file: - self.f.close() - self.f = None - - def seek_end(self) -> None: - self.f.seek(0, os.SEEK_END) - - def write_header(self) -> None: - self.f.write(b"%PDF-1.4\n") - - def write_comment(self, s): - self.f.write(f"% {s}\n".encode()) - - def write_catalog(self) -> IndirectReference: - self.del_root() - self.root_ref = self.next_object_id(self.f.tell()) - self.pages_ref = self.next_object_id(0) - self.rewrite_pages() - self.write_obj(self.root_ref, Type=PdfName(b"Catalog"), Pages=self.pages_ref) - self.write_obj( - self.pages_ref, - Type=PdfName(b"Pages"), - Count=len(self.pages), - Kids=self.pages, - ) - return self.root_ref - - def rewrite_pages(self) -> None: - pages_tree_nodes_to_delete = [] - for i, page_ref in enumerate(self.orig_pages): - page_info = self.cached_objects[page_ref] - del self.xref_table[page_ref.object_id] - pages_tree_nodes_to_delete.append(page_info[PdfName(b"Parent")]) - if page_ref not in self.pages: - # the page has been deleted - continue - # make dict keys into strings for passing to write_page - stringified_page_info = {} - for key, value in page_info.items(): - # key should be a PdfName - stringified_page_info[key.name_as_str()] = value - stringified_page_info["Parent"] = self.pages_ref - new_page_ref = self.write_page(None, **stringified_page_info) - for j, cur_page_ref in enumerate(self.pages): - if cur_page_ref == page_ref: - # replace the page reference with the new one - self.pages[j] = new_page_ref - # delete redundant Pages tree nodes from xref table - for pages_tree_node_ref in pages_tree_nodes_to_delete: - while pages_tree_node_ref: - pages_tree_node = self.cached_objects[pages_tree_node_ref] - if pages_tree_node_ref.object_id in self.xref_table: - del self.xref_table[pages_tree_node_ref.object_id] - pages_tree_node_ref = pages_tree_node.get(b"Parent", None) - self.orig_pages = [] - - def write_xref_and_trailer(self, new_root_ref=None): - if new_root_ref: - self.del_root() - self.root_ref = new_root_ref - if self.info: - self.info_ref = self.write_obj(None, self.info) - start_xref = self.xref_table.write(self.f) - num_entries = len(self.xref_table) - trailer_dict = {b"Root": self.root_ref, b"Size": num_entries} - if self.last_xref_section_offset is not None: - trailer_dict[b"Prev"] = self.last_xref_section_offset - if self.info: - trailer_dict[b"Info"] = self.info_ref - self.last_xref_section_offset = start_xref - self.f.write( - b"trailer\n" - + bytes(PdfDict(trailer_dict)) - + b"\nstartxref\n%d\n%%%%EOF" % start_xref - ) - - def write_page(self, ref, *objs, **dict_obj): - if isinstance(ref, int): - ref = self.pages[ref] - if "Type" not in dict_obj: - dict_obj["Type"] = PdfName(b"Page") - if "Parent" not in dict_obj: - dict_obj["Parent"] = self.pages_ref - return self.write_obj(ref, *objs, **dict_obj) - - def write_obj(self, ref, *objs, **dict_obj): - f = self.f - if ref is None: - ref = self.next_object_id(f.tell()) - else: - self.xref_table[ref.object_id] = (f.tell(), ref.generation) - f.write(bytes(IndirectObjectDef(*ref))) - stream = dict_obj.pop("stream", None) - if stream is not None: - dict_obj["Length"] = len(stream) - if dict_obj: - f.write(pdf_repr(dict_obj)) - for obj in objs: - f.write(pdf_repr(obj)) - if stream is not None: - f.write(b"stream\n") - f.write(stream) - f.write(b"\nendstream\n") - f.write(b"endobj\n") - return ref - - def del_root(self) -> None: - if self.root_ref is None: - return - del self.xref_table[self.root_ref.object_id] - del self.xref_table[self.root[b"Pages"].object_id] - - @staticmethod - def get_buf_from_file(f): - if hasattr(f, "getbuffer"): - return f.getbuffer() - elif hasattr(f, "getvalue"): - return f.getvalue() - else: - try: - return mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) - except ValueError: # cannot mmap an empty file - return b"" - - def read_pdf_info(self) -> None: - self.file_size_total = len(self.buf) - self.file_size_this = self.file_size_total - self.start_offset - self.read_trailer() - self.root_ref = self.trailer_dict[b"Root"] - self.info_ref = self.trailer_dict.get(b"Info", None) - self.root = PdfDict(self.read_indirect(self.root_ref)) - if self.info_ref is None: - self.info = PdfDict() - else: - self.info = PdfDict(self.read_indirect(self.info_ref)) - check_format_condition(b"Type" in self.root, "/Type missing in Root") - check_format_condition( - self.root[b"Type"] == b"Catalog", "/Type in Root is not /Catalog" - ) - check_format_condition(b"Pages" in self.root, "/Pages missing in Root") - check_format_condition( - isinstance(self.root[b"Pages"], IndirectReference), - "/Pages in Root is not an indirect reference", - ) - self.pages_ref = self.root[b"Pages"] - self.page_tree_root = self.read_indirect(self.pages_ref) - self.pages = self.linearize_page_tree(self.page_tree_root) - # save the original list of page references - # in case the user modifies, adds or deletes some pages - # and we need to rewrite the pages and their list - self.orig_pages = self.pages[:] - - def next_object_id(self, offset=None): - try: - # TODO: support reuse of deleted objects - reference = IndirectReference(max(self.xref_table.keys()) + 1, 0) - except ValueError: - reference = IndirectReference(1, 0) - if offset is not None: - self.xref_table[reference.object_id] = (offset, 0) - return reference - - delimiter = rb"[][()<>{}/%]" - delimiter_or_ws = rb"[][()<>{}/%\000\011\012\014\015\040]" - whitespace = rb"[\000\011\012\014\015\040]" - whitespace_or_hex = rb"[\000\011\012\014\015\0400-9a-fA-F]" - whitespace_optional = whitespace + b"*" - whitespace_mandatory = whitespace + b"+" - # No "\012" aka "\n" or "\015" aka "\r": - whitespace_optional_no_nl = rb"[\000\011\014\040]*" - newline_only = rb"[\r\n]+" - newline = whitespace_optional_no_nl + newline_only + whitespace_optional_no_nl - re_trailer_end = re.compile( - whitespace_mandatory - + rb"trailer" - + whitespace_optional - + rb"<<(.*>>)" - + newline - + rb"startxref" - + newline - + rb"([0-9]+)" - + newline - + rb"%%EOF" - + whitespace_optional - + rb"$", - re.DOTALL, - ) - re_trailer_prev = re.compile( - whitespace_optional - + rb"trailer" - + whitespace_optional - + rb"<<(.*?>>)" - + newline - + rb"startxref" - + newline - + rb"([0-9]+)" - + newline - + rb"%%EOF" - + whitespace_optional, - re.DOTALL, - ) - - def read_trailer(self): - search_start_offset = len(self.buf) - 16384 - if search_start_offset < self.start_offset: - search_start_offset = self.start_offset - m = self.re_trailer_end.search(self.buf, search_start_offset) - check_format_condition(m, "trailer end not found") - # make sure we found the LAST trailer - last_match = m - while m: - last_match = m - m = self.re_trailer_end.search(self.buf, m.start() + 16) - if not m: - m = last_match - trailer_data = m.group(1) - self.last_xref_section_offset = int(m.group(2)) - self.trailer_dict = self.interpret_trailer(trailer_data) - self.xref_table = XrefTable() - self.read_xref_table(xref_section_offset=self.last_xref_section_offset) - if b"Prev" in self.trailer_dict: - self.read_prev_trailer(self.trailer_dict[b"Prev"]) - - def read_prev_trailer(self, xref_section_offset): - trailer_offset = self.read_xref_table(xref_section_offset=xref_section_offset) - m = self.re_trailer_prev.search( - self.buf[trailer_offset : trailer_offset + 16384] - ) - check_format_condition(m, "previous trailer not found") - trailer_data = m.group(1) - check_format_condition( - int(m.group(2)) == xref_section_offset, - "xref section offset in previous trailer doesn't match what was expected", - ) - trailer_dict = self.interpret_trailer(trailer_data) - if b"Prev" in trailer_dict: - self.read_prev_trailer(trailer_dict[b"Prev"]) - - re_whitespace_optional = re.compile(whitespace_optional) - re_name = re.compile( - whitespace_optional - + rb"/([!-$&'*-.0-;=?-Z\\^-z|~]+)(?=" - + delimiter_or_ws - + rb")" - ) - re_dict_start = re.compile(whitespace_optional + rb"<<") - re_dict_end = re.compile(whitespace_optional + rb">>" + whitespace_optional) - - @classmethod - def interpret_trailer(cls, trailer_data): - trailer = {} - offset = 0 - while True: - m = cls.re_name.match(trailer_data, offset) - if not m: - m = cls.re_dict_end.match(trailer_data, offset) - check_format_condition( - m and m.end() == len(trailer_data), - "name not found in trailer, remaining data: " - + repr(trailer_data[offset:]), - ) - break - key = cls.interpret_name(m.group(1)) - value, offset = cls.get_value(trailer_data, m.end()) - trailer[key] = value - check_format_condition( - b"Size" in trailer and isinstance(trailer[b"Size"], int), - "/Size not in trailer or not an integer", - ) - check_format_condition( - b"Root" in trailer and isinstance(trailer[b"Root"], IndirectReference), - "/Root not in trailer or not an indirect reference", - ) - return trailer - - re_hashes_in_name = re.compile(rb"([^#]*)(#([0-9a-fA-F]{2}))?") - - @classmethod - def interpret_name(cls, raw, as_text=False): - name = b"" - for m in cls.re_hashes_in_name.finditer(raw): - if m.group(3): - name += m.group(1) + bytearray.fromhex(m.group(3).decode("us-ascii")) - else: - name += m.group(1) - if as_text: - return name.decode("utf-8") - else: - return bytes(name) - - re_null = re.compile(whitespace_optional + rb"null(?=" + delimiter_or_ws + rb")") - re_true = re.compile(whitespace_optional + rb"true(?=" + delimiter_or_ws + rb")") - re_false = re.compile(whitespace_optional + rb"false(?=" + delimiter_or_ws + rb")") - re_int = re.compile( - whitespace_optional + rb"([-+]?[0-9]+)(?=" + delimiter_or_ws + rb")" - ) - re_real = re.compile( - whitespace_optional - + rb"([-+]?([0-9]+\.[0-9]*|[0-9]*\.[0-9]+))(?=" - + delimiter_or_ws - + rb")" - ) - re_array_start = re.compile(whitespace_optional + rb"\[") - re_array_end = re.compile(whitespace_optional + rb"]") - re_string_hex = re.compile( - whitespace_optional + rb"<(" + whitespace_or_hex + rb"*)>" - ) - re_string_lit = re.compile(whitespace_optional + rb"\(") - re_indirect_reference = re.compile( - whitespace_optional - + rb"([-+]?[0-9]+)" - + whitespace_mandatory - + rb"([-+]?[0-9]+)" - + whitespace_mandatory - + rb"R(?=" - + delimiter_or_ws - + rb")" - ) - re_indirect_def_start = re.compile( - whitespace_optional - + rb"([-+]?[0-9]+)" - + whitespace_mandatory - + rb"([-+]?[0-9]+)" - + whitespace_mandatory - + rb"obj(?=" - + delimiter_or_ws - + rb")" - ) - re_indirect_def_end = re.compile( - whitespace_optional + rb"endobj(?=" + delimiter_or_ws + rb")" - ) - re_comment = re.compile( - rb"(" + whitespace_optional + rb"%[^\r\n]*" + newline + rb")*" - ) - re_stream_start = re.compile(whitespace_optional + rb"stream\r?\n") - re_stream_end = re.compile( - whitespace_optional + rb"endstream(?=" + delimiter_or_ws + rb")" - ) - - @classmethod - def get_value(cls, data, offset, expect_indirect=None, max_nesting=-1): - if max_nesting == 0: - return None, None - m = cls.re_comment.match(data, offset) - if m: - offset = m.end() - m = cls.re_indirect_def_start.match(data, offset) - if m: - check_format_condition( - int(m.group(1)) > 0, - "indirect object definition: object ID must be greater than 0", - ) - check_format_condition( - int(m.group(2)) >= 0, - "indirect object definition: generation must be non-negative", - ) - check_format_condition( - expect_indirect is None - or expect_indirect - == IndirectReference(int(m.group(1)), int(m.group(2))), - "indirect object definition different than expected", - ) - object, offset = cls.get_value(data, m.end(), max_nesting=max_nesting - 1) - if offset is None: - return object, None - m = cls.re_indirect_def_end.match(data, offset) - check_format_condition(m, "indirect object definition end not found") - return object, m.end() - check_format_condition( - not expect_indirect, "indirect object definition not found" - ) - m = cls.re_indirect_reference.match(data, offset) - if m: - check_format_condition( - int(m.group(1)) > 0, - "indirect object reference: object ID must be greater than 0", - ) - check_format_condition( - int(m.group(2)) >= 0, - "indirect object reference: generation must be non-negative", - ) - return IndirectReference(int(m.group(1)), int(m.group(2))), m.end() - m = cls.re_dict_start.match(data, offset) - if m: - offset = m.end() - result = {} - m = cls.re_dict_end.match(data, offset) - while not m: - key, offset = cls.get_value(data, offset, max_nesting=max_nesting - 1) - if offset is None: - return result, None - value, offset = cls.get_value(data, offset, max_nesting=max_nesting - 1) - result[key] = value - if offset is None: - return result, None - m = cls.re_dict_end.match(data, offset) - offset = m.end() - m = cls.re_stream_start.match(data, offset) - if m: - try: - stream_len_str = result.get(b"Length") - stream_len = int(stream_len_str) - except (TypeError, ValueError) as e: - msg = f"bad or missing Length in stream dict ({stream_len_str})" - raise PdfFormatError(msg) from e - stream_data = data[m.end() : m.end() + stream_len] - m = cls.re_stream_end.match(data, m.end() + stream_len) - check_format_condition(m, "stream end not found") - offset = m.end() - result = PdfStream(PdfDict(result), stream_data) - else: - result = PdfDict(result) - return result, offset - m = cls.re_array_start.match(data, offset) - if m: - offset = m.end() - result = [] - m = cls.re_array_end.match(data, offset) - while not m: - value, offset = cls.get_value(data, offset, max_nesting=max_nesting - 1) - result.append(value) - if offset is None: - return result, None - m = cls.re_array_end.match(data, offset) - return result, m.end() - m = cls.re_null.match(data, offset) - if m: - return None, m.end() - m = cls.re_true.match(data, offset) - if m: - return True, m.end() - m = cls.re_false.match(data, offset) - if m: - return False, m.end() - m = cls.re_name.match(data, offset) - if m: - return PdfName(cls.interpret_name(m.group(1))), m.end() - m = cls.re_int.match(data, offset) - if m: - return int(m.group(1)), m.end() - m = cls.re_real.match(data, offset) - if m: - # XXX Decimal instead of float??? - return float(m.group(1)), m.end() - m = cls.re_string_hex.match(data, offset) - if m: - # filter out whitespace - hex_string = bytearray( - b for b in m.group(1) if b in b"0123456789abcdefABCDEF" - ) - if len(hex_string) % 2 == 1: - # append a 0 if the length is not even - yes, at the end - hex_string.append(ord(b"0")) - return bytearray.fromhex(hex_string.decode("us-ascii")), m.end() - m = cls.re_string_lit.match(data, offset) - if m: - return cls.get_literal_string(data, m.end()) - # return None, offset # fallback (only for debugging) - msg = f"unrecognized object: {repr(data[offset : offset + 32])}" - raise PdfFormatError(msg) - - re_lit_str_token = re.compile( - rb"(\\[nrtbf()\\])|(\\[0-9]{1,3})|(\\(\r\n|\r|\n))|(\r\n|\r|\n)|(\()|(\))" - ) - escaped_chars = { - b"n": b"\n", - b"r": b"\r", - b"t": b"\t", - b"b": b"\b", - b"f": b"\f", - b"(": b"(", - b")": b")", - b"\\": b"\\", - ord(b"n"): b"\n", - ord(b"r"): b"\r", - ord(b"t"): b"\t", - ord(b"b"): b"\b", - ord(b"f"): b"\f", - ord(b"("): b"(", - ord(b")"): b")", - ord(b"\\"): b"\\", - } - - @classmethod - def get_literal_string(cls, data, offset): - nesting_depth = 0 - result = bytearray() - for m in cls.re_lit_str_token.finditer(data, offset): - result.extend(data[offset : m.start()]) - if m.group(1): - result.extend(cls.escaped_chars[m.group(1)[1]]) - elif m.group(2): - result.append(int(m.group(2)[1:], 8)) - elif m.group(3): - pass - elif m.group(5): - result.extend(b"\n") - elif m.group(6): - result.extend(b"(") - nesting_depth += 1 - elif m.group(7): - if nesting_depth == 0: - return bytes(result), m.end() - result.extend(b")") - nesting_depth -= 1 - offset = m.end() - msg = "unfinished literal string" - raise PdfFormatError(msg) - - re_xref_section_start = re.compile(whitespace_optional + rb"xref" + newline) - re_xref_subsection_start = re.compile( - whitespace_optional - + rb"([0-9]+)" - + whitespace_mandatory - + rb"([0-9]+)" - + whitespace_optional - + newline_only - ) - re_xref_entry = re.compile(rb"([0-9]{10}) ([0-9]{5}) ([fn])( \r| \n|\r\n)") - - def read_xref_table(self, xref_section_offset): - subsection_found = False - m = self.re_xref_section_start.match( - self.buf, xref_section_offset + self.start_offset - ) - check_format_condition(m, "xref section start not found") - offset = m.end() - while True: - m = self.re_xref_subsection_start.match(self.buf, offset) - if not m: - check_format_condition( - subsection_found, "xref subsection start not found" - ) - break - subsection_found = True - offset = m.end() - first_object = int(m.group(1)) - num_objects = int(m.group(2)) - for i in range(first_object, first_object + num_objects): - m = self.re_xref_entry.match(self.buf, offset) - check_format_condition(m, "xref entry not found") - offset = m.end() - is_free = m.group(3) == b"f" - if not is_free: - generation = int(m.group(2)) - new_entry = (int(m.group(1)), generation) - if i not in self.xref_table: - self.xref_table[i] = new_entry - return offset - - def read_indirect(self, ref, max_nesting=-1): - offset, generation = self.xref_table[ref[0]] - check_format_condition( - generation == ref[1], - f"expected to find generation {ref[1]} for object ID {ref[0]} in xref " - f"table, instead found generation {generation} at offset {offset}", - ) - value = self.get_value( - self.buf, - offset + self.start_offset, - expect_indirect=IndirectReference(*ref), - max_nesting=max_nesting, - )[0] - self.cached_objects[ref] = value - return value - - def linearize_page_tree(self, node=None): - if node is None: - node = self.page_tree_root - check_format_condition( - node[b"Type"] == b"Pages", "/Type of page tree node is not /Pages" - ) - pages = [] - for kid in node[b"Kids"]: - kid_object = self.read_indirect(kid) - if kid_object[b"Type"] == b"Page": - pages.append(kid) - else: - pages.extend(self.linearize_page_tree(node=kid_object)) - return pages +from __future__ import annotations + +import calendar +import codecs +import collections +import mmap +import os +import re +import time +import zlib +from typing import TYPE_CHECKING, Any, List, NamedTuple, Union + + +# see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set +# on page 656 +def encode_text(s: str) -> bytes: + return codecs.BOM_UTF16_BE + s.encode("utf_16_be") + + +PDFDocEncoding = { + 0x16: "\u0017", + 0x18: "\u02D8", + 0x19: "\u02C7", + 0x1A: "\u02C6", + 0x1B: "\u02D9", + 0x1C: "\u02DD", + 0x1D: "\u02DB", + 0x1E: "\u02DA", + 0x1F: "\u02DC", + 0x80: "\u2022", + 0x81: "\u2020", + 0x82: "\u2021", + 0x83: "\u2026", + 0x84: "\u2014", + 0x85: "\u2013", + 0x86: "\u0192", + 0x87: "\u2044", + 0x88: "\u2039", + 0x89: "\u203A", + 0x8A: "\u2212", + 0x8B: "\u2030", + 0x8C: "\u201E", + 0x8D: "\u201C", + 0x8E: "\u201D", + 0x8F: "\u2018", + 0x90: "\u2019", + 0x91: "\u201A", + 0x92: "\u2122", + 0x93: "\uFB01", + 0x94: "\uFB02", + 0x95: "\u0141", + 0x96: "\u0152", + 0x97: "\u0160", + 0x98: "\u0178", + 0x99: "\u017D", + 0x9A: "\u0131", + 0x9B: "\u0142", + 0x9C: "\u0153", + 0x9D: "\u0161", + 0x9E: "\u017E", + 0xA0: "\u20AC", +} + + +def decode_text(b): + if b[: len(codecs.BOM_UTF16_BE)] == codecs.BOM_UTF16_BE: + return b[len(codecs.BOM_UTF16_BE) :].decode("utf_16_be") + else: + return "".join(PDFDocEncoding.get(byte, chr(byte)) for byte in b) + + +class PdfFormatError(RuntimeError): + """An error that probably indicates a syntactic or semantic error in the + PDF file structure""" + + pass + + +def check_format_condition(condition: bool, error_message: str) -> None: + if not condition: + raise PdfFormatError(error_message) + + +class IndirectReferenceTuple(NamedTuple): + object_id: int + generation: int + + +class IndirectReference(IndirectReferenceTuple): + def __str__(self) -> str: + return f"{self.object_id} {self.generation} R" + + def __bytes__(self) -> bytes: + return self.__str__().encode("us-ascii") + + def __eq__(self, other: object) -> bool: + if self.__class__ is not other.__class__: + return False + assert isinstance(other, IndirectReference) + return other.object_id == self.object_id and other.generation == self.generation + + def __ne__(self, other): + return not (self == other) + + def __hash__(self) -> int: + return hash((self.object_id, self.generation)) + + +class IndirectObjectDef(IndirectReference): + def __str__(self) -> str: + return f"{self.object_id} {self.generation} obj" + + +class XrefTable: + def __init__(self): + self.existing_entries = {} # object ID => (offset, generation) + self.new_entries = {} # object ID => (offset, generation) + self.deleted_entries = {0: 65536} # object ID => generation + self.reading_finished = False + + def __setitem__(self, key, value): + if self.reading_finished: + self.new_entries[key] = value + else: + self.existing_entries[key] = value + if key in self.deleted_entries: + del self.deleted_entries[key] + + def __getitem__(self, key): + try: + return self.new_entries[key] + except KeyError: + return self.existing_entries[key] + + def __delitem__(self, key): + if key in self.new_entries: + generation = self.new_entries[key][1] + 1 + del self.new_entries[key] + self.deleted_entries[key] = generation + elif key in self.existing_entries: + generation = self.existing_entries[key][1] + 1 + self.deleted_entries[key] = generation + elif key in self.deleted_entries: + generation = self.deleted_entries[key] + else: + msg = f"object ID {key} cannot be deleted because it doesn't exist" + raise IndexError(msg) + + def __contains__(self, key): + return key in self.existing_entries or key in self.new_entries + + def __len__(self) -> int: + return len( + set(self.existing_entries.keys()) + | set(self.new_entries.keys()) + | set(self.deleted_entries.keys()) + ) + + def keys(self): + return ( + set(self.existing_entries.keys()) - set(self.deleted_entries.keys()) + ) | set(self.new_entries.keys()) + + def write(self, f): + keys = sorted(set(self.new_entries.keys()) | set(self.deleted_entries.keys())) + deleted_keys = sorted(set(self.deleted_entries.keys())) + startxref = f.tell() + f.write(b"xref\n") + while keys: + # find a contiguous sequence of object IDs + prev = None + for index, key in enumerate(keys): + if prev is None or prev + 1 == key: + prev = key + else: + contiguous_keys = keys[:index] + keys = keys[index:] + break + else: + contiguous_keys = keys + keys = None + f.write(b"%d %d\n" % (contiguous_keys[0], len(contiguous_keys))) + for object_id in contiguous_keys: + if object_id in self.new_entries: + f.write(b"%010d %05d n \n" % self.new_entries[object_id]) + else: + this_deleted_object_id = deleted_keys.pop(0) + check_format_condition( + object_id == this_deleted_object_id, + f"expected the next deleted object ID to be {object_id}, " + f"instead found {this_deleted_object_id}", + ) + try: + next_in_linked_list = deleted_keys[0] + except IndexError: + next_in_linked_list = 0 + f.write( + b"%010d %05d f \n" + % (next_in_linked_list, self.deleted_entries[object_id]) + ) + return startxref + + +class PdfName: + def __init__(self, name): + if isinstance(name, PdfName): + self.name = name.name + elif isinstance(name, bytes): + self.name = name + else: + self.name = name.encode("us-ascii") + + def name_as_str(self) -> str: + return self.name.decode("us-ascii") + + def __eq__(self, other): + return ( + isinstance(other, PdfName) and other.name == self.name + ) or other == self.name + + def __hash__(self) -> int: + return hash(self.name) + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({repr(self.name)})" + + @classmethod + def from_pdf_stream(cls, data): + return cls(PdfParser.interpret_name(data)) + + allowed_chars = set(range(33, 127)) - {ord(c) for c in "#%/()<>[]{}"} + + def __bytes__(self) -> bytes: + result = bytearray(b"/") + for b in self.name: + if b in self.allowed_chars: + result.append(b) + else: + result.extend(b"#%02X" % b) + return bytes(result) + + +class PdfArray(List[Any]): + def __bytes__(self) -> bytes: + return b"[ " + b" ".join(pdf_repr(x) for x in self) + b" ]" + + +if TYPE_CHECKING: + _DictBase = collections.UserDict[Union[str, bytes], Any] +else: + _DictBase = collections.UserDict + + +class PdfDict(_DictBase): + def __setattr__(self, key, value): + if key == "data": + collections.UserDict.__setattr__(self, key, value) + else: + self[key.encode("us-ascii")] = value + + def __getattr__(self, key): + try: + value = self[key.encode("us-ascii")] + except KeyError as e: + raise AttributeError(key) from e + if isinstance(value, bytes): + value = decode_text(value) + if key.endswith("Date"): + if value.startswith("D:"): + value = value[2:] + + relationship = "Z" + if len(value) > 17: + relationship = value[14] + offset = int(value[15:17]) * 60 + if len(value) > 20: + offset += int(value[18:20]) + + format = "%Y%m%d%H%M%S"[: len(value) - 2] + value = time.strptime(value[: len(format) + 2], format) + if relationship in ["+", "-"]: + offset *= 60 + if relationship == "+": + offset *= -1 + value = time.gmtime(calendar.timegm(value) + offset) + return value + + def __bytes__(self) -> bytes: + out = bytearray(b"<<") + for key, value in self.items(): + if value is None: + continue + value = pdf_repr(value) + out.extend(b"\n") + out.extend(bytes(PdfName(key))) + out.extend(b" ") + out.extend(value) + out.extend(b"\n>>") + return bytes(out) + + +class PdfBinary: + def __init__(self, data): + self.data = data + + def __bytes__(self) -> bytes: + return b"<%s>" % b"".join(b"%02X" % b for b in self.data) + + +class PdfStream: + def __init__(self, dictionary, buf): + self.dictionary = dictionary + self.buf = buf + + def decode(self): + try: + filter = self.dictionary.Filter + except AttributeError: + return self.buf + if filter == b"FlateDecode": + try: + expected_length = self.dictionary.DL + except AttributeError: + expected_length = self.dictionary.Length + return zlib.decompress(self.buf, bufsize=int(expected_length)) + else: + msg = f"stream filter {repr(self.dictionary.Filter)} unknown/unsupported" + raise NotImplementedError(msg) + + +def pdf_repr(x): + if x is True: + return b"true" + elif x is False: + return b"false" + elif x is None: + return b"null" + elif isinstance(x, (PdfName, PdfDict, PdfArray, PdfBinary)): + return bytes(x) + elif isinstance(x, (int, float)): + return str(x).encode("us-ascii") + elif isinstance(x, time.struct_time): + return b"(D:" + time.strftime("%Y%m%d%H%M%SZ", x).encode("us-ascii") + b")" + elif isinstance(x, dict): + return bytes(PdfDict(x)) + elif isinstance(x, list): + return bytes(PdfArray(x)) + elif isinstance(x, str): + return pdf_repr(encode_text(x)) + elif isinstance(x, bytes): + # XXX escape more chars? handle binary garbage + x = x.replace(b"\\", b"\\\\") + x = x.replace(b"(", b"\\(") + x = x.replace(b")", b"\\)") + return b"(" + x + b")" + else: + return bytes(x) + + +class PdfParser: + """Based on + https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/PDF32000_2008.pdf + Supports PDF up to 1.4 + """ + + def __init__(self, filename=None, f=None, buf=None, start_offset=0, mode="rb"): + if buf and f: + msg = "specify buf or f or filename, but not both buf and f" + raise RuntimeError(msg) + self.filename = filename + self.buf = buf + self.f = f + self.start_offset = start_offset + self.should_close_buf = False + self.should_close_file = False + if filename is not None and f is None: + self.f = f = open(filename, mode) + self.should_close_file = True + if f is not None: + self.buf = buf = self.get_buf_from_file(f) + self.should_close_buf = True + if not filename and hasattr(f, "name"): + self.filename = f.name + self.cached_objects = {} + if buf: + self.read_pdf_info() + else: + self.file_size_total = self.file_size_this = 0 + self.root = PdfDict() + self.root_ref = None + self.info = PdfDict() + self.info_ref = None + self.page_tree_root = {} + self.pages = [] + self.orig_pages = [] + self.pages_ref = None + self.last_xref_section_offset = None + self.trailer_dict = {} + self.xref_table = XrefTable() + self.xref_table.reading_finished = True + if f: + self.seek_end() + + def __enter__(self) -> PdfParser: + return self + + def __exit__(self, *args: object) -> None: + self.close() + + def start_writing(self) -> None: + self.close_buf() + self.seek_end() + + def close_buf(self) -> None: + try: + self.buf.close() + except AttributeError: + pass + self.buf = None + + def close(self) -> None: + if self.should_close_buf: + self.close_buf() + if self.f is not None and self.should_close_file: + self.f.close() + self.f = None + + def seek_end(self) -> None: + self.f.seek(0, os.SEEK_END) + + def write_header(self) -> None: + self.f.write(b"%PDF-1.4\n") + + def write_comment(self, s): + self.f.write(f"% {s}\n".encode()) + + def write_catalog(self) -> IndirectReference: + self.del_root() + self.root_ref = self.next_object_id(self.f.tell()) + self.pages_ref = self.next_object_id(0) + self.rewrite_pages() + self.write_obj(self.root_ref, Type=PdfName(b"Catalog"), Pages=self.pages_ref) + self.write_obj( + self.pages_ref, + Type=PdfName(b"Pages"), + Count=len(self.pages), + Kids=self.pages, + ) + return self.root_ref + + def rewrite_pages(self) -> None: + pages_tree_nodes_to_delete = [] + for i, page_ref in enumerate(self.orig_pages): + page_info = self.cached_objects[page_ref] + del self.xref_table[page_ref.object_id] + pages_tree_nodes_to_delete.append(page_info[PdfName(b"Parent")]) + if page_ref not in self.pages: + # the page has been deleted + continue + # make dict keys into strings for passing to write_page + stringified_page_info = {} + for key, value in page_info.items(): + # key should be a PdfName + stringified_page_info[key.name_as_str()] = value + stringified_page_info["Parent"] = self.pages_ref + new_page_ref = self.write_page(None, **stringified_page_info) + for j, cur_page_ref in enumerate(self.pages): + if cur_page_ref == page_ref: + # replace the page reference with the new one + self.pages[j] = new_page_ref + # delete redundant Pages tree nodes from xref table + for pages_tree_node_ref in pages_tree_nodes_to_delete: + while pages_tree_node_ref: + pages_tree_node = self.cached_objects[pages_tree_node_ref] + if pages_tree_node_ref.object_id in self.xref_table: + del self.xref_table[pages_tree_node_ref.object_id] + pages_tree_node_ref = pages_tree_node.get(b"Parent", None) + self.orig_pages = [] + + def write_xref_and_trailer(self, new_root_ref=None): + if new_root_ref: + self.del_root() + self.root_ref = new_root_ref + if self.info: + self.info_ref = self.write_obj(None, self.info) + start_xref = self.xref_table.write(self.f) + num_entries = len(self.xref_table) + trailer_dict = {b"Root": self.root_ref, b"Size": num_entries} + if self.last_xref_section_offset is not None: + trailer_dict[b"Prev"] = self.last_xref_section_offset + if self.info: + trailer_dict[b"Info"] = self.info_ref + self.last_xref_section_offset = start_xref + self.f.write( + b"trailer\n" + + bytes(PdfDict(trailer_dict)) + + b"\nstartxref\n%d\n%%%%EOF" % start_xref + ) + + def write_page(self, ref, *objs, **dict_obj): + if isinstance(ref, int): + ref = self.pages[ref] + if "Type" not in dict_obj: + dict_obj["Type"] = PdfName(b"Page") + if "Parent" not in dict_obj: + dict_obj["Parent"] = self.pages_ref + return self.write_obj(ref, *objs, **dict_obj) + + def write_obj(self, ref, *objs, **dict_obj): + f = self.f + if ref is None: + ref = self.next_object_id(f.tell()) + else: + self.xref_table[ref.object_id] = (f.tell(), ref.generation) + f.write(bytes(IndirectObjectDef(*ref))) + stream = dict_obj.pop("stream", None) + if stream is not None: + dict_obj["Length"] = len(stream) + if dict_obj: + f.write(pdf_repr(dict_obj)) + for obj in objs: + f.write(pdf_repr(obj)) + if stream is not None: + f.write(b"stream\n") + f.write(stream) + f.write(b"\nendstream\n") + f.write(b"endobj\n") + return ref + + def del_root(self) -> None: + if self.root_ref is None: + return + del self.xref_table[self.root_ref.object_id] + del self.xref_table[self.root[b"Pages"].object_id] + + @staticmethod + def get_buf_from_file(f): + if hasattr(f, "getbuffer"): + return f.getbuffer() + elif hasattr(f, "getvalue"): + return f.getvalue() + else: + try: + return mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) + except ValueError: # cannot mmap an empty file + return b"" + + def read_pdf_info(self) -> None: + self.file_size_total = len(self.buf) + self.file_size_this = self.file_size_total - self.start_offset + self.read_trailer() + self.root_ref = self.trailer_dict[b"Root"] + self.info_ref = self.trailer_dict.get(b"Info", None) + self.root = PdfDict(self.read_indirect(self.root_ref)) + if self.info_ref is None: + self.info = PdfDict() + else: + self.info = PdfDict(self.read_indirect(self.info_ref)) + check_format_condition(b"Type" in self.root, "/Type missing in Root") + check_format_condition( + self.root[b"Type"] == b"Catalog", "/Type in Root is not /Catalog" + ) + check_format_condition(b"Pages" in self.root, "/Pages missing in Root") + check_format_condition( + isinstance(self.root[b"Pages"], IndirectReference), + "/Pages in Root is not an indirect reference", + ) + self.pages_ref = self.root[b"Pages"] + self.page_tree_root = self.read_indirect(self.pages_ref) + self.pages = self.linearize_page_tree(self.page_tree_root) + # save the original list of page references + # in case the user modifies, adds or deletes some pages + # and we need to rewrite the pages and their list + self.orig_pages = self.pages[:] + + def next_object_id(self, offset=None): + try: + # TODO: support reuse of deleted objects + reference = IndirectReference(max(self.xref_table.keys()) + 1, 0) + except ValueError: + reference = IndirectReference(1, 0) + if offset is not None: + self.xref_table[reference.object_id] = (offset, 0) + return reference + + delimiter = rb"[][()<>{}/%]" + delimiter_or_ws = rb"[][()<>{}/%\000\011\012\014\015\040]" + whitespace = rb"[\000\011\012\014\015\040]" + whitespace_or_hex = rb"[\000\011\012\014\015\0400-9a-fA-F]" + whitespace_optional = whitespace + b"*" + whitespace_mandatory = whitespace + b"+" + # No "\012" aka "\n" or "\015" aka "\r": + whitespace_optional_no_nl = rb"[\000\011\014\040]*" + newline_only = rb"[\r\n]+" + newline = whitespace_optional_no_nl + newline_only + whitespace_optional_no_nl + re_trailer_end = re.compile( + whitespace_mandatory + + rb"trailer" + + whitespace_optional + + rb"<<(.*>>)" + + newline + + rb"startxref" + + newline + + rb"([0-9]+)" + + newline + + rb"%%EOF" + + whitespace_optional + + rb"$", + re.DOTALL, + ) + re_trailer_prev = re.compile( + whitespace_optional + + rb"trailer" + + whitespace_optional + + rb"<<(.*?>>)" + + newline + + rb"startxref" + + newline + + rb"([0-9]+)" + + newline + + rb"%%EOF" + + whitespace_optional, + re.DOTALL, + ) + + def read_trailer(self): + search_start_offset = len(self.buf) - 16384 + if search_start_offset < self.start_offset: + search_start_offset = self.start_offset + m = self.re_trailer_end.search(self.buf, search_start_offset) + check_format_condition(m, "trailer end not found") + # make sure we found the LAST trailer + last_match = m + while m: + last_match = m + m = self.re_trailer_end.search(self.buf, m.start() + 16) + if not m: + m = last_match + trailer_data = m.group(1) + self.last_xref_section_offset = int(m.group(2)) + self.trailer_dict = self.interpret_trailer(trailer_data) + self.xref_table = XrefTable() + self.read_xref_table(xref_section_offset=self.last_xref_section_offset) + if b"Prev" in self.trailer_dict: + self.read_prev_trailer(self.trailer_dict[b"Prev"]) + + def read_prev_trailer(self, xref_section_offset): + trailer_offset = self.read_xref_table(xref_section_offset=xref_section_offset) + m = self.re_trailer_prev.search( + self.buf[trailer_offset : trailer_offset + 16384] + ) + check_format_condition(m, "previous trailer not found") + trailer_data = m.group(1) + check_format_condition( + int(m.group(2)) == xref_section_offset, + "xref section offset in previous trailer doesn't match what was expected", + ) + trailer_dict = self.interpret_trailer(trailer_data) + if b"Prev" in trailer_dict: + self.read_prev_trailer(trailer_dict[b"Prev"]) + + re_whitespace_optional = re.compile(whitespace_optional) + re_name = re.compile( + whitespace_optional + + rb"/([!-$&'*-.0-;=?-Z\\^-z|~]+)(?=" + + delimiter_or_ws + + rb")" + ) + re_dict_start = re.compile(whitespace_optional + rb"<<") + re_dict_end = re.compile(whitespace_optional + rb">>" + whitespace_optional) + + @classmethod + def interpret_trailer(cls, trailer_data): + trailer = {} + offset = 0 + while True: + m = cls.re_name.match(trailer_data, offset) + if not m: + m = cls.re_dict_end.match(trailer_data, offset) + check_format_condition( + m and m.end() == len(trailer_data), + "name not found in trailer, remaining data: " + + repr(trailer_data[offset:]), + ) + break + key = cls.interpret_name(m.group(1)) + value, offset = cls.get_value(trailer_data, m.end()) + trailer[key] = value + check_format_condition( + b"Size" in trailer and isinstance(trailer[b"Size"], int), + "/Size not in trailer or not an integer", + ) + check_format_condition( + b"Root" in trailer and isinstance(trailer[b"Root"], IndirectReference), + "/Root not in trailer or not an indirect reference", + ) + return trailer + + re_hashes_in_name = re.compile(rb"([^#]*)(#([0-9a-fA-F]{2}))?") + + @classmethod + def interpret_name(cls, raw, as_text=False): + name = b"" + for m in cls.re_hashes_in_name.finditer(raw): + if m.group(3): + name += m.group(1) + bytearray.fromhex(m.group(3).decode("us-ascii")) + else: + name += m.group(1) + if as_text: + return name.decode("utf-8") + else: + return bytes(name) + + re_null = re.compile(whitespace_optional + rb"null(?=" + delimiter_or_ws + rb")") + re_true = re.compile(whitespace_optional + rb"true(?=" + delimiter_or_ws + rb")") + re_false = re.compile(whitespace_optional + rb"false(?=" + delimiter_or_ws + rb")") + re_int = re.compile( + whitespace_optional + rb"([-+]?[0-9]+)(?=" + delimiter_or_ws + rb")" + ) + re_real = re.compile( + whitespace_optional + + rb"([-+]?([0-9]+\.[0-9]*|[0-9]*\.[0-9]+))(?=" + + delimiter_or_ws + + rb")" + ) + re_array_start = re.compile(whitespace_optional + rb"\[") + re_array_end = re.compile(whitespace_optional + rb"]") + re_string_hex = re.compile( + whitespace_optional + rb"<(" + whitespace_or_hex + rb"*)>" + ) + re_string_lit = re.compile(whitespace_optional + rb"\(") + re_indirect_reference = re.compile( + whitespace_optional + + rb"([-+]?[0-9]+)" + + whitespace_mandatory + + rb"([-+]?[0-9]+)" + + whitespace_mandatory + + rb"R(?=" + + delimiter_or_ws + + rb")" + ) + re_indirect_def_start = re.compile( + whitespace_optional + + rb"([-+]?[0-9]+)" + + whitespace_mandatory + + rb"([-+]?[0-9]+)" + + whitespace_mandatory + + rb"obj(?=" + + delimiter_or_ws + + rb")" + ) + re_indirect_def_end = re.compile( + whitespace_optional + rb"endobj(?=" + delimiter_or_ws + rb")" + ) + re_comment = re.compile( + rb"(" + whitespace_optional + rb"%[^\r\n]*" + newline + rb")*" + ) + re_stream_start = re.compile(whitespace_optional + rb"stream\r?\n") + re_stream_end = re.compile( + whitespace_optional + rb"endstream(?=" + delimiter_or_ws + rb")" + ) + + @classmethod + def get_value(cls, data, offset, expect_indirect=None, max_nesting=-1): + if max_nesting == 0: + return None, None + m = cls.re_comment.match(data, offset) + if m: + offset = m.end() + m = cls.re_indirect_def_start.match(data, offset) + if m: + check_format_condition( + int(m.group(1)) > 0, + "indirect object definition: object ID must be greater than 0", + ) + check_format_condition( + int(m.group(2)) >= 0, + "indirect object definition: generation must be non-negative", + ) + check_format_condition( + expect_indirect is None + or expect_indirect + == IndirectReference(int(m.group(1)), int(m.group(2))), + "indirect object definition different than expected", + ) + object, offset = cls.get_value(data, m.end(), max_nesting=max_nesting - 1) + if offset is None: + return object, None + m = cls.re_indirect_def_end.match(data, offset) + check_format_condition(m, "indirect object definition end not found") + return object, m.end() + check_format_condition( + not expect_indirect, "indirect object definition not found" + ) + m = cls.re_indirect_reference.match(data, offset) + if m: + check_format_condition( + int(m.group(1)) > 0, + "indirect object reference: object ID must be greater than 0", + ) + check_format_condition( + int(m.group(2)) >= 0, + "indirect object reference: generation must be non-negative", + ) + return IndirectReference(int(m.group(1)), int(m.group(2))), m.end() + m = cls.re_dict_start.match(data, offset) + if m: + offset = m.end() + result = {} + m = cls.re_dict_end.match(data, offset) + while not m: + key, offset = cls.get_value(data, offset, max_nesting=max_nesting - 1) + if offset is None: + return result, None + value, offset = cls.get_value(data, offset, max_nesting=max_nesting - 1) + result[key] = value + if offset is None: + return result, None + m = cls.re_dict_end.match(data, offset) + offset = m.end() + m = cls.re_stream_start.match(data, offset) + if m: + try: + stream_len_str = result.get(b"Length") + stream_len = int(stream_len_str) + except (TypeError, ValueError) as e: + msg = f"bad or missing Length in stream dict ({stream_len_str})" + raise PdfFormatError(msg) from e + stream_data = data[m.end() : m.end() + stream_len] + m = cls.re_stream_end.match(data, m.end() + stream_len) + check_format_condition(m, "stream end not found") + offset = m.end() + result = PdfStream(PdfDict(result), stream_data) + else: + result = PdfDict(result) + return result, offset + m = cls.re_array_start.match(data, offset) + if m: + offset = m.end() + result = [] + m = cls.re_array_end.match(data, offset) + while not m: + value, offset = cls.get_value(data, offset, max_nesting=max_nesting - 1) + result.append(value) + if offset is None: + return result, None + m = cls.re_array_end.match(data, offset) + return result, m.end() + m = cls.re_null.match(data, offset) + if m: + return None, m.end() + m = cls.re_true.match(data, offset) + if m: + return True, m.end() + m = cls.re_false.match(data, offset) + if m: + return False, m.end() + m = cls.re_name.match(data, offset) + if m: + return PdfName(cls.interpret_name(m.group(1))), m.end() + m = cls.re_int.match(data, offset) + if m: + return int(m.group(1)), m.end() + m = cls.re_real.match(data, offset) + if m: + # XXX Decimal instead of float??? + return float(m.group(1)), m.end() + m = cls.re_string_hex.match(data, offset) + if m: + # filter out whitespace + hex_string = bytearray( + b for b in m.group(1) if b in b"0123456789abcdefABCDEF" + ) + if len(hex_string) % 2 == 1: + # append a 0 if the length is not even - yes, at the end + hex_string.append(ord(b"0")) + return bytearray.fromhex(hex_string.decode("us-ascii")), m.end() + m = cls.re_string_lit.match(data, offset) + if m: + return cls.get_literal_string(data, m.end()) + # return None, offset # fallback (only for debugging) + msg = f"unrecognized object: {repr(data[offset : offset + 32])}" + raise PdfFormatError(msg) + + re_lit_str_token = re.compile( + rb"(\\[nrtbf()\\])|(\\[0-9]{1,3})|(\\(\r\n|\r|\n))|(\r\n|\r|\n)|(\()|(\))" + ) + escaped_chars = { + b"n": b"\n", + b"r": b"\r", + b"t": b"\t", + b"b": b"\b", + b"f": b"\f", + b"(": b"(", + b")": b")", + b"\\": b"\\", + ord(b"n"): b"\n", + ord(b"r"): b"\r", + ord(b"t"): b"\t", + ord(b"b"): b"\b", + ord(b"f"): b"\f", + ord(b"("): b"(", + ord(b")"): b")", + ord(b"\\"): b"\\", + } + + @classmethod + def get_literal_string(cls, data, offset): + nesting_depth = 0 + result = bytearray() + for m in cls.re_lit_str_token.finditer(data, offset): + result.extend(data[offset : m.start()]) + if m.group(1): + result.extend(cls.escaped_chars[m.group(1)[1]]) + elif m.group(2): + result.append(int(m.group(2)[1:], 8)) + elif m.group(3): + pass + elif m.group(5): + result.extend(b"\n") + elif m.group(6): + result.extend(b"(") + nesting_depth += 1 + elif m.group(7): + if nesting_depth == 0: + return bytes(result), m.end() + result.extend(b")") + nesting_depth -= 1 + offset = m.end() + msg = "unfinished literal string" + raise PdfFormatError(msg) + + re_xref_section_start = re.compile(whitespace_optional + rb"xref" + newline) + re_xref_subsection_start = re.compile( + whitespace_optional + + rb"([0-9]+)" + + whitespace_mandatory + + rb"([0-9]+)" + + whitespace_optional + + newline_only + ) + re_xref_entry = re.compile(rb"([0-9]{10}) ([0-9]{5}) ([fn])( \r| \n|\r\n)") + + def read_xref_table(self, xref_section_offset): + subsection_found = False + m = self.re_xref_section_start.match( + self.buf, xref_section_offset + self.start_offset + ) + check_format_condition(m, "xref section start not found") + offset = m.end() + while True: + m = self.re_xref_subsection_start.match(self.buf, offset) + if not m: + check_format_condition( + subsection_found, "xref subsection start not found" + ) + break + subsection_found = True + offset = m.end() + first_object = int(m.group(1)) + num_objects = int(m.group(2)) + for i in range(first_object, first_object + num_objects): + m = self.re_xref_entry.match(self.buf, offset) + check_format_condition(m, "xref entry not found") + offset = m.end() + is_free = m.group(3) == b"f" + if not is_free: + generation = int(m.group(2)) + new_entry = (int(m.group(1)), generation) + if i not in self.xref_table: + self.xref_table[i] = new_entry + return offset + + def read_indirect(self, ref, max_nesting=-1): + offset, generation = self.xref_table[ref[0]] + check_format_condition( + generation == ref[1], + f"expected to find generation {ref[1]} for object ID {ref[0]} in xref " + f"table, instead found generation {generation} at offset {offset}", + ) + value = self.get_value( + self.buf, + offset + self.start_offset, + expect_indirect=IndirectReference(*ref), + max_nesting=max_nesting, + )[0] + self.cached_objects[ref] = value + return value + + def linearize_page_tree(self, node=None): + if node is None: + node = self.page_tree_root + check_format_condition( + node[b"Type"] == b"Pages", "/Type of page tree node is not /Pages" + ) + pages = [] + for kid in node[b"Kids"]: + kid_object = self.read_indirect(kid) + if kid_object[b"Type"] == b"Page": + pages.append(kid) + else: + pages.extend(self.linearize_page_tree(node=kid_object)) + return pages diff --git a/src/PIL/PixarImagePlugin.py b/src/PIL/PixarImagePlugin.py index 887b6568b..f5d1730fe 100644 --- a/src/PIL/PixarImagePlugin.py +++ b/src/PIL/PixarImagePlugin.py @@ -1,72 +1,72 @@ -# -# The Python Imaging Library. -# $Id$ -# -# PIXAR raster support for PIL -# -# history: -# 97-01-29 fl Created -# -# notes: -# This is incomplete; it is based on a few samples created with -# Photoshop 2.5 and 3.0, and a summary description provided by -# Greg Coats . Hopefully, "L" and -# "RGBA" support will be added in future versions. -# -# Copyright (c) Secret Labs AB 1997. -# Copyright (c) Fredrik Lundh 1997. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -from . import Image, ImageFile -from ._binary import i16le as i16 - -# -# helpers - - -def _accept(prefix: bytes) -> bool: - return prefix[:4] == b"\200\350\000\000" - - -## -# Image plugin for PIXAR raster images. - - -class PixarImageFile(ImageFile.ImageFile): - format = "PIXAR" - format_description = "PIXAR raster image" - - def _open(self) -> None: - # assuming a 4-byte magic label - assert self.fp is not None - - s = self.fp.read(4) - if not _accept(s): - msg = "not a PIXAR file" - raise SyntaxError(msg) - - # read rest of header - s = s + self.fp.read(508) - - self._size = i16(s, 418), i16(s, 416) - - # get channel/depth descriptions - mode = i16(s, 424), i16(s, 426) - - if mode == (14, 2): - self._mode = "RGB" - # FIXME: to be continued... - - # create tile descriptor (assuming "dumped") - self.tile = [("raw", (0, 0) + self.size, 1024, (self.mode, 0, 1))] - - -# -# -------------------------------------------------------------------- - -Image.register_open(PixarImageFile.format, PixarImageFile, _accept) - -Image.register_extension(PixarImageFile.format, ".pxr") +# +# The Python Imaging Library. +# $Id$ +# +# PIXAR raster support for PIL +# +# history: +# 97-01-29 fl Created +# +# notes: +# This is incomplete; it is based on a few samples created with +# Photoshop 2.5 and 3.0, and a summary description provided by +# Greg Coats . Hopefully, "L" and +# "RGBA" support will be added in future versions. +# +# Copyright (c) Secret Labs AB 1997. +# Copyright (c) Fredrik Lundh 1997. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +from . import Image, ImageFile +from ._binary import i16le as i16 + +# +# helpers + + +def _accept(prefix: bytes) -> bool: + return prefix[:4] == b"\200\350\000\000" + + +## +# Image plugin for PIXAR raster images. + + +class PixarImageFile(ImageFile.ImageFile): + format = "PIXAR" + format_description = "PIXAR raster image" + + def _open(self) -> None: + # assuming a 4-byte magic label + assert self.fp is not None + + s = self.fp.read(4) + if not _accept(s): + msg = "not a PIXAR file" + raise SyntaxError(msg) + + # read rest of header + s = s + self.fp.read(508) + + self._size = i16(s, 418), i16(s, 416) + + # get channel/depth descriptions + mode = i16(s, 424), i16(s, 426) + + if mode == (14, 2): + self._mode = "RGB" + # FIXME: to be continued... + + # create tile descriptor (assuming "dumped") + self.tile = [("raw", (0, 0) + self.size, 1024, (self.mode, 0, 1))] + + +# +# -------------------------------------------------------------------- + +Image.register_open(PixarImageFile.format, PixarImageFile, _accept) + +Image.register_extension(PixarImageFile.format, ".pxr") diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index ba9598065..35dceec13 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -1,1479 +1,1479 @@ -# -# The Python Imaging Library. -# $Id$ -# -# PNG support code -# -# See "PNG (Portable Network Graphics) Specification, version 1.0; -# W3C Recommendation", 1996-10-01, Thomas Boutell (ed.). -# -# history: -# 1996-05-06 fl Created (couldn't resist it) -# 1996-12-14 fl Upgraded, added read and verify support (0.2) -# 1996-12-15 fl Separate PNG stream parser -# 1996-12-29 fl Added write support, added getchunks -# 1996-12-30 fl Eliminated circular references in decoder (0.3) -# 1998-07-12 fl Read/write 16-bit images as mode I (0.4) -# 2001-02-08 fl Added transparency support (from Zircon) (0.5) -# 2001-04-16 fl Don't close data source in "open" method (0.6) -# 2004-02-24 fl Don't even pretend to support interlaced files (0.7) -# 2004-08-31 fl Do basic sanity check on chunk identifiers (0.8) -# 2004-09-20 fl Added PngInfo chunk container -# 2004-12-18 fl Added DPI read support (based on code by Niki Spahiev) -# 2008-08-13 fl Added tRNS support for RGB images -# 2009-03-06 fl Support for preserving ICC profiles (by Florian Hoech) -# 2009-03-08 fl Added zTXT support (from Lowell Alleman) -# 2009-03-29 fl Read interlaced PNG files (from Conrado Porto Lopes Gouvua) -# -# Copyright (c) 1997-2009 by Secret Labs AB -# Copyright (c) 1996 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import itertools -import logging -import re -import struct -import warnings -import zlib -from enum import IntEnum -from typing import IO, Any - -from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence -from ._binary import i16be as i16 -from ._binary import i32be as i32 -from ._binary import o8 -from ._binary import o16be as o16 -from ._binary import o32be as o32 - -logger = logging.getLogger(__name__) - -is_cid = re.compile(rb"\w\w\w\w").match - - -_MAGIC = b"\211PNG\r\n\032\n" - - -_MODES = { - # supported bits/color combinations, and corresponding modes/rawmodes - # Grayscale - (1, 0): ("1", "1"), - (2, 0): ("L", "L;2"), - (4, 0): ("L", "L;4"), - (8, 0): ("L", "L"), - (16, 0): ("I;16", "I;16B"), - # Truecolour - (8, 2): ("RGB", "RGB"), - (16, 2): ("RGB", "RGB;16B"), - # Indexed-colour - (1, 3): ("P", "P;1"), - (2, 3): ("P", "P;2"), - (4, 3): ("P", "P;4"), - (8, 3): ("P", "P"), - # Grayscale with alpha - (8, 4): ("LA", "LA"), - (16, 4): ("RGBA", "LA;16B"), # LA;16B->LA not yet available - # Truecolour with alpha - (8, 6): ("RGBA", "RGBA"), - (16, 6): ("RGBA", "RGBA;16B"), -} - - -_simple_palette = re.compile(b"^\xff*\x00\xff*$") - -MAX_TEXT_CHUNK = ImageFile.SAFEBLOCK -""" -Maximum decompressed size for a iTXt or zTXt chunk. -Eliminates decompression bombs where compressed chunks can expand 1000x. -See :ref:`Text in PNG File Format`. -""" -MAX_TEXT_MEMORY = 64 * MAX_TEXT_CHUNK -""" -Set the maximum total text chunk size. -See :ref:`Text in PNG File Format`. -""" - - -# APNG frame disposal modes -class Disposal(IntEnum): - OP_NONE = 0 - """ - No disposal is done on this frame before rendering the next frame. - See :ref:`Saving APNG sequences`. - """ - OP_BACKGROUND = 1 - """ - This frame’s modified region is cleared to fully transparent black before rendering - the next frame. - See :ref:`Saving APNG sequences`. - """ - OP_PREVIOUS = 2 - """ - This frame’s modified region is reverted to the previous frame’s contents before - rendering the next frame. - See :ref:`Saving APNG sequences`. - """ - - -# APNG frame blend modes -class Blend(IntEnum): - OP_SOURCE = 0 - """ - All color components of this frame, including alpha, overwrite the previous output - image contents. - See :ref:`Saving APNG sequences`. - """ - OP_OVER = 1 - """ - This frame should be alpha composited with the previous output image contents. - See :ref:`Saving APNG sequences`. - """ - - -def _safe_zlib_decompress(s): - dobj = zlib.decompressobj() - plaintext = dobj.decompress(s, MAX_TEXT_CHUNK) - if dobj.unconsumed_tail: - msg = "Decompressed Data Too Large" - raise ValueError(msg) - return plaintext - - -def _crc32(data, seed=0): - return zlib.crc32(data, seed) & 0xFFFFFFFF - - -# -------------------------------------------------------------------- -# Support classes. Suitable for PNG and related formats like MNG etc. - - -class ChunkStream: - def __init__(self, fp: IO[bytes]) -> None: - self.fp: IO[bytes] | None = fp - self.queue: list[tuple[bytes, int, int]] | None = [] - - def read(self) -> tuple[bytes, int, int]: - """Fetch a new chunk. Returns header information.""" - cid = None - - assert self.fp is not None - if self.queue: - cid, pos, length = self.queue.pop() - self.fp.seek(pos) - else: - s = self.fp.read(8) - cid = s[4:] - pos = self.fp.tell() - length = i32(s) - - if not is_cid(cid): - if not ImageFile.LOAD_TRUNCATED_IMAGES: - msg = f"broken PNG file (chunk {repr(cid)})" - raise SyntaxError(msg) - - return cid, pos, length - - def __enter__(self) -> ChunkStream: - return self - - def __exit__(self, *args: object) -> None: - self.close() - - def close(self) -> None: - self.queue = self.fp = None - - def push(self, cid: bytes, pos: int, length: int) -> None: - assert self.queue is not None - self.queue.append((cid, pos, length)) - - def call(self, cid, pos, length): - """Call the appropriate chunk handler""" - - logger.debug("STREAM %r %s %s", cid, pos, length) - return getattr(self, f"chunk_{cid.decode('ascii')}")(pos, length) - - def crc(self, cid: bytes, data: bytes) -> None: - """Read and verify checksum""" - - # Skip CRC checks for ancillary chunks if allowed to load truncated - # images - # 5th byte of first char is 1 [specs, section 5.4] - if ImageFile.LOAD_TRUNCATED_IMAGES and (cid[0] >> 5 & 1): - self.crc_skip(cid, data) - return - - assert self.fp is not None - try: - crc1 = _crc32(data, _crc32(cid)) - crc2 = i32(self.fp.read(4)) - if crc1 != crc2: - msg = f"broken PNG file (bad header checksum in {repr(cid)})" - raise SyntaxError(msg) - except struct.error as e: - msg = f"broken PNG file (incomplete checksum in {repr(cid)})" - raise SyntaxError(msg) from e - - def crc_skip(self, cid: bytes, data: bytes) -> None: - """Read checksum""" - - assert self.fp is not None - self.fp.read(4) - - def verify(self, endchunk: bytes = b"IEND") -> list[bytes]: - # Simple approach; just calculate checksum for all remaining - # blocks. Must be called directly after open. - - cids = [] - - while True: - try: - cid, pos, length = self.read() - except struct.error as e: - msg = "truncated PNG file" - raise OSError(msg) from e - - if cid == endchunk: - break - self.crc(cid, ImageFile._safe_read(self.fp, length)) - cids.append(cid) - - return cids - - -class iTXt(str): - """ - Subclass of string to allow iTXt chunks to look like strings while - keeping their extra information - - """ - - @staticmethod - def __new__(cls, text, lang=None, tkey=None): - """ - :param cls: the class to use when creating the instance - :param text: value for this key - :param lang: language code - :param tkey: UTF-8 version of the key name - """ - - self = str.__new__(cls, text) - self.lang = lang - self.tkey = tkey - return self - - -class PngInfo: - """ - PNG chunk container (for use with save(pnginfo=)) - - """ - - def __init__(self): - self.chunks = [] - - def add(self, cid, data, after_idat=False): - """Appends an arbitrary chunk. Use with caution. - - :param cid: a byte string, 4 bytes long. - :param data: a byte string of the encoded data - :param after_idat: for use with private chunks. Whether the chunk - should be written after IDAT - - """ - - chunk = [cid, data] - if after_idat: - chunk.append(True) - self.chunks.append(tuple(chunk)) - - def add_itxt(self, key, value, lang="", tkey="", zip=False): - """Appends an iTXt chunk. - - :param key: latin-1 encodable text key name - :param value: value for this key - :param lang: language code - :param tkey: UTF-8 version of the key name - :param zip: compression flag - - """ - - if not isinstance(key, bytes): - key = key.encode("latin-1", "strict") - if not isinstance(value, bytes): - value = value.encode("utf-8", "strict") - if not isinstance(lang, bytes): - lang = lang.encode("utf-8", "strict") - if not isinstance(tkey, bytes): - tkey = tkey.encode("utf-8", "strict") - - if zip: - self.add( - b"iTXt", - key + b"\0\x01\0" + lang + b"\0" + tkey + b"\0" + zlib.compress(value), - ) - else: - self.add(b"iTXt", key + b"\0\0\0" + lang + b"\0" + tkey + b"\0" + value) - - def add_text(self, key, value, zip=False): - """Appends a text chunk. - - :param key: latin-1 encodable text key name - :param value: value for this key, text or an - :py:class:`PIL.PngImagePlugin.iTXt` instance - :param zip: compression flag - - """ - if isinstance(value, iTXt): - return self.add_itxt(key, value, value.lang, value.tkey, zip=zip) - - # The tEXt chunk stores latin-1 text - if not isinstance(value, bytes): - try: - value = value.encode("latin-1", "strict") - except UnicodeError: - return self.add_itxt(key, value, zip=zip) - - if not isinstance(key, bytes): - key = key.encode("latin-1", "strict") - - if zip: - self.add(b"zTXt", key + b"\0\0" + zlib.compress(value)) - else: - self.add(b"tEXt", key + b"\0" + value) - - -# -------------------------------------------------------------------- -# PNG image stream (IHDR/IEND) - - -class PngStream(ChunkStream): - def __init__(self, fp): - super().__init__(fp) - - # local copies of Image attributes - self.im_info = {} - self.im_text = {} - self.im_size = (0, 0) - self.im_mode = None - self.im_tile = None - self.im_palette = None - self.im_custom_mimetype = None - self.im_n_frames = None - self._seq_num = None - self.rewind_state = None - - self.text_memory = 0 - - def check_text_memory(self, chunklen: int) -> None: - self.text_memory += chunklen - if self.text_memory > MAX_TEXT_MEMORY: - msg = ( - "Too much memory used in text chunks: " - f"{self.text_memory}>MAX_TEXT_MEMORY" - ) - raise ValueError(msg) - - def save_rewind(self) -> None: - self.rewind_state = { - "info": self.im_info.copy(), - "tile": self.im_tile, - "seq_num": self._seq_num, - } - - def rewind(self) -> None: - self.im_info = self.rewind_state["info"].copy() - self.im_tile = self.rewind_state["tile"] - self._seq_num = self.rewind_state["seq_num"] - - def chunk_iCCP(self, pos: int, length: int) -> bytes: - # ICC profile - s = ImageFile._safe_read(self.fp, length) - # according to PNG spec, the iCCP chunk contains: - # Profile name 1-79 bytes (character string) - # Null separator 1 byte (null character) - # Compression method 1 byte (0) - # Compressed profile n bytes (zlib with deflate compression) - i = s.find(b"\0") - logger.debug("iCCP profile name %r", s[:i]) - comp_method = s[i + 1] - logger.debug("Compression method %s", comp_method) - if comp_method != 0: - msg = f"Unknown compression method {comp_method} in iCCP chunk" - raise SyntaxError(msg) - try: - icc_profile = _safe_zlib_decompress(s[i + 2 :]) - except ValueError: - if ImageFile.LOAD_TRUNCATED_IMAGES: - icc_profile = None - else: - raise - except zlib.error: - icc_profile = None # FIXME - self.im_info["icc_profile"] = icc_profile - return s - - def chunk_IHDR(self, pos: int, length: int) -> bytes: - # image header - s = ImageFile._safe_read(self.fp, length) - if length < 13: - if ImageFile.LOAD_TRUNCATED_IMAGES: - return s - msg = "Truncated IHDR chunk" - raise ValueError(msg) - self.im_size = i32(s, 0), i32(s, 4) - try: - self.im_mode, self.im_rawmode = _MODES[(s[8], s[9])] - except Exception: - pass - if s[12]: - self.im_info["interlace"] = 1 - if s[11]: - msg = "unknown filter category" - raise SyntaxError(msg) - return s - - def chunk_IDAT(self, pos, length): - # image data - if "bbox" in self.im_info: - tile = [("zip", self.im_info["bbox"], pos, self.im_rawmode)] - else: - if self.im_n_frames is not None: - self.im_info["default_image"] = True - tile = [("zip", (0, 0) + self.im_size, pos, self.im_rawmode)] - self.im_tile = tile - self.im_idat = length - msg = "image data found" - raise EOFError(msg) - - def chunk_IEND(self, pos, length): - msg = "end of PNG image" - raise EOFError(msg) - - def chunk_PLTE(self, pos: int, length: int) -> bytes: - # palette - s = ImageFile._safe_read(self.fp, length) - if self.im_mode == "P": - self.im_palette = "RGB", s - return s - - def chunk_tRNS(self, pos: int, length: int) -> bytes: - # transparency - s = ImageFile._safe_read(self.fp, length) - if self.im_mode == "P": - if _simple_palette.match(s): - # tRNS contains only one full-transparent entry, - # other entries are full opaque - i = s.find(b"\0") - if i >= 0: - self.im_info["transparency"] = i - else: - # otherwise, we have a byte string with one alpha value - # for each palette entry - self.im_info["transparency"] = s - elif self.im_mode in ("1", "L", "I;16"): - self.im_info["transparency"] = i16(s) - elif self.im_mode == "RGB": - self.im_info["transparency"] = i16(s), i16(s, 2), i16(s, 4) - return s - - def chunk_gAMA(self, pos: int, length: int) -> bytes: - # gamma setting - s = ImageFile._safe_read(self.fp, length) - self.im_info["gamma"] = i32(s) / 100000.0 - return s - - def chunk_cHRM(self, pos: int, length: int) -> bytes: - # chromaticity, 8 unsigned ints, actual value is scaled by 100,000 - # WP x,y, Red x,y, Green x,y Blue x,y - - s = ImageFile._safe_read(self.fp, length) - raw_vals = struct.unpack(">%dI" % (len(s) // 4), s) - self.im_info["chromaticity"] = tuple(elt / 100000.0 for elt in raw_vals) - return s - - def chunk_sRGB(self, pos: int, length: int) -> bytes: - # srgb rendering intent, 1 byte - # 0 perceptual - # 1 relative colorimetric - # 2 saturation - # 3 absolute colorimetric - - s = ImageFile._safe_read(self.fp, length) - if length < 1: - if ImageFile.LOAD_TRUNCATED_IMAGES: - return s - msg = "Truncated sRGB chunk" - raise ValueError(msg) - self.im_info["srgb"] = s[0] - return s - - def chunk_pHYs(self, pos: int, length: int) -> bytes: - # pixels per unit - s = ImageFile._safe_read(self.fp, length) - if length < 9: - if ImageFile.LOAD_TRUNCATED_IMAGES: - return s - msg = "Truncated pHYs chunk" - raise ValueError(msg) - px, py = i32(s, 0), i32(s, 4) - unit = s[8] - if unit == 1: # meter - dpi = px * 0.0254, py * 0.0254 - self.im_info["dpi"] = dpi - elif unit == 0: - self.im_info["aspect"] = px, py - return s - - def chunk_tEXt(self, pos: int, length: int) -> bytes: - # text - s = ImageFile._safe_read(self.fp, length) - try: - k, v = s.split(b"\0", 1) - except ValueError: - # fallback for broken tEXt tags - k = s - v = b"" - if k: - k = k.decode("latin-1", "strict") - v_str = v.decode("latin-1", "replace") - - self.im_info[k] = v if k == "exif" else v_str - self.im_text[k] = v_str - self.check_text_memory(len(v_str)) - - return s - - def chunk_zTXt(self, pos: int, length: int) -> bytes: - # compressed text - s = ImageFile._safe_read(self.fp, length) - try: - k, v = s.split(b"\0", 1) - except ValueError: - k = s - v = b"" - if v: - comp_method = v[0] - else: - comp_method = 0 - if comp_method != 0: - msg = f"Unknown compression method {comp_method} in zTXt chunk" - raise SyntaxError(msg) - try: - v = _safe_zlib_decompress(v[1:]) - except ValueError: - if ImageFile.LOAD_TRUNCATED_IMAGES: - v = b"" - else: - raise - except zlib.error: - v = b"" - - if k: - k = k.decode("latin-1", "strict") - v = v.decode("latin-1", "replace") - - self.im_info[k] = self.im_text[k] = v - self.check_text_memory(len(v)) - - return s - - def chunk_iTXt(self, pos: int, length: int) -> bytes: - # international text - r = s = ImageFile._safe_read(self.fp, length) - try: - k, r = r.split(b"\0", 1) - except ValueError: - return s - if len(r) < 2: - return s - cf, cm, r = r[0], r[1], r[2:] - try: - lang, tk, v = r.split(b"\0", 2) - except ValueError: - return s - if cf != 0: - if cm == 0: - try: - v = _safe_zlib_decompress(v) - except ValueError: - if ImageFile.LOAD_TRUNCATED_IMAGES: - return s - else: - raise - except zlib.error: - return s - else: - return s - try: - k = k.decode("latin-1", "strict") - lang = lang.decode("utf-8", "strict") - tk = tk.decode("utf-8", "strict") - v = v.decode("utf-8", "strict") - except UnicodeError: - return s - - self.im_info[k] = self.im_text[k] = iTXt(v, lang, tk) - self.check_text_memory(len(v)) - - return s - - def chunk_eXIf(self, pos: int, length: int) -> bytes: - s = ImageFile._safe_read(self.fp, length) - self.im_info["exif"] = b"Exif\x00\x00" + s - return s - - # APNG chunks - def chunk_acTL(self, pos: int, length: int) -> bytes: - s = ImageFile._safe_read(self.fp, length) - if length < 8: - if ImageFile.LOAD_TRUNCATED_IMAGES: - return s - msg = "APNG contains truncated acTL chunk" - raise ValueError(msg) - if self.im_n_frames is not None: - self.im_n_frames = None - warnings.warn("Invalid APNG, will use default PNG image if possible") - return s - n_frames = i32(s) - if n_frames == 0 or n_frames > 0x80000000: - warnings.warn("Invalid APNG, will use default PNG image if possible") - return s - self.im_n_frames = n_frames - self.im_info["loop"] = i32(s, 4) - self.im_custom_mimetype = "image/apng" - return s - - def chunk_fcTL(self, pos: int, length: int) -> bytes: - s = ImageFile._safe_read(self.fp, length) - if length < 26: - if ImageFile.LOAD_TRUNCATED_IMAGES: - return s - msg = "APNG contains truncated fcTL chunk" - raise ValueError(msg) - seq = i32(s) - if (self._seq_num is None and seq != 0) or ( - self._seq_num is not None and self._seq_num != seq - 1 - ): - msg = "APNG contains frame sequence errors" - raise SyntaxError(msg) - self._seq_num = seq - width, height = i32(s, 4), i32(s, 8) - px, py = i32(s, 12), i32(s, 16) - im_w, im_h = self.im_size - if px + width > im_w or py + height > im_h: - msg = "APNG contains invalid frames" - raise SyntaxError(msg) - self.im_info["bbox"] = (px, py, px + width, py + height) - delay_num, delay_den = i16(s, 20), i16(s, 22) - if delay_den == 0: - delay_den = 100 - self.im_info["duration"] = float(delay_num) / float(delay_den) * 1000 - self.im_info["disposal"] = s[24] - self.im_info["blend"] = s[25] - return s - - def chunk_fdAT(self, pos: int, length: int) -> bytes: - if length < 4: - if ImageFile.LOAD_TRUNCATED_IMAGES: - s = ImageFile._safe_read(self.fp, length) - return s - msg = "APNG contains truncated fDAT chunk" - raise ValueError(msg) - s = ImageFile._safe_read(self.fp, 4) - seq = i32(s) - if self._seq_num != seq - 1: - msg = "APNG contains frame sequence errors" - raise SyntaxError(msg) - self._seq_num = seq - return self.chunk_IDAT(pos + 4, length - 4) - - -# -------------------------------------------------------------------- -# PNG reader - - -def _accept(prefix: bytes) -> bool: - return prefix[:8] == _MAGIC - - -## -# Image plugin for PNG images. - - -class PngImageFile(ImageFile.ImageFile): - format = "PNG" - format_description = "Portable network graphics" - - def _open(self) -> None: - if not _accept(self.fp.read(8)): - msg = "not a PNG file" - raise SyntaxError(msg) - self._fp = self.fp - self.__frame = 0 - - # - # Parse headers up to the first IDAT or fDAT chunk - - self.private_chunks: list[tuple[bytes, bytes] | tuple[bytes, bytes, bool]] = [] - self.png: PngStream | None = PngStream(self.fp) - - while True: - # - # get next chunk - - cid, pos, length = self.png.read() - - try: - s = self.png.call(cid, pos, length) - except EOFError: - break - except AttributeError: - logger.debug("%r %s %s (unknown)", cid, pos, length) - s = ImageFile._safe_read(self.fp, length) - if cid[1:2].islower(): - self.private_chunks.append((cid, s)) - - self.png.crc(cid, s) - - # - # Copy relevant attributes from the PngStream. An alternative - # would be to let the PngStream class modify these attributes - # directly, but that introduces circular references which are - # difficult to break if things go wrong in the decoder... - # (believe me, I've tried ;-) - - self._mode = self.png.im_mode - self._size = self.png.im_size - self.info = self.png.im_info - self._text = None - self.tile = self.png.im_tile - self.custom_mimetype = self.png.im_custom_mimetype - self.n_frames = self.png.im_n_frames or 1 - self.default_image = self.info.get("default_image", False) - - if self.png.im_palette: - rawmode, data = self.png.im_palette - self.palette = ImagePalette.raw(rawmode, data) - - if cid == b"fdAT": - self.__prepare_idat = length - 4 - else: - self.__prepare_idat = length # used by load_prepare() - - if self.png.im_n_frames is not None: - self._close_exclusive_fp_after_loading = False - self.png.save_rewind() - self.__rewind_idat = self.__prepare_idat - self.__rewind = self._fp.tell() - if self.default_image: - # IDAT chunk contains default image and not first animation frame - self.n_frames += 1 - self._seek(0) - self.is_animated = self.n_frames > 1 - - @property - def text(self): - # experimental - if self._text is None: - # iTxt, tEXt and zTXt chunks may appear at the end of the file - # So load the file to ensure that they are read - if self.is_animated: - frame = self.__frame - # for APNG, seek to the final frame before loading - self.seek(self.n_frames - 1) - self.load() - if self.is_animated: - self.seek(frame) - return self._text - - def verify(self) -> None: - """Verify PNG file""" - - if self.fp is None: - msg = "verify must be called directly after open" - raise RuntimeError(msg) - - # back up to beginning of IDAT block - self.fp.seek(self.tile[0][2] - 8) - - assert self.png is not None - self.png.verify() - self.png.close() - - if self._exclusive_fp: - self.fp.close() - self.fp = None - - def seek(self, frame: int) -> None: - if not self._seek_check(frame): - return - if frame < self.__frame: - self._seek(0, True) - - last_frame = self.__frame - for f in range(self.__frame + 1, frame + 1): - try: - self._seek(f) - except EOFError as e: - self.seek(last_frame) - msg = "no more images in APNG file" - raise EOFError(msg) from e - - def _seek(self, frame, rewind=False): - if frame == 0: - if rewind: - self._fp.seek(self.__rewind) - self.png.rewind() - self.__prepare_idat = self.__rewind_idat - self.im = None - if self.pyaccess: - self.pyaccess = None - self.info = self.png.im_info - self.tile = self.png.im_tile - self.fp = self._fp - self._prev_im = None - self.dispose = None - self.default_image = self.info.get("default_image", False) - self.dispose_op = self.info.get("disposal") - self.blend_op = self.info.get("blend") - self.dispose_extent = self.info.get("bbox") - self.__frame = 0 - else: - if frame != self.__frame + 1: - msg = f"cannot seek to frame {frame}" - raise ValueError(msg) - - # ensure previous frame was loaded - self.load() - - if self.dispose: - self.im.paste(self.dispose, self.dispose_extent) - self._prev_im = self.im.copy() - - self.fp = self._fp - - # advance to the next frame - if self.__prepare_idat: - ImageFile._safe_read(self.fp, self.__prepare_idat) - self.__prepare_idat = 0 - frame_start = False - while True: - self.fp.read(4) # CRC - - try: - cid, pos, length = self.png.read() - except (struct.error, SyntaxError): - break - - if cid == b"IEND": - msg = "No more images in APNG file" - raise EOFError(msg) - if cid == b"fcTL": - if frame_start: - # there must be at least one fdAT chunk between fcTL chunks - msg = "APNG missing frame data" - raise SyntaxError(msg) - frame_start = True - - try: - self.png.call(cid, pos, length) - except UnicodeDecodeError: - break - except EOFError: - if cid == b"fdAT": - length -= 4 - if frame_start: - self.__prepare_idat = length - break - ImageFile._safe_read(self.fp, length) - except AttributeError: - logger.debug("%r %s %s (unknown)", cid, pos, length) - ImageFile._safe_read(self.fp, length) - - self.__frame = frame - self.tile = self.png.im_tile - self.dispose_op = self.info.get("disposal") - self.blend_op = self.info.get("blend") - self.dispose_extent = self.info.get("bbox") - - if not self.tile: - msg = "image not found in APNG frame" - raise EOFError(msg) - - # setup frame disposal (actual disposal done when needed in the next _seek()) - if self._prev_im is None and self.dispose_op == Disposal.OP_PREVIOUS: - self.dispose_op = Disposal.OP_BACKGROUND - - if self.dispose_op == Disposal.OP_PREVIOUS: - self.dispose = self._prev_im.copy() - self.dispose = self._crop(self.dispose, self.dispose_extent) - elif self.dispose_op == Disposal.OP_BACKGROUND: - self.dispose = Image.core.fill(self.mode, self.size) - self.dispose = self._crop(self.dispose, self.dispose_extent) - else: - self.dispose = None - - def tell(self) -> int: - return self.__frame - - def load_prepare(self) -> None: - """internal: prepare to read PNG file""" - - if self.info.get("interlace"): - self.decoderconfig = self.decoderconfig + (1,) - - self.__idat = self.__prepare_idat # used by load_read() - ImageFile.ImageFile.load_prepare(self) - - def load_read(self, read_bytes: int) -> bytes: - """internal: read more image data""" - - assert self.png is not None - while self.__idat == 0: - # end of chunk, skip forward to next one - - self.fp.read(4) # CRC - - cid, pos, length = self.png.read() - - if cid not in [b"IDAT", b"DDAT", b"fdAT"]: - self.png.push(cid, pos, length) - return b"" - - if cid == b"fdAT": - try: - self.png.call(cid, pos, length) - except EOFError: - pass - self.__idat = length - 4 # sequence_num has already been read - else: - self.__idat = length # empty chunks are allowed - - # read more data from this chunk - if read_bytes <= 0: - read_bytes = self.__idat - else: - read_bytes = min(read_bytes, self.__idat) - - self.__idat = self.__idat - read_bytes - - return self.fp.read(read_bytes) - - def load_end(self) -> None: - """internal: finished reading image data""" - assert self.png is not None - if self.__idat != 0: - self.fp.read(self.__idat) - while True: - self.fp.read(4) # CRC - - try: - cid, pos, length = self.png.read() - except (struct.error, SyntaxError): - break - - if cid == b"IEND": - break - elif cid == b"fcTL" and self.is_animated: - # start of the next frame, stop reading - self.__prepare_idat = 0 - self.png.push(cid, pos, length) - break - - try: - self.png.call(cid, pos, length) - except UnicodeDecodeError: - break - except EOFError: - if cid == b"fdAT": - length -= 4 - try: - ImageFile._safe_read(self.fp, length) - except OSError as e: - if ImageFile.LOAD_TRUNCATED_IMAGES: - break - else: - raise e - except AttributeError: - logger.debug("%r %s %s (unknown)", cid, pos, length) - s = ImageFile._safe_read(self.fp, length) - if cid[1:2].islower(): - self.private_chunks.append((cid, s, True)) - self._text = self.png.im_text - if not self.is_animated: - self.png.close() - self.png = None - else: - if self._prev_im and self.blend_op == Blend.OP_OVER: - updated = self._crop(self.im, self.dispose_extent) - if self.im.mode == "RGB" and "transparency" in self.info: - mask = updated.convert_transparent( - "RGBA", self.info["transparency"] - ) - else: - mask = updated.convert("RGBA") - self._prev_im.paste(updated, self.dispose_extent, mask) - self.im = self._prev_im - if self.pyaccess: - self.pyaccess = None - - def _getexif(self) -> dict[str, Any] | None: - if "exif" not in self.info: - self.load() - if "exif" not in self.info and "Raw profile type exif" not in self.info: - return None - return self.getexif()._get_merged_dict() - - def getexif(self): - if "exif" not in self.info: - self.load() - - return super().getexif() - - def getxmp(self) -> dict[str, Any]: - """ - Returns a dictionary containing the XMP tags. - Requires defusedxml to be installed. - - :returns: XMP tags in a dictionary. - """ - return ( - self._getxmp(self.info["XML:com.adobe.xmp"]) - if "XML:com.adobe.xmp" in self.info - else {} - ) - - -# -------------------------------------------------------------------- -# PNG writer - -_OUTMODES = { - # supported PIL modes, and corresponding rawmode, bit depth and color type - "1": ("1", b"\x01", b"\x00"), - "L;1": ("L;1", b"\x01", b"\x00"), - "L;2": ("L;2", b"\x02", b"\x00"), - "L;4": ("L;4", b"\x04", b"\x00"), - "L": ("L", b"\x08", b"\x00"), - "LA": ("LA", b"\x08", b"\x04"), - "I": ("I;16B", b"\x10", b"\x00"), - "I;16": ("I;16B", b"\x10", b"\x00"), - "I;16B": ("I;16B", b"\x10", b"\x00"), - "P;1": ("P;1", b"\x01", b"\x03"), - "P;2": ("P;2", b"\x02", b"\x03"), - "P;4": ("P;4", b"\x04", b"\x03"), - "P": ("P", b"\x08", b"\x03"), - "RGB": ("RGB", b"\x08", b"\x02"), - "RGBA": ("RGBA", b"\x08", b"\x06"), -} - - -def putchunk(fp, cid, *data): - """Write a PNG chunk (including CRC field)""" - - data = b"".join(data) - - fp.write(o32(len(data)) + cid) - fp.write(data) - crc = _crc32(data, _crc32(cid)) - fp.write(o32(crc)) - - -class _idat: - # wrap output from the encoder in IDAT chunks - - def __init__(self, fp, chunk): - self.fp = fp - self.chunk = chunk - - def write(self, data: bytes) -> None: - self.chunk(self.fp, b"IDAT", data) - - -class _fdat: - # wrap encoder output in fdAT chunks - - def __init__(self, fp, chunk, seq_num): - self.fp = fp - self.chunk = chunk - self.seq_num = seq_num - - def write(self, data: bytes) -> None: - self.chunk(self.fp, b"fdAT", o32(self.seq_num), data) - self.seq_num += 1 - - -def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images): - duration = im.encoderinfo.get("duration", im.info.get("duration", 0)) - loop = im.encoderinfo.get("loop", im.info.get("loop", 0)) - disposal = im.encoderinfo.get("disposal", im.info.get("disposal", Disposal.OP_NONE)) - blend = im.encoderinfo.get("blend", im.info.get("blend", Blend.OP_SOURCE)) - - if default_image: - chain = itertools.chain(append_images) - else: - chain = itertools.chain([im], append_images) - - im_frames = [] - frame_count = 0 - for im_seq in chain: - for im_frame in ImageSequence.Iterator(im_seq): - if im_frame.mode == rawmode: - im_frame = im_frame.copy() - else: - im_frame = im_frame.convert(rawmode) - encoderinfo = im.encoderinfo.copy() - if isinstance(duration, (list, tuple)): - encoderinfo["duration"] = duration[frame_count] - if isinstance(disposal, (list, tuple)): - encoderinfo["disposal"] = disposal[frame_count] - if isinstance(blend, (list, tuple)): - encoderinfo["blend"] = blend[frame_count] - frame_count += 1 - - if im_frames: - previous = im_frames[-1] - prev_disposal = previous["encoderinfo"].get("disposal") - prev_blend = previous["encoderinfo"].get("blend") - if prev_disposal == Disposal.OP_PREVIOUS and len(im_frames) < 2: - prev_disposal = Disposal.OP_BACKGROUND - - if prev_disposal == Disposal.OP_BACKGROUND: - base_im = previous["im"].copy() - dispose = Image.core.fill("RGBA", im.size, (0, 0, 0, 0)) - bbox = previous["bbox"] - if bbox: - dispose = dispose.crop(bbox) - else: - bbox = (0, 0) + im.size - base_im.paste(dispose, bbox) - elif prev_disposal == Disposal.OP_PREVIOUS: - base_im = im_frames[-2]["im"] - else: - base_im = previous["im"] - delta = ImageChops.subtract_modulo( - im_frame.convert("RGBA"), base_im.convert("RGBA") - ) - bbox = delta.getbbox(alpha_only=False) - if ( - not bbox - and prev_disposal == encoderinfo.get("disposal") - and prev_blend == encoderinfo.get("blend") - ): - previous["encoderinfo"]["duration"] += encoderinfo.get( - "duration", duration - ) - continue - else: - bbox = None - if "duration" not in encoderinfo: - encoderinfo["duration"] = duration - im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo}) - - if len(im_frames) == 1 and not default_image: - return im_frames[0]["im"] - - # animation control - chunk( - fp, - b"acTL", - o32(len(im_frames)), # 0: num_frames - o32(loop), # 4: num_plays - ) - - # default image IDAT (if it exists) - if default_image: - if im.mode != rawmode: - im = im.convert(rawmode) - ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)]) - - seq_num = 0 - for frame, frame_data in enumerate(im_frames): - im_frame = frame_data["im"] - if not frame_data["bbox"]: - bbox = (0, 0) + im_frame.size - else: - bbox = frame_data["bbox"] - im_frame = im_frame.crop(bbox) - size = im_frame.size - encoderinfo = frame_data["encoderinfo"] - frame_duration = int(round(encoderinfo["duration"])) - frame_disposal = encoderinfo.get("disposal", disposal) - frame_blend = encoderinfo.get("blend", blend) - # frame control - chunk( - fp, - b"fcTL", - o32(seq_num), # sequence_number - o32(size[0]), # width - o32(size[1]), # height - o32(bbox[0]), # x_offset - o32(bbox[1]), # y_offset - o16(frame_duration), # delay_numerator - o16(1000), # delay_denominator - o8(frame_disposal), # dispose_op - o8(frame_blend), # blend_op - ) - seq_num += 1 - # frame data - if frame == 0 and not default_image: - # first frame must be in IDAT chunks for backwards compatibility - ImageFile._save( - im_frame, - _idat(fp, chunk), - [("zip", (0, 0) + im_frame.size, 0, rawmode)], - ) - else: - fdat_chunks = _fdat(fp, chunk, seq_num) - ImageFile._save( - im_frame, - fdat_chunks, - [("zip", (0, 0) + im_frame.size, 0, rawmode)], - ) - seq_num = fdat_chunks.seq_num - - -def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - _save(im, fp, filename, save_all=True) - - -def _save(im, fp, filename, chunk=putchunk, save_all=False): - # save an image to disk (called by the save method) - - if save_all: - default_image = im.encoderinfo.get( - "default_image", im.info.get("default_image") - ) - modes = set() - sizes = set() - append_images = im.encoderinfo.get("append_images", []) - for im_seq in itertools.chain([im], append_images): - for im_frame in ImageSequence.Iterator(im_seq): - modes.add(im_frame.mode) - sizes.add(im_frame.size) - for mode in ("RGBA", "RGB", "P"): - if mode in modes: - break - else: - mode = modes.pop() - size = tuple(max(frame_size[i] for frame_size in sizes) for i in range(2)) - else: - size = im.size - mode = im.mode - - if mode == "P": - # - # attempt to minimize storage requirements for palette images - if "bits" in im.encoderinfo: - # number of bits specified by user - colors = min(1 << im.encoderinfo["bits"], 256) - else: - # check palette contents - if im.palette: - colors = max(min(len(im.palette.getdata()[1]) // 3, 256), 1) - else: - colors = 256 - - if colors <= 16: - if colors <= 2: - bits = 1 - elif colors <= 4: - bits = 2 - else: - bits = 4 - mode = f"{mode};{bits}" - - # encoder options - im.encoderconfig = ( - im.encoderinfo.get("optimize", False), - im.encoderinfo.get("compress_level", -1), - im.encoderinfo.get("compress_type", -1), - im.encoderinfo.get("dictionary", b""), - ) - - # get the corresponding PNG mode - try: - rawmode, bit_depth, color_type = _OUTMODES[mode] - except KeyError as e: - msg = f"cannot write mode {mode} as PNG" - raise OSError(msg) from e - - # - # write minimal PNG file - - fp.write(_MAGIC) - - chunk( - fp, - b"IHDR", - o32(size[0]), # 0: size - o32(size[1]), - bit_depth, - color_type, - b"\0", # 10: compression - b"\0", # 11: filter category - b"\0", # 12: interlace flag - ) - - chunks = [b"cHRM", b"gAMA", b"sBIT", b"sRGB", b"tIME"] - - icc = im.encoderinfo.get("icc_profile", im.info.get("icc_profile")) - if icc: - # ICC profile - # according to PNG spec, the iCCP chunk contains: - # Profile name 1-79 bytes (character string) - # Null separator 1 byte (null character) - # Compression method 1 byte (0) - # Compressed profile n bytes (zlib with deflate compression) - name = b"ICC Profile" - data = name + b"\0\0" + zlib.compress(icc) - chunk(fp, b"iCCP", data) - - # You must either have sRGB or iCCP. - # Disallow sRGB chunks when an iCCP-chunk has been emitted. - chunks.remove(b"sRGB") - - info = im.encoderinfo.get("pnginfo") - if info: - chunks_multiple_allowed = [b"sPLT", b"iTXt", b"tEXt", b"zTXt"] - for info_chunk in info.chunks: - cid, data = info_chunk[:2] - if cid in chunks: - chunks.remove(cid) - chunk(fp, cid, data) - elif cid in chunks_multiple_allowed: - chunk(fp, cid, data) - elif cid[1:2].islower(): - # Private chunk - after_idat = info_chunk[2:3] - if not after_idat: - chunk(fp, cid, data) - - if im.mode == "P": - palette_byte_number = colors * 3 - palette_bytes = im.im.getpalette("RGB")[:palette_byte_number] - while len(palette_bytes) < palette_byte_number: - palette_bytes += b"\0" - chunk(fp, b"PLTE", palette_bytes) - - transparency = im.encoderinfo.get("transparency", im.info.get("transparency", None)) - - if transparency or transparency == 0: - if im.mode == "P": - # limit to actual palette size - alpha_bytes = colors - if isinstance(transparency, bytes): - chunk(fp, b"tRNS", transparency[:alpha_bytes]) - else: - transparency = max(0, min(255, transparency)) - alpha = b"\xFF" * transparency + b"\0" - chunk(fp, b"tRNS", alpha[:alpha_bytes]) - elif im.mode in ("1", "L", "I", "I;16"): - transparency = max(0, min(65535, transparency)) - chunk(fp, b"tRNS", o16(transparency)) - elif im.mode == "RGB": - red, green, blue = transparency - chunk(fp, b"tRNS", o16(red) + o16(green) + o16(blue)) - else: - if "transparency" in im.encoderinfo: - # don't bother with transparency if it's an RGBA - # and it's in the info dict. It's probably just stale. - msg = "cannot use transparency for this mode" - raise OSError(msg) - else: - if im.mode == "P" and im.im.getpalettemode() == "RGBA": - alpha = im.im.getpalette("RGBA", "A") - alpha_bytes = colors - chunk(fp, b"tRNS", alpha[:alpha_bytes]) - - dpi = im.encoderinfo.get("dpi") - if dpi: - chunk( - fp, - b"pHYs", - o32(int(dpi[0] / 0.0254 + 0.5)), - o32(int(dpi[1] / 0.0254 + 0.5)), - b"\x01", - ) - - if info: - chunks = [b"bKGD", b"hIST"] - for info_chunk in info.chunks: - cid, data = info_chunk[:2] - if cid in chunks: - chunks.remove(cid) - chunk(fp, cid, data) - - exif = im.encoderinfo.get("exif") - if exif: - if isinstance(exif, Image.Exif): - exif = exif.tobytes(8) - if exif.startswith(b"Exif\x00\x00"): - exif = exif[6:] - chunk(fp, b"eXIf", exif) - - if save_all: - im = _write_multiple_frames( - im, fp, chunk, rawmode, default_image, append_images - ) - if im: - ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)]) - - if info: - for info_chunk in info.chunks: - cid, data = info_chunk[:2] - if cid[1:2].islower(): - # Private chunk - after_idat = info_chunk[2:3] - if after_idat: - chunk(fp, cid, data) - - chunk(fp, b"IEND", b"") - - if hasattr(fp, "flush"): - fp.flush() - - -# -------------------------------------------------------------------- -# PNG chunk converter - - -def getchunks(im, **params): - """Return a list of PNG chunks representing this image.""" - - class collector: - data = [] - - def write(self, data: bytes) -> None: - pass - - def append(self, chunk: bytes) -> None: - self.data.append(chunk) - - def append(fp, cid, *data): - data = b"".join(data) - crc = o32(_crc32(data, _crc32(cid))) - fp.append((cid, data, crc)) - - fp = collector() - - try: - im.encoderinfo = params - _save(im, fp, None, append) - finally: - del im.encoderinfo - - return fp.data - - -# -------------------------------------------------------------------- -# Registry - -Image.register_open(PngImageFile.format, PngImageFile, _accept) -Image.register_save(PngImageFile.format, _save) -Image.register_save_all(PngImageFile.format, _save_all) - -Image.register_extensions(PngImageFile.format, [".png", ".apng"]) - -Image.register_mime(PngImageFile.format, "image/png") +# +# The Python Imaging Library. +# $Id$ +# +# PNG support code +# +# See "PNG (Portable Network Graphics) Specification, version 1.0; +# W3C Recommendation", 1996-10-01, Thomas Boutell (ed.). +# +# history: +# 1996-05-06 fl Created (couldn't resist it) +# 1996-12-14 fl Upgraded, added read and verify support (0.2) +# 1996-12-15 fl Separate PNG stream parser +# 1996-12-29 fl Added write support, added getchunks +# 1996-12-30 fl Eliminated circular references in decoder (0.3) +# 1998-07-12 fl Read/write 16-bit images as mode I (0.4) +# 2001-02-08 fl Added transparency support (from Zircon) (0.5) +# 2001-04-16 fl Don't close data source in "open" method (0.6) +# 2004-02-24 fl Don't even pretend to support interlaced files (0.7) +# 2004-08-31 fl Do basic sanity check on chunk identifiers (0.8) +# 2004-09-20 fl Added PngInfo chunk container +# 2004-12-18 fl Added DPI read support (based on code by Niki Spahiev) +# 2008-08-13 fl Added tRNS support for RGB images +# 2009-03-06 fl Support for preserving ICC profiles (by Florian Hoech) +# 2009-03-08 fl Added zTXT support (from Lowell Alleman) +# 2009-03-29 fl Read interlaced PNG files (from Conrado Porto Lopes Gouvua) +# +# Copyright (c) 1997-2009 by Secret Labs AB +# Copyright (c) 1996 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import itertools +import logging +import re +import struct +import warnings +import zlib +from enum import IntEnum +from typing import IO, Any + +from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence +from ._binary import i16be as i16 +from ._binary import i32be as i32 +from ._binary import o8 +from ._binary import o16be as o16 +from ._binary import o32be as o32 + +logger = logging.getLogger(__name__) + +is_cid = re.compile(rb"\w\w\w\w").match + + +_MAGIC = b"\211PNG\r\n\032\n" + + +_MODES = { + # supported bits/color combinations, and corresponding modes/rawmodes + # Grayscale + (1, 0): ("1", "1"), + (2, 0): ("L", "L;2"), + (4, 0): ("L", "L;4"), + (8, 0): ("L", "L"), + (16, 0): ("I;16", "I;16B"), + # Truecolour + (8, 2): ("RGB", "RGB"), + (16, 2): ("RGB", "RGB;16B"), + # Indexed-colour + (1, 3): ("P", "P;1"), + (2, 3): ("P", "P;2"), + (4, 3): ("P", "P;4"), + (8, 3): ("P", "P"), + # Grayscale with alpha + (8, 4): ("LA", "LA"), + (16, 4): ("RGBA", "LA;16B"), # LA;16B->LA not yet available + # Truecolour with alpha + (8, 6): ("RGBA", "RGBA"), + (16, 6): ("RGBA", "RGBA;16B"), +} + + +_simple_palette = re.compile(b"^\xff*\x00\xff*$") + +MAX_TEXT_CHUNK = ImageFile.SAFEBLOCK +""" +Maximum decompressed size for a iTXt or zTXt chunk. +Eliminates decompression bombs where compressed chunks can expand 1000x. +See :ref:`Text in PNG File Format`. +""" +MAX_TEXT_MEMORY = 64 * MAX_TEXT_CHUNK +""" +Set the maximum total text chunk size. +See :ref:`Text in PNG File Format`. +""" + + +# APNG frame disposal modes +class Disposal(IntEnum): + OP_NONE = 0 + """ + No disposal is done on this frame before rendering the next frame. + See :ref:`Saving APNG sequences`. + """ + OP_BACKGROUND = 1 + """ + This frame’s modified region is cleared to fully transparent black before rendering + the next frame. + See :ref:`Saving APNG sequences`. + """ + OP_PREVIOUS = 2 + """ + This frame’s modified region is reverted to the previous frame’s contents before + rendering the next frame. + See :ref:`Saving APNG sequences`. + """ + + +# APNG frame blend modes +class Blend(IntEnum): + OP_SOURCE = 0 + """ + All color components of this frame, including alpha, overwrite the previous output + image contents. + See :ref:`Saving APNG sequences`. + """ + OP_OVER = 1 + """ + This frame should be alpha composited with the previous output image contents. + See :ref:`Saving APNG sequences`. + """ + + +def _safe_zlib_decompress(s): + dobj = zlib.decompressobj() + plaintext = dobj.decompress(s, MAX_TEXT_CHUNK) + if dobj.unconsumed_tail: + msg = "Decompressed Data Too Large" + raise ValueError(msg) + return plaintext + + +def _crc32(data, seed=0): + return zlib.crc32(data, seed) & 0xFFFFFFFF + + +# -------------------------------------------------------------------- +# Support classes. Suitable for PNG and related formats like MNG etc. + + +class ChunkStream: + def __init__(self, fp: IO[bytes]) -> None: + self.fp: IO[bytes] | None = fp + self.queue: list[tuple[bytes, int, int]] | None = [] + + def read(self) -> tuple[bytes, int, int]: + """Fetch a new chunk. Returns header information.""" + cid = None + + assert self.fp is not None + if self.queue: + cid, pos, length = self.queue.pop() + self.fp.seek(pos) + else: + s = self.fp.read(8) + cid = s[4:] + pos = self.fp.tell() + length = i32(s) + + if not is_cid(cid): + if not ImageFile.LOAD_TRUNCATED_IMAGES: + msg = f"broken PNG file (chunk {repr(cid)})" + raise SyntaxError(msg) + + return cid, pos, length + + def __enter__(self) -> ChunkStream: + return self + + def __exit__(self, *args: object) -> None: + self.close() + + def close(self) -> None: + self.queue = self.fp = None + + def push(self, cid: bytes, pos: int, length: int) -> None: + assert self.queue is not None + self.queue.append((cid, pos, length)) + + def call(self, cid, pos, length): + """Call the appropriate chunk handler""" + + logger.debug("STREAM %r %s %s", cid, pos, length) + return getattr(self, f"chunk_{cid.decode('ascii')}")(pos, length) + + def crc(self, cid: bytes, data: bytes) -> None: + """Read and verify checksum""" + + # Skip CRC checks for ancillary chunks if allowed to load truncated + # images + # 5th byte of first char is 1 [specs, section 5.4] + if ImageFile.LOAD_TRUNCATED_IMAGES and (cid[0] >> 5 & 1): + self.crc_skip(cid, data) + return + + assert self.fp is not None + try: + crc1 = _crc32(data, _crc32(cid)) + crc2 = i32(self.fp.read(4)) + if crc1 != crc2: + msg = f"broken PNG file (bad header checksum in {repr(cid)})" + raise SyntaxError(msg) + except struct.error as e: + msg = f"broken PNG file (incomplete checksum in {repr(cid)})" + raise SyntaxError(msg) from e + + def crc_skip(self, cid: bytes, data: bytes) -> None: + """Read checksum""" + + assert self.fp is not None + self.fp.read(4) + + def verify(self, endchunk: bytes = b"IEND") -> list[bytes]: + # Simple approach; just calculate checksum for all remaining + # blocks. Must be called directly after open. + + cids = [] + + while True: + try: + cid, pos, length = self.read() + except struct.error as e: + msg = "truncated PNG file" + raise OSError(msg) from e + + if cid == endchunk: + break + self.crc(cid, ImageFile._safe_read(self.fp, length)) + cids.append(cid) + + return cids + + +class iTXt(str): + """ + Subclass of string to allow iTXt chunks to look like strings while + keeping their extra information + + """ + + @staticmethod + def __new__(cls, text, lang=None, tkey=None): + """ + :param cls: the class to use when creating the instance + :param text: value for this key + :param lang: language code + :param tkey: UTF-8 version of the key name + """ + + self = str.__new__(cls, text) + self.lang = lang + self.tkey = tkey + return self + + +class PngInfo: + """ + PNG chunk container (for use with save(pnginfo=)) + + """ + + def __init__(self): + self.chunks = [] + + def add(self, cid, data, after_idat=False): + """Appends an arbitrary chunk. Use with caution. + + :param cid: a byte string, 4 bytes long. + :param data: a byte string of the encoded data + :param after_idat: for use with private chunks. Whether the chunk + should be written after IDAT + + """ + + chunk = [cid, data] + if after_idat: + chunk.append(True) + self.chunks.append(tuple(chunk)) + + def add_itxt(self, key, value, lang="", tkey="", zip=False): + """Appends an iTXt chunk. + + :param key: latin-1 encodable text key name + :param value: value for this key + :param lang: language code + :param tkey: UTF-8 version of the key name + :param zip: compression flag + + """ + + if not isinstance(key, bytes): + key = key.encode("latin-1", "strict") + if not isinstance(value, bytes): + value = value.encode("utf-8", "strict") + if not isinstance(lang, bytes): + lang = lang.encode("utf-8", "strict") + if not isinstance(tkey, bytes): + tkey = tkey.encode("utf-8", "strict") + + if zip: + self.add( + b"iTXt", + key + b"\0\x01\0" + lang + b"\0" + tkey + b"\0" + zlib.compress(value), + ) + else: + self.add(b"iTXt", key + b"\0\0\0" + lang + b"\0" + tkey + b"\0" + value) + + def add_text(self, key, value, zip=False): + """Appends a text chunk. + + :param key: latin-1 encodable text key name + :param value: value for this key, text or an + :py:class:`PIL.PngImagePlugin.iTXt` instance + :param zip: compression flag + + """ + if isinstance(value, iTXt): + return self.add_itxt(key, value, value.lang, value.tkey, zip=zip) + + # The tEXt chunk stores latin-1 text + if not isinstance(value, bytes): + try: + value = value.encode("latin-1", "strict") + except UnicodeError: + return self.add_itxt(key, value, zip=zip) + + if not isinstance(key, bytes): + key = key.encode("latin-1", "strict") + + if zip: + self.add(b"zTXt", key + b"\0\0" + zlib.compress(value)) + else: + self.add(b"tEXt", key + b"\0" + value) + + +# -------------------------------------------------------------------- +# PNG image stream (IHDR/IEND) + + +class PngStream(ChunkStream): + def __init__(self, fp): + super().__init__(fp) + + # local copies of Image attributes + self.im_info = {} + self.im_text = {} + self.im_size = (0, 0) + self.im_mode = None + self.im_tile = None + self.im_palette = None + self.im_custom_mimetype = None + self.im_n_frames = None + self._seq_num = None + self.rewind_state = None + + self.text_memory = 0 + + def check_text_memory(self, chunklen: int) -> None: + self.text_memory += chunklen + if self.text_memory > MAX_TEXT_MEMORY: + msg = ( + "Too much memory used in text chunks: " + f"{self.text_memory}>MAX_TEXT_MEMORY" + ) + raise ValueError(msg) + + def save_rewind(self) -> None: + self.rewind_state = { + "info": self.im_info.copy(), + "tile": self.im_tile, + "seq_num": self._seq_num, + } + + def rewind(self) -> None: + self.im_info = self.rewind_state["info"].copy() + self.im_tile = self.rewind_state["tile"] + self._seq_num = self.rewind_state["seq_num"] + + def chunk_iCCP(self, pos: int, length: int) -> bytes: + # ICC profile + s = ImageFile._safe_read(self.fp, length) + # according to PNG spec, the iCCP chunk contains: + # Profile name 1-79 bytes (character string) + # Null separator 1 byte (null character) + # Compression method 1 byte (0) + # Compressed profile n bytes (zlib with deflate compression) + i = s.find(b"\0") + logger.debug("iCCP profile name %r", s[:i]) + comp_method = s[i + 1] + logger.debug("Compression method %s", comp_method) + if comp_method != 0: + msg = f"Unknown compression method {comp_method} in iCCP chunk" + raise SyntaxError(msg) + try: + icc_profile = _safe_zlib_decompress(s[i + 2 :]) + except ValueError: + if ImageFile.LOAD_TRUNCATED_IMAGES: + icc_profile = None + else: + raise + except zlib.error: + icc_profile = None # FIXME + self.im_info["icc_profile"] = icc_profile + return s + + def chunk_IHDR(self, pos: int, length: int) -> bytes: + # image header + s = ImageFile._safe_read(self.fp, length) + if length < 13: + if ImageFile.LOAD_TRUNCATED_IMAGES: + return s + msg = "Truncated IHDR chunk" + raise ValueError(msg) + self.im_size = i32(s, 0), i32(s, 4) + try: + self.im_mode, self.im_rawmode = _MODES[(s[8], s[9])] + except Exception: + pass + if s[12]: + self.im_info["interlace"] = 1 + if s[11]: + msg = "unknown filter category" + raise SyntaxError(msg) + return s + + def chunk_IDAT(self, pos, length): + # image data + if "bbox" in self.im_info: + tile = [("zip", self.im_info["bbox"], pos, self.im_rawmode)] + else: + if self.im_n_frames is not None: + self.im_info["default_image"] = True + tile = [("zip", (0, 0) + self.im_size, pos, self.im_rawmode)] + self.im_tile = tile + self.im_idat = length + msg = "image data found" + raise EOFError(msg) + + def chunk_IEND(self, pos, length): + msg = "end of PNG image" + raise EOFError(msg) + + def chunk_PLTE(self, pos: int, length: int) -> bytes: + # palette + s = ImageFile._safe_read(self.fp, length) + if self.im_mode == "P": + self.im_palette = "RGB", s + return s + + def chunk_tRNS(self, pos: int, length: int) -> bytes: + # transparency + s = ImageFile._safe_read(self.fp, length) + if self.im_mode == "P": + if _simple_palette.match(s): + # tRNS contains only one full-transparent entry, + # other entries are full opaque + i = s.find(b"\0") + if i >= 0: + self.im_info["transparency"] = i + else: + # otherwise, we have a byte string with one alpha value + # for each palette entry + self.im_info["transparency"] = s + elif self.im_mode in ("1", "L", "I;16"): + self.im_info["transparency"] = i16(s) + elif self.im_mode == "RGB": + self.im_info["transparency"] = i16(s), i16(s, 2), i16(s, 4) + return s + + def chunk_gAMA(self, pos: int, length: int) -> bytes: + # gamma setting + s = ImageFile._safe_read(self.fp, length) + self.im_info["gamma"] = i32(s) / 100000.0 + return s + + def chunk_cHRM(self, pos: int, length: int) -> bytes: + # chromaticity, 8 unsigned ints, actual value is scaled by 100,000 + # WP x,y, Red x,y, Green x,y Blue x,y + + s = ImageFile._safe_read(self.fp, length) + raw_vals = struct.unpack(">%dI" % (len(s) // 4), s) + self.im_info["chromaticity"] = tuple(elt / 100000.0 for elt in raw_vals) + return s + + def chunk_sRGB(self, pos: int, length: int) -> bytes: + # srgb rendering intent, 1 byte + # 0 perceptual + # 1 relative colorimetric + # 2 saturation + # 3 absolute colorimetric + + s = ImageFile._safe_read(self.fp, length) + if length < 1: + if ImageFile.LOAD_TRUNCATED_IMAGES: + return s + msg = "Truncated sRGB chunk" + raise ValueError(msg) + self.im_info["srgb"] = s[0] + return s + + def chunk_pHYs(self, pos: int, length: int) -> bytes: + # pixels per unit + s = ImageFile._safe_read(self.fp, length) + if length < 9: + if ImageFile.LOAD_TRUNCATED_IMAGES: + return s + msg = "Truncated pHYs chunk" + raise ValueError(msg) + px, py = i32(s, 0), i32(s, 4) + unit = s[8] + if unit == 1: # meter + dpi = px * 0.0254, py * 0.0254 + self.im_info["dpi"] = dpi + elif unit == 0: + self.im_info["aspect"] = px, py + return s + + def chunk_tEXt(self, pos: int, length: int) -> bytes: + # text + s = ImageFile._safe_read(self.fp, length) + try: + k, v = s.split(b"\0", 1) + except ValueError: + # fallback for broken tEXt tags + k = s + v = b"" + if k: + k = k.decode("latin-1", "strict") + v_str = v.decode("latin-1", "replace") + + self.im_info[k] = v if k == "exif" else v_str + self.im_text[k] = v_str + self.check_text_memory(len(v_str)) + + return s + + def chunk_zTXt(self, pos: int, length: int) -> bytes: + # compressed text + s = ImageFile._safe_read(self.fp, length) + try: + k, v = s.split(b"\0", 1) + except ValueError: + k = s + v = b"" + if v: + comp_method = v[0] + else: + comp_method = 0 + if comp_method != 0: + msg = f"Unknown compression method {comp_method} in zTXt chunk" + raise SyntaxError(msg) + try: + v = _safe_zlib_decompress(v[1:]) + except ValueError: + if ImageFile.LOAD_TRUNCATED_IMAGES: + v = b"" + else: + raise + except zlib.error: + v = b"" + + if k: + k = k.decode("latin-1", "strict") + v = v.decode("latin-1", "replace") + + self.im_info[k] = self.im_text[k] = v + self.check_text_memory(len(v)) + + return s + + def chunk_iTXt(self, pos: int, length: int) -> bytes: + # international text + r = s = ImageFile._safe_read(self.fp, length) + try: + k, r = r.split(b"\0", 1) + except ValueError: + return s + if len(r) < 2: + return s + cf, cm, r = r[0], r[1], r[2:] + try: + lang, tk, v = r.split(b"\0", 2) + except ValueError: + return s + if cf != 0: + if cm == 0: + try: + v = _safe_zlib_decompress(v) + except ValueError: + if ImageFile.LOAD_TRUNCATED_IMAGES: + return s + else: + raise + except zlib.error: + return s + else: + return s + try: + k = k.decode("latin-1", "strict") + lang = lang.decode("utf-8", "strict") + tk = tk.decode("utf-8", "strict") + v = v.decode("utf-8", "strict") + except UnicodeError: + return s + + self.im_info[k] = self.im_text[k] = iTXt(v, lang, tk) + self.check_text_memory(len(v)) + + return s + + def chunk_eXIf(self, pos: int, length: int) -> bytes: + s = ImageFile._safe_read(self.fp, length) + self.im_info["exif"] = b"Exif\x00\x00" + s + return s + + # APNG chunks + def chunk_acTL(self, pos: int, length: int) -> bytes: + s = ImageFile._safe_read(self.fp, length) + if length < 8: + if ImageFile.LOAD_TRUNCATED_IMAGES: + return s + msg = "APNG contains truncated acTL chunk" + raise ValueError(msg) + if self.im_n_frames is not None: + self.im_n_frames = None + warnings.warn("Invalid APNG, will use default PNG image if possible") + return s + n_frames = i32(s) + if n_frames == 0 or n_frames > 0x80000000: + warnings.warn("Invalid APNG, will use default PNG image if possible") + return s + self.im_n_frames = n_frames + self.im_info["loop"] = i32(s, 4) + self.im_custom_mimetype = "image/apng" + return s + + def chunk_fcTL(self, pos: int, length: int) -> bytes: + s = ImageFile._safe_read(self.fp, length) + if length < 26: + if ImageFile.LOAD_TRUNCATED_IMAGES: + return s + msg = "APNG contains truncated fcTL chunk" + raise ValueError(msg) + seq = i32(s) + if (self._seq_num is None and seq != 0) or ( + self._seq_num is not None and self._seq_num != seq - 1 + ): + msg = "APNG contains frame sequence errors" + raise SyntaxError(msg) + self._seq_num = seq + width, height = i32(s, 4), i32(s, 8) + px, py = i32(s, 12), i32(s, 16) + im_w, im_h = self.im_size + if px + width > im_w or py + height > im_h: + msg = "APNG contains invalid frames" + raise SyntaxError(msg) + self.im_info["bbox"] = (px, py, px + width, py + height) + delay_num, delay_den = i16(s, 20), i16(s, 22) + if delay_den == 0: + delay_den = 100 + self.im_info["duration"] = float(delay_num) / float(delay_den) * 1000 + self.im_info["disposal"] = s[24] + self.im_info["blend"] = s[25] + return s + + def chunk_fdAT(self, pos: int, length: int) -> bytes: + if length < 4: + if ImageFile.LOAD_TRUNCATED_IMAGES: + s = ImageFile._safe_read(self.fp, length) + return s + msg = "APNG contains truncated fDAT chunk" + raise ValueError(msg) + s = ImageFile._safe_read(self.fp, 4) + seq = i32(s) + if self._seq_num != seq - 1: + msg = "APNG contains frame sequence errors" + raise SyntaxError(msg) + self._seq_num = seq + return self.chunk_IDAT(pos + 4, length - 4) + + +# -------------------------------------------------------------------- +# PNG reader + + +def _accept(prefix: bytes) -> bool: + return prefix[:8] == _MAGIC + + +## +# Image plugin for PNG images. + + +class PngImageFile(ImageFile.ImageFile): + format = "PNG" + format_description = "Portable network graphics" + + def _open(self) -> None: + if not _accept(self.fp.read(8)): + msg = "not a PNG file" + raise SyntaxError(msg) + self._fp = self.fp + self.__frame = 0 + + # + # Parse headers up to the first IDAT or fDAT chunk + + self.private_chunks: list[tuple[bytes, bytes] | tuple[bytes, bytes, bool]] = [] + self.png: PngStream | None = PngStream(self.fp) + + while True: + # + # get next chunk + + cid, pos, length = self.png.read() + + try: + s = self.png.call(cid, pos, length) + except EOFError: + break + except AttributeError: + logger.debug("%r %s %s (unknown)", cid, pos, length) + s = ImageFile._safe_read(self.fp, length) + if cid[1:2].islower(): + self.private_chunks.append((cid, s)) + + self.png.crc(cid, s) + + # + # Copy relevant attributes from the PngStream. An alternative + # would be to let the PngStream class modify these attributes + # directly, but that introduces circular references which are + # difficult to break if things go wrong in the decoder... + # (believe me, I've tried ;-) + + self._mode = self.png.im_mode + self._size = self.png.im_size + self.info = self.png.im_info + self._text = None + self.tile = self.png.im_tile + self.custom_mimetype = self.png.im_custom_mimetype + self.n_frames = self.png.im_n_frames or 1 + self.default_image = self.info.get("default_image", False) + + if self.png.im_palette: + rawmode, data = self.png.im_palette + self.palette = ImagePalette.raw(rawmode, data) + + if cid == b"fdAT": + self.__prepare_idat = length - 4 + else: + self.__prepare_idat = length # used by load_prepare() + + if self.png.im_n_frames is not None: + self._close_exclusive_fp_after_loading = False + self.png.save_rewind() + self.__rewind_idat = self.__prepare_idat + self.__rewind = self._fp.tell() + if self.default_image: + # IDAT chunk contains default image and not first animation frame + self.n_frames += 1 + self._seek(0) + self.is_animated = self.n_frames > 1 + + @property + def text(self): + # experimental + if self._text is None: + # iTxt, tEXt and zTXt chunks may appear at the end of the file + # So load the file to ensure that they are read + if self.is_animated: + frame = self.__frame + # for APNG, seek to the final frame before loading + self.seek(self.n_frames - 1) + self.load() + if self.is_animated: + self.seek(frame) + return self._text + + def verify(self) -> None: + """Verify PNG file""" + + if self.fp is None: + msg = "verify must be called directly after open" + raise RuntimeError(msg) + + # back up to beginning of IDAT block + self.fp.seek(self.tile[0][2] - 8) + + assert self.png is not None + self.png.verify() + self.png.close() + + if self._exclusive_fp: + self.fp.close() + self.fp = None + + def seek(self, frame: int) -> None: + if not self._seek_check(frame): + return + if frame < self.__frame: + self._seek(0, True) + + last_frame = self.__frame + for f in range(self.__frame + 1, frame + 1): + try: + self._seek(f) + except EOFError as e: + self.seek(last_frame) + msg = "no more images in APNG file" + raise EOFError(msg) from e + + def _seek(self, frame, rewind=False): + if frame == 0: + if rewind: + self._fp.seek(self.__rewind) + self.png.rewind() + self.__prepare_idat = self.__rewind_idat + self.im = None + if self.pyaccess: + self.pyaccess = None + self.info = self.png.im_info + self.tile = self.png.im_tile + self.fp = self._fp + self._prev_im = None + self.dispose = None + self.default_image = self.info.get("default_image", False) + self.dispose_op = self.info.get("disposal") + self.blend_op = self.info.get("blend") + self.dispose_extent = self.info.get("bbox") + self.__frame = 0 + else: + if frame != self.__frame + 1: + msg = f"cannot seek to frame {frame}" + raise ValueError(msg) + + # ensure previous frame was loaded + self.load() + + if self.dispose: + self.im.paste(self.dispose, self.dispose_extent) + self._prev_im = self.im.copy() + + self.fp = self._fp + + # advance to the next frame + if self.__prepare_idat: + ImageFile._safe_read(self.fp, self.__prepare_idat) + self.__prepare_idat = 0 + frame_start = False + while True: + self.fp.read(4) # CRC + + try: + cid, pos, length = self.png.read() + except (struct.error, SyntaxError): + break + + if cid == b"IEND": + msg = "No more images in APNG file" + raise EOFError(msg) + if cid == b"fcTL": + if frame_start: + # there must be at least one fdAT chunk between fcTL chunks + msg = "APNG missing frame data" + raise SyntaxError(msg) + frame_start = True + + try: + self.png.call(cid, pos, length) + except UnicodeDecodeError: + break + except EOFError: + if cid == b"fdAT": + length -= 4 + if frame_start: + self.__prepare_idat = length + break + ImageFile._safe_read(self.fp, length) + except AttributeError: + logger.debug("%r %s %s (unknown)", cid, pos, length) + ImageFile._safe_read(self.fp, length) + + self.__frame = frame + self.tile = self.png.im_tile + self.dispose_op = self.info.get("disposal") + self.blend_op = self.info.get("blend") + self.dispose_extent = self.info.get("bbox") + + if not self.tile: + msg = "image not found in APNG frame" + raise EOFError(msg) + + # setup frame disposal (actual disposal done when needed in the next _seek()) + if self._prev_im is None and self.dispose_op == Disposal.OP_PREVIOUS: + self.dispose_op = Disposal.OP_BACKGROUND + + if self.dispose_op == Disposal.OP_PREVIOUS: + self.dispose = self._prev_im.copy() + self.dispose = self._crop(self.dispose, self.dispose_extent) + elif self.dispose_op == Disposal.OP_BACKGROUND: + self.dispose = Image.core.fill(self.mode, self.size) + self.dispose = self._crop(self.dispose, self.dispose_extent) + else: + self.dispose = None + + def tell(self) -> int: + return self.__frame + + def load_prepare(self) -> None: + """internal: prepare to read PNG file""" + + if self.info.get("interlace"): + self.decoderconfig = self.decoderconfig + (1,) + + self.__idat = self.__prepare_idat # used by load_read() + ImageFile.ImageFile.load_prepare(self) + + def load_read(self, read_bytes: int) -> bytes: + """internal: read more image data""" + + assert self.png is not None + while self.__idat == 0: + # end of chunk, skip forward to next one + + self.fp.read(4) # CRC + + cid, pos, length = self.png.read() + + if cid not in [b"IDAT", b"DDAT", b"fdAT"]: + self.png.push(cid, pos, length) + return b"" + + if cid == b"fdAT": + try: + self.png.call(cid, pos, length) + except EOFError: + pass + self.__idat = length - 4 # sequence_num has already been read + else: + self.__idat = length # empty chunks are allowed + + # read more data from this chunk + if read_bytes <= 0: + read_bytes = self.__idat + else: + read_bytes = min(read_bytes, self.__idat) + + self.__idat = self.__idat - read_bytes + + return self.fp.read(read_bytes) + + def load_end(self) -> None: + """internal: finished reading image data""" + assert self.png is not None + if self.__idat != 0: + self.fp.read(self.__idat) + while True: + self.fp.read(4) # CRC + + try: + cid, pos, length = self.png.read() + except (struct.error, SyntaxError): + break + + if cid == b"IEND": + break + elif cid == b"fcTL" and self.is_animated: + # start of the next frame, stop reading + self.__prepare_idat = 0 + self.png.push(cid, pos, length) + break + + try: + self.png.call(cid, pos, length) + except UnicodeDecodeError: + break + except EOFError: + if cid == b"fdAT": + length -= 4 + try: + ImageFile._safe_read(self.fp, length) + except OSError as e: + if ImageFile.LOAD_TRUNCATED_IMAGES: + break + else: + raise e + except AttributeError: + logger.debug("%r %s %s (unknown)", cid, pos, length) + s = ImageFile._safe_read(self.fp, length) + if cid[1:2].islower(): + self.private_chunks.append((cid, s, True)) + self._text = self.png.im_text + if not self.is_animated: + self.png.close() + self.png = None + else: + if self._prev_im and self.blend_op == Blend.OP_OVER: + updated = self._crop(self.im, self.dispose_extent) + if self.im.mode == "RGB" and "transparency" in self.info: + mask = updated.convert_transparent( + "RGBA", self.info["transparency"] + ) + else: + mask = updated.convert("RGBA") + self._prev_im.paste(updated, self.dispose_extent, mask) + self.im = self._prev_im + if self.pyaccess: + self.pyaccess = None + + def _getexif(self) -> dict[str, Any] | None: + if "exif" not in self.info: + self.load() + if "exif" not in self.info and "Raw profile type exif" not in self.info: + return None + return self.getexif()._get_merged_dict() + + def getexif(self): + if "exif" not in self.info: + self.load() + + return super().getexif() + + def getxmp(self) -> dict[str, Any]: + """ + Returns a dictionary containing the XMP tags. + Requires defusedxml to be installed. + + :returns: XMP tags in a dictionary. + """ + return ( + self._getxmp(self.info["XML:com.adobe.xmp"]) + if "XML:com.adobe.xmp" in self.info + else {} + ) + + +# -------------------------------------------------------------------- +# PNG writer + +_OUTMODES = { + # supported PIL modes, and corresponding rawmode, bit depth and color type + "1": ("1", b"\x01", b"\x00"), + "L;1": ("L;1", b"\x01", b"\x00"), + "L;2": ("L;2", b"\x02", b"\x00"), + "L;4": ("L;4", b"\x04", b"\x00"), + "L": ("L", b"\x08", b"\x00"), + "LA": ("LA", b"\x08", b"\x04"), + "I": ("I;16B", b"\x10", b"\x00"), + "I;16": ("I;16B", b"\x10", b"\x00"), + "I;16B": ("I;16B", b"\x10", b"\x00"), + "P;1": ("P;1", b"\x01", b"\x03"), + "P;2": ("P;2", b"\x02", b"\x03"), + "P;4": ("P;4", b"\x04", b"\x03"), + "P": ("P", b"\x08", b"\x03"), + "RGB": ("RGB", b"\x08", b"\x02"), + "RGBA": ("RGBA", b"\x08", b"\x06"), +} + + +def putchunk(fp, cid, *data): + """Write a PNG chunk (including CRC field)""" + + data = b"".join(data) + + fp.write(o32(len(data)) + cid) + fp.write(data) + crc = _crc32(data, _crc32(cid)) + fp.write(o32(crc)) + + +class _idat: + # wrap output from the encoder in IDAT chunks + + def __init__(self, fp, chunk): + self.fp = fp + self.chunk = chunk + + def write(self, data: bytes) -> None: + self.chunk(self.fp, b"IDAT", data) + + +class _fdat: + # wrap encoder output in fdAT chunks + + def __init__(self, fp, chunk, seq_num): + self.fp = fp + self.chunk = chunk + self.seq_num = seq_num + + def write(self, data: bytes) -> None: + self.chunk(self.fp, b"fdAT", o32(self.seq_num), data) + self.seq_num += 1 + + +def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images): + duration = im.encoderinfo.get("duration", im.info.get("duration", 0)) + loop = im.encoderinfo.get("loop", im.info.get("loop", 0)) + disposal = im.encoderinfo.get("disposal", im.info.get("disposal", Disposal.OP_NONE)) + blend = im.encoderinfo.get("blend", im.info.get("blend", Blend.OP_SOURCE)) + + if default_image: + chain = itertools.chain(append_images) + else: + chain = itertools.chain([im], append_images) + + im_frames = [] + frame_count = 0 + for im_seq in chain: + for im_frame in ImageSequence.Iterator(im_seq): + if im_frame.mode == rawmode: + im_frame = im_frame.copy() + else: + im_frame = im_frame.convert(rawmode) + encoderinfo = im.encoderinfo.copy() + if isinstance(duration, (list, tuple)): + encoderinfo["duration"] = duration[frame_count] + if isinstance(disposal, (list, tuple)): + encoderinfo["disposal"] = disposal[frame_count] + if isinstance(blend, (list, tuple)): + encoderinfo["blend"] = blend[frame_count] + frame_count += 1 + + if im_frames: + previous = im_frames[-1] + prev_disposal = previous["encoderinfo"].get("disposal") + prev_blend = previous["encoderinfo"].get("blend") + if prev_disposal == Disposal.OP_PREVIOUS and len(im_frames) < 2: + prev_disposal = Disposal.OP_BACKGROUND + + if prev_disposal == Disposal.OP_BACKGROUND: + base_im = previous["im"].copy() + dispose = Image.core.fill("RGBA", im.size, (0, 0, 0, 0)) + bbox = previous["bbox"] + if bbox: + dispose = dispose.crop(bbox) + else: + bbox = (0, 0) + im.size + base_im.paste(dispose, bbox) + elif prev_disposal == Disposal.OP_PREVIOUS: + base_im = im_frames[-2]["im"] + else: + base_im = previous["im"] + delta = ImageChops.subtract_modulo( + im_frame.convert("RGBA"), base_im.convert("RGBA") + ) + bbox = delta.getbbox(alpha_only=False) + if ( + not bbox + and prev_disposal == encoderinfo.get("disposal") + and prev_blend == encoderinfo.get("blend") + ): + previous["encoderinfo"]["duration"] += encoderinfo.get( + "duration", duration + ) + continue + else: + bbox = None + if "duration" not in encoderinfo: + encoderinfo["duration"] = duration + im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo}) + + if len(im_frames) == 1 and not default_image: + return im_frames[0]["im"] + + # animation control + chunk( + fp, + b"acTL", + o32(len(im_frames)), # 0: num_frames + o32(loop), # 4: num_plays + ) + + # default image IDAT (if it exists) + if default_image: + if im.mode != rawmode: + im = im.convert(rawmode) + ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)]) + + seq_num = 0 + for frame, frame_data in enumerate(im_frames): + im_frame = frame_data["im"] + if not frame_data["bbox"]: + bbox = (0, 0) + im_frame.size + else: + bbox = frame_data["bbox"] + im_frame = im_frame.crop(bbox) + size = im_frame.size + encoderinfo = frame_data["encoderinfo"] + frame_duration = int(round(encoderinfo["duration"])) + frame_disposal = encoderinfo.get("disposal", disposal) + frame_blend = encoderinfo.get("blend", blend) + # frame control + chunk( + fp, + b"fcTL", + o32(seq_num), # sequence_number + o32(size[0]), # width + o32(size[1]), # height + o32(bbox[0]), # x_offset + o32(bbox[1]), # y_offset + o16(frame_duration), # delay_numerator + o16(1000), # delay_denominator + o8(frame_disposal), # dispose_op + o8(frame_blend), # blend_op + ) + seq_num += 1 + # frame data + if frame == 0 and not default_image: + # first frame must be in IDAT chunks for backwards compatibility + ImageFile._save( + im_frame, + _idat(fp, chunk), + [("zip", (0, 0) + im_frame.size, 0, rawmode)], + ) + else: + fdat_chunks = _fdat(fp, chunk, seq_num) + ImageFile._save( + im_frame, + fdat_chunks, + [("zip", (0, 0) + im_frame.size, 0, rawmode)], + ) + seq_num = fdat_chunks.seq_num + + +def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + _save(im, fp, filename, save_all=True) + + +def _save(im, fp, filename, chunk=putchunk, save_all=False): + # save an image to disk (called by the save method) + + if save_all: + default_image = im.encoderinfo.get( + "default_image", im.info.get("default_image") + ) + modes = set() + sizes = set() + append_images = im.encoderinfo.get("append_images", []) + for im_seq in itertools.chain([im], append_images): + for im_frame in ImageSequence.Iterator(im_seq): + modes.add(im_frame.mode) + sizes.add(im_frame.size) + for mode in ("RGBA", "RGB", "P"): + if mode in modes: + break + else: + mode = modes.pop() + size = tuple(max(frame_size[i] for frame_size in sizes) for i in range(2)) + else: + size = im.size + mode = im.mode + + if mode == "P": + # + # attempt to minimize storage requirements for palette images + if "bits" in im.encoderinfo: + # number of bits specified by user + colors = min(1 << im.encoderinfo["bits"], 256) + else: + # check palette contents + if im.palette: + colors = max(min(len(im.palette.getdata()[1]) // 3, 256), 1) + else: + colors = 256 + + if colors <= 16: + if colors <= 2: + bits = 1 + elif colors <= 4: + bits = 2 + else: + bits = 4 + mode = f"{mode};{bits}" + + # encoder options + im.encoderconfig = ( + im.encoderinfo.get("optimize", False), + im.encoderinfo.get("compress_level", -1), + im.encoderinfo.get("compress_type", -1), + im.encoderinfo.get("dictionary", b""), + ) + + # get the corresponding PNG mode + try: + rawmode, bit_depth, color_type = _OUTMODES[mode] + except KeyError as e: + msg = f"cannot write mode {mode} as PNG" + raise OSError(msg) from e + + # + # write minimal PNG file + + fp.write(_MAGIC) + + chunk( + fp, + b"IHDR", + o32(size[0]), # 0: size + o32(size[1]), + bit_depth, + color_type, + b"\0", # 10: compression + b"\0", # 11: filter category + b"\0", # 12: interlace flag + ) + + chunks = [b"cHRM", b"gAMA", b"sBIT", b"sRGB", b"tIME"] + + icc = im.encoderinfo.get("icc_profile", im.info.get("icc_profile")) + if icc: + # ICC profile + # according to PNG spec, the iCCP chunk contains: + # Profile name 1-79 bytes (character string) + # Null separator 1 byte (null character) + # Compression method 1 byte (0) + # Compressed profile n bytes (zlib with deflate compression) + name = b"ICC Profile" + data = name + b"\0\0" + zlib.compress(icc) + chunk(fp, b"iCCP", data) + + # You must either have sRGB or iCCP. + # Disallow sRGB chunks when an iCCP-chunk has been emitted. + chunks.remove(b"sRGB") + + info = im.encoderinfo.get("pnginfo") + if info: + chunks_multiple_allowed = [b"sPLT", b"iTXt", b"tEXt", b"zTXt"] + for info_chunk in info.chunks: + cid, data = info_chunk[:2] + if cid in chunks: + chunks.remove(cid) + chunk(fp, cid, data) + elif cid in chunks_multiple_allowed: + chunk(fp, cid, data) + elif cid[1:2].islower(): + # Private chunk + after_idat = info_chunk[2:3] + if not after_idat: + chunk(fp, cid, data) + + if im.mode == "P": + palette_byte_number = colors * 3 + palette_bytes = im.im.getpalette("RGB")[:palette_byte_number] + while len(palette_bytes) < palette_byte_number: + palette_bytes += b"\0" + chunk(fp, b"PLTE", palette_bytes) + + transparency = im.encoderinfo.get("transparency", im.info.get("transparency", None)) + + if transparency or transparency == 0: + if im.mode == "P": + # limit to actual palette size + alpha_bytes = colors + if isinstance(transparency, bytes): + chunk(fp, b"tRNS", transparency[:alpha_bytes]) + else: + transparency = max(0, min(255, transparency)) + alpha = b"\xFF" * transparency + b"\0" + chunk(fp, b"tRNS", alpha[:alpha_bytes]) + elif im.mode in ("1", "L", "I", "I;16"): + transparency = max(0, min(65535, transparency)) + chunk(fp, b"tRNS", o16(transparency)) + elif im.mode == "RGB": + red, green, blue = transparency + chunk(fp, b"tRNS", o16(red) + o16(green) + o16(blue)) + else: + if "transparency" in im.encoderinfo: + # don't bother with transparency if it's an RGBA + # and it's in the info dict. It's probably just stale. + msg = "cannot use transparency for this mode" + raise OSError(msg) + else: + if im.mode == "P" and im.im.getpalettemode() == "RGBA": + alpha = im.im.getpalette("RGBA", "A") + alpha_bytes = colors + chunk(fp, b"tRNS", alpha[:alpha_bytes]) + + dpi = im.encoderinfo.get("dpi") + if dpi: + chunk( + fp, + b"pHYs", + o32(int(dpi[0] / 0.0254 + 0.5)), + o32(int(dpi[1] / 0.0254 + 0.5)), + b"\x01", + ) + + if info: + chunks = [b"bKGD", b"hIST"] + for info_chunk in info.chunks: + cid, data = info_chunk[:2] + if cid in chunks: + chunks.remove(cid) + chunk(fp, cid, data) + + exif = im.encoderinfo.get("exif") + if exif: + if isinstance(exif, Image.Exif): + exif = exif.tobytes(8) + if exif.startswith(b"Exif\x00\x00"): + exif = exif[6:] + chunk(fp, b"eXIf", exif) + + if save_all: + im = _write_multiple_frames( + im, fp, chunk, rawmode, default_image, append_images + ) + if im: + ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)]) + + if info: + for info_chunk in info.chunks: + cid, data = info_chunk[:2] + if cid[1:2].islower(): + # Private chunk + after_idat = info_chunk[2:3] + if after_idat: + chunk(fp, cid, data) + + chunk(fp, b"IEND", b"") + + if hasattr(fp, "flush"): + fp.flush() + + +# -------------------------------------------------------------------- +# PNG chunk converter + + +def getchunks(im, **params): + """Return a list of PNG chunks representing this image.""" + + class collector: + data = [] + + def write(self, data: bytes) -> None: + pass + + def append(self, chunk: bytes) -> None: + self.data.append(chunk) + + def append(fp, cid, *data): + data = b"".join(data) + crc = o32(_crc32(data, _crc32(cid))) + fp.append((cid, data, crc)) + + fp = collector() + + try: + im.encoderinfo = params + _save(im, fp, None, append) + finally: + del im.encoderinfo + + return fp.data + + +# -------------------------------------------------------------------- +# Registry + +Image.register_open(PngImageFile.format, PngImageFile, _accept) +Image.register_save(PngImageFile.format, _save) +Image.register_save_all(PngImageFile.format, _save_all) + +Image.register_extensions(PngImageFile.format, [".png", ".apng"]) + +Image.register_mime(PngImageFile.format, "image/png") diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 16c9ccbba..64112368b 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -1,371 +1,371 @@ -# -# The Python Imaging Library. -# $Id$ -# -# PPM support for PIL -# -# History: -# 96-03-24 fl Created -# 98-03-06 fl Write RGBA images (as RGB, that is) -# -# Copyright (c) Secret Labs AB 1997-98. -# Copyright (c) Fredrik Lundh 1996. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import math -from typing import IO - -from . import Image, ImageFile -from ._binary import i16be as i16 -from ._binary import o8 -from ._binary import o32le as o32 - -# -# -------------------------------------------------------------------- - -b_whitespace = b"\x20\x09\x0a\x0b\x0c\x0d" - -MODES = { - # standard - b"P1": "1", - b"P2": "L", - b"P3": "RGB", - b"P4": "1", - b"P5": "L", - b"P6": "RGB", - # extensions - b"P0CMYK": "CMYK", - b"Pf": "F", - # PIL extensions (for test purposes only) - b"PyP": "P", - b"PyRGBA": "RGBA", - b"PyCMYK": "CMYK", -} - - -def _accept(prefix: bytes) -> bool: - return prefix[0:1] == b"P" and prefix[1] in b"0123456fy" - - -## -# Image plugin for PBM, PGM, and PPM images. - - -class PpmImageFile(ImageFile.ImageFile): - format = "PPM" - format_description = "Pbmplus image" - - def _read_magic(self) -> bytes: - assert self.fp is not None - - magic = b"" - # read until whitespace or longest available magic number - for _ in range(6): - c = self.fp.read(1) - if not c or c in b_whitespace: - break - magic += c - return magic - - def _read_token(self) -> bytes: - assert self.fp is not None - - token = b"" - while len(token) <= 10: # read until next whitespace or limit of 10 characters - c = self.fp.read(1) - if not c: - break - elif c in b_whitespace: # token ended - if not token: - # skip whitespace at start - continue - break - elif c == b"#": - # ignores rest of the line; stops at CR, LF or EOF - while self.fp.read(1) not in b"\r\n": - pass - continue - token += c - if not token: - # Token was not even 1 byte - 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) - return token - - def _open(self) -> None: - assert self.fp is not None - - magic_number = self._read_magic() - try: - mode = MODES[magic_number] - except KeyError: - msg = "not a PPM file" - raise SyntaxError(msg) - self._mode = mode - - if magic_number in (b"P1", b"P4"): - self.custom_mimetype = "image/x-portable-bitmap" - elif magic_number in (b"P2", b"P5"): - self.custom_mimetype = "image/x-portable-graymap" - elif magic_number in (b"P3", b"P6"): - self.custom_mimetype = "image/x-portable-pixmap" - - self._size = int(self._read_token()), int(self._read_token()) - - decoder_name = "raw" - if magic_number in (b"P1", b"P2", b"P3"): - decoder_name = "ppm_plain" - - args: str | tuple[str | int, ...] - if mode == "1": - args = "1;I" - elif mode == "F": - scale = float(self._read_token()) - if scale == 0.0 or not math.isfinite(scale): - msg = "scale must be finite and non-zero" - raise ValueError(msg) - self.info["scale"] = abs(scale) - - rawmode = "F;32F" if scale < 0 else "F;32BF" - args = (rawmode, 0, -1) - else: - maxval = int(self._read_token()) - if not 0 < maxval < 65536: - msg = "maxval must be greater than 0 and less than 65536" - raise ValueError(msg) - if maxval > 255 and mode == "L": - self._mode = "I" - - rawmode = mode - if decoder_name != "ppm_plain": - # If maxval matches a bit depth, use the raw decoder directly - if maxval == 65535 and mode == "L": - rawmode = "I;16B" - elif maxval != 255: - decoder_name = "ppm" - - args = rawmode if decoder_name == "raw" else (rawmode, maxval) - self.tile = [(decoder_name, (0, 0) + self.size, self.fp.tell(), args)] - - -# -# -------------------------------------------------------------------- - - -class PpmPlainDecoder(ImageFile.PyDecoder): - _pulls_fd = True - _comment_spans: bool - - def _read_block(self) -> bytes: - assert self.fd is not None - - return self.fd.read(ImageFile.SAFEBLOCK) - - def _find_comment_end(self, block: bytes, start: int = 0) -> int: - a = block.find(b"\n", start) - b = block.find(b"\r", start) - return min(a, b) if a * b > 0 else max(a, b) # lowest nonnegative index (or -1) - - def _ignore_comments(self, block: bytes) -> bytes: - if self._comment_spans: - # Finish current comment - while block: - comment_end = self._find_comment_end(block) - if comment_end != -1: - # Comment ends in this block - # Delete tail of comment - block = block[comment_end + 1 :] - break - else: - # Comment spans whole block - # So read the next block, looking for the end - block = self._read_block() - - # Search for any further comments - self._comment_spans = False - while True: - comment_start = block.find(b"#") - if comment_start == -1: - # No comment found - break - comment_end = self._find_comment_end(block, comment_start) - if comment_end != -1: - # Comment ends in this block - # Delete comment - block = block[:comment_start] + block[comment_end + 1 :] - else: - # Comment continues to next block(s) - block = block[:comment_start] - self._comment_spans = True - break - return block - - def _decode_bitonal(self) -> bytearray: - """ - This is a separate method because in the plain PBM format, all data tokens are - exactly one byte, so the inter-token whitespace is optional. - """ - data = bytearray() - total_bytes = self.state.xsize * self.state.ysize - - while len(data) != total_bytes: - block = self._read_block() # read next block - if not block: - # eof - break - - block = self._ignore_comments(block) - - tokens = b"".join(block.split()) - for token in tokens: - if token not in (48, 49): - msg = b"Invalid token for this mode: %s" % bytes([token]) - raise ValueError(msg) - data = (data + tokens)[:total_bytes] - invert = bytes.maketrans(b"01", b"\xFF\x00") - return data.translate(invert) - - def _decode_blocks(self, maxval: int) -> bytearray: - data = bytearray() - max_len = 10 - out_byte_count = 4 if self.mode == "I" else 1 - out_max = 65535 if self.mode == "I" else 255 - bands = Image.getmodebands(self.mode) - total_bytes = self.state.xsize * self.state.ysize * bands * out_byte_count - - half_token = b"" - while len(data) != total_bytes: - block = self._read_block() # read next block - if not block: - if half_token: - block = bytearray(b" ") # flush half_token - else: - # eof - break - - block = self._ignore_comments(block) - - if half_token: - block = half_token + block # stitch half_token to new block - half_token = b"" - - tokens = block.split() - - if block and not block[-1:].isspace(): # block might split token - half_token = tokens.pop() # save half token for later - if len(half_token) > max_len: # prevent buildup of half_token - msg = ( - b"Token too long found in data: %s" % half_token[: max_len + 1] - ) - raise ValueError(msg) - - for token in tokens: - if len(token) > max_len: - msg = b"Token too long found in data: %s" % token[: max_len + 1] - raise ValueError(msg) - value = int(token) - if value < 0: - msg_str = f"Channel value is negative: {value}" - raise ValueError(msg_str) - if value > maxval: - msg_str = f"Channel value too large for this mode: {value}" - raise ValueError(msg_str) - value = round(value / maxval * out_max) - data += o32(value) if self.mode == "I" else o8(value) - if len(data) == total_bytes: # finished! - break - return data - - def decode(self, buffer: bytes) -> tuple[int, int]: - self._comment_spans = False - if self.mode == "1": - data = self._decode_bitonal() - rawmode = "1;8" - else: - maxval = self.args[-1] - data = self._decode_blocks(maxval) - rawmode = "I;32" if self.mode == "I" else self.mode - self.set_as_raw(bytes(data), rawmode) - return -1, 0 - - -class PpmDecoder(ImageFile.PyDecoder): - _pulls_fd = True - - def decode(self, buffer: bytes) -> tuple[int, int]: - assert self.fd is not None - - data = bytearray() - maxval = self.args[-1] - in_byte_count = 1 if maxval < 256 else 2 - out_byte_count = 4 if self.mode == "I" else 1 - out_max = 65535 if self.mode == "I" else 255 - bands = Image.getmodebands(self.mode) - dest_length = self.state.xsize * self.state.ysize * bands * out_byte_count - while len(data) < dest_length: - pixels = self.fd.read(in_byte_count * bands) - if len(pixels) < in_byte_count * bands: - # eof - break - for b in range(bands): - value = ( - pixels[b] if in_byte_count == 1 else i16(pixels, b * in_byte_count) - ) - value = min(out_max, round(value / maxval * out_max)) - data += o32(value) if self.mode == "I" else o8(value) - rawmode = "I;32" if self.mode == "I" else self.mode - self.set_as_raw(bytes(data), rawmode) - return -1, 0 - - -# -# -------------------------------------------------------------------- - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - if im.mode == "1": - rawmode, head = "1;I", b"P4" - elif im.mode == "L": - rawmode, head = "L", b"P5" - elif im.mode == "I": - rawmode, head = "I;16B", b"P5" - elif im.mode in ("RGB", "RGBA"): - rawmode, head = "RGB", b"P6" - elif im.mode == "F": - rawmode, head = "F;32F", b"Pf" - else: - msg = f"cannot write mode {im.mode} as PPM" - raise OSError(msg) - fp.write(head + b"\n%d %d\n" % im.size) - if head == b"P6": - fp.write(b"255\n") - elif head == b"P5": - if rawmode == "L": - fp.write(b"255\n") - else: - fp.write(b"65535\n") - elif head == b"Pf": - fp.write(b"-1.0\n") - row_order = -1 if im.mode == "F" else 1 - ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, row_order))]) - - -# -# -------------------------------------------------------------------- - - -Image.register_open(PpmImageFile.format, PpmImageFile, _accept) -Image.register_save(PpmImageFile.format, _save) - -Image.register_decoder("ppm", PpmDecoder) -Image.register_decoder("ppm_plain", PpmPlainDecoder) - -Image.register_extensions(PpmImageFile.format, [".pbm", ".pgm", ".ppm", ".pnm", ".pfm"]) - -Image.register_mime(PpmImageFile.format, "image/x-portable-anymap") +# +# The Python Imaging Library. +# $Id$ +# +# PPM support for PIL +# +# History: +# 96-03-24 fl Created +# 98-03-06 fl Write RGBA images (as RGB, that is) +# +# Copyright (c) Secret Labs AB 1997-98. +# Copyright (c) Fredrik Lundh 1996. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import math +from typing import IO + +from . import Image, ImageFile +from ._binary import i16be as i16 +from ._binary import o8 +from ._binary import o32le as o32 + +# +# -------------------------------------------------------------------- + +b_whitespace = b"\x20\x09\x0a\x0b\x0c\x0d" + +MODES = { + # standard + b"P1": "1", + b"P2": "L", + b"P3": "RGB", + b"P4": "1", + b"P5": "L", + b"P6": "RGB", + # extensions + b"P0CMYK": "CMYK", + b"Pf": "F", + # PIL extensions (for test purposes only) + b"PyP": "P", + b"PyRGBA": "RGBA", + b"PyCMYK": "CMYK", +} + + +def _accept(prefix: bytes) -> bool: + return prefix[0:1] == b"P" and prefix[1] in b"0123456fy" + + +## +# Image plugin for PBM, PGM, and PPM images. + + +class PpmImageFile(ImageFile.ImageFile): + format = "PPM" + format_description = "Pbmplus image" + + def _read_magic(self) -> bytes: + assert self.fp is not None + + magic = b"" + # read until whitespace or longest available magic number + for _ in range(6): + c = self.fp.read(1) + if not c or c in b_whitespace: + break + magic += c + return magic + + def _read_token(self) -> bytes: + assert self.fp is not None + + token = b"" + while len(token) <= 10: # read until next whitespace or limit of 10 characters + c = self.fp.read(1) + if not c: + break + elif c in b_whitespace: # token ended + if not token: + # skip whitespace at start + continue + break + elif c == b"#": + # ignores rest of the line; stops at CR, LF or EOF + while self.fp.read(1) not in b"\r\n": + pass + continue + token += c + if not token: + # Token was not even 1 byte + 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) + return token + + def _open(self) -> None: + assert self.fp is not None + + magic_number = self._read_magic() + try: + mode = MODES[magic_number] + except KeyError: + msg = "not a PPM file" + raise SyntaxError(msg) + self._mode = mode + + if magic_number in (b"P1", b"P4"): + self.custom_mimetype = "image/x-portable-bitmap" + elif magic_number in (b"P2", b"P5"): + self.custom_mimetype = "image/x-portable-graymap" + elif magic_number in (b"P3", b"P6"): + self.custom_mimetype = "image/x-portable-pixmap" + + self._size = int(self._read_token()), int(self._read_token()) + + decoder_name = "raw" + if magic_number in (b"P1", b"P2", b"P3"): + decoder_name = "ppm_plain" + + args: str | tuple[str | int, ...] + if mode == "1": + args = "1;I" + elif mode == "F": + scale = float(self._read_token()) + if scale == 0.0 or not math.isfinite(scale): + msg = "scale must be finite and non-zero" + raise ValueError(msg) + self.info["scale"] = abs(scale) + + rawmode = "F;32F" if scale < 0 else "F;32BF" + args = (rawmode, 0, -1) + else: + maxval = int(self._read_token()) + if not 0 < maxval < 65536: + msg = "maxval must be greater than 0 and less than 65536" + raise ValueError(msg) + if maxval > 255 and mode == "L": + self._mode = "I" + + rawmode = mode + if decoder_name != "ppm_plain": + # If maxval matches a bit depth, use the raw decoder directly + if maxval == 65535 and mode == "L": + rawmode = "I;16B" + elif maxval != 255: + decoder_name = "ppm" + + args = rawmode if decoder_name == "raw" else (rawmode, maxval) + self.tile = [(decoder_name, (0, 0) + self.size, self.fp.tell(), args)] + + +# +# -------------------------------------------------------------------- + + +class PpmPlainDecoder(ImageFile.PyDecoder): + _pulls_fd = True + _comment_spans: bool + + def _read_block(self) -> bytes: + assert self.fd is not None + + return self.fd.read(ImageFile.SAFEBLOCK) + + def _find_comment_end(self, block: bytes, start: int = 0) -> int: + a = block.find(b"\n", start) + b = block.find(b"\r", start) + return min(a, b) if a * b > 0 else max(a, b) # lowest nonnegative index (or -1) + + def _ignore_comments(self, block: bytes) -> bytes: + if self._comment_spans: + # Finish current comment + while block: + comment_end = self._find_comment_end(block) + if comment_end != -1: + # Comment ends in this block + # Delete tail of comment + block = block[comment_end + 1 :] + break + else: + # Comment spans whole block + # So read the next block, looking for the end + block = self._read_block() + + # Search for any further comments + self._comment_spans = False + while True: + comment_start = block.find(b"#") + if comment_start == -1: + # No comment found + break + comment_end = self._find_comment_end(block, comment_start) + if comment_end != -1: + # Comment ends in this block + # Delete comment + block = block[:comment_start] + block[comment_end + 1 :] + else: + # Comment continues to next block(s) + block = block[:comment_start] + self._comment_spans = True + break + return block + + def _decode_bitonal(self) -> bytearray: + """ + This is a separate method because in the plain PBM format, all data tokens are + exactly one byte, so the inter-token whitespace is optional. + """ + data = bytearray() + total_bytes = self.state.xsize * self.state.ysize + + while len(data) != total_bytes: + block = self._read_block() # read next block + if not block: + # eof + break + + block = self._ignore_comments(block) + + tokens = b"".join(block.split()) + for token in tokens: + if token not in (48, 49): + msg = b"Invalid token for this mode: %s" % bytes([token]) + raise ValueError(msg) + data = (data + tokens)[:total_bytes] + invert = bytes.maketrans(b"01", b"\xFF\x00") + return data.translate(invert) + + def _decode_blocks(self, maxval: int) -> bytearray: + data = bytearray() + max_len = 10 + out_byte_count = 4 if self.mode == "I" else 1 + out_max = 65535 if self.mode == "I" else 255 + bands = Image.getmodebands(self.mode) + total_bytes = self.state.xsize * self.state.ysize * bands * out_byte_count + + half_token = b"" + while len(data) != total_bytes: + block = self._read_block() # read next block + if not block: + if half_token: + block = bytearray(b" ") # flush half_token + else: + # eof + break + + block = self._ignore_comments(block) + + if half_token: + block = half_token + block # stitch half_token to new block + half_token = b"" + + tokens = block.split() + + if block and not block[-1:].isspace(): # block might split token + half_token = tokens.pop() # save half token for later + if len(half_token) > max_len: # prevent buildup of half_token + msg = ( + b"Token too long found in data: %s" % half_token[: max_len + 1] + ) + raise ValueError(msg) + + for token in tokens: + if len(token) > max_len: + msg = b"Token too long found in data: %s" % token[: max_len + 1] + raise ValueError(msg) + value = int(token) + if value < 0: + msg_str = f"Channel value is negative: {value}" + raise ValueError(msg_str) + if value > maxval: + msg_str = f"Channel value too large for this mode: {value}" + raise ValueError(msg_str) + value = round(value / maxval * out_max) + data += o32(value) if self.mode == "I" else o8(value) + if len(data) == total_bytes: # finished! + break + return data + + def decode(self, buffer: bytes) -> tuple[int, int]: + self._comment_spans = False + if self.mode == "1": + data = self._decode_bitonal() + rawmode = "1;8" + else: + maxval = self.args[-1] + data = self._decode_blocks(maxval) + rawmode = "I;32" if self.mode == "I" else self.mode + self.set_as_raw(bytes(data), rawmode) + return -1, 0 + + +class PpmDecoder(ImageFile.PyDecoder): + _pulls_fd = True + + def decode(self, buffer: bytes) -> tuple[int, int]: + assert self.fd is not None + + data = bytearray() + maxval = self.args[-1] + in_byte_count = 1 if maxval < 256 else 2 + out_byte_count = 4 if self.mode == "I" else 1 + out_max = 65535 if self.mode == "I" else 255 + bands = Image.getmodebands(self.mode) + dest_length = self.state.xsize * self.state.ysize * bands * out_byte_count + while len(data) < dest_length: + pixels = self.fd.read(in_byte_count * bands) + if len(pixels) < in_byte_count * bands: + # eof + break + for b in range(bands): + value = ( + pixels[b] if in_byte_count == 1 else i16(pixels, b * in_byte_count) + ) + value = min(out_max, round(value / maxval * out_max)) + data += o32(value) if self.mode == "I" else o8(value) + rawmode = "I;32" if self.mode == "I" else self.mode + self.set_as_raw(bytes(data), rawmode) + return -1, 0 + + +# +# -------------------------------------------------------------------- + + +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + if im.mode == "1": + rawmode, head = "1;I", b"P4" + elif im.mode == "L": + rawmode, head = "L", b"P5" + elif im.mode == "I": + rawmode, head = "I;16B", b"P5" + elif im.mode in ("RGB", "RGBA"): + rawmode, head = "RGB", b"P6" + elif im.mode == "F": + rawmode, head = "F;32F", b"Pf" + else: + msg = f"cannot write mode {im.mode} as PPM" + raise OSError(msg) + fp.write(head + b"\n%d %d\n" % im.size) + if head == b"P6": + fp.write(b"255\n") + elif head == b"P5": + if rawmode == "L": + fp.write(b"255\n") + else: + fp.write(b"65535\n") + elif head == b"Pf": + fp.write(b"-1.0\n") + row_order = -1 if im.mode == "F" else 1 + ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, row_order))]) + + +# +# -------------------------------------------------------------------- + + +Image.register_open(PpmImageFile.format, PpmImageFile, _accept) +Image.register_save(PpmImageFile.format, _save) + +Image.register_decoder("ppm", PpmDecoder) +Image.register_decoder("ppm_plain", PpmPlainDecoder) + +Image.register_extensions(PpmImageFile.format, [".pbm", ".pgm", ".ppm", ".pnm", ".pfm"]) + +Image.register_mime(PpmImageFile.format, "image/x-portable-anymap") diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index 86c1a6763..7366dda47 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -1,306 +1,306 @@ -# -# The Python Imaging Library -# $Id$ -# -# Adobe PSD 2.5/3.0 file handling -# -# History: -# 1995-09-01 fl Created -# 1997-01-03 fl Read most PSD images -# 1997-01-18 fl Fixed P and CMYK support -# 2001-10-21 fl Added seek/tell support (for layers) -# -# Copyright (c) 1997-2001 by Secret Labs AB. -# Copyright (c) 1995-2001 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import io - -from . import Image, ImageFile, ImagePalette -from ._binary import i8 -from ._binary import i16be as i16 -from ._binary import i32be as i32 -from ._binary import si16be as si16 -from ._binary import si32be as si32 - -MODES = { - # (photoshop mode, bits) -> (pil mode, required channels) - (0, 1): ("1", 1), - (0, 8): ("L", 1), - (1, 8): ("L", 1), - (2, 8): ("P", 1), - (3, 8): ("RGB", 3), - (4, 8): ("CMYK", 4), - (7, 8): ("L", 1), # FIXME: multilayer - (8, 8): ("L", 1), # duotone - (9, 8): ("LAB", 3), -} - - -# --------------------------------------------------------------------. -# read PSD images - - -def _accept(prefix: bytes) -> bool: - return prefix[:4] == b"8BPS" - - -## -# Image plugin for Photoshop images. - - -class PsdImageFile(ImageFile.ImageFile): - format = "PSD" - format_description = "Adobe Photoshop" - _close_exclusive_fp_after_loading = False - - def _open(self) -> None: - read = self.fp.read - - # - # header - - s = read(26) - if not _accept(s) or i16(s, 4) != 1: - msg = "not a PSD file" - raise SyntaxError(msg) - - psd_bits = i16(s, 22) - psd_channels = i16(s, 12) - psd_mode = i16(s, 24) - - mode, channels = MODES[(psd_mode, psd_bits)] - - if channels > psd_channels: - msg = "not enough channels" - raise OSError(msg) - if mode == "RGB" and psd_channels == 4: - mode = "RGBA" - channels = 4 - - self._mode = mode - self._size = i32(s, 18), i32(s, 14) - - # - # color mode data - - size = i32(read(4)) - if size: - data = read(size) - if mode == "P" and size == 768: - self.palette = ImagePalette.raw("RGB;L", data) - - # - # image resources - - self.resources = [] - - size = i32(read(4)) - if size: - # load resources - end = self.fp.tell() + size - while self.fp.tell() < end: - read(4) # signature - id = i16(read(2)) - name = read(i8(read(1))) - if not (len(name) & 1): - read(1) # padding - data = read(i32(read(4))) - if len(data) & 1: - read(1) # padding - self.resources.append((id, name, data)) - if id == 1039: # ICC profile - self.info["icc_profile"] = data - - # - # layer and mask information - - self.layers = [] - - size = i32(read(4)) - if size: - end = self.fp.tell() + size - size = i32(read(4)) - if size: - _layer_data = io.BytesIO(ImageFile._safe_read(self.fp, size)) - self.layers = _layerinfo(_layer_data, size) - self.fp.seek(end) - self.n_frames = len(self.layers) - self.is_animated = self.n_frames > 1 - - # - # image descriptor - - self.tile = _maketile(self.fp, mode, (0, 0) + self.size, channels) - - # keep the file open - self._fp = self.fp - self.frame = 1 - self._min_frame = 1 - - def seek(self, layer: int) -> None: - if not self._seek_check(layer): - return - - # 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 - - def tell(self) -> int: - # return layer number (0=image, 1..max=layers) - return self.frame - - -def _layerinfo(fp, ct_bytes): - # read layerinfo block - layers = [] - - def read(size): - return ImageFile._safe_read(fp, size) - - ct = si16(read(2)) - - # sanity check - if ct_bytes < (abs(ct) * 20): - msg = "Layer block too short for number of layers requested" - raise SyntaxError(msg) - - for _ in range(abs(ct)): - # bounding box - y0 = si32(read(4)) - x0 = si32(read(4)) - y1 = si32(read(4)) - x1 = si32(read(4)) - - # image info - mode = [] - ct_types = i16(read(2)) - if ct_types > 4: - fp.seek(ct_types * 6 + 12, io.SEEK_CUR) - size = i32(read(4)) - fp.seek(size, io.SEEK_CUR) - continue - - for _ in range(ct_types): - type = i16(read(2)) - - if type == 65535: - m = "A" - else: - m = "RGBA"[type] - - mode.append(m) - read(4) # size - - # figure out the image mode - mode.sort() - if mode == ["R"]: - mode = "L" - elif mode == ["B", "G", "R"]: - mode = "RGB" - elif mode == ["A", "B", "G", "R"]: - mode = "RGBA" - else: - mode = None # unknown - - # skip over blend flags and extra information - read(12) # filler - name = "" - size = i32(read(4)) # length of the extra data field - if size: - data_end = fp.tell() + size - - length = i32(read(4)) - if length: - fp.seek(length - 16, io.SEEK_CUR) - - length = i32(read(4)) - if length: - fp.seek(length, io.SEEK_CUR) - - length = i8(read(1)) - if length: - # Don't know the proper encoding, - # Latin-1 should be a good guess - name = read(length).decode("latin-1", "replace") - - fp.seek(data_end) - layers.append((name, mode, (x0, y0, x1, y1))) - - # get tiles - for i, (name, mode, bbox) in enumerate(layers): - tile = [] - for m in mode: - t = _maketile(fp, m, bbox, 1) - if t: - tile.extend(t) - layers[i] = name, mode, bbox, tile - - return layers - - -def _maketile(file, mode, bbox, channels): - tile = None - read = file.read - - compression = i16(read(2)) - - xsize = bbox[2] - bbox[0] - ysize = bbox[3] - bbox[1] - - offset = file.tell() - - if compression == 0: - # - # raw compression - tile = [] - for channel in range(channels): - layer = mode[channel] - if mode == "CMYK": - layer += ";I" - tile.append(("raw", bbox, offset, layer)) - offset = offset + xsize * ysize - - elif compression == 1: - # - # packbits compression - i = 0 - tile = [] - bytecount = read(channels * ysize * 2) - offset = file.tell() - for channel in range(channels): - layer = mode[channel] - if mode == "CMYK": - layer += ";I" - tile.append(("packbits", bbox, offset, layer)) - for y in range(ysize): - offset = offset + i16(bytecount, i) - i += 2 - - file.seek(offset) - - if offset & 1: - read(1) # padding - - return tile - - -# -------------------------------------------------------------------- -# registry - - -Image.register_open(PsdImageFile.format, PsdImageFile, _accept) - -Image.register_extension(PsdImageFile.format, ".psd") - -Image.register_mime(PsdImageFile.format, "image/vnd.adobe.photoshop") +# +# The Python Imaging Library +# $Id$ +# +# Adobe PSD 2.5/3.0 file handling +# +# History: +# 1995-09-01 fl Created +# 1997-01-03 fl Read most PSD images +# 1997-01-18 fl Fixed P and CMYK support +# 2001-10-21 fl Added seek/tell support (for layers) +# +# Copyright (c) 1997-2001 by Secret Labs AB. +# Copyright (c) 1995-2001 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import io + +from . import Image, ImageFile, ImagePalette +from ._binary import i8 +from ._binary import i16be as i16 +from ._binary import i32be as i32 +from ._binary import si16be as si16 +from ._binary import si32be as si32 + +MODES = { + # (photoshop mode, bits) -> (pil mode, required channels) + (0, 1): ("1", 1), + (0, 8): ("L", 1), + (1, 8): ("L", 1), + (2, 8): ("P", 1), + (3, 8): ("RGB", 3), + (4, 8): ("CMYK", 4), + (7, 8): ("L", 1), # FIXME: multilayer + (8, 8): ("L", 1), # duotone + (9, 8): ("LAB", 3), +} + + +# --------------------------------------------------------------------. +# read PSD images + + +def _accept(prefix: bytes) -> bool: + return prefix[:4] == b"8BPS" + + +## +# Image plugin for Photoshop images. + + +class PsdImageFile(ImageFile.ImageFile): + format = "PSD" + format_description = "Adobe Photoshop" + _close_exclusive_fp_after_loading = False + + def _open(self) -> None: + read = self.fp.read + + # + # header + + s = read(26) + if not _accept(s) or i16(s, 4) != 1: + msg = "not a PSD file" + raise SyntaxError(msg) + + psd_bits = i16(s, 22) + psd_channels = i16(s, 12) + psd_mode = i16(s, 24) + + mode, channels = MODES[(psd_mode, psd_bits)] + + if channels > psd_channels: + msg = "not enough channels" + raise OSError(msg) + if mode == "RGB" and psd_channels == 4: + mode = "RGBA" + channels = 4 + + self._mode = mode + self._size = i32(s, 18), i32(s, 14) + + # + # color mode data + + size = i32(read(4)) + if size: + data = read(size) + if mode == "P" and size == 768: + self.palette = ImagePalette.raw("RGB;L", data) + + # + # image resources + + self.resources = [] + + size = i32(read(4)) + if size: + # load resources + end = self.fp.tell() + size + while self.fp.tell() < end: + read(4) # signature + id = i16(read(2)) + name = read(i8(read(1))) + if not (len(name) & 1): + read(1) # padding + data = read(i32(read(4))) + if len(data) & 1: + read(1) # padding + self.resources.append((id, name, data)) + if id == 1039: # ICC profile + self.info["icc_profile"] = data + + # + # layer and mask information + + self.layers = [] + + size = i32(read(4)) + if size: + end = self.fp.tell() + size + size = i32(read(4)) + if size: + _layer_data = io.BytesIO(ImageFile._safe_read(self.fp, size)) + self.layers = _layerinfo(_layer_data, size) + self.fp.seek(end) + self.n_frames = len(self.layers) + self.is_animated = self.n_frames > 1 + + # + # image descriptor + + self.tile = _maketile(self.fp, mode, (0, 0) + self.size, channels) + + # keep the file open + self._fp = self.fp + self.frame = 1 + self._min_frame = 1 + + def seek(self, layer: int) -> None: + if not self._seek_check(layer): + return + + # 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 + + def tell(self) -> int: + # return layer number (0=image, 1..max=layers) + return self.frame + + +def _layerinfo(fp, ct_bytes): + # read layerinfo block + layers = [] + + def read(size): + return ImageFile._safe_read(fp, size) + + ct = si16(read(2)) + + # sanity check + if ct_bytes < (abs(ct) * 20): + msg = "Layer block too short for number of layers requested" + raise SyntaxError(msg) + + for _ in range(abs(ct)): + # bounding box + y0 = si32(read(4)) + x0 = si32(read(4)) + y1 = si32(read(4)) + x1 = si32(read(4)) + + # image info + mode = [] + ct_types = i16(read(2)) + if ct_types > 4: + fp.seek(ct_types * 6 + 12, io.SEEK_CUR) + size = i32(read(4)) + fp.seek(size, io.SEEK_CUR) + continue + + for _ in range(ct_types): + type = i16(read(2)) + + if type == 65535: + m = "A" + else: + m = "RGBA"[type] + + mode.append(m) + read(4) # size + + # figure out the image mode + mode.sort() + if mode == ["R"]: + mode = "L" + elif mode == ["B", "G", "R"]: + mode = "RGB" + elif mode == ["A", "B", "G", "R"]: + mode = "RGBA" + else: + mode = None # unknown + + # skip over blend flags and extra information + read(12) # filler + name = "" + size = i32(read(4)) # length of the extra data field + if size: + data_end = fp.tell() + size + + length = i32(read(4)) + if length: + fp.seek(length - 16, io.SEEK_CUR) + + length = i32(read(4)) + if length: + fp.seek(length, io.SEEK_CUR) + + length = i8(read(1)) + if length: + # Don't know the proper encoding, + # Latin-1 should be a good guess + name = read(length).decode("latin-1", "replace") + + fp.seek(data_end) + layers.append((name, mode, (x0, y0, x1, y1))) + + # get tiles + for i, (name, mode, bbox) in enumerate(layers): + tile = [] + for m in mode: + t = _maketile(fp, m, bbox, 1) + if t: + tile.extend(t) + layers[i] = name, mode, bbox, tile + + return layers + + +def _maketile(file, mode, bbox, channels): + tile = None + read = file.read + + compression = i16(read(2)) + + xsize = bbox[2] - bbox[0] + ysize = bbox[3] - bbox[1] + + offset = file.tell() + + if compression == 0: + # + # raw compression + tile = [] + for channel in range(channels): + layer = mode[channel] + if mode == "CMYK": + layer += ";I" + tile.append(("raw", bbox, offset, layer)) + offset = offset + xsize * ysize + + elif compression == 1: + # + # packbits compression + i = 0 + tile = [] + bytecount = read(channels * ysize * 2) + offset = file.tell() + for channel in range(channels): + layer = mode[channel] + if mode == "CMYK": + layer += ";I" + tile.append(("packbits", bbox, offset, layer)) + for y in range(ysize): + offset = offset + i16(bytecount, i) + i += 2 + + file.seek(offset) + + if offset & 1: + read(1) # padding + + return tile + + +# -------------------------------------------------------------------- +# registry + + +Image.register_open(PsdImageFile.format, PsdImageFile, _accept) + +Image.register_extension(PsdImageFile.format, ".psd") + +Image.register_mime(PsdImageFile.format, "image/vnd.adobe.photoshop") diff --git a/src/PIL/PyAccess.py b/src/PIL/PyAccess.py index fe12cb641..b0fb92625 100644 --- a/src/PIL/PyAccess.py +++ b/src/PIL/PyAccess.py @@ -1,376 +1,376 @@ -# -# The Python Imaging Library -# Pillow fork -# -# Python implementation of the PixelAccess Object -# -# Copyright (c) 1997-2009 by Secret Labs AB. All rights reserved. -# Copyright (c) 1995-2009 by Fredrik Lundh. -# Copyright (c) 2013 Eric Soroos -# -# See the README file for information on usage and redistribution -# - -# Notes: -# -# * Implements the pixel access object following Access.c -# * Taking only the tuple form, which is used from python. -# * Fill.c uses the integer form, but it's still going to use the old -# Access.c implementation. -# -from __future__ import annotations - -import logging -import sys -from typing import TYPE_CHECKING - -from ._deprecate import deprecate - -FFI: type -try: - from cffi import FFI - - defs = """ - struct Pixel_RGBA { - unsigned char r,g,b,a; - }; - struct Pixel_I16 { - unsigned char l,r; - }; - """ - ffi = FFI() - ffi.cdef(defs) -except ImportError as ex: - # Allow error import for doc purposes, but error out when accessing - # anything in core. - from ._util import DeferredError - - FFI = ffi = DeferredError.new(ex) - -logger = logging.getLogger(__name__) - -if TYPE_CHECKING: - from . import Image - - -class PyAccess: - def __init__(self, img: Image.Image, readonly: bool = False) -> None: - deprecate("PyAccess", 11) - vals = dict(img.im.unsafe_ptrs) - self.readonly = readonly - self.image8 = ffi.cast("unsigned char **", vals["image8"]) - self.image32 = ffi.cast("int **", vals["image32"]) - self.image = ffi.cast("unsigned char **", vals["image"]) - self.xsize, self.ysize = img.im.size - self._img = img - - # Keep pointer to im object to prevent dereferencing. - self._im = img.im - if self._im.mode in ("P", "PA"): - self._palette = img.palette - - # Debugging is polluting test traces, only useful here - # when hacking on PyAccess - # logger.debug("%s", vals) - self._post_init() - - def _post_init(self) -> None: - pass - - def __setitem__(self, xy, color): - """ - Modifies the pixel at x,y. The color is given as a single - numerical value for single band images, and a tuple for - multi-band images. In addition to this, RGB and RGBA tuples - are accepted for P and PA images. - - :param xy: The pixel coordinate, given as (x, y). See - :ref:`coordinate-system`. - :param color: The pixel value. - """ - if self.readonly: - msg = "Attempt to putpixel a read only image" - raise ValueError(msg) - (x, y) = xy - if x < 0: - x = self.xsize + x - if y < 0: - y = self.ysize + y - (x, y) = self.check_xy((x, y)) - - if ( - self._im.mode in ("P", "PA") - and isinstance(color, (list, tuple)) - and len(color) in [3, 4] - ): - # RGB or RGBA value for a P or PA image - if self._im.mode == "PA": - alpha = color[3] if len(color) == 4 else 255 - color = color[:3] - color = self._palette.getcolor(color, self._img) - if self._im.mode == "PA": - color = (color, alpha) - - return self.set_pixel(x, y, color) - - def __getitem__(self, xy: tuple[int, int]) -> float | tuple[int, ...]: - """ - Returns the pixel at x,y. The pixel is returned as a single - value for single band images or a tuple for multiple band - images - - :param xy: The pixel coordinate, given as (x, y). See - :ref:`coordinate-system`. - :returns: a pixel value for single band images, a tuple of - pixel values for multiband images. - """ - (x, y) = xy - if x < 0: - x = self.xsize + x - if y < 0: - y = self.ysize + y - (x, y) = self.check_xy((x, y)) - return self.get_pixel(x, y) - - putpixel = __setitem__ - getpixel = __getitem__ - - def check_xy(self, xy: tuple[int, int]) -> tuple[int, int]: - (x, y) = xy - if not (0 <= x < self.xsize and 0 <= y < self.ysize): - msg = "pixel location out of range" - raise ValueError(msg) - return xy - - def get_pixel(self, x: int, y: int) -> float | tuple[int, ...]: - raise NotImplementedError() - - def set_pixel(self, x: int, y: int, color: float | tuple[int, ...]) -> None: - raise NotImplementedError() - - -class _PyAccess32_2(PyAccess): - """PA, LA, stored in first and last bytes of a 32 bit word""" - - def _post_init(self, *args, **kwargs): - self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32) - - def get_pixel(self, x: int, y: int) -> tuple[int, int]: - pixel = self.pixels[y][x] - return pixel.r, pixel.a - - def set_pixel(self, x, y, color): - pixel = self.pixels[y][x] - # tuple - pixel.r = min(color[0], 255) - pixel.a = min(color[1], 255) - - -class _PyAccess32_3(PyAccess): - """RGB and friends, stored in the first three bytes of a 32 bit word""" - - def _post_init(self, *args, **kwargs): - self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32) - - def get_pixel(self, x: int, y: int) -> tuple[int, int, int]: - pixel = self.pixels[y][x] - return pixel.r, pixel.g, pixel.b - - def set_pixel(self, x, y, color): - pixel = self.pixels[y][x] - # tuple - pixel.r = min(color[0], 255) - pixel.g = min(color[1], 255) - pixel.b = min(color[2], 255) - pixel.a = 255 - - -class _PyAccess32_4(PyAccess): - """RGBA etc, all 4 bytes of a 32 bit word""" - - def _post_init(self, *args, **kwargs): - self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32) - - def get_pixel(self, x: int, y: int) -> tuple[int, int, int, int]: - pixel = self.pixels[y][x] - return pixel.r, pixel.g, pixel.b, pixel.a - - def set_pixel(self, x, y, color): - pixel = self.pixels[y][x] - # tuple - pixel.r = min(color[0], 255) - pixel.g = min(color[1], 255) - pixel.b = min(color[2], 255) - pixel.a = min(color[3], 255) - - -class _PyAccess8(PyAccess): - """1, L, P, 8 bit images stored as uint8""" - - def _post_init(self, *args, **kwargs): - self.pixels = self.image8 - - def get_pixel(self, x: int, y: int) -> int: - return self.pixels[y][x] - - def set_pixel(self, x, y, color): - try: - # integer - self.pixels[y][x] = min(color, 255) - except TypeError: - # tuple - self.pixels[y][x] = min(color[0], 255) - - -class _PyAccessI16_N(PyAccess): - """I;16 access, native bitendian without conversion""" - - def _post_init(self, *args, **kwargs): - self.pixels = ffi.cast("unsigned short **", self.image) - - def get_pixel(self, x: int, y: int) -> int: - return self.pixels[y][x] - - def set_pixel(self, x, y, color): - try: - # integer - self.pixels[y][x] = min(color, 65535) - except TypeError: - # tuple - self.pixels[y][x] = min(color[0], 65535) - - -class _PyAccessI16_L(PyAccess): - """I;16L access, with conversion""" - - def _post_init(self, *args, **kwargs): - self.pixels = ffi.cast("struct Pixel_I16 **", self.image) - - def get_pixel(self, x: int, y: int) -> int: - pixel = self.pixels[y][x] - return pixel.l + pixel.r * 256 - - def set_pixel(self, x, y, color): - pixel = self.pixels[y][x] - try: - color = min(color, 65535) - except TypeError: - color = min(color[0], 65535) - - pixel.l = color & 0xFF - pixel.r = color >> 8 - - -class _PyAccessI16_B(PyAccess): - """I;16B access, with conversion""" - - def _post_init(self, *args, **kwargs): - self.pixels = ffi.cast("struct Pixel_I16 **", self.image) - - def get_pixel(self, x: int, y: int) -> int: - pixel = self.pixels[y][x] - return pixel.l * 256 + pixel.r - - def set_pixel(self, x, y, color): - pixel = self.pixels[y][x] - try: - color = min(color, 65535) - except Exception: - color = min(color[0], 65535) - - pixel.l = color >> 8 - pixel.r = color & 0xFF - - -class _PyAccessI32_N(PyAccess): - """Signed Int32 access, native endian""" - - def _post_init(self, *args, **kwargs): - self.pixels = self.image32 - - def get_pixel(self, x: int, y: int) -> int: - return self.pixels[y][x] - - def set_pixel(self, x, y, color): - self.pixels[y][x] = color - - -class _PyAccessI32_Swap(PyAccess): - """I;32L/B access, with byteswapping conversion""" - - def _post_init(self, *args, **kwargs): - self.pixels = self.image32 - - def reverse(self, i): - orig = ffi.new("int *", i) - chars = ffi.cast("unsigned char *", orig) - chars[0], chars[1], chars[2], chars[3] = chars[3], chars[2], chars[1], chars[0] - return ffi.cast("int *", chars)[0] - - def get_pixel(self, x: int, y: int) -> int: - return self.reverse(self.pixels[y][x]) - - def set_pixel(self, x, y, color): - self.pixels[y][x] = self.reverse(color) - - -class _PyAccessF(PyAccess): - """32 bit float access""" - - def _post_init(self, *args, **kwargs): - self.pixels = ffi.cast("float **", self.image32) - - def get_pixel(self, x: int, y: int) -> float: - return self.pixels[y][x] - - def set_pixel(self, x, y, color): - try: - # not a tuple - self.pixels[y][x] = color - except TypeError: - # tuple - self.pixels[y][x] = color[0] - - -mode_map = { - "1": _PyAccess8, - "L": _PyAccess8, - "P": _PyAccess8, - "I;16N": _PyAccessI16_N, - "LA": _PyAccess32_2, - "La": _PyAccess32_2, - "PA": _PyAccess32_2, - "RGB": _PyAccess32_3, - "LAB": _PyAccess32_3, - "HSV": _PyAccess32_3, - "YCbCr": _PyAccess32_3, - "RGBA": _PyAccess32_4, - "RGBa": _PyAccess32_4, - "RGBX": _PyAccess32_4, - "CMYK": _PyAccess32_4, - "F": _PyAccessF, - "I": _PyAccessI32_N, -} - -if sys.byteorder == "little": - mode_map["I;16"] = _PyAccessI16_N - mode_map["I;16L"] = _PyAccessI16_N - mode_map["I;16B"] = _PyAccessI16_B - - mode_map["I;32L"] = _PyAccessI32_N - mode_map["I;32B"] = _PyAccessI32_Swap -else: - mode_map["I;16"] = _PyAccessI16_L - mode_map["I;16L"] = _PyAccessI16_L - mode_map["I;16B"] = _PyAccessI16_N - - mode_map["I;32L"] = _PyAccessI32_Swap - mode_map["I;32B"] = _PyAccessI32_N - - -def new(img: Image.Image, readonly: bool = False) -> PyAccess | None: - access_type = mode_map.get(img.mode, None) - if not access_type: - logger.debug("PyAccess Not Implemented: %s", img.mode) - return None - return access_type(img, readonly) +# +# The Python Imaging Library +# Pillow fork +# +# Python implementation of the PixelAccess Object +# +# Copyright (c) 1997-2009 by Secret Labs AB. All rights reserved. +# Copyright (c) 1995-2009 by Fredrik Lundh. +# Copyright (c) 2013 Eric Soroos +# +# See the README file for information on usage and redistribution +# + +# Notes: +# +# * Implements the pixel access object following Access.c +# * Taking only the tuple form, which is used from python. +# * Fill.c uses the integer form, but it's still going to use the old +# Access.c implementation. +# +from __future__ import annotations + +import logging +import sys +from typing import TYPE_CHECKING + +from ._deprecate import deprecate + +FFI: type +try: + from cffi import FFI + + defs = """ + struct Pixel_RGBA { + unsigned char r,g,b,a; + }; + struct Pixel_I16 { + unsigned char l,r; + }; + """ + ffi = FFI() + ffi.cdef(defs) +except ImportError as ex: + # Allow error import for doc purposes, but error out when accessing + # anything in core. + from ._util import DeferredError + + FFI = ffi = DeferredError.new(ex) + +logger = logging.getLogger(__name__) + +if TYPE_CHECKING: + from . import Image + + +class PyAccess: + def __init__(self, img: Image.Image, readonly: bool = False) -> None: + deprecate("PyAccess", 11) + vals = dict(img.im.unsafe_ptrs) + self.readonly = readonly + self.image8 = ffi.cast("unsigned char **", vals["image8"]) + self.image32 = ffi.cast("int **", vals["image32"]) + self.image = ffi.cast("unsigned char **", vals["image"]) + self.xsize, self.ysize = img.im.size + self._img = img + + # Keep pointer to im object to prevent dereferencing. + self._im = img.im + if self._im.mode in ("P", "PA"): + self._palette = img.palette + + # Debugging is polluting test traces, only useful here + # when hacking on PyAccess + # logger.debug("%s", vals) + self._post_init() + + def _post_init(self) -> None: + pass + + def __setitem__(self, xy, color): + """ + Modifies the pixel at x,y. The color is given as a single + numerical value for single band images, and a tuple for + multi-band images. In addition to this, RGB and RGBA tuples + are accepted for P and PA images. + + :param xy: The pixel coordinate, given as (x, y). See + :ref:`coordinate-system`. + :param color: The pixel value. + """ + if self.readonly: + msg = "Attempt to putpixel a read only image" + raise ValueError(msg) + (x, y) = xy + if x < 0: + x = self.xsize + x + if y < 0: + y = self.ysize + y + (x, y) = self.check_xy((x, y)) + + if ( + self._im.mode in ("P", "PA") + and isinstance(color, (list, tuple)) + and len(color) in [3, 4] + ): + # RGB or RGBA value for a P or PA image + if self._im.mode == "PA": + alpha = color[3] if len(color) == 4 else 255 + color = color[:3] + color = self._palette.getcolor(color, self._img) + if self._im.mode == "PA": + color = (color, alpha) + + return self.set_pixel(x, y, color) + + def __getitem__(self, xy: tuple[int, int]) -> float | tuple[int, ...]: + """ + Returns the pixel at x,y. The pixel is returned as a single + value for single band images or a tuple for multiple band + images + + :param xy: The pixel coordinate, given as (x, y). See + :ref:`coordinate-system`. + :returns: a pixel value for single band images, a tuple of + pixel values for multiband images. + """ + (x, y) = xy + if x < 0: + x = self.xsize + x + if y < 0: + y = self.ysize + y + (x, y) = self.check_xy((x, y)) + return self.get_pixel(x, y) + + putpixel = __setitem__ + getpixel = __getitem__ + + def check_xy(self, xy: tuple[int, int]) -> tuple[int, int]: + (x, y) = xy + if not (0 <= x < self.xsize and 0 <= y < self.ysize): + msg = "pixel location out of range" + raise ValueError(msg) + return xy + + def get_pixel(self, x: int, y: int) -> float | tuple[int, ...]: + raise NotImplementedError() + + def set_pixel(self, x: int, y: int, color: float | tuple[int, ...]) -> None: + raise NotImplementedError() + + +class _PyAccess32_2(PyAccess): + """PA, LA, stored in first and last bytes of a 32 bit word""" + + def _post_init(self, *args, **kwargs): + self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32) + + def get_pixel(self, x: int, y: int) -> tuple[int, int]: + pixel = self.pixels[y][x] + return pixel.r, pixel.a + + def set_pixel(self, x, y, color): + pixel = self.pixels[y][x] + # tuple + pixel.r = min(color[0], 255) + pixel.a = min(color[1], 255) + + +class _PyAccess32_3(PyAccess): + """RGB and friends, stored in the first three bytes of a 32 bit word""" + + def _post_init(self, *args, **kwargs): + self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32) + + def get_pixel(self, x: int, y: int) -> tuple[int, int, int]: + pixel = self.pixels[y][x] + return pixel.r, pixel.g, pixel.b + + def set_pixel(self, x, y, color): + pixel = self.pixels[y][x] + # tuple + pixel.r = min(color[0], 255) + pixel.g = min(color[1], 255) + pixel.b = min(color[2], 255) + pixel.a = 255 + + +class _PyAccess32_4(PyAccess): + """RGBA etc, all 4 bytes of a 32 bit word""" + + def _post_init(self, *args, **kwargs): + self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32) + + def get_pixel(self, x: int, y: int) -> tuple[int, int, int, int]: + pixel = self.pixels[y][x] + return pixel.r, pixel.g, pixel.b, pixel.a + + def set_pixel(self, x, y, color): + pixel = self.pixels[y][x] + # tuple + pixel.r = min(color[0], 255) + pixel.g = min(color[1], 255) + pixel.b = min(color[2], 255) + pixel.a = min(color[3], 255) + + +class _PyAccess8(PyAccess): + """1, L, P, 8 bit images stored as uint8""" + + def _post_init(self, *args, **kwargs): + self.pixels = self.image8 + + def get_pixel(self, x: int, y: int) -> int: + return self.pixels[y][x] + + def set_pixel(self, x, y, color): + try: + # integer + self.pixels[y][x] = min(color, 255) + except TypeError: + # tuple + self.pixels[y][x] = min(color[0], 255) + + +class _PyAccessI16_N(PyAccess): + """I;16 access, native bitendian without conversion""" + + def _post_init(self, *args, **kwargs): + self.pixels = ffi.cast("unsigned short **", self.image) + + def get_pixel(self, x: int, y: int) -> int: + return self.pixels[y][x] + + def set_pixel(self, x, y, color): + try: + # integer + self.pixels[y][x] = min(color, 65535) + except TypeError: + # tuple + self.pixels[y][x] = min(color[0], 65535) + + +class _PyAccessI16_L(PyAccess): + """I;16L access, with conversion""" + + def _post_init(self, *args, **kwargs): + self.pixels = ffi.cast("struct Pixel_I16 **", self.image) + + def get_pixel(self, x: int, y: int) -> int: + pixel = self.pixels[y][x] + return pixel.l + pixel.r * 256 + + def set_pixel(self, x, y, color): + pixel = self.pixels[y][x] + try: + color = min(color, 65535) + except TypeError: + color = min(color[0], 65535) + + pixel.l = color & 0xFF + pixel.r = color >> 8 + + +class _PyAccessI16_B(PyAccess): + """I;16B access, with conversion""" + + def _post_init(self, *args, **kwargs): + self.pixels = ffi.cast("struct Pixel_I16 **", self.image) + + def get_pixel(self, x: int, y: int) -> int: + pixel = self.pixels[y][x] + return pixel.l * 256 + pixel.r + + def set_pixel(self, x, y, color): + pixel = self.pixels[y][x] + try: + color = min(color, 65535) + except Exception: + color = min(color[0], 65535) + + pixel.l = color >> 8 + pixel.r = color & 0xFF + + +class _PyAccessI32_N(PyAccess): + """Signed Int32 access, native endian""" + + def _post_init(self, *args, **kwargs): + self.pixels = self.image32 + + def get_pixel(self, x: int, y: int) -> int: + return self.pixels[y][x] + + def set_pixel(self, x, y, color): + self.pixels[y][x] = color + + +class _PyAccessI32_Swap(PyAccess): + """I;32L/B access, with byteswapping conversion""" + + def _post_init(self, *args, **kwargs): + self.pixels = self.image32 + + def reverse(self, i): + orig = ffi.new("int *", i) + chars = ffi.cast("unsigned char *", orig) + chars[0], chars[1], chars[2], chars[3] = chars[3], chars[2], chars[1], chars[0] + return ffi.cast("int *", chars)[0] + + def get_pixel(self, x: int, y: int) -> int: + return self.reverse(self.pixels[y][x]) + + def set_pixel(self, x, y, color): + self.pixels[y][x] = self.reverse(color) + + +class _PyAccessF(PyAccess): + """32 bit float access""" + + def _post_init(self, *args, **kwargs): + self.pixels = ffi.cast("float **", self.image32) + + def get_pixel(self, x: int, y: int) -> float: + return self.pixels[y][x] + + def set_pixel(self, x, y, color): + try: + # not a tuple + self.pixels[y][x] = color + except TypeError: + # tuple + self.pixels[y][x] = color[0] + + +mode_map = { + "1": _PyAccess8, + "L": _PyAccess8, + "P": _PyAccess8, + "I;16N": _PyAccessI16_N, + "LA": _PyAccess32_2, + "La": _PyAccess32_2, + "PA": _PyAccess32_2, + "RGB": _PyAccess32_3, + "LAB": _PyAccess32_3, + "HSV": _PyAccess32_3, + "YCbCr": _PyAccess32_3, + "RGBA": _PyAccess32_4, + "RGBa": _PyAccess32_4, + "RGBX": _PyAccess32_4, + "CMYK": _PyAccess32_4, + "F": _PyAccessF, + "I": _PyAccessI32_N, +} + +if sys.byteorder == "little": + mode_map["I;16"] = _PyAccessI16_N + mode_map["I;16L"] = _PyAccessI16_N + mode_map["I;16B"] = _PyAccessI16_B + + mode_map["I;32L"] = _PyAccessI32_N + mode_map["I;32B"] = _PyAccessI32_Swap +else: + mode_map["I;16"] = _PyAccessI16_L + mode_map["I;16L"] = _PyAccessI16_L + mode_map["I;16B"] = _PyAccessI16_N + + mode_map["I;32L"] = _PyAccessI32_Swap + mode_map["I;32B"] = _PyAccessI32_N + + +def new(img: Image.Image, readonly: bool = False) -> PyAccess | None: + access_type = mode_map.get(img.mode, None) + if not access_type: + logger.debug("PyAccess Not Implemented: %s", img.mode) + return None + return access_type(img, readonly) diff --git a/src/PIL/QoiImagePlugin.py b/src/PIL/QoiImagePlugin.py index 202ef52d0..7c27819ed 100644 --- a/src/PIL/QoiImagePlugin.py +++ b/src/PIL/QoiImagePlugin.py @@ -1,115 +1,115 @@ -# -# The Python Imaging Library. -# -# QOI support for PIL -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import os - -from . import Image, ImageFile -from ._binary import i32be as i32 - - -def _accept(prefix: bytes) -> bool: - return prefix[:4] == b"qoif" - - -class QoiImageFile(ImageFile.ImageFile): - format = "QOI" - format_description = "Quite OK Image" - - def _open(self) -> None: - if not _accept(self.fp.read(4)): - msg = "not a QOI file" - raise SyntaxError(msg) - - self._size = tuple(i32(self.fp.read(4)) for i in range(2)) - - channels = self.fp.read(1)[0] - self._mode = "RGB" if channels == 3 else "RGBA" - - self.fp.seek(1, os.SEEK_CUR) # colorspace - self.tile = [("qoi", (0, 0) + self._size, self.fp.tell(), None)] - - -class QoiDecoder(ImageFile.PyDecoder): - _pulls_fd = True - _previous_pixel: bytes | bytearray | None = None - _previously_seen_pixels: dict[int, bytes | bytearray] = {} - - def _add_to_previous_pixels(self, value: bytes | bytearray) -> None: - self._previous_pixel = value - - r, g, b, a = value - hash_value = (r * 3 + g * 5 + b * 7 + a * 11) % 64 - self._previously_seen_pixels[hash_value] = value - - def decode(self, buffer: bytes) -> tuple[int, int]: - assert self.fd is not None - - self._previously_seen_pixels = {} - self._add_to_previous_pixels(bytearray((0, 0, 0, 255))) - - data = bytearray() - bands = Image.getmodebands(self.mode) - dest_length = self.state.xsize * self.state.ysize * bands - while len(data) < dest_length: - byte = self.fd.read(1)[0] - value: bytes | bytearray - if byte == 0b11111110 and self._previous_pixel: # QOI_OP_RGB - value = bytearray(self.fd.read(3)) + self._previous_pixel[3:] - elif byte == 0b11111111: # QOI_OP_RGBA - value = self.fd.read(4) - else: - op = byte >> 6 - if op == 0: # QOI_OP_INDEX - op_index = byte & 0b00111111 - value = self._previously_seen_pixels.get( - op_index, bytearray((0, 0, 0, 0)) - ) - elif op == 1 and self._previous_pixel: # QOI_OP_DIFF - value = bytearray( - ( - (self._previous_pixel[0] + ((byte & 0b00110000) >> 4) - 2) - % 256, - (self._previous_pixel[1] + ((byte & 0b00001100) >> 2) - 2) - % 256, - (self._previous_pixel[2] + (byte & 0b00000011) - 2) % 256, - self._previous_pixel[3], - ) - ) - elif op == 2 and self._previous_pixel: # QOI_OP_LUMA - second_byte = self.fd.read(1)[0] - diff_green = (byte & 0b00111111) - 32 - diff_red = ((second_byte & 0b11110000) >> 4) - 8 - diff_blue = (second_byte & 0b00001111) - 8 - - value = bytearray( - tuple( - (self._previous_pixel[i] + diff_green + diff) % 256 - for i, diff in enumerate((diff_red, 0, diff_blue)) - ) - ) - value += self._previous_pixel[3:] - elif op == 3 and self._previous_pixel: # QOI_OP_RUN - run_length = (byte & 0b00111111) + 1 - value = self._previous_pixel - if bands == 3: - value = value[:3] - data += value * run_length - continue - self._add_to_previous_pixels(value) - - if bands == 3: - value = value[:3] - data += value - self.set_as_raw(data) - return -1, 0 - - -Image.register_open(QoiImageFile.format, QoiImageFile, _accept) -Image.register_decoder("qoi", QoiDecoder) -Image.register_extension(QoiImageFile.format, ".qoi") +# +# The Python Imaging Library. +# +# QOI support for PIL +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import os + +from . import Image, ImageFile +from ._binary import i32be as i32 + + +def _accept(prefix: bytes) -> bool: + return prefix[:4] == b"qoif" + + +class QoiImageFile(ImageFile.ImageFile): + format = "QOI" + format_description = "Quite OK Image" + + def _open(self) -> None: + if not _accept(self.fp.read(4)): + msg = "not a QOI file" + raise SyntaxError(msg) + + self._size = tuple(i32(self.fp.read(4)) for i in range(2)) + + channels = self.fp.read(1)[0] + self._mode = "RGB" if channels == 3 else "RGBA" + + self.fp.seek(1, os.SEEK_CUR) # colorspace + self.tile = [("qoi", (0, 0) + self._size, self.fp.tell(), None)] + + +class QoiDecoder(ImageFile.PyDecoder): + _pulls_fd = True + _previous_pixel: bytes | bytearray | None = None + _previously_seen_pixels: dict[int, bytes | bytearray] = {} + + def _add_to_previous_pixels(self, value: bytes | bytearray) -> None: + self._previous_pixel = value + + r, g, b, a = value + hash_value = (r * 3 + g * 5 + b * 7 + a * 11) % 64 + self._previously_seen_pixels[hash_value] = value + + def decode(self, buffer: bytes) -> tuple[int, int]: + assert self.fd is not None + + self._previously_seen_pixels = {} + self._add_to_previous_pixels(bytearray((0, 0, 0, 255))) + + data = bytearray() + bands = Image.getmodebands(self.mode) + dest_length = self.state.xsize * self.state.ysize * bands + while len(data) < dest_length: + byte = self.fd.read(1)[0] + value: bytes | bytearray + if byte == 0b11111110 and self._previous_pixel: # QOI_OP_RGB + value = bytearray(self.fd.read(3)) + self._previous_pixel[3:] + elif byte == 0b11111111: # QOI_OP_RGBA + value = self.fd.read(4) + else: + op = byte >> 6 + if op == 0: # QOI_OP_INDEX + op_index = byte & 0b00111111 + value = self._previously_seen_pixels.get( + op_index, bytearray((0, 0, 0, 0)) + ) + elif op == 1 and self._previous_pixel: # QOI_OP_DIFF + value = bytearray( + ( + (self._previous_pixel[0] + ((byte & 0b00110000) >> 4) - 2) + % 256, + (self._previous_pixel[1] + ((byte & 0b00001100) >> 2) - 2) + % 256, + (self._previous_pixel[2] + (byte & 0b00000011) - 2) % 256, + self._previous_pixel[3], + ) + ) + elif op == 2 and self._previous_pixel: # QOI_OP_LUMA + second_byte = self.fd.read(1)[0] + diff_green = (byte & 0b00111111) - 32 + diff_red = ((second_byte & 0b11110000) >> 4) - 8 + diff_blue = (second_byte & 0b00001111) - 8 + + value = bytearray( + tuple( + (self._previous_pixel[i] + diff_green + diff) % 256 + for i, diff in enumerate((diff_red, 0, diff_blue)) + ) + ) + value += self._previous_pixel[3:] + elif op == 3 and self._previous_pixel: # QOI_OP_RUN + run_length = (byte & 0b00111111) + 1 + value = self._previous_pixel + if bands == 3: + value = value[:3] + data += value * run_length + continue + self._add_to_previous_pixels(value) + + if bands == 3: + value = value[:3] + data += value + self.set_as_raw(data) + return -1, 0 + + +Image.register_open(QoiImageFile.format, QoiImageFile, _accept) +Image.register_decoder("qoi", QoiDecoder) +Image.register_extension(QoiImageFile.format, ".qoi") diff --git a/src/PIL/SgiImagePlugin.py b/src/PIL/SgiImagePlugin.py index 50d979109..a93a40ce1 100644 --- a/src/PIL/SgiImagePlugin.py +++ b/src/PIL/SgiImagePlugin.py @@ -1,238 +1,238 @@ -# -# The Python Imaging Library. -# $Id$ -# -# SGI image file handling -# -# See "The SGI Image File Format (Draft version 0.97)", Paul Haeberli. -# -# -# -# History: -# 2017-22-07 mb Add RLE decompression -# 2016-16-10 mb Add save method without compression -# 1995-09-10 fl Created -# -# Copyright (c) 2016 by Mickael Bonfill. -# Copyright (c) 2008 by Karsten Hiddemann. -# Copyright (c) 1997 by Secret Labs AB. -# Copyright (c) 1995 by Fredrik Lundh. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import os -import struct -from typing import IO - -from . import Image, ImageFile -from ._binary import i16be as i16 -from ._binary import o8 - - -def _accept(prefix: bytes) -> bool: - return len(prefix) >= 2 and i16(prefix) == 474 - - -MODES = { - (1, 1, 1): "L", - (1, 2, 1): "L", - (2, 1, 1): "L;16B", - (2, 2, 1): "L;16B", - (1, 3, 3): "RGB", - (2, 3, 3): "RGB;16B", - (1, 3, 4): "RGBA", - (2, 3, 4): "RGBA;16B", -} - - -## -# Image plugin for SGI images. -class SgiImageFile(ImageFile.ImageFile): - format = "SGI" - format_description = "SGI Image File Format" - - def _open(self) -> None: - # HEAD - assert self.fp is not None - - headlen = 512 - s = self.fp.read(headlen) - - if not _accept(s): - msg = "Not an SGI image file" - raise ValueError(msg) - - # compression : verbatim or RLE - compression = s[2] - - # bpc : 1 or 2 bytes (8bits or 16bits) - bpc = s[3] - - # dimension : 1, 2 or 3 (depending on xsize, ysize and zsize) - dimension = i16(s, 4) - - # xsize : width - xsize = i16(s, 6) - - # ysize : height - ysize = i16(s, 8) - - # zsize : channels count - zsize = i16(s, 10) - - # layout - layout = bpc, dimension, zsize - - # determine mode from bits/zsize - rawmode = "" - try: - rawmode = MODES[layout] - except KeyError: - pass - - if rawmode == "": - msg = "Unsupported SGI image mode" - raise ValueError(msg) - - self._size = xsize, ysize - self._mode = rawmode.split(";")[0] - if self.mode == "RGB": - self.custom_mimetype = "image/rgb" - - # orientation -1 : scanlines begins at the bottom-left corner - orientation = -1 - - # decoder info - if compression == 0: - pagesize = xsize * ysize * bpc - if bpc == 2: - self.tile = [ - ("SGI16", (0, 0) + self.size, headlen, (self.mode, 0, orientation)) - ] - else: - self.tile = [] - offset = headlen - for layer in self.mode: - self.tile.append( - ("raw", (0, 0) + self.size, offset, (layer, 0, orientation)) - ) - offset += pagesize - elif compression == 1: - self.tile = [ - ("sgi_rle", (0, 0) + self.size, headlen, (rawmode, orientation, bpc)) - ] - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - if im.mode not in {"RGB", "RGBA", "L"}: - msg = "Unsupported SGI image mode" - raise ValueError(msg) - - # Get the keyword arguments - info = im.encoderinfo - - # Byte-per-pixel precision, 1 = 8bits per pixel - bpc = info.get("bpc", 1) - - if bpc not in (1, 2): - msg = "Unsupported number of bytes per pixel" - raise ValueError(msg) - - # Flip the image, since the origin of SGI file is the bottom-left corner - orientation = -1 - # Define the file as SGI File Format - magic_number = 474 - # 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) - - # Minimum Byte value - pinmin = 0 - # Maximum Byte value (255 = 8bits per pixel) - pinmax = 255 - # Image name (79 characters max, truncated below in write) - img_name = os.path.splitext(os.path.basename(filename))[0] - if isinstance(img_name, str): - img_name = img_name.encode("ascii", "ignore") - # Standard representation of pixel in the file - colormap = 0 - fp.write(struct.pack(">h", magic_number)) - fp.write(o8(rle)) - fp.write(o8(bpc)) - fp.write(struct.pack(">H", dim)) - fp.write(struct.pack(">H", x)) - fp.write(struct.pack(">H", y)) - fp.write(struct.pack(">H", z)) - fp.write(struct.pack(">l", pinmin)) - fp.write(struct.pack(">l", pinmax)) - fp.write(struct.pack("4s", b"")) # dummy - fp.write(struct.pack("79s", img_name)) # truncates to 79 chars - fp.write(struct.pack("s", b"")) # force null byte after img_name - fp.write(struct.pack(">l", colormap)) - fp.write(struct.pack("404s", b"")) # dummy - - rawmode = "L" - if bpc == 2: - rawmode = "L;16B" - - for channel in im.split(): - fp.write(channel.tobytes("raw", rawmode, 0, orientation)) - - if hasattr(fp, "flush"): - fp.flush() - - -class SGI16Decoder(ImageFile.PyDecoder): - _pulls_fd = True - - def decode(self, buffer: bytes) -> tuple[int, int]: - assert self.fd is not None - assert self.im is not None - - rawmode, stride, orientation = self.args - pagesize = self.state.xsize * self.state.ysize - zsize = len(self.mode) - self.fd.seek(512) - - for band in range(zsize): - channel = Image.new("L", (self.state.xsize, self.state.ysize)) - channel.frombytes( - self.fd.read(2 * pagesize), "raw", "L;16B", stride, orientation - ) - self.im.putband(channel.im, band) - - return -1, 0 - - -# -# registry - - -Image.register_decoder("SGI16", SGI16Decoder) -Image.register_open(SgiImageFile.format, SgiImageFile, _accept) -Image.register_save(SgiImageFile.format, _save) -Image.register_mime(SgiImageFile.format, "image/sgi") - -Image.register_extensions(SgiImageFile.format, [".bw", ".rgb", ".rgba", ".sgi"]) - -# End of file +# +# The Python Imaging Library. +# $Id$ +# +# SGI image file handling +# +# See "The SGI Image File Format (Draft version 0.97)", Paul Haeberli. +# +# +# +# History: +# 2017-22-07 mb Add RLE decompression +# 2016-16-10 mb Add save method without compression +# 1995-09-10 fl Created +# +# Copyright (c) 2016 by Mickael Bonfill. +# Copyright (c) 2008 by Karsten Hiddemann. +# Copyright (c) 1997 by Secret Labs AB. +# Copyright (c) 1995 by Fredrik Lundh. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import os +import struct +from typing import IO + +from . import Image, ImageFile +from ._binary import i16be as i16 +from ._binary import o8 + + +def _accept(prefix: bytes) -> bool: + return len(prefix) >= 2 and i16(prefix) == 474 + + +MODES = { + (1, 1, 1): "L", + (1, 2, 1): "L", + (2, 1, 1): "L;16B", + (2, 2, 1): "L;16B", + (1, 3, 3): "RGB", + (2, 3, 3): "RGB;16B", + (1, 3, 4): "RGBA", + (2, 3, 4): "RGBA;16B", +} + + +## +# Image plugin for SGI images. +class SgiImageFile(ImageFile.ImageFile): + format = "SGI" + format_description = "SGI Image File Format" + + def _open(self) -> None: + # HEAD + assert self.fp is not None + + headlen = 512 + s = self.fp.read(headlen) + + if not _accept(s): + msg = "Not an SGI image file" + raise ValueError(msg) + + # compression : verbatim or RLE + compression = s[2] + + # bpc : 1 or 2 bytes (8bits or 16bits) + bpc = s[3] + + # dimension : 1, 2 or 3 (depending on xsize, ysize and zsize) + dimension = i16(s, 4) + + # xsize : width + xsize = i16(s, 6) + + # ysize : height + ysize = i16(s, 8) + + # zsize : channels count + zsize = i16(s, 10) + + # layout + layout = bpc, dimension, zsize + + # determine mode from bits/zsize + rawmode = "" + try: + rawmode = MODES[layout] + except KeyError: + pass + + if rawmode == "": + msg = "Unsupported SGI image mode" + raise ValueError(msg) + + self._size = xsize, ysize + self._mode = rawmode.split(";")[0] + if self.mode == "RGB": + self.custom_mimetype = "image/rgb" + + # orientation -1 : scanlines begins at the bottom-left corner + orientation = -1 + + # decoder info + if compression == 0: + pagesize = xsize * ysize * bpc + if bpc == 2: + self.tile = [ + ("SGI16", (0, 0) + self.size, headlen, (self.mode, 0, orientation)) + ] + else: + self.tile = [] + offset = headlen + for layer in self.mode: + self.tile.append( + ("raw", (0, 0) + self.size, offset, (layer, 0, orientation)) + ) + offset += pagesize + elif compression == 1: + self.tile = [ + ("sgi_rle", (0, 0) + self.size, headlen, (rawmode, orientation, bpc)) + ] + + +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + if im.mode not in {"RGB", "RGBA", "L"}: + msg = "Unsupported SGI image mode" + raise ValueError(msg) + + # Get the keyword arguments + info = im.encoderinfo + + # Byte-per-pixel precision, 1 = 8bits per pixel + bpc = info.get("bpc", 1) + + if bpc not in (1, 2): + msg = "Unsupported number of bytes per pixel" + raise ValueError(msg) + + # Flip the image, since the origin of SGI file is the bottom-left corner + orientation = -1 + # Define the file as SGI File Format + magic_number = 474 + # 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) + + # Minimum Byte value + pinmin = 0 + # Maximum Byte value (255 = 8bits per pixel) + pinmax = 255 + # Image name (79 characters max, truncated below in write) + img_name = os.path.splitext(os.path.basename(filename))[0] + if isinstance(img_name, str): + img_name = img_name.encode("ascii", "ignore") + # Standard representation of pixel in the file + colormap = 0 + fp.write(struct.pack(">h", magic_number)) + fp.write(o8(rle)) + fp.write(o8(bpc)) + fp.write(struct.pack(">H", dim)) + fp.write(struct.pack(">H", x)) + fp.write(struct.pack(">H", y)) + fp.write(struct.pack(">H", z)) + fp.write(struct.pack(">l", pinmin)) + fp.write(struct.pack(">l", pinmax)) + fp.write(struct.pack("4s", b"")) # dummy + fp.write(struct.pack("79s", img_name)) # truncates to 79 chars + fp.write(struct.pack("s", b"")) # force null byte after img_name + fp.write(struct.pack(">l", colormap)) + fp.write(struct.pack("404s", b"")) # dummy + + rawmode = "L" + if bpc == 2: + rawmode = "L;16B" + + for channel in im.split(): + fp.write(channel.tobytes("raw", rawmode, 0, orientation)) + + if hasattr(fp, "flush"): + fp.flush() + + +class SGI16Decoder(ImageFile.PyDecoder): + _pulls_fd = True + + def decode(self, buffer: bytes) -> tuple[int, int]: + assert self.fd is not None + assert self.im is not None + + rawmode, stride, orientation = self.args + pagesize = self.state.xsize * self.state.ysize + zsize = len(self.mode) + self.fd.seek(512) + + for band in range(zsize): + channel = Image.new("L", (self.state.xsize, self.state.ysize)) + channel.frombytes( + self.fd.read(2 * pagesize), "raw", "L;16B", stride, orientation + ) + self.im.putband(channel.im, band) + + return -1, 0 + + +# +# registry + + +Image.register_decoder("SGI16", SGI16Decoder) +Image.register_open(SgiImageFile.format, SgiImageFile, _accept) +Image.register_save(SgiImageFile.format, _save) +Image.register_mime(SgiImageFile.format, "image/sgi") + +Image.register_extensions(SgiImageFile.format, [".bw", ".rgb", ".rgba", ".sgi"]) + +# End of file diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index a6cc00019..14aff84e0 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -1,323 +1,323 @@ -# -# The Python Imaging Library. -# -# SPIDER image file handling -# -# History: -# 2004-08-02 Created BB -# 2006-03-02 added save method -# 2006-03-13 added support for stack images -# -# Copyright (c) 2004 by Health Research Inc. (HRI) RENSSELAER, NY 12144. -# Copyright (c) 2004 by William Baxter. -# Copyright (c) 2004 by Secret Labs AB. -# Copyright (c) 2004 by Fredrik Lundh. -# - -## -# Image plugin for the Spider image format. This format is used -# by the SPIDER software, in processing image data from electron -# microscopy and tomography. -## - -# -# SpiderImagePlugin.py -# -# The Spider image format is used by SPIDER software, in processing -# image data from electron microscopy and tomography. -# -# Spider home page: -# https://spider.wadsworth.org/spider_doc/spider/docs/spider.html -# -# Details about the Spider image format: -# https://spider.wadsworth.org/spider_doc/spider/docs/image_doc.html -# -from __future__ import annotations - -import os -import struct -import sys -from typing import IO, TYPE_CHECKING - -from . import Image, ImageFile - - -def isInt(f): - try: - i = int(f) - if f - i == 0: - return 1 - else: - return 0 - except (ValueError, OverflowError): - return 0 - - -iforms = [1, 3, -11, -12, -21, -22] - - -# There is no magic number to identify Spider files, so just check a -# series of header locations to see if they have reasonable values. -# Returns no. of bytes in the header, if it is a valid Spider header, -# otherwise returns 0 - - -def isSpiderHeader(t): - h = (99,) + t # add 1 value so can use spider header index start=1 - # header values 1,2,5,12,13,22,23 should be integers - for i in [1, 2, 5, 12, 13, 22, 23]: - if not isInt(h[i]): - return 0 - # check iform - iform = int(h[5]) - if iform not in iforms: - return 0 - # check other header values - labrec = int(h[13]) # no. records in file header - labbyt = int(h[22]) # total no. of bytes in header - lenbyt = int(h[23]) # record length in bytes - if labbyt != (labrec * lenbyt): - return 0 - # looks like a valid header - return labbyt - - -def isSpiderImage(filename): - with open(filename, "rb") as fp: - f = fp.read(92) # read 23 * 4 bytes - t = struct.unpack(">23f", f) # try big-endian first - hdrlen = isSpiderHeader(t) - if hdrlen == 0: - t = struct.unpack("<23f", f) # little-endian - hdrlen = isSpiderHeader(t) - return hdrlen - - -class SpiderImageFile(ImageFile.ImageFile): - format = "SPIDER" - format_description = "Spider 2D image" - _close_exclusive_fp_after_loading = False - - def _open(self) -> None: - # check header - n = 27 * 4 # read 27 float values - f = self.fp.read(n) - - try: - self.bigendian = 1 - t = struct.unpack(">27f", f) # try big-endian first - hdrlen = isSpiderHeader(t) - if hdrlen == 0: - self.bigendian = 0 - t = struct.unpack("<27f", f) # little-endian - hdrlen = isSpiderHeader(t) - if hdrlen == 0: - msg = "not a valid Spider file" - raise SyntaxError(msg) - except struct.error as e: - msg = "not a valid Spider file" - raise SyntaxError(msg) from e - - h = (99,) + t # add 1 value : spider header index starts at 1 - iform = int(h[5]) - if iform != 1: - msg = "not a Spider 2D image" - raise SyntaxError(msg) - - self._size = int(h[12]), int(h[2]) # size in pixels (width, height) - self.istack = int(h[24]) - self.imgnumber = int(h[27]) - - if self.istack == 0 and self.imgnumber == 0: - # stk=0, img=0: a regular 2D image - offset = hdrlen - self._nimages = 1 - elif self.istack > 0 and self.imgnumber == 0: - # stk>0, img=0: Opening the stack for the first time - self.imgbytes = int(h[12]) * int(h[2]) * 4 - self.hdrlen = hdrlen - self._nimages = int(h[26]) - # Point to the first image in the stack - offset = hdrlen * 2 - self.imgnumber = 1 - elif self.istack == 0 and self.imgnumber > 0: - # stk=0, img>0: an image within the stack - offset = hdrlen + self.stkoffset - self.istack = 2 # So Image knows it's still a stack - else: - msg = "inconsistent stack header values" - raise SyntaxError(msg) - - if self.bigendian: - self.rawmode = "F;32BF" - else: - self.rawmode = "F;32F" - self._mode = "F" - - self.tile = [("raw", (0, 0) + self.size, offset, (self.rawmode, 0, 1))] - self._fp = self.fp # FIXME: hack - - @property - def n_frames(self) -> int: - return self._nimages - - @property - def is_animated(self) -> bool: - return self._nimages > 1 - - # 1st image index is zero (although SPIDER imgnumber starts at 1) - def tell(self) -> int: - if self.imgnumber < 1: - return 0 - else: - return self.imgnumber - 1 - - def seek(self, frame: int) -> None: - if self.istack == 0: - msg = "attempt to seek in a non-stack file" - raise EOFError(msg) - if not self._seek_check(frame): - return - self.stkoffset = self.hdrlen + frame * (self.hdrlen + self.imgbytes) - self.fp = self._fp - self.fp.seek(self.stkoffset) - self._open() - - # returns a byte image after rescaling to 0..255 - def convert2byte(self, depth=255): - (minimum, maximum) = self.getextrema() - m = 1 - if maximum != minimum: - m = depth / (maximum - minimum) - b = -m * minimum - return self.point(lambda i, m=m, b=b: i * m + b).convert("L") - - if TYPE_CHECKING: - from . import ImageTk - - # returns a ImageTk.PhotoImage object, after rescaling to 0..255 - def tkPhotoImage(self) -> ImageTk.PhotoImage: - from . import ImageTk - - return ImageTk.PhotoImage(self.convert2byte(), palette=256) - - -# -------------------------------------------------------------------- -# Image series - - -# given a list of filenames, return a list of images -def loadImageSeries(filelist=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 - - imglist = [] - 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() - 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 - - -# -------------------------------------------------------------------- -# For saving images in Spider format - - -def makeSpiderHeader(im: Image.Image) -> list[bytes]: - nsam, nrow = im.size - lenbyt = nsam * 4 # There are labrec records in the header - labrec = int(1024 / lenbyt) - if 1024 % lenbyt != 0: - labrec += 1 - labbyt = labrec * lenbyt - nvalues = int(labbyt / 4) - if nvalues < 23: - return [] - - hdr = [0.0] * nvalues - - # NB these are Fortran indices - hdr[1] = 1.0 # nslice (=1 for an image) - hdr[2] = float(nrow) # number of rows per slice - hdr[3] = float(nrow) # number of records in the image - hdr[5] = 1.0 # iform for 2D image - hdr[12] = float(nsam) # number of pixels per line - hdr[13] = float(labrec) # number of records in file header - hdr[22] = float(labbyt) # total number of bytes in header - hdr[23] = float(lenbyt) # record length in bytes - - # adjust for Fortran indexing - hdr = hdr[1:] - hdr.append(0.0) - # pack binary data into a string - return [struct.pack("f", v) for v in hdr] - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - if im.mode[0] != "F": - im = im.convert("F") - - hdr = makeSpiderHeader(im) - if len(hdr) < 256: - msg = "Error creating Spider header" - raise OSError(msg) - - # write the SPIDER header - fp.writelines(hdr) - - rawmode = "F;32NF" # 32-bit native floating point - ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))]) - - -def _save_spider(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - # get the filename extension and register it with Image - filename_ext = os.path.splitext(filename)[1] - ext = filename_ext.decode() if isinstance(filename_ext, bytes) else filename_ext - Image.register_extension(SpiderImageFile.format, ext) - _save(im, fp, filename) - - -# -------------------------------------------------------------------- - - -Image.register_open(SpiderImageFile.format, SpiderImageFile) -Image.register_save(SpiderImageFile.format, _save_spider) - -if __name__ == "__main__": - if len(sys.argv) < 2: - print("Syntax: python3 SpiderImagePlugin.py [infile] [outfile]") - sys.exit() - - filename = sys.argv[1] - if not isSpiderImage(filename): - print("input image must be in Spider format") - sys.exit() - - with Image.open(filename) as im: - print(f"image: {im}") - print(f"format: {im.format}") - print(f"size: {im.size}") - print(f"mode: {im.mode}") - print("max, min: ", end=" ") - print(im.getextrema()) - - if len(sys.argv) > 2: - outfile = sys.argv[2] - - # perform some image operation - im = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) - print( - f"saving a flipped version of {os.path.basename(filename)} " - f"as {outfile} " - ) - im.save(outfile, SpiderImageFile.format) +# +# The Python Imaging Library. +# +# SPIDER image file handling +# +# History: +# 2004-08-02 Created BB +# 2006-03-02 added save method +# 2006-03-13 added support for stack images +# +# Copyright (c) 2004 by Health Research Inc. (HRI) RENSSELAER, NY 12144. +# Copyright (c) 2004 by William Baxter. +# Copyright (c) 2004 by Secret Labs AB. +# Copyright (c) 2004 by Fredrik Lundh. +# + +## +# Image plugin for the Spider image format. This format is used +# by the SPIDER software, in processing image data from electron +# microscopy and tomography. +## + +# +# SpiderImagePlugin.py +# +# The Spider image format is used by SPIDER software, in processing +# image data from electron microscopy and tomography. +# +# Spider home page: +# https://spider.wadsworth.org/spider_doc/spider/docs/spider.html +# +# Details about the Spider image format: +# https://spider.wadsworth.org/spider_doc/spider/docs/image_doc.html +# +from __future__ import annotations + +import os +import struct +import sys +from typing import IO, TYPE_CHECKING + +from . import Image, ImageFile + + +def isInt(f): + try: + i = int(f) + if f - i == 0: + return 1 + else: + return 0 + except (ValueError, OverflowError): + return 0 + + +iforms = [1, 3, -11, -12, -21, -22] + + +# There is no magic number to identify Spider files, so just check a +# series of header locations to see if they have reasonable values. +# Returns no. of bytes in the header, if it is a valid Spider header, +# otherwise returns 0 + + +def isSpiderHeader(t): + h = (99,) + t # add 1 value so can use spider header index start=1 + # header values 1,2,5,12,13,22,23 should be integers + for i in [1, 2, 5, 12, 13, 22, 23]: + if not isInt(h[i]): + return 0 + # check iform + iform = int(h[5]) + if iform not in iforms: + return 0 + # check other header values + labrec = int(h[13]) # no. records in file header + labbyt = int(h[22]) # total no. of bytes in header + lenbyt = int(h[23]) # record length in bytes + if labbyt != (labrec * lenbyt): + return 0 + # looks like a valid header + return labbyt + + +def isSpiderImage(filename): + with open(filename, "rb") as fp: + f = fp.read(92) # read 23 * 4 bytes + t = struct.unpack(">23f", f) # try big-endian first + hdrlen = isSpiderHeader(t) + if hdrlen == 0: + t = struct.unpack("<23f", f) # little-endian + hdrlen = isSpiderHeader(t) + return hdrlen + + +class SpiderImageFile(ImageFile.ImageFile): + format = "SPIDER" + format_description = "Spider 2D image" + _close_exclusive_fp_after_loading = False + + def _open(self) -> None: + # check header + n = 27 * 4 # read 27 float values + f = self.fp.read(n) + + try: + self.bigendian = 1 + t = struct.unpack(">27f", f) # try big-endian first + hdrlen = isSpiderHeader(t) + if hdrlen == 0: + self.bigendian = 0 + t = struct.unpack("<27f", f) # little-endian + hdrlen = isSpiderHeader(t) + if hdrlen == 0: + msg = "not a valid Spider file" + raise SyntaxError(msg) + except struct.error as e: + msg = "not a valid Spider file" + raise SyntaxError(msg) from e + + h = (99,) + t # add 1 value : spider header index starts at 1 + iform = int(h[5]) + if iform != 1: + msg = "not a Spider 2D image" + raise SyntaxError(msg) + + self._size = int(h[12]), int(h[2]) # size in pixels (width, height) + self.istack = int(h[24]) + self.imgnumber = int(h[27]) + + if self.istack == 0 and self.imgnumber == 0: + # stk=0, img=0: a regular 2D image + offset = hdrlen + self._nimages = 1 + elif self.istack > 0 and self.imgnumber == 0: + # stk>0, img=0: Opening the stack for the first time + self.imgbytes = int(h[12]) * int(h[2]) * 4 + self.hdrlen = hdrlen + self._nimages = int(h[26]) + # Point to the first image in the stack + offset = hdrlen * 2 + self.imgnumber = 1 + elif self.istack == 0 and self.imgnumber > 0: + # stk=0, img>0: an image within the stack + offset = hdrlen + self.stkoffset + self.istack = 2 # So Image knows it's still a stack + else: + msg = "inconsistent stack header values" + raise SyntaxError(msg) + + if self.bigendian: + self.rawmode = "F;32BF" + else: + self.rawmode = "F;32F" + self._mode = "F" + + self.tile = [("raw", (0, 0) + self.size, offset, (self.rawmode, 0, 1))] + self._fp = self.fp # FIXME: hack + + @property + def n_frames(self) -> int: + return self._nimages + + @property + def is_animated(self) -> bool: + return self._nimages > 1 + + # 1st image index is zero (although SPIDER imgnumber starts at 1) + def tell(self) -> int: + if self.imgnumber < 1: + return 0 + else: + return self.imgnumber - 1 + + def seek(self, frame: int) -> None: + if self.istack == 0: + msg = "attempt to seek in a non-stack file" + raise EOFError(msg) + if not self._seek_check(frame): + return + self.stkoffset = self.hdrlen + frame * (self.hdrlen + self.imgbytes) + self.fp = self._fp + self.fp.seek(self.stkoffset) + self._open() + + # returns a byte image after rescaling to 0..255 + def convert2byte(self, depth=255): + (minimum, maximum) = self.getextrema() + m = 1 + if maximum != minimum: + m = depth / (maximum - minimum) + b = -m * minimum + return self.point(lambda i, m=m, b=b: i * m + b).convert("L") + + if TYPE_CHECKING: + from . import ImageTk + + # returns a ImageTk.PhotoImage object, after rescaling to 0..255 + def tkPhotoImage(self) -> ImageTk.PhotoImage: + from . import ImageTk + + return ImageTk.PhotoImage(self.convert2byte(), palette=256) + + +# -------------------------------------------------------------------- +# Image series + + +# given a list of filenames, return a list of images +def loadImageSeries(filelist=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 + + imglist = [] + 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() + 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 + + +# -------------------------------------------------------------------- +# For saving images in Spider format + + +def makeSpiderHeader(im: Image.Image) -> list[bytes]: + nsam, nrow = im.size + lenbyt = nsam * 4 # There are labrec records in the header + labrec = int(1024 / lenbyt) + if 1024 % lenbyt != 0: + labrec += 1 + labbyt = labrec * lenbyt + nvalues = int(labbyt / 4) + if nvalues < 23: + return [] + + hdr = [0.0] * nvalues + + # NB these are Fortran indices + hdr[1] = 1.0 # nslice (=1 for an image) + hdr[2] = float(nrow) # number of rows per slice + hdr[3] = float(nrow) # number of records in the image + hdr[5] = 1.0 # iform for 2D image + hdr[12] = float(nsam) # number of pixels per line + hdr[13] = float(labrec) # number of records in file header + hdr[22] = float(labbyt) # total number of bytes in header + hdr[23] = float(lenbyt) # record length in bytes + + # adjust for Fortran indexing + hdr = hdr[1:] + hdr.append(0.0) + # pack binary data into a string + return [struct.pack("f", v) for v in hdr] + + +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + if im.mode[0] != "F": + im = im.convert("F") + + hdr = makeSpiderHeader(im) + if len(hdr) < 256: + msg = "Error creating Spider header" + raise OSError(msg) + + # write the SPIDER header + fp.writelines(hdr) + + rawmode = "F;32NF" # 32-bit native floating point + ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))]) + + +def _save_spider(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + # get the filename extension and register it with Image + filename_ext = os.path.splitext(filename)[1] + ext = filename_ext.decode() if isinstance(filename_ext, bytes) else filename_ext + Image.register_extension(SpiderImageFile.format, ext) + _save(im, fp, filename) + + +# -------------------------------------------------------------------- + + +Image.register_open(SpiderImageFile.format, SpiderImageFile) +Image.register_save(SpiderImageFile.format, _save_spider) + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Syntax: python3 SpiderImagePlugin.py [infile] [outfile]") + sys.exit() + + filename = sys.argv[1] + if not isSpiderImage(filename): + print("input image must be in Spider format") + sys.exit() + + with Image.open(filename) as im: + print(f"image: {im}") + print(f"format: {im.format}") + print(f"size: {im.size}") + print(f"mode: {im.mode}") + print("max, min: ", end=" ") + print(im.getextrema()) + + if len(sys.argv) > 2: + outfile = sys.argv[2] + + # perform some image operation + im = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) + print( + f"saving a flipped version of {os.path.basename(filename)} " + f"as {outfile} " + ) + im.save(outfile, SpiderImageFile.format) diff --git a/src/PIL/SunImagePlugin.py b/src/PIL/SunImagePlugin.py index 4e098474a..d9dc4ab8d 100644 --- a/src/PIL/SunImagePlugin.py +++ b/src/PIL/SunImagePlugin.py @@ -1,141 +1,141 @@ -# -# The Python Imaging Library. -# $Id$ -# -# Sun image file handling -# -# History: -# 1995-09-10 fl Created -# 1996-05-28 fl Fixed 32-bit alignment -# 1998-12-29 fl Import ImagePalette module -# 2001-12-18 fl Fixed palette loading (from Jean-Claude Rimbault) -# -# Copyright (c) 1997-2001 by Secret Labs AB -# Copyright (c) 1995-1996 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -from . import Image, ImageFile, ImagePalette -from ._binary import i32be as i32 - - -def _accept(prefix: bytes) -> bool: - return len(prefix) >= 4 and i32(prefix) == 0x59A66A95 - - -## -# Image plugin for Sun raster files. - - -class SunImageFile(ImageFile.ImageFile): - format = "SUN" - format_description = "Sun Raster File" - - def _open(self) -> None: - # The Sun Raster file header is 32 bytes in length - # and has the following format: - - # typedef struct _SunRaster - # { - # DWORD MagicNumber; /* Magic (identification) number */ - # DWORD Width; /* Width of image in pixels */ - # DWORD Height; /* Height of image in pixels */ - # DWORD Depth; /* Number of bits per pixel */ - # DWORD Length; /* Size of image data in bytes */ - # DWORD Type; /* Type of raster file */ - # DWORD ColorMapType; /* Type of color map */ - # DWORD ColorMapLength; /* Size of the color map in bytes */ - # } SUNRASTER; - - assert self.fp is not None - - # HEAD - s = self.fp.read(32) - if not _accept(s): - msg = "not an SUN raster file" - raise SyntaxError(msg) - - offset = 32 - - self._size = i32(s, 4), i32(s, 8) - - depth = i32(s, 12) - # data_length = i32(s, 16) # unreliable, ignore. - file_type = i32(s, 20) - palette_type = i32(s, 24) # 0: None, 1: RGB, 2: Raw/arbitrary - palette_length = i32(s, 28) - - if depth == 1: - self._mode, rawmode = "1", "1;I" - elif depth == 4: - self._mode, rawmode = "L", "L;4" - elif depth == 8: - self._mode = rawmode = "L" - elif depth == 24: - if file_type == 3: - self._mode, rawmode = "RGB", "RGB" - else: - self._mode, rawmode = "RGB", "BGR" - elif depth == 32: - if file_type == 3: - self._mode, rawmode = "RGB", "RGBX" - else: - self._mode, rawmode = "RGB", "BGRX" - else: - msg = "Unsupported Mode/Bit Depth" - raise SyntaxError(msg) - - if palette_length: - if palette_length > 1024: - msg = "Unsupported Color Palette Length" - raise SyntaxError(msg) - - if palette_type != 1: - msg = "Unsupported Palette Type" - raise SyntaxError(msg) - - offset = offset + palette_length - self.palette = ImagePalette.raw("RGB;L", self.fp.read(palette_length)) - if self.mode == "L": - self._mode = "P" - rawmode = rawmode.replace("L", "P") - - # 16 bit boundaries on stride - stride = ((self.size[0] * depth + 15) // 16) * 2 - - # file type: Type is the version (or flavor) of the bitmap - # file. The following values are typically found in the Type - # field: - # 0000h Old - # 0001h Standard - # 0002h Byte-encoded - # 0003h RGB format - # 0004h TIFF format - # 0005h IFF format - # FFFFh Experimental - - # Old and standard are the same, except for the length tag. - # byte-encoded is run-length-encoded - # RGB looks similar to standard, but RGB byte order - # TIFF and IFF mean that they were converted from T/IFF - # Experimental means that it's something else. - # (https://www.fileformat.info/format/sunraster/egff.htm) - - if file_type in (0, 1, 3, 4, 5): - self.tile = [("raw", (0, 0) + self.size, offset, (rawmode, stride))] - elif file_type == 2: - self.tile = [("sun_rle", (0, 0) + self.size, offset, rawmode)] - else: - msg = "Unsupported Sun Raster file type" - raise SyntaxError(msg) - - -# -# registry - - -Image.register_open(SunImageFile.format, SunImageFile, _accept) - -Image.register_extension(SunImageFile.format, ".ras") +# +# The Python Imaging Library. +# $Id$ +# +# Sun image file handling +# +# History: +# 1995-09-10 fl Created +# 1996-05-28 fl Fixed 32-bit alignment +# 1998-12-29 fl Import ImagePalette module +# 2001-12-18 fl Fixed palette loading (from Jean-Claude Rimbault) +# +# Copyright (c) 1997-2001 by Secret Labs AB +# Copyright (c) 1995-1996 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +from . import Image, ImageFile, ImagePalette +from ._binary import i32be as i32 + + +def _accept(prefix: bytes) -> bool: + return len(prefix) >= 4 and i32(prefix) == 0x59A66A95 + + +## +# Image plugin for Sun raster files. + + +class SunImageFile(ImageFile.ImageFile): + format = "SUN" + format_description = "Sun Raster File" + + def _open(self) -> None: + # The Sun Raster file header is 32 bytes in length + # and has the following format: + + # typedef struct _SunRaster + # { + # DWORD MagicNumber; /* Magic (identification) number */ + # DWORD Width; /* Width of image in pixels */ + # DWORD Height; /* Height of image in pixels */ + # DWORD Depth; /* Number of bits per pixel */ + # DWORD Length; /* Size of image data in bytes */ + # DWORD Type; /* Type of raster file */ + # DWORD ColorMapType; /* Type of color map */ + # DWORD ColorMapLength; /* Size of the color map in bytes */ + # } SUNRASTER; + + assert self.fp is not None + + # HEAD + s = self.fp.read(32) + if not _accept(s): + msg = "not an SUN raster file" + raise SyntaxError(msg) + + offset = 32 + + self._size = i32(s, 4), i32(s, 8) + + depth = i32(s, 12) + # data_length = i32(s, 16) # unreliable, ignore. + file_type = i32(s, 20) + palette_type = i32(s, 24) # 0: None, 1: RGB, 2: Raw/arbitrary + palette_length = i32(s, 28) + + if depth == 1: + self._mode, rawmode = "1", "1;I" + elif depth == 4: + self._mode, rawmode = "L", "L;4" + elif depth == 8: + self._mode = rawmode = "L" + elif depth == 24: + if file_type == 3: + self._mode, rawmode = "RGB", "RGB" + else: + self._mode, rawmode = "RGB", "BGR" + elif depth == 32: + if file_type == 3: + self._mode, rawmode = "RGB", "RGBX" + else: + self._mode, rawmode = "RGB", "BGRX" + else: + msg = "Unsupported Mode/Bit Depth" + raise SyntaxError(msg) + + if palette_length: + if palette_length > 1024: + msg = "Unsupported Color Palette Length" + raise SyntaxError(msg) + + if palette_type != 1: + msg = "Unsupported Palette Type" + raise SyntaxError(msg) + + offset = offset + palette_length + self.palette = ImagePalette.raw("RGB;L", self.fp.read(palette_length)) + if self.mode == "L": + self._mode = "P" + rawmode = rawmode.replace("L", "P") + + # 16 bit boundaries on stride + stride = ((self.size[0] * depth + 15) // 16) * 2 + + # file type: Type is the version (or flavor) of the bitmap + # file. The following values are typically found in the Type + # field: + # 0000h Old + # 0001h Standard + # 0002h Byte-encoded + # 0003h RGB format + # 0004h TIFF format + # 0005h IFF format + # FFFFh Experimental + + # Old and standard are the same, except for the length tag. + # byte-encoded is run-length-encoded + # RGB looks similar to standard, but RGB byte order + # TIFF and IFF mean that they were converted from T/IFF + # Experimental means that it's something else. + # (https://www.fileformat.info/format/sunraster/egff.htm) + + if file_type in (0, 1, 3, 4, 5): + self.tile = [("raw", (0, 0) + self.size, offset, (rawmode, stride))] + elif file_type == 2: + self.tile = [("sun_rle", (0, 0) + self.size, offset, rawmode)] + else: + msg = "Unsupported Sun Raster file type" + raise SyntaxError(msg) + + +# +# registry + + +Image.register_open(SunImageFile.format, SunImageFile, _accept) + +Image.register_extension(SunImageFile.format, ".ras") diff --git a/src/PIL/TarIO.py b/src/PIL/TarIO.py index cba26d4b0..4267e3b82 100644 --- a/src/PIL/TarIO.py +++ b/src/PIL/TarIO.py @@ -1,67 +1,67 @@ -# -# The Python Imaging Library. -# $Id$ -# -# read files from within a tar file -# -# History: -# 95-06-18 fl Created -# 96-05-28 fl Open files in binary mode -# -# Copyright (c) Secret Labs AB 1997. -# Copyright (c) Fredrik Lundh 1995-96. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import io - -from . import ContainerIO - - -class TarIO(ContainerIO.ContainerIO[bytes]): - """A file object that provides read access to a given member of a TAR file.""" - - def __init__(self, tarfile: str, file: str) -> None: - """ - Create file object. - - :param tarfile: Name of TAR file. - :param file: Name of member file. - """ - self.fh = open(tarfile, "rb") - - while True: - s = self.fh.read(512) - if len(s) != 512: - msg = "unexpected end of tar file" - raise OSError(msg) - - name = s[:100].decode("utf-8") - i = name.find("\0") - if i == 0: - msg = "cannot find subfile" - raise OSError(msg) - if i > 0: - name = name[:i] - - size = int(s[124:135], 8) - - if file == name: - break - - self.fh.seek((size + 511) & (~511), io.SEEK_CUR) - - # Open region - super().__init__(self.fh, self.fh.tell(), size) - - # Context manager support - def __enter__(self) -> TarIO: - return self - - def __exit__(self, *args: object) -> None: - self.close() - - def close(self) -> None: - self.fh.close() +# +# The Python Imaging Library. +# $Id$ +# +# read files from within a tar file +# +# History: +# 95-06-18 fl Created +# 96-05-28 fl Open files in binary mode +# +# Copyright (c) Secret Labs AB 1997. +# Copyright (c) Fredrik Lundh 1995-96. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import io + +from . import ContainerIO + + +class TarIO(ContainerIO.ContainerIO[bytes]): + """A file object that provides read access to a given member of a TAR file.""" + + def __init__(self, tarfile: str, file: str) -> None: + """ + Create file object. + + :param tarfile: Name of TAR file. + :param file: Name of member file. + """ + self.fh = open(tarfile, "rb") + + while True: + s = self.fh.read(512) + if len(s) != 512: + msg = "unexpected end of tar file" + raise OSError(msg) + + name = s[:100].decode("utf-8") + i = name.find("\0") + if i == 0: + msg = "cannot find subfile" + raise OSError(msg) + if i > 0: + name = name[:i] + + size = int(s[124:135], 8) + + if file == name: + break + + self.fh.seek((size + 511) & (~511), io.SEEK_CUR) + + # Open region + super().__init__(self.fh, self.fh.tell(), size) + + # Context manager support + def __enter__(self) -> TarIO: + return self + + def __exit__(self, *args: object) -> None: + self.close() + + def close(self) -> None: + self.fh.close() diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index f16f075df..48505ed75 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -1,263 +1,263 @@ -# -# The Python Imaging Library. -# $Id$ -# -# TGA file handling -# -# History: -# 95-09-01 fl created (reads 24-bit files only) -# 97-01-04 fl support more TGA versions, including compressed images -# 98-07-04 fl fixed orientation and alpha layer bugs -# 98-09-11 fl fixed orientation for runlength decoder -# -# Copyright (c) Secret Labs AB 1997-98. -# Copyright (c) Fredrik Lundh 1995-97. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import warnings -from typing import IO - -from . import Image, ImageFile, ImagePalette -from ._binary import i16le as i16 -from ._binary import o8 -from ._binary import o16le as o16 - -# -# -------------------------------------------------------------------- -# Read RGA file - - -MODES = { - # map imagetype/depth to rawmode - (1, 8): "P", - (3, 1): "1", - (3, 8): "L", - (3, 16): "LA", - (2, 16): "BGR;5", - (2, 24): "BGR", - (2, 32): "BGRA", -} - - -## -# Image plugin for Targa files. - - -class TgaImageFile(ImageFile.ImageFile): - format = "TGA" - format_description = "Targa" - - def _open(self) -> None: - # process header - assert self.fp is not None - - s = self.fp.read(18) - - id_len = s[0] - - colormaptype = s[1] - imagetype = s[2] - - depth = s[16] - - flags = s[17] - - self._size = i16(s, 12), i16(s, 14) - - # validate header fields - if ( - colormaptype not in (0, 1) - or self.size[0] <= 0 - or self.size[1] <= 0 - or depth not in (1, 8, 16, 24, 32) - ): - msg = "not a TGA file" - raise SyntaxError(msg) - - # image mode - if imagetype in (3, 11): - self._mode = "L" - if depth == 1: - self._mode = "1" # ??? - elif depth == 16: - self._mode = "LA" - elif imagetype in (1, 9): - self._mode = "P" if colormaptype else "L" - elif imagetype in (2, 10): - self._mode = "RGB" - if depth == 32: - self._mode = "RGBA" - else: - msg = "unknown TGA mode" - raise SyntaxError(msg) - - # orientation - orientation = flags & 0x30 - self._flip_horizontally = orientation in [0x10, 0x30] - if orientation in [0x20, 0x30]: - orientation = 1 - elif orientation in [0, 0x10]: - orientation = -1 - else: - msg = "unknown TGA orientation" - raise SyntaxError(msg) - - self.info["orientation"] = orientation - - if imagetype & 8: - self.info["compression"] = "tga_rle" - - if id_len: - self.info["id_section"] = self.fp.read(id_len) - - if colormaptype: - # read palette - start, size, mapdepth = i16(s, 3), i16(s, 5), s[7] - if mapdepth == 16: - self.palette = ImagePalette.raw( - "BGR;15", b"\0" * 2 * start + self.fp.read(2 * size) - ) - elif mapdepth == 24: - self.palette = ImagePalette.raw( - "BGR", b"\0" * 3 * start + self.fp.read(3 * size) - ) - elif mapdepth == 32: - self.palette = ImagePalette.raw( - "BGRA", b"\0" * 4 * start + self.fp.read(4 * size) - ) - else: - msg = "unknown TGA map depth" - raise SyntaxError(msg) - - # setup tile descriptor - try: - rawmode = MODES[(imagetype & 7, depth)] - if imagetype & 8: - # compressed - self.tile = [ - ( - "tga_rle", - (0, 0) + self.size, - self.fp.tell(), - (rawmode, orientation, depth), - ) - ] - else: - self.tile = [ - ( - "raw", - (0, 0) + self.size, - self.fp.tell(), - (rawmode, 0, orientation), - ) - ] - except KeyError: - pass # cannot decode - - def load_end(self) -> None: - if self._flip_horizontally: - assert self.im is not None - self.im = self.im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) - - -# -# -------------------------------------------------------------------- -# Write TGA file - - -SAVE = { - "1": ("1", 1, 0, 3), - "L": ("L", 8, 0, 3), - "LA": ("LA", 16, 0, 3), - "P": ("P", 8, 1, 1), - "RGB": ("BGR", 24, 0, 2), - "RGBA": ("BGRA", 32, 0, 2), -} - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - try: - rawmode, bits, colormaptype, imagetype = SAVE[im.mode] - except KeyError as e: - msg = f"cannot write mode {im.mode} as TGA" - raise OSError(msg) from e - - if "rle" in im.encoderinfo: - rle = im.encoderinfo["rle"] - else: - compression = im.encoderinfo.get("compression", im.info.get("compression")) - rle = compression == "tga_rle" - if rle: - imagetype += 8 - - id_section = im.encoderinfo.get("id_section", im.info.get("id_section", "")) - id_len = len(id_section) - if id_len > 255: - id_len = 255 - id_section = id_section[:255] - warnings.warn("id_section has been trimmed to 255 characters") - - if colormaptype: - assert im.im is not None - palette = im.im.getpalette("RGB", "BGR") - colormaplength, colormapentry = len(palette) // 3, 24 - else: - colormaplength, colormapentry = 0, 0 - - if im.mode in ("LA", "RGBA"): - flags = 8 - else: - flags = 0 - - orientation = im.encoderinfo.get("orientation", im.info.get("orientation", -1)) - if orientation > 0: - flags = flags | 0x20 - - fp.write( - o8(id_len) - + o8(colormaptype) - + o8(imagetype) - + o16(0) # colormapfirst - + o16(colormaplength) - + o8(colormapentry) - + o16(0) - + o16(0) - + o16(im.size[0]) - + o16(im.size[1]) - + o8(bits) - + o8(flags) - ) - - if id_section: - fp.write(id_section) - - if colormaptype: - fp.write(palette) - - if rle: - ImageFile._save( - im, fp, [("tga_rle", (0, 0) + im.size, 0, (rawmode, orientation))] - ) - else: - ImageFile._save( - im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, orientation))] - ) - - # write targa version 2 footer - fp.write(b"\000" * 8 + b"TRUEVISION-XFILE." + b"\000") - - -# -# -------------------------------------------------------------------- -# Registry - - -Image.register_open(TgaImageFile.format, TgaImageFile) -Image.register_save(TgaImageFile.format, _save) - -Image.register_extensions(TgaImageFile.format, [".tga", ".icb", ".vda", ".vst"]) - -Image.register_mime(TgaImageFile.format, "image/x-tga") +# +# The Python Imaging Library. +# $Id$ +# +# TGA file handling +# +# History: +# 95-09-01 fl created (reads 24-bit files only) +# 97-01-04 fl support more TGA versions, including compressed images +# 98-07-04 fl fixed orientation and alpha layer bugs +# 98-09-11 fl fixed orientation for runlength decoder +# +# Copyright (c) Secret Labs AB 1997-98. +# Copyright (c) Fredrik Lundh 1995-97. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import warnings +from typing import IO + +from . import Image, ImageFile, ImagePalette +from ._binary import i16le as i16 +from ._binary import o8 +from ._binary import o16le as o16 + +# +# -------------------------------------------------------------------- +# Read RGA file + + +MODES = { + # map imagetype/depth to rawmode + (1, 8): "P", + (3, 1): "1", + (3, 8): "L", + (3, 16): "LA", + (2, 16): "BGR;5", + (2, 24): "BGR", + (2, 32): "BGRA", +} + + +## +# Image plugin for Targa files. + + +class TgaImageFile(ImageFile.ImageFile): + format = "TGA" + format_description = "Targa" + + def _open(self) -> None: + # process header + assert self.fp is not None + + s = self.fp.read(18) + + id_len = s[0] + + colormaptype = s[1] + imagetype = s[2] + + depth = s[16] + + flags = s[17] + + self._size = i16(s, 12), i16(s, 14) + + # validate header fields + if ( + colormaptype not in (0, 1) + or self.size[0] <= 0 + or self.size[1] <= 0 + or depth not in (1, 8, 16, 24, 32) + ): + msg = "not a TGA file" + raise SyntaxError(msg) + + # image mode + if imagetype in (3, 11): + self._mode = "L" + if depth == 1: + self._mode = "1" # ??? + elif depth == 16: + self._mode = "LA" + elif imagetype in (1, 9): + self._mode = "P" if colormaptype else "L" + elif imagetype in (2, 10): + self._mode = "RGB" + if depth == 32: + self._mode = "RGBA" + else: + msg = "unknown TGA mode" + raise SyntaxError(msg) + + # orientation + orientation = flags & 0x30 + self._flip_horizontally = orientation in [0x10, 0x30] + if orientation in [0x20, 0x30]: + orientation = 1 + elif orientation in [0, 0x10]: + orientation = -1 + else: + msg = "unknown TGA orientation" + raise SyntaxError(msg) + + self.info["orientation"] = orientation + + if imagetype & 8: + self.info["compression"] = "tga_rle" + + if id_len: + self.info["id_section"] = self.fp.read(id_len) + + if colormaptype: + # read palette + start, size, mapdepth = i16(s, 3), i16(s, 5), s[7] + if mapdepth == 16: + self.palette = ImagePalette.raw( + "BGR;15", b"\0" * 2 * start + self.fp.read(2 * size) + ) + elif mapdepth == 24: + self.palette = ImagePalette.raw( + "BGR", b"\0" * 3 * start + self.fp.read(3 * size) + ) + elif mapdepth == 32: + self.palette = ImagePalette.raw( + "BGRA", b"\0" * 4 * start + self.fp.read(4 * size) + ) + else: + msg = "unknown TGA map depth" + raise SyntaxError(msg) + + # setup tile descriptor + try: + rawmode = MODES[(imagetype & 7, depth)] + if imagetype & 8: + # compressed + self.tile = [ + ( + "tga_rle", + (0, 0) + self.size, + self.fp.tell(), + (rawmode, orientation, depth), + ) + ] + else: + self.tile = [ + ( + "raw", + (0, 0) + self.size, + self.fp.tell(), + (rawmode, 0, orientation), + ) + ] + except KeyError: + pass # cannot decode + + def load_end(self) -> None: + if self._flip_horizontally: + assert self.im is not None + self.im = self.im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) + + +# +# -------------------------------------------------------------------- +# Write TGA file + + +SAVE = { + "1": ("1", 1, 0, 3), + "L": ("L", 8, 0, 3), + "LA": ("LA", 16, 0, 3), + "P": ("P", 8, 1, 1), + "RGB": ("BGR", 24, 0, 2), + "RGBA": ("BGRA", 32, 0, 2), +} + + +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + try: + rawmode, bits, colormaptype, imagetype = SAVE[im.mode] + except KeyError as e: + msg = f"cannot write mode {im.mode} as TGA" + raise OSError(msg) from e + + if "rle" in im.encoderinfo: + rle = im.encoderinfo["rle"] + else: + compression = im.encoderinfo.get("compression", im.info.get("compression")) + rle = compression == "tga_rle" + if rle: + imagetype += 8 + + id_section = im.encoderinfo.get("id_section", im.info.get("id_section", "")) + id_len = len(id_section) + if id_len > 255: + id_len = 255 + id_section = id_section[:255] + warnings.warn("id_section has been trimmed to 255 characters") + + if colormaptype: + assert im.im is not None + palette = im.im.getpalette("RGB", "BGR") + colormaplength, colormapentry = len(palette) // 3, 24 + else: + colormaplength, colormapentry = 0, 0 + + if im.mode in ("LA", "RGBA"): + flags = 8 + else: + flags = 0 + + orientation = im.encoderinfo.get("orientation", im.info.get("orientation", -1)) + if orientation > 0: + flags = flags | 0x20 + + fp.write( + o8(id_len) + + o8(colormaptype) + + o8(imagetype) + + o16(0) # colormapfirst + + o16(colormaplength) + + o8(colormapentry) + + o16(0) + + o16(0) + + o16(im.size[0]) + + o16(im.size[1]) + + o8(bits) + + o8(flags) + ) + + if id_section: + fp.write(id_section) + + if colormaptype: + fp.write(palette) + + if rle: + ImageFile._save( + im, fp, [("tga_rle", (0, 0) + im.size, 0, (rawmode, orientation))] + ) + else: + ImageFile._save( + im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, orientation))] + ) + + # write targa version 2 footer + fp.write(b"\000" * 8 + b"TRUEVISION-XFILE." + b"\000") + + +# +# -------------------------------------------------------------------- +# Registry + + +Image.register_open(TgaImageFile.format, TgaImageFile) +Image.register_save(TgaImageFile.format, _save) + +Image.register_extensions(TgaImageFile.format, [".tga", ".icb", ".vda", ".vst"]) + +Image.register_mime(TgaImageFile.format, "image/x-tga") diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 702d8f33b..a923c152f 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1,2188 +1,2188 @@ -# -# The Python Imaging Library. -# $Id$ -# -# TIFF file handling -# -# TIFF is a flexible, if somewhat aged, image file format originally -# defined by Aldus. Although TIFF supports a wide variety of pixel -# layouts and compression methods, the name doesn't really stand for -# "thousands of incompatible file formats," it just feels that way. -# -# To read TIFF data from a stream, the stream must be seekable. For -# progressive decoding, make sure to use TIFF files where the tag -# directory is placed first in the file. -# -# History: -# 1995-09-01 fl Created -# 1996-05-04 fl Handle JPEGTABLES tag -# 1996-05-18 fl Fixed COLORMAP support -# 1997-01-05 fl Fixed PREDICTOR support -# 1997-08-27 fl Added support for rational tags (from Perry Stoll) -# 1998-01-10 fl Fixed seek/tell (from Jan Blom) -# 1998-07-15 fl Use private names for internal variables -# 1999-06-13 fl Rewritten for PIL 1.0 (1.0) -# 2000-10-11 fl Additional fixes for Python 2.0 (1.1) -# 2001-04-17 fl Fixed rewind support (seek to frame 0) (1.2) -# 2001-05-12 fl Added write support for more tags (from Greg Couch) (1.3) -# 2001-12-18 fl Added workaround for broken Matrox library -# 2002-01-18 fl Don't mess up if photometric tag is missing (D. Alan Stewart) -# 2003-05-19 fl Check FILLORDER tag -# 2003-09-26 fl Added RGBa support -# 2004-02-24 fl Added DPI support; fixed rational write support -# 2005-02-07 fl Added workaround for broken Corel Draw 10 files -# 2006-01-09 fl Added support for float/double tags (from Russell Nelson) -# -# Copyright (c) 1997-2006 by Secret Labs AB. All rights reserved. -# Copyright (c) 1995-1997 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import io -import itertools -import logging -import math -import os -import struct -import warnings -from collections.abc import MutableMapping -from fractions import Fraction -from numbers import Number, Rational -from typing import IO, TYPE_CHECKING, Any, Callable - -from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags -from ._binary import i16be as i16 -from ._binary import i32be as i32 -from ._binary import o8 -from ._deprecate import deprecate -from .TiffTags import TYPES - -logger = logging.getLogger(__name__) - -# Set these to true to force use of libtiff for reading or writing. -READ_LIBTIFF = False -WRITE_LIBTIFF = False -IFD_LEGACY_API = True -STRIP_SIZE = 65536 - -II = b"II" # little-endian (Intel style) -MM = b"MM" # big-endian (Motorola style) - -# -# -------------------------------------------------------------------- -# Read TIFF files - -# a few tag names, just to make the code below a bit more readable -OSUBFILETYPE = 255 -IMAGEWIDTH = 256 -IMAGELENGTH = 257 -BITSPERSAMPLE = 258 -COMPRESSION = 259 -PHOTOMETRIC_INTERPRETATION = 262 -FILLORDER = 266 -IMAGEDESCRIPTION = 270 -STRIPOFFSETS = 273 -SAMPLESPERPIXEL = 277 -ROWSPERSTRIP = 278 -STRIPBYTECOUNTS = 279 -X_RESOLUTION = 282 -Y_RESOLUTION = 283 -PLANAR_CONFIGURATION = 284 -RESOLUTION_UNIT = 296 -TRANSFERFUNCTION = 301 -SOFTWARE = 305 -DATE_TIME = 306 -ARTIST = 315 -PREDICTOR = 317 -COLORMAP = 320 -TILEWIDTH = 322 -TILELENGTH = 323 -TILEOFFSETS = 324 -TILEBYTECOUNTS = 325 -SUBIFD = 330 -EXTRASAMPLES = 338 -SAMPLEFORMAT = 339 -JPEGTABLES = 347 -YCBCRSUBSAMPLING = 530 -REFERENCEBLACKWHITE = 532 -COPYRIGHT = 33432 -IPTC_NAA_CHUNK = 33723 # newsphoto properties -PHOTOSHOP_CHUNK = 34377 # photoshop properties -ICCPROFILE = 34675 -EXIFIFD = 34665 -XMP = 700 -JPEGQUALITY = 65537 # pseudo-tag by libtiff - -# https://github.com/imagej/ImageJA/blob/master/src/main/java/ij/io/TiffDecoder.java -IMAGEJ_META_DATA_BYTE_COUNTS = 50838 -IMAGEJ_META_DATA = 50839 - -COMPRESSION_INFO = { - # Compression => pil compression name - 1: "raw", - 2: "tiff_ccitt", - 3: "group3", - 4: "group4", - 5: "tiff_lzw", - 6: "tiff_jpeg", # obsolete - 7: "jpeg", - 8: "tiff_adobe_deflate", - 32771: "tiff_raw_16", # 16-bit padding - 32773: "packbits", - 32809: "tiff_thunderscan", - 32946: "tiff_deflate", - 34676: "tiff_sgilog", - 34677: "tiff_sgilog24", - 34925: "lzma", - 50000: "zstd", - 50001: "webp", -} - -COMPRESSION_INFO_REV = {v: k for k, v in COMPRESSION_INFO.items()} - -OPEN_INFO = { - # (ByteOrder, PhotoInterpretation, SampleFormat, FillOrder, BitsPerSample, - # ExtraSamples) => mode, rawmode - (II, 0, (1,), 1, (1,), ()): ("1", "1;I"), - (MM, 0, (1,), 1, (1,), ()): ("1", "1;I"), - (II, 0, (1,), 2, (1,), ()): ("1", "1;IR"), - (MM, 0, (1,), 2, (1,), ()): ("1", "1;IR"), - (II, 1, (1,), 1, (1,), ()): ("1", "1"), - (MM, 1, (1,), 1, (1,), ()): ("1", "1"), - (II, 1, (1,), 2, (1,), ()): ("1", "1;R"), - (MM, 1, (1,), 2, (1,), ()): ("1", "1;R"), - (II, 0, (1,), 1, (2,), ()): ("L", "L;2I"), - (MM, 0, (1,), 1, (2,), ()): ("L", "L;2I"), - (II, 0, (1,), 2, (2,), ()): ("L", "L;2IR"), - (MM, 0, (1,), 2, (2,), ()): ("L", "L;2IR"), - (II, 1, (1,), 1, (2,), ()): ("L", "L;2"), - (MM, 1, (1,), 1, (2,), ()): ("L", "L;2"), - (II, 1, (1,), 2, (2,), ()): ("L", "L;2R"), - (MM, 1, (1,), 2, (2,), ()): ("L", "L;2R"), - (II, 0, (1,), 1, (4,), ()): ("L", "L;4I"), - (MM, 0, (1,), 1, (4,), ()): ("L", "L;4I"), - (II, 0, (1,), 2, (4,), ()): ("L", "L;4IR"), - (MM, 0, (1,), 2, (4,), ()): ("L", "L;4IR"), - (II, 1, (1,), 1, (4,), ()): ("L", "L;4"), - (MM, 1, (1,), 1, (4,), ()): ("L", "L;4"), - (II, 1, (1,), 2, (4,), ()): ("L", "L;4R"), - (MM, 1, (1,), 2, (4,), ()): ("L", "L;4R"), - (II, 0, (1,), 1, (8,), ()): ("L", "L;I"), - (MM, 0, (1,), 1, (8,), ()): ("L", "L;I"), - (II, 0, (1,), 2, (8,), ()): ("L", "L;IR"), - (MM, 0, (1,), 2, (8,), ()): ("L", "L;IR"), - (II, 1, (1,), 1, (8,), ()): ("L", "L"), - (MM, 1, (1,), 1, (8,), ()): ("L", "L"), - (II, 1, (2,), 1, (8,), ()): ("L", "L"), - (MM, 1, (2,), 1, (8,), ()): ("L", "L"), - (II, 1, (1,), 2, (8,), ()): ("L", "L;R"), - (MM, 1, (1,), 2, (8,), ()): ("L", "L;R"), - (II, 1, (1,), 1, (12,), ()): ("I;16", "I;12"), - (II, 0, (1,), 1, (16,), ()): ("I;16", "I;16"), - (II, 1, (1,), 1, (16,), ()): ("I;16", "I;16"), - (MM, 1, (1,), 1, (16,), ()): ("I;16B", "I;16B"), - (II, 1, (1,), 2, (16,), ()): ("I;16", "I;16R"), - (II, 1, (2,), 1, (16,), ()): ("I", "I;16S"), - (MM, 1, (2,), 1, (16,), ()): ("I", "I;16BS"), - (II, 0, (3,), 1, (32,), ()): ("F", "F;32F"), - (MM, 0, (3,), 1, (32,), ()): ("F", "F;32BF"), - (II, 1, (1,), 1, (32,), ()): ("I", "I;32N"), - (II, 1, (2,), 1, (32,), ()): ("I", "I;32S"), - (MM, 1, (2,), 1, (32,), ()): ("I", "I;32BS"), - (II, 1, (3,), 1, (32,), ()): ("F", "F;32F"), - (MM, 1, (3,), 1, (32,), ()): ("F", "F;32BF"), - (II, 1, (1,), 1, (8, 8), (2,)): ("LA", "LA"), - (MM, 1, (1,), 1, (8, 8), (2,)): ("LA", "LA"), - (II, 2, (1,), 1, (8, 8, 8), ()): ("RGB", "RGB"), - (MM, 2, (1,), 1, (8, 8, 8), ()): ("RGB", "RGB"), - (II, 2, (1,), 2, (8, 8, 8), ()): ("RGB", "RGB;R"), - (MM, 2, (1,), 2, (8, 8, 8), ()): ("RGB", "RGB;R"), - (II, 2, (1,), 1, (8, 8, 8, 8), ()): ("RGBA", "RGBA"), # missing ExtraSamples - (MM, 2, (1,), 1, (8, 8, 8, 8), ()): ("RGBA", "RGBA"), # missing ExtraSamples - (II, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGBX", "RGBX"), - (MM, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGBX", "RGBX"), - (II, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("RGBX", "RGBXX"), - (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("RGBX", "RGBXX"), - (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): ("RGBX", "RGBXXX"), - (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): ("RGBX", "RGBXXX"), - (II, 2, (1,), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"), - (MM, 2, (1,), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"), - (II, 2, (1,), 1, (8, 8, 8, 8, 8), (1, 0)): ("RGBA", "RGBaX"), - (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (1, 0)): ("RGBA", "RGBaX"), - (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (1, 0, 0)): ("RGBA", "RGBaXX"), - (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (1, 0, 0)): ("RGBA", "RGBaXX"), - (II, 2, (1,), 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"), - (MM, 2, (1,), 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"), - (II, 2, (1,), 1, (8, 8, 8, 8, 8), (2, 0)): ("RGBA", "RGBAX"), - (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (2, 0)): ("RGBA", "RGBAX"), - (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (2, 0, 0)): ("RGBA", "RGBAXX"), - (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (2, 0, 0)): ("RGBA", "RGBAXX"), - (II, 2, (1,), 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10 - (MM, 2, (1,), 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10 - (II, 2, (1,), 1, (16, 16, 16), ()): ("RGB", "RGB;16L"), - (MM, 2, (1,), 1, (16, 16, 16), ()): ("RGB", "RGB;16B"), - (II, 2, (1,), 1, (16, 16, 16, 16), ()): ("RGBA", "RGBA;16L"), - (MM, 2, (1,), 1, (16, 16, 16, 16), ()): ("RGBA", "RGBA;16B"), - (II, 2, (1,), 1, (16, 16, 16, 16), (0,)): ("RGBX", "RGBX;16L"), - (MM, 2, (1,), 1, (16, 16, 16, 16), (0,)): ("RGBX", "RGBX;16B"), - (II, 2, (1,), 1, (16, 16, 16, 16), (1,)): ("RGBA", "RGBa;16L"), - (MM, 2, (1,), 1, (16, 16, 16, 16), (1,)): ("RGBA", "RGBa;16B"), - (II, 2, (1,), 1, (16, 16, 16, 16), (2,)): ("RGBA", "RGBA;16L"), - (MM, 2, (1,), 1, (16, 16, 16, 16), (2,)): ("RGBA", "RGBA;16B"), - (II, 3, (1,), 1, (1,), ()): ("P", "P;1"), - (MM, 3, (1,), 1, (1,), ()): ("P", "P;1"), - (II, 3, (1,), 2, (1,), ()): ("P", "P;1R"), - (MM, 3, (1,), 2, (1,), ()): ("P", "P;1R"), - (II, 3, (1,), 1, (2,), ()): ("P", "P;2"), - (MM, 3, (1,), 1, (2,), ()): ("P", "P;2"), - (II, 3, (1,), 2, (2,), ()): ("P", "P;2R"), - (MM, 3, (1,), 2, (2,), ()): ("P", "P;2R"), - (II, 3, (1,), 1, (4,), ()): ("P", "P;4"), - (MM, 3, (1,), 1, (4,), ()): ("P", "P;4"), - (II, 3, (1,), 2, (4,), ()): ("P", "P;4R"), - (MM, 3, (1,), 2, (4,), ()): ("P", "P;4R"), - (II, 3, (1,), 1, (8,), ()): ("P", "P"), - (MM, 3, (1,), 1, (8,), ()): ("P", "P"), - (II, 3, (1,), 1, (8, 8), (0,)): ("P", "PX"), - (II, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"), - (MM, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"), - (II, 3, (1,), 2, (8,), ()): ("P", "P;R"), - (MM, 3, (1,), 2, (8,), ()): ("P", "P;R"), - (II, 5, (1,), 1, (8, 8, 8, 8), ()): ("CMYK", "CMYK"), - (MM, 5, (1,), 1, (8, 8, 8, 8), ()): ("CMYK", "CMYK"), - (II, 5, (1,), 1, (8, 8, 8, 8, 8), (0,)): ("CMYK", "CMYKX"), - (MM, 5, (1,), 1, (8, 8, 8, 8, 8), (0,)): ("CMYK", "CMYKX"), - (II, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"), - (MM, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"), - (II, 5, (1,), 1, (16, 16, 16, 16), ()): ("CMYK", "CMYK;16L"), - (II, 6, (1,), 1, (8,), ()): ("L", "L"), - (MM, 6, (1,), 1, (8,), ()): ("L", "L"), - # JPEG compressed images handled by LibTiff and auto-converted to RGBX - # Minimal Baseline TIFF requires YCbCr images to have 3 SamplesPerPixel - (II, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGBX"), - (MM, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGBX"), - (II, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"), - (MM, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"), -} - -MAX_SAMPLESPERPIXEL = max(len(key_tp[4]) for key_tp in OPEN_INFO) - -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 -] - -if not getattr(Image.core, "libtiff_support_custom_tags", True): - deprecate("Support for LibTIFF earlier than version 4", 12) - - -def _accept(prefix: bytes) -> bool: - return prefix[:4] in PREFIXES - - -def _limit_rational(val, max_val): - inv = abs(val) > 1 - n_d = IFDRational(1 / val if inv else val).limit_rational(max_val) - return n_d[::-1] if inv else n_d - - -def _limit_signed_rational(val, max_val, min_val): - frac = Fraction(val) - n_d = frac.numerator, frac.denominator - - if min(n_d) < min_val: - n_d = _limit_rational(val, abs(min_val)) - - if max(n_d) > max_val: - val = Fraction(*n_d) - n_d = _limit_rational(val, max_val) - - return n_d - - -## -# Wrapper for TIFF IFDs. - -_load_dispatch = {} -_write_dispatch = {} - - -def _delegate(op): - def delegate(self, *args): - return getattr(self._val, op)(*args) - - return delegate - - -class IFDRational(Rational): - """Implements a rational class where 0/0 is a legal value to match - the in the wild use of exif rationals. - - e.g., DigitalZoomRatio - 0.00/0.00 indicates that no digital zoom was used - """ - - """ If the denominator is 0, store this as a float('nan'), otherwise store - as a fractions.Fraction(). Delegate as appropriate - - """ - - __slots__ = ("_numerator", "_denominator", "_val") - - def __init__(self, value, denominator=1): - """ - :param value: either an integer numerator, a - float/rational/other number, or an IFDRational - :param denominator: Optional integer denominator - """ - if isinstance(value, IFDRational): - self._numerator = value.numerator - self._denominator = value.denominator - self._val = value._val - return - - if isinstance(value, Fraction): - self._numerator = value.numerator - self._denominator = value.denominator - else: - self._numerator = value - self._denominator = denominator - - if denominator == 0: - self._val = float("nan") - elif denominator == 1: - self._val = Fraction(value) - else: - self._val = Fraction(value, denominator) - - @property - def numerator(self): - return self._numerator - - @property - def denominator(self): - return self._denominator - - def limit_rational(self, max_denominator): - """ - - :param max_denominator: Integer, the maximum denominator value - :returns: Tuple of (numerator, denominator) - """ - - if self.denominator == 0: - return self.numerator, self.denominator - - f = self._val.limit_denominator(max_denominator) - return f.numerator, f.denominator - - def __repr__(self) -> str: - return str(float(self._val)) - - def __hash__(self): - return self._val.__hash__() - - def __eq__(self, other: object) -> bool: - val = self._val - if isinstance(other, IFDRational): - other = other._val - if isinstance(other, float): - val = float(val) - return val == other - - def __getstate__(self): - return [self._val, self._numerator, self._denominator] - - def __setstate__(self, state): - IFDRational.__init__(self, 0) - _val, _numerator, _denominator = state - self._val = _val - self._numerator = _numerator - self._denominator = _denominator - - """ a = ['add','radd', 'sub', 'rsub', 'mul', 'rmul', - 'truediv', 'rtruediv', 'floordiv', 'rfloordiv', - 'mod','rmod', 'pow','rpow', 'pos', 'neg', - 'abs', 'trunc', 'lt', 'gt', 'le', 'ge', 'bool', - 'ceil', 'floor', 'round'] - print("\n".join("__%s__ = _delegate('__%s__')" % (s,s) for s in a)) - """ - - __add__ = _delegate("__add__") - __radd__ = _delegate("__radd__") - __sub__ = _delegate("__sub__") - __rsub__ = _delegate("__rsub__") - __mul__ = _delegate("__mul__") - __rmul__ = _delegate("__rmul__") - __truediv__ = _delegate("__truediv__") - __rtruediv__ = _delegate("__rtruediv__") - __floordiv__ = _delegate("__floordiv__") - __rfloordiv__ = _delegate("__rfloordiv__") - __mod__ = _delegate("__mod__") - __rmod__ = _delegate("__rmod__") - __pow__ = _delegate("__pow__") - __rpow__ = _delegate("__rpow__") - __pos__ = _delegate("__pos__") - __neg__ = _delegate("__neg__") - __abs__ = _delegate("__abs__") - __trunc__ = _delegate("__trunc__") - __lt__ = _delegate("__lt__") - __gt__ = _delegate("__gt__") - __le__ = _delegate("__le__") - __ge__ = _delegate("__ge__") - __bool__ = _delegate("__bool__") - __ceil__ = _delegate("__ceil__") - __floor__ = _delegate("__floor__") - __round__ = _delegate("__round__") - # Python >= 3.11 - if hasattr(Fraction, "__int__"): - __int__ = _delegate("__int__") - - -def _register_loader(idx, size): - def decorator(func): - from .TiffTags import TYPES - - if func.__name__.startswith("load_"): - TYPES[idx] = func.__name__[5:].replace("_", " ") - _load_dispatch[idx] = size, func # noqa: F821 - return func - - return decorator - - -def _register_writer(idx): - def decorator(func): - _write_dispatch[idx] = func # noqa: F821 - return func - - return decorator - - -def _register_basic(idx_fmt_name): - from .TiffTags import TYPES - - idx, fmt, name = idx_fmt_name - TYPES[idx] = name - size = struct.calcsize(f"={fmt}") - _load_dispatch[idx] = ( # noqa: F821 - size, - lambda self, data, legacy_api=True: ( - self._unpack(f"{len(data) // size}{fmt}", data) - ), - ) - _write_dispatch[idx] = lambda self, *values: ( # noqa: F821 - b"".join(self._pack(fmt, value) for value in values) - ) - - -if TYPE_CHECKING: - _IFDv2Base = MutableMapping[int, Any] -else: - _IFDv2Base = MutableMapping - - -class ImageFileDirectory_v2(_IFDv2Base): - """This class represents a TIFF tag directory. To speed things up, we - don't decode tags unless they're asked for. - - Exposes a dictionary interface of the tags in the directory:: - - ifd = ImageFileDirectory_v2() - ifd[key] = 'Some Data' - ifd.tagtype[key] = TiffTags.ASCII - print(ifd[key]) - 'Some Data' - - Individual values are returned as the strings or numbers, sequences are - returned as tuples of the values. - - The tiff metadata type of each item is stored in a dictionary of - tag types in - :attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v2.tagtype`. The types - are read from a tiff file, guessed from the type added, or added - manually. - - Data Structures: - - * ``self.tagtype = {}`` - - * Key: numerical TIFF tag number - * Value: integer corresponding to the data type from - :py:data:`.TiffTags.TYPES` - - .. versionadded:: 3.0.0 - - 'Internal' data structures: - - * ``self._tags_v2 = {}`` - - * Key: numerical TIFF tag number - * Value: decoded data, as tuple for multiple values - - * ``self._tagdata = {}`` - - * Key: numerical TIFF tag number - * Value: undecoded byte string from file - - * ``self._tags_v1 = {}`` - - * Key: numerical TIFF tag number - * Value: decoded data in the v1 format - - Tags will be found in the private attributes ``self._tagdata``, and in - ``self._tags_v2`` once decoded. - - ``self.legacy_api`` is a value for internal use, and shouldn't be changed - from outside code. In cooperation with - :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1`, if ``legacy_api`` - is true, then decoded tags will be populated into both ``_tags_v1`` and - ``_tags_v2``. ``_tags_v2`` will be used if this IFD is used in the TIFF - save routine. Tags should be read from ``_tags_v1`` if - ``legacy_api == true``. - - """ - - _load_dispatch: dict[int, Callable[[ImageFileDirectory_v2, bytes, bool], Any]] = {} - _write_dispatch: dict[int, Callable[..., Any]] = {} - - def __init__(self, ifh=b"II\052\0\0\0\0\0", prefix=None, group=None): - """Initialize an ImageFileDirectory. - - To construct an ImageFileDirectory from a real file, pass the 8-byte - magic header to the constructor. To only set the endianness, pass it - as the 'prefix' keyword argument. - - :param ifh: One of the accepted magic headers (cf. PREFIXES); also sets - endianness. - :param prefix: Override the endianness of the file. - """ - if not _accept(ifh): - msg = f"not a TIFF file (header {repr(ifh)} not valid)" - raise SyntaxError(msg) - self._prefix = prefix if prefix is not None else ifh[:2] - if self._prefix == MM: - self._endian = ">" - elif self._prefix == II: - self._endian = "<" - else: - msg = "not a TIFF IFD" - raise SyntaxError(msg) - self._bigtiff = ifh[2] == 43 - self.group = group - self.tagtype = {} - """ Dictionary of tag types """ - self.reset() - (self.next,) = ( - self._unpack("Q", ifh[8:]) if self._bigtiff else self._unpack("L", ifh[4:]) - ) - self._legacy_api = False - - prefix = property(lambda self: self._prefix) - offset = property(lambda self: self._offset) - - @property - def legacy_api(self): - return self._legacy_api - - @legacy_api.setter - def legacy_api(self, value): - msg = "Not allowing setting of legacy api" - raise Exception(msg) - - def reset(self): - self._tags_v1 = {} # will remain empty if legacy_api is false - self._tags_v2 = {} # main tag storage - self._tagdata = {} - self.tagtype = {} # added 2008-06-05 by Florian Hoech - self._next = None - self._offset = None - - def __str__(self) -> str: - return str(dict(self)) - - def named(self): - """ - :returns: dict of name|key: value - - Returns the complete tag dictionary, with named tags where possible. - """ - return { - TiffTags.lookup(code, self.group).name: value - for code, value in self.items() - } - - def __len__(self) -> int: - return len(set(self._tagdata) | set(self._tags_v2)) - - def __getitem__(self, tag): - if tag not in self._tags_v2: # unpack on the fly - data = self._tagdata[tag] - typ = self.tagtype[tag] - size, handler = self._load_dispatch[typ] - self[tag] = handler(self, data, self.legacy_api) # check type - val = self._tags_v2[tag] - if self.legacy_api and not isinstance(val, (tuple, bytes)): - val = (val,) - return val - - def __contains__(self, tag): - return tag in self._tags_v2 or tag in self._tagdata - - def __setitem__(self, tag, value): - self._setitem(tag, value, self.legacy_api) - - def _setitem(self, tag, value, legacy_api): - basetypes = (Number, bytes, str) - - info = TiffTags.lookup(tag, self.group) - values = [value] if isinstance(value, basetypes) else value - - if tag not in self.tagtype: - if info.type: - self.tagtype[tag] = info.type - else: - self.tagtype[tag] = 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 - else: - self.tagtype[tag] = ( - TiffTags.LONG - if all(v >= 0 for v in values) - else TiffTags.SIGNED_LONG - ) - elif all(isinstance(v, float) for v in values): - self.tagtype[tag] = TiffTags.DOUBLE - elif all(isinstance(v, str) for v in values): - self.tagtype[tag] = TiffTags.ASCII - elif all(isinstance(v, bytes) for v in values): - self.tagtype[tag] = TiffTags.BYTE - - if self.tagtype[tag] == TiffTags.UNDEFINED: - values = [ - v.encode("ascii", "replace") if isinstance(v, str) else v - for v in values - ] - elif self.tagtype[tag] == TiffTags.RATIONAL: - values = [float(v) if isinstance(v, int) else v for v in values] - - is_ifd = self.tagtype[tag] == TiffTags.LONG and isinstance(values, dict) - if not is_ifd: - values = tuple(info.cvt_enum(value) for value in values) - - dest = self._tags_v1 if legacy_api else self._tags_v2 - - # Three branches: - # Spec'd length == 1, Actual length 1, store as element - # Spec'd length == 1, Actual > 1, Warn and truncate. Formerly barfed. - # No Spec, Actual length 1, Formerly (<4.2) returned a 1 element tuple. - # Don't mess with the legacy api, since it's frozen. - if not is_ifd and ( - (info.length == 1) - or self.tagtype[tag] == TiffTags.BYTE - or (info.length is None and len(values) == 1 and not legacy_api) - ): - # Don't mess with the legacy api, since it's frozen. - if legacy_api and self.tagtype[tag] in [ - TiffTags.RATIONAL, - TiffTags.SIGNED_RATIONAL, - ]: # rationals - values = (values,) - try: - (dest[tag],) = values - except ValueError: - # We've got a builtin tag with 1 expected entry - warnings.warn( - f"Metadata Warning, tag {tag} had too many entries: " - f"{len(values)}, expected 1" - ) - dest[tag] = values[0] - - else: - # Spec'd length > 1 or undefined - # Unspec'd, and length > 1 - dest[tag] = values - - def __delitem__(self, tag: int) -> None: - self._tags_v2.pop(tag, None) - self._tags_v1.pop(tag, None) - self._tagdata.pop(tag, None) - - def __iter__(self): - return iter(set(self._tagdata) | set(self._tags_v2)) - - def _unpack(self, fmt, data): - return struct.unpack(self._endian + fmt, data) - - def _pack(self, fmt, *values): - return struct.pack(self._endian + fmt, *values) - - list( - map( - _register_basic, - [ - (TiffTags.SHORT, "H", "short"), - (TiffTags.LONG, "L", "long"), - (TiffTags.SIGNED_BYTE, "b", "signed byte"), - (TiffTags.SIGNED_SHORT, "h", "signed short"), - (TiffTags.SIGNED_LONG, "l", "signed long"), - (TiffTags.FLOAT, "f", "float"), - (TiffTags.DOUBLE, "d", "double"), - (TiffTags.IFD, "L", "long"), - (TiffTags.LONG8, "Q", "long8"), - ], - ) - ) - - @_register_loader(1, 1) # Basic type, except for the legacy API. - def load_byte(self, data, legacy_api=True): - return data - - @_register_writer(1) # Basic type, except for the legacy API. - def write_byte(self, data): - if isinstance(data, IFDRational): - data = int(data) - if isinstance(data, int): - data = bytes((data,)) - return data - - @_register_loader(2, 1) - def load_string(self, data, legacy_api=True): - if data.endswith(b"\0"): - data = data[:-1] - return data.decode("latin-1", "replace") - - @_register_writer(2) - def write_string(self, value): - # remerge of https://github.com/python-pillow/Pillow/pull/1416 - if isinstance(value, int): - value = str(value) - if not isinstance(value, bytes): - value = value.encode("ascii", "replace") - return value + b"\0" - - @_register_loader(5, 8) - def load_rational(self, data, legacy_api=True): - vals = self._unpack(f"{len(data) // 4}L", data) - - def combine(a, b): - return (a, b) if legacy_api else IFDRational(a, b) - - return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2])) - - @_register_writer(5) - def write_rational(self, *values): - return b"".join( - self._pack("2L", *_limit_rational(frac, 2**32 - 1)) for frac in values - ) - - @_register_loader(7, 1) - def load_undefined(self, data, legacy_api=True): - return data - - @_register_writer(7) - def write_undefined(self, value): - if isinstance(value, IFDRational): - value = int(value) - if isinstance(value, int): - value = str(value).encode("ascii", "replace") - return value - - @_register_loader(10, 8) - def load_signed_rational(self, data, legacy_api=True): - vals = self._unpack(f"{len(data) // 4}l", data) - - def combine(a, b): - return (a, b) if legacy_api else IFDRational(a, b) - - return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2])) - - @_register_writer(10) - def write_signed_rational(self, *values): - return b"".join( - self._pack("2l", *_limit_signed_rational(frac, 2**31 - 1, -(2**31))) - for frac in values - ) - - def _ensure_read(self, fp, size): - ret = fp.read(size) - if len(ret) != size: - msg = ( - "Corrupt EXIF data. " - f"Expecting to read {size} bytes but only got {len(ret)}. " - ) - raise OSError(msg) - return ret - - def load(self, fp): - self.reset() - self._offset = fp.tell() - - try: - tag_count = ( - self._unpack("Q", self._ensure_read(fp, 8)) - if self._bigtiff - else self._unpack("H", self._ensure_read(fp, 2)) - )[0] - for i in range(tag_count): - tag, typ, count, data = ( - self._unpack("HHQ8s", self._ensure_read(fp, 20)) - if self._bigtiff - else self._unpack("HHL4s", self._ensure_read(fp, 12)) - ) - - tagname = TiffTags.lookup(tag, self.group).name - typname = TYPES.get(typ, "unknown") - msg = f"tag: {tagname} ({tag}) - type: {typname} ({typ})" - - try: - unit_size, handler = self._load_dispatch[typ] - except KeyError: - logger.debug("%s - unsupported type %s", msg, typ) - continue # ignore unsupported type - size = count * unit_size - if size > (8 if self._bigtiff else 4): - here = fp.tell() - (offset,) = self._unpack("Q" if self._bigtiff else "L", data) - msg += f" Tag Location: {here} - Data Location: {offset}" - fp.seek(offset) - data = ImageFile._safe_read(fp, size) - fp.seek(here) - else: - data = data[:size] - - if len(data) != size: - warnings.warn( - "Possibly corrupt EXIF data. " - f"Expecting to read {size} bytes but only got {len(data)}." - f" Skipping tag {tag}" - ) - logger.debug(msg) - continue - - if not data: - logger.debug(msg) - continue - - self._tagdata[tag] = data - self.tagtype[tag] = typ - - msg += " - value: " + ( - "" % size if size > 32 else repr(data) - ) - logger.debug(msg) - - (self.next,) = ( - self._unpack("Q", self._ensure_read(fp, 8)) - if self._bigtiff - else self._unpack("L", self._ensure_read(fp, 4)) - ) - except OSError as msg: - warnings.warn(str(msg)) - return - - def tobytes(self, offset=0): - # FIXME What about tagdata? - result = self._pack("H", len(self._tags_v2)) - - entries = [] - offset = offset + len(result) + len(self._tags_v2) * 12 + 4 - stripoffsets = None - - # pass 1: convert tags to binary format - # always write tags in ascending order - for tag, value in sorted(self._tags_v2.items()): - if tag == STRIPOFFSETS: - stripoffsets = len(entries) - typ = self.tagtype.get(tag) - logger.debug("Tag %s, Type: %s, Value: %s", tag, typ, repr(value)) - is_ifd = typ == TiffTags.LONG and isinstance(value, dict) - if is_ifd: - if self._endian == "<": - ifh = b"II\x2A\x00\x08\x00\x00\x00" - else: - ifh = b"MM\x00\x2A\x00\x00\x00\x08" - ifd = ImageFileDirectory_v2(ifh, group=tag) - values = self._tags_v2[tag] - for ifd_tag, ifd_value in values.items(): - ifd[ifd_tag] = ifd_value - data = ifd.tobytes(offset) - else: - values = value if isinstance(value, tuple) else (value,) - data = self._write_dispatch[typ](self, *values) - - tagname = TiffTags.lookup(tag, self.group).name - typname = "ifd" if is_ifd else TYPES.get(typ, "unknown") - msg = f"save: {tagname} ({tag}) - type: {typname} ({typ})" - msg += " - value: " + ( - "" % len(data) if len(data) >= 16 else str(values) - ) - logger.debug(msg) - - # count is sum of lengths for string and arbitrary data - if is_ifd: - count = 1 - elif typ in [TiffTags.BYTE, TiffTags.ASCII, TiffTags.UNDEFINED]: - count = len(data) - else: - count = len(values) - # figure out if data fits into the entry - if len(data) <= 4: - entries.append((tag, typ, count, data.ljust(4, b"\0"), b"")) - else: - entries.append((tag, typ, count, self._pack("L", offset), data)) - offset += (len(data) + 1) // 2 * 2 # pad to word - - # update strip offset data to point beyond auxiliary data - if stripoffsets is not None: - tag, typ, count, value, data = entries[stripoffsets] - if data: - msg = "multistrip support not yet implemented" - raise NotImplementedError(msg) - value = self._pack("L", self._unpack("L", value)[0] + offset) - 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) - - # -- overwrite here for multi-page -- - result += b"\0\0\0\0" # end of entries - - # pass 3: write auxiliary data to file - for tag, typ, count, value, data in entries: - result += data - if len(data) & 1: - result += b"\0" - - return result - - def save(self, fp): - if fp.tell() == 0: # skip TIFF header on subsequent pages - # tiff header -- PIL always starts the first IFD at offset 8 - fp.write(self._prefix + self._pack("HL", 42, 8)) - - offset = fp.tell() - result = self.tobytes(offset) - fp.write(result) - return offset + len(result) - - -ImageFileDirectory_v2._load_dispatch = _load_dispatch -ImageFileDirectory_v2._write_dispatch = _write_dispatch -for idx, name in TYPES.items(): - name = name.replace(" ", "_") - setattr(ImageFileDirectory_v2, f"load_{name}", _load_dispatch[idx][1]) - setattr(ImageFileDirectory_v2, f"write_{name}", _write_dispatch[idx]) -del _load_dispatch, _write_dispatch, idx, name - - -# Legacy ImageFileDirectory support. -class ImageFileDirectory_v1(ImageFileDirectory_v2): - """This class represents the **legacy** interface to a TIFF tag directory. - - Exposes a dictionary interface of the tags in the directory:: - - ifd = ImageFileDirectory_v1() - ifd[key] = 'Some Data' - ifd.tagtype[key] = TiffTags.ASCII - print(ifd[key]) - ('Some Data',) - - Also contains a dictionary of tag types as read from the tiff image file, - :attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v1.tagtype`. - - Values are returned as a tuple. - - .. deprecated:: 3.0.0 - """ - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self._legacy_api = True - - tags = property(lambda self: self._tags_v1) - tagdata = property(lambda self: self._tagdata) - - # defined in ImageFileDirectory_v2 - tagtype: dict[int, int] - """Dictionary of tag types""" - - @classmethod - def from_v2(cls, original): - """Returns an - :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` - instance with the same data as is contained in the original - :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` - instance. - - :returns: :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` - - """ - - ifd = cls(prefix=original.prefix) - ifd._tagdata = original._tagdata - ifd.tagtype = original.tagtype - ifd.next = original.next # an indicator for multipage tiffs - return ifd - - def to_v2(self) -> ImageFileDirectory_v2: - """Returns an - :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` - instance with the same data as is contained in the original - :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` - instance. - - :returns: :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` - - """ - - ifd = ImageFileDirectory_v2(prefix=self.prefix) - ifd._tagdata = dict(self._tagdata) - ifd.tagtype = dict(self.tagtype) - ifd._tags_v2 = dict(self._tags_v2) - return ifd - - def __contains__(self, tag): - return tag in self._tags_v1 or tag in self._tagdata - - def __len__(self) -> int: - return len(set(self._tagdata) | set(self._tags_v1)) - - def __iter__(self): - return iter(set(self._tagdata) | set(self._tags_v1)) - - def __setitem__(self, tag, value): - for legacy_api in (False, True): - self._setitem(tag, value, legacy_api) - - def __getitem__(self, tag): - if tag not in self._tags_v1: # unpack on the fly - data = self._tagdata[tag] - typ = self.tagtype[tag] - size, handler = self._load_dispatch[typ] - for legacy in (False, True): - self._setitem(tag, handler(self, data, legacy), legacy) - val = self._tags_v1[tag] - if not isinstance(val, (tuple, bytes)): - val = (val,) - return val - - -# undone -- switch this pointer when IFD_LEGACY_API == False -ImageFileDirectory = ImageFileDirectory_v1 - - -## -# Image plugin for TIFF files. - - -class TiffImageFile(ImageFile.ImageFile): - format = "TIFF" - format_description = "Adobe TIFF" - _close_exclusive_fp_after_loading = False - - def __init__(self, fp=None, filename=None): - self.tag_v2 = None - """ Image file directory (tag dictionary) """ - - self.tag = None - """ Legacy tag entries """ - - super().__init__(fp, filename) - - def _open(self) -> None: - """Open the first image in a TIFF file""" - - # Header - ifh = self.fp.read(8) - if ifh[2] == 43: - ifh += self.fp.read(8) - - self.tag_v2 = ImageFileDirectory_v2(ifh) - - # legacy IFD entries will be filled in later - self.ifd = None - - # setup frame pointers - self.__first = self.__next = self.tag_v2.next - self.__frame = -1 - self._fp = self.fp - self._frame_pos: list[int] = [] - self._n_frames: int | None = None - - logger.debug("*** TiffImageFile._open ***") - logger.debug("- __first: %s", self.__first) - logger.debug("- ifh: %s", repr(ifh)) # Use repr to avoid str(bytes) - - # and load the first frame - self._seek(0) - - @property - def n_frames(self): - if self._n_frames is None: - current = self.tell() - self._seek(len(self._frame_pos)) - while self._n_frames is None: - self._seek(self.tell() + 1) - self.seek(current) - return self._n_frames - - def seek(self, frame: int) -> None: - """Select a given frame as current image""" - if not self._seek_check(frame): - return - self._seek(frame) - # Create a new core image object on second and - # subsequent frames in the image. Image may be - # different size/mode. - Image._decompression_bomb_check(self.size) - self.im = Image.core.new(self.mode, self.size) - - def _seek(self, frame: int) -> None: - 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" - raise EOFError(msg) - logger.debug( - "Seeking to frame %s, on frame %s, __next %s, location: %s", - frame, - self.__frame, - self.__next, - self.fp.tell(), - ) - if self.__next >= 2**63: - msg = "Unable to seek to frame" - raise ValueError(msg) - self.fp.seek(self.__next) - self._frame_pos.append(self.__next) - logger.debug("Loading tags, location: %s", self.fp.tell()) - self.tag_v2.load(self.fp) - if self.tag_v2.next in self._frame_pos: - # This IFD has already been processed - # Declare this to be the end of the image - self.__next = 0 - else: - self.__next = self.tag_v2.next - if self.__next == 0: - self._n_frames = frame + 1 - if len(self._frame_pos) == 1: - self.is_animated = self.__next != 0 - self.__frame += 1 - self.fp.seek(self._frame_pos[frame]) - self.tag_v2.load(self.fp) - self._reload_exif() - # fill the legacy tag/ifd entries - self.tag = self.ifd = ImageFileDirectory_v1.from_v2(self.tag_v2) - self.__frame = frame - self._setup() - - def tell(self) -> int: - """Return the current frame number""" - return self.__frame - - def getxmp(self) -> dict[str, Any]: - """ - Returns a dictionary containing the XMP tags. - Requires defusedxml to be installed. - - :returns: XMP tags in a dictionary. - """ - return self._getxmp(self.tag_v2[XMP]) if XMP in self.tag_v2 else {} - - def get_photoshop_blocks(self): - """ - Returns a dictionary of Photoshop "Image Resource Blocks". - The keys are the image resource ID. For more information, see - https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_pgfId-1037727 - - :returns: Photoshop "Image Resource Blocks" in a dictionary. - """ - blocks = {} - val = self.tag_v2.get(ExifTags.Base.ImageResources) - if val: - while val[:4] == b"8BIM": - id = i16(val[4:6]) - n = math.ceil((val[6] + 1) / 2) * 2 - size = i32(val[6 + n : 10 + n]) - data = val[10 + n : 10 + n + size] - blocks[id] = {"data": data} - - val = val[math.ceil((10 + n + size) / 2) * 2 :] - return blocks - - def load(self): - if self.tile and self.use_load_libtiff: - return self._load_libtiff() - return super().load() - - def load_end(self) -> None: - # allow closing if we're on the first frame, there's no next - # This is the ImageFile.load path only, libtiff specific below. - if not self.is_animated: - self._close_exclusive_fp_after_loading = True - - # reset buffered io handle in case fp - # was passed to libtiff, invalidating the buffer - self.fp.tell() - - # load IFD data from fp before it is closed - exif = self.getexif() - for key in TiffTags.TAGS_V2_GROUPS: - if key not in exif: - continue - exif.get_ifd(key) - - ImageOps.exif_transpose(self, in_place=True) - if ExifTags.Base.Orientation in self.tag_v2: - del self.tag_v2[ExifTags.Base.Orientation] - - def _load_libtiff(self): - """Overload method triggered when we detect a compressed tiff - Calls out to libtiff""" - - Image.Image.load(self) - - self.load_prepare() - - if not len(self.tile) == 1: - msg = "Not exactly one tile" - raise OSError(msg) - - # (self._compression, (extents tuple), - # 0, (rawmode, self._compression, fp)) - extents = self.tile[0][1] - args = list(self.tile[0][3]) - - # To be nice on memory footprint, if there's a - # file descriptor, use that instead of reading - # into a string in python. - try: - fp = hasattr(self.fp, "fileno") and self.fp.fileno() - # flush the file descriptor, prevents error on pypy 2.4+ - # should also eliminate the need for fp.tell - # in _seek - if hasattr(self.fp, "flush"): - self.fp.flush() - except OSError: - # io.BytesIO have a fileno, but returns an OSError if - # it doesn't use a file descriptor. - fp = False - - if fp: - args[2] = fp - - decoder = Image._getdecoder( - self.mode, "libtiff", tuple(args), self.decoderconfig - ) - try: - decoder.setimage(self.im, extents) - except ValueError as e: - msg = "Couldn't set the image" - raise OSError(msg) from e - - close_self_fp = self._exclusive_fp and not self.is_animated - if hasattr(self.fp, "getvalue"): - # We've got a stringio like thing passed in. Yay for all in memory. - # The decoder needs the entire file in one shot, so there's not - # a lot we can do here other than give it the entire file. - # unless we could do something like get the address of the - # underlying string for stringio. - # - # Rearranging for supporting byteio items, since they have a fileno - # that returns an OSError if there's no underlying fp. Easier to - # deal with here by reordering. - logger.debug("have getvalue. just sending in a string from getvalue") - n, err = decoder.decode(self.fp.getvalue()) - elif fp: - # we've got a actual file on disk, pass in the fp. - logger.debug("have fileno, calling fileno version of the decoder.") - if not close_self_fp: - self.fp.seek(0) - # 4 bytes, otherwise the trace might error out - n, err = decoder.decode(b"fpfp") - else: - # we have something else. - logger.debug("don't have fileno or getvalue. just reading") - self.fp.seek(0) - # UNDONE -- so much for that buffer size thing. - n, err = decoder.decode(self.fp.read()) - - self.tile = [] - self.readonly = 0 - - self.load_end() - - if close_self_fp: - self.fp.close() - self.fp = None # might be shared - - if err < 0: - raise OSError(err) - - return Image.Image.load(self) - - def _setup(self): - """Setup this image object based on current tags""" - - if 0xBC01 in self.tag_v2: - msg = "Windows Media Photo files not yet supported" - raise OSError(msg) - - # extract relevant tags - self._compression = COMPRESSION_INFO[self.tag_v2.get(COMPRESSION, 1)] - self._planar_configuration = self.tag_v2.get(PLANAR_CONFIGURATION, 1) - - # photometric is a required tag, but not everyone is reading - # the specification - photo = self.tag_v2.get(PHOTOMETRIC_INTERPRETATION, 0) - - # old style jpeg compression images most certainly are YCbCr - if self._compression == "tiff_jpeg": - photo = 6 - - fillorder = self.tag_v2.get(FILLORDER, 1) - - logger.debug("*** Summary ***") - logger.debug("- compression: %s", self._compression) - logger.debug("- photometric_interpretation: %s", photo) - logger.debug("- planar_configuration: %s", self._planar_configuration) - logger.debug("- fill_order: %s", fillorder) - logger.debug("- YCbCr subsampling: %s", self.tag.get(YCBCRSUBSAMPLING)) - - # size - xsize = int(self.tag_v2.get(IMAGEWIDTH)) - ysize = int(self.tag_v2.get(IMAGELENGTH)) - self._size = xsize, ysize - - logger.debug("- size: %s", self.size) - - sample_format = self.tag_v2.get(SAMPLEFORMAT, (1,)) - if len(sample_format) > 1 and max(sample_format) == min(sample_format) == 1: - # SAMPLEFORMAT is properly per band, so an RGB image will - # be (1,1,1). But, we don't support per band pixel types, - # and anything more than one band is a uint8. So, just - # take the first element. Revisit this if adding support - # for more exotic images. - sample_format = (1,) - - bps_tuple = self.tag_v2.get(BITSPERSAMPLE, (1,)) - extra_tuple = self.tag_v2.get(EXTRASAMPLES, ()) - if photo in (2, 6, 8): # RGB, YCbCr, LAB - bps_count = 3 - elif photo == 5: # CMYK - bps_count = 4 - else: - bps_count = 1 - bps_count += len(extra_tuple) - bps_actual_count = len(bps_tuple) - samples_per_pixel = self.tag_v2.get( - SAMPLESPERPIXEL, - 3 if self._compression == "tiff_jpeg" and photo in (2, 6) else 1, - ) - - if samples_per_pixel > MAX_SAMPLESPERPIXEL: - # DOS check, samples_per_pixel can be a Long, and we extend the tuple below - logger.error( - "More samples per pixel than can be decoded: %s", samples_per_pixel - ) - msg = "Invalid value for samples per pixel" - raise SyntaxError(msg) - - if samples_per_pixel < bps_actual_count: - # If a file has more values in bps_tuple than expected, - # remove the excess. - bps_tuple = bps_tuple[:samples_per_pixel] - elif samples_per_pixel > bps_actual_count and bps_actual_count == 1: - # If a file has only one value in bps_tuple, when it should have more, - # presume it is the same number of bits for all of the samples. - bps_tuple = bps_tuple * samples_per_pixel - - if len(bps_tuple) != samples_per_pixel: - msg = "unknown data organization" - raise SyntaxError(msg) - - # mode: check photometric interpretation and bits per pixel - key = ( - self.tag_v2.prefix, - photo, - sample_format, - fillorder, - bps_tuple, - extra_tuple, - ) - logger.debug("format key: %s", key) - try: - self._mode, rawmode = OPEN_INFO[key] - except KeyError as e: - logger.debug("- unsupported format") - msg = "unknown pixel mode" - raise SyntaxError(msg) from e - - logger.debug("- raw mode: %s", rawmode) - logger.debug("- pil mode: %s", self.mode) - - self.info["compression"] = self._compression - - xres = self.tag_v2.get(X_RESOLUTION, 1) - yres = self.tag_v2.get(Y_RESOLUTION, 1) - - if xres and yres: - resunit = self.tag_v2.get(RESOLUTION_UNIT) - if resunit == 2: # dots per inch - self.info["dpi"] = (xres, yres) - elif resunit == 3: # dots per centimeter. convert to dpi - self.info["dpi"] = (xres * 2.54, yres * 2.54) - elif resunit is None: # used to default to 1, but now 2) - self.info["dpi"] = (xres, yres) - # For backward compatibility, - # we also preserve the old behavior - self.info["resolution"] = xres, yres - else: # No absolute unit of measurement - self.info["resolution"] = xres, yres - - # build tile descriptors - x = y = layer = 0 - self.tile = [] - self.use_load_libtiff = READ_LIBTIFF or self._compression != "raw" - if self.use_load_libtiff: - # Decoder expects entire file as one tile. - # There's a buffer size limit in load (64k) - # so large g4 images will fail if we use that - # function. - # - # Setup the one tile for the whole image, then - # use the _load_libtiff function. - - # libtiff handles the fillmode for us, so 1;IR should - # actually be 1;I. Including the R double reverses the - # bits, so stripes of the image are reversed. See - # https://github.com/python-pillow/Pillow/issues/279 - if fillorder == 2: - # Replace fillorder with fillorder=1 - key = key[:3] + (1,) + key[4:] - logger.debug("format key: %s", key) - # this should always work, since all the - # fillorder==2 modes have a corresponding - # fillorder=1 mode - self._mode, rawmode = OPEN_INFO[key] - # libtiff always returns the bytes in native order. - # we're expecting image byte order. So, if the rawmode - # contains I;16, we need to convert from native to image - # byte order. - if rawmode == "I;16": - rawmode = "I;16N" - if ";16B" in rawmode: - rawmode = rawmode.replace(";16B", ";16N") - if ";16L" in rawmode: - rawmode = rawmode.replace(";16L", ";16N") - - # YCbCr images with new jpeg compression with pixels in one plane - # unpacked straight into RGB values - if ( - photo == 6 - and self._compression == "jpeg" - and self._planar_configuration == 1 - ): - rawmode = "RGB" - - # Offset in the tile tuple is 0, we go from 0,0 to - # w,h, and we only do this once -- eds - a = (rawmode, self._compression, False, self.tag_v2.offset) - self.tile.append(("libtiff", (0, 0, xsize, ysize), 0, a)) - - elif STRIPOFFSETS in self.tag_v2 or TILEOFFSETS in self.tag_v2: - # striped image - if STRIPOFFSETS in self.tag_v2: - offsets = self.tag_v2[STRIPOFFSETS] - h = self.tag_v2.get(ROWSPERSTRIP, ysize) - w = self.size[0] - else: - # tiled image - offsets = self.tag_v2[TILEOFFSETS] - w = self.tag_v2.get(TILEWIDTH) - h = self.tag_v2.get(TILELENGTH) - - for offset in offsets: - if x + w > xsize: - stride = w * sum(bps_tuple) / 8 # bytes per line - else: - stride = 0 - - tile_rawmode = rawmode - if self._planar_configuration == 2: - # each band on it's own layer - tile_rawmode = rawmode[layer] - # adjust stride width accordingly - stride /= bps_count - - a = (tile_rawmode, int(stride), 1) - self.tile.append( - ( - self._compression, - (x, y, min(x + w, xsize), min(y + h, ysize)), - offset, - a, - ) - ) - x = x + w - if x >= self.size[0]: - x, y = 0, y + h - if y >= self.size[1]: - x = y = 0 - layer += 1 - else: - logger.debug("- unsupported data organization") - msg = "unknown data organization" - raise SyntaxError(msg) - - # Fix up info. - if ICCPROFILE in self.tag_v2: - self.info["icc_profile"] = self.tag_v2[ICCPROFILE] - - # fixup palette descriptor - - if self.mode in ["P", "PA"]: - palette = [o8(b // 256) for b in self.tag_v2[COLORMAP]] - self.palette = ImagePalette.raw("RGB;L", b"".join(palette)) - - -# -# -------------------------------------------------------------------- -# Write TIFF files - -# little endian is default except for image modes with -# explicit big endian byte-order - -SAVE_INFO = { - # mode => rawmode, byteorder, photometrics, - # sampleformat, bitspersample, extra - "1": ("1", II, 1, 1, (1,), None), - "L": ("L", II, 1, 1, (8,), None), - "LA": ("LA", II, 1, 1, (8, 8), 2), - "P": ("P", II, 3, 1, (8,), None), - "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), - "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), - "RGBA": ("RGBA", II, 2, 1, (8, 8, 8, 8), 2), - "CMYK": ("CMYK", II, 5, 1, (8, 8, 8, 8), None), - "YCbCr": ("YCbCr", II, 6, 1, (8, 8, 8), None), - "LAB": ("LAB", II, 8, 1, (8, 8, 8), None), - "I;32BS": ("I;32BS", MM, 1, 2, (32,), None), - "I;16B": ("I;16B", MM, 1, 1, (16,), None), - "I;16BS": ("I;16BS", MM, 1, 2, (16,), None), - "F;32BF": ("F;32BF", MM, 1, 3, (32,), None), -} - - -def _save(im, fp, filename): - try: - rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode] - except KeyError as e: - msg = f"cannot write mode {im.mode} as TIFF" - raise OSError(msg) from e - - ifd = ImageFileDirectory_v2(prefix=prefix) - - encoderinfo = im.encoderinfo - encoderconfig = im.encoderconfig - try: - compression = encoderinfo["compression"] - except KeyError: - compression = im.info.get("compression") - if isinstance(compression, int): - # compression value may be from BMP. Ignore it - compression = None - if compression is None: - compression = "raw" - elif compression == "tiff_jpeg": - # OJPEG is obsolete, so use new-style JPEG compression instead - compression = "jpeg" - elif compression == "tiff_deflate": - compression = "tiff_adobe_deflate" - - libtiff = WRITE_LIBTIFF or compression != "raw" - - # required for color libtiff images - ifd[PLANAR_CONFIGURATION] = 1 - - ifd[IMAGEWIDTH] = im.size[0] - ifd[IMAGELENGTH] = im.size[1] - - # write any arbitrary tags passed in as an ImageFileDirectory - if "tiffinfo" in encoderinfo: - info = encoderinfo["tiffinfo"] - elif "exif" in encoderinfo: - info = encoderinfo["exif"] - if isinstance(info, bytes): - exif = Image.Exif() - exif.load(info) - info = exif - else: - info = {} - logger.debug("Tiffinfo Keys: %s", list(info)) - if isinstance(info, ImageFileDirectory_v1): - info = info.to_v2() - for key in info: - if isinstance(info, Image.Exif) and key in TiffTags.TAGS_V2_GROUPS: - ifd[key] = info.get_ifd(key) - else: - ifd[key] = info.get(key) - try: - ifd.tagtype[key] = info.tagtype[key] - except Exception: - pass # might not be an IFD. Might not have populated type - - # additions written by Greg Couch, gregc@cgl.ucsf.edu - # inspired by image-sig posting from Kevin Cazabon, kcazabon@home.com - if hasattr(im, "tag_v2"): - # preserve tags from original TIFF image file - for key in ( - RESOLUTION_UNIT, - X_RESOLUTION, - Y_RESOLUTION, - IPTC_NAA_CHUNK, - PHOTOSHOP_CHUNK, - XMP, - ): - if key in im.tag_v2: - ifd[key] = im.tag_v2[key] - ifd.tagtype[key] = im.tag_v2.tagtype[key] - - # preserve ICC profile (should also work when saving other formats - # which support profiles as TIFF) -- 2008-06-06 Florian Hoech - icc = encoderinfo.get("icc_profile", im.info.get("icc_profile")) - if icc: - ifd[ICCPROFILE] = icc - - for key, name in [ - (IMAGEDESCRIPTION, "description"), - (X_RESOLUTION, "resolution"), - (Y_RESOLUTION, "resolution"), - (X_RESOLUTION, "x_resolution"), - (Y_RESOLUTION, "y_resolution"), - (RESOLUTION_UNIT, "resolution_unit"), - (SOFTWARE, "software"), - (DATE_TIME, "date_time"), - (ARTIST, "artist"), - (COPYRIGHT, "copyright"), - ]: - if name in encoderinfo: - ifd[key] = encoderinfo[name] - - dpi = encoderinfo.get("dpi") - if dpi: - ifd[RESOLUTION_UNIT] = 2 - ifd[X_RESOLUTION] = dpi[0] - ifd[Y_RESOLUTION] = dpi[1] - - if bits != (1,): - ifd[BITSPERSAMPLE] = bits - if len(bits) != 1: - ifd[SAMPLESPERPIXEL] = len(bits) - if extra is not None: - ifd[EXTRASAMPLES] = extra - if format != 1: - ifd[SAMPLEFORMAT] = format - - if PHOTOMETRIC_INTERPRETATION not in ifd: - ifd[PHOTOMETRIC_INTERPRETATION] = photo - elif im.mode in ("1", "L") and ifd[PHOTOMETRIC_INTERPRETATION] == 0: - if im.mode == "1": - inverted_im = im.copy() - px = inverted_im.load() - for y in range(inverted_im.height): - for x in range(inverted_im.width): - px[x, y] = 0 if px[x, y] == 255 else 255 - im = inverted_im - else: - im = ImageOps.invert(im) - - if im.mode in ["P", "PA"]: - lut = im.im.getpalette("RGB", "RGB;L") - colormap = [] - colors = len(lut) // 3 - for i in range(3): - colormap += [v * 256 for v in lut[colors * i : colors * (i + 1)]] - colormap += [0] * (256 - colors) - ifd[COLORMAP] = colormap - # data orientation - w, h = ifd[IMAGEWIDTH], ifd[IMAGELENGTH] - stride = len(bits) * ((w * bits[0] + 7) // 8) - if ROWSPERSTRIP not in ifd: - # aim for given strip size (64 KB by default) when using libtiff writer - if libtiff: - im_strip_size = encoderinfo.get("strip_size", STRIP_SIZE) - rows_per_strip = 1 if stride == 0 else min(im_strip_size // stride, h) - # JPEG encoder expects multiple of 8 rows - if compression == "jpeg": - rows_per_strip = min(((rows_per_strip + 7) // 8) * 8, h) - else: - rows_per_strip = h - if rows_per_strip == 0: - rows_per_strip = 1 - ifd[ROWSPERSTRIP] = rows_per_strip - strip_byte_counts = 1 if stride == 0 else stride * ifd[ROWSPERSTRIP] - strips_per_image = (h + ifd[ROWSPERSTRIP] - 1) // ifd[ROWSPERSTRIP] - if strip_byte_counts >= 2**16: - ifd.tagtype[STRIPBYTECOUNTS] = TiffTags.LONG - ifd[STRIPBYTECOUNTS] = (strip_byte_counts,) * (strips_per_image - 1) + ( - stride * h - strip_byte_counts * (strips_per_image - 1), - ) - ifd[STRIPOFFSETS] = tuple( - range(0, strip_byte_counts * strips_per_image, strip_byte_counts) - ) # this is adjusted by IFD writer - # no compression by default: - ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1) - - if im.mode == "YCbCr": - for tag, value in { - YCBCRSUBSAMPLING: (1, 1), - REFERENCEBLACKWHITE: (0, 255, 128, 255, 128, 255), - }.items(): - ifd.setdefault(tag, value) - - blocklist = [TILEWIDTH, TILELENGTH, TILEOFFSETS, TILEBYTECOUNTS] - if libtiff: - if "quality" in encoderinfo: - quality = encoderinfo["quality"] - if not isinstance(quality, int) or quality < 0 or quality > 100: - msg = "Invalid quality setting" - raise ValueError(msg) - if compression != "jpeg": - msg = "quality setting only supported for 'jpeg' compression" - raise ValueError(msg) - ifd[JPEGQUALITY] = quality - - logger.debug("Saving using libtiff encoder") - logger.debug("Items: %s", sorted(ifd.items())) - _fp = 0 - if hasattr(fp, "fileno"): - try: - fp.seek(0) - _fp = os.dup(fp.fileno()) - except io.UnsupportedOperation: - pass - - # optional types for non core tags - types = {} - # STRIPOFFSETS and STRIPBYTECOUNTS are added by the library - # based on the data in the strip. - # OSUBFILETYPE is deprecated. - # The other tags expect arrays with a certain length (fixed or depending on - # BITSPERSAMPLE, etc), passing arrays with a different length will result in - # segfaults. Block these tags until we add extra validation. - # SUBIFD may also cause a segfault. - blocklist += [ - OSUBFILETYPE, - REFERENCEBLACKWHITE, - STRIPBYTECOUNTS, - STRIPOFFSETS, - TRANSFERFUNCTION, - SUBIFD, - ] - - # bits per sample is a single short in the tiff directory, not a list. - atts = {BITSPERSAMPLE: bits[0]} - # Merge the ones that we have with (optional) more bits from - # the original file, e.g x,y resolution so that we can - # save(load('')) == original file. - legacy_ifd = {} - if hasattr(im, "tag"): - legacy_ifd = im.tag.to_v2() - - # SAMPLEFORMAT is determined by the image format and should not be copied - # from legacy_ifd. - supplied_tags = {**getattr(im, "tag_v2", {}), **legacy_ifd} - if SAMPLEFORMAT in supplied_tags: - del supplied_tags[SAMPLEFORMAT] - - for tag, value in itertools.chain(ifd.items(), supplied_tags.items()): - # Libtiff can only process certain core items without adding - # them to the custom dictionary. - # Custom items are supported for int, float, unicode, string and byte - # values. Other types and tuples require a tagtype. - if tag not in TiffTags.LIBTIFF_CORE: - if not getattr(Image.core, "libtiff_support_custom_tags", False): - continue - - if tag in ifd.tagtype: - types[tag] = ifd.tagtype[tag] - elif not (isinstance(value, (int, float, str, bytes))): - continue - else: - type = TiffTags.lookup(tag).type - if type: - types[tag] = type - if tag not in atts and tag not in blocklist: - if isinstance(value, str): - atts[tag] = value.encode("ascii", "replace") + b"\0" - elif isinstance(value, IFDRational): - atts[tag] = float(value) - else: - atts[tag] = value - - if SAMPLEFORMAT in atts and len(atts[SAMPLEFORMAT]) == 1: - atts[SAMPLEFORMAT] = atts[SAMPLEFORMAT][0] - - logger.debug("Converted items: %s", sorted(atts.items())) - - # libtiff always expects the bytes in native order. - # we're storing image byte order. So, if the rawmode - # contains I;16, we need to convert from native to image - # byte order. - if im.mode in ("I;16B", "I;16"): - rawmode = "I;16N" - - # Pass tags as sorted list so that the tags are set in a fixed order. - # This is required by libtiff for some tags. For example, the JPEGQUALITY - # pseudo tag requires that the COMPRESS tag was already set. - tags = list(atts.items()) - tags.sort() - a = (rawmode, compression, _fp, filename, tags, types) - encoder = Image._getencoder(im.mode, "libtiff", a, encoderconfig) - encoder.setimage(im.im, (0, 0) + im.size) - while True: - # undone, change to self.decodermaxblock: - errcode, data = encoder.encode(16 * 1024)[1:] - if not _fp: - fp.write(data) - if errcode: - break - if _fp: - try: - os.close(_fp) - except OSError: - pass - if errcode < 0: - msg = f"encoder error {errcode} when writing image file" - raise OSError(msg) - - else: - for tag in blocklist: - del ifd[tag] - offset = ifd.save(fp) - - ImageFile._save( - im, fp, [("raw", (0, 0) + im.size, offset, (rawmode, stride, 1))] - ) - - # -- helper for multi-page save -- - if "_debug_multipage" in encoderinfo: - # just to access o32 and o16 (using correct byte order) - im._debug_multipage = ifd - - -class AppendingTiffWriter: - fieldSizes = [ - 0, # None - 1, # byte - 1, # ascii - 2, # short - 4, # long - 8, # rational - 1, # sbyte - 1, # undefined - 2, # sshort - 4, # slong - 8, # srational - 4, # float - 8, # double - 4, # ifd - 2, # unicode - 4, # complex - 8, # long8 - ] - - Tags = { - 273, # StripOffsets - 288, # FreeOffsets - 324, # TileOffsets - 519, # JPEGQTables - 520, # JPEGDCTables - 521, # JPEGACTables - } - - def __init__(self, fn, new=False): - if hasattr(fn, "read"): - self.f = fn - self.close_fp = False - else: - self.name = fn - self.close_fp = True - try: - self.f = open(fn, "w+b" if new else "r+b") - except OSError: - self.f = open(fn, "w+b") - self.beginning = self.f.tell() - self.setup() - - def setup(self) -> None: - # Reset everything. - self.f.seek(self.beginning, os.SEEK_SET) - - self.whereToWriteNewIFDOffset = None - self.offsetOfNewPage = 0 - - self.IIMM = iimm = self.f.read(4) - if not iimm: - # empty file - first page - self.isFirst = True - return - - self.isFirst = False - if iimm == b"II\x2a\x00": - self.setEndian("<") - elif iimm == b"MM\x00\x2a": - self.setEndian(">") - else: - msg = "Invalid TIFF file header" - raise RuntimeError(msg) - - self.skipIFDs() - self.goToEnd() - - def finalize(self) -> None: - if self.isFirst: - return - - # fix offsets - self.f.seek(self.offsetOfNewPage) - - iimm = self.f.read(4) - if not iimm: - # Make it easy to finish a frame without committing to a new one. - return - - if iimm != self.IIMM: - msg = "IIMM of new page doesn't match IIMM of first page" - raise RuntimeError(msg) - - ifd_offset = self.readLong() - ifd_offset += self.offsetOfNewPage - self.f.seek(self.whereToWriteNewIFDOffset) - self.writeLong(ifd_offset) - self.f.seek(ifd_offset) - self.fixIFD() - - def newFrame(self) -> None: - # Call this to finish a frame. - self.finalize() - self.setup() - - def __enter__(self) -> AppendingTiffWriter: - return self - - def __exit__(self, *args: object) -> None: - if self.close_fp: - self.close() - - def tell(self) -> int: - return self.f.tell() - self.offsetOfNewPage - - def seek(self, offset, whence=io.SEEK_SET): - if whence == os.SEEK_SET: - offset += self.offsetOfNewPage - - self.f.seek(offset, whence) - return self.tell() - - def goToEnd(self) -> None: - self.f.seek(0, os.SEEK_END) - pos = self.f.tell() - - # pad to 16 byte boundary - pad_bytes = 16 - pos % 16 - if 0 < pad_bytes < 16: - self.f.write(bytes(pad_bytes)) - self.offsetOfNewPage = self.f.tell() - - def setEndian(self, endian: str) -> None: - self.endian = endian - self.longFmt = f"{self.endian}L" - self.shortFmt = f"{self.endian}H" - self.tagFormat = f"{self.endian}HHL" - - def skipIFDs(self) -> None: - while True: - ifd_offset = self.readLong() - if ifd_offset == 0: - self.whereToWriteNewIFDOffset = self.f.tell() - 4 - break - - self.f.seek(ifd_offset) - num_tags = self.readShort() - self.f.seek(num_tags * 12, os.SEEK_CUR) - - def write(self, data): - return self.f.write(data) - - def readShort(self) -> int: - (value,) = struct.unpack(self.shortFmt, self.f.read(2)) - return value - - def readLong(self) -> int: - (value,) = struct.unpack(self.longFmt, self.f.read(4)) - return value - - def rewriteLastShortToLong(self, value: int) -> None: - self.f.seek(-2, os.SEEK_CUR) - bytes_written = self.f.write(struct.pack(self.longFmt, value)) - if bytes_written is not None and bytes_written != 4: - msg = f"wrote only {bytes_written} bytes but wanted 4" - raise RuntimeError(msg) - - def rewriteLastShort(self, value: int) -> None: - self.f.seek(-2, os.SEEK_CUR) - bytes_written = self.f.write(struct.pack(self.shortFmt, value)) - if bytes_written is not None and bytes_written != 2: - msg = f"wrote only {bytes_written} bytes but wanted 2" - raise RuntimeError(msg) - - def rewriteLastLong(self, value: int) -> None: - self.f.seek(-4, os.SEEK_CUR) - bytes_written = self.f.write(struct.pack(self.longFmt, value)) - if bytes_written is not None and bytes_written != 4: - msg = f"wrote only {bytes_written} bytes but wanted 4" - raise RuntimeError(msg) - - def writeShort(self, value: int) -> None: - bytes_written = self.f.write(struct.pack(self.shortFmt, value)) - if bytes_written is not None and bytes_written != 2: - msg = f"wrote only {bytes_written} bytes but wanted 2" - raise RuntimeError(msg) - - def writeLong(self, value: int) -> None: - bytes_written = self.f.write(struct.pack(self.longFmt, value)) - if bytes_written is not None and bytes_written != 4: - msg = f"wrote only {bytes_written} bytes but wanted 4" - raise RuntimeError(msg) - - def close(self) -> None: - self.finalize() - self.f.close() - - def fixIFD(self) -> None: - num_tags = self.readShort() - - for i in range(num_tags): - tag, field_type, count = struct.unpack(self.tagFormat, self.f.read(8)) - - field_size = self.fieldSizes[field_type] - total_size = field_size * count - is_local = total_size <= 4 - offset: int | None - if not is_local: - offset = self.readLong() + self.offsetOfNewPage - self.rewriteLastLong(offset) - - if tag in self.Tags: - cur_pos = self.f.tell() - - if is_local: - self.fixOffsets( - count, isShort=(field_size == 2), isLong=(field_size == 4) - ) - self.f.seek(cur_pos + 4) - else: - self.f.seek(offset) - self.fixOffsets( - count, isShort=(field_size == 2), isLong=(field_size == 4) - ) - self.f.seek(cur_pos) - - offset = cur_pos = None - - elif is_local: - # skip the locally stored value that is not an offset - self.f.seek(4, os.SEEK_CUR) - - def fixOffsets(self, count, isShort=False, isLong=False): - if not isShort and not isLong: - msg = "offset is neither short nor long" - raise RuntimeError(msg) - - for i in range(count): - offset = self.readShort() if isShort else self.readLong() - offset += self.offsetOfNewPage - if isShort and offset >= 65536: - # offset is now too large - we must convert shorts to longs - if count != 1: - msg = "not implemented" - raise RuntimeError(msg) # XXX TODO - - # simple case - the offset is just one and therefore it is - # local (not referenced with another offset) - self.rewriteLastShortToLong(offset) - self.f.seek(-10, os.SEEK_CUR) - self.writeShort(TiffTags.LONG) # rewrite the type to LONG - self.f.seek(8, os.SEEK_CUR) - elif isShort: - self.rewriteLastShort(offset) - else: - self.rewriteLastLong(offset) - - -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", [])) - if not hasattr(im, "n_frames") and not append_images: - return _save(im, fp, filename) - - cur_idx = im.tell() - 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 - - for idx in range(nfr): - ims.seek(idx) - ims.load() - _save(ims, tf, filename) - tf.newFrame() - finally: - im.seek(cur_idx) - - -# -# -------------------------------------------------------------------- -# Register - -Image.register_open(TiffImageFile.format, TiffImageFile, _accept) -Image.register_save(TiffImageFile.format, _save) -Image.register_save_all(TiffImageFile.format, _save_all) - -Image.register_extensions(TiffImageFile.format, [".tif", ".tiff"]) - -Image.register_mime(TiffImageFile.format, "image/tiff") +# +# The Python Imaging Library. +# $Id$ +# +# TIFF file handling +# +# TIFF is a flexible, if somewhat aged, image file format originally +# defined by Aldus. Although TIFF supports a wide variety of pixel +# layouts and compression methods, the name doesn't really stand for +# "thousands of incompatible file formats," it just feels that way. +# +# To read TIFF data from a stream, the stream must be seekable. For +# progressive decoding, make sure to use TIFF files where the tag +# directory is placed first in the file. +# +# History: +# 1995-09-01 fl Created +# 1996-05-04 fl Handle JPEGTABLES tag +# 1996-05-18 fl Fixed COLORMAP support +# 1997-01-05 fl Fixed PREDICTOR support +# 1997-08-27 fl Added support for rational tags (from Perry Stoll) +# 1998-01-10 fl Fixed seek/tell (from Jan Blom) +# 1998-07-15 fl Use private names for internal variables +# 1999-06-13 fl Rewritten for PIL 1.0 (1.0) +# 2000-10-11 fl Additional fixes for Python 2.0 (1.1) +# 2001-04-17 fl Fixed rewind support (seek to frame 0) (1.2) +# 2001-05-12 fl Added write support for more tags (from Greg Couch) (1.3) +# 2001-12-18 fl Added workaround for broken Matrox library +# 2002-01-18 fl Don't mess up if photometric tag is missing (D. Alan Stewart) +# 2003-05-19 fl Check FILLORDER tag +# 2003-09-26 fl Added RGBa support +# 2004-02-24 fl Added DPI support; fixed rational write support +# 2005-02-07 fl Added workaround for broken Corel Draw 10 files +# 2006-01-09 fl Added support for float/double tags (from Russell Nelson) +# +# Copyright (c) 1997-2006 by Secret Labs AB. All rights reserved. +# Copyright (c) 1995-1997 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import io +import itertools +import logging +import math +import os +import struct +import warnings +from collections.abc import MutableMapping +from fractions import Fraction +from numbers import Number, Rational +from typing import IO, TYPE_CHECKING, Any, Callable + +from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags +from ._binary import i16be as i16 +from ._binary import i32be as i32 +from ._binary import o8 +from ._deprecate import deprecate +from .TiffTags import TYPES + +logger = logging.getLogger(__name__) + +# Set these to true to force use of libtiff for reading or writing. +READ_LIBTIFF = False +WRITE_LIBTIFF = False +IFD_LEGACY_API = True +STRIP_SIZE = 65536 + +II = b"II" # little-endian (Intel style) +MM = b"MM" # big-endian (Motorola style) + +# +# -------------------------------------------------------------------- +# Read TIFF files + +# a few tag names, just to make the code below a bit more readable +OSUBFILETYPE = 255 +IMAGEWIDTH = 256 +IMAGELENGTH = 257 +BITSPERSAMPLE = 258 +COMPRESSION = 259 +PHOTOMETRIC_INTERPRETATION = 262 +FILLORDER = 266 +IMAGEDESCRIPTION = 270 +STRIPOFFSETS = 273 +SAMPLESPERPIXEL = 277 +ROWSPERSTRIP = 278 +STRIPBYTECOUNTS = 279 +X_RESOLUTION = 282 +Y_RESOLUTION = 283 +PLANAR_CONFIGURATION = 284 +RESOLUTION_UNIT = 296 +TRANSFERFUNCTION = 301 +SOFTWARE = 305 +DATE_TIME = 306 +ARTIST = 315 +PREDICTOR = 317 +COLORMAP = 320 +TILEWIDTH = 322 +TILELENGTH = 323 +TILEOFFSETS = 324 +TILEBYTECOUNTS = 325 +SUBIFD = 330 +EXTRASAMPLES = 338 +SAMPLEFORMAT = 339 +JPEGTABLES = 347 +YCBCRSUBSAMPLING = 530 +REFERENCEBLACKWHITE = 532 +COPYRIGHT = 33432 +IPTC_NAA_CHUNK = 33723 # newsphoto properties +PHOTOSHOP_CHUNK = 34377 # photoshop properties +ICCPROFILE = 34675 +EXIFIFD = 34665 +XMP = 700 +JPEGQUALITY = 65537 # pseudo-tag by libtiff + +# https://github.com/imagej/ImageJA/blob/master/src/main/java/ij/io/TiffDecoder.java +IMAGEJ_META_DATA_BYTE_COUNTS = 50838 +IMAGEJ_META_DATA = 50839 + +COMPRESSION_INFO = { + # Compression => pil compression name + 1: "raw", + 2: "tiff_ccitt", + 3: "group3", + 4: "group4", + 5: "tiff_lzw", + 6: "tiff_jpeg", # obsolete + 7: "jpeg", + 8: "tiff_adobe_deflate", + 32771: "tiff_raw_16", # 16-bit padding + 32773: "packbits", + 32809: "tiff_thunderscan", + 32946: "tiff_deflate", + 34676: "tiff_sgilog", + 34677: "tiff_sgilog24", + 34925: "lzma", + 50000: "zstd", + 50001: "webp", +} + +COMPRESSION_INFO_REV = {v: k for k, v in COMPRESSION_INFO.items()} + +OPEN_INFO = { + # (ByteOrder, PhotoInterpretation, SampleFormat, FillOrder, BitsPerSample, + # ExtraSamples) => mode, rawmode + (II, 0, (1,), 1, (1,), ()): ("1", "1;I"), + (MM, 0, (1,), 1, (1,), ()): ("1", "1;I"), + (II, 0, (1,), 2, (1,), ()): ("1", "1;IR"), + (MM, 0, (1,), 2, (1,), ()): ("1", "1;IR"), + (II, 1, (1,), 1, (1,), ()): ("1", "1"), + (MM, 1, (1,), 1, (1,), ()): ("1", "1"), + (II, 1, (1,), 2, (1,), ()): ("1", "1;R"), + (MM, 1, (1,), 2, (1,), ()): ("1", "1;R"), + (II, 0, (1,), 1, (2,), ()): ("L", "L;2I"), + (MM, 0, (1,), 1, (2,), ()): ("L", "L;2I"), + (II, 0, (1,), 2, (2,), ()): ("L", "L;2IR"), + (MM, 0, (1,), 2, (2,), ()): ("L", "L;2IR"), + (II, 1, (1,), 1, (2,), ()): ("L", "L;2"), + (MM, 1, (1,), 1, (2,), ()): ("L", "L;2"), + (II, 1, (1,), 2, (2,), ()): ("L", "L;2R"), + (MM, 1, (1,), 2, (2,), ()): ("L", "L;2R"), + (II, 0, (1,), 1, (4,), ()): ("L", "L;4I"), + (MM, 0, (1,), 1, (4,), ()): ("L", "L;4I"), + (II, 0, (1,), 2, (4,), ()): ("L", "L;4IR"), + (MM, 0, (1,), 2, (4,), ()): ("L", "L;4IR"), + (II, 1, (1,), 1, (4,), ()): ("L", "L;4"), + (MM, 1, (1,), 1, (4,), ()): ("L", "L;4"), + (II, 1, (1,), 2, (4,), ()): ("L", "L;4R"), + (MM, 1, (1,), 2, (4,), ()): ("L", "L;4R"), + (II, 0, (1,), 1, (8,), ()): ("L", "L;I"), + (MM, 0, (1,), 1, (8,), ()): ("L", "L;I"), + (II, 0, (1,), 2, (8,), ()): ("L", "L;IR"), + (MM, 0, (1,), 2, (8,), ()): ("L", "L;IR"), + (II, 1, (1,), 1, (8,), ()): ("L", "L"), + (MM, 1, (1,), 1, (8,), ()): ("L", "L"), + (II, 1, (2,), 1, (8,), ()): ("L", "L"), + (MM, 1, (2,), 1, (8,), ()): ("L", "L"), + (II, 1, (1,), 2, (8,), ()): ("L", "L;R"), + (MM, 1, (1,), 2, (8,), ()): ("L", "L;R"), + (II, 1, (1,), 1, (12,), ()): ("I;16", "I;12"), + (II, 0, (1,), 1, (16,), ()): ("I;16", "I;16"), + (II, 1, (1,), 1, (16,), ()): ("I;16", "I;16"), + (MM, 1, (1,), 1, (16,), ()): ("I;16B", "I;16B"), + (II, 1, (1,), 2, (16,), ()): ("I;16", "I;16R"), + (II, 1, (2,), 1, (16,), ()): ("I", "I;16S"), + (MM, 1, (2,), 1, (16,), ()): ("I", "I;16BS"), + (II, 0, (3,), 1, (32,), ()): ("F", "F;32F"), + (MM, 0, (3,), 1, (32,), ()): ("F", "F;32BF"), + (II, 1, (1,), 1, (32,), ()): ("I", "I;32N"), + (II, 1, (2,), 1, (32,), ()): ("I", "I;32S"), + (MM, 1, (2,), 1, (32,), ()): ("I", "I;32BS"), + (II, 1, (3,), 1, (32,), ()): ("F", "F;32F"), + (MM, 1, (3,), 1, (32,), ()): ("F", "F;32BF"), + (II, 1, (1,), 1, (8, 8), (2,)): ("LA", "LA"), + (MM, 1, (1,), 1, (8, 8), (2,)): ("LA", "LA"), + (II, 2, (1,), 1, (8, 8, 8), ()): ("RGB", "RGB"), + (MM, 2, (1,), 1, (8, 8, 8), ()): ("RGB", "RGB"), + (II, 2, (1,), 2, (8, 8, 8), ()): ("RGB", "RGB;R"), + (MM, 2, (1,), 2, (8, 8, 8), ()): ("RGB", "RGB;R"), + (II, 2, (1,), 1, (8, 8, 8, 8), ()): ("RGBA", "RGBA"), # missing ExtraSamples + (MM, 2, (1,), 1, (8, 8, 8, 8), ()): ("RGBA", "RGBA"), # missing ExtraSamples + (II, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGBX", "RGBX"), + (MM, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGBX", "RGBX"), + (II, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("RGBX", "RGBXX"), + (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("RGBX", "RGBXX"), + (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): ("RGBX", "RGBXXX"), + (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): ("RGBX", "RGBXXX"), + (II, 2, (1,), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"), + (MM, 2, (1,), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"), + (II, 2, (1,), 1, (8, 8, 8, 8, 8), (1, 0)): ("RGBA", "RGBaX"), + (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (1, 0)): ("RGBA", "RGBaX"), + (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (1, 0, 0)): ("RGBA", "RGBaXX"), + (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (1, 0, 0)): ("RGBA", "RGBaXX"), + (II, 2, (1,), 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"), + (MM, 2, (1,), 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"), + (II, 2, (1,), 1, (8, 8, 8, 8, 8), (2, 0)): ("RGBA", "RGBAX"), + (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (2, 0)): ("RGBA", "RGBAX"), + (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (2, 0, 0)): ("RGBA", "RGBAXX"), + (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (2, 0, 0)): ("RGBA", "RGBAXX"), + (II, 2, (1,), 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10 + (MM, 2, (1,), 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10 + (II, 2, (1,), 1, (16, 16, 16), ()): ("RGB", "RGB;16L"), + (MM, 2, (1,), 1, (16, 16, 16), ()): ("RGB", "RGB;16B"), + (II, 2, (1,), 1, (16, 16, 16, 16), ()): ("RGBA", "RGBA;16L"), + (MM, 2, (1,), 1, (16, 16, 16, 16), ()): ("RGBA", "RGBA;16B"), + (II, 2, (1,), 1, (16, 16, 16, 16), (0,)): ("RGBX", "RGBX;16L"), + (MM, 2, (1,), 1, (16, 16, 16, 16), (0,)): ("RGBX", "RGBX;16B"), + (II, 2, (1,), 1, (16, 16, 16, 16), (1,)): ("RGBA", "RGBa;16L"), + (MM, 2, (1,), 1, (16, 16, 16, 16), (1,)): ("RGBA", "RGBa;16B"), + (II, 2, (1,), 1, (16, 16, 16, 16), (2,)): ("RGBA", "RGBA;16L"), + (MM, 2, (1,), 1, (16, 16, 16, 16), (2,)): ("RGBA", "RGBA;16B"), + (II, 3, (1,), 1, (1,), ()): ("P", "P;1"), + (MM, 3, (1,), 1, (1,), ()): ("P", "P;1"), + (II, 3, (1,), 2, (1,), ()): ("P", "P;1R"), + (MM, 3, (1,), 2, (1,), ()): ("P", "P;1R"), + (II, 3, (1,), 1, (2,), ()): ("P", "P;2"), + (MM, 3, (1,), 1, (2,), ()): ("P", "P;2"), + (II, 3, (1,), 2, (2,), ()): ("P", "P;2R"), + (MM, 3, (1,), 2, (2,), ()): ("P", "P;2R"), + (II, 3, (1,), 1, (4,), ()): ("P", "P;4"), + (MM, 3, (1,), 1, (4,), ()): ("P", "P;4"), + (II, 3, (1,), 2, (4,), ()): ("P", "P;4R"), + (MM, 3, (1,), 2, (4,), ()): ("P", "P;4R"), + (II, 3, (1,), 1, (8,), ()): ("P", "P"), + (MM, 3, (1,), 1, (8,), ()): ("P", "P"), + (II, 3, (1,), 1, (8, 8), (0,)): ("P", "PX"), + (II, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"), + (MM, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"), + (II, 3, (1,), 2, (8,), ()): ("P", "P;R"), + (MM, 3, (1,), 2, (8,), ()): ("P", "P;R"), + (II, 5, (1,), 1, (8, 8, 8, 8), ()): ("CMYK", "CMYK"), + (MM, 5, (1,), 1, (8, 8, 8, 8), ()): ("CMYK", "CMYK"), + (II, 5, (1,), 1, (8, 8, 8, 8, 8), (0,)): ("CMYK", "CMYKX"), + (MM, 5, (1,), 1, (8, 8, 8, 8, 8), (0,)): ("CMYK", "CMYKX"), + (II, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"), + (MM, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"), + (II, 5, (1,), 1, (16, 16, 16, 16), ()): ("CMYK", "CMYK;16L"), + (II, 6, (1,), 1, (8,), ()): ("L", "L"), + (MM, 6, (1,), 1, (8,), ()): ("L", "L"), + # JPEG compressed images handled by LibTiff and auto-converted to RGBX + # Minimal Baseline TIFF requires YCbCr images to have 3 SamplesPerPixel + (II, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGBX"), + (MM, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGBX"), + (II, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"), + (MM, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"), +} + +MAX_SAMPLESPERPIXEL = max(len(key_tp[4]) for key_tp in OPEN_INFO) + +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 +] + +if not getattr(Image.core, "libtiff_support_custom_tags", True): + deprecate("Support for LibTIFF earlier than version 4", 12) + + +def _accept(prefix: bytes) -> bool: + return prefix[:4] in PREFIXES + + +def _limit_rational(val, max_val): + inv = abs(val) > 1 + n_d = IFDRational(1 / val if inv else val).limit_rational(max_val) + return n_d[::-1] if inv else n_d + + +def _limit_signed_rational(val, max_val, min_val): + frac = Fraction(val) + n_d = frac.numerator, frac.denominator + + if min(n_d) < min_val: + n_d = _limit_rational(val, abs(min_val)) + + if max(n_d) > max_val: + val = Fraction(*n_d) + n_d = _limit_rational(val, max_val) + + return n_d + + +## +# Wrapper for TIFF IFDs. + +_load_dispatch = {} +_write_dispatch = {} + + +def _delegate(op): + def delegate(self, *args): + return getattr(self._val, op)(*args) + + return delegate + + +class IFDRational(Rational): + """Implements a rational class where 0/0 is a legal value to match + the in the wild use of exif rationals. + + e.g., DigitalZoomRatio - 0.00/0.00 indicates that no digital zoom was used + """ + + """ If the denominator is 0, store this as a float('nan'), otherwise store + as a fractions.Fraction(). Delegate as appropriate + + """ + + __slots__ = ("_numerator", "_denominator", "_val") + + def __init__(self, value, denominator=1): + """ + :param value: either an integer numerator, a + float/rational/other number, or an IFDRational + :param denominator: Optional integer denominator + """ + if isinstance(value, IFDRational): + self._numerator = value.numerator + self._denominator = value.denominator + self._val = value._val + return + + if isinstance(value, Fraction): + self._numerator = value.numerator + self._denominator = value.denominator + else: + self._numerator = value + self._denominator = denominator + + if denominator == 0: + self._val = float("nan") + elif denominator == 1: + self._val = Fraction(value) + else: + self._val = Fraction(value, denominator) + + @property + def numerator(self): + return self._numerator + + @property + def denominator(self): + return self._denominator + + def limit_rational(self, max_denominator): + """ + + :param max_denominator: Integer, the maximum denominator value + :returns: Tuple of (numerator, denominator) + """ + + if self.denominator == 0: + return self.numerator, self.denominator + + f = self._val.limit_denominator(max_denominator) + return f.numerator, f.denominator + + def __repr__(self) -> str: + return str(float(self._val)) + + def __hash__(self): + return self._val.__hash__() + + def __eq__(self, other: object) -> bool: + val = self._val + if isinstance(other, IFDRational): + other = other._val + if isinstance(other, float): + val = float(val) + return val == other + + def __getstate__(self): + return [self._val, self._numerator, self._denominator] + + def __setstate__(self, state): + IFDRational.__init__(self, 0) + _val, _numerator, _denominator = state + self._val = _val + self._numerator = _numerator + self._denominator = _denominator + + """ a = ['add','radd', 'sub', 'rsub', 'mul', 'rmul', + 'truediv', 'rtruediv', 'floordiv', 'rfloordiv', + 'mod','rmod', 'pow','rpow', 'pos', 'neg', + 'abs', 'trunc', 'lt', 'gt', 'le', 'ge', 'bool', + 'ceil', 'floor', 'round'] + print("\n".join("__%s__ = _delegate('__%s__')" % (s,s) for s in a)) + """ + + __add__ = _delegate("__add__") + __radd__ = _delegate("__radd__") + __sub__ = _delegate("__sub__") + __rsub__ = _delegate("__rsub__") + __mul__ = _delegate("__mul__") + __rmul__ = _delegate("__rmul__") + __truediv__ = _delegate("__truediv__") + __rtruediv__ = _delegate("__rtruediv__") + __floordiv__ = _delegate("__floordiv__") + __rfloordiv__ = _delegate("__rfloordiv__") + __mod__ = _delegate("__mod__") + __rmod__ = _delegate("__rmod__") + __pow__ = _delegate("__pow__") + __rpow__ = _delegate("__rpow__") + __pos__ = _delegate("__pos__") + __neg__ = _delegate("__neg__") + __abs__ = _delegate("__abs__") + __trunc__ = _delegate("__trunc__") + __lt__ = _delegate("__lt__") + __gt__ = _delegate("__gt__") + __le__ = _delegate("__le__") + __ge__ = _delegate("__ge__") + __bool__ = _delegate("__bool__") + __ceil__ = _delegate("__ceil__") + __floor__ = _delegate("__floor__") + __round__ = _delegate("__round__") + # Python >= 3.11 + if hasattr(Fraction, "__int__"): + __int__ = _delegate("__int__") + + +def _register_loader(idx, size): + def decorator(func): + from .TiffTags import TYPES + + if func.__name__.startswith("load_"): + TYPES[idx] = func.__name__[5:].replace("_", " ") + _load_dispatch[idx] = size, func # noqa: F821 + return func + + return decorator + + +def _register_writer(idx): + def decorator(func): + _write_dispatch[idx] = func # noqa: F821 + return func + + return decorator + + +def _register_basic(idx_fmt_name): + from .TiffTags import TYPES + + idx, fmt, name = idx_fmt_name + TYPES[idx] = name + size = struct.calcsize(f"={fmt}") + _load_dispatch[idx] = ( # noqa: F821 + size, + lambda self, data, legacy_api=True: ( + self._unpack(f"{len(data) // size}{fmt}", data) + ), + ) + _write_dispatch[idx] = lambda self, *values: ( # noqa: F821 + b"".join(self._pack(fmt, value) for value in values) + ) + + +if TYPE_CHECKING: + _IFDv2Base = MutableMapping[int, Any] +else: + _IFDv2Base = MutableMapping + + +class ImageFileDirectory_v2(_IFDv2Base): + """This class represents a TIFF tag directory. To speed things up, we + don't decode tags unless they're asked for. + + Exposes a dictionary interface of the tags in the directory:: + + ifd = ImageFileDirectory_v2() + ifd[key] = 'Some Data' + ifd.tagtype[key] = TiffTags.ASCII + print(ifd[key]) + 'Some Data' + + Individual values are returned as the strings or numbers, sequences are + returned as tuples of the values. + + The tiff metadata type of each item is stored in a dictionary of + tag types in + :attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v2.tagtype`. The types + are read from a tiff file, guessed from the type added, or added + manually. + + Data Structures: + + * ``self.tagtype = {}`` + + * Key: numerical TIFF tag number + * Value: integer corresponding to the data type from + :py:data:`.TiffTags.TYPES` + + .. versionadded:: 3.0.0 + + 'Internal' data structures: + + * ``self._tags_v2 = {}`` + + * Key: numerical TIFF tag number + * Value: decoded data, as tuple for multiple values + + * ``self._tagdata = {}`` + + * Key: numerical TIFF tag number + * Value: undecoded byte string from file + + * ``self._tags_v1 = {}`` + + * Key: numerical TIFF tag number + * Value: decoded data in the v1 format + + Tags will be found in the private attributes ``self._tagdata``, and in + ``self._tags_v2`` once decoded. + + ``self.legacy_api`` is a value for internal use, and shouldn't be changed + from outside code. In cooperation with + :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1`, if ``legacy_api`` + is true, then decoded tags will be populated into both ``_tags_v1`` and + ``_tags_v2``. ``_tags_v2`` will be used if this IFD is used in the TIFF + save routine. Tags should be read from ``_tags_v1`` if + ``legacy_api == true``. + + """ + + _load_dispatch: dict[int, Callable[[ImageFileDirectory_v2, bytes, bool], Any]] = {} + _write_dispatch: dict[int, Callable[..., Any]] = {} + + def __init__(self, ifh=b"II\052\0\0\0\0\0", prefix=None, group=None): + """Initialize an ImageFileDirectory. + + To construct an ImageFileDirectory from a real file, pass the 8-byte + magic header to the constructor. To only set the endianness, pass it + as the 'prefix' keyword argument. + + :param ifh: One of the accepted magic headers (cf. PREFIXES); also sets + endianness. + :param prefix: Override the endianness of the file. + """ + if not _accept(ifh): + msg = f"not a TIFF file (header {repr(ifh)} not valid)" + raise SyntaxError(msg) + self._prefix = prefix if prefix is not None else ifh[:2] + if self._prefix == MM: + self._endian = ">" + elif self._prefix == II: + self._endian = "<" + else: + msg = "not a TIFF IFD" + raise SyntaxError(msg) + self._bigtiff = ifh[2] == 43 + self.group = group + self.tagtype = {} + """ Dictionary of tag types """ + self.reset() + (self.next,) = ( + self._unpack("Q", ifh[8:]) if self._bigtiff else self._unpack("L", ifh[4:]) + ) + self._legacy_api = False + + prefix = property(lambda self: self._prefix) + offset = property(lambda self: self._offset) + + @property + def legacy_api(self): + return self._legacy_api + + @legacy_api.setter + def legacy_api(self, value): + msg = "Not allowing setting of legacy api" + raise Exception(msg) + + def reset(self): + self._tags_v1 = {} # will remain empty if legacy_api is false + self._tags_v2 = {} # main tag storage + self._tagdata = {} + self.tagtype = {} # added 2008-06-05 by Florian Hoech + self._next = None + self._offset = None + + def __str__(self) -> str: + return str(dict(self)) + + def named(self): + """ + :returns: dict of name|key: value + + Returns the complete tag dictionary, with named tags where possible. + """ + return { + TiffTags.lookup(code, self.group).name: value + for code, value in self.items() + } + + def __len__(self) -> int: + return len(set(self._tagdata) | set(self._tags_v2)) + + def __getitem__(self, tag): + if tag not in self._tags_v2: # unpack on the fly + data = self._tagdata[tag] + typ = self.tagtype[tag] + size, handler = self._load_dispatch[typ] + self[tag] = handler(self, data, self.legacy_api) # check type + val = self._tags_v2[tag] + if self.legacy_api and not isinstance(val, (tuple, bytes)): + val = (val,) + return val + + def __contains__(self, tag): + return tag in self._tags_v2 or tag in self._tagdata + + def __setitem__(self, tag, value): + self._setitem(tag, value, self.legacy_api) + + def _setitem(self, tag, value, legacy_api): + basetypes = (Number, bytes, str) + + info = TiffTags.lookup(tag, self.group) + values = [value] if isinstance(value, basetypes) else value + + if tag not in self.tagtype: + if info.type: + self.tagtype[tag] = info.type + else: + self.tagtype[tag] = 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 + else: + self.tagtype[tag] = ( + TiffTags.LONG + if all(v >= 0 for v in values) + else TiffTags.SIGNED_LONG + ) + elif all(isinstance(v, float) for v in values): + self.tagtype[tag] = TiffTags.DOUBLE + elif all(isinstance(v, str) for v in values): + self.tagtype[tag] = TiffTags.ASCII + elif all(isinstance(v, bytes) for v in values): + self.tagtype[tag] = TiffTags.BYTE + + if self.tagtype[tag] == TiffTags.UNDEFINED: + values = [ + v.encode("ascii", "replace") if isinstance(v, str) else v + for v in values + ] + elif self.tagtype[tag] == TiffTags.RATIONAL: + values = [float(v) if isinstance(v, int) else v for v in values] + + is_ifd = self.tagtype[tag] == TiffTags.LONG and isinstance(values, dict) + if not is_ifd: + values = tuple(info.cvt_enum(value) for value in values) + + dest = self._tags_v1 if legacy_api else self._tags_v2 + + # Three branches: + # Spec'd length == 1, Actual length 1, store as element + # Spec'd length == 1, Actual > 1, Warn and truncate. Formerly barfed. + # No Spec, Actual length 1, Formerly (<4.2) returned a 1 element tuple. + # Don't mess with the legacy api, since it's frozen. + if not is_ifd and ( + (info.length == 1) + or self.tagtype[tag] == TiffTags.BYTE + or (info.length is None and len(values) == 1 and not legacy_api) + ): + # Don't mess with the legacy api, since it's frozen. + if legacy_api and self.tagtype[tag] in [ + TiffTags.RATIONAL, + TiffTags.SIGNED_RATIONAL, + ]: # rationals + values = (values,) + try: + (dest[tag],) = values + except ValueError: + # We've got a builtin tag with 1 expected entry + warnings.warn( + f"Metadata Warning, tag {tag} had too many entries: " + f"{len(values)}, expected 1" + ) + dest[tag] = values[0] + + else: + # Spec'd length > 1 or undefined + # Unspec'd, and length > 1 + dest[tag] = values + + def __delitem__(self, tag: int) -> None: + self._tags_v2.pop(tag, None) + self._tags_v1.pop(tag, None) + self._tagdata.pop(tag, None) + + def __iter__(self): + return iter(set(self._tagdata) | set(self._tags_v2)) + + def _unpack(self, fmt, data): + return struct.unpack(self._endian + fmt, data) + + def _pack(self, fmt, *values): + return struct.pack(self._endian + fmt, *values) + + list( + map( + _register_basic, + [ + (TiffTags.SHORT, "H", "short"), + (TiffTags.LONG, "L", "long"), + (TiffTags.SIGNED_BYTE, "b", "signed byte"), + (TiffTags.SIGNED_SHORT, "h", "signed short"), + (TiffTags.SIGNED_LONG, "l", "signed long"), + (TiffTags.FLOAT, "f", "float"), + (TiffTags.DOUBLE, "d", "double"), + (TiffTags.IFD, "L", "long"), + (TiffTags.LONG8, "Q", "long8"), + ], + ) + ) + + @_register_loader(1, 1) # Basic type, except for the legacy API. + def load_byte(self, data, legacy_api=True): + return data + + @_register_writer(1) # Basic type, except for the legacy API. + def write_byte(self, data): + if isinstance(data, IFDRational): + data = int(data) + if isinstance(data, int): + data = bytes((data,)) + return data + + @_register_loader(2, 1) + def load_string(self, data, legacy_api=True): + if data.endswith(b"\0"): + data = data[:-1] + return data.decode("latin-1", "replace") + + @_register_writer(2) + def write_string(self, value): + # remerge of https://github.com/python-pillow/Pillow/pull/1416 + if isinstance(value, int): + value = str(value) + if not isinstance(value, bytes): + value = value.encode("ascii", "replace") + return value + b"\0" + + @_register_loader(5, 8) + def load_rational(self, data, legacy_api=True): + vals = self._unpack(f"{len(data) // 4}L", data) + + def combine(a, b): + return (a, b) if legacy_api else IFDRational(a, b) + + return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2])) + + @_register_writer(5) + def write_rational(self, *values): + return b"".join( + self._pack("2L", *_limit_rational(frac, 2**32 - 1)) for frac in values + ) + + @_register_loader(7, 1) + def load_undefined(self, data, legacy_api=True): + return data + + @_register_writer(7) + def write_undefined(self, value): + if isinstance(value, IFDRational): + value = int(value) + if isinstance(value, int): + value = str(value).encode("ascii", "replace") + return value + + @_register_loader(10, 8) + def load_signed_rational(self, data, legacy_api=True): + vals = self._unpack(f"{len(data) // 4}l", data) + + def combine(a, b): + return (a, b) if legacy_api else IFDRational(a, b) + + return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2])) + + @_register_writer(10) + def write_signed_rational(self, *values): + return b"".join( + self._pack("2l", *_limit_signed_rational(frac, 2**31 - 1, -(2**31))) + for frac in values + ) + + def _ensure_read(self, fp, size): + ret = fp.read(size) + if len(ret) != size: + msg = ( + "Corrupt EXIF data. " + f"Expecting to read {size} bytes but only got {len(ret)}. " + ) + raise OSError(msg) + return ret + + def load(self, fp): + self.reset() + self._offset = fp.tell() + + try: + tag_count = ( + self._unpack("Q", self._ensure_read(fp, 8)) + if self._bigtiff + else self._unpack("H", self._ensure_read(fp, 2)) + )[0] + for i in range(tag_count): + tag, typ, count, data = ( + self._unpack("HHQ8s", self._ensure_read(fp, 20)) + if self._bigtiff + else self._unpack("HHL4s", self._ensure_read(fp, 12)) + ) + + tagname = TiffTags.lookup(tag, self.group).name + typname = TYPES.get(typ, "unknown") + msg = f"tag: {tagname} ({tag}) - type: {typname} ({typ})" + + try: + unit_size, handler = self._load_dispatch[typ] + except KeyError: + logger.debug("%s - unsupported type %s", msg, typ) + continue # ignore unsupported type + size = count * unit_size + if size > (8 if self._bigtiff else 4): + here = fp.tell() + (offset,) = self._unpack("Q" if self._bigtiff else "L", data) + msg += f" Tag Location: {here} - Data Location: {offset}" + fp.seek(offset) + data = ImageFile._safe_read(fp, size) + fp.seek(here) + else: + data = data[:size] + + if len(data) != size: + warnings.warn( + "Possibly corrupt EXIF data. " + f"Expecting to read {size} bytes but only got {len(data)}." + f" Skipping tag {tag}" + ) + logger.debug(msg) + continue + + if not data: + logger.debug(msg) + continue + + self._tagdata[tag] = data + self.tagtype[tag] = typ + + msg += " - value: " + ( + "" % size if size > 32 else repr(data) + ) + logger.debug(msg) + + (self.next,) = ( + self._unpack("Q", self._ensure_read(fp, 8)) + if self._bigtiff + else self._unpack("L", self._ensure_read(fp, 4)) + ) + except OSError as msg: + warnings.warn(str(msg)) + return + + def tobytes(self, offset=0): + # FIXME What about tagdata? + result = self._pack("H", len(self._tags_v2)) + + entries = [] + offset = offset + len(result) + len(self._tags_v2) * 12 + 4 + stripoffsets = None + + # pass 1: convert tags to binary format + # always write tags in ascending order + for tag, value in sorted(self._tags_v2.items()): + if tag == STRIPOFFSETS: + stripoffsets = len(entries) + typ = self.tagtype.get(tag) + logger.debug("Tag %s, Type: %s, Value: %s", tag, typ, repr(value)) + is_ifd = typ == TiffTags.LONG and isinstance(value, dict) + if is_ifd: + if self._endian == "<": + ifh = b"II\x2A\x00\x08\x00\x00\x00" + else: + ifh = b"MM\x00\x2A\x00\x00\x00\x08" + ifd = ImageFileDirectory_v2(ifh, group=tag) + values = self._tags_v2[tag] + for ifd_tag, ifd_value in values.items(): + ifd[ifd_tag] = ifd_value + data = ifd.tobytes(offset) + else: + values = value if isinstance(value, tuple) else (value,) + data = self._write_dispatch[typ](self, *values) + + tagname = TiffTags.lookup(tag, self.group).name + typname = "ifd" if is_ifd else TYPES.get(typ, "unknown") + msg = f"save: {tagname} ({tag}) - type: {typname} ({typ})" + msg += " - value: " + ( + "" % len(data) if len(data) >= 16 else str(values) + ) + logger.debug(msg) + + # count is sum of lengths for string and arbitrary data + if is_ifd: + count = 1 + elif typ in [TiffTags.BYTE, TiffTags.ASCII, TiffTags.UNDEFINED]: + count = len(data) + else: + count = len(values) + # figure out if data fits into the entry + if len(data) <= 4: + entries.append((tag, typ, count, data.ljust(4, b"\0"), b"")) + else: + entries.append((tag, typ, count, self._pack("L", offset), data)) + offset += (len(data) + 1) // 2 * 2 # pad to word + + # update strip offset data to point beyond auxiliary data + if stripoffsets is not None: + tag, typ, count, value, data = entries[stripoffsets] + if data: + msg = "multistrip support not yet implemented" + raise NotImplementedError(msg) + value = self._pack("L", self._unpack("L", value)[0] + offset) + 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) + + # -- overwrite here for multi-page -- + result += b"\0\0\0\0" # end of entries + + # pass 3: write auxiliary data to file + for tag, typ, count, value, data in entries: + result += data + if len(data) & 1: + result += b"\0" + + return result + + def save(self, fp): + if fp.tell() == 0: # skip TIFF header on subsequent pages + # tiff header -- PIL always starts the first IFD at offset 8 + fp.write(self._prefix + self._pack("HL", 42, 8)) + + offset = fp.tell() + result = self.tobytes(offset) + fp.write(result) + return offset + len(result) + + +ImageFileDirectory_v2._load_dispatch = _load_dispatch +ImageFileDirectory_v2._write_dispatch = _write_dispatch +for idx, name in TYPES.items(): + name = name.replace(" ", "_") + setattr(ImageFileDirectory_v2, f"load_{name}", _load_dispatch[idx][1]) + setattr(ImageFileDirectory_v2, f"write_{name}", _write_dispatch[idx]) +del _load_dispatch, _write_dispatch, idx, name + + +# Legacy ImageFileDirectory support. +class ImageFileDirectory_v1(ImageFileDirectory_v2): + """This class represents the **legacy** interface to a TIFF tag directory. + + Exposes a dictionary interface of the tags in the directory:: + + ifd = ImageFileDirectory_v1() + ifd[key] = 'Some Data' + ifd.tagtype[key] = TiffTags.ASCII + print(ifd[key]) + ('Some Data',) + + Also contains a dictionary of tag types as read from the tiff image file, + :attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v1.tagtype`. + + Values are returned as a tuple. + + .. deprecated:: 3.0.0 + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._legacy_api = True + + tags = property(lambda self: self._tags_v1) + tagdata = property(lambda self: self._tagdata) + + # defined in ImageFileDirectory_v2 + tagtype: dict[int, int] + """Dictionary of tag types""" + + @classmethod + def from_v2(cls, original): + """Returns an + :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` + instance with the same data as is contained in the original + :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` + instance. + + :returns: :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` + + """ + + ifd = cls(prefix=original.prefix) + ifd._tagdata = original._tagdata + ifd.tagtype = original.tagtype + ifd.next = original.next # an indicator for multipage tiffs + return ifd + + def to_v2(self) -> ImageFileDirectory_v2: + """Returns an + :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` + instance with the same data as is contained in the original + :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` + instance. + + :returns: :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` + + """ + + ifd = ImageFileDirectory_v2(prefix=self.prefix) + ifd._tagdata = dict(self._tagdata) + ifd.tagtype = dict(self.tagtype) + ifd._tags_v2 = dict(self._tags_v2) + return ifd + + def __contains__(self, tag): + return tag in self._tags_v1 or tag in self._tagdata + + def __len__(self) -> int: + return len(set(self._tagdata) | set(self._tags_v1)) + + def __iter__(self): + return iter(set(self._tagdata) | set(self._tags_v1)) + + def __setitem__(self, tag, value): + for legacy_api in (False, True): + self._setitem(tag, value, legacy_api) + + def __getitem__(self, tag): + if tag not in self._tags_v1: # unpack on the fly + data = self._tagdata[tag] + typ = self.tagtype[tag] + size, handler = self._load_dispatch[typ] + for legacy in (False, True): + self._setitem(tag, handler(self, data, legacy), legacy) + val = self._tags_v1[tag] + if not isinstance(val, (tuple, bytes)): + val = (val,) + return val + + +# undone -- switch this pointer when IFD_LEGACY_API == False +ImageFileDirectory = ImageFileDirectory_v1 + + +## +# Image plugin for TIFF files. + + +class TiffImageFile(ImageFile.ImageFile): + format = "TIFF" + format_description = "Adobe TIFF" + _close_exclusive_fp_after_loading = False + + def __init__(self, fp=None, filename=None): + self.tag_v2 = None + """ Image file directory (tag dictionary) """ + + self.tag = None + """ Legacy tag entries """ + + super().__init__(fp, filename) + + def _open(self) -> None: + """Open the first image in a TIFF file""" + + # Header + ifh = self.fp.read(8) + if ifh[2] == 43: + ifh += self.fp.read(8) + + self.tag_v2 = ImageFileDirectory_v2(ifh) + + # legacy IFD entries will be filled in later + self.ifd = None + + # setup frame pointers + self.__first = self.__next = self.tag_v2.next + self.__frame = -1 + self._fp = self.fp + self._frame_pos: list[int] = [] + self._n_frames: int | None = None + + logger.debug("*** TiffImageFile._open ***") + logger.debug("- __first: %s", self.__first) + logger.debug("- ifh: %s", repr(ifh)) # Use repr to avoid str(bytes) + + # and load the first frame + self._seek(0) + + @property + def n_frames(self): + if self._n_frames is None: + current = self.tell() + self._seek(len(self._frame_pos)) + while self._n_frames is None: + self._seek(self.tell() + 1) + self.seek(current) + return self._n_frames + + def seek(self, frame: int) -> None: + """Select a given frame as current image""" + if not self._seek_check(frame): + return + self._seek(frame) + # Create a new core image object on second and + # subsequent frames in the image. Image may be + # different size/mode. + Image._decompression_bomb_check(self.size) + self.im = Image.core.new(self.mode, self.size) + + def _seek(self, frame: int) -> None: + 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" + raise EOFError(msg) + logger.debug( + "Seeking to frame %s, on frame %s, __next %s, location: %s", + frame, + self.__frame, + self.__next, + self.fp.tell(), + ) + if self.__next >= 2**63: + msg = "Unable to seek to frame" + raise ValueError(msg) + self.fp.seek(self.__next) + self._frame_pos.append(self.__next) + logger.debug("Loading tags, location: %s", self.fp.tell()) + self.tag_v2.load(self.fp) + if self.tag_v2.next in self._frame_pos: + # This IFD has already been processed + # Declare this to be the end of the image + self.__next = 0 + else: + self.__next = self.tag_v2.next + if self.__next == 0: + self._n_frames = frame + 1 + if len(self._frame_pos) == 1: + self.is_animated = self.__next != 0 + self.__frame += 1 + self.fp.seek(self._frame_pos[frame]) + self.tag_v2.load(self.fp) + self._reload_exif() + # fill the legacy tag/ifd entries + self.tag = self.ifd = ImageFileDirectory_v1.from_v2(self.tag_v2) + self.__frame = frame + self._setup() + + def tell(self) -> int: + """Return the current frame number""" + return self.__frame + + def getxmp(self) -> dict[str, Any]: + """ + Returns a dictionary containing the XMP tags. + Requires defusedxml to be installed. + + :returns: XMP tags in a dictionary. + """ + return self._getxmp(self.tag_v2[XMP]) if XMP in self.tag_v2 else {} + + def get_photoshop_blocks(self): + """ + Returns a dictionary of Photoshop "Image Resource Blocks". + The keys are the image resource ID. For more information, see + https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_pgfId-1037727 + + :returns: Photoshop "Image Resource Blocks" in a dictionary. + """ + blocks = {} + val = self.tag_v2.get(ExifTags.Base.ImageResources) + if val: + while val[:4] == b"8BIM": + id = i16(val[4:6]) + n = math.ceil((val[6] + 1) / 2) * 2 + size = i32(val[6 + n : 10 + n]) + data = val[10 + n : 10 + n + size] + blocks[id] = {"data": data} + + val = val[math.ceil((10 + n + size) / 2) * 2 :] + return blocks + + def load(self): + if self.tile and self.use_load_libtiff: + return self._load_libtiff() + return super().load() + + def load_end(self) -> None: + # allow closing if we're on the first frame, there's no next + # This is the ImageFile.load path only, libtiff specific below. + if not self.is_animated: + self._close_exclusive_fp_after_loading = True + + # reset buffered io handle in case fp + # was passed to libtiff, invalidating the buffer + self.fp.tell() + + # load IFD data from fp before it is closed + exif = self.getexif() + for key in TiffTags.TAGS_V2_GROUPS: + if key not in exif: + continue + exif.get_ifd(key) + + ImageOps.exif_transpose(self, in_place=True) + if ExifTags.Base.Orientation in self.tag_v2: + del self.tag_v2[ExifTags.Base.Orientation] + + def _load_libtiff(self): + """Overload method triggered when we detect a compressed tiff + Calls out to libtiff""" + + Image.Image.load(self) + + self.load_prepare() + + if not len(self.tile) == 1: + msg = "Not exactly one tile" + raise OSError(msg) + + # (self._compression, (extents tuple), + # 0, (rawmode, self._compression, fp)) + extents = self.tile[0][1] + args = list(self.tile[0][3]) + + # To be nice on memory footprint, if there's a + # file descriptor, use that instead of reading + # into a string in python. + try: + fp = hasattr(self.fp, "fileno") and self.fp.fileno() + # flush the file descriptor, prevents error on pypy 2.4+ + # should also eliminate the need for fp.tell + # in _seek + if hasattr(self.fp, "flush"): + self.fp.flush() + except OSError: + # io.BytesIO have a fileno, but returns an OSError if + # it doesn't use a file descriptor. + fp = False + + if fp: + args[2] = fp + + decoder = Image._getdecoder( + self.mode, "libtiff", tuple(args), self.decoderconfig + ) + try: + decoder.setimage(self.im, extents) + except ValueError as e: + msg = "Couldn't set the image" + raise OSError(msg) from e + + close_self_fp = self._exclusive_fp and not self.is_animated + if hasattr(self.fp, "getvalue"): + # We've got a stringio like thing passed in. Yay for all in memory. + # The decoder needs the entire file in one shot, so there's not + # a lot we can do here other than give it the entire file. + # unless we could do something like get the address of the + # underlying string for stringio. + # + # Rearranging for supporting byteio items, since they have a fileno + # that returns an OSError if there's no underlying fp. Easier to + # deal with here by reordering. + logger.debug("have getvalue. just sending in a string from getvalue") + n, err = decoder.decode(self.fp.getvalue()) + elif fp: + # we've got a actual file on disk, pass in the fp. + logger.debug("have fileno, calling fileno version of the decoder.") + if not close_self_fp: + self.fp.seek(0) + # 4 bytes, otherwise the trace might error out + n, err = decoder.decode(b"fpfp") + else: + # we have something else. + logger.debug("don't have fileno or getvalue. just reading") + self.fp.seek(0) + # UNDONE -- so much for that buffer size thing. + n, err = decoder.decode(self.fp.read()) + + self.tile = [] + self.readonly = 0 + + self.load_end() + + if close_self_fp: + self.fp.close() + self.fp = None # might be shared + + if err < 0: + raise OSError(err) + + return Image.Image.load(self) + + def _setup(self): + """Setup this image object based on current tags""" + + if 0xBC01 in self.tag_v2: + msg = "Windows Media Photo files not yet supported" + raise OSError(msg) + + # extract relevant tags + self._compression = COMPRESSION_INFO[self.tag_v2.get(COMPRESSION, 1)] + self._planar_configuration = self.tag_v2.get(PLANAR_CONFIGURATION, 1) + + # photometric is a required tag, but not everyone is reading + # the specification + photo = self.tag_v2.get(PHOTOMETRIC_INTERPRETATION, 0) + + # old style jpeg compression images most certainly are YCbCr + if self._compression == "tiff_jpeg": + photo = 6 + + fillorder = self.tag_v2.get(FILLORDER, 1) + + logger.debug("*** Summary ***") + logger.debug("- compression: %s", self._compression) + logger.debug("- photometric_interpretation: %s", photo) + logger.debug("- planar_configuration: %s", self._planar_configuration) + logger.debug("- fill_order: %s", fillorder) + logger.debug("- YCbCr subsampling: %s", self.tag.get(YCBCRSUBSAMPLING)) + + # size + xsize = int(self.tag_v2.get(IMAGEWIDTH)) + ysize = int(self.tag_v2.get(IMAGELENGTH)) + self._size = xsize, ysize + + logger.debug("- size: %s", self.size) + + sample_format = self.tag_v2.get(SAMPLEFORMAT, (1,)) + if len(sample_format) > 1 and max(sample_format) == min(sample_format) == 1: + # SAMPLEFORMAT is properly per band, so an RGB image will + # be (1,1,1). But, we don't support per band pixel types, + # and anything more than one band is a uint8. So, just + # take the first element. Revisit this if adding support + # for more exotic images. + sample_format = (1,) + + bps_tuple = self.tag_v2.get(BITSPERSAMPLE, (1,)) + extra_tuple = self.tag_v2.get(EXTRASAMPLES, ()) + if photo in (2, 6, 8): # RGB, YCbCr, LAB + bps_count = 3 + elif photo == 5: # CMYK + bps_count = 4 + else: + bps_count = 1 + bps_count += len(extra_tuple) + bps_actual_count = len(bps_tuple) + samples_per_pixel = self.tag_v2.get( + SAMPLESPERPIXEL, + 3 if self._compression == "tiff_jpeg" and photo in (2, 6) else 1, + ) + + if samples_per_pixel > MAX_SAMPLESPERPIXEL: + # DOS check, samples_per_pixel can be a Long, and we extend the tuple below + logger.error( + "More samples per pixel than can be decoded: %s", samples_per_pixel + ) + msg = "Invalid value for samples per pixel" + raise SyntaxError(msg) + + if samples_per_pixel < bps_actual_count: + # If a file has more values in bps_tuple than expected, + # remove the excess. + bps_tuple = bps_tuple[:samples_per_pixel] + elif samples_per_pixel > bps_actual_count and bps_actual_count == 1: + # If a file has only one value in bps_tuple, when it should have more, + # presume it is the same number of bits for all of the samples. + bps_tuple = bps_tuple * samples_per_pixel + + if len(bps_tuple) != samples_per_pixel: + msg = "unknown data organization" + raise SyntaxError(msg) + + # mode: check photometric interpretation and bits per pixel + key = ( + self.tag_v2.prefix, + photo, + sample_format, + fillorder, + bps_tuple, + extra_tuple, + ) + logger.debug("format key: %s", key) + try: + self._mode, rawmode = OPEN_INFO[key] + except KeyError as e: + logger.debug("- unsupported format") + msg = "unknown pixel mode" + raise SyntaxError(msg) from e + + logger.debug("- raw mode: %s", rawmode) + logger.debug("- pil mode: %s", self.mode) + + self.info["compression"] = self._compression + + xres = self.tag_v2.get(X_RESOLUTION, 1) + yres = self.tag_v2.get(Y_RESOLUTION, 1) + + if xres and yres: + resunit = self.tag_v2.get(RESOLUTION_UNIT) + if resunit == 2: # dots per inch + self.info["dpi"] = (xres, yres) + elif resunit == 3: # dots per centimeter. convert to dpi + self.info["dpi"] = (xres * 2.54, yres * 2.54) + elif resunit is None: # used to default to 1, but now 2) + self.info["dpi"] = (xres, yres) + # For backward compatibility, + # we also preserve the old behavior + self.info["resolution"] = xres, yres + else: # No absolute unit of measurement + self.info["resolution"] = xres, yres + + # build tile descriptors + x = y = layer = 0 + self.tile = [] + self.use_load_libtiff = READ_LIBTIFF or self._compression != "raw" + if self.use_load_libtiff: + # Decoder expects entire file as one tile. + # There's a buffer size limit in load (64k) + # so large g4 images will fail if we use that + # function. + # + # Setup the one tile for the whole image, then + # use the _load_libtiff function. + + # libtiff handles the fillmode for us, so 1;IR should + # actually be 1;I. Including the R double reverses the + # bits, so stripes of the image are reversed. See + # https://github.com/python-pillow/Pillow/issues/279 + if fillorder == 2: + # Replace fillorder with fillorder=1 + key = key[:3] + (1,) + key[4:] + logger.debug("format key: %s", key) + # this should always work, since all the + # fillorder==2 modes have a corresponding + # fillorder=1 mode + self._mode, rawmode = OPEN_INFO[key] + # libtiff always returns the bytes in native order. + # we're expecting image byte order. So, if the rawmode + # contains I;16, we need to convert from native to image + # byte order. + if rawmode == "I;16": + rawmode = "I;16N" + if ";16B" in rawmode: + rawmode = rawmode.replace(";16B", ";16N") + if ";16L" in rawmode: + rawmode = rawmode.replace(";16L", ";16N") + + # YCbCr images with new jpeg compression with pixels in one plane + # unpacked straight into RGB values + if ( + photo == 6 + and self._compression == "jpeg" + and self._planar_configuration == 1 + ): + rawmode = "RGB" + + # Offset in the tile tuple is 0, we go from 0,0 to + # w,h, and we only do this once -- eds + a = (rawmode, self._compression, False, self.tag_v2.offset) + self.tile.append(("libtiff", (0, 0, xsize, ysize), 0, a)) + + elif STRIPOFFSETS in self.tag_v2 or TILEOFFSETS in self.tag_v2: + # striped image + if STRIPOFFSETS in self.tag_v2: + offsets = self.tag_v2[STRIPOFFSETS] + h = self.tag_v2.get(ROWSPERSTRIP, ysize) + w = self.size[0] + else: + # tiled image + offsets = self.tag_v2[TILEOFFSETS] + w = self.tag_v2.get(TILEWIDTH) + h = self.tag_v2.get(TILELENGTH) + + for offset in offsets: + if x + w > xsize: + stride = w * sum(bps_tuple) / 8 # bytes per line + else: + stride = 0 + + tile_rawmode = rawmode + if self._planar_configuration == 2: + # each band on it's own layer + tile_rawmode = rawmode[layer] + # adjust stride width accordingly + stride /= bps_count + + a = (tile_rawmode, int(stride), 1) + self.tile.append( + ( + self._compression, + (x, y, min(x + w, xsize), min(y + h, ysize)), + offset, + a, + ) + ) + x = x + w + if x >= self.size[0]: + x, y = 0, y + h + if y >= self.size[1]: + x = y = 0 + layer += 1 + else: + logger.debug("- unsupported data organization") + msg = "unknown data organization" + raise SyntaxError(msg) + + # Fix up info. + if ICCPROFILE in self.tag_v2: + self.info["icc_profile"] = self.tag_v2[ICCPROFILE] + + # fixup palette descriptor + + if self.mode in ["P", "PA"]: + palette = [o8(b // 256) for b in self.tag_v2[COLORMAP]] + self.palette = ImagePalette.raw("RGB;L", b"".join(palette)) + + +# +# -------------------------------------------------------------------- +# Write TIFF files + +# little endian is default except for image modes with +# explicit big endian byte-order + +SAVE_INFO = { + # mode => rawmode, byteorder, photometrics, + # sampleformat, bitspersample, extra + "1": ("1", II, 1, 1, (1,), None), + "L": ("L", II, 1, 1, (8,), None), + "LA": ("LA", II, 1, 1, (8, 8), 2), + "P": ("P", II, 3, 1, (8,), None), + "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), + "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), + "RGBA": ("RGBA", II, 2, 1, (8, 8, 8, 8), 2), + "CMYK": ("CMYK", II, 5, 1, (8, 8, 8, 8), None), + "YCbCr": ("YCbCr", II, 6, 1, (8, 8, 8), None), + "LAB": ("LAB", II, 8, 1, (8, 8, 8), None), + "I;32BS": ("I;32BS", MM, 1, 2, (32,), None), + "I;16B": ("I;16B", MM, 1, 1, (16,), None), + "I;16BS": ("I;16BS", MM, 1, 2, (16,), None), + "F;32BF": ("F;32BF", MM, 1, 3, (32,), None), +} + + +def _save(im, fp, filename): + try: + rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode] + except KeyError as e: + msg = f"cannot write mode {im.mode} as TIFF" + raise OSError(msg) from e + + ifd = ImageFileDirectory_v2(prefix=prefix) + + encoderinfo = im.encoderinfo + encoderconfig = im.encoderconfig + try: + compression = encoderinfo["compression"] + except KeyError: + compression = im.info.get("compression") + if isinstance(compression, int): + # compression value may be from BMP. Ignore it + compression = None + if compression is None: + compression = "raw" + elif compression == "tiff_jpeg": + # OJPEG is obsolete, so use new-style JPEG compression instead + compression = "jpeg" + elif compression == "tiff_deflate": + compression = "tiff_adobe_deflate" + + libtiff = WRITE_LIBTIFF or compression != "raw" + + # required for color libtiff images + ifd[PLANAR_CONFIGURATION] = 1 + + ifd[IMAGEWIDTH] = im.size[0] + ifd[IMAGELENGTH] = im.size[1] + + # write any arbitrary tags passed in as an ImageFileDirectory + if "tiffinfo" in encoderinfo: + info = encoderinfo["tiffinfo"] + elif "exif" in encoderinfo: + info = encoderinfo["exif"] + if isinstance(info, bytes): + exif = Image.Exif() + exif.load(info) + info = exif + else: + info = {} + logger.debug("Tiffinfo Keys: %s", list(info)) + if isinstance(info, ImageFileDirectory_v1): + info = info.to_v2() + for key in info: + if isinstance(info, Image.Exif) and key in TiffTags.TAGS_V2_GROUPS: + ifd[key] = info.get_ifd(key) + else: + ifd[key] = info.get(key) + try: + ifd.tagtype[key] = info.tagtype[key] + except Exception: + pass # might not be an IFD. Might not have populated type + + # additions written by Greg Couch, gregc@cgl.ucsf.edu + # inspired by image-sig posting from Kevin Cazabon, kcazabon@home.com + if hasattr(im, "tag_v2"): + # preserve tags from original TIFF image file + for key in ( + RESOLUTION_UNIT, + X_RESOLUTION, + Y_RESOLUTION, + IPTC_NAA_CHUNK, + PHOTOSHOP_CHUNK, + XMP, + ): + if key in im.tag_v2: + ifd[key] = im.tag_v2[key] + ifd.tagtype[key] = im.tag_v2.tagtype[key] + + # preserve ICC profile (should also work when saving other formats + # which support profiles as TIFF) -- 2008-06-06 Florian Hoech + icc = encoderinfo.get("icc_profile", im.info.get("icc_profile")) + if icc: + ifd[ICCPROFILE] = icc + + for key, name in [ + (IMAGEDESCRIPTION, "description"), + (X_RESOLUTION, "resolution"), + (Y_RESOLUTION, "resolution"), + (X_RESOLUTION, "x_resolution"), + (Y_RESOLUTION, "y_resolution"), + (RESOLUTION_UNIT, "resolution_unit"), + (SOFTWARE, "software"), + (DATE_TIME, "date_time"), + (ARTIST, "artist"), + (COPYRIGHT, "copyright"), + ]: + if name in encoderinfo: + ifd[key] = encoderinfo[name] + + dpi = encoderinfo.get("dpi") + if dpi: + ifd[RESOLUTION_UNIT] = 2 + ifd[X_RESOLUTION] = dpi[0] + ifd[Y_RESOLUTION] = dpi[1] + + if bits != (1,): + ifd[BITSPERSAMPLE] = bits + if len(bits) != 1: + ifd[SAMPLESPERPIXEL] = len(bits) + if extra is not None: + ifd[EXTRASAMPLES] = extra + if format != 1: + ifd[SAMPLEFORMAT] = format + + if PHOTOMETRIC_INTERPRETATION not in ifd: + ifd[PHOTOMETRIC_INTERPRETATION] = photo + elif im.mode in ("1", "L") and ifd[PHOTOMETRIC_INTERPRETATION] == 0: + if im.mode == "1": + inverted_im = im.copy() + px = inverted_im.load() + for y in range(inverted_im.height): + for x in range(inverted_im.width): + px[x, y] = 0 if px[x, y] == 255 else 255 + im = inverted_im + else: + im = ImageOps.invert(im) + + if im.mode in ["P", "PA"]: + lut = im.im.getpalette("RGB", "RGB;L") + colormap = [] + colors = len(lut) // 3 + for i in range(3): + colormap += [v * 256 for v in lut[colors * i : colors * (i + 1)]] + colormap += [0] * (256 - colors) + ifd[COLORMAP] = colormap + # data orientation + w, h = ifd[IMAGEWIDTH], ifd[IMAGELENGTH] + stride = len(bits) * ((w * bits[0] + 7) // 8) + if ROWSPERSTRIP not in ifd: + # aim for given strip size (64 KB by default) when using libtiff writer + if libtiff: + im_strip_size = encoderinfo.get("strip_size", STRIP_SIZE) + rows_per_strip = 1 if stride == 0 else min(im_strip_size // stride, h) + # JPEG encoder expects multiple of 8 rows + if compression == "jpeg": + rows_per_strip = min(((rows_per_strip + 7) // 8) * 8, h) + else: + rows_per_strip = h + if rows_per_strip == 0: + rows_per_strip = 1 + ifd[ROWSPERSTRIP] = rows_per_strip + strip_byte_counts = 1 if stride == 0 else stride * ifd[ROWSPERSTRIP] + strips_per_image = (h + ifd[ROWSPERSTRIP] - 1) // ifd[ROWSPERSTRIP] + if strip_byte_counts >= 2**16: + ifd.tagtype[STRIPBYTECOUNTS] = TiffTags.LONG + ifd[STRIPBYTECOUNTS] = (strip_byte_counts,) * (strips_per_image - 1) + ( + stride * h - strip_byte_counts * (strips_per_image - 1), + ) + ifd[STRIPOFFSETS] = tuple( + range(0, strip_byte_counts * strips_per_image, strip_byte_counts) + ) # this is adjusted by IFD writer + # no compression by default: + ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1) + + if im.mode == "YCbCr": + for tag, value in { + YCBCRSUBSAMPLING: (1, 1), + REFERENCEBLACKWHITE: (0, 255, 128, 255, 128, 255), + }.items(): + ifd.setdefault(tag, value) + + blocklist = [TILEWIDTH, TILELENGTH, TILEOFFSETS, TILEBYTECOUNTS] + if libtiff: + if "quality" in encoderinfo: + quality = encoderinfo["quality"] + if not isinstance(quality, int) or quality < 0 or quality > 100: + msg = "Invalid quality setting" + raise ValueError(msg) + if compression != "jpeg": + msg = "quality setting only supported for 'jpeg' compression" + raise ValueError(msg) + ifd[JPEGQUALITY] = quality + + logger.debug("Saving using libtiff encoder") + logger.debug("Items: %s", sorted(ifd.items())) + _fp = 0 + if hasattr(fp, "fileno"): + try: + fp.seek(0) + _fp = os.dup(fp.fileno()) + except io.UnsupportedOperation: + pass + + # optional types for non core tags + types = {} + # STRIPOFFSETS and STRIPBYTECOUNTS are added by the library + # based on the data in the strip. + # OSUBFILETYPE is deprecated. + # The other tags expect arrays with a certain length (fixed or depending on + # BITSPERSAMPLE, etc), passing arrays with a different length will result in + # segfaults. Block these tags until we add extra validation. + # SUBIFD may also cause a segfault. + blocklist += [ + OSUBFILETYPE, + REFERENCEBLACKWHITE, + STRIPBYTECOUNTS, + STRIPOFFSETS, + TRANSFERFUNCTION, + SUBIFD, + ] + + # bits per sample is a single short in the tiff directory, not a list. + atts = {BITSPERSAMPLE: bits[0]} + # Merge the ones that we have with (optional) more bits from + # the original file, e.g x,y resolution so that we can + # save(load('')) == original file. + legacy_ifd = {} + if hasattr(im, "tag"): + legacy_ifd = im.tag.to_v2() + + # SAMPLEFORMAT is determined by the image format and should not be copied + # from legacy_ifd. + supplied_tags = {**getattr(im, "tag_v2", {}), **legacy_ifd} + if SAMPLEFORMAT in supplied_tags: + del supplied_tags[SAMPLEFORMAT] + + for tag, value in itertools.chain(ifd.items(), supplied_tags.items()): + # Libtiff can only process certain core items without adding + # them to the custom dictionary. + # Custom items are supported for int, float, unicode, string and byte + # values. Other types and tuples require a tagtype. + if tag not in TiffTags.LIBTIFF_CORE: + if not getattr(Image.core, "libtiff_support_custom_tags", False): + continue + + if tag in ifd.tagtype: + types[tag] = ifd.tagtype[tag] + elif not (isinstance(value, (int, float, str, bytes))): + continue + else: + type = TiffTags.lookup(tag).type + if type: + types[tag] = type + if tag not in atts and tag not in blocklist: + if isinstance(value, str): + atts[tag] = value.encode("ascii", "replace") + b"\0" + elif isinstance(value, IFDRational): + atts[tag] = float(value) + else: + atts[tag] = value + + if SAMPLEFORMAT in atts and len(atts[SAMPLEFORMAT]) == 1: + atts[SAMPLEFORMAT] = atts[SAMPLEFORMAT][0] + + logger.debug("Converted items: %s", sorted(atts.items())) + + # libtiff always expects the bytes in native order. + # we're storing image byte order. So, if the rawmode + # contains I;16, we need to convert from native to image + # byte order. + if im.mode in ("I;16B", "I;16"): + rawmode = "I;16N" + + # Pass tags as sorted list so that the tags are set in a fixed order. + # This is required by libtiff for some tags. For example, the JPEGQUALITY + # pseudo tag requires that the COMPRESS tag was already set. + tags = list(atts.items()) + tags.sort() + a = (rawmode, compression, _fp, filename, tags, types) + encoder = Image._getencoder(im.mode, "libtiff", a, encoderconfig) + encoder.setimage(im.im, (0, 0) + im.size) + while True: + # undone, change to self.decodermaxblock: + errcode, data = encoder.encode(16 * 1024)[1:] + if not _fp: + fp.write(data) + if errcode: + break + if _fp: + try: + os.close(_fp) + except OSError: + pass + if errcode < 0: + msg = f"encoder error {errcode} when writing image file" + raise OSError(msg) + + else: + for tag in blocklist: + del ifd[tag] + offset = ifd.save(fp) + + ImageFile._save( + im, fp, [("raw", (0, 0) + im.size, offset, (rawmode, stride, 1))] + ) + + # -- helper for multi-page save -- + if "_debug_multipage" in encoderinfo: + # just to access o32 and o16 (using correct byte order) + im._debug_multipage = ifd + + +class AppendingTiffWriter: + fieldSizes = [ + 0, # None + 1, # byte + 1, # ascii + 2, # short + 4, # long + 8, # rational + 1, # sbyte + 1, # undefined + 2, # sshort + 4, # slong + 8, # srational + 4, # float + 8, # double + 4, # ifd + 2, # unicode + 4, # complex + 8, # long8 + ] + + Tags = { + 273, # StripOffsets + 288, # FreeOffsets + 324, # TileOffsets + 519, # JPEGQTables + 520, # JPEGDCTables + 521, # JPEGACTables + } + + def __init__(self, fn, new=False): + if hasattr(fn, "read"): + self.f = fn + self.close_fp = False + else: + self.name = fn + self.close_fp = True + try: + self.f = open(fn, "w+b" if new else "r+b") + except OSError: + self.f = open(fn, "w+b") + self.beginning = self.f.tell() + self.setup() + + def setup(self) -> None: + # Reset everything. + self.f.seek(self.beginning, os.SEEK_SET) + + self.whereToWriteNewIFDOffset = None + self.offsetOfNewPage = 0 + + self.IIMM = iimm = self.f.read(4) + if not iimm: + # empty file - first page + self.isFirst = True + return + + self.isFirst = False + if iimm == b"II\x2a\x00": + self.setEndian("<") + elif iimm == b"MM\x00\x2a": + self.setEndian(">") + else: + msg = "Invalid TIFF file header" + raise RuntimeError(msg) + + self.skipIFDs() + self.goToEnd() + + def finalize(self) -> None: + if self.isFirst: + return + + # fix offsets + self.f.seek(self.offsetOfNewPage) + + iimm = self.f.read(4) + if not iimm: + # Make it easy to finish a frame without committing to a new one. + return + + if iimm != self.IIMM: + msg = "IIMM of new page doesn't match IIMM of first page" + raise RuntimeError(msg) + + ifd_offset = self.readLong() + ifd_offset += self.offsetOfNewPage + self.f.seek(self.whereToWriteNewIFDOffset) + self.writeLong(ifd_offset) + self.f.seek(ifd_offset) + self.fixIFD() + + def newFrame(self) -> None: + # Call this to finish a frame. + self.finalize() + self.setup() + + def __enter__(self) -> AppendingTiffWriter: + return self + + def __exit__(self, *args: object) -> None: + if self.close_fp: + self.close() + + def tell(self) -> int: + return self.f.tell() - self.offsetOfNewPage + + def seek(self, offset, whence=io.SEEK_SET): + if whence == os.SEEK_SET: + offset += self.offsetOfNewPage + + self.f.seek(offset, whence) + return self.tell() + + def goToEnd(self) -> None: + self.f.seek(0, os.SEEK_END) + pos = self.f.tell() + + # pad to 16 byte boundary + pad_bytes = 16 - pos % 16 + if 0 < pad_bytes < 16: + self.f.write(bytes(pad_bytes)) + self.offsetOfNewPage = self.f.tell() + + def setEndian(self, endian: str) -> None: + self.endian = endian + self.longFmt = f"{self.endian}L" + self.shortFmt = f"{self.endian}H" + self.tagFormat = f"{self.endian}HHL" + + def skipIFDs(self) -> None: + while True: + ifd_offset = self.readLong() + if ifd_offset == 0: + self.whereToWriteNewIFDOffset = self.f.tell() - 4 + break + + self.f.seek(ifd_offset) + num_tags = self.readShort() + self.f.seek(num_tags * 12, os.SEEK_CUR) + + def write(self, data): + return self.f.write(data) + + def readShort(self) -> int: + (value,) = struct.unpack(self.shortFmt, self.f.read(2)) + return value + + def readLong(self) -> int: + (value,) = struct.unpack(self.longFmt, self.f.read(4)) + return value + + def rewriteLastShortToLong(self, value: int) -> None: + self.f.seek(-2, os.SEEK_CUR) + bytes_written = self.f.write(struct.pack(self.longFmt, value)) + if bytes_written is not None and bytes_written != 4: + msg = f"wrote only {bytes_written} bytes but wanted 4" + raise RuntimeError(msg) + + def rewriteLastShort(self, value: int) -> None: + self.f.seek(-2, os.SEEK_CUR) + bytes_written = self.f.write(struct.pack(self.shortFmt, value)) + if bytes_written is not None and bytes_written != 2: + msg = f"wrote only {bytes_written} bytes but wanted 2" + raise RuntimeError(msg) + + def rewriteLastLong(self, value: int) -> None: + self.f.seek(-4, os.SEEK_CUR) + bytes_written = self.f.write(struct.pack(self.longFmt, value)) + if bytes_written is not None and bytes_written != 4: + msg = f"wrote only {bytes_written} bytes but wanted 4" + raise RuntimeError(msg) + + def writeShort(self, value: int) -> None: + bytes_written = self.f.write(struct.pack(self.shortFmt, value)) + if bytes_written is not None and bytes_written != 2: + msg = f"wrote only {bytes_written} bytes but wanted 2" + raise RuntimeError(msg) + + def writeLong(self, value: int) -> None: + bytes_written = self.f.write(struct.pack(self.longFmt, value)) + if bytes_written is not None and bytes_written != 4: + msg = f"wrote only {bytes_written} bytes but wanted 4" + raise RuntimeError(msg) + + def close(self) -> None: + self.finalize() + self.f.close() + + def fixIFD(self) -> None: + num_tags = self.readShort() + + for i in range(num_tags): + tag, field_type, count = struct.unpack(self.tagFormat, self.f.read(8)) + + field_size = self.fieldSizes[field_type] + total_size = field_size * count + is_local = total_size <= 4 + offset: int | None + if not is_local: + offset = self.readLong() + self.offsetOfNewPage + self.rewriteLastLong(offset) + + if tag in self.Tags: + cur_pos = self.f.tell() + + if is_local: + self.fixOffsets( + count, isShort=(field_size == 2), isLong=(field_size == 4) + ) + self.f.seek(cur_pos + 4) + else: + self.f.seek(offset) + self.fixOffsets( + count, isShort=(field_size == 2), isLong=(field_size == 4) + ) + self.f.seek(cur_pos) + + offset = cur_pos = None + + elif is_local: + # skip the locally stored value that is not an offset + self.f.seek(4, os.SEEK_CUR) + + def fixOffsets(self, count, isShort=False, isLong=False): + if not isShort and not isLong: + msg = "offset is neither short nor long" + raise RuntimeError(msg) + + for i in range(count): + offset = self.readShort() if isShort else self.readLong() + offset += self.offsetOfNewPage + if isShort and offset >= 65536: + # offset is now too large - we must convert shorts to longs + if count != 1: + msg = "not implemented" + raise RuntimeError(msg) # XXX TODO + + # simple case - the offset is just one and therefore it is + # local (not referenced with another offset) + self.rewriteLastShortToLong(offset) + self.f.seek(-10, os.SEEK_CUR) + self.writeShort(TiffTags.LONG) # rewrite the type to LONG + self.f.seek(8, os.SEEK_CUR) + elif isShort: + self.rewriteLastShort(offset) + else: + self.rewriteLastLong(offset) + + +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", [])) + if not hasattr(im, "n_frames") and not append_images: + return _save(im, fp, filename) + + cur_idx = im.tell() + 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 + + for idx in range(nfr): + ims.seek(idx) + ims.load() + _save(ims, tf, filename) + tf.newFrame() + finally: + im.seek(cur_idx) + + +# +# -------------------------------------------------------------------- +# Register + +Image.register_open(TiffImageFile.format, TiffImageFile, _accept) +Image.register_save(TiffImageFile.format, _save) +Image.register_save_all(TiffImageFile.format, _save_all) + +Image.register_extensions(TiffImageFile.format, [".tif", ".tiff"]) + +Image.register_mime(TiffImageFile.format, "image/tiff") diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py index 89fad7033..d8217d793 100644 --- a/src/PIL/TiffTags.py +++ b/src/PIL/TiffTags.py @@ -1,553 +1,553 @@ -# -# The Python Imaging Library. -# $Id$ -# -# TIFF tags -# -# This module provides clear-text names for various well-known -# TIFF tags. the TIFF codec works just fine without it. -# -# Copyright (c) Secret Labs AB 1999. -# -# See the README file for information on usage and redistribution. -# - -## -# This module provides constants and clear-text names for various -# well-known TIFF tags. -## -from __future__ import annotations - -from typing import NamedTuple - - -class _TagInfo(NamedTuple): - value: int | None - name: str - type: int | None - length: int | None - enum: dict[str, int] - - -class TagInfo(_TagInfo): - __slots__: list[str] = [] - - def __new__(cls, value=None, name="unknown", type=None, length=None, enum=None): - return super().__new__(cls, value, name, type, length, enum or {}) - - def cvt_enum(self, value): - # Using get will call hash(value), which can be expensive - # for some types (e.g. Fraction). Since self.enum is rarely - # used, it's usually better to test it first. - return self.enum.get(value, value) if self.enum else value - - -def lookup(tag, group=None): - """ - :param tag: Integer tag number - :param group: Which :py:data:`~PIL.TiffTags.TAGS_V2_GROUPS` to look in - - .. versionadded:: 8.3.0 - - :returns: Taginfo namedtuple, From the ``TAGS_V2`` info if possible, - otherwise just populating the value and name from ``TAGS``. - If the tag is not recognized, "unknown" is returned for the name - - """ - - if group is not None: - info = TAGS_V2_GROUPS[group].get(tag) if group in TAGS_V2_GROUPS else None - else: - info = TAGS_V2.get(tag) - return info or TagInfo(tag, TAGS.get(tag, "unknown")) - - -## -# Map tag numbers to tag info. -# -# id: (Name, Type, Length[, enum_values]) -# -# The length here differs from the length in the tiff spec. For -# numbers, the tiff spec is for the number of fields returned. We -# agree here. For string-like types, the tiff spec uses the length of -# field in bytes. In Pillow, we are using the number of expected -# fields, in general 1 for string-like types. - - -BYTE = 1 -ASCII = 2 -SHORT = 3 -LONG = 4 -RATIONAL = 5 -SIGNED_BYTE = 6 -UNDEFINED = 7 -SIGNED_SHORT = 8 -SIGNED_LONG = 9 -SIGNED_RATIONAL = 10 -FLOAT = 11 -DOUBLE = 12 -IFD = 13 -LONG8 = 16 - -TAGS_V2 = { - 254: ("NewSubfileType", LONG, 1), - 255: ("SubfileType", SHORT, 1), - 256: ("ImageWidth", LONG, 1), - 257: ("ImageLength", LONG, 1), - 258: ("BitsPerSample", SHORT, 0), - 259: ( - "Compression", - SHORT, - 1, - { - "Uncompressed": 1, - "CCITT 1d": 2, - "Group 3 Fax": 3, - "Group 4 Fax": 4, - "LZW": 5, - "JPEG": 6, - "PackBits": 32773, - }, - ), - 262: ( - "PhotometricInterpretation", - SHORT, - 1, - { - "WhiteIsZero": 0, - "BlackIsZero": 1, - "RGB": 2, - "RGB Palette": 3, - "Transparency Mask": 4, - "CMYK": 5, - "YCbCr": 6, - "CieLAB": 8, - "CFA": 32803, # TIFF/EP, Adobe DNG - "LinearRaw": 32892, # Adobe DNG - }, - ), - 263: ("Threshholding", SHORT, 1), - 264: ("CellWidth", SHORT, 1), - 265: ("CellLength", SHORT, 1), - 266: ("FillOrder", SHORT, 1), - 269: ("DocumentName", ASCII, 1), - 270: ("ImageDescription", ASCII, 1), - 271: ("Make", ASCII, 1), - 272: ("Model", ASCII, 1), - 273: ("StripOffsets", LONG, 0), - 274: ("Orientation", SHORT, 1), - 277: ("SamplesPerPixel", SHORT, 1), - 278: ("RowsPerStrip", LONG, 1), - 279: ("StripByteCounts", LONG, 0), - 280: ("MinSampleValue", SHORT, 0), - 281: ("MaxSampleValue", SHORT, 0), - 282: ("XResolution", RATIONAL, 1), - 283: ("YResolution", RATIONAL, 1), - 284: ("PlanarConfiguration", SHORT, 1, {"Contiguous": 1, "Separate": 2}), - 285: ("PageName", ASCII, 1), - 286: ("XPosition", RATIONAL, 1), - 287: ("YPosition", RATIONAL, 1), - 288: ("FreeOffsets", LONG, 1), - 289: ("FreeByteCounts", LONG, 1), - 290: ("GrayResponseUnit", SHORT, 1), - 291: ("GrayResponseCurve", SHORT, 0), - 292: ("T4Options", LONG, 1), - 293: ("T6Options", LONG, 1), - 296: ("ResolutionUnit", SHORT, 1, {"none": 1, "inch": 2, "cm": 3}), - 297: ("PageNumber", SHORT, 2), - 301: ("TransferFunction", SHORT, 0), - 305: ("Software", ASCII, 1), - 306: ("DateTime", ASCII, 1), - 315: ("Artist", ASCII, 1), - 316: ("HostComputer", ASCII, 1), - 317: ("Predictor", SHORT, 1, {"none": 1, "Horizontal Differencing": 2}), - 318: ("WhitePoint", RATIONAL, 2), - 319: ("PrimaryChromaticities", RATIONAL, 6), - 320: ("ColorMap", SHORT, 0), - 321: ("HalftoneHints", SHORT, 2), - 322: ("TileWidth", LONG, 1), - 323: ("TileLength", LONG, 1), - 324: ("TileOffsets", LONG, 0), - 325: ("TileByteCounts", LONG, 0), - 330: ("SubIFDs", LONG, 0), - 332: ("InkSet", SHORT, 1), - 333: ("InkNames", ASCII, 1), - 334: ("NumberOfInks", SHORT, 1), - 336: ("DotRange", SHORT, 0), - 337: ("TargetPrinter", ASCII, 1), - 338: ("ExtraSamples", SHORT, 0), - 339: ("SampleFormat", SHORT, 0), - 340: ("SMinSampleValue", DOUBLE, 0), - 341: ("SMaxSampleValue", DOUBLE, 0), - 342: ("TransferRange", SHORT, 6), - 347: ("JPEGTables", UNDEFINED, 1), - # obsolete JPEG tags - 512: ("JPEGProc", SHORT, 1), - 513: ("JPEGInterchangeFormat", LONG, 1), - 514: ("JPEGInterchangeFormatLength", LONG, 1), - 515: ("JPEGRestartInterval", SHORT, 1), - 517: ("JPEGLosslessPredictors", SHORT, 0), - 518: ("JPEGPointTransforms", SHORT, 0), - 519: ("JPEGQTables", LONG, 0), - 520: ("JPEGDCTables", LONG, 0), - 521: ("JPEGACTables", LONG, 0), - 529: ("YCbCrCoefficients", RATIONAL, 3), - 530: ("YCbCrSubSampling", SHORT, 2), - 531: ("YCbCrPositioning", SHORT, 1), - 532: ("ReferenceBlackWhite", RATIONAL, 6), - 700: ("XMP", BYTE, 0), - 33432: ("Copyright", ASCII, 1), - 33723: ("IptcNaaInfo", UNDEFINED, 1), - 34377: ("PhotoshopInfo", BYTE, 0), - # FIXME add more tags here - 34665: ("ExifIFD", LONG, 1), - 34675: ("ICCProfile", UNDEFINED, 1), - 34853: ("GPSInfoIFD", LONG, 1), - 36864: ("ExifVersion", UNDEFINED, 1), - 37724: ("ImageSourceData", UNDEFINED, 1), - 40965: ("InteroperabilityIFD", LONG, 1), - 41730: ("CFAPattern", UNDEFINED, 1), - # MPInfo - 45056: ("MPFVersion", UNDEFINED, 1), - 45057: ("NumberOfImages", LONG, 1), - 45058: ("MPEntry", UNDEFINED, 1), - 45059: ("ImageUIDList", UNDEFINED, 0), # UNDONE, check - 45060: ("TotalFrames", LONG, 1), - 45313: ("MPIndividualNum", LONG, 1), - 45569: ("PanOrientation", LONG, 1), - 45570: ("PanOverlap_H", RATIONAL, 1), - 45571: ("PanOverlap_V", RATIONAL, 1), - 45572: ("BaseViewpointNum", LONG, 1), - 45573: ("ConvergenceAngle", SIGNED_RATIONAL, 1), - 45574: ("BaselineLength", RATIONAL, 1), - 45575: ("VerticalDivergence", SIGNED_RATIONAL, 1), - 45576: ("AxisDistance_X", SIGNED_RATIONAL, 1), - 45577: ("AxisDistance_Y", SIGNED_RATIONAL, 1), - 45578: ("AxisDistance_Z", SIGNED_RATIONAL, 1), - 45579: ("YawAngle", SIGNED_RATIONAL, 1), - 45580: ("PitchAngle", SIGNED_RATIONAL, 1), - 45581: ("RollAngle", SIGNED_RATIONAL, 1), - 40960: ("FlashPixVersion", UNDEFINED, 1), - 50741: ("MakerNoteSafety", SHORT, 1, {"Unsafe": 0, "Safe": 1}), - 50780: ("BestQualityScale", RATIONAL, 1), - 50838: ("ImageJMetaDataByteCounts", LONG, 0), # Can be more than one - 50839: ("ImageJMetaData", UNDEFINED, 1), # see Issue #2006 -} -TAGS_V2_GROUPS = { - # ExifIFD - 34665: { - 36864: ("ExifVersion", UNDEFINED, 1), - 40960: ("FlashPixVersion", UNDEFINED, 1), - 40965: ("InteroperabilityIFD", LONG, 1), - 41730: ("CFAPattern", UNDEFINED, 1), - }, - # GPSInfoIFD - 34853: { - 0: ("GPSVersionID", BYTE, 4), - 1: ("GPSLatitudeRef", ASCII, 2), - 2: ("GPSLatitude", RATIONAL, 3), - 3: ("GPSLongitudeRef", ASCII, 2), - 4: ("GPSLongitude", RATIONAL, 3), - 5: ("GPSAltitudeRef", BYTE, 1), - 6: ("GPSAltitude", RATIONAL, 1), - 7: ("GPSTimeStamp", RATIONAL, 3), - 8: ("GPSSatellites", ASCII, 0), - 9: ("GPSStatus", ASCII, 2), - 10: ("GPSMeasureMode", ASCII, 2), - 11: ("GPSDOP", RATIONAL, 1), - 12: ("GPSSpeedRef", ASCII, 2), - 13: ("GPSSpeed", RATIONAL, 1), - 14: ("GPSTrackRef", ASCII, 2), - 15: ("GPSTrack", RATIONAL, 1), - 16: ("GPSImgDirectionRef", ASCII, 2), - 17: ("GPSImgDirection", RATIONAL, 1), - 18: ("GPSMapDatum", ASCII, 0), - 19: ("GPSDestLatitudeRef", ASCII, 2), - 20: ("GPSDestLatitude", RATIONAL, 3), - 21: ("GPSDestLongitudeRef", ASCII, 2), - 22: ("GPSDestLongitude", RATIONAL, 3), - 23: ("GPSDestBearingRef", ASCII, 2), - 24: ("GPSDestBearing", RATIONAL, 1), - 25: ("GPSDestDistanceRef", ASCII, 2), - 26: ("GPSDestDistance", RATIONAL, 1), - 27: ("GPSProcessingMethod", UNDEFINED, 0), - 28: ("GPSAreaInformation", UNDEFINED, 0), - 29: ("GPSDateStamp", ASCII, 11), - 30: ("GPSDifferential", SHORT, 1), - }, - # InteroperabilityIFD - 40965: {1: ("InteropIndex", ASCII, 1), 2: ("InteropVersion", UNDEFINED, 1)}, -} - -# Legacy Tags structure -# these tags aren't included above, but were in the previous versions -TAGS = { - 347: "JPEGTables", - 700: "XMP", - # Additional Exif Info - 32932: "Wang Annotation", - 33434: "ExposureTime", - 33437: "FNumber", - 33445: "MD FileTag", - 33446: "MD ScalePixel", - 33447: "MD ColorTable", - 33448: "MD LabName", - 33449: "MD SampleInfo", - 33450: "MD PrepDate", - 33451: "MD PrepTime", - 33452: "MD FileUnits", - 33550: "ModelPixelScaleTag", - 33723: "IptcNaaInfo", - 33918: "INGR Packet Data Tag", - 33919: "INGR Flag Registers", - 33920: "IrasB Transformation Matrix", - 33922: "ModelTiepointTag", - 34264: "ModelTransformationTag", - 34377: "PhotoshopInfo", - 34735: "GeoKeyDirectoryTag", - 34736: "GeoDoubleParamsTag", - 34737: "GeoAsciiParamsTag", - 34850: "ExposureProgram", - 34852: "SpectralSensitivity", - 34855: "ISOSpeedRatings", - 34856: "OECF", - 34864: "SensitivityType", - 34865: "StandardOutputSensitivity", - 34866: "RecommendedExposureIndex", - 34867: "ISOSpeed", - 34868: "ISOSpeedLatitudeyyy", - 34869: "ISOSpeedLatitudezzz", - 34908: "HylaFAX FaxRecvParams", - 34909: "HylaFAX FaxSubAddress", - 34910: "HylaFAX FaxRecvTime", - 36864: "ExifVersion", - 36867: "DateTimeOriginal", - 36868: "DateTimeDigitized", - 37121: "ComponentsConfiguration", - 37122: "CompressedBitsPerPixel", - 37724: "ImageSourceData", - 37377: "ShutterSpeedValue", - 37378: "ApertureValue", - 37379: "BrightnessValue", - 37380: "ExposureBiasValue", - 37381: "MaxApertureValue", - 37382: "SubjectDistance", - 37383: "MeteringMode", - 37384: "LightSource", - 37385: "Flash", - 37386: "FocalLength", - 37396: "SubjectArea", - 37500: "MakerNote", - 37510: "UserComment", - 37520: "SubSec", - 37521: "SubSecTimeOriginal", - 37522: "SubsecTimeDigitized", - 40960: "FlashPixVersion", - 40961: "ColorSpace", - 40962: "PixelXDimension", - 40963: "PixelYDimension", - 40964: "RelatedSoundFile", - 40965: "InteroperabilityIFD", - 41483: "FlashEnergy", - 41484: "SpatialFrequencyResponse", - 41486: "FocalPlaneXResolution", - 41487: "FocalPlaneYResolution", - 41488: "FocalPlaneResolutionUnit", - 41492: "SubjectLocation", - 41493: "ExposureIndex", - 41495: "SensingMethod", - 41728: "FileSource", - 41729: "SceneType", - 41730: "CFAPattern", - 41985: "CustomRendered", - 41986: "ExposureMode", - 41987: "WhiteBalance", - 41988: "DigitalZoomRatio", - 41989: "FocalLengthIn35mmFilm", - 41990: "SceneCaptureType", - 41991: "GainControl", - 41992: "Contrast", - 41993: "Saturation", - 41994: "Sharpness", - 41995: "DeviceSettingDescription", - 41996: "SubjectDistanceRange", - 42016: "ImageUniqueID", - 42032: "CameraOwnerName", - 42033: "BodySerialNumber", - 42034: "LensSpecification", - 42035: "LensMake", - 42036: "LensModel", - 42037: "LensSerialNumber", - 42112: "GDAL_METADATA", - 42113: "GDAL_NODATA", - 42240: "Gamma", - 50215: "Oce Scanjob Description", - 50216: "Oce Application Selector", - 50217: "Oce Identification Number", - 50218: "Oce ImageLogic Characteristics", - # Adobe DNG - 50706: "DNGVersion", - 50707: "DNGBackwardVersion", - 50708: "UniqueCameraModel", - 50709: "LocalizedCameraModel", - 50710: "CFAPlaneColor", - 50711: "CFALayout", - 50712: "LinearizationTable", - 50713: "BlackLevelRepeatDim", - 50714: "BlackLevel", - 50715: "BlackLevelDeltaH", - 50716: "BlackLevelDeltaV", - 50717: "WhiteLevel", - 50718: "DefaultScale", - 50719: "DefaultCropOrigin", - 50720: "DefaultCropSize", - 50721: "ColorMatrix1", - 50722: "ColorMatrix2", - 50723: "CameraCalibration1", - 50724: "CameraCalibration2", - 50725: "ReductionMatrix1", - 50726: "ReductionMatrix2", - 50727: "AnalogBalance", - 50728: "AsShotNeutral", - 50729: "AsShotWhiteXY", - 50730: "BaselineExposure", - 50731: "BaselineNoise", - 50732: "BaselineSharpness", - 50733: "BayerGreenSplit", - 50734: "LinearResponseLimit", - 50735: "CameraSerialNumber", - 50736: "LensInfo", - 50737: "ChromaBlurRadius", - 50738: "AntiAliasStrength", - 50740: "DNGPrivateData", - 50778: "CalibrationIlluminant1", - 50779: "CalibrationIlluminant2", - 50784: "Alias Layer Metadata", -} - - -def _populate(): - for k, v in TAGS_V2.items(): - # Populate legacy structure. - TAGS[k] = v[0] - if len(v) == 4: - for sk, sv in v[3].items(): - TAGS[(k, sv)] = sk - - TAGS_V2[k] = TagInfo(k, *v) - - for tags in TAGS_V2_GROUPS.values(): - for k, v in tags.items(): - tags[k] = TagInfo(k, *v) - - -_populate() -## -# Map type numbers to type names -- defined in ImageFileDirectory. - -TYPES: dict[int, str] = {} - -# -# These tags are handled by default in libtiff, without -# adding to the custom dictionary. From tif_dir.c, searching for -# case TIFFTAG in the _TIFFVSetField function: -# Line: item. -# 148: case TIFFTAG_SUBFILETYPE: -# 151: case TIFFTAG_IMAGEWIDTH: -# 154: case TIFFTAG_IMAGELENGTH: -# 157: case TIFFTAG_BITSPERSAMPLE: -# 181: case TIFFTAG_COMPRESSION: -# 202: case TIFFTAG_PHOTOMETRIC: -# 205: case TIFFTAG_THRESHHOLDING: -# 208: case TIFFTAG_FILLORDER: -# 214: case TIFFTAG_ORIENTATION: -# 221: case TIFFTAG_SAMPLESPERPIXEL: -# 228: case TIFFTAG_ROWSPERSTRIP: -# 238: case TIFFTAG_MINSAMPLEVALUE: -# 241: case TIFFTAG_MAXSAMPLEVALUE: -# 244: case TIFFTAG_SMINSAMPLEVALUE: -# 247: case TIFFTAG_SMAXSAMPLEVALUE: -# 250: case TIFFTAG_XRESOLUTION: -# 256: case TIFFTAG_YRESOLUTION: -# 262: case TIFFTAG_PLANARCONFIG: -# 268: case TIFFTAG_XPOSITION: -# 271: case TIFFTAG_YPOSITION: -# 274: case TIFFTAG_RESOLUTIONUNIT: -# 280: case TIFFTAG_PAGENUMBER: -# 284: case TIFFTAG_HALFTONEHINTS: -# 288: case TIFFTAG_COLORMAP: -# 294: case TIFFTAG_EXTRASAMPLES: -# 298: case TIFFTAG_MATTEING: -# 305: case TIFFTAG_TILEWIDTH: -# 316: case TIFFTAG_TILELENGTH: -# 327: case TIFFTAG_TILEDEPTH: -# 333: case TIFFTAG_DATATYPE: -# 344: case TIFFTAG_SAMPLEFORMAT: -# 361: case TIFFTAG_IMAGEDEPTH: -# 364: case TIFFTAG_SUBIFD: -# 376: case TIFFTAG_YCBCRPOSITIONING: -# 379: case TIFFTAG_YCBCRSUBSAMPLING: -# 383: case TIFFTAG_TRANSFERFUNCTION: -# 389: case TIFFTAG_REFERENCEBLACKWHITE: -# 393: case TIFFTAG_INKNAMES: - -# Following pseudo-tags are also handled by default in libtiff: -# TIFFTAG_JPEGQUALITY 65537 - -# some of these are not in our TAGS_V2 dict and were included from tiff.h - -# This list also exists in encode.c -LIBTIFF_CORE = { - 255, - 256, - 257, - 258, - 259, - 262, - 263, - 266, - 274, - 277, - 278, - 280, - 281, - 340, - 341, - 282, - 283, - 284, - 286, - 287, - 296, - 297, - 321, - 320, - 338, - 32995, - 322, - 323, - 32998, - 32996, - 339, - 32997, - 330, - 531, - 530, - 301, - 532, - 333, - # as above - 269, # this has been in our tests forever, and works - 65537, -} - -LIBTIFF_CORE.remove(255) # We don't have support for subfiletypes -LIBTIFF_CORE.remove(322) # We don't have support for writing tiled images with libtiff -LIBTIFF_CORE.remove(323) # Tiled images -LIBTIFF_CORE.remove(333) # Ink Names either - -# Note to advanced users: There may be combinations of these -# parameters and values that when added properly, will work and -# produce valid tiff images that may work in your application. -# It is safe to add and remove tags from this set from Pillow's point -# of view so long as you test against libtiff. +# +# The Python Imaging Library. +# $Id$ +# +# TIFF tags +# +# This module provides clear-text names for various well-known +# TIFF tags. the TIFF codec works just fine without it. +# +# Copyright (c) Secret Labs AB 1999. +# +# See the README file for information on usage and redistribution. +# + +## +# This module provides constants and clear-text names for various +# well-known TIFF tags. +## +from __future__ import annotations + +from typing import NamedTuple + + +class _TagInfo(NamedTuple): + value: int | None + name: str + type: int | None + length: int | None + enum: dict[str, int] + + +class TagInfo(_TagInfo): + __slots__: list[str] = [] + + def __new__(cls, value=None, name="unknown", type=None, length=None, enum=None): + return super().__new__(cls, value, name, type, length, enum or {}) + + def cvt_enum(self, value): + # Using get will call hash(value), which can be expensive + # for some types (e.g. Fraction). Since self.enum is rarely + # used, it's usually better to test it first. + return self.enum.get(value, value) if self.enum else value + + +def lookup(tag, group=None): + """ + :param tag: Integer tag number + :param group: Which :py:data:`~PIL.TiffTags.TAGS_V2_GROUPS` to look in + + .. versionadded:: 8.3.0 + + :returns: Taginfo namedtuple, From the ``TAGS_V2`` info if possible, + otherwise just populating the value and name from ``TAGS``. + If the tag is not recognized, "unknown" is returned for the name + + """ + + if group is not None: + info = TAGS_V2_GROUPS[group].get(tag) if group in TAGS_V2_GROUPS else None + else: + info = TAGS_V2.get(tag) + return info or TagInfo(tag, TAGS.get(tag, "unknown")) + + +## +# Map tag numbers to tag info. +# +# id: (Name, Type, Length[, enum_values]) +# +# The length here differs from the length in the tiff spec. For +# numbers, the tiff spec is for the number of fields returned. We +# agree here. For string-like types, the tiff spec uses the length of +# field in bytes. In Pillow, we are using the number of expected +# fields, in general 1 for string-like types. + + +BYTE = 1 +ASCII = 2 +SHORT = 3 +LONG = 4 +RATIONAL = 5 +SIGNED_BYTE = 6 +UNDEFINED = 7 +SIGNED_SHORT = 8 +SIGNED_LONG = 9 +SIGNED_RATIONAL = 10 +FLOAT = 11 +DOUBLE = 12 +IFD = 13 +LONG8 = 16 + +TAGS_V2 = { + 254: ("NewSubfileType", LONG, 1), + 255: ("SubfileType", SHORT, 1), + 256: ("ImageWidth", LONG, 1), + 257: ("ImageLength", LONG, 1), + 258: ("BitsPerSample", SHORT, 0), + 259: ( + "Compression", + SHORT, + 1, + { + "Uncompressed": 1, + "CCITT 1d": 2, + "Group 3 Fax": 3, + "Group 4 Fax": 4, + "LZW": 5, + "JPEG": 6, + "PackBits": 32773, + }, + ), + 262: ( + "PhotometricInterpretation", + SHORT, + 1, + { + "WhiteIsZero": 0, + "BlackIsZero": 1, + "RGB": 2, + "RGB Palette": 3, + "Transparency Mask": 4, + "CMYK": 5, + "YCbCr": 6, + "CieLAB": 8, + "CFA": 32803, # TIFF/EP, Adobe DNG + "LinearRaw": 32892, # Adobe DNG + }, + ), + 263: ("Threshholding", SHORT, 1), + 264: ("CellWidth", SHORT, 1), + 265: ("CellLength", SHORT, 1), + 266: ("FillOrder", SHORT, 1), + 269: ("DocumentName", ASCII, 1), + 270: ("ImageDescription", ASCII, 1), + 271: ("Make", ASCII, 1), + 272: ("Model", ASCII, 1), + 273: ("StripOffsets", LONG, 0), + 274: ("Orientation", SHORT, 1), + 277: ("SamplesPerPixel", SHORT, 1), + 278: ("RowsPerStrip", LONG, 1), + 279: ("StripByteCounts", LONG, 0), + 280: ("MinSampleValue", SHORT, 0), + 281: ("MaxSampleValue", SHORT, 0), + 282: ("XResolution", RATIONAL, 1), + 283: ("YResolution", RATIONAL, 1), + 284: ("PlanarConfiguration", SHORT, 1, {"Contiguous": 1, "Separate": 2}), + 285: ("PageName", ASCII, 1), + 286: ("XPosition", RATIONAL, 1), + 287: ("YPosition", RATIONAL, 1), + 288: ("FreeOffsets", LONG, 1), + 289: ("FreeByteCounts", LONG, 1), + 290: ("GrayResponseUnit", SHORT, 1), + 291: ("GrayResponseCurve", SHORT, 0), + 292: ("T4Options", LONG, 1), + 293: ("T6Options", LONG, 1), + 296: ("ResolutionUnit", SHORT, 1, {"none": 1, "inch": 2, "cm": 3}), + 297: ("PageNumber", SHORT, 2), + 301: ("TransferFunction", SHORT, 0), + 305: ("Software", ASCII, 1), + 306: ("DateTime", ASCII, 1), + 315: ("Artist", ASCII, 1), + 316: ("HostComputer", ASCII, 1), + 317: ("Predictor", SHORT, 1, {"none": 1, "Horizontal Differencing": 2}), + 318: ("WhitePoint", RATIONAL, 2), + 319: ("PrimaryChromaticities", RATIONAL, 6), + 320: ("ColorMap", SHORT, 0), + 321: ("HalftoneHints", SHORT, 2), + 322: ("TileWidth", LONG, 1), + 323: ("TileLength", LONG, 1), + 324: ("TileOffsets", LONG, 0), + 325: ("TileByteCounts", LONG, 0), + 330: ("SubIFDs", LONG, 0), + 332: ("InkSet", SHORT, 1), + 333: ("InkNames", ASCII, 1), + 334: ("NumberOfInks", SHORT, 1), + 336: ("DotRange", SHORT, 0), + 337: ("TargetPrinter", ASCII, 1), + 338: ("ExtraSamples", SHORT, 0), + 339: ("SampleFormat", SHORT, 0), + 340: ("SMinSampleValue", DOUBLE, 0), + 341: ("SMaxSampleValue", DOUBLE, 0), + 342: ("TransferRange", SHORT, 6), + 347: ("JPEGTables", UNDEFINED, 1), + # obsolete JPEG tags + 512: ("JPEGProc", SHORT, 1), + 513: ("JPEGInterchangeFormat", LONG, 1), + 514: ("JPEGInterchangeFormatLength", LONG, 1), + 515: ("JPEGRestartInterval", SHORT, 1), + 517: ("JPEGLosslessPredictors", SHORT, 0), + 518: ("JPEGPointTransforms", SHORT, 0), + 519: ("JPEGQTables", LONG, 0), + 520: ("JPEGDCTables", LONG, 0), + 521: ("JPEGACTables", LONG, 0), + 529: ("YCbCrCoefficients", RATIONAL, 3), + 530: ("YCbCrSubSampling", SHORT, 2), + 531: ("YCbCrPositioning", SHORT, 1), + 532: ("ReferenceBlackWhite", RATIONAL, 6), + 700: ("XMP", BYTE, 0), + 33432: ("Copyright", ASCII, 1), + 33723: ("IptcNaaInfo", UNDEFINED, 1), + 34377: ("PhotoshopInfo", BYTE, 0), + # FIXME add more tags here + 34665: ("ExifIFD", LONG, 1), + 34675: ("ICCProfile", UNDEFINED, 1), + 34853: ("GPSInfoIFD", LONG, 1), + 36864: ("ExifVersion", UNDEFINED, 1), + 37724: ("ImageSourceData", UNDEFINED, 1), + 40965: ("InteroperabilityIFD", LONG, 1), + 41730: ("CFAPattern", UNDEFINED, 1), + # MPInfo + 45056: ("MPFVersion", UNDEFINED, 1), + 45057: ("NumberOfImages", LONG, 1), + 45058: ("MPEntry", UNDEFINED, 1), + 45059: ("ImageUIDList", UNDEFINED, 0), # UNDONE, check + 45060: ("TotalFrames", LONG, 1), + 45313: ("MPIndividualNum", LONG, 1), + 45569: ("PanOrientation", LONG, 1), + 45570: ("PanOverlap_H", RATIONAL, 1), + 45571: ("PanOverlap_V", RATIONAL, 1), + 45572: ("BaseViewpointNum", LONG, 1), + 45573: ("ConvergenceAngle", SIGNED_RATIONAL, 1), + 45574: ("BaselineLength", RATIONAL, 1), + 45575: ("VerticalDivergence", SIGNED_RATIONAL, 1), + 45576: ("AxisDistance_X", SIGNED_RATIONAL, 1), + 45577: ("AxisDistance_Y", SIGNED_RATIONAL, 1), + 45578: ("AxisDistance_Z", SIGNED_RATIONAL, 1), + 45579: ("YawAngle", SIGNED_RATIONAL, 1), + 45580: ("PitchAngle", SIGNED_RATIONAL, 1), + 45581: ("RollAngle", SIGNED_RATIONAL, 1), + 40960: ("FlashPixVersion", UNDEFINED, 1), + 50741: ("MakerNoteSafety", SHORT, 1, {"Unsafe": 0, "Safe": 1}), + 50780: ("BestQualityScale", RATIONAL, 1), + 50838: ("ImageJMetaDataByteCounts", LONG, 0), # Can be more than one + 50839: ("ImageJMetaData", UNDEFINED, 1), # see Issue #2006 +} +TAGS_V2_GROUPS = { + # ExifIFD + 34665: { + 36864: ("ExifVersion", UNDEFINED, 1), + 40960: ("FlashPixVersion", UNDEFINED, 1), + 40965: ("InteroperabilityIFD", LONG, 1), + 41730: ("CFAPattern", UNDEFINED, 1), + }, + # GPSInfoIFD + 34853: { + 0: ("GPSVersionID", BYTE, 4), + 1: ("GPSLatitudeRef", ASCII, 2), + 2: ("GPSLatitude", RATIONAL, 3), + 3: ("GPSLongitudeRef", ASCII, 2), + 4: ("GPSLongitude", RATIONAL, 3), + 5: ("GPSAltitudeRef", BYTE, 1), + 6: ("GPSAltitude", RATIONAL, 1), + 7: ("GPSTimeStamp", RATIONAL, 3), + 8: ("GPSSatellites", ASCII, 0), + 9: ("GPSStatus", ASCII, 2), + 10: ("GPSMeasureMode", ASCII, 2), + 11: ("GPSDOP", RATIONAL, 1), + 12: ("GPSSpeedRef", ASCII, 2), + 13: ("GPSSpeed", RATIONAL, 1), + 14: ("GPSTrackRef", ASCII, 2), + 15: ("GPSTrack", RATIONAL, 1), + 16: ("GPSImgDirectionRef", ASCII, 2), + 17: ("GPSImgDirection", RATIONAL, 1), + 18: ("GPSMapDatum", ASCII, 0), + 19: ("GPSDestLatitudeRef", ASCII, 2), + 20: ("GPSDestLatitude", RATIONAL, 3), + 21: ("GPSDestLongitudeRef", ASCII, 2), + 22: ("GPSDestLongitude", RATIONAL, 3), + 23: ("GPSDestBearingRef", ASCII, 2), + 24: ("GPSDestBearing", RATIONAL, 1), + 25: ("GPSDestDistanceRef", ASCII, 2), + 26: ("GPSDestDistance", RATIONAL, 1), + 27: ("GPSProcessingMethod", UNDEFINED, 0), + 28: ("GPSAreaInformation", UNDEFINED, 0), + 29: ("GPSDateStamp", ASCII, 11), + 30: ("GPSDifferential", SHORT, 1), + }, + # InteroperabilityIFD + 40965: {1: ("InteropIndex", ASCII, 1), 2: ("InteropVersion", UNDEFINED, 1)}, +} + +# Legacy Tags structure +# these tags aren't included above, but were in the previous versions +TAGS = { + 347: "JPEGTables", + 700: "XMP", + # Additional Exif Info + 32932: "Wang Annotation", + 33434: "ExposureTime", + 33437: "FNumber", + 33445: "MD FileTag", + 33446: "MD ScalePixel", + 33447: "MD ColorTable", + 33448: "MD LabName", + 33449: "MD SampleInfo", + 33450: "MD PrepDate", + 33451: "MD PrepTime", + 33452: "MD FileUnits", + 33550: "ModelPixelScaleTag", + 33723: "IptcNaaInfo", + 33918: "INGR Packet Data Tag", + 33919: "INGR Flag Registers", + 33920: "IrasB Transformation Matrix", + 33922: "ModelTiepointTag", + 34264: "ModelTransformationTag", + 34377: "PhotoshopInfo", + 34735: "GeoKeyDirectoryTag", + 34736: "GeoDoubleParamsTag", + 34737: "GeoAsciiParamsTag", + 34850: "ExposureProgram", + 34852: "SpectralSensitivity", + 34855: "ISOSpeedRatings", + 34856: "OECF", + 34864: "SensitivityType", + 34865: "StandardOutputSensitivity", + 34866: "RecommendedExposureIndex", + 34867: "ISOSpeed", + 34868: "ISOSpeedLatitudeyyy", + 34869: "ISOSpeedLatitudezzz", + 34908: "HylaFAX FaxRecvParams", + 34909: "HylaFAX FaxSubAddress", + 34910: "HylaFAX FaxRecvTime", + 36864: "ExifVersion", + 36867: "DateTimeOriginal", + 36868: "DateTimeDigitized", + 37121: "ComponentsConfiguration", + 37122: "CompressedBitsPerPixel", + 37724: "ImageSourceData", + 37377: "ShutterSpeedValue", + 37378: "ApertureValue", + 37379: "BrightnessValue", + 37380: "ExposureBiasValue", + 37381: "MaxApertureValue", + 37382: "SubjectDistance", + 37383: "MeteringMode", + 37384: "LightSource", + 37385: "Flash", + 37386: "FocalLength", + 37396: "SubjectArea", + 37500: "MakerNote", + 37510: "UserComment", + 37520: "SubSec", + 37521: "SubSecTimeOriginal", + 37522: "SubsecTimeDigitized", + 40960: "FlashPixVersion", + 40961: "ColorSpace", + 40962: "PixelXDimension", + 40963: "PixelYDimension", + 40964: "RelatedSoundFile", + 40965: "InteroperabilityIFD", + 41483: "FlashEnergy", + 41484: "SpatialFrequencyResponse", + 41486: "FocalPlaneXResolution", + 41487: "FocalPlaneYResolution", + 41488: "FocalPlaneResolutionUnit", + 41492: "SubjectLocation", + 41493: "ExposureIndex", + 41495: "SensingMethod", + 41728: "FileSource", + 41729: "SceneType", + 41730: "CFAPattern", + 41985: "CustomRendered", + 41986: "ExposureMode", + 41987: "WhiteBalance", + 41988: "DigitalZoomRatio", + 41989: "FocalLengthIn35mmFilm", + 41990: "SceneCaptureType", + 41991: "GainControl", + 41992: "Contrast", + 41993: "Saturation", + 41994: "Sharpness", + 41995: "DeviceSettingDescription", + 41996: "SubjectDistanceRange", + 42016: "ImageUniqueID", + 42032: "CameraOwnerName", + 42033: "BodySerialNumber", + 42034: "LensSpecification", + 42035: "LensMake", + 42036: "LensModel", + 42037: "LensSerialNumber", + 42112: "GDAL_METADATA", + 42113: "GDAL_NODATA", + 42240: "Gamma", + 50215: "Oce Scanjob Description", + 50216: "Oce Application Selector", + 50217: "Oce Identification Number", + 50218: "Oce ImageLogic Characteristics", + # Adobe DNG + 50706: "DNGVersion", + 50707: "DNGBackwardVersion", + 50708: "UniqueCameraModel", + 50709: "LocalizedCameraModel", + 50710: "CFAPlaneColor", + 50711: "CFALayout", + 50712: "LinearizationTable", + 50713: "BlackLevelRepeatDim", + 50714: "BlackLevel", + 50715: "BlackLevelDeltaH", + 50716: "BlackLevelDeltaV", + 50717: "WhiteLevel", + 50718: "DefaultScale", + 50719: "DefaultCropOrigin", + 50720: "DefaultCropSize", + 50721: "ColorMatrix1", + 50722: "ColorMatrix2", + 50723: "CameraCalibration1", + 50724: "CameraCalibration2", + 50725: "ReductionMatrix1", + 50726: "ReductionMatrix2", + 50727: "AnalogBalance", + 50728: "AsShotNeutral", + 50729: "AsShotWhiteXY", + 50730: "BaselineExposure", + 50731: "BaselineNoise", + 50732: "BaselineSharpness", + 50733: "BayerGreenSplit", + 50734: "LinearResponseLimit", + 50735: "CameraSerialNumber", + 50736: "LensInfo", + 50737: "ChromaBlurRadius", + 50738: "AntiAliasStrength", + 50740: "DNGPrivateData", + 50778: "CalibrationIlluminant1", + 50779: "CalibrationIlluminant2", + 50784: "Alias Layer Metadata", +} + + +def _populate(): + for k, v in TAGS_V2.items(): + # Populate legacy structure. + TAGS[k] = v[0] + if len(v) == 4: + for sk, sv in v[3].items(): + TAGS[(k, sv)] = sk + + TAGS_V2[k] = TagInfo(k, *v) + + for tags in TAGS_V2_GROUPS.values(): + for k, v in tags.items(): + tags[k] = TagInfo(k, *v) + + +_populate() +## +# Map type numbers to type names -- defined in ImageFileDirectory. + +TYPES: dict[int, str] = {} + +# +# These tags are handled by default in libtiff, without +# adding to the custom dictionary. From tif_dir.c, searching for +# case TIFFTAG in the _TIFFVSetField function: +# Line: item. +# 148: case TIFFTAG_SUBFILETYPE: +# 151: case TIFFTAG_IMAGEWIDTH: +# 154: case TIFFTAG_IMAGELENGTH: +# 157: case TIFFTAG_BITSPERSAMPLE: +# 181: case TIFFTAG_COMPRESSION: +# 202: case TIFFTAG_PHOTOMETRIC: +# 205: case TIFFTAG_THRESHHOLDING: +# 208: case TIFFTAG_FILLORDER: +# 214: case TIFFTAG_ORIENTATION: +# 221: case TIFFTAG_SAMPLESPERPIXEL: +# 228: case TIFFTAG_ROWSPERSTRIP: +# 238: case TIFFTAG_MINSAMPLEVALUE: +# 241: case TIFFTAG_MAXSAMPLEVALUE: +# 244: case TIFFTAG_SMINSAMPLEVALUE: +# 247: case TIFFTAG_SMAXSAMPLEVALUE: +# 250: case TIFFTAG_XRESOLUTION: +# 256: case TIFFTAG_YRESOLUTION: +# 262: case TIFFTAG_PLANARCONFIG: +# 268: case TIFFTAG_XPOSITION: +# 271: case TIFFTAG_YPOSITION: +# 274: case TIFFTAG_RESOLUTIONUNIT: +# 280: case TIFFTAG_PAGENUMBER: +# 284: case TIFFTAG_HALFTONEHINTS: +# 288: case TIFFTAG_COLORMAP: +# 294: case TIFFTAG_EXTRASAMPLES: +# 298: case TIFFTAG_MATTEING: +# 305: case TIFFTAG_TILEWIDTH: +# 316: case TIFFTAG_TILELENGTH: +# 327: case TIFFTAG_TILEDEPTH: +# 333: case TIFFTAG_DATATYPE: +# 344: case TIFFTAG_SAMPLEFORMAT: +# 361: case TIFFTAG_IMAGEDEPTH: +# 364: case TIFFTAG_SUBIFD: +# 376: case TIFFTAG_YCBCRPOSITIONING: +# 379: case TIFFTAG_YCBCRSUBSAMPLING: +# 383: case TIFFTAG_TRANSFERFUNCTION: +# 389: case TIFFTAG_REFERENCEBLACKWHITE: +# 393: case TIFFTAG_INKNAMES: + +# Following pseudo-tags are also handled by default in libtiff: +# TIFFTAG_JPEGQUALITY 65537 + +# some of these are not in our TAGS_V2 dict and were included from tiff.h + +# This list also exists in encode.c +LIBTIFF_CORE = { + 255, + 256, + 257, + 258, + 259, + 262, + 263, + 266, + 274, + 277, + 278, + 280, + 281, + 340, + 341, + 282, + 283, + 284, + 286, + 287, + 296, + 297, + 321, + 320, + 338, + 32995, + 322, + 323, + 32998, + 32996, + 339, + 32997, + 330, + 531, + 530, + 301, + 532, + 333, + # as above + 269, # this has been in our tests forever, and works + 65537, +} + +LIBTIFF_CORE.remove(255) # We don't have support for subfiletypes +LIBTIFF_CORE.remove(322) # We don't have support for writing tiled images with libtiff +LIBTIFF_CORE.remove(323) # Tiled images +LIBTIFF_CORE.remove(333) # Ink Names either + +# Note to advanced users: There may be combinations of these +# parameters and values that when added properly, will work and +# produce valid tiff images that may work in your application. +# It is safe to add and remove tags from this set from Pillow's point +# of view so long as you test against libtiff. diff --git a/src/PIL/WalImageFile.py b/src/PIL/WalImageFile.py index fbd7be6ed..25c02b13f 100644 --- a/src/PIL/WalImageFile.py +++ b/src/PIL/WalImageFile.py @@ -1,124 +1,124 @@ -# -# The Python Imaging Library. -# $Id$ -# -# WAL file handling -# -# History: -# 2003-04-23 fl created -# -# Copyright (c) 2003 by Fredrik Lundh. -# -# See the README file for information on usage and redistribution. -# - -""" -This reader is based on the specification available from: -https://www.flipcode.com/archives/Quake_2_BSP_File_Format.shtml -and has been tested with a few sample files found using google. - -.. note:: - This format cannot be automatically recognized, so the reader - is not registered for use with :py:func:`PIL.Image.open()`. - To open a WAL file, use the :py:func:`PIL.WalImageFile.open()` function instead. -""" -from __future__ import annotations - -from . import Image, ImageFile -from ._binary import i32le as i32 - - -class WalImageFile(ImageFile.ImageFile): - format = "WAL" - format_description = "Quake2 Texture" - - def _open(self) -> None: - self._mode = "P" - - # read header fields - header = self.fp.read(32 + 24 + 32 + 12) - self._size = i32(header, 32), i32(header, 36) - Image._decompression_bomb_check(self.size) - - # load pixel data - offset = i32(header, 40) - self.fp.seek(offset) - - # strings are null-terminated - self.info["name"] = header[:32].split(b"\0", 1)[0] - next_name = header[56 : 56 + 32].split(b"\0", 1)[0] - if next_name: - self.info["next_name"] = next_name - - def load(self): - if not self.im: - self.im = Image.core.new(self.mode, self.size) - self.frombytes(self.fp.read(self.size[0] * self.size[1])) - self.putpalette(quake2palette) - return Image.Image.load(self) - - -def open(filename): - """ - Load texture from a Quake2 WAL texture file. - - By default, a Quake2 standard palette is attached to the texture. - To override the palette, use the :py:func:`PIL.Image.Image.putpalette()` method. - - :param filename: WAL file name, or an opened file handle. - :returns: An image instance. - """ - return WalImageFile(filename) - - -quake2palette = ( - # default palette taken from piffo 0.93 by Hans Häggström - b"\x01\x01\x01\x0b\x0b\x0b\x12\x12\x12\x17\x17\x17\x1b\x1b\x1b\x1e" - b"\x1e\x1e\x22\x22\x22\x26\x26\x26\x29\x29\x29\x2c\x2c\x2c\x2f\x2f" - b"\x2f\x32\x32\x32\x35\x35\x35\x37\x37\x37\x3a\x3a\x3a\x3c\x3c\x3c" - b"\x24\x1e\x13\x22\x1c\x12\x20\x1b\x12\x1f\x1a\x10\x1d\x19\x10\x1b" - b"\x17\x0f\x1a\x16\x0f\x18\x14\x0d\x17\x13\x0d\x16\x12\x0d\x14\x10" - b"\x0b\x13\x0f\x0b\x10\x0d\x0a\x0f\x0b\x0a\x0d\x0b\x07\x0b\x0a\x07" - b"\x23\x23\x26\x22\x22\x25\x22\x20\x23\x21\x1f\x22\x20\x1e\x20\x1f" - b"\x1d\x1e\x1d\x1b\x1c\x1b\x1a\x1a\x1a\x19\x19\x18\x17\x17\x17\x16" - b"\x16\x14\x14\x14\x13\x13\x13\x10\x10\x10\x0f\x0f\x0f\x0d\x0d\x0d" - b"\x2d\x28\x20\x29\x24\x1c\x27\x22\x1a\x25\x1f\x17\x38\x2e\x1e\x31" - b"\x29\x1a\x2c\x25\x17\x26\x20\x14\x3c\x30\x14\x37\x2c\x13\x33\x28" - b"\x12\x2d\x24\x10\x28\x1f\x0f\x22\x1a\x0b\x1b\x14\x0a\x13\x0f\x07" - b"\x31\x1a\x16\x30\x17\x13\x2e\x16\x10\x2c\x14\x0d\x2a\x12\x0b\x27" - b"\x0f\x0a\x25\x0f\x07\x21\x0d\x01\x1e\x0b\x01\x1c\x0b\x01\x1a\x0b" - b"\x01\x18\x0a\x01\x16\x0a\x01\x13\x0a\x01\x10\x07\x01\x0d\x07\x01" - b"\x29\x23\x1e\x27\x21\x1c\x26\x20\x1b\x25\x1f\x1a\x23\x1d\x19\x21" - b"\x1c\x18\x20\x1b\x17\x1e\x19\x16\x1c\x18\x14\x1b\x17\x13\x19\x14" - b"\x10\x17\x13\x0f\x14\x10\x0d\x12\x0f\x0b\x0f\x0b\x0a\x0b\x0a\x07" - b"\x26\x1a\x0f\x23\x19\x0f\x20\x17\x0f\x1c\x16\x0f\x19\x13\x0d\x14" - b"\x10\x0b\x10\x0d\x0a\x0b\x0a\x07\x33\x22\x1f\x35\x29\x26\x37\x2f" - b"\x2d\x39\x35\x34\x37\x39\x3a\x33\x37\x39\x30\x34\x36\x2b\x31\x34" - b"\x27\x2e\x31\x22\x2b\x2f\x1d\x28\x2c\x17\x25\x2a\x0f\x20\x26\x0d" - b"\x1e\x25\x0b\x1c\x22\x0a\x1b\x20\x07\x19\x1e\x07\x17\x1b\x07\x14" - b"\x18\x01\x12\x16\x01\x0f\x12\x01\x0b\x0d\x01\x07\x0a\x01\x01\x01" - b"\x2c\x21\x21\x2a\x1f\x1f\x29\x1d\x1d\x27\x1c\x1c\x26\x1a\x1a\x24" - b"\x18\x18\x22\x17\x17\x21\x16\x16\x1e\x13\x13\x1b\x12\x12\x18\x10" - b"\x10\x16\x0d\x0d\x12\x0b\x0b\x0d\x0a\x0a\x0a\x07\x07\x01\x01\x01" - b"\x2e\x30\x29\x2d\x2e\x27\x2b\x2c\x26\x2a\x2a\x24\x28\x29\x23\x27" - b"\x27\x21\x26\x26\x1f\x24\x24\x1d\x22\x22\x1c\x1f\x1f\x1a\x1c\x1c" - b"\x18\x19\x19\x16\x17\x17\x13\x13\x13\x10\x0f\x0f\x0d\x0b\x0b\x0a" - b"\x30\x1e\x1b\x2d\x1c\x19\x2c\x1a\x17\x2a\x19\x14\x28\x17\x13\x26" - b"\x16\x10\x24\x13\x0f\x21\x12\x0d\x1f\x10\x0b\x1c\x0f\x0a\x19\x0d" - b"\x0a\x16\x0b\x07\x12\x0a\x07\x0f\x07\x01\x0a\x01\x01\x01\x01\x01" - b"\x28\x29\x38\x26\x27\x36\x25\x26\x34\x24\x24\x31\x22\x22\x2f\x20" - b"\x21\x2d\x1e\x1f\x2a\x1d\x1d\x27\x1b\x1b\x25\x19\x19\x21\x17\x17" - b"\x1e\x14\x14\x1b\x13\x12\x17\x10\x0f\x13\x0d\x0b\x0f\x0a\x07\x07" - b"\x2f\x32\x29\x2d\x30\x26\x2b\x2e\x24\x29\x2c\x21\x27\x2a\x1e\x25" - b"\x28\x1c\x23\x26\x1a\x21\x25\x18\x1e\x22\x14\x1b\x1f\x10\x19\x1c" - b"\x0d\x17\x1a\x0a\x13\x17\x07\x10\x13\x01\x0d\x0f\x01\x0a\x0b\x01" - b"\x01\x3f\x01\x13\x3c\x0b\x1b\x39\x10\x20\x35\x14\x23\x31\x17\x23" - b"\x2d\x18\x23\x29\x18\x3f\x3f\x3f\x3f\x3f\x39\x3f\x3f\x31\x3f\x3f" - b"\x2a\x3f\x3f\x20\x3f\x3f\x14\x3f\x3c\x12\x3f\x39\x0f\x3f\x35\x0b" - b"\x3f\x32\x07\x3f\x2d\x01\x3d\x2a\x01\x3b\x26\x01\x39\x21\x01\x37" - b"\x1d\x01\x34\x1a\x01\x32\x16\x01\x2f\x12\x01\x2d\x0f\x01\x2a\x0b" - b"\x01\x27\x07\x01\x23\x01\x01\x1d\x01\x01\x17\x01\x01\x10\x01\x01" - b"\x3d\x01\x01\x19\x19\x3f\x3f\x01\x01\x01\x01\x3f\x16\x16\x13\x10" - b"\x10\x0f\x0d\x0d\x0b\x3c\x2e\x2a\x36\x27\x20\x30\x21\x18\x29\x1b" - b"\x10\x3c\x39\x37\x37\x32\x2f\x31\x2c\x28\x2b\x26\x21\x30\x22\x20" -) +# +# The Python Imaging Library. +# $Id$ +# +# WAL file handling +# +# History: +# 2003-04-23 fl created +# +# Copyright (c) 2003 by Fredrik Lundh. +# +# See the README file for information on usage and redistribution. +# + +""" +This reader is based on the specification available from: +https://www.flipcode.com/archives/Quake_2_BSP_File_Format.shtml +and has been tested with a few sample files found using google. + +.. note:: + This format cannot be automatically recognized, so the reader + is not registered for use with :py:func:`PIL.Image.open()`. + To open a WAL file, use the :py:func:`PIL.WalImageFile.open()` function instead. +""" +from __future__ import annotations + +from . import Image, ImageFile +from ._binary import i32le as i32 + + +class WalImageFile(ImageFile.ImageFile): + format = "WAL" + format_description = "Quake2 Texture" + + def _open(self) -> None: + self._mode = "P" + + # read header fields + header = self.fp.read(32 + 24 + 32 + 12) + self._size = i32(header, 32), i32(header, 36) + Image._decompression_bomb_check(self.size) + + # load pixel data + offset = i32(header, 40) + self.fp.seek(offset) + + # strings are null-terminated + self.info["name"] = header[:32].split(b"\0", 1)[0] + next_name = header[56 : 56 + 32].split(b"\0", 1)[0] + if next_name: + self.info["next_name"] = next_name + + def load(self): + if not self.im: + self.im = Image.core.new(self.mode, self.size) + self.frombytes(self.fp.read(self.size[0] * self.size[1])) + self.putpalette(quake2palette) + return Image.Image.load(self) + + +def open(filename): + """ + Load texture from a Quake2 WAL texture file. + + By default, a Quake2 standard palette is attached to the texture. + To override the palette, use the :py:func:`PIL.Image.Image.putpalette()` method. + + :param filename: WAL file name, or an opened file handle. + :returns: An image instance. + """ + return WalImageFile(filename) + + +quake2palette = ( + # default palette taken from piffo 0.93 by Hans Häggström + b"\x01\x01\x01\x0b\x0b\x0b\x12\x12\x12\x17\x17\x17\x1b\x1b\x1b\x1e" + b"\x1e\x1e\x22\x22\x22\x26\x26\x26\x29\x29\x29\x2c\x2c\x2c\x2f\x2f" + b"\x2f\x32\x32\x32\x35\x35\x35\x37\x37\x37\x3a\x3a\x3a\x3c\x3c\x3c" + b"\x24\x1e\x13\x22\x1c\x12\x20\x1b\x12\x1f\x1a\x10\x1d\x19\x10\x1b" + b"\x17\x0f\x1a\x16\x0f\x18\x14\x0d\x17\x13\x0d\x16\x12\x0d\x14\x10" + b"\x0b\x13\x0f\x0b\x10\x0d\x0a\x0f\x0b\x0a\x0d\x0b\x07\x0b\x0a\x07" + b"\x23\x23\x26\x22\x22\x25\x22\x20\x23\x21\x1f\x22\x20\x1e\x20\x1f" + b"\x1d\x1e\x1d\x1b\x1c\x1b\x1a\x1a\x1a\x19\x19\x18\x17\x17\x17\x16" + b"\x16\x14\x14\x14\x13\x13\x13\x10\x10\x10\x0f\x0f\x0f\x0d\x0d\x0d" + b"\x2d\x28\x20\x29\x24\x1c\x27\x22\x1a\x25\x1f\x17\x38\x2e\x1e\x31" + b"\x29\x1a\x2c\x25\x17\x26\x20\x14\x3c\x30\x14\x37\x2c\x13\x33\x28" + b"\x12\x2d\x24\x10\x28\x1f\x0f\x22\x1a\x0b\x1b\x14\x0a\x13\x0f\x07" + b"\x31\x1a\x16\x30\x17\x13\x2e\x16\x10\x2c\x14\x0d\x2a\x12\x0b\x27" + b"\x0f\x0a\x25\x0f\x07\x21\x0d\x01\x1e\x0b\x01\x1c\x0b\x01\x1a\x0b" + b"\x01\x18\x0a\x01\x16\x0a\x01\x13\x0a\x01\x10\x07\x01\x0d\x07\x01" + b"\x29\x23\x1e\x27\x21\x1c\x26\x20\x1b\x25\x1f\x1a\x23\x1d\x19\x21" + b"\x1c\x18\x20\x1b\x17\x1e\x19\x16\x1c\x18\x14\x1b\x17\x13\x19\x14" + b"\x10\x17\x13\x0f\x14\x10\x0d\x12\x0f\x0b\x0f\x0b\x0a\x0b\x0a\x07" + b"\x26\x1a\x0f\x23\x19\x0f\x20\x17\x0f\x1c\x16\x0f\x19\x13\x0d\x14" + b"\x10\x0b\x10\x0d\x0a\x0b\x0a\x07\x33\x22\x1f\x35\x29\x26\x37\x2f" + b"\x2d\x39\x35\x34\x37\x39\x3a\x33\x37\x39\x30\x34\x36\x2b\x31\x34" + b"\x27\x2e\x31\x22\x2b\x2f\x1d\x28\x2c\x17\x25\x2a\x0f\x20\x26\x0d" + b"\x1e\x25\x0b\x1c\x22\x0a\x1b\x20\x07\x19\x1e\x07\x17\x1b\x07\x14" + b"\x18\x01\x12\x16\x01\x0f\x12\x01\x0b\x0d\x01\x07\x0a\x01\x01\x01" + b"\x2c\x21\x21\x2a\x1f\x1f\x29\x1d\x1d\x27\x1c\x1c\x26\x1a\x1a\x24" + b"\x18\x18\x22\x17\x17\x21\x16\x16\x1e\x13\x13\x1b\x12\x12\x18\x10" + b"\x10\x16\x0d\x0d\x12\x0b\x0b\x0d\x0a\x0a\x0a\x07\x07\x01\x01\x01" + b"\x2e\x30\x29\x2d\x2e\x27\x2b\x2c\x26\x2a\x2a\x24\x28\x29\x23\x27" + b"\x27\x21\x26\x26\x1f\x24\x24\x1d\x22\x22\x1c\x1f\x1f\x1a\x1c\x1c" + b"\x18\x19\x19\x16\x17\x17\x13\x13\x13\x10\x0f\x0f\x0d\x0b\x0b\x0a" + b"\x30\x1e\x1b\x2d\x1c\x19\x2c\x1a\x17\x2a\x19\x14\x28\x17\x13\x26" + b"\x16\x10\x24\x13\x0f\x21\x12\x0d\x1f\x10\x0b\x1c\x0f\x0a\x19\x0d" + b"\x0a\x16\x0b\x07\x12\x0a\x07\x0f\x07\x01\x0a\x01\x01\x01\x01\x01" + b"\x28\x29\x38\x26\x27\x36\x25\x26\x34\x24\x24\x31\x22\x22\x2f\x20" + b"\x21\x2d\x1e\x1f\x2a\x1d\x1d\x27\x1b\x1b\x25\x19\x19\x21\x17\x17" + b"\x1e\x14\x14\x1b\x13\x12\x17\x10\x0f\x13\x0d\x0b\x0f\x0a\x07\x07" + b"\x2f\x32\x29\x2d\x30\x26\x2b\x2e\x24\x29\x2c\x21\x27\x2a\x1e\x25" + b"\x28\x1c\x23\x26\x1a\x21\x25\x18\x1e\x22\x14\x1b\x1f\x10\x19\x1c" + b"\x0d\x17\x1a\x0a\x13\x17\x07\x10\x13\x01\x0d\x0f\x01\x0a\x0b\x01" + b"\x01\x3f\x01\x13\x3c\x0b\x1b\x39\x10\x20\x35\x14\x23\x31\x17\x23" + b"\x2d\x18\x23\x29\x18\x3f\x3f\x3f\x3f\x3f\x39\x3f\x3f\x31\x3f\x3f" + b"\x2a\x3f\x3f\x20\x3f\x3f\x14\x3f\x3c\x12\x3f\x39\x0f\x3f\x35\x0b" + b"\x3f\x32\x07\x3f\x2d\x01\x3d\x2a\x01\x3b\x26\x01\x39\x21\x01\x37" + b"\x1d\x01\x34\x1a\x01\x32\x16\x01\x2f\x12\x01\x2d\x0f\x01\x2a\x0b" + b"\x01\x27\x07\x01\x23\x01\x01\x1d\x01\x01\x17\x01\x01\x10\x01\x01" + b"\x3d\x01\x01\x19\x19\x3f\x3f\x01\x01\x01\x01\x3f\x16\x16\x13\x10" + b"\x10\x0f\x0d\x0d\x0b\x3c\x2e\x2a\x36\x27\x20\x30\x21\x18\x29\x1b" + b"\x10\x3c\x39\x37\x37\x32\x2f\x31\x2c\x28\x2b\x26\x21\x30\x22\x20" +) diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index 97debc2ed..75b3e65b1 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -1,372 +1,372 @@ -from __future__ import annotations - -from io import BytesIO -from typing import IO, Any - -from . import Image, ImageFile - -try: - from . import _webp - - SUPPORTED = True -except ImportError: - SUPPORTED = False - - -_VALID_WEBP_MODES = {"RGBX": True, "RGBA": True, "RGB": True} - -_VALID_WEBP_LEGACY_MODES = {"RGB": True, "RGBA": True} - -_VP8_MODES_BY_IDENTIFIER = { - b"VP8 ": "RGB", - b"VP8X": "RGBA", - b"VP8L": "RGBA", # lossless -} - - -def _accept(prefix: bytes) -> bool | str: - is_riff_file_format = prefix[:4] == b"RIFF" - is_webp_file = prefix[8:12] == b"WEBP" - is_valid_vp8_mode = prefix[12:16] in _VP8_MODES_BY_IDENTIFIER - - if is_riff_file_format and is_webp_file and is_valid_vp8_mode: - if not SUPPORTED: - return ( - "image file could not be identified because WEBP support not installed" - ) - return True - return False - - -class WebPImageFile(ImageFile.ImageFile): - format = "WEBP" - format_description = "WebP image" - __loaded = 0 - __logical_frame = 0 - - def _open(self) -> None: - if not _webp.HAVE_WEBPANIM: - # Legacy mode - data, width, height, self._mode, icc_profile, exif = _webp.WebPDecode( - self.fp.read() - ) - if icc_profile: - self.info["icc_profile"] = icc_profile - if exif: - self.info["exif"] = exif - self._size = width, height - self.fp = BytesIO(data) - self.tile = [("raw", (0, 0) + self.size, 0, self.mode)] - self.n_frames = 1 - self.is_animated = False - return - - # Use the newer AnimDecoder API to parse the (possibly) animated file, - # and access muxed chunks like ICC/EXIF/XMP. - self._decoder = _webp.WebPAnimDecoder(self.fp.read()) - - # Get info from decoder - width, height, loop_count, bgcolor, frame_count, mode = self._decoder.get_info() - self._size = width, height - self.info["loop"] = loop_count - bg_a, bg_r, bg_g, bg_b = ( - (bgcolor >> 24) & 0xFF, - (bgcolor >> 16) & 0xFF, - (bgcolor >> 8) & 0xFF, - bgcolor & 0xFF, - ) - self.info["background"] = (bg_r, bg_g, bg_b, bg_a) - self.n_frames = frame_count - self.is_animated = self.n_frames > 1 - self._mode = "RGB" if mode == "RGBX" else mode - self.rawmode = mode - self.tile = [] - - # Attempt to read ICC / EXIF / XMP chunks from file - icc_profile = self._decoder.get_chunk("ICCP") - exif = self._decoder.get_chunk("EXIF") - xmp = self._decoder.get_chunk("XMP ") - if icc_profile: - self.info["icc_profile"] = icc_profile - if exif: - self.info["exif"] = exif - if xmp: - self.info["xmp"] = xmp - - # Initialize seek state - self._reset(reset=False) - - def _getexif(self) -> dict[str, Any] | None: - if "exif" not in self.info: - return None - return self.getexif()._get_merged_dict() - - def getxmp(self) -> dict[str, Any]: - """ - Returns a dictionary containing the XMP tags. - Requires defusedxml to be installed. - - :returns: XMP tags in a dictionary. - """ - return self._getxmp(self.info["xmp"]) if "xmp" in self.info else {} - - def seek(self, frame: int) -> None: - if not self._seek_check(frame): - return - - # Set logical frame to requested position - self.__logical_frame = frame - - def _reset(self, reset: bool = True) -> None: - if reset: - self._decoder.reset() - self.__physical_frame = 0 - self.__loaded = -1 - self.__timestamp = 0 - - def _get_next(self): - # Get next frame - ret = self._decoder.get_next() - self.__physical_frame += 1 - - # Check if an error occurred - if ret is None: - self._reset() # Reset just to be safe - self.seek(0) - msg = "failed to decode next frame in WebP file" - raise EOFError(msg) - - # Compute duration - data, timestamp = ret - duration = timestamp - self.__timestamp - self.__timestamp = timestamp - - # libwebp gives frame end, adjust to start of frame - timestamp -= duration - return data, timestamp, duration - - def _seek(self, frame: int) -> None: - if self.__physical_frame == frame: - return # Nothing to do - if frame < self.__physical_frame: - self._reset() # Rewind to beginning - while self.__physical_frame < frame: - self._get_next() # Advance to the requested frame - - def load(self): - if _webp.HAVE_WEBPANIM: - if self.__loaded != self.__logical_frame: - self._seek(self.__logical_frame) - - # We need to load the image data for this frame - data, timestamp, duration = self._get_next() - self.info["timestamp"] = timestamp - self.info["duration"] = duration - self.__loaded = self.__logical_frame - - # Set tile - if self.fp and self._exclusive_fp: - self.fp.close() - self.fp = BytesIO(data) - self.tile = [("raw", (0, 0) + self.size, 0, self.rawmode)] - - return super().load() - - def load_seek(self, pos: int) -> None: - pass - - def tell(self) -> int: - if not _webp.HAVE_WEBPANIM: - return super().tell() - - return self.__logical_frame - - -def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - encoderinfo = im.encoderinfo.copy() - append_images = list(encoderinfo.get("append_images", [])) - - # If total frame count is 1, then save using the legacy API, which - # will preserve non-alpha modes - total = 0 - for ims in [im] + append_images: - total += getattr(ims, "n_frames", 1) - if total == 1: - _save(im, fp, filename) - return - - background: int | tuple[int, ...] = (0, 0, 0, 0) - if "background" in encoderinfo: - background = encoderinfo["background"] - elif "background" in im.info: - background = im.info["background"] - if isinstance(background, int): - # GifImagePlugin stores a global color table index in - # info["background"]. So it must be converted to an RGBA value - palette = im.getpalette() - if palette: - r, g, b = palette[background * 3 : (background + 1) * 3] - background = (r, g, b, 255) - else: - background = (background, background, background, 255) - - duration = im.encoderinfo.get("duration", im.info.get("duration", 0)) - loop = im.encoderinfo.get("loop", 0) - minimize_size = im.encoderinfo.get("minimize_size", False) - kmin = im.encoderinfo.get("kmin", None) - kmax = im.encoderinfo.get("kmax", None) - allow_mixed = im.encoderinfo.get("allow_mixed", False) - verbose = False - lossless = im.encoderinfo.get("lossless", False) - quality = im.encoderinfo.get("quality", 80) - alpha_quality = im.encoderinfo.get("alpha_quality", 100) - method = im.encoderinfo.get("method", 0) - icc_profile = im.encoderinfo.get("icc_profile") or "" - exif = im.encoderinfo.get("exif", "") - if isinstance(exif, Image.Exif): - exif = exif.tobytes() - xmp = im.encoderinfo.get("xmp", "") - if allow_mixed: - lossless = False - - # Sensible keyframe defaults are from gif2webp.c script - if kmin is None: - kmin = 9 if lossless else 3 - if kmax is None: - kmax = 17 if lossless else 5 - - # Validate background color - if ( - not isinstance(background, (list, tuple)) - or len(background) != 4 - or not all(0 <= v < 256 for v in background) - ): - msg = f"Background color is not an RGBA tuple clamped to (0-255): {background}" - raise OSError(msg) - - # Convert to packed uint - bg_r, bg_g, bg_b, bg_a = background - background = (bg_a << 24) | (bg_r << 16) | (bg_g << 8) | (bg_b << 0) - - # Setup the WebP animation encoder - enc = _webp.WebPAnimEncoder( - im.size[0], - im.size[1], - background, - loop, - minimize_size, - kmin, - kmax, - allow_mixed, - verbose, - ) - - # Add each frame - frame_idx = 0 - timestamp = 0 - cur_idx = im.tell() - try: - for ims in [im] + append_images: - # Get # of frames in this image - nfr = getattr(ims, "n_frames", 1) - - for idx in range(nfr): - ims.seek(idx) - ims.load() - - # Make sure image mode is supported - frame = ims - rawmode = ims.mode - if ims.mode not in _VALID_WEBP_MODES: - alpha = ( - "A" in ims.mode - or "a" in ims.mode - or (ims.mode == "P" and "A" in ims.im.getpalettemode()) - ) - rawmode = "RGBA" if alpha else "RGB" - frame = ims.convert(rawmode) - - if rawmode == "RGB": - # For faster conversion, use RGBX - rawmode = "RGBX" - - # Append the frame to the animation encoder - enc.add( - frame.tobytes("raw", rawmode), - round(timestamp), - frame.size[0], - frame.size[1], - rawmode, - lossless, - quality, - alpha_quality, - method, - ) - - # Update timestamp and frame index - if isinstance(duration, (list, tuple)): - timestamp += duration[frame_idx] - else: - timestamp += duration - frame_idx += 1 - - finally: - im.seek(cur_idx) - - # Force encoder to flush frames - enc.add(None, round(timestamp), 0, 0, "", lossless, quality, alpha_quality, 0) - - # Get the final output from the encoder - data = enc.assemble(icc_profile, exif, xmp) - if data is None: - msg = "cannot write file as WebP (encoder returned None)" - raise OSError(msg) - - fp.write(data) - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - lossless = im.encoderinfo.get("lossless", False) - quality = im.encoderinfo.get("quality", 80) - alpha_quality = im.encoderinfo.get("alpha_quality", 100) - icc_profile = im.encoderinfo.get("icc_profile") or "" - exif = im.encoderinfo.get("exif", b"") - if isinstance(exif, Image.Exif): - exif = exif.tobytes() - if exif.startswith(b"Exif\x00\x00"): - exif = exif[6:] - xmp = im.encoderinfo.get("xmp", "") - method = im.encoderinfo.get("method", 4) - exact = 1 if im.encoderinfo.get("exact") else 0 - - if im.mode not in _VALID_WEBP_LEGACY_MODES: - im = im.convert("RGBA" if im.has_transparency_data else "RGB") - - data = _webp.WebPEncode( - im.tobytes(), - im.size[0], - im.size[1], - lossless, - float(quality), - float(alpha_quality), - im.mode, - icc_profile, - method, - exact, - exif, - xmp, - ) - if data is None: - msg = "cannot write file as WebP (encoder returned None)" - raise OSError(msg) - - fp.write(data) - - -Image.register_open(WebPImageFile.format, WebPImageFile, _accept) -if SUPPORTED: - Image.register_save(WebPImageFile.format, _save) - if _webp.HAVE_WEBPANIM: - Image.register_save_all(WebPImageFile.format, _save_all) - Image.register_extension(WebPImageFile.format, ".webp") - Image.register_mime(WebPImageFile.format, "image/webp") +from __future__ import annotations + +from io import BytesIO +from typing import IO, Any + +from . import Image, ImageFile + +try: + from . import _webp + + SUPPORTED = True +except ImportError: + SUPPORTED = False + + +_VALID_WEBP_MODES = {"RGBX": True, "RGBA": True, "RGB": True} + +_VALID_WEBP_LEGACY_MODES = {"RGB": True, "RGBA": True} + +_VP8_MODES_BY_IDENTIFIER = { + b"VP8 ": "RGB", + b"VP8X": "RGBA", + b"VP8L": "RGBA", # lossless +} + + +def _accept(prefix: bytes) -> bool | str: + is_riff_file_format = prefix[:4] == b"RIFF" + is_webp_file = prefix[8:12] == b"WEBP" + is_valid_vp8_mode = prefix[12:16] in _VP8_MODES_BY_IDENTIFIER + + if is_riff_file_format and is_webp_file and is_valid_vp8_mode: + if not SUPPORTED: + return ( + "image file could not be identified because WEBP support not installed" + ) + return True + return False + + +class WebPImageFile(ImageFile.ImageFile): + format = "WEBP" + format_description = "WebP image" + __loaded = 0 + __logical_frame = 0 + + def _open(self) -> None: + if not _webp.HAVE_WEBPANIM: + # Legacy mode + data, width, height, self._mode, icc_profile, exif = _webp.WebPDecode( + self.fp.read() + ) + if icc_profile: + self.info["icc_profile"] = icc_profile + if exif: + self.info["exif"] = exif + self._size = width, height + self.fp = BytesIO(data) + self.tile = [("raw", (0, 0) + self.size, 0, self.mode)] + self.n_frames = 1 + self.is_animated = False + return + + # Use the newer AnimDecoder API to parse the (possibly) animated file, + # and access muxed chunks like ICC/EXIF/XMP. + self._decoder = _webp.WebPAnimDecoder(self.fp.read()) + + # Get info from decoder + width, height, loop_count, bgcolor, frame_count, mode = self._decoder.get_info() + self._size = width, height + self.info["loop"] = loop_count + bg_a, bg_r, bg_g, bg_b = ( + (bgcolor >> 24) & 0xFF, + (bgcolor >> 16) & 0xFF, + (bgcolor >> 8) & 0xFF, + bgcolor & 0xFF, + ) + self.info["background"] = (bg_r, bg_g, bg_b, bg_a) + self.n_frames = frame_count + self.is_animated = self.n_frames > 1 + self._mode = "RGB" if mode == "RGBX" else mode + self.rawmode = mode + self.tile = [] + + # Attempt to read ICC / EXIF / XMP chunks from file + icc_profile = self._decoder.get_chunk("ICCP") + exif = self._decoder.get_chunk("EXIF") + xmp = self._decoder.get_chunk("XMP ") + if icc_profile: + self.info["icc_profile"] = icc_profile + if exif: + self.info["exif"] = exif + if xmp: + self.info["xmp"] = xmp + + # Initialize seek state + self._reset(reset=False) + + def _getexif(self) -> dict[str, Any] | None: + if "exif" not in self.info: + return None + return self.getexif()._get_merged_dict() + + def getxmp(self) -> dict[str, Any]: + """ + Returns a dictionary containing the XMP tags. + Requires defusedxml to be installed. + + :returns: XMP tags in a dictionary. + """ + return self._getxmp(self.info["xmp"]) if "xmp" in self.info else {} + + def seek(self, frame: int) -> None: + if not self._seek_check(frame): + return + + # Set logical frame to requested position + self.__logical_frame = frame + + def _reset(self, reset: bool = True) -> None: + if reset: + self._decoder.reset() + self.__physical_frame = 0 + self.__loaded = -1 + self.__timestamp = 0 + + def _get_next(self): + # Get next frame + ret = self._decoder.get_next() + self.__physical_frame += 1 + + # Check if an error occurred + if ret is None: + self._reset() # Reset just to be safe + self.seek(0) + msg = "failed to decode next frame in WebP file" + raise EOFError(msg) + + # Compute duration + data, timestamp = ret + duration = timestamp - self.__timestamp + self.__timestamp = timestamp + + # libwebp gives frame end, adjust to start of frame + timestamp -= duration + return data, timestamp, duration + + def _seek(self, frame: int) -> None: + if self.__physical_frame == frame: + return # Nothing to do + if frame < self.__physical_frame: + self._reset() # Rewind to beginning + while self.__physical_frame < frame: + self._get_next() # Advance to the requested frame + + def load(self): + if _webp.HAVE_WEBPANIM: + if self.__loaded != self.__logical_frame: + self._seek(self.__logical_frame) + + # We need to load the image data for this frame + data, timestamp, duration = self._get_next() + self.info["timestamp"] = timestamp + self.info["duration"] = duration + self.__loaded = self.__logical_frame + + # Set tile + if self.fp and self._exclusive_fp: + self.fp.close() + self.fp = BytesIO(data) + self.tile = [("raw", (0, 0) + self.size, 0, self.rawmode)] + + return super().load() + + def load_seek(self, pos: int) -> None: + pass + + def tell(self) -> int: + if not _webp.HAVE_WEBPANIM: + return super().tell() + + return self.__logical_frame + + +def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + encoderinfo = im.encoderinfo.copy() + append_images = list(encoderinfo.get("append_images", [])) + + # If total frame count is 1, then save using the legacy API, which + # will preserve non-alpha modes + total = 0 + for ims in [im] + append_images: + total += getattr(ims, "n_frames", 1) + if total == 1: + _save(im, fp, filename) + return + + background: int | tuple[int, ...] = (0, 0, 0, 0) + if "background" in encoderinfo: + background = encoderinfo["background"] + elif "background" in im.info: + background = im.info["background"] + if isinstance(background, int): + # GifImagePlugin stores a global color table index in + # info["background"]. So it must be converted to an RGBA value + palette = im.getpalette() + if palette: + r, g, b = palette[background * 3 : (background + 1) * 3] + background = (r, g, b, 255) + else: + background = (background, background, background, 255) + + duration = im.encoderinfo.get("duration", im.info.get("duration", 0)) + loop = im.encoderinfo.get("loop", 0) + minimize_size = im.encoderinfo.get("minimize_size", False) + kmin = im.encoderinfo.get("kmin", None) + kmax = im.encoderinfo.get("kmax", None) + allow_mixed = im.encoderinfo.get("allow_mixed", False) + verbose = False + lossless = im.encoderinfo.get("lossless", False) + quality = im.encoderinfo.get("quality", 80) + alpha_quality = im.encoderinfo.get("alpha_quality", 100) + method = im.encoderinfo.get("method", 0) + icc_profile = im.encoderinfo.get("icc_profile") or "" + exif = im.encoderinfo.get("exif", "") + if isinstance(exif, Image.Exif): + exif = exif.tobytes() + xmp = im.encoderinfo.get("xmp", "") + if allow_mixed: + lossless = False + + # Sensible keyframe defaults are from gif2webp.c script + if kmin is None: + kmin = 9 if lossless else 3 + if kmax is None: + kmax = 17 if lossless else 5 + + # Validate background color + if ( + not isinstance(background, (list, tuple)) + or len(background) != 4 + or not all(0 <= v < 256 for v in background) + ): + msg = f"Background color is not an RGBA tuple clamped to (0-255): {background}" + raise OSError(msg) + + # Convert to packed uint + bg_r, bg_g, bg_b, bg_a = background + background = (bg_a << 24) | (bg_r << 16) | (bg_g << 8) | (bg_b << 0) + + # Setup the WebP animation encoder + enc = _webp.WebPAnimEncoder( + im.size[0], + im.size[1], + background, + loop, + minimize_size, + kmin, + kmax, + allow_mixed, + verbose, + ) + + # Add each frame + frame_idx = 0 + timestamp = 0 + cur_idx = im.tell() + try: + for ims in [im] + append_images: + # Get # of frames in this image + nfr = getattr(ims, "n_frames", 1) + + for idx in range(nfr): + ims.seek(idx) + ims.load() + + # Make sure image mode is supported + frame = ims + rawmode = ims.mode + if ims.mode not in _VALID_WEBP_MODES: + alpha = ( + "A" in ims.mode + or "a" in ims.mode + or (ims.mode == "P" and "A" in ims.im.getpalettemode()) + ) + rawmode = "RGBA" if alpha else "RGB" + frame = ims.convert(rawmode) + + if rawmode == "RGB": + # For faster conversion, use RGBX + rawmode = "RGBX" + + # Append the frame to the animation encoder + enc.add( + frame.tobytes("raw", rawmode), + round(timestamp), + frame.size[0], + frame.size[1], + rawmode, + lossless, + quality, + alpha_quality, + method, + ) + + # Update timestamp and frame index + if isinstance(duration, (list, tuple)): + timestamp += duration[frame_idx] + else: + timestamp += duration + frame_idx += 1 + + finally: + im.seek(cur_idx) + + # Force encoder to flush frames + enc.add(None, round(timestamp), 0, 0, "", lossless, quality, alpha_quality, 0) + + # Get the final output from the encoder + data = enc.assemble(icc_profile, exif, xmp) + if data is None: + msg = "cannot write file as WebP (encoder returned None)" + raise OSError(msg) + + fp.write(data) + + +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + lossless = im.encoderinfo.get("lossless", False) + quality = im.encoderinfo.get("quality", 80) + alpha_quality = im.encoderinfo.get("alpha_quality", 100) + icc_profile = im.encoderinfo.get("icc_profile") or "" + exif = im.encoderinfo.get("exif", b"") + if isinstance(exif, Image.Exif): + exif = exif.tobytes() + if exif.startswith(b"Exif\x00\x00"): + exif = exif[6:] + xmp = im.encoderinfo.get("xmp", "") + method = im.encoderinfo.get("method", 4) + exact = 1 if im.encoderinfo.get("exact") else 0 + + if im.mode not in _VALID_WEBP_LEGACY_MODES: + im = im.convert("RGBA" if im.has_transparency_data else "RGB") + + data = _webp.WebPEncode( + im.tobytes(), + im.size[0], + im.size[1], + lossless, + float(quality), + float(alpha_quality), + im.mode, + icc_profile, + method, + exact, + exif, + xmp, + ) + if data is None: + msg = "cannot write file as WebP (encoder returned None)" + raise OSError(msg) + + fp.write(data) + + +Image.register_open(WebPImageFile.format, WebPImageFile, _accept) +if SUPPORTED: + Image.register_save(WebPImageFile.format, _save) + if _webp.HAVE_WEBPANIM: + Image.register_save_all(WebPImageFile.format, _save_all) + Image.register_extension(WebPImageFile.format, ".webp") + Image.register_mime(WebPImageFile.format, "image/webp") diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py index 3d5cddcc8..191935228 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -1,181 +1,181 @@ -# -# The Python Imaging Library -# $Id$ -# -# WMF stub codec -# -# history: -# 1996-12-14 fl Created -# 2004-02-22 fl Turned into a stub driver -# 2004-02-23 fl Added EMF support -# -# Copyright (c) Secret Labs AB 1997-2004. All rights reserved. -# Copyright (c) Fredrik Lundh 1996. -# -# See the README file for information on usage and redistribution. -# -# WMF/EMF reference documentation: -# https://winprotocoldoc.blob.core.windows.net/productionwindowsarchives/MS-WMF/[MS-WMF].pdf -# http://wvware.sourceforge.net/caolan/index.html -# http://wvware.sourceforge.net/caolan/ora-wmf.html -from __future__ import annotations - -from typing import IO - -from . import Image, ImageFile -from ._binary import i16le as word -from ._binary import si16le as short -from ._binary import si32le as _long - -_handler = None - - -def register_handler(handler: ImageFile.StubHandler | None) -> None: - """ - Install application-specific WMF image handler. - - :param handler: Handler object. - """ - global _handler - _handler = handler - - -if hasattr(Image.core, "drawwmf"): - # install default handler (windows only) - - class WmfHandler(ImageFile.StubHandler): - def open(self, im: ImageFile.StubImageFile) -> None: - im._mode = "RGB" - self.bbox = im.info["wmf_bbox"] - - def load(self, im: ImageFile.StubImageFile) -> Image.Image: - im.fp.seek(0) # rewind - return Image.frombytes( - "RGB", - im.size, - Image.core.drawwmf(im.fp.read(), im.size, self.bbox), - "raw", - "BGR", - (im.size[0] * 3 + 3) & -4, - -1, - ) - - register_handler(WmfHandler()) - -# -# -------------------------------------------------------------------- -# Read WMF file - - -def _accept(prefix: bytes) -> bool: - return ( - prefix[:6] == b"\xd7\xcd\xc6\x9a\x00\x00" or prefix[:4] == b"\x01\x00\x00\x00" - ) - - -## -# Image plugin for Windows metafiles. - - -class WmfStubImageFile(ImageFile.StubImageFile): - format = "WMF" - format_description = "Windows Metafile" - - def _open(self) -> None: - self._inch = None - - # check placable header - s = self.fp.read(80) - - if s[:6] == b"\xd7\xcd\xc6\x9a\x00\x00": - # placeable windows metafile - - # get units per inch - self._inch = word(s, 14) - - # get bounding box - x0 = short(s, 6) - y0 = short(s, 8) - x1 = short(s, 10) - y1 = short(s, 12) - - # 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, - ) - - self.info["wmf_bbox"] = x0, y0, x1, y1 - - # sanity check (standard metafile header) - if s[22:26] != b"\x01\x00\t\x00": - msg = "Unsupported WMF file format" - raise SyntaxError(msg) - - elif s[:4] == b"\x01\x00\x00\x00" and s[40:44] == b" EMF": - # enhanced metafile - - # get bounding box - x0 = _long(s, 8) - y0 = _long(s, 12) - x1 = _long(s, 16) - y1 = _long(s, 20) - - # get frame (in 0.01 millimeter units) - frame = _long(s, 24), _long(s, 28), _long(s, 32), _long(s, 36) - - size = x1 - x0, y1 - y0 - - # calculate dots per inch from bbox and frame - xdpi = 2540.0 * (x1 - y0) / (frame[2] - frame[0]) - ydpi = 2540.0 * (y1 - y0) / (frame[3] - frame[1]) - - self.info["wmf_bbox"] = x0, y0, x1, y1 - - if xdpi == ydpi: - self.info["dpi"] = xdpi - else: - self.info["dpi"] = xdpi, ydpi - - else: - msg = "Unsupported file format" - raise SyntaxError(msg) - - self._mode = "RGB" - self._size = size - - loader = self._load() - if loader: - loader.open(self) - - def _load(self) -> ImageFile.StubHandler | None: - return _handler - - def load(self, dpi=None): - if dpi is not None and self._inch is not None: - self.info["dpi"] = dpi - x0, y0, x1, y1 = self.info["wmf_bbox"] - self._size = ( - (x1 - x0) * self.info["dpi"] // self._inch, - (y1 - y0) * self.info["dpi"] // self._inch, - ) - return super().load() - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - if _handler is None or not hasattr(_handler, "save"): - msg = "WMF save handler not installed" - raise OSError(msg) - _handler.save(im, fp, filename) - - -# -# -------------------------------------------------------------------- -# Registry stuff - - -Image.register_open(WmfStubImageFile.format, WmfStubImageFile, _accept) -Image.register_save(WmfStubImageFile.format, _save) - -Image.register_extensions(WmfStubImageFile.format, [".wmf", ".emf"]) +# +# The Python Imaging Library +# $Id$ +# +# WMF stub codec +# +# history: +# 1996-12-14 fl Created +# 2004-02-22 fl Turned into a stub driver +# 2004-02-23 fl Added EMF support +# +# Copyright (c) Secret Labs AB 1997-2004. All rights reserved. +# Copyright (c) Fredrik Lundh 1996. +# +# See the README file for information on usage and redistribution. +# +# WMF/EMF reference documentation: +# https://winprotocoldoc.blob.core.windows.net/productionwindowsarchives/MS-WMF/[MS-WMF].pdf +# http://wvware.sourceforge.net/caolan/index.html +# http://wvware.sourceforge.net/caolan/ora-wmf.html +from __future__ import annotations + +from typing import IO + +from . import Image, ImageFile +from ._binary import i16le as word +from ._binary import si16le as short +from ._binary import si32le as _long + +_handler = None + + +def register_handler(handler: ImageFile.StubHandler | None) -> None: + """ + Install application-specific WMF image handler. + + :param handler: Handler object. + """ + global _handler + _handler = handler + + +if hasattr(Image.core, "drawwmf"): + # install default handler (windows only) + + class WmfHandler(ImageFile.StubHandler): + def open(self, im: ImageFile.StubImageFile) -> None: + im._mode = "RGB" + self.bbox = im.info["wmf_bbox"] + + def load(self, im: ImageFile.StubImageFile) -> Image.Image: + im.fp.seek(0) # rewind + return Image.frombytes( + "RGB", + im.size, + Image.core.drawwmf(im.fp.read(), im.size, self.bbox), + "raw", + "BGR", + (im.size[0] * 3 + 3) & -4, + -1, + ) + + register_handler(WmfHandler()) + +# +# -------------------------------------------------------------------- +# Read WMF file + + +def _accept(prefix: bytes) -> bool: + return ( + prefix[:6] == b"\xd7\xcd\xc6\x9a\x00\x00" or prefix[:4] == b"\x01\x00\x00\x00" + ) + + +## +# Image plugin for Windows metafiles. + + +class WmfStubImageFile(ImageFile.StubImageFile): + format = "WMF" + format_description = "Windows Metafile" + + def _open(self) -> None: + self._inch = None + + # check placable header + s = self.fp.read(80) + + if s[:6] == b"\xd7\xcd\xc6\x9a\x00\x00": + # placeable windows metafile + + # get units per inch + self._inch = word(s, 14) + + # get bounding box + x0 = short(s, 6) + y0 = short(s, 8) + x1 = short(s, 10) + y1 = short(s, 12) + + # 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, + ) + + self.info["wmf_bbox"] = x0, y0, x1, y1 + + # sanity check (standard metafile header) + if s[22:26] != b"\x01\x00\t\x00": + msg = "Unsupported WMF file format" + raise SyntaxError(msg) + + elif s[:4] == b"\x01\x00\x00\x00" and s[40:44] == b" EMF": + # enhanced metafile + + # get bounding box + x0 = _long(s, 8) + y0 = _long(s, 12) + x1 = _long(s, 16) + y1 = _long(s, 20) + + # get frame (in 0.01 millimeter units) + frame = _long(s, 24), _long(s, 28), _long(s, 32), _long(s, 36) + + size = x1 - x0, y1 - y0 + + # calculate dots per inch from bbox and frame + xdpi = 2540.0 * (x1 - y0) / (frame[2] - frame[0]) + ydpi = 2540.0 * (y1 - y0) / (frame[3] - frame[1]) + + self.info["wmf_bbox"] = x0, y0, x1, y1 + + if xdpi == ydpi: + self.info["dpi"] = xdpi + else: + self.info["dpi"] = xdpi, ydpi + + else: + msg = "Unsupported file format" + raise SyntaxError(msg) + + self._mode = "RGB" + self._size = size + + loader = self._load() + if loader: + loader.open(self) + + def _load(self) -> ImageFile.StubHandler | None: + return _handler + + def load(self, dpi=None): + if dpi is not None and self._inch is not None: + self.info["dpi"] = dpi + x0, y0, x1, y1 = self.info["wmf_bbox"] + self._size = ( + (x1 - x0) * self.info["dpi"] // self._inch, + (y1 - y0) * self.info["dpi"] // self._inch, + ) + return super().load() + + +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + if _handler is None or not hasattr(_handler, "save"): + msg = "WMF save handler not installed" + raise OSError(msg) + _handler.save(im, fp, filename) + + +# +# -------------------------------------------------------------------- +# Registry stuff + + +Image.register_open(WmfStubImageFile.format, WmfStubImageFile, _accept) +Image.register_save(WmfStubImageFile.format, _save) + +Image.register_extensions(WmfStubImageFile.format, [".wmf", ".emf"]) diff --git a/src/PIL/XVThumbImagePlugin.py b/src/PIL/XVThumbImagePlugin.py index c84adaca2..eb86cd671 100644 --- a/src/PIL/XVThumbImagePlugin.py +++ b/src/PIL/XVThumbImagePlugin.py @@ -1,81 +1,81 @@ -# -# The Python Imaging Library. -# $Id$ -# -# XV Thumbnail file handler by Charles E. "Gene" Cash -# (gcash@magicnet.net) -# -# see xvcolor.c and xvbrowse.c in the sources to John Bradley's XV, -# available from ftp://ftp.cis.upenn.edu/pub/xv/ -# -# history: -# 98-08-15 cec created (b/w only) -# 98-12-09 cec added color palette -# 98-12-28 fl added to PIL (with only a few very minor modifications) -# -# To do: -# FIXME: make save work (this requires quantization support) -# -from __future__ import annotations - -from . import Image, ImageFile, ImagePalette -from ._binary import o8 - -_MAGIC = b"P7 332" - -# standard color palette for thumbnails (RGB332) -PALETTE = b"" -for r in range(8): - for g in range(8): - for b in range(4): - PALETTE = PALETTE + ( - o8((r * 255) // 7) + o8((g * 255) // 7) + o8((b * 255) // 3) - ) - - -def _accept(prefix: bytes) -> bool: - return prefix[:6] == _MAGIC - - -## -# Image plugin for XV thumbnail images. - - -class XVThumbImageFile(ImageFile.ImageFile): - format = "XVThumb" - format_description = "XV thumbnail image" - - def _open(self) -> None: - # check magic - assert self.fp is not None - - if not _accept(self.fp.read(6)): - msg = "not an XV thumbnail file" - raise SyntaxError(msg) - - # Skip to beginning of next line - self.fp.readline() - - # skip info comments - while True: - s = self.fp.readline() - if not s: - msg = "Unexpected EOF reading XV thumbnail file" - raise SyntaxError(msg) - if s[0] != 35: # ie. when not a comment: '#' - break - - # parse header line (already read) - s = s.strip().split() - - self._mode = "P" - self._size = int(s[0]), int(s[1]) - - self.palette = ImagePalette.raw("RGB", PALETTE) - - self.tile = [("raw", (0, 0) + self.size, self.fp.tell(), (self.mode, 0, 1))] - - -# -------------------------------------------------------------------- - -Image.register_open(XVThumbImageFile.format, XVThumbImageFile, _accept) +# +# The Python Imaging Library. +# $Id$ +# +# XV Thumbnail file handler by Charles E. "Gene" Cash +# (gcash@magicnet.net) +# +# see xvcolor.c and xvbrowse.c in the sources to John Bradley's XV, +# available from ftp://ftp.cis.upenn.edu/pub/xv/ +# +# history: +# 98-08-15 cec created (b/w only) +# 98-12-09 cec added color palette +# 98-12-28 fl added to PIL (with only a few very minor modifications) +# +# To do: +# FIXME: make save work (this requires quantization support) +# +from __future__ import annotations + +from . import Image, ImageFile, ImagePalette +from ._binary import o8 + +_MAGIC = b"P7 332" + +# standard color palette for thumbnails (RGB332) +PALETTE = b"" +for r in range(8): + for g in range(8): + for b in range(4): + PALETTE = PALETTE + ( + o8((r * 255) // 7) + o8((g * 255) // 7) + o8((b * 255) // 3) + ) + + +def _accept(prefix: bytes) -> bool: + return prefix[:6] == _MAGIC + + +## +# Image plugin for XV thumbnail images. + + +class XVThumbImageFile(ImageFile.ImageFile): + format = "XVThumb" + format_description = "XV thumbnail image" + + def _open(self) -> None: + # check magic + assert self.fp is not None + + if not _accept(self.fp.read(6)): + msg = "not an XV thumbnail file" + raise SyntaxError(msg) + + # Skip to beginning of next line + self.fp.readline() + + # skip info comments + while True: + s = self.fp.readline() + if not s: + msg = "Unexpected EOF reading XV thumbnail file" + raise SyntaxError(msg) + if s[0] != 35: # ie. when not a comment: '#' + break + + # parse header line (already read) + s = s.strip().split() + + self._mode = "P" + self._size = int(s[0]), int(s[1]) + + self.palette = ImagePalette.raw("RGB", PALETTE) + + self.tile = [("raw", (0, 0) + self.size, self.fp.tell(), (self.mode, 0, 1))] + + +# -------------------------------------------------------------------- + +Image.register_open(XVThumbImageFile.format, XVThumbImageFile, _accept) diff --git a/src/PIL/XbmImagePlugin.py b/src/PIL/XbmImagePlugin.py index 6d11bbfcf..25b8bd92a 100644 --- a/src/PIL/XbmImagePlugin.py +++ b/src/PIL/XbmImagePlugin.py @@ -1,98 +1,98 @@ -# -# The Python Imaging Library. -# $Id$ -# -# XBM File handling -# -# History: -# 1995-09-08 fl Created -# 1996-11-01 fl Added save support -# 1997-07-07 fl Made header parser more tolerant -# 1997-07-22 fl Fixed yet another parser bug -# 2001-02-17 fl Use 're' instead of 'regex' (Python 2.1) (0.4) -# 2001-05-13 fl Added hotspot handling (based on code from Bernhard Herzog) -# 2004-02-24 fl Allow some whitespace before first #define -# -# Copyright (c) 1997-2004 by Secret Labs AB -# Copyright (c) 1996-1997 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import re -from typing import IO - -from . import Image, ImageFile - -# XBM header -xbm_head = re.compile( - rb"\s*#define[ \t]+.*_width[ \t]+(?P[0-9]+)[\r\n]+" - b"#define[ \t]+.*_height[ \t]+(?P[0-9]+)[\r\n]+" - b"(?P" - b"#define[ \t]+[^_]*_x_hot[ \t]+(?P[0-9]+)[\r\n]+" - b"#define[ \t]+[^_]*_y_hot[ \t]+(?P[0-9]+)[\r\n]+" - b")?" - rb"[\000-\377]*_bits\[]" -) - - -def _accept(prefix: bytes) -> bool: - return prefix.lstrip()[:7] == b"#define" - - -## -# Image plugin for X11 bitmaps. - - -class XbmImageFile(ImageFile.ImageFile): - format = "XBM" - format_description = "X11 Bitmap" - - def _open(self) -> None: - assert self.fp is not None - - m = xbm_head.match(self.fp.read(512)) - - if not m: - msg = "not a XBM file" - raise SyntaxError(msg) - - xsize = int(m.group("width")) - ysize = int(m.group("height")) - - if m.group("hotspot"): - self.info["hotspot"] = (int(m.group("xhot")), int(m.group("yhot"))) - - self._mode = "1" - self._size = xsize, ysize - - self.tile = [("xbm", (0, 0) + self.size, m.end(), None)] - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - if im.mode != "1": - msg = f"cannot write mode {im.mode} as XBM" - raise OSError(msg) - - fp.write(f"#define im_width {im.size[0]}\n".encode("ascii")) - fp.write(f"#define im_height {im.size[1]}\n".encode("ascii")) - - hotspot = im.encoderinfo.get("hotspot") - if hotspot: - fp.write(f"#define im_x_hot {hotspot[0]}\n".encode("ascii")) - fp.write(f"#define im_y_hot {hotspot[1]}\n".encode("ascii")) - - fp.write(b"static char im_bits[] = {\n") - - ImageFile._save(im, fp, [("xbm", (0, 0) + im.size, 0, None)]) - - fp.write(b"};\n") - - -Image.register_open(XbmImageFile.format, XbmImageFile, _accept) -Image.register_save(XbmImageFile.format, _save) - -Image.register_extension(XbmImageFile.format, ".xbm") - -Image.register_mime(XbmImageFile.format, "image/xbm") +# +# The Python Imaging Library. +# $Id$ +# +# XBM File handling +# +# History: +# 1995-09-08 fl Created +# 1996-11-01 fl Added save support +# 1997-07-07 fl Made header parser more tolerant +# 1997-07-22 fl Fixed yet another parser bug +# 2001-02-17 fl Use 're' instead of 'regex' (Python 2.1) (0.4) +# 2001-05-13 fl Added hotspot handling (based on code from Bernhard Herzog) +# 2004-02-24 fl Allow some whitespace before first #define +# +# Copyright (c) 1997-2004 by Secret Labs AB +# Copyright (c) 1996-1997 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import re +from typing import IO + +from . import Image, ImageFile + +# XBM header +xbm_head = re.compile( + rb"\s*#define[ \t]+.*_width[ \t]+(?P[0-9]+)[\r\n]+" + b"#define[ \t]+.*_height[ \t]+(?P[0-9]+)[\r\n]+" + b"(?P" + b"#define[ \t]+[^_]*_x_hot[ \t]+(?P[0-9]+)[\r\n]+" + b"#define[ \t]+[^_]*_y_hot[ \t]+(?P[0-9]+)[\r\n]+" + b")?" + rb"[\000-\377]*_bits\[]" +) + + +def _accept(prefix: bytes) -> bool: + return prefix.lstrip()[:7] == b"#define" + + +## +# Image plugin for X11 bitmaps. + + +class XbmImageFile(ImageFile.ImageFile): + format = "XBM" + format_description = "X11 Bitmap" + + def _open(self) -> None: + assert self.fp is not None + + m = xbm_head.match(self.fp.read(512)) + + if not m: + msg = "not a XBM file" + raise SyntaxError(msg) + + xsize = int(m.group("width")) + ysize = int(m.group("height")) + + if m.group("hotspot"): + self.info["hotspot"] = (int(m.group("xhot")), int(m.group("yhot"))) + + self._mode = "1" + self._size = xsize, ysize + + self.tile = [("xbm", (0, 0) + self.size, m.end(), None)] + + +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + if im.mode != "1": + msg = f"cannot write mode {im.mode} as XBM" + raise OSError(msg) + + fp.write(f"#define im_width {im.size[0]}\n".encode("ascii")) + fp.write(f"#define im_height {im.size[1]}\n".encode("ascii")) + + hotspot = im.encoderinfo.get("hotspot") + if hotspot: + fp.write(f"#define im_x_hot {hotspot[0]}\n".encode("ascii")) + fp.write(f"#define im_y_hot {hotspot[1]}\n".encode("ascii")) + + fp.write(b"static char im_bits[] = {\n") + + ImageFile._save(im, fp, [("xbm", (0, 0) + im.size, 0, None)]) + + fp.write(b"};\n") + + +Image.register_open(XbmImageFile.format, XbmImageFile, _accept) +Image.register_save(XbmImageFile.format, _save) + +Image.register_extension(XbmImageFile.format, ".xbm") + +Image.register_mime(XbmImageFile.format, "image/xbm") diff --git a/src/PIL/XpmImagePlugin.py b/src/PIL/XpmImagePlugin.py index 8d56331e6..b2e4d9065 100644 --- a/src/PIL/XpmImagePlugin.py +++ b/src/PIL/XpmImagePlugin.py @@ -1,125 +1,125 @@ -# -# The Python Imaging Library. -# $Id$ -# -# XPM File handling -# -# History: -# 1996-12-29 fl Created -# 2001-02-17 fl Use 're' instead of 'regex' (Python 2.1) (0.7) -# -# Copyright (c) Secret Labs AB 1997-2001. -# Copyright (c) Fredrik Lundh 1996-2001. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import re - -from . import Image, ImageFile, ImagePalette -from ._binary import o8 - -# XPM header -xpm_head = re.compile(b'"([0-9]*) ([0-9]*) ([0-9]*) ([0-9]*)') - - -def _accept(prefix: bytes) -> bool: - return prefix[:9] == b"/* XPM */" - - -## -# Image plugin for X11 pixel maps. - - -class XpmImageFile(ImageFile.ImageFile): - format = "XPM" - format_description = "X11 Pixel Map" - - def _open(self) -> 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: - msg = "broken XPM file" - raise SyntaxError(msg) - m = xpm_head.match(s) - if m: - break - - self._size = int(m.group(1)), int(m.group(2)) - - pal = 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 - - 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] - - c = s[1] - s = s[2:-2].split() - - for i in range(0, len(s), 2): - if s[i] == b"c": - # process colour key - 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) - palette[c] = ( - o8((rgb >> 16) & 255) + o8((rgb >> 8) & 255) + o8(rgb & 255) - ) - else: - # unknown colour - msg = "cannot read this XPM file" - raise ValueError(msg) - break - - else: - # missing colour key - msg = "cannot read this XPM file" - raise ValueError(msg) - - self._mode = "P" - self.palette = ImagePalette.raw("RGB", b"".join(palette)) - - self.tile = [("raw", (0, 0) + self.size, self.fp.tell(), ("P", 0, 1))] - - def load_read(self, read_bytes: int) -> bytes: - # - # load all image data in one chunk - - xsize, ysize = self.size - - s = [self.fp.readline()[1 : xsize + 1].ljust(xsize) for i in range(ysize)] - - return b"".join(s) - - -# -# Registry - - -Image.register_open(XpmImageFile.format, XpmImageFile, _accept) - -Image.register_extension(XpmImageFile.format, ".xpm") - -Image.register_mime(XpmImageFile.format, "image/xpm") +# +# The Python Imaging Library. +# $Id$ +# +# XPM File handling +# +# History: +# 1996-12-29 fl Created +# 2001-02-17 fl Use 're' instead of 'regex' (Python 2.1) (0.7) +# +# Copyright (c) Secret Labs AB 1997-2001. +# Copyright (c) Fredrik Lundh 1996-2001. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import re + +from . import Image, ImageFile, ImagePalette +from ._binary import o8 + +# XPM header +xpm_head = re.compile(b'"([0-9]*) ([0-9]*) ([0-9]*) ([0-9]*)') + + +def _accept(prefix: bytes) -> bool: + return prefix[:9] == b"/* XPM */" + + +## +# Image plugin for X11 pixel maps. + + +class XpmImageFile(ImageFile.ImageFile): + format = "XPM" + format_description = "X11 Pixel Map" + + def _open(self) -> 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: + msg = "broken XPM file" + raise SyntaxError(msg) + m = xpm_head.match(s) + if m: + break + + self._size = int(m.group(1)), int(m.group(2)) + + pal = 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 + + 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] + + c = s[1] + s = s[2:-2].split() + + for i in range(0, len(s), 2): + if s[i] == b"c": + # process colour key + 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) + palette[c] = ( + o8((rgb >> 16) & 255) + o8((rgb >> 8) & 255) + o8(rgb & 255) + ) + else: + # unknown colour + msg = "cannot read this XPM file" + raise ValueError(msg) + break + + else: + # missing colour key + msg = "cannot read this XPM file" + raise ValueError(msg) + + self._mode = "P" + self.palette = ImagePalette.raw("RGB", b"".join(palette)) + + self.tile = [("raw", (0, 0) + self.size, self.fp.tell(), ("P", 0, 1))] + + def load_read(self, read_bytes: int) -> bytes: + # + # load all image data in one chunk + + xsize, ysize = self.size + + s = [self.fp.readline()[1 : xsize + 1].ljust(xsize) for i in range(ysize)] + + return b"".join(s) + + +# +# Registry + + +Image.register_open(XpmImageFile.format, XpmImageFile, _accept) + +Image.register_extension(XpmImageFile.format, ".xpm") + +Image.register_mime(XpmImageFile.format, "image/xpm") diff --git a/src/PIL/__init__.py b/src/PIL/__init__.py index 09546fe63..996ecd99e 100644 --- a/src/PIL/__init__.py +++ b/src/PIL/__init__.py @@ -1,86 +1,86 @@ -"""Pillow (Fork of the Python Imaging Library) - -Pillow is the friendly PIL fork by Jeffrey A. Clark and contributors. - https://github.com/python-pillow/Pillow/ - -Pillow is forked from PIL 1.1.7. - -PIL is the Python Imaging Library by Fredrik Lundh and contributors. -Copyright (c) 1999 by Secret Labs AB. - -Use PIL.__version__ for this Pillow version. - -;-) -""" - -from __future__ import annotations - -from . import _version - -# VERSION was removed in Pillow 6.0.0. -# PILLOW_VERSION was removed in Pillow 9.0.0. -# Use __version__ instead. -__version__ = _version.__version__ -del _version - - -_plugins = [ - "BlpImagePlugin", - "BmpImagePlugin", - "BufrStubImagePlugin", - "CurImagePlugin", - "DcxImagePlugin", - "DdsImagePlugin", - "EpsImagePlugin", - "FitsImagePlugin", - "FliImagePlugin", - "FpxImagePlugin", - "FtexImagePlugin", - "GbrImagePlugin", - "GifImagePlugin", - "GribStubImagePlugin", - "Hdf5StubImagePlugin", - "IcnsImagePlugin", - "IcoImagePlugin", - "ImImagePlugin", - "ImtImagePlugin", - "IptcImagePlugin", - "JpegImagePlugin", - "Jpeg2KImagePlugin", - "McIdasImagePlugin", - "MicImagePlugin", - "MpegImagePlugin", - "MpoImagePlugin", - "MspImagePlugin", - "PalmImagePlugin", - "PcdImagePlugin", - "PcxImagePlugin", - "PdfImagePlugin", - "PixarImagePlugin", - "PngImagePlugin", - "PpmImagePlugin", - "PsdImagePlugin", - "QoiImagePlugin", - "SgiImagePlugin", - "SpiderImagePlugin", - "SunImagePlugin", - "TgaImagePlugin", - "TiffImagePlugin", - "WebPImagePlugin", - "WmfImagePlugin", - "XbmImagePlugin", - "XpmImagePlugin", - "XVThumbImagePlugin", -] - - -class UnidentifiedImageError(OSError): - """ - Raised in :py:meth:`PIL.Image.open` if an image cannot be opened and identified. - - If a PNG image raises this error, setting :data:`.ImageFile.LOAD_TRUNCATED_IMAGES` - to true may allow the image to be opened after all. The setting will ignore missing - data and checksum failures. - """ - - pass +"""Pillow (Fork of the Python Imaging Library) + +Pillow is the friendly PIL fork by Jeffrey A. Clark and contributors. + https://github.com/python-pillow/Pillow/ + +Pillow is forked from PIL 1.1.7. + +PIL is the Python Imaging Library by Fredrik Lundh and contributors. +Copyright (c) 1999 by Secret Labs AB. + +Use PIL.__version__ for this Pillow version. + +;-) +""" + +from __future__ import annotations + +from . import _version + +# VERSION was removed in Pillow 6.0.0. +# PILLOW_VERSION was removed in Pillow 9.0.0. +# Use __version__ instead. +__version__ = _version.__version__ +del _version + + +_plugins = [ + "BlpImagePlugin", + "BmpImagePlugin", + "BufrStubImagePlugin", + "CurImagePlugin", + "DcxImagePlugin", + "DdsImagePlugin", + "EpsImagePlugin", + "FitsImagePlugin", + "FliImagePlugin", + "FpxImagePlugin", + "FtexImagePlugin", + "GbrImagePlugin", + "GifImagePlugin", + "GribStubImagePlugin", + "Hdf5StubImagePlugin", + "IcnsImagePlugin", + "IcoImagePlugin", + "ImImagePlugin", + "ImtImagePlugin", + "IptcImagePlugin", + "JpegImagePlugin", + "Jpeg2KImagePlugin", + "McIdasImagePlugin", + "MicImagePlugin", + "MpegImagePlugin", + "MpoImagePlugin", + "MspImagePlugin", + "PalmImagePlugin", + "PcdImagePlugin", + "PcxImagePlugin", + "PdfImagePlugin", + "PixarImagePlugin", + "PngImagePlugin", + "PpmImagePlugin", + "PsdImagePlugin", + "QoiImagePlugin", + "SgiImagePlugin", + "SpiderImagePlugin", + "SunImagePlugin", + "TgaImagePlugin", + "TiffImagePlugin", + "WebPImagePlugin", + "WmfImagePlugin", + "XbmImagePlugin", + "XpmImagePlugin", + "XVThumbImagePlugin", +] + + +class UnidentifiedImageError(OSError): + """ + Raised in :py:meth:`PIL.Image.open` if an image cannot be opened and identified. + + If a PNG image raises this error, setting :data:`.ImageFile.LOAD_TRUNCATED_IMAGES` + to true may allow the image to be opened after all. The setting will ignore missing + data and checksum failures. + """ + + pass diff --git a/src/PIL/__main__.py b/src/PIL/__main__.py index 043156e89..833a73959 100644 --- a/src/PIL/__main__.py +++ b/src/PIL/__main__.py @@ -1,7 +1,7 @@ -from __future__ import annotations - -import sys - -from .features import pilinfo - -pilinfo(supported_formats="--report" not in sys.argv) +from __future__ import annotations + +import sys + +from .features import pilinfo + +pilinfo(supported_formats="--report" not in sys.argv) diff --git a/src/PIL/_binary.py b/src/PIL/_binary.py index 4594ccce3..ad8863074 100644 --- a/src/PIL/_binary.py +++ b/src/PIL/_binary.py @@ -1,112 +1,112 @@ -# -# The Python Imaging Library. -# $Id$ -# -# Binary input/output support routines. -# -# Copyright (c) 1997-2003 by Secret Labs AB -# Copyright (c) 1995-2003 by Fredrik Lundh -# Copyright (c) 2012 by Brian Crowell -# -# See the README file for information on usage and redistribution. -# - - -"""Binary input/output support routines.""" -from __future__ import annotations - -from struct import pack, unpack_from - - -def i8(c: bytes) -> int: - return c[0] - - -def o8(i: int) -> bytes: - return bytes((i & 255,)) - - -# Input, le = little endian, be = big endian -def i16le(c: bytes, o: int = 0) -> int: - """ - Converts a 2-bytes (16 bits) string to an unsigned integer. - - :param c: string containing bytes to convert - :param o: offset of bytes to convert in string - """ - return unpack_from(" int: - """ - Converts a 2-bytes (16 bits) string to a signed integer. - - :param c: string containing bytes to convert - :param o: offset of bytes to convert in string - """ - return unpack_from(" int: - """ - Converts a 2-bytes (16 bits) string to a signed integer, big endian. - - :param c: string containing bytes to convert - :param o: offset of bytes to convert in string - """ - return unpack_from(">h", c, o)[0] - - -def i32le(c: bytes, o: int = 0) -> int: - """ - Converts a 4-bytes (32 bits) string to an unsigned integer. - - :param c: string containing bytes to convert - :param o: offset of bytes to convert in string - """ - return unpack_from(" int: - """ - Converts a 4-bytes (32 bits) string to a signed integer. - - :param c: string containing bytes to convert - :param o: offset of bytes to convert in string - """ - return unpack_from(" int: - """ - Converts a 4-bytes (32 bits) string to a signed integer, big endian. - - :param c: string containing bytes to convert - :param o: offset of bytes to convert in string - """ - return unpack_from(">i", c, o)[0] - - -def i16be(c: bytes, o: int = 0) -> int: - return unpack_from(">H", c, o)[0] - - -def i32be(c: bytes, o: int = 0) -> int: - return unpack_from(">I", c, o)[0] - - -# Output, le = little endian, be = big endian -def o16le(i: int) -> bytes: - return pack(" bytes: - return pack(" bytes: - return pack(">H", i) - - -def o32be(i: int) -> bytes: - return pack(">I", i) +# +# The Python Imaging Library. +# $Id$ +# +# Binary input/output support routines. +# +# Copyright (c) 1997-2003 by Secret Labs AB +# Copyright (c) 1995-2003 by Fredrik Lundh +# Copyright (c) 2012 by Brian Crowell +# +# See the README file for information on usage and redistribution. +# + + +"""Binary input/output support routines.""" +from __future__ import annotations + +from struct import pack, unpack_from + + +def i8(c: bytes) -> int: + return c[0] + + +def o8(i: int) -> bytes: + return bytes((i & 255,)) + + +# Input, le = little endian, be = big endian +def i16le(c: bytes, o: int = 0) -> int: + """ + Converts a 2-bytes (16 bits) string to an unsigned integer. + + :param c: string containing bytes to convert + :param o: offset of bytes to convert in string + """ + return unpack_from(" int: + """ + Converts a 2-bytes (16 bits) string to a signed integer. + + :param c: string containing bytes to convert + :param o: offset of bytes to convert in string + """ + return unpack_from(" int: + """ + Converts a 2-bytes (16 bits) string to a signed integer, big endian. + + :param c: string containing bytes to convert + :param o: offset of bytes to convert in string + """ + return unpack_from(">h", c, o)[0] + + +def i32le(c: bytes, o: int = 0) -> int: + """ + Converts a 4-bytes (32 bits) string to an unsigned integer. + + :param c: string containing bytes to convert + :param o: offset of bytes to convert in string + """ + return unpack_from(" int: + """ + Converts a 4-bytes (32 bits) string to a signed integer. + + :param c: string containing bytes to convert + :param o: offset of bytes to convert in string + """ + return unpack_from(" int: + """ + Converts a 4-bytes (32 bits) string to a signed integer, big endian. + + :param c: string containing bytes to convert + :param o: offset of bytes to convert in string + """ + return unpack_from(">i", c, o)[0] + + +def i16be(c: bytes, o: int = 0) -> int: + return unpack_from(">H", c, o)[0] + + +def i32be(c: bytes, o: int = 0) -> int: + return unpack_from(">I", c, o)[0] + + +# Output, le = little endian, be = big endian +def o16le(i: int) -> bytes: + return pack(" bytes: + return pack(" bytes: + return pack(">H", i) + + +def o32be(i: int) -> bytes: + return pack(">I", i) diff --git a/src/PIL/_deprecate.py b/src/PIL/_deprecate.py index 33a0e07b3..9f7addfd4 100644 --- a/src/PIL/_deprecate.py +++ b/src/PIL/_deprecate.py @@ -1,71 +1,71 @@ -from __future__ import annotations - -import warnings - -from . import __version__ - - -def deprecate( - deprecated: str, - when: int | None, - replacement: str | None = None, - *, - action: str | None = None, - plural: bool = False, -) -> None: - """ - Deprecations helper. - - :param deprecated: Name of thing to be deprecated. - :param when: Pillow major version to be removed in. - :param replacement: Name of replacement. - :param action: Instead of "replacement", give a custom call to action - e.g. "Upgrade to new thing". - :param plural: if the deprecated thing is plural, needing "are" instead of "is". - - Usually of the form: - - "[deprecated] is deprecated and will be removed in Pillow [when] (yyyy-mm-dd). - Use [replacement] instead." - - You can leave out the replacement sentence: - - "[deprecated] is deprecated and will be removed in Pillow [when] (yyyy-mm-dd)" - - Or with another call to action: - - "[deprecated] is deprecated and will be removed in Pillow [when] (yyyy-mm-dd). - [action]." - """ - - is_ = "are" if plural else "is" - - if when is None: - removed = "a future version" - elif when <= int(__version__.split(".")[0]): - msg = f"{deprecated} {is_} deprecated and should be removed." - raise RuntimeError(msg) - elif when == 11: - removed = "Pillow 11 (2024-10-15)" - elif when == 12: - removed = "Pillow 12 (2025-10-15)" - else: - msg = f"Unknown removal version: {when}. Update {__name__}?" - raise ValueError(msg) - - if replacement and action: - msg = "Use only one of 'replacement' and 'action'" - raise ValueError(msg) - - if replacement: - action = f". Use {replacement} instead." - elif action: - action = f". {action.rstrip('.')}." - else: - action = "" - - warnings.warn( - f"{deprecated} {is_} deprecated and will be removed in {removed}{action}", - DeprecationWarning, - stacklevel=3, - ) +from __future__ import annotations + +import warnings + +from . import __version__ + + +def deprecate( + deprecated: str, + when: int | None, + replacement: str | None = None, + *, + action: str | None = None, + plural: bool = False, +) -> None: + """ + Deprecations helper. + + :param deprecated: Name of thing to be deprecated. + :param when: Pillow major version to be removed in. + :param replacement: Name of replacement. + :param action: Instead of "replacement", give a custom call to action + e.g. "Upgrade to new thing". + :param plural: if the deprecated thing is plural, needing "are" instead of "is". + + Usually of the form: + + "[deprecated] is deprecated and will be removed in Pillow [when] (yyyy-mm-dd). + Use [replacement] instead." + + You can leave out the replacement sentence: + + "[deprecated] is deprecated and will be removed in Pillow [when] (yyyy-mm-dd)" + + Or with another call to action: + + "[deprecated] is deprecated and will be removed in Pillow [when] (yyyy-mm-dd). + [action]." + """ + + is_ = "are" if plural else "is" + + if when is None: + removed = "a future version" + elif when <= int(__version__.split(".")[0]): + msg = f"{deprecated} {is_} deprecated and should be removed." + raise RuntimeError(msg) + elif when == 11: + removed = "Pillow 11 (2024-10-15)" + elif when == 12: + removed = "Pillow 12 (2025-10-15)" + else: + msg = f"Unknown removal version: {when}. Update {__name__}?" + raise ValueError(msg) + + if replacement and action: + msg = "Use only one of 'replacement' and 'action'" + raise ValueError(msg) + + if replacement: + action = f". Use {replacement} instead." + elif action: + action = f". {action.rstrip('.')}." + else: + action = "" + + warnings.warn( + f"{deprecated} {is_} deprecated and will be removed in {removed}{action}", + DeprecationWarning, + stacklevel=3, + ) diff --git a/src/PIL/_imaging.pyi b/src/PIL/_imaging.pyi index 1fe954417..0d311ea04 100644 --- a/src/PIL/_imaging.pyi +++ b/src/PIL/_imaging.pyi @@ -1,16 +1,16 @@ -from typing import Any - -class ImagingCore: - def __getattr__(self, name: str) -> Any: ... - -class ImagingFont: - def __getattr__(self, name: str) -> Any: ... - -class ImagingDraw: - def __getattr__(self, name: str) -> Any: ... - -class PixelAccess: - def __getattr__(self, name: str) -> Any: ... - -def font(image, glyphdata: bytes) -> ImagingFont: ... -def __getattr__(name: str) -> Any: ... +from typing import Any + +class ImagingCore: + def __getattr__(self, name: str) -> Any: ... + +class ImagingFont: + def __getattr__(self, name: str) -> Any: ... + +class ImagingDraw: + def __getattr__(self, name: str) -> Any: ... + +class PixelAccess: + def __getattr__(self, name: str) -> Any: ... + +def font(image, glyphdata: bytes) -> ImagingFont: ... +def __getattr__(name: str) -> Any: ... diff --git a/src/PIL/_imagingcms.pyi b/src/PIL/_imagingcms.pyi index 2abd6d0f7..80d91614c 100644 --- a/src/PIL/_imagingcms.pyi +++ b/src/PIL/_imagingcms.pyi @@ -1,141 +1,141 @@ -import datetime -import sys -from typing import Literal, SupportsFloat, TypedDict - -littlecms_version: str | None - -_Tuple3f = tuple[float, float, float] -_Tuple2x3f = tuple[_Tuple3f, _Tuple3f] -_Tuple3x3f = tuple[_Tuple3f, _Tuple3f, _Tuple3f] - -class _IccMeasurementCondition(TypedDict): - observer: int - backing: _Tuple3f - geo: str - flare: float - illuminant_type: str - -class _IccViewingCondition(TypedDict): - illuminant: _Tuple3f - surround: _Tuple3f - illuminant_type: str - -class CmsProfile: - @property - def rendering_intent(self) -> int: ... - @property - def creation_date(self) -> datetime.datetime | None: ... - @property - def copyright(self) -> str | None: ... - @property - def target(self) -> str | None: ... - @property - def manufacturer(self) -> str | None: ... - @property - def model(self) -> str | None: ... - @property - def profile_description(self) -> str | None: ... - @property - def screening_description(self) -> str | None: ... - @property - def viewing_condition(self) -> str | None: ... - @property - def version(self) -> float: ... - @property - def icc_version(self) -> int: ... - @property - def attributes(self) -> int: ... - @property - def header_flags(self) -> int: ... - @property - def header_manufacturer(self) -> str: ... - @property - def header_model(self) -> str: ... - @property - def device_class(self) -> str: ... - @property - def connection_space(self) -> str: ... - @property - def xcolor_space(self) -> str: ... - @property - def profile_id(self) -> bytes: ... - @property - def is_matrix_shaper(self) -> bool: ... - @property - def technology(self) -> str | None: ... - @property - def colorimetric_intent(self) -> str | None: ... - @property - def perceptual_rendering_intent_gamut(self) -> str | None: ... - @property - def saturation_rendering_intent_gamut(self) -> str | None: ... - @property - def red_colorant(self) -> _Tuple2x3f | None: ... - @property - def green_colorant(self) -> _Tuple2x3f | None: ... - @property - def blue_colorant(self) -> _Tuple2x3f | None: ... - @property - def red_primary(self) -> _Tuple2x3f | None: ... - @property - def green_primary(self) -> _Tuple2x3f | None: ... - @property - def blue_primary(self) -> _Tuple2x3f | None: ... - @property - def media_white_point_temperature(self) -> float | None: ... - @property - def media_white_point(self) -> _Tuple2x3f | None: ... - @property - def media_black_point(self) -> _Tuple2x3f | None: ... - @property - def luminance(self) -> _Tuple2x3f | None: ... - @property - def chromatic_adaptation(self) -> tuple[_Tuple3x3f, _Tuple3x3f] | None: ... - @property - def chromaticity(self) -> _Tuple3x3f | None: ... - @property - def colorant_table(self) -> list[str] | None: ... - @property - def colorant_table_out(self) -> list[str] | None: ... - @property - def intent_supported(self) -> dict[int, tuple[bool, bool, bool]] | None: ... - @property - def clut(self) -> dict[int, tuple[bool, bool, bool]] | None: ... - @property - def icc_measurement_condition(self) -> _IccMeasurementCondition | None: ... - @property - def icc_viewing_condition(self) -> _IccViewingCondition | None: ... - def is_intent_supported(self, intent: int, direction: int, /) -> int: ... - -class CmsTransform: - def apply(self, id_in: int, id_out: int) -> int: ... - -def profile_open(profile: str, /) -> CmsProfile: ... -def profile_frombytes(profile: bytes, /) -> CmsProfile: ... -def profile_tobytes(profile: CmsProfile, /) -> bytes: ... -def buildTransform( - input_profile: CmsProfile, - output_profile: CmsProfile, - in_mode: str, - out_mode: str, - rendering_intent: int = 0, - cms_flags: int = 0, - /, -) -> CmsTransform: ... -def buildProofTransform( - input_profile: CmsProfile, - output_profile: CmsProfile, - proof_profile: CmsProfile, - in_mode: str, - out_mode: str, - rendering_intent: int = 0, - proof_intent: int = 0, - cms_flags: int = 0, - /, -) -> CmsTransform: ... -def createProfile( - color_space: Literal["LAB", "XYZ", "sRGB"], color_temp: SupportsFloat = 0.0, / -) -> CmsProfile: ... - -if sys.platform == "win32": - def get_display_profile_win32(handle: int = 0, is_dc: int = 0, /) -> str | None: ... +import datetime +import sys +from typing import Literal, SupportsFloat, TypedDict + +littlecms_version: str | None + +_Tuple3f = tuple[float, float, float] +_Tuple2x3f = tuple[_Tuple3f, _Tuple3f] +_Tuple3x3f = tuple[_Tuple3f, _Tuple3f, _Tuple3f] + +class _IccMeasurementCondition(TypedDict): + observer: int + backing: _Tuple3f + geo: str + flare: float + illuminant_type: str + +class _IccViewingCondition(TypedDict): + illuminant: _Tuple3f + surround: _Tuple3f + illuminant_type: str + +class CmsProfile: + @property + def rendering_intent(self) -> int: ... + @property + def creation_date(self) -> datetime.datetime | None: ... + @property + def copyright(self) -> str | None: ... + @property + def target(self) -> str | None: ... + @property + def manufacturer(self) -> str | None: ... + @property + def model(self) -> str | None: ... + @property + def profile_description(self) -> str | None: ... + @property + def screening_description(self) -> str | None: ... + @property + def viewing_condition(self) -> str | None: ... + @property + def version(self) -> float: ... + @property + def icc_version(self) -> int: ... + @property + def attributes(self) -> int: ... + @property + def header_flags(self) -> int: ... + @property + def header_manufacturer(self) -> str: ... + @property + def header_model(self) -> str: ... + @property + def device_class(self) -> str: ... + @property + def connection_space(self) -> str: ... + @property + def xcolor_space(self) -> str: ... + @property + def profile_id(self) -> bytes: ... + @property + def is_matrix_shaper(self) -> bool: ... + @property + def technology(self) -> str | None: ... + @property + def colorimetric_intent(self) -> str | None: ... + @property + def perceptual_rendering_intent_gamut(self) -> str | None: ... + @property + def saturation_rendering_intent_gamut(self) -> str | None: ... + @property + def red_colorant(self) -> _Tuple2x3f | None: ... + @property + def green_colorant(self) -> _Tuple2x3f | None: ... + @property + def blue_colorant(self) -> _Tuple2x3f | None: ... + @property + def red_primary(self) -> _Tuple2x3f | None: ... + @property + def green_primary(self) -> _Tuple2x3f | None: ... + @property + def blue_primary(self) -> _Tuple2x3f | None: ... + @property + def media_white_point_temperature(self) -> float | None: ... + @property + def media_white_point(self) -> _Tuple2x3f | None: ... + @property + def media_black_point(self) -> _Tuple2x3f | None: ... + @property + def luminance(self) -> _Tuple2x3f | None: ... + @property + def chromatic_adaptation(self) -> tuple[_Tuple3x3f, _Tuple3x3f] | None: ... + @property + def chromaticity(self) -> _Tuple3x3f | None: ... + @property + def colorant_table(self) -> list[str] | None: ... + @property + def colorant_table_out(self) -> list[str] | None: ... + @property + def intent_supported(self) -> dict[int, tuple[bool, bool, bool]] | None: ... + @property + def clut(self) -> dict[int, tuple[bool, bool, bool]] | None: ... + @property + def icc_measurement_condition(self) -> _IccMeasurementCondition | None: ... + @property + def icc_viewing_condition(self) -> _IccViewingCondition | None: ... + def is_intent_supported(self, intent: int, direction: int, /) -> int: ... + +class CmsTransform: + def apply(self, id_in: int, id_out: int) -> int: ... + +def profile_open(profile: str, /) -> CmsProfile: ... +def profile_frombytes(profile: bytes, /) -> CmsProfile: ... +def profile_tobytes(profile: CmsProfile, /) -> bytes: ... +def buildTransform( + input_profile: CmsProfile, + output_profile: CmsProfile, + in_mode: str, + out_mode: str, + rendering_intent: int = 0, + cms_flags: int = 0, + /, +) -> CmsTransform: ... +def buildProofTransform( + input_profile: CmsProfile, + output_profile: CmsProfile, + proof_profile: CmsProfile, + in_mode: str, + out_mode: str, + rendering_intent: int = 0, + proof_intent: int = 0, + cms_flags: int = 0, + /, +) -> CmsTransform: ... +def createProfile( + color_space: Literal["LAB", "XYZ", "sRGB"], color_temp: SupportsFloat = 0.0, / +) -> CmsProfile: ... + +if sys.platform == "win32": + def get_display_profile_win32(handle: int = 0, is_dc: int = 0, /) -> str | None: ... diff --git a/src/PIL/_imagingft.pyi b/src/PIL/_imagingft.pyi index 6e0ddd2f1..d88a07509 100644 --- a/src/PIL/_imagingft.pyi +++ b/src/PIL/_imagingft.pyi @@ -1,69 +1,69 @@ -from typing import Any, TypedDict - -from . import _imaging - -class _Axis(TypedDict): - minimum: int | None - default: int | None - maximum: int | None - name: bytes | None - -class Font: - @property - def family(self) -> str | None: ... - @property - def style(self) -> str | None: ... - @property - def ascent(self) -> int: ... - @property - def descent(self) -> int: ... - @property - def height(self) -> int: ... - @property - def x_ppem(self) -> int: ... - @property - def y_ppem(self) -> int: ... - @property - def glyphs(self) -> int: ... - def render( - self, - string: str, - fill, - mode=..., - dir=..., - features=..., - lang=..., - stroke_width=..., - anchor=..., - foreground_ink_long=..., - x_start=..., - y_start=..., - /, - ) -> tuple[_imaging.ImagingCore, tuple[int, int]]: ... - def getsize( - self, - string: str | bytes | bytearray, - mode=..., - dir=..., - features=..., - lang=..., - anchor=..., - /, - ) -> tuple[tuple[int, int], tuple[int, int]]: ... - def getlength( - self, string: str, mode=..., dir=..., features=..., lang=..., / - ) -> float: ... - def getvarnames(self) -> list[bytes]: ... - def getvaraxes(self) -> list[_Axis] | None: ... - def setvarname(self, instance_index: int, /) -> None: ... - def setvaraxes(self, axes: list[float], /) -> None: ... - -def getfont( - filename: str | bytes, - size: float, - index=..., - encoding=..., - font_bytes=..., - layout_engine=..., -) -> Font: ... -def __getattr__(name: str) -> Any: ... +from typing import Any, TypedDict + +from . import _imaging + +class _Axis(TypedDict): + minimum: int | None + default: int | None + maximum: int | None + name: bytes | None + +class Font: + @property + def family(self) -> str | None: ... + @property + def style(self) -> str | None: ... + @property + def ascent(self) -> int: ... + @property + def descent(self) -> int: ... + @property + def height(self) -> int: ... + @property + def x_ppem(self) -> int: ... + @property + def y_ppem(self) -> int: ... + @property + def glyphs(self) -> int: ... + def render( + self, + string: str, + fill, + mode=..., + dir=..., + features=..., + lang=..., + stroke_width=..., + anchor=..., + foreground_ink_long=..., + x_start=..., + y_start=..., + /, + ) -> tuple[_imaging.ImagingCore, tuple[int, int]]: ... + def getsize( + self, + string: str | bytes | bytearray, + mode=..., + dir=..., + features=..., + lang=..., + anchor=..., + /, + ) -> tuple[tuple[int, int], tuple[int, int]]: ... + def getlength( + self, string: str, mode=..., dir=..., features=..., lang=..., / + ) -> float: ... + def getvarnames(self) -> list[bytes]: ... + def getvaraxes(self) -> list[_Axis] | None: ... + def setvarname(self, instance_index: int, /) -> None: ... + def setvaraxes(self, axes: list[float], /) -> None: ... + +def getfont( + filename: str | bytes, + size: float, + index=..., + encoding=..., + font_bytes=..., + layout_engine=..., +) -> Font: ... +def __getattr__(name: str) -> Any: ... diff --git a/src/PIL/_imagingmath.pyi b/src/PIL/_imagingmath.pyi index e27843e53..9342f10b9 100644 --- a/src/PIL/_imagingmath.pyi +++ b/src/PIL/_imagingmath.pyi @@ -1,3 +1,3 @@ -from typing import Any - -def __getattr__(name: str) -> Any: ... +from typing import Any + +def __getattr__(name: str) -> Any: ... diff --git a/src/PIL/_imagingmorph.pyi b/src/PIL/_imagingmorph.pyi index e27843e53..9342f10b9 100644 --- a/src/PIL/_imagingmorph.pyi +++ b/src/PIL/_imagingmorph.pyi @@ -1,3 +1,3 @@ -from typing import Any - -def __getattr__(name: str) -> Any: ... +from typing import Any + +def __getattr__(name: str) -> Any: ... diff --git a/src/PIL/_tkinter_finder.py b/src/PIL/_tkinter_finder.py index beddfb062..8cc85a7a4 100644 --- a/src/PIL/_tkinter_finder.py +++ b/src/PIL/_tkinter_finder.py @@ -1,21 +1,21 @@ -""" Find compiled module linking to Tcl / Tk libraries -""" - -from __future__ import annotations - -import sys -import tkinter - -tk = getattr(tkinter, "_tkinter") - -try: - if hasattr(sys, "pypy_find_executable"): - TKINTER_LIB = tk.tklib_cffi.__file__ - else: - TKINTER_LIB = tk.__file__ -except AttributeError: - # _tkinter may be compiled directly into Python, in which case __file__ is - # not available. load_tkinter_funcs will check the binary first in any case. - TKINTER_LIB = None - -tk_version = str(tkinter.TkVersion) +""" Find compiled module linking to Tcl / Tk libraries +""" + +from __future__ import annotations + +import sys +import tkinter + +tk = getattr(tkinter, "_tkinter") + +try: + if hasattr(sys, "pypy_find_executable"): + TKINTER_LIB = tk.tklib_cffi.__file__ + else: + TKINTER_LIB = tk.__file__ +except AttributeError: + # _tkinter may be compiled directly into Python, in which case __file__ is + # not available. load_tkinter_funcs will check the binary first in any case. + TKINTER_LIB = None + +tk_version = str(tkinter.TkVersion) diff --git a/src/PIL/_typing.py b/src/PIL/_typing.py index 7075e8672..075c16222 100644 --- a/src/PIL/_typing.py +++ b/src/PIL/_typing.py @@ -1,33 +1,33 @@ -from __future__ import annotations - -import os -import sys -from typing import Protocol, Sequence, TypeVar, Union - -if sys.version_info >= (3, 10): - from typing import TypeGuard -else: - try: - from typing_extensions import TypeGuard - except ImportError: - from typing import Any - - class TypeGuard: # type: ignore[no-redef] - def __class_getitem__(cls, item: Any) -> type[bool]: - return bool - - -Coords = Union[Sequence[float], Sequence[Sequence[float]]] - - -_T_co = TypeVar("_T_co", covariant=True) - - -class SupportsRead(Protocol[_T_co]): - def read(self, __length: int = ...) -> _T_co: ... - - -StrOrBytesPath = Union[str, bytes, "os.PathLike[str]", "os.PathLike[bytes]"] - - -__all__ = ["TypeGuard", "StrOrBytesPath", "SupportsRead"] +from __future__ import annotations + +import os +import sys +from typing import Protocol, Sequence, TypeVar, Union + +if sys.version_info >= (3, 10): + from typing import TypeGuard +else: + try: + from typing_extensions import TypeGuard + except ImportError: + from typing import Any + + class TypeGuard: # type: ignore[no-redef] + def __class_getitem__(cls, item: Any) -> type[bool]: + return bool + + +Coords = Union[Sequence[float], Sequence[Sequence[float]]] + + +_T_co = TypeVar("_T_co", covariant=True) + + +class SupportsRead(Protocol[_T_co]): + def read(self, __length: int = ...) -> _T_co: ... + + +StrOrBytesPath = Union[str, bytes, "os.PathLike[str]", "os.PathLike[bytes]"] + + +__all__ = ["TypeGuard", "StrOrBytesPath", "SupportsRead"] diff --git a/src/PIL/_util.py b/src/PIL/_util.py index 6bc762816..d5681fe3f 100644 --- a/src/PIL/_util.py +++ b/src/PIL/_util.py @@ -1,31 +1,31 @@ -from __future__ import annotations - -import os -from typing import Any, NoReturn - -from ._typing import StrOrBytesPath, TypeGuard - - -def is_path(f: Any) -> TypeGuard[StrOrBytesPath]: - return isinstance(f, (bytes, str, os.PathLike)) - - -def is_directory(f: Any) -> TypeGuard[StrOrBytesPath]: - """Checks if an object is a string, and that it points to a directory.""" - return is_path(f) and os.path.isdir(f) - - -class DeferredError: - def __init__(self, ex: BaseException): - self.ex = ex - - def __getattr__(self, elt: str) -> NoReturn: - raise self.ex - - @staticmethod - def new(ex: BaseException) -> Any: - """ - Creates an object that raises the wrapped exception ``ex`` when used, - and casts it to :py:obj:`~typing.Any` type. - """ - return DeferredError(ex) +from __future__ import annotations + +import os +from typing import Any, NoReturn + +from ._typing import StrOrBytesPath, TypeGuard + + +def is_path(f: Any) -> TypeGuard[StrOrBytesPath]: + return isinstance(f, (bytes, str, os.PathLike)) + + +def is_directory(f: Any) -> TypeGuard[StrOrBytesPath]: + """Checks if an object is a string, and that it points to a directory.""" + return is_path(f) and os.path.isdir(f) + + +class DeferredError: + def __init__(self, ex: BaseException): + self.ex = ex + + def __getattr__(self, elt: str) -> NoReturn: + raise self.ex + + @staticmethod + def new(ex: BaseException) -> Any: + """ + Creates an object that raises the wrapped exception ``ex`` when used, + and casts it to :py:obj:`~typing.Any` type. + """ + return DeferredError(ex) diff --git a/src/PIL/_version.py b/src/PIL/_version.py index 12d7412ea..ac8dc61df 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,4 +1,4 @@ -# Master version for Pillow -from __future__ import annotations - -__version__ = "10.4.0.dev0" +# Master version for Pillow +from __future__ import annotations + +__version__ = "10.4.0.dev0" diff --git a/src/PIL/_webp.pyi b/src/PIL/_webp.pyi index e27843e53..9342f10b9 100644 --- a/src/PIL/_webp.pyi +++ b/src/PIL/_webp.pyi @@ -1,3 +1,3 @@ -from typing import Any - -def __getattr__(name: str) -> Any: ... +from typing import Any + +def __getattr__(name: str) -> Any: ... diff --git a/src/PIL/features.py b/src/PIL/features.py index 16c749f14..acebd5c22 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -1,336 +1,336 @@ -from __future__ import annotations - -import collections -import os -import sys -import warnings - -import PIL - -from . import Image - -modules = { - "pil": ("PIL._imaging", "PILLOW_VERSION"), - "tkinter": ("PIL._tkinter_finder", "tk_version"), - "freetype2": ("PIL._imagingft", "freetype2_version"), - "littlecms2": ("PIL._imagingcms", "littlecms_version"), - "webp": ("PIL._webp", "webpdecoder_version"), -} - - -def check_module(feature: str) -> bool: - """ - Checks if a module is available. - - :param feature: The module to check for. - :returns: ``True`` if available, ``False`` otherwise. - :raises ValueError: If the module is not defined in this version of Pillow. - """ - if feature not in modules: - msg = f"Unknown module {feature}" - raise ValueError(msg) - - module, ver = modules[feature] - - try: - __import__(module) - return True - except ModuleNotFoundError: - return False - except ImportError as ex: - warnings.warn(str(ex)) - return False - - -def version_module(feature: str) -> str | None: - """ - :param feature: The module to check for. - :returns: - The loaded version number as a string, or ``None`` if unknown or not available. - :raises ValueError: If the module is not defined in this version of Pillow. - """ - if not check_module(feature): - return None - - module, ver = modules[feature] - - return getattr(__import__(module, fromlist=[ver]), ver) - - -def get_supported_modules() -> list[str]: - """ - :returns: A list of all supported modules. - """ - return [f for f in modules if check_module(f)] - - -codecs = { - "jpg": ("jpeg", "jpeglib"), - "jpg_2000": ("jpeg2k", "jp2klib"), - "zlib": ("zip", "zlib"), - "libtiff": ("libtiff", "libtiff"), -} - - -def check_codec(feature: str) -> bool: - """ - Checks if a codec is available. - - :param feature: The codec to check for. - :returns: ``True`` if available, ``False`` otherwise. - :raises ValueError: If the codec is not defined in this version of Pillow. - """ - if feature not in codecs: - msg = f"Unknown codec {feature}" - raise ValueError(msg) - - codec, lib = codecs[feature] - - return f"{codec}_encoder" in dir(Image.core) - - -def version_codec(feature: str) -> str | None: - """ - :param feature: The codec to check for. - :returns: - The version number as a string, or ``None`` if not available. - Checked at compile time for ``jpg``, run-time otherwise. - :raises ValueError: If the codec is not defined in this version of Pillow. - """ - if not check_codec(feature): - return None - - codec, lib = codecs[feature] - - version = getattr(Image.core, f"{lib}_version") - - if feature == "libtiff": - return version.split("\n")[0].split("Version ")[1] - - return version - - -def get_supported_codecs() -> list[str]: - """ - :returns: A list of all supported codecs. - """ - return [f for f in codecs if check_codec(f)] - - -features = { - "webp_anim": ("PIL._webp", "HAVE_WEBPANIM", None), - "webp_mux": ("PIL._webp", "HAVE_WEBPMUX", None), - "transp_webp": ("PIL._webp", "HAVE_TRANSPARENCY", None), - "raqm": ("PIL._imagingft", "HAVE_RAQM", "raqm_version"), - "fribidi": ("PIL._imagingft", "HAVE_FRIBIDI", "fribidi_version"), - "harfbuzz": ("PIL._imagingft", "HAVE_HARFBUZZ", "harfbuzz_version"), - "libjpeg_turbo": ("PIL._imaging", "HAVE_LIBJPEGTURBO", "libjpeg_turbo_version"), - "libimagequant": ("PIL._imaging", "HAVE_LIBIMAGEQUANT", "imagequant_version"), - "xcb": ("PIL._imaging", "HAVE_XCB", None), -} - - -def check_feature(feature: str) -> bool | None: - """ - Checks if a feature is available. - - :param feature: The feature to check for. - :returns: ``True`` if available, ``False`` if unavailable, ``None`` if unknown. - :raises ValueError: If the feature is not defined in this version of Pillow. - """ - if feature not in features: - msg = f"Unknown feature {feature}" - raise ValueError(msg) - - module, flag, ver = features[feature] - - try: - imported_module = __import__(module, fromlist=["PIL"]) - return getattr(imported_module, flag) - except ModuleNotFoundError: - return None - except ImportError as ex: - warnings.warn(str(ex)) - return None - - -def version_feature(feature: str) -> str | None: - """ - :param feature: The feature to check for. - :returns: The version number as a string, or ``None`` if not available. - :raises ValueError: If the feature is not defined in this version of Pillow. - """ - if not check_feature(feature): - return None - - module, flag, ver = features[feature] - - if ver is None: - return None - - return getattr(__import__(module, fromlist=[ver]), ver) - - -def get_supported_features() -> list[str]: - """ - :returns: A list of all supported features. - """ - return [f for f in features if check_feature(f)] - - -def check(feature: str) -> bool | None: - """ - :param feature: A module, codec, or feature name. - :returns: - ``True`` if the module, codec, or feature is available, - ``False`` or ``None`` otherwise. - """ - - if feature in modules: - return check_module(feature) - if feature in codecs: - return check_codec(feature) - if feature in features: - return check_feature(feature) - warnings.warn(f"Unknown feature '{feature}'.", stacklevel=2) - return False - - -def version(feature: str) -> str | None: - """ - :param feature: - The module, codec, or feature to check for. - :returns: - The version number as a string, or ``None`` if unknown or not available. - """ - if feature in modules: - return version_module(feature) - if feature in codecs: - return version_codec(feature) - if feature in features: - return version_feature(feature) - return None - - -def get_supported() -> list[str]: - """ - :returns: A list of all supported modules, features, and codecs. - """ - - ret = get_supported_modules() - ret.extend(get_supported_features()) - ret.extend(get_supported_codecs()) - return ret - - -def pilinfo(out=None, supported_formats=True): - """ - Prints information about this installation of Pillow. - This function can be called with ``python3 -m PIL``. - It can also be called with ``python3 -m PIL.report`` or ``python3 -m PIL --report`` - to have "supported_formats" set to ``False``, omitting the list of all supported - image file formats. - - :param out: - The output stream to print to. Defaults to ``sys.stdout`` if ``None``. - :param supported_formats: - If ``True``, a list of all supported image file formats will be printed. - """ - - if out is None: - out = sys.stdout - - Image.init() - - print("-" * 68, file=out) - print(f"Pillow {PIL.__version__}", file=out) - py_version = sys.version.splitlines() - print(f"Python {py_version[0].strip()}", file=out) - for py_version in py_version[1:]: - print(f" {py_version.strip()}", file=out) - print("-" * 68, file=out) - print(f"Python executable is {sys.executable or 'unknown'}", file=out) - if sys.prefix != sys.base_prefix: - print(f"Environment Python files loaded from {sys.prefix}", file=out) - print(f"System Python files loaded from {sys.base_prefix}", file=out) - print("-" * 68, file=out) - print( - f"Python Pillow modules loaded from {os.path.dirname(Image.__file__)}", - file=out, - ) - print( - f"Binary Pillow modules loaded from {os.path.dirname(Image.core.__file__)}", - file=out, - ) - print("-" * 68, file=out) - - for name, feature in [ - ("pil", "PIL CORE"), - ("tkinter", "TKINTER"), - ("freetype2", "FREETYPE2"), - ("littlecms2", "LITTLECMS2"), - ("webp", "WEBP"), - ("transp_webp", "WEBP Transparency"), - ("webp_mux", "WEBPMUX"), - ("webp_anim", "WEBP Animation"), - ("jpg", "JPEG"), - ("jpg_2000", "OPENJPEG (JPEG2000)"), - ("zlib", "ZLIB (PNG/ZIP)"), - ("libtiff", "LIBTIFF"), - ("raqm", "RAQM (Bidirectional Text)"), - ("libimagequant", "LIBIMAGEQUANT (Quantization method)"), - ("xcb", "XCB (X protocol)"), - ]: - if check(name): - if name == "jpg" and check_feature("libjpeg_turbo"): - v = "libjpeg-turbo " + version_feature("libjpeg_turbo") - else: - v = version(name) - if v is not None: - version_static = name in ("pil", "jpg") - if name == "littlecms2": - # this check is also in src/_imagingcms.c:setup_module() - version_static = tuple(int(x) for x in v.split(".")) < (2, 7) - t = "compiled for" if version_static else "loaded" - if name == "raqm": - for f in ("fribidi", "harfbuzz"): - v2 = version_feature(f) - if v2 is not None: - v += f", {f} {v2}" - print("---", feature, "support ok,", t, v, file=out) - else: - print("---", feature, "support ok", file=out) - else: - print("***", feature, "support not installed", file=out) - print("-" * 68, file=out) - - if supported_formats: - extensions = collections.defaultdict(list) - for ext, i in Image.EXTENSION.items(): - extensions[i].append(ext) - - for i in sorted(Image.ID): - line = f"{i}" - if i in Image.MIME: - line = f"{line} {Image.MIME[i]}" - print(line, file=out) - - if i in extensions: - print( - "Extensions: {}".format(", ".join(sorted(extensions[i]))), file=out - ) - - features = [] - if i in Image.OPEN: - features.append("open") - if i in Image.SAVE: - features.append("save") - if i in Image.SAVE_ALL: - features.append("save_all") - if i in Image.DECODERS: - features.append("decode") - if i in Image.ENCODERS: - features.append("encode") - - print("Features: {}".format(", ".join(features)), file=out) - print("-" * 68, file=out) +from __future__ import annotations + +import collections +import os +import sys +import warnings + +import PIL + +from . import Image + +modules = { + "pil": ("PIL._imaging", "PILLOW_VERSION"), + "tkinter": ("PIL._tkinter_finder", "tk_version"), + "freetype2": ("PIL._imagingft", "freetype2_version"), + "littlecms2": ("PIL._imagingcms", "littlecms_version"), + "webp": ("PIL._webp", "webpdecoder_version"), +} + + +def check_module(feature: str) -> bool: + """ + Checks if a module is available. + + :param feature: The module to check for. + :returns: ``True`` if available, ``False`` otherwise. + :raises ValueError: If the module is not defined in this version of Pillow. + """ + if feature not in modules: + msg = f"Unknown module {feature}" + raise ValueError(msg) + + module, ver = modules[feature] + + try: + __import__(module) + return True + except ModuleNotFoundError: + return False + except ImportError as ex: + warnings.warn(str(ex)) + return False + + +def version_module(feature: str) -> str | None: + """ + :param feature: The module to check for. + :returns: + The loaded version number as a string, or ``None`` if unknown or not available. + :raises ValueError: If the module is not defined in this version of Pillow. + """ + if not check_module(feature): + return None + + module, ver = modules[feature] + + return getattr(__import__(module, fromlist=[ver]), ver) + + +def get_supported_modules() -> list[str]: + """ + :returns: A list of all supported modules. + """ + return [f for f in modules if check_module(f)] + + +codecs = { + "jpg": ("jpeg", "jpeglib"), + "jpg_2000": ("jpeg2k", "jp2klib"), + "zlib": ("zip", "zlib"), + "libtiff": ("libtiff", "libtiff"), +} + + +def check_codec(feature: str) -> bool: + """ + Checks if a codec is available. + + :param feature: The codec to check for. + :returns: ``True`` if available, ``False`` otherwise. + :raises ValueError: If the codec is not defined in this version of Pillow. + """ + if feature not in codecs: + msg = f"Unknown codec {feature}" + raise ValueError(msg) + + codec, lib = codecs[feature] + + return f"{codec}_encoder" in dir(Image.core) + + +def version_codec(feature: str) -> str | None: + """ + :param feature: The codec to check for. + :returns: + The version number as a string, or ``None`` if not available. + Checked at compile time for ``jpg``, run-time otherwise. + :raises ValueError: If the codec is not defined in this version of Pillow. + """ + if not check_codec(feature): + return None + + codec, lib = codecs[feature] + + version = getattr(Image.core, f"{lib}_version") + + if feature == "libtiff": + return version.split("\n")[0].split("Version ")[1] + + return version + + +def get_supported_codecs() -> list[str]: + """ + :returns: A list of all supported codecs. + """ + return [f for f in codecs if check_codec(f)] + + +features = { + "webp_anim": ("PIL._webp", "HAVE_WEBPANIM", None), + "webp_mux": ("PIL._webp", "HAVE_WEBPMUX", None), + "transp_webp": ("PIL._webp", "HAVE_TRANSPARENCY", None), + "raqm": ("PIL._imagingft", "HAVE_RAQM", "raqm_version"), + "fribidi": ("PIL._imagingft", "HAVE_FRIBIDI", "fribidi_version"), + "harfbuzz": ("PIL._imagingft", "HAVE_HARFBUZZ", "harfbuzz_version"), + "libjpeg_turbo": ("PIL._imaging", "HAVE_LIBJPEGTURBO", "libjpeg_turbo_version"), + "libimagequant": ("PIL._imaging", "HAVE_LIBIMAGEQUANT", "imagequant_version"), + "xcb": ("PIL._imaging", "HAVE_XCB", None), +} + + +def check_feature(feature: str) -> bool | None: + """ + Checks if a feature is available. + + :param feature: The feature to check for. + :returns: ``True`` if available, ``False`` if unavailable, ``None`` if unknown. + :raises ValueError: If the feature is not defined in this version of Pillow. + """ + if feature not in features: + msg = f"Unknown feature {feature}" + raise ValueError(msg) + + module, flag, ver = features[feature] + + try: + imported_module = __import__(module, fromlist=["PIL"]) + return getattr(imported_module, flag) + except ModuleNotFoundError: + return None + except ImportError as ex: + warnings.warn(str(ex)) + return None + + +def version_feature(feature: str) -> str | None: + """ + :param feature: The feature to check for. + :returns: The version number as a string, or ``None`` if not available. + :raises ValueError: If the feature is not defined in this version of Pillow. + """ + if not check_feature(feature): + return None + + module, flag, ver = features[feature] + + if ver is None: + return None + + return getattr(__import__(module, fromlist=[ver]), ver) + + +def get_supported_features() -> list[str]: + """ + :returns: A list of all supported features. + """ + return [f for f in features if check_feature(f)] + + +def check(feature: str) -> bool | None: + """ + :param feature: A module, codec, or feature name. + :returns: + ``True`` if the module, codec, or feature is available, + ``False`` or ``None`` otherwise. + """ + + if feature in modules: + return check_module(feature) + if feature in codecs: + return check_codec(feature) + if feature in features: + return check_feature(feature) + warnings.warn(f"Unknown feature '{feature}'.", stacklevel=2) + return False + + +def version(feature: str) -> str | None: + """ + :param feature: + The module, codec, or feature to check for. + :returns: + The version number as a string, or ``None`` if unknown or not available. + """ + if feature in modules: + return version_module(feature) + if feature in codecs: + return version_codec(feature) + if feature in features: + return version_feature(feature) + return None + + +def get_supported() -> list[str]: + """ + :returns: A list of all supported modules, features, and codecs. + """ + + ret = get_supported_modules() + ret.extend(get_supported_features()) + ret.extend(get_supported_codecs()) + return ret + + +def pilinfo(out=None, supported_formats=True): + """ + Prints information about this installation of Pillow. + This function can be called with ``python3 -m PIL``. + It can also be called with ``python3 -m PIL.report`` or ``python3 -m PIL --report`` + to have "supported_formats" set to ``False``, omitting the list of all supported + image file formats. + + :param out: + The output stream to print to. Defaults to ``sys.stdout`` if ``None``. + :param supported_formats: + If ``True``, a list of all supported image file formats will be printed. + """ + + if out is None: + out = sys.stdout + + Image.init() + + print("-" * 68, file=out) + print(f"Pillow {PIL.__version__}", file=out) + py_version = sys.version.splitlines() + print(f"Python {py_version[0].strip()}", file=out) + for py_version in py_version[1:]: + print(f" {py_version.strip()}", file=out) + print("-" * 68, file=out) + print(f"Python executable is {sys.executable or 'unknown'}", file=out) + if sys.prefix != sys.base_prefix: + print(f"Environment Python files loaded from {sys.prefix}", file=out) + print(f"System Python files loaded from {sys.base_prefix}", file=out) + print("-" * 68, file=out) + print( + f"Python Pillow modules loaded from {os.path.dirname(Image.__file__)}", + file=out, + ) + print( + f"Binary Pillow modules loaded from {os.path.dirname(Image.core.__file__)}", + file=out, + ) + print("-" * 68, file=out) + + for name, feature in [ + ("pil", "PIL CORE"), + ("tkinter", "TKINTER"), + ("freetype2", "FREETYPE2"), + ("littlecms2", "LITTLECMS2"), + ("webp", "WEBP"), + ("transp_webp", "WEBP Transparency"), + ("webp_mux", "WEBPMUX"), + ("webp_anim", "WEBP Animation"), + ("jpg", "JPEG"), + ("jpg_2000", "OPENJPEG (JPEG2000)"), + ("zlib", "ZLIB (PNG/ZIP)"), + ("libtiff", "LIBTIFF"), + ("raqm", "RAQM (Bidirectional Text)"), + ("libimagequant", "LIBIMAGEQUANT (Quantization method)"), + ("xcb", "XCB (X protocol)"), + ]: + if check(name): + if name == "jpg" and check_feature("libjpeg_turbo"): + v = "libjpeg-turbo " + version_feature("libjpeg_turbo") + else: + v = version(name) + if v is not None: + version_static = name in ("pil", "jpg") + if name == "littlecms2": + # this check is also in src/_imagingcms.c:setup_module() + version_static = tuple(int(x) for x in v.split(".")) < (2, 7) + t = "compiled for" if version_static else "loaded" + if name == "raqm": + for f in ("fribidi", "harfbuzz"): + v2 = version_feature(f) + if v2 is not None: + v += f", {f} {v2}" + print("---", feature, "support ok,", t, v, file=out) + else: + print("---", feature, "support ok", file=out) + else: + print("***", feature, "support not installed", file=out) + print("-" * 68, file=out) + + if supported_formats: + extensions = collections.defaultdict(list) + for ext, i in Image.EXTENSION.items(): + extensions[i].append(ext) + + for i in sorted(Image.ID): + line = f"{i}" + if i in Image.MIME: + line = f"{line} {Image.MIME[i]}" + print(line, file=out) + + if i in extensions: + print( + "Extensions: {}".format(", ".join(sorted(extensions[i]))), file=out + ) + + features = [] + if i in Image.OPEN: + features.append("open") + if i in Image.SAVE: + features.append("save") + if i in Image.SAVE_ALL: + features.append("save_all") + if i in Image.DECODERS: + features.append("decode") + if i in Image.ENCODERS: + features.append("encode") + + print("Features: {}".format(", ".join(features)), file=out) + print("-" * 68, file=out) diff --git a/src/PIL/report.py b/src/PIL/report.py index d2815e845..5abafd44e 100644 --- a/src/PIL/report.py +++ b/src/PIL/report.py @@ -1,5 +1,5 @@ -from __future__ import annotations - -from .features import pilinfo - -pilinfo(supported_formats=False) +from __future__ import annotations + +from .features import pilinfo + +pilinfo(supported_formats=False) diff --git a/src/Tk/_tkmini.h b/src/Tk/_tkmini.h index 68247bc47..52c8ffb29 100644 --- a/src/Tk/_tkmini.h +++ b/src/Tk/_tkmini.h @@ -1,142 +1,142 @@ -/* Small excerpts from the Tcl / Tk 8.6 headers - * - * License terms copied from: - * https://www.tcl.tk/software/tcltk/license.html - * as of 20 May 2016. - * - * Copyright (c) 1987-1994 The Regents of the University of California. - * Copyright (c) 1993-1996 Lucent Technologies. - * Copyright (c) 1994-1998 Sun Microsystems, Inc. - * Copyright (c) 1998-2000 by Scriptics Corporation. - * Copyright (c) 2002 by Kevin B. Kenny. All rights reserved. - * - * This software is copyrighted by the Regents of the University - * of California, Sun Microsystems, Inc., Scriptics Corporation, - * and other parties. The following terms apply to all files - * associated with the software unless explicitly disclaimed in - * individual files. - * - * The authors hereby grant permission to use, copy, modify, - * distribute, and license this software and its documentation - * for any purpose, provided that existing copyright notices are - * retained in all copies and that this notice is included - * verbatim in any distributions. No written agreement, license, - * or royalty fee is required for any of the authorized uses. - * Modifications to this software may be copyrighted by their - * authors and need not follow the licensing terms described - * here, provided that the new terms are clearly indicated on - * the first page of each file where they apply. - * - * IN NO EVENT SHALL THE AUTHORS OR DISTRIBUTORS BE LIABLE TO - * ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR - * CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF THIS - * SOFTWARE, ITS DOCUMENTATION, OR ANY DERIVATIVES THEREOF, EVEN - * IF THE AUTHORS HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH - * DAMAGE. - * - * THE AUTHORS AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY - * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR - * PURPOSE, AND NON-INFRINGEMENT. THIS SOFTWARE IS PROVIDED ON - * AN "AS IS" BASIS, AND THE AUTHORS AND DISTRIBUTORS HAVE NO - * OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, - * ENHANCEMENTS, OR MODIFICATIONS. - * - * GOVERNMENT USE: If you are acquiring this software on behalf - * of the U.S. government, the Government shall have only - * "Restricted Rights" in the software and related documentation - * as defined in the Federal Acquisition Regulations (FARs) in - * Clause 52.227.19 (c) (2). If you are acquiring the software - * on behalf of the Department of Defense, the software shall be - * classified as "Commercial Computer Software" and the - * Government shall have only "Restricted Rights" as defined in - * Clause 252.227-7013 (c) (1) of DFARs. Notwithstanding the - * foregoing, the authors grant the U.S. Government and others - * acting in its behalf permission to use and distribute the - * software in accordance with the terms specified in this - * license - */ - -/* - * Unless otherwise noted, these definitions are stable from Tcl / Tk 8.4 - * through Tcl / Tk master as of 21 May 2016 - */ - -#ifdef __cplusplus -extern "C" { -#endif - -/* Tcl header excerpts */ -#define TCL_OK 0 -#define TCL_ERROR 1 - -/* - * Users of versions of Tcl >= 8.6 encouraged to treat Tcl_Interp as an opaque - * pointer. The following definition results when TCL_NO_DEPRECATED defined. - */ -typedef struct Tcl_Interp Tcl_Interp; - -typedef struct Tcl_Command_ *Tcl_Command; -typedef void *ClientData; - -typedef int(Tcl_CmdProc)( - ClientData clientData, Tcl_Interp *interp, int argc, const char *argv[]); -typedef void(Tcl_CmdDeleteProc)(ClientData clientData); - -/* Typedefs derived from function signatures in Tcl header */ -/* Tcl_CreateCommand */ -typedef Tcl_Command (*Tcl_CreateCommand_t)( - Tcl_Interp *interp, - const char *cmdName, - Tcl_CmdProc *proc, - ClientData clientData, - Tcl_CmdDeleteProc *deleteProc); -/* Tcl_AppendResult */ -typedef void (*Tcl_AppendResult_t)(Tcl_Interp *interp, ...); - -/* Tk header excerpts */ - -/* - * The following values control how blocks are combined into photo - * images when the alpha component of a pixel is not 255, a.k.a. the - * compositing rule. - */ - -#define TK_PHOTO_COMPOSITE_OVERLAY 0 -#define TK_PHOTO_COMPOSITE_SET 1 - -typedef struct Tk_Window_ *Tk_Window; - -typedef void *Tk_PhotoHandle; - -typedef struct Tk_PhotoImageBlock { - unsigned char *pixelPtr; - int width; - int height; - int pitch; - int pixelSize; - int offset[4]; -} Tk_PhotoImageBlock; - -/* Typedefs derived from function signatures in Tk header */ -typedef int (*Tk_PhotoPutBlock_t)( - Tcl_Interp *interp, - Tk_PhotoHandle handle, - Tk_PhotoImageBlock *blockPtr, - int x, - int y, - int width, - int height, - int compRule); -/* Tk_FindPhoto */ -typedef Tk_PhotoHandle (*Tk_FindPhoto_t)(Tcl_Interp *interp, const char *imageName); -/* Tk_PhotoGetImage */ -typedef int (*Tk_PhotoGetImage_t)(Tk_PhotoHandle handle, Tk_PhotoImageBlock *blockPtr); - -/* - * end block for C++ - */ - -#ifdef __cplusplus -} -#endif +/* Small excerpts from the Tcl / Tk 8.6 headers + * + * License terms copied from: + * https://www.tcl.tk/software/tcltk/license.html + * as of 20 May 2016. + * + * Copyright (c) 1987-1994 The Regents of the University of California. + * Copyright (c) 1993-1996 Lucent Technologies. + * Copyright (c) 1994-1998 Sun Microsystems, Inc. + * Copyright (c) 1998-2000 by Scriptics Corporation. + * Copyright (c) 2002 by Kevin B. Kenny. All rights reserved. + * + * This software is copyrighted by the Regents of the University + * of California, Sun Microsystems, Inc., Scriptics Corporation, + * and other parties. The following terms apply to all files + * associated with the software unless explicitly disclaimed in + * individual files. + * + * The authors hereby grant permission to use, copy, modify, + * distribute, and license this software and its documentation + * for any purpose, provided that existing copyright notices are + * retained in all copies and that this notice is included + * verbatim in any distributions. No written agreement, license, + * or royalty fee is required for any of the authorized uses. + * Modifications to this software may be copyrighted by their + * authors and need not follow the licensing terms described + * here, provided that the new terms are clearly indicated on + * the first page of each file where they apply. + * + * IN NO EVENT SHALL THE AUTHORS OR DISTRIBUTORS BE LIABLE TO + * ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR + * CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF THIS + * SOFTWARE, ITS DOCUMENTATION, OR ANY DERIVATIVES THEREOF, EVEN + * IF THE AUTHORS HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH + * DAMAGE. + * + * THE AUTHORS AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR + * PURPOSE, AND NON-INFRINGEMENT. THIS SOFTWARE IS PROVIDED ON + * AN "AS IS" BASIS, AND THE AUTHORS AND DISTRIBUTORS HAVE NO + * OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, + * ENHANCEMENTS, OR MODIFICATIONS. + * + * GOVERNMENT USE: If you are acquiring this software on behalf + * of the U.S. government, the Government shall have only + * "Restricted Rights" in the software and related documentation + * as defined in the Federal Acquisition Regulations (FARs) in + * Clause 52.227.19 (c) (2). If you are acquiring the software + * on behalf of the Department of Defense, the software shall be + * classified as "Commercial Computer Software" and the + * Government shall have only "Restricted Rights" as defined in + * Clause 252.227-7013 (c) (1) of DFARs. Notwithstanding the + * foregoing, the authors grant the U.S. Government and others + * acting in its behalf permission to use and distribute the + * software in accordance with the terms specified in this + * license + */ + +/* + * Unless otherwise noted, these definitions are stable from Tcl / Tk 8.4 + * through Tcl / Tk master as of 21 May 2016 + */ + +#ifdef __cplusplus +extern "C" { +#endif + +/* Tcl header excerpts */ +#define TCL_OK 0 +#define TCL_ERROR 1 + +/* + * Users of versions of Tcl >= 8.6 encouraged to treat Tcl_Interp as an opaque + * pointer. The following definition results when TCL_NO_DEPRECATED defined. + */ +typedef struct Tcl_Interp Tcl_Interp; + +typedef struct Tcl_Command_ *Tcl_Command; +typedef void *ClientData; + +typedef int(Tcl_CmdProc)( + ClientData clientData, Tcl_Interp *interp, int argc, const char *argv[]); +typedef void(Tcl_CmdDeleteProc)(ClientData clientData); + +/* Typedefs derived from function signatures in Tcl header */ +/* Tcl_CreateCommand */ +typedef Tcl_Command (*Tcl_CreateCommand_t)( + Tcl_Interp *interp, + const char *cmdName, + Tcl_CmdProc *proc, + ClientData clientData, + Tcl_CmdDeleteProc *deleteProc); +/* Tcl_AppendResult */ +typedef void (*Tcl_AppendResult_t)(Tcl_Interp *interp, ...); + +/* Tk header excerpts */ + +/* + * The following values control how blocks are combined into photo + * images when the alpha component of a pixel is not 255, a.k.a. the + * compositing rule. + */ + +#define TK_PHOTO_COMPOSITE_OVERLAY 0 +#define TK_PHOTO_COMPOSITE_SET 1 + +typedef struct Tk_Window_ *Tk_Window; + +typedef void *Tk_PhotoHandle; + +typedef struct Tk_PhotoImageBlock { + unsigned char *pixelPtr; + int width; + int height; + int pitch; + int pixelSize; + int offset[4]; +} Tk_PhotoImageBlock; + +/* Typedefs derived from function signatures in Tk header */ +typedef int (*Tk_PhotoPutBlock_t)( + Tcl_Interp *interp, + Tk_PhotoHandle handle, + Tk_PhotoImageBlock *blockPtr, + int x, + int y, + int width, + int height, + int compRule); +/* Tk_FindPhoto */ +typedef Tk_PhotoHandle (*Tk_FindPhoto_t)(Tcl_Interp *interp, const char *imageName); +/* Tk_PhotoGetImage */ +typedef int (*Tk_PhotoGetImage_t)(Tk_PhotoHandle handle, Tk_PhotoImageBlock *blockPtr); + +/* + * end block for C++ + */ + +#ifdef __cplusplus +} +#endif diff --git a/src/Tk/tkImaging.c b/src/Tk/tkImaging.c index ef1c00a94..2ba284c31 100644 --- a/src/Tk/tkImaging.c +++ b/src/Tk/tkImaging.c @@ -1,454 +1,454 @@ -/* - * The Python Imaging Library. - * $Id$ - * - * TK interface for Python Imaging objects - * - * Copies (parts of) a named display memory to a photo image object. - * Also contains code to create an display memory. Under Tk, a - * display memory is simply an "L" or "RGB" image memory that is - * allocated in a single block. - * - * To use this module, import the _imagingtk module (ImageTk does - * this for you). - * - * If you're using Python in an embedded context, you can add the - * following lines to your Tcl_AppInit function (in tkappinit.c) - * instead. Put them after the calls to Tcl_Init and Tk_Init: - * - * { - * extern void TkImaging_Init(Tcl_Interp* interp); - * TkImaging_Init(interp); - * } - * - * This registers a Tcl command called "PyImagingPhoto", which is used - * to communicate between PIL and Tk's PhotoImage handler. - * - - * History: - * 1995-09-12 fl Created - * 1996-04-08 fl Ready for release - * 1997-05-09 fl Use command instead of image type - * 2001-03-18 fl Initialize alpha layer pointer (struct changed in Tk 8.3) - * 2003-04-23 fl Fixed building for Tk 8.4.1 and later (Jack Jansen) - * 2004-06-24 fl Fixed building for Tk 8.4.6 and later. - * - * Copyright (c) 1997-2004 by Secret Labs AB - * Copyright (c) 1995-2004 by Fredrik Lundh - * - * See the README file for information on usage and redistribution. - */ - -#include "../libImaging/Imaging.h" -#include "_tkmini.h" - -#include - -/* - * Global vars for Tcl / Tk functions. We load these symbols from the tkinter - * extension module or loaded Tcl / Tk libraries at run-time. - */ -static Tcl_CreateCommand_t TCL_CREATE_COMMAND; -static Tcl_AppendResult_t TCL_APPEND_RESULT; -static Tk_FindPhoto_t TK_FIND_PHOTO; -static Tk_PhotoGetImage_t TK_PHOTO_GET_IMAGE; -static Tk_PhotoPutBlock_t TK_PHOTO_PUT_BLOCK; - -static Imaging -ImagingFind(const char *name) { - Py_ssize_t id; - - /* FIXME: use CObject instead? */ -#if defined(_WIN64) - id = _atoi64(name); -#else - id = atol(name); -#endif - if (!id) { - return NULL; - } - - return (Imaging)id; -} - -static int -PyImagingPhotoPut( - ClientData clientdata, Tcl_Interp *interp, int argc, const char **argv) { - Imaging im; - Tk_PhotoHandle photo; - Tk_PhotoImageBlock block; - - if (argc != 3) { - TCL_APPEND_RESULT( - interp, "usage: ", argv[0], " destPhoto srcImage", (char *)NULL); - return TCL_ERROR; - } - - /* get Tcl PhotoImage handle */ - photo = TK_FIND_PHOTO(interp, argv[1]); - if (photo == NULL) { - TCL_APPEND_RESULT(interp, "destination photo must exist", (char *)NULL); - return TCL_ERROR; - } - - /* get PIL Image handle */ - im = ImagingFind(argv[2]); - if (!im) { - TCL_APPEND_RESULT(interp, "bad name", (char *)NULL); - return TCL_ERROR; - } - if (!im->block) { - TCL_APPEND_RESULT(interp, "bad display memory", (char *)NULL); - return TCL_ERROR; - } - - /* Mode */ - - if (strcmp(im->mode, "1") == 0 || strcmp(im->mode, "L") == 0) { - block.pixelSize = 1; - block.offset[0] = block.offset[1] = block.offset[2] = block.offset[3] = 0; - } else if (strncmp(im->mode, "RGB", 3) == 0) { - block.pixelSize = 4; - block.offset[0] = 0; - block.offset[1] = 1; - block.offset[2] = 2; - if (strcmp(im->mode, "RGBA") == 0) { - block.offset[3] = 3; /* alpha (or reserved, under Tk 8.2) */ - } else { - block.offset[3] = 0; /* no alpha */ - } - } else { - TCL_APPEND_RESULT(interp, "Bad mode", (char *)NULL); - return TCL_ERROR; - } - - block.width = im->xsize; - block.height = im->ysize; - block.pitch = im->linesize; - block.pixelPtr = (unsigned char *)im->block; - - TK_PHOTO_PUT_BLOCK( - interp, photo, &block, 0, 0, block.width, block.height, TK_PHOTO_COMPOSITE_SET); - - return TCL_OK; -} - -static int -PyImagingPhotoGet( - ClientData clientdata, Tcl_Interp *interp, int argc, const char **argv) { - Imaging im; - Tk_PhotoHandle photo; - Tk_PhotoImageBlock block; - int x, y, z; - - if (argc != 3) { - TCL_APPEND_RESULT( - interp, "usage: ", argv[0], " srcPhoto destImage", (char *)NULL); - return TCL_ERROR; - } - - /* get Tcl PhotoImage handle */ - photo = TK_FIND_PHOTO(interp, argv[1]); - if (photo == NULL) { - TCL_APPEND_RESULT(interp, "source photo must exist", (char *)NULL); - return TCL_ERROR; - } - - /* get PIL Image handle */ - im = ImagingFind(argv[2]); - if (!im) { - TCL_APPEND_RESULT(interp, "bad name", (char *)NULL); - return TCL_ERROR; - } - - TK_PHOTO_GET_IMAGE(photo, &block); - - for (y = 0; y < block.height; y++) { - UINT8 *out = (UINT8 *)im->image32[y]; - for (x = 0; x < block.pitch; x += block.pixelSize) { - for (z = 0; z < block.pixelSize; z++) { - int offset = block.offset[z]; - out[x + offset] = block.pixelPtr[y * block.pitch + x + offset]; - } - } - } - - return TCL_OK; -} - -void -TkImaging_Init(Tcl_Interp *interp) { - TCL_CREATE_COMMAND( - interp, - "PyImagingPhoto", - PyImagingPhotoPut, - (ClientData)0, - (Tcl_CmdDeleteProc *)NULL); - TCL_CREATE_COMMAND( - interp, - "PyImagingPhotoGet", - PyImagingPhotoGet, - (ClientData)0, - (Tcl_CmdDeleteProc *)NULL); -} - -/* - * Functions to fill global Tcl / Tk function pointers by dynamic loading - */ - -#define TKINTER_FINDER "PIL._tkinter_finder" - -#if defined(_WIN32) || defined(__WIN32__) || defined(WIN32) || defined(__CYGWIN__) - -/* - * On Windows, we can't load the tkinter module to get the Tcl or Tk symbols, - * because Windows does not load symbols into the library name-space of - * importing modules. So, knowing that tkinter has already been imported by - * Python, we scan all modules in the running process for the Tcl and Tk - * function names. - */ -#include -#define PSAPI_VERSION 1 -#include -/* Must be linked with 'psapi' library */ - -#define TKINTER_PKG "tkinter" - -FARPROC -_dfunc(HMODULE lib_handle, const char *func_name) { - /* - * Load function `func_name` from `lib_handle`. - * Set Python exception if we can't find `func_name` in `lib_handle`. - * Returns function pointer or NULL if not present. - */ - - char message[100]; - - FARPROC func = GetProcAddress(lib_handle, func_name); - if (func == NULL) { - sprintf(message, "Cannot load function %s", func_name); - PyErr_SetString(PyExc_RuntimeError, message); - } - return func; -} - -int -get_tcl(HMODULE hMod) { - /* - * Try to fill Tcl global vars with function pointers. Return 0 for no - * functions found, 1 for all functions found, -1 for some but not all - * functions found. - */ - - if ((TCL_CREATE_COMMAND = - (Tcl_CreateCommand_t)GetProcAddress(hMod, "Tcl_CreateCommand")) == NULL) { - return 0; /* Maybe not Tcl module */ - } - return ((TCL_APPEND_RESULT = - (Tcl_AppendResult_t)_dfunc(hMod, "Tcl_AppendResult")) == NULL) - ? -1 - : 1; -} - -int -get_tk(HMODULE hMod) { - /* - * Try to fill Tk global vars with function pointers. Return 0 for no - * functions found, 1 for all functions found, -1 for some but not all - * functions found. - */ - - FARPROC func = GetProcAddress(hMod, "Tk_PhotoPutBlock"); - if (func == NULL) { /* Maybe not Tk module */ - return 0; - } - if ((TK_PHOTO_GET_IMAGE = (Tk_PhotoGetImage_t)_dfunc(hMod, "Tk_PhotoGetImage")) == - NULL) { - return -1; - }; - if ((TK_FIND_PHOTO = (Tk_FindPhoto_t)_dfunc(hMod, "Tk_FindPhoto")) == NULL) { - return -1; - }; - TK_PHOTO_PUT_BLOCK = (Tk_PhotoPutBlock_t)func; - return 1; -} - -int -load_tkinter_funcs(void) { - /* - * Load Tcl and Tk functions by searching all modules in current process. - * Return 0 for success, non-zero for failure. - */ - - HMODULE *hMods = NULL; - HANDLE hProcess; - DWORD cbNeeded; - unsigned int i; - int found_tcl = 0; - int found_tk = 0; - - /* First load tkinter module to make sure libraries are loaded */ - PyObject *pModule = PyImport_ImportModule(TKINTER_PKG); - if (pModule == NULL) { - return 1; - } - Py_DECREF(pModule); - - /* Returns pseudo-handle that does not need to be closed */ - hProcess = GetCurrentProcess(); - - /* Allocate module handlers array */ - if (!EnumProcessModules(hProcess, NULL, 0, &cbNeeded)) { -#if defined(__CYGWIN__) - PyErr_SetString(PyExc_OSError, "Call to EnumProcessModules failed"); -#else - PyErr_SetFromWindowsErr(0); -#endif - return 1; - } - if (!(hMods = (HMODULE *)malloc(cbNeeded))) { - PyErr_NoMemory(); - return 1; - } - - /* Iterate through modules in this process looking for Tcl / Tk names */ - if (EnumProcessModules(hProcess, hMods, cbNeeded, &cbNeeded)) { - for (i = 0; i < (cbNeeded / sizeof(HMODULE)); i++) { - if (!found_tcl) { - found_tcl = get_tcl(hMods[i]); - if (found_tcl == -1) { - break; - } - } - if (!found_tk) { - found_tk = get_tk(hMods[i]); - if (found_tk == -1) { - break; - } - } - if (found_tcl && found_tk) { - break; - } - } - } - - free(hMods); - if (found_tcl == 0) { - PyErr_SetString(PyExc_RuntimeError, "Could not find Tcl routines"); - } else if (found_tk == 0) { - PyErr_SetString(PyExc_RuntimeError, "Could not find Tk routines"); - } - return (int)((found_tcl != 1) || (found_tk != 1)); -} - -#else /* not Windows */ - -/* - * On Unix, we can get the Tcl and Tk symbols from the tkinter module, because - * tkinter uses these symbols, and the symbols are therefore visible in the - * tkinter dynamic library (module). - */ - -#include - -void * -_dfunc(void *lib_handle, const char *func_name) { - /* - * Load function `func_name` from `lib_handle`. - * Set Python exception if we can't find `func_name` in `lib_handle`. - * Returns function pointer or NULL if not present. - */ - - void *func; - /* Reset errors. */ - dlerror(); - func = dlsym(lib_handle, func_name); - if (func == NULL) { - const char *error = dlerror(); - PyErr_SetString(PyExc_RuntimeError, error); - } - return func; -} - -int -_func_loader(void *lib) { - /* - * Fill global function pointers from dynamic lib. - * Return 1 if any pointer is NULL, 0 otherwise. - */ - - if ((TCL_CREATE_COMMAND = (Tcl_CreateCommand_t)_dfunc(lib, "Tcl_CreateCommand")) == - NULL) { - return 1; - } - if ((TCL_APPEND_RESULT = (Tcl_AppendResult_t)_dfunc(lib, "Tcl_AppendResult")) == - NULL) { - return 1; - } - if ((TK_PHOTO_GET_IMAGE = (Tk_PhotoGetImage_t)_dfunc(lib, "Tk_PhotoGetImage")) == - NULL) { - return 1; - } - if ((TK_FIND_PHOTO = (Tk_FindPhoto_t)_dfunc(lib, "Tk_FindPhoto")) == NULL) { - return 1; - } - return ( - (TK_PHOTO_PUT_BLOCK = (Tk_PhotoPutBlock_t)_dfunc(lib, "Tk_PhotoPutBlock")) == - NULL); -} - -int -load_tkinter_funcs(void) { - /* - * Load tkinter global funcs from tkinter compiled module. - * Return 0 for success, non-zero for failure. - */ - - int ret = -1; - void *main_program, *tkinter_lib; - char *tkinter_libname; - PyObject *pModule = NULL, *pString = NULL, *pBytes = NULL; - - /* Try loading from the main program namespace first */ - main_program = dlopen(NULL, RTLD_LAZY); - if (_func_loader(main_program) == 0) { - dlclose(main_program); - return 0; - } - /* Clear exception triggered when we didn't find symbols above */ - PyErr_Clear(); - - /* Now try finding the tkinter compiled module */ - pModule = PyImport_ImportModule(TKINTER_FINDER); - if (pModule == NULL) { - goto exit; - } - pString = PyObject_GetAttrString(pModule, "TKINTER_LIB"); - if (pString == NULL) { - goto exit; - } - /* From module __file__ attribute to char *string for dlopen. */ - pBytes = PyUnicode_EncodeFSDefault(pString); - if (pBytes == NULL) { - goto exit; - } - tkinter_libname = PyBytes_AsString(pBytes); - if (tkinter_libname == NULL) { - goto exit; - } - tkinter_lib = dlopen(tkinter_libname, RTLD_LAZY); - if (tkinter_lib == NULL) { - PyErr_SetString(PyExc_RuntimeError, "Cannot dlopen tkinter module file"); - goto exit; - } - ret = _func_loader(tkinter_lib); - /* dlclose probably safe because tkinter has been imported. */ - dlclose(tkinter_lib); -exit: - dlclose(main_program); - Py_XDECREF(pModule); - Py_XDECREF(pString); - Py_XDECREF(pBytes); - return ret; -} -#endif /* end not Windows */ +/* + * The Python Imaging Library. + * $Id$ + * + * TK interface for Python Imaging objects + * + * Copies (parts of) a named display memory to a photo image object. + * Also contains code to create an display memory. Under Tk, a + * display memory is simply an "L" or "RGB" image memory that is + * allocated in a single block. + * + * To use this module, import the _imagingtk module (ImageTk does + * this for you). + * + * If you're using Python in an embedded context, you can add the + * following lines to your Tcl_AppInit function (in tkappinit.c) + * instead. Put them after the calls to Tcl_Init and Tk_Init: + * + * { + * extern void TkImaging_Init(Tcl_Interp* interp); + * TkImaging_Init(interp); + * } + * + * This registers a Tcl command called "PyImagingPhoto", which is used + * to communicate between PIL and Tk's PhotoImage handler. + * + + * History: + * 1995-09-12 fl Created + * 1996-04-08 fl Ready for release + * 1997-05-09 fl Use command instead of image type + * 2001-03-18 fl Initialize alpha layer pointer (struct changed in Tk 8.3) + * 2003-04-23 fl Fixed building for Tk 8.4.1 and later (Jack Jansen) + * 2004-06-24 fl Fixed building for Tk 8.4.6 and later. + * + * Copyright (c) 1997-2004 by Secret Labs AB + * Copyright (c) 1995-2004 by Fredrik Lundh + * + * See the README file for information on usage and redistribution. + */ + +#include "../libImaging/Imaging.h" +#include "_tkmini.h" + +#include + +/* + * Global vars for Tcl / Tk functions. We load these symbols from the tkinter + * extension module or loaded Tcl / Tk libraries at run-time. + */ +static Tcl_CreateCommand_t TCL_CREATE_COMMAND; +static Tcl_AppendResult_t TCL_APPEND_RESULT; +static Tk_FindPhoto_t TK_FIND_PHOTO; +static Tk_PhotoGetImage_t TK_PHOTO_GET_IMAGE; +static Tk_PhotoPutBlock_t TK_PHOTO_PUT_BLOCK; + +static Imaging +ImagingFind(const char *name) { + Py_ssize_t id; + + /* FIXME: use CObject instead? */ +#if defined(_WIN64) + id = _atoi64(name); +#else + id = atol(name); +#endif + if (!id) { + return NULL; + } + + return (Imaging)id; +} + +static int +PyImagingPhotoPut( + ClientData clientdata, Tcl_Interp *interp, int argc, const char **argv) { + Imaging im; + Tk_PhotoHandle photo; + Tk_PhotoImageBlock block; + + if (argc != 3) { + TCL_APPEND_RESULT( + interp, "usage: ", argv[0], " destPhoto srcImage", (char *)NULL); + return TCL_ERROR; + } + + /* get Tcl PhotoImage handle */ + photo = TK_FIND_PHOTO(interp, argv[1]); + if (photo == NULL) { + TCL_APPEND_RESULT(interp, "destination photo must exist", (char *)NULL); + return TCL_ERROR; + } + + /* get PIL Image handle */ + im = ImagingFind(argv[2]); + if (!im) { + TCL_APPEND_RESULT(interp, "bad name", (char *)NULL); + return TCL_ERROR; + } + if (!im->block) { + TCL_APPEND_RESULT(interp, "bad display memory", (char *)NULL); + return TCL_ERROR; + } + + /* Mode */ + + if (strcmp(im->mode, "1") == 0 || strcmp(im->mode, "L") == 0) { + block.pixelSize = 1; + block.offset[0] = block.offset[1] = block.offset[2] = block.offset[3] = 0; + } else if (strncmp(im->mode, "RGB", 3) == 0) { + block.pixelSize = 4; + block.offset[0] = 0; + block.offset[1] = 1; + block.offset[2] = 2; + if (strcmp(im->mode, "RGBA") == 0) { + block.offset[3] = 3; /* alpha (or reserved, under Tk 8.2) */ + } else { + block.offset[3] = 0; /* no alpha */ + } + } else { + TCL_APPEND_RESULT(interp, "Bad mode", (char *)NULL); + return TCL_ERROR; + } + + block.width = im->xsize; + block.height = im->ysize; + block.pitch = im->linesize; + block.pixelPtr = (unsigned char *)im->block; + + TK_PHOTO_PUT_BLOCK( + interp, photo, &block, 0, 0, block.width, block.height, TK_PHOTO_COMPOSITE_SET); + + return TCL_OK; +} + +static int +PyImagingPhotoGet( + ClientData clientdata, Tcl_Interp *interp, int argc, const char **argv) { + Imaging im; + Tk_PhotoHandle photo; + Tk_PhotoImageBlock block; + int x, y, z; + + if (argc != 3) { + TCL_APPEND_RESULT( + interp, "usage: ", argv[0], " srcPhoto destImage", (char *)NULL); + return TCL_ERROR; + } + + /* get Tcl PhotoImage handle */ + photo = TK_FIND_PHOTO(interp, argv[1]); + if (photo == NULL) { + TCL_APPEND_RESULT(interp, "source photo must exist", (char *)NULL); + return TCL_ERROR; + } + + /* get PIL Image handle */ + im = ImagingFind(argv[2]); + if (!im) { + TCL_APPEND_RESULT(interp, "bad name", (char *)NULL); + return TCL_ERROR; + } + + TK_PHOTO_GET_IMAGE(photo, &block); + + for (y = 0; y < block.height; y++) { + UINT8 *out = (UINT8 *)im->image32[y]; + for (x = 0; x < block.pitch; x += block.pixelSize) { + for (z = 0; z < block.pixelSize; z++) { + int offset = block.offset[z]; + out[x + offset] = block.pixelPtr[y * block.pitch + x + offset]; + } + } + } + + return TCL_OK; +} + +void +TkImaging_Init(Tcl_Interp *interp) { + TCL_CREATE_COMMAND( + interp, + "PyImagingPhoto", + PyImagingPhotoPut, + (ClientData)0, + (Tcl_CmdDeleteProc *)NULL); + TCL_CREATE_COMMAND( + interp, + "PyImagingPhotoGet", + PyImagingPhotoGet, + (ClientData)0, + (Tcl_CmdDeleteProc *)NULL); +} + +/* + * Functions to fill global Tcl / Tk function pointers by dynamic loading + */ + +#define TKINTER_FINDER "PIL._tkinter_finder" + +#if defined(_WIN32) || defined(__WIN32__) || defined(WIN32) || defined(__CYGWIN__) + +/* + * On Windows, we can't load the tkinter module to get the Tcl or Tk symbols, + * because Windows does not load symbols into the library name-space of + * importing modules. So, knowing that tkinter has already been imported by + * Python, we scan all modules in the running process for the Tcl and Tk + * function names. + */ +#include +#define PSAPI_VERSION 1 +#include +/* Must be linked with 'psapi' library */ + +#define TKINTER_PKG "tkinter" + +FARPROC +_dfunc(HMODULE lib_handle, const char *func_name) { + /* + * Load function `func_name` from `lib_handle`. + * Set Python exception if we can't find `func_name` in `lib_handle`. + * Returns function pointer or NULL if not present. + */ + + char message[100]; + + FARPROC func = GetProcAddress(lib_handle, func_name); + if (func == NULL) { + sprintf(message, "Cannot load function %s", func_name); + PyErr_SetString(PyExc_RuntimeError, message); + } + return func; +} + +int +get_tcl(HMODULE hMod) { + /* + * Try to fill Tcl global vars with function pointers. Return 0 for no + * functions found, 1 for all functions found, -1 for some but not all + * functions found. + */ + + if ((TCL_CREATE_COMMAND = + (Tcl_CreateCommand_t)GetProcAddress(hMod, "Tcl_CreateCommand")) == NULL) { + return 0; /* Maybe not Tcl module */ + } + return ((TCL_APPEND_RESULT = + (Tcl_AppendResult_t)_dfunc(hMod, "Tcl_AppendResult")) == NULL) + ? -1 + : 1; +} + +int +get_tk(HMODULE hMod) { + /* + * Try to fill Tk global vars with function pointers. Return 0 for no + * functions found, 1 for all functions found, -1 for some but not all + * functions found. + */ + + FARPROC func = GetProcAddress(hMod, "Tk_PhotoPutBlock"); + if (func == NULL) { /* Maybe not Tk module */ + return 0; + } + if ((TK_PHOTO_GET_IMAGE = (Tk_PhotoGetImage_t)_dfunc(hMod, "Tk_PhotoGetImage")) == + NULL) { + return -1; + }; + if ((TK_FIND_PHOTO = (Tk_FindPhoto_t)_dfunc(hMod, "Tk_FindPhoto")) == NULL) { + return -1; + }; + TK_PHOTO_PUT_BLOCK = (Tk_PhotoPutBlock_t)func; + return 1; +} + +int +load_tkinter_funcs(void) { + /* + * Load Tcl and Tk functions by searching all modules in current process. + * Return 0 for success, non-zero for failure. + */ + + HMODULE *hMods = NULL; + HANDLE hProcess; + DWORD cbNeeded; + unsigned int i; + int found_tcl = 0; + int found_tk = 0; + + /* First load tkinter module to make sure libraries are loaded */ + PyObject *pModule = PyImport_ImportModule(TKINTER_PKG); + if (pModule == NULL) { + return 1; + } + Py_DECREF(pModule); + + /* Returns pseudo-handle that does not need to be closed */ + hProcess = GetCurrentProcess(); + + /* Allocate module handlers array */ + if (!EnumProcessModules(hProcess, NULL, 0, &cbNeeded)) { +#if defined(__CYGWIN__) + PyErr_SetString(PyExc_OSError, "Call to EnumProcessModules failed"); +#else + PyErr_SetFromWindowsErr(0); +#endif + return 1; + } + if (!(hMods = (HMODULE *)malloc(cbNeeded))) { + PyErr_NoMemory(); + return 1; + } + + /* Iterate through modules in this process looking for Tcl / Tk names */ + if (EnumProcessModules(hProcess, hMods, cbNeeded, &cbNeeded)) { + for (i = 0; i < (cbNeeded / sizeof(HMODULE)); i++) { + if (!found_tcl) { + found_tcl = get_tcl(hMods[i]); + if (found_tcl == -1) { + break; + } + } + if (!found_tk) { + found_tk = get_tk(hMods[i]); + if (found_tk == -1) { + break; + } + } + if (found_tcl && found_tk) { + break; + } + } + } + + free(hMods); + if (found_tcl == 0) { + PyErr_SetString(PyExc_RuntimeError, "Could not find Tcl routines"); + } else if (found_tk == 0) { + PyErr_SetString(PyExc_RuntimeError, "Could not find Tk routines"); + } + return (int)((found_tcl != 1) || (found_tk != 1)); +} + +#else /* not Windows */ + +/* + * On Unix, we can get the Tcl and Tk symbols from the tkinter module, because + * tkinter uses these symbols, and the symbols are therefore visible in the + * tkinter dynamic library (module). + */ + +#include + +void * +_dfunc(void *lib_handle, const char *func_name) { + /* + * Load function `func_name` from `lib_handle`. + * Set Python exception if we can't find `func_name` in `lib_handle`. + * Returns function pointer or NULL if not present. + */ + + void *func; + /* Reset errors. */ + dlerror(); + func = dlsym(lib_handle, func_name); + if (func == NULL) { + const char *error = dlerror(); + PyErr_SetString(PyExc_RuntimeError, error); + } + return func; +} + +int +_func_loader(void *lib) { + /* + * Fill global function pointers from dynamic lib. + * Return 1 if any pointer is NULL, 0 otherwise. + */ + + if ((TCL_CREATE_COMMAND = (Tcl_CreateCommand_t)_dfunc(lib, "Tcl_CreateCommand")) == + NULL) { + return 1; + } + if ((TCL_APPEND_RESULT = (Tcl_AppendResult_t)_dfunc(lib, "Tcl_AppendResult")) == + NULL) { + return 1; + } + if ((TK_PHOTO_GET_IMAGE = (Tk_PhotoGetImage_t)_dfunc(lib, "Tk_PhotoGetImage")) == + NULL) { + return 1; + } + if ((TK_FIND_PHOTO = (Tk_FindPhoto_t)_dfunc(lib, "Tk_FindPhoto")) == NULL) { + return 1; + } + return ( + (TK_PHOTO_PUT_BLOCK = (Tk_PhotoPutBlock_t)_dfunc(lib, "Tk_PhotoPutBlock")) == + NULL); +} + +int +load_tkinter_funcs(void) { + /* + * Load tkinter global funcs from tkinter compiled module. + * Return 0 for success, non-zero for failure. + */ + + int ret = -1; + void *main_program, *tkinter_lib; + char *tkinter_libname; + PyObject *pModule = NULL, *pString = NULL, *pBytes = NULL; + + /* Try loading from the main program namespace first */ + main_program = dlopen(NULL, RTLD_LAZY); + if (_func_loader(main_program) == 0) { + dlclose(main_program); + return 0; + } + /* Clear exception triggered when we didn't find symbols above */ + PyErr_Clear(); + + /* Now try finding the tkinter compiled module */ + pModule = PyImport_ImportModule(TKINTER_FINDER); + if (pModule == NULL) { + goto exit; + } + pString = PyObject_GetAttrString(pModule, "TKINTER_LIB"); + if (pString == NULL) { + goto exit; + } + /* From module __file__ attribute to char *string for dlopen. */ + pBytes = PyUnicode_EncodeFSDefault(pString); + if (pBytes == NULL) { + goto exit; + } + tkinter_libname = PyBytes_AsString(pBytes); + if (tkinter_libname == NULL) { + goto exit; + } + tkinter_lib = dlopen(tkinter_libname, RTLD_LAZY); + if (tkinter_lib == NULL) { + PyErr_SetString(PyExc_RuntimeError, "Cannot dlopen tkinter module file"); + goto exit; + } + ret = _func_loader(tkinter_lib); + /* dlclose probably safe because tkinter has been imported. */ + dlclose(tkinter_lib); +exit: + dlclose(main_program); + Py_XDECREF(pModule); + Py_XDECREF(pString); + Py_XDECREF(pBytes); + return ret; +} +#endif /* end not Windows */ diff --git a/src/_imaging.c b/src/_imaging.c index f398c6c7c..9aad6d8af 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -1,4452 +1,4452 @@ -/* - * The Python Imaging Library. - * - * the imaging library bindings - * - * history: - * 1995-09-24 fl Created - * 1996-03-24 fl Ready for first public release (release 0.0) - * 1996-03-25 fl Added fromstring (for Jack's "img" library) - * 1996-03-28 fl Added channel operations - * 1996-03-31 fl Added point operation - * 1996-04-08 fl Added new/new_block/new_array factories - * 1996-04-13 fl Added decoders - * 1996-05-04 fl Added palette hack - * 1996-05-12 fl Compile cleanly as C++ - * 1996-05-19 fl Added matrix conversions, gradient fills - * 1996-05-27 fl Added display_mode - * 1996-07-22 fl Added getbbox, offset - * 1996-07-23 fl Added sequence semantics - * 1996-08-13 fl Added logical operators, point mode - * 1996-08-16 fl Modified paste interface - * 1996-09-06 fl Added putdata methods, use abstract interface - * 1996-11-01 fl Added xbm encoder - * 1996-11-04 fl Added experimental path stuff, draw_lines, etc - * 1996-12-10 fl Added zip decoder, crc32 interface - * 1996-12-14 fl Added modulo arithmetics - * 1996-12-29 fl Added zip encoder - * 1997-01-03 fl Added fli and msp decoders - * 1997-01-04 fl Added experimental sun_rle and tga_rle decoders - * 1997-01-05 fl Added gif encoder, getpalette hack - * 1997-02-23 fl Added histogram mask - * 1997-05-12 fl Minor tweaks to match the IFUNC95 interface - * 1997-05-21 fl Added noise generator, spread effect - * 1997-06-05 fl Added mandelbrot generator - * 1997-08-02 fl Modified putpalette to coerce image mode if necessary - * 1998-01-11 fl Added INT32 support - * 1998-01-22 fl Fixed draw_points to draw the last point too - * 1998-06-28 fl Added getpixel, getink, draw_ink - * 1998-07-12 fl Added getextrema - * 1998-07-17 fl Added point conversion to arbitrary formats - * 1998-09-21 fl Added support for resampling filters - * 1998-09-22 fl Added support for quad transform - * 1998-12-29 fl Added support for arcs, chords, and pieslices - * 1999-01-10 fl Added some experimental arrow graphics stuff - * 1999-02-06 fl Added draw_bitmap, font acceleration stuff - * 2001-04-17 fl Fixed some egcs compiler nits - * 2001-09-17 fl Added screen grab primitives (win32) - * 2002-03-09 fl Added stretch primitive - * 2002-03-10 fl Fixed filter handling in rotate - * 2002-06-06 fl Added I, F, and RGB support to putdata - * 2002-06-08 fl Added rankfilter - * 2002-06-09 fl Added support for user-defined filter kernels - * 2002-11-19 fl Added clipboard grab primitives (win32) - * 2002-12-11 fl Added draw context - * 2003-04-26 fl Tweaks for Python 2.3 beta 1 - * 2003-05-21 fl Added createwindow primitive (win32) - * 2003-09-13 fl Added thread section hooks - * 2003-09-15 fl Added expand helper - * 2003-09-26 fl Added experimental LA support - * 2004-02-21 fl Handle zero-size images in quantize - * 2004-06-05 fl Added ptr attribute (used to access Imaging objects) - * 2004-06-05 fl Don't crash when fetching pixels from zero-wide images - * 2004-09-17 fl Added getcolors - * 2004-10-04 fl Added modefilter - * 2005-10-02 fl Added access proxy - * 2006-06-18 fl Always draw last point in polyline - * - * Copyright (c) 1997-2006 by Secret Labs AB - * Copyright (c) 1995-2006 by Fredrik Lundh - * - * See the README file for information on usage and redistribution. - */ - -#define PY_SSIZE_T_CLEAN -#include "Python.h" - -#ifdef HAVE_LIBJPEG -#include "jconfig.h" -#endif - -#ifdef HAVE_LIBZ -#include "zlib.h" -#endif - -#ifdef HAVE_LIBTIFF -#ifndef _TIFFIO_ -#include -#endif -#endif - -#include "libImaging/Imaging.h" - -#define _USE_MATH_DEFINES -#include - -/* Configuration stuff. Feel free to undef things you don't need. */ -#define WITH_IMAGECHOPS /* ImageChops support */ -#define WITH_IMAGEDRAW /* ImageDraw support */ -#define WITH_MAPPING /* use memory mapping to read some file formats */ -#define WITH_IMAGEPATH /* ImagePath stuff */ -#define WITH_ARROW /* arrow graphics stuff (experimental) */ -#define WITH_EFFECTS /* special effects */ -#define WITH_QUANTIZE /* quantization support */ -#define WITH_RANKFILTER /* rank filter */ -#define WITH_MODEFILTER /* mode filter */ -#define WITH_THREADING /* "friendly" threading support */ -#define WITH_UNSHARPMASK /* Kevin Cazabon's unsharpmask module */ - -#undef VERBOSE - -#define B16(p, i) ((((int)p[(i)]) << 8) + p[(i) + 1]) -#define L16(p, i) ((((int)p[(i) + 1]) << 8) + p[(i)]) -#define S16(v) ((v) < 32768 ? (v) : ((v) - 65536)) - -/* -------------------------------------------------------------------- */ -/* OBJECT ADMINISTRATION */ -/* -------------------------------------------------------------------- */ - -typedef struct { - PyObject_HEAD Imaging image; - ImagingAccess access; -} ImagingObject; - -static PyTypeObject Imaging_Type; - -#ifdef WITH_IMAGEDRAW - -typedef struct { - /* to write a character, cut out sxy from glyph data, place - at current position plus dxy, and advance by (dx, dy) */ - int dx, dy; - int dx0, dy0, dx1, dy1; - int sx0, sy0, sx1, sy1; -} Glyph; - -typedef struct { - PyObject_HEAD ImagingObject *ref; - Imaging bitmap; - int ysize; - int baseline; - Glyph glyphs[256]; -} ImagingFontObject; - -static PyTypeObject ImagingFont_Type; - -typedef struct { - PyObject_HEAD ImagingObject *image; - UINT8 ink[4]; - int blend; -} ImagingDrawObject; - -static PyTypeObject ImagingDraw_Type; - -#endif - -typedef struct { - PyObject_HEAD ImagingObject *image; - int readonly; -} PixelAccessObject; - -static PyTypeObject PixelAccess_Type; - -PyObject * -PyImagingNew(Imaging imOut) { - ImagingObject *imagep; - - if (!imOut) { - return NULL; - } - - imagep = PyObject_New(ImagingObject, &Imaging_Type); - if (imagep == NULL) { - ImagingDelete(imOut); - return NULL; - } - -#ifdef VERBOSE - printf("imaging %p allocated\n", imagep); -#endif - - imagep->image = imOut; - imagep->access = ImagingAccessNew(imOut); - - return (PyObject *)imagep; -} - -static void -_dealloc(ImagingObject *imagep) { -#ifdef VERBOSE - printf("imaging %p deleted\n", imagep); -#endif - - if (imagep->access) { - ImagingAccessDelete(imagep->image, imagep->access); - } - ImagingDelete(imagep->image); - PyObject_Del(imagep); -} - -#define PyImaging_Check(op) (Py_TYPE(op) == &Imaging_Type) - -Imaging -PyImaging_AsImaging(PyObject *op) { - if (!PyImaging_Check(op)) { - PyErr_BadInternalCall(); - return NULL; - } - - return ((ImagingObject *)op)->image; -} - -/* -------------------------------------------------------------------- */ -/* THREAD HANDLING */ -/* -------------------------------------------------------------------- */ - -void -ImagingSectionEnter(ImagingSectionCookie *cookie) { -#ifdef WITH_THREADING - *cookie = (PyThreadState *)PyEval_SaveThread(); -#endif -} - -void -ImagingSectionLeave(ImagingSectionCookie *cookie) { -#ifdef WITH_THREADING - PyEval_RestoreThread((PyThreadState *)*cookie); -#endif -} - -/* -------------------------------------------------------------------- */ -/* BUFFER HANDLING */ -/* -------------------------------------------------------------------- */ -/* Python compatibility API */ - -int -PyImaging_CheckBuffer(PyObject *buffer) { - return PyObject_CheckBuffer(buffer); -} - -int -PyImaging_GetBuffer(PyObject *buffer, Py_buffer *view) { - /* must call check_buffer first! */ - return PyObject_GetBuffer(buffer, view, PyBUF_SIMPLE); -} - -/* -------------------------------------------------------------------- */ -/* EXCEPTION REROUTING */ -/* -------------------------------------------------------------------- */ - -/* error messages */ -static const char *must_be_sequence = "argument must be a sequence"; -static const char *must_be_two_coordinates = - "coordinate list must contain exactly 2 coordinates"; -static const char *incorrectly_ordered_x_coordinate = - "x1 must be greater than or equal to x0"; -static const char *incorrectly_ordered_y_coordinate = - "y1 must be greater than or equal to y0"; -static const char *wrong_mode = "unrecognized image mode"; -static const char *wrong_raw_mode = "unrecognized raw mode"; -static const char *outside_image = "image index out of range"; -static const char *outside_palette = "palette index out of range"; -static const char *wrong_palette_size = "invalid palette size"; -static const char *no_palette = "image has no palette"; -static const char *readonly = "image is readonly"; -/* static const char* no_content = "image has no content"; */ - -void * -ImagingError_OSError(void) { - PyErr_SetString(PyExc_OSError, "error when accessing file"); - return NULL; -} - -void * -ImagingError_MemoryError(void) { - return PyErr_NoMemory(); -} - -void * -ImagingError_Mismatch(void) { - PyErr_SetString(PyExc_ValueError, "images do not match"); - return NULL; -} - -void * -ImagingError_ModeError(void) { - PyErr_SetString(PyExc_ValueError, "image has wrong mode"); - return NULL; -} - -void * -ImagingError_ValueError(const char *message) { - PyErr_SetString( - PyExc_ValueError, (message) ? (char *)message : "unrecognized argument value"); - return NULL; -} - -void -ImagingError_Clear(void) { - PyErr_Clear(); -} - -/* -------------------------------------------------------------------- */ -/* HELPERS */ -/* -------------------------------------------------------------------- */ - -static int -getbands(const char *mode) { - Imaging im; - int bands; - - /* FIXME: add primitive to libImaging to avoid extra allocation */ - im = ImagingNew(mode, 0, 0); - if (!im) { - return -1; - } - - bands = im->bands; - - ImagingDelete(im); - - return bands; -} - -#define TYPE_UINT8 (0x100 | sizeof(UINT8)) -#define TYPE_INT32 (0x200 | sizeof(INT32)) -#define TYPE_FLOAT16 (0x500 | sizeof(FLOAT16)) -#define TYPE_FLOAT32 (0x300 | sizeof(FLOAT32)) -#define TYPE_DOUBLE (0x400 | sizeof(double)) - -static void * -getlist(PyObject *arg, Py_ssize_t *length, const char *wrong_length, int type) { - /* - allocates and returns a c array of the items in the - python sequence arg. - - the size of the returned array is in length - - all of the arg items must be numeric items of the type - specified in type - - sequence length is checked against the length parameter IF - an error parameter is passed in wrong_length - - caller is responsible for freeing the memory - */ - - Py_ssize_t i, n; - int itemp; - double dtemp; - FLOAT32 ftemp; - UINT8 *list; - PyObject *seq; - PyObject *op; - - if (!PySequence_Check(arg)) { - PyErr_SetString(PyExc_TypeError, must_be_sequence); - return NULL; - } - - n = PySequence_Size(arg); - if (length && wrong_length && n != *length) { - PyErr_SetString(PyExc_ValueError, wrong_length); - return NULL; - } - - /* malloc check ok, type & ff is just a sizeof(something) - calloc checks for overflow */ - list = calloc(n, type & 0xff); - if (!list) { - return ImagingError_MemoryError(); - } - - seq = PySequence_Fast(arg, must_be_sequence); - if (!seq) { - free(list); - return NULL; - } - - for (i = 0; i < n; i++) { - op = PySequence_Fast_GET_ITEM(seq, i); - // DRY, branch prediction is going to work _really_ well - // on this switch. And 3 fewer loops to copy/paste. - switch (type) { - case TYPE_UINT8: - itemp = PyLong_AsLong(op); - list[i] = CLIP8(itemp); - break; - case TYPE_INT32: - itemp = PyLong_AsLong(op); - memcpy(list + i * sizeof(INT32), &itemp, sizeof(itemp)); - break; - case TYPE_FLOAT32: - ftemp = (FLOAT32)PyFloat_AsDouble(op); - memcpy(list + i * sizeof(ftemp), &ftemp, sizeof(ftemp)); - break; - case TYPE_DOUBLE: - dtemp = PyFloat_AsDouble(op); - memcpy(list + i * sizeof(dtemp), &dtemp, sizeof(dtemp)); - break; - } - } - - Py_DECREF(seq); - - if (PyErr_Occurred()) { - free(list); - return NULL; - } - - if (length) { - *length = n; - } - - return list; -} - -FLOAT32 -float16tofloat32(const FLOAT16 in) { - UINT32 t1; - UINT32 t2; - UINT32 t3; - FLOAT32 out[1] = {0}; - - t1 = in & 0x7fff; // Non-sign bits - t2 = in & 0x8000; // Sign bit - t3 = in & 0x7c00; // Exponent - - t1 <<= 13; // Align mantissa on MSB - t2 <<= 16; // Shift sign bit into position - - t1 += 0x38000000; // Adjust bias - - t1 = (t3 == 0 ? 0 : t1); // Denormals-as-zero - - t1 |= t2; // Re-insert sign bit - - memcpy(out, &t1, 4); - return out[0]; -} - -static inline PyObject * -getpixel(Imaging im, ImagingAccess access, int x, int y) { - union { - UINT8 b[4]; - UINT16 h; - INT32 i; - FLOAT32 f; - } pixel; - - if (x < 0) { - x = im->xsize + x; - } - if (y < 0) { - y = im->ysize + y; - } - - if (x < 0 || x >= im->xsize || y < 0 || y >= im->ysize) { - PyErr_SetString(PyExc_IndexError, outside_image); - return NULL; - } - - access->get_pixel(im, x, y, &pixel); - - switch (im->type) { - case IMAGING_TYPE_UINT8: - switch (im->bands) { - case 1: - return PyLong_FromLong(pixel.b[0]); - case 2: - return Py_BuildValue("BB", pixel.b[0], pixel.b[1]); - case 3: - return Py_BuildValue("BBB", pixel.b[0], pixel.b[1], pixel.b[2]); - case 4: - return Py_BuildValue( - "BBBB", pixel.b[0], pixel.b[1], pixel.b[2], pixel.b[3]); - } - break; - case IMAGING_TYPE_INT32: - return PyLong_FromLong(pixel.i); - case IMAGING_TYPE_FLOAT32: - return PyFloat_FromDouble(pixel.f); - case IMAGING_TYPE_SPECIAL: - if (im->bands == 1) { - return PyLong_FromLong(pixel.h); - } else { - return Py_BuildValue("BBB", pixel.b[0], pixel.b[1], pixel.b[2]); - } - break; - } - - /* unknown type */ - Py_INCREF(Py_None); - return Py_None; -} - -static char * -getink(PyObject *color, Imaging im, char *ink) { - int g = 0, b = 0, a = 0; - double f = 0; - /* Windows 64 bit longs are 32 bits, and 0xFFFFFFFF (white) is a - Python long (not int) that raises an overflow error when trying - to return it into a 32 bit C long - */ - PY_LONG_LONG r = 0; - FLOAT32 ftmp; - INT32 itmp; - - /* fill ink buffer (four bytes) with something that can - be cast to either UINT8 or INT32 */ - - int rIsInt = 0; - int tupleSize = PyTuple_Check(color) ? PyTuple_GET_SIZE(color) : -1; - if (tupleSize == 1) { - color = PyTuple_GetItem(color, 0); - } - if (im->type == IMAGING_TYPE_UINT8 || im->type == IMAGING_TYPE_INT32 || - im->type == IMAGING_TYPE_SPECIAL) { - if (PyLong_Check(color)) { - r = PyLong_AsLongLong(color); - if (r == -1 && PyErr_Occurred()) { - return NULL; - } - rIsInt = 1; - } else if (im->bands == 1) { - PyErr_SetString( - PyExc_TypeError, "color must be int or single-element tuple"); - return NULL; - } else if (tupleSize == -1) { - PyErr_SetString(PyExc_TypeError, "color must be int or tuple"); - return NULL; - } - } - - switch (im->type) { - case IMAGING_TYPE_UINT8: - /* unsigned integer */ - if (im->bands == 1) { - /* unsigned integer, single layer */ - if (rIsInt != 1) { - if (tupleSize != 1) { - PyErr_SetString( - PyExc_TypeError, - "color must be int or single-element tuple"); - return NULL; - } else if (!PyArg_ParseTuple(color, "L", &r)) { - return NULL; - } - } - ink[0] = (char)CLIP8(r); - ink[1] = ink[2] = ink[3] = 0; - } else { - if (rIsInt) { - /* compatibility: ABGR */ - a = (UINT8)(r >> 24); - b = (UINT8)(r >> 16); - g = (UINT8)(r >> 8); - r = (UINT8)r; - } else { - a = 255; - if (im->bands == 2) { - if (tupleSize != 1 && tupleSize != 2) { - PyErr_SetString( - PyExc_TypeError, - "color must be int, or tuple of one or two elements"); - return NULL; - } else if (!PyArg_ParseTuple(color, "L|i", &r, &a)) { - return NULL; - } - g = b = r; - } else { - if (tupleSize != 3 && tupleSize != 4) { - PyErr_SetString( - PyExc_TypeError, - "color must be int, or tuple of one, three or four " - "elements"); - return NULL; - } else if (!PyArg_ParseTuple(color, "Lii|i", &r, &g, &b, &a)) { - return NULL; - } - } - } - ink[0] = (char)CLIP8(r); - ink[1] = (char)CLIP8(g); - ink[2] = (char)CLIP8(b); - ink[3] = (char)CLIP8(a); - } - return ink; - case IMAGING_TYPE_INT32: - /* signed integer */ - itmp = r; - memcpy(ink, &itmp, sizeof(itmp)); - return ink; - case IMAGING_TYPE_FLOAT32: - /* floating point */ - f = PyFloat_AsDouble(color); - if (f == -1.0 && PyErr_Occurred()) { - return NULL; - } - ftmp = f; - memcpy(ink, &ftmp, sizeof(ftmp)); - return ink; - case IMAGING_TYPE_SPECIAL: - if (strncmp(im->mode, "I;16", 4) == 0) { - ink[0] = (UINT8)r; - ink[1] = (UINT8)(r >> 8); - ink[2] = ink[3] = 0; - return ink; - } else { - if (rIsInt) { - b = (UINT8)(r >> 16); - g = (UINT8)(r >> 8); - r = (UINT8)r; - } else if (tupleSize != 3) { - PyErr_SetString( - PyExc_TypeError, - "color must be int, or tuple of one or three elements"); - return NULL; - } else if (!PyArg_ParseTuple(color, "iiL", &b, &g, &r)) { - return NULL; - } - if (!strcmp(im->mode, "BGR;15")) { - UINT16 v = ((((UINT16)r) << 7) & 0x7c00) + - ((((UINT16)g) << 2) & 0x03e0) + - ((((UINT16)b) >> 3) & 0x001f); - - ink[0] = (UINT8)v; - ink[1] = (UINT8)(v >> 8); - ink[2] = ink[3] = 0; - return ink; - } else if (!strcmp(im->mode, "BGR;16")) { - UINT16 v = ((((UINT16)r) << 8) & 0xf800) + - ((((UINT16)g) << 3) & 0x07e0) + - ((((UINT16)b) >> 3) & 0x001f); - ink[0] = (UINT8)v; - ink[1] = (UINT8)(v >> 8); - ink[2] = ink[3] = 0; - return ink; - } else if (!strcmp(im->mode, "BGR;24")) { - ink[0] = (UINT8)b; - ink[1] = (UINT8)g; - ink[2] = (UINT8)r; - ink[3] = 0; - return ink; - } - } - } - - PyErr_SetString(PyExc_ValueError, wrong_mode); - return NULL; -} - -/* -------------------------------------------------------------------- */ -/* FACTORIES */ -/* -------------------------------------------------------------------- */ - -static PyObject * -_fill(PyObject *self, PyObject *args) { - char *mode; - int xsize, ysize; - PyObject *color; - char buffer[4]; - Imaging im; - - xsize = ysize = 256; - color = NULL; - - if (!PyArg_ParseTuple(args, "s|(ii)O", &mode, &xsize, &ysize, &color)) { - return NULL; - } - - im = ImagingNewDirty(mode, xsize, ysize); - if (!im) { - return NULL; - } - - buffer[0] = buffer[1] = buffer[2] = buffer[3] = 0; - if (color) { - if (!getink(color, im, buffer)) { - ImagingDelete(im); - return NULL; - } - } - - (void)ImagingFill(im, buffer); - - return PyImagingNew(im); -} - -static PyObject * -_new(PyObject *self, PyObject *args) { - char *mode; - int xsize, ysize; - - if (!PyArg_ParseTuple(args, "s(ii)", &mode, &xsize, &ysize)) { - return NULL; - } - - return PyImagingNew(ImagingNew(mode, xsize, ysize)); -} - -static PyObject * -_new_block(PyObject *self, PyObject *args) { - char *mode; - int xsize, ysize; - - if (!PyArg_ParseTuple(args, "s(ii)", &mode, &xsize, &ysize)) { - return NULL; - } - - return PyImagingNew(ImagingNewBlock(mode, xsize, ysize)); -} - -static PyObject * -_linear_gradient(PyObject *self, PyObject *args) { - char *mode; - - if (!PyArg_ParseTuple(args, "s", &mode)) { - return NULL; - } - - return PyImagingNew(ImagingFillLinearGradient(mode)); -} - -static PyObject * -_radial_gradient(PyObject *self, PyObject *args) { - char *mode; - - if (!PyArg_ParseTuple(args, "s", &mode)) { - return NULL; - } - - return PyImagingNew(ImagingFillRadialGradient(mode)); -} - -static PyObject * -_alpha_composite(ImagingObject *self, PyObject *args) { - ImagingObject *imagep1; - ImagingObject *imagep2; - - if (!PyArg_ParseTuple( - args, "O!O!", &Imaging_Type, &imagep1, &Imaging_Type, &imagep2)) { - return NULL; - } - - return PyImagingNew(ImagingAlphaComposite(imagep1->image, imagep2->image)); -} - -static PyObject * -_blend(ImagingObject *self, PyObject *args) { - ImagingObject *imagep1; - ImagingObject *imagep2; - double alpha; - - alpha = 0.5; - if (!PyArg_ParseTuple( - args, "O!O!|d", &Imaging_Type, &imagep1, &Imaging_Type, &imagep2, &alpha)) { - return NULL; - } - - return PyImagingNew(ImagingBlend(imagep1->image, imagep2->image, (float)alpha)); -} - -/* -------------------------------------------------------------------- */ -/* METHODS */ -/* -------------------------------------------------------------------- */ - -static INT16 * -_prepare_lut_table(PyObject *table, Py_ssize_t table_size) { - int i; - Py_buffer buffer_info; - INT32 data_type = TYPE_FLOAT32; - float item = 0; - void *table_data = NULL; - int free_table_data = 0; - INT16 *prepared; - -/* NOTE: This value should be the same as in ColorLUT.c */ -#define PRECISION_BITS (16 - 8 - 2) - - const char *wrong_size = - ("The table should have table_channels * " - "size1D * size2D * size3D float items."); - - if (PyObject_CheckBuffer(table)) { - if (!PyObject_GetBuffer(table, &buffer_info, PyBUF_CONTIG_RO | PyBUF_FORMAT)) { - if (buffer_info.ndim == 1 && buffer_info.shape[0] == table_size) { - if (strlen(buffer_info.format) == 1) { - switch (buffer_info.format[0]) { - case 'e': - data_type = TYPE_FLOAT16; - table_data = buffer_info.buf; - break; - case 'f': - data_type = TYPE_FLOAT32; - table_data = buffer_info.buf; - break; - case 'd': - data_type = TYPE_DOUBLE; - table_data = buffer_info.buf; - break; - } - } - } - PyBuffer_Release(&buffer_info); - } - } - - if (!table_data) { - free_table_data = 1; - table_data = getlist(table, &table_size, wrong_size, TYPE_FLOAT32); - if (!table_data) { - return NULL; - } - } - - /* malloc check ok, max is 2 * 4 * 65**3 = 2197000 */ - prepared = (INT16 *)malloc(sizeof(INT16) * table_size); - if (!prepared) { - if (free_table_data) { - free(table_data); - } - return (INT16 *)ImagingError_MemoryError(); - } - - for (i = 0; i < table_size; i++) { - FLOAT16 htmp; - double dtmp; - switch (data_type) { - case TYPE_FLOAT16: - memcpy(&htmp, ((char *)table_data) + i * sizeof(htmp), sizeof(htmp)); - item = float16tofloat32(htmp); - break; - case TYPE_FLOAT32: - memcpy( - &item, ((char *)table_data) + i * sizeof(FLOAT32), sizeof(FLOAT32)); - break; - case TYPE_DOUBLE: - memcpy(&dtmp, ((char *)table_data) + i * sizeof(dtmp), sizeof(dtmp)); - item = (FLOAT32)dtmp; - break; - } - /* Max value for INT16 */ - if (item >= (0x7fff - 0.5) / (255 << PRECISION_BITS)) { - prepared[i] = 0x7fff; - continue; - } - /* Min value for INT16 */ - if (item <= (-0x8000 + 0.5) / (255 << PRECISION_BITS)) { - prepared[i] = -0x8000; - continue; - } - if (item < 0) { - prepared[i] = item * (255 << PRECISION_BITS) - 0.5; - } else { - prepared[i] = item * (255 << PRECISION_BITS) + 0.5; - } - } - -#undef PRECISION_BITS - if (free_table_data) { - free(table_data); - } - return prepared; -} - -static PyObject * -_color_lut_3d(ImagingObject *self, PyObject *args) { - char *mode; - int filter; - int table_channels; - int size1D, size2D, size3D; - PyObject *table; - - INT16 *prepared_table; - Imaging imOut; - - if (!PyArg_ParseTuple( - args, - "siiiiiO:color_lut_3d", - &mode, - &filter, - &table_channels, - &size1D, - &size2D, - &size3D, - &table)) { - return NULL; - } - - /* actually, it is trilinear */ - if (filter != IMAGING_TRANSFORM_BILINEAR) { - PyErr_SetString(PyExc_ValueError, "Only LINEAR filter is supported."); - return NULL; - } - - if (1 > table_channels || table_channels > 4) { - PyErr_SetString(PyExc_ValueError, "table_channels should be from 1 to 4"); - return NULL; - } - - if (2 > size1D || size1D > 65 || 2 > size2D || size2D > 65 || 2 > size3D || - size3D > 65) { - PyErr_SetString( - PyExc_ValueError, "Table size in any dimension should be from 2 to 65"); - return NULL; - } - - prepared_table = - _prepare_lut_table(table, table_channels * size1D * size2D * size3D); - if (!prepared_table) { - return NULL; - } - - imOut = ImagingNewDirty(mode, self->image->xsize, self->image->ysize); - if (!imOut) { - free(prepared_table); - return NULL; - } - - if (!ImagingColorLUT3D_linear( - imOut, - self->image, - table_channels, - size1D, - size2D, - size3D, - prepared_table)) { - free(prepared_table); - ImagingDelete(imOut); - return NULL; - } - - free(prepared_table); - - return PyImagingNew(imOut); -} - -static PyObject * -_convert(ImagingObject *self, PyObject *args) { - char *mode; - int dither = 0; - ImagingObject *paletteimage = NULL; - - if (!PyArg_ParseTuple(args, "s|iO", &mode, &dither, &paletteimage)) { - return NULL; - } - if (paletteimage != NULL) { - if (!PyImaging_Check(paletteimage)) { - PyObject_Print((PyObject *)paletteimage, stderr, 0); - PyErr_SetString( - PyExc_ValueError, "palette argument must be image with mode 'P'"); - return NULL; - } - if (paletteimage->image->palette == NULL) { - PyErr_SetString(PyExc_ValueError, "null palette"); - return NULL; - } - } - - return PyImagingNew(ImagingConvert( - self->image, mode, paletteimage ? paletteimage->image->palette : NULL, dither)); -} - -static PyObject * -_convert2(ImagingObject *self, PyObject *args) { - ImagingObject *imagep1; - ImagingObject *imagep2; - if (!PyArg_ParseTuple( - args, "O!O!", &Imaging_Type, &imagep1, &Imaging_Type, &imagep2)) { - return NULL; - } - - if (!ImagingConvert2(imagep1->image, imagep2->image)) { - return NULL; - } - - Py_INCREF(Py_None); - return Py_None; -} - -static PyObject * -_convert_matrix(ImagingObject *self, PyObject *args) { - char *mode; - float m[12]; - if (!PyArg_ParseTuple(args, "s(ffff)", &mode, m + 0, m + 1, m + 2, m + 3)) { - PyErr_Clear(); - if (!PyArg_ParseTuple( - args, - "s(ffffffffffff)", - &mode, - m + 0, - m + 1, - m + 2, - m + 3, - m + 4, - m + 5, - m + 6, - m + 7, - m + 8, - m + 9, - m + 10, - m + 11)) { - return NULL; - } - } - - return PyImagingNew(ImagingConvertMatrix(self->image, mode, m)); -} - -static PyObject * -_convert_transparent(ImagingObject *self, PyObject *args) { - char *mode; - int r, g, b; - if (PyArg_ParseTuple(args, "s(iii)", &mode, &r, &g, &b)) { - return PyImagingNew(ImagingConvertTransparent(self->image, mode, r, g, b)); - } - PyErr_Clear(); - if (PyArg_ParseTuple(args, "si", &mode, &r)) { - return PyImagingNew(ImagingConvertTransparent(self->image, mode, r, 0, 0)); - } - return NULL; -} - -static PyObject * -_copy(ImagingObject *self, PyObject *args) { - if (!PyArg_ParseTuple(args, "")) { - return NULL; - } - - return PyImagingNew(ImagingCopy(self->image)); -} - -static PyObject * -_crop(ImagingObject *self, PyObject *args) { - int x0, y0, x1, y1; - if (!PyArg_ParseTuple(args, "(iiii)", &x0, &y0, &x1, &y1)) { - return NULL; - } - - return PyImagingNew(ImagingCrop(self->image, x0, y0, x1, y1)); -} - -static PyObject * -_expand_image(ImagingObject *self, PyObject *args) { - int x, y; - if (!PyArg_ParseTuple(args, "ii", &x, &y)) { - return NULL; - } - - return PyImagingNew(ImagingExpand(self->image, x, y)); -} - -static PyObject * -_filter(ImagingObject *self, PyObject *args) { - PyObject *imOut; - Py_ssize_t kernelsize; - FLOAT32 *kerneldata; - - int xsize, ysize, i; - float divisor, offset; - PyObject *kernel = NULL; - if (!PyArg_ParseTuple( - args, "(ii)ffO", &xsize, &ysize, &divisor, &offset, &kernel)) { - return NULL; - } - - /* get user-defined kernel */ - kerneldata = getlist(kernel, &kernelsize, NULL, TYPE_FLOAT32); - if (!kerneldata) { - return NULL; - } - if (kernelsize != (Py_ssize_t)xsize * (Py_ssize_t)ysize) { - free(kerneldata); - return ImagingError_ValueError("bad kernel size"); - } - - for (i = 0; i < kernelsize; ++i) { - kerneldata[i] /= divisor; - } - - imOut = PyImagingNew(ImagingFilter(self->image, xsize, ysize, kerneldata, offset)); - - free(kerneldata); - - return imOut; -} - -#ifdef WITH_UNSHARPMASK -static PyObject * -_gaussian_blur(ImagingObject *self, PyObject *args) { - Imaging imIn; - Imaging imOut; - - float xradius, yradius; - int passes = 3; - if (!PyArg_ParseTuple(args, "(ff)|i", &xradius, &yradius, &passes)) { - return NULL; - } - - imIn = self->image; - imOut = ImagingNewDirty(imIn->mode, imIn->xsize, imIn->ysize); - if (!imOut) { - return NULL; - } - - if (!ImagingGaussianBlur(imOut, imIn, xradius, yradius, passes)) { - ImagingDelete(imOut); - return NULL; - } - - return PyImagingNew(imOut); -} -#endif - -static PyObject * -_getpalette(ImagingObject *self, PyObject *args) { - PyObject *palette; - int palettesize; - int bits; - ImagingShuffler pack; - - char *mode = "RGB"; - char *rawmode = "RGB"; - if (!PyArg_ParseTuple(args, "|ss", &mode, &rawmode)) { - return NULL; - } - - if (!self->image->palette) { - PyErr_SetString(PyExc_ValueError, no_palette); - return NULL; - } - - pack = ImagingFindPacker(mode, rawmode, &bits); - if (!pack) { - PyErr_SetString(PyExc_ValueError, wrong_raw_mode); - return NULL; - } - - palettesize = self->image->palette->size; - palette = PyBytes_FromStringAndSize(NULL, palettesize * bits / 8); - if (!palette) { - return NULL; - } - - pack( - (UINT8 *)PyBytes_AsString(palette), self->image->palette->palette, palettesize); - - return palette; -} - -static PyObject * -_getpalettemode(ImagingObject *self) { - if (!self->image->palette) { - PyErr_SetString(PyExc_ValueError, no_palette); - return NULL; - } - - return PyUnicode_FromString(self->image->palette->mode); -} - -static inline int -_getxy(PyObject *xy, int *x, int *y) { - PyObject *value; - - if (!PyTuple_Check(xy) || PyTuple_GET_SIZE(xy) != 2) { - goto badarg; - } - - value = PyTuple_GET_ITEM(xy, 0); - if (PyLong_Check(value)) { - *x = PyLong_AS_LONG(value); - } else if (PyFloat_Check(value)) { - *x = (int)PyFloat_AS_DOUBLE(value); - } else { - PyObject *int_value = PyObject_CallMethod(value, "__int__", NULL); - if (int_value != NULL && PyLong_Check(int_value)) { - *x = PyLong_AS_LONG(int_value); - } else { - goto badval; - } - } - - value = PyTuple_GET_ITEM(xy, 1); - if (PyLong_Check(value)) { - *y = PyLong_AS_LONG(value); - } else if (PyFloat_Check(value)) { - *y = (int)PyFloat_AS_DOUBLE(value); - } else { - PyObject *int_value = PyObject_CallMethod(value, "__int__", NULL); - if (int_value != NULL && PyLong_Check(int_value)) { - *y = PyLong_AS_LONG(int_value); - } else { - goto badval; - } - } - - return 0; - -badarg: - PyErr_SetString(PyExc_TypeError, "argument must be sequence of length 2"); - return -1; - -badval: - PyErr_SetString(PyExc_TypeError, "an integer is required"); - return -1; -} - -static PyObject * -_getpixel(ImagingObject *self, PyObject *args) { - PyObject *xy; - int x, y; - - if (PyTuple_GET_SIZE(args) != 1) { - PyErr_SetString(PyExc_TypeError, "argument 1 must be sequence of length 2"); - return NULL; - } - - xy = PyTuple_GET_ITEM(args, 0); - - if (_getxy(xy, &x, &y)) { - return NULL; - } - - if (self->access == NULL) { - Py_INCREF(Py_None); - return Py_None; - } - - return getpixel(self->image, self->access, x, y); -} - -union hist_extrema { - UINT8 u[2]; - INT32 i[2]; - FLOAT32 f[2]; -}; - -static union hist_extrema * -parse_histogram_extremap( - ImagingObject *self, PyObject *extremap, union hist_extrema *ep) { - int i0, i1; - double f0, f1; - - if (extremap) { - switch (self->image->type) { - case IMAGING_TYPE_UINT8: - if (!PyArg_ParseTuple(extremap, "ii", &i0, &i1)) { - return NULL; - } - ep->u[0] = CLIP8(i0); - ep->u[1] = CLIP8(i1); - break; - case IMAGING_TYPE_INT32: - if (!PyArg_ParseTuple(extremap, "ii", &i0, &i1)) { - return NULL; - } - ep->i[0] = i0; - ep->i[1] = i1; - break; - case IMAGING_TYPE_FLOAT32: - if (!PyArg_ParseTuple(extremap, "dd", &f0, &f1)) { - return NULL; - } - ep->f[0] = (FLOAT32)f0; - ep->f[1] = (FLOAT32)f1; - break; - default: - return NULL; - } - } else { - return NULL; - } - return ep; -} - -static PyObject * -_histogram(ImagingObject *self, PyObject *args) { - ImagingHistogram h; - PyObject *list; - int i; - union hist_extrema extrema; - union hist_extrema *ep; - - PyObject *extremap = NULL; - ImagingObject *maskp = NULL; - if (!PyArg_ParseTuple(args, "|OO!", &extremap, &Imaging_Type, &maskp)) { - return NULL; - } - - /* Using a var to avoid allocations. */ - ep = parse_histogram_extremap(self, extremap, &extrema); - h = ImagingGetHistogram(self->image, (maskp) ? maskp->image : NULL, ep); - - if (!h) { - return NULL; - } - - /* Build an integer list containing the histogram */ - list = PyList_New(h->bands * 256); - if (list == NULL) { - ImagingHistogramDelete(h); - return NULL; - } - for (i = 0; i < h->bands * 256; i++) { - PyObject *item; - item = PyLong_FromLong(h->histogram[i]); - if (item == NULL) { - Py_DECREF(list); - list = NULL; - break; - } - PyList_SetItem(list, i, item); - } - - /* Destroy the histogram structure */ - ImagingHistogramDelete(h); - - return list; -} - -static PyObject * -_entropy(ImagingObject *self, PyObject *args) { - ImagingHistogram h; - int idx, length; - long sum; - double entropy, fsum, p; - union hist_extrema extrema; - union hist_extrema *ep; - - PyObject *extremap = NULL; - ImagingObject *maskp = NULL; - if (!PyArg_ParseTuple(args, "|OO!", &extremap, &Imaging_Type, &maskp)) { - return NULL; - } - - /* Using a local var to avoid allocations. */ - ep = parse_histogram_extremap(self, extremap, &extrema); - h = ImagingGetHistogram(self->image, (maskp) ? maskp->image : NULL, ep); - - if (!h) { - return NULL; - } - - /* Calculate the histogram entropy */ - /* First, sum the histogram data */ - length = h->bands * 256; - sum = 0; - for (idx = 0; idx < length; idx++) { - sum += h->histogram[idx]; - } - - /* Next, normalize the histogram data, */ - /* using the histogram sum value */ - fsum = (double)sum; - entropy = 0.0; - for (idx = 0; idx < length; idx++) { - p = (double)h->histogram[idx] / fsum; - if (p != 0.0) { - entropy += p * log(p) * M_LOG2E; - } - } - - /* Destroy the histogram structure */ - ImagingHistogramDelete(h); - - return PyFloat_FromDouble(-entropy); -} - -#ifdef WITH_MODEFILTER -static PyObject * -_modefilter(ImagingObject *self, PyObject *args) { - int size; - if (!PyArg_ParseTuple(args, "i", &size)) { - return NULL; - } - - return PyImagingNew(ImagingModeFilter(self->image, size)); -} -#endif - -static PyObject * -_offset(ImagingObject *self, PyObject *args) { - int xoffset, yoffset; - if (!PyArg_ParseTuple(args, "ii", &xoffset, &yoffset)) { - return NULL; - } - - return PyImagingNew(ImagingOffset(self->image, xoffset, yoffset)); -} - -static PyObject * -_paste(ImagingObject *self, PyObject *args) { - int status; - char ink[4]; - - PyObject *source; - int x0, y0, x1, y1; - ImagingObject *maskp = NULL; - if (!PyArg_ParseTuple( - args, "O(iiii)|O!", &source, &x0, &y0, &x1, &y1, &Imaging_Type, &maskp)) { - return NULL; - } - - if (PyImaging_Check(source)) { - status = ImagingPaste( - self->image, - PyImaging_AsImaging(source), - (maskp) ? maskp->image : NULL, - x0, - y0, - x1, - y1); - - } else { - if (!getink(source, self->image, ink)) { - return NULL; - } - status = ImagingFill2( - self->image, ink, (maskp) ? maskp->image : NULL, x0, y0, x1, y1); - } - - if (status < 0) { - return NULL; - } - - Py_INCREF(Py_None); - return Py_None; -} - -static PyObject * -_point(ImagingObject *self, PyObject *args) { - static const char *wrong_number = "wrong number of lut entries"; - - Py_ssize_t n; - int i, bands; - Imaging im; - - PyObject *list; - char *mode; - if (!PyArg_ParseTuple(args, "Oz", &list, &mode)) { - return NULL; - } - - if (mode && !strcmp(mode, "F")) { - FLOAT32 *data; - - /* map from 8-bit data to floating point */ - n = 256; - data = getlist(list, &n, wrong_number, TYPE_FLOAT32); - if (!data) { - return NULL; - } - im = ImagingPoint(self->image, mode, (void *)data); - free(data); - - } else if (!strcmp(self->image->mode, "I") && mode && !strcmp(mode, "L")) { - UINT8 *data; - - /* map from 16-bit subset of 32-bit data to 8-bit */ - /* FIXME: support arbitrary number of entries (requires API change) */ - n = 65536; - data = getlist(list, &n, wrong_number, TYPE_UINT8); - if (!data) { - return NULL; - } - im = ImagingPoint(self->image, mode, (void *)data); - free(data); - - } else { - INT32 *data; - UINT8 lut[1024]; - - if (mode) { - bands = getbands(mode); - if (bands < 0) { - return NULL; - } - } else { - bands = self->image->bands; - } - - /* map to integer data */ - n = 256 * bands; - data = getlist(list, &n, wrong_number, TYPE_INT32); - if (!data) { - return NULL; - } - - if (mode && !strcmp(mode, "I")) { - im = ImagingPoint(self->image, mode, (void *)data); - } else if (mode && bands > 1) { - for (i = 0; i < 256; i++) { - lut[i * 4] = CLIP8(data[i]); - lut[i * 4 + 1] = CLIP8(data[i + 256]); - lut[i * 4 + 2] = CLIP8(data[i + 512]); - if (n > 768) { - lut[i * 4 + 3] = CLIP8(data[i + 768]); - } - } - im = ImagingPoint(self->image, mode, (void *)lut); - } else { - /* map individual bands */ - for (i = 0; i < n; i++) { - lut[i] = CLIP8(data[i]); - } - im = ImagingPoint(self->image, mode, (void *)lut); - } - free(data); - } - - return PyImagingNew(im); -} - -static PyObject * -_point_transform(ImagingObject *self, PyObject *args) { - double scale = 1.0; - double offset = 0.0; - if (!PyArg_ParseTuple(args, "|dd", &scale, &offset)) { - return NULL; - } - - return PyImagingNew(ImagingPointTransform(self->image, scale, offset)); -} - -static PyObject * -_putdata(ImagingObject *self, PyObject *args) { - Imaging image; - // i & n are # pixels, require py_ssize_t. x can be as large as n. y, just because. - Py_ssize_t n, i, x, y; - - PyObject *data; - PyObject *seq = NULL; - PyObject *op; - double scale = 1.0; - double offset = 0.0; - - if (!PyArg_ParseTuple(args, "O|dd", &data, &scale, &offset)) { - return NULL; - } - - if (!PySequence_Check(data)) { - PyErr_SetString(PyExc_TypeError, must_be_sequence); - return NULL; - } - - image = self->image; - - n = PyObject_Length(data); - if (n > (Py_ssize_t)image->xsize * (Py_ssize_t)image->ysize) { - PyErr_SetString(PyExc_TypeError, "too many data entries"); - return NULL; - } - -#define set_value_to_item(seq, i) \ - op = PySequence_Fast_GET_ITEM(seq, i); \ - if (PySequence_Check(op)) { \ - PyErr_SetString(PyExc_TypeError, "sequence must be flattened"); \ - return NULL; \ - } else { \ - value = PyFloat_AsDouble(op); \ - } - if (image->image8) { - if (PyBytes_Check(data)) { - unsigned char *p; - p = (unsigned char *)PyBytes_AS_STRING(data); - if (scale == 1.0 && offset == 0.0) { - /* Plain string data */ - for (i = y = 0; i < n; i += image->xsize, y++) { - x = n - i; - if (x > (int)image->xsize) { - x = image->xsize; - } - memcpy(image->image8[y], p + i, x); - } - } else { - /* Scaled and clipped string data */ - for (i = x = y = 0; i < n; i++) { - image->image8[y][x] = CLIP8((int)(p[i] * scale + offset)); - if (++x >= (int)image->xsize) { - x = 0, y++; - } - } - } - } else { - seq = PySequence_Fast(data, must_be_sequence); - if (!seq) { - PyErr_SetString(PyExc_TypeError, must_be_sequence); - return NULL; - } - double value; - if (image->bands == 1) { - int bigendian = 0; - if (image->type == IMAGING_TYPE_SPECIAL) { - // I;16* - if (strcmp(image->mode, "I;16N") == 0) { -#ifdef WORDS_BIGENDIAN - bigendian = 1; -#else - bigendian = 0; -#endif - } else if (strcmp(image->mode, "I;16B") == 0) { - bigendian = 1; - } else { - bigendian = 0; - } - } - for (i = x = y = 0; i < n; i++) { - set_value_to_item(seq, i); - if (scale != 1.0 || offset != 0.0) { - value = value * scale + offset; - } - if (image->type == IMAGING_TYPE_SPECIAL) { - image->image8[y][x * 2 + (bigendian ? 1 : 0)] = - CLIP8((int)value % 256); - image->image8[y][x * 2 + (bigendian ? 0 : 1)] = - CLIP8((int)value >> 8); - } else { - image->image8[y][x] = (UINT8)CLIP8(value); - } - if (++x >= (int)image->xsize) { - x = 0, y++; - } - } - } else { - // BGR;* - int b; - for (i = x = y = 0; i < n; i++) { - char ink[4]; - - op = PySequence_Fast_GET_ITEM(seq, i); - if (!op || !getink(op, image, ink)) { - Py_DECREF(seq); - return NULL; - } - /* FIXME: what about scale and offset? */ - for (b = 0; b < image->pixelsize; b++) { - image->image8[y][x * image->pixelsize + b] = ink[b]; - } - if (++x >= (int)image->xsize) { - x = 0, y++; - } - } - } - PyErr_Clear(); /* Avoid weird exceptions */ - } - } else { - /* 32-bit images */ - seq = PySequence_Fast(data, must_be_sequence); - if (!seq) { - PyErr_SetString(PyExc_TypeError, must_be_sequence); - return NULL; - } - switch (image->type) { - case IMAGING_TYPE_INT32: - for (i = x = y = 0; i < n; i++) { - double value; - set_value_to_item(seq, i); - IMAGING_PIXEL_INT32(image, x, y) = (INT32)(value * scale + offset); - if (++x >= (int)image->xsize) { - x = 0, y++; - } - } - PyErr_Clear(); /* Avoid weird exceptions */ - break; - case IMAGING_TYPE_FLOAT32: - for (i = x = y = 0; i < n; i++) { - double value; - set_value_to_item(seq, i); - IMAGING_PIXEL_FLOAT32(image, x, y) = - (FLOAT32)(value * scale + offset); - if (++x >= (int)image->xsize) { - x = 0, y++; - } - } - PyErr_Clear(); /* Avoid weird exceptions */ - break; - default: - for (i = x = y = 0; i < n; i++) { - union { - char ink[4]; - INT32 inkint; - } u; - - u.inkint = 0; - - op = PySequence_Fast_GET_ITEM(seq, i); - if (!op || !getink(op, image, u.ink)) { - Py_DECREF(seq); - return NULL; - } - /* FIXME: what about scale and offset? */ - image->image32[y][x] = u.inkint; - if (++x >= (int)image->xsize) { - x = 0, y++; - } - } - PyErr_Clear(); /* Avoid weird exceptions */ - break; - } - } - - Py_XDECREF(seq); - - Py_INCREF(Py_None); - return Py_None; -} - -#ifdef WITH_QUANTIZE - -static PyObject * -_quantize(ImagingObject *self, PyObject *args) { - int colours = 256; - int method = 0; - int kmeans = 0; - if (!PyArg_ParseTuple(args, "|iii", &colours, &method, &kmeans)) { - return NULL; - } - - if (!self->image->xsize || !self->image->ysize) { - /* no content; return an empty image */ - return PyImagingNew(ImagingNew("P", self->image->xsize, self->image->ysize)); - } - - return PyImagingNew(ImagingQuantize(self->image, colours, method, kmeans)); -} -#endif - -static PyObject * -_putpalette(ImagingObject *self, PyObject *args) { - ImagingShuffler unpack; - int bits; - - char *rawmode, *palette_mode; - UINT8 *palette; - Py_ssize_t palettesize; - if (!PyArg_ParseTuple(args, "sy#", &rawmode, &palette, &palettesize)) { - return NULL; - } - - if (strcmp(self->image->mode, "L") && strcmp(self->image->mode, "LA") && - strcmp(self->image->mode, "P") && strcmp(self->image->mode, "PA")) { - PyErr_SetString(PyExc_ValueError, wrong_mode); - return NULL; - } - - palette_mode = strncmp("RGBA", rawmode, 4) == 0 ? "RGBA" : "RGB"; - unpack = ImagingFindUnpacker(palette_mode, rawmode, &bits); - if (!unpack) { - PyErr_SetString(PyExc_ValueError, wrong_raw_mode); - return NULL; - } - - if (palettesize * 8 / bits > 256) { - PyErr_SetString(PyExc_ValueError, wrong_palette_size); - return NULL; - } - - ImagingPaletteDelete(self->image->palette); - - strcpy(self->image->mode, strlen(self->image->mode) == 2 ? "PA" : "P"); - - self->image->palette = ImagingPaletteNew(palette_mode); - - self->image->palette->size = palettesize * 8 / bits; - unpack(self->image->palette->palette, palette, self->image->palette->size); - - Py_INCREF(Py_None); - return Py_None; -} - -static PyObject * -_putpalettealpha(ImagingObject *self, PyObject *args) { - int index; - int alpha = 0; - if (!PyArg_ParseTuple(args, "i|i", &index, &alpha)) { - return NULL; - } - - if (!self->image->palette) { - PyErr_SetString(PyExc_ValueError, no_palette); - return NULL; - } - - if (index < 0 || index >= 256) { - PyErr_SetString(PyExc_ValueError, outside_palette); - return NULL; - } - - strcpy(self->image->palette->mode, "RGBA"); - self->image->palette->palette[index * 4 + 3] = (UINT8)alpha; - - Py_INCREF(Py_None); - return Py_None; -} - -static PyObject * -_putpalettealphas(ImagingObject *self, PyObject *args) { - int i; - UINT8 *values; - Py_ssize_t length; - if (!PyArg_ParseTuple(args, "y#", &values, &length)) { - return NULL; - } - - if (!self->image->palette) { - PyErr_SetString(PyExc_ValueError, no_palette); - return NULL; - } - - if (length > 256) { - PyErr_SetString(PyExc_ValueError, outside_palette); - return NULL; - } - - strcpy(self->image->palette->mode, "RGBA"); - for (i = 0; i < length; i++) { - self->image->palette->palette[i * 4 + 3] = (UINT8)values[i]; - } - - Py_INCREF(Py_None); - return Py_None; -} - -static PyObject * -_putpixel(ImagingObject *self, PyObject *args) { - Imaging im; - char ink[4]; - - int x, y; - PyObject *color; - if (!PyArg_ParseTuple(args, "(ii)O", &x, &y, &color)) { - return NULL; - } - - im = self->image; - - if (x < 0) { - x = im->xsize + x; - } - if (y < 0) { - y = im->ysize + y; - } - - if (x < 0 || x >= im->xsize || y < 0 || y >= im->ysize) { - PyErr_SetString(PyExc_IndexError, outside_image); - return NULL; - } - - if (!getink(color, im, ink)) { - return NULL; - } - - if (self->access) { - self->access->put_pixel(im, x, y, ink); - } - - Py_INCREF(Py_None); - return Py_None; -} - -#ifdef WITH_RANKFILTER -static PyObject * -_rankfilter(ImagingObject *self, PyObject *args) { - int size, rank; - if (!PyArg_ParseTuple(args, "ii", &size, &rank)) { - return NULL; - } - - return PyImagingNew(ImagingRankFilter(self->image, size, rank)); -} -#endif - -static PyObject * -_resize(ImagingObject *self, PyObject *args) { - Imaging imIn; - Imaging imOut; - - int xsize, ysize; - int filter = IMAGING_TRANSFORM_NEAREST; - float box[4] = {0, 0, 0, 0}; - - imIn = self->image; - box[2] = imIn->xsize; - box[3] = imIn->ysize; - - if (!PyArg_ParseTuple( - args, - "(ii)|i(ffff)", - &xsize, - &ysize, - &filter, - &box[0], - &box[1], - &box[2], - &box[3])) { - return NULL; - } - - if (xsize < 1 || ysize < 1) { - return ImagingError_ValueError("height and width must be > 0"); - } - - if (box[0] < 0 || box[1] < 0) { - return ImagingError_ValueError("box offset can't be negative"); - } - - if (box[2] > imIn->xsize || box[3] > imIn->ysize) { - return ImagingError_ValueError("box can't exceed original image size"); - } - - if (box[2] - box[0] < 0 || box[3] - box[1] < 0) { - return ImagingError_ValueError("box can't be empty"); - } - - // If box's coordinates are int and box size matches requested size - if (box[0] - (int)box[0] == 0 && box[2] - box[0] == xsize && - box[1] - (int)box[1] == 0 && box[3] - box[1] == ysize) { - imOut = ImagingCrop(imIn, box[0], box[1], box[2], box[3]); - } else if (filter == IMAGING_TRANSFORM_NEAREST) { - double a[8]; - - memset(a, 0, sizeof a); - a[0] = (double)(box[2] - box[0]) / xsize; - a[4] = (double)(box[3] - box[1]) / ysize; - a[2] = box[0]; - a[5] = box[1]; - - imOut = ImagingNewDirty(imIn->mode, xsize, ysize); - - imOut = ImagingTransform( - imOut, imIn, IMAGING_TRANSFORM_AFFINE, 0, 0, xsize, ysize, a, filter, 1); - } else { - imOut = ImagingResample(imIn, xsize, ysize, filter, box); - } - - return PyImagingNew(imOut); -} - -static PyObject * -_reduce(ImagingObject *self, PyObject *args) { - Imaging imIn; - Imaging imOut; - - int xscale, yscale; - int box[4] = {0, 0, 0, 0}; - - imIn = self->image; - box[2] = imIn->xsize; - box[3] = imIn->ysize; - - if (!PyArg_ParseTuple( - args, - "(ii)|(iiii)", - &xscale, - &yscale, - &box[0], - &box[1], - &box[2], - &box[3])) { - return NULL; - } - - if (xscale < 1 || yscale < 1) { - return ImagingError_ValueError("scale must be > 0"); - } - - if (box[0] < 0 || box[1] < 0) { - return ImagingError_ValueError("box offset can't be negative"); - } - - if (box[2] > imIn->xsize || box[3] > imIn->ysize) { - return ImagingError_ValueError("box can't exceed original image size"); - } - - if (box[2] <= box[0] || box[3] <= box[1]) { - return ImagingError_ValueError("box can't be empty"); - } - - if (xscale == 1 && yscale == 1) { - imOut = ImagingCrop(imIn, box[0], box[1], box[2], box[3]); - } else { - // Change box format: (left, top, width, height) - box[2] -= box[0]; - box[3] -= box[1]; - imOut = ImagingReduce(imIn, xscale, yscale, box); - } - - return PyImagingNew(imOut); -} - -#define IS_RGB(mode) \ - (!strcmp(mode, "RGB") || !strcmp(mode, "RGBA") || !strcmp(mode, "RGBX")) - -static PyObject * -im_setmode(ImagingObject *self, PyObject *args) { - /* attempt to modify the mode of an image in place */ - - Imaging im; - - char *mode; - Py_ssize_t modelen; - if (!PyArg_ParseTuple(args, "s#:setmode", &mode, &modelen)) { - return NULL; - } - - im = self->image; - - /* move all logic in here to the libImaging primitive */ - - if (!strcmp(im->mode, mode)) { - ; /* same mode; always succeeds */ - } else if (IS_RGB(im->mode) && IS_RGB(mode)) { - /* color to color */ - strcpy(im->mode, mode); - im->bands = modelen; - if (!strcmp(mode, "RGBA")) { - (void)ImagingFillBand(im, 3, 255); - } - } else { - /* trying doing an in-place conversion */ - if (!ImagingConvertInPlace(im, mode)) { - return NULL; - } - } - - if (self->access) { - ImagingAccessDelete(im, self->access); - } - self->access = ImagingAccessNew(im); - - Py_INCREF(Py_None); - return Py_None; -} - -static PyObject * -_transform(ImagingObject *self, PyObject *args) { - static const char *wrong_number = "wrong number of matrix entries"; - - Imaging imOut; - Py_ssize_t n; - double *a; - - ImagingObject *imagep; - int x0, y0, x1, y1; - int method; - PyObject *data; - int filter = IMAGING_TRANSFORM_NEAREST; - int fill = 1; - if (!PyArg_ParseTuple( - args, - "(iiii)O!iO|ii", - &x0, - &y0, - &x1, - &y1, - &Imaging_Type, - &imagep, - &method, - &data, - &filter, - &fill)) { - return NULL; - } - - switch (method) { - case IMAGING_TRANSFORM_AFFINE: - n = 6; - break; - case IMAGING_TRANSFORM_PERSPECTIVE: - n = 8; - break; - case IMAGING_TRANSFORM_QUAD: - n = 8; - break; - default: - n = -1; /* force error */ - } - - a = getlist(data, &n, wrong_number, TYPE_DOUBLE); - if (!a) { - return NULL; - } - - imOut = ImagingTransform( - self->image, imagep->image, method, x0, y0, x1, y1, a, filter, fill); - - free(a); - - if (!imOut) { - return NULL; - } - - Py_INCREF(Py_None); - return Py_None; -} - -static PyObject * -_transpose(ImagingObject *self, PyObject *args) { - Imaging imIn; - Imaging imOut; - - int op; - if (!PyArg_ParseTuple(args, "i", &op)) { - return NULL; - } - - imIn = self->image; - - switch (op) { - case 0: /* flip left right */ - case 1: /* flip top bottom */ - case 3: /* rotate 180 */ - imOut = ImagingNewDirty(imIn->mode, imIn->xsize, imIn->ysize); - break; - case 2: /* rotate 90 */ - case 4: /* rotate 270 */ - case 5: /* transpose */ - case 6: /* transverse */ - imOut = ImagingNewDirty(imIn->mode, imIn->ysize, imIn->xsize); - break; - default: - PyErr_SetString(PyExc_ValueError, "No such transpose operation"); - return NULL; - } - - if (imOut) { - switch (op) { - case 0: - (void)ImagingFlipLeftRight(imOut, imIn); - break; - case 1: - (void)ImagingFlipTopBottom(imOut, imIn); - break; - case 2: - (void)ImagingRotate90(imOut, imIn); - break; - case 3: - (void)ImagingRotate180(imOut, imIn); - break; - case 4: - (void)ImagingRotate270(imOut, imIn); - break; - case 5: - (void)ImagingTranspose(imOut, imIn); - break; - case 6: - (void)ImagingTransverse(imOut, imIn); - break; - } - } - - return PyImagingNew(imOut); -} - -#ifdef WITH_UNSHARPMASK -static PyObject * -_unsharp_mask(ImagingObject *self, PyObject *args) { - Imaging imIn; - Imaging imOut; - - float radius; - int percent, threshold; - if (!PyArg_ParseTuple(args, "fii", &radius, &percent, &threshold)) { - return NULL; - } - - imIn = self->image; - imOut = ImagingNewDirty(imIn->mode, imIn->xsize, imIn->ysize); - if (!imOut) { - return NULL; - } - - if (!ImagingUnsharpMask(imOut, imIn, radius, percent, threshold)) { - return NULL; - } - - return PyImagingNew(imOut); -} -#endif - -static PyObject * -_box_blur(ImagingObject *self, PyObject *args) { - Imaging imIn; - Imaging imOut; - - float xradius, yradius; - int n = 1; - if (!PyArg_ParseTuple(args, "(ff)|i", &xradius, &yradius, &n)) { - return NULL; - } - - imIn = self->image; - imOut = ImagingNewDirty(imIn->mode, imIn->xsize, imIn->ysize); - if (!imOut) { - return NULL; - } - - if (!ImagingBoxBlur(imOut, imIn, xradius, yradius, n)) { - ImagingDelete(imOut); - return NULL; - } - - return PyImagingNew(imOut); -} - -/* -------------------------------------------------------------------- */ - -static PyObject * -_isblock(ImagingObject *self) { - return PyBool_FromLong(self->image->block != NULL); -} - -static PyObject * -_getbbox(ImagingObject *self, PyObject *args) { - int bbox[4]; - - int alpha_only = 1; - if (!PyArg_ParseTuple(args, "|i", &alpha_only)) { - return NULL; - } - - if (!ImagingGetBBox(self->image, bbox, alpha_only)) { - Py_INCREF(Py_None); - return Py_None; - } - - return Py_BuildValue("iiii", bbox[0], bbox[1], bbox[2], bbox[3]); -} - -static PyObject * -_getcolors(ImagingObject *self, PyObject *args) { - ImagingColorItem *items; - int i, colors; - PyObject *out; - - int maxcolors = 256; - if (!PyArg_ParseTuple(args, "i:getcolors", &maxcolors)) { - return NULL; - } - - items = ImagingGetColors(self->image, maxcolors, &colors); - if (!items) { - return NULL; - } - - if (colors > maxcolors) { - out = Py_None; - Py_INCREF(out); - } else { - out = PyList_New(colors); - if (out == NULL) { - free(items); - return NULL; - } - for (i = 0; i < colors; i++) { - ImagingColorItem *v = &items[i]; - PyObject *item = Py_BuildValue( - "iN", v->count, getpixel(self->image, self->access, v->x, v->y)); - PyList_SetItem(out, i, item); - } - } - - free(items); - - return out; -} - -static PyObject * -_getextrema(ImagingObject *self) { - union { - UINT8 u[2]; - INT32 i[2]; - FLOAT32 f[2]; - UINT16 s[2]; - } extrema; - int status; - - status = ImagingGetExtrema(self->image, &extrema); - if (status < 0) { - return NULL; - } - - if (status) { - switch (self->image->type) { - case IMAGING_TYPE_UINT8: - return Py_BuildValue("BB", extrema.u[0], extrema.u[1]); - case IMAGING_TYPE_INT32: - return Py_BuildValue("ii", extrema.i[0], extrema.i[1]); - case IMAGING_TYPE_FLOAT32: - return Py_BuildValue("dd", extrema.f[0], extrema.f[1]); - case IMAGING_TYPE_SPECIAL: - if (strcmp(self->image->mode, "I;16") == 0) { - return Py_BuildValue("HH", extrema.s[0], extrema.s[1]); - } - } - } - - Py_INCREF(Py_None); - return Py_None; -} - -static PyObject * -_getprojection(ImagingObject *self) { - unsigned char *xprofile; - unsigned char *yprofile; - PyObject *result; - - /* malloc check ok */ - xprofile = malloc(self->image->xsize); - yprofile = malloc(self->image->ysize); - - if (xprofile == NULL || yprofile == NULL) { - free(xprofile); - free(yprofile); - return ImagingError_MemoryError(); - } - - ImagingGetProjection( - self->image, (unsigned char *)xprofile, (unsigned char *)yprofile); - - result = Py_BuildValue( - "y#y#", - xprofile, - (Py_ssize_t)self->image->xsize, - yprofile, - (Py_ssize_t)self->image->ysize); - - free(xprofile); - free(yprofile); - - return result; -} - -/* -------------------------------------------------------------------- */ - -static PyObject * -_getband(ImagingObject *self, PyObject *args) { - int band; - - if (!PyArg_ParseTuple(args, "i", &band)) { - return NULL; - } - - return PyImagingNew(ImagingGetBand(self->image, band)); -} - -static PyObject * -_fillband(ImagingObject *self, PyObject *args) { - int band; - int color; - - if (!PyArg_ParseTuple(args, "ii", &band, &color)) { - return NULL; - } - - if (!ImagingFillBand(self->image, band, color)) { - return NULL; - } - - Py_INCREF(Py_None); - return Py_None; -} - -static PyObject * -_putband(ImagingObject *self, PyObject *args) { - ImagingObject *imagep; - int band; - if (!PyArg_ParseTuple(args, "O!i", &Imaging_Type, &imagep, &band)) { - return NULL; - } - - if (!ImagingPutBand(self->image, imagep->image, band)) { - return NULL; - } - - Py_INCREF(Py_None); - return Py_None; -} - -static PyObject * -_merge(PyObject *self, PyObject *args) { - char *mode; - ImagingObject *band0 = NULL; - ImagingObject *band1 = NULL; - ImagingObject *band2 = NULL; - ImagingObject *band3 = NULL; - Imaging bands[4] = {NULL, NULL, NULL, NULL}; - - if (!PyArg_ParseTuple( - args, - "sO!|O!O!O!", - &mode, - &Imaging_Type, - &band0, - &Imaging_Type, - &band1, - &Imaging_Type, - &band2, - &Imaging_Type, - &band3)) { - return NULL; - } - - if (band0) { - bands[0] = band0->image; - } - if (band1) { - bands[1] = band1->image; - } - if (band2) { - bands[2] = band2->image; - } - if (band3) { - bands[3] = band3->image; - } - - return PyImagingNew(ImagingMerge(mode, bands)); -} - -static PyObject * -_split(ImagingObject *self) { - int fails = 0; - Py_ssize_t i; - PyObject *list; - PyObject *imaging_object; - Imaging bands[4] = {NULL, NULL, NULL, NULL}; - - if (!ImagingSplit(self->image, bands)) { - return NULL; - } - - list = PyTuple_New(self->image->bands); - for (i = 0; i < self->image->bands; i++) { - imaging_object = PyImagingNew(bands[i]); - if (!imaging_object) { - fails += 1; - } - PyTuple_SET_ITEM(list, i, imaging_object); - } - if (fails) { - Py_DECREF(list); - list = NULL; - } - return list; -} - -/* -------------------------------------------------------------------- */ - -#ifdef WITH_IMAGECHOPS - -static PyObject * -_chop_invert(ImagingObject *self) { - return PyImagingNew(ImagingNegative(self->image)); -} - -static PyObject * -_chop_lighter(ImagingObject *self, PyObject *args) { - ImagingObject *imagep; - - if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) { - return NULL; - } - - return PyImagingNew(ImagingChopLighter(self->image, imagep->image)); -} - -static PyObject * -_chop_darker(ImagingObject *self, PyObject *args) { - ImagingObject *imagep; - - if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) { - return NULL; - } - - return PyImagingNew(ImagingChopDarker(self->image, imagep->image)); -} - -static PyObject * -_chop_difference(ImagingObject *self, PyObject *args) { - ImagingObject *imagep; - - if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) { - return NULL; - } - - return PyImagingNew(ImagingChopDifference(self->image, imagep->image)); -} - -static PyObject * -_chop_multiply(ImagingObject *self, PyObject *args) { - ImagingObject *imagep; - - if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) { - return NULL; - } - - return PyImagingNew(ImagingChopMultiply(self->image, imagep->image)); -} - -static PyObject * -_chop_screen(ImagingObject *self, PyObject *args) { - ImagingObject *imagep; - - if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) { - return NULL; - } - - return PyImagingNew(ImagingChopScreen(self->image, imagep->image)); -} - -static PyObject * -_chop_add(ImagingObject *self, PyObject *args) { - ImagingObject *imagep; - float scale; - int offset; - - scale = 1.0; - offset = 0; - - if (!PyArg_ParseTuple(args, "O!|fi", &Imaging_Type, &imagep, &scale, &offset)) { - return NULL; - } - - return PyImagingNew(ImagingChopAdd(self->image, imagep->image, scale, offset)); -} - -static PyObject * -_chop_subtract(ImagingObject *self, PyObject *args) { - ImagingObject *imagep; - float scale; - int offset; - - scale = 1.0; - offset = 0; - - if (!PyArg_ParseTuple(args, "O!|fi", &Imaging_Type, &imagep, &scale, &offset)) { - return NULL; - } - - return PyImagingNew(ImagingChopSubtract(self->image, imagep->image, scale, offset)); -} - -static PyObject * -_chop_and(ImagingObject *self, PyObject *args) { - ImagingObject *imagep; - - if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) { - return NULL; - } - - return PyImagingNew(ImagingChopAnd(self->image, imagep->image)); -} - -static PyObject * -_chop_or(ImagingObject *self, PyObject *args) { - ImagingObject *imagep; - - if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) { - return NULL; - } - - return PyImagingNew(ImagingChopOr(self->image, imagep->image)); -} - -static PyObject * -_chop_xor(ImagingObject *self, PyObject *args) { - ImagingObject *imagep; - - if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) { - return NULL; - } - - return PyImagingNew(ImagingChopXor(self->image, imagep->image)); -} - -static PyObject * -_chop_add_modulo(ImagingObject *self, PyObject *args) { - ImagingObject *imagep; - - if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) { - return NULL; - } - - return PyImagingNew(ImagingChopAddModulo(self->image, imagep->image)); -} - -static PyObject * -_chop_subtract_modulo(ImagingObject *self, PyObject *args) { - ImagingObject *imagep; - - if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) { - return NULL; - } - - return PyImagingNew(ImagingChopSubtractModulo(self->image, imagep->image)); -} - -static PyObject * -_chop_soft_light(ImagingObject *self, PyObject *args) { - ImagingObject *imagep; - - if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) { - return NULL; - } - - return PyImagingNew(ImagingChopSoftLight(self->image, imagep->image)); -} - -static PyObject * -_chop_hard_light(ImagingObject *self, PyObject *args) { - ImagingObject *imagep; - - if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) { - return NULL; - } - - return PyImagingNew(ImagingChopHardLight(self->image, imagep->image)); -} - -static PyObject * -_chop_overlay(ImagingObject *self, PyObject *args) { - ImagingObject *imagep; - - if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) { - return NULL; - } - - return PyImagingNew(ImagingOverlay(self->image, imagep->image)); -} -#endif - -/* -------------------------------------------------------------------- */ - -#ifdef WITH_IMAGEDRAW - -static PyObject * -_font_new(PyObject *self_, PyObject *args) { - ImagingFontObject *self; - int i, y0, y1; - static const char *wrong_length = "descriptor table has wrong size"; - - ImagingObject *imagep; - unsigned char *glyphdata; - Py_ssize_t glyphdata_length; - if (!PyArg_ParseTuple( - args, "O!y#", &Imaging_Type, &imagep, &glyphdata, &glyphdata_length)) { - return NULL; - } - - if (glyphdata_length != 256 * 20) { - PyErr_SetString(PyExc_ValueError, wrong_length); - return NULL; - } - - self = PyObject_New(ImagingFontObject, &ImagingFont_Type); - if (self == NULL) { - return NULL; - } - - /* glyph bitmap */ - self->bitmap = imagep->image; - - y0 = y1 = 0; - - /* glyph glyphs */ - for (i = 0; i < 256; i++) { - self->glyphs[i].dx = S16(B16(glyphdata, 0)); - self->glyphs[i].dy = S16(B16(glyphdata, 2)); - self->glyphs[i].dx0 = S16(B16(glyphdata, 4)); - self->glyphs[i].dy0 = S16(B16(glyphdata, 6)); - self->glyphs[i].dx1 = S16(B16(glyphdata, 8)); - self->glyphs[i].dy1 = S16(B16(glyphdata, 10)); - self->glyphs[i].sx0 = S16(B16(glyphdata, 12)); - self->glyphs[i].sy0 = S16(B16(glyphdata, 14)); - self->glyphs[i].sx1 = S16(B16(glyphdata, 16)); - self->glyphs[i].sy1 = S16(B16(glyphdata, 18)); - - // Do not allow glyphs to extend beyond bitmap image - // Helps prevent DOS by stopping cropped images being larger than the original - if (self->glyphs[i].sx0 < 0) { - self->glyphs[i].dx0 -= self->glyphs[i].sx0; - self->glyphs[i].sx0 = 0; - } - if (self->glyphs[i].sy0 < 0) { - self->glyphs[i].dy0 -= self->glyphs[i].sy0; - self->glyphs[i].sy0 = 0; - } - if (self->glyphs[i].sx1 > self->bitmap->xsize) { - self->glyphs[i].dx1 -= self->glyphs[i].sx1 - self->bitmap->xsize; - self->glyphs[i].sx1 = self->bitmap->xsize; - } - if (self->glyphs[i].sy1 > self->bitmap->ysize) { - self->glyphs[i].dy1 -= self->glyphs[i].sy1 - self->bitmap->ysize; - self->glyphs[i].sy1 = self->bitmap->ysize; - } - - if (self->glyphs[i].dy0 < y0) { - y0 = self->glyphs[i].dy0; - } - if (self->glyphs[i].dy1 > y1) { - y1 = self->glyphs[i].dy1; - } - glyphdata += 20; - } - - self->baseline = -y0; - self->ysize = y1 - y0; - - /* keep a reference to the bitmap object */ - Py_INCREF(imagep); - self->ref = imagep; - - return (PyObject *)self; -} - -static void -_font_dealloc(ImagingFontObject *self) { - Py_XDECREF(self->ref); - PyObject_Del(self); -} - -static inline int -textwidth(ImagingFontObject *self, const unsigned char *text) { - int xsize; - - for (xsize = 0; *text; text++) { - xsize += self->glyphs[*text].dx; - } - - return xsize; -} - -void -_font_text_asBytes(PyObject *encoded_string, unsigned char **text) { - /* Allocates *text, returns a 'new reference'. Caller is required to free */ - - PyObject *bytes = NULL; - Py_ssize_t len = 0; - char *buffer; - - *text = NULL; - - if (PyUnicode_CheckExact(encoded_string)) { - bytes = PyUnicode_AsLatin1String(encoded_string); - if (!bytes) { - return; - } - PyBytes_AsStringAndSize(bytes, &buffer, &len); - } else if (PyBytes_Check(encoded_string)) { - PyBytes_AsStringAndSize(encoded_string, &buffer, &len); - } - - *text = calloc(len + 1, 1); - if (*text) { - memcpy(*text, buffer, len); - } else { - ImagingError_MemoryError(); - } - if (bytes) { - Py_DECREF(bytes); - } - - return; -} - -static PyObject * -_font_getmask(ImagingFontObject *self, PyObject *args) { - Imaging im; - Imaging bitmap = NULL; - int x, b; - int i = 0; - int status; - Glyph *glyph; - - PyObject *encoded_string; - - unsigned char *text; - char *mode; - - if (!PyArg_ParseTuple(args, "O|s:getmask", &encoded_string, &mode)) { - return NULL; - } - - _font_text_asBytes(encoded_string, &text); - if (!text) { - return NULL; - } - - im = ImagingNew(self->bitmap->mode, textwidth(self, text), self->ysize); - if (!im) { - free(text); - return ImagingError_MemoryError(); - } - - b = 0; - (void)ImagingFill(im, &b); - - b = self->baseline; - for (x = 0; text[i]; i++) { - glyph = &self->glyphs[text[i]]; - if (i == 0 || text[i] != text[i - 1]) { - ImagingDelete(bitmap); - bitmap = ImagingCrop( - self->bitmap, glyph->sx0, glyph->sy0, glyph->sx1, glyph->sy1); - if (!bitmap) { - goto failed; - } - } - status = ImagingPaste( - im, - bitmap, - NULL, - glyph->dx0 + x, - glyph->dy0 + b, - glyph->dx1 + x, - glyph->dy1 + b); - if (status < 0) { - goto failed; - } - x = x + glyph->dx; - b = b + glyph->dy; - } - ImagingDelete(bitmap); - free(text); - return PyImagingNew(im); - -failed: - ImagingDelete(bitmap); - free(text); - ImagingDelete(im); - Py_RETURN_NONE; -} - -static PyObject * -_font_getsize(ImagingFontObject *self, PyObject *args) { - unsigned char *text; - PyObject *encoded_string; - PyObject *val; - - if (!PyArg_ParseTuple(args, "O:getsize", &encoded_string)) { - return NULL; - } - - _font_text_asBytes(encoded_string, &text); - if (!text) { - return NULL; - } - - val = Py_BuildValue("ii", textwidth(self, text), self->ysize); - free(text); - return val; -} - -static struct PyMethodDef _font_methods[] = { - {"getmask", (PyCFunction)_font_getmask, METH_VARARGS}, - {"getsize", (PyCFunction)_font_getsize, METH_VARARGS}, - {NULL, NULL} /* sentinel */ -}; - -/* -------------------------------------------------------------------- */ - -static PyObject * -_draw_new(PyObject *self_, PyObject *args) { - ImagingDrawObject *self; - - ImagingObject *imagep; - int blend = 0; - if (!PyArg_ParseTuple(args, "O!|i", &Imaging_Type, &imagep, &blend)) { - return NULL; - } - - self = PyObject_New(ImagingDrawObject, &ImagingDraw_Type); - if (self == NULL) { - return NULL; - } - - /* keep a reference to the image object */ - Py_INCREF(imagep); - self->image = imagep; - - self->ink[0] = self->ink[1] = self->ink[2] = self->ink[3] = 0; - - self->blend = blend; - - return (PyObject *)self; -} - -static void -_draw_dealloc(ImagingDrawObject *self) { - Py_XDECREF(self->image); - PyObject_Del(self); -} - -extern Py_ssize_t -PyPath_Flatten(PyObject *data, double **xy); - -static PyObject * -_draw_ink(ImagingDrawObject *self, PyObject *args) { - INT32 ink = 0; - PyObject *color; - if (!PyArg_ParseTuple(args, "O", &color)) { - return NULL; - } - - if (!getink(color, self->image->image, (char *)&ink)) { - return NULL; - } - - return PyLong_FromLong((int)ink); -} - -static PyObject * -_draw_arc(ImagingDrawObject *self, PyObject *args) { - double *xy; - Py_ssize_t n; - - PyObject *data; - int ink; - int width = 0; - float start, end; - if (!PyArg_ParseTuple(args, "Offi|i", &data, &start, &end, &ink, &width)) { - return NULL; - } - - n = PyPath_Flatten(data, &xy); - if (n < 0) { - return NULL; - } - if (n != 2) { - PyErr_SetString(PyExc_TypeError, must_be_two_coordinates); - free(xy); - return NULL; - } - if (xy[2] < xy[0]) { - PyErr_SetString(PyExc_ValueError, incorrectly_ordered_x_coordinate); - free(xy); - return NULL; - } - if (xy[3] < xy[1]) { - PyErr_SetString(PyExc_ValueError, incorrectly_ordered_y_coordinate); - free(xy); - return NULL; - } - - n = ImagingDrawArc( - self->image->image, - (int)xy[0], - (int)xy[1], - (int)xy[2], - (int)xy[3], - start, - end, - &ink, - width, - self->blend); - - free(xy); - - if (n < 0) { - return NULL; - } - - Py_INCREF(Py_None); - return Py_None; -} - -static PyObject * -_draw_bitmap(ImagingDrawObject *self, PyObject *args) { - double *xy; - Py_ssize_t n; - - PyObject *data; - ImagingObject *bitmap; - int ink; - if (!PyArg_ParseTuple(args, "OO!i", &data, &Imaging_Type, &bitmap, &ink)) { - return NULL; - } - - n = PyPath_Flatten(data, &xy); - if (n < 0) { - return NULL; - } - if (n != 1) { - PyErr_SetString( - PyExc_TypeError, "coordinate list must contain exactly 1 coordinate"); - free(xy); - return NULL; - } - - n = ImagingDrawBitmap( - self->image->image, (int)xy[0], (int)xy[1], bitmap->image, &ink, self->blend); - - free(xy); - - if (n < 0) { - return NULL; - } - - Py_INCREF(Py_None); - return Py_None; -} - -static PyObject * -_draw_chord(ImagingDrawObject *self, PyObject *args) { - double *xy; - Py_ssize_t n; - - PyObject *data; - int ink, fill; - int width = 0; - float start, end; - if (!PyArg_ParseTuple(args, "Offii|i", &data, &start, &end, &ink, &fill, &width)) { - return NULL; - } - - n = PyPath_Flatten(data, &xy); - if (n < 0) { - return NULL; - } - if (n != 2) { - PyErr_SetString(PyExc_TypeError, must_be_two_coordinates); - free(xy); - return NULL; - } - if (xy[2] < xy[0]) { - PyErr_SetString(PyExc_ValueError, incorrectly_ordered_x_coordinate); - free(xy); - return NULL; - } - if (xy[3] < xy[1]) { - PyErr_SetString(PyExc_ValueError, incorrectly_ordered_y_coordinate); - free(xy); - return NULL; - } - - n = ImagingDrawChord( - self->image->image, - (int)xy[0], - (int)xy[1], - (int)xy[2], - (int)xy[3], - start, - end, - &ink, - fill, - width, - self->blend); - - free(xy); - - if (n < 0) { - return NULL; - } - - Py_INCREF(Py_None); - return Py_None; -} - -static PyObject * -_draw_ellipse(ImagingDrawObject *self, PyObject *args) { - double *xy; - Py_ssize_t n; - - PyObject *data; - int ink; - int fill = 0; - int width = 0; - if (!PyArg_ParseTuple(args, "Oi|ii", &data, &ink, &fill, &width)) { - return NULL; - } - - n = PyPath_Flatten(data, &xy); - if (n < 0) { - return NULL; - } - if (n != 2) { - PyErr_SetString(PyExc_TypeError, must_be_two_coordinates); - free(xy); - return NULL; - } - if (xy[2] < xy[0]) { - PyErr_SetString(PyExc_ValueError, incorrectly_ordered_x_coordinate); - free(xy); - return NULL; - } - if (xy[3] < xy[1]) { - PyErr_SetString(PyExc_ValueError, incorrectly_ordered_y_coordinate); - free(xy); - return NULL; - } - - n = ImagingDrawEllipse( - self->image->image, - (int)xy[0], - (int)xy[1], - (int)xy[2], - (int)xy[3], - &ink, - fill, - width, - self->blend); - - free(xy); - - if (n < 0) { - return NULL; - } - - Py_INCREF(Py_None); - return Py_None; -} - -static PyObject * -_draw_lines(ImagingDrawObject *self, PyObject *args) { - double *xy; - Py_ssize_t i, n; - - PyObject *data; - int ink; - int width = 0; - if (!PyArg_ParseTuple(args, "Oi|i", &data, &ink, &width)) { - return NULL; - } - - n = PyPath_Flatten(data, &xy); - if (n < 0) { - return NULL; - } - - if (width <= 1) { - double *p = NULL; - for (i = 0; i < n - 1; i++) { - p = &xy[i + i]; - if (ImagingDrawLine( - self->image->image, - (int)p[0], - (int)p[1], - (int)p[2], - (int)p[3], - &ink, - self->blend) < 0) { - free(xy); - return NULL; - } - } - if (p) { /* draw last point */ - ImagingDrawPoint( - self->image->image, (int)p[2], (int)p[3], &ink, self->blend); - } - } else { - for (i = 0; i < n - 1; i++) { - double *p = &xy[i + i]; - if (ImagingDrawWideLine( - self->image->image, - (int)p[0], - (int)p[1], - (int)p[2], - (int)p[3], - &ink, - width, - self->blend) < 0) { - free(xy); - return NULL; - } - } - } - - free(xy); - - Py_INCREF(Py_None); - return Py_None; -} - -static PyObject * -_draw_points(ImagingDrawObject *self, PyObject *args) { - double *xy; - Py_ssize_t i, n; - - PyObject *data; - int ink; - if (!PyArg_ParseTuple(args, "Oi", &data, &ink)) { - return NULL; - } - - n = PyPath_Flatten(data, &xy); - if (n < 0) { - return NULL; - } - - for (i = 0; i < n; i++) { - double *p = &xy[i + i]; - if (ImagingDrawPoint( - self->image->image, (int)p[0], (int)p[1], &ink, self->blend) < 0) { - free(xy); - return NULL; - } - } - - free(xy); - - Py_INCREF(Py_None); - return Py_None; -} - -#ifdef WITH_ARROW - -/* from outline.c */ -extern ImagingOutline -PyOutline_AsOutline(PyObject *outline); - -static PyObject * -_draw_outline(ImagingDrawObject *self, PyObject *args) { - ImagingOutline outline; - - PyObject *outline_; - int ink; - int fill = 0; - if (!PyArg_ParseTuple(args, "Oi|i", &outline_, &ink, &fill)) { - return NULL; - } - - outline = PyOutline_AsOutline(outline_); - if (!outline) { - PyErr_SetString(PyExc_TypeError, "expected outline object"); - return NULL; - } - - if (ImagingDrawOutline(self->image->image, outline, &ink, fill, self->blend) < 0) { - return NULL; - } - - Py_INCREF(Py_None); - return Py_None; -} - -#endif - -static PyObject * -_draw_pieslice(ImagingDrawObject *self, PyObject *args) { - double *xy; - Py_ssize_t n; - - PyObject *data; - int ink, fill; - int width = 0; - float start, end; - if (!PyArg_ParseTuple(args, "Offii|i", &data, &start, &end, &ink, &fill, &width)) { - return NULL; - } - - n = PyPath_Flatten(data, &xy); - if (n < 0) { - return NULL; - } - if (n != 2) { - PyErr_SetString(PyExc_TypeError, must_be_two_coordinates); - free(xy); - return NULL; - } - if (xy[2] < xy[0]) { - PyErr_SetString(PyExc_ValueError, incorrectly_ordered_x_coordinate); - free(xy); - return NULL; - } - if (xy[3] < xy[1]) { - PyErr_SetString(PyExc_ValueError, incorrectly_ordered_y_coordinate); - free(xy); - return NULL; - } - - n = ImagingDrawPieslice( - self->image->image, - (int)xy[0], - (int)xy[1], - (int)xy[2], - (int)xy[3], - start, - end, - &ink, - fill, - width, - self->blend); - - free(xy); - - if (n < 0) { - return NULL; - } - - Py_INCREF(Py_None); - return Py_None; -} - -static PyObject * -_draw_polygon(ImagingDrawObject *self, PyObject *args) { - double *xy; - int *ixy; - Py_ssize_t n, i; - - PyObject *data; - int ink; - int fill = 0; - int width = 0; - if (!PyArg_ParseTuple(args, "Oi|ii", &data, &ink, &fill, &width)) { - return NULL; - } - - n = PyPath_Flatten(data, &xy); - if (n < 0) { - return NULL; - } - if (n < 2) { - PyErr_SetString( - PyExc_TypeError, "coordinate list must contain at least 2 coordinates"); - free(xy); - return NULL; - } - - /* Copy list of vertices to array */ - ixy = (int *)calloc(n, 2 * sizeof(int)); - if (ixy == NULL) { - free(xy); - return ImagingError_MemoryError(); - } - - for (i = 0; i < n; i++) { - ixy[i + i] = (int)xy[i + i]; - ixy[i + i + 1] = (int)xy[i + i + 1]; - } - - free(xy); - - if (ImagingDrawPolygon(self->image->image, n, ixy, &ink, fill, width, self->blend) < - 0) { - free(ixy); - return NULL; - } - - free(ixy); - - Py_INCREF(Py_None); - return Py_None; -} - -static PyObject * -_draw_rectangle(ImagingDrawObject *self, PyObject *args) { - double *xy; - Py_ssize_t n; - - PyObject *data; - int ink; - int fill = 0; - int width = 0; - if (!PyArg_ParseTuple(args, "Oi|ii", &data, &ink, &fill, &width)) { - return NULL; - } - - n = PyPath_Flatten(data, &xy); - if (n < 0) { - return NULL; - } - if (n != 2) { - PyErr_SetString(PyExc_TypeError, must_be_two_coordinates); - free(xy); - return NULL; - } - if (xy[2] < xy[0]) { - PyErr_SetString(PyExc_ValueError, incorrectly_ordered_x_coordinate); - free(xy); - return NULL; - } - if (xy[3] < xy[1]) { - PyErr_SetString(PyExc_ValueError, incorrectly_ordered_y_coordinate); - free(xy); - return NULL; - } - - n = ImagingDrawRectangle( - self->image->image, - (int)xy[0], - (int)xy[1], - (int)xy[2], - (int)xy[3], - &ink, - fill, - width, - self->blend); - - free(xy); - - if (n < 0) { - return NULL; - } - - Py_INCREF(Py_None); - return Py_None; -} - -static struct PyMethodDef _draw_methods[] = { -#ifdef WITH_IMAGEDRAW - /* Graphics (ImageDraw) */ - {"draw_lines", (PyCFunction)_draw_lines, METH_VARARGS}, -#ifdef WITH_ARROW - {"draw_outline", (PyCFunction)_draw_outline, METH_VARARGS}, -#endif - {"draw_polygon", (PyCFunction)_draw_polygon, METH_VARARGS}, - {"draw_rectangle", (PyCFunction)_draw_rectangle, METH_VARARGS}, - {"draw_points", (PyCFunction)_draw_points, METH_VARARGS}, - {"draw_arc", (PyCFunction)_draw_arc, METH_VARARGS}, - {"draw_bitmap", (PyCFunction)_draw_bitmap, METH_VARARGS}, - {"draw_chord", (PyCFunction)_draw_chord, METH_VARARGS}, - {"draw_ellipse", (PyCFunction)_draw_ellipse, METH_VARARGS}, - {"draw_pieslice", (PyCFunction)_draw_pieslice, METH_VARARGS}, - {"draw_ink", (PyCFunction)_draw_ink, METH_VARARGS}, -#endif - {NULL, NULL} /* sentinel */ -}; - -#endif - -static PyObject * -pixel_access_new(ImagingObject *imagep, PyObject *args) { - PixelAccessObject *self; - - int readonly = 0; - if (!PyArg_ParseTuple(args, "|i", &readonly)) { - return NULL; - } - - self = PyObject_New(PixelAccessObject, &PixelAccess_Type); - if (self == NULL) { - return NULL; - } - - /* keep a reference to the image object */ - Py_INCREF(imagep); - self->image = imagep; - - self->readonly = readonly; - - return (PyObject *)self; -} - -static void -pixel_access_dealloc(PixelAccessObject *self) { - Py_XDECREF(self->image); - PyObject_Del(self); -} - -static PyObject * -pixel_access_getitem(PixelAccessObject *self, PyObject *xy) { - int x, y; - if (_getxy(xy, &x, &y)) { - return NULL; - } - - return getpixel(self->image->image, self->image->access, x, y); -} - -static int -pixel_access_setitem(PixelAccessObject *self, PyObject *xy, PyObject *color) { - Imaging im = self->image->image; - char ink[4]; - int x, y; - - if (self->readonly) { - (void)ImagingError_ValueError(readonly); - return -1; - } - - if (_getxy(xy, &x, &y)) { - return -1; - } - - if (x < 0) { - x = im->xsize + x; - } - if (y < 0) { - y = im->ysize + y; - } - - if (x < 0 || x >= im->xsize || y < 0 || y >= im->ysize) { - PyErr_SetString(PyExc_IndexError, outside_image); - return -1; - } - - if (!color) { /* FIXME: raise exception? */ - return 0; - } - - if (!getink(color, im, ink)) { - return -1; - } - - self->image->access->put_pixel(im, x, y, ink); - - return 0; -} - -/* -------------------------------------------------------------------- */ -/* EFFECTS (experimental) */ -/* -------------------------------------------------------------------- */ - -#ifdef WITH_EFFECTS - -static PyObject * -_effect_mandelbrot(ImagingObject *self, PyObject *args) { - int xsize = 512; - int ysize = 512; - double extent[4]; - int quality = 100; - - extent[0] = -3; - extent[1] = -2.5; - extent[2] = 2; - extent[3] = 2.5; - - if (!PyArg_ParseTuple( - args, - "|(ii)(dddd)i", - &xsize, - &ysize, - &extent[0], - &extent[1], - &extent[2], - &extent[3], - &quality)) { - return NULL; - } - - return PyImagingNew(ImagingEffectMandelbrot(xsize, ysize, extent, quality)); -} - -static PyObject * -_effect_noise(ImagingObject *self, PyObject *args) { - int xsize, ysize; - float sigma = 128; - if (!PyArg_ParseTuple(args, "(ii)|f", &xsize, &ysize, &sigma)) { - return NULL; - } - - return PyImagingNew(ImagingEffectNoise(xsize, ysize, sigma)); -} - -static PyObject * -_effect_spread(ImagingObject *self, PyObject *args) { - int dist; - - if (!PyArg_ParseTuple(args, "i", &dist)) { - return NULL; - } - - return PyImagingNew(ImagingEffectSpread(self->image, dist)); -} - -#endif - -/* -------------------------------------------------------------------- */ -/* UTILITIES */ -/* -------------------------------------------------------------------- */ - -static PyObject * -_getcodecstatus(PyObject *self, PyObject *args) { - int status; - char *msg; - - if (!PyArg_ParseTuple(args, "i", &status)) { - return NULL; - } - - switch (status) { - case IMAGING_CODEC_OVERRUN: - msg = "buffer overrun"; - break; - case IMAGING_CODEC_BROKEN: - msg = "broken data stream"; - break; - case IMAGING_CODEC_UNKNOWN: - msg = "unrecognized data stream contents"; - break; - case IMAGING_CODEC_CONFIG: - msg = "codec configuration error"; - break; - case IMAGING_CODEC_MEMORY: - msg = "out of memory"; - break; - default: - Py_RETURN_NONE; - } - - return PyUnicode_FromString(msg); -} - -/* -------------------------------------------------------------------- */ -/* DEBUGGING HELPERS */ -/* -------------------------------------------------------------------- */ - -static PyObject * -_save_ppm(ImagingObject *self, PyObject *args) { - char *filename; - - if (!PyArg_ParseTuple(args, "s", &filename)) { - return NULL; - } - - if (!ImagingSavePPM(self->image, filename)) { - return NULL; - } - - Py_INCREF(Py_None); - return Py_None; -} - -/* -------------------------------------------------------------------- */ - -/* methods */ - -static struct PyMethodDef methods[] = { - - /* Put commonly used methods first */ - {"getpixel", (PyCFunction)_getpixel, METH_VARARGS}, - {"putpixel", (PyCFunction)_putpixel, METH_VARARGS}, - - {"pixel_access", (PyCFunction)pixel_access_new, METH_VARARGS}, - - /* Standard processing methods (Image) */ - {"color_lut_3d", (PyCFunction)_color_lut_3d, METH_VARARGS}, - {"convert", (PyCFunction)_convert, METH_VARARGS}, - {"convert2", (PyCFunction)_convert2, METH_VARARGS}, - {"convert_matrix", (PyCFunction)_convert_matrix, METH_VARARGS}, - {"convert_transparent", (PyCFunction)_convert_transparent, METH_VARARGS}, - {"copy", (PyCFunction)_copy, METH_VARARGS}, - {"crop", (PyCFunction)_crop, METH_VARARGS}, - {"expand", (PyCFunction)_expand_image, METH_VARARGS}, - {"filter", (PyCFunction)_filter, METH_VARARGS}, - {"histogram", (PyCFunction)_histogram, METH_VARARGS}, - {"entropy", (PyCFunction)_entropy, METH_VARARGS}, -#ifdef WITH_MODEFILTER - {"modefilter", (PyCFunction)_modefilter, METH_VARARGS}, -#endif - {"offset", (PyCFunction)_offset, METH_VARARGS}, - {"paste", (PyCFunction)_paste, METH_VARARGS}, - {"point", (PyCFunction)_point, METH_VARARGS}, - {"point_transform", (PyCFunction)_point_transform, METH_VARARGS}, - {"putdata", (PyCFunction)_putdata, METH_VARARGS}, -#ifdef WITH_QUANTIZE - {"quantize", (PyCFunction)_quantize, METH_VARARGS}, -#endif -#ifdef WITH_RANKFILTER - {"rankfilter", (PyCFunction)_rankfilter, METH_VARARGS}, -#endif - {"resize", (PyCFunction)_resize, METH_VARARGS}, - {"reduce", (PyCFunction)_reduce, METH_VARARGS}, - {"transpose", (PyCFunction)_transpose, METH_VARARGS}, - {"transform", (PyCFunction)_transform, METH_VARARGS}, - - {"isblock", (PyCFunction)_isblock, METH_NOARGS}, - - {"getbbox", (PyCFunction)_getbbox, METH_VARARGS}, - {"getcolors", (PyCFunction)_getcolors, METH_VARARGS}, - {"getextrema", (PyCFunction)_getextrema, METH_NOARGS}, - {"getprojection", (PyCFunction)_getprojection, METH_NOARGS}, - - {"getband", (PyCFunction)_getband, METH_VARARGS}, - {"putband", (PyCFunction)_putband, METH_VARARGS}, - {"split", (PyCFunction)_split, METH_NOARGS}, - {"fillband", (PyCFunction)_fillband, METH_VARARGS}, - - {"setmode", (PyCFunction)im_setmode, METH_VARARGS}, - - {"getpalette", (PyCFunction)_getpalette, METH_VARARGS}, - {"getpalettemode", (PyCFunction)_getpalettemode, METH_NOARGS}, - {"putpalette", (PyCFunction)_putpalette, METH_VARARGS}, - {"putpalettealpha", (PyCFunction)_putpalettealpha, METH_VARARGS}, - {"putpalettealphas", (PyCFunction)_putpalettealphas, METH_VARARGS}, - -#ifdef WITH_IMAGECHOPS - /* Channel operations (ImageChops) */ - {"chop_invert", (PyCFunction)_chop_invert, METH_NOARGS}, - {"chop_lighter", (PyCFunction)_chop_lighter, METH_VARARGS}, - {"chop_darker", (PyCFunction)_chop_darker, METH_VARARGS}, - {"chop_difference", (PyCFunction)_chop_difference, METH_VARARGS}, - {"chop_multiply", (PyCFunction)_chop_multiply, METH_VARARGS}, - {"chop_screen", (PyCFunction)_chop_screen, METH_VARARGS}, - {"chop_add", (PyCFunction)_chop_add, METH_VARARGS}, - {"chop_subtract", (PyCFunction)_chop_subtract, METH_VARARGS}, - {"chop_add_modulo", (PyCFunction)_chop_add_modulo, METH_VARARGS}, - {"chop_subtract_modulo", (PyCFunction)_chop_subtract_modulo, METH_VARARGS}, - {"chop_and", (PyCFunction)_chop_and, METH_VARARGS}, - {"chop_or", (PyCFunction)_chop_or, METH_VARARGS}, - {"chop_xor", (PyCFunction)_chop_xor, METH_VARARGS}, - {"chop_soft_light", (PyCFunction)_chop_soft_light, METH_VARARGS}, - {"chop_hard_light", (PyCFunction)_chop_hard_light, METH_VARARGS}, - {"chop_overlay", (PyCFunction)_chop_overlay, METH_VARARGS}, - -#endif - -#ifdef WITH_UNSHARPMASK - /* Kevin Cazabon's unsharpmask extension */ - {"gaussian_blur", (PyCFunction)_gaussian_blur, METH_VARARGS}, - {"unsharp_mask", (PyCFunction)_unsharp_mask, METH_VARARGS}, -#endif - - {"box_blur", (PyCFunction)_box_blur, METH_VARARGS}, - -#ifdef WITH_EFFECTS - /* Special effects */ - {"effect_spread", (PyCFunction)_effect_spread, METH_VARARGS}, -#endif - - /* Misc. */ - {"new_block", (PyCFunction)_new_block, METH_VARARGS}, - - {"save_ppm", (PyCFunction)_save_ppm, METH_VARARGS}, - - {NULL, NULL} /* sentinel */ -}; - -/* attributes */ - -static PyObject * -_getattr_mode(ImagingObject *self, void *closure) { - return PyUnicode_FromString(self->image->mode); -} - -static PyObject * -_getattr_size(ImagingObject *self, void *closure) { - return Py_BuildValue("ii", self->image->xsize, self->image->ysize); -} - -static PyObject * -_getattr_bands(ImagingObject *self, void *closure) { - return PyLong_FromLong(self->image->bands); -} - -static PyObject * -_getattr_id(ImagingObject *self, void *closure) { - return PyLong_FromSsize_t((Py_ssize_t)self->image); -} - -static PyObject * -_getattr_ptr(ImagingObject *self, void *closure) { - return PyCapsule_New(self->image, IMAGING_MAGIC, NULL); -} - -static PyObject * -_getattr_unsafe_ptrs(ImagingObject *self, void *closure) { - return Py_BuildValue( - "(sn)(sn)(sn)", - "image8", - self->image->image8, - "image32", - self->image->image32, - "image", - self->image->image); -} - -static struct PyGetSetDef getsetters[] = { - {"mode", (getter)_getattr_mode}, - {"size", (getter)_getattr_size}, - {"bands", (getter)_getattr_bands}, - {"id", (getter)_getattr_id}, - {"ptr", (getter)_getattr_ptr}, - {"unsafe_ptrs", (getter)_getattr_unsafe_ptrs}, - {NULL}}; - -/* basic sequence semantics */ - -static Py_ssize_t -image_length(ImagingObject *self) { - Imaging im = self->image; - - return (Py_ssize_t)im->xsize * im->ysize; -} - -static PyObject * -image_item(ImagingObject *self, Py_ssize_t i) { - int x, y; - Imaging im = self->image; - - if (im->xsize > 0) { - x = i % im->xsize; - y = i / im->xsize; - } else { - x = y = 0; /* leave it to getpixel to raise an exception */ - } - - return getpixel(im, self->access, x, y); -} - -static PySequenceMethods image_as_sequence = { - (lenfunc)image_length, /*sq_length*/ - (binaryfunc)NULL, /*sq_concat*/ - (ssizeargfunc)NULL, /*sq_repeat*/ - (ssizeargfunc)image_item, /*sq_item*/ - (ssizessizeargfunc)NULL, /*sq_slice*/ - (ssizeobjargproc)NULL, /*sq_ass_item*/ - (ssizessizeobjargproc)NULL, /*sq_ass_slice*/ -}; - -/* 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*/ -}; - -#ifdef WITH_IMAGEDRAW - -static PyTypeObject ImagingFont_Type = { - PyVarObject_HEAD_INIT(NULL, 0) "ImagingFont", /*tp_name*/ - sizeof(ImagingFontObject), /*tp_basicsize*/ - 0, /*tp_itemsize*/ - /* methods */ - (destructor)_font_dealloc, /*tp_dealloc*/ - 0, /*tp_vectorcall_offset*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_as_async*/ - 0, /*tp_repr*/ - 0, /*tp_as_number*/ - 0, /*tp_as_sequence*/ - 0, /*tp_as_mapping*/ - 0, /*tp_hash*/ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT, /*tp_flags*/ - 0, /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - _font_methods, /*tp_methods*/ - 0, /*tp_members*/ - 0, /*tp_getset*/ -}; - -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*/ -}; - -#endif - -static PyMappingMethods pixel_access_as_mapping = { - (lenfunc)NULL, /*mp_length*/ - (binaryfunc)pixel_access_getitem, /*mp_subscript*/ - (objobjargproc)pixel_access_setitem, /*mp_ass_subscript*/ -}; - -/* type description */ - -static PyTypeObject PixelAccess_Type = { - PyVarObject_HEAD_INIT(NULL, 0) "PixelAccess", /*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*/ -}; - -/* -------------------------------------------------------------------- */ - -static PyObject * -_get_stats(PyObject *self, PyObject *args) { - PyObject *d; - PyObject *v; - ImagingMemoryArena arena = &ImagingDefaultArena; - - if (!PyArg_ParseTuple(args, ":get_stats")) { - return NULL; - } - - d = PyDict_New(); - if (!d) { - return NULL; - } - v = PyLong_FromLong(arena->stats_new_count); - PyDict_SetItemString(d, "new_count", v ? v : Py_None); - Py_XDECREF(v); - - v = PyLong_FromLong(arena->stats_allocated_blocks); - PyDict_SetItemString(d, "allocated_blocks", v ? v : Py_None); - Py_XDECREF(v); - - v = PyLong_FromLong(arena->stats_reused_blocks); - PyDict_SetItemString(d, "reused_blocks", v ? v : Py_None); - Py_XDECREF(v); - - v = PyLong_FromLong(arena->stats_reallocated_blocks); - PyDict_SetItemString(d, "reallocated_blocks", v ? v : Py_None); - Py_XDECREF(v); - - v = PyLong_FromLong(arena->stats_freed_blocks); - PyDict_SetItemString(d, "freed_blocks", v ? v : Py_None); - Py_XDECREF(v); - - v = PyLong_FromLong(arena->blocks_cached); - PyDict_SetItemString(d, "blocks_cached", v ? v : Py_None); - Py_XDECREF(v); - return d; -} - -static PyObject * -_reset_stats(PyObject *self, PyObject *args) { - ImagingMemoryArena arena = &ImagingDefaultArena; - - if (!PyArg_ParseTuple(args, ":reset_stats")) { - return NULL; - } - - arena->stats_new_count = 0; - arena->stats_allocated_blocks = 0; - arena->stats_reused_blocks = 0; - arena->stats_reallocated_blocks = 0; - arena->stats_freed_blocks = 0; - - Py_INCREF(Py_None); - return Py_None; -} - -static PyObject * -_get_alignment(PyObject *self, PyObject *args) { - if (!PyArg_ParseTuple(args, ":get_alignment")) { - return NULL; - } - - return PyLong_FromLong(ImagingDefaultArena.alignment); -} - -static PyObject * -_get_block_size(PyObject *self, PyObject *args) { - if (!PyArg_ParseTuple(args, ":get_block_size")) { - return NULL; - } - - return PyLong_FromLong(ImagingDefaultArena.block_size); -} - -static PyObject * -_get_blocks_max(PyObject *self, PyObject *args) { - if (!PyArg_ParseTuple(args, ":get_blocks_max")) { - return NULL; - } - - return PyLong_FromLong(ImagingDefaultArena.blocks_max); -} - -static PyObject * -_set_alignment(PyObject *self, PyObject *args) { - int alignment; - if (!PyArg_ParseTuple(args, "i:set_alignment", &alignment)) { - return NULL; - } - - if (alignment < 1 || alignment > 128) { - PyErr_SetString(PyExc_ValueError, "alignment should be from 1 to 128"); - return NULL; - } - /* Is power of two */ - if (alignment & (alignment - 1)) { - PyErr_SetString(PyExc_ValueError, "alignment should be power of two"); - return NULL; - } - - ImagingDefaultArena.alignment = alignment; - - Py_INCREF(Py_None); - return Py_None; -} - -static PyObject * -_set_block_size(PyObject *self, PyObject *args) { - int block_size; - if (!PyArg_ParseTuple(args, "i:set_block_size", &block_size)) { - return NULL; - } - - if (block_size <= 0) { - PyErr_SetString(PyExc_ValueError, "block_size should be greater than 0"); - return NULL; - } - - if (block_size & 0xfff) { - PyErr_SetString(PyExc_ValueError, "block_size should be multiple of 4096"); - return NULL; - } - - ImagingDefaultArena.block_size = block_size; - - Py_INCREF(Py_None); - return Py_None; -} - -static PyObject * -_set_blocks_max(PyObject *self, PyObject *args) { - int blocks_max; - if (!PyArg_ParseTuple(args, "i:set_blocks_max", &blocks_max)) { - return NULL; - } - - if (blocks_max < 0) { - PyErr_SetString(PyExc_ValueError, "blocks_max should be greater than 0"); - return NULL; - } else if ( - (unsigned long)blocks_max > - SIZE_MAX / sizeof(ImagingDefaultArena.blocks_pool[0])) { - PyErr_SetString(PyExc_ValueError, "blocks_max is too large"); - return NULL; - } - - if (!ImagingMemorySetBlocksMax(&ImagingDefaultArena, blocks_max)) { - return ImagingError_MemoryError(); - } - - Py_INCREF(Py_None); - return Py_None; -} - -static PyObject * -_clear_cache(PyObject *self, PyObject *args) { - int i = 0; - - if (!PyArg_ParseTuple(args, "|i:clear_cache", &i)) { - return NULL; - } - - ImagingMemoryClearCache(&ImagingDefaultArena, i); - - Py_INCREF(Py_None); - return Py_None; -} - -/* -------------------------------------------------------------------- */ - -/* FIXME: this is something of a mess. Should replace this with - pluggable codecs, but not before PIL 1.2 */ - -/* Decoders (in decode.c) */ -extern PyObject * -PyImaging_BcnDecoderNew(PyObject *self, PyObject *args); -extern PyObject * -PyImaging_BitDecoderNew(PyObject *self, PyObject *args); -extern PyObject * -PyImaging_FliDecoderNew(PyObject *self, PyObject *args); -extern PyObject * -PyImaging_GifDecoderNew(PyObject *self, PyObject *args); -extern PyObject * -PyImaging_HexDecoderNew(PyObject *self, PyObject *args); -extern PyObject * -PyImaging_JpegDecoderNew(PyObject *self, PyObject *args); -extern PyObject * -PyImaging_Jpeg2KDecoderNew(PyObject *self, PyObject *args); -extern PyObject * -PyImaging_LibTiffDecoderNew(PyObject *self, PyObject *args); -extern PyObject * -PyImaging_PackbitsDecoderNew(PyObject *self, PyObject *args); -extern PyObject * -PyImaging_PcdDecoderNew(PyObject *self, PyObject *args); -extern PyObject * -PyImaging_PcxDecoderNew(PyObject *self, PyObject *args); -extern PyObject * -PyImaging_RawDecoderNew(PyObject *self, PyObject *args); -extern PyObject * -PyImaging_SgiRleDecoderNew(PyObject *self, PyObject *args); -extern PyObject * -PyImaging_SunRleDecoderNew(PyObject *self, PyObject *args); -extern PyObject * -PyImaging_TgaRleDecoderNew(PyObject *self, PyObject *args); -extern PyObject * -PyImaging_XbmDecoderNew(PyObject *self, PyObject *args); -extern PyObject * -PyImaging_ZipDecoderNew(PyObject *self, PyObject *args); - -/* Encoders (in encode.c) */ -extern PyObject * -PyImaging_EpsEncoderNew(PyObject *self, PyObject *args); -extern PyObject * -PyImaging_GifEncoderNew(PyObject *self, PyObject *args); -extern PyObject * -PyImaging_JpegEncoderNew(PyObject *self, PyObject *args); -extern PyObject * -PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args); -extern PyObject * -PyImaging_PcxEncoderNew(PyObject *self, PyObject *args); -extern PyObject * -PyImaging_RawEncoderNew(PyObject *self, PyObject *args); -extern PyObject * -PyImaging_TgaRleEncoderNew(PyObject *self, PyObject *args); -extern PyObject * -PyImaging_XbmEncoderNew(PyObject *self, PyObject *args); -extern PyObject * -PyImaging_ZipEncoderNew(PyObject *self, PyObject *args); -extern PyObject * -PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args); - -/* Display support etc (in display.c) */ -#ifdef _WIN32 -extern PyObject * -PyImaging_CreateWindowWin32(PyObject *self, PyObject *args); -extern PyObject * -PyImaging_DisplayWin32(PyObject *self, PyObject *args); -extern PyObject * -PyImaging_DisplayModeWin32(PyObject *self, PyObject *args); -extern PyObject * -PyImaging_GrabScreenWin32(PyObject *self, PyObject *args); -extern PyObject * -PyImaging_GrabClipboardWin32(PyObject *self, PyObject *args); -extern PyObject * -PyImaging_EventLoopWin32(PyObject *self, PyObject *args); -extern PyObject * -PyImaging_DrawWmf(PyObject *self, PyObject *args); -#endif -#ifdef HAVE_XCB -extern PyObject * -PyImaging_GrabScreenX11(PyObject *self, PyObject *args); -#endif - -/* Experimental path stuff (in path.c) */ -extern PyObject * -PyPath_Create(ImagingObject *self, PyObject *args); - -/* Experimental outline stuff (in outline.c) */ -extern PyObject * -PyOutline_Create(ImagingObject *self, PyObject *args); - -extern PyObject * -PyImaging_MapBuffer(PyObject *self, PyObject *args); - -static PyMethodDef functions[] = { - - /* Object factories */ - {"alpha_composite", (PyCFunction)_alpha_composite, METH_VARARGS}, - {"blend", (PyCFunction)_blend, METH_VARARGS}, - {"fill", (PyCFunction)_fill, METH_VARARGS}, - {"new", (PyCFunction)_new, METH_VARARGS}, - {"merge", (PyCFunction)_merge, METH_VARARGS}, - - /* Functions */ - {"convert", (PyCFunction)_convert2, METH_VARARGS}, - - /* Codecs */ - {"bcn_decoder", (PyCFunction)PyImaging_BcnDecoderNew, METH_VARARGS}, - {"bit_decoder", (PyCFunction)PyImaging_BitDecoderNew, METH_VARARGS}, - {"eps_encoder", (PyCFunction)PyImaging_EpsEncoderNew, METH_VARARGS}, - {"fli_decoder", (PyCFunction)PyImaging_FliDecoderNew, METH_VARARGS}, - {"gif_decoder", (PyCFunction)PyImaging_GifDecoderNew, METH_VARARGS}, - {"gif_encoder", (PyCFunction)PyImaging_GifEncoderNew, METH_VARARGS}, - {"hex_decoder", (PyCFunction)PyImaging_HexDecoderNew, METH_VARARGS}, - {"hex_encoder", (PyCFunction)PyImaging_EpsEncoderNew, METH_VARARGS}, /* EPS=HEX! */ -#ifdef HAVE_LIBJPEG - {"jpeg_decoder", (PyCFunction)PyImaging_JpegDecoderNew, METH_VARARGS}, - {"jpeg_encoder", (PyCFunction)PyImaging_JpegEncoderNew, METH_VARARGS}, -#endif -#ifdef HAVE_OPENJPEG - {"jpeg2k_decoder", (PyCFunction)PyImaging_Jpeg2KDecoderNew, METH_VARARGS}, - {"jpeg2k_encoder", (PyCFunction)PyImaging_Jpeg2KEncoderNew, METH_VARARGS}, -#endif -#ifdef HAVE_LIBTIFF - {"libtiff_decoder", (PyCFunction)PyImaging_LibTiffDecoderNew, METH_VARARGS}, - {"libtiff_encoder", (PyCFunction)PyImaging_LibTiffEncoderNew, METH_VARARGS}, -#endif - {"packbits_decoder", (PyCFunction)PyImaging_PackbitsDecoderNew, METH_VARARGS}, - {"pcd_decoder", (PyCFunction)PyImaging_PcdDecoderNew, METH_VARARGS}, - {"pcx_decoder", (PyCFunction)PyImaging_PcxDecoderNew, METH_VARARGS}, - {"pcx_encoder", (PyCFunction)PyImaging_PcxEncoderNew, METH_VARARGS}, - {"raw_decoder", (PyCFunction)PyImaging_RawDecoderNew, METH_VARARGS}, - {"raw_encoder", (PyCFunction)PyImaging_RawEncoderNew, METH_VARARGS}, - {"sgi_rle_decoder", (PyCFunction)PyImaging_SgiRleDecoderNew, METH_VARARGS}, - {"sun_rle_decoder", (PyCFunction)PyImaging_SunRleDecoderNew, METH_VARARGS}, - {"tga_rle_decoder", (PyCFunction)PyImaging_TgaRleDecoderNew, METH_VARARGS}, - {"tga_rle_encoder", (PyCFunction)PyImaging_TgaRleEncoderNew, METH_VARARGS}, - {"xbm_decoder", (PyCFunction)PyImaging_XbmDecoderNew, METH_VARARGS}, - {"xbm_encoder", (PyCFunction)PyImaging_XbmEncoderNew, METH_VARARGS}, -#ifdef HAVE_LIBZ - {"zip_decoder", (PyCFunction)PyImaging_ZipDecoderNew, METH_VARARGS}, - {"zip_encoder", (PyCFunction)PyImaging_ZipEncoderNew, METH_VARARGS}, -#endif - -/* Memory mapping */ -#ifdef WITH_MAPPING - {"map_buffer", (PyCFunction)PyImaging_MapBuffer, METH_VARARGS}, -#endif - -/* Display support */ -#ifdef _WIN32 - {"display", (PyCFunction)PyImaging_DisplayWin32, METH_VARARGS}, - {"display_mode", (PyCFunction)PyImaging_DisplayModeWin32, METH_VARARGS}, - {"grabscreen_win32", (PyCFunction)PyImaging_GrabScreenWin32, METH_VARARGS}, - {"grabclipboard_win32", (PyCFunction)PyImaging_GrabClipboardWin32, METH_VARARGS}, - {"createwindow", (PyCFunction)PyImaging_CreateWindowWin32, METH_VARARGS}, - {"eventloop", (PyCFunction)PyImaging_EventLoopWin32, METH_VARARGS}, - {"drawwmf", (PyCFunction)PyImaging_DrawWmf, METH_VARARGS}, -#endif -#ifdef HAVE_XCB - {"grabscreen_x11", (PyCFunction)PyImaging_GrabScreenX11, METH_VARARGS}, -#endif - - /* Utilities */ - {"getcodecstatus", (PyCFunction)_getcodecstatus, METH_VARARGS}, - -/* Special effects (experimental) */ -#ifdef WITH_EFFECTS - {"effect_mandelbrot", (PyCFunction)_effect_mandelbrot, METH_VARARGS}, - {"effect_noise", (PyCFunction)_effect_noise, METH_VARARGS}, - {"linear_gradient", (PyCFunction)_linear_gradient, METH_VARARGS}, - {"radial_gradient", (PyCFunction)_radial_gradient, METH_VARARGS}, - {"wedge", (PyCFunction)_linear_gradient, METH_VARARGS}, /* Compatibility */ -#endif - -/* Drawing support stuff */ -#ifdef WITH_IMAGEDRAW - {"font", (PyCFunction)_font_new, METH_VARARGS}, - {"draw", (PyCFunction)_draw_new, METH_VARARGS}, -#endif - -/* Experimental path stuff */ -#ifdef WITH_IMAGEPATH - {"path", (PyCFunction)PyPath_Create, METH_VARARGS}, -#endif - -/* Experimental arrow graphics stuff */ -#ifdef WITH_ARROW - {"outline", (PyCFunction)PyOutline_Create, METH_VARARGS}, -#endif - - /* Resource management */ - {"get_stats", (PyCFunction)_get_stats, METH_VARARGS}, - {"reset_stats", (PyCFunction)_reset_stats, METH_VARARGS}, - {"get_alignment", (PyCFunction)_get_alignment, METH_VARARGS}, - {"get_block_size", (PyCFunction)_get_block_size, METH_VARARGS}, - {"get_blocks_max", (PyCFunction)_get_blocks_max, METH_VARARGS}, - {"set_alignment", (PyCFunction)_set_alignment, METH_VARARGS}, - {"set_block_size", (PyCFunction)_set_block_size, METH_VARARGS}, - {"set_blocks_max", (PyCFunction)_set_blocks_max, METH_VARARGS}, - {"clear_cache", (PyCFunction)_clear_cache, METH_VARARGS}, - - {NULL, NULL} /* sentinel */ -}; - -static int -setup_module(PyObject *m) { - PyObject *d = PyModule_GetDict(m); - const char *version = (char *)PILLOW_VERSION; - - /* Ready object types */ - if (PyType_Ready(&Imaging_Type) < 0) { - return -1; - } - -#ifdef WITH_IMAGEDRAW - if (PyType_Ready(&ImagingFont_Type) < 0) { - return -1; - } - - if (PyType_Ready(&ImagingDraw_Type) < 0) { - return -1; - } -#endif - if (PyType_Ready(&PixelAccess_Type) < 0) { - return -1; - } - - ImagingAccessInit(); - -#ifdef HAVE_LIBJPEG - { - extern const char *ImagingJpegVersion(void); - PyObject *v = PyUnicode_FromString(ImagingJpegVersion()); - PyDict_SetItemString(d, "jpeglib_version", v ? v : Py_None); - Py_XDECREF(v); - } -#endif - -#ifdef HAVE_OPENJPEG - { - extern const char *ImagingJpeg2KVersion(void); - PyObject *v = PyUnicode_FromString(ImagingJpeg2KVersion()); - PyDict_SetItemString(d, "jp2klib_version", v ? v : Py_None); - Py_XDECREF(v); - } -#endif - - PyObject *have_libjpegturbo; -#ifdef LIBJPEG_TURBO_VERSION - have_libjpegturbo = Py_True; - { -#define tostr1(a) #a -#define tostr(a) tostr1(a) - PyObject *v = PyUnicode_FromString(tostr(LIBJPEG_TURBO_VERSION)); - PyDict_SetItemString(d, "libjpeg_turbo_version", v ? v : Py_None); - Py_XDECREF(v); -#undef tostr -#undef tostr1 - } -#else - have_libjpegturbo = Py_False; -#endif - Py_INCREF(have_libjpegturbo); - PyModule_AddObject(m, "HAVE_LIBJPEGTURBO", have_libjpegturbo); - - PyObject *have_libimagequant; -#ifdef HAVE_LIBIMAGEQUANT - have_libimagequant = Py_True; - { - extern const char *ImagingImageQuantVersion(void); - PyObject *v = PyUnicode_FromString(ImagingImageQuantVersion()); - PyDict_SetItemString(d, "imagequant_version", v ? v : Py_None); - Py_XDECREF(v); - } -#else - have_libimagequant = Py_False; -#endif - Py_INCREF(have_libimagequant); - PyModule_AddObject(m, "HAVE_LIBIMAGEQUANT", have_libimagequant); - -#ifdef HAVE_LIBZ - /* zip encoding strategies */ - PyModule_AddIntConstant(m, "DEFAULT_STRATEGY", Z_DEFAULT_STRATEGY); - PyModule_AddIntConstant(m, "FILTERED", Z_FILTERED); - PyModule_AddIntConstant(m, "HUFFMAN_ONLY", Z_HUFFMAN_ONLY); - PyModule_AddIntConstant(m, "RLE", Z_RLE); - PyModule_AddIntConstant(m, "FIXED", Z_FIXED); - { - extern const char *ImagingZipVersion(void); - PyObject *v = PyUnicode_FromString(ImagingZipVersion()); - PyDict_SetItemString(d, "zlib_version", v ? v : Py_None); - Py_XDECREF(v); - } -#endif - -#ifdef HAVE_LIBTIFF - { - extern const char *ImagingTiffVersion(void); - PyObject *v = PyUnicode_FromString(ImagingTiffVersion()); - PyDict_SetItemString(d, "libtiff_version", v ? v : Py_None); - Py_XDECREF(v); - - // Test for libtiff 4.0 or later, excluding libtiff 3.9.6 and 3.9.7 - PyObject *support_custom_tags; -#if TIFFLIB_VERSION >= 20111221 && TIFFLIB_VERSION != 20120218 && \ - TIFFLIB_VERSION != 20120922 - support_custom_tags = Py_True; -#else - support_custom_tags = Py_False; -#endif - PyDict_SetItemString(d, "libtiff_support_custom_tags", support_custom_tags); - } -#endif - - PyObject *have_xcb; -#ifdef HAVE_XCB - have_xcb = Py_True; -#else - have_xcb = Py_False; -#endif - Py_INCREF(have_xcb); - PyModule_AddObject(m, "HAVE_XCB", have_xcb); - - PyObject *pillow_version = PyUnicode_FromString(version); - PyDict_SetItemString( - d, "PILLOW_VERSION", pillow_version ? pillow_version : Py_None); - Py_XDECREF(pillow_version); - - return 0; -} - -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 = PyModule_Create(&module_def); - - if (setup_module(m) < 0) { - Py_DECREF(m); - return NULL; - } - - return m; -} +/* + * The Python Imaging Library. + * + * the imaging library bindings + * + * history: + * 1995-09-24 fl Created + * 1996-03-24 fl Ready for first public release (release 0.0) + * 1996-03-25 fl Added fromstring (for Jack's "img" library) + * 1996-03-28 fl Added channel operations + * 1996-03-31 fl Added point operation + * 1996-04-08 fl Added new/new_block/new_array factories + * 1996-04-13 fl Added decoders + * 1996-05-04 fl Added palette hack + * 1996-05-12 fl Compile cleanly as C++ + * 1996-05-19 fl Added matrix conversions, gradient fills + * 1996-05-27 fl Added display_mode + * 1996-07-22 fl Added getbbox, offset + * 1996-07-23 fl Added sequence semantics + * 1996-08-13 fl Added logical operators, point mode + * 1996-08-16 fl Modified paste interface + * 1996-09-06 fl Added putdata methods, use abstract interface + * 1996-11-01 fl Added xbm encoder + * 1996-11-04 fl Added experimental path stuff, draw_lines, etc + * 1996-12-10 fl Added zip decoder, crc32 interface + * 1996-12-14 fl Added modulo arithmetics + * 1996-12-29 fl Added zip encoder + * 1997-01-03 fl Added fli and msp decoders + * 1997-01-04 fl Added experimental sun_rle and tga_rle decoders + * 1997-01-05 fl Added gif encoder, getpalette hack + * 1997-02-23 fl Added histogram mask + * 1997-05-12 fl Minor tweaks to match the IFUNC95 interface + * 1997-05-21 fl Added noise generator, spread effect + * 1997-06-05 fl Added mandelbrot generator + * 1997-08-02 fl Modified putpalette to coerce image mode if necessary + * 1998-01-11 fl Added INT32 support + * 1998-01-22 fl Fixed draw_points to draw the last point too + * 1998-06-28 fl Added getpixel, getink, draw_ink + * 1998-07-12 fl Added getextrema + * 1998-07-17 fl Added point conversion to arbitrary formats + * 1998-09-21 fl Added support for resampling filters + * 1998-09-22 fl Added support for quad transform + * 1998-12-29 fl Added support for arcs, chords, and pieslices + * 1999-01-10 fl Added some experimental arrow graphics stuff + * 1999-02-06 fl Added draw_bitmap, font acceleration stuff + * 2001-04-17 fl Fixed some egcs compiler nits + * 2001-09-17 fl Added screen grab primitives (win32) + * 2002-03-09 fl Added stretch primitive + * 2002-03-10 fl Fixed filter handling in rotate + * 2002-06-06 fl Added I, F, and RGB support to putdata + * 2002-06-08 fl Added rankfilter + * 2002-06-09 fl Added support for user-defined filter kernels + * 2002-11-19 fl Added clipboard grab primitives (win32) + * 2002-12-11 fl Added draw context + * 2003-04-26 fl Tweaks for Python 2.3 beta 1 + * 2003-05-21 fl Added createwindow primitive (win32) + * 2003-09-13 fl Added thread section hooks + * 2003-09-15 fl Added expand helper + * 2003-09-26 fl Added experimental LA support + * 2004-02-21 fl Handle zero-size images in quantize + * 2004-06-05 fl Added ptr attribute (used to access Imaging objects) + * 2004-06-05 fl Don't crash when fetching pixels from zero-wide images + * 2004-09-17 fl Added getcolors + * 2004-10-04 fl Added modefilter + * 2005-10-02 fl Added access proxy + * 2006-06-18 fl Always draw last point in polyline + * + * Copyright (c) 1997-2006 by Secret Labs AB + * Copyright (c) 1995-2006 by Fredrik Lundh + * + * See the README file for information on usage and redistribution. + */ + +#define PY_SSIZE_T_CLEAN +#include "Python.h" + +#ifdef HAVE_LIBJPEG +#include "jconfig.h" +#endif + +#ifdef HAVE_LIBZ +#include "zlib.h" +#endif + +#ifdef HAVE_LIBTIFF +#ifndef _TIFFIO_ +#include +#endif +#endif + +#include "libImaging/Imaging.h" + +#define _USE_MATH_DEFINES +#include + +/* Configuration stuff. Feel free to undef things you don't need. */ +#define WITH_IMAGECHOPS /* ImageChops support */ +#define WITH_IMAGEDRAW /* ImageDraw support */ +#define WITH_MAPPING /* use memory mapping to read some file formats */ +#define WITH_IMAGEPATH /* ImagePath stuff */ +#define WITH_ARROW /* arrow graphics stuff (experimental) */ +#define WITH_EFFECTS /* special effects */ +#define WITH_QUANTIZE /* quantization support */ +#define WITH_RANKFILTER /* rank filter */ +#define WITH_MODEFILTER /* mode filter */ +#define WITH_THREADING /* "friendly" threading support */ +#define WITH_UNSHARPMASK /* Kevin Cazabon's unsharpmask module */ + +#undef VERBOSE + +#define B16(p, i) ((((int)p[(i)]) << 8) + p[(i) + 1]) +#define L16(p, i) ((((int)p[(i) + 1]) << 8) + p[(i)]) +#define S16(v) ((v) < 32768 ? (v) : ((v) - 65536)) + +/* -------------------------------------------------------------------- */ +/* OBJECT ADMINISTRATION */ +/* -------------------------------------------------------------------- */ + +typedef struct { + PyObject_HEAD Imaging image; + ImagingAccess access; +} ImagingObject; + +static PyTypeObject Imaging_Type; + +#ifdef WITH_IMAGEDRAW + +typedef struct { + /* to write a character, cut out sxy from glyph data, place + at current position plus dxy, and advance by (dx, dy) */ + int dx, dy; + int dx0, dy0, dx1, dy1; + int sx0, sy0, sx1, sy1; +} Glyph; + +typedef struct { + PyObject_HEAD ImagingObject *ref; + Imaging bitmap; + int ysize; + int baseline; + Glyph glyphs[256]; +} ImagingFontObject; + +static PyTypeObject ImagingFont_Type; + +typedef struct { + PyObject_HEAD ImagingObject *image; + UINT8 ink[4]; + int blend; +} ImagingDrawObject; + +static PyTypeObject ImagingDraw_Type; + +#endif + +typedef struct { + PyObject_HEAD ImagingObject *image; + int readonly; +} PixelAccessObject; + +static PyTypeObject PixelAccess_Type; + +PyObject * +PyImagingNew(Imaging imOut) { + ImagingObject *imagep; + + if (!imOut) { + return NULL; + } + + imagep = PyObject_New(ImagingObject, &Imaging_Type); + if (imagep == NULL) { + ImagingDelete(imOut); + return NULL; + } + +#ifdef VERBOSE + printf("imaging %p allocated\n", imagep); +#endif + + imagep->image = imOut; + imagep->access = ImagingAccessNew(imOut); + + return (PyObject *)imagep; +} + +static void +_dealloc(ImagingObject *imagep) { +#ifdef VERBOSE + printf("imaging %p deleted\n", imagep); +#endif + + if (imagep->access) { + ImagingAccessDelete(imagep->image, imagep->access); + } + ImagingDelete(imagep->image); + PyObject_Del(imagep); +} + +#define PyImaging_Check(op) (Py_TYPE(op) == &Imaging_Type) + +Imaging +PyImaging_AsImaging(PyObject *op) { + if (!PyImaging_Check(op)) { + PyErr_BadInternalCall(); + return NULL; + } + + return ((ImagingObject *)op)->image; +} + +/* -------------------------------------------------------------------- */ +/* THREAD HANDLING */ +/* -------------------------------------------------------------------- */ + +void +ImagingSectionEnter(ImagingSectionCookie *cookie) { +#ifdef WITH_THREADING + *cookie = (PyThreadState *)PyEval_SaveThread(); +#endif +} + +void +ImagingSectionLeave(ImagingSectionCookie *cookie) { +#ifdef WITH_THREADING + PyEval_RestoreThread((PyThreadState *)*cookie); +#endif +} + +/* -------------------------------------------------------------------- */ +/* BUFFER HANDLING */ +/* -------------------------------------------------------------------- */ +/* Python compatibility API */ + +int +PyImaging_CheckBuffer(PyObject *buffer) { + return PyObject_CheckBuffer(buffer); +} + +int +PyImaging_GetBuffer(PyObject *buffer, Py_buffer *view) { + /* must call check_buffer first! */ + return PyObject_GetBuffer(buffer, view, PyBUF_SIMPLE); +} + +/* -------------------------------------------------------------------- */ +/* EXCEPTION REROUTING */ +/* -------------------------------------------------------------------- */ + +/* error messages */ +static const char *must_be_sequence = "argument must be a sequence"; +static const char *must_be_two_coordinates = + "coordinate list must contain exactly 2 coordinates"; +static const char *incorrectly_ordered_x_coordinate = + "x1 must be greater than or equal to x0"; +static const char *incorrectly_ordered_y_coordinate = + "y1 must be greater than or equal to y0"; +static const char *wrong_mode = "unrecognized image mode"; +static const char *wrong_raw_mode = "unrecognized raw mode"; +static const char *outside_image = "image index out of range"; +static const char *outside_palette = "palette index out of range"; +static const char *wrong_palette_size = "invalid palette size"; +static const char *no_palette = "image has no palette"; +static const char *readonly = "image is readonly"; +/* static const char* no_content = "image has no content"; */ + +void * +ImagingError_OSError(void) { + PyErr_SetString(PyExc_OSError, "error when accessing file"); + return NULL; +} + +void * +ImagingError_MemoryError(void) { + return PyErr_NoMemory(); +} + +void * +ImagingError_Mismatch(void) { + PyErr_SetString(PyExc_ValueError, "images do not match"); + return NULL; +} + +void * +ImagingError_ModeError(void) { + PyErr_SetString(PyExc_ValueError, "image has wrong mode"); + return NULL; +} + +void * +ImagingError_ValueError(const char *message) { + PyErr_SetString( + PyExc_ValueError, (message) ? (char *)message : "unrecognized argument value"); + return NULL; +} + +void +ImagingError_Clear(void) { + PyErr_Clear(); +} + +/* -------------------------------------------------------------------- */ +/* HELPERS */ +/* -------------------------------------------------------------------- */ + +static int +getbands(const char *mode) { + Imaging im; + int bands; + + /* FIXME: add primitive to libImaging to avoid extra allocation */ + im = ImagingNew(mode, 0, 0); + if (!im) { + return -1; + } + + bands = im->bands; + + ImagingDelete(im); + + return bands; +} + +#define TYPE_UINT8 (0x100 | sizeof(UINT8)) +#define TYPE_INT32 (0x200 | sizeof(INT32)) +#define TYPE_FLOAT16 (0x500 | sizeof(FLOAT16)) +#define TYPE_FLOAT32 (0x300 | sizeof(FLOAT32)) +#define TYPE_DOUBLE (0x400 | sizeof(double)) + +static void * +getlist(PyObject *arg, Py_ssize_t *length, const char *wrong_length, int type) { + /* - allocates and returns a c array of the items in the + python sequence arg. + - the size of the returned array is in length + - all of the arg items must be numeric items of the type + specified in type + - sequence length is checked against the length parameter IF + an error parameter is passed in wrong_length + - caller is responsible for freeing the memory + */ + + Py_ssize_t i, n; + int itemp; + double dtemp; + FLOAT32 ftemp; + UINT8 *list; + PyObject *seq; + PyObject *op; + + if (!PySequence_Check(arg)) { + PyErr_SetString(PyExc_TypeError, must_be_sequence); + return NULL; + } + + n = PySequence_Size(arg); + if (length && wrong_length && n != *length) { + PyErr_SetString(PyExc_ValueError, wrong_length); + return NULL; + } + + /* malloc check ok, type & ff is just a sizeof(something) + calloc checks for overflow */ + list = calloc(n, type & 0xff); + if (!list) { + return ImagingError_MemoryError(); + } + + seq = PySequence_Fast(arg, must_be_sequence); + if (!seq) { + free(list); + return NULL; + } + + for (i = 0; i < n; i++) { + op = PySequence_Fast_GET_ITEM(seq, i); + // DRY, branch prediction is going to work _really_ well + // on this switch. And 3 fewer loops to copy/paste. + switch (type) { + case TYPE_UINT8: + itemp = PyLong_AsLong(op); + list[i] = CLIP8(itemp); + break; + case TYPE_INT32: + itemp = PyLong_AsLong(op); + memcpy(list + i * sizeof(INT32), &itemp, sizeof(itemp)); + break; + case TYPE_FLOAT32: + ftemp = (FLOAT32)PyFloat_AsDouble(op); + memcpy(list + i * sizeof(ftemp), &ftemp, sizeof(ftemp)); + break; + case TYPE_DOUBLE: + dtemp = PyFloat_AsDouble(op); + memcpy(list + i * sizeof(dtemp), &dtemp, sizeof(dtemp)); + break; + } + } + + Py_DECREF(seq); + + if (PyErr_Occurred()) { + free(list); + return NULL; + } + + if (length) { + *length = n; + } + + return list; +} + +FLOAT32 +float16tofloat32(const FLOAT16 in) { + UINT32 t1; + UINT32 t2; + UINT32 t3; + FLOAT32 out[1] = {0}; + + t1 = in & 0x7fff; // Non-sign bits + t2 = in & 0x8000; // Sign bit + t3 = in & 0x7c00; // Exponent + + t1 <<= 13; // Align mantissa on MSB + t2 <<= 16; // Shift sign bit into position + + t1 += 0x38000000; // Adjust bias + + t1 = (t3 == 0 ? 0 : t1); // Denormals-as-zero + + t1 |= t2; // Re-insert sign bit + + memcpy(out, &t1, 4); + return out[0]; +} + +static inline PyObject * +getpixel(Imaging im, ImagingAccess access, int x, int y) { + union { + UINT8 b[4]; + UINT16 h; + INT32 i; + FLOAT32 f; + } pixel; + + if (x < 0) { + x = im->xsize + x; + } + if (y < 0) { + y = im->ysize + y; + } + + if (x < 0 || x >= im->xsize || y < 0 || y >= im->ysize) { + PyErr_SetString(PyExc_IndexError, outside_image); + return NULL; + } + + access->get_pixel(im, x, y, &pixel); + + switch (im->type) { + case IMAGING_TYPE_UINT8: + switch (im->bands) { + case 1: + return PyLong_FromLong(pixel.b[0]); + case 2: + return Py_BuildValue("BB", pixel.b[0], pixel.b[1]); + case 3: + return Py_BuildValue("BBB", pixel.b[0], pixel.b[1], pixel.b[2]); + case 4: + return Py_BuildValue( + "BBBB", pixel.b[0], pixel.b[1], pixel.b[2], pixel.b[3]); + } + break; + case IMAGING_TYPE_INT32: + return PyLong_FromLong(pixel.i); + case IMAGING_TYPE_FLOAT32: + return PyFloat_FromDouble(pixel.f); + case IMAGING_TYPE_SPECIAL: + if (im->bands == 1) { + return PyLong_FromLong(pixel.h); + } else { + return Py_BuildValue("BBB", pixel.b[0], pixel.b[1], pixel.b[2]); + } + break; + } + + /* unknown type */ + Py_INCREF(Py_None); + return Py_None; +} + +static char * +getink(PyObject *color, Imaging im, char *ink) { + int g = 0, b = 0, a = 0; + double f = 0; + /* Windows 64 bit longs are 32 bits, and 0xFFFFFFFF (white) is a + Python long (not int) that raises an overflow error when trying + to return it into a 32 bit C long + */ + PY_LONG_LONG r = 0; + FLOAT32 ftmp; + INT32 itmp; + + /* fill ink buffer (four bytes) with something that can + be cast to either UINT8 or INT32 */ + + int rIsInt = 0; + int tupleSize = PyTuple_Check(color) ? PyTuple_GET_SIZE(color) : -1; + if (tupleSize == 1) { + color = PyTuple_GetItem(color, 0); + } + if (im->type == IMAGING_TYPE_UINT8 || im->type == IMAGING_TYPE_INT32 || + im->type == IMAGING_TYPE_SPECIAL) { + if (PyLong_Check(color)) { + r = PyLong_AsLongLong(color); + if (r == -1 && PyErr_Occurred()) { + return NULL; + } + rIsInt = 1; + } else if (im->bands == 1) { + PyErr_SetString( + PyExc_TypeError, "color must be int or single-element tuple"); + return NULL; + } else if (tupleSize == -1) { + PyErr_SetString(PyExc_TypeError, "color must be int or tuple"); + return NULL; + } + } + + switch (im->type) { + case IMAGING_TYPE_UINT8: + /* unsigned integer */ + if (im->bands == 1) { + /* unsigned integer, single layer */ + if (rIsInt != 1) { + if (tupleSize != 1) { + PyErr_SetString( + PyExc_TypeError, + "color must be int or single-element tuple"); + return NULL; + } else if (!PyArg_ParseTuple(color, "L", &r)) { + return NULL; + } + } + ink[0] = (char)CLIP8(r); + ink[1] = ink[2] = ink[3] = 0; + } else { + if (rIsInt) { + /* compatibility: ABGR */ + a = (UINT8)(r >> 24); + b = (UINT8)(r >> 16); + g = (UINT8)(r >> 8); + r = (UINT8)r; + } else { + a = 255; + if (im->bands == 2) { + if (tupleSize != 1 && tupleSize != 2) { + PyErr_SetString( + PyExc_TypeError, + "color must be int, or tuple of one or two elements"); + return NULL; + } else if (!PyArg_ParseTuple(color, "L|i", &r, &a)) { + return NULL; + } + g = b = r; + } else { + if (tupleSize != 3 && tupleSize != 4) { + PyErr_SetString( + PyExc_TypeError, + "color must be int, or tuple of one, three or four " + "elements"); + return NULL; + } else if (!PyArg_ParseTuple(color, "Lii|i", &r, &g, &b, &a)) { + return NULL; + } + } + } + ink[0] = (char)CLIP8(r); + ink[1] = (char)CLIP8(g); + ink[2] = (char)CLIP8(b); + ink[3] = (char)CLIP8(a); + } + return ink; + case IMAGING_TYPE_INT32: + /* signed integer */ + itmp = r; + memcpy(ink, &itmp, sizeof(itmp)); + return ink; + case IMAGING_TYPE_FLOAT32: + /* floating point */ + f = PyFloat_AsDouble(color); + if (f == -1.0 && PyErr_Occurred()) { + return NULL; + } + ftmp = f; + memcpy(ink, &ftmp, sizeof(ftmp)); + return ink; + case IMAGING_TYPE_SPECIAL: + if (strncmp(im->mode, "I;16", 4) == 0) { + ink[0] = (UINT8)r; + ink[1] = (UINT8)(r >> 8); + ink[2] = ink[3] = 0; + return ink; + } else { + if (rIsInt) { + b = (UINT8)(r >> 16); + g = (UINT8)(r >> 8); + r = (UINT8)r; + } else if (tupleSize != 3) { + PyErr_SetString( + PyExc_TypeError, + "color must be int, or tuple of one or three elements"); + return NULL; + } else if (!PyArg_ParseTuple(color, "iiL", &b, &g, &r)) { + return NULL; + } + if (!strcmp(im->mode, "BGR;15")) { + UINT16 v = ((((UINT16)r) << 7) & 0x7c00) + + ((((UINT16)g) << 2) & 0x03e0) + + ((((UINT16)b) >> 3) & 0x001f); + + ink[0] = (UINT8)v; + ink[1] = (UINT8)(v >> 8); + ink[2] = ink[3] = 0; + return ink; + } else if (!strcmp(im->mode, "BGR;16")) { + UINT16 v = ((((UINT16)r) << 8) & 0xf800) + + ((((UINT16)g) << 3) & 0x07e0) + + ((((UINT16)b) >> 3) & 0x001f); + ink[0] = (UINT8)v; + ink[1] = (UINT8)(v >> 8); + ink[2] = ink[3] = 0; + return ink; + } else if (!strcmp(im->mode, "BGR;24")) { + ink[0] = (UINT8)b; + ink[1] = (UINT8)g; + ink[2] = (UINT8)r; + ink[3] = 0; + return ink; + } + } + } + + PyErr_SetString(PyExc_ValueError, wrong_mode); + return NULL; +} + +/* -------------------------------------------------------------------- */ +/* FACTORIES */ +/* -------------------------------------------------------------------- */ + +static PyObject * +_fill(PyObject *self, PyObject *args) { + char *mode; + int xsize, ysize; + PyObject *color; + char buffer[4]; + Imaging im; + + xsize = ysize = 256; + color = NULL; + + if (!PyArg_ParseTuple(args, "s|(ii)O", &mode, &xsize, &ysize, &color)) { + return NULL; + } + + im = ImagingNewDirty(mode, xsize, ysize); + if (!im) { + return NULL; + } + + buffer[0] = buffer[1] = buffer[2] = buffer[3] = 0; + if (color) { + if (!getink(color, im, buffer)) { + ImagingDelete(im); + return NULL; + } + } + + (void)ImagingFill(im, buffer); + + return PyImagingNew(im); +} + +static PyObject * +_new(PyObject *self, PyObject *args) { + char *mode; + int xsize, ysize; + + if (!PyArg_ParseTuple(args, "s(ii)", &mode, &xsize, &ysize)) { + return NULL; + } + + return PyImagingNew(ImagingNew(mode, xsize, ysize)); +} + +static PyObject * +_new_block(PyObject *self, PyObject *args) { + char *mode; + int xsize, ysize; + + if (!PyArg_ParseTuple(args, "s(ii)", &mode, &xsize, &ysize)) { + return NULL; + } + + return PyImagingNew(ImagingNewBlock(mode, xsize, ysize)); +} + +static PyObject * +_linear_gradient(PyObject *self, PyObject *args) { + char *mode; + + if (!PyArg_ParseTuple(args, "s", &mode)) { + return NULL; + } + + return PyImagingNew(ImagingFillLinearGradient(mode)); +} + +static PyObject * +_radial_gradient(PyObject *self, PyObject *args) { + char *mode; + + if (!PyArg_ParseTuple(args, "s", &mode)) { + return NULL; + } + + return PyImagingNew(ImagingFillRadialGradient(mode)); +} + +static PyObject * +_alpha_composite(ImagingObject *self, PyObject *args) { + ImagingObject *imagep1; + ImagingObject *imagep2; + + if (!PyArg_ParseTuple( + args, "O!O!", &Imaging_Type, &imagep1, &Imaging_Type, &imagep2)) { + return NULL; + } + + return PyImagingNew(ImagingAlphaComposite(imagep1->image, imagep2->image)); +} + +static PyObject * +_blend(ImagingObject *self, PyObject *args) { + ImagingObject *imagep1; + ImagingObject *imagep2; + double alpha; + + alpha = 0.5; + if (!PyArg_ParseTuple( + args, "O!O!|d", &Imaging_Type, &imagep1, &Imaging_Type, &imagep2, &alpha)) { + return NULL; + } + + return PyImagingNew(ImagingBlend(imagep1->image, imagep2->image, (float)alpha)); +} + +/* -------------------------------------------------------------------- */ +/* METHODS */ +/* -------------------------------------------------------------------- */ + +static INT16 * +_prepare_lut_table(PyObject *table, Py_ssize_t table_size) { + int i; + Py_buffer buffer_info; + INT32 data_type = TYPE_FLOAT32; + float item = 0; + void *table_data = NULL; + int free_table_data = 0; + INT16 *prepared; + +/* NOTE: This value should be the same as in ColorLUT.c */ +#define PRECISION_BITS (16 - 8 - 2) + + const char *wrong_size = + ("The table should have table_channels * " + "size1D * size2D * size3D float items."); + + if (PyObject_CheckBuffer(table)) { + if (!PyObject_GetBuffer(table, &buffer_info, PyBUF_CONTIG_RO | PyBUF_FORMAT)) { + if (buffer_info.ndim == 1 && buffer_info.shape[0] == table_size) { + if (strlen(buffer_info.format) == 1) { + switch (buffer_info.format[0]) { + case 'e': + data_type = TYPE_FLOAT16; + table_data = buffer_info.buf; + break; + case 'f': + data_type = TYPE_FLOAT32; + table_data = buffer_info.buf; + break; + case 'd': + data_type = TYPE_DOUBLE; + table_data = buffer_info.buf; + break; + } + } + } + PyBuffer_Release(&buffer_info); + } + } + + if (!table_data) { + free_table_data = 1; + table_data = getlist(table, &table_size, wrong_size, TYPE_FLOAT32); + if (!table_data) { + return NULL; + } + } + + /* malloc check ok, max is 2 * 4 * 65**3 = 2197000 */ + prepared = (INT16 *)malloc(sizeof(INT16) * table_size); + if (!prepared) { + if (free_table_data) { + free(table_data); + } + return (INT16 *)ImagingError_MemoryError(); + } + + for (i = 0; i < table_size; i++) { + FLOAT16 htmp; + double dtmp; + switch (data_type) { + case TYPE_FLOAT16: + memcpy(&htmp, ((char *)table_data) + i * sizeof(htmp), sizeof(htmp)); + item = float16tofloat32(htmp); + break; + case TYPE_FLOAT32: + memcpy( + &item, ((char *)table_data) + i * sizeof(FLOAT32), sizeof(FLOAT32)); + break; + case TYPE_DOUBLE: + memcpy(&dtmp, ((char *)table_data) + i * sizeof(dtmp), sizeof(dtmp)); + item = (FLOAT32)dtmp; + break; + } + /* Max value for INT16 */ + if (item >= (0x7fff - 0.5) / (255 << PRECISION_BITS)) { + prepared[i] = 0x7fff; + continue; + } + /* Min value for INT16 */ + if (item <= (-0x8000 + 0.5) / (255 << PRECISION_BITS)) { + prepared[i] = -0x8000; + continue; + } + if (item < 0) { + prepared[i] = item * (255 << PRECISION_BITS) - 0.5; + } else { + prepared[i] = item * (255 << PRECISION_BITS) + 0.5; + } + } + +#undef PRECISION_BITS + if (free_table_data) { + free(table_data); + } + return prepared; +} + +static PyObject * +_color_lut_3d(ImagingObject *self, PyObject *args) { + char *mode; + int filter; + int table_channels; + int size1D, size2D, size3D; + PyObject *table; + + INT16 *prepared_table; + Imaging imOut; + + if (!PyArg_ParseTuple( + args, + "siiiiiO:color_lut_3d", + &mode, + &filter, + &table_channels, + &size1D, + &size2D, + &size3D, + &table)) { + return NULL; + } + + /* actually, it is trilinear */ + if (filter != IMAGING_TRANSFORM_BILINEAR) { + PyErr_SetString(PyExc_ValueError, "Only LINEAR filter is supported."); + return NULL; + } + + if (1 > table_channels || table_channels > 4) { + PyErr_SetString(PyExc_ValueError, "table_channels should be from 1 to 4"); + return NULL; + } + + if (2 > size1D || size1D > 65 || 2 > size2D || size2D > 65 || 2 > size3D || + size3D > 65) { + PyErr_SetString( + PyExc_ValueError, "Table size in any dimension should be from 2 to 65"); + return NULL; + } + + prepared_table = + _prepare_lut_table(table, table_channels * size1D * size2D * size3D); + if (!prepared_table) { + return NULL; + } + + imOut = ImagingNewDirty(mode, self->image->xsize, self->image->ysize); + if (!imOut) { + free(prepared_table); + return NULL; + } + + if (!ImagingColorLUT3D_linear( + imOut, + self->image, + table_channels, + size1D, + size2D, + size3D, + prepared_table)) { + free(prepared_table); + ImagingDelete(imOut); + return NULL; + } + + free(prepared_table); + + return PyImagingNew(imOut); +} + +static PyObject * +_convert(ImagingObject *self, PyObject *args) { + char *mode; + int dither = 0; + ImagingObject *paletteimage = NULL; + + if (!PyArg_ParseTuple(args, "s|iO", &mode, &dither, &paletteimage)) { + return NULL; + } + if (paletteimage != NULL) { + if (!PyImaging_Check(paletteimage)) { + PyObject_Print((PyObject *)paletteimage, stderr, 0); + PyErr_SetString( + PyExc_ValueError, "palette argument must be image with mode 'P'"); + return NULL; + } + if (paletteimage->image->palette == NULL) { + PyErr_SetString(PyExc_ValueError, "null palette"); + return NULL; + } + } + + return PyImagingNew(ImagingConvert( + self->image, mode, paletteimage ? paletteimage->image->palette : NULL, dither)); +} + +static PyObject * +_convert2(ImagingObject *self, PyObject *args) { + ImagingObject *imagep1; + ImagingObject *imagep2; + if (!PyArg_ParseTuple( + args, "O!O!", &Imaging_Type, &imagep1, &Imaging_Type, &imagep2)) { + return NULL; + } + + if (!ImagingConvert2(imagep1->image, imagep2->image)) { + return NULL; + } + + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject * +_convert_matrix(ImagingObject *self, PyObject *args) { + char *mode; + float m[12]; + if (!PyArg_ParseTuple(args, "s(ffff)", &mode, m + 0, m + 1, m + 2, m + 3)) { + PyErr_Clear(); + if (!PyArg_ParseTuple( + args, + "s(ffffffffffff)", + &mode, + m + 0, + m + 1, + m + 2, + m + 3, + m + 4, + m + 5, + m + 6, + m + 7, + m + 8, + m + 9, + m + 10, + m + 11)) { + return NULL; + } + } + + return PyImagingNew(ImagingConvertMatrix(self->image, mode, m)); +} + +static PyObject * +_convert_transparent(ImagingObject *self, PyObject *args) { + char *mode; + int r, g, b; + if (PyArg_ParseTuple(args, "s(iii)", &mode, &r, &g, &b)) { + return PyImagingNew(ImagingConvertTransparent(self->image, mode, r, g, b)); + } + PyErr_Clear(); + if (PyArg_ParseTuple(args, "si", &mode, &r)) { + return PyImagingNew(ImagingConvertTransparent(self->image, mode, r, 0, 0)); + } + return NULL; +} + +static PyObject * +_copy(ImagingObject *self, PyObject *args) { + if (!PyArg_ParseTuple(args, "")) { + return NULL; + } + + return PyImagingNew(ImagingCopy(self->image)); +} + +static PyObject * +_crop(ImagingObject *self, PyObject *args) { + int x0, y0, x1, y1; + if (!PyArg_ParseTuple(args, "(iiii)", &x0, &y0, &x1, &y1)) { + return NULL; + } + + return PyImagingNew(ImagingCrop(self->image, x0, y0, x1, y1)); +} + +static PyObject * +_expand_image(ImagingObject *self, PyObject *args) { + int x, y; + if (!PyArg_ParseTuple(args, "ii", &x, &y)) { + return NULL; + } + + return PyImagingNew(ImagingExpand(self->image, x, y)); +} + +static PyObject * +_filter(ImagingObject *self, PyObject *args) { + PyObject *imOut; + Py_ssize_t kernelsize; + FLOAT32 *kerneldata; + + int xsize, ysize, i; + float divisor, offset; + PyObject *kernel = NULL; + if (!PyArg_ParseTuple( + args, "(ii)ffO", &xsize, &ysize, &divisor, &offset, &kernel)) { + return NULL; + } + + /* get user-defined kernel */ + kerneldata = getlist(kernel, &kernelsize, NULL, TYPE_FLOAT32); + if (!kerneldata) { + return NULL; + } + if (kernelsize != (Py_ssize_t)xsize * (Py_ssize_t)ysize) { + free(kerneldata); + return ImagingError_ValueError("bad kernel size"); + } + + for (i = 0; i < kernelsize; ++i) { + kerneldata[i] /= divisor; + } + + imOut = PyImagingNew(ImagingFilter(self->image, xsize, ysize, kerneldata, offset)); + + free(kerneldata); + + return imOut; +} + +#ifdef WITH_UNSHARPMASK +static PyObject * +_gaussian_blur(ImagingObject *self, PyObject *args) { + Imaging imIn; + Imaging imOut; + + float xradius, yradius; + int passes = 3; + if (!PyArg_ParseTuple(args, "(ff)|i", &xradius, &yradius, &passes)) { + return NULL; + } + + imIn = self->image; + imOut = ImagingNewDirty(imIn->mode, imIn->xsize, imIn->ysize); + if (!imOut) { + return NULL; + } + + if (!ImagingGaussianBlur(imOut, imIn, xradius, yradius, passes)) { + ImagingDelete(imOut); + return NULL; + } + + return PyImagingNew(imOut); +} +#endif + +static PyObject * +_getpalette(ImagingObject *self, PyObject *args) { + PyObject *palette; + int palettesize; + int bits; + ImagingShuffler pack; + + char *mode = "RGB"; + char *rawmode = "RGB"; + if (!PyArg_ParseTuple(args, "|ss", &mode, &rawmode)) { + return NULL; + } + + if (!self->image->palette) { + PyErr_SetString(PyExc_ValueError, no_palette); + return NULL; + } + + pack = ImagingFindPacker(mode, rawmode, &bits); + if (!pack) { + PyErr_SetString(PyExc_ValueError, wrong_raw_mode); + return NULL; + } + + palettesize = self->image->palette->size; + palette = PyBytes_FromStringAndSize(NULL, palettesize * bits / 8); + if (!palette) { + return NULL; + } + + pack( + (UINT8 *)PyBytes_AsString(palette), self->image->palette->palette, palettesize); + + return palette; +} + +static PyObject * +_getpalettemode(ImagingObject *self) { + if (!self->image->palette) { + PyErr_SetString(PyExc_ValueError, no_palette); + return NULL; + } + + return PyUnicode_FromString(self->image->palette->mode); +} + +static inline int +_getxy(PyObject *xy, int *x, int *y) { + PyObject *value; + + if (!PyTuple_Check(xy) || PyTuple_GET_SIZE(xy) != 2) { + goto badarg; + } + + value = PyTuple_GET_ITEM(xy, 0); + if (PyLong_Check(value)) { + *x = PyLong_AS_LONG(value); + } else if (PyFloat_Check(value)) { + *x = (int)PyFloat_AS_DOUBLE(value); + } else { + PyObject *int_value = PyObject_CallMethod(value, "__int__", NULL); + if (int_value != NULL && PyLong_Check(int_value)) { + *x = PyLong_AS_LONG(int_value); + } else { + goto badval; + } + } + + value = PyTuple_GET_ITEM(xy, 1); + if (PyLong_Check(value)) { + *y = PyLong_AS_LONG(value); + } else if (PyFloat_Check(value)) { + *y = (int)PyFloat_AS_DOUBLE(value); + } else { + PyObject *int_value = PyObject_CallMethod(value, "__int__", NULL); + if (int_value != NULL && PyLong_Check(int_value)) { + *y = PyLong_AS_LONG(int_value); + } else { + goto badval; + } + } + + return 0; + +badarg: + PyErr_SetString(PyExc_TypeError, "argument must be sequence of length 2"); + return -1; + +badval: + PyErr_SetString(PyExc_TypeError, "an integer is required"); + return -1; +} + +static PyObject * +_getpixel(ImagingObject *self, PyObject *args) { + PyObject *xy; + int x, y; + + if (PyTuple_GET_SIZE(args) != 1) { + PyErr_SetString(PyExc_TypeError, "argument 1 must be sequence of length 2"); + return NULL; + } + + xy = PyTuple_GET_ITEM(args, 0); + + if (_getxy(xy, &x, &y)) { + return NULL; + } + + if (self->access == NULL) { + Py_INCREF(Py_None); + return Py_None; + } + + return getpixel(self->image, self->access, x, y); +} + +union hist_extrema { + UINT8 u[2]; + INT32 i[2]; + FLOAT32 f[2]; +}; + +static union hist_extrema * +parse_histogram_extremap( + ImagingObject *self, PyObject *extremap, union hist_extrema *ep) { + int i0, i1; + double f0, f1; + + if (extremap) { + switch (self->image->type) { + case IMAGING_TYPE_UINT8: + if (!PyArg_ParseTuple(extremap, "ii", &i0, &i1)) { + return NULL; + } + ep->u[0] = CLIP8(i0); + ep->u[1] = CLIP8(i1); + break; + case IMAGING_TYPE_INT32: + if (!PyArg_ParseTuple(extremap, "ii", &i0, &i1)) { + return NULL; + } + ep->i[0] = i0; + ep->i[1] = i1; + break; + case IMAGING_TYPE_FLOAT32: + if (!PyArg_ParseTuple(extremap, "dd", &f0, &f1)) { + return NULL; + } + ep->f[0] = (FLOAT32)f0; + ep->f[1] = (FLOAT32)f1; + break; + default: + return NULL; + } + } else { + return NULL; + } + return ep; +} + +static PyObject * +_histogram(ImagingObject *self, PyObject *args) { + ImagingHistogram h; + PyObject *list; + int i; + union hist_extrema extrema; + union hist_extrema *ep; + + PyObject *extremap = NULL; + ImagingObject *maskp = NULL; + if (!PyArg_ParseTuple(args, "|OO!", &extremap, &Imaging_Type, &maskp)) { + return NULL; + } + + /* Using a var to avoid allocations. */ + ep = parse_histogram_extremap(self, extremap, &extrema); + h = ImagingGetHistogram(self->image, (maskp) ? maskp->image : NULL, ep); + + if (!h) { + return NULL; + } + + /* Build an integer list containing the histogram */ + list = PyList_New(h->bands * 256); + if (list == NULL) { + ImagingHistogramDelete(h); + return NULL; + } + for (i = 0; i < h->bands * 256; i++) { + PyObject *item; + item = PyLong_FromLong(h->histogram[i]); + if (item == NULL) { + Py_DECREF(list); + list = NULL; + break; + } + PyList_SetItem(list, i, item); + } + + /* Destroy the histogram structure */ + ImagingHistogramDelete(h); + + return list; +} + +static PyObject * +_entropy(ImagingObject *self, PyObject *args) { + ImagingHistogram h; + int idx, length; + long sum; + double entropy, fsum, p; + union hist_extrema extrema; + union hist_extrema *ep; + + PyObject *extremap = NULL; + ImagingObject *maskp = NULL; + if (!PyArg_ParseTuple(args, "|OO!", &extremap, &Imaging_Type, &maskp)) { + return NULL; + } + + /* Using a local var to avoid allocations. */ + ep = parse_histogram_extremap(self, extremap, &extrema); + h = ImagingGetHistogram(self->image, (maskp) ? maskp->image : NULL, ep); + + if (!h) { + return NULL; + } + + /* Calculate the histogram entropy */ + /* First, sum the histogram data */ + length = h->bands * 256; + sum = 0; + for (idx = 0; idx < length; idx++) { + sum += h->histogram[idx]; + } + + /* Next, normalize the histogram data, */ + /* using the histogram sum value */ + fsum = (double)sum; + entropy = 0.0; + for (idx = 0; idx < length; idx++) { + p = (double)h->histogram[idx] / fsum; + if (p != 0.0) { + entropy += p * log(p) * M_LOG2E; + } + } + + /* Destroy the histogram structure */ + ImagingHistogramDelete(h); + + return PyFloat_FromDouble(-entropy); +} + +#ifdef WITH_MODEFILTER +static PyObject * +_modefilter(ImagingObject *self, PyObject *args) { + int size; + if (!PyArg_ParseTuple(args, "i", &size)) { + return NULL; + } + + return PyImagingNew(ImagingModeFilter(self->image, size)); +} +#endif + +static PyObject * +_offset(ImagingObject *self, PyObject *args) { + int xoffset, yoffset; + if (!PyArg_ParseTuple(args, "ii", &xoffset, &yoffset)) { + return NULL; + } + + return PyImagingNew(ImagingOffset(self->image, xoffset, yoffset)); +} + +static PyObject * +_paste(ImagingObject *self, PyObject *args) { + int status; + char ink[4]; + + PyObject *source; + int x0, y0, x1, y1; + ImagingObject *maskp = NULL; + if (!PyArg_ParseTuple( + args, "O(iiii)|O!", &source, &x0, &y0, &x1, &y1, &Imaging_Type, &maskp)) { + return NULL; + } + + if (PyImaging_Check(source)) { + status = ImagingPaste( + self->image, + PyImaging_AsImaging(source), + (maskp) ? maskp->image : NULL, + x0, + y0, + x1, + y1); + + } else { + if (!getink(source, self->image, ink)) { + return NULL; + } + status = ImagingFill2( + self->image, ink, (maskp) ? maskp->image : NULL, x0, y0, x1, y1); + } + + if (status < 0) { + return NULL; + } + + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject * +_point(ImagingObject *self, PyObject *args) { + static const char *wrong_number = "wrong number of lut entries"; + + Py_ssize_t n; + int i, bands; + Imaging im; + + PyObject *list; + char *mode; + if (!PyArg_ParseTuple(args, "Oz", &list, &mode)) { + return NULL; + } + + if (mode && !strcmp(mode, "F")) { + FLOAT32 *data; + + /* map from 8-bit data to floating point */ + n = 256; + data = getlist(list, &n, wrong_number, TYPE_FLOAT32); + if (!data) { + return NULL; + } + im = ImagingPoint(self->image, mode, (void *)data); + free(data); + + } else if (!strcmp(self->image->mode, "I") && mode && !strcmp(mode, "L")) { + UINT8 *data; + + /* map from 16-bit subset of 32-bit data to 8-bit */ + /* FIXME: support arbitrary number of entries (requires API change) */ + n = 65536; + data = getlist(list, &n, wrong_number, TYPE_UINT8); + if (!data) { + return NULL; + } + im = ImagingPoint(self->image, mode, (void *)data); + free(data); + + } else { + INT32 *data; + UINT8 lut[1024]; + + if (mode) { + bands = getbands(mode); + if (bands < 0) { + return NULL; + } + } else { + bands = self->image->bands; + } + + /* map to integer data */ + n = 256 * bands; + data = getlist(list, &n, wrong_number, TYPE_INT32); + if (!data) { + return NULL; + } + + if (mode && !strcmp(mode, "I")) { + im = ImagingPoint(self->image, mode, (void *)data); + } else if (mode && bands > 1) { + for (i = 0; i < 256; i++) { + lut[i * 4] = CLIP8(data[i]); + lut[i * 4 + 1] = CLIP8(data[i + 256]); + lut[i * 4 + 2] = CLIP8(data[i + 512]); + if (n > 768) { + lut[i * 4 + 3] = CLIP8(data[i + 768]); + } + } + im = ImagingPoint(self->image, mode, (void *)lut); + } else { + /* map individual bands */ + for (i = 0; i < n; i++) { + lut[i] = CLIP8(data[i]); + } + im = ImagingPoint(self->image, mode, (void *)lut); + } + free(data); + } + + return PyImagingNew(im); +} + +static PyObject * +_point_transform(ImagingObject *self, PyObject *args) { + double scale = 1.0; + double offset = 0.0; + if (!PyArg_ParseTuple(args, "|dd", &scale, &offset)) { + return NULL; + } + + return PyImagingNew(ImagingPointTransform(self->image, scale, offset)); +} + +static PyObject * +_putdata(ImagingObject *self, PyObject *args) { + Imaging image; + // i & n are # pixels, require py_ssize_t. x can be as large as n. y, just because. + Py_ssize_t n, i, x, y; + + PyObject *data; + PyObject *seq = NULL; + PyObject *op; + double scale = 1.0; + double offset = 0.0; + + if (!PyArg_ParseTuple(args, "O|dd", &data, &scale, &offset)) { + return NULL; + } + + if (!PySequence_Check(data)) { + PyErr_SetString(PyExc_TypeError, must_be_sequence); + return NULL; + } + + image = self->image; + + n = PyObject_Length(data); + if (n > (Py_ssize_t)image->xsize * (Py_ssize_t)image->ysize) { + PyErr_SetString(PyExc_TypeError, "too many data entries"); + return NULL; + } + +#define set_value_to_item(seq, i) \ + op = PySequence_Fast_GET_ITEM(seq, i); \ + if (PySequence_Check(op)) { \ + PyErr_SetString(PyExc_TypeError, "sequence must be flattened"); \ + return NULL; \ + } else { \ + value = PyFloat_AsDouble(op); \ + } + if (image->image8) { + if (PyBytes_Check(data)) { + unsigned char *p; + p = (unsigned char *)PyBytes_AS_STRING(data); + if (scale == 1.0 && offset == 0.0) { + /* Plain string data */ + for (i = y = 0; i < n; i += image->xsize, y++) { + x = n - i; + if (x > (int)image->xsize) { + x = image->xsize; + } + memcpy(image->image8[y], p + i, x); + } + } else { + /* Scaled and clipped string data */ + for (i = x = y = 0; i < n; i++) { + image->image8[y][x] = CLIP8((int)(p[i] * scale + offset)); + if (++x >= (int)image->xsize) { + x = 0, y++; + } + } + } + } else { + seq = PySequence_Fast(data, must_be_sequence); + if (!seq) { + PyErr_SetString(PyExc_TypeError, must_be_sequence); + return NULL; + } + double value; + if (image->bands == 1) { + int bigendian = 0; + if (image->type == IMAGING_TYPE_SPECIAL) { + // I;16* + if (strcmp(image->mode, "I;16N") == 0) { +#ifdef WORDS_BIGENDIAN + bigendian = 1; +#else + bigendian = 0; +#endif + } else if (strcmp(image->mode, "I;16B") == 0) { + bigendian = 1; + } else { + bigendian = 0; + } + } + for (i = x = y = 0; i < n; i++) { + set_value_to_item(seq, i); + if (scale != 1.0 || offset != 0.0) { + value = value * scale + offset; + } + if (image->type == IMAGING_TYPE_SPECIAL) { + image->image8[y][x * 2 + (bigendian ? 1 : 0)] = + CLIP8((int)value % 256); + image->image8[y][x * 2 + (bigendian ? 0 : 1)] = + CLIP8((int)value >> 8); + } else { + image->image8[y][x] = (UINT8)CLIP8(value); + } + if (++x >= (int)image->xsize) { + x = 0, y++; + } + } + } else { + // BGR;* + int b; + for (i = x = y = 0; i < n; i++) { + char ink[4]; + + op = PySequence_Fast_GET_ITEM(seq, i); + if (!op || !getink(op, image, ink)) { + Py_DECREF(seq); + return NULL; + } + /* FIXME: what about scale and offset? */ + for (b = 0; b < image->pixelsize; b++) { + image->image8[y][x * image->pixelsize + b] = ink[b]; + } + if (++x >= (int)image->xsize) { + x = 0, y++; + } + } + } + PyErr_Clear(); /* Avoid weird exceptions */ + } + } else { + /* 32-bit images */ + seq = PySequence_Fast(data, must_be_sequence); + if (!seq) { + PyErr_SetString(PyExc_TypeError, must_be_sequence); + return NULL; + } + switch (image->type) { + case IMAGING_TYPE_INT32: + for (i = x = y = 0; i < n; i++) { + double value; + set_value_to_item(seq, i); + IMAGING_PIXEL_INT32(image, x, y) = (INT32)(value * scale + offset); + if (++x >= (int)image->xsize) { + x = 0, y++; + } + } + PyErr_Clear(); /* Avoid weird exceptions */ + break; + case IMAGING_TYPE_FLOAT32: + for (i = x = y = 0; i < n; i++) { + double value; + set_value_to_item(seq, i); + IMAGING_PIXEL_FLOAT32(image, x, y) = + (FLOAT32)(value * scale + offset); + if (++x >= (int)image->xsize) { + x = 0, y++; + } + } + PyErr_Clear(); /* Avoid weird exceptions */ + break; + default: + for (i = x = y = 0; i < n; i++) { + union { + char ink[4]; + INT32 inkint; + } u; + + u.inkint = 0; + + op = PySequence_Fast_GET_ITEM(seq, i); + if (!op || !getink(op, image, u.ink)) { + Py_DECREF(seq); + return NULL; + } + /* FIXME: what about scale and offset? */ + image->image32[y][x] = u.inkint; + if (++x >= (int)image->xsize) { + x = 0, y++; + } + } + PyErr_Clear(); /* Avoid weird exceptions */ + break; + } + } + + Py_XDECREF(seq); + + Py_INCREF(Py_None); + return Py_None; +} + +#ifdef WITH_QUANTIZE + +static PyObject * +_quantize(ImagingObject *self, PyObject *args) { + int colours = 256; + int method = 0; + int kmeans = 0; + if (!PyArg_ParseTuple(args, "|iii", &colours, &method, &kmeans)) { + return NULL; + } + + if (!self->image->xsize || !self->image->ysize) { + /* no content; return an empty image */ + return PyImagingNew(ImagingNew("P", self->image->xsize, self->image->ysize)); + } + + return PyImagingNew(ImagingQuantize(self->image, colours, method, kmeans)); +} +#endif + +static PyObject * +_putpalette(ImagingObject *self, PyObject *args) { + ImagingShuffler unpack; + int bits; + + char *rawmode, *palette_mode; + UINT8 *palette; + Py_ssize_t palettesize; + if (!PyArg_ParseTuple(args, "sy#", &rawmode, &palette, &palettesize)) { + return NULL; + } + + if (strcmp(self->image->mode, "L") && strcmp(self->image->mode, "LA") && + strcmp(self->image->mode, "P") && strcmp(self->image->mode, "PA")) { + PyErr_SetString(PyExc_ValueError, wrong_mode); + return NULL; + } + + palette_mode = strncmp("RGBA", rawmode, 4) == 0 ? "RGBA" : "RGB"; + unpack = ImagingFindUnpacker(palette_mode, rawmode, &bits); + if (!unpack) { + PyErr_SetString(PyExc_ValueError, wrong_raw_mode); + return NULL; + } + + if (palettesize * 8 / bits > 256) { + PyErr_SetString(PyExc_ValueError, wrong_palette_size); + return NULL; + } + + ImagingPaletteDelete(self->image->palette); + + strcpy(self->image->mode, strlen(self->image->mode) == 2 ? "PA" : "P"); + + self->image->palette = ImagingPaletteNew(palette_mode); + + self->image->palette->size = palettesize * 8 / bits; + unpack(self->image->palette->palette, palette, self->image->palette->size); + + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject * +_putpalettealpha(ImagingObject *self, PyObject *args) { + int index; + int alpha = 0; + if (!PyArg_ParseTuple(args, "i|i", &index, &alpha)) { + return NULL; + } + + if (!self->image->palette) { + PyErr_SetString(PyExc_ValueError, no_palette); + return NULL; + } + + if (index < 0 || index >= 256) { + PyErr_SetString(PyExc_ValueError, outside_palette); + return NULL; + } + + strcpy(self->image->palette->mode, "RGBA"); + self->image->palette->palette[index * 4 + 3] = (UINT8)alpha; + + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject * +_putpalettealphas(ImagingObject *self, PyObject *args) { + int i; + UINT8 *values; + Py_ssize_t length; + if (!PyArg_ParseTuple(args, "y#", &values, &length)) { + return NULL; + } + + if (!self->image->palette) { + PyErr_SetString(PyExc_ValueError, no_palette); + return NULL; + } + + if (length > 256) { + PyErr_SetString(PyExc_ValueError, outside_palette); + return NULL; + } + + strcpy(self->image->palette->mode, "RGBA"); + for (i = 0; i < length; i++) { + self->image->palette->palette[i * 4 + 3] = (UINT8)values[i]; + } + + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject * +_putpixel(ImagingObject *self, PyObject *args) { + Imaging im; + char ink[4]; + + int x, y; + PyObject *color; + if (!PyArg_ParseTuple(args, "(ii)O", &x, &y, &color)) { + return NULL; + } + + im = self->image; + + if (x < 0) { + x = im->xsize + x; + } + if (y < 0) { + y = im->ysize + y; + } + + if (x < 0 || x >= im->xsize || y < 0 || y >= im->ysize) { + PyErr_SetString(PyExc_IndexError, outside_image); + return NULL; + } + + if (!getink(color, im, ink)) { + return NULL; + } + + if (self->access) { + self->access->put_pixel(im, x, y, ink); + } + + Py_INCREF(Py_None); + return Py_None; +} + +#ifdef WITH_RANKFILTER +static PyObject * +_rankfilter(ImagingObject *self, PyObject *args) { + int size, rank; + if (!PyArg_ParseTuple(args, "ii", &size, &rank)) { + return NULL; + } + + return PyImagingNew(ImagingRankFilter(self->image, size, rank)); +} +#endif + +static PyObject * +_resize(ImagingObject *self, PyObject *args) { + Imaging imIn; + Imaging imOut; + + int xsize, ysize; + int filter = IMAGING_TRANSFORM_NEAREST; + float box[4] = {0, 0, 0, 0}; + + imIn = self->image; + box[2] = imIn->xsize; + box[3] = imIn->ysize; + + if (!PyArg_ParseTuple( + args, + "(ii)|i(ffff)", + &xsize, + &ysize, + &filter, + &box[0], + &box[1], + &box[2], + &box[3])) { + return NULL; + } + + if (xsize < 1 || ysize < 1) { + return ImagingError_ValueError("height and width must be > 0"); + } + + if (box[0] < 0 || box[1] < 0) { + return ImagingError_ValueError("box offset can't be negative"); + } + + if (box[2] > imIn->xsize || box[3] > imIn->ysize) { + return ImagingError_ValueError("box can't exceed original image size"); + } + + if (box[2] - box[0] < 0 || box[3] - box[1] < 0) { + return ImagingError_ValueError("box can't be empty"); + } + + // If box's coordinates are int and box size matches requested size + if (box[0] - (int)box[0] == 0 && box[2] - box[0] == xsize && + box[1] - (int)box[1] == 0 && box[3] - box[1] == ysize) { + imOut = ImagingCrop(imIn, box[0], box[1], box[2], box[3]); + } else if (filter == IMAGING_TRANSFORM_NEAREST) { + double a[8]; + + memset(a, 0, sizeof a); + a[0] = (double)(box[2] - box[0]) / xsize; + a[4] = (double)(box[3] - box[1]) / ysize; + a[2] = box[0]; + a[5] = box[1]; + + imOut = ImagingNewDirty(imIn->mode, xsize, ysize); + + imOut = ImagingTransform( + imOut, imIn, IMAGING_TRANSFORM_AFFINE, 0, 0, xsize, ysize, a, filter, 1); + } else { + imOut = ImagingResample(imIn, xsize, ysize, filter, box); + } + + return PyImagingNew(imOut); +} + +static PyObject * +_reduce(ImagingObject *self, PyObject *args) { + Imaging imIn; + Imaging imOut; + + int xscale, yscale; + int box[4] = {0, 0, 0, 0}; + + imIn = self->image; + box[2] = imIn->xsize; + box[3] = imIn->ysize; + + if (!PyArg_ParseTuple( + args, + "(ii)|(iiii)", + &xscale, + &yscale, + &box[0], + &box[1], + &box[2], + &box[3])) { + return NULL; + } + + if (xscale < 1 || yscale < 1) { + return ImagingError_ValueError("scale must be > 0"); + } + + if (box[0] < 0 || box[1] < 0) { + return ImagingError_ValueError("box offset can't be negative"); + } + + if (box[2] > imIn->xsize || box[3] > imIn->ysize) { + return ImagingError_ValueError("box can't exceed original image size"); + } + + if (box[2] <= box[0] || box[3] <= box[1]) { + return ImagingError_ValueError("box can't be empty"); + } + + if (xscale == 1 && yscale == 1) { + imOut = ImagingCrop(imIn, box[0], box[1], box[2], box[3]); + } else { + // Change box format: (left, top, width, height) + box[2] -= box[0]; + box[3] -= box[1]; + imOut = ImagingReduce(imIn, xscale, yscale, box); + } + + return PyImagingNew(imOut); +} + +#define IS_RGB(mode) \ + (!strcmp(mode, "RGB") || !strcmp(mode, "RGBA") || !strcmp(mode, "RGBX")) + +static PyObject * +im_setmode(ImagingObject *self, PyObject *args) { + /* attempt to modify the mode of an image in place */ + + Imaging im; + + char *mode; + Py_ssize_t modelen; + if (!PyArg_ParseTuple(args, "s#:setmode", &mode, &modelen)) { + return NULL; + } + + im = self->image; + + /* move all logic in here to the libImaging primitive */ + + if (!strcmp(im->mode, mode)) { + ; /* same mode; always succeeds */ + } else if (IS_RGB(im->mode) && IS_RGB(mode)) { + /* color to color */ + strcpy(im->mode, mode); + im->bands = modelen; + if (!strcmp(mode, "RGBA")) { + (void)ImagingFillBand(im, 3, 255); + } + } else { + /* trying doing an in-place conversion */ + if (!ImagingConvertInPlace(im, mode)) { + return NULL; + } + } + + if (self->access) { + ImagingAccessDelete(im, self->access); + } + self->access = ImagingAccessNew(im); + + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject * +_transform(ImagingObject *self, PyObject *args) { + static const char *wrong_number = "wrong number of matrix entries"; + + Imaging imOut; + Py_ssize_t n; + double *a; + + ImagingObject *imagep; + int x0, y0, x1, y1; + int method; + PyObject *data; + int filter = IMAGING_TRANSFORM_NEAREST; + int fill = 1; + if (!PyArg_ParseTuple( + args, + "(iiii)O!iO|ii", + &x0, + &y0, + &x1, + &y1, + &Imaging_Type, + &imagep, + &method, + &data, + &filter, + &fill)) { + return NULL; + } + + switch (method) { + case IMAGING_TRANSFORM_AFFINE: + n = 6; + break; + case IMAGING_TRANSFORM_PERSPECTIVE: + n = 8; + break; + case IMAGING_TRANSFORM_QUAD: + n = 8; + break; + default: + n = -1; /* force error */ + } + + a = getlist(data, &n, wrong_number, TYPE_DOUBLE); + if (!a) { + return NULL; + } + + imOut = ImagingTransform( + self->image, imagep->image, method, x0, y0, x1, y1, a, filter, fill); + + free(a); + + if (!imOut) { + return NULL; + } + + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject * +_transpose(ImagingObject *self, PyObject *args) { + Imaging imIn; + Imaging imOut; + + int op; + if (!PyArg_ParseTuple(args, "i", &op)) { + return NULL; + } + + imIn = self->image; + + switch (op) { + case 0: /* flip left right */ + case 1: /* flip top bottom */ + case 3: /* rotate 180 */ + imOut = ImagingNewDirty(imIn->mode, imIn->xsize, imIn->ysize); + break; + case 2: /* rotate 90 */ + case 4: /* rotate 270 */ + case 5: /* transpose */ + case 6: /* transverse */ + imOut = ImagingNewDirty(imIn->mode, imIn->ysize, imIn->xsize); + break; + default: + PyErr_SetString(PyExc_ValueError, "No such transpose operation"); + return NULL; + } + + if (imOut) { + switch (op) { + case 0: + (void)ImagingFlipLeftRight(imOut, imIn); + break; + case 1: + (void)ImagingFlipTopBottom(imOut, imIn); + break; + case 2: + (void)ImagingRotate90(imOut, imIn); + break; + case 3: + (void)ImagingRotate180(imOut, imIn); + break; + case 4: + (void)ImagingRotate270(imOut, imIn); + break; + case 5: + (void)ImagingTranspose(imOut, imIn); + break; + case 6: + (void)ImagingTransverse(imOut, imIn); + break; + } + } + + return PyImagingNew(imOut); +} + +#ifdef WITH_UNSHARPMASK +static PyObject * +_unsharp_mask(ImagingObject *self, PyObject *args) { + Imaging imIn; + Imaging imOut; + + float radius; + int percent, threshold; + if (!PyArg_ParseTuple(args, "fii", &radius, &percent, &threshold)) { + return NULL; + } + + imIn = self->image; + imOut = ImagingNewDirty(imIn->mode, imIn->xsize, imIn->ysize); + if (!imOut) { + return NULL; + } + + if (!ImagingUnsharpMask(imOut, imIn, radius, percent, threshold)) { + return NULL; + } + + return PyImagingNew(imOut); +} +#endif + +static PyObject * +_box_blur(ImagingObject *self, PyObject *args) { + Imaging imIn; + Imaging imOut; + + float xradius, yradius; + int n = 1; + if (!PyArg_ParseTuple(args, "(ff)|i", &xradius, &yradius, &n)) { + return NULL; + } + + imIn = self->image; + imOut = ImagingNewDirty(imIn->mode, imIn->xsize, imIn->ysize); + if (!imOut) { + return NULL; + } + + if (!ImagingBoxBlur(imOut, imIn, xradius, yradius, n)) { + ImagingDelete(imOut); + return NULL; + } + + return PyImagingNew(imOut); +} + +/* -------------------------------------------------------------------- */ + +static PyObject * +_isblock(ImagingObject *self) { + return PyBool_FromLong(self->image->block != NULL); +} + +static PyObject * +_getbbox(ImagingObject *self, PyObject *args) { + int bbox[4]; + + int alpha_only = 1; + if (!PyArg_ParseTuple(args, "|i", &alpha_only)) { + return NULL; + } + + if (!ImagingGetBBox(self->image, bbox, alpha_only)) { + Py_INCREF(Py_None); + return Py_None; + } + + return Py_BuildValue("iiii", bbox[0], bbox[1], bbox[2], bbox[3]); +} + +static PyObject * +_getcolors(ImagingObject *self, PyObject *args) { + ImagingColorItem *items; + int i, colors; + PyObject *out; + + int maxcolors = 256; + if (!PyArg_ParseTuple(args, "i:getcolors", &maxcolors)) { + return NULL; + } + + items = ImagingGetColors(self->image, maxcolors, &colors); + if (!items) { + return NULL; + } + + if (colors > maxcolors) { + out = Py_None; + Py_INCREF(out); + } else { + out = PyList_New(colors); + if (out == NULL) { + free(items); + return NULL; + } + for (i = 0; i < colors; i++) { + ImagingColorItem *v = &items[i]; + PyObject *item = Py_BuildValue( + "iN", v->count, getpixel(self->image, self->access, v->x, v->y)); + PyList_SetItem(out, i, item); + } + } + + free(items); + + return out; +} + +static PyObject * +_getextrema(ImagingObject *self) { + union { + UINT8 u[2]; + INT32 i[2]; + FLOAT32 f[2]; + UINT16 s[2]; + } extrema; + int status; + + status = ImagingGetExtrema(self->image, &extrema); + if (status < 0) { + return NULL; + } + + if (status) { + switch (self->image->type) { + case IMAGING_TYPE_UINT8: + return Py_BuildValue("BB", extrema.u[0], extrema.u[1]); + case IMAGING_TYPE_INT32: + return Py_BuildValue("ii", extrema.i[0], extrema.i[1]); + case IMAGING_TYPE_FLOAT32: + return Py_BuildValue("dd", extrema.f[0], extrema.f[1]); + case IMAGING_TYPE_SPECIAL: + if (strcmp(self->image->mode, "I;16") == 0) { + return Py_BuildValue("HH", extrema.s[0], extrema.s[1]); + } + } + } + + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject * +_getprojection(ImagingObject *self) { + unsigned char *xprofile; + unsigned char *yprofile; + PyObject *result; + + /* malloc check ok */ + xprofile = malloc(self->image->xsize); + yprofile = malloc(self->image->ysize); + + if (xprofile == NULL || yprofile == NULL) { + free(xprofile); + free(yprofile); + return ImagingError_MemoryError(); + } + + ImagingGetProjection( + self->image, (unsigned char *)xprofile, (unsigned char *)yprofile); + + result = Py_BuildValue( + "y#y#", + xprofile, + (Py_ssize_t)self->image->xsize, + yprofile, + (Py_ssize_t)self->image->ysize); + + free(xprofile); + free(yprofile); + + return result; +} + +/* -------------------------------------------------------------------- */ + +static PyObject * +_getband(ImagingObject *self, PyObject *args) { + int band; + + if (!PyArg_ParseTuple(args, "i", &band)) { + return NULL; + } + + return PyImagingNew(ImagingGetBand(self->image, band)); +} + +static PyObject * +_fillband(ImagingObject *self, PyObject *args) { + int band; + int color; + + if (!PyArg_ParseTuple(args, "ii", &band, &color)) { + return NULL; + } + + if (!ImagingFillBand(self->image, band, color)) { + return NULL; + } + + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject * +_putband(ImagingObject *self, PyObject *args) { + ImagingObject *imagep; + int band; + if (!PyArg_ParseTuple(args, "O!i", &Imaging_Type, &imagep, &band)) { + return NULL; + } + + if (!ImagingPutBand(self->image, imagep->image, band)) { + return NULL; + } + + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject * +_merge(PyObject *self, PyObject *args) { + char *mode; + ImagingObject *band0 = NULL; + ImagingObject *band1 = NULL; + ImagingObject *band2 = NULL; + ImagingObject *band3 = NULL; + Imaging bands[4] = {NULL, NULL, NULL, NULL}; + + if (!PyArg_ParseTuple( + args, + "sO!|O!O!O!", + &mode, + &Imaging_Type, + &band0, + &Imaging_Type, + &band1, + &Imaging_Type, + &band2, + &Imaging_Type, + &band3)) { + return NULL; + } + + if (band0) { + bands[0] = band0->image; + } + if (band1) { + bands[1] = band1->image; + } + if (band2) { + bands[2] = band2->image; + } + if (band3) { + bands[3] = band3->image; + } + + return PyImagingNew(ImagingMerge(mode, bands)); +} + +static PyObject * +_split(ImagingObject *self) { + int fails = 0; + Py_ssize_t i; + PyObject *list; + PyObject *imaging_object; + Imaging bands[4] = {NULL, NULL, NULL, NULL}; + + if (!ImagingSplit(self->image, bands)) { + return NULL; + } + + list = PyTuple_New(self->image->bands); + for (i = 0; i < self->image->bands; i++) { + imaging_object = PyImagingNew(bands[i]); + if (!imaging_object) { + fails += 1; + } + PyTuple_SET_ITEM(list, i, imaging_object); + } + if (fails) { + Py_DECREF(list); + list = NULL; + } + return list; +} + +/* -------------------------------------------------------------------- */ + +#ifdef WITH_IMAGECHOPS + +static PyObject * +_chop_invert(ImagingObject *self) { + return PyImagingNew(ImagingNegative(self->image)); +} + +static PyObject * +_chop_lighter(ImagingObject *self, PyObject *args) { + ImagingObject *imagep; + + if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) { + return NULL; + } + + return PyImagingNew(ImagingChopLighter(self->image, imagep->image)); +} + +static PyObject * +_chop_darker(ImagingObject *self, PyObject *args) { + ImagingObject *imagep; + + if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) { + return NULL; + } + + return PyImagingNew(ImagingChopDarker(self->image, imagep->image)); +} + +static PyObject * +_chop_difference(ImagingObject *self, PyObject *args) { + ImagingObject *imagep; + + if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) { + return NULL; + } + + return PyImagingNew(ImagingChopDifference(self->image, imagep->image)); +} + +static PyObject * +_chop_multiply(ImagingObject *self, PyObject *args) { + ImagingObject *imagep; + + if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) { + return NULL; + } + + return PyImagingNew(ImagingChopMultiply(self->image, imagep->image)); +} + +static PyObject * +_chop_screen(ImagingObject *self, PyObject *args) { + ImagingObject *imagep; + + if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) { + return NULL; + } + + return PyImagingNew(ImagingChopScreen(self->image, imagep->image)); +} + +static PyObject * +_chop_add(ImagingObject *self, PyObject *args) { + ImagingObject *imagep; + float scale; + int offset; + + scale = 1.0; + offset = 0; + + if (!PyArg_ParseTuple(args, "O!|fi", &Imaging_Type, &imagep, &scale, &offset)) { + return NULL; + } + + return PyImagingNew(ImagingChopAdd(self->image, imagep->image, scale, offset)); +} + +static PyObject * +_chop_subtract(ImagingObject *self, PyObject *args) { + ImagingObject *imagep; + float scale; + int offset; + + scale = 1.0; + offset = 0; + + if (!PyArg_ParseTuple(args, "O!|fi", &Imaging_Type, &imagep, &scale, &offset)) { + return NULL; + } + + return PyImagingNew(ImagingChopSubtract(self->image, imagep->image, scale, offset)); +} + +static PyObject * +_chop_and(ImagingObject *self, PyObject *args) { + ImagingObject *imagep; + + if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) { + return NULL; + } + + return PyImagingNew(ImagingChopAnd(self->image, imagep->image)); +} + +static PyObject * +_chop_or(ImagingObject *self, PyObject *args) { + ImagingObject *imagep; + + if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) { + return NULL; + } + + return PyImagingNew(ImagingChopOr(self->image, imagep->image)); +} + +static PyObject * +_chop_xor(ImagingObject *self, PyObject *args) { + ImagingObject *imagep; + + if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) { + return NULL; + } + + return PyImagingNew(ImagingChopXor(self->image, imagep->image)); +} + +static PyObject * +_chop_add_modulo(ImagingObject *self, PyObject *args) { + ImagingObject *imagep; + + if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) { + return NULL; + } + + return PyImagingNew(ImagingChopAddModulo(self->image, imagep->image)); +} + +static PyObject * +_chop_subtract_modulo(ImagingObject *self, PyObject *args) { + ImagingObject *imagep; + + if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) { + return NULL; + } + + return PyImagingNew(ImagingChopSubtractModulo(self->image, imagep->image)); +} + +static PyObject * +_chop_soft_light(ImagingObject *self, PyObject *args) { + ImagingObject *imagep; + + if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) { + return NULL; + } + + return PyImagingNew(ImagingChopSoftLight(self->image, imagep->image)); +} + +static PyObject * +_chop_hard_light(ImagingObject *self, PyObject *args) { + ImagingObject *imagep; + + if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) { + return NULL; + } + + return PyImagingNew(ImagingChopHardLight(self->image, imagep->image)); +} + +static PyObject * +_chop_overlay(ImagingObject *self, PyObject *args) { + ImagingObject *imagep; + + if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) { + return NULL; + } + + return PyImagingNew(ImagingOverlay(self->image, imagep->image)); +} +#endif + +/* -------------------------------------------------------------------- */ + +#ifdef WITH_IMAGEDRAW + +static PyObject * +_font_new(PyObject *self_, PyObject *args) { + ImagingFontObject *self; + int i, y0, y1; + static const char *wrong_length = "descriptor table has wrong size"; + + ImagingObject *imagep; + unsigned char *glyphdata; + Py_ssize_t glyphdata_length; + if (!PyArg_ParseTuple( + args, "O!y#", &Imaging_Type, &imagep, &glyphdata, &glyphdata_length)) { + return NULL; + } + + if (glyphdata_length != 256 * 20) { + PyErr_SetString(PyExc_ValueError, wrong_length); + return NULL; + } + + self = PyObject_New(ImagingFontObject, &ImagingFont_Type); + if (self == NULL) { + return NULL; + } + + /* glyph bitmap */ + self->bitmap = imagep->image; + + y0 = y1 = 0; + + /* glyph glyphs */ + for (i = 0; i < 256; i++) { + self->glyphs[i].dx = S16(B16(glyphdata, 0)); + self->glyphs[i].dy = S16(B16(glyphdata, 2)); + self->glyphs[i].dx0 = S16(B16(glyphdata, 4)); + self->glyphs[i].dy0 = S16(B16(glyphdata, 6)); + self->glyphs[i].dx1 = S16(B16(glyphdata, 8)); + self->glyphs[i].dy1 = S16(B16(glyphdata, 10)); + self->glyphs[i].sx0 = S16(B16(glyphdata, 12)); + self->glyphs[i].sy0 = S16(B16(glyphdata, 14)); + self->glyphs[i].sx1 = S16(B16(glyphdata, 16)); + self->glyphs[i].sy1 = S16(B16(glyphdata, 18)); + + // Do not allow glyphs to extend beyond bitmap image + // Helps prevent DOS by stopping cropped images being larger than the original + if (self->glyphs[i].sx0 < 0) { + self->glyphs[i].dx0 -= self->glyphs[i].sx0; + self->glyphs[i].sx0 = 0; + } + if (self->glyphs[i].sy0 < 0) { + self->glyphs[i].dy0 -= self->glyphs[i].sy0; + self->glyphs[i].sy0 = 0; + } + if (self->glyphs[i].sx1 > self->bitmap->xsize) { + self->glyphs[i].dx1 -= self->glyphs[i].sx1 - self->bitmap->xsize; + self->glyphs[i].sx1 = self->bitmap->xsize; + } + if (self->glyphs[i].sy1 > self->bitmap->ysize) { + self->glyphs[i].dy1 -= self->glyphs[i].sy1 - self->bitmap->ysize; + self->glyphs[i].sy1 = self->bitmap->ysize; + } + + if (self->glyphs[i].dy0 < y0) { + y0 = self->glyphs[i].dy0; + } + if (self->glyphs[i].dy1 > y1) { + y1 = self->glyphs[i].dy1; + } + glyphdata += 20; + } + + self->baseline = -y0; + self->ysize = y1 - y0; + + /* keep a reference to the bitmap object */ + Py_INCREF(imagep); + self->ref = imagep; + + return (PyObject *)self; +} + +static void +_font_dealloc(ImagingFontObject *self) { + Py_XDECREF(self->ref); + PyObject_Del(self); +} + +static inline int +textwidth(ImagingFontObject *self, const unsigned char *text) { + int xsize; + + for (xsize = 0; *text; text++) { + xsize += self->glyphs[*text].dx; + } + + return xsize; +} + +void +_font_text_asBytes(PyObject *encoded_string, unsigned char **text) { + /* Allocates *text, returns a 'new reference'. Caller is required to free */ + + PyObject *bytes = NULL; + Py_ssize_t len = 0; + char *buffer; + + *text = NULL; + + if (PyUnicode_CheckExact(encoded_string)) { + bytes = PyUnicode_AsLatin1String(encoded_string); + if (!bytes) { + return; + } + PyBytes_AsStringAndSize(bytes, &buffer, &len); + } else if (PyBytes_Check(encoded_string)) { + PyBytes_AsStringAndSize(encoded_string, &buffer, &len); + } + + *text = calloc(len + 1, 1); + if (*text) { + memcpy(*text, buffer, len); + } else { + ImagingError_MemoryError(); + } + if (bytes) { + Py_DECREF(bytes); + } + + return; +} + +static PyObject * +_font_getmask(ImagingFontObject *self, PyObject *args) { + Imaging im; + Imaging bitmap = NULL; + int x, b; + int i = 0; + int status; + Glyph *glyph; + + PyObject *encoded_string; + + unsigned char *text; + char *mode; + + if (!PyArg_ParseTuple(args, "O|s:getmask", &encoded_string, &mode)) { + return NULL; + } + + _font_text_asBytes(encoded_string, &text); + if (!text) { + return NULL; + } + + im = ImagingNew(self->bitmap->mode, textwidth(self, text), self->ysize); + if (!im) { + free(text); + return ImagingError_MemoryError(); + } + + b = 0; + (void)ImagingFill(im, &b); + + b = self->baseline; + for (x = 0; text[i]; i++) { + glyph = &self->glyphs[text[i]]; + if (i == 0 || text[i] != text[i - 1]) { + ImagingDelete(bitmap); + bitmap = ImagingCrop( + self->bitmap, glyph->sx0, glyph->sy0, glyph->sx1, glyph->sy1); + if (!bitmap) { + goto failed; + } + } + status = ImagingPaste( + im, + bitmap, + NULL, + glyph->dx0 + x, + glyph->dy0 + b, + glyph->dx1 + x, + glyph->dy1 + b); + if (status < 0) { + goto failed; + } + x = x + glyph->dx; + b = b + glyph->dy; + } + ImagingDelete(bitmap); + free(text); + return PyImagingNew(im); + +failed: + ImagingDelete(bitmap); + free(text); + ImagingDelete(im); + Py_RETURN_NONE; +} + +static PyObject * +_font_getsize(ImagingFontObject *self, PyObject *args) { + unsigned char *text; + PyObject *encoded_string; + PyObject *val; + + if (!PyArg_ParseTuple(args, "O:getsize", &encoded_string)) { + return NULL; + } + + _font_text_asBytes(encoded_string, &text); + if (!text) { + return NULL; + } + + val = Py_BuildValue("ii", textwidth(self, text), self->ysize); + free(text); + return val; +} + +static struct PyMethodDef _font_methods[] = { + {"getmask", (PyCFunction)_font_getmask, METH_VARARGS}, + {"getsize", (PyCFunction)_font_getsize, METH_VARARGS}, + {NULL, NULL} /* sentinel */ +}; + +/* -------------------------------------------------------------------- */ + +static PyObject * +_draw_new(PyObject *self_, PyObject *args) { + ImagingDrawObject *self; + + ImagingObject *imagep; + int blend = 0; + if (!PyArg_ParseTuple(args, "O!|i", &Imaging_Type, &imagep, &blend)) { + return NULL; + } + + self = PyObject_New(ImagingDrawObject, &ImagingDraw_Type); + if (self == NULL) { + return NULL; + } + + /* keep a reference to the image object */ + Py_INCREF(imagep); + self->image = imagep; + + self->ink[0] = self->ink[1] = self->ink[2] = self->ink[3] = 0; + + self->blend = blend; + + return (PyObject *)self; +} + +static void +_draw_dealloc(ImagingDrawObject *self) { + Py_XDECREF(self->image); + PyObject_Del(self); +} + +extern Py_ssize_t +PyPath_Flatten(PyObject *data, double **xy); + +static PyObject * +_draw_ink(ImagingDrawObject *self, PyObject *args) { + INT32 ink = 0; + PyObject *color; + if (!PyArg_ParseTuple(args, "O", &color)) { + return NULL; + } + + if (!getink(color, self->image->image, (char *)&ink)) { + return NULL; + } + + return PyLong_FromLong((int)ink); +} + +static PyObject * +_draw_arc(ImagingDrawObject *self, PyObject *args) { + double *xy; + Py_ssize_t n; + + PyObject *data; + int ink; + int width = 0; + float start, end; + if (!PyArg_ParseTuple(args, "Offi|i", &data, &start, &end, &ink, &width)) { + return NULL; + } + + n = PyPath_Flatten(data, &xy); + if (n < 0) { + return NULL; + } + if (n != 2) { + PyErr_SetString(PyExc_TypeError, must_be_two_coordinates); + free(xy); + return NULL; + } + if (xy[2] < xy[0]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_x_coordinate); + free(xy); + return NULL; + } + if (xy[3] < xy[1]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_y_coordinate); + free(xy); + return NULL; + } + + n = ImagingDrawArc( + self->image->image, + (int)xy[0], + (int)xy[1], + (int)xy[2], + (int)xy[3], + start, + end, + &ink, + width, + self->blend); + + free(xy); + + if (n < 0) { + return NULL; + } + + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject * +_draw_bitmap(ImagingDrawObject *self, PyObject *args) { + double *xy; + Py_ssize_t n; + + PyObject *data; + ImagingObject *bitmap; + int ink; + if (!PyArg_ParseTuple(args, "OO!i", &data, &Imaging_Type, &bitmap, &ink)) { + return NULL; + } + + n = PyPath_Flatten(data, &xy); + if (n < 0) { + return NULL; + } + if (n != 1) { + PyErr_SetString( + PyExc_TypeError, "coordinate list must contain exactly 1 coordinate"); + free(xy); + return NULL; + } + + n = ImagingDrawBitmap( + self->image->image, (int)xy[0], (int)xy[1], bitmap->image, &ink, self->blend); + + free(xy); + + if (n < 0) { + return NULL; + } + + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject * +_draw_chord(ImagingDrawObject *self, PyObject *args) { + double *xy; + Py_ssize_t n; + + PyObject *data; + int ink, fill; + int width = 0; + float start, end; + if (!PyArg_ParseTuple(args, "Offii|i", &data, &start, &end, &ink, &fill, &width)) { + return NULL; + } + + n = PyPath_Flatten(data, &xy); + if (n < 0) { + return NULL; + } + if (n != 2) { + PyErr_SetString(PyExc_TypeError, must_be_two_coordinates); + free(xy); + return NULL; + } + if (xy[2] < xy[0]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_x_coordinate); + free(xy); + return NULL; + } + if (xy[3] < xy[1]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_y_coordinate); + free(xy); + return NULL; + } + + n = ImagingDrawChord( + self->image->image, + (int)xy[0], + (int)xy[1], + (int)xy[2], + (int)xy[3], + start, + end, + &ink, + fill, + width, + self->blend); + + free(xy); + + if (n < 0) { + return NULL; + } + + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject * +_draw_ellipse(ImagingDrawObject *self, PyObject *args) { + double *xy; + Py_ssize_t n; + + PyObject *data; + int ink; + int fill = 0; + int width = 0; + if (!PyArg_ParseTuple(args, "Oi|ii", &data, &ink, &fill, &width)) { + return NULL; + } + + n = PyPath_Flatten(data, &xy); + if (n < 0) { + return NULL; + } + if (n != 2) { + PyErr_SetString(PyExc_TypeError, must_be_two_coordinates); + free(xy); + return NULL; + } + if (xy[2] < xy[0]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_x_coordinate); + free(xy); + return NULL; + } + if (xy[3] < xy[1]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_y_coordinate); + free(xy); + return NULL; + } + + n = ImagingDrawEllipse( + self->image->image, + (int)xy[0], + (int)xy[1], + (int)xy[2], + (int)xy[3], + &ink, + fill, + width, + self->blend); + + free(xy); + + if (n < 0) { + return NULL; + } + + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject * +_draw_lines(ImagingDrawObject *self, PyObject *args) { + double *xy; + Py_ssize_t i, n; + + PyObject *data; + int ink; + int width = 0; + if (!PyArg_ParseTuple(args, "Oi|i", &data, &ink, &width)) { + return NULL; + } + + n = PyPath_Flatten(data, &xy); + if (n < 0) { + return NULL; + } + + if (width <= 1) { + double *p = NULL; + for (i = 0; i < n - 1; i++) { + p = &xy[i + i]; + if (ImagingDrawLine( + self->image->image, + (int)p[0], + (int)p[1], + (int)p[2], + (int)p[3], + &ink, + self->blend) < 0) { + free(xy); + return NULL; + } + } + if (p) { /* draw last point */ + ImagingDrawPoint( + self->image->image, (int)p[2], (int)p[3], &ink, self->blend); + } + } else { + for (i = 0; i < n - 1; i++) { + double *p = &xy[i + i]; + if (ImagingDrawWideLine( + self->image->image, + (int)p[0], + (int)p[1], + (int)p[2], + (int)p[3], + &ink, + width, + self->blend) < 0) { + free(xy); + return NULL; + } + } + } + + free(xy); + + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject * +_draw_points(ImagingDrawObject *self, PyObject *args) { + double *xy; + Py_ssize_t i, n; + + PyObject *data; + int ink; + if (!PyArg_ParseTuple(args, "Oi", &data, &ink)) { + return NULL; + } + + n = PyPath_Flatten(data, &xy); + if (n < 0) { + return NULL; + } + + for (i = 0; i < n; i++) { + double *p = &xy[i + i]; + if (ImagingDrawPoint( + self->image->image, (int)p[0], (int)p[1], &ink, self->blend) < 0) { + free(xy); + return NULL; + } + } + + free(xy); + + Py_INCREF(Py_None); + return Py_None; +} + +#ifdef WITH_ARROW + +/* from outline.c */ +extern ImagingOutline +PyOutline_AsOutline(PyObject *outline); + +static PyObject * +_draw_outline(ImagingDrawObject *self, PyObject *args) { + ImagingOutline outline; + + PyObject *outline_; + int ink; + int fill = 0; + if (!PyArg_ParseTuple(args, "Oi|i", &outline_, &ink, &fill)) { + return NULL; + } + + outline = PyOutline_AsOutline(outline_); + if (!outline) { + PyErr_SetString(PyExc_TypeError, "expected outline object"); + return NULL; + } + + if (ImagingDrawOutline(self->image->image, outline, &ink, fill, self->blend) < 0) { + return NULL; + } + + Py_INCREF(Py_None); + return Py_None; +} + +#endif + +static PyObject * +_draw_pieslice(ImagingDrawObject *self, PyObject *args) { + double *xy; + Py_ssize_t n; + + PyObject *data; + int ink, fill; + int width = 0; + float start, end; + if (!PyArg_ParseTuple(args, "Offii|i", &data, &start, &end, &ink, &fill, &width)) { + return NULL; + } + + n = PyPath_Flatten(data, &xy); + if (n < 0) { + return NULL; + } + if (n != 2) { + PyErr_SetString(PyExc_TypeError, must_be_two_coordinates); + free(xy); + return NULL; + } + if (xy[2] < xy[0]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_x_coordinate); + free(xy); + return NULL; + } + if (xy[3] < xy[1]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_y_coordinate); + free(xy); + return NULL; + } + + n = ImagingDrawPieslice( + self->image->image, + (int)xy[0], + (int)xy[1], + (int)xy[2], + (int)xy[3], + start, + end, + &ink, + fill, + width, + self->blend); + + free(xy); + + if (n < 0) { + return NULL; + } + + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject * +_draw_polygon(ImagingDrawObject *self, PyObject *args) { + double *xy; + int *ixy; + Py_ssize_t n, i; + + PyObject *data; + int ink; + int fill = 0; + int width = 0; + if (!PyArg_ParseTuple(args, "Oi|ii", &data, &ink, &fill, &width)) { + return NULL; + } + + n = PyPath_Flatten(data, &xy); + if (n < 0) { + return NULL; + } + if (n < 2) { + PyErr_SetString( + PyExc_TypeError, "coordinate list must contain at least 2 coordinates"); + free(xy); + return NULL; + } + + /* Copy list of vertices to array */ + ixy = (int *)calloc(n, 2 * sizeof(int)); + if (ixy == NULL) { + free(xy); + return ImagingError_MemoryError(); + } + + for (i = 0; i < n; i++) { + ixy[i + i] = (int)xy[i + i]; + ixy[i + i + 1] = (int)xy[i + i + 1]; + } + + free(xy); + + if (ImagingDrawPolygon(self->image->image, n, ixy, &ink, fill, width, self->blend) < + 0) { + free(ixy); + return NULL; + } + + free(ixy); + + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject * +_draw_rectangle(ImagingDrawObject *self, PyObject *args) { + double *xy; + Py_ssize_t n; + + PyObject *data; + int ink; + int fill = 0; + int width = 0; + if (!PyArg_ParseTuple(args, "Oi|ii", &data, &ink, &fill, &width)) { + return NULL; + } + + n = PyPath_Flatten(data, &xy); + if (n < 0) { + return NULL; + } + if (n != 2) { + PyErr_SetString(PyExc_TypeError, must_be_two_coordinates); + free(xy); + return NULL; + } + if (xy[2] < xy[0]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_x_coordinate); + free(xy); + return NULL; + } + if (xy[3] < xy[1]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_y_coordinate); + free(xy); + return NULL; + } + + n = ImagingDrawRectangle( + self->image->image, + (int)xy[0], + (int)xy[1], + (int)xy[2], + (int)xy[3], + &ink, + fill, + width, + self->blend); + + free(xy); + + if (n < 0) { + return NULL; + } + + Py_INCREF(Py_None); + return Py_None; +} + +static struct PyMethodDef _draw_methods[] = { +#ifdef WITH_IMAGEDRAW + /* Graphics (ImageDraw) */ + {"draw_lines", (PyCFunction)_draw_lines, METH_VARARGS}, +#ifdef WITH_ARROW + {"draw_outline", (PyCFunction)_draw_outline, METH_VARARGS}, +#endif + {"draw_polygon", (PyCFunction)_draw_polygon, METH_VARARGS}, + {"draw_rectangle", (PyCFunction)_draw_rectangle, METH_VARARGS}, + {"draw_points", (PyCFunction)_draw_points, METH_VARARGS}, + {"draw_arc", (PyCFunction)_draw_arc, METH_VARARGS}, + {"draw_bitmap", (PyCFunction)_draw_bitmap, METH_VARARGS}, + {"draw_chord", (PyCFunction)_draw_chord, METH_VARARGS}, + {"draw_ellipse", (PyCFunction)_draw_ellipse, METH_VARARGS}, + {"draw_pieslice", (PyCFunction)_draw_pieslice, METH_VARARGS}, + {"draw_ink", (PyCFunction)_draw_ink, METH_VARARGS}, +#endif + {NULL, NULL} /* sentinel */ +}; + +#endif + +static PyObject * +pixel_access_new(ImagingObject *imagep, PyObject *args) { + PixelAccessObject *self; + + int readonly = 0; + if (!PyArg_ParseTuple(args, "|i", &readonly)) { + return NULL; + } + + self = PyObject_New(PixelAccessObject, &PixelAccess_Type); + if (self == NULL) { + return NULL; + } + + /* keep a reference to the image object */ + Py_INCREF(imagep); + self->image = imagep; + + self->readonly = readonly; + + return (PyObject *)self; +} + +static void +pixel_access_dealloc(PixelAccessObject *self) { + Py_XDECREF(self->image); + PyObject_Del(self); +} + +static PyObject * +pixel_access_getitem(PixelAccessObject *self, PyObject *xy) { + int x, y; + if (_getxy(xy, &x, &y)) { + return NULL; + } + + return getpixel(self->image->image, self->image->access, x, y); +} + +static int +pixel_access_setitem(PixelAccessObject *self, PyObject *xy, PyObject *color) { + Imaging im = self->image->image; + char ink[4]; + int x, y; + + if (self->readonly) { + (void)ImagingError_ValueError(readonly); + return -1; + } + + if (_getxy(xy, &x, &y)) { + return -1; + } + + if (x < 0) { + x = im->xsize + x; + } + if (y < 0) { + y = im->ysize + y; + } + + if (x < 0 || x >= im->xsize || y < 0 || y >= im->ysize) { + PyErr_SetString(PyExc_IndexError, outside_image); + return -1; + } + + if (!color) { /* FIXME: raise exception? */ + return 0; + } + + if (!getink(color, im, ink)) { + return -1; + } + + self->image->access->put_pixel(im, x, y, ink); + + return 0; +} + +/* -------------------------------------------------------------------- */ +/* EFFECTS (experimental) */ +/* -------------------------------------------------------------------- */ + +#ifdef WITH_EFFECTS + +static PyObject * +_effect_mandelbrot(ImagingObject *self, PyObject *args) { + int xsize = 512; + int ysize = 512; + double extent[4]; + int quality = 100; + + extent[0] = -3; + extent[1] = -2.5; + extent[2] = 2; + extent[3] = 2.5; + + if (!PyArg_ParseTuple( + args, + "|(ii)(dddd)i", + &xsize, + &ysize, + &extent[0], + &extent[1], + &extent[2], + &extent[3], + &quality)) { + return NULL; + } + + return PyImagingNew(ImagingEffectMandelbrot(xsize, ysize, extent, quality)); +} + +static PyObject * +_effect_noise(ImagingObject *self, PyObject *args) { + int xsize, ysize; + float sigma = 128; + if (!PyArg_ParseTuple(args, "(ii)|f", &xsize, &ysize, &sigma)) { + return NULL; + } + + return PyImagingNew(ImagingEffectNoise(xsize, ysize, sigma)); +} + +static PyObject * +_effect_spread(ImagingObject *self, PyObject *args) { + int dist; + + if (!PyArg_ParseTuple(args, "i", &dist)) { + return NULL; + } + + return PyImagingNew(ImagingEffectSpread(self->image, dist)); +} + +#endif + +/* -------------------------------------------------------------------- */ +/* UTILITIES */ +/* -------------------------------------------------------------------- */ + +static PyObject * +_getcodecstatus(PyObject *self, PyObject *args) { + int status; + char *msg; + + if (!PyArg_ParseTuple(args, "i", &status)) { + return NULL; + } + + switch (status) { + case IMAGING_CODEC_OVERRUN: + msg = "buffer overrun"; + break; + case IMAGING_CODEC_BROKEN: + msg = "broken data stream"; + break; + case IMAGING_CODEC_UNKNOWN: + msg = "unrecognized data stream contents"; + break; + case IMAGING_CODEC_CONFIG: + msg = "codec configuration error"; + break; + case IMAGING_CODEC_MEMORY: + msg = "out of memory"; + break; + default: + Py_RETURN_NONE; + } + + return PyUnicode_FromString(msg); +} + +/* -------------------------------------------------------------------- */ +/* DEBUGGING HELPERS */ +/* -------------------------------------------------------------------- */ + +static PyObject * +_save_ppm(ImagingObject *self, PyObject *args) { + char *filename; + + if (!PyArg_ParseTuple(args, "s", &filename)) { + return NULL; + } + + if (!ImagingSavePPM(self->image, filename)) { + return NULL; + } + + Py_INCREF(Py_None); + return Py_None; +} + +/* -------------------------------------------------------------------- */ + +/* methods */ + +static struct PyMethodDef methods[] = { + + /* Put commonly used methods first */ + {"getpixel", (PyCFunction)_getpixel, METH_VARARGS}, + {"putpixel", (PyCFunction)_putpixel, METH_VARARGS}, + + {"pixel_access", (PyCFunction)pixel_access_new, METH_VARARGS}, + + /* Standard processing methods (Image) */ + {"color_lut_3d", (PyCFunction)_color_lut_3d, METH_VARARGS}, + {"convert", (PyCFunction)_convert, METH_VARARGS}, + {"convert2", (PyCFunction)_convert2, METH_VARARGS}, + {"convert_matrix", (PyCFunction)_convert_matrix, METH_VARARGS}, + {"convert_transparent", (PyCFunction)_convert_transparent, METH_VARARGS}, + {"copy", (PyCFunction)_copy, METH_VARARGS}, + {"crop", (PyCFunction)_crop, METH_VARARGS}, + {"expand", (PyCFunction)_expand_image, METH_VARARGS}, + {"filter", (PyCFunction)_filter, METH_VARARGS}, + {"histogram", (PyCFunction)_histogram, METH_VARARGS}, + {"entropy", (PyCFunction)_entropy, METH_VARARGS}, +#ifdef WITH_MODEFILTER + {"modefilter", (PyCFunction)_modefilter, METH_VARARGS}, +#endif + {"offset", (PyCFunction)_offset, METH_VARARGS}, + {"paste", (PyCFunction)_paste, METH_VARARGS}, + {"point", (PyCFunction)_point, METH_VARARGS}, + {"point_transform", (PyCFunction)_point_transform, METH_VARARGS}, + {"putdata", (PyCFunction)_putdata, METH_VARARGS}, +#ifdef WITH_QUANTIZE + {"quantize", (PyCFunction)_quantize, METH_VARARGS}, +#endif +#ifdef WITH_RANKFILTER + {"rankfilter", (PyCFunction)_rankfilter, METH_VARARGS}, +#endif + {"resize", (PyCFunction)_resize, METH_VARARGS}, + {"reduce", (PyCFunction)_reduce, METH_VARARGS}, + {"transpose", (PyCFunction)_transpose, METH_VARARGS}, + {"transform", (PyCFunction)_transform, METH_VARARGS}, + + {"isblock", (PyCFunction)_isblock, METH_NOARGS}, + + {"getbbox", (PyCFunction)_getbbox, METH_VARARGS}, + {"getcolors", (PyCFunction)_getcolors, METH_VARARGS}, + {"getextrema", (PyCFunction)_getextrema, METH_NOARGS}, + {"getprojection", (PyCFunction)_getprojection, METH_NOARGS}, + + {"getband", (PyCFunction)_getband, METH_VARARGS}, + {"putband", (PyCFunction)_putband, METH_VARARGS}, + {"split", (PyCFunction)_split, METH_NOARGS}, + {"fillband", (PyCFunction)_fillband, METH_VARARGS}, + + {"setmode", (PyCFunction)im_setmode, METH_VARARGS}, + + {"getpalette", (PyCFunction)_getpalette, METH_VARARGS}, + {"getpalettemode", (PyCFunction)_getpalettemode, METH_NOARGS}, + {"putpalette", (PyCFunction)_putpalette, METH_VARARGS}, + {"putpalettealpha", (PyCFunction)_putpalettealpha, METH_VARARGS}, + {"putpalettealphas", (PyCFunction)_putpalettealphas, METH_VARARGS}, + +#ifdef WITH_IMAGECHOPS + /* Channel operations (ImageChops) */ + {"chop_invert", (PyCFunction)_chop_invert, METH_NOARGS}, + {"chop_lighter", (PyCFunction)_chop_lighter, METH_VARARGS}, + {"chop_darker", (PyCFunction)_chop_darker, METH_VARARGS}, + {"chop_difference", (PyCFunction)_chop_difference, METH_VARARGS}, + {"chop_multiply", (PyCFunction)_chop_multiply, METH_VARARGS}, + {"chop_screen", (PyCFunction)_chop_screen, METH_VARARGS}, + {"chop_add", (PyCFunction)_chop_add, METH_VARARGS}, + {"chop_subtract", (PyCFunction)_chop_subtract, METH_VARARGS}, + {"chop_add_modulo", (PyCFunction)_chop_add_modulo, METH_VARARGS}, + {"chop_subtract_modulo", (PyCFunction)_chop_subtract_modulo, METH_VARARGS}, + {"chop_and", (PyCFunction)_chop_and, METH_VARARGS}, + {"chop_or", (PyCFunction)_chop_or, METH_VARARGS}, + {"chop_xor", (PyCFunction)_chop_xor, METH_VARARGS}, + {"chop_soft_light", (PyCFunction)_chop_soft_light, METH_VARARGS}, + {"chop_hard_light", (PyCFunction)_chop_hard_light, METH_VARARGS}, + {"chop_overlay", (PyCFunction)_chop_overlay, METH_VARARGS}, + +#endif + +#ifdef WITH_UNSHARPMASK + /* Kevin Cazabon's unsharpmask extension */ + {"gaussian_blur", (PyCFunction)_gaussian_blur, METH_VARARGS}, + {"unsharp_mask", (PyCFunction)_unsharp_mask, METH_VARARGS}, +#endif + + {"box_blur", (PyCFunction)_box_blur, METH_VARARGS}, + +#ifdef WITH_EFFECTS + /* Special effects */ + {"effect_spread", (PyCFunction)_effect_spread, METH_VARARGS}, +#endif + + /* Misc. */ + {"new_block", (PyCFunction)_new_block, METH_VARARGS}, + + {"save_ppm", (PyCFunction)_save_ppm, METH_VARARGS}, + + {NULL, NULL} /* sentinel */ +}; + +/* attributes */ + +static PyObject * +_getattr_mode(ImagingObject *self, void *closure) { + return PyUnicode_FromString(self->image->mode); +} + +static PyObject * +_getattr_size(ImagingObject *self, void *closure) { + return Py_BuildValue("ii", self->image->xsize, self->image->ysize); +} + +static PyObject * +_getattr_bands(ImagingObject *self, void *closure) { + return PyLong_FromLong(self->image->bands); +} + +static PyObject * +_getattr_id(ImagingObject *self, void *closure) { + return PyLong_FromSsize_t((Py_ssize_t)self->image); +} + +static PyObject * +_getattr_ptr(ImagingObject *self, void *closure) { + return PyCapsule_New(self->image, IMAGING_MAGIC, NULL); +} + +static PyObject * +_getattr_unsafe_ptrs(ImagingObject *self, void *closure) { + return Py_BuildValue( + "(sn)(sn)(sn)", + "image8", + self->image->image8, + "image32", + self->image->image32, + "image", + self->image->image); +} + +static struct PyGetSetDef getsetters[] = { + {"mode", (getter)_getattr_mode}, + {"size", (getter)_getattr_size}, + {"bands", (getter)_getattr_bands}, + {"id", (getter)_getattr_id}, + {"ptr", (getter)_getattr_ptr}, + {"unsafe_ptrs", (getter)_getattr_unsafe_ptrs}, + {NULL}}; + +/* basic sequence semantics */ + +static Py_ssize_t +image_length(ImagingObject *self) { + Imaging im = self->image; + + return (Py_ssize_t)im->xsize * im->ysize; +} + +static PyObject * +image_item(ImagingObject *self, Py_ssize_t i) { + int x, y; + Imaging im = self->image; + + if (im->xsize > 0) { + x = i % im->xsize; + y = i / im->xsize; + } else { + x = y = 0; /* leave it to getpixel to raise an exception */ + } + + return getpixel(im, self->access, x, y); +} + +static PySequenceMethods image_as_sequence = { + (lenfunc)image_length, /*sq_length*/ + (binaryfunc)NULL, /*sq_concat*/ + (ssizeargfunc)NULL, /*sq_repeat*/ + (ssizeargfunc)image_item, /*sq_item*/ + (ssizessizeargfunc)NULL, /*sq_slice*/ + (ssizeobjargproc)NULL, /*sq_ass_item*/ + (ssizessizeobjargproc)NULL, /*sq_ass_slice*/ +}; + +/* 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*/ +}; + +#ifdef WITH_IMAGEDRAW + +static PyTypeObject ImagingFont_Type = { + PyVarObject_HEAD_INIT(NULL, 0) "ImagingFont", /*tp_name*/ + sizeof(ImagingFontObject), /*tp_basicsize*/ + 0, /*tp_itemsize*/ + /* methods */ + (destructor)_font_dealloc, /*tp_dealloc*/ + 0, /*tp_vectorcall_offset*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_as_async*/ + 0, /*tp_repr*/ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ + 0, /*tp_hash*/ + 0, /*tp_call*/ + 0, /*tp_str*/ + 0, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT, /*tp_flags*/ + 0, /*tp_doc*/ + 0, /*tp_traverse*/ + 0, /*tp_clear*/ + 0, /*tp_richcompare*/ + 0, /*tp_weaklistoffset*/ + 0, /*tp_iter*/ + 0, /*tp_iternext*/ + _font_methods, /*tp_methods*/ + 0, /*tp_members*/ + 0, /*tp_getset*/ +}; + +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*/ +}; + +#endif + +static PyMappingMethods pixel_access_as_mapping = { + (lenfunc)NULL, /*mp_length*/ + (binaryfunc)pixel_access_getitem, /*mp_subscript*/ + (objobjargproc)pixel_access_setitem, /*mp_ass_subscript*/ +}; + +/* type description */ + +static PyTypeObject PixelAccess_Type = { + PyVarObject_HEAD_INIT(NULL, 0) "PixelAccess", /*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*/ +}; + +/* -------------------------------------------------------------------- */ + +static PyObject * +_get_stats(PyObject *self, PyObject *args) { + PyObject *d; + PyObject *v; + ImagingMemoryArena arena = &ImagingDefaultArena; + + if (!PyArg_ParseTuple(args, ":get_stats")) { + return NULL; + } + + d = PyDict_New(); + if (!d) { + return NULL; + } + v = PyLong_FromLong(arena->stats_new_count); + PyDict_SetItemString(d, "new_count", v ? v : Py_None); + Py_XDECREF(v); + + v = PyLong_FromLong(arena->stats_allocated_blocks); + PyDict_SetItemString(d, "allocated_blocks", v ? v : Py_None); + Py_XDECREF(v); + + v = PyLong_FromLong(arena->stats_reused_blocks); + PyDict_SetItemString(d, "reused_blocks", v ? v : Py_None); + Py_XDECREF(v); + + v = PyLong_FromLong(arena->stats_reallocated_blocks); + PyDict_SetItemString(d, "reallocated_blocks", v ? v : Py_None); + Py_XDECREF(v); + + v = PyLong_FromLong(arena->stats_freed_blocks); + PyDict_SetItemString(d, "freed_blocks", v ? v : Py_None); + Py_XDECREF(v); + + v = PyLong_FromLong(arena->blocks_cached); + PyDict_SetItemString(d, "blocks_cached", v ? v : Py_None); + Py_XDECREF(v); + return d; +} + +static PyObject * +_reset_stats(PyObject *self, PyObject *args) { + ImagingMemoryArena arena = &ImagingDefaultArena; + + if (!PyArg_ParseTuple(args, ":reset_stats")) { + return NULL; + } + + arena->stats_new_count = 0; + arena->stats_allocated_blocks = 0; + arena->stats_reused_blocks = 0; + arena->stats_reallocated_blocks = 0; + arena->stats_freed_blocks = 0; + + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject * +_get_alignment(PyObject *self, PyObject *args) { + if (!PyArg_ParseTuple(args, ":get_alignment")) { + return NULL; + } + + return PyLong_FromLong(ImagingDefaultArena.alignment); +} + +static PyObject * +_get_block_size(PyObject *self, PyObject *args) { + if (!PyArg_ParseTuple(args, ":get_block_size")) { + return NULL; + } + + return PyLong_FromLong(ImagingDefaultArena.block_size); +} + +static PyObject * +_get_blocks_max(PyObject *self, PyObject *args) { + if (!PyArg_ParseTuple(args, ":get_blocks_max")) { + return NULL; + } + + return PyLong_FromLong(ImagingDefaultArena.blocks_max); +} + +static PyObject * +_set_alignment(PyObject *self, PyObject *args) { + int alignment; + if (!PyArg_ParseTuple(args, "i:set_alignment", &alignment)) { + return NULL; + } + + if (alignment < 1 || alignment > 128) { + PyErr_SetString(PyExc_ValueError, "alignment should be from 1 to 128"); + return NULL; + } + /* Is power of two */ + if (alignment & (alignment - 1)) { + PyErr_SetString(PyExc_ValueError, "alignment should be power of two"); + return NULL; + } + + ImagingDefaultArena.alignment = alignment; + + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject * +_set_block_size(PyObject *self, PyObject *args) { + int block_size; + if (!PyArg_ParseTuple(args, "i:set_block_size", &block_size)) { + return NULL; + } + + if (block_size <= 0) { + PyErr_SetString(PyExc_ValueError, "block_size should be greater than 0"); + return NULL; + } + + if (block_size & 0xfff) { + PyErr_SetString(PyExc_ValueError, "block_size should be multiple of 4096"); + return NULL; + } + + ImagingDefaultArena.block_size = block_size; + + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject * +_set_blocks_max(PyObject *self, PyObject *args) { + int blocks_max; + if (!PyArg_ParseTuple(args, "i:set_blocks_max", &blocks_max)) { + return NULL; + } + + if (blocks_max < 0) { + PyErr_SetString(PyExc_ValueError, "blocks_max should be greater than 0"); + return NULL; + } else if ( + (unsigned long)blocks_max > + SIZE_MAX / sizeof(ImagingDefaultArena.blocks_pool[0])) { + PyErr_SetString(PyExc_ValueError, "blocks_max is too large"); + return NULL; + } + + if (!ImagingMemorySetBlocksMax(&ImagingDefaultArena, blocks_max)) { + return ImagingError_MemoryError(); + } + + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject * +_clear_cache(PyObject *self, PyObject *args) { + int i = 0; + + if (!PyArg_ParseTuple(args, "|i:clear_cache", &i)) { + return NULL; + } + + ImagingMemoryClearCache(&ImagingDefaultArena, i); + + Py_INCREF(Py_None); + return Py_None; +} + +/* -------------------------------------------------------------------- */ + +/* FIXME: this is something of a mess. Should replace this with + pluggable codecs, but not before PIL 1.2 */ + +/* Decoders (in decode.c) */ +extern PyObject * +PyImaging_BcnDecoderNew(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_BitDecoderNew(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_FliDecoderNew(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_GifDecoderNew(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_HexDecoderNew(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_JpegDecoderNew(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_Jpeg2KDecoderNew(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_LibTiffDecoderNew(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_PackbitsDecoderNew(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_PcdDecoderNew(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_PcxDecoderNew(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_RawDecoderNew(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_SgiRleDecoderNew(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_SunRleDecoderNew(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_TgaRleDecoderNew(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_XbmDecoderNew(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_ZipDecoderNew(PyObject *self, PyObject *args); + +/* Encoders (in encode.c) */ +extern PyObject * +PyImaging_EpsEncoderNew(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_GifEncoderNew(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_JpegEncoderNew(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_PcxEncoderNew(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_RawEncoderNew(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_TgaRleEncoderNew(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_XbmEncoderNew(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_ZipEncoderNew(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args); + +/* Display support etc (in display.c) */ +#ifdef _WIN32 +extern PyObject * +PyImaging_CreateWindowWin32(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_DisplayWin32(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_DisplayModeWin32(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_GrabScreenWin32(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_GrabClipboardWin32(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_EventLoopWin32(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_DrawWmf(PyObject *self, PyObject *args); +#endif +#ifdef HAVE_XCB +extern PyObject * +PyImaging_GrabScreenX11(PyObject *self, PyObject *args); +#endif + +/* Experimental path stuff (in path.c) */ +extern PyObject * +PyPath_Create(ImagingObject *self, PyObject *args); + +/* Experimental outline stuff (in outline.c) */ +extern PyObject * +PyOutline_Create(ImagingObject *self, PyObject *args); + +extern PyObject * +PyImaging_MapBuffer(PyObject *self, PyObject *args); + +static PyMethodDef functions[] = { + + /* Object factories */ + {"alpha_composite", (PyCFunction)_alpha_composite, METH_VARARGS}, + {"blend", (PyCFunction)_blend, METH_VARARGS}, + {"fill", (PyCFunction)_fill, METH_VARARGS}, + {"new", (PyCFunction)_new, METH_VARARGS}, + {"merge", (PyCFunction)_merge, METH_VARARGS}, + + /* Functions */ + {"convert", (PyCFunction)_convert2, METH_VARARGS}, + + /* Codecs */ + {"bcn_decoder", (PyCFunction)PyImaging_BcnDecoderNew, METH_VARARGS}, + {"bit_decoder", (PyCFunction)PyImaging_BitDecoderNew, METH_VARARGS}, + {"eps_encoder", (PyCFunction)PyImaging_EpsEncoderNew, METH_VARARGS}, + {"fli_decoder", (PyCFunction)PyImaging_FliDecoderNew, METH_VARARGS}, + {"gif_decoder", (PyCFunction)PyImaging_GifDecoderNew, METH_VARARGS}, + {"gif_encoder", (PyCFunction)PyImaging_GifEncoderNew, METH_VARARGS}, + {"hex_decoder", (PyCFunction)PyImaging_HexDecoderNew, METH_VARARGS}, + {"hex_encoder", (PyCFunction)PyImaging_EpsEncoderNew, METH_VARARGS}, /* EPS=HEX! */ +#ifdef HAVE_LIBJPEG + {"jpeg_decoder", (PyCFunction)PyImaging_JpegDecoderNew, METH_VARARGS}, + {"jpeg_encoder", (PyCFunction)PyImaging_JpegEncoderNew, METH_VARARGS}, +#endif +#ifdef HAVE_OPENJPEG + {"jpeg2k_decoder", (PyCFunction)PyImaging_Jpeg2KDecoderNew, METH_VARARGS}, + {"jpeg2k_encoder", (PyCFunction)PyImaging_Jpeg2KEncoderNew, METH_VARARGS}, +#endif +#ifdef HAVE_LIBTIFF + {"libtiff_decoder", (PyCFunction)PyImaging_LibTiffDecoderNew, METH_VARARGS}, + {"libtiff_encoder", (PyCFunction)PyImaging_LibTiffEncoderNew, METH_VARARGS}, +#endif + {"packbits_decoder", (PyCFunction)PyImaging_PackbitsDecoderNew, METH_VARARGS}, + {"pcd_decoder", (PyCFunction)PyImaging_PcdDecoderNew, METH_VARARGS}, + {"pcx_decoder", (PyCFunction)PyImaging_PcxDecoderNew, METH_VARARGS}, + {"pcx_encoder", (PyCFunction)PyImaging_PcxEncoderNew, METH_VARARGS}, + {"raw_decoder", (PyCFunction)PyImaging_RawDecoderNew, METH_VARARGS}, + {"raw_encoder", (PyCFunction)PyImaging_RawEncoderNew, METH_VARARGS}, + {"sgi_rle_decoder", (PyCFunction)PyImaging_SgiRleDecoderNew, METH_VARARGS}, + {"sun_rle_decoder", (PyCFunction)PyImaging_SunRleDecoderNew, METH_VARARGS}, + {"tga_rle_decoder", (PyCFunction)PyImaging_TgaRleDecoderNew, METH_VARARGS}, + {"tga_rle_encoder", (PyCFunction)PyImaging_TgaRleEncoderNew, METH_VARARGS}, + {"xbm_decoder", (PyCFunction)PyImaging_XbmDecoderNew, METH_VARARGS}, + {"xbm_encoder", (PyCFunction)PyImaging_XbmEncoderNew, METH_VARARGS}, +#ifdef HAVE_LIBZ + {"zip_decoder", (PyCFunction)PyImaging_ZipDecoderNew, METH_VARARGS}, + {"zip_encoder", (PyCFunction)PyImaging_ZipEncoderNew, METH_VARARGS}, +#endif + +/* Memory mapping */ +#ifdef WITH_MAPPING + {"map_buffer", (PyCFunction)PyImaging_MapBuffer, METH_VARARGS}, +#endif + +/* Display support */ +#ifdef _WIN32 + {"display", (PyCFunction)PyImaging_DisplayWin32, METH_VARARGS}, + {"display_mode", (PyCFunction)PyImaging_DisplayModeWin32, METH_VARARGS}, + {"grabscreen_win32", (PyCFunction)PyImaging_GrabScreenWin32, METH_VARARGS}, + {"grabclipboard_win32", (PyCFunction)PyImaging_GrabClipboardWin32, METH_VARARGS}, + {"createwindow", (PyCFunction)PyImaging_CreateWindowWin32, METH_VARARGS}, + {"eventloop", (PyCFunction)PyImaging_EventLoopWin32, METH_VARARGS}, + {"drawwmf", (PyCFunction)PyImaging_DrawWmf, METH_VARARGS}, +#endif +#ifdef HAVE_XCB + {"grabscreen_x11", (PyCFunction)PyImaging_GrabScreenX11, METH_VARARGS}, +#endif + + /* Utilities */ + {"getcodecstatus", (PyCFunction)_getcodecstatus, METH_VARARGS}, + +/* Special effects (experimental) */ +#ifdef WITH_EFFECTS + {"effect_mandelbrot", (PyCFunction)_effect_mandelbrot, METH_VARARGS}, + {"effect_noise", (PyCFunction)_effect_noise, METH_VARARGS}, + {"linear_gradient", (PyCFunction)_linear_gradient, METH_VARARGS}, + {"radial_gradient", (PyCFunction)_radial_gradient, METH_VARARGS}, + {"wedge", (PyCFunction)_linear_gradient, METH_VARARGS}, /* Compatibility */ +#endif + +/* Drawing support stuff */ +#ifdef WITH_IMAGEDRAW + {"font", (PyCFunction)_font_new, METH_VARARGS}, + {"draw", (PyCFunction)_draw_new, METH_VARARGS}, +#endif + +/* Experimental path stuff */ +#ifdef WITH_IMAGEPATH + {"path", (PyCFunction)PyPath_Create, METH_VARARGS}, +#endif + +/* Experimental arrow graphics stuff */ +#ifdef WITH_ARROW + {"outline", (PyCFunction)PyOutline_Create, METH_VARARGS}, +#endif + + /* Resource management */ + {"get_stats", (PyCFunction)_get_stats, METH_VARARGS}, + {"reset_stats", (PyCFunction)_reset_stats, METH_VARARGS}, + {"get_alignment", (PyCFunction)_get_alignment, METH_VARARGS}, + {"get_block_size", (PyCFunction)_get_block_size, METH_VARARGS}, + {"get_blocks_max", (PyCFunction)_get_blocks_max, METH_VARARGS}, + {"set_alignment", (PyCFunction)_set_alignment, METH_VARARGS}, + {"set_block_size", (PyCFunction)_set_block_size, METH_VARARGS}, + {"set_blocks_max", (PyCFunction)_set_blocks_max, METH_VARARGS}, + {"clear_cache", (PyCFunction)_clear_cache, METH_VARARGS}, + + {NULL, NULL} /* sentinel */ +}; + +static int +setup_module(PyObject *m) { + PyObject *d = PyModule_GetDict(m); + const char *version = (char *)PILLOW_VERSION; + + /* Ready object types */ + if (PyType_Ready(&Imaging_Type) < 0) { + return -1; + } + +#ifdef WITH_IMAGEDRAW + if (PyType_Ready(&ImagingFont_Type) < 0) { + return -1; + } + + if (PyType_Ready(&ImagingDraw_Type) < 0) { + return -1; + } +#endif + if (PyType_Ready(&PixelAccess_Type) < 0) { + return -1; + } + + ImagingAccessInit(); + +#ifdef HAVE_LIBJPEG + { + extern const char *ImagingJpegVersion(void); + PyObject *v = PyUnicode_FromString(ImagingJpegVersion()); + PyDict_SetItemString(d, "jpeglib_version", v ? v : Py_None); + Py_XDECREF(v); + } +#endif + +#ifdef HAVE_OPENJPEG + { + extern const char *ImagingJpeg2KVersion(void); + PyObject *v = PyUnicode_FromString(ImagingJpeg2KVersion()); + PyDict_SetItemString(d, "jp2klib_version", v ? v : Py_None); + Py_XDECREF(v); + } +#endif + + PyObject *have_libjpegturbo; +#ifdef LIBJPEG_TURBO_VERSION + have_libjpegturbo = Py_True; + { +#define tostr1(a) #a +#define tostr(a) tostr1(a) + PyObject *v = PyUnicode_FromString(tostr(LIBJPEG_TURBO_VERSION)); + PyDict_SetItemString(d, "libjpeg_turbo_version", v ? v : Py_None); + Py_XDECREF(v); +#undef tostr +#undef tostr1 + } +#else + have_libjpegturbo = Py_False; +#endif + Py_INCREF(have_libjpegturbo); + PyModule_AddObject(m, "HAVE_LIBJPEGTURBO", have_libjpegturbo); + + PyObject *have_libimagequant; +#ifdef HAVE_LIBIMAGEQUANT + have_libimagequant = Py_True; + { + extern const char *ImagingImageQuantVersion(void); + PyObject *v = PyUnicode_FromString(ImagingImageQuantVersion()); + PyDict_SetItemString(d, "imagequant_version", v ? v : Py_None); + Py_XDECREF(v); + } +#else + have_libimagequant = Py_False; +#endif + Py_INCREF(have_libimagequant); + PyModule_AddObject(m, "HAVE_LIBIMAGEQUANT", have_libimagequant); + +#ifdef HAVE_LIBZ + /* zip encoding strategies */ + PyModule_AddIntConstant(m, "DEFAULT_STRATEGY", Z_DEFAULT_STRATEGY); + PyModule_AddIntConstant(m, "FILTERED", Z_FILTERED); + PyModule_AddIntConstant(m, "HUFFMAN_ONLY", Z_HUFFMAN_ONLY); + PyModule_AddIntConstant(m, "RLE", Z_RLE); + PyModule_AddIntConstant(m, "FIXED", Z_FIXED); + { + extern const char *ImagingZipVersion(void); + PyObject *v = PyUnicode_FromString(ImagingZipVersion()); + PyDict_SetItemString(d, "zlib_version", v ? v : Py_None); + Py_XDECREF(v); + } +#endif + +#ifdef HAVE_LIBTIFF + { + extern const char *ImagingTiffVersion(void); + PyObject *v = PyUnicode_FromString(ImagingTiffVersion()); + PyDict_SetItemString(d, "libtiff_version", v ? v : Py_None); + Py_XDECREF(v); + + // Test for libtiff 4.0 or later, excluding libtiff 3.9.6 and 3.9.7 + PyObject *support_custom_tags; +#if TIFFLIB_VERSION >= 20111221 && TIFFLIB_VERSION != 20120218 && \ + TIFFLIB_VERSION != 20120922 + support_custom_tags = Py_True; +#else + support_custom_tags = Py_False; +#endif + PyDict_SetItemString(d, "libtiff_support_custom_tags", support_custom_tags); + } +#endif + + PyObject *have_xcb; +#ifdef HAVE_XCB + have_xcb = Py_True; +#else + have_xcb = Py_False; +#endif + Py_INCREF(have_xcb); + PyModule_AddObject(m, "HAVE_XCB", have_xcb); + + PyObject *pillow_version = PyUnicode_FromString(version); + PyDict_SetItemString( + d, "PILLOW_VERSION", pillow_version ? pillow_version : Py_None); + Py_XDECREF(pillow_version); + + return 0; +} + +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 = PyModule_Create(&module_def); + + if (setup_module(m) < 0) { + Py_DECREF(m); + return NULL; + } + + return m; +} diff --git a/src/_imagingcms.c b/src/_imagingcms.c index 2b9612db7..ea50ae4d3 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -1,1540 +1,1540 @@ -/* - * pyCMS - * a Python / PIL interface to the littleCMS ICC Color Management System - * Copyright (C) 2002-2003 Kevin Cazabon - * kevin@cazabon.com - * https://www.cazabon.com - * Adapted/reworked for PIL by Fredrik Lundh - * Copyright (c) 2009 Fredrik Lundh - * Updated to LCMS2 - * Copyright (c) 2013 Eric Soroos - * - * pyCMS home page: https://www.cazabon.com/pyCMS - * littleCMS home page: https://www.littlecms.com - * (littleCMS is Copyright (C) 1998-2001 Marti Maria) - * - * Originally released under LGPL. Graciously donated to PIL in - * March 2009, for distribution under the standard PIL license - */ - -#define COPYRIGHTINFO \ - "\ -pyCMS\n\ -a Python / PIL interface to the littleCMS ICC Color Management System\n\ -Copyright (C) 2002-2003 Kevin Cazabon\n\ -kevin@cazabon.com\n\ -https://www.cazabon.com\n\ -" - -#define PY_SSIZE_T_CLEAN -#include "Python.h" // Include before wchar.h so _GNU_SOURCE is set -#include "wchar.h" -#include "datetime.h" - -#include "lcms2.h" -#include "libImaging/Imaging.h" - -#define PYCMSVERSION "1.0.0 pil" - -/* version history */ - -/* - 1.0.0 pil Integrating littleCMS2 - 0.1.0 pil integration & refactoring - 0.0.2 alpha: Minor updates, added interfaces to littleCMS features, Jan 6, 2003 - - fixed some memory holes in how transforms/profiles were created and passed back to - Python due to improper destructor setup for PyCObjects - - added buildProofTransformFromOpenProfiles() function - - eliminated some code redundancy, centralizing several common tasks with internal - functions - - 0.0.1 alpha: First public release Dec 26, 2002 - -*/ - -/* known to-do list: - - Verify that PILmode->littleCMStype conversion in findLCMStype is correct for all - PIL modes (it probably isn't for the more obscure ones) - - Add support for creating custom RGB profiles on the fly - Add support for checking presence of a specific tag in a profile - Add support for other littleCMS features as required - -*/ - -/* - INTENT_PERCEPTUAL 0 - INTENT_RELATIVE_COLORIMETRIC 1 - INTENT_SATURATION 2 - INTENT_ABSOLUTE_COLORIMETRIC 3 -*/ - -/* -------------------------------------------------------------------- */ -/* wrapper classes */ - -/* a profile represents the ICC characteristics for a specific device */ - -typedef struct { - PyObject_HEAD cmsHPROFILE profile; -} CmsProfileObject; - -static PyTypeObject CmsProfile_Type; - -#define CmsProfile_Check(op) (Py_TYPE(op) == &CmsProfile_Type) - -static PyObject * -cms_profile_new(cmsHPROFILE profile) { - CmsProfileObject *self; - - self = PyObject_New(CmsProfileObject, &CmsProfile_Type); - if (!self) { - return NULL; - } - - self->profile = profile; - - return (PyObject *)self; -} - -static PyObject * -cms_profile_open(PyObject *self, PyObject *args) { - cmsHPROFILE hProfile; - - char *sProfile; - if (!PyArg_ParseTuple(args, "s:profile_open", &sProfile)) { - return NULL; - } - - hProfile = cmsOpenProfileFromFile(sProfile, "r"); - if (!hProfile) { - PyErr_SetString(PyExc_OSError, "cannot open profile file"); - return NULL; - } - - return cms_profile_new(hProfile); -} - -static PyObject * -cms_profile_frombytes(PyObject *self, PyObject *args) { - cmsHPROFILE hProfile; - - char *pProfile; - Py_ssize_t nProfile; - if (!PyArg_ParseTuple(args, "y#:profile_frombytes", &pProfile, &nProfile)) { - return NULL; - } - - hProfile = cmsOpenProfileFromMem(pProfile, nProfile); - if (!hProfile) { - PyErr_SetString(PyExc_OSError, "cannot open profile from string"); - return NULL; - } - - return cms_profile_new(hProfile); -} - -static PyObject * -cms_profile_tobytes(PyObject *self, PyObject *args) { - char *pProfile = NULL; - cmsUInt32Number nProfile; - PyObject *CmsProfile; - - cmsHPROFILE *profile; - - PyObject *ret; - if (!PyArg_ParseTuple(args, "O!", &CmsProfile_Type, &CmsProfile)) { - return NULL; - } - - profile = ((CmsProfileObject *)CmsProfile)->profile; - - if (!cmsSaveProfileToMem(profile, pProfile, &nProfile)) { - PyErr_SetString(PyExc_OSError, "Could not determine profile size"); - return NULL; - } - - pProfile = (char *)malloc(nProfile); - if (!pProfile) { - PyErr_SetString(PyExc_OSError, "Out of Memory"); - return NULL; - } - - if (!cmsSaveProfileToMem(profile, pProfile, &nProfile)) { - PyErr_SetString(PyExc_OSError, "Could not get profile"); - free(pProfile); - return NULL; - } - - ret = PyBytes_FromStringAndSize(pProfile, (Py_ssize_t)nProfile); - - free(pProfile); - return ret; -} - -static void -cms_profile_dealloc(CmsProfileObject *self) { - (void)cmsCloseProfile(self->profile); - PyObject_Del(self); -} - -/* a transform represents the mapping between two profiles */ - -typedef struct { - PyObject_HEAD cmsHTRANSFORM transform; -} CmsTransformObject; - -static PyTypeObject CmsTransform_Type; - -#define CmsTransform_Check(op) (Py_TYPE(op) == &CmsTransform_Type) - -static PyObject * -cms_transform_new(cmsHTRANSFORM transform) { - CmsTransformObject *self; - - self = PyObject_New(CmsTransformObject, &CmsTransform_Type); - if (!self) { - return NULL; - } - - self->transform = transform; - - return (PyObject *)self; -} - -static void -cms_transform_dealloc(CmsTransformObject *self) { - cmsDeleteTransform(self->transform); - PyObject_Del(self); -} - -/* -------------------------------------------------------------------- */ -/* internal functions */ - -static cmsUInt32Number -findLCMStype(char *PILmode) { - if (strcmp(PILmode, "RGB") == 0 || strcmp(PILmode, "RGBA") == 0 || - strcmp(PILmode, "RGBX") == 0) { - return TYPE_RGBA_8; - } - if (strcmp(PILmode, "RGBA;16B") == 0) { - return TYPE_RGBA_16; - } - if (strcmp(PILmode, "CMYK") == 0) { - return TYPE_CMYK_8; - } - if (strcmp(PILmode, "L;16") == 0) { - return TYPE_GRAY_16; - } - if (strcmp(PILmode, "L;16B") == 0) { - return TYPE_GRAY_16_SE; - } - if (strcmp(PILmode, "YCCA") == 0 || strcmp(PILmode, "YCC") == 0) { - return TYPE_YCbCr_8; - } - if (strcmp(PILmode, "LAB") == 0) { - // LabX equivalent like ALab, but not reversed -- no #define in lcms2 - return (COLORSPACE_SH(PT_LabV2) | CHANNELS_SH(3) | BYTES_SH(1) | EXTRA_SH(1)); - } - /* presume "L" by default */ - return TYPE_GRAY_8; -} - -#define Cms_Min(a, b) ((a) < (b) ? (a) : (b)) - -static int -pyCMSgetAuxChannelChannel(cmsUInt32Number format, int auxChannelNdx) { - int numColors = T_CHANNELS(format); - int numExtras = T_EXTRA(format); - - if (T_SWAPFIRST(format) && T_DOSWAP(format)) { - // reverse order, before anything but last extra is shifted last - if (auxChannelNdx == numExtras - 1) { - return numColors + numExtras - 1; - } else { - return numExtras - 2 - auxChannelNdx; - } - } else if (T_SWAPFIRST(format)) { - // in order, after color channels, but last extra is shifted to first - if (auxChannelNdx == numExtras - 1) { - return 0; - } else { - return numColors + 1 + auxChannelNdx; - } - } else if (T_DOSWAP(format)) { - // reverse order, before anything - return numExtras - 1 - auxChannelNdx; - } else { - // in order, after color channels - return numColors + auxChannelNdx; - } -} - -static void -pyCMScopyAux(cmsHTRANSFORM hTransform, Imaging imDst, const Imaging imSrc) { - cmsUInt32Number dstLCMSFormat; - cmsUInt32Number srcLCMSFormat; - int numSrcExtras; - int numDstExtras; - int numExtras; - int ySize; - int xSize; - int channelSize; - int srcChunkSize; - int dstChunkSize; - int e; - - // trivially copied - if (imDst == imSrc) { - return; - } - - dstLCMSFormat = cmsGetTransformOutputFormat(hTransform); - srcLCMSFormat = cmsGetTransformInputFormat(hTransform); - - // currently, all Pillow formats are chunky formats, but check it anyway - if (T_PLANAR(dstLCMSFormat) || T_PLANAR(srcLCMSFormat)) { - return; - } - - // copy only if channel format is identical, except OPTIMIZED is ignored as it - // does not affect the aux channel - if (T_FLOAT(dstLCMSFormat) != T_FLOAT(srcLCMSFormat) || - T_FLAVOR(dstLCMSFormat) != T_FLAVOR(srcLCMSFormat) || - T_ENDIAN16(dstLCMSFormat) != T_ENDIAN16(srcLCMSFormat) || - T_BYTES(dstLCMSFormat) != T_BYTES(srcLCMSFormat)) { - return; - } - - numSrcExtras = T_EXTRA(srcLCMSFormat); - numDstExtras = T_EXTRA(dstLCMSFormat); - numExtras = Cms_Min(numSrcExtras, numDstExtras); - ySize = Cms_Min(imSrc->ysize, imDst->ysize); - xSize = Cms_Min(imSrc->xsize, imDst->xsize); - channelSize = T_BYTES(dstLCMSFormat); - srcChunkSize = (T_CHANNELS(srcLCMSFormat) + T_EXTRA(srcLCMSFormat)) * channelSize; - dstChunkSize = (T_CHANNELS(dstLCMSFormat) + T_EXTRA(dstLCMSFormat)) * channelSize; - - for (e = 0; e < numExtras; ++e) { - int y; - int dstChannel = pyCMSgetAuxChannelChannel(dstLCMSFormat, e); - int srcChannel = pyCMSgetAuxChannelChannel(srcLCMSFormat, e); - - for (y = 0; y < ySize; y++) { - int x; - char *pDstExtras = imDst->image[y] + dstChannel * channelSize; - const char *pSrcExtras = imSrc->image[y] + srcChannel * channelSize; - - for (x = 0; x < xSize; x++) { - memcpy( - pDstExtras + x * dstChunkSize, - pSrcExtras + x * srcChunkSize, - channelSize); - } - } - } -} - -static int -pyCMSdoTransform(Imaging im, Imaging imOut, cmsHTRANSFORM hTransform) { - int i; - - if (im->xsize > imOut->xsize || im->ysize > imOut->ysize) { - return -1; - } - - Py_BEGIN_ALLOW_THREADS - - // transform color channels only - for (i = 0; i < im->ysize; i++) { - cmsDoTransform(hTransform, im->image[i], imOut->image[i], im->xsize); - } - - // lcms by default does nothing to the auxiliary channels leaving those - // unchanged. To do "the right thing" here, i.e. maintain identical results - // with and without inPlace, we replicate those channels to the output. - // - // As of lcms 2.8, a new cmsFLAGS_COPY_ALPHA flag is introduced which would - // do the same thing automagically. Unfortunately, lcms2.8 is not yet widely - // enough available on all platforms, so we polyfill it here for now. - pyCMScopyAux(hTransform, imOut, im); - - Py_END_ALLOW_THREADS - - return 0; -} - -static cmsHTRANSFORM -_buildTransform( - cmsHPROFILE hInputProfile, - cmsHPROFILE hOutputProfile, - char *sInMode, - char *sOutMode, - int iRenderingIntent, - cmsUInt32Number cmsFLAGS) { - cmsHTRANSFORM hTransform; - - Py_BEGIN_ALLOW_THREADS - - /* create the transform */ - hTransform = cmsCreateTransform( - hInputProfile, - findLCMStype(sInMode), - hOutputProfile, - findLCMStype(sOutMode), - iRenderingIntent, - cmsFLAGS); - - Py_END_ALLOW_THREADS; - - if (!hTransform) { - PyErr_SetString(PyExc_ValueError, "cannot build transform"); - } - - return hTransform; /* if NULL, an exception is set */ -} - -static cmsHTRANSFORM -_buildProofTransform( - cmsHPROFILE hInputProfile, - cmsHPROFILE hOutputProfile, - cmsHPROFILE hProofProfile, - char *sInMode, - char *sOutMode, - int iRenderingIntent, - int iProofIntent, - cmsUInt32Number cmsFLAGS) { - cmsHTRANSFORM hTransform; - - Py_BEGIN_ALLOW_THREADS - - /* create the transform */ - hTransform = cmsCreateProofingTransform( - hInputProfile, - findLCMStype(sInMode), - hOutputProfile, - findLCMStype(sOutMode), - hProofProfile, - iRenderingIntent, - iProofIntent, - cmsFLAGS); - - Py_END_ALLOW_THREADS; - - if (!hTransform) { - PyErr_SetString(PyExc_ValueError, "cannot build proof transform"); - } - - return hTransform; /* if NULL, an exception is set */ -} - -/* -------------------------------------------------------------------- */ -/* Python callable functions */ - -static PyObject * -buildTransform(PyObject *self, PyObject *args) { - CmsProfileObject *pInputProfile; - CmsProfileObject *pOutputProfile; - char *sInMode; - char *sOutMode; - int iRenderingIntent = 0; - int cmsFLAGS = 0; - - cmsHTRANSFORM transform = NULL; - - if (!PyArg_ParseTuple( - args, - "O!O!ss|ii:buildTransform", - &CmsProfile_Type, - &pInputProfile, - &CmsProfile_Type, - &pOutputProfile, - &sInMode, - &sOutMode, - &iRenderingIntent, - &cmsFLAGS)) { - return NULL; - } - - transform = _buildTransform( - pInputProfile->profile, - pOutputProfile->profile, - sInMode, - sOutMode, - iRenderingIntent, - cmsFLAGS); - - if (!transform) { - return NULL; - } - - return cms_transform_new(transform); -} - -static PyObject * -buildProofTransform(PyObject *self, PyObject *args) { - CmsProfileObject *pInputProfile; - CmsProfileObject *pOutputProfile; - CmsProfileObject *pProofProfile; - char *sInMode; - char *sOutMode; - int iRenderingIntent = 0; - int iProofIntent = 0; - int cmsFLAGS = 0; - - cmsHTRANSFORM transform = NULL; - - if (!PyArg_ParseTuple( - args, - "O!O!O!ss|iii:buildProofTransform", - &CmsProfile_Type, - &pInputProfile, - &CmsProfile_Type, - &pOutputProfile, - &CmsProfile_Type, - &pProofProfile, - &sInMode, - &sOutMode, - &iRenderingIntent, - &iProofIntent, - &cmsFLAGS)) { - return NULL; - } - - transform = _buildProofTransform( - pInputProfile->profile, - pOutputProfile->profile, - pProofProfile->profile, - sInMode, - sOutMode, - iRenderingIntent, - iProofIntent, - cmsFLAGS); - - if (!transform) { - return NULL; - } - - return cms_transform_new(transform); -} - -static PyObject * -cms_transform_apply(CmsTransformObject *self, PyObject *args) { - Py_ssize_t idIn; - Py_ssize_t idOut; - Imaging im; - Imaging imOut; - - int result; - - if (!PyArg_ParseTuple(args, "nn:apply", &idIn, &idOut)) { - return NULL; - } - - im = (Imaging)idIn; - imOut = (Imaging)idOut; - - result = pyCMSdoTransform(im, imOut, self->transform); - - return Py_BuildValue("i", result); -} - -/* -------------------------------------------------------------------- */ -/* Python-Callable On-The-Fly profile creation functions */ - -static PyObject * -createProfile(PyObject *self, PyObject *args) { - char *sColorSpace; - cmsHPROFILE hProfile; - cmsFloat64Number dColorTemp = 0.0; - cmsCIExyY whitePoint; - cmsBool result; - - if (!PyArg_ParseTuple(args, "s|d:createProfile", &sColorSpace, &dColorTemp)) { - return NULL; - } - - if (strcmp(sColorSpace, "LAB") == 0) { - if (dColorTemp > 0.0) { - result = cmsWhitePointFromTemp(&whitePoint, dColorTemp); - if (!result) { - PyErr_SetString( - PyExc_ValueError, - "ERROR: Could not calculate white point from color temperature " - "provided, must be float in degrees Kelvin"); - return NULL; - } - hProfile = cmsCreateLab2Profile(&whitePoint); - } else { - hProfile = cmsCreateLab2Profile(NULL); - } - } else if (strcmp(sColorSpace, "XYZ") == 0) { - hProfile = cmsCreateXYZProfile(); - } else if (strcmp(sColorSpace, "sRGB") == 0) { - hProfile = cmsCreate_sRGBProfile(); - } else { - hProfile = NULL; - } - - if (!hProfile) { - PyErr_SetString(PyExc_ValueError, "failed to create requested color space"); - return NULL; - } - - return cms_profile_new(hProfile); -} - -/* -------------------------------------------------------------------- */ -/* profile methods */ - -static PyObject * -cms_profile_is_intent_supported(CmsProfileObject *self, PyObject *args) { - cmsBool result; - - int intent; - int direction; - if (!PyArg_ParseTuple(args, "ii:is_intent_supported", &intent, &direction)) { - return NULL; - } - - result = cmsIsIntentSupported(self->profile, intent, direction); - - /* printf("cmsIsIntentSupported(%p, %d, %d) => %d\n", self->profile, intent, - * direction, result); */ - - return PyLong_FromLong(result != 0); -} - -#ifdef _WIN32 - -#ifdef _WIN64 -#define F_HANDLE "K" -#else -#define F_HANDLE "k" -#endif - -static PyObject * -cms_get_display_profile_win32(PyObject *self, PyObject *args) { - char filename[MAX_PATH]; - DWORD filename_size; - BOOL ok; - - HANDLE handle = 0; - int is_dc = 0; - if (!PyArg_ParseTuple( - args, "|" F_HANDLE "i:get_display_profile", &handle, &is_dc)) { - return NULL; - } - - filename_size = sizeof(filename); - - if (is_dc) { - ok = GetICMProfile((HDC)handle, &filename_size, filename); - } else { - HDC dc = GetDC((HWND)handle); - ok = GetICMProfile(dc, &filename_size, filename); - ReleaseDC((HWND)handle, dc); - } - - if (ok) { - return PyUnicode_FromStringAndSize(filename, filename_size - 1); - } - - Py_INCREF(Py_None); - return Py_None; -} -#endif - -/* -------------------------------------------------------------------- */ -/* Helper functions. */ - -static PyObject * -_profile_read_mlu(CmsProfileObject *self, cmsTagSignature info) { - PyObject *uni; - char *lc = "en"; - char *cc = cmsNoCountry; - cmsMLU *mlu; - cmsUInt32Number len; - wchar_t *buf; - - if (!cmsIsTag(self->profile, info)) { - Py_INCREF(Py_None); - return Py_None; - } - - mlu = cmsReadTag(self->profile, info); - if (!mlu) { - Py_INCREF(Py_None); - return Py_None; - } - - len = cmsMLUgetWide(mlu, lc, cc, NULL, 0); - if (len == 0) { - Py_INCREF(Py_None); - return Py_None; - } - - buf = malloc(len); - if (!buf) { - PyErr_SetString(PyExc_OSError, "Out of Memory"); - return NULL; - } - /* Just in case the next call fails. */ - buf[0] = '\0'; - - cmsMLUgetWide(mlu, lc, cc, buf, len); - // buf contains additional junk after \0 - uni = PyUnicode_FromWideChar(buf, wcslen(buf)); - free(buf); - - return uni; -} - -static PyObject * -_profile_read_int_as_string(cmsUInt32Number nr) { - PyObject *ret; - char buf[5]; - buf[0] = (char)((nr >> 24) & 0xff); - buf[1] = (char)((nr >> 16) & 0xff); - buf[2] = (char)((nr >> 8) & 0xff); - buf[3] = (char)(nr & 0xff); - buf[4] = 0; - - ret = PyUnicode_DecodeASCII(buf, 4, NULL); - return ret; -} - -static PyObject * -_profile_read_signature(CmsProfileObject *self, cmsTagSignature info) { - unsigned int *sig; - - if (!cmsIsTag(self->profile, info)) { - Py_INCREF(Py_None); - return Py_None; - } - - sig = (unsigned int *)cmsReadTag(self->profile, info); - if (!sig) { - Py_INCREF(Py_None); - return Py_None; - } - - return _profile_read_int_as_string(*sig); -} - -static PyObject * -_xyz_py(cmsCIEXYZ *XYZ) { - cmsCIExyY xyY; - cmsXYZ2xyY(&xyY, XYZ); - return Py_BuildValue( - "((d,d,d),(d,d,d))", XYZ->X, XYZ->Y, XYZ->Z, xyY.x, xyY.y, xyY.Y); -} - -static PyObject * -_xyz3_py(cmsCIEXYZ *XYZ) { - cmsCIExyY xyY[3]; - cmsXYZ2xyY(&xyY[0], &XYZ[0]); - cmsXYZ2xyY(&xyY[1], &XYZ[1]); - cmsXYZ2xyY(&xyY[2], &XYZ[2]); - - return Py_BuildValue( - "(((d,d,d),(d,d,d),(d,d,d)),((d,d,d),(d,d,d),(d,d,d)))", - XYZ[0].X, - XYZ[0].Y, - XYZ[0].Z, - XYZ[1].X, - XYZ[1].Y, - XYZ[1].Z, - XYZ[2].X, - XYZ[2].Y, - XYZ[2].Z, - xyY[0].x, - xyY[0].y, - xyY[0].Y, - xyY[1].x, - xyY[1].y, - xyY[1].Y, - xyY[2].x, - xyY[2].y, - xyY[2].Y); -} - -static PyObject * -_profile_read_ciexyz(CmsProfileObject *self, cmsTagSignature info, int multi) { - cmsCIEXYZ *XYZ; - - if (!cmsIsTag(self->profile, info)) { - Py_INCREF(Py_None); - return Py_None; - } - - XYZ = (cmsCIEXYZ *)cmsReadTag(self->profile, info); - if (!XYZ) { - Py_INCREF(Py_None); - return Py_None; - } - if (multi) { - return _xyz3_py(XYZ); - } else { - return _xyz_py(XYZ); - } -} - -static PyObject * -_profile_read_ciexyy_triple(CmsProfileObject *self, cmsTagSignature info) { - cmsCIExyYTRIPLE *triple; - - if (!cmsIsTag(self->profile, info)) { - Py_INCREF(Py_None); - return Py_None; - } - - triple = (cmsCIExyYTRIPLE *)cmsReadTag(self->profile, info); - if (!triple) { - Py_INCREF(Py_None); - return Py_None; - } - - /* Note: lcms does all the heavy lifting and error checking (nr of - channels == 3). */ - return Py_BuildValue( - "((d,d,d),(d,d,d),(d,d,d)),", - triple->Red.x, - triple->Red.y, - triple->Red.Y, - triple->Green.x, - triple->Green.y, - triple->Green.Y, - triple->Blue.x, - triple->Blue.y, - triple->Blue.Y); -} - -static PyObject * -_profile_read_named_color_list(CmsProfileObject *self, cmsTagSignature info) { - cmsNAMEDCOLORLIST *ncl; - int i, n; - char name[cmsMAX_PATH]; - PyObject *result; - - if (!cmsIsTag(self->profile, info)) { - Py_INCREF(Py_None); - return Py_None; - } - - ncl = (cmsNAMEDCOLORLIST *)cmsReadTag(self->profile, info); - if (ncl == NULL) { - Py_INCREF(Py_None); - return Py_None; - } - - n = cmsNamedColorCount(ncl); - result = PyList_New(n); - if (!result) { - Py_INCREF(Py_None); - return Py_None; - } - - for (i = 0; i < n; i++) { - PyObject *str; - cmsNamedColorInfo(ncl, i, name, NULL, NULL, NULL, NULL); - str = PyUnicode_FromString(name); - if (str == NULL) { - Py_DECREF(result); - Py_INCREF(Py_None); - return Py_None; - } - PyList_SET_ITEM(result, i, str); - } - - return result; -} - -static cmsBool -_calculate_rgb_primaries(CmsProfileObject *self, cmsCIEXYZTRIPLE *result) { - double input[3][3] = {{1, 0, 0}, {0, 1, 0}, {0, 0, 1}}; - cmsHPROFILE hXYZ; - cmsHTRANSFORM hTransform; - - /* https://littlecms2.blogspot.com/2009/07/less-is-more.html */ - - // double array of RGB values with max on each identity - hXYZ = cmsCreateXYZProfile(); - if (hXYZ == NULL) { - return 0; - } - - // transform from our profile to XYZ using doubles for highest precision - hTransform = cmsCreateTransform( - self->profile, - TYPE_RGB_DBL, - hXYZ, - TYPE_XYZ_DBL, - INTENT_RELATIVE_COLORIMETRIC, - cmsFLAGS_NOCACHE | cmsFLAGS_NOOPTIMIZE); - cmsCloseProfile(hXYZ); - if (hTransform == NULL) { - return 0; - } - - cmsDoTransform(hTransform, (void *)input, result, 3); - cmsDeleteTransform(hTransform); - return 1; -} - -static cmsBool -_check_intent( - int clut, - cmsHPROFILE hProfile, - cmsUInt32Number Intent, - cmsUInt32Number UsedDirection) { - if (clut) { - return cmsIsCLUT(hProfile, Intent, UsedDirection); - } else { - return cmsIsIntentSupported(hProfile, Intent, UsedDirection); - } -} - -#define INTENTS 200 - -static PyObject * -_is_intent_supported(CmsProfileObject *self, int clut) { - PyObject *result; - int n; - int i; - cmsUInt32Number intent_ids[INTENTS]; - char *intent_descs[INTENTS]; - - result = PyDict_New(); - if (result == NULL) { - Py_INCREF(Py_None); - return Py_None; - } - - n = cmsGetSupportedIntents(INTENTS, intent_ids, intent_descs); - for (i = 0; i < n; i++) { - int intent = (int)intent_ids[i]; - PyObject *id; - PyObject *entry; - - /* Only valid for ICC Intents (otherwise we read invalid memory in lcms - * cmsio1.c). */ - if (!(intent == INTENT_PERCEPTUAL || intent == INTENT_RELATIVE_COLORIMETRIC || - intent == INTENT_SATURATION || intent == INTENT_ABSOLUTE_COLORIMETRIC)) { - continue; - } - - id = PyLong_FromLong((long)intent); - entry = Py_BuildValue( - "(OOO)", - _check_intent(clut, self->profile, intent, LCMS_USED_AS_INPUT) ? Py_True - : Py_False, - _check_intent(clut, self->profile, intent, LCMS_USED_AS_OUTPUT) ? Py_True - : Py_False, - _check_intent(clut, self->profile, intent, LCMS_USED_AS_PROOF) ? Py_True - : Py_False); - if (id == NULL || entry == NULL) { - Py_XDECREF(id); - Py_XDECREF(entry); - Py_XDECREF(result); - Py_INCREF(Py_None); - return Py_None; - } - PyDict_SetItem(result, id, entry); - Py_DECREF(id); - Py_DECREF(entry); - } - return result; -} - -/* -------------------------------------------------------------------- */ -/* Python interface setup */ - -static PyMethodDef pyCMSdll_methods[] = { - - {"profile_open", cms_profile_open, METH_VARARGS}, - {"profile_frombytes", cms_profile_frombytes, METH_VARARGS}, - {"profile_tobytes", cms_profile_tobytes, METH_VARARGS}, - - /* profile and transform functions */ - {"buildTransform", buildTransform, METH_VARARGS}, - {"buildProofTransform", buildProofTransform, METH_VARARGS}, - {"createProfile", createProfile, METH_VARARGS}, - -/* platform specific tools */ -#ifdef _WIN32 - {"get_display_profile_win32", cms_get_display_profile_win32, METH_VARARGS}, -#endif - - {NULL, NULL}}; - -static struct PyMethodDef cms_profile_methods[] = { - {"is_intent_supported", (PyCFunction)cms_profile_is_intent_supported, METH_VARARGS}, - {NULL, NULL} /* sentinel */ -}; - -static PyObject * -cms_profile_getattr_rendering_intent(CmsProfileObject *self, void *closure) { - return PyLong_FromLong(cmsGetHeaderRenderingIntent(self->profile)); -} - -/* New-style unicode interfaces. */ -static PyObject * -cms_profile_getattr_copyright(CmsProfileObject *self, void *closure) { - return _profile_read_mlu(self, cmsSigCopyrightTag); -} - -static PyObject * -cms_profile_getattr_target(CmsProfileObject *self, void *closure) { - return _profile_read_mlu(self, cmsSigCharTargetTag); -} - -static PyObject * -cms_profile_getattr_manufacturer(CmsProfileObject *self, void *closure) { - return _profile_read_mlu(self, cmsSigDeviceMfgDescTag); -} - -static PyObject * -cms_profile_getattr_model(CmsProfileObject *self, void *closure) { - return _profile_read_mlu(self, cmsSigDeviceModelDescTag); -} - -static PyObject * -cms_profile_getattr_profile_description(CmsProfileObject *self, void *closure) { - return _profile_read_mlu(self, cmsSigProfileDescriptionTag); -} - -static PyObject * -cms_profile_getattr_screening_description(CmsProfileObject *self, void *closure) { - return _profile_read_mlu(self, cmsSigScreeningDescTag); -} - -static PyObject * -cms_profile_getattr_viewing_condition(CmsProfileObject *self, void *closure) { - return _profile_read_mlu(self, cmsSigViewingCondDescTag); -} - -static PyObject * -cms_profile_getattr_creation_date(CmsProfileObject *self, void *closure) { - cmsBool result; - struct tm ct; - - result = cmsGetHeaderCreationDateTime(self->profile, &ct); - if (!result) { - Py_INCREF(Py_None); - return Py_None; - } - - return PyDateTime_FromDateAndTime( - 1900 + ct.tm_year, ct.tm_mon, ct.tm_mday, ct.tm_hour, ct.tm_min, ct.tm_sec, 0); -} - -static PyObject * -cms_profile_getattr_version(CmsProfileObject *self, void *closure) { - cmsFloat64Number version = cmsGetProfileVersion(self->profile); - return PyFloat_FromDouble(version); -} - -static PyObject * -cms_profile_getattr_icc_version(CmsProfileObject *self, void *closure) { - return PyLong_FromLong((long)cmsGetEncodedICCversion(self->profile)); -} - -static PyObject * -cms_profile_getattr_attributes(CmsProfileObject *self, void *closure) { - cmsUInt64Number attr; - cmsGetHeaderAttributes(self->profile, &attr); - /* This works just as well on Windows (LLP64), 32-bit Linux - (ILP32) and 64-bit Linux (LP64) systems. */ - return PyLong_FromUnsignedLongLong((unsigned long long)attr); -} - -static PyObject * -cms_profile_getattr_header_flags(CmsProfileObject *self, void *closure) { - cmsUInt32Number flags = cmsGetHeaderFlags(self->profile); - return PyLong_FromLong(flags); -} - -static PyObject * -cms_profile_getattr_header_manufacturer(CmsProfileObject *self, void *closure) { - return _profile_read_int_as_string(cmsGetHeaderManufacturer(self->profile)); -} - -static PyObject * -cms_profile_getattr_header_model(CmsProfileObject *self, void *closure) { - return _profile_read_int_as_string(cmsGetHeaderModel(self->profile)); -} - -static PyObject * -cms_profile_getattr_device_class(CmsProfileObject *self, void *closure) { - return _profile_read_int_as_string(cmsGetDeviceClass(self->profile)); -} - -static PyObject * -cms_profile_getattr_connection_space(CmsProfileObject *self, void *closure) { - return _profile_read_int_as_string(cmsGetPCS(self->profile)); -} - -static PyObject * -cms_profile_getattr_xcolor_space(CmsProfileObject *self, void *closure) { - return _profile_read_int_as_string(cmsGetColorSpace(self->profile)); -} - -static PyObject * -cms_profile_getattr_profile_id(CmsProfileObject *self, void *closure) { - cmsUInt8Number id[16]; - cmsGetHeaderProfileID(self->profile, id); - return PyBytes_FromStringAndSize((char *)id, 16); -} - -static PyObject * -cms_profile_getattr_is_matrix_shaper(CmsProfileObject *self, void *closure) { - return PyBool_FromLong((long)cmsIsMatrixShaper(self->profile)); -} - -static PyObject * -cms_profile_getattr_technology(CmsProfileObject *self, void *closure) { - return _profile_read_signature(self, cmsSigTechnologyTag); -} - -static PyObject * -cms_profile_getattr_colorimetric_intent(CmsProfileObject *self, void *closure) { - return _profile_read_signature(self, cmsSigColorimetricIntentImageStateTag); -} - -static PyObject * -cms_profile_getattr_perceptual_rendering_intent_gamut( - CmsProfileObject *self, void *closure) { - return _profile_read_signature(self, cmsSigPerceptualRenderingIntentGamutTag); -} - -static PyObject * -cms_profile_getattr_saturation_rendering_intent_gamut( - CmsProfileObject *self, void *closure) { - return _profile_read_signature(self, cmsSigSaturationRenderingIntentGamutTag); -} - -static PyObject * -cms_profile_getattr_red_colorant(CmsProfileObject *self, void *closure) { - if (!cmsIsMatrixShaper(self->profile)) { - Py_INCREF(Py_None); - return Py_None; - } - return _profile_read_ciexyz(self, cmsSigRedColorantTag, 0); -} - -static PyObject * -cms_profile_getattr_green_colorant(CmsProfileObject *self, void *closure) { - if (!cmsIsMatrixShaper(self->profile)) { - Py_INCREF(Py_None); - return Py_None; - } - return _profile_read_ciexyz(self, cmsSigGreenColorantTag, 0); -} - -static PyObject * -cms_profile_getattr_blue_colorant(CmsProfileObject *self, void *closure) { - if (!cmsIsMatrixShaper(self->profile)) { - Py_INCREF(Py_None); - return Py_None; - } - return _profile_read_ciexyz(self, cmsSigBlueColorantTag, 0); -} - -static PyObject * -cms_profile_getattr_media_white_point_temperature( - CmsProfileObject *self, void *closure) { - cmsCIEXYZ *XYZ; - cmsCIExyY xyY; - cmsFloat64Number tempK; - cmsTagSignature info = cmsSigMediaWhitePointTag; - cmsBool result; - - if (!cmsIsTag(self->profile, info)) { - Py_INCREF(Py_None); - return Py_None; - } - - XYZ = (cmsCIEXYZ *)cmsReadTag(self->profile, info); - if (XYZ == NULL || XYZ->X == 0) { - Py_INCREF(Py_None); - return Py_None; - } - - cmsXYZ2xyY(&xyY, XYZ); - result = cmsTempFromWhitePoint(&tempK, &xyY); - if (!result) { - Py_INCREF(Py_None); - return Py_None; - } - return PyFloat_FromDouble(tempK); -} - -static PyObject * -cms_profile_getattr_media_white_point(CmsProfileObject *self, void *closure) { - return _profile_read_ciexyz(self, cmsSigMediaWhitePointTag, 0); -} - -static PyObject * -cms_profile_getattr_media_black_point(CmsProfileObject *self, void *closure) { - return _profile_read_ciexyz(self, cmsSigMediaBlackPointTag, 0); -} - -static PyObject * -cms_profile_getattr_luminance(CmsProfileObject *self, void *closure) { - return _profile_read_ciexyz(self, cmsSigLuminanceTag, 0); -} - -static PyObject * -cms_profile_getattr_chromatic_adaptation(CmsProfileObject *self, void *closure) { - return _profile_read_ciexyz(self, cmsSigChromaticAdaptationTag, 1); -} - -static PyObject * -cms_profile_getattr_chromaticity(CmsProfileObject *self, void *closure) { - return _profile_read_ciexyy_triple(self, cmsSigChromaticityTag); -} - -static PyObject * -cms_profile_getattr_red_primary(CmsProfileObject *self, void *closure) { - cmsBool result = 0; - cmsCIEXYZTRIPLE primaries; - - if (cmsIsMatrixShaper(self->profile)) { - result = _calculate_rgb_primaries(self, &primaries); - } - if (!result) { - Py_INCREF(Py_None); - return Py_None; - } - - return _xyz_py(&primaries.Red); -} - -static PyObject * -cms_profile_getattr_green_primary(CmsProfileObject *self, void *closure) { - cmsBool result = 0; - cmsCIEXYZTRIPLE primaries; - - if (cmsIsMatrixShaper(self->profile)) { - result = _calculate_rgb_primaries(self, &primaries); - } - if (!result) { - Py_INCREF(Py_None); - return Py_None; - } - - return _xyz_py(&primaries.Green); -} - -static PyObject * -cms_profile_getattr_blue_primary(CmsProfileObject *self, void *closure) { - cmsBool result = 0; - cmsCIEXYZTRIPLE primaries; - - if (cmsIsMatrixShaper(self->profile)) { - result = _calculate_rgb_primaries(self, &primaries); - } - if (!result) { - Py_INCREF(Py_None); - return Py_None; - } - - return _xyz_py(&primaries.Blue); -} - -static PyObject * -cms_profile_getattr_colorant_table(CmsProfileObject *self, void *closure) { - return _profile_read_named_color_list(self, cmsSigColorantTableTag); -} - -static PyObject * -cms_profile_getattr_colorant_table_out(CmsProfileObject *self, void *closure) { - return _profile_read_named_color_list(self, cmsSigColorantTableOutTag); -} - -static PyObject * -cms_profile_getattr_is_intent_supported(CmsProfileObject *self, void *closure) { - return _is_intent_supported(self, 0); -} - -static PyObject * -cms_profile_getattr_is_clut(CmsProfileObject *self, void *closure) { - return _is_intent_supported(self, 1); -} - -static const char * -_illu_map(int i) { - switch (i) { - case 0: - return "unknown"; - case 1: - return "D50"; - case 2: - return "D65"; - case 3: - return "D93"; - case 4: - return "F2"; - case 5: - return "D55"; - case 6: - return "A"; - case 7: - return "E"; - case 8: - return "F8"; - default: - return NULL; - } -} - -static PyObject * -cms_profile_getattr_icc_measurement_condition(CmsProfileObject *self, void *closure) { - cmsICCMeasurementConditions *mc; - cmsTagSignature info = cmsSigMeasurementTag; - const char *geo; - - if (!cmsIsTag(self->profile, info)) { - Py_INCREF(Py_None); - return Py_None; - } - - mc = (cmsICCMeasurementConditions *)cmsReadTag(self->profile, info); - if (!mc) { - Py_INCREF(Py_None); - return Py_None; - } - - if (mc->Geometry == 1) { - geo = "45/0, 0/45"; - } else if (mc->Geometry == 2) { - geo = "0d, d/0"; - } else { - geo = "unknown"; - } - - return Py_BuildValue( - "{s:i,s:(ddd),s:s,s:d,s:s}", - "observer", - mc->Observer, - "backing", - mc->Backing.X, - mc->Backing.Y, - mc->Backing.Z, - "geo", - geo, - "flare", - mc->Flare, - "illuminant_type", - _illu_map(mc->IlluminantType)); -} - -static PyObject * -cms_profile_getattr_icc_viewing_condition(CmsProfileObject *self, void *closure) { - cmsICCViewingConditions *vc; - cmsTagSignature info = cmsSigViewingConditionsTag; - - if (!cmsIsTag(self->profile, info)) { - Py_INCREF(Py_None); - return Py_None; - } - - vc = (cmsICCViewingConditions *)cmsReadTag(self->profile, info); - if (!vc) { - Py_INCREF(Py_None); - return Py_None; - } - - return Py_BuildValue( - "{s:(ddd),s:(ddd),s:s}", - "illuminant", - vc->IlluminantXYZ.X, - vc->IlluminantXYZ.Y, - vc->IlluminantXYZ.Z, - "surround", - vc->SurroundXYZ.X, - vc->SurroundXYZ.Y, - vc->SurroundXYZ.Z, - "illuminant_type", - _illu_map(vc->IlluminantType)); -} - -static struct PyGetSetDef cms_profile_getsetters[] = { - /* New style interfaces. */ - {"rendering_intent", (getter)cms_profile_getattr_rendering_intent}, - {"creation_date", (getter)cms_profile_getattr_creation_date}, - {"copyright", (getter)cms_profile_getattr_copyright}, - {"target", (getter)cms_profile_getattr_target}, - {"manufacturer", (getter)cms_profile_getattr_manufacturer}, - {"model", (getter)cms_profile_getattr_model}, - {"profile_description", (getter)cms_profile_getattr_profile_description}, - {"screening_description", (getter)cms_profile_getattr_screening_description}, - {"viewing_condition", (getter)cms_profile_getattr_viewing_condition}, - {"version", (getter)cms_profile_getattr_version}, - {"icc_version", (getter)cms_profile_getattr_icc_version}, - {"attributes", (getter)cms_profile_getattr_attributes}, - {"header_flags", (getter)cms_profile_getattr_header_flags}, - {"header_manufacturer", (getter)cms_profile_getattr_header_manufacturer}, - {"header_model", (getter)cms_profile_getattr_header_model}, - {"device_class", (getter)cms_profile_getattr_device_class}, - {"connection_space", (getter)cms_profile_getattr_connection_space}, - {"xcolor_space", (getter)cms_profile_getattr_xcolor_space}, - {"profile_id", (getter)cms_profile_getattr_profile_id}, - {"is_matrix_shaper", (getter)cms_profile_getattr_is_matrix_shaper}, - {"technology", (getter)cms_profile_getattr_technology}, - {"colorimetric_intent", (getter)cms_profile_getattr_colorimetric_intent}, - {"perceptual_rendering_intent_gamut", - (getter)cms_profile_getattr_perceptual_rendering_intent_gamut}, - {"saturation_rendering_intent_gamut", - (getter)cms_profile_getattr_saturation_rendering_intent_gamut}, - {"red_colorant", (getter)cms_profile_getattr_red_colorant}, - {"green_colorant", (getter)cms_profile_getattr_green_colorant}, - {"blue_colorant", (getter)cms_profile_getattr_blue_colorant}, - {"red_primary", (getter)cms_profile_getattr_red_primary}, - {"green_primary", (getter)cms_profile_getattr_green_primary}, - {"blue_primary", (getter)cms_profile_getattr_blue_primary}, - {"media_white_point_temperature", - (getter)cms_profile_getattr_media_white_point_temperature}, - {"media_white_point", (getter)cms_profile_getattr_media_white_point}, - {"media_black_point", (getter)cms_profile_getattr_media_black_point}, - {"luminance", (getter)cms_profile_getattr_luminance}, - {"chromatic_adaptation", (getter)cms_profile_getattr_chromatic_adaptation}, - {"chromaticity", (getter)cms_profile_getattr_chromaticity}, - {"colorant_table", (getter)cms_profile_getattr_colorant_table}, - {"colorant_table_out", (getter)cms_profile_getattr_colorant_table_out}, - {"intent_supported", (getter)cms_profile_getattr_is_intent_supported}, - {"clut", (getter)cms_profile_getattr_is_clut}, - {"icc_measurement_condition", - (getter)cms_profile_getattr_icc_measurement_condition}, - {"icc_viewing_condition", (getter)cms_profile_getattr_icc_viewing_condition}, - - {NULL}}; - -static PyTypeObject CmsProfile_Type = { - PyVarObject_HEAD_INIT(NULL, 0) "PIL.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*/ -}; - -static struct PyMethodDef cms_transform_methods[] = { - {"apply", (PyCFunction)cms_transform_apply, 1}, {NULL, NULL} /* sentinel */ -}; - -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*/ -}; - -static int -setup_module(PyObject *m) { - PyObject *d; - PyObject *v; - int vn; - - /* Ready object types */ - PyType_Ready(&CmsProfile_Type); - PyType_Ready(&CmsTransform_Type); - - Py_INCREF(&CmsProfile_Type); - PyModule_AddObject(m, "CmsProfile", (PyObject *)&CmsProfile_Type); - - Py_INCREF(&CmsTransform_Type); - PyModule_AddObject(m, "CmsTransform", (PyObject *)&CmsTransform_Type); - - d = PyModule_GetDict(m); - - /* this check is also in PIL.features.pilinfo() */ -#if LCMS_VERSION < 2070 - vn = LCMS_VERSION; -#else - vn = cmsGetEncodedCMMversion(); -#endif - if (vn % 10) { - v = PyUnicode_FromFormat("%d.%d.%d", vn / 1000, (vn / 10) % 100, vn % 10); - } else { - v = PyUnicode_FromFormat("%d.%d", vn / 1000, (vn / 10) % 100); - } - PyDict_SetItemString(d, "littlecms_version", v ? v : Py_None); - Py_XDECREF(v); - - return 0; -} - -PyMODINIT_FUNC -PyInit__imagingcms(void) { - PyObject *m; - - static PyModuleDef module_def = { - PyModuleDef_HEAD_INIT, - "_imagingcms", /* m_name */ - NULL, /* m_doc */ - -1, /* m_size */ - pyCMSdll_methods, /* m_methods */ - }; - - m = PyModule_Create(&module_def); - - if (setup_module(m) < 0) { - return NULL; - } - - PyDateTime_IMPORT; - - return m; -} +/* + * pyCMS + * a Python / PIL interface to the littleCMS ICC Color Management System + * Copyright (C) 2002-2003 Kevin Cazabon + * kevin@cazabon.com + * https://www.cazabon.com + * Adapted/reworked for PIL by Fredrik Lundh + * Copyright (c) 2009 Fredrik Lundh + * Updated to LCMS2 + * Copyright (c) 2013 Eric Soroos + * + * pyCMS home page: https://www.cazabon.com/pyCMS + * littleCMS home page: https://www.littlecms.com + * (littleCMS is Copyright (C) 1998-2001 Marti Maria) + * + * Originally released under LGPL. Graciously donated to PIL in + * March 2009, for distribution under the standard PIL license + */ + +#define COPYRIGHTINFO \ + "\ +pyCMS\n\ +a Python / PIL interface to the littleCMS ICC Color Management System\n\ +Copyright (C) 2002-2003 Kevin Cazabon\n\ +kevin@cazabon.com\n\ +https://www.cazabon.com\n\ +" + +#define PY_SSIZE_T_CLEAN +#include "Python.h" // Include before wchar.h so _GNU_SOURCE is set +#include "wchar.h" +#include "datetime.h" + +#include "lcms2.h" +#include "libImaging/Imaging.h" + +#define PYCMSVERSION "1.0.0 pil" + +/* version history */ + +/* + 1.0.0 pil Integrating littleCMS2 + 0.1.0 pil integration & refactoring + 0.0.2 alpha: Minor updates, added interfaces to littleCMS features, Jan 6, 2003 + - fixed some memory holes in how transforms/profiles were created and passed back to + Python due to improper destructor setup for PyCObjects + - added buildProofTransformFromOpenProfiles() function + - eliminated some code redundancy, centralizing several common tasks with internal + functions + + 0.0.1 alpha: First public release Dec 26, 2002 + +*/ + +/* known to-do list: + + Verify that PILmode->littleCMStype conversion in findLCMStype is correct for all + PIL modes (it probably isn't for the more obscure ones) + + Add support for creating custom RGB profiles on the fly + Add support for checking presence of a specific tag in a profile + Add support for other littleCMS features as required + +*/ + +/* + INTENT_PERCEPTUAL 0 + INTENT_RELATIVE_COLORIMETRIC 1 + INTENT_SATURATION 2 + INTENT_ABSOLUTE_COLORIMETRIC 3 +*/ + +/* -------------------------------------------------------------------- */ +/* wrapper classes */ + +/* a profile represents the ICC characteristics for a specific device */ + +typedef struct { + PyObject_HEAD cmsHPROFILE profile; +} CmsProfileObject; + +static PyTypeObject CmsProfile_Type; + +#define CmsProfile_Check(op) (Py_TYPE(op) == &CmsProfile_Type) + +static PyObject * +cms_profile_new(cmsHPROFILE profile) { + CmsProfileObject *self; + + self = PyObject_New(CmsProfileObject, &CmsProfile_Type); + if (!self) { + return NULL; + } + + self->profile = profile; + + return (PyObject *)self; +} + +static PyObject * +cms_profile_open(PyObject *self, PyObject *args) { + cmsHPROFILE hProfile; + + char *sProfile; + if (!PyArg_ParseTuple(args, "s:profile_open", &sProfile)) { + return NULL; + } + + hProfile = cmsOpenProfileFromFile(sProfile, "r"); + if (!hProfile) { + PyErr_SetString(PyExc_OSError, "cannot open profile file"); + return NULL; + } + + return cms_profile_new(hProfile); +} + +static PyObject * +cms_profile_frombytes(PyObject *self, PyObject *args) { + cmsHPROFILE hProfile; + + char *pProfile; + Py_ssize_t nProfile; + if (!PyArg_ParseTuple(args, "y#:profile_frombytes", &pProfile, &nProfile)) { + return NULL; + } + + hProfile = cmsOpenProfileFromMem(pProfile, nProfile); + if (!hProfile) { + PyErr_SetString(PyExc_OSError, "cannot open profile from string"); + return NULL; + } + + return cms_profile_new(hProfile); +} + +static PyObject * +cms_profile_tobytes(PyObject *self, PyObject *args) { + char *pProfile = NULL; + cmsUInt32Number nProfile; + PyObject *CmsProfile; + + cmsHPROFILE *profile; + + PyObject *ret; + if (!PyArg_ParseTuple(args, "O!", &CmsProfile_Type, &CmsProfile)) { + return NULL; + } + + profile = ((CmsProfileObject *)CmsProfile)->profile; + + if (!cmsSaveProfileToMem(profile, pProfile, &nProfile)) { + PyErr_SetString(PyExc_OSError, "Could not determine profile size"); + return NULL; + } + + pProfile = (char *)malloc(nProfile); + if (!pProfile) { + PyErr_SetString(PyExc_OSError, "Out of Memory"); + return NULL; + } + + if (!cmsSaveProfileToMem(profile, pProfile, &nProfile)) { + PyErr_SetString(PyExc_OSError, "Could not get profile"); + free(pProfile); + return NULL; + } + + ret = PyBytes_FromStringAndSize(pProfile, (Py_ssize_t)nProfile); + + free(pProfile); + return ret; +} + +static void +cms_profile_dealloc(CmsProfileObject *self) { + (void)cmsCloseProfile(self->profile); + PyObject_Del(self); +} + +/* a transform represents the mapping between two profiles */ + +typedef struct { + PyObject_HEAD cmsHTRANSFORM transform; +} CmsTransformObject; + +static PyTypeObject CmsTransform_Type; + +#define CmsTransform_Check(op) (Py_TYPE(op) == &CmsTransform_Type) + +static PyObject * +cms_transform_new(cmsHTRANSFORM transform) { + CmsTransformObject *self; + + self = PyObject_New(CmsTransformObject, &CmsTransform_Type); + if (!self) { + return NULL; + } + + self->transform = transform; + + return (PyObject *)self; +} + +static void +cms_transform_dealloc(CmsTransformObject *self) { + cmsDeleteTransform(self->transform); + PyObject_Del(self); +} + +/* -------------------------------------------------------------------- */ +/* internal functions */ + +static cmsUInt32Number +findLCMStype(char *PILmode) { + if (strcmp(PILmode, "RGB") == 0 || strcmp(PILmode, "RGBA") == 0 || + strcmp(PILmode, "RGBX") == 0) { + return TYPE_RGBA_8; + } + if (strcmp(PILmode, "RGBA;16B") == 0) { + return TYPE_RGBA_16; + } + if (strcmp(PILmode, "CMYK") == 0) { + return TYPE_CMYK_8; + } + if (strcmp(PILmode, "L;16") == 0) { + return TYPE_GRAY_16; + } + if (strcmp(PILmode, "L;16B") == 0) { + return TYPE_GRAY_16_SE; + } + if (strcmp(PILmode, "YCCA") == 0 || strcmp(PILmode, "YCC") == 0) { + return TYPE_YCbCr_8; + } + if (strcmp(PILmode, "LAB") == 0) { + // LabX equivalent like ALab, but not reversed -- no #define in lcms2 + return (COLORSPACE_SH(PT_LabV2) | CHANNELS_SH(3) | BYTES_SH(1) | EXTRA_SH(1)); + } + /* presume "L" by default */ + return TYPE_GRAY_8; +} + +#define Cms_Min(a, b) ((a) < (b) ? (a) : (b)) + +static int +pyCMSgetAuxChannelChannel(cmsUInt32Number format, int auxChannelNdx) { + int numColors = T_CHANNELS(format); + int numExtras = T_EXTRA(format); + + if (T_SWAPFIRST(format) && T_DOSWAP(format)) { + // reverse order, before anything but last extra is shifted last + if (auxChannelNdx == numExtras - 1) { + return numColors + numExtras - 1; + } else { + return numExtras - 2 - auxChannelNdx; + } + } else if (T_SWAPFIRST(format)) { + // in order, after color channels, but last extra is shifted to first + if (auxChannelNdx == numExtras - 1) { + return 0; + } else { + return numColors + 1 + auxChannelNdx; + } + } else if (T_DOSWAP(format)) { + // reverse order, before anything + return numExtras - 1 - auxChannelNdx; + } else { + // in order, after color channels + return numColors + auxChannelNdx; + } +} + +static void +pyCMScopyAux(cmsHTRANSFORM hTransform, Imaging imDst, const Imaging imSrc) { + cmsUInt32Number dstLCMSFormat; + cmsUInt32Number srcLCMSFormat; + int numSrcExtras; + int numDstExtras; + int numExtras; + int ySize; + int xSize; + int channelSize; + int srcChunkSize; + int dstChunkSize; + int e; + + // trivially copied + if (imDst == imSrc) { + return; + } + + dstLCMSFormat = cmsGetTransformOutputFormat(hTransform); + srcLCMSFormat = cmsGetTransformInputFormat(hTransform); + + // currently, all Pillow formats are chunky formats, but check it anyway + if (T_PLANAR(dstLCMSFormat) || T_PLANAR(srcLCMSFormat)) { + return; + } + + // copy only if channel format is identical, except OPTIMIZED is ignored as it + // does not affect the aux channel + if (T_FLOAT(dstLCMSFormat) != T_FLOAT(srcLCMSFormat) || + T_FLAVOR(dstLCMSFormat) != T_FLAVOR(srcLCMSFormat) || + T_ENDIAN16(dstLCMSFormat) != T_ENDIAN16(srcLCMSFormat) || + T_BYTES(dstLCMSFormat) != T_BYTES(srcLCMSFormat)) { + return; + } + + numSrcExtras = T_EXTRA(srcLCMSFormat); + numDstExtras = T_EXTRA(dstLCMSFormat); + numExtras = Cms_Min(numSrcExtras, numDstExtras); + ySize = Cms_Min(imSrc->ysize, imDst->ysize); + xSize = Cms_Min(imSrc->xsize, imDst->xsize); + channelSize = T_BYTES(dstLCMSFormat); + srcChunkSize = (T_CHANNELS(srcLCMSFormat) + T_EXTRA(srcLCMSFormat)) * channelSize; + dstChunkSize = (T_CHANNELS(dstLCMSFormat) + T_EXTRA(dstLCMSFormat)) * channelSize; + + for (e = 0; e < numExtras; ++e) { + int y; + int dstChannel = pyCMSgetAuxChannelChannel(dstLCMSFormat, e); + int srcChannel = pyCMSgetAuxChannelChannel(srcLCMSFormat, e); + + for (y = 0; y < ySize; y++) { + int x; + char *pDstExtras = imDst->image[y] + dstChannel * channelSize; + const char *pSrcExtras = imSrc->image[y] + srcChannel * channelSize; + + for (x = 0; x < xSize; x++) { + memcpy( + pDstExtras + x * dstChunkSize, + pSrcExtras + x * srcChunkSize, + channelSize); + } + } + } +} + +static int +pyCMSdoTransform(Imaging im, Imaging imOut, cmsHTRANSFORM hTransform) { + int i; + + if (im->xsize > imOut->xsize || im->ysize > imOut->ysize) { + return -1; + } + + Py_BEGIN_ALLOW_THREADS + + // transform color channels only + for (i = 0; i < im->ysize; i++) { + cmsDoTransform(hTransform, im->image[i], imOut->image[i], im->xsize); + } + + // lcms by default does nothing to the auxiliary channels leaving those + // unchanged. To do "the right thing" here, i.e. maintain identical results + // with and without inPlace, we replicate those channels to the output. + // + // As of lcms 2.8, a new cmsFLAGS_COPY_ALPHA flag is introduced which would + // do the same thing automagically. Unfortunately, lcms2.8 is not yet widely + // enough available on all platforms, so we polyfill it here for now. + pyCMScopyAux(hTransform, imOut, im); + + Py_END_ALLOW_THREADS + + return 0; +} + +static cmsHTRANSFORM +_buildTransform( + cmsHPROFILE hInputProfile, + cmsHPROFILE hOutputProfile, + char *sInMode, + char *sOutMode, + int iRenderingIntent, + cmsUInt32Number cmsFLAGS) { + cmsHTRANSFORM hTransform; + + Py_BEGIN_ALLOW_THREADS + + /* create the transform */ + hTransform = cmsCreateTransform( + hInputProfile, + findLCMStype(sInMode), + hOutputProfile, + findLCMStype(sOutMode), + iRenderingIntent, + cmsFLAGS); + + Py_END_ALLOW_THREADS; + + if (!hTransform) { + PyErr_SetString(PyExc_ValueError, "cannot build transform"); + } + + return hTransform; /* if NULL, an exception is set */ +} + +static cmsHTRANSFORM +_buildProofTransform( + cmsHPROFILE hInputProfile, + cmsHPROFILE hOutputProfile, + cmsHPROFILE hProofProfile, + char *sInMode, + char *sOutMode, + int iRenderingIntent, + int iProofIntent, + cmsUInt32Number cmsFLAGS) { + cmsHTRANSFORM hTransform; + + Py_BEGIN_ALLOW_THREADS + + /* create the transform */ + hTransform = cmsCreateProofingTransform( + hInputProfile, + findLCMStype(sInMode), + hOutputProfile, + findLCMStype(sOutMode), + hProofProfile, + iRenderingIntent, + iProofIntent, + cmsFLAGS); + + Py_END_ALLOW_THREADS; + + if (!hTransform) { + PyErr_SetString(PyExc_ValueError, "cannot build proof transform"); + } + + return hTransform; /* if NULL, an exception is set */ +} + +/* -------------------------------------------------------------------- */ +/* Python callable functions */ + +static PyObject * +buildTransform(PyObject *self, PyObject *args) { + CmsProfileObject *pInputProfile; + CmsProfileObject *pOutputProfile; + char *sInMode; + char *sOutMode; + int iRenderingIntent = 0; + int cmsFLAGS = 0; + + cmsHTRANSFORM transform = NULL; + + if (!PyArg_ParseTuple( + args, + "O!O!ss|ii:buildTransform", + &CmsProfile_Type, + &pInputProfile, + &CmsProfile_Type, + &pOutputProfile, + &sInMode, + &sOutMode, + &iRenderingIntent, + &cmsFLAGS)) { + return NULL; + } + + transform = _buildTransform( + pInputProfile->profile, + pOutputProfile->profile, + sInMode, + sOutMode, + iRenderingIntent, + cmsFLAGS); + + if (!transform) { + return NULL; + } + + return cms_transform_new(transform); +} + +static PyObject * +buildProofTransform(PyObject *self, PyObject *args) { + CmsProfileObject *pInputProfile; + CmsProfileObject *pOutputProfile; + CmsProfileObject *pProofProfile; + char *sInMode; + char *sOutMode; + int iRenderingIntent = 0; + int iProofIntent = 0; + int cmsFLAGS = 0; + + cmsHTRANSFORM transform = NULL; + + if (!PyArg_ParseTuple( + args, + "O!O!O!ss|iii:buildProofTransform", + &CmsProfile_Type, + &pInputProfile, + &CmsProfile_Type, + &pOutputProfile, + &CmsProfile_Type, + &pProofProfile, + &sInMode, + &sOutMode, + &iRenderingIntent, + &iProofIntent, + &cmsFLAGS)) { + return NULL; + } + + transform = _buildProofTransform( + pInputProfile->profile, + pOutputProfile->profile, + pProofProfile->profile, + sInMode, + sOutMode, + iRenderingIntent, + iProofIntent, + cmsFLAGS); + + if (!transform) { + return NULL; + } + + return cms_transform_new(transform); +} + +static PyObject * +cms_transform_apply(CmsTransformObject *self, PyObject *args) { + Py_ssize_t idIn; + Py_ssize_t idOut; + Imaging im; + Imaging imOut; + + int result; + + if (!PyArg_ParseTuple(args, "nn:apply", &idIn, &idOut)) { + return NULL; + } + + im = (Imaging)idIn; + imOut = (Imaging)idOut; + + result = pyCMSdoTransform(im, imOut, self->transform); + + return Py_BuildValue("i", result); +} + +/* -------------------------------------------------------------------- */ +/* Python-Callable On-The-Fly profile creation functions */ + +static PyObject * +createProfile(PyObject *self, PyObject *args) { + char *sColorSpace; + cmsHPROFILE hProfile; + cmsFloat64Number dColorTemp = 0.0; + cmsCIExyY whitePoint; + cmsBool result; + + if (!PyArg_ParseTuple(args, "s|d:createProfile", &sColorSpace, &dColorTemp)) { + return NULL; + } + + if (strcmp(sColorSpace, "LAB") == 0) { + if (dColorTemp > 0.0) { + result = cmsWhitePointFromTemp(&whitePoint, dColorTemp); + if (!result) { + PyErr_SetString( + PyExc_ValueError, + "ERROR: Could not calculate white point from color temperature " + "provided, must be float in degrees Kelvin"); + return NULL; + } + hProfile = cmsCreateLab2Profile(&whitePoint); + } else { + hProfile = cmsCreateLab2Profile(NULL); + } + } else if (strcmp(sColorSpace, "XYZ") == 0) { + hProfile = cmsCreateXYZProfile(); + } else if (strcmp(sColorSpace, "sRGB") == 0) { + hProfile = cmsCreate_sRGBProfile(); + } else { + hProfile = NULL; + } + + if (!hProfile) { + PyErr_SetString(PyExc_ValueError, "failed to create requested color space"); + return NULL; + } + + return cms_profile_new(hProfile); +} + +/* -------------------------------------------------------------------- */ +/* profile methods */ + +static PyObject * +cms_profile_is_intent_supported(CmsProfileObject *self, PyObject *args) { + cmsBool result; + + int intent; + int direction; + if (!PyArg_ParseTuple(args, "ii:is_intent_supported", &intent, &direction)) { + return NULL; + } + + result = cmsIsIntentSupported(self->profile, intent, direction); + + /* printf("cmsIsIntentSupported(%p, %d, %d) => %d\n", self->profile, intent, + * direction, result); */ + + return PyLong_FromLong(result != 0); +} + +#ifdef _WIN32 + +#ifdef _WIN64 +#define F_HANDLE "K" +#else +#define F_HANDLE "k" +#endif + +static PyObject * +cms_get_display_profile_win32(PyObject *self, PyObject *args) { + char filename[MAX_PATH]; + DWORD filename_size; + BOOL ok; + + HANDLE handle = 0; + int is_dc = 0; + if (!PyArg_ParseTuple( + args, "|" F_HANDLE "i:get_display_profile", &handle, &is_dc)) { + return NULL; + } + + filename_size = sizeof(filename); + + if (is_dc) { + ok = GetICMProfile((HDC)handle, &filename_size, filename); + } else { + HDC dc = GetDC((HWND)handle); + ok = GetICMProfile(dc, &filename_size, filename); + ReleaseDC((HWND)handle, dc); + } + + if (ok) { + return PyUnicode_FromStringAndSize(filename, filename_size - 1); + } + + Py_INCREF(Py_None); + return Py_None; +} +#endif + +/* -------------------------------------------------------------------- */ +/* Helper functions. */ + +static PyObject * +_profile_read_mlu(CmsProfileObject *self, cmsTagSignature info) { + PyObject *uni; + char *lc = "en"; + char *cc = cmsNoCountry; + cmsMLU *mlu; + cmsUInt32Number len; + wchar_t *buf; + + if (!cmsIsTag(self->profile, info)) { + Py_INCREF(Py_None); + return Py_None; + } + + mlu = cmsReadTag(self->profile, info); + if (!mlu) { + Py_INCREF(Py_None); + return Py_None; + } + + len = cmsMLUgetWide(mlu, lc, cc, NULL, 0); + if (len == 0) { + Py_INCREF(Py_None); + return Py_None; + } + + buf = malloc(len); + if (!buf) { + PyErr_SetString(PyExc_OSError, "Out of Memory"); + return NULL; + } + /* Just in case the next call fails. */ + buf[0] = '\0'; + + cmsMLUgetWide(mlu, lc, cc, buf, len); + // buf contains additional junk after \0 + uni = PyUnicode_FromWideChar(buf, wcslen(buf)); + free(buf); + + return uni; +} + +static PyObject * +_profile_read_int_as_string(cmsUInt32Number nr) { + PyObject *ret; + char buf[5]; + buf[0] = (char)((nr >> 24) & 0xff); + buf[1] = (char)((nr >> 16) & 0xff); + buf[2] = (char)((nr >> 8) & 0xff); + buf[3] = (char)(nr & 0xff); + buf[4] = 0; + + ret = PyUnicode_DecodeASCII(buf, 4, NULL); + return ret; +} + +static PyObject * +_profile_read_signature(CmsProfileObject *self, cmsTagSignature info) { + unsigned int *sig; + + if (!cmsIsTag(self->profile, info)) { + Py_INCREF(Py_None); + return Py_None; + } + + sig = (unsigned int *)cmsReadTag(self->profile, info); + if (!sig) { + Py_INCREF(Py_None); + return Py_None; + } + + return _profile_read_int_as_string(*sig); +} + +static PyObject * +_xyz_py(cmsCIEXYZ *XYZ) { + cmsCIExyY xyY; + cmsXYZ2xyY(&xyY, XYZ); + return Py_BuildValue( + "((d,d,d),(d,d,d))", XYZ->X, XYZ->Y, XYZ->Z, xyY.x, xyY.y, xyY.Y); +} + +static PyObject * +_xyz3_py(cmsCIEXYZ *XYZ) { + cmsCIExyY xyY[3]; + cmsXYZ2xyY(&xyY[0], &XYZ[0]); + cmsXYZ2xyY(&xyY[1], &XYZ[1]); + cmsXYZ2xyY(&xyY[2], &XYZ[2]); + + return Py_BuildValue( + "(((d,d,d),(d,d,d),(d,d,d)),((d,d,d),(d,d,d),(d,d,d)))", + XYZ[0].X, + XYZ[0].Y, + XYZ[0].Z, + XYZ[1].X, + XYZ[1].Y, + XYZ[1].Z, + XYZ[2].X, + XYZ[2].Y, + XYZ[2].Z, + xyY[0].x, + xyY[0].y, + xyY[0].Y, + xyY[1].x, + xyY[1].y, + xyY[1].Y, + xyY[2].x, + xyY[2].y, + xyY[2].Y); +} + +static PyObject * +_profile_read_ciexyz(CmsProfileObject *self, cmsTagSignature info, int multi) { + cmsCIEXYZ *XYZ; + + if (!cmsIsTag(self->profile, info)) { + Py_INCREF(Py_None); + return Py_None; + } + + XYZ = (cmsCIEXYZ *)cmsReadTag(self->profile, info); + if (!XYZ) { + Py_INCREF(Py_None); + return Py_None; + } + if (multi) { + return _xyz3_py(XYZ); + } else { + return _xyz_py(XYZ); + } +} + +static PyObject * +_profile_read_ciexyy_triple(CmsProfileObject *self, cmsTagSignature info) { + cmsCIExyYTRIPLE *triple; + + if (!cmsIsTag(self->profile, info)) { + Py_INCREF(Py_None); + return Py_None; + } + + triple = (cmsCIExyYTRIPLE *)cmsReadTag(self->profile, info); + if (!triple) { + Py_INCREF(Py_None); + return Py_None; + } + + /* Note: lcms does all the heavy lifting and error checking (nr of + channels == 3). */ + return Py_BuildValue( + "((d,d,d),(d,d,d),(d,d,d)),", + triple->Red.x, + triple->Red.y, + triple->Red.Y, + triple->Green.x, + triple->Green.y, + triple->Green.Y, + triple->Blue.x, + triple->Blue.y, + triple->Blue.Y); +} + +static PyObject * +_profile_read_named_color_list(CmsProfileObject *self, cmsTagSignature info) { + cmsNAMEDCOLORLIST *ncl; + int i, n; + char name[cmsMAX_PATH]; + PyObject *result; + + if (!cmsIsTag(self->profile, info)) { + Py_INCREF(Py_None); + return Py_None; + } + + ncl = (cmsNAMEDCOLORLIST *)cmsReadTag(self->profile, info); + if (ncl == NULL) { + Py_INCREF(Py_None); + return Py_None; + } + + n = cmsNamedColorCount(ncl); + result = PyList_New(n); + if (!result) { + Py_INCREF(Py_None); + return Py_None; + } + + for (i = 0; i < n; i++) { + PyObject *str; + cmsNamedColorInfo(ncl, i, name, NULL, NULL, NULL, NULL); + str = PyUnicode_FromString(name); + if (str == NULL) { + Py_DECREF(result); + Py_INCREF(Py_None); + return Py_None; + } + PyList_SET_ITEM(result, i, str); + } + + return result; +} + +static cmsBool +_calculate_rgb_primaries(CmsProfileObject *self, cmsCIEXYZTRIPLE *result) { + double input[3][3] = {{1, 0, 0}, {0, 1, 0}, {0, 0, 1}}; + cmsHPROFILE hXYZ; + cmsHTRANSFORM hTransform; + + /* https://littlecms2.blogspot.com/2009/07/less-is-more.html */ + + // double array of RGB values with max on each identity + hXYZ = cmsCreateXYZProfile(); + if (hXYZ == NULL) { + return 0; + } + + // transform from our profile to XYZ using doubles for highest precision + hTransform = cmsCreateTransform( + self->profile, + TYPE_RGB_DBL, + hXYZ, + TYPE_XYZ_DBL, + INTENT_RELATIVE_COLORIMETRIC, + cmsFLAGS_NOCACHE | cmsFLAGS_NOOPTIMIZE); + cmsCloseProfile(hXYZ); + if (hTransform == NULL) { + return 0; + } + + cmsDoTransform(hTransform, (void *)input, result, 3); + cmsDeleteTransform(hTransform); + return 1; +} + +static cmsBool +_check_intent( + int clut, + cmsHPROFILE hProfile, + cmsUInt32Number Intent, + cmsUInt32Number UsedDirection) { + if (clut) { + return cmsIsCLUT(hProfile, Intent, UsedDirection); + } else { + return cmsIsIntentSupported(hProfile, Intent, UsedDirection); + } +} + +#define INTENTS 200 + +static PyObject * +_is_intent_supported(CmsProfileObject *self, int clut) { + PyObject *result; + int n; + int i; + cmsUInt32Number intent_ids[INTENTS]; + char *intent_descs[INTENTS]; + + result = PyDict_New(); + if (result == NULL) { + Py_INCREF(Py_None); + return Py_None; + } + + n = cmsGetSupportedIntents(INTENTS, intent_ids, intent_descs); + for (i = 0; i < n; i++) { + int intent = (int)intent_ids[i]; + PyObject *id; + PyObject *entry; + + /* Only valid for ICC Intents (otherwise we read invalid memory in lcms + * cmsio1.c). */ + if (!(intent == INTENT_PERCEPTUAL || intent == INTENT_RELATIVE_COLORIMETRIC || + intent == INTENT_SATURATION || intent == INTENT_ABSOLUTE_COLORIMETRIC)) { + continue; + } + + id = PyLong_FromLong((long)intent); + entry = Py_BuildValue( + "(OOO)", + _check_intent(clut, self->profile, intent, LCMS_USED_AS_INPUT) ? Py_True + : Py_False, + _check_intent(clut, self->profile, intent, LCMS_USED_AS_OUTPUT) ? Py_True + : Py_False, + _check_intent(clut, self->profile, intent, LCMS_USED_AS_PROOF) ? Py_True + : Py_False); + if (id == NULL || entry == NULL) { + Py_XDECREF(id); + Py_XDECREF(entry); + Py_XDECREF(result); + Py_INCREF(Py_None); + return Py_None; + } + PyDict_SetItem(result, id, entry); + Py_DECREF(id); + Py_DECREF(entry); + } + return result; +} + +/* -------------------------------------------------------------------- */ +/* Python interface setup */ + +static PyMethodDef pyCMSdll_methods[] = { + + {"profile_open", cms_profile_open, METH_VARARGS}, + {"profile_frombytes", cms_profile_frombytes, METH_VARARGS}, + {"profile_tobytes", cms_profile_tobytes, METH_VARARGS}, + + /* profile and transform functions */ + {"buildTransform", buildTransform, METH_VARARGS}, + {"buildProofTransform", buildProofTransform, METH_VARARGS}, + {"createProfile", createProfile, METH_VARARGS}, + +/* platform specific tools */ +#ifdef _WIN32 + {"get_display_profile_win32", cms_get_display_profile_win32, METH_VARARGS}, +#endif + + {NULL, NULL}}; + +static struct PyMethodDef cms_profile_methods[] = { + {"is_intent_supported", (PyCFunction)cms_profile_is_intent_supported, METH_VARARGS}, + {NULL, NULL} /* sentinel */ +}; + +static PyObject * +cms_profile_getattr_rendering_intent(CmsProfileObject *self, void *closure) { + return PyLong_FromLong(cmsGetHeaderRenderingIntent(self->profile)); +} + +/* New-style unicode interfaces. */ +static PyObject * +cms_profile_getattr_copyright(CmsProfileObject *self, void *closure) { + return _profile_read_mlu(self, cmsSigCopyrightTag); +} + +static PyObject * +cms_profile_getattr_target(CmsProfileObject *self, void *closure) { + return _profile_read_mlu(self, cmsSigCharTargetTag); +} + +static PyObject * +cms_profile_getattr_manufacturer(CmsProfileObject *self, void *closure) { + return _profile_read_mlu(self, cmsSigDeviceMfgDescTag); +} + +static PyObject * +cms_profile_getattr_model(CmsProfileObject *self, void *closure) { + return _profile_read_mlu(self, cmsSigDeviceModelDescTag); +} + +static PyObject * +cms_profile_getattr_profile_description(CmsProfileObject *self, void *closure) { + return _profile_read_mlu(self, cmsSigProfileDescriptionTag); +} + +static PyObject * +cms_profile_getattr_screening_description(CmsProfileObject *self, void *closure) { + return _profile_read_mlu(self, cmsSigScreeningDescTag); +} + +static PyObject * +cms_profile_getattr_viewing_condition(CmsProfileObject *self, void *closure) { + return _profile_read_mlu(self, cmsSigViewingCondDescTag); +} + +static PyObject * +cms_profile_getattr_creation_date(CmsProfileObject *self, void *closure) { + cmsBool result; + struct tm ct; + + result = cmsGetHeaderCreationDateTime(self->profile, &ct); + if (!result) { + Py_INCREF(Py_None); + return Py_None; + } + + return PyDateTime_FromDateAndTime( + 1900 + ct.tm_year, ct.tm_mon, ct.tm_mday, ct.tm_hour, ct.tm_min, ct.tm_sec, 0); +} + +static PyObject * +cms_profile_getattr_version(CmsProfileObject *self, void *closure) { + cmsFloat64Number version = cmsGetProfileVersion(self->profile); + return PyFloat_FromDouble(version); +} + +static PyObject * +cms_profile_getattr_icc_version(CmsProfileObject *self, void *closure) { + return PyLong_FromLong((long)cmsGetEncodedICCversion(self->profile)); +} + +static PyObject * +cms_profile_getattr_attributes(CmsProfileObject *self, void *closure) { + cmsUInt64Number attr; + cmsGetHeaderAttributes(self->profile, &attr); + /* This works just as well on Windows (LLP64), 32-bit Linux + (ILP32) and 64-bit Linux (LP64) systems. */ + return PyLong_FromUnsignedLongLong((unsigned long long)attr); +} + +static PyObject * +cms_profile_getattr_header_flags(CmsProfileObject *self, void *closure) { + cmsUInt32Number flags = cmsGetHeaderFlags(self->profile); + return PyLong_FromLong(flags); +} + +static PyObject * +cms_profile_getattr_header_manufacturer(CmsProfileObject *self, void *closure) { + return _profile_read_int_as_string(cmsGetHeaderManufacturer(self->profile)); +} + +static PyObject * +cms_profile_getattr_header_model(CmsProfileObject *self, void *closure) { + return _profile_read_int_as_string(cmsGetHeaderModel(self->profile)); +} + +static PyObject * +cms_profile_getattr_device_class(CmsProfileObject *self, void *closure) { + return _profile_read_int_as_string(cmsGetDeviceClass(self->profile)); +} + +static PyObject * +cms_profile_getattr_connection_space(CmsProfileObject *self, void *closure) { + return _profile_read_int_as_string(cmsGetPCS(self->profile)); +} + +static PyObject * +cms_profile_getattr_xcolor_space(CmsProfileObject *self, void *closure) { + return _profile_read_int_as_string(cmsGetColorSpace(self->profile)); +} + +static PyObject * +cms_profile_getattr_profile_id(CmsProfileObject *self, void *closure) { + cmsUInt8Number id[16]; + cmsGetHeaderProfileID(self->profile, id); + return PyBytes_FromStringAndSize((char *)id, 16); +} + +static PyObject * +cms_profile_getattr_is_matrix_shaper(CmsProfileObject *self, void *closure) { + return PyBool_FromLong((long)cmsIsMatrixShaper(self->profile)); +} + +static PyObject * +cms_profile_getattr_technology(CmsProfileObject *self, void *closure) { + return _profile_read_signature(self, cmsSigTechnologyTag); +} + +static PyObject * +cms_profile_getattr_colorimetric_intent(CmsProfileObject *self, void *closure) { + return _profile_read_signature(self, cmsSigColorimetricIntentImageStateTag); +} + +static PyObject * +cms_profile_getattr_perceptual_rendering_intent_gamut( + CmsProfileObject *self, void *closure) { + return _profile_read_signature(self, cmsSigPerceptualRenderingIntentGamutTag); +} + +static PyObject * +cms_profile_getattr_saturation_rendering_intent_gamut( + CmsProfileObject *self, void *closure) { + return _profile_read_signature(self, cmsSigSaturationRenderingIntentGamutTag); +} + +static PyObject * +cms_profile_getattr_red_colorant(CmsProfileObject *self, void *closure) { + if (!cmsIsMatrixShaper(self->profile)) { + Py_INCREF(Py_None); + return Py_None; + } + return _profile_read_ciexyz(self, cmsSigRedColorantTag, 0); +} + +static PyObject * +cms_profile_getattr_green_colorant(CmsProfileObject *self, void *closure) { + if (!cmsIsMatrixShaper(self->profile)) { + Py_INCREF(Py_None); + return Py_None; + } + return _profile_read_ciexyz(self, cmsSigGreenColorantTag, 0); +} + +static PyObject * +cms_profile_getattr_blue_colorant(CmsProfileObject *self, void *closure) { + if (!cmsIsMatrixShaper(self->profile)) { + Py_INCREF(Py_None); + return Py_None; + } + return _profile_read_ciexyz(self, cmsSigBlueColorantTag, 0); +} + +static PyObject * +cms_profile_getattr_media_white_point_temperature( + CmsProfileObject *self, void *closure) { + cmsCIEXYZ *XYZ; + cmsCIExyY xyY; + cmsFloat64Number tempK; + cmsTagSignature info = cmsSigMediaWhitePointTag; + cmsBool result; + + if (!cmsIsTag(self->profile, info)) { + Py_INCREF(Py_None); + return Py_None; + } + + XYZ = (cmsCIEXYZ *)cmsReadTag(self->profile, info); + if (XYZ == NULL || XYZ->X == 0) { + Py_INCREF(Py_None); + return Py_None; + } + + cmsXYZ2xyY(&xyY, XYZ); + result = cmsTempFromWhitePoint(&tempK, &xyY); + if (!result) { + Py_INCREF(Py_None); + return Py_None; + } + return PyFloat_FromDouble(tempK); +} + +static PyObject * +cms_profile_getattr_media_white_point(CmsProfileObject *self, void *closure) { + return _profile_read_ciexyz(self, cmsSigMediaWhitePointTag, 0); +} + +static PyObject * +cms_profile_getattr_media_black_point(CmsProfileObject *self, void *closure) { + return _profile_read_ciexyz(self, cmsSigMediaBlackPointTag, 0); +} + +static PyObject * +cms_profile_getattr_luminance(CmsProfileObject *self, void *closure) { + return _profile_read_ciexyz(self, cmsSigLuminanceTag, 0); +} + +static PyObject * +cms_profile_getattr_chromatic_adaptation(CmsProfileObject *self, void *closure) { + return _profile_read_ciexyz(self, cmsSigChromaticAdaptationTag, 1); +} + +static PyObject * +cms_profile_getattr_chromaticity(CmsProfileObject *self, void *closure) { + return _profile_read_ciexyy_triple(self, cmsSigChromaticityTag); +} + +static PyObject * +cms_profile_getattr_red_primary(CmsProfileObject *self, void *closure) { + cmsBool result = 0; + cmsCIEXYZTRIPLE primaries; + + if (cmsIsMatrixShaper(self->profile)) { + result = _calculate_rgb_primaries(self, &primaries); + } + if (!result) { + Py_INCREF(Py_None); + return Py_None; + } + + return _xyz_py(&primaries.Red); +} + +static PyObject * +cms_profile_getattr_green_primary(CmsProfileObject *self, void *closure) { + cmsBool result = 0; + cmsCIEXYZTRIPLE primaries; + + if (cmsIsMatrixShaper(self->profile)) { + result = _calculate_rgb_primaries(self, &primaries); + } + if (!result) { + Py_INCREF(Py_None); + return Py_None; + } + + return _xyz_py(&primaries.Green); +} + +static PyObject * +cms_profile_getattr_blue_primary(CmsProfileObject *self, void *closure) { + cmsBool result = 0; + cmsCIEXYZTRIPLE primaries; + + if (cmsIsMatrixShaper(self->profile)) { + result = _calculate_rgb_primaries(self, &primaries); + } + if (!result) { + Py_INCREF(Py_None); + return Py_None; + } + + return _xyz_py(&primaries.Blue); +} + +static PyObject * +cms_profile_getattr_colorant_table(CmsProfileObject *self, void *closure) { + return _profile_read_named_color_list(self, cmsSigColorantTableTag); +} + +static PyObject * +cms_profile_getattr_colorant_table_out(CmsProfileObject *self, void *closure) { + return _profile_read_named_color_list(self, cmsSigColorantTableOutTag); +} + +static PyObject * +cms_profile_getattr_is_intent_supported(CmsProfileObject *self, void *closure) { + return _is_intent_supported(self, 0); +} + +static PyObject * +cms_profile_getattr_is_clut(CmsProfileObject *self, void *closure) { + return _is_intent_supported(self, 1); +} + +static const char * +_illu_map(int i) { + switch (i) { + case 0: + return "unknown"; + case 1: + return "D50"; + case 2: + return "D65"; + case 3: + return "D93"; + case 4: + return "F2"; + case 5: + return "D55"; + case 6: + return "A"; + case 7: + return "E"; + case 8: + return "F8"; + default: + return NULL; + } +} + +static PyObject * +cms_profile_getattr_icc_measurement_condition(CmsProfileObject *self, void *closure) { + cmsICCMeasurementConditions *mc; + cmsTagSignature info = cmsSigMeasurementTag; + const char *geo; + + if (!cmsIsTag(self->profile, info)) { + Py_INCREF(Py_None); + return Py_None; + } + + mc = (cmsICCMeasurementConditions *)cmsReadTag(self->profile, info); + if (!mc) { + Py_INCREF(Py_None); + return Py_None; + } + + if (mc->Geometry == 1) { + geo = "45/0, 0/45"; + } else if (mc->Geometry == 2) { + geo = "0d, d/0"; + } else { + geo = "unknown"; + } + + return Py_BuildValue( + "{s:i,s:(ddd),s:s,s:d,s:s}", + "observer", + mc->Observer, + "backing", + mc->Backing.X, + mc->Backing.Y, + mc->Backing.Z, + "geo", + geo, + "flare", + mc->Flare, + "illuminant_type", + _illu_map(mc->IlluminantType)); +} + +static PyObject * +cms_profile_getattr_icc_viewing_condition(CmsProfileObject *self, void *closure) { + cmsICCViewingConditions *vc; + cmsTagSignature info = cmsSigViewingConditionsTag; + + if (!cmsIsTag(self->profile, info)) { + Py_INCREF(Py_None); + return Py_None; + } + + vc = (cmsICCViewingConditions *)cmsReadTag(self->profile, info); + if (!vc) { + Py_INCREF(Py_None); + return Py_None; + } + + return Py_BuildValue( + "{s:(ddd),s:(ddd),s:s}", + "illuminant", + vc->IlluminantXYZ.X, + vc->IlluminantXYZ.Y, + vc->IlluminantXYZ.Z, + "surround", + vc->SurroundXYZ.X, + vc->SurroundXYZ.Y, + vc->SurroundXYZ.Z, + "illuminant_type", + _illu_map(vc->IlluminantType)); +} + +static struct PyGetSetDef cms_profile_getsetters[] = { + /* New style interfaces. */ + {"rendering_intent", (getter)cms_profile_getattr_rendering_intent}, + {"creation_date", (getter)cms_profile_getattr_creation_date}, + {"copyright", (getter)cms_profile_getattr_copyright}, + {"target", (getter)cms_profile_getattr_target}, + {"manufacturer", (getter)cms_profile_getattr_manufacturer}, + {"model", (getter)cms_profile_getattr_model}, + {"profile_description", (getter)cms_profile_getattr_profile_description}, + {"screening_description", (getter)cms_profile_getattr_screening_description}, + {"viewing_condition", (getter)cms_profile_getattr_viewing_condition}, + {"version", (getter)cms_profile_getattr_version}, + {"icc_version", (getter)cms_profile_getattr_icc_version}, + {"attributes", (getter)cms_profile_getattr_attributes}, + {"header_flags", (getter)cms_profile_getattr_header_flags}, + {"header_manufacturer", (getter)cms_profile_getattr_header_manufacturer}, + {"header_model", (getter)cms_profile_getattr_header_model}, + {"device_class", (getter)cms_profile_getattr_device_class}, + {"connection_space", (getter)cms_profile_getattr_connection_space}, + {"xcolor_space", (getter)cms_profile_getattr_xcolor_space}, + {"profile_id", (getter)cms_profile_getattr_profile_id}, + {"is_matrix_shaper", (getter)cms_profile_getattr_is_matrix_shaper}, + {"technology", (getter)cms_profile_getattr_technology}, + {"colorimetric_intent", (getter)cms_profile_getattr_colorimetric_intent}, + {"perceptual_rendering_intent_gamut", + (getter)cms_profile_getattr_perceptual_rendering_intent_gamut}, + {"saturation_rendering_intent_gamut", + (getter)cms_profile_getattr_saturation_rendering_intent_gamut}, + {"red_colorant", (getter)cms_profile_getattr_red_colorant}, + {"green_colorant", (getter)cms_profile_getattr_green_colorant}, + {"blue_colorant", (getter)cms_profile_getattr_blue_colorant}, + {"red_primary", (getter)cms_profile_getattr_red_primary}, + {"green_primary", (getter)cms_profile_getattr_green_primary}, + {"blue_primary", (getter)cms_profile_getattr_blue_primary}, + {"media_white_point_temperature", + (getter)cms_profile_getattr_media_white_point_temperature}, + {"media_white_point", (getter)cms_profile_getattr_media_white_point}, + {"media_black_point", (getter)cms_profile_getattr_media_black_point}, + {"luminance", (getter)cms_profile_getattr_luminance}, + {"chromatic_adaptation", (getter)cms_profile_getattr_chromatic_adaptation}, + {"chromaticity", (getter)cms_profile_getattr_chromaticity}, + {"colorant_table", (getter)cms_profile_getattr_colorant_table}, + {"colorant_table_out", (getter)cms_profile_getattr_colorant_table_out}, + {"intent_supported", (getter)cms_profile_getattr_is_intent_supported}, + {"clut", (getter)cms_profile_getattr_is_clut}, + {"icc_measurement_condition", + (getter)cms_profile_getattr_icc_measurement_condition}, + {"icc_viewing_condition", (getter)cms_profile_getattr_icc_viewing_condition}, + + {NULL}}; + +static PyTypeObject CmsProfile_Type = { + PyVarObject_HEAD_INIT(NULL, 0) "PIL.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*/ +}; + +static struct PyMethodDef cms_transform_methods[] = { + {"apply", (PyCFunction)cms_transform_apply, 1}, {NULL, NULL} /* sentinel */ +}; + +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*/ +}; + +static int +setup_module(PyObject *m) { + PyObject *d; + PyObject *v; + int vn; + + /* Ready object types */ + PyType_Ready(&CmsProfile_Type); + PyType_Ready(&CmsTransform_Type); + + Py_INCREF(&CmsProfile_Type); + PyModule_AddObject(m, "CmsProfile", (PyObject *)&CmsProfile_Type); + + Py_INCREF(&CmsTransform_Type); + PyModule_AddObject(m, "CmsTransform", (PyObject *)&CmsTransform_Type); + + d = PyModule_GetDict(m); + + /* this check is also in PIL.features.pilinfo() */ +#if LCMS_VERSION < 2070 + vn = LCMS_VERSION; +#else + vn = cmsGetEncodedCMMversion(); +#endif + if (vn % 10) { + v = PyUnicode_FromFormat("%d.%d.%d", vn / 1000, (vn / 10) % 100, vn % 10); + } else { + v = PyUnicode_FromFormat("%d.%d", vn / 1000, (vn / 10) % 100); + } + PyDict_SetItemString(d, "littlecms_version", v ? v : Py_None); + Py_XDECREF(v); + + return 0; +} + +PyMODINIT_FUNC +PyInit__imagingcms(void) { + PyObject *m; + + static PyModuleDef module_def = { + PyModuleDef_HEAD_INIT, + "_imagingcms", /* m_name */ + NULL, /* m_doc */ + -1, /* m_size */ + pyCMSdll_methods, /* m_methods */ + }; + + m = PyModule_Create(&module_def); + + if (setup_module(m) < 0) { + return NULL; + } + + PyDateTime_IMPORT; + + return m; +} diff --git a/src/_imagingft.c b/src/_imagingft.c index e83ddfec1..262c85510 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -1,1584 +1,1584 @@ -/* - * PIL FreeType Driver - * - * a FreeType 2.X driver for PIL - * - * history: - * 2001-02-17 fl Created (based on old experimental freetype 1.0 code) - * 2001-04-18 fl Fixed some egcs compiler nits - * 2002-11-08 fl Added unicode support; more font metrics, etc - * 2003-05-20 fl Fixed compilation under 1.5.2 and newer non-unicode builds - * 2003-09-27 fl Added charmap encoding support - * 2004-05-15 fl Fixed compilation for FreeType 2.1.8 - * 2004-09-10 fl Added support for monochrome bitmaps - * 2006-06-18 fl Fixed glyph bearing calculation - * 2007-12-23 fl Fixed crash in family/style attribute fetch - * 2008-01-02 fl Handle Unicode filenames properly - * - * Copyright (c) 1998-2007 by Secret Labs AB - */ - -#define PY_SSIZE_T_CLEAN -#include "Python.h" -#include "libImaging/Imaging.h" - -#include -#include FT_FREETYPE_H -#include FT_GLYPH_H -#include FT_BITMAP_H -#include FT_STROKER_H -#include FT_MULTIPLE_MASTERS_H -#include FT_SFNT_NAMES_H -#ifdef FT_COLOR_H -#include FT_COLOR_H -#endif - -/* -------------------------------------------------------------------- */ -/* error table */ - -#undef FTERRORS_H -#undef __FTERRORS_H__ - -#define FT_ERRORDEF(e, v, s) {e, s}, -#define FT_ERROR_START_LIST { -#define FT_ERROR_END_LIST \ - { 0, 0 } \ - } \ - ; - -#ifdef HAVE_RAQM -#ifdef HAVE_RAQM_SYSTEM -#include -#else -#include "thirdparty/raqm/raqm.h" -#ifdef HAVE_FRIBIDI_SYSTEM -#include -#else -#include "thirdparty/fribidi-shim/fribidi.h" -#include -#endif -#endif -#endif - -static int have_raqm = 0; - -#define LAYOUT_FALLBACK 0 -#define LAYOUT_RAQM 1 - -typedef struct { - int index, x_offset, x_advance, y_offset, y_advance; - unsigned int cluster; -} GlyphInfo; - -struct { - int code; - const char *message; -} ft_errors[] = - -#include FT_ERRORS_H - - /* -------------------------------------------------------------------- */ - /* font objects */ - - static FT_Library library; - -typedef struct { - PyObject_HEAD FT_Face face; - unsigned char *font_bytes; - int layout_engine; -} FontObject; - -static PyTypeObject Font_Type; - -/* round a 26.6 pixel coordinate to the nearest integer */ -#define PIXEL(x) ((((x) + 32) & -64) >> 6) - -static PyObject * -geterror(int code) { - int i; - - for (i = 0; ft_errors[i].message; i++) { - if (ft_errors[i].code == code) { - PyErr_SetString(PyExc_OSError, ft_errors[i].message); - return NULL; - } - } - - PyErr_SetString(PyExc_OSError, "unknown freetype error"); - return NULL; -} - -static PyObject * -getfont(PyObject *self_, PyObject *args, PyObject *kw) { - /* create a font object from a file name and a size (in pixels) */ - - FontObject *self; - int error = 0; - - char *filename = NULL; - float size; - FT_Size_RequestRec req; - FT_Long width; - Py_ssize_t index = 0; - Py_ssize_t layout_engine = 0; - unsigned char *encoding; - unsigned char *font_bytes; - Py_ssize_t font_bytes_size = 0; - static char *kwlist[] = { - "filename", "size", "index", "encoding", "font_bytes", "layout_engine", NULL}; - - if (!library) { - PyErr_SetString(PyExc_OSError, "failed to initialize FreeType library"); - return NULL; - } - -#if PY_MAJOR_VERSION > 3 || PY_MINOR_VERSION > 11 - PyConfig config; - PyConfig_InitPythonConfig(&config); - if (!PyArg_ParseTupleAndKeywords( - args, - kw, - "etf|nsy#n", - kwlist, - config.filesystem_encoding, - &filename, - &size, - &index, - &encoding, - &font_bytes, - &font_bytes_size, - &layout_engine)) { - PyConfig_Clear(&config); - return NULL; - } - PyConfig_Clear(&config); -#else - if (!PyArg_ParseTupleAndKeywords( - args, - kw, - "etf|nsy#n", - kwlist, - Py_FileSystemDefaultEncoding, - &filename, - &size, - &index, - &encoding, - &font_bytes, - &font_bytes_size, - &layout_engine)) { - return NULL; - } -#endif - - self = PyObject_New(FontObject, &Font_Type); - if (!self) { - if (filename) { - PyMem_Free(filename); - } - return NULL; - } - - self->face = NULL; - self->layout_engine = layout_engine; - - if (filename && font_bytes_size <= 0) { - self->font_bytes = NULL; - error = FT_New_Face(library, filename, index, &self->face); - } else { - /* need to have allocated storage for font_bytes for the life of the object.*/ - /* Don't free this before FT_Done_Face */ - self->font_bytes = PyMem_Malloc(font_bytes_size); - if (!self->font_bytes) { - error = FT_Err_Out_Of_Memory; - } - if (!error) { - memcpy(self->font_bytes, font_bytes, (size_t)font_bytes_size); - error = FT_New_Memory_Face( - library, - (FT_Byte *)self->font_bytes, - font_bytes_size, - index, - &self->face); - } - } - - if (!error) { - width = size * 64; - req.type = FT_SIZE_REQUEST_TYPE_NOMINAL; - req.width = width; - req.height = width; - req.horiResolution = 0; - req.vertResolution = 0; - error = FT_Request_Size(self->face, &req); - } - - if (!error && encoding && strlen((char *)encoding) == 4) { - FT_Encoding encoding_tag = - FT_MAKE_TAG(encoding[0], encoding[1], encoding[2], encoding[3]); - error = FT_Select_Charmap(self->face, encoding_tag); - } - if (filename) { - PyMem_Free(filename); - } - - if (error) { - if (self->font_bytes) { - PyMem_Free(self->font_bytes); - self->font_bytes = NULL; - } - Py_DECREF(self); - return geterror(error); - } - - return (PyObject *)self; -} - -static int -font_getchar(PyObject *string, int index, FT_ULong *char_out) { - if (PyUnicode_Check(string)) { - if (index >= PyUnicode_GET_LENGTH(string)) { - return 0; - } - *char_out = PyUnicode_READ_CHAR(string, index); - return 1; - } - return 0; -} - -#ifdef HAVE_RAQM - -static size_t -text_layout_raqm( - PyObject *string, - FontObject *self, - const char *dir, - PyObject *features, - const char *lang, - GlyphInfo **glyph_info) { - size_t i = 0, count = 0, start = 0; - raqm_t *rq; - raqm_glyph_t *glyphs = NULL; - raqm_direction_t direction; - - rq = raqm_create(); - if (rq == NULL) { - PyErr_SetString(PyExc_ValueError, "raqm_create() failed."); - goto failed; - } - - if (PyUnicode_Check(string)) { - Py_UCS4 *text = PyUnicode_AsUCS4Copy(string); - Py_ssize_t size = PyUnicode_GET_LENGTH(string); - if (!text || !size) { - /* return 0 and clean up, no glyphs==no size, - and raqm fails with empty strings */ - goto failed; - } - int set_text = raqm_set_text(rq, text, size); - PyMem_Free(text); - if (!set_text) { - PyErr_SetString(PyExc_ValueError, "raqm_set_text() failed"); - goto failed; - } - if (lang) { - if (!raqm_set_language(rq, lang, start, size)) { - PyErr_SetString(PyExc_ValueError, "raqm_set_language() failed"); - goto failed; - } - } - } else { - PyErr_SetString(PyExc_TypeError, "expected string"); - goto failed; - } - - direction = RAQM_DIRECTION_DEFAULT; - if (dir) { - if (strcmp(dir, "rtl") == 0) { - direction = RAQM_DIRECTION_RTL; - } else if (strcmp(dir, "ltr") == 0) { - direction = RAQM_DIRECTION_LTR; - } else if (strcmp(dir, "ttb") == 0) { - direction = RAQM_DIRECTION_TTB; -#if !defined(RAQM_VERSION_ATLEAST) - /* RAQM_VERSION_ATLEAST was added in Raqm 0.7.0 */ - PyErr_SetString( - PyExc_ValueError, - "libraqm 0.7 or greater required for 'ttb' direction"); - goto failed; -#endif - } else { - PyErr_SetString( - PyExc_ValueError, "direction must be either 'rtl', 'ltr' or 'ttb'"); - goto failed; - } - } - - if (!raqm_set_par_direction(rq, direction)) { - PyErr_SetString(PyExc_ValueError, "raqm_set_par_direction() failed"); - goto failed; - } - - if (features != Py_None) { - int j, len; - PyObject *seq = PySequence_Fast(features, "expected a sequence"); - if (!seq) { - goto failed; - } - - 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_DECREF(seq); - goto failed; - } - feature = PyBytes_AS_STRING(bytes); - size = PyBytes_GET_SIZE(bytes); - if (!raqm_add_font_feature(rq, feature, size)) { - Py_DECREF(seq); - Py_DECREF(bytes); - PyErr_SetString(PyExc_ValueError, "raqm_add_font_feature() failed"); - goto failed; - } - Py_DECREF(bytes); - } - Py_DECREF(seq); - } - - if (!raqm_set_freetype_face(rq, self->face)) { - PyErr_SetString(PyExc_RuntimeError, "raqm_set_freetype_face() failed."); - goto failed; - } - - if (!raqm_layout(rq)) { - PyErr_SetString(PyExc_RuntimeError, "raqm_layout() failed."); - goto failed; - } - - glyphs = raqm_get_glyphs(rq, &count); - if (glyphs == NULL) { - PyErr_SetString(PyExc_ValueError, "raqm_get_glyphs() failed."); - count = 0; - goto failed; - } - - (*glyph_info) = PyMem_New(GlyphInfo, count); - if ((*glyph_info) == NULL) { - PyErr_SetString(PyExc_MemoryError, "PyMem_New() failed"); - count = 0; - goto failed; - } - - for (i = 0; i < count; i++) { - (*glyph_info)[i].index = glyphs[i].index; - (*glyph_info)[i].x_offset = glyphs[i].x_offset; - (*glyph_info)[i].x_advance = glyphs[i].x_advance; - (*glyph_info)[i].y_offset = glyphs[i].y_offset; - (*glyph_info)[i].y_advance = glyphs[i].y_advance; - (*glyph_info)[i].cluster = glyphs[i].cluster; - } - -failed: - raqm_destroy(rq); - return count; -} - -#endif - -static size_t -text_layout_fallback( - PyObject *string, - FontObject *self, - const char *dir, - PyObject *features, - const char *lang, - GlyphInfo **glyph_info, - int mask, - int color) { - int error, load_flags; - FT_ULong ch; - Py_ssize_t count; - FT_GlyphSlot glyph; - FT_Bool kerning = FT_HAS_KERNING(self->face); - FT_UInt last_index = 0; - int i; - - if (features != Py_None || dir != NULL || lang != NULL) { - PyErr_SetString( - PyExc_KeyError, - "setting text direction, language or font features is not supported " - "without libraqm"); - } - if (!PyUnicode_Check(string)) { - PyErr_SetString(PyExc_TypeError, "expected string"); - return 0; - } - - count = 0; - while (font_getchar(string, count, &ch)) { - count++; - } - if (count == 0) { - return 0; - } - - (*glyph_info) = PyMem_New(GlyphInfo, count); - if ((*glyph_info) == NULL) { - PyErr_SetString(PyExc_MemoryError, "PyMem_New() failed"); - return 0; - } - - load_flags = FT_LOAD_DEFAULT; - if (mask) { - load_flags |= FT_LOAD_TARGET_MONO; - } - if (color) { - load_flags |= FT_LOAD_COLOR; - } - for (i = 0; font_getchar(string, i, &ch); i++) { - (*glyph_info)[i].index = FT_Get_Char_Index(self->face, ch); - error = FT_Load_Glyph(self->face, (*glyph_info)[i].index, load_flags); - if (error) { - geterror(error); - return 0; - } - glyph = self->face->glyph; - (*glyph_info)[i].x_offset = 0; - (*glyph_info)[i].y_offset = 0; - if (kerning && last_index && (*glyph_info)[i].index) { - FT_Vector delta; - if (FT_Get_Kerning( - self->face, - last_index, - (*glyph_info)[i].index, - ft_kerning_default, - &delta) == 0) { - (*glyph_info)[i - 1].x_advance += PIXEL(delta.x); - (*glyph_info)[i - 1].y_advance += PIXEL(delta.y); - } - } - - (*glyph_info)[i].x_advance = glyph->metrics.horiAdvance; - // y_advance is only used in ttb, which is not supported by basic layout - (*glyph_info)[i].y_advance = 0; - last_index = (*glyph_info)[i].index; - (*glyph_info)[i].cluster = ch; - } - return count; -} - -static size_t -text_layout( - PyObject *string, - FontObject *self, - const char *dir, - PyObject *features, - const char *lang, - GlyphInfo **glyph_info, - int mask, - int color) { - size_t count; -#ifdef HAVE_RAQM - if (have_raqm && self->layout_engine == LAYOUT_RAQM) { - count = text_layout_raqm(string, self, dir, features, lang, glyph_info); - } else -#endif - { - count = text_layout_fallback( - string, self, dir, features, lang, glyph_info, mask, color); - } - return count; -} - -static PyObject * -font_getlength(FontObject *self, PyObject *args) { - int length; /* length along primary axis, in 26.6 precision */ - GlyphInfo *glyph_info = NULL; /* computed text layout */ - size_t i, count; /* glyph_info index and length */ - int horizontal_dir; /* is primary axis horizontal? */ - int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ - int color = 0; /* is FT_LOAD_COLOR enabled? */ - const char *mode = NULL; - const char *dir = NULL; - const char *lang = NULL; - PyObject *features = Py_None; - PyObject *string; - - /* calculate size and bearing for a given string */ - - if (!PyArg_ParseTuple( - args, "O|zzOz:getlength", &string, &mode, &dir, &features, &lang)) { - return NULL; - } - - horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; - - mask = mode && strcmp(mode, "1") == 0; - color = mode && strcmp(mode, "RGBA") == 0; - - count = text_layout(string, self, dir, features, lang, &glyph_info, mask, color); - if (PyErr_Occurred()) { - return NULL; - } - - length = 0; - for (i = 0; i < count; i++) { - if (horizontal_dir) { - length += glyph_info[i].x_advance; - } else { - length -= glyph_info[i].y_advance; - } - } - - if (glyph_info) { - PyMem_Free(glyph_info); - glyph_info = NULL; - } - - return PyLong_FromLong(length); -} - -static int -bounding_box_and_anchors( - FT_Face face, - const char *anchor, - int horizontal_dir, - GlyphInfo *glyph_info, - size_t count, - int load_flags, - int *width, - int *height, - int *x_offset, - int *y_offset) { - int position; /* pen position along primary axis, in 26.6 precision */ - int advanced; /* pen position along primary axis, in pixels */ - int px, py; /* position of current glyph, in pixels */ - int x_min, x_max, y_min, y_max; /* text bounding box, in pixels */ - int x_anchor, y_anchor; /* offset of point drawn at (0, 0), in pixels */ - int error; - FT_Glyph glyph; - FT_BBox bbox; /* glyph bounding box */ - size_t i; /* glyph_info index */ - /* - * text bounds are given by: - * - bounding boxes of individual glyphs - * - pen line, i.e. 0 to `advanced` along primary axis - * this means point (0, 0) is part of the text bounding box - */ - position = x_min = x_max = y_min = y_max = 0; - for (i = 0; i < count; i++) { - if (horizontal_dir) { - px = PIXEL(position + glyph_info[i].x_offset); - py = PIXEL(glyph_info[i].y_offset); - - position += glyph_info[i].x_advance; - advanced = PIXEL(position); - if (advanced > x_max) { - x_max = advanced; - } - } else { - px = PIXEL(glyph_info[i].x_offset); - py = PIXEL(position + glyph_info[i].y_offset); - - position += glyph_info[i].y_advance; - advanced = PIXEL(position); - if (advanced < y_min) { - y_min = advanced; - } - } - - error = FT_Load_Glyph(face, glyph_info[i].index, load_flags); - if (error) { - geterror(error); - return 1; - } - - error = FT_Get_Glyph(face->glyph, &glyph); - if (error) { - geterror(error); - return 1; - } - - FT_Glyph_Get_CBox(glyph, FT_GLYPH_BBOX_PIXELS, &bbox); - bbox.xMax += px; - if (bbox.xMax > x_max) { - x_max = bbox.xMax; - } - bbox.xMin += px; - if (bbox.xMin < x_min) { - x_min = bbox.xMin; - } - bbox.yMax += py; - if (bbox.yMax > y_max) { - y_max = bbox.yMax; - } - bbox.yMin += py; - if (bbox.yMin < y_min) { - y_min = bbox.yMin; - } - - FT_Done_Glyph(glyph); - } - - if (anchor == NULL) { - anchor = horizontal_dir ? "la" : "lt"; - } - if (strlen(anchor) != 2) { - goto bad_anchor; - } - - x_anchor = y_anchor = 0; - if (count) { - if (horizontal_dir) { - switch (anchor[0]) { - case 'l': // left - x_anchor = 0; - break; - case 'm': // middle (left + right) / 2 - x_anchor = PIXEL(position / 2); - break; - case 'r': // right - x_anchor = PIXEL(position); - break; - case 's': // vertical baseline - default: - goto bad_anchor; - } - switch (anchor[1]) { - case 'a': // ascender - y_anchor = PIXEL(face->size->metrics.ascender); - break; - case 't': // top - y_anchor = y_max; - break; - case 'm': // middle (ascender + descender) / 2 - y_anchor = PIXEL( - (face->size->metrics.ascender + face->size->metrics.descender) / - 2); - break; - case 's': // horizontal baseline - y_anchor = 0; - break; - case 'b': // bottom - y_anchor = y_min; - break; - case 'd': // descender - y_anchor = PIXEL(face->size->metrics.descender); - break; - default: - goto bad_anchor; - } - } else { - switch (anchor[0]) { - case 'l': // left - x_anchor = x_min; - break; - case 'm': // middle (left + right) / 2 - x_anchor = (x_min + x_max) / 2; - break; - case 'r': // right - x_anchor = x_max; - break; - case 's': // vertical baseline - x_anchor = 0; - break; - default: - goto bad_anchor; - } - switch (anchor[1]) { - case 't': // top - y_anchor = 0; - break; - case 'm': // middle (top + bottom) / 2 - y_anchor = PIXEL(position / 2); - break; - case 'b': // bottom - y_anchor = PIXEL(position); - break; - case 'a': // ascender - case 's': // horizontal baseline - case 'd': // descender - default: - goto bad_anchor; - } - } - } - *width = x_max - x_min; - *height = y_max - y_min; - *x_offset = -x_anchor + x_min; - *y_offset = -(-y_anchor + y_max); - return 0; - -bad_anchor: - PyErr_Format(PyExc_ValueError, "bad anchor specified: %s", anchor); - return 1; -} - -static PyObject * -font_getsize(FontObject *self, PyObject *args) { - int width, height, x_offset, y_offset; - int load_flags; /* FreeType load_flags parameter */ - int error; - GlyphInfo *glyph_info = NULL; /* computed text layout */ - size_t count; /* glyph_info length */ - int horizontal_dir; /* is primary axis horizontal? */ - int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ - int color = 0; /* is FT_LOAD_COLOR enabled? */ - const char *mode = NULL; - const char *dir = NULL; - const char *lang = NULL; - const char *anchor = NULL; - PyObject *features = Py_None; - PyObject *string; - - /* calculate size and bearing for a given string */ - - if (!PyArg_ParseTuple( - args, "O|zzOzz:getsize", &string, &mode, &dir, &features, &lang, &anchor)) { - return NULL; - } - - horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; - - mask = mode && strcmp(mode, "1") == 0; - color = mode && strcmp(mode, "RGBA") == 0; - - count = text_layout(string, self, dir, features, lang, &glyph_info, mask, color); - if (PyErr_Occurred()) { - return NULL; - } - - load_flags = FT_LOAD_DEFAULT; - if (mask) { - load_flags |= FT_LOAD_TARGET_MONO; - } - if (color) { - load_flags |= FT_LOAD_COLOR; - } - - error = bounding_box_and_anchors( - self->face, - anchor, - horizontal_dir, - glyph_info, - count, - load_flags, - &width, - &height, - &x_offset, - &y_offset); - if (glyph_info) { - PyMem_Free(glyph_info); - glyph_info = NULL; - } - if (error) { - return NULL; - } - - return Py_BuildValue("(ii)(ii)", width, height, x_offset, y_offset); -} - -static PyObject * -font_render(FontObject *self, PyObject *args) { - int x, y; /* pen position, in 26.6 precision */ - int px, py; /* position of current glyph, in pixels */ - int x_min, y_max; /* text offset in 26.6 precision */ - int load_flags; /* FreeType load_flags parameter */ - int error; - FT_Glyph glyph; - FT_GlyphSlot glyph_slot; - FT_Bitmap bitmap; - FT_Bitmap bitmap_converted; /* initialized lazily, for non-8bpp fonts */ - FT_BitmapGlyph bitmap_glyph; - FT_Stroker stroker = NULL; - int bitmap_converted_ready = 0; /* has bitmap_converted been initialized */ - GlyphInfo *glyph_info = NULL; /* computed text layout */ - size_t i, count; /* glyph_info index and length */ - int xx, yy; /* pixel offset of current glyph bitmap */ - int x0, x1; /* horizontal bounds of glyph bitmap to copy */ - unsigned int bitmap_y; /* glyph bitmap y index */ - unsigned char *source; /* glyph bitmap source buffer */ - unsigned char convert_scale; /* scale factor for non-8bpp bitmaps */ - PyObject *image; - Imaging im; - Py_ssize_t id; - int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ - int color = 0; /* is FT_LOAD_COLOR enabled? */ - int stroke_width = 0; - PY_LONG_LONG foreground_ink_long = 0; - unsigned int foreground_ink; - const char *mode = NULL; - const char *dir = NULL; - const char *lang = NULL; - const char *anchor = NULL; - PyObject *features = Py_None; - PyObject *string; - PyObject *fill; - float x_start = 0; - float y_start = 0; - int width, height, x_offset, y_offset; - int horizontal_dir; /* is primary axis horizontal? */ - - /* render string into given buffer (the buffer *must* have - the right size, or this will crash) */ - - if (!PyArg_ParseTuple( - args, - "OO|zzOzizLffO:render", - &string, - &fill, - &mode, - &dir, - &features, - &lang, - &stroke_width, - &anchor, - &foreground_ink_long, - &x_start, - &y_start)) { - return NULL; - } - - mask = mode && strcmp(mode, "1") == 0; - color = mode && strcmp(mode, "RGBA") == 0; - - foreground_ink = foreground_ink_long; - -#ifdef FT_COLOR_H - if (color) { - FT_Color foreground_color; - FT_Byte *ink = (FT_Byte *)&foreground_ink; - foreground_color.red = ink[0]; - foreground_color.green = ink[1]; - foreground_color.blue = ink[2]; - foreground_color.alpha = - (FT_Byte)255; /* ink alpha is handled in ImageDraw.text */ - FT_Palette_Set_Foreground_Color(self->face, foreground_color); - } -#endif - - count = text_layout(string, self, dir, features, lang, &glyph_info, mask, color); - if (PyErr_Occurred()) { - return NULL; - } - - load_flags = stroke_width ? FT_LOAD_NO_BITMAP : FT_LOAD_DEFAULT; - if (mask) { - load_flags |= FT_LOAD_TARGET_MONO; - } - if (color) { - load_flags |= FT_LOAD_COLOR; - } - - horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; - - error = bounding_box_and_anchors( - self->face, - anchor, - horizontal_dir, - glyph_info, - count, - load_flags, - &width, - &height, - &x_offset, - &y_offset); - if (error) { - PyMem_Del(glyph_info); - return NULL; - } - - width += stroke_width * 2 + ceil(x_start); - height += stroke_width * 2 + ceil(y_start); - image = PyObject_CallFunction(fill, "ii", width, height); - if (image == Py_None) { - PyMem_Del(glyph_info); - return Py_BuildValue("N(ii)", image, 0, 0); - } else if (image == NULL) { - PyMem_Del(glyph_info); - return NULL; - } - PyObject *imageId = PyObject_GetAttrString(image, "id"); - id = PyLong_AsSsize_t(imageId); - Py_XDECREF(imageId); - im = (Imaging)id; - - x_offset -= stroke_width; - y_offset -= stroke_width; - if (count == 0 || width == 0 || height == 0) { - PyMem_Del(glyph_info); - return Py_BuildValue("N(ii)", image, x_offset, y_offset); - } - - if (stroke_width) { - error = FT_Stroker_New(library, &stroker); - if (error) { - geterror(error); - goto glyph_error; - } - - FT_Stroker_Set( - stroker, - (FT_Fixed)stroke_width * 64, - FT_STROKER_LINECAP_ROUND, - FT_STROKER_LINEJOIN_ROUND, - 0); - } - - /* - * calculate x_min and y_max - * must match font_getsize or there may be clipping! - */ - x = y = x_min = y_max = 0; - for (i = 0; i < count; i++) { - px = PIXEL(x + glyph_info[i].x_offset); - py = PIXEL(y + glyph_info[i].y_offset); - - error = - FT_Load_Glyph(self->face, glyph_info[i].index, load_flags | FT_LOAD_RENDER); - if (error) { - geterror(error); - goto glyph_error; - } - - glyph_slot = self->face->glyph; - bitmap = glyph_slot->bitmap; - - if (glyph_slot->bitmap_top + py > y_max) { - y_max = glyph_slot->bitmap_top + py; - } - if (glyph_slot->bitmap_left + px < x_min) { - x_min = glyph_slot->bitmap_left + px; - } - - x += glyph_info[i].x_advance; - y += glyph_info[i].y_advance; - } - - /* set pen position to text origin */ - x = (-x_min + stroke_width + x_start) * 64; - y = (-y_max + (-stroke_width) - y_start) * 64; - - if (stroker == NULL) { - load_flags |= FT_LOAD_RENDER; - } - - for (i = 0; i < count; i++) { - px = PIXEL(x + glyph_info[i].x_offset); - py = PIXEL(y + glyph_info[i].y_offset); - - error = FT_Load_Glyph(self->face, glyph_info[i].index, load_flags); - if (error) { - geterror(error); - goto glyph_error; - } - - glyph_slot = self->face->glyph; - if (stroker != NULL) { - error = FT_Get_Glyph(glyph_slot, &glyph); - if (!error) { - error = FT_Glyph_Stroke(&glyph, stroker, 1); - } - if (!error) { - FT_Vector origin = {0, 0}; - error = FT_Glyph_To_Bitmap(&glyph, FT_RENDER_MODE_NORMAL, &origin, 1); - } - if (error) { - geterror(error); - goto glyph_error; - } - - bitmap_glyph = (FT_BitmapGlyph)glyph; - - bitmap = bitmap_glyph->bitmap; - xx = px + bitmap_glyph->left; - yy = -(py + bitmap_glyph->top); - } else { - bitmap = glyph_slot->bitmap; - xx = px + glyph_slot->bitmap_left; - yy = -(py + glyph_slot->bitmap_top); - } - - // Null buffer, is dereferenced in FT_Bitmap_Convert - if (!bitmap.buffer && bitmap.rows) { - PyErr_SetString(PyExc_OSError, "Bitmap missing for glyph"); - goto glyph_error; - } - - /* convert non-8bpp bitmaps */ - switch (bitmap.pixel_mode) { - case FT_PIXEL_MODE_MONO: - convert_scale = 255; - break; - case FT_PIXEL_MODE_GRAY2: - convert_scale = 255 / 3; - break; - case FT_PIXEL_MODE_GRAY4: - convert_scale = 255 / 15; - break; - default: - convert_scale = 1; - } - switch (bitmap.pixel_mode) { - case FT_PIXEL_MODE_MONO: - case FT_PIXEL_MODE_GRAY2: - case FT_PIXEL_MODE_GRAY4: - if (!bitmap_converted_ready) { - FT_Bitmap_Init(&bitmap_converted); - bitmap_converted_ready = 1; - } - error = FT_Bitmap_Convert(library, &bitmap, &bitmap_converted, 1); - if (error) { - geterror(error); - goto glyph_error; - } - bitmap = bitmap_converted; - /* bitmap is now FT_PIXEL_MODE_GRAY, fall through */ - case FT_PIXEL_MODE_GRAY: - break; - case FT_PIXEL_MODE_BGRA: - if (color) { - break; - } - /* we didn't ask for color, fall through to default */ - default: - PyErr_SetString(PyExc_OSError, "unsupported bitmap pixel mode"); - goto glyph_error; - } - - /* clip glyph bitmap width to target image bounds */ - x0 = 0; - x1 = bitmap.width; - if (xx < 0) { - x0 = -xx; - } - if (xx + x1 > im->xsize) { - x1 = im->xsize - xx; - } - - source = (unsigned char *)bitmap.buffer; - for (bitmap_y = 0; bitmap_y < bitmap.rows; bitmap_y++, yy++) { - /* clip glyph bitmap height to target image bounds */ - if (yy >= 0 && yy < im->ysize) { - /* blend this glyph into the buffer */ - int k; - unsigned char *target; - unsigned int tmp; - if (color) { - /* target[RGB] returns the color, target[A] returns the mask */ - /* target bands get split again in ImageDraw.text */ - target = (unsigned char *)im->image[yy] + xx * 4; - } else { - target = im->image8[yy] + xx; - } - if (color && bitmap.pixel_mode == FT_PIXEL_MODE_BGRA) { - /* paste color glyph */ - for (k = x0; k < x1; k++) { - unsigned int src_alpha = source[k * 4 + 3]; - - /* paste only if source has data */ - if (src_alpha > 0) { - /* unpremultiply BGRa */ - int src_red = - CLIP8((255 * (int)source[k * 4 + 2]) / src_alpha); - int src_green = - CLIP8((255 * (int)source[k * 4 + 1]) / src_alpha); - int src_blue = - CLIP8((255 * (int)source[k * 4 + 0]) / src_alpha); - - /* blend required if target has data */ - if (target[k * 4 + 3] > 0) { - /* blend RGBA colors */ - target[k * 4 + 0] = - BLEND(src_alpha, target[k * 4 + 0], src_red, tmp); - target[k * 4 + 1] = - BLEND(src_alpha, target[k * 4 + 1], src_green, tmp); - target[k * 4 + 2] = - BLEND(src_alpha, target[k * 4 + 2], src_blue, tmp); - target[k * 4 + 3] = CLIP8( - src_alpha + - MULDIV255( - target[k * 4 + 3], (255 - src_alpha), tmp)); - } else { - /* paste unpremultiplied RGBA values */ - target[k * 4 + 0] = src_red; - target[k * 4 + 1] = src_green; - target[k * 4 + 2] = src_blue; - target[k * 4 + 3] = src_alpha; - } - } - } - } else if (bitmap.pixel_mode == FT_PIXEL_MODE_GRAY) { - if (color) { - unsigned char *ink = (unsigned char *)&foreground_ink; - for (k = x0; k < x1; k++) { - unsigned int src_alpha = source[k] * convert_scale; - if (src_alpha > 0) { - if (target[k * 4 + 3] > 0) { - target[k * 4 + 0] = BLEND( - src_alpha, target[k * 4 + 0], ink[0], tmp); - target[k * 4 + 1] = BLEND( - src_alpha, target[k * 4 + 1], ink[1], tmp); - target[k * 4 + 2] = BLEND( - src_alpha, target[k * 4 + 2], ink[2], tmp); - target[k * 4 + 3] = CLIP8( - src_alpha + - MULDIV255( - target[k * 4 + 3], (255 - src_alpha), tmp)); - } else { - target[k * 4 + 0] = ink[0]; - target[k * 4 + 1] = ink[1]; - target[k * 4 + 2] = ink[2]; - target[k * 4 + 3] = src_alpha; - } - } - } - } else { - for (k = x0; k < x1; k++) { - unsigned int src_alpha = source[k] * convert_scale; - if (src_alpha > 0) { - target[k] = - target[k] > 0 - ? CLIP8( - src_alpha + - MULDIV255( - target[k], (255 - src_alpha), tmp)) - : src_alpha; - } - } - } - } else { - PyErr_SetString(PyExc_OSError, "unsupported bitmap pixel mode"); - goto glyph_error; - } - } - source += bitmap.pitch; - } - x += glyph_info[i].x_advance; - y += glyph_info[i].y_advance; - if (stroker != NULL) { - FT_Done_Glyph(glyph); - } - } - - if (bitmap_converted_ready) { - FT_Bitmap_Done(library, &bitmap_converted); - } - FT_Stroker_Done(stroker); - PyMem_Del(glyph_info); - return Py_BuildValue("N(ii)", image, x_offset, y_offset); - -glyph_error: - Py_DECREF(image); - if (stroker != NULL) { - FT_Done_Glyph(glyph); - } - if (bitmap_converted_ready) { - FT_Bitmap_Done(library, &bitmap_converted); - } - FT_Stroker_Done(stroker); - PyMem_Del(glyph_info); - return NULL; -} - -#if FREETYPE_MAJOR > 2 || (FREETYPE_MAJOR == 2 && FREETYPE_MINOR > 9) || \ - (FREETYPE_MAJOR == 2 && FREETYPE_MINOR == 9 && FREETYPE_PATCH == 1) -static PyObject * -font_getvarnames(FontObject *self) { - int error; - FT_UInt i, j, num_namedstyles, name_count; - FT_MM_Var *master; - FT_SfntName name; - PyObject *list_names, *list_name; - - error = FT_Get_MM_Var(self->face, &master); - if (error) { - return geterror(error); - } - - num_namedstyles = master->num_namedstyles; - list_names = PyList_New(num_namedstyles); - if (list_names == NULL) { - FT_Done_MM_Var(library, master); - return NULL; - } - - name_count = FT_Get_Sfnt_Name_Count(self->face); - for (i = 0; i < name_count; i++) { - error = FT_Get_Sfnt_Name(self->face, i, &name); - if (error) { - Py_DECREF(list_names); - FT_Done_MM_Var(library, master); - return geterror(error); - } - - for (j = 0; j < num_namedstyles; j++) { - if (PyList_GetItem(list_names, j) != NULL) { - continue; - } - - if (master->namedstyle[j].strid == name.name_id) { - list_name = Py_BuildValue("y#", name.string, name.string_len); - PyList_SetItem(list_names, j, list_name); - break; - } - } - } - - FT_Done_MM_Var(library, master); - - return list_names; -} - -static PyObject * -font_getvaraxes(FontObject *self) { - int error; - FT_UInt i, j, num_axis, name_count; - FT_MM_Var *master; - FT_Var_Axis axis; - FT_SfntName name; - PyObject *list_axes, *list_axis, *axis_name; - error = FT_Get_MM_Var(self->face, &master); - if (error) { - return geterror(error); - } - - num_axis = master->num_axis; - name_count = FT_Get_Sfnt_Name_Count(self->face); - - list_axes = PyList_New(num_axis); - if (list_axes == NULL) { - FT_Done_MM_Var(library, master); - return NULL; - } - for (i = 0; i < num_axis; i++) { - axis = master->axis[i]; - - list_axis = PyDict_New(); - if (list_axis == NULL) { - Py_DECREF(list_axes); - FT_Done_MM_Var(library, master); - return NULL; - } - PyObject *minimum = PyLong_FromLong(axis.minimum / 65536); - PyDict_SetItemString(list_axis, "minimum", minimum ? minimum : Py_None); - Py_XDECREF(minimum); - - PyObject *def = PyLong_FromLong(axis.def / 65536); - PyDict_SetItemString(list_axis, "default", def ? def : Py_None); - Py_XDECREF(def); - - PyObject *maximum = PyLong_FromLong(axis.maximum / 65536); - PyDict_SetItemString(list_axis, "maximum", maximum ? maximum : Py_None); - Py_XDECREF(maximum); - - for (j = 0; j < name_count; j++) { - error = FT_Get_Sfnt_Name(self->face, j, &name); - if (error) { - Py_DECREF(list_axis); - Py_DECREF(list_axes); - FT_Done_MM_Var(library, master); - return geterror(error); - } - - if (name.name_id == axis.strid) { - axis_name = Py_BuildValue("y#", name.string, name.string_len); - PyDict_SetItemString( - list_axis, "name", axis_name ? axis_name : Py_None); - Py_XDECREF(axis_name); - break; - } - } - - PyList_SetItem(list_axes, i, list_axis); - } - - FT_Done_MM_Var(library, master); - - return list_axes; -} - -static PyObject * -font_setvarname(FontObject *self, PyObject *args) { - int error; - - int instance_index; - if (!PyArg_ParseTuple(args, "i", &instance_index)) { - return NULL; - } - - error = FT_Set_Named_Instance(self->face, instance_index); - if (error) { - return geterror(error); - } - - Py_INCREF(Py_None); - return Py_None; -} - -static PyObject * -font_setvaraxes(FontObject *self, PyObject *args) { - int error; - - PyObject *axes, *item; - Py_ssize_t i, num_coords; - FT_Fixed *coords; - FT_Fixed coord; - if (!PyArg_ParseTuple(args, "O", &axes)) { - return NULL; - } - - if (!PyList_Check(axes)) { - PyErr_SetString(PyExc_TypeError, "argument must be a list"); - return NULL; - } - - num_coords = PyObject_Length(axes); - coords = (FT_Fixed *)malloc(num_coords * sizeof(FT_Fixed)); - if (coords == NULL) { - return PyErr_NoMemory(); - } - for (i = 0; i < num_coords; i++) { - item = PyList_GET_ITEM(axes, i); - if (PyFloat_Check(item)) { - coord = PyFloat_AS_DOUBLE(item); - } else if (PyLong_Check(item)) { - coord = (float)PyLong_AS_LONG(item); - } else if (PyNumber_Check(item)) { - coord = PyFloat_AsDouble(item); - } else { - free(coords); - PyErr_SetString(PyExc_TypeError, "list must contain numbers"); - return NULL; - } - coords[i] = coord * 65536; - } - - error = FT_Set_Var_Design_Coordinates(self->face, num_coords, coords); - free(coords); - if (error) { - return geterror(error); - } - - Py_INCREF(Py_None); - return Py_None; -} -#endif - -static void -font_dealloc(FontObject *self) { - if (self->face) { - FT_Done_Face(self->face); - } - if (self->font_bytes) { - PyMem_Free(self->font_bytes); - } - PyObject_Del(self); -} - -static PyMethodDef font_methods[] = { - {"render", (PyCFunction)font_render, METH_VARARGS}, - {"getsize", (PyCFunction)font_getsize, METH_VARARGS}, - {"getlength", (PyCFunction)font_getlength, METH_VARARGS}, -#if FREETYPE_MAJOR > 2 || (FREETYPE_MAJOR == 2 && FREETYPE_MINOR > 9) || \ - (FREETYPE_MAJOR == 2 && FREETYPE_MINOR == 9 && FREETYPE_PATCH == 1) - {"getvarnames", (PyCFunction)font_getvarnames, METH_NOARGS}, - {"getvaraxes", (PyCFunction)font_getvaraxes, METH_NOARGS}, - {"setvarname", (PyCFunction)font_setvarname, METH_VARARGS}, - {"setvaraxes", (PyCFunction)font_setvaraxes, METH_VARARGS}, -#endif - {NULL, NULL}}; - -static PyObject * -font_getattr_family(FontObject *self, void *closure) { - if (self->face->family_name) { - return PyUnicode_FromString(self->face->family_name); - } - Py_RETURN_NONE; -} - -static PyObject * -font_getattr_style(FontObject *self, void *closure) { - if (self->face->style_name) { - return PyUnicode_FromString(self->face->style_name); - } - Py_RETURN_NONE; -} - -static PyObject * -font_getattr_ascent(FontObject *self, void *closure) { - return PyLong_FromLong(PIXEL(self->face->size->metrics.ascender)); -} - -static PyObject * -font_getattr_descent(FontObject *self, void *closure) { - return PyLong_FromLong(-PIXEL(self->face->size->metrics.descender)); -} - -static PyObject * -font_getattr_height(FontObject *self, void *closure) { - return PyLong_FromLong(PIXEL(self->face->size->metrics.height)); -} - -static PyObject * -font_getattr_x_ppem(FontObject *self, void *closure) { - return PyLong_FromLong(self->face->size->metrics.x_ppem); -} - -static PyObject * -font_getattr_y_ppem(FontObject *self, void *closure) { - return PyLong_FromLong(self->face->size->metrics.y_ppem); -} - -static PyObject * -font_getattr_glyphs(FontObject *self, void *closure) { - return PyLong_FromLong(self->face->num_glyphs); -} - -static struct PyGetSetDef font_getsetters[] = { - {"family", (getter)font_getattr_family}, - {"style", (getter)font_getattr_style}, - {"ascent", (getter)font_getattr_ascent}, - {"descent", (getter)font_getattr_descent}, - {"height", (getter)font_getattr_height}, - {"x_ppem", (getter)font_getattr_x_ppem}, - {"y_ppem", (getter)font_getattr_y_ppem}, - {"glyphs", (getter)font_getattr_glyphs}, - {NULL}}; - -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*/ -}; - -static PyMethodDef _functions[] = { - {"getfont", (PyCFunction)getfont, METH_VARARGS | METH_KEYWORDS}, {NULL, NULL}}; - -static int -setup_module(PyObject *m) { - PyObject *d; - PyObject *v; - int major, minor, patch; - - d = PyModule_GetDict(m); - - /* Ready object type */ - PyType_Ready(&Font_Type); - - if (FT_Init_FreeType(&library)) { - return 0; /* leave it uninitialized */ - } - - FT_Library_Version(library, &major, &minor, &patch); - - v = PyUnicode_FromFormat("%d.%d.%d", major, minor, patch); - PyDict_SetItemString(d, "freetype2_version", v ? v : Py_None); - Py_XDECREF(v); - -#ifdef HAVE_RAQM -#if defined(HAVE_RAQM_SYSTEM) || defined(HAVE_FRIBIDI_SYSTEM) - have_raqm = 1; -#else - load_fribidi(); - have_raqm = !!p_fribidi; -#endif -#else - have_raqm = 0; -#endif - - /* if we have Raqm, we have all three (but possibly no version info) */ - v = PyBool_FromLong(have_raqm); - PyDict_SetItemString(d, "HAVE_RAQM", v); - PyDict_SetItemString(d, "HAVE_FRIBIDI", v); - PyDict_SetItemString(d, "HAVE_HARFBUZZ", v); - Py_DECREF(v); - if (have_raqm) { - v = NULL; -#ifdef RAQM_VERSION_MAJOR - v = PyUnicode_FromString(raqm_version_string()); -#endif - PyDict_SetItemString(d, "raqm_version", v ? v : Py_None); - Py_XDECREF(v); - - v = NULL; -#ifdef FRIBIDI_MAJOR_VERSION - { - const char *a = strchr(fribidi_version_info, ')'); - const char *b = strchr(fribidi_version_info, '\n'); - if (a && b && a + 2 < b) { - v = PyUnicode_FromStringAndSize(a + 2, b - (a + 2)); - } - } -#endif - PyDict_SetItemString(d, "fribidi_version", v ? v : Py_None); - Py_XDECREF(v); - - v = NULL; -#ifdef HB_VERSION_STRING - v = PyUnicode_FromString(hb_version_string()); -#endif - PyDict_SetItemString(d, "harfbuzz_version", v ? v : Py_None); - Py_XDECREF(v); - } - - return 0; -} - -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 = PyModule_Create(&module_def); - - if (setup_module(m) < 0) { - return NULL; - } - - return m; -} +/* + * PIL FreeType Driver + * + * a FreeType 2.X driver for PIL + * + * history: + * 2001-02-17 fl Created (based on old experimental freetype 1.0 code) + * 2001-04-18 fl Fixed some egcs compiler nits + * 2002-11-08 fl Added unicode support; more font metrics, etc + * 2003-05-20 fl Fixed compilation under 1.5.2 and newer non-unicode builds + * 2003-09-27 fl Added charmap encoding support + * 2004-05-15 fl Fixed compilation for FreeType 2.1.8 + * 2004-09-10 fl Added support for monochrome bitmaps + * 2006-06-18 fl Fixed glyph bearing calculation + * 2007-12-23 fl Fixed crash in family/style attribute fetch + * 2008-01-02 fl Handle Unicode filenames properly + * + * Copyright (c) 1998-2007 by Secret Labs AB + */ + +#define PY_SSIZE_T_CLEAN +#include "Python.h" +#include "libImaging/Imaging.h" + +#include +#include FT_FREETYPE_H +#include FT_GLYPH_H +#include FT_BITMAP_H +#include FT_STROKER_H +#include FT_MULTIPLE_MASTERS_H +#include FT_SFNT_NAMES_H +#ifdef FT_COLOR_H +#include FT_COLOR_H +#endif + +/* -------------------------------------------------------------------- */ +/* error table */ + +#undef FTERRORS_H +#undef __FTERRORS_H__ + +#define FT_ERRORDEF(e, v, s) {e, s}, +#define FT_ERROR_START_LIST { +#define FT_ERROR_END_LIST \ + { 0, 0 } \ + } \ + ; + +#ifdef HAVE_RAQM +#ifdef HAVE_RAQM_SYSTEM +#include +#else +#include "thirdparty/raqm/raqm.h" +#ifdef HAVE_FRIBIDI_SYSTEM +#include +#else +#include "thirdparty/fribidi-shim/fribidi.h" +#include +#endif +#endif +#endif + +static int have_raqm = 0; + +#define LAYOUT_FALLBACK 0 +#define LAYOUT_RAQM 1 + +typedef struct { + int index, x_offset, x_advance, y_offset, y_advance; + unsigned int cluster; +} GlyphInfo; + +struct { + int code; + const char *message; +} ft_errors[] = + +#include FT_ERRORS_H + + /* -------------------------------------------------------------------- */ + /* font objects */ + + static FT_Library library; + +typedef struct { + PyObject_HEAD FT_Face face; + unsigned char *font_bytes; + int layout_engine; +} FontObject; + +static PyTypeObject Font_Type; + +/* round a 26.6 pixel coordinate to the nearest integer */ +#define PIXEL(x) ((((x) + 32) & -64) >> 6) + +static PyObject * +geterror(int code) { + int i; + + for (i = 0; ft_errors[i].message; i++) { + if (ft_errors[i].code == code) { + PyErr_SetString(PyExc_OSError, ft_errors[i].message); + return NULL; + } + } + + PyErr_SetString(PyExc_OSError, "unknown freetype error"); + return NULL; +} + +static PyObject * +getfont(PyObject *self_, PyObject *args, PyObject *kw) { + /* create a font object from a file name and a size (in pixels) */ + + FontObject *self; + int error = 0; + + char *filename = NULL; + float size; + FT_Size_RequestRec req; + FT_Long width; + Py_ssize_t index = 0; + Py_ssize_t layout_engine = 0; + unsigned char *encoding; + unsigned char *font_bytes; + Py_ssize_t font_bytes_size = 0; + static char *kwlist[] = { + "filename", "size", "index", "encoding", "font_bytes", "layout_engine", NULL}; + + if (!library) { + PyErr_SetString(PyExc_OSError, "failed to initialize FreeType library"); + return NULL; + } + +#if PY_MAJOR_VERSION > 3 || PY_MINOR_VERSION > 11 + PyConfig config; + PyConfig_InitPythonConfig(&config); + if (!PyArg_ParseTupleAndKeywords( + args, + kw, + "etf|nsy#n", + kwlist, + config.filesystem_encoding, + &filename, + &size, + &index, + &encoding, + &font_bytes, + &font_bytes_size, + &layout_engine)) { + PyConfig_Clear(&config); + return NULL; + } + PyConfig_Clear(&config); +#else + if (!PyArg_ParseTupleAndKeywords( + args, + kw, + "etf|nsy#n", + kwlist, + Py_FileSystemDefaultEncoding, + &filename, + &size, + &index, + &encoding, + &font_bytes, + &font_bytes_size, + &layout_engine)) { + return NULL; + } +#endif + + self = PyObject_New(FontObject, &Font_Type); + if (!self) { + if (filename) { + PyMem_Free(filename); + } + return NULL; + } + + self->face = NULL; + self->layout_engine = layout_engine; + + if (filename && font_bytes_size <= 0) { + self->font_bytes = NULL; + error = FT_New_Face(library, filename, index, &self->face); + } else { + /* need to have allocated storage for font_bytes for the life of the object.*/ + /* Don't free this before FT_Done_Face */ + self->font_bytes = PyMem_Malloc(font_bytes_size); + if (!self->font_bytes) { + error = FT_Err_Out_Of_Memory; + } + if (!error) { + memcpy(self->font_bytes, font_bytes, (size_t)font_bytes_size); + error = FT_New_Memory_Face( + library, + (FT_Byte *)self->font_bytes, + font_bytes_size, + index, + &self->face); + } + } + + if (!error) { + width = size * 64; + req.type = FT_SIZE_REQUEST_TYPE_NOMINAL; + req.width = width; + req.height = width; + req.horiResolution = 0; + req.vertResolution = 0; + error = FT_Request_Size(self->face, &req); + } + + if (!error && encoding && strlen((char *)encoding) == 4) { + FT_Encoding encoding_tag = + FT_MAKE_TAG(encoding[0], encoding[1], encoding[2], encoding[3]); + error = FT_Select_Charmap(self->face, encoding_tag); + } + if (filename) { + PyMem_Free(filename); + } + + if (error) { + if (self->font_bytes) { + PyMem_Free(self->font_bytes); + self->font_bytes = NULL; + } + Py_DECREF(self); + return geterror(error); + } + + return (PyObject *)self; +} + +static int +font_getchar(PyObject *string, int index, FT_ULong *char_out) { + if (PyUnicode_Check(string)) { + if (index >= PyUnicode_GET_LENGTH(string)) { + return 0; + } + *char_out = PyUnicode_READ_CHAR(string, index); + return 1; + } + return 0; +} + +#ifdef HAVE_RAQM + +static size_t +text_layout_raqm( + PyObject *string, + FontObject *self, + const char *dir, + PyObject *features, + const char *lang, + GlyphInfo **glyph_info) { + size_t i = 0, count = 0, start = 0; + raqm_t *rq; + raqm_glyph_t *glyphs = NULL; + raqm_direction_t direction; + + rq = raqm_create(); + if (rq == NULL) { + PyErr_SetString(PyExc_ValueError, "raqm_create() failed."); + goto failed; + } + + if (PyUnicode_Check(string)) { + Py_UCS4 *text = PyUnicode_AsUCS4Copy(string); + Py_ssize_t size = PyUnicode_GET_LENGTH(string); + if (!text || !size) { + /* return 0 and clean up, no glyphs==no size, + and raqm fails with empty strings */ + goto failed; + } + int set_text = raqm_set_text(rq, text, size); + PyMem_Free(text); + if (!set_text) { + PyErr_SetString(PyExc_ValueError, "raqm_set_text() failed"); + goto failed; + } + if (lang) { + if (!raqm_set_language(rq, lang, start, size)) { + PyErr_SetString(PyExc_ValueError, "raqm_set_language() failed"); + goto failed; + } + } + } else { + PyErr_SetString(PyExc_TypeError, "expected string"); + goto failed; + } + + direction = RAQM_DIRECTION_DEFAULT; + if (dir) { + if (strcmp(dir, "rtl") == 0) { + direction = RAQM_DIRECTION_RTL; + } else if (strcmp(dir, "ltr") == 0) { + direction = RAQM_DIRECTION_LTR; + } else if (strcmp(dir, "ttb") == 0) { + direction = RAQM_DIRECTION_TTB; +#if !defined(RAQM_VERSION_ATLEAST) + /* RAQM_VERSION_ATLEAST was added in Raqm 0.7.0 */ + PyErr_SetString( + PyExc_ValueError, + "libraqm 0.7 or greater required for 'ttb' direction"); + goto failed; +#endif + } else { + PyErr_SetString( + PyExc_ValueError, "direction must be either 'rtl', 'ltr' or 'ttb'"); + goto failed; + } + } + + if (!raqm_set_par_direction(rq, direction)) { + PyErr_SetString(PyExc_ValueError, "raqm_set_par_direction() failed"); + goto failed; + } + + if (features != Py_None) { + int j, len; + PyObject *seq = PySequence_Fast(features, "expected a sequence"); + if (!seq) { + goto failed; + } + + 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_DECREF(seq); + goto failed; + } + feature = PyBytes_AS_STRING(bytes); + size = PyBytes_GET_SIZE(bytes); + if (!raqm_add_font_feature(rq, feature, size)) { + Py_DECREF(seq); + Py_DECREF(bytes); + PyErr_SetString(PyExc_ValueError, "raqm_add_font_feature() failed"); + goto failed; + } + Py_DECREF(bytes); + } + Py_DECREF(seq); + } + + if (!raqm_set_freetype_face(rq, self->face)) { + PyErr_SetString(PyExc_RuntimeError, "raqm_set_freetype_face() failed."); + goto failed; + } + + if (!raqm_layout(rq)) { + PyErr_SetString(PyExc_RuntimeError, "raqm_layout() failed."); + goto failed; + } + + glyphs = raqm_get_glyphs(rq, &count); + if (glyphs == NULL) { + PyErr_SetString(PyExc_ValueError, "raqm_get_glyphs() failed."); + count = 0; + goto failed; + } + + (*glyph_info) = PyMem_New(GlyphInfo, count); + if ((*glyph_info) == NULL) { + PyErr_SetString(PyExc_MemoryError, "PyMem_New() failed"); + count = 0; + goto failed; + } + + for (i = 0; i < count; i++) { + (*glyph_info)[i].index = glyphs[i].index; + (*glyph_info)[i].x_offset = glyphs[i].x_offset; + (*glyph_info)[i].x_advance = glyphs[i].x_advance; + (*glyph_info)[i].y_offset = glyphs[i].y_offset; + (*glyph_info)[i].y_advance = glyphs[i].y_advance; + (*glyph_info)[i].cluster = glyphs[i].cluster; + } + +failed: + raqm_destroy(rq); + return count; +} + +#endif + +static size_t +text_layout_fallback( + PyObject *string, + FontObject *self, + const char *dir, + PyObject *features, + const char *lang, + GlyphInfo **glyph_info, + int mask, + int color) { + int error, load_flags; + FT_ULong ch; + Py_ssize_t count; + FT_GlyphSlot glyph; + FT_Bool kerning = FT_HAS_KERNING(self->face); + FT_UInt last_index = 0; + int i; + + if (features != Py_None || dir != NULL || lang != NULL) { + PyErr_SetString( + PyExc_KeyError, + "setting text direction, language or font features is not supported " + "without libraqm"); + } + if (!PyUnicode_Check(string)) { + PyErr_SetString(PyExc_TypeError, "expected string"); + return 0; + } + + count = 0; + while (font_getchar(string, count, &ch)) { + count++; + } + if (count == 0) { + return 0; + } + + (*glyph_info) = PyMem_New(GlyphInfo, count); + if ((*glyph_info) == NULL) { + PyErr_SetString(PyExc_MemoryError, "PyMem_New() failed"); + return 0; + } + + load_flags = FT_LOAD_DEFAULT; + if (mask) { + load_flags |= FT_LOAD_TARGET_MONO; + } + if (color) { + load_flags |= FT_LOAD_COLOR; + } + for (i = 0; font_getchar(string, i, &ch); i++) { + (*glyph_info)[i].index = FT_Get_Char_Index(self->face, ch); + error = FT_Load_Glyph(self->face, (*glyph_info)[i].index, load_flags); + if (error) { + geterror(error); + return 0; + } + glyph = self->face->glyph; + (*glyph_info)[i].x_offset = 0; + (*glyph_info)[i].y_offset = 0; + if (kerning && last_index && (*glyph_info)[i].index) { + FT_Vector delta; + if (FT_Get_Kerning( + self->face, + last_index, + (*glyph_info)[i].index, + ft_kerning_default, + &delta) == 0) { + (*glyph_info)[i - 1].x_advance += PIXEL(delta.x); + (*glyph_info)[i - 1].y_advance += PIXEL(delta.y); + } + } + + (*glyph_info)[i].x_advance = glyph->metrics.horiAdvance; + // y_advance is only used in ttb, which is not supported by basic layout + (*glyph_info)[i].y_advance = 0; + last_index = (*glyph_info)[i].index; + (*glyph_info)[i].cluster = ch; + } + return count; +} + +static size_t +text_layout( + PyObject *string, + FontObject *self, + const char *dir, + PyObject *features, + const char *lang, + GlyphInfo **glyph_info, + int mask, + int color) { + size_t count; +#ifdef HAVE_RAQM + if (have_raqm && self->layout_engine == LAYOUT_RAQM) { + count = text_layout_raqm(string, self, dir, features, lang, glyph_info); + } else +#endif + { + count = text_layout_fallback( + string, self, dir, features, lang, glyph_info, mask, color); + } + return count; +} + +static PyObject * +font_getlength(FontObject *self, PyObject *args) { + int length; /* length along primary axis, in 26.6 precision */ + GlyphInfo *glyph_info = NULL; /* computed text layout */ + size_t i, count; /* glyph_info index and length */ + int horizontal_dir; /* is primary axis horizontal? */ + int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ + int color = 0; /* is FT_LOAD_COLOR enabled? */ + const char *mode = NULL; + const char *dir = NULL; + const char *lang = NULL; + PyObject *features = Py_None; + PyObject *string; + + /* calculate size and bearing for a given string */ + + if (!PyArg_ParseTuple( + args, "O|zzOz:getlength", &string, &mode, &dir, &features, &lang)) { + return NULL; + } + + horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; + + mask = mode && strcmp(mode, "1") == 0; + color = mode && strcmp(mode, "RGBA") == 0; + + count = text_layout(string, self, dir, features, lang, &glyph_info, mask, color); + if (PyErr_Occurred()) { + return NULL; + } + + length = 0; + for (i = 0; i < count; i++) { + if (horizontal_dir) { + length += glyph_info[i].x_advance; + } else { + length -= glyph_info[i].y_advance; + } + } + + if (glyph_info) { + PyMem_Free(glyph_info); + glyph_info = NULL; + } + + return PyLong_FromLong(length); +} + +static int +bounding_box_and_anchors( + FT_Face face, + const char *anchor, + int horizontal_dir, + GlyphInfo *glyph_info, + size_t count, + int load_flags, + int *width, + int *height, + int *x_offset, + int *y_offset) { + int position; /* pen position along primary axis, in 26.6 precision */ + int advanced; /* pen position along primary axis, in pixels */ + int px, py; /* position of current glyph, in pixels */ + int x_min, x_max, y_min, y_max; /* text bounding box, in pixels */ + int x_anchor, y_anchor; /* offset of point drawn at (0, 0), in pixels */ + int error; + FT_Glyph glyph; + FT_BBox bbox; /* glyph bounding box */ + size_t i; /* glyph_info index */ + /* + * text bounds are given by: + * - bounding boxes of individual glyphs + * - pen line, i.e. 0 to `advanced` along primary axis + * this means point (0, 0) is part of the text bounding box + */ + position = x_min = x_max = y_min = y_max = 0; + for (i = 0; i < count; i++) { + if (horizontal_dir) { + px = PIXEL(position + glyph_info[i].x_offset); + py = PIXEL(glyph_info[i].y_offset); + + position += glyph_info[i].x_advance; + advanced = PIXEL(position); + if (advanced > x_max) { + x_max = advanced; + } + } else { + px = PIXEL(glyph_info[i].x_offset); + py = PIXEL(position + glyph_info[i].y_offset); + + position += glyph_info[i].y_advance; + advanced = PIXEL(position); + if (advanced < y_min) { + y_min = advanced; + } + } + + error = FT_Load_Glyph(face, glyph_info[i].index, load_flags); + if (error) { + geterror(error); + return 1; + } + + error = FT_Get_Glyph(face->glyph, &glyph); + if (error) { + geterror(error); + return 1; + } + + FT_Glyph_Get_CBox(glyph, FT_GLYPH_BBOX_PIXELS, &bbox); + bbox.xMax += px; + if (bbox.xMax > x_max) { + x_max = bbox.xMax; + } + bbox.xMin += px; + if (bbox.xMin < x_min) { + x_min = bbox.xMin; + } + bbox.yMax += py; + if (bbox.yMax > y_max) { + y_max = bbox.yMax; + } + bbox.yMin += py; + if (bbox.yMin < y_min) { + y_min = bbox.yMin; + } + + FT_Done_Glyph(glyph); + } + + if (anchor == NULL) { + anchor = horizontal_dir ? "la" : "lt"; + } + if (strlen(anchor) != 2) { + goto bad_anchor; + } + + x_anchor = y_anchor = 0; + if (count) { + if (horizontal_dir) { + switch (anchor[0]) { + case 'l': // left + x_anchor = 0; + break; + case 'm': // middle (left + right) / 2 + x_anchor = PIXEL(position / 2); + break; + case 'r': // right + x_anchor = PIXEL(position); + break; + case 's': // vertical baseline + default: + goto bad_anchor; + } + switch (anchor[1]) { + case 'a': // ascender + y_anchor = PIXEL(face->size->metrics.ascender); + break; + case 't': // top + y_anchor = y_max; + break; + case 'm': // middle (ascender + descender) / 2 + y_anchor = PIXEL( + (face->size->metrics.ascender + face->size->metrics.descender) / + 2); + break; + case 's': // horizontal baseline + y_anchor = 0; + break; + case 'b': // bottom + y_anchor = y_min; + break; + case 'd': // descender + y_anchor = PIXEL(face->size->metrics.descender); + break; + default: + goto bad_anchor; + } + } else { + switch (anchor[0]) { + case 'l': // left + x_anchor = x_min; + break; + case 'm': // middle (left + right) / 2 + x_anchor = (x_min + x_max) / 2; + break; + case 'r': // right + x_anchor = x_max; + break; + case 's': // vertical baseline + x_anchor = 0; + break; + default: + goto bad_anchor; + } + switch (anchor[1]) { + case 't': // top + y_anchor = 0; + break; + case 'm': // middle (top + bottom) / 2 + y_anchor = PIXEL(position / 2); + break; + case 'b': // bottom + y_anchor = PIXEL(position); + break; + case 'a': // ascender + case 's': // horizontal baseline + case 'd': // descender + default: + goto bad_anchor; + } + } + } + *width = x_max - x_min; + *height = y_max - y_min; + *x_offset = -x_anchor + x_min; + *y_offset = -(-y_anchor + y_max); + return 0; + +bad_anchor: + PyErr_Format(PyExc_ValueError, "bad anchor specified: %s", anchor); + return 1; +} + +static PyObject * +font_getsize(FontObject *self, PyObject *args) { + int width, height, x_offset, y_offset; + int load_flags; /* FreeType load_flags parameter */ + int error; + GlyphInfo *glyph_info = NULL; /* computed text layout */ + size_t count; /* glyph_info length */ + int horizontal_dir; /* is primary axis horizontal? */ + int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ + int color = 0; /* is FT_LOAD_COLOR enabled? */ + const char *mode = NULL; + const char *dir = NULL; + const char *lang = NULL; + const char *anchor = NULL; + PyObject *features = Py_None; + PyObject *string; + + /* calculate size and bearing for a given string */ + + if (!PyArg_ParseTuple( + args, "O|zzOzz:getsize", &string, &mode, &dir, &features, &lang, &anchor)) { + return NULL; + } + + horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; + + mask = mode && strcmp(mode, "1") == 0; + color = mode && strcmp(mode, "RGBA") == 0; + + count = text_layout(string, self, dir, features, lang, &glyph_info, mask, color); + if (PyErr_Occurred()) { + return NULL; + } + + load_flags = FT_LOAD_DEFAULT; + if (mask) { + load_flags |= FT_LOAD_TARGET_MONO; + } + if (color) { + load_flags |= FT_LOAD_COLOR; + } + + error = bounding_box_and_anchors( + self->face, + anchor, + horizontal_dir, + glyph_info, + count, + load_flags, + &width, + &height, + &x_offset, + &y_offset); + if (glyph_info) { + PyMem_Free(glyph_info); + glyph_info = NULL; + } + if (error) { + return NULL; + } + + return Py_BuildValue("(ii)(ii)", width, height, x_offset, y_offset); +} + +static PyObject * +font_render(FontObject *self, PyObject *args) { + int x, y; /* pen position, in 26.6 precision */ + int px, py; /* position of current glyph, in pixels */ + int x_min, y_max; /* text offset in 26.6 precision */ + int load_flags; /* FreeType load_flags parameter */ + int error; + FT_Glyph glyph; + FT_GlyphSlot glyph_slot; + FT_Bitmap bitmap; + FT_Bitmap bitmap_converted; /* initialized lazily, for non-8bpp fonts */ + FT_BitmapGlyph bitmap_glyph; + FT_Stroker stroker = NULL; + int bitmap_converted_ready = 0; /* has bitmap_converted been initialized */ + GlyphInfo *glyph_info = NULL; /* computed text layout */ + size_t i, count; /* glyph_info index and length */ + int xx, yy; /* pixel offset of current glyph bitmap */ + int x0, x1; /* horizontal bounds of glyph bitmap to copy */ + unsigned int bitmap_y; /* glyph bitmap y index */ + unsigned char *source; /* glyph bitmap source buffer */ + unsigned char convert_scale; /* scale factor for non-8bpp bitmaps */ + PyObject *image; + Imaging im; + Py_ssize_t id; + int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ + int color = 0; /* is FT_LOAD_COLOR enabled? */ + int stroke_width = 0; + PY_LONG_LONG foreground_ink_long = 0; + unsigned int foreground_ink; + const char *mode = NULL; + const char *dir = NULL; + const char *lang = NULL; + const char *anchor = NULL; + PyObject *features = Py_None; + PyObject *string; + PyObject *fill; + float x_start = 0; + float y_start = 0; + int width, height, x_offset, y_offset; + int horizontal_dir; /* is primary axis horizontal? */ + + /* render string into given buffer (the buffer *must* have + the right size, or this will crash) */ + + if (!PyArg_ParseTuple( + args, + "OO|zzOzizLffO:render", + &string, + &fill, + &mode, + &dir, + &features, + &lang, + &stroke_width, + &anchor, + &foreground_ink_long, + &x_start, + &y_start)) { + return NULL; + } + + mask = mode && strcmp(mode, "1") == 0; + color = mode && strcmp(mode, "RGBA") == 0; + + foreground_ink = foreground_ink_long; + +#ifdef FT_COLOR_H + if (color) { + FT_Color foreground_color; + FT_Byte *ink = (FT_Byte *)&foreground_ink; + foreground_color.red = ink[0]; + foreground_color.green = ink[1]; + foreground_color.blue = ink[2]; + foreground_color.alpha = + (FT_Byte)255; /* ink alpha is handled in ImageDraw.text */ + FT_Palette_Set_Foreground_Color(self->face, foreground_color); + } +#endif + + count = text_layout(string, self, dir, features, lang, &glyph_info, mask, color); + if (PyErr_Occurred()) { + return NULL; + } + + load_flags = stroke_width ? FT_LOAD_NO_BITMAP : FT_LOAD_DEFAULT; + if (mask) { + load_flags |= FT_LOAD_TARGET_MONO; + } + if (color) { + load_flags |= FT_LOAD_COLOR; + } + + horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; + + error = bounding_box_and_anchors( + self->face, + anchor, + horizontal_dir, + glyph_info, + count, + load_flags, + &width, + &height, + &x_offset, + &y_offset); + if (error) { + PyMem_Del(glyph_info); + return NULL; + } + + width += stroke_width * 2 + ceil(x_start); + height += stroke_width * 2 + ceil(y_start); + image = PyObject_CallFunction(fill, "ii", width, height); + if (image == Py_None) { + PyMem_Del(glyph_info); + return Py_BuildValue("N(ii)", image, 0, 0); + } else if (image == NULL) { + PyMem_Del(glyph_info); + return NULL; + } + PyObject *imageId = PyObject_GetAttrString(image, "id"); + id = PyLong_AsSsize_t(imageId); + Py_XDECREF(imageId); + im = (Imaging)id; + + x_offset -= stroke_width; + y_offset -= stroke_width; + if (count == 0 || width == 0 || height == 0) { + PyMem_Del(glyph_info); + return Py_BuildValue("N(ii)", image, x_offset, y_offset); + } + + if (stroke_width) { + error = FT_Stroker_New(library, &stroker); + if (error) { + geterror(error); + goto glyph_error; + } + + FT_Stroker_Set( + stroker, + (FT_Fixed)stroke_width * 64, + FT_STROKER_LINECAP_ROUND, + FT_STROKER_LINEJOIN_ROUND, + 0); + } + + /* + * calculate x_min and y_max + * must match font_getsize or there may be clipping! + */ + x = y = x_min = y_max = 0; + for (i = 0; i < count; i++) { + px = PIXEL(x + glyph_info[i].x_offset); + py = PIXEL(y + glyph_info[i].y_offset); + + error = + FT_Load_Glyph(self->face, glyph_info[i].index, load_flags | FT_LOAD_RENDER); + if (error) { + geterror(error); + goto glyph_error; + } + + glyph_slot = self->face->glyph; + bitmap = glyph_slot->bitmap; + + if (glyph_slot->bitmap_top + py > y_max) { + y_max = glyph_slot->bitmap_top + py; + } + if (glyph_slot->bitmap_left + px < x_min) { + x_min = glyph_slot->bitmap_left + px; + } + + x += glyph_info[i].x_advance; + y += glyph_info[i].y_advance; + } + + /* set pen position to text origin */ + x = (-x_min + stroke_width + x_start) * 64; + y = (-y_max + (-stroke_width) - y_start) * 64; + + if (stroker == NULL) { + load_flags |= FT_LOAD_RENDER; + } + + for (i = 0; i < count; i++) { + px = PIXEL(x + glyph_info[i].x_offset); + py = PIXEL(y + glyph_info[i].y_offset); + + error = FT_Load_Glyph(self->face, glyph_info[i].index, load_flags); + if (error) { + geterror(error); + goto glyph_error; + } + + glyph_slot = self->face->glyph; + if (stroker != NULL) { + error = FT_Get_Glyph(glyph_slot, &glyph); + if (!error) { + error = FT_Glyph_Stroke(&glyph, stroker, 1); + } + if (!error) { + FT_Vector origin = {0, 0}; + error = FT_Glyph_To_Bitmap(&glyph, FT_RENDER_MODE_NORMAL, &origin, 1); + } + if (error) { + geterror(error); + goto glyph_error; + } + + bitmap_glyph = (FT_BitmapGlyph)glyph; + + bitmap = bitmap_glyph->bitmap; + xx = px + bitmap_glyph->left; + yy = -(py + bitmap_glyph->top); + } else { + bitmap = glyph_slot->bitmap; + xx = px + glyph_slot->bitmap_left; + yy = -(py + glyph_slot->bitmap_top); + } + + // Null buffer, is dereferenced in FT_Bitmap_Convert + if (!bitmap.buffer && bitmap.rows) { + PyErr_SetString(PyExc_OSError, "Bitmap missing for glyph"); + goto glyph_error; + } + + /* convert non-8bpp bitmaps */ + switch (bitmap.pixel_mode) { + case FT_PIXEL_MODE_MONO: + convert_scale = 255; + break; + case FT_PIXEL_MODE_GRAY2: + convert_scale = 255 / 3; + break; + case FT_PIXEL_MODE_GRAY4: + convert_scale = 255 / 15; + break; + default: + convert_scale = 1; + } + switch (bitmap.pixel_mode) { + case FT_PIXEL_MODE_MONO: + case FT_PIXEL_MODE_GRAY2: + case FT_PIXEL_MODE_GRAY4: + if (!bitmap_converted_ready) { + FT_Bitmap_Init(&bitmap_converted); + bitmap_converted_ready = 1; + } + error = FT_Bitmap_Convert(library, &bitmap, &bitmap_converted, 1); + if (error) { + geterror(error); + goto glyph_error; + } + bitmap = bitmap_converted; + /* bitmap is now FT_PIXEL_MODE_GRAY, fall through */ + case FT_PIXEL_MODE_GRAY: + break; + case FT_PIXEL_MODE_BGRA: + if (color) { + break; + } + /* we didn't ask for color, fall through to default */ + default: + PyErr_SetString(PyExc_OSError, "unsupported bitmap pixel mode"); + goto glyph_error; + } + + /* clip glyph bitmap width to target image bounds */ + x0 = 0; + x1 = bitmap.width; + if (xx < 0) { + x0 = -xx; + } + if (xx + x1 > im->xsize) { + x1 = im->xsize - xx; + } + + source = (unsigned char *)bitmap.buffer; + for (bitmap_y = 0; bitmap_y < bitmap.rows; bitmap_y++, yy++) { + /* clip glyph bitmap height to target image bounds */ + if (yy >= 0 && yy < im->ysize) { + /* blend this glyph into the buffer */ + int k; + unsigned char *target; + unsigned int tmp; + if (color) { + /* target[RGB] returns the color, target[A] returns the mask */ + /* target bands get split again in ImageDraw.text */ + target = (unsigned char *)im->image[yy] + xx * 4; + } else { + target = im->image8[yy] + xx; + } + if (color && bitmap.pixel_mode == FT_PIXEL_MODE_BGRA) { + /* paste color glyph */ + for (k = x0; k < x1; k++) { + unsigned int src_alpha = source[k * 4 + 3]; + + /* paste only if source has data */ + if (src_alpha > 0) { + /* unpremultiply BGRa */ + int src_red = + CLIP8((255 * (int)source[k * 4 + 2]) / src_alpha); + int src_green = + CLIP8((255 * (int)source[k * 4 + 1]) / src_alpha); + int src_blue = + CLIP8((255 * (int)source[k * 4 + 0]) / src_alpha); + + /* blend required if target has data */ + if (target[k * 4 + 3] > 0) { + /* blend RGBA colors */ + target[k * 4 + 0] = + BLEND(src_alpha, target[k * 4 + 0], src_red, tmp); + target[k * 4 + 1] = + BLEND(src_alpha, target[k * 4 + 1], src_green, tmp); + target[k * 4 + 2] = + BLEND(src_alpha, target[k * 4 + 2], src_blue, tmp); + target[k * 4 + 3] = CLIP8( + src_alpha + + MULDIV255( + target[k * 4 + 3], (255 - src_alpha), tmp)); + } else { + /* paste unpremultiplied RGBA values */ + target[k * 4 + 0] = src_red; + target[k * 4 + 1] = src_green; + target[k * 4 + 2] = src_blue; + target[k * 4 + 3] = src_alpha; + } + } + } + } else if (bitmap.pixel_mode == FT_PIXEL_MODE_GRAY) { + if (color) { + unsigned char *ink = (unsigned char *)&foreground_ink; + for (k = x0; k < x1; k++) { + unsigned int src_alpha = source[k] * convert_scale; + if (src_alpha > 0) { + if (target[k * 4 + 3] > 0) { + target[k * 4 + 0] = BLEND( + src_alpha, target[k * 4 + 0], ink[0], tmp); + target[k * 4 + 1] = BLEND( + src_alpha, target[k * 4 + 1], ink[1], tmp); + target[k * 4 + 2] = BLEND( + src_alpha, target[k * 4 + 2], ink[2], tmp); + target[k * 4 + 3] = CLIP8( + src_alpha + + MULDIV255( + target[k * 4 + 3], (255 - src_alpha), tmp)); + } else { + target[k * 4 + 0] = ink[0]; + target[k * 4 + 1] = ink[1]; + target[k * 4 + 2] = ink[2]; + target[k * 4 + 3] = src_alpha; + } + } + } + } else { + for (k = x0; k < x1; k++) { + unsigned int src_alpha = source[k] * convert_scale; + if (src_alpha > 0) { + target[k] = + target[k] > 0 + ? CLIP8( + src_alpha + + MULDIV255( + target[k], (255 - src_alpha), tmp)) + : src_alpha; + } + } + } + } else { + PyErr_SetString(PyExc_OSError, "unsupported bitmap pixel mode"); + goto glyph_error; + } + } + source += bitmap.pitch; + } + x += glyph_info[i].x_advance; + y += glyph_info[i].y_advance; + if (stroker != NULL) { + FT_Done_Glyph(glyph); + } + } + + if (bitmap_converted_ready) { + FT_Bitmap_Done(library, &bitmap_converted); + } + FT_Stroker_Done(stroker); + PyMem_Del(glyph_info); + return Py_BuildValue("N(ii)", image, x_offset, y_offset); + +glyph_error: + Py_DECREF(image); + if (stroker != NULL) { + FT_Done_Glyph(glyph); + } + if (bitmap_converted_ready) { + FT_Bitmap_Done(library, &bitmap_converted); + } + FT_Stroker_Done(stroker); + PyMem_Del(glyph_info); + return NULL; +} + +#if FREETYPE_MAJOR > 2 || (FREETYPE_MAJOR == 2 && FREETYPE_MINOR > 9) || \ + (FREETYPE_MAJOR == 2 && FREETYPE_MINOR == 9 && FREETYPE_PATCH == 1) +static PyObject * +font_getvarnames(FontObject *self) { + int error; + FT_UInt i, j, num_namedstyles, name_count; + FT_MM_Var *master; + FT_SfntName name; + PyObject *list_names, *list_name; + + error = FT_Get_MM_Var(self->face, &master); + if (error) { + return geterror(error); + } + + num_namedstyles = master->num_namedstyles; + list_names = PyList_New(num_namedstyles); + if (list_names == NULL) { + FT_Done_MM_Var(library, master); + return NULL; + } + + name_count = FT_Get_Sfnt_Name_Count(self->face); + for (i = 0; i < name_count; i++) { + error = FT_Get_Sfnt_Name(self->face, i, &name); + if (error) { + Py_DECREF(list_names); + FT_Done_MM_Var(library, master); + return geterror(error); + } + + for (j = 0; j < num_namedstyles; j++) { + if (PyList_GetItem(list_names, j) != NULL) { + continue; + } + + if (master->namedstyle[j].strid == name.name_id) { + list_name = Py_BuildValue("y#", name.string, name.string_len); + PyList_SetItem(list_names, j, list_name); + break; + } + } + } + + FT_Done_MM_Var(library, master); + + return list_names; +} + +static PyObject * +font_getvaraxes(FontObject *self) { + int error; + FT_UInt i, j, num_axis, name_count; + FT_MM_Var *master; + FT_Var_Axis axis; + FT_SfntName name; + PyObject *list_axes, *list_axis, *axis_name; + error = FT_Get_MM_Var(self->face, &master); + if (error) { + return geterror(error); + } + + num_axis = master->num_axis; + name_count = FT_Get_Sfnt_Name_Count(self->face); + + list_axes = PyList_New(num_axis); + if (list_axes == NULL) { + FT_Done_MM_Var(library, master); + return NULL; + } + for (i = 0; i < num_axis; i++) { + axis = master->axis[i]; + + list_axis = PyDict_New(); + if (list_axis == NULL) { + Py_DECREF(list_axes); + FT_Done_MM_Var(library, master); + return NULL; + } + PyObject *minimum = PyLong_FromLong(axis.minimum / 65536); + PyDict_SetItemString(list_axis, "minimum", minimum ? minimum : Py_None); + Py_XDECREF(minimum); + + PyObject *def = PyLong_FromLong(axis.def / 65536); + PyDict_SetItemString(list_axis, "default", def ? def : Py_None); + Py_XDECREF(def); + + PyObject *maximum = PyLong_FromLong(axis.maximum / 65536); + PyDict_SetItemString(list_axis, "maximum", maximum ? maximum : Py_None); + Py_XDECREF(maximum); + + for (j = 0; j < name_count; j++) { + error = FT_Get_Sfnt_Name(self->face, j, &name); + if (error) { + Py_DECREF(list_axis); + Py_DECREF(list_axes); + FT_Done_MM_Var(library, master); + return geterror(error); + } + + if (name.name_id == axis.strid) { + axis_name = Py_BuildValue("y#", name.string, name.string_len); + PyDict_SetItemString( + list_axis, "name", axis_name ? axis_name : Py_None); + Py_XDECREF(axis_name); + break; + } + } + + PyList_SetItem(list_axes, i, list_axis); + } + + FT_Done_MM_Var(library, master); + + return list_axes; +} + +static PyObject * +font_setvarname(FontObject *self, PyObject *args) { + int error; + + int instance_index; + if (!PyArg_ParseTuple(args, "i", &instance_index)) { + return NULL; + } + + error = FT_Set_Named_Instance(self->face, instance_index); + if (error) { + return geterror(error); + } + + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject * +font_setvaraxes(FontObject *self, PyObject *args) { + int error; + + PyObject *axes, *item; + Py_ssize_t i, num_coords; + FT_Fixed *coords; + FT_Fixed coord; + if (!PyArg_ParseTuple(args, "O", &axes)) { + return NULL; + } + + if (!PyList_Check(axes)) { + PyErr_SetString(PyExc_TypeError, "argument must be a list"); + return NULL; + } + + num_coords = PyObject_Length(axes); + coords = (FT_Fixed *)malloc(num_coords * sizeof(FT_Fixed)); + if (coords == NULL) { + return PyErr_NoMemory(); + } + for (i = 0; i < num_coords; i++) { + item = PyList_GET_ITEM(axes, i); + if (PyFloat_Check(item)) { + coord = PyFloat_AS_DOUBLE(item); + } else if (PyLong_Check(item)) { + coord = (float)PyLong_AS_LONG(item); + } else if (PyNumber_Check(item)) { + coord = PyFloat_AsDouble(item); + } else { + free(coords); + PyErr_SetString(PyExc_TypeError, "list must contain numbers"); + return NULL; + } + coords[i] = coord * 65536; + } + + error = FT_Set_Var_Design_Coordinates(self->face, num_coords, coords); + free(coords); + if (error) { + return geterror(error); + } + + Py_INCREF(Py_None); + return Py_None; +} +#endif + +static void +font_dealloc(FontObject *self) { + if (self->face) { + FT_Done_Face(self->face); + } + if (self->font_bytes) { + PyMem_Free(self->font_bytes); + } + PyObject_Del(self); +} + +static PyMethodDef font_methods[] = { + {"render", (PyCFunction)font_render, METH_VARARGS}, + {"getsize", (PyCFunction)font_getsize, METH_VARARGS}, + {"getlength", (PyCFunction)font_getlength, METH_VARARGS}, +#if FREETYPE_MAJOR > 2 || (FREETYPE_MAJOR == 2 && FREETYPE_MINOR > 9) || \ + (FREETYPE_MAJOR == 2 && FREETYPE_MINOR == 9 && FREETYPE_PATCH == 1) + {"getvarnames", (PyCFunction)font_getvarnames, METH_NOARGS}, + {"getvaraxes", (PyCFunction)font_getvaraxes, METH_NOARGS}, + {"setvarname", (PyCFunction)font_setvarname, METH_VARARGS}, + {"setvaraxes", (PyCFunction)font_setvaraxes, METH_VARARGS}, +#endif + {NULL, NULL}}; + +static PyObject * +font_getattr_family(FontObject *self, void *closure) { + if (self->face->family_name) { + return PyUnicode_FromString(self->face->family_name); + } + Py_RETURN_NONE; +} + +static PyObject * +font_getattr_style(FontObject *self, void *closure) { + if (self->face->style_name) { + return PyUnicode_FromString(self->face->style_name); + } + Py_RETURN_NONE; +} + +static PyObject * +font_getattr_ascent(FontObject *self, void *closure) { + return PyLong_FromLong(PIXEL(self->face->size->metrics.ascender)); +} + +static PyObject * +font_getattr_descent(FontObject *self, void *closure) { + return PyLong_FromLong(-PIXEL(self->face->size->metrics.descender)); +} + +static PyObject * +font_getattr_height(FontObject *self, void *closure) { + return PyLong_FromLong(PIXEL(self->face->size->metrics.height)); +} + +static PyObject * +font_getattr_x_ppem(FontObject *self, void *closure) { + return PyLong_FromLong(self->face->size->metrics.x_ppem); +} + +static PyObject * +font_getattr_y_ppem(FontObject *self, void *closure) { + return PyLong_FromLong(self->face->size->metrics.y_ppem); +} + +static PyObject * +font_getattr_glyphs(FontObject *self, void *closure) { + return PyLong_FromLong(self->face->num_glyphs); +} + +static struct PyGetSetDef font_getsetters[] = { + {"family", (getter)font_getattr_family}, + {"style", (getter)font_getattr_style}, + {"ascent", (getter)font_getattr_ascent}, + {"descent", (getter)font_getattr_descent}, + {"height", (getter)font_getattr_height}, + {"x_ppem", (getter)font_getattr_x_ppem}, + {"y_ppem", (getter)font_getattr_y_ppem}, + {"glyphs", (getter)font_getattr_glyphs}, + {NULL}}; + +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*/ +}; + +static PyMethodDef _functions[] = { + {"getfont", (PyCFunction)getfont, METH_VARARGS | METH_KEYWORDS}, {NULL, NULL}}; + +static int +setup_module(PyObject *m) { + PyObject *d; + PyObject *v; + int major, minor, patch; + + d = PyModule_GetDict(m); + + /* Ready object type */ + PyType_Ready(&Font_Type); + + if (FT_Init_FreeType(&library)) { + return 0; /* leave it uninitialized */ + } + + FT_Library_Version(library, &major, &minor, &patch); + + v = PyUnicode_FromFormat("%d.%d.%d", major, minor, patch); + PyDict_SetItemString(d, "freetype2_version", v ? v : Py_None); + Py_XDECREF(v); + +#ifdef HAVE_RAQM +#if defined(HAVE_RAQM_SYSTEM) || defined(HAVE_FRIBIDI_SYSTEM) + have_raqm = 1; +#else + load_fribidi(); + have_raqm = !!p_fribidi; +#endif +#else + have_raqm = 0; +#endif + + /* if we have Raqm, we have all three (but possibly no version info) */ + v = PyBool_FromLong(have_raqm); + PyDict_SetItemString(d, "HAVE_RAQM", v); + PyDict_SetItemString(d, "HAVE_FRIBIDI", v); + PyDict_SetItemString(d, "HAVE_HARFBUZZ", v); + Py_DECREF(v); + if (have_raqm) { + v = NULL; +#ifdef RAQM_VERSION_MAJOR + v = PyUnicode_FromString(raqm_version_string()); +#endif + PyDict_SetItemString(d, "raqm_version", v ? v : Py_None); + Py_XDECREF(v); + + v = NULL; +#ifdef FRIBIDI_MAJOR_VERSION + { + const char *a = strchr(fribidi_version_info, ')'); + const char *b = strchr(fribidi_version_info, '\n'); + if (a && b && a + 2 < b) { + v = PyUnicode_FromStringAndSize(a + 2, b - (a + 2)); + } + } +#endif + PyDict_SetItemString(d, "fribidi_version", v ? v : Py_None); + Py_XDECREF(v); + + v = NULL; +#ifdef HB_VERSION_STRING + v = PyUnicode_FromString(hb_version_string()); +#endif + PyDict_SetItemString(d, "harfbuzz_version", v ? v : Py_None); + Py_XDECREF(v); + } + + return 0; +} + +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 = PyModule_Create(&module_def); + + if (setup_module(m) < 0) { + return NULL; + } + + return m; +} diff --git a/src/_imagingmath.c b/src/_imagingmath.c index 067c165b2..4eec78e1d 100644 --- a/src/_imagingmath.c +++ b/src/_imagingmath.c @@ -1,294 +1,294 @@ -/* - * The Python Imaging Library - * - * a simple math add-on for the Python Imaging Library - * - * history: - * 1999-02-15 fl Created - * 2005-05-05 fl Simplified and cleaned up for PIL 1.1.6 - * - * Copyright (c) 1999-2005 by Secret Labs AB - * Copyright (c) 2005 by Fredrik Lundh - * - * See the README file for information on usage and redistribution. - */ - -#include "Python.h" - -#include "libImaging/Imaging.h" - -#include "math.h" -#include "float.h" - -#define MAX_INT32 2147483647.0 -#define MIN_INT32 -2147483648.0 - -#define UNOP(name, op, type) \ - void name(Imaging out, Imaging im1) { \ - int x, y; \ - for (y = 0; y < out->ysize; y++) { \ - type *p0 = (type *)out->image[y]; \ - type *p1 = (type *)im1->image[y]; \ - for (x = 0; x < out->xsize; x++) { \ - *p0 = op(type, *p1); \ - p0++; \ - p1++; \ - } \ - } \ - } - -#define BINOP(name, op, type) \ - void name(Imaging out, Imaging im1, Imaging im2) { \ - int x, y; \ - for (y = 0; y < out->ysize; y++) { \ - type *p0 = (type *)out->image[y]; \ - type *p1 = (type *)im1->image[y]; \ - type *p2 = (type *)im2->image[y]; \ - for (x = 0; x < out->xsize; x++) { \ - *p0 = op(type, *p1, *p2); \ - p0++; \ - p1++; \ - p2++; \ - } \ - } \ - } - -#define NEG(type, v1) -(v1) -#define INVERT(type, v1) ~(v1) - -#define ADD(type, v1, v2) (v1) + (v2) -#define SUB(type, v1, v2) (v1) - (v2) -#define MUL(type, v1, v2) (v1) * (v2) - -#define MIN(type, v1, v2) ((v1) < (v2)) ? (v1) : (v2) -#define MAX(type, v1, v2) ((v1) > (v2)) ? (v1) : (v2) - -#define AND(type, v1, v2) (v1) & (v2) -#define OR(type, v1, v2) (v1) | (v2) -#define XOR(type, v1, v2) (v1) ^ (v2) -#define LSHIFT(type, v1, v2) (v1) << (v2) -#define RSHIFT(type, v1, v2) (v1) >> (v2) - -#define ABS_I(type, v1) abs((v1)) -#define ABS_F(type, v1) fabs((v1)) - -/* -------------------------------------------------------------------- - * some day, we should add FPE protection mechanisms. see pyfpe.h for - * details. - * - * PyFPE_START_PROTECT("Error in foobar", return 0) - * PyFPE_END_PROTECT(result) - */ - -#define DIV_I(type, v1, v2) ((v2) != 0) ? (v1) / (v2) : 0 -#define DIV_F(type, v1, v2) ((v2) != 0.0F) ? (v1) / (v2) : 0.0F - -#define MOD_I(type, v1, v2) ((v2) != 0) ? (v1) % (v2) : 0 -#define MOD_F(type, v1, v2) ((v2) != 0.0F) ? fmod((v1), (v2)) : 0.0F - -static int -powi(int x, int y) { - double v = pow(x, y) + 0.5; - if (errno == EDOM) { - return 0; - } - if (v < MIN_INT32) { - v = MIN_INT32; - } else if (v > MAX_INT32) { - v = MAX_INT32; - } - return (int)v; -} - -#define POW_I(type, v1, v2) powi(v1, v2) -#define POW_F(type, v1, v2) powf(v1, v2) /* FIXME: EDOM handling */ - -#define DIFF_I(type, v1, v2) abs((v1) - (v2)) -#define DIFF_F(type, v1, v2) fabs((v1) - (v2)) - -#define EQ(type, v1, v2) (v1) == (v2) -#define NE(type, v1, v2) (v1) != (v2) -#define LT(type, v1, v2) (v1) < (v2) -#define LE(type, v1, v2) (v1) <= (v2) -#define GT(type, v1, v2) (v1) > (v2) -#define GE(type, v1, v2) (v1) >= (v2) - -UNOP(abs_I, ABS_I, INT32) -UNOP(neg_I, NEG, INT32) - -BINOP(add_I, ADD, INT32) -BINOP(sub_I, SUB, INT32) -BINOP(mul_I, MUL, INT32) -BINOP(div_I, DIV_I, INT32) -BINOP(mod_I, MOD_I, INT32) -BINOP(pow_I, POW_I, INT32) -BINOP(diff_I, DIFF_I, INT32) - -UNOP(invert_I, INVERT, INT32) -BINOP(and_I, AND, INT32) -BINOP(or_I, OR, INT32) -BINOP(xor_I, XOR, INT32) -BINOP(lshift_I, LSHIFT, INT32) -BINOP(rshift_I, RSHIFT, INT32) - -BINOP(min_I, MIN, INT32) -BINOP(max_I, MAX, INT32) - -BINOP(eq_I, EQ, INT32) -BINOP(ne_I, NE, INT32) -BINOP(lt_I, LT, INT32) -BINOP(le_I, LE, INT32) -BINOP(gt_I, GT, INT32) -BINOP(ge_I, GE, INT32) - -UNOP(abs_F, ABS_F, FLOAT32) -UNOP(neg_F, NEG, FLOAT32) - -BINOP(add_F, ADD, FLOAT32) -BINOP(sub_F, SUB, FLOAT32) -BINOP(mul_F, MUL, FLOAT32) -BINOP(div_F, DIV_F, FLOAT32) -BINOP(mod_F, MOD_F, FLOAT32) -BINOP(pow_F, POW_F, FLOAT32) -BINOP(diff_F, DIFF_F, FLOAT32) - -BINOP(min_F, MIN, FLOAT32) -BINOP(max_F, MAX, FLOAT32) - -BINOP(eq_F, EQ, FLOAT32) -BINOP(ne_F, NE, FLOAT32) -BINOP(lt_F, LT, FLOAT32) -BINOP(le_F, LE, FLOAT32) -BINOP(gt_F, GT, FLOAT32) -BINOP(ge_F, GE, FLOAT32) - -static PyObject * -_unop(PyObject *self, PyObject *args) { - Imaging out; - Imaging im1; - void (*unop)(Imaging, Imaging); - - Py_ssize_t op, i0, i1; - if (!PyArg_ParseTuple(args, "nnn", &op, &i0, &i1)) { - return NULL; - } - - out = (Imaging)i0; - im1 = (Imaging)i1; - - unop = (void *)op; - - unop(out, im1); - - Py_INCREF(Py_None); - return Py_None; -} - -static PyObject * -_binop(PyObject *self, PyObject *args) { - Imaging out; - Imaging im1; - Imaging im2; - void (*binop)(Imaging, Imaging, Imaging); - - Py_ssize_t op, i0, i1, i2; - if (!PyArg_ParseTuple(args, "nnnn", &op, &i0, &i1, &i2)) { - return NULL; - } - - out = (Imaging)i0; - im1 = (Imaging)i1; - im2 = (Imaging)i2; - - binop = (void *)op; - - binop(out, im1, im2); - - Py_INCREF(Py_None); - return Py_None; -} - -static PyMethodDef _functions[] = { - {"unop", _unop, 1}, {"binop", _binop, 1}, {NULL, NULL}}; - -static void -install(PyObject *d, char *name, void *value) { - PyObject *v = PyLong_FromSsize_t((Py_ssize_t)value); - if (!v || PyDict_SetItemString(d, name, v)) { - PyErr_Clear(); - } - Py_XDECREF(v); -} - -static int -setup_module(PyObject *m) { - PyObject *d = PyModule_GetDict(m); - - install(d, "abs_I", abs_I); - install(d, "neg_I", neg_I); - install(d, "add_I", add_I); - install(d, "sub_I", sub_I); - install(d, "diff_I", diff_I); - install(d, "mul_I", mul_I); - install(d, "div_I", div_I); - install(d, "mod_I", mod_I); - install(d, "min_I", min_I); - install(d, "max_I", max_I); - install(d, "pow_I", pow_I); - - install(d, "invert_I", invert_I); - install(d, "and_I", and_I); - install(d, "or_I", or_I); - install(d, "xor_I", xor_I); - install(d, "lshift_I", lshift_I); - install(d, "rshift_I", rshift_I); - - install(d, "eq_I", eq_I); - install(d, "ne_I", ne_I); - install(d, "lt_I", lt_I); - install(d, "le_I", le_I); - install(d, "gt_I", gt_I); - install(d, "ge_I", ge_I); - - install(d, "abs_F", abs_F); - install(d, "neg_F", neg_F); - install(d, "add_F", add_F); - install(d, "sub_F", sub_F); - install(d, "diff_F", diff_F); - install(d, "mul_F", mul_F); - install(d, "div_F", div_F); - install(d, "mod_F", mod_F); - install(d, "min_F", min_F); - install(d, "max_F", max_F); - install(d, "pow_F", pow_F); - - install(d, "eq_F", eq_F); - install(d, "ne_F", ne_F); - install(d, "lt_F", lt_F); - install(d, "le_F", le_F); - install(d, "gt_F", gt_F); - install(d, "ge_F", ge_F); - - return 0; -} - -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 = PyModule_Create(&module_def); - - if (setup_module(m) < 0) { - return NULL; - } - - return m; -} +/* + * The Python Imaging Library + * + * a simple math add-on for the Python Imaging Library + * + * history: + * 1999-02-15 fl Created + * 2005-05-05 fl Simplified and cleaned up for PIL 1.1.6 + * + * Copyright (c) 1999-2005 by Secret Labs AB + * Copyright (c) 2005 by Fredrik Lundh + * + * See the README file for information on usage and redistribution. + */ + +#include "Python.h" + +#include "libImaging/Imaging.h" + +#include "math.h" +#include "float.h" + +#define MAX_INT32 2147483647.0 +#define MIN_INT32 -2147483648.0 + +#define UNOP(name, op, type) \ + void name(Imaging out, Imaging im1) { \ + int x, y; \ + for (y = 0; y < out->ysize; y++) { \ + type *p0 = (type *)out->image[y]; \ + type *p1 = (type *)im1->image[y]; \ + for (x = 0; x < out->xsize; x++) { \ + *p0 = op(type, *p1); \ + p0++; \ + p1++; \ + } \ + } \ + } + +#define BINOP(name, op, type) \ + void name(Imaging out, Imaging im1, Imaging im2) { \ + int x, y; \ + for (y = 0; y < out->ysize; y++) { \ + type *p0 = (type *)out->image[y]; \ + type *p1 = (type *)im1->image[y]; \ + type *p2 = (type *)im2->image[y]; \ + for (x = 0; x < out->xsize; x++) { \ + *p0 = op(type, *p1, *p2); \ + p0++; \ + p1++; \ + p2++; \ + } \ + } \ + } + +#define NEG(type, v1) -(v1) +#define INVERT(type, v1) ~(v1) + +#define ADD(type, v1, v2) (v1) + (v2) +#define SUB(type, v1, v2) (v1) - (v2) +#define MUL(type, v1, v2) (v1) * (v2) + +#define MIN(type, v1, v2) ((v1) < (v2)) ? (v1) : (v2) +#define MAX(type, v1, v2) ((v1) > (v2)) ? (v1) : (v2) + +#define AND(type, v1, v2) (v1) & (v2) +#define OR(type, v1, v2) (v1) | (v2) +#define XOR(type, v1, v2) (v1) ^ (v2) +#define LSHIFT(type, v1, v2) (v1) << (v2) +#define RSHIFT(type, v1, v2) (v1) >> (v2) + +#define ABS_I(type, v1) abs((v1)) +#define ABS_F(type, v1) fabs((v1)) + +/* -------------------------------------------------------------------- + * some day, we should add FPE protection mechanisms. see pyfpe.h for + * details. + * + * PyFPE_START_PROTECT("Error in foobar", return 0) + * PyFPE_END_PROTECT(result) + */ + +#define DIV_I(type, v1, v2) ((v2) != 0) ? (v1) / (v2) : 0 +#define DIV_F(type, v1, v2) ((v2) != 0.0F) ? (v1) / (v2) : 0.0F + +#define MOD_I(type, v1, v2) ((v2) != 0) ? (v1) % (v2) : 0 +#define MOD_F(type, v1, v2) ((v2) != 0.0F) ? fmod((v1), (v2)) : 0.0F + +static int +powi(int x, int y) { + double v = pow(x, y) + 0.5; + if (errno == EDOM) { + return 0; + } + if (v < MIN_INT32) { + v = MIN_INT32; + } else if (v > MAX_INT32) { + v = MAX_INT32; + } + return (int)v; +} + +#define POW_I(type, v1, v2) powi(v1, v2) +#define POW_F(type, v1, v2) powf(v1, v2) /* FIXME: EDOM handling */ + +#define DIFF_I(type, v1, v2) abs((v1) - (v2)) +#define DIFF_F(type, v1, v2) fabs((v1) - (v2)) + +#define EQ(type, v1, v2) (v1) == (v2) +#define NE(type, v1, v2) (v1) != (v2) +#define LT(type, v1, v2) (v1) < (v2) +#define LE(type, v1, v2) (v1) <= (v2) +#define GT(type, v1, v2) (v1) > (v2) +#define GE(type, v1, v2) (v1) >= (v2) + +UNOP(abs_I, ABS_I, INT32) +UNOP(neg_I, NEG, INT32) + +BINOP(add_I, ADD, INT32) +BINOP(sub_I, SUB, INT32) +BINOP(mul_I, MUL, INT32) +BINOP(div_I, DIV_I, INT32) +BINOP(mod_I, MOD_I, INT32) +BINOP(pow_I, POW_I, INT32) +BINOP(diff_I, DIFF_I, INT32) + +UNOP(invert_I, INVERT, INT32) +BINOP(and_I, AND, INT32) +BINOP(or_I, OR, INT32) +BINOP(xor_I, XOR, INT32) +BINOP(lshift_I, LSHIFT, INT32) +BINOP(rshift_I, RSHIFT, INT32) + +BINOP(min_I, MIN, INT32) +BINOP(max_I, MAX, INT32) + +BINOP(eq_I, EQ, INT32) +BINOP(ne_I, NE, INT32) +BINOP(lt_I, LT, INT32) +BINOP(le_I, LE, INT32) +BINOP(gt_I, GT, INT32) +BINOP(ge_I, GE, INT32) + +UNOP(abs_F, ABS_F, FLOAT32) +UNOP(neg_F, NEG, FLOAT32) + +BINOP(add_F, ADD, FLOAT32) +BINOP(sub_F, SUB, FLOAT32) +BINOP(mul_F, MUL, FLOAT32) +BINOP(div_F, DIV_F, FLOAT32) +BINOP(mod_F, MOD_F, FLOAT32) +BINOP(pow_F, POW_F, FLOAT32) +BINOP(diff_F, DIFF_F, FLOAT32) + +BINOP(min_F, MIN, FLOAT32) +BINOP(max_F, MAX, FLOAT32) + +BINOP(eq_F, EQ, FLOAT32) +BINOP(ne_F, NE, FLOAT32) +BINOP(lt_F, LT, FLOAT32) +BINOP(le_F, LE, FLOAT32) +BINOP(gt_F, GT, FLOAT32) +BINOP(ge_F, GE, FLOAT32) + +static PyObject * +_unop(PyObject *self, PyObject *args) { + Imaging out; + Imaging im1; + void (*unop)(Imaging, Imaging); + + Py_ssize_t op, i0, i1; + if (!PyArg_ParseTuple(args, "nnn", &op, &i0, &i1)) { + return NULL; + } + + out = (Imaging)i0; + im1 = (Imaging)i1; + + unop = (void *)op; + + unop(out, im1); + + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject * +_binop(PyObject *self, PyObject *args) { + Imaging out; + Imaging im1; + Imaging im2; + void (*binop)(Imaging, Imaging, Imaging); + + Py_ssize_t op, i0, i1, i2; + if (!PyArg_ParseTuple(args, "nnnn", &op, &i0, &i1, &i2)) { + return NULL; + } + + out = (Imaging)i0; + im1 = (Imaging)i1; + im2 = (Imaging)i2; + + binop = (void *)op; + + binop(out, im1, im2); + + Py_INCREF(Py_None); + return Py_None; +} + +static PyMethodDef _functions[] = { + {"unop", _unop, 1}, {"binop", _binop, 1}, {NULL, NULL}}; + +static void +install(PyObject *d, char *name, void *value) { + PyObject *v = PyLong_FromSsize_t((Py_ssize_t)value); + if (!v || PyDict_SetItemString(d, name, v)) { + PyErr_Clear(); + } + Py_XDECREF(v); +} + +static int +setup_module(PyObject *m) { + PyObject *d = PyModule_GetDict(m); + + install(d, "abs_I", abs_I); + install(d, "neg_I", neg_I); + install(d, "add_I", add_I); + install(d, "sub_I", sub_I); + install(d, "diff_I", diff_I); + install(d, "mul_I", mul_I); + install(d, "div_I", div_I); + install(d, "mod_I", mod_I); + install(d, "min_I", min_I); + install(d, "max_I", max_I); + install(d, "pow_I", pow_I); + + install(d, "invert_I", invert_I); + install(d, "and_I", and_I); + install(d, "or_I", or_I); + install(d, "xor_I", xor_I); + install(d, "lshift_I", lshift_I); + install(d, "rshift_I", rshift_I); + + install(d, "eq_I", eq_I); + install(d, "ne_I", ne_I); + install(d, "lt_I", lt_I); + install(d, "le_I", le_I); + install(d, "gt_I", gt_I); + install(d, "ge_I", ge_I); + + install(d, "abs_F", abs_F); + install(d, "neg_F", neg_F); + install(d, "add_F", add_F); + install(d, "sub_F", sub_F); + install(d, "diff_F", diff_F); + install(d, "mul_F", mul_F); + install(d, "div_F", div_F); + install(d, "mod_F", mod_F); + install(d, "min_F", min_F); + install(d, "max_F", max_F); + install(d, "pow_F", pow_F); + + install(d, "eq_F", eq_F); + install(d, "ne_F", ne_F); + install(d, "lt_F", lt_F); + install(d, "le_F", le_F); + install(d, "gt_F", gt_F); + install(d, "ge_F", ge_F); + + return 0; +} + +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 = PyModule_Create(&module_def); + + if (setup_module(m) < 0) { + return NULL; + } + + return m; +} diff --git a/src/_imagingmorph.c b/src/_imagingmorph.c index 8815c2b7e..0edebe2af 100644 --- a/src/_imagingmorph.c +++ b/src/_imagingmorph.c @@ -1,273 +1,273 @@ -/* - * The Python Imaging Library - * - * A binary morphology add-on for the Python Imaging Library - * - * History: - * 2014-06-04 Initial version. - * - * Copyright (c) 2014 Dov Grobgeld - * - * See the README file for information on usage and redistribution. - */ - -#include "Python.h" -#include "libImaging/Imaging.h" - -#define LUT_SIZE (1 << 9) - -/* Apply a morphologic LUT to a binary image. Outputs a - a new binary image. - - Expected parameters: - - 1. a LUT - a 512 byte size lookup table. - 2. an input Imaging image id. - 3. an output Imaging image id - - Returns number of changed pixels. -*/ -static PyObject * -apply(PyObject *self, PyObject *args) { - const char *lut; - PyObject *py_lut; - Py_ssize_t lut_len, i0, i1; - Imaging imgin, imgout; - int width, height; - int row_idx, col_idx; - UINT8 **inrows, **outrows; - int num_changed_pixels = 0; - - if (!PyArg_ParseTuple(args, "Onn", &py_lut, &i0, &i1)) { - PyErr_SetString(PyExc_RuntimeError, "Argument parsing problem"); - return NULL; - } - - if (!PyBytes_Check(py_lut)) { - PyErr_SetString(PyExc_RuntimeError, "The morphology LUT is not a bytes object"); - return NULL; - } - - lut_len = PyBytes_Size(py_lut); - - if (lut_len < LUT_SIZE) { - PyErr_SetString(PyExc_RuntimeError, "The morphology LUT has the wrong size"); - return NULL; - } - - lut = PyBytes_AsString(py_lut); - - imgin = (Imaging)i0; - imgout = (Imaging)i1; - width = imgin->xsize; - height = imgin->ysize; - - if (imgin->type != IMAGING_TYPE_UINT8 || imgin->bands != 1) { - PyErr_SetString(PyExc_RuntimeError, "Unsupported image type"); - return NULL; - } - if (imgout->type != IMAGING_TYPE_UINT8 || imgout->bands != 1) { - PyErr_SetString(PyExc_RuntimeError, "Unsupported image type"); - return NULL; - } - - inrows = imgin->image8; - outrows = imgout->image8; - - for (row_idx = 0; row_idx < height; row_idx++) { - UINT8 *outrow = outrows[row_idx]; - UINT8 *inrow = inrows[row_idx]; - UINT8 *prow, *nrow; /* Previous and next row */ - - /* zero boundary conditions. TBD support other modes */ - outrow[0] = outrow[width - 1] = 0; - if (row_idx == 0 || row_idx == height - 1) { - for (col_idx = 0; col_idx < width; col_idx++) { - outrow[col_idx] = 0; - } - continue; - } - - prow = inrows[row_idx - 1]; - nrow = inrows[row_idx + 1]; - - for (col_idx = 1; col_idx < width - 1; col_idx++) { - int cim = col_idx - 1; - int cip = col_idx + 1; - unsigned char b0 = prow[cim] & 1; - unsigned char b1 = prow[col_idx] & 1; - unsigned char b2 = prow[cip] & 1; - - unsigned char b3 = inrow[cim] & 1; - unsigned char b4 = inrow[col_idx] & 1; - unsigned char b5 = inrow[cip] & 1; - - unsigned char b6 = nrow[cim] & 1; - unsigned char b7 = nrow[col_idx] & 1; - unsigned char b8 = nrow[cip] & 1; - - int lut_idx = - (b0 | (b1 << 1) | (b2 << 2) | (b3 << 3) | (b4 << 4) | (b5 << 5) | - (b6 << 6) | (b7 << 7) | (b8 << 8)); - outrow[col_idx] = 255 * (lut[lut_idx] & 1); - num_changed_pixels += ((b4 & 1) != (outrow[col_idx] & 1)); - } - } - return Py_BuildValue("i", num_changed_pixels); -} - -/* Match a morphologic LUT to a binary image and return a list - of the coordinates of all matching pixels. - - Expected parameters: - - 1. a LUT - a 512 byte size lookup table. - 2. an input Imaging image id. - - Returns list of matching pixels. -*/ -static PyObject * -match(PyObject *self, PyObject *args) { - const char *lut; - PyObject *py_lut; - Py_ssize_t lut_len, i0; - Imaging imgin; - int width, height; - int row_idx, col_idx; - UINT8 **inrows; - PyObject *ret = PyList_New(0); - if (ret == NULL) { - return NULL; - } - - if (!PyArg_ParseTuple(args, "On", &py_lut, &i0)) { - Py_DECREF(ret); - PyErr_SetString(PyExc_RuntimeError, "Argument parsing problem"); - return NULL; - } - - if (!PyBytes_Check(py_lut)) { - Py_DECREF(ret); - PyErr_SetString(PyExc_RuntimeError, "The morphology LUT is not a bytes object"); - return NULL; - } - - lut_len = PyBytes_Size(py_lut); - - if (lut_len < LUT_SIZE) { - Py_DECREF(ret); - PyErr_SetString(PyExc_RuntimeError, "The morphology LUT has the wrong size"); - return NULL; - } - - lut = PyBytes_AsString(py_lut); - imgin = (Imaging)i0; - - if (imgin->type != IMAGING_TYPE_UINT8 || imgin->bands != 1) { - Py_DECREF(ret); - PyErr_SetString(PyExc_RuntimeError, "Unsupported image type"); - return NULL; - } - - inrows = imgin->image8; - width = imgin->xsize; - height = imgin->ysize; - - for (row_idx = 1; row_idx < height - 1; row_idx++) { - UINT8 *inrow = inrows[row_idx]; - UINT8 *prow, *nrow; - - prow = inrows[row_idx - 1]; - nrow = inrows[row_idx + 1]; - - for (col_idx = 1; col_idx < width - 1; col_idx++) { - int cim = col_idx - 1; - int cip = col_idx + 1; - unsigned char b0 = prow[cim] & 1; - unsigned char b1 = prow[col_idx] & 1; - unsigned char b2 = prow[cip] & 1; - - unsigned char b3 = inrow[cim] & 1; - unsigned char b4 = inrow[col_idx] & 1; - unsigned char b5 = inrow[cip] & 1; - - unsigned char b6 = nrow[cim] & 1; - unsigned char b7 = nrow[col_idx] & 1; - unsigned char b8 = nrow[cip] & 1; - - int lut_idx = - (b0 | (b1 << 1) | (b2 << 2) | (b3 << 3) | (b4 << 4) | (b5 << 5) | - (b6 << 6) | (b7 << 7) | (b8 << 8)); - if (lut[lut_idx]) { - PyObject *coordObj = Py_BuildValue("(nn)", col_idx, row_idx); - PyList_Append(ret, coordObj); - Py_XDECREF(coordObj); - } - } - } - - return ret; -} - -/* Return a list of the coordinates of all turned on pixels in an image. - May be used to extract features after a sequence of MorphOps were applied. - This is faster than match as only 1x1 lookup is made. -*/ -static PyObject * -get_on_pixels(PyObject *self, PyObject *args) { - Py_ssize_t i0; - Imaging img; - UINT8 **rows; - int row_idx, col_idx; - int width, height; - PyObject *ret = PyList_New(0); - if (ret == NULL) { - return NULL; - } - - if (!PyArg_ParseTuple(args, "n", &i0)) { - Py_DECREF(ret); - PyErr_SetString(PyExc_RuntimeError, "Argument parsing problem"); - return NULL; - } - img = (Imaging)i0; - rows = img->image8; - width = img->xsize; - height = img->ysize; - - for (row_idx = 0; row_idx < height; row_idx++) { - UINT8 *row = rows[row_idx]; - for (col_idx = 0; col_idx < width; col_idx++) { - if (row[col_idx]) { - PyObject *coordObj = Py_BuildValue("(nn)", col_idx, row_idx); - PyList_Append(ret, coordObj); - Py_XDECREF(coordObj); - } - } - } - return ret; -} - -static PyMethodDef functions[] = { - /* Functions */ - {"apply", (PyCFunction)apply, METH_VARARGS, NULL}, - {"get_on_pixels", (PyCFunction)get_on_pixels, METH_VARARGS, NULL}, - {"match", (PyCFunction)match, METH_VARARGS, NULL}, - {NULL, NULL, 0, NULL}}; - -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 = PyModule_Create(&module_def); - - return m; -} +/* + * The Python Imaging Library + * + * A binary morphology add-on for the Python Imaging Library + * + * History: + * 2014-06-04 Initial version. + * + * Copyright (c) 2014 Dov Grobgeld + * + * See the README file for information on usage and redistribution. + */ + +#include "Python.h" +#include "libImaging/Imaging.h" + +#define LUT_SIZE (1 << 9) + +/* Apply a morphologic LUT to a binary image. Outputs a + a new binary image. + + Expected parameters: + + 1. a LUT - a 512 byte size lookup table. + 2. an input Imaging image id. + 3. an output Imaging image id + + Returns number of changed pixels. +*/ +static PyObject * +apply(PyObject *self, PyObject *args) { + const char *lut; + PyObject *py_lut; + Py_ssize_t lut_len, i0, i1; + Imaging imgin, imgout; + int width, height; + int row_idx, col_idx; + UINT8 **inrows, **outrows; + int num_changed_pixels = 0; + + if (!PyArg_ParseTuple(args, "Onn", &py_lut, &i0, &i1)) { + PyErr_SetString(PyExc_RuntimeError, "Argument parsing problem"); + return NULL; + } + + if (!PyBytes_Check(py_lut)) { + PyErr_SetString(PyExc_RuntimeError, "The morphology LUT is not a bytes object"); + return NULL; + } + + lut_len = PyBytes_Size(py_lut); + + if (lut_len < LUT_SIZE) { + PyErr_SetString(PyExc_RuntimeError, "The morphology LUT has the wrong size"); + return NULL; + } + + lut = PyBytes_AsString(py_lut); + + imgin = (Imaging)i0; + imgout = (Imaging)i1; + width = imgin->xsize; + height = imgin->ysize; + + if (imgin->type != IMAGING_TYPE_UINT8 || imgin->bands != 1) { + PyErr_SetString(PyExc_RuntimeError, "Unsupported image type"); + return NULL; + } + if (imgout->type != IMAGING_TYPE_UINT8 || imgout->bands != 1) { + PyErr_SetString(PyExc_RuntimeError, "Unsupported image type"); + return NULL; + } + + inrows = imgin->image8; + outrows = imgout->image8; + + for (row_idx = 0; row_idx < height; row_idx++) { + UINT8 *outrow = outrows[row_idx]; + UINT8 *inrow = inrows[row_idx]; + UINT8 *prow, *nrow; /* Previous and next row */ + + /* zero boundary conditions. TBD support other modes */ + outrow[0] = outrow[width - 1] = 0; + if (row_idx == 0 || row_idx == height - 1) { + for (col_idx = 0; col_idx < width; col_idx++) { + outrow[col_idx] = 0; + } + continue; + } + + prow = inrows[row_idx - 1]; + nrow = inrows[row_idx + 1]; + + for (col_idx = 1; col_idx < width - 1; col_idx++) { + int cim = col_idx - 1; + int cip = col_idx + 1; + unsigned char b0 = prow[cim] & 1; + unsigned char b1 = prow[col_idx] & 1; + unsigned char b2 = prow[cip] & 1; + + unsigned char b3 = inrow[cim] & 1; + unsigned char b4 = inrow[col_idx] & 1; + unsigned char b5 = inrow[cip] & 1; + + unsigned char b6 = nrow[cim] & 1; + unsigned char b7 = nrow[col_idx] & 1; + unsigned char b8 = nrow[cip] & 1; + + int lut_idx = + (b0 | (b1 << 1) | (b2 << 2) | (b3 << 3) | (b4 << 4) | (b5 << 5) | + (b6 << 6) | (b7 << 7) | (b8 << 8)); + outrow[col_idx] = 255 * (lut[lut_idx] & 1); + num_changed_pixels += ((b4 & 1) != (outrow[col_idx] & 1)); + } + } + return Py_BuildValue("i", num_changed_pixels); +} + +/* Match a morphologic LUT to a binary image and return a list + of the coordinates of all matching pixels. + + Expected parameters: + + 1. a LUT - a 512 byte size lookup table. + 2. an input Imaging image id. + + Returns list of matching pixels. +*/ +static PyObject * +match(PyObject *self, PyObject *args) { + const char *lut; + PyObject *py_lut; + Py_ssize_t lut_len, i0; + Imaging imgin; + int width, height; + int row_idx, col_idx; + UINT8 **inrows; + PyObject *ret = PyList_New(0); + if (ret == NULL) { + return NULL; + } + + if (!PyArg_ParseTuple(args, "On", &py_lut, &i0)) { + Py_DECREF(ret); + PyErr_SetString(PyExc_RuntimeError, "Argument parsing problem"); + return NULL; + } + + if (!PyBytes_Check(py_lut)) { + Py_DECREF(ret); + PyErr_SetString(PyExc_RuntimeError, "The morphology LUT is not a bytes object"); + return NULL; + } + + lut_len = PyBytes_Size(py_lut); + + if (lut_len < LUT_SIZE) { + Py_DECREF(ret); + PyErr_SetString(PyExc_RuntimeError, "The morphology LUT has the wrong size"); + return NULL; + } + + lut = PyBytes_AsString(py_lut); + imgin = (Imaging)i0; + + if (imgin->type != IMAGING_TYPE_UINT8 || imgin->bands != 1) { + Py_DECREF(ret); + PyErr_SetString(PyExc_RuntimeError, "Unsupported image type"); + return NULL; + } + + inrows = imgin->image8; + width = imgin->xsize; + height = imgin->ysize; + + for (row_idx = 1; row_idx < height - 1; row_idx++) { + UINT8 *inrow = inrows[row_idx]; + UINT8 *prow, *nrow; + + prow = inrows[row_idx - 1]; + nrow = inrows[row_idx + 1]; + + for (col_idx = 1; col_idx < width - 1; col_idx++) { + int cim = col_idx - 1; + int cip = col_idx + 1; + unsigned char b0 = prow[cim] & 1; + unsigned char b1 = prow[col_idx] & 1; + unsigned char b2 = prow[cip] & 1; + + unsigned char b3 = inrow[cim] & 1; + unsigned char b4 = inrow[col_idx] & 1; + unsigned char b5 = inrow[cip] & 1; + + unsigned char b6 = nrow[cim] & 1; + unsigned char b7 = nrow[col_idx] & 1; + unsigned char b8 = nrow[cip] & 1; + + int lut_idx = + (b0 | (b1 << 1) | (b2 << 2) | (b3 << 3) | (b4 << 4) | (b5 << 5) | + (b6 << 6) | (b7 << 7) | (b8 << 8)); + if (lut[lut_idx]) { + PyObject *coordObj = Py_BuildValue("(nn)", col_idx, row_idx); + PyList_Append(ret, coordObj); + Py_XDECREF(coordObj); + } + } + } + + return ret; +} + +/* Return a list of the coordinates of all turned on pixels in an image. + May be used to extract features after a sequence of MorphOps were applied. + This is faster than match as only 1x1 lookup is made. +*/ +static PyObject * +get_on_pixels(PyObject *self, PyObject *args) { + Py_ssize_t i0; + Imaging img; + UINT8 **rows; + int row_idx, col_idx; + int width, height; + PyObject *ret = PyList_New(0); + if (ret == NULL) { + return NULL; + } + + if (!PyArg_ParseTuple(args, "n", &i0)) { + Py_DECREF(ret); + PyErr_SetString(PyExc_RuntimeError, "Argument parsing problem"); + return NULL; + } + img = (Imaging)i0; + rows = img->image8; + width = img->xsize; + height = img->ysize; + + for (row_idx = 0; row_idx < height; row_idx++) { + UINT8 *row = rows[row_idx]; + for (col_idx = 0; col_idx < width; col_idx++) { + if (row[col_idx]) { + PyObject *coordObj = Py_BuildValue("(nn)", col_idx, row_idx); + PyList_Append(ret, coordObj); + Py_XDECREF(coordObj); + } + } + } + return ret; +} + +static PyMethodDef functions[] = { + /* Functions */ + {"apply", (PyCFunction)apply, METH_VARARGS, NULL}, + {"get_on_pixels", (PyCFunction)get_on_pixels, METH_VARARGS, NULL}, + {"match", (PyCFunction)match, METH_VARARGS, NULL}, + {NULL, NULL, 0, NULL}}; + +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 = PyModule_Create(&module_def); + + return m; +} diff --git a/src/_imagingtk.c b/src/_imagingtk.c index efa7fc1b6..1fb0fa7e1 100644 --- a/src/_imagingtk.c +++ b/src/_imagingtk.c @@ -1,66 +1,66 @@ -/* - * The Python Imaging Library. - * - * tkinter hooks - * - * history: - * 99-07-26 fl created - * 99-08-15 fl moved to its own support module - * - * Copyright (c) Secret Labs AB 1999. - * - * See the README file for information on usage and redistribution. - */ - -#include "Python.h" -#include "libImaging/Imaging.h" - -#include "Tk/_tkmini.h" - -/* must link with Tk/tkImaging.c */ -extern void -TkImaging_Init(Tcl_Interp *interp); -extern int -load_tkinter_funcs(void); - -static PyObject * -_tkinit(PyObject *self, PyObject *args) { - Tcl_Interp *interp; - - PyObject *arg; - if (!PyArg_ParseTuple(args, "O", &arg)) { - return NULL; - } - - interp = (Tcl_Interp *)PyLong_AsVoidPtr(arg); - - /* This will bomb if interp is invalid... */ - TkImaging_Init(interp); - - Py_INCREF(Py_None); - return Py_None; -} - -static PyMethodDef functions[] = { - /* Tkinter interface stuff */ - {"tkinit", (PyCFunction)_tkinit, 1}, - {NULL, NULL} /* sentinel */ -}; - -PyMODINIT_FUNC -PyInit__imagingtk(void) { - static PyModuleDef module_def = { - PyModuleDef_HEAD_INIT, - "_imagingtk", /* m_name */ - NULL, /* m_doc */ - -1, /* m_size */ - functions, /* m_methods */ - }; - PyObject *m; - m = PyModule_Create(&module_def); - if (load_tkinter_funcs() != 0) { - Py_DECREF(m); - return NULL; - } - return m; -} +/* + * The Python Imaging Library. + * + * tkinter hooks + * + * history: + * 99-07-26 fl created + * 99-08-15 fl moved to its own support module + * + * Copyright (c) Secret Labs AB 1999. + * + * See the README file for information on usage and redistribution. + */ + +#include "Python.h" +#include "libImaging/Imaging.h" + +#include "Tk/_tkmini.h" + +/* must link with Tk/tkImaging.c */ +extern void +TkImaging_Init(Tcl_Interp *interp); +extern int +load_tkinter_funcs(void); + +static PyObject * +_tkinit(PyObject *self, PyObject *args) { + Tcl_Interp *interp; + + PyObject *arg; + if (!PyArg_ParseTuple(args, "O", &arg)) { + return NULL; + } + + interp = (Tcl_Interp *)PyLong_AsVoidPtr(arg); + + /* This will bomb if interp is invalid... */ + TkImaging_Init(interp); + + Py_INCREF(Py_None); + return Py_None; +} + +static PyMethodDef functions[] = { + /* Tkinter interface stuff */ + {"tkinit", (PyCFunction)_tkinit, 1}, + {NULL, NULL} /* sentinel */ +}; + +PyMODINIT_FUNC +PyInit__imagingtk(void) { + static PyModuleDef module_def = { + PyModuleDef_HEAD_INIT, + "_imagingtk", /* m_name */ + NULL, /* m_doc */ + -1, /* m_size */ + functions, /* m_methods */ + }; + PyObject *m; + m = PyModule_Create(&module_def); + if (load_tkinter_funcs() != 0) { + Py_DECREF(m); + return NULL; + } + return m; +} diff --git a/src/_webp.c b/src/_webp.c index 0a70e3357..4b516db8b 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -1,1009 +1,1009 @@ -#define PY_SSIZE_T_CLEAN -#include -#include "libImaging/Imaging.h" -#include -#include -#include - -#ifdef HAVE_WEBPMUX -#include -#include - -/* - * Check the versions from mux.h and demux.h, to ensure the WebPAnimEncoder and - * WebPAnimDecoder APIs are present (initial support was added in 0.5.0). The - * very early versions had some significant differences, so we require later - * versions, before enabling animation support. - */ -#if WEBP_MUX_ABI_VERSION >= 0x0104 && WEBP_DEMUX_ABI_VERSION >= 0x0105 -#define HAVE_WEBPANIM -#endif - -#endif - -void -ImagingSectionEnter(ImagingSectionCookie *cookie) { - *cookie = (PyThreadState *)PyEval_SaveThread(); -} - -void -ImagingSectionLeave(ImagingSectionCookie *cookie) { - PyEval_RestoreThread((PyThreadState *)*cookie); -} - -/* -------------------------------------------------------------------- */ -/* WebP Muxer Error Handling */ -/* -------------------------------------------------------------------- */ - -#ifdef HAVE_WEBPMUX - -static const char *const kErrorMessages[-WEBP_MUX_NOT_ENOUGH_DATA + 1] = { - "WEBP_MUX_NOT_FOUND", - "WEBP_MUX_INVALID_ARGUMENT", - "WEBP_MUX_BAD_DATA", - "WEBP_MUX_MEMORY_ERROR", - "WEBP_MUX_NOT_ENOUGH_DATA"}; - -PyObject * -HandleMuxError(WebPMuxError err, char *chunk) { - char message[100]; - int message_len; - assert(err <= WEBP_MUX_NOT_FOUND && err >= WEBP_MUX_NOT_ENOUGH_DATA); - - // Check for a memory error first - if (err == WEBP_MUX_MEMORY_ERROR) { - return PyErr_NoMemory(); - } - - // Create the error message - if (chunk == NULL) { - message_len = - sprintf(message, "could not assemble chunks: %s", kErrorMessages[-err]); - } else { - message_len = sprintf( - message, "could not set %.4s chunk: %s", chunk, kErrorMessages[-err]); - } - if (message_len < 0) { - PyErr_SetString(PyExc_RuntimeError, "failed to construct error message"); - return NULL; - } - - // Set the proper error type - switch (err) { - case WEBP_MUX_NOT_FOUND: - case WEBP_MUX_INVALID_ARGUMENT: - PyErr_SetString(PyExc_ValueError, message); - break; - - case WEBP_MUX_BAD_DATA: - case WEBP_MUX_NOT_ENOUGH_DATA: - PyErr_SetString(PyExc_OSError, message); - break; - - default: - PyErr_SetString(PyExc_RuntimeError, message); - break; - } - return NULL; -} - -#endif - -/* -------------------------------------------------------------------- */ -/* WebP Animation Support */ -/* -------------------------------------------------------------------- */ - -#ifdef HAVE_WEBPANIM - -// Encoder type -typedef struct { - PyObject_HEAD WebPAnimEncoder *enc; - WebPPicture frame; -} WebPAnimEncoderObject; - -static PyTypeObject WebPAnimEncoder_Type; - -// Decoder type -typedef struct { - PyObject_HEAD WebPAnimDecoder *dec; - WebPAnimInfo info; - WebPData data; - char *mode; -} WebPAnimDecoderObject; - -static PyTypeObject WebPAnimDecoder_Type; - -// Encoder functions -PyObject * -_anim_encoder_new(PyObject *self, PyObject *args) { - int width, height; - uint32_t bgcolor; - int loop_count; - int minimize_size; - int kmin, kmax; - int allow_mixed; - int verbose; - WebPAnimEncoderOptions enc_options; - WebPAnimEncoderObject *encp = NULL; - WebPAnimEncoder *enc = NULL; - - if (!PyArg_ParseTuple( - args, - "iiIiiiiii", - &width, - &height, - &bgcolor, - &loop_count, - &minimize_size, - &kmin, - &kmax, - &allow_mixed, - &verbose)) { - return NULL; - } - - // Setup and configure the encoder's options (these are animation-specific) - if (!WebPAnimEncoderOptionsInit(&enc_options)) { - PyErr_SetString(PyExc_RuntimeError, "failed to initialize encoder options"); - return NULL; - } - enc_options.anim_params.bgcolor = bgcolor; - enc_options.anim_params.loop_count = loop_count; - enc_options.minimize_size = minimize_size; - enc_options.kmin = kmin; - enc_options.kmax = kmax; - enc_options.allow_mixed = allow_mixed; - enc_options.verbose = verbose; - - // Validate canvas dimensions - if (width <= 0 || height <= 0) { - PyErr_SetString(PyExc_ValueError, "invalid canvas dimensions"); - return NULL; - } - - // Create a new animation encoder and picture frame - encp = PyObject_New(WebPAnimEncoderObject, &WebPAnimEncoder_Type); - if (encp) { - if (WebPPictureInit(&(encp->frame))) { - enc = WebPAnimEncoderNew(width, height, &enc_options); - if (enc) { - encp->enc = enc; - return (PyObject *)encp; - } - WebPPictureFree(&(encp->frame)); - } - PyObject_Del(encp); - } - PyErr_SetString(PyExc_RuntimeError, "could not create encoder object"); - return NULL; -} - -void -_anim_encoder_dealloc(PyObject *self) { - WebPAnimEncoderObject *encp = (WebPAnimEncoderObject *)self; - WebPPictureFree(&(encp->frame)); - WebPAnimEncoderDelete(encp->enc); -} - -PyObject * -_anim_encoder_add(PyObject *self, PyObject *args) { - uint8_t *rgb; - Py_ssize_t size; - int timestamp; - int width; - int height; - char *mode; - int lossless; - float quality_factor; - float alpha_quality_factor; - int method; - WebPConfig config; - WebPAnimEncoderObject *encp = (WebPAnimEncoderObject *)self; - WebPAnimEncoder *enc = encp->enc; - WebPPicture *frame = &(encp->frame); - - if (!PyArg_ParseTuple( - args, - "z#iiisiffi", - (char **)&rgb, - &size, - ×tamp, - &width, - &height, - &mode, - &lossless, - &quality_factor, - &alpha_quality_factor, - &method)) { - return NULL; - } - - // Check for NULL frame, which sets duration of final frame - if (!rgb) { - WebPAnimEncoderAdd(enc, NULL, timestamp, NULL); - Py_RETURN_NONE; - } - - // Setup config for this frame - if (!WebPConfigInit(&config)) { - PyErr_SetString(PyExc_RuntimeError, "failed to initialize config!"); - return NULL; - } - config.lossless = lossless; - config.quality = quality_factor; - config.alpha_quality = alpha_quality_factor; - config.method = method; - - // Validate the config - if (!WebPValidateConfig(&config)) { - PyErr_SetString(PyExc_ValueError, "invalid configuration"); - return NULL; - } - - // Populate the frame with raw bytes passed to us - frame->width = width; - frame->height = height; - frame->use_argb = 1; // Don't convert RGB pixels to YUV - if (strcmp(mode, "RGBA") == 0) { - WebPPictureImportRGBA(frame, rgb, 4 * width); - } else if (strcmp(mode, "RGBX") == 0) { - WebPPictureImportRGBX(frame, rgb, 4 * width); - } else { - WebPPictureImportRGB(frame, rgb, 3 * width); - } - - // Add the frame to the encoder - if (!WebPAnimEncoderAdd(enc, frame, timestamp, &config)) { - PyErr_SetString(PyExc_RuntimeError, WebPAnimEncoderGetError(enc)); - return NULL; - } - - Py_RETURN_NONE; -} - -PyObject * -_anim_encoder_assemble(PyObject *self, PyObject *args) { - uint8_t *icc_bytes; - uint8_t *exif_bytes; - uint8_t *xmp_bytes; - Py_ssize_t icc_size; - Py_ssize_t exif_size; - Py_ssize_t xmp_size; - WebPData webp_data; - WebPAnimEncoderObject *encp = (WebPAnimEncoderObject *)self; - WebPAnimEncoder *enc = encp->enc; - WebPMux *mux = NULL; - PyObject *ret = NULL; - - if (!PyArg_ParseTuple( - args, - "s#s#s#", - &icc_bytes, - &icc_size, - &exif_bytes, - &exif_size, - &xmp_bytes, - &xmp_size)) { - return NULL; - } - - // Init the output buffer - WebPDataInit(&webp_data); - - // Assemble everything into the output buffer - if (!WebPAnimEncoderAssemble(enc, &webp_data)) { - PyErr_SetString(PyExc_RuntimeError, WebPAnimEncoderGetError(enc)); - return NULL; - } - - // Re-mux to add metadata as needed - if (icc_size > 0 || exif_size > 0 || xmp_size > 0) { - WebPMuxError err = WEBP_MUX_OK; - int i_icc_size = (int)icc_size; - int i_exif_size = (int)exif_size; - int i_xmp_size = (int)xmp_size; - WebPData icc_profile = {icc_bytes, i_icc_size}; - WebPData exif = {exif_bytes, i_exif_size}; - WebPData xmp = {xmp_bytes, i_xmp_size}; - - mux = WebPMuxCreate(&webp_data, 1); - if (mux == NULL) { - PyErr_SetString(PyExc_RuntimeError, "could not re-mux to add metadata"); - return NULL; - } - WebPDataClear(&webp_data); - - // Add ICCP chunk - if (i_icc_size > 0) { - err = WebPMuxSetChunk(mux, "ICCP", &icc_profile, 1); - if (err != WEBP_MUX_OK) { - return HandleMuxError(err, "ICCP"); - } - } - - // Add EXIF chunk - if (i_exif_size > 0) { - err = WebPMuxSetChunk(mux, "EXIF", &exif, 1); - if (err != WEBP_MUX_OK) { - return HandleMuxError(err, "EXIF"); - } - } - - // Add XMP chunk - if (i_xmp_size > 0) { - err = WebPMuxSetChunk(mux, "XMP ", &xmp, 1); - if (err != WEBP_MUX_OK) { - return HandleMuxError(err, "XMP"); - } - } - - err = WebPMuxAssemble(mux, &webp_data); - if (err != WEBP_MUX_OK) { - return HandleMuxError(err, NULL); - } - } - - // Convert to Python bytes - ret = PyBytes_FromStringAndSize((char *)webp_data.bytes, webp_data.size); - WebPDataClear(&webp_data); - - // If we had to re-mux, we should free it now that we're done with it - if (mux != NULL) { - WebPMuxDelete(mux); - } - - return ret; -} - -// Decoder functions -PyObject * -_anim_decoder_new(PyObject *self, PyObject *args) { - PyBytesObject *webp_string; - const uint8_t *webp; - Py_ssize_t size; - WebPData webp_src; - char *mode; - WebPDecoderConfig config; - WebPAnimDecoderObject *decp = NULL; - WebPAnimDecoder *dec = NULL; - - if (!PyArg_ParseTuple(args, "S", &webp_string)) { - return NULL; - } - PyBytes_AsStringAndSize((PyObject *)webp_string, (char **)&webp, &size); - webp_src.bytes = webp; - webp_src.size = size; - - // Sniff the mode, since the decoder API doesn't tell us - mode = "RGBA"; - if (WebPGetFeatures(webp, size, &config.input) == VP8_STATUS_OK) { - if (!config.input.has_alpha) { - mode = "RGBX"; - } - } - - // Create the decoder (default mode is RGBA, if no options passed) - decp = PyObject_New(WebPAnimDecoderObject, &WebPAnimDecoder_Type); - if (decp) { - decp->mode = mode; - if (WebPDataCopy(&webp_src, &(decp->data))) { - dec = WebPAnimDecoderNew(&(decp->data), NULL); - if (dec) { - if (WebPAnimDecoderGetInfo(dec, &(decp->info))) { - decp->dec = dec; - return (PyObject *)decp; - } - } - WebPDataClear(&(decp->data)); - } - PyObject_Del(decp); - } - PyErr_SetString(PyExc_OSError, "could not create decoder object"); - return NULL; -} - -void -_anim_decoder_dealloc(PyObject *self) { - WebPAnimDecoderObject *decp = (WebPAnimDecoderObject *)self; - WebPDataClear(&(decp->data)); - WebPAnimDecoderDelete(decp->dec); -} - -PyObject * -_anim_decoder_get_info(PyObject *self) { - WebPAnimDecoderObject *decp = (WebPAnimDecoderObject *)self; - WebPAnimInfo *info = &(decp->info); - - return Py_BuildValue( - "IIIIIs", - info->canvas_width, - info->canvas_height, - info->loop_count, - info->bgcolor, - info->frame_count, - decp->mode); -} - -PyObject * -_anim_decoder_get_chunk(PyObject *self, PyObject *args) { - char *mode; - WebPAnimDecoderObject *decp = (WebPAnimDecoderObject *)self; - const WebPDemuxer *demux; - WebPChunkIterator iter; - PyObject *ret; - - if (!PyArg_ParseTuple(args, "s", &mode)) { - return NULL; - } - - demux = WebPAnimDecoderGetDemuxer(decp->dec); - if (!WebPDemuxGetChunk(demux, mode, 1, &iter)) { - Py_RETURN_NONE; - } - - ret = PyBytes_FromStringAndSize((const char *)iter.chunk.bytes, iter.chunk.size); - WebPDemuxReleaseChunkIterator(&iter); - - return ret; -} - -PyObject * -_anim_decoder_get_next(PyObject *self) { - uint8_t *buf; - int timestamp; - int ok; - PyObject *bytes; - PyObject *ret; - ImagingSectionCookie cookie; - WebPAnimDecoderObject *decp = (WebPAnimDecoderObject *)self; - - ImagingSectionEnter(&cookie); - ok = WebPAnimDecoderGetNext(decp->dec, &buf, ×tamp); - ImagingSectionLeave(&cookie); - if (!ok) { - PyErr_SetString(PyExc_OSError, "failed to read next frame"); - return NULL; - } - - bytes = PyBytes_FromStringAndSize( - (char *)buf, decp->info.canvas_width * 4 * decp->info.canvas_height); - - ret = Py_BuildValue("Si", bytes, timestamp); - - Py_DECREF(bytes); - return ret; -} - -PyObject * -_anim_decoder_reset(PyObject *self) { - WebPAnimDecoderObject *decp = (WebPAnimDecoderObject *)self; - WebPAnimDecoderReset(decp->dec); - Py_RETURN_NONE; -} - -/* -------------------------------------------------------------------- */ -/* Type Definitions */ -/* -------------------------------------------------------------------- */ - -// WebPAnimEncoder methods -static struct PyMethodDef _anim_encoder_methods[] = { - {"add", (PyCFunction)_anim_encoder_add, METH_VARARGS, "add"}, - {"assemble", (PyCFunction)_anim_encoder_assemble, METH_VARARGS, "assemble"}, - {NULL, NULL} /* sentinel */ -}; - -// 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*/ -}; - -// WebPAnimDecoder methods -static struct PyMethodDef _anim_decoder_methods[] = { - {"get_info", (PyCFunction)_anim_decoder_get_info, METH_NOARGS, "get_info"}, - {"get_chunk", (PyCFunction)_anim_decoder_get_chunk, METH_VARARGS, "get_chunk"}, - {"get_next", (PyCFunction)_anim_decoder_get_next, METH_NOARGS, "get_next"}, - {"reset", (PyCFunction)_anim_decoder_reset, METH_NOARGS, "reset"}, - {NULL, NULL} /* sentinel */ -}; - -// WebPAnimDecoder type definition -static PyTypeObject WebPAnimDecoder_Type = { - PyVarObject_HEAD_INIT(NULL, 0) "WebPAnimDecoder", /*tp_name */ - sizeof(WebPAnimDecoderObject), /*tp_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*/ -}; - -#endif - -/* -------------------------------------------------------------------- */ -/* Legacy WebP Support */ -/* -------------------------------------------------------------------- */ - -PyObject * -WebPEncode_wrapper(PyObject *self, PyObject *args) { - int width; - int height; - int lossless; - float quality_factor; - float alpha_quality_factor; - int method; - int exact; - uint8_t *rgb; - uint8_t *icc_bytes; - uint8_t *exif_bytes; - uint8_t *xmp_bytes; - uint8_t *output; - char *mode; - Py_ssize_t size; - Py_ssize_t icc_size; - Py_ssize_t exif_size; - Py_ssize_t xmp_size; - size_t ret_size; - int rgba_mode; - int channels; - int ok; - ImagingSectionCookie cookie; - WebPConfig config; - WebPMemoryWriter writer; - WebPPicture pic; - - if (!PyArg_ParseTuple( - args, - "y#iiiffss#iis#s#", - (char **)&rgb, - &size, - &width, - &height, - &lossless, - &quality_factor, - &alpha_quality_factor, - &mode, - &icc_bytes, - &icc_size, - &method, - &exact, - &exif_bytes, - &exif_size, - &xmp_bytes, - &xmp_size)) { - return NULL; - } - - rgba_mode = strcmp(mode, "RGBA") == 0; - if (!rgba_mode && strcmp(mode, "RGB") != 0) { - Py_RETURN_NONE; - } - - channels = rgba_mode ? 4 : 3; - if (size < width * height * channels) { - Py_RETURN_NONE; - } - - // Setup config for this frame - if (!WebPConfigInit(&config)) { - PyErr_SetString(PyExc_RuntimeError, "failed to initialize config!"); - return NULL; - } - config.lossless = lossless; - config.quality = quality_factor; - config.alpha_quality = alpha_quality_factor; - config.method = method; -#if WEBP_ENCODER_ABI_VERSION >= 0x0209 - // the "exact" flag is only available in libwebp 0.5.0 and later - config.exact = exact; -#endif - - // Validate the config - if (!WebPValidateConfig(&config)) { - PyErr_SetString(PyExc_ValueError, "invalid configuration"); - return NULL; - } - - if (!WebPPictureInit(&pic)) { - PyErr_SetString(PyExc_ValueError, "could not initialise picture"); - return NULL; - } - pic.width = width; - pic.height = height; - pic.use_argb = 1; // Don't convert RGB pixels to YUV - - if (rgba_mode) { - WebPPictureImportRGBA(&pic, rgb, channels * width); - } else { - WebPPictureImportRGB(&pic, rgb, channels * width); - } - - WebPMemoryWriterInit(&writer); - pic.writer = WebPMemoryWrite; - pic.custom_ptr = &writer; - - ImagingSectionEnter(&cookie); - ok = WebPEncode(&config, &pic); - ImagingSectionLeave(&cookie); - - WebPPictureFree(&pic); - if (!ok) { - PyErr_Format(PyExc_ValueError, "encoding error %d", (&pic)->error_code); - return NULL; - } - output = writer.mem; - ret_size = writer.size; - -#ifndef HAVE_WEBPMUX - if (ret_size > 0) { - PyObject *ret = PyBytes_FromStringAndSize((char *)output, ret_size); - free(output); - return ret; - } -#else - { - /* I want to truncate the *_size items that get passed into WebP - data. Pypy2.1.0 had some issues where the Py_ssize_t items had - data in the upper byte. (Not sure why, it shouldn't have been there) - */ - int i_icc_size = (int)icc_size; - int i_exif_size = (int)exif_size; - int i_xmp_size = (int)xmp_size; - WebPData output_data = {0}; - WebPData image = {output, ret_size}; - WebPData icc_profile = {icc_bytes, i_icc_size}; - WebPData exif = {exif_bytes, i_exif_size}; - WebPData xmp = {xmp_bytes, i_xmp_size}; - WebPMuxError err; - int dbg = 0; - - int copy_data = 0; // value 1 indicates given data WILL be copied to the mux - // and value 0 indicates data will NOT be copied. - - WebPMux *mux = WebPMuxNew(); - WebPMuxSetImage(mux, &image, copy_data); - - if (dbg) { - /* was getting %ld icc_size == 0, icc_size>0 was true */ - fprintf(stderr, "icc size %d, %d \n", i_icc_size, i_icc_size > 0); - } - - if (i_icc_size > 0) { - if (dbg) { - fprintf(stderr, "Adding ICC Profile\n"); - } - err = WebPMuxSetChunk(mux, "ICCP", &icc_profile, copy_data); - if (err != WEBP_MUX_OK) { - return HandleMuxError(err, "ICCP"); - } - } - - if (dbg) { - fprintf(stderr, "exif size %d \n", i_exif_size); - } - if (i_exif_size > 0) { - if (dbg) { - fprintf(stderr, "Adding Exif Data\n"); - } - err = WebPMuxSetChunk(mux, "EXIF", &exif, copy_data); - if (err != WEBP_MUX_OK) { - return HandleMuxError(err, "EXIF"); - } - } - - if (dbg) { - fprintf(stderr, "xmp size %d \n", i_xmp_size); - } - if (i_xmp_size > 0) { - if (dbg) { - fprintf(stderr, "Adding XMP Data\n"); - } - err = WebPMuxSetChunk(mux, "XMP ", &xmp, copy_data); - if (err != WEBP_MUX_OK) { - return HandleMuxError(err, "XMP "); - } - } - - WebPMuxAssemble(mux, &output_data); - WebPMuxDelete(mux); - free(output); - - ret_size = output_data.size; - if (ret_size > 0) { - PyObject *ret = - PyBytes_FromStringAndSize((char *)output_data.bytes, ret_size); - WebPDataClear(&output_data); - return ret; - } - } -#endif - Py_RETURN_NONE; -} - -PyObject * -WebPDecode_wrapper(PyObject *self, PyObject *args) { - PyBytesObject *webp_string; - const uint8_t *webp; - Py_ssize_t size; - PyObject *ret = Py_None, *bytes = NULL, *pymode = NULL, *icc_profile = NULL, - *exif = NULL; - WebPDecoderConfig config; - VP8StatusCode vp8_status_code = VP8_STATUS_OK; - char *mode = "RGB"; - - if (!PyArg_ParseTuple(args, "S", &webp_string)) { - return NULL; - } - - if (!WebPInitDecoderConfig(&config)) { - Py_RETURN_NONE; - } - - PyBytes_AsStringAndSize((PyObject *)webp_string, (char **)&webp, &size); - - vp8_status_code = WebPGetFeatures(webp, size, &config.input); - if (vp8_status_code == VP8_STATUS_OK) { - // If we don't set it, we don't get alpha. - // Initialized to MODE_RGB - if (config.input.has_alpha) { - config.output.colorspace = MODE_RGBA; - mode = "RGBA"; - } - -#ifndef HAVE_WEBPMUX - vp8_status_code = WebPDecode(webp, size, &config); -#else - { - int copy_data = 0; - WebPData data = {webp, size}; - WebPMuxFrameInfo image; - WebPData icc_profile_data = {0}; - WebPData exif_data = {0}; - - WebPMux *mux = WebPMuxCreate(&data, copy_data); - if (NULL == mux) { - goto end; - } - - if (WEBP_MUX_OK != WebPMuxGetFrame(mux, 1, &image)) { - WebPMuxDelete(mux); - goto end; - } - - webp = image.bitstream.bytes; - size = image.bitstream.size; - - vp8_status_code = WebPDecode(webp, size, &config); - - if (WEBP_MUX_OK == WebPMuxGetChunk(mux, "ICCP", &icc_profile_data)) { - icc_profile = PyBytes_FromStringAndSize( - (const char *)icc_profile_data.bytes, icc_profile_data.size); - } - - if (WEBP_MUX_OK == WebPMuxGetChunk(mux, "EXIF", &exif_data)) { - exif = PyBytes_FromStringAndSize( - (const char *)exif_data.bytes, exif_data.size); - } - - WebPDataClear(&image.bitstream); - WebPMuxDelete(mux); - } -#endif - } - - if (vp8_status_code != VP8_STATUS_OK) { - goto end; - } - - if (config.output.colorspace < MODE_YUV) { - bytes = PyBytes_FromStringAndSize( - (char *)config.output.u.RGBA.rgba, config.output.u.RGBA.size); - } else { - // Skipping YUV for now. Need Test Images. - // UNDONE -- unclear if we'll ever get here if we set mode_rgb* - bytes = PyBytes_FromStringAndSize( - (char *)config.output.u.YUVA.y, config.output.u.YUVA.y_size); - } - - pymode = PyUnicode_FromString(mode); - ret = Py_BuildValue( - "SiiSSS", - bytes, - config.output.width, - config.output.height, - pymode, - NULL == icc_profile ? Py_None : icc_profile, - NULL == exif ? Py_None : exif); - -end: - WebPFreeDecBuffer(&config.output); - - Py_XDECREF(bytes); - Py_XDECREF(pymode); - Py_XDECREF(icc_profile); - Py_XDECREF(exif); - - if (Py_None == ret) { - Py_RETURN_NONE; - } - - return ret; -} - -// Return the decoder's version number, packed in hexadecimal using 8bits for -// each of major/minor/revision. E.g: v2.5.7 is 0x020507. -PyObject * -WebPDecoderVersion_wrapper() { - return Py_BuildValue("i", WebPGetDecoderVersion()); -} - -// Version as string -const char * -WebPDecoderVersion_str(void) { - static char version[20]; - int version_number = WebPGetDecoderVersion(); - sprintf( - version, - "%d.%d.%d", - version_number >> 16, - (version_number >> 8) % 0x100, - version_number % 0x100); - return version; -} - -/* - * The version of webp that ships with (0.1.3) Ubuntu 12.04 doesn't handle alpha well. - * Files that are valid with 0.3 are reported as being invalid. - */ -int -WebPDecoderBuggyAlpha(void) { - return WebPGetDecoderVersion() == 0x0103; -} - -PyObject * -WebPDecoderBuggyAlpha_wrapper() { - return Py_BuildValue("i", WebPDecoderBuggyAlpha()); -} - -/* -------------------------------------------------------------------- */ -/* Module Setup */ -/* -------------------------------------------------------------------- */ - -static PyMethodDef webpMethods[] = { -#ifdef HAVE_WEBPANIM - {"WebPAnimDecoder", _anim_decoder_new, METH_VARARGS, "WebPAnimDecoder"}, - {"WebPAnimEncoder", _anim_encoder_new, METH_VARARGS, "WebPAnimEncoder"}, -#endif - {"WebPEncode", WebPEncode_wrapper, METH_VARARGS, "WebPEncode"}, - {"WebPDecode", WebPDecode_wrapper, METH_VARARGS, "WebPDecode"}, - {"WebPDecoderVersion", WebPDecoderVersion_wrapper, METH_NOARGS, "WebPVersion"}, - {"WebPDecoderBuggyAlpha", - WebPDecoderBuggyAlpha_wrapper, - METH_NOARGS, - "WebPDecoderBuggyAlpha"}, - {NULL, NULL}}; - -void -addMuxFlagToModule(PyObject *m) { - PyObject *have_webpmux; -#ifdef HAVE_WEBPMUX - have_webpmux = Py_True; -#else - have_webpmux = Py_False; -#endif - Py_INCREF(have_webpmux); - PyModule_AddObject(m, "HAVE_WEBPMUX", have_webpmux); -} - -void -addAnimFlagToModule(PyObject *m) { - PyObject *have_webpanim; -#ifdef HAVE_WEBPANIM - have_webpanim = Py_True; -#else - have_webpanim = Py_False; -#endif - Py_INCREF(have_webpanim); - PyModule_AddObject(m, "HAVE_WEBPANIM", have_webpanim); -} - -void -addTransparencyFlagToModule(PyObject *m) { - PyObject *have_transparency = PyBool_FromLong(!WebPDecoderBuggyAlpha()); - if (PyModule_AddObject(m, "HAVE_TRANSPARENCY", have_transparency)) { - Py_DECREF(have_transparency); - } -} - -static int -setup_module(PyObject *m) { -#ifdef HAVE_WEBPANIM - /* Ready object types */ - if (PyType_Ready(&WebPAnimDecoder_Type) < 0 || - PyType_Ready(&WebPAnimEncoder_Type) < 0) { - return -1; - } -#endif - PyObject *d = PyModule_GetDict(m); - addMuxFlagToModule(m); - addAnimFlagToModule(m); - addTransparencyFlagToModule(m); - - PyObject *v = PyUnicode_FromString(WebPDecoderVersion_str()); - PyDict_SetItemString(d, "webpdecoder_version", v ? v : Py_None); - Py_XDECREF(v); - - return 0; -} - -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 = PyModule_Create(&module_def); - if (setup_module(m) < 0) { - Py_DECREF(m); - return NULL; - } - - return m; -} +#define PY_SSIZE_T_CLEAN +#include +#include "libImaging/Imaging.h" +#include +#include +#include + +#ifdef HAVE_WEBPMUX +#include +#include + +/* + * Check the versions from mux.h and demux.h, to ensure the WebPAnimEncoder and + * WebPAnimDecoder APIs are present (initial support was added in 0.5.0). The + * very early versions had some significant differences, so we require later + * versions, before enabling animation support. + */ +#if WEBP_MUX_ABI_VERSION >= 0x0104 && WEBP_DEMUX_ABI_VERSION >= 0x0105 +#define HAVE_WEBPANIM +#endif + +#endif + +void +ImagingSectionEnter(ImagingSectionCookie *cookie) { + *cookie = (PyThreadState *)PyEval_SaveThread(); +} + +void +ImagingSectionLeave(ImagingSectionCookie *cookie) { + PyEval_RestoreThread((PyThreadState *)*cookie); +} + +/* -------------------------------------------------------------------- */ +/* WebP Muxer Error Handling */ +/* -------------------------------------------------------------------- */ + +#ifdef HAVE_WEBPMUX + +static const char *const kErrorMessages[-WEBP_MUX_NOT_ENOUGH_DATA + 1] = { + "WEBP_MUX_NOT_FOUND", + "WEBP_MUX_INVALID_ARGUMENT", + "WEBP_MUX_BAD_DATA", + "WEBP_MUX_MEMORY_ERROR", + "WEBP_MUX_NOT_ENOUGH_DATA"}; + +PyObject * +HandleMuxError(WebPMuxError err, char *chunk) { + char message[100]; + int message_len; + assert(err <= WEBP_MUX_NOT_FOUND && err >= WEBP_MUX_NOT_ENOUGH_DATA); + + // Check for a memory error first + if (err == WEBP_MUX_MEMORY_ERROR) { + return PyErr_NoMemory(); + } + + // Create the error message + if (chunk == NULL) { + message_len = + sprintf(message, "could not assemble chunks: %s", kErrorMessages[-err]); + } else { + message_len = sprintf( + message, "could not set %.4s chunk: %s", chunk, kErrorMessages[-err]); + } + if (message_len < 0) { + PyErr_SetString(PyExc_RuntimeError, "failed to construct error message"); + return NULL; + } + + // Set the proper error type + switch (err) { + case WEBP_MUX_NOT_FOUND: + case WEBP_MUX_INVALID_ARGUMENT: + PyErr_SetString(PyExc_ValueError, message); + break; + + case WEBP_MUX_BAD_DATA: + case WEBP_MUX_NOT_ENOUGH_DATA: + PyErr_SetString(PyExc_OSError, message); + break; + + default: + PyErr_SetString(PyExc_RuntimeError, message); + break; + } + return NULL; +} + +#endif + +/* -------------------------------------------------------------------- */ +/* WebP Animation Support */ +/* -------------------------------------------------------------------- */ + +#ifdef HAVE_WEBPANIM + +// Encoder type +typedef struct { + PyObject_HEAD WebPAnimEncoder *enc; + WebPPicture frame; +} WebPAnimEncoderObject; + +static PyTypeObject WebPAnimEncoder_Type; + +// Decoder type +typedef struct { + PyObject_HEAD WebPAnimDecoder *dec; + WebPAnimInfo info; + WebPData data; + char *mode; +} WebPAnimDecoderObject; + +static PyTypeObject WebPAnimDecoder_Type; + +// Encoder functions +PyObject * +_anim_encoder_new(PyObject *self, PyObject *args) { + int width, height; + uint32_t bgcolor; + int loop_count; + int minimize_size; + int kmin, kmax; + int allow_mixed; + int verbose; + WebPAnimEncoderOptions enc_options; + WebPAnimEncoderObject *encp = NULL; + WebPAnimEncoder *enc = NULL; + + if (!PyArg_ParseTuple( + args, + "iiIiiiiii", + &width, + &height, + &bgcolor, + &loop_count, + &minimize_size, + &kmin, + &kmax, + &allow_mixed, + &verbose)) { + return NULL; + } + + // Setup and configure the encoder's options (these are animation-specific) + if (!WebPAnimEncoderOptionsInit(&enc_options)) { + PyErr_SetString(PyExc_RuntimeError, "failed to initialize encoder options"); + return NULL; + } + enc_options.anim_params.bgcolor = bgcolor; + enc_options.anim_params.loop_count = loop_count; + enc_options.minimize_size = minimize_size; + enc_options.kmin = kmin; + enc_options.kmax = kmax; + enc_options.allow_mixed = allow_mixed; + enc_options.verbose = verbose; + + // Validate canvas dimensions + if (width <= 0 || height <= 0) { + PyErr_SetString(PyExc_ValueError, "invalid canvas dimensions"); + return NULL; + } + + // Create a new animation encoder and picture frame + encp = PyObject_New(WebPAnimEncoderObject, &WebPAnimEncoder_Type); + if (encp) { + if (WebPPictureInit(&(encp->frame))) { + enc = WebPAnimEncoderNew(width, height, &enc_options); + if (enc) { + encp->enc = enc; + return (PyObject *)encp; + } + WebPPictureFree(&(encp->frame)); + } + PyObject_Del(encp); + } + PyErr_SetString(PyExc_RuntimeError, "could not create encoder object"); + return NULL; +} + +void +_anim_encoder_dealloc(PyObject *self) { + WebPAnimEncoderObject *encp = (WebPAnimEncoderObject *)self; + WebPPictureFree(&(encp->frame)); + WebPAnimEncoderDelete(encp->enc); +} + +PyObject * +_anim_encoder_add(PyObject *self, PyObject *args) { + uint8_t *rgb; + Py_ssize_t size; + int timestamp; + int width; + int height; + char *mode; + int lossless; + float quality_factor; + float alpha_quality_factor; + int method; + WebPConfig config; + WebPAnimEncoderObject *encp = (WebPAnimEncoderObject *)self; + WebPAnimEncoder *enc = encp->enc; + WebPPicture *frame = &(encp->frame); + + if (!PyArg_ParseTuple( + args, + "z#iiisiffi", + (char **)&rgb, + &size, + ×tamp, + &width, + &height, + &mode, + &lossless, + &quality_factor, + &alpha_quality_factor, + &method)) { + return NULL; + } + + // Check for NULL frame, which sets duration of final frame + if (!rgb) { + WebPAnimEncoderAdd(enc, NULL, timestamp, NULL); + Py_RETURN_NONE; + } + + // Setup config for this frame + if (!WebPConfigInit(&config)) { + PyErr_SetString(PyExc_RuntimeError, "failed to initialize config!"); + return NULL; + } + config.lossless = lossless; + config.quality = quality_factor; + config.alpha_quality = alpha_quality_factor; + config.method = method; + + // Validate the config + if (!WebPValidateConfig(&config)) { + PyErr_SetString(PyExc_ValueError, "invalid configuration"); + return NULL; + } + + // Populate the frame with raw bytes passed to us + frame->width = width; + frame->height = height; + frame->use_argb = 1; // Don't convert RGB pixels to YUV + if (strcmp(mode, "RGBA") == 0) { + WebPPictureImportRGBA(frame, rgb, 4 * width); + } else if (strcmp(mode, "RGBX") == 0) { + WebPPictureImportRGBX(frame, rgb, 4 * width); + } else { + WebPPictureImportRGB(frame, rgb, 3 * width); + } + + // Add the frame to the encoder + if (!WebPAnimEncoderAdd(enc, frame, timestamp, &config)) { + PyErr_SetString(PyExc_RuntimeError, WebPAnimEncoderGetError(enc)); + return NULL; + } + + Py_RETURN_NONE; +} + +PyObject * +_anim_encoder_assemble(PyObject *self, PyObject *args) { + uint8_t *icc_bytes; + uint8_t *exif_bytes; + uint8_t *xmp_bytes; + Py_ssize_t icc_size; + Py_ssize_t exif_size; + Py_ssize_t xmp_size; + WebPData webp_data; + WebPAnimEncoderObject *encp = (WebPAnimEncoderObject *)self; + WebPAnimEncoder *enc = encp->enc; + WebPMux *mux = NULL; + PyObject *ret = NULL; + + if (!PyArg_ParseTuple( + args, + "s#s#s#", + &icc_bytes, + &icc_size, + &exif_bytes, + &exif_size, + &xmp_bytes, + &xmp_size)) { + return NULL; + } + + // Init the output buffer + WebPDataInit(&webp_data); + + // Assemble everything into the output buffer + if (!WebPAnimEncoderAssemble(enc, &webp_data)) { + PyErr_SetString(PyExc_RuntimeError, WebPAnimEncoderGetError(enc)); + return NULL; + } + + // Re-mux to add metadata as needed + if (icc_size > 0 || exif_size > 0 || xmp_size > 0) { + WebPMuxError err = WEBP_MUX_OK; + int i_icc_size = (int)icc_size; + int i_exif_size = (int)exif_size; + int i_xmp_size = (int)xmp_size; + WebPData icc_profile = {icc_bytes, i_icc_size}; + WebPData exif = {exif_bytes, i_exif_size}; + WebPData xmp = {xmp_bytes, i_xmp_size}; + + mux = WebPMuxCreate(&webp_data, 1); + if (mux == NULL) { + PyErr_SetString(PyExc_RuntimeError, "could not re-mux to add metadata"); + return NULL; + } + WebPDataClear(&webp_data); + + // Add ICCP chunk + if (i_icc_size > 0) { + err = WebPMuxSetChunk(mux, "ICCP", &icc_profile, 1); + if (err != WEBP_MUX_OK) { + return HandleMuxError(err, "ICCP"); + } + } + + // Add EXIF chunk + if (i_exif_size > 0) { + err = WebPMuxSetChunk(mux, "EXIF", &exif, 1); + if (err != WEBP_MUX_OK) { + return HandleMuxError(err, "EXIF"); + } + } + + // Add XMP chunk + if (i_xmp_size > 0) { + err = WebPMuxSetChunk(mux, "XMP ", &xmp, 1); + if (err != WEBP_MUX_OK) { + return HandleMuxError(err, "XMP"); + } + } + + err = WebPMuxAssemble(mux, &webp_data); + if (err != WEBP_MUX_OK) { + return HandleMuxError(err, NULL); + } + } + + // Convert to Python bytes + ret = PyBytes_FromStringAndSize((char *)webp_data.bytes, webp_data.size); + WebPDataClear(&webp_data); + + // If we had to re-mux, we should free it now that we're done with it + if (mux != NULL) { + WebPMuxDelete(mux); + } + + return ret; +} + +// Decoder functions +PyObject * +_anim_decoder_new(PyObject *self, PyObject *args) { + PyBytesObject *webp_string; + const uint8_t *webp; + Py_ssize_t size; + WebPData webp_src; + char *mode; + WebPDecoderConfig config; + WebPAnimDecoderObject *decp = NULL; + WebPAnimDecoder *dec = NULL; + + if (!PyArg_ParseTuple(args, "S", &webp_string)) { + return NULL; + } + PyBytes_AsStringAndSize((PyObject *)webp_string, (char **)&webp, &size); + webp_src.bytes = webp; + webp_src.size = size; + + // Sniff the mode, since the decoder API doesn't tell us + mode = "RGBA"; + if (WebPGetFeatures(webp, size, &config.input) == VP8_STATUS_OK) { + if (!config.input.has_alpha) { + mode = "RGBX"; + } + } + + // Create the decoder (default mode is RGBA, if no options passed) + decp = PyObject_New(WebPAnimDecoderObject, &WebPAnimDecoder_Type); + if (decp) { + decp->mode = mode; + if (WebPDataCopy(&webp_src, &(decp->data))) { + dec = WebPAnimDecoderNew(&(decp->data), NULL); + if (dec) { + if (WebPAnimDecoderGetInfo(dec, &(decp->info))) { + decp->dec = dec; + return (PyObject *)decp; + } + } + WebPDataClear(&(decp->data)); + } + PyObject_Del(decp); + } + PyErr_SetString(PyExc_OSError, "could not create decoder object"); + return NULL; +} + +void +_anim_decoder_dealloc(PyObject *self) { + WebPAnimDecoderObject *decp = (WebPAnimDecoderObject *)self; + WebPDataClear(&(decp->data)); + WebPAnimDecoderDelete(decp->dec); +} + +PyObject * +_anim_decoder_get_info(PyObject *self) { + WebPAnimDecoderObject *decp = (WebPAnimDecoderObject *)self; + WebPAnimInfo *info = &(decp->info); + + return Py_BuildValue( + "IIIIIs", + info->canvas_width, + info->canvas_height, + info->loop_count, + info->bgcolor, + info->frame_count, + decp->mode); +} + +PyObject * +_anim_decoder_get_chunk(PyObject *self, PyObject *args) { + char *mode; + WebPAnimDecoderObject *decp = (WebPAnimDecoderObject *)self; + const WebPDemuxer *demux; + WebPChunkIterator iter; + PyObject *ret; + + if (!PyArg_ParseTuple(args, "s", &mode)) { + return NULL; + } + + demux = WebPAnimDecoderGetDemuxer(decp->dec); + if (!WebPDemuxGetChunk(demux, mode, 1, &iter)) { + Py_RETURN_NONE; + } + + ret = PyBytes_FromStringAndSize((const char *)iter.chunk.bytes, iter.chunk.size); + WebPDemuxReleaseChunkIterator(&iter); + + return ret; +} + +PyObject * +_anim_decoder_get_next(PyObject *self) { + uint8_t *buf; + int timestamp; + int ok; + PyObject *bytes; + PyObject *ret; + ImagingSectionCookie cookie; + WebPAnimDecoderObject *decp = (WebPAnimDecoderObject *)self; + + ImagingSectionEnter(&cookie); + ok = WebPAnimDecoderGetNext(decp->dec, &buf, ×tamp); + ImagingSectionLeave(&cookie); + if (!ok) { + PyErr_SetString(PyExc_OSError, "failed to read next frame"); + return NULL; + } + + bytes = PyBytes_FromStringAndSize( + (char *)buf, decp->info.canvas_width * 4 * decp->info.canvas_height); + + ret = Py_BuildValue("Si", bytes, timestamp); + + Py_DECREF(bytes); + return ret; +} + +PyObject * +_anim_decoder_reset(PyObject *self) { + WebPAnimDecoderObject *decp = (WebPAnimDecoderObject *)self; + WebPAnimDecoderReset(decp->dec); + Py_RETURN_NONE; +} + +/* -------------------------------------------------------------------- */ +/* Type Definitions */ +/* -------------------------------------------------------------------- */ + +// WebPAnimEncoder methods +static struct PyMethodDef _anim_encoder_methods[] = { + {"add", (PyCFunction)_anim_encoder_add, METH_VARARGS, "add"}, + {"assemble", (PyCFunction)_anim_encoder_assemble, METH_VARARGS, "assemble"}, + {NULL, NULL} /* sentinel */ +}; + +// 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*/ +}; + +// WebPAnimDecoder methods +static struct PyMethodDef _anim_decoder_methods[] = { + {"get_info", (PyCFunction)_anim_decoder_get_info, METH_NOARGS, "get_info"}, + {"get_chunk", (PyCFunction)_anim_decoder_get_chunk, METH_VARARGS, "get_chunk"}, + {"get_next", (PyCFunction)_anim_decoder_get_next, METH_NOARGS, "get_next"}, + {"reset", (PyCFunction)_anim_decoder_reset, METH_NOARGS, "reset"}, + {NULL, NULL} /* sentinel */ +}; + +// WebPAnimDecoder type definition +static PyTypeObject WebPAnimDecoder_Type = { + PyVarObject_HEAD_INIT(NULL, 0) "WebPAnimDecoder", /*tp_name */ + sizeof(WebPAnimDecoderObject), /*tp_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*/ +}; + +#endif + +/* -------------------------------------------------------------------- */ +/* Legacy WebP Support */ +/* -------------------------------------------------------------------- */ + +PyObject * +WebPEncode_wrapper(PyObject *self, PyObject *args) { + int width; + int height; + int lossless; + float quality_factor; + float alpha_quality_factor; + int method; + int exact; + uint8_t *rgb; + uint8_t *icc_bytes; + uint8_t *exif_bytes; + uint8_t *xmp_bytes; + uint8_t *output; + char *mode; + Py_ssize_t size; + Py_ssize_t icc_size; + Py_ssize_t exif_size; + Py_ssize_t xmp_size; + size_t ret_size; + int rgba_mode; + int channels; + int ok; + ImagingSectionCookie cookie; + WebPConfig config; + WebPMemoryWriter writer; + WebPPicture pic; + + if (!PyArg_ParseTuple( + args, + "y#iiiffss#iis#s#", + (char **)&rgb, + &size, + &width, + &height, + &lossless, + &quality_factor, + &alpha_quality_factor, + &mode, + &icc_bytes, + &icc_size, + &method, + &exact, + &exif_bytes, + &exif_size, + &xmp_bytes, + &xmp_size)) { + return NULL; + } + + rgba_mode = strcmp(mode, "RGBA") == 0; + if (!rgba_mode && strcmp(mode, "RGB") != 0) { + Py_RETURN_NONE; + } + + channels = rgba_mode ? 4 : 3; + if (size < width * height * channels) { + Py_RETURN_NONE; + } + + // Setup config for this frame + if (!WebPConfigInit(&config)) { + PyErr_SetString(PyExc_RuntimeError, "failed to initialize config!"); + return NULL; + } + config.lossless = lossless; + config.quality = quality_factor; + config.alpha_quality = alpha_quality_factor; + config.method = method; +#if WEBP_ENCODER_ABI_VERSION >= 0x0209 + // the "exact" flag is only available in libwebp 0.5.0 and later + config.exact = exact; +#endif + + // Validate the config + if (!WebPValidateConfig(&config)) { + PyErr_SetString(PyExc_ValueError, "invalid configuration"); + return NULL; + } + + if (!WebPPictureInit(&pic)) { + PyErr_SetString(PyExc_ValueError, "could not initialise picture"); + return NULL; + } + pic.width = width; + pic.height = height; + pic.use_argb = 1; // Don't convert RGB pixels to YUV + + if (rgba_mode) { + WebPPictureImportRGBA(&pic, rgb, channels * width); + } else { + WebPPictureImportRGB(&pic, rgb, channels * width); + } + + WebPMemoryWriterInit(&writer); + pic.writer = WebPMemoryWrite; + pic.custom_ptr = &writer; + + ImagingSectionEnter(&cookie); + ok = WebPEncode(&config, &pic); + ImagingSectionLeave(&cookie); + + WebPPictureFree(&pic); + if (!ok) { + PyErr_Format(PyExc_ValueError, "encoding error %d", (&pic)->error_code); + return NULL; + } + output = writer.mem; + ret_size = writer.size; + +#ifndef HAVE_WEBPMUX + if (ret_size > 0) { + PyObject *ret = PyBytes_FromStringAndSize((char *)output, ret_size); + free(output); + return ret; + } +#else + { + /* I want to truncate the *_size items that get passed into WebP + data. Pypy2.1.0 had some issues where the Py_ssize_t items had + data in the upper byte. (Not sure why, it shouldn't have been there) + */ + int i_icc_size = (int)icc_size; + int i_exif_size = (int)exif_size; + int i_xmp_size = (int)xmp_size; + WebPData output_data = {0}; + WebPData image = {output, ret_size}; + WebPData icc_profile = {icc_bytes, i_icc_size}; + WebPData exif = {exif_bytes, i_exif_size}; + WebPData xmp = {xmp_bytes, i_xmp_size}; + WebPMuxError err; + int dbg = 0; + + int copy_data = 0; // value 1 indicates given data WILL be copied to the mux + // and value 0 indicates data will NOT be copied. + + WebPMux *mux = WebPMuxNew(); + WebPMuxSetImage(mux, &image, copy_data); + + if (dbg) { + /* was getting %ld icc_size == 0, icc_size>0 was true */ + fprintf(stderr, "icc size %d, %d \n", i_icc_size, i_icc_size > 0); + } + + if (i_icc_size > 0) { + if (dbg) { + fprintf(stderr, "Adding ICC Profile\n"); + } + err = WebPMuxSetChunk(mux, "ICCP", &icc_profile, copy_data); + if (err != WEBP_MUX_OK) { + return HandleMuxError(err, "ICCP"); + } + } + + if (dbg) { + fprintf(stderr, "exif size %d \n", i_exif_size); + } + if (i_exif_size > 0) { + if (dbg) { + fprintf(stderr, "Adding Exif Data\n"); + } + err = WebPMuxSetChunk(mux, "EXIF", &exif, copy_data); + if (err != WEBP_MUX_OK) { + return HandleMuxError(err, "EXIF"); + } + } + + if (dbg) { + fprintf(stderr, "xmp size %d \n", i_xmp_size); + } + if (i_xmp_size > 0) { + if (dbg) { + fprintf(stderr, "Adding XMP Data\n"); + } + err = WebPMuxSetChunk(mux, "XMP ", &xmp, copy_data); + if (err != WEBP_MUX_OK) { + return HandleMuxError(err, "XMP "); + } + } + + WebPMuxAssemble(mux, &output_data); + WebPMuxDelete(mux); + free(output); + + ret_size = output_data.size; + if (ret_size > 0) { + PyObject *ret = + PyBytes_FromStringAndSize((char *)output_data.bytes, ret_size); + WebPDataClear(&output_data); + return ret; + } + } +#endif + Py_RETURN_NONE; +} + +PyObject * +WebPDecode_wrapper(PyObject *self, PyObject *args) { + PyBytesObject *webp_string; + const uint8_t *webp; + Py_ssize_t size; + PyObject *ret = Py_None, *bytes = NULL, *pymode = NULL, *icc_profile = NULL, + *exif = NULL; + WebPDecoderConfig config; + VP8StatusCode vp8_status_code = VP8_STATUS_OK; + char *mode = "RGB"; + + if (!PyArg_ParseTuple(args, "S", &webp_string)) { + return NULL; + } + + if (!WebPInitDecoderConfig(&config)) { + Py_RETURN_NONE; + } + + PyBytes_AsStringAndSize((PyObject *)webp_string, (char **)&webp, &size); + + vp8_status_code = WebPGetFeatures(webp, size, &config.input); + if (vp8_status_code == VP8_STATUS_OK) { + // If we don't set it, we don't get alpha. + // Initialized to MODE_RGB + if (config.input.has_alpha) { + config.output.colorspace = MODE_RGBA; + mode = "RGBA"; + } + +#ifndef HAVE_WEBPMUX + vp8_status_code = WebPDecode(webp, size, &config); +#else + { + int copy_data = 0; + WebPData data = {webp, size}; + WebPMuxFrameInfo image; + WebPData icc_profile_data = {0}; + WebPData exif_data = {0}; + + WebPMux *mux = WebPMuxCreate(&data, copy_data); + if (NULL == mux) { + goto end; + } + + if (WEBP_MUX_OK != WebPMuxGetFrame(mux, 1, &image)) { + WebPMuxDelete(mux); + goto end; + } + + webp = image.bitstream.bytes; + size = image.bitstream.size; + + vp8_status_code = WebPDecode(webp, size, &config); + + if (WEBP_MUX_OK == WebPMuxGetChunk(mux, "ICCP", &icc_profile_data)) { + icc_profile = PyBytes_FromStringAndSize( + (const char *)icc_profile_data.bytes, icc_profile_data.size); + } + + if (WEBP_MUX_OK == WebPMuxGetChunk(mux, "EXIF", &exif_data)) { + exif = PyBytes_FromStringAndSize( + (const char *)exif_data.bytes, exif_data.size); + } + + WebPDataClear(&image.bitstream); + WebPMuxDelete(mux); + } +#endif + } + + if (vp8_status_code != VP8_STATUS_OK) { + goto end; + } + + if (config.output.colorspace < MODE_YUV) { + bytes = PyBytes_FromStringAndSize( + (char *)config.output.u.RGBA.rgba, config.output.u.RGBA.size); + } else { + // Skipping YUV for now. Need Test Images. + // UNDONE -- unclear if we'll ever get here if we set mode_rgb* + bytes = PyBytes_FromStringAndSize( + (char *)config.output.u.YUVA.y, config.output.u.YUVA.y_size); + } + + pymode = PyUnicode_FromString(mode); + ret = Py_BuildValue( + "SiiSSS", + bytes, + config.output.width, + config.output.height, + pymode, + NULL == icc_profile ? Py_None : icc_profile, + NULL == exif ? Py_None : exif); + +end: + WebPFreeDecBuffer(&config.output); + + Py_XDECREF(bytes); + Py_XDECREF(pymode); + Py_XDECREF(icc_profile); + Py_XDECREF(exif); + + if (Py_None == ret) { + Py_RETURN_NONE; + } + + return ret; +} + +// Return the decoder's version number, packed in hexadecimal using 8bits for +// each of major/minor/revision. E.g: v2.5.7 is 0x020507. +PyObject * +WebPDecoderVersion_wrapper() { + return Py_BuildValue("i", WebPGetDecoderVersion()); +} + +// Version as string +const char * +WebPDecoderVersion_str(void) { + static char version[20]; + int version_number = WebPGetDecoderVersion(); + sprintf( + version, + "%d.%d.%d", + version_number >> 16, + (version_number >> 8) % 0x100, + version_number % 0x100); + return version; +} + +/* + * The version of webp that ships with (0.1.3) Ubuntu 12.04 doesn't handle alpha well. + * Files that are valid with 0.3 are reported as being invalid. + */ +int +WebPDecoderBuggyAlpha(void) { + return WebPGetDecoderVersion() == 0x0103; +} + +PyObject * +WebPDecoderBuggyAlpha_wrapper() { + return Py_BuildValue("i", WebPDecoderBuggyAlpha()); +} + +/* -------------------------------------------------------------------- */ +/* Module Setup */ +/* -------------------------------------------------------------------- */ + +static PyMethodDef webpMethods[] = { +#ifdef HAVE_WEBPANIM + {"WebPAnimDecoder", _anim_decoder_new, METH_VARARGS, "WebPAnimDecoder"}, + {"WebPAnimEncoder", _anim_encoder_new, METH_VARARGS, "WebPAnimEncoder"}, +#endif + {"WebPEncode", WebPEncode_wrapper, METH_VARARGS, "WebPEncode"}, + {"WebPDecode", WebPDecode_wrapper, METH_VARARGS, "WebPDecode"}, + {"WebPDecoderVersion", WebPDecoderVersion_wrapper, METH_NOARGS, "WebPVersion"}, + {"WebPDecoderBuggyAlpha", + WebPDecoderBuggyAlpha_wrapper, + METH_NOARGS, + "WebPDecoderBuggyAlpha"}, + {NULL, NULL}}; + +void +addMuxFlagToModule(PyObject *m) { + PyObject *have_webpmux; +#ifdef HAVE_WEBPMUX + have_webpmux = Py_True; +#else + have_webpmux = Py_False; +#endif + Py_INCREF(have_webpmux); + PyModule_AddObject(m, "HAVE_WEBPMUX", have_webpmux); +} + +void +addAnimFlagToModule(PyObject *m) { + PyObject *have_webpanim; +#ifdef HAVE_WEBPANIM + have_webpanim = Py_True; +#else + have_webpanim = Py_False; +#endif + Py_INCREF(have_webpanim); + PyModule_AddObject(m, "HAVE_WEBPANIM", have_webpanim); +} + +void +addTransparencyFlagToModule(PyObject *m) { + PyObject *have_transparency = PyBool_FromLong(!WebPDecoderBuggyAlpha()); + if (PyModule_AddObject(m, "HAVE_TRANSPARENCY", have_transparency)) { + Py_DECREF(have_transparency); + } +} + +static int +setup_module(PyObject *m) { +#ifdef HAVE_WEBPANIM + /* Ready object types */ + if (PyType_Ready(&WebPAnimDecoder_Type) < 0 || + PyType_Ready(&WebPAnimEncoder_Type) < 0) { + return -1; + } +#endif + PyObject *d = PyModule_GetDict(m); + addMuxFlagToModule(m); + addAnimFlagToModule(m); + addTransparencyFlagToModule(m); + + PyObject *v = PyUnicode_FromString(WebPDecoderVersion_str()); + PyDict_SetItemString(d, "webpdecoder_version", v ? v : Py_None); + Py_XDECREF(v); + + return 0; +} + +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 = PyModule_Create(&module_def); + if (setup_module(m) < 0) { + Py_DECREF(m); + return NULL; + } + + return m; +} diff --git a/src/decode.c b/src/decode.c index ea2f3af80..63d290035 100644 --- a/src/decode.c +++ b/src/decode.c @@ -1,925 +1,925 @@ -/* - * The Python Imaging Library. - * - * standard decoder interfaces for the Imaging library - * - * history: - * 1996-03-28 fl Moved from _imagingmodule.c - * 1996-04-15 fl Support subregions in setimage - * 1996-04-19 fl Allocate decoder buffer (where appropriate) - * 1996-05-02 fl Added jpeg decoder - * 1996-05-12 fl Compile cleanly as C++ - * 1996-05-16 fl Added hex decoder - * 1996-05-26 fl Added jpeg configuration parameters - * 1996-12-14 fl Added zip decoder - * 1996-12-30 fl Plugged potential memory leak for tiled images - * 1997-01-03 fl Added fli and msp decoders - * 1997-01-04 fl Added sun_rle and tga_rle decoders - * 1997-05-31 fl Added bitfield decoder - * 1998-09-11 fl Added orientation and pixelsize fields to tga_rle decoder - * 1998-12-29 fl Added mode/rawmode argument to decoders - * 1998-12-30 fl Added mode argument to *all* decoders - * 2002-06-09 fl Added stride argument to pcx decoder - * - * Copyright (c) 1997-2002 by Secret Labs AB. - * Copyright (c) 1995-2002 by Fredrik Lundh. - * - * See the README file for information on usage and redistribution. - */ - -/* FIXME: make these pluggable! */ - -#define PY_SSIZE_T_CLEAN -#include "Python.h" - -#include "libImaging/Imaging.h" - -#include "libImaging/Bit.h" -#include "libImaging/Bcn.h" -#include "libImaging/Gif.h" -#include "libImaging/Raw.h" -#include "libImaging/Sgi.h" - -/* -------------------------------------------------------------------- */ -/* Common */ -/* -------------------------------------------------------------------- */ - -typedef struct { - PyObject_HEAD int (*decode)( - Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); - int (*cleanup)(ImagingCodecState state); - struct ImagingCodecStateInstance state; - Imaging im; - PyObject *lock; - int pulls_fd; -} ImagingDecoderObject; - -static PyTypeObject ImagingDecoderType; - -static ImagingDecoderObject * -PyImaging_DecoderNew(int contextsize) { - ImagingDecoderObject *decoder; - void *context; - - if (PyType_Ready(&ImagingDecoderType) < 0) { - return NULL; - } - - decoder = PyObject_New(ImagingDecoderObject, &ImagingDecoderType); - if (decoder == NULL) { - return NULL; - } - - /* Clear the decoder state */ - memset(&decoder->state, 0, sizeof(decoder->state)); - - /* Allocate decoder context */ - if (contextsize > 0) { - context = (void *)calloc(1, contextsize); - if (!context) { - Py_DECREF(decoder); - (void)ImagingError_MemoryError(); - return NULL; - } - } else { - context = 0; - } - - /* Initialize decoder context */ - decoder->state.context = context; - - /* Target image */ - decoder->lock = NULL; - decoder->im = NULL; - - /* Initialize the cleanup function pointer */ - decoder->cleanup = NULL; - - /* set if the decoder needs to pull data from the fd, instead of - having it pushed */ - decoder->pulls_fd = 0; - - return decoder; -} - -static void -_dealloc(ImagingDecoderObject *decoder) { - if (decoder->cleanup) { - decoder->cleanup(&decoder->state); - } - free(decoder->state.buffer); - free(decoder->state.context); - Py_XDECREF(decoder->lock); - Py_XDECREF(decoder->state.fd); - PyObject_Del(decoder); -} - -static PyObject * -_decode(ImagingDecoderObject *decoder, PyObject *args) { - Py_buffer buffer; - int status; - ImagingSectionCookie cookie; - - if (!PyArg_ParseTuple(args, "y*", &buffer)) { - return NULL; - } - - if (!decoder->pulls_fd) { - ImagingSectionEnter(&cookie); - } - - status = decoder->decode(decoder->im, &decoder->state, buffer.buf, buffer.len); - - if (!decoder->pulls_fd) { - ImagingSectionLeave(&cookie); - } - - PyBuffer_Release(&buffer); - return Py_BuildValue("ii", status, decoder->state.errcode); -} - -static PyObject * -_decode_cleanup(ImagingDecoderObject *decoder, PyObject *args) { - int status = 0; - - if (decoder->cleanup) { - status = decoder->cleanup(&decoder->state); - } - - return Py_BuildValue("i", status); -} - -extern Imaging -PyImaging_AsImaging(PyObject *op); - -static PyObject * -_setimage(ImagingDecoderObject *decoder, PyObject *args) { - PyObject *op; - Imaging im; - ImagingCodecState state; - int x0, y0, x1, y1; - - x0 = y0 = x1 = y1 = 0; - - /* FIXME: should publish the ImagingType descriptor */ - if (!PyArg_ParseTuple(args, "O|(iiii)", &op, &x0, &y0, &x1, &y1)) { - return NULL; - } - im = PyImaging_AsImaging(op); - if (!im) { - return NULL; - } - - decoder->im = im; - - state = &decoder->state; - - /* Setup decoding tile extent */ - if (x0 == 0 && x1 == 0) { - state->xsize = im->xsize; - state->ysize = im->ysize; - } else { - state->xoff = x0; - state->yoff = y0; - state->xsize = x1 - x0; - state->ysize = y1 - y0; - } - - if (state->xsize <= 0 || state->xsize + state->xoff > (int)im->xsize || - state->ysize <= 0 || state->ysize + state->yoff > (int)im->ysize) { - PyErr_SetString(PyExc_ValueError, "tile cannot extend outside image"); - return NULL; - } - - /* Allocate memory buffer (if bits field is set) */ - if (state->bits > 0) { - if (!state->bytes) { - if (state->xsize > ((INT_MAX / state->bits) - 7)) { - return ImagingError_MemoryError(); - } - state->bytes = (state->bits * state->xsize + 7) / 8; - } - /* malloc check ok, overflow checked above */ - state->buffer = (UINT8 *)calloc(1, state->bytes); - if (!state->buffer) { - return ImagingError_MemoryError(); - } - } - - /* Keep a reference to the image object, to make sure it doesn't - go away before we do */ - Py_INCREF(op); - Py_XDECREF(decoder->lock); - decoder->lock = op; - - Py_INCREF(Py_None); - return Py_None; -} - -static PyObject * -_setfd(ImagingDecoderObject *decoder, PyObject *args) { - PyObject *fd; - ImagingCodecState state; - - if (!PyArg_ParseTuple(args, "O", &fd)) { - return NULL; - } - - state = &decoder->state; - - Py_XINCREF(fd); - state->fd = fd; - - Py_INCREF(Py_None); - return Py_None; -} - -static PyObject * -_get_pulls_fd(ImagingDecoderObject *decoder, void *closure) { - return PyBool_FromLong(decoder->pulls_fd); -} - -static struct PyMethodDef methods[] = { - {"decode", (PyCFunction)_decode, METH_VARARGS}, - {"cleanup", (PyCFunction)_decode_cleanup, METH_VARARGS}, - {"setimage", (PyCFunction)_setimage, METH_VARARGS}, - {"setfd", (PyCFunction)_setfd, METH_VARARGS}, - {NULL, NULL} /* sentinel */ -}; - -static struct PyGetSetDef getseters[] = { - {"pulls_fd", - (getter)_get_pulls_fd, - NULL, - "True if this decoder expects to pull from self.fd itself.", - NULL}, - {NULL, NULL, NULL, NULL, NULL} /* sentinel */ -}; - -static PyTypeObject ImagingDecoderType = { - PyVarObject_HEAD_INIT(NULL, 0) "ImagingDecoder", /*tp_name*/ - sizeof(ImagingDecoderObject), /*tp_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*/ -}; - -/* -------------------------------------------------------------------- */ - -int -get_unpacker(ImagingDecoderObject *decoder, const char *mode, const char *rawmode) { - int bits; - ImagingShuffler unpack; - - unpack = ImagingFindUnpacker(mode, rawmode, &bits); - if (!unpack) { - Py_DECREF(decoder); - PyErr_SetString(PyExc_ValueError, "unknown raw mode for given image mode"); - return -1; - } - - decoder->state.shuffle = unpack; - decoder->state.bits = bits; - - return 0; -} - -/* -------------------------------------------------------------------- */ -/* BIT (packed fields) */ -/* -------------------------------------------------------------------- */ - -PyObject * -PyImaging_BitDecoderNew(PyObject *self, PyObject *args) { - ImagingDecoderObject *decoder; - - char *mode; - int bits = 8; - int pad = 8; - int fill = 0; - int sign = 0; - int ystep = 1; - if (!PyArg_ParseTuple(args, "s|iiiii", &mode, &bits, &pad, &fill, &sign, &ystep)) { - return NULL; - } - - if (strcmp(mode, "F") != 0) { - PyErr_SetString(PyExc_ValueError, "bad image mode"); - return NULL; - } - - decoder = PyImaging_DecoderNew(sizeof(BITSTATE)); - if (decoder == NULL) { - return NULL; - } - - decoder->decode = ImagingBitDecode; - - decoder->state.ystep = ystep; - - ((BITSTATE *)decoder->state.context)->bits = bits; - ((BITSTATE *)decoder->state.context)->pad = pad; - ((BITSTATE *)decoder->state.context)->fill = fill; - ((BITSTATE *)decoder->state.context)->sign = sign; - - return (PyObject *)decoder; -} - -/* -------------------------------------------------------------------- */ -/* BCn: GPU block-compressed texture formats */ -/* -------------------------------------------------------------------- */ - -PyObject * -PyImaging_BcnDecoderNew(PyObject *self, PyObject *args) { - ImagingDecoderObject *decoder; - - char *mode; - char *actual; - int n = 0; - char *pixel_format = ""; - if (!PyArg_ParseTuple(args, "si|s", &mode, &n, &pixel_format)) { - return NULL; - } - - switch (n) { - case 1: /* BC1: 565 color, 1-bit alpha */ - case 2: /* BC2: 565 color, 4-bit alpha */ - case 3: /* BC3: 565 color, 2-endpoint 8-bit interpolated alpha */ - case 7: /* BC7: 4-channel 8-bit via everything */ - actual = "RGBA"; - break; - case 4: /* BC4: 1-channel 8-bit via 1 BC3 alpha block */ - actual = "L"; - break; - case 5: /* BC5: 2-channel 8-bit via 2 BC3 alpha blocks */ - case 6: /* BC6: 3-channel 16-bit float */ - actual = "RGB"; - break; - default: - PyErr_SetString(PyExc_ValueError, "block compression type unknown"); - return NULL; - } - - if (strcmp(mode, actual) != 0) { - PyErr_SetString(PyExc_ValueError, "bad image mode"); - return NULL; - } - - decoder = PyImaging_DecoderNew(sizeof(char *)); - if (decoder == NULL) { - return NULL; - } - - decoder->decode = ImagingBcnDecode; - decoder->state.state = n; - ((BCNSTATE *)decoder->state.context)->pixel_format = pixel_format; - - return (PyObject *)decoder; -} - -/* -------------------------------------------------------------------- */ -/* FLI */ -/* -------------------------------------------------------------------- */ - -PyObject * -PyImaging_FliDecoderNew(PyObject *self, PyObject *args) { - ImagingDecoderObject *decoder; - - decoder = PyImaging_DecoderNew(0); - if (decoder == NULL) { - return NULL; - } - - decoder->decode = ImagingFliDecode; - - return (PyObject *)decoder; -} - -/* -------------------------------------------------------------------- */ -/* GIF */ -/* -------------------------------------------------------------------- */ - -PyObject * -PyImaging_GifDecoderNew(PyObject *self, PyObject *args) { - ImagingDecoderObject *decoder; - - char *mode; - int bits = 8; - int interlace = 0; - int transparency = -1; - if (!PyArg_ParseTuple(args, "s|iii", &mode, &bits, &interlace, &transparency)) { - return NULL; - } - - if (strcmp(mode, "L") != 0 && strcmp(mode, "P") != 0) { - PyErr_SetString(PyExc_ValueError, "bad image mode"); - return NULL; - } - - decoder = PyImaging_DecoderNew(sizeof(GIFDECODERSTATE)); - if (decoder == NULL) { - return NULL; - } - - decoder->decode = ImagingGifDecode; - - ((GIFDECODERSTATE *)decoder->state.context)->bits = bits; - ((GIFDECODERSTATE *)decoder->state.context)->interlace = interlace; - ((GIFDECODERSTATE *)decoder->state.context)->transparency = transparency; - - return (PyObject *)decoder; -} - -/* -------------------------------------------------------------------- */ -/* HEX */ -/* -------------------------------------------------------------------- */ - -PyObject * -PyImaging_HexDecoderNew(PyObject *self, PyObject *args) { - ImagingDecoderObject *decoder; - - char *mode; - char *rawmode; - if (!PyArg_ParseTuple(args, "ss", &mode, &rawmode)) { - return NULL; - } - - decoder = PyImaging_DecoderNew(0); - if (decoder == NULL) { - return NULL; - } - - if (get_unpacker(decoder, mode, rawmode) < 0) { - return NULL; - } - - decoder->decode = ImagingHexDecode; - - return (PyObject *)decoder; -} - -/* -------------------------------------------------------------------- */ -/* LibTiff */ -/* -------------------------------------------------------------------- */ - -#ifdef HAVE_LIBTIFF - -#include "libImaging/TiffDecode.h" - -#include - -PyObject * -PyImaging_LibTiffDecoderNew(PyObject *self, PyObject *args) { - ImagingDecoderObject *decoder; - char *mode; - char *rawmode; - char *compname; - int fp; - uint32_t ifdoffset; - - if (!PyArg_ParseTuple(args, "sssiI", &mode, &rawmode, &compname, &fp, &ifdoffset)) { - return NULL; - } - - TRACE(("new tiff decoder %s\n", compname)); - - decoder = PyImaging_DecoderNew(sizeof(TIFFSTATE)); - if (decoder == NULL) { - return NULL; - } - - if (get_unpacker(decoder, mode, rawmode) < 0) { - return NULL; - } - - if (!ImagingLibTiffInit(&decoder->state, fp, ifdoffset)) { - Py_DECREF(decoder); - PyErr_SetString(PyExc_RuntimeError, "tiff codec initialization failed"); - return NULL; - } - - decoder->decode = ImagingLibTiffDecode; - - return (PyObject *)decoder; -} - -#endif - -/* -------------------------------------------------------------------- */ -/* PackBits */ -/* -------------------------------------------------------------------- */ - -PyObject * -PyImaging_PackbitsDecoderNew(PyObject *self, PyObject *args) { - ImagingDecoderObject *decoder; - - char *mode; - char *rawmode; - if (!PyArg_ParseTuple(args, "ss", &mode, &rawmode)) { - return NULL; - } - - decoder = PyImaging_DecoderNew(0); - if (decoder == NULL) { - return NULL; - } - - if (get_unpacker(decoder, mode, rawmode) < 0) { - return NULL; - } - - decoder->decode = ImagingPackbitsDecode; - - return (PyObject *)decoder; -} - -/* -------------------------------------------------------------------- */ -/* PCD */ -/* -------------------------------------------------------------------- */ - -PyObject * -PyImaging_PcdDecoderNew(PyObject *self, PyObject *args) { - ImagingDecoderObject *decoder; - - decoder = PyImaging_DecoderNew(0); - if (decoder == NULL) { - return NULL; - } - - /* Unpack from PhotoYCC to RGB */ - if (get_unpacker(decoder, "RGB", "YCC;P") < 0) { - return NULL; - } - - decoder->decode = ImagingPcdDecode; - - return (PyObject *)decoder; -} - -/* -------------------------------------------------------------------- */ -/* PCX */ -/* -------------------------------------------------------------------- */ - -PyObject * -PyImaging_PcxDecoderNew(PyObject *self, PyObject *args) { - ImagingDecoderObject *decoder; - - char *mode; - char *rawmode; - int stride; - if (!PyArg_ParseTuple(args, "ssi", &mode, &rawmode, &stride)) { - return NULL; - } - - decoder = PyImaging_DecoderNew(0); - if (decoder == NULL) { - return NULL; - } - - if (get_unpacker(decoder, mode, rawmode) < 0) { - return NULL; - } - - decoder->state.bytes = stride; - - decoder->decode = ImagingPcxDecode; - - return (PyObject *)decoder; -} - -/* -------------------------------------------------------------------- */ -/* RAW */ -/* -------------------------------------------------------------------- */ - -PyObject * -PyImaging_RawDecoderNew(PyObject *self, PyObject *args) { - ImagingDecoderObject *decoder; - - char *mode; - char *rawmode; - int stride = 0; - int ystep = 1; - if (!PyArg_ParseTuple(args, "ss|ii", &mode, &rawmode, &stride, &ystep)) { - return NULL; - } - - decoder = PyImaging_DecoderNew(sizeof(RAWSTATE)); - if (decoder == NULL) { - return NULL; - } - - if (get_unpacker(decoder, mode, rawmode) < 0) { - return NULL; - } - - decoder->decode = ImagingRawDecode; - - decoder->state.ystep = ystep; - - ((RAWSTATE *)decoder->state.context)->stride = stride; - - return (PyObject *)decoder; -} - -/* -------------------------------------------------------------------- */ -/* SGI RLE */ -/* -------------------------------------------------------------------- */ - -PyObject * -PyImaging_SgiRleDecoderNew(PyObject *self, PyObject *args) { - ImagingDecoderObject *decoder; - - char *mode; - char *rawmode; - int ystep = 1; - int bpc = 1; - if (!PyArg_ParseTuple(args, "ss|ii", &mode, &rawmode, &ystep, &bpc)) { - return NULL; - } - - decoder = PyImaging_DecoderNew(sizeof(SGISTATE)); - if (decoder == NULL) { - return NULL; - } - - if (get_unpacker(decoder, mode, rawmode) < 0) { - return NULL; - } - - decoder->pulls_fd = 1; - decoder->decode = ImagingSgiRleDecode; - decoder->state.ystep = ystep; - - ((SGISTATE *)decoder->state.context)->bpc = bpc; - - return (PyObject *)decoder; -} - -/* -------------------------------------------------------------------- */ -/* SUN RLE */ -/* -------------------------------------------------------------------- */ - -PyObject * -PyImaging_SunRleDecoderNew(PyObject *self, PyObject *args) { - ImagingDecoderObject *decoder; - - char *mode; - char *rawmode; - if (!PyArg_ParseTuple(args, "ss", &mode, &rawmode)) { - return NULL; - } - - decoder = PyImaging_DecoderNew(0); - if (decoder == NULL) { - return NULL; - } - - if (get_unpacker(decoder, mode, rawmode) < 0) { - return NULL; - } - - decoder->decode = ImagingSunRleDecode; - - return (PyObject *)decoder; -} - -/* -------------------------------------------------------------------- */ -/* TGA RLE */ -/* -------------------------------------------------------------------- */ - -PyObject * -PyImaging_TgaRleDecoderNew(PyObject *self, PyObject *args) { - ImagingDecoderObject *decoder; - - char *mode; - char *rawmode; - int ystep = 1; - int depth = 8; - if (!PyArg_ParseTuple(args, "ss|ii", &mode, &rawmode, &ystep, &depth)) { - return NULL; - } - - decoder = PyImaging_DecoderNew(0); - if (decoder == NULL) { - return NULL; - } - - if (get_unpacker(decoder, mode, rawmode) < 0) { - return NULL; - } - - decoder->decode = ImagingTgaRleDecode; - - decoder->state.ystep = ystep; - decoder->state.count = depth / 8; - - return (PyObject *)decoder; -} - -/* -------------------------------------------------------------------- */ -/* XBM */ -/* -------------------------------------------------------------------- */ - -PyObject * -PyImaging_XbmDecoderNew(PyObject *self, PyObject *args) { - ImagingDecoderObject *decoder; - - decoder = PyImaging_DecoderNew(0); - if (decoder == NULL) { - return NULL; - } - - if (get_unpacker(decoder, "1", "1;R") < 0) { - return NULL; - } - - decoder->decode = ImagingXbmDecode; - - return (PyObject *)decoder; -} - -/* -------------------------------------------------------------------- */ -/* ZIP */ -/* -------------------------------------------------------------------- */ - -#ifdef HAVE_LIBZ - -#include "libImaging/ZipCodecs.h" - -PyObject * -PyImaging_ZipDecoderNew(PyObject *self, PyObject *args) { - ImagingDecoderObject *decoder; - - char *mode; - char *rawmode; - int interlaced = 0; - if (!PyArg_ParseTuple(args, "ss|i", &mode, &rawmode, &interlaced)) { - return NULL; - } - - decoder = PyImaging_DecoderNew(sizeof(ZIPSTATE)); - if (decoder == NULL) { - return NULL; - } - - if (get_unpacker(decoder, mode, rawmode) < 0) { - return NULL; - } - - decoder->decode = ImagingZipDecode; - decoder->cleanup = ImagingZipDecodeCleanup; - - ((ZIPSTATE *)decoder->state.context)->interlaced = interlaced; - - return (PyObject *)decoder; -} -#endif - -/* -------------------------------------------------------------------- */ -/* JPEG */ -/* -------------------------------------------------------------------- */ - -#ifdef HAVE_LIBJPEG - -/* We better define this decoder last in this file, so the following - undef's won't mess things up for the Imaging library proper. */ - -#undef HAVE_PROTOTYPES -#undef HAVE_STDDEF_H -#undef HAVE_STDLIB_H -#undef UINT8 -#undef UINT16 -#undef UINT32 -#undef INT8 -#undef INT16 -#undef INT32 - -#include "libImaging/Jpeg.h" - -PyObject * -PyImaging_JpegDecoderNew(PyObject *self, PyObject *args) { - ImagingDecoderObject *decoder; - - char *mode; - char *rawmode; /* what we want from the decoder */ - char *jpegmode; /* what's in the file */ - int scale = 1; - int draft = 0; - - if (!PyArg_ParseTuple(args, "ssz|ii", &mode, &rawmode, &jpegmode, &scale, &draft)) { - return NULL; - } - - if (!jpegmode) { - jpegmode = ""; - } - - decoder = PyImaging_DecoderNew(sizeof(JPEGSTATE)); - if (decoder == NULL) { - return NULL; - } - - // libjpeg-turbo supports different output formats. - // We are choosing Pillow's native format (3 color bytes + 1 padding) - // to avoid extra conversion in Unpack.c. - if (ImagingJpegUseJCSExtensions() && strcmp(rawmode, "RGB") == 0) { - rawmode = "RGBX"; - } - - if (get_unpacker(decoder, mode, rawmode) < 0) { - return NULL; - } - - decoder->decode = ImagingJpegDecode; - decoder->cleanup = ImagingJpegDecodeCleanup; - - strncpy(((JPEGSTATE *)decoder->state.context)->rawmode, rawmode, 8); - strncpy(((JPEGSTATE *)decoder->state.context)->jpegmode, jpegmode, 8); - - ((JPEGSTATE *)decoder->state.context)->scale = scale; - ((JPEGSTATE *)decoder->state.context)->draft = draft; - - return (PyObject *)decoder; -} -#endif - -/* -------------------------------------------------------------------- */ -/* JPEG 2000 */ -/* -------------------------------------------------------------------- */ - -#ifdef HAVE_OPENJPEG - -#include "libImaging/Jpeg2K.h" - -PyObject * -PyImaging_Jpeg2KDecoderNew(PyObject *self, PyObject *args) { - ImagingDecoderObject *decoder; - JPEG2KDECODESTATE *context; - - char *mode; - char *format; - OPJ_CODEC_FORMAT codec_format; - int reduce = 0; - int layers = 0; - int fd = -1; - PY_LONG_LONG length = -1; - - if (!PyArg_ParseTuple( - args, "ss|iiiL", &mode, &format, &reduce, &layers, &fd, &length)) { - return NULL; - } - - if (strcmp(format, "j2k") == 0) { - codec_format = OPJ_CODEC_J2K; - } else if (strcmp(format, "jpt") == 0) { - codec_format = OPJ_CODEC_JPT; - } else if (strcmp(format, "jp2") == 0) { - codec_format = OPJ_CODEC_JP2; - } else { - return NULL; - } - - decoder = PyImaging_DecoderNew(sizeof(JPEG2KDECODESTATE)); - if (decoder == NULL) { - return NULL; - } - - decoder->pulls_fd = 1; - decoder->decode = ImagingJpeg2KDecode; - decoder->cleanup = ImagingJpeg2KDecodeCleanup; - - context = (JPEG2KDECODESTATE *)decoder->state.context; - - context->fd = fd; - context->length = (off_t)length; - context->format = codec_format; - context->reduce = reduce; - context->layers = layers; - - return (PyObject *)decoder; -} -#endif /* HAVE_OPENJPEG */ +/* + * The Python Imaging Library. + * + * standard decoder interfaces for the Imaging library + * + * history: + * 1996-03-28 fl Moved from _imagingmodule.c + * 1996-04-15 fl Support subregions in setimage + * 1996-04-19 fl Allocate decoder buffer (where appropriate) + * 1996-05-02 fl Added jpeg decoder + * 1996-05-12 fl Compile cleanly as C++ + * 1996-05-16 fl Added hex decoder + * 1996-05-26 fl Added jpeg configuration parameters + * 1996-12-14 fl Added zip decoder + * 1996-12-30 fl Plugged potential memory leak for tiled images + * 1997-01-03 fl Added fli and msp decoders + * 1997-01-04 fl Added sun_rle and tga_rle decoders + * 1997-05-31 fl Added bitfield decoder + * 1998-09-11 fl Added orientation and pixelsize fields to tga_rle decoder + * 1998-12-29 fl Added mode/rawmode argument to decoders + * 1998-12-30 fl Added mode argument to *all* decoders + * 2002-06-09 fl Added stride argument to pcx decoder + * + * Copyright (c) 1997-2002 by Secret Labs AB. + * Copyright (c) 1995-2002 by Fredrik Lundh. + * + * See the README file for information on usage and redistribution. + */ + +/* FIXME: make these pluggable! */ + +#define PY_SSIZE_T_CLEAN +#include "Python.h" + +#include "libImaging/Imaging.h" + +#include "libImaging/Bit.h" +#include "libImaging/Bcn.h" +#include "libImaging/Gif.h" +#include "libImaging/Raw.h" +#include "libImaging/Sgi.h" + +/* -------------------------------------------------------------------- */ +/* Common */ +/* -------------------------------------------------------------------- */ + +typedef struct { + PyObject_HEAD int (*decode)( + Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); + int (*cleanup)(ImagingCodecState state); + struct ImagingCodecStateInstance state; + Imaging im; + PyObject *lock; + int pulls_fd; +} ImagingDecoderObject; + +static PyTypeObject ImagingDecoderType; + +static ImagingDecoderObject * +PyImaging_DecoderNew(int contextsize) { + ImagingDecoderObject *decoder; + void *context; + + if (PyType_Ready(&ImagingDecoderType) < 0) { + return NULL; + } + + decoder = PyObject_New(ImagingDecoderObject, &ImagingDecoderType); + if (decoder == NULL) { + return NULL; + } + + /* Clear the decoder state */ + memset(&decoder->state, 0, sizeof(decoder->state)); + + /* Allocate decoder context */ + if (contextsize > 0) { + context = (void *)calloc(1, contextsize); + if (!context) { + Py_DECREF(decoder); + (void)ImagingError_MemoryError(); + return NULL; + } + } else { + context = 0; + } + + /* Initialize decoder context */ + decoder->state.context = context; + + /* Target image */ + decoder->lock = NULL; + decoder->im = NULL; + + /* Initialize the cleanup function pointer */ + decoder->cleanup = NULL; + + /* set if the decoder needs to pull data from the fd, instead of + having it pushed */ + decoder->pulls_fd = 0; + + return decoder; +} + +static void +_dealloc(ImagingDecoderObject *decoder) { + if (decoder->cleanup) { + decoder->cleanup(&decoder->state); + } + free(decoder->state.buffer); + free(decoder->state.context); + Py_XDECREF(decoder->lock); + Py_XDECREF(decoder->state.fd); + PyObject_Del(decoder); +} + +static PyObject * +_decode(ImagingDecoderObject *decoder, PyObject *args) { + Py_buffer buffer; + int status; + ImagingSectionCookie cookie; + + if (!PyArg_ParseTuple(args, "y*", &buffer)) { + return NULL; + } + + if (!decoder->pulls_fd) { + ImagingSectionEnter(&cookie); + } + + status = decoder->decode(decoder->im, &decoder->state, buffer.buf, buffer.len); + + if (!decoder->pulls_fd) { + ImagingSectionLeave(&cookie); + } + + PyBuffer_Release(&buffer); + return Py_BuildValue("ii", status, decoder->state.errcode); +} + +static PyObject * +_decode_cleanup(ImagingDecoderObject *decoder, PyObject *args) { + int status = 0; + + if (decoder->cleanup) { + status = decoder->cleanup(&decoder->state); + } + + return Py_BuildValue("i", status); +} + +extern Imaging +PyImaging_AsImaging(PyObject *op); + +static PyObject * +_setimage(ImagingDecoderObject *decoder, PyObject *args) { + PyObject *op; + Imaging im; + ImagingCodecState state; + int x0, y0, x1, y1; + + x0 = y0 = x1 = y1 = 0; + + /* FIXME: should publish the ImagingType descriptor */ + if (!PyArg_ParseTuple(args, "O|(iiii)", &op, &x0, &y0, &x1, &y1)) { + return NULL; + } + im = PyImaging_AsImaging(op); + if (!im) { + return NULL; + } + + decoder->im = im; + + state = &decoder->state; + + /* Setup decoding tile extent */ + if (x0 == 0 && x1 == 0) { + state->xsize = im->xsize; + state->ysize = im->ysize; + } else { + state->xoff = x0; + state->yoff = y0; + state->xsize = x1 - x0; + state->ysize = y1 - y0; + } + + if (state->xsize <= 0 || state->xsize + state->xoff > (int)im->xsize || + state->ysize <= 0 || state->ysize + state->yoff > (int)im->ysize) { + PyErr_SetString(PyExc_ValueError, "tile cannot extend outside image"); + return NULL; + } + + /* Allocate memory buffer (if bits field is set) */ + if (state->bits > 0) { + if (!state->bytes) { + if (state->xsize > ((INT_MAX / state->bits) - 7)) { + return ImagingError_MemoryError(); + } + state->bytes = (state->bits * state->xsize + 7) / 8; + } + /* malloc check ok, overflow checked above */ + state->buffer = (UINT8 *)calloc(1, state->bytes); + if (!state->buffer) { + return ImagingError_MemoryError(); + } + } + + /* Keep a reference to the image object, to make sure it doesn't + go away before we do */ + Py_INCREF(op); + Py_XDECREF(decoder->lock); + decoder->lock = op; + + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject * +_setfd(ImagingDecoderObject *decoder, PyObject *args) { + PyObject *fd; + ImagingCodecState state; + + if (!PyArg_ParseTuple(args, "O", &fd)) { + return NULL; + } + + state = &decoder->state; + + Py_XINCREF(fd); + state->fd = fd; + + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject * +_get_pulls_fd(ImagingDecoderObject *decoder, void *closure) { + return PyBool_FromLong(decoder->pulls_fd); +} + +static struct PyMethodDef methods[] = { + {"decode", (PyCFunction)_decode, METH_VARARGS}, + {"cleanup", (PyCFunction)_decode_cleanup, METH_VARARGS}, + {"setimage", (PyCFunction)_setimage, METH_VARARGS}, + {"setfd", (PyCFunction)_setfd, METH_VARARGS}, + {NULL, NULL} /* sentinel */ +}; + +static struct PyGetSetDef getseters[] = { + {"pulls_fd", + (getter)_get_pulls_fd, + NULL, + "True if this decoder expects to pull from self.fd itself.", + NULL}, + {NULL, NULL, NULL, NULL, NULL} /* sentinel */ +}; + +static PyTypeObject ImagingDecoderType = { + PyVarObject_HEAD_INIT(NULL, 0) "ImagingDecoder", /*tp_name*/ + sizeof(ImagingDecoderObject), /*tp_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*/ +}; + +/* -------------------------------------------------------------------- */ + +int +get_unpacker(ImagingDecoderObject *decoder, const char *mode, const char *rawmode) { + int bits; + ImagingShuffler unpack; + + unpack = ImagingFindUnpacker(mode, rawmode, &bits); + if (!unpack) { + Py_DECREF(decoder); + PyErr_SetString(PyExc_ValueError, "unknown raw mode for given image mode"); + return -1; + } + + decoder->state.shuffle = unpack; + decoder->state.bits = bits; + + return 0; +} + +/* -------------------------------------------------------------------- */ +/* BIT (packed fields) */ +/* -------------------------------------------------------------------- */ + +PyObject * +PyImaging_BitDecoderNew(PyObject *self, PyObject *args) { + ImagingDecoderObject *decoder; + + char *mode; + int bits = 8; + int pad = 8; + int fill = 0; + int sign = 0; + int ystep = 1; + if (!PyArg_ParseTuple(args, "s|iiiii", &mode, &bits, &pad, &fill, &sign, &ystep)) { + return NULL; + } + + if (strcmp(mode, "F") != 0) { + PyErr_SetString(PyExc_ValueError, "bad image mode"); + return NULL; + } + + decoder = PyImaging_DecoderNew(sizeof(BITSTATE)); + if (decoder == NULL) { + return NULL; + } + + decoder->decode = ImagingBitDecode; + + decoder->state.ystep = ystep; + + ((BITSTATE *)decoder->state.context)->bits = bits; + ((BITSTATE *)decoder->state.context)->pad = pad; + ((BITSTATE *)decoder->state.context)->fill = fill; + ((BITSTATE *)decoder->state.context)->sign = sign; + + return (PyObject *)decoder; +} + +/* -------------------------------------------------------------------- */ +/* BCn: GPU block-compressed texture formats */ +/* -------------------------------------------------------------------- */ + +PyObject * +PyImaging_BcnDecoderNew(PyObject *self, PyObject *args) { + ImagingDecoderObject *decoder; + + char *mode; + char *actual; + int n = 0; + char *pixel_format = ""; + if (!PyArg_ParseTuple(args, "si|s", &mode, &n, &pixel_format)) { + return NULL; + } + + switch (n) { + case 1: /* BC1: 565 color, 1-bit alpha */ + case 2: /* BC2: 565 color, 4-bit alpha */ + case 3: /* BC3: 565 color, 2-endpoint 8-bit interpolated alpha */ + case 7: /* BC7: 4-channel 8-bit via everything */ + actual = "RGBA"; + break; + case 4: /* BC4: 1-channel 8-bit via 1 BC3 alpha block */ + actual = "L"; + break; + case 5: /* BC5: 2-channel 8-bit via 2 BC3 alpha blocks */ + case 6: /* BC6: 3-channel 16-bit float */ + actual = "RGB"; + break; + default: + PyErr_SetString(PyExc_ValueError, "block compression type unknown"); + return NULL; + } + + if (strcmp(mode, actual) != 0) { + PyErr_SetString(PyExc_ValueError, "bad image mode"); + return NULL; + } + + decoder = PyImaging_DecoderNew(sizeof(char *)); + if (decoder == NULL) { + return NULL; + } + + decoder->decode = ImagingBcnDecode; + decoder->state.state = n; + ((BCNSTATE *)decoder->state.context)->pixel_format = pixel_format; + + return (PyObject *)decoder; +} + +/* -------------------------------------------------------------------- */ +/* FLI */ +/* -------------------------------------------------------------------- */ + +PyObject * +PyImaging_FliDecoderNew(PyObject *self, PyObject *args) { + ImagingDecoderObject *decoder; + + decoder = PyImaging_DecoderNew(0); + if (decoder == NULL) { + return NULL; + } + + decoder->decode = ImagingFliDecode; + + return (PyObject *)decoder; +} + +/* -------------------------------------------------------------------- */ +/* GIF */ +/* -------------------------------------------------------------------- */ + +PyObject * +PyImaging_GifDecoderNew(PyObject *self, PyObject *args) { + ImagingDecoderObject *decoder; + + char *mode; + int bits = 8; + int interlace = 0; + int transparency = -1; + if (!PyArg_ParseTuple(args, "s|iii", &mode, &bits, &interlace, &transparency)) { + return NULL; + } + + if (strcmp(mode, "L") != 0 && strcmp(mode, "P") != 0) { + PyErr_SetString(PyExc_ValueError, "bad image mode"); + return NULL; + } + + decoder = PyImaging_DecoderNew(sizeof(GIFDECODERSTATE)); + if (decoder == NULL) { + return NULL; + } + + decoder->decode = ImagingGifDecode; + + ((GIFDECODERSTATE *)decoder->state.context)->bits = bits; + ((GIFDECODERSTATE *)decoder->state.context)->interlace = interlace; + ((GIFDECODERSTATE *)decoder->state.context)->transparency = transparency; + + return (PyObject *)decoder; +} + +/* -------------------------------------------------------------------- */ +/* HEX */ +/* -------------------------------------------------------------------- */ + +PyObject * +PyImaging_HexDecoderNew(PyObject *self, PyObject *args) { + ImagingDecoderObject *decoder; + + char *mode; + char *rawmode; + if (!PyArg_ParseTuple(args, "ss", &mode, &rawmode)) { + return NULL; + } + + decoder = PyImaging_DecoderNew(0); + if (decoder == NULL) { + return NULL; + } + + if (get_unpacker(decoder, mode, rawmode) < 0) { + return NULL; + } + + decoder->decode = ImagingHexDecode; + + return (PyObject *)decoder; +} + +/* -------------------------------------------------------------------- */ +/* LibTiff */ +/* -------------------------------------------------------------------- */ + +#ifdef HAVE_LIBTIFF + +#include "libImaging/TiffDecode.h" + +#include + +PyObject * +PyImaging_LibTiffDecoderNew(PyObject *self, PyObject *args) { + ImagingDecoderObject *decoder; + char *mode; + char *rawmode; + char *compname; + int fp; + uint32_t ifdoffset; + + if (!PyArg_ParseTuple(args, "sssiI", &mode, &rawmode, &compname, &fp, &ifdoffset)) { + return NULL; + } + + TRACE(("new tiff decoder %s\n", compname)); + + decoder = PyImaging_DecoderNew(sizeof(TIFFSTATE)); + if (decoder == NULL) { + return NULL; + } + + if (get_unpacker(decoder, mode, rawmode) < 0) { + return NULL; + } + + if (!ImagingLibTiffInit(&decoder->state, fp, ifdoffset)) { + Py_DECREF(decoder); + PyErr_SetString(PyExc_RuntimeError, "tiff codec initialization failed"); + return NULL; + } + + decoder->decode = ImagingLibTiffDecode; + + return (PyObject *)decoder; +} + +#endif + +/* -------------------------------------------------------------------- */ +/* PackBits */ +/* -------------------------------------------------------------------- */ + +PyObject * +PyImaging_PackbitsDecoderNew(PyObject *self, PyObject *args) { + ImagingDecoderObject *decoder; + + char *mode; + char *rawmode; + if (!PyArg_ParseTuple(args, "ss", &mode, &rawmode)) { + return NULL; + } + + decoder = PyImaging_DecoderNew(0); + if (decoder == NULL) { + return NULL; + } + + if (get_unpacker(decoder, mode, rawmode) < 0) { + return NULL; + } + + decoder->decode = ImagingPackbitsDecode; + + return (PyObject *)decoder; +} + +/* -------------------------------------------------------------------- */ +/* PCD */ +/* -------------------------------------------------------------------- */ + +PyObject * +PyImaging_PcdDecoderNew(PyObject *self, PyObject *args) { + ImagingDecoderObject *decoder; + + decoder = PyImaging_DecoderNew(0); + if (decoder == NULL) { + return NULL; + } + + /* Unpack from PhotoYCC to RGB */ + if (get_unpacker(decoder, "RGB", "YCC;P") < 0) { + return NULL; + } + + decoder->decode = ImagingPcdDecode; + + return (PyObject *)decoder; +} + +/* -------------------------------------------------------------------- */ +/* PCX */ +/* -------------------------------------------------------------------- */ + +PyObject * +PyImaging_PcxDecoderNew(PyObject *self, PyObject *args) { + ImagingDecoderObject *decoder; + + char *mode; + char *rawmode; + int stride; + if (!PyArg_ParseTuple(args, "ssi", &mode, &rawmode, &stride)) { + return NULL; + } + + decoder = PyImaging_DecoderNew(0); + if (decoder == NULL) { + return NULL; + } + + if (get_unpacker(decoder, mode, rawmode) < 0) { + return NULL; + } + + decoder->state.bytes = stride; + + decoder->decode = ImagingPcxDecode; + + return (PyObject *)decoder; +} + +/* -------------------------------------------------------------------- */ +/* RAW */ +/* -------------------------------------------------------------------- */ + +PyObject * +PyImaging_RawDecoderNew(PyObject *self, PyObject *args) { + ImagingDecoderObject *decoder; + + char *mode; + char *rawmode; + int stride = 0; + int ystep = 1; + if (!PyArg_ParseTuple(args, "ss|ii", &mode, &rawmode, &stride, &ystep)) { + return NULL; + } + + decoder = PyImaging_DecoderNew(sizeof(RAWSTATE)); + if (decoder == NULL) { + return NULL; + } + + if (get_unpacker(decoder, mode, rawmode) < 0) { + return NULL; + } + + decoder->decode = ImagingRawDecode; + + decoder->state.ystep = ystep; + + ((RAWSTATE *)decoder->state.context)->stride = stride; + + return (PyObject *)decoder; +} + +/* -------------------------------------------------------------------- */ +/* SGI RLE */ +/* -------------------------------------------------------------------- */ + +PyObject * +PyImaging_SgiRleDecoderNew(PyObject *self, PyObject *args) { + ImagingDecoderObject *decoder; + + char *mode; + char *rawmode; + int ystep = 1; + int bpc = 1; + if (!PyArg_ParseTuple(args, "ss|ii", &mode, &rawmode, &ystep, &bpc)) { + return NULL; + } + + decoder = PyImaging_DecoderNew(sizeof(SGISTATE)); + if (decoder == NULL) { + return NULL; + } + + if (get_unpacker(decoder, mode, rawmode) < 0) { + return NULL; + } + + decoder->pulls_fd = 1; + decoder->decode = ImagingSgiRleDecode; + decoder->state.ystep = ystep; + + ((SGISTATE *)decoder->state.context)->bpc = bpc; + + return (PyObject *)decoder; +} + +/* -------------------------------------------------------------------- */ +/* SUN RLE */ +/* -------------------------------------------------------------------- */ + +PyObject * +PyImaging_SunRleDecoderNew(PyObject *self, PyObject *args) { + ImagingDecoderObject *decoder; + + char *mode; + char *rawmode; + if (!PyArg_ParseTuple(args, "ss", &mode, &rawmode)) { + return NULL; + } + + decoder = PyImaging_DecoderNew(0); + if (decoder == NULL) { + return NULL; + } + + if (get_unpacker(decoder, mode, rawmode) < 0) { + return NULL; + } + + decoder->decode = ImagingSunRleDecode; + + return (PyObject *)decoder; +} + +/* -------------------------------------------------------------------- */ +/* TGA RLE */ +/* -------------------------------------------------------------------- */ + +PyObject * +PyImaging_TgaRleDecoderNew(PyObject *self, PyObject *args) { + ImagingDecoderObject *decoder; + + char *mode; + char *rawmode; + int ystep = 1; + int depth = 8; + if (!PyArg_ParseTuple(args, "ss|ii", &mode, &rawmode, &ystep, &depth)) { + return NULL; + } + + decoder = PyImaging_DecoderNew(0); + if (decoder == NULL) { + return NULL; + } + + if (get_unpacker(decoder, mode, rawmode) < 0) { + return NULL; + } + + decoder->decode = ImagingTgaRleDecode; + + decoder->state.ystep = ystep; + decoder->state.count = depth / 8; + + return (PyObject *)decoder; +} + +/* -------------------------------------------------------------------- */ +/* XBM */ +/* -------------------------------------------------------------------- */ + +PyObject * +PyImaging_XbmDecoderNew(PyObject *self, PyObject *args) { + ImagingDecoderObject *decoder; + + decoder = PyImaging_DecoderNew(0); + if (decoder == NULL) { + return NULL; + } + + if (get_unpacker(decoder, "1", "1;R") < 0) { + return NULL; + } + + decoder->decode = ImagingXbmDecode; + + return (PyObject *)decoder; +} + +/* -------------------------------------------------------------------- */ +/* ZIP */ +/* -------------------------------------------------------------------- */ + +#ifdef HAVE_LIBZ + +#include "libImaging/ZipCodecs.h" + +PyObject * +PyImaging_ZipDecoderNew(PyObject *self, PyObject *args) { + ImagingDecoderObject *decoder; + + char *mode; + char *rawmode; + int interlaced = 0; + if (!PyArg_ParseTuple(args, "ss|i", &mode, &rawmode, &interlaced)) { + return NULL; + } + + decoder = PyImaging_DecoderNew(sizeof(ZIPSTATE)); + if (decoder == NULL) { + return NULL; + } + + if (get_unpacker(decoder, mode, rawmode) < 0) { + return NULL; + } + + decoder->decode = ImagingZipDecode; + decoder->cleanup = ImagingZipDecodeCleanup; + + ((ZIPSTATE *)decoder->state.context)->interlaced = interlaced; + + return (PyObject *)decoder; +} +#endif + +/* -------------------------------------------------------------------- */ +/* JPEG */ +/* -------------------------------------------------------------------- */ + +#ifdef HAVE_LIBJPEG + +/* We better define this decoder last in this file, so the following + undef's won't mess things up for the Imaging library proper. */ + +#undef HAVE_PROTOTYPES +#undef HAVE_STDDEF_H +#undef HAVE_STDLIB_H +#undef UINT8 +#undef UINT16 +#undef UINT32 +#undef INT8 +#undef INT16 +#undef INT32 + +#include "libImaging/Jpeg.h" + +PyObject * +PyImaging_JpegDecoderNew(PyObject *self, PyObject *args) { + ImagingDecoderObject *decoder; + + char *mode; + char *rawmode; /* what we want from the decoder */ + char *jpegmode; /* what's in the file */ + int scale = 1; + int draft = 0; + + if (!PyArg_ParseTuple(args, "ssz|ii", &mode, &rawmode, &jpegmode, &scale, &draft)) { + return NULL; + } + + if (!jpegmode) { + jpegmode = ""; + } + + decoder = PyImaging_DecoderNew(sizeof(JPEGSTATE)); + if (decoder == NULL) { + return NULL; + } + + // libjpeg-turbo supports different output formats. + // We are choosing Pillow's native format (3 color bytes + 1 padding) + // to avoid extra conversion in Unpack.c. + if (ImagingJpegUseJCSExtensions() && strcmp(rawmode, "RGB") == 0) { + rawmode = "RGBX"; + } + + if (get_unpacker(decoder, mode, rawmode) < 0) { + return NULL; + } + + decoder->decode = ImagingJpegDecode; + decoder->cleanup = ImagingJpegDecodeCleanup; + + strncpy(((JPEGSTATE *)decoder->state.context)->rawmode, rawmode, 8); + strncpy(((JPEGSTATE *)decoder->state.context)->jpegmode, jpegmode, 8); + + ((JPEGSTATE *)decoder->state.context)->scale = scale; + ((JPEGSTATE *)decoder->state.context)->draft = draft; + + return (PyObject *)decoder; +} +#endif + +/* -------------------------------------------------------------------- */ +/* JPEG 2000 */ +/* -------------------------------------------------------------------- */ + +#ifdef HAVE_OPENJPEG + +#include "libImaging/Jpeg2K.h" + +PyObject * +PyImaging_Jpeg2KDecoderNew(PyObject *self, PyObject *args) { + ImagingDecoderObject *decoder; + JPEG2KDECODESTATE *context; + + char *mode; + char *format; + OPJ_CODEC_FORMAT codec_format; + int reduce = 0; + int layers = 0; + int fd = -1; + PY_LONG_LONG length = -1; + + if (!PyArg_ParseTuple( + args, "ss|iiiL", &mode, &format, &reduce, &layers, &fd, &length)) { + return NULL; + } + + if (strcmp(format, "j2k") == 0) { + codec_format = OPJ_CODEC_J2K; + } else if (strcmp(format, "jpt") == 0) { + codec_format = OPJ_CODEC_JPT; + } else if (strcmp(format, "jp2") == 0) { + codec_format = OPJ_CODEC_JP2; + } else { + return NULL; + } + + decoder = PyImaging_DecoderNew(sizeof(JPEG2KDECODESTATE)); + if (decoder == NULL) { + return NULL; + } + + decoder->pulls_fd = 1; + decoder->decode = ImagingJpeg2KDecode; + decoder->cleanup = ImagingJpeg2KDecodeCleanup; + + context = (JPEG2KDECODESTATE *)decoder->state.context; + + context->fd = fd; + context->length = (off_t)length; + context->format = codec_format; + context->reduce = reduce; + context->layers = layers; + + return (PyObject *)decoder; +} +#endif /* HAVE_OPENJPEG */ diff --git a/src/display.c b/src/display.c index abf94f1e1..e43ce1bd3 100644 --- a/src/display.c +++ b/src/display.c @@ -1,915 +1,915 @@ -/* - * The Python Imaging Library. - * - * display support (and other windows-related stuff) - * - * History: - * 1996-05-13 fl Windows DIB support - * 1996-05-21 fl Added palette stuff - * 1996-05-28 fl Added display_mode stuff - * 1997-09-21 fl Added draw primitive - * 2001-09-17 fl Added ImagingGrabScreen (from _grabscreen.c) - * 2002-05-12 fl Added ImagingListWindows - * 2002-11-19 fl Added clipboard support - * 2002-11-25 fl Added GetDC/ReleaseDC helpers - * 2003-05-21 fl Added create window support (including window callback) - * 2003-09-05 fl Added fromstring/tostring methods - * 2009-03-14 fl Added WMF support (from pilwmf) - * - * Copyright (c) 1997-2003 by Secret Labs AB. - * Copyright (c) 1996-1997 by Fredrik Lundh. - * - * See the README file for information on usage and redistribution. - */ - -#define PY_SSIZE_T_CLEAN -#include "Python.h" - -#include "libImaging/Imaging.h" - -/* -------------------------------------------------------------------- */ -/* Windows DIB support */ - -#ifdef _WIN32 - -#include "libImaging/ImDib.h" - -#if SIZEOF_VOID_P == 8 -#define F_HANDLE "K" -#else -#define F_HANDLE "k" -#endif - -typedef struct { - PyObject_HEAD ImagingDIB dib; -} ImagingDisplayObject; - -static PyTypeObject ImagingDisplayType; - -static ImagingDisplayObject * -_new(const char *mode, int xsize, int ysize) { - ImagingDisplayObject *display; - - if (PyType_Ready(&ImagingDisplayType) < 0) { - return NULL; - } - - display = PyObject_New(ImagingDisplayObject, &ImagingDisplayType); - if (display == NULL) { - return NULL; - } - - display->dib = ImagingNewDIB(mode, xsize, ysize); - if (!display->dib) { - Py_DECREF(display); - return NULL; - } - - return display; -} - -static void -_delete(ImagingDisplayObject *display) { - if (display->dib) { - ImagingDeleteDIB(display->dib); - } - PyObject_Del(display); -} - -static PyObject * -_expose(ImagingDisplayObject *display, PyObject *args) { - HDC hdc; - if (!PyArg_ParseTuple(args, F_HANDLE, &hdc)) { - return NULL; - } - - ImagingExposeDIB(display->dib, hdc); - - Py_INCREF(Py_None); - return Py_None; -} - -static PyObject * -_draw(ImagingDisplayObject *display, PyObject *args) { - HDC hdc; - int dst[4]; - int src[4]; - if (!PyArg_ParseTuple( - args, - F_HANDLE "(iiii)(iiii)", - &hdc, - dst + 0, - dst + 1, - dst + 2, - dst + 3, - src + 0, - src + 1, - src + 2, - src + 3)) { - return NULL; - } - - ImagingDrawDIB(display->dib, hdc, dst, src); - - Py_INCREF(Py_None); - return Py_None; -} - -extern Imaging -PyImaging_AsImaging(PyObject *op); - -static PyObject * -_paste(ImagingDisplayObject *display, PyObject *args) { - Imaging im; - - PyObject *op; - int xy[4]; - xy[0] = xy[1] = xy[2] = xy[3] = 0; - if (!PyArg_ParseTuple(args, "O|(iiii)", &op, xy + 0, xy + 1, xy + 2, xy + 3)) { - return NULL; - } - im = PyImaging_AsImaging(op); - if (!im) { - return NULL; - } - - if (xy[2] <= xy[0]) { - xy[2] = xy[0] + im->xsize; - } - if (xy[3] <= xy[1]) { - xy[3] = xy[1] + im->ysize; - } - - ImagingPasteDIB(display->dib, im, xy); - - Py_INCREF(Py_None); - return Py_None; -} - -static PyObject * -_query_palette(ImagingDisplayObject *display, PyObject *args) { - HDC hdc; - int status; - - if (!PyArg_ParseTuple(args, F_HANDLE, &hdc)) { - return NULL; - } - - status = ImagingQueryPaletteDIB(display->dib, hdc); - - return Py_BuildValue("i", status); -} - -static PyObject * -_getdc(ImagingDisplayObject *display, PyObject *args) { - HWND window; - HDC dc; - - if (!PyArg_ParseTuple(args, F_HANDLE, &window)) { - return NULL; - } - - dc = GetDC(window); - if (!dc) { - PyErr_SetString(PyExc_OSError, "cannot create dc"); - return NULL; - } - - return Py_BuildValue(F_HANDLE, dc); -} - -static PyObject * -_releasedc(ImagingDisplayObject *display, PyObject *args) { - HWND window; - HDC dc; - - if (!PyArg_ParseTuple(args, F_HANDLE F_HANDLE, &window, &dc)) { - return NULL; - } - - ReleaseDC(window, dc); - - Py_INCREF(Py_None); - return Py_None; -} - -static PyObject * -_frombytes(ImagingDisplayObject *display, PyObject *args) { - Py_buffer buffer; - - if (!PyArg_ParseTuple(args, "y*:frombytes", &buffer)) { - return NULL; - } - - if (display->dib->ysize * display->dib->linesize != buffer.len) { - PyBuffer_Release(&buffer); - PyErr_SetString(PyExc_ValueError, "wrong size"); - return NULL; - } - - memcpy(display->dib->bits, buffer.buf, buffer.len); - - PyBuffer_Release(&buffer); - Py_INCREF(Py_None); - return Py_None; -} - -static PyObject * -_tobytes(ImagingDisplayObject *display, PyObject *args) { - if (!PyArg_ParseTuple(args, ":tobytes")) { - return NULL; - } - - return PyBytes_FromStringAndSize( - display->dib->bits, display->dib->ysize * display->dib->linesize); -} - -static struct PyMethodDef methods[] = { - {"draw", (PyCFunction)_draw, METH_VARARGS}, - {"expose", (PyCFunction)_expose, METH_VARARGS}, - {"paste", (PyCFunction)_paste, METH_VARARGS}, - {"query_palette", (PyCFunction)_query_palette, METH_VARARGS}, - {"getdc", (PyCFunction)_getdc, METH_VARARGS}, - {"releasedc", (PyCFunction)_releasedc, METH_VARARGS}, - {"frombytes", (PyCFunction)_frombytes, METH_VARARGS}, - {"tobytes", (PyCFunction)_tobytes, METH_VARARGS}, - {NULL, NULL} /* sentinel */ -}; - -static PyObject * -_getattr_mode(ImagingDisplayObject *self, void *closure) { - return Py_BuildValue("s", self->dib->mode); -} - -static PyObject * -_getattr_size(ImagingDisplayObject *self, void *closure) { - return Py_BuildValue("ii", self->dib->xsize, self->dib->ysize); -} - -static struct PyGetSetDef getsetters[] = { - {"mode", (getter)_getattr_mode}, {"size", (getter)_getattr_size}, {NULL}}; - -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*/ -}; - -PyObject * -PyImaging_DisplayWin32(PyObject *self, PyObject *args) { - ImagingDisplayObject *display; - char *mode; - int xsize, ysize; - - if (!PyArg_ParseTuple(args, "s(ii)", &mode, &xsize, &ysize)) { - return NULL; - } - - display = _new(mode, xsize, ysize); - if (display == NULL) { - return NULL; - } - - return (PyObject *)display; -} - -PyObject * -PyImaging_DisplayModeWin32(PyObject *self, PyObject *args) { - char *mode; - int size[2]; - - mode = ImagingGetModeDIB(size); - - return Py_BuildValue("s(ii)", mode, size[0], size[1]); -} - -/* -------------------------------------------------------------------- */ -/* Windows screen grabber */ - -typedef HANDLE(__stdcall *Func_SetThreadDpiAwarenessContext)(HANDLE); - -PyObject * -PyImaging_GrabScreenWin32(PyObject *self, PyObject *args) { - int x = 0, y = 0, width, height; - int includeLayeredWindows = 0, all_screens = 0; - HBITMAP bitmap; - BITMAPCOREHEADER core; - HDC screen, screen_copy; - DWORD rop; - PyObject *buffer; - HANDLE dpiAwareness; - HMODULE user32; - Func_SetThreadDpiAwarenessContext SetThreadDpiAwarenessContext_function; - - if (!PyArg_ParseTuple(args, "|ii", &includeLayeredWindows, &all_screens)) { - return NULL; - } - - /* step 1: create a memory DC large enough to hold the - entire screen */ - - 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"); - if (SetThreadDpiAwarenessContext_function != NULL) { - // DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE = ((DPI_CONTEXT_HANDLE)-3) - dpiAwareness = SetThreadDpiAwarenessContext_function((HANDLE)-3); - } - - if (all_screens) { - x = GetSystemMetrics(SM_XVIRTUALSCREEN); - y = GetSystemMetrics(SM_YVIRTUALSCREEN); - width = GetSystemMetrics(SM_CXVIRTUALSCREEN); - height = GetSystemMetrics(SM_CYVIRTUALSCREEN); - } else { - width = GetDeviceCaps(screen, HORZRES); - height = GetDeviceCaps(screen, VERTRES); - } - - if (SetThreadDpiAwarenessContext_function != NULL) { - SetThreadDpiAwarenessContext_function(dpiAwareness); - } - - FreeLibrary(user32); - - bitmap = CreateCompatibleBitmap(screen, width, height); - if (!bitmap) { - goto error; - } - - if (!SelectObject(screen_copy, bitmap)) { - goto error; - } - - /* step 2: copy bits into memory DC bitmap */ - - rop = SRCCOPY; - if (includeLayeredWindows) { - rop |= CAPTUREBLT; - } - if (!BitBlt(screen_copy, 0, 0, width, height, screen, x, y, rop)) { - goto error; - } - - /* step 3: extract bits from bitmap */ - - buffer = PyBytes_FromStringAndSize(NULL, height * ((width * 3 + 3) & -4)); - if (!buffer) { - return NULL; - } - - core.bcSize = sizeof(core); - core.bcWidth = width; - core.bcHeight = height; - core.bcPlanes = 1; - core.bcBitCount = 24; - if (!GetDIBits( - screen_copy, - bitmap, - 0, - height, - PyBytes_AS_STRING(buffer), - (BITMAPINFO *)&core, - DIB_RGB_COLORS)) { - goto error; - } - - DeleteObject(bitmap); - DeleteDC(screen_copy); - DeleteDC(screen); - - return Py_BuildValue("(ii)(ii)N", x, y, width, height, buffer); - -error: - PyErr_SetString(PyExc_OSError, "screen grab failed"); - - DeleteDC(screen_copy); - DeleteDC(screen); - - return NULL; -} - -/* -------------------------------------------------------------------- */ -/* Windows clipboard grabber */ - -PyObject * -PyImaging_GrabClipboardWin32(PyObject *self, PyObject *args) { - HANDLE handle = NULL; - int size; - void *data; - PyObject *result; - UINT format; - UINT formats[] = {CF_DIB, CF_DIBV5, CF_HDROP, RegisterClipboardFormatA("PNG"), 0}; - LPCSTR format_names[] = {"DIB", "DIB", "file", "png", NULL}; - - if (!OpenClipboard(NULL)) { - // Maybe the clipboard is temporarily in use by another process. - // Wait and try again - Sleep(500); - - if (!OpenClipboard(NULL)) { - PyErr_SetString(PyExc_OSError, "failed to open clipboard"); - return NULL; - } - } - - // find best format as set by clipboard owner - format = 0; - while (!handle && (format = EnumClipboardFormats(format))) { - for (UINT i = 0; formats[i] != 0; i++) { - if (format == formats[i]) { - handle = GetClipboardData(format); - format = i; - break; - } - } - } - - if (!handle) { - CloseClipboard(); - return Py_BuildValue("zO", NULL, Py_None); - } - - data = GlobalLock(handle); - size = GlobalSize(handle); - - result = PyBytes_FromStringAndSize(data, size); - - GlobalUnlock(handle); - CloseClipboard(); - - return Py_BuildValue("zN", format_names[format], result); -} - -/* -------------------------------------------------------------------- */ -/* Windows class */ - -#ifndef WM_MOUSEWHEEL -#define WM_MOUSEWHEEL 522 -#endif - -static int mainloop = 0; - -static void -callback_error(const char *handler) { - PyObject *sys_stderr; - - sys_stderr = PySys_GetObject("stderr"); - - if (sys_stderr) { - PyFile_WriteString("*** ImageWin: error in ", sys_stderr); - PyFile_WriteString((char *)handler, sys_stderr); - PyFile_WriteString(":\n", sys_stderr); - } - - PyErr_Print(); - PyErr_Clear(); -} - -static LRESULT CALLBACK -windowCallback(HWND wnd, UINT message, WPARAM wParam, LPARAM lParam) { - PAINTSTRUCT ps; - PyObject *callback = NULL; - PyObject *result; - PyThreadState *threadstate; - PyThreadState *current_threadstate; - HDC dc; - RECT rect; - LRESULT status = 0; - - /* set up threadstate for messages that calls back into python */ - switch (message) { - case WM_CREATE: - mainloop++; - break; - case WM_DESTROY: - mainloop--; - /* fall through... */ - case WM_PAINT: - case WM_SIZE: - callback = (PyObject *)GetWindowLongPtr(wnd, 0); - if (callback) { - threadstate = - (PyThreadState *)GetWindowLongPtr(wnd, sizeof(PyObject *)); - current_threadstate = PyThreadState_Swap(NULL); - PyEval_RestoreThread(threadstate); - } else { - return DefWindowProc(wnd, message, wParam, lParam); - } - } - - /* process message */ - switch (message) { - case WM_PAINT: - /* redraw (part of) window. this generates a WCK-style - damage/clear/repair cascade */ - BeginPaint(wnd, &ps); - dc = GetDC(wnd); - GetWindowRect(wnd, &rect); /* in screen coordinates */ - - result = PyObject_CallFunction( - callback, - "siiii", - "damage", - ps.rcPaint.left, - ps.rcPaint.top, - ps.rcPaint.right, - ps.rcPaint.bottom); - if (result) { - Py_DECREF(result); - } else { - callback_error("window damage callback"); - } - - result = PyObject_CallFunction( - callback, - "s" F_HANDLE "iiii", - "clear", - dc, - 0, - 0, - rect.right - rect.left, - rect.bottom - rect.top); - if (result) { - Py_DECREF(result); - } else { - callback_error("window clear callback"); - } - - result = PyObject_CallFunction( - callback, - "s" F_HANDLE "iiii", - "repair", - dc, - 0, - 0, - rect.right - rect.left, - rect.bottom - rect.top); - if (result) { - Py_DECREF(result); - } else { - callback_error("window repair callback"); - } - - ReleaseDC(wnd, dc); - EndPaint(wnd, &ps); - break; - - case WM_SIZE: - /* resize window */ - result = PyObject_CallFunction( - callback, "sii", "resize", LOWORD(lParam), HIWORD(lParam)); - if (result) { - InvalidateRect(wnd, NULL, 1); - Py_DECREF(result); - } else { - callback_error("window resize callback"); - } - break; - - case WM_DESTROY: - /* destroy window */ - result = PyObject_CallFunction(callback, "s", "destroy"); - if (result) { - Py_DECREF(result); - } else { - callback_error("window destroy callback"); - } - Py_DECREF(callback); - break; - - default: - status = DefWindowProc(wnd, message, wParam, lParam); - } - - if (callback) { - /* restore thread state */ - PyEval_SaveThread(); - PyThreadState_Swap(threadstate); - } - - return status; -} - -PyObject * -PyImaging_CreateWindowWin32(PyObject *self, PyObject *args) { - HWND wnd; - WNDCLASS windowClass; - - char *title; - PyObject *callback; - int width = 0, height = 0; - if (!PyArg_ParseTuple(args, "sO|ii", &title, &callback, &width, &height)) { - return NULL; - } - - if (width <= 0) { - width = CW_USEDEFAULT; - } - if (height <= 0) { - height = CW_USEDEFAULT; - } - - /* register toplevel window class */ - windowClass.style = CS_CLASSDC; - windowClass.cbClsExtra = 0; - windowClass.cbWndExtra = sizeof(PyObject *) + sizeof(PyThreadState *); - windowClass.hInstance = GetModuleHandle(NULL); - /* windowClass.hbrBackground = (HBRUSH) (COLOR_BTNFACE + 1); */ - windowClass.hbrBackground = NULL; - windowClass.lpszMenuName = NULL; - windowClass.lpszClassName = "pilWindow"; - windowClass.lpfnWndProc = windowCallback; - windowClass.hIcon = LoadIcon(GetModuleHandle(NULL), MAKEINTRESOURCE(1)); - windowClass.hCursor = LoadCursor(NULL, IDC_ARROW); /* CROSS? */ - - RegisterClass(&windowClass); /* FIXME: check return status */ - - wnd = CreateWindowEx( - 0, - windowClass.lpszClassName, - title, - WS_OVERLAPPEDWINDOW, - CW_USEDEFAULT, - CW_USEDEFAULT, - width, - height, - HWND_DESKTOP, - NULL, - NULL, - NULL); - - if (!wnd) { - PyErr_SetString(PyExc_OSError, "failed to create window"); - return NULL; - } - - /* register window callback */ - Py_INCREF(callback); - SetWindowLongPtr(wnd, 0, (LONG_PTR)callback); - SetWindowLongPtr(wnd, sizeof(callback), (LONG_PTR)PyThreadState_Get()); - - Py_BEGIN_ALLOW_THREADS ShowWindow(wnd, SW_SHOWNORMAL); - SetForegroundWindow(wnd); /* to make sure it's visible */ - Py_END_ALLOW_THREADS - - return Py_BuildValue(F_HANDLE, wnd); -} - -PyObject * -PyImaging_EventLoopWin32(PyObject *self, PyObject *args) { - MSG msg; - - Py_BEGIN_ALLOW_THREADS while (mainloop && GetMessage(&msg, NULL, 0, 0)) { - TranslateMessage(&msg); - DispatchMessage(&msg); - } - Py_END_ALLOW_THREADS - - Py_INCREF(Py_None); - return Py_None; -} - -/* -------------------------------------------------------------------- */ -/* windows WMF renderer */ - -#define GET32(p, o) ((DWORD *)(p + o))[0] - -PyObject * -PyImaging_DrawWmf(PyObject *self, PyObject *args) { - HBITMAP bitmap; - HENHMETAFILE meta; - BITMAPCOREHEADER core; - HDC dc; - RECT rect; - PyObject *buffer = NULL; - void *ptr; - - char *data; - Py_ssize_t datasize; - int width, height; - int x0, y0, x1, y1; - if (!PyArg_ParseTuple( - args, - "y#(ii)(iiii):_load", - &data, - &datasize, - &width, - &height, - &x0, - &x1, - &y0, - &y1)) { - return NULL; - } - - /* step 1: copy metafile contents into METAFILE object */ - - if (datasize > 22 && GET32(data, 0) == 0x9ac6cdd7) { - /* placeable windows metafile (22-byte aldus header) */ - meta = SetWinMetaFileBits(datasize - 22, data + 22, NULL, NULL); - - } else if (datasize > 80 && GET32(data, 0) == 1 && GET32(data, 40) == 0x464d4520) { - /* enhanced metafile */ - meta = SetEnhMetaFileBits(datasize, data); - - } else { - /* unknown meta format */ - meta = NULL; - } - - if (!meta) { - PyErr_SetString(PyExc_OSError, "cannot load metafile"); - return NULL; - } - - /* step 2: create bitmap */ - - core.bcSize = sizeof(core); - core.bcWidth = width; - core.bcHeight = height; - core.bcPlanes = 1; - core.bcBitCount = 24; - - dc = CreateCompatibleDC(NULL); - - bitmap = CreateDIBSection(dc, (BITMAPINFO *)&core, DIB_RGB_COLORS, &ptr, NULL, 0); - - if (!bitmap) { - PyErr_SetString(PyExc_OSError, "cannot create bitmap"); - goto error; - } - - if (!SelectObject(dc, bitmap)) { - PyErr_SetString(PyExc_OSError, "cannot select bitmap"); - goto error; - } - - /* step 3: render metafile into bitmap */ - - rect.left = rect.top = 0; - rect.right = width; - rect.bottom = height; - - /* 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; - } - - /* step 4: extract bits from bitmap */ - - GdiFlush(); - - buffer = PyBytes_FromStringAndSize(ptr, height * ((width * 3 + 3) & -4)); - -error: - DeleteEnhMetaFile(meta); - - if (bitmap) { - DeleteObject(bitmap); - } - - DeleteDC(dc); - - return buffer; -} - -#endif /* _WIN32 */ - -/* -------------------------------------------------------------------- */ -/* X11 support */ - -#ifdef HAVE_XCB -#include - -/* -------------------------------------------------------------------- */ -/* X11 screen grabber */ - -PyObject * -PyImaging_GrabScreenX11(PyObject *self, PyObject *args) { - int width, height; - char *display_name; - xcb_connection_t *connection; - int screen_number; - xcb_screen_iterator_t iter; - xcb_screen_t *screen = NULL; - xcb_get_image_reply_t *reply; - xcb_generic_error_t *error; - PyObject *buffer = NULL; - - if (!PyArg_ParseTuple(args, "|z", &display_name)) { - return NULL; - } - - /* connect to X and get screen data */ - - connection = xcb_connect(display_name, &screen_number); - if (xcb_connection_has_error(connection)) { - PyErr_Format( - PyExc_OSError, - "X connection failed: error %i", - xcb_connection_has_error(connection)); - xcb_disconnect(connection); - return NULL; - } - - iter = xcb_setup_roots_iterator(xcb_get_setup(connection)); - for (; iter.rem; --screen_number, xcb_screen_next(&iter)) { - if (screen_number == 0) { - screen = iter.data; - break; - } - } - if (screen == NULL || screen->root == 0) { - // this case is usually caught with "X connection failed: error 6" above - xcb_disconnect(connection); - PyErr_SetString(PyExc_OSError, "X screen not found"); - return NULL; - } - - width = screen->width_in_pixels; - height = screen->height_in_pixels; - - /* get image data */ - - reply = xcb_get_image_reply( - connection, - xcb_get_image( - connection, - XCB_IMAGE_FORMAT_Z_PIXMAP, - screen->root, - 0, - 0, - width, - height, - 0x00ffffff), - &error); - if (reply == NULL) { - PyErr_Format( - PyExc_OSError, - "X get_image failed: error %i (%i, %i, %i)", - error->error_code, - error->major_code, - error->minor_code, - error->resource_id); - free(error); - xcb_disconnect(connection); - return NULL; - } - - /* store data in Python buffer */ - - if (reply->depth == 24) { - buffer = PyBytes_FromStringAndSize( - (char *)xcb_get_image_data(reply), xcb_get_image_data_length(reply)); - } else { - PyErr_Format(PyExc_OSError, "unsupported bit depth: %i", reply->depth); - } - - free(reply); - xcb_disconnect(connection); - - if (!buffer) { - return NULL; - } - - return Py_BuildValue("(ii)N", width, height, buffer); -} - -#endif /* HAVE_XCB */ +/* + * The Python Imaging Library. + * + * display support (and other windows-related stuff) + * + * History: + * 1996-05-13 fl Windows DIB support + * 1996-05-21 fl Added palette stuff + * 1996-05-28 fl Added display_mode stuff + * 1997-09-21 fl Added draw primitive + * 2001-09-17 fl Added ImagingGrabScreen (from _grabscreen.c) + * 2002-05-12 fl Added ImagingListWindows + * 2002-11-19 fl Added clipboard support + * 2002-11-25 fl Added GetDC/ReleaseDC helpers + * 2003-05-21 fl Added create window support (including window callback) + * 2003-09-05 fl Added fromstring/tostring methods + * 2009-03-14 fl Added WMF support (from pilwmf) + * + * Copyright (c) 1997-2003 by Secret Labs AB. + * Copyright (c) 1996-1997 by Fredrik Lundh. + * + * See the README file for information on usage and redistribution. + */ + +#define PY_SSIZE_T_CLEAN +#include "Python.h" + +#include "libImaging/Imaging.h" + +/* -------------------------------------------------------------------- */ +/* Windows DIB support */ + +#ifdef _WIN32 + +#include "libImaging/ImDib.h" + +#if SIZEOF_VOID_P == 8 +#define F_HANDLE "K" +#else +#define F_HANDLE "k" +#endif + +typedef struct { + PyObject_HEAD ImagingDIB dib; +} ImagingDisplayObject; + +static PyTypeObject ImagingDisplayType; + +static ImagingDisplayObject * +_new(const char *mode, int xsize, int ysize) { + ImagingDisplayObject *display; + + if (PyType_Ready(&ImagingDisplayType) < 0) { + return NULL; + } + + display = PyObject_New(ImagingDisplayObject, &ImagingDisplayType); + if (display == NULL) { + return NULL; + } + + display->dib = ImagingNewDIB(mode, xsize, ysize); + if (!display->dib) { + Py_DECREF(display); + return NULL; + } + + return display; +} + +static void +_delete(ImagingDisplayObject *display) { + if (display->dib) { + ImagingDeleteDIB(display->dib); + } + PyObject_Del(display); +} + +static PyObject * +_expose(ImagingDisplayObject *display, PyObject *args) { + HDC hdc; + if (!PyArg_ParseTuple(args, F_HANDLE, &hdc)) { + return NULL; + } + + ImagingExposeDIB(display->dib, hdc); + + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject * +_draw(ImagingDisplayObject *display, PyObject *args) { + HDC hdc; + int dst[4]; + int src[4]; + if (!PyArg_ParseTuple( + args, + F_HANDLE "(iiii)(iiii)", + &hdc, + dst + 0, + dst + 1, + dst + 2, + dst + 3, + src + 0, + src + 1, + src + 2, + src + 3)) { + return NULL; + } + + ImagingDrawDIB(display->dib, hdc, dst, src); + + Py_INCREF(Py_None); + return Py_None; +} + +extern Imaging +PyImaging_AsImaging(PyObject *op); + +static PyObject * +_paste(ImagingDisplayObject *display, PyObject *args) { + Imaging im; + + PyObject *op; + int xy[4]; + xy[0] = xy[1] = xy[2] = xy[3] = 0; + if (!PyArg_ParseTuple(args, "O|(iiii)", &op, xy + 0, xy + 1, xy + 2, xy + 3)) { + return NULL; + } + im = PyImaging_AsImaging(op); + if (!im) { + return NULL; + } + + if (xy[2] <= xy[0]) { + xy[2] = xy[0] + im->xsize; + } + if (xy[3] <= xy[1]) { + xy[3] = xy[1] + im->ysize; + } + + ImagingPasteDIB(display->dib, im, xy); + + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject * +_query_palette(ImagingDisplayObject *display, PyObject *args) { + HDC hdc; + int status; + + if (!PyArg_ParseTuple(args, F_HANDLE, &hdc)) { + return NULL; + } + + status = ImagingQueryPaletteDIB(display->dib, hdc); + + return Py_BuildValue("i", status); +} + +static PyObject * +_getdc(ImagingDisplayObject *display, PyObject *args) { + HWND window; + HDC dc; + + if (!PyArg_ParseTuple(args, F_HANDLE, &window)) { + return NULL; + } + + dc = GetDC(window); + if (!dc) { + PyErr_SetString(PyExc_OSError, "cannot create dc"); + return NULL; + } + + return Py_BuildValue(F_HANDLE, dc); +} + +static PyObject * +_releasedc(ImagingDisplayObject *display, PyObject *args) { + HWND window; + HDC dc; + + if (!PyArg_ParseTuple(args, F_HANDLE F_HANDLE, &window, &dc)) { + return NULL; + } + + ReleaseDC(window, dc); + + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject * +_frombytes(ImagingDisplayObject *display, PyObject *args) { + Py_buffer buffer; + + if (!PyArg_ParseTuple(args, "y*:frombytes", &buffer)) { + return NULL; + } + + if (display->dib->ysize * display->dib->linesize != buffer.len) { + PyBuffer_Release(&buffer); + PyErr_SetString(PyExc_ValueError, "wrong size"); + return NULL; + } + + memcpy(display->dib->bits, buffer.buf, buffer.len); + + PyBuffer_Release(&buffer); + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject * +_tobytes(ImagingDisplayObject *display, PyObject *args) { + if (!PyArg_ParseTuple(args, ":tobytes")) { + return NULL; + } + + return PyBytes_FromStringAndSize( + display->dib->bits, display->dib->ysize * display->dib->linesize); +} + +static struct PyMethodDef methods[] = { + {"draw", (PyCFunction)_draw, METH_VARARGS}, + {"expose", (PyCFunction)_expose, METH_VARARGS}, + {"paste", (PyCFunction)_paste, METH_VARARGS}, + {"query_palette", (PyCFunction)_query_palette, METH_VARARGS}, + {"getdc", (PyCFunction)_getdc, METH_VARARGS}, + {"releasedc", (PyCFunction)_releasedc, METH_VARARGS}, + {"frombytes", (PyCFunction)_frombytes, METH_VARARGS}, + {"tobytes", (PyCFunction)_tobytes, METH_VARARGS}, + {NULL, NULL} /* sentinel */ +}; + +static PyObject * +_getattr_mode(ImagingDisplayObject *self, void *closure) { + return Py_BuildValue("s", self->dib->mode); +} + +static PyObject * +_getattr_size(ImagingDisplayObject *self, void *closure) { + return Py_BuildValue("ii", self->dib->xsize, self->dib->ysize); +} + +static struct PyGetSetDef getsetters[] = { + {"mode", (getter)_getattr_mode}, {"size", (getter)_getattr_size}, {NULL}}; + +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*/ +}; + +PyObject * +PyImaging_DisplayWin32(PyObject *self, PyObject *args) { + ImagingDisplayObject *display; + char *mode; + int xsize, ysize; + + if (!PyArg_ParseTuple(args, "s(ii)", &mode, &xsize, &ysize)) { + return NULL; + } + + display = _new(mode, xsize, ysize); + if (display == NULL) { + return NULL; + } + + return (PyObject *)display; +} + +PyObject * +PyImaging_DisplayModeWin32(PyObject *self, PyObject *args) { + char *mode; + int size[2]; + + mode = ImagingGetModeDIB(size); + + return Py_BuildValue("s(ii)", mode, size[0], size[1]); +} + +/* -------------------------------------------------------------------- */ +/* Windows screen grabber */ + +typedef HANDLE(__stdcall *Func_SetThreadDpiAwarenessContext)(HANDLE); + +PyObject * +PyImaging_GrabScreenWin32(PyObject *self, PyObject *args) { + int x = 0, y = 0, width, height; + int includeLayeredWindows = 0, all_screens = 0; + HBITMAP bitmap; + BITMAPCOREHEADER core; + HDC screen, screen_copy; + DWORD rop; + PyObject *buffer; + HANDLE dpiAwareness; + HMODULE user32; + Func_SetThreadDpiAwarenessContext SetThreadDpiAwarenessContext_function; + + if (!PyArg_ParseTuple(args, "|ii", &includeLayeredWindows, &all_screens)) { + return NULL; + } + + /* step 1: create a memory DC large enough to hold the + entire screen */ + + 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"); + if (SetThreadDpiAwarenessContext_function != NULL) { + // DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE = ((DPI_CONTEXT_HANDLE)-3) + dpiAwareness = SetThreadDpiAwarenessContext_function((HANDLE)-3); + } + + if (all_screens) { + x = GetSystemMetrics(SM_XVIRTUALSCREEN); + y = GetSystemMetrics(SM_YVIRTUALSCREEN); + width = GetSystemMetrics(SM_CXVIRTUALSCREEN); + height = GetSystemMetrics(SM_CYVIRTUALSCREEN); + } else { + width = GetDeviceCaps(screen, HORZRES); + height = GetDeviceCaps(screen, VERTRES); + } + + if (SetThreadDpiAwarenessContext_function != NULL) { + SetThreadDpiAwarenessContext_function(dpiAwareness); + } + + FreeLibrary(user32); + + bitmap = CreateCompatibleBitmap(screen, width, height); + if (!bitmap) { + goto error; + } + + if (!SelectObject(screen_copy, bitmap)) { + goto error; + } + + /* step 2: copy bits into memory DC bitmap */ + + rop = SRCCOPY; + if (includeLayeredWindows) { + rop |= CAPTUREBLT; + } + if (!BitBlt(screen_copy, 0, 0, width, height, screen, x, y, rop)) { + goto error; + } + + /* step 3: extract bits from bitmap */ + + buffer = PyBytes_FromStringAndSize(NULL, height * ((width * 3 + 3) & -4)); + if (!buffer) { + return NULL; + } + + core.bcSize = sizeof(core); + core.bcWidth = width; + core.bcHeight = height; + core.bcPlanes = 1; + core.bcBitCount = 24; + if (!GetDIBits( + screen_copy, + bitmap, + 0, + height, + PyBytes_AS_STRING(buffer), + (BITMAPINFO *)&core, + DIB_RGB_COLORS)) { + goto error; + } + + DeleteObject(bitmap); + DeleteDC(screen_copy); + DeleteDC(screen); + + return Py_BuildValue("(ii)(ii)N", x, y, width, height, buffer); + +error: + PyErr_SetString(PyExc_OSError, "screen grab failed"); + + DeleteDC(screen_copy); + DeleteDC(screen); + + return NULL; +} + +/* -------------------------------------------------------------------- */ +/* Windows clipboard grabber */ + +PyObject * +PyImaging_GrabClipboardWin32(PyObject *self, PyObject *args) { + HANDLE handle = NULL; + int size; + void *data; + PyObject *result; + UINT format; + UINT formats[] = {CF_DIB, CF_DIBV5, CF_HDROP, RegisterClipboardFormatA("PNG"), 0}; + LPCSTR format_names[] = {"DIB", "DIB", "file", "png", NULL}; + + if (!OpenClipboard(NULL)) { + // Maybe the clipboard is temporarily in use by another process. + // Wait and try again + Sleep(500); + + if (!OpenClipboard(NULL)) { + PyErr_SetString(PyExc_OSError, "failed to open clipboard"); + return NULL; + } + } + + // find best format as set by clipboard owner + format = 0; + while (!handle && (format = EnumClipboardFormats(format))) { + for (UINT i = 0; formats[i] != 0; i++) { + if (format == formats[i]) { + handle = GetClipboardData(format); + format = i; + break; + } + } + } + + if (!handle) { + CloseClipboard(); + return Py_BuildValue("zO", NULL, Py_None); + } + + data = GlobalLock(handle); + size = GlobalSize(handle); + + result = PyBytes_FromStringAndSize(data, size); + + GlobalUnlock(handle); + CloseClipboard(); + + return Py_BuildValue("zN", format_names[format], result); +} + +/* -------------------------------------------------------------------- */ +/* Windows class */ + +#ifndef WM_MOUSEWHEEL +#define WM_MOUSEWHEEL 522 +#endif + +static int mainloop = 0; + +static void +callback_error(const char *handler) { + PyObject *sys_stderr; + + sys_stderr = PySys_GetObject("stderr"); + + if (sys_stderr) { + PyFile_WriteString("*** ImageWin: error in ", sys_stderr); + PyFile_WriteString((char *)handler, sys_stderr); + PyFile_WriteString(":\n", sys_stderr); + } + + PyErr_Print(); + PyErr_Clear(); +} + +static LRESULT CALLBACK +windowCallback(HWND wnd, UINT message, WPARAM wParam, LPARAM lParam) { + PAINTSTRUCT ps; + PyObject *callback = NULL; + PyObject *result; + PyThreadState *threadstate; + PyThreadState *current_threadstate; + HDC dc; + RECT rect; + LRESULT status = 0; + + /* set up threadstate for messages that calls back into python */ + switch (message) { + case WM_CREATE: + mainloop++; + break; + case WM_DESTROY: + mainloop--; + /* fall through... */ + case WM_PAINT: + case WM_SIZE: + callback = (PyObject *)GetWindowLongPtr(wnd, 0); + if (callback) { + threadstate = + (PyThreadState *)GetWindowLongPtr(wnd, sizeof(PyObject *)); + current_threadstate = PyThreadState_Swap(NULL); + PyEval_RestoreThread(threadstate); + } else { + return DefWindowProc(wnd, message, wParam, lParam); + } + } + + /* process message */ + switch (message) { + case WM_PAINT: + /* redraw (part of) window. this generates a WCK-style + damage/clear/repair cascade */ + BeginPaint(wnd, &ps); + dc = GetDC(wnd); + GetWindowRect(wnd, &rect); /* in screen coordinates */ + + result = PyObject_CallFunction( + callback, + "siiii", + "damage", + ps.rcPaint.left, + ps.rcPaint.top, + ps.rcPaint.right, + ps.rcPaint.bottom); + if (result) { + Py_DECREF(result); + } else { + callback_error("window damage callback"); + } + + result = PyObject_CallFunction( + callback, + "s" F_HANDLE "iiii", + "clear", + dc, + 0, + 0, + rect.right - rect.left, + rect.bottom - rect.top); + if (result) { + Py_DECREF(result); + } else { + callback_error("window clear callback"); + } + + result = PyObject_CallFunction( + callback, + "s" F_HANDLE "iiii", + "repair", + dc, + 0, + 0, + rect.right - rect.left, + rect.bottom - rect.top); + if (result) { + Py_DECREF(result); + } else { + callback_error("window repair callback"); + } + + ReleaseDC(wnd, dc); + EndPaint(wnd, &ps); + break; + + case WM_SIZE: + /* resize window */ + result = PyObject_CallFunction( + callback, "sii", "resize", LOWORD(lParam), HIWORD(lParam)); + if (result) { + InvalidateRect(wnd, NULL, 1); + Py_DECREF(result); + } else { + callback_error("window resize callback"); + } + break; + + case WM_DESTROY: + /* destroy window */ + result = PyObject_CallFunction(callback, "s", "destroy"); + if (result) { + Py_DECREF(result); + } else { + callback_error("window destroy callback"); + } + Py_DECREF(callback); + break; + + default: + status = DefWindowProc(wnd, message, wParam, lParam); + } + + if (callback) { + /* restore thread state */ + PyEval_SaveThread(); + PyThreadState_Swap(threadstate); + } + + return status; +} + +PyObject * +PyImaging_CreateWindowWin32(PyObject *self, PyObject *args) { + HWND wnd; + WNDCLASS windowClass; + + char *title; + PyObject *callback; + int width = 0, height = 0; + if (!PyArg_ParseTuple(args, "sO|ii", &title, &callback, &width, &height)) { + return NULL; + } + + if (width <= 0) { + width = CW_USEDEFAULT; + } + if (height <= 0) { + height = CW_USEDEFAULT; + } + + /* register toplevel window class */ + windowClass.style = CS_CLASSDC; + windowClass.cbClsExtra = 0; + windowClass.cbWndExtra = sizeof(PyObject *) + sizeof(PyThreadState *); + windowClass.hInstance = GetModuleHandle(NULL); + /* windowClass.hbrBackground = (HBRUSH) (COLOR_BTNFACE + 1); */ + windowClass.hbrBackground = NULL; + windowClass.lpszMenuName = NULL; + windowClass.lpszClassName = "pilWindow"; + windowClass.lpfnWndProc = windowCallback; + windowClass.hIcon = LoadIcon(GetModuleHandle(NULL), MAKEINTRESOURCE(1)); + windowClass.hCursor = LoadCursor(NULL, IDC_ARROW); /* CROSS? */ + + RegisterClass(&windowClass); /* FIXME: check return status */ + + wnd = CreateWindowEx( + 0, + windowClass.lpszClassName, + title, + WS_OVERLAPPEDWINDOW, + CW_USEDEFAULT, + CW_USEDEFAULT, + width, + height, + HWND_DESKTOP, + NULL, + NULL, + NULL); + + if (!wnd) { + PyErr_SetString(PyExc_OSError, "failed to create window"); + return NULL; + } + + /* register window callback */ + Py_INCREF(callback); + SetWindowLongPtr(wnd, 0, (LONG_PTR)callback); + SetWindowLongPtr(wnd, sizeof(callback), (LONG_PTR)PyThreadState_Get()); + + Py_BEGIN_ALLOW_THREADS ShowWindow(wnd, SW_SHOWNORMAL); + SetForegroundWindow(wnd); /* to make sure it's visible */ + Py_END_ALLOW_THREADS + + return Py_BuildValue(F_HANDLE, wnd); +} + +PyObject * +PyImaging_EventLoopWin32(PyObject *self, PyObject *args) { + MSG msg; + + Py_BEGIN_ALLOW_THREADS while (mainloop && GetMessage(&msg, NULL, 0, 0)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + Py_END_ALLOW_THREADS + + Py_INCREF(Py_None); + return Py_None; +} + +/* -------------------------------------------------------------------- */ +/* windows WMF renderer */ + +#define GET32(p, o) ((DWORD *)(p + o))[0] + +PyObject * +PyImaging_DrawWmf(PyObject *self, PyObject *args) { + HBITMAP bitmap; + HENHMETAFILE meta; + BITMAPCOREHEADER core; + HDC dc; + RECT rect; + PyObject *buffer = NULL; + void *ptr; + + char *data; + Py_ssize_t datasize; + int width, height; + int x0, y0, x1, y1; + if (!PyArg_ParseTuple( + args, + "y#(ii)(iiii):_load", + &data, + &datasize, + &width, + &height, + &x0, + &x1, + &y0, + &y1)) { + return NULL; + } + + /* step 1: copy metafile contents into METAFILE object */ + + if (datasize > 22 && GET32(data, 0) == 0x9ac6cdd7) { + /* placeable windows metafile (22-byte aldus header) */ + meta = SetWinMetaFileBits(datasize - 22, data + 22, NULL, NULL); + + } else if (datasize > 80 && GET32(data, 0) == 1 && GET32(data, 40) == 0x464d4520) { + /* enhanced metafile */ + meta = SetEnhMetaFileBits(datasize, data); + + } else { + /* unknown meta format */ + meta = NULL; + } + + if (!meta) { + PyErr_SetString(PyExc_OSError, "cannot load metafile"); + return NULL; + } + + /* step 2: create bitmap */ + + core.bcSize = sizeof(core); + core.bcWidth = width; + core.bcHeight = height; + core.bcPlanes = 1; + core.bcBitCount = 24; + + dc = CreateCompatibleDC(NULL); + + bitmap = CreateDIBSection(dc, (BITMAPINFO *)&core, DIB_RGB_COLORS, &ptr, NULL, 0); + + if (!bitmap) { + PyErr_SetString(PyExc_OSError, "cannot create bitmap"); + goto error; + } + + if (!SelectObject(dc, bitmap)) { + PyErr_SetString(PyExc_OSError, "cannot select bitmap"); + goto error; + } + + /* step 3: render metafile into bitmap */ + + rect.left = rect.top = 0; + rect.right = width; + rect.bottom = height; + + /* 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; + } + + /* step 4: extract bits from bitmap */ + + GdiFlush(); + + buffer = PyBytes_FromStringAndSize(ptr, height * ((width * 3 + 3) & -4)); + +error: + DeleteEnhMetaFile(meta); + + if (bitmap) { + DeleteObject(bitmap); + } + + DeleteDC(dc); + + return buffer; +} + +#endif /* _WIN32 */ + +/* -------------------------------------------------------------------- */ +/* X11 support */ + +#ifdef HAVE_XCB +#include + +/* -------------------------------------------------------------------- */ +/* X11 screen grabber */ + +PyObject * +PyImaging_GrabScreenX11(PyObject *self, PyObject *args) { + int width, height; + char *display_name; + xcb_connection_t *connection; + int screen_number; + xcb_screen_iterator_t iter; + xcb_screen_t *screen = NULL; + xcb_get_image_reply_t *reply; + xcb_generic_error_t *error; + PyObject *buffer = NULL; + + if (!PyArg_ParseTuple(args, "|z", &display_name)) { + return NULL; + } + + /* connect to X and get screen data */ + + connection = xcb_connect(display_name, &screen_number); + if (xcb_connection_has_error(connection)) { + PyErr_Format( + PyExc_OSError, + "X connection failed: error %i", + xcb_connection_has_error(connection)); + xcb_disconnect(connection); + return NULL; + } + + iter = xcb_setup_roots_iterator(xcb_get_setup(connection)); + for (; iter.rem; --screen_number, xcb_screen_next(&iter)) { + if (screen_number == 0) { + screen = iter.data; + break; + } + } + if (screen == NULL || screen->root == 0) { + // this case is usually caught with "X connection failed: error 6" above + xcb_disconnect(connection); + PyErr_SetString(PyExc_OSError, "X screen not found"); + return NULL; + } + + width = screen->width_in_pixels; + height = screen->height_in_pixels; + + /* get image data */ + + reply = xcb_get_image_reply( + connection, + xcb_get_image( + connection, + XCB_IMAGE_FORMAT_Z_PIXMAP, + screen->root, + 0, + 0, + width, + height, + 0x00ffffff), + &error); + if (reply == NULL) { + PyErr_Format( + PyExc_OSError, + "X get_image failed: error %i (%i, %i, %i)", + error->error_code, + error->major_code, + error->minor_code, + error->resource_id); + free(error); + xcb_disconnect(connection); + return NULL; + } + + /* store data in Python buffer */ + + if (reply->depth == 24) { + buffer = PyBytes_FromStringAndSize( + (char *)xcb_get_image_data(reply), xcb_get_image_data_length(reply)); + } else { + PyErr_Format(PyExc_OSError, "unsupported bit depth: %i", reply->depth); + } + + free(reply); + xcb_disconnect(connection); + + if (!buffer) { + return NULL; + } + + return Py_BuildValue("(ii)N", width, height, buffer); +} + +#endif /* HAVE_XCB */ diff --git a/src/encode.c b/src/encode.c index 442b5d04f..5659d894a 100644 --- a/src/encode.c +++ b/src/encode.c @@ -1,1382 +1,1382 @@ -/* - * The Python Imaging Library. - * - * standard encoder interfaces for the Imaging library - * - * History: - * 1996-04-19 fl Based on decoders.c - * 1996-05-12 fl Compile cleanly as C++ - * 1996-12-30 fl Plugged potential memory leak for tiled images - * 1997-01-03 fl Added GIF encoder - * 1997-01-05 fl Plugged encoder buffer leaks - * 1997-01-11 fl Added encode_to_file method - * 1998-03-09 fl Added mode/rawmode argument to encoders - * 1998-07-09 fl Added interlace argument to GIF encoder - * 1999-02-07 fl Added PCX encoder - * - * Copyright (c) 1997-2001 by Secret Labs AB - * Copyright (c) 1996-1997 by Fredrik Lundh - * - * See the README file for information on usage and redistribution. - */ - -/* FIXME: make these pluggable! */ - -#define PY_SSIZE_T_CLEAN -#include "Python.h" - -#include "libImaging/Imaging.h" -#include "libImaging/Gif.h" - -#ifdef HAVE_UNISTD_H -#include /* write */ -#endif - -/* -------------------------------------------------------------------- */ -/* Common */ -/* -------------------------------------------------------------------- */ - -typedef struct { - PyObject_HEAD int (*encode)( - Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); - int (*cleanup)(ImagingCodecState state); - struct ImagingCodecStateInstance state; - Imaging im; - PyObject *lock; - int pushes_fd; -} ImagingEncoderObject; - -static PyTypeObject ImagingEncoderType; - -static ImagingEncoderObject * -PyImaging_EncoderNew(int contextsize) { - ImagingEncoderObject *encoder; - void *context; - - if (PyType_Ready(&ImagingEncoderType) < 0) { - return NULL; - } - - encoder = PyObject_New(ImagingEncoderObject, &ImagingEncoderType); - if (encoder == NULL) { - return NULL; - } - - /* Clear the encoder state */ - memset(&encoder->state, 0, sizeof(encoder->state)); - - /* Allocate encoder context */ - if (contextsize > 0) { - context = (void *)calloc(1, contextsize); - if (!context) { - Py_DECREF(encoder); - (void)ImagingError_MemoryError(); - return NULL; - } - } else { - context = 0; - } - - /* Initialize encoder context */ - encoder->state.context = context; - - /* Most encoders don't need this */ - encoder->cleanup = NULL; - - /* Target image */ - encoder->lock = NULL; - encoder->im = NULL; - encoder->pushes_fd = 0; - - return encoder; -} - -static void -_dealloc(ImagingEncoderObject *encoder) { - if (encoder->cleanup) { - encoder->cleanup(&encoder->state); - } - free(encoder->state.buffer); - free(encoder->state.context); - Py_XDECREF(encoder->lock); - Py_XDECREF(encoder->state.fd); - PyObject_Del(encoder); -} - -static PyObject * -_encode_cleanup(ImagingEncoderObject *encoder, PyObject *args) { - int status = 0; - - if (encoder->cleanup) { - status = encoder->cleanup(&encoder->state); - } - - return Py_BuildValue("i", status); -} - -static PyObject * -_encode(ImagingEncoderObject *encoder, PyObject *args) { - PyObject *buf; - PyObject *result; - int status; - - /* Encode to a Python string (allocated by this method) */ - - Py_ssize_t bufsize = 16384; - - if (!PyArg_ParseTuple(args, "|n", &bufsize)) { - return NULL; - } - - buf = PyBytes_FromStringAndSize(NULL, bufsize); - if (!buf) { - return NULL; - } - - status = encoder->encode( - encoder->im, &encoder->state, (UINT8 *)PyBytes_AsString(buf), bufsize); - - /* adjust string length to avoid slicing in encoder */ - if (_PyBytes_Resize(&buf, (status > 0) ? status : 0) < 0) { - return NULL; - } - - result = Py_BuildValue("iiO", status, encoder->state.errcode, buf); - - Py_DECREF(buf); /* must release buffer!!! */ - - return result; -} - -static PyObject * -_encode_to_pyfd(ImagingEncoderObject *encoder) { - PyObject *result; - int status; - - if (!encoder->pushes_fd) { - // UNDONE, appropriate errcode??? - result = Py_BuildValue("ii", 0, IMAGING_CODEC_CONFIG); - return result; - } - - status = encoder->encode(encoder->im, &encoder->state, (UINT8 *)NULL, 0); - - result = Py_BuildValue("ii", status, encoder->state.errcode); - - return result; -} - -static PyObject * -_encode_to_file(ImagingEncoderObject *encoder, PyObject *args) { - UINT8 *buf; - int status; - ImagingSectionCookie cookie; - - /* Encode to a file handle */ - - Py_ssize_t fh; - Py_ssize_t bufsize = 16384; - - if (!PyArg_ParseTuple(args, "n|n", &fh, &bufsize)) { - return NULL; - } - - /* Allocate an encoder buffer */ - /* malloc check ok, either constant int, or checked by PyArg_ParseTuple */ - buf = (UINT8 *)malloc(bufsize); - if (!buf) { - return ImagingError_MemoryError(); - } - - ImagingSectionEnter(&cookie); - - do { - /* This replaces the inner loop in the ImageFile _save - function. */ - - status = encoder->encode(encoder->im, &encoder->state, buf, bufsize); - - if (status > 0) { - if (write(fh, buf, status) < 0) { - ImagingSectionLeave(&cookie); - free(buf); - return PyErr_SetFromErrno(PyExc_OSError); - } - } - - } while (encoder->state.errcode == 0); - - ImagingSectionLeave(&cookie); - - free(buf); - - return Py_BuildValue("i", encoder->state.errcode); -} - -extern Imaging -PyImaging_AsImaging(PyObject *op); - -static PyObject * -_setimage(ImagingEncoderObject *encoder, PyObject *args) { - PyObject *op; - Imaging im; - ImagingCodecState state; - Py_ssize_t x0, y0, x1, y1; - - /* Define where image data should be stored */ - - x0 = y0 = x1 = y1 = 0; - - /* FIXME: should publish the ImagingType descriptor */ - if (!PyArg_ParseTuple(args, "O|(nnnn)", &op, &x0, &y0, &x1, &y1)) { - return NULL; - } - im = PyImaging_AsImaging(op); - if (!im) { - return NULL; - } - - encoder->im = im; - - state = &encoder->state; - - if (x0 == 0 && x1 == 0) { - state->xsize = im->xsize; - state->ysize = im->ysize; - } else { - state->xoff = x0; - state->yoff = y0; - state->xsize = x1 - x0; - state->ysize = y1 - y0; - } - - if (state->xsize <= 0 || state->xsize + state->xoff > im->xsize || - state->ysize <= 0 || state->ysize + state->yoff > im->ysize) { - PyErr_SetString(PyExc_SystemError, "tile cannot extend outside image"); - return NULL; - } - - /* Allocate memory buffer (if bits field is set) */ - if (state->bits > 0) { - if (state->xsize > ((INT_MAX / state->bits) - 7)) { - return ImagingError_MemoryError(); - } - state->bytes = (state->bits * state->xsize + 7) / 8; - /* malloc check ok, overflow checked above */ - state->buffer = (UINT8 *)calloc(1, state->bytes); - if (!state->buffer) { - return ImagingError_MemoryError(); - } - } - - /* Keep a reference to the image object, to make sure it doesn't - go away before we do */ - Py_INCREF(op); - Py_XDECREF(encoder->lock); - encoder->lock = op; - - Py_INCREF(Py_None); - return Py_None; -} - -static PyObject * -_setfd(ImagingEncoderObject *encoder, PyObject *args) { - PyObject *fd; - ImagingCodecState state; - - if (!PyArg_ParseTuple(args, "O", &fd)) { - return NULL; - } - - state = &encoder->state; - - Py_XINCREF(fd); - state->fd = fd; - - Py_INCREF(Py_None); - return Py_None; -} - -static PyObject * -_get_pushes_fd(ImagingEncoderObject *encoder, void *closure) { - return PyBool_FromLong(encoder->pushes_fd); -} - -static struct PyMethodDef methods[] = { - {"encode", (PyCFunction)_encode, METH_VARARGS}, - {"cleanup", (PyCFunction)_encode_cleanup, METH_VARARGS}, - {"encode_to_file", (PyCFunction)_encode_to_file, METH_VARARGS}, - {"encode_to_pyfd", (PyCFunction)_encode_to_pyfd, METH_NOARGS}, - {"setimage", (PyCFunction)_setimage, METH_VARARGS}, - {"setfd", (PyCFunction)_setfd, METH_VARARGS}, - {NULL, NULL} /* sentinel */ -}; - -static struct PyGetSetDef getseters[] = { - {"pushes_fd", - (getter)_get_pushes_fd, - NULL, - "True if this decoder expects to push directly to self.fd", - NULL}, - {NULL, NULL, NULL, NULL, NULL} /* sentinel */ -}; - -static PyTypeObject ImagingEncoderType = { - PyVarObject_HEAD_INIT(NULL, 0) "ImagingEncoder", /*tp_name*/ - sizeof(ImagingEncoderObject), /*tp_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*/ -}; - -/* -------------------------------------------------------------------- */ - -int -get_packer(ImagingEncoderObject *encoder, const char *mode, const char *rawmode) { - int bits; - ImagingShuffler pack; - - pack = ImagingFindPacker(mode, rawmode, &bits); - if (!pack) { - Py_DECREF(encoder); - PyErr_Format(PyExc_ValueError, "No packer found from %s to %s", mode, rawmode); - return -1; - } - - encoder->state.shuffle = pack; - encoder->state.bits = bits; - - return 0; -} - -/* -------------------------------------------------------------------- */ -/* EPS */ -/* -------------------------------------------------------------------- */ - -PyObject * -PyImaging_EpsEncoderNew(PyObject *self, PyObject *args) { - ImagingEncoderObject *encoder; - - encoder = PyImaging_EncoderNew(0); - if (encoder == NULL) { - return NULL; - } - - encoder->encode = ImagingEpsEncode; - - return (PyObject *)encoder; -} - -/* -------------------------------------------------------------------- */ -/* GIF */ -/* -------------------------------------------------------------------- */ - -PyObject * -PyImaging_GifEncoderNew(PyObject *self, PyObject *args) { - ImagingEncoderObject *encoder; - - char *mode; - char *rawmode; - Py_ssize_t bits = 8; - Py_ssize_t interlace = 0; - if (!PyArg_ParseTuple(args, "ss|nn", &mode, &rawmode, &bits, &interlace)) { - return NULL; - } - - encoder = PyImaging_EncoderNew(sizeof(GIFENCODERSTATE)); - if (encoder == NULL) { - return NULL; - } - - if (get_packer(encoder, mode, rawmode) < 0) { - return NULL; - } - - encoder->encode = ImagingGifEncode; - - ((GIFENCODERSTATE *)encoder->state.context)->bits = bits; - ((GIFENCODERSTATE *)encoder->state.context)->interlace = interlace; - - return (PyObject *)encoder; -} - -/* -------------------------------------------------------------------- */ -/* PCX */ -/* -------------------------------------------------------------------- */ - -PyObject * -PyImaging_PcxEncoderNew(PyObject *self, PyObject *args) { - ImagingEncoderObject *encoder; - - char *mode; - char *rawmode; - Py_ssize_t bits = 8; - - if (!PyArg_ParseTuple(args, "ss|n", &mode, &rawmode, &bits)) { - return NULL; - } - - encoder = PyImaging_EncoderNew(0); - if (encoder == NULL) { - return NULL; - } - - if (get_packer(encoder, mode, rawmode) < 0) { - return NULL; - } - - encoder->encode = ImagingPcxEncode; - - return (PyObject *)encoder; -} - -/* -------------------------------------------------------------------- */ -/* RAW */ -/* -------------------------------------------------------------------- */ - -PyObject * -PyImaging_RawEncoderNew(PyObject *self, PyObject *args) { - ImagingEncoderObject *encoder; - - char *mode; - char *rawmode; - Py_ssize_t stride = 0; - Py_ssize_t ystep = 1; - - if (!PyArg_ParseTuple(args, "ss|nn", &mode, &rawmode, &stride, &ystep)) { - return NULL; - } - - encoder = PyImaging_EncoderNew(0); - if (encoder == NULL) { - return NULL; - } - - if (get_packer(encoder, mode, rawmode) < 0) { - return NULL; - } - - encoder->encode = ImagingRawEncode; - - encoder->state.ystep = ystep; - encoder->state.count = stride; - - return (PyObject *)encoder; -} - -/* -------------------------------------------------------------------- */ -/* TGA */ -/* -------------------------------------------------------------------- */ - -PyObject * -PyImaging_TgaRleEncoderNew(PyObject *self, PyObject *args) { - ImagingEncoderObject *encoder; - - char *mode; - char *rawmode; - Py_ssize_t ystep = 1; - - if (!PyArg_ParseTuple(args, "ss|n", &mode, &rawmode, &ystep)) { - return NULL; - } - - encoder = PyImaging_EncoderNew(0); - if (encoder == NULL) { - return NULL; - } - - if (get_packer(encoder, mode, rawmode) < 0) { - return NULL; - } - - encoder->encode = ImagingTgaRleEncode; - - encoder->state.ystep = ystep; - - return (PyObject *)encoder; -} - -/* -------------------------------------------------------------------- */ -/* XBM */ -/* -------------------------------------------------------------------- */ - -PyObject * -PyImaging_XbmEncoderNew(PyObject *self, PyObject *args) { - ImagingEncoderObject *encoder; - - encoder = PyImaging_EncoderNew(0); - if (encoder == NULL) { - return NULL; - } - - if (get_packer(encoder, "1", "1;R") < 0) { - return NULL; - } - - encoder->encode = ImagingXbmEncode; - - return (PyObject *)encoder; -} - -/* -------------------------------------------------------------------- */ -/* ZIP */ -/* -------------------------------------------------------------------- */ - -#ifdef HAVE_LIBZ - -#include "libImaging/ZipCodecs.h" - -PyObject * -PyImaging_ZipEncoderNew(PyObject *self, PyObject *args) { - ImagingEncoderObject *encoder; - - char *mode; - char *rawmode; - Py_ssize_t optimize = 0; - Py_ssize_t compress_level = -1; - Py_ssize_t compress_type = -1; - char *dictionary = NULL; - Py_ssize_t dictionary_size = 0; - if (!PyArg_ParseTuple( - args, - "ss|nnny#", - &mode, - &rawmode, - &optimize, - &compress_level, - &compress_type, - &dictionary, - &dictionary_size)) { - return NULL; - } - - /* Copy to avoid referencing Python's memory */ - if (dictionary && dictionary_size > 0) { - /* malloc check ok, size comes from PyArg_ParseTuple */ - char *p = malloc(dictionary_size); - if (!p) { - return ImagingError_MemoryError(); - } - memcpy(p, dictionary, dictionary_size); - dictionary = p; - } else { - dictionary = NULL; - } - - encoder = PyImaging_EncoderNew(sizeof(ZIPSTATE)); - if (encoder == NULL) { - free(dictionary); - return NULL; - } - - if (get_packer(encoder, mode, rawmode) < 0) { - free(dictionary); - return NULL; - } - - encoder->encode = ImagingZipEncode; - encoder->cleanup = ImagingZipEncodeCleanup; - - if (rawmode[0] == 'P') { - /* disable filtering */ - ((ZIPSTATE *)encoder->state.context)->mode = ZIP_PNG_PALETTE; - } - - ((ZIPSTATE *)encoder->state.context)->optimize = optimize; - ((ZIPSTATE *)encoder->state.context)->compress_level = compress_level; - ((ZIPSTATE *)encoder->state.context)->compress_type = compress_type; - ((ZIPSTATE *)encoder->state.context)->dictionary = dictionary; - ((ZIPSTATE *)encoder->state.context)->dictionary_size = dictionary_size; - - return (PyObject *)encoder; -} -#endif - -/* -------------------------------------------------------------------- */ -/* LibTiff */ -/* -------------------------------------------------------------------- */ - -#ifdef HAVE_LIBTIFF - -#include "libImaging/TiffDecode.h" - -#include - -PyObject * -PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { - ImagingEncoderObject *encoder; - - char *mode; - char *rawmode; - char *compname; - char *filename; - Py_ssize_t fp; - - PyObject *tags, *types; - PyObject *key, *value; - Py_ssize_t pos = 0; - int key_int, status, is_core_tag, is_var_length, num_core_tags, i; - TIFFDataType type = TIFF_NOTYPE; - // This list also exists in TiffTags.py - const int core_tags[] = {256, 257, 258, 259, 262, 263, 266, 269, 274, - 277, 278, 280, 281, 340, 341, 282, 283, 284, - 286, 287, 296, 297, 320, 321, 338, 32995, 32998, - 32996, 339, 32997, 330, 531, 530, 65537, 301, 532}; - - Py_ssize_t tags_size; - PyObject *item; - - if (!PyArg_ParseTuple( - args, - "sssnsOO", - &mode, - &rawmode, - &compname, - &fp, - &filename, - &tags, - &types)) { - return NULL; - } - - if (!PyList_Check(tags)) { - PyErr_SetString(PyExc_ValueError, "Invalid tags list"); - return NULL; - } else { - tags_size = PyList_Size(tags); - TRACE(("tags size: %d\n", (int)tags_size)); - for (pos = 0; pos < tags_size; pos++) { - item = PyList_GetItem(tags, pos); - if (!PyTuple_Check(item) || PyTuple_Size(item) != 2) { - PyErr_SetString(PyExc_ValueError, "Invalid tags list"); - return NULL; - } - } - pos = 0; - } - if (!PyDict_Check(types)) { - PyErr_SetString(PyExc_ValueError, "Invalid types dictionary"); - return NULL; - } - - TRACE(("new tiff encoder %s fp: %d, filename: %s \n", compname, fp, filename)); - - encoder = PyImaging_EncoderNew(sizeof(TIFFSTATE)); - if (encoder == NULL) { - return NULL; - } - - if (get_packer(encoder, mode, rawmode) < 0) { - return NULL; - } - - if (!ImagingLibTiffEncodeInit(&encoder->state, filename, fp)) { - Py_DECREF(encoder); - PyErr_SetString(PyExc_RuntimeError, "tiff codec initialization failed"); - return NULL; - } - - num_core_tags = sizeof(core_tags) / sizeof(int); - for (pos = 0; pos < tags_size; pos++) { - item = PyList_GetItem(tags, pos); - // We already checked that tags is a 2-tuple list. - key = PyTuple_GetItem(item, 0); - key_int = (int)PyLong_AsLong(key); - value = PyTuple_GetItem(item, 1); - status = 0; - is_core_tag = 0; - is_var_length = 0; - type = TIFF_NOTYPE; - - for (i = 0; i < num_core_tags; i++) { - if (core_tags[i] == key_int) { - is_core_tag = 1; - break; - } - } - - if (!is_core_tag) { - PyObject *tag_type = PyDict_GetItem(types, key); - if (tag_type) { - int type_int = PyLong_AsLong(tag_type); - if (type_int >= TIFF_BYTE && type_int <= TIFF_DOUBLE) { - type = (TIFFDataType)type_int; - } - } - } - - if (type == TIFF_NOTYPE) { - // Autodetect type. Types should not be changed for backwards - // compatibility. - if (PyLong_Check(value)) { - type = TIFF_LONG; - } else if (PyFloat_Check(value)) { - type = TIFF_DOUBLE; - } else if (PyBytes_Check(value)) { - type = TIFF_ASCII; - } - } - - if (PyTuple_Check(value)) { - Py_ssize_t len; - len = PyTuple_Size(value); - - is_var_length = 1; - - if (!len) { - continue; - } - - if (type == TIFF_NOTYPE) { - // Autodetect type based on first item. Types should not be - // changed for backwards compatibility. - if (PyLong_Check(PyTuple_GetItem(value, 0))) { - type = TIFF_LONG; - } else if (PyFloat_Check(PyTuple_GetItem(value, 0))) { - type = TIFF_FLOAT; - } - } - } - - if (!is_core_tag) { - // Register field for non core tags. - if (type == TIFF_BYTE) { - is_var_length = 1; - } - if (ImagingLibTiffMergeFieldInfo( - &encoder->state, type, key_int, is_var_length)) { - continue; - } - } - - if (type == TIFF_BYTE || type == TIFF_UNDEFINED) { - status = ImagingLibTiffSetField( - &encoder->state, - (ttag_t)key_int, - PyBytes_Size(value), - PyBytes_AsString(value)); - } else if (is_var_length) { - Py_ssize_t len, i; - TRACE(("Setting from Tuple: %d \n", key_int)); - len = PyTuple_Size(value); - - if (key_int == TIFFTAG_COLORMAP) { - int stride = 256; - if (len != 768) { - PyErr_SetString( - PyExc_ValueError, "Requiring 768 items for Colormap"); - return NULL; - } - UINT16 *av; - /* malloc check ok, calloc checks for overflow */ - av = calloc(len, sizeof(UINT16)); - if (av) { - for (i = 0; i < len; i++) { - av[i] = (UINT16)PyLong_AsLong(PyTuple_GetItem(value, i)); - } - status = ImagingLibTiffSetField( - &encoder->state, - (ttag_t)key_int, - av, - av + stride, - av + stride * 2); - free(av); - } - } else if (key_int == TIFFTAG_YCBCRSUBSAMPLING) { - status = ImagingLibTiffSetField( - &encoder->state, - (ttag_t)key_int, - (UINT16)PyLong_AsLong(PyTuple_GetItem(value, 0)), - (UINT16)PyLong_AsLong(PyTuple_GetItem(value, 1))); - } else if (type == TIFF_SHORT) { - UINT16 *av; - /* malloc check ok, calloc checks for overflow */ - av = calloc(len, sizeof(UINT16)); - if (av) { - for (i = 0; i < len; i++) { - av[i] = (UINT16)PyLong_AsLong(PyTuple_GetItem(value, i)); - } - status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, len, av); - free(av); - } - } else if (type == TIFF_LONG) { - UINT32 *av; - /* malloc check ok, calloc checks for overflow */ - av = calloc(len, sizeof(UINT32)); - if (av) { - for (i = 0; i < len; i++) { - av[i] = (UINT32)PyLong_AsLong(PyTuple_GetItem(value, i)); - } - status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, len, av); - free(av); - } - } else if (type == TIFF_SBYTE) { - INT8 *av; - /* malloc check ok, calloc checks for overflow */ - av = calloc(len, sizeof(INT8)); - if (av) { - for (i = 0; i < len; i++) { - av[i] = (INT8)PyLong_AsLong(PyTuple_GetItem(value, i)); - } - status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, len, av); - free(av); - } - } else if (type == TIFF_SSHORT) { - INT16 *av; - /* malloc check ok, calloc checks for overflow */ - av = calloc(len, sizeof(INT16)); - if (av) { - for (i = 0; i < len; i++) { - av[i] = (INT16)PyLong_AsLong(PyTuple_GetItem(value, i)); - } - status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, len, av); - free(av); - } - } else if (type == TIFF_SLONG) { - INT32 *av; - /* malloc check ok, calloc checks for overflow */ - av = calloc(len, sizeof(INT32)); - if (av) { - for (i = 0; i < len; i++) { - av[i] = (INT32)PyLong_AsLong(PyTuple_GetItem(value, i)); - } - status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, len, av); - free(av); - } - } else if (type == TIFF_FLOAT) { - FLOAT32 *av; - /* malloc check ok, calloc checks for overflow */ - av = calloc(len, sizeof(FLOAT32)); - if (av) { - for (i = 0; i < len; i++) { - av[i] = (FLOAT32)PyFloat_AsDouble(PyTuple_GetItem(value, i)); - } - status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, len, av); - free(av); - } - } else if (type == TIFF_DOUBLE) { - FLOAT64 *av; - /* malloc check ok, calloc checks for overflow */ - av = calloc(len, sizeof(FLOAT64)); - if (av) { - for (i = 0; i < len; i++) { - av[i] = PyFloat_AsDouble(PyTuple_GetItem(value, i)); - } - status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, len, av); - free(av); - } - } - } else { - if (type == TIFF_SHORT) { - status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, (UINT16)PyLong_AsLong(value)); - } else if (type == TIFF_LONG) { - status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, PyLong_AsLongLong(value)); - } else if (type == TIFF_SSHORT) { - status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, (INT16)PyLong_AsLong(value)); - } else if (type == TIFF_SLONG) { - status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, (INT32)PyLong_AsLong(value)); - } else if (type == TIFF_FLOAT) { - status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, (FLOAT32)PyFloat_AsDouble(value)); - } else if (type == TIFF_DOUBLE) { - status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, (FLOAT64)PyFloat_AsDouble(value)); - } else if (type == TIFF_SBYTE) { - status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, (INT8)PyLong_AsLong(value)); - } else if (type == TIFF_ASCII) { - status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, PyBytes_AsString(value)); - } else if (type == TIFF_RATIONAL) { - status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, (FLOAT64)PyFloat_AsDouble(value)); - } else { - TRACE( - ("Unhandled type for key %d : %s \n", - key_int, - PyBytes_AsString(PyObject_Str(value)))); - } - } - if (!status) { - TRACE(("Error setting Field\n")); - Py_DECREF(encoder); - PyErr_SetString(PyExc_RuntimeError, "Error setting from dictionary"); - return NULL; - } - } - - encoder->encode = ImagingLibTiffEncode; - - return (PyObject *)encoder; -} - -#endif - -/* -------------------------------------------------------------------- */ -/* JPEG */ -/* -------------------------------------------------------------------- */ - -#ifdef HAVE_LIBJPEG - -/* We better define this encoder last in this file, so the following - undef's won't mess things up for the Imaging library proper. */ - -#undef HAVE_PROTOTYPES -#undef HAVE_STDDEF_H -#undef HAVE_STDLIB_H -#undef UINT8 -#undef UINT16 -#undef UINT32 -#undef INT8 -#undef INT16 -#undef INT32 - -#include "libImaging/Jpeg.h" - -static unsigned int * -get_qtables_arrays(PyObject *qtables, int *qtablesLen) { - PyObject *tables; - PyObject *table; - PyObject *table_data; - int i, j, num_tables; - unsigned int *qarrays; - - if ((qtables == NULL) || (qtables == Py_None)) { - return NULL; - } - - if (!PySequence_Check(qtables)) { - PyErr_SetString(PyExc_ValueError, "Invalid quantization tables"); - return NULL; - } - - tables = PySequence_Fast(qtables, "expected a sequence"); - num_tables = PySequence_Size(qtables); - if (num_tables < 1 || num_tables > NUM_QUANT_TBLS) { - PyErr_SetString( - PyExc_ValueError, - "Not a valid number of quantization tables. Should be between 1 and 4."); - Py_DECREF(tables); - return NULL; - } - /* malloc check ok, num_tables <4, DCTSIZE2 == 64 from jpeglib.h */ - qarrays = (unsigned int *)malloc(num_tables * DCTSIZE2 * sizeof(unsigned int)); - if (!qarrays) { - Py_DECREF(tables); - return ImagingError_MemoryError(); - } - for (i = 0; i < num_tables; i++) { - table = PySequence_Fast_GET_ITEM(tables, i); - if (!PySequence_Check(table)) { - PyErr_SetString(PyExc_ValueError, "Invalid quantization tables"); - goto JPEG_QTABLES_ERR; - } - if (PySequence_Size(table) != DCTSIZE2) { - PyErr_SetString(PyExc_ValueError, "Invalid quantization table size"); - goto JPEG_QTABLES_ERR; - } - table_data = PySequence_Fast(table, "expected a sequence"); - for (j = 0; j < DCTSIZE2; j++) { - qarrays[i * DCTSIZE2 + j] = - PyLong_AS_LONG(PySequence_Fast_GET_ITEM(table_data, j)); - } - Py_DECREF(table_data); - } - - *qtablesLen = num_tables; - -JPEG_QTABLES_ERR: - Py_DECREF(tables); // Run on both error and not error - if (PyErr_Occurred()) { - free(qarrays); - qarrays = NULL; - return NULL; - } - - return qarrays; -} - -PyObject * -PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { - ImagingEncoderObject *encoder; - - char *mode; - char *rawmode; - Py_ssize_t quality = 0; - Py_ssize_t progressive = 0; - Py_ssize_t smooth = 0; - Py_ssize_t optimize = 0; - int keep_rgb = 0; - Py_ssize_t streamtype = 0; /* 0=interchange, 1=tables only, 2=image only */ - Py_ssize_t xdpi = 0, ydpi = 0; - Py_ssize_t subsampling = -1; /* -1=default, 0=none, 1=medium, 2=high */ - Py_ssize_t restart_marker_blocks = 0; - Py_ssize_t restart_marker_rows = 0; - PyObject *qtables = NULL; - unsigned int *qarrays = NULL; - int qtablesLen = 0; - char *comment = NULL; - Py_ssize_t comment_size; - char *extra = NULL; - Py_ssize_t extra_size; - char *rawExif = NULL; - Py_ssize_t rawExifLen = 0; - - if (!PyArg_ParseTuple( - args, - "ss|nnnnpnnnnnnOz#y#y#", - &mode, - &rawmode, - &quality, - &progressive, - &smooth, - &optimize, - &keep_rgb, - &streamtype, - &xdpi, - &ydpi, - &subsampling, - &restart_marker_blocks, - &restart_marker_rows, - &qtables, - &comment, - &comment_size, - &extra, - &extra_size, - &rawExif, - &rawExifLen)) { - return NULL; - } - - encoder = PyImaging_EncoderNew(sizeof(JPEGENCODERSTATE)); - if (encoder == NULL) { - return NULL; - } - - // libjpeg-turbo supports different output formats. - // We are choosing Pillow's native format (3 color bytes + 1 padding) - // to avoid extra conversion in Pack.c. - if (ImagingJpegUseJCSExtensions() && strcmp(rawmode, "RGB") == 0) { - rawmode = "RGBX"; - } - - if (get_packer(encoder, mode, rawmode) < 0) { - return NULL; - } - - // Freed in JpegEncode, Case 6 - qarrays = get_qtables_arrays(qtables, &qtablesLen); - - if (comment && comment_size > 0) { - /* malloc check ok, length is from python parsearg */ - char *p = malloc(comment_size); // Freed in JpegEncode, Case 6 - if (!p) { - return ImagingError_MemoryError(); - } - memcpy(p, comment, comment_size); - comment = p; - } else { - comment = NULL; - } - - if (extra && extra_size > 0) { - /* malloc check ok, length is from python parsearg */ - char *p = malloc(extra_size); // Freed in JpegEncode, Case 6 - if (!p) { - if (comment) { - free(comment); - } - return ImagingError_MemoryError(); - } - memcpy(p, extra, extra_size); - extra = p; - } else { - extra = NULL; - } - - if (rawExif && rawExifLen > 0) { - /* malloc check ok, length is from python parsearg */ - char *pp = malloc(rawExifLen); // Freed in JpegEncode, Case 6 - if (!pp) { - if (comment) { - free(comment); - } - if (extra) { - free(extra); - } - return ImagingError_MemoryError(); - } - memcpy(pp, rawExif, rawExifLen); - rawExif = pp; - } else { - rawExif = NULL; - } - - encoder->encode = ImagingJpegEncode; - - strncpy(((JPEGENCODERSTATE *)encoder->state.context)->rawmode, rawmode, 8); - - ((JPEGENCODERSTATE *)encoder->state.context)->keep_rgb = keep_rgb; - ((JPEGENCODERSTATE *)encoder->state.context)->quality = quality; - ((JPEGENCODERSTATE *)encoder->state.context)->qtables = qarrays; - ((JPEGENCODERSTATE *)encoder->state.context)->qtablesLen = qtablesLen; - ((JPEGENCODERSTATE *)encoder->state.context)->subsampling = subsampling; - ((JPEGENCODERSTATE *)encoder->state.context)->progressive = progressive; - ((JPEGENCODERSTATE *)encoder->state.context)->smooth = smooth; - ((JPEGENCODERSTATE *)encoder->state.context)->optimize = optimize; - ((JPEGENCODERSTATE *)encoder->state.context)->streamtype = streamtype; - ((JPEGENCODERSTATE *)encoder->state.context)->xdpi = xdpi; - ((JPEGENCODERSTATE *)encoder->state.context)->ydpi = ydpi; - ((JPEGENCODERSTATE *)encoder->state.context)->restart_marker_blocks = - restart_marker_blocks; - ((JPEGENCODERSTATE *)encoder->state.context)->restart_marker_rows = - restart_marker_rows; - ((JPEGENCODERSTATE *)encoder->state.context)->comment = comment; - ((JPEGENCODERSTATE *)encoder->state.context)->comment_size = comment_size; - ((JPEGENCODERSTATE *)encoder->state.context)->extra = extra; - ((JPEGENCODERSTATE *)encoder->state.context)->extra_size = extra_size; - ((JPEGENCODERSTATE *)encoder->state.context)->rawExif = rawExif; - ((JPEGENCODERSTATE *)encoder->state.context)->rawExifLen = rawExifLen; - - return (PyObject *)encoder; -} - -#endif - -/* -------------------------------------------------------------------- */ -/* JPEG 2000 */ -/* -------------------------------------------------------------------- */ - -#ifdef HAVE_OPENJPEG - -#include "libImaging/Jpeg2K.h" - -static void -j2k_decode_coord_tuple(PyObject *tuple, int *x, int *y) { - *x = *y = 0; - - if (tuple && PyTuple_Check(tuple) && PyTuple_GET_SIZE(tuple) == 2) { - *x = (int)PyLong_AsLong(PyTuple_GET_ITEM(tuple, 0)); - *y = (int)PyLong_AsLong(PyTuple_GET_ITEM(tuple, 1)); - - if (*x < 0) { - *x = 0; - } - if (*y < 0) { - *y = 0; - } - } -} - -PyObject * -PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { - ImagingEncoderObject *encoder; - JPEG2KENCODESTATE *context; - - char *mode; - char *format; - OPJ_CODEC_FORMAT codec_format; - PyObject *offset = NULL, *tile_offset = NULL, *tile_size = NULL; - char *quality_mode = "rates"; - PyObject *quality_layers = NULL; - Py_ssize_t num_resolutions = 0; - PyObject *cblk_size = NULL, *precinct_size = NULL; - PyObject *irreversible = NULL; - char *progression = "LRCP"; - OPJ_PROG_ORDER prog_order; - char *cinema_mode = "no"; - OPJ_CINEMA_MODE cine_mode; - char mct = 0; - int sgnd = 0; - Py_ssize_t fd = -1; - char *comment; - Py_ssize_t comment_size; - int plt = 0; - - if (!PyArg_ParseTuple( - args, - "ss|OOOsOnOOOssbbnz#p", - &mode, - &format, - &offset, - &tile_offset, - &tile_size, - &quality_mode, - &quality_layers, - &num_resolutions, - &cblk_size, - &precinct_size, - &irreversible, - &progression, - &cinema_mode, - &mct, - &sgnd, - &fd, - &comment, - &comment_size, - &plt)) { - return NULL; - } - - if (strcmp(format, "j2k") == 0) { - codec_format = OPJ_CODEC_J2K; - } else if (strcmp(format, "jpt") == 0) { - codec_format = OPJ_CODEC_JPT; - } else if (strcmp(format, "jp2") == 0) { - codec_format = OPJ_CODEC_JP2; - } else { - return NULL; - } - - if (strcmp(progression, "LRCP") == 0) { - prog_order = OPJ_LRCP; - } else if (strcmp(progression, "RLCP") == 0) { - prog_order = OPJ_RLCP; - } else if (strcmp(progression, "RPCL") == 0) { - prog_order = OPJ_RPCL; - } else if (strcmp(progression, "PCRL") == 0) { - prog_order = OPJ_PCRL; - } else if (strcmp(progression, "CPRL") == 0) { - prog_order = OPJ_CPRL; - } else { - return NULL; - } - - if (strcmp(cinema_mode, "no") == 0) { - cine_mode = OPJ_OFF; - } else if (strcmp(cinema_mode, "cinema2k-24") == 0) { - cine_mode = OPJ_CINEMA2K_24; - } else if (strcmp(cinema_mode, "cinema2k-48") == 0) { - cine_mode = OPJ_CINEMA2K_48; - } else if (strcmp(cinema_mode, "cinema4k-24") == 0) { - cine_mode = OPJ_CINEMA4K_24; - } else { - return NULL; - } - - encoder = PyImaging_EncoderNew(sizeof(JPEG2KENCODESTATE)); - if (!encoder) { - return NULL; - } - - encoder->encode = ImagingJpeg2KEncode; - encoder->cleanup = ImagingJpeg2KEncodeCleanup; - encoder->pushes_fd = 1; - - context = (JPEG2KENCODESTATE *)encoder->state.context; - - context->fd = fd; - context->format = codec_format; - context->offset_x = context->offset_y = 0; - - j2k_decode_coord_tuple(offset, &context->offset_x, &context->offset_y); - j2k_decode_coord_tuple( - tile_offset, &context->tile_offset_x, &context->tile_offset_y); - j2k_decode_coord_tuple(tile_size, &context->tile_size_x, &context->tile_size_y); - - /* Error on illegal tile offsets */ - if (context->tile_size_x && context->tile_size_y) { - if (context->tile_offset_x <= context->offset_x - context->tile_size_x || - context->tile_offset_y <= context->offset_y - context->tile_size_y) { - PyErr_SetString( - PyExc_ValueError, - "JPEG 2000 tile offset too small; top left tile must " - "intersect image area"); - Py_DECREF(encoder); - return NULL; - } - - if (context->tile_offset_x > context->offset_x || - context->tile_offset_y > context->offset_y) { - PyErr_SetString( - PyExc_ValueError, - "JPEG 2000 tile offset too large to cover image area"); - Py_DECREF(encoder); - return NULL; - } - } - - if (comment && comment_size > 0) { - /* Size is stored as as an uint16, subtract 4 bytes for the header */ - if (comment_size >= 65532) { - PyErr_SetString(PyExc_ValueError, "JPEG 2000 comment is too long"); - Py_DECREF(encoder); - return NULL; - } - - char *p = malloc(comment_size + 1); - if (!p) { - Py_DECREF(encoder); - return ImagingError_MemoryError(); - } - memcpy(p, comment, comment_size); - p[comment_size] = '\0'; - context->comment = p; - } - - if (quality_layers && PySequence_Check(quality_layers)) { - context->quality_is_in_db = strcmp(quality_mode, "dB") == 0; - context->quality_layers = quality_layers; - Py_INCREF(quality_layers); - } - - context->num_resolutions = num_resolutions; - - j2k_decode_coord_tuple(cblk_size, &context->cblk_width, &context->cblk_height); - j2k_decode_coord_tuple( - precinct_size, &context->precinct_width, &context->precinct_height); - - context->irreversible = PyObject_IsTrue(irreversible); - context->progression = prog_order; - context->cinema_mode = cine_mode; - context->mct = mct; - context->sgnd = sgnd; - context->plt = plt; - - return (PyObject *)encoder; -} - -#endif - -/* - * Local Variables: - * c-basic-offset: 4 - * End: - * - */ +/* + * The Python Imaging Library. + * + * standard encoder interfaces for the Imaging library + * + * History: + * 1996-04-19 fl Based on decoders.c + * 1996-05-12 fl Compile cleanly as C++ + * 1996-12-30 fl Plugged potential memory leak for tiled images + * 1997-01-03 fl Added GIF encoder + * 1997-01-05 fl Plugged encoder buffer leaks + * 1997-01-11 fl Added encode_to_file method + * 1998-03-09 fl Added mode/rawmode argument to encoders + * 1998-07-09 fl Added interlace argument to GIF encoder + * 1999-02-07 fl Added PCX encoder + * + * Copyright (c) 1997-2001 by Secret Labs AB + * Copyright (c) 1996-1997 by Fredrik Lundh + * + * See the README file for information on usage and redistribution. + */ + +/* FIXME: make these pluggable! */ + +#define PY_SSIZE_T_CLEAN +#include "Python.h" + +#include "libImaging/Imaging.h" +#include "libImaging/Gif.h" + +#ifdef HAVE_UNISTD_H +#include /* write */ +#endif + +/* -------------------------------------------------------------------- */ +/* Common */ +/* -------------------------------------------------------------------- */ + +typedef struct { + PyObject_HEAD int (*encode)( + Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); + int (*cleanup)(ImagingCodecState state); + struct ImagingCodecStateInstance state; + Imaging im; + PyObject *lock; + int pushes_fd; +} ImagingEncoderObject; + +static PyTypeObject ImagingEncoderType; + +static ImagingEncoderObject * +PyImaging_EncoderNew(int contextsize) { + ImagingEncoderObject *encoder; + void *context; + + if (PyType_Ready(&ImagingEncoderType) < 0) { + return NULL; + } + + encoder = PyObject_New(ImagingEncoderObject, &ImagingEncoderType); + if (encoder == NULL) { + return NULL; + } + + /* Clear the encoder state */ + memset(&encoder->state, 0, sizeof(encoder->state)); + + /* Allocate encoder context */ + if (contextsize > 0) { + context = (void *)calloc(1, contextsize); + if (!context) { + Py_DECREF(encoder); + (void)ImagingError_MemoryError(); + return NULL; + } + } else { + context = 0; + } + + /* Initialize encoder context */ + encoder->state.context = context; + + /* Most encoders don't need this */ + encoder->cleanup = NULL; + + /* Target image */ + encoder->lock = NULL; + encoder->im = NULL; + encoder->pushes_fd = 0; + + return encoder; +} + +static void +_dealloc(ImagingEncoderObject *encoder) { + if (encoder->cleanup) { + encoder->cleanup(&encoder->state); + } + free(encoder->state.buffer); + free(encoder->state.context); + Py_XDECREF(encoder->lock); + Py_XDECREF(encoder->state.fd); + PyObject_Del(encoder); +} + +static PyObject * +_encode_cleanup(ImagingEncoderObject *encoder, PyObject *args) { + int status = 0; + + if (encoder->cleanup) { + status = encoder->cleanup(&encoder->state); + } + + return Py_BuildValue("i", status); +} + +static PyObject * +_encode(ImagingEncoderObject *encoder, PyObject *args) { + PyObject *buf; + PyObject *result; + int status; + + /* Encode to a Python string (allocated by this method) */ + + Py_ssize_t bufsize = 16384; + + if (!PyArg_ParseTuple(args, "|n", &bufsize)) { + return NULL; + } + + buf = PyBytes_FromStringAndSize(NULL, bufsize); + if (!buf) { + return NULL; + } + + status = encoder->encode( + encoder->im, &encoder->state, (UINT8 *)PyBytes_AsString(buf), bufsize); + + /* adjust string length to avoid slicing in encoder */ + if (_PyBytes_Resize(&buf, (status > 0) ? status : 0) < 0) { + return NULL; + } + + result = Py_BuildValue("iiO", status, encoder->state.errcode, buf); + + Py_DECREF(buf); /* must release buffer!!! */ + + return result; +} + +static PyObject * +_encode_to_pyfd(ImagingEncoderObject *encoder) { + PyObject *result; + int status; + + if (!encoder->pushes_fd) { + // UNDONE, appropriate errcode??? + result = Py_BuildValue("ii", 0, IMAGING_CODEC_CONFIG); + return result; + } + + status = encoder->encode(encoder->im, &encoder->state, (UINT8 *)NULL, 0); + + result = Py_BuildValue("ii", status, encoder->state.errcode); + + return result; +} + +static PyObject * +_encode_to_file(ImagingEncoderObject *encoder, PyObject *args) { + UINT8 *buf; + int status; + ImagingSectionCookie cookie; + + /* Encode to a file handle */ + + Py_ssize_t fh; + Py_ssize_t bufsize = 16384; + + if (!PyArg_ParseTuple(args, "n|n", &fh, &bufsize)) { + return NULL; + } + + /* Allocate an encoder buffer */ + /* malloc check ok, either constant int, or checked by PyArg_ParseTuple */ + buf = (UINT8 *)malloc(bufsize); + if (!buf) { + return ImagingError_MemoryError(); + } + + ImagingSectionEnter(&cookie); + + do { + /* This replaces the inner loop in the ImageFile _save + function. */ + + status = encoder->encode(encoder->im, &encoder->state, buf, bufsize); + + if (status > 0) { + if (write(fh, buf, status) < 0) { + ImagingSectionLeave(&cookie); + free(buf); + return PyErr_SetFromErrno(PyExc_OSError); + } + } + + } while (encoder->state.errcode == 0); + + ImagingSectionLeave(&cookie); + + free(buf); + + return Py_BuildValue("i", encoder->state.errcode); +} + +extern Imaging +PyImaging_AsImaging(PyObject *op); + +static PyObject * +_setimage(ImagingEncoderObject *encoder, PyObject *args) { + PyObject *op; + Imaging im; + ImagingCodecState state; + Py_ssize_t x0, y0, x1, y1; + + /* Define where image data should be stored */ + + x0 = y0 = x1 = y1 = 0; + + /* FIXME: should publish the ImagingType descriptor */ + if (!PyArg_ParseTuple(args, "O|(nnnn)", &op, &x0, &y0, &x1, &y1)) { + return NULL; + } + im = PyImaging_AsImaging(op); + if (!im) { + return NULL; + } + + encoder->im = im; + + state = &encoder->state; + + if (x0 == 0 && x1 == 0) { + state->xsize = im->xsize; + state->ysize = im->ysize; + } else { + state->xoff = x0; + state->yoff = y0; + state->xsize = x1 - x0; + state->ysize = y1 - y0; + } + + if (state->xsize <= 0 || state->xsize + state->xoff > im->xsize || + state->ysize <= 0 || state->ysize + state->yoff > im->ysize) { + PyErr_SetString(PyExc_SystemError, "tile cannot extend outside image"); + return NULL; + } + + /* Allocate memory buffer (if bits field is set) */ + if (state->bits > 0) { + if (state->xsize > ((INT_MAX / state->bits) - 7)) { + return ImagingError_MemoryError(); + } + state->bytes = (state->bits * state->xsize + 7) / 8; + /* malloc check ok, overflow checked above */ + state->buffer = (UINT8 *)calloc(1, state->bytes); + if (!state->buffer) { + return ImagingError_MemoryError(); + } + } + + /* Keep a reference to the image object, to make sure it doesn't + go away before we do */ + Py_INCREF(op); + Py_XDECREF(encoder->lock); + encoder->lock = op; + + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject * +_setfd(ImagingEncoderObject *encoder, PyObject *args) { + PyObject *fd; + ImagingCodecState state; + + if (!PyArg_ParseTuple(args, "O", &fd)) { + return NULL; + } + + state = &encoder->state; + + Py_XINCREF(fd); + state->fd = fd; + + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject * +_get_pushes_fd(ImagingEncoderObject *encoder, void *closure) { + return PyBool_FromLong(encoder->pushes_fd); +} + +static struct PyMethodDef methods[] = { + {"encode", (PyCFunction)_encode, METH_VARARGS}, + {"cleanup", (PyCFunction)_encode_cleanup, METH_VARARGS}, + {"encode_to_file", (PyCFunction)_encode_to_file, METH_VARARGS}, + {"encode_to_pyfd", (PyCFunction)_encode_to_pyfd, METH_NOARGS}, + {"setimage", (PyCFunction)_setimage, METH_VARARGS}, + {"setfd", (PyCFunction)_setfd, METH_VARARGS}, + {NULL, NULL} /* sentinel */ +}; + +static struct PyGetSetDef getseters[] = { + {"pushes_fd", + (getter)_get_pushes_fd, + NULL, + "True if this decoder expects to push directly to self.fd", + NULL}, + {NULL, NULL, NULL, NULL, NULL} /* sentinel */ +}; + +static PyTypeObject ImagingEncoderType = { + PyVarObject_HEAD_INIT(NULL, 0) "ImagingEncoder", /*tp_name*/ + sizeof(ImagingEncoderObject), /*tp_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*/ +}; + +/* -------------------------------------------------------------------- */ + +int +get_packer(ImagingEncoderObject *encoder, const char *mode, const char *rawmode) { + int bits; + ImagingShuffler pack; + + pack = ImagingFindPacker(mode, rawmode, &bits); + if (!pack) { + Py_DECREF(encoder); + PyErr_Format(PyExc_ValueError, "No packer found from %s to %s", mode, rawmode); + return -1; + } + + encoder->state.shuffle = pack; + encoder->state.bits = bits; + + return 0; +} + +/* -------------------------------------------------------------------- */ +/* EPS */ +/* -------------------------------------------------------------------- */ + +PyObject * +PyImaging_EpsEncoderNew(PyObject *self, PyObject *args) { + ImagingEncoderObject *encoder; + + encoder = PyImaging_EncoderNew(0); + if (encoder == NULL) { + return NULL; + } + + encoder->encode = ImagingEpsEncode; + + return (PyObject *)encoder; +} + +/* -------------------------------------------------------------------- */ +/* GIF */ +/* -------------------------------------------------------------------- */ + +PyObject * +PyImaging_GifEncoderNew(PyObject *self, PyObject *args) { + ImagingEncoderObject *encoder; + + char *mode; + char *rawmode; + Py_ssize_t bits = 8; + Py_ssize_t interlace = 0; + if (!PyArg_ParseTuple(args, "ss|nn", &mode, &rawmode, &bits, &interlace)) { + return NULL; + } + + encoder = PyImaging_EncoderNew(sizeof(GIFENCODERSTATE)); + if (encoder == NULL) { + return NULL; + } + + if (get_packer(encoder, mode, rawmode) < 0) { + return NULL; + } + + encoder->encode = ImagingGifEncode; + + ((GIFENCODERSTATE *)encoder->state.context)->bits = bits; + ((GIFENCODERSTATE *)encoder->state.context)->interlace = interlace; + + return (PyObject *)encoder; +} + +/* -------------------------------------------------------------------- */ +/* PCX */ +/* -------------------------------------------------------------------- */ + +PyObject * +PyImaging_PcxEncoderNew(PyObject *self, PyObject *args) { + ImagingEncoderObject *encoder; + + char *mode; + char *rawmode; + Py_ssize_t bits = 8; + + if (!PyArg_ParseTuple(args, "ss|n", &mode, &rawmode, &bits)) { + return NULL; + } + + encoder = PyImaging_EncoderNew(0); + if (encoder == NULL) { + return NULL; + } + + if (get_packer(encoder, mode, rawmode) < 0) { + return NULL; + } + + encoder->encode = ImagingPcxEncode; + + return (PyObject *)encoder; +} + +/* -------------------------------------------------------------------- */ +/* RAW */ +/* -------------------------------------------------------------------- */ + +PyObject * +PyImaging_RawEncoderNew(PyObject *self, PyObject *args) { + ImagingEncoderObject *encoder; + + char *mode; + char *rawmode; + Py_ssize_t stride = 0; + Py_ssize_t ystep = 1; + + if (!PyArg_ParseTuple(args, "ss|nn", &mode, &rawmode, &stride, &ystep)) { + return NULL; + } + + encoder = PyImaging_EncoderNew(0); + if (encoder == NULL) { + return NULL; + } + + if (get_packer(encoder, mode, rawmode) < 0) { + return NULL; + } + + encoder->encode = ImagingRawEncode; + + encoder->state.ystep = ystep; + encoder->state.count = stride; + + return (PyObject *)encoder; +} + +/* -------------------------------------------------------------------- */ +/* TGA */ +/* -------------------------------------------------------------------- */ + +PyObject * +PyImaging_TgaRleEncoderNew(PyObject *self, PyObject *args) { + ImagingEncoderObject *encoder; + + char *mode; + char *rawmode; + Py_ssize_t ystep = 1; + + if (!PyArg_ParseTuple(args, "ss|n", &mode, &rawmode, &ystep)) { + return NULL; + } + + encoder = PyImaging_EncoderNew(0); + if (encoder == NULL) { + return NULL; + } + + if (get_packer(encoder, mode, rawmode) < 0) { + return NULL; + } + + encoder->encode = ImagingTgaRleEncode; + + encoder->state.ystep = ystep; + + return (PyObject *)encoder; +} + +/* -------------------------------------------------------------------- */ +/* XBM */ +/* -------------------------------------------------------------------- */ + +PyObject * +PyImaging_XbmEncoderNew(PyObject *self, PyObject *args) { + ImagingEncoderObject *encoder; + + encoder = PyImaging_EncoderNew(0); + if (encoder == NULL) { + return NULL; + } + + if (get_packer(encoder, "1", "1;R") < 0) { + return NULL; + } + + encoder->encode = ImagingXbmEncode; + + return (PyObject *)encoder; +} + +/* -------------------------------------------------------------------- */ +/* ZIP */ +/* -------------------------------------------------------------------- */ + +#ifdef HAVE_LIBZ + +#include "libImaging/ZipCodecs.h" + +PyObject * +PyImaging_ZipEncoderNew(PyObject *self, PyObject *args) { + ImagingEncoderObject *encoder; + + char *mode; + char *rawmode; + Py_ssize_t optimize = 0; + Py_ssize_t compress_level = -1; + Py_ssize_t compress_type = -1; + char *dictionary = NULL; + Py_ssize_t dictionary_size = 0; + if (!PyArg_ParseTuple( + args, + "ss|nnny#", + &mode, + &rawmode, + &optimize, + &compress_level, + &compress_type, + &dictionary, + &dictionary_size)) { + return NULL; + } + + /* Copy to avoid referencing Python's memory */ + if (dictionary && dictionary_size > 0) { + /* malloc check ok, size comes from PyArg_ParseTuple */ + char *p = malloc(dictionary_size); + if (!p) { + return ImagingError_MemoryError(); + } + memcpy(p, dictionary, dictionary_size); + dictionary = p; + } else { + dictionary = NULL; + } + + encoder = PyImaging_EncoderNew(sizeof(ZIPSTATE)); + if (encoder == NULL) { + free(dictionary); + return NULL; + } + + if (get_packer(encoder, mode, rawmode) < 0) { + free(dictionary); + return NULL; + } + + encoder->encode = ImagingZipEncode; + encoder->cleanup = ImagingZipEncodeCleanup; + + if (rawmode[0] == 'P') { + /* disable filtering */ + ((ZIPSTATE *)encoder->state.context)->mode = ZIP_PNG_PALETTE; + } + + ((ZIPSTATE *)encoder->state.context)->optimize = optimize; + ((ZIPSTATE *)encoder->state.context)->compress_level = compress_level; + ((ZIPSTATE *)encoder->state.context)->compress_type = compress_type; + ((ZIPSTATE *)encoder->state.context)->dictionary = dictionary; + ((ZIPSTATE *)encoder->state.context)->dictionary_size = dictionary_size; + + return (PyObject *)encoder; +} +#endif + +/* -------------------------------------------------------------------- */ +/* LibTiff */ +/* -------------------------------------------------------------------- */ + +#ifdef HAVE_LIBTIFF + +#include "libImaging/TiffDecode.h" + +#include + +PyObject * +PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { + ImagingEncoderObject *encoder; + + char *mode; + char *rawmode; + char *compname; + char *filename; + Py_ssize_t fp; + + PyObject *tags, *types; + PyObject *key, *value; + Py_ssize_t pos = 0; + int key_int, status, is_core_tag, is_var_length, num_core_tags, i; + TIFFDataType type = TIFF_NOTYPE; + // This list also exists in TiffTags.py + const int core_tags[] = {256, 257, 258, 259, 262, 263, 266, 269, 274, + 277, 278, 280, 281, 340, 341, 282, 283, 284, + 286, 287, 296, 297, 320, 321, 338, 32995, 32998, + 32996, 339, 32997, 330, 531, 530, 65537, 301, 532}; + + Py_ssize_t tags_size; + PyObject *item; + + if (!PyArg_ParseTuple( + args, + "sssnsOO", + &mode, + &rawmode, + &compname, + &fp, + &filename, + &tags, + &types)) { + return NULL; + } + + if (!PyList_Check(tags)) { + PyErr_SetString(PyExc_ValueError, "Invalid tags list"); + return NULL; + } else { + tags_size = PyList_Size(tags); + TRACE(("tags size: %d\n", (int)tags_size)); + for (pos = 0; pos < tags_size; pos++) { + item = PyList_GetItem(tags, pos); + if (!PyTuple_Check(item) || PyTuple_Size(item) != 2) { + PyErr_SetString(PyExc_ValueError, "Invalid tags list"); + return NULL; + } + } + pos = 0; + } + if (!PyDict_Check(types)) { + PyErr_SetString(PyExc_ValueError, "Invalid types dictionary"); + return NULL; + } + + TRACE(("new tiff encoder %s fp: %d, filename: %s \n", compname, fp, filename)); + + encoder = PyImaging_EncoderNew(sizeof(TIFFSTATE)); + if (encoder == NULL) { + return NULL; + } + + if (get_packer(encoder, mode, rawmode) < 0) { + return NULL; + } + + if (!ImagingLibTiffEncodeInit(&encoder->state, filename, fp)) { + Py_DECREF(encoder); + PyErr_SetString(PyExc_RuntimeError, "tiff codec initialization failed"); + return NULL; + } + + num_core_tags = sizeof(core_tags) / sizeof(int); + for (pos = 0; pos < tags_size; pos++) { + item = PyList_GetItem(tags, pos); + // We already checked that tags is a 2-tuple list. + key = PyTuple_GetItem(item, 0); + key_int = (int)PyLong_AsLong(key); + value = PyTuple_GetItem(item, 1); + status = 0; + is_core_tag = 0; + is_var_length = 0; + type = TIFF_NOTYPE; + + for (i = 0; i < num_core_tags; i++) { + if (core_tags[i] == key_int) { + is_core_tag = 1; + break; + } + } + + if (!is_core_tag) { + PyObject *tag_type = PyDict_GetItem(types, key); + if (tag_type) { + int type_int = PyLong_AsLong(tag_type); + if (type_int >= TIFF_BYTE && type_int <= TIFF_DOUBLE) { + type = (TIFFDataType)type_int; + } + } + } + + if (type == TIFF_NOTYPE) { + // Autodetect type. Types should not be changed for backwards + // compatibility. + if (PyLong_Check(value)) { + type = TIFF_LONG; + } else if (PyFloat_Check(value)) { + type = TIFF_DOUBLE; + } else if (PyBytes_Check(value)) { + type = TIFF_ASCII; + } + } + + if (PyTuple_Check(value)) { + Py_ssize_t len; + len = PyTuple_Size(value); + + is_var_length = 1; + + if (!len) { + continue; + } + + if (type == TIFF_NOTYPE) { + // Autodetect type based on first item. Types should not be + // changed for backwards compatibility. + if (PyLong_Check(PyTuple_GetItem(value, 0))) { + type = TIFF_LONG; + } else if (PyFloat_Check(PyTuple_GetItem(value, 0))) { + type = TIFF_FLOAT; + } + } + } + + if (!is_core_tag) { + // Register field for non core tags. + if (type == TIFF_BYTE) { + is_var_length = 1; + } + if (ImagingLibTiffMergeFieldInfo( + &encoder->state, type, key_int, is_var_length)) { + continue; + } + } + + if (type == TIFF_BYTE || type == TIFF_UNDEFINED) { + status = ImagingLibTiffSetField( + &encoder->state, + (ttag_t)key_int, + PyBytes_Size(value), + PyBytes_AsString(value)); + } else if (is_var_length) { + Py_ssize_t len, i; + TRACE(("Setting from Tuple: %d \n", key_int)); + len = PyTuple_Size(value); + + if (key_int == TIFFTAG_COLORMAP) { + int stride = 256; + if (len != 768) { + PyErr_SetString( + PyExc_ValueError, "Requiring 768 items for Colormap"); + return NULL; + } + UINT16 *av; + /* malloc check ok, calloc checks for overflow */ + av = calloc(len, sizeof(UINT16)); + if (av) { + for (i = 0; i < len; i++) { + av[i] = (UINT16)PyLong_AsLong(PyTuple_GetItem(value, i)); + } + status = ImagingLibTiffSetField( + &encoder->state, + (ttag_t)key_int, + av, + av + stride, + av + stride * 2); + free(av); + } + } else if (key_int == TIFFTAG_YCBCRSUBSAMPLING) { + status = ImagingLibTiffSetField( + &encoder->state, + (ttag_t)key_int, + (UINT16)PyLong_AsLong(PyTuple_GetItem(value, 0)), + (UINT16)PyLong_AsLong(PyTuple_GetItem(value, 1))); + } else if (type == TIFF_SHORT) { + UINT16 *av; + /* malloc check ok, calloc checks for overflow */ + av = calloc(len, sizeof(UINT16)); + if (av) { + for (i = 0; i < len; i++) { + av[i] = (UINT16)PyLong_AsLong(PyTuple_GetItem(value, i)); + } + status = ImagingLibTiffSetField( + &encoder->state, (ttag_t)key_int, len, av); + free(av); + } + } else if (type == TIFF_LONG) { + UINT32 *av; + /* malloc check ok, calloc checks for overflow */ + av = calloc(len, sizeof(UINT32)); + if (av) { + for (i = 0; i < len; i++) { + av[i] = (UINT32)PyLong_AsLong(PyTuple_GetItem(value, i)); + } + status = ImagingLibTiffSetField( + &encoder->state, (ttag_t)key_int, len, av); + free(av); + } + } else if (type == TIFF_SBYTE) { + INT8 *av; + /* malloc check ok, calloc checks for overflow */ + av = calloc(len, sizeof(INT8)); + if (av) { + for (i = 0; i < len; i++) { + av[i] = (INT8)PyLong_AsLong(PyTuple_GetItem(value, i)); + } + status = ImagingLibTiffSetField( + &encoder->state, (ttag_t)key_int, len, av); + free(av); + } + } else if (type == TIFF_SSHORT) { + INT16 *av; + /* malloc check ok, calloc checks for overflow */ + av = calloc(len, sizeof(INT16)); + if (av) { + for (i = 0; i < len; i++) { + av[i] = (INT16)PyLong_AsLong(PyTuple_GetItem(value, i)); + } + status = ImagingLibTiffSetField( + &encoder->state, (ttag_t)key_int, len, av); + free(av); + } + } else if (type == TIFF_SLONG) { + INT32 *av; + /* malloc check ok, calloc checks for overflow */ + av = calloc(len, sizeof(INT32)); + if (av) { + for (i = 0; i < len; i++) { + av[i] = (INT32)PyLong_AsLong(PyTuple_GetItem(value, i)); + } + status = ImagingLibTiffSetField( + &encoder->state, (ttag_t)key_int, len, av); + free(av); + } + } else if (type == TIFF_FLOAT) { + FLOAT32 *av; + /* malloc check ok, calloc checks for overflow */ + av = calloc(len, sizeof(FLOAT32)); + if (av) { + for (i = 0; i < len; i++) { + av[i] = (FLOAT32)PyFloat_AsDouble(PyTuple_GetItem(value, i)); + } + status = ImagingLibTiffSetField( + &encoder->state, (ttag_t)key_int, len, av); + free(av); + } + } else if (type == TIFF_DOUBLE) { + FLOAT64 *av; + /* malloc check ok, calloc checks for overflow */ + av = calloc(len, sizeof(FLOAT64)); + if (av) { + for (i = 0; i < len; i++) { + av[i] = PyFloat_AsDouble(PyTuple_GetItem(value, i)); + } + status = ImagingLibTiffSetField( + &encoder->state, (ttag_t)key_int, len, av); + free(av); + } + } + } else { + if (type == TIFF_SHORT) { + status = ImagingLibTiffSetField( + &encoder->state, (ttag_t)key_int, (UINT16)PyLong_AsLong(value)); + } else if (type == TIFF_LONG) { + status = ImagingLibTiffSetField( + &encoder->state, (ttag_t)key_int, PyLong_AsLongLong(value)); + } else if (type == TIFF_SSHORT) { + status = ImagingLibTiffSetField( + &encoder->state, (ttag_t)key_int, (INT16)PyLong_AsLong(value)); + } else if (type == TIFF_SLONG) { + status = ImagingLibTiffSetField( + &encoder->state, (ttag_t)key_int, (INT32)PyLong_AsLong(value)); + } else if (type == TIFF_FLOAT) { + status = ImagingLibTiffSetField( + &encoder->state, (ttag_t)key_int, (FLOAT32)PyFloat_AsDouble(value)); + } else if (type == TIFF_DOUBLE) { + status = ImagingLibTiffSetField( + &encoder->state, (ttag_t)key_int, (FLOAT64)PyFloat_AsDouble(value)); + } else if (type == TIFF_SBYTE) { + status = ImagingLibTiffSetField( + &encoder->state, (ttag_t)key_int, (INT8)PyLong_AsLong(value)); + } else if (type == TIFF_ASCII) { + status = ImagingLibTiffSetField( + &encoder->state, (ttag_t)key_int, PyBytes_AsString(value)); + } else if (type == TIFF_RATIONAL) { + status = ImagingLibTiffSetField( + &encoder->state, (ttag_t)key_int, (FLOAT64)PyFloat_AsDouble(value)); + } else { + TRACE( + ("Unhandled type for key %d : %s \n", + key_int, + PyBytes_AsString(PyObject_Str(value)))); + } + } + if (!status) { + TRACE(("Error setting Field\n")); + Py_DECREF(encoder); + PyErr_SetString(PyExc_RuntimeError, "Error setting from dictionary"); + return NULL; + } + } + + encoder->encode = ImagingLibTiffEncode; + + return (PyObject *)encoder; +} + +#endif + +/* -------------------------------------------------------------------- */ +/* JPEG */ +/* -------------------------------------------------------------------- */ + +#ifdef HAVE_LIBJPEG + +/* We better define this encoder last in this file, so the following + undef's won't mess things up for the Imaging library proper. */ + +#undef HAVE_PROTOTYPES +#undef HAVE_STDDEF_H +#undef HAVE_STDLIB_H +#undef UINT8 +#undef UINT16 +#undef UINT32 +#undef INT8 +#undef INT16 +#undef INT32 + +#include "libImaging/Jpeg.h" + +static unsigned int * +get_qtables_arrays(PyObject *qtables, int *qtablesLen) { + PyObject *tables; + PyObject *table; + PyObject *table_data; + int i, j, num_tables; + unsigned int *qarrays; + + if ((qtables == NULL) || (qtables == Py_None)) { + return NULL; + } + + if (!PySequence_Check(qtables)) { + PyErr_SetString(PyExc_ValueError, "Invalid quantization tables"); + return NULL; + } + + tables = PySequence_Fast(qtables, "expected a sequence"); + num_tables = PySequence_Size(qtables); + if (num_tables < 1 || num_tables > NUM_QUANT_TBLS) { + PyErr_SetString( + PyExc_ValueError, + "Not a valid number of quantization tables. Should be between 1 and 4."); + Py_DECREF(tables); + return NULL; + } + /* malloc check ok, num_tables <4, DCTSIZE2 == 64 from jpeglib.h */ + qarrays = (unsigned int *)malloc(num_tables * DCTSIZE2 * sizeof(unsigned int)); + if (!qarrays) { + Py_DECREF(tables); + return ImagingError_MemoryError(); + } + for (i = 0; i < num_tables; i++) { + table = PySequence_Fast_GET_ITEM(tables, i); + if (!PySequence_Check(table)) { + PyErr_SetString(PyExc_ValueError, "Invalid quantization tables"); + goto JPEG_QTABLES_ERR; + } + if (PySequence_Size(table) != DCTSIZE2) { + PyErr_SetString(PyExc_ValueError, "Invalid quantization table size"); + goto JPEG_QTABLES_ERR; + } + table_data = PySequence_Fast(table, "expected a sequence"); + for (j = 0; j < DCTSIZE2; j++) { + qarrays[i * DCTSIZE2 + j] = + PyLong_AS_LONG(PySequence_Fast_GET_ITEM(table_data, j)); + } + Py_DECREF(table_data); + } + + *qtablesLen = num_tables; + +JPEG_QTABLES_ERR: + Py_DECREF(tables); // Run on both error and not error + if (PyErr_Occurred()) { + free(qarrays); + qarrays = NULL; + return NULL; + } + + return qarrays; +} + +PyObject * +PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { + ImagingEncoderObject *encoder; + + char *mode; + char *rawmode; + Py_ssize_t quality = 0; + Py_ssize_t progressive = 0; + Py_ssize_t smooth = 0; + Py_ssize_t optimize = 0; + int keep_rgb = 0; + Py_ssize_t streamtype = 0; /* 0=interchange, 1=tables only, 2=image only */ + Py_ssize_t xdpi = 0, ydpi = 0; + Py_ssize_t subsampling = -1; /* -1=default, 0=none, 1=medium, 2=high */ + Py_ssize_t restart_marker_blocks = 0; + Py_ssize_t restart_marker_rows = 0; + PyObject *qtables = NULL; + unsigned int *qarrays = NULL; + int qtablesLen = 0; + char *comment = NULL; + Py_ssize_t comment_size; + char *extra = NULL; + Py_ssize_t extra_size; + char *rawExif = NULL; + Py_ssize_t rawExifLen = 0; + + if (!PyArg_ParseTuple( + args, + "ss|nnnnpnnnnnnOz#y#y#", + &mode, + &rawmode, + &quality, + &progressive, + &smooth, + &optimize, + &keep_rgb, + &streamtype, + &xdpi, + &ydpi, + &subsampling, + &restart_marker_blocks, + &restart_marker_rows, + &qtables, + &comment, + &comment_size, + &extra, + &extra_size, + &rawExif, + &rawExifLen)) { + return NULL; + } + + encoder = PyImaging_EncoderNew(sizeof(JPEGENCODERSTATE)); + if (encoder == NULL) { + return NULL; + } + + // libjpeg-turbo supports different output formats. + // We are choosing Pillow's native format (3 color bytes + 1 padding) + // to avoid extra conversion in Pack.c. + if (ImagingJpegUseJCSExtensions() && strcmp(rawmode, "RGB") == 0) { + rawmode = "RGBX"; + } + + if (get_packer(encoder, mode, rawmode) < 0) { + return NULL; + } + + // Freed in JpegEncode, Case 6 + qarrays = get_qtables_arrays(qtables, &qtablesLen); + + if (comment && comment_size > 0) { + /* malloc check ok, length is from python parsearg */ + char *p = malloc(comment_size); // Freed in JpegEncode, Case 6 + if (!p) { + return ImagingError_MemoryError(); + } + memcpy(p, comment, comment_size); + comment = p; + } else { + comment = NULL; + } + + if (extra && extra_size > 0) { + /* malloc check ok, length is from python parsearg */ + char *p = malloc(extra_size); // Freed in JpegEncode, Case 6 + if (!p) { + if (comment) { + free(comment); + } + return ImagingError_MemoryError(); + } + memcpy(p, extra, extra_size); + extra = p; + } else { + extra = NULL; + } + + if (rawExif && rawExifLen > 0) { + /* malloc check ok, length is from python parsearg */ + char *pp = malloc(rawExifLen); // Freed in JpegEncode, Case 6 + if (!pp) { + if (comment) { + free(comment); + } + if (extra) { + free(extra); + } + return ImagingError_MemoryError(); + } + memcpy(pp, rawExif, rawExifLen); + rawExif = pp; + } else { + rawExif = NULL; + } + + encoder->encode = ImagingJpegEncode; + + strncpy(((JPEGENCODERSTATE *)encoder->state.context)->rawmode, rawmode, 8); + + ((JPEGENCODERSTATE *)encoder->state.context)->keep_rgb = keep_rgb; + ((JPEGENCODERSTATE *)encoder->state.context)->quality = quality; + ((JPEGENCODERSTATE *)encoder->state.context)->qtables = qarrays; + ((JPEGENCODERSTATE *)encoder->state.context)->qtablesLen = qtablesLen; + ((JPEGENCODERSTATE *)encoder->state.context)->subsampling = subsampling; + ((JPEGENCODERSTATE *)encoder->state.context)->progressive = progressive; + ((JPEGENCODERSTATE *)encoder->state.context)->smooth = smooth; + ((JPEGENCODERSTATE *)encoder->state.context)->optimize = optimize; + ((JPEGENCODERSTATE *)encoder->state.context)->streamtype = streamtype; + ((JPEGENCODERSTATE *)encoder->state.context)->xdpi = xdpi; + ((JPEGENCODERSTATE *)encoder->state.context)->ydpi = ydpi; + ((JPEGENCODERSTATE *)encoder->state.context)->restart_marker_blocks = + restart_marker_blocks; + ((JPEGENCODERSTATE *)encoder->state.context)->restart_marker_rows = + restart_marker_rows; + ((JPEGENCODERSTATE *)encoder->state.context)->comment = comment; + ((JPEGENCODERSTATE *)encoder->state.context)->comment_size = comment_size; + ((JPEGENCODERSTATE *)encoder->state.context)->extra = extra; + ((JPEGENCODERSTATE *)encoder->state.context)->extra_size = extra_size; + ((JPEGENCODERSTATE *)encoder->state.context)->rawExif = rawExif; + ((JPEGENCODERSTATE *)encoder->state.context)->rawExifLen = rawExifLen; + + return (PyObject *)encoder; +} + +#endif + +/* -------------------------------------------------------------------- */ +/* JPEG 2000 */ +/* -------------------------------------------------------------------- */ + +#ifdef HAVE_OPENJPEG + +#include "libImaging/Jpeg2K.h" + +static void +j2k_decode_coord_tuple(PyObject *tuple, int *x, int *y) { + *x = *y = 0; + + if (tuple && PyTuple_Check(tuple) && PyTuple_GET_SIZE(tuple) == 2) { + *x = (int)PyLong_AsLong(PyTuple_GET_ITEM(tuple, 0)); + *y = (int)PyLong_AsLong(PyTuple_GET_ITEM(tuple, 1)); + + if (*x < 0) { + *x = 0; + } + if (*y < 0) { + *y = 0; + } + } +} + +PyObject * +PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { + ImagingEncoderObject *encoder; + JPEG2KENCODESTATE *context; + + char *mode; + char *format; + OPJ_CODEC_FORMAT codec_format; + PyObject *offset = NULL, *tile_offset = NULL, *tile_size = NULL; + char *quality_mode = "rates"; + PyObject *quality_layers = NULL; + Py_ssize_t num_resolutions = 0; + PyObject *cblk_size = NULL, *precinct_size = NULL; + PyObject *irreversible = NULL; + char *progression = "LRCP"; + OPJ_PROG_ORDER prog_order; + char *cinema_mode = "no"; + OPJ_CINEMA_MODE cine_mode; + char mct = 0; + int sgnd = 0; + Py_ssize_t fd = -1; + char *comment; + Py_ssize_t comment_size; + int plt = 0; + + if (!PyArg_ParseTuple( + args, + "ss|OOOsOnOOOssbbnz#p", + &mode, + &format, + &offset, + &tile_offset, + &tile_size, + &quality_mode, + &quality_layers, + &num_resolutions, + &cblk_size, + &precinct_size, + &irreversible, + &progression, + &cinema_mode, + &mct, + &sgnd, + &fd, + &comment, + &comment_size, + &plt)) { + return NULL; + } + + if (strcmp(format, "j2k") == 0) { + codec_format = OPJ_CODEC_J2K; + } else if (strcmp(format, "jpt") == 0) { + codec_format = OPJ_CODEC_JPT; + } else if (strcmp(format, "jp2") == 0) { + codec_format = OPJ_CODEC_JP2; + } else { + return NULL; + } + + if (strcmp(progression, "LRCP") == 0) { + prog_order = OPJ_LRCP; + } else if (strcmp(progression, "RLCP") == 0) { + prog_order = OPJ_RLCP; + } else if (strcmp(progression, "RPCL") == 0) { + prog_order = OPJ_RPCL; + } else if (strcmp(progression, "PCRL") == 0) { + prog_order = OPJ_PCRL; + } else if (strcmp(progression, "CPRL") == 0) { + prog_order = OPJ_CPRL; + } else { + return NULL; + } + + if (strcmp(cinema_mode, "no") == 0) { + cine_mode = OPJ_OFF; + } else if (strcmp(cinema_mode, "cinema2k-24") == 0) { + cine_mode = OPJ_CINEMA2K_24; + } else if (strcmp(cinema_mode, "cinema2k-48") == 0) { + cine_mode = OPJ_CINEMA2K_48; + } else if (strcmp(cinema_mode, "cinema4k-24") == 0) { + cine_mode = OPJ_CINEMA4K_24; + } else { + return NULL; + } + + encoder = PyImaging_EncoderNew(sizeof(JPEG2KENCODESTATE)); + if (!encoder) { + return NULL; + } + + encoder->encode = ImagingJpeg2KEncode; + encoder->cleanup = ImagingJpeg2KEncodeCleanup; + encoder->pushes_fd = 1; + + context = (JPEG2KENCODESTATE *)encoder->state.context; + + context->fd = fd; + context->format = codec_format; + context->offset_x = context->offset_y = 0; + + j2k_decode_coord_tuple(offset, &context->offset_x, &context->offset_y); + j2k_decode_coord_tuple( + tile_offset, &context->tile_offset_x, &context->tile_offset_y); + j2k_decode_coord_tuple(tile_size, &context->tile_size_x, &context->tile_size_y); + + /* Error on illegal tile offsets */ + if (context->tile_size_x && context->tile_size_y) { + if (context->tile_offset_x <= context->offset_x - context->tile_size_x || + context->tile_offset_y <= context->offset_y - context->tile_size_y) { + PyErr_SetString( + PyExc_ValueError, + "JPEG 2000 tile offset too small; top left tile must " + "intersect image area"); + Py_DECREF(encoder); + return NULL; + } + + if (context->tile_offset_x > context->offset_x || + context->tile_offset_y > context->offset_y) { + PyErr_SetString( + PyExc_ValueError, + "JPEG 2000 tile offset too large to cover image area"); + Py_DECREF(encoder); + return NULL; + } + } + + if (comment && comment_size > 0) { + /* Size is stored as as an uint16, subtract 4 bytes for the header */ + if (comment_size >= 65532) { + PyErr_SetString(PyExc_ValueError, "JPEG 2000 comment is too long"); + Py_DECREF(encoder); + return NULL; + } + + char *p = malloc(comment_size + 1); + if (!p) { + Py_DECREF(encoder); + return ImagingError_MemoryError(); + } + memcpy(p, comment, comment_size); + p[comment_size] = '\0'; + context->comment = p; + } + + if (quality_layers && PySequence_Check(quality_layers)) { + context->quality_is_in_db = strcmp(quality_mode, "dB") == 0; + context->quality_layers = quality_layers; + Py_INCREF(quality_layers); + } + + context->num_resolutions = num_resolutions; + + j2k_decode_coord_tuple(cblk_size, &context->cblk_width, &context->cblk_height); + j2k_decode_coord_tuple( + precinct_size, &context->precinct_width, &context->precinct_height); + + context->irreversible = PyObject_IsTrue(irreversible); + context->progression = prog_order; + context->cinema_mode = cine_mode; + context->mct = mct; + context->sgnd = sgnd; + context->plt = plt; + + return (PyObject *)encoder; +} + +#endif + +/* + * Local Variables: + * c-basic-offset: 4 + * End: + * + */ diff --git a/src/libImaging/Access.c b/src/libImaging/Access.c index 3a5e918e8..1722e625b 100644 --- a/src/libImaging/Access.c +++ b/src/libImaging/Access.c @@ -1,237 +1,237 @@ -/* - * The Python Imaging Library - * $Id$ - * - * imaging access objects - * - * Copyright (c) Fredrik Lundh 2009. - * - * See the README file for information on usage and redistribution. - */ - -#include "Imaging.h" - -/* use make_hash.py from the pillow-scripts repository to calculate these values */ -#define ACCESS_TABLE_SIZE 35 -#define ACCESS_TABLE_HASH 8940 - -static struct ImagingAccessInstance access_table[ACCESS_TABLE_SIZE]; - -static inline UINT32 -hash(const char *mode) { - UINT32 i = ACCESS_TABLE_HASH; - while (*mode) { - i = ((i << 5) + i) ^ (UINT8)*mode++; - } - return i % ACCESS_TABLE_SIZE; -} - -static ImagingAccess -add_item(const char *mode) { - UINT32 i = hash(mode); - /* printf("hash %s => %d\n", mode, i); */ - if (access_table[i].mode && strcmp(access_table[i].mode, mode) != 0) { - fprintf( - stderr, - "AccessInit: hash collision: %d for both %s and %s\n", - i, - mode, - access_table[i].mode); - exit(1); - } - access_table[i].mode = mode; - return &access_table[i]; -} - -/* fetch individual pixel */ - -static void -get_pixel_32_2bands(Imaging im, int x, int y, void *color) { - char *out = color; - UINT8 *p = (UINT8 *)&im->image32[y][x]; - out[0] = p[0]; - out[1] = p[3]; -} - -static void -get_pixel_8(Imaging im, int x, int y, void *color) { - char *out = color; - out[0] = im->image8[y][x]; -} - -static void -get_pixel_16L(Imaging im, int x, int y, void *color) { - UINT8 *in = (UINT8 *)&im->image[y][x + x]; -#ifdef WORDS_BIGENDIAN - UINT16 out = in[0] + (in[1] << 8); - memcpy(color, &out, sizeof(out)); -#else - memcpy(color, in, sizeof(UINT16)); -#endif -} - -static void -get_pixel_16B(Imaging im, int x, int y, void *color) { - UINT8 *in = (UINT8 *)&im->image[y][x + x]; -#ifdef WORDS_BIGENDIAN - memcpy(color, in, sizeof(UINT16)); -#else - UINT16 out = in[1] + (in[0] << 8); - memcpy(color, &out, sizeof(out)); -#endif -} - -static void -get_pixel_BGR15(Imaging im, int x, int y, void *color) { - UINT8 *in = (UINT8 *)&im->image8[y][x * 2]; - UINT16 pixel = in[0] + (in[1] << 8); - char *out = color; - out[0] = (pixel & 31) * 255 / 31; - out[1] = ((pixel >> 5) & 31) * 255 / 31; - out[2] = ((pixel >> 10) & 31) * 255 / 31; -} - -static void -get_pixel_BGR16(Imaging im, int x, int y, void *color) { - UINT8 *in = (UINT8 *)&im->image8[y][x * 2]; - UINT16 pixel = in[0] + (in[1] << 8); - char *out = color; - out[0] = (pixel & 31) * 255 / 31; - out[1] = ((pixel >> 5) & 63) * 255 / 63; - out[2] = ((pixel >> 11) & 31) * 255 / 31; -} - -static void -get_pixel_BGR24(Imaging im, int x, int y, void *color) { - memcpy(color, &im->image8[y][x * 3], sizeof(UINT8) * 3); -} - -static void -get_pixel_32(Imaging im, int x, int y, void *color) { - memcpy(color, &im->image32[y][x], sizeof(INT32)); -} - -static void -get_pixel_32L(Imaging im, int x, int y, void *color) { - UINT8 *in = (UINT8 *)&im->image[y][x * 4]; -#ifdef WORDS_BIGENDIAN - INT32 out = in[0] + (in[1] << 8) + (in[2] << 16) + (in[3] << 24); - memcpy(color, &out, sizeof(out)); -#else - memcpy(color, in, sizeof(INT32)); -#endif -} - -static void -get_pixel_32B(Imaging im, int x, int y, void *color) { - UINT8 *in = (UINT8 *)&im->image[y][x * 4]; -#ifdef WORDS_BIGENDIAN - memcpy(color, in, sizeof(INT32)); -#else - INT32 out = in[3] + (in[2] << 8) + (in[1] << 16) + (in[0] << 24); - memcpy(color, &out, sizeof(out)); -#endif -} - -/* store individual pixel */ - -static void -put_pixel_8(Imaging im, int x, int y, const void *color) { - im->image8[y][x] = *((UINT8 *)color); -} - -static void -put_pixel_16L(Imaging im, int x, int y, const void *color) { - memcpy(&im->image8[y][x + x], color, 2); -} - -static void -put_pixel_16B(Imaging im, int x, int y, const void *color) { - const char *in = color; - UINT8 *out = (UINT8 *)&im->image8[y][x + x]; - out[0] = in[1]; - out[1] = in[0]; -} - -static void -put_pixel_BGR1516(Imaging im, int x, int y, const void *color) { - memcpy(&im->image8[y][x * 2], color, 2); -} - -static void -put_pixel_BGR24(Imaging im, int x, int y, const void *color) { - memcpy(&im->image8[y][x * 3], color, 3); -} - -static void -put_pixel_32L(Imaging im, int x, int y, const void *color) { - memcpy(&im->image8[y][x * 4], color, 4); -} - -static void -put_pixel_32B(Imaging im, int x, int y, const void *color) { - const char *in = color; - UINT8 *out = (UINT8 *)&im->image8[y][x * 4]; - out[0] = in[3]; - out[1] = in[2]; - out[2] = in[1]; - out[3] = in[0]; -} - -static void -put_pixel_32(Imaging im, int x, int y, const void *color) { - memcpy(&im->image32[y][x], color, sizeof(INT32)); -} - -void -ImagingAccessInit() { -#define ADD(mode_, get_pixel_, put_pixel_) \ - { \ - ImagingAccess access = add_item(mode_); \ - access->get_pixel = get_pixel_; \ - access->put_pixel = put_pixel_; \ - } - - /* populate access table */ - ADD("1", get_pixel_8, put_pixel_8); - ADD("L", get_pixel_8, put_pixel_8); - ADD("LA", get_pixel_32_2bands, put_pixel_32); - ADD("La", get_pixel_32_2bands, put_pixel_32); - ADD("I", get_pixel_32, put_pixel_32); - ADD("I;16", get_pixel_16L, put_pixel_16L); - ADD("I;16L", get_pixel_16L, put_pixel_16L); - ADD("I;16B", get_pixel_16B, put_pixel_16B); -#ifdef WORDS_BIGENDIAN - ADD("I;16N", get_pixel_16B, put_pixel_16B); -#else - ADD("I;16N", get_pixel_16L, put_pixel_16L); -#endif - ADD("I;32L", get_pixel_32L, put_pixel_32L); - ADD("I;32B", get_pixel_32B, put_pixel_32B); - ADD("F", get_pixel_32, put_pixel_32); - ADD("P", get_pixel_8, put_pixel_8); - ADD("PA", get_pixel_32_2bands, put_pixel_32); - ADD("BGR;15", get_pixel_BGR15, put_pixel_BGR1516); - ADD("BGR;16", get_pixel_BGR16, put_pixel_BGR1516); - ADD("BGR;24", get_pixel_BGR24, put_pixel_BGR24); - ADD("RGB", get_pixel_32, put_pixel_32); - ADD("RGBA", get_pixel_32, put_pixel_32); - ADD("RGBa", get_pixel_32, put_pixel_32); - ADD("RGBX", get_pixel_32, put_pixel_32); - ADD("CMYK", get_pixel_32, put_pixel_32); - ADD("YCbCr", get_pixel_32, put_pixel_32); - ADD("LAB", get_pixel_32, put_pixel_32); - ADD("HSV", get_pixel_32, put_pixel_32); -} - -ImagingAccess -ImagingAccessNew(Imaging im) { - ImagingAccess access = &access_table[hash(im->mode)]; - if (im->mode[0] != access->mode[0] || strcmp(im->mode, access->mode) != 0) { - return NULL; - } - return access; -} - -void -_ImagingAccessDelete(Imaging im, ImagingAccess access) {} +/* + * The Python Imaging Library + * $Id$ + * + * imaging access objects + * + * Copyright (c) Fredrik Lundh 2009. + * + * See the README file for information on usage and redistribution. + */ + +#include "Imaging.h" + +/* use make_hash.py from the pillow-scripts repository to calculate these values */ +#define ACCESS_TABLE_SIZE 35 +#define ACCESS_TABLE_HASH 8940 + +static struct ImagingAccessInstance access_table[ACCESS_TABLE_SIZE]; + +static inline UINT32 +hash(const char *mode) { + UINT32 i = ACCESS_TABLE_HASH; + while (*mode) { + i = ((i << 5) + i) ^ (UINT8)*mode++; + } + return i % ACCESS_TABLE_SIZE; +} + +static ImagingAccess +add_item(const char *mode) { + UINT32 i = hash(mode); + /* printf("hash %s => %d\n", mode, i); */ + if (access_table[i].mode && strcmp(access_table[i].mode, mode) != 0) { + fprintf( + stderr, + "AccessInit: hash collision: %d for both %s and %s\n", + i, + mode, + access_table[i].mode); + exit(1); + } + access_table[i].mode = mode; + return &access_table[i]; +} + +/* fetch individual pixel */ + +static void +get_pixel_32_2bands(Imaging im, int x, int y, void *color) { + char *out = color; + UINT8 *p = (UINT8 *)&im->image32[y][x]; + out[0] = p[0]; + out[1] = p[3]; +} + +static void +get_pixel_8(Imaging im, int x, int y, void *color) { + char *out = color; + out[0] = im->image8[y][x]; +} + +static void +get_pixel_16L(Imaging im, int x, int y, void *color) { + UINT8 *in = (UINT8 *)&im->image[y][x + x]; +#ifdef WORDS_BIGENDIAN + UINT16 out = in[0] + (in[1] << 8); + memcpy(color, &out, sizeof(out)); +#else + memcpy(color, in, sizeof(UINT16)); +#endif +} + +static void +get_pixel_16B(Imaging im, int x, int y, void *color) { + UINT8 *in = (UINT8 *)&im->image[y][x + x]; +#ifdef WORDS_BIGENDIAN + memcpy(color, in, sizeof(UINT16)); +#else + UINT16 out = in[1] + (in[0] << 8); + memcpy(color, &out, sizeof(out)); +#endif +} + +static void +get_pixel_BGR15(Imaging im, int x, int y, void *color) { + UINT8 *in = (UINT8 *)&im->image8[y][x * 2]; + UINT16 pixel = in[0] + (in[1] << 8); + char *out = color; + out[0] = (pixel & 31) * 255 / 31; + out[1] = ((pixel >> 5) & 31) * 255 / 31; + out[2] = ((pixel >> 10) & 31) * 255 / 31; +} + +static void +get_pixel_BGR16(Imaging im, int x, int y, void *color) { + UINT8 *in = (UINT8 *)&im->image8[y][x * 2]; + UINT16 pixel = in[0] + (in[1] << 8); + char *out = color; + out[0] = (pixel & 31) * 255 / 31; + out[1] = ((pixel >> 5) & 63) * 255 / 63; + out[2] = ((pixel >> 11) & 31) * 255 / 31; +} + +static void +get_pixel_BGR24(Imaging im, int x, int y, void *color) { + memcpy(color, &im->image8[y][x * 3], sizeof(UINT8) * 3); +} + +static void +get_pixel_32(Imaging im, int x, int y, void *color) { + memcpy(color, &im->image32[y][x], sizeof(INT32)); +} + +static void +get_pixel_32L(Imaging im, int x, int y, void *color) { + UINT8 *in = (UINT8 *)&im->image[y][x * 4]; +#ifdef WORDS_BIGENDIAN + INT32 out = in[0] + (in[1] << 8) + (in[2] << 16) + (in[3] << 24); + memcpy(color, &out, sizeof(out)); +#else + memcpy(color, in, sizeof(INT32)); +#endif +} + +static void +get_pixel_32B(Imaging im, int x, int y, void *color) { + UINT8 *in = (UINT8 *)&im->image[y][x * 4]; +#ifdef WORDS_BIGENDIAN + memcpy(color, in, sizeof(INT32)); +#else + INT32 out = in[3] + (in[2] << 8) + (in[1] << 16) + (in[0] << 24); + memcpy(color, &out, sizeof(out)); +#endif +} + +/* store individual pixel */ + +static void +put_pixel_8(Imaging im, int x, int y, const void *color) { + im->image8[y][x] = *((UINT8 *)color); +} + +static void +put_pixel_16L(Imaging im, int x, int y, const void *color) { + memcpy(&im->image8[y][x + x], color, 2); +} + +static void +put_pixel_16B(Imaging im, int x, int y, const void *color) { + const char *in = color; + UINT8 *out = (UINT8 *)&im->image8[y][x + x]; + out[0] = in[1]; + out[1] = in[0]; +} + +static void +put_pixel_BGR1516(Imaging im, int x, int y, const void *color) { + memcpy(&im->image8[y][x * 2], color, 2); +} + +static void +put_pixel_BGR24(Imaging im, int x, int y, const void *color) { + memcpy(&im->image8[y][x * 3], color, 3); +} + +static void +put_pixel_32L(Imaging im, int x, int y, const void *color) { + memcpy(&im->image8[y][x * 4], color, 4); +} + +static void +put_pixel_32B(Imaging im, int x, int y, const void *color) { + const char *in = color; + UINT8 *out = (UINT8 *)&im->image8[y][x * 4]; + out[0] = in[3]; + out[1] = in[2]; + out[2] = in[1]; + out[3] = in[0]; +} + +static void +put_pixel_32(Imaging im, int x, int y, const void *color) { + memcpy(&im->image32[y][x], color, sizeof(INT32)); +} + +void +ImagingAccessInit() { +#define ADD(mode_, get_pixel_, put_pixel_) \ + { \ + ImagingAccess access = add_item(mode_); \ + access->get_pixel = get_pixel_; \ + access->put_pixel = put_pixel_; \ + } + + /* populate access table */ + ADD("1", get_pixel_8, put_pixel_8); + ADD("L", get_pixel_8, put_pixel_8); + ADD("LA", get_pixel_32_2bands, put_pixel_32); + ADD("La", get_pixel_32_2bands, put_pixel_32); + ADD("I", get_pixel_32, put_pixel_32); + ADD("I;16", get_pixel_16L, put_pixel_16L); + ADD("I;16L", get_pixel_16L, put_pixel_16L); + ADD("I;16B", get_pixel_16B, put_pixel_16B); +#ifdef WORDS_BIGENDIAN + ADD("I;16N", get_pixel_16B, put_pixel_16B); +#else + ADD("I;16N", get_pixel_16L, put_pixel_16L); +#endif + ADD("I;32L", get_pixel_32L, put_pixel_32L); + ADD("I;32B", get_pixel_32B, put_pixel_32B); + ADD("F", get_pixel_32, put_pixel_32); + ADD("P", get_pixel_8, put_pixel_8); + ADD("PA", get_pixel_32_2bands, put_pixel_32); + ADD("BGR;15", get_pixel_BGR15, put_pixel_BGR1516); + ADD("BGR;16", get_pixel_BGR16, put_pixel_BGR1516); + ADD("BGR;24", get_pixel_BGR24, put_pixel_BGR24); + ADD("RGB", get_pixel_32, put_pixel_32); + ADD("RGBA", get_pixel_32, put_pixel_32); + ADD("RGBa", get_pixel_32, put_pixel_32); + ADD("RGBX", get_pixel_32, put_pixel_32); + ADD("CMYK", get_pixel_32, put_pixel_32); + ADD("YCbCr", get_pixel_32, put_pixel_32); + ADD("LAB", get_pixel_32, put_pixel_32); + ADD("HSV", get_pixel_32, put_pixel_32); +} + +ImagingAccess +ImagingAccessNew(Imaging im) { + ImagingAccess access = &access_table[hash(im->mode)]; + if (im->mode[0] != access->mode[0] || strcmp(im->mode, access->mode) != 0) { + return NULL; + } + return access; +} + +void +_ImagingAccessDelete(Imaging im, ImagingAccess access) {} diff --git a/src/libImaging/AlphaComposite.c b/src/libImaging/AlphaComposite.c index 6d728f908..c2dde5336 100644 --- a/src/libImaging/AlphaComposite.c +++ b/src/libImaging/AlphaComposite.c @@ -1,85 +1,85 @@ -/* - * The Python Imaging Library - * $Id$ - * - * Alpha composite imSrc over imDst. - * https://en.wikipedia.org/wiki/Alpha_compositing - * - * See the README file for details on usage and redistribution. - */ - -#include "Imaging.h" - -#define PRECISION_BITS 7 - -typedef struct { - UINT8 r; - UINT8 g; - UINT8 b; - UINT8 a; -} rgba8; - -Imaging -ImagingAlphaComposite(Imaging imDst, Imaging imSrc) { - Imaging imOut; - int x, y; - - /* Check arguments */ - if (!imDst || !imSrc || strcmp(imDst->mode, "RGBA") || - imDst->type != IMAGING_TYPE_UINT8 || imDst->bands != 4) { - return ImagingError_ModeError(); - } - - if (strcmp(imDst->mode, imSrc->mode) || imDst->type != imSrc->type || - imDst->bands != imSrc->bands || imDst->xsize != imSrc->xsize || - imDst->ysize != imSrc->ysize) { - return ImagingError_Mismatch(); - } - - imOut = ImagingNewDirty(imDst->mode, imDst->xsize, imDst->ysize); - if (!imOut) { - return NULL; - } - - for (y = 0; y < imDst->ysize; y++) { - rgba8 *dst = (rgba8 *)imDst->image[y]; - rgba8 *src = (rgba8 *)imSrc->image[y]; - rgba8 *out = (rgba8 *)imOut->image[y]; - - for (x = 0; x < imDst->xsize; x++) { - if (src->a == 0) { - // Copy 4 bytes at once. - *out = *dst; - } else { - // Integer implementation with increased precision. - // Each variable has extra meaningful bits. - // Divisions are rounded. - - UINT32 tmpr, tmpg, tmpb; - UINT32 blend = dst->a * (255 - src->a); - UINT32 outa255 = src->a * 255 + blend; - // There we use 7 bits for precision. - // We could use more, but we go beyond 32 bits. - UINT32 coef1 = src->a * 255 * 255 * (1 << PRECISION_BITS) / outa255; - UINT32 coef2 = 255 * (1 << PRECISION_BITS) - coef1; - - tmpr = src->r * coef1 + dst->r * coef2; - tmpg = src->g * coef1 + dst->g * coef2; - tmpb = src->b * coef1 + dst->b * coef2; - out->r = - SHIFTFORDIV255(tmpr + (0x80 << PRECISION_BITS)) >> PRECISION_BITS; - out->g = - SHIFTFORDIV255(tmpg + (0x80 << PRECISION_BITS)) >> PRECISION_BITS; - out->b = - SHIFTFORDIV255(tmpb + (0x80 << PRECISION_BITS)) >> PRECISION_BITS; - out->a = SHIFTFORDIV255(outa255 + 0x80); - } - - dst++; - src++; - out++; - } - } - - return imOut; -} +/* + * The Python Imaging Library + * $Id$ + * + * Alpha composite imSrc over imDst. + * https://en.wikipedia.org/wiki/Alpha_compositing + * + * See the README file for details on usage and redistribution. + */ + +#include "Imaging.h" + +#define PRECISION_BITS 7 + +typedef struct { + UINT8 r; + UINT8 g; + UINT8 b; + UINT8 a; +} rgba8; + +Imaging +ImagingAlphaComposite(Imaging imDst, Imaging imSrc) { + Imaging imOut; + int x, y; + + /* Check arguments */ + if (!imDst || !imSrc || strcmp(imDst->mode, "RGBA") || + imDst->type != IMAGING_TYPE_UINT8 || imDst->bands != 4) { + return ImagingError_ModeError(); + } + + if (strcmp(imDst->mode, imSrc->mode) || imDst->type != imSrc->type || + imDst->bands != imSrc->bands || imDst->xsize != imSrc->xsize || + imDst->ysize != imSrc->ysize) { + return ImagingError_Mismatch(); + } + + imOut = ImagingNewDirty(imDst->mode, imDst->xsize, imDst->ysize); + if (!imOut) { + return NULL; + } + + for (y = 0; y < imDst->ysize; y++) { + rgba8 *dst = (rgba8 *)imDst->image[y]; + rgba8 *src = (rgba8 *)imSrc->image[y]; + rgba8 *out = (rgba8 *)imOut->image[y]; + + for (x = 0; x < imDst->xsize; x++) { + if (src->a == 0) { + // Copy 4 bytes at once. + *out = *dst; + } else { + // Integer implementation with increased precision. + // Each variable has extra meaningful bits. + // Divisions are rounded. + + UINT32 tmpr, tmpg, tmpb; + UINT32 blend = dst->a * (255 - src->a); + UINT32 outa255 = src->a * 255 + blend; + // There we use 7 bits for precision. + // We could use more, but we go beyond 32 bits. + UINT32 coef1 = src->a * 255 * 255 * (1 << PRECISION_BITS) / outa255; + UINT32 coef2 = 255 * (1 << PRECISION_BITS) - coef1; + + tmpr = src->r * coef1 + dst->r * coef2; + tmpg = src->g * coef1 + dst->g * coef2; + tmpb = src->b * coef1 + dst->b * coef2; + out->r = + SHIFTFORDIV255(tmpr + (0x80 << PRECISION_BITS)) >> PRECISION_BITS; + out->g = + SHIFTFORDIV255(tmpg + (0x80 << PRECISION_BITS)) >> PRECISION_BITS; + out->b = + SHIFTFORDIV255(tmpb + (0x80 << PRECISION_BITS)) >> PRECISION_BITS; + out->a = SHIFTFORDIV255(outa255 + 0x80); + } + + dst++; + src++; + out++; + } + } + + return imOut; +} diff --git a/src/libImaging/Bands.c b/src/libImaging/Bands.c index e1b16b34a..96c4b1080 100644 --- a/src/libImaging/Bands.c +++ b/src/libImaging/Bands.c @@ -1,315 +1,315 @@ -/* - * The Python Imaging Library - * $Id$ - * - * stuff to extract and paste back individual bands - * - * history: - * 1996-03-20 fl Created - * 1997-08-27 fl Fixed putband for single band targets. - * 2003-09-26 fl Fixed getband/putband for 2-band images (LA, PA). - * - * Copyright (c) 1997-2003 by Secret Labs AB. - * Copyright (c) 1996-1997 by Fredrik Lundh. - * - * See the README file for details on usage and redistribution. - */ - -#include "Imaging.h" - -Imaging -ImagingGetBand(Imaging imIn, int band) { - Imaging imOut; - int x, y; - - /* Check arguments */ - if (!imIn || imIn->type != IMAGING_TYPE_UINT8) { - return (Imaging)ImagingError_ModeError(); - } - - if (band < 0 || band >= imIn->bands) { - return (Imaging)ImagingError_ValueError("band index out of range"); - } - - /* Shortcuts */ - if (imIn->bands == 1) { - return ImagingCopy(imIn); - } - - /* Special case for LXXA etc */ - if (imIn->bands == 2 && band == 1) { - band = 3; - } - - imOut = ImagingNewDirty("L", imIn->xsize, imIn->ysize); - if (!imOut) { - return NULL; - } - - /* Extract band from image */ - for (y = 0; y < imIn->ysize; y++) { - UINT8 *in = (UINT8 *)imIn->image[y] + band; - UINT8 *out = imOut->image8[y]; - x = 0; - for (; x < imIn->xsize - 3; x += 4) { - UINT32 v = MAKE_UINT32(in[0], in[4], in[8], in[12]); - memcpy(out + x, &v, sizeof(v)); - in += 16; - } - for (; x < imIn->xsize; x++) { - out[x] = *in; - in += 4; - } - } - - return imOut; -} - -int -ImagingSplit(Imaging imIn, Imaging bands[4]) { - int i, j, x, y; - - /* Check arguments */ - if (!imIn || imIn->type != IMAGING_TYPE_UINT8) { - (void)ImagingError_ModeError(); - return 0; - } - - /* Shortcuts */ - if (imIn->bands == 1) { - bands[0] = ImagingCopy(imIn); - return imIn->bands; - } - - for (i = 0; i < imIn->bands; i++) { - bands[i] = ImagingNewDirty("L", imIn->xsize, imIn->ysize); - if (!bands[i]) { - for (j = 0; j < i; ++j) { - ImagingDelete(bands[j]); - } - return 0; - } - } - - /* Extract bands from image */ - if (imIn->bands == 2) { - for (y = 0; y < imIn->ysize; y++) { - UINT8 *in = (UINT8 *)imIn->image[y]; - UINT8 *out0 = bands[0]->image8[y]; - UINT8 *out1 = bands[1]->image8[y]; - x = 0; - for (; x < imIn->xsize - 3; x += 4) { - UINT32 v = MAKE_UINT32(in[0], in[4], in[8], in[12]); - memcpy(out0 + x, &v, sizeof(v)); - v = MAKE_UINT32(in[0 + 3], in[4 + 3], in[8 + 3], in[12 + 3]); - memcpy(out1 + x, &v, sizeof(v)); - in += 16; - } - for (; x < imIn->xsize; x++) { - out0[x] = in[0]; - out1[x] = in[3]; - in += 4; - } - } - } else if (imIn->bands == 3) { - for (y = 0; y < imIn->ysize; y++) { - UINT8 *in = (UINT8 *)imIn->image[y]; - UINT8 *out0 = bands[0]->image8[y]; - UINT8 *out1 = bands[1]->image8[y]; - UINT8 *out2 = bands[2]->image8[y]; - x = 0; - for (; x < imIn->xsize - 3; x += 4) { - UINT32 v = MAKE_UINT32(in[0], in[4], in[8], in[12]); - memcpy(out0 + x, &v, sizeof(v)); - v = MAKE_UINT32(in[0 + 1], in[4 + 1], in[8 + 1], in[12 + 1]); - memcpy(out1 + x, &v, sizeof(v)); - v = MAKE_UINT32(in[0 + 2], in[4 + 2], in[8 + 2], in[12 + 2]); - memcpy(out2 + x, &v, sizeof(v)); - in += 16; - } - for (; x < imIn->xsize; x++) { - out0[x] = in[0]; - out1[x] = in[1]; - out2[x] = in[2]; - in += 4; - } - } - } else { - for (y = 0; y < imIn->ysize; y++) { - UINT8 *in = (UINT8 *)imIn->image[y]; - UINT8 *out0 = bands[0]->image8[y]; - UINT8 *out1 = bands[1]->image8[y]; - UINT8 *out2 = bands[2]->image8[y]; - UINT8 *out3 = bands[3]->image8[y]; - x = 0; - for (; x < imIn->xsize - 3; x += 4) { - UINT32 v = MAKE_UINT32(in[0], in[4], in[8], in[12]); - memcpy(out0 + x, &v, sizeof(v)); - v = MAKE_UINT32(in[0 + 1], in[4 + 1], in[8 + 1], in[12 + 1]); - memcpy(out1 + x, &v, sizeof(v)); - v = MAKE_UINT32(in[0 + 2], in[4 + 2], in[8 + 2], in[12 + 2]); - memcpy(out2 + x, &v, sizeof(v)); - v = MAKE_UINT32(in[0 + 3], in[4 + 3], in[8 + 3], in[12 + 3]); - memcpy(out3 + x, &v, sizeof(v)); - in += 16; - } - for (; x < imIn->xsize; x++) { - out0[x] = in[0]; - out1[x] = in[1]; - out2[x] = in[2]; - out3[x] = in[3]; - in += 4; - } - } - } - - return imIn->bands; -} - -Imaging -ImagingPutBand(Imaging imOut, Imaging imIn, int band) { - int x, y; - - /* Check arguments */ - if (!imIn || imIn->bands != 1 || !imOut) { - return (Imaging)ImagingError_ModeError(); - } - - if (band < 0 || band >= imOut->bands) { - return (Imaging)ImagingError_ValueError("band index out of range"); - } - - if (imIn->type != imOut->type || imIn->xsize != imOut->xsize || - imIn->ysize != imOut->ysize) { - return (Imaging)ImagingError_Mismatch(); - } - - /* Shortcuts */ - if (imOut->bands == 1) { - return ImagingCopy2(imOut, imIn); - } - - /* Special case for LXXA etc */ - if (imOut->bands == 2 && band == 1) { - band = 3; - } - - /* Insert band into image */ - for (y = 0; y < imIn->ysize; y++) { - UINT8 *in = imIn->image8[y]; - UINT8 *out = (UINT8 *)imOut->image[y] + band; - for (x = 0; x < imIn->xsize; x++) { - *out = in[x]; - out += 4; - } - } - - return imOut; -} - -Imaging -ImagingFillBand(Imaging imOut, int band, int color) { - int x, y; - - /* Check arguments */ - if (!imOut || imOut->type != IMAGING_TYPE_UINT8) { - return (Imaging)ImagingError_ModeError(); - } - - if (band < 0 || band >= imOut->bands) { - return (Imaging)ImagingError_ValueError("band index out of range"); - } - - /* Special case for LXXA etc */ - if (imOut->bands == 2 && band == 1) { - band = 3; - } - - color = CLIP8(color); - - /* Insert color into image */ - for (y = 0; y < imOut->ysize; y++) { - UINT8 *out = (UINT8 *)imOut->image[y] + band; - for (x = 0; x < imOut->xsize; x++) { - *out = (UINT8)color; - out += 4; - } - } - - return imOut; -} - -Imaging -ImagingMerge(const char *mode, Imaging bands[4]) { - int i, x, y; - int bandsCount = 0; - Imaging imOut; - Imaging firstBand; - - firstBand = bands[0]; - if (!firstBand) { - return (Imaging)ImagingError_ValueError("wrong number of bands"); - } - - for (i = 0; i < 4; ++i) { - if (!bands[i]) { - break; - } - if (bands[i]->bands != 1) { - return (Imaging)ImagingError_ModeError(); - } - if (bands[i]->xsize != firstBand->xsize || - bands[i]->ysize != firstBand->ysize) { - return (Imaging)ImagingError_Mismatch(); - } - } - bandsCount = i; - - imOut = ImagingNewDirty(mode, firstBand->xsize, firstBand->ysize); - if (!imOut) { - return NULL; - } - - if (imOut->bands != bandsCount) { - ImagingDelete(imOut); - return (Imaging)ImagingError_ValueError("wrong number of bands"); - } - - if (imOut->bands == 1) { - return ImagingCopy2(imOut, firstBand); - } - - if (imOut->bands == 2) { - for (y = 0; y < imOut->ysize; y++) { - UINT8 *in0 = bands[0]->image8[y]; - UINT8 *in1 = bands[1]->image8[y]; - UINT32 *out = (UINT32 *)imOut->image32[y]; - for (x = 0; x < imOut->xsize; x++) { - out[x] = MAKE_UINT32(in0[x], 0, 0, in1[x]); - } - } - } else if (imOut->bands == 3) { - for (y = 0; y < imOut->ysize; y++) { - UINT8 *in0 = bands[0]->image8[y]; - UINT8 *in1 = bands[1]->image8[y]; - UINT8 *in2 = bands[2]->image8[y]; - UINT32 *out = (UINT32 *)imOut->image32[y]; - for (x = 0; x < imOut->xsize; x++) { - out[x] = MAKE_UINT32(in0[x], in1[x], in2[x], 0); - } - } - } else if (imOut->bands == 4) { - for (y = 0; y < imOut->ysize; y++) { - UINT8 *in0 = bands[0]->image8[y]; - UINT8 *in1 = bands[1]->image8[y]; - UINT8 *in2 = bands[2]->image8[y]; - UINT8 *in3 = bands[3]->image8[y]; - UINT32 *out = (UINT32 *)imOut->image32[y]; - for (x = 0; x < imOut->xsize; x++) { - out[x] = MAKE_UINT32(in0[x], in1[x], in2[x], in3[x]); - } - } - } - - return imOut; -} +/* + * The Python Imaging Library + * $Id$ + * + * stuff to extract and paste back individual bands + * + * history: + * 1996-03-20 fl Created + * 1997-08-27 fl Fixed putband for single band targets. + * 2003-09-26 fl Fixed getband/putband for 2-band images (LA, PA). + * + * Copyright (c) 1997-2003 by Secret Labs AB. + * Copyright (c) 1996-1997 by Fredrik Lundh. + * + * See the README file for details on usage and redistribution. + */ + +#include "Imaging.h" + +Imaging +ImagingGetBand(Imaging imIn, int band) { + Imaging imOut; + int x, y; + + /* Check arguments */ + if (!imIn || imIn->type != IMAGING_TYPE_UINT8) { + return (Imaging)ImagingError_ModeError(); + } + + if (band < 0 || band >= imIn->bands) { + return (Imaging)ImagingError_ValueError("band index out of range"); + } + + /* Shortcuts */ + if (imIn->bands == 1) { + return ImagingCopy(imIn); + } + + /* Special case for LXXA etc */ + if (imIn->bands == 2 && band == 1) { + band = 3; + } + + imOut = ImagingNewDirty("L", imIn->xsize, imIn->ysize); + if (!imOut) { + return NULL; + } + + /* Extract band from image */ + for (y = 0; y < imIn->ysize; y++) { + UINT8 *in = (UINT8 *)imIn->image[y] + band; + UINT8 *out = imOut->image8[y]; + x = 0; + for (; x < imIn->xsize - 3; x += 4) { + UINT32 v = MAKE_UINT32(in[0], in[4], in[8], in[12]); + memcpy(out + x, &v, sizeof(v)); + in += 16; + } + for (; x < imIn->xsize; x++) { + out[x] = *in; + in += 4; + } + } + + return imOut; +} + +int +ImagingSplit(Imaging imIn, Imaging bands[4]) { + int i, j, x, y; + + /* Check arguments */ + if (!imIn || imIn->type != IMAGING_TYPE_UINT8) { + (void)ImagingError_ModeError(); + return 0; + } + + /* Shortcuts */ + if (imIn->bands == 1) { + bands[0] = ImagingCopy(imIn); + return imIn->bands; + } + + for (i = 0; i < imIn->bands; i++) { + bands[i] = ImagingNewDirty("L", imIn->xsize, imIn->ysize); + if (!bands[i]) { + for (j = 0; j < i; ++j) { + ImagingDelete(bands[j]); + } + return 0; + } + } + + /* Extract bands from image */ + if (imIn->bands == 2) { + for (y = 0; y < imIn->ysize; y++) { + UINT8 *in = (UINT8 *)imIn->image[y]; + UINT8 *out0 = bands[0]->image8[y]; + UINT8 *out1 = bands[1]->image8[y]; + x = 0; + for (; x < imIn->xsize - 3; x += 4) { + UINT32 v = MAKE_UINT32(in[0], in[4], in[8], in[12]); + memcpy(out0 + x, &v, sizeof(v)); + v = MAKE_UINT32(in[0 + 3], in[4 + 3], in[8 + 3], in[12 + 3]); + memcpy(out1 + x, &v, sizeof(v)); + in += 16; + } + for (; x < imIn->xsize; x++) { + out0[x] = in[0]; + out1[x] = in[3]; + in += 4; + } + } + } else if (imIn->bands == 3) { + for (y = 0; y < imIn->ysize; y++) { + UINT8 *in = (UINT8 *)imIn->image[y]; + UINT8 *out0 = bands[0]->image8[y]; + UINT8 *out1 = bands[1]->image8[y]; + UINT8 *out2 = bands[2]->image8[y]; + x = 0; + for (; x < imIn->xsize - 3; x += 4) { + UINT32 v = MAKE_UINT32(in[0], in[4], in[8], in[12]); + memcpy(out0 + x, &v, sizeof(v)); + v = MAKE_UINT32(in[0 + 1], in[4 + 1], in[8 + 1], in[12 + 1]); + memcpy(out1 + x, &v, sizeof(v)); + v = MAKE_UINT32(in[0 + 2], in[4 + 2], in[8 + 2], in[12 + 2]); + memcpy(out2 + x, &v, sizeof(v)); + in += 16; + } + for (; x < imIn->xsize; x++) { + out0[x] = in[0]; + out1[x] = in[1]; + out2[x] = in[2]; + in += 4; + } + } + } else { + for (y = 0; y < imIn->ysize; y++) { + UINT8 *in = (UINT8 *)imIn->image[y]; + UINT8 *out0 = bands[0]->image8[y]; + UINT8 *out1 = bands[1]->image8[y]; + UINT8 *out2 = bands[2]->image8[y]; + UINT8 *out3 = bands[3]->image8[y]; + x = 0; + for (; x < imIn->xsize - 3; x += 4) { + UINT32 v = MAKE_UINT32(in[0], in[4], in[8], in[12]); + memcpy(out0 + x, &v, sizeof(v)); + v = MAKE_UINT32(in[0 + 1], in[4 + 1], in[8 + 1], in[12 + 1]); + memcpy(out1 + x, &v, sizeof(v)); + v = MAKE_UINT32(in[0 + 2], in[4 + 2], in[8 + 2], in[12 + 2]); + memcpy(out2 + x, &v, sizeof(v)); + v = MAKE_UINT32(in[0 + 3], in[4 + 3], in[8 + 3], in[12 + 3]); + memcpy(out3 + x, &v, sizeof(v)); + in += 16; + } + for (; x < imIn->xsize; x++) { + out0[x] = in[0]; + out1[x] = in[1]; + out2[x] = in[2]; + out3[x] = in[3]; + in += 4; + } + } + } + + return imIn->bands; +} + +Imaging +ImagingPutBand(Imaging imOut, Imaging imIn, int band) { + int x, y; + + /* Check arguments */ + if (!imIn || imIn->bands != 1 || !imOut) { + return (Imaging)ImagingError_ModeError(); + } + + if (band < 0 || band >= imOut->bands) { + return (Imaging)ImagingError_ValueError("band index out of range"); + } + + if (imIn->type != imOut->type || imIn->xsize != imOut->xsize || + imIn->ysize != imOut->ysize) { + return (Imaging)ImagingError_Mismatch(); + } + + /* Shortcuts */ + if (imOut->bands == 1) { + return ImagingCopy2(imOut, imIn); + } + + /* Special case for LXXA etc */ + if (imOut->bands == 2 && band == 1) { + band = 3; + } + + /* Insert band into image */ + for (y = 0; y < imIn->ysize; y++) { + UINT8 *in = imIn->image8[y]; + UINT8 *out = (UINT8 *)imOut->image[y] + band; + for (x = 0; x < imIn->xsize; x++) { + *out = in[x]; + out += 4; + } + } + + return imOut; +} + +Imaging +ImagingFillBand(Imaging imOut, int band, int color) { + int x, y; + + /* Check arguments */ + if (!imOut || imOut->type != IMAGING_TYPE_UINT8) { + return (Imaging)ImagingError_ModeError(); + } + + if (band < 0 || band >= imOut->bands) { + return (Imaging)ImagingError_ValueError("band index out of range"); + } + + /* Special case for LXXA etc */ + if (imOut->bands == 2 && band == 1) { + band = 3; + } + + color = CLIP8(color); + + /* Insert color into image */ + for (y = 0; y < imOut->ysize; y++) { + UINT8 *out = (UINT8 *)imOut->image[y] + band; + for (x = 0; x < imOut->xsize; x++) { + *out = (UINT8)color; + out += 4; + } + } + + return imOut; +} + +Imaging +ImagingMerge(const char *mode, Imaging bands[4]) { + int i, x, y; + int bandsCount = 0; + Imaging imOut; + Imaging firstBand; + + firstBand = bands[0]; + if (!firstBand) { + return (Imaging)ImagingError_ValueError("wrong number of bands"); + } + + for (i = 0; i < 4; ++i) { + if (!bands[i]) { + break; + } + if (bands[i]->bands != 1) { + return (Imaging)ImagingError_ModeError(); + } + if (bands[i]->xsize != firstBand->xsize || + bands[i]->ysize != firstBand->ysize) { + return (Imaging)ImagingError_Mismatch(); + } + } + bandsCount = i; + + imOut = ImagingNewDirty(mode, firstBand->xsize, firstBand->ysize); + if (!imOut) { + return NULL; + } + + if (imOut->bands != bandsCount) { + ImagingDelete(imOut); + return (Imaging)ImagingError_ValueError("wrong number of bands"); + } + + if (imOut->bands == 1) { + return ImagingCopy2(imOut, firstBand); + } + + if (imOut->bands == 2) { + for (y = 0; y < imOut->ysize; y++) { + UINT8 *in0 = bands[0]->image8[y]; + UINT8 *in1 = bands[1]->image8[y]; + UINT32 *out = (UINT32 *)imOut->image32[y]; + for (x = 0; x < imOut->xsize; x++) { + out[x] = MAKE_UINT32(in0[x], 0, 0, in1[x]); + } + } + } else if (imOut->bands == 3) { + for (y = 0; y < imOut->ysize; y++) { + UINT8 *in0 = bands[0]->image8[y]; + UINT8 *in1 = bands[1]->image8[y]; + UINT8 *in2 = bands[2]->image8[y]; + UINT32 *out = (UINT32 *)imOut->image32[y]; + for (x = 0; x < imOut->xsize; x++) { + out[x] = MAKE_UINT32(in0[x], in1[x], in2[x], 0); + } + } + } else if (imOut->bands == 4) { + for (y = 0; y < imOut->ysize; y++) { + UINT8 *in0 = bands[0]->image8[y]; + UINT8 *in1 = bands[1]->image8[y]; + UINT8 *in2 = bands[2]->image8[y]; + UINT8 *in3 = bands[3]->image8[y]; + UINT32 *out = (UINT32 *)imOut->image32[y]; + for (x = 0; x < imOut->xsize; x++) { + out[x] = MAKE_UINT32(in0[x], in1[x], in2[x], in3[x]); + } + } + } + + return imOut; +} diff --git a/src/libImaging/Bcn.h b/src/libImaging/Bcn.h index 1a6fbee45..42b9b3cf5 100644 --- a/src/libImaging/Bcn.h +++ b/src/libImaging/Bcn.h @@ -1,3 +1,3 @@ -typedef struct { - char *pixel_format; -} BCNSTATE; +typedef struct { + char *pixel_format; +} BCNSTATE; diff --git a/src/libImaging/BcnDecode.c b/src/libImaging/BcnDecode.c index 72f478d8d..2c09aef2e 100644 --- a/src/libImaging/BcnDecode.c +++ b/src/libImaging/BcnDecode.c @@ -1,899 +1,899 @@ -/* - * The Python Imaging Library - * - * decoder for DXTn-compressed data - * - * Format documentation: - * https://web.archive.org/web/20170802060935/http://oss.sgi.com/projects/ogl-sample/registry/EXT/texture_compression_s3tc.txt - * - * The contents of this file are in the public domain (CC0) - * Full text of the CC0 license: - * https://creativecommons.org/publicdomain/zero/1.0/ - */ - -#include "Imaging.h" - -#include "Bcn.h" - -typedef struct { - UINT8 r, g, b, a; -} rgba; - -typedef struct { - UINT8 l; -} lum; - -typedef struct { - UINT16 c0, c1; - UINT32 lut; -} bc1_color; - -typedef struct { - UINT8 a0, a1; - UINT8 lut[6]; -} bc3_alpha; - -typedef struct { - INT8 a0, a1; - UINT8 lut[6]; -} bc5s_alpha; - -#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 -decode_565(UINT16 x) { - rgba c; - int r, g, b; - r = (x & 0xf800) >> 8; - r |= r >> 5; - c.r = r; - g = (x & 0x7e0) >> 3; - g |= g >> 6; - c.g = g; - b = (x & 0x1f) << 3; - b |= b >> 5; - c.b = b; - c.a = 0xff; - return c; -} - -static void -decode_bc1_color(rgba *dst, const UINT8 *src, int separate_alpha) { - bc1_color col; - rgba p[4]; - int n, cw; - UINT16 r0, g0, b0, r1, g1, b1; - bc1_color_load(&col, src); - - p[0] = decode_565(col.c0); - r0 = p[0].r; - g0 = p[0].g; - b0 = p[0].b; - p[1] = decode_565(col.c1); - r1 = p[1].r; - g1 = p[1].g; - b1 = p[1].b; - - /* NOTE: BC2 and BC3 reuse BC1 color blocks but always act like c0 > c1 */ - if (col.c0 > col.c1 || separate_alpha) { - p[2].r = (2 * r0 + 1 * r1) / 3; - p[2].g = (2 * g0 + 1 * g1) / 3; - p[2].b = (2 * b0 + 1 * b1) / 3; - p[2].a = 0xff; - p[3].r = (1 * r0 + 2 * r1) / 3; - p[3].g = (1 * g0 + 2 * g1) / 3; - p[3].b = (1 * b0 + 2 * b1) / 3; - p[3].a = 0xff; - } else { - p[2].r = (r0 + r1) / 2; - p[2].g = (g0 + g1) / 2; - p[2].b = (b0 + b1) / 2; - p[2].a = 0xff; - p[3].r = 0; - p[3].g = 0; - p[3].b = 0; - p[3].a = 0; - } - for (n = 0; n < 16; n++) { - cw = 3 & (col.lut >> (2 * n)); - dst[n] = p[cw]; - } -} - -static void -decode_bc3_alpha(char *dst, const UINT8 *src, int stride, int o, int sign) { - UINT16 a0, a1; - UINT8 a[8]; - int n, lut1, lut2, aw; - if (sign == 1) { - bc5s_alpha b; - memcpy(&b, src, sizeof(bc5s_alpha)); - a0 = b.a0 + 128; - a1 = b.a1 + 128; - lut1 = b.lut[0] | (b.lut[1] << 8) | (b.lut[2] << 16); - lut2 = b.lut[3] | (b.lut[4] << 8) | (b.lut[5] << 16); - } else { - bc3_alpha b; - memcpy(&b, src, sizeof(bc3_alpha)); - a0 = b.a0; - a1 = b.a1; - lut1 = b.lut[0] | (b.lut[1] << 8) | (b.lut[2] << 16); - lut2 = b.lut[3] | (b.lut[4] << 8) | (b.lut[5] << 16); - } - - a[0] = (UINT8)a0; - a[1] = (UINT8)a1; - if (a0 > a1) { - a[2] = (6 * a0 + 1 * a1) / 7; - a[3] = (5 * a0 + 2 * a1) / 7; - a[4] = (4 * a0 + 3 * a1) / 7; - a[5] = (3 * a0 + 4 * a1) / 7; - a[6] = (2 * a0 + 5 * a1) / 7; - a[7] = (1 * a0 + 6 * a1) / 7; - } else { - a[2] = (4 * a0 + 1 * a1) / 5; - a[3] = (3 * a0 + 2 * a1) / 5; - a[4] = (2 * a0 + 3 * a1) / 5; - a[5] = (1 * a0 + 4 * a1) / 5; - a[6] = 0; - a[7] = 0xff; - } - for (n = 0; n < 8; n++) { - aw = 7 & (lut1 >> (3 * n)); - dst[stride * n + o] = a[aw]; - } - for (n = 0; n < 8; n++) { - aw = 7 & (lut2 >> (3 * n)); - dst[stride * (8 + n) + o] = a[aw]; - } -} - -static void -decode_bc1_block(rgba *col, const UINT8 *src) { - decode_bc1_color(col, src, 0); -} - -static void -decode_bc2_block(rgba *col, const UINT8 *src) { - int n, bitI, byI, av; - decode_bc1_color(col, src + 8, 1); - for (n = 0; n < 16; n++) { - bitI = n * 4; - byI = bitI >> 3; - av = 0xf & (src[byI] >> (bitI & 7)); - av = (av << 4) | av; - col[n].a = av; - } -} - -static void -decode_bc3_block(rgba *col, const UINT8 *src) { - decode_bc1_color(col, src + 8, 1); - decode_bc3_alpha((char *)col, src, sizeof(col[0]), 3, 0); -} - -static void -decode_bc4_block(lum *col, const UINT8 *src) { - decode_bc3_alpha((char *)col, src, sizeof(col[0]), 0, 0); -} - -static void -decode_bc5_block(rgba *col, const UINT8 *src, int sign) { - decode_bc3_alpha((char *)col, src, sizeof(col[0]), 0, sign); - decode_bc3_alpha((char *)col, src + 8, sizeof(col[0]), 1, sign); -} - -/* BC6 and BC7 are described here: - https://www.khronos.org/registry/OpenGL/extensions/ARB/ARB_texture_compression_bptc.txt - */ - -static UINT8 -get_bit(const UINT8 *src, int bit) { - int by = bit >> 3; - bit &= 7; - return (src[by] >> bit) & 1; -} - -static UINT8 -get_bits(const UINT8 *src, int bit, int count) { - UINT8 v; - int x; - int by = bit >> 3; - bit &= 7; - if (!count) { - return 0; - } - if (bit + count <= 8) { - v = (src[by] >> bit) & ((1 << count) - 1); - } else { - x = src[by] | (src[by + 1] << 8); - v = (x >> bit) & ((1 << count) - 1); - } - return v; -} - -/* BC7 */ -typedef struct { - char ns; - char pb; - char rb; - char isb; - char cb; - char ab; - char epb; - char spb; - char ib; - char ib2; -} bc7_mode_info; - -static const bc7_mode_info bc7_modes[] = { - {3, 4, 0, 0, 4, 0, 1, 0, 3, 0}, - {2, 6, 0, 0, 6, 0, 0, 1, 3, 0}, - {3, 6, 0, 0, 5, 0, 0, 0, 2, 0}, - {2, 6, 0, 0, 7, 0, 1, 0, 2, 0}, - {1, 0, 2, 1, 5, 6, 0, 0, 2, 3}, - {1, 0, 2, 0, 7, 8, 0, 0, 2, 2}, - {1, 0, 0, 0, 7, 7, 1, 0, 4, 0}, - {2, 6, 0, 0, 5, 5, 1, 0, 2, 0}}; - -/* Subset indices: - Table.P2, 1 bit per index */ -static const UINT16 bc7_si2[] = { - 0xcccc, 0x8888, 0xeeee, 0xecc8, 0xc880, 0xfeec, 0xfec8, 0xec80, 0xc800, 0xffec, - 0xfe80, 0xe800, 0xffe8, 0xff00, 0xfff0, 0xf000, 0xf710, 0x008e, 0x7100, 0x08ce, - 0x008c, 0x7310, 0x3100, 0x8cce, 0x088c, 0x3110, 0x6666, 0x366c, 0x17e8, 0x0ff0, - 0x718e, 0x399c, 0xaaaa, 0xf0f0, 0x5a5a, 0x33cc, 0x3c3c, 0x55aa, 0x9696, 0xa55a, - 0x73ce, 0x13c8, 0x324c, 0x3bdc, 0x6996, 0xc33c, 0x9966, 0x0660, 0x0272, 0x04e4, - 0x4e40, 0x2720, 0xc936, 0x936c, 0x39c6, 0x639c, 0x9336, 0x9cc6, 0x817e, 0xe718, - 0xccf0, 0x0fcc, 0x7744, 0xee22}; - -/* Table.P3, 2 bits per index */ -static const UINT32 bc7_si3[] = { - 0xaa685050, 0x6a5a5040, 0x5a5a4200, 0x5450a0a8, 0xa5a50000, 0xa0a05050, 0x5555a0a0, - 0x5a5a5050, 0xaa550000, 0xaa555500, 0xaaaa5500, 0x90909090, 0x94949494, 0xa4a4a4a4, - 0xa9a59450, 0x2a0a4250, 0xa5945040, 0x0a425054, 0xa5a5a500, 0x55a0a0a0, 0xa8a85454, - 0x6a6a4040, 0xa4a45000, 0x1a1a0500, 0x0050a4a4, 0xaaa59090, 0x14696914, 0x69691400, - 0xa08585a0, 0xaa821414, 0x50a4a450, 0x6a5a0200, 0xa9a58000, 0x5090a0a8, 0xa8a09050, - 0x24242424, 0x00aa5500, 0x24924924, 0x24499224, 0x50a50a50, 0x500aa550, 0xaaaa4444, - 0x66660000, 0xa5a0a5a0, 0x50a050a0, 0x69286928, 0x44aaaa44, 0x66666600, 0xaa444444, - 0x54a854a8, 0x95809580, 0x96969600, 0xa85454a8, 0x80959580, 0xaa141414, 0x96960000, - 0xaaaa1414, 0xa05050a0, 0xa0a5a5a0, 0x96000000, 0x40804080, 0xa9a8a9a8, 0xaaaaaa44, - 0x2a4a5254}; - -/* Anchor indices: - Table.A2 */ -static const char bc7_ai0[] = { - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 2, 8, 2, 2, 8, - 8, 15, 2, 8, 2, 2, 8, 8, 2, 2, 15, 15, 6, 8, 2, 8, 15, 15, 2, 8, 2, 2, - 2, 15, 15, 6, 6, 2, 6, 8, 15, 15, 2, 2, 15, 15, 15, 15, 15, 2, 2, 15}; - -/* Table.A3a */ -static const char bc7_ai1[] = { - 3, 3, 15, 15, 8, 3, 15, 15, 8, 8, 6, 6, 6, 5, 3, 3, 3, 3, 8, 15, 3, 3, - 6, 10, 5, 8, 8, 6, 8, 5, 15, 15, 8, 15, 3, 5, 6, 10, 8, 15, 15, 3, 15, 5, - 15, 15, 15, 15, 3, 15, 5, 5, 5, 8, 5, 10, 5, 10, 8, 13, 15, 12, 3, 3}; - -/* Table.A3b */ -static const char bc7_ai2[] = {15, 8, 8, 3, 15, 15, 3, 8, 15, 15, 15, 15, 15, - 15, 15, 8, 15, 8, 15, 3, 15, 8, 15, 8, 3, 15, - 6, 10, 15, 15, 10, 8, 15, 3, 15, 10, 10, 8, 9, - 10, 6, 15, 8, 15, 3, 6, 6, 8, 15, 3, 15, 15, - 15, 15, 15, 15, 15, 15, 15, 15, 3, 15, 15, 8}; - -/* Interpolation weights */ -static const char bc7_weights2[] = {0, 21, 43, 64}; -static const char bc7_weights3[] = {0, 9, 18, 27, 37, 46, 55, 64}; -static const char bc7_weights4[] = { - 0, 4, 9, 13, 17, 21, 26, 30, 34, 38, 43, 47, 51, 55, 60, 64}; - -static const char * -bc7_get_weights(int n) { - if (n == 2) { - return bc7_weights2; - } - if (n == 3) { - return bc7_weights3; - } - return bc7_weights4; -} - -static int -bc7_get_subset(int ns, int partition, int n) { - if (ns == 2) { - return 1 & (bc7_si2[partition] >> n); - } - if (ns == 3) { - return 3 & (bc7_si3[partition] >> (2 * n)); - } - return 0; -} - -static UINT8 -expand_quantized(UINT8 v, int bits) { - v = v << (8 - bits); - return v | (v >> bits); -} - -static void -bc7_lerp(rgba *dst, const rgba *e, int s0, int s1) { - int t0 = 64 - s0; - int t1 = 64 - s1; - dst->r = (UINT8)((t0 * e[0].r + s0 * e[1].r + 32) >> 6); - dst->g = (UINT8)((t0 * e[0].g + s0 * e[1].g + 32) >> 6); - dst->b = (UINT8)((t0 * e[0].b + s0 * e[1].b + 32) >> 6); - dst->a = (UINT8)((t1 * e[0].a + s1 * e[1].a + 32) >> 6); -} - -static void -decode_bc7_block(rgba *col, const UINT8 *src) { - rgba endpoints[6]; - int bit = 0, cibit, aibit; - int mode = src[0]; - int i, j; - int numep, cb, ab, ib, ib2, i0, i1, s; - UINT8 index_sel, partition, rotation, val; - const char *cw, *aw; - const bc7_mode_info *info; - - /* mode is the number of unset bits before the first set bit: */ - if (!mode) { - /* degenerate case when no bits set */ - for (i = 0; i < 16; i++) { - col[i].r = col[i].g = col[i].b = 0; - col[i].a = 255; - } - return; - } - while (!(mode & (1 << bit++))); - mode = bit - 1; - info = &bc7_modes[mode]; - /* color selection bits: {subset}{endpoint} */ - cb = info->cb; - ab = info->ab; - cw = bc7_get_weights(info->ib); - aw = bc7_get_weights((ab && info->ib2) ? info->ib2 : info->ib); - -#define LOAD(DST, N) \ - DST = get_bits(src, bit, N); \ - bit += N; - LOAD(partition, info->pb); - LOAD(rotation, info->rb); - LOAD(index_sel, info->isb); - numep = info->ns << 1; - - /* red */ - for (i = 0; i < numep; i++) { - LOAD(val, cb); - endpoints[i].r = val; - } - - /* green */ - for (i = 0; i < numep; i++) { - LOAD(val, cb); - endpoints[i].g = val; - } - - /* blue */ - for (i = 0; i < numep; i++) { - LOAD(val, cb); - endpoints[i].b = val; - } - - /* alpha */ - for (i = 0; i < numep; i++) { - if (ab) { - LOAD(val, ab); - } else { - val = 255; - } - endpoints[i].a = val; - } - - /* p-bits */ -#define ASSIGN_P(x) x = (x << 1) | val - if (info->epb) { - /* per endpoint */ - cb++; - if (ab) { - ab++; - } - for (i = 0; i < numep; i++) { - LOAD(val, 1); - ASSIGN_P(endpoints[i].r); - ASSIGN_P(endpoints[i].g); - ASSIGN_P(endpoints[i].b); - if (ab) { - ASSIGN_P(endpoints[i].a); - } - } - } - if (info->spb) { - /* per subset */ - cb++; - if (ab) { - ab++; - } - for (i = 0; i < numep; i += 2) { - LOAD(val, 1); - for (j = 0; j < 2; j++) { - ASSIGN_P(endpoints[i + j].r); - ASSIGN_P(endpoints[i + j].g); - ASSIGN_P(endpoints[i + j].b); - if (ab) { - ASSIGN_P(endpoints[i + j].a); - } - } - } - } -#undef ASSIGN_P -#define EXPAND(x, b) x = expand_quantized(x, b) - for (i = 0; i < numep; i++) { - EXPAND(endpoints[i].r, cb); - EXPAND(endpoints[i].g, cb); - EXPAND(endpoints[i].b, cb); - if (ab) { - EXPAND(endpoints[i].a, ab); - } - } -#undef EXPAND -#undef LOAD - cibit = bit; - aibit = cibit + 16 * info->ib - info->ns; - for (i = 0; i < 16; i++) { - s = bc7_get_subset(info->ns, partition, i) << 1; - ib = info->ib; - if (i == 0) { - ib--; - } else if (info->ns == 2) { - if (i == bc7_ai0[partition]) { - ib--; - } - } else if (info->ns == 3) { - if (i == bc7_ai1[partition]) { - ib--; - } else if (i == bc7_ai2[partition]) { - ib--; - } - } - i0 = get_bits(src, cibit, ib); - cibit += ib; - - if (ab && info->ib2) { - ib2 = info->ib2; - if (ib2 && i == 0) { - ib2--; - } - i1 = get_bits(src, aibit, ib2); - aibit += ib2; - if (index_sel) { - bc7_lerp(&col[i], &endpoints[s], aw[i1], cw[i0]); - } else { - bc7_lerp(&col[i], &endpoints[s], cw[i0], aw[i1]); - } - } else { - bc7_lerp(&col[i], &endpoints[s], cw[i0], cw[i0]); - } -#define ROTATE(x, y) \ - val = x; \ - x = y; \ - y = val - if (rotation == 1) { - ROTATE(col[i].r, col[i].a); - } else if (rotation == 2) { - ROTATE(col[i].g, col[i].a); - } else if (rotation == 3) { - ROTATE(col[i].b, col[i].a); - } -#undef ROTATE - } -} - -/* BC6 */ -typedef struct { - char ns; /* number of subsets (also called regions) */ - char tr; /* whether endpoints are delta-compressed */ - char pb; /* partition bits */ - char epb; /* endpoint bits */ - char rb; /* red bits (delta) */ - char gb; /* green bits (delta) */ - char bb; /* blue bits (delta) */ -} bc6_mode_info; - -static const bc6_mode_info bc6_modes[] = { - // 00 - {2, 1, 5, 10, 5, 5, 5}, - // 01 - {2, 1, 5, 7, 6, 6, 6}, - // 10 - {2, 1, 5, 11, 5, 4, 4}, - {2, 1, 5, 11, 4, 5, 4}, - {2, 1, 5, 11, 4, 4, 5}, - {2, 1, 5, 9, 5, 5, 5}, - {2, 1, 5, 8, 6, 5, 5}, - {2, 1, 5, 8, 5, 6, 5}, - {2, 1, 5, 8, 5, 5, 6}, - {2, 0, 5, 6, 6, 6, 6}, - // 11 - {1, 0, 0, 10, 10, 10, 10}, - {1, 1, 0, 11, 9, 9, 9}, - {1, 1, 0, 12, 8, 8, 8}, - {1, 1, 0, 16, 4, 4, 4}}; - -/* Table.F, encoded as a sequence of bit indices */ -static const UINT8 bc6_bit_packings[][75] = { - {116, 132, 180, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, - 18, 19, 20, 21, 22, 23, 24, 25, 32, 33, 34, 35, 36, 37, 38, - 39, 40, 41, 48, 49, 50, 51, 52, 164, 112, 113, 114, 115, 64, 65, - 66, 67, 68, 176, 160, 161, 162, 163, 80, 81, 82, 83, 84, 177, 128, - 129, 130, 131, 96, 97, 98, 99, 100, 178, 144, 145, 146, 147, 148, 179}, - {117, 164, 165, 0, 1, 2, 3, 4, 5, 6, 176, 177, 132, 16, 17, - 18, 19, 20, 21, 22, 133, 178, 116, 32, 33, 34, 35, 36, 37, 38, - 179, 181, 180, 48, 49, 50, 51, 52, 53, 112, 113, 114, 115, 64, 65, - 66, 67, 68, 69, 160, 161, 162, 163, 80, 81, 82, 83, 84, 85, 128, - 129, 130, 131, 96, 97, 98, 99, 100, 101, 144, 145, 146, 147, 148, 149}, - {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, - 21, 22, 23, 24, 25, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, - 48, 49, 50, 51, 52, 10, 112, 113, 114, 115, 64, 65, 66, 67, 26, - 176, 160, 161, 162, 163, 80, 81, 82, 83, 42, 177, 128, 129, 130, 131, - 96, 97, 98, 99, 100, 178, 144, 145, 146, 147, 148, 179}, - {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, - 21, 22, 23, 24, 25, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, - 48, 49, 50, 51, 10, 164, 112, 113, 114, 115, 64, 65, 66, 67, 68, - 26, 160, 161, 162, 163, 80, 81, 82, 83, 42, 177, 128, 129, 130, 131, - 96, 97, 98, 99, 176, 178, 144, 145, 146, 147, 116, 179}, - {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, - 21, 22, 23, 24, 25, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, - 48, 49, 50, 51, 10, 132, 112, 113, 114, 115, 64, 65, 66, 67, 26, - 176, 160, 161, 162, 163, 80, 81, 82, 83, 84, 42, 128, 129, 130, 131, - 96, 97, 98, 99, 177, 178, 144, 145, 146, 147, 180, 179}, - {0, 1, 2, 3, 4, 5, 6, 7, 8, 132, 16, 17, 18, 19, 20, - 21, 22, 23, 24, 116, 32, 33, 34, 35, 36, 37, 38, 39, 40, 180, - 48, 49, 50, 51, 52, 164, 112, 113, 114, 115, 64, 65, 66, 67, 68, - 176, 160, 161, 162, 163, 80, 81, 82, 83, 84, 177, 128, 129, 130, 131, - 96, 97, 98, 99, 100, 178, 144, 145, 146, 147, 148, 179}, - {0, 1, 2, 3, 4, 5, 6, 7, 164, 132, 16, 17, 18, 19, 20, - 21, 22, 23, 178, 116, 32, 33, 34, 35, 36, 37, 38, 39, 179, 180, - 48, 49, 50, 51, 52, 53, 112, 113, 114, 115, 64, 65, 66, 67, 68, - 176, 160, 161, 162, 163, 80, 81, 82, 83, 84, 177, 128, 129, 130, 131, - 96, 97, 98, 99, 100, 101, 144, 145, 146, 147, 148, 149}, - {0, 1, 2, 3, 4, 5, 6, 7, 176, 132, 16, 17, 18, 19, 20, - 21, 22, 23, 117, 116, 32, 33, 34, 35, 36, 37, 38, 39, 165, 180, - 48, 49, 50, 51, 52, 164, 112, 113, 114, 115, 64, 65, 66, 67, 68, - 69, 160, 161, 162, 163, 80, 81, 82, 83, 84, 177, 128, 129, 130, 131, - 96, 97, 98, 99, 100, 178, 144, 145, 146, 147, 148, 179}, - {0, 1, 2, 3, 4, 5, 6, 7, 177, 132, 16, 17, 18, 19, 20, - 21, 22, 23, 133, 116, 32, 33, 34, 35, 36, 37, 38, 39, 181, 180, - 48, 49, 50, 51, 52, 164, 112, 113, 114, 115, 64, 65, 66, 67, 68, - 176, 160, 161, 162, 163, 80, 81, 82, 83, 84, 85, 128, 129, 130, 131, - 96, 97, 98, 99, 100, 178, 144, 145, 146, 147, 148, 179}, - {0, 1, 2, 3, 4, 5, 164, 176, 177, 132, 16, 17, 18, 19, 20, - 21, 117, 133, 178, 116, 32, 33, 34, 35, 36, 37, 165, 179, 181, 180, - 48, 49, 50, 51, 52, 53, 112, 113, 114, 115, 64, 65, 66, 67, 68, - 69, 160, 161, 162, 163, 80, 81, 82, 83, 84, 85, 128, 129, 130, 131, - 96, 97, 98, 99, 100, 101, 144, 145, 146, 147, 148, 149}, - {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, - 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, - 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89}, - {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, - 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 48, 49, 50, 51, 52, 53, 54, 55, 56, 10, - 64, 65, 66, 67, 68, 69, 70, 71, 72, 26, 80, 81, 82, 83, 84, 85, 86, 87, 88, 42}, - {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, - 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 48, 49, 50, 51, 52, 53, 54, 55, 11, 10, - 64, 65, 66, 67, 68, 69, 70, 71, 27, 26, 80, 81, 82, 83, 84, 85, 86, 87, 43, 42}, - {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, - 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 48, 49, 50, 51, 15, 14, 13, 12, 11, 10, - 64, 65, 66, 67, 31, 30, 29, 28, 27, 26, 80, 81, 82, 83, 47, 46, 45, 44, 43, 42}}; - -static void -bc6_sign_extend(UINT16 *v, int prec) { - int x = *v; - if (x & (1 << (prec - 1))) { - x |= -1 << prec; - } - *v = (UINT16)x; -} - -static int -bc6_unquantize(UINT16 v, int prec, int sign) { - int s = 0; - int x; - if (!sign) { - x = v; - if (prec >= 15) { - return x; - } - if (x == 0) { - return 0; - } - if (x == ((1 << prec) - 1)) { - return 0xffff; - } - return ((x << 15) + 0x4000) >> (prec - 1); - } else { - x = (INT16)v; - if (prec >= 16) { - return x; - } - if (x < 0) { - s = 1; - x = -x; - } - - if (x != 0) { - if (x >= ((1 << (prec - 1)) - 1)) { - x = 0x7fff; - } else { - x = ((x << 15) + 0x4000) >> (prec - 1); - } - } - - if (s) { - return -x; - } - return x; - } -} - -static float -half_to_float(UINT16 h) { - /* https://gist.github.com/rygorous/2144712 */ - union { - UINT32 u; - float f; - } o, m; - m.u = 0x77800000; - o.u = (h & 0x7fff) << 13; - o.f *= m.f; - m.u = 0x47800000; - if (o.f >= m.f) { - o.u |= 255 << 23; - } - o.u |= (h & 0x8000) << 16; - return o.f; -} - -static float -bc6_finalize(int v, int sign) { - if (sign) { - if (v < 0) { - v = ((-v) * 31) / 32; - return half_to_float((UINT16)(0x8000 | v)); - } else { - return half_to_float((UINT16)((v * 31) / 32)); - } - } else { - return half_to_float((UINT16)((v * 31) / 64)); - } -} - -static UINT8 -bc6_clamp(float value) { - if (value < 0.0f) { - return 0; - } else if (value > 1.0f) { - return 255; - } else { - return (UINT8)(value * 255.0f); - } -} - -static void -bc6_lerp(rgba *col, int *e0, int *e1, int s, int sign) { - int r, g, b; - int t = 64 - s; - r = (e0[0] * t + e1[0] * s) >> 6; - g = (e0[1] * t + e1[1] * s) >> 6; - b = (e0[2] * t + e1[2] * s) >> 6; - col->r = bc6_clamp(bc6_finalize(r, sign)); - col->g = bc6_clamp(bc6_finalize(g, sign)); - col->b = bc6_clamp(bc6_finalize(b, sign)); -} - -static void -decode_bc6_block(rgba *col, const UINT8 *src, int sign) { - UINT16 endpoints[12]; /* storage for r0, g0, b0, r1, ... */ - int ueps[12]; - int i, i0, ib2, di, dw, mask, numep, s; - UINT8 partition; - const bc6_mode_info *info; - const char *cw; - int bit = 5; - int epbits = 75; - int ib = 3; - int mode = src[0] & 0x1f; - if ((mode & 3) == 0 || (mode & 3) == 1) { - mode &= 3; - bit = 2; - } else if ((mode & 3) == 2) { - mode = 2 + (mode >> 2); - epbits = 72; - } else { - mode = 10 + (mode >> 2); - epbits = 60; - ib = 4; - } - if (mode >= 14) { - /* invalid block */ - memset(col, 0, 16 * sizeof(col[0])); - return; - } - info = &bc6_modes[mode]; - cw = bc7_get_weights(ib); - numep = info->ns == 2 ? 12 : 6; - for (i = 0; i < 12; i++) { - endpoints[i] = 0; - } - for (i = 0; i < epbits; i++) { - di = bc6_bit_packings[mode][i]; - dw = di >> 4; - di &= 15; - endpoints[dw] |= (UINT16)get_bit(src, bit + i) << di; - } - bit += epbits; - partition = get_bits(src, bit, info->pb); - bit += info->pb; - mask = (1 << info->epb) - 1; - if (sign) { /* sign-extend e0 if signed */ - bc6_sign_extend(&endpoints[0], info->epb); - bc6_sign_extend(&endpoints[1], info->epb); - bc6_sign_extend(&endpoints[2], info->epb); - } - if (sign || info->tr) { /* sign-extend e1,2,3 if signed or deltas */ - for (i = 3; i < numep; i += 3) { - bc6_sign_extend(&endpoints[i], info->rb); - bc6_sign_extend(&endpoints[i + 1], info->gb); - bc6_sign_extend(&endpoints[i + 2], info->bb); - } - } - if (info->tr) { /* apply deltas */ - for (i = 3; i < numep; i += 3) { - endpoints[i] = (endpoints[i] + endpoints[0]) & mask; - endpoints[i + 1] = (endpoints[i + 1] + endpoints[1]) & mask; - endpoints[i + 2] = (endpoints[i + 2] + endpoints[2]) & mask; - } - } - for (i = 0; i < numep; i++) { - ueps[i] = bc6_unquantize(endpoints[i], info->epb, sign); - } - for (i = 0; i < 16; i++) { - s = bc7_get_subset(info->ns, partition, i) * 6; - ib2 = ib; - if (i == 0) { - ib2--; - } else if (info->ns == 2) { - if (i == bc7_ai0[partition]) { - ib2--; - } - } - i0 = get_bits(src, bit, ib2); - bit += ib2; - - bc6_lerp(&col[i], &ueps[s], &ueps[s + 3], cw[i0], sign); - } -} - -static void -put_block(Imaging im, ImagingCodecState state, const char *col, int sz, int C) { - int width = state->xsize; - int height = state->ysize; - int xmax = width + state->xoff; - int ymax = height + state->yoff; - int j, i, y, x; - char *dst; - for (j = 0; j < 4; j++) { - y = state->y + j; - if (C) { - if (y >= height) { - continue; - } - if (state->ystep < 0) { - y = state->yoff + ymax - y - 1; - } - dst = im->image[y]; - for (i = 0; i < 4; i++) { - x = state->x + i; - if (x >= width) { - continue; - } - memcpy(dst + sz * x, col + sz * (j * 4 + i), sz); - } - } else { - if (state->ystep < 0) { - y = state->yoff + ymax - y - 1; - } - x = state->x; - dst = im->image[y] + sz * x; - memcpy(dst, col + sz * (j * 4), 4 * sz); - } - } - state->x += 4; - if (state->x >= xmax) { - state->y += 4; - state->x = state->xoff; - } -} - -static int -decode_bcn( - Imaging im, - ImagingCodecState state, - const UINT8 *src, - int bytes, - int N, - int C, - char *pixel_format) { - int ymax = state->ysize + state->yoff; - const UINT8 *ptr = src; - switch (N) { -#define DECODE_LOOP(NN, SZ, TY, ...) \ - case NN: \ - while (bytes >= SZ) { \ - TY col[16]; \ - memset(col, 0, 16 * sizeof(col[0])); \ - decode_bc##NN##_block(col, ptr); \ - put_block(im, state, (const char *)col, sizeof(col[0]), C); \ - ptr += SZ; \ - bytes -= SZ; \ - if (state->y >= ymax) { \ - return -1; \ - } \ - } \ - break - - DECODE_LOOP(1, 8, rgba); - DECODE_LOOP(2, 16, rgba); - DECODE_LOOP(3, 16, rgba); - DECODE_LOOP(4, 8, lum); - case 5: { - int sign = strcmp(pixel_format, "BC5S") == 0 ? 1 : 0; - while (bytes >= 16) { - rgba col[16]; - memset(col, sign ? 128 : 0, 16 * sizeof(col[0])); - decode_bc5_block(col, ptr, sign); - put_block(im, state, (const char *)col, sizeof(col[0]), C); - ptr += 16; - bytes -= 16; - if (state->y >= ymax) { - return -1; - } - } - break; - } - case 6: { - int sign = strcmp(pixel_format, "BC6HS") == 0 ? 1 : 0; - while (bytes >= 16) { - rgba col[16]; - decode_bc6_block(col, ptr, sign); - put_block(im, state, (const char *)col, sizeof(col[0]), C); - ptr += 16; - bytes -= 16; - if (state->y >= ymax) { - return -1; - } - } - break; - } - DECODE_LOOP(7, 16, rgba); -#undef DECODE_LOOP - } - return (int)(ptr - src); -} - -int -ImagingBcnDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) { - int N = state->state & 0xf; - int width = state->xsize; - int height = state->ysize; - int C = (width & 3) | (height & 3) ? 1 : 0; - char *pixel_format = ((BCNSTATE *)state->context)->pixel_format; - return decode_bcn(im, state, buf, bytes, N, C, pixel_format); -} +/* + * The Python Imaging Library + * + * decoder for DXTn-compressed data + * + * Format documentation: + * https://web.archive.org/web/20170802060935/http://oss.sgi.com/projects/ogl-sample/registry/EXT/texture_compression_s3tc.txt + * + * The contents of this file are in the public domain (CC0) + * Full text of the CC0 license: + * https://creativecommons.org/publicdomain/zero/1.0/ + */ + +#include "Imaging.h" + +#include "Bcn.h" + +typedef struct { + UINT8 r, g, b, a; +} rgba; + +typedef struct { + UINT8 l; +} lum; + +typedef struct { + UINT16 c0, c1; + UINT32 lut; +} bc1_color; + +typedef struct { + UINT8 a0, a1; + UINT8 lut[6]; +} bc3_alpha; + +typedef struct { + INT8 a0, a1; + UINT8 lut[6]; +} bc5s_alpha; + +#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 +decode_565(UINT16 x) { + rgba c; + int r, g, b; + r = (x & 0xf800) >> 8; + r |= r >> 5; + c.r = r; + g = (x & 0x7e0) >> 3; + g |= g >> 6; + c.g = g; + b = (x & 0x1f) << 3; + b |= b >> 5; + c.b = b; + c.a = 0xff; + return c; +} + +static void +decode_bc1_color(rgba *dst, const UINT8 *src, int separate_alpha) { + bc1_color col; + rgba p[4]; + int n, cw; + UINT16 r0, g0, b0, r1, g1, b1; + bc1_color_load(&col, src); + + p[0] = decode_565(col.c0); + r0 = p[0].r; + g0 = p[0].g; + b0 = p[0].b; + p[1] = decode_565(col.c1); + r1 = p[1].r; + g1 = p[1].g; + b1 = p[1].b; + + /* NOTE: BC2 and BC3 reuse BC1 color blocks but always act like c0 > c1 */ + if (col.c0 > col.c1 || separate_alpha) { + p[2].r = (2 * r0 + 1 * r1) / 3; + p[2].g = (2 * g0 + 1 * g1) / 3; + p[2].b = (2 * b0 + 1 * b1) / 3; + p[2].a = 0xff; + p[3].r = (1 * r0 + 2 * r1) / 3; + p[3].g = (1 * g0 + 2 * g1) / 3; + p[3].b = (1 * b0 + 2 * b1) / 3; + p[3].a = 0xff; + } else { + p[2].r = (r0 + r1) / 2; + p[2].g = (g0 + g1) / 2; + p[2].b = (b0 + b1) / 2; + p[2].a = 0xff; + p[3].r = 0; + p[3].g = 0; + p[3].b = 0; + p[3].a = 0; + } + for (n = 0; n < 16; n++) { + cw = 3 & (col.lut >> (2 * n)); + dst[n] = p[cw]; + } +} + +static void +decode_bc3_alpha(char *dst, const UINT8 *src, int stride, int o, int sign) { + UINT16 a0, a1; + UINT8 a[8]; + int n, lut1, lut2, aw; + if (sign == 1) { + bc5s_alpha b; + memcpy(&b, src, sizeof(bc5s_alpha)); + a0 = b.a0 + 128; + a1 = b.a1 + 128; + lut1 = b.lut[0] | (b.lut[1] << 8) | (b.lut[2] << 16); + lut2 = b.lut[3] | (b.lut[4] << 8) | (b.lut[5] << 16); + } else { + bc3_alpha b; + memcpy(&b, src, sizeof(bc3_alpha)); + a0 = b.a0; + a1 = b.a1; + lut1 = b.lut[0] | (b.lut[1] << 8) | (b.lut[2] << 16); + lut2 = b.lut[3] | (b.lut[4] << 8) | (b.lut[5] << 16); + } + + a[0] = (UINT8)a0; + a[1] = (UINT8)a1; + if (a0 > a1) { + a[2] = (6 * a0 + 1 * a1) / 7; + a[3] = (5 * a0 + 2 * a1) / 7; + a[4] = (4 * a0 + 3 * a1) / 7; + a[5] = (3 * a0 + 4 * a1) / 7; + a[6] = (2 * a0 + 5 * a1) / 7; + a[7] = (1 * a0 + 6 * a1) / 7; + } else { + a[2] = (4 * a0 + 1 * a1) / 5; + a[3] = (3 * a0 + 2 * a1) / 5; + a[4] = (2 * a0 + 3 * a1) / 5; + a[5] = (1 * a0 + 4 * a1) / 5; + a[6] = 0; + a[7] = 0xff; + } + for (n = 0; n < 8; n++) { + aw = 7 & (lut1 >> (3 * n)); + dst[stride * n + o] = a[aw]; + } + for (n = 0; n < 8; n++) { + aw = 7 & (lut2 >> (3 * n)); + dst[stride * (8 + n) + o] = a[aw]; + } +} + +static void +decode_bc1_block(rgba *col, const UINT8 *src) { + decode_bc1_color(col, src, 0); +} + +static void +decode_bc2_block(rgba *col, const UINT8 *src) { + int n, bitI, byI, av; + decode_bc1_color(col, src + 8, 1); + for (n = 0; n < 16; n++) { + bitI = n * 4; + byI = bitI >> 3; + av = 0xf & (src[byI] >> (bitI & 7)); + av = (av << 4) | av; + col[n].a = av; + } +} + +static void +decode_bc3_block(rgba *col, const UINT8 *src) { + decode_bc1_color(col, src + 8, 1); + decode_bc3_alpha((char *)col, src, sizeof(col[0]), 3, 0); +} + +static void +decode_bc4_block(lum *col, const UINT8 *src) { + decode_bc3_alpha((char *)col, src, sizeof(col[0]), 0, 0); +} + +static void +decode_bc5_block(rgba *col, const UINT8 *src, int sign) { + decode_bc3_alpha((char *)col, src, sizeof(col[0]), 0, sign); + decode_bc3_alpha((char *)col, src + 8, sizeof(col[0]), 1, sign); +} + +/* BC6 and BC7 are described here: + https://www.khronos.org/registry/OpenGL/extensions/ARB/ARB_texture_compression_bptc.txt + */ + +static UINT8 +get_bit(const UINT8 *src, int bit) { + int by = bit >> 3; + bit &= 7; + return (src[by] >> bit) & 1; +} + +static UINT8 +get_bits(const UINT8 *src, int bit, int count) { + UINT8 v; + int x; + int by = bit >> 3; + bit &= 7; + if (!count) { + return 0; + } + if (bit + count <= 8) { + v = (src[by] >> bit) & ((1 << count) - 1); + } else { + x = src[by] | (src[by + 1] << 8); + v = (x >> bit) & ((1 << count) - 1); + } + return v; +} + +/* BC7 */ +typedef struct { + char ns; + char pb; + char rb; + char isb; + char cb; + char ab; + char epb; + char spb; + char ib; + char ib2; +} bc7_mode_info; + +static const bc7_mode_info bc7_modes[] = { + {3, 4, 0, 0, 4, 0, 1, 0, 3, 0}, + {2, 6, 0, 0, 6, 0, 0, 1, 3, 0}, + {3, 6, 0, 0, 5, 0, 0, 0, 2, 0}, + {2, 6, 0, 0, 7, 0, 1, 0, 2, 0}, + {1, 0, 2, 1, 5, 6, 0, 0, 2, 3}, + {1, 0, 2, 0, 7, 8, 0, 0, 2, 2}, + {1, 0, 0, 0, 7, 7, 1, 0, 4, 0}, + {2, 6, 0, 0, 5, 5, 1, 0, 2, 0}}; + +/* Subset indices: + Table.P2, 1 bit per index */ +static const UINT16 bc7_si2[] = { + 0xcccc, 0x8888, 0xeeee, 0xecc8, 0xc880, 0xfeec, 0xfec8, 0xec80, 0xc800, 0xffec, + 0xfe80, 0xe800, 0xffe8, 0xff00, 0xfff0, 0xf000, 0xf710, 0x008e, 0x7100, 0x08ce, + 0x008c, 0x7310, 0x3100, 0x8cce, 0x088c, 0x3110, 0x6666, 0x366c, 0x17e8, 0x0ff0, + 0x718e, 0x399c, 0xaaaa, 0xf0f0, 0x5a5a, 0x33cc, 0x3c3c, 0x55aa, 0x9696, 0xa55a, + 0x73ce, 0x13c8, 0x324c, 0x3bdc, 0x6996, 0xc33c, 0x9966, 0x0660, 0x0272, 0x04e4, + 0x4e40, 0x2720, 0xc936, 0x936c, 0x39c6, 0x639c, 0x9336, 0x9cc6, 0x817e, 0xe718, + 0xccf0, 0x0fcc, 0x7744, 0xee22}; + +/* Table.P3, 2 bits per index */ +static const UINT32 bc7_si3[] = { + 0xaa685050, 0x6a5a5040, 0x5a5a4200, 0x5450a0a8, 0xa5a50000, 0xa0a05050, 0x5555a0a0, + 0x5a5a5050, 0xaa550000, 0xaa555500, 0xaaaa5500, 0x90909090, 0x94949494, 0xa4a4a4a4, + 0xa9a59450, 0x2a0a4250, 0xa5945040, 0x0a425054, 0xa5a5a500, 0x55a0a0a0, 0xa8a85454, + 0x6a6a4040, 0xa4a45000, 0x1a1a0500, 0x0050a4a4, 0xaaa59090, 0x14696914, 0x69691400, + 0xa08585a0, 0xaa821414, 0x50a4a450, 0x6a5a0200, 0xa9a58000, 0x5090a0a8, 0xa8a09050, + 0x24242424, 0x00aa5500, 0x24924924, 0x24499224, 0x50a50a50, 0x500aa550, 0xaaaa4444, + 0x66660000, 0xa5a0a5a0, 0x50a050a0, 0x69286928, 0x44aaaa44, 0x66666600, 0xaa444444, + 0x54a854a8, 0x95809580, 0x96969600, 0xa85454a8, 0x80959580, 0xaa141414, 0x96960000, + 0xaaaa1414, 0xa05050a0, 0xa0a5a5a0, 0x96000000, 0x40804080, 0xa9a8a9a8, 0xaaaaaa44, + 0x2a4a5254}; + +/* Anchor indices: + Table.A2 */ +static const char bc7_ai0[] = { + 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 2, 8, 2, 2, 8, + 8, 15, 2, 8, 2, 2, 8, 8, 2, 2, 15, 15, 6, 8, 2, 8, 15, 15, 2, 8, 2, 2, + 2, 15, 15, 6, 6, 2, 6, 8, 15, 15, 2, 2, 15, 15, 15, 15, 15, 2, 2, 15}; + +/* Table.A3a */ +static const char bc7_ai1[] = { + 3, 3, 15, 15, 8, 3, 15, 15, 8, 8, 6, 6, 6, 5, 3, 3, 3, 3, 8, 15, 3, 3, + 6, 10, 5, 8, 8, 6, 8, 5, 15, 15, 8, 15, 3, 5, 6, 10, 8, 15, 15, 3, 15, 5, + 15, 15, 15, 15, 3, 15, 5, 5, 5, 8, 5, 10, 5, 10, 8, 13, 15, 12, 3, 3}; + +/* Table.A3b */ +static const char bc7_ai2[] = {15, 8, 8, 3, 15, 15, 3, 8, 15, 15, 15, 15, 15, + 15, 15, 8, 15, 8, 15, 3, 15, 8, 15, 8, 3, 15, + 6, 10, 15, 15, 10, 8, 15, 3, 15, 10, 10, 8, 9, + 10, 6, 15, 8, 15, 3, 6, 6, 8, 15, 3, 15, 15, + 15, 15, 15, 15, 15, 15, 15, 15, 3, 15, 15, 8}; + +/* Interpolation weights */ +static const char bc7_weights2[] = {0, 21, 43, 64}; +static const char bc7_weights3[] = {0, 9, 18, 27, 37, 46, 55, 64}; +static const char bc7_weights4[] = { + 0, 4, 9, 13, 17, 21, 26, 30, 34, 38, 43, 47, 51, 55, 60, 64}; + +static const char * +bc7_get_weights(int n) { + if (n == 2) { + return bc7_weights2; + } + if (n == 3) { + return bc7_weights3; + } + return bc7_weights4; +} + +static int +bc7_get_subset(int ns, int partition, int n) { + if (ns == 2) { + return 1 & (bc7_si2[partition] >> n); + } + if (ns == 3) { + return 3 & (bc7_si3[partition] >> (2 * n)); + } + return 0; +} + +static UINT8 +expand_quantized(UINT8 v, int bits) { + v = v << (8 - bits); + return v | (v >> bits); +} + +static void +bc7_lerp(rgba *dst, const rgba *e, int s0, int s1) { + int t0 = 64 - s0; + int t1 = 64 - s1; + dst->r = (UINT8)((t0 * e[0].r + s0 * e[1].r + 32) >> 6); + dst->g = (UINT8)((t0 * e[0].g + s0 * e[1].g + 32) >> 6); + dst->b = (UINT8)((t0 * e[0].b + s0 * e[1].b + 32) >> 6); + dst->a = (UINT8)((t1 * e[0].a + s1 * e[1].a + 32) >> 6); +} + +static void +decode_bc7_block(rgba *col, const UINT8 *src) { + rgba endpoints[6]; + int bit = 0, cibit, aibit; + int mode = src[0]; + int i, j; + int numep, cb, ab, ib, ib2, i0, i1, s; + UINT8 index_sel, partition, rotation, val; + const char *cw, *aw; + const bc7_mode_info *info; + + /* mode is the number of unset bits before the first set bit: */ + if (!mode) { + /* degenerate case when no bits set */ + for (i = 0; i < 16; i++) { + col[i].r = col[i].g = col[i].b = 0; + col[i].a = 255; + } + return; + } + while (!(mode & (1 << bit++))); + mode = bit - 1; + info = &bc7_modes[mode]; + /* color selection bits: {subset}{endpoint} */ + cb = info->cb; + ab = info->ab; + cw = bc7_get_weights(info->ib); + aw = bc7_get_weights((ab && info->ib2) ? info->ib2 : info->ib); + +#define LOAD(DST, N) \ + DST = get_bits(src, bit, N); \ + bit += N; + LOAD(partition, info->pb); + LOAD(rotation, info->rb); + LOAD(index_sel, info->isb); + numep = info->ns << 1; + + /* red */ + for (i = 0; i < numep; i++) { + LOAD(val, cb); + endpoints[i].r = val; + } + + /* green */ + for (i = 0; i < numep; i++) { + LOAD(val, cb); + endpoints[i].g = val; + } + + /* blue */ + for (i = 0; i < numep; i++) { + LOAD(val, cb); + endpoints[i].b = val; + } + + /* alpha */ + for (i = 0; i < numep; i++) { + if (ab) { + LOAD(val, ab); + } else { + val = 255; + } + endpoints[i].a = val; + } + + /* p-bits */ +#define ASSIGN_P(x) x = (x << 1) | val + if (info->epb) { + /* per endpoint */ + cb++; + if (ab) { + ab++; + } + for (i = 0; i < numep; i++) { + LOAD(val, 1); + ASSIGN_P(endpoints[i].r); + ASSIGN_P(endpoints[i].g); + ASSIGN_P(endpoints[i].b); + if (ab) { + ASSIGN_P(endpoints[i].a); + } + } + } + if (info->spb) { + /* per subset */ + cb++; + if (ab) { + ab++; + } + for (i = 0; i < numep; i += 2) { + LOAD(val, 1); + for (j = 0; j < 2; j++) { + ASSIGN_P(endpoints[i + j].r); + ASSIGN_P(endpoints[i + j].g); + ASSIGN_P(endpoints[i + j].b); + if (ab) { + ASSIGN_P(endpoints[i + j].a); + } + } + } + } +#undef ASSIGN_P +#define EXPAND(x, b) x = expand_quantized(x, b) + for (i = 0; i < numep; i++) { + EXPAND(endpoints[i].r, cb); + EXPAND(endpoints[i].g, cb); + EXPAND(endpoints[i].b, cb); + if (ab) { + EXPAND(endpoints[i].a, ab); + } + } +#undef EXPAND +#undef LOAD + cibit = bit; + aibit = cibit + 16 * info->ib - info->ns; + for (i = 0; i < 16; i++) { + s = bc7_get_subset(info->ns, partition, i) << 1; + ib = info->ib; + if (i == 0) { + ib--; + } else if (info->ns == 2) { + if (i == bc7_ai0[partition]) { + ib--; + } + } else if (info->ns == 3) { + if (i == bc7_ai1[partition]) { + ib--; + } else if (i == bc7_ai2[partition]) { + ib--; + } + } + i0 = get_bits(src, cibit, ib); + cibit += ib; + + if (ab && info->ib2) { + ib2 = info->ib2; + if (ib2 && i == 0) { + ib2--; + } + i1 = get_bits(src, aibit, ib2); + aibit += ib2; + if (index_sel) { + bc7_lerp(&col[i], &endpoints[s], aw[i1], cw[i0]); + } else { + bc7_lerp(&col[i], &endpoints[s], cw[i0], aw[i1]); + } + } else { + bc7_lerp(&col[i], &endpoints[s], cw[i0], cw[i0]); + } +#define ROTATE(x, y) \ + val = x; \ + x = y; \ + y = val + if (rotation == 1) { + ROTATE(col[i].r, col[i].a); + } else if (rotation == 2) { + ROTATE(col[i].g, col[i].a); + } else if (rotation == 3) { + ROTATE(col[i].b, col[i].a); + } +#undef ROTATE + } +} + +/* BC6 */ +typedef struct { + char ns; /* number of subsets (also called regions) */ + char tr; /* whether endpoints are delta-compressed */ + char pb; /* partition bits */ + char epb; /* endpoint bits */ + char rb; /* red bits (delta) */ + char gb; /* green bits (delta) */ + char bb; /* blue bits (delta) */ +} bc6_mode_info; + +static const bc6_mode_info bc6_modes[] = { + // 00 + {2, 1, 5, 10, 5, 5, 5}, + // 01 + {2, 1, 5, 7, 6, 6, 6}, + // 10 + {2, 1, 5, 11, 5, 4, 4}, + {2, 1, 5, 11, 4, 5, 4}, + {2, 1, 5, 11, 4, 4, 5}, + {2, 1, 5, 9, 5, 5, 5}, + {2, 1, 5, 8, 6, 5, 5}, + {2, 1, 5, 8, 5, 6, 5}, + {2, 1, 5, 8, 5, 5, 6}, + {2, 0, 5, 6, 6, 6, 6}, + // 11 + {1, 0, 0, 10, 10, 10, 10}, + {1, 1, 0, 11, 9, 9, 9}, + {1, 1, 0, 12, 8, 8, 8}, + {1, 1, 0, 16, 4, 4, 4}}; + +/* Table.F, encoded as a sequence of bit indices */ +static const UINT8 bc6_bit_packings[][75] = { + {116, 132, 180, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, + 18, 19, 20, 21, 22, 23, 24, 25, 32, 33, 34, 35, 36, 37, 38, + 39, 40, 41, 48, 49, 50, 51, 52, 164, 112, 113, 114, 115, 64, 65, + 66, 67, 68, 176, 160, 161, 162, 163, 80, 81, 82, 83, 84, 177, 128, + 129, 130, 131, 96, 97, 98, 99, 100, 178, 144, 145, 146, 147, 148, 179}, + {117, 164, 165, 0, 1, 2, 3, 4, 5, 6, 176, 177, 132, 16, 17, + 18, 19, 20, 21, 22, 133, 178, 116, 32, 33, 34, 35, 36, 37, 38, + 179, 181, 180, 48, 49, 50, 51, 52, 53, 112, 113, 114, 115, 64, 65, + 66, 67, 68, 69, 160, 161, 162, 163, 80, 81, 82, 83, 84, 85, 128, + 129, 130, 131, 96, 97, 98, 99, 100, 101, 144, 145, 146, 147, 148, 149}, + {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, + 21, 22, 23, 24, 25, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, + 48, 49, 50, 51, 52, 10, 112, 113, 114, 115, 64, 65, 66, 67, 26, + 176, 160, 161, 162, 163, 80, 81, 82, 83, 42, 177, 128, 129, 130, 131, + 96, 97, 98, 99, 100, 178, 144, 145, 146, 147, 148, 179}, + {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, + 21, 22, 23, 24, 25, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, + 48, 49, 50, 51, 10, 164, 112, 113, 114, 115, 64, 65, 66, 67, 68, + 26, 160, 161, 162, 163, 80, 81, 82, 83, 42, 177, 128, 129, 130, 131, + 96, 97, 98, 99, 176, 178, 144, 145, 146, 147, 116, 179}, + {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, + 21, 22, 23, 24, 25, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, + 48, 49, 50, 51, 10, 132, 112, 113, 114, 115, 64, 65, 66, 67, 26, + 176, 160, 161, 162, 163, 80, 81, 82, 83, 84, 42, 128, 129, 130, 131, + 96, 97, 98, 99, 177, 178, 144, 145, 146, 147, 180, 179}, + {0, 1, 2, 3, 4, 5, 6, 7, 8, 132, 16, 17, 18, 19, 20, + 21, 22, 23, 24, 116, 32, 33, 34, 35, 36, 37, 38, 39, 40, 180, + 48, 49, 50, 51, 52, 164, 112, 113, 114, 115, 64, 65, 66, 67, 68, + 176, 160, 161, 162, 163, 80, 81, 82, 83, 84, 177, 128, 129, 130, 131, + 96, 97, 98, 99, 100, 178, 144, 145, 146, 147, 148, 179}, + {0, 1, 2, 3, 4, 5, 6, 7, 164, 132, 16, 17, 18, 19, 20, + 21, 22, 23, 178, 116, 32, 33, 34, 35, 36, 37, 38, 39, 179, 180, + 48, 49, 50, 51, 52, 53, 112, 113, 114, 115, 64, 65, 66, 67, 68, + 176, 160, 161, 162, 163, 80, 81, 82, 83, 84, 177, 128, 129, 130, 131, + 96, 97, 98, 99, 100, 101, 144, 145, 146, 147, 148, 149}, + {0, 1, 2, 3, 4, 5, 6, 7, 176, 132, 16, 17, 18, 19, 20, + 21, 22, 23, 117, 116, 32, 33, 34, 35, 36, 37, 38, 39, 165, 180, + 48, 49, 50, 51, 52, 164, 112, 113, 114, 115, 64, 65, 66, 67, 68, + 69, 160, 161, 162, 163, 80, 81, 82, 83, 84, 177, 128, 129, 130, 131, + 96, 97, 98, 99, 100, 178, 144, 145, 146, 147, 148, 179}, + {0, 1, 2, 3, 4, 5, 6, 7, 177, 132, 16, 17, 18, 19, 20, + 21, 22, 23, 133, 116, 32, 33, 34, 35, 36, 37, 38, 39, 181, 180, + 48, 49, 50, 51, 52, 164, 112, 113, 114, 115, 64, 65, 66, 67, 68, + 176, 160, 161, 162, 163, 80, 81, 82, 83, 84, 85, 128, 129, 130, 131, + 96, 97, 98, 99, 100, 178, 144, 145, 146, 147, 148, 179}, + {0, 1, 2, 3, 4, 5, 164, 176, 177, 132, 16, 17, 18, 19, 20, + 21, 117, 133, 178, 116, 32, 33, 34, 35, 36, 37, 165, 179, 181, 180, + 48, 49, 50, 51, 52, 53, 112, 113, 114, 115, 64, 65, 66, 67, 68, + 69, 160, 161, 162, 163, 80, 81, 82, 83, 84, 85, 128, 129, 130, 131, + 96, 97, 98, 99, 100, 101, 144, 145, 146, 147, 148, 149}, + {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, + 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89}, + {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 48, 49, 50, 51, 52, 53, 54, 55, 56, 10, + 64, 65, 66, 67, 68, 69, 70, 71, 72, 26, 80, 81, 82, 83, 84, 85, 86, 87, 88, 42}, + {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 48, 49, 50, 51, 52, 53, 54, 55, 11, 10, + 64, 65, 66, 67, 68, 69, 70, 71, 27, 26, 80, 81, 82, 83, 84, 85, 86, 87, 43, 42}, + {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 48, 49, 50, 51, 15, 14, 13, 12, 11, 10, + 64, 65, 66, 67, 31, 30, 29, 28, 27, 26, 80, 81, 82, 83, 47, 46, 45, 44, 43, 42}}; + +static void +bc6_sign_extend(UINT16 *v, int prec) { + int x = *v; + if (x & (1 << (prec - 1))) { + x |= -1 << prec; + } + *v = (UINT16)x; +} + +static int +bc6_unquantize(UINT16 v, int prec, int sign) { + int s = 0; + int x; + if (!sign) { + x = v; + if (prec >= 15) { + return x; + } + if (x == 0) { + return 0; + } + if (x == ((1 << prec) - 1)) { + return 0xffff; + } + return ((x << 15) + 0x4000) >> (prec - 1); + } else { + x = (INT16)v; + if (prec >= 16) { + return x; + } + if (x < 0) { + s = 1; + x = -x; + } + + if (x != 0) { + if (x >= ((1 << (prec - 1)) - 1)) { + x = 0x7fff; + } else { + x = ((x << 15) + 0x4000) >> (prec - 1); + } + } + + if (s) { + return -x; + } + return x; + } +} + +static float +half_to_float(UINT16 h) { + /* https://gist.github.com/rygorous/2144712 */ + union { + UINT32 u; + float f; + } o, m; + m.u = 0x77800000; + o.u = (h & 0x7fff) << 13; + o.f *= m.f; + m.u = 0x47800000; + if (o.f >= m.f) { + o.u |= 255 << 23; + } + o.u |= (h & 0x8000) << 16; + return o.f; +} + +static float +bc6_finalize(int v, int sign) { + if (sign) { + if (v < 0) { + v = ((-v) * 31) / 32; + return half_to_float((UINT16)(0x8000 | v)); + } else { + return half_to_float((UINT16)((v * 31) / 32)); + } + } else { + return half_to_float((UINT16)((v * 31) / 64)); + } +} + +static UINT8 +bc6_clamp(float value) { + if (value < 0.0f) { + return 0; + } else if (value > 1.0f) { + return 255; + } else { + return (UINT8)(value * 255.0f); + } +} + +static void +bc6_lerp(rgba *col, int *e0, int *e1, int s, int sign) { + int r, g, b; + int t = 64 - s; + r = (e0[0] * t + e1[0] * s) >> 6; + g = (e0[1] * t + e1[1] * s) >> 6; + b = (e0[2] * t + e1[2] * s) >> 6; + col->r = bc6_clamp(bc6_finalize(r, sign)); + col->g = bc6_clamp(bc6_finalize(g, sign)); + col->b = bc6_clamp(bc6_finalize(b, sign)); +} + +static void +decode_bc6_block(rgba *col, const UINT8 *src, int sign) { + UINT16 endpoints[12]; /* storage for r0, g0, b0, r1, ... */ + int ueps[12]; + int i, i0, ib2, di, dw, mask, numep, s; + UINT8 partition; + const bc6_mode_info *info; + const char *cw; + int bit = 5; + int epbits = 75; + int ib = 3; + int mode = src[0] & 0x1f; + if ((mode & 3) == 0 || (mode & 3) == 1) { + mode &= 3; + bit = 2; + } else if ((mode & 3) == 2) { + mode = 2 + (mode >> 2); + epbits = 72; + } else { + mode = 10 + (mode >> 2); + epbits = 60; + ib = 4; + } + if (mode >= 14) { + /* invalid block */ + memset(col, 0, 16 * sizeof(col[0])); + return; + } + info = &bc6_modes[mode]; + cw = bc7_get_weights(ib); + numep = info->ns == 2 ? 12 : 6; + for (i = 0; i < 12; i++) { + endpoints[i] = 0; + } + for (i = 0; i < epbits; i++) { + di = bc6_bit_packings[mode][i]; + dw = di >> 4; + di &= 15; + endpoints[dw] |= (UINT16)get_bit(src, bit + i) << di; + } + bit += epbits; + partition = get_bits(src, bit, info->pb); + bit += info->pb; + mask = (1 << info->epb) - 1; + if (sign) { /* sign-extend e0 if signed */ + bc6_sign_extend(&endpoints[0], info->epb); + bc6_sign_extend(&endpoints[1], info->epb); + bc6_sign_extend(&endpoints[2], info->epb); + } + if (sign || info->tr) { /* sign-extend e1,2,3 if signed or deltas */ + for (i = 3; i < numep; i += 3) { + bc6_sign_extend(&endpoints[i], info->rb); + bc6_sign_extend(&endpoints[i + 1], info->gb); + bc6_sign_extend(&endpoints[i + 2], info->bb); + } + } + if (info->tr) { /* apply deltas */ + for (i = 3; i < numep; i += 3) { + endpoints[i] = (endpoints[i] + endpoints[0]) & mask; + endpoints[i + 1] = (endpoints[i + 1] + endpoints[1]) & mask; + endpoints[i + 2] = (endpoints[i + 2] + endpoints[2]) & mask; + } + } + for (i = 0; i < numep; i++) { + ueps[i] = bc6_unquantize(endpoints[i], info->epb, sign); + } + for (i = 0; i < 16; i++) { + s = bc7_get_subset(info->ns, partition, i) * 6; + ib2 = ib; + if (i == 0) { + ib2--; + } else if (info->ns == 2) { + if (i == bc7_ai0[partition]) { + ib2--; + } + } + i0 = get_bits(src, bit, ib2); + bit += ib2; + + bc6_lerp(&col[i], &ueps[s], &ueps[s + 3], cw[i0], sign); + } +} + +static void +put_block(Imaging im, ImagingCodecState state, const char *col, int sz, int C) { + int width = state->xsize; + int height = state->ysize; + int xmax = width + state->xoff; + int ymax = height + state->yoff; + int j, i, y, x; + char *dst; + for (j = 0; j < 4; j++) { + y = state->y + j; + if (C) { + if (y >= height) { + continue; + } + if (state->ystep < 0) { + y = state->yoff + ymax - y - 1; + } + dst = im->image[y]; + for (i = 0; i < 4; i++) { + x = state->x + i; + if (x >= width) { + continue; + } + memcpy(dst + sz * x, col + sz * (j * 4 + i), sz); + } + } else { + if (state->ystep < 0) { + y = state->yoff + ymax - y - 1; + } + x = state->x; + dst = im->image[y] + sz * x; + memcpy(dst, col + sz * (j * 4), 4 * sz); + } + } + state->x += 4; + if (state->x >= xmax) { + state->y += 4; + state->x = state->xoff; + } +} + +static int +decode_bcn( + Imaging im, + ImagingCodecState state, + const UINT8 *src, + int bytes, + int N, + int C, + char *pixel_format) { + int ymax = state->ysize + state->yoff; + const UINT8 *ptr = src; + switch (N) { +#define DECODE_LOOP(NN, SZ, TY, ...) \ + case NN: \ + while (bytes >= SZ) { \ + TY col[16]; \ + memset(col, 0, 16 * sizeof(col[0])); \ + decode_bc##NN##_block(col, ptr); \ + put_block(im, state, (const char *)col, sizeof(col[0]), C); \ + ptr += SZ; \ + bytes -= SZ; \ + if (state->y >= ymax) { \ + return -1; \ + } \ + } \ + break + + DECODE_LOOP(1, 8, rgba); + DECODE_LOOP(2, 16, rgba); + DECODE_LOOP(3, 16, rgba); + DECODE_LOOP(4, 8, lum); + case 5: { + int sign = strcmp(pixel_format, "BC5S") == 0 ? 1 : 0; + while (bytes >= 16) { + rgba col[16]; + memset(col, sign ? 128 : 0, 16 * sizeof(col[0])); + decode_bc5_block(col, ptr, sign); + put_block(im, state, (const char *)col, sizeof(col[0]), C); + ptr += 16; + bytes -= 16; + if (state->y >= ymax) { + return -1; + } + } + break; + } + case 6: { + int sign = strcmp(pixel_format, "BC6HS") == 0 ? 1 : 0; + while (bytes >= 16) { + rgba col[16]; + decode_bc6_block(col, ptr, sign); + put_block(im, state, (const char *)col, sizeof(col[0]), C); + ptr += 16; + bytes -= 16; + if (state->y >= ymax) { + return -1; + } + } + break; + } + DECODE_LOOP(7, 16, rgba); +#undef DECODE_LOOP + } + return (int)(ptr - src); +} + +int +ImagingBcnDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) { + int N = state->state & 0xf; + int width = state->xsize; + int height = state->ysize; + int C = (width & 3) | (height & 3) ? 1 : 0; + char *pixel_format = ((BCNSTATE *)state->context)->pixel_format; + return decode_bcn(im, state, buf, bytes, N, C, pixel_format); +} diff --git a/src/libImaging/Bit.h b/src/libImaging/Bit.h index f64bfb469..249af2d6c 100644 --- a/src/libImaging/Bit.h +++ b/src/libImaging/Bit.h @@ -1,29 +1,29 @@ -/* Bit.h */ - -typedef struct { - /* CONFIGURATION */ - - /* Number of bits per pixel */ - int bits; - - /* Line padding (0 or 8) */ - int pad; - - /* Fill order */ - /* 0=msb/msb, 1=msbfill/lsbshift, 2=lsbfill/msbshift, 3=lsb/lsb */ - int fill; - - /* Signed integers (0=unsigned, 1=signed) */ - int sign; - - /* Lookup table (not implemented) */ - unsigned long lutsize; - FLOAT32 *lut; - - /* INTERNAL */ - unsigned long mask; - unsigned long signmask; - unsigned long bitbuffer; - int bitcount; - -} BITSTATE; +/* Bit.h */ + +typedef struct { + /* CONFIGURATION */ + + /* Number of bits per pixel */ + int bits; + + /* Line padding (0 or 8) */ + int pad; + + /* Fill order */ + /* 0=msb/msb, 1=msbfill/lsbshift, 2=lsbfill/msbshift, 3=lsb/lsb */ + int fill; + + /* Signed integers (0=unsigned, 1=signed) */ + int sign; + + /* Lookup table (not implemented) */ + unsigned long lutsize; + FLOAT32 *lut; + + /* INTERNAL */ + unsigned long mask; + unsigned long signmask; + unsigned long bitbuffer; + int bitcount; + +} BITSTATE; diff --git a/src/libImaging/BitDecode.c b/src/libImaging/BitDecode.c index 28baa8b7e..13477fe6b 100644 --- a/src/libImaging/BitDecode.c +++ b/src/libImaging/BitDecode.c @@ -1,138 +1,138 @@ -/* - * The Python Imaging Library. - * $Id$ - * - * decoder for packed bitfields (converts to floating point) - * - * history: - * 97-05-31 fl created (much more than originally intended) - * - * Copyright (c) Fredrik Lundh 1997. - * Copyright (c) Secret Labs AB 1997. - * - * See the README file for information on usage and redistribution. - */ - -#include "Imaging.h" - -#include "Bit.h" - -int -ImagingBitDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) { - BITSTATE *bitstate = state->context; - UINT8 *ptr; - - if (state->state == 0) { - /* Initialize context variables */ - - /* this decoder only works for float32 image buffers */ - if (im->type != IMAGING_TYPE_FLOAT32) { - state->errcode = IMAGING_CODEC_CONFIG; - return -1; - } - - /* sanity check */ - if (bitstate->bits < 1 || bitstate->bits >= 32) { - state->errcode = IMAGING_CODEC_CONFIG; - return -1; - } - - bitstate->mask = (1 << bitstate->bits) - 1; - - if (bitstate->sign) { - bitstate->signmask = (1 << (bitstate->bits - 1)); - } - - /* check image orientation */ - if (state->ystep < 0) { - state->y = state->ysize - 1; - state->ystep = -1; - } else { - state->ystep = 1; - } - - state->state = 1; - } - - ptr = buf; - - while (bytes > 0) { - UINT8 byte = *ptr; - - ptr++; - bytes--; - - /* get a byte from the input stream and insert in the bit buffer */ - if (bitstate->fill & 1) { - /* fill MSB first */ - bitstate->bitbuffer |= (unsigned long)byte << bitstate->bitcount; - } else { - /* fill LSB first */ - bitstate->bitbuffer = (bitstate->bitbuffer << 8) | byte; - } - - bitstate->bitcount += 8; - - while (bitstate->bitcount >= bitstate->bits) { - /* get a pixel from the bit buffer */ - unsigned long data; - FLOAT32 pixel; - - if (bitstate->fill & 2) { - /* store LSB first */ - data = bitstate->bitbuffer & bitstate->mask; - if (bitstate->bitcount > 32) { - /* bitbuffer overflow; restore it from last input byte */ - bitstate->bitbuffer = - byte >> (8 - (bitstate->bitcount - bitstate->bits)); - } else { - bitstate->bitbuffer >>= bitstate->bits; - } - } else { - /* store MSB first */ - data = (bitstate->bitbuffer >> (bitstate->bitcount - bitstate->bits)) & - bitstate->mask; - } - - bitstate->bitcount -= bitstate->bits; - - if (bitstate->lutsize > 0) { - /* map through lookup table */ - if (data <= 0) { - pixel = bitstate->lut[0]; - } else if (data >= bitstate->lutsize) { - pixel = bitstate->lut[bitstate->lutsize - 1]; - } else { - pixel = bitstate->lut[data]; - } - } else { - /* convert */ - if (data & bitstate->signmask) { - /* image memory contains signed data */ - pixel = (FLOAT32)(INT32)(data | ~bitstate->mask); - } else { - pixel = (FLOAT32)data; - } - } - - *(FLOAT32 *)(&im->image32[state->y][state->x]) = pixel; - - /* step forward */ - if (++state->x >= state->xsize) { - /* new line */ - state->y += state->ystep; - if (state->y < 0 || state->y >= state->ysize) { - /* end of file (errcode = 0) */ - return -1; - } - state->x = 0; - /* reset bit buffer */ - if (bitstate->pad > 0) { - bitstate->bitcount = 0; - } - } - } - } - - return ptr - buf; -} +/* + * The Python Imaging Library. + * $Id$ + * + * decoder for packed bitfields (converts to floating point) + * + * history: + * 97-05-31 fl created (much more than originally intended) + * + * Copyright (c) Fredrik Lundh 1997. + * Copyright (c) Secret Labs AB 1997. + * + * See the README file for information on usage and redistribution. + */ + +#include "Imaging.h" + +#include "Bit.h" + +int +ImagingBitDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) { + BITSTATE *bitstate = state->context; + UINT8 *ptr; + + if (state->state == 0) { + /* Initialize context variables */ + + /* this decoder only works for float32 image buffers */ + if (im->type != IMAGING_TYPE_FLOAT32) { + state->errcode = IMAGING_CODEC_CONFIG; + return -1; + } + + /* sanity check */ + if (bitstate->bits < 1 || bitstate->bits >= 32) { + state->errcode = IMAGING_CODEC_CONFIG; + return -1; + } + + bitstate->mask = (1 << bitstate->bits) - 1; + + if (bitstate->sign) { + bitstate->signmask = (1 << (bitstate->bits - 1)); + } + + /* check image orientation */ + if (state->ystep < 0) { + state->y = state->ysize - 1; + state->ystep = -1; + } else { + state->ystep = 1; + } + + state->state = 1; + } + + ptr = buf; + + while (bytes > 0) { + UINT8 byte = *ptr; + + ptr++; + bytes--; + + /* get a byte from the input stream and insert in the bit buffer */ + if (bitstate->fill & 1) { + /* fill MSB first */ + bitstate->bitbuffer |= (unsigned long)byte << bitstate->bitcount; + } else { + /* fill LSB first */ + bitstate->bitbuffer = (bitstate->bitbuffer << 8) | byte; + } + + bitstate->bitcount += 8; + + while (bitstate->bitcount >= bitstate->bits) { + /* get a pixel from the bit buffer */ + unsigned long data; + FLOAT32 pixel; + + if (bitstate->fill & 2) { + /* store LSB first */ + data = bitstate->bitbuffer & bitstate->mask; + if (bitstate->bitcount > 32) { + /* bitbuffer overflow; restore it from last input byte */ + bitstate->bitbuffer = + byte >> (8 - (bitstate->bitcount - bitstate->bits)); + } else { + bitstate->bitbuffer >>= bitstate->bits; + } + } else { + /* store MSB first */ + data = (bitstate->bitbuffer >> (bitstate->bitcount - bitstate->bits)) & + bitstate->mask; + } + + bitstate->bitcount -= bitstate->bits; + + if (bitstate->lutsize > 0) { + /* map through lookup table */ + if (data <= 0) { + pixel = bitstate->lut[0]; + } else if (data >= bitstate->lutsize) { + pixel = bitstate->lut[bitstate->lutsize - 1]; + } else { + pixel = bitstate->lut[data]; + } + } else { + /* convert */ + if (data & bitstate->signmask) { + /* image memory contains signed data */ + pixel = (FLOAT32)(INT32)(data | ~bitstate->mask); + } else { + pixel = (FLOAT32)data; + } + } + + *(FLOAT32 *)(&im->image32[state->y][state->x]) = pixel; + + /* step forward */ + if (++state->x >= state->xsize) { + /* new line */ + state->y += state->ystep; + if (state->y < 0 || state->y >= state->ysize) { + /* end of file (errcode = 0) */ + return -1; + } + state->x = 0; + /* reset bit buffer */ + if (bitstate->pad > 0) { + bitstate->bitcount = 0; + } + } + } + } + + return ptr - buf; +} diff --git a/src/libImaging/Blend.c b/src/libImaging/Blend.c index a53ae0fad..690622552 100644 --- a/src/libImaging/Blend.c +++ b/src/libImaging/Blend.c @@ -1,79 +1,79 @@ -/* - * The Python Imaging Library - * $Id$ - * - * interpolate between two existing images - * - * history: - * 96-03-20 fl Created - * 96-05-18 fl Simplified blend expression - * 96-10-05 fl Fixed expression bug, special case for interpolation - * - * Copyright (c) Fredrik Lundh 1996. - * Copyright (c) Secret Labs AB 1997. - * - * See the README file for details on usage and redistribution. - */ - -#include "Imaging.h" - -Imaging -ImagingBlend(Imaging imIn1, Imaging imIn2, float alpha) { - Imaging imOut; - int x, y; - - /* Check arguments */ - if (!imIn1 || !imIn2 || imIn1->type != IMAGING_TYPE_UINT8 || imIn1->palette || - strcmp(imIn1->mode, "1") == 0 || imIn2->palette || - strcmp(imIn2->mode, "1") == 0) { - return ImagingError_ModeError(); - } - - if (imIn1->type != imIn2->type || imIn1->bands != imIn2->bands || - imIn1->xsize != imIn2->xsize || imIn1->ysize != imIn2->ysize) { - return ImagingError_Mismatch(); - } - - /* Shortcuts */ - if (alpha == 0.0) { - return ImagingCopy(imIn1); - } else if (alpha == 1.0) { - return ImagingCopy(imIn2); - } - - imOut = ImagingNewDirty(imIn1->mode, imIn1->xsize, imIn1->ysize); - if (!imOut) { - return NULL; - } - - if (alpha >= 0 && alpha <= 1.0) { - /* Interpolate between bands */ - for (y = 0; y < imIn1->ysize; y++) { - UINT8 *in1 = (UINT8 *)imIn1->image[y]; - UINT8 *in2 = (UINT8 *)imIn2->image[y]; - UINT8 *out = (UINT8 *)imOut->image[y]; - for (x = 0; x < imIn1->linesize; x++) { - out[x] = (UINT8)((int)in1[x] + alpha * ((int)in2[x] - (int)in1[x])); - } - } - } else { - /* Extrapolation; must make sure to clip resulting values */ - for (y = 0; y < imIn1->ysize; y++) { - UINT8 *in1 = (UINT8 *)imIn1->image[y]; - UINT8 *in2 = (UINT8 *)imIn2->image[y]; - UINT8 *out = (UINT8 *)imOut->image[y]; - for (x = 0; x < imIn1->linesize; x++) { - float temp = (float)((int)in1[x] + alpha * ((int)in2[x] - (int)in1[x])); - if (temp <= 0.0) { - out[x] = 0; - } else if (temp >= 255.0) { - out[x] = 255; - } else { - out[x] = (UINT8)temp; - } - } - } - } - - return imOut; -} +/* + * The Python Imaging Library + * $Id$ + * + * interpolate between two existing images + * + * history: + * 96-03-20 fl Created + * 96-05-18 fl Simplified blend expression + * 96-10-05 fl Fixed expression bug, special case for interpolation + * + * Copyright (c) Fredrik Lundh 1996. + * Copyright (c) Secret Labs AB 1997. + * + * See the README file for details on usage and redistribution. + */ + +#include "Imaging.h" + +Imaging +ImagingBlend(Imaging imIn1, Imaging imIn2, float alpha) { + Imaging imOut; + int x, y; + + /* Check arguments */ + if (!imIn1 || !imIn2 || imIn1->type != IMAGING_TYPE_UINT8 || imIn1->palette || + strcmp(imIn1->mode, "1") == 0 || imIn2->palette || + strcmp(imIn2->mode, "1") == 0) { + return ImagingError_ModeError(); + } + + if (imIn1->type != imIn2->type || imIn1->bands != imIn2->bands || + imIn1->xsize != imIn2->xsize || imIn1->ysize != imIn2->ysize) { + return ImagingError_Mismatch(); + } + + /* Shortcuts */ + if (alpha == 0.0) { + return ImagingCopy(imIn1); + } else if (alpha == 1.0) { + return ImagingCopy(imIn2); + } + + imOut = ImagingNewDirty(imIn1->mode, imIn1->xsize, imIn1->ysize); + if (!imOut) { + return NULL; + } + + if (alpha >= 0 && alpha <= 1.0) { + /* Interpolate between bands */ + for (y = 0; y < imIn1->ysize; y++) { + UINT8 *in1 = (UINT8 *)imIn1->image[y]; + UINT8 *in2 = (UINT8 *)imIn2->image[y]; + UINT8 *out = (UINT8 *)imOut->image[y]; + for (x = 0; x < imIn1->linesize; x++) { + out[x] = (UINT8)((int)in1[x] + alpha * ((int)in2[x] - (int)in1[x])); + } + } + } else { + /* Extrapolation; must make sure to clip resulting values */ + for (y = 0; y < imIn1->ysize; y++) { + UINT8 *in1 = (UINT8 *)imIn1->image[y]; + UINT8 *in2 = (UINT8 *)imIn2->image[y]; + UINT8 *out = (UINT8 *)imOut->image[y]; + for (x = 0; x < imIn1->linesize; x++) { + float temp = (float)((int)in1[x] + alpha * ((int)in2[x] - (int)in1[x])); + if (temp <= 0.0) { + out[x] = 0; + } else if (temp >= 255.0) { + out[x] = 255; + } else { + out[x] = (UINT8)temp; + } + } + } + } + + return imOut; +} diff --git a/src/libImaging/BoxBlur.c b/src/libImaging/BoxBlur.c index 4ea9c7717..e3e40dc07 100644 --- a/src/libImaging/BoxBlur.c +++ b/src/libImaging/BoxBlur.c @@ -1,324 +1,324 @@ -#include "Imaging.h" - -#define MAX(x, y) (((x) > (y)) ? (x) : (y)) -#define MIN(x, y) (((x) < (y)) ? (x) : (y)) - -typedef UINT8 pixel[4]; - -void static inline ImagingLineBoxBlur32( - pixel *lineOut, - pixel *lineIn, - int lastx, - int radius, - int edgeA, - int edgeB, - UINT32 ww, - UINT32 fw) { - int x; - UINT32 acc[4]; - UINT32 bulk[4]; - -#define MOVE_ACC(acc, subtract, add) \ - acc[0] += lineIn[add][0] - lineIn[subtract][0]; \ - acc[1] += lineIn[add][1] - lineIn[subtract][1]; \ - acc[2] += lineIn[add][2] - lineIn[subtract][2]; \ - acc[3] += lineIn[add][3] - lineIn[subtract][3]; - -#define ADD_FAR(bulk, acc, left, right) \ - bulk[0] = (acc[0] * ww) + (lineIn[left][0] + lineIn[right][0]) * fw; \ - bulk[1] = (acc[1] * ww) + (lineIn[left][1] + lineIn[right][1]) * fw; \ - bulk[2] = (acc[2] * ww) + (lineIn[left][2] + lineIn[right][2]) * fw; \ - bulk[3] = (acc[3] * ww) + (lineIn[left][3] + lineIn[right][3]) * fw; - -#define SAVE(x, bulk) \ - lineOut[x][0] = (UINT8)((bulk[0] + (1 << 23)) >> 24); \ - lineOut[x][1] = (UINT8)((bulk[1] + (1 << 23)) >> 24); \ - lineOut[x][2] = (UINT8)((bulk[2] + (1 << 23)) >> 24); \ - lineOut[x][3] = (UINT8)((bulk[3] + (1 << 23)) >> 24); - - /* Compute acc for -1 pixel (outside of image): - From "-radius-1" to "-1" get first pixel, - then from "0" to "radius-1". */ - acc[0] = lineIn[0][0] * (radius + 1); - acc[1] = lineIn[0][1] * (radius + 1); - acc[2] = lineIn[0][2] * (radius + 1); - acc[3] = lineIn[0][3] * (radius + 1); - /* As radius can be bigger than xsize, iterate to edgeA -1. */ - for (x = 0; x < edgeA - 1; x++) { - acc[0] += lineIn[x][0]; - acc[1] += lineIn[x][1]; - acc[2] += lineIn[x][2]; - acc[3] += lineIn[x][3]; - } - /* Then multiply remainder to last x. */ - acc[0] += lineIn[lastx][0] * (radius - edgeA + 1); - acc[1] += lineIn[lastx][1] * (radius - edgeA + 1); - acc[2] += lineIn[lastx][2] * (radius - edgeA + 1); - acc[3] += lineIn[lastx][3] * (radius - edgeA + 1); - - if (edgeA <= edgeB) { - /* Subtract pixel from left ("0"). - Add pixels from radius. */ - for (x = 0; x < edgeA; x++) { - MOVE_ACC(acc, 0, x + radius); - ADD_FAR(bulk, acc, 0, x + radius + 1); - SAVE(x, bulk); - } - /* Subtract previous pixel from "-radius". - Add pixels from radius. */ - for (x = edgeA; x < edgeB; x++) { - MOVE_ACC(acc, x - radius - 1, x + radius); - ADD_FAR(bulk, acc, x - radius - 1, x + radius + 1); - SAVE(x, bulk); - } - /* Subtract previous pixel from "-radius". - Add last pixel. */ - for (x = edgeB; x <= lastx; x++) { - MOVE_ACC(acc, x - radius - 1, lastx); - ADD_FAR(bulk, acc, x - radius - 1, lastx); - SAVE(x, bulk); - } - } else { - for (x = 0; x < edgeB; x++) { - MOVE_ACC(acc, 0, x + radius); - ADD_FAR(bulk, acc, 0, x + radius + 1); - SAVE(x, bulk); - } - for (x = edgeB; x < edgeA; x++) { - MOVE_ACC(acc, 0, lastx); - ADD_FAR(bulk, acc, 0, lastx); - SAVE(x, bulk); - } - for (x = edgeA; x <= lastx; x++) { - MOVE_ACC(acc, x - radius - 1, lastx); - ADD_FAR(bulk, acc, x - radius - 1, lastx); - SAVE(x, bulk); - } - } - -#undef MOVE_ACC -#undef ADD_FAR -#undef SAVE -} - -void static inline ImagingLineBoxBlur8( - UINT8 *lineOut, - UINT8 *lineIn, - int lastx, - int radius, - int edgeA, - int edgeB, - UINT32 ww, - UINT32 fw) { - int x; - UINT32 acc; - UINT32 bulk; - -#define MOVE_ACC(acc, subtract, add) acc += lineIn[add] - lineIn[subtract]; - -#define ADD_FAR(bulk, acc, left, right) \ - bulk = (acc * ww) + (lineIn[left] + lineIn[right]) * fw; - -#define SAVE(x, bulk) lineOut[x] = (UINT8)((bulk + (1 << 23)) >> 24) - - acc = lineIn[0] * (radius + 1); - for (x = 0; x < edgeA - 1; x++) { - acc += lineIn[x]; - } - acc += lineIn[lastx] * (radius - edgeA + 1); - - if (edgeA <= edgeB) { - for (x = 0; x < edgeA; x++) { - MOVE_ACC(acc, 0, x + radius); - ADD_FAR(bulk, acc, 0, x + radius + 1); - SAVE(x, bulk); - } - for (x = edgeA; x < edgeB; x++) { - MOVE_ACC(acc, x - radius - 1, x + radius); - ADD_FAR(bulk, acc, x - radius - 1, x + radius + 1); - SAVE(x, bulk); - } - for (x = edgeB; x <= lastx; x++) { - MOVE_ACC(acc, x - radius - 1, lastx); - ADD_FAR(bulk, acc, x - radius - 1, lastx); - SAVE(x, bulk); - } - } else { - for (x = 0; x < edgeB; x++) { - MOVE_ACC(acc, 0, x + radius); - ADD_FAR(bulk, acc, 0, x + radius + 1); - SAVE(x, bulk); - } - for (x = edgeB; x < edgeA; x++) { - MOVE_ACC(acc, 0, lastx); - ADD_FAR(bulk, acc, 0, lastx); - SAVE(x, bulk); - } - for (x = edgeA; x <= lastx; x++) { - MOVE_ACC(acc, x - radius - 1, lastx); - ADD_FAR(bulk, acc, x - radius - 1, lastx); - SAVE(x, bulk); - } - } - -#undef MOVE_ACC -#undef ADD_FAR -#undef SAVE -} - -Imaging -ImagingHorizontalBoxBlur(Imaging imOut, Imaging imIn, float floatRadius) { - ImagingSectionCookie cookie; - - int y; - - int radius = (int)floatRadius; - UINT32 ww = (UINT32)(1 << 24) / (floatRadius * 2 + 1); - UINT32 fw = ((1 << 24) - (radius * 2 + 1) * ww) / 2; - - int edgeA = MIN(radius + 1, imIn->xsize); - int edgeB = MAX(imIn->xsize - radius - 1, 0); - - UINT32 *lineOut = calloc(imIn->xsize, sizeof(UINT32)); - if (lineOut == NULL) { - return ImagingError_MemoryError(); - } - - // printf(">>> %d %d %d\n", radius, ww, fw); - - ImagingSectionEnter(&cookie); - - if (imIn->image8) { - for (y = 0; y < imIn->ysize; y++) { - ImagingLineBoxBlur8( - (imIn == imOut ? (UINT8 *)lineOut : imOut->image8[y]), - imIn->image8[y], - imIn->xsize - 1, - radius, - edgeA, - edgeB, - ww, - fw); - if (imIn == imOut) { - // Commit. - memcpy(imOut->image8[y], lineOut, imIn->xsize); - } - } - } else { - for (y = 0; y < imIn->ysize; y++) { - ImagingLineBoxBlur32( - imIn == imOut ? (pixel *)lineOut : (pixel *)imOut->image32[y], - (pixel *)imIn->image32[y], - imIn->xsize - 1, - radius, - edgeA, - edgeB, - ww, - fw); - if (imIn == imOut) { - // Commit. - memcpy(imOut->image32[y], lineOut, imIn->xsize * 4); - } - } - } - - ImagingSectionLeave(&cookie); - - free(lineOut); - - return imOut; -} - -Imaging -ImagingBoxBlur(Imaging imOut, Imaging imIn, float xradius, float yradius, int n) { - int i; - Imaging imTransposed; - - if (n < 1) { - return ImagingError_ValueError("number of passes must be greater than zero"); - } - if (xradius < 0 || yradius < 0) { - return ImagingError_ValueError("radius must be >= 0"); - } - - if (strcmp(imIn->mode, imOut->mode) || imIn->type != imOut->type || - imIn->bands != imOut->bands || imIn->xsize != imOut->xsize || - imIn->ysize != imOut->ysize) { - return ImagingError_Mismatch(); - } - - if (imIn->type != IMAGING_TYPE_UINT8) { - return ImagingError_ModeError(); - } - - if (!(strcmp(imIn->mode, "RGB") == 0 || strcmp(imIn->mode, "RGBA") == 0 || - strcmp(imIn->mode, "RGBa") == 0 || strcmp(imIn->mode, "RGBX") == 0 || - strcmp(imIn->mode, "CMYK") == 0 || strcmp(imIn->mode, "L") == 0 || - strcmp(imIn->mode, "LA") == 0 || strcmp(imIn->mode, "La") == 0)) { - return ImagingError_ModeError(); - } - - /* Apply blur in one dimension. - Use imOut as a destination at first pass, - then use imOut as a source too. */ - - if (xradius != 0) { - ImagingHorizontalBoxBlur(imOut, imIn, xradius); - for (i = 1; i < n; i++) { - ImagingHorizontalBoxBlur(imOut, imOut, xradius); - } - } - if (yradius != 0) { - imTransposed = ImagingNewDirty(imIn->mode, imIn->ysize, imIn->xsize); - if (!imTransposed) { - return NULL; - } - - /* Transpose result for blur in another direction. */ - ImagingTranspose(imTransposed, xradius == 0 ? imIn : imOut); - - /* Reuse imTransposed as a source and destination there. */ - for (i = 0; i < n; i++) { - ImagingHorizontalBoxBlur(imTransposed, imTransposed, yradius); - } - /* Restore original orientation. */ - ImagingTranspose(imOut, imTransposed); - - ImagingDelete(imTransposed); - } - if (xradius == 0 && yradius == 0) { - if (!ImagingCopy2(imOut, imIn)) { - return NULL; - } - } - - return imOut; -} - -static float -_gaussian_blur_radius(float radius, int passes) { - float sigma2, L, l, a; - - sigma2 = radius * radius / passes; - // from https://www.mia.uni-saarland.de/Publications/gwosdek-ssvm11.pdf - // [7] Box length. - L = sqrt(12.0 * sigma2 + 1.0); - // [11] Integer part of box radius. - l = floor((L - 1.0) / 2.0); - // [14], [Fig. 2] Fractional part of box radius. - a = (2 * l + 1) * (l * (l + 1) - 3 * sigma2); - a /= 6 * (sigma2 - (l + 1) * (l + 1)); - - return l + a; -} - -Imaging -ImagingGaussianBlur( - Imaging imOut, Imaging imIn, float xradius, float yradius, int passes) { - return ImagingBoxBlur( - imOut, - imIn, - _gaussian_blur_radius(xradius, passes), - _gaussian_blur_radius(yradius, passes), - passes); -} +#include "Imaging.h" + +#define MAX(x, y) (((x) > (y)) ? (x) : (y)) +#define MIN(x, y) (((x) < (y)) ? (x) : (y)) + +typedef UINT8 pixel[4]; + +void static inline ImagingLineBoxBlur32( + pixel *lineOut, + pixel *lineIn, + int lastx, + int radius, + int edgeA, + int edgeB, + UINT32 ww, + UINT32 fw) { + int x; + UINT32 acc[4]; + UINT32 bulk[4]; + +#define MOVE_ACC(acc, subtract, add) \ + acc[0] += lineIn[add][0] - lineIn[subtract][0]; \ + acc[1] += lineIn[add][1] - lineIn[subtract][1]; \ + acc[2] += lineIn[add][2] - lineIn[subtract][2]; \ + acc[3] += lineIn[add][3] - lineIn[subtract][3]; + +#define ADD_FAR(bulk, acc, left, right) \ + bulk[0] = (acc[0] * ww) + (lineIn[left][0] + lineIn[right][0]) * fw; \ + bulk[1] = (acc[1] * ww) + (lineIn[left][1] + lineIn[right][1]) * fw; \ + bulk[2] = (acc[2] * ww) + (lineIn[left][2] + lineIn[right][2]) * fw; \ + bulk[3] = (acc[3] * ww) + (lineIn[left][3] + lineIn[right][3]) * fw; + +#define SAVE(x, bulk) \ + lineOut[x][0] = (UINT8)((bulk[0] + (1 << 23)) >> 24); \ + lineOut[x][1] = (UINT8)((bulk[1] + (1 << 23)) >> 24); \ + lineOut[x][2] = (UINT8)((bulk[2] + (1 << 23)) >> 24); \ + lineOut[x][3] = (UINT8)((bulk[3] + (1 << 23)) >> 24); + + /* Compute acc for -1 pixel (outside of image): + From "-radius-1" to "-1" get first pixel, + then from "0" to "radius-1". */ + acc[0] = lineIn[0][0] * (radius + 1); + acc[1] = lineIn[0][1] * (radius + 1); + acc[2] = lineIn[0][2] * (radius + 1); + acc[3] = lineIn[0][3] * (radius + 1); + /* As radius can be bigger than xsize, iterate to edgeA -1. */ + for (x = 0; x < edgeA - 1; x++) { + acc[0] += lineIn[x][0]; + acc[1] += lineIn[x][1]; + acc[2] += lineIn[x][2]; + acc[3] += lineIn[x][3]; + } + /* Then multiply remainder to last x. */ + acc[0] += lineIn[lastx][0] * (radius - edgeA + 1); + acc[1] += lineIn[lastx][1] * (radius - edgeA + 1); + acc[2] += lineIn[lastx][2] * (radius - edgeA + 1); + acc[3] += lineIn[lastx][3] * (radius - edgeA + 1); + + if (edgeA <= edgeB) { + /* Subtract pixel from left ("0"). + Add pixels from radius. */ + for (x = 0; x < edgeA; x++) { + MOVE_ACC(acc, 0, x + radius); + ADD_FAR(bulk, acc, 0, x + radius + 1); + SAVE(x, bulk); + } + /* Subtract previous pixel from "-radius". + Add pixels from radius. */ + for (x = edgeA; x < edgeB; x++) { + MOVE_ACC(acc, x - radius - 1, x + radius); + ADD_FAR(bulk, acc, x - radius - 1, x + radius + 1); + SAVE(x, bulk); + } + /* Subtract previous pixel from "-radius". + Add last pixel. */ + for (x = edgeB; x <= lastx; x++) { + MOVE_ACC(acc, x - radius - 1, lastx); + ADD_FAR(bulk, acc, x - radius - 1, lastx); + SAVE(x, bulk); + } + } else { + for (x = 0; x < edgeB; x++) { + MOVE_ACC(acc, 0, x + radius); + ADD_FAR(bulk, acc, 0, x + radius + 1); + SAVE(x, bulk); + } + for (x = edgeB; x < edgeA; x++) { + MOVE_ACC(acc, 0, lastx); + ADD_FAR(bulk, acc, 0, lastx); + SAVE(x, bulk); + } + for (x = edgeA; x <= lastx; x++) { + MOVE_ACC(acc, x - radius - 1, lastx); + ADD_FAR(bulk, acc, x - radius - 1, lastx); + SAVE(x, bulk); + } + } + +#undef MOVE_ACC +#undef ADD_FAR +#undef SAVE +} + +void static inline ImagingLineBoxBlur8( + UINT8 *lineOut, + UINT8 *lineIn, + int lastx, + int radius, + int edgeA, + int edgeB, + UINT32 ww, + UINT32 fw) { + int x; + UINT32 acc; + UINT32 bulk; + +#define MOVE_ACC(acc, subtract, add) acc += lineIn[add] - lineIn[subtract]; + +#define ADD_FAR(bulk, acc, left, right) \ + bulk = (acc * ww) + (lineIn[left] + lineIn[right]) * fw; + +#define SAVE(x, bulk) lineOut[x] = (UINT8)((bulk + (1 << 23)) >> 24) + + acc = lineIn[0] * (radius + 1); + for (x = 0; x < edgeA - 1; x++) { + acc += lineIn[x]; + } + acc += lineIn[lastx] * (radius - edgeA + 1); + + if (edgeA <= edgeB) { + for (x = 0; x < edgeA; x++) { + MOVE_ACC(acc, 0, x + radius); + ADD_FAR(bulk, acc, 0, x + radius + 1); + SAVE(x, bulk); + } + for (x = edgeA; x < edgeB; x++) { + MOVE_ACC(acc, x - radius - 1, x + radius); + ADD_FAR(bulk, acc, x - radius - 1, x + radius + 1); + SAVE(x, bulk); + } + for (x = edgeB; x <= lastx; x++) { + MOVE_ACC(acc, x - radius - 1, lastx); + ADD_FAR(bulk, acc, x - radius - 1, lastx); + SAVE(x, bulk); + } + } else { + for (x = 0; x < edgeB; x++) { + MOVE_ACC(acc, 0, x + radius); + ADD_FAR(bulk, acc, 0, x + radius + 1); + SAVE(x, bulk); + } + for (x = edgeB; x < edgeA; x++) { + MOVE_ACC(acc, 0, lastx); + ADD_FAR(bulk, acc, 0, lastx); + SAVE(x, bulk); + } + for (x = edgeA; x <= lastx; x++) { + MOVE_ACC(acc, x - radius - 1, lastx); + ADD_FAR(bulk, acc, x - radius - 1, lastx); + SAVE(x, bulk); + } + } + +#undef MOVE_ACC +#undef ADD_FAR +#undef SAVE +} + +Imaging +ImagingHorizontalBoxBlur(Imaging imOut, Imaging imIn, float floatRadius) { + ImagingSectionCookie cookie; + + int y; + + int radius = (int)floatRadius; + UINT32 ww = (UINT32)(1 << 24) / (floatRadius * 2 + 1); + UINT32 fw = ((1 << 24) - (radius * 2 + 1) * ww) / 2; + + int edgeA = MIN(radius + 1, imIn->xsize); + int edgeB = MAX(imIn->xsize - radius - 1, 0); + + UINT32 *lineOut = calloc(imIn->xsize, sizeof(UINT32)); + if (lineOut == NULL) { + return ImagingError_MemoryError(); + } + + // printf(">>> %d %d %d\n", radius, ww, fw); + + ImagingSectionEnter(&cookie); + + if (imIn->image8) { + for (y = 0; y < imIn->ysize; y++) { + ImagingLineBoxBlur8( + (imIn == imOut ? (UINT8 *)lineOut : imOut->image8[y]), + imIn->image8[y], + imIn->xsize - 1, + radius, + edgeA, + edgeB, + ww, + fw); + if (imIn == imOut) { + // Commit. + memcpy(imOut->image8[y], lineOut, imIn->xsize); + } + } + } else { + for (y = 0; y < imIn->ysize; y++) { + ImagingLineBoxBlur32( + imIn == imOut ? (pixel *)lineOut : (pixel *)imOut->image32[y], + (pixel *)imIn->image32[y], + imIn->xsize - 1, + radius, + edgeA, + edgeB, + ww, + fw); + if (imIn == imOut) { + // Commit. + memcpy(imOut->image32[y], lineOut, imIn->xsize * 4); + } + } + } + + ImagingSectionLeave(&cookie); + + free(lineOut); + + return imOut; +} + +Imaging +ImagingBoxBlur(Imaging imOut, Imaging imIn, float xradius, float yradius, int n) { + int i; + Imaging imTransposed; + + if (n < 1) { + return ImagingError_ValueError("number of passes must be greater than zero"); + } + if (xradius < 0 || yradius < 0) { + return ImagingError_ValueError("radius must be >= 0"); + } + + if (strcmp(imIn->mode, imOut->mode) || imIn->type != imOut->type || + imIn->bands != imOut->bands || imIn->xsize != imOut->xsize || + imIn->ysize != imOut->ysize) { + return ImagingError_Mismatch(); + } + + if (imIn->type != IMAGING_TYPE_UINT8) { + return ImagingError_ModeError(); + } + + if (!(strcmp(imIn->mode, "RGB") == 0 || strcmp(imIn->mode, "RGBA") == 0 || + strcmp(imIn->mode, "RGBa") == 0 || strcmp(imIn->mode, "RGBX") == 0 || + strcmp(imIn->mode, "CMYK") == 0 || strcmp(imIn->mode, "L") == 0 || + strcmp(imIn->mode, "LA") == 0 || strcmp(imIn->mode, "La") == 0)) { + return ImagingError_ModeError(); + } + + /* Apply blur in one dimension. + Use imOut as a destination at first pass, + then use imOut as a source too. */ + + if (xradius != 0) { + ImagingHorizontalBoxBlur(imOut, imIn, xradius); + for (i = 1; i < n; i++) { + ImagingHorizontalBoxBlur(imOut, imOut, xradius); + } + } + if (yradius != 0) { + imTransposed = ImagingNewDirty(imIn->mode, imIn->ysize, imIn->xsize); + if (!imTransposed) { + return NULL; + } + + /* Transpose result for blur in another direction. */ + ImagingTranspose(imTransposed, xradius == 0 ? imIn : imOut); + + /* Reuse imTransposed as a source and destination there. */ + for (i = 0; i < n; i++) { + ImagingHorizontalBoxBlur(imTransposed, imTransposed, yradius); + } + /* Restore original orientation. */ + ImagingTranspose(imOut, imTransposed); + + ImagingDelete(imTransposed); + } + if (xradius == 0 && yradius == 0) { + if (!ImagingCopy2(imOut, imIn)) { + return NULL; + } + } + + return imOut; +} + +static float +_gaussian_blur_radius(float radius, int passes) { + float sigma2, L, l, a; + + sigma2 = radius * radius / passes; + // from https://www.mia.uni-saarland.de/Publications/gwosdek-ssvm11.pdf + // [7] Box length. + L = sqrt(12.0 * sigma2 + 1.0); + // [11] Integer part of box radius. + l = floor((L - 1.0) / 2.0); + // [14], [Fig. 2] Fractional part of box radius. + a = (2 * l + 1) * (l * (l + 1) - 3 * sigma2); + a /= 6 * (sigma2 - (l + 1) * (l + 1)); + + return l + a; +} + +Imaging +ImagingGaussianBlur( + Imaging imOut, Imaging imIn, float xradius, float yradius, int passes) { + return ImagingBoxBlur( + imOut, + imIn, + _gaussian_blur_radius(xradius, passes), + _gaussian_blur_radius(yradius, passes), + passes); +} diff --git a/src/libImaging/Chops.c b/src/libImaging/Chops.c index f9c005efe..f65ca5edc 100644 --- a/src/libImaging/Chops.c +++ b/src/libImaging/Chops.c @@ -1,162 +1,162 @@ -/* - * The Python Imaging Library - * $Id$ - * - * basic channel operations - * - * history: - * 1996-03-28 fl Created - * 1996-08-13 fl Added and/or/xor for "1" images - * 1996-12-14 fl Added add_modulo, sub_modulo - * 2005-09-10 fl Fixed output values from and/or/xor - * - * Copyright (c) 1996 by Fredrik Lundh. - * Copyright (c) 1997 by Secret Labs AB. - * - * See the README file for details on usage and redistribution. - */ - -#include "Imaging.h" - -#define CHOP(operation) \ - int x, y; \ - Imaging imOut; \ - imOut = create(imIn1, imIn2, NULL); \ - if (!imOut) { \ - return NULL; \ - } \ - for (y = 0; y < imOut->ysize; y++) { \ - UINT8 *out = (UINT8 *)imOut->image[y]; \ - UINT8 *in1 = (UINT8 *)imIn1->image[y]; \ - UINT8 *in2 = (UINT8 *)imIn2->image[y]; \ - for (x = 0; x < imOut->linesize; x++) { \ - int temp = operation; \ - if (temp <= 0) { \ - out[x] = 0; \ - } else if (temp >= 255) { \ - out[x] = 255; \ - } else { \ - out[x] = temp; \ - } \ - } \ - } \ - return imOut; - -#define CHOP2(operation, mode) \ - int x, y; \ - Imaging imOut; \ - imOut = create(imIn1, imIn2, mode); \ - if (!imOut) { \ - return NULL; \ - } \ - for (y = 0; y < imOut->ysize; y++) { \ - UINT8 *out = (UINT8 *)imOut->image[y]; \ - UINT8 *in1 = (UINT8 *)imIn1->image[y]; \ - UINT8 *in2 = (UINT8 *)imIn2->image[y]; \ - for (x = 0; x < imOut->linesize; x++) { \ - out[x] = operation; \ - } \ - } \ - return imOut; - -static Imaging -create(Imaging im1, Imaging im2, char *mode) { - int xsize, ysize; - - if (!im1 || !im2 || im1->type != IMAGING_TYPE_UINT8 || - (mode != NULL && (strcmp(im1->mode, "1") || strcmp(im2->mode, "1")))) { - return (Imaging)ImagingError_ModeError(); - } - if (im1->type != im2->type || im1->bands != im2->bands) { - return (Imaging)ImagingError_Mismatch(); - } - - xsize = (im1->xsize < im2->xsize) ? im1->xsize : im2->xsize; - ysize = (im1->ysize < im2->ysize) ? im1->ysize : im2->ysize; - - return ImagingNewDirty(im1->mode, xsize, ysize); -} - -Imaging -ImagingChopLighter(Imaging imIn1, Imaging imIn2) { - CHOP((in1[x] > in2[x]) ? in1[x] : in2[x]); -} - -Imaging -ImagingChopDarker(Imaging imIn1, Imaging imIn2) { - CHOP((in1[x] < in2[x]) ? in1[x] : in2[x]); -} - -Imaging -ImagingChopDifference(Imaging imIn1, Imaging imIn2) { - CHOP(abs((int)in1[x] - (int)in2[x])); -} - -Imaging -ImagingChopMultiply(Imaging imIn1, Imaging imIn2) { - CHOP((int)in1[x] * (int)in2[x] / 255); -} - -Imaging -ImagingChopScreen(Imaging imIn1, Imaging imIn2) { - CHOP(255 - ((int)(255 - in1[x]) * (int)(255 - in2[x])) / 255); -} - -Imaging -ImagingChopAdd(Imaging imIn1, Imaging imIn2, float scale, int offset) { - CHOP(((int)in1[x] + (int)in2[x]) / scale + offset); -} - -Imaging -ImagingChopSubtract(Imaging imIn1, Imaging imIn2, float scale, int offset) { - CHOP(((int)in1[x] - (int)in2[x]) / scale + offset); -} - -Imaging -ImagingChopAnd(Imaging imIn1, Imaging imIn2) { - CHOP2((in1[x] && in2[x]) ? 255 : 0, "1"); -} - -Imaging -ImagingChopOr(Imaging imIn1, Imaging imIn2) { - CHOP2((in1[x] || in2[x]) ? 255 : 0, "1"); -} - -Imaging -ImagingChopXor(Imaging imIn1, Imaging imIn2) { - CHOP2(((in1[x] != 0) ^ (in2[x] != 0)) ? 255 : 0, "1"); -} - -Imaging -ImagingChopAddModulo(Imaging imIn1, Imaging imIn2) { - CHOP2(in1[x] + in2[x], NULL); -} - -Imaging -ImagingChopSubtractModulo(Imaging imIn1, Imaging imIn2) { - CHOP2(in1[x] - in2[x], NULL); -} - -Imaging -ImagingChopSoftLight(Imaging imIn1, Imaging imIn2) { - CHOP2( - (((255 - in1[x]) * (in1[x] * in2[x])) / 65536) + - (in1[x] * (255 - ((255 - in1[x]) * (255 - in2[x]) / 255))) / 255, - NULL); -} - -Imaging -ImagingChopHardLight(Imaging imIn1, Imaging imIn2) { - CHOP2( - (in2[x] < 128) ? ((in1[x] * in2[x]) / 127) - : 255 - (((255 - in2[x]) * (255 - in1[x])) / 127), - NULL); -} - -Imaging -ImagingOverlay(Imaging imIn1, Imaging imIn2) { - CHOP2( - (in1[x] < 128) ? ((in1[x] * in2[x]) / 127) - : 255 - (((255 - in1[x]) * (255 - in2[x])) / 127), - NULL); -} +/* + * The Python Imaging Library + * $Id$ + * + * basic channel operations + * + * history: + * 1996-03-28 fl Created + * 1996-08-13 fl Added and/or/xor for "1" images + * 1996-12-14 fl Added add_modulo, sub_modulo + * 2005-09-10 fl Fixed output values from and/or/xor + * + * Copyright (c) 1996 by Fredrik Lundh. + * Copyright (c) 1997 by Secret Labs AB. + * + * See the README file for details on usage and redistribution. + */ + +#include "Imaging.h" + +#define CHOP(operation) \ + int x, y; \ + Imaging imOut; \ + imOut = create(imIn1, imIn2, NULL); \ + if (!imOut) { \ + return NULL; \ + } \ + for (y = 0; y < imOut->ysize; y++) { \ + UINT8 *out = (UINT8 *)imOut->image[y]; \ + UINT8 *in1 = (UINT8 *)imIn1->image[y]; \ + UINT8 *in2 = (UINT8 *)imIn2->image[y]; \ + for (x = 0; x < imOut->linesize; x++) { \ + int temp = operation; \ + if (temp <= 0) { \ + out[x] = 0; \ + } else if (temp >= 255) { \ + out[x] = 255; \ + } else { \ + out[x] = temp; \ + } \ + } \ + } \ + return imOut; + +#define CHOP2(operation, mode) \ + int x, y; \ + Imaging imOut; \ + imOut = create(imIn1, imIn2, mode); \ + if (!imOut) { \ + return NULL; \ + } \ + for (y = 0; y < imOut->ysize; y++) { \ + UINT8 *out = (UINT8 *)imOut->image[y]; \ + UINT8 *in1 = (UINT8 *)imIn1->image[y]; \ + UINT8 *in2 = (UINT8 *)imIn2->image[y]; \ + for (x = 0; x < imOut->linesize; x++) { \ + out[x] = operation; \ + } \ + } \ + return imOut; + +static Imaging +create(Imaging im1, Imaging im2, char *mode) { + int xsize, ysize; + + if (!im1 || !im2 || im1->type != IMAGING_TYPE_UINT8 || + (mode != NULL && (strcmp(im1->mode, "1") || strcmp(im2->mode, "1")))) { + return (Imaging)ImagingError_ModeError(); + } + if (im1->type != im2->type || im1->bands != im2->bands) { + return (Imaging)ImagingError_Mismatch(); + } + + xsize = (im1->xsize < im2->xsize) ? im1->xsize : im2->xsize; + ysize = (im1->ysize < im2->ysize) ? im1->ysize : im2->ysize; + + return ImagingNewDirty(im1->mode, xsize, ysize); +} + +Imaging +ImagingChopLighter(Imaging imIn1, Imaging imIn2) { + CHOP((in1[x] > in2[x]) ? in1[x] : in2[x]); +} + +Imaging +ImagingChopDarker(Imaging imIn1, Imaging imIn2) { + CHOP((in1[x] < in2[x]) ? in1[x] : in2[x]); +} + +Imaging +ImagingChopDifference(Imaging imIn1, Imaging imIn2) { + CHOP(abs((int)in1[x] - (int)in2[x])); +} + +Imaging +ImagingChopMultiply(Imaging imIn1, Imaging imIn2) { + CHOP((int)in1[x] * (int)in2[x] / 255); +} + +Imaging +ImagingChopScreen(Imaging imIn1, Imaging imIn2) { + CHOP(255 - ((int)(255 - in1[x]) * (int)(255 - in2[x])) / 255); +} + +Imaging +ImagingChopAdd(Imaging imIn1, Imaging imIn2, float scale, int offset) { + CHOP(((int)in1[x] + (int)in2[x]) / scale + offset); +} + +Imaging +ImagingChopSubtract(Imaging imIn1, Imaging imIn2, float scale, int offset) { + CHOP(((int)in1[x] - (int)in2[x]) / scale + offset); +} + +Imaging +ImagingChopAnd(Imaging imIn1, Imaging imIn2) { + CHOP2((in1[x] && in2[x]) ? 255 : 0, "1"); +} + +Imaging +ImagingChopOr(Imaging imIn1, Imaging imIn2) { + CHOP2((in1[x] || in2[x]) ? 255 : 0, "1"); +} + +Imaging +ImagingChopXor(Imaging imIn1, Imaging imIn2) { + CHOP2(((in1[x] != 0) ^ (in2[x] != 0)) ? 255 : 0, "1"); +} + +Imaging +ImagingChopAddModulo(Imaging imIn1, Imaging imIn2) { + CHOP2(in1[x] + in2[x], NULL); +} + +Imaging +ImagingChopSubtractModulo(Imaging imIn1, Imaging imIn2) { + CHOP2(in1[x] - in2[x], NULL); +} + +Imaging +ImagingChopSoftLight(Imaging imIn1, Imaging imIn2) { + CHOP2( + (((255 - in1[x]) * (in1[x] * in2[x])) / 65536) + + (in1[x] * (255 - ((255 - in1[x]) * (255 - in2[x]) / 255))) / 255, + NULL); +} + +Imaging +ImagingChopHardLight(Imaging imIn1, Imaging imIn2) { + CHOP2( + (in2[x] < 128) ? ((in1[x] * in2[x]) / 127) + : 255 - (((255 - in2[x]) * (255 - in1[x])) / 127), + NULL); +} + +Imaging +ImagingOverlay(Imaging imIn1, Imaging imIn2) { + CHOP2( + (in1[x] < 128) ? ((in1[x] * in2[x]) / 127) + : 255 - (((255 - in1[x]) * (255 - in2[x])) / 127), + NULL); +} diff --git a/src/libImaging/ColorLUT.c b/src/libImaging/ColorLUT.c index aee7cda06..aaccf9276 100644 --- a/src/libImaging/ColorLUT.c +++ b/src/libImaging/ColorLUT.c @@ -1,187 +1,187 @@ -#include "Imaging.h" -#include - -/* 8 bits for result. Table can overflow [0, 1.0] range, - so we need extra bits for overflow and negative values. - NOTE: This value should be the same as in _imaging/_prepare_lut_table() */ -#define PRECISION_BITS (16 - 8 - 2) -#define PRECISION_ROUNDING (1 << (PRECISION_BITS - 1)) - -/* 8 - scales are multiplied on byte. - 6 - max index in the table - (max size is 65, but index 64 is not reachable) */ -#define SCALE_BITS (32 - 8 - 6) -#define SCALE_MASK ((1 << SCALE_BITS) - 1) - -#define SHIFT_BITS (16 - 1) - -static inline UINT8 -clip8(int in) { - return clip8_lookups[(in + PRECISION_ROUNDING) >> PRECISION_BITS]; -} - -static inline void -interpolate3(INT16 out[3], const INT16 a[3], const INT16 b[3], INT16 shift) { - out[0] = (a[0] * ((1 << SHIFT_BITS) - shift) + b[0] * shift) >> SHIFT_BITS; - out[1] = (a[1] * ((1 << SHIFT_BITS) - shift) + b[1] * shift) >> SHIFT_BITS; - out[2] = (a[2] * ((1 << SHIFT_BITS) - shift) + b[2] * shift) >> SHIFT_BITS; -} - -static inline void -interpolate4(INT16 out[4], const INT16 a[4], const INT16 b[4], INT16 shift) { - out[0] = (a[0] * ((1 << SHIFT_BITS) - shift) + b[0] * shift) >> SHIFT_BITS; - out[1] = (a[1] * ((1 << SHIFT_BITS) - shift) + b[1] * shift) >> SHIFT_BITS; - out[2] = (a[2] * ((1 << SHIFT_BITS) - shift) + b[2] * shift) >> SHIFT_BITS; - out[3] = (a[3] * ((1 << SHIFT_BITS) - shift) + b[3] * shift) >> SHIFT_BITS; -} - -static inline int -table_index3D(int index1D, int index2D, int index3D, int size1D, int size1D_2D) { - return index1D + index2D * size1D + index3D * size1D_2D; -} - -/* - Transforms colors of imIn using provided 3D lookup table - and puts the result in imOut. Returns imOut on success or 0 on error. - - imOut, imIn - images, should be the same size and may be the same image. - Should have 3 or 4 channels. - table_channels - number of channels in the lookup table, 3 or 4. - Should be less or equal than number of channels in imOut image; - size1D, size_2D and size3D - dimensions of provided table; - table - flat table, - array with table_channels * size1D * size2D * size3D elements, - where channels are changed first, then 1D, then 2D, then 3D. - Each element is signed 16-bit int where 0 is lowest output value - and 255 << PRECISION_BITS (16320) is highest value. -*/ -Imaging -ImagingColorLUT3D_linear( - Imaging imOut, - Imaging imIn, - int table_channels, - int size1D, - int size2D, - int size3D, - INT16 *table) { - /* This float to int conversion doesn't have rounding - error compensation (+0.5) for two reasons: - 1. As we don't hit the highest value, - we can use one extra bit for precision. - 2. For every pixel, we interpolate 8 elements from the table: - current and +1 for every dimension and their combinations. - If we hit the upper cells from the table, - +1 cells will be outside of the table. - With this compensation we never hit the upper cells - but this also doesn't introduce any noticeable difference. */ - UINT32 scale1D = (size1D - 1) / 255.0 * (1 << SCALE_BITS); - UINT32 scale2D = (size2D - 1) / 255.0 * (1 << SCALE_BITS); - UINT32 scale3D = (size3D - 1) / 255.0 * (1 << SCALE_BITS); - int size1D_2D = size1D * size2D; - int x, y; - ImagingSectionCookie cookie; - - if (table_channels < 3 || table_channels > 4) { - PyErr_SetString(PyExc_ValueError, "table_channels could be 3 or 4"); - return NULL; - } - - if (imIn->type != IMAGING_TYPE_UINT8 || imOut->type != IMAGING_TYPE_UINT8 || - imIn->bands < 3 || imOut->bands < table_channels) { - return (Imaging)ImagingError_ModeError(); - } - - /* In case we have one extra band in imOut and don't have in imIn.*/ - if (imOut->bands > table_channels && imOut->bands > imIn->bands) { - return (Imaging)ImagingError_ModeError(); - } - - ImagingSectionEnter(&cookie); - for (y = 0; y < imOut->ysize; y++) { - UINT8 *rowIn = (UINT8 *)imIn->image[y]; - char *rowOut = (char *)imOut->image[y]; - for (x = 0; x < imOut->xsize; x++) { - UINT32 index1D = rowIn[x * 4 + 0] * scale1D; - UINT32 index2D = rowIn[x * 4 + 1] * scale2D; - UINT32 index3D = rowIn[x * 4 + 2] * scale3D; - INT16 shift1D = (SCALE_MASK & index1D) >> (SCALE_BITS - SHIFT_BITS); - INT16 shift2D = (SCALE_MASK & index2D) >> (SCALE_BITS - SHIFT_BITS); - INT16 shift3D = (SCALE_MASK & index3D) >> (SCALE_BITS - SHIFT_BITS); - int idx = table_channels * table_index3D( - index1D >> SCALE_BITS, - index2D >> SCALE_BITS, - index3D >> SCALE_BITS, - size1D, - size1D_2D); - INT16 result[4], left[4], right[4]; - INT16 leftleft[4], leftright[4], rightleft[4], rightright[4]; - - if (table_channels == 3) { - UINT32 v; - interpolate3(leftleft, &table[idx + 0], &table[idx + 3], shift1D); - interpolate3( - leftright, - &table[idx + size1D * 3], - &table[idx + size1D * 3 + 3], - shift1D); - interpolate3(left, leftleft, leftright, shift2D); - - interpolate3( - rightleft, - &table[idx + size1D_2D * 3], - &table[idx + size1D_2D * 3 + 3], - shift1D); - interpolate3( - rightright, - &table[idx + size1D_2D * 3 + size1D * 3], - &table[idx + size1D_2D * 3 + size1D * 3 + 3], - shift1D); - interpolate3(right, rightleft, rightright, shift2D); - - interpolate3(result, left, right, shift3D); - - v = MAKE_UINT32( - clip8(result[0]), - clip8(result[1]), - clip8(result[2]), - rowIn[x * 4 + 3]); - memcpy(rowOut + x * sizeof(v), &v, sizeof(v)); - } - - if (table_channels == 4) { - UINT32 v; - interpolate4(leftleft, &table[idx + 0], &table[idx + 4], shift1D); - interpolate4( - leftright, - &table[idx + size1D * 4], - &table[idx + size1D * 4 + 4], - shift1D); - interpolate4(left, leftleft, leftright, shift2D); - - interpolate4( - rightleft, - &table[idx + size1D_2D * 4], - &table[idx + size1D_2D * 4 + 4], - shift1D); - interpolate4( - rightright, - &table[idx + size1D_2D * 4 + size1D * 4], - &table[idx + size1D_2D * 4 + size1D * 4 + 4], - shift1D); - interpolate4(right, rightleft, rightright, shift2D); - - interpolate4(result, left, right, shift3D); - - v = MAKE_UINT32( - clip8(result[0]), - clip8(result[1]), - clip8(result[2]), - clip8(result[3])); - memcpy(rowOut + x * sizeof(v), &v, sizeof(v)); - } - } - } - ImagingSectionLeave(&cookie); - - return imOut; -} +#include "Imaging.h" +#include + +/* 8 bits for result. Table can overflow [0, 1.0] range, + so we need extra bits for overflow and negative values. + NOTE: This value should be the same as in _imaging/_prepare_lut_table() */ +#define PRECISION_BITS (16 - 8 - 2) +#define PRECISION_ROUNDING (1 << (PRECISION_BITS - 1)) + +/* 8 - scales are multiplied on byte. + 6 - max index in the table + (max size is 65, but index 64 is not reachable) */ +#define SCALE_BITS (32 - 8 - 6) +#define SCALE_MASK ((1 << SCALE_BITS) - 1) + +#define SHIFT_BITS (16 - 1) + +static inline UINT8 +clip8(int in) { + return clip8_lookups[(in + PRECISION_ROUNDING) >> PRECISION_BITS]; +} + +static inline void +interpolate3(INT16 out[3], const INT16 a[3], const INT16 b[3], INT16 shift) { + out[0] = (a[0] * ((1 << SHIFT_BITS) - shift) + b[0] * shift) >> SHIFT_BITS; + out[1] = (a[1] * ((1 << SHIFT_BITS) - shift) + b[1] * shift) >> SHIFT_BITS; + out[2] = (a[2] * ((1 << SHIFT_BITS) - shift) + b[2] * shift) >> SHIFT_BITS; +} + +static inline void +interpolate4(INT16 out[4], const INT16 a[4], const INT16 b[4], INT16 shift) { + out[0] = (a[0] * ((1 << SHIFT_BITS) - shift) + b[0] * shift) >> SHIFT_BITS; + out[1] = (a[1] * ((1 << SHIFT_BITS) - shift) + b[1] * shift) >> SHIFT_BITS; + out[2] = (a[2] * ((1 << SHIFT_BITS) - shift) + b[2] * shift) >> SHIFT_BITS; + out[3] = (a[3] * ((1 << SHIFT_BITS) - shift) + b[3] * shift) >> SHIFT_BITS; +} + +static inline int +table_index3D(int index1D, int index2D, int index3D, int size1D, int size1D_2D) { + return index1D + index2D * size1D + index3D * size1D_2D; +} + +/* + Transforms colors of imIn using provided 3D lookup table + and puts the result in imOut. Returns imOut on success or 0 on error. + + imOut, imIn - images, should be the same size and may be the same image. + Should have 3 or 4 channels. + table_channels - number of channels in the lookup table, 3 or 4. + Should be less or equal than number of channels in imOut image; + size1D, size_2D and size3D - dimensions of provided table; + table - flat table, + array with table_channels * size1D * size2D * size3D elements, + where channels are changed first, then 1D, then 2D, then 3D. + Each element is signed 16-bit int where 0 is lowest output value + and 255 << PRECISION_BITS (16320) is highest value. +*/ +Imaging +ImagingColorLUT3D_linear( + Imaging imOut, + Imaging imIn, + int table_channels, + int size1D, + int size2D, + int size3D, + INT16 *table) { + /* This float to int conversion doesn't have rounding + error compensation (+0.5) for two reasons: + 1. As we don't hit the highest value, + we can use one extra bit for precision. + 2. For every pixel, we interpolate 8 elements from the table: + current and +1 for every dimension and their combinations. + If we hit the upper cells from the table, + +1 cells will be outside of the table. + With this compensation we never hit the upper cells + but this also doesn't introduce any noticeable difference. */ + UINT32 scale1D = (size1D - 1) / 255.0 * (1 << SCALE_BITS); + UINT32 scale2D = (size2D - 1) / 255.0 * (1 << SCALE_BITS); + UINT32 scale3D = (size3D - 1) / 255.0 * (1 << SCALE_BITS); + int size1D_2D = size1D * size2D; + int x, y; + ImagingSectionCookie cookie; + + if (table_channels < 3 || table_channels > 4) { + PyErr_SetString(PyExc_ValueError, "table_channels could be 3 or 4"); + return NULL; + } + + if (imIn->type != IMAGING_TYPE_UINT8 || imOut->type != IMAGING_TYPE_UINT8 || + imIn->bands < 3 || imOut->bands < table_channels) { + return (Imaging)ImagingError_ModeError(); + } + + /* In case we have one extra band in imOut and don't have in imIn.*/ + if (imOut->bands > table_channels && imOut->bands > imIn->bands) { + return (Imaging)ImagingError_ModeError(); + } + + ImagingSectionEnter(&cookie); + for (y = 0; y < imOut->ysize; y++) { + UINT8 *rowIn = (UINT8 *)imIn->image[y]; + char *rowOut = (char *)imOut->image[y]; + for (x = 0; x < imOut->xsize; x++) { + UINT32 index1D = rowIn[x * 4 + 0] * scale1D; + UINT32 index2D = rowIn[x * 4 + 1] * scale2D; + UINT32 index3D = rowIn[x * 4 + 2] * scale3D; + INT16 shift1D = (SCALE_MASK & index1D) >> (SCALE_BITS - SHIFT_BITS); + INT16 shift2D = (SCALE_MASK & index2D) >> (SCALE_BITS - SHIFT_BITS); + INT16 shift3D = (SCALE_MASK & index3D) >> (SCALE_BITS - SHIFT_BITS); + int idx = table_channels * table_index3D( + index1D >> SCALE_BITS, + index2D >> SCALE_BITS, + index3D >> SCALE_BITS, + size1D, + size1D_2D); + INT16 result[4], left[4], right[4]; + INT16 leftleft[4], leftright[4], rightleft[4], rightright[4]; + + if (table_channels == 3) { + UINT32 v; + interpolate3(leftleft, &table[idx + 0], &table[idx + 3], shift1D); + interpolate3( + leftright, + &table[idx + size1D * 3], + &table[idx + size1D * 3 + 3], + shift1D); + interpolate3(left, leftleft, leftright, shift2D); + + interpolate3( + rightleft, + &table[idx + size1D_2D * 3], + &table[idx + size1D_2D * 3 + 3], + shift1D); + interpolate3( + rightright, + &table[idx + size1D_2D * 3 + size1D * 3], + &table[idx + size1D_2D * 3 + size1D * 3 + 3], + shift1D); + interpolate3(right, rightleft, rightright, shift2D); + + interpolate3(result, left, right, shift3D); + + v = MAKE_UINT32( + clip8(result[0]), + clip8(result[1]), + clip8(result[2]), + rowIn[x * 4 + 3]); + memcpy(rowOut + x * sizeof(v), &v, sizeof(v)); + } + + if (table_channels == 4) { + UINT32 v; + interpolate4(leftleft, &table[idx + 0], &table[idx + 4], shift1D); + interpolate4( + leftright, + &table[idx + size1D * 4], + &table[idx + size1D * 4 + 4], + shift1D); + interpolate4(left, leftleft, leftright, shift2D); + + interpolate4( + rightleft, + &table[idx + size1D_2D * 4], + &table[idx + size1D_2D * 4 + 4], + shift1D); + interpolate4( + rightright, + &table[idx + size1D_2D * 4 + size1D * 4], + &table[idx + size1D_2D * 4 + size1D * 4 + 4], + shift1D); + interpolate4(right, rightleft, rightright, shift2D); + + interpolate4(result, left, right, shift3D); + + v = MAKE_UINT32( + clip8(result[0]), + clip8(result[1]), + clip8(result[2]), + clip8(result[3])); + memcpy(rowOut + x * sizeof(v), &v, sizeof(v)); + } + } + } + ImagingSectionLeave(&cookie); + + return imOut; +} diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c index fcb5f7ad9..0a39098c0 100644 --- a/src/libImaging/Convert.c +++ b/src/libImaging/Convert.c @@ -1,1802 +1,1802 @@ -/* - * The Python Imaging Library - * $Id$ - * - * convert images - * - * history: - * 1995-06-15 fl created - * 1995-11-28 fl added some "RGBA" and "CMYK" conversions - * 1996-04-22 fl added "1" conversions (same as "L") - * 1996-05-05 fl added palette conversions (hack) - * 1996-07-23 fl fixed "1" conversions to zero/non-zero convention - * 1996-11-01 fl fixed "P" to "L" and "RGB" to "1" conversions - * 1996-12-29 fl set alpha byte in RGB converters - * 1997-05-12 fl added ImagingConvert2 - * 1997-05-30 fl added floating point support - * 1997-08-27 fl added "P" to "1" and "P" to "F" conversions - * 1998-01-11 fl added integer support - * 1998-07-01 fl added "YCbCr" support - * 1998-07-02 fl added "RGBX" conversions (sort of) - * 1998-07-04 fl added floyd-steinberg dithering - * 1998-07-12 fl changed "YCrCb" to "YCbCr" (!) - * 1998-12-29 fl added basic "I;16" and "I;16B" conversions - * 1999-02-03 fl added "RGBa", and "BGR" conversions (experimental) - * 2003-09-26 fl added "LA" and "PA" conversions (experimental) - * 2005-05-05 fl fixed "P" to "1" threshold - * 2005-12-08 fl fixed palette memory leak in topalette - * - * Copyright (c) 1997-2005 by Secret Labs AB. - * Copyright (c) 1995-1997 by Fredrik Lundh. - * - * See the README file for details on usage and redistribution. - */ - -#include "Imaging.h" - -#define MAX(a, b) (a) > (b) ? (a) : (b) -#define MIN(a, b) (a) < (b) ? (a) : (b) - -#define CLIP16(v) ((v) <= 0 ? 0 : (v) >= 65535 ? 65535 : (v)) - -/* ITU-R Recommendation 601-2 (assuming nonlinear RGB) */ -#define L(rgb) ((INT32)(rgb)[0] * 299 + (INT32)(rgb)[1] * 587 + (INT32)(rgb)[2] * 114) -#define L24(rgb) ((rgb)[0] * 19595 + (rgb)[1] * 38470 + (rgb)[2] * 7471 + 0x8000) - -/* ------------------- */ -/* 1 (bit) conversions */ -/* ------------------- */ - -static void -bit2l(UINT8 *out, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++) *out++ = (*in++ != 0) ? 255 : 0; -} - -static void -bit2rgb(UINT8 *out, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++) { - UINT8 v = (*in++ != 0) ? 255 : 0; - *out++ = v; - *out++ = v; - *out++ = v; - *out++ = 255; - } -} - -static void -bit2cmyk(UINT8 *out, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++) { - *out++ = 0; - *out++ = 0; - *out++ = 0; - *out++ = (*in++ != 0) ? 0 : 255; - } -} - -static void -bit2ycbcr(UINT8 *out, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++) { - *out++ = (*in++ != 0) ? 255 : 0; - *out++ = 128; - *out++ = 128; - *out++ = 255; - } -} - -static void -bit2hsv(UINT8 *out, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++, out += 4) { - UINT8 v = (*in++ != 0) ? 255 : 0; - out[0] = 0; - out[1] = 0; - out[2] = v; - out[3] = 255; - } -} - -/* ----------------- */ -/* RGB/L conversions */ -/* ----------------- */ - -static void -l2bit(UINT8 *out, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++) { - *out++ = (*in++ >= 128) ? 255 : 0; - } -} - -static void -lA2la(UINT8 *out, const UINT8 *in, int xsize) { - int x; - unsigned int alpha, pixel, tmp; - for (x = 0; x < xsize; x++, in += 4) { - alpha = in[3]; - pixel = MULDIV255(in[0], alpha, tmp); - *out++ = (UINT8)pixel; - *out++ = (UINT8)pixel; - *out++ = (UINT8)pixel; - *out++ = (UINT8)alpha; - } -} - -/* RGBa -> RGBA conversion to remove premultiplication - Needed for correct transforms/resizing on RGBA images */ -static void -la2lA(UINT8 *out, const UINT8 *in, int xsize) { - int x; - unsigned int alpha, pixel; - for (x = 0; x < xsize; x++, in += 4) { - alpha = in[3]; - if (alpha == 255 || alpha == 0) { - pixel = in[0]; - } else { - pixel = CLIP8((255 * in[0]) / alpha); - } - *out++ = (UINT8)pixel; - *out++ = (UINT8)pixel; - *out++ = (UINT8)pixel; - *out++ = (UINT8)alpha; - } -} - -static void -l2la(UINT8 *out, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++) { - UINT8 v = *in++; - *out++ = v; - *out++ = v; - *out++ = v; - *out++ = 255; - } -} - -static void -l2rgb(UINT8 *out, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++) { - UINT8 v = *in++; - *out++ = v; - *out++ = v; - *out++ = v; - *out++ = 255; - } -} - -static void -l2hsv(UINT8 *out, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++, out += 4) { - UINT8 v = *in++; - out[0] = 0; - out[1] = 0; - out[2] = v; - out[3] = 255; - } -} - -static void -la2l(UINT8 *out, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++, in += 4) { - *out++ = in[0]; - } -} - -static void -la2rgb(UINT8 *out, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++, in += 4) { - UINT8 v = in[0]; - *out++ = v; - *out++ = v; - *out++ = v; - *out++ = in[3]; - } -} - -static void -la2hsv(UINT8 *out, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++, in += 4, out += 4) { - UINT8 v = in[0]; - out[0] = 0; - out[1] = 0; - out[2] = v; - out[3] = in[3]; - } -} - -static void -rgb2bit(UINT8 *out, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++, in += 4) { - /* ITU-R Recommendation 601-2 (assuming nonlinear RGB) */ - *out++ = (L(in) >= 128000) ? 255 : 0; - } -} - -static void -rgb2l(UINT8 *out, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++, in += 4) { - /* ITU-R Recommendation 601-2 (assuming nonlinear RGB) */ - *out++ = L24(in) >> 16; - } -} - -static void -rgb2la(UINT8 *out, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++, in += 4, out += 4) { - /* ITU-R Recommendation 601-2 (assuming nonlinear RGB) */ - out[0] = out[1] = out[2] = L24(in) >> 16; - out[3] = 255; - } -} - -static void -rgb2i(UINT8 *out_, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++, in += 4, out_ += 4) { - INT32 v = L24(in) >> 16; - memcpy(out_, &v, sizeof(v)); - } -} - -static void -rgb2i16l(UINT8 *out_, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++, in += 4) { - *out_++ = L24(in) >> 16; - *out_++ = 0; - } -} - -static void -rgb2i16b(UINT8 *out_, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++, in += 4) { - *out_++ = 0; - *out_++ = L24(in) >> 16; - } -} - -static void -rgb2f(UINT8 *out_, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++, in += 4, out_ += 4) { - FLOAT32 v = (float)L(in) / 1000.0F; - memcpy(out_, &v, sizeof(v)); - } -} - -static void -rgb2bgr15(UINT8 *out_, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++, in += 4, out_ += 2) { - UINT16 v = ((((UINT16)in[0]) << 7) & 0x7c00) + - ((((UINT16)in[1]) << 2) & 0x03e0) + - ((((UINT16)in[2]) >> 3) & 0x001f); - memcpy(out_, &v, sizeof(v)); - } -} - -static void -rgb2bgr16(UINT8 *out_, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++, in += 4, out_ += 2) { - UINT16 v = ((((UINT16)in[0]) << 8) & 0xf800) + - ((((UINT16)in[1]) << 3) & 0x07e0) + - ((((UINT16)in[2]) >> 3) & 0x001f); - memcpy(out_, &v, sizeof(v)); - } -} - -static void -rgb2bgr24(UINT8 *out, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++, in += 4) { - *out++ = in[2]; - *out++ = in[1]; - *out++ = in[0]; - } -} - -static void -rgb2hsv_row(UINT8 *out, const UINT8 *in) { // following colorsys.py - float h, s, rc, gc, bc, cr; - UINT8 maxc, minc; - UINT8 r, g, b; - UINT8 uh, us, uv; - - r = in[0]; - g = in[1]; - b = in[2]; - maxc = MAX(r, MAX(g, b)); - minc = MIN(r, MIN(g, b)); - uv = maxc; - if (minc == maxc) { - uh = 0; - us = 0; - } else { - cr = (float)(maxc - minc); - s = cr / (float)maxc; - rc = ((float)(maxc - r)) / cr; - gc = ((float)(maxc - g)) / cr; - bc = ((float)(maxc - b)) / cr; - if (r == maxc) { - h = bc - gc; - } else if (g == maxc) { - h = 2.0 + rc - bc; - } else { - h = 4.0 + gc - rc; - } - // incorrect hue happens if h/6 is negative. - h = fmod((h / 6.0 + 1.0), 1.0); - - uh = (UINT8)CLIP8((int)(h * 255.0)); - us = (UINT8)CLIP8((int)(s * 255.0)); - } - out[0] = uh; - out[1] = us; - out[2] = uv; -} - -static void -rgb2hsv(UINT8 *out, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++, in += 4, out += 4) { - rgb2hsv_row(out, in); - out[3] = in[3]; - } -} - -static void -hsv2rgb(UINT8 *out, const UINT8 *in, int xsize) { // following colorsys.py - - int p, q, t; - UINT8 up, uq, ut; - int i, x; - float f, fs; - UINT8 h, s, v; - - for (x = 0; x < xsize; x++, in += 4) { - h = in[0]; - s = in[1]; - v = in[2]; - - if (s == 0) { - *out++ = v; - *out++ = v; - *out++ = v; - } else { - i = floor((float)h * 6.0 / 255.0); // 0 - 6 - f = (float)h * 6.0 / 255.0 - (float)i; // 0-1 : remainder. - fs = ((float)s) / 255.0; - - p = round((float)v * (1.0 - fs)); - q = round((float)v * (1.0 - fs * f)); - t = round((float)v * (1.0 - fs * (1.0 - f))); - up = (UINT8)CLIP8(p); - uq = (UINT8)CLIP8(q); - ut = (UINT8)CLIP8(t); - - switch (i % 6) { - case 0: - *out++ = v; - *out++ = ut; - *out++ = up; - break; - case 1: - *out++ = uq; - *out++ = v; - *out++ = up; - break; - case 2: - *out++ = up; - *out++ = v; - *out++ = ut; - break; - case 3: - *out++ = up; - *out++ = uq; - *out++ = v; - break; - case 4: - *out++ = ut; - *out++ = up; - *out++ = v; - break; - case 5: - *out++ = v; - *out++ = up; - *out++ = uq; - break; - } - } - *out++ = in[3]; - } -} - -/* ---------------- */ -/* RGBA conversions */ -/* ---------------- */ - -static void -rgb2rgba(UINT8 *out, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++) { - *out++ = *in++; - *out++ = *in++; - *out++ = *in++; - *out++ = 255; - in++; - } -} - -static void -rgba2la(UINT8 *out, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++, in += 4, out += 4) { - /* ITU-R Recommendation 601-2 (assuming nonlinear RGB) */ - out[0] = out[1] = out[2] = L24(in) >> 16; - out[3] = in[3]; - } -} - -static void -rgba2rgb(UINT8 *out, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++) { - *out++ = *in++; - *out++ = *in++; - *out++ = *in++; - *out++ = 255; - in++; - } -} - -static void -rgbA2rgba(UINT8 *out, const UINT8 *in, int xsize) { - int x; - unsigned int alpha, tmp; - for (x = 0; x < xsize; x++) { - alpha = in[3]; - *out++ = MULDIV255(*in++, alpha, tmp); - *out++ = MULDIV255(*in++, alpha, tmp); - *out++ = MULDIV255(*in++, alpha, tmp); - *out++ = *in++; - } -} - -/* RGBa -> RGBA conversion to remove premultiplication - Needed for correct transforms/resizing on RGBA images */ -static void -rgba2rgbA(UINT8 *out, const UINT8 *in, int xsize) { - int x; - unsigned int alpha; - for (x = 0; x < xsize; x++, in += 4) { - alpha = in[3]; - if (alpha == 255 || alpha == 0) { - *out++ = in[0]; - *out++ = in[1]; - *out++ = in[2]; - } else { - *out++ = CLIP8((255 * in[0]) / alpha); - *out++ = CLIP8((255 * in[1]) / alpha); - *out++ = CLIP8((255 * in[2]) / alpha); - } - *out++ = in[3]; - } -} - -static void -rgba2rgb_(UINT8 *out, const UINT8 *in, int xsize) { - int x; - unsigned int alpha; - for (x = 0; x < xsize; x++, in += 4) { - alpha = in[3]; - if (alpha == 255 || alpha == 0) { - *out++ = in[0]; - *out++ = in[1]; - *out++ = in[2]; - } else { - *out++ = CLIP8((255 * in[0]) / alpha); - *out++ = CLIP8((255 * in[1]) / alpha); - *out++ = CLIP8((255 * in[2]) / alpha); - } - *out++ = 255; - } -} - -/* - * Conversion of RGB + single transparent color either to - * RGBA or LA, where any pixel matching the color will have the alpha channel set to 0, - * or RGBa or La, where any pixel matching the color will have all channels set to 0 - */ - -static void -rgbT2a(UINT8 *out, UINT8 *in, int xsize, int r, int g, int b, int premultiplied) { -#ifdef WORDS_BIGENDIAN - UINT32 trns = ((r & 0xff) << 24) | ((g & 0xff) << 16) | ((b & 0xff) << 8) | 0xff; - UINT32 repl = premultiplied ? 0 : (trns & 0xffffff00); -#else - UINT32 trns = (0xffU << 24) | ((b & 0xff) << 16) | ((g & 0xff) << 8) | (r & 0xff); - UINT32 repl = premultiplied ? 0 : (trns & 0x00ffffff); -#endif - - int i; - - UINT8 *ref = in != NULL ? in : out; - for (i = 0; i < xsize; i++, ref += sizeof(trns), out += sizeof(trns)) { - UINT32 v; - memcpy(&v, ref, sizeof(v)); - if (v == trns) { - memcpy(out, &repl, sizeof(repl)); - } - } -} - -/* ---------------- */ -/* CMYK conversions */ -/* ---------------- */ - -static void -l2cmyk(UINT8 *out, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++) { - *out++ = 0; - *out++ = 0; - *out++ = 0; - *out++ = ~(*in++); - } -} - -static void -la2cmyk(UINT8 *out, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++, in += 4) { - *out++ = 0; - *out++ = 0; - *out++ = 0; - *out++ = ~(in[0]); - } -} - -static void -rgb2cmyk(UINT8 *out, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++) { - /* Note: no undercolour removal */ - *out++ = ~(*in++); - *out++ = ~(*in++); - *out++ = ~(*in++); - *out++ = 0; - in++; - } -} - -void -cmyk2rgb(UINT8 *out, const UINT8 *in, int xsize) { - int x, nk, tmp; - for (x = 0; x < xsize; x++) { - nk = 255 - in[3]; - out[0] = CLIP8(nk - MULDIV255(in[0], nk, tmp)); - out[1] = CLIP8(nk - MULDIV255(in[1], nk, tmp)); - out[2] = CLIP8(nk - MULDIV255(in[2], nk, tmp)); - out[3] = 255; - out += 4; - in += 4; - } -} - -static void -cmyk2hsv(UINT8 *out, const UINT8 *in, int xsize) { - int x, nk, tmp; - for (x = 0; x < xsize; x++) { - nk = 255 - in[3]; - out[0] = CLIP8(nk - MULDIV255(in[0], nk, tmp)); - out[1] = CLIP8(nk - MULDIV255(in[1], nk, tmp)); - out[2] = CLIP8(nk - MULDIV255(in[2], nk, tmp)); - rgb2hsv_row(out, out); - out[3] = 255; - out += 4; - in += 4; - } -} - -/* ------------- */ -/* I conversions */ -/* ------------- */ - -static void -bit2i(UINT8 *out_, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++, out_ += 4) { - INT32 v = (*in++ != 0) ? 255 : 0; - memcpy(out_, &v, sizeof(v)); - } -} - -static void -l2i(UINT8 *out_, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++, out_ += 4) { - INT32 v = *in++; - memcpy(out_, &v, sizeof(v)); - } -} - -static void -i2l(UINT8 *out, const UINT8 *in_, int xsize) { - int x; - for (x = 0; x < xsize; x++, out++, in_ += 4) { - INT32 v; - memcpy(&v, in_, sizeof(v)); - if (v <= 0) { - *out = 0; - } else if (v >= 255) { - *out = 255; - } else { - *out = (UINT8)v; - } - } -} - -static void -i2f(UINT8 *out_, const UINT8 *in_, int xsize) { - int x; - for (x = 0; x < xsize; x++, in_ += 4, out_ += 4) { - INT32 i; - FLOAT32 f; - memcpy(&i, in_, sizeof(i)); - f = i; - memcpy(out_, &f, sizeof(f)); - } -} - -static void -i2rgb(UINT8 *out, const UINT8 *in_, int xsize) { - int x; - INT32 *in = (INT32 *)in_; - for (x = 0; x < xsize; x++, in++, out += 4) { - if (*in <= 0) { - out[0] = out[1] = out[2] = 0; - } else if (*in >= 255) { - out[0] = out[1] = out[2] = 255; - } else { - out[0] = out[1] = out[2] = (UINT8)*in; - } - out[3] = 255; - } -} - -static void -i2hsv(UINT8 *out, const UINT8 *in_, int xsize) { - int x; - INT32 *in = (INT32 *)in_; - for (x = 0; x < xsize; x++, in++, out += 4) { - out[0] = 0; - out[1] = 0; - if (*in <= 0) { - out[2] = 0; - } else if (*in >= 255) { - out[2] = 255; - } else { - out[2] = (UINT8)*in; - } - out[3] = 255; - } -} - -/* ------------- */ -/* F conversions */ -/* ------------- */ - -static void -bit2f(UINT8 *out_, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++, out_ += 4) { - FLOAT32 f = (*in++ != 0) ? 255.0F : 0.0F; - memcpy(out_, &f, sizeof(f)); - } -} - -static void -l2f(UINT8 *out_, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++, out_ += 4) { - FLOAT32 f = (FLOAT32)*in++; - memcpy(out_, &f, sizeof(f)); - } -} - -static void -f2l(UINT8 *out, const UINT8 *in_, int xsize) { - int x; - for (x = 0; x < xsize; x++, out++, in_ += 4) { - FLOAT32 v; - memcpy(&v, in_, sizeof(v)); - if (v <= 0.0) { - *out = 0; - } else if (v >= 255.0) { - *out = 255; - } else { - *out = (UINT8)v; - } - } -} - -static void -f2i(UINT8 *out_, const UINT8 *in_, int xsize) { - int x; - for (x = 0; x < xsize; x++, in_ += 4, out_ += 4) { - FLOAT32 f; - INT32 i; - memcpy(&f, in_, sizeof(f)); - i = f; - memcpy(out_, &i, sizeof(i)); - } -} - -/* ----------------- */ -/* YCbCr conversions */ -/* ----------------- */ - -/* See ConvertYCbCr.c for RGB/YCbCr tables */ - -static void -l2ycbcr(UINT8 *out, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++) { - *out++ = *in++; - *out++ = 128; - *out++ = 128; - *out++ = 255; - } -} - -static void -la2ycbcr(UINT8 *out, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++, in += 4) { - *out++ = in[0]; - *out++ = 128; - *out++ = 128; - *out++ = 255; - } -} - -static void -ycbcr2l(UINT8 *out, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++, in += 4) { - *out++ = in[0]; - } -} - -static void -ycbcr2la(UINT8 *out, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++, in += 4, out += 4) { - out[0] = out[1] = out[2] = in[0]; - out[3] = 255; - } -} - -/* ------------------------- */ -/* I;16 (16-bit) conversions */ -/* ------------------------- */ - -static void -I_I16L(UINT8 *out, const UINT8 *in_, int xsize) { - int x, v; - for (x = 0; x < xsize; x++, in_ += 4) { - INT32 i; - memcpy(&i, in_, sizeof(i)); - v = CLIP16(i); - *out++ = (UINT8)v; - *out++ = (UINT8)(v >> 8); - } -} - -static void -I_I16B(UINT8 *out, const UINT8 *in_, int xsize) { - int x, v; - for (x = 0; x < xsize; x++, in_ += 4) { - INT32 i; - memcpy(&i, in_, sizeof(i)); - v = CLIP16(i); - *out++ = (UINT8)(v >> 8); - *out++ = (UINT8)v; - } -} - -static void -I16L_I(UINT8 *out_, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++, in += 2, out_ += 4) { - INT32 v = in[0] + ((int)in[1] << 8); - memcpy(out_, &v, sizeof(v)); - } -} - -static void -I16B_I(UINT8 *out_, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++, in += 2, out_ += 4) { - INT32 v = in[1] + ((int)in[0] << 8); - memcpy(out_, &v, sizeof(v)); - } -} - -static void -I16L_F(UINT8 *out_, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++, in += 2, out_ += 4) { - FLOAT32 v = in[0] + ((int)in[1] << 8); - memcpy(out_, &v, sizeof(v)); - } -} - -static void -I16B_F(UINT8 *out_, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++, in += 2, out_ += 4) { - FLOAT32 v = in[1] + ((int)in[0] << 8); - memcpy(out_, &v, sizeof(v)); - } -} - -static void -L_I16L(UINT8 *out, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++, in++) { - *out++ = *in; - *out++ = 0; - } -} - -static void -L_I16B(UINT8 *out, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++, in++) { - *out++ = 0; - *out++ = *in; - } -} - -static void -I16L_L(UINT8 *out, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++, in += 2) { - if (in[1] != 0) { - *out++ = 255; - } else { - *out++ = in[0]; - } - } -} - -static void -I16B_L(UINT8 *out, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++, in += 2) { - if (in[0] != 0) { - *out++ = 255; - } else { - *out++ = in[1]; - } - } -} - -static void -I16_RGB(UINT8 *out, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++, in += 2) { - UINT8 v = in[1] == 0 ? in[0] : 255; - *out++ = v; - *out++ = v; - *out++ = v; - *out++ = 255; - } -} - -static struct { - const char *from; - const char *to; - ImagingShuffler convert; -} converters[] = { - - {"1", "L", bit2l}, - {"1", "I", bit2i}, - {"1", "F", bit2f}, - {"1", "RGB", bit2rgb}, - {"1", "RGBA", bit2rgb}, - {"1", "RGBX", bit2rgb}, - {"1", "CMYK", bit2cmyk}, - {"1", "YCbCr", bit2ycbcr}, - {"1", "HSV", bit2hsv}, - - {"L", "1", l2bit}, - {"L", "LA", l2la}, - {"L", "I", l2i}, - {"L", "F", l2f}, - {"L", "RGB", l2rgb}, - {"L", "RGBA", l2rgb}, - {"L", "RGBX", l2rgb}, - {"L", "CMYK", l2cmyk}, - {"L", "YCbCr", l2ycbcr}, - {"L", "HSV", l2hsv}, - - {"LA", "L", la2l}, - {"LA", "La", lA2la}, - {"LA", "RGB", la2rgb}, - {"LA", "RGBA", la2rgb}, - {"LA", "RGBX", la2rgb}, - {"LA", "CMYK", la2cmyk}, - {"LA", "YCbCr", la2ycbcr}, - {"LA", "HSV", la2hsv}, - - {"La", "LA", la2lA}, - - {"I", "L", i2l}, - {"I", "F", i2f}, - {"I", "RGB", i2rgb}, - {"I", "RGBA", i2rgb}, - {"I", "RGBX", i2rgb}, - {"I", "HSV", i2hsv}, - - {"F", "L", f2l}, - {"F", "I", f2i}, - - {"RGB", "1", rgb2bit}, - {"RGB", "L", rgb2l}, - {"RGB", "LA", rgb2la}, - {"RGB", "La", rgb2la}, - {"RGB", "I", rgb2i}, - {"RGB", "I;16", rgb2i16l}, - {"RGB", "I;16L", rgb2i16l}, - {"RGB", "I;16B", rgb2i16b}, -#ifdef WORDS_BIGENDIAN - {"RGB", "I;16N", rgb2i16b}, -#else - {"RGB", "I;16N", rgb2i16l}, -#endif - {"RGB", "F", rgb2f}, - {"RGB", "BGR;15", rgb2bgr15}, - {"RGB", "BGR;16", rgb2bgr16}, - {"RGB", "BGR;24", rgb2bgr24}, - {"RGB", "RGBA", rgb2rgba}, - {"RGB", "RGBa", rgb2rgba}, - {"RGB", "RGBX", rgb2rgba}, - {"RGB", "CMYK", rgb2cmyk}, - {"RGB", "YCbCr", ImagingConvertRGB2YCbCr}, - {"RGB", "HSV", rgb2hsv}, - - {"RGBA", "1", rgb2bit}, - {"RGBA", "L", rgb2l}, - {"RGBA", "LA", rgba2la}, - {"RGBA", "I", rgb2i}, - {"RGBA", "F", rgb2f}, - {"RGBA", "RGB", rgba2rgb}, - {"RGBA", "RGBa", rgbA2rgba}, - {"RGBA", "RGBX", rgb2rgba}, - {"RGBA", "CMYK", rgb2cmyk}, - {"RGBA", "YCbCr", ImagingConvertRGB2YCbCr}, - {"RGBA", "HSV", rgb2hsv}, - - {"RGBa", "RGBA", rgba2rgbA}, - {"RGBa", "RGB", rgba2rgb_}, - - {"RGBX", "1", rgb2bit}, - {"RGBX", "L", rgb2l}, - {"RGBX", "LA", rgb2la}, - {"RGBX", "I", rgb2i}, - {"RGBX", "F", rgb2f}, - {"RGBX", "RGB", rgba2rgb}, - {"RGBX", "CMYK", rgb2cmyk}, - {"RGBX", "YCbCr", ImagingConvertRGB2YCbCr}, - {"RGBX", "HSV", rgb2hsv}, - - {"CMYK", "RGB", cmyk2rgb}, - {"CMYK", "RGBA", cmyk2rgb}, - {"CMYK", "RGBX", cmyk2rgb}, - {"CMYK", "HSV", cmyk2hsv}, - - {"YCbCr", "L", ycbcr2l}, - {"YCbCr", "LA", ycbcr2la}, - {"YCbCr", "RGB", ImagingConvertYCbCr2RGB}, - - {"HSV", "RGB", hsv2rgb}, - - {"I", "I;16", I_I16L}, - {"I;16", "I", I16L_I}, - {"I;16", "RGB", I16_RGB}, - {"L", "I;16", L_I16L}, - {"I;16", "L", I16L_L}, - - {"I", "I;16L", I_I16L}, - {"I;16L", "I", I16L_I}, - {"I", "I;16B", I_I16B}, - {"I;16B", "I", I16B_I}, - - {"L", "I;16L", L_I16L}, - {"I;16L", "L", I16L_L}, - {"L", "I;16B", L_I16B}, - {"I;16B", "L", I16B_L}, -#ifdef WORDS_BIGENDIAN - {"L", "I;16N", L_I16B}, - {"I;16N", "L", I16B_L}, -#else - {"L", "I;16N", L_I16L}, - {"I;16N", "L", I16L_L}, -#endif - - {"I;16", "F", I16L_F}, - {"I;16L", "F", I16L_F}, - {"I;16B", "F", I16B_F}, - - {NULL}}; - -/* FIXME: translate indexed versions to pointer versions below this line */ - -/* ------------------- */ -/* Palette conversions */ -/* ------------------- */ - -static void -p2bit(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { - int x; - /* FIXME: precalculate grayscale palette? */ - for (x = 0; x < xsize; x++) { - *out++ = (L(&palette->palette[in[x] * 4]) >= 128000) ? 255 : 0; - } -} - -static void -pa2bit(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { - int x; - /* FIXME: precalculate grayscale palette? */ - for (x = 0; x < xsize; x++, in += 4) { - *out++ = (L(&palette->palette[in[0] * 4]) >= 128000) ? 255 : 0; - } -} - -static void -p2l(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { - int x; - /* FIXME: precalculate grayscale palette? */ - for (x = 0; x < xsize; x++) { - *out++ = L24(&palette->palette[in[x] * 4]) >> 16; - } -} - -static void -pa2l(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { - int x; - /* FIXME: precalculate grayscale palette? */ - for (x = 0; x < xsize; x++, in += 4) { - *out++ = L24(&palette->palette[in[0] * 4]) >> 16; - } -} - -static void -pa2p(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { - int x; - for (x = 0; x < xsize; x++, in += 4) { - *out++ = in[0]; - } -} - -static void -p2pa(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { - int x; - int rgb = strcmp(palette->mode, "RGB"); - for (x = 0; x < xsize; x++, in++) { - const UINT8 *rgba = &palette->palette[in[0] * 4]; - *out++ = in[0]; - *out++ = in[0]; - *out++ = in[0]; - *out++ = rgb == 0 ? 255 : rgba[3]; - } -} - -static void -p2la(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { - int x; - /* FIXME: precalculate grayscale palette? */ - for (x = 0; x < xsize; x++, out += 4) { - const UINT8 *rgba = &palette->palette[*in++ * 4]; - out[0] = out[1] = out[2] = L24(rgba) >> 16; - out[3] = rgba[3]; - } -} - -static void -pa2la(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { - int x; - /* FIXME: precalculate grayscale palette? */ - for (x = 0; x < xsize; x++, in += 4, out += 4) { - out[0] = out[1] = out[2] = L24(&palette->palette[in[0] * 4]) >> 16; - out[3] = in[3]; - } -} - -static void -p2i(UINT8 *out_, const UINT8 *in, int xsize, ImagingPalette palette) { - int x; - for (x = 0; x < xsize; x++, out_ += 4) { - INT32 v = L24(&palette->palette[in[x] * 4]) >> 16; - memcpy(out_, &v, sizeof(v)); - } -} - -static void -pa2i(UINT8 *out_, const UINT8 *in, int xsize, ImagingPalette palette) { - int x; - INT32 *out = (INT32 *)out_; - for (x = 0; x < xsize; x++, in += 4) { - *out++ = L24(&palette->palette[in[0] * 4]) >> 16; - } -} - -static void -p2f(UINT8 *out_, const UINT8 *in, int xsize, ImagingPalette palette) { - int x; - for (x = 0; x < xsize; x++, out_ += 4) { - FLOAT32 v = L(&palette->palette[in[x] * 4]) / 1000.0F; - memcpy(out_, &v, sizeof(v)); - } -} - -static void -pa2f(UINT8 *out_, const UINT8 *in, int xsize, ImagingPalette palette) { - int x; - FLOAT32 *out = (FLOAT32 *)out_; - for (x = 0; x < xsize; x++, in += 4) { - *out++ = (float)L(&palette->palette[in[0] * 4]) / 1000.0F; - } -} - -static void -p2rgb(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { - int x; - for (x = 0; x < xsize; x++) { - const UINT8 *rgb = &palette->palette[*in++ * 4]; - *out++ = rgb[0]; - *out++ = rgb[1]; - *out++ = rgb[2]; - *out++ = 255; - } -} - -static void -pa2rgb(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { - int x; - for (x = 0; x < xsize; x++, in += 4) { - const UINT8 *rgb = &palette->palette[in[0] * 4]; - *out++ = rgb[0]; - *out++ = rgb[1]; - *out++ = rgb[2]; - *out++ = 255; - } -} - -static void -p2hsv(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { - int x; - for (x = 0; x < xsize; x++, out += 4) { - const UINT8 *rgb = &palette->palette[*in++ * 4]; - rgb2hsv_row(out, rgb); - out[3] = 255; - } -} - -static void -pa2hsv(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { - int x; - for (x = 0; x < xsize; x++, in += 4, out += 4) { - const UINT8 *rgb = &palette->palette[in[0] * 4]; - rgb2hsv_row(out, rgb); - out[3] = 255; - } -} - -static void -p2rgba(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { - int x; - for (x = 0; x < xsize; x++) { - const UINT8 *rgba = &palette->palette[*in++ * 4]; - *out++ = rgba[0]; - *out++ = rgba[1]; - *out++ = rgba[2]; - *out++ = rgba[3]; - } -} - -static void -pa2rgba(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { - int x; - for (x = 0; x < xsize; x++, in += 4) { - const UINT8 *rgb = &palette->palette[in[0] * 4]; - *out++ = rgb[0]; - *out++ = rgb[1]; - *out++ = rgb[2]; - *out++ = in[3]; - } -} - -static void -p2cmyk(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { - p2rgb(out, in, xsize, palette); - rgb2cmyk(out, out, xsize); -} - -static void -pa2cmyk(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { - pa2rgb(out, in, xsize, palette); - rgb2cmyk(out, out, xsize); -} - -static void -p2ycbcr(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { - p2rgb(out, in, xsize, palette); - ImagingConvertRGB2YCbCr(out, out, xsize); -} - -static void -pa2ycbcr(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { - pa2rgb(out, in, xsize, palette); - ImagingConvertRGB2YCbCr(out, out, xsize); -} - -static Imaging -frompalette(Imaging imOut, Imaging imIn, const char *mode) { - ImagingSectionCookie cookie; - int alpha; - int y; - void (*convert)(UINT8 *, const UINT8 *, int, ImagingPalette); - - /* Map palette image to L, RGB, RGBA, or CMYK */ - - if (!imIn->palette) { - return (Imaging)ImagingError_ValueError("no palette"); - } - - alpha = !strcmp(imIn->mode, "PA"); - - if (strcmp(mode, "1") == 0) { - convert = alpha ? pa2bit : p2bit; - } else if (strcmp(mode, "L") == 0) { - convert = alpha ? pa2l : p2l; - } else if (strcmp(mode, "LA") == 0) { - convert = alpha ? pa2la : p2la; - } else if (strcmp(mode, "P") == 0) { - convert = pa2p; - } else if (strcmp(mode, "PA") == 0) { - convert = p2pa; - } else if (strcmp(mode, "I") == 0) { - convert = alpha ? pa2i : p2i; - } else if (strcmp(mode, "F") == 0) { - convert = alpha ? pa2f : p2f; - } else if (strcmp(mode, "RGB") == 0) { - convert = alpha ? pa2rgb : p2rgb; - } else if (strcmp(mode, "RGBA") == 0 || strcmp(mode, "RGBX") == 0) { - convert = alpha ? pa2rgba : p2rgba; - } else if (strcmp(mode, "CMYK") == 0) { - convert = alpha ? pa2cmyk : p2cmyk; - } else if (strcmp(mode, "YCbCr") == 0) { - convert = alpha ? pa2ycbcr : p2ycbcr; - } else if (strcmp(mode, "HSV") == 0) { - convert = alpha ? pa2hsv : p2hsv; - } else { - return (Imaging)ImagingError_ValueError("conversion not supported"); - } - - imOut = ImagingNew2Dirty(mode, imOut, imIn); - if (!imOut) { - return NULL; - } - if (strcmp(mode, "P") == 0 || strcmp(mode, "PA") == 0) { - ImagingPaletteDelete(imOut->palette); - imOut->palette = ImagingPaletteDuplicate(imIn->palette); - } - - ImagingSectionEnter(&cookie); - for (y = 0; y < imIn->ysize; y++) { - (*convert)( - (UINT8 *)imOut->image[y], - (UINT8 *)imIn->image[y], - imIn->xsize, - imIn->palette); - } - ImagingSectionLeave(&cookie); - - return imOut; -} - -#if defined(_MSC_VER) -#pragma optimize("", off) -#endif -static Imaging -topalette( - Imaging imOut, - Imaging imIn, - const char *mode, - ImagingPalette inpalette, - int dither) { - ImagingSectionCookie cookie; - int alpha; - int x, y; - ImagingPalette palette = inpalette; - - /* Map L or RGB/RGBX/RGBA to palette image */ - if (strcmp(imIn->mode, "L") != 0 && strncmp(imIn->mode, "RGB", 3) != 0) { - return (Imaging)ImagingError_ValueError("conversion not supported"); - } - - alpha = !strcmp(mode, "PA"); - - if (palette == NULL) { - /* FIXME: make user configurable */ - if (imIn->bands == 1) { - palette = ImagingPaletteNew("RGB"); - - palette->size = 256; - int i; - for (i = 0; i < 256; i++) { - palette->palette[i * 4] = palette->palette[i * 4 + 1] = - palette->palette[i * 4 + 2] = (UINT8)i; - } - } else { - palette = ImagingPaletteNewBrowser(); /* Standard colour cube */ - } - } - - if (!palette) { - return (Imaging)ImagingError_ValueError("no palette"); - } - - imOut = ImagingNew2Dirty(mode, imOut, imIn); - if (!imOut) { - if (palette != inpalette) { - ImagingPaletteDelete(palette); - } - return NULL; - } - - ImagingPaletteDelete(imOut->palette); - imOut->palette = ImagingPaletteDuplicate(palette); - - if (imIn->bands == 1) { - /* grayscale image */ - - /* Grayscale palette: copy data as is */ - ImagingSectionEnter(&cookie); - for (y = 0; y < imIn->ysize; y++) { - if (alpha) { - l2la((UINT8 *)imOut->image[y], (UINT8 *)imIn->image[y], imIn->xsize); - } else { - memcpy(imOut->image[y], imIn->image[y], imIn->linesize); - } - } - ImagingSectionLeave(&cookie); - - } else { - /* colour image */ - - /* Create mapping cache */ - if (ImagingPaletteCachePrepare(palette) < 0) { - ImagingDelete(imOut); - if (palette != inpalette) { - ImagingPaletteDelete(palette); - } - return NULL; - } - - if (dither) { - /* floyd-steinberg dither */ - - int *errors; - errors = calloc(imIn->xsize + 1, sizeof(int) * 3); - if (!errors) { - ImagingDelete(imOut); - return ImagingError_MemoryError(); - } - - /* Map each pixel to the nearest palette entry */ - ImagingSectionEnter(&cookie); - for (y = 0; y < imIn->ysize; y++) { - int r, r0, r1, r2; - int g, g0, g1, g2; - int b, b0, b1, b2; - UINT8 *in = (UINT8 *)imIn->image[y]; - UINT8 *out = alpha ? (UINT8 *)imOut->image32[y] : imOut->image8[y]; - int *e = errors; - - r = r0 = r1 = 0; - g = g0 = g1 = 0; - b = b0 = b1 = b2 = 0; - - for (x = 0; x < imIn->xsize; x++, in += 4) { - int d2; - INT16 *cache; - - r = CLIP8(in[0] + (r + e[3 + 0]) / 16); - g = CLIP8(in[1] + (g + e[3 + 1]) / 16); - b = CLIP8(in[2] + (b + e[3 + 2]) / 16); - - /* get closest colour */ - cache = &ImagingPaletteCache(palette, r, g, b); - if (cache[0] == 0x100) { - ImagingPaletteCacheUpdate(palette, r, g, b); - } - if (alpha) { - out[x * 4] = out[x * 4 + 1] = out[x * 4 + 2] = (UINT8)cache[0]; - out[x * 4 + 3] = 255; - } else { - out[x] = (UINT8)cache[0]; - } - - r -= (int)palette->palette[cache[0] * 4]; - g -= (int)palette->palette[cache[0] * 4 + 1]; - b -= (int)palette->palette[cache[0] * 4 + 2]; - - /* propagate errors (don't ask ;-) */ - r2 = r; - d2 = r + r; - r += d2; - e[0] = r + r0; - r += d2; - r0 = r + r1; - r1 = r2; - r += d2; - g2 = g; - d2 = g + g; - g += d2; - e[1] = g + g0; - g += d2; - g0 = g + g1; - g1 = g2; - g += d2; - b2 = b; - d2 = b + b; - b += d2; - e[2] = b + b0; - b += d2; - b0 = b + b1; - b1 = b2; - b += d2; - - e += 3; - } - - e[0] = b0; - e[1] = b1; - e[2] = b2; - } - ImagingSectionLeave(&cookie); - free(errors); - - } else { - /* closest colour */ - ImagingSectionEnter(&cookie); - for (y = 0; y < imIn->ysize; y++) { - int r, g, b; - UINT8 *in = (UINT8 *)imIn->image[y]; - UINT8 *out = alpha ? (UINT8 *)imOut->image32[y] : imOut->image8[y]; - - for (x = 0; x < imIn->xsize; x++, in += 4) { - INT16 *cache; - - r = in[0]; - g = in[1]; - b = in[2]; - - /* get closest colour */ - cache = &ImagingPaletteCache(palette, r, g, b); - if (cache[0] == 0x100) { - ImagingPaletteCacheUpdate(palette, r, g, b); - } - if (alpha) { - out[x * 4] = out[x * 4 + 1] = out[x * 4 + 2] = (UINT8)cache[0]; - out[x * 4 + 3] = 255; - } else { - out[x] = (UINT8)cache[0]; - } - } - } - ImagingSectionLeave(&cookie); - } - if (inpalette != palette) { - ImagingPaletteCacheDelete(palette); - } - } - - if (inpalette != palette) { - ImagingPaletteDelete(palette); - } - - return imOut; -} - -static Imaging -tobilevel(Imaging imOut, Imaging imIn) { - ImagingSectionCookie cookie; - int x, y; - int *errors; - - /* Map L or RGB to dithered 1 image */ - if (strcmp(imIn->mode, "L") != 0 && strcmp(imIn->mode, "RGB") != 0) { - return (Imaging)ImagingError_ValueError("conversion not supported"); - } - - imOut = ImagingNew2Dirty("1", imOut, imIn); - if (!imOut) { - return NULL; - } - - errors = calloc(imIn->xsize + 1, sizeof(int)); - if (!errors) { - ImagingDelete(imOut); - return ImagingError_MemoryError(); - } - - if (imIn->bands == 1) { - /* map each pixel to black or white, using error diffusion */ - ImagingSectionEnter(&cookie); - for (y = 0; y < imIn->ysize; y++) { - int l, l0, l1, l2, d2; - UINT8 *in = (UINT8 *)imIn->image[y]; - UINT8 *out = imOut->image8[y]; - - l = l0 = l1 = 0; - - for (x = 0; x < imIn->xsize; x++) { - /* pick closest colour */ - l = CLIP8(in[x] + (l + errors[x + 1]) / 16); - out[x] = (l > 128) ? 255 : 0; - - /* propagate errors */ - l -= (int)out[x]; - l2 = l; - d2 = l + l; - l += d2; - errors[x] = l + l0; - l += d2; - l0 = l + l1; - l1 = l2; - l += d2; - } - - errors[x] = l0; - } - ImagingSectionLeave(&cookie); - - } else { - /* map each pixel to black or white, using error diffusion */ - ImagingSectionEnter(&cookie); - for (y = 0; y < imIn->ysize; y++) { - int l, l0, l1, l2, d2; - UINT8 *in = (UINT8 *)imIn->image[y]; - UINT8 *out = imOut->image8[y]; - - l = l0 = l1 = 0; - - for (x = 0; x < imIn->xsize; x++, in += 4) { - /* pick closest colour */ - l = CLIP8(L(in) / 1000 + (l + errors[x + 1]) / 16); - out[x] = (l > 128) ? 255 : 0; - - /* propagate errors */ - l -= (int)out[x]; - l2 = l; - d2 = l + l; - l += d2; - errors[x] = l + l0; - l += d2; - l0 = l + l1; - l1 = l2; - l += d2; - } - - errors[x] = l0; - } - ImagingSectionLeave(&cookie); - } - - free(errors); - - return imOut; -} -#if defined(_MSC_VER) -#pragma optimize("", on) -#endif - -static Imaging -convert( - Imaging imOut, Imaging imIn, const char *mode, ImagingPalette palette, int dither) { - ImagingSectionCookie cookie; - ImagingShuffler convert; - int y; - - if (!imIn) { - return (Imaging)ImagingError_ModeError(); - } - - if (!mode) { - /* Map palette image to full depth */ - if (!imIn->palette) { - return (Imaging)ImagingError_ModeError(); - } - mode = imIn->palette->mode; - } else { - /* Same mode? */ - if (!strcmp(imIn->mode, mode)) { - return ImagingCopy2(imOut, imIn); - } - } - - /* test for special conversions */ - - if (strcmp(imIn->mode, "P") == 0 || strcmp(imIn->mode, "PA") == 0) { - return frompalette(imOut, imIn, mode); - } - - if (strcmp(mode, "P") == 0 || strcmp(mode, "PA") == 0) { - return topalette(imOut, imIn, mode, palette, dither); - } - - if (dither && strcmp(mode, "1") == 0) { - return tobilevel(imOut, imIn); - } - - /* standard conversion machinery */ - - convert = NULL; - - for (y = 0; converters[y].from; y++) { - if (!strcmp(imIn->mode, converters[y].from) && - !strcmp(mode, converters[y].to)) { - convert = converters[y].convert; - break; - } - } - - if (!convert) { -#ifdef notdef - return (Imaging)ImagingError_ValueError("conversion not supported"); -#else - static char buf[100]; - snprintf( - buf, 100, "conversion from %.10s to %.10s not supported", imIn->mode, mode); - return (Imaging)ImagingError_ValueError(buf); -#endif - } - - imOut = ImagingNew2Dirty(mode, imOut, imIn); - if (!imOut) { - return NULL; - } - - ImagingSectionEnter(&cookie); - for (y = 0; y < imIn->ysize; y++) { - (*convert)((UINT8 *)imOut->image[y], (UINT8 *)imIn->image[y], imIn->xsize); - } - ImagingSectionLeave(&cookie); - - return imOut; -} - -Imaging -ImagingConvert(Imaging imIn, const char *mode, ImagingPalette palette, int dither) { - return convert(NULL, imIn, mode, palette, dither); -} - -Imaging -ImagingConvert2(Imaging imOut, Imaging imIn) { - return convert(imOut, imIn, imOut->mode, NULL, 0); -} - -Imaging -ImagingConvertTransparent(Imaging imIn, const char *mode, int r, int g, int b) { - ImagingSectionCookie cookie; - ImagingShuffler convert; - Imaging imOut = NULL; - int premultiplied = 0; - // If the transparency matches pixels in the source image, not the converted image - UINT8 *source; - int source_transparency = 0; - int y; - - if (!imIn) { - return (Imaging)ImagingError_ModeError(); - } - - if (strcmp(imIn->mode, "RGB") == 0 && - (strcmp(mode, "RGBA") == 0 || strcmp(mode, "RGBa") == 0)) { - convert = rgb2rgba; - if (strcmp(mode, "RGBa") == 0) { - premultiplied = 1; - } - } else if ( - strcmp(imIn->mode, "RGB") == 0 && - (strcmp(mode, "LA") == 0 || strcmp(mode, "La") == 0)) { - convert = rgb2la; - source_transparency = 1; - if (strcmp(mode, "La") == 0) { - premultiplied = 1; - } - } else if ( - (strcmp(imIn->mode, "1") == 0 || strcmp(imIn->mode, "I") == 0 || - strcmp(imIn->mode, "I;16") == 0 || strcmp(imIn->mode, "L") == 0) && - (strcmp(mode, "RGBA") == 0 || strcmp(mode, "LA") == 0)) { - if (strcmp(imIn->mode, "1") == 0) { - convert = bit2rgb; - } else if (strcmp(imIn->mode, "I") == 0) { - convert = i2rgb; - } else if (strcmp(imIn->mode, "I;16") == 0) { - convert = I16_RGB; - } else { - convert = l2rgb; - } - g = b = r; - } else { - static char buf[100]; - snprintf( - buf, - 100, - "conversion from %.10s to %.10s not supported in convert_transparent", - imIn->mode, - mode); - return (Imaging)ImagingError_ValueError(buf); - } - - imOut = ImagingNew2Dirty(mode, imOut, imIn); - if (!imOut) { - return NULL; - } - - ImagingSectionEnter(&cookie); - for (y = 0; y < imIn->ysize; y++) { - (*convert)((UINT8 *)imOut->image[y], (UINT8 *)imIn->image[y], imIn->xsize); - - source = source_transparency ? (UINT8 *)imIn->image[y] : NULL; - rgbT2a((UINT8 *)imOut->image[y], source, imIn->xsize, r, g, b, premultiplied); - } - ImagingSectionLeave(&cookie); - - return imOut; -} - -Imaging -ImagingConvertInPlace(Imaging imIn, const char *mode) { - ImagingSectionCookie cookie; - ImagingShuffler convert; - int y; - - /* limited support for inplace conversion */ - if (strcmp(imIn->mode, "L") == 0 && strcmp(mode, "1") == 0) { - convert = l2bit; - } else if (strcmp(imIn->mode, "1") == 0 && strcmp(mode, "L") == 0) { - convert = bit2l; - } else { - return ImagingError_ModeError(); - } - - ImagingSectionEnter(&cookie); - for (y = 0; y < imIn->ysize; y++) { - (*convert)((UINT8 *)imIn->image[y], (UINT8 *)imIn->image[y], imIn->xsize); - } - ImagingSectionLeave(&cookie); - - return imIn; -} +/* + * The Python Imaging Library + * $Id$ + * + * convert images + * + * history: + * 1995-06-15 fl created + * 1995-11-28 fl added some "RGBA" and "CMYK" conversions + * 1996-04-22 fl added "1" conversions (same as "L") + * 1996-05-05 fl added palette conversions (hack) + * 1996-07-23 fl fixed "1" conversions to zero/non-zero convention + * 1996-11-01 fl fixed "P" to "L" and "RGB" to "1" conversions + * 1996-12-29 fl set alpha byte in RGB converters + * 1997-05-12 fl added ImagingConvert2 + * 1997-05-30 fl added floating point support + * 1997-08-27 fl added "P" to "1" and "P" to "F" conversions + * 1998-01-11 fl added integer support + * 1998-07-01 fl added "YCbCr" support + * 1998-07-02 fl added "RGBX" conversions (sort of) + * 1998-07-04 fl added floyd-steinberg dithering + * 1998-07-12 fl changed "YCrCb" to "YCbCr" (!) + * 1998-12-29 fl added basic "I;16" and "I;16B" conversions + * 1999-02-03 fl added "RGBa", and "BGR" conversions (experimental) + * 2003-09-26 fl added "LA" and "PA" conversions (experimental) + * 2005-05-05 fl fixed "P" to "1" threshold + * 2005-12-08 fl fixed palette memory leak in topalette + * + * Copyright (c) 1997-2005 by Secret Labs AB. + * Copyright (c) 1995-1997 by Fredrik Lundh. + * + * See the README file for details on usage and redistribution. + */ + +#include "Imaging.h" + +#define MAX(a, b) (a) > (b) ? (a) : (b) +#define MIN(a, b) (a) < (b) ? (a) : (b) + +#define CLIP16(v) ((v) <= 0 ? 0 : (v) >= 65535 ? 65535 : (v)) + +/* ITU-R Recommendation 601-2 (assuming nonlinear RGB) */ +#define L(rgb) ((INT32)(rgb)[0] * 299 + (INT32)(rgb)[1] * 587 + (INT32)(rgb)[2] * 114) +#define L24(rgb) ((rgb)[0] * 19595 + (rgb)[1] * 38470 + (rgb)[2] * 7471 + 0x8000) + +/* ------------------- */ +/* 1 (bit) conversions */ +/* ------------------- */ + +static void +bit2l(UINT8 *out, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++) *out++ = (*in++ != 0) ? 255 : 0; +} + +static void +bit2rgb(UINT8 *out, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++) { + UINT8 v = (*in++ != 0) ? 255 : 0; + *out++ = v; + *out++ = v; + *out++ = v; + *out++ = 255; + } +} + +static void +bit2cmyk(UINT8 *out, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++) { + *out++ = 0; + *out++ = 0; + *out++ = 0; + *out++ = (*in++ != 0) ? 0 : 255; + } +} + +static void +bit2ycbcr(UINT8 *out, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++) { + *out++ = (*in++ != 0) ? 255 : 0; + *out++ = 128; + *out++ = 128; + *out++ = 255; + } +} + +static void +bit2hsv(UINT8 *out, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++, out += 4) { + UINT8 v = (*in++ != 0) ? 255 : 0; + out[0] = 0; + out[1] = 0; + out[2] = v; + out[3] = 255; + } +} + +/* ----------------- */ +/* RGB/L conversions */ +/* ----------------- */ + +static void +l2bit(UINT8 *out, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++) { + *out++ = (*in++ >= 128) ? 255 : 0; + } +} + +static void +lA2la(UINT8 *out, const UINT8 *in, int xsize) { + int x; + unsigned int alpha, pixel, tmp; + for (x = 0; x < xsize; x++, in += 4) { + alpha = in[3]; + pixel = MULDIV255(in[0], alpha, tmp); + *out++ = (UINT8)pixel; + *out++ = (UINT8)pixel; + *out++ = (UINT8)pixel; + *out++ = (UINT8)alpha; + } +} + +/* RGBa -> RGBA conversion to remove premultiplication + Needed for correct transforms/resizing on RGBA images */ +static void +la2lA(UINT8 *out, const UINT8 *in, int xsize) { + int x; + unsigned int alpha, pixel; + for (x = 0; x < xsize; x++, in += 4) { + alpha = in[3]; + if (alpha == 255 || alpha == 0) { + pixel = in[0]; + } else { + pixel = CLIP8((255 * in[0]) / alpha); + } + *out++ = (UINT8)pixel; + *out++ = (UINT8)pixel; + *out++ = (UINT8)pixel; + *out++ = (UINT8)alpha; + } +} + +static void +l2la(UINT8 *out, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++) { + UINT8 v = *in++; + *out++ = v; + *out++ = v; + *out++ = v; + *out++ = 255; + } +} + +static void +l2rgb(UINT8 *out, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++) { + UINT8 v = *in++; + *out++ = v; + *out++ = v; + *out++ = v; + *out++ = 255; + } +} + +static void +l2hsv(UINT8 *out, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++, out += 4) { + UINT8 v = *in++; + out[0] = 0; + out[1] = 0; + out[2] = v; + out[3] = 255; + } +} + +static void +la2l(UINT8 *out, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++, in += 4) { + *out++ = in[0]; + } +} + +static void +la2rgb(UINT8 *out, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++, in += 4) { + UINT8 v = in[0]; + *out++ = v; + *out++ = v; + *out++ = v; + *out++ = in[3]; + } +} + +static void +la2hsv(UINT8 *out, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++, in += 4, out += 4) { + UINT8 v = in[0]; + out[0] = 0; + out[1] = 0; + out[2] = v; + out[3] = in[3]; + } +} + +static void +rgb2bit(UINT8 *out, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++, in += 4) { + /* ITU-R Recommendation 601-2 (assuming nonlinear RGB) */ + *out++ = (L(in) >= 128000) ? 255 : 0; + } +} + +static void +rgb2l(UINT8 *out, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++, in += 4) { + /* ITU-R Recommendation 601-2 (assuming nonlinear RGB) */ + *out++ = L24(in) >> 16; + } +} + +static void +rgb2la(UINT8 *out, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++, in += 4, out += 4) { + /* ITU-R Recommendation 601-2 (assuming nonlinear RGB) */ + out[0] = out[1] = out[2] = L24(in) >> 16; + out[3] = 255; + } +} + +static void +rgb2i(UINT8 *out_, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++, in += 4, out_ += 4) { + INT32 v = L24(in) >> 16; + memcpy(out_, &v, sizeof(v)); + } +} + +static void +rgb2i16l(UINT8 *out_, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++, in += 4) { + *out_++ = L24(in) >> 16; + *out_++ = 0; + } +} + +static void +rgb2i16b(UINT8 *out_, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++, in += 4) { + *out_++ = 0; + *out_++ = L24(in) >> 16; + } +} + +static void +rgb2f(UINT8 *out_, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++, in += 4, out_ += 4) { + FLOAT32 v = (float)L(in) / 1000.0F; + memcpy(out_, &v, sizeof(v)); + } +} + +static void +rgb2bgr15(UINT8 *out_, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++, in += 4, out_ += 2) { + UINT16 v = ((((UINT16)in[0]) << 7) & 0x7c00) + + ((((UINT16)in[1]) << 2) & 0x03e0) + + ((((UINT16)in[2]) >> 3) & 0x001f); + memcpy(out_, &v, sizeof(v)); + } +} + +static void +rgb2bgr16(UINT8 *out_, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++, in += 4, out_ += 2) { + UINT16 v = ((((UINT16)in[0]) << 8) & 0xf800) + + ((((UINT16)in[1]) << 3) & 0x07e0) + + ((((UINT16)in[2]) >> 3) & 0x001f); + memcpy(out_, &v, sizeof(v)); + } +} + +static void +rgb2bgr24(UINT8 *out, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++, in += 4) { + *out++ = in[2]; + *out++ = in[1]; + *out++ = in[0]; + } +} + +static void +rgb2hsv_row(UINT8 *out, const UINT8 *in) { // following colorsys.py + float h, s, rc, gc, bc, cr; + UINT8 maxc, minc; + UINT8 r, g, b; + UINT8 uh, us, uv; + + r = in[0]; + g = in[1]; + b = in[2]; + maxc = MAX(r, MAX(g, b)); + minc = MIN(r, MIN(g, b)); + uv = maxc; + if (minc == maxc) { + uh = 0; + us = 0; + } else { + cr = (float)(maxc - minc); + s = cr / (float)maxc; + rc = ((float)(maxc - r)) / cr; + gc = ((float)(maxc - g)) / cr; + bc = ((float)(maxc - b)) / cr; + if (r == maxc) { + h = bc - gc; + } else if (g == maxc) { + h = 2.0 + rc - bc; + } else { + h = 4.0 + gc - rc; + } + // incorrect hue happens if h/6 is negative. + h = fmod((h / 6.0 + 1.0), 1.0); + + uh = (UINT8)CLIP8((int)(h * 255.0)); + us = (UINT8)CLIP8((int)(s * 255.0)); + } + out[0] = uh; + out[1] = us; + out[2] = uv; +} + +static void +rgb2hsv(UINT8 *out, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++, in += 4, out += 4) { + rgb2hsv_row(out, in); + out[3] = in[3]; + } +} + +static void +hsv2rgb(UINT8 *out, const UINT8 *in, int xsize) { // following colorsys.py + + int p, q, t; + UINT8 up, uq, ut; + int i, x; + float f, fs; + UINT8 h, s, v; + + for (x = 0; x < xsize; x++, in += 4) { + h = in[0]; + s = in[1]; + v = in[2]; + + if (s == 0) { + *out++ = v; + *out++ = v; + *out++ = v; + } else { + i = floor((float)h * 6.0 / 255.0); // 0 - 6 + f = (float)h * 6.0 / 255.0 - (float)i; // 0-1 : remainder. + fs = ((float)s) / 255.0; + + p = round((float)v * (1.0 - fs)); + q = round((float)v * (1.0 - fs * f)); + t = round((float)v * (1.0 - fs * (1.0 - f))); + up = (UINT8)CLIP8(p); + uq = (UINT8)CLIP8(q); + ut = (UINT8)CLIP8(t); + + switch (i % 6) { + case 0: + *out++ = v; + *out++ = ut; + *out++ = up; + break; + case 1: + *out++ = uq; + *out++ = v; + *out++ = up; + break; + case 2: + *out++ = up; + *out++ = v; + *out++ = ut; + break; + case 3: + *out++ = up; + *out++ = uq; + *out++ = v; + break; + case 4: + *out++ = ut; + *out++ = up; + *out++ = v; + break; + case 5: + *out++ = v; + *out++ = up; + *out++ = uq; + break; + } + } + *out++ = in[3]; + } +} + +/* ---------------- */ +/* RGBA conversions */ +/* ---------------- */ + +static void +rgb2rgba(UINT8 *out, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++) { + *out++ = *in++; + *out++ = *in++; + *out++ = *in++; + *out++ = 255; + in++; + } +} + +static void +rgba2la(UINT8 *out, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++, in += 4, out += 4) { + /* ITU-R Recommendation 601-2 (assuming nonlinear RGB) */ + out[0] = out[1] = out[2] = L24(in) >> 16; + out[3] = in[3]; + } +} + +static void +rgba2rgb(UINT8 *out, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++) { + *out++ = *in++; + *out++ = *in++; + *out++ = *in++; + *out++ = 255; + in++; + } +} + +static void +rgbA2rgba(UINT8 *out, const UINT8 *in, int xsize) { + int x; + unsigned int alpha, tmp; + for (x = 0; x < xsize; x++) { + alpha = in[3]; + *out++ = MULDIV255(*in++, alpha, tmp); + *out++ = MULDIV255(*in++, alpha, tmp); + *out++ = MULDIV255(*in++, alpha, tmp); + *out++ = *in++; + } +} + +/* RGBa -> RGBA conversion to remove premultiplication + Needed for correct transforms/resizing on RGBA images */ +static void +rgba2rgbA(UINT8 *out, const UINT8 *in, int xsize) { + int x; + unsigned int alpha; + for (x = 0; x < xsize; x++, in += 4) { + alpha = in[3]; + if (alpha == 255 || alpha == 0) { + *out++ = in[0]; + *out++ = in[1]; + *out++ = in[2]; + } else { + *out++ = CLIP8((255 * in[0]) / alpha); + *out++ = CLIP8((255 * in[1]) / alpha); + *out++ = CLIP8((255 * in[2]) / alpha); + } + *out++ = in[3]; + } +} + +static void +rgba2rgb_(UINT8 *out, const UINT8 *in, int xsize) { + int x; + unsigned int alpha; + for (x = 0; x < xsize; x++, in += 4) { + alpha = in[3]; + if (alpha == 255 || alpha == 0) { + *out++ = in[0]; + *out++ = in[1]; + *out++ = in[2]; + } else { + *out++ = CLIP8((255 * in[0]) / alpha); + *out++ = CLIP8((255 * in[1]) / alpha); + *out++ = CLIP8((255 * in[2]) / alpha); + } + *out++ = 255; + } +} + +/* + * Conversion of RGB + single transparent color either to + * RGBA or LA, where any pixel matching the color will have the alpha channel set to 0, + * or RGBa or La, where any pixel matching the color will have all channels set to 0 + */ + +static void +rgbT2a(UINT8 *out, UINT8 *in, int xsize, int r, int g, int b, int premultiplied) { +#ifdef WORDS_BIGENDIAN + UINT32 trns = ((r & 0xff) << 24) | ((g & 0xff) << 16) | ((b & 0xff) << 8) | 0xff; + UINT32 repl = premultiplied ? 0 : (trns & 0xffffff00); +#else + UINT32 trns = (0xffU << 24) | ((b & 0xff) << 16) | ((g & 0xff) << 8) | (r & 0xff); + UINT32 repl = premultiplied ? 0 : (trns & 0x00ffffff); +#endif + + int i; + + UINT8 *ref = in != NULL ? in : out; + for (i = 0; i < xsize; i++, ref += sizeof(trns), out += sizeof(trns)) { + UINT32 v; + memcpy(&v, ref, sizeof(v)); + if (v == trns) { + memcpy(out, &repl, sizeof(repl)); + } + } +} + +/* ---------------- */ +/* CMYK conversions */ +/* ---------------- */ + +static void +l2cmyk(UINT8 *out, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++) { + *out++ = 0; + *out++ = 0; + *out++ = 0; + *out++ = ~(*in++); + } +} + +static void +la2cmyk(UINT8 *out, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++, in += 4) { + *out++ = 0; + *out++ = 0; + *out++ = 0; + *out++ = ~(in[0]); + } +} + +static void +rgb2cmyk(UINT8 *out, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++) { + /* Note: no undercolour removal */ + *out++ = ~(*in++); + *out++ = ~(*in++); + *out++ = ~(*in++); + *out++ = 0; + in++; + } +} + +void +cmyk2rgb(UINT8 *out, const UINT8 *in, int xsize) { + int x, nk, tmp; + for (x = 0; x < xsize; x++) { + nk = 255 - in[3]; + out[0] = CLIP8(nk - MULDIV255(in[0], nk, tmp)); + out[1] = CLIP8(nk - MULDIV255(in[1], nk, tmp)); + out[2] = CLIP8(nk - MULDIV255(in[2], nk, tmp)); + out[3] = 255; + out += 4; + in += 4; + } +} + +static void +cmyk2hsv(UINT8 *out, const UINT8 *in, int xsize) { + int x, nk, tmp; + for (x = 0; x < xsize; x++) { + nk = 255 - in[3]; + out[0] = CLIP8(nk - MULDIV255(in[0], nk, tmp)); + out[1] = CLIP8(nk - MULDIV255(in[1], nk, tmp)); + out[2] = CLIP8(nk - MULDIV255(in[2], nk, tmp)); + rgb2hsv_row(out, out); + out[3] = 255; + out += 4; + in += 4; + } +} + +/* ------------- */ +/* I conversions */ +/* ------------- */ + +static void +bit2i(UINT8 *out_, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++, out_ += 4) { + INT32 v = (*in++ != 0) ? 255 : 0; + memcpy(out_, &v, sizeof(v)); + } +} + +static void +l2i(UINT8 *out_, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++, out_ += 4) { + INT32 v = *in++; + memcpy(out_, &v, sizeof(v)); + } +} + +static void +i2l(UINT8 *out, const UINT8 *in_, int xsize) { + int x; + for (x = 0; x < xsize; x++, out++, in_ += 4) { + INT32 v; + memcpy(&v, in_, sizeof(v)); + if (v <= 0) { + *out = 0; + } else if (v >= 255) { + *out = 255; + } else { + *out = (UINT8)v; + } + } +} + +static void +i2f(UINT8 *out_, const UINT8 *in_, int xsize) { + int x; + for (x = 0; x < xsize; x++, in_ += 4, out_ += 4) { + INT32 i; + FLOAT32 f; + memcpy(&i, in_, sizeof(i)); + f = i; + memcpy(out_, &f, sizeof(f)); + } +} + +static void +i2rgb(UINT8 *out, const UINT8 *in_, int xsize) { + int x; + INT32 *in = (INT32 *)in_; + for (x = 0; x < xsize; x++, in++, out += 4) { + if (*in <= 0) { + out[0] = out[1] = out[2] = 0; + } else if (*in >= 255) { + out[0] = out[1] = out[2] = 255; + } else { + out[0] = out[1] = out[2] = (UINT8)*in; + } + out[3] = 255; + } +} + +static void +i2hsv(UINT8 *out, const UINT8 *in_, int xsize) { + int x; + INT32 *in = (INT32 *)in_; + for (x = 0; x < xsize; x++, in++, out += 4) { + out[0] = 0; + out[1] = 0; + if (*in <= 0) { + out[2] = 0; + } else if (*in >= 255) { + out[2] = 255; + } else { + out[2] = (UINT8)*in; + } + out[3] = 255; + } +} + +/* ------------- */ +/* F conversions */ +/* ------------- */ + +static void +bit2f(UINT8 *out_, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++, out_ += 4) { + FLOAT32 f = (*in++ != 0) ? 255.0F : 0.0F; + memcpy(out_, &f, sizeof(f)); + } +} + +static void +l2f(UINT8 *out_, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++, out_ += 4) { + FLOAT32 f = (FLOAT32)*in++; + memcpy(out_, &f, sizeof(f)); + } +} + +static void +f2l(UINT8 *out, const UINT8 *in_, int xsize) { + int x; + for (x = 0; x < xsize; x++, out++, in_ += 4) { + FLOAT32 v; + memcpy(&v, in_, sizeof(v)); + if (v <= 0.0) { + *out = 0; + } else if (v >= 255.0) { + *out = 255; + } else { + *out = (UINT8)v; + } + } +} + +static void +f2i(UINT8 *out_, const UINT8 *in_, int xsize) { + int x; + for (x = 0; x < xsize; x++, in_ += 4, out_ += 4) { + FLOAT32 f; + INT32 i; + memcpy(&f, in_, sizeof(f)); + i = f; + memcpy(out_, &i, sizeof(i)); + } +} + +/* ----------------- */ +/* YCbCr conversions */ +/* ----------------- */ + +/* See ConvertYCbCr.c for RGB/YCbCr tables */ + +static void +l2ycbcr(UINT8 *out, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++) { + *out++ = *in++; + *out++ = 128; + *out++ = 128; + *out++ = 255; + } +} + +static void +la2ycbcr(UINT8 *out, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++, in += 4) { + *out++ = in[0]; + *out++ = 128; + *out++ = 128; + *out++ = 255; + } +} + +static void +ycbcr2l(UINT8 *out, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++, in += 4) { + *out++ = in[0]; + } +} + +static void +ycbcr2la(UINT8 *out, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++, in += 4, out += 4) { + out[0] = out[1] = out[2] = in[0]; + out[3] = 255; + } +} + +/* ------------------------- */ +/* I;16 (16-bit) conversions */ +/* ------------------------- */ + +static void +I_I16L(UINT8 *out, const UINT8 *in_, int xsize) { + int x, v; + for (x = 0; x < xsize; x++, in_ += 4) { + INT32 i; + memcpy(&i, in_, sizeof(i)); + v = CLIP16(i); + *out++ = (UINT8)v; + *out++ = (UINT8)(v >> 8); + } +} + +static void +I_I16B(UINT8 *out, const UINT8 *in_, int xsize) { + int x, v; + for (x = 0; x < xsize; x++, in_ += 4) { + INT32 i; + memcpy(&i, in_, sizeof(i)); + v = CLIP16(i); + *out++ = (UINT8)(v >> 8); + *out++ = (UINT8)v; + } +} + +static void +I16L_I(UINT8 *out_, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++, in += 2, out_ += 4) { + INT32 v = in[0] + ((int)in[1] << 8); + memcpy(out_, &v, sizeof(v)); + } +} + +static void +I16B_I(UINT8 *out_, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++, in += 2, out_ += 4) { + INT32 v = in[1] + ((int)in[0] << 8); + memcpy(out_, &v, sizeof(v)); + } +} + +static void +I16L_F(UINT8 *out_, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++, in += 2, out_ += 4) { + FLOAT32 v = in[0] + ((int)in[1] << 8); + memcpy(out_, &v, sizeof(v)); + } +} + +static void +I16B_F(UINT8 *out_, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++, in += 2, out_ += 4) { + FLOAT32 v = in[1] + ((int)in[0] << 8); + memcpy(out_, &v, sizeof(v)); + } +} + +static void +L_I16L(UINT8 *out, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++, in++) { + *out++ = *in; + *out++ = 0; + } +} + +static void +L_I16B(UINT8 *out, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++, in++) { + *out++ = 0; + *out++ = *in; + } +} + +static void +I16L_L(UINT8 *out, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++, in += 2) { + if (in[1] != 0) { + *out++ = 255; + } else { + *out++ = in[0]; + } + } +} + +static void +I16B_L(UINT8 *out, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++, in += 2) { + if (in[0] != 0) { + *out++ = 255; + } else { + *out++ = in[1]; + } + } +} + +static void +I16_RGB(UINT8 *out, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++, in += 2) { + UINT8 v = in[1] == 0 ? in[0] : 255; + *out++ = v; + *out++ = v; + *out++ = v; + *out++ = 255; + } +} + +static struct { + const char *from; + const char *to; + ImagingShuffler convert; +} converters[] = { + + {"1", "L", bit2l}, + {"1", "I", bit2i}, + {"1", "F", bit2f}, + {"1", "RGB", bit2rgb}, + {"1", "RGBA", bit2rgb}, + {"1", "RGBX", bit2rgb}, + {"1", "CMYK", bit2cmyk}, + {"1", "YCbCr", bit2ycbcr}, + {"1", "HSV", bit2hsv}, + + {"L", "1", l2bit}, + {"L", "LA", l2la}, + {"L", "I", l2i}, + {"L", "F", l2f}, + {"L", "RGB", l2rgb}, + {"L", "RGBA", l2rgb}, + {"L", "RGBX", l2rgb}, + {"L", "CMYK", l2cmyk}, + {"L", "YCbCr", l2ycbcr}, + {"L", "HSV", l2hsv}, + + {"LA", "L", la2l}, + {"LA", "La", lA2la}, + {"LA", "RGB", la2rgb}, + {"LA", "RGBA", la2rgb}, + {"LA", "RGBX", la2rgb}, + {"LA", "CMYK", la2cmyk}, + {"LA", "YCbCr", la2ycbcr}, + {"LA", "HSV", la2hsv}, + + {"La", "LA", la2lA}, + + {"I", "L", i2l}, + {"I", "F", i2f}, + {"I", "RGB", i2rgb}, + {"I", "RGBA", i2rgb}, + {"I", "RGBX", i2rgb}, + {"I", "HSV", i2hsv}, + + {"F", "L", f2l}, + {"F", "I", f2i}, + + {"RGB", "1", rgb2bit}, + {"RGB", "L", rgb2l}, + {"RGB", "LA", rgb2la}, + {"RGB", "La", rgb2la}, + {"RGB", "I", rgb2i}, + {"RGB", "I;16", rgb2i16l}, + {"RGB", "I;16L", rgb2i16l}, + {"RGB", "I;16B", rgb2i16b}, +#ifdef WORDS_BIGENDIAN + {"RGB", "I;16N", rgb2i16b}, +#else + {"RGB", "I;16N", rgb2i16l}, +#endif + {"RGB", "F", rgb2f}, + {"RGB", "BGR;15", rgb2bgr15}, + {"RGB", "BGR;16", rgb2bgr16}, + {"RGB", "BGR;24", rgb2bgr24}, + {"RGB", "RGBA", rgb2rgba}, + {"RGB", "RGBa", rgb2rgba}, + {"RGB", "RGBX", rgb2rgba}, + {"RGB", "CMYK", rgb2cmyk}, + {"RGB", "YCbCr", ImagingConvertRGB2YCbCr}, + {"RGB", "HSV", rgb2hsv}, + + {"RGBA", "1", rgb2bit}, + {"RGBA", "L", rgb2l}, + {"RGBA", "LA", rgba2la}, + {"RGBA", "I", rgb2i}, + {"RGBA", "F", rgb2f}, + {"RGBA", "RGB", rgba2rgb}, + {"RGBA", "RGBa", rgbA2rgba}, + {"RGBA", "RGBX", rgb2rgba}, + {"RGBA", "CMYK", rgb2cmyk}, + {"RGBA", "YCbCr", ImagingConvertRGB2YCbCr}, + {"RGBA", "HSV", rgb2hsv}, + + {"RGBa", "RGBA", rgba2rgbA}, + {"RGBa", "RGB", rgba2rgb_}, + + {"RGBX", "1", rgb2bit}, + {"RGBX", "L", rgb2l}, + {"RGBX", "LA", rgb2la}, + {"RGBX", "I", rgb2i}, + {"RGBX", "F", rgb2f}, + {"RGBX", "RGB", rgba2rgb}, + {"RGBX", "CMYK", rgb2cmyk}, + {"RGBX", "YCbCr", ImagingConvertRGB2YCbCr}, + {"RGBX", "HSV", rgb2hsv}, + + {"CMYK", "RGB", cmyk2rgb}, + {"CMYK", "RGBA", cmyk2rgb}, + {"CMYK", "RGBX", cmyk2rgb}, + {"CMYK", "HSV", cmyk2hsv}, + + {"YCbCr", "L", ycbcr2l}, + {"YCbCr", "LA", ycbcr2la}, + {"YCbCr", "RGB", ImagingConvertYCbCr2RGB}, + + {"HSV", "RGB", hsv2rgb}, + + {"I", "I;16", I_I16L}, + {"I;16", "I", I16L_I}, + {"I;16", "RGB", I16_RGB}, + {"L", "I;16", L_I16L}, + {"I;16", "L", I16L_L}, + + {"I", "I;16L", I_I16L}, + {"I;16L", "I", I16L_I}, + {"I", "I;16B", I_I16B}, + {"I;16B", "I", I16B_I}, + + {"L", "I;16L", L_I16L}, + {"I;16L", "L", I16L_L}, + {"L", "I;16B", L_I16B}, + {"I;16B", "L", I16B_L}, +#ifdef WORDS_BIGENDIAN + {"L", "I;16N", L_I16B}, + {"I;16N", "L", I16B_L}, +#else + {"L", "I;16N", L_I16L}, + {"I;16N", "L", I16L_L}, +#endif + + {"I;16", "F", I16L_F}, + {"I;16L", "F", I16L_F}, + {"I;16B", "F", I16B_F}, + + {NULL}}; + +/* FIXME: translate indexed versions to pointer versions below this line */ + +/* ------------------- */ +/* Palette conversions */ +/* ------------------- */ + +static void +p2bit(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { + int x; + /* FIXME: precalculate grayscale palette? */ + for (x = 0; x < xsize; x++) { + *out++ = (L(&palette->palette[in[x] * 4]) >= 128000) ? 255 : 0; + } +} + +static void +pa2bit(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { + int x; + /* FIXME: precalculate grayscale palette? */ + for (x = 0; x < xsize; x++, in += 4) { + *out++ = (L(&palette->palette[in[0] * 4]) >= 128000) ? 255 : 0; + } +} + +static void +p2l(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { + int x; + /* FIXME: precalculate grayscale palette? */ + for (x = 0; x < xsize; x++) { + *out++ = L24(&palette->palette[in[x] * 4]) >> 16; + } +} + +static void +pa2l(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { + int x; + /* FIXME: precalculate grayscale palette? */ + for (x = 0; x < xsize; x++, in += 4) { + *out++ = L24(&palette->palette[in[0] * 4]) >> 16; + } +} + +static void +pa2p(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { + int x; + for (x = 0; x < xsize; x++, in += 4) { + *out++ = in[0]; + } +} + +static void +p2pa(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { + int x; + int rgb = strcmp(palette->mode, "RGB"); + for (x = 0; x < xsize; x++, in++) { + const UINT8 *rgba = &palette->palette[in[0] * 4]; + *out++ = in[0]; + *out++ = in[0]; + *out++ = in[0]; + *out++ = rgb == 0 ? 255 : rgba[3]; + } +} + +static void +p2la(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { + int x; + /* FIXME: precalculate grayscale palette? */ + for (x = 0; x < xsize; x++, out += 4) { + const UINT8 *rgba = &palette->palette[*in++ * 4]; + out[0] = out[1] = out[2] = L24(rgba) >> 16; + out[3] = rgba[3]; + } +} + +static void +pa2la(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { + int x; + /* FIXME: precalculate grayscale palette? */ + for (x = 0; x < xsize; x++, in += 4, out += 4) { + out[0] = out[1] = out[2] = L24(&palette->palette[in[0] * 4]) >> 16; + out[3] = in[3]; + } +} + +static void +p2i(UINT8 *out_, const UINT8 *in, int xsize, ImagingPalette palette) { + int x; + for (x = 0; x < xsize; x++, out_ += 4) { + INT32 v = L24(&palette->palette[in[x] * 4]) >> 16; + memcpy(out_, &v, sizeof(v)); + } +} + +static void +pa2i(UINT8 *out_, const UINT8 *in, int xsize, ImagingPalette palette) { + int x; + INT32 *out = (INT32 *)out_; + for (x = 0; x < xsize; x++, in += 4) { + *out++ = L24(&palette->palette[in[0] * 4]) >> 16; + } +} + +static void +p2f(UINT8 *out_, const UINT8 *in, int xsize, ImagingPalette palette) { + int x; + for (x = 0; x < xsize; x++, out_ += 4) { + FLOAT32 v = L(&palette->palette[in[x] * 4]) / 1000.0F; + memcpy(out_, &v, sizeof(v)); + } +} + +static void +pa2f(UINT8 *out_, const UINT8 *in, int xsize, ImagingPalette palette) { + int x; + FLOAT32 *out = (FLOAT32 *)out_; + for (x = 0; x < xsize; x++, in += 4) { + *out++ = (float)L(&palette->palette[in[0] * 4]) / 1000.0F; + } +} + +static void +p2rgb(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { + int x; + for (x = 0; x < xsize; x++) { + const UINT8 *rgb = &palette->palette[*in++ * 4]; + *out++ = rgb[0]; + *out++ = rgb[1]; + *out++ = rgb[2]; + *out++ = 255; + } +} + +static void +pa2rgb(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { + int x; + for (x = 0; x < xsize; x++, in += 4) { + const UINT8 *rgb = &palette->palette[in[0] * 4]; + *out++ = rgb[0]; + *out++ = rgb[1]; + *out++ = rgb[2]; + *out++ = 255; + } +} + +static void +p2hsv(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { + int x; + for (x = 0; x < xsize; x++, out += 4) { + const UINT8 *rgb = &palette->palette[*in++ * 4]; + rgb2hsv_row(out, rgb); + out[3] = 255; + } +} + +static void +pa2hsv(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { + int x; + for (x = 0; x < xsize; x++, in += 4, out += 4) { + const UINT8 *rgb = &palette->palette[in[0] * 4]; + rgb2hsv_row(out, rgb); + out[3] = 255; + } +} + +static void +p2rgba(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { + int x; + for (x = 0; x < xsize; x++) { + const UINT8 *rgba = &palette->palette[*in++ * 4]; + *out++ = rgba[0]; + *out++ = rgba[1]; + *out++ = rgba[2]; + *out++ = rgba[3]; + } +} + +static void +pa2rgba(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { + int x; + for (x = 0; x < xsize; x++, in += 4) { + const UINT8 *rgb = &palette->palette[in[0] * 4]; + *out++ = rgb[0]; + *out++ = rgb[1]; + *out++ = rgb[2]; + *out++ = in[3]; + } +} + +static void +p2cmyk(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { + p2rgb(out, in, xsize, palette); + rgb2cmyk(out, out, xsize); +} + +static void +pa2cmyk(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { + pa2rgb(out, in, xsize, palette); + rgb2cmyk(out, out, xsize); +} + +static void +p2ycbcr(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { + p2rgb(out, in, xsize, palette); + ImagingConvertRGB2YCbCr(out, out, xsize); +} + +static void +pa2ycbcr(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { + pa2rgb(out, in, xsize, palette); + ImagingConvertRGB2YCbCr(out, out, xsize); +} + +static Imaging +frompalette(Imaging imOut, Imaging imIn, const char *mode) { + ImagingSectionCookie cookie; + int alpha; + int y; + void (*convert)(UINT8 *, const UINT8 *, int, ImagingPalette); + + /* Map palette image to L, RGB, RGBA, or CMYK */ + + if (!imIn->palette) { + return (Imaging)ImagingError_ValueError("no palette"); + } + + alpha = !strcmp(imIn->mode, "PA"); + + if (strcmp(mode, "1") == 0) { + convert = alpha ? pa2bit : p2bit; + } else if (strcmp(mode, "L") == 0) { + convert = alpha ? pa2l : p2l; + } else if (strcmp(mode, "LA") == 0) { + convert = alpha ? pa2la : p2la; + } else if (strcmp(mode, "P") == 0) { + convert = pa2p; + } else if (strcmp(mode, "PA") == 0) { + convert = p2pa; + } else if (strcmp(mode, "I") == 0) { + convert = alpha ? pa2i : p2i; + } else if (strcmp(mode, "F") == 0) { + convert = alpha ? pa2f : p2f; + } else if (strcmp(mode, "RGB") == 0) { + convert = alpha ? pa2rgb : p2rgb; + } else if (strcmp(mode, "RGBA") == 0 || strcmp(mode, "RGBX") == 0) { + convert = alpha ? pa2rgba : p2rgba; + } else if (strcmp(mode, "CMYK") == 0) { + convert = alpha ? pa2cmyk : p2cmyk; + } else if (strcmp(mode, "YCbCr") == 0) { + convert = alpha ? pa2ycbcr : p2ycbcr; + } else if (strcmp(mode, "HSV") == 0) { + convert = alpha ? pa2hsv : p2hsv; + } else { + return (Imaging)ImagingError_ValueError("conversion not supported"); + } + + imOut = ImagingNew2Dirty(mode, imOut, imIn); + if (!imOut) { + return NULL; + } + if (strcmp(mode, "P") == 0 || strcmp(mode, "PA") == 0) { + ImagingPaletteDelete(imOut->palette); + imOut->palette = ImagingPaletteDuplicate(imIn->palette); + } + + ImagingSectionEnter(&cookie); + for (y = 0; y < imIn->ysize; y++) { + (*convert)( + (UINT8 *)imOut->image[y], + (UINT8 *)imIn->image[y], + imIn->xsize, + imIn->palette); + } + ImagingSectionLeave(&cookie); + + return imOut; +} + +#if defined(_MSC_VER) +#pragma optimize("", off) +#endif +static Imaging +topalette( + Imaging imOut, + Imaging imIn, + const char *mode, + ImagingPalette inpalette, + int dither) { + ImagingSectionCookie cookie; + int alpha; + int x, y; + ImagingPalette palette = inpalette; + + /* Map L or RGB/RGBX/RGBA to palette image */ + if (strcmp(imIn->mode, "L") != 0 && strncmp(imIn->mode, "RGB", 3) != 0) { + return (Imaging)ImagingError_ValueError("conversion not supported"); + } + + alpha = !strcmp(mode, "PA"); + + if (palette == NULL) { + /* FIXME: make user configurable */ + if (imIn->bands == 1) { + palette = ImagingPaletteNew("RGB"); + + palette->size = 256; + int i; + for (i = 0; i < 256; i++) { + palette->palette[i * 4] = palette->palette[i * 4 + 1] = + palette->palette[i * 4 + 2] = (UINT8)i; + } + } else { + palette = ImagingPaletteNewBrowser(); /* Standard colour cube */ + } + } + + if (!palette) { + return (Imaging)ImagingError_ValueError("no palette"); + } + + imOut = ImagingNew2Dirty(mode, imOut, imIn); + if (!imOut) { + if (palette != inpalette) { + ImagingPaletteDelete(palette); + } + return NULL; + } + + ImagingPaletteDelete(imOut->palette); + imOut->palette = ImagingPaletteDuplicate(palette); + + if (imIn->bands == 1) { + /* grayscale image */ + + /* Grayscale palette: copy data as is */ + ImagingSectionEnter(&cookie); + for (y = 0; y < imIn->ysize; y++) { + if (alpha) { + l2la((UINT8 *)imOut->image[y], (UINT8 *)imIn->image[y], imIn->xsize); + } else { + memcpy(imOut->image[y], imIn->image[y], imIn->linesize); + } + } + ImagingSectionLeave(&cookie); + + } else { + /* colour image */ + + /* Create mapping cache */ + if (ImagingPaletteCachePrepare(palette) < 0) { + ImagingDelete(imOut); + if (palette != inpalette) { + ImagingPaletteDelete(palette); + } + return NULL; + } + + if (dither) { + /* floyd-steinberg dither */ + + int *errors; + errors = calloc(imIn->xsize + 1, sizeof(int) * 3); + if (!errors) { + ImagingDelete(imOut); + return ImagingError_MemoryError(); + } + + /* Map each pixel to the nearest palette entry */ + ImagingSectionEnter(&cookie); + for (y = 0; y < imIn->ysize; y++) { + int r, r0, r1, r2; + int g, g0, g1, g2; + int b, b0, b1, b2; + UINT8 *in = (UINT8 *)imIn->image[y]; + UINT8 *out = alpha ? (UINT8 *)imOut->image32[y] : imOut->image8[y]; + int *e = errors; + + r = r0 = r1 = 0; + g = g0 = g1 = 0; + b = b0 = b1 = b2 = 0; + + for (x = 0; x < imIn->xsize; x++, in += 4) { + int d2; + INT16 *cache; + + r = CLIP8(in[0] + (r + e[3 + 0]) / 16); + g = CLIP8(in[1] + (g + e[3 + 1]) / 16); + b = CLIP8(in[2] + (b + e[3 + 2]) / 16); + + /* get closest colour */ + cache = &ImagingPaletteCache(palette, r, g, b); + if (cache[0] == 0x100) { + ImagingPaletteCacheUpdate(palette, r, g, b); + } + if (alpha) { + out[x * 4] = out[x * 4 + 1] = out[x * 4 + 2] = (UINT8)cache[0]; + out[x * 4 + 3] = 255; + } else { + out[x] = (UINT8)cache[0]; + } + + r -= (int)palette->palette[cache[0] * 4]; + g -= (int)palette->palette[cache[0] * 4 + 1]; + b -= (int)palette->palette[cache[0] * 4 + 2]; + + /* propagate errors (don't ask ;-) */ + r2 = r; + d2 = r + r; + r += d2; + e[0] = r + r0; + r += d2; + r0 = r + r1; + r1 = r2; + r += d2; + g2 = g; + d2 = g + g; + g += d2; + e[1] = g + g0; + g += d2; + g0 = g + g1; + g1 = g2; + g += d2; + b2 = b; + d2 = b + b; + b += d2; + e[2] = b + b0; + b += d2; + b0 = b + b1; + b1 = b2; + b += d2; + + e += 3; + } + + e[0] = b0; + e[1] = b1; + e[2] = b2; + } + ImagingSectionLeave(&cookie); + free(errors); + + } else { + /* closest colour */ + ImagingSectionEnter(&cookie); + for (y = 0; y < imIn->ysize; y++) { + int r, g, b; + UINT8 *in = (UINT8 *)imIn->image[y]; + UINT8 *out = alpha ? (UINT8 *)imOut->image32[y] : imOut->image8[y]; + + for (x = 0; x < imIn->xsize; x++, in += 4) { + INT16 *cache; + + r = in[0]; + g = in[1]; + b = in[2]; + + /* get closest colour */ + cache = &ImagingPaletteCache(palette, r, g, b); + if (cache[0] == 0x100) { + ImagingPaletteCacheUpdate(palette, r, g, b); + } + if (alpha) { + out[x * 4] = out[x * 4 + 1] = out[x * 4 + 2] = (UINT8)cache[0]; + out[x * 4 + 3] = 255; + } else { + out[x] = (UINT8)cache[0]; + } + } + } + ImagingSectionLeave(&cookie); + } + if (inpalette != palette) { + ImagingPaletteCacheDelete(palette); + } + } + + if (inpalette != palette) { + ImagingPaletteDelete(palette); + } + + return imOut; +} + +static Imaging +tobilevel(Imaging imOut, Imaging imIn) { + ImagingSectionCookie cookie; + int x, y; + int *errors; + + /* Map L or RGB to dithered 1 image */ + if (strcmp(imIn->mode, "L") != 0 && strcmp(imIn->mode, "RGB") != 0) { + return (Imaging)ImagingError_ValueError("conversion not supported"); + } + + imOut = ImagingNew2Dirty("1", imOut, imIn); + if (!imOut) { + return NULL; + } + + errors = calloc(imIn->xsize + 1, sizeof(int)); + if (!errors) { + ImagingDelete(imOut); + return ImagingError_MemoryError(); + } + + if (imIn->bands == 1) { + /* map each pixel to black or white, using error diffusion */ + ImagingSectionEnter(&cookie); + for (y = 0; y < imIn->ysize; y++) { + int l, l0, l1, l2, d2; + UINT8 *in = (UINT8 *)imIn->image[y]; + UINT8 *out = imOut->image8[y]; + + l = l0 = l1 = 0; + + for (x = 0; x < imIn->xsize; x++) { + /* pick closest colour */ + l = CLIP8(in[x] + (l + errors[x + 1]) / 16); + out[x] = (l > 128) ? 255 : 0; + + /* propagate errors */ + l -= (int)out[x]; + l2 = l; + d2 = l + l; + l += d2; + errors[x] = l + l0; + l += d2; + l0 = l + l1; + l1 = l2; + l += d2; + } + + errors[x] = l0; + } + ImagingSectionLeave(&cookie); + + } else { + /* map each pixel to black or white, using error diffusion */ + ImagingSectionEnter(&cookie); + for (y = 0; y < imIn->ysize; y++) { + int l, l0, l1, l2, d2; + UINT8 *in = (UINT8 *)imIn->image[y]; + UINT8 *out = imOut->image8[y]; + + l = l0 = l1 = 0; + + for (x = 0; x < imIn->xsize; x++, in += 4) { + /* pick closest colour */ + l = CLIP8(L(in) / 1000 + (l + errors[x + 1]) / 16); + out[x] = (l > 128) ? 255 : 0; + + /* propagate errors */ + l -= (int)out[x]; + l2 = l; + d2 = l + l; + l += d2; + errors[x] = l + l0; + l += d2; + l0 = l + l1; + l1 = l2; + l += d2; + } + + errors[x] = l0; + } + ImagingSectionLeave(&cookie); + } + + free(errors); + + return imOut; +} +#if defined(_MSC_VER) +#pragma optimize("", on) +#endif + +static Imaging +convert( + Imaging imOut, Imaging imIn, const char *mode, ImagingPalette palette, int dither) { + ImagingSectionCookie cookie; + ImagingShuffler convert; + int y; + + if (!imIn) { + return (Imaging)ImagingError_ModeError(); + } + + if (!mode) { + /* Map palette image to full depth */ + if (!imIn->palette) { + return (Imaging)ImagingError_ModeError(); + } + mode = imIn->palette->mode; + } else { + /* Same mode? */ + if (!strcmp(imIn->mode, mode)) { + return ImagingCopy2(imOut, imIn); + } + } + + /* test for special conversions */ + + if (strcmp(imIn->mode, "P") == 0 || strcmp(imIn->mode, "PA") == 0) { + return frompalette(imOut, imIn, mode); + } + + if (strcmp(mode, "P") == 0 || strcmp(mode, "PA") == 0) { + return topalette(imOut, imIn, mode, palette, dither); + } + + if (dither && strcmp(mode, "1") == 0) { + return tobilevel(imOut, imIn); + } + + /* standard conversion machinery */ + + convert = NULL; + + for (y = 0; converters[y].from; y++) { + if (!strcmp(imIn->mode, converters[y].from) && + !strcmp(mode, converters[y].to)) { + convert = converters[y].convert; + break; + } + } + + if (!convert) { +#ifdef notdef + return (Imaging)ImagingError_ValueError("conversion not supported"); +#else + static char buf[100]; + snprintf( + buf, 100, "conversion from %.10s to %.10s not supported", imIn->mode, mode); + return (Imaging)ImagingError_ValueError(buf); +#endif + } + + imOut = ImagingNew2Dirty(mode, imOut, imIn); + if (!imOut) { + return NULL; + } + + ImagingSectionEnter(&cookie); + for (y = 0; y < imIn->ysize; y++) { + (*convert)((UINT8 *)imOut->image[y], (UINT8 *)imIn->image[y], imIn->xsize); + } + ImagingSectionLeave(&cookie); + + return imOut; +} + +Imaging +ImagingConvert(Imaging imIn, const char *mode, ImagingPalette palette, int dither) { + return convert(NULL, imIn, mode, palette, dither); +} + +Imaging +ImagingConvert2(Imaging imOut, Imaging imIn) { + return convert(imOut, imIn, imOut->mode, NULL, 0); +} + +Imaging +ImagingConvertTransparent(Imaging imIn, const char *mode, int r, int g, int b) { + ImagingSectionCookie cookie; + ImagingShuffler convert; + Imaging imOut = NULL; + int premultiplied = 0; + // If the transparency matches pixels in the source image, not the converted image + UINT8 *source; + int source_transparency = 0; + int y; + + if (!imIn) { + return (Imaging)ImagingError_ModeError(); + } + + if (strcmp(imIn->mode, "RGB") == 0 && + (strcmp(mode, "RGBA") == 0 || strcmp(mode, "RGBa") == 0)) { + convert = rgb2rgba; + if (strcmp(mode, "RGBa") == 0) { + premultiplied = 1; + } + } else if ( + strcmp(imIn->mode, "RGB") == 0 && + (strcmp(mode, "LA") == 0 || strcmp(mode, "La") == 0)) { + convert = rgb2la; + source_transparency = 1; + if (strcmp(mode, "La") == 0) { + premultiplied = 1; + } + } else if ( + (strcmp(imIn->mode, "1") == 0 || strcmp(imIn->mode, "I") == 0 || + strcmp(imIn->mode, "I;16") == 0 || strcmp(imIn->mode, "L") == 0) && + (strcmp(mode, "RGBA") == 0 || strcmp(mode, "LA") == 0)) { + if (strcmp(imIn->mode, "1") == 0) { + convert = bit2rgb; + } else if (strcmp(imIn->mode, "I") == 0) { + convert = i2rgb; + } else if (strcmp(imIn->mode, "I;16") == 0) { + convert = I16_RGB; + } else { + convert = l2rgb; + } + g = b = r; + } else { + static char buf[100]; + snprintf( + buf, + 100, + "conversion from %.10s to %.10s not supported in convert_transparent", + imIn->mode, + mode); + return (Imaging)ImagingError_ValueError(buf); + } + + imOut = ImagingNew2Dirty(mode, imOut, imIn); + if (!imOut) { + return NULL; + } + + ImagingSectionEnter(&cookie); + for (y = 0; y < imIn->ysize; y++) { + (*convert)((UINT8 *)imOut->image[y], (UINT8 *)imIn->image[y], imIn->xsize); + + source = source_transparency ? (UINT8 *)imIn->image[y] : NULL; + rgbT2a((UINT8 *)imOut->image[y], source, imIn->xsize, r, g, b, premultiplied); + } + ImagingSectionLeave(&cookie); + + return imOut; +} + +Imaging +ImagingConvertInPlace(Imaging imIn, const char *mode) { + ImagingSectionCookie cookie; + ImagingShuffler convert; + int y; + + /* limited support for inplace conversion */ + if (strcmp(imIn->mode, "L") == 0 && strcmp(mode, "1") == 0) { + convert = l2bit; + } else if (strcmp(imIn->mode, "1") == 0 && strcmp(mode, "L") == 0) { + convert = bit2l; + } else { + return ImagingError_ModeError(); + } + + ImagingSectionEnter(&cookie); + for (y = 0; y < imIn->ysize; y++) { + (*convert)((UINT8 *)imIn->image[y], (UINT8 *)imIn->image[y], imIn->xsize); + } + ImagingSectionLeave(&cookie); + + return imIn; +} diff --git a/src/libImaging/Convert.h b/src/libImaging/Convert.h index e688e3018..36dcb01d1 100644 --- a/src/libImaging/Convert.h +++ b/src/libImaging/Convert.h @@ -1,2 +1,2 @@ -extern void -cmyk2rgb(UINT8 *out, const UINT8 *in, int xsize); +extern void +cmyk2rgb(UINT8 *out, const UINT8 *in, int xsize); diff --git a/src/libImaging/ConvertYCbCr.c b/src/libImaging/ConvertYCbCr.c index 142f065e5..a67cf092b 100644 --- a/src/libImaging/ConvertYCbCr.c +++ b/src/libImaging/ConvertYCbCr.c @@ -1,363 +1,363 @@ -/* - * The Python Imaging Library. - * $Id$ - * - * code to convert YCbCr data - * - * history: - * 98-07-01 hk Created - * - * Copyright (c) Secret Labs AB 1998 - * - * See the README file for information on usage and redistribution. - */ - -#include "Imaging.h" - -/* JPEG/JFIF YCbCr conversions - - Y = R * 0.29900 + G * 0.58700 + B * 0.11400 - Cb = R * -0.16874 + G * -0.33126 + B * 0.50000 + 128 - Cr = R * 0.50000 + G * -0.41869 + B * -0.08131 + 128 - - R = Y + + (Cr - 128) * 1.40200 - G = Y + (Cb - 128) * -0.34414 + (Cr - 128) * -0.71414 - B = Y + (Cb - 128) * 1.77200 - -*/ - -#define SCALE 6 /* bits */ - -static INT16 Y_R[] = { - 0, 19, 38, 57, 77, 96, 115, 134, 153, 172, 191, 210, 230, 249, - 268, 287, 306, 325, 344, 364, 383, 402, 421, 440, 459, 478, 498, 517, - 536, 555, 574, 593, 612, 631, 651, 670, 689, 708, 727, 746, 765, 785, - 804, 823, 842, 861, 880, 899, 919, 938, 957, 976, 995, 1014, 1033, 1052, - 1072, 1091, 1110, 1129, 1148, 1167, 1186, 1206, 1225, 1244, 1263, 1282, 1301, 1320, - 1340, 1359, 1378, 1397, 1416, 1435, 1454, 1473, 1493, 1512, 1531, 1550, 1569, 1588, - 1607, 1627, 1646, 1665, 1684, 1703, 1722, 1741, 1761, 1780, 1799, 1818, 1837, 1856, - 1875, 1894, 1914, 1933, 1952, 1971, 1990, 2009, 2028, 2048, 2067, 2086, 2105, 2124, - 2143, 2162, 2182, 2201, 2220, 2239, 2258, 2277, 2296, 2315, 2335, 2354, 2373, 2392, - 2411, 2430, 2449, 2469, 2488, 2507, 2526, 2545, 2564, 2583, 2602, 2622, 2641, 2660, - 2679, 2698, 2717, 2736, 2756, 2775, 2794, 2813, 2832, 2851, 2870, 2890, 2909, 2928, - 2947, 2966, 2985, 3004, 3023, 3043, 3062, 3081, 3100, 3119, 3138, 3157, 3177, 3196, - 3215, 3234, 3253, 3272, 3291, 3311, 3330, 3349, 3368, 3387, 3406, 3425, 3444, 3464, - 3483, 3502, 3521, 3540, 3559, 3578, 3598, 3617, 3636, 3655, 3674, 3693, 3712, 3732, - 3751, 3770, 3789, 3808, 3827, 3846, 3865, 3885, 3904, 3923, 3942, 3961, 3980, 3999, - 4019, 4038, 4057, 4076, 4095, 4114, 4133, 4153, 4172, 4191, 4210, 4229, 4248, 4267, - 4286, 4306, 4325, 4344, 4363, 4382, 4401, 4420, 4440, 4459, 4478, 4497, 4516, 4535, - 4554, 4574, 4593, 4612, 4631, 4650, 4669, 4688, 4707, 4727, 4746, 4765, 4784, 4803, - 4822, 4841, 4861, 4880}; - -static INT16 Y_G[] = { - 0, 38, 75, 113, 150, 188, 225, 263, 301, 338, 376, 413, 451, 488, - 526, 564, 601, 639, 676, 714, 751, 789, 826, 864, 902, 939, 977, 1014, - 1052, 1089, 1127, 1165, 1202, 1240, 1277, 1315, 1352, 1390, 1428, 1465, 1503, 1540, - 1578, 1615, 1653, 1691, 1728, 1766, 1803, 1841, 1878, 1916, 1954, 1991, 2029, 2066, - 2104, 2141, 2179, 2217, 2254, 2292, 2329, 2367, 2404, 2442, 2479, 2517, 2555, 2592, - 2630, 2667, 2705, 2742, 2780, 2818, 2855, 2893, 2930, 2968, 3005, 3043, 3081, 3118, - 3156, 3193, 3231, 3268, 3306, 3344, 3381, 3419, 3456, 3494, 3531, 3569, 3607, 3644, - 3682, 3719, 3757, 3794, 3832, 3870, 3907, 3945, 3982, 4020, 4057, 4095, 4132, 4170, - 4208, 4245, 4283, 4320, 4358, 4395, 4433, 4471, 4508, 4546, 4583, 4621, 4658, 4696, - 4734, 4771, 4809, 4846, 4884, 4921, 4959, 4997, 5034, 5072, 5109, 5147, 5184, 5222, - 5260, 5297, 5335, 5372, 5410, 5447, 5485, 5522, 5560, 5598, 5635, 5673, 5710, 5748, - 5785, 5823, 5861, 5898, 5936, 5973, 6011, 6048, 6086, 6124, 6161, 6199, 6236, 6274, - 6311, 6349, 6387, 6424, 6462, 6499, 6537, 6574, 6612, 6650, 6687, 6725, 6762, 6800, - 6837, 6875, 6913, 6950, 6988, 7025, 7063, 7100, 7138, 7175, 7213, 7251, 7288, 7326, - 7363, 7401, 7438, 7476, 7514, 7551, 7589, 7626, 7664, 7701, 7739, 7777, 7814, 7852, - 7889, 7927, 7964, 8002, 8040, 8077, 8115, 8152, 8190, 8227, 8265, 8303, 8340, 8378, - 8415, 8453, 8490, 8528, 8566, 8603, 8641, 8678, 8716, 8753, 8791, 8828, 8866, 8904, - 8941, 8979, 9016, 9054, 9091, 9129, 9167, 9204, 9242, 9279, 9317, 9354, 9392, 9430, - 9467, 9505, 9542, 9580}; - -static INT16 Y_B[] = { - 0, 7, 15, 22, 29, 36, 44, 51, 58, 66, 73, 80, 88, 95, - 102, 109, 117, 124, 131, 139, 146, 153, 161, 168, 175, 182, 190, 197, - 204, 212, 219, 226, 233, 241, 248, 255, 263, 270, 277, 285, 292, 299, - 306, 314, 321, 328, 336, 343, 350, 358, 365, 372, 379, 387, 394, 401, - 409, 416, 423, 430, 438, 445, 452, 460, 467, 474, 482, 489, 496, 503, - 511, 518, 525, 533, 540, 547, 554, 562, 569, 576, 584, 591, 598, 606, - 613, 620, 627, 635, 642, 649, 657, 664, 671, 679, 686, 693, 700, 708, - 715, 722, 730, 737, 744, 751, 759, 766, 773, 781, 788, 795, 803, 810, - 817, 824, 832, 839, 846, 854, 861, 868, 876, 883, 890, 897, 905, 912, - 919, 927, 934, 941, 948, 956, 963, 970, 978, 985, 992, 1000, 1007, 1014, - 1021, 1029, 1036, 1043, 1051, 1058, 1065, 1073, 1080, 1087, 1094, 1102, 1109, 1116, - 1124, 1131, 1138, 1145, 1153, 1160, 1167, 1175, 1182, 1189, 1197, 1204, 1211, 1218, - 1226, 1233, 1240, 1248, 1255, 1262, 1270, 1277, 1284, 1291, 1299, 1306, 1313, 1321, - 1328, 1335, 1342, 1350, 1357, 1364, 1372, 1379, 1386, 1394, 1401, 1408, 1415, 1423, - 1430, 1437, 1445, 1452, 1459, 1466, 1474, 1481, 1488, 1496, 1503, 1510, 1518, 1525, - 1532, 1539, 1547, 1554, 1561, 1569, 1576, 1583, 1591, 1598, 1605, 1612, 1620, 1627, - 1634, 1642, 1649, 1656, 1663, 1671, 1678, 1685, 1693, 1700, 1707, 1715, 1722, 1729, - 1736, 1744, 1751, 1758, 1766, 1773, 1780, 1788, 1795, 1802, 1809, 1817, 1824, 1831, - 1839, 1846, 1853, 1860}; - -static INT16 Cb_R[] = { - 0, -10, -21, -31, -42, -53, -64, -75, -85, -96, -107, -118, - -129, -139, -150, -161, -172, -183, -193, -204, -215, -226, -237, -247, - -258, -269, -280, -291, -301, -312, -323, -334, -345, -355, -366, -377, - -388, -399, -409, -420, -431, -442, -453, -463, -474, -485, -496, -507, - -517, -528, -539, -550, -561, -571, -582, -593, -604, -615, -625, -636, - -647, -658, -669, -679, -690, -701, -712, -723, -733, -744, -755, -766, - -777, -787, -798, -809, -820, -831, -841, -852, -863, -874, -885, -895, - -906, -917, -928, -939, -949, -960, -971, -982, -993, -1003, -1014, -1025, - -1036, -1047, -1057, -1068, -1079, -1090, -1101, -1111, -1122, -1133, -1144, -1155, - -1165, -1176, -1187, -1198, -1209, -1219, -1230, -1241, -1252, -1263, -1273, -1284, - -1295, -1306, -1317, -1327, -1338, -1349, -1360, -1371, -1381, -1392, -1403, -1414, - -1425, -1435, -1446, -1457, -1468, -1479, -1489, -1500, -1511, -1522, -1533, -1543, - -1554, -1565, -1576, -1587, -1597, -1608, -1619, -1630, -1641, -1651, -1662, -1673, - -1684, -1694, -1705, -1716, -1727, -1738, -1748, -1759, -1770, -1781, -1792, -1802, - -1813, -1824, -1835, -1846, -1856, -1867, -1878, -1889, -1900, -1910, -1921, -1932, - -1943, -1954, -1964, -1975, -1986, -1997, -2008, -2018, -2029, -2040, -2051, -2062, - -2072, -2083, -2094, -2105, -2116, -2126, -2137, -2148, -2159, -2170, -2180, -2191, - -2202, -2213, -2224, -2234, -2245, -2256, -2267, -2278, -2288, -2299, -2310, -2321, - -2332, -2342, -2353, -2364, -2375, -2386, -2396, -2407, -2418, -2429, -2440, -2450, - -2461, -2472, -2483, -2494, -2504, -2515, -2526, -2537, -2548, -2558, -2569, -2580, - -2591, -2602, -2612, -2623, -2634, -2645, -2656, -2666, -2677, -2688, -2699, -2710, - -2720, -2731, -2742, -2753}; - -static INT16 Cb_G[] = { - 0, -20, -41, -63, -84, -105, -126, -147, -169, -190, -211, -232, - -253, -275, -296, -317, -338, -359, -381, -402, -423, -444, -465, -487, - -508, -529, -550, -571, -593, -614, -635, -656, -677, -699, -720, -741, - -762, -783, -805, -826, -847, -868, -889, -911, -932, -953, -974, -995, - -1017, -1038, -1059, -1080, -1101, -1123, -1144, -1165, -1186, -1207, -1229, -1250, - -1271, -1292, -1313, -1335, -1356, -1377, -1398, -1419, -1441, -1462, -1483, -1504, - -1525, -1547, -1568, -1589, -1610, -1631, -1653, -1674, -1695, -1716, -1737, -1759, - -1780, -1801, -1822, -1843, -1865, -1886, -1907, -1928, -1949, -1971, -1992, -2013, - -2034, -2055, -2077, -2098, -2119, -2140, -2161, -2183, -2204, -2225, -2246, -2267, - -2289, -2310, -2331, -2352, -2373, -2395, -2416, -2437, -2458, -2479, -2501, -2522, - -2543, -2564, -2585, -2607, -2628, -2649, -2670, -2691, -2713, -2734, -2755, -2776, - -2797, -2819, -2840, -2861, -2882, -2903, -2925, -2946, -2967, -2988, -3009, -3031, - -3052, -3073, -3094, -3115, -3137, -3158, -3179, -3200, -3221, -3243, -3264, -3285, - -3306, -3328, -3349, -3370, -3391, -3412, -3434, -3455, -3476, -3497, -3518, -3540, - -3561, -3582, -3603, -3624, -3646, -3667, -3688, -3709, -3730, -3752, -3773, -3794, - -3815, -3836, -3858, -3879, -3900, -3921, -3942, -3964, -3985, -4006, -4027, -4048, - -4070, -4091, -4112, -4133, -4154, -4176, -4197, -4218, -4239, -4260, -4282, -4303, - -4324, -4345, -4366, -4388, -4409, -4430, -4451, -4472, -4494, -4515, -4536, -4557, - -4578, -4600, -4621, -4642, -4663, -4684, -4706, -4727, -4748, -4769, -4790, -4812, - -4833, -4854, -4875, -4896, -4918, -4939, -4960, -4981, -5002, -5024, -5045, -5066, - -5087, -5108, -5130, -5151, -5172, -5193, -5214, -5236, -5257, -5278, -5299, -5320, - -5342, -5363, -5384, -5405}; - -static INT16 Cb_B[] = { - 0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, - 448, 480, 512, 544, 576, 608, 640, 672, 704, 736, 768, 800, 832, 864, - 896, 928, 960, 992, 1024, 1056, 1088, 1120, 1152, 1184, 1216, 1248, 1280, 1312, - 1344, 1376, 1408, 1440, 1472, 1504, 1536, 1568, 1600, 1632, 1664, 1696, 1728, 1760, - 1792, 1824, 1856, 1888, 1920, 1952, 1984, 2016, 2048, 2080, 2112, 2144, 2176, 2208, - 2240, 2272, 2304, 2336, 2368, 2400, 2432, 2464, 2496, 2528, 2560, 2592, 2624, 2656, - 2688, 2720, 2752, 2784, 2816, 2848, 2880, 2912, 2944, 2976, 3008, 3040, 3072, 3104, - 3136, 3168, 3200, 3232, 3264, 3296, 3328, 3360, 3392, 3424, 3456, 3488, 3520, 3552, - 3584, 3616, 3648, 3680, 3712, 3744, 3776, 3808, 3840, 3872, 3904, 3936, 3968, 4000, - 4032, 4064, 4096, 4128, 4160, 4192, 4224, 4256, 4288, 4320, 4352, 4384, 4416, 4448, - 4480, 4512, 4544, 4576, 4608, 4640, 4672, 4704, 4736, 4768, 4800, 4832, 4864, 4896, - 4928, 4960, 4992, 5024, 5056, 5088, 5120, 5152, 5184, 5216, 5248, 5280, 5312, 5344, - 5376, 5408, 5440, 5472, 5504, 5536, 5568, 5600, 5632, 5664, 5696, 5728, 5760, 5792, - 5824, 5856, 5888, 5920, 5952, 5984, 6016, 6048, 6080, 6112, 6144, 6176, 6208, 6240, - 6272, 6304, 6336, 6368, 6400, 6432, 6464, 6496, 6528, 6560, 6592, 6624, 6656, 6688, - 6720, 6752, 6784, 6816, 6848, 6880, 6912, 6944, 6976, 7008, 7040, 7072, 7104, 7136, - 7168, 7200, 7232, 7264, 7296, 7328, 7360, 7392, 7424, 7456, 7488, 7520, 7552, 7584, - 7616, 7648, 7680, 7712, 7744, 7776, 7808, 7840, 7872, 7904, 7936, 7968, 8000, 8032, - 8064, 8096, 8128, 8160}; - -#define Cr_R Cb_B - -static INT16 Cr_G[] = { - 0, -26, -53, -79, -106, -133, -160, -187, -213, -240, -267, -294, - -321, -347, -374, -401, -428, -455, -481, -508, -535, -562, -589, -615, - -642, -669, -696, -722, -749, -776, -803, -830, -856, -883, -910, -937, - -964, -990, -1017, -1044, -1071, -1098, -1124, -1151, -1178, -1205, -1232, -1258, - -1285, -1312, -1339, -1366, -1392, -1419, -1446, -1473, -1500, -1526, -1553, -1580, - -1607, -1634, -1660, -1687, -1714, -1741, -1768, -1794, -1821, -1848, -1875, -1902, - -1928, -1955, -1982, -2009, -2036, -2062, -2089, -2116, -2143, -2169, -2196, -2223, - -2250, -2277, -2303, -2330, -2357, -2384, -2411, -2437, -2464, -2491, -2518, -2545, - -2571, -2598, -2625, -2652, -2679, -2705, -2732, -2759, -2786, -2813, -2839, -2866, - -2893, -2920, -2947, -2973, -3000, -3027, -3054, -3081, -3107, -3134, -3161, -3188, - -3215, -3241, -3268, -3295, -3322, -3349, -3375, -3402, -3429, -3456, -3483, -3509, - -3536, -3563, -3590, -3616, -3643, -3670, -3697, -3724, -3750, -3777, -3804, -3831, - -3858, -3884, -3911, -3938, -3965, -3992, -4018, -4045, -4072, -4099, -4126, -4152, - -4179, -4206, -4233, -4260, -4286, -4313, -4340, -4367, -4394, -4420, -4447, -4474, - -4501, -4528, -4554, -4581, -4608, -4635, -4662, -4688, -4715, -4742, -4769, -4796, - -4822, -4849, -4876, -4903, -4929, -4956, -4983, -5010, -5037, -5063, -5090, -5117, - -5144, -5171, -5197, -5224, -5251, -5278, -5305, -5331, -5358, -5385, -5412, -5439, - -5465, -5492, -5519, -5546, -5573, -5599, -5626, -5653, -5680, -5707, -5733, -5760, - -5787, -5814, -5841, -5867, -5894, -5921, -5948, -5975, -6001, -6028, -6055, -6082, - -6109, -6135, -6162, -6189, -6216, -6243, -6269, -6296, -6323, -6350, -6376, -6403, - -6430, -6457, -6484, -6510, -6537, -6564, -6591, -6618, -6644, -6671, -6698, -6725, - -6752, -6778, -6805, -6832}; - -static INT16 Cr_B[] = { - 0, -4, -9, -15, -20, -25, -30, -35, -41, -46, -51, -56, - -61, -67, -72, -77, -82, -87, -93, -98, -103, -108, -113, -119, - -124, -129, -134, -140, -145, -150, -155, -160, -166, -171, -176, -181, - -186, -192, -197, -202, -207, -212, -218, -223, -228, -233, -238, -244, - -249, -254, -259, -264, -270, -275, -280, -285, -290, -296, -301, -306, - -311, -316, -322, -327, -332, -337, -342, -348, -353, -358, -363, -368, - -374, -379, -384, -389, -394, -400, -405, -410, -415, -421, -426, -431, - -436, -441, -447, -452, -457, -462, -467, -473, -478, -483, -488, -493, - -499, -504, -509, -514, -519, -525, -530, -535, -540, -545, -551, -556, - -561, -566, -571, -577, -582, -587, -592, -597, -603, -608, -613, -618, - -623, -629, -634, -639, -644, -649, -655, -660, -665, -670, -675, -681, - -686, -691, -696, -702, -707, -712, -717, -722, -728, -733, -738, -743, - -748, -754, -759, -764, -769, -774, -780, -785, -790, -795, -800, -806, - -811, -816, -821, -826, -832, -837, -842, -847, -852, -858, -863, -868, - -873, -878, -884, -889, -894, -899, -904, -910, -915, -920, -925, -930, - -936, -941, -946, -951, -957, -962, -967, -972, -977, -983, -988, -993, - -998, -1003, -1009, -1014, -1019, -1024, -1029, -1035, -1040, -1045, -1050, -1055, - -1061, -1066, -1071, -1076, -1081, -1087, -1092, -1097, -1102, -1107, -1113, -1118, - -1123, -1128, -1133, -1139, -1144, -1149, -1154, -1159, -1165, -1170, -1175, -1180, - -1185, -1191, -1196, -1201, -1206, -1211, -1217, -1222, -1227, -1232, -1238, -1243, - -1248, -1253, -1258, -1264, -1269, -1274, -1279, -1284, -1290, -1295, -1300, -1305, - -1310, -1316, -1321, -1326}; - -static INT16 R_Cr[] = { - -11484, -11394, -11305, -11215, -11125, -11036, -10946, -10856, -10766, -10677, - -10587, -10497, -10407, -10318, -10228, -10138, -10049, -9959, -9869, -9779, - -9690, -9600, -9510, -9420, -9331, -9241, -9151, -9062, -8972, -8882, - -8792, -8703, -8613, -8523, -8433, -8344, -8254, -8164, -8075, -7985, - -7895, -7805, -7716, -7626, -7536, -7446, -7357, -7267, -7177, -7088, - -6998, -6908, -6818, -6729, -6639, -6549, -6459, -6370, -6280, -6190, - -6101, -6011, -5921, -5831, -5742, -5652, -5562, -5472, -5383, -5293, - -5203, -5113, -5024, -4934, -4844, -4755, -4665, -4575, -4485, -4396, - -4306, -4216, -4126, -4037, -3947, -3857, -3768, -3678, -3588, -3498, - -3409, -3319, -3229, -3139, -3050, -2960, -2870, -2781, -2691, -2601, - -2511, -2422, -2332, -2242, -2152, -2063, -1973, -1883, -1794, -1704, - -1614, -1524, -1435, -1345, -1255, -1165, -1076, -986, -896, -807, - -717, -627, -537, -448, -358, -268, -178, -89, 0, 90, - 179, 269, 359, 449, 538, 628, 718, 808, 897, 987, - 1077, 1166, 1256, 1346, 1436, 1525, 1615, 1705, 1795, 1884, - 1974, 2064, 2153, 2243, 2333, 2423, 2512, 2602, 2692, 2782, - 2871, 2961, 3051, 3140, 3230, 3320, 3410, 3499, 3589, 3679, - 3769, 3858, 3948, 4038, 4127, 4217, 4307, 4397, 4486, 4576, - 4666, 4756, 4845, 4935, 5025, 5114, 5204, 5294, 5384, 5473, - 5563, 5653, 5743, 5832, 5922, 6012, 6102, 6191, 6281, 6371, - 6460, 6550, 6640, 6730, 6819, 6909, 6999, 7089, 7178, 7268, - 7358, 7447, 7537, 7627, 7717, 7806, 7896, 7986, 8076, 8165, - 8255, 8345, 8434, 8524, 8614, 8704, 8793, 8883, 8973, 9063, - 9152, 9242, 9332, 9421, 9511, 9601, 9691, 9780, 9870, 9960, - 10050, 10139, 10229, 10319, 10408, 10498, 10588, 10678, 10767, 10857, - 10947, 11037, 11126, 11216, 11306, 11395}; - -static INT16 G_Cb[] = { - 2819, 2797, 2775, 2753, 2731, 2709, 2687, 2665, 2643, 2621, 2599, 2577, - 2555, 2533, 2511, 2489, 2467, 2445, 2423, 2401, 2379, 2357, 2335, 2313, - 2291, 2269, 2247, 2225, 2202, 2180, 2158, 2136, 2114, 2092, 2070, 2048, - 2026, 2004, 1982, 1960, 1938, 1916, 1894, 1872, 1850, 1828, 1806, 1784, - 1762, 1740, 1718, 1696, 1674, 1652, 1630, 1608, 1586, 1564, 1542, 1520, - 1498, 1476, 1454, 1432, 1410, 1388, 1366, 1344, 1321, 1299, 1277, 1255, - 1233, 1211, 1189, 1167, 1145, 1123, 1101, 1079, 1057, 1035, 1013, 991, - 969, 947, 925, 903, 881, 859, 837, 815, 793, 771, 749, 727, - 705, 683, 661, 639, 617, 595, 573, 551, 529, 507, 485, 463, - 440, 418, 396, 374, 352, 330, 308, 286, 264, 242, 220, 198, - 176, 154, 132, 110, 88, 66, 44, 22, 0, -21, -43, -65, - -87, -109, -131, -153, -175, -197, -219, -241, -263, -285, -307, -329, - -351, -373, -395, -417, -439, -462, -484, -506, -528, -550, -572, -594, - -616, -638, -660, -682, -704, -726, -748, -770, -792, -814, -836, -858, - -880, -902, -924, -946, -968, -990, -1012, -1034, -1056, -1078, -1100, -1122, - -1144, -1166, -1188, -1210, -1232, -1254, -1276, -1298, -1320, -1343, -1365, -1387, - -1409, -1431, -1453, -1475, -1497, -1519, -1541, -1563, -1585, -1607, -1629, -1651, - -1673, -1695, -1717, -1739, -1761, -1783, -1805, -1827, -1849, -1871, -1893, -1915, - -1937, -1959, -1981, -2003, -2025, -2047, -2069, -2091, -2113, -2135, -2157, -2179, - -2201, -2224, -2246, -2268, -2290, -2312, -2334, -2356, -2378, -2400, -2422, -2444, - -2466, -2488, -2510, -2532, -2554, -2576, -2598, -2620, -2642, -2664, -2686, -2708, - -2730, -2752, -2774, -2796}; - -static INT16 G_Cr[] = { - 5850, 5805, 5759, 5713, 5667, 5622, 5576, 5530, 5485, 5439, 5393, 5347, - 5302, 5256, 5210, 5165, 5119, 5073, 5028, 4982, 4936, 4890, 4845, 4799, - 4753, 4708, 4662, 4616, 4570, 4525, 4479, 4433, 4388, 4342, 4296, 4251, - 4205, 4159, 4113, 4068, 4022, 3976, 3931, 3885, 3839, 3794, 3748, 3702, - 3656, 3611, 3565, 3519, 3474, 3428, 3382, 3336, 3291, 3245, 3199, 3154, - 3108, 3062, 3017, 2971, 2925, 2879, 2834, 2788, 2742, 2697, 2651, 2605, - 2559, 2514, 2468, 2422, 2377, 2331, 2285, 2240, 2194, 2148, 2102, 2057, - 2011, 1965, 1920, 1874, 1828, 1782, 1737, 1691, 1645, 1600, 1554, 1508, - 1463, 1417, 1371, 1325, 1280, 1234, 1188, 1143, 1097, 1051, 1006, 960, - 914, 868, 823, 777, 731, 686, 640, 594, 548, 503, 457, 411, - 366, 320, 274, 229, 183, 137, 91, 46, 0, -45, -90, -136, - -182, -228, -273, -319, -365, -410, -456, -502, -547, -593, -639, -685, - -730, -776, -822, -867, -913, -959, -1005, -1050, -1096, -1142, -1187, -1233, - -1279, -1324, -1370, -1416, -1462, -1507, -1553, -1599, -1644, -1690, -1736, -1781, - -1827, -1873, -1919, -1964, -2010, -2056, -2101, -2147, -2193, -2239, -2284, -2330, - -2376, -2421, -2467, -2513, -2558, -2604, -2650, -2696, -2741, -2787, -2833, -2878, - -2924, -2970, -3016, -3061, -3107, -3153, -3198, -3244, -3290, -3335, -3381, -3427, - -3473, -3518, -3564, -3610, -3655, -3701, -3747, -3793, -3838, -3884, -3930, -3975, - -4021, -4067, -4112, -4158, -4204, -4250, -4295, -4341, -4387, -4432, -4478, -4524, - -4569, -4615, -4661, -4707, -4752, -4798, -4844, -4889, -4935, -4981, -5027, -5072, - -5118, -5164, -5209, -5255, -5301, -5346, -5392, -5438, -5484, -5529, -5575, -5621, - -5666, -5712, -5758, -5804}; - -static INT16 B_Cb[] = { - -14515, -14402, -14288, -14175, -14062, -13948, -13835, -13721, -13608, -13495, - -13381, -13268, -13154, -13041, -12928, -12814, -12701, -12587, -12474, -12360, - -12247, -12134, -12020, -11907, -11793, -11680, -11567, -11453, -11340, -11226, - -11113, -11000, -10886, -10773, -10659, -10546, -10433, -10319, -10206, -10092, - -9979, -9865, -9752, -9639, -9525, -9412, -9298, -9185, -9072, -8958, - -8845, -8731, -8618, -8505, -8391, -8278, -8164, -8051, -7938, -7824, - -7711, -7597, -7484, -7371, -7257, -7144, -7030, -6917, -6803, -6690, - -6577, -6463, -6350, -6236, -6123, -6010, -5896, -5783, -5669, -5556, - -5443, -5329, -5216, -5102, -4989, -4876, -4762, -4649, -4535, -4422, - -4309, -4195, -4082, -3968, -3855, -3741, -3628, -3515, -3401, -3288, - -3174, -3061, -2948, -2834, -2721, -2607, -2494, -2381, -2267, -2154, - -2040, -1927, -1814, -1700, -1587, -1473, -1360, -1246, -1133, -1020, - -906, -793, -679, -566, -453, -339, -226, -112, 0, 113, - 227, 340, 454, 567, 680, 794, 907, 1021, 1134, 1247, - 1361, 1474, 1588, 1701, 1815, 1928, 2041, 2155, 2268, 2382, - 2495, 2608, 2722, 2835, 2949, 3062, 3175, 3289, 3402, 3516, - 3629, 3742, 3856, 3969, 4083, 4196, 4310, 4423, 4536, 4650, - 4763, 4877, 4990, 5103, 5217, 5330, 5444, 5557, 5670, 5784, - 5897, 6011, 6124, 6237, 6351, 6464, 6578, 6691, 6804, 6918, - 7031, 7145, 7258, 7372, 7485, 7598, 7712, 7825, 7939, 8052, - 8165, 8279, 8392, 8506, 8619, 8732, 8846, 8959, 9073, 9186, - 9299, 9413, 9526, 9640, 9753, 9866, 9980, 10093, 10207, 10320, - 10434, 10547, 10660, 10774, 10887, 11001, 11114, 11227, 11341, 11454, - 11568, 11681, 11794, 11908, 12021, 12135, 12248, 12361, 12475, 12588, - 12702, 12815, 12929, 13042, 13155, 13269, 13382, 13496, 13609, 13722, - 13836, 13949, 14063, 14176, 14289, 14403}; - -void -ImagingConvertRGB2YCbCr(UINT8 *out, const UINT8 *in, int pixels) { - int x; - UINT8 a; - int r, g, b; - int y, cr, cb; - - for (x = 0; x < pixels; x++, in += 4, out += 4) { - r = in[0]; - g = in[1]; - b = in[2]; - a = in[3]; - - y = (Y_R[r] + Y_G[g] + Y_B[b]) >> SCALE; - cb = ((Cb_R[r] + Cb_G[g] + Cb_B[b]) >> SCALE) + 128; - cr = ((Cr_R[r] + Cr_G[g] + Cr_B[b]) >> SCALE) + 128; - - out[0] = (UINT8)y; - out[1] = (UINT8)cb; - out[2] = (UINT8)cr; - out[3] = a; - } -} - -void -ImagingConvertYCbCr2RGB(UINT8 *out, const UINT8 *in, int pixels) { - int x; - UINT8 a; - int r, g, b; - int y, cr, cb; - - for (x = 0; x < pixels; x++, in += 4, out += 4) { - y = in[0]; - cb = in[1]; - cr = in[2]; - a = in[3]; - - r = y + ((R_Cr[cr]) >> SCALE); - g = y + ((G_Cb[cb] + G_Cr[cr]) >> SCALE); - b = y + ((B_Cb[cb]) >> SCALE); - - out[0] = (r <= 0) ? 0 : (r >= 255) ? 255 : r; - out[1] = (g <= 0) ? 0 : (g >= 255) ? 255 : g; - out[2] = (b <= 0) ? 0 : (b >= 255) ? 255 : b; - out[3] = a; - } -} +/* + * The Python Imaging Library. + * $Id$ + * + * code to convert YCbCr data + * + * history: + * 98-07-01 hk Created + * + * Copyright (c) Secret Labs AB 1998 + * + * See the README file for information on usage and redistribution. + */ + +#include "Imaging.h" + +/* JPEG/JFIF YCbCr conversions + + Y = R * 0.29900 + G * 0.58700 + B * 0.11400 + Cb = R * -0.16874 + G * -0.33126 + B * 0.50000 + 128 + Cr = R * 0.50000 + G * -0.41869 + B * -0.08131 + 128 + + R = Y + + (Cr - 128) * 1.40200 + G = Y + (Cb - 128) * -0.34414 + (Cr - 128) * -0.71414 + B = Y + (Cb - 128) * 1.77200 + +*/ + +#define SCALE 6 /* bits */ + +static INT16 Y_R[] = { + 0, 19, 38, 57, 77, 96, 115, 134, 153, 172, 191, 210, 230, 249, + 268, 287, 306, 325, 344, 364, 383, 402, 421, 440, 459, 478, 498, 517, + 536, 555, 574, 593, 612, 631, 651, 670, 689, 708, 727, 746, 765, 785, + 804, 823, 842, 861, 880, 899, 919, 938, 957, 976, 995, 1014, 1033, 1052, + 1072, 1091, 1110, 1129, 1148, 1167, 1186, 1206, 1225, 1244, 1263, 1282, 1301, 1320, + 1340, 1359, 1378, 1397, 1416, 1435, 1454, 1473, 1493, 1512, 1531, 1550, 1569, 1588, + 1607, 1627, 1646, 1665, 1684, 1703, 1722, 1741, 1761, 1780, 1799, 1818, 1837, 1856, + 1875, 1894, 1914, 1933, 1952, 1971, 1990, 2009, 2028, 2048, 2067, 2086, 2105, 2124, + 2143, 2162, 2182, 2201, 2220, 2239, 2258, 2277, 2296, 2315, 2335, 2354, 2373, 2392, + 2411, 2430, 2449, 2469, 2488, 2507, 2526, 2545, 2564, 2583, 2602, 2622, 2641, 2660, + 2679, 2698, 2717, 2736, 2756, 2775, 2794, 2813, 2832, 2851, 2870, 2890, 2909, 2928, + 2947, 2966, 2985, 3004, 3023, 3043, 3062, 3081, 3100, 3119, 3138, 3157, 3177, 3196, + 3215, 3234, 3253, 3272, 3291, 3311, 3330, 3349, 3368, 3387, 3406, 3425, 3444, 3464, + 3483, 3502, 3521, 3540, 3559, 3578, 3598, 3617, 3636, 3655, 3674, 3693, 3712, 3732, + 3751, 3770, 3789, 3808, 3827, 3846, 3865, 3885, 3904, 3923, 3942, 3961, 3980, 3999, + 4019, 4038, 4057, 4076, 4095, 4114, 4133, 4153, 4172, 4191, 4210, 4229, 4248, 4267, + 4286, 4306, 4325, 4344, 4363, 4382, 4401, 4420, 4440, 4459, 4478, 4497, 4516, 4535, + 4554, 4574, 4593, 4612, 4631, 4650, 4669, 4688, 4707, 4727, 4746, 4765, 4784, 4803, + 4822, 4841, 4861, 4880}; + +static INT16 Y_G[] = { + 0, 38, 75, 113, 150, 188, 225, 263, 301, 338, 376, 413, 451, 488, + 526, 564, 601, 639, 676, 714, 751, 789, 826, 864, 902, 939, 977, 1014, + 1052, 1089, 1127, 1165, 1202, 1240, 1277, 1315, 1352, 1390, 1428, 1465, 1503, 1540, + 1578, 1615, 1653, 1691, 1728, 1766, 1803, 1841, 1878, 1916, 1954, 1991, 2029, 2066, + 2104, 2141, 2179, 2217, 2254, 2292, 2329, 2367, 2404, 2442, 2479, 2517, 2555, 2592, + 2630, 2667, 2705, 2742, 2780, 2818, 2855, 2893, 2930, 2968, 3005, 3043, 3081, 3118, + 3156, 3193, 3231, 3268, 3306, 3344, 3381, 3419, 3456, 3494, 3531, 3569, 3607, 3644, + 3682, 3719, 3757, 3794, 3832, 3870, 3907, 3945, 3982, 4020, 4057, 4095, 4132, 4170, + 4208, 4245, 4283, 4320, 4358, 4395, 4433, 4471, 4508, 4546, 4583, 4621, 4658, 4696, + 4734, 4771, 4809, 4846, 4884, 4921, 4959, 4997, 5034, 5072, 5109, 5147, 5184, 5222, + 5260, 5297, 5335, 5372, 5410, 5447, 5485, 5522, 5560, 5598, 5635, 5673, 5710, 5748, + 5785, 5823, 5861, 5898, 5936, 5973, 6011, 6048, 6086, 6124, 6161, 6199, 6236, 6274, + 6311, 6349, 6387, 6424, 6462, 6499, 6537, 6574, 6612, 6650, 6687, 6725, 6762, 6800, + 6837, 6875, 6913, 6950, 6988, 7025, 7063, 7100, 7138, 7175, 7213, 7251, 7288, 7326, + 7363, 7401, 7438, 7476, 7514, 7551, 7589, 7626, 7664, 7701, 7739, 7777, 7814, 7852, + 7889, 7927, 7964, 8002, 8040, 8077, 8115, 8152, 8190, 8227, 8265, 8303, 8340, 8378, + 8415, 8453, 8490, 8528, 8566, 8603, 8641, 8678, 8716, 8753, 8791, 8828, 8866, 8904, + 8941, 8979, 9016, 9054, 9091, 9129, 9167, 9204, 9242, 9279, 9317, 9354, 9392, 9430, + 9467, 9505, 9542, 9580}; + +static INT16 Y_B[] = { + 0, 7, 15, 22, 29, 36, 44, 51, 58, 66, 73, 80, 88, 95, + 102, 109, 117, 124, 131, 139, 146, 153, 161, 168, 175, 182, 190, 197, + 204, 212, 219, 226, 233, 241, 248, 255, 263, 270, 277, 285, 292, 299, + 306, 314, 321, 328, 336, 343, 350, 358, 365, 372, 379, 387, 394, 401, + 409, 416, 423, 430, 438, 445, 452, 460, 467, 474, 482, 489, 496, 503, + 511, 518, 525, 533, 540, 547, 554, 562, 569, 576, 584, 591, 598, 606, + 613, 620, 627, 635, 642, 649, 657, 664, 671, 679, 686, 693, 700, 708, + 715, 722, 730, 737, 744, 751, 759, 766, 773, 781, 788, 795, 803, 810, + 817, 824, 832, 839, 846, 854, 861, 868, 876, 883, 890, 897, 905, 912, + 919, 927, 934, 941, 948, 956, 963, 970, 978, 985, 992, 1000, 1007, 1014, + 1021, 1029, 1036, 1043, 1051, 1058, 1065, 1073, 1080, 1087, 1094, 1102, 1109, 1116, + 1124, 1131, 1138, 1145, 1153, 1160, 1167, 1175, 1182, 1189, 1197, 1204, 1211, 1218, + 1226, 1233, 1240, 1248, 1255, 1262, 1270, 1277, 1284, 1291, 1299, 1306, 1313, 1321, + 1328, 1335, 1342, 1350, 1357, 1364, 1372, 1379, 1386, 1394, 1401, 1408, 1415, 1423, + 1430, 1437, 1445, 1452, 1459, 1466, 1474, 1481, 1488, 1496, 1503, 1510, 1518, 1525, + 1532, 1539, 1547, 1554, 1561, 1569, 1576, 1583, 1591, 1598, 1605, 1612, 1620, 1627, + 1634, 1642, 1649, 1656, 1663, 1671, 1678, 1685, 1693, 1700, 1707, 1715, 1722, 1729, + 1736, 1744, 1751, 1758, 1766, 1773, 1780, 1788, 1795, 1802, 1809, 1817, 1824, 1831, + 1839, 1846, 1853, 1860}; + +static INT16 Cb_R[] = { + 0, -10, -21, -31, -42, -53, -64, -75, -85, -96, -107, -118, + -129, -139, -150, -161, -172, -183, -193, -204, -215, -226, -237, -247, + -258, -269, -280, -291, -301, -312, -323, -334, -345, -355, -366, -377, + -388, -399, -409, -420, -431, -442, -453, -463, -474, -485, -496, -507, + -517, -528, -539, -550, -561, -571, -582, -593, -604, -615, -625, -636, + -647, -658, -669, -679, -690, -701, -712, -723, -733, -744, -755, -766, + -777, -787, -798, -809, -820, -831, -841, -852, -863, -874, -885, -895, + -906, -917, -928, -939, -949, -960, -971, -982, -993, -1003, -1014, -1025, + -1036, -1047, -1057, -1068, -1079, -1090, -1101, -1111, -1122, -1133, -1144, -1155, + -1165, -1176, -1187, -1198, -1209, -1219, -1230, -1241, -1252, -1263, -1273, -1284, + -1295, -1306, -1317, -1327, -1338, -1349, -1360, -1371, -1381, -1392, -1403, -1414, + -1425, -1435, -1446, -1457, -1468, -1479, -1489, -1500, -1511, -1522, -1533, -1543, + -1554, -1565, -1576, -1587, -1597, -1608, -1619, -1630, -1641, -1651, -1662, -1673, + -1684, -1694, -1705, -1716, -1727, -1738, -1748, -1759, -1770, -1781, -1792, -1802, + -1813, -1824, -1835, -1846, -1856, -1867, -1878, -1889, -1900, -1910, -1921, -1932, + -1943, -1954, -1964, -1975, -1986, -1997, -2008, -2018, -2029, -2040, -2051, -2062, + -2072, -2083, -2094, -2105, -2116, -2126, -2137, -2148, -2159, -2170, -2180, -2191, + -2202, -2213, -2224, -2234, -2245, -2256, -2267, -2278, -2288, -2299, -2310, -2321, + -2332, -2342, -2353, -2364, -2375, -2386, -2396, -2407, -2418, -2429, -2440, -2450, + -2461, -2472, -2483, -2494, -2504, -2515, -2526, -2537, -2548, -2558, -2569, -2580, + -2591, -2602, -2612, -2623, -2634, -2645, -2656, -2666, -2677, -2688, -2699, -2710, + -2720, -2731, -2742, -2753}; + +static INT16 Cb_G[] = { + 0, -20, -41, -63, -84, -105, -126, -147, -169, -190, -211, -232, + -253, -275, -296, -317, -338, -359, -381, -402, -423, -444, -465, -487, + -508, -529, -550, -571, -593, -614, -635, -656, -677, -699, -720, -741, + -762, -783, -805, -826, -847, -868, -889, -911, -932, -953, -974, -995, + -1017, -1038, -1059, -1080, -1101, -1123, -1144, -1165, -1186, -1207, -1229, -1250, + -1271, -1292, -1313, -1335, -1356, -1377, -1398, -1419, -1441, -1462, -1483, -1504, + -1525, -1547, -1568, -1589, -1610, -1631, -1653, -1674, -1695, -1716, -1737, -1759, + -1780, -1801, -1822, -1843, -1865, -1886, -1907, -1928, -1949, -1971, -1992, -2013, + -2034, -2055, -2077, -2098, -2119, -2140, -2161, -2183, -2204, -2225, -2246, -2267, + -2289, -2310, -2331, -2352, -2373, -2395, -2416, -2437, -2458, -2479, -2501, -2522, + -2543, -2564, -2585, -2607, -2628, -2649, -2670, -2691, -2713, -2734, -2755, -2776, + -2797, -2819, -2840, -2861, -2882, -2903, -2925, -2946, -2967, -2988, -3009, -3031, + -3052, -3073, -3094, -3115, -3137, -3158, -3179, -3200, -3221, -3243, -3264, -3285, + -3306, -3328, -3349, -3370, -3391, -3412, -3434, -3455, -3476, -3497, -3518, -3540, + -3561, -3582, -3603, -3624, -3646, -3667, -3688, -3709, -3730, -3752, -3773, -3794, + -3815, -3836, -3858, -3879, -3900, -3921, -3942, -3964, -3985, -4006, -4027, -4048, + -4070, -4091, -4112, -4133, -4154, -4176, -4197, -4218, -4239, -4260, -4282, -4303, + -4324, -4345, -4366, -4388, -4409, -4430, -4451, -4472, -4494, -4515, -4536, -4557, + -4578, -4600, -4621, -4642, -4663, -4684, -4706, -4727, -4748, -4769, -4790, -4812, + -4833, -4854, -4875, -4896, -4918, -4939, -4960, -4981, -5002, -5024, -5045, -5066, + -5087, -5108, -5130, -5151, -5172, -5193, -5214, -5236, -5257, -5278, -5299, -5320, + -5342, -5363, -5384, -5405}; + +static INT16 Cb_B[] = { + 0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, + 448, 480, 512, 544, 576, 608, 640, 672, 704, 736, 768, 800, 832, 864, + 896, 928, 960, 992, 1024, 1056, 1088, 1120, 1152, 1184, 1216, 1248, 1280, 1312, + 1344, 1376, 1408, 1440, 1472, 1504, 1536, 1568, 1600, 1632, 1664, 1696, 1728, 1760, + 1792, 1824, 1856, 1888, 1920, 1952, 1984, 2016, 2048, 2080, 2112, 2144, 2176, 2208, + 2240, 2272, 2304, 2336, 2368, 2400, 2432, 2464, 2496, 2528, 2560, 2592, 2624, 2656, + 2688, 2720, 2752, 2784, 2816, 2848, 2880, 2912, 2944, 2976, 3008, 3040, 3072, 3104, + 3136, 3168, 3200, 3232, 3264, 3296, 3328, 3360, 3392, 3424, 3456, 3488, 3520, 3552, + 3584, 3616, 3648, 3680, 3712, 3744, 3776, 3808, 3840, 3872, 3904, 3936, 3968, 4000, + 4032, 4064, 4096, 4128, 4160, 4192, 4224, 4256, 4288, 4320, 4352, 4384, 4416, 4448, + 4480, 4512, 4544, 4576, 4608, 4640, 4672, 4704, 4736, 4768, 4800, 4832, 4864, 4896, + 4928, 4960, 4992, 5024, 5056, 5088, 5120, 5152, 5184, 5216, 5248, 5280, 5312, 5344, + 5376, 5408, 5440, 5472, 5504, 5536, 5568, 5600, 5632, 5664, 5696, 5728, 5760, 5792, + 5824, 5856, 5888, 5920, 5952, 5984, 6016, 6048, 6080, 6112, 6144, 6176, 6208, 6240, + 6272, 6304, 6336, 6368, 6400, 6432, 6464, 6496, 6528, 6560, 6592, 6624, 6656, 6688, + 6720, 6752, 6784, 6816, 6848, 6880, 6912, 6944, 6976, 7008, 7040, 7072, 7104, 7136, + 7168, 7200, 7232, 7264, 7296, 7328, 7360, 7392, 7424, 7456, 7488, 7520, 7552, 7584, + 7616, 7648, 7680, 7712, 7744, 7776, 7808, 7840, 7872, 7904, 7936, 7968, 8000, 8032, + 8064, 8096, 8128, 8160}; + +#define Cr_R Cb_B + +static INT16 Cr_G[] = { + 0, -26, -53, -79, -106, -133, -160, -187, -213, -240, -267, -294, + -321, -347, -374, -401, -428, -455, -481, -508, -535, -562, -589, -615, + -642, -669, -696, -722, -749, -776, -803, -830, -856, -883, -910, -937, + -964, -990, -1017, -1044, -1071, -1098, -1124, -1151, -1178, -1205, -1232, -1258, + -1285, -1312, -1339, -1366, -1392, -1419, -1446, -1473, -1500, -1526, -1553, -1580, + -1607, -1634, -1660, -1687, -1714, -1741, -1768, -1794, -1821, -1848, -1875, -1902, + -1928, -1955, -1982, -2009, -2036, -2062, -2089, -2116, -2143, -2169, -2196, -2223, + -2250, -2277, -2303, -2330, -2357, -2384, -2411, -2437, -2464, -2491, -2518, -2545, + -2571, -2598, -2625, -2652, -2679, -2705, -2732, -2759, -2786, -2813, -2839, -2866, + -2893, -2920, -2947, -2973, -3000, -3027, -3054, -3081, -3107, -3134, -3161, -3188, + -3215, -3241, -3268, -3295, -3322, -3349, -3375, -3402, -3429, -3456, -3483, -3509, + -3536, -3563, -3590, -3616, -3643, -3670, -3697, -3724, -3750, -3777, -3804, -3831, + -3858, -3884, -3911, -3938, -3965, -3992, -4018, -4045, -4072, -4099, -4126, -4152, + -4179, -4206, -4233, -4260, -4286, -4313, -4340, -4367, -4394, -4420, -4447, -4474, + -4501, -4528, -4554, -4581, -4608, -4635, -4662, -4688, -4715, -4742, -4769, -4796, + -4822, -4849, -4876, -4903, -4929, -4956, -4983, -5010, -5037, -5063, -5090, -5117, + -5144, -5171, -5197, -5224, -5251, -5278, -5305, -5331, -5358, -5385, -5412, -5439, + -5465, -5492, -5519, -5546, -5573, -5599, -5626, -5653, -5680, -5707, -5733, -5760, + -5787, -5814, -5841, -5867, -5894, -5921, -5948, -5975, -6001, -6028, -6055, -6082, + -6109, -6135, -6162, -6189, -6216, -6243, -6269, -6296, -6323, -6350, -6376, -6403, + -6430, -6457, -6484, -6510, -6537, -6564, -6591, -6618, -6644, -6671, -6698, -6725, + -6752, -6778, -6805, -6832}; + +static INT16 Cr_B[] = { + 0, -4, -9, -15, -20, -25, -30, -35, -41, -46, -51, -56, + -61, -67, -72, -77, -82, -87, -93, -98, -103, -108, -113, -119, + -124, -129, -134, -140, -145, -150, -155, -160, -166, -171, -176, -181, + -186, -192, -197, -202, -207, -212, -218, -223, -228, -233, -238, -244, + -249, -254, -259, -264, -270, -275, -280, -285, -290, -296, -301, -306, + -311, -316, -322, -327, -332, -337, -342, -348, -353, -358, -363, -368, + -374, -379, -384, -389, -394, -400, -405, -410, -415, -421, -426, -431, + -436, -441, -447, -452, -457, -462, -467, -473, -478, -483, -488, -493, + -499, -504, -509, -514, -519, -525, -530, -535, -540, -545, -551, -556, + -561, -566, -571, -577, -582, -587, -592, -597, -603, -608, -613, -618, + -623, -629, -634, -639, -644, -649, -655, -660, -665, -670, -675, -681, + -686, -691, -696, -702, -707, -712, -717, -722, -728, -733, -738, -743, + -748, -754, -759, -764, -769, -774, -780, -785, -790, -795, -800, -806, + -811, -816, -821, -826, -832, -837, -842, -847, -852, -858, -863, -868, + -873, -878, -884, -889, -894, -899, -904, -910, -915, -920, -925, -930, + -936, -941, -946, -951, -957, -962, -967, -972, -977, -983, -988, -993, + -998, -1003, -1009, -1014, -1019, -1024, -1029, -1035, -1040, -1045, -1050, -1055, + -1061, -1066, -1071, -1076, -1081, -1087, -1092, -1097, -1102, -1107, -1113, -1118, + -1123, -1128, -1133, -1139, -1144, -1149, -1154, -1159, -1165, -1170, -1175, -1180, + -1185, -1191, -1196, -1201, -1206, -1211, -1217, -1222, -1227, -1232, -1238, -1243, + -1248, -1253, -1258, -1264, -1269, -1274, -1279, -1284, -1290, -1295, -1300, -1305, + -1310, -1316, -1321, -1326}; + +static INT16 R_Cr[] = { + -11484, -11394, -11305, -11215, -11125, -11036, -10946, -10856, -10766, -10677, + -10587, -10497, -10407, -10318, -10228, -10138, -10049, -9959, -9869, -9779, + -9690, -9600, -9510, -9420, -9331, -9241, -9151, -9062, -8972, -8882, + -8792, -8703, -8613, -8523, -8433, -8344, -8254, -8164, -8075, -7985, + -7895, -7805, -7716, -7626, -7536, -7446, -7357, -7267, -7177, -7088, + -6998, -6908, -6818, -6729, -6639, -6549, -6459, -6370, -6280, -6190, + -6101, -6011, -5921, -5831, -5742, -5652, -5562, -5472, -5383, -5293, + -5203, -5113, -5024, -4934, -4844, -4755, -4665, -4575, -4485, -4396, + -4306, -4216, -4126, -4037, -3947, -3857, -3768, -3678, -3588, -3498, + -3409, -3319, -3229, -3139, -3050, -2960, -2870, -2781, -2691, -2601, + -2511, -2422, -2332, -2242, -2152, -2063, -1973, -1883, -1794, -1704, + -1614, -1524, -1435, -1345, -1255, -1165, -1076, -986, -896, -807, + -717, -627, -537, -448, -358, -268, -178, -89, 0, 90, + 179, 269, 359, 449, 538, 628, 718, 808, 897, 987, + 1077, 1166, 1256, 1346, 1436, 1525, 1615, 1705, 1795, 1884, + 1974, 2064, 2153, 2243, 2333, 2423, 2512, 2602, 2692, 2782, + 2871, 2961, 3051, 3140, 3230, 3320, 3410, 3499, 3589, 3679, + 3769, 3858, 3948, 4038, 4127, 4217, 4307, 4397, 4486, 4576, + 4666, 4756, 4845, 4935, 5025, 5114, 5204, 5294, 5384, 5473, + 5563, 5653, 5743, 5832, 5922, 6012, 6102, 6191, 6281, 6371, + 6460, 6550, 6640, 6730, 6819, 6909, 6999, 7089, 7178, 7268, + 7358, 7447, 7537, 7627, 7717, 7806, 7896, 7986, 8076, 8165, + 8255, 8345, 8434, 8524, 8614, 8704, 8793, 8883, 8973, 9063, + 9152, 9242, 9332, 9421, 9511, 9601, 9691, 9780, 9870, 9960, + 10050, 10139, 10229, 10319, 10408, 10498, 10588, 10678, 10767, 10857, + 10947, 11037, 11126, 11216, 11306, 11395}; + +static INT16 G_Cb[] = { + 2819, 2797, 2775, 2753, 2731, 2709, 2687, 2665, 2643, 2621, 2599, 2577, + 2555, 2533, 2511, 2489, 2467, 2445, 2423, 2401, 2379, 2357, 2335, 2313, + 2291, 2269, 2247, 2225, 2202, 2180, 2158, 2136, 2114, 2092, 2070, 2048, + 2026, 2004, 1982, 1960, 1938, 1916, 1894, 1872, 1850, 1828, 1806, 1784, + 1762, 1740, 1718, 1696, 1674, 1652, 1630, 1608, 1586, 1564, 1542, 1520, + 1498, 1476, 1454, 1432, 1410, 1388, 1366, 1344, 1321, 1299, 1277, 1255, + 1233, 1211, 1189, 1167, 1145, 1123, 1101, 1079, 1057, 1035, 1013, 991, + 969, 947, 925, 903, 881, 859, 837, 815, 793, 771, 749, 727, + 705, 683, 661, 639, 617, 595, 573, 551, 529, 507, 485, 463, + 440, 418, 396, 374, 352, 330, 308, 286, 264, 242, 220, 198, + 176, 154, 132, 110, 88, 66, 44, 22, 0, -21, -43, -65, + -87, -109, -131, -153, -175, -197, -219, -241, -263, -285, -307, -329, + -351, -373, -395, -417, -439, -462, -484, -506, -528, -550, -572, -594, + -616, -638, -660, -682, -704, -726, -748, -770, -792, -814, -836, -858, + -880, -902, -924, -946, -968, -990, -1012, -1034, -1056, -1078, -1100, -1122, + -1144, -1166, -1188, -1210, -1232, -1254, -1276, -1298, -1320, -1343, -1365, -1387, + -1409, -1431, -1453, -1475, -1497, -1519, -1541, -1563, -1585, -1607, -1629, -1651, + -1673, -1695, -1717, -1739, -1761, -1783, -1805, -1827, -1849, -1871, -1893, -1915, + -1937, -1959, -1981, -2003, -2025, -2047, -2069, -2091, -2113, -2135, -2157, -2179, + -2201, -2224, -2246, -2268, -2290, -2312, -2334, -2356, -2378, -2400, -2422, -2444, + -2466, -2488, -2510, -2532, -2554, -2576, -2598, -2620, -2642, -2664, -2686, -2708, + -2730, -2752, -2774, -2796}; + +static INT16 G_Cr[] = { + 5850, 5805, 5759, 5713, 5667, 5622, 5576, 5530, 5485, 5439, 5393, 5347, + 5302, 5256, 5210, 5165, 5119, 5073, 5028, 4982, 4936, 4890, 4845, 4799, + 4753, 4708, 4662, 4616, 4570, 4525, 4479, 4433, 4388, 4342, 4296, 4251, + 4205, 4159, 4113, 4068, 4022, 3976, 3931, 3885, 3839, 3794, 3748, 3702, + 3656, 3611, 3565, 3519, 3474, 3428, 3382, 3336, 3291, 3245, 3199, 3154, + 3108, 3062, 3017, 2971, 2925, 2879, 2834, 2788, 2742, 2697, 2651, 2605, + 2559, 2514, 2468, 2422, 2377, 2331, 2285, 2240, 2194, 2148, 2102, 2057, + 2011, 1965, 1920, 1874, 1828, 1782, 1737, 1691, 1645, 1600, 1554, 1508, + 1463, 1417, 1371, 1325, 1280, 1234, 1188, 1143, 1097, 1051, 1006, 960, + 914, 868, 823, 777, 731, 686, 640, 594, 548, 503, 457, 411, + 366, 320, 274, 229, 183, 137, 91, 46, 0, -45, -90, -136, + -182, -228, -273, -319, -365, -410, -456, -502, -547, -593, -639, -685, + -730, -776, -822, -867, -913, -959, -1005, -1050, -1096, -1142, -1187, -1233, + -1279, -1324, -1370, -1416, -1462, -1507, -1553, -1599, -1644, -1690, -1736, -1781, + -1827, -1873, -1919, -1964, -2010, -2056, -2101, -2147, -2193, -2239, -2284, -2330, + -2376, -2421, -2467, -2513, -2558, -2604, -2650, -2696, -2741, -2787, -2833, -2878, + -2924, -2970, -3016, -3061, -3107, -3153, -3198, -3244, -3290, -3335, -3381, -3427, + -3473, -3518, -3564, -3610, -3655, -3701, -3747, -3793, -3838, -3884, -3930, -3975, + -4021, -4067, -4112, -4158, -4204, -4250, -4295, -4341, -4387, -4432, -4478, -4524, + -4569, -4615, -4661, -4707, -4752, -4798, -4844, -4889, -4935, -4981, -5027, -5072, + -5118, -5164, -5209, -5255, -5301, -5346, -5392, -5438, -5484, -5529, -5575, -5621, + -5666, -5712, -5758, -5804}; + +static INT16 B_Cb[] = { + -14515, -14402, -14288, -14175, -14062, -13948, -13835, -13721, -13608, -13495, + -13381, -13268, -13154, -13041, -12928, -12814, -12701, -12587, -12474, -12360, + -12247, -12134, -12020, -11907, -11793, -11680, -11567, -11453, -11340, -11226, + -11113, -11000, -10886, -10773, -10659, -10546, -10433, -10319, -10206, -10092, + -9979, -9865, -9752, -9639, -9525, -9412, -9298, -9185, -9072, -8958, + -8845, -8731, -8618, -8505, -8391, -8278, -8164, -8051, -7938, -7824, + -7711, -7597, -7484, -7371, -7257, -7144, -7030, -6917, -6803, -6690, + -6577, -6463, -6350, -6236, -6123, -6010, -5896, -5783, -5669, -5556, + -5443, -5329, -5216, -5102, -4989, -4876, -4762, -4649, -4535, -4422, + -4309, -4195, -4082, -3968, -3855, -3741, -3628, -3515, -3401, -3288, + -3174, -3061, -2948, -2834, -2721, -2607, -2494, -2381, -2267, -2154, + -2040, -1927, -1814, -1700, -1587, -1473, -1360, -1246, -1133, -1020, + -906, -793, -679, -566, -453, -339, -226, -112, 0, 113, + 227, 340, 454, 567, 680, 794, 907, 1021, 1134, 1247, + 1361, 1474, 1588, 1701, 1815, 1928, 2041, 2155, 2268, 2382, + 2495, 2608, 2722, 2835, 2949, 3062, 3175, 3289, 3402, 3516, + 3629, 3742, 3856, 3969, 4083, 4196, 4310, 4423, 4536, 4650, + 4763, 4877, 4990, 5103, 5217, 5330, 5444, 5557, 5670, 5784, + 5897, 6011, 6124, 6237, 6351, 6464, 6578, 6691, 6804, 6918, + 7031, 7145, 7258, 7372, 7485, 7598, 7712, 7825, 7939, 8052, + 8165, 8279, 8392, 8506, 8619, 8732, 8846, 8959, 9073, 9186, + 9299, 9413, 9526, 9640, 9753, 9866, 9980, 10093, 10207, 10320, + 10434, 10547, 10660, 10774, 10887, 11001, 11114, 11227, 11341, 11454, + 11568, 11681, 11794, 11908, 12021, 12135, 12248, 12361, 12475, 12588, + 12702, 12815, 12929, 13042, 13155, 13269, 13382, 13496, 13609, 13722, + 13836, 13949, 14063, 14176, 14289, 14403}; + +void +ImagingConvertRGB2YCbCr(UINT8 *out, const UINT8 *in, int pixels) { + int x; + UINT8 a; + int r, g, b; + int y, cr, cb; + + for (x = 0; x < pixels; x++, in += 4, out += 4) { + r = in[0]; + g = in[1]; + b = in[2]; + a = in[3]; + + y = (Y_R[r] + Y_G[g] + Y_B[b]) >> SCALE; + cb = ((Cb_R[r] + Cb_G[g] + Cb_B[b]) >> SCALE) + 128; + cr = ((Cr_R[r] + Cr_G[g] + Cr_B[b]) >> SCALE) + 128; + + out[0] = (UINT8)y; + out[1] = (UINT8)cb; + out[2] = (UINT8)cr; + out[3] = a; + } +} + +void +ImagingConvertYCbCr2RGB(UINT8 *out, const UINT8 *in, int pixels) { + int x; + UINT8 a; + int r, g, b; + int y, cr, cb; + + for (x = 0; x < pixels; x++, in += 4, out += 4) { + y = in[0]; + cb = in[1]; + cr = in[2]; + a = in[3]; + + r = y + ((R_Cr[cr]) >> SCALE); + g = y + ((G_Cb[cb] + G_Cr[cr]) >> SCALE); + b = y + ((B_Cb[cb]) >> SCALE); + + out[0] = (r <= 0) ? 0 : (r >= 255) ? 255 : r; + out[1] = (g <= 0) ? 0 : (g >= 255) ? 255 : g; + out[2] = (b <= 0) ? 0 : (b >= 255) ? 255 : b; + out[3] = a; + } +} diff --git a/src/libImaging/Copy.c b/src/libImaging/Copy.c index 571133e14..805f3a519 100644 --- a/src/libImaging/Copy.c +++ b/src/libImaging/Copy.c @@ -1,57 +1,57 @@ -/* - * The Python Imaging Library - * $Id$ - * - * copy image - * - * history: - * 95-11-26 fl Moved from Imaging.c - * 97-05-12 fl Added ImagingCopy2 - * 97-08-28 fl Allow imOut == NULL in ImagingCopy2 - * - * Copyright (c) Fredrik Lundh 1995-97. - * Copyright (c) Secret Labs AB 1997. - * - * See the README file for details on usage and redistribution. - */ - -#include "Imaging.h" - -static Imaging -_copy(Imaging imOut, Imaging imIn) { - ImagingSectionCookie cookie; - int y; - - if (!imIn) { - return (Imaging)ImagingError_ValueError(NULL); - } - - imOut = ImagingNew2Dirty(imIn->mode, imOut, imIn); - if (!imOut) { - return NULL; - } - - ImagingCopyPalette(imOut, imIn); - - ImagingSectionEnter(&cookie); - if (imIn->block != NULL && imOut->block != NULL) { - memcpy(imOut->block, imIn->block, imIn->ysize * imIn->linesize); - } else { - for (y = 0; y < imIn->ysize; y++) { - memcpy(imOut->image[y], imIn->image[y], imIn->linesize); - } - } - ImagingSectionLeave(&cookie); - - return imOut; -} - -Imaging -ImagingCopy(Imaging imIn) { - return _copy(NULL, imIn); -} - -Imaging -ImagingCopy2(Imaging imOut, Imaging imIn) { - return _copy(imOut, imIn); -} +/* + * The Python Imaging Library + * $Id$ + * + * copy image + * + * history: + * 95-11-26 fl Moved from Imaging.c + * 97-05-12 fl Added ImagingCopy2 + * 97-08-28 fl Allow imOut == NULL in ImagingCopy2 + * + * Copyright (c) Fredrik Lundh 1995-97. + * Copyright (c) Secret Labs AB 1997. + * + * See the README file for details on usage and redistribution. + */ + +#include "Imaging.h" + +static Imaging +_copy(Imaging imOut, Imaging imIn) { + ImagingSectionCookie cookie; + int y; + + if (!imIn) { + return (Imaging)ImagingError_ValueError(NULL); + } + + imOut = ImagingNew2Dirty(imIn->mode, imOut, imIn); + if (!imOut) { + return NULL; + } + + ImagingCopyPalette(imOut, imIn); + + ImagingSectionEnter(&cookie); + if (imIn->block != NULL && imOut->block != NULL) { + memcpy(imOut->block, imIn->block, imIn->ysize * imIn->linesize); + } else { + for (y = 0; y < imIn->ysize; y++) { + memcpy(imOut->image[y], imIn->image[y], imIn->linesize); + } + } + ImagingSectionLeave(&cookie); + + return imOut; +} + +Imaging +ImagingCopy(Imaging imIn) { + return _copy(NULL, imIn); +} + +Imaging +ImagingCopy2(Imaging imOut, Imaging imIn) { + return _copy(imOut, imIn); +} diff --git a/src/libImaging/Crop.c b/src/libImaging/Crop.c index 2425b4cd5..f39f226bb 100644 --- a/src/libImaging/Crop.c +++ b/src/libImaging/Crop.c @@ -1,63 +1,63 @@ -/* - * The Python Imaging Library - * $Id$ - * - * cut region from image - * - * history: - * 95-11-27 fl Created - * 98-07-10 fl Fixed "null result" error - * 99-02-05 fl Rewritten to use Paste primitive - * - * Copyright (c) Secret Labs AB 1997-99. - * Copyright (c) Fredrik Lundh 1995. - * - * See the README file for information on usage and redistribution. - */ - -#include "Imaging.h" - -Imaging -ImagingCrop(Imaging imIn, int sx0, int sy0, int sx1, int sy1) { - Imaging imOut; - int xsize, ysize; - int dx0, dy0, dx1, dy1; - INT32 zero = 0; - - if (!imIn) { - return (Imaging)ImagingError_ModeError(); - } - - xsize = sx1 - sx0; - if (xsize < 0) { - xsize = 0; - } - ysize = sy1 - sy0; - if (ysize < 0) { - ysize = 0; - } - - imOut = ImagingNewDirty(imIn->mode, xsize, ysize); - if (!imOut) { - return NULL; - } - - ImagingCopyPalette(imOut, imIn); - - if (sx0 < 0 || sy0 < 0 || sx1 > imIn->xsize || sy1 > imIn->ysize) { - (void)ImagingFill(imOut, &zero); - } - - dx0 = -sx0; - dy0 = -sy0; - dx1 = imIn->xsize - sx0; - dy1 = imIn->ysize - sy0; - - /* paste the source image on top of the output image!!! */ - if (ImagingPaste(imOut, imIn, NULL, dx0, dy0, dx1, dy1) < 0) { - ImagingDelete(imOut); - return NULL; - } - - return imOut; -} +/* + * The Python Imaging Library + * $Id$ + * + * cut region from image + * + * history: + * 95-11-27 fl Created + * 98-07-10 fl Fixed "null result" error + * 99-02-05 fl Rewritten to use Paste primitive + * + * Copyright (c) Secret Labs AB 1997-99. + * Copyright (c) Fredrik Lundh 1995. + * + * See the README file for information on usage and redistribution. + */ + +#include "Imaging.h" + +Imaging +ImagingCrop(Imaging imIn, int sx0, int sy0, int sx1, int sy1) { + Imaging imOut; + int xsize, ysize; + int dx0, dy0, dx1, dy1; + INT32 zero = 0; + + if (!imIn) { + return (Imaging)ImagingError_ModeError(); + } + + xsize = sx1 - sx0; + if (xsize < 0) { + xsize = 0; + } + ysize = sy1 - sy0; + if (ysize < 0) { + ysize = 0; + } + + imOut = ImagingNewDirty(imIn->mode, xsize, ysize); + if (!imOut) { + return NULL; + } + + ImagingCopyPalette(imOut, imIn); + + if (sx0 < 0 || sy0 < 0 || sx1 > imIn->xsize || sy1 > imIn->ysize) { + (void)ImagingFill(imOut, &zero); + } + + dx0 = -sx0; + dy0 = -sy0; + dx1 = imIn->xsize - sx0; + dy1 = imIn->ysize - sy0; + + /* paste the source image on top of the output image!!! */ + if (ImagingPaste(imOut, imIn, NULL, dx0, dy0, dx1, dy1) < 0) { + ImagingDelete(imOut); + return NULL; + } + + return imOut; +} diff --git a/src/libImaging/Dib.c b/src/libImaging/Dib.c index 269be1058..c8f098a9f 100644 --- a/src/libImaging/Dib.c +++ b/src/libImaging/Dib.c @@ -1,313 +1,313 @@ -/* - * The Python Imaging Library - * $Id$ - * - * imaging display object for Windows - * - * history: - * 1996-05-12 fl Created - * 1996-05-17 fl Up and running - * 1996-05-21 fl Added palette stuff - * 1996-05-26 fl Added query palette and mode inquery - * 1997-09-21 fl Added draw primitive - * 1998-01-20 fl Use StretchDIBits instead of StretchBlt - * 1998-12-30 fl Plugged a resource leak in DeleteDIB (from Roger Burnham) - * - * Copyright (c) Secret Labs AB 1997-2001. - * Copyright (c) Fredrik Lundh 1996. - * - * See the README file for information on usage and redistribution. - */ - -#include "Imaging.h" - -#ifdef _WIN32 - -#include "ImDib.h" - -char * -ImagingGetModeDIB(int size_out[2]) { - /* Get device characteristics */ - - HDC dc; - char *mode; - - dc = CreateCompatibleDC(NULL); - - mode = "P"; - if (!(GetDeviceCaps(dc, RASTERCAPS) & RC_PALETTE)) { - mode = "RGB"; - if (GetDeviceCaps(dc, BITSPIXEL) == 1) { - mode = "1"; - } - } - - if (size_out) { - size_out[0] = GetDeviceCaps(dc, HORZRES); - size_out[1] = GetDeviceCaps(dc, VERTRES); - } - - DeleteDC(dc); - - return mode; -} - -ImagingDIB -ImagingNewDIB(const char *mode, int xsize, int ysize) { - /* Create a Windows bitmap */ - - ImagingDIB dib; - RGBQUAD *palette; - int i; - - /* Check mode */ - if (strcmp(mode, "1") != 0 && strcmp(mode, "L") != 0 && strcmp(mode, "RGB") != 0) { - return (ImagingDIB)ImagingError_ModeError(); - } - - /* Create DIB context and info header */ - /* malloc check ok, small constant allocation */ - dib = (ImagingDIB)malloc(sizeof(*dib)); - if (!dib) { - return (ImagingDIB)ImagingError_MemoryError(); - } - /* malloc check ok, small constant allocation */ - dib->info = (BITMAPINFO *)malloc(sizeof(BITMAPINFOHEADER) + 256 * sizeof(RGBQUAD)); - if (!dib->info) { - free(dib); - return (ImagingDIB)ImagingError_MemoryError(); - } - - memset(dib->info, 0, sizeof(BITMAPINFOHEADER)); - dib->info->bmiHeader.biSize = sizeof(BITMAPINFOHEADER); - dib->info->bmiHeader.biWidth = xsize; - dib->info->bmiHeader.biHeight = ysize; - dib->info->bmiHeader.biPlanes = 1; - dib->info->bmiHeader.biBitCount = strlen(mode) * 8; - dib->info->bmiHeader.biCompression = BI_RGB; - - /* Create DIB */ - dib->dc = CreateCompatibleDC(NULL); - if (!dib->dc) { - free(dib->info); - free(dib); - return (ImagingDIB)ImagingError_MemoryError(); - } - - dib->bitmap = CreateDIBSection( - dib->dc, dib->info, DIB_RGB_COLORS, (void **)&dib->bits, NULL, 0); - if (!dib->bitmap) { - free(dib->info); - free(dib); - return (ImagingDIB)ImagingError_MemoryError(); - } - - strcpy(dib->mode, mode); - dib->xsize = xsize; - dib->ysize = ysize; - - dib->pixelsize = strlen(mode); - dib->linesize = (xsize * dib->pixelsize + 3) & -4; - - if (dib->pixelsize == 1) { - dib->pack = dib->unpack = (ImagingShuffler)memcpy; - } else { - dib->pack = ImagingPackBGR; - dib->unpack = ImagingPackBGR; - } - - /* Bind the DIB to the device context */ - dib->old_bitmap = SelectObject(dib->dc, dib->bitmap); - - palette = dib->info->bmiColors; - - /* Bind a palette to it as well (only required for 8-bit DIBs) */ - if (dib->pixelsize == 1) { - for (i = 0; i < 256; i++) { - palette[i].rgbRed = palette[i].rgbGreen = palette[i].rgbBlue = i; - palette[i].rgbReserved = 0; - } - SetDIBColorTable(dib->dc, 0, 256, palette); - } - - /* Create an associated palette (for 8-bit displays only) */ - if (strcmp(ImagingGetModeDIB(NULL), "P") == 0) { - char palbuf[sizeof(LOGPALETTE) + 256 * sizeof(PALETTEENTRY)]; - LPLOGPALETTE pal = (LPLOGPALETTE)palbuf; - int i, r, g, b; - - /* Load system palette */ - pal->palVersion = 0x300; - pal->palNumEntries = 256; - GetSystemPaletteEntries(dib->dc, 0, 256, pal->palPalEntry); - - if (strcmp(mode, "L") == 0) { - /* Grayscale DIB. Fill all 236 slots with a grayscale ramp - * (this is usually overkill on Windows since VGA only offers - * 6 bits grayscale resolution). Ignore the slots already - * allocated by Windows */ - - i = 10; - for (r = 0; r < 236; r++) { - pal->palPalEntry[i].peRed = pal->palPalEntry[i].peGreen = - pal->palPalEntry[i].peBlue = i; - i++; - } - - dib->palette = CreatePalette(pal); - - } else if (strcmp(mode, "RGB") == 0) { -#ifdef CUBE216 - - /* Colour DIB. Create a 6x6x6 colour cube (216 entries) and - * add 20 extra graylevels for best result with grayscale - * images. */ - - i = 10; - for (r = 0; r < 256; r += 51) { - for (g = 0; g < 256; g += 51) { - for (b = 0; b < 256; b += 51) { - pal->palPalEntry[i].peRed = r; - pal->palPalEntry[i].peGreen = g; - pal->palPalEntry[i].peBlue = b; - i++; - } - } - } - for (r = 1; r < 22 - 1; r++) { - /* Black and white are already provided by the cube. */ - pal->palPalEntry[i].peRed = pal->palPalEntry[i].peGreen = - pal->palPalEntry[i].peBlue = r * 255 / (22 - 1); - i++; - } - -#else - - /* Colour DIB. Alternate palette. */ - - i = 10; - for (r = 0; r < 256; r += 37) { - for (g = 0; g < 256; g += 32) { - for (b = 0; b < 256; b += 64) { - pal->palPalEntry[i].peRed = r; - pal->palPalEntry[i].peGreen = g; - pal->palPalEntry[i].peBlue = b; - i++; - } - } - } - -#endif - - dib->palette = CreatePalette(pal); - } - } - - return dib; -} - -void -ImagingPasteDIB(ImagingDIB dib, Imaging im, int xy[4]) { - /* Paste image data into a bitmap */ - - /* FIXME: check size! */ - - int y; - for (y = 0; y < im->ysize; y++) { - dib->pack( - dib->bits + dib->linesize * (dib->ysize - (xy[1] + y) - 1) + - xy[0] * dib->pixelsize, - im->image[y], - im->xsize); - } -} - -void -ImagingExposeDIB(ImagingDIB dib, void *dc) { - /* Copy bitmap to display */ - - if (dib->palette != 0) { - SelectPalette((HDC)dc, dib->palette, FALSE); - } - BitBlt((HDC)dc, 0, 0, dib->xsize, dib->ysize, dib->dc, 0, 0, SRCCOPY); -} - -void -ImagingDrawDIB(ImagingDIB dib, void *dc, int dst[4], int src[4]) { - /* Copy bitmap to printer/display */ - - if (GetDeviceCaps((HDC)dc, RASTERCAPS) & RC_STRETCHDIB) { - /* stretchdib (printers) */ - StretchDIBits( - (HDC)dc, - dst[0], - dst[1], - dst[2] - dst[0], - dst[3] - dst[1], - src[0], - src[1], - src[2] - src[0], - src[3] - src[1], - dib->bits, - dib->info, - DIB_RGB_COLORS, - SRCCOPY); - } else { - /* stretchblt (displays) */ - if (dib->palette != 0) { - SelectPalette((HDC)dc, dib->palette, FALSE); - } - StretchBlt( - (HDC)dc, - dst[0], - dst[1], - dst[2] - dst[0], - dst[3] - dst[1], - dib->dc, - src[0], - src[1], - src[2] - src[0], - src[3] - src[1], - SRCCOPY); - } -} - -int -ImagingQueryPaletteDIB(ImagingDIB dib, void *dc) { - /* Install bitmap palette */ - - int n; - - if (dib->palette != 0) { - /* Realize associated palette */ - HPALETTE now = SelectPalette((HDC)dc, dib->palette, FALSE); - n = RealizePalette((HDC)dc); - - /* Restore palette */ - SelectPalette((HDC)dc, now, FALSE); - - } else { - n = 0; - } - - return n; /* number of colours that was changed */ -} - -void -ImagingDeleteDIB(ImagingDIB dib) { - /* Clean up */ - - if (dib->palette) { - DeleteObject(dib->palette); - } - if (dib->bitmap) { - SelectObject(dib->dc, dib->old_bitmap); - DeleteObject(dib->bitmap); - } - if (dib->dc) { - DeleteDC(dib->dc); - } - free(dib->info); -} - -#endif /* _WIN32 */ +/* + * The Python Imaging Library + * $Id$ + * + * imaging display object for Windows + * + * history: + * 1996-05-12 fl Created + * 1996-05-17 fl Up and running + * 1996-05-21 fl Added palette stuff + * 1996-05-26 fl Added query palette and mode inquery + * 1997-09-21 fl Added draw primitive + * 1998-01-20 fl Use StretchDIBits instead of StretchBlt + * 1998-12-30 fl Plugged a resource leak in DeleteDIB (from Roger Burnham) + * + * Copyright (c) Secret Labs AB 1997-2001. + * Copyright (c) Fredrik Lundh 1996. + * + * See the README file for information on usage and redistribution. + */ + +#include "Imaging.h" + +#ifdef _WIN32 + +#include "ImDib.h" + +char * +ImagingGetModeDIB(int size_out[2]) { + /* Get device characteristics */ + + HDC dc; + char *mode; + + dc = CreateCompatibleDC(NULL); + + mode = "P"; + if (!(GetDeviceCaps(dc, RASTERCAPS) & RC_PALETTE)) { + mode = "RGB"; + if (GetDeviceCaps(dc, BITSPIXEL) == 1) { + mode = "1"; + } + } + + if (size_out) { + size_out[0] = GetDeviceCaps(dc, HORZRES); + size_out[1] = GetDeviceCaps(dc, VERTRES); + } + + DeleteDC(dc); + + return mode; +} + +ImagingDIB +ImagingNewDIB(const char *mode, int xsize, int ysize) { + /* Create a Windows bitmap */ + + ImagingDIB dib; + RGBQUAD *palette; + int i; + + /* Check mode */ + if (strcmp(mode, "1") != 0 && strcmp(mode, "L") != 0 && strcmp(mode, "RGB") != 0) { + return (ImagingDIB)ImagingError_ModeError(); + } + + /* Create DIB context and info header */ + /* malloc check ok, small constant allocation */ + dib = (ImagingDIB)malloc(sizeof(*dib)); + if (!dib) { + return (ImagingDIB)ImagingError_MemoryError(); + } + /* malloc check ok, small constant allocation */ + dib->info = (BITMAPINFO *)malloc(sizeof(BITMAPINFOHEADER) + 256 * sizeof(RGBQUAD)); + if (!dib->info) { + free(dib); + return (ImagingDIB)ImagingError_MemoryError(); + } + + memset(dib->info, 0, sizeof(BITMAPINFOHEADER)); + dib->info->bmiHeader.biSize = sizeof(BITMAPINFOHEADER); + dib->info->bmiHeader.biWidth = xsize; + dib->info->bmiHeader.biHeight = ysize; + dib->info->bmiHeader.biPlanes = 1; + dib->info->bmiHeader.biBitCount = strlen(mode) * 8; + dib->info->bmiHeader.biCompression = BI_RGB; + + /* Create DIB */ + dib->dc = CreateCompatibleDC(NULL); + if (!dib->dc) { + free(dib->info); + free(dib); + return (ImagingDIB)ImagingError_MemoryError(); + } + + dib->bitmap = CreateDIBSection( + dib->dc, dib->info, DIB_RGB_COLORS, (void **)&dib->bits, NULL, 0); + if (!dib->bitmap) { + free(dib->info); + free(dib); + return (ImagingDIB)ImagingError_MemoryError(); + } + + strcpy(dib->mode, mode); + dib->xsize = xsize; + dib->ysize = ysize; + + dib->pixelsize = strlen(mode); + dib->linesize = (xsize * dib->pixelsize + 3) & -4; + + if (dib->pixelsize == 1) { + dib->pack = dib->unpack = (ImagingShuffler)memcpy; + } else { + dib->pack = ImagingPackBGR; + dib->unpack = ImagingPackBGR; + } + + /* Bind the DIB to the device context */ + dib->old_bitmap = SelectObject(dib->dc, dib->bitmap); + + palette = dib->info->bmiColors; + + /* Bind a palette to it as well (only required for 8-bit DIBs) */ + if (dib->pixelsize == 1) { + for (i = 0; i < 256; i++) { + palette[i].rgbRed = palette[i].rgbGreen = palette[i].rgbBlue = i; + palette[i].rgbReserved = 0; + } + SetDIBColorTable(dib->dc, 0, 256, palette); + } + + /* Create an associated palette (for 8-bit displays only) */ + if (strcmp(ImagingGetModeDIB(NULL), "P") == 0) { + char palbuf[sizeof(LOGPALETTE) + 256 * sizeof(PALETTEENTRY)]; + LPLOGPALETTE pal = (LPLOGPALETTE)palbuf; + int i, r, g, b; + + /* Load system palette */ + pal->palVersion = 0x300; + pal->palNumEntries = 256; + GetSystemPaletteEntries(dib->dc, 0, 256, pal->palPalEntry); + + if (strcmp(mode, "L") == 0) { + /* Grayscale DIB. Fill all 236 slots with a grayscale ramp + * (this is usually overkill on Windows since VGA only offers + * 6 bits grayscale resolution). Ignore the slots already + * allocated by Windows */ + + i = 10; + for (r = 0; r < 236; r++) { + pal->palPalEntry[i].peRed = pal->palPalEntry[i].peGreen = + pal->palPalEntry[i].peBlue = i; + i++; + } + + dib->palette = CreatePalette(pal); + + } else if (strcmp(mode, "RGB") == 0) { +#ifdef CUBE216 + + /* Colour DIB. Create a 6x6x6 colour cube (216 entries) and + * add 20 extra graylevels for best result with grayscale + * images. */ + + i = 10; + for (r = 0; r < 256; r += 51) { + for (g = 0; g < 256; g += 51) { + for (b = 0; b < 256; b += 51) { + pal->palPalEntry[i].peRed = r; + pal->palPalEntry[i].peGreen = g; + pal->palPalEntry[i].peBlue = b; + i++; + } + } + } + for (r = 1; r < 22 - 1; r++) { + /* Black and white are already provided by the cube. */ + pal->palPalEntry[i].peRed = pal->palPalEntry[i].peGreen = + pal->palPalEntry[i].peBlue = r * 255 / (22 - 1); + i++; + } + +#else + + /* Colour DIB. Alternate palette. */ + + i = 10; + for (r = 0; r < 256; r += 37) { + for (g = 0; g < 256; g += 32) { + for (b = 0; b < 256; b += 64) { + pal->palPalEntry[i].peRed = r; + pal->palPalEntry[i].peGreen = g; + pal->palPalEntry[i].peBlue = b; + i++; + } + } + } + +#endif + + dib->palette = CreatePalette(pal); + } + } + + return dib; +} + +void +ImagingPasteDIB(ImagingDIB dib, Imaging im, int xy[4]) { + /* Paste image data into a bitmap */ + + /* FIXME: check size! */ + + int y; + for (y = 0; y < im->ysize; y++) { + dib->pack( + dib->bits + dib->linesize * (dib->ysize - (xy[1] + y) - 1) + + xy[0] * dib->pixelsize, + im->image[y], + im->xsize); + } +} + +void +ImagingExposeDIB(ImagingDIB dib, void *dc) { + /* Copy bitmap to display */ + + if (dib->palette != 0) { + SelectPalette((HDC)dc, dib->palette, FALSE); + } + BitBlt((HDC)dc, 0, 0, dib->xsize, dib->ysize, dib->dc, 0, 0, SRCCOPY); +} + +void +ImagingDrawDIB(ImagingDIB dib, void *dc, int dst[4], int src[4]) { + /* Copy bitmap to printer/display */ + + if (GetDeviceCaps((HDC)dc, RASTERCAPS) & RC_STRETCHDIB) { + /* stretchdib (printers) */ + StretchDIBits( + (HDC)dc, + dst[0], + dst[1], + dst[2] - dst[0], + dst[3] - dst[1], + src[0], + src[1], + src[2] - src[0], + src[3] - src[1], + dib->bits, + dib->info, + DIB_RGB_COLORS, + SRCCOPY); + } else { + /* stretchblt (displays) */ + if (dib->palette != 0) { + SelectPalette((HDC)dc, dib->palette, FALSE); + } + StretchBlt( + (HDC)dc, + dst[0], + dst[1], + dst[2] - dst[0], + dst[3] - dst[1], + dib->dc, + src[0], + src[1], + src[2] - src[0], + src[3] - src[1], + SRCCOPY); + } +} + +int +ImagingQueryPaletteDIB(ImagingDIB dib, void *dc) { + /* Install bitmap palette */ + + int n; + + if (dib->palette != 0) { + /* Realize associated palette */ + HPALETTE now = SelectPalette((HDC)dc, dib->palette, FALSE); + n = RealizePalette((HDC)dc); + + /* Restore palette */ + SelectPalette((HDC)dc, now, FALSE); + + } else { + n = 0; + } + + return n; /* number of colours that was changed */ +} + +void +ImagingDeleteDIB(ImagingDIB dib) { + /* Clean up */ + + if (dib->palette) { + DeleteObject(dib->palette); + } + if (dib->bitmap) { + SelectObject(dib->dc, dib->old_bitmap); + DeleteObject(dib->bitmap); + } + if (dib->dc) { + DeleteDC(dib->dc); + } + free(dib->info); +} + +#endif /* _WIN32 */ diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c index 133696dd8..08b3d2a5c 100644 --- a/src/libImaging/Draw.c +++ b/src/libImaging/Draw.c @@ -1,1982 +1,1982 @@ -/* - * The Python Imaging Library. - * $Id$ - * - * a simple drawing package for the Imaging library - * - * history: - * 1996-04-13 fl Created. - * 1996-04-30 fl Added transforms and polygon support. - * 1996-08-12 fl Added filled polygons. - * 1996-11-05 fl Fixed float/int confusion in polygon filler - * 1997-07-04 fl Support 32-bit images (C++ would have been nice) - * 1998-09-09 fl Eliminated qsort casts; improved rectangle clipping - * 1998-09-10 fl Fixed fill rectangle to include lower edge (!) - * 1998-12-29 fl Added arc, chord, and pieslice primitives - * 1999-01-10 fl Added some level 2 ("arrow") stuff (experimental) - * 1999-02-06 fl Added bitmap primitive - * 1999-07-26 fl Eliminated a compiler warning - * 1999-07-31 fl Pass ink as void* instead of int - * 2002-12-10 fl Added experimental RGBA-on-RGB drawing - * 2004-09-04 fl Support simple wide lines (no joins) - * 2005-05-25 fl Fixed line width calculation - * - * Copyright (c) 1996-2006 by Fredrik Lundh - * Copyright (c) 1997-2006 by Secret Labs AB. - * - * See the README file for information on usage and redistribution. - */ - -/* FIXME: support fill/outline attribute for all filled shapes */ -/* FIXME: support zero-winding fill */ -/* FIXME: add drawing context, support affine transforms */ -/* FIXME: support clip window (and mask?) */ - -#include "Imaging.h" - -#include -#include - -#define CEIL(v) (int)ceil(v) -#define FLOOR(v) ((v) >= 0.0 ? (int)(v) : (int)floor(v)) - -#define INK8(ink) (*(UINT8 *)ink) -#define INK16(ink) (*(UINT16 *)ink) - -/* - * Rounds around zero (up=away from zero, down=towards zero) - * This guarantees that ROUND_UP|DOWN(f) == -ROUND_UP|DOWN(-f) - */ -#define ROUND_UP(f) ((int)((f) >= 0.0 ? floor((f) + 0.5F) : -floor(fabs(f) + 0.5F))) -#define ROUND_DOWN(f) ((int)((f) >= 0.0 ? ceil((f) - 0.5F) : -ceil(fabs(f) - 0.5F))) - -/* -------------------------------------------------------------------- */ -/* Primitives */ -/* -------------------------------------------------------------------- */ - -typedef struct { - /* edge descriptor for polygon engine */ - int d; - int x0, y0; - int xmin, ymin, xmax, ymax; - float dx; -} Edge; - -/* Type used in "polygon*" functions */ -typedef void (*hline_handler)(Imaging, int, int, int, int); - -static inline void -point8(Imaging im, int x, int y, int ink) { - if (x >= 0 && x < im->xsize && y >= 0 && y < im->ysize) { - if (strncmp(im->mode, "I;16", 4) == 0) { -#ifdef WORDS_BIGENDIAN - im->image8[y][x * 2] = (UINT8)(ink >> 8); - im->image8[y][x * 2 + 1] = (UINT8)ink; -#else - im->image8[y][x * 2] = (UINT8)ink; - im->image8[y][x * 2 + 1] = (UINT8)(ink >> 8); -#endif - } else { - im->image8[y][x] = (UINT8)ink; - } - } -} - -static inline void -point32(Imaging im, int x, int y, int ink) { - if (x >= 0 && x < im->xsize && y >= 0 && y < im->ysize) { - im->image32[y][x] = ink; - } -} - -static inline void -point32rgba(Imaging im, int x, int y, int ink) { - unsigned int tmp; - - if (x >= 0 && x < im->xsize && y >= 0 && y < im->ysize) { - UINT8 *out = (UINT8 *)im->image[y] + x * 4; - UINT8 *in = (UINT8 *)&ink; - 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); - } -} - -static inline void -hline8(Imaging im, int x0, int y0, int x1, int ink) { - int pixelwidth; - - if (y0 >= 0 && y0 < im->ysize) { - if (x0 < 0) { - x0 = 0; - } else if (x0 >= im->xsize) { - return; - } - if (x1 < 0) { - return; - } else if (x1 >= im->xsize) { - x1 = im->xsize - 1; - } - if (x0 <= x1) { - pixelwidth = strncmp(im->mode, "I;16", 4) == 0 ? 2 : 1; - memset( - im->image8[y0] + x0 * pixelwidth, - (UINT8)ink, - (x1 - x0 + 1) * pixelwidth); - } - } -} - -static inline void -hline32(Imaging im, int x0, int y0, int x1, int ink) { - INT32 *p; - - if (y0 >= 0 && y0 < im->ysize) { - if (x0 < 0) { - x0 = 0; - } else if (x0 >= im->xsize) { - return; - } - if (x1 < 0) { - return; - } else if (x1 >= im->xsize) { - x1 = im->xsize - 1; - } - p = im->image32[y0]; - while (x0 <= x1) { - p[x0++] = ink; - } - } -} - -static inline void -hline32rgba(Imaging im, int x0, int y0, int x1, int ink) { - unsigned int tmp; - - if (y0 >= 0 && y0 < im->ysize) { - if (x0 < 0) { - x0 = 0; - } else if (x0 >= im->xsize) { - return; - } - if (x1 < 0) { - return; - } else if (x1 >= im->xsize) { - x1 = im->xsize - 1; - } - if (x0 <= x1) { - 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); - x0++; - out += 4; - } - } - } -} - -static inline void -line8(Imaging im, int x0, int y0, int x1, int y1, int ink) { - int i, n, e; - int dx, dy; - int xs, ys; - - /* normalize coordinates */ - dx = x1 - x0; - if (dx < 0) { - dx = -dx, xs = -1; - } else { - xs = 1; - } - dy = y1 - y0; - if (dy < 0) { - dy = -dy, ys = -1; - } else { - ys = 1; - } - - n = (dx > dy) ? dx : dy; - - if (dx == 0) { - /* vertical */ - for (i = 0; i < dy; i++) { - point8(im, x0, y0, ink); - y0 += ys; - } - - } else if (dy == 0) { - /* horizontal */ - for (i = 0; i < dx; i++) { - point8(im, x0, y0, ink); - x0 += xs; - } - - } else if (dx > dy) { - /* bresenham, horizontal slope */ - n = dx; - dy += dy; - e = dy - dx; - dx += dx; - - for (i = 0; i < n; i++) { - point8(im, x0, y0, ink); - if (e >= 0) { - y0 += ys; - e -= dx; - } - e += dy; - x0 += xs; - } - - } else { - /* bresenham, vertical slope */ - n = dy; - dx += dx; - e = dx - dy; - dy += dy; - - for (i = 0; i < n; i++) { - point8(im, x0, y0, ink); - if (e >= 0) { - x0 += xs; - e -= dy; - } - e += dx; - y0 += ys; - } - } -} - -static inline void -line32(Imaging im, int x0, int y0, int x1, int y1, int ink) { - int i, n, e; - int dx, dy; - int xs, ys; - - /* normalize coordinates */ - dx = x1 - x0; - if (dx < 0) { - dx = -dx, xs = -1; - } else { - xs = 1; - } - dy = y1 - y0; - if (dy < 0) { - dy = -dy, ys = -1; - } else { - ys = 1; - } - - n = (dx > dy) ? dx : dy; - - if (dx == 0) { - /* vertical */ - for (i = 0; i < dy; i++) { - point32(im, x0, y0, ink); - y0 += ys; - } - - } else if (dy == 0) { - /* horizontal */ - for (i = 0; i < dx; i++) { - point32(im, x0, y0, ink); - x0 += xs; - } - - } else if (dx > dy) { - /* bresenham, horizontal slope */ - n = dx; - dy += dy; - e = dy - dx; - dx += dx; - - for (i = 0; i < n; i++) { - point32(im, x0, y0, ink); - if (e >= 0) { - y0 += ys; - e -= dx; - } - e += dy; - x0 += xs; - } - - } else { - /* bresenham, vertical slope */ - n = dy; - dx += dx; - e = dx - dy; - dy += dy; - - for (i = 0; i < n; i++) { - point32(im, x0, y0, ink); - if (e >= 0) { - x0 += xs; - e -= dy; - } - e += dx; - y0 += ys; - } - } -} - -static inline void -line32rgba(Imaging im, int x0, int y0, int x1, int y1, int ink) { - int i, n, e; - int dx, dy; - int xs, ys; - - /* normalize coordinates */ - dx = x1 - x0; - if (dx < 0) { - dx = -dx, xs = -1; - } else { - xs = 1; - } - dy = y1 - y0; - if (dy < 0) { - dy = -dy, ys = -1; - } else { - ys = 1; - } - - n = (dx > dy) ? dx : dy; - - if (dx == 0) { - /* vertical */ - for (i = 0; i < dy; i++) { - point32rgba(im, x0, y0, ink); - y0 += ys; - } - - } else if (dy == 0) { - /* horizontal */ - for (i = 0; i < dx; i++) { - point32rgba(im, x0, y0, ink); - x0 += xs; - } - - } else if (dx > dy) { - /* bresenham, horizontal slope */ - n = dx; - dy += dy; - e = dy - dx; - dx += dx; - - for (i = 0; i < n; i++) { - point32rgba(im, x0, y0, ink); - if (e >= 0) { - y0 += ys; - e -= dx; - } - e += dy; - x0 += xs; - } - - } else { - /* bresenham, vertical slope */ - n = dy; - dx += dx; - e = dx - dy; - dy += dy; - - for (i = 0; i < n; i++) { - point32rgba(im, x0, y0, ink); - if (e >= 0) { - x0 += xs; - e -= dy; - } - e += dx; - y0 += ys; - } - } -} - -static int -x_cmp(const void *x0, const void *x1) { - float diff = *((float *)x0) - *((float *)x1); - if (diff < 0) { - return -1; - } else if (diff > 0) { - return 1; - } else { - return 0; - } -} - -static void -draw_horizontal_lines( - Imaging im, int n, Edge *e, int ink, int *x_pos, int y, hline_handler hline) { - int i; - for (i = 0; i < n; i++) { - if (e[i].ymin == y && e[i].ymin == e[i].ymax) { - int xmax; - int xmin = e[i].xmin; - if (*x_pos != -1 && *x_pos < xmin) { - // Line would be after the current position - continue; - } - - xmax = e[i].xmax; - if (*x_pos > xmin) { - // Line would be partway through x_pos, so increase the starting point - xmin = *x_pos; - if (xmax < xmin) { - // Line would now end before it started - continue; - } - } - - (*hline)(im, xmin, e[i].ymin, xmax, ink); - *x_pos = xmax + 1; - } - } -} - -/* - * Filled polygon draw function using scan line algorithm. - */ -static inline int -polygon_generic( - Imaging im, - int n, - Edge *e, - int ink, - int eofill, - hline_handler hline, - int hasAlpha) { - Edge **edge_table; - float *xx; - int edge_count = 0; - int ymin = im->ysize - 1; - int ymax = 0; - int i, j, k; - float adjacent_line_x, adjacent_line_x_other_edge; - - if (n <= 0) { - return 0; - } - - /* Initialize the edge table and find polygon boundaries */ - /* malloc check ok, using calloc */ - edge_table = calloc(n, sizeof(Edge *)); - if (!edge_table) { - return -1; - } - - for (i = 0; i < n; i++) { - if (ymin > e[i].ymin) { - ymin = e[i].ymin; - } - if (ymax < e[i].ymax) { - ymax = e[i].ymax; - } - if (e[i].ymin == e[i].ymax) { - if (hasAlpha != 1) { - (*hline)(im, e[i].xmin, e[i].ymin, e[i].xmax, ink); - } - continue; - } - edge_table[edge_count++] = (e + i); - } - if (ymin < 0) { - ymin = 0; - } - if (ymax > im->ysize) { - ymax = im->ysize; - } - - /* Process the edge table with a scan line searching for intersections */ - /* malloc check ok, using calloc */ - xx = calloc(edge_count * 2, sizeof(float)); - if (!xx) { - free(edge_table); - return -1; - } - for (; ymin <= ymax; ymin++) { - j = 0; - for (i = 0; i < edge_count; i++) { - Edge *current = edge_table[i]; - if (ymin >= current->ymin && ymin <= current->ymax) { - xx[j++] = (ymin - current->y0) * current->dx + current->x0; - - if (ymin == current->ymax && ymin < ymax) { - // Needed to draw consistent polygons - xx[j] = xx[j - 1]; - j++; - } else if (current->dx != 0 && roundf(xx[j - 1]) == xx[j - 1]) { - // Connect discontiguous corners - for (k = 0; k < i; k++) { - Edge *other_edge = edge_table[k]; - if ((current->dx > 0 && other_edge->dx <= 0) || - (current->dx < 0 && other_edge->dx >= 0)) { - continue; - } - // Check if the two edges join to make a corner - if (((ymin == current->ymin && ymin == other_edge->ymin) || - (ymin == current->ymax && ymin == other_edge->ymax)) && - xx[j - 1] == (ymin - other_edge->y0) * other_edge->dx + - other_edge->x0) { - // Determine points from the edges on the next row - // Or if this is the last row, check the previous row - int offset = ymin == ymax ? -1 : 1; - adjacent_line_x = - (ymin + offset - current->y0) * current->dx + - current->x0; - adjacent_line_x_other_edge = - (ymin + offset - other_edge->y0) * other_edge->dx + - other_edge->x0; - if (ymin == current->ymax) { - if (current->dx > 0) { - xx[k] = fmax( - adjacent_line_x, - adjacent_line_x_other_edge) + - 1; - } else { - xx[k] = fmin( - adjacent_line_x, - adjacent_line_x_other_edge) - - 1; - } - } else { - if (current->dx > 0) { - xx[k] = fmin( - adjacent_line_x, adjacent_line_x_other_edge); - } else { - xx[k] = fmax( - adjacent_line_x, - adjacent_line_x_other_edge) + - 1; - } - } - break; - } - } - } - } - } - qsort(xx, j, sizeof(float), x_cmp); - if (hasAlpha == 1) { - int x_pos = j == 0 ? -1 : 0; - for (i = 1; i < j; i += 2) { - int x_end = ROUND_DOWN(xx[i]); - if (x_end < x_pos) { - // Line would be before the current position - continue; - } - draw_horizontal_lines(im, n, e, ink, &x_pos, ymin, hline); - if (x_end < x_pos) { - // Line would be before the current position - continue; - } - - int x_start = ROUND_UP(xx[i - 1]); - if (x_pos > x_start) { - // Line would be partway through x_pos, so increase the starting - // point - x_start = x_pos; - if (x_end < x_start) { - // Line would now end before it started - continue; - } - } - (*hline)(im, x_start, ymin, x_end, ink); - x_pos = x_end + 1; - } - draw_horizontal_lines(im, n, e, ink, &x_pos, ymin, hline); - } else { - for (i = 1; i < j; i += 2) { - (*hline)(im, ROUND_UP(xx[i - 1]), ymin, ROUND_DOWN(xx[i]), ink); - } - } - } - - free(xx); - free(edge_table); - 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); */ - - if (x0 <= x1) { - e->xmin = x0, e->xmax = x1; - } else { - e->xmin = x1, e->xmax = x0; - } - - if (y0 <= y1) { - e->ymin = y0, e->ymax = y1; - } else { - e->ymin = y1, e->ymax = y0; - } - - if (y0 == y1) { - e->d = 0; - e->dx = 0.0; - } else { - e->dx = ((float)(x1 - x0)) / (y1 - y0); - if (y0 == e->ymin) { - e->d = 1; - } else { - e->d = -1; - } - } - - e->x0 = x0; - e->y0 = y0; -} - -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 (*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}; - -/* -------------------------------------------------------------------- */ -/* Interface */ -/* -------------------------------------------------------------------- */ - -#define DRAWINIT() \ - if (im->image8) { \ - draw = &draw8; \ - if (strncmp(im->mode, "I;16", 4) == 0) { \ - ink = INK16(ink_); \ - } else { \ - ink = INK8(ink_); \ - } \ - } else { \ - draw = (op) ? &draw32rgba : &draw32; \ - memcpy(&ink, ink_, sizeof(ink)); \ - } - -int -ImagingDrawPoint(Imaging im, int x0, int y0, const void *ink_, int op) { - DRAW *draw; - INT32 ink; - - DRAWINIT(); - - draw->point(im, x0, y0, ink); - - return 0; -} - -int -ImagingDrawLine(Imaging im, int x0, int y0, int x1, int y1, const void *ink_, int op) { - DRAW *draw; - INT32 ink; - - DRAWINIT(); - - draw->line(im, x0, y0, x1, y1, ink); - - return 0; -} - -int -ImagingDrawWideLine( - Imaging im, int x0, int y0, int x1, int y1, const void *ink_, int width, int op) { - DRAW *draw; - INT32 ink; - int dx, dy; - double big_hypotenuse, small_hypotenuse, ratio_max, ratio_min; - int dxmin, dxmax, dymin, dymax; - Edge e[4]; - - DRAWINIT(); - - dx = x1 - x0; - dy = y1 - y0; - if (dx == 0 && dy == 0) { - draw->point(im, x0, y0, ink); - return 0; - } - - big_hypotenuse = hypot(dx, dy); - small_hypotenuse = (width - 1) / 2.0; - ratio_max = ROUND_UP(small_hypotenuse) / big_hypotenuse; - ratio_min = ROUND_DOWN(small_hypotenuse) / big_hypotenuse; - - dxmin = ROUND_DOWN(ratio_min * dy); - dxmax = ROUND_DOWN(ratio_max * dy); - dymin = ROUND_DOWN(ratio_min * dx); - dymax = ROUND_DOWN(ratio_max * dx); - { - int vertices[4][2] = { - {x0 - dxmin, y0 + dymax}, - {x1 - dxmin, y1 + dymax}, - {x1 + dxmax, y1 - dymin}, - {x0 + dxmax, y0 - dymin}}; - - add_edge(e + 0, vertices[0][0], vertices[0][1], vertices[1][0], vertices[1][1]); - add_edge(e + 1, vertices[1][0], vertices[1][1], vertices[2][0], vertices[2][1]); - add_edge(e + 2, vertices[2][0], vertices[2][1], vertices[3][0], vertices[3][1]); - add_edge(e + 3, vertices[3][0], vertices[3][1], vertices[0][0], vertices[0][1]); - - draw->polygon(im, 4, e, ink, 0); - } - return 0; -} - -int -ImagingDrawRectangle( - Imaging im, - int x0, - int y0, - int x1, - int y1, - const void *ink_, - int fill, - int width, - int op) { - int i; - int y; - int tmp; - DRAW *draw; - INT32 ink; - - DRAWINIT(); - - if (y0 > y1) { - tmp = y0, y0 = y1, y1 = tmp; - } - - if (fill) { - if (y0 < 0) { - y0 = 0; - } else if (y0 >= im->ysize) { - return 0; - } - - if (y1 < 0) { - return 0; - } else if (y1 > im->ysize) { - y1 = im->ysize; - } - - for (y = y0; y <= y1; y++) { - draw->hline(im, x0, y, x1, ink); - } - - } else { - /* outline */ - if (width == 0) { - width = 1; - } - for (i = 0; i < width; i++) { - draw->hline(im, x0, y0 + i, x1, ink); - draw->hline(im, x0, y1 - i, x1, ink); - draw->line(im, x1 - i, y0 + width, x1 - i, y1 - width + 1, ink); - draw->line(im, x0 + i, y0 + width, x0 + i, y1 - width + 1, ink); - } - } - - return 0; -} - -int -ImagingDrawPolygon( - Imaging im, int count, int *xy, const void *ink_, int fill, int width, int op) { - int i, n, x0, y0, x1, y1; - DRAW *draw; - INT32 ink; - - if (count <= 0) { - return 0; - } - - DRAWINIT(); - - if (fill) { - /* Build edge list */ - /* malloc check ok, using calloc */ - Edge *e = calloc(count, sizeof(Edge)); - if (!e) { - (void)ImagingError_MemoryError(); - return -1; - } - for (i = n = 0; i < count - 1; i++) { - x0 = xy[i * 2]; - y0 = xy[i * 2 + 1]; - x1 = xy[i * 2 + 2]; - y1 = xy[i * 2 + 3]; - if (y0 == y1 && i != 0 && y0 == xy[i * 2 - 1]) { - // This is a horizontal line, - // that immediately follows another horizontal line - Edge *last_e = &e[n - 1]; - if (x1 > x0 && x0 > xy[i * 2 - 2]) { - // They are both increasing in x - last_e->xmax = x1; - continue; - } else if (x1 < x0 && x0 < xy[i * 2 - 2]) { - // They are both decreasing in x - last_e->xmin = x1; - continue; - } - } - add_edge(&e[n++], x0, y0, x1, y1); - } - if (xy[i * 2] != xy[0] || xy[i * 2 + 1] != xy[1]) { - add_edge(&e[n++], xy[i * 2], xy[i * 2 + 1], xy[0], xy[1]); - } - draw->polygon(im, n, e, ink, 0); - free(e); - - } else { - /* Outline */ - if (width == 1) { - for (i = 0; i < count - 1; i++) { - draw->line( - im, xy[i * 2], xy[i * 2 + 1], xy[i * 2 + 2], xy[i * 2 + 3], ink); - } - draw->line(im, xy[i * 2], xy[i * 2 + 1], xy[0], xy[1], ink); - } else { - for (i = 0; i < count - 1; i++) { - ImagingDrawWideLine( - im, - xy[i * 2], - xy[i * 2 + 1], - xy[i * 2 + 2], - xy[i * 2 + 3], - ink_, - width, - op); - } - ImagingDrawWideLine( - im, xy[i * 2], xy[i * 2 + 1], xy[0], xy[1], ink_, width, op); - } - } - - return 0; -} - -int -ImagingDrawBitmap(Imaging im, int x0, int y0, Imaging bitmap, const void *ink, int op) { - return ImagingFill2( - im, ink, bitmap, x0, y0, x0 + bitmap->xsize, y0 + bitmap->ysize); -} - -/* -------------------------------------------------------------------- */ -/* standard shapes */ - -// Imagine 2D plane and ellipse with center in (0, 0) and semi-major axes a and b. -// Then quarter_* stuff approximates its top right quarter (x, y >= 0) with integer -// points from set {(2x+x0, 2y+y0) | x,y in Z} where x0, y0 are from {0, 1} and -// are such that point (a, b) is in the set. - -typedef struct { - int32_t a, b, cx, cy, ex, ey; - int64_t a2, b2, a2b2; - int8_t finished; -} quarter_state; - -void -quarter_init(quarter_state *s, int32_t a, int32_t b) { - if (a < 0 || b < 0) { - s->finished = 1; - } else { - s->a = a; - s->b = b; - s->cx = a; - s->cy = b % 2; - s->ex = a % 2; - s->ey = b; - s->a2 = a * a; - s->b2 = b * b; - s->a2b2 = s->a2 * s->b2; - s->finished = 0; - } -} - -// deviation of the point from ellipse curve, basically a substitution -// of the point into the ellipse equation -int64_t -quarter_delta(quarter_state *s, int64_t x, int64_t y) { - return llabs(s->a2 * y * y + s->b2 * x * x - s->a2b2); -} - -int8_t -quarter_next(quarter_state *s, int32_t *ret_x, int32_t *ret_y) { - if (s->finished) { - return -1; - } - *ret_x = s->cx; - *ret_y = s->cy; - if (s->cx == s->ex && s->cy == s->ey) { - s->finished = 1; - } else { - // Bresenham's algorithm, possible optimization: only consider 2 of 3 - // next points depending on current slope - int32_t nx = s->cx; - int32_t ny = s->cy + 2; - int64_t ndelta = quarter_delta(s, nx, ny); - if (nx > 1) { - int64_t newdelta = quarter_delta(s, s->cx - 2, s->cy + 2); - if (ndelta > newdelta) { - nx = s->cx - 2; - ny = s->cy + 2; - ndelta = newdelta; - } - newdelta = quarter_delta(s, s->cx - 2, s->cy); - if (ndelta > newdelta) { - nx = s->cx - 2; - ny = s->cy; - } - } - s->cx = nx; - s->cy = ny; - } - return 0; -} - -// quarter_* stuff can "draw" a quarter of an ellipse with thickness 1, great. -// Now we use ellipse_* stuff to join all four quarters of two different sized -// ellipses and receive horizontal segments of a complete ellipse with -// specified thickness. -// -// Still using integer grid with step 2 at this point (like in quarter_*) -// to ease angle clipping in future. - -typedef struct { - quarter_state st_o, st_i; - int32_t py, pl, pr; - int32_t cy[4], cl[4], cr[4]; - int8_t bufcnt; - int8_t finished; - int8_t leftmost; -} ellipse_state; - -void -ellipse_init(ellipse_state *s, int32_t a, int32_t b, int32_t w) { - s->bufcnt = 0; - s->leftmost = a % 2; - quarter_init(&s->st_o, a, b); - if (w < 1 || quarter_next(&s->st_o, &s->pr, &s->py) == -1) { - s->finished = 1; - } else { - s->finished = 0; - quarter_init(&s->st_i, a - 2 * (w - 1), b - 2 * (w - 1)); - s->pl = s->leftmost; - } -} - -int8_t -ellipse_next(ellipse_state *s, int32_t *ret_x0, int32_t *ret_y, int32_t *ret_x1) { - if (s->bufcnt == 0) { - if (s->finished) { - return -1; - } - int32_t y = s->py; - int32_t l = s->pl; - int32_t r = s->pr; - int32_t cx = 0, cy = 0; - int8_t next_ret; - while ((next_ret = quarter_next(&s->st_o, &cx, &cy)) != -1 && cy <= y) { - } - if (next_ret == -1) { - s->finished = 1; - } else { - s->pr = cx; - s->py = cy; - } - while ((next_ret = quarter_next(&s->st_i, &cx, &cy)) != -1 && cy <= y) { - l = cx; - } - s->pl = next_ret == -1 ? s->leftmost : cx; - - if ((l > 0 || l < r) && y > 0) { - s->cl[s->bufcnt] = l == 0 ? 2 : l; - s->cy[s->bufcnt] = y; - s->cr[s->bufcnt] = r; - ++s->bufcnt; - } - if (y > 0) { - s->cl[s->bufcnt] = -r; - s->cy[s->bufcnt] = y; - s->cr[s->bufcnt] = -l; - ++s->bufcnt; - } - if (l > 0 || l < r) { - s->cl[s->bufcnt] = l == 0 ? 2 : l; - s->cy[s->bufcnt] = -y; - s->cr[s->bufcnt] = r; - ++s->bufcnt; - } - s->cl[s->bufcnt] = -r; - s->cy[s->bufcnt] = -y; - s->cr[s->bufcnt] = -l; - ++s->bufcnt; - } - --s->bufcnt; - *ret_x0 = s->cl[s->bufcnt]; - *ret_y = s->cy[s->bufcnt]; - *ret_x1 = s->cr[s->bufcnt]; - return 0; -} - -// Clipping tree consists of half-plane clipping nodes and combining nodes. -// We can throw a horizontal segment in such a tree and collect an ordered set -// of resulting disjoint clipped segments organized into a sorted linked list -// of their end points. -typedef enum { - CT_AND, // intersection - CT_OR, // union - CT_CLIP // half-plane clipping -} clip_type; - -typedef struct clip_node { - clip_type type; - double a, b, c; // half-plane coeffs, only used in clipping nodes - struct clip_node *l; // child pointers, are only non-NULL in combining nodes - struct clip_node *r; -} clip_node; - -// Linked list for the ends of the clipped horizontal segments. -// Since the segment is always horizontal, we don't need to store Y coordinate. -typedef struct event_list { - int32_t x; - int8_t type; // used internally, 1 for the left end (smaller X), -1 for the - // right end; pointless in output since the output segments - // are disjoint, therefore the types would always come in pairs - // and interchange (1 -1 1 -1 ...) - struct event_list *next; -} event_list; - -// Mirrors all the clipping nodes of the tree relative to the y = x line. -void -clip_tree_transpose(clip_node *root) { - if (root != NULL) { - if (root->type == CT_CLIP) { - double t = root->a; - root->a = root->b; - root->b = t; - } - clip_tree_transpose(root->l); - clip_tree_transpose(root->r); - } -} - -// Outputs a sequence of open-close events (types -1 and 1) for -// non-intersecting segments sorted by X coordinate. -// Combining nodes (AND, OR) may also accept sequences for intersecting -// segments, i.e. something like correct bracket sequences. -int -clip_tree_do_clip( - clip_node *root, int32_t x0, int32_t y, int32_t x1, event_list **ret) { - if (root == NULL) { - event_list *start = malloc(sizeof(event_list)); - if (!start) { - ImagingError_MemoryError(); - return -1; - } - event_list *end = malloc(sizeof(event_list)); - if (!end) { - free(start); - ImagingError_MemoryError(); - return -1; - } - start->x = x0; - start->type = 1; - start->next = end; - end->x = x1; - end->type = -1; - end->next = NULL; - *ret = start; - return 0; - } - if (root->type == CT_CLIP) { - double eps = 1e-9; - double A = root->a; - double B = root->b; - double C = root->c; - if (fabs(A) < eps) { - if (B * y + C < -eps) { - x0 = 1; - x1 = 0; - } - } else { - // X of intersection - double ix = -(B * y + C) / A; - if (A * x0 + B * y + C < eps) { - x0 = lround(fmax(x0, ix)); - } - if (A * x1 + B * y + C < eps) { - x1 = lround(fmin(x1, ix)); - } - } - if (x0 <= x1) { - event_list *start = malloc(sizeof(event_list)); - if (!start) { - ImagingError_MemoryError(); - return -1; - } - event_list *end = malloc(sizeof(event_list)); - if (!end) { - free(start); - ImagingError_MemoryError(); - return -1; - } - start->x = x0; - start->type = 1; - start->next = end; - end->x = x1; - end->type = -1; - end->next = NULL; - *ret = start; - } else { - *ret = NULL; - } - return 0; - } - if (root->type == CT_OR || root->type == CT_AND) { - event_list *l1; - event_list *l2; - if (clip_tree_do_clip(root->l, x0, y, x1, &l1) < 0) { - return -1; - } - if (clip_tree_do_clip(root->r, x0, y, x1, &l2) < 0) { - while (l1) { - l2 = l1->next; - free(l1); - l1 = l2; - } - return -1; - } - *ret = NULL; - event_list *tail = NULL; - int32_t k1 = 0; - int32_t k2 = 0; - while (l1 != NULL || l2 != NULL) { - event_list *t; - if (l2 == NULL || - (l1 != NULL && - (l1->x < l2->x || (l1->x == l2->x && l1->type > l2->type)))) { - t = l1; - k1 += t->type; - assert(k1 >= 0); - l1 = l1->next; - } else { - t = l2; - k2 += t->type; - assert(k2 >= 0); - l2 = l2->next; - } - t->next = NULL; - if ((root->type == CT_OR && - ((t->type == 1 && (tail == NULL || tail->type == -1)) || - (t->type == -1 && k1 == 0 && k2 == 0))) || - (root->type == CT_AND && - ((t->type == 1 && (tail == NULL || tail->type == -1) && k1 > 0 && - k2 > 0) || - (t->type == -1 && tail != NULL && tail->type == 1 && - (k1 == 0 || k2 == 0))))) { - if (tail == NULL) { - *ret = t; - } else { - tail->next = t; - } - tail = t; - } else { - free(t); - } - } - return 0; - } - *ret = NULL; - return 0; -} - -// One more layer of processing on top of the regular ellipse. -// Uses the clipping tree. -// Used for producing ellipse derivatives such as arc, chord, pie, etc. -typedef struct { - ellipse_state st; - clip_node *root; - clip_node nodes[7]; - int32_t node_count; - event_list *head; - int32_t y; -} clip_ellipse_state; - -typedef void (*clip_ellipse_init)( - clip_ellipse_state *, int32_t, int32_t, int32_t, float, float); - -void -debug_clip_tree(clip_node *root, int space) { - if (root == NULL) { - return; - } - if (root->type == CT_CLIP) { - int t = space; - while (t--) { - fputc(' ', stderr); - } - fprintf(stderr, "clip %+fx%+fy%+f > 0\n", root->a, root->b, root->c); - } else { - debug_clip_tree(root->l, space + 2); - int t = space; - while (t--) { - fputc(' ', stderr); - } - fprintf(stderr, "%s\n", root->type == CT_AND ? "and" : "or"); - debug_clip_tree(root->r, space + 2); - } - if (space == 0) { - fputc('\n', stderr); - } -} - -// Resulting angles will satisfy 0 <= al < 360, al <= ar <= al + 360 -void -normalize_angles(float *al, float *ar) { - if (*ar - *al >= 360) { - *al = 0; - *ar = 360; - } else { - *al = fmod(*al < 0 ? 360 - (fmod(-*al, 360)) : *al, 360); - *ar = *al + fmod(*ar < *al ? 360 - fmod(*al - *ar, 360) : *ar - *al, 360); - } -} - -// An arc with caps orthogonal to the ellipse curve. -void -arc_init(clip_ellipse_state *s, int32_t a, int32_t b, int32_t w, float al, float ar) { - if (a < b) { - // transpose the coordinate system - arc_init(s, b, a, w, 90 - ar, 90 - al); - ellipse_init(&s->st, a, b, w); - clip_tree_transpose(s->root); - } else { - // a >= b, based on "wide" ellipse - ellipse_init(&s->st, a, b, w); - - s->head = NULL; - s->node_count = 0; - normalize_angles(&al, &ar); - - // building clipping tree, a lot of different cases - if (ar == al + 360) { - s->root = NULL; - } else { - clip_node *lc = s->nodes + s->node_count++; - clip_node *rc = s->nodes + s->node_count++; - lc->l = lc->r = rc->l = rc->r = NULL; - lc->type = rc->type = CT_CLIP; - lc->a = -a * sin(al * M_PI / 180.0); - lc->b = b * cos(al * M_PI / 180.0); - lc->c = (a * a - b * b) * sin(al * M_PI / 90.0) / 2.0; - rc->a = a * sin(ar * M_PI / 180.0); - rc->b = -b * cos(ar * M_PI / 180.0); - rc->c = (b * b - a * a) * sin(ar * M_PI / 90.0) / 2.0; - if (fmod(al, 180) == 0 || fmod(ar, 180) == 0) { - s->root = s->nodes + s->node_count++; - s->root->l = lc; - s->root->r = rc; - s->root->type = ar - al < 180 ? CT_AND : CT_OR; - } else if (((int)(al / 180) + (int)(ar / 180)) % 2 == 1) { - s->root = s->nodes + s->node_count++; - s->root->l = s->nodes + s->node_count++; - s->root->l->l = s->nodes + s->node_count++; - s->root->l->r = lc; - s->root->r = s->nodes + s->node_count++; - s->root->r->l = s->nodes + s->node_count++; - s->root->r->r = rc; - s->root->type = CT_OR; - s->root->l->type = CT_AND; - s->root->r->type = CT_AND; - s->root->l->l->type = CT_CLIP; - s->root->r->l->type = CT_CLIP; - s->root->l->l->l = s->root->l->l->r = NULL; - s->root->r->l->l = s->root->r->l->r = NULL; - s->root->l->l->a = s->root->l->l->c = 0; - s->root->r->l->a = s->root->r->l->c = 0; - s->root->l->l->b = (int)(al / 180) % 2 == 0 ? 1 : -1; - s->root->r->l->b = (int)(ar / 180) % 2 == 0 ? 1 : -1; - } else { - s->root = s->nodes + s->node_count++; - s->root->l = s->nodes + s->node_count++; - s->root->r = s->nodes + s->node_count++; - s->root->type = s->root->l->type = ar - al < 180 ? CT_AND : CT_OR; - s->root->l->l = lc; - s->root->l->r = rc; - s->root->r->type = CT_CLIP; - s->root->r->l = s->root->r->r = NULL; - s->root->r->a = s->root->r->c = 0; - s->root->r->b = ar < 180 || ar > 540 ? 1 : -1; - } - } - } -} - -// A chord line. -void -chord_line_init( - clip_ellipse_state *s, int32_t a, int32_t b, int32_t w, float al, float ar) { - ellipse_init(&s->st, a, b, a + b + 1); - - s->head = NULL; - s->node_count = 0; - - // line equation for chord - double xl = a * cos(al * M_PI / 180.0), xr = a * cos(ar * M_PI / 180.0); - double yl = b * sin(al * M_PI / 180.0), yr = b * sin(ar * M_PI / 180.0); - s->root = s->nodes + s->node_count++; - s->root->l = s->nodes + s->node_count++; - s->root->r = s->nodes + s->node_count++; - s->root->type = CT_AND; - s->root->l->type = s->root->r->type = CT_CLIP; - s->root->l->l = s->root->l->r = s->root->r->l = s->root->r->r = NULL; - s->root->l->a = yr - yl; - s->root->l->b = xl - xr; - s->root->l->c = -(s->root->l->a * xl + s->root->l->b * yl); - s->root->r->a = -s->root->l->a; - s->root->r->b = -s->root->l->b; - s->root->r->c = - 2 * w * sqrt(pow(s->root->l->a, 2.0) + pow(s->root->l->b, 2.0)) - s->root->l->c; -} - -// Pie side. -void -pie_side_init( - clip_ellipse_state *s, int32_t a, int32_t b, int32_t w, float al, float _) { - ellipse_init(&s->st, a, b, a + b + 1); - - s->head = NULL; - s->node_count = 0; - - double xl = a * cos(al * M_PI / 180.0); - double yl = b * sin(al * M_PI / 180.0); - double a1 = -yl; - double b1 = xl; - double c1 = w * sqrt(a1 * a1 + b1 * b1); - - s->root = s->nodes + s->node_count++; - s->root->type = CT_AND; - s->root->l = s->nodes + s->node_count++; - s->root->l->type = CT_AND; - - clip_node *cnode; - cnode = s->nodes + s->node_count++; - cnode->l = cnode->r = NULL; - cnode->type = CT_CLIP; - cnode->a = a1; - cnode->b = b1; - cnode->c = c1; - s->root->l->l = cnode; - cnode = s->nodes + s->node_count++; - cnode->l = cnode->r = NULL; - cnode->type = CT_CLIP; - cnode->a = -a1; - cnode->b = -b1; - cnode->c = c1; - s->root->l->r = cnode; - cnode = s->nodes + s->node_count++; - cnode->l = cnode->r = NULL; - cnode->type = CT_CLIP; - cnode->a = b1; - cnode->b = -a1; - cnode->c = 0; - s->root->r = cnode; -} - -// A chord. -void -chord_init(clip_ellipse_state *s, int32_t a, int32_t b, int32_t w, float al, float ar) { - ellipse_init(&s->st, a, b, w); - - s->head = NULL; - s->node_count = 0; - - // line equation for chord - double xl = a * cos(al * M_PI / 180.0), xr = a * cos(ar * M_PI / 180.0); - double yl = b * sin(al * M_PI / 180.0), yr = b * sin(ar * M_PI / 180.0); - s->root = s->nodes + s->node_count++; - s->root->l = s->root->r = NULL; - s->root->type = CT_CLIP; - s->root->a = yr - yl; - s->root->b = xl - xr; - s->root->c = -(s->root->a * xl + s->root->b * yl); -} - -// A pie. Can also be used to draw an arc with ugly sharp caps. -void -pie_init(clip_ellipse_state *s, int32_t a, int32_t b, int32_t w, float al, float ar) { - ellipse_init(&s->st, a, b, w); - - s->head = NULL; - s->node_count = 0; - - // line equations for pie sides - double xl = a * cos(al * M_PI / 180.0), xr = a * cos(ar * M_PI / 180.0); - double yl = b * sin(al * M_PI / 180.0), yr = b * sin(ar * M_PI / 180.0); - - clip_node *lc = s->nodes + s->node_count++; - clip_node *rc = s->nodes + s->node_count++; - lc->l = lc->r = rc->l = rc->r = NULL; - lc->type = rc->type = CT_CLIP; - lc->a = -yl; - lc->b = xl; - lc->c = 0; - rc->a = yr; - rc->b = -xr; - rc->c = 0; - - s->root = s->nodes + s->node_count++; - s->root->l = lc; - s->root->r = rc; - s->root->type = ar - al < 180 ? CT_AND : CT_OR; - - // add one more semiplane to avoid spikes - if (ar - al < 90) { - clip_node *old_root = s->root; - clip_node *spike_clipper = s->nodes + s->node_count++; - s->root = s->nodes + s->node_count++; - s->root->l = old_root; - s->root->r = spike_clipper; - s->root->type = CT_AND; - - spike_clipper->l = spike_clipper->r = NULL; - spike_clipper->type = CT_CLIP; - spike_clipper->a = (xl + xr) / 2.0; - spike_clipper->b = (yl + yr) / 2.0; - spike_clipper->c = 0; - } -} - -void -clip_ellipse_free(clip_ellipse_state *s) { - while (s->head != NULL) { - event_list *t = s->head; - s->head = s->head->next; - free(t); - } -} - -int8_t -clip_ellipse_next( - clip_ellipse_state *s, int32_t *ret_x0, int32_t *ret_y, int32_t *ret_x1) { - int32_t x0, y, x1; - while (s->head == NULL && ellipse_next(&s->st, &x0, &y, &x1) >= 0) { - if (clip_tree_do_clip(s->root, x0, y, x1, &s->head) < 0) { - return -2; - } - s->y = y; - } - if (s->head != NULL) { - *ret_y = s->y; - event_list *t = s->head; - s->head = s->head->next; - *ret_x0 = t->x; - free(t); - t = s->head; - assert(t != NULL); - s->head = s->head->next; - *ret_x1 = t->x; - free(t); - return 0; - } - return -1; -} - -static int -ellipseNew( - Imaging im, - int x0, - int y0, - int x1, - int y1, - const void *ink_, - int fill, - int width, - int op) { - DRAW *draw; - INT32 ink; - DRAWINIT(); - - int a = x1 - x0; - int b = y1 - y0; - if (a < 0 || b < 0) { - return 0; - } - if (fill) { - width = a + b; - } - - ellipse_state st; - ellipse_init(&st, a, b, width); - int32_t X0, Y, X1; - while (ellipse_next(&st, &X0, &Y, &X1) != -1) { - draw->hline(im, x0 + (X0 + a) / 2, y0 + (Y + b) / 2, x0 + (X1 + a) / 2, ink); - } - return 0; -} - -static int -clipEllipseNew( - Imaging im, - int x0, - int y0, - int x1, - int y1, - float start, - float end, - const void *ink_, - int width, - int op, - clip_ellipse_init init) { - DRAW *draw; - INT32 ink; - DRAWINIT(); - - int a = x1 - x0; - int b = y1 - y0; - if (a < 0 || b < 0) { - return 0; - } - - clip_ellipse_state st; - init(&st, a, b, width, start, end); - // debug_clip_tree(st.root, 0); - int32_t X0, Y, X1; - int next_code; - while ((next_code = clip_ellipse_next(&st, &X0, &Y, &X1)) >= 0) { - draw->hline(im, x0 + (X0 + a) / 2, y0 + (Y + b) / 2, x0 + (X1 + a) / 2, ink); - } - clip_ellipse_free(&st); - return next_code == -1 ? 0 : -1; -} -static int -arcNew( - Imaging im, - int x0, - int y0, - int x1, - int y1, - float start, - float end, - const void *ink_, - int width, - int op) { - return clipEllipseNew(im, x0, y0, x1, y1, start, end, ink_, width, op, arc_init); -} - -static int -chordNew( - Imaging im, - int x0, - int y0, - int x1, - int y1, - float start, - float end, - const void *ink_, - int width, - int op) { - return clipEllipseNew(im, x0, y0, x1, y1, start, end, ink_, width, op, chord_init); -} - -static int -chordLineNew( - Imaging im, - int x0, - int y0, - int x1, - int y1, - float start, - float end, - const void *ink_, - int width, - int op) { - return clipEllipseNew( - im, x0, y0, x1, y1, start, end, ink_, width, op, chord_line_init); -} - -static int -pieNew( - Imaging im, - int x0, - int y0, - int x1, - int y1, - float start, - float end, - const void *ink_, - int width, - int op) { - return clipEllipseNew(im, x0, y0, x1, y1, start, end, ink_, width, op, pie_init); -} - -static int -pieSideNew( - Imaging im, - int x0, - int y0, - int x1, - int y1, - float start, - const void *ink_, - int width, - int op) { - return clipEllipseNew(im, x0, y0, x1, y1, start, 0, ink_, width, op, pie_side_init); -} - -int -ImagingDrawEllipse( - Imaging im, - int x0, - int y0, - int x1, - int y1, - const void *ink, - int fill, - int width, - int op) { - return ellipseNew(im, x0, y0, x1, y1, ink, fill, width, op); -} - -int -ImagingDrawArc( - Imaging im, - int x0, - int y0, - int x1, - int y1, - float start, - float end, - const void *ink, - int width, - int op) { - normalize_angles(&start, &end); - if (start + 360 == end) { - return ImagingDrawEllipse(im, x0, y0, x1, y1, ink, 0, width, op); - } - if (start == end) { - return 0; - } - return arcNew(im, x0, y0, x1, y1, start, end, ink, width, op); -} - -int -ImagingDrawChord( - Imaging im, - int x0, - int y0, - int x1, - int y1, - float start, - float end, - const void *ink, - int fill, - int width, - int op) { - normalize_angles(&start, &end); - if (start + 360 == end) { - return ImagingDrawEllipse(im, x0, y0, x1, y1, ink, fill, width, op); - } - if (start == end) { - return 0; - } - if (fill) { - return chordNew(im, x0, y0, x1, y1, start, end, ink, x1 - x0 + y1 - y0 + 1, op); - } else { - if (chordLineNew(im, x0, y0, x1, y1, start, end, ink, width, op)) { - return -1; - } - return chordNew(im, x0, y0, x1, y1, start, end, ink, width, op); - } -} - -int -ImagingDrawPieslice( - Imaging im, - int x0, - int y0, - int x1, - int y1, - float start, - float end, - const void *ink, - int fill, - int width, - int op) { - normalize_angles(&start, &end); - if (start + 360 == end) { - return ellipseNew(im, x0, y0, x1, y1, ink, fill, width, op); - } - if (start == end) { - return 0; - } - if (fill) { - return pieNew(im, x0, y0, x1, y1, start, end, ink, x1 + y1 - x0 - y0, op); - } else { - if (pieSideNew(im, x0, y0, x1, y1, start, ink, width, op)) { - return -1; - } - if (pieSideNew(im, x0, y0, x1, y1, end, ink, width, op)) { - return -1; - } - int xc = lround((x0 + x1 - width) / 2.0), yc = lround((y0 + y1 - width) / 2.0); - ellipseNew(im, xc, yc, xc + width - 1, yc + width - 1, ink, 1, 0, op); - return pieNew(im, x0, y0, x1, y1, start, end, ink, width, op); - } -} - -/* -------------------------------------------------------------------- */ - -/* experimental level 2 ("arrow") graphics stuff. this implements - portions of the arrow api on top of the Edge structure. the - semantics are ok, except that "curve" flattens the bezier curves by - itself */ - -struct ImagingOutlineInstance { - float x0, y0; - - float x, y; - - int count; - Edge *edges; - - int size; -}; - -ImagingOutline -ImagingOutlineNew(void) { - ImagingOutline outline; - - outline = calloc(1, sizeof(struct ImagingOutlineInstance)); - if (!outline) { - return (ImagingOutline)ImagingError_MemoryError(); - } - - outline->edges = NULL; - outline->count = outline->size = 0; - - ImagingOutlineMove(outline, 0, 0); - - return outline; -} - -void -ImagingOutlineDelete(ImagingOutline outline) { - if (!outline) { - return; - } - - if (outline->edges) { - free(outline->edges); - } - - free(outline); -} - -static Edge * -allocate(ImagingOutline outline, int extra) { - Edge *e; - - if (outline->count + extra > outline->size) { - /* expand outline buffer */ - outline->size += extra + 25; - if (!outline->edges) { - /* malloc check ok, uses calloc for overflow */ - e = calloc(outline->size, sizeof(Edge)); - } else { - if (outline->size > INT_MAX / (int)sizeof(Edge)) { - return NULL; - } - /* malloc check ok, overflow checked above */ - e = realloc(outline->edges, outline->size * sizeof(Edge)); - } - if (!e) { - return NULL; - } - outline->edges = e; - } - - e = outline->edges + outline->count; - - outline->count += extra; - - return e; -} - -int -ImagingOutlineMove(ImagingOutline outline, float x0, float y0) { - outline->x = outline->x0 = x0; - outline->y = outline->y0 = y0; - - return 0; -} - -int -ImagingOutlineLine(ImagingOutline outline, float x1, float y1) { - Edge *e; - - e = allocate(outline, 1); - if (!e) { - return -1; /* out of memory */ - } - - add_edge(e, (int)outline->x, (int)outline->y, (int)x1, (int)y1); - - outline->x = x1; - outline->y = y1; - - return 0; -} - -int -ImagingOutlineCurve( - ImagingOutline outline, - float x1, - float y1, - float x2, - float y2, - float x3, - float y3) { - Edge *e; - int i; - float xo, yo; - -#define STEPS 32 - - e = allocate(outline, STEPS); - if (!e) { - return -1; /* out of memory */ - } - - xo = outline->x; - yo = outline->y; - - /* flatten the bezier segment */ - - for (i = 1; i <= STEPS; i++) { - float t = ((float)i) / STEPS; - float t2 = t * t; - float t3 = t2 * t; - - float u = 1.0F - t; - float u2 = u * u; - float u3 = u2 * u; - - float x = outline->x * u3 + 3 * (x1 * t * u2 + x2 * t2 * u) + x3 * t3 + 0.5; - float y = outline->y * u3 + 3 * (y1 * t * u2 + y2 * t2 * u) + y3 * t3 + 0.5; - - add_edge(e++, xo, yo, (int)x, (int)y); - - xo = x, yo = y; - } - - outline->x = xo; - outline->y = yo; - - return 0; -} - -int -ImagingOutlineClose(ImagingOutline outline) { - if (outline->x == outline->x0 && outline->y == outline->y0) { - return 0; - } - return ImagingOutlineLine(outline, outline->x0, outline->y0); -} - -int -ImagingOutlineTransform(ImagingOutline outline, double a[6]) { - Edge *eIn; - Edge *eOut; - int i, n; - int x0, y0, x1, y1; - int X0, Y0, X1, Y1; - - double a0 = a[0]; - double a1 = a[1]; - double a2 = a[2]; - double a3 = a[3]; - double a4 = a[4]; - double a5 = a[5]; - - eIn = outline->edges; - n = outline->count; - - eOut = allocate(outline, n); - if (!eOut) { - ImagingError_MemoryError(); - return -1; - } - - for (i = 0; i < n; i++) { - x0 = eIn->x0; - y0 = eIn->y0; - - /* FIXME: ouch! */ - if (eIn->x0 == eIn->xmin) { - x1 = eIn->xmax; - } else { - x1 = eIn->xmin; - } - if (eIn->y0 == eIn->ymin) { - y1 = eIn->ymax; - } else { - y1 = eIn->ymin; - } - - /* full moon tonight! if this doesn't work, you may need to - upgrade your compiler (make sure you have the right service - pack) */ - - X0 = (int)(a0 * x0 + a1 * y0 + a2); - Y0 = (int)(a3 * x0 + a4 * y0 + a5); - X1 = (int)(a0 * x1 + a1 * y1 + a2); - Y1 = (int)(a3 * x1 + a4 * y1 + a5); - - add_edge(eOut, X0, Y0, X1, Y1); - - eIn++; - eOut++; - } - - free(outline->edges); - - /* FIXME: ugly! */ - outline->edges = NULL; - outline->count = outline->size = 0; - - return 0; -} - -int -ImagingDrawOutline( - Imaging im, ImagingOutline outline, const void *ink_, int fill, int op) { - DRAW *draw; - INT32 ink; - - DRAWINIT(); - - draw->polygon(im, outline->count, outline->edges, ink, 0); - - return 0; -} +/* + * The Python Imaging Library. + * $Id$ + * + * a simple drawing package for the Imaging library + * + * history: + * 1996-04-13 fl Created. + * 1996-04-30 fl Added transforms and polygon support. + * 1996-08-12 fl Added filled polygons. + * 1996-11-05 fl Fixed float/int confusion in polygon filler + * 1997-07-04 fl Support 32-bit images (C++ would have been nice) + * 1998-09-09 fl Eliminated qsort casts; improved rectangle clipping + * 1998-09-10 fl Fixed fill rectangle to include lower edge (!) + * 1998-12-29 fl Added arc, chord, and pieslice primitives + * 1999-01-10 fl Added some level 2 ("arrow") stuff (experimental) + * 1999-02-06 fl Added bitmap primitive + * 1999-07-26 fl Eliminated a compiler warning + * 1999-07-31 fl Pass ink as void* instead of int + * 2002-12-10 fl Added experimental RGBA-on-RGB drawing + * 2004-09-04 fl Support simple wide lines (no joins) + * 2005-05-25 fl Fixed line width calculation + * + * Copyright (c) 1996-2006 by Fredrik Lundh + * Copyright (c) 1997-2006 by Secret Labs AB. + * + * See the README file for information on usage and redistribution. + */ + +/* FIXME: support fill/outline attribute for all filled shapes */ +/* FIXME: support zero-winding fill */ +/* FIXME: add drawing context, support affine transforms */ +/* FIXME: support clip window (and mask?) */ + +#include "Imaging.h" + +#include +#include + +#define CEIL(v) (int)ceil(v) +#define FLOOR(v) ((v) >= 0.0 ? (int)(v) : (int)floor(v)) + +#define INK8(ink) (*(UINT8 *)ink) +#define INK16(ink) (*(UINT16 *)ink) + +/* + * Rounds around zero (up=away from zero, down=towards zero) + * This guarantees that ROUND_UP|DOWN(f) == -ROUND_UP|DOWN(-f) + */ +#define ROUND_UP(f) ((int)((f) >= 0.0 ? floor((f) + 0.5F) : -floor(fabs(f) + 0.5F))) +#define ROUND_DOWN(f) ((int)((f) >= 0.0 ? ceil((f) - 0.5F) : -ceil(fabs(f) - 0.5F))) + +/* -------------------------------------------------------------------- */ +/* Primitives */ +/* -------------------------------------------------------------------- */ + +typedef struct { + /* edge descriptor for polygon engine */ + int d; + int x0, y0; + int xmin, ymin, xmax, ymax; + float dx; +} Edge; + +/* Type used in "polygon*" functions */ +typedef void (*hline_handler)(Imaging, int, int, int, int); + +static inline void +point8(Imaging im, int x, int y, int ink) { + if (x >= 0 && x < im->xsize && y >= 0 && y < im->ysize) { + if (strncmp(im->mode, "I;16", 4) == 0) { +#ifdef WORDS_BIGENDIAN + im->image8[y][x * 2] = (UINT8)(ink >> 8); + im->image8[y][x * 2 + 1] = (UINT8)ink; +#else + im->image8[y][x * 2] = (UINT8)ink; + im->image8[y][x * 2 + 1] = (UINT8)(ink >> 8); +#endif + } else { + im->image8[y][x] = (UINT8)ink; + } + } +} + +static inline void +point32(Imaging im, int x, int y, int ink) { + if (x >= 0 && x < im->xsize && y >= 0 && y < im->ysize) { + im->image32[y][x] = ink; + } +} + +static inline void +point32rgba(Imaging im, int x, int y, int ink) { + unsigned int tmp; + + if (x >= 0 && x < im->xsize && y >= 0 && y < im->ysize) { + UINT8 *out = (UINT8 *)im->image[y] + x * 4; + UINT8 *in = (UINT8 *)&ink; + 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); + } +} + +static inline void +hline8(Imaging im, int x0, int y0, int x1, int ink) { + int pixelwidth; + + if (y0 >= 0 && y0 < im->ysize) { + if (x0 < 0) { + x0 = 0; + } else if (x0 >= im->xsize) { + return; + } + if (x1 < 0) { + return; + } else if (x1 >= im->xsize) { + x1 = im->xsize - 1; + } + if (x0 <= x1) { + pixelwidth = strncmp(im->mode, "I;16", 4) == 0 ? 2 : 1; + memset( + im->image8[y0] + x0 * pixelwidth, + (UINT8)ink, + (x1 - x0 + 1) * pixelwidth); + } + } +} + +static inline void +hline32(Imaging im, int x0, int y0, int x1, int ink) { + INT32 *p; + + if (y0 >= 0 && y0 < im->ysize) { + if (x0 < 0) { + x0 = 0; + } else if (x0 >= im->xsize) { + return; + } + if (x1 < 0) { + return; + } else if (x1 >= im->xsize) { + x1 = im->xsize - 1; + } + p = im->image32[y0]; + while (x0 <= x1) { + p[x0++] = ink; + } + } +} + +static inline void +hline32rgba(Imaging im, int x0, int y0, int x1, int ink) { + unsigned int tmp; + + if (y0 >= 0 && y0 < im->ysize) { + if (x0 < 0) { + x0 = 0; + } else if (x0 >= im->xsize) { + return; + } + if (x1 < 0) { + return; + } else if (x1 >= im->xsize) { + x1 = im->xsize - 1; + } + if (x0 <= x1) { + 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); + x0++; + out += 4; + } + } + } +} + +static inline void +line8(Imaging im, int x0, int y0, int x1, int y1, int ink) { + int i, n, e; + int dx, dy; + int xs, ys; + + /* normalize coordinates */ + dx = x1 - x0; + if (dx < 0) { + dx = -dx, xs = -1; + } else { + xs = 1; + } + dy = y1 - y0; + if (dy < 0) { + dy = -dy, ys = -1; + } else { + ys = 1; + } + + n = (dx > dy) ? dx : dy; + + if (dx == 0) { + /* vertical */ + for (i = 0; i < dy; i++) { + point8(im, x0, y0, ink); + y0 += ys; + } + + } else if (dy == 0) { + /* horizontal */ + for (i = 0; i < dx; i++) { + point8(im, x0, y0, ink); + x0 += xs; + } + + } else if (dx > dy) { + /* bresenham, horizontal slope */ + n = dx; + dy += dy; + e = dy - dx; + dx += dx; + + for (i = 0; i < n; i++) { + point8(im, x0, y0, ink); + if (e >= 0) { + y0 += ys; + e -= dx; + } + e += dy; + x0 += xs; + } + + } else { + /* bresenham, vertical slope */ + n = dy; + dx += dx; + e = dx - dy; + dy += dy; + + for (i = 0; i < n; i++) { + point8(im, x0, y0, ink); + if (e >= 0) { + x0 += xs; + e -= dy; + } + e += dx; + y0 += ys; + } + } +} + +static inline void +line32(Imaging im, int x0, int y0, int x1, int y1, int ink) { + int i, n, e; + int dx, dy; + int xs, ys; + + /* normalize coordinates */ + dx = x1 - x0; + if (dx < 0) { + dx = -dx, xs = -1; + } else { + xs = 1; + } + dy = y1 - y0; + if (dy < 0) { + dy = -dy, ys = -1; + } else { + ys = 1; + } + + n = (dx > dy) ? dx : dy; + + if (dx == 0) { + /* vertical */ + for (i = 0; i < dy; i++) { + point32(im, x0, y0, ink); + y0 += ys; + } + + } else if (dy == 0) { + /* horizontal */ + for (i = 0; i < dx; i++) { + point32(im, x0, y0, ink); + x0 += xs; + } + + } else if (dx > dy) { + /* bresenham, horizontal slope */ + n = dx; + dy += dy; + e = dy - dx; + dx += dx; + + for (i = 0; i < n; i++) { + point32(im, x0, y0, ink); + if (e >= 0) { + y0 += ys; + e -= dx; + } + e += dy; + x0 += xs; + } + + } else { + /* bresenham, vertical slope */ + n = dy; + dx += dx; + e = dx - dy; + dy += dy; + + for (i = 0; i < n; i++) { + point32(im, x0, y0, ink); + if (e >= 0) { + x0 += xs; + e -= dy; + } + e += dx; + y0 += ys; + } + } +} + +static inline void +line32rgba(Imaging im, int x0, int y0, int x1, int y1, int ink) { + int i, n, e; + int dx, dy; + int xs, ys; + + /* normalize coordinates */ + dx = x1 - x0; + if (dx < 0) { + dx = -dx, xs = -1; + } else { + xs = 1; + } + dy = y1 - y0; + if (dy < 0) { + dy = -dy, ys = -1; + } else { + ys = 1; + } + + n = (dx > dy) ? dx : dy; + + if (dx == 0) { + /* vertical */ + for (i = 0; i < dy; i++) { + point32rgba(im, x0, y0, ink); + y0 += ys; + } + + } else if (dy == 0) { + /* horizontal */ + for (i = 0; i < dx; i++) { + point32rgba(im, x0, y0, ink); + x0 += xs; + } + + } else if (dx > dy) { + /* bresenham, horizontal slope */ + n = dx; + dy += dy; + e = dy - dx; + dx += dx; + + for (i = 0; i < n; i++) { + point32rgba(im, x0, y0, ink); + if (e >= 0) { + y0 += ys; + e -= dx; + } + e += dy; + x0 += xs; + } + + } else { + /* bresenham, vertical slope */ + n = dy; + dx += dx; + e = dx - dy; + dy += dy; + + for (i = 0; i < n; i++) { + point32rgba(im, x0, y0, ink); + if (e >= 0) { + x0 += xs; + e -= dy; + } + e += dx; + y0 += ys; + } + } +} + +static int +x_cmp(const void *x0, const void *x1) { + float diff = *((float *)x0) - *((float *)x1); + if (diff < 0) { + return -1; + } else if (diff > 0) { + return 1; + } else { + return 0; + } +} + +static void +draw_horizontal_lines( + Imaging im, int n, Edge *e, int ink, int *x_pos, int y, hline_handler hline) { + int i; + for (i = 0; i < n; i++) { + if (e[i].ymin == y && e[i].ymin == e[i].ymax) { + int xmax; + int xmin = e[i].xmin; + if (*x_pos != -1 && *x_pos < xmin) { + // Line would be after the current position + continue; + } + + xmax = e[i].xmax; + if (*x_pos > xmin) { + // Line would be partway through x_pos, so increase the starting point + xmin = *x_pos; + if (xmax < xmin) { + // Line would now end before it started + continue; + } + } + + (*hline)(im, xmin, e[i].ymin, xmax, ink); + *x_pos = xmax + 1; + } + } +} + +/* + * Filled polygon draw function using scan line algorithm. + */ +static inline int +polygon_generic( + Imaging im, + int n, + Edge *e, + int ink, + int eofill, + hline_handler hline, + int hasAlpha) { + Edge **edge_table; + float *xx; + int edge_count = 0; + int ymin = im->ysize - 1; + int ymax = 0; + int i, j, k; + float adjacent_line_x, adjacent_line_x_other_edge; + + if (n <= 0) { + return 0; + } + + /* Initialize the edge table and find polygon boundaries */ + /* malloc check ok, using calloc */ + edge_table = calloc(n, sizeof(Edge *)); + if (!edge_table) { + return -1; + } + + for (i = 0; i < n; i++) { + if (ymin > e[i].ymin) { + ymin = e[i].ymin; + } + if (ymax < e[i].ymax) { + ymax = e[i].ymax; + } + if (e[i].ymin == e[i].ymax) { + if (hasAlpha != 1) { + (*hline)(im, e[i].xmin, e[i].ymin, e[i].xmax, ink); + } + continue; + } + edge_table[edge_count++] = (e + i); + } + if (ymin < 0) { + ymin = 0; + } + if (ymax > im->ysize) { + ymax = im->ysize; + } + + /* Process the edge table with a scan line searching for intersections */ + /* malloc check ok, using calloc */ + xx = calloc(edge_count * 2, sizeof(float)); + if (!xx) { + free(edge_table); + return -1; + } + for (; ymin <= ymax; ymin++) { + j = 0; + for (i = 0; i < edge_count; i++) { + Edge *current = edge_table[i]; + if (ymin >= current->ymin && ymin <= current->ymax) { + xx[j++] = (ymin - current->y0) * current->dx + current->x0; + + if (ymin == current->ymax && ymin < ymax) { + // Needed to draw consistent polygons + xx[j] = xx[j - 1]; + j++; + } else if (current->dx != 0 && roundf(xx[j - 1]) == xx[j - 1]) { + // Connect discontiguous corners + for (k = 0; k < i; k++) { + Edge *other_edge = edge_table[k]; + if ((current->dx > 0 && other_edge->dx <= 0) || + (current->dx < 0 && other_edge->dx >= 0)) { + continue; + } + // Check if the two edges join to make a corner + if (((ymin == current->ymin && ymin == other_edge->ymin) || + (ymin == current->ymax && ymin == other_edge->ymax)) && + xx[j - 1] == (ymin - other_edge->y0) * other_edge->dx + + other_edge->x0) { + // Determine points from the edges on the next row + // Or if this is the last row, check the previous row + int offset = ymin == ymax ? -1 : 1; + adjacent_line_x = + (ymin + offset - current->y0) * current->dx + + current->x0; + adjacent_line_x_other_edge = + (ymin + offset - other_edge->y0) * other_edge->dx + + other_edge->x0; + if (ymin == current->ymax) { + if (current->dx > 0) { + xx[k] = fmax( + adjacent_line_x, + adjacent_line_x_other_edge) + + 1; + } else { + xx[k] = fmin( + adjacent_line_x, + adjacent_line_x_other_edge) - + 1; + } + } else { + if (current->dx > 0) { + xx[k] = fmin( + adjacent_line_x, adjacent_line_x_other_edge); + } else { + xx[k] = fmax( + adjacent_line_x, + adjacent_line_x_other_edge) + + 1; + } + } + break; + } + } + } + } + } + qsort(xx, j, sizeof(float), x_cmp); + if (hasAlpha == 1) { + int x_pos = j == 0 ? -1 : 0; + for (i = 1; i < j; i += 2) { + int x_end = ROUND_DOWN(xx[i]); + if (x_end < x_pos) { + // Line would be before the current position + continue; + } + draw_horizontal_lines(im, n, e, ink, &x_pos, ymin, hline); + if (x_end < x_pos) { + // Line would be before the current position + continue; + } + + int x_start = ROUND_UP(xx[i - 1]); + if (x_pos > x_start) { + // Line would be partway through x_pos, so increase the starting + // point + x_start = x_pos; + if (x_end < x_start) { + // Line would now end before it started + continue; + } + } + (*hline)(im, x_start, ymin, x_end, ink); + x_pos = x_end + 1; + } + draw_horizontal_lines(im, n, e, ink, &x_pos, ymin, hline); + } else { + for (i = 1; i < j; i += 2) { + (*hline)(im, ROUND_UP(xx[i - 1]), ymin, ROUND_DOWN(xx[i]), ink); + } + } + } + + free(xx); + free(edge_table); + 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); */ + + if (x0 <= x1) { + e->xmin = x0, e->xmax = x1; + } else { + e->xmin = x1, e->xmax = x0; + } + + if (y0 <= y1) { + e->ymin = y0, e->ymax = y1; + } else { + e->ymin = y1, e->ymax = y0; + } + + if (y0 == y1) { + e->d = 0; + e->dx = 0.0; + } else { + e->dx = ((float)(x1 - x0)) / (y1 - y0); + if (y0 == e->ymin) { + e->d = 1; + } else { + e->d = -1; + } + } + + e->x0 = x0; + e->y0 = y0; +} + +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 (*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}; + +/* -------------------------------------------------------------------- */ +/* Interface */ +/* -------------------------------------------------------------------- */ + +#define DRAWINIT() \ + if (im->image8) { \ + draw = &draw8; \ + if (strncmp(im->mode, "I;16", 4) == 0) { \ + ink = INK16(ink_); \ + } else { \ + ink = INK8(ink_); \ + } \ + } else { \ + draw = (op) ? &draw32rgba : &draw32; \ + memcpy(&ink, ink_, sizeof(ink)); \ + } + +int +ImagingDrawPoint(Imaging im, int x0, int y0, const void *ink_, int op) { + DRAW *draw; + INT32 ink; + + DRAWINIT(); + + draw->point(im, x0, y0, ink); + + return 0; +} + +int +ImagingDrawLine(Imaging im, int x0, int y0, int x1, int y1, const void *ink_, int op) { + DRAW *draw; + INT32 ink; + + DRAWINIT(); + + draw->line(im, x0, y0, x1, y1, ink); + + return 0; +} + +int +ImagingDrawWideLine( + Imaging im, int x0, int y0, int x1, int y1, const void *ink_, int width, int op) { + DRAW *draw; + INT32 ink; + int dx, dy; + double big_hypotenuse, small_hypotenuse, ratio_max, ratio_min; + int dxmin, dxmax, dymin, dymax; + Edge e[4]; + + DRAWINIT(); + + dx = x1 - x0; + dy = y1 - y0; + if (dx == 0 && dy == 0) { + draw->point(im, x0, y0, ink); + return 0; + } + + big_hypotenuse = hypot(dx, dy); + small_hypotenuse = (width - 1) / 2.0; + ratio_max = ROUND_UP(small_hypotenuse) / big_hypotenuse; + ratio_min = ROUND_DOWN(small_hypotenuse) / big_hypotenuse; + + dxmin = ROUND_DOWN(ratio_min * dy); + dxmax = ROUND_DOWN(ratio_max * dy); + dymin = ROUND_DOWN(ratio_min * dx); + dymax = ROUND_DOWN(ratio_max * dx); + { + int vertices[4][2] = { + {x0 - dxmin, y0 + dymax}, + {x1 - dxmin, y1 + dymax}, + {x1 + dxmax, y1 - dymin}, + {x0 + dxmax, y0 - dymin}}; + + add_edge(e + 0, vertices[0][0], vertices[0][1], vertices[1][0], vertices[1][1]); + add_edge(e + 1, vertices[1][0], vertices[1][1], vertices[2][0], vertices[2][1]); + add_edge(e + 2, vertices[2][0], vertices[2][1], vertices[3][0], vertices[3][1]); + add_edge(e + 3, vertices[3][0], vertices[3][1], vertices[0][0], vertices[0][1]); + + draw->polygon(im, 4, e, ink, 0); + } + return 0; +} + +int +ImagingDrawRectangle( + Imaging im, + int x0, + int y0, + int x1, + int y1, + const void *ink_, + int fill, + int width, + int op) { + int i; + int y; + int tmp; + DRAW *draw; + INT32 ink; + + DRAWINIT(); + + if (y0 > y1) { + tmp = y0, y0 = y1, y1 = tmp; + } + + if (fill) { + if (y0 < 0) { + y0 = 0; + } else if (y0 >= im->ysize) { + return 0; + } + + if (y1 < 0) { + return 0; + } else if (y1 > im->ysize) { + y1 = im->ysize; + } + + for (y = y0; y <= y1; y++) { + draw->hline(im, x0, y, x1, ink); + } + + } else { + /* outline */ + if (width == 0) { + width = 1; + } + for (i = 0; i < width; i++) { + draw->hline(im, x0, y0 + i, x1, ink); + draw->hline(im, x0, y1 - i, x1, ink); + draw->line(im, x1 - i, y0 + width, x1 - i, y1 - width + 1, ink); + draw->line(im, x0 + i, y0 + width, x0 + i, y1 - width + 1, ink); + } + } + + return 0; +} + +int +ImagingDrawPolygon( + Imaging im, int count, int *xy, const void *ink_, int fill, int width, int op) { + int i, n, x0, y0, x1, y1; + DRAW *draw; + INT32 ink; + + if (count <= 0) { + return 0; + } + + DRAWINIT(); + + if (fill) { + /* Build edge list */ + /* malloc check ok, using calloc */ + Edge *e = calloc(count, sizeof(Edge)); + if (!e) { + (void)ImagingError_MemoryError(); + return -1; + } + for (i = n = 0; i < count - 1; i++) { + x0 = xy[i * 2]; + y0 = xy[i * 2 + 1]; + x1 = xy[i * 2 + 2]; + y1 = xy[i * 2 + 3]; + if (y0 == y1 && i != 0 && y0 == xy[i * 2 - 1]) { + // This is a horizontal line, + // that immediately follows another horizontal line + Edge *last_e = &e[n - 1]; + if (x1 > x0 && x0 > xy[i * 2 - 2]) { + // They are both increasing in x + last_e->xmax = x1; + continue; + } else if (x1 < x0 && x0 < xy[i * 2 - 2]) { + // They are both decreasing in x + last_e->xmin = x1; + continue; + } + } + add_edge(&e[n++], x0, y0, x1, y1); + } + if (xy[i * 2] != xy[0] || xy[i * 2 + 1] != xy[1]) { + add_edge(&e[n++], xy[i * 2], xy[i * 2 + 1], xy[0], xy[1]); + } + draw->polygon(im, n, e, ink, 0); + free(e); + + } else { + /* Outline */ + if (width == 1) { + for (i = 0; i < count - 1; i++) { + draw->line( + im, xy[i * 2], xy[i * 2 + 1], xy[i * 2 + 2], xy[i * 2 + 3], ink); + } + draw->line(im, xy[i * 2], xy[i * 2 + 1], xy[0], xy[1], ink); + } else { + for (i = 0; i < count - 1; i++) { + ImagingDrawWideLine( + im, + xy[i * 2], + xy[i * 2 + 1], + xy[i * 2 + 2], + xy[i * 2 + 3], + ink_, + width, + op); + } + ImagingDrawWideLine( + im, xy[i * 2], xy[i * 2 + 1], xy[0], xy[1], ink_, width, op); + } + } + + return 0; +} + +int +ImagingDrawBitmap(Imaging im, int x0, int y0, Imaging bitmap, const void *ink, int op) { + return ImagingFill2( + im, ink, bitmap, x0, y0, x0 + bitmap->xsize, y0 + bitmap->ysize); +} + +/* -------------------------------------------------------------------- */ +/* standard shapes */ + +// Imagine 2D plane and ellipse with center in (0, 0) and semi-major axes a and b. +// Then quarter_* stuff approximates its top right quarter (x, y >= 0) with integer +// points from set {(2x+x0, 2y+y0) | x,y in Z} where x0, y0 are from {0, 1} and +// are such that point (a, b) is in the set. + +typedef struct { + int32_t a, b, cx, cy, ex, ey; + int64_t a2, b2, a2b2; + int8_t finished; +} quarter_state; + +void +quarter_init(quarter_state *s, int32_t a, int32_t b) { + if (a < 0 || b < 0) { + s->finished = 1; + } else { + s->a = a; + s->b = b; + s->cx = a; + s->cy = b % 2; + s->ex = a % 2; + s->ey = b; + s->a2 = a * a; + s->b2 = b * b; + s->a2b2 = s->a2 * s->b2; + s->finished = 0; + } +} + +// deviation of the point from ellipse curve, basically a substitution +// of the point into the ellipse equation +int64_t +quarter_delta(quarter_state *s, int64_t x, int64_t y) { + return llabs(s->a2 * y * y + s->b2 * x * x - s->a2b2); +} + +int8_t +quarter_next(quarter_state *s, int32_t *ret_x, int32_t *ret_y) { + if (s->finished) { + return -1; + } + *ret_x = s->cx; + *ret_y = s->cy; + if (s->cx == s->ex && s->cy == s->ey) { + s->finished = 1; + } else { + // Bresenham's algorithm, possible optimization: only consider 2 of 3 + // next points depending on current slope + int32_t nx = s->cx; + int32_t ny = s->cy + 2; + int64_t ndelta = quarter_delta(s, nx, ny); + if (nx > 1) { + int64_t newdelta = quarter_delta(s, s->cx - 2, s->cy + 2); + if (ndelta > newdelta) { + nx = s->cx - 2; + ny = s->cy + 2; + ndelta = newdelta; + } + newdelta = quarter_delta(s, s->cx - 2, s->cy); + if (ndelta > newdelta) { + nx = s->cx - 2; + ny = s->cy; + } + } + s->cx = nx; + s->cy = ny; + } + return 0; +} + +// quarter_* stuff can "draw" a quarter of an ellipse with thickness 1, great. +// Now we use ellipse_* stuff to join all four quarters of two different sized +// ellipses and receive horizontal segments of a complete ellipse with +// specified thickness. +// +// Still using integer grid with step 2 at this point (like in quarter_*) +// to ease angle clipping in future. + +typedef struct { + quarter_state st_o, st_i; + int32_t py, pl, pr; + int32_t cy[4], cl[4], cr[4]; + int8_t bufcnt; + int8_t finished; + int8_t leftmost; +} ellipse_state; + +void +ellipse_init(ellipse_state *s, int32_t a, int32_t b, int32_t w) { + s->bufcnt = 0; + s->leftmost = a % 2; + quarter_init(&s->st_o, a, b); + if (w < 1 || quarter_next(&s->st_o, &s->pr, &s->py) == -1) { + s->finished = 1; + } else { + s->finished = 0; + quarter_init(&s->st_i, a - 2 * (w - 1), b - 2 * (w - 1)); + s->pl = s->leftmost; + } +} + +int8_t +ellipse_next(ellipse_state *s, int32_t *ret_x0, int32_t *ret_y, int32_t *ret_x1) { + if (s->bufcnt == 0) { + if (s->finished) { + return -1; + } + int32_t y = s->py; + int32_t l = s->pl; + int32_t r = s->pr; + int32_t cx = 0, cy = 0; + int8_t next_ret; + while ((next_ret = quarter_next(&s->st_o, &cx, &cy)) != -1 && cy <= y) { + } + if (next_ret == -1) { + s->finished = 1; + } else { + s->pr = cx; + s->py = cy; + } + while ((next_ret = quarter_next(&s->st_i, &cx, &cy)) != -1 && cy <= y) { + l = cx; + } + s->pl = next_ret == -1 ? s->leftmost : cx; + + if ((l > 0 || l < r) && y > 0) { + s->cl[s->bufcnt] = l == 0 ? 2 : l; + s->cy[s->bufcnt] = y; + s->cr[s->bufcnt] = r; + ++s->bufcnt; + } + if (y > 0) { + s->cl[s->bufcnt] = -r; + s->cy[s->bufcnt] = y; + s->cr[s->bufcnt] = -l; + ++s->bufcnt; + } + if (l > 0 || l < r) { + s->cl[s->bufcnt] = l == 0 ? 2 : l; + s->cy[s->bufcnt] = -y; + s->cr[s->bufcnt] = r; + ++s->bufcnt; + } + s->cl[s->bufcnt] = -r; + s->cy[s->bufcnt] = -y; + s->cr[s->bufcnt] = -l; + ++s->bufcnt; + } + --s->bufcnt; + *ret_x0 = s->cl[s->bufcnt]; + *ret_y = s->cy[s->bufcnt]; + *ret_x1 = s->cr[s->bufcnt]; + return 0; +} + +// Clipping tree consists of half-plane clipping nodes and combining nodes. +// We can throw a horizontal segment in such a tree and collect an ordered set +// of resulting disjoint clipped segments organized into a sorted linked list +// of their end points. +typedef enum { + CT_AND, // intersection + CT_OR, // union + CT_CLIP // half-plane clipping +} clip_type; + +typedef struct clip_node { + clip_type type; + double a, b, c; // half-plane coeffs, only used in clipping nodes + struct clip_node *l; // child pointers, are only non-NULL in combining nodes + struct clip_node *r; +} clip_node; + +// Linked list for the ends of the clipped horizontal segments. +// Since the segment is always horizontal, we don't need to store Y coordinate. +typedef struct event_list { + int32_t x; + int8_t type; // used internally, 1 for the left end (smaller X), -1 for the + // right end; pointless in output since the output segments + // are disjoint, therefore the types would always come in pairs + // and interchange (1 -1 1 -1 ...) + struct event_list *next; +} event_list; + +// Mirrors all the clipping nodes of the tree relative to the y = x line. +void +clip_tree_transpose(clip_node *root) { + if (root != NULL) { + if (root->type == CT_CLIP) { + double t = root->a; + root->a = root->b; + root->b = t; + } + clip_tree_transpose(root->l); + clip_tree_transpose(root->r); + } +} + +// Outputs a sequence of open-close events (types -1 and 1) for +// non-intersecting segments sorted by X coordinate. +// Combining nodes (AND, OR) may also accept sequences for intersecting +// segments, i.e. something like correct bracket sequences. +int +clip_tree_do_clip( + clip_node *root, int32_t x0, int32_t y, int32_t x1, event_list **ret) { + if (root == NULL) { + event_list *start = malloc(sizeof(event_list)); + if (!start) { + ImagingError_MemoryError(); + return -1; + } + event_list *end = malloc(sizeof(event_list)); + if (!end) { + free(start); + ImagingError_MemoryError(); + return -1; + } + start->x = x0; + start->type = 1; + start->next = end; + end->x = x1; + end->type = -1; + end->next = NULL; + *ret = start; + return 0; + } + if (root->type == CT_CLIP) { + double eps = 1e-9; + double A = root->a; + double B = root->b; + double C = root->c; + if (fabs(A) < eps) { + if (B * y + C < -eps) { + x0 = 1; + x1 = 0; + } + } else { + // X of intersection + double ix = -(B * y + C) / A; + if (A * x0 + B * y + C < eps) { + x0 = lround(fmax(x0, ix)); + } + if (A * x1 + B * y + C < eps) { + x1 = lround(fmin(x1, ix)); + } + } + if (x0 <= x1) { + event_list *start = malloc(sizeof(event_list)); + if (!start) { + ImagingError_MemoryError(); + return -1; + } + event_list *end = malloc(sizeof(event_list)); + if (!end) { + free(start); + ImagingError_MemoryError(); + return -1; + } + start->x = x0; + start->type = 1; + start->next = end; + end->x = x1; + end->type = -1; + end->next = NULL; + *ret = start; + } else { + *ret = NULL; + } + return 0; + } + if (root->type == CT_OR || root->type == CT_AND) { + event_list *l1; + event_list *l2; + if (clip_tree_do_clip(root->l, x0, y, x1, &l1) < 0) { + return -1; + } + if (clip_tree_do_clip(root->r, x0, y, x1, &l2) < 0) { + while (l1) { + l2 = l1->next; + free(l1); + l1 = l2; + } + return -1; + } + *ret = NULL; + event_list *tail = NULL; + int32_t k1 = 0; + int32_t k2 = 0; + while (l1 != NULL || l2 != NULL) { + event_list *t; + if (l2 == NULL || + (l1 != NULL && + (l1->x < l2->x || (l1->x == l2->x && l1->type > l2->type)))) { + t = l1; + k1 += t->type; + assert(k1 >= 0); + l1 = l1->next; + } else { + t = l2; + k2 += t->type; + assert(k2 >= 0); + l2 = l2->next; + } + t->next = NULL; + if ((root->type == CT_OR && + ((t->type == 1 && (tail == NULL || tail->type == -1)) || + (t->type == -1 && k1 == 0 && k2 == 0))) || + (root->type == CT_AND && + ((t->type == 1 && (tail == NULL || tail->type == -1) && k1 > 0 && + k2 > 0) || + (t->type == -1 && tail != NULL && tail->type == 1 && + (k1 == 0 || k2 == 0))))) { + if (tail == NULL) { + *ret = t; + } else { + tail->next = t; + } + tail = t; + } else { + free(t); + } + } + return 0; + } + *ret = NULL; + return 0; +} + +// One more layer of processing on top of the regular ellipse. +// Uses the clipping tree. +// Used for producing ellipse derivatives such as arc, chord, pie, etc. +typedef struct { + ellipse_state st; + clip_node *root; + clip_node nodes[7]; + int32_t node_count; + event_list *head; + int32_t y; +} clip_ellipse_state; + +typedef void (*clip_ellipse_init)( + clip_ellipse_state *, int32_t, int32_t, int32_t, float, float); + +void +debug_clip_tree(clip_node *root, int space) { + if (root == NULL) { + return; + } + if (root->type == CT_CLIP) { + int t = space; + while (t--) { + fputc(' ', stderr); + } + fprintf(stderr, "clip %+fx%+fy%+f > 0\n", root->a, root->b, root->c); + } else { + debug_clip_tree(root->l, space + 2); + int t = space; + while (t--) { + fputc(' ', stderr); + } + fprintf(stderr, "%s\n", root->type == CT_AND ? "and" : "or"); + debug_clip_tree(root->r, space + 2); + } + if (space == 0) { + fputc('\n', stderr); + } +} + +// Resulting angles will satisfy 0 <= al < 360, al <= ar <= al + 360 +void +normalize_angles(float *al, float *ar) { + if (*ar - *al >= 360) { + *al = 0; + *ar = 360; + } else { + *al = fmod(*al < 0 ? 360 - (fmod(-*al, 360)) : *al, 360); + *ar = *al + fmod(*ar < *al ? 360 - fmod(*al - *ar, 360) : *ar - *al, 360); + } +} + +// An arc with caps orthogonal to the ellipse curve. +void +arc_init(clip_ellipse_state *s, int32_t a, int32_t b, int32_t w, float al, float ar) { + if (a < b) { + // transpose the coordinate system + arc_init(s, b, a, w, 90 - ar, 90 - al); + ellipse_init(&s->st, a, b, w); + clip_tree_transpose(s->root); + } else { + // a >= b, based on "wide" ellipse + ellipse_init(&s->st, a, b, w); + + s->head = NULL; + s->node_count = 0; + normalize_angles(&al, &ar); + + // building clipping tree, a lot of different cases + if (ar == al + 360) { + s->root = NULL; + } else { + clip_node *lc = s->nodes + s->node_count++; + clip_node *rc = s->nodes + s->node_count++; + lc->l = lc->r = rc->l = rc->r = NULL; + lc->type = rc->type = CT_CLIP; + lc->a = -a * sin(al * M_PI / 180.0); + lc->b = b * cos(al * M_PI / 180.0); + lc->c = (a * a - b * b) * sin(al * M_PI / 90.0) / 2.0; + rc->a = a * sin(ar * M_PI / 180.0); + rc->b = -b * cos(ar * M_PI / 180.0); + rc->c = (b * b - a * a) * sin(ar * M_PI / 90.0) / 2.0; + if (fmod(al, 180) == 0 || fmod(ar, 180) == 0) { + s->root = s->nodes + s->node_count++; + s->root->l = lc; + s->root->r = rc; + s->root->type = ar - al < 180 ? CT_AND : CT_OR; + } else if (((int)(al / 180) + (int)(ar / 180)) % 2 == 1) { + s->root = s->nodes + s->node_count++; + s->root->l = s->nodes + s->node_count++; + s->root->l->l = s->nodes + s->node_count++; + s->root->l->r = lc; + s->root->r = s->nodes + s->node_count++; + s->root->r->l = s->nodes + s->node_count++; + s->root->r->r = rc; + s->root->type = CT_OR; + s->root->l->type = CT_AND; + s->root->r->type = CT_AND; + s->root->l->l->type = CT_CLIP; + s->root->r->l->type = CT_CLIP; + s->root->l->l->l = s->root->l->l->r = NULL; + s->root->r->l->l = s->root->r->l->r = NULL; + s->root->l->l->a = s->root->l->l->c = 0; + s->root->r->l->a = s->root->r->l->c = 0; + s->root->l->l->b = (int)(al / 180) % 2 == 0 ? 1 : -1; + s->root->r->l->b = (int)(ar / 180) % 2 == 0 ? 1 : -1; + } else { + s->root = s->nodes + s->node_count++; + s->root->l = s->nodes + s->node_count++; + s->root->r = s->nodes + s->node_count++; + s->root->type = s->root->l->type = ar - al < 180 ? CT_AND : CT_OR; + s->root->l->l = lc; + s->root->l->r = rc; + s->root->r->type = CT_CLIP; + s->root->r->l = s->root->r->r = NULL; + s->root->r->a = s->root->r->c = 0; + s->root->r->b = ar < 180 || ar > 540 ? 1 : -1; + } + } + } +} + +// A chord line. +void +chord_line_init( + clip_ellipse_state *s, int32_t a, int32_t b, int32_t w, float al, float ar) { + ellipse_init(&s->st, a, b, a + b + 1); + + s->head = NULL; + s->node_count = 0; + + // line equation for chord + double xl = a * cos(al * M_PI / 180.0), xr = a * cos(ar * M_PI / 180.0); + double yl = b * sin(al * M_PI / 180.0), yr = b * sin(ar * M_PI / 180.0); + s->root = s->nodes + s->node_count++; + s->root->l = s->nodes + s->node_count++; + s->root->r = s->nodes + s->node_count++; + s->root->type = CT_AND; + s->root->l->type = s->root->r->type = CT_CLIP; + s->root->l->l = s->root->l->r = s->root->r->l = s->root->r->r = NULL; + s->root->l->a = yr - yl; + s->root->l->b = xl - xr; + s->root->l->c = -(s->root->l->a * xl + s->root->l->b * yl); + s->root->r->a = -s->root->l->a; + s->root->r->b = -s->root->l->b; + s->root->r->c = + 2 * w * sqrt(pow(s->root->l->a, 2.0) + pow(s->root->l->b, 2.0)) - s->root->l->c; +} + +// Pie side. +void +pie_side_init( + clip_ellipse_state *s, int32_t a, int32_t b, int32_t w, float al, float _) { + ellipse_init(&s->st, a, b, a + b + 1); + + s->head = NULL; + s->node_count = 0; + + double xl = a * cos(al * M_PI / 180.0); + double yl = b * sin(al * M_PI / 180.0); + double a1 = -yl; + double b1 = xl; + double c1 = w * sqrt(a1 * a1 + b1 * b1); + + s->root = s->nodes + s->node_count++; + s->root->type = CT_AND; + s->root->l = s->nodes + s->node_count++; + s->root->l->type = CT_AND; + + clip_node *cnode; + cnode = s->nodes + s->node_count++; + cnode->l = cnode->r = NULL; + cnode->type = CT_CLIP; + cnode->a = a1; + cnode->b = b1; + cnode->c = c1; + s->root->l->l = cnode; + cnode = s->nodes + s->node_count++; + cnode->l = cnode->r = NULL; + cnode->type = CT_CLIP; + cnode->a = -a1; + cnode->b = -b1; + cnode->c = c1; + s->root->l->r = cnode; + cnode = s->nodes + s->node_count++; + cnode->l = cnode->r = NULL; + cnode->type = CT_CLIP; + cnode->a = b1; + cnode->b = -a1; + cnode->c = 0; + s->root->r = cnode; +} + +// A chord. +void +chord_init(clip_ellipse_state *s, int32_t a, int32_t b, int32_t w, float al, float ar) { + ellipse_init(&s->st, a, b, w); + + s->head = NULL; + s->node_count = 0; + + // line equation for chord + double xl = a * cos(al * M_PI / 180.0), xr = a * cos(ar * M_PI / 180.0); + double yl = b * sin(al * M_PI / 180.0), yr = b * sin(ar * M_PI / 180.0); + s->root = s->nodes + s->node_count++; + s->root->l = s->root->r = NULL; + s->root->type = CT_CLIP; + s->root->a = yr - yl; + s->root->b = xl - xr; + s->root->c = -(s->root->a * xl + s->root->b * yl); +} + +// A pie. Can also be used to draw an arc with ugly sharp caps. +void +pie_init(clip_ellipse_state *s, int32_t a, int32_t b, int32_t w, float al, float ar) { + ellipse_init(&s->st, a, b, w); + + s->head = NULL; + s->node_count = 0; + + // line equations for pie sides + double xl = a * cos(al * M_PI / 180.0), xr = a * cos(ar * M_PI / 180.0); + double yl = b * sin(al * M_PI / 180.0), yr = b * sin(ar * M_PI / 180.0); + + clip_node *lc = s->nodes + s->node_count++; + clip_node *rc = s->nodes + s->node_count++; + lc->l = lc->r = rc->l = rc->r = NULL; + lc->type = rc->type = CT_CLIP; + lc->a = -yl; + lc->b = xl; + lc->c = 0; + rc->a = yr; + rc->b = -xr; + rc->c = 0; + + s->root = s->nodes + s->node_count++; + s->root->l = lc; + s->root->r = rc; + s->root->type = ar - al < 180 ? CT_AND : CT_OR; + + // add one more semiplane to avoid spikes + if (ar - al < 90) { + clip_node *old_root = s->root; + clip_node *spike_clipper = s->nodes + s->node_count++; + s->root = s->nodes + s->node_count++; + s->root->l = old_root; + s->root->r = spike_clipper; + s->root->type = CT_AND; + + spike_clipper->l = spike_clipper->r = NULL; + spike_clipper->type = CT_CLIP; + spike_clipper->a = (xl + xr) / 2.0; + spike_clipper->b = (yl + yr) / 2.0; + spike_clipper->c = 0; + } +} + +void +clip_ellipse_free(clip_ellipse_state *s) { + while (s->head != NULL) { + event_list *t = s->head; + s->head = s->head->next; + free(t); + } +} + +int8_t +clip_ellipse_next( + clip_ellipse_state *s, int32_t *ret_x0, int32_t *ret_y, int32_t *ret_x1) { + int32_t x0, y, x1; + while (s->head == NULL && ellipse_next(&s->st, &x0, &y, &x1) >= 0) { + if (clip_tree_do_clip(s->root, x0, y, x1, &s->head) < 0) { + return -2; + } + s->y = y; + } + if (s->head != NULL) { + *ret_y = s->y; + event_list *t = s->head; + s->head = s->head->next; + *ret_x0 = t->x; + free(t); + t = s->head; + assert(t != NULL); + s->head = s->head->next; + *ret_x1 = t->x; + free(t); + return 0; + } + return -1; +} + +static int +ellipseNew( + Imaging im, + int x0, + int y0, + int x1, + int y1, + const void *ink_, + int fill, + int width, + int op) { + DRAW *draw; + INT32 ink; + DRAWINIT(); + + int a = x1 - x0; + int b = y1 - y0; + if (a < 0 || b < 0) { + return 0; + } + if (fill) { + width = a + b; + } + + ellipse_state st; + ellipse_init(&st, a, b, width); + int32_t X0, Y, X1; + while (ellipse_next(&st, &X0, &Y, &X1) != -1) { + draw->hline(im, x0 + (X0 + a) / 2, y0 + (Y + b) / 2, x0 + (X1 + a) / 2, ink); + } + return 0; +} + +static int +clipEllipseNew( + Imaging im, + int x0, + int y0, + int x1, + int y1, + float start, + float end, + const void *ink_, + int width, + int op, + clip_ellipse_init init) { + DRAW *draw; + INT32 ink; + DRAWINIT(); + + int a = x1 - x0; + int b = y1 - y0; + if (a < 0 || b < 0) { + return 0; + } + + clip_ellipse_state st; + init(&st, a, b, width, start, end); + // debug_clip_tree(st.root, 0); + int32_t X0, Y, X1; + int next_code; + while ((next_code = clip_ellipse_next(&st, &X0, &Y, &X1)) >= 0) { + draw->hline(im, x0 + (X0 + a) / 2, y0 + (Y + b) / 2, x0 + (X1 + a) / 2, ink); + } + clip_ellipse_free(&st); + return next_code == -1 ? 0 : -1; +} +static int +arcNew( + Imaging im, + int x0, + int y0, + int x1, + int y1, + float start, + float end, + const void *ink_, + int width, + int op) { + return clipEllipseNew(im, x0, y0, x1, y1, start, end, ink_, width, op, arc_init); +} + +static int +chordNew( + Imaging im, + int x0, + int y0, + int x1, + int y1, + float start, + float end, + const void *ink_, + int width, + int op) { + return clipEllipseNew(im, x0, y0, x1, y1, start, end, ink_, width, op, chord_init); +} + +static int +chordLineNew( + Imaging im, + int x0, + int y0, + int x1, + int y1, + float start, + float end, + const void *ink_, + int width, + int op) { + return clipEllipseNew( + im, x0, y0, x1, y1, start, end, ink_, width, op, chord_line_init); +} + +static int +pieNew( + Imaging im, + int x0, + int y0, + int x1, + int y1, + float start, + float end, + const void *ink_, + int width, + int op) { + return clipEllipseNew(im, x0, y0, x1, y1, start, end, ink_, width, op, pie_init); +} + +static int +pieSideNew( + Imaging im, + int x0, + int y0, + int x1, + int y1, + float start, + const void *ink_, + int width, + int op) { + return clipEllipseNew(im, x0, y0, x1, y1, start, 0, ink_, width, op, pie_side_init); +} + +int +ImagingDrawEllipse( + Imaging im, + int x0, + int y0, + int x1, + int y1, + const void *ink, + int fill, + int width, + int op) { + return ellipseNew(im, x0, y0, x1, y1, ink, fill, width, op); +} + +int +ImagingDrawArc( + Imaging im, + int x0, + int y0, + int x1, + int y1, + float start, + float end, + const void *ink, + int width, + int op) { + normalize_angles(&start, &end); + if (start + 360 == end) { + return ImagingDrawEllipse(im, x0, y0, x1, y1, ink, 0, width, op); + } + if (start == end) { + return 0; + } + return arcNew(im, x0, y0, x1, y1, start, end, ink, width, op); +} + +int +ImagingDrawChord( + Imaging im, + int x0, + int y0, + int x1, + int y1, + float start, + float end, + const void *ink, + int fill, + int width, + int op) { + normalize_angles(&start, &end); + if (start + 360 == end) { + return ImagingDrawEllipse(im, x0, y0, x1, y1, ink, fill, width, op); + } + if (start == end) { + return 0; + } + if (fill) { + return chordNew(im, x0, y0, x1, y1, start, end, ink, x1 - x0 + y1 - y0 + 1, op); + } else { + if (chordLineNew(im, x0, y0, x1, y1, start, end, ink, width, op)) { + return -1; + } + return chordNew(im, x0, y0, x1, y1, start, end, ink, width, op); + } +} + +int +ImagingDrawPieslice( + Imaging im, + int x0, + int y0, + int x1, + int y1, + float start, + float end, + const void *ink, + int fill, + int width, + int op) { + normalize_angles(&start, &end); + if (start + 360 == end) { + return ellipseNew(im, x0, y0, x1, y1, ink, fill, width, op); + } + if (start == end) { + return 0; + } + if (fill) { + return pieNew(im, x0, y0, x1, y1, start, end, ink, x1 + y1 - x0 - y0, op); + } else { + if (pieSideNew(im, x0, y0, x1, y1, start, ink, width, op)) { + return -1; + } + if (pieSideNew(im, x0, y0, x1, y1, end, ink, width, op)) { + return -1; + } + int xc = lround((x0 + x1 - width) / 2.0), yc = lround((y0 + y1 - width) / 2.0); + ellipseNew(im, xc, yc, xc + width - 1, yc + width - 1, ink, 1, 0, op); + return pieNew(im, x0, y0, x1, y1, start, end, ink, width, op); + } +} + +/* -------------------------------------------------------------------- */ + +/* experimental level 2 ("arrow") graphics stuff. this implements + portions of the arrow api on top of the Edge structure. the + semantics are ok, except that "curve" flattens the bezier curves by + itself */ + +struct ImagingOutlineInstance { + float x0, y0; + + float x, y; + + int count; + Edge *edges; + + int size; +}; + +ImagingOutline +ImagingOutlineNew(void) { + ImagingOutline outline; + + outline = calloc(1, sizeof(struct ImagingOutlineInstance)); + if (!outline) { + return (ImagingOutline)ImagingError_MemoryError(); + } + + outline->edges = NULL; + outline->count = outline->size = 0; + + ImagingOutlineMove(outline, 0, 0); + + return outline; +} + +void +ImagingOutlineDelete(ImagingOutline outline) { + if (!outline) { + return; + } + + if (outline->edges) { + free(outline->edges); + } + + free(outline); +} + +static Edge * +allocate(ImagingOutline outline, int extra) { + Edge *e; + + if (outline->count + extra > outline->size) { + /* expand outline buffer */ + outline->size += extra + 25; + if (!outline->edges) { + /* malloc check ok, uses calloc for overflow */ + e = calloc(outline->size, sizeof(Edge)); + } else { + if (outline->size > INT_MAX / (int)sizeof(Edge)) { + return NULL; + } + /* malloc check ok, overflow checked above */ + e = realloc(outline->edges, outline->size * sizeof(Edge)); + } + if (!e) { + return NULL; + } + outline->edges = e; + } + + e = outline->edges + outline->count; + + outline->count += extra; + + return e; +} + +int +ImagingOutlineMove(ImagingOutline outline, float x0, float y0) { + outline->x = outline->x0 = x0; + outline->y = outline->y0 = y0; + + return 0; +} + +int +ImagingOutlineLine(ImagingOutline outline, float x1, float y1) { + Edge *e; + + e = allocate(outline, 1); + if (!e) { + return -1; /* out of memory */ + } + + add_edge(e, (int)outline->x, (int)outline->y, (int)x1, (int)y1); + + outline->x = x1; + outline->y = y1; + + return 0; +} + +int +ImagingOutlineCurve( + ImagingOutline outline, + float x1, + float y1, + float x2, + float y2, + float x3, + float y3) { + Edge *e; + int i; + float xo, yo; + +#define STEPS 32 + + e = allocate(outline, STEPS); + if (!e) { + return -1; /* out of memory */ + } + + xo = outline->x; + yo = outline->y; + + /* flatten the bezier segment */ + + for (i = 1; i <= STEPS; i++) { + float t = ((float)i) / STEPS; + float t2 = t * t; + float t3 = t2 * t; + + float u = 1.0F - t; + float u2 = u * u; + float u3 = u2 * u; + + float x = outline->x * u3 + 3 * (x1 * t * u2 + x2 * t2 * u) + x3 * t3 + 0.5; + float y = outline->y * u3 + 3 * (y1 * t * u2 + y2 * t2 * u) + y3 * t3 + 0.5; + + add_edge(e++, xo, yo, (int)x, (int)y); + + xo = x, yo = y; + } + + outline->x = xo; + outline->y = yo; + + return 0; +} + +int +ImagingOutlineClose(ImagingOutline outline) { + if (outline->x == outline->x0 && outline->y == outline->y0) { + return 0; + } + return ImagingOutlineLine(outline, outline->x0, outline->y0); +} + +int +ImagingOutlineTransform(ImagingOutline outline, double a[6]) { + Edge *eIn; + Edge *eOut; + int i, n; + int x0, y0, x1, y1; + int X0, Y0, X1, Y1; + + double a0 = a[0]; + double a1 = a[1]; + double a2 = a[2]; + double a3 = a[3]; + double a4 = a[4]; + double a5 = a[5]; + + eIn = outline->edges; + n = outline->count; + + eOut = allocate(outline, n); + if (!eOut) { + ImagingError_MemoryError(); + return -1; + } + + for (i = 0; i < n; i++) { + x0 = eIn->x0; + y0 = eIn->y0; + + /* FIXME: ouch! */ + if (eIn->x0 == eIn->xmin) { + x1 = eIn->xmax; + } else { + x1 = eIn->xmin; + } + if (eIn->y0 == eIn->ymin) { + y1 = eIn->ymax; + } else { + y1 = eIn->ymin; + } + + /* full moon tonight! if this doesn't work, you may need to + upgrade your compiler (make sure you have the right service + pack) */ + + X0 = (int)(a0 * x0 + a1 * y0 + a2); + Y0 = (int)(a3 * x0 + a4 * y0 + a5); + X1 = (int)(a0 * x1 + a1 * y1 + a2); + Y1 = (int)(a3 * x1 + a4 * y1 + a5); + + add_edge(eOut, X0, Y0, X1, Y1); + + eIn++; + eOut++; + } + + free(outline->edges); + + /* FIXME: ugly! */ + outline->edges = NULL; + outline->count = outline->size = 0; + + return 0; +} + +int +ImagingDrawOutline( + Imaging im, ImagingOutline outline, const void *ink_, int fill, int op) { + DRAW *draw; + INT32 ink; + + DRAWINIT(); + + draw->polygon(im, outline->count, outline->edges, ink, 0); + + return 0; +} diff --git a/src/libImaging/Effects.c b/src/libImaging/Effects.c index 93e7af0bc..54cea408a 100644 --- a/src/libImaging/Effects.c +++ b/src/libImaging/Effects.c @@ -1,160 +1,160 @@ -/* - * The Python Imaging Library - * $Id$ - * - * various special effects and image generators - * - * history: - * 1997-05-21 fl Just for fun - * 1997-06-05 fl Added mandelbrot generator - * 2003-05-24 fl Added perlin_turbulence generator (in progress) - * - * Copyright (c) 1997-2003 by Fredrik Lundh. - * Copyright (c) 1997 by Secret Labs AB. - * - * See the README file for information on usage and redistribution. - */ - -#include "Imaging.h" - -#include - -Imaging -ImagingEffectMandelbrot(int xsize, int ysize, double extent[4], int quality) { - /* Generate a Mandelbrot set covering the given extent */ - - Imaging im; - int x, y, k; - double width, height; - double x1, y1, xi2, yi2, cr, ci, radius; - double dr, di; - - /* Check arguments */ - width = extent[2] - extent[0]; - height = extent[3] - extent[1]; - if (width < 0.0 || height < 0.0 || quality < 2) { - return (Imaging)ImagingError_ValueError(NULL); - } - - im = ImagingNewDirty("L", xsize, ysize); - if (!im) { - return NULL; - } - - dr = width / (xsize - 1); - di = height / (ysize - 1); - - radius = 100.0; - - for (y = 0; y < ysize; y++) { - UINT8 *buf = im->image8[y]; - for (x = 0; x < xsize; x++) { - x1 = y1 = xi2 = yi2 = 0.0; - cr = x * dr + extent[0]; - ci = y * di + extent[1]; - for (k = 1;; k++) { - y1 = 2 * x1 * y1 + ci; - x1 = xi2 - yi2 + cr; - xi2 = x1 * x1; - yi2 = y1 * y1; - if ((xi2 + yi2) > radius) { - buf[x] = k * 255 / quality; - break; - } - if (k > quality) { - buf[x] = 0; - break; - } - } - } - } - return im; -} - -Imaging -ImagingEffectNoise(int xsize, int ysize, float sigma) { - /* Generate Gaussian noise centered around 128 */ - - Imaging imOut; - int x, y; - int nextok; - double this, next; - - imOut = ImagingNewDirty("L", xsize, ysize); - if (!imOut) { - return NULL; - } - - next = 0.0; - nextok = 0; - - for (y = 0; y < imOut->ysize; y++) { - UINT8 *out = imOut->image8[y]; - for (x = 0; x < imOut->xsize; x++) { - if (nextok) { - this = next; - nextok = 0; - } else { - /* after numerical recipes */ - double v1, v2, radius, factor; - do { - v1 = rand() * (2.0 / RAND_MAX) - 1.0; - v2 = rand() * (2.0 / RAND_MAX) - 1.0; - radius = v1 * v1 + v2 * v2; - } while (radius >= 1.0); - factor = sqrt(-2.0 * log(radius) / radius); - this = factor * v1; - next = factor * v2; - } - out[x] = CLIP8(128 + sigma * this); - } - } - - return imOut; -} - -Imaging -ImagingEffectSpread(Imaging imIn, int distance) { - /* Randomly spread pixels in an image */ - - Imaging imOut; - int x, y; - - imOut = ImagingNewDirty(imIn->mode, imIn->xsize, imIn->ysize); - - if (!imOut) { - return NULL; - } - -#define SPREAD(type, image) \ - if (distance == 0) { \ - for (y = 0; y < imOut->ysize; y++) { \ - for (x = 0; x < imOut->xsize; x++) { \ - imOut->image[y][x] = imIn->image[y][x]; \ - } \ - } \ - } else { \ - for (y = 0; y < imOut->ysize; y++) { \ - for (x = 0; x < imOut->xsize; x++) { \ - int xx = x + (rand() % distance) - distance / 2; \ - int yy = y + (rand() % distance) - distance / 2; \ - if (xx >= 0 && xx < imIn->xsize && yy >= 0 && yy < imIn->ysize) { \ - imOut->image[yy][xx] = imIn->image[y][x]; \ - imOut->image[y][x] = imIn->image[yy][xx]; \ - } else { \ - imOut->image[y][x] = imIn->image[y][x]; \ - } \ - } \ - } \ - } - - if (imIn->image8) { - SPREAD(UINT8, image8); - } else { - SPREAD(INT32, image32); - } - - ImagingCopyPalette(imOut, imIn); - - return imOut; -} +/* + * The Python Imaging Library + * $Id$ + * + * various special effects and image generators + * + * history: + * 1997-05-21 fl Just for fun + * 1997-06-05 fl Added mandelbrot generator + * 2003-05-24 fl Added perlin_turbulence generator (in progress) + * + * Copyright (c) 1997-2003 by Fredrik Lundh. + * Copyright (c) 1997 by Secret Labs AB. + * + * See the README file for information on usage and redistribution. + */ + +#include "Imaging.h" + +#include + +Imaging +ImagingEffectMandelbrot(int xsize, int ysize, double extent[4], int quality) { + /* Generate a Mandelbrot set covering the given extent */ + + Imaging im; + int x, y, k; + double width, height; + double x1, y1, xi2, yi2, cr, ci, radius; + double dr, di; + + /* Check arguments */ + width = extent[2] - extent[0]; + height = extent[3] - extent[1]; + if (width < 0.0 || height < 0.0 || quality < 2) { + return (Imaging)ImagingError_ValueError(NULL); + } + + im = ImagingNewDirty("L", xsize, ysize); + if (!im) { + return NULL; + } + + dr = width / (xsize - 1); + di = height / (ysize - 1); + + radius = 100.0; + + for (y = 0; y < ysize; y++) { + UINT8 *buf = im->image8[y]; + for (x = 0; x < xsize; x++) { + x1 = y1 = xi2 = yi2 = 0.0; + cr = x * dr + extent[0]; + ci = y * di + extent[1]; + for (k = 1;; k++) { + y1 = 2 * x1 * y1 + ci; + x1 = xi2 - yi2 + cr; + xi2 = x1 * x1; + yi2 = y1 * y1; + if ((xi2 + yi2) > radius) { + buf[x] = k * 255 / quality; + break; + } + if (k > quality) { + buf[x] = 0; + break; + } + } + } + } + return im; +} + +Imaging +ImagingEffectNoise(int xsize, int ysize, float sigma) { + /* Generate Gaussian noise centered around 128 */ + + Imaging imOut; + int x, y; + int nextok; + double this, next; + + imOut = ImagingNewDirty("L", xsize, ysize); + if (!imOut) { + return NULL; + } + + next = 0.0; + nextok = 0; + + for (y = 0; y < imOut->ysize; y++) { + UINT8 *out = imOut->image8[y]; + for (x = 0; x < imOut->xsize; x++) { + if (nextok) { + this = next; + nextok = 0; + } else { + /* after numerical recipes */ + double v1, v2, radius, factor; + do { + v1 = rand() * (2.0 / RAND_MAX) - 1.0; + v2 = rand() * (2.0 / RAND_MAX) - 1.0; + radius = v1 * v1 + v2 * v2; + } while (radius >= 1.0); + factor = sqrt(-2.0 * log(radius) / radius); + this = factor * v1; + next = factor * v2; + } + out[x] = CLIP8(128 + sigma * this); + } + } + + return imOut; +} + +Imaging +ImagingEffectSpread(Imaging imIn, int distance) { + /* Randomly spread pixels in an image */ + + Imaging imOut; + int x, y; + + imOut = ImagingNewDirty(imIn->mode, imIn->xsize, imIn->ysize); + + if (!imOut) { + return NULL; + } + +#define SPREAD(type, image) \ + if (distance == 0) { \ + for (y = 0; y < imOut->ysize; y++) { \ + for (x = 0; x < imOut->xsize; x++) { \ + imOut->image[y][x] = imIn->image[y][x]; \ + } \ + } \ + } else { \ + for (y = 0; y < imOut->ysize; y++) { \ + for (x = 0; x < imOut->xsize; x++) { \ + int xx = x + (rand() % distance) - distance / 2; \ + int yy = y + (rand() % distance) - distance / 2; \ + if (xx >= 0 && xx < imIn->xsize && yy >= 0 && yy < imIn->ysize) { \ + imOut->image[yy][xx] = imIn->image[y][x]; \ + imOut->image[y][x] = imIn->image[yy][xx]; \ + } else { \ + imOut->image[y][x] = imIn->image[y][x]; \ + } \ + } \ + } \ + } + + if (imIn->image8) { + SPREAD(UINT8, image8); + } else { + SPREAD(INT32, image32); + } + + ImagingCopyPalette(imOut, imIn); + + return imOut; +} diff --git a/src/libImaging/EpsEncode.c b/src/libImaging/EpsEncode.c index 3f2cb33b2..6b25b744a 100644 --- a/src/libImaging/EpsEncode.c +++ b/src/libImaging/EpsEncode.c @@ -1,77 +1,77 @@ -/* - * The Python Imaging Library. - * $Id$ - * - * encoder for EPS hex data - * - * history: - * 96-04-19 fl created - * 96-06-27 fl don't drop last block of encoded data - * - * notes: - * FIXME: rename to HexEncode.c ?? - * - * Copyright (c) Fredrik Lundh 1996. - * Copyright (c) Secret Labs AB 1997. - * - * See the README file for information on usage and redistribution. - */ - -#include "Imaging.h" - -int -ImagingEpsEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { - enum { HEXBYTE = 1, NEWLINE }; - const char *hex = "0123456789abcdef"; - - UINT8 *ptr = buf; - UINT8 *in, i; - - if (!state->state) { - state->state = HEXBYTE; - state->xsize *= im->pixelsize; /* Hack! */ - } - - in = (UINT8 *)im->image[state->y]; - - for (;;) { - if (state->state == NEWLINE) { - if (bytes < 1) { - break; - } - *ptr++ = '\n'; - bytes--; - state->state = HEXBYTE; - } - - if (bytes < 2) { - break; - } - - i = in[state->x++]; - *ptr++ = hex[(i >> 4) & 15]; - *ptr++ = hex[i & 15]; - bytes -= 2; - - /* Skip junk bytes */ - if (im->bands == 3 && (state->x & 3) == 3) { - state->x++; - } - - if (++state->count >= 79 / 2) { - state->state = NEWLINE; - state->count = 0; - } - - if (state->x >= state->xsize) { - state->x = 0; - if (++state->y >= state->ysize) { - state->errcode = IMAGING_CODEC_END; - break; - } - in = (UINT8 *)im->image[state->y]; - } - } - - return ptr - buf; -} +/* + * The Python Imaging Library. + * $Id$ + * + * encoder for EPS hex data + * + * history: + * 96-04-19 fl created + * 96-06-27 fl don't drop last block of encoded data + * + * notes: + * FIXME: rename to HexEncode.c ?? + * + * Copyright (c) Fredrik Lundh 1996. + * Copyright (c) Secret Labs AB 1997. + * + * See the README file for information on usage and redistribution. + */ + +#include "Imaging.h" + +int +ImagingEpsEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { + enum { HEXBYTE = 1, NEWLINE }; + const char *hex = "0123456789abcdef"; + + UINT8 *ptr = buf; + UINT8 *in, i; + + if (!state->state) { + state->state = HEXBYTE; + state->xsize *= im->pixelsize; /* Hack! */ + } + + in = (UINT8 *)im->image[state->y]; + + for (;;) { + if (state->state == NEWLINE) { + if (bytes < 1) { + break; + } + *ptr++ = '\n'; + bytes--; + state->state = HEXBYTE; + } + + if (bytes < 2) { + break; + } + + i = in[state->x++]; + *ptr++ = hex[(i >> 4) & 15]; + *ptr++ = hex[i & 15]; + bytes -= 2; + + /* Skip junk bytes */ + if (im->bands == 3 && (state->x & 3) == 3) { + state->x++; + } + + if (++state->count >= 79 / 2) { + state->state = NEWLINE; + state->count = 0; + } + + if (state->x >= state->xsize) { + state->x = 0; + if (++state->y >= state->ysize) { + state->errcode = IMAGING_CODEC_END; + break; + } + in = (UINT8 *)im->image[state->y]; + } + } + + return ptr - buf; +} diff --git a/src/libImaging/Except.c b/src/libImaging/Except.c index f42ff9aec..41ffe71c6 100644 --- a/src/libImaging/Except.c +++ b/src/libImaging/Except.c @@ -1,72 +1,72 @@ -/* - * 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 */ -} +/* + * 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..c343ff5fd 100644 --- a/src/libImaging/File.c +++ b/src/libImaging/File.c @@ -1,78 +1,78 @@ -/* - * The Python Imaging Library - * $Id$ - * - * built-in image file handling - * - * history: - * 1995-11-26 fl Created, supports PGM/PPM - * 1996-08-07 fl Write "1" images as PGM - * 1999-02-21 fl Don't write non-standard modes - * - * Copyright (c) 1997-99 by Secret Labs AB. - * Copyright (c) 1995-96 by Fredrik Lundh. - * - * See the README file for information on usage and redistribution. - */ - -#include "Imaging.h" - -#include - -int -ImagingSaveRaw(Imaging im, FILE *fp) { - int x, y, i; - - if (strcmp(im->mode, "1") == 0 || strcmp(im->mode, "L") == 0) { - /* @PIL227: FIXME: for mode "1", map != 0 to 255 */ - - /* PGM "L" */ - for (y = 0; y < im->ysize; y++) { - fwrite(im->image[y], 1, im->xsize, fp); - } - - } else { - /* PPM "RGB" or other internal format */ - for (y = 0; y < im->ysize; y++) { - for (x = i = 0; x < im->xsize; x++, i += im->pixelsize) { - fwrite(im->image[y] + i, 1, im->bands, fp); - } - } - } - - return 1; -} - -int -ImagingSavePPM(Imaging im, const char *outfile) { - FILE *fp; - - if (!im) { - (void)ImagingError_ValueError(NULL); - return 0; - } - - fp = fopen(outfile, "wb"); - if (!fp) { - (void)ImagingError_OSError(); - return 0; - } - - if (strcmp(im->mode, "1") == 0 || strcmp(im->mode, "L") == 0) { - /* Write "PGM" */ - fprintf(fp, "P5\n%d %d\n255\n", im->xsize, im->ysize); - } else if (strcmp(im->mode, "RGB") == 0) { - /* Write "PPM" */ - fprintf(fp, "P6\n%d %d\n255\n", im->xsize, im->ysize); - } else { - fclose(fp); - (void)ImagingError_ModeError(); - return 0; - } - - ImagingSaveRaw(im, fp); - - fclose(fp); - - return 1; -} +/* + * The Python Imaging Library + * $Id$ + * + * built-in image file handling + * + * history: + * 1995-11-26 fl Created, supports PGM/PPM + * 1996-08-07 fl Write "1" images as PGM + * 1999-02-21 fl Don't write non-standard modes + * + * Copyright (c) 1997-99 by Secret Labs AB. + * Copyright (c) 1995-96 by Fredrik Lundh. + * + * See the README file for information on usage and redistribution. + */ + +#include "Imaging.h" + +#include + +int +ImagingSaveRaw(Imaging im, FILE *fp) { + int x, y, i; + + if (strcmp(im->mode, "1") == 0 || strcmp(im->mode, "L") == 0) { + /* @PIL227: FIXME: for mode "1", map != 0 to 255 */ + + /* PGM "L" */ + for (y = 0; y < im->ysize; y++) { + fwrite(im->image[y], 1, im->xsize, fp); + } + + } else { + /* PPM "RGB" or other internal format */ + for (y = 0; y < im->ysize; y++) { + for (x = i = 0; x < im->xsize; x++, i += im->pixelsize) { + fwrite(im->image[y] + i, 1, im->bands, fp); + } + } + } + + return 1; +} + +int +ImagingSavePPM(Imaging im, const char *outfile) { + FILE *fp; + + if (!im) { + (void)ImagingError_ValueError(NULL); + return 0; + } + + fp = fopen(outfile, "wb"); + if (!fp) { + (void)ImagingError_OSError(); + return 0; + } + + if (strcmp(im->mode, "1") == 0 || strcmp(im->mode, "L") == 0) { + /* Write "PGM" */ + fprintf(fp, "P5\n%d %d\n255\n", im->xsize, im->ysize); + } else if (strcmp(im->mode, "RGB") == 0) { + /* Write "PPM" */ + fprintf(fp, "P6\n%d %d\n255\n", im->xsize, im->ysize); + } else { + fclose(fp); + (void)ImagingError_ModeError(); + return 0; + } + + ImagingSaveRaw(im, fp); + + fclose(fp); + + return 1; +} diff --git a/src/libImaging/Fill.c b/src/libImaging/Fill.c index 5b6bfb89c..fab06e326 100644 --- a/src/libImaging/Fill.c +++ b/src/libImaging/Fill.c @@ -1,139 +1,139 @@ -/* - * The Python Imaging Library - * $Id$ - * - * fill image with constant pixel value - * - * history: - * 95-11-26 fl moved from Imaging.c - * 96-05-17 fl added radial fill, renamed wedge to linear - * 98-06-23 fl changed ImageFill signature - * - * Copyright (c) Secret Labs AB 1997-98. All rights reserved. - * Copyright (c) Fredrik Lundh 1995-96. - * - * See the README file for information on usage and redistribution. - */ - -#include "Imaging.h" - -#include "math.h" - -Imaging -ImagingFill(Imaging im, const void *colour) { - int x, y; - ImagingSectionCookie cookie; - - /* 0-width or 0-height image. No need to do anything */ - if (!im->linesize || !im->ysize) { - return im; - } - - if (im->type == IMAGING_TYPE_SPECIAL) { - /* use generic API */ - ImagingAccess access = ImagingAccessNew(im); - if (access) { - for (y = 0; y < im->ysize; y++) { - for (x = 0; x < im->xsize; x++) { - access->put_pixel(im, x, y, colour); - } - } - ImagingAccessDelete(im, access); - } else { - /* wipe the image */ - for (y = 0; y < im->ysize; y++) { - memset(im->image[y], 0, im->linesize); - } - } - } else { - INT32 c = 0L; - ImagingSectionEnter(&cookie); - memcpy(&c, colour, im->pixelsize); - if (im->image32 && c != 0L) { - for (y = 0; y < im->ysize; y++) { - for (x = 0; x < im->xsize; x++) { - im->image32[y][x] = c; - } - } - } else { - unsigned char cc = (unsigned char)*(UINT8 *)colour; - for (y = 0; y < im->ysize; y++) { - memset(im->image[y], cc, im->linesize); - } - } - ImagingSectionLeave(&cookie); - } - - return im; -} - -Imaging -ImagingFillLinearGradient(const char *mode) { - Imaging im; - int y; - - if (strlen(mode) != 1) { - return (Imaging)ImagingError_ModeError(); - } - - im = ImagingNewDirty(mode, 256, 256); - if (!im) { - return NULL; - } - - if (im->image8) { - for (y = 0; y < 256; y++) { - memset(im->image8[y], (unsigned char)y, 256); - } - } else { - int x; - for (y = 0; y < 256; y++) { - for (x = 0; x < 256; x++) { - if (im->type == IMAGING_TYPE_FLOAT32) { - IMAGING_PIXEL_FLOAT32(im, x, y) = y; - } else { - IMAGING_PIXEL_INT32(im, x, y) = y; - } - } - } - } - - return im; -} - -Imaging -ImagingFillRadialGradient(const char *mode) { - Imaging im; - int x, y; - int d; - - if (strlen(mode) != 1) { - return (Imaging)ImagingError_ModeError(); - } - - im = ImagingNewDirty(mode, 256, 256); - if (!im) { - return NULL; - } - - 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); - if (d >= 255) { - d = 255; - } - if (im->image8) { - im->image8[y][x] = d; - } else { - if (im->type == IMAGING_TYPE_FLOAT32) { - IMAGING_PIXEL_FLOAT32(im, x, y) = d; - } else { - IMAGING_PIXEL_INT32(im, x, y) = d; - } - } - } - } - - return im; -} +/* + * The Python Imaging Library + * $Id$ + * + * fill image with constant pixel value + * + * history: + * 95-11-26 fl moved from Imaging.c + * 96-05-17 fl added radial fill, renamed wedge to linear + * 98-06-23 fl changed ImageFill signature + * + * Copyright (c) Secret Labs AB 1997-98. All rights reserved. + * Copyright (c) Fredrik Lundh 1995-96. + * + * See the README file for information on usage and redistribution. + */ + +#include "Imaging.h" + +#include "math.h" + +Imaging +ImagingFill(Imaging im, const void *colour) { + int x, y; + ImagingSectionCookie cookie; + + /* 0-width or 0-height image. No need to do anything */ + if (!im->linesize || !im->ysize) { + return im; + } + + if (im->type == IMAGING_TYPE_SPECIAL) { + /* use generic API */ + ImagingAccess access = ImagingAccessNew(im); + if (access) { + for (y = 0; y < im->ysize; y++) { + for (x = 0; x < im->xsize; x++) { + access->put_pixel(im, x, y, colour); + } + } + ImagingAccessDelete(im, access); + } else { + /* wipe the image */ + for (y = 0; y < im->ysize; y++) { + memset(im->image[y], 0, im->linesize); + } + } + } else { + INT32 c = 0L; + ImagingSectionEnter(&cookie); + memcpy(&c, colour, im->pixelsize); + if (im->image32 && c != 0L) { + for (y = 0; y < im->ysize; y++) { + for (x = 0; x < im->xsize; x++) { + im->image32[y][x] = c; + } + } + } else { + unsigned char cc = (unsigned char)*(UINT8 *)colour; + for (y = 0; y < im->ysize; y++) { + memset(im->image[y], cc, im->linesize); + } + } + ImagingSectionLeave(&cookie); + } + + return im; +} + +Imaging +ImagingFillLinearGradient(const char *mode) { + Imaging im; + int y; + + if (strlen(mode) != 1) { + return (Imaging)ImagingError_ModeError(); + } + + im = ImagingNewDirty(mode, 256, 256); + if (!im) { + return NULL; + } + + if (im->image8) { + for (y = 0; y < 256; y++) { + memset(im->image8[y], (unsigned char)y, 256); + } + } else { + int x; + for (y = 0; y < 256; y++) { + for (x = 0; x < 256; x++) { + if (im->type == IMAGING_TYPE_FLOAT32) { + IMAGING_PIXEL_FLOAT32(im, x, y) = y; + } else { + IMAGING_PIXEL_INT32(im, x, y) = y; + } + } + } + } + + return im; +} + +Imaging +ImagingFillRadialGradient(const char *mode) { + Imaging im; + int x, y; + int d; + + if (strlen(mode) != 1) { + return (Imaging)ImagingError_ModeError(); + } + + im = ImagingNewDirty(mode, 256, 256); + if (!im) { + return NULL; + } + + 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); + if (d >= 255) { + d = 255; + } + if (im->image8) { + im->image8[y][x] = d; + } else { + if (im->type == IMAGING_TYPE_FLOAT32) { + IMAGING_PIXEL_FLOAT32(im, x, y) = d; + } else { + IMAGING_PIXEL_INT32(im, x, y) = d; + } + } + } + } + + return im; +} diff --git a/src/libImaging/Filter.c b/src/libImaging/Filter.c index 85de77fcb..81bb39ae8 100644 --- a/src/libImaging/Filter.c +++ b/src/libImaging/Filter.c @@ -1,411 +1,411 @@ -/* - * The Python Imaging Library - * $Id$ - * - * apply convolution kernel to image - * - * history: - * 1995-11-26 fl Created, supports 3x3 kernels - * 1995-11-27 fl Added 5x5 kernels, copy border - * 1999-07-26 fl Eliminated a few compiler warnings - * 2002-06-09 fl Moved kernel definitions to Python - * 2002-06-11 fl Support floating point kernels - * 2003-09-15 fl Added ImagingExpand helper - * - * Copyright (c) Secret Labs AB 1997-2002. All rights reserved. - * Copyright (c) Fredrik Lundh 1995. - * - * See the README file for information on usage and redistribution. - */ - -/* - * FIXME: Support RGB and RGBA/CMYK modes as well - * FIXME: Expand image border (current version leaves border as is) - * FIXME: Implement image processing gradient filters - */ - -#include "Imaging.h" - -static inline UINT8 -clip8(float in) { - if (in <= 0.0) { - return 0; - } - if (in >= 255.0) { - return 255; - } - return (UINT8)in; -} - -static inline INT32 -clip32(float in) { - if (in <= 0.0) { - return 0; - } - if (in >= pow(2, 31) - 1) { - return pow(2, 31) - 1; - } - return (INT32)in; -} - -Imaging -ImagingExpand(Imaging imIn, int xmargin, int ymargin) { - Imaging imOut; - int x, y; - ImagingSectionCookie cookie; - - if (xmargin < 0 && ymargin < 0) { - return (Imaging)ImagingError_ValueError("bad kernel size"); - } - - imOut = ImagingNewDirty( - imIn->mode, imIn->xsize + 2 * xmargin, imIn->ysize + 2 * ymargin); - if (!imOut) { - return NULL; - } - -#define EXPAND_LINE(type, image, yin, yout) \ - { \ - for (x = 0; x < xmargin; x++) { \ - imOut->image[yout][x] = imIn->image[yin][0]; \ - } \ - for (x = 0; x < imIn->xsize; x++) { \ - imOut->image[yout][x + xmargin] = imIn->image[yin][x]; \ - } \ - for (x = 0; x < xmargin; x++) { \ - imOut->image[yout][xmargin + imIn->xsize + x] = \ - imIn->image[yin][imIn->xsize - 1]; \ - } \ - } - -#define EXPAND(type, image) \ - { \ - for (y = 0; y < ymargin; y++) { \ - EXPAND_LINE(type, image, 0, y); \ - } \ - for (y = 0; y < imIn->ysize; y++) { \ - EXPAND_LINE(type, image, y, y + ymargin); \ - } \ - for (y = 0; y < ymargin; y++) { \ - EXPAND_LINE(type, image, imIn->ysize - 1, ymargin + imIn->ysize + y); \ - } \ - } - - ImagingSectionEnter(&cookie); - if (imIn->image8) { - EXPAND(UINT8, image8); - } else { - EXPAND(INT32, image32); - } - ImagingSectionLeave(&cookie); - - ImagingCopyPalette(imOut, imIn); - - return imOut; -} - -void -ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) { -#define KERNEL1x3(in0, x, kernel, d) \ - (_i2f(in0[x - d]) * (kernel)[0] + _i2f(in0[x]) * (kernel)[1] + \ - _i2f(in0[x + d]) * (kernel)[2]) - - int x = 0, y = 0; - - memcpy(imOut->image[0], im->image[0], im->linesize); - if (im->bands == 1) { - // Add one time for rounding - offset += 0.5; - if (im->type == IMAGING_TYPE_INT32) { - for (y = 1; y < im->ysize - 1; y++) { - INT32 *in_1 = (INT32 *)im->image[y - 1]; - INT32 *in0 = (INT32 *)im->image[y]; - INT32 *in1 = (INT32 *)im->image[y + 1]; - INT32 *out = (INT32 *)imOut->image[y]; - - out[0] = in0[0]; - for (x = 1; x < im->xsize - 1; x++) { - float ss = offset; - ss += KERNEL1x3(in1, x, &kernel[0], 1); - ss += KERNEL1x3(in0, x, &kernel[3], 1); - ss += KERNEL1x3(in_1, x, &kernel[6], 1); - out[x] = clip32(ss); - } - out[x] = in0[x]; - } - } else { - for (y = 1; y < im->ysize - 1; y++) { - UINT8 *in_1 = (UINT8 *)im->image[y - 1]; - UINT8 *in0 = (UINT8 *)im->image[y]; - UINT8 *in1 = (UINT8 *)im->image[y + 1]; - UINT8 *out = (UINT8 *)imOut->image[y]; - - out[0] = in0[0]; - for (x = 1; x < im->xsize - 1; x++) { - float ss = offset; - ss += KERNEL1x3(in1, x, &kernel[0], 1); - ss += KERNEL1x3(in0, x, &kernel[3], 1); - ss += KERNEL1x3(in_1, x, &kernel[6], 1); - out[x] = clip8(ss); - } - out[x] = in0[x]; - } - } - } else { - // Add one time for rounding - offset += 0.5; - for (y = 1; y < im->ysize - 1; y++) { - UINT8 *in_1 = (UINT8 *)im->image[y - 1]; - UINT8 *in0 = (UINT8 *)im->image[y]; - UINT8 *in1 = (UINT8 *)im->image[y + 1]; - UINT8 *out = (UINT8 *)imOut->image[y]; - - memcpy(out, in0, sizeof(UINT32)); - if (im->bands == 2) { - for (x = 1; x < im->xsize - 1; x++) { - float ss0 = offset; - float ss3 = offset; - UINT32 v; - ss0 += KERNEL1x3(in1, x * 4 + 0, &kernel[0], 4); - ss3 += KERNEL1x3(in1, x * 4 + 3, &kernel[0], 4); - ss0 += KERNEL1x3(in0, x * 4 + 0, &kernel[3], 4); - ss3 += KERNEL1x3(in0, x * 4 + 3, &kernel[3], 4); - ss0 += KERNEL1x3(in_1, x * 4 + 0, &kernel[6], 4); - ss3 += KERNEL1x3(in_1, x * 4 + 3, &kernel[6], 4); - v = MAKE_UINT32(clip8(ss0), 0, 0, clip8(ss3)); - memcpy(out + x * sizeof(v), &v, sizeof(v)); - } - } else if (im->bands == 3) { - for (x = 1; x < im->xsize - 1; x++) { - float ss0 = offset; - float ss1 = offset; - float ss2 = offset; - UINT32 v; - ss0 += KERNEL1x3(in1, x * 4 + 0, &kernel[0], 4); - ss1 += KERNEL1x3(in1, x * 4 + 1, &kernel[0], 4); - ss2 += KERNEL1x3(in1, x * 4 + 2, &kernel[0], 4); - ss0 += KERNEL1x3(in0, x * 4 + 0, &kernel[3], 4); - ss1 += KERNEL1x3(in0, x * 4 + 1, &kernel[3], 4); - ss2 += KERNEL1x3(in0, x * 4 + 2, &kernel[3], 4); - ss0 += KERNEL1x3(in_1, x * 4 + 0, &kernel[6], 4); - ss1 += KERNEL1x3(in_1, x * 4 + 1, &kernel[6], 4); - ss2 += KERNEL1x3(in_1, x * 4 + 2, &kernel[6], 4); - v = MAKE_UINT32(clip8(ss0), clip8(ss1), clip8(ss2), 0); - memcpy(out + x * sizeof(v), &v, sizeof(v)); - } - } else if (im->bands == 4) { - for (x = 1; x < im->xsize - 1; x++) { - float ss0 = offset; - float ss1 = offset; - float ss2 = offset; - float ss3 = offset; - UINT32 v; - ss0 += KERNEL1x3(in1, x * 4 + 0, &kernel[0], 4); - ss1 += KERNEL1x3(in1, x * 4 + 1, &kernel[0], 4); - ss2 += KERNEL1x3(in1, x * 4 + 2, &kernel[0], 4); - ss3 += KERNEL1x3(in1, x * 4 + 3, &kernel[0], 4); - ss0 += KERNEL1x3(in0, x * 4 + 0, &kernel[3], 4); - ss1 += KERNEL1x3(in0, x * 4 + 1, &kernel[3], 4); - ss2 += KERNEL1x3(in0, x * 4 + 2, &kernel[3], 4); - ss3 += KERNEL1x3(in0, x * 4 + 3, &kernel[3], 4); - ss0 += KERNEL1x3(in_1, x * 4 + 0, &kernel[6], 4); - ss1 += KERNEL1x3(in_1, x * 4 + 1, &kernel[6], 4); - ss2 += KERNEL1x3(in_1, x * 4 + 2, &kernel[6], 4); - ss3 += KERNEL1x3(in_1, x * 4 + 3, &kernel[6], 4); - v = MAKE_UINT32(clip8(ss0), clip8(ss1), clip8(ss2), clip8(ss3)); - memcpy(out + x * sizeof(v), &v, sizeof(v)); - } - } - memcpy(out + x * sizeof(UINT32), in0 + x * sizeof(UINT32), sizeof(UINT32)); - } - } - memcpy(imOut->image[y], im->image[y], im->linesize); -} - -void -ImagingFilter5x5(Imaging imOut, Imaging im, const float *kernel, float offset) { -#define KERNEL1x5(in0, x, kernel, d) \ - (_i2f(in0[x - d - d]) * (kernel)[0] + _i2f(in0[x - d]) * (kernel)[1] + \ - _i2f(in0[x]) * (kernel)[2] + _i2f(in0[x + d]) * (kernel)[3] + \ - _i2f(in0[x + d + d]) * (kernel)[4]) - - int x = 0, y = 0; - - memcpy(imOut->image[0], im->image[0], im->linesize); - memcpy(imOut->image[1], im->image[1], im->linesize); - if (im->bands == 1) { - // Add one time for rounding - offset += 0.5; - if (im->type == IMAGING_TYPE_INT32) { - for (y = 2; y < im->ysize - 2; y++) { - INT32 *in_2 = (INT32 *)im->image[y - 2]; - INT32 *in_1 = (INT32 *)im->image[y - 1]; - INT32 *in0 = (INT32 *)im->image[y]; - INT32 *in1 = (INT32 *)im->image[y + 1]; - INT32 *in2 = (INT32 *)im->image[y + 2]; - INT32 *out = (INT32 *)imOut->image[y]; - - out[0] = in0[0]; - out[1] = in0[1]; - for (x = 2; x < im->xsize - 2; x++) { - float ss = offset; - ss += KERNEL1x5(in2, x, &kernel[0], 1); - ss += KERNEL1x5(in1, x, &kernel[5], 1); - ss += KERNEL1x5(in0, x, &kernel[10], 1); - ss += KERNEL1x5(in_1, x, &kernel[15], 1); - ss += KERNEL1x5(in_2, x, &kernel[20], 1); - out[x] = clip32(ss); - } - out[x + 0] = in0[x + 0]; - out[x + 1] = in0[x + 1]; - } - } else { - for (y = 2; y < im->ysize - 2; y++) { - UINT8 *in_2 = (UINT8 *)im->image[y - 2]; - UINT8 *in_1 = (UINT8 *)im->image[y - 1]; - UINT8 *in0 = (UINT8 *)im->image[y]; - UINT8 *in1 = (UINT8 *)im->image[y + 1]; - UINT8 *in2 = (UINT8 *)im->image[y + 2]; - UINT8 *out = (UINT8 *)imOut->image[y]; - - out[0] = in0[0]; - out[1] = in0[1]; - for (x = 2; x < im->xsize - 2; x++) { - float ss = offset; - ss += KERNEL1x5(in2, x, &kernel[0], 1); - ss += KERNEL1x5(in1, x, &kernel[5], 1); - ss += KERNEL1x5(in0, x, &kernel[10], 1); - ss += KERNEL1x5(in_1, x, &kernel[15], 1); - ss += KERNEL1x5(in_2, x, &kernel[20], 1); - out[x] = clip8(ss); - } - out[x + 0] = in0[x + 0]; - out[x + 1] = in0[x + 1]; - } - } - } else { - // Add one time for rounding - offset += 0.5; - for (y = 2; y < im->ysize - 2; y++) { - UINT8 *in_2 = (UINT8 *)im->image[y - 2]; - UINT8 *in_1 = (UINT8 *)im->image[y - 1]; - UINT8 *in0 = (UINT8 *)im->image[y]; - UINT8 *in1 = (UINT8 *)im->image[y + 1]; - UINT8 *in2 = (UINT8 *)im->image[y + 2]; - UINT8 *out = (UINT8 *)imOut->image[y]; - - memcpy(out, in0, sizeof(UINT32) * 2); - if (im->bands == 2) { - for (x = 2; x < im->xsize - 2; x++) { - float ss0 = offset; - float ss3 = offset; - UINT32 v; - ss0 += KERNEL1x5(in2, x * 4 + 0, &kernel[0], 4); - ss3 += KERNEL1x5(in2, x * 4 + 3, &kernel[0], 4); - ss0 += KERNEL1x5(in1, x * 4 + 0, &kernel[5], 4); - ss3 += KERNEL1x5(in1, x * 4 + 3, &kernel[5], 4); - ss0 += KERNEL1x5(in0, x * 4 + 0, &kernel[10], 4); - ss3 += KERNEL1x5(in0, x * 4 + 3, &kernel[10], 4); - ss0 += KERNEL1x5(in_1, x * 4 + 0, &kernel[15], 4); - ss3 += KERNEL1x5(in_1, x * 4 + 3, &kernel[15], 4); - ss0 += KERNEL1x5(in_2, x * 4 + 0, &kernel[20], 4); - ss3 += KERNEL1x5(in_2, x * 4 + 3, &kernel[20], 4); - v = MAKE_UINT32(clip8(ss0), 0, 0, clip8(ss3)); - memcpy(out + x * sizeof(v), &v, sizeof(v)); - } - } else if (im->bands == 3) { - for (x = 2; x < im->xsize - 2; x++) { - float ss0 = offset; - float ss1 = offset; - float ss2 = offset; - UINT32 v; - ss0 += KERNEL1x5(in2, x * 4 + 0, &kernel[0], 4); - ss1 += KERNEL1x5(in2, x * 4 + 1, &kernel[0], 4); - ss2 += KERNEL1x5(in2, x * 4 + 2, &kernel[0], 4); - ss0 += KERNEL1x5(in1, x * 4 + 0, &kernel[5], 4); - ss1 += KERNEL1x5(in1, x * 4 + 1, &kernel[5], 4); - ss2 += KERNEL1x5(in1, x * 4 + 2, &kernel[5], 4); - ss0 += KERNEL1x5(in0, x * 4 + 0, &kernel[10], 4); - ss1 += KERNEL1x5(in0, x * 4 + 1, &kernel[10], 4); - ss2 += KERNEL1x5(in0, x * 4 + 2, &kernel[10], 4); - ss0 += KERNEL1x5(in_1, x * 4 + 0, &kernel[15], 4); - ss1 += KERNEL1x5(in_1, x * 4 + 1, &kernel[15], 4); - ss2 += KERNEL1x5(in_1, x * 4 + 2, &kernel[15], 4); - ss0 += KERNEL1x5(in_2, x * 4 + 0, &kernel[20], 4); - ss1 += KERNEL1x5(in_2, x * 4 + 1, &kernel[20], 4); - ss2 += KERNEL1x5(in_2, x * 4 + 2, &kernel[20], 4); - v = MAKE_UINT32(clip8(ss0), clip8(ss1), clip8(ss2), 0); - memcpy(out + x * sizeof(v), &v, sizeof(v)); - } - } else if (im->bands == 4) { - for (x = 2; x < im->xsize - 2; x++) { - float ss0 = offset; - float ss1 = offset; - float ss2 = offset; - float ss3 = offset; - UINT32 v; - ss0 += KERNEL1x5(in2, x * 4 + 0, &kernel[0], 4); - ss1 += KERNEL1x5(in2, x * 4 + 1, &kernel[0], 4); - ss2 += KERNEL1x5(in2, x * 4 + 2, &kernel[0], 4); - ss3 += KERNEL1x5(in2, x * 4 + 3, &kernel[0], 4); - ss0 += KERNEL1x5(in1, x * 4 + 0, &kernel[5], 4); - ss1 += KERNEL1x5(in1, x * 4 + 1, &kernel[5], 4); - ss2 += KERNEL1x5(in1, x * 4 + 2, &kernel[5], 4); - ss3 += KERNEL1x5(in1, x * 4 + 3, &kernel[5], 4); - ss0 += KERNEL1x5(in0, x * 4 + 0, &kernel[10], 4); - ss1 += KERNEL1x5(in0, x * 4 + 1, &kernel[10], 4); - ss2 += KERNEL1x5(in0, x * 4 + 2, &kernel[10], 4); - ss3 += KERNEL1x5(in0, x * 4 + 3, &kernel[10], 4); - ss0 += KERNEL1x5(in_1, x * 4 + 0, &kernel[15], 4); - ss1 += KERNEL1x5(in_1, x * 4 + 1, &kernel[15], 4); - ss2 += KERNEL1x5(in_1, x * 4 + 2, &kernel[15], 4); - ss3 += KERNEL1x5(in_1, x * 4 + 3, &kernel[15], 4); - ss0 += KERNEL1x5(in_2, x * 4 + 0, &kernel[20], 4); - ss1 += KERNEL1x5(in_2, x * 4 + 1, &kernel[20], 4); - ss2 += KERNEL1x5(in_2, x * 4 + 2, &kernel[20], 4); - ss3 += KERNEL1x5(in_2, x * 4 + 3, &kernel[20], 4); - v = MAKE_UINT32(clip8(ss0), clip8(ss1), clip8(ss2), clip8(ss3)); - memcpy(out + x * sizeof(v), &v, sizeof(v)); - } - } - memcpy( - out + x * sizeof(UINT32), in0 + x * sizeof(UINT32), sizeof(UINT32) * 2); - } - } - memcpy(imOut->image[y], im->image[y], im->linesize); - memcpy(imOut->image[y + 1], im->image[y + 1], im->linesize); -} - -Imaging -ImagingFilter(Imaging im, int xsize, int ysize, const FLOAT32 *kernel, FLOAT32 offset) { - Imaging imOut; - ImagingSectionCookie cookie; - - if (im->type != IMAGING_TYPE_UINT8 && im->type != IMAGING_TYPE_INT32) { - return (Imaging)ImagingError_ModeError(); - } - - if (im->xsize < xsize || im->ysize < ysize) { - return ImagingCopy(im); - } - - if ((xsize != 3 && xsize != 5) || xsize != ysize) { - return (Imaging)ImagingError_ValueError("bad kernel size"); - } - - imOut = ImagingNewDirty(im->mode, im->xsize, im->ysize); - if (!imOut) { - return NULL; - } - - ImagingSectionEnter(&cookie); - if (xsize == 3) { - /* 3x3 kernel. */ - ImagingFilter3x3(imOut, im, kernel, offset); - } else { - /* 5x5 kernel. */ - ImagingFilter5x5(imOut, im, kernel, offset); - } - ImagingSectionLeave(&cookie); - return imOut; -} +/* + * The Python Imaging Library + * $Id$ + * + * apply convolution kernel to image + * + * history: + * 1995-11-26 fl Created, supports 3x3 kernels + * 1995-11-27 fl Added 5x5 kernels, copy border + * 1999-07-26 fl Eliminated a few compiler warnings + * 2002-06-09 fl Moved kernel definitions to Python + * 2002-06-11 fl Support floating point kernels + * 2003-09-15 fl Added ImagingExpand helper + * + * Copyright (c) Secret Labs AB 1997-2002. All rights reserved. + * Copyright (c) Fredrik Lundh 1995. + * + * See the README file for information on usage and redistribution. + */ + +/* + * FIXME: Support RGB and RGBA/CMYK modes as well + * FIXME: Expand image border (current version leaves border as is) + * FIXME: Implement image processing gradient filters + */ + +#include "Imaging.h" + +static inline UINT8 +clip8(float in) { + if (in <= 0.0) { + return 0; + } + if (in >= 255.0) { + return 255; + } + return (UINT8)in; +} + +static inline INT32 +clip32(float in) { + if (in <= 0.0) { + return 0; + } + if (in >= pow(2, 31) - 1) { + return pow(2, 31) - 1; + } + return (INT32)in; +} + +Imaging +ImagingExpand(Imaging imIn, int xmargin, int ymargin) { + Imaging imOut; + int x, y; + ImagingSectionCookie cookie; + + if (xmargin < 0 && ymargin < 0) { + return (Imaging)ImagingError_ValueError("bad kernel size"); + } + + imOut = ImagingNewDirty( + imIn->mode, imIn->xsize + 2 * xmargin, imIn->ysize + 2 * ymargin); + if (!imOut) { + return NULL; + } + +#define EXPAND_LINE(type, image, yin, yout) \ + { \ + for (x = 0; x < xmargin; x++) { \ + imOut->image[yout][x] = imIn->image[yin][0]; \ + } \ + for (x = 0; x < imIn->xsize; x++) { \ + imOut->image[yout][x + xmargin] = imIn->image[yin][x]; \ + } \ + for (x = 0; x < xmargin; x++) { \ + imOut->image[yout][xmargin + imIn->xsize + x] = \ + imIn->image[yin][imIn->xsize - 1]; \ + } \ + } + +#define EXPAND(type, image) \ + { \ + for (y = 0; y < ymargin; y++) { \ + EXPAND_LINE(type, image, 0, y); \ + } \ + for (y = 0; y < imIn->ysize; y++) { \ + EXPAND_LINE(type, image, y, y + ymargin); \ + } \ + for (y = 0; y < ymargin; y++) { \ + EXPAND_LINE(type, image, imIn->ysize - 1, ymargin + imIn->ysize + y); \ + } \ + } + + ImagingSectionEnter(&cookie); + if (imIn->image8) { + EXPAND(UINT8, image8); + } else { + EXPAND(INT32, image32); + } + ImagingSectionLeave(&cookie); + + ImagingCopyPalette(imOut, imIn); + + return imOut; +} + +void +ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) { +#define KERNEL1x3(in0, x, kernel, d) \ + (_i2f(in0[x - d]) * (kernel)[0] + _i2f(in0[x]) * (kernel)[1] + \ + _i2f(in0[x + d]) * (kernel)[2]) + + int x = 0, y = 0; + + memcpy(imOut->image[0], im->image[0], im->linesize); + if (im->bands == 1) { + // Add one time for rounding + offset += 0.5; + if (im->type == IMAGING_TYPE_INT32) { + for (y = 1; y < im->ysize - 1; y++) { + INT32 *in_1 = (INT32 *)im->image[y - 1]; + INT32 *in0 = (INT32 *)im->image[y]; + INT32 *in1 = (INT32 *)im->image[y + 1]; + INT32 *out = (INT32 *)imOut->image[y]; + + out[0] = in0[0]; + for (x = 1; x < im->xsize - 1; x++) { + float ss = offset; + ss += KERNEL1x3(in1, x, &kernel[0], 1); + ss += KERNEL1x3(in0, x, &kernel[3], 1); + ss += KERNEL1x3(in_1, x, &kernel[6], 1); + out[x] = clip32(ss); + } + out[x] = in0[x]; + } + } else { + for (y = 1; y < im->ysize - 1; y++) { + UINT8 *in_1 = (UINT8 *)im->image[y - 1]; + UINT8 *in0 = (UINT8 *)im->image[y]; + UINT8 *in1 = (UINT8 *)im->image[y + 1]; + UINT8 *out = (UINT8 *)imOut->image[y]; + + out[0] = in0[0]; + for (x = 1; x < im->xsize - 1; x++) { + float ss = offset; + ss += KERNEL1x3(in1, x, &kernel[0], 1); + ss += KERNEL1x3(in0, x, &kernel[3], 1); + ss += KERNEL1x3(in_1, x, &kernel[6], 1); + out[x] = clip8(ss); + } + out[x] = in0[x]; + } + } + } else { + // Add one time for rounding + offset += 0.5; + for (y = 1; y < im->ysize - 1; y++) { + UINT8 *in_1 = (UINT8 *)im->image[y - 1]; + UINT8 *in0 = (UINT8 *)im->image[y]; + UINT8 *in1 = (UINT8 *)im->image[y + 1]; + UINT8 *out = (UINT8 *)imOut->image[y]; + + memcpy(out, in0, sizeof(UINT32)); + if (im->bands == 2) { + for (x = 1; x < im->xsize - 1; x++) { + float ss0 = offset; + float ss3 = offset; + UINT32 v; + ss0 += KERNEL1x3(in1, x * 4 + 0, &kernel[0], 4); + ss3 += KERNEL1x3(in1, x * 4 + 3, &kernel[0], 4); + ss0 += KERNEL1x3(in0, x * 4 + 0, &kernel[3], 4); + ss3 += KERNEL1x3(in0, x * 4 + 3, &kernel[3], 4); + ss0 += KERNEL1x3(in_1, x * 4 + 0, &kernel[6], 4); + ss3 += KERNEL1x3(in_1, x * 4 + 3, &kernel[6], 4); + v = MAKE_UINT32(clip8(ss0), 0, 0, clip8(ss3)); + memcpy(out + x * sizeof(v), &v, sizeof(v)); + } + } else if (im->bands == 3) { + for (x = 1; x < im->xsize - 1; x++) { + float ss0 = offset; + float ss1 = offset; + float ss2 = offset; + UINT32 v; + ss0 += KERNEL1x3(in1, x * 4 + 0, &kernel[0], 4); + ss1 += KERNEL1x3(in1, x * 4 + 1, &kernel[0], 4); + ss2 += KERNEL1x3(in1, x * 4 + 2, &kernel[0], 4); + ss0 += KERNEL1x3(in0, x * 4 + 0, &kernel[3], 4); + ss1 += KERNEL1x3(in0, x * 4 + 1, &kernel[3], 4); + ss2 += KERNEL1x3(in0, x * 4 + 2, &kernel[3], 4); + ss0 += KERNEL1x3(in_1, x * 4 + 0, &kernel[6], 4); + ss1 += KERNEL1x3(in_1, x * 4 + 1, &kernel[6], 4); + ss2 += KERNEL1x3(in_1, x * 4 + 2, &kernel[6], 4); + v = MAKE_UINT32(clip8(ss0), clip8(ss1), clip8(ss2), 0); + memcpy(out + x * sizeof(v), &v, sizeof(v)); + } + } else if (im->bands == 4) { + for (x = 1; x < im->xsize - 1; x++) { + float ss0 = offset; + float ss1 = offset; + float ss2 = offset; + float ss3 = offset; + UINT32 v; + ss0 += KERNEL1x3(in1, x * 4 + 0, &kernel[0], 4); + ss1 += KERNEL1x3(in1, x * 4 + 1, &kernel[0], 4); + ss2 += KERNEL1x3(in1, x * 4 + 2, &kernel[0], 4); + ss3 += KERNEL1x3(in1, x * 4 + 3, &kernel[0], 4); + ss0 += KERNEL1x3(in0, x * 4 + 0, &kernel[3], 4); + ss1 += KERNEL1x3(in0, x * 4 + 1, &kernel[3], 4); + ss2 += KERNEL1x3(in0, x * 4 + 2, &kernel[3], 4); + ss3 += KERNEL1x3(in0, x * 4 + 3, &kernel[3], 4); + ss0 += KERNEL1x3(in_1, x * 4 + 0, &kernel[6], 4); + ss1 += KERNEL1x3(in_1, x * 4 + 1, &kernel[6], 4); + ss2 += KERNEL1x3(in_1, x * 4 + 2, &kernel[6], 4); + ss3 += KERNEL1x3(in_1, x * 4 + 3, &kernel[6], 4); + v = MAKE_UINT32(clip8(ss0), clip8(ss1), clip8(ss2), clip8(ss3)); + memcpy(out + x * sizeof(v), &v, sizeof(v)); + } + } + memcpy(out + x * sizeof(UINT32), in0 + x * sizeof(UINT32), sizeof(UINT32)); + } + } + memcpy(imOut->image[y], im->image[y], im->linesize); +} + +void +ImagingFilter5x5(Imaging imOut, Imaging im, const float *kernel, float offset) { +#define KERNEL1x5(in0, x, kernel, d) \ + (_i2f(in0[x - d - d]) * (kernel)[0] + _i2f(in0[x - d]) * (kernel)[1] + \ + _i2f(in0[x]) * (kernel)[2] + _i2f(in0[x + d]) * (kernel)[3] + \ + _i2f(in0[x + d + d]) * (kernel)[4]) + + int x = 0, y = 0; + + memcpy(imOut->image[0], im->image[0], im->linesize); + memcpy(imOut->image[1], im->image[1], im->linesize); + if (im->bands == 1) { + // Add one time for rounding + offset += 0.5; + if (im->type == IMAGING_TYPE_INT32) { + for (y = 2; y < im->ysize - 2; y++) { + INT32 *in_2 = (INT32 *)im->image[y - 2]; + INT32 *in_1 = (INT32 *)im->image[y - 1]; + INT32 *in0 = (INT32 *)im->image[y]; + INT32 *in1 = (INT32 *)im->image[y + 1]; + INT32 *in2 = (INT32 *)im->image[y + 2]; + INT32 *out = (INT32 *)imOut->image[y]; + + out[0] = in0[0]; + out[1] = in0[1]; + for (x = 2; x < im->xsize - 2; x++) { + float ss = offset; + ss += KERNEL1x5(in2, x, &kernel[0], 1); + ss += KERNEL1x5(in1, x, &kernel[5], 1); + ss += KERNEL1x5(in0, x, &kernel[10], 1); + ss += KERNEL1x5(in_1, x, &kernel[15], 1); + ss += KERNEL1x5(in_2, x, &kernel[20], 1); + out[x] = clip32(ss); + } + out[x + 0] = in0[x + 0]; + out[x + 1] = in0[x + 1]; + } + } else { + for (y = 2; y < im->ysize - 2; y++) { + UINT8 *in_2 = (UINT8 *)im->image[y - 2]; + UINT8 *in_1 = (UINT8 *)im->image[y - 1]; + UINT8 *in0 = (UINT8 *)im->image[y]; + UINT8 *in1 = (UINT8 *)im->image[y + 1]; + UINT8 *in2 = (UINT8 *)im->image[y + 2]; + UINT8 *out = (UINT8 *)imOut->image[y]; + + out[0] = in0[0]; + out[1] = in0[1]; + for (x = 2; x < im->xsize - 2; x++) { + float ss = offset; + ss += KERNEL1x5(in2, x, &kernel[0], 1); + ss += KERNEL1x5(in1, x, &kernel[5], 1); + ss += KERNEL1x5(in0, x, &kernel[10], 1); + ss += KERNEL1x5(in_1, x, &kernel[15], 1); + ss += KERNEL1x5(in_2, x, &kernel[20], 1); + out[x] = clip8(ss); + } + out[x + 0] = in0[x + 0]; + out[x + 1] = in0[x + 1]; + } + } + } else { + // Add one time for rounding + offset += 0.5; + for (y = 2; y < im->ysize - 2; y++) { + UINT8 *in_2 = (UINT8 *)im->image[y - 2]; + UINT8 *in_1 = (UINT8 *)im->image[y - 1]; + UINT8 *in0 = (UINT8 *)im->image[y]; + UINT8 *in1 = (UINT8 *)im->image[y + 1]; + UINT8 *in2 = (UINT8 *)im->image[y + 2]; + UINT8 *out = (UINT8 *)imOut->image[y]; + + memcpy(out, in0, sizeof(UINT32) * 2); + if (im->bands == 2) { + for (x = 2; x < im->xsize - 2; x++) { + float ss0 = offset; + float ss3 = offset; + UINT32 v; + ss0 += KERNEL1x5(in2, x * 4 + 0, &kernel[0], 4); + ss3 += KERNEL1x5(in2, x * 4 + 3, &kernel[0], 4); + ss0 += KERNEL1x5(in1, x * 4 + 0, &kernel[5], 4); + ss3 += KERNEL1x5(in1, x * 4 + 3, &kernel[5], 4); + ss0 += KERNEL1x5(in0, x * 4 + 0, &kernel[10], 4); + ss3 += KERNEL1x5(in0, x * 4 + 3, &kernel[10], 4); + ss0 += KERNEL1x5(in_1, x * 4 + 0, &kernel[15], 4); + ss3 += KERNEL1x5(in_1, x * 4 + 3, &kernel[15], 4); + ss0 += KERNEL1x5(in_2, x * 4 + 0, &kernel[20], 4); + ss3 += KERNEL1x5(in_2, x * 4 + 3, &kernel[20], 4); + v = MAKE_UINT32(clip8(ss0), 0, 0, clip8(ss3)); + memcpy(out + x * sizeof(v), &v, sizeof(v)); + } + } else if (im->bands == 3) { + for (x = 2; x < im->xsize - 2; x++) { + float ss0 = offset; + float ss1 = offset; + float ss2 = offset; + UINT32 v; + ss0 += KERNEL1x5(in2, x * 4 + 0, &kernel[0], 4); + ss1 += KERNEL1x5(in2, x * 4 + 1, &kernel[0], 4); + ss2 += KERNEL1x5(in2, x * 4 + 2, &kernel[0], 4); + ss0 += KERNEL1x5(in1, x * 4 + 0, &kernel[5], 4); + ss1 += KERNEL1x5(in1, x * 4 + 1, &kernel[5], 4); + ss2 += KERNEL1x5(in1, x * 4 + 2, &kernel[5], 4); + ss0 += KERNEL1x5(in0, x * 4 + 0, &kernel[10], 4); + ss1 += KERNEL1x5(in0, x * 4 + 1, &kernel[10], 4); + ss2 += KERNEL1x5(in0, x * 4 + 2, &kernel[10], 4); + ss0 += KERNEL1x5(in_1, x * 4 + 0, &kernel[15], 4); + ss1 += KERNEL1x5(in_1, x * 4 + 1, &kernel[15], 4); + ss2 += KERNEL1x5(in_1, x * 4 + 2, &kernel[15], 4); + ss0 += KERNEL1x5(in_2, x * 4 + 0, &kernel[20], 4); + ss1 += KERNEL1x5(in_2, x * 4 + 1, &kernel[20], 4); + ss2 += KERNEL1x5(in_2, x * 4 + 2, &kernel[20], 4); + v = MAKE_UINT32(clip8(ss0), clip8(ss1), clip8(ss2), 0); + memcpy(out + x * sizeof(v), &v, sizeof(v)); + } + } else if (im->bands == 4) { + for (x = 2; x < im->xsize - 2; x++) { + float ss0 = offset; + float ss1 = offset; + float ss2 = offset; + float ss3 = offset; + UINT32 v; + ss0 += KERNEL1x5(in2, x * 4 + 0, &kernel[0], 4); + ss1 += KERNEL1x5(in2, x * 4 + 1, &kernel[0], 4); + ss2 += KERNEL1x5(in2, x * 4 + 2, &kernel[0], 4); + ss3 += KERNEL1x5(in2, x * 4 + 3, &kernel[0], 4); + ss0 += KERNEL1x5(in1, x * 4 + 0, &kernel[5], 4); + ss1 += KERNEL1x5(in1, x * 4 + 1, &kernel[5], 4); + ss2 += KERNEL1x5(in1, x * 4 + 2, &kernel[5], 4); + ss3 += KERNEL1x5(in1, x * 4 + 3, &kernel[5], 4); + ss0 += KERNEL1x5(in0, x * 4 + 0, &kernel[10], 4); + ss1 += KERNEL1x5(in0, x * 4 + 1, &kernel[10], 4); + ss2 += KERNEL1x5(in0, x * 4 + 2, &kernel[10], 4); + ss3 += KERNEL1x5(in0, x * 4 + 3, &kernel[10], 4); + ss0 += KERNEL1x5(in_1, x * 4 + 0, &kernel[15], 4); + ss1 += KERNEL1x5(in_1, x * 4 + 1, &kernel[15], 4); + ss2 += KERNEL1x5(in_1, x * 4 + 2, &kernel[15], 4); + ss3 += KERNEL1x5(in_1, x * 4 + 3, &kernel[15], 4); + ss0 += KERNEL1x5(in_2, x * 4 + 0, &kernel[20], 4); + ss1 += KERNEL1x5(in_2, x * 4 + 1, &kernel[20], 4); + ss2 += KERNEL1x5(in_2, x * 4 + 2, &kernel[20], 4); + ss3 += KERNEL1x5(in_2, x * 4 + 3, &kernel[20], 4); + v = MAKE_UINT32(clip8(ss0), clip8(ss1), clip8(ss2), clip8(ss3)); + memcpy(out + x * sizeof(v), &v, sizeof(v)); + } + } + memcpy( + out + x * sizeof(UINT32), in0 + x * sizeof(UINT32), sizeof(UINT32) * 2); + } + } + memcpy(imOut->image[y], im->image[y], im->linesize); + memcpy(imOut->image[y + 1], im->image[y + 1], im->linesize); +} + +Imaging +ImagingFilter(Imaging im, int xsize, int ysize, const FLOAT32 *kernel, FLOAT32 offset) { + Imaging imOut; + ImagingSectionCookie cookie; + + if (im->type != IMAGING_TYPE_UINT8 && im->type != IMAGING_TYPE_INT32) { + return (Imaging)ImagingError_ModeError(); + } + + if (im->xsize < xsize || im->ysize < ysize) { + return ImagingCopy(im); + } + + if ((xsize != 3 && xsize != 5) || xsize != ysize) { + return (Imaging)ImagingError_ValueError("bad kernel size"); + } + + imOut = ImagingNewDirty(im->mode, im->xsize, im->ysize); + if (!imOut) { + return NULL; + } + + ImagingSectionEnter(&cookie); + if (xsize == 3) { + /* 3x3 kernel. */ + ImagingFilter3x3(imOut, im, kernel, offset); + } else { + /* 5x5 kernel. */ + ImagingFilter5x5(imOut, im, kernel, offset); + } + ImagingSectionLeave(&cookie); + return imOut; +} diff --git a/src/libImaging/FliDecode.c b/src/libImaging/FliDecode.c index 6b2518d35..f7a55cf94 100644 --- a/src/libImaging/FliDecode.c +++ b/src/libImaging/FliDecode.c @@ -1,269 +1,269 @@ -/* - * The Python Imaging Library. - * $Id$ - * - * decoder for Autodesk Animator FLI/FLC animations - * - * history: - * 97-01-03 fl Created - * 97-01-17 fl Added SS2 support (FLC) - * - * Copyright (c) Fredrik Lundh 1997. - * Copyright (c) Secret Labs AB 1997. - * - * See the README file for information on usage and redistribution. - */ - -#include "Imaging.h" - -#define I16(ptr) ((ptr)[0] + ((ptr)[1] << 8)) - -#define I32(ptr) ((ptr)[0] + ((ptr)[1] << 8) + ((ptr)[2] << 16) + ((ptr)[3] << 24)) - -#define ERR_IF_DATA_OOB(offset) \ - if ((data + (offset)) > ptr + bytes) { \ - state->errcode = IMAGING_CODEC_OVERRUN; \ - return -1; \ - } - -int -ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) { - UINT8 *ptr; - int framesize; - int c, chunks, advance; - int l, lines; - int i, j, x = 0, y, ymax; - - /* If not even the chunk size is present, we'd better leave */ - - if (bytes < 4) { - return 0; - } - - /* We don't decode anything unless we have a full chunk in the - input buffer */ - - ptr = buf; - - framesize = I32(ptr); - // there can be one pad byte in the framesize - if (bytes + (bytes % 2) < framesize) { - return 0; - } - - /* Make sure this is a frame chunk. The Python driver takes - case of other chunk types. */ - - if (bytes < 8) { - state->errcode = IMAGING_CODEC_OVERRUN; - return -1; - } - if (I16(ptr + 4) != 0xF1FA) { - state->errcode = IMAGING_CODEC_UNKNOWN; - return -1; - } - - chunks = I16(ptr + 6); - ptr += 16; - bytes -= 16; - - /* Process subchunks */ - for (c = 0; c < chunks; c++) { - UINT8 *data; - if (bytes < 10) { - state->errcode = IMAGING_CODEC_OVERRUN; - return -1; - } - data = ptr + 6; - switch (I16(ptr + 4)) { - case 4: - case 11: - /* FLI COLOR chunk */ - break; /* ignored; handled by Python code */ - case 7: - /* FLI SS2 chunk (word delta) */ - /* OOB ok, we've got 4 bytes min on entry */ - lines = I16(data); - data += 2; - for (l = y = 0; l < lines && y < state->ysize; l++, y++) { - UINT8 *local_buf = (UINT8 *)im->image[y]; - int p, packets; - ERR_IF_DATA_OOB(2) - packets = I16(data); - data += 2; - while (packets & 0x8000) { - /* flag word */ - if (packets & 0x4000) { - y += 65536 - packets; /* skip lines */ - if (y >= state->ysize) { - state->errcode = IMAGING_CODEC_OVERRUN; - return -1; - } - local_buf = (UINT8 *)im->image[y]; - } else { - /* store last byte (used if line width is odd) */ - local_buf[state->xsize - 1] = (UINT8)packets; - } - ERR_IF_DATA_OOB(2) - packets = I16(data); - data += 2; - } - for (p = x = 0; p < packets; p++) { - ERR_IF_DATA_OOB(2) - x += data[0]; /* pixel skip */ - if (data[1] >= 128) { - ERR_IF_DATA_OOB(4) - i = 256 - data[1]; /* run */ - if (x + i + i > state->xsize) { - break; - } - for (j = 0; j < i; j++) { - local_buf[x++] = data[2]; - local_buf[x++] = data[3]; - } - data += 2 + 2; - } else { - i = 2 * (int)data[1]; /* chunk */ - if (x + i > state->xsize) { - break; - } - ERR_IF_DATA_OOB(2 + i) - memcpy(local_buf + x, data + 2, i); - data += 2 + i; - x += i; - } - } - if (p < packets) { - break; /* didn't process all packets */ - } - } - if (l < lines) { - /* didn't process all lines */ - state->errcode = IMAGING_CODEC_OVERRUN; - return -1; - } - break; - case 12: - /* FLI LC chunk (byte delta) */ - /* OOB Check ok, we have 4 bytes min here */ - y = I16(data); - ymax = y + I16(data + 2); - data += 4; - for (; y < ymax && y < state->ysize; y++) { - UINT8 *out = (UINT8 *)im->image[y]; - ERR_IF_DATA_OOB(1) - int p, packets = *data++; - for (p = x = 0; p < packets; p++, x += i) { - ERR_IF_DATA_OOB(2) - x += data[0]; /* skip pixels */ - if (data[1] & 0x80) { - i = 256 - data[1]; /* run */ - if (x + i > state->xsize) { - break; - } - ERR_IF_DATA_OOB(3) - memset(out + x, data[2], i); - data += 3; - } else { - i = data[1]; /* chunk */ - if (x + i > state->xsize) { - break; - } - ERR_IF_DATA_OOB(2 + i) - memcpy(out + x, data + 2, i); - data += i + 2; - } - } - if (p < packets) { - break; /* didn't process all packets */ - } - } - if (y < ymax) { - /* didn't process all lines */ - state->errcode = IMAGING_CODEC_OVERRUN; - return -1; - } - break; - case 13: - /* FLI BLACK chunk */ - for (y = 0; y < state->ysize; y++) { - memset(im->image[y], 0, state->xsize); - } - break; - case 15: - /* FLI BRUN chunk */ - /* OOB, ok, we've got 4 bytes min on entry */ - for (y = 0; y < state->ysize; y++) { - UINT8 *out = (UINT8 *)im->image[y]; - data += 1; /* ignore packetcount byte */ - for (x = 0; x < state->xsize; x += i) { - ERR_IF_DATA_OOB(2) - if (data[0] & 0x80) { - i = 256 - data[0]; - if (x + i > state->xsize) { - break; /* safety first */ - } - ERR_IF_DATA_OOB(i + 1) - memcpy(out + x, data + 1, i); - data += i + 1; - } else { - i = data[0]; - if (x + i > state->xsize) { - break; /* safety first */ - } - memset(out + x, data[1], i); - data += 2; - } - } - if (x != state->xsize) { - /* didn't unpack whole line */ - state->errcode = IMAGING_CODEC_OVERRUN; - return -1; - } - } - break; - case 16: - /* COPY chunk */ - if (INT32_MAX / state->xsize < state->ysize) { - /* Integer overflow, bail */ - state->errcode = IMAGING_CODEC_OVERRUN; - return -1; - } - /* Note, have to check Data + size, not just ptr + size) */ - if (data + (state->xsize * state->ysize) > ptr + bytes) { - // not enough data for frame - // UNDONE Unclear that we're actually going to leave the buffer at - // the right place. - return ptr - buf; /* bytes consumed */ - } - for (y = 0; y < state->ysize; y++) { - UINT8 *local_buf = (UINT8 *)im->image[y]; - memcpy(local_buf, data, state->xsize); - data += state->xsize; - } - break; - case 18: - /* PSTAMP chunk */ - break; /* ignored */ - default: - /* unknown chunk */ - /* printf("unknown FLI/FLC chunk: %d\n", I16(ptr+4)); */ - state->errcode = IMAGING_CODEC_UNKNOWN; - return -1; - } - advance = I32(ptr); - if (advance == 0) { - // If there's no advance, we're in an infinite loop - state->errcode = IMAGING_CODEC_BROKEN; - return -1; - } - if (advance < 0 || advance > bytes) { - state->errcode = IMAGING_CODEC_OVERRUN; - return -1; - } - ptr += advance; - bytes -= advance; - } - - return -1; /* end of frame */ -} +/* + * The Python Imaging Library. + * $Id$ + * + * decoder for Autodesk Animator FLI/FLC animations + * + * history: + * 97-01-03 fl Created + * 97-01-17 fl Added SS2 support (FLC) + * + * Copyright (c) Fredrik Lundh 1997. + * Copyright (c) Secret Labs AB 1997. + * + * See the README file for information on usage and redistribution. + */ + +#include "Imaging.h" + +#define I16(ptr) ((ptr)[0] + ((ptr)[1] << 8)) + +#define I32(ptr) ((ptr)[0] + ((ptr)[1] << 8) + ((ptr)[2] << 16) + ((ptr)[3] << 24)) + +#define ERR_IF_DATA_OOB(offset) \ + if ((data + (offset)) > ptr + bytes) { \ + state->errcode = IMAGING_CODEC_OVERRUN; \ + return -1; \ + } + +int +ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) { + UINT8 *ptr; + int framesize; + int c, chunks, advance; + int l, lines; + int i, j, x = 0, y, ymax; + + /* If not even the chunk size is present, we'd better leave */ + + if (bytes < 4) { + return 0; + } + + /* We don't decode anything unless we have a full chunk in the + input buffer */ + + ptr = buf; + + framesize = I32(ptr); + // there can be one pad byte in the framesize + if (bytes + (bytes % 2) < framesize) { + return 0; + } + + /* Make sure this is a frame chunk. The Python driver takes + case of other chunk types. */ + + if (bytes < 8) { + state->errcode = IMAGING_CODEC_OVERRUN; + return -1; + } + if (I16(ptr + 4) != 0xF1FA) { + state->errcode = IMAGING_CODEC_UNKNOWN; + return -1; + } + + chunks = I16(ptr + 6); + ptr += 16; + bytes -= 16; + + /* Process subchunks */ + for (c = 0; c < chunks; c++) { + UINT8 *data; + if (bytes < 10) { + state->errcode = IMAGING_CODEC_OVERRUN; + return -1; + } + data = ptr + 6; + switch (I16(ptr + 4)) { + case 4: + case 11: + /* FLI COLOR chunk */ + break; /* ignored; handled by Python code */ + case 7: + /* FLI SS2 chunk (word delta) */ + /* OOB ok, we've got 4 bytes min on entry */ + lines = I16(data); + data += 2; + for (l = y = 0; l < lines && y < state->ysize; l++, y++) { + UINT8 *local_buf = (UINT8 *)im->image[y]; + int p, packets; + ERR_IF_DATA_OOB(2) + packets = I16(data); + data += 2; + while (packets & 0x8000) { + /* flag word */ + if (packets & 0x4000) { + y += 65536 - packets; /* skip lines */ + if (y >= state->ysize) { + state->errcode = IMAGING_CODEC_OVERRUN; + return -1; + } + local_buf = (UINT8 *)im->image[y]; + } else { + /* store last byte (used if line width is odd) */ + local_buf[state->xsize - 1] = (UINT8)packets; + } + ERR_IF_DATA_OOB(2) + packets = I16(data); + data += 2; + } + for (p = x = 0; p < packets; p++) { + ERR_IF_DATA_OOB(2) + x += data[0]; /* pixel skip */ + if (data[1] >= 128) { + ERR_IF_DATA_OOB(4) + i = 256 - data[1]; /* run */ + if (x + i + i > state->xsize) { + break; + } + for (j = 0; j < i; j++) { + local_buf[x++] = data[2]; + local_buf[x++] = data[3]; + } + data += 2 + 2; + } else { + i = 2 * (int)data[1]; /* chunk */ + if (x + i > state->xsize) { + break; + } + ERR_IF_DATA_OOB(2 + i) + memcpy(local_buf + x, data + 2, i); + data += 2 + i; + x += i; + } + } + if (p < packets) { + break; /* didn't process all packets */ + } + } + if (l < lines) { + /* didn't process all lines */ + state->errcode = IMAGING_CODEC_OVERRUN; + return -1; + } + break; + case 12: + /* FLI LC chunk (byte delta) */ + /* OOB Check ok, we have 4 bytes min here */ + y = I16(data); + ymax = y + I16(data + 2); + data += 4; + for (; y < ymax && y < state->ysize; y++) { + UINT8 *out = (UINT8 *)im->image[y]; + ERR_IF_DATA_OOB(1) + int p, packets = *data++; + for (p = x = 0; p < packets; p++, x += i) { + ERR_IF_DATA_OOB(2) + x += data[0]; /* skip pixels */ + if (data[1] & 0x80) { + i = 256 - data[1]; /* run */ + if (x + i > state->xsize) { + break; + } + ERR_IF_DATA_OOB(3) + memset(out + x, data[2], i); + data += 3; + } else { + i = data[1]; /* chunk */ + if (x + i > state->xsize) { + break; + } + ERR_IF_DATA_OOB(2 + i) + memcpy(out + x, data + 2, i); + data += i + 2; + } + } + if (p < packets) { + break; /* didn't process all packets */ + } + } + if (y < ymax) { + /* didn't process all lines */ + state->errcode = IMAGING_CODEC_OVERRUN; + return -1; + } + break; + case 13: + /* FLI BLACK chunk */ + for (y = 0; y < state->ysize; y++) { + memset(im->image[y], 0, state->xsize); + } + break; + case 15: + /* FLI BRUN chunk */ + /* OOB, ok, we've got 4 bytes min on entry */ + for (y = 0; y < state->ysize; y++) { + UINT8 *out = (UINT8 *)im->image[y]; + data += 1; /* ignore packetcount byte */ + for (x = 0; x < state->xsize; x += i) { + ERR_IF_DATA_OOB(2) + if (data[0] & 0x80) { + i = 256 - data[0]; + if (x + i > state->xsize) { + break; /* safety first */ + } + ERR_IF_DATA_OOB(i + 1) + memcpy(out + x, data + 1, i); + data += i + 1; + } else { + i = data[0]; + if (x + i > state->xsize) { + break; /* safety first */ + } + memset(out + x, data[1], i); + data += 2; + } + } + if (x != state->xsize) { + /* didn't unpack whole line */ + state->errcode = IMAGING_CODEC_OVERRUN; + return -1; + } + } + break; + case 16: + /* COPY chunk */ + if (INT32_MAX / state->xsize < state->ysize) { + /* Integer overflow, bail */ + state->errcode = IMAGING_CODEC_OVERRUN; + return -1; + } + /* Note, have to check Data + size, not just ptr + size) */ + if (data + (state->xsize * state->ysize) > ptr + bytes) { + // not enough data for frame + // UNDONE Unclear that we're actually going to leave the buffer at + // the right place. + return ptr - buf; /* bytes consumed */ + } + for (y = 0; y < state->ysize; y++) { + UINT8 *local_buf = (UINT8 *)im->image[y]; + memcpy(local_buf, data, state->xsize); + data += state->xsize; + } + break; + case 18: + /* PSTAMP chunk */ + break; /* ignored */ + default: + /* unknown chunk */ + /* printf("unknown FLI/FLC chunk: %d\n", I16(ptr+4)); */ + state->errcode = IMAGING_CODEC_UNKNOWN; + return -1; + } + advance = I32(ptr); + if (advance == 0) { + // If there's no advance, we're in an infinite loop + state->errcode = IMAGING_CODEC_BROKEN; + return -1; + } + if (advance < 0 || advance > bytes) { + state->errcode = IMAGING_CODEC_OVERRUN; + return -1; + } + ptr += advance; + bytes -= advance; + } + + return -1; /* end of frame */ +} diff --git a/src/libImaging/Geometry.c b/src/libImaging/Geometry.c index cf3bc9979..25d514e99 100644 --- a/src/libImaging/Geometry.c +++ b/src/libImaging/Geometry.c @@ -1,1157 +1,1157 @@ -#include "Imaging.h" - -/* For large images rotation is an inefficient operation in terms of CPU cache. - One row in the source image affects each column in destination. - Rotating in chunks that fit in the cache can speed up rotation - 8x on a modern CPU. A chunk size of 128 requires only 65k and is large enough - that the overhead from the extra loops are not apparent. */ -#define ROTATE_CHUNK 512 -#define ROTATE_SMALL_CHUNK 8 - -#define COORD(v) ((v) < 0.0 ? -1 : ((int)(v))) -#define FLOOR(v) ((v) < 0.0 ? ((int)floor(v)) : ((int)(v))) - -/* -------------------------------------------------------------------- */ -/* Transpose operations */ - -Imaging -ImagingFlipLeftRight(Imaging imOut, Imaging imIn) { - ImagingSectionCookie cookie; - int x, y, xr; - - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { - return (Imaging)ImagingError_ModeError(); - } - if (imIn->xsize != imOut->xsize || imIn->ysize != imOut->ysize) { - return (Imaging)ImagingError_Mismatch(); - } - - ImagingCopyPalette(imOut, imIn); - -#define FLIP_LEFT_RIGHT(INT, image) \ - for (y = 0; y < imIn->ysize; y++) { \ - INT *in = (INT *)imIn->image[y]; \ - INT *out = (INT *)imOut->image[y]; \ - xr = imIn->xsize - 1; \ - for (x = 0; x < imIn->xsize; x++, xr--) { \ - out[xr] = in[x]; \ - } \ - } - - ImagingSectionEnter(&cookie); - - if (imIn->image8) { - if (strncmp(imIn->mode, "I;16", 4) == 0) { - FLIP_LEFT_RIGHT(UINT16, image8) - } else { - FLIP_LEFT_RIGHT(UINT8, image8) - } - } else { - FLIP_LEFT_RIGHT(INT32, image32) - } - - ImagingSectionLeave(&cookie); - -#undef FLIP_LEFT_RIGHT - - return imOut; -} - -Imaging -ImagingFlipTopBottom(Imaging imOut, Imaging imIn) { - ImagingSectionCookie cookie; - int y, yr; - - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { - return (Imaging)ImagingError_ModeError(); - } - if (imIn->xsize != imOut->xsize || imIn->ysize != imOut->ysize) { - return (Imaging)ImagingError_Mismatch(); - } - - ImagingCopyPalette(imOut, imIn); - - ImagingSectionEnter(&cookie); - - yr = imIn->ysize - 1; - for (y = 0; y < imIn->ysize; y++, yr--) { - memcpy(imOut->image[yr], imIn->image[y], imIn->linesize); - } - - ImagingSectionLeave(&cookie); - - return imOut; -} - -Imaging -ImagingRotate90(Imaging imOut, Imaging imIn) { - ImagingSectionCookie cookie; - int x, y, xx, yy, xr, xxsize, yysize; - int xxx, yyy, xxxsize, yyysize; - - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { - return (Imaging)ImagingError_ModeError(); - } - if (imIn->xsize != imOut->ysize || imIn->ysize != imOut->xsize) { - return (Imaging)ImagingError_Mismatch(); - } - - ImagingCopyPalette(imOut, imIn); - -#define ROTATE_90(INT, image) \ - for (y = 0; y < imIn->ysize; y += ROTATE_CHUNK) { \ - for (x = 0; x < imIn->xsize; x += ROTATE_CHUNK) { \ - yysize = y + ROTATE_CHUNK < imIn->ysize ? y + ROTATE_CHUNK : imIn->ysize; \ - xxsize = x + ROTATE_CHUNK < imIn->xsize ? x + ROTATE_CHUNK : imIn->xsize; \ - for (yy = y; yy < yysize; yy += ROTATE_SMALL_CHUNK) { \ - for (xx = x; xx < xxsize; xx += ROTATE_SMALL_CHUNK) { \ - yyysize = yy + ROTATE_SMALL_CHUNK < imIn->ysize \ - ? yy + ROTATE_SMALL_CHUNK \ - : imIn->ysize; \ - xxxsize = xx + ROTATE_SMALL_CHUNK < imIn->xsize \ - ? xx + ROTATE_SMALL_CHUNK \ - : imIn->xsize; \ - for (yyy = yy; yyy < yyysize; yyy++) { \ - INT *in = (INT *)imIn->image[yyy]; \ - xr = imIn->xsize - 1 - xx; \ - for (xxx = xx; xxx < xxxsize; xxx++, xr--) { \ - INT *out = (INT *)imOut->image[xr]; \ - out[yyy] = in[xxx]; \ - } \ - } \ - } \ - } \ - } \ - } - - ImagingSectionEnter(&cookie); - - if (imIn->image8) { - if (strncmp(imIn->mode, "I;16", 4) == 0) { - ROTATE_90(UINT16, image8); - } else { - ROTATE_90(UINT8, image8); - } - } else { - ROTATE_90(INT32, image32); - } - - ImagingSectionLeave(&cookie); - -#undef ROTATE_90 - - return imOut; -} - -Imaging -ImagingTranspose(Imaging imOut, Imaging imIn) { - ImagingSectionCookie cookie; - int x, y, xx, yy, xxsize, yysize; - int xxx, yyy, xxxsize, yyysize; - - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { - return (Imaging)ImagingError_ModeError(); - } - if (imIn->xsize != imOut->ysize || imIn->ysize != imOut->xsize) { - return (Imaging)ImagingError_Mismatch(); - } - - ImagingCopyPalette(imOut, imIn); - -#define TRANSPOSE(INT, image) \ - for (y = 0; y < imIn->ysize; y += ROTATE_CHUNK) { \ - for (x = 0; x < imIn->xsize; x += ROTATE_CHUNK) { \ - yysize = y + ROTATE_CHUNK < imIn->ysize ? y + ROTATE_CHUNK : imIn->ysize; \ - xxsize = x + ROTATE_CHUNK < imIn->xsize ? x + ROTATE_CHUNK : imIn->xsize; \ - for (yy = y; yy < yysize; yy += ROTATE_SMALL_CHUNK) { \ - for (xx = x; xx < xxsize; xx += ROTATE_SMALL_CHUNK) { \ - yyysize = yy + ROTATE_SMALL_CHUNK < imIn->ysize \ - ? yy + ROTATE_SMALL_CHUNK \ - : imIn->ysize; \ - xxxsize = xx + ROTATE_SMALL_CHUNK < imIn->xsize \ - ? xx + ROTATE_SMALL_CHUNK \ - : imIn->xsize; \ - for (yyy = yy; yyy < yyysize; yyy++) { \ - INT *in = (INT *)imIn->image[yyy]; \ - for (xxx = xx; xxx < xxxsize; xxx++) { \ - INT *out = (INT *)imOut->image[xxx]; \ - out[yyy] = in[xxx]; \ - } \ - } \ - } \ - } \ - } \ - } - - ImagingSectionEnter(&cookie); - - if (imIn->image8) { - if (strncmp(imIn->mode, "I;16", 4) == 0) { - TRANSPOSE(UINT16, image8); - } else { - TRANSPOSE(UINT8, image8); - } - } else { - TRANSPOSE(INT32, image32); - } - - ImagingSectionLeave(&cookie); - -#undef TRANSPOSE - - return imOut; -} - -Imaging -ImagingTransverse(Imaging imOut, Imaging imIn) { - ImagingSectionCookie cookie; - int x, y, xr, yr, xx, yy, xxsize, yysize; - int xxx, yyy, xxxsize, yyysize; - - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { - return (Imaging)ImagingError_ModeError(); - } - if (imIn->xsize != imOut->ysize || imIn->ysize != imOut->xsize) { - return (Imaging)ImagingError_Mismatch(); - } - - ImagingCopyPalette(imOut, imIn); - -#define TRANSVERSE(INT, image) \ - for (y = 0; y < imIn->ysize; y += ROTATE_CHUNK) { \ - for (x = 0; x < imIn->xsize; x += ROTATE_CHUNK) { \ - yysize = y + ROTATE_CHUNK < imIn->ysize ? y + ROTATE_CHUNK : imIn->ysize; \ - xxsize = x + ROTATE_CHUNK < imIn->xsize ? x + ROTATE_CHUNK : imIn->xsize; \ - for (yy = y; yy < yysize; yy += ROTATE_SMALL_CHUNK) { \ - for (xx = x; xx < xxsize; xx += ROTATE_SMALL_CHUNK) { \ - yyysize = yy + ROTATE_SMALL_CHUNK < imIn->ysize \ - ? yy + ROTATE_SMALL_CHUNK \ - : imIn->ysize; \ - xxxsize = xx + ROTATE_SMALL_CHUNK < imIn->xsize \ - ? xx + ROTATE_SMALL_CHUNK \ - : imIn->xsize; \ - yr = imIn->ysize - 1 - yy; \ - for (yyy = yy; yyy < yyysize; yyy++, yr--) { \ - INT *in = (INT *)imIn->image[yyy]; \ - xr = imIn->xsize - 1 - xx; \ - for (xxx = xx; xxx < xxxsize; xxx++, xr--) { \ - INT *out = (INT *)imOut->image[xr]; \ - out[yr] = in[xxx]; \ - } \ - } \ - } \ - } \ - } \ - } - - ImagingSectionEnter(&cookie); - - if (imIn->image8) { - if (strncmp(imIn->mode, "I;16", 4) == 0) { - TRANSVERSE(UINT16, image8); - } else { - TRANSVERSE(UINT8, image8); - } - } else { - TRANSVERSE(INT32, image32); - } - - ImagingSectionLeave(&cookie); - -#undef TRANSVERSE - - return imOut; -} - -Imaging -ImagingRotate180(Imaging imOut, Imaging imIn) { - ImagingSectionCookie cookie; - int x, y, xr, yr; - - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { - return (Imaging)ImagingError_ModeError(); - } - if (imIn->xsize != imOut->xsize || imIn->ysize != imOut->ysize) { - return (Imaging)ImagingError_Mismatch(); - } - - ImagingCopyPalette(imOut, imIn); - -#define ROTATE_180(INT, image) \ - for (y = 0; y < imIn->ysize; y++, yr--) { \ - INT *in = (INT *)imIn->image[y]; \ - INT *out = (INT *)imOut->image[yr]; \ - xr = imIn->xsize - 1; \ - for (x = 0; x < imIn->xsize; x++, xr--) { \ - out[xr] = in[x]; \ - } \ - } - - ImagingSectionEnter(&cookie); - - yr = imIn->ysize - 1; - if (imIn->image8) { - if (strncmp(imIn->mode, "I;16", 4) == 0) { - ROTATE_180(UINT16, image8) - } else { - ROTATE_180(UINT8, image8) - } - } else { - ROTATE_180(INT32, image32) - } - - ImagingSectionLeave(&cookie); - -#undef ROTATE_180 - - return imOut; -} - -Imaging -ImagingRotate270(Imaging imOut, Imaging imIn) { - ImagingSectionCookie cookie; - int x, y, xx, yy, yr, xxsize, yysize; - int xxx, yyy, xxxsize, yyysize; - - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { - return (Imaging)ImagingError_ModeError(); - } - if (imIn->xsize != imOut->ysize || imIn->ysize != imOut->xsize) { - return (Imaging)ImagingError_Mismatch(); - } - - ImagingCopyPalette(imOut, imIn); - -#define ROTATE_270(INT, image) \ - for (y = 0; y < imIn->ysize; y += ROTATE_CHUNK) { \ - for (x = 0; x < imIn->xsize; x += ROTATE_CHUNK) { \ - yysize = y + ROTATE_CHUNK < imIn->ysize ? y + ROTATE_CHUNK : imIn->ysize; \ - xxsize = x + ROTATE_CHUNK < imIn->xsize ? x + ROTATE_CHUNK : imIn->xsize; \ - for (yy = y; yy < yysize; yy += ROTATE_SMALL_CHUNK) { \ - for (xx = x; xx < xxsize; xx += ROTATE_SMALL_CHUNK) { \ - yyysize = yy + ROTATE_SMALL_CHUNK < imIn->ysize \ - ? yy + ROTATE_SMALL_CHUNK \ - : imIn->ysize; \ - xxxsize = xx + ROTATE_SMALL_CHUNK < imIn->xsize \ - ? xx + ROTATE_SMALL_CHUNK \ - : imIn->xsize; \ - yr = imIn->ysize - 1 - yy; \ - for (yyy = yy; yyy < yyysize; yyy++, yr--) { \ - INT *in = (INT *)imIn->image[yyy]; \ - for (xxx = xx; xxx < xxxsize; xxx++) { \ - INT *out = (INT *)imOut->image[xxx]; \ - out[yr] = in[xxx]; \ - } \ - } \ - } \ - } \ - } \ - } - - ImagingSectionEnter(&cookie); - - if (imIn->image8) { - if (strncmp(imIn->mode, "I;16", 4) == 0) { - ROTATE_270(UINT16, image8); - } else { - ROTATE_270(UINT8, image8); - } - } else { - ROTATE_270(INT32, image32); - } - - ImagingSectionLeave(&cookie); - -#undef ROTATE_270 - - return imOut; -} - -/* -------------------------------------------------------------------- */ -/* Transforms */ - -/* transform primitives (ImagingTransformMap) */ - -static int -affine_transform(double *xout, double *yout, int x, int y, void *data) { - /* full moon tonight. your compiler will generate bogus code - for simple expressions, unless you reorganize the code, or - install Service Pack 3 */ - - double *a = (double *)data; - double a0 = a[0]; - double a1 = a[1]; - double a2 = a[2]; - double a3 = a[3]; - double a4 = a[4]; - double a5 = a[5]; - - double xin = x + 0.5; - double yin = y + 0.5; - - xout[0] = a0 * xin + a1 * yin + a2; - yout[0] = a3 * xin + a4 * yin + a5; - - return 1; -} - -static int -perspective_transform(double *xout, double *yout, int x, int y, void *data) { - double *a = (double *)data; - double a0 = a[0]; - double a1 = a[1]; - double a2 = a[2]; - double a3 = a[3]; - double a4 = a[4]; - double a5 = a[5]; - double a6 = a[6]; - double a7 = a[7]; - - double xin = x + 0.5; - double yin = y + 0.5; - - xout[0] = (a0 * xin + a1 * yin + a2) / (a6 * xin + a7 * yin + 1); - yout[0] = (a3 * xin + a4 * yin + a5) / (a6 * xin + a7 * yin + 1); - - return 1; -} - -static int -quad_transform(double *xout, double *yout, int x, int y, void *data) { - /* quad warp: map quadrilateral to rectangle */ - - double *a = (double *)data; - double a0 = a[0]; - double a1 = a[1]; - double a2 = a[2]; - double a3 = a[3]; - double a4 = a[4]; - double a5 = a[5]; - double a6 = a[6]; - double a7 = a[7]; - - double xin = x + 0.5; - double yin = y + 0.5; - - xout[0] = a0 + a1 * xin + a2 * yin + a3 * xin * yin; - yout[0] = a4 + a5 * xin + a6 * yin + a7 * xin * yin; - - return 1; -} - -/* transform filters (ImagingTransformFilter) */ - -static int -nearest_filter8(void *out, Imaging im, double xin, double yin) { - int x = COORD(xin); - int y = COORD(yin); - if (x < 0 || x >= im->xsize || y < 0 || y >= im->ysize) { - return 0; - } - ((UINT8 *)out)[0] = im->image8[y][x]; - return 1; -} - -static int -nearest_filter16(void *out, Imaging im, double xin, double yin) { - int x = COORD(xin); - int y = COORD(yin); - if (x < 0 || x >= im->xsize || y < 0 || y >= im->ysize) { - return 0; - } - memcpy(out, im->image8[y] + x * sizeof(INT16), sizeof(INT16)); - return 1; -} - -static int -nearest_filter32(void *out, Imaging im, double xin, double yin) { - int x = COORD(xin); - int y = COORD(yin); - if (x < 0 || x >= im->xsize || y < 0 || y >= im->ysize) { - return 0; - } - memcpy(out, &im->image32[y][x], sizeof(INT32)); - return 1; -} - -#define XCLIP(im, x) (((x) < 0) ? 0 : ((x) < im->xsize) ? (x) : im->xsize - 1) -#define YCLIP(im, y) (((y) < 0) ? 0 : ((y) < im->ysize) ? (y) : im->ysize - 1) - -#define BILINEAR(v, a, b, d) (v = (a) + ((b) - (a)) * (d)) - -#define BILINEAR_HEAD(type) \ - int x, y; \ - int x0, x1; \ - double v1, v2; \ - double dx, dy; \ - type *in; \ - if (xin < 0.0 || xin >= im->xsize || yin < 0.0 || yin >= im->ysize) { \ - return 0; \ - } \ - xin -= 0.5; \ - yin -= 0.5; \ - x = FLOOR(xin); \ - y = FLOOR(yin); \ - dx = xin - x; \ - dy = yin - y; - -#define BILINEAR_BODY(type, image, step, offset) \ - { \ - in = (type *)((image)[YCLIP(im, y)] + offset); \ - x0 = XCLIP(im, x + 0) * step; \ - x1 = XCLIP(im, x + 1) * step; \ - BILINEAR(v1, in[x0], in[x1], dx); \ - if (y + 1 >= 0 && y + 1 < im->ysize) { \ - in = (type *)((image)[y + 1] + offset); \ - BILINEAR(v2, in[x0], in[x1], dx); \ - } else { \ - v2 = v1; \ - } \ - BILINEAR(v1, v1, v2, dy); \ - } - -static int -bilinear_filter8(void *out, Imaging im, double xin, double yin) { - BILINEAR_HEAD(UINT8); - BILINEAR_BODY(UINT8, im->image8, 1, 0); - ((UINT8 *)out)[0] = (UINT8)v1; - return 1; -} - -static int -bilinear_filter32I(void *out, Imaging im, double xin, double yin) { - INT32 k; - BILINEAR_HEAD(INT32); - BILINEAR_BODY(INT32, im->image32, 1, 0); - k = v1; - memcpy(out, &k, sizeof(k)); - return 1; -} - -static int -bilinear_filter32F(void *out, Imaging im, double xin, double yin) { - FLOAT32 k; - BILINEAR_HEAD(FLOAT32); - BILINEAR_BODY(FLOAT32, im->image32, 1, 0); - k = v1; - memcpy(out, &k, sizeof(k)); - return 1; -} - -static int -bilinear_filter32LA(void *out, Imaging im, double xin, double yin) { - BILINEAR_HEAD(UINT8); - BILINEAR_BODY(UINT8, im->image, 4, 0); - ((UINT8 *)out)[0] = (UINT8)v1; - ((UINT8 *)out)[1] = (UINT8)v1; - ((UINT8 *)out)[2] = (UINT8)v1; - BILINEAR_BODY(UINT8, im->image, 4, 3); - ((UINT8 *)out)[3] = (UINT8)v1; - return 1; -} - -static int -bilinear_filter32RGB(void *out, Imaging im, double xin, double yin) { - int b; - BILINEAR_HEAD(UINT8); - for (b = 0; b < im->bands; b++) { - BILINEAR_BODY(UINT8, im->image, 4, b); - ((UINT8 *)out)[b] = (UINT8)v1; - } - return 1; -} - -#undef BILINEAR -#undef BILINEAR_HEAD -#undef BILINEAR_BODY - -#define BICUBIC(v, v1, v2, v3, v4, d) \ - { \ - double p1 = v2; \ - double p2 = -v1 + v3; \ - double p3 = 2 * (v1 - v2) + v3 - v4; \ - double p4 = -v1 + v2 - v3 + v4; \ - v = p1 + (d) * (p2 + (d) * (p3 + (d) * p4)); \ - } - -#define BICUBIC_HEAD(type) \ - int x = FLOOR(xin); \ - int y = FLOOR(yin); \ - int x0, x1, x2, x3; \ - double v1, v2, v3, v4; \ - double dx, dy; \ - type *in; \ - if (xin < 0.0 || xin >= im->xsize || yin < 0.0 || yin >= im->ysize) { \ - return 0; \ - } \ - xin -= 0.5; \ - yin -= 0.5; \ - x = FLOOR(xin); \ - y = FLOOR(yin); \ - dx = xin - x; \ - dy = yin - y; \ - x--; \ - y--; - -#define BICUBIC_BODY(type, image, step, offset) \ - { \ - in = (type *)((image)[YCLIP(im, y)] + offset); \ - x0 = XCLIP(im, x + 0) * step; \ - x1 = XCLIP(im, x + 1) * step; \ - x2 = XCLIP(im, x + 2) * step; \ - x3 = XCLIP(im, x + 3) * step; \ - BICUBIC(v1, in[x0], in[x1], in[x2], in[x3], dx); \ - if (y + 1 >= 0 && y + 1 < im->ysize) { \ - in = (type *)((image)[y + 1] + offset); \ - BICUBIC(v2, in[x0], in[x1], in[x2], in[x3], dx); \ - } else { \ - v2 = v1; \ - } \ - if (y + 2 >= 0 && y + 2 < im->ysize) { \ - in = (type *)((image)[y + 2] + offset); \ - BICUBIC(v3, in[x0], in[x1], in[x2], in[x3], dx); \ - } else { \ - v3 = v2; \ - } \ - if (y + 3 >= 0 && y + 3 < im->ysize) { \ - in = (type *)((image)[y + 3] + offset); \ - BICUBIC(v4, in[x0], in[x1], in[x2], in[x3], dx); \ - } else { \ - v4 = v3; \ - } \ - BICUBIC(v1, v1, v2, v3, v4, dy); \ - } - -static int -bicubic_filter8(void *out, Imaging im, double xin, double yin) { - BICUBIC_HEAD(UINT8); - BICUBIC_BODY(UINT8, im->image8, 1, 0); - if (v1 <= 0.0) { - ((UINT8 *)out)[0] = 0; - } else if (v1 >= 255.0) { - ((UINT8 *)out)[0] = 255; - } else { - ((UINT8 *)out)[0] = (UINT8)v1; - } - return 1; -} - -static int -bicubic_filter32I(void *out, Imaging im, double xin, double yin) { - INT32 k; - BICUBIC_HEAD(INT32); - BICUBIC_BODY(INT32, im->image32, 1, 0); - k = v1; - memcpy(out, &k, sizeof(k)); - return 1; -} - -static int -bicubic_filter32F(void *out, Imaging im, double xin, double yin) { - FLOAT32 k; - BICUBIC_HEAD(FLOAT32); - BICUBIC_BODY(FLOAT32, im->image32, 1, 0); - k = v1; - memcpy(out, &k, sizeof(k)); - return 1; -} - -static int -bicubic_filter32LA(void *out, Imaging im, double xin, double yin) { - BICUBIC_HEAD(UINT8); - BICUBIC_BODY(UINT8, im->image, 4, 0); - if (v1 <= 0.0) { - ((UINT8 *)out)[0] = 0; - ((UINT8 *)out)[1] = 0; - ((UINT8 *)out)[2] = 0; - } else if (v1 >= 255.0) { - ((UINT8 *)out)[0] = 255; - ((UINT8 *)out)[1] = 255; - ((UINT8 *)out)[2] = 255; - } else { - ((UINT8 *)out)[0] = (UINT8)v1; - ((UINT8 *)out)[1] = (UINT8)v1; - ((UINT8 *)out)[2] = (UINT8)v1; - } - BICUBIC_BODY(UINT8, im->image, 4, 3); - if (v1 <= 0.0) { - ((UINT8 *)out)[3] = 0; - } else if (v1 >= 255.0) { - ((UINT8 *)out)[3] = 255; - } else { - ((UINT8 *)out)[3] = (UINT8)v1; - } - return 1; -} - -static int -bicubic_filter32RGB(void *out, Imaging im, double xin, double yin) { - int b; - BICUBIC_HEAD(UINT8); - for (b = 0; b < im->bands; b++) { - BICUBIC_BODY(UINT8, im->image, 4, b); - if (v1 <= 0.0) { - ((UINT8 *)out)[b] = 0; - } else if (v1 >= 255.0) { - ((UINT8 *)out)[b] = 255; - } else { - ((UINT8 *)out)[b] = (UINT8)v1; - } - } - return 1; -} - -#undef BICUBIC -#undef BICUBIC_HEAD -#undef BICUBIC_BODY - -static ImagingTransformFilter -getfilter(Imaging im, int filterid) { - switch (filterid) { - case IMAGING_TRANSFORM_NEAREST: - if (im->image8) { - switch (im->type) { - case IMAGING_TYPE_UINT8: - return nearest_filter8; - case IMAGING_TYPE_SPECIAL: - switch (im->pixelsize) { - case 1: - return nearest_filter8; - case 2: - return nearest_filter16; - case 4: - return nearest_filter32; - } - } - } else { - return nearest_filter32; - } - break; - case IMAGING_TRANSFORM_BILINEAR: - if (im->image8) { - return bilinear_filter8; - } else if (im->image32) { - switch (im->type) { - case IMAGING_TYPE_UINT8: - if (im->bands == 2) { - return bilinear_filter32LA; - } else { - return bilinear_filter32RGB; - } - case IMAGING_TYPE_INT32: - return bilinear_filter32I; - case IMAGING_TYPE_FLOAT32: - return bilinear_filter32F; - } - } - break; - case IMAGING_TRANSFORM_BICUBIC: - if (im->image8) { - return bicubic_filter8; - } else if (im->image32) { - switch (im->type) { - case IMAGING_TYPE_UINT8: - if (im->bands == 2) { - return bicubic_filter32LA; - } else { - return bicubic_filter32RGB; - } - case IMAGING_TYPE_INT32: - return bicubic_filter32I; - case IMAGING_TYPE_FLOAT32: - return bicubic_filter32F; - } - } - break; - } - /* no such filter */ - return NULL; -} - -/* transformation engines */ - -Imaging -ImagingGenericTransform( - Imaging imOut, - Imaging imIn, - int x0, - int y0, - int x1, - int y1, - ImagingTransformMap transform, - void *transform_data, - int filterid, - int fill) { - /* slow generic transformation. use ImagingTransformAffine or - ImagingScaleAffine where possible. */ - - ImagingSectionCookie cookie; - int x, y; - char *out; - double xx, yy; - - ImagingTransformFilter filter = getfilter(imIn, filterid); - if (!filter) { - return (Imaging)ImagingError_ValueError("bad filter number"); - } - - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { - return (Imaging)ImagingError_ModeError(); - } - - ImagingCopyPalette(imOut, imIn); - - ImagingSectionEnter(&cookie); - - if (x0 < 0) { - x0 = 0; - } - if (y0 < 0) { - y0 = 0; - } - if (x1 > imOut->xsize) { - x1 = imOut->xsize; - } - if (y1 > imOut->ysize) { - y1 = imOut->ysize; - } - - for (y = y0; y < y1; y++) { - out = imOut->image[y] + x0 * imOut->pixelsize; - for (x = x0; x < x1; x++) { - if (!transform(&xx, &yy, x - x0, y - y0, transform_data) || - !filter(out, imIn, xx, yy)) { - if (fill) { - memset(out, 0, imOut->pixelsize); - } - } - out += imOut->pixelsize; - } - } - - ImagingSectionLeave(&cookie); - - return imOut; -} - -static Imaging -ImagingScaleAffine( - Imaging imOut, - Imaging imIn, - int x0, - int y0, - int x1, - int y1, - double a[6], - int fill) { - /* scale, nearest neighbour resampling */ - - ImagingSectionCookie cookie; - int x, y; - int xin; - double xo, yo; - int xmin, xmax; - int *xintab; - - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { - return (Imaging)ImagingError_ModeError(); - } - - ImagingCopyPalette(imOut, imIn); - - if (x0 < 0) { - x0 = 0; - } - if (y0 < 0) { - y0 = 0; - } - if (x1 > imOut->xsize) { - x1 = imOut->xsize; - } - if (y1 > imOut->ysize) { - y1 = imOut->ysize; - } - - /* malloc check ok, uses calloc for overflow */ - xintab = (int *)calloc(imOut->xsize, sizeof(int)); - if (!xintab) { - ImagingDelete(imOut); - return (Imaging)ImagingError_MemoryError(); - } - - xo = a[2] + a[0] * 0.5; - yo = a[5] + a[4] * 0.5; - - xmin = x1; - xmax = x0; - - /* Pretabulate horizontal pixel positions */ - for (x = x0; x < x1; x++) { - xin = COORD(xo); - if (xin >= 0 && xin < (int)imIn->xsize) { - xmax = x + 1; - if (x < xmin) { - xmin = x; - } - xintab[x] = xin; - } - xo += a[0]; - } - -#define AFFINE_SCALE(pixel, image) \ - for (y = y0; y < y1; y++) { \ - int yi = COORD(yo); \ - pixel *in, *out; \ - out = imOut->image[y]; \ - if (fill && x1 > x0) { \ - memset(out + x0, 0, (x1 - x0) * sizeof(pixel)); \ - } \ - if (yi >= 0 && yi < imIn->ysize) { \ - in = imIn->image[yi]; \ - for (x = xmin; x < xmax; x++) { \ - out[x] = in[xintab[x]]; \ - } \ - } \ - yo += a[4]; \ - } - - ImagingSectionEnter(&cookie); - - if (imIn->image8) { - AFFINE_SCALE(UINT8, image8); - } else { - AFFINE_SCALE(INT32, image32); - } - - ImagingSectionLeave(&cookie); - -#undef AFFINE_SCALE - - free(xintab); - - return imOut; -} - -static inline int -check_fixed(double a[6], int x, int y) { - return ( - fabs(x * a[0] + y * a[1] + a[2]) < 32768.0 && - fabs(x * a[3] + y * a[4] + a[5]) < 32768.0); -} - -static inline Imaging -affine_fixed( - Imaging imOut, - Imaging imIn, - int x0, - int y0, - int x1, - int y1, - double a[6], - int filterid, - int fill) { - /* affine transform, nearest neighbour resampling, fixed point - arithmetics */ - - ImagingSectionCookie cookie; - int x, y; - int xin, yin; - int xsize, ysize; - int xx, yy; - int a0, a1, a2, a3, a4, a5; - - ImagingCopyPalette(imOut, imIn); - - xsize = (int)imIn->xsize; - ysize = (int)imIn->ysize; - -/* use 16.16 fixed point arithmetics */ -#define FIX(v) FLOOR((v) * 65536.0 + 0.5) - - a0 = FIX(a[0]); - a1 = FIX(a[1]); - a3 = FIX(a[3]); - a4 = FIX(a[4]); - a2 = FIX(a[2] + a[0] * 0.5 + a[1] * 0.5); - a5 = FIX(a[5] + a[3] * 0.5 + a[4] * 0.5); - -#undef FIX - -#define AFFINE_TRANSFORM_FIXED(pixel, image) \ - for (y = y0; y < y1; y++) { \ - pixel *out; \ - xx = a2; \ - yy = a5; \ - out = imOut->image[y]; \ - if (fill && x1 > x0) { \ - memset(out + x0, 0, (x1 - x0) * sizeof(pixel)); \ - } \ - for (x = x0; x < x1; x++, out++) { \ - xin = xx >> 16; \ - if (xin >= 0 && xin < xsize) { \ - yin = yy >> 16; \ - if (yin >= 0 && yin < ysize) { \ - *out = imIn->image[yin][xin]; \ - } \ - } \ - xx += a0; \ - yy += a3; \ - } \ - a2 += a1; \ - a5 += a4; \ - } - - ImagingSectionEnter(&cookie); - - if (imIn->image8) { - AFFINE_TRANSFORM_FIXED(UINT8, image8) - } else { - AFFINE_TRANSFORM_FIXED(INT32, image32) - } - - ImagingSectionLeave(&cookie); - -#undef AFFINE_TRANSFORM_FIXED - - return imOut; -} - -Imaging -ImagingTransformAffine( - Imaging imOut, - Imaging imIn, - int x0, - int y0, - int x1, - int y1, - double a[6], - int filterid, - int fill) { - /* affine transform, nearest neighbour resampling, floating point - arithmetics*/ - - ImagingSectionCookie cookie; - int x, y; - int xin, yin; - int xsize, ysize; - double xx, yy; - double xo, yo; - - if (filterid || imIn->type == IMAGING_TYPE_SPECIAL) { - return ImagingGenericTransform( - imOut, imIn, x0, y0, x1, y1, affine_transform, a, filterid, fill); - } - - if (a[1] == 0 && a[3] == 0) { - /* Scaling */ - return ImagingScaleAffine(imOut, imIn, x0, y0, x1, y1, a, fill); - } - - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { - return (Imaging)ImagingError_ModeError(); - } - - if (x0 < 0) { - x0 = 0; - } - if (y0 < 0) { - y0 = 0; - } - if (x1 > imOut->xsize) { - x1 = imOut->xsize; - } - if (y1 > imOut->ysize) { - y1 = imOut->ysize; - } - - /* translate all four corners to check if they are within the - range that can be represented by the fixed point arithmetics */ - - if (check_fixed(a, 0, 0) && check_fixed(a, x1 - x0, y1 - y0) && - check_fixed(a, 0, y1 - y0) && check_fixed(a, x1 - x0, 0)) { - return affine_fixed(imOut, imIn, x0, y0, x1, y1, a, filterid, fill); - } - - /* FIXME: cannot really think of any reasonable case when the - following code is used. maybe we should fall back on the slow - generic transform engine in this case? */ - - ImagingCopyPalette(imOut, imIn); - - xsize = (int)imIn->xsize; - ysize = (int)imIn->ysize; - - xo = a[2] + a[1] * 0.5 + a[0] * 0.5; - yo = a[5] + a[4] * 0.5 + a[3] * 0.5; - -#define AFFINE_TRANSFORM(pixel, image) \ - for (y = y0; y < y1; y++) { \ - pixel *out; \ - xx = xo; \ - yy = yo; \ - out = imOut->image[y]; \ - if (fill && x1 > x0) { \ - memset(out + x0, 0, (x1 - x0) * sizeof(pixel)); \ - } \ - for (x = x0; x < x1; x++, out++) { \ - xin = COORD(xx); \ - if (xin >= 0 && xin < xsize) { \ - yin = COORD(yy); \ - if (yin >= 0 && yin < ysize) { \ - *out = imIn->image[yin][xin]; \ - } \ - } \ - xx += a[0]; \ - yy += a[3]; \ - } \ - xo += a[1]; \ - yo += a[4]; \ - } - - ImagingSectionEnter(&cookie); - - if (imIn->image8) { - AFFINE_TRANSFORM(UINT8, image8) - } else { - AFFINE_TRANSFORM(INT32, image32) - } - - ImagingSectionLeave(&cookie); - -#undef AFFINE_TRANSFORM - - return imOut; -} - -Imaging -ImagingTransform( - Imaging imOut, - Imaging imIn, - int method, - int x0, - int y0, - int x1, - int y1, - double a[8], - int filterid, - int fill) { - ImagingTransformMap transform; - - switch (method) { - case IMAGING_TRANSFORM_AFFINE: - return ImagingTransformAffine( - imOut, imIn, x0, y0, x1, y1, a, filterid, fill); - break; - case IMAGING_TRANSFORM_PERSPECTIVE: - transform = perspective_transform; - break; - case IMAGING_TRANSFORM_QUAD: - transform = quad_transform; - break; - default: - return (Imaging)ImagingError_ValueError("bad transform method"); - } - - return ImagingGenericTransform( - imOut, imIn, x0, y0, x1, y1, transform, a, filterid, fill); -} +#include "Imaging.h" + +/* For large images rotation is an inefficient operation in terms of CPU cache. + One row in the source image affects each column in destination. + Rotating in chunks that fit in the cache can speed up rotation + 8x on a modern CPU. A chunk size of 128 requires only 65k and is large enough + that the overhead from the extra loops are not apparent. */ +#define ROTATE_CHUNK 512 +#define ROTATE_SMALL_CHUNK 8 + +#define COORD(v) ((v) < 0.0 ? -1 : ((int)(v))) +#define FLOOR(v) ((v) < 0.0 ? ((int)floor(v)) : ((int)(v))) + +/* -------------------------------------------------------------------- */ +/* Transpose operations */ + +Imaging +ImagingFlipLeftRight(Imaging imOut, Imaging imIn) { + ImagingSectionCookie cookie; + int x, y, xr; + + if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { + return (Imaging)ImagingError_ModeError(); + } + if (imIn->xsize != imOut->xsize || imIn->ysize != imOut->ysize) { + return (Imaging)ImagingError_Mismatch(); + } + + ImagingCopyPalette(imOut, imIn); + +#define FLIP_LEFT_RIGHT(INT, image) \ + for (y = 0; y < imIn->ysize; y++) { \ + INT *in = (INT *)imIn->image[y]; \ + INT *out = (INT *)imOut->image[y]; \ + xr = imIn->xsize - 1; \ + for (x = 0; x < imIn->xsize; x++, xr--) { \ + out[xr] = in[x]; \ + } \ + } + + ImagingSectionEnter(&cookie); + + if (imIn->image8) { + if (strncmp(imIn->mode, "I;16", 4) == 0) { + FLIP_LEFT_RIGHT(UINT16, image8) + } else { + FLIP_LEFT_RIGHT(UINT8, image8) + } + } else { + FLIP_LEFT_RIGHT(INT32, image32) + } + + ImagingSectionLeave(&cookie); + +#undef FLIP_LEFT_RIGHT + + return imOut; +} + +Imaging +ImagingFlipTopBottom(Imaging imOut, Imaging imIn) { + ImagingSectionCookie cookie; + int y, yr; + + if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { + return (Imaging)ImagingError_ModeError(); + } + if (imIn->xsize != imOut->xsize || imIn->ysize != imOut->ysize) { + return (Imaging)ImagingError_Mismatch(); + } + + ImagingCopyPalette(imOut, imIn); + + ImagingSectionEnter(&cookie); + + yr = imIn->ysize - 1; + for (y = 0; y < imIn->ysize; y++, yr--) { + memcpy(imOut->image[yr], imIn->image[y], imIn->linesize); + } + + ImagingSectionLeave(&cookie); + + return imOut; +} + +Imaging +ImagingRotate90(Imaging imOut, Imaging imIn) { + ImagingSectionCookie cookie; + int x, y, xx, yy, xr, xxsize, yysize; + int xxx, yyy, xxxsize, yyysize; + + if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { + return (Imaging)ImagingError_ModeError(); + } + if (imIn->xsize != imOut->ysize || imIn->ysize != imOut->xsize) { + return (Imaging)ImagingError_Mismatch(); + } + + ImagingCopyPalette(imOut, imIn); + +#define ROTATE_90(INT, image) \ + for (y = 0; y < imIn->ysize; y += ROTATE_CHUNK) { \ + for (x = 0; x < imIn->xsize; x += ROTATE_CHUNK) { \ + yysize = y + ROTATE_CHUNK < imIn->ysize ? y + ROTATE_CHUNK : imIn->ysize; \ + xxsize = x + ROTATE_CHUNK < imIn->xsize ? x + ROTATE_CHUNK : imIn->xsize; \ + for (yy = y; yy < yysize; yy += ROTATE_SMALL_CHUNK) { \ + for (xx = x; xx < xxsize; xx += ROTATE_SMALL_CHUNK) { \ + yyysize = yy + ROTATE_SMALL_CHUNK < imIn->ysize \ + ? yy + ROTATE_SMALL_CHUNK \ + : imIn->ysize; \ + xxxsize = xx + ROTATE_SMALL_CHUNK < imIn->xsize \ + ? xx + ROTATE_SMALL_CHUNK \ + : imIn->xsize; \ + for (yyy = yy; yyy < yyysize; yyy++) { \ + INT *in = (INT *)imIn->image[yyy]; \ + xr = imIn->xsize - 1 - xx; \ + for (xxx = xx; xxx < xxxsize; xxx++, xr--) { \ + INT *out = (INT *)imOut->image[xr]; \ + out[yyy] = in[xxx]; \ + } \ + } \ + } \ + } \ + } \ + } + + ImagingSectionEnter(&cookie); + + if (imIn->image8) { + if (strncmp(imIn->mode, "I;16", 4) == 0) { + ROTATE_90(UINT16, image8); + } else { + ROTATE_90(UINT8, image8); + } + } else { + ROTATE_90(INT32, image32); + } + + ImagingSectionLeave(&cookie); + +#undef ROTATE_90 + + return imOut; +} + +Imaging +ImagingTranspose(Imaging imOut, Imaging imIn) { + ImagingSectionCookie cookie; + int x, y, xx, yy, xxsize, yysize; + int xxx, yyy, xxxsize, yyysize; + + if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { + return (Imaging)ImagingError_ModeError(); + } + if (imIn->xsize != imOut->ysize || imIn->ysize != imOut->xsize) { + return (Imaging)ImagingError_Mismatch(); + } + + ImagingCopyPalette(imOut, imIn); + +#define TRANSPOSE(INT, image) \ + for (y = 0; y < imIn->ysize; y += ROTATE_CHUNK) { \ + for (x = 0; x < imIn->xsize; x += ROTATE_CHUNK) { \ + yysize = y + ROTATE_CHUNK < imIn->ysize ? y + ROTATE_CHUNK : imIn->ysize; \ + xxsize = x + ROTATE_CHUNK < imIn->xsize ? x + ROTATE_CHUNK : imIn->xsize; \ + for (yy = y; yy < yysize; yy += ROTATE_SMALL_CHUNK) { \ + for (xx = x; xx < xxsize; xx += ROTATE_SMALL_CHUNK) { \ + yyysize = yy + ROTATE_SMALL_CHUNK < imIn->ysize \ + ? yy + ROTATE_SMALL_CHUNK \ + : imIn->ysize; \ + xxxsize = xx + ROTATE_SMALL_CHUNK < imIn->xsize \ + ? xx + ROTATE_SMALL_CHUNK \ + : imIn->xsize; \ + for (yyy = yy; yyy < yyysize; yyy++) { \ + INT *in = (INT *)imIn->image[yyy]; \ + for (xxx = xx; xxx < xxxsize; xxx++) { \ + INT *out = (INT *)imOut->image[xxx]; \ + out[yyy] = in[xxx]; \ + } \ + } \ + } \ + } \ + } \ + } + + ImagingSectionEnter(&cookie); + + if (imIn->image8) { + if (strncmp(imIn->mode, "I;16", 4) == 0) { + TRANSPOSE(UINT16, image8); + } else { + TRANSPOSE(UINT8, image8); + } + } else { + TRANSPOSE(INT32, image32); + } + + ImagingSectionLeave(&cookie); + +#undef TRANSPOSE + + return imOut; +} + +Imaging +ImagingTransverse(Imaging imOut, Imaging imIn) { + ImagingSectionCookie cookie; + int x, y, xr, yr, xx, yy, xxsize, yysize; + int xxx, yyy, xxxsize, yyysize; + + if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { + return (Imaging)ImagingError_ModeError(); + } + if (imIn->xsize != imOut->ysize || imIn->ysize != imOut->xsize) { + return (Imaging)ImagingError_Mismatch(); + } + + ImagingCopyPalette(imOut, imIn); + +#define TRANSVERSE(INT, image) \ + for (y = 0; y < imIn->ysize; y += ROTATE_CHUNK) { \ + for (x = 0; x < imIn->xsize; x += ROTATE_CHUNK) { \ + yysize = y + ROTATE_CHUNK < imIn->ysize ? y + ROTATE_CHUNK : imIn->ysize; \ + xxsize = x + ROTATE_CHUNK < imIn->xsize ? x + ROTATE_CHUNK : imIn->xsize; \ + for (yy = y; yy < yysize; yy += ROTATE_SMALL_CHUNK) { \ + for (xx = x; xx < xxsize; xx += ROTATE_SMALL_CHUNK) { \ + yyysize = yy + ROTATE_SMALL_CHUNK < imIn->ysize \ + ? yy + ROTATE_SMALL_CHUNK \ + : imIn->ysize; \ + xxxsize = xx + ROTATE_SMALL_CHUNK < imIn->xsize \ + ? xx + ROTATE_SMALL_CHUNK \ + : imIn->xsize; \ + yr = imIn->ysize - 1 - yy; \ + for (yyy = yy; yyy < yyysize; yyy++, yr--) { \ + INT *in = (INT *)imIn->image[yyy]; \ + xr = imIn->xsize - 1 - xx; \ + for (xxx = xx; xxx < xxxsize; xxx++, xr--) { \ + INT *out = (INT *)imOut->image[xr]; \ + out[yr] = in[xxx]; \ + } \ + } \ + } \ + } \ + } \ + } + + ImagingSectionEnter(&cookie); + + if (imIn->image8) { + if (strncmp(imIn->mode, "I;16", 4) == 0) { + TRANSVERSE(UINT16, image8); + } else { + TRANSVERSE(UINT8, image8); + } + } else { + TRANSVERSE(INT32, image32); + } + + ImagingSectionLeave(&cookie); + +#undef TRANSVERSE + + return imOut; +} + +Imaging +ImagingRotate180(Imaging imOut, Imaging imIn) { + ImagingSectionCookie cookie; + int x, y, xr, yr; + + if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { + return (Imaging)ImagingError_ModeError(); + } + if (imIn->xsize != imOut->xsize || imIn->ysize != imOut->ysize) { + return (Imaging)ImagingError_Mismatch(); + } + + ImagingCopyPalette(imOut, imIn); + +#define ROTATE_180(INT, image) \ + for (y = 0; y < imIn->ysize; y++, yr--) { \ + INT *in = (INT *)imIn->image[y]; \ + INT *out = (INT *)imOut->image[yr]; \ + xr = imIn->xsize - 1; \ + for (x = 0; x < imIn->xsize; x++, xr--) { \ + out[xr] = in[x]; \ + } \ + } + + ImagingSectionEnter(&cookie); + + yr = imIn->ysize - 1; + if (imIn->image8) { + if (strncmp(imIn->mode, "I;16", 4) == 0) { + ROTATE_180(UINT16, image8) + } else { + ROTATE_180(UINT8, image8) + } + } else { + ROTATE_180(INT32, image32) + } + + ImagingSectionLeave(&cookie); + +#undef ROTATE_180 + + return imOut; +} + +Imaging +ImagingRotate270(Imaging imOut, Imaging imIn) { + ImagingSectionCookie cookie; + int x, y, xx, yy, yr, xxsize, yysize; + int xxx, yyy, xxxsize, yyysize; + + if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { + return (Imaging)ImagingError_ModeError(); + } + if (imIn->xsize != imOut->ysize || imIn->ysize != imOut->xsize) { + return (Imaging)ImagingError_Mismatch(); + } + + ImagingCopyPalette(imOut, imIn); + +#define ROTATE_270(INT, image) \ + for (y = 0; y < imIn->ysize; y += ROTATE_CHUNK) { \ + for (x = 0; x < imIn->xsize; x += ROTATE_CHUNK) { \ + yysize = y + ROTATE_CHUNK < imIn->ysize ? y + ROTATE_CHUNK : imIn->ysize; \ + xxsize = x + ROTATE_CHUNK < imIn->xsize ? x + ROTATE_CHUNK : imIn->xsize; \ + for (yy = y; yy < yysize; yy += ROTATE_SMALL_CHUNK) { \ + for (xx = x; xx < xxsize; xx += ROTATE_SMALL_CHUNK) { \ + yyysize = yy + ROTATE_SMALL_CHUNK < imIn->ysize \ + ? yy + ROTATE_SMALL_CHUNK \ + : imIn->ysize; \ + xxxsize = xx + ROTATE_SMALL_CHUNK < imIn->xsize \ + ? xx + ROTATE_SMALL_CHUNK \ + : imIn->xsize; \ + yr = imIn->ysize - 1 - yy; \ + for (yyy = yy; yyy < yyysize; yyy++, yr--) { \ + INT *in = (INT *)imIn->image[yyy]; \ + for (xxx = xx; xxx < xxxsize; xxx++) { \ + INT *out = (INT *)imOut->image[xxx]; \ + out[yr] = in[xxx]; \ + } \ + } \ + } \ + } \ + } \ + } + + ImagingSectionEnter(&cookie); + + if (imIn->image8) { + if (strncmp(imIn->mode, "I;16", 4) == 0) { + ROTATE_270(UINT16, image8); + } else { + ROTATE_270(UINT8, image8); + } + } else { + ROTATE_270(INT32, image32); + } + + ImagingSectionLeave(&cookie); + +#undef ROTATE_270 + + return imOut; +} + +/* -------------------------------------------------------------------- */ +/* Transforms */ + +/* transform primitives (ImagingTransformMap) */ + +static int +affine_transform(double *xout, double *yout, int x, int y, void *data) { + /* full moon tonight. your compiler will generate bogus code + for simple expressions, unless you reorganize the code, or + install Service Pack 3 */ + + double *a = (double *)data; + double a0 = a[0]; + double a1 = a[1]; + double a2 = a[2]; + double a3 = a[3]; + double a4 = a[4]; + double a5 = a[5]; + + double xin = x + 0.5; + double yin = y + 0.5; + + xout[0] = a0 * xin + a1 * yin + a2; + yout[0] = a3 * xin + a4 * yin + a5; + + return 1; +} + +static int +perspective_transform(double *xout, double *yout, int x, int y, void *data) { + double *a = (double *)data; + double a0 = a[0]; + double a1 = a[1]; + double a2 = a[2]; + double a3 = a[3]; + double a4 = a[4]; + double a5 = a[5]; + double a6 = a[6]; + double a7 = a[7]; + + double xin = x + 0.5; + double yin = y + 0.5; + + xout[0] = (a0 * xin + a1 * yin + a2) / (a6 * xin + a7 * yin + 1); + yout[0] = (a3 * xin + a4 * yin + a5) / (a6 * xin + a7 * yin + 1); + + return 1; +} + +static int +quad_transform(double *xout, double *yout, int x, int y, void *data) { + /* quad warp: map quadrilateral to rectangle */ + + double *a = (double *)data; + double a0 = a[0]; + double a1 = a[1]; + double a2 = a[2]; + double a3 = a[3]; + double a4 = a[4]; + double a5 = a[5]; + double a6 = a[6]; + double a7 = a[7]; + + double xin = x + 0.5; + double yin = y + 0.5; + + xout[0] = a0 + a1 * xin + a2 * yin + a3 * xin * yin; + yout[0] = a4 + a5 * xin + a6 * yin + a7 * xin * yin; + + return 1; +} + +/* transform filters (ImagingTransformFilter) */ + +static int +nearest_filter8(void *out, Imaging im, double xin, double yin) { + int x = COORD(xin); + int y = COORD(yin); + if (x < 0 || x >= im->xsize || y < 0 || y >= im->ysize) { + return 0; + } + ((UINT8 *)out)[0] = im->image8[y][x]; + return 1; +} + +static int +nearest_filter16(void *out, Imaging im, double xin, double yin) { + int x = COORD(xin); + int y = COORD(yin); + if (x < 0 || x >= im->xsize || y < 0 || y >= im->ysize) { + return 0; + } + memcpy(out, im->image8[y] + x * sizeof(INT16), sizeof(INT16)); + return 1; +} + +static int +nearest_filter32(void *out, Imaging im, double xin, double yin) { + int x = COORD(xin); + int y = COORD(yin); + if (x < 0 || x >= im->xsize || y < 0 || y >= im->ysize) { + return 0; + } + memcpy(out, &im->image32[y][x], sizeof(INT32)); + return 1; +} + +#define XCLIP(im, x) (((x) < 0) ? 0 : ((x) < im->xsize) ? (x) : im->xsize - 1) +#define YCLIP(im, y) (((y) < 0) ? 0 : ((y) < im->ysize) ? (y) : im->ysize - 1) + +#define BILINEAR(v, a, b, d) (v = (a) + ((b) - (a)) * (d)) + +#define BILINEAR_HEAD(type) \ + int x, y; \ + int x0, x1; \ + double v1, v2; \ + double dx, dy; \ + type *in; \ + if (xin < 0.0 || xin >= im->xsize || yin < 0.0 || yin >= im->ysize) { \ + return 0; \ + } \ + xin -= 0.5; \ + yin -= 0.5; \ + x = FLOOR(xin); \ + y = FLOOR(yin); \ + dx = xin - x; \ + dy = yin - y; + +#define BILINEAR_BODY(type, image, step, offset) \ + { \ + in = (type *)((image)[YCLIP(im, y)] + offset); \ + x0 = XCLIP(im, x + 0) * step; \ + x1 = XCLIP(im, x + 1) * step; \ + BILINEAR(v1, in[x0], in[x1], dx); \ + if (y + 1 >= 0 && y + 1 < im->ysize) { \ + in = (type *)((image)[y + 1] + offset); \ + BILINEAR(v2, in[x0], in[x1], dx); \ + } else { \ + v2 = v1; \ + } \ + BILINEAR(v1, v1, v2, dy); \ + } + +static int +bilinear_filter8(void *out, Imaging im, double xin, double yin) { + BILINEAR_HEAD(UINT8); + BILINEAR_BODY(UINT8, im->image8, 1, 0); + ((UINT8 *)out)[0] = (UINT8)v1; + return 1; +} + +static int +bilinear_filter32I(void *out, Imaging im, double xin, double yin) { + INT32 k; + BILINEAR_HEAD(INT32); + BILINEAR_BODY(INT32, im->image32, 1, 0); + k = v1; + memcpy(out, &k, sizeof(k)); + return 1; +} + +static int +bilinear_filter32F(void *out, Imaging im, double xin, double yin) { + FLOAT32 k; + BILINEAR_HEAD(FLOAT32); + BILINEAR_BODY(FLOAT32, im->image32, 1, 0); + k = v1; + memcpy(out, &k, sizeof(k)); + return 1; +} + +static int +bilinear_filter32LA(void *out, Imaging im, double xin, double yin) { + BILINEAR_HEAD(UINT8); + BILINEAR_BODY(UINT8, im->image, 4, 0); + ((UINT8 *)out)[0] = (UINT8)v1; + ((UINT8 *)out)[1] = (UINT8)v1; + ((UINT8 *)out)[2] = (UINT8)v1; + BILINEAR_BODY(UINT8, im->image, 4, 3); + ((UINT8 *)out)[3] = (UINT8)v1; + return 1; +} + +static int +bilinear_filter32RGB(void *out, Imaging im, double xin, double yin) { + int b; + BILINEAR_HEAD(UINT8); + for (b = 0; b < im->bands; b++) { + BILINEAR_BODY(UINT8, im->image, 4, b); + ((UINT8 *)out)[b] = (UINT8)v1; + } + return 1; +} + +#undef BILINEAR +#undef BILINEAR_HEAD +#undef BILINEAR_BODY + +#define BICUBIC(v, v1, v2, v3, v4, d) \ + { \ + double p1 = v2; \ + double p2 = -v1 + v3; \ + double p3 = 2 * (v1 - v2) + v3 - v4; \ + double p4 = -v1 + v2 - v3 + v4; \ + v = p1 + (d) * (p2 + (d) * (p3 + (d) * p4)); \ + } + +#define BICUBIC_HEAD(type) \ + int x = FLOOR(xin); \ + int y = FLOOR(yin); \ + int x0, x1, x2, x3; \ + double v1, v2, v3, v4; \ + double dx, dy; \ + type *in; \ + if (xin < 0.0 || xin >= im->xsize || yin < 0.0 || yin >= im->ysize) { \ + return 0; \ + } \ + xin -= 0.5; \ + yin -= 0.5; \ + x = FLOOR(xin); \ + y = FLOOR(yin); \ + dx = xin - x; \ + dy = yin - y; \ + x--; \ + y--; + +#define BICUBIC_BODY(type, image, step, offset) \ + { \ + in = (type *)((image)[YCLIP(im, y)] + offset); \ + x0 = XCLIP(im, x + 0) * step; \ + x1 = XCLIP(im, x + 1) * step; \ + x2 = XCLIP(im, x + 2) * step; \ + x3 = XCLIP(im, x + 3) * step; \ + BICUBIC(v1, in[x0], in[x1], in[x2], in[x3], dx); \ + if (y + 1 >= 0 && y + 1 < im->ysize) { \ + in = (type *)((image)[y + 1] + offset); \ + BICUBIC(v2, in[x0], in[x1], in[x2], in[x3], dx); \ + } else { \ + v2 = v1; \ + } \ + if (y + 2 >= 0 && y + 2 < im->ysize) { \ + in = (type *)((image)[y + 2] + offset); \ + BICUBIC(v3, in[x0], in[x1], in[x2], in[x3], dx); \ + } else { \ + v3 = v2; \ + } \ + if (y + 3 >= 0 && y + 3 < im->ysize) { \ + in = (type *)((image)[y + 3] + offset); \ + BICUBIC(v4, in[x0], in[x1], in[x2], in[x3], dx); \ + } else { \ + v4 = v3; \ + } \ + BICUBIC(v1, v1, v2, v3, v4, dy); \ + } + +static int +bicubic_filter8(void *out, Imaging im, double xin, double yin) { + BICUBIC_HEAD(UINT8); + BICUBIC_BODY(UINT8, im->image8, 1, 0); + if (v1 <= 0.0) { + ((UINT8 *)out)[0] = 0; + } else if (v1 >= 255.0) { + ((UINT8 *)out)[0] = 255; + } else { + ((UINT8 *)out)[0] = (UINT8)v1; + } + return 1; +} + +static int +bicubic_filter32I(void *out, Imaging im, double xin, double yin) { + INT32 k; + BICUBIC_HEAD(INT32); + BICUBIC_BODY(INT32, im->image32, 1, 0); + k = v1; + memcpy(out, &k, sizeof(k)); + return 1; +} + +static int +bicubic_filter32F(void *out, Imaging im, double xin, double yin) { + FLOAT32 k; + BICUBIC_HEAD(FLOAT32); + BICUBIC_BODY(FLOAT32, im->image32, 1, 0); + k = v1; + memcpy(out, &k, sizeof(k)); + return 1; +} + +static int +bicubic_filter32LA(void *out, Imaging im, double xin, double yin) { + BICUBIC_HEAD(UINT8); + BICUBIC_BODY(UINT8, im->image, 4, 0); + if (v1 <= 0.0) { + ((UINT8 *)out)[0] = 0; + ((UINT8 *)out)[1] = 0; + ((UINT8 *)out)[2] = 0; + } else if (v1 >= 255.0) { + ((UINT8 *)out)[0] = 255; + ((UINT8 *)out)[1] = 255; + ((UINT8 *)out)[2] = 255; + } else { + ((UINT8 *)out)[0] = (UINT8)v1; + ((UINT8 *)out)[1] = (UINT8)v1; + ((UINT8 *)out)[2] = (UINT8)v1; + } + BICUBIC_BODY(UINT8, im->image, 4, 3); + if (v1 <= 0.0) { + ((UINT8 *)out)[3] = 0; + } else if (v1 >= 255.0) { + ((UINT8 *)out)[3] = 255; + } else { + ((UINT8 *)out)[3] = (UINT8)v1; + } + return 1; +} + +static int +bicubic_filter32RGB(void *out, Imaging im, double xin, double yin) { + int b; + BICUBIC_HEAD(UINT8); + for (b = 0; b < im->bands; b++) { + BICUBIC_BODY(UINT8, im->image, 4, b); + if (v1 <= 0.0) { + ((UINT8 *)out)[b] = 0; + } else if (v1 >= 255.0) { + ((UINT8 *)out)[b] = 255; + } else { + ((UINT8 *)out)[b] = (UINT8)v1; + } + } + return 1; +} + +#undef BICUBIC +#undef BICUBIC_HEAD +#undef BICUBIC_BODY + +static ImagingTransformFilter +getfilter(Imaging im, int filterid) { + switch (filterid) { + case IMAGING_TRANSFORM_NEAREST: + if (im->image8) { + switch (im->type) { + case IMAGING_TYPE_UINT8: + return nearest_filter8; + case IMAGING_TYPE_SPECIAL: + switch (im->pixelsize) { + case 1: + return nearest_filter8; + case 2: + return nearest_filter16; + case 4: + return nearest_filter32; + } + } + } else { + return nearest_filter32; + } + break; + case IMAGING_TRANSFORM_BILINEAR: + if (im->image8) { + return bilinear_filter8; + } else if (im->image32) { + switch (im->type) { + case IMAGING_TYPE_UINT8: + if (im->bands == 2) { + return bilinear_filter32LA; + } else { + return bilinear_filter32RGB; + } + case IMAGING_TYPE_INT32: + return bilinear_filter32I; + case IMAGING_TYPE_FLOAT32: + return bilinear_filter32F; + } + } + break; + case IMAGING_TRANSFORM_BICUBIC: + if (im->image8) { + return bicubic_filter8; + } else if (im->image32) { + switch (im->type) { + case IMAGING_TYPE_UINT8: + if (im->bands == 2) { + return bicubic_filter32LA; + } else { + return bicubic_filter32RGB; + } + case IMAGING_TYPE_INT32: + return bicubic_filter32I; + case IMAGING_TYPE_FLOAT32: + return bicubic_filter32F; + } + } + break; + } + /* no such filter */ + return NULL; +} + +/* transformation engines */ + +Imaging +ImagingGenericTransform( + Imaging imOut, + Imaging imIn, + int x0, + int y0, + int x1, + int y1, + ImagingTransformMap transform, + void *transform_data, + int filterid, + int fill) { + /* slow generic transformation. use ImagingTransformAffine or + ImagingScaleAffine where possible. */ + + ImagingSectionCookie cookie; + int x, y; + char *out; + double xx, yy; + + ImagingTransformFilter filter = getfilter(imIn, filterid); + if (!filter) { + return (Imaging)ImagingError_ValueError("bad filter number"); + } + + if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { + return (Imaging)ImagingError_ModeError(); + } + + ImagingCopyPalette(imOut, imIn); + + ImagingSectionEnter(&cookie); + + if (x0 < 0) { + x0 = 0; + } + if (y0 < 0) { + y0 = 0; + } + if (x1 > imOut->xsize) { + x1 = imOut->xsize; + } + if (y1 > imOut->ysize) { + y1 = imOut->ysize; + } + + for (y = y0; y < y1; y++) { + out = imOut->image[y] + x0 * imOut->pixelsize; + for (x = x0; x < x1; x++) { + if (!transform(&xx, &yy, x - x0, y - y0, transform_data) || + !filter(out, imIn, xx, yy)) { + if (fill) { + memset(out, 0, imOut->pixelsize); + } + } + out += imOut->pixelsize; + } + } + + ImagingSectionLeave(&cookie); + + return imOut; +} + +static Imaging +ImagingScaleAffine( + Imaging imOut, + Imaging imIn, + int x0, + int y0, + int x1, + int y1, + double a[6], + int fill) { + /* scale, nearest neighbour resampling */ + + ImagingSectionCookie cookie; + int x, y; + int xin; + double xo, yo; + int xmin, xmax; + int *xintab; + + if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { + return (Imaging)ImagingError_ModeError(); + } + + ImagingCopyPalette(imOut, imIn); + + if (x0 < 0) { + x0 = 0; + } + if (y0 < 0) { + y0 = 0; + } + if (x1 > imOut->xsize) { + x1 = imOut->xsize; + } + if (y1 > imOut->ysize) { + y1 = imOut->ysize; + } + + /* malloc check ok, uses calloc for overflow */ + xintab = (int *)calloc(imOut->xsize, sizeof(int)); + if (!xintab) { + ImagingDelete(imOut); + return (Imaging)ImagingError_MemoryError(); + } + + xo = a[2] + a[0] * 0.5; + yo = a[5] + a[4] * 0.5; + + xmin = x1; + xmax = x0; + + /* Pretabulate horizontal pixel positions */ + for (x = x0; x < x1; x++) { + xin = COORD(xo); + if (xin >= 0 && xin < (int)imIn->xsize) { + xmax = x + 1; + if (x < xmin) { + xmin = x; + } + xintab[x] = xin; + } + xo += a[0]; + } + +#define AFFINE_SCALE(pixel, image) \ + for (y = y0; y < y1; y++) { \ + int yi = COORD(yo); \ + pixel *in, *out; \ + out = imOut->image[y]; \ + if (fill && x1 > x0) { \ + memset(out + x0, 0, (x1 - x0) * sizeof(pixel)); \ + } \ + if (yi >= 0 && yi < imIn->ysize) { \ + in = imIn->image[yi]; \ + for (x = xmin; x < xmax; x++) { \ + out[x] = in[xintab[x]]; \ + } \ + } \ + yo += a[4]; \ + } + + ImagingSectionEnter(&cookie); + + if (imIn->image8) { + AFFINE_SCALE(UINT8, image8); + } else { + AFFINE_SCALE(INT32, image32); + } + + ImagingSectionLeave(&cookie); + +#undef AFFINE_SCALE + + free(xintab); + + return imOut; +} + +static inline int +check_fixed(double a[6], int x, int y) { + return ( + fabs(x * a[0] + y * a[1] + a[2]) < 32768.0 && + fabs(x * a[3] + y * a[4] + a[5]) < 32768.0); +} + +static inline Imaging +affine_fixed( + Imaging imOut, + Imaging imIn, + int x0, + int y0, + int x1, + int y1, + double a[6], + int filterid, + int fill) { + /* affine transform, nearest neighbour resampling, fixed point + arithmetics */ + + ImagingSectionCookie cookie; + int x, y; + int xin, yin; + int xsize, ysize; + int xx, yy; + int a0, a1, a2, a3, a4, a5; + + ImagingCopyPalette(imOut, imIn); + + xsize = (int)imIn->xsize; + ysize = (int)imIn->ysize; + +/* use 16.16 fixed point arithmetics */ +#define FIX(v) FLOOR((v) * 65536.0 + 0.5) + + a0 = FIX(a[0]); + a1 = FIX(a[1]); + a3 = FIX(a[3]); + a4 = FIX(a[4]); + a2 = FIX(a[2] + a[0] * 0.5 + a[1] * 0.5); + a5 = FIX(a[5] + a[3] * 0.5 + a[4] * 0.5); + +#undef FIX + +#define AFFINE_TRANSFORM_FIXED(pixel, image) \ + for (y = y0; y < y1; y++) { \ + pixel *out; \ + xx = a2; \ + yy = a5; \ + out = imOut->image[y]; \ + if (fill && x1 > x0) { \ + memset(out + x0, 0, (x1 - x0) * sizeof(pixel)); \ + } \ + for (x = x0; x < x1; x++, out++) { \ + xin = xx >> 16; \ + if (xin >= 0 && xin < xsize) { \ + yin = yy >> 16; \ + if (yin >= 0 && yin < ysize) { \ + *out = imIn->image[yin][xin]; \ + } \ + } \ + xx += a0; \ + yy += a3; \ + } \ + a2 += a1; \ + a5 += a4; \ + } + + ImagingSectionEnter(&cookie); + + if (imIn->image8) { + AFFINE_TRANSFORM_FIXED(UINT8, image8) + } else { + AFFINE_TRANSFORM_FIXED(INT32, image32) + } + + ImagingSectionLeave(&cookie); + +#undef AFFINE_TRANSFORM_FIXED + + return imOut; +} + +Imaging +ImagingTransformAffine( + Imaging imOut, + Imaging imIn, + int x0, + int y0, + int x1, + int y1, + double a[6], + int filterid, + int fill) { + /* affine transform, nearest neighbour resampling, floating point + arithmetics*/ + + ImagingSectionCookie cookie; + int x, y; + int xin, yin; + int xsize, ysize; + double xx, yy; + double xo, yo; + + if (filterid || imIn->type == IMAGING_TYPE_SPECIAL) { + return ImagingGenericTransform( + imOut, imIn, x0, y0, x1, y1, affine_transform, a, filterid, fill); + } + + if (a[1] == 0 && a[3] == 0) { + /* Scaling */ + return ImagingScaleAffine(imOut, imIn, x0, y0, x1, y1, a, fill); + } + + if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { + return (Imaging)ImagingError_ModeError(); + } + + if (x0 < 0) { + x0 = 0; + } + if (y0 < 0) { + y0 = 0; + } + if (x1 > imOut->xsize) { + x1 = imOut->xsize; + } + if (y1 > imOut->ysize) { + y1 = imOut->ysize; + } + + /* translate all four corners to check if they are within the + range that can be represented by the fixed point arithmetics */ + + if (check_fixed(a, 0, 0) && check_fixed(a, x1 - x0, y1 - y0) && + check_fixed(a, 0, y1 - y0) && check_fixed(a, x1 - x0, 0)) { + return affine_fixed(imOut, imIn, x0, y0, x1, y1, a, filterid, fill); + } + + /* FIXME: cannot really think of any reasonable case when the + following code is used. maybe we should fall back on the slow + generic transform engine in this case? */ + + ImagingCopyPalette(imOut, imIn); + + xsize = (int)imIn->xsize; + ysize = (int)imIn->ysize; + + xo = a[2] + a[1] * 0.5 + a[0] * 0.5; + yo = a[5] + a[4] * 0.5 + a[3] * 0.5; + +#define AFFINE_TRANSFORM(pixel, image) \ + for (y = y0; y < y1; y++) { \ + pixel *out; \ + xx = xo; \ + yy = yo; \ + out = imOut->image[y]; \ + if (fill && x1 > x0) { \ + memset(out + x0, 0, (x1 - x0) * sizeof(pixel)); \ + } \ + for (x = x0; x < x1; x++, out++) { \ + xin = COORD(xx); \ + if (xin >= 0 && xin < xsize) { \ + yin = COORD(yy); \ + if (yin >= 0 && yin < ysize) { \ + *out = imIn->image[yin][xin]; \ + } \ + } \ + xx += a[0]; \ + yy += a[3]; \ + } \ + xo += a[1]; \ + yo += a[4]; \ + } + + ImagingSectionEnter(&cookie); + + if (imIn->image8) { + AFFINE_TRANSFORM(UINT8, image8) + } else { + AFFINE_TRANSFORM(INT32, image32) + } + + ImagingSectionLeave(&cookie); + +#undef AFFINE_TRANSFORM + + return imOut; +} + +Imaging +ImagingTransform( + Imaging imOut, + Imaging imIn, + int method, + int x0, + int y0, + int x1, + int y1, + double a[8], + int filterid, + int fill) { + ImagingTransformMap transform; + + switch (method) { + case IMAGING_TRANSFORM_AFFINE: + return ImagingTransformAffine( + imOut, imIn, x0, y0, x1, y1, a, filterid, fill); + break; + case IMAGING_TRANSFORM_PERSPECTIVE: + transform = perspective_transform; + break; + case IMAGING_TRANSFORM_QUAD: + transform = quad_transform; + break; + default: + return (Imaging)ImagingError_ValueError("bad transform method"); + } + + return ImagingGenericTransform( + imOut, imIn, x0, y0, x1, y1, transform, a, filterid, fill); +} diff --git a/src/libImaging/GetBBox.c b/src/libImaging/GetBBox.c index bd2a2778c..e1989e6d1 100644 --- a/src/libImaging/GetBBox.c +++ b/src/libImaging/GetBBox.c @@ -1,356 +1,356 @@ -/* - * The Python Imaging Library - * $Id$ - * - * helpers to bounding boxes, min/max values, number of colors, etc. - * - * history: - * 1996-07-22 fl Created - * 1996-12-30 fl Added projection stuff - * 1998-07-12 fl Added extrema stuff - * 2004-09-17 fl Added colors stuff - * - * Copyright (c) 1997-2004 by Secret Labs AB. - * Copyright (c) 1996-2004 by Fredrik Lundh. - * - * See the README file for details on usage and redistribution. - */ - -#include "Imaging.h" - -int -ImagingGetBBox(Imaging im, int bbox[4], int alpha_only) { - /* Get the bounding box for any non-zero data in the image.*/ - - int x, y; - int has_data; - - /* Initialize bounding box to max values */ - bbox[0] = im->xsize; - bbox[1] = -1; - bbox[2] = bbox[3] = 0; - -#define GETBBOX(image, mask) \ - for (y = 0; y < im->ysize; y++) { \ - has_data = 0; \ - for (x = 0; x < im->xsize; x++) { \ - if (im->image[y][x] & mask) { \ - has_data = 1; \ - if (x < bbox[0]) { \ - bbox[0] = x; \ - } \ - if (x >= bbox[2]) { \ - bbox[2] = x + 1; \ - } \ - } \ - } \ - if (has_data) { \ - if (bbox[1] < 0) { \ - bbox[1] = y; \ - } \ - bbox[3] = y + 1; \ - } \ - } - - if (im->image8) { - GETBBOX(image8, 0xff); - } else { - INT32 mask = 0xffffffff; - if (im->bands == 3) { - ((UINT8 *)&mask)[3] = 0; - } else if ( - alpha_only && - (strcmp(im->mode, "RGBa") == 0 || strcmp(im->mode, "RGBA") == 0 || - strcmp(im->mode, "La") == 0 || strcmp(im->mode, "LA") == 0 || - strcmp(im->mode, "PA") == 0)) { -#ifdef WORDS_BIGENDIAN - mask = 0x000000ff; -#else - mask = 0xff000000; -#endif - } - GETBBOX(image32, mask); - } - - /* Check that we got a box */ - if (bbox[1] < 0) { - return 0; /* no data */ - } - - return 1; /* ok */ -} - -int -ImagingGetProjection(Imaging im, UINT8 *xproj, UINT8 *yproj) { - /* Get projection arrays for non-zero data in the image.*/ - - int x, y; - int has_data; - - /* Initialize projection arrays */ - memset(xproj, 0, im->xsize); - memset(yproj, 0, im->ysize); - -#define GETPROJ(image, mask) \ - for (y = 0; y < im->ysize; y++) { \ - has_data = 0; \ - for (x = 0; x < im->xsize; x++) { \ - if (im->image[y][x] & mask) { \ - has_data = 1; \ - xproj[x] = 1; \ - } \ - } \ - if (has_data) { \ - yproj[y] = 1; \ - } \ - } - - if (im->image8) { - GETPROJ(image8, 0xff); - } else { - INT32 mask = 0xffffffff; - if (im->bands == 3) { - ((UINT8 *)&mask)[3] = 0; - } - GETPROJ(image32, mask); - } - - return 1; /* ok */ -} - -int -ImagingGetExtrema(Imaging im, void *extrema) { - int x, y; - INT32 imin, imax; - FLOAT32 fmin, fmax; - - if (im->bands != 1) { - (void)ImagingError_ModeError(); - return -1; /* mismatch */ - } - - if (!im->xsize || !im->ysize) { - return 0; /* zero size */ - } - - switch (im->type) { - case IMAGING_TYPE_UINT8: - imin = imax = im->image8[0][0]; - for (y = 0; y < im->ysize; y++) { - UINT8 *in = im->image8[y]; - for (x = 0; x < im->xsize; x++) { - if (imin > in[x]) { - imin = in[x]; - } else if (imax < in[x]) { - imax = in[x]; - } - } - } - ((UINT8 *)extrema)[0] = (UINT8)imin; - ((UINT8 *)extrema)[1] = (UINT8)imax; - break; - case IMAGING_TYPE_INT32: - imin = imax = im->image32[0][0]; - for (y = 0; y < im->ysize; y++) { - INT32 *in = im->image32[y]; - for (x = 0; x < im->xsize; x++) { - if (imin > in[x]) { - imin = in[x]; - } else if (imax < in[x]) { - imax = in[x]; - } - } - } - memcpy(extrema, &imin, sizeof(imin)); - memcpy(((char *)extrema) + sizeof(imin), &imax, sizeof(imax)); - break; - case IMAGING_TYPE_FLOAT32: - fmin = fmax = ((FLOAT32 *)im->image32[0])[0]; - for (y = 0; y < im->ysize; y++) { - FLOAT32 *in = (FLOAT32 *)im->image32[y]; - for (x = 0; x < im->xsize; x++) { - if (fmin > in[x]) { - fmin = in[x]; - } else if (fmax < in[x]) { - fmax = in[x]; - } - } - } - memcpy(extrema, &fmin, sizeof(fmin)); - memcpy(((char *)extrema) + sizeof(fmin), &fmax, sizeof(fmax)); - break; - case IMAGING_TYPE_SPECIAL: - if (strcmp(im->mode, "I;16") == 0) { - UINT16 v; - UINT8 *pixel = *im->image8; -#ifdef WORDS_BIGENDIAN - v = pixel[0] + (pixel[1] << 8); -#else - memcpy(&v, pixel, sizeof(v)); -#endif - imin = imax = v; - for (y = 0; y < im->ysize; y++) { - for (x = 0; x < im->xsize; x++) { - pixel = (UINT8 *)im->image[y] + x * sizeof(v); -#ifdef WORDS_BIGENDIAN - v = pixel[0] + (pixel[1] << 8); -#else - memcpy(&v, pixel, sizeof(v)); -#endif - if (imin > v) { - imin = v; - } else if (imax < v) { - imax = v; - } - } - } - v = (UINT16)imin; - memcpy(extrema, &v, sizeof(v)); - v = (UINT16)imax; - memcpy(((char *)extrema) + sizeof(v), &v, sizeof(v)); - break; - } - /* FALL THROUGH */ - default: - (void)ImagingError_ModeError(); - return -1; - } - return 1; /* ok */ -} - -/* static ImagingColorItem* getcolors8(Imaging im, int maxcolors, int* size);*/ -static ImagingColorItem * -getcolors32(Imaging im, int maxcolors, int *size); - -ImagingColorItem * -ImagingGetColors(Imaging im, int maxcolors, int *size) { - /* FIXME: add support for 8-bit images */ - return getcolors32(im, maxcolors, size); -} - -static ImagingColorItem * -getcolors32(Imaging im, int maxcolors, int *size) { - unsigned int h; - unsigned int i, incr; - int colors; - INT32 pixel_mask; - int x, y; - ImagingColorItem *table; - ImagingColorItem *v; - - unsigned int code_size; - unsigned int code_poly; - unsigned int code_mask; - - /* note: the hash algorithm used here is based on the dictionary - code in Python 2.1.3; the exact implementation is borrowed from - Python's Unicode property database (written by yours truly) /F */ - - static int SIZES[] = { - 4, 3, 8, 3, 16, 3, 32, 5, 64, 3, - 128, 3, 256, 29, 512, 17, 1024, 9, 2048, 5, - 4096, 83, 8192, 27, 16384, 43, 32768, 3, 65536, 45, - 131072, 9, 262144, 39, 524288, 39, 1048576, 9, 2097152, 5, - 4194304, 3, 8388608, 33, 16777216, 27, 33554432, 9, 67108864, 71, - 134217728, 39, 268435456, 9, 536870912, 5, 1073741824, 83, 0}; - - code_size = code_poly = code_mask = 0; - - for (i = 0; SIZES[i]; i += 2) { - if (SIZES[i] > maxcolors) { - code_size = SIZES[i]; - code_poly = SIZES[i + 1]; - code_mask = code_size - 1; - break; - } - } - - /* printf("code_size=%d\n", code_size); */ - /* printf("code_poly=%d\n", code_poly); */ - - if (!code_size) { - return ImagingError_MemoryError(); /* just give up */ - } - - if (!im->image32) { - return ImagingError_ModeError(); - } - - table = calloc(code_size + 1, sizeof(ImagingColorItem)); - if (!table) { - return ImagingError_MemoryError(); - } - - pixel_mask = 0xffffffff; - if (im->bands == 3) { - ((UINT8 *)&pixel_mask)[3] = 0; - } - - colors = 0; - - for (y = 0; y < im->ysize; y++) { - INT32 *p = im->image32[y]; - for (x = 0; x < im->xsize; x++) { - INT32 pixel = p[x] & pixel_mask; - h = (pixel); /* null hashing */ - i = (~h) & code_mask; - v = &table[i]; - if (!v->count) { - /* add to table */ - if (colors++ == maxcolors) { - goto overflow; - } - v->x = x; - v->y = y; - v->pixel = pixel; - v->count = 1; - continue; - } else if (v->pixel == pixel) { - v->count++; - continue; - } - incr = (h ^ (h >> 3)) & code_mask; - if (!incr) { - incr = code_mask; - } - for (;;) { - i = (i + incr) & code_mask; - v = &table[i]; - if (!v->count) { - /* add to table */ - if (colors++ == maxcolors) { - goto overflow; - } - v->x = x; - v->y = y; - v->pixel = pixel; - v->count = 1; - break; - } else if (v->pixel == pixel) { - v->count++; - break; - } - incr = incr << 1; - if (incr > code_mask) { - incr = incr ^ code_poly; - } - } - } - } - -overflow: - - /* pack the table */ - for (x = y = 0; x < (int)code_size; x++) - if (table[x].count) { - if (x != y) { - table[y] = table[x]; - } - y++; - } - table[y].count = 0; /* mark end of table */ - - *size = colors; - - return table; -} +/* + * The Python Imaging Library + * $Id$ + * + * helpers to bounding boxes, min/max values, number of colors, etc. + * + * history: + * 1996-07-22 fl Created + * 1996-12-30 fl Added projection stuff + * 1998-07-12 fl Added extrema stuff + * 2004-09-17 fl Added colors stuff + * + * Copyright (c) 1997-2004 by Secret Labs AB. + * Copyright (c) 1996-2004 by Fredrik Lundh. + * + * See the README file for details on usage and redistribution. + */ + +#include "Imaging.h" + +int +ImagingGetBBox(Imaging im, int bbox[4], int alpha_only) { + /* Get the bounding box for any non-zero data in the image.*/ + + int x, y; + int has_data; + + /* Initialize bounding box to max values */ + bbox[0] = im->xsize; + bbox[1] = -1; + bbox[2] = bbox[3] = 0; + +#define GETBBOX(image, mask) \ + for (y = 0; y < im->ysize; y++) { \ + has_data = 0; \ + for (x = 0; x < im->xsize; x++) { \ + if (im->image[y][x] & mask) { \ + has_data = 1; \ + if (x < bbox[0]) { \ + bbox[0] = x; \ + } \ + if (x >= bbox[2]) { \ + bbox[2] = x + 1; \ + } \ + } \ + } \ + if (has_data) { \ + if (bbox[1] < 0) { \ + bbox[1] = y; \ + } \ + bbox[3] = y + 1; \ + } \ + } + + if (im->image8) { + GETBBOX(image8, 0xff); + } else { + INT32 mask = 0xffffffff; + if (im->bands == 3) { + ((UINT8 *)&mask)[3] = 0; + } else if ( + alpha_only && + (strcmp(im->mode, "RGBa") == 0 || strcmp(im->mode, "RGBA") == 0 || + strcmp(im->mode, "La") == 0 || strcmp(im->mode, "LA") == 0 || + strcmp(im->mode, "PA") == 0)) { +#ifdef WORDS_BIGENDIAN + mask = 0x000000ff; +#else + mask = 0xff000000; +#endif + } + GETBBOX(image32, mask); + } + + /* Check that we got a box */ + if (bbox[1] < 0) { + return 0; /* no data */ + } + + return 1; /* ok */ +} + +int +ImagingGetProjection(Imaging im, UINT8 *xproj, UINT8 *yproj) { + /* Get projection arrays for non-zero data in the image.*/ + + int x, y; + int has_data; + + /* Initialize projection arrays */ + memset(xproj, 0, im->xsize); + memset(yproj, 0, im->ysize); + +#define GETPROJ(image, mask) \ + for (y = 0; y < im->ysize; y++) { \ + has_data = 0; \ + for (x = 0; x < im->xsize; x++) { \ + if (im->image[y][x] & mask) { \ + has_data = 1; \ + xproj[x] = 1; \ + } \ + } \ + if (has_data) { \ + yproj[y] = 1; \ + } \ + } + + if (im->image8) { + GETPROJ(image8, 0xff); + } else { + INT32 mask = 0xffffffff; + if (im->bands == 3) { + ((UINT8 *)&mask)[3] = 0; + } + GETPROJ(image32, mask); + } + + return 1; /* ok */ +} + +int +ImagingGetExtrema(Imaging im, void *extrema) { + int x, y; + INT32 imin, imax; + FLOAT32 fmin, fmax; + + if (im->bands != 1) { + (void)ImagingError_ModeError(); + return -1; /* mismatch */ + } + + if (!im->xsize || !im->ysize) { + return 0; /* zero size */ + } + + switch (im->type) { + case IMAGING_TYPE_UINT8: + imin = imax = im->image8[0][0]; + for (y = 0; y < im->ysize; y++) { + UINT8 *in = im->image8[y]; + for (x = 0; x < im->xsize; x++) { + if (imin > in[x]) { + imin = in[x]; + } else if (imax < in[x]) { + imax = in[x]; + } + } + } + ((UINT8 *)extrema)[0] = (UINT8)imin; + ((UINT8 *)extrema)[1] = (UINT8)imax; + break; + case IMAGING_TYPE_INT32: + imin = imax = im->image32[0][0]; + for (y = 0; y < im->ysize; y++) { + INT32 *in = im->image32[y]; + for (x = 0; x < im->xsize; x++) { + if (imin > in[x]) { + imin = in[x]; + } else if (imax < in[x]) { + imax = in[x]; + } + } + } + memcpy(extrema, &imin, sizeof(imin)); + memcpy(((char *)extrema) + sizeof(imin), &imax, sizeof(imax)); + break; + case IMAGING_TYPE_FLOAT32: + fmin = fmax = ((FLOAT32 *)im->image32[0])[0]; + for (y = 0; y < im->ysize; y++) { + FLOAT32 *in = (FLOAT32 *)im->image32[y]; + for (x = 0; x < im->xsize; x++) { + if (fmin > in[x]) { + fmin = in[x]; + } else if (fmax < in[x]) { + fmax = in[x]; + } + } + } + memcpy(extrema, &fmin, sizeof(fmin)); + memcpy(((char *)extrema) + sizeof(fmin), &fmax, sizeof(fmax)); + break; + case IMAGING_TYPE_SPECIAL: + if (strcmp(im->mode, "I;16") == 0) { + UINT16 v; + UINT8 *pixel = *im->image8; +#ifdef WORDS_BIGENDIAN + v = pixel[0] + (pixel[1] << 8); +#else + memcpy(&v, pixel, sizeof(v)); +#endif + imin = imax = v; + for (y = 0; y < im->ysize; y++) { + for (x = 0; x < im->xsize; x++) { + pixel = (UINT8 *)im->image[y] + x * sizeof(v); +#ifdef WORDS_BIGENDIAN + v = pixel[0] + (pixel[1] << 8); +#else + memcpy(&v, pixel, sizeof(v)); +#endif + if (imin > v) { + imin = v; + } else if (imax < v) { + imax = v; + } + } + } + v = (UINT16)imin; + memcpy(extrema, &v, sizeof(v)); + v = (UINT16)imax; + memcpy(((char *)extrema) + sizeof(v), &v, sizeof(v)); + break; + } + /* FALL THROUGH */ + default: + (void)ImagingError_ModeError(); + return -1; + } + return 1; /* ok */ +} + +/* static ImagingColorItem* getcolors8(Imaging im, int maxcolors, int* size);*/ +static ImagingColorItem * +getcolors32(Imaging im, int maxcolors, int *size); + +ImagingColorItem * +ImagingGetColors(Imaging im, int maxcolors, int *size) { + /* FIXME: add support for 8-bit images */ + return getcolors32(im, maxcolors, size); +} + +static ImagingColorItem * +getcolors32(Imaging im, int maxcolors, int *size) { + unsigned int h; + unsigned int i, incr; + int colors; + INT32 pixel_mask; + int x, y; + ImagingColorItem *table; + ImagingColorItem *v; + + unsigned int code_size; + unsigned int code_poly; + unsigned int code_mask; + + /* note: the hash algorithm used here is based on the dictionary + code in Python 2.1.3; the exact implementation is borrowed from + Python's Unicode property database (written by yours truly) /F */ + + static int SIZES[] = { + 4, 3, 8, 3, 16, 3, 32, 5, 64, 3, + 128, 3, 256, 29, 512, 17, 1024, 9, 2048, 5, + 4096, 83, 8192, 27, 16384, 43, 32768, 3, 65536, 45, + 131072, 9, 262144, 39, 524288, 39, 1048576, 9, 2097152, 5, + 4194304, 3, 8388608, 33, 16777216, 27, 33554432, 9, 67108864, 71, + 134217728, 39, 268435456, 9, 536870912, 5, 1073741824, 83, 0}; + + code_size = code_poly = code_mask = 0; + + for (i = 0; SIZES[i]; i += 2) { + if (SIZES[i] > maxcolors) { + code_size = SIZES[i]; + code_poly = SIZES[i + 1]; + code_mask = code_size - 1; + break; + } + } + + /* printf("code_size=%d\n", code_size); */ + /* printf("code_poly=%d\n", code_poly); */ + + if (!code_size) { + return ImagingError_MemoryError(); /* just give up */ + } + + if (!im->image32) { + return ImagingError_ModeError(); + } + + table = calloc(code_size + 1, sizeof(ImagingColorItem)); + if (!table) { + return ImagingError_MemoryError(); + } + + pixel_mask = 0xffffffff; + if (im->bands == 3) { + ((UINT8 *)&pixel_mask)[3] = 0; + } + + colors = 0; + + for (y = 0; y < im->ysize; y++) { + INT32 *p = im->image32[y]; + for (x = 0; x < im->xsize; x++) { + INT32 pixel = p[x] & pixel_mask; + h = (pixel); /* null hashing */ + i = (~h) & code_mask; + v = &table[i]; + if (!v->count) { + /* add to table */ + if (colors++ == maxcolors) { + goto overflow; + } + v->x = x; + v->y = y; + v->pixel = pixel; + v->count = 1; + continue; + } else if (v->pixel == pixel) { + v->count++; + continue; + } + incr = (h ^ (h >> 3)) & code_mask; + if (!incr) { + incr = code_mask; + } + for (;;) { + i = (i + incr) & code_mask; + v = &table[i]; + if (!v->count) { + /* add to table */ + if (colors++ == maxcolors) { + goto overflow; + } + v->x = x; + v->y = y; + v->pixel = pixel; + v->count = 1; + break; + } else if (v->pixel == pixel) { + v->count++; + break; + } + incr = incr << 1; + if (incr > code_mask) { + incr = incr ^ code_poly; + } + } + } + } + +overflow: + + /* pack the table */ + for (x = y = 0; x < (int)code_size; x++) + if (table[x].count) { + if (x != y) { + table[y] = table[x]; + } + y++; + } + table[y].count = 0; /* mark end of table */ + + *size = colors; + + return table; +} diff --git a/src/libImaging/Gif.h b/src/libImaging/Gif.h index 8edfbc2ed..d3c4e12c0 100644 --- a/src/libImaging/Gif.h +++ b/src/libImaging/Gif.h @@ -1,100 +1,100 @@ -/* - * The Python Imaging Library. - * $Id$ - * - * Declarations for a fast, suspendable GIF decoder. - * - * Copyright (c) Fredrik Lundh 1995-96. - */ - -/* Max size for a LZW code word. */ - -#define GIFBITS 12 - -#define GIFTABLE (1 << GIFBITS) -#define GIFBUFFER (1 << GIFBITS) - -typedef struct { - /* CONFIGURATION */ - - /* Initial number of bits. The caller should clear all fields in - this structure and set this field before calling the decoder - the first time. */ - int bits; - - /* If set, this is an interlaced image. Process it the following way: - * 1st pass: start at top line, lines are 8 pixels high, step 8 pixels - * 2nd pass: start at line 4, lines are 4 pixels high, step 8 pixels - * 3rd pass: start at line 2, lines are 2 pixels high, step 4 pixels - * 4th pass: start at line 1, lines are 1 pixels high, step 2 pixels - */ - int interlace; - - /* The transparent palette index, or -1 for no transparency */ - int transparency; - - /* PRIVATE CONTEXT (set by decoder) */ - - /* Interlace parameters */ - int step, repeat; - - /* Input bit buffer */ - INT32 bitbuffer; - int bitcount; - int blocksize; - - /* Code buffer */ - int codesize; - int codemask; - - /* Constant symbol codes */ - int clear, end; - - /* Symbol history */ - int lastcode; - unsigned char lastdata; - - /* History buffer */ - int bufferindex; - unsigned char buffer[GIFTABLE]; - - /* Symbol table */ - UINT16 link[GIFTABLE]; - unsigned char data[GIFTABLE]; - int next; - -} GIFDECODERSTATE; - -/* For GIF LZW encoder. */ -#define TABLE_SIZE 8192 - -typedef struct { - /* CONFIGURATION */ - - /* Initial number of bits. The caller should clear all fields in - this structure and set this field before calling the encoder - the first time. */ - int bits; - - /* NOTE: the expanding encoder ignores this field */ - - /* If set, write an interlaced image (see above) */ - int interlace; - - /* PRIVATE CONTEXT (set by encoder) */ - - /* Interlace parameters */ - int step; - - /* For GIF LZW encoder. */ - UINT32 put_state; - UINT32 entry_state; - UINT32 clear_code, end_code, next_code, max_code; - UINT32 code_width, code_bits_left, buf_bits_left; - UINT32 code_buffer; - UINT32 head, tail; - int probe; - UINT32 code; - UINT32 codes[TABLE_SIZE]; - -} GIFENCODERSTATE; +/* + * The Python Imaging Library. + * $Id$ + * + * Declarations for a fast, suspendable GIF decoder. + * + * Copyright (c) Fredrik Lundh 1995-96. + */ + +/* Max size for a LZW code word. */ + +#define GIFBITS 12 + +#define GIFTABLE (1 << GIFBITS) +#define GIFBUFFER (1 << GIFBITS) + +typedef struct { + /* CONFIGURATION */ + + /* Initial number of bits. The caller should clear all fields in + this structure and set this field before calling the decoder + the first time. */ + int bits; + + /* If set, this is an interlaced image. Process it the following way: + * 1st pass: start at top line, lines are 8 pixels high, step 8 pixels + * 2nd pass: start at line 4, lines are 4 pixels high, step 8 pixels + * 3rd pass: start at line 2, lines are 2 pixels high, step 4 pixels + * 4th pass: start at line 1, lines are 1 pixels high, step 2 pixels + */ + int interlace; + + /* The transparent palette index, or -1 for no transparency */ + int transparency; + + /* PRIVATE CONTEXT (set by decoder) */ + + /* Interlace parameters */ + int step, repeat; + + /* Input bit buffer */ + INT32 bitbuffer; + int bitcount; + int blocksize; + + /* Code buffer */ + int codesize; + int codemask; + + /* Constant symbol codes */ + int clear, end; + + /* Symbol history */ + int lastcode; + unsigned char lastdata; + + /* History buffer */ + int bufferindex; + unsigned char buffer[GIFTABLE]; + + /* Symbol table */ + UINT16 link[GIFTABLE]; + unsigned char data[GIFTABLE]; + int next; + +} GIFDECODERSTATE; + +/* For GIF LZW encoder. */ +#define TABLE_SIZE 8192 + +typedef struct { + /* CONFIGURATION */ + + /* Initial number of bits. The caller should clear all fields in + this structure and set this field before calling the encoder + the first time. */ + int bits; + + /* NOTE: the expanding encoder ignores this field */ + + /* If set, write an interlaced image (see above) */ + int interlace; + + /* PRIVATE CONTEXT (set by encoder) */ + + /* Interlace parameters */ + int step; + + /* For GIF LZW encoder. */ + UINT32 put_state; + UINT32 entry_state; + UINT32 clear_code, end_code, next_code, max_code; + UINT32 code_width, code_bits_left, buf_bits_left; + UINT32 code_buffer; + UINT32 head, tail; + int probe; + UINT32 code; + UINT32 codes[TABLE_SIZE]; + +} GIFENCODERSTATE; diff --git a/src/libImaging/GifDecode.c b/src/libImaging/GifDecode.c index 92b2607b4..a26f88dd5 100644 --- a/src/libImaging/GifDecode.c +++ b/src/libImaging/GifDecode.c @@ -1,285 +1,285 @@ -/* - * The Python Imaging Library. - * $Id$ - * - * a fast, suspendable GIF decoder - * - * history: - * 95-09-03 fl Created - * 95-09-05 fl Fixed sign problem on 16-bit platforms - * 95-09-13 fl Added some storage shortcuts - * 96-03-28 fl Revised API, integrated with PIL - * 96-12-10 fl Added interlace support - * 96-12-16 fl Fixed premature termination bug introduced by last fix - * 97-01-05 fl Don't mess up on bogus configuration - * 97-01-17 fl Don't mess up on very small, interlaced files - * 99-02-07 fl Minor speedups - * - * Copyright (c) Secret Labs AB 1997-99. - * Copyright (c) Fredrik Lundh 1995-97. - * - * See the README file for information on usage and redistribution. - */ - -#include "Imaging.h" - -#include -#include /* memcpy() */ - -#include "Gif.h" - -#define NEWLINE(state, context) \ - { \ - state->x = 0; \ - state->y += context->step; \ - while (state->y >= state->ysize) switch (context->interlace) { \ - case 1: \ - context->repeat = state->y = 4; \ - context->interlace = 2; \ - break; \ - case 2: \ - context->step = 4; \ - context->repeat = state->y = 2; \ - context->interlace = 3; \ - break; \ - case 3: \ - context->step = 2; \ - context->repeat = state->y = 1; \ - context->interlace = 0; \ - break; \ - default: \ - return -1; \ - } \ - if (state->y < state->ysize) { \ - out = im->image8[state->y + state->yoff] + state->xoff; \ - } \ - } - -int -ImagingGifDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes) { - UINT8 *p; - UINT8 *out; - int c, i; - int thiscode; - GIFDECODERSTATE *context = (GIFDECODERSTATE *)state->context; - - UINT8 *ptr = buffer; - - if (!state->state) { - /* Initialise state */ - if (context->bits < 0 || context->bits > 12) { - state->errcode = IMAGING_CODEC_CONFIG; - return -1; - } - - /* Clear code */ - context->clear = 1 << context->bits; - - /* End code */ - context->end = context->clear + 1; - - /* Interlace */ - if (context->interlace) { - context->interlace = 1; - context->step = context->repeat = 8; - } else { - context->step = 1; - } - - state->state = 1; - } - - out = im->image8[state->y + state->yoff] + state->xoff + state->x; - - for (;;) { - if (state->state == 1) { - /* First free entry in table */ - context->next = context->clear + 2; - - /* Initial code size */ - context->codesize = context->bits + 1; - context->codemask = (1 << context->codesize) - 1; - - /* Buffer pointer. We fill the buffer from right, which - allows us to return all of it in one operation. */ - context->bufferindex = GIFBUFFER; - - state->state = 2; - } - - if (context->bufferindex < GIFBUFFER) { - /* Return whole buffer in one chunk */ - i = GIFBUFFER - context->bufferindex; - p = &context->buffer[context->bufferindex]; - - context->bufferindex = GIFBUFFER; - - } else { - /* Get current symbol */ - - while (context->bitcount < context->codesize) { - if (context->blocksize > 0) { - /* Read next byte */ - c = *ptr++; - bytes--; - - context->blocksize--; - - /* New bits are shifted in from the left. */ - context->bitbuffer |= (INT32)c << context->bitcount; - context->bitcount += 8; - - } else { - /* New GIF block */ - - /* We don't start decoding unless we have a full block */ - if (bytes < 1) { - return ptr - buffer; - } - c = *ptr; - if (bytes < c + 1) { - return ptr - buffer; - } - - context->blocksize = c; - - ptr++; - bytes--; - } - } - - /* Extract current symbol from bit buffer. */ - c = (int)context->bitbuffer & context->codemask; - - /* Adjust buffer */ - context->bitbuffer >>= context->codesize; - context->bitcount -= context->codesize; - - /* If c is less than "clear", it's a data byte. Otherwise, - it's either clear/end or a code symbol which should be - expanded. */ - - if (c == context->clear) { - if (state->state != 2) { - state->state = 1; - } - continue; - } - - if (c == context->end) { - break; - } - - i = 1; - p = &context->lastdata; - - if (state->state == 2) { - /* First valid symbol after clear; use as is */ - if (c > context->clear) { - state->errcode = IMAGING_CODEC_BROKEN; - return -1; - } - - context->lastdata = context->lastcode = c; - state->state = 3; - - } else { - thiscode = c; - - if (c > context->next) { - state->errcode = IMAGING_CODEC_BROKEN; - return -1; - } - - if (c == context->next) { - /* c == next is allowed. not sure why. */ - - if (context->bufferindex <= 0) { - state->errcode = IMAGING_CODEC_BROKEN; - return -1; - } - - context->buffer[--context->bufferindex] = context->lastdata; - - c = context->lastcode; - } - - while (c >= context->clear) { - /* Copy data string to buffer (beginning from right) */ - - if (context->bufferindex <= 0 || c >= GIFTABLE) { - state->errcode = IMAGING_CODEC_BROKEN; - return -1; - } - - context->buffer[--context->bufferindex] = context->data[c]; - - c = context->link[c]; - } - - context->lastdata = c; - - if (context->next < GIFTABLE) { - /* We'll only add this symbol if we have room - for it (take the advice, Netscape!) */ - context->data[context->next] = c; - context->link[context->next] = context->lastcode; - - if (context->next == context->codemask && - context->codesize < GIFBITS) { - /* Expand code size */ - context->codesize++; - context->codemask = (1 << context->codesize) - 1; - } - - context->next++; - } - - context->lastcode = thiscode; - } - } - - /* Copy the bytes into the image */ - if (state->y >= state->ysize) { - state->errcode = IMAGING_CODEC_OVERRUN; - return -1; - } - - /* To squeeze some extra pixels out of this loop, we test for - some common cases and handle them separately. */ - - /* This cannot be used if there is transparency */ - if (context->transparency == -1) { - if (i == 1) { - if (state->x < state->xsize - 1) { - /* Single pixel, not at the end of the line. */ - *out++ = p[0]; - state->x++; - continue; - } - } else if (state->x + i <= state->xsize) { - /* This string fits into current line. */ - memcpy(out, p, i); - out += i; - state->x += i; - if (state->x == state->xsize) { - NEWLINE(state, context); - } - continue; - } - } - - /* No shortcut, copy pixel by pixel */ - for (c = 0; c < i; c++) { - if (p[c] != context->transparency) { - *out = p[c]; - } - out++; - if (++state->x >= state->xsize) { - NEWLINE(state, context); - } - } - } - - return ptr - buffer; -} +/* + * The Python Imaging Library. + * $Id$ + * + * a fast, suspendable GIF decoder + * + * history: + * 95-09-03 fl Created + * 95-09-05 fl Fixed sign problem on 16-bit platforms + * 95-09-13 fl Added some storage shortcuts + * 96-03-28 fl Revised API, integrated with PIL + * 96-12-10 fl Added interlace support + * 96-12-16 fl Fixed premature termination bug introduced by last fix + * 97-01-05 fl Don't mess up on bogus configuration + * 97-01-17 fl Don't mess up on very small, interlaced files + * 99-02-07 fl Minor speedups + * + * Copyright (c) Secret Labs AB 1997-99. + * Copyright (c) Fredrik Lundh 1995-97. + * + * See the README file for information on usage and redistribution. + */ + +#include "Imaging.h" + +#include +#include /* memcpy() */ + +#include "Gif.h" + +#define NEWLINE(state, context) \ + { \ + state->x = 0; \ + state->y += context->step; \ + while (state->y >= state->ysize) switch (context->interlace) { \ + case 1: \ + context->repeat = state->y = 4; \ + context->interlace = 2; \ + break; \ + case 2: \ + context->step = 4; \ + context->repeat = state->y = 2; \ + context->interlace = 3; \ + break; \ + case 3: \ + context->step = 2; \ + context->repeat = state->y = 1; \ + context->interlace = 0; \ + break; \ + default: \ + return -1; \ + } \ + if (state->y < state->ysize) { \ + out = im->image8[state->y + state->yoff] + state->xoff; \ + } \ + } + +int +ImagingGifDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes) { + UINT8 *p; + UINT8 *out; + int c, i; + int thiscode; + GIFDECODERSTATE *context = (GIFDECODERSTATE *)state->context; + + UINT8 *ptr = buffer; + + if (!state->state) { + /* Initialise state */ + if (context->bits < 0 || context->bits > 12) { + state->errcode = IMAGING_CODEC_CONFIG; + return -1; + } + + /* Clear code */ + context->clear = 1 << context->bits; + + /* End code */ + context->end = context->clear + 1; + + /* Interlace */ + if (context->interlace) { + context->interlace = 1; + context->step = context->repeat = 8; + } else { + context->step = 1; + } + + state->state = 1; + } + + out = im->image8[state->y + state->yoff] + state->xoff + state->x; + + for (;;) { + if (state->state == 1) { + /* First free entry in table */ + context->next = context->clear + 2; + + /* Initial code size */ + context->codesize = context->bits + 1; + context->codemask = (1 << context->codesize) - 1; + + /* Buffer pointer. We fill the buffer from right, which + allows us to return all of it in one operation. */ + context->bufferindex = GIFBUFFER; + + state->state = 2; + } + + if (context->bufferindex < GIFBUFFER) { + /* Return whole buffer in one chunk */ + i = GIFBUFFER - context->bufferindex; + p = &context->buffer[context->bufferindex]; + + context->bufferindex = GIFBUFFER; + + } else { + /* Get current symbol */ + + while (context->bitcount < context->codesize) { + if (context->blocksize > 0) { + /* Read next byte */ + c = *ptr++; + bytes--; + + context->blocksize--; + + /* New bits are shifted in from the left. */ + context->bitbuffer |= (INT32)c << context->bitcount; + context->bitcount += 8; + + } else { + /* New GIF block */ + + /* We don't start decoding unless we have a full block */ + if (bytes < 1) { + return ptr - buffer; + } + c = *ptr; + if (bytes < c + 1) { + return ptr - buffer; + } + + context->blocksize = c; + + ptr++; + bytes--; + } + } + + /* Extract current symbol from bit buffer. */ + c = (int)context->bitbuffer & context->codemask; + + /* Adjust buffer */ + context->bitbuffer >>= context->codesize; + context->bitcount -= context->codesize; + + /* If c is less than "clear", it's a data byte. Otherwise, + it's either clear/end or a code symbol which should be + expanded. */ + + if (c == context->clear) { + if (state->state != 2) { + state->state = 1; + } + continue; + } + + if (c == context->end) { + break; + } + + i = 1; + p = &context->lastdata; + + if (state->state == 2) { + /* First valid symbol after clear; use as is */ + if (c > context->clear) { + state->errcode = IMAGING_CODEC_BROKEN; + return -1; + } + + context->lastdata = context->lastcode = c; + state->state = 3; + + } else { + thiscode = c; + + if (c > context->next) { + state->errcode = IMAGING_CODEC_BROKEN; + return -1; + } + + if (c == context->next) { + /* c == next is allowed. not sure why. */ + + if (context->bufferindex <= 0) { + state->errcode = IMAGING_CODEC_BROKEN; + return -1; + } + + context->buffer[--context->bufferindex] = context->lastdata; + + c = context->lastcode; + } + + while (c >= context->clear) { + /* Copy data string to buffer (beginning from right) */ + + if (context->bufferindex <= 0 || c >= GIFTABLE) { + state->errcode = IMAGING_CODEC_BROKEN; + return -1; + } + + context->buffer[--context->bufferindex] = context->data[c]; + + c = context->link[c]; + } + + context->lastdata = c; + + if (context->next < GIFTABLE) { + /* We'll only add this symbol if we have room + for it (take the advice, Netscape!) */ + context->data[context->next] = c; + context->link[context->next] = context->lastcode; + + if (context->next == context->codemask && + context->codesize < GIFBITS) { + /* Expand code size */ + context->codesize++; + context->codemask = (1 << context->codesize) - 1; + } + + context->next++; + } + + context->lastcode = thiscode; + } + } + + /* Copy the bytes into the image */ + if (state->y >= state->ysize) { + state->errcode = IMAGING_CODEC_OVERRUN; + return -1; + } + + /* To squeeze some extra pixels out of this loop, we test for + some common cases and handle them separately. */ + + /* This cannot be used if there is transparency */ + if (context->transparency == -1) { + if (i == 1) { + if (state->x < state->xsize - 1) { + /* Single pixel, not at the end of the line. */ + *out++ = p[0]; + state->x++; + continue; + } + } else if (state->x + i <= state->xsize) { + /* This string fits into current line. */ + memcpy(out, p, i); + out += i; + state->x += i; + if (state->x == state->xsize) { + NEWLINE(state, context); + } + continue; + } + } + + /* No shortcut, copy pixel by pixel */ + for (c = 0; c < i; c++) { + if (p[c] != context->transparency) { + *out = p[c]; + } + out++; + if (++state->x >= state->xsize) { + NEWLINE(state, context); + } + } + } + + return ptr - buffer; +} diff --git a/src/libImaging/GifEncode.c b/src/libImaging/GifEncode.c index 45b67616d..56d625fda 100644 --- a/src/libImaging/GifEncode.c +++ b/src/libImaging/GifEncode.c @@ -1,375 +1,375 @@ -/* - * The Python Imaging Library. - * $Id$ - * - * encoder for uncompressed GIF data - * - * history: - * 97-01-05 fl created (writes uncompressed data) - * 97-08-27 fl fixed off-by-one error in buffer size test - * 98-07-09 fl added interlace write support - * 99-02-07 fl rewritten, now uses a run-length encoding strategy - * 99-02-08 fl improved run-length encoding for long runs - * 2020-12-12 rdg Reworked for LZW compression. - * - * Copyright (c) Secret Labs AB 1997-99. - * Copyright (c) Fredrik Lundh 1997. - * - * See the README file for information on usage and redistribution. - */ - -#include "Imaging.h" - -#include "Gif.h" - -enum { INIT, ENCODE, FINISH }; - -/* GIF LZW encoder by Raymond Gardner. */ -/* Released here under PIL license. */ - -/* This LZW encoder conforms to the GIF LZW format specified in the original - * Compuserve GIF 87a and GIF 89a specifications (see e.g. - * https://www.w3.org/Graphics/GIF/spec-gif87.txt Appendix C and - * https://www.w3.org/Graphics/GIF/spec-gif89a.txt Appendix F). - */ - -/* Return values */ -#define GLZW_OK 0 -#define GLZW_NO_INPUT_AVAIL 1 -#define GLZW_NO_OUTPUT_AVAIL 2 -#define GLZW_INTERNAL_ERROR 3 - -#define CODE_LIMIT 4096 - -/* Values of entry_state */ -enum { - LZW_INITIAL, - LZW_TRY_IN1, - LZW_TRY_IN2, - LZW_TRY_OUT1, - LZW_TRY_OUT2, - LZW_FINISHED -}; - -/* Values of control_state */ -enum { PUT_HEAD, PUT_INIT_CLEAR, PUT_CLEAR, PUT_LAST_HEAD, PUT_END }; - -static void -glzwe_reset(GIFENCODERSTATE *st) { - st->next_code = st->end_code + 1; - st->max_code = 2 * st->clear_code - 1; - st->code_width = st->bits + 1; - memset(st->codes, 0, sizeof(st->codes)); -} - -static void -glzwe_init(GIFENCODERSTATE *st) { - st->clear_code = 1 << st->bits; - st->end_code = st->clear_code + 1; - glzwe_reset(st); - st->entry_state = LZW_INITIAL; - st->buf_bits_left = 8; - st->code_buffer = 0; -} - -static int -glzwe( - GIFENCODERSTATE *st, - const UINT8 *in_ptr, - UINT8 *out_ptr, - UINT32 *in_avail, - UINT32 *out_avail, - UINT32 end_of_data) { - switch (st->entry_state) { - case LZW_TRY_IN1: -get_first_byte: - if (!*in_avail) { - if (end_of_data) { - goto end_of_data; - } - st->entry_state = LZW_TRY_IN1; - return GLZW_NO_INPUT_AVAIL; - } - st->head = *in_ptr++; - (*in_avail)--; - - case LZW_TRY_IN2: -encode_loop: - if (!*in_avail) { - if (end_of_data) { - st->code = st->head; - st->put_state = PUT_LAST_HEAD; - goto put_code; - } - st->entry_state = LZW_TRY_IN2; - return GLZW_NO_INPUT_AVAIL; - } - st->tail = *in_ptr++; - (*in_avail)--; - - /* Knuth TAOCP vol 3 sec. 6.4 algorithm D. */ - /* Hash found experimentally to be pretty good. */ - /* This works ONLY with TABLE_SIZE a power of 2. */ - st->probe = ((st->head ^ (st->tail << 6)) * 31) & (TABLE_SIZE - 1); - while (st->codes[st->probe]) { - if ((st->codes[st->probe] & 0xFFFFF) == ((st->head << 8) | st->tail)) { - st->head = st->codes[st->probe] >> 20; - goto encode_loop; - } else { - // Reprobe decrement must be non-zero and relatively prime to table - // size. So, any odd positive number for power-of-2 size. - if ((st->probe -= ((st->tail << 2) | 1)) < 0) { - st->probe += TABLE_SIZE; - } - } - } - /* Key not found, probe is at empty slot. */ - st->code = st->head; - st->put_state = PUT_HEAD; - goto put_code; -insert_code_or_clear: /* jump here after put_code */ - if (st->next_code < CODE_LIMIT) { - st->codes[st->probe] = - (st->next_code << 20) | (st->head << 8) | st->tail; - if (st->next_code > st->max_code) { - st->max_code = st->max_code * 2 + 1; - st->code_width++; - } - st->next_code++; - } else { - st->code = st->clear_code; - st->put_state = PUT_CLEAR; - goto put_code; -reset_after_clear: /* jump here after put_code */ - glzwe_reset(st); - } - st->head = st->tail; - goto encode_loop; - - case LZW_INITIAL: - glzwe_reset(st); - st->code = st->clear_code; - st->put_state = PUT_INIT_CLEAR; -put_code: - st->code_bits_left = st->code_width; -check_buf_bits: - if (!st->buf_bits_left) { /* out buffer full */ - - case LZW_TRY_OUT1: - if (!*out_avail) { - st->entry_state = LZW_TRY_OUT1; - return GLZW_NO_OUTPUT_AVAIL; - } - *out_ptr++ = st->code_buffer; - (*out_avail)--; - st->code_buffer = 0; - st->buf_bits_left = 8; - } - /* code bits to pack */ - UINT32 n = st->buf_bits_left < st->code_bits_left ? st->buf_bits_left - : st->code_bits_left; - st->code_buffer |= (st->code & ((1 << n) - 1)) << (8 - st->buf_bits_left); - st->code >>= n; - st->buf_bits_left -= n; - st->code_bits_left -= n; - if (st->code_bits_left) { - goto check_buf_bits; - } - switch (st->put_state) { - case PUT_INIT_CLEAR: - goto get_first_byte; - case PUT_HEAD: - goto insert_code_or_clear; - case PUT_CLEAR: - goto reset_after_clear; - case PUT_LAST_HEAD: - goto end_of_data; - case PUT_END: - goto flush_code_buffer; - default: - return GLZW_INTERNAL_ERROR; - } - -end_of_data: - st->code = st->end_code; - st->put_state = PUT_END; - goto put_code; -flush_code_buffer: /* jump here after put_code */ - if (st->buf_bits_left < 8) { - case LZW_TRY_OUT2: - if (!*out_avail) { - st->entry_state = LZW_TRY_OUT2; - return GLZW_NO_OUTPUT_AVAIL; - } - *out_ptr++ = st->code_buffer; - (*out_avail)--; - } - st->entry_state = LZW_FINISHED; - return GLZW_OK; - - case LZW_FINISHED: - return GLZW_OK; - - default: - return GLZW_INTERNAL_ERROR; - } -} -/* -END- GIF LZW encoder. */ - -int -ImagingGifEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { - UINT8 *ptr; - UINT8 *sub_block_ptr; - UINT8 *sub_block_limit; - UINT8 *buf_limit; - GIFENCODERSTATE *context = (GIFENCODERSTATE *)state->context; - int r; - - UINT32 in_avail, in_used; - UINT32 out_avail, out_used; - - if (state->state == INIT) { - state->state = ENCODE; - glzwe_init(context); - - if (context->interlace) { - context->interlace = 1; - context->step = 8; - } else { - context->step = 1; - } - - /* Need at least 2 bytes for data sub-block; 5 for empty image */ - if (bytes < 5) { - state->errcode = IMAGING_CODEC_CONFIG; - return 0; - } - /* sanity check */ - if (state->xsize <= 0 || state->ysize <= 0) { - /* Is this better than an error return? */ - /* This will handle any legal "LZW Minimum Code Size" */ - memset(buf, 0, 5); - in_avail = 0; - out_avail = 5; - r = glzwe(context, (const UINT8 *)"", buf + 1, &in_avail, &out_avail, 1); - if (r == GLZW_OK) { - r = 5 - out_avail; - if (r < 1 || r > 3) { - state->errcode = IMAGING_CODEC_BROKEN; - return 0; - } - buf[0] = r; - state->errcode = IMAGING_CODEC_END; - return r + 2; - } else { - /* Should not be possible unless something external to this - * routine messes with our state data */ - state->errcode = IMAGING_CODEC_BROKEN; - return 0; - } - } - /* Init state->x to make if() below true the first time through. */ - state->x = state->xsize; - } - - buf_limit = buf + bytes; - sub_block_limit = sub_block_ptr = ptr = buf; - - /* On entry, buf is output buffer, bytes is space available in buf. - * Loop here getting input until buf is full or image is all encoded. */ - for (;;) { - /* Set up sub-block ptr and limit. sub_block_ptr stays at beginning - * of sub-block until it is full. ptr will advance when any data is - * placed in buf. - */ - if (ptr >= sub_block_limit) { - if (buf_limit - ptr < 2) { /* Need at least 2 for data sub-block */ - return ptr - buf; - } - sub_block_ptr = ptr; - sub_block_limit = - sub_block_ptr + - (256 < buf_limit - sub_block_ptr ? 256 : buf_limit - sub_block_ptr); - *ptr++ = 0; - } - - /* Get next row of pixels. */ - /* This if() originally tested state->x==0 for the first time through. - * This no longer works, as the loop will not advance state->x if - * glzwe() does not consume any input; this would advance the row - * spuriously. Now pre-init state->x above for first time, and avoid - * entering if() when state->state is FINISH, or it will loop - * infinitely. - */ - if (state->x >= state->xsize && state->state == ENCODE) { - if (!context->interlace && state->y >= state->ysize) { - state->state = FINISH; - continue; - } - - /* get another line of data */ - state->shuffle( - state->buffer, - (UINT8 *)im->image[state->y + state->yoff] + - state->xoff * im->pixelsize, - state->xsize); - state->x = 0; - - /* step forward, according to the interlace settings */ - state->y += context->step; - while (context->interlace && state->y >= state->ysize) { - switch (context->interlace) { - case 1: - state->y = 4; - context->interlace = 2; - break; - case 2: - context->step = 4; - state->y = 2; - context->interlace = 3; - break; - case 3: - context->step = 2; - state->y = 1; - context->interlace = 0; - break; - default: - /* just make sure we don't loop forever */ - context->interlace = 0; - } - } - } - - in_avail = state->xsize - state->x; /* bytes left in line */ - out_avail = sub_block_limit - ptr; /* bytes left in sub-block */ - r = glzwe( - context, - &state->buffer[state->x], - ptr, - &in_avail, - &out_avail, - state->state == FINISH); - out_used = sub_block_limit - ptr - out_avail; - *sub_block_ptr += out_used; - ptr += out_used; - in_used = state->xsize - state->x - in_avail; - state->x += in_used; - - if (r == GLZW_OK) { - /* Should not be possible when end-of-data flag is false. */ - state->errcode = IMAGING_CODEC_END; - return ptr - buf; - } else if (r == GLZW_NO_INPUT_AVAIL) { - /* Used all the input line; get another line */ - continue; - } else if (r == GLZW_NO_OUTPUT_AVAIL) { - /* subblock is full */ - continue; - } else { - /* Should not be possible unless something external to this - * routine messes with our state data */ - state->errcode = IMAGING_CODEC_BROKEN; - return 0; - } - } -} +/* + * The Python Imaging Library. + * $Id$ + * + * encoder for uncompressed GIF data + * + * history: + * 97-01-05 fl created (writes uncompressed data) + * 97-08-27 fl fixed off-by-one error in buffer size test + * 98-07-09 fl added interlace write support + * 99-02-07 fl rewritten, now uses a run-length encoding strategy + * 99-02-08 fl improved run-length encoding for long runs + * 2020-12-12 rdg Reworked for LZW compression. + * + * Copyright (c) Secret Labs AB 1997-99. + * Copyright (c) Fredrik Lundh 1997. + * + * See the README file for information on usage and redistribution. + */ + +#include "Imaging.h" + +#include "Gif.h" + +enum { INIT, ENCODE, FINISH }; + +/* GIF LZW encoder by Raymond Gardner. */ +/* Released here under PIL license. */ + +/* This LZW encoder conforms to the GIF LZW format specified in the original + * Compuserve GIF 87a and GIF 89a specifications (see e.g. + * https://www.w3.org/Graphics/GIF/spec-gif87.txt Appendix C and + * https://www.w3.org/Graphics/GIF/spec-gif89a.txt Appendix F). + */ + +/* Return values */ +#define GLZW_OK 0 +#define GLZW_NO_INPUT_AVAIL 1 +#define GLZW_NO_OUTPUT_AVAIL 2 +#define GLZW_INTERNAL_ERROR 3 + +#define CODE_LIMIT 4096 + +/* Values of entry_state */ +enum { + LZW_INITIAL, + LZW_TRY_IN1, + LZW_TRY_IN2, + LZW_TRY_OUT1, + LZW_TRY_OUT2, + LZW_FINISHED +}; + +/* Values of control_state */ +enum { PUT_HEAD, PUT_INIT_CLEAR, PUT_CLEAR, PUT_LAST_HEAD, PUT_END }; + +static void +glzwe_reset(GIFENCODERSTATE *st) { + st->next_code = st->end_code + 1; + st->max_code = 2 * st->clear_code - 1; + st->code_width = st->bits + 1; + memset(st->codes, 0, sizeof(st->codes)); +} + +static void +glzwe_init(GIFENCODERSTATE *st) { + st->clear_code = 1 << st->bits; + st->end_code = st->clear_code + 1; + glzwe_reset(st); + st->entry_state = LZW_INITIAL; + st->buf_bits_left = 8; + st->code_buffer = 0; +} + +static int +glzwe( + GIFENCODERSTATE *st, + const UINT8 *in_ptr, + UINT8 *out_ptr, + UINT32 *in_avail, + UINT32 *out_avail, + UINT32 end_of_data) { + switch (st->entry_state) { + case LZW_TRY_IN1: +get_first_byte: + if (!*in_avail) { + if (end_of_data) { + goto end_of_data; + } + st->entry_state = LZW_TRY_IN1; + return GLZW_NO_INPUT_AVAIL; + } + st->head = *in_ptr++; + (*in_avail)--; + + case LZW_TRY_IN2: +encode_loop: + if (!*in_avail) { + if (end_of_data) { + st->code = st->head; + st->put_state = PUT_LAST_HEAD; + goto put_code; + } + st->entry_state = LZW_TRY_IN2; + return GLZW_NO_INPUT_AVAIL; + } + st->tail = *in_ptr++; + (*in_avail)--; + + /* Knuth TAOCP vol 3 sec. 6.4 algorithm D. */ + /* Hash found experimentally to be pretty good. */ + /* This works ONLY with TABLE_SIZE a power of 2. */ + st->probe = ((st->head ^ (st->tail << 6)) * 31) & (TABLE_SIZE - 1); + while (st->codes[st->probe]) { + if ((st->codes[st->probe] & 0xFFFFF) == ((st->head << 8) | st->tail)) { + st->head = st->codes[st->probe] >> 20; + goto encode_loop; + } else { + // Reprobe decrement must be non-zero and relatively prime to table + // size. So, any odd positive number for power-of-2 size. + if ((st->probe -= ((st->tail << 2) | 1)) < 0) { + st->probe += TABLE_SIZE; + } + } + } + /* Key not found, probe is at empty slot. */ + st->code = st->head; + st->put_state = PUT_HEAD; + goto put_code; +insert_code_or_clear: /* jump here after put_code */ + if (st->next_code < CODE_LIMIT) { + st->codes[st->probe] = + (st->next_code << 20) | (st->head << 8) | st->tail; + if (st->next_code > st->max_code) { + st->max_code = st->max_code * 2 + 1; + st->code_width++; + } + st->next_code++; + } else { + st->code = st->clear_code; + st->put_state = PUT_CLEAR; + goto put_code; +reset_after_clear: /* jump here after put_code */ + glzwe_reset(st); + } + st->head = st->tail; + goto encode_loop; + + case LZW_INITIAL: + glzwe_reset(st); + st->code = st->clear_code; + st->put_state = PUT_INIT_CLEAR; +put_code: + st->code_bits_left = st->code_width; +check_buf_bits: + if (!st->buf_bits_left) { /* out buffer full */ + + case LZW_TRY_OUT1: + if (!*out_avail) { + st->entry_state = LZW_TRY_OUT1; + return GLZW_NO_OUTPUT_AVAIL; + } + *out_ptr++ = st->code_buffer; + (*out_avail)--; + st->code_buffer = 0; + st->buf_bits_left = 8; + } + /* code bits to pack */ + UINT32 n = st->buf_bits_left < st->code_bits_left ? st->buf_bits_left + : st->code_bits_left; + st->code_buffer |= (st->code & ((1 << n) - 1)) << (8 - st->buf_bits_left); + st->code >>= n; + st->buf_bits_left -= n; + st->code_bits_left -= n; + if (st->code_bits_left) { + goto check_buf_bits; + } + switch (st->put_state) { + case PUT_INIT_CLEAR: + goto get_first_byte; + case PUT_HEAD: + goto insert_code_or_clear; + case PUT_CLEAR: + goto reset_after_clear; + case PUT_LAST_HEAD: + goto end_of_data; + case PUT_END: + goto flush_code_buffer; + default: + return GLZW_INTERNAL_ERROR; + } + +end_of_data: + st->code = st->end_code; + st->put_state = PUT_END; + goto put_code; +flush_code_buffer: /* jump here after put_code */ + if (st->buf_bits_left < 8) { + case LZW_TRY_OUT2: + if (!*out_avail) { + st->entry_state = LZW_TRY_OUT2; + return GLZW_NO_OUTPUT_AVAIL; + } + *out_ptr++ = st->code_buffer; + (*out_avail)--; + } + st->entry_state = LZW_FINISHED; + return GLZW_OK; + + case LZW_FINISHED: + return GLZW_OK; + + default: + return GLZW_INTERNAL_ERROR; + } +} +/* -END- GIF LZW encoder. */ + +int +ImagingGifEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { + UINT8 *ptr; + UINT8 *sub_block_ptr; + UINT8 *sub_block_limit; + UINT8 *buf_limit; + GIFENCODERSTATE *context = (GIFENCODERSTATE *)state->context; + int r; + + UINT32 in_avail, in_used; + UINT32 out_avail, out_used; + + if (state->state == INIT) { + state->state = ENCODE; + glzwe_init(context); + + if (context->interlace) { + context->interlace = 1; + context->step = 8; + } else { + context->step = 1; + } + + /* Need at least 2 bytes for data sub-block; 5 for empty image */ + if (bytes < 5) { + state->errcode = IMAGING_CODEC_CONFIG; + return 0; + } + /* sanity check */ + if (state->xsize <= 0 || state->ysize <= 0) { + /* Is this better than an error return? */ + /* This will handle any legal "LZW Minimum Code Size" */ + memset(buf, 0, 5); + in_avail = 0; + out_avail = 5; + r = glzwe(context, (const UINT8 *)"", buf + 1, &in_avail, &out_avail, 1); + if (r == GLZW_OK) { + r = 5 - out_avail; + if (r < 1 || r > 3) { + state->errcode = IMAGING_CODEC_BROKEN; + return 0; + } + buf[0] = r; + state->errcode = IMAGING_CODEC_END; + return r + 2; + } else { + /* Should not be possible unless something external to this + * routine messes with our state data */ + state->errcode = IMAGING_CODEC_BROKEN; + return 0; + } + } + /* Init state->x to make if() below true the first time through. */ + state->x = state->xsize; + } + + buf_limit = buf + bytes; + sub_block_limit = sub_block_ptr = ptr = buf; + + /* On entry, buf is output buffer, bytes is space available in buf. + * Loop here getting input until buf is full or image is all encoded. */ + for (;;) { + /* Set up sub-block ptr and limit. sub_block_ptr stays at beginning + * of sub-block until it is full. ptr will advance when any data is + * placed in buf. + */ + if (ptr >= sub_block_limit) { + if (buf_limit - ptr < 2) { /* Need at least 2 for data sub-block */ + return ptr - buf; + } + sub_block_ptr = ptr; + sub_block_limit = + sub_block_ptr + + (256 < buf_limit - sub_block_ptr ? 256 : buf_limit - sub_block_ptr); + *ptr++ = 0; + } + + /* Get next row of pixels. */ + /* This if() originally tested state->x==0 for the first time through. + * This no longer works, as the loop will not advance state->x if + * glzwe() does not consume any input; this would advance the row + * spuriously. Now pre-init state->x above for first time, and avoid + * entering if() when state->state is FINISH, or it will loop + * infinitely. + */ + if (state->x >= state->xsize && state->state == ENCODE) { + if (!context->interlace && state->y >= state->ysize) { + state->state = FINISH; + continue; + } + + /* get another line of data */ + state->shuffle( + state->buffer, + (UINT8 *)im->image[state->y + state->yoff] + + state->xoff * im->pixelsize, + state->xsize); + state->x = 0; + + /* step forward, according to the interlace settings */ + state->y += context->step; + while (context->interlace && state->y >= state->ysize) { + switch (context->interlace) { + case 1: + state->y = 4; + context->interlace = 2; + break; + case 2: + context->step = 4; + state->y = 2; + context->interlace = 3; + break; + case 3: + context->step = 2; + state->y = 1; + context->interlace = 0; + break; + default: + /* just make sure we don't loop forever */ + context->interlace = 0; + } + } + } + + in_avail = state->xsize - state->x; /* bytes left in line */ + out_avail = sub_block_limit - ptr; /* bytes left in sub-block */ + r = glzwe( + context, + &state->buffer[state->x], + ptr, + &in_avail, + &out_avail, + state->state == FINISH); + out_used = sub_block_limit - ptr - out_avail; + *sub_block_ptr += out_used; + ptr += out_used; + in_used = state->xsize - state->x - in_avail; + state->x += in_used; + + if (r == GLZW_OK) { + /* Should not be possible when end-of-data flag is false. */ + state->errcode = IMAGING_CODEC_END; + return ptr - buf; + } else if (r == GLZW_NO_INPUT_AVAIL) { + /* Used all the input line; get another line */ + continue; + } else if (r == GLZW_NO_OUTPUT_AVAIL) { + /* subblock is full */ + continue; + } else { + /* Should not be possible unless something external to this + * routine messes with our state data */ + state->errcode = IMAGING_CODEC_BROKEN; + return 0; + } + } +} diff --git a/src/libImaging/HexDecode.c b/src/libImaging/HexDecode.c index bd16cdbe1..8f8627992 100644 --- a/src/libImaging/HexDecode.c +++ b/src/libImaging/HexDecode.c @@ -1,63 +1,63 @@ -/* - * The Python Imaging Library. - * $Id$ - * - * decoder for hex encoded image data - * - * history: - * 96-05-16 fl Created - * - * Copyright (c) Fredrik Lundh 1996. - * Copyright (c) Secret Labs AB 1997. - * - * See the README file for information on usage and redistribution. - */ - -#include "Imaging.h" - -#define HEX(v) \ - ((v >= '0' && v <= '9') ? v - '0' \ - : (v >= 'a' && v <= 'f') ? v - 'a' + 10 \ - : (v >= 'A' && v <= 'F') ? v - 'A' + 10 \ - : -1) - -int -ImagingHexDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) { - UINT8 *ptr; - int a, b; - - ptr = buf; - - for (;;) { - if (bytes < 2) { - return ptr - buf; - } - - a = HEX(ptr[0]); - b = HEX(ptr[1]); - - if (a < 0 || b < 0) { - ptr++; - bytes--; - - } else { - ptr += 2; - bytes -= 2; - - state->buffer[state->x] = (a << 4) + b; - - if (++state->x >= state->bytes) { - /* Got a full line, unpack it */ - state->shuffle( - (UINT8 *)im->image[state->y], state->buffer, state->xsize); - - state->x = 0; - - if (++state->y >= state->ysize) { - /* End of file (errcode = 0) */ - return -1; - } - } - } - } -} +/* + * The Python Imaging Library. + * $Id$ + * + * decoder for hex encoded image data + * + * history: + * 96-05-16 fl Created + * + * Copyright (c) Fredrik Lundh 1996. + * Copyright (c) Secret Labs AB 1997. + * + * See the README file for information on usage and redistribution. + */ + +#include "Imaging.h" + +#define HEX(v) \ + ((v >= '0' && v <= '9') ? v - '0' \ + : (v >= 'a' && v <= 'f') ? v - 'a' + 10 \ + : (v >= 'A' && v <= 'F') ? v - 'A' + 10 \ + : -1) + +int +ImagingHexDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) { + UINT8 *ptr; + int a, b; + + ptr = buf; + + for (;;) { + if (bytes < 2) { + return ptr - buf; + } + + a = HEX(ptr[0]); + b = HEX(ptr[1]); + + if (a < 0 || b < 0) { + ptr++; + bytes--; + + } else { + ptr += 2; + bytes -= 2; + + state->buffer[state->x] = (a << 4) + b; + + if (++state->x >= state->bytes) { + /* Got a full line, unpack it */ + state->shuffle( + (UINT8 *)im->image[state->y], state->buffer, state->xsize); + + state->x = 0; + + if (++state->y >= state->ysize) { + /* End of file (errcode = 0) */ + return -1; + } + } + } + } +} diff --git a/src/libImaging/Histo.c b/src/libImaging/Histo.c index c5a547a64..51e548871 100644 --- a/src/libImaging/Histo.c +++ b/src/libImaging/Histo.c @@ -1,201 +1,201 @@ -/* - * The Python Imaging Library - * $Id$ - * - * histogram support - * - * history: - * 1995-06-15 fl Created. - * 1996-04-05 fl Fixed histogram for multiband images. - * 1997-02-23 fl Added mask support - * 1998-07-01 fl Added basic 32-bit float/integer support - * - * 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" - -/* HISTOGRAM */ -/* -------------------------------------------------------------------- - * Take a histogram of an image. Returns a histogram object containing - * 256 slots per band in the input image. - */ - -void -ImagingHistogramDelete(ImagingHistogram h) { - if (h) { - if (h->histogram) { - free(h->histogram); - } - free(h); - } -} - -ImagingHistogram -ImagingHistogramNew(Imaging im) { - ImagingHistogram h; - - /* Create histogram descriptor */ - h = calloc(1, sizeof(struct ImagingHistogramInstance)); - if (!h) { - return (ImagingHistogram)ImagingError_MemoryError(); - } - strncpy(h->mode, im->mode, IMAGING_MODE_LENGTH - 1); - h->mode[IMAGING_MODE_LENGTH - 1] = 0; - - h->bands = im->bands; - h->histogram = calloc(im->pixelsize, 256 * sizeof(long)); - if (!h->histogram) { - free(h); - return (ImagingHistogram)ImagingError_MemoryError(); - } - - return h; -} - -ImagingHistogram -ImagingGetHistogram(Imaging im, Imaging imMask, void *minmax) { - ImagingSectionCookie cookie; - int x, y, i; - ImagingHistogram h; - INT32 imin, imax; - FLOAT32 fmin, fmax, scale; - - if (!im) { - return ImagingError_ModeError(); - } - - if (imMask) { - /* Validate mask */ - if (im->xsize != imMask->xsize || im->ysize != imMask->ysize) { - return ImagingError_Mismatch(); - } - if (strcmp(imMask->mode, "1") != 0 && strcmp(imMask->mode, "L") != 0) { - return ImagingError_ValueError("bad transparency mask"); - } - } - - h = ImagingHistogramNew(im); - if (!h) { - return NULL; - } - - if (imMask) { - /* mask */ - if (im->image8) { - ImagingSectionEnter(&cookie); - for (y = 0; y < im->ysize; y++) { - for (x = 0; x < im->xsize; x++) { - if (imMask->image8[y][x] != 0) { - h->histogram[im->image8[y][x]]++; - } - } - } - ImagingSectionLeave(&cookie); - } else { /* yes, we need the braces. C isn't Python! */ - if (im->type != IMAGING_TYPE_UINT8) { - ImagingHistogramDelete(h); - return ImagingError_ModeError(); - } - ImagingSectionEnter(&cookie); - for (y = 0; y < im->ysize; y++) { - UINT8 *in = (UINT8 *)im->image32[y]; - for (x = 0; x < im->xsize; x++) { - if (imMask->image8[y][x] != 0) { - h->histogram[(*in++)]++; - h->histogram[(*in++) + 256]++; - h->histogram[(*in++) + 512]++; - h->histogram[(*in++) + 768]++; - } else { - in += 4; - } - } - } - ImagingSectionLeave(&cookie); - } - } else { - /* mask not given; process pixels in image */ - if (im->image8) { - ImagingSectionEnter(&cookie); - for (y = 0; y < im->ysize; y++) { - for (x = 0; x < im->xsize; x++) { - h->histogram[im->image8[y][x]]++; - } - } - ImagingSectionLeave(&cookie); - } else { - switch (im->type) { - case IMAGING_TYPE_UINT8: - ImagingSectionEnter(&cookie); - for (y = 0; y < im->ysize; y++) { - UINT8 *in = (UINT8 *)im->image[y]; - for (x = 0; x < im->xsize; x++) { - h->histogram[(*in++)]++; - h->histogram[(*in++) + 256]++; - h->histogram[(*in++) + 512]++; - h->histogram[(*in++) + 768]++; - } - } - ImagingSectionLeave(&cookie); - break; - case IMAGING_TYPE_INT32: - if (!minmax) { - ImagingHistogramDelete(h); - return ImagingError_ValueError("min/max not given"); - } - if (!im->xsize || !im->ysize) { - break; - } - memcpy(&imin, minmax, sizeof(imin)); - memcpy(&imax, ((char *)minmax) + sizeof(imin), sizeof(imax)); - if (imin >= imax) { - break; - } - ImagingSectionEnter(&cookie); - scale = 255.0F / (imax - imin); - for (y = 0; y < im->ysize; y++) { - INT32 *in = im->image32[y]; - for (x = 0; x < im->xsize; x++) { - i = (int)(((*in++) - imin) * scale); - if (i >= 0 && i < 256) { - h->histogram[i]++; - } - } - } - ImagingSectionLeave(&cookie); - break; - case IMAGING_TYPE_FLOAT32: - if (!minmax) { - ImagingHistogramDelete(h); - return ImagingError_ValueError("min/max not given"); - } - if (!im->xsize || !im->ysize) { - break; - } - memcpy(&fmin, minmax, sizeof(fmin)); - memcpy(&fmax, ((char *)minmax) + sizeof(fmin), sizeof(fmax)); - if (fmin >= fmax) { - break; - } - ImagingSectionEnter(&cookie); - scale = 255.0F / (fmax - fmin); - for (y = 0; y < im->ysize; y++) { - FLOAT32 *in = (FLOAT32 *)im->image32[y]; - for (x = 0; x < im->xsize; x++) { - i = (int)(((*in++) - fmin) * scale); - if (i >= 0 && i < 256) { - h->histogram[i]++; - } - } - } - ImagingSectionLeave(&cookie); - break; - } - } - } - - return h; -} +/* + * The Python Imaging Library + * $Id$ + * + * histogram support + * + * history: + * 1995-06-15 fl Created. + * 1996-04-05 fl Fixed histogram for multiband images. + * 1997-02-23 fl Added mask support + * 1998-07-01 fl Added basic 32-bit float/integer support + * + * 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" + +/* HISTOGRAM */ +/* -------------------------------------------------------------------- + * Take a histogram of an image. Returns a histogram object containing + * 256 slots per band in the input image. + */ + +void +ImagingHistogramDelete(ImagingHistogram h) { + if (h) { + if (h->histogram) { + free(h->histogram); + } + free(h); + } +} + +ImagingHistogram +ImagingHistogramNew(Imaging im) { + ImagingHistogram h; + + /* Create histogram descriptor */ + h = calloc(1, sizeof(struct ImagingHistogramInstance)); + if (!h) { + return (ImagingHistogram)ImagingError_MemoryError(); + } + strncpy(h->mode, im->mode, IMAGING_MODE_LENGTH - 1); + h->mode[IMAGING_MODE_LENGTH - 1] = 0; + + h->bands = im->bands; + h->histogram = calloc(im->pixelsize, 256 * sizeof(long)); + if (!h->histogram) { + free(h); + return (ImagingHistogram)ImagingError_MemoryError(); + } + + return h; +} + +ImagingHistogram +ImagingGetHistogram(Imaging im, Imaging imMask, void *minmax) { + ImagingSectionCookie cookie; + int x, y, i; + ImagingHistogram h; + INT32 imin, imax; + FLOAT32 fmin, fmax, scale; + + if (!im) { + return ImagingError_ModeError(); + } + + if (imMask) { + /* Validate mask */ + if (im->xsize != imMask->xsize || im->ysize != imMask->ysize) { + return ImagingError_Mismatch(); + } + if (strcmp(imMask->mode, "1") != 0 && strcmp(imMask->mode, "L") != 0) { + return ImagingError_ValueError("bad transparency mask"); + } + } + + h = ImagingHistogramNew(im); + if (!h) { + return NULL; + } + + if (imMask) { + /* mask */ + if (im->image8) { + ImagingSectionEnter(&cookie); + for (y = 0; y < im->ysize; y++) { + for (x = 0; x < im->xsize; x++) { + if (imMask->image8[y][x] != 0) { + h->histogram[im->image8[y][x]]++; + } + } + } + ImagingSectionLeave(&cookie); + } else { /* yes, we need the braces. C isn't Python! */ + if (im->type != IMAGING_TYPE_UINT8) { + ImagingHistogramDelete(h); + return ImagingError_ModeError(); + } + ImagingSectionEnter(&cookie); + for (y = 0; y < im->ysize; y++) { + UINT8 *in = (UINT8 *)im->image32[y]; + for (x = 0; x < im->xsize; x++) { + if (imMask->image8[y][x] != 0) { + h->histogram[(*in++)]++; + h->histogram[(*in++) + 256]++; + h->histogram[(*in++) + 512]++; + h->histogram[(*in++) + 768]++; + } else { + in += 4; + } + } + } + ImagingSectionLeave(&cookie); + } + } else { + /* mask not given; process pixels in image */ + if (im->image8) { + ImagingSectionEnter(&cookie); + for (y = 0; y < im->ysize; y++) { + for (x = 0; x < im->xsize; x++) { + h->histogram[im->image8[y][x]]++; + } + } + ImagingSectionLeave(&cookie); + } else { + switch (im->type) { + case IMAGING_TYPE_UINT8: + ImagingSectionEnter(&cookie); + for (y = 0; y < im->ysize; y++) { + UINT8 *in = (UINT8 *)im->image[y]; + for (x = 0; x < im->xsize; x++) { + h->histogram[(*in++)]++; + h->histogram[(*in++) + 256]++; + h->histogram[(*in++) + 512]++; + h->histogram[(*in++) + 768]++; + } + } + ImagingSectionLeave(&cookie); + break; + case IMAGING_TYPE_INT32: + if (!minmax) { + ImagingHistogramDelete(h); + return ImagingError_ValueError("min/max not given"); + } + if (!im->xsize || !im->ysize) { + break; + } + memcpy(&imin, minmax, sizeof(imin)); + memcpy(&imax, ((char *)minmax) + sizeof(imin), sizeof(imax)); + if (imin >= imax) { + break; + } + ImagingSectionEnter(&cookie); + scale = 255.0F / (imax - imin); + for (y = 0; y < im->ysize; y++) { + INT32 *in = im->image32[y]; + for (x = 0; x < im->xsize; x++) { + i = (int)(((*in++) - imin) * scale); + if (i >= 0 && i < 256) { + h->histogram[i]++; + } + } + } + ImagingSectionLeave(&cookie); + break; + case IMAGING_TYPE_FLOAT32: + if (!minmax) { + ImagingHistogramDelete(h); + return ImagingError_ValueError("min/max not given"); + } + if (!im->xsize || !im->ysize) { + break; + } + memcpy(&fmin, minmax, sizeof(fmin)); + memcpy(&fmax, ((char *)minmax) + sizeof(fmin), sizeof(fmax)); + if (fmin >= fmax) { + break; + } + ImagingSectionEnter(&cookie); + scale = 255.0F / (fmax - fmin); + for (y = 0; y < im->ysize; y++) { + FLOAT32 *in = (FLOAT32 *)im->image32[y]; + for (x = 0; x < im->xsize; x++) { + i = (int)(((*in++) - fmin) * scale); + if (i >= 0 && i < 256) { + h->histogram[i]++; + } + } + } + ImagingSectionLeave(&cookie); + break; + } + } + } + + return h; +} diff --git a/src/libImaging/ImDib.h b/src/libImaging/ImDib.h index 91ff3f322..7970ef4a6 100644 --- a/src/libImaging/ImDib.h +++ b/src/libImaging/ImDib.h @@ -1,64 +1,64 @@ -/* - * The Python Imaging Library - * $Id$ - * - * Windows DIB specifics - * - * Copyright (c) Secret Labs AB 1997-98. - * Copyright (c) Fredrik Lundh 1996. - * - * See the README file for information on usage and redistribution. - */ - -#ifdef _WIN32 - -#include "ImPlatform.h" - -#if defined(__cplusplus) -extern "C" { -#endif - -struct ImagingDIBInstance { - /* Windows interface */ - HDC dc; - HBITMAP bitmap; - HGDIOBJ old_bitmap; - BITMAPINFO *info; - UINT8 *bits; - HPALETTE palette; - /* Used by cut and paste */ - char mode[4]; - int xsize, ysize; - int pixelsize; - int linesize; - ImagingShuffler pack; - ImagingShuffler unpack; -}; - -typedef struct ImagingDIBInstance *ImagingDIB; - -extern char * -ImagingGetModeDIB(int size_out[2]); - -extern ImagingDIB -ImagingNewDIB(const char *mode, int xsize, int ysize); - -extern void -ImagingDeleteDIB(ImagingDIB im); - -extern void -ImagingDrawDIB(ImagingDIB dib, void *dc, int dst[4], int src[4]); -extern void -ImagingExposeDIB(ImagingDIB dib, void *dc); - -extern int -ImagingQueryPaletteDIB(ImagingDIB dib, void *dc); - -extern void -ImagingPasteDIB(ImagingDIB dib, Imaging im, int xy[4]); - -#if defined(__cplusplus) -} -#endif - -#endif +/* + * The Python Imaging Library + * $Id$ + * + * Windows DIB specifics + * + * Copyright (c) Secret Labs AB 1997-98. + * Copyright (c) Fredrik Lundh 1996. + * + * See the README file for information on usage and redistribution. + */ + +#ifdef _WIN32 + +#include "ImPlatform.h" + +#if defined(__cplusplus) +extern "C" { +#endif + +struct ImagingDIBInstance { + /* Windows interface */ + HDC dc; + HBITMAP bitmap; + HGDIOBJ old_bitmap; + BITMAPINFO *info; + UINT8 *bits; + HPALETTE palette; + /* Used by cut and paste */ + char mode[4]; + int xsize, ysize; + int pixelsize; + int linesize; + ImagingShuffler pack; + ImagingShuffler unpack; +}; + +typedef struct ImagingDIBInstance *ImagingDIB; + +extern char * +ImagingGetModeDIB(int size_out[2]); + +extern ImagingDIB +ImagingNewDIB(const char *mode, int xsize, int ysize); + +extern void +ImagingDeleteDIB(ImagingDIB im); + +extern void +ImagingDrawDIB(ImagingDIB dib, void *dc, int dst[4], int src[4]); +extern void +ImagingExposeDIB(ImagingDIB dib, void *dc); + +extern int +ImagingQueryPaletteDIB(ImagingDIB dib, void *dc); + +extern void +ImagingPasteDIB(ImagingDIB dib, Imaging im, int xy[4]); + +#if defined(__cplusplus) +} +#endif + +#endif diff --git a/src/libImaging/ImPlatform.h b/src/libImaging/ImPlatform.h index f6e7fb6b9..938ab1fc2 100644 --- a/src/libImaging/ImPlatform.h +++ b/src/libImaging/ImPlatform.h @@ -1,98 +1,98 @@ -/* - * The Python Imaging Library - * $Id$ - * - * platform declarations for the imaging core library - * - * Copyright (c) Fredrik Lundh 1995-2003. - */ - -#include "Python.h" - -/* Check that we have an ANSI compliant compiler */ -#ifndef HAVE_PROTOTYPES -#error Sorry, this library requires support for ANSI prototypes. -#endif -#ifndef STDC_HEADERS -#error Sorry, this library requires ANSI header files. -#endif - -#if defined(PIL_NO_INLINE) -#define inline -#else -#if defined(_MSC_VER) && !defined(__GNUC__) -#define inline __inline -#endif -#endif - -#if defined(_WIN32) || defined(__CYGWIN__) /* WIN */ - -#define WIN32_LEAN_AND_MEAN -#include - -#ifdef __CYGWIN__ -#undef _WIN64 -#undef _WIN32 -#undef __WIN32__ -#undef WIN32 -#endif - -#else /* not WIN */ -/* For System that are not Windows, we'll need to define these. */ -/* We have to define them instead of using typedef because the JPEG lib also - 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 -#define UINT8 uint8_t -#define INT16 int16_t -#define UINT16 uint16_t -#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) */ -#define FLOAT16 UINT16 -#define FLOAT32 float -#define FLOAT64 double - -#ifdef _MSC_VER -typedef signed __int64 int64_t; -#endif - -#ifdef __GNUC__ -#define GCC_VERSION (__GNUC__ * 10000 + __GNUC_MINOR__ * 100 + __GNUC_PATCHLEVEL__) -#endif +/* + * The Python Imaging Library + * $Id$ + * + * platform declarations for the imaging core library + * + * Copyright (c) Fredrik Lundh 1995-2003. + */ + +#include "Python.h" + +/* Check that we have an ANSI compliant compiler */ +#ifndef HAVE_PROTOTYPES +#error Sorry, this library requires support for ANSI prototypes. +#endif +#ifndef STDC_HEADERS +#error Sorry, this library requires ANSI header files. +#endif + +#if defined(PIL_NO_INLINE) +#define inline +#else +#if defined(_MSC_VER) && !defined(__GNUC__) +#define inline __inline +#endif +#endif + +#if defined(_WIN32) || defined(__CYGWIN__) /* WIN */ + +#define WIN32_LEAN_AND_MEAN +#include + +#ifdef __CYGWIN__ +#undef _WIN64 +#undef _WIN32 +#undef __WIN32__ +#undef WIN32 +#endif + +#else /* not WIN */ +/* For System that are not Windows, we'll need to define these. */ +/* We have to define them instead of using typedef because the JPEG lib also + 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 +#define UINT8 uint8_t +#define INT16 int16_t +#define UINT16 uint16_t +#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) */ +#define FLOAT16 UINT16 +#define FLOAT32 float +#define FLOAT64 double + +#ifdef _MSC_VER +typedef signed __int64 int64_t; +#endif + +#ifdef __GNUC__ +#define GCC_VERSION (__GNUC__ * 10000 + __GNUC_MINOR__ * 100 + __GNUC_PATCHLEVEL__) +#endif diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index 1f2c03e93..9b1e6ef2f 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -1,695 +1,695 @@ -/* - * The Python Imaging Library - * $Id$ - * - * declarations for the imaging core library - * - * Copyright (c) 1997-2005 by Secret Labs AB - * Copyright (c) 1995-2005 by Fredrik Lundh - * - * See the README file for information on usage and redistribution. - */ - -#include "ImPlatform.h" - -#if defined(__cplusplus) -extern "C" { -#endif - -#ifndef M_PI -#define M_PI 3.1415926535897932384626433832795 -#endif - -/* -------------------------------------------------------------------- */ - -/* - * Image data organization: - * - * mode bytes byte order - * ------------------------------- - * 1 1 1 - * L 1 L - * P 1 P - * I 4 I (32-bit integer, native byte order) - * F 4 F (32-bit IEEE float, native byte order) - * RGB 4 R, G, B, - - * RGBA 4 R, G, B, A - * CMYK 4 C, M, Y, K - * YCbCr 4 Y, Cb, Cr, - - * Lab 4 L, a, b, - - * - * experimental modes (incomplete): - * LA 4 L, -, -, A - * PA 4 P, -, -, A - * I;16 2 I (16-bit integer, native byte order) - * - * "P" is an 8-bit palette mode, which should be mapped through the - * palette member to get an output image. Check palette->mode to - * find the corresponding "real" mode. - * - * For information on how to access Imaging objects from your own C - * extensions, see http://www.effbot.org/zone/pil-extending.htm - */ - -/* Handles */ - -typedef struct ImagingMemoryInstance *Imaging; - -typedef struct ImagingAccessInstance *ImagingAccess; -typedef struct ImagingHistogramInstance *ImagingHistogram; -typedef struct ImagingOutlineInstance *ImagingOutline; -typedef struct ImagingPaletteInstance *ImagingPalette; - -/* handle magics (used with PyCObject). */ -#define IMAGING_MAGIC "PIL Imaging" - -/* pixel types */ -#define IMAGING_TYPE_UINT8 0 -#define IMAGING_TYPE_INT32 1 -#define IMAGING_TYPE_FLOAT32 2 -#define IMAGING_TYPE_SPECIAL 3 /* check mode for details */ - -#define IMAGING_MODE_LENGTH \ - 6 + 1 /* Band names ("1", "L", "P", "RGB", "RGBA", "CMYK", "YCbCr", "BGR;xy") */ - -typedef struct { - char *ptr; - int size; -} ImagingMemoryBlock; - -struct ImagingMemoryInstance { - /* Format */ - char mode[IMAGING_MODE_LENGTH]; /* Band names ("1", "L", "P", "RGB", "RGBA", "CMYK", - "YCbCr", "BGR;xy") */ - int type; /* Data type (IMAGING_TYPE_*) */ - int depth; /* Depth (ignored in this version) */ - int bands; /* Number of bands (1, 2, 3, or 4) */ - int xsize; /* Image dimension. */ - int ysize; - - /* Colour palette (for "P" images only) */ - ImagingPalette palette; - - /* Data pointers */ - UINT8 **image8; /* Set for 8-bit images (pixelsize=1). */ - INT32 **image32; /* Set for 32-bit images (pixelsize=4). */ - - /* Internals */ - char **image; /* Actual raster data. */ - char *block; /* Set if data is allocated in a single block. */ - ImagingMemoryBlock *blocks; /* Memory blocks for pixel storage */ - - int pixelsize; /* Size of a pixel, in bytes (1, 2 or 4) */ - int linesize; /* Size of a line, in bytes (xsize * pixelsize) */ - - /* Virtual methods */ - void (*destroy)(Imaging im); -}; - -#define IMAGING_PIXEL_1(im, x, y) ((im)->image8[(y)][(x)]) -#define IMAGING_PIXEL_L(im, x, y) ((im)->image8[(y)][(x)]) -#define IMAGING_PIXEL_LA(im, x, y) ((im)->image[(y)][(x) * 4]) -#define IMAGING_PIXEL_P(im, x, y) ((im)->image8[(y)][(x)]) -#define IMAGING_PIXEL_PA(im, x, y) ((im)->image[(y)][(x) * 4]) -#define IMAGING_PIXEL_I(im, x, y) ((im)->image32[(y)][(x)]) -#define IMAGING_PIXEL_F(im, x, y) (((FLOAT32 *)(im)->image32[y])[x]) -#define IMAGING_PIXEL_RGB(im, x, y) ((im)->image[(y)][(x) * 4]) -#define IMAGING_PIXEL_RGBA(im, x, y) ((im)->image[(y)][(x) * 4]) -#define IMAGING_PIXEL_CMYK(im, x, y) ((im)->image[(y)][(x) * 4]) -#define IMAGING_PIXEL_YCbCr(im, x, y) ((im)->image[(y)][(x) * 4]) - -#define IMAGING_PIXEL_UINT8(im, x, y) ((im)->image8[(y)][(x)]) -#define IMAGING_PIXEL_INT32(im, x, y) ((im)->image32[(y)][(x)]) -#define IMAGING_PIXEL_FLOAT32(im, x, y) (((FLOAT32 *)(im)->image32[y])[x]) - -struct ImagingAccessInstance { - const char *mode; - void (*get_pixel)(Imaging im, int x, int y, void *pixel); - void (*put_pixel)(Imaging im, int x, int y, const void *pixel); -}; - -struct ImagingHistogramInstance { - /* Format */ - char mode[IMAGING_MODE_LENGTH]; /* Band names (of corresponding source image) */ - int bands; /* Number of bands (1, 3, or 4) */ - - /* Data */ - long *histogram; /* Histogram (bands*256 longs) */ -}; - -struct ImagingPaletteInstance { - /* Format */ - char mode[IMAGING_MODE_LENGTH]; /* Band names */ - - /* Data */ - int size; - UINT8 palette[1024]; /* Palette data (same format as image data) */ - - INT16 *cache; /* Palette cache (used for predefined palettes) */ - int keep_cache; /* This palette will be reused; keep cache */ -}; - -typedef struct ImagingMemoryArena { - int alignment; /* Alignment in memory of each line of an image */ - int block_size; /* Preferred block size, bytes */ - int blocks_max; /* Maximum number of cached blocks */ - int blocks_cached; /* Current number of blocks not associated with images */ - ImagingMemoryBlock *blocks_pool; - int stats_new_count; /* Number of new allocated images */ - int stats_allocated_blocks; /* Number of allocated blocks */ - int stats_reused_blocks; /* Number of blocks which were retrieved from a pool */ - int stats_reallocated_blocks; /* Number of blocks which were actually reallocated - after retrieving */ - int stats_freed_blocks; /* Number of freed blocks */ -} *ImagingMemoryArena; - -/* Objects */ -/* ------- */ - -extern struct ImagingMemoryArena ImagingDefaultArena; -extern int -ImagingMemorySetBlocksMax(ImagingMemoryArena arena, int blocks_max); -extern void -ImagingMemoryClearCache(ImagingMemoryArena arena, int new_size); - -extern Imaging -ImagingNew(const char *mode, int xsize, int ysize); -extern Imaging -ImagingNewDirty(const char *mode, int xsize, int ysize); -extern Imaging -ImagingNew2Dirty(const char *mode, Imaging imOut, Imaging imIn); -extern void -ImagingDelete(Imaging im); - -extern Imaging -ImagingNewBlock(const char *mode, int xsize, int ysize); - -extern Imaging -ImagingNewPrologue(const char *mode, int xsize, int ysize); -extern Imaging -ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int structure_size); - -extern void -ImagingCopyPalette(Imaging destination, Imaging source); - -extern void -ImagingHistogramDelete(ImagingHistogram histogram); - -extern void -ImagingAccessInit(void); -extern ImagingAccess -ImagingAccessNew(Imaging im); -extern void -_ImagingAccessDelete(Imaging im, ImagingAccess access); -#define ImagingAccessDelete(im, access) /* nop, for now */ - -extern ImagingPalette -ImagingPaletteNew(const char *mode); -extern ImagingPalette -ImagingPaletteNewBrowser(void); -extern ImagingPalette -ImagingPaletteDuplicate(ImagingPalette palette); -extern void -ImagingPaletteDelete(ImagingPalette palette); - -extern int -ImagingPaletteCachePrepare(ImagingPalette palette); -extern void -ImagingPaletteCacheUpdate(ImagingPalette palette, int r, int g, int b); -extern void -ImagingPaletteCacheDelete(ImagingPalette palette); - -#define ImagingPaletteCache(p, r, g, b) \ - p->cache[(r >> 2) + (g >> 2) * 64 + (b >> 2) * 64 * 64] - -extern Imaging -ImagingQuantize(Imaging im, int colours, int mode, int kmeans); - -/* Threading */ -/* --------- */ - -typedef void *ImagingSectionCookie; - -extern void -ImagingSectionEnter(ImagingSectionCookie *cookie); -extern void -ImagingSectionLeave(ImagingSectionCookie *cookie); - -/* Exceptions */ -/* ---------- */ - -extern void * -ImagingError_OSError(void); -extern void * -ImagingError_MemoryError(void); -extern void * -ImagingError_ModeError(void); /* maps to ValueError by default */ -extern void * -ImagingError_Mismatch(void); /* maps to ValueError by default */ -extern void * -ImagingError_ValueError(const char *message); -extern void -ImagingError_Clear(void); - -/* Transform callbacks */ -/* ------------------- */ - -/* standard transforms */ -#define IMAGING_TRANSFORM_AFFINE 0 -#define IMAGING_TRANSFORM_PERSPECTIVE 2 -#define IMAGING_TRANSFORM_QUAD 3 - -/* standard filters */ -#define IMAGING_TRANSFORM_NEAREST 0 -#define IMAGING_TRANSFORM_BOX 4 -#define IMAGING_TRANSFORM_BILINEAR 2 -#define IMAGING_TRANSFORM_HAMMING 5 -#define IMAGING_TRANSFORM_BICUBIC 3 -#define IMAGING_TRANSFORM_LANCZOS 1 - -typedef int (*ImagingTransformMap)(double *X, double *Y, int x, int y, void *data); -typedef int (*ImagingTransformFilter)(void *out, Imaging im, double x, double y); - -/* Image Manipulation Methods */ -/* -------------------------- */ - -extern Imaging -ImagingAlphaComposite(Imaging imIn1, Imaging imIn2); -extern Imaging -ImagingBlend(Imaging imIn1, Imaging imIn2, float alpha); -extern Imaging -ImagingCopy(Imaging im); -extern Imaging -ImagingConvert(Imaging im, const char *mode, ImagingPalette palette, int dither); -extern Imaging -ImagingConvertInPlace(Imaging im, const char *mode); -extern Imaging -ImagingConvertMatrix(Imaging im, const char *mode, float m[]); -extern Imaging -ImagingConvertTransparent(Imaging im, const char *mode, int r, int g, int b); -extern Imaging -ImagingCrop(Imaging im, int x0, int y0, int x1, int y1); -extern Imaging -ImagingExpand(Imaging im, int x, int y); -extern Imaging -ImagingFill(Imaging im, const void *ink); -extern int -ImagingFill2( - Imaging into, const void *ink, Imaging mask, int x0, int y0, int x1, int y1); -extern Imaging -ImagingFillBand(Imaging im, int band, int color); -extern Imaging -ImagingFillLinearGradient(const char *mode); -extern Imaging -ImagingFillRadialGradient(const char *mode); -extern Imaging -ImagingFilter(Imaging im, int xsize, int ysize, const FLOAT32 *kernel, FLOAT32 offset); -extern Imaging -ImagingFlipLeftRight(Imaging imOut, Imaging imIn); -extern Imaging -ImagingFlipTopBottom(Imaging imOut, Imaging imIn); -extern Imaging -ImagingGaussianBlur( - Imaging imOut, Imaging imIn, float xradius, float yradius, int passes); -extern Imaging -ImagingGetBand(Imaging im, int band); -extern Imaging -ImagingMerge(const char *mode, Imaging bands[4]); -extern int -ImagingSplit(Imaging im, Imaging bands[4]); -extern int -ImagingGetBBox(Imaging im, int bbox[4], int alpha_only); -typedef struct { - int x, y; - INT32 count; - INT32 pixel; -} ImagingColorItem; -extern ImagingColorItem * -ImagingGetColors(Imaging im, int maxcolors, int *colors); -extern int -ImagingGetExtrema(Imaging im, void *extrema); -extern int -ImagingGetProjection(Imaging im, UINT8 *xproj, UINT8 *yproj); -extern ImagingHistogram -ImagingGetHistogram(Imaging im, Imaging mask, void *extrema); -extern Imaging -ImagingModeFilter(Imaging im, int size); -extern Imaging -ImagingNegative(Imaging im); -extern Imaging -ImagingOffset(Imaging im, int xoffset, int yoffset); -extern int -ImagingPaste(Imaging into, Imaging im, Imaging mask, int x0, int y0, int x1, int y1); -extern Imaging -ImagingPoint(Imaging im, const char *tablemode, const void *table); -extern Imaging -ImagingPointTransform(Imaging imIn, double scale, double offset); -extern Imaging -ImagingPutBand(Imaging im, Imaging imIn, int band); -extern Imaging -ImagingRankFilter(Imaging im, int size, int rank); -extern Imaging -ImagingRotate90(Imaging imOut, Imaging imIn); -extern Imaging -ImagingRotate180(Imaging imOut, Imaging imIn); -extern Imaging -ImagingRotate270(Imaging imOut, Imaging imIn); -extern Imaging -ImagingTranspose(Imaging imOut, Imaging imIn); -extern Imaging -ImagingTransverse(Imaging imOut, Imaging imIn); -extern Imaging -ImagingResample(Imaging imIn, int xsize, int ysize, int filter, float box[4]); -extern Imaging -ImagingReduce(Imaging imIn, int xscale, int yscale, int box[4]); -extern Imaging -ImagingTransform( - Imaging imOut, - Imaging imIn, - int method, - int x0, - int y0, - int x1, - int y1, - double a[8], - int filter, - int fill); -extern Imaging -ImagingUnsharpMask(Imaging imOut, Imaging im, float radius, int percent, int threshold); -extern Imaging -ImagingBoxBlur(Imaging imOut, Imaging imIn, float xradius, float yradius, int n); -extern Imaging -ImagingColorLUT3D_linear( - Imaging imOut, - Imaging imIn, - int table_channels, - int size1D, - int size2D, - int size3D, - INT16 *table); - -extern Imaging -ImagingCopy2(Imaging imOut, Imaging imIn); -extern Imaging -ImagingConvert2(Imaging imOut, Imaging imIn); - -/* Channel operations */ -/* any mode, except "F" */ -extern Imaging -ImagingChopLighter(Imaging imIn1, Imaging imIn2); -extern Imaging -ImagingChopDarker(Imaging imIn1, Imaging imIn2); -extern Imaging -ImagingChopDifference(Imaging imIn1, Imaging imIn2); -extern Imaging -ImagingChopMultiply(Imaging imIn1, Imaging imIn2); -extern Imaging -ImagingChopScreen(Imaging imIn1, Imaging imIn2); -extern Imaging -ImagingChopAdd(Imaging imIn1, Imaging imIn2, float scale, int offset); -extern Imaging -ImagingChopSubtract(Imaging imIn1, Imaging imIn2, float scale, int offset); -extern Imaging -ImagingChopAddModulo(Imaging imIn1, Imaging imIn2); -extern Imaging -ImagingChopSubtractModulo(Imaging imIn1, Imaging imIn2); -extern Imaging -ImagingChopSoftLight(Imaging imIn1, Imaging imIn2); -extern Imaging -ImagingChopHardLight(Imaging imIn1, Imaging imIn2); -extern Imaging -ImagingOverlay(Imaging imIn1, Imaging imIn2); - -/* "1" images only */ -extern Imaging -ImagingChopAnd(Imaging imIn1, Imaging imIn2); -extern Imaging -ImagingChopOr(Imaging imIn1, Imaging imIn2); -extern Imaging -ImagingChopXor(Imaging imIn1, Imaging imIn2); - -/* Graphics */ -extern int -ImagingDrawArc( - Imaging im, - int x0, - int y0, - int x1, - int y1, - float start, - float end, - const void *ink, - int width, - int op); -extern int -ImagingDrawBitmap(Imaging im, int x0, int y0, Imaging bitmap, const void *ink, int op); -extern int -ImagingDrawChord( - Imaging im, - int x0, - int y0, - int x1, - int y1, - float start, - float end, - const void *ink, - int fill, - int width, - int op); -extern int -ImagingDrawEllipse( - Imaging im, - int x0, - int y0, - int x1, - int y1, - const void *ink, - int fill, - int width, - int op); -extern int -ImagingDrawLine(Imaging im, int x0, int y0, int x1, int y1, const void *ink, int op); -extern int -ImagingDrawWideLine( - Imaging im, int x0, int y0, int x1, int y1, const void *ink, int width, int op); -extern int -ImagingDrawPieslice( - Imaging im, - int x0, - int y0, - int x1, - int y1, - float start, - float end, - const void *ink, - int fill, - int width, - int op); -extern int -ImagingDrawPoint(Imaging im, int x, int y, const void *ink, int op); -extern int -ImagingDrawPolygon( - Imaging im, int points, int *xy, const void *ink, int fill, int width, int op); -extern int -ImagingDrawRectangle( - Imaging im, - int x0, - int y0, - int x1, - int y1, - const void *ink, - int fill, - int width, - int op); - -/* Level 2 graphics (WORK IN PROGRESS) */ -extern ImagingOutline -ImagingOutlineNew(void); -extern void -ImagingOutlineDelete(ImagingOutline outline); - -extern int -ImagingDrawOutline( - Imaging im, ImagingOutline outline, const void *ink, int fill, int op); - -extern int -ImagingOutlineMove(ImagingOutline outline, float x, float y); -extern int -ImagingOutlineLine(ImagingOutline outline, float x, float y); -extern int -ImagingOutlineCurve( - ImagingOutline outline, float x1, float y1, float x2, float y2, float x3, float y3); -extern int -ImagingOutlineTransform(ImagingOutline outline, double a[6]); - -extern int -ImagingOutlineClose(ImagingOutline outline); - -/* Special effects */ -extern Imaging -ImagingEffectSpread(Imaging imIn, int distance); -extern Imaging -ImagingEffectNoise(int xsize, int ysize, float sigma); -extern Imaging -ImagingEffectMandelbrot(int xsize, int ysize, double extent[4], int quality); - -/* File I/O */ -/* -------- */ - -/* Built-in drivers */ -extern Imaging -ImagingOpenPPM(const char *filename); -extern int -ImagingSavePPM(Imaging im, const char *filename); - -/* Codecs */ -typedef struct ImagingCodecStateInstance *ImagingCodecState; -typedef int (*ImagingCodec)( - Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); - -extern int -ImagingBcnDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); -extern int -ImagingBitDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); -extern int -ImagingEpsEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); -extern int -ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); -extern int -ImagingGifDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); -extern int -ImagingGifEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); -extern int -ImagingHexDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); -#ifdef HAVE_LIBJPEG -extern int -ImagingJpegDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); -extern int -ImagingJpegDecodeCleanup(ImagingCodecState state); -extern int -ImagingJpegUseJCSExtensions(void); - -extern int -ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); -#endif -#ifdef HAVE_OPENJPEG -extern int -ImagingJpeg2KDecode( - Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); -extern int -ImagingJpeg2KDecodeCleanup(ImagingCodecState state); -extern int -ImagingJpeg2KEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); -extern int -ImagingJpeg2KEncodeCleanup(ImagingCodecState state); -#endif -#ifdef HAVE_LIBTIFF -extern int -ImagingLibTiffDecode( - Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); -extern int -ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); -#endif -#ifdef HAVE_LIBMPEG -extern int -ImagingMpegDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); -#endif -extern int -ImagingMspDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); -extern int -ImagingPackbitsDecode( - Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); -extern int -ImagingPcdDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); -extern int -ImagingPcxDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); -extern int -ImagingPcxEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); -extern int -ImagingRawDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); -extern int -ImagingRawEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); -extern int -ImagingSgiRleDecode( - Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); -extern int -ImagingSunRleDecode( - Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); -extern int -ImagingTgaRleDecode( - Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); -extern int -ImagingTgaRleEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); -extern int -ImagingXbmDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); -extern int -ImagingXbmEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); -#ifdef HAVE_LIBZ -extern int -ImagingZipDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); -extern int -ImagingZipDecodeCleanup(ImagingCodecState state); -extern int -ImagingZipEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); -extern int -ImagingZipEncodeCleanup(ImagingCodecState state); -#endif - -typedef void (*ImagingShuffler)(UINT8 *out, const UINT8 *in, int pixels); - -/* Public shufflers */ -extern void -ImagingPackBGR(UINT8 *out, const UINT8 *in, int pixels); -extern void -ImagingUnpackYCC(UINT8 *out, const UINT8 *in, int pixels); -extern void -ImagingUnpackYCCA(UINT8 *out, const UINT8 *in, int pixels); - -extern void -ImagingConvertRGB2YCbCr(UINT8 *out, const UINT8 *in, int pixels); -extern void -ImagingConvertYCbCr2RGB(UINT8 *out, const UINT8 *in, int pixels); - -extern ImagingShuffler -ImagingFindUnpacker(const char *mode, const char *rawmode, int *bits_out); -extern ImagingShuffler -ImagingFindPacker(const char *mode, const char *rawmode, int *bits_out); - -struct ImagingCodecStateInstance { - int count; - int state; - int errcode; - int x, y; - int ystep; - int xsize, ysize, xoff, yoff; - ImagingShuffler shuffle; - int bits, bytes; - UINT8 *buffer; - void *context; - PyObject *fd; -}; - -/* Codec read/write python fd */ -extern Py_ssize_t -_imaging_read_pyFd(PyObject *fd, char *dest, Py_ssize_t bytes); -extern Py_ssize_t -_imaging_write_pyFd(PyObject *fd, char *src, Py_ssize_t bytes); -extern int -_imaging_seek_pyFd(PyObject *fd, Py_ssize_t offset, int whence); -extern Py_ssize_t -_imaging_tell_pyFd(PyObject *fd); - -/* Errcodes */ -#define IMAGING_CODEC_END 1 -#define IMAGING_CODEC_OVERRUN -1 -#define IMAGING_CODEC_BROKEN -2 -#define IMAGING_CODEC_UNKNOWN -3 -#define IMAGING_CODEC_CONFIG -8 -#define IMAGING_CODEC_MEMORY -9 - -#include "ImagingUtils.h" -extern UINT8 *clip8_lookups; - -#if defined(__cplusplus) -} -#endif +/* + * The Python Imaging Library + * $Id$ + * + * declarations for the imaging core library + * + * Copyright (c) 1997-2005 by Secret Labs AB + * Copyright (c) 1995-2005 by Fredrik Lundh + * + * See the README file for information on usage and redistribution. + */ + +#include "ImPlatform.h" + +#if defined(__cplusplus) +extern "C" { +#endif + +#ifndef M_PI +#define M_PI 3.1415926535897932384626433832795 +#endif + +/* -------------------------------------------------------------------- */ + +/* + * Image data organization: + * + * mode bytes byte order + * ------------------------------- + * 1 1 1 + * L 1 L + * P 1 P + * I 4 I (32-bit integer, native byte order) + * F 4 F (32-bit IEEE float, native byte order) + * RGB 4 R, G, B, - + * RGBA 4 R, G, B, A + * CMYK 4 C, M, Y, K + * YCbCr 4 Y, Cb, Cr, - + * Lab 4 L, a, b, - + * + * experimental modes (incomplete): + * LA 4 L, -, -, A + * PA 4 P, -, -, A + * I;16 2 I (16-bit integer, native byte order) + * + * "P" is an 8-bit palette mode, which should be mapped through the + * palette member to get an output image. Check palette->mode to + * find the corresponding "real" mode. + * + * For information on how to access Imaging objects from your own C + * extensions, see http://www.effbot.org/zone/pil-extending.htm + */ + +/* Handles */ + +typedef struct ImagingMemoryInstance *Imaging; + +typedef struct ImagingAccessInstance *ImagingAccess; +typedef struct ImagingHistogramInstance *ImagingHistogram; +typedef struct ImagingOutlineInstance *ImagingOutline; +typedef struct ImagingPaletteInstance *ImagingPalette; + +/* handle magics (used with PyCObject). */ +#define IMAGING_MAGIC "PIL Imaging" + +/* pixel types */ +#define IMAGING_TYPE_UINT8 0 +#define IMAGING_TYPE_INT32 1 +#define IMAGING_TYPE_FLOAT32 2 +#define IMAGING_TYPE_SPECIAL 3 /* check mode for details */ + +#define IMAGING_MODE_LENGTH \ + 6 + 1 /* Band names ("1", "L", "P", "RGB", "RGBA", "CMYK", "YCbCr", "BGR;xy") */ + +typedef struct { + char *ptr; + int size; +} ImagingMemoryBlock; + +struct ImagingMemoryInstance { + /* Format */ + char mode[IMAGING_MODE_LENGTH]; /* Band names ("1", "L", "P", "RGB", "RGBA", "CMYK", + "YCbCr", "BGR;xy") */ + int type; /* Data type (IMAGING_TYPE_*) */ + int depth; /* Depth (ignored in this version) */ + int bands; /* Number of bands (1, 2, 3, or 4) */ + int xsize; /* Image dimension. */ + int ysize; + + /* Colour palette (for "P" images only) */ + ImagingPalette palette; + + /* Data pointers */ + UINT8 **image8; /* Set for 8-bit images (pixelsize=1). */ + INT32 **image32; /* Set for 32-bit images (pixelsize=4). */ + + /* Internals */ + char **image; /* Actual raster data. */ + char *block; /* Set if data is allocated in a single block. */ + ImagingMemoryBlock *blocks; /* Memory blocks for pixel storage */ + + int pixelsize; /* Size of a pixel, in bytes (1, 2 or 4) */ + int linesize; /* Size of a line, in bytes (xsize * pixelsize) */ + + /* Virtual methods */ + void (*destroy)(Imaging im); +}; + +#define IMAGING_PIXEL_1(im, x, y) ((im)->image8[(y)][(x)]) +#define IMAGING_PIXEL_L(im, x, y) ((im)->image8[(y)][(x)]) +#define IMAGING_PIXEL_LA(im, x, y) ((im)->image[(y)][(x) * 4]) +#define IMAGING_PIXEL_P(im, x, y) ((im)->image8[(y)][(x)]) +#define IMAGING_PIXEL_PA(im, x, y) ((im)->image[(y)][(x) * 4]) +#define IMAGING_PIXEL_I(im, x, y) ((im)->image32[(y)][(x)]) +#define IMAGING_PIXEL_F(im, x, y) (((FLOAT32 *)(im)->image32[y])[x]) +#define IMAGING_PIXEL_RGB(im, x, y) ((im)->image[(y)][(x) * 4]) +#define IMAGING_PIXEL_RGBA(im, x, y) ((im)->image[(y)][(x) * 4]) +#define IMAGING_PIXEL_CMYK(im, x, y) ((im)->image[(y)][(x) * 4]) +#define IMAGING_PIXEL_YCbCr(im, x, y) ((im)->image[(y)][(x) * 4]) + +#define IMAGING_PIXEL_UINT8(im, x, y) ((im)->image8[(y)][(x)]) +#define IMAGING_PIXEL_INT32(im, x, y) ((im)->image32[(y)][(x)]) +#define IMAGING_PIXEL_FLOAT32(im, x, y) (((FLOAT32 *)(im)->image32[y])[x]) + +struct ImagingAccessInstance { + const char *mode; + void (*get_pixel)(Imaging im, int x, int y, void *pixel); + void (*put_pixel)(Imaging im, int x, int y, const void *pixel); +}; + +struct ImagingHistogramInstance { + /* Format */ + char mode[IMAGING_MODE_LENGTH]; /* Band names (of corresponding source image) */ + int bands; /* Number of bands (1, 3, or 4) */ + + /* Data */ + long *histogram; /* Histogram (bands*256 longs) */ +}; + +struct ImagingPaletteInstance { + /* Format */ + char mode[IMAGING_MODE_LENGTH]; /* Band names */ + + /* Data */ + int size; + UINT8 palette[1024]; /* Palette data (same format as image data) */ + + INT16 *cache; /* Palette cache (used for predefined palettes) */ + int keep_cache; /* This palette will be reused; keep cache */ +}; + +typedef struct ImagingMemoryArena { + int alignment; /* Alignment in memory of each line of an image */ + int block_size; /* Preferred block size, bytes */ + int blocks_max; /* Maximum number of cached blocks */ + int blocks_cached; /* Current number of blocks not associated with images */ + ImagingMemoryBlock *blocks_pool; + int stats_new_count; /* Number of new allocated images */ + int stats_allocated_blocks; /* Number of allocated blocks */ + int stats_reused_blocks; /* Number of blocks which were retrieved from a pool */ + int stats_reallocated_blocks; /* Number of blocks which were actually reallocated + after retrieving */ + int stats_freed_blocks; /* Number of freed blocks */ +} *ImagingMemoryArena; + +/* Objects */ +/* ------- */ + +extern struct ImagingMemoryArena ImagingDefaultArena; +extern int +ImagingMemorySetBlocksMax(ImagingMemoryArena arena, int blocks_max); +extern void +ImagingMemoryClearCache(ImagingMemoryArena arena, int new_size); + +extern Imaging +ImagingNew(const char *mode, int xsize, int ysize); +extern Imaging +ImagingNewDirty(const char *mode, int xsize, int ysize); +extern Imaging +ImagingNew2Dirty(const char *mode, Imaging imOut, Imaging imIn); +extern void +ImagingDelete(Imaging im); + +extern Imaging +ImagingNewBlock(const char *mode, int xsize, int ysize); + +extern Imaging +ImagingNewPrologue(const char *mode, int xsize, int ysize); +extern Imaging +ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int structure_size); + +extern void +ImagingCopyPalette(Imaging destination, Imaging source); + +extern void +ImagingHistogramDelete(ImagingHistogram histogram); + +extern void +ImagingAccessInit(void); +extern ImagingAccess +ImagingAccessNew(Imaging im); +extern void +_ImagingAccessDelete(Imaging im, ImagingAccess access); +#define ImagingAccessDelete(im, access) /* nop, for now */ + +extern ImagingPalette +ImagingPaletteNew(const char *mode); +extern ImagingPalette +ImagingPaletteNewBrowser(void); +extern ImagingPalette +ImagingPaletteDuplicate(ImagingPalette palette); +extern void +ImagingPaletteDelete(ImagingPalette palette); + +extern int +ImagingPaletteCachePrepare(ImagingPalette palette); +extern void +ImagingPaletteCacheUpdate(ImagingPalette palette, int r, int g, int b); +extern void +ImagingPaletteCacheDelete(ImagingPalette palette); + +#define ImagingPaletteCache(p, r, g, b) \ + p->cache[(r >> 2) + (g >> 2) * 64 + (b >> 2) * 64 * 64] + +extern Imaging +ImagingQuantize(Imaging im, int colours, int mode, int kmeans); + +/* Threading */ +/* --------- */ + +typedef void *ImagingSectionCookie; + +extern void +ImagingSectionEnter(ImagingSectionCookie *cookie); +extern void +ImagingSectionLeave(ImagingSectionCookie *cookie); + +/* Exceptions */ +/* ---------- */ + +extern void * +ImagingError_OSError(void); +extern void * +ImagingError_MemoryError(void); +extern void * +ImagingError_ModeError(void); /* maps to ValueError by default */ +extern void * +ImagingError_Mismatch(void); /* maps to ValueError by default */ +extern void * +ImagingError_ValueError(const char *message); +extern void +ImagingError_Clear(void); + +/* Transform callbacks */ +/* ------------------- */ + +/* standard transforms */ +#define IMAGING_TRANSFORM_AFFINE 0 +#define IMAGING_TRANSFORM_PERSPECTIVE 2 +#define IMAGING_TRANSFORM_QUAD 3 + +/* standard filters */ +#define IMAGING_TRANSFORM_NEAREST 0 +#define IMAGING_TRANSFORM_BOX 4 +#define IMAGING_TRANSFORM_BILINEAR 2 +#define IMAGING_TRANSFORM_HAMMING 5 +#define IMAGING_TRANSFORM_BICUBIC 3 +#define IMAGING_TRANSFORM_LANCZOS 1 + +typedef int (*ImagingTransformMap)(double *X, double *Y, int x, int y, void *data); +typedef int (*ImagingTransformFilter)(void *out, Imaging im, double x, double y); + +/* Image Manipulation Methods */ +/* -------------------------- */ + +extern Imaging +ImagingAlphaComposite(Imaging imIn1, Imaging imIn2); +extern Imaging +ImagingBlend(Imaging imIn1, Imaging imIn2, float alpha); +extern Imaging +ImagingCopy(Imaging im); +extern Imaging +ImagingConvert(Imaging im, const char *mode, ImagingPalette palette, int dither); +extern Imaging +ImagingConvertInPlace(Imaging im, const char *mode); +extern Imaging +ImagingConvertMatrix(Imaging im, const char *mode, float m[]); +extern Imaging +ImagingConvertTransparent(Imaging im, const char *mode, int r, int g, int b); +extern Imaging +ImagingCrop(Imaging im, int x0, int y0, int x1, int y1); +extern Imaging +ImagingExpand(Imaging im, int x, int y); +extern Imaging +ImagingFill(Imaging im, const void *ink); +extern int +ImagingFill2( + Imaging into, const void *ink, Imaging mask, int x0, int y0, int x1, int y1); +extern Imaging +ImagingFillBand(Imaging im, int band, int color); +extern Imaging +ImagingFillLinearGradient(const char *mode); +extern Imaging +ImagingFillRadialGradient(const char *mode); +extern Imaging +ImagingFilter(Imaging im, int xsize, int ysize, const FLOAT32 *kernel, FLOAT32 offset); +extern Imaging +ImagingFlipLeftRight(Imaging imOut, Imaging imIn); +extern Imaging +ImagingFlipTopBottom(Imaging imOut, Imaging imIn); +extern Imaging +ImagingGaussianBlur( + Imaging imOut, Imaging imIn, float xradius, float yradius, int passes); +extern Imaging +ImagingGetBand(Imaging im, int band); +extern Imaging +ImagingMerge(const char *mode, Imaging bands[4]); +extern int +ImagingSplit(Imaging im, Imaging bands[4]); +extern int +ImagingGetBBox(Imaging im, int bbox[4], int alpha_only); +typedef struct { + int x, y; + INT32 count; + INT32 pixel; +} ImagingColorItem; +extern ImagingColorItem * +ImagingGetColors(Imaging im, int maxcolors, int *colors); +extern int +ImagingGetExtrema(Imaging im, void *extrema); +extern int +ImagingGetProjection(Imaging im, UINT8 *xproj, UINT8 *yproj); +extern ImagingHistogram +ImagingGetHistogram(Imaging im, Imaging mask, void *extrema); +extern Imaging +ImagingModeFilter(Imaging im, int size); +extern Imaging +ImagingNegative(Imaging im); +extern Imaging +ImagingOffset(Imaging im, int xoffset, int yoffset); +extern int +ImagingPaste(Imaging into, Imaging im, Imaging mask, int x0, int y0, int x1, int y1); +extern Imaging +ImagingPoint(Imaging im, const char *tablemode, const void *table); +extern Imaging +ImagingPointTransform(Imaging imIn, double scale, double offset); +extern Imaging +ImagingPutBand(Imaging im, Imaging imIn, int band); +extern Imaging +ImagingRankFilter(Imaging im, int size, int rank); +extern Imaging +ImagingRotate90(Imaging imOut, Imaging imIn); +extern Imaging +ImagingRotate180(Imaging imOut, Imaging imIn); +extern Imaging +ImagingRotate270(Imaging imOut, Imaging imIn); +extern Imaging +ImagingTranspose(Imaging imOut, Imaging imIn); +extern Imaging +ImagingTransverse(Imaging imOut, Imaging imIn); +extern Imaging +ImagingResample(Imaging imIn, int xsize, int ysize, int filter, float box[4]); +extern Imaging +ImagingReduce(Imaging imIn, int xscale, int yscale, int box[4]); +extern Imaging +ImagingTransform( + Imaging imOut, + Imaging imIn, + int method, + int x0, + int y0, + int x1, + int y1, + double a[8], + int filter, + int fill); +extern Imaging +ImagingUnsharpMask(Imaging imOut, Imaging im, float radius, int percent, int threshold); +extern Imaging +ImagingBoxBlur(Imaging imOut, Imaging imIn, float xradius, float yradius, int n); +extern Imaging +ImagingColorLUT3D_linear( + Imaging imOut, + Imaging imIn, + int table_channels, + int size1D, + int size2D, + int size3D, + INT16 *table); + +extern Imaging +ImagingCopy2(Imaging imOut, Imaging imIn); +extern Imaging +ImagingConvert2(Imaging imOut, Imaging imIn); + +/* Channel operations */ +/* any mode, except "F" */ +extern Imaging +ImagingChopLighter(Imaging imIn1, Imaging imIn2); +extern Imaging +ImagingChopDarker(Imaging imIn1, Imaging imIn2); +extern Imaging +ImagingChopDifference(Imaging imIn1, Imaging imIn2); +extern Imaging +ImagingChopMultiply(Imaging imIn1, Imaging imIn2); +extern Imaging +ImagingChopScreen(Imaging imIn1, Imaging imIn2); +extern Imaging +ImagingChopAdd(Imaging imIn1, Imaging imIn2, float scale, int offset); +extern Imaging +ImagingChopSubtract(Imaging imIn1, Imaging imIn2, float scale, int offset); +extern Imaging +ImagingChopAddModulo(Imaging imIn1, Imaging imIn2); +extern Imaging +ImagingChopSubtractModulo(Imaging imIn1, Imaging imIn2); +extern Imaging +ImagingChopSoftLight(Imaging imIn1, Imaging imIn2); +extern Imaging +ImagingChopHardLight(Imaging imIn1, Imaging imIn2); +extern Imaging +ImagingOverlay(Imaging imIn1, Imaging imIn2); + +/* "1" images only */ +extern Imaging +ImagingChopAnd(Imaging imIn1, Imaging imIn2); +extern Imaging +ImagingChopOr(Imaging imIn1, Imaging imIn2); +extern Imaging +ImagingChopXor(Imaging imIn1, Imaging imIn2); + +/* Graphics */ +extern int +ImagingDrawArc( + Imaging im, + int x0, + int y0, + int x1, + int y1, + float start, + float end, + const void *ink, + int width, + int op); +extern int +ImagingDrawBitmap(Imaging im, int x0, int y0, Imaging bitmap, const void *ink, int op); +extern int +ImagingDrawChord( + Imaging im, + int x0, + int y0, + int x1, + int y1, + float start, + float end, + const void *ink, + int fill, + int width, + int op); +extern int +ImagingDrawEllipse( + Imaging im, + int x0, + int y0, + int x1, + int y1, + const void *ink, + int fill, + int width, + int op); +extern int +ImagingDrawLine(Imaging im, int x0, int y0, int x1, int y1, const void *ink, int op); +extern int +ImagingDrawWideLine( + Imaging im, int x0, int y0, int x1, int y1, const void *ink, int width, int op); +extern int +ImagingDrawPieslice( + Imaging im, + int x0, + int y0, + int x1, + int y1, + float start, + float end, + const void *ink, + int fill, + int width, + int op); +extern int +ImagingDrawPoint(Imaging im, int x, int y, const void *ink, int op); +extern int +ImagingDrawPolygon( + Imaging im, int points, int *xy, const void *ink, int fill, int width, int op); +extern int +ImagingDrawRectangle( + Imaging im, + int x0, + int y0, + int x1, + int y1, + const void *ink, + int fill, + int width, + int op); + +/* Level 2 graphics (WORK IN PROGRESS) */ +extern ImagingOutline +ImagingOutlineNew(void); +extern void +ImagingOutlineDelete(ImagingOutline outline); + +extern int +ImagingDrawOutline( + Imaging im, ImagingOutline outline, const void *ink, int fill, int op); + +extern int +ImagingOutlineMove(ImagingOutline outline, float x, float y); +extern int +ImagingOutlineLine(ImagingOutline outline, float x, float y); +extern int +ImagingOutlineCurve( + ImagingOutline outline, float x1, float y1, float x2, float y2, float x3, float y3); +extern int +ImagingOutlineTransform(ImagingOutline outline, double a[6]); + +extern int +ImagingOutlineClose(ImagingOutline outline); + +/* Special effects */ +extern Imaging +ImagingEffectSpread(Imaging imIn, int distance); +extern Imaging +ImagingEffectNoise(int xsize, int ysize, float sigma); +extern Imaging +ImagingEffectMandelbrot(int xsize, int ysize, double extent[4], int quality); + +/* File I/O */ +/* -------- */ + +/* Built-in drivers */ +extern Imaging +ImagingOpenPPM(const char *filename); +extern int +ImagingSavePPM(Imaging im, const char *filename); + +/* Codecs */ +typedef struct ImagingCodecStateInstance *ImagingCodecState; +typedef int (*ImagingCodec)( + Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); + +extern int +ImagingBcnDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); +extern int +ImagingBitDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); +extern int +ImagingEpsEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); +extern int +ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); +extern int +ImagingGifDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); +extern int +ImagingGifEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); +extern int +ImagingHexDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); +#ifdef HAVE_LIBJPEG +extern int +ImagingJpegDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); +extern int +ImagingJpegDecodeCleanup(ImagingCodecState state); +extern int +ImagingJpegUseJCSExtensions(void); + +extern int +ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); +#endif +#ifdef HAVE_OPENJPEG +extern int +ImagingJpeg2KDecode( + Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); +extern int +ImagingJpeg2KDecodeCleanup(ImagingCodecState state); +extern int +ImagingJpeg2KEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); +extern int +ImagingJpeg2KEncodeCleanup(ImagingCodecState state); +#endif +#ifdef HAVE_LIBTIFF +extern int +ImagingLibTiffDecode( + Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); +extern int +ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); +#endif +#ifdef HAVE_LIBMPEG +extern int +ImagingMpegDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); +#endif +extern int +ImagingMspDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); +extern int +ImagingPackbitsDecode( + Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); +extern int +ImagingPcdDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); +extern int +ImagingPcxDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); +extern int +ImagingPcxEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); +extern int +ImagingRawDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); +extern int +ImagingRawEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); +extern int +ImagingSgiRleDecode( + Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); +extern int +ImagingSunRleDecode( + Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); +extern int +ImagingTgaRleDecode( + Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); +extern int +ImagingTgaRleEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); +extern int +ImagingXbmDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); +extern int +ImagingXbmEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); +#ifdef HAVE_LIBZ +extern int +ImagingZipDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); +extern int +ImagingZipDecodeCleanup(ImagingCodecState state); +extern int +ImagingZipEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); +extern int +ImagingZipEncodeCleanup(ImagingCodecState state); +#endif + +typedef void (*ImagingShuffler)(UINT8 *out, const UINT8 *in, int pixels); + +/* Public shufflers */ +extern void +ImagingPackBGR(UINT8 *out, const UINT8 *in, int pixels); +extern void +ImagingUnpackYCC(UINT8 *out, const UINT8 *in, int pixels); +extern void +ImagingUnpackYCCA(UINT8 *out, const UINT8 *in, int pixels); + +extern void +ImagingConvertRGB2YCbCr(UINT8 *out, const UINT8 *in, int pixels); +extern void +ImagingConvertYCbCr2RGB(UINT8 *out, const UINT8 *in, int pixels); + +extern ImagingShuffler +ImagingFindUnpacker(const char *mode, const char *rawmode, int *bits_out); +extern ImagingShuffler +ImagingFindPacker(const char *mode, const char *rawmode, int *bits_out); + +struct ImagingCodecStateInstance { + int count; + int state; + int errcode; + int x, y; + int ystep; + int xsize, ysize, xoff, yoff; + ImagingShuffler shuffle; + int bits, bytes; + UINT8 *buffer; + void *context; + PyObject *fd; +}; + +/* Codec read/write python fd */ +extern Py_ssize_t +_imaging_read_pyFd(PyObject *fd, char *dest, Py_ssize_t bytes); +extern Py_ssize_t +_imaging_write_pyFd(PyObject *fd, char *src, Py_ssize_t bytes); +extern int +_imaging_seek_pyFd(PyObject *fd, Py_ssize_t offset, int whence); +extern Py_ssize_t +_imaging_tell_pyFd(PyObject *fd); + +/* Errcodes */ +#define IMAGING_CODEC_END 1 +#define IMAGING_CODEC_OVERRUN -1 +#define IMAGING_CODEC_BROKEN -2 +#define IMAGING_CODEC_UNKNOWN -3 +#define IMAGING_CODEC_CONFIG -8 +#define IMAGING_CODEC_MEMORY -9 + +#include "ImagingUtils.h" +extern UINT8 *clip8_lookups; + +#if defined(__cplusplus) +} +#endif diff --git a/src/libImaging/ImagingUtils.h b/src/libImaging/ImagingUtils.h index 714458ad0..4d11027d9 100644 --- a/src/libImaging/ImagingUtils.h +++ b/src/libImaging/ImagingUtils.h @@ -1,42 +1,42 @@ -#ifdef WORDS_BIGENDIAN -#define MAKE_UINT32(u0, u1, u2, u3) \ - ((UINT32)(u3) | ((UINT32)(u2) << 8) | ((UINT32)(u1) << 16) | ((UINT32)(u0) << 24)) -#define MASK_UINT32_CHANNEL_0 0xff000000 -#define MASK_UINT32_CHANNEL_1 0x00ff0000 -#define MASK_UINT32_CHANNEL_2 0x0000ff00 -#define MASK_UINT32_CHANNEL_3 0x000000ff -#else -#define MAKE_UINT32(u0, u1, u2, u3) \ - ((UINT32)(u0) | ((UINT32)(u1) << 8) | ((UINT32)(u2) << 16) | ((UINT32)(u3) << 24)) -#define MASK_UINT32_CHANNEL_0 0x000000ff -#define MASK_UINT32_CHANNEL_1 0x0000ff00 -#define MASK_UINT32_CHANNEL_2 0x00ff0000 -#define MASK_UINT32_CHANNEL_3 0xff000000 -#endif - -#define SHIFTFORDIV255(a) ((((a) >> 8) + a) >> 8) - -/* like (a * b + 127) / 255), but much faster on most platforms */ -#define MULDIV255(a, b, tmp) (tmp = (a) * (b) + 128, SHIFTFORDIV255(tmp)) - -#define DIV255(a, tmp) (tmp = (a) + 128, SHIFTFORDIV255(tmp)) - -#define BLEND(mask, in1, in2, tmp1) DIV255(in1 * (255 - mask) + in2 * mask, tmp1) - -#define PREBLEND(mask, in1, in2, tmp1) (MULDIV255(in1, (255 - mask), tmp1) + in2) - -#define CLIP8(v) ((v) <= 0 ? 0 : (v) < 256 ? (v) : 255) - -/* This is to work around a bug in GCC prior 4.9 in 64 bit mode. - GCC generates code with partial dependency which is 3 times slower. - See: https://stackoverflow.com/a/26588074/253146 */ -#if defined(__x86_64__) && defined(__SSE__) && !defined(__NO_INLINE__) && \ - !defined(__clang__) && defined(GCC_VERSION) && (GCC_VERSION < 40900) -static float __attribute__((always_inline)) inline _i2f(int v) { - float x; - __asm__("xorps %0, %0; cvtsi2ss %1, %0" : "=x"(x) : "r"(v)); - return x; -} -#else -static float inline _i2f(int v) { return (float)v; } -#endif +#ifdef WORDS_BIGENDIAN +#define MAKE_UINT32(u0, u1, u2, u3) \ + ((UINT32)(u3) | ((UINT32)(u2) << 8) | ((UINT32)(u1) << 16) | ((UINT32)(u0) << 24)) +#define MASK_UINT32_CHANNEL_0 0xff000000 +#define MASK_UINT32_CHANNEL_1 0x00ff0000 +#define MASK_UINT32_CHANNEL_2 0x0000ff00 +#define MASK_UINT32_CHANNEL_3 0x000000ff +#else +#define MAKE_UINT32(u0, u1, u2, u3) \ + ((UINT32)(u0) | ((UINT32)(u1) << 8) | ((UINT32)(u2) << 16) | ((UINT32)(u3) << 24)) +#define MASK_UINT32_CHANNEL_0 0x000000ff +#define MASK_UINT32_CHANNEL_1 0x0000ff00 +#define MASK_UINT32_CHANNEL_2 0x00ff0000 +#define MASK_UINT32_CHANNEL_3 0xff000000 +#endif + +#define SHIFTFORDIV255(a) ((((a) >> 8) + a) >> 8) + +/* like (a * b + 127) / 255), but much faster on most platforms */ +#define MULDIV255(a, b, tmp) (tmp = (a) * (b) + 128, SHIFTFORDIV255(tmp)) + +#define DIV255(a, tmp) (tmp = (a) + 128, SHIFTFORDIV255(tmp)) + +#define BLEND(mask, in1, in2, tmp1) DIV255(in1 * (255 - mask) + in2 * mask, tmp1) + +#define PREBLEND(mask, in1, in2, tmp1) (MULDIV255(in1, (255 - mask), tmp1) + in2) + +#define CLIP8(v) ((v) <= 0 ? 0 : (v) < 256 ? (v) : 255) + +/* This is to work around a bug in GCC prior 4.9 in 64 bit mode. + GCC generates code with partial dependency which is 3 times slower. + See: https://stackoverflow.com/a/26588074/253146 */ +#if defined(__x86_64__) && defined(__SSE__) && !defined(__NO_INLINE__) && \ + !defined(__clang__) && defined(GCC_VERSION) && (GCC_VERSION < 40900) +static float __attribute__((always_inline)) inline _i2f(int v) { + float x; + __asm__("xorps %0, %0; cvtsi2ss %1, %0" : "=x"(x) : "r"(v)); + return x; +} +#else +static float inline _i2f(int v) { return (float)v; } +#endif diff --git a/src/libImaging/Jpeg.h b/src/libImaging/Jpeg.h index 7cdba9022..fa04e5591 100644 --- a/src/libImaging/Jpeg.h +++ b/src/libImaging/Jpeg.h @@ -1,123 +1,123 @@ -/* - * The Python Imaging Library. - * $Id$ - * - * declarations for the IJG JPEG codec interface. - * - * Copyright (c) 1995-2001 by Secret Labs AB - * Copyright (c) 1995-1996 by Fredrik Lundh - */ - -#include "jpeglib.h" - -#include - -typedef struct { - struct jpeg_error_mgr pub; /* "public" fields */ - jmp_buf setjmp_buffer; /* for return to caller */ -} JPEGERROR; - -/* -------------------------------------------------------------------- */ -/* Decoder */ - -typedef struct { - struct jpeg_source_mgr pub; - int skip; -} JPEGSOURCE; - -typedef struct { - /* CONFIGURATION */ - - /* Jpeg file mode (empty if not known) */ - char jpegmode[8 + 1]; - - /* Converter output mode (input to the shuffler). If empty, - convert conversions are disabled */ - char rawmode[8 + 1]; - - /* If set, trade quality for speed */ - int draft; - - /* Scale factor (1, 2, 4, 8) */ - int scale; - - /* PRIVATE CONTEXT (set by decoder) */ - - struct jpeg_decompress_struct cinfo; - - JPEGERROR error; - - JPEGSOURCE source; - -} JPEGSTATE; - -/* -------------------------------------------------------------------- */ -/* Encoder */ - -typedef struct { - struct jpeg_destination_mgr pub; - /* might add something some other day */ -} JPEGDESTINATION; - -typedef struct { - /* CONFIGURATION */ - - /* Quality (0-100, -1 means default) */ - int quality; - - /* Progressive mode */ - int progressive; - - /* Smoothing factor (1-100, 0 means none) */ - int smooth; - - /* Optimize Huffman tables (slow) */ - int optimize; - - /* Disable automatic conversion of RGB images to YCbCr if non-zero */ - int keep_rgb; - - /* Stream type (0=full, 1=tables only, 2=image only) */ - int streamtype; - - /* DPI setting (0=square pixels, otherwise DPI) */ - int xdpi, ydpi; - - /* Chroma Subsampling (-1=default, 0=none, 1=medium, 2=high) */ - int subsampling; - - /* Restart marker interval, in MCU blocks or MCU rows, or 0 for none */ - unsigned int restart_marker_blocks; - unsigned int restart_marker_rows; - - /* Converter input mode (input to the shuffler) */ - char rawmode[8 + 1]; - - /* Custom quantization tables () */ - unsigned int *qtables; - - /* in factors of DCTSIZE2 */ - int qtablesLen; - - /* Comment */ - char *comment; - size_t comment_size; - - /* Extra data (to be injected after header) */ - char *extra; - int extra_size; - - /* PRIVATE CONTEXT (set by encoder) */ - - struct jpeg_compress_struct cinfo; - - JPEGERROR error; - - JPEGDESTINATION destination; - - int extra_offset; - - size_t rawExifLen; /* EXIF data length */ - char *rawExif; /* EXIF buffer pointer */ - -} JPEGENCODERSTATE; +/* + * The Python Imaging Library. + * $Id$ + * + * declarations for the IJG JPEG codec interface. + * + * Copyright (c) 1995-2001 by Secret Labs AB + * Copyright (c) 1995-1996 by Fredrik Lundh + */ + +#include "jpeglib.h" + +#include + +typedef struct { + struct jpeg_error_mgr pub; /* "public" fields */ + jmp_buf setjmp_buffer; /* for return to caller */ +} JPEGERROR; + +/* -------------------------------------------------------------------- */ +/* Decoder */ + +typedef struct { + struct jpeg_source_mgr pub; + int skip; +} JPEGSOURCE; + +typedef struct { + /* CONFIGURATION */ + + /* Jpeg file mode (empty if not known) */ + char jpegmode[8 + 1]; + + /* Converter output mode (input to the shuffler). If empty, + convert conversions are disabled */ + char rawmode[8 + 1]; + + /* If set, trade quality for speed */ + int draft; + + /* Scale factor (1, 2, 4, 8) */ + int scale; + + /* PRIVATE CONTEXT (set by decoder) */ + + struct jpeg_decompress_struct cinfo; + + JPEGERROR error; + + JPEGSOURCE source; + +} JPEGSTATE; + +/* -------------------------------------------------------------------- */ +/* Encoder */ + +typedef struct { + struct jpeg_destination_mgr pub; + /* might add something some other day */ +} JPEGDESTINATION; + +typedef struct { + /* CONFIGURATION */ + + /* Quality (0-100, -1 means default) */ + int quality; + + /* Progressive mode */ + int progressive; + + /* Smoothing factor (1-100, 0 means none) */ + int smooth; + + /* Optimize Huffman tables (slow) */ + int optimize; + + /* Disable automatic conversion of RGB images to YCbCr if non-zero */ + int keep_rgb; + + /* Stream type (0=full, 1=tables only, 2=image only) */ + int streamtype; + + /* DPI setting (0=square pixels, otherwise DPI) */ + int xdpi, ydpi; + + /* Chroma Subsampling (-1=default, 0=none, 1=medium, 2=high) */ + int subsampling; + + /* Restart marker interval, in MCU blocks or MCU rows, or 0 for none */ + unsigned int restart_marker_blocks; + unsigned int restart_marker_rows; + + /* Converter input mode (input to the shuffler) */ + char rawmode[8 + 1]; + + /* Custom quantization tables () */ + unsigned int *qtables; + + /* in factors of DCTSIZE2 */ + int qtablesLen; + + /* Comment */ + char *comment; + size_t comment_size; + + /* Extra data (to be injected after header) */ + char *extra; + int extra_size; + + /* PRIVATE CONTEXT (set by encoder) */ + + struct jpeg_compress_struct cinfo; + + JPEGERROR error; + + JPEGDESTINATION destination; + + int extra_offset; + + size_t rawExifLen; /* EXIF data length */ + char *rawExif; /* EXIF buffer pointer */ + +} JPEGENCODERSTATE; diff --git a/src/libImaging/Jpeg2K.h b/src/libImaging/Jpeg2K.h index e8d92f7b6..20a8d092b 100644 --- a/src/libImaging/Jpeg2K.h +++ b/src/libImaging/Jpeg2K.h @@ -1,113 +1,113 @@ -/* - * The Python Imaging Library - * $Id$ - * - * declarations for the OpenJPEG codec interface. - * - * Copyright (c) 2014 by Coriolis Systems Limited - * Copyright (c) 2014 by Alastair Houghton - */ - -#include - -/* 1MB for now */ -#define BUFFER_SIZE OPJ_J2K_STREAM_CHUNK_SIZE - -/* -------------------------------------------------------------------- */ -/* Decoder */ -/* -------------------------------------------------------------------- */ - -typedef struct { - /* CONFIGURATION */ - - /* File descriptor, if available; otherwise, -1 */ - int fd; - - /* File pointer, when opened */ - FILE *pfile; - - /* Length of data, if available; otherwise, -1 */ - off_t length; - - /* Specify the desired format */ - OPJ_CODEC_FORMAT format; - - /* Set to divide image resolution by 2**reduce. */ - int reduce; - - /* Set to limit the number of quality layers to decode (0 = all layers) */ - int layers; - - /* PRIVATE CONTEXT (set by decoder) */ - const char *error_msg; - -} JPEG2KDECODESTATE; - -/* -------------------------------------------------------------------- */ -/* Encoder */ -/* -------------------------------------------------------------------- */ - -typedef struct { - /* CONFIGURATION */ - - /* File descriptor, if available; otherwise, -1 */ - int fd; - - /* File pointer, when opened */ - FILE *pfile; - - /* Specify the desired format */ - OPJ_CODEC_FORMAT format; - - /* Image offset */ - int offset_x, offset_y; - - /* Tile information */ - int tile_offset_x, tile_offset_y; - int tile_size_x, tile_size_y; - - /* Quality layers (a sequence of numbers giving *either* rates or dB) */ - int quality_is_in_db; - PyObject *quality_layers; - - /* Number of resolutions (DWT decompositions + 1 */ - int num_resolutions; - - /* Code block size */ - int cblk_width, cblk_height; - - /* Precinct size */ - int precinct_width, precinct_height; - - /* Compression style */ - int irreversible; - - /* Set multiple component transformation */ - char mct; - - /* Signed */ - int sgnd; - - /* Progression order (LRCP/RLCP/RPCL/PCRL/CPRL) */ - OPJ_PROG_ORDER progression; - - /* Cinema mode */ - OPJ_CINEMA_MODE cinema_mode; - - /* PRIVATE CONTEXT (set by decoder) */ - const char *error_msg; - - /* Custom comment */ - char *comment; - - /* Include PLT marker segment */ - int plt; - -} JPEG2KENCODESTATE; - -/* - * Local Variables: - * c-basic-offset: 4 - * End: - * - */ +/* + * The Python Imaging Library + * $Id$ + * + * declarations for the OpenJPEG codec interface. + * + * Copyright (c) 2014 by Coriolis Systems Limited + * Copyright (c) 2014 by Alastair Houghton + */ + +#include + +/* 1MB for now */ +#define BUFFER_SIZE OPJ_J2K_STREAM_CHUNK_SIZE + +/* -------------------------------------------------------------------- */ +/* Decoder */ +/* -------------------------------------------------------------------- */ + +typedef struct { + /* CONFIGURATION */ + + /* File descriptor, if available; otherwise, -1 */ + int fd; + + /* File pointer, when opened */ + FILE *pfile; + + /* Length of data, if available; otherwise, -1 */ + off_t length; + + /* Specify the desired format */ + OPJ_CODEC_FORMAT format; + + /* Set to divide image resolution by 2**reduce. */ + int reduce; + + /* Set to limit the number of quality layers to decode (0 = all layers) */ + int layers; + + /* PRIVATE CONTEXT (set by decoder) */ + const char *error_msg; + +} JPEG2KDECODESTATE; + +/* -------------------------------------------------------------------- */ +/* Encoder */ +/* -------------------------------------------------------------------- */ + +typedef struct { + /* CONFIGURATION */ + + /* File descriptor, if available; otherwise, -1 */ + int fd; + + /* File pointer, when opened */ + FILE *pfile; + + /* Specify the desired format */ + OPJ_CODEC_FORMAT format; + + /* Image offset */ + int offset_x, offset_y; + + /* Tile information */ + int tile_offset_x, tile_offset_y; + int tile_size_x, tile_size_y; + + /* Quality layers (a sequence of numbers giving *either* rates or dB) */ + int quality_is_in_db; + PyObject *quality_layers; + + /* Number of resolutions (DWT decompositions + 1 */ + int num_resolutions; + + /* Code block size */ + int cblk_width, cblk_height; + + /* Precinct size */ + int precinct_width, precinct_height; + + /* Compression style */ + int irreversible; + + /* Set multiple component transformation */ + char mct; + + /* Signed */ + int sgnd; + + /* Progression order (LRCP/RLCP/RPCL/PCRL/CPRL) */ + OPJ_PROG_ORDER progression; + + /* Cinema mode */ + OPJ_CINEMA_MODE cinema_mode; + + /* PRIVATE CONTEXT (set by decoder) */ + const char *error_msg; + + /* Custom comment */ + char *comment; + + /* Include PLT marker segment */ + int plt; + +} JPEG2KENCODESTATE; + +/* + * Local Variables: + * c-basic-offset: 4 + * End: + * + */ diff --git a/src/libImaging/Jpeg2KDecode.c b/src/libImaging/Jpeg2KDecode.c index dd066c10b..f93d58c5d 100644 --- a/src/libImaging/Jpeg2KDecode.c +++ b/src/libImaging/Jpeg2KDecode.c @@ -1,1002 +1,1002 @@ -/* - * The Python Imaging Library. - * $Id$ - * - * decoder for JPEG2000 image data. - * - * history: - * 2014-03-12 ajh Created - * - * Copyright (c) 2014 Coriolis Systems Limited - * Copyright (c) 2014 Alastair Houghton - * - * See the README file for details on usage and redistribution. - */ - -#include "Imaging.h" - -#ifdef HAVE_OPENJPEG - -#include -#include "Jpeg2K.h" - -typedef struct { - OPJ_UINT32 tile_index; - OPJ_UINT32 data_size; - OPJ_INT32 x0, y0, x1, y1; - OPJ_UINT32 nb_comps; -} JPEG2KTILEINFO; - -/* -------------------------------------------------------------------- */ -/* Error handler */ -/* -------------------------------------------------------------------- */ - -static void -j2k_error(const char *msg, void *client_data) { - JPEG2KDECODESTATE *state = (JPEG2KDECODESTATE *)client_data; - free((void *)state->error_msg); - state->error_msg = strdup(msg); -} - -/* -------------------------------------------------------------------- */ -/* Buffer input stream */ -/* -------------------------------------------------------------------- */ - -static OPJ_SIZE_T -j2k_read(void *p_buffer, OPJ_SIZE_T p_nb_bytes, void *p_user_data) { - ImagingCodecState state = (ImagingCodecState)p_user_data; - - size_t len = _imaging_read_pyFd(state->fd, p_buffer, p_nb_bytes); - - return len ? len : (OPJ_SIZE_T)-1; -} - -static OPJ_OFF_T -j2k_skip(OPJ_OFF_T p_nb_bytes, void *p_user_data) { - off_t pos; - ImagingCodecState state = (ImagingCodecState)p_user_data; - - _imaging_seek_pyFd(state->fd, p_nb_bytes, SEEK_CUR); - pos = _imaging_tell_pyFd(state->fd); - - return pos ? pos : (OPJ_OFF_T)-1; -} - -/* -------------------------------------------------------------------- */ -/* Unpackers */ -/* -------------------------------------------------------------------- */ - -typedef void (*j2k_unpacker_t)( - opj_image_t *in, const JPEG2KTILEINFO *tileInfo, const UINT8 *data, Imaging im); - -struct j2k_decode_unpacker { - const char *mode; - OPJ_COLOR_SPACE color_space; - unsigned components; - /* bool indicating if unpacker supports subsampling */ - int subsampling; - j2k_unpacker_t unpacker; -}; - -static inline unsigned -j2ku_shift(unsigned x, int n) { - if (n < 0) { - return x >> -n; - } else { - return x << n; - } -} - -static void -j2ku_gray_l( - opj_image_t *in, - const JPEG2KTILEINFO *tileinfo, - const UINT8 *tiledata, - Imaging im) { - unsigned x0 = tileinfo->x0 - in->x0, y0 = tileinfo->y0 - in->y0; - unsigned w = tileinfo->x1 - tileinfo->x0; - unsigned h = tileinfo->y1 - tileinfo->y0; - - int shift = 8 - in->comps[0].prec; - int offset = in->comps[0].sgnd ? 1 << (in->comps[0].prec - 1) : 0; - int csiz = (in->comps[0].prec + 7) >> 3; - - unsigned x, y; - - if (csiz == 3) { - csiz = 4; - } - - if (shift < 0) { - offset += 1 << (-shift - 1); - } - - /* csiz*h*w + offset = tileinfo.datasize */ - switch (csiz) { - case 1: - for (y = 0; y < h; ++y) { - const UINT8 *data = &tiledata[y * w]; - UINT8 *row = (UINT8 *)im->image[y0 + y] + x0; - for (x = 0; x < w; ++x) { - *row++ = j2ku_shift(offset + *data++, shift); - } - } - break; - case 2: - for (y = 0; y < h; ++y) { - const UINT16 *data = (const UINT16 *)&tiledata[2 * y * w]; - UINT8 *row = (UINT8 *)im->image[y0 + y] + x0; - for (x = 0; x < w; ++x) { - *row++ = j2ku_shift(offset + *data++, shift); - } - } - break; - case 4: - for (y = 0; y < h; ++y) { - const UINT32 *data = (const UINT32 *)&tiledata[4 * y * w]; - UINT8 *row = (UINT8 *)im->image[y0 + y] + x0; - for (x = 0; x < w; ++x) { - *row++ = j2ku_shift(offset + *data++, shift); - } - } - break; - } -} - -static void -j2ku_gray_i( - opj_image_t *in, - const JPEG2KTILEINFO *tileinfo, - const UINT8 *tiledata, - Imaging im) { - unsigned x0 = tileinfo->x0 - in->x0, y0 = tileinfo->y0 - in->y0; - unsigned w = tileinfo->x1 - tileinfo->x0; - unsigned h = tileinfo->y1 - tileinfo->y0; - - int shift = 16 - in->comps[0].prec; - int offset = in->comps[0].sgnd ? 1 << (in->comps[0].prec - 1) : 0; - int csiz = (in->comps[0].prec + 7) >> 3; - - unsigned x, y; - - if (csiz == 3) { - csiz = 4; - } - - if (shift < 0) { - offset += 1 << (-shift - 1); - } - - switch (csiz) { - case 1: - for (y = 0; y < h; ++y) { - const UINT8 *data = &tiledata[y * w]; - UINT16 *row = (UINT16 *)im->image[y0 + y] + x0; - for (x = 0; x < w; ++x) { - *row++ = j2ku_shift(offset + *data++, shift); - } - } - break; - case 2: - for (y = 0; y < h; ++y) { - const UINT16 *data = (const UINT16 *)&tiledata[2 * y * w]; - UINT16 *row = (UINT16 *)im->image[y0 + y] + x0; - for (x = 0; x < w; ++x) { - UINT16 pixel = j2ku_shift(offset + *data++, shift); -#ifdef WORDS_BIGENDIAN - pixel = (pixel >> 8) | (pixel << 8); -#endif - *row++ = pixel; - } - } - break; - case 4: - for (y = 0; y < h; ++y) { - const UINT32 *data = (const UINT32 *)&tiledata[4 * y * w]; - UINT16 *row = (UINT16 *)im->image[y0 + y] + x0; - for (x = 0; x < w; ++x) { - *row++ = j2ku_shift(offset + *data++, shift); - } - } - break; - } -} - -static void -j2ku_gray_rgb( - opj_image_t *in, - const JPEG2KTILEINFO *tileinfo, - const UINT8 *tiledata, - Imaging im) { - unsigned x0 = tileinfo->x0 - in->x0, y0 = tileinfo->y0 - in->y0; - unsigned w = tileinfo->x1 - tileinfo->x0; - unsigned h = tileinfo->y1 - tileinfo->y0; - - int shift = 8 - in->comps[0].prec; - int offset = in->comps[0].sgnd ? 1 << (in->comps[0].prec - 1) : 0; - int csiz = (in->comps[0].prec + 7) >> 3; - - unsigned x, y; - - if (shift < 0) { - offset += 1 << (-shift - 1); - } - - if (csiz == 3) { - csiz = 4; - } - - switch (csiz) { - case 1: - for (y = 0; y < h; ++y) { - const UINT8 *data = &tiledata[y * w]; - UINT8 *row = (UINT8 *)im->image[y0 + y] + x0; - for (x = 0; x < w; ++x) { - UINT8 byte = j2ku_shift(offset + *data++, shift); - row[0] = row[1] = row[2] = byte; - row[3] = 0xff; - row += 4; - } - } - break; - case 2: - for (y = 0; y < h; ++y) { - const UINT16 *data = (UINT16 *)&tiledata[2 * y * w]; - UINT8 *row = (UINT8 *)im->image[y0 + y] + x0; - for (x = 0; x < w; ++x) { - UINT8 byte = j2ku_shift(offset + *data++, shift); - row[0] = row[1] = row[2] = byte; - row[3] = 0xff; - row += 4; - } - } - break; - case 4: - for (y = 0; y < h; ++y) { - const UINT32 *data = (UINT32 *)&tiledata[4 * y * w]; - UINT8 *row = (UINT8 *)im->image[y0 + y] + x0; - for (x = 0; x < w; ++x) { - UINT8 byte = j2ku_shift(offset + *data++, shift); - row[0] = row[1] = row[2] = byte; - row[3] = 0xff; - row += 4; - } - } - break; - } -} - -static void -j2ku_graya_la( - opj_image_t *in, - const JPEG2KTILEINFO *tileinfo, - const UINT8 *tiledata, - Imaging im) { - unsigned x0 = tileinfo->x0 - in->x0, y0 = tileinfo->y0 - in->y0; - unsigned w = tileinfo->x1 - tileinfo->x0; - unsigned h = tileinfo->y1 - tileinfo->y0; - - int shift = 8 - in->comps[0].prec; - int offset = in->comps[0].sgnd ? 1 << (in->comps[0].prec - 1) : 0; - int csiz = (in->comps[0].prec + 7) >> 3; - int ashift = 8 - in->comps[1].prec; - int aoffset = in->comps[1].sgnd ? 1 << (in->comps[1].prec - 1) : 0; - int acsiz = (in->comps[1].prec + 7) >> 3; - const UINT8 *atiledata; - - unsigned x, y; - - if (csiz == 3) { - csiz = 4; - } - if (acsiz == 3) { - acsiz = 4; - } - - if (shift < 0) { - offset += 1 << (-shift - 1); - } - if (ashift < 0) { - aoffset += 1 << (-ashift - 1); - } - - atiledata = tiledata + csiz * w * h; - - for (y = 0; y < h; ++y) { - const UINT8 *data = &tiledata[csiz * y * w]; - const UINT8 *adata = &atiledata[acsiz * y * w]; - UINT8 *row = (UINT8 *)im->image[y0 + y] + x0 * 4; - for (x = 0; x < w; ++x) { - UINT32 word = 0, aword = 0, byte; - - switch (csiz) { - case 1: - word = *data++; - break; - case 2: - word = *(const UINT16 *)data; - data += 2; - break; - case 4: - word = *(const UINT32 *)data; - data += 4; - break; - } - - switch (acsiz) { - case 1: - aword = *adata++; - break; - case 2: - aword = *(const UINT16 *)adata; - adata += 2; - break; - case 4: - aword = *(const UINT32 *)adata; - adata += 4; - break; - } - - byte = j2ku_shift(offset + word, shift); - row[0] = row[1] = row[2] = byte; - row[3] = j2ku_shift(aoffset + aword, ashift); - row += 4; - } - } -} - -static void -j2ku_srgb_rgb( - opj_image_t *in, - const JPEG2KTILEINFO *tileinfo, - const UINT8 *tiledata, - Imaging im) { - unsigned x0 = tileinfo->x0 - in->x0, y0 = tileinfo->y0 - in->y0; - unsigned w = tileinfo->x1 - tileinfo->x0; - unsigned h = tileinfo->y1 - tileinfo->y0; - - int shifts[3], offsets[3], csiz[3]; - unsigned dx[3], dy[3]; - const UINT8 *cdata[3]; - const UINT8 *cptr = tiledata; - unsigned n, x, y; - - for (n = 0; n < 3; ++n) { - cdata[n] = cptr; - shifts[n] = 8 - in->comps[n].prec; - offsets[n] = in->comps[n].sgnd ? 1 << (in->comps[n].prec - 1) : 0; - csiz[n] = (in->comps[n].prec + 7) >> 3; - dx[n] = (in->comps[n].dx); - dy[n] = (in->comps[n].dy); - - if (csiz[n] == 3) { - csiz[n] = 4; - } - - if (shifts[n] < 0) { - offsets[n] += 1 << (-shifts[n] - 1); - } - - cptr += csiz[n] * (w / dx[n]) * (h / dy[n]); - } - - for (y = 0; y < h; ++y) { - const UINT8 *data[3]; - UINT8 *row = (UINT8 *)im->image[y0 + y] + x0 * 4; - for (n = 0; n < 3; ++n) { - data[n] = &cdata[n][csiz[n] * (y / dy[n]) * (w / dx[n])]; - } - - for (x = 0; x < w; ++x) { - for (n = 0; n < 3; ++n) { - UINT32 word = 0; - - switch (csiz[n]) { - case 1: - word = data[n][x / dx[n]]; - break; - case 2: - word = ((const UINT16 *)data[n])[x / dx[n]]; - break; - case 4: - word = ((const UINT32 *)data[n])[x / dx[n]]; - break; - } - - row[n] = j2ku_shift(offsets[n] + word, shifts[n]); - } - row[3] = 0xff; - row += 4; - } - } -} - -static void -j2ku_sycc_rgb( - opj_image_t *in, - const JPEG2KTILEINFO *tileinfo, - const UINT8 *tiledata, - Imaging im) { - unsigned x0 = tileinfo->x0 - in->x0, y0 = tileinfo->y0 - in->y0; - unsigned w = tileinfo->x1 - tileinfo->x0; - unsigned h = tileinfo->y1 - tileinfo->y0; - - int shifts[3], offsets[3], csiz[3]; - unsigned dx[3], dy[3]; - const UINT8 *cdata[3]; - const UINT8 *cptr = tiledata; - unsigned n, x, y; - - for (n = 0; n < 3; ++n) { - cdata[n] = cptr; - shifts[n] = 8 - in->comps[n].prec; - offsets[n] = in->comps[n].sgnd ? 1 << (in->comps[n].prec - 1) : 0; - csiz[n] = (in->comps[n].prec + 7) >> 3; - dx[n] = (in->comps[n].dx); - dy[n] = (in->comps[n].dy); - - if (csiz[n] == 3) { - csiz[n] = 4; - } - - if (shifts[n] < 0) { - offsets[n] += 1 << (-shifts[n] - 1); - } - - cptr += csiz[n] * (w / dx[n]) * (h / dy[n]); - } - - for (y = 0; y < h; ++y) { - const UINT8 *data[3]; - UINT8 *row = (UINT8 *)im->image[y0 + y] + x0 * 4; - UINT8 *row_start = row; - for (n = 0; n < 3; ++n) { - data[n] = &cdata[n][csiz[n] * (y / dy[n]) * (w / dx[n])]; - } - - for (x = 0; x < w; ++x) { - for (n = 0; n < 3; ++n) { - UINT32 word = 0; - - switch (csiz[n]) { - case 1: - word = data[n][x / dx[n]]; - break; - case 2: - word = ((const UINT16 *)data[n])[x / dx[n]]; - break; - case 4: - word = ((const UINT32 *)data[n])[x / dx[n]]; - break; - } - - row[n] = j2ku_shift(offsets[n] + word, shifts[n]); - } - row[3] = 0xff; - row += 4; - } - - ImagingConvertYCbCr2RGB(row_start, row_start, w); - } -} - -static void -j2ku_srgba_rgba( - opj_image_t *in, - const JPEG2KTILEINFO *tileinfo, - const UINT8 *tiledata, - Imaging im) { - unsigned x0 = tileinfo->x0 - in->x0, y0 = tileinfo->y0 - in->y0; - unsigned w = tileinfo->x1 - tileinfo->x0; - unsigned h = tileinfo->y1 - tileinfo->y0; - - int shifts[4], offsets[4], csiz[4]; - unsigned dx[4], dy[4]; - const UINT8 *cdata[4]; - const UINT8 *cptr = tiledata; - unsigned n, x, y; - - for (n = 0; n < 4; ++n) { - cdata[n] = cptr; - shifts[n] = 8 - in->comps[n].prec; - offsets[n] = in->comps[n].sgnd ? 1 << (in->comps[n].prec - 1) : 0; - csiz[n] = (in->comps[n].prec + 7) >> 3; - dx[n] = (in->comps[n].dx); - dy[n] = (in->comps[n].dy); - - if (csiz[n] == 3) { - csiz[n] = 4; - } - - if (shifts[n] < 0) { - offsets[n] += 1 << (-shifts[n] - 1); - } - - cptr += csiz[n] * (w / dx[n]) * (h / dy[n]); - } - - for (y = 0; y < h; ++y) { - const UINT8 *data[4]; - UINT8 *row = (UINT8 *)im->image[y0 + y] + x0 * 4; - for (n = 0; n < 4; ++n) { - data[n] = &cdata[n][csiz[n] * (y / dy[n]) * (w / dx[n])]; - } - - for (x = 0; x < w; ++x) { - for (n = 0; n < 4; ++n) { - UINT32 word = 0; - - switch (csiz[n]) { - case 1: - word = data[n][x / dx[n]]; - break; - case 2: - word = ((const UINT16 *)data[n])[x / dx[n]]; - break; - case 4: - word = ((const UINT32 *)data[n])[x / dx[n]]; - break; - } - - row[n] = j2ku_shift(offsets[n] + word, shifts[n]); - } - row += 4; - } - } -} - -static void -j2ku_sycca_rgba( - opj_image_t *in, - const JPEG2KTILEINFO *tileinfo, - const UINT8 *tiledata, - Imaging im) { - unsigned x0 = tileinfo->x0 - in->x0, y0 = tileinfo->y0 - in->y0; - unsigned w = tileinfo->x1 - tileinfo->x0; - unsigned h = tileinfo->y1 - tileinfo->y0; - - int shifts[4], offsets[4], csiz[4]; - unsigned dx[4], dy[4]; - const UINT8 *cdata[4]; - const UINT8 *cptr = tiledata; - unsigned n, x, y; - - for (n = 0; n < 4; ++n) { - cdata[n] = cptr; - shifts[n] = 8 - in->comps[n].prec; - offsets[n] = in->comps[n].sgnd ? 1 << (in->comps[n].prec - 1) : 0; - csiz[n] = (in->comps[n].prec + 7) >> 3; - dx[n] = (in->comps[n].dx); - dy[n] = (in->comps[n].dy); - - if (csiz[n] == 3) { - csiz[n] = 4; - } - - if (shifts[n] < 0) { - offsets[n] += 1 << (-shifts[n] - 1); - } - - cptr += csiz[n] * (w / dx[n]) * (h / dy[n]); - } - - for (y = 0; y < h; ++y) { - const UINT8 *data[4]; - UINT8 *row = (UINT8 *)im->image[y0 + y] + x0 * 4; - UINT8 *row_start = row; - for (n = 0; n < 4; ++n) { - data[n] = &cdata[n][csiz[n] * (y / dy[n]) * (w / dx[n])]; - } - - for (x = 0; x < w; ++x) { - for (n = 0; n < 4; ++n) { - UINT32 word = 0; - - switch (csiz[n]) { - case 1: - word = data[n][x / dx[n]]; - break; - case 2: - word = ((const UINT16 *)data[n])[x / dx[n]]; - break; - case 4: - word = ((const UINT32 *)data[n])[x / dx[n]]; - break; - } - - row[n] = j2ku_shift(offsets[n] + word, shifts[n]); - } - row += 4; - } - - ImagingConvertYCbCr2RGB(row_start, row_start, w); - } -} - -static const struct j2k_decode_unpacker j2k_unpackers[] = { - {"L", OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_l}, - {"P", OPJ_CLRSPC_SRGB, 1, 0, j2ku_gray_l}, - {"PA", OPJ_CLRSPC_SRGB, 2, 0, j2ku_graya_la}, - {"I;16", OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_i}, - {"I;16B", OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_i}, - {"LA", OPJ_CLRSPC_GRAY, 2, 0, j2ku_graya_la}, - {"RGB", OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_rgb}, - {"RGB", OPJ_CLRSPC_GRAY, 2, 0, j2ku_gray_rgb}, - {"RGB", OPJ_CLRSPC_SRGB, 3, 1, j2ku_srgb_rgb}, - {"RGB", OPJ_CLRSPC_SYCC, 3, 1, j2ku_sycc_rgb}, - {"RGB", OPJ_CLRSPC_SRGB, 4, 1, j2ku_srgb_rgb}, - {"RGB", OPJ_CLRSPC_SYCC, 4, 1, j2ku_sycc_rgb}, - {"RGBA", OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_rgb}, - {"RGBA", OPJ_CLRSPC_GRAY, 2, 0, j2ku_graya_la}, - {"RGBA", OPJ_CLRSPC_SRGB, 3, 1, j2ku_srgb_rgb}, - {"RGBA", OPJ_CLRSPC_SYCC, 3, 1, j2ku_sycc_rgb}, - {"RGBA", OPJ_CLRSPC_SRGB, 4, 1, j2ku_srgba_rgba}, - {"RGBA", OPJ_CLRSPC_SYCC, 4, 1, j2ku_sycca_rgba}, - {"CMYK", OPJ_CLRSPC_CMYK, 4, 1, j2ku_srgba_rgba}, -}; - -/* -------------------------------------------------------------------- */ -/* Decoder */ -/* -------------------------------------------------------------------- */ - -enum { - J2K_STATE_START = 0, - J2K_STATE_DECODING = 1, - J2K_STATE_DONE = 2, - J2K_STATE_FAILED = 3, -}; - -static int -j2k_decode_entry(Imaging im, ImagingCodecState state) { - JPEG2KDECODESTATE *context = (JPEG2KDECODESTATE *)state->context; - opj_stream_t *stream = NULL; - opj_image_t *image = NULL; - opj_codec_t *codec = NULL; - opj_dparameters_t params; - OPJ_COLOR_SPACE color_space; - j2k_unpacker_t unpack = NULL; - size_t buffer_size = 0, tile_bytes = 0; - unsigned n, tile_height, tile_width; - int subsampling; - int total_component_width = 0; - - stream = opj_stream_create(BUFFER_SIZE, OPJ_TRUE); - - if (!stream) { - state->errcode = IMAGING_CODEC_BROKEN; - state->state = J2K_STATE_FAILED; - goto quick_exit; - } - - opj_stream_set_read_function(stream, j2k_read); - opj_stream_set_skip_function(stream, j2k_skip); - - /* OpenJPEG 2.0 doesn't have OPJ_VERSION_MAJOR */ -#ifndef OPJ_VERSION_MAJOR - opj_stream_set_user_data(stream, state); -#else - opj_stream_set_user_data(stream, state, NULL); - - /* Hack: if we don't know the length, the largest file we can - possibly support is 4GB. We can't go larger than this, because - OpenJPEG truncates this value for the final box in the file, and - the box lengths in OpenJPEG are currently 32 bit. */ - if (context->length < 0) { - opj_stream_set_user_data_length(stream, 0xffffffff); - } else { - opj_stream_set_user_data_length(stream, context->length); - } -#endif - - /* Setup decompression context */ - context->error_msg = NULL; - - opj_set_default_decoder_parameters(¶ms); - params.cp_reduce = context->reduce; - params.cp_layer = context->layers; - - codec = opj_create_decompress(context->format); - - if (!codec) { - state->errcode = IMAGING_CODEC_BROKEN; - state->state = J2K_STATE_FAILED; - goto quick_exit; - } - - opj_set_error_handler(codec, j2k_error, context); - opj_setup_decoder(codec, ¶ms); - - if (!opj_read_header(stream, codec, &image)) { - state->errcode = IMAGING_CODEC_BROKEN; - state->state = J2K_STATE_FAILED; - goto quick_exit; - } - - /* Check that this image is something we can handle */ - if (image->numcomps < 1 || image->numcomps > 4 || - image->color_space == OPJ_CLRSPC_UNKNOWN) { - state->errcode = IMAGING_CODEC_BROKEN; - state->state = J2K_STATE_FAILED; - goto quick_exit; - } - - /* - * Find first component with subsampling. - * - * This is a heuristic to determine the colorspace if unspecified. - */ - subsampling = -1; - for (n = 0; n < image->numcomps; ++n) { - if (image->comps[n].dx != 1 || image->comps[n].dy != 1) { - subsampling = n; - break; - } - } - - /* - Colorspace Number of components PIL mode - ------------------------------------------------------ - sRGB 3 RGB - sRGB 4 RGBA - gray 1 L or I - gray 2 LA - YCC 3 YCbCr - - - If colorspace is unspecified, we assume: - - Number of components Subsampling Colorspace - ------------------------------------------------------- - 1 Any gray - 2 Any gray (+ alpha) - 3 -1, 0 sRGB - 3 1, 2 YCbCr - 4 -1, 0, 3 sRGB (+ alpha) - 4 1, 2 YCbCr (+ alpha) - - */ - - /* Find the correct unpacker */ - color_space = image->color_space; - - if (color_space == OPJ_CLRSPC_UNSPECIFIED) { - switch (image->numcomps) { - case 1: - case 2: - color_space = OPJ_CLRSPC_GRAY; - break; - case 3: - case 4: - switch (subsampling) { - case -1: - case 0: - case 3: - color_space = OPJ_CLRSPC_SRGB; - break; - case 1: - case 2: - color_space = OPJ_CLRSPC_SYCC; - break; - } - break; - } - } - - for (n = 0; n < sizeof(j2k_unpackers) / sizeof(j2k_unpackers[0]); ++n) { - if (color_space == j2k_unpackers[n].color_space && - image->numcomps == j2k_unpackers[n].components && - (j2k_unpackers[n].subsampling || (subsampling == -1)) && - strcmp(im->mode, j2k_unpackers[n].mode) == 0) { - unpack = j2k_unpackers[n].unpacker; - break; - } - } - - if (!unpack) { - state->errcode = IMAGING_CODEC_BROKEN; - state->state = J2K_STATE_FAILED; - goto quick_exit; - } - - /* Decode the image tile-by-tile; this means we only need use as much - memory as is required for one tile's worth of components. */ - for (;;) { - JPEG2KTILEINFO tile_info; - OPJ_BOOL should_continue; - unsigned correction = (1 << params.cp_reduce) - 1; - - if (!opj_read_tile_header( - codec, - stream, - &tile_info.tile_index, - &tile_info.data_size, - &tile_info.x0, - &tile_info.y0, - &tile_info.x1, - &tile_info.y1, - &tile_info.nb_comps, - &should_continue)) { - state->errcode = IMAGING_CODEC_BROKEN; - state->state = J2K_STATE_FAILED; - goto quick_exit; - } - - if (!should_continue) { - break; - } - - /* Adjust the tile co-ordinates based on the reduction (OpenJPEG - doesn't do this for us) */ - tile_info.x0 = (tile_info.x0 + correction) >> context->reduce; - tile_info.y0 = (tile_info.y0 + correction) >> context->reduce; - tile_info.x1 = (tile_info.x1 + correction) >> context->reduce; - tile_info.y1 = (tile_info.y1 + correction) >> context->reduce; - - /* Check the tile bounds; if the tile is outside the image area, - or if it has a negative width or height (i.e. the coordinates are - swapped), bail. */ - if (tile_info.x0 >= tile_info.x1 || tile_info.y0 >= tile_info.y1 || - tile_info.x0 < 0 || tile_info.y0 < 0 || - (OPJ_UINT32)tile_info.x0 < image->x0 || - (OPJ_UINT32)tile_info.y0 < image->y0 || - (OPJ_INT32)(tile_info.x1 - image->x0) > im->xsize || - (OPJ_INT32)(tile_info.y1 - image->y0) > im->ysize) { - state->errcode = IMAGING_CODEC_BROKEN; - state->state = J2K_STATE_FAILED; - goto quick_exit; - } - - if (tile_info.nb_comps != image->numcomps) { - state->errcode = IMAGING_CODEC_BROKEN; - state->state = J2K_STATE_FAILED; - goto quick_exit; - } - - /* Sometimes the tile_info.datasize we get back from openjpeg - is less than sum(comp_bytes)*w*h, and we overflow in the - shuffle stage */ - - tile_width = tile_info.x1 - tile_info.x0; - tile_height = tile_info.y1 - tile_info.y0; - - /* Total component width = sum (component_width) e.g, it's - legal for an la file to have a 1 byte width for l, and 4 for - a, and then a malicious file could have a smaller tile_bytes - */ - - for (n = 0; n < tile_info.nb_comps; n++) { - // see csize /acsize calcs - int csize = (image->comps[n].prec + 7) >> 3; - csize = (csize == 3) ? 4 : csize; - total_component_width += csize; - } - if ((tile_width > UINT_MAX / total_component_width) || - (tile_height > UINT_MAX / total_component_width) || - (tile_width > UINT_MAX / (tile_height * total_component_width)) || - (tile_height > UINT_MAX / (tile_width * total_component_width))) { - state->errcode = IMAGING_CODEC_BROKEN; - state->state = J2K_STATE_FAILED; - goto quick_exit; - } - - tile_bytes = tile_width * tile_height * total_component_width; - - if (tile_bytes > tile_info.data_size) { - tile_info.data_size = tile_bytes; - } - - if (buffer_size < tile_info.data_size) { - /* malloc check ok, overflow and tile size sanity check above */ - UINT8 *new = realloc(state->buffer, tile_info.data_size); - if (!new) { - state->errcode = IMAGING_CODEC_MEMORY; - state->state = J2K_STATE_FAILED; - goto quick_exit; - } - /* Undefined behavior, sometimes decode_tile_data doesn't - fill the buffer and we do things with it later, leading - to valgrind errors. */ - memset(new, 0, tile_info.data_size); - state->buffer = new; - buffer_size = tile_info.data_size; - } - - if (!opj_decode_tile_data( - codec, - tile_info.tile_index, - (OPJ_BYTE *)state->buffer, - tile_info.data_size, - stream)) { - state->errcode = IMAGING_CODEC_BROKEN; - state->state = J2K_STATE_FAILED; - goto quick_exit; - } - - unpack(image, &tile_info, state->buffer, im); - } - - if (!opj_end_decompress(codec, stream)) { - state->errcode = IMAGING_CODEC_BROKEN; - state->state = J2K_STATE_FAILED; - goto quick_exit; - } - - state->state = J2K_STATE_DONE; - state->errcode = IMAGING_CODEC_END; - - if (context->pfile) { - if (fclose(context->pfile)) { - context->pfile = NULL; - } - } - -quick_exit: - if (codec) { - opj_destroy_codec(codec); - } - if (image) { - opj_image_destroy(image); - } - if (stream) { - opj_stream_destroy(stream); - } - - return -1; -} - -int -ImagingJpeg2KDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) { - if (bytes) { - state->errcode = IMAGING_CODEC_BROKEN; - state->state = J2K_STATE_FAILED; - return -1; - } - - if (state->state == J2K_STATE_DONE || state->state == J2K_STATE_FAILED) { - return -1; - } - - if (state->state == J2K_STATE_START) { - state->state = J2K_STATE_DECODING; - - return j2k_decode_entry(im, state); - } - - if (state->state == J2K_STATE_DECODING) { - state->errcode = IMAGING_CODEC_BROKEN; - state->state = J2K_STATE_FAILED; - return -1; - } - return -1; -} - -/* -------------------------------------------------------------------- */ -/* Cleanup */ -/* -------------------------------------------------------------------- */ - -int -ImagingJpeg2KDecodeCleanup(ImagingCodecState state) { - JPEG2KDECODESTATE *context = (JPEG2KDECODESTATE *)state->context; - - if (context->error_msg) { - free((void *)context->error_msg); - } - - context->error_msg = NULL; - - return -1; -} - -const char * -ImagingJpeg2KVersion(void) { - return opj_version(); -} - -#endif /* HAVE_OPENJPEG */ - -/* - * Local Variables: - * c-basic-offset: 4 - * End: - * - */ +/* + * The Python Imaging Library. + * $Id$ + * + * decoder for JPEG2000 image data. + * + * history: + * 2014-03-12 ajh Created + * + * Copyright (c) 2014 Coriolis Systems Limited + * Copyright (c) 2014 Alastair Houghton + * + * See the README file for details on usage and redistribution. + */ + +#include "Imaging.h" + +#ifdef HAVE_OPENJPEG + +#include +#include "Jpeg2K.h" + +typedef struct { + OPJ_UINT32 tile_index; + OPJ_UINT32 data_size; + OPJ_INT32 x0, y0, x1, y1; + OPJ_UINT32 nb_comps; +} JPEG2KTILEINFO; + +/* -------------------------------------------------------------------- */ +/* Error handler */ +/* -------------------------------------------------------------------- */ + +static void +j2k_error(const char *msg, void *client_data) { + JPEG2KDECODESTATE *state = (JPEG2KDECODESTATE *)client_data; + free((void *)state->error_msg); + state->error_msg = strdup(msg); +} + +/* -------------------------------------------------------------------- */ +/* Buffer input stream */ +/* -------------------------------------------------------------------- */ + +static OPJ_SIZE_T +j2k_read(void *p_buffer, OPJ_SIZE_T p_nb_bytes, void *p_user_data) { + ImagingCodecState state = (ImagingCodecState)p_user_data; + + size_t len = _imaging_read_pyFd(state->fd, p_buffer, p_nb_bytes); + + return len ? len : (OPJ_SIZE_T)-1; +} + +static OPJ_OFF_T +j2k_skip(OPJ_OFF_T p_nb_bytes, void *p_user_data) { + off_t pos; + ImagingCodecState state = (ImagingCodecState)p_user_data; + + _imaging_seek_pyFd(state->fd, p_nb_bytes, SEEK_CUR); + pos = _imaging_tell_pyFd(state->fd); + + return pos ? pos : (OPJ_OFF_T)-1; +} + +/* -------------------------------------------------------------------- */ +/* Unpackers */ +/* -------------------------------------------------------------------- */ + +typedef void (*j2k_unpacker_t)( + opj_image_t *in, const JPEG2KTILEINFO *tileInfo, const UINT8 *data, Imaging im); + +struct j2k_decode_unpacker { + const char *mode; + OPJ_COLOR_SPACE color_space; + unsigned components; + /* bool indicating if unpacker supports subsampling */ + int subsampling; + j2k_unpacker_t unpacker; +}; + +static inline unsigned +j2ku_shift(unsigned x, int n) { + if (n < 0) { + return x >> -n; + } else { + return x << n; + } +} + +static void +j2ku_gray_l( + opj_image_t *in, + const JPEG2KTILEINFO *tileinfo, + const UINT8 *tiledata, + Imaging im) { + unsigned x0 = tileinfo->x0 - in->x0, y0 = tileinfo->y0 - in->y0; + unsigned w = tileinfo->x1 - tileinfo->x0; + unsigned h = tileinfo->y1 - tileinfo->y0; + + int shift = 8 - in->comps[0].prec; + int offset = in->comps[0].sgnd ? 1 << (in->comps[0].prec - 1) : 0; + int csiz = (in->comps[0].prec + 7) >> 3; + + unsigned x, y; + + if (csiz == 3) { + csiz = 4; + } + + if (shift < 0) { + offset += 1 << (-shift - 1); + } + + /* csiz*h*w + offset = tileinfo.datasize */ + switch (csiz) { + case 1: + for (y = 0; y < h; ++y) { + const UINT8 *data = &tiledata[y * w]; + UINT8 *row = (UINT8 *)im->image[y0 + y] + x0; + for (x = 0; x < w; ++x) { + *row++ = j2ku_shift(offset + *data++, shift); + } + } + break; + case 2: + for (y = 0; y < h; ++y) { + const UINT16 *data = (const UINT16 *)&tiledata[2 * y * w]; + UINT8 *row = (UINT8 *)im->image[y0 + y] + x0; + for (x = 0; x < w; ++x) { + *row++ = j2ku_shift(offset + *data++, shift); + } + } + break; + case 4: + for (y = 0; y < h; ++y) { + const UINT32 *data = (const UINT32 *)&tiledata[4 * y * w]; + UINT8 *row = (UINT8 *)im->image[y0 + y] + x0; + for (x = 0; x < w; ++x) { + *row++ = j2ku_shift(offset + *data++, shift); + } + } + break; + } +} + +static void +j2ku_gray_i( + opj_image_t *in, + const JPEG2KTILEINFO *tileinfo, + const UINT8 *tiledata, + Imaging im) { + unsigned x0 = tileinfo->x0 - in->x0, y0 = tileinfo->y0 - in->y0; + unsigned w = tileinfo->x1 - tileinfo->x0; + unsigned h = tileinfo->y1 - tileinfo->y0; + + int shift = 16 - in->comps[0].prec; + int offset = in->comps[0].sgnd ? 1 << (in->comps[0].prec - 1) : 0; + int csiz = (in->comps[0].prec + 7) >> 3; + + unsigned x, y; + + if (csiz == 3) { + csiz = 4; + } + + if (shift < 0) { + offset += 1 << (-shift - 1); + } + + switch (csiz) { + case 1: + for (y = 0; y < h; ++y) { + const UINT8 *data = &tiledata[y * w]; + UINT16 *row = (UINT16 *)im->image[y0 + y] + x0; + for (x = 0; x < w; ++x) { + *row++ = j2ku_shift(offset + *data++, shift); + } + } + break; + case 2: + for (y = 0; y < h; ++y) { + const UINT16 *data = (const UINT16 *)&tiledata[2 * y * w]; + UINT16 *row = (UINT16 *)im->image[y0 + y] + x0; + for (x = 0; x < w; ++x) { + UINT16 pixel = j2ku_shift(offset + *data++, shift); +#ifdef WORDS_BIGENDIAN + pixel = (pixel >> 8) | (pixel << 8); +#endif + *row++ = pixel; + } + } + break; + case 4: + for (y = 0; y < h; ++y) { + const UINT32 *data = (const UINT32 *)&tiledata[4 * y * w]; + UINT16 *row = (UINT16 *)im->image[y0 + y] + x0; + for (x = 0; x < w; ++x) { + *row++ = j2ku_shift(offset + *data++, shift); + } + } + break; + } +} + +static void +j2ku_gray_rgb( + opj_image_t *in, + const JPEG2KTILEINFO *tileinfo, + const UINT8 *tiledata, + Imaging im) { + unsigned x0 = tileinfo->x0 - in->x0, y0 = tileinfo->y0 - in->y0; + unsigned w = tileinfo->x1 - tileinfo->x0; + unsigned h = tileinfo->y1 - tileinfo->y0; + + int shift = 8 - in->comps[0].prec; + int offset = in->comps[0].sgnd ? 1 << (in->comps[0].prec - 1) : 0; + int csiz = (in->comps[0].prec + 7) >> 3; + + unsigned x, y; + + if (shift < 0) { + offset += 1 << (-shift - 1); + } + + if (csiz == 3) { + csiz = 4; + } + + switch (csiz) { + case 1: + for (y = 0; y < h; ++y) { + const UINT8 *data = &tiledata[y * w]; + UINT8 *row = (UINT8 *)im->image[y0 + y] + x0; + for (x = 0; x < w; ++x) { + UINT8 byte = j2ku_shift(offset + *data++, shift); + row[0] = row[1] = row[2] = byte; + row[3] = 0xff; + row += 4; + } + } + break; + case 2: + for (y = 0; y < h; ++y) { + const UINT16 *data = (UINT16 *)&tiledata[2 * y * w]; + UINT8 *row = (UINT8 *)im->image[y0 + y] + x0; + for (x = 0; x < w; ++x) { + UINT8 byte = j2ku_shift(offset + *data++, shift); + row[0] = row[1] = row[2] = byte; + row[3] = 0xff; + row += 4; + } + } + break; + case 4: + for (y = 0; y < h; ++y) { + const UINT32 *data = (UINT32 *)&tiledata[4 * y * w]; + UINT8 *row = (UINT8 *)im->image[y0 + y] + x0; + for (x = 0; x < w; ++x) { + UINT8 byte = j2ku_shift(offset + *data++, shift); + row[0] = row[1] = row[2] = byte; + row[3] = 0xff; + row += 4; + } + } + break; + } +} + +static void +j2ku_graya_la( + opj_image_t *in, + const JPEG2KTILEINFO *tileinfo, + const UINT8 *tiledata, + Imaging im) { + unsigned x0 = tileinfo->x0 - in->x0, y0 = tileinfo->y0 - in->y0; + unsigned w = tileinfo->x1 - tileinfo->x0; + unsigned h = tileinfo->y1 - tileinfo->y0; + + int shift = 8 - in->comps[0].prec; + int offset = in->comps[0].sgnd ? 1 << (in->comps[0].prec - 1) : 0; + int csiz = (in->comps[0].prec + 7) >> 3; + int ashift = 8 - in->comps[1].prec; + int aoffset = in->comps[1].sgnd ? 1 << (in->comps[1].prec - 1) : 0; + int acsiz = (in->comps[1].prec + 7) >> 3; + const UINT8 *atiledata; + + unsigned x, y; + + if (csiz == 3) { + csiz = 4; + } + if (acsiz == 3) { + acsiz = 4; + } + + if (shift < 0) { + offset += 1 << (-shift - 1); + } + if (ashift < 0) { + aoffset += 1 << (-ashift - 1); + } + + atiledata = tiledata + csiz * w * h; + + for (y = 0; y < h; ++y) { + const UINT8 *data = &tiledata[csiz * y * w]; + const UINT8 *adata = &atiledata[acsiz * y * w]; + UINT8 *row = (UINT8 *)im->image[y0 + y] + x0 * 4; + for (x = 0; x < w; ++x) { + UINT32 word = 0, aword = 0, byte; + + switch (csiz) { + case 1: + word = *data++; + break; + case 2: + word = *(const UINT16 *)data; + data += 2; + break; + case 4: + word = *(const UINT32 *)data; + data += 4; + break; + } + + switch (acsiz) { + case 1: + aword = *adata++; + break; + case 2: + aword = *(const UINT16 *)adata; + adata += 2; + break; + case 4: + aword = *(const UINT32 *)adata; + adata += 4; + break; + } + + byte = j2ku_shift(offset + word, shift); + row[0] = row[1] = row[2] = byte; + row[3] = j2ku_shift(aoffset + aword, ashift); + row += 4; + } + } +} + +static void +j2ku_srgb_rgb( + opj_image_t *in, + const JPEG2KTILEINFO *tileinfo, + const UINT8 *tiledata, + Imaging im) { + unsigned x0 = tileinfo->x0 - in->x0, y0 = tileinfo->y0 - in->y0; + unsigned w = tileinfo->x1 - tileinfo->x0; + unsigned h = tileinfo->y1 - tileinfo->y0; + + int shifts[3], offsets[3], csiz[3]; + unsigned dx[3], dy[3]; + const UINT8 *cdata[3]; + const UINT8 *cptr = tiledata; + unsigned n, x, y; + + for (n = 0; n < 3; ++n) { + cdata[n] = cptr; + shifts[n] = 8 - in->comps[n].prec; + offsets[n] = in->comps[n].sgnd ? 1 << (in->comps[n].prec - 1) : 0; + csiz[n] = (in->comps[n].prec + 7) >> 3; + dx[n] = (in->comps[n].dx); + dy[n] = (in->comps[n].dy); + + if (csiz[n] == 3) { + csiz[n] = 4; + } + + if (shifts[n] < 0) { + offsets[n] += 1 << (-shifts[n] - 1); + } + + cptr += csiz[n] * (w / dx[n]) * (h / dy[n]); + } + + for (y = 0; y < h; ++y) { + const UINT8 *data[3]; + UINT8 *row = (UINT8 *)im->image[y0 + y] + x0 * 4; + for (n = 0; n < 3; ++n) { + data[n] = &cdata[n][csiz[n] * (y / dy[n]) * (w / dx[n])]; + } + + for (x = 0; x < w; ++x) { + for (n = 0; n < 3; ++n) { + UINT32 word = 0; + + switch (csiz[n]) { + case 1: + word = data[n][x / dx[n]]; + break; + case 2: + word = ((const UINT16 *)data[n])[x / dx[n]]; + break; + case 4: + word = ((const UINT32 *)data[n])[x / dx[n]]; + break; + } + + row[n] = j2ku_shift(offsets[n] + word, shifts[n]); + } + row[3] = 0xff; + row += 4; + } + } +} + +static void +j2ku_sycc_rgb( + opj_image_t *in, + const JPEG2KTILEINFO *tileinfo, + const UINT8 *tiledata, + Imaging im) { + unsigned x0 = tileinfo->x0 - in->x0, y0 = tileinfo->y0 - in->y0; + unsigned w = tileinfo->x1 - tileinfo->x0; + unsigned h = tileinfo->y1 - tileinfo->y0; + + int shifts[3], offsets[3], csiz[3]; + unsigned dx[3], dy[3]; + const UINT8 *cdata[3]; + const UINT8 *cptr = tiledata; + unsigned n, x, y; + + for (n = 0; n < 3; ++n) { + cdata[n] = cptr; + shifts[n] = 8 - in->comps[n].prec; + offsets[n] = in->comps[n].sgnd ? 1 << (in->comps[n].prec - 1) : 0; + csiz[n] = (in->comps[n].prec + 7) >> 3; + dx[n] = (in->comps[n].dx); + dy[n] = (in->comps[n].dy); + + if (csiz[n] == 3) { + csiz[n] = 4; + } + + if (shifts[n] < 0) { + offsets[n] += 1 << (-shifts[n] - 1); + } + + cptr += csiz[n] * (w / dx[n]) * (h / dy[n]); + } + + for (y = 0; y < h; ++y) { + const UINT8 *data[3]; + UINT8 *row = (UINT8 *)im->image[y0 + y] + x0 * 4; + UINT8 *row_start = row; + for (n = 0; n < 3; ++n) { + data[n] = &cdata[n][csiz[n] * (y / dy[n]) * (w / dx[n])]; + } + + for (x = 0; x < w; ++x) { + for (n = 0; n < 3; ++n) { + UINT32 word = 0; + + switch (csiz[n]) { + case 1: + word = data[n][x / dx[n]]; + break; + case 2: + word = ((const UINT16 *)data[n])[x / dx[n]]; + break; + case 4: + word = ((const UINT32 *)data[n])[x / dx[n]]; + break; + } + + row[n] = j2ku_shift(offsets[n] + word, shifts[n]); + } + row[3] = 0xff; + row += 4; + } + + ImagingConvertYCbCr2RGB(row_start, row_start, w); + } +} + +static void +j2ku_srgba_rgba( + opj_image_t *in, + const JPEG2KTILEINFO *tileinfo, + const UINT8 *tiledata, + Imaging im) { + unsigned x0 = tileinfo->x0 - in->x0, y0 = tileinfo->y0 - in->y0; + unsigned w = tileinfo->x1 - tileinfo->x0; + unsigned h = tileinfo->y1 - tileinfo->y0; + + int shifts[4], offsets[4], csiz[4]; + unsigned dx[4], dy[4]; + const UINT8 *cdata[4]; + const UINT8 *cptr = tiledata; + unsigned n, x, y; + + for (n = 0; n < 4; ++n) { + cdata[n] = cptr; + shifts[n] = 8 - in->comps[n].prec; + offsets[n] = in->comps[n].sgnd ? 1 << (in->comps[n].prec - 1) : 0; + csiz[n] = (in->comps[n].prec + 7) >> 3; + dx[n] = (in->comps[n].dx); + dy[n] = (in->comps[n].dy); + + if (csiz[n] == 3) { + csiz[n] = 4; + } + + if (shifts[n] < 0) { + offsets[n] += 1 << (-shifts[n] - 1); + } + + cptr += csiz[n] * (w / dx[n]) * (h / dy[n]); + } + + for (y = 0; y < h; ++y) { + const UINT8 *data[4]; + UINT8 *row = (UINT8 *)im->image[y0 + y] + x0 * 4; + for (n = 0; n < 4; ++n) { + data[n] = &cdata[n][csiz[n] * (y / dy[n]) * (w / dx[n])]; + } + + for (x = 0; x < w; ++x) { + for (n = 0; n < 4; ++n) { + UINT32 word = 0; + + switch (csiz[n]) { + case 1: + word = data[n][x / dx[n]]; + break; + case 2: + word = ((const UINT16 *)data[n])[x / dx[n]]; + break; + case 4: + word = ((const UINT32 *)data[n])[x / dx[n]]; + break; + } + + row[n] = j2ku_shift(offsets[n] + word, shifts[n]); + } + row += 4; + } + } +} + +static void +j2ku_sycca_rgba( + opj_image_t *in, + const JPEG2KTILEINFO *tileinfo, + const UINT8 *tiledata, + Imaging im) { + unsigned x0 = tileinfo->x0 - in->x0, y0 = tileinfo->y0 - in->y0; + unsigned w = tileinfo->x1 - tileinfo->x0; + unsigned h = tileinfo->y1 - tileinfo->y0; + + int shifts[4], offsets[4], csiz[4]; + unsigned dx[4], dy[4]; + const UINT8 *cdata[4]; + const UINT8 *cptr = tiledata; + unsigned n, x, y; + + for (n = 0; n < 4; ++n) { + cdata[n] = cptr; + shifts[n] = 8 - in->comps[n].prec; + offsets[n] = in->comps[n].sgnd ? 1 << (in->comps[n].prec - 1) : 0; + csiz[n] = (in->comps[n].prec + 7) >> 3; + dx[n] = (in->comps[n].dx); + dy[n] = (in->comps[n].dy); + + if (csiz[n] == 3) { + csiz[n] = 4; + } + + if (shifts[n] < 0) { + offsets[n] += 1 << (-shifts[n] - 1); + } + + cptr += csiz[n] * (w / dx[n]) * (h / dy[n]); + } + + for (y = 0; y < h; ++y) { + const UINT8 *data[4]; + UINT8 *row = (UINT8 *)im->image[y0 + y] + x0 * 4; + UINT8 *row_start = row; + for (n = 0; n < 4; ++n) { + data[n] = &cdata[n][csiz[n] * (y / dy[n]) * (w / dx[n])]; + } + + for (x = 0; x < w; ++x) { + for (n = 0; n < 4; ++n) { + UINT32 word = 0; + + switch (csiz[n]) { + case 1: + word = data[n][x / dx[n]]; + break; + case 2: + word = ((const UINT16 *)data[n])[x / dx[n]]; + break; + case 4: + word = ((const UINT32 *)data[n])[x / dx[n]]; + break; + } + + row[n] = j2ku_shift(offsets[n] + word, shifts[n]); + } + row += 4; + } + + ImagingConvertYCbCr2RGB(row_start, row_start, w); + } +} + +static const struct j2k_decode_unpacker j2k_unpackers[] = { + {"L", OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_l}, + {"P", OPJ_CLRSPC_SRGB, 1, 0, j2ku_gray_l}, + {"PA", OPJ_CLRSPC_SRGB, 2, 0, j2ku_graya_la}, + {"I;16", OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_i}, + {"I;16B", OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_i}, + {"LA", OPJ_CLRSPC_GRAY, 2, 0, j2ku_graya_la}, + {"RGB", OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_rgb}, + {"RGB", OPJ_CLRSPC_GRAY, 2, 0, j2ku_gray_rgb}, + {"RGB", OPJ_CLRSPC_SRGB, 3, 1, j2ku_srgb_rgb}, + {"RGB", OPJ_CLRSPC_SYCC, 3, 1, j2ku_sycc_rgb}, + {"RGB", OPJ_CLRSPC_SRGB, 4, 1, j2ku_srgb_rgb}, + {"RGB", OPJ_CLRSPC_SYCC, 4, 1, j2ku_sycc_rgb}, + {"RGBA", OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_rgb}, + {"RGBA", OPJ_CLRSPC_GRAY, 2, 0, j2ku_graya_la}, + {"RGBA", OPJ_CLRSPC_SRGB, 3, 1, j2ku_srgb_rgb}, + {"RGBA", OPJ_CLRSPC_SYCC, 3, 1, j2ku_sycc_rgb}, + {"RGBA", OPJ_CLRSPC_SRGB, 4, 1, j2ku_srgba_rgba}, + {"RGBA", OPJ_CLRSPC_SYCC, 4, 1, j2ku_sycca_rgba}, + {"CMYK", OPJ_CLRSPC_CMYK, 4, 1, j2ku_srgba_rgba}, +}; + +/* -------------------------------------------------------------------- */ +/* Decoder */ +/* -------------------------------------------------------------------- */ + +enum { + J2K_STATE_START = 0, + J2K_STATE_DECODING = 1, + J2K_STATE_DONE = 2, + J2K_STATE_FAILED = 3, +}; + +static int +j2k_decode_entry(Imaging im, ImagingCodecState state) { + JPEG2KDECODESTATE *context = (JPEG2KDECODESTATE *)state->context; + opj_stream_t *stream = NULL; + opj_image_t *image = NULL; + opj_codec_t *codec = NULL; + opj_dparameters_t params; + OPJ_COLOR_SPACE color_space; + j2k_unpacker_t unpack = NULL; + size_t buffer_size = 0, tile_bytes = 0; + unsigned n, tile_height, tile_width; + int subsampling; + int total_component_width = 0; + + stream = opj_stream_create(BUFFER_SIZE, OPJ_TRUE); + + if (!stream) { + state->errcode = IMAGING_CODEC_BROKEN; + state->state = J2K_STATE_FAILED; + goto quick_exit; + } + + opj_stream_set_read_function(stream, j2k_read); + opj_stream_set_skip_function(stream, j2k_skip); + + /* OpenJPEG 2.0 doesn't have OPJ_VERSION_MAJOR */ +#ifndef OPJ_VERSION_MAJOR + opj_stream_set_user_data(stream, state); +#else + opj_stream_set_user_data(stream, state, NULL); + + /* Hack: if we don't know the length, the largest file we can + possibly support is 4GB. We can't go larger than this, because + OpenJPEG truncates this value for the final box in the file, and + the box lengths in OpenJPEG are currently 32 bit. */ + if (context->length < 0) { + opj_stream_set_user_data_length(stream, 0xffffffff); + } else { + opj_stream_set_user_data_length(stream, context->length); + } +#endif + + /* Setup decompression context */ + context->error_msg = NULL; + + opj_set_default_decoder_parameters(¶ms); + params.cp_reduce = context->reduce; + params.cp_layer = context->layers; + + codec = opj_create_decompress(context->format); + + if (!codec) { + state->errcode = IMAGING_CODEC_BROKEN; + state->state = J2K_STATE_FAILED; + goto quick_exit; + } + + opj_set_error_handler(codec, j2k_error, context); + opj_setup_decoder(codec, ¶ms); + + if (!opj_read_header(stream, codec, &image)) { + state->errcode = IMAGING_CODEC_BROKEN; + state->state = J2K_STATE_FAILED; + goto quick_exit; + } + + /* Check that this image is something we can handle */ + if (image->numcomps < 1 || image->numcomps > 4 || + image->color_space == OPJ_CLRSPC_UNKNOWN) { + state->errcode = IMAGING_CODEC_BROKEN; + state->state = J2K_STATE_FAILED; + goto quick_exit; + } + + /* + * Find first component with subsampling. + * + * This is a heuristic to determine the colorspace if unspecified. + */ + subsampling = -1; + for (n = 0; n < image->numcomps; ++n) { + if (image->comps[n].dx != 1 || image->comps[n].dy != 1) { + subsampling = n; + break; + } + } + + /* + Colorspace Number of components PIL mode + ------------------------------------------------------ + sRGB 3 RGB + sRGB 4 RGBA + gray 1 L or I + gray 2 LA + YCC 3 YCbCr + + + If colorspace is unspecified, we assume: + + Number of components Subsampling Colorspace + ------------------------------------------------------- + 1 Any gray + 2 Any gray (+ alpha) + 3 -1, 0 sRGB + 3 1, 2 YCbCr + 4 -1, 0, 3 sRGB (+ alpha) + 4 1, 2 YCbCr (+ alpha) + + */ + + /* Find the correct unpacker */ + color_space = image->color_space; + + if (color_space == OPJ_CLRSPC_UNSPECIFIED) { + switch (image->numcomps) { + case 1: + case 2: + color_space = OPJ_CLRSPC_GRAY; + break; + case 3: + case 4: + switch (subsampling) { + case -1: + case 0: + case 3: + color_space = OPJ_CLRSPC_SRGB; + break; + case 1: + case 2: + color_space = OPJ_CLRSPC_SYCC; + break; + } + break; + } + } + + for (n = 0; n < sizeof(j2k_unpackers) / sizeof(j2k_unpackers[0]); ++n) { + if (color_space == j2k_unpackers[n].color_space && + image->numcomps == j2k_unpackers[n].components && + (j2k_unpackers[n].subsampling || (subsampling == -1)) && + strcmp(im->mode, j2k_unpackers[n].mode) == 0) { + unpack = j2k_unpackers[n].unpacker; + break; + } + } + + if (!unpack) { + state->errcode = IMAGING_CODEC_BROKEN; + state->state = J2K_STATE_FAILED; + goto quick_exit; + } + + /* Decode the image tile-by-tile; this means we only need use as much + memory as is required for one tile's worth of components. */ + for (;;) { + JPEG2KTILEINFO tile_info; + OPJ_BOOL should_continue; + unsigned correction = (1 << params.cp_reduce) - 1; + + if (!opj_read_tile_header( + codec, + stream, + &tile_info.tile_index, + &tile_info.data_size, + &tile_info.x0, + &tile_info.y0, + &tile_info.x1, + &tile_info.y1, + &tile_info.nb_comps, + &should_continue)) { + state->errcode = IMAGING_CODEC_BROKEN; + state->state = J2K_STATE_FAILED; + goto quick_exit; + } + + if (!should_continue) { + break; + } + + /* Adjust the tile co-ordinates based on the reduction (OpenJPEG + doesn't do this for us) */ + tile_info.x0 = (tile_info.x0 + correction) >> context->reduce; + tile_info.y0 = (tile_info.y0 + correction) >> context->reduce; + tile_info.x1 = (tile_info.x1 + correction) >> context->reduce; + tile_info.y1 = (tile_info.y1 + correction) >> context->reduce; + + /* Check the tile bounds; if the tile is outside the image area, + or if it has a negative width or height (i.e. the coordinates are + swapped), bail. */ + if (tile_info.x0 >= tile_info.x1 || tile_info.y0 >= tile_info.y1 || + tile_info.x0 < 0 || tile_info.y0 < 0 || + (OPJ_UINT32)tile_info.x0 < image->x0 || + (OPJ_UINT32)tile_info.y0 < image->y0 || + (OPJ_INT32)(tile_info.x1 - image->x0) > im->xsize || + (OPJ_INT32)(tile_info.y1 - image->y0) > im->ysize) { + state->errcode = IMAGING_CODEC_BROKEN; + state->state = J2K_STATE_FAILED; + goto quick_exit; + } + + if (tile_info.nb_comps != image->numcomps) { + state->errcode = IMAGING_CODEC_BROKEN; + state->state = J2K_STATE_FAILED; + goto quick_exit; + } + + /* Sometimes the tile_info.datasize we get back from openjpeg + is less than sum(comp_bytes)*w*h, and we overflow in the + shuffle stage */ + + tile_width = tile_info.x1 - tile_info.x0; + tile_height = tile_info.y1 - tile_info.y0; + + /* Total component width = sum (component_width) e.g, it's + legal for an la file to have a 1 byte width for l, and 4 for + a, and then a malicious file could have a smaller tile_bytes + */ + + for (n = 0; n < tile_info.nb_comps; n++) { + // see csize /acsize calcs + int csize = (image->comps[n].prec + 7) >> 3; + csize = (csize == 3) ? 4 : csize; + total_component_width += csize; + } + if ((tile_width > UINT_MAX / total_component_width) || + (tile_height > UINT_MAX / total_component_width) || + (tile_width > UINT_MAX / (tile_height * total_component_width)) || + (tile_height > UINT_MAX / (tile_width * total_component_width))) { + state->errcode = IMAGING_CODEC_BROKEN; + state->state = J2K_STATE_FAILED; + goto quick_exit; + } + + tile_bytes = tile_width * tile_height * total_component_width; + + if (tile_bytes > tile_info.data_size) { + tile_info.data_size = tile_bytes; + } + + if (buffer_size < tile_info.data_size) { + /* malloc check ok, overflow and tile size sanity check above */ + UINT8 *new = realloc(state->buffer, tile_info.data_size); + if (!new) { + state->errcode = IMAGING_CODEC_MEMORY; + state->state = J2K_STATE_FAILED; + goto quick_exit; + } + /* Undefined behavior, sometimes decode_tile_data doesn't + fill the buffer and we do things with it later, leading + to valgrind errors. */ + memset(new, 0, tile_info.data_size); + state->buffer = new; + buffer_size = tile_info.data_size; + } + + if (!opj_decode_tile_data( + codec, + tile_info.tile_index, + (OPJ_BYTE *)state->buffer, + tile_info.data_size, + stream)) { + state->errcode = IMAGING_CODEC_BROKEN; + state->state = J2K_STATE_FAILED; + goto quick_exit; + } + + unpack(image, &tile_info, state->buffer, im); + } + + if (!opj_end_decompress(codec, stream)) { + state->errcode = IMAGING_CODEC_BROKEN; + state->state = J2K_STATE_FAILED; + goto quick_exit; + } + + state->state = J2K_STATE_DONE; + state->errcode = IMAGING_CODEC_END; + + if (context->pfile) { + if (fclose(context->pfile)) { + context->pfile = NULL; + } + } + +quick_exit: + if (codec) { + opj_destroy_codec(codec); + } + if (image) { + opj_image_destroy(image); + } + if (stream) { + opj_stream_destroy(stream); + } + + return -1; +} + +int +ImagingJpeg2KDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) { + if (bytes) { + state->errcode = IMAGING_CODEC_BROKEN; + state->state = J2K_STATE_FAILED; + return -1; + } + + if (state->state == J2K_STATE_DONE || state->state == J2K_STATE_FAILED) { + return -1; + } + + if (state->state == J2K_STATE_START) { + state->state = J2K_STATE_DECODING; + + return j2k_decode_entry(im, state); + } + + if (state->state == J2K_STATE_DECODING) { + state->errcode = IMAGING_CODEC_BROKEN; + state->state = J2K_STATE_FAILED; + return -1; + } + return -1; +} + +/* -------------------------------------------------------------------- */ +/* Cleanup */ +/* -------------------------------------------------------------------- */ + +int +ImagingJpeg2KDecodeCleanup(ImagingCodecState state) { + JPEG2KDECODESTATE *context = (JPEG2KDECODESTATE *)state->context; + + if (context->error_msg) { + free((void *)context->error_msg); + } + + context->error_msg = NULL; + + return -1; +} + +const char * +ImagingJpeg2KVersion(void) { + return opj_version(); +} + +#endif /* HAVE_OPENJPEG */ + +/* + * Local Variables: + * c-basic-offset: 4 + * End: + * + */ diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c index 7f1aeaddb..c815c0744 100644 --- a/src/libImaging/Jpeg2KEncode.c +++ b/src/libImaging/Jpeg2KEncode.c @@ -1,659 +1,659 @@ -/* - * The Python Imaging Library. - * $Id$ - * - * decoder for JPEG2000 image data. - * - * history: - * 2014-03-12 ajh Created - * - * Copyright (c) 2014 Coriolis Systems Limited - * Copyright (c) 2014 Alastair Houghton - * - * See the README file for details on usage and redistribution. - */ - -#include "Imaging.h" - -#ifdef HAVE_OPENJPEG - -#include "Jpeg2K.h" - -#define CINEMA_24_CS_LENGTH 1302083 -#define CINEMA_48_CS_LENGTH 651041 -#define COMP_24_CS_MAX_LENGTH 1041666 -#define COMP_48_CS_MAX_LENGTH 520833 - -/* -------------------------------------------------------------------- */ -/* Error handler */ -/* -------------------------------------------------------------------- */ - -static void -j2k_error(const char *msg, void *client_data) { - JPEG2KENCODESTATE *state = (JPEG2KENCODESTATE *)client_data; - free((void *)state->error_msg); - state->error_msg = strdup(msg); -} - -static void -j2k_warn(const char *msg, void *client_data) { - // Null handler -} - -/* -------------------------------------------------------------------- */ -/* Buffer output stream */ -/* -------------------------------------------------------------------- */ - -static OPJ_SIZE_T -j2k_write(void *p_buffer, OPJ_SIZE_T p_nb_bytes, void *p_user_data) { - ImagingCodecState state = (ImagingCodecState)p_user_data; - unsigned int result; - - result = _imaging_write_pyFd(state->fd, p_buffer, p_nb_bytes); - - return result ? result : (OPJ_SIZE_T)-1; -} - -static OPJ_OFF_T -j2k_skip(OPJ_OFF_T p_nb_bytes, void *p_user_data) { - ImagingCodecState state = (ImagingCodecState)p_user_data; - char *buffer; - int result; - - /* Explicitly write zeros */ - buffer = calloc(p_nb_bytes, 1); - if (!buffer) { - return (OPJ_OFF_T)-1; - } - - result = _imaging_write_pyFd(state->fd, buffer, p_nb_bytes); - - free(buffer); - - return result ? result : p_nb_bytes; -} - -static OPJ_BOOL -j2k_seek(OPJ_OFF_T p_nb_bytes, void *p_user_data) { - ImagingCodecState state = (ImagingCodecState)p_user_data; - off_t pos = 0; - - _imaging_seek_pyFd(state->fd, p_nb_bytes, SEEK_SET); - pos = _imaging_tell_pyFd(state->fd); - - return pos == p_nb_bytes; -} - -/* -------------------------------------------------------------------- */ -/* Encoder */ -/* -------------------------------------------------------------------- */ - -typedef void (*j2k_pack_tile_t)( - Imaging im, UINT8 *buf, unsigned x0, unsigned y0, unsigned w, unsigned h); - -static void -j2k_pack_l(Imaging im, UINT8 *buf, unsigned x0, unsigned y0, unsigned w, unsigned h) { - UINT8 *ptr = buf; - unsigned x, y; - for (y = 0; y < h; ++y) { - UINT8 *data = (UINT8 *)(im->image[y + y0] + x0); - for (x = 0; x < w; ++x) { - *ptr++ = *data++; - } - } -} - -static void -j2k_pack_i16(Imaging im, UINT8 *buf, unsigned x0, unsigned y0, unsigned w, unsigned h) { - UINT8 *ptr = buf; - unsigned x, y; - for (y = 0; y < h; ++y) { - UINT8 *data = (UINT8 *)(im->image[y + y0] + x0); - for (x = 0; x < w; ++x) { -#ifdef WORDS_BIGENDIAN - ptr[0] = data[1]; - ptr[1] = data[0]; -#else - ptr[0] = data[0]; - ptr[1] = data[1]; -#endif - ptr += 2; - data += 2; - } - } -} - -static void -j2k_pack_la(Imaging im, UINT8 *buf, unsigned x0, unsigned y0, unsigned w, unsigned h) { - UINT8 *ptr = buf; - UINT8 *ptra = buf + w * h; - unsigned x, y; - for (y = 0; y < h; ++y) { - UINT8 *data = (UINT8 *)(im->image[y + y0] + 4 * x0); - for (x = 0; x < w; ++x) { - *ptr++ = data[0]; - *ptra++ = data[3]; - data += 4; - } - } -} - -static void -j2k_pack_rgb(Imaging im, UINT8 *buf, unsigned x0, unsigned y0, unsigned w, unsigned h) { - UINT8 *pr = buf; - UINT8 *pg = pr + w * h; - UINT8 *pb = pg + w * h; - unsigned x, y; - for (y = 0; y < h; ++y) { - UINT8 *data = (UINT8 *)(im->image[y + y0] + 4 * x0); - for (x = 0; x < w; ++x) { - *pr++ = data[0]; - *pg++ = data[1]; - *pb++ = data[2]; - data += 4; - } - } -} - -static void -j2k_pack_rgba( - Imaging im, UINT8 *buf, unsigned x0, unsigned y0, unsigned w, unsigned h) { - UINT8 *pr = buf; - UINT8 *pg = pr + w * h; - UINT8 *pb = pg + w * h; - UINT8 *pa = pb + w * h; - unsigned x, y; - for (y = 0; y < h; ++y) { - UINT8 *data = (UINT8 *)(im->image[y + y0] + 4 * x0); - for (x = 0; x < w; ++x) { - *pr++ = *data++; - *pg++ = *data++; - *pb++ = *data++; - *pa++ = *data++; - } - } -} - -enum { - J2K_STATE_START = 0, - J2K_STATE_ENCODING = 1, - J2K_STATE_DONE = 2, - J2K_STATE_FAILED = 3, -}; - -static void -j2k_set_cinema_params(Imaging im, int components, opj_cparameters_t *params) { - float rate; - int n; - - /* These settings have been copied from opj_compress in the OpenJPEG - sources. */ - - params->tile_size_on = OPJ_FALSE; - params->cp_tdx = params->cp_tdy = 1; - params->tp_flag = 'C'; - params->tp_on = 1; - params->cp_tx0 = params->cp_ty0 = 0; - params->image_offset_x0 = params->image_offset_y0 = 0; - params->cblockw_init = 32; - params->cblockh_init = 32; - params->csty |= 0x01; - params->prog_order = OPJ_CPRL; - params->roi_compno = -1; - params->subsampling_dx = params->subsampling_dy = 1; - params->irreversible = 1; - - if (params->cp_cinema == OPJ_CINEMA4K_24) { - float max_rate = - ((float)(components * im->xsize * im->ysize * 8) / - (CINEMA_24_CS_LENGTH * 8)); - - params->POC[0].tile = 1; - params->POC[0].resno0 = 0; - params->POC[0].compno0 = 0; - params->POC[0].layno1 = 1; - params->POC[0].resno1 = params->numresolution - 1; - params->POC[0].compno1 = 3; - params->POC[0].prg1 = OPJ_CPRL; - params->POC[1].tile = 1; - params->POC[1].resno0 = 0; - params->POC[1].compno0 = 0; - params->POC[1].layno1 = 1; - params->POC[1].resno1 = params->numresolution - 1; - params->POC[1].compno1 = 3; - params->POC[1].prg1 = OPJ_CPRL; - params->numpocs = 2; - - for (n = 0; n < params->tcp_numlayers; ++n) { - rate = 0; - if (params->tcp_rates[0] == 0) { - params->tcp_rates[n] = max_rate; - } else { - rate = - ((float)(components * im->xsize * im->ysize * 8) / - (params->tcp_rates[n] * 8)); - if (rate > CINEMA_24_CS_LENGTH) { - params->tcp_rates[n] = max_rate; - } - } - } - - params->max_comp_size = COMP_24_CS_MAX_LENGTH; - } else { - float max_rate = - ((float)(components * im->xsize * im->ysize * 8) / - (CINEMA_48_CS_LENGTH * 8)); - - for (n = 0; n < params->tcp_numlayers; ++n) { - rate = 0; - if (params->tcp_rates[0] == 0) { - params->tcp_rates[n] = max_rate; - } else { - rate = - ((float)(components * im->xsize * im->ysize * 8) / - (params->tcp_rates[n] * 8)); - if (rate > CINEMA_48_CS_LENGTH) { - params->tcp_rates[n] = max_rate; - } - } - } - - params->max_comp_size = COMP_48_CS_MAX_LENGTH; - } -} - -static int -j2k_encode_entry(Imaging im, ImagingCodecState state) { - JPEG2KENCODESTATE *context = (JPEG2KENCODESTATE *)state->context; - opj_stream_t *stream = NULL; - opj_image_t *image = NULL; - opj_codec_t *codec = NULL; - opj_cparameters_t params; - unsigned components; - OPJ_COLOR_SPACE color_space; - opj_image_cmptparm_t image_params[4]; - unsigned xsiz, ysiz; - unsigned tile_width, tile_height; - unsigned tiles_x, tiles_y; - unsigned x, y, tile_ndx; - unsigned n; - j2k_pack_tile_t pack; - int ret = -1; - - unsigned prec = 8; - unsigned _overflow_scale_factor; - - stream = opj_stream_create(BUFFER_SIZE, OPJ_FALSE); - - if (!stream) { - state->errcode = IMAGING_CODEC_BROKEN; - state->state = J2K_STATE_FAILED; - goto quick_exit; - } - - opj_stream_set_write_function(stream, j2k_write); - opj_stream_set_skip_function(stream, j2k_skip); - opj_stream_set_seek_function(stream, j2k_seek); - - /* OpenJPEG 2.0 doesn't have OPJ_VERSION_MAJOR */ -#ifndef OPJ_VERSION_MAJOR - opj_stream_set_user_data(stream, state); -#else - opj_stream_set_user_data(stream, state, NULL); -#endif - - /* Setup an opj_image */ - if (strcmp(im->mode, "L") == 0) { - components = 1; - color_space = OPJ_CLRSPC_GRAY; - pack = j2k_pack_l; - } else if (strcmp(im->mode, "I;16") == 0 || strcmp(im->mode, "I;16B") == 0) { - components = 1; - color_space = OPJ_CLRSPC_GRAY; - pack = j2k_pack_i16; - prec = 16; - } else if (strcmp(im->mode, "LA") == 0) { - components = 2; - color_space = OPJ_CLRSPC_GRAY; - pack = j2k_pack_la; - } else if (strcmp(im->mode, "RGB") == 0) { - components = 3; - color_space = OPJ_CLRSPC_SRGB; - pack = j2k_pack_rgb; - } else if (strcmp(im->mode, "YCbCr") == 0) { - components = 3; - color_space = OPJ_CLRSPC_SYCC; - pack = j2k_pack_rgb; - } else if (strcmp(im->mode, "RGBA") == 0) { - components = 4; - color_space = OPJ_CLRSPC_SRGB; - pack = j2k_pack_rgba; - } else { - state->errcode = IMAGING_CODEC_BROKEN; - state->state = J2K_STATE_FAILED; - goto quick_exit; - } - - for (n = 0; n < components; ++n) { - image_params[n].dx = image_params[n].dy = 1; - image_params[n].w = im->xsize; - image_params[n].h = im->ysize; - image_params[n].x0 = image_params[n].y0 = 0; - image_params[n].prec = prec; - image_params[n].sgnd = context->sgnd == 0 ? 0 : 1; - } - - image = opj_image_create(components, image_params, color_space); - if (!image) { - state->errcode = IMAGING_CODEC_BROKEN; - state->state = J2K_STATE_FAILED; - goto quick_exit; - } - - /* Setup compression context */ - context->error_msg = NULL; - - opj_set_default_encoder_parameters(¶ms); - - params.image_offset_x0 = context->offset_x; - params.image_offset_y0 = context->offset_y; - - if (context->tile_size_x && context->tile_size_y) { - params.tile_size_on = OPJ_TRUE; - params.cp_tx0 = context->tile_offset_x; - params.cp_ty0 = context->tile_offset_y; - params.cp_tdx = context->tile_size_x; - params.cp_tdy = context->tile_size_y; - - tile_width = params.cp_tdx; - tile_height = params.cp_tdy; - } else { - params.cp_tx0 = 0; - params.cp_ty0 = 0; - params.cp_tdx = 1; - params.cp_tdy = 1; - - tile_width = im->xsize; - tile_height = im->ysize; - } - - if (context->quality_layers && PySequence_Check(context->quality_layers)) { - Py_ssize_t len = PySequence_Length(context->quality_layers); - Py_ssize_t n; - float *pq; - - if (len > 0) { - if ((size_t)len > sizeof(params.tcp_rates) / sizeof(params.tcp_rates[0])) { - len = sizeof(params.tcp_rates) / sizeof(params.tcp_rates[0]); - } - - params.tcp_numlayers = (int)len; - - if (context->quality_is_in_db) { - params.cp_disto_alloc = params.cp_fixed_alloc = 0; - params.cp_fixed_quality = 1; - pq = params.tcp_distoratio; - } else { - params.cp_disto_alloc = 1; - params.cp_fixed_alloc = params.cp_fixed_quality = 0; - pq = params.tcp_rates; - } - - for (n = 0; n < len; ++n) { - PyObject *obj = PySequence_ITEM(context->quality_layers, n); - pq[n] = PyFloat_AsDouble(obj); - } - } - } else { - params.tcp_numlayers = 1; - params.tcp_rates[0] = 0; - params.cp_disto_alloc = 1; - } - - if (context->num_resolutions) { - params.numresolution = context->num_resolutions; - } - - if (context->cblk_width >= 4 && context->cblk_width <= 1024 && - context->cblk_height >= 4 && context->cblk_height <= 1024 && - context->cblk_width * context->cblk_height <= 4096) { - params.cblockw_init = context->cblk_width; - params.cblockh_init = context->cblk_height; - } - - if (context->precinct_width >= 4 && context->precinct_height >= 4 && - context->precinct_width >= context->cblk_width && - context->precinct_height > context->cblk_height) { - params.prcw_init[0] = context->precinct_width; - params.prch_init[0] = context->precinct_height; - params.res_spec = 1; - params.csty |= 0x01; - } - - params.irreversible = context->irreversible; - if (components == 3) { - params.tcp_mct = context->mct; - } - - if (context->comment) { - params.cp_comment = context->comment; - } - - params.prog_order = context->progression; - - params.cp_cinema = context->cinema_mode; - - switch (params.cp_cinema) { - case OPJ_OFF: - params.cp_rsiz = OPJ_STD_RSIZ; - break; - case OPJ_CINEMA2K_24: - case OPJ_CINEMA2K_48: - params.cp_rsiz = OPJ_CINEMA2K; - if (params.numresolution > 6) { - params.numresolution = 6; - } - break; - case OPJ_CINEMA4K_24: - params.cp_rsiz = OPJ_CINEMA4K; - if (params.numresolution > 7) { - params.numresolution = 7; - } - break; - } - - if (!context->num_resolutions) { - while (tile_width < (1U << (params.numresolution - 1U)) || - tile_height < (1U << (params.numresolution - 1U))) { - params.numresolution -= 1; - } - } - - if (context->cinema_mode != OPJ_OFF) { - j2k_set_cinema_params(im, components, ¶ms); - } - - /* Set up the reference grid in the image */ - image->x0 = params.image_offset_x0; - image->y0 = params.image_offset_y0; - image->x1 = xsiz = im->xsize + params.image_offset_x0; - image->y1 = ysiz = im->ysize + params.image_offset_y0; - - /* Create the compressor */ - codec = opj_create_compress(context->format); - - if (!codec) { - state->errcode = IMAGING_CODEC_BROKEN; - state->state = J2K_STATE_FAILED; - goto quick_exit; - } - - if (strcmp(im->mode, "RGBA") == 0) { - image->comps[3].alpha = 1; - } else if (strcmp(im->mode, "LA") == 0) { - image->comps[1].alpha = 1; - } - - opj_set_error_handler(codec, j2k_error, context); - opj_set_info_handler(codec, j2k_warn, context); - opj_set_warning_handler(codec, j2k_warn, context); - opj_setup_encoder(codec, ¶ms, image); - - /* Enabling PLT markers only supported in OpenJPEG 2.4.0 and up */ -#if ((OPJ_VERSION_MAJOR == 2 && OPJ_VERSION_MINOR >= 4) || OPJ_VERSION_MAJOR > 2) - if (context->plt) { - const char *plt_option[2] = {"PLT=YES", NULL}; - opj_encoder_set_extra_options(codec, plt_option); - } -#endif - - /* Start encoding */ - if (!opj_start_compress(codec, image, stream)) { - state->errcode = IMAGING_CODEC_BROKEN; - state->state = J2K_STATE_FAILED; - goto quick_exit; - } - - /* Write each tile */ - tiles_x = (im->xsize + (params.image_offset_x0 - params.cp_tx0) + tile_width - 1) / - tile_width; - tiles_y = (im->ysize + (params.image_offset_y0 - params.cp_ty0) + tile_height - 1) / - tile_height; - - /* check for integer overflow for the malloc line, checking any expression - that may multiply either tile_width or tile_height */ - _overflow_scale_factor = components * prec; - if ((tile_width > UINT_MAX / _overflow_scale_factor) || - (tile_height > UINT_MAX / _overflow_scale_factor) || - (tile_width > UINT_MAX / (tile_height * _overflow_scale_factor)) || - (tile_height > UINT_MAX / (tile_width * _overflow_scale_factor))) { - state->errcode = IMAGING_CODEC_BROKEN; - state->state = J2K_STATE_FAILED; - goto quick_exit; - } - /* malloc check ok, checked for overflow above */ - state->buffer = malloc(tile_width * tile_height * components * prec / 8); - if (!state->buffer) { - state->errcode = IMAGING_CODEC_BROKEN; - state->state = J2K_STATE_FAILED; - goto quick_exit; - } - - tile_ndx = 0; - for (y = 0; y < tiles_y; ++y) { - int ty0 = params.cp_ty0 + y * tile_height; - unsigned ty1 = ty0 + tile_height; - unsigned pixy, pixh; - - if (ty0 < params.image_offset_y0) { - ty0 = params.image_offset_y0; - } - if (ty1 > ysiz) { - ty1 = ysiz; - } - - pixy = ty0 - params.image_offset_y0; - pixh = ty1 - ty0; - - for (x = 0; x < tiles_x; ++x) { - int tx0 = params.cp_tx0 + x * tile_width; - unsigned tx1 = tx0 + tile_width; - unsigned pixx, pixw; - unsigned data_size; - - if (tx0 < params.image_offset_x0) { - tx0 = params.image_offset_x0; - } - if (tx1 > xsiz) { - tx1 = xsiz; - } - - pixx = tx0 - params.image_offset_x0; - pixw = tx1 - tx0; - - pack(im, state->buffer, pixx, pixy, pixw, pixh); - - data_size = pixw * pixh * components * prec / 8; - - if (!opj_write_tile(codec, tile_ndx++, state->buffer, data_size, stream)) { - state->errcode = IMAGING_CODEC_BROKEN; - state->state = J2K_STATE_FAILED; - goto quick_exit; - } - } - } - - if (!opj_end_compress(codec, stream)) { - state->errcode = IMAGING_CODEC_BROKEN; - state->state = J2K_STATE_FAILED; - goto quick_exit; - } - - state->errcode = IMAGING_CODEC_END; - state->state = J2K_STATE_DONE; - ret = -1; - -quick_exit: - if (codec) { - opj_destroy_codec(codec); - } - if (image) { - opj_image_destroy(image); - } - if (stream) { - opj_stream_destroy(stream); - } - - return ret; -} - -int -ImagingJpeg2KEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { - if (state->state == J2K_STATE_FAILED) { - return -1; - } - - if (state->state == J2K_STATE_START) { - state->state = J2K_STATE_ENCODING; - - return j2k_encode_entry(im, state); - } - - return -1; -} - -/* -------------------------------------------------------------------- */ -/* Cleanup */ -/* -------------------------------------------------------------------- */ - -int -ImagingJpeg2KEncodeCleanup(ImagingCodecState state) { - JPEG2KENCODESTATE *context = (JPEG2KENCODESTATE *)state->context; - - if (context->quality_layers) { - Py_XDECREF(context->quality_layers); - context->quality_layers = NULL; - } - - if (context->error_msg) { - free((void *)context->error_msg); - } - - if (context->comment) { - free((void *)context->comment); - } - - context->error_msg = NULL; - context->comment = NULL; - - return -1; -} - -#endif /* HAVE_OPENJPEG */ - -/* - * Local Variables: - * c-basic-offset: 4 - * End: - * - */ +/* + * The Python Imaging Library. + * $Id$ + * + * decoder for JPEG2000 image data. + * + * history: + * 2014-03-12 ajh Created + * + * Copyright (c) 2014 Coriolis Systems Limited + * Copyright (c) 2014 Alastair Houghton + * + * See the README file for details on usage and redistribution. + */ + +#include "Imaging.h" + +#ifdef HAVE_OPENJPEG + +#include "Jpeg2K.h" + +#define CINEMA_24_CS_LENGTH 1302083 +#define CINEMA_48_CS_LENGTH 651041 +#define COMP_24_CS_MAX_LENGTH 1041666 +#define COMP_48_CS_MAX_LENGTH 520833 + +/* -------------------------------------------------------------------- */ +/* Error handler */ +/* -------------------------------------------------------------------- */ + +static void +j2k_error(const char *msg, void *client_data) { + JPEG2KENCODESTATE *state = (JPEG2KENCODESTATE *)client_data; + free((void *)state->error_msg); + state->error_msg = strdup(msg); +} + +static void +j2k_warn(const char *msg, void *client_data) { + // Null handler +} + +/* -------------------------------------------------------------------- */ +/* Buffer output stream */ +/* -------------------------------------------------------------------- */ + +static OPJ_SIZE_T +j2k_write(void *p_buffer, OPJ_SIZE_T p_nb_bytes, void *p_user_data) { + ImagingCodecState state = (ImagingCodecState)p_user_data; + unsigned int result; + + result = _imaging_write_pyFd(state->fd, p_buffer, p_nb_bytes); + + return result ? result : (OPJ_SIZE_T)-1; +} + +static OPJ_OFF_T +j2k_skip(OPJ_OFF_T p_nb_bytes, void *p_user_data) { + ImagingCodecState state = (ImagingCodecState)p_user_data; + char *buffer; + int result; + + /* Explicitly write zeros */ + buffer = calloc(p_nb_bytes, 1); + if (!buffer) { + return (OPJ_OFF_T)-1; + } + + result = _imaging_write_pyFd(state->fd, buffer, p_nb_bytes); + + free(buffer); + + return result ? result : p_nb_bytes; +} + +static OPJ_BOOL +j2k_seek(OPJ_OFF_T p_nb_bytes, void *p_user_data) { + ImagingCodecState state = (ImagingCodecState)p_user_data; + off_t pos = 0; + + _imaging_seek_pyFd(state->fd, p_nb_bytes, SEEK_SET); + pos = _imaging_tell_pyFd(state->fd); + + return pos == p_nb_bytes; +} + +/* -------------------------------------------------------------------- */ +/* Encoder */ +/* -------------------------------------------------------------------- */ + +typedef void (*j2k_pack_tile_t)( + Imaging im, UINT8 *buf, unsigned x0, unsigned y0, unsigned w, unsigned h); + +static void +j2k_pack_l(Imaging im, UINT8 *buf, unsigned x0, unsigned y0, unsigned w, unsigned h) { + UINT8 *ptr = buf; + unsigned x, y; + for (y = 0; y < h; ++y) { + UINT8 *data = (UINT8 *)(im->image[y + y0] + x0); + for (x = 0; x < w; ++x) { + *ptr++ = *data++; + } + } +} + +static void +j2k_pack_i16(Imaging im, UINT8 *buf, unsigned x0, unsigned y0, unsigned w, unsigned h) { + UINT8 *ptr = buf; + unsigned x, y; + for (y = 0; y < h; ++y) { + UINT8 *data = (UINT8 *)(im->image[y + y0] + x0); + for (x = 0; x < w; ++x) { +#ifdef WORDS_BIGENDIAN + ptr[0] = data[1]; + ptr[1] = data[0]; +#else + ptr[0] = data[0]; + ptr[1] = data[1]; +#endif + ptr += 2; + data += 2; + } + } +} + +static void +j2k_pack_la(Imaging im, UINT8 *buf, unsigned x0, unsigned y0, unsigned w, unsigned h) { + UINT8 *ptr = buf; + UINT8 *ptra = buf + w * h; + unsigned x, y; + for (y = 0; y < h; ++y) { + UINT8 *data = (UINT8 *)(im->image[y + y0] + 4 * x0); + for (x = 0; x < w; ++x) { + *ptr++ = data[0]; + *ptra++ = data[3]; + data += 4; + } + } +} + +static void +j2k_pack_rgb(Imaging im, UINT8 *buf, unsigned x0, unsigned y0, unsigned w, unsigned h) { + UINT8 *pr = buf; + UINT8 *pg = pr + w * h; + UINT8 *pb = pg + w * h; + unsigned x, y; + for (y = 0; y < h; ++y) { + UINT8 *data = (UINT8 *)(im->image[y + y0] + 4 * x0); + for (x = 0; x < w; ++x) { + *pr++ = data[0]; + *pg++ = data[1]; + *pb++ = data[2]; + data += 4; + } + } +} + +static void +j2k_pack_rgba( + Imaging im, UINT8 *buf, unsigned x0, unsigned y0, unsigned w, unsigned h) { + UINT8 *pr = buf; + UINT8 *pg = pr + w * h; + UINT8 *pb = pg + w * h; + UINT8 *pa = pb + w * h; + unsigned x, y; + for (y = 0; y < h; ++y) { + UINT8 *data = (UINT8 *)(im->image[y + y0] + 4 * x0); + for (x = 0; x < w; ++x) { + *pr++ = *data++; + *pg++ = *data++; + *pb++ = *data++; + *pa++ = *data++; + } + } +} + +enum { + J2K_STATE_START = 0, + J2K_STATE_ENCODING = 1, + J2K_STATE_DONE = 2, + J2K_STATE_FAILED = 3, +}; + +static void +j2k_set_cinema_params(Imaging im, int components, opj_cparameters_t *params) { + float rate; + int n; + + /* These settings have been copied from opj_compress in the OpenJPEG + sources. */ + + params->tile_size_on = OPJ_FALSE; + params->cp_tdx = params->cp_tdy = 1; + params->tp_flag = 'C'; + params->tp_on = 1; + params->cp_tx0 = params->cp_ty0 = 0; + params->image_offset_x0 = params->image_offset_y0 = 0; + params->cblockw_init = 32; + params->cblockh_init = 32; + params->csty |= 0x01; + params->prog_order = OPJ_CPRL; + params->roi_compno = -1; + params->subsampling_dx = params->subsampling_dy = 1; + params->irreversible = 1; + + if (params->cp_cinema == OPJ_CINEMA4K_24) { + float max_rate = + ((float)(components * im->xsize * im->ysize * 8) / + (CINEMA_24_CS_LENGTH * 8)); + + params->POC[0].tile = 1; + params->POC[0].resno0 = 0; + params->POC[0].compno0 = 0; + params->POC[0].layno1 = 1; + params->POC[0].resno1 = params->numresolution - 1; + params->POC[0].compno1 = 3; + params->POC[0].prg1 = OPJ_CPRL; + params->POC[1].tile = 1; + params->POC[1].resno0 = 0; + params->POC[1].compno0 = 0; + params->POC[1].layno1 = 1; + params->POC[1].resno1 = params->numresolution - 1; + params->POC[1].compno1 = 3; + params->POC[1].prg1 = OPJ_CPRL; + params->numpocs = 2; + + for (n = 0; n < params->tcp_numlayers; ++n) { + rate = 0; + if (params->tcp_rates[0] == 0) { + params->tcp_rates[n] = max_rate; + } else { + rate = + ((float)(components * im->xsize * im->ysize * 8) / + (params->tcp_rates[n] * 8)); + if (rate > CINEMA_24_CS_LENGTH) { + params->tcp_rates[n] = max_rate; + } + } + } + + params->max_comp_size = COMP_24_CS_MAX_LENGTH; + } else { + float max_rate = + ((float)(components * im->xsize * im->ysize * 8) / + (CINEMA_48_CS_LENGTH * 8)); + + for (n = 0; n < params->tcp_numlayers; ++n) { + rate = 0; + if (params->tcp_rates[0] == 0) { + params->tcp_rates[n] = max_rate; + } else { + rate = + ((float)(components * im->xsize * im->ysize * 8) / + (params->tcp_rates[n] * 8)); + if (rate > CINEMA_48_CS_LENGTH) { + params->tcp_rates[n] = max_rate; + } + } + } + + params->max_comp_size = COMP_48_CS_MAX_LENGTH; + } +} + +static int +j2k_encode_entry(Imaging im, ImagingCodecState state) { + JPEG2KENCODESTATE *context = (JPEG2KENCODESTATE *)state->context; + opj_stream_t *stream = NULL; + opj_image_t *image = NULL; + opj_codec_t *codec = NULL; + opj_cparameters_t params; + unsigned components; + OPJ_COLOR_SPACE color_space; + opj_image_cmptparm_t image_params[4]; + unsigned xsiz, ysiz; + unsigned tile_width, tile_height; + unsigned tiles_x, tiles_y; + unsigned x, y, tile_ndx; + unsigned n; + j2k_pack_tile_t pack; + int ret = -1; + + unsigned prec = 8; + unsigned _overflow_scale_factor; + + stream = opj_stream_create(BUFFER_SIZE, OPJ_FALSE); + + if (!stream) { + state->errcode = IMAGING_CODEC_BROKEN; + state->state = J2K_STATE_FAILED; + goto quick_exit; + } + + opj_stream_set_write_function(stream, j2k_write); + opj_stream_set_skip_function(stream, j2k_skip); + opj_stream_set_seek_function(stream, j2k_seek); + + /* OpenJPEG 2.0 doesn't have OPJ_VERSION_MAJOR */ +#ifndef OPJ_VERSION_MAJOR + opj_stream_set_user_data(stream, state); +#else + opj_stream_set_user_data(stream, state, NULL); +#endif + + /* Setup an opj_image */ + if (strcmp(im->mode, "L") == 0) { + components = 1; + color_space = OPJ_CLRSPC_GRAY; + pack = j2k_pack_l; + } else if (strcmp(im->mode, "I;16") == 0 || strcmp(im->mode, "I;16B") == 0) { + components = 1; + color_space = OPJ_CLRSPC_GRAY; + pack = j2k_pack_i16; + prec = 16; + } else if (strcmp(im->mode, "LA") == 0) { + components = 2; + color_space = OPJ_CLRSPC_GRAY; + pack = j2k_pack_la; + } else if (strcmp(im->mode, "RGB") == 0) { + components = 3; + color_space = OPJ_CLRSPC_SRGB; + pack = j2k_pack_rgb; + } else if (strcmp(im->mode, "YCbCr") == 0) { + components = 3; + color_space = OPJ_CLRSPC_SYCC; + pack = j2k_pack_rgb; + } else if (strcmp(im->mode, "RGBA") == 0) { + components = 4; + color_space = OPJ_CLRSPC_SRGB; + pack = j2k_pack_rgba; + } else { + state->errcode = IMAGING_CODEC_BROKEN; + state->state = J2K_STATE_FAILED; + goto quick_exit; + } + + for (n = 0; n < components; ++n) { + image_params[n].dx = image_params[n].dy = 1; + image_params[n].w = im->xsize; + image_params[n].h = im->ysize; + image_params[n].x0 = image_params[n].y0 = 0; + image_params[n].prec = prec; + image_params[n].sgnd = context->sgnd == 0 ? 0 : 1; + } + + image = opj_image_create(components, image_params, color_space); + if (!image) { + state->errcode = IMAGING_CODEC_BROKEN; + state->state = J2K_STATE_FAILED; + goto quick_exit; + } + + /* Setup compression context */ + context->error_msg = NULL; + + opj_set_default_encoder_parameters(¶ms); + + params.image_offset_x0 = context->offset_x; + params.image_offset_y0 = context->offset_y; + + if (context->tile_size_x && context->tile_size_y) { + params.tile_size_on = OPJ_TRUE; + params.cp_tx0 = context->tile_offset_x; + params.cp_ty0 = context->tile_offset_y; + params.cp_tdx = context->tile_size_x; + params.cp_tdy = context->tile_size_y; + + tile_width = params.cp_tdx; + tile_height = params.cp_tdy; + } else { + params.cp_tx0 = 0; + params.cp_ty0 = 0; + params.cp_tdx = 1; + params.cp_tdy = 1; + + tile_width = im->xsize; + tile_height = im->ysize; + } + + if (context->quality_layers && PySequence_Check(context->quality_layers)) { + Py_ssize_t len = PySequence_Length(context->quality_layers); + Py_ssize_t n; + float *pq; + + if (len > 0) { + if ((size_t)len > sizeof(params.tcp_rates) / sizeof(params.tcp_rates[0])) { + len = sizeof(params.tcp_rates) / sizeof(params.tcp_rates[0]); + } + + params.tcp_numlayers = (int)len; + + if (context->quality_is_in_db) { + params.cp_disto_alloc = params.cp_fixed_alloc = 0; + params.cp_fixed_quality = 1; + pq = params.tcp_distoratio; + } else { + params.cp_disto_alloc = 1; + params.cp_fixed_alloc = params.cp_fixed_quality = 0; + pq = params.tcp_rates; + } + + for (n = 0; n < len; ++n) { + PyObject *obj = PySequence_ITEM(context->quality_layers, n); + pq[n] = PyFloat_AsDouble(obj); + } + } + } else { + params.tcp_numlayers = 1; + params.tcp_rates[0] = 0; + params.cp_disto_alloc = 1; + } + + if (context->num_resolutions) { + params.numresolution = context->num_resolutions; + } + + if (context->cblk_width >= 4 && context->cblk_width <= 1024 && + context->cblk_height >= 4 && context->cblk_height <= 1024 && + context->cblk_width * context->cblk_height <= 4096) { + params.cblockw_init = context->cblk_width; + params.cblockh_init = context->cblk_height; + } + + if (context->precinct_width >= 4 && context->precinct_height >= 4 && + context->precinct_width >= context->cblk_width && + context->precinct_height > context->cblk_height) { + params.prcw_init[0] = context->precinct_width; + params.prch_init[0] = context->precinct_height; + params.res_spec = 1; + params.csty |= 0x01; + } + + params.irreversible = context->irreversible; + if (components == 3) { + params.tcp_mct = context->mct; + } + + if (context->comment) { + params.cp_comment = context->comment; + } + + params.prog_order = context->progression; + + params.cp_cinema = context->cinema_mode; + + switch (params.cp_cinema) { + case OPJ_OFF: + params.cp_rsiz = OPJ_STD_RSIZ; + break; + case OPJ_CINEMA2K_24: + case OPJ_CINEMA2K_48: + params.cp_rsiz = OPJ_CINEMA2K; + if (params.numresolution > 6) { + params.numresolution = 6; + } + break; + case OPJ_CINEMA4K_24: + params.cp_rsiz = OPJ_CINEMA4K; + if (params.numresolution > 7) { + params.numresolution = 7; + } + break; + } + + if (!context->num_resolutions) { + while (tile_width < (1U << (params.numresolution - 1U)) || + tile_height < (1U << (params.numresolution - 1U))) { + params.numresolution -= 1; + } + } + + if (context->cinema_mode != OPJ_OFF) { + j2k_set_cinema_params(im, components, ¶ms); + } + + /* Set up the reference grid in the image */ + image->x0 = params.image_offset_x0; + image->y0 = params.image_offset_y0; + image->x1 = xsiz = im->xsize + params.image_offset_x0; + image->y1 = ysiz = im->ysize + params.image_offset_y0; + + /* Create the compressor */ + codec = opj_create_compress(context->format); + + if (!codec) { + state->errcode = IMAGING_CODEC_BROKEN; + state->state = J2K_STATE_FAILED; + goto quick_exit; + } + + if (strcmp(im->mode, "RGBA") == 0) { + image->comps[3].alpha = 1; + } else if (strcmp(im->mode, "LA") == 0) { + image->comps[1].alpha = 1; + } + + opj_set_error_handler(codec, j2k_error, context); + opj_set_info_handler(codec, j2k_warn, context); + opj_set_warning_handler(codec, j2k_warn, context); + opj_setup_encoder(codec, ¶ms, image); + + /* Enabling PLT markers only supported in OpenJPEG 2.4.0 and up */ +#if ((OPJ_VERSION_MAJOR == 2 && OPJ_VERSION_MINOR >= 4) || OPJ_VERSION_MAJOR > 2) + if (context->plt) { + const char *plt_option[2] = {"PLT=YES", NULL}; + opj_encoder_set_extra_options(codec, plt_option); + } +#endif + + /* Start encoding */ + if (!opj_start_compress(codec, image, stream)) { + state->errcode = IMAGING_CODEC_BROKEN; + state->state = J2K_STATE_FAILED; + goto quick_exit; + } + + /* Write each tile */ + tiles_x = (im->xsize + (params.image_offset_x0 - params.cp_tx0) + tile_width - 1) / + tile_width; + tiles_y = (im->ysize + (params.image_offset_y0 - params.cp_ty0) + tile_height - 1) / + tile_height; + + /* check for integer overflow for the malloc line, checking any expression + that may multiply either tile_width or tile_height */ + _overflow_scale_factor = components * prec; + if ((tile_width > UINT_MAX / _overflow_scale_factor) || + (tile_height > UINT_MAX / _overflow_scale_factor) || + (tile_width > UINT_MAX / (tile_height * _overflow_scale_factor)) || + (tile_height > UINT_MAX / (tile_width * _overflow_scale_factor))) { + state->errcode = IMAGING_CODEC_BROKEN; + state->state = J2K_STATE_FAILED; + goto quick_exit; + } + /* malloc check ok, checked for overflow above */ + state->buffer = malloc(tile_width * tile_height * components * prec / 8); + if (!state->buffer) { + state->errcode = IMAGING_CODEC_BROKEN; + state->state = J2K_STATE_FAILED; + goto quick_exit; + } + + tile_ndx = 0; + for (y = 0; y < tiles_y; ++y) { + int ty0 = params.cp_ty0 + y * tile_height; + unsigned ty1 = ty0 + tile_height; + unsigned pixy, pixh; + + if (ty0 < params.image_offset_y0) { + ty0 = params.image_offset_y0; + } + if (ty1 > ysiz) { + ty1 = ysiz; + } + + pixy = ty0 - params.image_offset_y0; + pixh = ty1 - ty0; + + for (x = 0; x < tiles_x; ++x) { + int tx0 = params.cp_tx0 + x * tile_width; + unsigned tx1 = tx0 + tile_width; + unsigned pixx, pixw; + unsigned data_size; + + if (tx0 < params.image_offset_x0) { + tx0 = params.image_offset_x0; + } + if (tx1 > xsiz) { + tx1 = xsiz; + } + + pixx = tx0 - params.image_offset_x0; + pixw = tx1 - tx0; + + pack(im, state->buffer, pixx, pixy, pixw, pixh); + + data_size = pixw * pixh * components * prec / 8; + + if (!opj_write_tile(codec, tile_ndx++, state->buffer, data_size, stream)) { + state->errcode = IMAGING_CODEC_BROKEN; + state->state = J2K_STATE_FAILED; + goto quick_exit; + } + } + } + + if (!opj_end_compress(codec, stream)) { + state->errcode = IMAGING_CODEC_BROKEN; + state->state = J2K_STATE_FAILED; + goto quick_exit; + } + + state->errcode = IMAGING_CODEC_END; + state->state = J2K_STATE_DONE; + ret = -1; + +quick_exit: + if (codec) { + opj_destroy_codec(codec); + } + if (image) { + opj_image_destroy(image); + } + if (stream) { + opj_stream_destroy(stream); + } + + return ret; +} + +int +ImagingJpeg2KEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { + if (state->state == J2K_STATE_FAILED) { + return -1; + } + + if (state->state == J2K_STATE_START) { + state->state = J2K_STATE_ENCODING; + + return j2k_encode_entry(im, state); + } + + return -1; +} + +/* -------------------------------------------------------------------- */ +/* Cleanup */ +/* -------------------------------------------------------------------- */ + +int +ImagingJpeg2KEncodeCleanup(ImagingCodecState state) { + JPEG2KENCODESTATE *context = (JPEG2KENCODESTATE *)state->context; + + if (context->quality_layers) { + Py_XDECREF(context->quality_layers); + context->quality_layers = NULL; + } + + if (context->error_msg) { + free((void *)context->error_msg); + } + + if (context->comment) { + free((void *)context->comment); + } + + context->error_msg = NULL; + context->comment = NULL; + + return -1; +} + +#endif /* HAVE_OPENJPEG */ + +/* + * Local Variables: + * c-basic-offset: 4 + * End: + * + */ diff --git a/src/libImaging/JpegDecode.c b/src/libImaging/JpegDecode.c index 6f75d8670..19f57fb39 100644 --- a/src/libImaging/JpegDecode.c +++ b/src/libImaging/JpegDecode.c @@ -1,304 +1,304 @@ -/* - * The Python Imaging Library. - * $Id$ - * - * decoder for JPEG image data. - * - * history: - * 1996-05-02 fl Created - * 1996-05-05 fl Handle small JPEG files correctly - * 1996-05-28 fl Added "draft mode" support - * 1997-01-25 fl Added colour conversion override - * 1998-01-31 fl Adapted to libjpeg 6a - * 1998-07-12 fl Extended YCbCr support - * 1998-12-29 fl Added new state to handle suspension in multipass modes - * 2000-10-12 fl Suppress warnings - * 2000-12-04 fl Suppress errors beyond end of image data - * - * Copyright (c) 1998-2000 Secret Labs AB - * Copyright (c) 1996-2000 Fredrik Lundh - * - * See the README file for details on usage and redistribution. - */ - -#include "Imaging.h" - -#ifdef HAVE_LIBJPEG - -#undef HAVE_PROTOTYPES -#undef HAVE_STDLIB_H -#undef HAVE_STDDEF_H -#undef UINT8 -#undef UINT16 -#undef UINT32 -#undef INT16 -#undef INT32 - -#include "Jpeg.h" - -#define STRINGIFY(x) #x -#define TOSTRING(x) STRINGIFY(x) - -// There is no way to compare versions on compile time, -// so we have to do that in runtime. -#ifdef LIBJPEG_TURBO_VERSION -char *libjpeg_turbo_version = TOSTRING(LIBJPEG_TURBO_VERSION); -#else -char *libjpeg_turbo_version = NULL; -#endif - -int -ImagingJpegUseJCSExtensions() { - int use_jcs_extensions = 0; -#ifdef JCS_EXTENSIONS -#if defined(LIBJPEG_TURBO_VERSION_NUMBER) -#if LIBJPEG_TURBO_VERSION_NUMBER >= 1002010 - use_jcs_extensions = 1; -#endif -#else - if (libjpeg_turbo_version) { - use_jcs_extensions = strcmp(libjpeg_turbo_version, "1.2.1") >= 0; - } -#endif -#endif - return use_jcs_extensions; -} - -/* -------------------------------------------------------------------- */ -/* Suspending input handler */ -/* -------------------------------------------------------------------- */ - -METHODDEF(void) -stub(j_decompress_ptr cinfo) { /* empty */ } - -METHODDEF(boolean) -fill_input_buffer(j_decompress_ptr cinfo) { - /* Suspension */ - return FALSE; -} - -METHODDEF(void) -skip_input_data(j_decompress_ptr cinfo, long num_bytes) { - JPEGSOURCE *source = (JPEGSOURCE *)cinfo->src; - - if (num_bytes > (long)source->pub.bytes_in_buffer) { - /* We need to skip more data than we have in the buffer. - This will force the JPEG library to suspend decoding. */ - source->skip = num_bytes - source->pub.bytes_in_buffer; - source->pub.next_input_byte += source->pub.bytes_in_buffer; - source->pub.bytes_in_buffer = 0; - } else { - /* Skip portion of the buffer */ - source->pub.bytes_in_buffer -= num_bytes; - source->pub.next_input_byte += num_bytes; - source->skip = 0; - } -} - -GLOBAL(void) -jpeg_buffer_src(j_decompress_ptr cinfo, JPEGSOURCE *source) { - cinfo->src = (void *)source; - - /* Prepare for suspending reader */ - source->pub.init_source = stub; - source->pub.fill_input_buffer = fill_input_buffer; - source->pub.skip_input_data = skip_input_data; - source->pub.resync_to_restart = jpeg_resync_to_restart; - source->pub.term_source = stub; - source->pub.bytes_in_buffer = 0; /* forces fill_input_buffer on first read */ - - source->skip = 0; -} - -/* -------------------------------------------------------------------- */ -/* Error handler */ -/* -------------------------------------------------------------------- */ - -METHODDEF(void) -error(j_common_ptr cinfo) { - JPEGERROR *error; - error = (JPEGERROR *)cinfo->err; - longjmp(error->setjmp_buffer, 1); -} - -METHODDEF(void) -output(j_common_ptr cinfo) { /* nothing */ } - -/* -------------------------------------------------------------------- */ -/* Decoder */ -/* -------------------------------------------------------------------- */ - -int -ImagingJpegDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) { - JPEGSTATE *context = (JPEGSTATE *)state->context; - int ok; - - if (setjmp(context->error.setjmp_buffer)) { - /* JPEG error handler */ - jpeg_destroy_decompress(&context->cinfo); - state->errcode = IMAGING_CODEC_BROKEN; - return -1; - } - - if (!state->state) { - /* Setup decompression context */ - context->cinfo.err = jpeg_std_error(&context->error.pub); - context->error.pub.error_exit = error; - context->error.pub.output_message = output; - jpeg_create_decompress(&context->cinfo); - jpeg_buffer_src(&context->cinfo, &context->source); - - /* Ready to decode */ - state->state = 1; - } - - /* Load the source buffer */ - context->source.pub.next_input_byte = buf; - context->source.pub.bytes_in_buffer = bytes; - - if (context->source.skip > 0) { - skip_input_data(&context->cinfo, context->source.skip); - if (context->source.skip > 0) { - return context->source.pub.next_input_byte - buf; - } - } - - switch (state->state) { - case 1: - - /* Read JPEG header, until we find an image body. */ - do { - /* Note that we cannot return unless we have decoded - as much data as possible. */ - ok = jpeg_read_header(&context->cinfo, FALSE); - - } while (ok == JPEG_HEADER_TABLES_ONLY); - - if (ok == JPEG_SUSPENDED) { - break; - } - - /* Decoder settings */ - - /* jpegmode indicates what's in the file; if not set, we'll - trust the decoder */ - if (strcmp(context->jpegmode, "L") == 0) { - context->cinfo.jpeg_color_space = JCS_GRAYSCALE; - } else if (strcmp(context->jpegmode, "RGB") == 0) { - context->cinfo.jpeg_color_space = JCS_RGB; - } else if (strcmp(context->jpegmode, "CMYK") == 0) { - context->cinfo.jpeg_color_space = JCS_CMYK; - } else if (strcmp(context->jpegmode, "YCbCr") == 0) { - context->cinfo.jpeg_color_space = JCS_YCbCr; - } else if (strcmp(context->jpegmode, "YCbCrK") == 0) { - context->cinfo.jpeg_color_space = JCS_YCCK; - } - - /* rawmode indicates what we want from the decoder. if not - set, conversions are disabled */ - if (strcmp(context->rawmode, "L") == 0) { - context->cinfo.out_color_space = JCS_GRAYSCALE; - } else if (strcmp(context->rawmode, "RGB") == 0) { - context->cinfo.out_color_space = JCS_RGB; - } -#ifdef JCS_EXTENSIONS - else if (strcmp(context->rawmode, "RGBX") == 0) { - context->cinfo.out_color_space = JCS_EXT_RGBX; - } -#endif - else if ( - strcmp(context->rawmode, "CMYK") == 0 || - strcmp(context->rawmode, "CMYK;I") == 0) { - context->cinfo.out_color_space = JCS_CMYK; - } else if (strcmp(context->rawmode, "YCbCr") == 0) { - context->cinfo.out_color_space = JCS_YCbCr; - } else if (strcmp(context->rawmode, "YCbCrK") == 0) { - context->cinfo.out_color_space = JCS_YCCK; - } else { - /* Disable decoder conversions */ - context->cinfo.jpeg_color_space = JCS_UNKNOWN; - context->cinfo.out_color_space = JCS_UNKNOWN; - } - - if (context->scale > 1) { - context->cinfo.scale_num = 1; - context->cinfo.scale_denom = context->scale; - } - if (context->draft) { - context->cinfo.do_fancy_upsampling = FALSE; - context->cinfo.dct_method = JDCT_FASTEST; - } - - state->state++; - /* fall through */ - - case 2: - - /* Set things up for decompression (this processes the entire - file if necessary to return data line by line) */ - if (!jpeg_start_decompress(&context->cinfo)) { - break; - } - - state->state++; - /* fall through */ - - case 3: - - /* Decompress a single line of data */ - ok = 1; - while (state->y < state->ysize) { - ok = jpeg_read_scanlines(&context->cinfo, &state->buffer, 1); - if (ok != 1) { - break; - } - state->shuffle( - (UINT8 *)im->image[state->y + state->yoff] + - state->xoff * im->pixelsize, - state->buffer, - state->xsize); - state->y++; - } - if (ok != 1) { - break; - } - state->state++; - /* fall through */ - - case 4: - - /* Finish decompression */ - if (!jpeg_finish_decompress(&context->cinfo)) { - /* FIXME: add strictness mode test */ - if (state->y < state->ysize) { - break; - } - } - - /* Clean up */ - jpeg_destroy_decompress(&context->cinfo); - /* if (jerr.pub.num_warnings) return BROKEN; */ - return -1; - } - - /* Return number of bytes consumed */ - return context->source.pub.next_input_byte - buf; -} - -/* -------------------------------------------------------------------- */ -/* Cleanup */ -/* -------------------------------------------------------------------- */ - -int -ImagingJpegDecodeCleanup(ImagingCodecState state) { - /* called to free the decompression engine when the decode terminates - due to a corrupt or truncated image - */ - JPEGSTATE *context = (JPEGSTATE *)state->context; - - /* Clean up */ - jpeg_destroy_decompress(&context->cinfo); - return -1; -} - -#endif +/* + * The Python Imaging Library. + * $Id$ + * + * decoder for JPEG image data. + * + * history: + * 1996-05-02 fl Created + * 1996-05-05 fl Handle small JPEG files correctly + * 1996-05-28 fl Added "draft mode" support + * 1997-01-25 fl Added colour conversion override + * 1998-01-31 fl Adapted to libjpeg 6a + * 1998-07-12 fl Extended YCbCr support + * 1998-12-29 fl Added new state to handle suspension in multipass modes + * 2000-10-12 fl Suppress warnings + * 2000-12-04 fl Suppress errors beyond end of image data + * + * Copyright (c) 1998-2000 Secret Labs AB + * Copyright (c) 1996-2000 Fredrik Lundh + * + * See the README file for details on usage and redistribution. + */ + +#include "Imaging.h" + +#ifdef HAVE_LIBJPEG + +#undef HAVE_PROTOTYPES +#undef HAVE_STDLIB_H +#undef HAVE_STDDEF_H +#undef UINT8 +#undef UINT16 +#undef UINT32 +#undef INT16 +#undef INT32 + +#include "Jpeg.h" + +#define STRINGIFY(x) #x +#define TOSTRING(x) STRINGIFY(x) + +// There is no way to compare versions on compile time, +// so we have to do that in runtime. +#ifdef LIBJPEG_TURBO_VERSION +char *libjpeg_turbo_version = TOSTRING(LIBJPEG_TURBO_VERSION); +#else +char *libjpeg_turbo_version = NULL; +#endif + +int +ImagingJpegUseJCSExtensions() { + int use_jcs_extensions = 0; +#ifdef JCS_EXTENSIONS +#if defined(LIBJPEG_TURBO_VERSION_NUMBER) +#if LIBJPEG_TURBO_VERSION_NUMBER >= 1002010 + use_jcs_extensions = 1; +#endif +#else + if (libjpeg_turbo_version) { + use_jcs_extensions = strcmp(libjpeg_turbo_version, "1.2.1") >= 0; + } +#endif +#endif + return use_jcs_extensions; +} + +/* -------------------------------------------------------------------- */ +/* Suspending input handler */ +/* -------------------------------------------------------------------- */ + +METHODDEF(void) +stub(j_decompress_ptr cinfo) { /* empty */ } + +METHODDEF(boolean) +fill_input_buffer(j_decompress_ptr cinfo) { + /* Suspension */ + return FALSE; +} + +METHODDEF(void) +skip_input_data(j_decompress_ptr cinfo, long num_bytes) { + JPEGSOURCE *source = (JPEGSOURCE *)cinfo->src; + + if (num_bytes > (long)source->pub.bytes_in_buffer) { + /* We need to skip more data than we have in the buffer. + This will force the JPEG library to suspend decoding. */ + source->skip = num_bytes - source->pub.bytes_in_buffer; + source->pub.next_input_byte += source->pub.bytes_in_buffer; + source->pub.bytes_in_buffer = 0; + } else { + /* Skip portion of the buffer */ + source->pub.bytes_in_buffer -= num_bytes; + source->pub.next_input_byte += num_bytes; + source->skip = 0; + } +} + +GLOBAL(void) +jpeg_buffer_src(j_decompress_ptr cinfo, JPEGSOURCE *source) { + cinfo->src = (void *)source; + + /* Prepare for suspending reader */ + source->pub.init_source = stub; + source->pub.fill_input_buffer = fill_input_buffer; + source->pub.skip_input_data = skip_input_data; + source->pub.resync_to_restart = jpeg_resync_to_restart; + source->pub.term_source = stub; + source->pub.bytes_in_buffer = 0; /* forces fill_input_buffer on first read */ + + source->skip = 0; +} + +/* -------------------------------------------------------------------- */ +/* Error handler */ +/* -------------------------------------------------------------------- */ + +METHODDEF(void) +error(j_common_ptr cinfo) { + JPEGERROR *error; + error = (JPEGERROR *)cinfo->err; + longjmp(error->setjmp_buffer, 1); +} + +METHODDEF(void) +output(j_common_ptr cinfo) { /* nothing */ } + +/* -------------------------------------------------------------------- */ +/* Decoder */ +/* -------------------------------------------------------------------- */ + +int +ImagingJpegDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) { + JPEGSTATE *context = (JPEGSTATE *)state->context; + int ok; + + if (setjmp(context->error.setjmp_buffer)) { + /* JPEG error handler */ + jpeg_destroy_decompress(&context->cinfo); + state->errcode = IMAGING_CODEC_BROKEN; + return -1; + } + + if (!state->state) { + /* Setup decompression context */ + context->cinfo.err = jpeg_std_error(&context->error.pub); + context->error.pub.error_exit = error; + context->error.pub.output_message = output; + jpeg_create_decompress(&context->cinfo); + jpeg_buffer_src(&context->cinfo, &context->source); + + /* Ready to decode */ + state->state = 1; + } + + /* Load the source buffer */ + context->source.pub.next_input_byte = buf; + context->source.pub.bytes_in_buffer = bytes; + + if (context->source.skip > 0) { + skip_input_data(&context->cinfo, context->source.skip); + if (context->source.skip > 0) { + return context->source.pub.next_input_byte - buf; + } + } + + switch (state->state) { + case 1: + + /* Read JPEG header, until we find an image body. */ + do { + /* Note that we cannot return unless we have decoded + as much data as possible. */ + ok = jpeg_read_header(&context->cinfo, FALSE); + + } while (ok == JPEG_HEADER_TABLES_ONLY); + + if (ok == JPEG_SUSPENDED) { + break; + } + + /* Decoder settings */ + + /* jpegmode indicates what's in the file; if not set, we'll + trust the decoder */ + if (strcmp(context->jpegmode, "L") == 0) { + context->cinfo.jpeg_color_space = JCS_GRAYSCALE; + } else if (strcmp(context->jpegmode, "RGB") == 0) { + context->cinfo.jpeg_color_space = JCS_RGB; + } else if (strcmp(context->jpegmode, "CMYK") == 0) { + context->cinfo.jpeg_color_space = JCS_CMYK; + } else if (strcmp(context->jpegmode, "YCbCr") == 0) { + context->cinfo.jpeg_color_space = JCS_YCbCr; + } else if (strcmp(context->jpegmode, "YCbCrK") == 0) { + context->cinfo.jpeg_color_space = JCS_YCCK; + } + + /* rawmode indicates what we want from the decoder. if not + set, conversions are disabled */ + if (strcmp(context->rawmode, "L") == 0) { + context->cinfo.out_color_space = JCS_GRAYSCALE; + } else if (strcmp(context->rawmode, "RGB") == 0) { + context->cinfo.out_color_space = JCS_RGB; + } +#ifdef JCS_EXTENSIONS + else if (strcmp(context->rawmode, "RGBX") == 0) { + context->cinfo.out_color_space = JCS_EXT_RGBX; + } +#endif + else if ( + strcmp(context->rawmode, "CMYK") == 0 || + strcmp(context->rawmode, "CMYK;I") == 0) { + context->cinfo.out_color_space = JCS_CMYK; + } else if (strcmp(context->rawmode, "YCbCr") == 0) { + context->cinfo.out_color_space = JCS_YCbCr; + } else if (strcmp(context->rawmode, "YCbCrK") == 0) { + context->cinfo.out_color_space = JCS_YCCK; + } else { + /* Disable decoder conversions */ + context->cinfo.jpeg_color_space = JCS_UNKNOWN; + context->cinfo.out_color_space = JCS_UNKNOWN; + } + + if (context->scale > 1) { + context->cinfo.scale_num = 1; + context->cinfo.scale_denom = context->scale; + } + if (context->draft) { + context->cinfo.do_fancy_upsampling = FALSE; + context->cinfo.dct_method = JDCT_FASTEST; + } + + state->state++; + /* fall through */ + + case 2: + + /* Set things up for decompression (this processes the entire + file if necessary to return data line by line) */ + if (!jpeg_start_decompress(&context->cinfo)) { + break; + } + + state->state++; + /* fall through */ + + case 3: + + /* Decompress a single line of data */ + ok = 1; + while (state->y < state->ysize) { + ok = jpeg_read_scanlines(&context->cinfo, &state->buffer, 1); + if (ok != 1) { + break; + } + state->shuffle( + (UINT8 *)im->image[state->y + state->yoff] + + state->xoff * im->pixelsize, + state->buffer, + state->xsize); + state->y++; + } + if (ok != 1) { + break; + } + state->state++; + /* fall through */ + + case 4: + + /* Finish decompression */ + if (!jpeg_finish_decompress(&context->cinfo)) { + /* FIXME: add strictness mode test */ + if (state->y < state->ysize) { + break; + } + } + + /* Clean up */ + jpeg_destroy_decompress(&context->cinfo); + /* if (jerr.pub.num_warnings) return BROKEN; */ + return -1; + } + + /* Return number of bytes consumed */ + return context->source.pub.next_input_byte - buf; +} + +/* -------------------------------------------------------------------- */ +/* Cleanup */ +/* -------------------------------------------------------------------- */ + +int +ImagingJpegDecodeCleanup(ImagingCodecState state) { + /* called to free the decompression engine when the decode terminates + due to a corrupt or truncated image + */ + JPEGSTATE *context = (JPEGSTATE *)state->context; + + /* Clean up */ + jpeg_destroy_decompress(&context->cinfo); + return -1; +} + +#endif diff --git a/src/libImaging/JpegEncode.c b/src/libImaging/JpegEncode.c index ba8353c2d..7afd6b56f 100644 --- a/src/libImaging/JpegEncode.c +++ b/src/libImaging/JpegEncode.c @@ -1,385 +1,385 @@ -/* - * The Python Imaging Library. - * $Id$ - * - * coder for JPEG data - * - * history: - * 1996-05-06 fl created - * 1996-07-16 fl don't drop last block of encoded data - * 1996-12-30 fl added quality and progressive settings - * 1997-01-08 fl added streamtype settings - * 1998-01-31 fl adapted to libjpeg 6a - * 1998-07-12 fl added YCbCr support - * 2001-04-16 fl added DPI write support - * - * Copyright (c) 1997-2001 by Secret Labs AB - * Copyright (c) 1995-1997 by Fredrik Lundh - * - * See the README file for details on usage and redistribution. - */ - -#include "Imaging.h" - -#ifdef HAVE_LIBJPEG - -#undef HAVE_PROTOTYPES -#undef HAVE_STDLIB_H -#undef HAVE_STDDEF_H -#undef UINT8 -#undef UINT16 -#undef UINT32 -#undef INT16 -#undef INT32 - -#include "Jpeg.h" - -/* -------------------------------------------------------------------- */ -/* Suspending output handler */ -/* -------------------------------------------------------------------- */ - -METHODDEF(void) -stub(j_compress_ptr cinfo) { /* empty */ } - -METHODDEF(boolean) -empty_output_buffer(j_compress_ptr cinfo) { - /* Suspension */ - return FALSE; -} - -GLOBAL(void) -jpeg_buffer_dest(j_compress_ptr cinfo, JPEGDESTINATION *destination) { - cinfo->dest = (void *)destination; - - destination->pub.init_destination = stub; - destination->pub.empty_output_buffer = empty_output_buffer; - destination->pub.term_destination = stub; -} - -/* -------------------------------------------------------------------- */ -/* Error handler */ -/* -------------------------------------------------------------------- */ - -METHODDEF(void) -error(j_common_ptr cinfo) { - JPEGERROR *error; - error = (JPEGERROR *)cinfo->err; - (*cinfo->err->output_message)(cinfo); - longjmp(error->setjmp_buffer, 1); -} - -/* -------------------------------------------------------------------- */ -/* Encoder */ -/* -------------------------------------------------------------------- */ - -int -ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { - JPEGENCODERSTATE *context = (JPEGENCODERSTATE *)state->context; - int ok; - - if (setjmp(context->error.setjmp_buffer)) { - /* JPEG error handler */ - jpeg_destroy_compress(&context->cinfo); - state->errcode = IMAGING_CODEC_BROKEN; - return -1; - } - - if (!state->state) { - /* Setup compression context (very similar to the decoder) */ - context->cinfo.err = jpeg_std_error(&context->error.pub); - context->error.pub.error_exit = error; - jpeg_create_compress(&context->cinfo); - jpeg_buffer_dest(&context->cinfo, &context->destination); - - context->extra_offset = 0; - - /* Ready to encode */ - state->state = 1; - } - - /* Load the destination buffer */ - context->destination.pub.next_output_byte = buf; - context->destination.pub.free_in_buffer = bytes; - - switch (state->state) { - case 1: - - context->cinfo.image_width = state->xsize; - context->cinfo.image_height = state->ysize; - - switch (state->bits) { - case 8: - context->cinfo.input_components = 1; - context->cinfo.in_color_space = JCS_GRAYSCALE; - break; - case 24: - context->cinfo.input_components = 3; - if (strcmp(im->mode, "YCbCr") == 0) { - context->cinfo.in_color_space = JCS_YCbCr; - } else { - context->cinfo.in_color_space = JCS_RGB; - } - break; - case 32: - context->cinfo.input_components = 4; - context->cinfo.in_color_space = JCS_CMYK; -#ifdef JCS_EXTENSIONS - if (strcmp(context->rawmode, "RGBX") == 0) { - context->cinfo.in_color_space = JCS_EXT_RGBX; - } -#endif - break; - default: - state->errcode = IMAGING_CODEC_CONFIG; - return -1; - } - - /* Compressor configuration */ - jpeg_set_defaults(&context->cinfo); - - /* Prevent RGB -> YCbCr conversion */ - if (context->keep_rgb) { - switch (context->cinfo.in_color_space) { - case JCS_RGB: -#ifdef JCS_EXTENSIONS - case JCS_EXT_RGBX: -#endif - switch (context->subsampling) { - case -1: /* Default */ - case 0: /* No subsampling */ - break; - default: - /* Would subsample the green and blue - channels, which doesn't make sense */ - state->errcode = IMAGING_CODEC_CONFIG; - return -1; - } - jpeg_set_colorspace(&context->cinfo, JCS_RGB); - break; - default: - break; - } - } - - /* Use custom quantization tables */ - if (context->qtables) { - int i; - int quality = 100; - int last_q = 0; - if (context->quality != -1) { - quality = context->quality; - } - for (i = 0; i < context->qtablesLen; i++) { - jpeg_add_quant_table( - &context->cinfo, - i, - &context->qtables[i * DCTSIZE2], - quality, - FALSE); - context->cinfo.comp_info[i].quant_tbl_no = i; - last_q = i; - } - if (context->qtablesLen == 1) { - // jpeg_set_defaults created two qtables internally, but we only - // wanted one. - jpeg_add_quant_table( - &context->cinfo, 1, &context->qtables[0], quality, FALSE); - } - for (i = last_q; i < context->cinfo.num_components; i++) { - context->cinfo.comp_info[i].quant_tbl_no = last_q; - } - } else if (context->quality != -1) { - jpeg_set_quality(&context->cinfo, context->quality, TRUE); - } - - /* Set subsampling options */ - switch (context->subsampling) { - case 0: /* 1x1 1x1 1x1 (4:4:4) : None */ - { - context->cinfo.comp_info[0].h_samp_factor = 1; - context->cinfo.comp_info[0].v_samp_factor = 1; - context->cinfo.comp_info[1].h_samp_factor = 1; - context->cinfo.comp_info[1].v_samp_factor = 1; - context->cinfo.comp_info[2].h_samp_factor = 1; - context->cinfo.comp_info[2].v_samp_factor = 1; - break; - } - case 1: /* 2x1, 1x1, 1x1 (4:2:2) : Medium */ - { - context->cinfo.comp_info[0].h_samp_factor = 2; - context->cinfo.comp_info[0].v_samp_factor = 1; - context->cinfo.comp_info[1].h_samp_factor = 1; - context->cinfo.comp_info[1].v_samp_factor = 1; - context->cinfo.comp_info[2].h_samp_factor = 1; - context->cinfo.comp_info[2].v_samp_factor = 1; - break; - } - case 2: /* 2x2, 1x1, 1x1 (4:2:0) : High */ - { - context->cinfo.comp_info[0].h_samp_factor = 2; - context->cinfo.comp_info[0].v_samp_factor = 2; - context->cinfo.comp_info[1].h_samp_factor = 1; - context->cinfo.comp_info[1].v_samp_factor = 1; - context->cinfo.comp_info[2].h_samp_factor = 1; - context->cinfo.comp_info[2].v_samp_factor = 1; - break; - } - default: { - /* Use the lib's default */ - break; - } - } - if (context->progressive) { - jpeg_simple_progression(&context->cinfo); - } - context->cinfo.smoothing_factor = context->smooth; - context->cinfo.optimize_coding = (boolean)context->optimize; - context->cinfo.restart_interval = context->restart_marker_blocks; - context->cinfo.restart_in_rows = context->restart_marker_rows; - if (context->xdpi > 0 && context->ydpi > 0) { - context->cinfo.write_JFIF_header = TRUE; - context->cinfo.density_unit = 1; /* dots per inch */ - context->cinfo.X_density = context->xdpi; - context->cinfo.Y_density = context->ydpi; - } - switch (context->streamtype) { - case 1: - /* tables only */ - jpeg_write_tables(&context->cinfo); - goto cleanup; - case 2: - /* image only */ - jpeg_suppress_tables(&context->cinfo, TRUE); - jpeg_start_compress(&context->cinfo, FALSE); - /* suppress extra section */ - context->extra_offset = context->extra_size; - break; - default: - /* interchange stream */ - jpeg_start_compress(&context->cinfo, TRUE); - break; - } - state->state++; - /* fall through */ - - case 2: - // check for exif len + 'APP1' header bytes - if (context->rawExifLen + 5 > context->destination.pub.free_in_buffer) { - break; - } - // add exif header - if (context->rawExifLen > 0) { - jpeg_write_marker( - &context->cinfo, - JPEG_APP0 + 1, - (unsigned char *)context->rawExif, - context->rawExifLen); - } - - state->state++; - /* fall through */ - case 3: - - if (context->extra) { - /* copy extra buffer to output buffer */ - unsigned int n = context->extra_size - context->extra_offset; - if (n > context->destination.pub.free_in_buffer) { - n = context->destination.pub.free_in_buffer; - } - memcpy( - context->destination.pub.next_output_byte, - context->extra + context->extra_offset, - n); - context->destination.pub.next_output_byte += n; - context->destination.pub.free_in_buffer -= n; - context->extra_offset += n; - if (context->extra_offset >= context->extra_size) { - state->state++; - } else { - break; - } - } else { - state->state++; - } - - case 4: - - if (context->comment) { - jpeg_write_marker( - &context->cinfo, - JPEG_COM, - (unsigned char *)context->comment, - context->comment_size); - } - state->state++; - - case 5: - if (1024 > context->destination.pub.free_in_buffer) { - break; - } - - ok = 1; - while (state->y < state->ysize) { - state->shuffle( - state->buffer, - (UINT8 *)im->image[state->y + state->yoff] + - state->xoff * im->pixelsize, - state->xsize); - ok = jpeg_write_scanlines(&context->cinfo, &state->buffer, 1); - if (ok != 1) { - break; - } - state->y++; - } - - if (ok != 1) { - break; - } - state->state++; - /* fall through */ - - case 6: - - /* Finish compression */ - if (context->destination.pub.free_in_buffer < 100) { - break; - } - jpeg_finish_compress(&context->cinfo); - -cleanup: - /* Clean up */ - if (context->comment) { - free(context->comment); - context->comment = NULL; - } - if (context->extra) { - free(context->extra); - context->extra = NULL; - } - if (context->rawExif) { - free(context->rawExif); - context->rawExif = NULL; - } - if (context->qtables) { - free(context->qtables); - context->qtables = NULL; - } - - jpeg_destroy_compress(&context->cinfo); - /* if (jerr.pub.num_warnings) return BROKEN; */ - state->errcode = IMAGING_CODEC_END; - break; - } - - /* Return number of bytes in output buffer */ - return context->destination.pub.next_output_byte - buf; -} - -const char * -ImagingJpegVersion(void) { - static char version[20]; - sprintf(version, "%d.%d", JPEG_LIB_VERSION / 10, JPEG_LIB_VERSION % 10); - return version; -} - -#endif +/* + * The Python Imaging Library. + * $Id$ + * + * coder for JPEG data + * + * history: + * 1996-05-06 fl created + * 1996-07-16 fl don't drop last block of encoded data + * 1996-12-30 fl added quality and progressive settings + * 1997-01-08 fl added streamtype settings + * 1998-01-31 fl adapted to libjpeg 6a + * 1998-07-12 fl added YCbCr support + * 2001-04-16 fl added DPI write support + * + * Copyright (c) 1997-2001 by Secret Labs AB + * Copyright (c) 1995-1997 by Fredrik Lundh + * + * See the README file for details on usage and redistribution. + */ + +#include "Imaging.h" + +#ifdef HAVE_LIBJPEG + +#undef HAVE_PROTOTYPES +#undef HAVE_STDLIB_H +#undef HAVE_STDDEF_H +#undef UINT8 +#undef UINT16 +#undef UINT32 +#undef INT16 +#undef INT32 + +#include "Jpeg.h" + +/* -------------------------------------------------------------------- */ +/* Suspending output handler */ +/* -------------------------------------------------------------------- */ + +METHODDEF(void) +stub(j_compress_ptr cinfo) { /* empty */ } + +METHODDEF(boolean) +empty_output_buffer(j_compress_ptr cinfo) { + /* Suspension */ + return FALSE; +} + +GLOBAL(void) +jpeg_buffer_dest(j_compress_ptr cinfo, JPEGDESTINATION *destination) { + cinfo->dest = (void *)destination; + + destination->pub.init_destination = stub; + destination->pub.empty_output_buffer = empty_output_buffer; + destination->pub.term_destination = stub; +} + +/* -------------------------------------------------------------------- */ +/* Error handler */ +/* -------------------------------------------------------------------- */ + +METHODDEF(void) +error(j_common_ptr cinfo) { + JPEGERROR *error; + error = (JPEGERROR *)cinfo->err; + (*cinfo->err->output_message)(cinfo); + longjmp(error->setjmp_buffer, 1); +} + +/* -------------------------------------------------------------------- */ +/* Encoder */ +/* -------------------------------------------------------------------- */ + +int +ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { + JPEGENCODERSTATE *context = (JPEGENCODERSTATE *)state->context; + int ok; + + if (setjmp(context->error.setjmp_buffer)) { + /* JPEG error handler */ + jpeg_destroy_compress(&context->cinfo); + state->errcode = IMAGING_CODEC_BROKEN; + return -1; + } + + if (!state->state) { + /* Setup compression context (very similar to the decoder) */ + context->cinfo.err = jpeg_std_error(&context->error.pub); + context->error.pub.error_exit = error; + jpeg_create_compress(&context->cinfo); + jpeg_buffer_dest(&context->cinfo, &context->destination); + + context->extra_offset = 0; + + /* Ready to encode */ + state->state = 1; + } + + /* Load the destination buffer */ + context->destination.pub.next_output_byte = buf; + context->destination.pub.free_in_buffer = bytes; + + switch (state->state) { + case 1: + + context->cinfo.image_width = state->xsize; + context->cinfo.image_height = state->ysize; + + switch (state->bits) { + case 8: + context->cinfo.input_components = 1; + context->cinfo.in_color_space = JCS_GRAYSCALE; + break; + case 24: + context->cinfo.input_components = 3; + if (strcmp(im->mode, "YCbCr") == 0) { + context->cinfo.in_color_space = JCS_YCbCr; + } else { + context->cinfo.in_color_space = JCS_RGB; + } + break; + case 32: + context->cinfo.input_components = 4; + context->cinfo.in_color_space = JCS_CMYK; +#ifdef JCS_EXTENSIONS + if (strcmp(context->rawmode, "RGBX") == 0) { + context->cinfo.in_color_space = JCS_EXT_RGBX; + } +#endif + break; + default: + state->errcode = IMAGING_CODEC_CONFIG; + return -1; + } + + /* Compressor configuration */ + jpeg_set_defaults(&context->cinfo); + + /* Prevent RGB -> YCbCr conversion */ + if (context->keep_rgb) { + switch (context->cinfo.in_color_space) { + case JCS_RGB: +#ifdef JCS_EXTENSIONS + case JCS_EXT_RGBX: +#endif + switch (context->subsampling) { + case -1: /* Default */ + case 0: /* No subsampling */ + break; + default: + /* Would subsample the green and blue + channels, which doesn't make sense */ + state->errcode = IMAGING_CODEC_CONFIG; + return -1; + } + jpeg_set_colorspace(&context->cinfo, JCS_RGB); + break; + default: + break; + } + } + + /* Use custom quantization tables */ + if (context->qtables) { + int i; + int quality = 100; + int last_q = 0; + if (context->quality != -1) { + quality = context->quality; + } + for (i = 0; i < context->qtablesLen; i++) { + jpeg_add_quant_table( + &context->cinfo, + i, + &context->qtables[i * DCTSIZE2], + quality, + FALSE); + context->cinfo.comp_info[i].quant_tbl_no = i; + last_q = i; + } + if (context->qtablesLen == 1) { + // jpeg_set_defaults created two qtables internally, but we only + // wanted one. + jpeg_add_quant_table( + &context->cinfo, 1, &context->qtables[0], quality, FALSE); + } + for (i = last_q; i < context->cinfo.num_components; i++) { + context->cinfo.comp_info[i].quant_tbl_no = last_q; + } + } else if (context->quality != -1) { + jpeg_set_quality(&context->cinfo, context->quality, TRUE); + } + + /* Set subsampling options */ + switch (context->subsampling) { + case 0: /* 1x1 1x1 1x1 (4:4:4) : None */ + { + context->cinfo.comp_info[0].h_samp_factor = 1; + context->cinfo.comp_info[0].v_samp_factor = 1; + context->cinfo.comp_info[1].h_samp_factor = 1; + context->cinfo.comp_info[1].v_samp_factor = 1; + context->cinfo.comp_info[2].h_samp_factor = 1; + context->cinfo.comp_info[2].v_samp_factor = 1; + break; + } + case 1: /* 2x1, 1x1, 1x1 (4:2:2) : Medium */ + { + context->cinfo.comp_info[0].h_samp_factor = 2; + context->cinfo.comp_info[0].v_samp_factor = 1; + context->cinfo.comp_info[1].h_samp_factor = 1; + context->cinfo.comp_info[1].v_samp_factor = 1; + context->cinfo.comp_info[2].h_samp_factor = 1; + context->cinfo.comp_info[2].v_samp_factor = 1; + break; + } + case 2: /* 2x2, 1x1, 1x1 (4:2:0) : High */ + { + context->cinfo.comp_info[0].h_samp_factor = 2; + context->cinfo.comp_info[0].v_samp_factor = 2; + context->cinfo.comp_info[1].h_samp_factor = 1; + context->cinfo.comp_info[1].v_samp_factor = 1; + context->cinfo.comp_info[2].h_samp_factor = 1; + context->cinfo.comp_info[2].v_samp_factor = 1; + break; + } + default: { + /* Use the lib's default */ + break; + } + } + if (context->progressive) { + jpeg_simple_progression(&context->cinfo); + } + context->cinfo.smoothing_factor = context->smooth; + context->cinfo.optimize_coding = (boolean)context->optimize; + context->cinfo.restart_interval = context->restart_marker_blocks; + context->cinfo.restart_in_rows = context->restart_marker_rows; + if (context->xdpi > 0 && context->ydpi > 0) { + context->cinfo.write_JFIF_header = TRUE; + context->cinfo.density_unit = 1; /* dots per inch */ + context->cinfo.X_density = context->xdpi; + context->cinfo.Y_density = context->ydpi; + } + switch (context->streamtype) { + case 1: + /* tables only */ + jpeg_write_tables(&context->cinfo); + goto cleanup; + case 2: + /* image only */ + jpeg_suppress_tables(&context->cinfo, TRUE); + jpeg_start_compress(&context->cinfo, FALSE); + /* suppress extra section */ + context->extra_offset = context->extra_size; + break; + default: + /* interchange stream */ + jpeg_start_compress(&context->cinfo, TRUE); + break; + } + state->state++; + /* fall through */ + + case 2: + // check for exif len + 'APP1' header bytes + if (context->rawExifLen + 5 > context->destination.pub.free_in_buffer) { + break; + } + // add exif header + if (context->rawExifLen > 0) { + jpeg_write_marker( + &context->cinfo, + JPEG_APP0 + 1, + (unsigned char *)context->rawExif, + context->rawExifLen); + } + + state->state++; + /* fall through */ + case 3: + + if (context->extra) { + /* copy extra buffer to output buffer */ + unsigned int n = context->extra_size - context->extra_offset; + if (n > context->destination.pub.free_in_buffer) { + n = context->destination.pub.free_in_buffer; + } + memcpy( + context->destination.pub.next_output_byte, + context->extra + context->extra_offset, + n); + context->destination.pub.next_output_byte += n; + context->destination.pub.free_in_buffer -= n; + context->extra_offset += n; + if (context->extra_offset >= context->extra_size) { + state->state++; + } else { + break; + } + } else { + state->state++; + } + + case 4: + + if (context->comment) { + jpeg_write_marker( + &context->cinfo, + JPEG_COM, + (unsigned char *)context->comment, + context->comment_size); + } + state->state++; + + case 5: + if (1024 > context->destination.pub.free_in_buffer) { + break; + } + + ok = 1; + while (state->y < state->ysize) { + state->shuffle( + state->buffer, + (UINT8 *)im->image[state->y + state->yoff] + + state->xoff * im->pixelsize, + state->xsize); + ok = jpeg_write_scanlines(&context->cinfo, &state->buffer, 1); + if (ok != 1) { + break; + } + state->y++; + } + + if (ok != 1) { + break; + } + state->state++; + /* fall through */ + + case 6: + + /* Finish compression */ + if (context->destination.pub.free_in_buffer < 100) { + break; + } + jpeg_finish_compress(&context->cinfo); + +cleanup: + /* Clean up */ + if (context->comment) { + free(context->comment); + context->comment = NULL; + } + if (context->extra) { + free(context->extra); + context->extra = NULL; + } + if (context->rawExif) { + free(context->rawExif); + context->rawExif = NULL; + } + if (context->qtables) { + free(context->qtables); + context->qtables = NULL; + } + + jpeg_destroy_compress(&context->cinfo); + /* if (jerr.pub.num_warnings) return BROKEN; */ + state->errcode = IMAGING_CODEC_END; + break; + } + + /* Return number of bytes in output buffer */ + return context->destination.pub.next_output_byte - buf; +} + +const char * +ImagingJpegVersion(void) { + static char version[20]; + sprintf(version, "%d.%d", JPEG_LIB_VERSION / 10, JPEG_LIB_VERSION % 10); + return version; +} + +#endif diff --git a/src/libImaging/Matrix.c b/src/libImaging/Matrix.c index ec7f4d93e..0ed636b8b 100644 --- a/src/libImaging/Matrix.c +++ b/src/libImaging/Matrix.c @@ -1,78 +1,78 @@ -/* - * The Python Imaging Library - * $Id$ - * - * colour and luminance matrix transforms - * - * history: - * 1996-05-18 fl: created (brute force implementation) - * - * Copyright (c) Fredrik Lundh 1996. - * Copyright (c) Secret Labs AB 1997. - * - * See the README file for information on usage and redistribution. - */ - -#include "Imaging.h" - -#define CLIPF(v) ((v <= 0.0) ? 0 : (v >= 255.0F) ? 255 : (UINT8)v) - -Imaging -ImagingConvertMatrix(Imaging im, const char *mode, float m[]) { - Imaging imOut; - int x, y; - ImagingSectionCookie cookie; - - /* Assume there's enough data in the buffer */ - if (!im || im->bands != 3) { - return (Imaging)ImagingError_ModeError(); - } - - if (strcmp(mode, "L") == 0) { - imOut = ImagingNewDirty("L", im->xsize, im->ysize); - if (!imOut) { - return NULL; - } - - ImagingSectionEnter(&cookie); - for (y = 0; y < im->ysize; y++) { - UINT8 *in = (UINT8 *)im->image[y]; - UINT8 *out = (UINT8 *)imOut->image[y]; - - for (x = 0; x < im->xsize; x++) { - float v = m[0] * in[0] + m[1] * in[1] + m[2] * in[2] + m[3] + 0.5; - out[x] = CLIPF(v); - in += 4; - } - } - ImagingSectionLeave(&cookie); - - } else if (strlen(mode) == 3) { - imOut = ImagingNewDirty(mode, im->xsize, im->ysize); - if (!imOut) { - return NULL; - } - - for (y = 0; y < im->ysize; y++) { - UINT8 *in = (UINT8 *)im->image[y]; - UINT8 *out = (UINT8 *)imOut->image[y]; - - ImagingSectionEnter(&cookie); - for (x = 0; x < im->xsize; x++) { - float v0 = m[0] * in[0] + m[1] * in[1] + m[2] * in[2] + m[3] + 0.5; - float v1 = m[4] * in[0] + m[5] * in[1] + m[6] * in[2] + m[7] + 0.5; - float v2 = m[8] * in[0] + m[9] * in[1] + m[10] * in[2] + m[11] + 0.5; - out[0] = CLIPF(v0); - out[1] = CLIPF(v1); - out[2] = CLIPF(v2); - in += 4; - out += 4; - } - ImagingSectionLeave(&cookie); - } - } else { - return (Imaging)ImagingError_ModeError(); - } - - return imOut; -} +/* + * The Python Imaging Library + * $Id$ + * + * colour and luminance matrix transforms + * + * history: + * 1996-05-18 fl: created (brute force implementation) + * + * Copyright (c) Fredrik Lundh 1996. + * Copyright (c) Secret Labs AB 1997. + * + * See the README file for information on usage and redistribution. + */ + +#include "Imaging.h" + +#define CLIPF(v) ((v <= 0.0) ? 0 : (v >= 255.0F) ? 255 : (UINT8)v) + +Imaging +ImagingConvertMatrix(Imaging im, const char *mode, float m[]) { + Imaging imOut; + int x, y; + ImagingSectionCookie cookie; + + /* Assume there's enough data in the buffer */ + if (!im || im->bands != 3) { + return (Imaging)ImagingError_ModeError(); + } + + if (strcmp(mode, "L") == 0) { + imOut = ImagingNewDirty("L", im->xsize, im->ysize); + if (!imOut) { + return NULL; + } + + ImagingSectionEnter(&cookie); + for (y = 0; y < im->ysize; y++) { + UINT8 *in = (UINT8 *)im->image[y]; + UINT8 *out = (UINT8 *)imOut->image[y]; + + for (x = 0; x < im->xsize; x++) { + float v = m[0] * in[0] + m[1] * in[1] + m[2] * in[2] + m[3] + 0.5; + out[x] = CLIPF(v); + in += 4; + } + } + ImagingSectionLeave(&cookie); + + } else if (strlen(mode) == 3) { + imOut = ImagingNewDirty(mode, im->xsize, im->ysize); + if (!imOut) { + return NULL; + } + + for (y = 0; y < im->ysize; y++) { + UINT8 *in = (UINT8 *)im->image[y]; + UINT8 *out = (UINT8 *)imOut->image[y]; + + ImagingSectionEnter(&cookie); + for (x = 0; x < im->xsize; x++) { + float v0 = m[0] * in[0] + m[1] * in[1] + m[2] * in[2] + m[3] + 0.5; + float v1 = m[4] * in[0] + m[5] * in[1] + m[6] * in[2] + m[7] + 0.5; + float v2 = m[8] * in[0] + m[9] * in[1] + m[10] * in[2] + m[11] + 0.5; + out[0] = CLIPF(v0); + out[1] = CLIPF(v1); + out[2] = CLIPF(v2); + in += 4; + out += 4; + } + ImagingSectionLeave(&cookie); + } + } else { + return (Imaging)ImagingError_ModeError(); + } + + return imOut; +} diff --git a/src/libImaging/ModeFilter.c b/src/libImaging/ModeFilter.c index 757cbc3fb..9f5486eb4 100644 --- a/src/libImaging/ModeFilter.c +++ b/src/libImaging/ModeFilter.c @@ -1,81 +1,81 @@ -/* - * The Python Imaging Library - * $Id$ - * - * mode filter - * - * history: - * 2002-06-08 fl Created (based on code from IFUNC95) - * 2004-10-05 fl Rewritten; use a simpler brute-force algorithm - * - * Copyright (c) Secret Labs AB 2002-2004. All rights reserved. - * - * See the README file for information on usage and redistribution. - */ - -#include "Imaging.h" - -Imaging -ImagingModeFilter(Imaging im, int size) { - Imaging imOut; - int x, y, i; - int xx, yy; - int maxcount; - UINT8 maxpixel; - int histogram[256]; - - if (!im || im->bands != 1 || im->type != IMAGING_TYPE_UINT8) { - return (Imaging)ImagingError_ModeError(); - } - - imOut = ImagingNewDirty(im->mode, im->xsize, im->ysize); - if (!imOut) { - return NULL; - } - - size = size / 2; - - for (y = 0; y < imOut->ysize; y++) { - UINT8 *out = &IMAGING_PIXEL_L(imOut, 0, y); - for (x = 0; x < imOut->xsize; x++) { - /* calculate histogram over current area */ - - /* FIXME: brute force! to improve, update the histogram - incrementally. may also add a "frequent list", like in - the old implementation, but I'm not sure that's worth - the added complexity... */ - - memset(histogram, 0, sizeof(histogram)); - for (yy = y - size; yy <= y + size; yy++) { - if (yy >= 0 && yy < imOut->ysize) { - UINT8 *in = &IMAGING_PIXEL_L(im, 0, yy); - for (xx = x - size; xx <= x + size; xx++) { - if (xx >= 0 && xx < imOut->xsize) { - histogram[in[xx]]++; - } - } - } - } - - /* find most frequent pixel value in this region */ - maxpixel = 0; - maxcount = histogram[maxpixel]; - for (i = 1; i < 256; i++) { - if (histogram[i] > maxcount) { - maxcount = histogram[i]; - maxpixel = (UINT8)i; - } - } - - if (maxcount > 2) { - out[x] = maxpixel; - } else { - out[x] = IMAGING_PIXEL_L(im, x, y); - } - } - } - - ImagingCopyPalette(imOut, im); - - return imOut; -} +/* + * The Python Imaging Library + * $Id$ + * + * mode filter + * + * history: + * 2002-06-08 fl Created (based on code from IFUNC95) + * 2004-10-05 fl Rewritten; use a simpler brute-force algorithm + * + * Copyright (c) Secret Labs AB 2002-2004. All rights reserved. + * + * See the README file for information on usage and redistribution. + */ + +#include "Imaging.h" + +Imaging +ImagingModeFilter(Imaging im, int size) { + Imaging imOut; + int x, y, i; + int xx, yy; + int maxcount; + UINT8 maxpixel; + int histogram[256]; + + if (!im || im->bands != 1 || im->type != IMAGING_TYPE_UINT8) { + return (Imaging)ImagingError_ModeError(); + } + + imOut = ImagingNewDirty(im->mode, im->xsize, im->ysize); + if (!imOut) { + return NULL; + } + + size = size / 2; + + for (y = 0; y < imOut->ysize; y++) { + UINT8 *out = &IMAGING_PIXEL_L(imOut, 0, y); + for (x = 0; x < imOut->xsize; x++) { + /* calculate histogram over current area */ + + /* FIXME: brute force! to improve, update the histogram + incrementally. may also add a "frequent list", like in + the old implementation, but I'm not sure that's worth + the added complexity... */ + + memset(histogram, 0, sizeof(histogram)); + for (yy = y - size; yy <= y + size; yy++) { + if (yy >= 0 && yy < imOut->ysize) { + UINT8 *in = &IMAGING_PIXEL_L(im, 0, yy); + for (xx = x - size; xx <= x + size; xx++) { + if (xx >= 0 && xx < imOut->xsize) { + histogram[in[xx]]++; + } + } + } + } + + /* find most frequent pixel value in this region */ + maxpixel = 0; + maxcount = histogram[maxpixel]; + for (i = 1; i < 256; i++) { + if (histogram[i] > maxcount) { + maxcount = histogram[i]; + maxpixel = (UINT8)i; + } + } + + if (maxcount > 2) { + out[x] = maxpixel; + } else { + out[x] = IMAGING_PIXEL_L(im, x, y); + } + } + } + + ImagingCopyPalette(imOut, im); + + return imOut; +} diff --git a/src/libImaging/Negative.c b/src/libImaging/Negative.c index 70b96c397..e0868134d 100644 --- a/src/libImaging/Negative.c +++ b/src/libImaging/Negative.c @@ -1,42 +1,42 @@ -/* - * The Python Imaging Library - * $Id$ - * - * negate image - * - * to do: - * FIXME: Maybe this should be implemented using ImagingPoint() - * - * history: - * 95-11-27 fl: Created - * - * Copyright (c) Fredrik Lundh 1995. - * Copyright (c) Secret Labs AB 1997. - * - * See the README file for information on usage and redistribution. - */ - -#include "Imaging.h" - -Imaging -ImagingNegative(Imaging im) { - Imaging imOut; - int x, y; - - if (!im) { - return (Imaging)ImagingError_ModeError(); - } - - imOut = ImagingNewDirty(im->mode, im->xsize, im->ysize); - if (!imOut) { - return NULL; - } - - for (y = 0; y < im->ysize; y++) { - for (x = 0; x < im->linesize; x++) { - imOut->image[y][x] = ~im->image[y][x]; - } - } - - return imOut; -} +/* + * The Python Imaging Library + * $Id$ + * + * negate image + * + * to do: + * FIXME: Maybe this should be implemented using ImagingPoint() + * + * history: + * 95-11-27 fl: Created + * + * Copyright (c) Fredrik Lundh 1995. + * Copyright (c) Secret Labs AB 1997. + * + * See the README file for information on usage and redistribution. + */ + +#include "Imaging.h" + +Imaging +ImagingNegative(Imaging im) { + Imaging imOut; + int x, y; + + if (!im) { + return (Imaging)ImagingError_ModeError(); + } + + imOut = ImagingNewDirty(im->mode, im->xsize, im->ysize); + if (!imOut) { + return NULL; + } + + for (y = 0; y < im->ysize; y++) { + for (x = 0; x < im->linesize; x++) { + imOut->image[y][x] = ~im->image[y][x]; + } + } + + return imOut; +} diff --git a/src/libImaging/Offset.c b/src/libImaging/Offset.c index 91ee91083..88a894c9c 100644 --- a/src/libImaging/Offset.c +++ b/src/libImaging/Offset.c @@ -1,64 +1,64 @@ -/* - * The Python Imaging Library - * $Id$ - * - * offset an image in x and y directions - * - * history: - * 96-07-22 fl: Created - * 98-11-01 cgw@pgt.com: Fixed negative-array index bug - * - * Copyright (c) Fredrik Lundh 1996. - * Copyright (c) Secret Labs AB 1997. - * - * See the README file for information on usage and redistribution. - */ - -#include "Imaging.h" - -Imaging -ImagingOffset(Imaging im, int xoffset, int yoffset) { - int x, y; - Imaging imOut; - - if (!im) { - return (Imaging)ImagingError_ModeError(); - } - - imOut = ImagingNewDirty(im->mode, im->xsize, im->ysize); - if (!imOut) { - return NULL; - } - - ImagingCopyPalette(imOut, im); - - /* make offsets positive to avoid negative coordinates */ - xoffset %= im->xsize; - xoffset = im->xsize - xoffset; - if (xoffset < 0) { - xoffset += im->xsize; - } - - yoffset %= im->ysize; - yoffset = im->ysize - yoffset; - if (yoffset < 0) { - yoffset += im->ysize; - } - -#define OFFSET(image) \ - for (y = 0; y < im->ysize; y++) { \ - for (x = 0; x < im->xsize; x++) { \ - int yi = (y + yoffset) % im->ysize; \ - int xi = (x + xoffset) % im->xsize; \ - imOut->image[y][x] = im->image[yi][xi]; \ - } \ - } - - if (im->image8) { - OFFSET(image8) - } else { - OFFSET(image32) - } - - return imOut; -} +/* + * The Python Imaging Library + * $Id$ + * + * offset an image in x and y directions + * + * history: + * 96-07-22 fl: Created + * 98-11-01 cgw@pgt.com: Fixed negative-array index bug + * + * Copyright (c) Fredrik Lundh 1996. + * Copyright (c) Secret Labs AB 1997. + * + * See the README file for information on usage and redistribution. + */ + +#include "Imaging.h" + +Imaging +ImagingOffset(Imaging im, int xoffset, int yoffset) { + int x, y; + Imaging imOut; + + if (!im) { + return (Imaging)ImagingError_ModeError(); + } + + imOut = ImagingNewDirty(im->mode, im->xsize, im->ysize); + if (!imOut) { + return NULL; + } + + ImagingCopyPalette(imOut, im); + + /* make offsets positive to avoid negative coordinates */ + xoffset %= im->xsize; + xoffset = im->xsize - xoffset; + if (xoffset < 0) { + xoffset += im->xsize; + } + + yoffset %= im->ysize; + yoffset = im->ysize - yoffset; + if (yoffset < 0) { + yoffset += im->ysize; + } + +#define OFFSET(image) \ + for (y = 0; y < im->ysize; y++) { \ + for (x = 0; x < im->xsize; x++) { \ + int yi = (y + yoffset) % im->ysize; \ + int xi = (x + xoffset) % im->xsize; \ + imOut->image[y][x] = im->image[yi][xi]; \ + } \ + } + + if (im->image8) { + OFFSET(image8) + } else { + OFFSET(image32) + } + + return imOut; +} diff --git a/src/libImaging/Pack.c b/src/libImaging/Pack.c index f3b714215..a232fd9fe 100644 --- a/src/libImaging/Pack.c +++ b/src/libImaging/Pack.c @@ -1,693 +1,693 @@ -/* - * The Python Imaging Library. - * $Id$ - * - * code to pack raw data - * - * history: - * 1996-04-30 fl Created - * 1996-05-12 fl Published a few RGB packers - * 1996-11-01 fl More RGB packers (Tk booster stuff) - * 1996-12-30 fl Added P;1, P;2 and P;4 packers - * 1997-06-02 fl Added F (F;32NF) packer - * 1997-08-28 fl Added 1 as L packer - * 1998-02-08 fl Added I packer - * 1998-03-09 fl Added mode field, RGBA/RGBX as RGB packers - * 1998-07-01 fl Added YCbCr support - * 1998-07-12 fl Added I 16 packer - * 1999-02-03 fl Added BGR packers - * 2003-09-26 fl Added LA/PA packers - * 2006-06-22 fl Added CMYK;I packer - * - * Copyright (c) 1997-2006 by Secret Labs AB. - * Copyright (c) 1996-1997 by Fredrik Lundh. - * - * See the README file for information on usage and redistribution. - */ - -#include "Imaging.h" - -#define R 0 -#define G 1 -#define B 2 -#define X 3 -#define A 3 - -#define C 0 -#define M 1 -#define Y 2 -#define K 3 - -/* byte swapping macros */ - -#define C16N (out[0] = tmp[0], out[1] = tmp[1]); -#define C16S (out[1] = tmp[0], out[0] = tmp[1]); -#define C32N (out[0] = tmp[0], out[1] = tmp[1], out[2] = tmp[2], out[3] = tmp[3]); -#define C32S (out[3] = tmp[0], out[2] = tmp[1], out[1] = tmp[2], out[0] = tmp[3]); -#define C64N \ - (out[0] = tmp[0], \ - out[1] = tmp[1], \ - out[2] = tmp[2], \ - out[3] = tmp[3], \ - out[4] = tmp[4], \ - out[5] = tmp[5], \ - out[6] = tmp[6], \ - out[7] = tmp[7]); -#define C64S \ - (out[7] = tmp[0], \ - out[6] = tmp[1], \ - out[5] = tmp[2], \ - out[4] = tmp[3], \ - out[3] = tmp[4], \ - out[2] = tmp[5], \ - out[1] = tmp[6], \ - out[0] = tmp[7]); - -#ifdef WORDS_BIGENDIAN -#define C16B C16N -#define C16L C16S -#define C32B C32N -#define C32L C32S -#define C64B C64N -#define C64L C64S -#else -#define C16B C16S -#define C16L C16N -#define C32B C32S -#define C32L C32N -#define C64B C64S -#define C64L C64N -#endif - -static void -pack1(UINT8 *out, const UINT8 *in, int pixels) { - int i, m, b; - /* bilevel (black is 0) */ - b = 0; - m = 128; - for (i = 0; i < pixels; i++) { - if (in[i] != 0) { - b |= m; - } - m >>= 1; - if (m == 0) { - *out++ = b; - b = 0; - m = 128; - } - } - if (m != 128) { - *out++ = b; - } -} - -static void -pack1I(UINT8 *out, const UINT8 *in, int pixels) { - int i, m, b; - /* bilevel (black is 1) */ - b = 0; - m = 128; - for (i = 0; i < pixels; i++) { - if (in[i] == 0) { - b |= m; - } - m >>= 1; - if (m == 0) { - *out++ = b; - b = 0; - m = 128; - } - } - if (m != 128) { - *out++ = b; - } -} - -static void -pack1R(UINT8 *out, const UINT8 *in, int pixels) { - int i, m, b; - /* bilevel, lsb first (black is 0) */ - b = 0; - m = 1; - for (i = 0; i < pixels; i++) { - if (in[i] != 0) { - b |= m; - } - m <<= 1; - if (m == 256) { - *out++ = b; - b = 0; - m = 1; - } - } - if (m != 1) { - *out++ = b; - } -} - -static void -pack1IR(UINT8 *out, const UINT8 *in, int pixels) { - int i, m, b; - /* bilevel, lsb first (black is 1) */ - b = 0; - m = 1; - for (i = 0; i < pixels; i++) { - if (in[i] == 0) { - b |= m; - } - m <<= 1; - if (m == 256) { - *out++ = b; - b = 0; - m = 1; - } - } - if (m != 1) { - *out++ = b; - } -} - -static void -pack1L(UINT8 *out, const UINT8 *in, int pixels) { - int i; - /* bilevel, stored as bytes */ - for (i = 0; i < pixels; i++) { - out[i] = (in[i] != 0) ? 255 : 0; - } -} - -static void -packP4(UINT8 *out, const UINT8 *in, int pixels) { - while (pixels >= 2) { - *out++ = (in[0] << 4) | (in[1] & 15); - in += 2; - pixels -= 2; - } - - if (pixels) { - out[0] = (in[0] << 4); - } -} - -static void -packP2(UINT8 *out, const UINT8 *in, int pixels) { - while (pixels >= 4) { - *out++ = (in[0] << 6) | ((in[1] & 3) << 4) | ((in[2] & 3) << 2) | (in[3] & 3); - in += 4; - pixels -= 4; - } - - switch (pixels) { - case 3: - out[0] = (in[0] << 6) | ((in[1] & 3) << 4) | ((in[2] & 3) << 2); - break; - case 2: - out[0] = (in[0] << 6) | ((in[1] & 3) << 4); - break; - case 1: - out[0] = (in[0] << 6); - } -} - -static void -packL16(UINT8 *out, const UINT8 *in, int pixels) { - int i; - /* L -> L;16, e.g: \xff77 -> \x00\xff\x00\x77 */ - for (i = 0; i < pixels; i++) { - out[0] = 0; - out[1] = in[i]; - out += 2; - } -} - -static void -packL16B(UINT8 *out, const UINT8 *in, int pixels) { - int i; - /* L -> L;16B, e.g: \xff77 -> \xff\x00\x77\x00 */ - for (i = 0; i < pixels; i++) { - out[0] = in[i]; - out[1] = 0; - out += 2; - } -} - -static void -packLA(UINT8 *out, const UINT8 *in, int pixels) { - int i; - /* LA, pixel interleaved */ - for (i = 0; i < pixels; i++) { - out[0] = in[R]; - out[1] = in[A]; - out += 2; - in += 4; - } -} - -static void -packLAL(UINT8 *out, const UINT8 *in, int pixels) { - int i; - /* LA, line interleaved */ - for (i = 0; i < pixels; i++) { - out[i] = in[R]; - out[i + pixels] = in[A]; - in += 4; - } -} - -void -ImagingPackRGB(UINT8 *out, const UINT8 *in, int pixels) { - int i = 0; - /* RGB triplets */ -#ifdef __sparc - /* SPARC CPUs cannot read integers from nonaligned addresses. */ - for (; i < pixels; i++) { - out[0] = in[R]; - out[1] = in[G]; - out[2] = in[B]; - out += 3; - in += 4; - } -#else - for (; i < pixels - 1; i++) { - memcpy(out, in + i * 4, 4); - out += 3; - } - for (; i < pixels; i++) { - out[0] = in[i * 4 + R]; - out[1] = in[i * 4 + G]; - out[2] = in[i * 4 + B]; - out += 3; - } -#endif -} - -void -ImagingPackXRGB(UINT8 *out, const UINT8 *in, int pixels) { - int i; - /* XRGB, triplets with left padding */ - for (i = 0; i < pixels; i++) { - out[0] = 0; - out[1] = in[R]; - out[2] = in[G]; - out[3] = in[B]; - out += 4; - in += 4; - } -} - -void -ImagingPackBGR(UINT8 *out, const UINT8 *in, int pixels) { - int i; - /* RGB, reversed bytes */ - for (i = 0; i < pixels; i++) { - out[0] = in[B]; - out[1] = in[G]; - out[2] = in[R]; - out += 3; - in += 4; - } -} - -void -ImagingPackBGRX(UINT8 *out, const UINT8 *in, int pixels) { - int i; - /* BGRX, reversed bytes with right padding */ - for (i = 0; i < pixels; i++) { - out[0] = in[B]; - out[1] = in[G]; - out[2] = in[R]; - out[3] = 0; - out += 4; - in += 4; - } -} - -void -ImagingPackXBGR(UINT8 *out, const UINT8 *in, int pixels) { - int i; - /* XBGR, reversed bytes with left padding */ - for (i = 0; i < pixels; i++) { - out[0] = 0; - out[1] = in[B]; - out[2] = in[G]; - out[3] = in[R]; - out += 4; - in += 4; - } -} - -void -ImagingPackBGRA(UINT8 *out, const UINT8 *in, int pixels) { - int i; - /* BGRA, reversed bytes with right alpha */ - for (i = 0; i < pixels; i++) { - out[0] = in[B]; - out[1] = in[G]; - out[2] = in[R]; - out[3] = in[A]; - out += 4; - in += 4; - } -} - -void -ImagingPackABGR(UINT8 *out, const UINT8 *in, int pixels) { - int i; - /* ABGR, reversed bytes with left alpha */ - for (i = 0; i < pixels; i++) { - out[0] = in[A]; - out[1] = in[B]; - out[2] = in[G]; - out[3] = in[R]; - out += 4; - in += 4; - } -} - -void -ImagingPackBGRa(UINT8 *out, const UINT8 *in, int pixels) { - int i; - /* BGRa, reversed bytes with premultiplied alpha */ - for (i = 0; i < pixels; i++) { - int alpha = out[3] = in[A]; - int tmp; - out[0] = MULDIV255(in[B], alpha, tmp); - out[1] = MULDIV255(in[G], alpha, tmp); - out[2] = MULDIV255(in[R], alpha, tmp); - out += 4; - in += 4; - } -} - -static void -packRGBL(UINT8 *out, const UINT8 *in, int pixels) { - int i; - /* RGB, line interleaved */ - for (i = 0; i < pixels; i++) { - out[i] = in[R]; - out[i + pixels] = in[G]; - out[i + pixels + pixels] = in[B]; - in += 4; - } -} - -static void -packRGBXL(UINT8 *out, const UINT8 *in, int pixels) { - int i; - /* RGBX, line interleaved */ - for (i = 0; i < pixels; i++) { - out[i] = in[R]; - out[i + pixels] = in[G]; - out[i + pixels + pixels] = in[B]; - out[i + pixels + pixels + pixels] = in[X]; - in += 4; - } -} - -static void -packI16B(UINT8 *out, const UINT8 *in_, int pixels) { - int i; - UINT16 tmp_; - UINT8 *tmp = (UINT8 *)&tmp_; - for (i = 0; i < pixels; i++) { - INT32 in; - memcpy(&in, in_, sizeof(in)); - if (in <= 0) { - tmp_ = 0; - } else if (in > 65535) { - tmp_ = 65535; - } else { - tmp_ = in; - } - C16B; - out += 2; - in_ += sizeof(in); - } -} - -static void -packI16N_I16B(UINT8 *out, const UINT8 *in, int pixels) { - int i; - UINT8 *tmp = (UINT8 *)in; - for (i = 0; i < pixels; i++) { - C16B; - out += 2; - tmp += 2; - } -} -static void -packI16N_I16(UINT8 *out, const UINT8 *in, int pixels) { - int i; - UINT8 *tmp = (UINT8 *)in; - for (i = 0; i < pixels; i++) { - C16L; - out += 2; - tmp += 2; - } -} - -static void -packI32S(UINT8 *out, const UINT8 *in, int pixels) { - int i; - UINT8 *tmp = (UINT8 *)in; - for (i = 0; i < pixels; i++) { - C32L; - out += 4; - tmp += 4; - } -} - -void -ImagingPackLAB(UINT8 *out, const UINT8 *in, int pixels) { - int i; - /* LAB triplets */ - for (i = 0; i < pixels; i++) { - out[0] = in[0]; - out[1] = in[1] ^ 128; /* signed in outside world */ - out[2] = in[2] ^ 128; - out += 3; - in += 4; - } -} - -static void -copy1(UINT8 *out, const UINT8 *in, int pixels) { - /* L, P */ - memcpy(out, in, pixels); -} - -static void -copy2(UINT8 *out, const UINT8 *in, int pixels) { - /* I;16, etc */ - memcpy(out, in, pixels * 2); -} - -static void -copy3(UINT8 *out, const UINT8 *in, int pixels) { - /* BGR;24, etc */ - memcpy(out, in, pixels * 3); -} - -static void -copy4(UINT8 *out, const UINT8 *in, int pixels) { - /* RGBA, CMYK quadruples */ - memcpy(out, in, 4 * pixels); -} - -static void -copy4I(UINT8 *out, const UINT8 *in, int pixels) { - /* RGBA, CMYK quadruples, inverted */ - int i; - for (i = 0; i < pixels * 4; i++) { - out[i] = ~in[i]; - } -} - -static void -band0(UINT8 *out, const UINT8 *in, int pixels) { - int i; - for (i = 0; i < pixels; i++, in += 4) { - out[i] = in[0]; - } -} - -static void -band1(UINT8 *out, const UINT8 *in, int pixels) { - int i; - for (i = 0; i < pixels; i++, in += 4) { - out[i] = in[1]; - } -} - -static void -band2(UINT8 *out, const UINT8 *in, int pixels) { - int i; - for (i = 0; i < pixels; i++, in += 4) { - out[i] = in[2]; - } -} - -static void -band3(UINT8 *out, const UINT8 *in, int pixels) { - int i; - for (i = 0; i < pixels; i++, in += 4) { - out[i] = in[3]; - } -} - -static struct { - const char *mode; - const char *rawmode; - int bits; - ImagingShuffler pack; -} packers[] = { - - /* bilevel */ - {"1", "1", 1, pack1}, - {"1", "1;I", 1, pack1I}, - {"1", "1;R", 1, pack1R}, - {"1", "1;IR", 1, pack1IR}, - {"1", "L", 8, pack1L}, - - /* grayscale */ - {"L", "L", 8, copy1}, - {"L", "L;16", 16, packL16}, - {"L", "L;16B", 16, packL16B}, - - /* grayscale w. alpha */ - {"LA", "LA", 16, packLA}, - {"LA", "LA;L", 16, packLAL}, - - /* grayscale w. alpha premultiplied */ - {"La", "La", 16, packLA}, - - /* palette */ - {"P", "P;1", 1, pack1}, - {"P", "P;2", 2, packP2}, - {"P", "P;4", 4, packP4}, - {"P", "P", 8, copy1}, - - /* palette w. alpha */ - {"PA", "PA", 16, packLA}, - {"PA", "PA;L", 16, packLAL}, - - /* true colour */ - {"RGB", "RGB", 24, ImagingPackRGB}, - {"RGB", "RGBX", 32, copy4}, - {"RGB", "RGBA", 32, copy4}, - {"RGB", "XRGB", 32, ImagingPackXRGB}, - {"RGB", "BGR", 24, ImagingPackBGR}, - {"RGB", "BGRX", 32, ImagingPackBGRX}, - {"RGB", "XBGR", 32, ImagingPackXBGR}, - {"RGB", "RGB;L", 24, packRGBL}, - {"RGB", "R", 8, band0}, - {"RGB", "G", 8, band1}, - {"RGB", "B", 8, band2}, - - /* true colour w. alpha */ - {"RGBA", "RGBA", 32, copy4}, - {"RGBA", "RGBA;L", 32, packRGBXL}, - {"RGBA", "RGB", 24, ImagingPackRGB}, - {"RGBA", "BGR", 24, ImagingPackBGR}, - {"RGBA", "BGRA", 32, ImagingPackBGRA}, - {"RGBA", "ABGR", 32, ImagingPackABGR}, - {"RGBA", "BGRa", 32, ImagingPackBGRa}, - {"RGBA", "R", 8, band0}, - {"RGBA", "G", 8, band1}, - {"RGBA", "B", 8, band2}, - {"RGBA", "A", 8, band3}, - - /* true colour w. alpha premultiplied */ - {"RGBa", "RGBa", 32, copy4}, - {"RGBa", "BGRa", 32, ImagingPackBGRA}, - {"RGBa", "aBGR", 32, ImagingPackABGR}, - - /* true colour w. padding */ - {"RGBX", "RGBX", 32, copy4}, - {"RGBX", "RGBX;L", 32, packRGBXL}, - {"RGBX", "RGB", 24, ImagingPackRGB}, - {"RGBX", "BGR", 24, ImagingPackBGR}, - {"RGBX", "BGRX", 32, ImagingPackBGRX}, - {"RGBX", "XBGR", 32, ImagingPackXBGR}, - {"RGBX", "R", 8, band0}, - {"RGBX", "G", 8, band1}, - {"RGBX", "B", 8, band2}, - {"RGBX", "X", 8, band3}, - - /* colour separation */ - {"CMYK", "CMYK", 32, copy4}, - {"CMYK", "CMYK;I", 32, copy4I}, - {"CMYK", "CMYK;L", 32, packRGBXL}, - {"CMYK", "C", 8, band0}, - {"CMYK", "M", 8, band1}, - {"CMYK", "Y", 8, band2}, - {"CMYK", "K", 8, band3}, - - /* video (YCbCr) */ - {"YCbCr", "YCbCr", 24, ImagingPackRGB}, - {"YCbCr", "YCbCr;L", 24, packRGBL}, - {"YCbCr", "YCbCrX", 32, copy4}, - {"YCbCr", "YCbCrK", 32, copy4}, - {"YCbCr", "Y", 8, band0}, - {"YCbCr", "Cb", 8, band1}, - {"YCbCr", "Cr", 8, band2}, - - /* LAB Color */ - {"LAB", "LAB", 24, ImagingPackLAB}, - {"LAB", "L", 8, band0}, - {"LAB", "A", 8, band1}, - {"LAB", "B", 8, band2}, - - /* HSV */ - {"HSV", "HSV", 24, ImagingPackRGB}, - {"HSV", "H", 8, band0}, - {"HSV", "S", 8, band1}, - {"HSV", "V", 8, band2}, - - /* integer */ - {"I", "I", 32, copy4}, - {"I", "I;16B", 16, packI16B}, - {"I", "I;32S", 32, packI32S}, - {"I", "I;32NS", 32, copy4}, - - /* floating point */ - {"F", "F", 32, copy4}, - {"F", "F;32F", 32, packI32S}, - {"F", "F;32NF", 32, copy4}, - - /* storage modes */ - {"I;16", "I;16", 16, copy2}, -#ifdef WORDS_BIGENDIAN - {"I;16", "I;16B", 16, packI16N_I16}, -#else - {"I;16", "I;16B", 16, packI16N_I16B}, -#endif - {"I;16B", "I;16B", 16, copy2}, - {"I;16L", "I;16L", 16, copy2}, - {"I;16N", "I;16N", 16, copy2}, - {"I;16", "I;16N", 16, packI16N_I16}, // LibTiff native->image endian. - {"I;16L", "I;16N", 16, packI16N_I16}, - {"I;16B", "I;16N", 16, packI16N_I16B}, - {"BGR;15", "BGR;15", 16, copy2}, - {"BGR;16", "BGR;16", 16, copy2}, - {"BGR;24", "BGR;24", 24, copy3}, - - {NULL} /* sentinel */ -}; - -ImagingShuffler -ImagingFindPacker(const char *mode, const char *rawmode, int *bits_out) { - int i; - - /* find a suitable pixel packer */ - for (i = 0; packers[i].rawmode; i++) { - if (strcmp(packers[i].mode, mode) == 0 && - strcmp(packers[i].rawmode, rawmode) == 0) { - if (bits_out) { - *bits_out = packers[i].bits; - } - return packers[i].pack; - } - } - return NULL; -} +/* + * The Python Imaging Library. + * $Id$ + * + * code to pack raw data + * + * history: + * 1996-04-30 fl Created + * 1996-05-12 fl Published a few RGB packers + * 1996-11-01 fl More RGB packers (Tk booster stuff) + * 1996-12-30 fl Added P;1, P;2 and P;4 packers + * 1997-06-02 fl Added F (F;32NF) packer + * 1997-08-28 fl Added 1 as L packer + * 1998-02-08 fl Added I packer + * 1998-03-09 fl Added mode field, RGBA/RGBX as RGB packers + * 1998-07-01 fl Added YCbCr support + * 1998-07-12 fl Added I 16 packer + * 1999-02-03 fl Added BGR packers + * 2003-09-26 fl Added LA/PA packers + * 2006-06-22 fl Added CMYK;I packer + * + * Copyright (c) 1997-2006 by Secret Labs AB. + * Copyright (c) 1996-1997 by Fredrik Lundh. + * + * See the README file for information on usage and redistribution. + */ + +#include "Imaging.h" + +#define R 0 +#define G 1 +#define B 2 +#define X 3 +#define A 3 + +#define C 0 +#define M 1 +#define Y 2 +#define K 3 + +/* byte swapping macros */ + +#define C16N (out[0] = tmp[0], out[1] = tmp[1]); +#define C16S (out[1] = tmp[0], out[0] = tmp[1]); +#define C32N (out[0] = tmp[0], out[1] = tmp[1], out[2] = tmp[2], out[3] = tmp[3]); +#define C32S (out[3] = tmp[0], out[2] = tmp[1], out[1] = tmp[2], out[0] = tmp[3]); +#define C64N \ + (out[0] = tmp[0], \ + out[1] = tmp[1], \ + out[2] = tmp[2], \ + out[3] = tmp[3], \ + out[4] = tmp[4], \ + out[5] = tmp[5], \ + out[6] = tmp[6], \ + out[7] = tmp[7]); +#define C64S \ + (out[7] = tmp[0], \ + out[6] = tmp[1], \ + out[5] = tmp[2], \ + out[4] = tmp[3], \ + out[3] = tmp[4], \ + out[2] = tmp[5], \ + out[1] = tmp[6], \ + out[0] = tmp[7]); + +#ifdef WORDS_BIGENDIAN +#define C16B C16N +#define C16L C16S +#define C32B C32N +#define C32L C32S +#define C64B C64N +#define C64L C64S +#else +#define C16B C16S +#define C16L C16N +#define C32B C32S +#define C32L C32N +#define C64B C64S +#define C64L C64N +#endif + +static void +pack1(UINT8 *out, const UINT8 *in, int pixels) { + int i, m, b; + /* bilevel (black is 0) */ + b = 0; + m = 128; + for (i = 0; i < pixels; i++) { + if (in[i] != 0) { + b |= m; + } + m >>= 1; + if (m == 0) { + *out++ = b; + b = 0; + m = 128; + } + } + if (m != 128) { + *out++ = b; + } +} + +static void +pack1I(UINT8 *out, const UINT8 *in, int pixels) { + int i, m, b; + /* bilevel (black is 1) */ + b = 0; + m = 128; + for (i = 0; i < pixels; i++) { + if (in[i] == 0) { + b |= m; + } + m >>= 1; + if (m == 0) { + *out++ = b; + b = 0; + m = 128; + } + } + if (m != 128) { + *out++ = b; + } +} + +static void +pack1R(UINT8 *out, const UINT8 *in, int pixels) { + int i, m, b; + /* bilevel, lsb first (black is 0) */ + b = 0; + m = 1; + for (i = 0; i < pixels; i++) { + if (in[i] != 0) { + b |= m; + } + m <<= 1; + if (m == 256) { + *out++ = b; + b = 0; + m = 1; + } + } + if (m != 1) { + *out++ = b; + } +} + +static void +pack1IR(UINT8 *out, const UINT8 *in, int pixels) { + int i, m, b; + /* bilevel, lsb first (black is 1) */ + b = 0; + m = 1; + for (i = 0; i < pixels; i++) { + if (in[i] == 0) { + b |= m; + } + m <<= 1; + if (m == 256) { + *out++ = b; + b = 0; + m = 1; + } + } + if (m != 1) { + *out++ = b; + } +} + +static void +pack1L(UINT8 *out, const UINT8 *in, int pixels) { + int i; + /* bilevel, stored as bytes */ + for (i = 0; i < pixels; i++) { + out[i] = (in[i] != 0) ? 255 : 0; + } +} + +static void +packP4(UINT8 *out, const UINT8 *in, int pixels) { + while (pixels >= 2) { + *out++ = (in[0] << 4) | (in[1] & 15); + in += 2; + pixels -= 2; + } + + if (pixels) { + out[0] = (in[0] << 4); + } +} + +static void +packP2(UINT8 *out, const UINT8 *in, int pixels) { + while (pixels >= 4) { + *out++ = (in[0] << 6) | ((in[1] & 3) << 4) | ((in[2] & 3) << 2) | (in[3] & 3); + in += 4; + pixels -= 4; + } + + switch (pixels) { + case 3: + out[0] = (in[0] << 6) | ((in[1] & 3) << 4) | ((in[2] & 3) << 2); + break; + case 2: + out[0] = (in[0] << 6) | ((in[1] & 3) << 4); + break; + case 1: + out[0] = (in[0] << 6); + } +} + +static void +packL16(UINT8 *out, const UINT8 *in, int pixels) { + int i; + /* L -> L;16, e.g: \xff77 -> \x00\xff\x00\x77 */ + for (i = 0; i < pixels; i++) { + out[0] = 0; + out[1] = in[i]; + out += 2; + } +} + +static void +packL16B(UINT8 *out, const UINT8 *in, int pixels) { + int i; + /* L -> L;16B, e.g: \xff77 -> \xff\x00\x77\x00 */ + for (i = 0; i < pixels; i++) { + out[0] = in[i]; + out[1] = 0; + out += 2; + } +} + +static void +packLA(UINT8 *out, const UINT8 *in, int pixels) { + int i; + /* LA, pixel interleaved */ + for (i = 0; i < pixels; i++) { + out[0] = in[R]; + out[1] = in[A]; + out += 2; + in += 4; + } +} + +static void +packLAL(UINT8 *out, const UINT8 *in, int pixels) { + int i; + /* LA, line interleaved */ + for (i = 0; i < pixels; i++) { + out[i] = in[R]; + out[i + pixels] = in[A]; + in += 4; + } +} + +void +ImagingPackRGB(UINT8 *out, const UINT8 *in, int pixels) { + int i = 0; + /* RGB triplets */ +#ifdef __sparc + /* SPARC CPUs cannot read integers from nonaligned addresses. */ + for (; i < pixels; i++) { + out[0] = in[R]; + out[1] = in[G]; + out[2] = in[B]; + out += 3; + in += 4; + } +#else + for (; i < pixels - 1; i++) { + memcpy(out, in + i * 4, 4); + out += 3; + } + for (; i < pixels; i++) { + out[0] = in[i * 4 + R]; + out[1] = in[i * 4 + G]; + out[2] = in[i * 4 + B]; + out += 3; + } +#endif +} + +void +ImagingPackXRGB(UINT8 *out, const UINT8 *in, int pixels) { + int i; + /* XRGB, triplets with left padding */ + for (i = 0; i < pixels; i++) { + out[0] = 0; + out[1] = in[R]; + out[2] = in[G]; + out[3] = in[B]; + out += 4; + in += 4; + } +} + +void +ImagingPackBGR(UINT8 *out, const UINT8 *in, int pixels) { + int i; + /* RGB, reversed bytes */ + for (i = 0; i < pixels; i++) { + out[0] = in[B]; + out[1] = in[G]; + out[2] = in[R]; + out += 3; + in += 4; + } +} + +void +ImagingPackBGRX(UINT8 *out, const UINT8 *in, int pixels) { + int i; + /* BGRX, reversed bytes with right padding */ + for (i = 0; i < pixels; i++) { + out[0] = in[B]; + out[1] = in[G]; + out[2] = in[R]; + out[3] = 0; + out += 4; + in += 4; + } +} + +void +ImagingPackXBGR(UINT8 *out, const UINT8 *in, int pixels) { + int i; + /* XBGR, reversed bytes with left padding */ + for (i = 0; i < pixels; i++) { + out[0] = 0; + out[1] = in[B]; + out[2] = in[G]; + out[3] = in[R]; + out += 4; + in += 4; + } +} + +void +ImagingPackBGRA(UINT8 *out, const UINT8 *in, int pixels) { + int i; + /* BGRA, reversed bytes with right alpha */ + for (i = 0; i < pixels; i++) { + out[0] = in[B]; + out[1] = in[G]; + out[2] = in[R]; + out[3] = in[A]; + out += 4; + in += 4; + } +} + +void +ImagingPackABGR(UINT8 *out, const UINT8 *in, int pixels) { + int i; + /* ABGR, reversed bytes with left alpha */ + for (i = 0; i < pixels; i++) { + out[0] = in[A]; + out[1] = in[B]; + out[2] = in[G]; + out[3] = in[R]; + out += 4; + in += 4; + } +} + +void +ImagingPackBGRa(UINT8 *out, const UINT8 *in, int pixels) { + int i; + /* BGRa, reversed bytes with premultiplied alpha */ + for (i = 0; i < pixels; i++) { + int alpha = out[3] = in[A]; + int tmp; + out[0] = MULDIV255(in[B], alpha, tmp); + out[1] = MULDIV255(in[G], alpha, tmp); + out[2] = MULDIV255(in[R], alpha, tmp); + out += 4; + in += 4; + } +} + +static void +packRGBL(UINT8 *out, const UINT8 *in, int pixels) { + int i; + /* RGB, line interleaved */ + for (i = 0; i < pixels; i++) { + out[i] = in[R]; + out[i + pixels] = in[G]; + out[i + pixels + pixels] = in[B]; + in += 4; + } +} + +static void +packRGBXL(UINT8 *out, const UINT8 *in, int pixels) { + int i; + /* RGBX, line interleaved */ + for (i = 0; i < pixels; i++) { + out[i] = in[R]; + out[i + pixels] = in[G]; + out[i + pixels + pixels] = in[B]; + out[i + pixels + pixels + pixels] = in[X]; + in += 4; + } +} + +static void +packI16B(UINT8 *out, const UINT8 *in_, int pixels) { + int i; + UINT16 tmp_; + UINT8 *tmp = (UINT8 *)&tmp_; + for (i = 0; i < pixels; i++) { + INT32 in; + memcpy(&in, in_, sizeof(in)); + if (in <= 0) { + tmp_ = 0; + } else if (in > 65535) { + tmp_ = 65535; + } else { + tmp_ = in; + } + C16B; + out += 2; + in_ += sizeof(in); + } +} + +static void +packI16N_I16B(UINT8 *out, const UINT8 *in, int pixels) { + int i; + UINT8 *tmp = (UINT8 *)in; + for (i = 0; i < pixels; i++) { + C16B; + out += 2; + tmp += 2; + } +} +static void +packI16N_I16(UINT8 *out, const UINT8 *in, int pixels) { + int i; + UINT8 *tmp = (UINT8 *)in; + for (i = 0; i < pixels; i++) { + C16L; + out += 2; + tmp += 2; + } +} + +static void +packI32S(UINT8 *out, const UINT8 *in, int pixels) { + int i; + UINT8 *tmp = (UINT8 *)in; + for (i = 0; i < pixels; i++) { + C32L; + out += 4; + tmp += 4; + } +} + +void +ImagingPackLAB(UINT8 *out, const UINT8 *in, int pixels) { + int i; + /* LAB triplets */ + for (i = 0; i < pixels; i++) { + out[0] = in[0]; + out[1] = in[1] ^ 128; /* signed in outside world */ + out[2] = in[2] ^ 128; + out += 3; + in += 4; + } +} + +static void +copy1(UINT8 *out, const UINT8 *in, int pixels) { + /* L, P */ + memcpy(out, in, pixels); +} + +static void +copy2(UINT8 *out, const UINT8 *in, int pixels) { + /* I;16, etc */ + memcpy(out, in, pixels * 2); +} + +static void +copy3(UINT8 *out, const UINT8 *in, int pixels) { + /* BGR;24, etc */ + memcpy(out, in, pixels * 3); +} + +static void +copy4(UINT8 *out, const UINT8 *in, int pixels) { + /* RGBA, CMYK quadruples */ + memcpy(out, in, 4 * pixels); +} + +static void +copy4I(UINT8 *out, const UINT8 *in, int pixels) { + /* RGBA, CMYK quadruples, inverted */ + int i; + for (i = 0; i < pixels * 4; i++) { + out[i] = ~in[i]; + } +} + +static void +band0(UINT8 *out, const UINT8 *in, int pixels) { + int i; + for (i = 0; i < pixels; i++, in += 4) { + out[i] = in[0]; + } +} + +static void +band1(UINT8 *out, const UINT8 *in, int pixels) { + int i; + for (i = 0; i < pixels; i++, in += 4) { + out[i] = in[1]; + } +} + +static void +band2(UINT8 *out, const UINT8 *in, int pixels) { + int i; + for (i = 0; i < pixels; i++, in += 4) { + out[i] = in[2]; + } +} + +static void +band3(UINT8 *out, const UINT8 *in, int pixels) { + int i; + for (i = 0; i < pixels; i++, in += 4) { + out[i] = in[3]; + } +} + +static struct { + const char *mode; + const char *rawmode; + int bits; + ImagingShuffler pack; +} packers[] = { + + /* bilevel */ + {"1", "1", 1, pack1}, + {"1", "1;I", 1, pack1I}, + {"1", "1;R", 1, pack1R}, + {"1", "1;IR", 1, pack1IR}, + {"1", "L", 8, pack1L}, + + /* grayscale */ + {"L", "L", 8, copy1}, + {"L", "L;16", 16, packL16}, + {"L", "L;16B", 16, packL16B}, + + /* grayscale w. alpha */ + {"LA", "LA", 16, packLA}, + {"LA", "LA;L", 16, packLAL}, + + /* grayscale w. alpha premultiplied */ + {"La", "La", 16, packLA}, + + /* palette */ + {"P", "P;1", 1, pack1}, + {"P", "P;2", 2, packP2}, + {"P", "P;4", 4, packP4}, + {"P", "P", 8, copy1}, + + /* palette w. alpha */ + {"PA", "PA", 16, packLA}, + {"PA", "PA;L", 16, packLAL}, + + /* true colour */ + {"RGB", "RGB", 24, ImagingPackRGB}, + {"RGB", "RGBX", 32, copy4}, + {"RGB", "RGBA", 32, copy4}, + {"RGB", "XRGB", 32, ImagingPackXRGB}, + {"RGB", "BGR", 24, ImagingPackBGR}, + {"RGB", "BGRX", 32, ImagingPackBGRX}, + {"RGB", "XBGR", 32, ImagingPackXBGR}, + {"RGB", "RGB;L", 24, packRGBL}, + {"RGB", "R", 8, band0}, + {"RGB", "G", 8, band1}, + {"RGB", "B", 8, band2}, + + /* true colour w. alpha */ + {"RGBA", "RGBA", 32, copy4}, + {"RGBA", "RGBA;L", 32, packRGBXL}, + {"RGBA", "RGB", 24, ImagingPackRGB}, + {"RGBA", "BGR", 24, ImagingPackBGR}, + {"RGBA", "BGRA", 32, ImagingPackBGRA}, + {"RGBA", "ABGR", 32, ImagingPackABGR}, + {"RGBA", "BGRa", 32, ImagingPackBGRa}, + {"RGBA", "R", 8, band0}, + {"RGBA", "G", 8, band1}, + {"RGBA", "B", 8, band2}, + {"RGBA", "A", 8, band3}, + + /* true colour w. alpha premultiplied */ + {"RGBa", "RGBa", 32, copy4}, + {"RGBa", "BGRa", 32, ImagingPackBGRA}, + {"RGBa", "aBGR", 32, ImagingPackABGR}, + + /* true colour w. padding */ + {"RGBX", "RGBX", 32, copy4}, + {"RGBX", "RGBX;L", 32, packRGBXL}, + {"RGBX", "RGB", 24, ImagingPackRGB}, + {"RGBX", "BGR", 24, ImagingPackBGR}, + {"RGBX", "BGRX", 32, ImagingPackBGRX}, + {"RGBX", "XBGR", 32, ImagingPackXBGR}, + {"RGBX", "R", 8, band0}, + {"RGBX", "G", 8, band1}, + {"RGBX", "B", 8, band2}, + {"RGBX", "X", 8, band3}, + + /* colour separation */ + {"CMYK", "CMYK", 32, copy4}, + {"CMYK", "CMYK;I", 32, copy4I}, + {"CMYK", "CMYK;L", 32, packRGBXL}, + {"CMYK", "C", 8, band0}, + {"CMYK", "M", 8, band1}, + {"CMYK", "Y", 8, band2}, + {"CMYK", "K", 8, band3}, + + /* video (YCbCr) */ + {"YCbCr", "YCbCr", 24, ImagingPackRGB}, + {"YCbCr", "YCbCr;L", 24, packRGBL}, + {"YCbCr", "YCbCrX", 32, copy4}, + {"YCbCr", "YCbCrK", 32, copy4}, + {"YCbCr", "Y", 8, band0}, + {"YCbCr", "Cb", 8, band1}, + {"YCbCr", "Cr", 8, band2}, + + /* LAB Color */ + {"LAB", "LAB", 24, ImagingPackLAB}, + {"LAB", "L", 8, band0}, + {"LAB", "A", 8, band1}, + {"LAB", "B", 8, band2}, + + /* HSV */ + {"HSV", "HSV", 24, ImagingPackRGB}, + {"HSV", "H", 8, band0}, + {"HSV", "S", 8, band1}, + {"HSV", "V", 8, band2}, + + /* integer */ + {"I", "I", 32, copy4}, + {"I", "I;16B", 16, packI16B}, + {"I", "I;32S", 32, packI32S}, + {"I", "I;32NS", 32, copy4}, + + /* floating point */ + {"F", "F", 32, copy4}, + {"F", "F;32F", 32, packI32S}, + {"F", "F;32NF", 32, copy4}, + + /* storage modes */ + {"I;16", "I;16", 16, copy2}, +#ifdef WORDS_BIGENDIAN + {"I;16", "I;16B", 16, packI16N_I16}, +#else + {"I;16", "I;16B", 16, packI16N_I16B}, +#endif + {"I;16B", "I;16B", 16, copy2}, + {"I;16L", "I;16L", 16, copy2}, + {"I;16N", "I;16N", 16, copy2}, + {"I;16", "I;16N", 16, packI16N_I16}, // LibTiff native->image endian. + {"I;16L", "I;16N", 16, packI16N_I16}, + {"I;16B", "I;16N", 16, packI16N_I16B}, + {"BGR;15", "BGR;15", 16, copy2}, + {"BGR;16", "BGR;16", 16, copy2}, + {"BGR;24", "BGR;24", 24, copy3}, + + {NULL} /* sentinel */ +}; + +ImagingShuffler +ImagingFindPacker(const char *mode, const char *rawmode, int *bits_out) { + int i; + + /* find a suitable pixel packer */ + for (i = 0; packers[i].rawmode; i++) { + if (strcmp(packers[i].mode, mode) == 0 && + strcmp(packers[i].rawmode, rawmode) == 0) { + if (bits_out) { + *bits_out = packers[i].bits; + } + return packers[i].pack; + } + } + return NULL; +} diff --git a/src/libImaging/PackDecode.c b/src/libImaging/PackDecode.c index 7dd432b91..6cbbc2b98 100644 --- a/src/libImaging/PackDecode.c +++ b/src/libImaging/PackDecode.c @@ -1,92 +1,92 @@ -/* - * The Python Imaging Library. - * $Id$ - * - * decoder for PackBits image data. - * - * history: - * 96-04-19 fl Created - * - * Copyright (c) Fredrik Lundh 1996. - * Copyright (c) Secret Labs AB 1997. - * - * See the README file for information on usage and redistribution. - */ - -#include "Imaging.h" - -int -ImagingPackbitsDecode( - Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) { - UINT8 n; - UINT8 *ptr; - int i; - - ptr = buf; - - for (;;) { - if (bytes < 1) { - return ptr - buf; - } - - if (ptr[0] & 0x80) { - if (ptr[0] == 0x80) { - /* Nop */ - ptr++; - bytes--; - continue; - } - - /* Run */ - if (bytes < 2) { - return ptr - buf; - } - - for (n = 257 - ptr[0]; n > 0; n--) { - if (state->x >= state->bytes) { - /* state->errcode = IMAGING_CODEC_OVERRUN; */ - break; - } - state->buffer[state->x++] = ptr[1]; - } - - ptr += 2; - bytes -= 2; - - } else { - /* Literal */ - n = ptr[0] + 2; - - if (bytes < n) { - return ptr - buf; - } - - for (i = 1; i < n; i++) { - if (state->x >= state->bytes) { - /* state->errcode = IMAGING_CODEC_OVERRUN; */ - break; - } - state->buffer[state->x++] = ptr[i]; - } - - ptr += n; - bytes -= n; - } - - if (state->x >= state->bytes) { - /* Got a full line, unpack it */ - state->shuffle( - (UINT8 *)im->image[state->y + state->yoff] + - state->xoff * im->pixelsize, - state->buffer, - state->xsize); - - state->x = 0; - - if (++state->y >= state->ysize) { - /* End of file (errcode = 0) */ - return -1; - } - } - } -} +/* + * The Python Imaging Library. + * $Id$ + * + * decoder for PackBits image data. + * + * history: + * 96-04-19 fl Created + * + * Copyright (c) Fredrik Lundh 1996. + * Copyright (c) Secret Labs AB 1997. + * + * See the README file for information on usage and redistribution. + */ + +#include "Imaging.h" + +int +ImagingPackbitsDecode( + Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) { + UINT8 n; + UINT8 *ptr; + int i; + + ptr = buf; + + for (;;) { + if (bytes < 1) { + return ptr - buf; + } + + if (ptr[0] & 0x80) { + if (ptr[0] == 0x80) { + /* Nop */ + ptr++; + bytes--; + continue; + } + + /* Run */ + if (bytes < 2) { + return ptr - buf; + } + + for (n = 257 - ptr[0]; n > 0; n--) { + if (state->x >= state->bytes) { + /* state->errcode = IMAGING_CODEC_OVERRUN; */ + break; + } + state->buffer[state->x++] = ptr[1]; + } + + ptr += 2; + bytes -= 2; + + } else { + /* Literal */ + n = ptr[0] + 2; + + if (bytes < n) { + return ptr - buf; + } + + for (i = 1; i < n; i++) { + if (state->x >= state->bytes) { + /* state->errcode = IMAGING_CODEC_OVERRUN; */ + break; + } + state->buffer[state->x++] = ptr[i]; + } + + ptr += n; + bytes -= n; + } + + if (state->x >= state->bytes) { + /* Got a full line, unpack it */ + state->shuffle( + (UINT8 *)im->image[state->y + state->yoff] + + state->xoff * im->pixelsize, + state->buffer, + state->xsize); + + state->x = 0; + + if (++state->y >= state->ysize) { + /* End of file (errcode = 0) */ + return -1; + } + } + } +} diff --git a/src/libImaging/Palette.c b/src/libImaging/Palette.c index 78916bca5..5cac43a38 100644 --- a/src/libImaging/Palette.c +++ b/src/libImaging/Palette.c @@ -1,307 +1,307 @@ -/* - * The Python Imaging Library - * $Id$ - * - * imaging palette object - * - * history: - * 1996-05-05 fl Added to library - * 1996-05-27 fl Added colour mapping stuff - * 1997-05-12 fl Support RGBA palettes - * 2005-02-09 fl Removed grayscale entries from web palette - * - * Copyright (c) Secret Labs AB 1997-2005. All rights reserved. - * Copyright (c) Fredrik Lundh 1995-1997. - * - * See the README file for information on usage and redistribution. - */ - -#include "Imaging.h" - -#include - -ImagingPalette -ImagingPaletteNew(const char *mode) { - /* Create a palette object */ - - int i; - ImagingPalette palette; - - if (strcmp(mode, "RGB") && strcmp(mode, "RGBA")) { - return (ImagingPalette)ImagingError_ModeError(); - } - - palette = calloc(1, sizeof(struct ImagingPaletteInstance)); - if (!palette) { - return (ImagingPalette)ImagingError_MemoryError(); - } - - strncpy(palette->mode, mode, IMAGING_MODE_LENGTH - 1); - palette->mode[IMAGING_MODE_LENGTH - 1] = 0; - - palette->size = 0; - for (i = 0; i < 256; i++) { - palette->palette[i * 4 + 3] = 255; /* opaque */ - } - - return palette; -} - -ImagingPalette -ImagingPaletteNewBrowser(void) { - /* Create a standard "browser" palette object */ - - int i, r, g, b; - ImagingPalette palette; - - palette = ImagingPaletteNew("RGB"); - if (!palette) { - return NULL; - } - - /* FIXME: Add 10-level windows palette here? */ - - /* Simple 6x6x6 colour cube */ - i = 10; - for (b = 0; b < 256; b += 51) { - for (g = 0; g < 256; g += 51) { - for (r = 0; r < 256; r += 51) { - palette->palette[i * 4 + 0] = r; - palette->palette[i * 4 + 1] = g; - palette->palette[i * 4 + 2] = b; - i++; - } - } - } - palette->size = i; - - /* FIXME: add 30-level grayscale wedge here? */ - - return palette; -} - -ImagingPalette -ImagingPaletteDuplicate(ImagingPalette palette) { - /* Duplicate palette descriptor */ - - ImagingPalette new_palette; - - if (!palette) { - return NULL; - } - /* malloc check ok, small constant allocation */ - new_palette = malloc(sizeof(struct ImagingPaletteInstance)); - if (!new_palette) { - return (ImagingPalette)ImagingError_MemoryError(); - } - - memcpy(new_palette, palette, sizeof(struct ImagingPaletteInstance)); - - /* Don't share the cache */ - new_palette->cache = NULL; - - return new_palette; -} - -void -ImagingPaletteDelete(ImagingPalette palette) { - /* Destroy palette object */ - - if (palette) { - if (palette->cache) { - free(palette->cache); - } - free(palette); - } -} - -/* -------------------------------------------------------------------- */ -/* Colour mapping */ -/* -------------------------------------------------------------------- */ - -/* This code is used to map RGB triplets to palette indices, using - a palette index cache. */ - -/* - * This implementation is loosely based on the corresponding code in - * the IJG JPEG library by Thomas G. Lane. Original algorithms by - * Paul Heckbert and Spencer W. Thomas. - * - * The IJG JPEG library is copyright (C) 1991-1995, Thomas G. Lane. */ - -#define DIST(a, b, s) (a - b) * (a - b) * s - -/* Colour weights (no scaling, for now) */ -#define RSCALE 1 -#define GSCALE 1 -#define BSCALE 1 - -/* Calculated scaled distances */ -#define RDIST(a, b) DIST(a, b, RSCALE *RSCALE) -#define GDIST(a, b) DIST(a, b, GSCALE *GSCALE) -#define BDIST(a, b) DIST(a, b, BSCALE *BSCALE) - -/* Incremental steps */ -#define RSTEP (4 * RSCALE) -#define GSTEP (4 * GSCALE) -#define BSTEP (4 * BSCALE) - -#define BOX 8 - -#define BOXVOLUME BOX *BOX *BOX - -void -ImagingPaletteCacheUpdate(ImagingPalette palette, int r, int g, int b) { - int i, j; - unsigned int dmin[256], dmax; - int r0, g0, b0; - int r1, g1, b1; - int rc, gc, bc; - unsigned int d[BOXVOLUME]; - UINT8 c[BOXVOLUME]; - - /* Get box boundaries for the given (r,g,b)-triplet. Each box - covers eight cache slots (32 colour values, that is). */ - - r0 = r & 0xe0; - r1 = r0 + 0x1f; - rc = (r0 + r1) / 2; - g0 = g & 0xe0; - g1 = g0 + 0x1f; - gc = (g0 + g1) / 2; - b0 = b & 0xe0; - b1 = b0 + 0x1f; - bc = (b0 + b1) / 2; - - /* Step 1 -- Select relevant palette entries (after Heckbert) */ - - /* For each palette entry, calculate the min and max distances to - * any position in the box given by the colour we're looking for. */ - - dmax = (unsigned int)~0; - - for (i = 0; i < palette->size; i++) { - int r, g, b; - unsigned int tmin, tmax; - - /* Find min and max distances to any point in the box */ - r = palette->palette[i * 4 + 0]; - tmin = (r < r0) ? RDIST(r, r0) : (r > r1) ? RDIST(r, r1) : 0; - tmax = (r <= rc) ? RDIST(r, r1) : RDIST(r, r0); - - g = palette->palette[i * 4 + 1]; - tmin += (g < g0) ? GDIST(g, g0) : (g > g1) ? GDIST(g, g1) : 0; - tmax += (g <= gc) ? GDIST(g, g1) : GDIST(g, g0); - - b = palette->palette[i * 4 + 2]; - tmin += (b < b0) ? BDIST(b, b0) : (b > b1) ? BDIST(b, b1) : 0; - tmax += (b <= bc) ? BDIST(b, b1) : BDIST(b, b0); - - dmin[i] = tmin; - if (tmax < dmax) { - dmax = tmax; /* keep the smallest max distance only */ - } - } - - /* Step 2 -- Incrementally update cache slot (after Thomas) */ - - /* Find the box containing the nearest palette entry, and update - * all slots in that box. We only check boxes for which the min - * distance is less than or equal the smallest max distance */ - - for (i = 0; i < BOXVOLUME; i++) { - d[i] = (unsigned int)~0; - } - - for (i = 0; i < palette->size; i++) { - if (dmin[i] <= dmax) { - int rd, gd, bd; - int ri, gi, bi; - int rx, gx, bx; - - ri = (r0 - palette->palette[i * 4 + 0]) * RSCALE; - gi = (g0 - palette->palette[i * 4 + 1]) * GSCALE; - bi = (b0 - palette->palette[i * 4 + 2]) * BSCALE; - - rd = ri * ri + gi * gi + bi * bi; - - ri = ri * (2 * RSTEP) + RSTEP * RSTEP; - gi = gi * (2 * GSTEP) + GSTEP * GSTEP; - bi = bi * (2 * BSTEP) + BSTEP * BSTEP; - - rx = ri; - for (r = j = 0; r < BOX; r++) { - gd = rd; - gx = gi; - for (g = 0; g < BOX; g++) { - bd = gd; - bx = bi; - for (b = 0; b < BOX; b++) { - if ((unsigned int)bd < d[j]) { - d[j] = bd; - c[j] = (UINT8)i; - } - bd += bx; - bx += 2 * BSTEP * BSTEP; - j++; - } - gd += gx; - gx += 2 * GSTEP * GSTEP; - } - rd += rx; - rx += 2 * RSTEP * RSTEP; - } - } - } - - /* Step 3 -- Update cache */ - - /* The c array now contains the closest match for each - * cache slot in the box. Update the cache. */ - - j = 0; - for (r = r0; r < r1; r += 4) { - for (g = g0; g < g1; g += 4) { - for (b = b0; b < b1; b += 4) { - ImagingPaletteCache(palette, r, g, b) = c[j++]; - } - } - } -} - -int -ImagingPaletteCachePrepare(ImagingPalette palette) { - /* Add a colour cache to a palette */ - - int i; - int entries = 64 * 64 * 64; - - if (palette->cache == NULL) { - /* The cache is 512k. It might be a good idea to break it - up into a pointer array (e.g. an 8-bit image?) */ - - /* malloc check ok, small constant allocation */ - palette->cache = (INT16 *)malloc(entries * sizeof(INT16)); - if (!palette->cache) { - (void)ImagingError_MemoryError(); - return -1; - } - - /* Mark all entries as empty */ - for (i = 0; i < entries; i++) { - palette->cache[i] = 0x100; - } - } - - return 0; -} - -void -ImagingPaletteCacheDelete(ImagingPalette palette) { - /* Release the colour cache, if any */ - - if (palette && palette->cache) { - free(palette->cache); - palette->cache = NULL; - } -} +/* + * The Python Imaging Library + * $Id$ + * + * imaging palette object + * + * history: + * 1996-05-05 fl Added to library + * 1996-05-27 fl Added colour mapping stuff + * 1997-05-12 fl Support RGBA palettes + * 2005-02-09 fl Removed grayscale entries from web palette + * + * Copyright (c) Secret Labs AB 1997-2005. All rights reserved. + * Copyright (c) Fredrik Lundh 1995-1997. + * + * See the README file for information on usage and redistribution. + */ + +#include "Imaging.h" + +#include + +ImagingPalette +ImagingPaletteNew(const char *mode) { + /* Create a palette object */ + + int i; + ImagingPalette palette; + + if (strcmp(mode, "RGB") && strcmp(mode, "RGBA")) { + return (ImagingPalette)ImagingError_ModeError(); + } + + palette = calloc(1, sizeof(struct ImagingPaletteInstance)); + if (!palette) { + return (ImagingPalette)ImagingError_MemoryError(); + } + + strncpy(palette->mode, mode, IMAGING_MODE_LENGTH - 1); + palette->mode[IMAGING_MODE_LENGTH - 1] = 0; + + palette->size = 0; + for (i = 0; i < 256; i++) { + palette->palette[i * 4 + 3] = 255; /* opaque */ + } + + return palette; +} + +ImagingPalette +ImagingPaletteNewBrowser(void) { + /* Create a standard "browser" palette object */ + + int i, r, g, b; + ImagingPalette palette; + + palette = ImagingPaletteNew("RGB"); + if (!palette) { + return NULL; + } + + /* FIXME: Add 10-level windows palette here? */ + + /* Simple 6x6x6 colour cube */ + i = 10; + for (b = 0; b < 256; b += 51) { + for (g = 0; g < 256; g += 51) { + for (r = 0; r < 256; r += 51) { + palette->palette[i * 4 + 0] = r; + palette->palette[i * 4 + 1] = g; + palette->palette[i * 4 + 2] = b; + i++; + } + } + } + palette->size = i; + + /* FIXME: add 30-level grayscale wedge here? */ + + return palette; +} + +ImagingPalette +ImagingPaletteDuplicate(ImagingPalette palette) { + /* Duplicate palette descriptor */ + + ImagingPalette new_palette; + + if (!palette) { + return NULL; + } + /* malloc check ok, small constant allocation */ + new_palette = malloc(sizeof(struct ImagingPaletteInstance)); + if (!new_palette) { + return (ImagingPalette)ImagingError_MemoryError(); + } + + memcpy(new_palette, palette, sizeof(struct ImagingPaletteInstance)); + + /* Don't share the cache */ + new_palette->cache = NULL; + + return new_palette; +} + +void +ImagingPaletteDelete(ImagingPalette palette) { + /* Destroy palette object */ + + if (palette) { + if (palette->cache) { + free(palette->cache); + } + free(palette); + } +} + +/* -------------------------------------------------------------------- */ +/* Colour mapping */ +/* -------------------------------------------------------------------- */ + +/* This code is used to map RGB triplets to palette indices, using + a palette index cache. */ + +/* + * This implementation is loosely based on the corresponding code in + * the IJG JPEG library by Thomas G. Lane. Original algorithms by + * Paul Heckbert and Spencer W. Thomas. + * + * The IJG JPEG library is copyright (C) 1991-1995, Thomas G. Lane. */ + +#define DIST(a, b, s) (a - b) * (a - b) * s + +/* Colour weights (no scaling, for now) */ +#define RSCALE 1 +#define GSCALE 1 +#define BSCALE 1 + +/* Calculated scaled distances */ +#define RDIST(a, b) DIST(a, b, RSCALE *RSCALE) +#define GDIST(a, b) DIST(a, b, GSCALE *GSCALE) +#define BDIST(a, b) DIST(a, b, BSCALE *BSCALE) + +/* Incremental steps */ +#define RSTEP (4 * RSCALE) +#define GSTEP (4 * GSCALE) +#define BSTEP (4 * BSCALE) + +#define BOX 8 + +#define BOXVOLUME BOX *BOX *BOX + +void +ImagingPaletteCacheUpdate(ImagingPalette palette, int r, int g, int b) { + int i, j; + unsigned int dmin[256], dmax; + int r0, g0, b0; + int r1, g1, b1; + int rc, gc, bc; + unsigned int d[BOXVOLUME]; + UINT8 c[BOXVOLUME]; + + /* Get box boundaries for the given (r,g,b)-triplet. Each box + covers eight cache slots (32 colour values, that is). */ + + r0 = r & 0xe0; + r1 = r0 + 0x1f; + rc = (r0 + r1) / 2; + g0 = g & 0xe0; + g1 = g0 + 0x1f; + gc = (g0 + g1) / 2; + b0 = b & 0xe0; + b1 = b0 + 0x1f; + bc = (b0 + b1) / 2; + + /* Step 1 -- Select relevant palette entries (after Heckbert) */ + + /* For each palette entry, calculate the min and max distances to + * any position in the box given by the colour we're looking for. */ + + dmax = (unsigned int)~0; + + for (i = 0; i < palette->size; i++) { + int r, g, b; + unsigned int tmin, tmax; + + /* Find min and max distances to any point in the box */ + r = palette->palette[i * 4 + 0]; + tmin = (r < r0) ? RDIST(r, r0) : (r > r1) ? RDIST(r, r1) : 0; + tmax = (r <= rc) ? RDIST(r, r1) : RDIST(r, r0); + + g = palette->palette[i * 4 + 1]; + tmin += (g < g0) ? GDIST(g, g0) : (g > g1) ? GDIST(g, g1) : 0; + tmax += (g <= gc) ? GDIST(g, g1) : GDIST(g, g0); + + b = palette->palette[i * 4 + 2]; + tmin += (b < b0) ? BDIST(b, b0) : (b > b1) ? BDIST(b, b1) : 0; + tmax += (b <= bc) ? BDIST(b, b1) : BDIST(b, b0); + + dmin[i] = tmin; + if (tmax < dmax) { + dmax = tmax; /* keep the smallest max distance only */ + } + } + + /* Step 2 -- Incrementally update cache slot (after Thomas) */ + + /* Find the box containing the nearest palette entry, and update + * all slots in that box. We only check boxes for which the min + * distance is less than or equal the smallest max distance */ + + for (i = 0; i < BOXVOLUME; i++) { + d[i] = (unsigned int)~0; + } + + for (i = 0; i < palette->size; i++) { + if (dmin[i] <= dmax) { + int rd, gd, bd; + int ri, gi, bi; + int rx, gx, bx; + + ri = (r0 - palette->palette[i * 4 + 0]) * RSCALE; + gi = (g0 - palette->palette[i * 4 + 1]) * GSCALE; + bi = (b0 - palette->palette[i * 4 + 2]) * BSCALE; + + rd = ri * ri + gi * gi + bi * bi; + + ri = ri * (2 * RSTEP) + RSTEP * RSTEP; + gi = gi * (2 * GSTEP) + GSTEP * GSTEP; + bi = bi * (2 * BSTEP) + BSTEP * BSTEP; + + rx = ri; + for (r = j = 0; r < BOX; r++) { + gd = rd; + gx = gi; + for (g = 0; g < BOX; g++) { + bd = gd; + bx = bi; + for (b = 0; b < BOX; b++) { + if ((unsigned int)bd < d[j]) { + d[j] = bd; + c[j] = (UINT8)i; + } + bd += bx; + bx += 2 * BSTEP * BSTEP; + j++; + } + gd += gx; + gx += 2 * GSTEP * GSTEP; + } + rd += rx; + rx += 2 * RSTEP * RSTEP; + } + } + } + + /* Step 3 -- Update cache */ + + /* The c array now contains the closest match for each + * cache slot in the box. Update the cache. */ + + j = 0; + for (r = r0; r < r1; r += 4) { + for (g = g0; g < g1; g += 4) { + for (b = b0; b < b1; b += 4) { + ImagingPaletteCache(palette, r, g, b) = c[j++]; + } + } + } +} + +int +ImagingPaletteCachePrepare(ImagingPalette palette) { + /* Add a colour cache to a palette */ + + int i; + int entries = 64 * 64 * 64; + + if (palette->cache == NULL) { + /* The cache is 512k. It might be a good idea to break it + up into a pointer array (e.g. an 8-bit image?) */ + + /* malloc check ok, small constant allocation */ + palette->cache = (INT16 *)malloc(entries * sizeof(INT16)); + if (!palette->cache) { + (void)ImagingError_MemoryError(); + return -1; + } + + /* Mark all entries as empty */ + for (i = 0; i < entries; i++) { + palette->cache[i] = 0x100; + } + } + + return 0; +} + +void +ImagingPaletteCacheDelete(ImagingPalette palette) { + /* Release the colour cache, if any */ + + if (palette && palette->cache) { + free(palette->cache); + palette->cache = NULL; + } +} diff --git a/src/libImaging/Paste.c b/src/libImaging/Paste.c index a018225b2..9723fb6b7 100644 --- a/src/libImaging/Paste.c +++ b/src/libImaging/Paste.c @@ -1,627 +1,627 @@ -/* - * The Python Imaging Library - * $Id$ - * - * paste image on another image - * - * history: - * 96-03-27 fl Created - * 96-07-16 fl Support "1", "L" and "RGBA" masks - * 96-08-16 fl Merged with opaque paste - * 97-01-17 fl Faster blending, added support for RGBa images - * 97-08-27 fl Faster masking for 32-bit images - * 98-02-02 fl Fixed MULDIV255 macro for gcc - * 99-02-02 fl Added "RGBa" mask support - * 99-02-06 fl Rewritten. Added support for masked fill operations. - * 99-12-08 fl Fixed matte fill. - * - * Copyright (c) Fredrik Lundh 1996-97. - * Copyright (c) Secret Labs AB 1997-99. - * - * See the README file for information on usage and redistribution. - */ - -#include "Imaging.h" - -static inline void -paste( - Imaging imOut, - Imaging imIn, - int dx, - int dy, - int sx, - int sy, - int xsize, - int ysize, - int pixelsize) { - /* paste opaque region */ - - int y; - - dx *= pixelsize; - sx *= pixelsize; - - xsize *= pixelsize; - - for (y = 0; y < ysize; y++) { - memcpy(imOut->image[y + dy] + dx, imIn->image[y + sy] + sx, xsize); - } -} - -static inline void -paste_mask_1( - Imaging imOut, - Imaging imIn, - Imaging imMask, - int dx, - int dy, - int sx, - int sy, - int xsize, - int ysize, - int pixelsize) { - /* paste with mode "1" mask */ - - int x, y; - - if (imOut->image8) { - for (y = 0; y < ysize; y++) { - UINT8 *out = imOut->image8[y + dy] + dx; - UINT8 *in = imIn->image8[y + sy] + sx; - UINT8 *mask = imMask->image8[y + sy] + sx; - for (x = 0; x < xsize; x++) { - if (*mask++) { - *out = *in; - } - out++, in++; - } - } - - } else { - for (y = 0; y < ysize; y++) { - INT32 *out = imOut->image32[y + dy] + dx; - INT32 *in = imIn->image32[y + sy] + sx; - UINT8 *mask = imMask->image8[y + sy] + sx; - for (x = 0; x < xsize; x++) { - if (*mask++) { - *out = *in; - } - out++, in++; - } - } - } -} - -static inline void -paste_mask_L( - Imaging imOut, - Imaging imIn, - Imaging imMask, - int dx, - int dy, - int sx, - int sy, - int xsize, - int ysize, - int pixelsize) { - /* paste with mode "L" matte */ - - int x, y; - unsigned int tmp1; - - if (imOut->image8) { - for (y = 0; y < ysize; y++) { - UINT8 *out = imOut->image8[y + dy] + dx; - UINT8 *in = imIn->image8[y + sy] + sx; - UINT8 *mask = imMask->image8[y + sy] + sx; - for (x = 0; x < xsize; x++) { - *out = BLEND(*mask, *out, *in, tmp1); - out++, in++, mask++; - } - } - - } else { - for (y = 0; y < ysize; y++) { - UINT8 *out = (UINT8 *)(imOut->image32[y + dy] + dx); - UINT8 *in = (UINT8 *)(imIn->image32[y + sy] + sx); - UINT8 *mask = (UINT8 *)(imMask->image8[y + sy] + sx); - for (x = 0; x < xsize; x++) { - UINT8 a = mask[0]; - out[0] = BLEND(a, out[0], in[0], tmp1); - out[1] = BLEND(a, out[1], in[1], tmp1); - out[2] = BLEND(a, out[2], in[2], tmp1); - out[3] = BLEND(a, out[3], in[3], tmp1); - out += 4; - in += 4; - mask++; - } - } - } -} - -static inline void -paste_mask_RGBA( - Imaging imOut, - Imaging imIn, - Imaging imMask, - int dx, - int dy, - int sx, - int sy, - int xsize, - int ysize, - int pixelsize) { - /* paste with mode "RGBA" matte */ - - int x, y; - unsigned int tmp1; - - if (imOut->image8) { - for (y = 0; y < ysize; y++) { - UINT8 *out = imOut->image8[y + dy] + dx; - UINT8 *in = imIn->image8[y + sy] + sx; - UINT8 *mask = (UINT8 *)imMask->image[y + sy] + sx * 4 + 3; - for (x = 0; x < xsize; x++) { - *out = BLEND(*mask, *out, *in, tmp1); - out++, in++, mask += 4; - } - } - - } else { - for (y = 0; y < ysize; y++) { - UINT8 *out = (UINT8 *)(imOut->image32[y + dy] + dx); - UINT8 *in = (UINT8 *)(imIn->image32[y + sy] + sx); - UINT8 *mask = (UINT8 *)(imMask->image32[y + sy] + sx); - for (x = 0; x < xsize; x++) { - UINT8 a = mask[3]; - out[0] = BLEND(a, out[0], in[0], tmp1); - out[1] = BLEND(a, out[1], in[1], tmp1); - out[2] = BLEND(a, out[2], in[2], tmp1); - out[3] = BLEND(a, out[3], in[3], tmp1); - out += 4; - in += 4; - mask += 4; - } - } - } -} - -static inline void -paste_mask_RGBa( - Imaging imOut, - Imaging imIn, - Imaging imMask, - int dx, - int dy, - int sx, - int sy, - int xsize, - int ysize, - int pixelsize) { - /* paste with mode "RGBa" matte */ - - int x, y; - unsigned int tmp1; - - if (imOut->image8) { - for (y = 0; y < ysize; y++) { - UINT8 *out = imOut->image8[y + dy] + dx; - UINT8 *in = imIn->image8[y + sy] + sx; - UINT8 *mask = (UINT8 *)imMask->image[y + sy] + sx * 4 + 3; - for (x = 0; x < xsize; x++) { - *out = PREBLEND(*mask, *out, *in, tmp1); - out++, in++, mask += 4; - } - } - - } else { - for (y = 0; y < ysize; y++) { - UINT8 *out = (UINT8 *)(imOut->image32[y + dy] + dx); - UINT8 *in = (UINT8 *)(imIn->image32[y + sy] + sx); - UINT8 *mask = (UINT8 *)(imMask->image32[y + sy] + sx); - for (x = 0; x < xsize; x++) { - UINT8 a = mask[3]; - out[0] = PREBLEND(a, out[0], in[0], tmp1); - out[1] = PREBLEND(a, out[1], in[1], tmp1); - out[2] = PREBLEND(a, out[2], in[2], tmp1); - out[3] = PREBLEND(a, out[3], in[3], tmp1); - out += 4; - in += 4; - mask += 4; - } - } - } -} - -int -ImagingPaste( - Imaging imOut, Imaging imIn, Imaging imMask, int dx0, int dy0, int dx1, int dy1) { - int xsize, ysize; - int pixelsize; - int sx0, sy0; - ImagingSectionCookie cookie; - - if (!imOut || !imIn) { - (void)ImagingError_ModeError(); - return -1; - } - - pixelsize = imOut->pixelsize; - - xsize = dx1 - dx0; - ysize = dy1 - dy0; - - if (xsize != imIn->xsize || ysize != imIn->ysize || pixelsize != imIn->pixelsize) { - (void)ImagingError_Mismatch(); - return -1; - } - - if (imMask && (xsize != imMask->xsize || ysize != imMask->ysize)) { - (void)ImagingError_Mismatch(); - return -1; - } - - /* Determine which region to copy */ - sx0 = sy0 = 0; - if (dx0 < 0) { - xsize += dx0, sx0 = -dx0, dx0 = 0; - } - if (dx0 + xsize > imOut->xsize) { - xsize = imOut->xsize - dx0; - } - if (dy0 < 0) { - ysize += dy0, sy0 = -dy0, dy0 = 0; - } - if (dy0 + ysize > imOut->ysize) { - ysize = imOut->ysize - dy0; - } - - if (xsize <= 0 || ysize <= 0) { - return 0; - } - - if (!imMask) { - ImagingSectionEnter(&cookie); - paste(imOut, imIn, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize); - ImagingSectionLeave(&cookie); - - } else if (strcmp(imMask->mode, "1") == 0) { - ImagingSectionEnter(&cookie); - paste_mask_1(imOut, imIn, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize); - ImagingSectionLeave(&cookie); - - } else if (strcmp(imMask->mode, "L") == 0) { - ImagingSectionEnter(&cookie); - paste_mask_L(imOut, imIn, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize); - ImagingSectionLeave(&cookie); - - } else if (strcmp(imMask->mode, "LA") == 0 || strcmp(imMask->mode, "RGBA") == 0) { - ImagingSectionEnter(&cookie); - paste_mask_RGBA( - imOut, imIn, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize); - ImagingSectionLeave(&cookie); - - } else if (strcmp(imMask->mode, "RGBa") == 0) { - ImagingSectionEnter(&cookie); - paste_mask_RGBa( - imOut, imIn, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize); - ImagingSectionLeave(&cookie); - - } else { - (void)ImagingError_ValueError("bad transparency mask"); - return -1; - } - - return 0; -} - -static inline void -fill( - Imaging imOut, - const void *ink_, - int dx, - int dy, - int xsize, - int ysize, - int pixelsize) { - /* fill opaque region */ - - int x, y; - UINT8 ink8 = 0; - INT32 ink32 = 0L; - - memcpy(&ink32, ink_, pixelsize); - memcpy(&ink8, ink_, sizeof(ink8)); - - if (imOut->image8 || ink32 == 0L) { - dx *= pixelsize; - xsize *= pixelsize; - for (y = 0; y < ysize; y++) { - memset(imOut->image[y + dy] + dx, ink8, xsize); - } - - } else { - for (y = 0; y < ysize; y++) { - INT32 *out = imOut->image32[y + dy] + dx; - for (x = 0; x < xsize; x++) { - out[x] = ink32; - } - } - } -} - -static inline void -fill_mask_1( - Imaging imOut, - const void *ink_, - Imaging imMask, - int dx, - int dy, - int sx, - int sy, - int xsize, - int ysize, - int pixelsize) { - /* fill with mode "1" mask */ - - int x, y; - UINT8 ink8 = 0; - INT32 ink32 = 0L; - - memcpy(&ink32, ink_, pixelsize); - memcpy(&ink8, ink_, sizeof(ink8)); - - if (imOut->image8) { - for (y = 0; y < ysize; y++) { - UINT8 *out = imOut->image8[y + dy] + dx; - UINT8 *mask = imMask->image8[y + sy] + sx; - for (x = 0; x < xsize; x++) { - if (*mask++) { - *out = ink8; - } - out++; - } - } - - } else { - for (y = 0; y < ysize; y++) { - INT32 *out = imOut->image32[y + dy] + dx; - UINT8 *mask = imMask->image8[y + sy] + sx; - for (x = 0; x < xsize; x++) { - if (*mask++) { - *out = ink32; - } - out++; - } - } - } -} - -static inline void -fill_mask_L( - Imaging imOut, - const UINT8 *ink, - Imaging imMask, - int dx, - int dy, - int sx, - int sy, - int xsize, - int ysize, - int pixelsize) { - /* fill with mode "L" matte */ - - int x, y, i; - unsigned int tmp1; - - if (imOut->image8) { - for (y = 0; y < ysize; y++) { - UINT8 *out = imOut->image8[y + dy] + dx; - if (strncmp(imOut->mode, "I;16", 4) == 0) { - out += dx; - } - UINT8 *mask = imMask->image8[y + sy] + sx; - for (x = 0; x < xsize; x++) { - *out = BLEND(*mask, *out, ink[0], tmp1); - if (strncmp(imOut->mode, "I;16", 4) == 0) { - out++; - *out = BLEND(*mask, *out, ink[1], tmp1); - } - out++, mask++; - } - } - - } else { - int alpha_channel = - strcmp(imOut->mode, "RGBa") == 0 || strcmp(imOut->mode, "RGBA") == 0 || - strcmp(imOut->mode, "La") == 0 || strcmp(imOut->mode, "LA") == 0 || - strcmp(imOut->mode, "PA") == 0; - for (y = 0; y < ysize; y++) { - UINT8 *out = (UINT8 *)imOut->image[y + dy] + dx * pixelsize; - UINT8 *mask = (UINT8 *)imMask->image[y + sy] + sx; - for (x = 0; x < xsize; x++) { - for (i = 0; i < pixelsize; i++) { - UINT8 channel_mask = *mask; - if (alpha_channel && i != 3 && channel_mask != 0) { - channel_mask = - 255 - (255 - channel_mask) * (1 - (255 - out[3]) / 255); - } - out[i] = BLEND(channel_mask, out[i], ink[i], tmp1); - } - out += pixelsize; - mask++; - } - } - } -} - -static inline void -fill_mask_RGBA( - Imaging imOut, - const UINT8 *ink, - Imaging imMask, - int dx, - int dy, - int sx, - int sy, - int xsize, - int ysize, - int pixelsize) { - /* fill with mode "RGBA" matte */ - - int x, y, i; - unsigned int tmp1; - - if (imOut->image8) { - sx = sx * 4 + 3; - for (y = 0; y < ysize; y++) { - UINT8 *out = imOut->image8[y + dy] + dx; - UINT8 *mask = (UINT8 *)imMask->image[y + sy] + sx; - for (x = 0; x < xsize; x++) { - *out = BLEND(*mask, *out, ink[0], tmp1); - out++, mask += 4; - } - } - - } else { - dx *= pixelsize; - sx = sx * 4 + 3; - for (y = 0; y < ysize; y++) { - UINT8 *out = (UINT8 *)imOut->image[y + dy] + dx; - UINT8 *mask = (UINT8 *)imMask->image[y + sy] + sx; - for (x = 0; x < xsize; x++) { - for (i = 0; i < pixelsize; i++) { - *out = BLEND(*mask, *out, ink[i], tmp1); - out++; - } - mask += 4; - } - } - } -} - -static inline void -fill_mask_RGBa( - Imaging imOut, - const UINT8 *ink, - Imaging imMask, - int dx, - int dy, - int sx, - int sy, - int xsize, - int ysize, - int pixelsize) { - /* fill with mode "RGBa" matte */ - - int x, y, i; - unsigned int tmp1; - - if (imOut->image8) { - sx = sx * 4 + 3; - for (y = 0; y < ysize; y++) { - UINT8 *out = imOut->image8[y + dy] + dx; - UINT8 *mask = (UINT8 *)imMask->image[y + sy] + sx; - for (x = 0; x < xsize; x++) { - *out = PREBLEND(*mask, *out, ink[0], tmp1); - out++, mask += 4; - } - } - - } else { - dx *= pixelsize; - sx = sx * 4 + 3; - for (y = 0; y < ysize; y++) { - UINT8 *out = (UINT8 *)imOut->image[y + dy] + dx; - UINT8 *mask = (UINT8 *)imMask->image[y + sy] + sx; - for (x = 0; x < xsize; x++) { - for (i = 0; i < pixelsize; i++) { - *out = PREBLEND(*mask, *out, ink[i], tmp1); - out++; - } - mask += 4; - } - } - } -} - -int -ImagingFill2( - Imaging imOut, - const void *ink, - Imaging imMask, - int dx0, - int dy0, - int dx1, - int dy1) { - ImagingSectionCookie cookie; - int xsize, ysize; - int pixelsize; - int sx0, sy0; - - if (!imOut || !ink) { - (void)ImagingError_ModeError(); - return -1; - } - - pixelsize = imOut->pixelsize; - - xsize = dx1 - dx0; - ysize = dy1 - dy0; - - if (imMask && (xsize != imMask->xsize || ysize != imMask->ysize)) { - (void)ImagingError_Mismatch(); - return -1; - } - - /* Determine which region to fill */ - sx0 = sy0 = 0; - if (dx0 < 0) { - xsize += dx0, sx0 = -dx0, dx0 = 0; - } - if (dx0 + xsize > imOut->xsize) { - xsize = imOut->xsize - dx0; - } - if (dy0 < 0) { - ysize += dy0, sy0 = -dy0, dy0 = 0; - } - if (dy0 + ysize > imOut->ysize) { - ysize = imOut->ysize - dy0; - } - - if (xsize <= 0 || ysize <= 0) { - return 0; - } - - if (!imMask) { - ImagingSectionEnter(&cookie); - fill(imOut, ink, dx0, dy0, xsize, ysize, pixelsize); - ImagingSectionLeave(&cookie); - - } else if (strcmp(imMask->mode, "1") == 0) { - ImagingSectionEnter(&cookie); - fill_mask_1(imOut, ink, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize); - ImagingSectionLeave(&cookie); - - } else if (strcmp(imMask->mode, "L") == 0) { - ImagingSectionEnter(&cookie); - fill_mask_L(imOut, ink, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize); - ImagingSectionLeave(&cookie); - - } else if (strcmp(imMask->mode, "RGBA") == 0) { - ImagingSectionEnter(&cookie); - fill_mask_RGBA(imOut, ink, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize); - ImagingSectionLeave(&cookie); - - } else if (strcmp(imMask->mode, "RGBa") == 0) { - ImagingSectionEnter(&cookie); - fill_mask_RGBa(imOut, ink, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize); - ImagingSectionLeave(&cookie); - - } else { - (void)ImagingError_ValueError("bad transparency mask"); - return -1; - } - - return 0; -} +/* + * The Python Imaging Library + * $Id$ + * + * paste image on another image + * + * history: + * 96-03-27 fl Created + * 96-07-16 fl Support "1", "L" and "RGBA" masks + * 96-08-16 fl Merged with opaque paste + * 97-01-17 fl Faster blending, added support for RGBa images + * 97-08-27 fl Faster masking for 32-bit images + * 98-02-02 fl Fixed MULDIV255 macro for gcc + * 99-02-02 fl Added "RGBa" mask support + * 99-02-06 fl Rewritten. Added support for masked fill operations. + * 99-12-08 fl Fixed matte fill. + * + * Copyright (c) Fredrik Lundh 1996-97. + * Copyright (c) Secret Labs AB 1997-99. + * + * See the README file for information on usage and redistribution. + */ + +#include "Imaging.h" + +static inline void +paste( + Imaging imOut, + Imaging imIn, + int dx, + int dy, + int sx, + int sy, + int xsize, + int ysize, + int pixelsize) { + /* paste opaque region */ + + int y; + + dx *= pixelsize; + sx *= pixelsize; + + xsize *= pixelsize; + + for (y = 0; y < ysize; y++) { + memcpy(imOut->image[y + dy] + dx, imIn->image[y + sy] + sx, xsize); + } +} + +static inline void +paste_mask_1( + Imaging imOut, + Imaging imIn, + Imaging imMask, + int dx, + int dy, + int sx, + int sy, + int xsize, + int ysize, + int pixelsize) { + /* paste with mode "1" mask */ + + int x, y; + + if (imOut->image8) { + for (y = 0; y < ysize; y++) { + UINT8 *out = imOut->image8[y + dy] + dx; + UINT8 *in = imIn->image8[y + sy] + sx; + UINT8 *mask = imMask->image8[y + sy] + sx; + for (x = 0; x < xsize; x++) { + if (*mask++) { + *out = *in; + } + out++, in++; + } + } + + } else { + for (y = 0; y < ysize; y++) { + INT32 *out = imOut->image32[y + dy] + dx; + INT32 *in = imIn->image32[y + sy] + sx; + UINT8 *mask = imMask->image8[y + sy] + sx; + for (x = 0; x < xsize; x++) { + if (*mask++) { + *out = *in; + } + out++, in++; + } + } + } +} + +static inline void +paste_mask_L( + Imaging imOut, + Imaging imIn, + Imaging imMask, + int dx, + int dy, + int sx, + int sy, + int xsize, + int ysize, + int pixelsize) { + /* paste with mode "L" matte */ + + int x, y; + unsigned int tmp1; + + if (imOut->image8) { + for (y = 0; y < ysize; y++) { + UINT8 *out = imOut->image8[y + dy] + dx; + UINT8 *in = imIn->image8[y + sy] + sx; + UINT8 *mask = imMask->image8[y + sy] + sx; + for (x = 0; x < xsize; x++) { + *out = BLEND(*mask, *out, *in, tmp1); + out++, in++, mask++; + } + } + + } else { + for (y = 0; y < ysize; y++) { + UINT8 *out = (UINT8 *)(imOut->image32[y + dy] + dx); + UINT8 *in = (UINT8 *)(imIn->image32[y + sy] + sx); + UINT8 *mask = (UINT8 *)(imMask->image8[y + sy] + sx); + for (x = 0; x < xsize; x++) { + UINT8 a = mask[0]; + out[0] = BLEND(a, out[0], in[0], tmp1); + out[1] = BLEND(a, out[1], in[1], tmp1); + out[2] = BLEND(a, out[2], in[2], tmp1); + out[3] = BLEND(a, out[3], in[3], tmp1); + out += 4; + in += 4; + mask++; + } + } + } +} + +static inline void +paste_mask_RGBA( + Imaging imOut, + Imaging imIn, + Imaging imMask, + int dx, + int dy, + int sx, + int sy, + int xsize, + int ysize, + int pixelsize) { + /* paste with mode "RGBA" matte */ + + int x, y; + unsigned int tmp1; + + if (imOut->image8) { + for (y = 0; y < ysize; y++) { + UINT8 *out = imOut->image8[y + dy] + dx; + UINT8 *in = imIn->image8[y + sy] + sx; + UINT8 *mask = (UINT8 *)imMask->image[y + sy] + sx * 4 + 3; + for (x = 0; x < xsize; x++) { + *out = BLEND(*mask, *out, *in, tmp1); + out++, in++, mask += 4; + } + } + + } else { + for (y = 0; y < ysize; y++) { + UINT8 *out = (UINT8 *)(imOut->image32[y + dy] + dx); + UINT8 *in = (UINT8 *)(imIn->image32[y + sy] + sx); + UINT8 *mask = (UINT8 *)(imMask->image32[y + sy] + sx); + for (x = 0; x < xsize; x++) { + UINT8 a = mask[3]; + out[0] = BLEND(a, out[0], in[0], tmp1); + out[1] = BLEND(a, out[1], in[1], tmp1); + out[2] = BLEND(a, out[2], in[2], tmp1); + out[3] = BLEND(a, out[3], in[3], tmp1); + out += 4; + in += 4; + mask += 4; + } + } + } +} + +static inline void +paste_mask_RGBa( + Imaging imOut, + Imaging imIn, + Imaging imMask, + int dx, + int dy, + int sx, + int sy, + int xsize, + int ysize, + int pixelsize) { + /* paste with mode "RGBa" matte */ + + int x, y; + unsigned int tmp1; + + if (imOut->image8) { + for (y = 0; y < ysize; y++) { + UINT8 *out = imOut->image8[y + dy] + dx; + UINT8 *in = imIn->image8[y + sy] + sx; + UINT8 *mask = (UINT8 *)imMask->image[y + sy] + sx * 4 + 3; + for (x = 0; x < xsize; x++) { + *out = PREBLEND(*mask, *out, *in, tmp1); + out++, in++, mask += 4; + } + } + + } else { + for (y = 0; y < ysize; y++) { + UINT8 *out = (UINT8 *)(imOut->image32[y + dy] + dx); + UINT8 *in = (UINT8 *)(imIn->image32[y + sy] + sx); + UINT8 *mask = (UINT8 *)(imMask->image32[y + sy] + sx); + for (x = 0; x < xsize; x++) { + UINT8 a = mask[3]; + out[0] = PREBLEND(a, out[0], in[0], tmp1); + out[1] = PREBLEND(a, out[1], in[1], tmp1); + out[2] = PREBLEND(a, out[2], in[2], tmp1); + out[3] = PREBLEND(a, out[3], in[3], tmp1); + out += 4; + in += 4; + mask += 4; + } + } + } +} + +int +ImagingPaste( + Imaging imOut, Imaging imIn, Imaging imMask, int dx0, int dy0, int dx1, int dy1) { + int xsize, ysize; + int pixelsize; + int sx0, sy0; + ImagingSectionCookie cookie; + + if (!imOut || !imIn) { + (void)ImagingError_ModeError(); + return -1; + } + + pixelsize = imOut->pixelsize; + + xsize = dx1 - dx0; + ysize = dy1 - dy0; + + if (xsize != imIn->xsize || ysize != imIn->ysize || pixelsize != imIn->pixelsize) { + (void)ImagingError_Mismatch(); + return -1; + } + + if (imMask && (xsize != imMask->xsize || ysize != imMask->ysize)) { + (void)ImagingError_Mismatch(); + return -1; + } + + /* Determine which region to copy */ + sx0 = sy0 = 0; + if (dx0 < 0) { + xsize += dx0, sx0 = -dx0, dx0 = 0; + } + if (dx0 + xsize > imOut->xsize) { + xsize = imOut->xsize - dx0; + } + if (dy0 < 0) { + ysize += dy0, sy0 = -dy0, dy0 = 0; + } + if (dy0 + ysize > imOut->ysize) { + ysize = imOut->ysize - dy0; + } + + if (xsize <= 0 || ysize <= 0) { + return 0; + } + + if (!imMask) { + ImagingSectionEnter(&cookie); + paste(imOut, imIn, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize); + ImagingSectionLeave(&cookie); + + } else if (strcmp(imMask->mode, "1") == 0) { + ImagingSectionEnter(&cookie); + paste_mask_1(imOut, imIn, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize); + ImagingSectionLeave(&cookie); + + } else if (strcmp(imMask->mode, "L") == 0) { + ImagingSectionEnter(&cookie); + paste_mask_L(imOut, imIn, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize); + ImagingSectionLeave(&cookie); + + } else if (strcmp(imMask->mode, "LA") == 0 || strcmp(imMask->mode, "RGBA") == 0) { + ImagingSectionEnter(&cookie); + paste_mask_RGBA( + imOut, imIn, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize); + ImagingSectionLeave(&cookie); + + } else if (strcmp(imMask->mode, "RGBa") == 0) { + ImagingSectionEnter(&cookie); + paste_mask_RGBa( + imOut, imIn, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize); + ImagingSectionLeave(&cookie); + + } else { + (void)ImagingError_ValueError("bad transparency mask"); + return -1; + } + + return 0; +} + +static inline void +fill( + Imaging imOut, + const void *ink_, + int dx, + int dy, + int xsize, + int ysize, + int pixelsize) { + /* fill opaque region */ + + int x, y; + UINT8 ink8 = 0; + INT32 ink32 = 0L; + + memcpy(&ink32, ink_, pixelsize); + memcpy(&ink8, ink_, sizeof(ink8)); + + if (imOut->image8 || ink32 == 0L) { + dx *= pixelsize; + xsize *= pixelsize; + for (y = 0; y < ysize; y++) { + memset(imOut->image[y + dy] + dx, ink8, xsize); + } + + } else { + for (y = 0; y < ysize; y++) { + INT32 *out = imOut->image32[y + dy] + dx; + for (x = 0; x < xsize; x++) { + out[x] = ink32; + } + } + } +} + +static inline void +fill_mask_1( + Imaging imOut, + const void *ink_, + Imaging imMask, + int dx, + int dy, + int sx, + int sy, + int xsize, + int ysize, + int pixelsize) { + /* fill with mode "1" mask */ + + int x, y; + UINT8 ink8 = 0; + INT32 ink32 = 0L; + + memcpy(&ink32, ink_, pixelsize); + memcpy(&ink8, ink_, sizeof(ink8)); + + if (imOut->image8) { + for (y = 0; y < ysize; y++) { + UINT8 *out = imOut->image8[y + dy] + dx; + UINT8 *mask = imMask->image8[y + sy] + sx; + for (x = 0; x < xsize; x++) { + if (*mask++) { + *out = ink8; + } + out++; + } + } + + } else { + for (y = 0; y < ysize; y++) { + INT32 *out = imOut->image32[y + dy] + dx; + UINT8 *mask = imMask->image8[y + sy] + sx; + for (x = 0; x < xsize; x++) { + if (*mask++) { + *out = ink32; + } + out++; + } + } + } +} + +static inline void +fill_mask_L( + Imaging imOut, + const UINT8 *ink, + Imaging imMask, + int dx, + int dy, + int sx, + int sy, + int xsize, + int ysize, + int pixelsize) { + /* fill with mode "L" matte */ + + int x, y, i; + unsigned int tmp1; + + if (imOut->image8) { + for (y = 0; y < ysize; y++) { + UINT8 *out = imOut->image8[y + dy] + dx; + if (strncmp(imOut->mode, "I;16", 4) == 0) { + out += dx; + } + UINT8 *mask = imMask->image8[y + sy] + sx; + for (x = 0; x < xsize; x++) { + *out = BLEND(*mask, *out, ink[0], tmp1); + if (strncmp(imOut->mode, "I;16", 4) == 0) { + out++; + *out = BLEND(*mask, *out, ink[1], tmp1); + } + out++, mask++; + } + } + + } else { + int alpha_channel = + strcmp(imOut->mode, "RGBa") == 0 || strcmp(imOut->mode, "RGBA") == 0 || + strcmp(imOut->mode, "La") == 0 || strcmp(imOut->mode, "LA") == 0 || + strcmp(imOut->mode, "PA") == 0; + for (y = 0; y < ysize; y++) { + UINT8 *out = (UINT8 *)imOut->image[y + dy] + dx * pixelsize; + UINT8 *mask = (UINT8 *)imMask->image[y + sy] + sx; + for (x = 0; x < xsize; x++) { + for (i = 0; i < pixelsize; i++) { + UINT8 channel_mask = *mask; + if (alpha_channel && i != 3 && channel_mask != 0) { + channel_mask = + 255 - (255 - channel_mask) * (1 - (255 - out[3]) / 255); + } + out[i] = BLEND(channel_mask, out[i], ink[i], tmp1); + } + out += pixelsize; + mask++; + } + } + } +} + +static inline void +fill_mask_RGBA( + Imaging imOut, + const UINT8 *ink, + Imaging imMask, + int dx, + int dy, + int sx, + int sy, + int xsize, + int ysize, + int pixelsize) { + /* fill with mode "RGBA" matte */ + + int x, y, i; + unsigned int tmp1; + + if (imOut->image8) { + sx = sx * 4 + 3; + for (y = 0; y < ysize; y++) { + UINT8 *out = imOut->image8[y + dy] + dx; + UINT8 *mask = (UINT8 *)imMask->image[y + sy] + sx; + for (x = 0; x < xsize; x++) { + *out = BLEND(*mask, *out, ink[0], tmp1); + out++, mask += 4; + } + } + + } else { + dx *= pixelsize; + sx = sx * 4 + 3; + for (y = 0; y < ysize; y++) { + UINT8 *out = (UINT8 *)imOut->image[y + dy] + dx; + UINT8 *mask = (UINT8 *)imMask->image[y + sy] + sx; + for (x = 0; x < xsize; x++) { + for (i = 0; i < pixelsize; i++) { + *out = BLEND(*mask, *out, ink[i], tmp1); + out++; + } + mask += 4; + } + } + } +} + +static inline void +fill_mask_RGBa( + Imaging imOut, + const UINT8 *ink, + Imaging imMask, + int dx, + int dy, + int sx, + int sy, + int xsize, + int ysize, + int pixelsize) { + /* fill with mode "RGBa" matte */ + + int x, y, i; + unsigned int tmp1; + + if (imOut->image8) { + sx = sx * 4 + 3; + for (y = 0; y < ysize; y++) { + UINT8 *out = imOut->image8[y + dy] + dx; + UINT8 *mask = (UINT8 *)imMask->image[y + sy] + sx; + for (x = 0; x < xsize; x++) { + *out = PREBLEND(*mask, *out, ink[0], tmp1); + out++, mask += 4; + } + } + + } else { + dx *= pixelsize; + sx = sx * 4 + 3; + for (y = 0; y < ysize; y++) { + UINT8 *out = (UINT8 *)imOut->image[y + dy] + dx; + UINT8 *mask = (UINT8 *)imMask->image[y + sy] + sx; + for (x = 0; x < xsize; x++) { + for (i = 0; i < pixelsize; i++) { + *out = PREBLEND(*mask, *out, ink[i], tmp1); + out++; + } + mask += 4; + } + } + } +} + +int +ImagingFill2( + Imaging imOut, + const void *ink, + Imaging imMask, + int dx0, + int dy0, + int dx1, + int dy1) { + ImagingSectionCookie cookie; + int xsize, ysize; + int pixelsize; + int sx0, sy0; + + if (!imOut || !ink) { + (void)ImagingError_ModeError(); + return -1; + } + + pixelsize = imOut->pixelsize; + + xsize = dx1 - dx0; + ysize = dy1 - dy0; + + if (imMask && (xsize != imMask->xsize || ysize != imMask->ysize)) { + (void)ImagingError_Mismatch(); + return -1; + } + + /* Determine which region to fill */ + sx0 = sy0 = 0; + if (dx0 < 0) { + xsize += dx0, sx0 = -dx0, dx0 = 0; + } + if (dx0 + xsize > imOut->xsize) { + xsize = imOut->xsize - dx0; + } + if (dy0 < 0) { + ysize += dy0, sy0 = -dy0, dy0 = 0; + } + if (dy0 + ysize > imOut->ysize) { + ysize = imOut->ysize - dy0; + } + + if (xsize <= 0 || ysize <= 0) { + return 0; + } + + if (!imMask) { + ImagingSectionEnter(&cookie); + fill(imOut, ink, dx0, dy0, xsize, ysize, pixelsize); + ImagingSectionLeave(&cookie); + + } else if (strcmp(imMask->mode, "1") == 0) { + ImagingSectionEnter(&cookie); + fill_mask_1(imOut, ink, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize); + ImagingSectionLeave(&cookie); + + } else if (strcmp(imMask->mode, "L") == 0) { + ImagingSectionEnter(&cookie); + fill_mask_L(imOut, ink, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize); + ImagingSectionLeave(&cookie); + + } else if (strcmp(imMask->mode, "RGBA") == 0) { + ImagingSectionEnter(&cookie); + fill_mask_RGBA(imOut, ink, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize); + ImagingSectionLeave(&cookie); + + } else if (strcmp(imMask->mode, "RGBa") == 0) { + ImagingSectionEnter(&cookie); + fill_mask_RGBa(imOut, ink, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize); + ImagingSectionLeave(&cookie); + + } else { + (void)ImagingError_ValueError("bad transparency mask"); + return -1; + } + + return 0; +} diff --git a/src/libImaging/PcdDecode.c b/src/libImaging/PcdDecode.c index f13803cb6..0de509746 100644 --- a/src/libImaging/PcdDecode.c +++ b/src/libImaging/PcdDecode.c @@ -1,74 +1,74 @@ -/* - * The Python Imaging Library. - * $Id$ - * - * decoder for uncompressed PCD image data. - * - * history: - * 96-05-10 fl Created - * 96-05-18 fl New tables - * 97-01-25 fl Use PhotoYCC unpacker - * - * notes: - * This driver supports uncompressed PCD modes only - * (resolutions up to 768x512). - * - * Copyright (c) Fredrik Lundh 1996-97. - * Copyright (c) Secret Labs AB 1997. - * - * See the README file for information on usage and redistribution. - */ - -#include "Imaging.h" - -int -ImagingPcdDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) { - int x; - int chunk; - UINT8 *out; - UINT8 *ptr; - - ptr = buf; - - chunk = 3 * state->xsize; - - for (;;) { - /* We need data for two full lines before we can do anything */ - if (bytes < chunk) { - return ptr - buf; - } - - /* Unpack first line */ - out = state->buffer; - for (x = 0; x < state->xsize; x++) { - out[0] = ptr[x]; - out[1] = ptr[(x + 4 * state->xsize) / 2]; - out[2] = ptr[(x + 5 * state->xsize) / 2]; - out += 3; - } - - state->shuffle((UINT8 *)im->image[state->y], state->buffer, state->xsize); - - if (++state->y >= state->ysize) { - return -1; /* This can hardly happen */ - } - - /* Unpack second line */ - out = state->buffer; - for (x = 0; x < state->xsize; x++) { - out[0] = ptr[x + state->xsize]; - out[1] = ptr[(x + 4 * state->xsize) / 2]; - out[2] = ptr[(x + 5 * state->xsize) / 2]; - out += 3; - } - - state->shuffle((UINT8 *)im->image[state->y], state->buffer, state->xsize); - - if (++state->y >= state->ysize) { - return -1; - } - - ptr += chunk; - bytes -= chunk; - } -} +/* + * The Python Imaging Library. + * $Id$ + * + * decoder for uncompressed PCD image data. + * + * history: + * 96-05-10 fl Created + * 96-05-18 fl New tables + * 97-01-25 fl Use PhotoYCC unpacker + * + * notes: + * This driver supports uncompressed PCD modes only + * (resolutions up to 768x512). + * + * Copyright (c) Fredrik Lundh 1996-97. + * Copyright (c) Secret Labs AB 1997. + * + * See the README file for information on usage and redistribution. + */ + +#include "Imaging.h" + +int +ImagingPcdDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) { + int x; + int chunk; + UINT8 *out; + UINT8 *ptr; + + ptr = buf; + + chunk = 3 * state->xsize; + + for (;;) { + /* We need data for two full lines before we can do anything */ + if (bytes < chunk) { + return ptr - buf; + } + + /* Unpack first line */ + out = state->buffer; + for (x = 0; x < state->xsize; x++) { + out[0] = ptr[x]; + out[1] = ptr[(x + 4 * state->xsize) / 2]; + out[2] = ptr[(x + 5 * state->xsize) / 2]; + out += 3; + } + + state->shuffle((UINT8 *)im->image[state->y], state->buffer, state->xsize); + + if (++state->y >= state->ysize) { + return -1; /* This can hardly happen */ + } + + /* Unpack second line */ + out = state->buffer; + for (x = 0; x < state->xsize; x++) { + out[0] = ptr[x + state->xsize]; + out[1] = ptr[(x + 4 * state->xsize) / 2]; + out[2] = ptr[(x + 5 * state->xsize) / 2]; + out += 3; + } + + state->shuffle((UINT8 *)im->image[state->y], state->buffer, state->xsize); + + if (++state->y >= state->ysize) { + return -1; + } + + ptr += chunk; + bytes -= chunk; + } +} diff --git a/src/libImaging/PcxDecode.c b/src/libImaging/PcxDecode.c index c95ffc869..83ad7f6dc 100644 --- a/src/libImaging/PcxDecode.c +++ b/src/libImaging/PcxDecode.c @@ -1,89 +1,89 @@ -/* - * The Python Imaging Library. - * $Id$ - * - * decoder for PCX image data. - * - * history: - * 95-09-14 fl Created - * - * Copyright (c) Fredrik Lundh 1995. - * Copyright (c) Secret Labs AB 1997. - * - * See the README file for information on usage and redistribution. - */ - -#include "Imaging.h" - -int -ImagingPcxDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) { - UINT8 n; - UINT8 *ptr; - - if ((state->xsize * state->bits + 7) / 8 > state->bytes) { - state->errcode = IMAGING_CODEC_OVERRUN; - return -1; - } - - ptr = buf; - - for (;;) { - if (bytes < 1) { - return ptr - buf; - } - - if ((*ptr & 0xC0) == 0xC0) { - /* Run */ - if (bytes < 2) { - return ptr - buf; - } - - n = ptr[0] & 0x3F; - - while (n > 0) { - if (state->x >= state->bytes) { - state->errcode = IMAGING_CODEC_OVERRUN; - break; - } - state->buffer[state->x++] = ptr[1]; - n--; - } - - ptr += 2; - bytes -= 2; - - } else { - /* Literal */ - state->buffer[state->x++] = ptr[0]; - ptr++; - bytes--; - } - - if (state->x >= state->bytes) { - if (state->bytes % state->xsize && state->bytes > state->xsize) { - int bands = state->bytes / state->xsize; - int stride = state->bytes / bands; - int i; - for (i = 1; i < bands; i++) { // note -- skipping first band - memmove( - &state->buffer[i * state->xsize], - &state->buffer[i * stride], - state->xsize); - } - } - /* Got a full line, unpack it */ - state->shuffle( - (UINT8 *)im->image[state->y + state->yoff] + - state->xoff * im->pixelsize, - state->buffer, - state->xsize); - - state->x = 0; - - if (++state->y >= state->ysize) { - /* End of file (errcode = 0) */ - return -1; - } - } - } -} +/* + * The Python Imaging Library. + * $Id$ + * + * decoder for PCX image data. + * + * history: + * 95-09-14 fl Created + * + * Copyright (c) Fredrik Lundh 1995. + * Copyright (c) Secret Labs AB 1997. + * + * See the README file for information on usage and redistribution. + */ + +#include "Imaging.h" + +int +ImagingPcxDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) { + UINT8 n; + UINT8 *ptr; + + if ((state->xsize * state->bits + 7) / 8 > state->bytes) { + state->errcode = IMAGING_CODEC_OVERRUN; + return -1; + } + + ptr = buf; + + for (;;) { + if (bytes < 1) { + return ptr - buf; + } + + if ((*ptr & 0xC0) == 0xC0) { + /* Run */ + if (bytes < 2) { + return ptr - buf; + } + + n = ptr[0] & 0x3F; + + while (n > 0) { + if (state->x >= state->bytes) { + state->errcode = IMAGING_CODEC_OVERRUN; + break; + } + state->buffer[state->x++] = ptr[1]; + n--; + } + + ptr += 2; + bytes -= 2; + + } else { + /* Literal */ + state->buffer[state->x++] = ptr[0]; + ptr++; + bytes--; + } + + if (state->x >= state->bytes) { + if (state->bytes % state->xsize && state->bytes > state->xsize) { + int bands = state->bytes / state->xsize; + int stride = state->bytes / bands; + int i; + for (i = 1; i < bands; i++) { // note -- skipping first band + memmove( + &state->buffer[i * state->xsize], + &state->buffer[i * stride], + state->xsize); + } + } + /* Got a full line, unpack it */ + state->shuffle( + (UINT8 *)im->image[state->y + state->yoff] + + state->xoff * im->pixelsize, + state->buffer, + state->xsize); + + state->x = 0; + + if (++state->y >= state->ysize) { + /* End of file (errcode = 0) */ + return -1; + } + } + } +} diff --git a/src/libImaging/PcxEncode.c b/src/libImaging/PcxEncode.c index 549614bfd..d3bdee9d4 100644 --- a/src/libImaging/PcxEncode.c +++ b/src/libImaging/PcxEncode.c @@ -1,187 +1,187 @@ -/* - * The Python Imaging Library. - * $Id$ - * - * encoder for PCX data - * - * history: - * 99-02-07 fl created - * - * Copyright (c) Fredrik Lundh 1999. - * Copyright (c) Secret Labs AB 1999. - * - * See the README file for information on usage and redistribution. - */ - -#include "Imaging.h" - -enum { INIT, FETCH, ENCODE }; - -/* we're reusing "ystep" to store the last value */ -#define LAST ystep - -int -ImagingPcxEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { - UINT8 *ptr; - int this; - int bytes_per_line = 0; - int padding = 0; - int stride = 0; - int bpp = 0; - int planes = 1; - int i; - - ptr = buf; - - if (!state->state) { - /* sanity check */ - if (state->xsize <= 0 || state->ysize <= 0) { - state->errcode = IMAGING_CODEC_END; - return 0; - } - state->state = FETCH; - } - - bpp = state->bits; - if (state->bits == 24) { - planes = 3; - bpp = 8; - } - - bytes_per_line = (state->xsize * bpp + 7) / 8; - /* The stride here needs to be kept in sync with the version in - PcxImagePlugin.py. If it's not, the header and the body of the - image will be out of sync and bad things will happen on decode. - */ - stride = bytes_per_line + (bytes_per_line % 2); - - padding = stride - bytes_per_line; - - for (;;) { - switch (state->state) { - case FETCH: - - /* get a line of data */ - if (state->y >= state->ysize) { - state->errcode = IMAGING_CODEC_END; - return ptr - buf; - } - - state->shuffle( - state->buffer, - (UINT8 *)im->image[state->y + state->yoff] + - state->xoff * im->pixelsize, - state->xsize); - - state->y += 1; - - state->count = 1; - state->LAST = state->buffer[0]; - - state->x = 1; - - state->state = ENCODE; - /* fall through */ - - case ENCODE: - /* compress this line */ - - /* when we arrive here, "count" contains the number of - bytes having the value of "LAST" that we've already - seen */ - do { - /* If we're encoding an odd width file, and we've - got more than one plane, we need to pad each - color row with padding bytes at the end. Since - The pixels are stored RRRRRGGGGGBBBBB, so we need - to have the padding be RRRRRPGGGGGPBBBBBP. Hence - the double loop - */ - while (state->x % bytes_per_line) { - if (state->count == 63) { - /* this run is full; flush it */ - if (bytes < 2) { - return ptr - buf; - } - ptr[0] = 0xff; - ptr[1] = state->LAST; - ptr += 2; - bytes -= 2; - - state->count = 0; - } - - this = state->buffer[state->x]; - - if (this == state->LAST) { - /* extend the current run */ - state->x += 1; - state->count += 1; - - } else { - /* start a new run */ - if (state->count == 1 && (state->LAST < 0xc0)) { - if (bytes < 1) { - return ptr - buf; - } - ptr[0] = state->LAST; - ptr += 1; - bytes -= 1; - } else { - if (state->count > 0) { - if (bytes < 2) { - return ptr - buf; - } - ptr[0] = 0xc0 | state->count; - ptr[1] = state->LAST; - ptr += 2; - bytes -= 2; - } - } - - state->LAST = this; - state->count = 1; - - state->x += 1; - } - } - - /* end of line; flush the current run */ - if (state->count == 1 && (state->LAST < 0xc0)) { - if (bytes < 1 + padding) { - return ptr - buf; - } - ptr[0] = state->LAST; - ptr += 1; - bytes -= 1; - } else { - if (state->count > 0) { - if (bytes < 2 + padding) { - return ptr - buf; - } - ptr[0] = 0xc0 | state->count; - ptr[1] = state->LAST; - ptr += 2; - bytes -= 2; - } - } - /* add the padding */ - for (i = 0; i < padding; i++) { - ptr[0] = 0; - ptr += 1; - bytes -= 1; - } - /* reset for the next color plane. */ - if (state->x < planes * bytes_per_line) { - state->count = 1; - state->LAST = state->buffer[state->x]; - state->x += 1; - } - } while (state->x < planes * bytes_per_line); - - /* read next line */ - state->state = FETCH; - break; - } - } -} +/* + * The Python Imaging Library. + * $Id$ + * + * encoder for PCX data + * + * history: + * 99-02-07 fl created + * + * Copyright (c) Fredrik Lundh 1999. + * Copyright (c) Secret Labs AB 1999. + * + * See the README file for information on usage and redistribution. + */ + +#include "Imaging.h" + +enum { INIT, FETCH, ENCODE }; + +/* we're reusing "ystep" to store the last value */ +#define LAST ystep + +int +ImagingPcxEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { + UINT8 *ptr; + int this; + int bytes_per_line = 0; + int padding = 0; + int stride = 0; + int bpp = 0; + int planes = 1; + int i; + + ptr = buf; + + if (!state->state) { + /* sanity check */ + if (state->xsize <= 0 || state->ysize <= 0) { + state->errcode = IMAGING_CODEC_END; + return 0; + } + state->state = FETCH; + } + + bpp = state->bits; + if (state->bits == 24) { + planes = 3; + bpp = 8; + } + + bytes_per_line = (state->xsize * bpp + 7) / 8; + /* The stride here needs to be kept in sync with the version in + PcxImagePlugin.py. If it's not, the header and the body of the + image will be out of sync and bad things will happen on decode. + */ + stride = bytes_per_line + (bytes_per_line % 2); + + padding = stride - bytes_per_line; + + for (;;) { + switch (state->state) { + case FETCH: + + /* get a line of data */ + if (state->y >= state->ysize) { + state->errcode = IMAGING_CODEC_END; + return ptr - buf; + } + + state->shuffle( + state->buffer, + (UINT8 *)im->image[state->y + state->yoff] + + state->xoff * im->pixelsize, + state->xsize); + + state->y += 1; + + state->count = 1; + state->LAST = state->buffer[0]; + + state->x = 1; + + state->state = ENCODE; + /* fall through */ + + case ENCODE: + /* compress this line */ + + /* when we arrive here, "count" contains the number of + bytes having the value of "LAST" that we've already + seen */ + do { + /* If we're encoding an odd width file, and we've + got more than one plane, we need to pad each + color row with padding bytes at the end. Since + The pixels are stored RRRRRGGGGGBBBBB, so we need + to have the padding be RRRRRPGGGGGPBBBBBP. Hence + the double loop + */ + while (state->x % bytes_per_line) { + if (state->count == 63) { + /* this run is full; flush it */ + if (bytes < 2) { + return ptr - buf; + } + ptr[0] = 0xff; + ptr[1] = state->LAST; + ptr += 2; + bytes -= 2; + + state->count = 0; + } + + this = state->buffer[state->x]; + + if (this == state->LAST) { + /* extend the current run */ + state->x += 1; + state->count += 1; + + } else { + /* start a new run */ + if (state->count == 1 && (state->LAST < 0xc0)) { + if (bytes < 1) { + return ptr - buf; + } + ptr[0] = state->LAST; + ptr += 1; + bytes -= 1; + } else { + if (state->count > 0) { + if (bytes < 2) { + return ptr - buf; + } + ptr[0] = 0xc0 | state->count; + ptr[1] = state->LAST; + ptr += 2; + bytes -= 2; + } + } + + state->LAST = this; + state->count = 1; + + state->x += 1; + } + } + + /* end of line; flush the current run */ + if (state->count == 1 && (state->LAST < 0xc0)) { + if (bytes < 1 + padding) { + return ptr - buf; + } + ptr[0] = state->LAST; + ptr += 1; + bytes -= 1; + } else { + if (state->count > 0) { + if (bytes < 2 + padding) { + return ptr - buf; + } + ptr[0] = 0xc0 | state->count; + ptr[1] = state->LAST; + ptr += 2; + bytes -= 2; + } + } + /* add the padding */ + for (i = 0; i < padding; i++) { + ptr[0] = 0; + ptr += 1; + bytes -= 1; + } + /* reset for the next color plane. */ + if (state->x < planes * bytes_per_line) { + state->count = 1; + state->LAST = state->buffer[state->x]; + state->x += 1; + } + } while (state->x < planes * bytes_per_line); + + /* read next line */ + state->state = FETCH; + break; + } + } +} diff --git a/src/libImaging/Point.c b/src/libImaging/Point.c index dd06f3940..9b0bbed0c 100644 --- a/src/libImaging/Point.c +++ b/src/libImaging/Point.c @@ -1,270 +1,270 @@ -/* - * The Python Imaging Library - * $Id$ - * - * point (pixel) translation - * - * history: - * 1995-11-27 fl Created - * 1996-03-31 fl Fixed colour support - * 1996-08-13 fl Support 8-bit to "1" thresholding - * 1997-05-31 fl Added floating point transform - * 1998-07-02 fl Added integer point transform - * 1998-07-17 fl Support L to anything lookup - * 2004-12-18 fl Refactored; added I to L lookup - * - * Copyright (c) 1997-2004 by Secret Labs AB. - * Copyright (c) 1995-2004 by Fredrik Lundh. - * - * See the README file for information on usage and redistribution. - */ - -#include "Imaging.h" - -typedef struct { - const void *table; -} im_point_context; - -static void -im_point_8_8(Imaging imOut, Imaging imIn, im_point_context *context) { - int x, y; - /* 8-bit source, 8-bit destination */ - UINT8 *table = (UINT8 *)context->table; - for (y = 0; y < imIn->ysize; y++) { - UINT8 *in = imIn->image8[y]; - UINT8 *out = imOut->image8[y]; - for (x = 0; x < imIn->xsize; x++) { - out[x] = table[in[x]]; - } - } -} - -static void -im_point_2x8_2x8(Imaging imOut, Imaging imIn, im_point_context *context) { - int x, y; - /* 2x8-bit source, 2x8-bit destination */ - UINT8 *table = (UINT8 *)context->table; - for (y = 0; y < imIn->ysize; y++) { - UINT8 *in = (UINT8 *)imIn->image[y]; - UINT8 *out = (UINT8 *)imOut->image[y]; - for (x = 0; x < imIn->xsize; x++) { - out[0] = table[in[0]]; - out[3] = table[in[3] + 256]; - in += 4; - out += 4; - } - } -} - -static void -im_point_3x8_3x8(Imaging imOut, Imaging imIn, im_point_context *context) { - int x, y; - /* 3x8-bit source, 3x8-bit destination */ - UINT8 *table = (UINT8 *)context->table; - for (y = 0; y < imIn->ysize; y++) { - UINT8 *in = (UINT8 *)imIn->image[y]; - UINT8 *out = (UINT8 *)imOut->image[y]; - for (x = 0; x < imIn->xsize; x++) { - out[0] = table[in[0]]; - out[1] = table[in[1] + 256]; - out[2] = table[in[2] + 512]; - in += 4; - out += 4; - } - } -} - -static void -im_point_4x8_4x8(Imaging imOut, Imaging imIn, im_point_context *context) { - int x, y; - /* 4x8-bit source, 4x8-bit destination */ - UINT8 *table = (UINT8 *)context->table; - for (y = 0; y < imIn->ysize; y++) { - UINT8 *in = (UINT8 *)imIn->image[y]; - UINT8 *out = (UINT8 *)imOut->image[y]; - for (x = 0; x < imIn->xsize; x++) { - out[0] = table[in[0]]; - out[1] = table[in[1] + 256]; - out[2] = table[in[2] + 512]; - out[3] = table[in[3] + 768]; - in += 4; - out += 4; - } - } -} - -static void -im_point_8_32(Imaging imOut, Imaging imIn, im_point_context *context) { - int x, y; - /* 8-bit source, 32-bit destination */ - char *table = (char *)context->table; - for (y = 0; y < imIn->ysize; y++) { - UINT8 *in = imIn->image8[y]; - INT32 *out = imOut->image32[y]; - for (x = 0; x < imIn->xsize; x++) { - memcpy(out + x, table + in[x] * sizeof(INT32), sizeof(INT32)); - } - } -} - -static void -im_point_32_8(Imaging imOut, Imaging imIn, im_point_context *context) { - int x, y; - /* 32-bit source, 8-bit destination */ - UINT8 *table = (UINT8 *)context->table; - for (y = 0; y < imIn->ysize; y++) { - INT32 *in = imIn->image32[y]; - UINT8 *out = imOut->image8[y]; - for (x = 0; x < imIn->xsize; x++) { - int v = in[x]; - if (v < 0) { - v = 0; - } else if (v > 65535) { - v = 65535; - } - out[x] = table[v]; - } - } -} - -Imaging -ImagingPoint(Imaging imIn, const char *mode, const void *table) { - /* lookup table transform */ - - ImagingSectionCookie cookie; - Imaging imOut; - im_point_context context; - void (*point)(Imaging imIn, Imaging imOut, im_point_context *context); - - if (!imIn) { - return (Imaging)ImagingError_ModeError(); - } - - if (!mode) { - mode = imIn->mode; - } - - if (imIn->type != IMAGING_TYPE_UINT8) { - if (imIn->type != IMAGING_TYPE_INT32 || strcmp(mode, "L") != 0) { - goto mode_mismatch; - } - } else if (!imIn->image8 && strcmp(imIn->mode, mode) != 0) { - goto mode_mismatch; - } - - imOut = ImagingNew(mode, imIn->xsize, imIn->ysize); - if (!imOut) { - return NULL; - } - - /* find appropriate handler */ - if (imIn->type == IMAGING_TYPE_UINT8) { - if (imIn->bands == imOut->bands && imIn->type == imOut->type) { - switch (imIn->bands) { - case 1: - point = im_point_8_8; - break; - case 2: - point = im_point_2x8_2x8; - break; - case 3: - point = im_point_3x8_3x8; - break; - case 4: - point = im_point_4x8_4x8; - break; - default: - /* this cannot really happen */ - point = im_point_8_8; - break; - } - } else { - point = im_point_8_32; - } - } else { - point = im_point_32_8; - } - - ImagingCopyPalette(imOut, imIn); - - ImagingSectionEnter(&cookie); - - context.table = table; - point(imOut, imIn, &context); - - ImagingSectionLeave(&cookie); - - return imOut; - -mode_mismatch: - return (Imaging)ImagingError_ValueError( - "point operation not supported for this mode"); -} - -Imaging -ImagingPointTransform(Imaging imIn, double scale, double offset) { - /* scale/offset transform */ - - ImagingSectionCookie cookie; - Imaging imOut; - int x, y; - - if (!imIn || (strcmp(imIn->mode, "I") != 0 && strcmp(imIn->mode, "I;16") != 0 && - strcmp(imIn->mode, "F") != 0)) { - return (Imaging)ImagingError_ModeError(); - } - - imOut = ImagingNew(imIn->mode, imIn->xsize, imIn->ysize); - if (!imOut) { - return NULL; - } - - switch (imIn->type) { - case IMAGING_TYPE_INT32: - ImagingSectionEnter(&cookie); - for (y = 0; y < imIn->ysize; y++) { - INT32 *in = imIn->image32[y]; - INT32 *out = imOut->image32[y]; - /* FIXME: add clipping? */ - for (x = 0; x < imIn->xsize; x++) { - out[x] = in[x] * scale + offset; - } - } - ImagingSectionLeave(&cookie); - break; - case IMAGING_TYPE_FLOAT32: - ImagingSectionEnter(&cookie); - for (y = 0; y < imIn->ysize; y++) { - FLOAT32 *in = (FLOAT32 *)imIn->image32[y]; - FLOAT32 *out = (FLOAT32 *)imOut->image32[y]; - for (x = 0; x < imIn->xsize; x++) { - out[x] = in[x] * scale + offset; - } - } - ImagingSectionLeave(&cookie); - break; - case IMAGING_TYPE_SPECIAL: - if (strcmp(imIn->mode, "I;16") == 0) { - ImagingSectionEnter(&cookie); - for (y = 0; y < imIn->ysize; y++) { - char *in = (char *)imIn->image[y]; - char *out = (char *)imOut->image[y]; - /* FIXME: add clipping? */ - for (x = 0; x < imIn->xsize; x++) { - UINT16 v; - memcpy(&v, in + x * sizeof(v), sizeof(v)); - v = v * scale + offset; - memcpy(out + x * sizeof(UINT16), &v, sizeof(v)); - } - } - ImagingSectionLeave(&cookie); - break; - } - /* FALL THROUGH */ - default: - ImagingDelete(imOut); - return (Imaging)ImagingError_ValueError("internal error"); - } - - return imOut; -} +/* + * The Python Imaging Library + * $Id$ + * + * point (pixel) translation + * + * history: + * 1995-11-27 fl Created + * 1996-03-31 fl Fixed colour support + * 1996-08-13 fl Support 8-bit to "1" thresholding + * 1997-05-31 fl Added floating point transform + * 1998-07-02 fl Added integer point transform + * 1998-07-17 fl Support L to anything lookup + * 2004-12-18 fl Refactored; added I to L lookup + * + * Copyright (c) 1997-2004 by Secret Labs AB. + * Copyright (c) 1995-2004 by Fredrik Lundh. + * + * See the README file for information on usage and redistribution. + */ + +#include "Imaging.h" + +typedef struct { + const void *table; +} im_point_context; + +static void +im_point_8_8(Imaging imOut, Imaging imIn, im_point_context *context) { + int x, y; + /* 8-bit source, 8-bit destination */ + UINT8 *table = (UINT8 *)context->table; + for (y = 0; y < imIn->ysize; y++) { + UINT8 *in = imIn->image8[y]; + UINT8 *out = imOut->image8[y]; + for (x = 0; x < imIn->xsize; x++) { + out[x] = table[in[x]]; + } + } +} + +static void +im_point_2x8_2x8(Imaging imOut, Imaging imIn, im_point_context *context) { + int x, y; + /* 2x8-bit source, 2x8-bit destination */ + UINT8 *table = (UINT8 *)context->table; + for (y = 0; y < imIn->ysize; y++) { + UINT8 *in = (UINT8 *)imIn->image[y]; + UINT8 *out = (UINT8 *)imOut->image[y]; + for (x = 0; x < imIn->xsize; x++) { + out[0] = table[in[0]]; + out[3] = table[in[3] + 256]; + in += 4; + out += 4; + } + } +} + +static void +im_point_3x8_3x8(Imaging imOut, Imaging imIn, im_point_context *context) { + int x, y; + /* 3x8-bit source, 3x8-bit destination */ + UINT8 *table = (UINT8 *)context->table; + for (y = 0; y < imIn->ysize; y++) { + UINT8 *in = (UINT8 *)imIn->image[y]; + UINT8 *out = (UINT8 *)imOut->image[y]; + for (x = 0; x < imIn->xsize; x++) { + out[0] = table[in[0]]; + out[1] = table[in[1] + 256]; + out[2] = table[in[2] + 512]; + in += 4; + out += 4; + } + } +} + +static void +im_point_4x8_4x8(Imaging imOut, Imaging imIn, im_point_context *context) { + int x, y; + /* 4x8-bit source, 4x8-bit destination */ + UINT8 *table = (UINT8 *)context->table; + for (y = 0; y < imIn->ysize; y++) { + UINT8 *in = (UINT8 *)imIn->image[y]; + UINT8 *out = (UINT8 *)imOut->image[y]; + for (x = 0; x < imIn->xsize; x++) { + out[0] = table[in[0]]; + out[1] = table[in[1] + 256]; + out[2] = table[in[2] + 512]; + out[3] = table[in[3] + 768]; + in += 4; + out += 4; + } + } +} + +static void +im_point_8_32(Imaging imOut, Imaging imIn, im_point_context *context) { + int x, y; + /* 8-bit source, 32-bit destination */ + char *table = (char *)context->table; + for (y = 0; y < imIn->ysize; y++) { + UINT8 *in = imIn->image8[y]; + INT32 *out = imOut->image32[y]; + for (x = 0; x < imIn->xsize; x++) { + memcpy(out + x, table + in[x] * sizeof(INT32), sizeof(INT32)); + } + } +} + +static void +im_point_32_8(Imaging imOut, Imaging imIn, im_point_context *context) { + int x, y; + /* 32-bit source, 8-bit destination */ + UINT8 *table = (UINT8 *)context->table; + for (y = 0; y < imIn->ysize; y++) { + INT32 *in = imIn->image32[y]; + UINT8 *out = imOut->image8[y]; + for (x = 0; x < imIn->xsize; x++) { + int v = in[x]; + if (v < 0) { + v = 0; + } else if (v > 65535) { + v = 65535; + } + out[x] = table[v]; + } + } +} + +Imaging +ImagingPoint(Imaging imIn, const char *mode, const void *table) { + /* lookup table transform */ + + ImagingSectionCookie cookie; + Imaging imOut; + im_point_context context; + void (*point)(Imaging imIn, Imaging imOut, im_point_context *context); + + if (!imIn) { + return (Imaging)ImagingError_ModeError(); + } + + if (!mode) { + mode = imIn->mode; + } + + if (imIn->type != IMAGING_TYPE_UINT8) { + if (imIn->type != IMAGING_TYPE_INT32 || strcmp(mode, "L") != 0) { + goto mode_mismatch; + } + } else if (!imIn->image8 && strcmp(imIn->mode, mode) != 0) { + goto mode_mismatch; + } + + imOut = ImagingNew(mode, imIn->xsize, imIn->ysize); + if (!imOut) { + return NULL; + } + + /* find appropriate handler */ + if (imIn->type == IMAGING_TYPE_UINT8) { + if (imIn->bands == imOut->bands && imIn->type == imOut->type) { + switch (imIn->bands) { + case 1: + point = im_point_8_8; + break; + case 2: + point = im_point_2x8_2x8; + break; + case 3: + point = im_point_3x8_3x8; + break; + case 4: + point = im_point_4x8_4x8; + break; + default: + /* this cannot really happen */ + point = im_point_8_8; + break; + } + } else { + point = im_point_8_32; + } + } else { + point = im_point_32_8; + } + + ImagingCopyPalette(imOut, imIn); + + ImagingSectionEnter(&cookie); + + context.table = table; + point(imOut, imIn, &context); + + ImagingSectionLeave(&cookie); + + return imOut; + +mode_mismatch: + return (Imaging)ImagingError_ValueError( + "point operation not supported for this mode"); +} + +Imaging +ImagingPointTransform(Imaging imIn, double scale, double offset) { + /* scale/offset transform */ + + ImagingSectionCookie cookie; + Imaging imOut; + int x, y; + + if (!imIn || (strcmp(imIn->mode, "I") != 0 && strcmp(imIn->mode, "I;16") != 0 && + strcmp(imIn->mode, "F") != 0)) { + return (Imaging)ImagingError_ModeError(); + } + + imOut = ImagingNew(imIn->mode, imIn->xsize, imIn->ysize); + if (!imOut) { + return NULL; + } + + switch (imIn->type) { + case IMAGING_TYPE_INT32: + ImagingSectionEnter(&cookie); + for (y = 0; y < imIn->ysize; y++) { + INT32 *in = imIn->image32[y]; + INT32 *out = imOut->image32[y]; + /* FIXME: add clipping? */ + for (x = 0; x < imIn->xsize; x++) { + out[x] = in[x] * scale + offset; + } + } + ImagingSectionLeave(&cookie); + break; + case IMAGING_TYPE_FLOAT32: + ImagingSectionEnter(&cookie); + for (y = 0; y < imIn->ysize; y++) { + FLOAT32 *in = (FLOAT32 *)imIn->image32[y]; + FLOAT32 *out = (FLOAT32 *)imOut->image32[y]; + for (x = 0; x < imIn->xsize; x++) { + out[x] = in[x] * scale + offset; + } + } + ImagingSectionLeave(&cookie); + break; + case IMAGING_TYPE_SPECIAL: + if (strcmp(imIn->mode, "I;16") == 0) { + ImagingSectionEnter(&cookie); + for (y = 0; y < imIn->ysize; y++) { + char *in = (char *)imIn->image[y]; + char *out = (char *)imOut->image[y]; + /* FIXME: add clipping? */ + for (x = 0; x < imIn->xsize; x++) { + UINT16 v; + memcpy(&v, in + x * sizeof(v), sizeof(v)); + v = v * scale + offset; + memcpy(out + x * sizeof(UINT16), &v, sizeof(v)); + } + } + ImagingSectionLeave(&cookie); + break; + } + /* FALL THROUGH */ + default: + ImagingDelete(imOut); + return (Imaging)ImagingError_ValueError("internal error"); + } + + return imOut; +} diff --git a/src/libImaging/Quant.c b/src/libImaging/Quant.c index cdc614536..43114a2d5 100644 --- a/src/libImaging/Quant.c +++ b/src/libImaging/Quant.c @@ -1,1844 +1,1844 @@ -/* - * The Python Imaging Library - * $Id$ - * - * image quantizer - * - * history: - * 1998-09-10 tjs Contributed - * 1998-12-29 fl Added to PIL 1.0b1 - * 2004-02-21 fl Fixed bogus free() on quantization error - * 2005-02-07 fl Limit number of colors to 256 - * - * Written by Toby J Sargeant . - * - * Copyright (c) 1998 by Toby J Sargeant - * Copyright (c) 1998-2004 by Secret Labs AB. All rights reserved. - * - * See the README file for information on usage and redistribution. - */ - -#include "Imaging.h" - -#include -#include -#include -#include - -#include "QuantTypes.h" -#include "QuantOctree.h" -#include "QuantPngQuant.h" -#include "QuantHash.h" -#include "QuantHeap.h" - -/* MSVC9.0 */ -#ifndef UINT32_MAX -#define UINT32_MAX 0xffffffff -#endif - -#define NO_OUTPUT - -typedef struct { - uint32_t scale; -} PixelHashData; - -typedef struct _PixelList { - struct _PixelList *next[3], *prev[3]; - Pixel p; - unsigned int flag : 1; - int count; -} PixelList; - -typedef struct _BoxNode { - struct _BoxNode *l, *r; - PixelList *head[3], *tail[3]; - int axis; - int volume; - uint32_t pixelCount; -} BoxNode; - -#define _SQR(x) ((x) * (x)) -#define _DISTSQR(p1, p2) \ - _SQR((int)((p1)->c.r) - (int)((p2)->c.r)) + \ - _SQR((int)((p1)->c.g) - (int)((p2)->c.g)) + \ - _SQR((int)((p1)->c.b) - (int)((p2)->c.b)) - -#define MAX_HASH_ENTRIES 65536 - -#define PIXEL_HASH(r, g, b) \ - (((unsigned int)(r)) * 463 ^ ((unsigned int)(g) << 8) * 10069 ^ \ - ((unsigned int)(b) << 16) * 64997) - -#define PIXEL_UNSCALE(p, q, s) \ - ((q)->c.r = (p)->c.r << (s)), ((q)->c.g = (p)->c.g << (s)), \ - ((q)->c.b = (p)->c.b << (s)) - -#define PIXEL_SCALE(p, q, s) \ - ((q)->c.r = (p)->c.r >> (s)), ((q)->c.g = (p)->c.g >> (s)), \ - ((q)->c.b = (p)->c.b >> (s)) - -static uint32_t -unshifted_pixel_hash(const HashTable *h, const Pixel pixel) { - return PIXEL_HASH(pixel.c.r, pixel.c.g, pixel.c.b); -} - -static int -unshifted_pixel_cmp(const HashTable *h, const Pixel pixel1, const Pixel pixel2) { - if (pixel1.c.r == pixel2.c.r) { - if (pixel1.c.g == pixel2.c.g) { - if (pixel1.c.b == pixel2.c.b) { - return 0; - } else { - return (int)(pixel1.c.b) - (int)(pixel2.c.b); - } - } else { - return (int)(pixel1.c.g) - (int)(pixel2.c.g); - } - } else { - return (int)(pixel1.c.r) - (int)(pixel2.c.r); - } -} - -static uint32_t -pixel_hash(const HashTable *h, const Pixel pixel) { - PixelHashData *d = (PixelHashData *)hashtable_get_user_data(h); - return PIXEL_HASH( - pixel.c.r >> d->scale, pixel.c.g >> d->scale, pixel.c.b >> d->scale); -} - -static int -pixel_cmp(const HashTable *h, const Pixel pixel1, const Pixel pixel2) { - PixelHashData *d = (PixelHashData *)hashtable_get_user_data(h); - uint32_t A, B; - A = PIXEL_HASH( - pixel1.c.r >> d->scale, pixel1.c.g >> d->scale, pixel1.c.b >> d->scale); - B = PIXEL_HASH( - pixel2.c.r >> d->scale, pixel2.c.g >> d->scale, pixel2.c.b >> d->scale); - return (A == B) ? 0 : ((A < B) ? -1 : 1); -} - -static void -exists_count_func(const HashTable *h, const Pixel key, uint32_t *val) { - *val += 1; -} - -static void -new_count_func(const HashTable *h, const Pixel key, uint32_t *val) { - *val = 1; -} - -static void -rehash_collide( - const HashTable *h, Pixel *keyp, uint32_t *valp, Pixel newkey, uint32_t newval) { - *valp += newval; -} - -/* %% */ - -static HashTable * -create_pixel_hash(Pixel *pixelData, uint32_t nPixels) { - PixelHashData *d; - HashTable *hash; - uint32_t i; -#ifndef NO_OUTPUT - uint32_t timer, timer2, timer3; -#endif - - /* malloc check ok, small constant allocation */ - d = malloc(sizeof(PixelHashData)); - if (!d) { - return NULL; - } - hash = hashtable_new(pixel_hash, pixel_cmp); - hashtable_set_user_data(hash, d); - d->scale = 0; -#ifndef NO_OUTPUT - timer = timer3 = clock(); -#endif - for (i = 0; i < nPixels; i++) { - if (!hashtable_insert_or_update_computed( - hash, pixelData[i], new_count_func, exists_count_func)) { - ; - } - while (hashtable_get_count(hash) > MAX_HASH_ENTRIES) { - d->scale++; -#ifndef NO_OUTPUT - printf("rehashing - new scale: %d\n", (int)d->scale); - timer2 = clock(); -#endif - hashtable_rehash_compute(hash, rehash_collide); -#ifndef NO_OUTPUT - timer2 = clock() - timer2; - printf("rehash took %f sec\n", timer2 / (double)CLOCKS_PER_SEC); - timer += timer2; -#endif - } - } -#ifndef NO_OUTPUT - printf("inserts took %f sec\n", (clock() - timer) / (double)CLOCKS_PER_SEC); -#endif -#ifndef NO_OUTPUT - printf("total %f sec\n", (clock() - timer3) / (double)CLOCKS_PER_SEC); -#endif - return hash; -} - -static void -destroy_pixel_hash(HashTable *hash) { - PixelHashData *d = (PixelHashData *)hashtable_get_user_data(hash); - if (d) { - free(d); - } - hashtable_free(hash); -} - -/* 1. hash quantized pixels. */ -/* 2. create R,G,B lists of sorted quantized pixels. */ -/* 3. median cut. */ -/* 4. build hash table from median cut boxes. */ -/* 5. for each pixel, compute entry in hash table, and hence median cut box. */ -/* 6. compute median cut box pixel averages. */ -/* 7. map each pixel to nearest average. */ - -static int -compute_box_volume(BoxNode *b) { - unsigned char rl, rh, gl, gh, bl, bh; - if (b->volume >= 0) { - return b->volume; - } - if (!b->head[0]) { - b->volume = 0; - } else { - rh = b->head[0]->p.c.r; - rl = b->tail[0]->p.c.r; - gh = b->head[1]->p.c.g; - gl = b->tail[1]->p.c.g; - bh = b->head[2]->p.c.b; - bl = b->tail[2]->p.c.b; - b->volume = (rh - rl + 1) * (gh - gl + 1) * (bh - bl + 1); - } - return b->volume; -} - -static void -hash_to_list(const HashTable *h, const Pixel pixel, const uint32_t count, void *u) { - PixelHashData *d = (PixelHashData *)hashtable_get_user_data(h); - PixelList **pl = (PixelList **)u; - PixelList *p; - int i; - Pixel q; - - PIXEL_SCALE(&pixel, &q, d->scale); - - /* malloc check ok, small constant allocation */ - p = malloc(sizeof(PixelList)); - if (!p) { - return; - } - - p->flag = 0; - p->p = q; - p->count = count; - for (i = 0; i < 3; i++) { - p->next[i] = pl[i]; - p->prev[i] = NULL; - if (pl[i]) { - pl[i]->prev[i] = p; - } - pl[i] = p; - } -} - -static PixelList * -mergesort_pixels(PixelList *head, int i) { - PixelList *c, *t, *a, *b, *p; - if (!head || !head->next[i]) { - if (head) { - head->next[i] = NULL; - head->prev[i] = NULL; - } - return head; - } - for (c = t = head; c && t; - c = c->next[i], t = (t->next[i]) ? t->next[i]->next[i] : NULL); - if (c) { - if (c->prev[i]) { - c->prev[i]->next[i] = NULL; - } - c->prev[i] = NULL; - } - a = mergesort_pixels(head, i); - b = mergesort_pixels(c, i); - head = NULL; - p = NULL; - while (a && b) { - if (a->p.a.v[i] > b->p.a.v[i]) { - c = a; - a = a->next[i]; - } else { - c = b; - b = b->next[i]; - } - c->prev[i] = p; - c->next[i] = NULL; - if (p) { - p->next[i] = c; - } - p = c; - if (!head) { - head = c; - } - } - if (a) { - c->next[i] = a; - a->prev[i] = c; - } else if (b) { - c->next[i] = b; - b->prev[i] = c; - } - return head; -} - -#if defined(TEST_MERGESORT) || defined(TEST_SORTED) -static int -test_sorted(PixelList *pl[3]) { - int i, n, l; - PixelList *t; - - for (i = 0; i < 3; i++) { - n = 0; - l = 256; - for (t = pl[i]; t; t = t->next[i]) { - if (l < t->p.a.v[i]) - return 0; - l = t->p.a.v[i]; - } - } - return 1; -} -#endif - -static int -box_heap_cmp(const Heap *h, const void *A, const void *B) { - BoxNode *a = (BoxNode *)A; - BoxNode *b = (BoxNode *)B; - return (int)a->pixelCount - (int)b->pixelCount; -} - -#define LUMINANCE(p) (77 * (p)->c.r + 150 * (p)->c.g + 29 * (p)->c.b) - -static int -splitlists( - PixelList *h[3], - PixelList *t[3], - PixelList *nh[2][3], - PixelList *nt[2][3], - uint32_t nCount[2], - int axis, - uint32_t pixelCount) { - uint32_t left; - - PixelList *l, *r, *c, *n; - int i; - int nRight; -#ifndef NO_OUTPUT - int nLeft; -#endif - int splitColourVal; - -#ifdef TEST_SPLIT - { - PixelList *_prevTest, *_nextTest; - int _i, _nextCount[3], _prevCount[3]; - for (_i = 0; _i < 3; _i++) { - for (_nextCount[_i] = 0, _nextTest = h[_i]; - _nextTest && _nextTest->next[_i]; - _nextTest = _nextTest->next[_i], _nextCount[_i]++); - for (_prevCount[_i] = 0, _prevTest = t[_i]; - _prevTest && _prevTest->prev[_i]; - _prevTest = _prevTest->prev[_i], _prevCount[_i]++); - if (_nextTest != t[_i]) { - printf("next-list of axis %d does not end at tail\n", _i); - exit(1); - } - if (_prevTest != h[_i]) { - printf("prev-list of axis %d does not end at head\n", _i); - exit(1); - } - for (; _nextTest && _nextTest->prev[_i]; _nextTest = _nextTest->prev[_i]); - for (; _prevTest && _prevTest->next[_i]; _prevTest = _prevTest->next[_i]); - if (_nextTest != h[_i]) { - printf("next-list of axis %d does not loop back to head\n", _i); - exit(1); - } - if (_prevTest != t[_i]) { - printf("prev-list of axis %d does not loop back to tail\n", _i); - exit(1); - } - } - for (_i = 1; _i < 3; _i++) { - if (_prevCount[_i] != _prevCount[_i - 1] || - _nextCount[_i] != _nextCount[_i - 1] || - _prevCount[_i] != _nextCount[_i]) { - printf( - "{%d %d %d} {%d %d %d}\n", - _prevCount[0], - _prevCount[1], - _prevCount[2], - _nextCount[0], - _nextCount[1], - _nextCount[2]); - exit(1); - } - } - } -#endif - nCount[0] = nCount[1] = 0; - nRight = 0; -#ifndef NO_OUTPUT - nLeft = 0; -#endif - for (left = 0, c = h[axis]; c;) { - left = left + c->count; - nCount[0] += c->count; - c->flag = 0; -#ifndef NO_OUTPUT - nLeft++; -#endif - c = c->next[axis]; - if (left * 2 > pixelCount) { - break; - } - } - if (c) { - splitColourVal = c->prev[axis]->p.a.v[axis]; - for (; c; c = c->next[axis]) { - if (splitColourVal != c->p.a.v[axis]) { - break; - } - c->flag = 0; -#ifndef NO_OUTPUT - nLeft++; -#endif - nCount[0] += c->count; - } - } - for (; c; c = c->next[axis]) { - c->flag = 1; - nRight++; - nCount[1] += c->count; - } - if (!nRight) { - for (c = t[axis], splitColourVal = t[axis]->p.a.v[axis]; c; c = c->prev[axis]) { - if (splitColourVal != c->p.a.v[axis]) { - break; - } - c->flag = 1; - nRight++; -#ifndef NO_OUTPUT - nLeft--; -#endif - nCount[0] -= c->count; - nCount[1] += c->count; - } - } -#ifndef NO_OUTPUT - if (!nLeft) { - for (c = h[axis]; c; c = c->next[axis]) { - printf("[%d %d %d]\n", c->p.c.r, c->p.c.g, c->p.c.b); - } - printf("warning... trivial split\n"); - } -#endif - - for (i = 0; i < 3; i++) { - l = r = NULL; - nh[0][i] = nt[0][i] = NULL; - nh[1][i] = nt[1][i] = NULL; - for (c = h[i]; c; c = n) { - n = c->next[i]; - if (c->flag) { /* move pixel to right list*/ - if (r) { - r->next[i] = c; - } else { - nh[1][i] = c; - } - c->prev[i] = r; - r = c; - } else { /* move pixel to left list */ - if (l) { - l->next[i] = c; - } else { - nh[0][i] = c; - } - c->prev[i] = l; - l = c; - } - } - if (l) { - l->next[i] = NULL; - } - if (r) { - r->next[i] = NULL; - } - nt[0][i] = l; - nt[1][i] = r; - } - return 1; -} - -static int -split(BoxNode *node) { - unsigned char rl, rh, gl, gh, bl, bh; - int f[3]; - int best, axis; - int i; - PixelList *heads[2][3]; - PixelList *tails[2][3]; - uint32_t newCounts[2]; - BoxNode *left, *right; - - rh = node->head[0]->p.c.r; - rl = node->tail[0]->p.c.r; - gh = node->head[1]->p.c.g; - gl = node->tail[1]->p.c.g; - bh = node->head[2]->p.c.b; - bl = node->tail[2]->p.c.b; -#ifdef TEST_SPLIT - printf("splitting node [%d %d %d] [%d %d %d] ", rl, gl, bl, rh, gh, bh); -#endif - f[0] = (rh - rl) * 77; - f[1] = (gh - gl) * 150; - f[2] = (bh - bl) * 29; - - best = f[0]; - axis = 0; - for (i = 1; i < 3; i++) { - if (best < f[i]) { - best = f[i]; - axis = i; - } - } -#ifdef TEST_SPLIT - printf("along axis %d\n", axis + 1); -#endif - -#ifdef TEST_SPLIT - { - PixelList *_prevTest, *_nextTest; - int _i, _nextCount[3], _prevCount[3]; - for (_i = 0; _i < 3; _i++) { - if (node->tail[_i]->next[_i]) { - printf("tail is not tail\n"); - printf( - "node->tail[%d]->next[%d]=%p\n", _i, _i, node->tail[_i]->next[_i]); - } - if (node->head[_i]->prev[_i]) { - printf("head is not head\n"); - printf( - "node->head[%d]->prev[%d]=%p\n", _i, _i, node->head[_i]->prev[_i]); - } - } - - for (_i = 0; _i < 3; _i++) { - for (_nextCount[_i] = 0, _nextTest = node->head[_i]; - _nextTest && _nextTest->next[_i]; - _nextTest = _nextTest->next[_i], _nextCount[_i]++); - for (_prevCount[_i] = 0, _prevTest = node->tail[_i]; - _prevTest && _prevTest->prev[_i]; - _prevTest = _prevTest->prev[_i], _prevCount[_i]++); - if (_nextTest != node->tail[_i]) { - printf("next-list of axis %d does not end at tail\n", _i); - } - if (_prevTest != node->head[_i]) { - printf("prev-list of axis %d does not end at head\n", _i); - } - for (; _nextTest && _nextTest->prev[_i]; _nextTest = _nextTest->prev[_i]); - for (; _prevTest && _prevTest->next[_i]; _prevTest = _prevTest->next[_i]); - if (_nextTest != node->head[_i]) { - printf("next-list of axis %d does not loop back to head\n", _i); - } - if (_prevTest != node->tail[_i]) { - printf("prev-list of axis %d does not loop back to tail\n", _i); - } - } - for (_i = 1; _i < 3; _i++) { - if (_prevCount[_i] != _prevCount[_i - 1] || - _nextCount[_i] != _nextCount[_i - 1] || - _prevCount[_i] != _nextCount[_i]) { - printf( - "{%d %d %d} {%d %d %d}\n", - _prevCount[0], - _prevCount[1], - _prevCount[2], - _nextCount[0], - _nextCount[1], - _nextCount[2]); - } - } - } -#endif - node->axis = axis; - if (!splitlists( - node->head, node->tail, heads, tails, newCounts, axis, node->pixelCount)) { -#ifndef NO_OUTPUT - printf("list split failed.\n"); -#endif - return 0; - } -#ifdef TEST_SPLIT - if (!test_sorted(heads[0])) { - printf("bug in split"); - exit(1); - } - if (!test_sorted(heads[1])) { - printf("bug in split"); - exit(1); - } -#endif - /* malloc check ok, small constant allocation */ - left = malloc(sizeof(BoxNode)); - right = malloc(sizeof(BoxNode)); - if (!left || !right) { - free(left); - free(right); - return 0; - } - for (i = 0; i < 3; i++) { - left->head[i] = heads[0][i]; - left->tail[i] = tails[0][i]; - right->head[i] = heads[1][i]; - right->tail[i] = tails[1][i]; - node->head[i] = NULL; - node->tail[i] = NULL; - } -#ifdef TEST_SPLIT - if (left->head[0]) { - rh = left->head[0]->p.c.r; - rl = left->tail[0]->p.c.r; - gh = left->head[1]->p.c.g; - gl = left->tail[1]->p.c.g; - bh = left->head[2]->p.c.b; - bl = left->tail[2]->p.c.b; - printf(" left node [%3d %3d %3d] [%3d %3d %3d]\n", rl, gl, bl, rh, gh, bh); - } - if (right->head[0]) { - rh = right->head[0]->p.c.r; - rl = right->tail[0]->p.c.r; - gh = right->head[1]->p.c.g; - gl = right->tail[1]->p.c.g; - bh = right->head[2]->p.c.b; - bl = right->tail[2]->p.c.b; - printf(" right node [%3d %3d %3d] [%3d %3d %3d]\n", rl, gl, bl, rh, gh, bh); - } -#endif - left->l = left->r = NULL; - right->l = right->r = NULL; - left->axis = right->axis = -1; - left->volume = right->volume = -1; - left->pixelCount = newCounts[0]; - right->pixelCount = newCounts[1]; - node->l = left; - node->r = right; - return 1; -} - -static BoxNode * -median_cut(PixelList *hl[3], uint32_t imPixelCount, int nPixels) { - PixelList *tl[3]; - int i; - BoxNode *root; - Heap *h; - BoxNode *thisNode; - - h = ImagingQuantHeapNew(box_heap_cmp); - /* malloc check ok, small constant allocation */ - root = malloc(sizeof(BoxNode)); - if (!root) { - ImagingQuantHeapFree(h); - return NULL; - } - for (i = 0; i < 3; i++) { - for (tl[i] = hl[i]; tl[i] && tl[i]->next[i]; tl[i] = tl[i]->next[i]); - root->head[i] = hl[i]; - root->tail[i] = tl[i]; - } - root->l = root->r = NULL; - root->axis = -1; - root->volume = -1; - root->pixelCount = imPixelCount; - - ImagingQuantHeapAdd(h, (void *)root); - while (--nPixels) { - do { - if (!ImagingQuantHeapRemove(h, (void **)&thisNode)) { - goto done; - } - } while (compute_box_volume(thisNode) == 1); - if (!split(thisNode)) { -#ifndef NO_OUTPUT - printf("Oops, split failed...\n"); -#endif - exit(1); - } - ImagingQuantHeapAdd(h, (void *)(thisNode->l)); - ImagingQuantHeapAdd(h, (void *)(thisNode->r)); - } -done: - ImagingQuantHeapFree(h); - return root; -} - -static void -free_box_tree(BoxNode *n) { - PixelList *p, *pp; - if (n->l) { - free_box_tree(n->l); - } - if (n->r) { - free_box_tree(n->r); - } - for (p = n->head[0]; p; p = pp) { - pp = p->next[0]; - free(p); - } - free(n); -} - -#ifdef TEST_SPLIT_INTEGRITY -static int -checkContained(BoxNode *n, Pixel *pp) { - if (n->l && n->r) { - return checkContained(n->l, pp) + checkContained(n->r, pp); - } - if (n->l || n->r) { -#ifndef NO_OUTPUT - printf("box tree is dead\n"); -#endif - return 0; - } - if (pp->c.r <= n->head[0]->p.c.r && pp->c.r >= n->tail[0]->p.c.r && - pp->c.g <= n->head[1]->p.c.g && pp->c.g >= n->tail[1]->p.c.g && - pp->c.b <= n->head[2]->p.c.b && pp->c.b >= n->tail[2]->p.c.b) { - return 1; - } - return 0; -} -#endif - -static int -annotate_hash_table(BoxNode *n, HashTable *h, uint32_t *box) { - PixelList *p; - PixelHashData *d = (PixelHashData *)hashtable_get_user_data(h); - Pixel q; - if (n->l && n->r) { - return annotate_hash_table(n->l, h, box) && annotate_hash_table(n->r, h, box); - } - if (n->l || n->r) { -#ifndef NO_OUTPUT - printf("box tree is dead\n"); -#endif - return 0; - } - for (p = n->head[0]; p; p = p->next[0]) { - PIXEL_UNSCALE(&(p->p), &q, d->scale); - if (!hashtable_insert(h, q, *box)) { -#ifndef NO_OUTPUT - printf("hashtable insert failed\n"); -#endif - return 0; - } - } - if (n->head[0]) { - (*box)++; - } - return 1; -} - -typedef struct { - uint32_t *distance; - uint32_t index; -} DistanceWithIndex; - -static int -_distance_index_cmp(const void *a, const void *b) { - DistanceWithIndex *A = (DistanceWithIndex *)a; - DistanceWithIndex *B = (DistanceWithIndex *)b; - if (*A->distance == *B->distance) { - return A->index < B->index ? -1 : +1; - } - return *A->distance < *B->distance ? -1 : +1; -} - -static int -resort_distance_tables( - uint32_t *avgDist, uint32_t **avgDistSortKey, Pixel *p, uint32_t nEntries) { - uint32_t i, j, k; - uint32_t **skRow; - uint32_t *skElt; - - for (i = 0; i < nEntries; i++) { - avgDist[i * nEntries + i] = 0; - for (j = 0; j < i; j++) { - avgDist[j * nEntries + i] = avgDist[i * nEntries + j] = - _DISTSQR(p + i, p + j); - } - } - for (i = 0; i < nEntries; i++) { - skRow = avgDistSortKey + i * nEntries; - for (j = 1; j < nEntries; j++) { - skElt = skRow[j]; - for (k = j; k && (*(skRow[k - 1]) > *(skRow[k])); k--) { - skRow[k] = skRow[k - 1]; - } - if (k != j) { - skRow[k] = skElt; - } - } - } - return 1; -} - -static int -build_distance_tables( - uint32_t *avgDist, uint32_t **avgDistSortKey, Pixel *p, uint32_t nEntries) { - uint32_t i, j; - DistanceWithIndex *dwi; - - for (i = 0; i < nEntries; i++) { - avgDist[i * nEntries + i] = 0; - avgDistSortKey[i * nEntries + i] = &(avgDist[i * nEntries + i]); - for (j = 0; j < i; j++) { - avgDist[j * nEntries + i] = avgDist[i * nEntries + j] = - _DISTSQR(p + i, p + j); - avgDistSortKey[j * nEntries + i] = &(avgDist[j * nEntries + i]); - avgDistSortKey[i * nEntries + j] = &(avgDist[i * nEntries + j]); - } - } - - dwi = calloc(nEntries, sizeof(DistanceWithIndex)); - if (!dwi) { - return 0; - } - for (i = 0; i < nEntries; i++) { - for (j = 0; j < nEntries; j++) { - dwi[j] = (DistanceWithIndex){&(avgDist[i * nEntries + j]), j}; - } - qsort(dwi, nEntries, sizeof(DistanceWithIndex), _distance_index_cmp); - for (j = 0; j < nEntries; j++) { - avgDistSortKey[i * nEntries + j] = dwi[j].distance; - } - } - free(dwi); - return 1; -} - -static int -map_image_pixels( - Pixel *pixelData, - uint32_t nPixels, - Pixel *paletteData, - uint32_t nPaletteEntries, - uint32_t *avgDist, - uint32_t **avgDistSortKey, - uint32_t *pixelArray) { - uint32_t *aD, **aDSK; - uint32_t idx; - uint32_t i, j; - uint32_t bestdist, bestmatch, dist; - uint32_t initialdist; - HashTable *h2; - - h2 = hashtable_new(unshifted_pixel_hash, unshifted_pixel_cmp); - for (i = 0; i < nPixels; i++) { - if (!hashtable_lookup(h2, pixelData[i], &bestmatch)) { - bestmatch = 0; - initialdist = _DISTSQR(paletteData + bestmatch, pixelData + i); - bestdist = initialdist; - initialdist <<= 2; - aDSK = avgDistSortKey + bestmatch * nPaletteEntries; - aD = avgDist + bestmatch * nPaletteEntries; - for (j = 0; j < nPaletteEntries; j++) { - idx = aDSK[j] - aD; - if (*(aDSK[j]) <= initialdist) { - dist = _DISTSQR(paletteData + idx, pixelData + i); - if (dist < bestdist) { - bestdist = dist; - bestmatch = idx; - } - } else { - break; - } - } - hashtable_insert(h2, pixelData[i], bestmatch); - } - pixelArray[i] = bestmatch; - } - hashtable_free(h2); - return 1; -} - -static int -map_image_pixels_from_quantized_pixels( - Pixel *pixelData, - uint32_t nPixels, - Pixel *paletteData, - uint32_t nPaletteEntries, - uint32_t *avgDist, - uint32_t **avgDistSortKey, - uint32_t *pixelArray, - uint32_t *avg[3], - uint32_t *count) { - uint32_t *aD, **aDSK; - uint32_t idx; - uint32_t i, j; - uint32_t bestdist, bestmatch, dist; - uint32_t initialdist; - HashTable *h2; - int changes = 0; - - h2 = hashtable_new(unshifted_pixel_hash, unshifted_pixel_cmp); - for (i = 0; i < nPixels; i++) { - if (!hashtable_lookup(h2, pixelData[i], &bestmatch)) { - bestmatch = pixelArray[i]; - initialdist = _DISTSQR(paletteData + bestmatch, pixelData + i); - bestdist = initialdist; - initialdist <<= 2; - aDSK = avgDistSortKey + bestmatch * nPaletteEntries; - aD = avgDist + bestmatch * nPaletteEntries; - for (j = 0; j < nPaletteEntries; j++) { - idx = aDSK[j] - aD; - if (*(aDSK[j]) <= initialdist) { - dist = _DISTSQR(paletteData + idx, pixelData + i); - if (dist < bestdist) { - bestdist = dist; - bestmatch = idx; - } - } else { - break; - } - } - hashtable_insert(h2, pixelData[i], bestmatch); - } - if (pixelArray[i] != bestmatch) { - changes++; - avg[0][bestmatch] += pixelData[i].c.r; - avg[1][bestmatch] += pixelData[i].c.g; - avg[2][bestmatch] += pixelData[i].c.b; - avg[0][pixelArray[i]] -= pixelData[i].c.r; - avg[1][pixelArray[i]] -= pixelData[i].c.g; - avg[2][pixelArray[i]] -= pixelData[i].c.b; - count[bestmatch]++; - count[pixelArray[i]]--; - pixelArray[i] = bestmatch; - } - } - hashtable_free(h2); - return changes; -} - -static int -map_image_pixels_from_median_box( - Pixel *pixelData, - uint32_t nPixels, - Pixel *paletteData, - uint32_t nPaletteEntries, - HashTable *medianBoxHash, - uint32_t *avgDist, - uint32_t **avgDistSortKey, - uint32_t *pixelArray) { - uint32_t *aD, **aDSK; - uint32_t idx; - uint32_t i, j; - uint32_t bestdist, bestmatch, dist; - uint32_t initialdist; - HashTable *h2; - uint32_t pixelVal; - - h2 = hashtable_new(unshifted_pixel_hash, unshifted_pixel_cmp); - for (i = 0; i < nPixels; i++) { - if (hashtable_lookup(h2, pixelData[i], &pixelVal)) { - pixelArray[i] = pixelVal; - continue; - } - if (!hashtable_lookup(medianBoxHash, pixelData[i], &pixelVal)) { -#ifndef NO_OUTPUT - printf("pixel lookup failed\n"); -#endif - return 0; - } - initialdist = _DISTSQR(paletteData + pixelVal, pixelData + i); - bestdist = initialdist; - bestmatch = pixelVal; - initialdist <<= 2; - aDSK = avgDistSortKey + pixelVal * nPaletteEntries; - aD = avgDist + pixelVal * nPaletteEntries; - for (j = 0; j < nPaletteEntries; j++) { - idx = aDSK[j] - aD; - if (*(aDSK[j]) <= initialdist) { - dist = _DISTSQR(paletteData + idx, pixelData + i); - if (dist < bestdist) { - bestdist = dist; - bestmatch = idx; - } - } else { - break; - } - } - pixelArray[i] = bestmatch; - hashtable_insert(h2, pixelData[i], bestmatch); - } - hashtable_free(h2); - return 1; -} - -static int -compute_palette_from_median_cut( - Pixel *pixelData, - uint32_t nPixels, - HashTable *medianBoxHash, - Pixel **palette, - uint32_t nPaletteEntries) { - uint32_t i; - uint32_t paletteEntry; - Pixel *p; - uint32_t *avg[3]; - uint32_t *count; - - *palette = NULL; - /* malloc check ok, using calloc */ - if (!(count = calloc(nPaletteEntries, sizeof(uint32_t)))) { - return 0; - } - for (i = 0; i < 3; i++) { - avg[i] = NULL; - } - for (i = 0; i < 3; i++) { - /* malloc check ok, using calloc */ - if (!(avg[i] = calloc(nPaletteEntries, sizeof(uint32_t)))) { - for (i = 0; i < 3; i++) { - if (avg[i]) { - free(avg[i]); - } - } - free(count); - return 0; - } - } - for (i = 0; i < nPixels; i++) { -#ifdef TEST_SPLIT_INTEGRITY - if (!(i % 100)) { - printf("%05d\r", i); - fflush(stdout); - } - if (checkContained(root, pixelData + i) > 1) { - printf("pixel in two boxes\n"); - for (i = 0; i < 3; i++) { - free(avg[i]); - } - free(count); - return 0; - } -#endif - if (!hashtable_lookup(medianBoxHash, pixelData[i], &paletteEntry)) { -#ifndef NO_OUTPUT - printf("pixel lookup failed\n"); -#endif - for (i = 0; i < 3; i++) { - free(avg[i]); - } - free(count); - return 0; - } - if (paletteEntry >= nPaletteEntries) { -#ifndef NO_OUTPUT - printf( - "panic - paletteEntry>=nPaletteEntries (%d>=%d)\n", - (int)paletteEntry, - (int)nPaletteEntries); -#endif - for (i = 0; i < 3; i++) { - free(avg[i]); - } - free(count); - return 0; - } - avg[0][paletteEntry] += pixelData[i].c.r; - avg[1][paletteEntry] += pixelData[i].c.g; - avg[2][paletteEntry] += pixelData[i].c.b; - count[paletteEntry]++; - } - /* malloc check ok, using calloc */ - p = calloc(nPaletteEntries, sizeof(Pixel)); - if (!p) { - for (i = 0; i < 3; i++) { - free(avg[i]); - } - free(count); - return 0; - } - for (i = 0; i < nPaletteEntries; i++) { - p[i].c.r = (int)(.5 + (double)avg[0][i] / (double)count[i]); - p[i].c.g = (int)(.5 + (double)avg[1][i] / (double)count[i]); - p[i].c.b = (int)(.5 + (double)avg[2][i] / (double)count[i]); - } - *palette = p; - for (i = 0; i < 3; i++) { - free(avg[i]); - } - free(count); - return 1; -} - -static int -recompute_palette_from_averages( - Pixel *palette, uint32_t nPaletteEntries, uint32_t *avg[3], uint32_t *count) { - uint32_t i; - - for (i = 0; i < nPaletteEntries; i++) { - palette[i].c.r = (int)(.5 + (double)avg[0][i] / (double)count[i]); - palette[i].c.g = (int)(.5 + (double)avg[1][i] / (double)count[i]); - palette[i].c.b = (int)(.5 + (double)avg[2][i] / (double)count[i]); - } - return 1; -} - -static int -compute_palette_from_quantized_pixels( - Pixel *pixelData, - uint32_t nPixels, - Pixel *palette, - uint32_t nPaletteEntries, - uint32_t *avg[3], - uint32_t *count, - uint32_t *qp) { - uint32_t i; - - memset(count, 0, sizeof(uint32_t) * nPaletteEntries); - for (i = 0; i < 3; i++) { - memset(avg[i], 0, sizeof(uint32_t) * nPaletteEntries); - } - for (i = 0; i < nPixels; i++) { - if (qp[i] >= nPaletteEntries) { -#ifndef NO_OUTPUT - printf("scream\n"); -#endif - return 0; - } - avg[0][qp[i]] += pixelData[i].c.r; - avg[1][qp[i]] += pixelData[i].c.g; - avg[2][qp[i]] += pixelData[i].c.b; - count[qp[i]]++; - } - for (i = 0; i < nPaletteEntries; i++) { - palette[i].c.r = (int)(.5 + (double)avg[0][i] / (double)count[i]); - palette[i].c.g = (int)(.5 + (double)avg[1][i] / (double)count[i]); - palette[i].c.b = (int)(.5 + (double)avg[2][i] / (double)count[i]); - } - return 1; -} - -static int -k_means( - Pixel *pixelData, - uint32_t nPixels, - Pixel *paletteData, - uint32_t nPaletteEntries, - uint32_t *qp, - int threshold) { - uint32_t *avg[3]; - uint32_t *count; - uint32_t i; - uint32_t *avgDist; - uint32_t **avgDistSortKey; - int changes; - int built = 0; - - if (nPaletteEntries > UINT32_MAX / (sizeof(uint32_t))) { - return 0; - } - /* malloc check ok, using calloc */ - if (!(count = calloc(nPaletteEntries, sizeof(uint32_t)))) { - return 0; - } - for (i = 0; i < 3; i++) { - avg[i] = NULL; - } - for (i = 0; i < 3; i++) { - /* malloc check ok, using calloc */ - if (!(avg[i] = calloc(nPaletteEntries, sizeof(uint32_t)))) { - goto error_1; - } - } - - /* this is enough of a check, since the multiplication n*size is done above */ - if (nPaletteEntries > UINT32_MAX / nPaletteEntries) { - goto error_1; - } - /* malloc check ok, using calloc, checking n*n above */ - avgDist = calloc(nPaletteEntries * nPaletteEntries, sizeof(uint32_t)); - if (!avgDist) { - goto error_1; - } - - /* malloc check ok, using calloc, checking n*n above */ - avgDistSortKey = calloc(nPaletteEntries * nPaletteEntries, sizeof(uint32_t *)); - if (!avgDistSortKey) { - goto error_2; - } - -#ifndef NO_OUTPUT - printf("["); - fflush(stdout); -#endif - while (1) { - if (!built) { - compute_palette_from_quantized_pixels( - pixelData, nPixels, paletteData, nPaletteEntries, avg, count, qp); - if (!build_distance_tables( - avgDist, avgDistSortKey, paletteData, nPaletteEntries)) { - goto error_3; - } - built = 1; - } else { - recompute_palette_from_averages(paletteData, nPaletteEntries, avg, count); - resort_distance_tables( - avgDist, avgDistSortKey, paletteData, nPaletteEntries); - } - changes = map_image_pixels_from_quantized_pixels( - pixelData, - nPixels, - paletteData, - nPaletteEntries, - avgDist, - avgDistSortKey, - qp, - avg, - count); - if (changes < 0) { - goto error_3; - } -#ifndef NO_OUTPUT - printf(".(%d)", changes); - fflush(stdout); -#endif - if (changes <= threshold) { - break; - } - } -#ifndef NO_OUTPUT - printf("]\n"); -#endif - if (avgDistSortKey) { - free(avgDistSortKey); - } - if (avgDist) { - free(avgDist); - } - for (i = 0; i < 3; i++) { - if (avg[i]) { - free(avg[i]); - } - } - if (count) { - free(count); - } - return 1; - -error_3: - if (avgDistSortKey) { - free(avgDistSortKey); - } -error_2: - if (avgDist) { - free(avgDist); - } -error_1: - for (i = 0; i < 3; i++) { - if (avg[i]) { - free(avg[i]); - } - } - if (count) { - free(count); - } - return 0; -} - -static int -quantize( - Pixel *pixelData, - uint32_t nPixels, - uint32_t nQuantPixels, - Pixel **palette, - uint32_t *paletteLength, - uint32_t **quantizedPixels, - int kmeans) { - PixelList *hl[3]; - HashTable *h; - BoxNode *root; - uint32_t i; - uint32_t *qp; - uint32_t nPaletteEntries; - - uint32_t *avgDist; - uint32_t **avgDistSortKey; - Pixel *p; - -#ifndef NO_OUTPUT - uint32_t timer, timer2; -#endif - -#ifndef NO_OUTPUT - timer2 = clock(); - printf("create hash table..."); - fflush(stdout); - timer = clock(); -#endif - h = create_pixel_hash(pixelData, nPixels); -#ifndef NO_OUTPUT - printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); -#endif - if (!h) { - goto error_0; - } - -#ifndef NO_OUTPUT - printf("create lists from hash table..."); - fflush(stdout); - timer = clock(); -#endif - hl[0] = hl[1] = hl[2] = NULL; - hashtable_foreach(h, hash_to_list, hl); -#ifndef NO_OUTPUT - printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); -#endif - - if (!hl[0]) { - goto error_1; - } - -#ifndef NO_OUTPUT - printf("mergesort lists..."); - fflush(stdout); - timer = clock(); -#endif - for (i = 0; i < 3; i++) { - hl[i] = mergesort_pixels(hl[i], i); - } -#ifdef TEST_MERGESORT - if (!test_sorted(hl)) { - printf("bug in mergesort\n"); - goto error_1; - } -#endif -#ifndef NO_OUTPUT - printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); -#endif - -#ifndef NO_OUTPUT - printf("median cut..."); - fflush(stdout); - timer = clock(); -#endif - root = median_cut(hl, nPixels, nQuantPixels); -#ifndef NO_OUTPUT - printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); -#endif - if (!root) { - goto error_1; - } - nPaletteEntries = 0; -#ifndef NO_OUTPUT - printf("median cut tree to hash table..."); - fflush(stdout); - timer = clock(); -#endif - annotate_hash_table(root, h, &nPaletteEntries); -#ifndef NO_OUTPUT - printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); -#endif -#ifndef NO_OUTPUT - printf("compute palette...\n"); - fflush(stdout); - timer = clock(); -#endif - if (!compute_palette_from_median_cut(pixelData, nPixels, h, &p, nPaletteEntries)) { - goto error_3; - } -#ifndef NO_OUTPUT - printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); -#endif - - free_box_tree(root); - root = NULL; - - /* malloc check ok, using calloc for overflow */ - qp = calloc(nPixels, sizeof(uint32_t)); - if (!qp) { - goto error_4; - } - - if (nPaletteEntries > UINT32_MAX / nPaletteEntries) { - goto error_5; - } - /* malloc check ok, using calloc for overflow, check of n*n above */ - avgDist = calloc(nPaletteEntries * nPaletteEntries, sizeof(uint32_t)); - if (!avgDist) { - goto error_5; - } - - /* malloc check ok, using calloc for overflow, check of n*n above */ - avgDistSortKey = calloc(nPaletteEntries * nPaletteEntries, sizeof(uint32_t *)); - if (!avgDistSortKey) { - goto error_6; - } - - if (!build_distance_tables(avgDist, avgDistSortKey, p, nPaletteEntries)) { - goto error_7; - } - - if (!map_image_pixels_from_median_box( - pixelData, nPixels, p, nPaletteEntries, h, avgDist, avgDistSortKey, qp)) { - goto error_7; - } - -#ifdef TEST_NEAREST_NEIGHBOUR -#include - { - uint32_t bestmatch, bestdist, dist; - HashTable *h2; - printf("nearest neighbour search (full search)..."); - fflush(stdout); - timer = clock(); - h2 = hashtable_new(unshifted_pixel_hash, unshifted_pixel_cmp); - for (i = 0; i < nPixels; i++) { - if (hashtable_lookup(h2, pixelData[i], &paletteEntry)) { - bestmatch = paletteEntry; - } else { - bestmatch = 0; - bestdist = _SQR(pixelData[i].c.r - p[0].c.r) + - _SQR(pixelData[i].c.g - p[0].c.g) + - _SQR(pixelData[i].c.b - p[0].c.b); - for (j = 1; j < nPaletteEntries; j++) { - dist = _SQR(pixelData[i].c.r - p[j].c.r) + - _SQR(pixelData[i].c.g - p[j].c.g) + - _SQR(pixelData[i].c.b - p[j].c.b); - if (dist == bestdist && j == qp[i]) { - bestmatch = j; - } - if (dist < bestdist) { - bestdist = dist; - bestmatch = j; - } - } - hashtable_insert(h2, pixelData[i], bestmatch); - } - if (qp[i] != bestmatch) { - printf( - "discrepancy in matching algorithms pixel %d [%d %d] %f %f\n", - i, - qp[i], - bestmatch, - sqrt((double)(_SQR(pixelData[i].c.r - p[qp[i]].c.r) + - _SQR(pixelData[i].c.g - p[qp[i]].c.g) + - _SQR(pixelData[i].c.b - p[qp[i]].c.b))), - sqrt((double)(_SQR(pixelData[i].c.r - p[bestmatch].c.r) + - _SQR(pixelData[i].c.g - p[bestmatch].c.g) + - _SQR(pixelData[i].c.b - p[bestmatch].c.b)))); - } - } - hashtable_free(h2); - } -#endif -#ifndef NO_OUTPUT - printf("k means...\n"); - fflush(stdout); - timer = clock(); -#endif - if (kmeans > 0) { - k_means(pixelData, nPixels, p, nPaletteEntries, qp, kmeans - 1); - } -#ifndef NO_OUTPUT - printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); -#endif - - *quantizedPixels = qp; - *palette = p; - *paletteLength = nPaletteEntries; - -#ifndef NO_OUTPUT - printf("cleanup..."); - fflush(stdout); - timer = clock(); -#endif - if (avgDist) { - free(avgDist); - } - if (avgDistSortKey) { - free(avgDistSortKey); - } - destroy_pixel_hash(h); -#ifndef NO_OUTPUT - printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); - printf("-----\ntotal time %f\n", (clock() - timer2) / (double)CLOCKS_PER_SEC); -#endif - return 1; - -error_7: - if (avgDistSortKey) { - free(avgDistSortKey); - } -error_6: - if (avgDist) { - free(avgDist); - } -error_5: - if (qp) { - free(qp); - } -error_4: - if (p) { - free(p); - } -error_3: - if (root) { - free_box_tree(root); - } -error_1: - destroy_pixel_hash(h); -error_0: - *quantizedPixels = NULL; - *paletteLength = 0; - *palette = NULL; - return 0; -} - -typedef struct { - Pixel new; - uint32_t furthestV; - uint32_t furthestDistance; - int secondPixel; -} DistanceData; - -static void -compute_distances(const HashTable *h, const Pixel pixel, uint32_t *dist, void *u) { - DistanceData *data = (DistanceData *)u; - uint32_t oldDist = *dist; - uint32_t newDist; - newDist = _DISTSQR(&(data->new), &pixel); - if (data->secondPixel || newDist < oldDist) { - *dist = newDist; - oldDist = newDist; - } - if (oldDist > data->furthestDistance) { - data->furthestDistance = oldDist; - data->furthestV = pixel.v; - } -} - -static int -quantize2( - Pixel *pixelData, - uint32_t nPixels, - uint32_t nQuantPixels, - Pixel **palette, - uint32_t *paletteLength, - uint32_t **quantizedPixels, - int kmeans) { - HashTable *h; - uint32_t i; - uint32_t mean[3]; - Pixel *p; - DistanceData data; - - uint32_t *qp; - uint32_t *avgDist; - uint32_t **avgDistSortKey; - - /* malloc check ok, using calloc */ - p = calloc(nQuantPixels, sizeof(Pixel)); - if (!p) { - return 0; - } - mean[0] = mean[1] = mean[2] = 0; - h = hashtable_new(unshifted_pixel_hash, unshifted_pixel_cmp); - for (i = 0; i < nPixels; i++) { - hashtable_insert(h, pixelData[i], 0xffffffff); - mean[0] += pixelData[i].c.r; - mean[1] += pixelData[i].c.g; - mean[2] += pixelData[i].c.b; - } - data.new.c.r = (int)(.5 + (double)mean[0] / (double)nPixels); - data.new.c.g = (int)(.5 + (double)mean[1] / (double)nPixels); - data.new.c.b = (int)(.5 + (double)mean[2] / (double)nPixels); - for (i = 0; i < nQuantPixels; i++) { - data.furthestDistance = 0; - data.furthestV = pixelData[0].v; - data.secondPixel = (i == 1) ? 1 : 0; - hashtable_foreach_update(h, compute_distances, &data); - p[i].v = data.furthestV; - data.new.v = data.furthestV; - } - hashtable_free(h); - - /* malloc check ok, using calloc */ - qp = calloc(nPixels, sizeof(uint32_t)); - if (!qp) { - goto error_1; - } - - if (nQuantPixels > UINT32_MAX / nQuantPixels) { - goto error_2; - } - - /* malloc check ok, using calloc for overflow, check of n*n above */ - avgDist = calloc(nQuantPixels * nQuantPixels, sizeof(uint32_t)); - if (!avgDist) { - goto error_2; - } - - /* malloc check ok, using calloc for overflow, check of n*n above */ - avgDistSortKey = calloc(nQuantPixels * nQuantPixels, sizeof(uint32_t *)); - if (!avgDistSortKey) { - goto error_3; - } - - if (!build_distance_tables(avgDist, avgDistSortKey, p, nQuantPixels)) { - goto error_4; - } - - if (!map_image_pixels( - pixelData, nPixels, p, nQuantPixels, avgDist, avgDistSortKey, qp)) { - goto error_4; - } - if (kmeans > 0) { - k_means(pixelData, nPixels, p, nQuantPixels, qp, kmeans - 1); - } - - *paletteLength = nQuantPixels; - *palette = p; - *quantizedPixels = qp; - free(avgDistSortKey); - free(avgDist); - return 1; - -error_4: - free(avgDistSortKey); -error_3: - free(avgDist); -error_2: - free(qp); -error_1: - free(p); - return 0; -} - -Imaging -ImagingQuantize(Imaging im, int colors, int mode, int kmeans) { - int i, j; - int x, y, v; - UINT8 *pp; - Pixel *p; - Pixel *palette; - uint32_t paletteLength; - int result; - uint32_t *newData; - Imaging imOut; - int withAlpha = 0; - ImagingSectionCookie cookie; - - if (!im) { - return ImagingError_ModeError(); - } - if (colors < 1 || colors > 256) { - /* FIXME: for colors > 256, consider returning an RGB image - instead (see @PIL205) */ - return (Imaging)ImagingError_ValueError("bad number of colors"); - } - - if (strcmp(im->mode, "L") != 0 && strcmp(im->mode, "P") != 0 && - strcmp(im->mode, "RGB") != 0 && strcmp(im->mode, "RGBA") != 0) { - return ImagingError_ModeError(); - } - - /* only octree and imagequant supports RGBA */ - if (!strcmp(im->mode, "RGBA") && mode != 2 && mode != 3) { - return ImagingError_ModeError(); - } - - if (im->xsize > INT_MAX / im->ysize) { - return ImagingError_MemoryError(); - } - /* malloc check ok, using calloc for final overflow, x*y above */ - p = calloc(im->xsize * im->ysize, sizeof(Pixel)); - if (!p) { - return ImagingError_MemoryError(); - } - - /* collect statistics */ - - /* FIXME: maybe we could load the hash tables directly from the - image data? */ - - if (!strcmp(im->mode, "L")) { - /* grayscale */ - - /* FIXME: converting a "L" image to "P" with 256 colors - should be done by a simple copy... */ - - for (i = y = 0; y < im->ysize; y++) { - for (x = 0; x < im->xsize; x++, i++) { - p[i].c.r = p[i].c.g = p[i].c.b = im->image8[y][x]; - p[i].c.a = 255; - } - } - - } else if (!strcmp(im->mode, "P")) { - /* palette */ - - pp = im->palette->palette; - - for (i = y = 0; y < im->ysize; y++) { - for (x = 0; x < im->xsize; x++, i++) { - v = im->image8[y][x]; - p[i].c.r = pp[v * 4 + 0]; - p[i].c.g = pp[v * 4 + 1]; - p[i].c.b = pp[v * 4 + 2]; - p[i].c.a = pp[v * 4 + 3]; - } - } - - } else if (!strcmp(im->mode, "RGB") || !strcmp(im->mode, "RGBA")) { - /* true colour */ - - withAlpha = !strcmp(im->mode, "RGBA"); - int transparency = 0; - unsigned char r = 0, g = 0, b = 0; - for (i = y = 0; y < im->ysize; y++) { - for (x = 0; x < im->xsize; x++, i++) { - p[i].v = im->image32[y][x]; - if (withAlpha && p[i].c.a == 0) { - if (transparency == 0) { - transparency = 1; - r = p[i].c.r; - g = p[i].c.g; - b = p[i].c.b; - } else { - /* Set all subsequent transparent pixels - to the same colour as the first */ - p[i].c.r = r; - p[i].c.g = g; - p[i].c.b = b; - } - } - } - } - - } else { - free(p); - return (Imaging)ImagingError_ValueError("internal error"); - } - - ImagingSectionEnter(&cookie); - - switch (mode) { - case 0: - /* median cut */ - result = quantize( - p, - im->xsize * im->ysize, - colors, - &palette, - &paletteLength, - &newData, - kmeans); - break; - case 1: - /* maximum coverage */ - result = quantize2( - p, - im->xsize * im->ysize, - colors, - &palette, - &paletteLength, - &newData, - kmeans); - break; - case 2: - result = quantize_octree( - p, - im->xsize * im->ysize, - colors, - &palette, - &paletteLength, - &newData, - withAlpha); - break; - case 3: -#ifdef HAVE_LIBIMAGEQUANT - result = quantize_pngquant( - p, - im->xsize, - im->ysize, - colors, - &palette, - &paletteLength, - &newData, - withAlpha); -#else - result = -1; -#endif - break; - default: - result = 0; - break; - } - - free(p); - ImagingSectionLeave(&cookie); - - if (result > 0) { - imOut = ImagingNewDirty("P", im->xsize, im->ysize); - ImagingSectionEnter(&cookie); - - for (i = y = 0; y < im->ysize; y++) { - for (x = 0; x < im->xsize; x++) { - imOut->image8[y][x] = (unsigned char)newData[i++]; - } - } - - free(newData); - - imOut->palette->size = (int)paletteLength; - pp = imOut->palette->palette; - - for (i = j = 0; i < (int)paletteLength; i++) { - *pp++ = palette[i].c.r; - *pp++ = palette[i].c.g; - *pp++ = palette[i].c.b; - if (withAlpha) { - *pp = palette[i].c.a; - } - pp++; - } - - if (withAlpha) { - strcpy(imOut->palette->mode, "RGBA"); - } - - free(palette); - ImagingSectionLeave(&cookie); - - return imOut; - - } else { - if (result == -1) { - return (Imaging)ImagingError_ValueError( - "dependency required by this method was not " - "enabled at compile time"); - } - - return (Imaging)ImagingError_ValueError("quantization error"); - } -} +/* + * The Python Imaging Library + * $Id$ + * + * image quantizer + * + * history: + * 1998-09-10 tjs Contributed + * 1998-12-29 fl Added to PIL 1.0b1 + * 2004-02-21 fl Fixed bogus free() on quantization error + * 2005-02-07 fl Limit number of colors to 256 + * + * Written by Toby J Sargeant . + * + * Copyright (c) 1998 by Toby J Sargeant + * Copyright (c) 1998-2004 by Secret Labs AB. All rights reserved. + * + * See the README file for information on usage and redistribution. + */ + +#include "Imaging.h" + +#include +#include +#include +#include + +#include "QuantTypes.h" +#include "QuantOctree.h" +#include "QuantPngQuant.h" +#include "QuantHash.h" +#include "QuantHeap.h" + +/* MSVC9.0 */ +#ifndef UINT32_MAX +#define UINT32_MAX 0xffffffff +#endif + +#define NO_OUTPUT + +typedef struct { + uint32_t scale; +} PixelHashData; + +typedef struct _PixelList { + struct _PixelList *next[3], *prev[3]; + Pixel p; + unsigned int flag : 1; + int count; +} PixelList; + +typedef struct _BoxNode { + struct _BoxNode *l, *r; + PixelList *head[3], *tail[3]; + int axis; + int volume; + uint32_t pixelCount; +} BoxNode; + +#define _SQR(x) ((x) * (x)) +#define _DISTSQR(p1, p2) \ + _SQR((int)((p1)->c.r) - (int)((p2)->c.r)) + \ + _SQR((int)((p1)->c.g) - (int)((p2)->c.g)) + \ + _SQR((int)((p1)->c.b) - (int)((p2)->c.b)) + +#define MAX_HASH_ENTRIES 65536 + +#define PIXEL_HASH(r, g, b) \ + (((unsigned int)(r)) * 463 ^ ((unsigned int)(g) << 8) * 10069 ^ \ + ((unsigned int)(b) << 16) * 64997) + +#define PIXEL_UNSCALE(p, q, s) \ + ((q)->c.r = (p)->c.r << (s)), ((q)->c.g = (p)->c.g << (s)), \ + ((q)->c.b = (p)->c.b << (s)) + +#define PIXEL_SCALE(p, q, s) \ + ((q)->c.r = (p)->c.r >> (s)), ((q)->c.g = (p)->c.g >> (s)), \ + ((q)->c.b = (p)->c.b >> (s)) + +static uint32_t +unshifted_pixel_hash(const HashTable *h, const Pixel pixel) { + return PIXEL_HASH(pixel.c.r, pixel.c.g, pixel.c.b); +} + +static int +unshifted_pixel_cmp(const HashTable *h, const Pixel pixel1, const Pixel pixel2) { + if (pixel1.c.r == pixel2.c.r) { + if (pixel1.c.g == pixel2.c.g) { + if (pixel1.c.b == pixel2.c.b) { + return 0; + } else { + return (int)(pixel1.c.b) - (int)(pixel2.c.b); + } + } else { + return (int)(pixel1.c.g) - (int)(pixel2.c.g); + } + } else { + return (int)(pixel1.c.r) - (int)(pixel2.c.r); + } +} + +static uint32_t +pixel_hash(const HashTable *h, const Pixel pixel) { + PixelHashData *d = (PixelHashData *)hashtable_get_user_data(h); + return PIXEL_HASH( + pixel.c.r >> d->scale, pixel.c.g >> d->scale, pixel.c.b >> d->scale); +} + +static int +pixel_cmp(const HashTable *h, const Pixel pixel1, const Pixel pixel2) { + PixelHashData *d = (PixelHashData *)hashtable_get_user_data(h); + uint32_t A, B; + A = PIXEL_HASH( + pixel1.c.r >> d->scale, pixel1.c.g >> d->scale, pixel1.c.b >> d->scale); + B = PIXEL_HASH( + pixel2.c.r >> d->scale, pixel2.c.g >> d->scale, pixel2.c.b >> d->scale); + return (A == B) ? 0 : ((A < B) ? -1 : 1); +} + +static void +exists_count_func(const HashTable *h, const Pixel key, uint32_t *val) { + *val += 1; +} + +static void +new_count_func(const HashTable *h, const Pixel key, uint32_t *val) { + *val = 1; +} + +static void +rehash_collide( + const HashTable *h, Pixel *keyp, uint32_t *valp, Pixel newkey, uint32_t newval) { + *valp += newval; +} + +/* %% */ + +static HashTable * +create_pixel_hash(Pixel *pixelData, uint32_t nPixels) { + PixelHashData *d; + HashTable *hash; + uint32_t i; +#ifndef NO_OUTPUT + uint32_t timer, timer2, timer3; +#endif + + /* malloc check ok, small constant allocation */ + d = malloc(sizeof(PixelHashData)); + if (!d) { + return NULL; + } + hash = hashtable_new(pixel_hash, pixel_cmp); + hashtable_set_user_data(hash, d); + d->scale = 0; +#ifndef NO_OUTPUT + timer = timer3 = clock(); +#endif + for (i = 0; i < nPixels; i++) { + if (!hashtable_insert_or_update_computed( + hash, pixelData[i], new_count_func, exists_count_func)) { + ; + } + while (hashtable_get_count(hash) > MAX_HASH_ENTRIES) { + d->scale++; +#ifndef NO_OUTPUT + printf("rehashing - new scale: %d\n", (int)d->scale); + timer2 = clock(); +#endif + hashtable_rehash_compute(hash, rehash_collide); +#ifndef NO_OUTPUT + timer2 = clock() - timer2; + printf("rehash took %f sec\n", timer2 / (double)CLOCKS_PER_SEC); + timer += timer2; +#endif + } + } +#ifndef NO_OUTPUT + printf("inserts took %f sec\n", (clock() - timer) / (double)CLOCKS_PER_SEC); +#endif +#ifndef NO_OUTPUT + printf("total %f sec\n", (clock() - timer3) / (double)CLOCKS_PER_SEC); +#endif + return hash; +} + +static void +destroy_pixel_hash(HashTable *hash) { + PixelHashData *d = (PixelHashData *)hashtable_get_user_data(hash); + if (d) { + free(d); + } + hashtable_free(hash); +} + +/* 1. hash quantized pixels. */ +/* 2. create R,G,B lists of sorted quantized pixels. */ +/* 3. median cut. */ +/* 4. build hash table from median cut boxes. */ +/* 5. for each pixel, compute entry in hash table, and hence median cut box. */ +/* 6. compute median cut box pixel averages. */ +/* 7. map each pixel to nearest average. */ + +static int +compute_box_volume(BoxNode *b) { + unsigned char rl, rh, gl, gh, bl, bh; + if (b->volume >= 0) { + return b->volume; + } + if (!b->head[0]) { + b->volume = 0; + } else { + rh = b->head[0]->p.c.r; + rl = b->tail[0]->p.c.r; + gh = b->head[1]->p.c.g; + gl = b->tail[1]->p.c.g; + bh = b->head[2]->p.c.b; + bl = b->tail[2]->p.c.b; + b->volume = (rh - rl + 1) * (gh - gl + 1) * (bh - bl + 1); + } + return b->volume; +} + +static void +hash_to_list(const HashTable *h, const Pixel pixel, const uint32_t count, void *u) { + PixelHashData *d = (PixelHashData *)hashtable_get_user_data(h); + PixelList **pl = (PixelList **)u; + PixelList *p; + int i; + Pixel q; + + PIXEL_SCALE(&pixel, &q, d->scale); + + /* malloc check ok, small constant allocation */ + p = malloc(sizeof(PixelList)); + if (!p) { + return; + } + + p->flag = 0; + p->p = q; + p->count = count; + for (i = 0; i < 3; i++) { + p->next[i] = pl[i]; + p->prev[i] = NULL; + if (pl[i]) { + pl[i]->prev[i] = p; + } + pl[i] = p; + } +} + +static PixelList * +mergesort_pixels(PixelList *head, int i) { + PixelList *c, *t, *a, *b, *p; + if (!head || !head->next[i]) { + if (head) { + head->next[i] = NULL; + head->prev[i] = NULL; + } + return head; + } + for (c = t = head; c && t; + c = c->next[i], t = (t->next[i]) ? t->next[i]->next[i] : NULL); + if (c) { + if (c->prev[i]) { + c->prev[i]->next[i] = NULL; + } + c->prev[i] = NULL; + } + a = mergesort_pixels(head, i); + b = mergesort_pixels(c, i); + head = NULL; + p = NULL; + while (a && b) { + if (a->p.a.v[i] > b->p.a.v[i]) { + c = a; + a = a->next[i]; + } else { + c = b; + b = b->next[i]; + } + c->prev[i] = p; + c->next[i] = NULL; + if (p) { + p->next[i] = c; + } + p = c; + if (!head) { + head = c; + } + } + if (a) { + c->next[i] = a; + a->prev[i] = c; + } else if (b) { + c->next[i] = b; + b->prev[i] = c; + } + return head; +} + +#if defined(TEST_MERGESORT) || defined(TEST_SORTED) +static int +test_sorted(PixelList *pl[3]) { + int i, n, l; + PixelList *t; + + for (i = 0; i < 3; i++) { + n = 0; + l = 256; + for (t = pl[i]; t; t = t->next[i]) { + if (l < t->p.a.v[i]) + return 0; + l = t->p.a.v[i]; + } + } + return 1; +} +#endif + +static int +box_heap_cmp(const Heap *h, const void *A, const void *B) { + BoxNode *a = (BoxNode *)A; + BoxNode *b = (BoxNode *)B; + return (int)a->pixelCount - (int)b->pixelCount; +} + +#define LUMINANCE(p) (77 * (p)->c.r + 150 * (p)->c.g + 29 * (p)->c.b) + +static int +splitlists( + PixelList *h[3], + PixelList *t[3], + PixelList *nh[2][3], + PixelList *nt[2][3], + uint32_t nCount[2], + int axis, + uint32_t pixelCount) { + uint32_t left; + + PixelList *l, *r, *c, *n; + int i; + int nRight; +#ifndef NO_OUTPUT + int nLeft; +#endif + int splitColourVal; + +#ifdef TEST_SPLIT + { + PixelList *_prevTest, *_nextTest; + int _i, _nextCount[3], _prevCount[3]; + for (_i = 0; _i < 3; _i++) { + for (_nextCount[_i] = 0, _nextTest = h[_i]; + _nextTest && _nextTest->next[_i]; + _nextTest = _nextTest->next[_i], _nextCount[_i]++); + for (_prevCount[_i] = 0, _prevTest = t[_i]; + _prevTest && _prevTest->prev[_i]; + _prevTest = _prevTest->prev[_i], _prevCount[_i]++); + if (_nextTest != t[_i]) { + printf("next-list of axis %d does not end at tail\n", _i); + exit(1); + } + if (_prevTest != h[_i]) { + printf("prev-list of axis %d does not end at head\n", _i); + exit(1); + } + for (; _nextTest && _nextTest->prev[_i]; _nextTest = _nextTest->prev[_i]); + for (; _prevTest && _prevTest->next[_i]; _prevTest = _prevTest->next[_i]); + if (_nextTest != h[_i]) { + printf("next-list of axis %d does not loop back to head\n", _i); + exit(1); + } + if (_prevTest != t[_i]) { + printf("prev-list of axis %d does not loop back to tail\n", _i); + exit(1); + } + } + for (_i = 1; _i < 3; _i++) { + if (_prevCount[_i] != _prevCount[_i - 1] || + _nextCount[_i] != _nextCount[_i - 1] || + _prevCount[_i] != _nextCount[_i]) { + printf( + "{%d %d %d} {%d %d %d}\n", + _prevCount[0], + _prevCount[1], + _prevCount[2], + _nextCount[0], + _nextCount[1], + _nextCount[2]); + exit(1); + } + } + } +#endif + nCount[0] = nCount[1] = 0; + nRight = 0; +#ifndef NO_OUTPUT + nLeft = 0; +#endif + for (left = 0, c = h[axis]; c;) { + left = left + c->count; + nCount[0] += c->count; + c->flag = 0; +#ifndef NO_OUTPUT + nLeft++; +#endif + c = c->next[axis]; + if (left * 2 > pixelCount) { + break; + } + } + if (c) { + splitColourVal = c->prev[axis]->p.a.v[axis]; + for (; c; c = c->next[axis]) { + if (splitColourVal != c->p.a.v[axis]) { + break; + } + c->flag = 0; +#ifndef NO_OUTPUT + nLeft++; +#endif + nCount[0] += c->count; + } + } + for (; c; c = c->next[axis]) { + c->flag = 1; + nRight++; + nCount[1] += c->count; + } + if (!nRight) { + for (c = t[axis], splitColourVal = t[axis]->p.a.v[axis]; c; c = c->prev[axis]) { + if (splitColourVal != c->p.a.v[axis]) { + break; + } + c->flag = 1; + nRight++; +#ifndef NO_OUTPUT + nLeft--; +#endif + nCount[0] -= c->count; + nCount[1] += c->count; + } + } +#ifndef NO_OUTPUT + if (!nLeft) { + for (c = h[axis]; c; c = c->next[axis]) { + printf("[%d %d %d]\n", c->p.c.r, c->p.c.g, c->p.c.b); + } + printf("warning... trivial split\n"); + } +#endif + + for (i = 0; i < 3; i++) { + l = r = NULL; + nh[0][i] = nt[0][i] = NULL; + nh[1][i] = nt[1][i] = NULL; + for (c = h[i]; c; c = n) { + n = c->next[i]; + if (c->flag) { /* move pixel to right list*/ + if (r) { + r->next[i] = c; + } else { + nh[1][i] = c; + } + c->prev[i] = r; + r = c; + } else { /* move pixel to left list */ + if (l) { + l->next[i] = c; + } else { + nh[0][i] = c; + } + c->prev[i] = l; + l = c; + } + } + if (l) { + l->next[i] = NULL; + } + if (r) { + r->next[i] = NULL; + } + nt[0][i] = l; + nt[1][i] = r; + } + return 1; +} + +static int +split(BoxNode *node) { + unsigned char rl, rh, gl, gh, bl, bh; + int f[3]; + int best, axis; + int i; + PixelList *heads[2][3]; + PixelList *tails[2][3]; + uint32_t newCounts[2]; + BoxNode *left, *right; + + rh = node->head[0]->p.c.r; + rl = node->tail[0]->p.c.r; + gh = node->head[1]->p.c.g; + gl = node->tail[1]->p.c.g; + bh = node->head[2]->p.c.b; + bl = node->tail[2]->p.c.b; +#ifdef TEST_SPLIT + printf("splitting node [%d %d %d] [%d %d %d] ", rl, gl, bl, rh, gh, bh); +#endif + f[0] = (rh - rl) * 77; + f[1] = (gh - gl) * 150; + f[2] = (bh - bl) * 29; + + best = f[0]; + axis = 0; + for (i = 1; i < 3; i++) { + if (best < f[i]) { + best = f[i]; + axis = i; + } + } +#ifdef TEST_SPLIT + printf("along axis %d\n", axis + 1); +#endif + +#ifdef TEST_SPLIT + { + PixelList *_prevTest, *_nextTest; + int _i, _nextCount[3], _prevCount[3]; + for (_i = 0; _i < 3; _i++) { + if (node->tail[_i]->next[_i]) { + printf("tail is not tail\n"); + printf( + "node->tail[%d]->next[%d]=%p\n", _i, _i, node->tail[_i]->next[_i]); + } + if (node->head[_i]->prev[_i]) { + printf("head is not head\n"); + printf( + "node->head[%d]->prev[%d]=%p\n", _i, _i, node->head[_i]->prev[_i]); + } + } + + for (_i = 0; _i < 3; _i++) { + for (_nextCount[_i] = 0, _nextTest = node->head[_i]; + _nextTest && _nextTest->next[_i]; + _nextTest = _nextTest->next[_i], _nextCount[_i]++); + for (_prevCount[_i] = 0, _prevTest = node->tail[_i]; + _prevTest && _prevTest->prev[_i]; + _prevTest = _prevTest->prev[_i], _prevCount[_i]++); + if (_nextTest != node->tail[_i]) { + printf("next-list of axis %d does not end at tail\n", _i); + } + if (_prevTest != node->head[_i]) { + printf("prev-list of axis %d does not end at head\n", _i); + } + for (; _nextTest && _nextTest->prev[_i]; _nextTest = _nextTest->prev[_i]); + for (; _prevTest && _prevTest->next[_i]; _prevTest = _prevTest->next[_i]); + if (_nextTest != node->head[_i]) { + printf("next-list of axis %d does not loop back to head\n", _i); + } + if (_prevTest != node->tail[_i]) { + printf("prev-list of axis %d does not loop back to tail\n", _i); + } + } + for (_i = 1; _i < 3; _i++) { + if (_prevCount[_i] != _prevCount[_i - 1] || + _nextCount[_i] != _nextCount[_i - 1] || + _prevCount[_i] != _nextCount[_i]) { + printf( + "{%d %d %d} {%d %d %d}\n", + _prevCount[0], + _prevCount[1], + _prevCount[2], + _nextCount[0], + _nextCount[1], + _nextCount[2]); + } + } + } +#endif + node->axis = axis; + if (!splitlists( + node->head, node->tail, heads, tails, newCounts, axis, node->pixelCount)) { +#ifndef NO_OUTPUT + printf("list split failed.\n"); +#endif + return 0; + } +#ifdef TEST_SPLIT + if (!test_sorted(heads[0])) { + printf("bug in split"); + exit(1); + } + if (!test_sorted(heads[1])) { + printf("bug in split"); + exit(1); + } +#endif + /* malloc check ok, small constant allocation */ + left = malloc(sizeof(BoxNode)); + right = malloc(sizeof(BoxNode)); + if (!left || !right) { + free(left); + free(right); + return 0; + } + for (i = 0; i < 3; i++) { + left->head[i] = heads[0][i]; + left->tail[i] = tails[0][i]; + right->head[i] = heads[1][i]; + right->tail[i] = tails[1][i]; + node->head[i] = NULL; + node->tail[i] = NULL; + } +#ifdef TEST_SPLIT + if (left->head[0]) { + rh = left->head[0]->p.c.r; + rl = left->tail[0]->p.c.r; + gh = left->head[1]->p.c.g; + gl = left->tail[1]->p.c.g; + bh = left->head[2]->p.c.b; + bl = left->tail[2]->p.c.b; + printf(" left node [%3d %3d %3d] [%3d %3d %3d]\n", rl, gl, bl, rh, gh, bh); + } + if (right->head[0]) { + rh = right->head[0]->p.c.r; + rl = right->tail[0]->p.c.r; + gh = right->head[1]->p.c.g; + gl = right->tail[1]->p.c.g; + bh = right->head[2]->p.c.b; + bl = right->tail[2]->p.c.b; + printf(" right node [%3d %3d %3d] [%3d %3d %3d]\n", rl, gl, bl, rh, gh, bh); + } +#endif + left->l = left->r = NULL; + right->l = right->r = NULL; + left->axis = right->axis = -1; + left->volume = right->volume = -1; + left->pixelCount = newCounts[0]; + right->pixelCount = newCounts[1]; + node->l = left; + node->r = right; + return 1; +} + +static BoxNode * +median_cut(PixelList *hl[3], uint32_t imPixelCount, int nPixels) { + PixelList *tl[3]; + int i; + BoxNode *root; + Heap *h; + BoxNode *thisNode; + + h = ImagingQuantHeapNew(box_heap_cmp); + /* malloc check ok, small constant allocation */ + root = malloc(sizeof(BoxNode)); + if (!root) { + ImagingQuantHeapFree(h); + return NULL; + } + for (i = 0; i < 3; i++) { + for (tl[i] = hl[i]; tl[i] && tl[i]->next[i]; tl[i] = tl[i]->next[i]); + root->head[i] = hl[i]; + root->tail[i] = tl[i]; + } + root->l = root->r = NULL; + root->axis = -1; + root->volume = -1; + root->pixelCount = imPixelCount; + + ImagingQuantHeapAdd(h, (void *)root); + while (--nPixels) { + do { + if (!ImagingQuantHeapRemove(h, (void **)&thisNode)) { + goto done; + } + } while (compute_box_volume(thisNode) == 1); + if (!split(thisNode)) { +#ifndef NO_OUTPUT + printf("Oops, split failed...\n"); +#endif + exit(1); + } + ImagingQuantHeapAdd(h, (void *)(thisNode->l)); + ImagingQuantHeapAdd(h, (void *)(thisNode->r)); + } +done: + ImagingQuantHeapFree(h); + return root; +} + +static void +free_box_tree(BoxNode *n) { + PixelList *p, *pp; + if (n->l) { + free_box_tree(n->l); + } + if (n->r) { + free_box_tree(n->r); + } + for (p = n->head[0]; p; p = pp) { + pp = p->next[0]; + free(p); + } + free(n); +} + +#ifdef TEST_SPLIT_INTEGRITY +static int +checkContained(BoxNode *n, Pixel *pp) { + if (n->l && n->r) { + return checkContained(n->l, pp) + checkContained(n->r, pp); + } + if (n->l || n->r) { +#ifndef NO_OUTPUT + printf("box tree is dead\n"); +#endif + return 0; + } + if (pp->c.r <= n->head[0]->p.c.r && pp->c.r >= n->tail[0]->p.c.r && + pp->c.g <= n->head[1]->p.c.g && pp->c.g >= n->tail[1]->p.c.g && + pp->c.b <= n->head[2]->p.c.b && pp->c.b >= n->tail[2]->p.c.b) { + return 1; + } + return 0; +} +#endif + +static int +annotate_hash_table(BoxNode *n, HashTable *h, uint32_t *box) { + PixelList *p; + PixelHashData *d = (PixelHashData *)hashtable_get_user_data(h); + Pixel q; + if (n->l && n->r) { + return annotate_hash_table(n->l, h, box) && annotate_hash_table(n->r, h, box); + } + if (n->l || n->r) { +#ifndef NO_OUTPUT + printf("box tree is dead\n"); +#endif + return 0; + } + for (p = n->head[0]; p; p = p->next[0]) { + PIXEL_UNSCALE(&(p->p), &q, d->scale); + if (!hashtable_insert(h, q, *box)) { +#ifndef NO_OUTPUT + printf("hashtable insert failed\n"); +#endif + return 0; + } + } + if (n->head[0]) { + (*box)++; + } + return 1; +} + +typedef struct { + uint32_t *distance; + uint32_t index; +} DistanceWithIndex; + +static int +_distance_index_cmp(const void *a, const void *b) { + DistanceWithIndex *A = (DistanceWithIndex *)a; + DistanceWithIndex *B = (DistanceWithIndex *)b; + if (*A->distance == *B->distance) { + return A->index < B->index ? -1 : +1; + } + return *A->distance < *B->distance ? -1 : +1; +} + +static int +resort_distance_tables( + uint32_t *avgDist, uint32_t **avgDistSortKey, Pixel *p, uint32_t nEntries) { + uint32_t i, j, k; + uint32_t **skRow; + uint32_t *skElt; + + for (i = 0; i < nEntries; i++) { + avgDist[i * nEntries + i] = 0; + for (j = 0; j < i; j++) { + avgDist[j * nEntries + i] = avgDist[i * nEntries + j] = + _DISTSQR(p + i, p + j); + } + } + for (i = 0; i < nEntries; i++) { + skRow = avgDistSortKey + i * nEntries; + for (j = 1; j < nEntries; j++) { + skElt = skRow[j]; + for (k = j; k && (*(skRow[k - 1]) > *(skRow[k])); k--) { + skRow[k] = skRow[k - 1]; + } + if (k != j) { + skRow[k] = skElt; + } + } + } + return 1; +} + +static int +build_distance_tables( + uint32_t *avgDist, uint32_t **avgDistSortKey, Pixel *p, uint32_t nEntries) { + uint32_t i, j; + DistanceWithIndex *dwi; + + for (i = 0; i < nEntries; i++) { + avgDist[i * nEntries + i] = 0; + avgDistSortKey[i * nEntries + i] = &(avgDist[i * nEntries + i]); + for (j = 0; j < i; j++) { + avgDist[j * nEntries + i] = avgDist[i * nEntries + j] = + _DISTSQR(p + i, p + j); + avgDistSortKey[j * nEntries + i] = &(avgDist[j * nEntries + i]); + avgDistSortKey[i * nEntries + j] = &(avgDist[i * nEntries + j]); + } + } + + dwi = calloc(nEntries, sizeof(DistanceWithIndex)); + if (!dwi) { + return 0; + } + for (i = 0; i < nEntries; i++) { + for (j = 0; j < nEntries; j++) { + dwi[j] = (DistanceWithIndex){&(avgDist[i * nEntries + j]), j}; + } + qsort(dwi, nEntries, sizeof(DistanceWithIndex), _distance_index_cmp); + for (j = 0; j < nEntries; j++) { + avgDistSortKey[i * nEntries + j] = dwi[j].distance; + } + } + free(dwi); + return 1; +} + +static int +map_image_pixels( + Pixel *pixelData, + uint32_t nPixels, + Pixel *paletteData, + uint32_t nPaletteEntries, + uint32_t *avgDist, + uint32_t **avgDistSortKey, + uint32_t *pixelArray) { + uint32_t *aD, **aDSK; + uint32_t idx; + uint32_t i, j; + uint32_t bestdist, bestmatch, dist; + uint32_t initialdist; + HashTable *h2; + + h2 = hashtable_new(unshifted_pixel_hash, unshifted_pixel_cmp); + for (i = 0; i < nPixels; i++) { + if (!hashtable_lookup(h2, pixelData[i], &bestmatch)) { + bestmatch = 0; + initialdist = _DISTSQR(paletteData + bestmatch, pixelData + i); + bestdist = initialdist; + initialdist <<= 2; + aDSK = avgDistSortKey + bestmatch * nPaletteEntries; + aD = avgDist + bestmatch * nPaletteEntries; + for (j = 0; j < nPaletteEntries; j++) { + idx = aDSK[j] - aD; + if (*(aDSK[j]) <= initialdist) { + dist = _DISTSQR(paletteData + idx, pixelData + i); + if (dist < bestdist) { + bestdist = dist; + bestmatch = idx; + } + } else { + break; + } + } + hashtable_insert(h2, pixelData[i], bestmatch); + } + pixelArray[i] = bestmatch; + } + hashtable_free(h2); + return 1; +} + +static int +map_image_pixels_from_quantized_pixels( + Pixel *pixelData, + uint32_t nPixels, + Pixel *paletteData, + uint32_t nPaletteEntries, + uint32_t *avgDist, + uint32_t **avgDistSortKey, + uint32_t *pixelArray, + uint32_t *avg[3], + uint32_t *count) { + uint32_t *aD, **aDSK; + uint32_t idx; + uint32_t i, j; + uint32_t bestdist, bestmatch, dist; + uint32_t initialdist; + HashTable *h2; + int changes = 0; + + h2 = hashtable_new(unshifted_pixel_hash, unshifted_pixel_cmp); + for (i = 0; i < nPixels; i++) { + if (!hashtable_lookup(h2, pixelData[i], &bestmatch)) { + bestmatch = pixelArray[i]; + initialdist = _DISTSQR(paletteData + bestmatch, pixelData + i); + bestdist = initialdist; + initialdist <<= 2; + aDSK = avgDistSortKey + bestmatch * nPaletteEntries; + aD = avgDist + bestmatch * nPaletteEntries; + for (j = 0; j < nPaletteEntries; j++) { + idx = aDSK[j] - aD; + if (*(aDSK[j]) <= initialdist) { + dist = _DISTSQR(paletteData + idx, pixelData + i); + if (dist < bestdist) { + bestdist = dist; + bestmatch = idx; + } + } else { + break; + } + } + hashtable_insert(h2, pixelData[i], bestmatch); + } + if (pixelArray[i] != bestmatch) { + changes++; + avg[0][bestmatch] += pixelData[i].c.r; + avg[1][bestmatch] += pixelData[i].c.g; + avg[2][bestmatch] += pixelData[i].c.b; + avg[0][pixelArray[i]] -= pixelData[i].c.r; + avg[1][pixelArray[i]] -= pixelData[i].c.g; + avg[2][pixelArray[i]] -= pixelData[i].c.b; + count[bestmatch]++; + count[pixelArray[i]]--; + pixelArray[i] = bestmatch; + } + } + hashtable_free(h2); + return changes; +} + +static int +map_image_pixels_from_median_box( + Pixel *pixelData, + uint32_t nPixels, + Pixel *paletteData, + uint32_t nPaletteEntries, + HashTable *medianBoxHash, + uint32_t *avgDist, + uint32_t **avgDistSortKey, + uint32_t *pixelArray) { + uint32_t *aD, **aDSK; + uint32_t idx; + uint32_t i, j; + uint32_t bestdist, bestmatch, dist; + uint32_t initialdist; + HashTable *h2; + uint32_t pixelVal; + + h2 = hashtable_new(unshifted_pixel_hash, unshifted_pixel_cmp); + for (i = 0; i < nPixels; i++) { + if (hashtable_lookup(h2, pixelData[i], &pixelVal)) { + pixelArray[i] = pixelVal; + continue; + } + if (!hashtable_lookup(medianBoxHash, pixelData[i], &pixelVal)) { +#ifndef NO_OUTPUT + printf("pixel lookup failed\n"); +#endif + return 0; + } + initialdist = _DISTSQR(paletteData + pixelVal, pixelData + i); + bestdist = initialdist; + bestmatch = pixelVal; + initialdist <<= 2; + aDSK = avgDistSortKey + pixelVal * nPaletteEntries; + aD = avgDist + pixelVal * nPaletteEntries; + for (j = 0; j < nPaletteEntries; j++) { + idx = aDSK[j] - aD; + if (*(aDSK[j]) <= initialdist) { + dist = _DISTSQR(paletteData + idx, pixelData + i); + if (dist < bestdist) { + bestdist = dist; + bestmatch = idx; + } + } else { + break; + } + } + pixelArray[i] = bestmatch; + hashtable_insert(h2, pixelData[i], bestmatch); + } + hashtable_free(h2); + return 1; +} + +static int +compute_palette_from_median_cut( + Pixel *pixelData, + uint32_t nPixels, + HashTable *medianBoxHash, + Pixel **palette, + uint32_t nPaletteEntries) { + uint32_t i; + uint32_t paletteEntry; + Pixel *p; + uint32_t *avg[3]; + uint32_t *count; + + *palette = NULL; + /* malloc check ok, using calloc */ + if (!(count = calloc(nPaletteEntries, sizeof(uint32_t)))) { + return 0; + } + for (i = 0; i < 3; i++) { + avg[i] = NULL; + } + for (i = 0; i < 3; i++) { + /* malloc check ok, using calloc */ + if (!(avg[i] = calloc(nPaletteEntries, sizeof(uint32_t)))) { + for (i = 0; i < 3; i++) { + if (avg[i]) { + free(avg[i]); + } + } + free(count); + return 0; + } + } + for (i = 0; i < nPixels; i++) { +#ifdef TEST_SPLIT_INTEGRITY + if (!(i % 100)) { + printf("%05d\r", i); + fflush(stdout); + } + if (checkContained(root, pixelData + i) > 1) { + printf("pixel in two boxes\n"); + for (i = 0; i < 3; i++) { + free(avg[i]); + } + free(count); + return 0; + } +#endif + if (!hashtable_lookup(medianBoxHash, pixelData[i], &paletteEntry)) { +#ifndef NO_OUTPUT + printf("pixel lookup failed\n"); +#endif + for (i = 0; i < 3; i++) { + free(avg[i]); + } + free(count); + return 0; + } + if (paletteEntry >= nPaletteEntries) { +#ifndef NO_OUTPUT + printf( + "panic - paletteEntry>=nPaletteEntries (%d>=%d)\n", + (int)paletteEntry, + (int)nPaletteEntries); +#endif + for (i = 0; i < 3; i++) { + free(avg[i]); + } + free(count); + return 0; + } + avg[0][paletteEntry] += pixelData[i].c.r; + avg[1][paletteEntry] += pixelData[i].c.g; + avg[2][paletteEntry] += pixelData[i].c.b; + count[paletteEntry]++; + } + /* malloc check ok, using calloc */ + p = calloc(nPaletteEntries, sizeof(Pixel)); + if (!p) { + for (i = 0; i < 3; i++) { + free(avg[i]); + } + free(count); + return 0; + } + for (i = 0; i < nPaletteEntries; i++) { + p[i].c.r = (int)(.5 + (double)avg[0][i] / (double)count[i]); + p[i].c.g = (int)(.5 + (double)avg[1][i] / (double)count[i]); + p[i].c.b = (int)(.5 + (double)avg[2][i] / (double)count[i]); + } + *palette = p; + for (i = 0; i < 3; i++) { + free(avg[i]); + } + free(count); + return 1; +} + +static int +recompute_palette_from_averages( + Pixel *palette, uint32_t nPaletteEntries, uint32_t *avg[3], uint32_t *count) { + uint32_t i; + + for (i = 0; i < nPaletteEntries; i++) { + palette[i].c.r = (int)(.5 + (double)avg[0][i] / (double)count[i]); + palette[i].c.g = (int)(.5 + (double)avg[1][i] / (double)count[i]); + palette[i].c.b = (int)(.5 + (double)avg[2][i] / (double)count[i]); + } + return 1; +} + +static int +compute_palette_from_quantized_pixels( + Pixel *pixelData, + uint32_t nPixels, + Pixel *palette, + uint32_t nPaletteEntries, + uint32_t *avg[3], + uint32_t *count, + uint32_t *qp) { + uint32_t i; + + memset(count, 0, sizeof(uint32_t) * nPaletteEntries); + for (i = 0; i < 3; i++) { + memset(avg[i], 0, sizeof(uint32_t) * nPaletteEntries); + } + for (i = 0; i < nPixels; i++) { + if (qp[i] >= nPaletteEntries) { +#ifndef NO_OUTPUT + printf("scream\n"); +#endif + return 0; + } + avg[0][qp[i]] += pixelData[i].c.r; + avg[1][qp[i]] += pixelData[i].c.g; + avg[2][qp[i]] += pixelData[i].c.b; + count[qp[i]]++; + } + for (i = 0; i < nPaletteEntries; i++) { + palette[i].c.r = (int)(.5 + (double)avg[0][i] / (double)count[i]); + palette[i].c.g = (int)(.5 + (double)avg[1][i] / (double)count[i]); + palette[i].c.b = (int)(.5 + (double)avg[2][i] / (double)count[i]); + } + return 1; +} + +static int +k_means( + Pixel *pixelData, + uint32_t nPixels, + Pixel *paletteData, + uint32_t nPaletteEntries, + uint32_t *qp, + int threshold) { + uint32_t *avg[3]; + uint32_t *count; + uint32_t i; + uint32_t *avgDist; + uint32_t **avgDistSortKey; + int changes; + int built = 0; + + if (nPaletteEntries > UINT32_MAX / (sizeof(uint32_t))) { + return 0; + } + /* malloc check ok, using calloc */ + if (!(count = calloc(nPaletteEntries, sizeof(uint32_t)))) { + return 0; + } + for (i = 0; i < 3; i++) { + avg[i] = NULL; + } + for (i = 0; i < 3; i++) { + /* malloc check ok, using calloc */ + if (!(avg[i] = calloc(nPaletteEntries, sizeof(uint32_t)))) { + goto error_1; + } + } + + /* this is enough of a check, since the multiplication n*size is done above */ + if (nPaletteEntries > UINT32_MAX / nPaletteEntries) { + goto error_1; + } + /* malloc check ok, using calloc, checking n*n above */ + avgDist = calloc(nPaletteEntries * nPaletteEntries, sizeof(uint32_t)); + if (!avgDist) { + goto error_1; + } + + /* malloc check ok, using calloc, checking n*n above */ + avgDistSortKey = calloc(nPaletteEntries * nPaletteEntries, sizeof(uint32_t *)); + if (!avgDistSortKey) { + goto error_2; + } + +#ifndef NO_OUTPUT + printf("["); + fflush(stdout); +#endif + while (1) { + if (!built) { + compute_palette_from_quantized_pixels( + pixelData, nPixels, paletteData, nPaletteEntries, avg, count, qp); + if (!build_distance_tables( + avgDist, avgDistSortKey, paletteData, nPaletteEntries)) { + goto error_3; + } + built = 1; + } else { + recompute_palette_from_averages(paletteData, nPaletteEntries, avg, count); + resort_distance_tables( + avgDist, avgDistSortKey, paletteData, nPaletteEntries); + } + changes = map_image_pixels_from_quantized_pixels( + pixelData, + nPixels, + paletteData, + nPaletteEntries, + avgDist, + avgDistSortKey, + qp, + avg, + count); + if (changes < 0) { + goto error_3; + } +#ifndef NO_OUTPUT + printf(".(%d)", changes); + fflush(stdout); +#endif + if (changes <= threshold) { + break; + } + } +#ifndef NO_OUTPUT + printf("]\n"); +#endif + if (avgDistSortKey) { + free(avgDistSortKey); + } + if (avgDist) { + free(avgDist); + } + for (i = 0; i < 3; i++) { + if (avg[i]) { + free(avg[i]); + } + } + if (count) { + free(count); + } + return 1; + +error_3: + if (avgDistSortKey) { + free(avgDistSortKey); + } +error_2: + if (avgDist) { + free(avgDist); + } +error_1: + for (i = 0; i < 3; i++) { + if (avg[i]) { + free(avg[i]); + } + } + if (count) { + free(count); + } + return 0; +} + +static int +quantize( + Pixel *pixelData, + uint32_t nPixels, + uint32_t nQuantPixels, + Pixel **palette, + uint32_t *paletteLength, + uint32_t **quantizedPixels, + int kmeans) { + PixelList *hl[3]; + HashTable *h; + BoxNode *root; + uint32_t i; + uint32_t *qp; + uint32_t nPaletteEntries; + + uint32_t *avgDist; + uint32_t **avgDistSortKey; + Pixel *p; + +#ifndef NO_OUTPUT + uint32_t timer, timer2; +#endif + +#ifndef NO_OUTPUT + timer2 = clock(); + printf("create hash table..."); + fflush(stdout); + timer = clock(); +#endif + h = create_pixel_hash(pixelData, nPixels); +#ifndef NO_OUTPUT + printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); +#endif + if (!h) { + goto error_0; + } + +#ifndef NO_OUTPUT + printf("create lists from hash table..."); + fflush(stdout); + timer = clock(); +#endif + hl[0] = hl[1] = hl[2] = NULL; + hashtable_foreach(h, hash_to_list, hl); +#ifndef NO_OUTPUT + printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); +#endif + + if (!hl[0]) { + goto error_1; + } + +#ifndef NO_OUTPUT + printf("mergesort lists..."); + fflush(stdout); + timer = clock(); +#endif + for (i = 0; i < 3; i++) { + hl[i] = mergesort_pixels(hl[i], i); + } +#ifdef TEST_MERGESORT + if (!test_sorted(hl)) { + printf("bug in mergesort\n"); + goto error_1; + } +#endif +#ifndef NO_OUTPUT + printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); +#endif + +#ifndef NO_OUTPUT + printf("median cut..."); + fflush(stdout); + timer = clock(); +#endif + root = median_cut(hl, nPixels, nQuantPixels); +#ifndef NO_OUTPUT + printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); +#endif + if (!root) { + goto error_1; + } + nPaletteEntries = 0; +#ifndef NO_OUTPUT + printf("median cut tree to hash table..."); + fflush(stdout); + timer = clock(); +#endif + annotate_hash_table(root, h, &nPaletteEntries); +#ifndef NO_OUTPUT + printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); +#endif +#ifndef NO_OUTPUT + printf("compute palette...\n"); + fflush(stdout); + timer = clock(); +#endif + if (!compute_palette_from_median_cut(pixelData, nPixels, h, &p, nPaletteEntries)) { + goto error_3; + } +#ifndef NO_OUTPUT + printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); +#endif + + free_box_tree(root); + root = NULL; + + /* malloc check ok, using calloc for overflow */ + qp = calloc(nPixels, sizeof(uint32_t)); + if (!qp) { + goto error_4; + } + + if (nPaletteEntries > UINT32_MAX / nPaletteEntries) { + goto error_5; + } + /* malloc check ok, using calloc for overflow, check of n*n above */ + avgDist = calloc(nPaletteEntries * nPaletteEntries, sizeof(uint32_t)); + if (!avgDist) { + goto error_5; + } + + /* malloc check ok, using calloc for overflow, check of n*n above */ + avgDistSortKey = calloc(nPaletteEntries * nPaletteEntries, sizeof(uint32_t *)); + if (!avgDistSortKey) { + goto error_6; + } + + if (!build_distance_tables(avgDist, avgDistSortKey, p, nPaletteEntries)) { + goto error_7; + } + + if (!map_image_pixels_from_median_box( + pixelData, nPixels, p, nPaletteEntries, h, avgDist, avgDistSortKey, qp)) { + goto error_7; + } + +#ifdef TEST_NEAREST_NEIGHBOUR +#include + { + uint32_t bestmatch, bestdist, dist; + HashTable *h2; + printf("nearest neighbour search (full search)..."); + fflush(stdout); + timer = clock(); + h2 = hashtable_new(unshifted_pixel_hash, unshifted_pixel_cmp); + for (i = 0; i < nPixels; i++) { + if (hashtable_lookup(h2, pixelData[i], &paletteEntry)) { + bestmatch = paletteEntry; + } else { + bestmatch = 0; + bestdist = _SQR(pixelData[i].c.r - p[0].c.r) + + _SQR(pixelData[i].c.g - p[0].c.g) + + _SQR(pixelData[i].c.b - p[0].c.b); + for (j = 1; j < nPaletteEntries; j++) { + dist = _SQR(pixelData[i].c.r - p[j].c.r) + + _SQR(pixelData[i].c.g - p[j].c.g) + + _SQR(pixelData[i].c.b - p[j].c.b); + if (dist == bestdist && j == qp[i]) { + bestmatch = j; + } + if (dist < bestdist) { + bestdist = dist; + bestmatch = j; + } + } + hashtable_insert(h2, pixelData[i], bestmatch); + } + if (qp[i] != bestmatch) { + printf( + "discrepancy in matching algorithms pixel %d [%d %d] %f %f\n", + i, + qp[i], + bestmatch, + sqrt((double)(_SQR(pixelData[i].c.r - p[qp[i]].c.r) + + _SQR(pixelData[i].c.g - p[qp[i]].c.g) + + _SQR(pixelData[i].c.b - p[qp[i]].c.b))), + sqrt((double)(_SQR(pixelData[i].c.r - p[bestmatch].c.r) + + _SQR(pixelData[i].c.g - p[bestmatch].c.g) + + _SQR(pixelData[i].c.b - p[bestmatch].c.b)))); + } + } + hashtable_free(h2); + } +#endif +#ifndef NO_OUTPUT + printf("k means...\n"); + fflush(stdout); + timer = clock(); +#endif + if (kmeans > 0) { + k_means(pixelData, nPixels, p, nPaletteEntries, qp, kmeans - 1); + } +#ifndef NO_OUTPUT + printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); +#endif + + *quantizedPixels = qp; + *palette = p; + *paletteLength = nPaletteEntries; + +#ifndef NO_OUTPUT + printf("cleanup..."); + fflush(stdout); + timer = clock(); +#endif + if (avgDist) { + free(avgDist); + } + if (avgDistSortKey) { + free(avgDistSortKey); + } + destroy_pixel_hash(h); +#ifndef NO_OUTPUT + printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); + printf("-----\ntotal time %f\n", (clock() - timer2) / (double)CLOCKS_PER_SEC); +#endif + return 1; + +error_7: + if (avgDistSortKey) { + free(avgDistSortKey); + } +error_6: + if (avgDist) { + free(avgDist); + } +error_5: + if (qp) { + free(qp); + } +error_4: + if (p) { + free(p); + } +error_3: + if (root) { + free_box_tree(root); + } +error_1: + destroy_pixel_hash(h); +error_0: + *quantizedPixels = NULL; + *paletteLength = 0; + *palette = NULL; + return 0; +} + +typedef struct { + Pixel new; + uint32_t furthestV; + uint32_t furthestDistance; + int secondPixel; +} DistanceData; + +static void +compute_distances(const HashTable *h, const Pixel pixel, uint32_t *dist, void *u) { + DistanceData *data = (DistanceData *)u; + uint32_t oldDist = *dist; + uint32_t newDist; + newDist = _DISTSQR(&(data->new), &pixel); + if (data->secondPixel || newDist < oldDist) { + *dist = newDist; + oldDist = newDist; + } + if (oldDist > data->furthestDistance) { + data->furthestDistance = oldDist; + data->furthestV = pixel.v; + } +} + +static int +quantize2( + Pixel *pixelData, + uint32_t nPixels, + uint32_t nQuantPixels, + Pixel **palette, + uint32_t *paletteLength, + uint32_t **quantizedPixels, + int kmeans) { + HashTable *h; + uint32_t i; + uint32_t mean[3]; + Pixel *p; + DistanceData data; + + uint32_t *qp; + uint32_t *avgDist; + uint32_t **avgDistSortKey; + + /* malloc check ok, using calloc */ + p = calloc(nQuantPixels, sizeof(Pixel)); + if (!p) { + return 0; + } + mean[0] = mean[1] = mean[2] = 0; + h = hashtable_new(unshifted_pixel_hash, unshifted_pixel_cmp); + for (i = 0; i < nPixels; i++) { + hashtable_insert(h, pixelData[i], 0xffffffff); + mean[0] += pixelData[i].c.r; + mean[1] += pixelData[i].c.g; + mean[2] += pixelData[i].c.b; + } + data.new.c.r = (int)(.5 + (double)mean[0] / (double)nPixels); + data.new.c.g = (int)(.5 + (double)mean[1] / (double)nPixels); + data.new.c.b = (int)(.5 + (double)mean[2] / (double)nPixels); + for (i = 0; i < nQuantPixels; i++) { + data.furthestDistance = 0; + data.furthestV = pixelData[0].v; + data.secondPixel = (i == 1) ? 1 : 0; + hashtable_foreach_update(h, compute_distances, &data); + p[i].v = data.furthestV; + data.new.v = data.furthestV; + } + hashtable_free(h); + + /* malloc check ok, using calloc */ + qp = calloc(nPixels, sizeof(uint32_t)); + if (!qp) { + goto error_1; + } + + if (nQuantPixels > UINT32_MAX / nQuantPixels) { + goto error_2; + } + + /* malloc check ok, using calloc for overflow, check of n*n above */ + avgDist = calloc(nQuantPixels * nQuantPixels, sizeof(uint32_t)); + if (!avgDist) { + goto error_2; + } + + /* malloc check ok, using calloc for overflow, check of n*n above */ + avgDistSortKey = calloc(nQuantPixels * nQuantPixels, sizeof(uint32_t *)); + if (!avgDistSortKey) { + goto error_3; + } + + if (!build_distance_tables(avgDist, avgDistSortKey, p, nQuantPixels)) { + goto error_4; + } + + if (!map_image_pixels( + pixelData, nPixels, p, nQuantPixels, avgDist, avgDistSortKey, qp)) { + goto error_4; + } + if (kmeans > 0) { + k_means(pixelData, nPixels, p, nQuantPixels, qp, kmeans - 1); + } + + *paletteLength = nQuantPixels; + *palette = p; + *quantizedPixels = qp; + free(avgDistSortKey); + free(avgDist); + return 1; + +error_4: + free(avgDistSortKey); +error_3: + free(avgDist); +error_2: + free(qp); +error_1: + free(p); + return 0; +} + +Imaging +ImagingQuantize(Imaging im, int colors, int mode, int kmeans) { + int i, j; + int x, y, v; + UINT8 *pp; + Pixel *p; + Pixel *palette; + uint32_t paletteLength; + int result; + uint32_t *newData; + Imaging imOut; + int withAlpha = 0; + ImagingSectionCookie cookie; + + if (!im) { + return ImagingError_ModeError(); + } + if (colors < 1 || colors > 256) { + /* FIXME: for colors > 256, consider returning an RGB image + instead (see @PIL205) */ + return (Imaging)ImagingError_ValueError("bad number of colors"); + } + + if (strcmp(im->mode, "L") != 0 && strcmp(im->mode, "P") != 0 && + strcmp(im->mode, "RGB") != 0 && strcmp(im->mode, "RGBA") != 0) { + return ImagingError_ModeError(); + } + + /* only octree and imagequant supports RGBA */ + if (!strcmp(im->mode, "RGBA") && mode != 2 && mode != 3) { + return ImagingError_ModeError(); + } + + if (im->xsize > INT_MAX / im->ysize) { + return ImagingError_MemoryError(); + } + /* malloc check ok, using calloc for final overflow, x*y above */ + p = calloc(im->xsize * im->ysize, sizeof(Pixel)); + if (!p) { + return ImagingError_MemoryError(); + } + + /* collect statistics */ + + /* FIXME: maybe we could load the hash tables directly from the + image data? */ + + if (!strcmp(im->mode, "L")) { + /* grayscale */ + + /* FIXME: converting a "L" image to "P" with 256 colors + should be done by a simple copy... */ + + for (i = y = 0; y < im->ysize; y++) { + for (x = 0; x < im->xsize; x++, i++) { + p[i].c.r = p[i].c.g = p[i].c.b = im->image8[y][x]; + p[i].c.a = 255; + } + } + + } else if (!strcmp(im->mode, "P")) { + /* palette */ + + pp = im->palette->palette; + + for (i = y = 0; y < im->ysize; y++) { + for (x = 0; x < im->xsize; x++, i++) { + v = im->image8[y][x]; + p[i].c.r = pp[v * 4 + 0]; + p[i].c.g = pp[v * 4 + 1]; + p[i].c.b = pp[v * 4 + 2]; + p[i].c.a = pp[v * 4 + 3]; + } + } + + } else if (!strcmp(im->mode, "RGB") || !strcmp(im->mode, "RGBA")) { + /* true colour */ + + withAlpha = !strcmp(im->mode, "RGBA"); + int transparency = 0; + unsigned char r = 0, g = 0, b = 0; + for (i = y = 0; y < im->ysize; y++) { + for (x = 0; x < im->xsize; x++, i++) { + p[i].v = im->image32[y][x]; + if (withAlpha && p[i].c.a == 0) { + if (transparency == 0) { + transparency = 1; + r = p[i].c.r; + g = p[i].c.g; + b = p[i].c.b; + } else { + /* Set all subsequent transparent pixels + to the same colour as the first */ + p[i].c.r = r; + p[i].c.g = g; + p[i].c.b = b; + } + } + } + } + + } else { + free(p); + return (Imaging)ImagingError_ValueError("internal error"); + } + + ImagingSectionEnter(&cookie); + + switch (mode) { + case 0: + /* median cut */ + result = quantize( + p, + im->xsize * im->ysize, + colors, + &palette, + &paletteLength, + &newData, + kmeans); + break; + case 1: + /* maximum coverage */ + result = quantize2( + p, + im->xsize * im->ysize, + colors, + &palette, + &paletteLength, + &newData, + kmeans); + break; + case 2: + result = quantize_octree( + p, + im->xsize * im->ysize, + colors, + &palette, + &paletteLength, + &newData, + withAlpha); + break; + case 3: +#ifdef HAVE_LIBIMAGEQUANT + result = quantize_pngquant( + p, + im->xsize, + im->ysize, + colors, + &palette, + &paletteLength, + &newData, + withAlpha); +#else + result = -1; +#endif + break; + default: + result = 0; + break; + } + + free(p); + ImagingSectionLeave(&cookie); + + if (result > 0) { + imOut = ImagingNewDirty("P", im->xsize, im->ysize); + ImagingSectionEnter(&cookie); + + for (i = y = 0; y < im->ysize; y++) { + for (x = 0; x < im->xsize; x++) { + imOut->image8[y][x] = (unsigned char)newData[i++]; + } + } + + free(newData); + + imOut->palette->size = (int)paletteLength; + pp = imOut->palette->palette; + + for (i = j = 0; i < (int)paletteLength; i++) { + *pp++ = palette[i].c.r; + *pp++ = palette[i].c.g; + *pp++ = palette[i].c.b; + if (withAlpha) { + *pp = palette[i].c.a; + } + pp++; + } + + if (withAlpha) { + strcpy(imOut->palette->mode, "RGBA"); + } + + free(palette); + ImagingSectionLeave(&cookie); + + return imOut; + + } else { + if (result == -1) { + return (Imaging)ImagingError_ValueError( + "dependency required by this method was not " + "enabled at compile time"); + } + + return (Imaging)ImagingError_ValueError("quantization error"); + } +} diff --git a/src/libImaging/QuantHash.c b/src/libImaging/QuantHash.c index ea75d6037..ef8dba623 100644 --- a/src/libImaging/QuantHash.c +++ b/src/libImaging/QuantHash.c @@ -1,336 +1,336 @@ -/* - * The Python Imaging Library - * $Id$ - * - * hash tables used by the image quantizer - * - * history: - * 98-09-10 tjs Contributed - * 98-12-29 fl Added to PIL 1.0b1 - * - * Written by Toby J Sargeant . - * - * Copyright (c) 1998 by Toby J Sargeant - * Copyright (c) 1998 by Secret Labs AB - * - * See the README file for information on usage and redistribution. - */ - -#include -#include -#include -#include - -#include "QuantHash.h" - -typedef struct _HashNode { - struct _HashNode *next; - HashKey_t key; - HashVal_t value; -} HashNode; - -struct _HashTable { - HashNode **table; - uint32_t length; - uint32_t count; - HashFunc hashFunc; - HashCmpFunc cmpFunc; - void *userData; -}; - -#define MIN_LENGTH 11 -#define RESIZE_FACTOR 3 - -static int -_hashtable_insert_node(HashTable *, HashNode *, int, int, CollisionFunc); - -HashTable * -hashtable_new(HashFunc hf, HashCmpFunc cf) { - HashTable *h; - h = malloc(sizeof(HashTable)); - if (!h) { - return NULL; - } - h->hashFunc = hf; - h->cmpFunc = cf; - h->length = MIN_LENGTH; - h->count = 0; - h->userData = NULL; - h->table = malloc(sizeof(HashNode *) * h->length); - if (!h->table) { - free(h); - return NULL; - } - memset(h->table, 0, sizeof(HashNode *) * h->length); - return h; -} - -static uint32_t -_findPrime(uint32_t start, int dir) { - static int unit[] = {0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0}; - uint32_t t; - while (start > 1) { - if (!unit[start & 0x0f]) { - start += dir; - continue; - } - for (t = 2; t < sqrt((double)start); t++) { - if (!start % t) { - break; - } - } - if (t >= sqrt((double)start)) { - break; - } - start += dir; - } - return start; -} - -static void -_hashtable_rehash(HashTable *h, CollisionFunc cf, uint32_t newSize) { - HashNode **oldTable = h->table; - uint32_t i; - HashNode *n, *nn; - uint32_t oldSize; - oldSize = h->length; - h->table = malloc(sizeof(HashNode *) * newSize); - if (!h->table) { - h->table = oldTable; - return; - } - h->length = newSize; - h->count = 0; - memset(h->table, 0, sizeof(HashNode *) * h->length); - for (i = 0; i < oldSize; i++) { - for (n = oldTable[i]; n; n = nn) { - nn = n->next; - _hashtable_insert_node(h, n, 0, 0, cf); - } - } - free(oldTable); -} - -static void -_hashtable_resize(HashTable *h) { - uint32_t newSize; - uint32_t oldSize; - oldSize = h->length; - newSize = oldSize; - if (h->count * RESIZE_FACTOR < h->length) { - newSize = _findPrime(h->length / 2 - 1, -1); - } else if (h->length * RESIZE_FACTOR < h->count) { - newSize = _findPrime(h->length * 2 + 1, +1); - } - if (newSize < MIN_LENGTH) { - newSize = oldSize; - } - if (newSize != oldSize) { - _hashtable_rehash(h, NULL, newSize); - } -} - -static int -_hashtable_insert_node( - HashTable *h, HashNode *node, int resize, int update, CollisionFunc cf) { - uint32_t hash = h->hashFunc(h, node->key) % h->length; - HashNode **n, *nv; - int i; - - for (n = &(h->table[hash]); *n; n = &((*n)->next)) { - nv = *n; - i = h->cmpFunc(h, nv->key, node->key); - if (!i) { - if (cf) { - nv->key = node->key; - cf(h, &(nv->key), &(nv->value), node->key, node->value); - free(node); - return 1; - } else { - nv->key = node->key; - nv->value = node->value; - free(node); - return 1; - } - } else if (i > 0) { - break; - } - } - if (!update) { - node->next = *n; - *n = node; - h->count++; - if (resize) { - _hashtable_resize(h); - } - return 1; - } else { - return 0; - } -} - -static int -_hashtable_insert(HashTable *h, HashKey_t key, HashVal_t val, int resize, int update) { - HashNode **n, *nv; - HashNode *t; - int i; - uint32_t hash = h->hashFunc(h, key) % h->length; - - for (n = &(h->table[hash]); *n; n = &((*n)->next)) { - nv = *n; - i = h->cmpFunc(h, nv->key, key); - if (!i) { - nv->value = val; - return 1; - } else if (i > 0) { - break; - } - } - if (!update) { - t = malloc(sizeof(HashNode)); - if (!t) { - return 0; - } - t->next = *n; - *n = t; - t->key = key; - t->value = val; - h->count++; - if (resize) { - _hashtable_resize(h); - } - return 1; - } else { - return 0; - } -} - -int -hashtable_insert_or_update_computed( - HashTable *h, HashKey_t key, ComputeFunc newFunc, ComputeFunc existsFunc) { - HashNode **n, *nv; - HashNode *t; - int i; - uint32_t hash = h->hashFunc(h, key) % h->length; - - for (n = &(h->table[hash]); *n; n = &((*n)->next)) { - nv = *n; - i = h->cmpFunc(h, nv->key, key); - if (!i) { - if (existsFunc) { - existsFunc(h, nv->key, &(nv->value)); - } else { - return 0; - } - return 1; - } else if (i > 0) { - break; - } - } - t = malloc(sizeof(HashNode)); - if (!t) { - return 0; - } - t->key = key; - t->next = *n; - *n = t; - if (newFunc) { - newFunc(h, t->key, &(t->value)); - } else { - free(t); - return 0; - } - h->count++; - _hashtable_resize(h); - return 1; -} - -int -hashtable_insert(HashTable *h, HashKey_t key, HashVal_t val) { - return _hashtable_insert(h, key, val, 1, 0); -} - -void -hashtable_foreach_update(HashTable *h, IteratorUpdateFunc i, void *u) { - HashNode *n; - uint32_t x; - - if (h->table) { - for (x = 0; x < h->length; x++) { - for (n = h->table[x]; n; n = n->next) { - i(h, n->key, &(n->value), u); - } - } - } -} - -void -hashtable_foreach(HashTable *h, IteratorFunc i, void *u) { - HashNode *n; - uint32_t x; - - if (h->table) { - for (x = 0; x < h->length; x++) { - for (n = h->table[x]; n; n = n->next) { - i(h, n->key, n->value, u); - } - } - } -} - -void -hashtable_free(HashTable *h) { - HashNode *n, *nn; - uint32_t i; - - if (h->table) { - for (i = 0; i < h->length; i++) { - for (n = h->table[i]; n; n = nn) { - nn = n->next; - free(n); - } - } - free(h->table); - } - free(h); -} - -void -hashtable_rehash_compute(HashTable *h, CollisionFunc cf) { - _hashtable_rehash(h, cf, h->length); -} - -int -hashtable_lookup(const HashTable *h, const HashKey_t key, HashVal_t *valp) { - uint32_t hash = h->hashFunc(h, key) % h->length; - HashNode *n; - int i; - - for (n = h->table[hash]; n; n = n->next) { - i = h->cmpFunc(h, n->key, key); - if (!i) { - *valp = n->value; - return 1; - } else if (i > 0) { - break; - } - } - return 0; -} - -uint32_t -hashtable_get_count(const HashTable *h) { - return h->count; -} - -void * -hashtable_get_user_data(const HashTable *h) { - return h->userData; -} - -void * -hashtable_set_user_data(HashTable *h, void *data) { - void *r = h->userData; - h->userData = data; - return r; -} +/* + * The Python Imaging Library + * $Id$ + * + * hash tables used by the image quantizer + * + * history: + * 98-09-10 tjs Contributed + * 98-12-29 fl Added to PIL 1.0b1 + * + * Written by Toby J Sargeant . + * + * Copyright (c) 1998 by Toby J Sargeant + * Copyright (c) 1998 by Secret Labs AB + * + * See the README file for information on usage and redistribution. + */ + +#include +#include +#include +#include + +#include "QuantHash.h" + +typedef struct _HashNode { + struct _HashNode *next; + HashKey_t key; + HashVal_t value; +} HashNode; + +struct _HashTable { + HashNode **table; + uint32_t length; + uint32_t count; + HashFunc hashFunc; + HashCmpFunc cmpFunc; + void *userData; +}; + +#define MIN_LENGTH 11 +#define RESIZE_FACTOR 3 + +static int +_hashtable_insert_node(HashTable *, HashNode *, int, int, CollisionFunc); + +HashTable * +hashtable_new(HashFunc hf, HashCmpFunc cf) { + HashTable *h; + h = malloc(sizeof(HashTable)); + if (!h) { + return NULL; + } + h->hashFunc = hf; + h->cmpFunc = cf; + h->length = MIN_LENGTH; + h->count = 0; + h->userData = NULL; + h->table = malloc(sizeof(HashNode *) * h->length); + if (!h->table) { + free(h); + return NULL; + } + memset(h->table, 0, sizeof(HashNode *) * h->length); + return h; +} + +static uint32_t +_findPrime(uint32_t start, int dir) { + static int unit[] = {0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0}; + uint32_t t; + while (start > 1) { + if (!unit[start & 0x0f]) { + start += dir; + continue; + } + for (t = 2; t < sqrt((double)start); t++) { + if (!start % t) { + break; + } + } + if (t >= sqrt((double)start)) { + break; + } + start += dir; + } + return start; +} + +static void +_hashtable_rehash(HashTable *h, CollisionFunc cf, uint32_t newSize) { + HashNode **oldTable = h->table; + uint32_t i; + HashNode *n, *nn; + uint32_t oldSize; + oldSize = h->length; + h->table = malloc(sizeof(HashNode *) * newSize); + if (!h->table) { + h->table = oldTable; + return; + } + h->length = newSize; + h->count = 0; + memset(h->table, 0, sizeof(HashNode *) * h->length); + for (i = 0; i < oldSize; i++) { + for (n = oldTable[i]; n; n = nn) { + nn = n->next; + _hashtable_insert_node(h, n, 0, 0, cf); + } + } + free(oldTable); +} + +static void +_hashtable_resize(HashTable *h) { + uint32_t newSize; + uint32_t oldSize; + oldSize = h->length; + newSize = oldSize; + if (h->count * RESIZE_FACTOR < h->length) { + newSize = _findPrime(h->length / 2 - 1, -1); + } else if (h->length * RESIZE_FACTOR < h->count) { + newSize = _findPrime(h->length * 2 + 1, +1); + } + if (newSize < MIN_LENGTH) { + newSize = oldSize; + } + if (newSize != oldSize) { + _hashtable_rehash(h, NULL, newSize); + } +} + +static int +_hashtable_insert_node( + HashTable *h, HashNode *node, int resize, int update, CollisionFunc cf) { + uint32_t hash = h->hashFunc(h, node->key) % h->length; + HashNode **n, *nv; + int i; + + for (n = &(h->table[hash]); *n; n = &((*n)->next)) { + nv = *n; + i = h->cmpFunc(h, nv->key, node->key); + if (!i) { + if (cf) { + nv->key = node->key; + cf(h, &(nv->key), &(nv->value), node->key, node->value); + free(node); + return 1; + } else { + nv->key = node->key; + nv->value = node->value; + free(node); + return 1; + } + } else if (i > 0) { + break; + } + } + if (!update) { + node->next = *n; + *n = node; + h->count++; + if (resize) { + _hashtable_resize(h); + } + return 1; + } else { + return 0; + } +} + +static int +_hashtable_insert(HashTable *h, HashKey_t key, HashVal_t val, int resize, int update) { + HashNode **n, *nv; + HashNode *t; + int i; + uint32_t hash = h->hashFunc(h, key) % h->length; + + for (n = &(h->table[hash]); *n; n = &((*n)->next)) { + nv = *n; + i = h->cmpFunc(h, nv->key, key); + if (!i) { + nv->value = val; + return 1; + } else if (i > 0) { + break; + } + } + if (!update) { + t = malloc(sizeof(HashNode)); + if (!t) { + return 0; + } + t->next = *n; + *n = t; + t->key = key; + t->value = val; + h->count++; + if (resize) { + _hashtable_resize(h); + } + return 1; + } else { + return 0; + } +} + +int +hashtable_insert_or_update_computed( + HashTable *h, HashKey_t key, ComputeFunc newFunc, ComputeFunc existsFunc) { + HashNode **n, *nv; + HashNode *t; + int i; + uint32_t hash = h->hashFunc(h, key) % h->length; + + for (n = &(h->table[hash]); *n; n = &((*n)->next)) { + nv = *n; + i = h->cmpFunc(h, nv->key, key); + if (!i) { + if (existsFunc) { + existsFunc(h, nv->key, &(nv->value)); + } else { + return 0; + } + return 1; + } else if (i > 0) { + break; + } + } + t = malloc(sizeof(HashNode)); + if (!t) { + return 0; + } + t->key = key; + t->next = *n; + *n = t; + if (newFunc) { + newFunc(h, t->key, &(t->value)); + } else { + free(t); + return 0; + } + h->count++; + _hashtable_resize(h); + return 1; +} + +int +hashtable_insert(HashTable *h, HashKey_t key, HashVal_t val) { + return _hashtable_insert(h, key, val, 1, 0); +} + +void +hashtable_foreach_update(HashTable *h, IteratorUpdateFunc i, void *u) { + HashNode *n; + uint32_t x; + + if (h->table) { + for (x = 0; x < h->length; x++) { + for (n = h->table[x]; n; n = n->next) { + i(h, n->key, &(n->value), u); + } + } + } +} + +void +hashtable_foreach(HashTable *h, IteratorFunc i, void *u) { + HashNode *n; + uint32_t x; + + if (h->table) { + for (x = 0; x < h->length; x++) { + for (n = h->table[x]; n; n = n->next) { + i(h, n->key, n->value, u); + } + } + } +} + +void +hashtable_free(HashTable *h) { + HashNode *n, *nn; + uint32_t i; + + if (h->table) { + for (i = 0; i < h->length; i++) { + for (n = h->table[i]; n; n = nn) { + nn = n->next; + free(n); + } + } + free(h->table); + } + free(h); +} + +void +hashtable_rehash_compute(HashTable *h, CollisionFunc cf) { + _hashtable_rehash(h, cf, h->length); +} + +int +hashtable_lookup(const HashTable *h, const HashKey_t key, HashVal_t *valp) { + uint32_t hash = h->hashFunc(h, key) % h->length; + HashNode *n; + int i; + + for (n = h->table[hash]; n; n = n->next) { + i = h->cmpFunc(h, n->key, key); + if (!i) { + *valp = n->value; + return 1; + } else if (i > 0) { + break; + } + } + return 0; +} + +uint32_t +hashtable_get_count(const HashTable *h) { + return h->count; +} + +void * +hashtable_get_user_data(const HashTable *h) { + return h->userData; +} + +void * +hashtable_set_user_data(HashTable *h, void *data) { + void *r = h->userData; + h->userData = data; + return r; +} diff --git a/src/libImaging/QuantHash.h b/src/libImaging/QuantHash.h index fc1a99003..d1baf9d2a 100644 --- a/src/libImaging/QuantHash.h +++ b/src/libImaging/QuantHash.h @@ -1,55 +1,55 @@ -/* - * The Python Imaging Library - * $Id$ - * - * image quantizer - * - * Written by Toby J Sargeant . - * - * See the README file for information on usage and redistribution. - */ - -#ifndef __QUANTHASH_H__ -#define __QUANTHASH_H__ - -#include "QuantTypes.h" - -typedef struct _HashTable HashTable; -typedef Pixel HashKey_t; -typedef uint32_t HashVal_t; - -typedef uint32_t (*HashFunc)(const HashTable *, const HashKey_t); -typedef int (*HashCmpFunc)(const HashTable *, const HashKey_t, const HashKey_t); -typedef void (*IteratorFunc)( - const HashTable *, const HashKey_t, const HashVal_t, void *); -typedef void (*IteratorUpdateFunc)( - const HashTable *, const HashKey_t, HashVal_t *, void *); -typedef void (*ComputeFunc)(const HashTable *, const HashKey_t, HashVal_t *); -typedef void (*CollisionFunc)( - const HashTable *, HashKey_t *, HashVal_t *, HashKey_t, HashVal_t); - -HashTable * -hashtable_new(HashFunc hf, HashCmpFunc cf); -void -hashtable_free(HashTable *h); -void -hashtable_foreach(HashTable *h, IteratorFunc i, void *u); -void -hashtable_foreach_update(HashTable *h, IteratorUpdateFunc i, void *u); -int -hashtable_insert(HashTable *h, HashKey_t key, HashVal_t val); -int -hashtable_lookup(const HashTable *h, const HashKey_t key, HashVal_t *valp); -int -hashtable_insert_or_update_computed( - HashTable *h, HashKey_t key, ComputeFunc newFunc, ComputeFunc existsFunc); -void * -hashtable_set_user_data(HashTable *h, void *data); -void * -hashtable_get_user_data(const HashTable *h); -uint32_t -hashtable_get_count(const HashTable *h); -void -hashtable_rehash_compute(HashTable *h, CollisionFunc cf); - -#endif // __QUANTHASH_H__ +/* + * The Python Imaging Library + * $Id$ + * + * image quantizer + * + * Written by Toby J Sargeant . + * + * See the README file for information on usage and redistribution. + */ + +#ifndef __QUANTHASH_H__ +#define __QUANTHASH_H__ + +#include "QuantTypes.h" + +typedef struct _HashTable HashTable; +typedef Pixel HashKey_t; +typedef uint32_t HashVal_t; + +typedef uint32_t (*HashFunc)(const HashTable *, const HashKey_t); +typedef int (*HashCmpFunc)(const HashTable *, const HashKey_t, const HashKey_t); +typedef void (*IteratorFunc)( + const HashTable *, const HashKey_t, const HashVal_t, void *); +typedef void (*IteratorUpdateFunc)( + const HashTable *, const HashKey_t, HashVal_t *, void *); +typedef void (*ComputeFunc)(const HashTable *, const HashKey_t, HashVal_t *); +typedef void (*CollisionFunc)( + const HashTable *, HashKey_t *, HashVal_t *, HashKey_t, HashVal_t); + +HashTable * +hashtable_new(HashFunc hf, HashCmpFunc cf); +void +hashtable_free(HashTable *h); +void +hashtable_foreach(HashTable *h, IteratorFunc i, void *u); +void +hashtable_foreach_update(HashTable *h, IteratorUpdateFunc i, void *u); +int +hashtable_insert(HashTable *h, HashKey_t key, HashVal_t val); +int +hashtable_lookup(const HashTable *h, const HashKey_t key, HashVal_t *valp); +int +hashtable_insert_or_update_computed( + HashTable *h, HashKey_t key, ComputeFunc newFunc, ComputeFunc existsFunc); +void * +hashtable_set_user_data(HashTable *h, void *data); +void * +hashtable_get_user_data(const HashTable *h); +uint32_t +hashtable_get_count(const HashTable *h); +void +hashtable_rehash_compute(HashTable *h, CollisionFunc cf); + +#endif // __QUANTHASH_H__ diff --git a/src/libImaging/QuantHeap.c b/src/libImaging/QuantHeap.c index 6fb52d890..403310b9e 100644 --- a/src/libImaging/QuantHeap.c +++ b/src/libImaging/QuantHeap.c @@ -1,176 +1,176 @@ -/* - * The Python Imaging Library - * $Id$ - * - * heap data type used by the image quantizer - * - * history: - * 98-09-10 tjs Contributed - * 98-12-29 fl Added to PIL 1.0b1 - * - * Written by Toby J Sargeant . - * - * Copyright (c) 1998 by Toby J Sargeant - * Copyright (c) 1998 by Secret Labs AB - * - * See the README file for information on usage and redistribution. - */ - -#include -#include -#include -#include -#include - -#include "QuantHeap.h" - -struct _Heap { - void **heap; - unsigned int heapsize; - unsigned int heapcount; - HeapCmpFunc cf; -}; - -#define INITIAL_SIZE 256 - -// #define DEBUG - -#ifdef DEBUG -static int -_heap_test(Heap *); -#endif - -void -ImagingQuantHeapFree(Heap *h) { - free(h->heap); - free(h); -} - -static int -_heap_grow(Heap *h, unsigned int newsize) { - void *newheap; - if (!newsize) { - newsize = h->heapsize << 1; - } - if (newsize < h->heapsize) { - return 0; - } - if (newsize > INT_MAX / sizeof(void *)) { - return 0; - } - /* malloc check ok, using calloc for overflow, also checking - above due to memcpy below*/ - newheap = calloc(newsize, sizeof(void *)); - if (!newheap) { - return 0; - } - memcpy(newheap, h->heap, sizeof(void *) * h->heapsize); - free(h->heap); - h->heap = newheap; - h->heapsize = newsize; - return 1; -} - -#ifdef DEBUG -static int -_heap_test(Heap *h) { - unsigned int k; - for (k = 1; k * 2 <= h->heapcount; k++) { - if (h->cf(h, h->heap[k], h->heap[k * 2]) < 0) { - printf("heap is bad\n"); - return 0; - } - if (k * 2 + 1 <= h->heapcount && h->cf(h, h->heap[k], h->heap[k * 2 + 1]) < 0) { - printf("heap is bad\n"); - return 0; - } - } - return 1; -} -#endif - -int -ImagingQuantHeapRemove(Heap *h, void **r) { - unsigned int k, l; - void *v; - - if (!h->heapcount) { - return 0; - } - *r = h->heap[1]; - v = h->heap[h->heapcount--]; - for (k = 1; k * 2 <= h->heapcount; k = l) { - l = k * 2; - if (l < h->heapcount) { - if (h->cf(h, h->heap[l], h->heap[l + 1]) < 0) { - l++; - } - } - if (h->cf(h, v, h->heap[l]) > 0) { - break; - } - h->heap[k] = h->heap[l]; - } - h->heap[k] = v; -#ifdef DEBUG - if (!_heap_test(h)) { - printf("oops - heap_remove messed up the heap\n"); - exit(1); - } -#endif - return 1; -} - -int -ImagingQuantHeapAdd(Heap *h, void *val) { - int k; - if (h->heapcount == h->heapsize - 1) { - _heap_grow(h, 0); - } - k = ++h->heapcount; - while (k != 1) { - if (h->cf(h, val, h->heap[k / 2]) <= 0) { - break; - } - h->heap[k] = h->heap[k / 2]; - k >>= 1; - } - h->heap[k] = val; -#ifdef DEBUG - if (!_heap_test(h)) { - printf("oops - heap_add messed up the heap\n"); - exit(1); - } -#endif - return 1; -} - -int -ImagingQuantHeapTop(Heap *h, void **r) { - if (!h->heapcount) { - return 0; - } - *r = h->heap[1]; - return 1; -} - -Heap * -ImagingQuantHeapNew(HeapCmpFunc cf) { - Heap *h; - - /* malloc check ok, small constant allocation */ - h = malloc(sizeof(Heap)); - if (!h) { - return NULL; - } - h->heapsize = INITIAL_SIZE; - /* malloc check ok, using calloc for overflow */ - h->heap = calloc(h->heapsize, sizeof(void *)); - if (!h->heap) { - free(h); - return NULL; - } - h->heapcount = 0; - h->cf = cf; - return h; -} +/* + * The Python Imaging Library + * $Id$ + * + * heap data type used by the image quantizer + * + * history: + * 98-09-10 tjs Contributed + * 98-12-29 fl Added to PIL 1.0b1 + * + * Written by Toby J Sargeant . + * + * Copyright (c) 1998 by Toby J Sargeant + * Copyright (c) 1998 by Secret Labs AB + * + * See the README file for information on usage and redistribution. + */ + +#include +#include +#include +#include +#include + +#include "QuantHeap.h" + +struct _Heap { + void **heap; + unsigned int heapsize; + unsigned int heapcount; + HeapCmpFunc cf; +}; + +#define INITIAL_SIZE 256 + +// #define DEBUG + +#ifdef DEBUG +static int +_heap_test(Heap *); +#endif + +void +ImagingQuantHeapFree(Heap *h) { + free(h->heap); + free(h); +} + +static int +_heap_grow(Heap *h, unsigned int newsize) { + void *newheap; + if (!newsize) { + newsize = h->heapsize << 1; + } + if (newsize < h->heapsize) { + return 0; + } + if (newsize > INT_MAX / sizeof(void *)) { + return 0; + } + /* malloc check ok, using calloc for overflow, also checking + above due to memcpy below*/ + newheap = calloc(newsize, sizeof(void *)); + if (!newheap) { + return 0; + } + memcpy(newheap, h->heap, sizeof(void *) * h->heapsize); + free(h->heap); + h->heap = newheap; + h->heapsize = newsize; + return 1; +} + +#ifdef DEBUG +static int +_heap_test(Heap *h) { + unsigned int k; + for (k = 1; k * 2 <= h->heapcount; k++) { + if (h->cf(h, h->heap[k], h->heap[k * 2]) < 0) { + printf("heap is bad\n"); + return 0; + } + if (k * 2 + 1 <= h->heapcount && h->cf(h, h->heap[k], h->heap[k * 2 + 1]) < 0) { + printf("heap is bad\n"); + return 0; + } + } + return 1; +} +#endif + +int +ImagingQuantHeapRemove(Heap *h, void **r) { + unsigned int k, l; + void *v; + + if (!h->heapcount) { + return 0; + } + *r = h->heap[1]; + v = h->heap[h->heapcount--]; + for (k = 1; k * 2 <= h->heapcount; k = l) { + l = k * 2; + if (l < h->heapcount) { + if (h->cf(h, h->heap[l], h->heap[l + 1]) < 0) { + l++; + } + } + if (h->cf(h, v, h->heap[l]) > 0) { + break; + } + h->heap[k] = h->heap[l]; + } + h->heap[k] = v; +#ifdef DEBUG + if (!_heap_test(h)) { + printf("oops - heap_remove messed up the heap\n"); + exit(1); + } +#endif + return 1; +} + +int +ImagingQuantHeapAdd(Heap *h, void *val) { + int k; + if (h->heapcount == h->heapsize - 1) { + _heap_grow(h, 0); + } + k = ++h->heapcount; + while (k != 1) { + if (h->cf(h, val, h->heap[k / 2]) <= 0) { + break; + } + h->heap[k] = h->heap[k / 2]; + k >>= 1; + } + h->heap[k] = val; +#ifdef DEBUG + if (!_heap_test(h)) { + printf("oops - heap_add messed up the heap\n"); + exit(1); + } +#endif + return 1; +} + +int +ImagingQuantHeapTop(Heap *h, void **r) { + if (!h->heapcount) { + return 0; + } + *r = h->heap[1]; + return 1; +} + +Heap * +ImagingQuantHeapNew(HeapCmpFunc cf) { + Heap *h; + + /* malloc check ok, small constant allocation */ + h = malloc(sizeof(Heap)); + if (!h) { + return NULL; + } + h->heapsize = INITIAL_SIZE; + /* malloc check ok, using calloc for overflow */ + h->heap = calloc(h->heapsize, sizeof(void *)); + if (!h->heap) { + free(h); + return NULL; + } + h->heapcount = 0; + h->cf = cf; + return h; +} diff --git a/src/libImaging/QuantHeap.h b/src/libImaging/QuantHeap.h index c5286dff2..9b45afd1d 100644 --- a/src/libImaging/QuantHeap.h +++ b/src/libImaging/QuantHeap.h @@ -1,31 +1,31 @@ -/* - * The Python Imaging Library - * $Id$ - * - * image quantizer - * - * Written by Toby J Sargeant . - * - * See the README file for information on usage and redistribution. - */ - -#ifndef __QUANTHEAP_H__ -#define __QUANTHEAP_H__ - -#include "QuantTypes.h" - -typedef struct _Heap Heap; - -typedef int (*HeapCmpFunc)(const Heap *, const void *, const void *); - -void -ImagingQuantHeapFree(Heap *); -int -ImagingQuantHeapRemove(Heap *, void **); -int -ImagingQuantHeapAdd(Heap *, void *); -int -ImagingQuantHeapTop(Heap *, void **); -Heap *ImagingQuantHeapNew(HeapCmpFunc); - -#endif // __QUANTHEAP_H__ +/* + * The Python Imaging Library + * $Id$ + * + * image quantizer + * + * Written by Toby J Sargeant . + * + * See the README file for information on usage and redistribution. + */ + +#ifndef __QUANTHEAP_H__ +#define __QUANTHEAP_H__ + +#include "QuantTypes.h" + +typedef struct _Heap Heap; + +typedef int (*HeapCmpFunc)(const Heap *, const void *, const void *); + +void +ImagingQuantHeapFree(Heap *); +int +ImagingQuantHeapRemove(Heap *, void **); +int +ImagingQuantHeapAdd(Heap *, void *); +int +ImagingQuantHeapTop(Heap *, void **); +Heap *ImagingQuantHeapNew(HeapCmpFunc); + +#endif // __QUANTHEAP_H__ diff --git a/src/libImaging/QuantOctree.c b/src/libImaging/QuantOctree.c index 1331a30ad..86a5b994b 100644 --- a/src/libImaging/QuantOctree.c +++ b/src/libImaging/QuantOctree.c @@ -1,538 +1,538 @@ -/* Copyright (c) 2010 Oliver Tonnhofer , Omniscale -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -*/ - -/* -// This file implements a variation of the octree color quantization algorithm. -*/ - -#include -#include -#include -#include - -#include "ImagingUtils.h" -#include "QuantOctree.h" - -typedef struct _ColorBucket { - /* contains palette index when used for look up cube */ - uint32_t count; - uint64_t r; - uint64_t g; - uint64_t b; - uint64_t a; -} *ColorBucket; - -typedef struct _ColorCube { - unsigned int rBits, gBits, bBits, aBits; - unsigned int rWidth, gWidth, bWidth, aWidth; - unsigned int rOffset, gOffset, bOffset, aOffset; - - unsigned long size; - ColorBucket buckets; -} *ColorCube; - -#define MAX(a, b) (a) > (b) ? (a) : (b) - -static ColorCube -new_color_cube(int r, int g, int b, int a) { - ColorCube cube; - - /* malloc check ok, small constant allocation */ - cube = malloc(sizeof(struct _ColorCube)); - if (!cube) { - return NULL; - } - - cube->rBits = MAX(r, 0); - cube->gBits = MAX(g, 0); - cube->bBits = MAX(b, 0); - cube->aBits = MAX(a, 0); - - /* overflow check for size multiplication below */ - if (cube->rBits + cube->gBits + cube->bBits + cube->aBits > 31) { - free(cube); - return NULL; - } - - /* the width of the cube for each dimension */ - cube->rWidth = 1 << cube->rBits; - cube->gWidth = 1 << cube->gBits; - cube->bWidth = 1 << cube->bBits; - cube->aWidth = 1 << cube->aBits; - - /* the offsets of each color */ - - cube->rOffset = cube->gBits + cube->bBits + cube->aBits; - cube->gOffset = cube->bBits + cube->aBits; - cube->bOffset = cube->aBits; - cube->aOffset = 0; - - /* the number of color buckets */ - cube->size = cube->rWidth * cube->gWidth * cube->bWidth * cube->aWidth; - /* malloc check ok, overflow checked above */ - cube->buckets = calloc(cube->size, sizeof(struct _ColorBucket)); - - if (!cube->buckets) { - free(cube); - return NULL; - } - return cube; -} - -static void -free_color_cube(ColorCube cube) { - if (cube != NULL) { - free(cube->buckets); - free(cube); - } -} - -static long -color_bucket_offset_pos( - const ColorCube cube, - unsigned int r, - unsigned int g, - unsigned int b, - unsigned int a) { - return r << cube->rOffset | g << cube->gOffset | b << cube->bOffset | - a << cube->aOffset; -} - -static long -color_bucket_offset(const ColorCube cube, const Pixel *p) { - unsigned int r = p->c.r >> (8 - cube->rBits); - unsigned int g = p->c.g >> (8 - cube->gBits); - unsigned int b = p->c.b >> (8 - cube->bBits); - unsigned int a = p->c.a >> (8 - cube->aBits); - return color_bucket_offset_pos(cube, r, g, b, a); -} - -static ColorBucket -color_bucket_from_cube(const ColorCube cube, const Pixel *p) { - unsigned int offset = color_bucket_offset(cube, p); - return &cube->buckets[offset]; -} - -static void -add_color_to_color_cube(const ColorCube cube, const Pixel *p) { - ColorBucket bucket = color_bucket_from_cube(cube, p); - bucket->count += 1; - bucket->r += p->c.r; - bucket->g += p->c.g; - bucket->b += p->c.b; - bucket->a += p->c.a; -} - -static unsigned long -count_used_color_buckets(const ColorCube cube) { - unsigned long usedBuckets = 0; - unsigned long i; - for (i = 0; i < cube->size; i++) { - if (cube->buckets[i].count > 0) { - usedBuckets += 1; - } - } - return usedBuckets; -} - -static void -avg_color_from_color_bucket(const ColorBucket bucket, Pixel *dst) { - float count = bucket->count; - if (count != 0) { - dst->c.r = CLIP8((int)(bucket->r / count)); - dst->c.g = CLIP8((int)(bucket->g / count)); - dst->c.b = CLIP8((int)(bucket->b / count)); - dst->c.a = CLIP8((int)(bucket->a / count)); - } else { - dst->c.r = 0; - dst->c.g = 0; - dst->c.b = 0; - dst->c.a = 0; - } -} - -static int -compare_bucket_count(const ColorBucket a, const ColorBucket b) { - return b->count - a->count; -} - -static ColorBucket -create_sorted_color_palette(const ColorCube cube) { - ColorBucket buckets; - if (cube->size > LONG_MAX / sizeof(struct _ColorBucket)) { - return NULL; - } - /* malloc check ok, calloc + overflow check above for memcpy */ - buckets = calloc(cube->size, sizeof(struct _ColorBucket)); - if (!buckets) { - return NULL; - } - memcpy(buckets, cube->buckets, sizeof(struct _ColorBucket) * cube->size); - - qsort( - buckets, - cube->size, - sizeof(struct _ColorBucket), - (int (*)(void const *, void const *)) & compare_bucket_count); - - return buckets; -} - -void -add_bucket_values(ColorBucket src, ColorBucket dst) { - dst->count += src->count; - dst->r += src->r; - dst->g += src->g; - dst->b += src->b; - dst->a += src->a; -} - -/* expand or shrink a given cube to level */ -static ColorCube -copy_color_cube( - const ColorCube cube, - unsigned int rBits, - unsigned int gBits, - unsigned int bBits, - unsigned int aBits) { - unsigned int r, g, b, a; - long src_pos, dst_pos; - unsigned int src_reduce[4] = {0}, dst_reduce[4] = {0}; - unsigned int width[4]; - ColorCube result; - - result = new_color_cube(rBits, gBits, bBits, aBits); - if (!result) { - return NULL; - } - - if (cube->rBits > rBits) { - dst_reduce[0] = cube->rBits - result->rBits; - width[0] = cube->rWidth; - } else { - src_reduce[0] = result->rBits - cube->rBits; - width[0] = result->rWidth; - } - if (cube->gBits > gBits) { - dst_reduce[1] = cube->gBits - result->gBits; - width[1] = cube->gWidth; - } else { - src_reduce[1] = result->gBits - cube->gBits; - width[1] = result->gWidth; - } - if (cube->bBits > bBits) { - dst_reduce[2] = cube->bBits - result->bBits; - width[2] = cube->bWidth; - } else { - src_reduce[2] = result->bBits - cube->bBits; - width[2] = result->bWidth; - } - if (cube->aBits > aBits) { - dst_reduce[3] = cube->aBits - result->aBits; - width[3] = cube->aWidth; - } else { - src_reduce[3] = result->aBits - cube->aBits; - width[3] = result->aWidth; - } - - for (r = 0; r < width[0]; r++) { - for (g = 0; g < width[1]; g++) { - for (b = 0; b < width[2]; b++) { - for (a = 0; a < width[3]; a++) { - src_pos = color_bucket_offset_pos( - cube, - r >> src_reduce[0], - g >> src_reduce[1], - b >> src_reduce[2], - a >> src_reduce[3]); - dst_pos = color_bucket_offset_pos( - result, - r >> dst_reduce[0], - g >> dst_reduce[1], - b >> dst_reduce[2], - a >> dst_reduce[3]); - add_bucket_values( - &cube->buckets[src_pos], &result->buckets[dst_pos]); - } - } - } - } - return result; -} - -void -subtract_color_buckets(ColorCube cube, ColorBucket buckets, long nBuckets) { - ColorBucket minuend, subtrahend; - long i; - Pixel p; - for (i = 0; i < nBuckets; i++) { - subtrahend = &buckets[i]; - - // If the subtrahend contains no buckets, there is nothing to subtract. - if (subtrahend->count == 0) { - continue; - } - - avg_color_from_color_bucket(subtrahend, &p); - minuend = color_bucket_from_cube(cube, &p); - minuend->count -= subtrahend->count; - minuend->r -= subtrahend->r; - minuend->g -= subtrahend->g; - minuend->b -= subtrahend->b; - minuend->a -= subtrahend->a; - } -} - -static void -set_lookup_value(const ColorCube cube, const Pixel *p, long value) { - ColorBucket bucket = color_bucket_from_cube(cube, p); - bucket->count = value; -} - -uint64_t -lookup_color(const ColorCube cube, const Pixel *p) { - ColorBucket bucket = color_bucket_from_cube(cube, p); - return bucket->count; -} - -void -add_lookup_buckets(ColorCube cube, ColorBucket palette, long nColors, long offset) { - long i; - Pixel p; - for (i = offset + nColors - 1; i >= offset; i--) { - avg_color_from_color_bucket(&palette[i], &p); - set_lookup_value(cube, &p, i); - } -} - -ColorBucket -combined_palette( - ColorBucket bucketsA, - unsigned long nBucketsA, - ColorBucket bucketsB, - unsigned long nBucketsB) { - ColorBucket result; - if (nBucketsA > LONG_MAX - nBucketsB || - (nBucketsA + nBucketsB) > LONG_MAX / sizeof(struct _ColorBucket)) { - return NULL; - } - /* malloc check ok, overflow check above */ - result = calloc(nBucketsA + nBucketsB, sizeof(struct _ColorBucket)); - if (!result) { - return NULL; - } - memcpy(result, bucketsA, sizeof(struct _ColorBucket) * nBucketsA); - memcpy(&result[nBucketsA], bucketsB, sizeof(struct _ColorBucket) * nBucketsB); - return result; -} - -static Pixel * -create_palette_array(const ColorBucket palette, unsigned int paletteLength) { - Pixel *paletteArray; - unsigned int i; - - /* malloc check ok, calloc for overflow */ - paletteArray = calloc(paletteLength, sizeof(Pixel)); - if (!paletteArray) { - return NULL; - } - - for (i = 0; i < paletteLength; i++) { - avg_color_from_color_bucket(&palette[i], &paletteArray[i]); - } - return paletteArray; -} - -static void -map_image_pixels( - const Pixel *pixelData, - uint32_t nPixels, - const ColorCube lookupCube, - uint32_t *pixelArray) { - long i; - for (i = 0; i < nPixels; i++) { - pixelArray[i] = lookup_color(lookupCube, &pixelData[i]); - } -} - -const unsigned int CUBE_LEVELS[8] = {4, 4, 4, 0, 2, 2, 2, 0}; -const unsigned int CUBE_LEVELS_ALPHA[8] = {3, 4, 3, 3, 2, 2, 2, 2}; - -int -quantize_octree( - Pixel *pixelData, - uint32_t nPixels, - uint32_t nQuantPixels, - Pixel **palette, - uint32_t *paletteLength, - uint32_t **quantizedPixels, - int withAlpha) { - ColorCube fineCube = NULL; - ColorCube coarseCube = NULL; - ColorCube lookupCube = NULL; - ColorCube coarseLookupCube = NULL; - ColorBucket paletteBucketsCoarse = NULL; - ColorBucket paletteBucketsFine = NULL; - ColorBucket paletteBuckets = NULL; - uint32_t *qp = NULL; - long i; - unsigned long nCoarseColors, nFineColors, nAlreadySubtracted; - const unsigned int *cubeBits; - - if (withAlpha) { - cubeBits = CUBE_LEVELS_ALPHA; - } else { - cubeBits = CUBE_LEVELS; - } - - /* - Create two color cubes, one fine grained with 8x16x8=1024 - colors buckets and a coarse with 4x4x4=64 color buckets. - The coarse one guarantees that there are color buckets available for - the whole color range (assuming nQuantPixels > 64). - - For a quantization to 256 colors all 64 coarse colors will be used - plus the 192 most used color buckets from the fine color cube. - The average of all colors within one bucket is used as the actual - color for that bucket. - - For images with alpha the cubes gets a forth dimension, - 8x16x8x8 and 4x4x4x4. - */ - - /* create fine cube */ - fineCube = new_color_cube(cubeBits[0], cubeBits[1], cubeBits[2], cubeBits[3]); - if (!fineCube) { - goto error; - } - for (i = 0; i < nPixels; i++) { - add_color_to_color_cube(fineCube, &pixelData[i]); - } - - /* create coarse cube */ - coarseCube = - copy_color_cube(fineCube, cubeBits[4], cubeBits[5], cubeBits[6], cubeBits[7]); - if (!coarseCube) { - goto error; - } - nCoarseColors = count_used_color_buckets(coarseCube); - - /* limit to nQuantPixels */ - if (nCoarseColors > nQuantPixels) { - nCoarseColors = nQuantPixels; - } - - /* how many space do we have in our palette for fine colors? */ - nFineColors = nQuantPixels - nCoarseColors; - - /* create fine color palette */ - paletteBucketsFine = create_sorted_color_palette(fineCube); - if (!paletteBucketsFine) { - goto error; - } - - /* remove the used fine colors from the coarse cube */ - subtract_color_buckets(coarseCube, paletteBucketsFine, nFineColors); - - /* did the subtraction cleared one or more coarse bucket? */ - while (nCoarseColors > count_used_color_buckets(coarseCube)) { - /* then we can use the free buckets for fine colors */ - nAlreadySubtracted = nFineColors; - nCoarseColors = count_used_color_buckets(coarseCube); - nFineColors = nQuantPixels - nCoarseColors; - subtract_color_buckets( - coarseCube, - &paletteBucketsFine[nAlreadySubtracted], - nFineColors - nAlreadySubtracted); - } - - /* create our palette buckets with fine and coarse combined */ - paletteBucketsCoarse = create_sorted_color_palette(coarseCube); - if (!paletteBucketsCoarse) { - goto error; - } - paletteBuckets = combined_palette( - paletteBucketsCoarse, nCoarseColors, paletteBucketsFine, nFineColors); - - free(paletteBucketsFine); - paletteBucketsFine = NULL; - free(paletteBucketsCoarse); - paletteBucketsCoarse = NULL; - if (!paletteBuckets) { - goto error; - } - - /* add all coarse colors to our coarse lookup cube. */ - coarseLookupCube = - new_color_cube(cubeBits[4], cubeBits[5], cubeBits[6], cubeBits[7]); - if (!coarseLookupCube) { - goto error; - } - add_lookup_buckets(coarseLookupCube, paletteBuckets, nCoarseColors, 0); - - /* expand coarse cube (64) to larger fine cube (4k). the value of each - coarse bucket is then present in the according 64 fine buckets. */ - lookupCube = copy_color_cube( - coarseLookupCube, cubeBits[0], cubeBits[1], cubeBits[2], cubeBits[3]); - if (!lookupCube) { - goto error; - } - - /* add fine colors to the lookup cube */ - add_lookup_buckets(lookupCube, paletteBuckets, nFineColors, nCoarseColors); - - /* create result pixels and map palette indices */ - /* malloc check ok, calloc for overflow */ - qp = calloc(nPixels, sizeof(Pixel)); - if (!qp) { - goto error; - } - map_image_pixels(pixelData, nPixels, lookupCube, qp); - - /* convert palette buckets to RGB pixel palette */ - *palette = create_palette_array(paletteBuckets, nQuantPixels); - if (!(*palette)) { - goto error; - } - - *quantizedPixels = qp; - *paletteLength = nQuantPixels; - - free_color_cube(coarseCube); - free_color_cube(fineCube); - free_color_cube(lookupCube); - free_color_cube(coarseLookupCube); - free(paletteBuckets); - return 1; - -error: - /* everything is initialized to NULL - so we are safe to call free */ - free(qp); - free_color_cube(lookupCube); - free_color_cube(coarseLookupCube); - free(paletteBuckets); - free(paletteBucketsCoarse); - free(paletteBucketsFine); - free_color_cube(coarseCube); - free_color_cube(fineCube); - return 0; -} +/* Copyright (c) 2010 Oliver Tonnhofer , Omniscale +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +*/ + +/* +// This file implements a variation of the octree color quantization algorithm. +*/ + +#include +#include +#include +#include + +#include "ImagingUtils.h" +#include "QuantOctree.h" + +typedef struct _ColorBucket { + /* contains palette index when used for look up cube */ + uint32_t count; + uint64_t r; + uint64_t g; + uint64_t b; + uint64_t a; +} *ColorBucket; + +typedef struct _ColorCube { + unsigned int rBits, gBits, bBits, aBits; + unsigned int rWidth, gWidth, bWidth, aWidth; + unsigned int rOffset, gOffset, bOffset, aOffset; + + unsigned long size; + ColorBucket buckets; +} *ColorCube; + +#define MAX(a, b) (a) > (b) ? (a) : (b) + +static ColorCube +new_color_cube(int r, int g, int b, int a) { + ColorCube cube; + + /* malloc check ok, small constant allocation */ + cube = malloc(sizeof(struct _ColorCube)); + if (!cube) { + return NULL; + } + + cube->rBits = MAX(r, 0); + cube->gBits = MAX(g, 0); + cube->bBits = MAX(b, 0); + cube->aBits = MAX(a, 0); + + /* overflow check for size multiplication below */ + if (cube->rBits + cube->gBits + cube->bBits + cube->aBits > 31) { + free(cube); + return NULL; + } + + /* the width of the cube for each dimension */ + cube->rWidth = 1 << cube->rBits; + cube->gWidth = 1 << cube->gBits; + cube->bWidth = 1 << cube->bBits; + cube->aWidth = 1 << cube->aBits; + + /* the offsets of each color */ + + cube->rOffset = cube->gBits + cube->bBits + cube->aBits; + cube->gOffset = cube->bBits + cube->aBits; + cube->bOffset = cube->aBits; + cube->aOffset = 0; + + /* the number of color buckets */ + cube->size = cube->rWidth * cube->gWidth * cube->bWidth * cube->aWidth; + /* malloc check ok, overflow checked above */ + cube->buckets = calloc(cube->size, sizeof(struct _ColorBucket)); + + if (!cube->buckets) { + free(cube); + return NULL; + } + return cube; +} + +static void +free_color_cube(ColorCube cube) { + if (cube != NULL) { + free(cube->buckets); + free(cube); + } +} + +static long +color_bucket_offset_pos( + const ColorCube cube, + unsigned int r, + unsigned int g, + unsigned int b, + unsigned int a) { + return r << cube->rOffset | g << cube->gOffset | b << cube->bOffset | + a << cube->aOffset; +} + +static long +color_bucket_offset(const ColorCube cube, const Pixel *p) { + unsigned int r = p->c.r >> (8 - cube->rBits); + unsigned int g = p->c.g >> (8 - cube->gBits); + unsigned int b = p->c.b >> (8 - cube->bBits); + unsigned int a = p->c.a >> (8 - cube->aBits); + return color_bucket_offset_pos(cube, r, g, b, a); +} + +static ColorBucket +color_bucket_from_cube(const ColorCube cube, const Pixel *p) { + unsigned int offset = color_bucket_offset(cube, p); + return &cube->buckets[offset]; +} + +static void +add_color_to_color_cube(const ColorCube cube, const Pixel *p) { + ColorBucket bucket = color_bucket_from_cube(cube, p); + bucket->count += 1; + bucket->r += p->c.r; + bucket->g += p->c.g; + bucket->b += p->c.b; + bucket->a += p->c.a; +} + +static unsigned long +count_used_color_buckets(const ColorCube cube) { + unsigned long usedBuckets = 0; + unsigned long i; + for (i = 0; i < cube->size; i++) { + if (cube->buckets[i].count > 0) { + usedBuckets += 1; + } + } + return usedBuckets; +} + +static void +avg_color_from_color_bucket(const ColorBucket bucket, Pixel *dst) { + float count = bucket->count; + if (count != 0) { + dst->c.r = CLIP8((int)(bucket->r / count)); + dst->c.g = CLIP8((int)(bucket->g / count)); + dst->c.b = CLIP8((int)(bucket->b / count)); + dst->c.a = CLIP8((int)(bucket->a / count)); + } else { + dst->c.r = 0; + dst->c.g = 0; + dst->c.b = 0; + dst->c.a = 0; + } +} + +static int +compare_bucket_count(const ColorBucket a, const ColorBucket b) { + return b->count - a->count; +} + +static ColorBucket +create_sorted_color_palette(const ColorCube cube) { + ColorBucket buckets; + if (cube->size > LONG_MAX / sizeof(struct _ColorBucket)) { + return NULL; + } + /* malloc check ok, calloc + overflow check above for memcpy */ + buckets = calloc(cube->size, sizeof(struct _ColorBucket)); + if (!buckets) { + return NULL; + } + memcpy(buckets, cube->buckets, sizeof(struct _ColorBucket) * cube->size); + + qsort( + buckets, + cube->size, + sizeof(struct _ColorBucket), + (int (*)(void const *, void const *)) & compare_bucket_count); + + return buckets; +} + +void +add_bucket_values(ColorBucket src, ColorBucket dst) { + dst->count += src->count; + dst->r += src->r; + dst->g += src->g; + dst->b += src->b; + dst->a += src->a; +} + +/* expand or shrink a given cube to level */ +static ColorCube +copy_color_cube( + const ColorCube cube, + unsigned int rBits, + unsigned int gBits, + unsigned int bBits, + unsigned int aBits) { + unsigned int r, g, b, a; + long src_pos, dst_pos; + unsigned int src_reduce[4] = {0}, dst_reduce[4] = {0}; + unsigned int width[4]; + ColorCube result; + + result = new_color_cube(rBits, gBits, bBits, aBits); + if (!result) { + return NULL; + } + + if (cube->rBits > rBits) { + dst_reduce[0] = cube->rBits - result->rBits; + width[0] = cube->rWidth; + } else { + src_reduce[0] = result->rBits - cube->rBits; + width[0] = result->rWidth; + } + if (cube->gBits > gBits) { + dst_reduce[1] = cube->gBits - result->gBits; + width[1] = cube->gWidth; + } else { + src_reduce[1] = result->gBits - cube->gBits; + width[1] = result->gWidth; + } + if (cube->bBits > bBits) { + dst_reduce[2] = cube->bBits - result->bBits; + width[2] = cube->bWidth; + } else { + src_reduce[2] = result->bBits - cube->bBits; + width[2] = result->bWidth; + } + if (cube->aBits > aBits) { + dst_reduce[3] = cube->aBits - result->aBits; + width[3] = cube->aWidth; + } else { + src_reduce[3] = result->aBits - cube->aBits; + width[3] = result->aWidth; + } + + for (r = 0; r < width[0]; r++) { + for (g = 0; g < width[1]; g++) { + for (b = 0; b < width[2]; b++) { + for (a = 0; a < width[3]; a++) { + src_pos = color_bucket_offset_pos( + cube, + r >> src_reduce[0], + g >> src_reduce[1], + b >> src_reduce[2], + a >> src_reduce[3]); + dst_pos = color_bucket_offset_pos( + result, + r >> dst_reduce[0], + g >> dst_reduce[1], + b >> dst_reduce[2], + a >> dst_reduce[3]); + add_bucket_values( + &cube->buckets[src_pos], &result->buckets[dst_pos]); + } + } + } + } + return result; +} + +void +subtract_color_buckets(ColorCube cube, ColorBucket buckets, long nBuckets) { + ColorBucket minuend, subtrahend; + long i; + Pixel p; + for (i = 0; i < nBuckets; i++) { + subtrahend = &buckets[i]; + + // If the subtrahend contains no buckets, there is nothing to subtract. + if (subtrahend->count == 0) { + continue; + } + + avg_color_from_color_bucket(subtrahend, &p); + minuend = color_bucket_from_cube(cube, &p); + minuend->count -= subtrahend->count; + minuend->r -= subtrahend->r; + minuend->g -= subtrahend->g; + minuend->b -= subtrahend->b; + minuend->a -= subtrahend->a; + } +} + +static void +set_lookup_value(const ColorCube cube, const Pixel *p, long value) { + ColorBucket bucket = color_bucket_from_cube(cube, p); + bucket->count = value; +} + +uint64_t +lookup_color(const ColorCube cube, const Pixel *p) { + ColorBucket bucket = color_bucket_from_cube(cube, p); + return bucket->count; +} + +void +add_lookup_buckets(ColorCube cube, ColorBucket palette, long nColors, long offset) { + long i; + Pixel p; + for (i = offset + nColors - 1; i >= offset; i--) { + avg_color_from_color_bucket(&palette[i], &p); + set_lookup_value(cube, &p, i); + } +} + +ColorBucket +combined_palette( + ColorBucket bucketsA, + unsigned long nBucketsA, + ColorBucket bucketsB, + unsigned long nBucketsB) { + ColorBucket result; + if (nBucketsA > LONG_MAX - nBucketsB || + (nBucketsA + nBucketsB) > LONG_MAX / sizeof(struct _ColorBucket)) { + return NULL; + } + /* malloc check ok, overflow check above */ + result = calloc(nBucketsA + nBucketsB, sizeof(struct _ColorBucket)); + if (!result) { + return NULL; + } + memcpy(result, bucketsA, sizeof(struct _ColorBucket) * nBucketsA); + memcpy(&result[nBucketsA], bucketsB, sizeof(struct _ColorBucket) * nBucketsB); + return result; +} + +static Pixel * +create_palette_array(const ColorBucket palette, unsigned int paletteLength) { + Pixel *paletteArray; + unsigned int i; + + /* malloc check ok, calloc for overflow */ + paletteArray = calloc(paletteLength, sizeof(Pixel)); + if (!paletteArray) { + return NULL; + } + + for (i = 0; i < paletteLength; i++) { + avg_color_from_color_bucket(&palette[i], &paletteArray[i]); + } + return paletteArray; +} + +static void +map_image_pixels( + const Pixel *pixelData, + uint32_t nPixels, + const ColorCube lookupCube, + uint32_t *pixelArray) { + long i; + for (i = 0; i < nPixels; i++) { + pixelArray[i] = lookup_color(lookupCube, &pixelData[i]); + } +} + +const unsigned int CUBE_LEVELS[8] = {4, 4, 4, 0, 2, 2, 2, 0}; +const unsigned int CUBE_LEVELS_ALPHA[8] = {3, 4, 3, 3, 2, 2, 2, 2}; + +int +quantize_octree( + Pixel *pixelData, + uint32_t nPixels, + uint32_t nQuantPixels, + Pixel **palette, + uint32_t *paletteLength, + uint32_t **quantizedPixels, + int withAlpha) { + ColorCube fineCube = NULL; + ColorCube coarseCube = NULL; + ColorCube lookupCube = NULL; + ColorCube coarseLookupCube = NULL; + ColorBucket paletteBucketsCoarse = NULL; + ColorBucket paletteBucketsFine = NULL; + ColorBucket paletteBuckets = NULL; + uint32_t *qp = NULL; + long i; + unsigned long nCoarseColors, nFineColors, nAlreadySubtracted; + const unsigned int *cubeBits; + + if (withAlpha) { + cubeBits = CUBE_LEVELS_ALPHA; + } else { + cubeBits = CUBE_LEVELS; + } + + /* + Create two color cubes, one fine grained with 8x16x8=1024 + colors buckets and a coarse with 4x4x4=64 color buckets. + The coarse one guarantees that there are color buckets available for + the whole color range (assuming nQuantPixels > 64). + + For a quantization to 256 colors all 64 coarse colors will be used + plus the 192 most used color buckets from the fine color cube. + The average of all colors within one bucket is used as the actual + color for that bucket. + + For images with alpha the cubes gets a forth dimension, + 8x16x8x8 and 4x4x4x4. + */ + + /* create fine cube */ + fineCube = new_color_cube(cubeBits[0], cubeBits[1], cubeBits[2], cubeBits[3]); + if (!fineCube) { + goto error; + } + for (i = 0; i < nPixels; i++) { + add_color_to_color_cube(fineCube, &pixelData[i]); + } + + /* create coarse cube */ + coarseCube = + copy_color_cube(fineCube, cubeBits[4], cubeBits[5], cubeBits[6], cubeBits[7]); + if (!coarseCube) { + goto error; + } + nCoarseColors = count_used_color_buckets(coarseCube); + + /* limit to nQuantPixels */ + if (nCoarseColors > nQuantPixels) { + nCoarseColors = nQuantPixels; + } + + /* how many space do we have in our palette for fine colors? */ + nFineColors = nQuantPixels - nCoarseColors; + + /* create fine color palette */ + paletteBucketsFine = create_sorted_color_palette(fineCube); + if (!paletteBucketsFine) { + goto error; + } + + /* remove the used fine colors from the coarse cube */ + subtract_color_buckets(coarseCube, paletteBucketsFine, nFineColors); + + /* did the subtraction cleared one or more coarse bucket? */ + while (nCoarseColors > count_used_color_buckets(coarseCube)) { + /* then we can use the free buckets for fine colors */ + nAlreadySubtracted = nFineColors; + nCoarseColors = count_used_color_buckets(coarseCube); + nFineColors = nQuantPixels - nCoarseColors; + subtract_color_buckets( + coarseCube, + &paletteBucketsFine[nAlreadySubtracted], + nFineColors - nAlreadySubtracted); + } + + /* create our palette buckets with fine and coarse combined */ + paletteBucketsCoarse = create_sorted_color_palette(coarseCube); + if (!paletteBucketsCoarse) { + goto error; + } + paletteBuckets = combined_palette( + paletteBucketsCoarse, nCoarseColors, paletteBucketsFine, nFineColors); + + free(paletteBucketsFine); + paletteBucketsFine = NULL; + free(paletteBucketsCoarse); + paletteBucketsCoarse = NULL; + if (!paletteBuckets) { + goto error; + } + + /* add all coarse colors to our coarse lookup cube. */ + coarseLookupCube = + new_color_cube(cubeBits[4], cubeBits[5], cubeBits[6], cubeBits[7]); + if (!coarseLookupCube) { + goto error; + } + add_lookup_buckets(coarseLookupCube, paletteBuckets, nCoarseColors, 0); + + /* expand coarse cube (64) to larger fine cube (4k). the value of each + coarse bucket is then present in the according 64 fine buckets. */ + lookupCube = copy_color_cube( + coarseLookupCube, cubeBits[0], cubeBits[1], cubeBits[2], cubeBits[3]); + if (!lookupCube) { + goto error; + } + + /* add fine colors to the lookup cube */ + add_lookup_buckets(lookupCube, paletteBuckets, nFineColors, nCoarseColors); + + /* create result pixels and map palette indices */ + /* malloc check ok, calloc for overflow */ + qp = calloc(nPixels, sizeof(Pixel)); + if (!qp) { + goto error; + } + map_image_pixels(pixelData, nPixels, lookupCube, qp); + + /* convert palette buckets to RGB pixel palette */ + *palette = create_palette_array(paletteBuckets, nQuantPixels); + if (!(*palette)) { + goto error; + } + + *quantizedPixels = qp; + *paletteLength = nQuantPixels; + + free_color_cube(coarseCube); + free_color_cube(fineCube); + free_color_cube(lookupCube); + free_color_cube(coarseLookupCube); + free(paletteBuckets); + return 1; + +error: + /* everything is initialized to NULL + so we are safe to call free */ + free(qp); + free_color_cube(lookupCube); + free_color_cube(coarseLookupCube); + free(paletteBuckets); + free(paletteBucketsCoarse); + free(paletteBucketsFine); + free_color_cube(coarseCube); + free_color_cube(fineCube); + return 0; +} diff --git a/src/libImaging/QuantOctree.h b/src/libImaging/QuantOctree.h index e1c504074..b19ca7bfc 100644 --- a/src/libImaging/QuantOctree.h +++ b/src/libImaging/QuantOctree.h @@ -1,9 +1,9 @@ -#ifndef __QUANT_OCTREE_H__ -#define __QUANT_OCTREE_H__ - -#include "QuantTypes.h" - -int -quantize_octree(Pixel *, uint32_t, uint32_t, Pixel **, uint32_t *, uint32_t **, int); - -#endif +#ifndef __QUANT_OCTREE_H__ +#define __QUANT_OCTREE_H__ + +#include "QuantTypes.h" + +int +quantize_octree(Pixel *, uint32_t, uint32_t, Pixel **, uint32_t *, uint32_t **, int); + +#endif diff --git a/src/libImaging/QuantPngQuant.c b/src/libImaging/QuantPngQuant.c index 7a36300e4..9e88a75fd 100644 --- a/src/libImaging/QuantPngQuant.c +++ b/src/libImaging/QuantPngQuant.c @@ -1,132 +1,132 @@ -/* - * The Python Imaging Library - * $Id$ - * - * quantization using libimagequant, a part of pngquant. - * - * Copyright (c) 2016 Marcin Kurczewski - * - */ - -#include -#include -#include - -#include "QuantPngQuant.h" - -#ifdef HAVE_LIBIMAGEQUANT -#include "libimagequant.h" - -int -quantize_pngquant( - Pixel *pixelData, - unsigned int width, - unsigned int height, - uint32_t quantPixels, - Pixel **palette, - uint32_t *paletteLength, - uint32_t **quantizedPixels, - int withAlpha) { - int result = 0; - liq_image *image = NULL; - liq_attr *attr = NULL; - liq_result *remap = NULL; - unsigned char *charMatrix = NULL; - unsigned char **charMatrixRows = NULL; - unsigned int i, y; - *palette = NULL; - *paletteLength = 0; - *quantizedPixels = NULL; - - /* configure pngquant */ - attr = liq_attr_create(); - if (!attr) { - goto err; - } - if (quantPixels) { - liq_set_max_colors(attr, quantPixels); - } - - /* prepare input image */ - image = liq_image_create_rgba(attr, pixelData, width, height, 0.45455 /* gamma */); - if (!image) { - goto err; - } - - /* quantize the image */ - remap = liq_quantize_image(attr, image); - if (!remap) { - goto err; - } - liq_set_output_gamma(remap, 0.45455); - liq_set_dithering_level(remap, 1); - - /* write output palette */ - const liq_palette *l_palette = liq_get_palette(remap); - *paletteLength = l_palette->count; - *palette = malloc(sizeof(Pixel) * l_palette->count); - if (!*palette) { - goto err; - } - for (i = 0; i < l_palette->count; i++) { - (*palette)[i].c.b = l_palette->entries[i].b; - (*palette)[i].c.g = l_palette->entries[i].g; - (*palette)[i].c.r = l_palette->entries[i].r; - (*palette)[i].c.a = l_palette->entries[i].a; - } - - /* write output pixels (pngquant uses char array) */ - charMatrix = malloc(width * height); - if (!charMatrix) { - goto err; - } - charMatrixRows = malloc(height * sizeof(unsigned char *)); - if (!charMatrixRows) { - goto err; - } - for (y = 0; y < height; y++) { - charMatrixRows[y] = &charMatrix[y * width]; - } - if (LIQ_OK != liq_write_remapped_image_rows(remap, image, charMatrixRows)) { - goto err; - } - - /* transcribe output pixels (pillow uses uint32_t array) */ - *quantizedPixels = malloc(sizeof(uint32_t) * width * height); - if (!*quantizedPixels) { - goto err; - } - for (i = 0; i < width * height; i++) { - (*quantizedPixels)[i] = charMatrix[i]; - } - - result = 1; - -err: - if (attr) { - liq_attr_destroy(attr); - } - if (image) { - liq_image_destroy(image); - } - if (remap) { - liq_result_destroy(remap); - } - free(charMatrix); - free(charMatrixRows); - if (!result) { - free(*quantizedPixels); - free(*palette); - } - return result; -} - -const char * -ImagingImageQuantVersion(void) { - static char version[20]; - int number = liq_version(); - sprintf(version, "%d.%d.%d", number / 10000, (number / 100) % 100, number % 100); - return version; -} - -#endif +/* + * The Python Imaging Library + * $Id$ + * + * quantization using libimagequant, a part of pngquant. + * + * Copyright (c) 2016 Marcin Kurczewski + * + */ + +#include +#include +#include + +#include "QuantPngQuant.h" + +#ifdef HAVE_LIBIMAGEQUANT +#include "libimagequant.h" + +int +quantize_pngquant( + Pixel *pixelData, + unsigned int width, + unsigned int height, + uint32_t quantPixels, + Pixel **palette, + uint32_t *paletteLength, + uint32_t **quantizedPixels, + int withAlpha) { + int result = 0; + liq_image *image = NULL; + liq_attr *attr = NULL; + liq_result *remap = NULL; + unsigned char *charMatrix = NULL; + unsigned char **charMatrixRows = NULL; + unsigned int i, y; + *palette = NULL; + *paletteLength = 0; + *quantizedPixels = NULL; + + /* configure pngquant */ + attr = liq_attr_create(); + if (!attr) { + goto err; + } + if (quantPixels) { + liq_set_max_colors(attr, quantPixels); + } + + /* prepare input image */ + image = liq_image_create_rgba(attr, pixelData, width, height, 0.45455 /* gamma */); + if (!image) { + goto err; + } + + /* quantize the image */ + remap = liq_quantize_image(attr, image); + if (!remap) { + goto err; + } + liq_set_output_gamma(remap, 0.45455); + liq_set_dithering_level(remap, 1); + + /* write output palette */ + const liq_palette *l_palette = liq_get_palette(remap); + *paletteLength = l_palette->count; + *palette = malloc(sizeof(Pixel) * l_palette->count); + if (!*palette) { + goto err; + } + for (i = 0; i < l_palette->count; i++) { + (*palette)[i].c.b = l_palette->entries[i].b; + (*palette)[i].c.g = l_palette->entries[i].g; + (*palette)[i].c.r = l_palette->entries[i].r; + (*palette)[i].c.a = l_palette->entries[i].a; + } + + /* write output pixels (pngquant uses char array) */ + charMatrix = malloc(width * height); + if (!charMatrix) { + goto err; + } + charMatrixRows = malloc(height * sizeof(unsigned char *)); + if (!charMatrixRows) { + goto err; + } + for (y = 0; y < height; y++) { + charMatrixRows[y] = &charMatrix[y * width]; + } + if (LIQ_OK != liq_write_remapped_image_rows(remap, image, charMatrixRows)) { + goto err; + } + + /* transcribe output pixels (pillow uses uint32_t array) */ + *quantizedPixels = malloc(sizeof(uint32_t) * width * height); + if (!*quantizedPixels) { + goto err; + } + for (i = 0; i < width * height; i++) { + (*quantizedPixels)[i] = charMatrix[i]; + } + + result = 1; + +err: + if (attr) { + liq_attr_destroy(attr); + } + if (image) { + liq_image_destroy(image); + } + if (remap) { + liq_result_destroy(remap); + } + free(charMatrix); + free(charMatrixRows); + if (!result) { + free(*quantizedPixels); + free(*palette); + } + return result; +} + +const char * +ImagingImageQuantVersion(void) { + static char version[20]; + int number = liq_version(); + sprintf(version, "%d.%d.%d", number / 10000, (number / 100) % 100, number % 100); + return version; +} + +#endif diff --git a/src/libImaging/QuantPngQuant.h b/src/libImaging/QuantPngQuant.h index d65e42590..f3bb1e545 100644 --- a/src/libImaging/QuantPngQuant.h +++ b/src/libImaging/QuantPngQuant.h @@ -1,17 +1,17 @@ -#ifndef __QUANT_PNGQUANT_H__ -#define __QUANT_PNGQUANT_H__ - -#include "QuantTypes.h" - -int -quantize_pngquant( - Pixel *, - unsigned int, - unsigned int, - uint32_t, - Pixel **, - uint32_t *, - uint32_t **, - int); - -#endif +#ifndef __QUANT_PNGQUANT_H__ +#define __QUANT_PNGQUANT_H__ + +#include "QuantTypes.h" + +int +quantize_pngquant( + Pixel *, + unsigned int, + unsigned int, + uint32_t, + Pixel **, + uint32_t *, + uint32_t **, + int); + +#endif diff --git a/src/libImaging/QuantTypes.h b/src/libImaging/QuantTypes.h index 986b70806..dd302d2d7 100644 --- a/src/libImaging/QuantTypes.h +++ b/src/libImaging/QuantTypes.h @@ -1,32 +1,32 @@ -/* - * The Python Imaging Library - * $Id$ - * - * image quantizer - * - * Written by Toby J Sargeant . - * - * See the README file for information on usage and redistribution. - */ - -#ifndef __TYPES_H__ -#define __TYPES_H__ - -#ifdef _MSC_VER -typedef unsigned __int32 uint32_t; -typedef unsigned __int64 uint64_t; -#else -#include -#endif - -typedef union { - struct { - unsigned char r, g, b, a; - } c; - struct { - unsigned char v[4]; - } a; - uint32_t v; -} Pixel; - -#endif +/* + * The Python Imaging Library + * $Id$ + * + * image quantizer + * + * Written by Toby J Sargeant . + * + * See the README file for information on usage and redistribution. + */ + +#ifndef __TYPES_H__ +#define __TYPES_H__ + +#ifdef _MSC_VER +typedef unsigned __int32 uint32_t; +typedef unsigned __int64 uint64_t; +#else +#include +#endif + +typedef union { + struct { + unsigned char r, g, b, a; + } c; + struct { + unsigned char v[4]; + } a; + uint32_t v; +} Pixel; + +#endif diff --git a/src/libImaging/RankFilter.c b/src/libImaging/RankFilter.c index 73a6baecb..9029b3692 100644 --- a/src/libImaging/RankFilter.c +++ b/src/libImaging/RankFilter.c @@ -1,132 +1,132 @@ -/* - * The Python Imaging Library - * $Id$ - * - * min, max, median filters - * - * history: - * 2002-06-08 fl Created - * - * Copyright (c) Secret Labs AB 2002. All rights reserved. - * - * See the README file for information on usage and redistribution. - */ - -#include "Imaging.h" - -/* Fast rank algorithm (due to Wirth), based on public domain code - by Nicolas Devillard, available at http://ndevilla.free.fr */ - -#define SWAP(type, a, b) \ - { \ - register type t = (a); \ - (a) = (b); \ - (b) = t; \ - } - -#define MakeRankFunction(type) \ - static type Rank##type(type a[], int n, int k) { \ - register int i, j, l, m; \ - register type x; \ - l = 0; \ - m = n - 1; \ - while (l < m) { \ - x = a[k]; \ - i = l; \ - j = m; \ - do { \ - while (a[i] < x) { \ - i++; \ - } \ - while (x < a[j]) { \ - j--; \ - } \ - if (i <= j) { \ - SWAP(type, a[i], a[j]); \ - i++; \ - j--; \ - } \ - } while (i <= j); \ - if (j < k) { \ - l = i; \ - } \ - if (k < i) { \ - m = j; \ - } \ - } \ - return a[k]; \ - } - -MakeRankFunction(UINT8) MakeRankFunction(INT32) MakeRankFunction(FLOAT32) - - Imaging ImagingRankFilter(Imaging im, int size, int rank) { - Imaging imOut = NULL; - int x, y; - int i, margin, size2; - - if (!im || im->bands != 1 || im->type == IMAGING_TYPE_SPECIAL) { - return (Imaging)ImagingError_ModeError(); - } - - if (!(size & 1)) { - return (Imaging)ImagingError_ValueError("bad filter size"); - } - - /* malloc check ok, for overflow in the define below */ - if (size > INT_MAX / size || size > INT_MAX / (size * (int)sizeof(FLOAT32))) { - return (Imaging)ImagingError_ValueError("filter size too large"); - } - - size2 = size * size; - margin = (size - 1) / 2; - - if (rank < 0 || rank >= size2) { - return (Imaging)ImagingError_ValueError("bad rank value"); - } - - imOut = ImagingNew(im->mode, im->xsize - 2 * margin, im->ysize - 2 * margin); - if (!imOut) { - return NULL; - } - - /* malloc check ok, checked above */ -#define RANK_BODY(type) \ - do { \ - type *buf = malloc(size2 * sizeof(type)); \ - if (!buf) { \ - goto nomemory; \ - } \ - for (y = 0; y < imOut->ysize; y++) { \ - for (x = 0; x < imOut->xsize; x++) { \ - for (i = 0; i < size; i++) { \ - memcpy( \ - buf + i * size, \ - &IMAGING_PIXEL_##type(im, x, y + i), \ - size * sizeof(type)); \ - } \ - IMAGING_PIXEL_##type(imOut, x, y) = Rank##type(buf, size2, rank); \ - } \ - } \ - free(buf); \ - } while (0) - - if (im->image8) { - RANK_BODY(UINT8); - } else if (im->type == IMAGING_TYPE_INT32) { - RANK_BODY(INT32); - } else if (im->type == IMAGING_TYPE_FLOAT32) { - RANK_BODY(FLOAT32); - } else { - /* safety net (we shouldn't end up here) */ - ImagingDelete(imOut); - return (Imaging)ImagingError_ModeError(); - } - - ImagingCopyPalette(imOut, im); - - return imOut; - -nomemory: - ImagingDelete(imOut); - return (Imaging)ImagingError_MemoryError(); -} +/* + * The Python Imaging Library + * $Id$ + * + * min, max, median filters + * + * history: + * 2002-06-08 fl Created + * + * Copyright (c) Secret Labs AB 2002. All rights reserved. + * + * See the README file for information on usage and redistribution. + */ + +#include "Imaging.h" + +/* Fast rank algorithm (due to Wirth), based on public domain code + by Nicolas Devillard, available at http://ndevilla.free.fr */ + +#define SWAP(type, a, b) \ + { \ + register type t = (a); \ + (a) = (b); \ + (b) = t; \ + } + +#define MakeRankFunction(type) \ + static type Rank##type(type a[], int n, int k) { \ + register int i, j, l, m; \ + register type x; \ + l = 0; \ + m = n - 1; \ + while (l < m) { \ + x = a[k]; \ + i = l; \ + j = m; \ + do { \ + while (a[i] < x) { \ + i++; \ + } \ + while (x < a[j]) { \ + j--; \ + } \ + if (i <= j) { \ + SWAP(type, a[i], a[j]); \ + i++; \ + j--; \ + } \ + } while (i <= j); \ + if (j < k) { \ + l = i; \ + } \ + if (k < i) { \ + m = j; \ + } \ + } \ + return a[k]; \ + } + +MakeRankFunction(UINT8) MakeRankFunction(INT32) MakeRankFunction(FLOAT32) + + Imaging ImagingRankFilter(Imaging im, int size, int rank) { + Imaging imOut = NULL; + int x, y; + int i, margin, size2; + + if (!im || im->bands != 1 || im->type == IMAGING_TYPE_SPECIAL) { + return (Imaging)ImagingError_ModeError(); + } + + if (!(size & 1)) { + return (Imaging)ImagingError_ValueError("bad filter size"); + } + + /* malloc check ok, for overflow in the define below */ + if (size > INT_MAX / size || size > INT_MAX / (size * (int)sizeof(FLOAT32))) { + return (Imaging)ImagingError_ValueError("filter size too large"); + } + + size2 = size * size; + margin = (size - 1) / 2; + + if (rank < 0 || rank >= size2) { + return (Imaging)ImagingError_ValueError("bad rank value"); + } + + imOut = ImagingNew(im->mode, im->xsize - 2 * margin, im->ysize - 2 * margin); + if (!imOut) { + return NULL; + } + + /* malloc check ok, checked above */ +#define RANK_BODY(type) \ + do { \ + type *buf = malloc(size2 * sizeof(type)); \ + if (!buf) { \ + goto nomemory; \ + } \ + for (y = 0; y < imOut->ysize; y++) { \ + for (x = 0; x < imOut->xsize; x++) { \ + for (i = 0; i < size; i++) { \ + memcpy( \ + buf + i * size, \ + &IMAGING_PIXEL_##type(im, x, y + i), \ + size * sizeof(type)); \ + } \ + IMAGING_PIXEL_##type(imOut, x, y) = Rank##type(buf, size2, rank); \ + } \ + } \ + free(buf); \ + } while (0) + + if (im->image8) { + RANK_BODY(UINT8); + } else if (im->type == IMAGING_TYPE_INT32) { + RANK_BODY(INT32); + } else if (im->type == IMAGING_TYPE_FLOAT32) { + RANK_BODY(FLOAT32); + } else { + /* safety net (we shouldn't end up here) */ + ImagingDelete(imOut); + return (Imaging)ImagingError_ModeError(); + } + + ImagingCopyPalette(imOut, im); + + return imOut; + +nomemory: + ImagingDelete(imOut); + return (Imaging)ImagingError_MemoryError(); +} diff --git a/src/libImaging/Raw.h b/src/libImaging/Raw.h index ab718837f..ea02dc4c1 100644 --- a/src/libImaging/Raw.h +++ b/src/libImaging/Raw.h @@ -1,14 +1,14 @@ -/* Raw.h */ - -typedef struct { - /* CONFIGURATION */ - - /* Distance between lines (0=no padding) */ - int stride; - - /* PRIVATE (initialized by decoder) */ - - /* Padding between lines */ - int skip; - -} RAWSTATE; +/* Raw.h */ + +typedef struct { + /* CONFIGURATION */ + + /* Distance between lines (0=no padding) */ + int stride; + + /* PRIVATE (initialized by decoder) */ + + /* Padding between lines */ + int skip; + +} RAWSTATE; diff --git a/src/libImaging/RawDecode.c b/src/libImaging/RawDecode.c index 24abe4804..17930ec7c 100644 --- a/src/libImaging/RawDecode.c +++ b/src/libImaging/RawDecode.c @@ -1,91 +1,91 @@ -/* - * The Python Imaging Library. - * $Id$ - * - * decoder for raw (uncompressed) image data - * - * history: - * 96-03-07 fl rewritten - * - * Copyright (c) Fredrik Lundh 1996. - * Copyright (c) Secret Labs AB 1997. - * - * See the README file for information on usage and redistribution. - */ - -#include "Imaging.h" - -#include "Raw.h" - -int -ImagingRawDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) { - enum { LINE = 1, SKIP }; - RAWSTATE *rawstate = state->context; - - UINT8 *ptr; - - if (state->state == 0) { - /* Initialize context variables */ - - /* get size of image data and padding */ - state->bytes = (state->xsize * state->bits + 7) / 8; - if (rawstate->stride) { - rawstate->skip = rawstate->stride - state->bytes; - if (rawstate->skip < 0) { - state->errcode = IMAGING_CODEC_CONFIG; - return -1; - } - } else { - rawstate->skip = 0; - } - - /* check image orientation */ - if (state->ystep < 0) { - state->y = state->ysize - 1; - state->ystep = -1; - } else { - state->ystep = 1; - } - - state->state = LINE; - } - - ptr = buf; - - for (;;) { - if (state->state == SKIP) { - /* Skip padding between lines */ - - if (bytes < rawstate->skip) { - return ptr - buf; - } - - ptr += rawstate->skip; - bytes -= rawstate->skip; - - state->state = LINE; - } - - if (bytes < state->bytes) { - return ptr - buf; - } - - /* Unpack data */ - state->shuffle( - (UINT8 *)im->image[state->y + state->yoff] + state->xoff * im->pixelsize, - ptr, - state->xsize); - - ptr += state->bytes; - bytes -= state->bytes; - - state->y += state->ystep; - - if (state->y < 0 || state->y >= state->ysize) { - /* End of file (errcode = 0) */ - return -1; - } - - state->state = SKIP; - } -} +/* + * The Python Imaging Library. + * $Id$ + * + * decoder for raw (uncompressed) image data + * + * history: + * 96-03-07 fl rewritten + * + * Copyright (c) Fredrik Lundh 1996. + * Copyright (c) Secret Labs AB 1997. + * + * See the README file for information on usage and redistribution. + */ + +#include "Imaging.h" + +#include "Raw.h" + +int +ImagingRawDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) { + enum { LINE = 1, SKIP }; + RAWSTATE *rawstate = state->context; + + UINT8 *ptr; + + if (state->state == 0) { + /* Initialize context variables */ + + /* get size of image data and padding */ + state->bytes = (state->xsize * state->bits + 7) / 8; + if (rawstate->stride) { + rawstate->skip = rawstate->stride - state->bytes; + if (rawstate->skip < 0) { + state->errcode = IMAGING_CODEC_CONFIG; + return -1; + } + } else { + rawstate->skip = 0; + } + + /* check image orientation */ + if (state->ystep < 0) { + state->y = state->ysize - 1; + state->ystep = -1; + } else { + state->ystep = 1; + } + + state->state = LINE; + } + + ptr = buf; + + for (;;) { + if (state->state == SKIP) { + /* Skip padding between lines */ + + if (bytes < rawstate->skip) { + return ptr - buf; + } + + ptr += rawstate->skip; + bytes -= rawstate->skip; + + state->state = LINE; + } + + if (bytes < state->bytes) { + return ptr - buf; + } + + /* Unpack data */ + state->shuffle( + (UINT8 *)im->image[state->y + state->yoff] + state->xoff * im->pixelsize, + ptr, + state->xsize); + + ptr += state->bytes; + bytes -= state->bytes; + + state->y += state->ystep; + + if (state->y < 0 || state->y >= state->ysize) { + /* End of file (errcode = 0) */ + return -1; + } + + state->state = SKIP; + } +} diff --git a/src/libImaging/RawEncode.c b/src/libImaging/RawEncode.c index 50de8d982..8964b9d64 100644 --- a/src/libImaging/RawEncode.c +++ b/src/libImaging/RawEncode.c @@ -1,87 +1,87 @@ -/* - * The Python Imaging Library. - * $Id$ - * - * coder for raw data - * - * FIXME: This encoder will fail if the buffer is not large enough to - * hold one full line of data. There's a workaround for this problem - * in ImageFile.py, but it should be solved here instead. - * - * history: - * 96-04-30 fl created - * 97-01-03 fl fixed padding - * - * Copyright (c) Fredrik Lundh 1996-97. - * Copyright (c) Secret Labs AB 1997. - * - * See the README file for information on usage and redistribution. */ - -#include "Imaging.h" - -int -ImagingRawEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { - UINT8 *ptr; - - if (!state->state) { - /* The "count" field holds the stride, if specified. Fix - things up so "bytes" is the full size, and "count" the - packed size */ - - if (state->count > 0) { - int bytes = state->count; - - /* stride must not be less than real size */ - if (state->count < state->bytes) { - state->errcode = IMAGING_CODEC_CONFIG; - return -1; - } - state->count = state->bytes; - state->bytes = bytes; - } else { - state->count = state->bytes; - } - - /* The "ystep" field specifies the orientation */ - - if (state->ystep < 0) { - state->y = state->ysize - 1; - state->ystep = -1; - } else { - state->ystep = 1; - } - - state->state = 1; - } - - if (bytes < state->bytes) { - state->errcode = IMAGING_CODEC_CONFIG; - return 0; - } - - ptr = buf; - - while (bytes >= state->bytes) { - state->shuffle( - ptr, - (UINT8 *)im->image[state->y + state->yoff] + state->xoff * im->pixelsize, - state->xsize); - - if (state->bytes > state->count) { - /* zero-pad the buffer, if necessary */ - memset(ptr + state->count, 0, state->bytes - state->count); - } - - ptr += state->bytes; - bytes -= state->bytes; - - state->y += state->ystep; - - if (state->y < 0 || state->y >= state->ysize) { - state->errcode = IMAGING_CODEC_END; - break; - } - } - - return ptr - buf; -} +/* + * The Python Imaging Library. + * $Id$ + * + * coder for raw data + * + * FIXME: This encoder will fail if the buffer is not large enough to + * hold one full line of data. There's a workaround for this problem + * in ImageFile.py, but it should be solved here instead. + * + * history: + * 96-04-30 fl created + * 97-01-03 fl fixed padding + * + * Copyright (c) Fredrik Lundh 1996-97. + * Copyright (c) Secret Labs AB 1997. + * + * See the README file for information on usage and redistribution. */ + +#include "Imaging.h" + +int +ImagingRawEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { + UINT8 *ptr; + + if (!state->state) { + /* The "count" field holds the stride, if specified. Fix + things up so "bytes" is the full size, and "count" the + packed size */ + + if (state->count > 0) { + int bytes = state->count; + + /* stride must not be less than real size */ + if (state->count < state->bytes) { + state->errcode = IMAGING_CODEC_CONFIG; + return -1; + } + state->count = state->bytes; + state->bytes = bytes; + } else { + state->count = state->bytes; + } + + /* The "ystep" field specifies the orientation */ + + if (state->ystep < 0) { + state->y = state->ysize - 1; + state->ystep = -1; + } else { + state->ystep = 1; + } + + state->state = 1; + } + + if (bytes < state->bytes) { + state->errcode = IMAGING_CODEC_CONFIG; + return 0; + } + + ptr = buf; + + while (bytes >= state->bytes) { + state->shuffle( + ptr, + (UINT8 *)im->image[state->y + state->yoff] + state->xoff * im->pixelsize, + state->xsize); + + if (state->bytes > state->count) { + /* zero-pad the buffer, if necessary */ + memset(ptr + state->count, 0, state->bytes - state->count); + } + + ptr += state->bytes; + bytes -= state->bytes; + + state->y += state->ystep; + + if (state->y < 0 || state->y >= state->ysize) { + state->errcode = IMAGING_CODEC_END; + break; + } + } + + return ptr - buf; +} diff --git a/src/libImaging/Reduce.c b/src/libImaging/Reduce.c index 61566f0c5..7dae92282 100644 --- a/src/libImaging/Reduce.c +++ b/src/libImaging/Reduce.c @@ -1,1483 +1,1483 @@ -#include "Imaging.h" - -#include - -#define ROUND_UP(f) ((int)((f) >= 0.0 ? (f) + 0.5F : (f) - 0.5F)) - -UINT32 -division_UINT32(int divider, int result_bits) { - UINT32 max_dividend = (1 << result_bits) * divider; - float max_int = (1 << 30) * 4.0; - return (UINT32)(max_int / max_dividend); -} - -void -ImagingReduceNxN(Imaging imOut, Imaging imIn, int box[4], int xscale, int yscale) { - /* The most general implementation for any xscale and yscale - */ - int x, y, xx, yy; - UINT32 multiplier = division_UINT32(yscale * xscale, 8); - UINT32 amend = yscale * xscale / 2; - - if (imIn->image8) { - for (y = 0; y < box[3] / yscale; y++) { - int yy_from = box[1] + y * yscale; - for (x = 0; x < box[2] / xscale; x++) { - int xx_from = box[0] + x * xscale; - UINT32 ss = amend; - for (yy = yy_from; yy < yy_from + yscale - 1; yy += 2) { - UINT8 *line0 = (UINT8 *)imIn->image8[yy]; - UINT8 *line1 = (UINT8 *)imIn->image8[yy + 1]; - for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { - ss += line0[xx + 0] + line0[xx + 1] + line1[xx + 0] + - line1[xx + 1]; - } - if (xscale & 0x01) { - ss += line0[xx + 0] + line1[xx + 0]; - } - } - if (yscale & 0x01) { - UINT8 *line = (UINT8 *)imIn->image8[yy]; - for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { - ss += line[xx + 0] + line[xx + 1]; - } - if (xscale & 0x01) { - ss += line[xx + 0]; - } - } - imOut->image8[y][x] = (ss * multiplier) >> 24; - } - } - } else { - for (y = 0; y < box[3] / yscale; y++) { - int yy_from = box[1] + y * yscale; - if (imIn->bands == 2) { - for (x = 0; x < box[2] / xscale; x++) { - int xx_from = box[0] + x * xscale; - UINT32 v; - UINT32 ss0 = amend, ss3 = amend; - for (yy = yy_from; yy < yy_from + yscale - 1; yy += 2) { - UINT8 *line0 = (UINT8 *)imIn->image[yy]; - UINT8 *line1 = (UINT8 *)imIn->image[yy + 1]; - for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { - ss0 += line0[xx * 4 + 0] + line0[xx * 4 + 4] + - line1[xx * 4 + 0] + line1[xx * 4 + 4]; - ss3 += line0[xx * 4 + 3] + line0[xx * 4 + 7] + - line1[xx * 4 + 3] + line1[xx * 4 + 7]; - } - if (xscale & 0x01) { - ss0 += line0[xx * 4 + 0] + line1[xx * 4 + 0]; - ss3 += line0[xx * 4 + 3] + line1[xx * 4 + 3]; - } - } - if (yscale & 0x01) { - UINT8 *line = (UINT8 *)imIn->image[yy]; - for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { - ss0 += line[xx * 4 + 0] + line[xx * 4 + 4]; - ss3 += line[xx * 4 + 3] + line[xx * 4 + 7]; - } - if (xscale & 0x01) { - ss0 += line[xx * 4 + 0]; - ss3 += line[xx * 4 + 3]; - } - } - v = MAKE_UINT32( - (ss0 * multiplier) >> 24, 0, 0, (ss3 * multiplier) >> 24); - memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); - } - } else if (imIn->bands == 3) { - for (x = 0; x < box[2] / xscale; x++) { - int xx_from = box[0] + x * xscale; - UINT32 v; - UINT32 ss0 = amend, ss1 = amend, ss2 = amend; - for (yy = yy_from; yy < yy_from + yscale - 1; yy += 2) { - UINT8 *line0 = (UINT8 *)imIn->image[yy]; - UINT8 *line1 = (UINT8 *)imIn->image[yy + 1]; - for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { - ss0 += line0[xx * 4 + 0] + line0[xx * 4 + 4] + - line1[xx * 4 + 0] + line1[xx * 4 + 4]; - ss1 += line0[xx * 4 + 1] + line0[xx * 4 + 5] + - line1[xx * 4 + 1] + line1[xx * 4 + 5]; - ss2 += line0[xx * 4 + 2] + line0[xx * 4 + 6] + - line1[xx * 4 + 2] + line1[xx * 4 + 6]; - } - if (xscale & 0x01) { - ss0 += line0[xx * 4 + 0] + line1[xx * 4 + 0]; - ss1 += line0[xx * 4 + 1] + line1[xx * 4 + 1]; - ss2 += line0[xx * 4 + 2] + line1[xx * 4 + 2]; - } - } - if (yscale & 0x01) { - UINT8 *line = (UINT8 *)imIn->image[yy]; - for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { - ss0 += line[xx * 4 + 0] + line[xx * 4 + 4]; - ss1 += line[xx * 4 + 1] + line[xx * 4 + 5]; - ss2 += line[xx * 4 + 2] + line[xx * 4 + 6]; - } - if (xscale & 0x01) { - ss0 += line[xx * 4 + 0]; - ss1 += line[xx * 4 + 1]; - ss2 += line[xx * 4 + 2]; - } - } - v = MAKE_UINT32( - (ss0 * multiplier) >> 24, - (ss1 * multiplier) >> 24, - (ss2 * multiplier) >> 24, - 0); - memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); - } - } else { // bands == 4 - for (x = 0; x < box[2] / xscale; x++) { - int xx_from = box[0] + x * xscale; - UINT32 v; - UINT32 ss0 = amend, ss1 = amend, ss2 = amend, ss3 = amend; - for (yy = yy_from; yy < yy_from + yscale - 1; yy += 2) { - UINT8 *line0 = (UINT8 *)imIn->image[yy]; - UINT8 *line1 = (UINT8 *)imIn->image[yy + 1]; - for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { - ss0 += line0[xx * 4 + 0] + line0[xx * 4 + 4] + - line1[xx * 4 + 0] + line1[xx * 4 + 4]; - ss1 += line0[xx * 4 + 1] + line0[xx * 4 + 5] + - line1[xx * 4 + 1] + line1[xx * 4 + 5]; - ss2 += line0[xx * 4 + 2] + line0[xx * 4 + 6] + - line1[xx * 4 + 2] + line1[xx * 4 + 6]; - ss3 += line0[xx * 4 + 3] + line0[xx * 4 + 7] + - line1[xx * 4 + 3] + line1[xx * 4 + 7]; - } - if (xscale & 0x01) { - ss0 += line0[xx * 4 + 0] + line1[xx * 4 + 0]; - ss1 += line0[xx * 4 + 1] + line1[xx * 4 + 1]; - ss2 += line0[xx * 4 + 2] + line1[xx * 4 + 2]; - ss3 += line0[xx * 4 + 3] + line1[xx * 4 + 3]; - } - } - if (yscale & 0x01) { - UINT8 *line = (UINT8 *)imIn->image[yy]; - for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { - ss0 += line[xx * 4 + 0] + line[xx * 4 + 4]; - ss1 += line[xx * 4 + 1] + line[xx * 4 + 5]; - ss2 += line[xx * 4 + 2] + line[xx * 4 + 6]; - ss3 += line[xx * 4 + 3] + line[xx * 4 + 7]; - } - if (xscale & 0x01) { - ss0 += line[xx * 4 + 0]; - ss1 += line[xx * 4 + 1]; - ss2 += line[xx * 4 + 2]; - ss3 += line[xx * 4 + 3]; - } - } - v = MAKE_UINT32( - (ss0 * multiplier) >> 24, - (ss1 * multiplier) >> 24, - (ss2 * multiplier) >> 24, - (ss3 * multiplier) >> 24); - memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); - } - } - } - } -} - -void -ImagingReduce1xN(Imaging imOut, Imaging imIn, int box[4], int yscale) { - /* Optimized implementation for xscale = 1. - */ - int x, y, yy; - int xscale = 1; - UINT32 multiplier = division_UINT32(yscale * xscale, 8); - UINT32 amend = yscale * xscale / 2; - - if (imIn->image8) { - for (y = 0; y < box[3] / yscale; y++) { - int yy_from = box[1] + y * yscale; - for (x = 0; x < box[2] / xscale; x++) { - int xx = box[0] + x * xscale; - UINT32 ss = amend; - for (yy = yy_from; yy < yy_from + yscale - 1; yy += 2) { - UINT8 *line0 = (UINT8 *)imIn->image8[yy]; - UINT8 *line1 = (UINT8 *)imIn->image8[yy + 1]; - ss += line0[xx + 0] + line1[xx + 0]; - } - if (yscale & 0x01) { - UINT8 *line = (UINT8 *)imIn->image8[yy]; - ss += line[xx + 0]; - } - imOut->image8[y][x] = (ss * multiplier) >> 24; - } - } - } else { - for (y = 0; y < box[3] / yscale; y++) { - int yy_from = box[1] + y * yscale; - if (imIn->bands == 2) { - for (x = 0; x < box[2] / xscale; x++) { - int xx = box[0] + x * xscale; - UINT32 v; - UINT32 ss0 = amend, ss3 = amend; - for (yy = yy_from; yy < yy_from + yscale - 1; yy += 2) { - UINT8 *line0 = (UINT8 *)imIn->image[yy]; - UINT8 *line1 = (UINT8 *)imIn->image[yy + 1]; - ss0 += line0[xx * 4 + 0] + line1[xx * 4 + 0]; - ss3 += line0[xx * 4 + 3] + line1[xx * 4 + 3]; - } - if (yscale & 0x01) { - UINT8 *line = (UINT8 *)imIn->image[yy]; - ss0 += line[xx * 4 + 0]; - ss3 += line[xx * 4 + 3]; - } - v = MAKE_UINT32( - (ss0 * multiplier) >> 24, 0, 0, (ss3 * multiplier) >> 24); - memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); - } - } else if (imIn->bands == 3) { - for (x = 0; x < box[2] / xscale; x++) { - int xx = box[0] + x * xscale; - UINT32 v; - UINT32 ss0 = amend, ss1 = amend, ss2 = amend; - for (yy = yy_from; yy < yy_from + yscale - 1; yy += 2) { - UINT8 *line0 = (UINT8 *)imIn->image[yy]; - UINT8 *line1 = (UINT8 *)imIn->image[yy + 1]; - ss0 += line0[xx * 4 + 0] + line1[xx * 4 + 0]; - ss1 += line0[xx * 4 + 1] + line1[xx * 4 + 1]; - ss2 += line0[xx * 4 + 2] + line1[xx * 4 + 2]; - } - if (yscale & 0x01) { - UINT8 *line = (UINT8 *)imIn->image[yy]; - ss0 += line[xx * 4 + 0]; - ss1 += line[xx * 4 + 1]; - ss2 += line[xx * 4 + 2]; - } - v = MAKE_UINT32( - (ss0 * multiplier) >> 24, - (ss1 * multiplier) >> 24, - (ss2 * multiplier) >> 24, - 0); - memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); - } - } else { // bands == 4 - for (x = 0; x < box[2] / xscale; x++) { - int xx = box[0] + x * xscale; - UINT32 v; - UINT32 ss0 = amend, ss1 = amend, ss2 = amend, ss3 = amend; - for (yy = yy_from; yy < yy_from + yscale - 1; yy += 2) { - UINT8 *line0 = (UINT8 *)imIn->image[yy]; - UINT8 *line1 = (UINT8 *)imIn->image[yy + 1]; - ss0 += line0[xx * 4 + 0] + line1[xx * 4 + 0]; - ss1 += line0[xx * 4 + 1] + line1[xx * 4 + 1]; - ss2 += line0[xx * 4 + 2] + line1[xx * 4 + 2]; - ss3 += line0[xx * 4 + 3] + line1[xx * 4 + 3]; - } - if (yscale & 0x01) { - UINT8 *line = (UINT8 *)imIn->image[yy]; - ss0 += line[xx * 4 + 0]; - ss1 += line[xx * 4 + 1]; - ss2 += line[xx * 4 + 2]; - ss3 += line[xx * 4 + 3]; - } - v = MAKE_UINT32( - (ss0 * multiplier) >> 24, - (ss1 * multiplier) >> 24, - (ss2 * multiplier) >> 24, - (ss3 * multiplier) >> 24); - memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); - } - } - } - } -} - -void -ImagingReduceNx1(Imaging imOut, Imaging imIn, int box[4], int xscale) { - /* Optimized implementation for yscale = 1. - */ - int x, y, xx; - int yscale = 1; - UINT32 multiplier = division_UINT32(yscale * xscale, 8); - UINT32 amend = yscale * xscale / 2; - - if (imIn->image8) { - for (y = 0; y < box[3] / yscale; y++) { - int yy = box[1] + y * yscale; - UINT8 *line = (UINT8 *)imIn->image8[yy]; - for (x = 0; x < box[2] / xscale; x++) { - int xx_from = box[0] + x * xscale; - UINT32 ss = amend; - for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { - ss += line[xx + 0] + line[xx + 1]; - } - if (xscale & 0x01) { - ss += line[xx + 0]; - } - imOut->image8[y][x] = (ss * multiplier) >> 24; - } - } - } else { - for (y = 0; y < box[3] / yscale; y++) { - int yy = box[1] + y * yscale; - UINT8 *line = (UINT8 *)imIn->image[yy]; - if (imIn->bands == 2) { - for (x = 0; x < box[2] / xscale; x++) { - int xx_from = box[0] + x * xscale; - UINT32 v; - UINT32 ss0 = amend, ss3 = amend; - for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { - ss0 += line[xx * 4 + 0] + line[xx * 4 + 4]; - ss3 += line[xx * 4 + 3] + line[xx * 4 + 7]; - } - if (xscale & 0x01) { - ss0 += line[xx * 4 + 0]; - ss3 += line[xx * 4 + 3]; - } - v = MAKE_UINT32( - (ss0 * multiplier) >> 24, 0, 0, (ss3 * multiplier) >> 24); - memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); - } - } else if (imIn->bands == 3) { - for (x = 0; x < box[2] / xscale; x++) { - int xx_from = box[0] + x * xscale; - UINT32 v; - UINT32 ss0 = amend, ss1 = amend, ss2 = amend; - for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { - ss0 += line[xx * 4 + 0] + line[xx * 4 + 4]; - ss1 += line[xx * 4 + 1] + line[xx * 4 + 5]; - ss2 += line[xx * 4 + 2] + line[xx * 4 + 6]; - } - if (xscale & 0x01) { - ss0 += line[xx * 4 + 0]; - ss1 += line[xx * 4 + 1]; - ss2 += line[xx * 4 + 2]; - } - v = MAKE_UINT32( - (ss0 * multiplier) >> 24, - (ss1 * multiplier) >> 24, - (ss2 * multiplier) >> 24, - 0); - memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); - } - } else { // bands == 4 - for (x = 0; x < box[2] / xscale; x++) { - int xx_from = box[0] + x * xscale; - UINT32 v; - UINT32 ss0 = amend, ss1 = amend, ss2 = amend, ss3 = amend; - for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { - ss0 += line[xx * 4 + 0] + line[xx * 4 + 4]; - ss1 += line[xx * 4 + 1] + line[xx * 4 + 5]; - ss2 += line[xx * 4 + 2] + line[xx * 4 + 6]; - ss3 += line[xx * 4 + 3] + line[xx * 4 + 7]; - } - if (xscale & 0x01) { - ss0 += line[xx * 4 + 0]; - ss1 += line[xx * 4 + 1]; - ss2 += line[xx * 4 + 2]; - ss3 += line[xx * 4 + 3]; - } - v = MAKE_UINT32( - (ss0 * multiplier) >> 24, - (ss1 * multiplier) >> 24, - (ss2 * multiplier) >> 24, - (ss3 * multiplier) >> 24); - memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); - } - } - } - } -} - -void -ImagingReduce1x2(Imaging imOut, Imaging imIn, int box[4]) { - /* Optimized implementation for xscale = 1 and yscale = 2. - */ - int xscale = 1, yscale = 2; - int x, y; - UINT32 ss0, ss1, ss2, ss3; - UINT32 amend = yscale * xscale / 2; - - if (imIn->image8) { - for (y = 0; y < box[3] / yscale; y++) { - int yy = box[1] + y * yscale; - UINT8 *line0 = (UINT8 *)imIn->image8[yy + 0]; - UINT8 *line1 = (UINT8 *)imIn->image8[yy + 1]; - for (x = 0; x < box[2] / xscale; x++) { - int xx = box[0] + x * xscale; - ss0 = line0[xx + 0] + line1[xx + 0]; - imOut->image8[y][x] = (ss0 + amend) >> 1; - } - } - } else { - for (y = 0; y < box[3] / yscale; y++) { - int yy = box[1] + y * yscale; - UINT8 *line0 = (UINT8 *)imIn->image[yy + 0]; - UINT8 *line1 = (UINT8 *)imIn->image[yy + 1]; - if (imIn->bands == 2) { - for (x = 0; x < box[2] / xscale; x++) { - int xx = box[0] + x * xscale; - UINT32 v; - ss0 = line0[xx * 4 + 0] + line1[xx * 4 + 0]; - ss3 = line0[xx * 4 + 3] + line1[xx * 4 + 3]; - v = MAKE_UINT32((ss0 + amend) >> 1, 0, 0, (ss3 + amend) >> 1); - memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); - } - } else if (imIn->bands == 3) { - for (x = 0; x < box[2] / xscale; x++) { - int xx = box[0] + x * xscale; - UINT32 v; - ss0 = line0[xx * 4 + 0] + line1[xx * 4 + 0]; - ss1 = line0[xx * 4 + 1] + line1[xx * 4 + 1]; - ss2 = line0[xx * 4 + 2] + line1[xx * 4 + 2]; - v = MAKE_UINT32( - (ss0 + amend) >> 1, (ss1 + amend) >> 1, (ss2 + amend) >> 1, 0); - memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); - } - } else { // bands == 4 - for (x = 0; x < box[2] / xscale; x++) { - int xx = box[0] + x * xscale; - UINT32 v; - ss0 = line0[xx * 4 + 0] + line1[xx * 4 + 0]; - ss1 = line0[xx * 4 + 1] + line1[xx * 4 + 1]; - ss2 = line0[xx * 4 + 2] + line1[xx * 4 + 2]; - ss3 = line0[xx * 4 + 3] + line1[xx * 4 + 3]; - v = MAKE_UINT32( - (ss0 + amend) >> 1, - (ss1 + amend) >> 1, - (ss2 + amend) >> 1, - (ss3 + amend) >> 1); - memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); - } - } - } - } -} - -void -ImagingReduce2x1(Imaging imOut, Imaging imIn, int box[4]) { - /* Optimized implementation for xscale = 2 and yscale = 1. - */ - int xscale = 2, yscale = 1; - int x, y; - UINT32 ss0, ss1, ss2, ss3; - UINT32 amend = yscale * xscale / 2; - - if (imIn->image8) { - for (y = 0; y < box[3] / yscale; y++) { - int yy = box[1] + y * yscale; - UINT8 *line0 = (UINT8 *)imIn->image8[yy + 0]; - for (x = 0; x < box[2] / xscale; x++) { - int xx = box[0] + x * xscale; - ss0 = line0[xx + 0] + line0[xx + 1]; - imOut->image8[y][x] = (ss0 + amend) >> 1; - } - } - } else { - for (y = 0; y < box[3] / yscale; y++) { - int yy = box[1] + y * yscale; - UINT8 *line0 = (UINT8 *)imIn->image[yy + 0]; - if (imIn->bands == 2) { - for (x = 0; x < box[2] / xscale; x++) { - int xx = box[0] + x * xscale; - UINT32 v; - ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4]; - ss3 = line0[xx * 4 + 3] + line0[xx * 4 + 7]; - v = MAKE_UINT32((ss0 + amend) >> 1, 0, 0, (ss3 + amend) >> 1); - memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); - } - } else if (imIn->bands == 3) { - for (x = 0; x < box[2] / xscale; x++) { - int xx = box[0] + x * xscale; - UINT32 v; - ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4]; - ss1 = line0[xx * 4 + 1] + line0[xx * 4 + 5]; - ss2 = line0[xx * 4 + 2] + line0[xx * 4 + 6]; - v = MAKE_UINT32( - (ss0 + amend) >> 1, (ss1 + amend) >> 1, (ss2 + amend) >> 1, 0); - memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); - } - } else { // bands == 4 - for (x = 0; x < box[2] / xscale; x++) { - int xx = box[0] + x * xscale; - UINT32 v; - ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4]; - ss1 = line0[xx * 4 + 1] + line0[xx * 4 + 5]; - ss2 = line0[xx * 4 + 2] + line0[xx * 4 + 6]; - ss3 = line0[xx * 4 + 3] + line0[xx * 4 + 7]; - v = MAKE_UINT32( - (ss0 + amend) >> 1, - (ss1 + amend) >> 1, - (ss2 + amend) >> 1, - (ss3 + amend) >> 1); - memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); - } - } - } - } -} - -void -ImagingReduce2x2(Imaging imOut, Imaging imIn, int box[4]) { - /* Optimized implementation for xscale = 2 and yscale = 2. - */ - int xscale = 2, yscale = 2; - int x, y; - UINT32 ss0, ss1, ss2, ss3; - UINT32 amend = yscale * xscale / 2; - - if (imIn->image8) { - for (y = 0; y < box[3] / yscale; y++) { - int yy = box[1] + y * yscale; - UINT8 *line0 = (UINT8 *)imIn->image8[yy + 0]; - UINT8 *line1 = (UINT8 *)imIn->image8[yy + 1]; - for (x = 0; x < box[2] / xscale; x++) { - int xx = box[0] + x * xscale; - ss0 = line0[xx + 0] + line0[xx + 1] + line1[xx + 0] + line1[xx + 1]; - imOut->image8[y][x] = (ss0 + amend) >> 2; - } - } - } else { - for (y = 0; y < box[3] / yscale; y++) { - int yy = box[1] + y * yscale; - UINT8 *line0 = (UINT8 *)imIn->image[yy + 0]; - UINT8 *line1 = (UINT8 *)imIn->image[yy + 1]; - if (imIn->bands == 2) { - for (x = 0; x < box[2] / xscale; x++) { - int xx = box[0] + x * xscale; - UINT32 v; - ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4] + line1[xx * 4 + 0] + - line1[xx * 4 + 4]; - ss3 = line0[xx * 4 + 3] + line0[xx * 4 + 7] + line1[xx * 4 + 3] + - line1[xx * 4 + 7]; - v = MAKE_UINT32((ss0 + amend) >> 2, 0, 0, (ss3 + amend) >> 2); - memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); - } - } else if (imIn->bands == 3) { - for (x = 0; x < box[2] / xscale; x++) { - int xx = box[0] + x * xscale; - UINT32 v; - ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4] + line1[xx * 4 + 0] + - line1[xx * 4 + 4]; - ss1 = line0[xx * 4 + 1] + line0[xx * 4 + 5] + line1[xx * 4 + 1] + - line1[xx * 4 + 5]; - ss2 = line0[xx * 4 + 2] + line0[xx * 4 + 6] + line1[xx * 4 + 2] + - line1[xx * 4 + 6]; - v = MAKE_UINT32( - (ss0 + amend) >> 2, (ss1 + amend) >> 2, (ss2 + amend) >> 2, 0); - memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); - } - } else { // bands == 4 - for (x = 0; x < box[2] / xscale; x++) { - int xx = box[0] + x * xscale; - UINT32 v; - ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4] + line1[xx * 4 + 0] + - line1[xx * 4 + 4]; - ss1 = line0[xx * 4 + 1] + line0[xx * 4 + 5] + line1[xx * 4 + 1] + - line1[xx * 4 + 5]; - ss2 = line0[xx * 4 + 2] + line0[xx * 4 + 6] + line1[xx * 4 + 2] + - line1[xx * 4 + 6]; - ss3 = line0[xx * 4 + 3] + line0[xx * 4 + 7] + line1[xx * 4 + 3] + - line1[xx * 4 + 7]; - v = MAKE_UINT32( - (ss0 + amend) >> 2, - (ss1 + amend) >> 2, - (ss2 + amend) >> 2, - (ss3 + amend) >> 2); - memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); - } - } - } - } -} - -void -ImagingReduce1x3(Imaging imOut, Imaging imIn, int box[4]) { - /* Optimized implementation for xscale = 1 and yscale = 3. - */ - int xscale = 1, yscale = 3; - int x, y; - UINT32 ss0, ss1, ss2, ss3; - UINT32 multiplier = division_UINT32(yscale * xscale, 8); - UINT32 amend = yscale * xscale / 2; - - if (imIn->image8) { - for (y = 0; y < box[3] / yscale; y++) { - int yy = box[1] + y * yscale; - UINT8 *line0 = (UINT8 *)imIn->image8[yy + 0]; - UINT8 *line1 = (UINT8 *)imIn->image8[yy + 1]; - UINT8 *line2 = (UINT8 *)imIn->image8[yy + 2]; - for (x = 0; x < box[2] / xscale; x++) { - int xx = box[0] + x * xscale; - ss0 = line0[xx + 0] + line1[xx + 0] + line2[xx + 0]; - imOut->image8[y][x] = ((ss0 + amend) * multiplier) >> 24; - } - } - } else { - for (y = 0; y < box[3] / yscale; y++) { - int yy = box[1] + y * yscale; - UINT8 *line0 = (UINT8 *)imIn->image[yy + 0]; - UINT8 *line1 = (UINT8 *)imIn->image[yy + 1]; - UINT8 *line2 = (UINT8 *)imIn->image[yy + 2]; - if (imIn->bands == 2) { - for (x = 0; x < box[2] / xscale; x++) { - int xx = box[0] + x * xscale; - UINT32 v; - ss0 = line0[xx * 4 + 0] + line1[xx * 4 + 0] + line2[xx * 4 + 0]; - ss3 = line0[xx * 4 + 3] + line1[xx * 4 + 3] + line2[xx * 4 + 3]; - v = MAKE_UINT32( - ((ss0 + amend) * multiplier) >> 24, - 0, - 0, - ((ss3 + amend) * multiplier) >> 24); - memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); - } - } else if (imIn->bands == 3) { - for (x = 0; x < box[2] / xscale; x++) { - int xx = box[0] + x * xscale; - UINT32 v; - ss0 = line0[xx * 4 + 0] + line1[xx * 4 + 0] + line2[xx * 4 + 0]; - ss1 = line0[xx * 4 + 1] + line1[xx * 4 + 1] + line2[xx * 4 + 1]; - ss2 = line0[xx * 4 + 2] + line1[xx * 4 + 2] + line2[xx * 4 + 2]; - v = MAKE_UINT32( - ((ss0 + amend) * multiplier) >> 24, - ((ss1 + amend) * multiplier) >> 24, - ((ss2 + amend) * multiplier) >> 24, - 0); - memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); - } - } else { // bands == 4 - for (x = 0; x < box[2] / xscale; x++) { - int xx = box[0] + x * xscale; - UINT32 v; - ss0 = line0[xx * 4 + 0] + line1[xx * 4 + 0] + line2[xx * 4 + 0]; - ss1 = line0[xx * 4 + 1] + line1[xx * 4 + 1] + line2[xx * 4 + 1]; - ss2 = line0[xx * 4 + 2] + line1[xx * 4 + 2] + line2[xx * 4 + 2]; - ss3 = line0[xx * 4 + 3] + line1[xx * 4 + 3] + line2[xx * 4 + 3]; - v = MAKE_UINT32( - ((ss0 + amend) * multiplier) >> 24, - ((ss1 + amend) * multiplier) >> 24, - ((ss2 + amend) * multiplier) >> 24, - ((ss3 + amend) * multiplier) >> 24); - memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); - } - } - } - } -} - -void -ImagingReduce3x1(Imaging imOut, Imaging imIn, int box[4]) { - /* Optimized implementation for xscale = 3 and yscale = 1. - */ - int xscale = 3, yscale = 1; - int x, y; - UINT32 ss0, ss1, ss2, ss3; - UINT32 multiplier = division_UINT32(yscale * xscale, 8); - UINT32 amend = yscale * xscale / 2; - - if (imIn->image8) { - for (y = 0; y < box[3] / yscale; y++) { - int yy = box[1] + y * yscale; - UINT8 *line0 = (UINT8 *)imIn->image8[yy + 0]; - for (x = 0; x < box[2] / xscale; x++) { - int xx = box[0] + x * xscale; - ss0 = line0[xx + 0] + line0[xx + 1] + line0[xx + 2]; - imOut->image8[y][x] = ((ss0 + amend) * multiplier) >> 24; - } - } - } else { - for (y = 0; y < box[3] / yscale; y++) { - int yy = box[1] + y * yscale; - UINT8 *line0 = (UINT8 *)imIn->image[yy + 0]; - if (imIn->bands == 2) { - for (x = 0; x < box[2] / xscale; x++) { - int xx = box[0] + x * xscale; - UINT32 v; - ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4] + line0[xx * 4 + 8]; - ss3 = line0[xx * 4 + 3] + line0[xx * 4 + 7] + line0[xx * 4 + 11]; - v = MAKE_UINT32( - ((ss0 + amend) * multiplier) >> 24, - 0, - 0, - ((ss3 + amend) * multiplier) >> 24); - memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); - } - } else if (imIn->bands == 3) { - for (x = 0; x < box[2] / xscale; x++) { - int xx = box[0] + x * xscale; - UINT32 v; - ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4] + line0[xx * 4 + 8]; - ss1 = line0[xx * 4 + 1] + line0[xx * 4 + 5] + line0[xx * 4 + 9]; - ss2 = line0[xx * 4 + 2] + line0[xx * 4 + 6] + line0[xx * 4 + 10]; - v = MAKE_UINT32( - ((ss0 + amend) * multiplier) >> 24, - ((ss1 + amend) * multiplier) >> 24, - ((ss2 + amend) * multiplier) >> 24, - 0); - memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); - } - } else { // bands == 4 - for (x = 0; x < box[2] / xscale; x++) { - int xx = box[0] + x * xscale; - UINT32 v; - ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4] + line0[xx * 4 + 8]; - ss1 = line0[xx * 4 + 1] + line0[xx * 4 + 5] + line0[xx * 4 + 9]; - ss2 = line0[xx * 4 + 2] + line0[xx * 4 + 6] + line0[xx * 4 + 10]; - ss3 = line0[xx * 4 + 3] + line0[xx * 4 + 7] + line0[xx * 4 + 11]; - v = MAKE_UINT32( - ((ss0 + amend) * multiplier) >> 24, - ((ss1 + amend) * multiplier) >> 24, - ((ss2 + amend) * multiplier) >> 24, - ((ss3 + amend) * multiplier) >> 24); - memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); - } - } - } - } -} - -void -ImagingReduce3x3(Imaging imOut, Imaging imIn, int box[4]) { - /* Optimized implementation for xscale = 3 and yscale = 3. - */ - int xscale = 3, yscale = 3; - int x, y; - UINT32 ss0, ss1, ss2, ss3; - UINT32 multiplier = division_UINT32(yscale * xscale, 8); - UINT32 amend = yscale * xscale / 2; - - if (imIn->image8) { - for (y = 0; y < box[3] / yscale; y++) { - int yy = box[1] + y * yscale; - UINT8 *line0 = (UINT8 *)imIn->image8[yy + 0]; - UINT8 *line1 = (UINT8 *)imIn->image8[yy + 1]; - UINT8 *line2 = (UINT8 *)imIn->image8[yy + 2]; - for (x = 0; x < box[2] / xscale; x++) { - int xx = box[0] + x * xscale; - ss0 = line0[xx + 0] + line0[xx + 1] + line0[xx + 2] + line1[xx + 0] + - line1[xx + 1] + line1[xx + 2] + line2[xx + 0] + line2[xx + 1] + - line2[xx + 2]; - imOut->image8[y][x] = ((ss0 + amend) * multiplier) >> 24; - } - } - } else { - for (y = 0; y < box[3] / yscale; y++) { - int yy = box[1] + y * yscale; - UINT8 *line0 = (UINT8 *)imIn->image[yy + 0]; - UINT8 *line1 = (UINT8 *)imIn->image[yy + 1]; - UINT8 *line2 = (UINT8 *)imIn->image[yy + 2]; - if (imIn->bands == 2) { - for (x = 0; x < box[2] / xscale; x++) { - int xx = box[0] + x * xscale; - UINT32 v; - ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4] + line0[xx * 4 + 8] + - line1[xx * 4 + 0] + line1[xx * 4 + 4] + line1[xx * 4 + 8] + - line2[xx * 4 + 0] + line2[xx * 4 + 4] + line2[xx * 4 + 8]; - ss3 = line0[xx * 4 + 3] + line0[xx * 4 + 7] + line0[xx * 4 + 11] + - line1[xx * 4 + 3] + line1[xx * 4 + 7] + line1[xx * 4 + 11] + - line2[xx * 4 + 3] + line2[xx * 4 + 7] + line2[xx * 4 + 11]; - v = MAKE_UINT32( - ((ss0 + amend) * multiplier) >> 24, - 0, - 0, - ((ss3 + amend) * multiplier) >> 24); - memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); - } - } else if (imIn->bands == 3) { - for (x = 0; x < box[2] / xscale; x++) { - int xx = box[0] + x * xscale; - UINT32 v; - ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4] + line0[xx * 4 + 8] + - line1[xx * 4 + 0] + line1[xx * 4 + 4] + line1[xx * 4 + 8] + - line2[xx * 4 + 0] + line2[xx * 4 + 4] + line2[xx * 4 + 8]; - ss1 = line0[xx * 4 + 1] + line0[xx * 4 + 5] + line0[xx * 4 + 9] + - line1[xx * 4 + 1] + line1[xx * 4 + 5] + line1[xx * 4 + 9] + - line2[xx * 4 + 1] + line2[xx * 4 + 5] + line2[xx * 4 + 9]; - ss2 = line0[xx * 4 + 2] + line0[xx * 4 + 6] + line0[xx * 4 + 10] + - line1[xx * 4 + 2] + line1[xx * 4 + 6] + line1[xx * 4 + 10] + - line2[xx * 4 + 2] + line2[xx * 4 + 6] + line2[xx * 4 + 10]; - v = MAKE_UINT32( - ((ss0 + amend) * multiplier) >> 24, - ((ss1 + amend) * multiplier) >> 24, - ((ss2 + amend) * multiplier) >> 24, - 0); - memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); - } - } else { // bands == 4 - for (x = 0; x < box[2] / xscale; x++) { - int xx = box[0] + x * xscale; - UINT32 v; - ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4] + line0[xx * 4 + 8] + - line1[xx * 4 + 0] + line1[xx * 4 + 4] + line1[xx * 4 + 8] + - line2[xx * 4 + 0] + line2[xx * 4 + 4] + line2[xx * 4 + 8]; - ss1 = line0[xx * 4 + 1] + line0[xx * 4 + 5] + line0[xx * 4 + 9] + - line1[xx * 4 + 1] + line1[xx * 4 + 5] + line1[xx * 4 + 9] + - line2[xx * 4 + 1] + line2[xx * 4 + 5] + line2[xx * 4 + 9]; - ss2 = line0[xx * 4 + 2] + line0[xx * 4 + 6] + line0[xx * 4 + 10] + - line1[xx * 4 + 2] + line1[xx * 4 + 6] + line1[xx * 4 + 10] + - line2[xx * 4 + 2] + line2[xx * 4 + 6] + line2[xx * 4 + 10]; - ss3 = line0[xx * 4 + 3] + line0[xx * 4 + 7] + line0[xx * 4 + 11] + - line1[xx * 4 + 3] + line1[xx * 4 + 7] + line1[xx * 4 + 11] + - line2[xx * 4 + 3] + line2[xx * 4 + 7] + line2[xx * 4 + 11]; - v = MAKE_UINT32( - ((ss0 + amend) * multiplier) >> 24, - ((ss1 + amend) * multiplier) >> 24, - ((ss2 + amend) * multiplier) >> 24, - ((ss3 + amend) * multiplier) >> 24); - memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); - } - } - } - } -} - -void -ImagingReduce4x4(Imaging imOut, Imaging imIn, int box[4]) { - /* Optimized implementation for xscale = 4 and yscale = 4. - */ - int xscale = 4, yscale = 4; - int x, y; - UINT32 ss0, ss1, ss2, ss3; - UINT32 amend = yscale * xscale / 2; - - if (imIn->image8) { - for (y = 0; y < box[3] / yscale; y++) { - int yy = box[1] + y * yscale; - UINT8 *line0 = (UINT8 *)imIn->image8[yy + 0]; - UINT8 *line1 = (UINT8 *)imIn->image8[yy + 1]; - UINT8 *line2 = (UINT8 *)imIn->image8[yy + 2]; - UINT8 *line3 = (UINT8 *)imIn->image8[yy + 3]; - for (x = 0; x < box[2] / xscale; x++) { - int xx = box[0] + x * xscale; - ss0 = line0[xx + 0] + line0[xx + 1] + line0[xx + 2] + line0[xx + 3] + - line1[xx + 0] + line1[xx + 1] + line1[xx + 2] + line1[xx + 3] + - line2[xx + 0] + line2[xx + 1] + line2[xx + 2] + line2[xx + 3] + - line3[xx + 0] + line3[xx + 1] + line3[xx + 2] + line3[xx + 3]; - imOut->image8[y][x] = (ss0 + amend) >> 4; - } - } - } else { - for (y = 0; y < box[3] / yscale; y++) { - int yy = box[1] + y * yscale; - UINT8 *line0 = (UINT8 *)imIn->image[yy + 0]; - UINT8 *line1 = (UINT8 *)imIn->image[yy + 1]; - UINT8 *line2 = (UINT8 *)imIn->image[yy + 2]; - UINT8 *line3 = (UINT8 *)imIn->image[yy + 3]; - if (imIn->bands == 2) { - for (x = 0; x < box[2] / xscale; x++) { - int xx = box[0] + x * xscale; - UINT32 v; - ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4] + line0[xx * 4 + 8] + - line0[xx * 4 + 12] + line1[xx * 4 + 0] + line1[xx * 4 + 4] + - line1[xx * 4 + 8] + line1[xx * 4 + 12] + line2[xx * 4 + 0] + - line2[xx * 4 + 4] + line2[xx * 4 + 8] + line2[xx * 4 + 12] + - line3[xx * 4 + 0] + line3[xx * 4 + 4] + line3[xx * 4 + 8] + - line3[xx * 4 + 12]; - ss3 = line0[xx * 4 + 3] + line0[xx * 4 + 7] + line0[xx * 4 + 11] + - line0[xx * 4 + 15] + line1[xx * 4 + 3] + line1[xx * 4 + 7] + - line1[xx * 4 + 11] + line1[xx * 4 + 15] + line2[xx * 4 + 3] + - line2[xx * 4 + 7] + line2[xx * 4 + 11] + line2[xx * 4 + 15] + - line3[xx * 4 + 3] + line3[xx * 4 + 7] + line3[xx * 4 + 11] + - line3[xx * 4 + 15]; - v = MAKE_UINT32((ss0 + amend) >> 4, 0, 0, (ss3 + amend) >> 4); - memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); - } - } else if (imIn->bands == 3) { - for (x = 0; x < box[2] / xscale; x++) { - int xx = box[0] + x * xscale; - UINT32 v; - ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4] + line0[xx * 4 + 8] + - line0[xx * 4 + 12] + line1[xx * 4 + 0] + line1[xx * 4 + 4] + - line1[xx * 4 + 8] + line1[xx * 4 + 12] + line2[xx * 4 + 0] + - line2[xx * 4 + 4] + line2[xx * 4 + 8] + line2[xx * 4 + 12] + - line3[xx * 4 + 0] + line3[xx * 4 + 4] + line3[xx * 4 + 8] + - line3[xx * 4 + 12]; - ss1 = line0[xx * 4 + 1] + line0[xx * 4 + 5] + line0[xx * 4 + 9] + - line0[xx * 4 + 13] + line1[xx * 4 + 1] + line1[xx * 4 + 5] + - line1[xx * 4 + 9] + line1[xx * 4 + 13] + line2[xx * 4 + 1] + - line2[xx * 4 + 5] + line2[xx * 4 + 9] + line2[xx * 4 + 13] + - line3[xx * 4 + 1] + line3[xx * 4 + 5] + line3[xx * 4 + 9] + - line3[xx * 4 + 13]; - ss2 = line0[xx * 4 + 2] + line0[xx * 4 + 6] + line0[xx * 4 + 10] + - line0[xx * 4 + 14] + line1[xx * 4 + 2] + line1[xx * 4 + 6] + - line1[xx * 4 + 10] + line1[xx * 4 + 14] + line2[xx * 4 + 2] + - line2[xx * 4 + 6] + line2[xx * 4 + 10] + line2[xx * 4 + 14] + - line3[xx * 4 + 2] + line3[xx * 4 + 6] + line3[xx * 4 + 10] + - line3[xx * 4 + 14]; - v = MAKE_UINT32( - (ss0 + amend) >> 4, (ss1 + amend) >> 4, (ss2 + amend) >> 4, 0); - memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); - } - } else { // bands == 4 - for (x = 0; x < box[2] / xscale; x++) { - int xx = box[0] + x * xscale; - UINT32 v; - ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4] + line0[xx * 4 + 8] + - line0[xx * 4 + 12] + line1[xx * 4 + 0] + line1[xx * 4 + 4] + - line1[xx * 4 + 8] + line1[xx * 4 + 12] + line2[xx * 4 + 0] + - line2[xx * 4 + 4] + line2[xx * 4 + 8] + line2[xx * 4 + 12] + - line3[xx * 4 + 0] + line3[xx * 4 + 4] + line3[xx * 4 + 8] + - line3[xx * 4 + 12]; - ss1 = line0[xx * 4 + 1] + line0[xx * 4 + 5] + line0[xx * 4 + 9] + - line0[xx * 4 + 13] + line1[xx * 4 + 1] + line1[xx * 4 + 5] + - line1[xx * 4 + 9] + line1[xx * 4 + 13] + line2[xx * 4 + 1] + - line2[xx * 4 + 5] + line2[xx * 4 + 9] + line2[xx * 4 + 13] + - line3[xx * 4 + 1] + line3[xx * 4 + 5] + line3[xx * 4 + 9] + - line3[xx * 4 + 13]; - ss2 = line0[xx * 4 + 2] + line0[xx * 4 + 6] + line0[xx * 4 + 10] + - line0[xx * 4 + 14] + line1[xx * 4 + 2] + line1[xx * 4 + 6] + - line1[xx * 4 + 10] + line1[xx * 4 + 14] + line2[xx * 4 + 2] + - line2[xx * 4 + 6] + line2[xx * 4 + 10] + line2[xx * 4 + 14] + - line3[xx * 4 + 2] + line3[xx * 4 + 6] + line3[xx * 4 + 10] + - line3[xx * 4 + 14]; - ss3 = line0[xx * 4 + 3] + line0[xx * 4 + 7] + line0[xx * 4 + 11] + - line0[xx * 4 + 15] + line1[xx * 4 + 3] + line1[xx * 4 + 7] + - line1[xx * 4 + 11] + line1[xx * 4 + 15] + line2[xx * 4 + 3] + - line2[xx * 4 + 7] + line2[xx * 4 + 11] + line2[xx * 4 + 15] + - line3[xx * 4 + 3] + line3[xx * 4 + 7] + line3[xx * 4 + 11] + - line3[xx * 4 + 15]; - v = MAKE_UINT32( - (ss0 + amend) >> 4, - (ss1 + amend) >> 4, - (ss2 + amend) >> 4, - (ss3 + amend) >> 4); - memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); - } - } - } - } -} - -void -ImagingReduce5x5(Imaging imOut, Imaging imIn, int box[4]) { - /* Fast special case for xscale = 5 and yscale = 5. - */ - int xscale = 5, yscale = 5; - int x, y; - UINT32 ss0, ss1, ss2, ss3; - UINT32 multiplier = division_UINT32(yscale * xscale, 8); - UINT32 amend = yscale * xscale / 2; - - if (imIn->image8) { - for (y = 0; y < box[3] / yscale; y++) { - int yy = box[1] + y * yscale; - UINT8 *line0 = (UINT8 *)imIn->image8[yy + 0]; - UINT8 *line1 = (UINT8 *)imIn->image8[yy + 1]; - UINT8 *line2 = (UINT8 *)imIn->image8[yy + 2]; - UINT8 *line3 = (UINT8 *)imIn->image8[yy + 3]; - UINT8 *line4 = (UINT8 *)imIn->image8[yy + 4]; - for (x = 0; x < box[2] / xscale; x++) { - int xx = box[0] + x * xscale; - ss0 = line0[xx + 0] + line0[xx + 1] + line0[xx + 2] + line0[xx + 3] + - line0[xx + 4] + line1[xx + 0] + line1[xx + 1] + line1[xx + 2] + - line1[xx + 3] + line1[xx + 4] + line2[xx + 0] + line2[xx + 1] + - line2[xx + 2] + line2[xx + 3] + line2[xx + 4] + line3[xx + 0] + - line3[xx + 1] + line3[xx + 2] + line3[xx + 3] + line3[xx + 4] + - line4[xx + 0] + line4[xx + 1] + line4[xx + 2] + line4[xx + 3] + - line4[xx + 4]; - imOut->image8[y][x] = ((ss0 + amend) * multiplier) >> 24; - } - } - } else { - for (y = 0; y < box[3] / yscale; y++) { - int yy = box[1] + y * yscale; - UINT8 *line0 = (UINT8 *)imIn->image[yy + 0]; - UINT8 *line1 = (UINT8 *)imIn->image[yy + 1]; - UINT8 *line2 = (UINT8 *)imIn->image[yy + 2]; - UINT8 *line3 = (UINT8 *)imIn->image[yy + 3]; - UINT8 *line4 = (UINT8 *)imIn->image[yy + 4]; - if (imIn->bands == 2) { - for (x = 0; x < box[2] / xscale; x++) { - int xx = box[0] + x * xscale; - UINT32 v; - ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4] + line0[xx * 4 + 8] + - line0[xx * 4 + 12] + line0[xx * 4 + 16] + line1[xx * 4 + 0] + - line1[xx * 4 + 4] + line1[xx * 4 + 8] + line1[xx * 4 + 12] + - line1[xx * 4 + 16] + line2[xx * 4 + 0] + line2[xx * 4 + 4] + - line2[xx * 4 + 8] + line2[xx * 4 + 12] + line2[xx * 4 + 16] + - line3[xx * 4 + 0] + line3[xx * 4 + 4] + line3[xx * 4 + 8] + - line3[xx * 4 + 12] + line3[xx * 4 + 16] + line4[xx * 4 + 0] + - line4[xx * 4 + 4] + line4[xx * 4 + 8] + line4[xx * 4 + 12] + - line4[xx * 4 + 16]; - ss3 = line0[xx * 4 + 3] + line0[xx * 4 + 7] + line0[xx * 4 + 11] + - line0[xx * 4 + 15] + line0[xx * 4 + 19] + line1[xx * 4 + 3] + - line1[xx * 4 + 7] + line1[xx * 4 + 11] + line1[xx * 4 + 15] + - line1[xx * 4 + 19] + line2[xx * 4 + 3] + line2[xx * 4 + 7] + - line2[xx * 4 + 11] + line2[xx * 4 + 15] + line2[xx * 4 + 19] + - line3[xx * 4 + 3] + line3[xx * 4 + 7] + line3[xx * 4 + 11] + - line3[xx * 4 + 15] + line3[xx * 4 + 19] + line4[xx * 4 + 3] + - line4[xx * 4 + 7] + line4[xx * 4 + 11] + line4[xx * 4 + 15] + - line4[xx * 4 + 19]; - v = MAKE_UINT32( - ((ss0 + amend) * multiplier) >> 24, - 0, - 0, - ((ss3 + amend) * multiplier) >> 24); - memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); - } - } else if (imIn->bands == 3) { - for (x = 0; x < box[2] / xscale; x++) { - int xx = box[0] + x * xscale; - UINT32 v; - ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4] + line0[xx * 4 + 8] + - line0[xx * 4 + 12] + line0[xx * 4 + 16] + line1[xx * 4 + 0] + - line1[xx * 4 + 4] + line1[xx * 4 + 8] + line1[xx * 4 + 12] + - line1[xx * 4 + 16] + line2[xx * 4 + 0] + line2[xx * 4 + 4] + - line2[xx * 4 + 8] + line2[xx * 4 + 12] + line2[xx * 4 + 16] + - line3[xx * 4 + 0] + line3[xx * 4 + 4] + line3[xx * 4 + 8] + - line3[xx * 4 + 12] + line3[xx * 4 + 16] + line4[xx * 4 + 0] + - line4[xx * 4 + 4] + line4[xx * 4 + 8] + line4[xx * 4 + 12] + - line4[xx * 4 + 16]; - ss1 = line0[xx * 4 + 1] + line0[xx * 4 + 5] + line0[xx * 4 + 9] + - line0[xx * 4 + 13] + line0[xx * 4 + 17] + line1[xx * 4 + 1] + - line1[xx * 4 + 5] + line1[xx * 4 + 9] + line1[xx * 4 + 13] + - line1[xx * 4 + 17] + line2[xx * 4 + 1] + line2[xx * 4 + 5] + - line2[xx * 4 + 9] + line2[xx * 4 + 13] + line2[xx * 4 + 17] + - line3[xx * 4 + 1] + line3[xx * 4 + 5] + line3[xx * 4 + 9] + - line3[xx * 4 + 13] + line3[xx * 4 + 17] + line4[xx * 4 + 1] + - line4[xx * 4 + 5] + line4[xx * 4 + 9] + line4[xx * 4 + 13] + - line4[xx * 4 + 17]; - ss2 = line0[xx * 4 + 2] + line0[xx * 4 + 6] + line0[xx * 4 + 10] + - line0[xx * 4 + 14] + line0[xx * 4 + 18] + line1[xx * 4 + 2] + - line1[xx * 4 + 6] + line1[xx * 4 + 10] + line1[xx * 4 + 14] + - line1[xx * 4 + 18] + line2[xx * 4 + 2] + line2[xx * 4 + 6] + - line2[xx * 4 + 10] + line2[xx * 4 + 14] + line2[xx * 4 + 18] + - line3[xx * 4 + 2] + line3[xx * 4 + 6] + line3[xx * 4 + 10] + - line3[xx * 4 + 14] + line3[xx * 4 + 18] + line4[xx * 4 + 2] + - line4[xx * 4 + 6] + line4[xx * 4 + 10] + line4[xx * 4 + 14] + - line4[xx * 4 + 18]; - v = MAKE_UINT32( - ((ss0 + amend) * multiplier) >> 24, - ((ss1 + amend) * multiplier) >> 24, - ((ss2 + amend) * multiplier) >> 24, - 0); - memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); - } - } else { // bands == 4 - for (x = 0; x < box[2] / xscale; x++) { - int xx = box[0] + x * xscale; - UINT32 v; - ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4] + line0[xx * 4 + 8] + - line0[xx * 4 + 12] + line0[xx * 4 + 16] + line1[xx * 4 + 0] + - line1[xx * 4 + 4] + line1[xx * 4 + 8] + line1[xx * 4 + 12] + - line1[xx * 4 + 16] + line2[xx * 4 + 0] + line2[xx * 4 + 4] + - line2[xx * 4 + 8] + line2[xx * 4 + 12] + line2[xx * 4 + 16] + - line3[xx * 4 + 0] + line3[xx * 4 + 4] + line3[xx * 4 + 8] + - line3[xx * 4 + 12] + line3[xx * 4 + 16] + line4[xx * 4 + 0] + - line4[xx * 4 + 4] + line4[xx * 4 + 8] + line4[xx * 4 + 12] + - line4[xx * 4 + 16]; - ss1 = line0[xx * 4 + 1] + line0[xx * 4 + 5] + line0[xx * 4 + 9] + - line0[xx * 4 + 13] + line0[xx * 4 + 17] + line1[xx * 4 + 1] + - line1[xx * 4 + 5] + line1[xx * 4 + 9] + line1[xx * 4 + 13] + - line1[xx * 4 + 17] + line2[xx * 4 + 1] + line2[xx * 4 + 5] + - line2[xx * 4 + 9] + line2[xx * 4 + 13] + line2[xx * 4 + 17] + - line3[xx * 4 + 1] + line3[xx * 4 + 5] + line3[xx * 4 + 9] + - line3[xx * 4 + 13] + line3[xx * 4 + 17] + line4[xx * 4 + 1] + - line4[xx * 4 + 5] + line4[xx * 4 + 9] + line4[xx * 4 + 13] + - line4[xx * 4 + 17]; - ss2 = line0[xx * 4 + 2] + line0[xx * 4 + 6] + line0[xx * 4 + 10] + - line0[xx * 4 + 14] + line0[xx * 4 + 18] + line1[xx * 4 + 2] + - line1[xx * 4 + 6] + line1[xx * 4 + 10] + line1[xx * 4 + 14] + - line1[xx * 4 + 18] + line2[xx * 4 + 2] + line2[xx * 4 + 6] + - line2[xx * 4 + 10] + line2[xx * 4 + 14] + line2[xx * 4 + 18] + - line3[xx * 4 + 2] + line3[xx * 4 + 6] + line3[xx * 4 + 10] + - line3[xx * 4 + 14] + line3[xx * 4 + 18] + line4[xx * 4 + 2] + - line4[xx * 4 + 6] + line4[xx * 4 + 10] + line4[xx * 4 + 14] + - line4[xx * 4 + 18]; - ss3 = line0[xx * 4 + 3] + line0[xx * 4 + 7] + line0[xx * 4 + 11] + - line0[xx * 4 + 15] + line0[xx * 4 + 19] + line1[xx * 4 + 3] + - line1[xx * 4 + 7] + line1[xx * 4 + 11] + line1[xx * 4 + 15] + - line1[xx * 4 + 19] + line2[xx * 4 + 3] + line2[xx * 4 + 7] + - line2[xx * 4 + 11] + line2[xx * 4 + 15] + line2[xx * 4 + 19] + - line3[xx * 4 + 3] + line3[xx * 4 + 7] + line3[xx * 4 + 11] + - line3[xx * 4 + 15] + line3[xx * 4 + 19] + line4[xx * 4 + 3] + - line4[xx * 4 + 7] + line4[xx * 4 + 11] + line4[xx * 4 + 15] + - line4[xx * 4 + 19]; - v = MAKE_UINT32( - ((ss0 + amend) * multiplier) >> 24, - ((ss1 + amend) * multiplier) >> 24, - ((ss2 + amend) * multiplier) >> 24, - ((ss3 + amend) * multiplier) >> 24); - memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); - } - } - } - } -} - -void -ImagingReduceCorners(Imaging imOut, Imaging imIn, int box[4], int xscale, int yscale) { - /* Fill the last row and the last column for any xscale and yscale. - */ - int x, y, xx, yy; - - if (imIn->image8) { - if (box[2] % xscale) { - int scale = (box[2] % xscale) * yscale; - UINT32 multiplier = division_UINT32(scale, 8); - UINT32 amend = scale / 2; - for (y = 0; y < box[3] / yscale; y++) { - int yy_from = box[1] + y * yscale; - UINT32 ss = amend; - x = box[2] / xscale; - - for (yy = yy_from; yy < yy_from + yscale; yy++) { - UINT8 *line = (UINT8 *)imIn->image8[yy]; - for (xx = box[0] + x * xscale; xx < box[0] + box[2]; xx++) { - ss += line[xx + 0]; - } - } - imOut->image8[y][x] = (ss * multiplier) >> 24; - } - } - if (box[3] % yscale) { - int scale = xscale * (box[3] % yscale); - UINT32 multiplier = division_UINT32(scale, 8); - UINT32 amend = scale / 2; - y = box[3] / yscale; - for (x = 0; x < box[2] / xscale; x++) { - int xx_from = box[0] + x * xscale; - UINT32 ss = amend; - for (yy = box[1] + y * yscale; yy < box[1] + box[3]; yy++) { - UINT8 *line = (UINT8 *)imIn->image8[yy]; - for (xx = xx_from; xx < xx_from + xscale; xx++) { - ss += line[xx + 0]; - } - } - imOut->image8[y][x] = (ss * multiplier) >> 24; - } - } - if (box[2] % xscale && box[3] % yscale) { - int scale = (box[2] % xscale) * (box[3] % yscale); - UINT32 multiplier = division_UINT32(scale, 8); - UINT32 amend = scale / 2; - UINT32 ss = amend; - x = box[2] / xscale; - y = box[3] / yscale; - for (yy = box[1] + y * yscale; yy < box[1] + box[3]; yy++) { - UINT8 *line = (UINT8 *)imIn->image8[yy]; - for (xx = box[0] + x * xscale; xx < box[0] + box[2]; xx++) { - ss += line[xx + 0]; - } - } - imOut->image8[y][x] = (ss * multiplier) >> 24; - } - } else { - if (box[2] % xscale) { - int scale = (box[2] % xscale) * yscale; - UINT32 multiplier = division_UINT32(scale, 8); - UINT32 amend = scale / 2; - for (y = 0; y < box[3] / yscale; y++) { - int yy_from = box[1] + y * yscale; - UINT32 v; - UINT32 ss0 = amend, ss1 = amend, ss2 = amend, ss3 = amend; - x = box[2] / xscale; - - for (yy = yy_from; yy < yy_from + yscale; yy++) { - UINT8 *line = (UINT8 *)imIn->image[yy]; - for (xx = box[0] + x * xscale; xx < box[0] + box[2]; xx++) { - ss0 += line[xx * 4 + 0]; - ss1 += line[xx * 4 + 1]; - ss2 += line[xx * 4 + 2]; - ss3 += line[xx * 4 + 3]; - } - } - v = MAKE_UINT32( - (ss0 * multiplier) >> 24, - (ss1 * multiplier) >> 24, - (ss2 * multiplier) >> 24, - (ss3 * multiplier) >> 24); - memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); - } - } - if (box[3] % yscale) { - int scale = xscale * (box[3] % yscale); - UINT32 multiplier = division_UINT32(scale, 8); - UINT32 amend = scale / 2; - y = box[3] / yscale; - for (x = 0; x < box[2] / xscale; x++) { - int xx_from = box[0] + x * xscale; - UINT32 v; - UINT32 ss0 = amend, ss1 = amend, ss2 = amend, ss3 = amend; - for (yy = box[1] + y * yscale; yy < box[1] + box[3]; yy++) { - UINT8 *line = (UINT8 *)imIn->image[yy]; - for (xx = xx_from; xx < xx_from + xscale; xx++) { - ss0 += line[xx * 4 + 0]; - ss1 += line[xx * 4 + 1]; - ss2 += line[xx * 4 + 2]; - ss3 += line[xx * 4 + 3]; - } - } - v = MAKE_UINT32( - (ss0 * multiplier) >> 24, - (ss1 * multiplier) >> 24, - (ss2 * multiplier) >> 24, - (ss3 * multiplier) >> 24); - memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); - } - } - if (box[2] % xscale && box[3] % yscale) { - int scale = (box[2] % xscale) * (box[3] % yscale); - UINT32 multiplier = division_UINT32(scale, 8); - UINT32 amend = scale / 2; - UINT32 v; - UINT32 ss0 = amend, ss1 = amend, ss2 = amend, ss3 = amend; - x = box[2] / xscale; - y = box[3] / yscale; - for (yy = box[1] + y * yscale; yy < box[1] + box[3]; yy++) { - UINT8 *line = (UINT8 *)imIn->image[yy]; - for (xx = box[0] + x * xscale; xx < box[0] + box[2]; xx++) { - ss0 += line[xx * 4 + 0]; - ss1 += line[xx * 4 + 1]; - ss2 += line[xx * 4 + 2]; - ss3 += line[xx * 4 + 3]; - } - } - v = MAKE_UINT32( - (ss0 * multiplier) >> 24, - (ss1 * multiplier) >> 24, - (ss2 * multiplier) >> 24, - (ss3 * multiplier) >> 24); - memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); - } - } -} - -void -ImagingReduceNxN_32bpc( - Imaging imOut, Imaging imIn, int box[4], int xscale, int yscale) { - /* The most general implementation for any xscale and yscale - */ - int x, y, xx, yy; - double multiplier = 1.0 / (yscale * xscale); - - switch (imIn->type) { - case IMAGING_TYPE_INT32: - for (y = 0; y < box[3] / yscale; y++) { - int yy_from = box[1] + y * yscale; - for (x = 0; x < box[2] / xscale; x++) { - int xx_from = box[0] + x * xscale; - double ss = 0; - for (yy = yy_from; yy < yy_from + yscale - 1; yy += 2) { - INT32 *line0 = (INT32 *)imIn->image32[yy]; - INT32 *line1 = (INT32 *)imIn->image32[yy + 1]; - for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { - ss += line0[xx + 0] + line0[xx + 1] + line1[xx + 0] + - line1[xx + 1]; - } - if (xscale & 0x01) { - ss += line0[xx + 0] + line1[xx + 0]; - } - } - if (yscale & 0x01) { - INT32 *line = (INT32 *)imIn->image32[yy]; - for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { - ss += line[xx + 0] + line[xx + 1]; - } - if (xscale & 0x01) { - ss += line[xx + 0]; - } - } - IMAGING_PIXEL_I(imOut, x, y) = ROUND_UP(ss * multiplier); - } - } - break; - - case IMAGING_TYPE_FLOAT32: - for (y = 0; y < box[3] / yscale; y++) { - int yy_from = box[1] + y * yscale; - for (x = 0; x < box[2] / xscale; x++) { - int xx_from = box[0] + x * xscale; - double ss = 0; - for (yy = yy_from; yy < yy_from + yscale - 1; yy += 2) { - FLOAT32 *line0 = (FLOAT32 *)imIn->image32[yy]; - FLOAT32 *line1 = (FLOAT32 *)imIn->image32[yy + 1]; - for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { - ss += line0[xx + 0] + line0[xx + 1] + line1[xx + 0] + - line1[xx + 1]; - } - if (xscale & 0x01) { - ss += line0[xx + 0] + line1[xx + 0]; - } - } - if (yscale & 0x01) { - FLOAT32 *line = (FLOAT32 *)imIn->image32[yy]; - for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { - ss += line[xx + 0] + line[xx + 1]; - } - if (xscale & 0x01) { - ss += line[xx + 0]; - } - } - IMAGING_PIXEL_F(imOut, x, y) = ss * multiplier; - } - } - break; - } -} - -void -ImagingReduceCorners_32bpc( - Imaging imOut, Imaging imIn, int box[4], int xscale, int yscale) { - /* Fill the last row and the last column for any xscale and yscale. - */ - int x, y, xx, yy; - - switch (imIn->type) { - case IMAGING_TYPE_INT32: - if (box[2] % xscale) { - double multiplier = 1.0 / ((box[2] % xscale) * yscale); - for (y = 0; y < box[3] / yscale; y++) { - int yy_from = box[1] + y * yscale; - double ss = 0; - x = box[2] / xscale; - for (yy = yy_from; yy < yy_from + yscale; yy++) { - INT32 *line = (INT32 *)imIn->image32[yy]; - for (xx = box[0] + x * xscale; xx < box[0] + box[2]; xx++) { - ss += line[xx + 0]; - } - } - IMAGING_PIXEL_I(imOut, x, y) = ROUND_UP(ss * multiplier); - } - } - if (box[3] % yscale) { - double multiplier = 1.0 / (xscale * (box[3] % yscale)); - y = box[3] / yscale; - for (x = 0; x < box[2] / xscale; x++) { - int xx_from = box[0] + x * xscale; - double ss = 0; - for (yy = box[1] + y * yscale; yy < box[1] + box[3]; yy++) { - INT32 *line = (INT32 *)imIn->image32[yy]; - for (xx = xx_from; xx < xx_from + xscale; xx++) { - ss += line[xx + 0]; - } - } - IMAGING_PIXEL_I(imOut, x, y) = ROUND_UP(ss * multiplier); - } - } - if (box[2] % xscale && box[3] % yscale) { - double multiplier = 1.0 / ((box[2] % xscale) * (box[3] % yscale)); - double ss = 0; - x = box[2] / xscale; - y = box[3] / yscale; - for (yy = box[1] + y * yscale; yy < box[1] + box[3]; yy++) { - INT32 *line = (INT32 *)imIn->image32[yy]; - for (xx = box[0] + x * xscale; xx < box[0] + box[2]; xx++) { - ss += line[xx + 0]; - } - } - IMAGING_PIXEL_I(imOut, x, y) = ROUND_UP(ss * multiplier); - } - break; - - case IMAGING_TYPE_FLOAT32: - if (box[2] % xscale) { - double multiplier = 1.0 / ((box[2] % xscale) * yscale); - for (y = 0; y < box[3] / yscale; y++) { - int yy_from = box[1] + y * yscale; - double ss = 0; - x = box[2] / xscale; - for (yy = yy_from; yy < yy_from + yscale; yy++) { - FLOAT32 *line = (FLOAT32 *)imIn->image32[yy]; - for (xx = box[0] + x * xscale; xx < box[0] + box[2]; xx++) { - ss += line[xx + 0]; - } - } - IMAGING_PIXEL_F(imOut, x, y) = ss * multiplier; - } - } - if (box[3] % yscale) { - double multiplier = 1.0 / (xscale * (box[3] % yscale)); - y = box[3] / yscale; - for (x = 0; x < box[2] / xscale; x++) { - int xx_from = box[0] + x * xscale; - double ss = 0; - for (yy = box[1] + y * yscale; yy < box[1] + box[3]; yy++) { - FLOAT32 *line = (FLOAT32 *)imIn->image32[yy]; - for (xx = xx_from; xx < xx_from + xscale; xx++) { - ss += line[xx + 0]; - } - } - IMAGING_PIXEL_F(imOut, x, y) = ss * multiplier; - } - } - if (box[2] % xscale && box[3] % yscale) { - double multiplier = 1.0 / ((box[2] % xscale) * (box[3] % yscale)); - double ss = 0; - x = box[2] / xscale; - y = box[3] / yscale; - for (yy = box[1] + y * yscale; yy < box[1] + box[3]; yy++) { - FLOAT32 *line = (FLOAT32 *)imIn->image32[yy]; - for (xx = box[0] + x * xscale; xx < box[0] + box[2]; xx++) { - ss += line[xx + 0]; - } - } - IMAGING_PIXEL_F(imOut, x, y) = ss * multiplier; - } - break; - } -} - -Imaging -ImagingReduce(Imaging imIn, int xscale, int yscale, int box[4]) { - ImagingSectionCookie cookie; - Imaging imOut = NULL; - - if (strcmp(imIn->mode, "P") == 0 || strcmp(imIn->mode, "1") == 0) { - return (Imaging)ImagingError_ModeError(); - } - - if (imIn->type == IMAGING_TYPE_SPECIAL) { - return (Imaging)ImagingError_ModeError(); - } - - imOut = ImagingNewDirty( - imIn->mode, (box[2] + xscale - 1) / xscale, (box[3] + yscale - 1) / yscale); - if (!imOut) { - return NULL; - } - - ImagingSectionEnter(&cookie); - - switch (imIn->type) { - case IMAGING_TYPE_UINT8: - if (xscale == 1) { - if (yscale == 2) { - ImagingReduce1x2(imOut, imIn, box); - } else if (yscale == 3) { - ImagingReduce1x3(imOut, imIn, box); - } else { - ImagingReduce1xN(imOut, imIn, box, yscale); - } - } else if (yscale == 1) { - if (xscale == 2) { - ImagingReduce2x1(imOut, imIn, box); - } else if (xscale == 3) { - ImagingReduce3x1(imOut, imIn, box); - } else { - ImagingReduceNx1(imOut, imIn, box, xscale); - } - } else if (xscale == yscale && xscale <= 5) { - if (xscale == 2) { - ImagingReduce2x2(imOut, imIn, box); - } else if (xscale == 3) { - ImagingReduce3x3(imOut, imIn, box); - } else if (xscale == 4) { - ImagingReduce4x4(imOut, imIn, box); - } else { - ImagingReduce5x5(imOut, imIn, box); - } - } else { - ImagingReduceNxN(imOut, imIn, box, xscale, yscale); - } - - ImagingReduceCorners(imOut, imIn, box, xscale, yscale); - break; - - case IMAGING_TYPE_INT32: - case IMAGING_TYPE_FLOAT32: - ImagingReduceNxN_32bpc(imOut, imIn, box, xscale, yscale); - - ImagingReduceCorners_32bpc(imOut, imIn, box, xscale, yscale); - break; - } - - ImagingSectionLeave(&cookie); - - return imOut; -} +#include "Imaging.h" + +#include + +#define ROUND_UP(f) ((int)((f) >= 0.0 ? (f) + 0.5F : (f) - 0.5F)) + +UINT32 +division_UINT32(int divider, int result_bits) { + UINT32 max_dividend = (1 << result_bits) * divider; + float max_int = (1 << 30) * 4.0; + return (UINT32)(max_int / max_dividend); +} + +void +ImagingReduceNxN(Imaging imOut, Imaging imIn, int box[4], int xscale, int yscale) { + /* The most general implementation for any xscale and yscale + */ + int x, y, xx, yy; + UINT32 multiplier = division_UINT32(yscale * xscale, 8); + UINT32 amend = yscale * xscale / 2; + + if (imIn->image8) { + for (y = 0; y < box[3] / yscale; y++) { + int yy_from = box[1] + y * yscale; + for (x = 0; x < box[2] / xscale; x++) { + int xx_from = box[0] + x * xscale; + UINT32 ss = amend; + for (yy = yy_from; yy < yy_from + yscale - 1; yy += 2) { + UINT8 *line0 = (UINT8 *)imIn->image8[yy]; + UINT8 *line1 = (UINT8 *)imIn->image8[yy + 1]; + for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { + ss += line0[xx + 0] + line0[xx + 1] + line1[xx + 0] + + line1[xx + 1]; + } + if (xscale & 0x01) { + ss += line0[xx + 0] + line1[xx + 0]; + } + } + if (yscale & 0x01) { + UINT8 *line = (UINT8 *)imIn->image8[yy]; + for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { + ss += line[xx + 0] + line[xx + 1]; + } + if (xscale & 0x01) { + ss += line[xx + 0]; + } + } + imOut->image8[y][x] = (ss * multiplier) >> 24; + } + } + } else { + for (y = 0; y < box[3] / yscale; y++) { + int yy_from = box[1] + y * yscale; + if (imIn->bands == 2) { + for (x = 0; x < box[2] / xscale; x++) { + int xx_from = box[0] + x * xscale; + UINT32 v; + UINT32 ss0 = amend, ss3 = amend; + for (yy = yy_from; yy < yy_from + yscale - 1; yy += 2) { + UINT8 *line0 = (UINT8 *)imIn->image[yy]; + UINT8 *line1 = (UINT8 *)imIn->image[yy + 1]; + for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { + ss0 += line0[xx * 4 + 0] + line0[xx * 4 + 4] + + line1[xx * 4 + 0] + line1[xx * 4 + 4]; + ss3 += line0[xx * 4 + 3] + line0[xx * 4 + 7] + + line1[xx * 4 + 3] + line1[xx * 4 + 7]; + } + if (xscale & 0x01) { + ss0 += line0[xx * 4 + 0] + line1[xx * 4 + 0]; + ss3 += line0[xx * 4 + 3] + line1[xx * 4 + 3]; + } + } + if (yscale & 0x01) { + UINT8 *line = (UINT8 *)imIn->image[yy]; + for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { + ss0 += line[xx * 4 + 0] + line[xx * 4 + 4]; + ss3 += line[xx * 4 + 3] + line[xx * 4 + 7]; + } + if (xscale & 0x01) { + ss0 += line[xx * 4 + 0]; + ss3 += line[xx * 4 + 3]; + } + } + v = MAKE_UINT32( + (ss0 * multiplier) >> 24, 0, 0, (ss3 * multiplier) >> 24); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else if (imIn->bands == 3) { + for (x = 0; x < box[2] / xscale; x++) { + int xx_from = box[0] + x * xscale; + UINT32 v; + UINT32 ss0 = amend, ss1 = amend, ss2 = amend; + for (yy = yy_from; yy < yy_from + yscale - 1; yy += 2) { + UINT8 *line0 = (UINT8 *)imIn->image[yy]; + UINT8 *line1 = (UINT8 *)imIn->image[yy + 1]; + for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { + ss0 += line0[xx * 4 + 0] + line0[xx * 4 + 4] + + line1[xx * 4 + 0] + line1[xx * 4 + 4]; + ss1 += line0[xx * 4 + 1] + line0[xx * 4 + 5] + + line1[xx * 4 + 1] + line1[xx * 4 + 5]; + ss2 += line0[xx * 4 + 2] + line0[xx * 4 + 6] + + line1[xx * 4 + 2] + line1[xx * 4 + 6]; + } + if (xscale & 0x01) { + ss0 += line0[xx * 4 + 0] + line1[xx * 4 + 0]; + ss1 += line0[xx * 4 + 1] + line1[xx * 4 + 1]; + ss2 += line0[xx * 4 + 2] + line1[xx * 4 + 2]; + } + } + if (yscale & 0x01) { + UINT8 *line = (UINT8 *)imIn->image[yy]; + for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { + ss0 += line[xx * 4 + 0] + line[xx * 4 + 4]; + ss1 += line[xx * 4 + 1] + line[xx * 4 + 5]; + ss2 += line[xx * 4 + 2] + line[xx * 4 + 6]; + } + if (xscale & 0x01) { + ss0 += line[xx * 4 + 0]; + ss1 += line[xx * 4 + 1]; + ss2 += line[xx * 4 + 2]; + } + } + v = MAKE_UINT32( + (ss0 * multiplier) >> 24, + (ss1 * multiplier) >> 24, + (ss2 * multiplier) >> 24, + 0); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else { // bands == 4 + for (x = 0; x < box[2] / xscale; x++) { + int xx_from = box[0] + x * xscale; + UINT32 v; + UINT32 ss0 = amend, ss1 = amend, ss2 = amend, ss3 = amend; + for (yy = yy_from; yy < yy_from + yscale - 1; yy += 2) { + UINT8 *line0 = (UINT8 *)imIn->image[yy]; + UINT8 *line1 = (UINT8 *)imIn->image[yy + 1]; + for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { + ss0 += line0[xx * 4 + 0] + line0[xx * 4 + 4] + + line1[xx * 4 + 0] + line1[xx * 4 + 4]; + ss1 += line0[xx * 4 + 1] + line0[xx * 4 + 5] + + line1[xx * 4 + 1] + line1[xx * 4 + 5]; + ss2 += line0[xx * 4 + 2] + line0[xx * 4 + 6] + + line1[xx * 4 + 2] + line1[xx * 4 + 6]; + ss3 += line0[xx * 4 + 3] + line0[xx * 4 + 7] + + line1[xx * 4 + 3] + line1[xx * 4 + 7]; + } + if (xscale & 0x01) { + ss0 += line0[xx * 4 + 0] + line1[xx * 4 + 0]; + ss1 += line0[xx * 4 + 1] + line1[xx * 4 + 1]; + ss2 += line0[xx * 4 + 2] + line1[xx * 4 + 2]; + ss3 += line0[xx * 4 + 3] + line1[xx * 4 + 3]; + } + } + if (yscale & 0x01) { + UINT8 *line = (UINT8 *)imIn->image[yy]; + for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { + ss0 += line[xx * 4 + 0] + line[xx * 4 + 4]; + ss1 += line[xx * 4 + 1] + line[xx * 4 + 5]; + ss2 += line[xx * 4 + 2] + line[xx * 4 + 6]; + ss3 += line[xx * 4 + 3] + line[xx * 4 + 7]; + } + if (xscale & 0x01) { + ss0 += line[xx * 4 + 0]; + ss1 += line[xx * 4 + 1]; + ss2 += line[xx * 4 + 2]; + ss3 += line[xx * 4 + 3]; + } + } + v = MAKE_UINT32( + (ss0 * multiplier) >> 24, + (ss1 * multiplier) >> 24, + (ss2 * multiplier) >> 24, + (ss3 * multiplier) >> 24); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } + } + } +} + +void +ImagingReduce1xN(Imaging imOut, Imaging imIn, int box[4], int yscale) { + /* Optimized implementation for xscale = 1. + */ + int x, y, yy; + int xscale = 1; + UINT32 multiplier = division_UINT32(yscale * xscale, 8); + UINT32 amend = yscale * xscale / 2; + + if (imIn->image8) { + for (y = 0; y < box[3] / yscale; y++) { + int yy_from = box[1] + y * yscale; + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 ss = amend; + for (yy = yy_from; yy < yy_from + yscale - 1; yy += 2) { + UINT8 *line0 = (UINT8 *)imIn->image8[yy]; + UINT8 *line1 = (UINT8 *)imIn->image8[yy + 1]; + ss += line0[xx + 0] + line1[xx + 0]; + } + if (yscale & 0x01) { + UINT8 *line = (UINT8 *)imIn->image8[yy]; + ss += line[xx + 0]; + } + imOut->image8[y][x] = (ss * multiplier) >> 24; + } + } + } else { + for (y = 0; y < box[3] / yscale; y++) { + int yy_from = box[1] + y * yscale; + if (imIn->bands == 2) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + UINT32 ss0 = amend, ss3 = amend; + for (yy = yy_from; yy < yy_from + yscale - 1; yy += 2) { + UINT8 *line0 = (UINT8 *)imIn->image[yy]; + UINT8 *line1 = (UINT8 *)imIn->image[yy + 1]; + ss0 += line0[xx * 4 + 0] + line1[xx * 4 + 0]; + ss3 += line0[xx * 4 + 3] + line1[xx * 4 + 3]; + } + if (yscale & 0x01) { + UINT8 *line = (UINT8 *)imIn->image[yy]; + ss0 += line[xx * 4 + 0]; + ss3 += line[xx * 4 + 3]; + } + v = MAKE_UINT32( + (ss0 * multiplier) >> 24, 0, 0, (ss3 * multiplier) >> 24); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else if (imIn->bands == 3) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + UINT32 ss0 = amend, ss1 = amend, ss2 = amend; + for (yy = yy_from; yy < yy_from + yscale - 1; yy += 2) { + UINT8 *line0 = (UINT8 *)imIn->image[yy]; + UINT8 *line1 = (UINT8 *)imIn->image[yy + 1]; + ss0 += line0[xx * 4 + 0] + line1[xx * 4 + 0]; + ss1 += line0[xx * 4 + 1] + line1[xx * 4 + 1]; + ss2 += line0[xx * 4 + 2] + line1[xx * 4 + 2]; + } + if (yscale & 0x01) { + UINT8 *line = (UINT8 *)imIn->image[yy]; + ss0 += line[xx * 4 + 0]; + ss1 += line[xx * 4 + 1]; + ss2 += line[xx * 4 + 2]; + } + v = MAKE_UINT32( + (ss0 * multiplier) >> 24, + (ss1 * multiplier) >> 24, + (ss2 * multiplier) >> 24, + 0); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else { // bands == 4 + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + UINT32 ss0 = amend, ss1 = amend, ss2 = amend, ss3 = amend; + for (yy = yy_from; yy < yy_from + yscale - 1; yy += 2) { + UINT8 *line0 = (UINT8 *)imIn->image[yy]; + UINT8 *line1 = (UINT8 *)imIn->image[yy + 1]; + ss0 += line0[xx * 4 + 0] + line1[xx * 4 + 0]; + ss1 += line0[xx * 4 + 1] + line1[xx * 4 + 1]; + ss2 += line0[xx * 4 + 2] + line1[xx * 4 + 2]; + ss3 += line0[xx * 4 + 3] + line1[xx * 4 + 3]; + } + if (yscale & 0x01) { + UINT8 *line = (UINT8 *)imIn->image[yy]; + ss0 += line[xx * 4 + 0]; + ss1 += line[xx * 4 + 1]; + ss2 += line[xx * 4 + 2]; + ss3 += line[xx * 4 + 3]; + } + v = MAKE_UINT32( + (ss0 * multiplier) >> 24, + (ss1 * multiplier) >> 24, + (ss2 * multiplier) >> 24, + (ss3 * multiplier) >> 24); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } + } + } +} + +void +ImagingReduceNx1(Imaging imOut, Imaging imIn, int box[4], int xscale) { + /* Optimized implementation for yscale = 1. + */ + int x, y, xx; + int yscale = 1; + UINT32 multiplier = division_UINT32(yscale * xscale, 8); + UINT32 amend = yscale * xscale / 2; + + if (imIn->image8) { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y * yscale; + UINT8 *line = (UINT8 *)imIn->image8[yy]; + for (x = 0; x < box[2] / xscale; x++) { + int xx_from = box[0] + x * xscale; + UINT32 ss = amend; + for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { + ss += line[xx + 0] + line[xx + 1]; + } + if (xscale & 0x01) { + ss += line[xx + 0]; + } + imOut->image8[y][x] = (ss * multiplier) >> 24; + } + } + } else { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y * yscale; + UINT8 *line = (UINT8 *)imIn->image[yy]; + if (imIn->bands == 2) { + for (x = 0; x < box[2] / xscale; x++) { + int xx_from = box[0] + x * xscale; + UINT32 v; + UINT32 ss0 = amend, ss3 = amend; + for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { + ss0 += line[xx * 4 + 0] + line[xx * 4 + 4]; + ss3 += line[xx * 4 + 3] + line[xx * 4 + 7]; + } + if (xscale & 0x01) { + ss0 += line[xx * 4 + 0]; + ss3 += line[xx * 4 + 3]; + } + v = MAKE_UINT32( + (ss0 * multiplier) >> 24, 0, 0, (ss3 * multiplier) >> 24); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else if (imIn->bands == 3) { + for (x = 0; x < box[2] / xscale; x++) { + int xx_from = box[0] + x * xscale; + UINT32 v; + UINT32 ss0 = amend, ss1 = amend, ss2 = amend; + for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { + ss0 += line[xx * 4 + 0] + line[xx * 4 + 4]; + ss1 += line[xx * 4 + 1] + line[xx * 4 + 5]; + ss2 += line[xx * 4 + 2] + line[xx * 4 + 6]; + } + if (xscale & 0x01) { + ss0 += line[xx * 4 + 0]; + ss1 += line[xx * 4 + 1]; + ss2 += line[xx * 4 + 2]; + } + v = MAKE_UINT32( + (ss0 * multiplier) >> 24, + (ss1 * multiplier) >> 24, + (ss2 * multiplier) >> 24, + 0); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else { // bands == 4 + for (x = 0; x < box[2] / xscale; x++) { + int xx_from = box[0] + x * xscale; + UINT32 v; + UINT32 ss0 = amend, ss1 = amend, ss2 = amend, ss3 = amend; + for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { + ss0 += line[xx * 4 + 0] + line[xx * 4 + 4]; + ss1 += line[xx * 4 + 1] + line[xx * 4 + 5]; + ss2 += line[xx * 4 + 2] + line[xx * 4 + 6]; + ss3 += line[xx * 4 + 3] + line[xx * 4 + 7]; + } + if (xscale & 0x01) { + ss0 += line[xx * 4 + 0]; + ss1 += line[xx * 4 + 1]; + ss2 += line[xx * 4 + 2]; + ss3 += line[xx * 4 + 3]; + } + v = MAKE_UINT32( + (ss0 * multiplier) >> 24, + (ss1 * multiplier) >> 24, + (ss2 * multiplier) >> 24, + (ss3 * multiplier) >> 24); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } + } + } +} + +void +ImagingReduce1x2(Imaging imOut, Imaging imIn, int box[4]) { + /* Optimized implementation for xscale = 1 and yscale = 2. + */ + int xscale = 1, yscale = 2; + int x, y; + UINT32 ss0, ss1, ss2, ss3; + UINT32 amend = yscale * xscale / 2; + + if (imIn->image8) { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y * yscale; + UINT8 *line0 = (UINT8 *)imIn->image8[yy + 0]; + UINT8 *line1 = (UINT8 *)imIn->image8[yy + 1]; + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + ss0 = line0[xx + 0] + line1[xx + 0]; + imOut->image8[y][x] = (ss0 + amend) >> 1; + } + } + } else { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y * yscale; + UINT8 *line0 = (UINT8 *)imIn->image[yy + 0]; + UINT8 *line1 = (UINT8 *)imIn->image[yy + 1]; + if (imIn->bands == 2) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + ss0 = line0[xx * 4 + 0] + line1[xx * 4 + 0]; + ss3 = line0[xx * 4 + 3] + line1[xx * 4 + 3]; + v = MAKE_UINT32((ss0 + amend) >> 1, 0, 0, (ss3 + amend) >> 1); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else if (imIn->bands == 3) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + ss0 = line0[xx * 4 + 0] + line1[xx * 4 + 0]; + ss1 = line0[xx * 4 + 1] + line1[xx * 4 + 1]; + ss2 = line0[xx * 4 + 2] + line1[xx * 4 + 2]; + v = MAKE_UINT32( + (ss0 + amend) >> 1, (ss1 + amend) >> 1, (ss2 + amend) >> 1, 0); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else { // bands == 4 + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + ss0 = line0[xx * 4 + 0] + line1[xx * 4 + 0]; + ss1 = line0[xx * 4 + 1] + line1[xx * 4 + 1]; + ss2 = line0[xx * 4 + 2] + line1[xx * 4 + 2]; + ss3 = line0[xx * 4 + 3] + line1[xx * 4 + 3]; + v = MAKE_UINT32( + (ss0 + amend) >> 1, + (ss1 + amend) >> 1, + (ss2 + amend) >> 1, + (ss3 + amend) >> 1); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } + } + } +} + +void +ImagingReduce2x1(Imaging imOut, Imaging imIn, int box[4]) { + /* Optimized implementation for xscale = 2 and yscale = 1. + */ + int xscale = 2, yscale = 1; + int x, y; + UINT32 ss0, ss1, ss2, ss3; + UINT32 amend = yscale * xscale / 2; + + if (imIn->image8) { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y * yscale; + UINT8 *line0 = (UINT8 *)imIn->image8[yy + 0]; + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + ss0 = line0[xx + 0] + line0[xx + 1]; + imOut->image8[y][x] = (ss0 + amend) >> 1; + } + } + } else { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y * yscale; + UINT8 *line0 = (UINT8 *)imIn->image[yy + 0]; + if (imIn->bands == 2) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4]; + ss3 = line0[xx * 4 + 3] + line0[xx * 4 + 7]; + v = MAKE_UINT32((ss0 + amend) >> 1, 0, 0, (ss3 + amend) >> 1); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else if (imIn->bands == 3) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4]; + ss1 = line0[xx * 4 + 1] + line0[xx * 4 + 5]; + ss2 = line0[xx * 4 + 2] + line0[xx * 4 + 6]; + v = MAKE_UINT32( + (ss0 + amend) >> 1, (ss1 + amend) >> 1, (ss2 + amend) >> 1, 0); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else { // bands == 4 + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4]; + ss1 = line0[xx * 4 + 1] + line0[xx * 4 + 5]; + ss2 = line0[xx * 4 + 2] + line0[xx * 4 + 6]; + ss3 = line0[xx * 4 + 3] + line0[xx * 4 + 7]; + v = MAKE_UINT32( + (ss0 + amend) >> 1, + (ss1 + amend) >> 1, + (ss2 + amend) >> 1, + (ss3 + amend) >> 1); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } + } + } +} + +void +ImagingReduce2x2(Imaging imOut, Imaging imIn, int box[4]) { + /* Optimized implementation for xscale = 2 and yscale = 2. + */ + int xscale = 2, yscale = 2; + int x, y; + UINT32 ss0, ss1, ss2, ss3; + UINT32 amend = yscale * xscale / 2; + + if (imIn->image8) { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y * yscale; + UINT8 *line0 = (UINT8 *)imIn->image8[yy + 0]; + UINT8 *line1 = (UINT8 *)imIn->image8[yy + 1]; + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + ss0 = line0[xx + 0] + line0[xx + 1] + line1[xx + 0] + line1[xx + 1]; + imOut->image8[y][x] = (ss0 + amend) >> 2; + } + } + } else { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y * yscale; + UINT8 *line0 = (UINT8 *)imIn->image[yy + 0]; + UINT8 *line1 = (UINT8 *)imIn->image[yy + 1]; + if (imIn->bands == 2) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4] + line1[xx * 4 + 0] + + line1[xx * 4 + 4]; + ss3 = line0[xx * 4 + 3] + line0[xx * 4 + 7] + line1[xx * 4 + 3] + + line1[xx * 4 + 7]; + v = MAKE_UINT32((ss0 + amend) >> 2, 0, 0, (ss3 + amend) >> 2); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else if (imIn->bands == 3) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4] + line1[xx * 4 + 0] + + line1[xx * 4 + 4]; + ss1 = line0[xx * 4 + 1] + line0[xx * 4 + 5] + line1[xx * 4 + 1] + + line1[xx * 4 + 5]; + ss2 = line0[xx * 4 + 2] + line0[xx * 4 + 6] + line1[xx * 4 + 2] + + line1[xx * 4 + 6]; + v = MAKE_UINT32( + (ss0 + amend) >> 2, (ss1 + amend) >> 2, (ss2 + amend) >> 2, 0); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else { // bands == 4 + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4] + line1[xx * 4 + 0] + + line1[xx * 4 + 4]; + ss1 = line0[xx * 4 + 1] + line0[xx * 4 + 5] + line1[xx * 4 + 1] + + line1[xx * 4 + 5]; + ss2 = line0[xx * 4 + 2] + line0[xx * 4 + 6] + line1[xx * 4 + 2] + + line1[xx * 4 + 6]; + ss3 = line0[xx * 4 + 3] + line0[xx * 4 + 7] + line1[xx * 4 + 3] + + line1[xx * 4 + 7]; + v = MAKE_UINT32( + (ss0 + amend) >> 2, + (ss1 + amend) >> 2, + (ss2 + amend) >> 2, + (ss3 + amend) >> 2); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } + } + } +} + +void +ImagingReduce1x3(Imaging imOut, Imaging imIn, int box[4]) { + /* Optimized implementation for xscale = 1 and yscale = 3. + */ + int xscale = 1, yscale = 3; + int x, y; + UINT32 ss0, ss1, ss2, ss3; + UINT32 multiplier = division_UINT32(yscale * xscale, 8); + UINT32 amend = yscale * xscale / 2; + + if (imIn->image8) { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y * yscale; + UINT8 *line0 = (UINT8 *)imIn->image8[yy + 0]; + UINT8 *line1 = (UINT8 *)imIn->image8[yy + 1]; + UINT8 *line2 = (UINT8 *)imIn->image8[yy + 2]; + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + ss0 = line0[xx + 0] + line1[xx + 0] + line2[xx + 0]; + imOut->image8[y][x] = ((ss0 + amend) * multiplier) >> 24; + } + } + } else { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y * yscale; + UINT8 *line0 = (UINT8 *)imIn->image[yy + 0]; + UINT8 *line1 = (UINT8 *)imIn->image[yy + 1]; + UINT8 *line2 = (UINT8 *)imIn->image[yy + 2]; + if (imIn->bands == 2) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + ss0 = line0[xx * 4 + 0] + line1[xx * 4 + 0] + line2[xx * 4 + 0]; + ss3 = line0[xx * 4 + 3] + line1[xx * 4 + 3] + line2[xx * 4 + 3]; + v = MAKE_UINT32( + ((ss0 + amend) * multiplier) >> 24, + 0, + 0, + ((ss3 + amend) * multiplier) >> 24); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else if (imIn->bands == 3) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + ss0 = line0[xx * 4 + 0] + line1[xx * 4 + 0] + line2[xx * 4 + 0]; + ss1 = line0[xx * 4 + 1] + line1[xx * 4 + 1] + line2[xx * 4 + 1]; + ss2 = line0[xx * 4 + 2] + line1[xx * 4 + 2] + line2[xx * 4 + 2]; + v = MAKE_UINT32( + ((ss0 + amend) * multiplier) >> 24, + ((ss1 + amend) * multiplier) >> 24, + ((ss2 + amend) * multiplier) >> 24, + 0); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else { // bands == 4 + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + ss0 = line0[xx * 4 + 0] + line1[xx * 4 + 0] + line2[xx * 4 + 0]; + ss1 = line0[xx * 4 + 1] + line1[xx * 4 + 1] + line2[xx * 4 + 1]; + ss2 = line0[xx * 4 + 2] + line1[xx * 4 + 2] + line2[xx * 4 + 2]; + ss3 = line0[xx * 4 + 3] + line1[xx * 4 + 3] + line2[xx * 4 + 3]; + v = MAKE_UINT32( + ((ss0 + amend) * multiplier) >> 24, + ((ss1 + amend) * multiplier) >> 24, + ((ss2 + amend) * multiplier) >> 24, + ((ss3 + amend) * multiplier) >> 24); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } + } + } +} + +void +ImagingReduce3x1(Imaging imOut, Imaging imIn, int box[4]) { + /* Optimized implementation for xscale = 3 and yscale = 1. + */ + int xscale = 3, yscale = 1; + int x, y; + UINT32 ss0, ss1, ss2, ss3; + UINT32 multiplier = division_UINT32(yscale * xscale, 8); + UINT32 amend = yscale * xscale / 2; + + if (imIn->image8) { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y * yscale; + UINT8 *line0 = (UINT8 *)imIn->image8[yy + 0]; + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + ss0 = line0[xx + 0] + line0[xx + 1] + line0[xx + 2]; + imOut->image8[y][x] = ((ss0 + amend) * multiplier) >> 24; + } + } + } else { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y * yscale; + UINT8 *line0 = (UINT8 *)imIn->image[yy + 0]; + if (imIn->bands == 2) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4] + line0[xx * 4 + 8]; + ss3 = line0[xx * 4 + 3] + line0[xx * 4 + 7] + line0[xx * 4 + 11]; + v = MAKE_UINT32( + ((ss0 + amend) * multiplier) >> 24, + 0, + 0, + ((ss3 + amend) * multiplier) >> 24); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else if (imIn->bands == 3) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4] + line0[xx * 4 + 8]; + ss1 = line0[xx * 4 + 1] + line0[xx * 4 + 5] + line0[xx * 4 + 9]; + ss2 = line0[xx * 4 + 2] + line0[xx * 4 + 6] + line0[xx * 4 + 10]; + v = MAKE_UINT32( + ((ss0 + amend) * multiplier) >> 24, + ((ss1 + amend) * multiplier) >> 24, + ((ss2 + amend) * multiplier) >> 24, + 0); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else { // bands == 4 + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4] + line0[xx * 4 + 8]; + ss1 = line0[xx * 4 + 1] + line0[xx * 4 + 5] + line0[xx * 4 + 9]; + ss2 = line0[xx * 4 + 2] + line0[xx * 4 + 6] + line0[xx * 4 + 10]; + ss3 = line0[xx * 4 + 3] + line0[xx * 4 + 7] + line0[xx * 4 + 11]; + v = MAKE_UINT32( + ((ss0 + amend) * multiplier) >> 24, + ((ss1 + amend) * multiplier) >> 24, + ((ss2 + amend) * multiplier) >> 24, + ((ss3 + amend) * multiplier) >> 24); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } + } + } +} + +void +ImagingReduce3x3(Imaging imOut, Imaging imIn, int box[4]) { + /* Optimized implementation for xscale = 3 and yscale = 3. + */ + int xscale = 3, yscale = 3; + int x, y; + UINT32 ss0, ss1, ss2, ss3; + UINT32 multiplier = division_UINT32(yscale * xscale, 8); + UINT32 amend = yscale * xscale / 2; + + if (imIn->image8) { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y * yscale; + UINT8 *line0 = (UINT8 *)imIn->image8[yy + 0]; + UINT8 *line1 = (UINT8 *)imIn->image8[yy + 1]; + UINT8 *line2 = (UINT8 *)imIn->image8[yy + 2]; + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + ss0 = line0[xx + 0] + line0[xx + 1] + line0[xx + 2] + line1[xx + 0] + + line1[xx + 1] + line1[xx + 2] + line2[xx + 0] + line2[xx + 1] + + line2[xx + 2]; + imOut->image8[y][x] = ((ss0 + amend) * multiplier) >> 24; + } + } + } else { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y * yscale; + UINT8 *line0 = (UINT8 *)imIn->image[yy + 0]; + UINT8 *line1 = (UINT8 *)imIn->image[yy + 1]; + UINT8 *line2 = (UINT8 *)imIn->image[yy + 2]; + if (imIn->bands == 2) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4] + line0[xx * 4 + 8] + + line1[xx * 4 + 0] + line1[xx * 4 + 4] + line1[xx * 4 + 8] + + line2[xx * 4 + 0] + line2[xx * 4 + 4] + line2[xx * 4 + 8]; + ss3 = line0[xx * 4 + 3] + line0[xx * 4 + 7] + line0[xx * 4 + 11] + + line1[xx * 4 + 3] + line1[xx * 4 + 7] + line1[xx * 4 + 11] + + line2[xx * 4 + 3] + line2[xx * 4 + 7] + line2[xx * 4 + 11]; + v = MAKE_UINT32( + ((ss0 + amend) * multiplier) >> 24, + 0, + 0, + ((ss3 + amend) * multiplier) >> 24); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else if (imIn->bands == 3) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4] + line0[xx * 4 + 8] + + line1[xx * 4 + 0] + line1[xx * 4 + 4] + line1[xx * 4 + 8] + + line2[xx * 4 + 0] + line2[xx * 4 + 4] + line2[xx * 4 + 8]; + ss1 = line0[xx * 4 + 1] + line0[xx * 4 + 5] + line0[xx * 4 + 9] + + line1[xx * 4 + 1] + line1[xx * 4 + 5] + line1[xx * 4 + 9] + + line2[xx * 4 + 1] + line2[xx * 4 + 5] + line2[xx * 4 + 9]; + ss2 = line0[xx * 4 + 2] + line0[xx * 4 + 6] + line0[xx * 4 + 10] + + line1[xx * 4 + 2] + line1[xx * 4 + 6] + line1[xx * 4 + 10] + + line2[xx * 4 + 2] + line2[xx * 4 + 6] + line2[xx * 4 + 10]; + v = MAKE_UINT32( + ((ss0 + amend) * multiplier) >> 24, + ((ss1 + amend) * multiplier) >> 24, + ((ss2 + amend) * multiplier) >> 24, + 0); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else { // bands == 4 + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4] + line0[xx * 4 + 8] + + line1[xx * 4 + 0] + line1[xx * 4 + 4] + line1[xx * 4 + 8] + + line2[xx * 4 + 0] + line2[xx * 4 + 4] + line2[xx * 4 + 8]; + ss1 = line0[xx * 4 + 1] + line0[xx * 4 + 5] + line0[xx * 4 + 9] + + line1[xx * 4 + 1] + line1[xx * 4 + 5] + line1[xx * 4 + 9] + + line2[xx * 4 + 1] + line2[xx * 4 + 5] + line2[xx * 4 + 9]; + ss2 = line0[xx * 4 + 2] + line0[xx * 4 + 6] + line0[xx * 4 + 10] + + line1[xx * 4 + 2] + line1[xx * 4 + 6] + line1[xx * 4 + 10] + + line2[xx * 4 + 2] + line2[xx * 4 + 6] + line2[xx * 4 + 10]; + ss3 = line0[xx * 4 + 3] + line0[xx * 4 + 7] + line0[xx * 4 + 11] + + line1[xx * 4 + 3] + line1[xx * 4 + 7] + line1[xx * 4 + 11] + + line2[xx * 4 + 3] + line2[xx * 4 + 7] + line2[xx * 4 + 11]; + v = MAKE_UINT32( + ((ss0 + amend) * multiplier) >> 24, + ((ss1 + amend) * multiplier) >> 24, + ((ss2 + amend) * multiplier) >> 24, + ((ss3 + amend) * multiplier) >> 24); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } + } + } +} + +void +ImagingReduce4x4(Imaging imOut, Imaging imIn, int box[4]) { + /* Optimized implementation for xscale = 4 and yscale = 4. + */ + int xscale = 4, yscale = 4; + int x, y; + UINT32 ss0, ss1, ss2, ss3; + UINT32 amend = yscale * xscale / 2; + + if (imIn->image8) { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y * yscale; + UINT8 *line0 = (UINT8 *)imIn->image8[yy + 0]; + UINT8 *line1 = (UINT8 *)imIn->image8[yy + 1]; + UINT8 *line2 = (UINT8 *)imIn->image8[yy + 2]; + UINT8 *line3 = (UINT8 *)imIn->image8[yy + 3]; + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + ss0 = line0[xx + 0] + line0[xx + 1] + line0[xx + 2] + line0[xx + 3] + + line1[xx + 0] + line1[xx + 1] + line1[xx + 2] + line1[xx + 3] + + line2[xx + 0] + line2[xx + 1] + line2[xx + 2] + line2[xx + 3] + + line3[xx + 0] + line3[xx + 1] + line3[xx + 2] + line3[xx + 3]; + imOut->image8[y][x] = (ss0 + amend) >> 4; + } + } + } else { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y * yscale; + UINT8 *line0 = (UINT8 *)imIn->image[yy + 0]; + UINT8 *line1 = (UINT8 *)imIn->image[yy + 1]; + UINT8 *line2 = (UINT8 *)imIn->image[yy + 2]; + UINT8 *line3 = (UINT8 *)imIn->image[yy + 3]; + if (imIn->bands == 2) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4] + line0[xx * 4 + 8] + + line0[xx * 4 + 12] + line1[xx * 4 + 0] + line1[xx * 4 + 4] + + line1[xx * 4 + 8] + line1[xx * 4 + 12] + line2[xx * 4 + 0] + + line2[xx * 4 + 4] + line2[xx * 4 + 8] + line2[xx * 4 + 12] + + line3[xx * 4 + 0] + line3[xx * 4 + 4] + line3[xx * 4 + 8] + + line3[xx * 4 + 12]; + ss3 = line0[xx * 4 + 3] + line0[xx * 4 + 7] + line0[xx * 4 + 11] + + line0[xx * 4 + 15] + line1[xx * 4 + 3] + line1[xx * 4 + 7] + + line1[xx * 4 + 11] + line1[xx * 4 + 15] + line2[xx * 4 + 3] + + line2[xx * 4 + 7] + line2[xx * 4 + 11] + line2[xx * 4 + 15] + + line3[xx * 4 + 3] + line3[xx * 4 + 7] + line3[xx * 4 + 11] + + line3[xx * 4 + 15]; + v = MAKE_UINT32((ss0 + amend) >> 4, 0, 0, (ss3 + amend) >> 4); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else if (imIn->bands == 3) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4] + line0[xx * 4 + 8] + + line0[xx * 4 + 12] + line1[xx * 4 + 0] + line1[xx * 4 + 4] + + line1[xx * 4 + 8] + line1[xx * 4 + 12] + line2[xx * 4 + 0] + + line2[xx * 4 + 4] + line2[xx * 4 + 8] + line2[xx * 4 + 12] + + line3[xx * 4 + 0] + line3[xx * 4 + 4] + line3[xx * 4 + 8] + + line3[xx * 4 + 12]; + ss1 = line0[xx * 4 + 1] + line0[xx * 4 + 5] + line0[xx * 4 + 9] + + line0[xx * 4 + 13] + line1[xx * 4 + 1] + line1[xx * 4 + 5] + + line1[xx * 4 + 9] + line1[xx * 4 + 13] + line2[xx * 4 + 1] + + line2[xx * 4 + 5] + line2[xx * 4 + 9] + line2[xx * 4 + 13] + + line3[xx * 4 + 1] + line3[xx * 4 + 5] + line3[xx * 4 + 9] + + line3[xx * 4 + 13]; + ss2 = line0[xx * 4 + 2] + line0[xx * 4 + 6] + line0[xx * 4 + 10] + + line0[xx * 4 + 14] + line1[xx * 4 + 2] + line1[xx * 4 + 6] + + line1[xx * 4 + 10] + line1[xx * 4 + 14] + line2[xx * 4 + 2] + + line2[xx * 4 + 6] + line2[xx * 4 + 10] + line2[xx * 4 + 14] + + line3[xx * 4 + 2] + line3[xx * 4 + 6] + line3[xx * 4 + 10] + + line3[xx * 4 + 14]; + v = MAKE_UINT32( + (ss0 + amend) >> 4, (ss1 + amend) >> 4, (ss2 + amend) >> 4, 0); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else { // bands == 4 + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4] + line0[xx * 4 + 8] + + line0[xx * 4 + 12] + line1[xx * 4 + 0] + line1[xx * 4 + 4] + + line1[xx * 4 + 8] + line1[xx * 4 + 12] + line2[xx * 4 + 0] + + line2[xx * 4 + 4] + line2[xx * 4 + 8] + line2[xx * 4 + 12] + + line3[xx * 4 + 0] + line3[xx * 4 + 4] + line3[xx * 4 + 8] + + line3[xx * 4 + 12]; + ss1 = line0[xx * 4 + 1] + line0[xx * 4 + 5] + line0[xx * 4 + 9] + + line0[xx * 4 + 13] + line1[xx * 4 + 1] + line1[xx * 4 + 5] + + line1[xx * 4 + 9] + line1[xx * 4 + 13] + line2[xx * 4 + 1] + + line2[xx * 4 + 5] + line2[xx * 4 + 9] + line2[xx * 4 + 13] + + line3[xx * 4 + 1] + line3[xx * 4 + 5] + line3[xx * 4 + 9] + + line3[xx * 4 + 13]; + ss2 = line0[xx * 4 + 2] + line0[xx * 4 + 6] + line0[xx * 4 + 10] + + line0[xx * 4 + 14] + line1[xx * 4 + 2] + line1[xx * 4 + 6] + + line1[xx * 4 + 10] + line1[xx * 4 + 14] + line2[xx * 4 + 2] + + line2[xx * 4 + 6] + line2[xx * 4 + 10] + line2[xx * 4 + 14] + + line3[xx * 4 + 2] + line3[xx * 4 + 6] + line3[xx * 4 + 10] + + line3[xx * 4 + 14]; + ss3 = line0[xx * 4 + 3] + line0[xx * 4 + 7] + line0[xx * 4 + 11] + + line0[xx * 4 + 15] + line1[xx * 4 + 3] + line1[xx * 4 + 7] + + line1[xx * 4 + 11] + line1[xx * 4 + 15] + line2[xx * 4 + 3] + + line2[xx * 4 + 7] + line2[xx * 4 + 11] + line2[xx * 4 + 15] + + line3[xx * 4 + 3] + line3[xx * 4 + 7] + line3[xx * 4 + 11] + + line3[xx * 4 + 15]; + v = MAKE_UINT32( + (ss0 + amend) >> 4, + (ss1 + amend) >> 4, + (ss2 + amend) >> 4, + (ss3 + amend) >> 4); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } + } + } +} + +void +ImagingReduce5x5(Imaging imOut, Imaging imIn, int box[4]) { + /* Fast special case for xscale = 5 and yscale = 5. + */ + int xscale = 5, yscale = 5; + int x, y; + UINT32 ss0, ss1, ss2, ss3; + UINT32 multiplier = division_UINT32(yscale * xscale, 8); + UINT32 amend = yscale * xscale / 2; + + if (imIn->image8) { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y * yscale; + UINT8 *line0 = (UINT8 *)imIn->image8[yy + 0]; + UINT8 *line1 = (UINT8 *)imIn->image8[yy + 1]; + UINT8 *line2 = (UINT8 *)imIn->image8[yy + 2]; + UINT8 *line3 = (UINT8 *)imIn->image8[yy + 3]; + UINT8 *line4 = (UINT8 *)imIn->image8[yy + 4]; + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + ss0 = line0[xx + 0] + line0[xx + 1] + line0[xx + 2] + line0[xx + 3] + + line0[xx + 4] + line1[xx + 0] + line1[xx + 1] + line1[xx + 2] + + line1[xx + 3] + line1[xx + 4] + line2[xx + 0] + line2[xx + 1] + + line2[xx + 2] + line2[xx + 3] + line2[xx + 4] + line3[xx + 0] + + line3[xx + 1] + line3[xx + 2] + line3[xx + 3] + line3[xx + 4] + + line4[xx + 0] + line4[xx + 1] + line4[xx + 2] + line4[xx + 3] + + line4[xx + 4]; + imOut->image8[y][x] = ((ss0 + amend) * multiplier) >> 24; + } + } + } else { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y * yscale; + UINT8 *line0 = (UINT8 *)imIn->image[yy + 0]; + UINT8 *line1 = (UINT8 *)imIn->image[yy + 1]; + UINT8 *line2 = (UINT8 *)imIn->image[yy + 2]; + UINT8 *line3 = (UINT8 *)imIn->image[yy + 3]; + UINT8 *line4 = (UINT8 *)imIn->image[yy + 4]; + if (imIn->bands == 2) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4] + line0[xx * 4 + 8] + + line0[xx * 4 + 12] + line0[xx * 4 + 16] + line1[xx * 4 + 0] + + line1[xx * 4 + 4] + line1[xx * 4 + 8] + line1[xx * 4 + 12] + + line1[xx * 4 + 16] + line2[xx * 4 + 0] + line2[xx * 4 + 4] + + line2[xx * 4 + 8] + line2[xx * 4 + 12] + line2[xx * 4 + 16] + + line3[xx * 4 + 0] + line3[xx * 4 + 4] + line3[xx * 4 + 8] + + line3[xx * 4 + 12] + line3[xx * 4 + 16] + line4[xx * 4 + 0] + + line4[xx * 4 + 4] + line4[xx * 4 + 8] + line4[xx * 4 + 12] + + line4[xx * 4 + 16]; + ss3 = line0[xx * 4 + 3] + line0[xx * 4 + 7] + line0[xx * 4 + 11] + + line0[xx * 4 + 15] + line0[xx * 4 + 19] + line1[xx * 4 + 3] + + line1[xx * 4 + 7] + line1[xx * 4 + 11] + line1[xx * 4 + 15] + + line1[xx * 4 + 19] + line2[xx * 4 + 3] + line2[xx * 4 + 7] + + line2[xx * 4 + 11] + line2[xx * 4 + 15] + line2[xx * 4 + 19] + + line3[xx * 4 + 3] + line3[xx * 4 + 7] + line3[xx * 4 + 11] + + line3[xx * 4 + 15] + line3[xx * 4 + 19] + line4[xx * 4 + 3] + + line4[xx * 4 + 7] + line4[xx * 4 + 11] + line4[xx * 4 + 15] + + line4[xx * 4 + 19]; + v = MAKE_UINT32( + ((ss0 + amend) * multiplier) >> 24, + 0, + 0, + ((ss3 + amend) * multiplier) >> 24); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else if (imIn->bands == 3) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4] + line0[xx * 4 + 8] + + line0[xx * 4 + 12] + line0[xx * 4 + 16] + line1[xx * 4 + 0] + + line1[xx * 4 + 4] + line1[xx * 4 + 8] + line1[xx * 4 + 12] + + line1[xx * 4 + 16] + line2[xx * 4 + 0] + line2[xx * 4 + 4] + + line2[xx * 4 + 8] + line2[xx * 4 + 12] + line2[xx * 4 + 16] + + line3[xx * 4 + 0] + line3[xx * 4 + 4] + line3[xx * 4 + 8] + + line3[xx * 4 + 12] + line3[xx * 4 + 16] + line4[xx * 4 + 0] + + line4[xx * 4 + 4] + line4[xx * 4 + 8] + line4[xx * 4 + 12] + + line4[xx * 4 + 16]; + ss1 = line0[xx * 4 + 1] + line0[xx * 4 + 5] + line0[xx * 4 + 9] + + line0[xx * 4 + 13] + line0[xx * 4 + 17] + line1[xx * 4 + 1] + + line1[xx * 4 + 5] + line1[xx * 4 + 9] + line1[xx * 4 + 13] + + line1[xx * 4 + 17] + line2[xx * 4 + 1] + line2[xx * 4 + 5] + + line2[xx * 4 + 9] + line2[xx * 4 + 13] + line2[xx * 4 + 17] + + line3[xx * 4 + 1] + line3[xx * 4 + 5] + line3[xx * 4 + 9] + + line3[xx * 4 + 13] + line3[xx * 4 + 17] + line4[xx * 4 + 1] + + line4[xx * 4 + 5] + line4[xx * 4 + 9] + line4[xx * 4 + 13] + + line4[xx * 4 + 17]; + ss2 = line0[xx * 4 + 2] + line0[xx * 4 + 6] + line0[xx * 4 + 10] + + line0[xx * 4 + 14] + line0[xx * 4 + 18] + line1[xx * 4 + 2] + + line1[xx * 4 + 6] + line1[xx * 4 + 10] + line1[xx * 4 + 14] + + line1[xx * 4 + 18] + line2[xx * 4 + 2] + line2[xx * 4 + 6] + + line2[xx * 4 + 10] + line2[xx * 4 + 14] + line2[xx * 4 + 18] + + line3[xx * 4 + 2] + line3[xx * 4 + 6] + line3[xx * 4 + 10] + + line3[xx * 4 + 14] + line3[xx * 4 + 18] + line4[xx * 4 + 2] + + line4[xx * 4 + 6] + line4[xx * 4 + 10] + line4[xx * 4 + 14] + + line4[xx * 4 + 18]; + v = MAKE_UINT32( + ((ss0 + amend) * multiplier) >> 24, + ((ss1 + amend) * multiplier) >> 24, + ((ss2 + amend) * multiplier) >> 24, + 0); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else { // bands == 4 + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4] + line0[xx * 4 + 8] + + line0[xx * 4 + 12] + line0[xx * 4 + 16] + line1[xx * 4 + 0] + + line1[xx * 4 + 4] + line1[xx * 4 + 8] + line1[xx * 4 + 12] + + line1[xx * 4 + 16] + line2[xx * 4 + 0] + line2[xx * 4 + 4] + + line2[xx * 4 + 8] + line2[xx * 4 + 12] + line2[xx * 4 + 16] + + line3[xx * 4 + 0] + line3[xx * 4 + 4] + line3[xx * 4 + 8] + + line3[xx * 4 + 12] + line3[xx * 4 + 16] + line4[xx * 4 + 0] + + line4[xx * 4 + 4] + line4[xx * 4 + 8] + line4[xx * 4 + 12] + + line4[xx * 4 + 16]; + ss1 = line0[xx * 4 + 1] + line0[xx * 4 + 5] + line0[xx * 4 + 9] + + line0[xx * 4 + 13] + line0[xx * 4 + 17] + line1[xx * 4 + 1] + + line1[xx * 4 + 5] + line1[xx * 4 + 9] + line1[xx * 4 + 13] + + line1[xx * 4 + 17] + line2[xx * 4 + 1] + line2[xx * 4 + 5] + + line2[xx * 4 + 9] + line2[xx * 4 + 13] + line2[xx * 4 + 17] + + line3[xx * 4 + 1] + line3[xx * 4 + 5] + line3[xx * 4 + 9] + + line3[xx * 4 + 13] + line3[xx * 4 + 17] + line4[xx * 4 + 1] + + line4[xx * 4 + 5] + line4[xx * 4 + 9] + line4[xx * 4 + 13] + + line4[xx * 4 + 17]; + ss2 = line0[xx * 4 + 2] + line0[xx * 4 + 6] + line0[xx * 4 + 10] + + line0[xx * 4 + 14] + line0[xx * 4 + 18] + line1[xx * 4 + 2] + + line1[xx * 4 + 6] + line1[xx * 4 + 10] + line1[xx * 4 + 14] + + line1[xx * 4 + 18] + line2[xx * 4 + 2] + line2[xx * 4 + 6] + + line2[xx * 4 + 10] + line2[xx * 4 + 14] + line2[xx * 4 + 18] + + line3[xx * 4 + 2] + line3[xx * 4 + 6] + line3[xx * 4 + 10] + + line3[xx * 4 + 14] + line3[xx * 4 + 18] + line4[xx * 4 + 2] + + line4[xx * 4 + 6] + line4[xx * 4 + 10] + line4[xx * 4 + 14] + + line4[xx * 4 + 18]; + ss3 = line0[xx * 4 + 3] + line0[xx * 4 + 7] + line0[xx * 4 + 11] + + line0[xx * 4 + 15] + line0[xx * 4 + 19] + line1[xx * 4 + 3] + + line1[xx * 4 + 7] + line1[xx * 4 + 11] + line1[xx * 4 + 15] + + line1[xx * 4 + 19] + line2[xx * 4 + 3] + line2[xx * 4 + 7] + + line2[xx * 4 + 11] + line2[xx * 4 + 15] + line2[xx * 4 + 19] + + line3[xx * 4 + 3] + line3[xx * 4 + 7] + line3[xx * 4 + 11] + + line3[xx * 4 + 15] + line3[xx * 4 + 19] + line4[xx * 4 + 3] + + line4[xx * 4 + 7] + line4[xx * 4 + 11] + line4[xx * 4 + 15] + + line4[xx * 4 + 19]; + v = MAKE_UINT32( + ((ss0 + amend) * multiplier) >> 24, + ((ss1 + amend) * multiplier) >> 24, + ((ss2 + amend) * multiplier) >> 24, + ((ss3 + amend) * multiplier) >> 24); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } + } + } +} + +void +ImagingReduceCorners(Imaging imOut, Imaging imIn, int box[4], int xscale, int yscale) { + /* Fill the last row and the last column for any xscale and yscale. + */ + int x, y, xx, yy; + + if (imIn->image8) { + if (box[2] % xscale) { + int scale = (box[2] % xscale) * yscale; + UINT32 multiplier = division_UINT32(scale, 8); + UINT32 amend = scale / 2; + for (y = 0; y < box[3] / yscale; y++) { + int yy_from = box[1] + y * yscale; + UINT32 ss = amend; + x = box[2] / xscale; + + for (yy = yy_from; yy < yy_from + yscale; yy++) { + UINT8 *line = (UINT8 *)imIn->image8[yy]; + for (xx = box[0] + x * xscale; xx < box[0] + box[2]; xx++) { + ss += line[xx + 0]; + } + } + imOut->image8[y][x] = (ss * multiplier) >> 24; + } + } + if (box[3] % yscale) { + int scale = xscale * (box[3] % yscale); + UINT32 multiplier = division_UINT32(scale, 8); + UINT32 amend = scale / 2; + y = box[3] / yscale; + for (x = 0; x < box[2] / xscale; x++) { + int xx_from = box[0] + x * xscale; + UINT32 ss = amend; + for (yy = box[1] + y * yscale; yy < box[1] + box[3]; yy++) { + UINT8 *line = (UINT8 *)imIn->image8[yy]; + for (xx = xx_from; xx < xx_from + xscale; xx++) { + ss += line[xx + 0]; + } + } + imOut->image8[y][x] = (ss * multiplier) >> 24; + } + } + if (box[2] % xscale && box[3] % yscale) { + int scale = (box[2] % xscale) * (box[3] % yscale); + UINT32 multiplier = division_UINT32(scale, 8); + UINT32 amend = scale / 2; + UINT32 ss = amend; + x = box[2] / xscale; + y = box[3] / yscale; + for (yy = box[1] + y * yscale; yy < box[1] + box[3]; yy++) { + UINT8 *line = (UINT8 *)imIn->image8[yy]; + for (xx = box[0] + x * xscale; xx < box[0] + box[2]; xx++) { + ss += line[xx + 0]; + } + } + imOut->image8[y][x] = (ss * multiplier) >> 24; + } + } else { + if (box[2] % xscale) { + int scale = (box[2] % xscale) * yscale; + UINT32 multiplier = division_UINT32(scale, 8); + UINT32 amend = scale / 2; + for (y = 0; y < box[3] / yscale; y++) { + int yy_from = box[1] + y * yscale; + UINT32 v; + UINT32 ss0 = amend, ss1 = amend, ss2 = amend, ss3 = amend; + x = box[2] / xscale; + + for (yy = yy_from; yy < yy_from + yscale; yy++) { + UINT8 *line = (UINT8 *)imIn->image[yy]; + for (xx = box[0] + x * xscale; xx < box[0] + box[2]; xx++) { + ss0 += line[xx * 4 + 0]; + ss1 += line[xx * 4 + 1]; + ss2 += line[xx * 4 + 2]; + ss3 += line[xx * 4 + 3]; + } + } + v = MAKE_UINT32( + (ss0 * multiplier) >> 24, + (ss1 * multiplier) >> 24, + (ss2 * multiplier) >> 24, + (ss3 * multiplier) >> 24); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } + if (box[3] % yscale) { + int scale = xscale * (box[3] % yscale); + UINT32 multiplier = division_UINT32(scale, 8); + UINT32 amend = scale / 2; + y = box[3] / yscale; + for (x = 0; x < box[2] / xscale; x++) { + int xx_from = box[0] + x * xscale; + UINT32 v; + UINT32 ss0 = amend, ss1 = amend, ss2 = amend, ss3 = amend; + for (yy = box[1] + y * yscale; yy < box[1] + box[3]; yy++) { + UINT8 *line = (UINT8 *)imIn->image[yy]; + for (xx = xx_from; xx < xx_from + xscale; xx++) { + ss0 += line[xx * 4 + 0]; + ss1 += line[xx * 4 + 1]; + ss2 += line[xx * 4 + 2]; + ss3 += line[xx * 4 + 3]; + } + } + v = MAKE_UINT32( + (ss0 * multiplier) >> 24, + (ss1 * multiplier) >> 24, + (ss2 * multiplier) >> 24, + (ss3 * multiplier) >> 24); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } + if (box[2] % xscale && box[3] % yscale) { + int scale = (box[2] % xscale) * (box[3] % yscale); + UINT32 multiplier = division_UINT32(scale, 8); + UINT32 amend = scale / 2; + UINT32 v; + UINT32 ss0 = amend, ss1 = amend, ss2 = amend, ss3 = amend; + x = box[2] / xscale; + y = box[3] / yscale; + for (yy = box[1] + y * yscale; yy < box[1] + box[3]; yy++) { + UINT8 *line = (UINT8 *)imIn->image[yy]; + for (xx = box[0] + x * xscale; xx < box[0] + box[2]; xx++) { + ss0 += line[xx * 4 + 0]; + ss1 += line[xx * 4 + 1]; + ss2 += line[xx * 4 + 2]; + ss3 += line[xx * 4 + 3]; + } + } + v = MAKE_UINT32( + (ss0 * multiplier) >> 24, + (ss1 * multiplier) >> 24, + (ss2 * multiplier) >> 24, + (ss3 * multiplier) >> 24); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } +} + +void +ImagingReduceNxN_32bpc( + Imaging imOut, Imaging imIn, int box[4], int xscale, int yscale) { + /* The most general implementation for any xscale and yscale + */ + int x, y, xx, yy; + double multiplier = 1.0 / (yscale * xscale); + + switch (imIn->type) { + case IMAGING_TYPE_INT32: + for (y = 0; y < box[3] / yscale; y++) { + int yy_from = box[1] + y * yscale; + for (x = 0; x < box[2] / xscale; x++) { + int xx_from = box[0] + x * xscale; + double ss = 0; + for (yy = yy_from; yy < yy_from + yscale - 1; yy += 2) { + INT32 *line0 = (INT32 *)imIn->image32[yy]; + INT32 *line1 = (INT32 *)imIn->image32[yy + 1]; + for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { + ss += line0[xx + 0] + line0[xx + 1] + line1[xx + 0] + + line1[xx + 1]; + } + if (xscale & 0x01) { + ss += line0[xx + 0] + line1[xx + 0]; + } + } + if (yscale & 0x01) { + INT32 *line = (INT32 *)imIn->image32[yy]; + for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { + ss += line[xx + 0] + line[xx + 1]; + } + if (xscale & 0x01) { + ss += line[xx + 0]; + } + } + IMAGING_PIXEL_I(imOut, x, y) = ROUND_UP(ss * multiplier); + } + } + break; + + case IMAGING_TYPE_FLOAT32: + for (y = 0; y < box[3] / yscale; y++) { + int yy_from = box[1] + y * yscale; + for (x = 0; x < box[2] / xscale; x++) { + int xx_from = box[0] + x * xscale; + double ss = 0; + for (yy = yy_from; yy < yy_from + yscale - 1; yy += 2) { + FLOAT32 *line0 = (FLOAT32 *)imIn->image32[yy]; + FLOAT32 *line1 = (FLOAT32 *)imIn->image32[yy + 1]; + for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { + ss += line0[xx + 0] + line0[xx + 1] + line1[xx + 0] + + line1[xx + 1]; + } + if (xscale & 0x01) { + ss += line0[xx + 0] + line1[xx + 0]; + } + } + if (yscale & 0x01) { + FLOAT32 *line = (FLOAT32 *)imIn->image32[yy]; + for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { + ss += line[xx + 0] + line[xx + 1]; + } + if (xscale & 0x01) { + ss += line[xx + 0]; + } + } + IMAGING_PIXEL_F(imOut, x, y) = ss * multiplier; + } + } + break; + } +} + +void +ImagingReduceCorners_32bpc( + Imaging imOut, Imaging imIn, int box[4], int xscale, int yscale) { + /* Fill the last row and the last column for any xscale and yscale. + */ + int x, y, xx, yy; + + switch (imIn->type) { + case IMAGING_TYPE_INT32: + if (box[2] % xscale) { + double multiplier = 1.0 / ((box[2] % xscale) * yscale); + for (y = 0; y < box[3] / yscale; y++) { + int yy_from = box[1] + y * yscale; + double ss = 0; + x = box[2] / xscale; + for (yy = yy_from; yy < yy_from + yscale; yy++) { + INT32 *line = (INT32 *)imIn->image32[yy]; + for (xx = box[0] + x * xscale; xx < box[0] + box[2]; xx++) { + ss += line[xx + 0]; + } + } + IMAGING_PIXEL_I(imOut, x, y) = ROUND_UP(ss * multiplier); + } + } + if (box[3] % yscale) { + double multiplier = 1.0 / (xscale * (box[3] % yscale)); + y = box[3] / yscale; + for (x = 0; x < box[2] / xscale; x++) { + int xx_from = box[0] + x * xscale; + double ss = 0; + for (yy = box[1] + y * yscale; yy < box[1] + box[3]; yy++) { + INT32 *line = (INT32 *)imIn->image32[yy]; + for (xx = xx_from; xx < xx_from + xscale; xx++) { + ss += line[xx + 0]; + } + } + IMAGING_PIXEL_I(imOut, x, y) = ROUND_UP(ss * multiplier); + } + } + if (box[2] % xscale && box[3] % yscale) { + double multiplier = 1.0 / ((box[2] % xscale) * (box[3] % yscale)); + double ss = 0; + x = box[2] / xscale; + y = box[3] / yscale; + for (yy = box[1] + y * yscale; yy < box[1] + box[3]; yy++) { + INT32 *line = (INT32 *)imIn->image32[yy]; + for (xx = box[0] + x * xscale; xx < box[0] + box[2]; xx++) { + ss += line[xx + 0]; + } + } + IMAGING_PIXEL_I(imOut, x, y) = ROUND_UP(ss * multiplier); + } + break; + + case IMAGING_TYPE_FLOAT32: + if (box[2] % xscale) { + double multiplier = 1.0 / ((box[2] % xscale) * yscale); + for (y = 0; y < box[3] / yscale; y++) { + int yy_from = box[1] + y * yscale; + double ss = 0; + x = box[2] / xscale; + for (yy = yy_from; yy < yy_from + yscale; yy++) { + FLOAT32 *line = (FLOAT32 *)imIn->image32[yy]; + for (xx = box[0] + x * xscale; xx < box[0] + box[2]; xx++) { + ss += line[xx + 0]; + } + } + IMAGING_PIXEL_F(imOut, x, y) = ss * multiplier; + } + } + if (box[3] % yscale) { + double multiplier = 1.0 / (xscale * (box[3] % yscale)); + y = box[3] / yscale; + for (x = 0; x < box[2] / xscale; x++) { + int xx_from = box[0] + x * xscale; + double ss = 0; + for (yy = box[1] + y * yscale; yy < box[1] + box[3]; yy++) { + FLOAT32 *line = (FLOAT32 *)imIn->image32[yy]; + for (xx = xx_from; xx < xx_from + xscale; xx++) { + ss += line[xx + 0]; + } + } + IMAGING_PIXEL_F(imOut, x, y) = ss * multiplier; + } + } + if (box[2] % xscale && box[3] % yscale) { + double multiplier = 1.0 / ((box[2] % xscale) * (box[3] % yscale)); + double ss = 0; + x = box[2] / xscale; + y = box[3] / yscale; + for (yy = box[1] + y * yscale; yy < box[1] + box[3]; yy++) { + FLOAT32 *line = (FLOAT32 *)imIn->image32[yy]; + for (xx = box[0] + x * xscale; xx < box[0] + box[2]; xx++) { + ss += line[xx + 0]; + } + } + IMAGING_PIXEL_F(imOut, x, y) = ss * multiplier; + } + break; + } +} + +Imaging +ImagingReduce(Imaging imIn, int xscale, int yscale, int box[4]) { + ImagingSectionCookie cookie; + Imaging imOut = NULL; + + if (strcmp(imIn->mode, "P") == 0 || strcmp(imIn->mode, "1") == 0) { + return (Imaging)ImagingError_ModeError(); + } + + if (imIn->type == IMAGING_TYPE_SPECIAL) { + return (Imaging)ImagingError_ModeError(); + } + + imOut = ImagingNewDirty( + imIn->mode, (box[2] + xscale - 1) / xscale, (box[3] + yscale - 1) / yscale); + if (!imOut) { + return NULL; + } + + ImagingSectionEnter(&cookie); + + switch (imIn->type) { + case IMAGING_TYPE_UINT8: + if (xscale == 1) { + if (yscale == 2) { + ImagingReduce1x2(imOut, imIn, box); + } else if (yscale == 3) { + ImagingReduce1x3(imOut, imIn, box); + } else { + ImagingReduce1xN(imOut, imIn, box, yscale); + } + } else if (yscale == 1) { + if (xscale == 2) { + ImagingReduce2x1(imOut, imIn, box); + } else if (xscale == 3) { + ImagingReduce3x1(imOut, imIn, box); + } else { + ImagingReduceNx1(imOut, imIn, box, xscale); + } + } else if (xscale == yscale && xscale <= 5) { + if (xscale == 2) { + ImagingReduce2x2(imOut, imIn, box); + } else if (xscale == 3) { + ImagingReduce3x3(imOut, imIn, box); + } else if (xscale == 4) { + ImagingReduce4x4(imOut, imIn, box); + } else { + ImagingReduce5x5(imOut, imIn, box); + } + } else { + ImagingReduceNxN(imOut, imIn, box, xscale, yscale); + } + + ImagingReduceCorners(imOut, imIn, box, xscale, yscale); + break; + + case IMAGING_TYPE_INT32: + case IMAGING_TYPE_FLOAT32: + ImagingReduceNxN_32bpc(imOut, imIn, box, xscale, yscale); + + ImagingReduceCorners_32bpc(imOut, imIn, box, xscale, yscale); + break; + } + + ImagingSectionLeave(&cookie); + + return imOut; +} diff --git a/src/libImaging/Resample.c b/src/libImaging/Resample.c index 59c27b3f4..047f17139 100644 --- a/src/libImaging/Resample.c +++ b/src/libImaging/Resample.c @@ -1,708 +1,708 @@ -#include "Imaging.h" - -#include - -#define ROUND_UP(f) ((int)((f) >= 0.0 ? (f) + 0.5F : (f) - 0.5F)) - -struct filter { - double (*filter)(double x); - double support; -}; - -static inline double -box_filter(double x) { - if (x > -0.5 && x <= 0.5) { - return 1.0; - } - return 0.0; -} - -static inline double -bilinear_filter(double x) { - if (x < 0.0) { - x = -x; - } - if (x < 1.0) { - return 1.0 - x; - } - return 0.0; -} - -static inline double -hamming_filter(double x) { - if (x < 0.0) { - x = -x; - } - if (x == 0.0) { - return 1.0; - } - if (x >= 1.0) { - return 0.0; - } - x = x * M_PI; - return sin(x) / x * (0.54f + 0.46f * cos(x)); -} - -static inline double -bicubic_filter(double x) { - /* https://en.wikipedia.org/wiki/Bicubic_interpolation#Bicubic_convolution_algorithm - */ -#define a -0.5 - if (x < 0.0) { - x = -x; - } - if (x < 1.0) { - return ((a + 2.0) * x - (a + 3.0)) * x * x + 1; - } - if (x < 2.0) { - return (((x - 5) * x + 8) * x - 4) * a; - } - return 0.0; -#undef a -} - -static inline double -sinc_filter(double x) { - if (x == 0.0) { - return 1.0; - } - x = x * M_PI; - return sin(x) / x; -} - -static inline double -lanczos_filter(double x) { - /* truncated sinc */ - if (-3.0 <= x && x < 3.0) { - return sinc_filter(x) * sinc_filter(x / 3); - } - return 0.0; -} - -static struct filter BOX = {box_filter, 0.5}; -static struct filter BILINEAR = {bilinear_filter, 1.0}; -static struct filter HAMMING = {hamming_filter, 1.0}; -static struct filter BICUBIC = {bicubic_filter, 2.0}; -static struct filter LANCZOS = {lanczos_filter, 3.0}; - -/* 8 bits for result. Filter can have negative areas. - In one cases the sum of the coefficients will be negative, - in the other it will be more than 1.0. That is why we need - two extra bits for overflow and int type. */ -#define PRECISION_BITS (32 - 8 - 2) - -/* Handles values form -640 to 639. */ -UINT8 _clip8_lookups[1280] = { - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, - 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, - 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, - 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, - 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, - 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, - 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, - 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, - 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, - 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, - 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, - 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, - 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, - 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, - 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, - 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, -}; - -UINT8 *clip8_lookups = &_clip8_lookups[640]; - -static inline UINT8 -clip8(int in) { - return clip8_lookups[in >> PRECISION_BITS]; -} - -int -precompute_coeffs( - int inSize, - float in0, - float in1, - int outSize, - struct filter *filterp, - int **boundsp, - double **kkp) { - double support, scale, filterscale; - double center, ww, ss; - int xx, x, ksize, xmin, xmax; - int *bounds; - double *kk, *k; - - /* prepare for horizontal stretch */ - filterscale = scale = (double)(in1 - in0) / outSize; - if (filterscale < 1.0) { - filterscale = 1.0; - } - - /* determine support size (length of resampling filter) */ - support = filterp->support * filterscale; - - /* maximum number of coeffs */ - ksize = (int)ceil(support) * 2 + 1; - - // check for overflow - if (outSize > INT_MAX / (ksize * (int)sizeof(double))) { - ImagingError_MemoryError(); - return 0; - } - - /* coefficient buffer */ - /* malloc check ok, overflow checked above */ - kk = malloc(outSize * ksize * sizeof(double)); - if (!kk) { - ImagingError_MemoryError(); - return 0; - } - - /* malloc check ok, ksize*sizeof(double) > 2*sizeof(int) */ - bounds = malloc(outSize * 2 * sizeof(int)); - if (!bounds) { - free(kk); - ImagingError_MemoryError(); - return 0; - } - - for (xx = 0; xx < outSize; xx++) { - center = in0 + (xx + 0.5) * scale; - ww = 0.0; - ss = 1.0 / filterscale; - // Round the value - xmin = (int)(center - support + 0.5); - if (xmin < 0) { - xmin = 0; - } - // Round the value - xmax = (int)(center + support + 0.5); - if (xmax > inSize) { - xmax = inSize; - } - xmax -= xmin; - k = &kk[xx * ksize]; - for (x = 0; x < xmax; x++) { - double w = filterp->filter((x + xmin - center + 0.5) * ss); - k[x] = w; - ww += w; - } - for (x = 0; x < xmax; x++) { - if (ww != 0.0) { - k[x] /= ww; - } - } - // Remaining values should stay empty if they are used despite of xmax. - for (; x < ksize; x++) { - k[x] = 0; - } - bounds[xx * 2 + 0] = xmin; - bounds[xx * 2 + 1] = xmax; - } - *boundsp = bounds; - *kkp = kk; - return ksize; -} - -void -normalize_coeffs_8bpc(int outSize, int ksize, double *prekk) { - int x; - INT32 *kk; - - // use the same buffer for normalized coefficients - kk = (INT32 *)prekk; - - for (x = 0; x < outSize * ksize; x++) { - if (prekk[x] < 0) { - kk[x] = (int)(-0.5 + prekk[x] * (1 << PRECISION_BITS)); - } else { - kk[x] = (int)(0.5 + prekk[x] * (1 << PRECISION_BITS)); - } - } -} - -void -ImagingResampleHorizontal_8bpc( - Imaging imOut, Imaging imIn, int offset, int ksize, int *bounds, double *prekk) { - ImagingSectionCookie cookie; - int ss0, ss1, ss2, ss3; - int xx, yy, x, xmin, xmax; - INT32 *k, *kk; - - // use the same buffer for normalized coefficients - kk = (INT32 *)prekk; - normalize_coeffs_8bpc(imOut->xsize, ksize, prekk); - - ImagingSectionEnter(&cookie); - if (imIn->image8) { - for (yy = 0; yy < imOut->ysize; yy++) { - for (xx = 0; xx < imOut->xsize; xx++) { - xmin = bounds[xx * 2 + 0]; - xmax = bounds[xx * 2 + 1]; - k = &kk[xx * ksize]; - ss0 = 1 << (PRECISION_BITS - 1); - for (x = 0; x < xmax; x++) { - ss0 += ((UINT8)imIn->image8[yy + offset][x + xmin]) * k[x]; - } - imOut->image8[yy][xx] = clip8(ss0); - } - } - } else if (imIn->type == IMAGING_TYPE_UINT8) { - if (imIn->bands == 2) { - for (yy = 0; yy < imOut->ysize; yy++) { - for (xx = 0; xx < imOut->xsize; xx++) { - UINT32 v; - xmin = bounds[xx * 2 + 0]; - xmax = bounds[xx * 2 + 1]; - k = &kk[xx * ksize]; - ss0 = ss3 = 1 << (PRECISION_BITS - 1); - for (x = 0; x < xmax; x++) { - ss0 += ((UINT8)imIn->image[yy + offset][(x + xmin) * 4 + 0]) * - k[x]; - ss3 += ((UINT8)imIn->image[yy + offset][(x + xmin) * 4 + 3]) * - k[x]; - } - v = MAKE_UINT32(clip8(ss0), 0, 0, clip8(ss3)); - memcpy(imOut->image[yy] + xx * sizeof(v), &v, sizeof(v)); - } - } - } else if (imIn->bands == 3) { - for (yy = 0; yy < imOut->ysize; yy++) { - for (xx = 0; xx < imOut->xsize; xx++) { - UINT32 v; - xmin = bounds[xx * 2 + 0]; - xmax = bounds[xx * 2 + 1]; - k = &kk[xx * ksize]; - ss0 = ss1 = ss2 = 1 << (PRECISION_BITS - 1); - for (x = 0; x < xmax; x++) { - ss0 += ((UINT8)imIn->image[yy + offset][(x + xmin) * 4 + 0]) * - k[x]; - ss1 += ((UINT8)imIn->image[yy + offset][(x + xmin) * 4 + 1]) * - k[x]; - ss2 += ((UINT8)imIn->image[yy + offset][(x + xmin) * 4 + 2]) * - k[x]; - } - v = MAKE_UINT32(clip8(ss0), clip8(ss1), clip8(ss2), 0); - memcpy(imOut->image[yy] + xx * sizeof(v), &v, sizeof(v)); - } - } - } else { - for (yy = 0; yy < imOut->ysize; yy++) { - for (xx = 0; xx < imOut->xsize; xx++) { - UINT32 v; - xmin = bounds[xx * 2 + 0]; - xmax = bounds[xx * 2 + 1]; - k = &kk[xx * ksize]; - ss0 = ss1 = ss2 = ss3 = 1 << (PRECISION_BITS - 1); - for (x = 0; x < xmax; x++) { - ss0 += ((UINT8)imIn->image[yy + offset][(x + xmin) * 4 + 0]) * - k[x]; - ss1 += ((UINT8)imIn->image[yy + offset][(x + xmin) * 4 + 1]) * - k[x]; - ss2 += ((UINT8)imIn->image[yy + offset][(x + xmin) * 4 + 2]) * - k[x]; - ss3 += ((UINT8)imIn->image[yy + offset][(x + xmin) * 4 + 3]) * - k[x]; - } - v = MAKE_UINT32(clip8(ss0), clip8(ss1), clip8(ss2), clip8(ss3)); - memcpy(imOut->image[yy] + xx * sizeof(v), &v, sizeof(v)); - } - } - } - } - ImagingSectionLeave(&cookie); -} - -void -ImagingResampleVertical_8bpc( - Imaging imOut, Imaging imIn, int offset, int ksize, int *bounds, double *prekk) { - ImagingSectionCookie cookie; - int ss0, ss1, ss2, ss3; - int xx, yy, y, ymin, ymax; - INT32 *k, *kk; - - // use the same buffer for normalized coefficients - kk = (INT32 *)prekk; - normalize_coeffs_8bpc(imOut->ysize, ksize, prekk); - - ImagingSectionEnter(&cookie); - if (imIn->image8) { - for (yy = 0; yy < imOut->ysize; yy++) { - k = &kk[yy * ksize]; - ymin = bounds[yy * 2 + 0]; - ymax = bounds[yy * 2 + 1]; - for (xx = 0; xx < imOut->xsize; xx++) { - ss0 = 1 << (PRECISION_BITS - 1); - for (y = 0; y < ymax; y++) { - ss0 += ((UINT8)imIn->image8[y + ymin][xx]) * k[y]; - } - imOut->image8[yy][xx] = clip8(ss0); - } - } - } else if (imIn->type == IMAGING_TYPE_UINT8) { - if (imIn->bands == 2) { - for (yy = 0; yy < imOut->ysize; yy++) { - k = &kk[yy * ksize]; - ymin = bounds[yy * 2 + 0]; - ymax = bounds[yy * 2 + 1]; - for (xx = 0; xx < imOut->xsize; xx++) { - UINT32 v; - ss0 = ss3 = 1 << (PRECISION_BITS - 1); - for (y = 0; y < ymax; y++) { - ss0 += ((UINT8)imIn->image[y + ymin][xx * 4 + 0]) * k[y]; - ss3 += ((UINT8)imIn->image[y + ymin][xx * 4 + 3]) * k[y]; - } - v = MAKE_UINT32(clip8(ss0), 0, 0, clip8(ss3)); - memcpy(imOut->image[yy] + xx * sizeof(v), &v, sizeof(v)); - } - } - } else if (imIn->bands == 3) { - for (yy = 0; yy < imOut->ysize; yy++) { - k = &kk[yy * ksize]; - ymin = bounds[yy * 2 + 0]; - ymax = bounds[yy * 2 + 1]; - for (xx = 0; xx < imOut->xsize; xx++) { - UINT32 v; - ss0 = ss1 = ss2 = 1 << (PRECISION_BITS - 1); - for (y = 0; y < ymax; y++) { - ss0 += ((UINT8)imIn->image[y + ymin][xx * 4 + 0]) * k[y]; - ss1 += ((UINT8)imIn->image[y + ymin][xx * 4 + 1]) * k[y]; - ss2 += ((UINT8)imIn->image[y + ymin][xx * 4 + 2]) * k[y]; - } - v = MAKE_UINT32(clip8(ss0), clip8(ss1), clip8(ss2), 0); - memcpy(imOut->image[yy] + xx * sizeof(v), &v, sizeof(v)); - } - } - } else { - for (yy = 0; yy < imOut->ysize; yy++) { - k = &kk[yy * ksize]; - ymin = bounds[yy * 2 + 0]; - ymax = bounds[yy * 2 + 1]; - for (xx = 0; xx < imOut->xsize; xx++) { - UINT32 v; - ss0 = ss1 = ss2 = ss3 = 1 << (PRECISION_BITS - 1); - for (y = 0; y < ymax; y++) { - ss0 += ((UINT8)imIn->image[y + ymin][xx * 4 + 0]) * k[y]; - ss1 += ((UINT8)imIn->image[y + ymin][xx * 4 + 1]) * k[y]; - ss2 += ((UINT8)imIn->image[y + ymin][xx * 4 + 2]) * k[y]; - ss3 += ((UINT8)imIn->image[y + ymin][xx * 4 + 3]) * k[y]; - } - v = MAKE_UINT32(clip8(ss0), clip8(ss1), clip8(ss2), clip8(ss3)); - memcpy(imOut->image[yy] + xx * sizeof(v), &v, sizeof(v)); - } - } - } - } - ImagingSectionLeave(&cookie); -} - -void -ImagingResampleHorizontal_32bpc( - Imaging imOut, Imaging imIn, int offset, int ksize, int *bounds, double *kk) { - ImagingSectionCookie cookie; - double ss; - int xx, yy, x, xmin, xmax; - double *k; - - ImagingSectionEnter(&cookie); - switch (imIn->type) { - case IMAGING_TYPE_INT32: - for (yy = 0; yy < imOut->ysize; yy++) { - for (xx = 0; xx < imOut->xsize; xx++) { - xmin = bounds[xx * 2 + 0]; - xmax = bounds[xx * 2 + 1]; - k = &kk[xx * ksize]; - ss = 0.0; - for (x = 0; x < xmax; x++) { - ss += IMAGING_PIXEL_I(imIn, x + xmin, yy + offset) * k[x]; - } - IMAGING_PIXEL_I(imOut, xx, yy) = ROUND_UP(ss); - } - } - break; - - case IMAGING_TYPE_FLOAT32: - for (yy = 0; yy < imOut->ysize; yy++) { - for (xx = 0; xx < imOut->xsize; xx++) { - xmin = bounds[xx * 2 + 0]; - xmax = bounds[xx * 2 + 1]; - k = &kk[xx * ksize]; - ss = 0.0; - for (x = 0; x < xmax; x++) { - ss += IMAGING_PIXEL_F(imIn, x + xmin, yy + offset) * k[x]; - } - IMAGING_PIXEL_F(imOut, xx, yy) = ss; - } - } - break; - } - ImagingSectionLeave(&cookie); -} - -void -ImagingResampleVertical_32bpc( - Imaging imOut, Imaging imIn, int offset, int ksize, int *bounds, double *kk) { - ImagingSectionCookie cookie; - double ss; - int xx, yy, y, ymin, ymax; - double *k; - - ImagingSectionEnter(&cookie); - switch (imIn->type) { - case IMAGING_TYPE_INT32: - for (yy = 0; yy < imOut->ysize; yy++) { - ymin = bounds[yy * 2 + 0]; - ymax = bounds[yy * 2 + 1]; - k = &kk[yy * ksize]; - for (xx = 0; xx < imOut->xsize; xx++) { - ss = 0.0; - for (y = 0; y < ymax; y++) { - ss += IMAGING_PIXEL_I(imIn, xx, y + ymin) * k[y]; - } - IMAGING_PIXEL_I(imOut, xx, yy) = ROUND_UP(ss); - } - } - break; - - case IMAGING_TYPE_FLOAT32: - for (yy = 0; yy < imOut->ysize; yy++) { - ymin = bounds[yy * 2 + 0]; - ymax = bounds[yy * 2 + 1]; - k = &kk[yy * ksize]; - for (xx = 0; xx < imOut->xsize; xx++) { - ss = 0.0; - for (y = 0; y < ymax; y++) { - ss += IMAGING_PIXEL_F(imIn, xx, y + ymin) * k[y]; - } - IMAGING_PIXEL_F(imOut, xx, yy) = ss; - } - } - break; - } - ImagingSectionLeave(&cookie); -} - -typedef void (*ResampleFunction)( - Imaging imOut, Imaging imIn, int offset, int ksize, int *bounds, double *kk); - -Imaging -ImagingResampleInner( - Imaging imIn, - int xsize, - int ysize, - struct filter *filterp, - float box[4], - ResampleFunction ResampleHorizontal, - ResampleFunction ResampleVertical); - -Imaging -ImagingResample(Imaging imIn, int xsize, int ysize, int filter, float box[4]) { - struct filter *filterp; - ResampleFunction ResampleHorizontal; - ResampleFunction ResampleVertical; - - if (strcmp(imIn->mode, "P") == 0 || strcmp(imIn->mode, "1") == 0) { - return (Imaging)ImagingError_ModeError(); - } - - if (imIn->type == IMAGING_TYPE_SPECIAL) { - return (Imaging)ImagingError_ModeError(); - } else if (imIn->image8) { - ResampleHorizontal = ImagingResampleHorizontal_8bpc; - ResampleVertical = ImagingResampleVertical_8bpc; - } else { - switch (imIn->type) { - case IMAGING_TYPE_UINT8: - ResampleHorizontal = ImagingResampleHorizontal_8bpc; - ResampleVertical = ImagingResampleVertical_8bpc; - break; - case IMAGING_TYPE_INT32: - case IMAGING_TYPE_FLOAT32: - ResampleHorizontal = ImagingResampleHorizontal_32bpc; - ResampleVertical = ImagingResampleVertical_32bpc; - break; - default: - return (Imaging)ImagingError_ModeError(); - } - } - - /* check filter */ - switch (filter) { - case IMAGING_TRANSFORM_BOX: - filterp = &BOX; - break; - case IMAGING_TRANSFORM_BILINEAR: - filterp = &BILINEAR; - break; - case IMAGING_TRANSFORM_HAMMING: - filterp = &HAMMING; - break; - case IMAGING_TRANSFORM_BICUBIC: - filterp = &BICUBIC; - break; - case IMAGING_TRANSFORM_LANCZOS: - filterp = &LANCZOS; - break; - default: - return (Imaging)ImagingError_ValueError("unsupported resampling filter"); - } - - return ImagingResampleInner( - imIn, xsize, ysize, filterp, box, ResampleHorizontal, ResampleVertical); -} - -Imaging -ImagingResampleInner( - Imaging imIn, - int xsize, - int ysize, - struct filter *filterp, - float box[4], - ResampleFunction ResampleHorizontal, - ResampleFunction ResampleVertical) { - Imaging imTemp = NULL; - Imaging imOut = NULL; - - int i, need_horizontal, need_vertical; - int ybox_first, ybox_last; - int ksize_horiz, ksize_vert; - int *bounds_horiz, *bounds_vert; - double *kk_horiz, *kk_vert; - - need_horizontal = xsize != imIn->xsize || box[0] || box[2] != xsize; - need_vertical = ysize != imIn->ysize || box[1] || box[3] != ysize; - - ksize_horiz = precompute_coeffs( - imIn->xsize, box[0], box[2], xsize, filterp, &bounds_horiz, &kk_horiz); - if (!ksize_horiz) { - return NULL; - } - - ksize_vert = precompute_coeffs( - imIn->ysize, box[1], box[3], ysize, filterp, &bounds_vert, &kk_vert); - if (!ksize_vert) { - free(bounds_horiz); - free(kk_horiz); - return NULL; - } - - // First used row in the source image - ybox_first = bounds_vert[0]; - // Last used row in the source image - ybox_last = bounds_vert[ysize * 2 - 2] + bounds_vert[ysize * 2 - 1]; - - /* two-pass resize, horizontal pass */ - if (need_horizontal) { - // Shift bounds for vertical pass - for (i = 0; i < ysize; i++) { - bounds_vert[i * 2] -= ybox_first; - } - - imTemp = ImagingNewDirty(imIn->mode, xsize, ybox_last - ybox_first); - if (imTemp) { - ResampleHorizontal( - imTemp, imIn, ybox_first, ksize_horiz, bounds_horiz, kk_horiz); - } - free(bounds_horiz); - free(kk_horiz); - if (!imTemp) { - free(bounds_vert); - free(kk_vert); - return NULL; - } - imOut = imIn = imTemp; - } else { - // Free in any case - free(bounds_horiz); - free(kk_horiz); - } - - /* vertical pass */ - if (need_vertical) { - imOut = ImagingNewDirty(imIn->mode, imIn->xsize, ysize); - if (imOut) { - /* imIn can be the original image or horizontally resampled one */ - ResampleVertical(imOut, imIn, 0, ksize_vert, bounds_vert, kk_vert); - } - /* it's safe to call ImagingDelete with empty value - if previous step was not performed. */ - ImagingDelete(imTemp); - free(bounds_vert); - free(kk_vert); - if (!imOut) { - return NULL; - } - } else { - // Free in any case - free(bounds_vert); - free(kk_vert); - } - - /* none of the previous steps are performed, copying */ - if (!imOut) { - imOut = ImagingCopy(imIn); - } - - return imOut; -} +#include "Imaging.h" + +#include + +#define ROUND_UP(f) ((int)((f) >= 0.0 ? (f) + 0.5F : (f) - 0.5F)) + +struct filter { + double (*filter)(double x); + double support; +}; + +static inline double +box_filter(double x) { + if (x > -0.5 && x <= 0.5) { + return 1.0; + } + return 0.0; +} + +static inline double +bilinear_filter(double x) { + if (x < 0.0) { + x = -x; + } + if (x < 1.0) { + return 1.0 - x; + } + return 0.0; +} + +static inline double +hamming_filter(double x) { + if (x < 0.0) { + x = -x; + } + if (x == 0.0) { + return 1.0; + } + if (x >= 1.0) { + return 0.0; + } + x = x * M_PI; + return sin(x) / x * (0.54f + 0.46f * cos(x)); +} + +static inline double +bicubic_filter(double x) { + /* https://en.wikipedia.org/wiki/Bicubic_interpolation#Bicubic_convolution_algorithm + */ +#define a -0.5 + if (x < 0.0) { + x = -x; + } + if (x < 1.0) { + return ((a + 2.0) * x - (a + 3.0)) * x * x + 1; + } + if (x < 2.0) { + return (((x - 5) * x + 8) * x - 4) * a; + } + return 0.0; +#undef a +} + +static inline double +sinc_filter(double x) { + if (x == 0.0) { + return 1.0; + } + x = x * M_PI; + return sin(x) / x; +} + +static inline double +lanczos_filter(double x) { + /* truncated sinc */ + if (-3.0 <= x && x < 3.0) { + return sinc_filter(x) * sinc_filter(x / 3); + } + return 0.0; +} + +static struct filter BOX = {box_filter, 0.5}; +static struct filter BILINEAR = {bilinear_filter, 1.0}; +static struct filter HAMMING = {hamming_filter, 1.0}; +static struct filter BICUBIC = {bicubic_filter, 2.0}; +static struct filter LANCZOS = {lanczos_filter, 3.0}; + +/* 8 bits for result. Filter can have negative areas. + In one cases the sum of the coefficients will be negative, + in the other it will be more than 1.0. That is why we need + two extra bits for overflow and int type. */ +#define PRECISION_BITS (32 - 8 - 2) + +/* Handles values form -640 to 639. */ +UINT8 _clip8_lookups[1280] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, + 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, + 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, + 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, + 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, + 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, + 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, + 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, + 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, + 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, + 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, + 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, + 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, + 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, + 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, + 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, +}; + +UINT8 *clip8_lookups = &_clip8_lookups[640]; + +static inline UINT8 +clip8(int in) { + return clip8_lookups[in >> PRECISION_BITS]; +} + +int +precompute_coeffs( + int inSize, + float in0, + float in1, + int outSize, + struct filter *filterp, + int **boundsp, + double **kkp) { + double support, scale, filterscale; + double center, ww, ss; + int xx, x, ksize, xmin, xmax; + int *bounds; + double *kk, *k; + + /* prepare for horizontal stretch */ + filterscale = scale = (double)(in1 - in0) / outSize; + if (filterscale < 1.0) { + filterscale = 1.0; + } + + /* determine support size (length of resampling filter) */ + support = filterp->support * filterscale; + + /* maximum number of coeffs */ + ksize = (int)ceil(support) * 2 + 1; + + // check for overflow + if (outSize > INT_MAX / (ksize * (int)sizeof(double))) { + ImagingError_MemoryError(); + return 0; + } + + /* coefficient buffer */ + /* malloc check ok, overflow checked above */ + kk = malloc(outSize * ksize * sizeof(double)); + if (!kk) { + ImagingError_MemoryError(); + return 0; + } + + /* malloc check ok, ksize*sizeof(double) > 2*sizeof(int) */ + bounds = malloc(outSize * 2 * sizeof(int)); + if (!bounds) { + free(kk); + ImagingError_MemoryError(); + return 0; + } + + for (xx = 0; xx < outSize; xx++) { + center = in0 + (xx + 0.5) * scale; + ww = 0.0; + ss = 1.0 / filterscale; + // Round the value + xmin = (int)(center - support + 0.5); + if (xmin < 0) { + xmin = 0; + } + // Round the value + xmax = (int)(center + support + 0.5); + if (xmax > inSize) { + xmax = inSize; + } + xmax -= xmin; + k = &kk[xx * ksize]; + for (x = 0; x < xmax; x++) { + double w = filterp->filter((x + xmin - center + 0.5) * ss); + k[x] = w; + ww += w; + } + for (x = 0; x < xmax; x++) { + if (ww != 0.0) { + k[x] /= ww; + } + } + // Remaining values should stay empty if they are used despite of xmax. + for (; x < ksize; x++) { + k[x] = 0; + } + bounds[xx * 2 + 0] = xmin; + bounds[xx * 2 + 1] = xmax; + } + *boundsp = bounds; + *kkp = kk; + return ksize; +} + +void +normalize_coeffs_8bpc(int outSize, int ksize, double *prekk) { + int x; + INT32 *kk; + + // use the same buffer for normalized coefficients + kk = (INT32 *)prekk; + + for (x = 0; x < outSize * ksize; x++) { + if (prekk[x] < 0) { + kk[x] = (int)(-0.5 + prekk[x] * (1 << PRECISION_BITS)); + } else { + kk[x] = (int)(0.5 + prekk[x] * (1 << PRECISION_BITS)); + } + } +} + +void +ImagingResampleHorizontal_8bpc( + Imaging imOut, Imaging imIn, int offset, int ksize, int *bounds, double *prekk) { + ImagingSectionCookie cookie; + int ss0, ss1, ss2, ss3; + int xx, yy, x, xmin, xmax; + INT32 *k, *kk; + + // use the same buffer for normalized coefficients + kk = (INT32 *)prekk; + normalize_coeffs_8bpc(imOut->xsize, ksize, prekk); + + ImagingSectionEnter(&cookie); + if (imIn->image8) { + for (yy = 0; yy < imOut->ysize; yy++) { + for (xx = 0; xx < imOut->xsize; xx++) { + xmin = bounds[xx * 2 + 0]; + xmax = bounds[xx * 2 + 1]; + k = &kk[xx * ksize]; + ss0 = 1 << (PRECISION_BITS - 1); + for (x = 0; x < xmax; x++) { + ss0 += ((UINT8)imIn->image8[yy + offset][x + xmin]) * k[x]; + } + imOut->image8[yy][xx] = clip8(ss0); + } + } + } else if (imIn->type == IMAGING_TYPE_UINT8) { + if (imIn->bands == 2) { + for (yy = 0; yy < imOut->ysize; yy++) { + for (xx = 0; xx < imOut->xsize; xx++) { + UINT32 v; + xmin = bounds[xx * 2 + 0]; + xmax = bounds[xx * 2 + 1]; + k = &kk[xx * ksize]; + ss0 = ss3 = 1 << (PRECISION_BITS - 1); + for (x = 0; x < xmax; x++) { + ss0 += ((UINT8)imIn->image[yy + offset][(x + xmin) * 4 + 0]) * + k[x]; + ss3 += ((UINT8)imIn->image[yy + offset][(x + xmin) * 4 + 3]) * + k[x]; + } + v = MAKE_UINT32(clip8(ss0), 0, 0, clip8(ss3)); + memcpy(imOut->image[yy] + xx * sizeof(v), &v, sizeof(v)); + } + } + } else if (imIn->bands == 3) { + for (yy = 0; yy < imOut->ysize; yy++) { + for (xx = 0; xx < imOut->xsize; xx++) { + UINT32 v; + xmin = bounds[xx * 2 + 0]; + xmax = bounds[xx * 2 + 1]; + k = &kk[xx * ksize]; + ss0 = ss1 = ss2 = 1 << (PRECISION_BITS - 1); + for (x = 0; x < xmax; x++) { + ss0 += ((UINT8)imIn->image[yy + offset][(x + xmin) * 4 + 0]) * + k[x]; + ss1 += ((UINT8)imIn->image[yy + offset][(x + xmin) * 4 + 1]) * + k[x]; + ss2 += ((UINT8)imIn->image[yy + offset][(x + xmin) * 4 + 2]) * + k[x]; + } + v = MAKE_UINT32(clip8(ss0), clip8(ss1), clip8(ss2), 0); + memcpy(imOut->image[yy] + xx * sizeof(v), &v, sizeof(v)); + } + } + } else { + for (yy = 0; yy < imOut->ysize; yy++) { + for (xx = 0; xx < imOut->xsize; xx++) { + UINT32 v; + xmin = bounds[xx * 2 + 0]; + xmax = bounds[xx * 2 + 1]; + k = &kk[xx * ksize]; + ss0 = ss1 = ss2 = ss3 = 1 << (PRECISION_BITS - 1); + for (x = 0; x < xmax; x++) { + ss0 += ((UINT8)imIn->image[yy + offset][(x + xmin) * 4 + 0]) * + k[x]; + ss1 += ((UINT8)imIn->image[yy + offset][(x + xmin) * 4 + 1]) * + k[x]; + ss2 += ((UINT8)imIn->image[yy + offset][(x + xmin) * 4 + 2]) * + k[x]; + ss3 += ((UINT8)imIn->image[yy + offset][(x + xmin) * 4 + 3]) * + k[x]; + } + v = MAKE_UINT32(clip8(ss0), clip8(ss1), clip8(ss2), clip8(ss3)); + memcpy(imOut->image[yy] + xx * sizeof(v), &v, sizeof(v)); + } + } + } + } + ImagingSectionLeave(&cookie); +} + +void +ImagingResampleVertical_8bpc( + Imaging imOut, Imaging imIn, int offset, int ksize, int *bounds, double *prekk) { + ImagingSectionCookie cookie; + int ss0, ss1, ss2, ss3; + int xx, yy, y, ymin, ymax; + INT32 *k, *kk; + + // use the same buffer for normalized coefficients + kk = (INT32 *)prekk; + normalize_coeffs_8bpc(imOut->ysize, ksize, prekk); + + ImagingSectionEnter(&cookie); + if (imIn->image8) { + for (yy = 0; yy < imOut->ysize; yy++) { + k = &kk[yy * ksize]; + ymin = bounds[yy * 2 + 0]; + ymax = bounds[yy * 2 + 1]; + for (xx = 0; xx < imOut->xsize; xx++) { + ss0 = 1 << (PRECISION_BITS - 1); + for (y = 0; y < ymax; y++) { + ss0 += ((UINT8)imIn->image8[y + ymin][xx]) * k[y]; + } + imOut->image8[yy][xx] = clip8(ss0); + } + } + } else if (imIn->type == IMAGING_TYPE_UINT8) { + if (imIn->bands == 2) { + for (yy = 0; yy < imOut->ysize; yy++) { + k = &kk[yy * ksize]; + ymin = bounds[yy * 2 + 0]; + ymax = bounds[yy * 2 + 1]; + for (xx = 0; xx < imOut->xsize; xx++) { + UINT32 v; + ss0 = ss3 = 1 << (PRECISION_BITS - 1); + for (y = 0; y < ymax; y++) { + ss0 += ((UINT8)imIn->image[y + ymin][xx * 4 + 0]) * k[y]; + ss3 += ((UINT8)imIn->image[y + ymin][xx * 4 + 3]) * k[y]; + } + v = MAKE_UINT32(clip8(ss0), 0, 0, clip8(ss3)); + memcpy(imOut->image[yy] + xx * sizeof(v), &v, sizeof(v)); + } + } + } else if (imIn->bands == 3) { + for (yy = 0; yy < imOut->ysize; yy++) { + k = &kk[yy * ksize]; + ymin = bounds[yy * 2 + 0]; + ymax = bounds[yy * 2 + 1]; + for (xx = 0; xx < imOut->xsize; xx++) { + UINT32 v; + ss0 = ss1 = ss2 = 1 << (PRECISION_BITS - 1); + for (y = 0; y < ymax; y++) { + ss0 += ((UINT8)imIn->image[y + ymin][xx * 4 + 0]) * k[y]; + ss1 += ((UINT8)imIn->image[y + ymin][xx * 4 + 1]) * k[y]; + ss2 += ((UINT8)imIn->image[y + ymin][xx * 4 + 2]) * k[y]; + } + v = MAKE_UINT32(clip8(ss0), clip8(ss1), clip8(ss2), 0); + memcpy(imOut->image[yy] + xx * sizeof(v), &v, sizeof(v)); + } + } + } else { + for (yy = 0; yy < imOut->ysize; yy++) { + k = &kk[yy * ksize]; + ymin = bounds[yy * 2 + 0]; + ymax = bounds[yy * 2 + 1]; + for (xx = 0; xx < imOut->xsize; xx++) { + UINT32 v; + ss0 = ss1 = ss2 = ss3 = 1 << (PRECISION_BITS - 1); + for (y = 0; y < ymax; y++) { + ss0 += ((UINT8)imIn->image[y + ymin][xx * 4 + 0]) * k[y]; + ss1 += ((UINT8)imIn->image[y + ymin][xx * 4 + 1]) * k[y]; + ss2 += ((UINT8)imIn->image[y + ymin][xx * 4 + 2]) * k[y]; + ss3 += ((UINT8)imIn->image[y + ymin][xx * 4 + 3]) * k[y]; + } + v = MAKE_UINT32(clip8(ss0), clip8(ss1), clip8(ss2), clip8(ss3)); + memcpy(imOut->image[yy] + xx * sizeof(v), &v, sizeof(v)); + } + } + } + } + ImagingSectionLeave(&cookie); +} + +void +ImagingResampleHorizontal_32bpc( + Imaging imOut, Imaging imIn, int offset, int ksize, int *bounds, double *kk) { + ImagingSectionCookie cookie; + double ss; + int xx, yy, x, xmin, xmax; + double *k; + + ImagingSectionEnter(&cookie); + switch (imIn->type) { + case IMAGING_TYPE_INT32: + for (yy = 0; yy < imOut->ysize; yy++) { + for (xx = 0; xx < imOut->xsize; xx++) { + xmin = bounds[xx * 2 + 0]; + xmax = bounds[xx * 2 + 1]; + k = &kk[xx * ksize]; + ss = 0.0; + for (x = 0; x < xmax; x++) { + ss += IMAGING_PIXEL_I(imIn, x + xmin, yy + offset) * k[x]; + } + IMAGING_PIXEL_I(imOut, xx, yy) = ROUND_UP(ss); + } + } + break; + + case IMAGING_TYPE_FLOAT32: + for (yy = 0; yy < imOut->ysize; yy++) { + for (xx = 0; xx < imOut->xsize; xx++) { + xmin = bounds[xx * 2 + 0]; + xmax = bounds[xx * 2 + 1]; + k = &kk[xx * ksize]; + ss = 0.0; + for (x = 0; x < xmax; x++) { + ss += IMAGING_PIXEL_F(imIn, x + xmin, yy + offset) * k[x]; + } + IMAGING_PIXEL_F(imOut, xx, yy) = ss; + } + } + break; + } + ImagingSectionLeave(&cookie); +} + +void +ImagingResampleVertical_32bpc( + Imaging imOut, Imaging imIn, int offset, int ksize, int *bounds, double *kk) { + ImagingSectionCookie cookie; + double ss; + int xx, yy, y, ymin, ymax; + double *k; + + ImagingSectionEnter(&cookie); + switch (imIn->type) { + case IMAGING_TYPE_INT32: + for (yy = 0; yy < imOut->ysize; yy++) { + ymin = bounds[yy * 2 + 0]; + ymax = bounds[yy * 2 + 1]; + k = &kk[yy * ksize]; + for (xx = 0; xx < imOut->xsize; xx++) { + ss = 0.0; + for (y = 0; y < ymax; y++) { + ss += IMAGING_PIXEL_I(imIn, xx, y + ymin) * k[y]; + } + IMAGING_PIXEL_I(imOut, xx, yy) = ROUND_UP(ss); + } + } + break; + + case IMAGING_TYPE_FLOAT32: + for (yy = 0; yy < imOut->ysize; yy++) { + ymin = bounds[yy * 2 + 0]; + ymax = bounds[yy * 2 + 1]; + k = &kk[yy * ksize]; + for (xx = 0; xx < imOut->xsize; xx++) { + ss = 0.0; + for (y = 0; y < ymax; y++) { + ss += IMAGING_PIXEL_F(imIn, xx, y + ymin) * k[y]; + } + IMAGING_PIXEL_F(imOut, xx, yy) = ss; + } + } + break; + } + ImagingSectionLeave(&cookie); +} + +typedef void (*ResampleFunction)( + Imaging imOut, Imaging imIn, int offset, int ksize, int *bounds, double *kk); + +Imaging +ImagingResampleInner( + Imaging imIn, + int xsize, + int ysize, + struct filter *filterp, + float box[4], + ResampleFunction ResampleHorizontal, + ResampleFunction ResampleVertical); + +Imaging +ImagingResample(Imaging imIn, int xsize, int ysize, int filter, float box[4]) { + struct filter *filterp; + ResampleFunction ResampleHorizontal; + ResampleFunction ResampleVertical; + + if (strcmp(imIn->mode, "P") == 0 || strcmp(imIn->mode, "1") == 0) { + return (Imaging)ImagingError_ModeError(); + } + + if (imIn->type == IMAGING_TYPE_SPECIAL) { + return (Imaging)ImagingError_ModeError(); + } else if (imIn->image8) { + ResampleHorizontal = ImagingResampleHorizontal_8bpc; + ResampleVertical = ImagingResampleVertical_8bpc; + } else { + switch (imIn->type) { + case IMAGING_TYPE_UINT8: + ResampleHorizontal = ImagingResampleHorizontal_8bpc; + ResampleVertical = ImagingResampleVertical_8bpc; + break; + case IMAGING_TYPE_INT32: + case IMAGING_TYPE_FLOAT32: + ResampleHorizontal = ImagingResampleHorizontal_32bpc; + ResampleVertical = ImagingResampleVertical_32bpc; + break; + default: + return (Imaging)ImagingError_ModeError(); + } + } + + /* check filter */ + switch (filter) { + case IMAGING_TRANSFORM_BOX: + filterp = &BOX; + break; + case IMAGING_TRANSFORM_BILINEAR: + filterp = &BILINEAR; + break; + case IMAGING_TRANSFORM_HAMMING: + filterp = &HAMMING; + break; + case IMAGING_TRANSFORM_BICUBIC: + filterp = &BICUBIC; + break; + case IMAGING_TRANSFORM_LANCZOS: + filterp = &LANCZOS; + break; + default: + return (Imaging)ImagingError_ValueError("unsupported resampling filter"); + } + + return ImagingResampleInner( + imIn, xsize, ysize, filterp, box, ResampleHorizontal, ResampleVertical); +} + +Imaging +ImagingResampleInner( + Imaging imIn, + int xsize, + int ysize, + struct filter *filterp, + float box[4], + ResampleFunction ResampleHorizontal, + ResampleFunction ResampleVertical) { + Imaging imTemp = NULL; + Imaging imOut = NULL; + + int i, need_horizontal, need_vertical; + int ybox_first, ybox_last; + int ksize_horiz, ksize_vert; + int *bounds_horiz, *bounds_vert; + double *kk_horiz, *kk_vert; + + need_horizontal = xsize != imIn->xsize || box[0] || box[2] != xsize; + need_vertical = ysize != imIn->ysize || box[1] || box[3] != ysize; + + ksize_horiz = precompute_coeffs( + imIn->xsize, box[0], box[2], xsize, filterp, &bounds_horiz, &kk_horiz); + if (!ksize_horiz) { + return NULL; + } + + ksize_vert = precompute_coeffs( + imIn->ysize, box[1], box[3], ysize, filterp, &bounds_vert, &kk_vert); + if (!ksize_vert) { + free(bounds_horiz); + free(kk_horiz); + return NULL; + } + + // First used row in the source image + ybox_first = bounds_vert[0]; + // Last used row in the source image + ybox_last = bounds_vert[ysize * 2 - 2] + bounds_vert[ysize * 2 - 1]; + + /* two-pass resize, horizontal pass */ + if (need_horizontal) { + // Shift bounds for vertical pass + for (i = 0; i < ysize; i++) { + bounds_vert[i * 2] -= ybox_first; + } + + imTemp = ImagingNewDirty(imIn->mode, xsize, ybox_last - ybox_first); + if (imTemp) { + ResampleHorizontal( + imTemp, imIn, ybox_first, ksize_horiz, bounds_horiz, kk_horiz); + } + free(bounds_horiz); + free(kk_horiz); + if (!imTemp) { + free(bounds_vert); + free(kk_vert); + return NULL; + } + imOut = imIn = imTemp; + } else { + // Free in any case + free(bounds_horiz); + free(kk_horiz); + } + + /* vertical pass */ + if (need_vertical) { + imOut = ImagingNewDirty(imIn->mode, imIn->xsize, ysize); + if (imOut) { + /* imIn can be the original image or horizontally resampled one */ + ResampleVertical(imOut, imIn, 0, ksize_vert, bounds_vert, kk_vert); + } + /* it's safe to call ImagingDelete with empty value + if previous step was not performed. */ + ImagingDelete(imTemp); + free(bounds_vert); + free(kk_vert); + if (!imOut) { + return NULL; + } + } else { + // Free in any case + free(bounds_vert); + free(kk_vert); + } + + /* none of the previous steps are performed, copying */ + if (!imOut) { + imOut = ImagingCopy(imIn); + } + + return imOut; +} diff --git a/src/libImaging/Sgi.h b/src/libImaging/Sgi.h index 797e5cbf9..f8d85909f 100644 --- a/src/libImaging/Sgi.h +++ b/src/libImaging/Sgi.h @@ -1,39 +1,39 @@ -/* Sgi.h */ - -typedef struct { - /* CONFIGURATION */ - - /* Number of bytes per channel per pixel */ - int bpc; - - /* RLE offsets table */ - UINT32 *starttab; - - /* RLE lengths table */ - UINT32 *lengthtab; - - /* current row offset */ - UINT32 rleoffset; - - /* current row length */ - UINT32 rlelength; - - /* RLE table size */ - int tablen; - - /* RLE table index */ - int tabindex; - - /* buffer index */ - int bufindex; - - /* current row index */ - int rowno; - - /* current channel index */ - int channo; - - /* image data size from file descriptor */ - long bufsize; - -} SGISTATE; +/* Sgi.h */ + +typedef struct { + /* CONFIGURATION */ + + /* Number of bytes per channel per pixel */ + int bpc; + + /* RLE offsets table */ + UINT32 *starttab; + + /* RLE lengths table */ + UINT32 *lengthtab; + + /* current row offset */ + UINT32 rleoffset; + + /* current row length */ + UINT32 rlelength; + + /* RLE table size */ + int tablen; + + /* RLE table index */ + int tabindex; + + /* buffer index */ + int bufindex; + + /* current row index */ + int rowno; + + /* current channel index */ + int channo; + + /* image data size from file descriptor */ + long bufsize; + +} SGISTATE; diff --git a/src/libImaging/SgiRleDecode.c b/src/libImaging/SgiRleDecode.c index 89dedb525..ef8a6ad86 100644 --- a/src/libImaging/SgiRleDecode.c +++ b/src/libImaging/SgiRleDecode.c @@ -1,287 +1,287 @@ -/* - * The Python Imaging Library. - * $Id$ - * - * decoder for Sgi RLE data. - * - * history: - * 2017-07-28 mb fixed for images larger than 64KB - * 2017-07-20 mb created - * - * Copyright (c) Mickael Bonfill 2017. - * - * See the README file for information on usage and redistribution. - */ - -#include "Imaging.h" -#include "Sgi.h" - -#define SGI_HEADER_SIZE 512 -#define RLE_COPY_FLAG 0x80 -#define RLE_MAX_RUN 0x7f - -static void -read4B(UINT32 *dest, UINT8 *buf) { - *dest = (UINT32)((buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3]); -} - -/* - SgiRleDecoding is done in a single channel row oriented set of RLE chunks. - - * The file is arranged as - - SGI Header - - Rle Offset Table - - Rle Length Table - - Scanline Data - - * Each RLE atom is c->bpc bytes wide (1 or 2) - - * Each RLE Chunk is [specifier atom] [ 1 or n data atoms ] - - * Copy Atoms are a byte with the high bit set, and the low 7 are - the number of bytes to copy from the source to the - destination. e.g. - - CBBBBBBBB or 0CHLHLHLHLHLHL (B=byte, H/L = Hi low bytes) - - * Run atoms do not have the high bit set, and the low 7 bits are - the number of copies of the next atom to copy to the - destination. e.g.: - - RB -> BBBBB or RHL -> HLHLHLHLHL - - The upshot of this is, there is no way to determine the required - length of the input buffer from reloffset and rlelength without - going through the data at that scan line. - - Furthermore, there's no requirement that individual scan lines - pointed to from the rleoffset table are in any sort of order or - used only once, or even disjoint. There's also no requirement that - all of the data in the scan line area of the image file be used - - */ -static int -expandrow(UINT8 *dest, UINT8 *src, int n, int z, int xsize, UINT8 *end_of_buffer) { - /* - * n here is the number of rlechunks - * z is the number of channels, for calculating the interleave - * offset to go to RGBA style pixels - * xsize is the row width - * end_of_buffer is the address of the end of the input buffer - */ - - UINT8 pixel, count; - int x = 0; - - for (; n > 0; n--) { - if (src > end_of_buffer) { - return -1; - } - pixel = *src++; - if (n == 1 && pixel != 0) { - return n; - } - count = pixel & RLE_MAX_RUN; - if (!count) { - return count; - } - if (x + count > xsize) { - return -1; - } - x += count; - if (pixel & RLE_COPY_FLAG) { - if (src + count > end_of_buffer) { - return -1; - } - while (count--) { - *dest = *src++; - dest += z; - } - - } else { - if (src > end_of_buffer) { - return -1; - } - pixel = *src++; - while (count--) { - *dest = pixel; - dest += z; - } - } - } - return 0; -} - -static int -expandrow2( - UINT8 *dest, const UINT8 *src, int n, int z, int xsize, UINT8 *end_of_buffer) { - UINT8 pixel, count; - int x = 0; - - for (; n > 0; n--) { - if (src + 1 > end_of_buffer) { - return -1; - } - pixel = src[1]; - src += 2; - if (n == 1 && pixel != 0) { - return n; - } - count = pixel & RLE_MAX_RUN; - if (!count) { - return count; - } - if (x + count > xsize) { - return -1; - } - x += count; - if (pixel & RLE_COPY_FLAG) { - if (src + 2 * count > end_of_buffer) { - return -1; - } - while (count--) { - memcpy(dest, src, 2); - src += 2; - dest += z * 2; - } - } else { - if (src + 2 > end_of_buffer) { - return -1; - } - while (count--) { - memcpy(dest, src, 2); - dest += z * 2; - } - src += 2; - } - } - return 0; -} - -int -ImagingSgiRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) { - UINT8 *ptr; - SGISTATE *c; - int err = 0; - int status; - - /* size check */ - if (im->xsize > INT_MAX / im->bands || im->ysize > INT_MAX / im->bands) { - state->errcode = IMAGING_CODEC_MEMORY; - return -1; - } - - /* Get all data from File descriptor */ - c = (SGISTATE *)state->context; - _imaging_seek_pyFd(state->fd, 0L, SEEK_END); - c->bufsize = _imaging_tell_pyFd(state->fd); - c->bufsize -= SGI_HEADER_SIZE; - - c->tablen = im->bands * im->ysize; - /* below, we populate the starttab and lentab into the bufsize, - each with 4 bytes per element of tablen - Check here before we allocate any memory - */ - if (c->bufsize < 8 * c->tablen) { - state->errcode = IMAGING_CODEC_OVERRUN; - return -1; - } - - ptr = malloc(sizeof(UINT8) * c->bufsize); - if (!ptr) { - state->errcode = IMAGING_CODEC_MEMORY; - return -1; - } - _imaging_seek_pyFd(state->fd, SGI_HEADER_SIZE, SEEK_SET); - if (_imaging_read_pyFd(state->fd, (char *)ptr, c->bufsize) != c->bufsize) { - state->errcode = IMAGING_CODEC_UNKNOWN; - return -1; - } - - /* decoder initialization */ - state->count = 0; - state->y = 0; - if (state->ystep < 0) { - state->y = im->ysize - 1; - } else { - state->ystep = 1; - } - - /* Allocate memory for RLE tables and rows */ - free(state->buffer); - state->buffer = NULL; - /* malloc overflow check above */ - state->buffer = calloc(im->xsize * im->bands, sizeof(UINT8) * 2); - c->starttab = calloc(c->tablen, sizeof(UINT32)); - c->lengthtab = calloc(c->tablen, sizeof(UINT32)); - if (!state->buffer || !c->starttab || !c->lengthtab) { - err = IMAGING_CODEC_MEMORY; - goto sgi_finish_decode; - } - /* populate offsets table */ - for (c->tabindex = 0, c->bufindex = 0; c->tabindex < c->tablen; - c->tabindex++, c->bufindex += 4) { - read4B(&c->starttab[c->tabindex], &ptr[c->bufindex]); - } - /* populate lengths table */ - for (c->tabindex = 0, c->bufindex = c->tablen * sizeof(UINT32); - c->tabindex < c->tablen; - c->tabindex++, c->bufindex += 4) { - read4B(&c->lengthtab[c->tabindex], &ptr[c->bufindex]); - } - - /* read compressed rows */ - for (c->rowno = 0; c->rowno < im->ysize; c->rowno++, state->y += state->ystep) { - for (c->channo = 0; c->channo < im->bands; c->channo++) { - c->rleoffset = c->starttab[c->rowno + c->channo * im->ysize]; - c->rlelength = c->lengthtab[c->rowno + c->channo * im->ysize]; - - // Check for underflow of rleoffset-SGI_HEADER_SIZE - if (c->rleoffset < SGI_HEADER_SIZE) { - state->errcode = IMAGING_CODEC_OVERRUN; - goto sgi_finish_decode; - } - - c->rleoffset -= SGI_HEADER_SIZE; - - /* row decompression */ - if (c->bpc == 1) { - status = expandrow( - &state->buffer[c->channo], - &ptr[c->rleoffset], - c->rlelength, - im->bands, - im->xsize, - &ptr[c->bufsize - 1]); - } else { - status = expandrow2( - &state->buffer[c->channo * 2], - &ptr[c->rleoffset], - c->rlelength, - im->bands, - im->xsize, - &ptr[c->bufsize - 1]); - } - if (status == -1) { - state->errcode = IMAGING_CODEC_OVERRUN; - goto sgi_finish_decode; - } else if (status == 1) { - goto sgi_finish_decode; - } - } - - /* store decompressed data in image */ - state->shuffle((UINT8 *)im->image[state->y], state->buffer, im->xsize); - } - -sgi_finish_decode:; - - free(c->starttab); - free(c->lengthtab); - free(ptr); - if (err != 0) { - state->errcode = err; - return -1; - } - return 0; -} +/* + * The Python Imaging Library. + * $Id$ + * + * decoder for Sgi RLE data. + * + * history: + * 2017-07-28 mb fixed for images larger than 64KB + * 2017-07-20 mb created + * + * Copyright (c) Mickael Bonfill 2017. + * + * See the README file for information on usage and redistribution. + */ + +#include "Imaging.h" +#include "Sgi.h" + +#define SGI_HEADER_SIZE 512 +#define RLE_COPY_FLAG 0x80 +#define RLE_MAX_RUN 0x7f + +static void +read4B(UINT32 *dest, UINT8 *buf) { + *dest = (UINT32)((buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3]); +} + +/* + SgiRleDecoding is done in a single channel row oriented set of RLE chunks. + + * The file is arranged as + - SGI Header + - Rle Offset Table + - Rle Length Table + - Scanline Data + + * Each RLE atom is c->bpc bytes wide (1 or 2) + + * Each RLE Chunk is [specifier atom] [ 1 or n data atoms ] + + * Copy Atoms are a byte with the high bit set, and the low 7 are + the number of bytes to copy from the source to the + destination. e.g. + + CBBBBBBBB or 0CHLHLHLHLHLHL (B=byte, H/L = Hi low bytes) + + * Run atoms do not have the high bit set, and the low 7 bits are + the number of copies of the next atom to copy to the + destination. e.g.: + + RB -> BBBBB or RHL -> HLHLHLHLHL + + The upshot of this is, there is no way to determine the required + length of the input buffer from reloffset and rlelength without + going through the data at that scan line. + + Furthermore, there's no requirement that individual scan lines + pointed to from the rleoffset table are in any sort of order or + used only once, or even disjoint. There's also no requirement that + all of the data in the scan line area of the image file be used + + */ +static int +expandrow(UINT8 *dest, UINT8 *src, int n, int z, int xsize, UINT8 *end_of_buffer) { + /* + * n here is the number of rlechunks + * z is the number of channels, for calculating the interleave + * offset to go to RGBA style pixels + * xsize is the row width + * end_of_buffer is the address of the end of the input buffer + */ + + UINT8 pixel, count; + int x = 0; + + for (; n > 0; n--) { + if (src > end_of_buffer) { + return -1; + } + pixel = *src++; + if (n == 1 && pixel != 0) { + return n; + } + count = pixel & RLE_MAX_RUN; + if (!count) { + return count; + } + if (x + count > xsize) { + return -1; + } + x += count; + if (pixel & RLE_COPY_FLAG) { + if (src + count > end_of_buffer) { + return -1; + } + while (count--) { + *dest = *src++; + dest += z; + } + + } else { + if (src > end_of_buffer) { + return -1; + } + pixel = *src++; + while (count--) { + *dest = pixel; + dest += z; + } + } + } + return 0; +} + +static int +expandrow2( + UINT8 *dest, const UINT8 *src, int n, int z, int xsize, UINT8 *end_of_buffer) { + UINT8 pixel, count; + int x = 0; + + for (; n > 0; n--) { + if (src + 1 > end_of_buffer) { + return -1; + } + pixel = src[1]; + src += 2; + if (n == 1 && pixel != 0) { + return n; + } + count = pixel & RLE_MAX_RUN; + if (!count) { + return count; + } + if (x + count > xsize) { + return -1; + } + x += count; + if (pixel & RLE_COPY_FLAG) { + if (src + 2 * count > end_of_buffer) { + return -1; + } + while (count--) { + memcpy(dest, src, 2); + src += 2; + dest += z * 2; + } + } else { + if (src + 2 > end_of_buffer) { + return -1; + } + while (count--) { + memcpy(dest, src, 2); + dest += z * 2; + } + src += 2; + } + } + return 0; +} + +int +ImagingSgiRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) { + UINT8 *ptr; + SGISTATE *c; + int err = 0; + int status; + + /* size check */ + if (im->xsize > INT_MAX / im->bands || im->ysize > INT_MAX / im->bands) { + state->errcode = IMAGING_CODEC_MEMORY; + return -1; + } + + /* Get all data from File descriptor */ + c = (SGISTATE *)state->context; + _imaging_seek_pyFd(state->fd, 0L, SEEK_END); + c->bufsize = _imaging_tell_pyFd(state->fd); + c->bufsize -= SGI_HEADER_SIZE; + + c->tablen = im->bands * im->ysize; + /* below, we populate the starttab and lentab into the bufsize, + each with 4 bytes per element of tablen + Check here before we allocate any memory + */ + if (c->bufsize < 8 * c->tablen) { + state->errcode = IMAGING_CODEC_OVERRUN; + return -1; + } + + ptr = malloc(sizeof(UINT8) * c->bufsize); + if (!ptr) { + state->errcode = IMAGING_CODEC_MEMORY; + return -1; + } + _imaging_seek_pyFd(state->fd, SGI_HEADER_SIZE, SEEK_SET); + if (_imaging_read_pyFd(state->fd, (char *)ptr, c->bufsize) != c->bufsize) { + state->errcode = IMAGING_CODEC_UNKNOWN; + return -1; + } + + /* decoder initialization */ + state->count = 0; + state->y = 0; + if (state->ystep < 0) { + state->y = im->ysize - 1; + } else { + state->ystep = 1; + } + + /* Allocate memory for RLE tables and rows */ + free(state->buffer); + state->buffer = NULL; + /* malloc overflow check above */ + state->buffer = calloc(im->xsize * im->bands, sizeof(UINT8) * 2); + c->starttab = calloc(c->tablen, sizeof(UINT32)); + c->lengthtab = calloc(c->tablen, sizeof(UINT32)); + if (!state->buffer || !c->starttab || !c->lengthtab) { + err = IMAGING_CODEC_MEMORY; + goto sgi_finish_decode; + } + /* populate offsets table */ + for (c->tabindex = 0, c->bufindex = 0; c->tabindex < c->tablen; + c->tabindex++, c->bufindex += 4) { + read4B(&c->starttab[c->tabindex], &ptr[c->bufindex]); + } + /* populate lengths table */ + for (c->tabindex = 0, c->bufindex = c->tablen * sizeof(UINT32); + c->tabindex < c->tablen; + c->tabindex++, c->bufindex += 4) { + read4B(&c->lengthtab[c->tabindex], &ptr[c->bufindex]); + } + + /* read compressed rows */ + for (c->rowno = 0; c->rowno < im->ysize; c->rowno++, state->y += state->ystep) { + for (c->channo = 0; c->channo < im->bands; c->channo++) { + c->rleoffset = c->starttab[c->rowno + c->channo * im->ysize]; + c->rlelength = c->lengthtab[c->rowno + c->channo * im->ysize]; + + // Check for underflow of rleoffset-SGI_HEADER_SIZE + if (c->rleoffset < SGI_HEADER_SIZE) { + state->errcode = IMAGING_CODEC_OVERRUN; + goto sgi_finish_decode; + } + + c->rleoffset -= SGI_HEADER_SIZE; + + /* row decompression */ + if (c->bpc == 1) { + status = expandrow( + &state->buffer[c->channo], + &ptr[c->rleoffset], + c->rlelength, + im->bands, + im->xsize, + &ptr[c->bufsize - 1]); + } else { + status = expandrow2( + &state->buffer[c->channo * 2], + &ptr[c->rleoffset], + c->rlelength, + im->bands, + im->xsize, + &ptr[c->bufsize - 1]); + } + if (status == -1) { + state->errcode = IMAGING_CODEC_OVERRUN; + goto sgi_finish_decode; + } else if (status == 1) { + goto sgi_finish_decode; + } + } + + /* store decompressed data in image */ + state->shuffle((UINT8 *)im->image[state->y], state->buffer, im->xsize); + } + +sgi_finish_decode:; + + free(c->starttab); + free(c->lengthtab); + free(ptr); + if (err != 0) { + state->errcode = err; + return -1; + } + return 0; +} diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index b27195a35..2e05a0e17 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -1,576 +1,576 @@ -/* - * The Python Imaging Library - * $Id$ - * - * imaging storage object - * - * This baseline implementation is designed to efficiently handle - * large images, provided they fit into the available memory. - * - * history: - * 1995-06-15 fl Created - * 1995-09-12 fl Updated API, compiles silently under ANSI C++ - * 1995-11-26 fl Compiles silently under Borland 4.5 as well - * 1996-05-05 fl Correctly test status from Prologue - * 1997-05-12 fl Increased THRESHOLD (to speed up Tk interface) - * 1997-05-30 fl Added support for floating point images - * 1997-11-17 fl Added support for "RGBX" images - * 1998-01-11 fl Added support for integer images - * 1998-03-05 fl Exported Prologue/Epilogue functions - * 1998-07-01 fl Added basic "YCrCb" support - * 1998-07-03 fl Attach palette in prologue for "P" images - * 1998-07-09 hk Don't report MemoryError on zero-size images - * 1998-07-12 fl Change "YCrCb" to "YCbCr" (!) - * 1998-10-26 fl Added "I;16" and "I;16B" storage modes (experimental) - * 1998-12-29 fl Fixed allocation bug caused by previous fix - * 1999-02-03 fl Added "RGBa" and "BGR" modes (experimental) - * 2001-04-22 fl Fixed potential memory leak in ImagingCopyPalette - * 2003-09-26 fl Added "LA" and "PA" modes (experimental) - * 2005-10-02 fl Added image counter - * - * Copyright (c) 1998-2005 by Secret Labs AB - * Copyright (c) 1995-2005 by Fredrik Lundh - * - * See the README file for information on usage and redistribution. - */ - -#include "Imaging.h" -#include - -/* -------------------------------------------------------------------- - * Standard image object. - */ - -Imaging -ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { - Imaging im; - - /* linesize overflow check, roughly the current largest space req'd */ - if (xsize > (INT_MAX / 4) - 1) { - return (Imaging)ImagingError_MemoryError(); - } - - im = (Imaging)calloc(1, size); - if (!im) { - return (Imaging)ImagingError_MemoryError(); - } - - /* Setup image descriptor */ - im->xsize = xsize; - im->ysize = ysize; - - im->type = IMAGING_TYPE_UINT8; - - if (strcmp(mode, "1") == 0) { - /* 1-bit images */ - im->bands = im->pixelsize = 1; - im->linesize = xsize; - - } else if (strcmp(mode, "P") == 0) { - /* 8-bit palette mapped images */ - im->bands = im->pixelsize = 1; - im->linesize = xsize; - im->palette = ImagingPaletteNew("RGB"); - - } else if (strcmp(mode, "PA") == 0) { - /* 8-bit palette with alpha */ - im->bands = 2; - im->pixelsize = 4; /* store in image32 memory */ - im->linesize = xsize * 4; - im->palette = ImagingPaletteNew("RGB"); - - } else if (strcmp(mode, "L") == 0) { - /* 8-bit grayscale (luminance) images */ - im->bands = im->pixelsize = 1; - im->linesize = xsize; - - } 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; - - } 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; - - } else if (strcmp(mode, "F") == 0) { - /* 32-bit floating point images */ - im->bands = 1; - im->pixelsize = 4; - im->linesize = xsize * 4; - im->type = IMAGING_TYPE_FLOAT32; - - } else if (strcmp(mode, "I") == 0) { - /* 32-bit integer images */ - im->bands = 1; - im->pixelsize = 4; - im->linesize = xsize * 4; - im->type = IMAGING_TYPE_INT32; - - } else if ( - strcmp(mode, "I;16") == 0 || strcmp(mode, "I;16L") == 0 || - strcmp(mode, "I;16B") == 0 || strcmp(mode, "I;16N") == 0) { - /* EXPERIMENTAL */ - /* 16-bit raw integer images */ - im->bands = 1; - im->pixelsize = 2; - im->linesize = xsize * 2; - im->type = IMAGING_TYPE_SPECIAL; - - } else if (strcmp(mode, "RGB") == 0) { - /* 24-bit true colour images */ - im->bands = 3; - im->pixelsize = 4; - im->linesize = xsize * 4; - - } else if (strcmp(mode, "BGR;15") == 0) { - /* EXPERIMENTAL */ - /* 15-bit reversed true colour */ - im->bands = 3; - im->pixelsize = 2; - im->linesize = (xsize * 2 + 3) & -4; - im->type = IMAGING_TYPE_SPECIAL; - - } else if (strcmp(mode, "BGR;16") == 0) { - /* EXPERIMENTAL */ - /* 16-bit reversed true colour */ - im->bands = 3; - im->pixelsize = 2; - im->linesize = (xsize * 2 + 3) & -4; - im->type = IMAGING_TYPE_SPECIAL; - - } else if (strcmp(mode, "BGR;24") == 0) { - /* EXPERIMENTAL */ - /* 24-bit reversed true colour */ - im->bands = 3; - im->pixelsize = 3; - im->linesize = (xsize * 3 + 3) & -4; - im->type = IMAGING_TYPE_SPECIAL; - - } else if (strcmp(mode, "RGBX") == 0) { - /* 32-bit true colour images with padding */ - im->bands = im->pixelsize = 4; - im->linesize = xsize * 4; - - } else if (strcmp(mode, "RGBA") == 0) { - /* 32-bit true colour images with alpha */ - im->bands = im->pixelsize = 4; - im->linesize = xsize * 4; - - } else if (strcmp(mode, "RGBa") == 0) { - /* 32-bit true colour images with premultiplied alpha */ - im->bands = im->pixelsize = 4; - im->linesize = xsize * 4; - - } else if (strcmp(mode, "CMYK") == 0) { - /* 32-bit colour separation */ - im->bands = im->pixelsize = 4; - im->linesize = xsize * 4; - - } else if (strcmp(mode, "YCbCr") == 0) { - /* 24-bit video format */ - im->bands = 3; - im->pixelsize = 4; - im->linesize = xsize * 4; - - } else if (strcmp(mode, "LAB") == 0) { - /* 24-bit color, luminance, + 2 color channels */ - /* L is uint8, a,b are int8 */ - im->bands = 3; - im->pixelsize = 4; - im->linesize = xsize * 4; - - } else if (strcmp(mode, "HSV") == 0) { - /* 24-bit color, luminance, + 2 color channels */ - /* L is uint8, a,b are int8 */ - im->bands = 3; - im->pixelsize = 4; - im->linesize = xsize * 4; - - } else { - free(im); - return (Imaging)ImagingError_ValueError("unrecognized image mode"); - } - - /* Setup image descriptor */ - strcpy(im->mode, mode); - - /* Pointer array (allocate at least one line, to avoid MemoryError - exceptions on platforms where calloc(0, x) returns NULL) */ - im->image = (char **)calloc((ysize > 0) ? ysize : 1, sizeof(void *)); - - if (!im->image) { - free(im); - return (Imaging)ImagingError_MemoryError(); - } - - /* Initialize alias pointers to pixel data. */ - switch (im->pixelsize) { - case 1: - case 2: - case 3: - im->image8 = (UINT8 **)im->image; - break; - case 4: - im->image32 = (INT32 **)im->image; - break; - } - - ImagingDefaultArena.stats_new_count += 1; - - return im; -} - -Imaging -ImagingNewPrologue(const char *mode, int xsize, int ysize) { - return ImagingNewPrologueSubtype( - mode, xsize, ysize, sizeof(struct ImagingMemoryInstance)); -} - -void -ImagingDelete(Imaging im) { - if (!im) { - return; - } - - if (im->palette) { - ImagingPaletteDelete(im->palette); - } - - if (im->destroy) { - im->destroy(im); - } - - if (im->image) { - free(im->image); - } - - free(im); -} - -/* Array Storage Type */ -/* ------------------ */ -/* Allocate image as an array of line buffers. */ - -#define IMAGING_PAGE_SIZE (4096) - -struct ImagingMemoryArena ImagingDefaultArena = { - 1, // alignment - 16 * 1024 * 1024, // block_size - 0, // blocks_max - 0, // blocks_cached - NULL, // blocks_pool - 0, - 0, - 0, - 0, - 0 // Stats -}; - -int -ImagingMemorySetBlocksMax(ImagingMemoryArena arena, int blocks_max) { - void *p; - /* Free already cached blocks */ - ImagingMemoryClearCache(arena, blocks_max); - - if (blocks_max == 0 && arena->blocks_pool != NULL) { - free(arena->blocks_pool); - arena->blocks_pool = NULL; - } else if (arena->blocks_pool != NULL) { - p = realloc(arena->blocks_pool, sizeof(*arena->blocks_pool) * blocks_max); - if (!p) { - // Leave previous blocks_max value - return 0; - } - arena->blocks_pool = p; - } else { - arena->blocks_pool = calloc(sizeof(*arena->blocks_pool), blocks_max); - if (!arena->blocks_pool) { - return 0; - } - } - arena->blocks_max = blocks_max; - - return 1; -} - -void -ImagingMemoryClearCache(ImagingMemoryArena arena, int new_size) { - while (arena->blocks_cached > new_size) { - arena->blocks_cached -= 1; - free(arena->blocks_pool[arena->blocks_cached].ptr); - arena->stats_freed_blocks += 1; - } -} - -ImagingMemoryBlock -memory_get_block(ImagingMemoryArena arena, int requested_size, int dirty) { - ImagingMemoryBlock block = {NULL, 0}; - - if (arena->blocks_cached > 0) { - // Get block from cache - arena->blocks_cached -= 1; - block = arena->blocks_pool[arena->blocks_cached]; - // Reallocate if needed - if (block.size != requested_size) { - block.ptr = realloc(block.ptr, requested_size); - } - if (!block.ptr) { - // Can't allocate, free previous pointer (it is still valid) - free(arena->blocks_pool[arena->blocks_cached].ptr); - arena->stats_freed_blocks += 1; - return block; - } - if (!dirty) { - memset(block.ptr, 0, requested_size); - } - arena->stats_reused_blocks += 1; - if (block.ptr != arena->blocks_pool[arena->blocks_cached].ptr) { - arena->stats_reallocated_blocks += 1; - } - } else { - if (dirty) { - block.ptr = malloc(requested_size); - } else { - block.ptr = calloc(1, requested_size); - } - arena->stats_allocated_blocks += 1; - } - block.size = requested_size; - return block; -} - -void -memory_return_block(ImagingMemoryArena arena, ImagingMemoryBlock block) { - if (arena->blocks_cached < arena->blocks_max) { - // Reduce block size - if (block.size > arena->block_size) { - block.size = arena->block_size; - block.ptr = realloc(block.ptr, arena->block_size); - } - arena->blocks_pool[arena->blocks_cached] = block; - arena->blocks_cached += 1; - } else { - free(block.ptr); - arena->stats_freed_blocks += 1; - } -} - -static void -ImagingDestroyArray(Imaging im) { - int y = 0; - - if (im->blocks) { - while (im->blocks[y].ptr) { - memory_return_block(&ImagingDefaultArena, im->blocks[y]); - y += 1; - } - free(im->blocks); - } -} - -Imaging -ImagingAllocateArray(Imaging im, int dirty, int block_size) { - int y, line_in_block, current_block; - ImagingMemoryArena arena = &ImagingDefaultArena; - ImagingMemoryBlock block = {NULL, 0}; - int aligned_linesize, lines_per_block, blocks_count; - char *aligned_ptr = NULL; - - /* 0-width or 0-height image. No need to do anything */ - if (!im->linesize || !im->ysize) { - return im; - } - - aligned_linesize = (im->linesize + arena->alignment - 1) & -arena->alignment; - lines_per_block = (block_size - (arena->alignment - 1)) / aligned_linesize; - if (lines_per_block == 0) { - lines_per_block = 1; - } - blocks_count = (im->ysize + lines_per_block - 1) / lines_per_block; - // printf("NEW size: %dx%d, ls: %d, lpb: %d, blocks: %d\n", - // im->xsize, im->ysize, aligned_linesize, lines_per_block, blocks_count); - - /* One extra pointer is always NULL */ - im->blocks = calloc(sizeof(*im->blocks), blocks_count + 1); - if (!im->blocks) { - return (Imaging)ImagingError_MemoryError(); - } - - /* Allocate image as an array of lines */ - line_in_block = 0; - current_block = 0; - for (y = 0; y < im->ysize; y++) { - if (line_in_block == 0) { - int required; - int lines_remaining = lines_per_block; - if (lines_remaining > im->ysize - y) { - lines_remaining = im->ysize - y; - } - required = lines_remaining * aligned_linesize + arena->alignment - 1; - block = memory_get_block(arena, required, dirty); - if (!block.ptr) { - ImagingDestroyArray(im); - return (Imaging)ImagingError_MemoryError(); - } - im->blocks[current_block] = block; - /* Bulletproof code from libc _int_memalign */ - aligned_ptr = (char *)(((size_t)(block.ptr + arena->alignment - 1)) & - -((Py_ssize_t)arena->alignment)); - } - - im->image[y] = aligned_ptr + aligned_linesize * line_in_block; - - line_in_block += 1; - if (line_in_block >= lines_per_block) { - /* Reset counter and start new block */ - line_in_block = 0; - current_block += 1; - } - } - - im->destroy = ImagingDestroyArray; - - return im; -} - -/* Block Storage Type */ -/* ------------------ */ -/* Allocate image as a single block. */ - -static void -ImagingDestroyBlock(Imaging im) { - if (im->block) { - free(im->block); - } -} - -Imaging -ImagingAllocateBlock(Imaging im) { - Py_ssize_t y, i; - - /* overflow check for malloc */ - if (im->linesize && im->ysize > INT_MAX / im->linesize) { - return (Imaging)ImagingError_MemoryError(); - } - - if (im->ysize * im->linesize <= 0) { - /* some platforms return NULL for malloc(0); this fix - prevents MemoryError on zero-sized images on such - platforms */ - im->block = (char *)malloc(1); - } else { - /* malloc check ok, overflow check above */ - im->block = (char *)calloc(im->ysize, im->linesize); - } - - if (!im->block) { - return (Imaging)ImagingError_MemoryError(); - } - - for (y = i = 0; y < im->ysize; y++) { - im->image[y] = im->block + i; - i += im->linesize; - } - - im->destroy = ImagingDestroyBlock; - - return im; -} - -/* -------------------------------------------------------------------- - * Create a new, internally allocated, image. - */ - -Imaging -ImagingNewInternal(const char *mode, int xsize, int ysize, int dirty) { - Imaging im; - - if (xsize < 0 || ysize < 0) { - return (Imaging)ImagingError_ValueError("bad image size"); - } - - im = ImagingNewPrologue(mode, xsize, ysize); - if (!im) { - return NULL; - } - - if (ImagingAllocateArray(im, dirty, ImagingDefaultArena.block_size)) { - return im; - } - - ImagingError_Clear(); - - // Try to allocate the image once more with smallest possible block size - if (ImagingAllocateArray(im, dirty, IMAGING_PAGE_SIZE)) { - return im; - } - - ImagingDelete(im); - return NULL; -} - -Imaging -ImagingNew(const char *mode, int xsize, int ysize) { - return ImagingNewInternal(mode, xsize, ysize, 0); -} - -Imaging -ImagingNewDirty(const char *mode, int xsize, int ysize) { - return ImagingNewInternal(mode, xsize, ysize, 1); -} - -Imaging -ImagingNewBlock(const char *mode, int xsize, int ysize) { - Imaging im; - - if (xsize < 0 || ysize < 0) { - return (Imaging)ImagingError_ValueError("bad image size"); - } - - im = ImagingNewPrologue(mode, xsize, ysize); - if (!im) { - return NULL; - } - - if (ImagingAllocateBlock(im)) { - return im; - } - - ImagingDelete(im); - return NULL; -} - -Imaging -ImagingNew2Dirty(const char *mode, Imaging imOut, Imaging imIn) { - /* allocate or validate output image */ - - if (imOut) { - /* make sure images match */ - if (strcmp(imOut->mode, mode) != 0 || imOut->xsize != imIn->xsize || - imOut->ysize != imIn->ysize) { - return ImagingError_Mismatch(); - } - } else { - /* create new image */ - imOut = ImagingNewDirty(mode, imIn->xsize, imIn->ysize); - if (!imOut) { - return NULL; - } - } - - return imOut; -} - -void -ImagingCopyPalette(Imaging destination, Imaging source) { - if (source->palette) { - if (destination->palette) { - ImagingPaletteDelete(destination->palette); - } - destination->palette = ImagingPaletteDuplicate(source->palette); - } -} +/* + * The Python Imaging Library + * $Id$ + * + * imaging storage object + * + * This baseline implementation is designed to efficiently handle + * large images, provided they fit into the available memory. + * + * history: + * 1995-06-15 fl Created + * 1995-09-12 fl Updated API, compiles silently under ANSI C++ + * 1995-11-26 fl Compiles silently under Borland 4.5 as well + * 1996-05-05 fl Correctly test status from Prologue + * 1997-05-12 fl Increased THRESHOLD (to speed up Tk interface) + * 1997-05-30 fl Added support for floating point images + * 1997-11-17 fl Added support for "RGBX" images + * 1998-01-11 fl Added support for integer images + * 1998-03-05 fl Exported Prologue/Epilogue functions + * 1998-07-01 fl Added basic "YCrCb" support + * 1998-07-03 fl Attach palette in prologue for "P" images + * 1998-07-09 hk Don't report MemoryError on zero-size images + * 1998-07-12 fl Change "YCrCb" to "YCbCr" (!) + * 1998-10-26 fl Added "I;16" and "I;16B" storage modes (experimental) + * 1998-12-29 fl Fixed allocation bug caused by previous fix + * 1999-02-03 fl Added "RGBa" and "BGR" modes (experimental) + * 2001-04-22 fl Fixed potential memory leak in ImagingCopyPalette + * 2003-09-26 fl Added "LA" and "PA" modes (experimental) + * 2005-10-02 fl Added image counter + * + * Copyright (c) 1998-2005 by Secret Labs AB + * Copyright (c) 1995-2005 by Fredrik Lundh + * + * See the README file for information on usage and redistribution. + */ + +#include "Imaging.h" +#include + +/* -------------------------------------------------------------------- + * Standard image object. + */ + +Imaging +ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { + Imaging im; + + /* linesize overflow check, roughly the current largest space req'd */ + if (xsize > (INT_MAX / 4) - 1) { + return (Imaging)ImagingError_MemoryError(); + } + + im = (Imaging)calloc(1, size); + if (!im) { + return (Imaging)ImagingError_MemoryError(); + } + + /* Setup image descriptor */ + im->xsize = xsize; + im->ysize = ysize; + + im->type = IMAGING_TYPE_UINT8; + + if (strcmp(mode, "1") == 0) { + /* 1-bit images */ + im->bands = im->pixelsize = 1; + im->linesize = xsize; + + } else if (strcmp(mode, "P") == 0) { + /* 8-bit palette mapped images */ + im->bands = im->pixelsize = 1; + im->linesize = xsize; + im->palette = ImagingPaletteNew("RGB"); + + } else if (strcmp(mode, "PA") == 0) { + /* 8-bit palette with alpha */ + im->bands = 2; + im->pixelsize = 4; /* store in image32 memory */ + im->linesize = xsize * 4; + im->palette = ImagingPaletteNew("RGB"); + + } else if (strcmp(mode, "L") == 0) { + /* 8-bit grayscale (luminance) images */ + im->bands = im->pixelsize = 1; + im->linesize = xsize; + + } 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; + + } 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; + + } else if (strcmp(mode, "F") == 0) { + /* 32-bit floating point images */ + im->bands = 1; + im->pixelsize = 4; + im->linesize = xsize * 4; + im->type = IMAGING_TYPE_FLOAT32; + + } else if (strcmp(mode, "I") == 0) { + /* 32-bit integer images */ + im->bands = 1; + im->pixelsize = 4; + im->linesize = xsize * 4; + im->type = IMAGING_TYPE_INT32; + + } else if ( + strcmp(mode, "I;16") == 0 || strcmp(mode, "I;16L") == 0 || + strcmp(mode, "I;16B") == 0 || strcmp(mode, "I;16N") == 0) { + /* EXPERIMENTAL */ + /* 16-bit raw integer images */ + im->bands = 1; + im->pixelsize = 2; + im->linesize = xsize * 2; + im->type = IMAGING_TYPE_SPECIAL; + + } else if (strcmp(mode, "RGB") == 0) { + /* 24-bit true colour images */ + im->bands = 3; + im->pixelsize = 4; + im->linesize = xsize * 4; + + } else if (strcmp(mode, "BGR;15") == 0) { + /* EXPERIMENTAL */ + /* 15-bit reversed true colour */ + im->bands = 3; + im->pixelsize = 2; + im->linesize = (xsize * 2 + 3) & -4; + im->type = IMAGING_TYPE_SPECIAL; + + } else if (strcmp(mode, "BGR;16") == 0) { + /* EXPERIMENTAL */ + /* 16-bit reversed true colour */ + im->bands = 3; + im->pixelsize = 2; + im->linesize = (xsize * 2 + 3) & -4; + im->type = IMAGING_TYPE_SPECIAL; + + } else if (strcmp(mode, "BGR;24") == 0) { + /* EXPERIMENTAL */ + /* 24-bit reversed true colour */ + im->bands = 3; + im->pixelsize = 3; + im->linesize = (xsize * 3 + 3) & -4; + im->type = IMAGING_TYPE_SPECIAL; + + } else if (strcmp(mode, "RGBX") == 0) { + /* 32-bit true colour images with padding */ + im->bands = im->pixelsize = 4; + im->linesize = xsize * 4; + + } else if (strcmp(mode, "RGBA") == 0) { + /* 32-bit true colour images with alpha */ + im->bands = im->pixelsize = 4; + im->linesize = xsize * 4; + + } else if (strcmp(mode, "RGBa") == 0) { + /* 32-bit true colour images with premultiplied alpha */ + im->bands = im->pixelsize = 4; + im->linesize = xsize * 4; + + } else if (strcmp(mode, "CMYK") == 0) { + /* 32-bit colour separation */ + im->bands = im->pixelsize = 4; + im->linesize = xsize * 4; + + } else if (strcmp(mode, "YCbCr") == 0) { + /* 24-bit video format */ + im->bands = 3; + im->pixelsize = 4; + im->linesize = xsize * 4; + + } else if (strcmp(mode, "LAB") == 0) { + /* 24-bit color, luminance, + 2 color channels */ + /* L is uint8, a,b are int8 */ + im->bands = 3; + im->pixelsize = 4; + im->linesize = xsize * 4; + + } else if (strcmp(mode, "HSV") == 0) { + /* 24-bit color, luminance, + 2 color channels */ + /* L is uint8, a,b are int8 */ + im->bands = 3; + im->pixelsize = 4; + im->linesize = xsize * 4; + + } else { + free(im); + return (Imaging)ImagingError_ValueError("unrecognized image mode"); + } + + /* Setup image descriptor */ + strcpy(im->mode, mode); + + /* Pointer array (allocate at least one line, to avoid MemoryError + exceptions on platforms where calloc(0, x) returns NULL) */ + im->image = (char **)calloc((ysize > 0) ? ysize : 1, sizeof(void *)); + + if (!im->image) { + free(im); + return (Imaging)ImagingError_MemoryError(); + } + + /* Initialize alias pointers to pixel data. */ + switch (im->pixelsize) { + case 1: + case 2: + case 3: + im->image8 = (UINT8 **)im->image; + break; + case 4: + im->image32 = (INT32 **)im->image; + break; + } + + ImagingDefaultArena.stats_new_count += 1; + + return im; +} + +Imaging +ImagingNewPrologue(const char *mode, int xsize, int ysize) { + return ImagingNewPrologueSubtype( + mode, xsize, ysize, sizeof(struct ImagingMemoryInstance)); +} + +void +ImagingDelete(Imaging im) { + if (!im) { + return; + } + + if (im->palette) { + ImagingPaletteDelete(im->palette); + } + + if (im->destroy) { + im->destroy(im); + } + + if (im->image) { + free(im->image); + } + + free(im); +} + +/* Array Storage Type */ +/* ------------------ */ +/* Allocate image as an array of line buffers. */ + +#define IMAGING_PAGE_SIZE (4096) + +struct ImagingMemoryArena ImagingDefaultArena = { + 1, // alignment + 16 * 1024 * 1024, // block_size + 0, // blocks_max + 0, // blocks_cached + NULL, // blocks_pool + 0, + 0, + 0, + 0, + 0 // Stats +}; + +int +ImagingMemorySetBlocksMax(ImagingMemoryArena arena, int blocks_max) { + void *p; + /* Free already cached blocks */ + ImagingMemoryClearCache(arena, blocks_max); + + if (blocks_max == 0 && arena->blocks_pool != NULL) { + free(arena->blocks_pool); + arena->blocks_pool = NULL; + } else if (arena->blocks_pool != NULL) { + p = realloc(arena->blocks_pool, sizeof(*arena->blocks_pool) * blocks_max); + if (!p) { + // Leave previous blocks_max value + return 0; + } + arena->blocks_pool = p; + } else { + arena->blocks_pool = calloc(sizeof(*arena->blocks_pool), blocks_max); + if (!arena->blocks_pool) { + return 0; + } + } + arena->blocks_max = blocks_max; + + return 1; +} + +void +ImagingMemoryClearCache(ImagingMemoryArena arena, int new_size) { + while (arena->blocks_cached > new_size) { + arena->blocks_cached -= 1; + free(arena->blocks_pool[arena->blocks_cached].ptr); + arena->stats_freed_blocks += 1; + } +} + +ImagingMemoryBlock +memory_get_block(ImagingMemoryArena arena, int requested_size, int dirty) { + ImagingMemoryBlock block = {NULL, 0}; + + if (arena->blocks_cached > 0) { + // Get block from cache + arena->blocks_cached -= 1; + block = arena->blocks_pool[arena->blocks_cached]; + // Reallocate if needed + if (block.size != requested_size) { + block.ptr = realloc(block.ptr, requested_size); + } + if (!block.ptr) { + // Can't allocate, free previous pointer (it is still valid) + free(arena->blocks_pool[arena->blocks_cached].ptr); + arena->stats_freed_blocks += 1; + return block; + } + if (!dirty) { + memset(block.ptr, 0, requested_size); + } + arena->stats_reused_blocks += 1; + if (block.ptr != arena->blocks_pool[arena->blocks_cached].ptr) { + arena->stats_reallocated_blocks += 1; + } + } else { + if (dirty) { + block.ptr = malloc(requested_size); + } else { + block.ptr = calloc(1, requested_size); + } + arena->stats_allocated_blocks += 1; + } + block.size = requested_size; + return block; +} + +void +memory_return_block(ImagingMemoryArena arena, ImagingMemoryBlock block) { + if (arena->blocks_cached < arena->blocks_max) { + // Reduce block size + if (block.size > arena->block_size) { + block.size = arena->block_size; + block.ptr = realloc(block.ptr, arena->block_size); + } + arena->blocks_pool[arena->blocks_cached] = block; + arena->blocks_cached += 1; + } else { + free(block.ptr); + arena->stats_freed_blocks += 1; + } +} + +static void +ImagingDestroyArray(Imaging im) { + int y = 0; + + if (im->blocks) { + while (im->blocks[y].ptr) { + memory_return_block(&ImagingDefaultArena, im->blocks[y]); + y += 1; + } + free(im->blocks); + } +} + +Imaging +ImagingAllocateArray(Imaging im, int dirty, int block_size) { + int y, line_in_block, current_block; + ImagingMemoryArena arena = &ImagingDefaultArena; + ImagingMemoryBlock block = {NULL, 0}; + int aligned_linesize, lines_per_block, blocks_count; + char *aligned_ptr = NULL; + + /* 0-width or 0-height image. No need to do anything */ + if (!im->linesize || !im->ysize) { + return im; + } + + aligned_linesize = (im->linesize + arena->alignment - 1) & -arena->alignment; + lines_per_block = (block_size - (arena->alignment - 1)) / aligned_linesize; + if (lines_per_block == 0) { + lines_per_block = 1; + } + blocks_count = (im->ysize + lines_per_block - 1) / lines_per_block; + // printf("NEW size: %dx%d, ls: %d, lpb: %d, blocks: %d\n", + // im->xsize, im->ysize, aligned_linesize, lines_per_block, blocks_count); + + /* One extra pointer is always NULL */ + im->blocks = calloc(sizeof(*im->blocks), blocks_count + 1); + if (!im->blocks) { + return (Imaging)ImagingError_MemoryError(); + } + + /* Allocate image as an array of lines */ + line_in_block = 0; + current_block = 0; + for (y = 0; y < im->ysize; y++) { + if (line_in_block == 0) { + int required; + int lines_remaining = lines_per_block; + if (lines_remaining > im->ysize - y) { + lines_remaining = im->ysize - y; + } + required = lines_remaining * aligned_linesize + arena->alignment - 1; + block = memory_get_block(arena, required, dirty); + if (!block.ptr) { + ImagingDestroyArray(im); + return (Imaging)ImagingError_MemoryError(); + } + im->blocks[current_block] = block; + /* Bulletproof code from libc _int_memalign */ + aligned_ptr = (char *)(((size_t)(block.ptr + arena->alignment - 1)) & + -((Py_ssize_t)arena->alignment)); + } + + im->image[y] = aligned_ptr + aligned_linesize * line_in_block; + + line_in_block += 1; + if (line_in_block >= lines_per_block) { + /* Reset counter and start new block */ + line_in_block = 0; + current_block += 1; + } + } + + im->destroy = ImagingDestroyArray; + + return im; +} + +/* Block Storage Type */ +/* ------------------ */ +/* Allocate image as a single block. */ + +static void +ImagingDestroyBlock(Imaging im) { + if (im->block) { + free(im->block); + } +} + +Imaging +ImagingAllocateBlock(Imaging im) { + Py_ssize_t y, i; + + /* overflow check for malloc */ + if (im->linesize && im->ysize > INT_MAX / im->linesize) { + return (Imaging)ImagingError_MemoryError(); + } + + if (im->ysize * im->linesize <= 0) { + /* some platforms return NULL for malloc(0); this fix + prevents MemoryError on zero-sized images on such + platforms */ + im->block = (char *)malloc(1); + } else { + /* malloc check ok, overflow check above */ + im->block = (char *)calloc(im->ysize, im->linesize); + } + + if (!im->block) { + return (Imaging)ImagingError_MemoryError(); + } + + for (y = i = 0; y < im->ysize; y++) { + im->image[y] = im->block + i; + i += im->linesize; + } + + im->destroy = ImagingDestroyBlock; + + return im; +} + +/* -------------------------------------------------------------------- + * Create a new, internally allocated, image. + */ + +Imaging +ImagingNewInternal(const char *mode, int xsize, int ysize, int dirty) { + Imaging im; + + if (xsize < 0 || ysize < 0) { + return (Imaging)ImagingError_ValueError("bad image size"); + } + + im = ImagingNewPrologue(mode, xsize, ysize); + if (!im) { + return NULL; + } + + if (ImagingAllocateArray(im, dirty, ImagingDefaultArena.block_size)) { + return im; + } + + ImagingError_Clear(); + + // Try to allocate the image once more with smallest possible block size + if (ImagingAllocateArray(im, dirty, IMAGING_PAGE_SIZE)) { + return im; + } + + ImagingDelete(im); + return NULL; +} + +Imaging +ImagingNew(const char *mode, int xsize, int ysize) { + return ImagingNewInternal(mode, xsize, ysize, 0); +} + +Imaging +ImagingNewDirty(const char *mode, int xsize, int ysize) { + return ImagingNewInternal(mode, xsize, ysize, 1); +} + +Imaging +ImagingNewBlock(const char *mode, int xsize, int ysize) { + Imaging im; + + if (xsize < 0 || ysize < 0) { + return (Imaging)ImagingError_ValueError("bad image size"); + } + + im = ImagingNewPrologue(mode, xsize, ysize); + if (!im) { + return NULL; + } + + if (ImagingAllocateBlock(im)) { + return im; + } + + ImagingDelete(im); + return NULL; +} + +Imaging +ImagingNew2Dirty(const char *mode, Imaging imOut, Imaging imIn) { + /* allocate or validate output image */ + + if (imOut) { + /* make sure images match */ + if (strcmp(imOut->mode, mode) != 0 || imOut->xsize != imIn->xsize || + imOut->ysize != imIn->ysize) { + return ImagingError_Mismatch(); + } + } else { + /* create new image */ + imOut = ImagingNewDirty(mode, imIn->xsize, imIn->ysize); + if (!imOut) { + return NULL; + } + } + + return imOut; +} + +void +ImagingCopyPalette(Imaging destination, Imaging source) { + if (source->palette) { + if (destination->palette) { + ImagingPaletteDelete(destination->palette); + } + destination->palette = ImagingPaletteDuplicate(source->palette); + } +} diff --git a/src/libImaging/SunRleDecode.c b/src/libImaging/SunRleDecode.c index 9d8e1292a..ae4ba0f07 100644 --- a/src/libImaging/SunRleDecode.c +++ b/src/libImaging/SunRleDecode.c @@ -1,139 +1,139 @@ -/* - * THIS IS WORK IN PROGRESS - * - * The Python Imaging Library. - * $Id$ - * - * decoder for SUN RLE data. - * - * history: - * 97-01-04 fl Created - * - * Copyright (c) Fredrik Lundh 1997. - * Copyright (c) Secret Labs AB 1997. - * - * See the README file for information on usage and redistribution. - */ - -#include "Imaging.h" - -int -ImagingSunRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) { - int n; - UINT8 *ptr; - UINT8 extra_data = 0; - UINT8 extra_bytes = 0; - - ptr = buf; - - for (;;) { - if (bytes < 1) { - return ptr - buf; - } - - if (ptr[0] == 0x80) { - if (bytes < 2) { - break; - } - - n = ptr[1]; - - if (n == 0) { - /* Literal 0x80 (2 bytes) */ - n = 1; - - state->buffer[state->x] = 0x80; - - ptr += 2; - bytes -= 2; - - } else { - /* Run (3 bytes) */ - if (bytes < 3) { - break; - } - - /* from (https://www.fileformat.info/format/sunraster/egff.htm) - - For example, a run of 100 pixels with the value of - 0Ah would encode as the values 80h 64h 0Ah. A - single pixel value of 80h would encode as the - values 80h 00h. The four unencoded bytes 12345678h - would be stored in the RLE stream as 12h 34h 56h - 78h. 100 pixels, n=100, not 100 pixels, n=99. - - But Wait! There's More! - (https://www.fileformat.info/format/sunraster/spec/598a59c4fac64c52897585d390d86360/view.htm) - - If the first byte is 0x80, and the second byte is - not zero, the record is three bytes long. The - second byte is a count and the third byte is a - value. Output (count+1) pixels of that value. - - 2 specs, same site, but Imagemagick and GIMP seem - to agree on the second one. - */ - n += 1; - - if (state->x + n > state->bytes) { - extra_bytes = n; /* full value */ - n = state->bytes - state->x; - extra_bytes -= n; - extra_data = ptr[2]; - } - - memset(state->buffer + state->x, ptr[2], n); - - ptr += 3; - bytes -= 3; - } - - } else { - /* Literal byte */ - n = 1; - - state->buffer[state->x] = ptr[0]; - - ptr += 1; - bytes -= 1; - } - - for (;;) { - state->x += n; - - if (state->x >= state->bytes) { - /* Got a full line, unpack it */ - state->shuffle( - (UINT8 *)im->image[state->y + state->yoff] + - state->xoff * im->pixelsize, - state->buffer, - state->xsize); - - state->x = 0; - - if (++state->y >= state->ysize) { - /* End of file (errcode = 0) */ - return -1; - } - } - - if (extra_bytes == 0) { - break; - } - - if (state->x > 0) { - break; // assert - } - - if (extra_bytes >= state->bytes) { - n = state->bytes; - } else { - n = extra_bytes; - } - memset(state->buffer + state->x, extra_data, n); - extra_bytes -= n; - } - } - - return ptr - buf; -} +/* + * THIS IS WORK IN PROGRESS + * + * The Python Imaging Library. + * $Id$ + * + * decoder for SUN RLE data. + * + * history: + * 97-01-04 fl Created + * + * Copyright (c) Fredrik Lundh 1997. + * Copyright (c) Secret Labs AB 1997. + * + * See the README file for information on usage and redistribution. + */ + +#include "Imaging.h" + +int +ImagingSunRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) { + int n; + UINT8 *ptr; + UINT8 extra_data = 0; + UINT8 extra_bytes = 0; + + ptr = buf; + + for (;;) { + if (bytes < 1) { + return ptr - buf; + } + + if (ptr[0] == 0x80) { + if (bytes < 2) { + break; + } + + n = ptr[1]; + + if (n == 0) { + /* Literal 0x80 (2 bytes) */ + n = 1; + + state->buffer[state->x] = 0x80; + + ptr += 2; + bytes -= 2; + + } else { + /* Run (3 bytes) */ + if (bytes < 3) { + break; + } + + /* from (https://www.fileformat.info/format/sunraster/egff.htm) + + For example, a run of 100 pixels with the value of + 0Ah would encode as the values 80h 64h 0Ah. A + single pixel value of 80h would encode as the + values 80h 00h. The four unencoded bytes 12345678h + would be stored in the RLE stream as 12h 34h 56h + 78h. 100 pixels, n=100, not 100 pixels, n=99. + + But Wait! There's More! + (https://www.fileformat.info/format/sunraster/spec/598a59c4fac64c52897585d390d86360/view.htm) + + If the first byte is 0x80, and the second byte is + not zero, the record is three bytes long. The + second byte is a count and the third byte is a + value. Output (count+1) pixels of that value. + + 2 specs, same site, but Imagemagick and GIMP seem + to agree on the second one. + */ + n += 1; + + if (state->x + n > state->bytes) { + extra_bytes = n; /* full value */ + n = state->bytes - state->x; + extra_bytes -= n; + extra_data = ptr[2]; + } + + memset(state->buffer + state->x, ptr[2], n); + + ptr += 3; + bytes -= 3; + } + + } else { + /* Literal byte */ + n = 1; + + state->buffer[state->x] = ptr[0]; + + ptr += 1; + bytes -= 1; + } + + for (;;) { + state->x += n; + + if (state->x >= state->bytes) { + /* Got a full line, unpack it */ + state->shuffle( + (UINT8 *)im->image[state->y + state->yoff] + + state->xoff * im->pixelsize, + state->buffer, + state->xsize); + + state->x = 0; + + if (++state->y >= state->ysize) { + /* End of file (errcode = 0) */ + return -1; + } + } + + if (extra_bytes == 0) { + break; + } + + if (state->x > 0) { + break; // assert + } + + if (extra_bytes >= state->bytes) { + n = state->bytes; + } else { + n = extra_bytes; + } + memset(state->buffer + state->x, extra_data, n); + extra_bytes -= n; + } + } + + return ptr - buf; +} diff --git a/src/libImaging/TgaRleDecode.c b/src/libImaging/TgaRleDecode.c index 95ae9b622..aeb8ded7d 100644 --- a/src/libImaging/TgaRleDecode.c +++ b/src/libImaging/TgaRleDecode.c @@ -1,129 +1,129 @@ -/* - * The Python Imaging Library. - * $Id$ - * - * decoder for Targa RLE data. - * - * history: - * 97-01-04 fl created - * 98-09-11 fl don't one byte per pixel; take orientation into account - * - * Copyright (c) Fredrik Lundh 1997. - * Copyright (c) Secret Labs AB 1997-98. - * - * See the README file for information on usage and redistribution. - */ - -#include "Imaging.h" - -int -ImagingTgaRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) { - int n, depth; - UINT8 *ptr; - int extra_bytes = 0; - - ptr = buf; - - if (state->state == 0) { - /* check image orientation */ - if (state->ystep < 0) { - state->y = state->ysize - 1; - state->ystep = -1; - } else { - state->ystep = 1; - } - - state->state = 1; - } - - depth = state->count; - - for (;;) { - if (bytes < 1) { - return ptr - buf; - } - - n = depth * ((ptr[0] & 0x7f) + 1); - if (ptr[0] & 0x80) { - /* Run (1 + pixelsize bytes) */ - if (bytes < 1 + depth) { - break; - } - - if (state->x + n > state->bytes) { - state->errcode = IMAGING_CODEC_OVERRUN; - return -1; - } - - if (depth == 1) { - memset(state->buffer + state->x, ptr[1], n); - } else { - int i; - for (i = 0; i < n; i += depth) { - memcpy(state->buffer + state->x + i, ptr + 1, depth); - } - } - - ptr += 1 + depth; - bytes -= 1 + depth; - } else { - /* Literal (1+n+1 bytes block) */ - if (bytes < 1 + n) { - break; - } - - if (state->x + n > state->bytes) { - extra_bytes = n; /* full value */ - n = state->bytes - state->x; - extra_bytes -= n; - } - - memcpy(state->buffer + state->x, ptr + 1, n); - - ptr += 1 + n; - bytes -= 1 + n; - } - - for (;;) { - state->x += n; - - if (state->x >= state->bytes) { - /* Got a full line, unpack it */ - state->shuffle( - (UINT8 *)im->image[state->y + state->yoff] + - state->xoff * im->pixelsize, - state->buffer, - state->xsize); - - state->x = 0; - - state->y += state->ystep; - - if (state->y < 0 || state->y >= state->ysize) { - /* End of file (errcode = 0) */ - return -1; - } - } - - if (extra_bytes == 0) { - break; - } - - if (state->x > 0) { - break; // assert - } - - if (extra_bytes >= state->bytes) { - n = state->bytes; - } else { - n = extra_bytes; - } - memcpy(state->buffer + state->x, ptr, n); - ptr += n; - bytes -= n; - extra_bytes -= n; - } - } - - return ptr - buf; -} +/* + * The Python Imaging Library. + * $Id$ + * + * decoder for Targa RLE data. + * + * history: + * 97-01-04 fl created + * 98-09-11 fl don't one byte per pixel; take orientation into account + * + * Copyright (c) Fredrik Lundh 1997. + * Copyright (c) Secret Labs AB 1997-98. + * + * See the README file for information on usage and redistribution. + */ + +#include "Imaging.h" + +int +ImagingTgaRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) { + int n, depth; + UINT8 *ptr; + int extra_bytes = 0; + + ptr = buf; + + if (state->state == 0) { + /* check image orientation */ + if (state->ystep < 0) { + state->y = state->ysize - 1; + state->ystep = -1; + } else { + state->ystep = 1; + } + + state->state = 1; + } + + depth = state->count; + + for (;;) { + if (bytes < 1) { + return ptr - buf; + } + + n = depth * ((ptr[0] & 0x7f) + 1); + if (ptr[0] & 0x80) { + /* Run (1 + pixelsize bytes) */ + if (bytes < 1 + depth) { + break; + } + + if (state->x + n > state->bytes) { + state->errcode = IMAGING_CODEC_OVERRUN; + return -1; + } + + if (depth == 1) { + memset(state->buffer + state->x, ptr[1], n); + } else { + int i; + for (i = 0; i < n; i += depth) { + memcpy(state->buffer + state->x + i, ptr + 1, depth); + } + } + + ptr += 1 + depth; + bytes -= 1 + depth; + } else { + /* Literal (1+n+1 bytes block) */ + if (bytes < 1 + n) { + break; + } + + if (state->x + n > state->bytes) { + extra_bytes = n; /* full value */ + n = state->bytes - state->x; + extra_bytes -= n; + } + + memcpy(state->buffer + state->x, ptr + 1, n); + + ptr += 1 + n; + bytes -= 1 + n; + } + + for (;;) { + state->x += n; + + if (state->x >= state->bytes) { + /* Got a full line, unpack it */ + state->shuffle( + (UINT8 *)im->image[state->y + state->yoff] + + state->xoff * im->pixelsize, + state->buffer, + state->xsize); + + state->x = 0; + + state->y += state->ystep; + + if (state->y < 0 || state->y >= state->ysize) { + /* End of file (errcode = 0) */ + return -1; + } + } + + if (extra_bytes == 0) { + break; + } + + if (state->x > 0) { + break; // assert + } + + if (extra_bytes >= state->bytes) { + n = state->bytes; + } else { + n = extra_bytes; + } + memcpy(state->buffer + state->x, ptr, n); + ptr += n; + bytes -= n; + extra_bytes -= n; + } + } + + return ptr - buf; +} diff --git a/src/libImaging/TgaRleEncode.c b/src/libImaging/TgaRleEncode.c index aa7e7b96d..38154b0a1 100644 --- a/src/libImaging/TgaRleEncode.c +++ b/src/libImaging/TgaRleEncode.c @@ -1,157 +1,157 @@ - -#include "Imaging.h" - -#include -#include - -static int -comparePixels(const UINT8 *buf, int x, int bytesPerPixel) { - buf += x * bytesPerPixel; - return memcmp(buf, buf + bytesPerPixel, bytesPerPixel) == 0; -} - -int -ImagingTgaRleEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { - UINT8 *dst; - int bytesPerPixel; - - if (state->state == 0) { - if (state->ystep < 0) { - state->ystep = -1; - state->y = state->ysize - 1; - } else { - state->ystep = 1; - } - - state->state = 1; - } - - dst = buf; - bytesPerPixel = (state->bits + 7) / 8; - - while (1) { - int flushCount; - - /* - * state->count is the numbers of bytes in the packet, - * excluding the 1-byte descriptor. - */ - if (state->count == 0) { - UINT8 *row; - UINT8 descriptor; - int startX; - - assert(state->x <= state->xsize); - - /* Make sure we have space for the descriptor. */ - if (bytes < 1) { - break; - } - - if (state->x == state->xsize) { - state->x = 0; - - state->y += state->ystep; - if (state->y < 0 || state->y >= state->ysize) { - state->errcode = IMAGING_CODEC_END; - break; - } - } - - if (state->x == 0) { - state->shuffle( - state->buffer, - (UINT8 *)im->image[state->y + state->yoff] + - state->xoff * im->pixelsize, - state->xsize); - } - - row = state->buffer; - - /* Start with a raw packet for 1 px. */ - descriptor = 0; - startX = state->x; - state->count = bytesPerPixel; - - if (state->x + 1 < state->xsize) { - int maxLookup; - int isRaw; - - isRaw = !comparePixels(row, state->x, bytesPerPixel); - ++state->x; - - /* - * A packet can contain up to 128 pixels; - * 2 are already behind (state->x points to - * the second one). - */ - maxLookup = state->x + 126; - /* A packet must not span multiple rows. */ - if (maxLookup > state->xsize - 1) { - maxLookup = state->xsize - 1; - } - - if (isRaw) { - while (state->x < maxLookup) { - if (!comparePixels(row, state->x, bytesPerPixel)) { - ++state->x; - } else { - /* Two identical pixels will go to RLE packet. */ - --state->x; - break; - } - } - - state->count += (state->x - startX) * bytesPerPixel; - } else { - descriptor |= 0x80; - - while (state->x < maxLookup) { - if (comparePixels(row, state->x, bytesPerPixel)) { - ++state->x; - } else { - break; - } - } - } - } - - /* - * state->x currently points to the last pixel to be - * included in the packet. The pixel count in the - * descriptor is 1 less than actual number of pixels in - * the packet, that is, state->x == startX if we encode - * only 1 pixel. - */ - descriptor += state->x - startX; - *dst++ = descriptor; - --bytes; - - /* Advance to past-the-last encoded pixel. */ - ++state->x; - } - - assert(bytes >= 0); - assert(state->count > 0); - assert(state->x > 0); - assert(state->count <= state->x * bytesPerPixel); - - if (bytes == 0) { - break; - } - - flushCount = state->count; - if (flushCount > bytes) { - flushCount = bytes; - } - - memcpy( - dst, state->buffer + (state->x * bytesPerPixel - state->count), flushCount); - dst += flushCount; - bytes -= flushCount; - - state->count -= flushCount; - } - - return dst - buf; -} + +#include "Imaging.h" + +#include +#include + +static int +comparePixels(const UINT8 *buf, int x, int bytesPerPixel) { + buf += x * bytesPerPixel; + return memcmp(buf, buf + bytesPerPixel, bytesPerPixel) == 0; +} + +int +ImagingTgaRleEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { + UINT8 *dst; + int bytesPerPixel; + + if (state->state == 0) { + if (state->ystep < 0) { + state->ystep = -1; + state->y = state->ysize - 1; + } else { + state->ystep = 1; + } + + state->state = 1; + } + + dst = buf; + bytesPerPixel = (state->bits + 7) / 8; + + while (1) { + int flushCount; + + /* + * state->count is the numbers of bytes in the packet, + * excluding the 1-byte descriptor. + */ + if (state->count == 0) { + UINT8 *row; + UINT8 descriptor; + int startX; + + assert(state->x <= state->xsize); + + /* Make sure we have space for the descriptor. */ + if (bytes < 1) { + break; + } + + if (state->x == state->xsize) { + state->x = 0; + + state->y += state->ystep; + if (state->y < 0 || state->y >= state->ysize) { + state->errcode = IMAGING_CODEC_END; + break; + } + } + + if (state->x == 0) { + state->shuffle( + state->buffer, + (UINT8 *)im->image[state->y + state->yoff] + + state->xoff * im->pixelsize, + state->xsize); + } + + row = state->buffer; + + /* Start with a raw packet for 1 px. */ + descriptor = 0; + startX = state->x; + state->count = bytesPerPixel; + + if (state->x + 1 < state->xsize) { + int maxLookup; + int isRaw; + + isRaw = !comparePixels(row, state->x, bytesPerPixel); + ++state->x; + + /* + * A packet can contain up to 128 pixels; + * 2 are already behind (state->x points to + * the second one). + */ + maxLookup = state->x + 126; + /* A packet must not span multiple rows. */ + if (maxLookup > state->xsize - 1) { + maxLookup = state->xsize - 1; + } + + if (isRaw) { + while (state->x < maxLookup) { + if (!comparePixels(row, state->x, bytesPerPixel)) { + ++state->x; + } else { + /* Two identical pixels will go to RLE packet. */ + --state->x; + break; + } + } + + state->count += (state->x - startX) * bytesPerPixel; + } else { + descriptor |= 0x80; + + while (state->x < maxLookup) { + if (comparePixels(row, state->x, bytesPerPixel)) { + ++state->x; + } else { + break; + } + } + } + } + + /* + * state->x currently points to the last pixel to be + * included in the packet. The pixel count in the + * descriptor is 1 less than actual number of pixels in + * the packet, that is, state->x == startX if we encode + * only 1 pixel. + */ + descriptor += state->x - startX; + *dst++ = descriptor; + --bytes; + + /* Advance to past-the-last encoded pixel. */ + ++state->x; + } + + assert(bytes >= 0); + assert(state->count > 0); + assert(state->x > 0); + assert(state->count <= state->x * bytesPerPixel); + + if (bytes == 0) { + break; + } + + flushCount = state->count; + if (flushCount > bytes) { + flushCount = bytes; + } + + memcpy( + dst, state->buffer + (state->x * bytesPerPixel - state->count), flushCount); + dst += flushCount; + bytes -= flushCount; + + state->count -= flushCount; + } + + return dst - buf; +} diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c index abffdeabc..f156e8598 100644 --- a/src/libImaging/TiffDecode.c +++ b/src/libImaging/TiffDecode.c @@ -1,1034 +1,1034 @@ -/* - * The Python Imaging Library. - * $Id: //modules/pil/libImaging/TiffDecode.c#1 $ - * - * LibTiff-based Group3 and Group4 decoder - * - * - * started modding to use non-private tiff functions to port to libtiff 4.x - * eds 3/12/12 - * - */ - -#include "Imaging.h" - -#ifdef HAVE_LIBTIFF - -#ifdef HAVE_UNISTD_H -#include /* lseek */ -#endif - -#ifndef uint -#define uint uint32 -#endif - -#include "TiffDecode.h" - -/* Convert C file descriptor to WinApi HFILE if LibTiff was compiled with tif_win32.c - * - * This cast is safe, as the top 32-bits of HFILE are guaranteed to be zero, - * see - * https://learn.microsoft.com/en-us/windows/win32/winprog64/interprocess-communication - */ -#ifndef USE_WIN32_FILEIO -#define fd_to_tiff_fd(fd) (fd) -#else -#define fd_to_tiff_fd(fd) ((int)_get_osfhandle(fd)) -#endif - -void -dump_state(const TIFFSTATE *state) { - TRACE( - ("State: Location %u size %d eof %d data: %p ifd: %d\n", - (uint)state->loc, - (int)state->size, - (uint)state->eof, - state->data, - state->ifd)); -} - -/* - procs for TIFFOpenClient -*/ - -tsize_t -_tiffReadProc(thandle_t hdata, tdata_t buf, tsize_t size) { - TIFFSTATE *state = (TIFFSTATE *)hdata; - tsize_t to_read; - - TRACE(("_tiffReadProc: %d \n", (int)size)); - dump_state(state); - - if (state->loc > state->eof) { - TIFFError( - "_tiffReadProc", - "Invalid Read at loc %" PRIu64 ", eof: %" PRIu64, - state->loc, - state->eof); - return 0; - } - to_read = min(size, min(state->size, (tsize_t)state->eof) - (tsize_t)state->loc); - TRACE(("to_read: %d\n", (int)to_read)); - - _TIFFmemcpy(buf, (UINT8 *)state->data + state->loc, to_read); - state->loc += (toff_t)to_read; - - TRACE(("location: %u\n", (uint)state->loc)); - return to_read; -} - -tsize_t -_tiffWriteProc(thandle_t hdata, tdata_t buf, tsize_t size) { - TIFFSTATE *state = (TIFFSTATE *)hdata; - tsize_t to_write; - - TRACE(("_tiffWriteProc: %d \n", (int)size)); - dump_state(state); - - to_write = min(size, state->size - (tsize_t)state->loc); - if (state->flrealloc && size > to_write) { - tdata_t new_data; - tsize_t newsize = state->size; - while (newsize < (size + state->size)) { - if (newsize > INT_MAX - 64 * 1024) { - return 0; - } - newsize += 64 * 1024; - // newsize*=2; // UNDONE, by 64k chunks? - } - TRACE(("Reallocing in write to %d bytes\n", (int)newsize)); - /* malloc check ok, overflow checked above */ - new_data = realloc(state->data, newsize); - if (!new_data) { - // fail out - return 0; - } - state->data = new_data; - state->size = newsize; - to_write = size; - } - - TRACE(("to_write: %d\n", (int)to_write)); - - _TIFFmemcpy((UINT8 *)state->data + state->loc, buf, to_write); - state->loc += (toff_t)to_write; - state->eof = max(state->loc, state->eof); - - dump_state(state); - return to_write; -} - -toff_t -_tiffSeekProc(thandle_t hdata, toff_t off, int whence) { - TIFFSTATE *state = (TIFFSTATE *)hdata; - - TRACE(("_tiffSeekProc: off: %u whence: %d \n", (uint)off, whence)); - dump_state(state); - switch (whence) { - case 0: - state->loc = off; - break; - case 1: - state->loc += off; - break; - case 2: - state->loc = state->eof + off; - break; - } - dump_state(state); - return state->loc; -} - -int -_tiffCloseProc(thandle_t hdata) { - TIFFSTATE *state = (TIFFSTATE *)hdata; - - TRACE(("_tiffCloseProc \n")); - dump_state(state); - - return 0; -} - -toff_t -_tiffSizeProc(thandle_t hdata) { - TIFFSTATE *state = (TIFFSTATE *)hdata; - - TRACE(("_tiffSizeProc \n")); - dump_state(state); - - return (toff_t)state->size; -} - -int -_tiffMapProc(thandle_t hdata, tdata_t *pbase, toff_t *psize) { - TIFFSTATE *state = (TIFFSTATE *)hdata; - - TRACE(("_tiffMapProc input size: %u, data: %p\n", (uint)*psize, *pbase)); - dump_state(state); - - *pbase = state->data; - *psize = state->size; - TRACE(("_tiffMapProc returning size: %u, data: %p\n", (uint)*psize, *pbase)); - return (1); -} - -int -_tiffNullMapProc(thandle_t hdata, tdata_t *pbase, toff_t *psize) { - (void)hdata; - (void)pbase; - (void)psize; - return (0); -} - -void -_tiffUnmapProc(thandle_t hdata, tdata_t base, toff_t size) { - TRACE(("_tiffUnMapProc\n")); - (void)hdata; - (void)base; - (void)size; -} - -int -ImagingLibTiffInit(ImagingCodecState state, int fp, uint32_t offset) { - TIFFSTATE *clientstate = (TIFFSTATE *)state->context; - - TRACE(("initing libtiff\n")); - TRACE(("filepointer: %d \n", fp)); - TRACE( - ("State: count %d, state %d, x %d, y %d, ystep %d\n", - state->count, - state->state, - state->x, - state->y, - state->ystep)); - TRACE( - ("State: xsize %d, ysize %d, xoff %d, yoff %d \n", - state->xsize, - state->ysize, - state->xoff, - state->yoff)); - TRACE(("State: bits %d, bytes %d \n", state->bits, state->bytes)); - TRACE(("State: context %p \n", state->context)); - - clientstate->loc = 0; - clientstate->size = 0; - clientstate->data = 0; - clientstate->fp = fp; - clientstate->ifd = offset; - clientstate->eof = 0; - - return 1; -} - -int -_pickUnpackers( - Imaging im, - ImagingCodecState state, - TIFF *tiff, - uint16_t planarconfig, - ImagingShuffler *unpackers) { - // if number of bands is 1, there is no difference with contig case - if (planarconfig == PLANARCONFIG_SEPARATE && im->bands > 1) { - uint16_t bits_per_sample = 8; - - TIFFGetFieldDefaulted(tiff, TIFFTAG_BITSPERSAMPLE, &bits_per_sample); - if (bits_per_sample != 8 && bits_per_sample != 16) { - TRACE(("Invalid value for bits per sample: %d\n", bits_per_sample)); - state->errcode = IMAGING_CODEC_BROKEN; - return -1; - } - - // We'll pick appropriate set of unpackers depending on planar_configuration - // It does not matter if data is RGB(A), CMYK or LUV really, - // we just copy it plane by plane - unpackers[0] = - ImagingFindUnpacker("RGBA", bits_per_sample == 16 ? "R;16N" : "R", NULL); - unpackers[1] = - ImagingFindUnpacker("RGBA", bits_per_sample == 16 ? "G;16N" : "G", NULL); - unpackers[2] = - ImagingFindUnpacker("RGBA", bits_per_sample == 16 ? "B;16N" : "B", NULL); - unpackers[3] = - ImagingFindUnpacker("RGBA", bits_per_sample == 16 ? "A;16N" : "A", NULL); - - return im->bands; - } else { - unpackers[0] = state->shuffle; - - return 1; - } -} - -int -_decodeAsRGBA(Imaging im, ImagingCodecState state, TIFF *tiff) { - // To avoid dealing with YCbCr subsampling and other complications, let libtiff - // handle it Use a TIFFRGBAImage wrapping the tiff image, and let libtiff handle all - // of the conversion. Metadata read from the TIFFRGBAImage could be different from - // the metadata that the base tiff returns. - - INT32 current_row; - UINT8 *new_data; - UINT32 rows_per_block, row_byte_size, rows_to_read; - int ret; - TIFFRGBAImage img; - char emsg[1024] = ""; - - // Since using TIFFRGBAImage* functions, we can read whole tiff into rastrr in one - // call Let's select smaller block size. Multiplying image width by (tile length OR - // rows per strip) gives us manageable block size in pixels - if (TIFFIsTiled(tiff)) { - ret = TIFFGetFieldDefaulted(tiff, TIFFTAG_TILELENGTH, &rows_per_block); - } else { - ret = TIFFGetFieldDefaulted(tiff, TIFFTAG_ROWSPERSTRIP, &rows_per_block); - } - - if (ret != 1 || rows_per_block == (UINT32)(-1)) { - rows_per_block = state->ysize; - } - - TRACE(("RowsPerBlock: %u \n", rows_per_block)); - - if (!(TIFFRGBAImageOK(tiff, emsg) && TIFFRGBAImageBegin(&img, tiff, 0, emsg))) { - TRACE(("Decode error, msg: %s", emsg)); - state->errcode = IMAGING_CODEC_BROKEN; - // nothing to clean up, just return - return -1; - } - - img.req_orientation = ORIENTATION_TOPLEFT; - img.col_offset = 0; - - /* overflow check for row byte size */ - if (INT_MAX / 4 < img.width) { - state->errcode = IMAGING_CODEC_MEMORY; - goto decodergba_err; - } - - // TiffRGBAImages are 32bits/pixel. - row_byte_size = img.width * 4; - - /* overflow check for realloc */ - if (INT_MAX / row_byte_size < rows_per_block) { - state->errcode = IMAGING_CODEC_MEMORY; - goto decodergba_err; - } - - state->bytes = rows_per_block * row_byte_size; - - TRACE(("BlockSize: %d \n", state->bytes)); - - /* realloc to fit whole strip */ - /* malloc check above */ - new_data = realloc(state->buffer, state->bytes); - if (!new_data) { - state->errcode = IMAGING_CODEC_MEMORY; - goto decodergba_err; - } - - state->buffer = new_data; - - for (; state->y < state->ysize; state->y += rows_per_block) { - img.row_offset = state->y; - rows_to_read = min(rows_per_block, img.height - state->y); - - if (!TIFFRGBAImageGet(&img, (UINT32 *)state->buffer, img.width, rows_to_read)) { - TRACE(("Decode Error, y: %d\n", state->y)); - state->errcode = IMAGING_CODEC_BROKEN; - goto decodergba_err; - } - -#if WORDS_BIGENDIAN - TIFFSwabArrayOfLong((UINT32 *)state->buffer, img.width * rows_to_read); -#endif - - TRACE(("Decoded strip for row %d \n", state->y)); - - // iterate over each row in the strip and stuff data into image - for (current_row = 0; - current_row < min((INT32)rows_per_block, state->ysize - state->y); - current_row++) { - TRACE(("Writing data into line %d ; \n", state->y + current_row)); - - // UINT8 * bbb = state->buffer + current_row * (state->bytes / - // rows_per_block); TRACE(("chars: %x %x %x %x\n", ((UINT8 *)bbb)[0], - // ((UINT8 *)bbb)[1], ((UINT8 *)bbb)[2], ((UINT8 *)bbb)[3])); - - state->shuffle( - (UINT8 *)im->image[state->y + state->yoff + current_row] + - state->xoff * im->pixelsize, - state->buffer + current_row * row_byte_size, - state->xsize); - } - } - -decodergba_err: - TIFFRGBAImageEnd(&img); - if (state->errcode != 0) { - return -1; - } - return 0; -} - -int -_decodeTile( - Imaging im, - ImagingCodecState state, - TIFF *tiff, - int planes, - ImagingShuffler *unpackers) { - INT32 x, y, tile_y, current_tile_length, current_tile_width; - UINT32 tile_width, tile_length; - tsize_t tile_bytes_size, row_byte_size; - UINT8 *new_data; - - tile_bytes_size = TIFFTileSize(tiff); - - if (tile_bytes_size == 0) { - TRACE(("Decode Error, Can not calculate TileSize\n")); - state->errcode = IMAGING_CODEC_BROKEN; - return -1; - } - - row_byte_size = TIFFTileRowSize(tiff); - - if (row_byte_size == 0 || row_byte_size > tile_bytes_size) { - TRACE(("Decode Error, Can not calculate TileRowSize\n")); - state->errcode = IMAGING_CODEC_BROKEN; - return -1; - } - - /* overflow check for realloc */ - if (tile_bytes_size > INT_MAX - 1) { - state->errcode = IMAGING_CODEC_MEMORY; - return -1; - } - - TIFFGetField(tiff, TIFFTAG_TILEWIDTH, &tile_width); - TIFFGetField(tiff, TIFFTAG_TILELENGTH, &tile_length); - - if (tile_width > INT_MAX || tile_length > INT_MAX) { - // state->x and state->y are ints - state->errcode = IMAGING_CODEC_MEMORY; - return -1; - } - - if (tile_bytes_size > ((tile_length * state->bits / planes + 7) / 8) * tile_width) { - // If the tile size as expected by LibTiff isn't what we're expecting, abort. - // man: TIFFTileSize returns the equivalent size for a tile of data as it - // would be returned in a call to TIFFReadTile ... - state->errcode = IMAGING_CODEC_BROKEN; - return -1; - } - - state->bytes = tile_bytes_size; - - TRACE(("TIFFTileSize: %d\n", state->bytes)); - - /* realloc to fit whole tile */ - /* malloc check above */ - new_data = realloc(state->buffer, state->bytes); - if (!new_data) { - state->errcode = IMAGING_CODEC_MEMORY; - return -1; - } - state->buffer = new_data; - - for (y = state->yoff; y < state->ysize; y += tile_length) { - int plane; - for (plane = 0; plane < planes; plane++) { - ImagingShuffler shuffler = unpackers[plane]; - for (x = state->xoff; x < state->xsize; x += tile_width) { - if (TIFFReadTile(tiff, (tdata_t)state->buffer, x, y, 0, plane) == -1) { - TRACE(("Decode Error, Tile at %dx%d\n", x, y)); - state->errcode = IMAGING_CODEC_BROKEN; - return -1; - } - - TRACE(("Read tile at %dx%d; \n\n", x, y)); - - current_tile_width = min((INT32)tile_width, state->xsize - x); - current_tile_length = min((INT32)tile_length, state->ysize - y); - // iterate over each line in the tile and stuff data into image - for (tile_y = 0; tile_y < current_tile_length; tile_y++) { - TRACE( - ("Writing tile data at %dx%d using tile_width: %d; \n", - tile_y + y, - x, - current_tile_width)); - - // UINT8 * bbb = state->buffer + tile_y * row_byte_size; - // TRACE(("chars: %x%x%x%x\n", ((UINT8 *)bbb)[0], ((UINT8 *)bbb)[1], - // ((UINT8 *)bbb)[2], ((UINT8 *)bbb)[3])); - - shuffler( - (UINT8 *)im->image[tile_y + y] + x * im->pixelsize, - state->buffer + tile_y * row_byte_size, - current_tile_width); - } - } - } - } - - return 0; -} - -int -_decodeStrip( - Imaging im, - ImagingCodecState state, - TIFF *tiff, - int planes, - ImagingShuffler *unpackers) { - INT32 strip_row = 0; - UINT8 *new_data; - UINT32 rows_per_strip; - int ret; - tsize_t strip_size, row_byte_size, unpacker_row_byte_size; - - ret = TIFFGetField(tiff, TIFFTAG_ROWSPERSTRIP, &rows_per_strip); - if (ret != 1 || rows_per_strip == (UINT32)(-1)) { - rows_per_strip = state->ysize; - } - - if (rows_per_strip > INT_MAX) { - state->errcode = IMAGING_CODEC_MEMORY; - return -1; - } - - TRACE(("RowsPerStrip: %u\n", rows_per_strip)); - - strip_size = TIFFStripSize(tiff); - if (strip_size > INT_MAX - 1) { - state->errcode = IMAGING_CODEC_MEMORY; - return -1; - } - - unpacker_row_byte_size = (state->xsize * state->bits / planes + 7) / 8; - if (strip_size > (unpacker_row_byte_size * rows_per_strip)) { - // If the strip size as expected by LibTiff isn't what we're expecting, abort. - // man: TIFFStripSize returns the equivalent size for a strip of data as it - // would be returned in a call to TIFFReadEncodedStrip ... - state->errcode = IMAGING_CODEC_BROKEN; - return -1; - } - - state->bytes = strip_size; - - TRACE(("StripSize: %d \n", state->bytes)); - - row_byte_size = TIFFScanlineSize(tiff); - - // if the unpacker calculated row size is > row byte size, (at least) the last - // row of the strip will have a read buffer overflow. - if (row_byte_size == 0 || unpacker_row_byte_size > row_byte_size) { - state->errcode = IMAGING_CODEC_BROKEN; - return -1; - } - - TRACE(("RowsByteSize: %u \n", row_byte_size)); - - /* realloc to fit whole strip */ - /* malloc check above */ - new_data = realloc(state->buffer, state->bytes); - if (!new_data) { - state->errcode = IMAGING_CODEC_MEMORY; - return -1; - } - - state->buffer = new_data; - - for (; state->y < state->ysize; state->y += rows_per_strip) { - int plane; - for (plane = 0; plane < planes; plane++) { - ImagingShuffler shuffler = unpackers[plane]; - if (TIFFReadEncodedStrip( - tiff, - TIFFComputeStrip(tiff, state->y, plane), - (tdata_t)state->buffer, - strip_size) == -1) { - TRACE( - ("Decode Error, strip %d\n", TIFFComputeStrip(tiff, state->y, 0))); - state->errcode = IMAGING_CODEC_BROKEN; - return -1; - } - - TRACE(("Decoded strip for row %d \n", state->y)); - - // iterate over each row in the strip and stuff data into image - for (strip_row = 0; - strip_row < min((INT32)rows_per_strip, state->ysize - state->y); - strip_row++) { - TRACE(("Writing data into line %d ; \n", state->y + strip_row)); - - // UINT8 * bbb = state->buffer + strip_row * (state->bytes / - // rows_per_strip); TRACE(("chars: %x %x %x %x\n", ((UINT8 *)bbb)[0], - // ((UINT8 *)bbb)[1], ((UINT8 *)bbb)[2], ((UINT8 *)bbb)[3])); - - shuffler( - (UINT8 *)im->image[state->y + state->yoff + strip_row] + - state->xoff * im->pixelsize, - state->buffer + strip_row * row_byte_size, - state->xsize); - } - } - } - - return 0; -} - -int -ImagingLibTiffDecode( - Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes) { - TIFFSTATE *clientstate = (TIFFSTATE *)state->context; - char *filename = "tempfile.tif"; - char *mode = "rC"; - TIFF *tiff; - uint16_t photometric = 0; // init to not PHOTOMETRIC_YCBCR - uint16_t compression; - int readAsRGBA = 0; - uint16_t planarconfig = 0; - int planes = 1; - ImagingShuffler unpackers[4]; - INT32 img_width, img_height; - - memset(unpackers, 0, sizeof(ImagingShuffler) * 4); - - /* buffer is the encoded file, bytes is the length of the encoded file */ - /* it all ends up in state->buffer, which is a uint8* from Imaging.h */ - - TRACE(("in decoder: bytes %d\n", bytes)); - TRACE( - ("State: count %d, state %d, x %d, y %d, ystep %d\n", - state->count, - state->state, - state->x, - state->y, - state->ystep)); - TRACE( - ("State: xsize %d, ysize %d, xoff %d, yoff %d \n", - state->xsize, - state->ysize, - state->xoff, - state->yoff)); - TRACE(("State: bits %d, bytes %d \n", state->bits, state->bytes)); - TRACE( - ("Buffer: %p: %c%c%c%c\n", - buffer, - (char)buffer[0], - (char)buffer[1], - (char)buffer[2], - (char)buffer[3])); - TRACE( - ("State->Buffer: %c%c%c%c\n", - (char)state->buffer[0], - (char)state->buffer[1], - (char)state->buffer[2], - (char)state->buffer[3])); - TRACE( - ("Image: mode %s, type %d, bands: %d, xsize %d, ysize %d \n", - im->mode, - im->type, - im->bands, - im->xsize, - im->ysize)); - TRACE( - ("Image: image8 %p, image32 %p, image %p, block %p \n", - im->image8, - im->image32, - im->image, - im->block)); - TRACE(("Image: pixelsize: %d, linesize %d \n", im->pixelsize, im->linesize)); - - dump_state(clientstate); - clientstate->size = bytes; - clientstate->eof = clientstate->size; - clientstate->loc = 0; - clientstate->data = (tdata_t)buffer; - clientstate->flrealloc = 0; - dump_state(clientstate); - - TIFFSetWarningHandler(NULL); - TIFFSetWarningHandlerExt(NULL); - - if (clientstate->fp) { - TRACE(("Opening using fd: %d\n", clientstate->fp)); - lseek(clientstate->fp, 0, SEEK_SET); // Sometimes, I get it set to the end. - tiff = TIFFFdOpen(fd_to_tiff_fd(clientstate->fp), filename, mode); - } else { - TRACE(("Opening from string\n")); - tiff = TIFFClientOpen( - filename, - mode, - (thandle_t)clientstate, - _tiffReadProc, - _tiffWriteProc, - _tiffSeekProc, - _tiffCloseProc, - _tiffSizeProc, - _tiffMapProc, - _tiffUnmapProc); - } - - if (!tiff) { - TRACE(("Error, didn't get the tiff\n")); - state->errcode = IMAGING_CODEC_BROKEN; - return -1; - } - - if (clientstate->ifd) { - int rv; - uint32_t ifdoffset = clientstate->ifd; - TRACE(("reading tiff ifd %u\n", ifdoffset)); - rv = TIFFSetSubDirectory(tiff, ifdoffset); - if (!rv) { - TRACE(("error in TIFFSetSubDirectory")); - goto decode_err; - } - } - - TIFFGetField(tiff, TIFFTAG_IMAGEWIDTH, &img_width); - TIFFGetField(tiff, TIFFTAG_IMAGELENGTH, &img_height); - - if (state->xsize != img_width || state->ysize != img_height) { - TRACE( - ("Inconsistent Image Error: %d =? %d, %d =? %d", - state->xsize, - img_width, - state->ysize, - img_height)); - state->errcode = IMAGING_CODEC_BROKEN; - goto decode_err; - } - - TIFFGetField(tiff, TIFFTAG_PHOTOMETRIC, &photometric); - TIFFGetField(tiff, TIFFTAG_COMPRESSION, &compression); - TIFFGetFieldDefaulted(tiff, TIFFTAG_PLANARCONFIG, &planarconfig); - - // Dealing with YCbCr images is complicated in case if subsampling - // Let LibTiff read them as RGBA - readAsRGBA = photometric == PHOTOMETRIC_YCBCR; - - if (readAsRGBA && compression == COMPRESSION_JPEG && - planarconfig == PLANARCONFIG_CONTIG) { - // If using new JPEG compression, let libjpeg do RGB conversion for performance - // reasons - TIFFSetField(tiff, TIFFTAG_JPEGCOLORMODE, JPEGCOLORMODE_RGB); - readAsRGBA = 0; - } - - if (readAsRGBA) { - _decodeAsRGBA(im, state, tiff); - } else { - planes = _pickUnpackers(im, state, tiff, planarconfig, unpackers); - if (planes <= 0) { - goto decode_err; - } - - if (TIFFIsTiled(tiff)) { - _decodeTile(im, state, tiff, planes, unpackers); - } else { - _decodeStrip(im, state, tiff, planes, unpackers); - } - - if (!state->errcode) { - // Check if raw mode was RGBa and it was stored on separate planes - // so we have to convert it to RGBA - if (planes > 3 && strcmp(im->mode, "RGBA") == 0) { - uint16_t extrasamples; - uint16_t *sampleinfo; - ImagingShuffler shuffle; - INT32 y; - - TIFFGetFieldDefaulted( - tiff, TIFFTAG_EXTRASAMPLES, &extrasamples, &sampleinfo); - - if (extrasamples >= 1 && (sampleinfo[0] == EXTRASAMPLE_UNSPECIFIED || - sampleinfo[0] == EXTRASAMPLE_ASSOCALPHA)) { - shuffle = ImagingFindUnpacker("RGBA", "RGBa", NULL); - - for (y = state->yoff; y < state->ysize; y++) { - UINT8 *ptr = (UINT8 *)im->image[y + state->yoff] + - state->xoff * im->pixelsize; - shuffle(ptr, ptr, state->xsize); - } - } - } - } - } - -decode_err: - // TIFFClose in libtiff calls tif_closeproc and TIFFCleanup - if (clientstate->fp) { - // Pillow 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); - } - TRACE(("Done Decoding, Returning \n")); - // Returning -1 here to force ImageFile.load to break, rather than - // even think about looping back around. - return -1; -} - -int -ImagingLibTiffEncodeInit(ImagingCodecState state, char *filename, int fp) { - // Open the FD or the pointer as a tiff file, for writing. - // We may have to do some monkeying around to make this really work. - // If we have a fp, then we're good. - // If we have a memory string, we're probably going to have to malloc, then - // shuffle bytes into the writescanline process. - // Going to have to deal with the directory as well. - - TIFFSTATE *clientstate = (TIFFSTATE *)state->context; - int bufsize = 64 * 1024; - char *mode = "w"; - - TRACE(("initing libtiff\n")); - TRACE(("Filename %s, filepointer: %d \n", filename, fp)); - TRACE( - ("State: count %d, state %d, x %d, y %d, ystep %d\n", - state->count, - state->state, - state->x, - state->y, - state->ystep)); - TRACE( - ("State: xsize %d, ysize %d, xoff %d, yoff %d \n", - state->xsize, - state->ysize, - state->xoff, - state->yoff)); - TRACE(("State: bits %d, bytes %d \n", state->bits, state->bytes)); - TRACE(("State: context %p \n", state->context)); - - clientstate->loc = 0; - clientstate->size = 0; - clientstate->eof = 0; - clientstate->data = 0; - clientstate->flrealloc = 0; - clientstate->fp = fp; - - state->state = 0; - - if (fp) { - TRACE(("Opening using fd: %d for writing \n", clientstate->fp)); - clientstate->tiff = TIFFFdOpen(fd_to_tiff_fd(clientstate->fp), filename, mode); - } else { - // calloc a buffer to write the tif, we're going to need to realloc or something - // if we need bigger. - TRACE(("Opening a buffer for writing \n")); - /* calloc check ok, small constant allocation */ - clientstate->data = calloc(bufsize, 1); - clientstate->size = bufsize; - clientstate->flrealloc = 1; - - if (!clientstate->data) { - TRACE(("Error, couldn't allocate a buffer of size %d\n", bufsize)); - return 0; - } - - clientstate->tiff = TIFFClientOpen( - filename, - mode, - (thandle_t)clientstate, - _tiffReadProc, - _tiffWriteProc, - _tiffSeekProc, - _tiffCloseProc, - _tiffSizeProc, - _tiffNullMapProc, - _tiffUnmapProc); /*force no mmap*/ - } - - if (!clientstate->tiff) { - TRACE(("Error, couldn't open tiff file\n")); - return 0; - } - - return 1; -} - -int -ImagingLibTiffMergeFieldInfo( - ImagingCodecState state, TIFFDataType field_type, int key, int is_var_length) { - // Refer to libtiff docs (http://www.simplesystems.org/libtiff/addingtags.html) - TIFFSTATE *clientstate = (TIFFSTATE *)state->context; - uint32_t n; - int status = 0; - - // custom fields added with ImagingLibTiffMergeFieldInfo are only used for - // decoding, ignore readcount; - int readcount = is_var_length ? TIFF_VARIABLE : 1; - // we support writing a single value, or a variable number of values - int writecount = is_var_length ? TIFF_VARIABLE : 1; - // whether the first value should encode the number of values. - int passcount = (is_var_length && field_type != TIFF_ASCII) ? 1 : 0; - - TIFFFieldInfo info[] = { - {key, - readcount, - writecount, - field_type, - FIELD_CUSTOM, - 1, - passcount, - "CustomField"}}; - - n = sizeof(info) / sizeof(info[0]); - - // Test for libtiff 4.0 or later, excluding libtiff 3.9.6 and 3.9.7 -#if TIFFLIB_VERSION >= 20111221 && TIFFLIB_VERSION != 20120218 && \ - TIFFLIB_VERSION != 20120922 - status = TIFFMergeFieldInfo(clientstate->tiff, info, n); -#else - TIFFMergeFieldInfo(clientstate->tiff, info, n); -#endif - return status; -} - -int -ImagingLibTiffSetField(ImagingCodecState state, ttag_t tag, ...) { - // after tif_dir.c->TIFFSetField. - TIFFSTATE *clientstate = (TIFFSTATE *)state->context; - va_list ap; - int status; - - va_start(ap, tag); - status = TIFFVSetField(clientstate->tiff, tag, ap); - va_end(ap); - return status; -} - -int -ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes) { - /* One shot encoder. Encode everything to the tiff in the clientstate. - If we're running off of a FD, then run once, we're good, everything - ends up in the file, we close and we're done. - - If we're going to memory, then we need to write the whole file into memory, then - parcel it back out to the pystring buffer bytes at a time. - - */ - - TIFFSTATE *clientstate = (TIFFSTATE *)state->context; - TIFF *tiff = clientstate->tiff; - - TRACE(("in encoder: bytes %d\n", bytes)); - TRACE( - ("State: count %d, state %d, x %d, y %d, ystep %d\n", - state->count, - state->state, - state->x, - state->y, - state->ystep)); - TRACE( - ("State: xsize %d, ysize %d, xoff %d, yoff %d \n", - state->xsize, - state->ysize, - state->xoff, - state->yoff)); - TRACE(("State: bits %d, bytes %d \n", state->bits, state->bytes)); - TRACE( - ("Buffer: %p: %c%c%c%c\n", - buffer, - (char)buffer[0], - (char)buffer[1], - (char)buffer[2], - (char)buffer[3])); - TRACE( - ("State->Buffer: %c%c%c%c\n", - (char)state->buffer[0], - (char)state->buffer[1], - (char)state->buffer[2], - (char)state->buffer[3])); - TRACE( - ("Image: mode %s, type %d, bands: %d, xsize %d, ysize %d \n", - im->mode, - im->type, - im->bands, - im->xsize, - im->ysize)); - TRACE( - ("Image: image8 %p, image32 %p, image %p, block %p \n", - im->image8, - im->image32, - im->image, - im->block)); - TRACE(("Image: pixelsize: %d, linesize %d \n", im->pixelsize, im->linesize)); - - dump_state(clientstate); - - if (state->state == 0) { - TRACE(("Encoding line by line")); - while (state->y < state->ysize) { - state->shuffle( - state->buffer, - (UINT8 *)im->image[state->y + state->yoff] + - state->xoff * im->pixelsize, - state->xsize); - - if (TIFFWriteScanline( - tiff, (tdata_t)(state->buffer), (uint32_t)state->y, 0) == -1) { - TRACE(("Encode Error, row %d\n", state->y)); - state->errcode = IMAGING_CODEC_BROKEN; - TIFFClose(tiff); - if (!clientstate->fp) { - free(clientstate->data); - } - return -1; - } - state->y++; - } - - if (state->y == state->ysize) { - state->state = 1; - - TRACE(("Flushing \n")); - if (!TIFFFlush(tiff)) { - TRACE(("Error flushing the tiff")); - // likely reason is memory. - state->errcode = IMAGING_CODEC_MEMORY; - TIFFClose(tiff); - if (!clientstate->fp) { - free(clientstate->data); - } - return -1; - } - TRACE(("Closing \n")); - TIFFClose(tiff); - // reset the clientstate metadata to use it to read out the buffer. - clientstate->loc = 0; - clientstate->size = clientstate->eof; // redundant? - } - } - - if (state->state == 1 && !clientstate->fp) { - int read = - (int)_tiffReadProc((thandle_t)clientstate, (tdata_t)buffer, (tsize_t)bytes); - TRACE( - ("Buffer: %p: %c%c%c%c\n", - buffer, - (char)buffer[0], - (char)buffer[1], - (char)buffer[2], - (char)buffer[3])); - if (clientstate->loc == clientstate->eof) { - TRACE(("Hit EOF, calling an end, freeing data")); - state->errcode = IMAGING_CODEC_END; - free(clientstate->data); - } - return read; - } - - state->errcode = IMAGING_CODEC_END; - return 0; -} - -const char * -ImagingTiffVersion(void) { - return TIFFGetVersion(); -} - -#endif +/* + * The Python Imaging Library. + * $Id: //modules/pil/libImaging/TiffDecode.c#1 $ + * + * LibTiff-based Group3 and Group4 decoder + * + * + * started modding to use non-private tiff functions to port to libtiff 4.x + * eds 3/12/12 + * + */ + +#include "Imaging.h" + +#ifdef HAVE_LIBTIFF + +#ifdef HAVE_UNISTD_H +#include /* lseek */ +#endif + +#ifndef uint +#define uint uint32 +#endif + +#include "TiffDecode.h" + +/* Convert C file descriptor to WinApi HFILE if LibTiff was compiled with tif_win32.c + * + * This cast is safe, as the top 32-bits of HFILE are guaranteed to be zero, + * see + * https://learn.microsoft.com/en-us/windows/win32/winprog64/interprocess-communication + */ +#ifndef USE_WIN32_FILEIO +#define fd_to_tiff_fd(fd) (fd) +#else +#define fd_to_tiff_fd(fd) ((int)_get_osfhandle(fd)) +#endif + +void +dump_state(const TIFFSTATE *state) { + TRACE( + ("State: Location %u size %d eof %d data: %p ifd: %d\n", + (uint)state->loc, + (int)state->size, + (uint)state->eof, + state->data, + state->ifd)); +} + +/* + procs for TIFFOpenClient +*/ + +tsize_t +_tiffReadProc(thandle_t hdata, tdata_t buf, tsize_t size) { + TIFFSTATE *state = (TIFFSTATE *)hdata; + tsize_t to_read; + + TRACE(("_tiffReadProc: %d \n", (int)size)); + dump_state(state); + + if (state->loc > state->eof) { + TIFFError( + "_tiffReadProc", + "Invalid Read at loc %" PRIu64 ", eof: %" PRIu64, + state->loc, + state->eof); + return 0; + } + to_read = min(size, min(state->size, (tsize_t)state->eof) - (tsize_t)state->loc); + TRACE(("to_read: %d\n", (int)to_read)); + + _TIFFmemcpy(buf, (UINT8 *)state->data + state->loc, to_read); + state->loc += (toff_t)to_read; + + TRACE(("location: %u\n", (uint)state->loc)); + return to_read; +} + +tsize_t +_tiffWriteProc(thandle_t hdata, tdata_t buf, tsize_t size) { + TIFFSTATE *state = (TIFFSTATE *)hdata; + tsize_t to_write; + + TRACE(("_tiffWriteProc: %d \n", (int)size)); + dump_state(state); + + to_write = min(size, state->size - (tsize_t)state->loc); + if (state->flrealloc && size > to_write) { + tdata_t new_data; + tsize_t newsize = state->size; + while (newsize < (size + state->size)) { + if (newsize > INT_MAX - 64 * 1024) { + return 0; + } + newsize += 64 * 1024; + // newsize*=2; // UNDONE, by 64k chunks? + } + TRACE(("Reallocing in write to %d bytes\n", (int)newsize)); + /* malloc check ok, overflow checked above */ + new_data = realloc(state->data, newsize); + if (!new_data) { + // fail out + return 0; + } + state->data = new_data; + state->size = newsize; + to_write = size; + } + + TRACE(("to_write: %d\n", (int)to_write)); + + _TIFFmemcpy((UINT8 *)state->data + state->loc, buf, to_write); + state->loc += (toff_t)to_write; + state->eof = max(state->loc, state->eof); + + dump_state(state); + return to_write; +} + +toff_t +_tiffSeekProc(thandle_t hdata, toff_t off, int whence) { + TIFFSTATE *state = (TIFFSTATE *)hdata; + + TRACE(("_tiffSeekProc: off: %u whence: %d \n", (uint)off, whence)); + dump_state(state); + switch (whence) { + case 0: + state->loc = off; + break; + case 1: + state->loc += off; + break; + case 2: + state->loc = state->eof + off; + break; + } + dump_state(state); + return state->loc; +} + +int +_tiffCloseProc(thandle_t hdata) { + TIFFSTATE *state = (TIFFSTATE *)hdata; + + TRACE(("_tiffCloseProc \n")); + dump_state(state); + + return 0; +} + +toff_t +_tiffSizeProc(thandle_t hdata) { + TIFFSTATE *state = (TIFFSTATE *)hdata; + + TRACE(("_tiffSizeProc \n")); + dump_state(state); + + return (toff_t)state->size; +} + +int +_tiffMapProc(thandle_t hdata, tdata_t *pbase, toff_t *psize) { + TIFFSTATE *state = (TIFFSTATE *)hdata; + + TRACE(("_tiffMapProc input size: %u, data: %p\n", (uint)*psize, *pbase)); + dump_state(state); + + *pbase = state->data; + *psize = state->size; + TRACE(("_tiffMapProc returning size: %u, data: %p\n", (uint)*psize, *pbase)); + return (1); +} + +int +_tiffNullMapProc(thandle_t hdata, tdata_t *pbase, toff_t *psize) { + (void)hdata; + (void)pbase; + (void)psize; + return (0); +} + +void +_tiffUnmapProc(thandle_t hdata, tdata_t base, toff_t size) { + TRACE(("_tiffUnMapProc\n")); + (void)hdata; + (void)base; + (void)size; +} + +int +ImagingLibTiffInit(ImagingCodecState state, int fp, uint32_t offset) { + TIFFSTATE *clientstate = (TIFFSTATE *)state->context; + + TRACE(("initing libtiff\n")); + TRACE(("filepointer: %d \n", fp)); + TRACE( + ("State: count %d, state %d, x %d, y %d, ystep %d\n", + state->count, + state->state, + state->x, + state->y, + state->ystep)); + TRACE( + ("State: xsize %d, ysize %d, xoff %d, yoff %d \n", + state->xsize, + state->ysize, + state->xoff, + state->yoff)); + TRACE(("State: bits %d, bytes %d \n", state->bits, state->bytes)); + TRACE(("State: context %p \n", state->context)); + + clientstate->loc = 0; + clientstate->size = 0; + clientstate->data = 0; + clientstate->fp = fp; + clientstate->ifd = offset; + clientstate->eof = 0; + + return 1; +} + +int +_pickUnpackers( + Imaging im, + ImagingCodecState state, + TIFF *tiff, + uint16_t planarconfig, + ImagingShuffler *unpackers) { + // if number of bands is 1, there is no difference with contig case + if (planarconfig == PLANARCONFIG_SEPARATE && im->bands > 1) { + uint16_t bits_per_sample = 8; + + TIFFGetFieldDefaulted(tiff, TIFFTAG_BITSPERSAMPLE, &bits_per_sample); + if (bits_per_sample != 8 && bits_per_sample != 16) { + TRACE(("Invalid value for bits per sample: %d\n", bits_per_sample)); + state->errcode = IMAGING_CODEC_BROKEN; + return -1; + } + + // We'll pick appropriate set of unpackers depending on planar_configuration + // It does not matter if data is RGB(A), CMYK or LUV really, + // we just copy it plane by plane + unpackers[0] = + ImagingFindUnpacker("RGBA", bits_per_sample == 16 ? "R;16N" : "R", NULL); + unpackers[1] = + ImagingFindUnpacker("RGBA", bits_per_sample == 16 ? "G;16N" : "G", NULL); + unpackers[2] = + ImagingFindUnpacker("RGBA", bits_per_sample == 16 ? "B;16N" : "B", NULL); + unpackers[3] = + ImagingFindUnpacker("RGBA", bits_per_sample == 16 ? "A;16N" : "A", NULL); + + return im->bands; + } else { + unpackers[0] = state->shuffle; + + return 1; + } +} + +int +_decodeAsRGBA(Imaging im, ImagingCodecState state, TIFF *tiff) { + // To avoid dealing with YCbCr subsampling and other complications, let libtiff + // handle it Use a TIFFRGBAImage wrapping the tiff image, and let libtiff handle all + // of the conversion. Metadata read from the TIFFRGBAImage could be different from + // the metadata that the base tiff returns. + + INT32 current_row; + UINT8 *new_data; + UINT32 rows_per_block, row_byte_size, rows_to_read; + int ret; + TIFFRGBAImage img; + char emsg[1024] = ""; + + // Since using TIFFRGBAImage* functions, we can read whole tiff into rastrr in one + // call Let's select smaller block size. Multiplying image width by (tile length OR + // rows per strip) gives us manageable block size in pixels + if (TIFFIsTiled(tiff)) { + ret = TIFFGetFieldDefaulted(tiff, TIFFTAG_TILELENGTH, &rows_per_block); + } else { + ret = TIFFGetFieldDefaulted(tiff, TIFFTAG_ROWSPERSTRIP, &rows_per_block); + } + + if (ret != 1 || rows_per_block == (UINT32)(-1)) { + rows_per_block = state->ysize; + } + + TRACE(("RowsPerBlock: %u \n", rows_per_block)); + + if (!(TIFFRGBAImageOK(tiff, emsg) && TIFFRGBAImageBegin(&img, tiff, 0, emsg))) { + TRACE(("Decode error, msg: %s", emsg)); + state->errcode = IMAGING_CODEC_BROKEN; + // nothing to clean up, just return + return -1; + } + + img.req_orientation = ORIENTATION_TOPLEFT; + img.col_offset = 0; + + /* overflow check for row byte size */ + if (INT_MAX / 4 < img.width) { + state->errcode = IMAGING_CODEC_MEMORY; + goto decodergba_err; + } + + // TiffRGBAImages are 32bits/pixel. + row_byte_size = img.width * 4; + + /* overflow check for realloc */ + if (INT_MAX / row_byte_size < rows_per_block) { + state->errcode = IMAGING_CODEC_MEMORY; + goto decodergba_err; + } + + state->bytes = rows_per_block * row_byte_size; + + TRACE(("BlockSize: %d \n", state->bytes)); + + /* realloc to fit whole strip */ + /* malloc check above */ + new_data = realloc(state->buffer, state->bytes); + if (!new_data) { + state->errcode = IMAGING_CODEC_MEMORY; + goto decodergba_err; + } + + state->buffer = new_data; + + for (; state->y < state->ysize; state->y += rows_per_block) { + img.row_offset = state->y; + rows_to_read = min(rows_per_block, img.height - state->y); + + if (!TIFFRGBAImageGet(&img, (UINT32 *)state->buffer, img.width, rows_to_read)) { + TRACE(("Decode Error, y: %d\n", state->y)); + state->errcode = IMAGING_CODEC_BROKEN; + goto decodergba_err; + } + +#if WORDS_BIGENDIAN + TIFFSwabArrayOfLong((UINT32 *)state->buffer, img.width * rows_to_read); +#endif + + TRACE(("Decoded strip for row %d \n", state->y)); + + // iterate over each row in the strip and stuff data into image + for (current_row = 0; + current_row < min((INT32)rows_per_block, state->ysize - state->y); + current_row++) { + TRACE(("Writing data into line %d ; \n", state->y + current_row)); + + // UINT8 * bbb = state->buffer + current_row * (state->bytes / + // rows_per_block); TRACE(("chars: %x %x %x %x\n", ((UINT8 *)bbb)[0], + // ((UINT8 *)bbb)[1], ((UINT8 *)bbb)[2], ((UINT8 *)bbb)[3])); + + state->shuffle( + (UINT8 *)im->image[state->y + state->yoff + current_row] + + state->xoff * im->pixelsize, + state->buffer + current_row * row_byte_size, + state->xsize); + } + } + +decodergba_err: + TIFFRGBAImageEnd(&img); + if (state->errcode != 0) { + return -1; + } + return 0; +} + +int +_decodeTile( + Imaging im, + ImagingCodecState state, + TIFF *tiff, + int planes, + ImagingShuffler *unpackers) { + INT32 x, y, tile_y, current_tile_length, current_tile_width; + UINT32 tile_width, tile_length; + tsize_t tile_bytes_size, row_byte_size; + UINT8 *new_data; + + tile_bytes_size = TIFFTileSize(tiff); + + if (tile_bytes_size == 0) { + TRACE(("Decode Error, Can not calculate TileSize\n")); + state->errcode = IMAGING_CODEC_BROKEN; + return -1; + } + + row_byte_size = TIFFTileRowSize(tiff); + + if (row_byte_size == 0 || row_byte_size > tile_bytes_size) { + TRACE(("Decode Error, Can not calculate TileRowSize\n")); + state->errcode = IMAGING_CODEC_BROKEN; + return -1; + } + + /* overflow check for realloc */ + if (tile_bytes_size > INT_MAX - 1) { + state->errcode = IMAGING_CODEC_MEMORY; + return -1; + } + + TIFFGetField(tiff, TIFFTAG_TILEWIDTH, &tile_width); + TIFFGetField(tiff, TIFFTAG_TILELENGTH, &tile_length); + + if (tile_width > INT_MAX || tile_length > INT_MAX) { + // state->x and state->y are ints + state->errcode = IMAGING_CODEC_MEMORY; + return -1; + } + + if (tile_bytes_size > ((tile_length * state->bits / planes + 7) / 8) * tile_width) { + // If the tile size as expected by LibTiff isn't what we're expecting, abort. + // man: TIFFTileSize returns the equivalent size for a tile of data as it + // would be returned in a call to TIFFReadTile ... + state->errcode = IMAGING_CODEC_BROKEN; + return -1; + } + + state->bytes = tile_bytes_size; + + TRACE(("TIFFTileSize: %d\n", state->bytes)); + + /* realloc to fit whole tile */ + /* malloc check above */ + new_data = realloc(state->buffer, state->bytes); + if (!new_data) { + state->errcode = IMAGING_CODEC_MEMORY; + return -1; + } + state->buffer = new_data; + + for (y = state->yoff; y < state->ysize; y += tile_length) { + int plane; + for (plane = 0; plane < planes; plane++) { + ImagingShuffler shuffler = unpackers[plane]; + for (x = state->xoff; x < state->xsize; x += tile_width) { + if (TIFFReadTile(tiff, (tdata_t)state->buffer, x, y, 0, plane) == -1) { + TRACE(("Decode Error, Tile at %dx%d\n", x, y)); + state->errcode = IMAGING_CODEC_BROKEN; + return -1; + } + + TRACE(("Read tile at %dx%d; \n\n", x, y)); + + current_tile_width = min((INT32)tile_width, state->xsize - x); + current_tile_length = min((INT32)tile_length, state->ysize - y); + // iterate over each line in the tile and stuff data into image + for (tile_y = 0; tile_y < current_tile_length; tile_y++) { + TRACE( + ("Writing tile data at %dx%d using tile_width: %d; \n", + tile_y + y, + x, + current_tile_width)); + + // UINT8 * bbb = state->buffer + tile_y * row_byte_size; + // TRACE(("chars: %x%x%x%x\n", ((UINT8 *)bbb)[0], ((UINT8 *)bbb)[1], + // ((UINT8 *)bbb)[2], ((UINT8 *)bbb)[3])); + + shuffler( + (UINT8 *)im->image[tile_y + y] + x * im->pixelsize, + state->buffer + tile_y * row_byte_size, + current_tile_width); + } + } + } + } + + return 0; +} + +int +_decodeStrip( + Imaging im, + ImagingCodecState state, + TIFF *tiff, + int planes, + ImagingShuffler *unpackers) { + INT32 strip_row = 0; + UINT8 *new_data; + UINT32 rows_per_strip; + int ret; + tsize_t strip_size, row_byte_size, unpacker_row_byte_size; + + ret = TIFFGetField(tiff, TIFFTAG_ROWSPERSTRIP, &rows_per_strip); + if (ret != 1 || rows_per_strip == (UINT32)(-1)) { + rows_per_strip = state->ysize; + } + + if (rows_per_strip > INT_MAX) { + state->errcode = IMAGING_CODEC_MEMORY; + return -1; + } + + TRACE(("RowsPerStrip: %u\n", rows_per_strip)); + + strip_size = TIFFStripSize(tiff); + if (strip_size > INT_MAX - 1) { + state->errcode = IMAGING_CODEC_MEMORY; + return -1; + } + + unpacker_row_byte_size = (state->xsize * state->bits / planes + 7) / 8; + if (strip_size > (unpacker_row_byte_size * rows_per_strip)) { + // If the strip size as expected by LibTiff isn't what we're expecting, abort. + // man: TIFFStripSize returns the equivalent size for a strip of data as it + // would be returned in a call to TIFFReadEncodedStrip ... + state->errcode = IMAGING_CODEC_BROKEN; + return -1; + } + + state->bytes = strip_size; + + TRACE(("StripSize: %d \n", state->bytes)); + + row_byte_size = TIFFScanlineSize(tiff); + + // if the unpacker calculated row size is > row byte size, (at least) the last + // row of the strip will have a read buffer overflow. + if (row_byte_size == 0 || unpacker_row_byte_size > row_byte_size) { + state->errcode = IMAGING_CODEC_BROKEN; + return -1; + } + + TRACE(("RowsByteSize: %u \n", row_byte_size)); + + /* realloc to fit whole strip */ + /* malloc check above */ + new_data = realloc(state->buffer, state->bytes); + if (!new_data) { + state->errcode = IMAGING_CODEC_MEMORY; + return -1; + } + + state->buffer = new_data; + + for (; state->y < state->ysize; state->y += rows_per_strip) { + int plane; + for (plane = 0; plane < planes; plane++) { + ImagingShuffler shuffler = unpackers[plane]; + if (TIFFReadEncodedStrip( + tiff, + TIFFComputeStrip(tiff, state->y, plane), + (tdata_t)state->buffer, + strip_size) == -1) { + TRACE( + ("Decode Error, strip %d\n", TIFFComputeStrip(tiff, state->y, 0))); + state->errcode = IMAGING_CODEC_BROKEN; + return -1; + } + + TRACE(("Decoded strip for row %d \n", state->y)); + + // iterate over each row in the strip and stuff data into image + for (strip_row = 0; + strip_row < min((INT32)rows_per_strip, state->ysize - state->y); + strip_row++) { + TRACE(("Writing data into line %d ; \n", state->y + strip_row)); + + // UINT8 * bbb = state->buffer + strip_row * (state->bytes / + // rows_per_strip); TRACE(("chars: %x %x %x %x\n", ((UINT8 *)bbb)[0], + // ((UINT8 *)bbb)[1], ((UINT8 *)bbb)[2], ((UINT8 *)bbb)[3])); + + shuffler( + (UINT8 *)im->image[state->y + state->yoff + strip_row] + + state->xoff * im->pixelsize, + state->buffer + strip_row * row_byte_size, + state->xsize); + } + } + } + + return 0; +} + +int +ImagingLibTiffDecode( + Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes) { + TIFFSTATE *clientstate = (TIFFSTATE *)state->context; + char *filename = "tempfile.tif"; + char *mode = "rC"; + TIFF *tiff; + uint16_t photometric = 0; // init to not PHOTOMETRIC_YCBCR + uint16_t compression; + int readAsRGBA = 0; + uint16_t planarconfig = 0; + int planes = 1; + ImagingShuffler unpackers[4]; + INT32 img_width, img_height; + + memset(unpackers, 0, sizeof(ImagingShuffler) * 4); + + /* buffer is the encoded file, bytes is the length of the encoded file */ + /* it all ends up in state->buffer, which is a uint8* from Imaging.h */ + + TRACE(("in decoder: bytes %d\n", bytes)); + TRACE( + ("State: count %d, state %d, x %d, y %d, ystep %d\n", + state->count, + state->state, + state->x, + state->y, + state->ystep)); + TRACE( + ("State: xsize %d, ysize %d, xoff %d, yoff %d \n", + state->xsize, + state->ysize, + state->xoff, + state->yoff)); + TRACE(("State: bits %d, bytes %d \n", state->bits, state->bytes)); + TRACE( + ("Buffer: %p: %c%c%c%c\n", + buffer, + (char)buffer[0], + (char)buffer[1], + (char)buffer[2], + (char)buffer[3])); + TRACE( + ("State->Buffer: %c%c%c%c\n", + (char)state->buffer[0], + (char)state->buffer[1], + (char)state->buffer[2], + (char)state->buffer[3])); + TRACE( + ("Image: mode %s, type %d, bands: %d, xsize %d, ysize %d \n", + im->mode, + im->type, + im->bands, + im->xsize, + im->ysize)); + TRACE( + ("Image: image8 %p, image32 %p, image %p, block %p \n", + im->image8, + im->image32, + im->image, + im->block)); + TRACE(("Image: pixelsize: %d, linesize %d \n", im->pixelsize, im->linesize)); + + dump_state(clientstate); + clientstate->size = bytes; + clientstate->eof = clientstate->size; + clientstate->loc = 0; + clientstate->data = (tdata_t)buffer; + clientstate->flrealloc = 0; + dump_state(clientstate); + + TIFFSetWarningHandler(NULL); + TIFFSetWarningHandlerExt(NULL); + + if (clientstate->fp) { + TRACE(("Opening using fd: %d\n", clientstate->fp)); + lseek(clientstate->fp, 0, SEEK_SET); // Sometimes, I get it set to the end. + tiff = TIFFFdOpen(fd_to_tiff_fd(clientstate->fp), filename, mode); + } else { + TRACE(("Opening from string\n")); + tiff = TIFFClientOpen( + filename, + mode, + (thandle_t)clientstate, + _tiffReadProc, + _tiffWriteProc, + _tiffSeekProc, + _tiffCloseProc, + _tiffSizeProc, + _tiffMapProc, + _tiffUnmapProc); + } + + if (!tiff) { + TRACE(("Error, didn't get the tiff\n")); + state->errcode = IMAGING_CODEC_BROKEN; + return -1; + } + + if (clientstate->ifd) { + int rv; + uint32_t ifdoffset = clientstate->ifd; + TRACE(("reading tiff ifd %u\n", ifdoffset)); + rv = TIFFSetSubDirectory(tiff, ifdoffset); + if (!rv) { + TRACE(("error in TIFFSetSubDirectory")); + goto decode_err; + } + } + + TIFFGetField(tiff, TIFFTAG_IMAGEWIDTH, &img_width); + TIFFGetField(tiff, TIFFTAG_IMAGELENGTH, &img_height); + + if (state->xsize != img_width || state->ysize != img_height) { + TRACE( + ("Inconsistent Image Error: %d =? %d, %d =? %d", + state->xsize, + img_width, + state->ysize, + img_height)); + state->errcode = IMAGING_CODEC_BROKEN; + goto decode_err; + } + + TIFFGetField(tiff, TIFFTAG_PHOTOMETRIC, &photometric); + TIFFGetField(tiff, TIFFTAG_COMPRESSION, &compression); + TIFFGetFieldDefaulted(tiff, TIFFTAG_PLANARCONFIG, &planarconfig); + + // Dealing with YCbCr images is complicated in case if subsampling + // Let LibTiff read them as RGBA + readAsRGBA = photometric == PHOTOMETRIC_YCBCR; + + if (readAsRGBA && compression == COMPRESSION_JPEG && + planarconfig == PLANARCONFIG_CONTIG) { + // If using new JPEG compression, let libjpeg do RGB conversion for performance + // reasons + TIFFSetField(tiff, TIFFTAG_JPEGCOLORMODE, JPEGCOLORMODE_RGB); + readAsRGBA = 0; + } + + if (readAsRGBA) { + _decodeAsRGBA(im, state, tiff); + } else { + planes = _pickUnpackers(im, state, tiff, planarconfig, unpackers); + if (planes <= 0) { + goto decode_err; + } + + if (TIFFIsTiled(tiff)) { + _decodeTile(im, state, tiff, planes, unpackers); + } else { + _decodeStrip(im, state, tiff, planes, unpackers); + } + + if (!state->errcode) { + // Check if raw mode was RGBa and it was stored on separate planes + // so we have to convert it to RGBA + if (planes > 3 && strcmp(im->mode, "RGBA") == 0) { + uint16_t extrasamples; + uint16_t *sampleinfo; + ImagingShuffler shuffle; + INT32 y; + + TIFFGetFieldDefaulted( + tiff, TIFFTAG_EXTRASAMPLES, &extrasamples, &sampleinfo); + + if (extrasamples >= 1 && (sampleinfo[0] == EXTRASAMPLE_UNSPECIFIED || + sampleinfo[0] == EXTRASAMPLE_ASSOCALPHA)) { + shuffle = ImagingFindUnpacker("RGBA", "RGBa", NULL); + + for (y = state->yoff; y < state->ysize; y++) { + UINT8 *ptr = (UINT8 *)im->image[y + state->yoff] + + state->xoff * im->pixelsize; + shuffle(ptr, ptr, state->xsize); + } + } + } + } + } + +decode_err: + // TIFFClose in libtiff calls tif_closeproc and TIFFCleanup + if (clientstate->fp) { + // Pillow 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); + } + TRACE(("Done Decoding, Returning \n")); + // Returning -1 here to force ImageFile.load to break, rather than + // even think about looping back around. + return -1; +} + +int +ImagingLibTiffEncodeInit(ImagingCodecState state, char *filename, int fp) { + // Open the FD or the pointer as a tiff file, for writing. + // We may have to do some monkeying around to make this really work. + // If we have a fp, then we're good. + // If we have a memory string, we're probably going to have to malloc, then + // shuffle bytes into the writescanline process. + // Going to have to deal with the directory as well. + + TIFFSTATE *clientstate = (TIFFSTATE *)state->context; + int bufsize = 64 * 1024; + char *mode = "w"; + + TRACE(("initing libtiff\n")); + TRACE(("Filename %s, filepointer: %d \n", filename, fp)); + TRACE( + ("State: count %d, state %d, x %d, y %d, ystep %d\n", + state->count, + state->state, + state->x, + state->y, + state->ystep)); + TRACE( + ("State: xsize %d, ysize %d, xoff %d, yoff %d \n", + state->xsize, + state->ysize, + state->xoff, + state->yoff)); + TRACE(("State: bits %d, bytes %d \n", state->bits, state->bytes)); + TRACE(("State: context %p \n", state->context)); + + clientstate->loc = 0; + clientstate->size = 0; + clientstate->eof = 0; + clientstate->data = 0; + clientstate->flrealloc = 0; + clientstate->fp = fp; + + state->state = 0; + + if (fp) { + TRACE(("Opening using fd: %d for writing \n", clientstate->fp)); + clientstate->tiff = TIFFFdOpen(fd_to_tiff_fd(clientstate->fp), filename, mode); + } else { + // calloc a buffer to write the tif, we're going to need to realloc or something + // if we need bigger. + TRACE(("Opening a buffer for writing \n")); + /* calloc check ok, small constant allocation */ + clientstate->data = calloc(bufsize, 1); + clientstate->size = bufsize; + clientstate->flrealloc = 1; + + if (!clientstate->data) { + TRACE(("Error, couldn't allocate a buffer of size %d\n", bufsize)); + return 0; + } + + clientstate->tiff = TIFFClientOpen( + filename, + mode, + (thandle_t)clientstate, + _tiffReadProc, + _tiffWriteProc, + _tiffSeekProc, + _tiffCloseProc, + _tiffSizeProc, + _tiffNullMapProc, + _tiffUnmapProc); /*force no mmap*/ + } + + if (!clientstate->tiff) { + TRACE(("Error, couldn't open tiff file\n")); + return 0; + } + + return 1; +} + +int +ImagingLibTiffMergeFieldInfo( + ImagingCodecState state, TIFFDataType field_type, int key, int is_var_length) { + // Refer to libtiff docs (http://www.simplesystems.org/libtiff/addingtags.html) + TIFFSTATE *clientstate = (TIFFSTATE *)state->context; + uint32_t n; + int status = 0; + + // custom fields added with ImagingLibTiffMergeFieldInfo are only used for + // decoding, ignore readcount; + int readcount = is_var_length ? TIFF_VARIABLE : 1; + // we support writing a single value, or a variable number of values + int writecount = is_var_length ? TIFF_VARIABLE : 1; + // whether the first value should encode the number of values. + int passcount = (is_var_length && field_type != TIFF_ASCII) ? 1 : 0; + + TIFFFieldInfo info[] = { + {key, + readcount, + writecount, + field_type, + FIELD_CUSTOM, + 1, + passcount, + "CustomField"}}; + + n = sizeof(info) / sizeof(info[0]); + + // Test for libtiff 4.0 or later, excluding libtiff 3.9.6 and 3.9.7 +#if TIFFLIB_VERSION >= 20111221 && TIFFLIB_VERSION != 20120218 && \ + TIFFLIB_VERSION != 20120922 + status = TIFFMergeFieldInfo(clientstate->tiff, info, n); +#else + TIFFMergeFieldInfo(clientstate->tiff, info, n); +#endif + return status; +} + +int +ImagingLibTiffSetField(ImagingCodecState state, ttag_t tag, ...) { + // after tif_dir.c->TIFFSetField. + TIFFSTATE *clientstate = (TIFFSTATE *)state->context; + va_list ap; + int status; + + va_start(ap, tag); + status = TIFFVSetField(clientstate->tiff, tag, ap); + va_end(ap); + return status; +} + +int +ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes) { + /* One shot encoder. Encode everything to the tiff in the clientstate. + If we're running off of a FD, then run once, we're good, everything + ends up in the file, we close and we're done. + + If we're going to memory, then we need to write the whole file into memory, then + parcel it back out to the pystring buffer bytes at a time. + + */ + + TIFFSTATE *clientstate = (TIFFSTATE *)state->context; + TIFF *tiff = clientstate->tiff; + + TRACE(("in encoder: bytes %d\n", bytes)); + TRACE( + ("State: count %d, state %d, x %d, y %d, ystep %d\n", + state->count, + state->state, + state->x, + state->y, + state->ystep)); + TRACE( + ("State: xsize %d, ysize %d, xoff %d, yoff %d \n", + state->xsize, + state->ysize, + state->xoff, + state->yoff)); + TRACE(("State: bits %d, bytes %d \n", state->bits, state->bytes)); + TRACE( + ("Buffer: %p: %c%c%c%c\n", + buffer, + (char)buffer[0], + (char)buffer[1], + (char)buffer[2], + (char)buffer[3])); + TRACE( + ("State->Buffer: %c%c%c%c\n", + (char)state->buffer[0], + (char)state->buffer[1], + (char)state->buffer[2], + (char)state->buffer[3])); + TRACE( + ("Image: mode %s, type %d, bands: %d, xsize %d, ysize %d \n", + im->mode, + im->type, + im->bands, + im->xsize, + im->ysize)); + TRACE( + ("Image: image8 %p, image32 %p, image %p, block %p \n", + im->image8, + im->image32, + im->image, + im->block)); + TRACE(("Image: pixelsize: %d, linesize %d \n", im->pixelsize, im->linesize)); + + dump_state(clientstate); + + if (state->state == 0) { + TRACE(("Encoding line by line")); + while (state->y < state->ysize) { + state->shuffle( + state->buffer, + (UINT8 *)im->image[state->y + state->yoff] + + state->xoff * im->pixelsize, + state->xsize); + + if (TIFFWriteScanline( + tiff, (tdata_t)(state->buffer), (uint32_t)state->y, 0) == -1) { + TRACE(("Encode Error, row %d\n", state->y)); + state->errcode = IMAGING_CODEC_BROKEN; + TIFFClose(tiff); + if (!clientstate->fp) { + free(clientstate->data); + } + return -1; + } + state->y++; + } + + if (state->y == state->ysize) { + state->state = 1; + + TRACE(("Flushing \n")); + if (!TIFFFlush(tiff)) { + TRACE(("Error flushing the tiff")); + // likely reason is memory. + state->errcode = IMAGING_CODEC_MEMORY; + TIFFClose(tiff); + if (!clientstate->fp) { + free(clientstate->data); + } + return -1; + } + TRACE(("Closing \n")); + TIFFClose(tiff); + // reset the clientstate metadata to use it to read out the buffer. + clientstate->loc = 0; + clientstate->size = clientstate->eof; // redundant? + } + } + + if (state->state == 1 && !clientstate->fp) { + int read = + (int)_tiffReadProc((thandle_t)clientstate, (tdata_t)buffer, (tsize_t)bytes); + TRACE( + ("Buffer: %p: %c%c%c%c\n", + buffer, + (char)buffer[0], + (char)buffer[1], + (char)buffer[2], + (char)buffer[3])); + if (clientstate->loc == clientstate->eof) { + TRACE(("Hit EOF, calling an end, freeing data")); + state->errcode = IMAGING_CODEC_END; + free(clientstate->data); + } + return read; + } + + state->errcode = IMAGING_CODEC_END; + return 0; +} + +const char * +ImagingTiffVersion(void) { + return TIFFGetVersion(); +} + +#endif diff --git a/src/libImaging/TiffDecode.h b/src/libImaging/TiffDecode.h index 212b7dee6..eecee0f6f 100644 --- a/src/libImaging/TiffDecode.h +++ b/src/libImaging/TiffDecode.h @@ -1,60 +1,60 @@ -/* - * The Python Imaging Library. - * $Id: //modules/pil/libImaging/Tiff.h#1 $ - * - * declarations for the LibTiff-based Group3 and Group4 decoder - * - */ - -#ifndef _TIFFIO_ -#include -#endif -#ifndef _TIFF_ -#include -#endif - -#ifndef min -#define min(x, y) ((x > y) ? y : x) -#define max(x, y) ((x < y) ? y : x) -#endif - -#ifndef _PIL_LIBTIFF_ -#define _PIL_LIBTIFF_ - -typedef struct { - tdata_t data; /* tdata_t == void* */ - toff_t loc; /* toff_t == uint32 */ - tsize_t size; /* tsize_t == int32 */ - int fp; - uint32_t ifd; /* offset of the ifd, used for multipage - * Should be uint32 for libtiff 3.9.x - * uint64 for libtiff 4.0.x - */ - TIFF *tiff; /* Used in write */ - toff_t eof; - int flrealloc; /* may we realloc */ -} TIFFSTATE; - -extern int -ImagingLibTiffInit(ImagingCodecState state, int fp, uint32_t offset); -extern int -ImagingLibTiffEncodeInit(ImagingCodecState state, char *filename, int fp); -extern int -ImagingLibTiffMergeFieldInfo( - ImagingCodecState state, TIFFDataType field_type, int key, int is_var_length); -extern int -ImagingLibTiffSetField(ImagingCodecState state, ttag_t tag, ...); - -/* - Trace debugging - legacy, don't enable for Python 3.x, unicode issues. -*/ - -/* -#define VA_ARGS(...) __VA_ARGS__ -#define TRACE(args) fprintf(stderr, VA_ARGS args) -*/ - -#define TRACE(args) - -#endif +/* + * The Python Imaging Library. + * $Id: //modules/pil/libImaging/Tiff.h#1 $ + * + * declarations for the LibTiff-based Group3 and Group4 decoder + * + */ + +#ifndef _TIFFIO_ +#include +#endif +#ifndef _TIFF_ +#include +#endif + +#ifndef min +#define min(x, y) ((x > y) ? y : x) +#define max(x, y) ((x < y) ? y : x) +#endif + +#ifndef _PIL_LIBTIFF_ +#define _PIL_LIBTIFF_ + +typedef struct { + tdata_t data; /* tdata_t == void* */ + toff_t loc; /* toff_t == uint32 */ + tsize_t size; /* tsize_t == int32 */ + int fp; + uint32_t ifd; /* offset of the ifd, used for multipage + * Should be uint32 for libtiff 3.9.x + * uint64 for libtiff 4.0.x + */ + TIFF *tiff; /* Used in write */ + toff_t eof; + int flrealloc; /* may we realloc */ +} TIFFSTATE; + +extern int +ImagingLibTiffInit(ImagingCodecState state, int fp, uint32_t offset); +extern int +ImagingLibTiffEncodeInit(ImagingCodecState state, char *filename, int fp); +extern int +ImagingLibTiffMergeFieldInfo( + ImagingCodecState state, TIFFDataType field_type, int key, int is_var_length); +extern int +ImagingLibTiffSetField(ImagingCodecState state, ttag_t tag, ...); + +/* + Trace debugging + legacy, don't enable for Python 3.x, unicode issues. +*/ + +/* +#define VA_ARGS(...) __VA_ARGS__ +#define TRACE(args) fprintf(stderr, VA_ARGS args) +*/ + +#define TRACE(args) + +#endif diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c index 1b84cd68f..e41dd1628 100644 --- a/src/libImaging/Unpack.c +++ b/src/libImaging/Unpack.c @@ -1,1845 +1,1845 @@ -/* - * The Python Imaging Library. - * $Id$ - * - * code to unpack raw data from various file formats - * - * history: - * 1996-03-07 fl Created (from various decoders) - * 1996-04-19 fl Added band unpackers - * 1996-05-12 fl Published RGB unpackers - * 1996-05-27 fl Added nibble unpacker - * 1996-12-10 fl Added complete set of PNG unpackers - * 1996-12-29 fl Set alpha byte in RGB unpackers - * 1997-01-05 fl Added remaining TGA unpackers - * 1997-01-18 fl Added inverting band unpackers - * 1997-01-25 fl Added FlashPix unpackers - * 1997-05-31 fl Added floating point unpackers - * 1998-02-08 fl Added I unpacker - * 1998-07-01 fl Added YCbCr unpacker - * 1998-07-02 fl Added full set of integer unpackers - * 1998-12-29 fl Added mode field, I;16 unpackers - * 1998-12-30 fl Added RGBX modes - * 1999-02-04 fl Fixed I;16 unpackers - * 2003-05-13 fl Added L/RGB reversed unpackers - * 2003-09-26 fl Added LA/PA and RGBa->RGB unpackers - * - * Copyright (c) 1997-2003 by Secret Labs AB. - * Copyright (c) 1996-1997 by Fredrik Lundh. - * - * See the README file for information on usage and redistribution. - */ - -#include "Imaging.h" -#include "Convert.h" - -#define R 0 -#define G 1 -#define B 2 -#define X 3 - -#define A 3 - -#define C 0 -#define M 1 -#define Y 2 -#define K 3 - -/* byte-swapping macros */ - -#define C16N (tmp[0] = in[0], tmp[1] = in[1]); -#define C16S (tmp[1] = in[0], tmp[0] = in[1]); -#define C32N (tmp[0] = in[0], tmp[1] = in[1], tmp[2] = in[2], tmp[3] = in[3]); -#define C32S (tmp[3] = in[0], tmp[2] = in[1], tmp[1] = in[2], tmp[0] = in[3]); -#define C64N \ - (tmp[0] = in[0], \ - tmp[1] = in[1], \ - tmp[2] = in[2], \ - tmp[3] = in[3], \ - tmp[4] = in[4], \ - tmp[5] = in[5], \ - tmp[6] = in[6], \ - tmp[7] = in[7]); -#define C64S \ - (tmp[7] = in[0], \ - tmp[6] = in[1], \ - tmp[5] = in[2], \ - tmp[4] = in[3], \ - tmp[3] = in[4], \ - tmp[2] = in[5], \ - tmp[1] = in[6], \ - tmp[0] = in[7]); - -#ifdef WORDS_BIGENDIAN -#define C16B C16N -#define C16L C16S -#define C32B C32N -#define C32L C32S -#define C64B C64N -#define C64L C64S -#else -#define C16B C16S -#define C16L C16N -#define C32B C32S -#define C32L C32N -#define C64B C64S -#define C64L C64N -#endif - -/* bit-swapping */ - -static UINT8 BITFLIP[] = { - 0, 128, 64, 192, 32, 160, 96, 224, 16, 144, 80, 208, 48, 176, 112, 240, - 8, 136, 72, 200, 40, 168, 104, 232, 24, 152, 88, 216, 56, 184, 120, 248, - 4, 132, 68, 196, 36, 164, 100, 228, 20, 148, 84, 212, 52, 180, 116, 244, - 12, 140, 76, 204, 44, 172, 108, 236, 28, 156, 92, 220, 60, 188, 124, 252, - 2, 130, 66, 194, 34, 162, 98, 226, 18, 146, 82, 210, 50, 178, 114, 242, - 10, 138, 74, 202, 42, 170, 106, 234, 26, 154, 90, 218, 58, 186, 122, 250, - 6, 134, 70, 198, 38, 166, 102, 230, 22, 150, 86, 214, 54, 182, 118, 246, - 14, 142, 78, 206, 46, 174, 110, 238, 30, 158, 94, 222, 62, 190, 126, 254, - 1, 129, 65, 193, 33, 161, 97, 225, 17, 145, 81, 209, 49, 177, 113, 241, - 9, 137, 73, 201, 41, 169, 105, 233, 25, 153, 89, 217, 57, 185, 121, 249, - 5, 133, 69, 197, 37, 165, 101, 229, 21, 149, 85, 213, 53, 181, 117, 245, - 13, 141, 77, 205, 45, 173, 109, 237, 29, 157, 93, 221, 61, 189, 125, 253, - 3, 131, 67, 195, 35, 163, 99, 227, 19, 147, 83, 211, 51, 179, 115, 243, - 11, 139, 75, 203, 43, 171, 107, 235, 27, 155, 91, 219, 59, 187, 123, 251, - 7, 135, 71, 199, 39, 167, 103, 231, 23, 151, 87, 215, 55, 183, 119, 247, - 15, 143, 79, 207, 47, 175, 111, 239, 31, 159, 95, 223, 63, 191, 127, 255}; - -/* Unpack to "1" image */ - -static void -unpack1(UINT8 *out, const UINT8 *in, int pixels) { - /* bits (msb first, white is non-zero) */ - while (pixels > 0) { - UINT8 byte = *in++; - switch (pixels) { - default: - *out++ = (byte & 128) ? 255 : 0; - byte <<= 1; - case 7: - *out++ = (byte & 128) ? 255 : 0; - byte <<= 1; - case 6: - *out++ = (byte & 128) ? 255 : 0; - byte <<= 1; - case 5: - *out++ = (byte & 128) ? 255 : 0; - byte <<= 1; - case 4: - *out++ = (byte & 128) ? 255 : 0; - byte <<= 1; - case 3: - *out++ = (byte & 128) ? 255 : 0; - byte <<= 1; - case 2: - *out++ = (byte & 128) ? 255 : 0; - byte <<= 1; - case 1: - *out++ = (byte & 128) ? 255 : 0; - } - pixels -= 8; - } -} - -static void -unpack1I(UINT8 *out, const UINT8 *in, int pixels) { - /* bits (msb first, white is zero) */ - while (pixels > 0) { - UINT8 byte = *in++; - switch (pixels) { - default: - *out++ = (byte & 128) ? 0 : 255; - byte <<= 1; - case 7: - *out++ = (byte & 128) ? 0 : 255; - byte <<= 1; - case 6: - *out++ = (byte & 128) ? 0 : 255; - byte <<= 1; - case 5: - *out++ = (byte & 128) ? 0 : 255; - byte <<= 1; - case 4: - *out++ = (byte & 128) ? 0 : 255; - byte <<= 1; - case 3: - *out++ = (byte & 128) ? 0 : 255; - byte <<= 1; - case 2: - *out++ = (byte & 128) ? 0 : 255; - byte <<= 1; - case 1: - *out++ = (byte & 128) ? 0 : 255; - } - pixels -= 8; - } -} - -static void -unpack1R(UINT8 *out, const UINT8 *in, int pixels) { - /* bits (lsb first, white is non-zero) */ - while (pixels > 0) { - UINT8 byte = *in++; - switch (pixels) { - default: - *out++ = (byte & 1) ? 255 : 0; - byte >>= 1; - case 7: - *out++ = (byte & 1) ? 255 : 0; - byte >>= 1; - case 6: - *out++ = (byte & 1) ? 255 : 0; - byte >>= 1; - case 5: - *out++ = (byte & 1) ? 255 : 0; - byte >>= 1; - case 4: - *out++ = (byte & 1) ? 255 : 0; - byte >>= 1; - case 3: - *out++ = (byte & 1) ? 255 : 0; - byte >>= 1; - case 2: - *out++ = (byte & 1) ? 255 : 0; - byte >>= 1; - case 1: - *out++ = (byte & 1) ? 255 : 0; - } - pixels -= 8; - } -} - -static void -unpack1IR(UINT8 *out, const UINT8 *in, int pixels) { - /* bits (lsb first, white is zero) */ - while (pixels > 0) { - UINT8 byte = *in++; - switch (pixels) { - default: - *out++ = (byte & 1) ? 0 : 255; - byte >>= 1; - case 7: - *out++ = (byte & 1) ? 0 : 255; - byte >>= 1; - case 6: - *out++ = (byte & 1) ? 0 : 255; - byte >>= 1; - case 5: - *out++ = (byte & 1) ? 0 : 255; - byte >>= 1; - case 4: - *out++ = (byte & 1) ? 0 : 255; - byte >>= 1; - case 3: - *out++ = (byte & 1) ? 0 : 255; - byte >>= 1; - case 2: - *out++ = (byte & 1) ? 0 : 255; - byte >>= 1; - case 1: - *out++ = (byte & 1) ? 0 : 255; - } - pixels -= 8; - } -} - -static void -unpack18(UINT8 *out, const UINT8 *in, int pixels) { - /* Unpack a '|b1' image, which is a numpy boolean. - 1 == true, 0==false, in bytes */ - - int i; - for (i = 0; i < pixels; i++) { - out[i] = in[i] > 0 ? 255 : 0; - } -} - -/* Unpack to "L" image */ - -static void -unpackL2(UINT8 *out, const UINT8 *in, int pixels) { - /* nibbles (msb first, white is non-zero) */ - while (pixels > 0) { - UINT8 byte = *in++; - switch (pixels) { - default: - *out++ = ((byte >> 6) & 0x03U) * 0x55U; - byte <<= 2; - case 3: - *out++ = ((byte >> 6) & 0x03U) * 0x55U; - byte <<= 2; - case 2: - *out++ = ((byte >> 6) & 0x03U) * 0x55U; - byte <<= 2; - case 1: - *out++ = ((byte >> 6) & 0x03U) * 0x55U; - } - pixels -= 4; - } -} - -static void -unpackL2I(UINT8 *out, const UINT8 *in, int pixels) { - /* nibbles (msb first, white is zero) */ - while (pixels > 0) { - UINT8 byte = *in++; - switch (pixels) { - default: - *out++ = 0xFFU - (UINT8)(((byte >> 6) & 0x03U) * 0x55U); - byte <<= 2; - case 3: - *out++ = 0xFFU - (UINT8)(((byte >> 6) & 0x03U) * 0x55U); - byte <<= 2; - case 2: - *out++ = 0xFFU - (UINT8)(((byte >> 6) & 0x03U) * 0x55U); - byte <<= 2; - case 1: - *out++ = 0xFFU - (UINT8)(((byte >> 6) & 0x03U) * 0x55U); - } - pixels -= 4; - } -} - -static void -unpackL2R(UINT8 *out, const UINT8 *in, int pixels) { - /* nibbles (bit order reversed, white is non-zero) */ - while (pixels > 0) { - UINT8 byte = *in++; - byte = BITFLIP[byte]; - switch (pixels) { - default: - *out++ = ((byte >> 6) & 0x03U) * 0x55U; - byte <<= 2; - case 3: - *out++ = ((byte >> 6) & 0x03U) * 0x55U; - byte <<= 2; - case 2: - *out++ = ((byte >> 6) & 0x03U) * 0x55U; - byte <<= 2; - case 1: - *out++ = ((byte >> 6) & 0x03U) * 0x55U; - } - pixels -= 4; - } -} - -static void -unpackL2IR(UINT8 *out, const UINT8 *in, int pixels) { - /* nibbles (bit order reversed, white is zero) */ - while (pixels > 0) { - UINT8 byte = *in++; - byte = BITFLIP[byte]; - switch (pixels) { - default: - *out++ = 0xFFU - (UINT8)(((byte >> 6) & 0x03U) * 0x55U); - byte <<= 2; - case 3: - *out++ = 0xFFU - (UINT8)(((byte >> 6) & 0x03U) * 0x55U); - byte <<= 2; - case 2: - *out++ = 0xFFU - (UINT8)(((byte >> 6) & 0x03U) * 0x55U); - byte <<= 2; - case 1: - *out++ = 0xFFU - (UINT8)(((byte >> 6) & 0x03U) * 0x55U); - } - pixels -= 4; - } -} - -static void -unpackL4(UINT8 *out, const UINT8 *in, int pixels) { - /* nibbles (msb first, white is non-zero) */ - while (pixels > 0) { - UINT8 byte = *in++; - switch (pixels) { - default: - *out++ = ((byte >> 4) & 0x0FU) * 0x11U; - byte <<= 4; - case 1: - *out++ = ((byte >> 4) & 0x0FU) * 0x11U; - } - pixels -= 2; - } -} - -static void -unpackL4I(UINT8 *out, const UINT8 *in, int pixels) { - /* nibbles (msb first, white is zero) */ - while (pixels > 0) { - UINT8 byte = *in++; - switch (pixels) { - default: - *out++ = 0xFFU - (UINT8)(((byte >> 4) & 0x0FU) * 0x11U); - byte <<= 4; - case 1: - *out++ = 0xFFU - (UINT8)(((byte >> 4) & 0x0FU) * 0x11U); - } - pixels -= 2; - } -} - -static void -unpackL4R(UINT8 *out, const UINT8 *in, int pixels) { - /* nibbles (bit order reversed, white is non-zero) */ - while (pixels > 0) { - UINT8 byte = *in++; - byte = BITFLIP[byte]; - switch (pixels) { - default: - *out++ = ((byte >> 4) & 0x0FU) * 0x11U; - byte <<= 4; - case 1: - *out++ = ((byte >> 4) & 0x0FU) * 0x11U; - } - pixels -= 2; - } -} - -static void -unpackL4IR(UINT8 *out, const UINT8 *in, int pixels) { - /* nibbles (bit order reversed, white is zero) */ - while (pixels > 0) { - UINT8 byte = *in++; - byte = BITFLIP[byte]; - switch (pixels) { - default: - *out++ = 0xFFU - (UINT8)(((byte >> 4) & 0x0FU) * 0x11U); - byte <<= 4; - case 1: - *out++ = 0xFFU - (UINT8)(((byte >> 4) & 0x0FU) * 0x11U); - } - pixels -= 2; - } -} - -static void -unpackLA(UINT8 *_out, const UINT8 *in, int pixels) { - int i; - /* LA, pixel interleaved */ - for (i = 0; i < pixels; i++) { - UINT32 iv = MAKE_UINT32(in[0], in[0], in[0], in[1]); - memcpy(_out, &iv, sizeof(iv)); - in += 2; - _out += 4; - } -} - -static void -unpackLAL(UINT8 *_out, const UINT8 *in, int pixels) { - int i; - /* LA, line interleaved */ - for (i = 0; i < pixels; i++, _out += 4) { - UINT32 iv = MAKE_UINT32(in[i], in[i], in[i], in[i + pixels]); - memcpy(_out, &iv, sizeof(iv)); - } -} - -static void -unpackLI(UINT8 *out, const UINT8 *in, int pixels) { - /* negative */ - int i; - for (i = 0; i < pixels; i++) { - out[i] = ~in[i]; - } -} - -static void -unpackLR(UINT8 *out, const UINT8 *in, int pixels) { - int i; - /* RGB, bit reversed */ - for (i = 0; i < pixels; i++) { - out[i] = BITFLIP[in[i]]; - } -} - -static void -unpackL16(UINT8 *out, const UINT8 *in, int pixels) { - /* int16 (upper byte, little endian) */ - int i; - for (i = 0; i < pixels; i++) { - out[i] = in[1]; - in += 2; - } -} - -static void -unpackL16B(UINT8 *out, const UINT8 *in, int pixels) { - int i; - /* int16 (upper byte, big endian) */ - for (i = 0; i < pixels; i++) { - out[i] = in[0]; - in += 2; - } -} - -/* Unpack to "P" image */ - -static void -unpackP1(UINT8 *out, const UINT8 *in, int pixels) { - /* bits */ - while (pixels > 0) { - UINT8 byte = *in++; - switch (pixels) { - default: - *out++ = (byte >> 7) & 1; - byte <<= 1; - case 7: - *out++ = (byte >> 7) & 1; - byte <<= 1; - case 6: - *out++ = (byte >> 7) & 1; - byte <<= 1; - case 5: - *out++ = (byte >> 7) & 1; - byte <<= 1; - case 4: - *out++ = (byte >> 7) & 1; - byte <<= 1; - case 3: - *out++ = (byte >> 7) & 1; - byte <<= 1; - case 2: - *out++ = (byte >> 7) & 1; - byte <<= 1; - case 1: - *out++ = (byte >> 7) & 1; - } - pixels -= 8; - } -} - -static void -unpackP2(UINT8 *out, const UINT8 *in, int pixels) { - /* bit pairs */ - while (pixels > 0) { - UINT8 byte = *in++; - switch (pixels) { - default: - *out++ = (byte >> 6) & 3; - byte <<= 2; - case 3: - *out++ = (byte >> 6) & 3; - byte <<= 2; - case 2: - *out++ = (byte >> 6) & 3; - byte <<= 2; - case 1: - *out++ = (byte >> 6) & 3; - } - pixels -= 4; - } -} - -static void -unpackP4(UINT8 *out, const UINT8 *in, int pixels) { - /* nibbles */ - while (pixels > 0) { - UINT8 byte = *in++; - switch (pixels) { - default: - *out++ = (byte >> 4) & 15; - byte <<= 4; - case 1: - *out++ = (byte >> 4) & 15; - } - pixels -= 2; - } -} - -static void -unpackP2L(UINT8 *out, const UINT8 *in, int pixels) { - int i, j, m, s; - /* bit layers */ - m = 128; - s = (pixels + 7) / 8; - for (i = j = 0; i < pixels; i++) { - out[i] = ((in[j] & m) ? 1 : 0) + ((in[j + s] & m) ? 2 : 0); - if ((m >>= 1) == 0) { - m = 128; - j++; - } - } -} - -static void -unpackP4L(UINT8 *out, const UINT8 *in, int pixels) { - int i, j, m, s; - /* bit layers (trust the optimizer ;-) */ - m = 128; - s = (pixels + 7) / 8; - for (i = j = 0; i < pixels; i++) { - out[i] = ((in[j] & m) ? 1 : 0) + ((in[j + s] & m) ? 2 : 0) + - ((in[j + 2 * s] & m) ? 4 : 0) + ((in[j + 3 * s] & m) ? 8 : 0); - if ((m >>= 1) == 0) { - m = 128; - j++; - } - } -} - -/* Unpack to "RGB" image */ - -void -ImagingUnpackRGB(UINT8 *_out, const UINT8 *in, int pixels) { - int i = 0; - /* RGB triplets */ - for (; i < pixels - 1; i++) { - UINT32 iv; - memcpy(&iv, in, sizeof(iv)); - iv |= MASK_UINT32_CHANNEL_3; - memcpy(_out, &iv, sizeof(iv)); - in += 3; - _out += 4; - } - for (; i < pixels; i++) { - UINT32 iv = MAKE_UINT32(in[0], in[1], in[2], 255); - memcpy(_out, &iv, sizeof(iv)); - in += 3; - _out += 4; - } -} - -void -unpackRGB16L(UINT8 *_out, const UINT8 *in, int pixels) { - int i; - /* 16-bit RGB triplets, little-endian order */ - for (i = 0; i < pixels; i++) { - UINT32 iv = MAKE_UINT32(in[1], in[3], in[5], 255); - memcpy(_out, &iv, sizeof(iv)); - in += 6; - _out += 4; - } -} - -void -unpackRGB16B(UINT8 *_out, const UINT8 *in, int pixels) { - int i; - /* 16-bit RGB triplets, big-endian order */ - for (i = 0; i < pixels; i++) { - UINT32 iv = MAKE_UINT32(in[0], in[2], in[4], 255); - memcpy(_out, &iv, sizeof(iv)); - in += 6; - _out += 4; - } -} - -static void -unpackRGBL(UINT8 *_out, const UINT8 *in, int pixels) { - int i; - /* RGB, line interleaved */ - for (i = 0; i < pixels; i++, _out += 4) { - UINT32 iv = MAKE_UINT32(in[i], in[i + pixels], in[i + pixels + pixels], 255); - memcpy(_out, &iv, sizeof(iv)); - } -} - -static void -unpackRGBR(UINT8 *_out, const UINT8 *in, int pixels) { - int i; - /* RGB, bit reversed */ - for (i = 0; i < pixels; i++) { - UINT32 iv = MAKE_UINT32(BITFLIP[in[0]], BITFLIP[in[1]], BITFLIP[in[2]], 255); - memcpy(_out, &iv, sizeof(iv)); - in += 3; - _out += 4; - } -} - -void -ImagingUnpackBGR(UINT8 *_out, const UINT8 *in, int pixels) { - int i; - /* RGB, reversed bytes */ - for (i = 0; i < pixels; i++) { - UINT32 iv = MAKE_UINT32(in[2], in[1], in[0], 255); - memcpy(_out, &iv, sizeof(iv)); - in += 3; - _out += 4; - } -} - -void -ImagingUnpackRGB15(UINT8 *out, const UINT8 *in, int pixels) { - int i, pixel; - /* RGB, 5 bits per pixel */ - for (i = 0; i < pixels; i++) { - pixel = in[0] + (in[1] << 8); - out[R] = (pixel & 31) * 255 / 31; - out[G] = ((pixel >> 5) & 31) * 255 / 31; - out[B] = ((pixel >> 10) & 31) * 255 / 31; - out[A] = 255; - out += 4; - in += 2; - } -} - -void -ImagingUnpackRGBA15(UINT8 *out, const UINT8 *in, int pixels) { - int i, pixel; - /* RGB, 5/5/5/1 bits per pixel */ - for (i = 0; i < pixels; i++) { - pixel = in[0] + (in[1] << 8); - out[R] = (pixel & 31) * 255 / 31; - out[G] = ((pixel >> 5) & 31) * 255 / 31; - out[B] = ((pixel >> 10) & 31) * 255 / 31; - out[A] = (pixel >> 15) * 255; - out += 4; - in += 2; - } -} - -void -ImagingUnpackBGR15(UINT8 *out, const UINT8 *in, int pixels) { - int i, pixel; - /* RGB, reversed bytes, 5 bits per pixel */ - for (i = 0; i < pixels; i++) { - pixel = in[0] + (in[1] << 8); - out[B] = (pixel & 31) * 255 / 31; - out[G] = ((pixel >> 5) & 31) * 255 / 31; - out[R] = ((pixel >> 10) & 31) * 255 / 31; - out[A] = 255; - out += 4; - in += 2; - } -} - -void -ImagingUnpackBGRA15(UINT8 *out, const UINT8 *in, int pixels) { - int i, pixel; - /* RGB, rearranged channels, 5/5/5/1 bits per pixel */ - for (i = 0; i < pixels; i++) { - pixel = in[0] + (in[1] << 8); - out[B] = (pixel & 31) * 255 / 31; - out[G] = ((pixel >> 5) & 31) * 255 / 31; - out[R] = ((pixel >> 10) & 31) * 255 / 31; - out[A] = (pixel >> 15) * 255; - out += 4; - in += 2; - } -} - -void -ImagingUnpackRGB16(UINT8 *out, const UINT8 *in, int pixels) { - int i, pixel; - /* RGB, 5/6/5 bits per pixel */ - for (i = 0; i < pixels; i++) { - pixel = in[0] + (in[1] << 8); - out[R] = (pixel & 31) * 255 / 31; - out[G] = ((pixel >> 5) & 63) * 255 / 63; - out[B] = ((pixel >> 11) & 31) * 255 / 31; - out[A] = 255; - out += 4; - in += 2; - } -} - -void -ImagingUnpackBGR16(UINT8 *out, const UINT8 *in, int pixels) { - int i, pixel; - /* RGB, reversed bytes, 5/6/5 bits per pixel */ - for (i = 0; i < pixels; i++) { - pixel = in[0] + (in[1] << 8); - out[B] = (pixel & 31) * 255 / 31; - out[G] = ((pixel >> 5) & 63) * 255 / 63; - out[R] = ((pixel >> 11) & 31) * 255 / 31; - out[A] = 255; - out += 4; - in += 2; - } -} - -void -ImagingUnpackRGB4B(UINT8 *out, const UINT8 *in, int pixels) { - int i, pixel; - /* RGB, 4 bits per pixel */ - for (i = 0; i < pixels; i++) { - pixel = in[0] + (in[1] << 8); - out[R] = (pixel & 15) * 17; - out[G] = ((pixel >> 4) & 15) * 17; - out[B] = ((pixel >> 8) & 15) * 17; - out[A] = 255; - out += 4; - in += 2; - } -} - -void -ImagingUnpackRGBA4B(UINT8 *out, const UINT8 *in, int pixels) { - int i, pixel; - /* RGBA, 4 bits per pixel */ - for (i = 0; i < pixels; i++) { - pixel = in[0] + (in[1] << 8); - out[R] = (pixel & 15) * 17; - out[G] = ((pixel >> 4) & 15) * 17; - out[B] = ((pixel >> 8) & 15) * 17; - out[A] = ((pixel >> 12) & 15) * 17; - out += 4; - in += 2; - } -} - -static void -ImagingUnpackBGRX(UINT8 *_out, const UINT8 *in, int pixels) { - int i; - /* RGB, reversed bytes with padding */ - for (i = 0; i < pixels; i++) { - UINT32 iv = MAKE_UINT32(in[2], in[1], in[0], 255); - memcpy(_out, &iv, sizeof(iv)); - in += 4; - _out += 4; - } -} - -static void -ImagingUnpackBGXR(UINT8 *_out, const UINT8 *in, int pixels) { - int i; - for (i = 0; i < pixels; i++) { - UINT32 iv = MAKE_UINT32(in[3], in[1], in[0], 255); - memcpy(_out, &iv, sizeof(iv)); - in += 4; - _out += 4; - } -} - -static void -ImagingUnpackXRGB(UINT8 *_out, const UINT8 *in, int pixels) { - int i; - /* RGB, leading pad */ - for (i = 0; i < pixels; i++) { - UINT32 iv = MAKE_UINT32(in[1], in[2], in[3], 255); - memcpy(_out, &iv, sizeof(iv)); - in += 4; - _out += 4; - } -} - -static void -ImagingUnpackXBGR(UINT8 *_out, const UINT8 *in, int pixels) { - int i; - /* RGB, reversed bytes, leading pad */ - for (i = 0; i < pixels; i++) { - UINT32 iv = MAKE_UINT32(in[3], in[2], in[1], 255); - memcpy(_out, &iv, sizeof(iv)); - in += 4; - _out += 4; - } -} - -/* Unpack to "RGBA" image */ - -static void -unpackRGBALA(UINT8 *_out, const UINT8 *in, int pixels) { - int i; - /* grayscale with alpha */ - for (i = 0; i < pixels; i++) { - UINT32 iv = MAKE_UINT32(in[0], in[0], in[0], in[1]); - memcpy(_out, &iv, sizeof(iv)); - in += 2; - _out += 4; - } -} - -static void -unpackRGBALA16B(UINT8 *_out, const UINT8 *in, int pixels) { - int i; - /* 16-bit grayscale with alpha, big-endian */ - for (i = 0; i < pixels; i++) { - UINT32 iv = MAKE_UINT32(in[0], in[0], in[0], in[2]); - memcpy(_out, &iv, sizeof(iv)); - in += 4; - _out += 4; - } -} - -static void -unpackRGBa16L(UINT8 *_out, const UINT8 *in, int pixels) { - int i; - /* premultiplied 16-bit RGBA, little-endian */ - for (i = 0; i < pixels; i++) { - int a = in[7]; - UINT32 iv; - if (!a) { - iv = 0; - } else if (a == 255) { - iv = MAKE_UINT32(in[1], in[3], in[5], a); - } else { - iv = MAKE_UINT32( - CLIP8(in[1] * 255 / a), - CLIP8(in[3] * 255 / a), - CLIP8(in[5] * 255 / a), - a); - } - memcpy(_out, &iv, sizeof(iv)); - in += 8; - _out += 4; - } -} - -static void -unpackRGBa16B(UINT8 *_out, const UINT8 *in, int pixels) { - int i; - /* premultiplied 16-bit RGBA, big-endian */ - for (i = 0; i < pixels; i++) { - int a = in[6]; - UINT32 iv; - if (!a) { - iv = 0; - } else if (a == 255) { - iv = MAKE_UINT32(in[0], in[2], in[4], a); - } else { - iv = MAKE_UINT32( - CLIP8(in[0] * 255 / a), - CLIP8(in[2] * 255 / a), - CLIP8(in[4] * 255 / a), - a); - } - memcpy(_out, &iv, sizeof(iv)); - in += 8; - _out += 4; - } -} - -static void -unpackRGBa(UINT8 *_out, const UINT8 *in, int pixels) { - int i; - /* premultiplied RGBA */ - for (i = 0; i < pixels; i++) { - int a = in[3]; - UINT32 iv; - if (!a) { - iv = 0; - } else if (a == 255) { - iv = MAKE_UINT32(in[0], in[1], in[2], a); - } else { - iv = MAKE_UINT32( - CLIP8(in[0] * 255 / a), - CLIP8(in[1] * 255 / a), - CLIP8(in[2] * 255 / a), - a); - } - memcpy(_out, &iv, sizeof(iv)); - in += 4; - _out += 4; - } -} - -static void -unpackRGBaskip1(UINT8 *_out, const UINT8 *in, int pixels) { - int i; - UINT32 *out = (UINT32 *)_out; - /* premultiplied RGBA */ - for (i = 0; i < pixels; i++) { - int a = in[3]; - if (!a) { - out[i] = 0; - } else if (a == 255) { - out[i] = MAKE_UINT32(in[0], in[1], in[2], a); - } else { - out[i] = MAKE_UINT32( - CLIP8(in[0] * 255 / a), - CLIP8(in[1] * 255 / a), - CLIP8(in[2] * 255 / a), - a); - } - in += 5; - } -} - -static void -unpackRGBaskip2(UINT8 *_out, const UINT8 *in, int pixels) { - int i; - UINT32 *out = (UINT32 *)_out; - /* premultiplied RGBA */ - for (i = 0; i < pixels; i++) { - int a = in[3]; - if (!a) { - out[i] = 0; - } else if (a == 255) { - out[i] = MAKE_UINT32(in[0], in[1], in[2], a); - } else { - out[i] = MAKE_UINT32( - CLIP8(in[0] * 255 / a), - CLIP8(in[1] * 255 / a), - CLIP8(in[2] * 255 / a), - a); - } - in += 6; - } -} - -static void -unpackBGRa(UINT8 *_out, const UINT8 *in, int pixels) { - int i; - /* premultiplied BGRA */ - for (i = 0; i < pixels; i++) { - int a = in[3]; - UINT32 iv; - if (!a) { - iv = 0; - } else if (a == 255) { - iv = MAKE_UINT32(in[2], in[1], in[0], a); - } else { - iv = MAKE_UINT32( - CLIP8(in[2] * 255 / a), - CLIP8(in[1] * 255 / a), - CLIP8(in[0] * 255 / a), - a); - } - memcpy(_out, &iv, sizeof(iv)); - in += 4; - _out += 4; - } -} - -static void -unpackRGBAI(UINT8 *out, const UINT8 *in, int pixels) { - int i; - /* RGBA, inverted RGB bytes (FlashPix) */ - for (i = 0; i < pixels; i++) { - out[R] = ~in[0]; - out[G] = ~in[1]; - out[B] = ~in[2]; - out[A] = in[3]; - out += 4; - in += 4; - } -} - -static void -unpackRGBAL(UINT8 *_out, const UINT8 *in, int pixels) { - int i; - /* RGBA, line interleaved */ - for (i = 0; i < pixels; i++, _out += 4) { - UINT32 iv = MAKE_UINT32( - in[i], - in[i + pixels], - in[i + pixels + pixels], - in[i + pixels + pixels + pixels]); - memcpy(_out, &iv, sizeof(iv)); - } -} - -void -unpackRGBA16L(UINT8 *_out, const UINT8 *in, int pixels) { - int i; - /* 16-bit RGBA, little-endian order */ - for (i = 0; i < pixels; i++, _out += 4) { - UINT32 iv = MAKE_UINT32(in[1], in[3], in[5], in[7]); - memcpy(_out, &iv, sizeof(iv)); - in += 8; - } -} - -void -unpackRGBA16B(UINT8 *_out, const UINT8 *in, int pixels) { - int i; - /* 16-bit RGBA, big-endian order */ - for (i = 0; i < pixels; i++, _out += 4) { - UINT32 iv = MAKE_UINT32(in[0], in[2], in[4], in[6]); - memcpy(_out, &iv, sizeof(iv)); - in += 8; - } -} - -static void -unpackARGB(UINT8 *_out, const UINT8 *in, int pixels) { - int i; - /* RGBA, leading pad */ - for (i = 0; i < pixels; i++) { - UINT32 iv = MAKE_UINT32(in[1], in[2], in[3], in[0]); - memcpy(_out, &iv, sizeof(iv)); - in += 4; - _out += 4; - } -} - -static void -unpackABGR(UINT8 *_out, const UINT8 *in, int pixels) { - int i; - /* RGBA, reversed bytes */ - for (i = 0; i < pixels; i++) { - UINT32 iv = MAKE_UINT32(in[3], in[2], in[1], in[0]); - memcpy(_out, &iv, sizeof(iv)); - in += 4; - _out += 4; - } -} - -static void -unpackBGRA(UINT8 *_out, const UINT8 *in, int pixels) { - int i; - /* RGBA, rearranged channels */ - for (i = 0; i < pixels; i++) { - UINT32 iv = MAKE_UINT32(in[2], in[1], in[0], in[3]); - memcpy(_out, &iv, sizeof(iv)); - in += 4; - _out += 4; - } -} - -static void -unpackBGRA16L(UINT8 *_out, const UINT8 *in, int pixels) { - int i; - /* 16-bit RGBA, little-endian order, rearranged channels */ - for (i = 0; i < pixels; i++) { - UINT32 iv = MAKE_UINT32(in[5], in[3], in[1], in[7]); - memcpy(_out, &iv, sizeof(iv)); - in += 8; - _out += 4; - } -} - -static void -unpackBGRA16B(UINT8 *_out, const UINT8 *in, int pixels) { - int i; - /* 16-bit RGBA, big-endian order, rearranged channels */ - for (i = 0; i < pixels; i++) { - UINT32 iv = MAKE_UINT32(in[4], in[2], in[0], in[6]); - memcpy(_out, &iv, sizeof(iv)); - in += 8; - _out += 4; - } -} - -static void -unpackBGAR(UINT8 *_out, const UINT8 *in, int pixels) { - int i; - for (i = 0; i < pixels; i++) { - UINT32 iv = MAKE_UINT32(in[3], in[1], in[0], in[2]); - memcpy(_out, &iv, sizeof(iv)); - in += 4; - _out += 4; - } -} - -/* Unpack to "CMYK" image */ - -static void -unpackCMYKI(UINT8 *_out, const UINT8 *in, int pixels) { - int i; - /* CMYK, inverted bytes (Photoshop 2.5) */ - for (i = 0; i < pixels; i++) { - UINT32 iv = ~MAKE_UINT32(in[0], in[1], in[2], in[3]); - memcpy(_out, &iv, sizeof(iv)); - in += 4; - _out += 4; - } -} - -/* Unpack to "LAB" image */ -/* There are two representations of LAB images for whatever precision: - L: Uint (in PS, it's 0-100) - A: Int (in ps, -128 .. 128, or elsewhere 0..255, with 128 as middle. - Channels in PS display a 0 value as middle gray, - LCMS appears to use 128 as the 0 value for these channels) - B: Int (as above) - - Since we don't have any signed ints, we're going with the shifted versions - internally, and we'll unshift for saving and whatnot. -*/ -void -ImagingUnpackLAB(UINT8 *out, const UINT8 *in, int pixels) { - int i; - /* LAB triplets */ - for (i = 0; i < pixels; i++) { - out[0] = in[0]; - out[1] = in[1] ^ 128; /* signed in outside world */ - out[2] = in[2] ^ 128; - out[3] = 255; - out += 4; - in += 3; - } -} - -static void -unpackI16N_I16B(UINT8 *out, const UINT8 *in, int pixels) { - int i; - UINT8 *tmp = (UINT8 *)out; - for (i = 0; i < pixels; i++) { - C16B; - in += 2; - tmp += 2; - } -} -static void -unpackI16N_I16(UINT8 *out, const UINT8 *in, int pixels) { - int i; - UINT8 *tmp = (UINT8 *)out; - for (i = 0; i < pixels; i++) { - C16L; - in += 2; - tmp += 2; - } -} -static void -unpackI16B_I16(UINT8 *out, const UINT8 *in, int pixels) { - int i; - for (i = 0; i < pixels; i++) { - out[0] = in[1]; - out[1] = in[0]; - in += 2; - out += 2; - } -} -static void -unpackI16R_I16(UINT8 *out, const UINT8 *in, int pixels) { - int i; - for (i = 0; i < pixels; i++) { - out[0] = BITFLIP[in[0]]; - out[1] = BITFLIP[in[1]]; - in += 2; - out += 2; - } -} - -static void -unpackI12_I16(UINT8 *out, const UINT8 *in, int pixels) { - /* Fillorder 1/MSB -> LittleEndian, for 12bit integer grayscale tiffs. - - According to the TIFF spec: - - FillOrder = 2 should be used only when BitsPerSample = 1 and - the data is either uncompressed or compressed using CCITT 1D - or 2D compression, to avoid potentially ambiguous situations. - - Yeah. I thought so. We'll see how well people read the spec. - We've got several fillorder=2 modes in TiffImagePlugin.py - - There's no spec I can find. It appears that the in storage - layout is: 00 80 00 ... -> (128 , 0 ...). The samples are - stored in a single big bitian 12bit block, but need to be - pulled out to little endian format to be stored in a 2 byte - int. - */ - - int i; - UINT16 pixel; -#ifdef WORDS_BIGENDIAN - UINT8 *tmp = (UINT8 *)&pixel; -#endif - for (i = 0; i < pixels - 1; i += 2) { - pixel = (((UINT16)in[0]) << 4) + (in[1] >> 4); -#ifdef WORDS_BIGENDIAN - out[0] = tmp[1]; - out[1] = tmp[0]; -#else - memcpy(out, &pixel, sizeof(pixel)); -#endif - - out += 2; - pixel = (((UINT16)(in[1] & 0x0F)) << 8) + in[2]; -#ifdef WORDS_BIGENDIAN - out[0] = tmp[1]; - out[1] = tmp[0]; -#else - memcpy(out, &pixel, sizeof(pixel)); -#endif - - in += 3; - out += 2; - } - if (i == pixels - 1) { - pixel = (((UINT16)in[0]) << 4) + (in[1] >> 4); -#ifdef WORDS_BIGENDIAN - out[0] = tmp[1]; - out[1] = tmp[0]; -#else - memcpy(out, &pixel, sizeof(pixel)); -#endif - } -} - -static void -copy1(UINT8 *out, const UINT8 *in, int pixels) { - /* L, P */ - memcpy(out, in, pixels); -} - -static void -copy2(UINT8 *out, const UINT8 *in, int pixels) { - /* I;16 */ - memcpy(out, in, pixels * 2); -} - -static void -copy3(UINT8 *out, const UINT8 *in, int pixels) { - /* BGR;24 */ - memcpy(out, in, pixels * 3); -} - -static void -copy4(UINT8 *out, const UINT8 *in, int pixels) { - /* RGBA, CMYK quadruples */ - memcpy(out, in, 4 * pixels); -} - -static void -copy4skip1(UINT8 *_out, const UINT8 *in, int pixels) { - int i; - for (i = 0; i < pixels; i++) { - memcpy(_out, in, 4); - in += 5; - _out += 4; - } -} - -static void -copy4skip2(UINT8 *_out, const UINT8 *in, int pixels) { - int i; - for (i = 0; i < pixels; i++) { - memcpy(_out, in, 4); - in += 6; - _out += 4; - } -} - -/* Unpack to "I" and "F" images */ - -#define UNPACK_RAW(NAME, GET, INTYPE, OUTTYPE) \ - static void NAME(UINT8 *out_, const UINT8 *in, int pixels) { \ - int i; \ - OUTTYPE *out = (OUTTYPE *)out_; \ - for (i = 0; i < pixels; i++, in += sizeof(INTYPE)) { \ - out[i] = (OUTTYPE)((INTYPE)GET); \ - } \ - } - -#define UNPACK(NAME, COPY, INTYPE, OUTTYPE) \ - static void NAME(UINT8 *out_, const UINT8 *in, int pixels) { \ - int i; \ - OUTTYPE *out = (OUTTYPE *)out_; \ - INTYPE tmp_; \ - UINT8 *tmp = (UINT8 *)&tmp_; \ - for (i = 0; i < pixels; i++, in += sizeof(INTYPE)) { \ - COPY; \ - out[i] = (OUTTYPE)tmp_; \ - } \ - } - -UNPACK_RAW(unpackI8, in[0], UINT8, INT32) -UNPACK_RAW(unpackI8S, in[0], INT8, INT32) -UNPACK(unpackI16, C16L, UINT16, INT32) -UNPACK(unpackI16S, C16L, INT16, INT32) -UNPACK(unpackI16B, C16B, UINT16, INT32) -UNPACK(unpackI16BS, C16B, INT16, INT32) -UNPACK(unpackI16N, C16N, UINT16, INT32) -UNPACK(unpackI16NS, C16N, INT16, INT32) -UNPACK(unpackI32, C32L, UINT32, INT32) -UNPACK(unpackI32S, C32L, INT32, INT32) -UNPACK(unpackI32B, C32B, UINT32, INT32) -UNPACK(unpackI32BS, C32B, INT32, INT32) -UNPACK(unpackI32N, C32N, UINT32, INT32) -UNPACK(unpackI32NS, C32N, INT32, INT32) - -UNPACK_RAW(unpackF8, in[0], UINT8, FLOAT32) -UNPACK_RAW(unpackF8S, in[0], INT8, FLOAT32) -UNPACK(unpackF16, C16L, UINT16, FLOAT32) -UNPACK(unpackF16S, C16L, INT16, FLOAT32) -UNPACK(unpackF16B, C16B, UINT16, FLOAT32) -UNPACK(unpackF16BS, C16B, INT16, FLOAT32) -UNPACK(unpackF16N, C16N, UINT16, FLOAT32) -UNPACK(unpackF16NS, C16N, INT16, FLOAT32) -UNPACK(unpackF32, C32L, UINT32, FLOAT32) -UNPACK(unpackF32S, C32L, INT32, FLOAT32) -UNPACK(unpackF32B, C32B, UINT32, FLOAT32) -UNPACK(unpackF32BS, C32B, INT32, FLOAT32) -UNPACK(unpackF32N, C32N, UINT32, FLOAT32) -UNPACK(unpackF32NS, C32N, INT32, FLOAT32) -UNPACK(unpackF32F, C32L, FLOAT32, FLOAT32) -UNPACK(unpackF32BF, C32B, FLOAT32, FLOAT32) -UNPACK(unpackF32NF, C32N, FLOAT32, FLOAT32) -#ifdef FLOAT64 -UNPACK(unpackF64F, C64L, FLOAT64, FLOAT32) -UNPACK(unpackF64BF, C64B, FLOAT64, FLOAT32) -UNPACK(unpackF64NF, C64N, FLOAT64, FLOAT32) -#endif - -/* Misc. unpackers */ - -static void -band0(UINT8 *out, const UINT8 *in, int pixels) { - int i; - /* band 0 only */ - for (i = 0; i < pixels; i++) { - out[0] = in[i]; - out += 4; - } -} - -static void -band1(UINT8 *out, const UINT8 *in, int pixels) { - int i; - /* band 1 only */ - for (i = 0; i < pixels; i++) { - out[1] = in[i]; - out += 4; - } -} - -static void -band2(UINT8 *out, const UINT8 *in, int pixels) { - int i; - /* band 2 only */ - for (i = 0; i < pixels; i++) { - out[2] = in[i]; - out += 4; - } -} - -static void -band3(UINT8 *out, const UINT8 *in, int pixels) { - /* band 3 only */ - int i; - for (i = 0; i < pixels; i++) { - out[3] = in[i]; - out += 4; - } -} - -static void -band0I(UINT8 *out, const UINT8 *in, int pixels) { - int i; - /* band 0 only */ - for (i = 0; i < pixels; i++) { - out[0] = ~in[i]; - out += 4; - } -} - -static void -band1I(UINT8 *out, const UINT8 *in, int pixels) { - int i; - /* band 1 only */ - for (i = 0; i < pixels; i++) { - out[1] = ~in[i]; - out += 4; - } -} - -static void -band2I(UINT8 *out, const UINT8 *in, int pixels) { - int i; - /* band 2 only */ - for (i = 0; i < pixels; i++) { - out[2] = ~in[i]; - out += 4; - } -} - -static void -band3I(UINT8 *out, const UINT8 *in, int pixels) { - /* band 3 only */ - int i; - for (i = 0; i < pixels; i++) { - out[3] = ~in[i]; - out += 4; - } -} - -static void -band016B(UINT8 *out, const UINT8 *in, int pixels) { - int i; - /* band 0 only, big endian */ - for (i = 0; i < pixels; i++) { - out[0] = in[0]; - out += 4; - in += 2; - } -} - -static void -band116B(UINT8 *out, const UINT8 *in, int pixels) { - int i; - /* band 1 only, big endian */ - for (i = 0; i < pixels; i++) { - out[1] = in[0]; - out += 4; - in += 2; - } -} - -static void -band216B(UINT8 *out, const UINT8 *in, int pixels) { - int i; - /* band 2 only, big endian */ - for (i = 0; i < pixels; i++) { - out[2] = in[0]; - out += 4; - in += 2; - } -} - -static void -band316B(UINT8 *out, const UINT8 *in, int pixels) { - int i; - /* band 3 only, big endian */ - for (i = 0; i < pixels; i++) { - out[3] = in[0]; - out += 4; - in += 2; - } -} - -static void -band016L(UINT8 *out, const UINT8 *in, int pixels) { - int i; - /* band 0 only, little endian */ - for (i = 0; i < pixels; i++) { - out[0] = in[1]; - out += 4; - in += 2; - } -} - -static void -band116L(UINT8 *out, const UINT8 *in, int pixels) { - int i; - /* band 1 only, little endian */ - for (i = 0; i < pixels; i++) { - out[1] = in[1]; - out += 4; - in += 2; - } -} - -static void -band216L(UINT8 *out, const UINT8 *in, int pixels) { - int i; - /* band 2 only, little endian */ - for (i = 0; i < pixels; i++) { - out[2] = in[1]; - out += 4; - in += 2; - } -} - -static void -band316L(UINT8 *out, const UINT8 *in, int pixels) { - int i; - /* band 3 only, little endian */ - for (i = 0; i < pixels; i++) { - out[3] = in[1]; - out += 4; - in += 2; - } -} - -static struct { - const char *mode; - const char *rawmode; - int bits; - ImagingShuffler unpack; -} unpackers[] = { - - /* raw mode syntax is ";" where "bits" defaults - depending on mode (1 for "1", 8 for "P" and "L", etc), and - "flags" should be given in alphabetical order. if both bits - and flags have their default values, the ; should be left out */ - - /* flags: "I" inverted data; "R" reversed bit order; "B" big - endian byte order (default is little endian); "L" line - interleave, "S" signed, "F" floating point */ - - /* exception: rawmodes "I" and "F" are always native endian byte order */ - - /* bilevel */ - {"1", "1", 1, unpack1}, - {"1", "1;I", 1, unpack1I}, - {"1", "1;R", 1, unpack1R}, - {"1", "1;IR", 1, unpack1IR}, - {"1", "1;8", 8, unpack18}, - - /* grayscale */ - {"L", "L;2", 2, unpackL2}, - {"L", "L;2I", 2, unpackL2I}, - {"L", "L;2R", 2, unpackL2R}, - {"L", "L;2IR", 2, unpackL2IR}, - - {"L", "L;4", 4, unpackL4}, - {"L", "L;4I", 4, unpackL4I}, - {"L", "L;4R", 4, unpackL4R}, - {"L", "L;4IR", 4, unpackL4IR}, - - {"L", "L", 8, copy1}, - {"L", "L;I", 8, unpackLI}, - {"L", "L;R", 8, unpackLR}, - {"L", "L;16", 16, unpackL16}, - {"L", "L;16B", 16, unpackL16B}, - - /* grayscale w. alpha */ - {"LA", "LA", 16, unpackLA}, - {"LA", "LA;L", 16, unpackLAL}, - - /* grayscale w. alpha premultiplied */ - {"La", "La", 16, unpackLA}, - - /* palette */ - {"P", "P;1", 1, unpackP1}, - {"P", "P;2", 2, unpackP2}, - {"P", "P;2L", 2, unpackP2L}, - {"P", "P;4", 4, unpackP4}, - {"P", "P;4L", 4, unpackP4L}, - {"P", "P", 8, copy1}, - {"P", "P;R", 8, unpackLR}, - {"P", "L", 8, copy1}, - {"P", "PX", 16, unpackL16B}, - - /* palette w. alpha */ - {"PA", "PA", 16, unpackLA}, - {"PA", "PA;L", 16, unpackLAL}, - {"PA", "LA", 16, unpackLA}, - - /* true colour */ - {"RGB", "RGB", 24, ImagingUnpackRGB}, - {"RGB", "RGB;L", 24, unpackRGBL}, - {"RGB", "RGB;R", 24, unpackRGBR}, - {"RGB", "RGB;16L", 48, unpackRGB16L}, - {"RGB", "RGB;16B", 48, unpackRGB16B}, - {"RGB", "BGR", 24, ImagingUnpackBGR}, - {"RGB", "RGB;15", 16, ImagingUnpackRGB15}, - {"RGB", "BGR;15", 16, ImagingUnpackBGR15}, - {"RGB", "RGB;16", 16, ImagingUnpackRGB16}, - {"RGB", "BGR;16", 16, ImagingUnpackBGR16}, - {"RGB", "RGB;4B", 16, ImagingUnpackRGB4B}, - {"RGB", "BGR;5", 16, ImagingUnpackBGR15}, /* compat */ - {"RGB", "RGBX", 32, copy4}, - {"RGB", "RGBX;L", 32, unpackRGBAL}, - {"RGB", "RGBA;L", 32, unpackRGBAL}, - {"RGB", "RGBA;15", 16, ImagingUnpackRGBA15}, - {"RGB", "BGRX", 32, ImagingUnpackBGRX}, - {"RGB", "BGXR", 32, ImagingUnpackBGXR}, - {"RGB", "XRGB", 32, ImagingUnpackXRGB}, - {"RGB", "XBGR", 32, ImagingUnpackXBGR}, - {"RGB", "YCC;P", 24, ImagingUnpackYCC}, - {"RGB", "R", 8, band0}, - {"RGB", "G", 8, band1}, - {"RGB", "B", 8, band2}, - {"RGB", "R;16L", 16, band016L}, - {"RGB", "G;16L", 16, band116L}, - {"RGB", "B;16L", 16, band216L}, - {"RGB", "R;16B", 16, band016B}, - {"RGB", "G;16B", 16, band116B}, - {"RGB", "B;16B", 16, band216B}, - {"RGB", "CMYK", 32, cmyk2rgb}, - - {"BGR;15", "BGR;15", 16, copy2}, - {"BGR;16", "BGR;16", 16, copy2}, - {"BGR;24", "BGR;24", 24, copy3}, - - /* true colour w. alpha */ - {"RGBA", "LA", 16, unpackRGBALA}, - {"RGBA", "LA;16B", 32, unpackRGBALA16B}, - {"RGBA", "RGBA", 32, copy4}, - {"RGBA", "RGBAX", 40, copy4skip1}, - {"RGBA", "RGBAXX", 48, copy4skip2}, - {"RGBA", "RGBa", 32, unpackRGBa}, - {"RGBA", "RGBaX", 40, unpackRGBaskip1}, - {"RGBA", "RGBaXX", 48, unpackRGBaskip2}, - {"RGBA", "RGBa;16L", 64, unpackRGBa16L}, - {"RGBA", "RGBa;16B", 64, unpackRGBa16B}, - {"RGBA", "BGRa", 32, unpackBGRa}, - {"RGBA", "RGBA;I", 32, unpackRGBAI}, - {"RGBA", "RGBA;L", 32, unpackRGBAL}, - {"RGBA", "RGBA;15", 16, ImagingUnpackRGBA15}, - {"RGBA", "BGRA;15", 16, ImagingUnpackBGRA15}, - {"RGBA", "RGBA;4B", 16, ImagingUnpackRGBA4B}, - {"RGBA", "RGBA;16L", 64, unpackRGBA16L}, - {"RGBA", "RGBA;16B", 64, unpackRGBA16B}, - {"RGBA", "BGRA", 32, unpackBGRA}, - {"RGBA", "BGRA;16L", 64, unpackBGRA16L}, - {"RGBA", "BGRA;16B", 64, unpackBGRA16B}, - {"RGBA", "BGAR", 32, unpackBGAR}, - {"RGBA", "ARGB", 32, unpackARGB}, - {"RGBA", "ABGR", 32, unpackABGR}, - {"RGBA", "YCCA;P", 32, ImagingUnpackYCCA}, - {"RGBA", "R", 8, band0}, - {"RGBA", "G", 8, band1}, - {"RGBA", "B", 8, band2}, - {"RGBA", "A", 8, band3}, - {"RGBA", "R;16L", 16, band016L}, - {"RGBA", "G;16L", 16, band116L}, - {"RGBA", "B;16L", 16, band216L}, - {"RGBA", "A;16L", 16, band316L}, - {"RGBA", "R;16B", 16, band016B}, - {"RGBA", "G;16B", 16, band116B}, - {"RGBA", "B;16B", 16, band216B}, - {"RGBA", "A;16B", 16, band316B}, - -#ifdef WORDS_BIGENDIAN - {"RGB", "RGB;16N", 48, unpackRGB16B}, - {"RGBA", "RGBa;16N", 64, unpackRGBa16B}, - {"RGBA", "RGBA;16N", 64, unpackRGBA16B}, - {"RGBX", "RGBX;16N", 64, unpackRGBA16B}, - {"RGB", "R;16N", 16, band016B}, - {"RGB", "G;16N", 16, band116B}, - {"RGB", "B;16N", 16, band216B}, - - {"RGBA", "R;16N", 16, band016B}, - {"RGBA", "G;16N", 16, band116B}, - {"RGBA", "B;16N", 16, band216B}, - {"RGBA", "A;16N", 16, band316B}, -#else - {"RGB", "RGB;16N", 48, unpackRGB16L}, - {"RGBA", "RGBa;16N", 64, unpackRGBa16L}, - {"RGBA", "RGBA;16N", 64, unpackRGBA16L}, - {"RGBX", "RGBX;16N", 64, unpackRGBA16L}, - {"RGB", "R;16N", 16, band016L}, - {"RGB", "G;16N", 16, band116L}, - {"RGB", "B;16N", 16, band216L}, - - {"RGBA", "R;16N", 16, band016L}, - {"RGBA", "G;16N", 16, band116L}, - {"RGBA", "B;16N", 16, band216L}, - {"RGBA", "A;16N", 16, band316L}, -#endif - - /* true colour w. alpha premultiplied */ - {"RGBa", "RGBa", 32, copy4}, - {"RGBa", "BGRa", 32, unpackBGRA}, - {"RGBa", "aRGB", 32, unpackARGB}, - {"RGBa", "aBGR", 32, unpackABGR}, - - /* true colour w. padding */ - {"RGBX", "RGB", 24, ImagingUnpackRGB}, - {"RGBX", "RGB;L", 24, unpackRGBL}, - {"RGBX", "RGB;16B", 48, unpackRGB16B}, - {"RGBX", "BGR", 24, ImagingUnpackBGR}, - {"RGBX", "RGB;15", 16, ImagingUnpackRGB15}, - {"RGBX", "BGR;15", 16, ImagingUnpackBGR15}, - {"RGBX", "RGB;4B", 16, ImagingUnpackRGB4B}, - {"RGBX", "BGR;5", 16, ImagingUnpackBGR15}, /* compat */ - {"RGBX", "RGBX", 32, copy4}, - {"RGBX", "RGBXX", 40, copy4skip1}, - {"RGBX", "RGBXXX", 48, copy4skip2}, - {"RGBX", "RGBX;L", 32, unpackRGBAL}, - {"RGBX", "RGBX;16L", 64, unpackRGBA16L}, - {"RGBX", "RGBX;16B", 64, unpackRGBA16B}, - {"RGBX", "BGRX", 32, ImagingUnpackBGRX}, - {"RGBX", "XRGB", 32, ImagingUnpackXRGB}, - {"RGBX", "XBGR", 32, ImagingUnpackXBGR}, - {"RGBX", "YCC;P", 24, ImagingUnpackYCC}, - {"RGBX", "R", 8, band0}, - {"RGBX", "G", 8, band1}, - {"RGBX", "B", 8, band2}, - {"RGBX", "X", 8, band3}, - - /* colour separation */ - {"CMYK", "CMYK", 32, copy4}, - {"CMYK", "CMYKX", 40, copy4skip1}, - {"CMYK", "CMYKXX", 48, copy4skip2}, - {"CMYK", "CMYK;I", 32, unpackCMYKI}, - {"CMYK", "CMYK;L", 32, unpackRGBAL}, - {"CMYK", "CMYK;16L", 64, unpackRGBA16L}, - {"CMYK", "CMYK;16B", 64, unpackRGBA16B}, - {"CMYK", "C", 8, band0}, - {"CMYK", "M", 8, band1}, - {"CMYK", "Y", 8, band2}, - {"CMYK", "K", 8, band3}, - {"CMYK", "C;I", 8, band0I}, - {"CMYK", "M;I", 8, band1I}, - {"CMYK", "Y;I", 8, band2I}, - {"CMYK", "K;I", 8, band3I}, - -#ifdef WORDS_BIGENDIAN - {"CMYK", "CMYK;16N", 64, unpackRGBA16B}, -#else - {"CMYK", "CMYK;16N", 64, unpackRGBA16L}, -#endif - - /* video (YCbCr) */ - {"YCbCr", "YCbCr", 24, ImagingUnpackRGB}, - {"YCbCr", "YCbCr;L", 24, unpackRGBL}, - {"YCbCr", "YCbCrX", 32, copy4}, - {"YCbCr", "YCbCrK", 32, copy4}, - - /* LAB Color */ - {"LAB", "LAB", 24, ImagingUnpackLAB}, - {"LAB", "L", 8, band0}, - {"LAB", "A", 8, band1}, - {"LAB", "B", 8, band2}, - - /* HSV Color */ - {"HSV", "HSV", 24, ImagingUnpackRGB}, - {"HSV", "H", 8, band0}, - {"HSV", "S", 8, band1}, - {"HSV", "V", 8, band2}, - - /* integer variations */ - {"I", "I", 32, copy4}, - {"I", "I;8", 8, unpackI8}, - {"I", "I;8S", 8, unpackI8S}, - {"I", "I;16", 16, unpackI16}, - {"I", "I;16S", 16, unpackI16S}, - {"I", "I;16B", 16, unpackI16B}, - {"I", "I;16BS", 16, unpackI16BS}, - {"I", "I;16N", 16, unpackI16N}, - {"I", "I;16NS", 16, unpackI16NS}, - {"I", "I;32", 32, unpackI32}, - {"I", "I;32S", 32, unpackI32S}, - {"I", "I;32B", 32, unpackI32B}, - {"I", "I;32BS", 32, unpackI32BS}, - {"I", "I;32N", 32, unpackI32N}, - {"I", "I;32NS", 32, unpackI32NS}, - - /* floating point variations */ - {"F", "F", 32, copy4}, - {"F", "F;8", 8, unpackF8}, - {"F", "F;8S", 8, unpackF8S}, - {"F", "F;16", 16, unpackF16}, - {"F", "F;16S", 16, unpackF16S}, - {"F", "F;16B", 16, unpackF16B}, - {"F", "F;16BS", 16, unpackF16BS}, - {"F", "F;16N", 16, unpackF16N}, - {"F", "F;16NS", 16, unpackF16NS}, - {"F", "F;32", 32, unpackF32}, - {"F", "F;32S", 32, unpackF32S}, - {"F", "F;32B", 32, unpackF32B}, - {"F", "F;32BS", 32, unpackF32BS}, - {"F", "F;32N", 32, unpackF32N}, - {"F", "F;32NS", 32, unpackF32NS}, - {"F", "F;32F", 32, unpackF32F}, - {"F", "F;32BF", 32, unpackF32BF}, - {"F", "F;32NF", 32, unpackF32NF}, -#ifdef FLOAT64 - {"F", "F;64F", 64, unpackF64F}, - {"F", "F;64BF", 64, unpackF64BF}, - {"F", "F;64NF", 64, unpackF64NF}, -#endif - - /* storage modes */ - {"I;16", "I;16", 16, copy2}, - {"I;16B", "I;16B", 16, copy2}, - {"I;16L", "I;16L", 16, copy2}, - {"I;16N", "I;16N", 16, copy2}, - - {"I;16", "I;16B", 16, unpackI16B_I16}, - {"I;16", "I;16N", 16, unpackI16N_I16}, // LibTiff native->image endian. - {"I;16L", "I;16N", 16, unpackI16N_I16}, // LibTiff native->image endian. - {"I;16B", "I;16N", 16, unpackI16N_I16B}, - - {"I;16", "I;16R", 16, unpackI16R_I16}, - - {"I;16", "I;12", 12, unpackI12_I16}, // 12 bit Tiffs stored in 16bits. - - {NULL} /* sentinel */ -}; - -ImagingShuffler -ImagingFindUnpacker(const char *mode, const char *rawmode, int *bits_out) { - int i; - - /* find a suitable pixel unpacker */ - for (i = 0; unpackers[i].rawmode; i++) { - if (strcmp(unpackers[i].mode, mode) == 0 && - strcmp(unpackers[i].rawmode, rawmode) == 0) { - if (bits_out) { - *bits_out = unpackers[i].bits; - } - return unpackers[i].unpack; - } - } - - /* FIXME: configure a general unpacker based on the type codes... */ - - return NULL; -} +/* + * The Python Imaging Library. + * $Id$ + * + * code to unpack raw data from various file formats + * + * history: + * 1996-03-07 fl Created (from various decoders) + * 1996-04-19 fl Added band unpackers + * 1996-05-12 fl Published RGB unpackers + * 1996-05-27 fl Added nibble unpacker + * 1996-12-10 fl Added complete set of PNG unpackers + * 1996-12-29 fl Set alpha byte in RGB unpackers + * 1997-01-05 fl Added remaining TGA unpackers + * 1997-01-18 fl Added inverting band unpackers + * 1997-01-25 fl Added FlashPix unpackers + * 1997-05-31 fl Added floating point unpackers + * 1998-02-08 fl Added I unpacker + * 1998-07-01 fl Added YCbCr unpacker + * 1998-07-02 fl Added full set of integer unpackers + * 1998-12-29 fl Added mode field, I;16 unpackers + * 1998-12-30 fl Added RGBX modes + * 1999-02-04 fl Fixed I;16 unpackers + * 2003-05-13 fl Added L/RGB reversed unpackers + * 2003-09-26 fl Added LA/PA and RGBa->RGB unpackers + * + * Copyright (c) 1997-2003 by Secret Labs AB. + * Copyright (c) 1996-1997 by Fredrik Lundh. + * + * See the README file for information on usage and redistribution. + */ + +#include "Imaging.h" +#include "Convert.h" + +#define R 0 +#define G 1 +#define B 2 +#define X 3 + +#define A 3 + +#define C 0 +#define M 1 +#define Y 2 +#define K 3 + +/* byte-swapping macros */ + +#define C16N (tmp[0] = in[0], tmp[1] = in[1]); +#define C16S (tmp[1] = in[0], tmp[0] = in[1]); +#define C32N (tmp[0] = in[0], tmp[1] = in[1], tmp[2] = in[2], tmp[3] = in[3]); +#define C32S (tmp[3] = in[0], tmp[2] = in[1], tmp[1] = in[2], tmp[0] = in[3]); +#define C64N \ + (tmp[0] = in[0], \ + tmp[1] = in[1], \ + tmp[2] = in[2], \ + tmp[3] = in[3], \ + tmp[4] = in[4], \ + tmp[5] = in[5], \ + tmp[6] = in[6], \ + tmp[7] = in[7]); +#define C64S \ + (tmp[7] = in[0], \ + tmp[6] = in[1], \ + tmp[5] = in[2], \ + tmp[4] = in[3], \ + tmp[3] = in[4], \ + tmp[2] = in[5], \ + tmp[1] = in[6], \ + tmp[0] = in[7]); + +#ifdef WORDS_BIGENDIAN +#define C16B C16N +#define C16L C16S +#define C32B C32N +#define C32L C32S +#define C64B C64N +#define C64L C64S +#else +#define C16B C16S +#define C16L C16N +#define C32B C32S +#define C32L C32N +#define C64B C64S +#define C64L C64N +#endif + +/* bit-swapping */ + +static UINT8 BITFLIP[] = { + 0, 128, 64, 192, 32, 160, 96, 224, 16, 144, 80, 208, 48, 176, 112, 240, + 8, 136, 72, 200, 40, 168, 104, 232, 24, 152, 88, 216, 56, 184, 120, 248, + 4, 132, 68, 196, 36, 164, 100, 228, 20, 148, 84, 212, 52, 180, 116, 244, + 12, 140, 76, 204, 44, 172, 108, 236, 28, 156, 92, 220, 60, 188, 124, 252, + 2, 130, 66, 194, 34, 162, 98, 226, 18, 146, 82, 210, 50, 178, 114, 242, + 10, 138, 74, 202, 42, 170, 106, 234, 26, 154, 90, 218, 58, 186, 122, 250, + 6, 134, 70, 198, 38, 166, 102, 230, 22, 150, 86, 214, 54, 182, 118, 246, + 14, 142, 78, 206, 46, 174, 110, 238, 30, 158, 94, 222, 62, 190, 126, 254, + 1, 129, 65, 193, 33, 161, 97, 225, 17, 145, 81, 209, 49, 177, 113, 241, + 9, 137, 73, 201, 41, 169, 105, 233, 25, 153, 89, 217, 57, 185, 121, 249, + 5, 133, 69, 197, 37, 165, 101, 229, 21, 149, 85, 213, 53, 181, 117, 245, + 13, 141, 77, 205, 45, 173, 109, 237, 29, 157, 93, 221, 61, 189, 125, 253, + 3, 131, 67, 195, 35, 163, 99, 227, 19, 147, 83, 211, 51, 179, 115, 243, + 11, 139, 75, 203, 43, 171, 107, 235, 27, 155, 91, 219, 59, 187, 123, 251, + 7, 135, 71, 199, 39, 167, 103, 231, 23, 151, 87, 215, 55, 183, 119, 247, + 15, 143, 79, 207, 47, 175, 111, 239, 31, 159, 95, 223, 63, 191, 127, 255}; + +/* Unpack to "1" image */ + +static void +unpack1(UINT8 *out, const UINT8 *in, int pixels) { + /* bits (msb first, white is non-zero) */ + while (pixels > 0) { + UINT8 byte = *in++; + switch (pixels) { + default: + *out++ = (byte & 128) ? 255 : 0; + byte <<= 1; + case 7: + *out++ = (byte & 128) ? 255 : 0; + byte <<= 1; + case 6: + *out++ = (byte & 128) ? 255 : 0; + byte <<= 1; + case 5: + *out++ = (byte & 128) ? 255 : 0; + byte <<= 1; + case 4: + *out++ = (byte & 128) ? 255 : 0; + byte <<= 1; + case 3: + *out++ = (byte & 128) ? 255 : 0; + byte <<= 1; + case 2: + *out++ = (byte & 128) ? 255 : 0; + byte <<= 1; + case 1: + *out++ = (byte & 128) ? 255 : 0; + } + pixels -= 8; + } +} + +static void +unpack1I(UINT8 *out, const UINT8 *in, int pixels) { + /* bits (msb first, white is zero) */ + while (pixels > 0) { + UINT8 byte = *in++; + switch (pixels) { + default: + *out++ = (byte & 128) ? 0 : 255; + byte <<= 1; + case 7: + *out++ = (byte & 128) ? 0 : 255; + byte <<= 1; + case 6: + *out++ = (byte & 128) ? 0 : 255; + byte <<= 1; + case 5: + *out++ = (byte & 128) ? 0 : 255; + byte <<= 1; + case 4: + *out++ = (byte & 128) ? 0 : 255; + byte <<= 1; + case 3: + *out++ = (byte & 128) ? 0 : 255; + byte <<= 1; + case 2: + *out++ = (byte & 128) ? 0 : 255; + byte <<= 1; + case 1: + *out++ = (byte & 128) ? 0 : 255; + } + pixels -= 8; + } +} + +static void +unpack1R(UINT8 *out, const UINT8 *in, int pixels) { + /* bits (lsb first, white is non-zero) */ + while (pixels > 0) { + UINT8 byte = *in++; + switch (pixels) { + default: + *out++ = (byte & 1) ? 255 : 0; + byte >>= 1; + case 7: + *out++ = (byte & 1) ? 255 : 0; + byte >>= 1; + case 6: + *out++ = (byte & 1) ? 255 : 0; + byte >>= 1; + case 5: + *out++ = (byte & 1) ? 255 : 0; + byte >>= 1; + case 4: + *out++ = (byte & 1) ? 255 : 0; + byte >>= 1; + case 3: + *out++ = (byte & 1) ? 255 : 0; + byte >>= 1; + case 2: + *out++ = (byte & 1) ? 255 : 0; + byte >>= 1; + case 1: + *out++ = (byte & 1) ? 255 : 0; + } + pixels -= 8; + } +} + +static void +unpack1IR(UINT8 *out, const UINT8 *in, int pixels) { + /* bits (lsb first, white is zero) */ + while (pixels > 0) { + UINT8 byte = *in++; + switch (pixels) { + default: + *out++ = (byte & 1) ? 0 : 255; + byte >>= 1; + case 7: + *out++ = (byte & 1) ? 0 : 255; + byte >>= 1; + case 6: + *out++ = (byte & 1) ? 0 : 255; + byte >>= 1; + case 5: + *out++ = (byte & 1) ? 0 : 255; + byte >>= 1; + case 4: + *out++ = (byte & 1) ? 0 : 255; + byte >>= 1; + case 3: + *out++ = (byte & 1) ? 0 : 255; + byte >>= 1; + case 2: + *out++ = (byte & 1) ? 0 : 255; + byte >>= 1; + case 1: + *out++ = (byte & 1) ? 0 : 255; + } + pixels -= 8; + } +} + +static void +unpack18(UINT8 *out, const UINT8 *in, int pixels) { + /* Unpack a '|b1' image, which is a numpy boolean. + 1 == true, 0==false, in bytes */ + + int i; + for (i = 0; i < pixels; i++) { + out[i] = in[i] > 0 ? 255 : 0; + } +} + +/* Unpack to "L" image */ + +static void +unpackL2(UINT8 *out, const UINT8 *in, int pixels) { + /* nibbles (msb first, white is non-zero) */ + while (pixels > 0) { + UINT8 byte = *in++; + switch (pixels) { + default: + *out++ = ((byte >> 6) & 0x03U) * 0x55U; + byte <<= 2; + case 3: + *out++ = ((byte >> 6) & 0x03U) * 0x55U; + byte <<= 2; + case 2: + *out++ = ((byte >> 6) & 0x03U) * 0x55U; + byte <<= 2; + case 1: + *out++ = ((byte >> 6) & 0x03U) * 0x55U; + } + pixels -= 4; + } +} + +static void +unpackL2I(UINT8 *out, const UINT8 *in, int pixels) { + /* nibbles (msb first, white is zero) */ + while (pixels > 0) { + UINT8 byte = *in++; + switch (pixels) { + default: + *out++ = 0xFFU - (UINT8)(((byte >> 6) & 0x03U) * 0x55U); + byte <<= 2; + case 3: + *out++ = 0xFFU - (UINT8)(((byte >> 6) & 0x03U) * 0x55U); + byte <<= 2; + case 2: + *out++ = 0xFFU - (UINT8)(((byte >> 6) & 0x03U) * 0x55U); + byte <<= 2; + case 1: + *out++ = 0xFFU - (UINT8)(((byte >> 6) & 0x03U) * 0x55U); + } + pixels -= 4; + } +} + +static void +unpackL2R(UINT8 *out, const UINT8 *in, int pixels) { + /* nibbles (bit order reversed, white is non-zero) */ + while (pixels > 0) { + UINT8 byte = *in++; + byte = BITFLIP[byte]; + switch (pixels) { + default: + *out++ = ((byte >> 6) & 0x03U) * 0x55U; + byte <<= 2; + case 3: + *out++ = ((byte >> 6) & 0x03U) * 0x55U; + byte <<= 2; + case 2: + *out++ = ((byte >> 6) & 0x03U) * 0x55U; + byte <<= 2; + case 1: + *out++ = ((byte >> 6) & 0x03U) * 0x55U; + } + pixels -= 4; + } +} + +static void +unpackL2IR(UINT8 *out, const UINT8 *in, int pixels) { + /* nibbles (bit order reversed, white is zero) */ + while (pixels > 0) { + UINT8 byte = *in++; + byte = BITFLIP[byte]; + switch (pixels) { + default: + *out++ = 0xFFU - (UINT8)(((byte >> 6) & 0x03U) * 0x55U); + byte <<= 2; + case 3: + *out++ = 0xFFU - (UINT8)(((byte >> 6) & 0x03U) * 0x55U); + byte <<= 2; + case 2: + *out++ = 0xFFU - (UINT8)(((byte >> 6) & 0x03U) * 0x55U); + byte <<= 2; + case 1: + *out++ = 0xFFU - (UINT8)(((byte >> 6) & 0x03U) * 0x55U); + } + pixels -= 4; + } +} + +static void +unpackL4(UINT8 *out, const UINT8 *in, int pixels) { + /* nibbles (msb first, white is non-zero) */ + while (pixels > 0) { + UINT8 byte = *in++; + switch (pixels) { + default: + *out++ = ((byte >> 4) & 0x0FU) * 0x11U; + byte <<= 4; + case 1: + *out++ = ((byte >> 4) & 0x0FU) * 0x11U; + } + pixels -= 2; + } +} + +static void +unpackL4I(UINT8 *out, const UINT8 *in, int pixels) { + /* nibbles (msb first, white is zero) */ + while (pixels > 0) { + UINT8 byte = *in++; + switch (pixels) { + default: + *out++ = 0xFFU - (UINT8)(((byte >> 4) & 0x0FU) * 0x11U); + byte <<= 4; + case 1: + *out++ = 0xFFU - (UINT8)(((byte >> 4) & 0x0FU) * 0x11U); + } + pixels -= 2; + } +} + +static void +unpackL4R(UINT8 *out, const UINT8 *in, int pixels) { + /* nibbles (bit order reversed, white is non-zero) */ + while (pixels > 0) { + UINT8 byte = *in++; + byte = BITFLIP[byte]; + switch (pixels) { + default: + *out++ = ((byte >> 4) & 0x0FU) * 0x11U; + byte <<= 4; + case 1: + *out++ = ((byte >> 4) & 0x0FU) * 0x11U; + } + pixels -= 2; + } +} + +static void +unpackL4IR(UINT8 *out, const UINT8 *in, int pixels) { + /* nibbles (bit order reversed, white is zero) */ + while (pixels > 0) { + UINT8 byte = *in++; + byte = BITFLIP[byte]; + switch (pixels) { + default: + *out++ = 0xFFU - (UINT8)(((byte >> 4) & 0x0FU) * 0x11U); + byte <<= 4; + case 1: + *out++ = 0xFFU - (UINT8)(((byte >> 4) & 0x0FU) * 0x11U); + } + pixels -= 2; + } +} + +static void +unpackLA(UINT8 *_out, const UINT8 *in, int pixels) { + int i; + /* LA, pixel interleaved */ + for (i = 0; i < pixels; i++) { + UINT32 iv = MAKE_UINT32(in[0], in[0], in[0], in[1]); + memcpy(_out, &iv, sizeof(iv)); + in += 2; + _out += 4; + } +} + +static void +unpackLAL(UINT8 *_out, const UINT8 *in, int pixels) { + int i; + /* LA, line interleaved */ + for (i = 0; i < pixels; i++, _out += 4) { + UINT32 iv = MAKE_UINT32(in[i], in[i], in[i], in[i + pixels]); + memcpy(_out, &iv, sizeof(iv)); + } +} + +static void +unpackLI(UINT8 *out, const UINT8 *in, int pixels) { + /* negative */ + int i; + for (i = 0; i < pixels; i++) { + out[i] = ~in[i]; + } +} + +static void +unpackLR(UINT8 *out, const UINT8 *in, int pixels) { + int i; + /* RGB, bit reversed */ + for (i = 0; i < pixels; i++) { + out[i] = BITFLIP[in[i]]; + } +} + +static void +unpackL16(UINT8 *out, const UINT8 *in, int pixels) { + /* int16 (upper byte, little endian) */ + int i; + for (i = 0; i < pixels; i++) { + out[i] = in[1]; + in += 2; + } +} + +static void +unpackL16B(UINT8 *out, const UINT8 *in, int pixels) { + int i; + /* int16 (upper byte, big endian) */ + for (i = 0; i < pixels; i++) { + out[i] = in[0]; + in += 2; + } +} + +/* Unpack to "P" image */ + +static void +unpackP1(UINT8 *out, const UINT8 *in, int pixels) { + /* bits */ + while (pixels > 0) { + UINT8 byte = *in++; + switch (pixels) { + default: + *out++ = (byte >> 7) & 1; + byte <<= 1; + case 7: + *out++ = (byte >> 7) & 1; + byte <<= 1; + case 6: + *out++ = (byte >> 7) & 1; + byte <<= 1; + case 5: + *out++ = (byte >> 7) & 1; + byte <<= 1; + case 4: + *out++ = (byte >> 7) & 1; + byte <<= 1; + case 3: + *out++ = (byte >> 7) & 1; + byte <<= 1; + case 2: + *out++ = (byte >> 7) & 1; + byte <<= 1; + case 1: + *out++ = (byte >> 7) & 1; + } + pixels -= 8; + } +} + +static void +unpackP2(UINT8 *out, const UINT8 *in, int pixels) { + /* bit pairs */ + while (pixels > 0) { + UINT8 byte = *in++; + switch (pixels) { + default: + *out++ = (byte >> 6) & 3; + byte <<= 2; + case 3: + *out++ = (byte >> 6) & 3; + byte <<= 2; + case 2: + *out++ = (byte >> 6) & 3; + byte <<= 2; + case 1: + *out++ = (byte >> 6) & 3; + } + pixels -= 4; + } +} + +static void +unpackP4(UINT8 *out, const UINT8 *in, int pixels) { + /* nibbles */ + while (pixels > 0) { + UINT8 byte = *in++; + switch (pixels) { + default: + *out++ = (byte >> 4) & 15; + byte <<= 4; + case 1: + *out++ = (byte >> 4) & 15; + } + pixels -= 2; + } +} + +static void +unpackP2L(UINT8 *out, const UINT8 *in, int pixels) { + int i, j, m, s; + /* bit layers */ + m = 128; + s = (pixels + 7) / 8; + for (i = j = 0; i < pixels; i++) { + out[i] = ((in[j] & m) ? 1 : 0) + ((in[j + s] & m) ? 2 : 0); + if ((m >>= 1) == 0) { + m = 128; + j++; + } + } +} + +static void +unpackP4L(UINT8 *out, const UINT8 *in, int pixels) { + int i, j, m, s; + /* bit layers (trust the optimizer ;-) */ + m = 128; + s = (pixels + 7) / 8; + for (i = j = 0; i < pixels; i++) { + out[i] = ((in[j] & m) ? 1 : 0) + ((in[j + s] & m) ? 2 : 0) + + ((in[j + 2 * s] & m) ? 4 : 0) + ((in[j + 3 * s] & m) ? 8 : 0); + if ((m >>= 1) == 0) { + m = 128; + j++; + } + } +} + +/* Unpack to "RGB" image */ + +void +ImagingUnpackRGB(UINT8 *_out, const UINT8 *in, int pixels) { + int i = 0; + /* RGB triplets */ + for (; i < pixels - 1; i++) { + UINT32 iv; + memcpy(&iv, in, sizeof(iv)); + iv |= MASK_UINT32_CHANNEL_3; + memcpy(_out, &iv, sizeof(iv)); + in += 3; + _out += 4; + } + for (; i < pixels; i++) { + UINT32 iv = MAKE_UINT32(in[0], in[1], in[2], 255); + memcpy(_out, &iv, sizeof(iv)); + in += 3; + _out += 4; + } +} + +void +unpackRGB16L(UINT8 *_out, const UINT8 *in, int pixels) { + int i; + /* 16-bit RGB triplets, little-endian order */ + for (i = 0; i < pixels; i++) { + UINT32 iv = MAKE_UINT32(in[1], in[3], in[5], 255); + memcpy(_out, &iv, sizeof(iv)); + in += 6; + _out += 4; + } +} + +void +unpackRGB16B(UINT8 *_out, const UINT8 *in, int pixels) { + int i; + /* 16-bit RGB triplets, big-endian order */ + for (i = 0; i < pixels; i++) { + UINT32 iv = MAKE_UINT32(in[0], in[2], in[4], 255); + memcpy(_out, &iv, sizeof(iv)); + in += 6; + _out += 4; + } +} + +static void +unpackRGBL(UINT8 *_out, const UINT8 *in, int pixels) { + int i; + /* RGB, line interleaved */ + for (i = 0; i < pixels; i++, _out += 4) { + UINT32 iv = MAKE_UINT32(in[i], in[i + pixels], in[i + pixels + pixels], 255); + memcpy(_out, &iv, sizeof(iv)); + } +} + +static void +unpackRGBR(UINT8 *_out, const UINT8 *in, int pixels) { + int i; + /* RGB, bit reversed */ + for (i = 0; i < pixels; i++) { + UINT32 iv = MAKE_UINT32(BITFLIP[in[0]], BITFLIP[in[1]], BITFLIP[in[2]], 255); + memcpy(_out, &iv, sizeof(iv)); + in += 3; + _out += 4; + } +} + +void +ImagingUnpackBGR(UINT8 *_out, const UINT8 *in, int pixels) { + int i; + /* RGB, reversed bytes */ + for (i = 0; i < pixels; i++) { + UINT32 iv = MAKE_UINT32(in[2], in[1], in[0], 255); + memcpy(_out, &iv, sizeof(iv)); + in += 3; + _out += 4; + } +} + +void +ImagingUnpackRGB15(UINT8 *out, const UINT8 *in, int pixels) { + int i, pixel; + /* RGB, 5 bits per pixel */ + for (i = 0; i < pixels; i++) { + pixel = in[0] + (in[1] << 8); + out[R] = (pixel & 31) * 255 / 31; + out[G] = ((pixel >> 5) & 31) * 255 / 31; + out[B] = ((pixel >> 10) & 31) * 255 / 31; + out[A] = 255; + out += 4; + in += 2; + } +} + +void +ImagingUnpackRGBA15(UINT8 *out, const UINT8 *in, int pixels) { + int i, pixel; + /* RGB, 5/5/5/1 bits per pixel */ + for (i = 0; i < pixels; i++) { + pixel = in[0] + (in[1] << 8); + out[R] = (pixel & 31) * 255 / 31; + out[G] = ((pixel >> 5) & 31) * 255 / 31; + out[B] = ((pixel >> 10) & 31) * 255 / 31; + out[A] = (pixel >> 15) * 255; + out += 4; + in += 2; + } +} + +void +ImagingUnpackBGR15(UINT8 *out, const UINT8 *in, int pixels) { + int i, pixel; + /* RGB, reversed bytes, 5 bits per pixel */ + for (i = 0; i < pixels; i++) { + pixel = in[0] + (in[1] << 8); + out[B] = (pixel & 31) * 255 / 31; + out[G] = ((pixel >> 5) & 31) * 255 / 31; + out[R] = ((pixel >> 10) & 31) * 255 / 31; + out[A] = 255; + out += 4; + in += 2; + } +} + +void +ImagingUnpackBGRA15(UINT8 *out, const UINT8 *in, int pixels) { + int i, pixel; + /* RGB, rearranged channels, 5/5/5/1 bits per pixel */ + for (i = 0; i < pixels; i++) { + pixel = in[0] + (in[1] << 8); + out[B] = (pixel & 31) * 255 / 31; + out[G] = ((pixel >> 5) & 31) * 255 / 31; + out[R] = ((pixel >> 10) & 31) * 255 / 31; + out[A] = (pixel >> 15) * 255; + out += 4; + in += 2; + } +} + +void +ImagingUnpackRGB16(UINT8 *out, const UINT8 *in, int pixels) { + int i, pixel; + /* RGB, 5/6/5 bits per pixel */ + for (i = 0; i < pixels; i++) { + pixel = in[0] + (in[1] << 8); + out[R] = (pixel & 31) * 255 / 31; + out[G] = ((pixel >> 5) & 63) * 255 / 63; + out[B] = ((pixel >> 11) & 31) * 255 / 31; + out[A] = 255; + out += 4; + in += 2; + } +} + +void +ImagingUnpackBGR16(UINT8 *out, const UINT8 *in, int pixels) { + int i, pixel; + /* RGB, reversed bytes, 5/6/5 bits per pixel */ + for (i = 0; i < pixels; i++) { + pixel = in[0] + (in[1] << 8); + out[B] = (pixel & 31) * 255 / 31; + out[G] = ((pixel >> 5) & 63) * 255 / 63; + out[R] = ((pixel >> 11) & 31) * 255 / 31; + out[A] = 255; + out += 4; + in += 2; + } +} + +void +ImagingUnpackRGB4B(UINT8 *out, const UINT8 *in, int pixels) { + int i, pixel; + /* RGB, 4 bits per pixel */ + for (i = 0; i < pixels; i++) { + pixel = in[0] + (in[1] << 8); + out[R] = (pixel & 15) * 17; + out[G] = ((pixel >> 4) & 15) * 17; + out[B] = ((pixel >> 8) & 15) * 17; + out[A] = 255; + out += 4; + in += 2; + } +} + +void +ImagingUnpackRGBA4B(UINT8 *out, const UINT8 *in, int pixels) { + int i, pixel; + /* RGBA, 4 bits per pixel */ + for (i = 0; i < pixels; i++) { + pixel = in[0] + (in[1] << 8); + out[R] = (pixel & 15) * 17; + out[G] = ((pixel >> 4) & 15) * 17; + out[B] = ((pixel >> 8) & 15) * 17; + out[A] = ((pixel >> 12) & 15) * 17; + out += 4; + in += 2; + } +} + +static void +ImagingUnpackBGRX(UINT8 *_out, const UINT8 *in, int pixels) { + int i; + /* RGB, reversed bytes with padding */ + for (i = 0; i < pixels; i++) { + UINT32 iv = MAKE_UINT32(in[2], in[1], in[0], 255); + memcpy(_out, &iv, sizeof(iv)); + in += 4; + _out += 4; + } +} + +static void +ImagingUnpackBGXR(UINT8 *_out, const UINT8 *in, int pixels) { + int i; + for (i = 0; i < pixels; i++) { + UINT32 iv = MAKE_UINT32(in[3], in[1], in[0], 255); + memcpy(_out, &iv, sizeof(iv)); + in += 4; + _out += 4; + } +} + +static void +ImagingUnpackXRGB(UINT8 *_out, const UINT8 *in, int pixels) { + int i; + /* RGB, leading pad */ + for (i = 0; i < pixels; i++) { + UINT32 iv = MAKE_UINT32(in[1], in[2], in[3], 255); + memcpy(_out, &iv, sizeof(iv)); + in += 4; + _out += 4; + } +} + +static void +ImagingUnpackXBGR(UINT8 *_out, const UINT8 *in, int pixels) { + int i; + /* RGB, reversed bytes, leading pad */ + for (i = 0; i < pixels; i++) { + UINT32 iv = MAKE_UINT32(in[3], in[2], in[1], 255); + memcpy(_out, &iv, sizeof(iv)); + in += 4; + _out += 4; + } +} + +/* Unpack to "RGBA" image */ + +static void +unpackRGBALA(UINT8 *_out, const UINT8 *in, int pixels) { + int i; + /* grayscale with alpha */ + for (i = 0; i < pixels; i++) { + UINT32 iv = MAKE_UINT32(in[0], in[0], in[0], in[1]); + memcpy(_out, &iv, sizeof(iv)); + in += 2; + _out += 4; + } +} + +static void +unpackRGBALA16B(UINT8 *_out, const UINT8 *in, int pixels) { + int i; + /* 16-bit grayscale with alpha, big-endian */ + for (i = 0; i < pixels; i++) { + UINT32 iv = MAKE_UINT32(in[0], in[0], in[0], in[2]); + memcpy(_out, &iv, sizeof(iv)); + in += 4; + _out += 4; + } +} + +static void +unpackRGBa16L(UINT8 *_out, const UINT8 *in, int pixels) { + int i; + /* premultiplied 16-bit RGBA, little-endian */ + for (i = 0; i < pixels; i++) { + int a = in[7]; + UINT32 iv; + if (!a) { + iv = 0; + } else if (a == 255) { + iv = MAKE_UINT32(in[1], in[3], in[5], a); + } else { + iv = MAKE_UINT32( + CLIP8(in[1] * 255 / a), + CLIP8(in[3] * 255 / a), + CLIP8(in[5] * 255 / a), + a); + } + memcpy(_out, &iv, sizeof(iv)); + in += 8; + _out += 4; + } +} + +static void +unpackRGBa16B(UINT8 *_out, const UINT8 *in, int pixels) { + int i; + /* premultiplied 16-bit RGBA, big-endian */ + for (i = 0; i < pixels; i++) { + int a = in[6]; + UINT32 iv; + if (!a) { + iv = 0; + } else if (a == 255) { + iv = MAKE_UINT32(in[0], in[2], in[4], a); + } else { + iv = MAKE_UINT32( + CLIP8(in[0] * 255 / a), + CLIP8(in[2] * 255 / a), + CLIP8(in[4] * 255 / a), + a); + } + memcpy(_out, &iv, sizeof(iv)); + in += 8; + _out += 4; + } +} + +static void +unpackRGBa(UINT8 *_out, const UINT8 *in, int pixels) { + int i; + /* premultiplied RGBA */ + for (i = 0; i < pixels; i++) { + int a = in[3]; + UINT32 iv; + if (!a) { + iv = 0; + } else if (a == 255) { + iv = MAKE_UINT32(in[0], in[1], in[2], a); + } else { + iv = MAKE_UINT32( + CLIP8(in[0] * 255 / a), + CLIP8(in[1] * 255 / a), + CLIP8(in[2] * 255 / a), + a); + } + memcpy(_out, &iv, sizeof(iv)); + in += 4; + _out += 4; + } +} + +static void +unpackRGBaskip1(UINT8 *_out, const UINT8 *in, int pixels) { + int i; + UINT32 *out = (UINT32 *)_out; + /* premultiplied RGBA */ + for (i = 0; i < pixels; i++) { + int a = in[3]; + if (!a) { + out[i] = 0; + } else if (a == 255) { + out[i] = MAKE_UINT32(in[0], in[1], in[2], a); + } else { + out[i] = MAKE_UINT32( + CLIP8(in[0] * 255 / a), + CLIP8(in[1] * 255 / a), + CLIP8(in[2] * 255 / a), + a); + } + in += 5; + } +} + +static void +unpackRGBaskip2(UINT8 *_out, const UINT8 *in, int pixels) { + int i; + UINT32 *out = (UINT32 *)_out; + /* premultiplied RGBA */ + for (i = 0; i < pixels; i++) { + int a = in[3]; + if (!a) { + out[i] = 0; + } else if (a == 255) { + out[i] = MAKE_UINT32(in[0], in[1], in[2], a); + } else { + out[i] = MAKE_UINT32( + CLIP8(in[0] * 255 / a), + CLIP8(in[1] * 255 / a), + CLIP8(in[2] * 255 / a), + a); + } + in += 6; + } +} + +static void +unpackBGRa(UINT8 *_out, const UINT8 *in, int pixels) { + int i; + /* premultiplied BGRA */ + for (i = 0; i < pixels; i++) { + int a = in[3]; + UINT32 iv; + if (!a) { + iv = 0; + } else if (a == 255) { + iv = MAKE_UINT32(in[2], in[1], in[0], a); + } else { + iv = MAKE_UINT32( + CLIP8(in[2] * 255 / a), + CLIP8(in[1] * 255 / a), + CLIP8(in[0] * 255 / a), + a); + } + memcpy(_out, &iv, sizeof(iv)); + in += 4; + _out += 4; + } +} + +static void +unpackRGBAI(UINT8 *out, const UINT8 *in, int pixels) { + int i; + /* RGBA, inverted RGB bytes (FlashPix) */ + for (i = 0; i < pixels; i++) { + out[R] = ~in[0]; + out[G] = ~in[1]; + out[B] = ~in[2]; + out[A] = in[3]; + out += 4; + in += 4; + } +} + +static void +unpackRGBAL(UINT8 *_out, const UINT8 *in, int pixels) { + int i; + /* RGBA, line interleaved */ + for (i = 0; i < pixels; i++, _out += 4) { + UINT32 iv = MAKE_UINT32( + in[i], + in[i + pixels], + in[i + pixels + pixels], + in[i + pixels + pixels + pixels]); + memcpy(_out, &iv, sizeof(iv)); + } +} + +void +unpackRGBA16L(UINT8 *_out, const UINT8 *in, int pixels) { + int i; + /* 16-bit RGBA, little-endian order */ + for (i = 0; i < pixels; i++, _out += 4) { + UINT32 iv = MAKE_UINT32(in[1], in[3], in[5], in[7]); + memcpy(_out, &iv, sizeof(iv)); + in += 8; + } +} + +void +unpackRGBA16B(UINT8 *_out, const UINT8 *in, int pixels) { + int i; + /* 16-bit RGBA, big-endian order */ + for (i = 0; i < pixels; i++, _out += 4) { + UINT32 iv = MAKE_UINT32(in[0], in[2], in[4], in[6]); + memcpy(_out, &iv, sizeof(iv)); + in += 8; + } +} + +static void +unpackARGB(UINT8 *_out, const UINT8 *in, int pixels) { + int i; + /* RGBA, leading pad */ + for (i = 0; i < pixels; i++) { + UINT32 iv = MAKE_UINT32(in[1], in[2], in[3], in[0]); + memcpy(_out, &iv, sizeof(iv)); + in += 4; + _out += 4; + } +} + +static void +unpackABGR(UINT8 *_out, const UINT8 *in, int pixels) { + int i; + /* RGBA, reversed bytes */ + for (i = 0; i < pixels; i++) { + UINT32 iv = MAKE_UINT32(in[3], in[2], in[1], in[0]); + memcpy(_out, &iv, sizeof(iv)); + in += 4; + _out += 4; + } +} + +static void +unpackBGRA(UINT8 *_out, const UINT8 *in, int pixels) { + int i; + /* RGBA, rearranged channels */ + for (i = 0; i < pixels; i++) { + UINT32 iv = MAKE_UINT32(in[2], in[1], in[0], in[3]); + memcpy(_out, &iv, sizeof(iv)); + in += 4; + _out += 4; + } +} + +static void +unpackBGRA16L(UINT8 *_out, const UINT8 *in, int pixels) { + int i; + /* 16-bit RGBA, little-endian order, rearranged channels */ + for (i = 0; i < pixels; i++) { + UINT32 iv = MAKE_UINT32(in[5], in[3], in[1], in[7]); + memcpy(_out, &iv, sizeof(iv)); + in += 8; + _out += 4; + } +} + +static void +unpackBGRA16B(UINT8 *_out, const UINT8 *in, int pixels) { + int i; + /* 16-bit RGBA, big-endian order, rearranged channels */ + for (i = 0; i < pixels; i++) { + UINT32 iv = MAKE_UINT32(in[4], in[2], in[0], in[6]); + memcpy(_out, &iv, sizeof(iv)); + in += 8; + _out += 4; + } +} + +static void +unpackBGAR(UINT8 *_out, const UINT8 *in, int pixels) { + int i; + for (i = 0; i < pixels; i++) { + UINT32 iv = MAKE_UINT32(in[3], in[1], in[0], in[2]); + memcpy(_out, &iv, sizeof(iv)); + in += 4; + _out += 4; + } +} + +/* Unpack to "CMYK" image */ + +static void +unpackCMYKI(UINT8 *_out, const UINT8 *in, int pixels) { + int i; + /* CMYK, inverted bytes (Photoshop 2.5) */ + for (i = 0; i < pixels; i++) { + UINT32 iv = ~MAKE_UINT32(in[0], in[1], in[2], in[3]); + memcpy(_out, &iv, sizeof(iv)); + in += 4; + _out += 4; + } +} + +/* Unpack to "LAB" image */ +/* There are two representations of LAB images for whatever precision: + L: Uint (in PS, it's 0-100) + A: Int (in ps, -128 .. 128, or elsewhere 0..255, with 128 as middle. + Channels in PS display a 0 value as middle gray, + LCMS appears to use 128 as the 0 value for these channels) + B: Int (as above) + + Since we don't have any signed ints, we're going with the shifted versions + internally, and we'll unshift for saving and whatnot. +*/ +void +ImagingUnpackLAB(UINT8 *out, const UINT8 *in, int pixels) { + int i; + /* LAB triplets */ + for (i = 0; i < pixels; i++) { + out[0] = in[0]; + out[1] = in[1] ^ 128; /* signed in outside world */ + out[2] = in[2] ^ 128; + out[3] = 255; + out += 4; + in += 3; + } +} + +static void +unpackI16N_I16B(UINT8 *out, const UINT8 *in, int pixels) { + int i; + UINT8 *tmp = (UINT8 *)out; + for (i = 0; i < pixels; i++) { + C16B; + in += 2; + tmp += 2; + } +} +static void +unpackI16N_I16(UINT8 *out, const UINT8 *in, int pixels) { + int i; + UINT8 *tmp = (UINT8 *)out; + for (i = 0; i < pixels; i++) { + C16L; + in += 2; + tmp += 2; + } +} +static void +unpackI16B_I16(UINT8 *out, const UINT8 *in, int pixels) { + int i; + for (i = 0; i < pixels; i++) { + out[0] = in[1]; + out[1] = in[0]; + in += 2; + out += 2; + } +} +static void +unpackI16R_I16(UINT8 *out, const UINT8 *in, int pixels) { + int i; + for (i = 0; i < pixels; i++) { + out[0] = BITFLIP[in[0]]; + out[1] = BITFLIP[in[1]]; + in += 2; + out += 2; + } +} + +static void +unpackI12_I16(UINT8 *out, const UINT8 *in, int pixels) { + /* Fillorder 1/MSB -> LittleEndian, for 12bit integer grayscale tiffs. + + According to the TIFF spec: + + FillOrder = 2 should be used only when BitsPerSample = 1 and + the data is either uncompressed or compressed using CCITT 1D + or 2D compression, to avoid potentially ambiguous situations. + + Yeah. I thought so. We'll see how well people read the spec. + We've got several fillorder=2 modes in TiffImagePlugin.py + + There's no spec I can find. It appears that the in storage + layout is: 00 80 00 ... -> (128 , 0 ...). The samples are + stored in a single big bitian 12bit block, but need to be + pulled out to little endian format to be stored in a 2 byte + int. + */ + + int i; + UINT16 pixel; +#ifdef WORDS_BIGENDIAN + UINT8 *tmp = (UINT8 *)&pixel; +#endif + for (i = 0; i < pixels - 1; i += 2) { + pixel = (((UINT16)in[0]) << 4) + (in[1] >> 4); +#ifdef WORDS_BIGENDIAN + out[0] = tmp[1]; + out[1] = tmp[0]; +#else + memcpy(out, &pixel, sizeof(pixel)); +#endif + + out += 2; + pixel = (((UINT16)(in[1] & 0x0F)) << 8) + in[2]; +#ifdef WORDS_BIGENDIAN + out[0] = tmp[1]; + out[1] = tmp[0]; +#else + memcpy(out, &pixel, sizeof(pixel)); +#endif + + in += 3; + out += 2; + } + if (i == pixels - 1) { + pixel = (((UINT16)in[0]) << 4) + (in[1] >> 4); +#ifdef WORDS_BIGENDIAN + out[0] = tmp[1]; + out[1] = tmp[0]; +#else + memcpy(out, &pixel, sizeof(pixel)); +#endif + } +} + +static void +copy1(UINT8 *out, const UINT8 *in, int pixels) { + /* L, P */ + memcpy(out, in, pixels); +} + +static void +copy2(UINT8 *out, const UINT8 *in, int pixels) { + /* I;16 */ + memcpy(out, in, pixels * 2); +} + +static void +copy3(UINT8 *out, const UINT8 *in, int pixels) { + /* BGR;24 */ + memcpy(out, in, pixels * 3); +} + +static void +copy4(UINT8 *out, const UINT8 *in, int pixels) { + /* RGBA, CMYK quadruples */ + memcpy(out, in, 4 * pixels); +} + +static void +copy4skip1(UINT8 *_out, const UINT8 *in, int pixels) { + int i; + for (i = 0; i < pixels; i++) { + memcpy(_out, in, 4); + in += 5; + _out += 4; + } +} + +static void +copy4skip2(UINT8 *_out, const UINT8 *in, int pixels) { + int i; + for (i = 0; i < pixels; i++) { + memcpy(_out, in, 4); + in += 6; + _out += 4; + } +} + +/* Unpack to "I" and "F" images */ + +#define UNPACK_RAW(NAME, GET, INTYPE, OUTTYPE) \ + static void NAME(UINT8 *out_, const UINT8 *in, int pixels) { \ + int i; \ + OUTTYPE *out = (OUTTYPE *)out_; \ + for (i = 0; i < pixels; i++, in += sizeof(INTYPE)) { \ + out[i] = (OUTTYPE)((INTYPE)GET); \ + } \ + } + +#define UNPACK(NAME, COPY, INTYPE, OUTTYPE) \ + static void NAME(UINT8 *out_, const UINT8 *in, int pixels) { \ + int i; \ + OUTTYPE *out = (OUTTYPE *)out_; \ + INTYPE tmp_; \ + UINT8 *tmp = (UINT8 *)&tmp_; \ + for (i = 0; i < pixels; i++, in += sizeof(INTYPE)) { \ + COPY; \ + out[i] = (OUTTYPE)tmp_; \ + } \ + } + +UNPACK_RAW(unpackI8, in[0], UINT8, INT32) +UNPACK_RAW(unpackI8S, in[0], INT8, INT32) +UNPACK(unpackI16, C16L, UINT16, INT32) +UNPACK(unpackI16S, C16L, INT16, INT32) +UNPACK(unpackI16B, C16B, UINT16, INT32) +UNPACK(unpackI16BS, C16B, INT16, INT32) +UNPACK(unpackI16N, C16N, UINT16, INT32) +UNPACK(unpackI16NS, C16N, INT16, INT32) +UNPACK(unpackI32, C32L, UINT32, INT32) +UNPACK(unpackI32S, C32L, INT32, INT32) +UNPACK(unpackI32B, C32B, UINT32, INT32) +UNPACK(unpackI32BS, C32B, INT32, INT32) +UNPACK(unpackI32N, C32N, UINT32, INT32) +UNPACK(unpackI32NS, C32N, INT32, INT32) + +UNPACK_RAW(unpackF8, in[0], UINT8, FLOAT32) +UNPACK_RAW(unpackF8S, in[0], INT8, FLOAT32) +UNPACK(unpackF16, C16L, UINT16, FLOAT32) +UNPACK(unpackF16S, C16L, INT16, FLOAT32) +UNPACK(unpackF16B, C16B, UINT16, FLOAT32) +UNPACK(unpackF16BS, C16B, INT16, FLOAT32) +UNPACK(unpackF16N, C16N, UINT16, FLOAT32) +UNPACK(unpackF16NS, C16N, INT16, FLOAT32) +UNPACK(unpackF32, C32L, UINT32, FLOAT32) +UNPACK(unpackF32S, C32L, INT32, FLOAT32) +UNPACK(unpackF32B, C32B, UINT32, FLOAT32) +UNPACK(unpackF32BS, C32B, INT32, FLOAT32) +UNPACK(unpackF32N, C32N, UINT32, FLOAT32) +UNPACK(unpackF32NS, C32N, INT32, FLOAT32) +UNPACK(unpackF32F, C32L, FLOAT32, FLOAT32) +UNPACK(unpackF32BF, C32B, FLOAT32, FLOAT32) +UNPACK(unpackF32NF, C32N, FLOAT32, FLOAT32) +#ifdef FLOAT64 +UNPACK(unpackF64F, C64L, FLOAT64, FLOAT32) +UNPACK(unpackF64BF, C64B, FLOAT64, FLOAT32) +UNPACK(unpackF64NF, C64N, FLOAT64, FLOAT32) +#endif + +/* Misc. unpackers */ + +static void +band0(UINT8 *out, const UINT8 *in, int pixels) { + int i; + /* band 0 only */ + for (i = 0; i < pixels; i++) { + out[0] = in[i]; + out += 4; + } +} + +static void +band1(UINT8 *out, const UINT8 *in, int pixels) { + int i; + /* band 1 only */ + for (i = 0; i < pixels; i++) { + out[1] = in[i]; + out += 4; + } +} + +static void +band2(UINT8 *out, const UINT8 *in, int pixels) { + int i; + /* band 2 only */ + for (i = 0; i < pixels; i++) { + out[2] = in[i]; + out += 4; + } +} + +static void +band3(UINT8 *out, const UINT8 *in, int pixels) { + /* band 3 only */ + int i; + for (i = 0; i < pixels; i++) { + out[3] = in[i]; + out += 4; + } +} + +static void +band0I(UINT8 *out, const UINT8 *in, int pixels) { + int i; + /* band 0 only */ + for (i = 0; i < pixels; i++) { + out[0] = ~in[i]; + out += 4; + } +} + +static void +band1I(UINT8 *out, const UINT8 *in, int pixels) { + int i; + /* band 1 only */ + for (i = 0; i < pixels; i++) { + out[1] = ~in[i]; + out += 4; + } +} + +static void +band2I(UINT8 *out, const UINT8 *in, int pixels) { + int i; + /* band 2 only */ + for (i = 0; i < pixels; i++) { + out[2] = ~in[i]; + out += 4; + } +} + +static void +band3I(UINT8 *out, const UINT8 *in, int pixels) { + /* band 3 only */ + int i; + for (i = 0; i < pixels; i++) { + out[3] = ~in[i]; + out += 4; + } +} + +static void +band016B(UINT8 *out, const UINT8 *in, int pixels) { + int i; + /* band 0 only, big endian */ + for (i = 0; i < pixels; i++) { + out[0] = in[0]; + out += 4; + in += 2; + } +} + +static void +band116B(UINT8 *out, const UINT8 *in, int pixels) { + int i; + /* band 1 only, big endian */ + for (i = 0; i < pixels; i++) { + out[1] = in[0]; + out += 4; + in += 2; + } +} + +static void +band216B(UINT8 *out, const UINT8 *in, int pixels) { + int i; + /* band 2 only, big endian */ + for (i = 0; i < pixels; i++) { + out[2] = in[0]; + out += 4; + in += 2; + } +} + +static void +band316B(UINT8 *out, const UINT8 *in, int pixels) { + int i; + /* band 3 only, big endian */ + for (i = 0; i < pixels; i++) { + out[3] = in[0]; + out += 4; + in += 2; + } +} + +static void +band016L(UINT8 *out, const UINT8 *in, int pixels) { + int i; + /* band 0 only, little endian */ + for (i = 0; i < pixels; i++) { + out[0] = in[1]; + out += 4; + in += 2; + } +} + +static void +band116L(UINT8 *out, const UINT8 *in, int pixels) { + int i; + /* band 1 only, little endian */ + for (i = 0; i < pixels; i++) { + out[1] = in[1]; + out += 4; + in += 2; + } +} + +static void +band216L(UINT8 *out, const UINT8 *in, int pixels) { + int i; + /* band 2 only, little endian */ + for (i = 0; i < pixels; i++) { + out[2] = in[1]; + out += 4; + in += 2; + } +} + +static void +band316L(UINT8 *out, const UINT8 *in, int pixels) { + int i; + /* band 3 only, little endian */ + for (i = 0; i < pixels; i++) { + out[3] = in[1]; + out += 4; + in += 2; + } +} + +static struct { + const char *mode; + const char *rawmode; + int bits; + ImagingShuffler unpack; +} unpackers[] = { + + /* raw mode syntax is ";" where "bits" defaults + depending on mode (1 for "1", 8 for "P" and "L", etc), and + "flags" should be given in alphabetical order. if both bits + and flags have their default values, the ; should be left out */ + + /* flags: "I" inverted data; "R" reversed bit order; "B" big + endian byte order (default is little endian); "L" line + interleave, "S" signed, "F" floating point */ + + /* exception: rawmodes "I" and "F" are always native endian byte order */ + + /* bilevel */ + {"1", "1", 1, unpack1}, + {"1", "1;I", 1, unpack1I}, + {"1", "1;R", 1, unpack1R}, + {"1", "1;IR", 1, unpack1IR}, + {"1", "1;8", 8, unpack18}, + + /* grayscale */ + {"L", "L;2", 2, unpackL2}, + {"L", "L;2I", 2, unpackL2I}, + {"L", "L;2R", 2, unpackL2R}, + {"L", "L;2IR", 2, unpackL2IR}, + + {"L", "L;4", 4, unpackL4}, + {"L", "L;4I", 4, unpackL4I}, + {"L", "L;4R", 4, unpackL4R}, + {"L", "L;4IR", 4, unpackL4IR}, + + {"L", "L", 8, copy1}, + {"L", "L;I", 8, unpackLI}, + {"L", "L;R", 8, unpackLR}, + {"L", "L;16", 16, unpackL16}, + {"L", "L;16B", 16, unpackL16B}, + + /* grayscale w. alpha */ + {"LA", "LA", 16, unpackLA}, + {"LA", "LA;L", 16, unpackLAL}, + + /* grayscale w. alpha premultiplied */ + {"La", "La", 16, unpackLA}, + + /* palette */ + {"P", "P;1", 1, unpackP1}, + {"P", "P;2", 2, unpackP2}, + {"P", "P;2L", 2, unpackP2L}, + {"P", "P;4", 4, unpackP4}, + {"P", "P;4L", 4, unpackP4L}, + {"P", "P", 8, copy1}, + {"P", "P;R", 8, unpackLR}, + {"P", "L", 8, copy1}, + {"P", "PX", 16, unpackL16B}, + + /* palette w. alpha */ + {"PA", "PA", 16, unpackLA}, + {"PA", "PA;L", 16, unpackLAL}, + {"PA", "LA", 16, unpackLA}, + + /* true colour */ + {"RGB", "RGB", 24, ImagingUnpackRGB}, + {"RGB", "RGB;L", 24, unpackRGBL}, + {"RGB", "RGB;R", 24, unpackRGBR}, + {"RGB", "RGB;16L", 48, unpackRGB16L}, + {"RGB", "RGB;16B", 48, unpackRGB16B}, + {"RGB", "BGR", 24, ImagingUnpackBGR}, + {"RGB", "RGB;15", 16, ImagingUnpackRGB15}, + {"RGB", "BGR;15", 16, ImagingUnpackBGR15}, + {"RGB", "RGB;16", 16, ImagingUnpackRGB16}, + {"RGB", "BGR;16", 16, ImagingUnpackBGR16}, + {"RGB", "RGB;4B", 16, ImagingUnpackRGB4B}, + {"RGB", "BGR;5", 16, ImagingUnpackBGR15}, /* compat */ + {"RGB", "RGBX", 32, copy4}, + {"RGB", "RGBX;L", 32, unpackRGBAL}, + {"RGB", "RGBA;L", 32, unpackRGBAL}, + {"RGB", "RGBA;15", 16, ImagingUnpackRGBA15}, + {"RGB", "BGRX", 32, ImagingUnpackBGRX}, + {"RGB", "BGXR", 32, ImagingUnpackBGXR}, + {"RGB", "XRGB", 32, ImagingUnpackXRGB}, + {"RGB", "XBGR", 32, ImagingUnpackXBGR}, + {"RGB", "YCC;P", 24, ImagingUnpackYCC}, + {"RGB", "R", 8, band0}, + {"RGB", "G", 8, band1}, + {"RGB", "B", 8, band2}, + {"RGB", "R;16L", 16, band016L}, + {"RGB", "G;16L", 16, band116L}, + {"RGB", "B;16L", 16, band216L}, + {"RGB", "R;16B", 16, band016B}, + {"RGB", "G;16B", 16, band116B}, + {"RGB", "B;16B", 16, band216B}, + {"RGB", "CMYK", 32, cmyk2rgb}, + + {"BGR;15", "BGR;15", 16, copy2}, + {"BGR;16", "BGR;16", 16, copy2}, + {"BGR;24", "BGR;24", 24, copy3}, + + /* true colour w. alpha */ + {"RGBA", "LA", 16, unpackRGBALA}, + {"RGBA", "LA;16B", 32, unpackRGBALA16B}, + {"RGBA", "RGBA", 32, copy4}, + {"RGBA", "RGBAX", 40, copy4skip1}, + {"RGBA", "RGBAXX", 48, copy4skip2}, + {"RGBA", "RGBa", 32, unpackRGBa}, + {"RGBA", "RGBaX", 40, unpackRGBaskip1}, + {"RGBA", "RGBaXX", 48, unpackRGBaskip2}, + {"RGBA", "RGBa;16L", 64, unpackRGBa16L}, + {"RGBA", "RGBa;16B", 64, unpackRGBa16B}, + {"RGBA", "BGRa", 32, unpackBGRa}, + {"RGBA", "RGBA;I", 32, unpackRGBAI}, + {"RGBA", "RGBA;L", 32, unpackRGBAL}, + {"RGBA", "RGBA;15", 16, ImagingUnpackRGBA15}, + {"RGBA", "BGRA;15", 16, ImagingUnpackBGRA15}, + {"RGBA", "RGBA;4B", 16, ImagingUnpackRGBA4B}, + {"RGBA", "RGBA;16L", 64, unpackRGBA16L}, + {"RGBA", "RGBA;16B", 64, unpackRGBA16B}, + {"RGBA", "BGRA", 32, unpackBGRA}, + {"RGBA", "BGRA;16L", 64, unpackBGRA16L}, + {"RGBA", "BGRA;16B", 64, unpackBGRA16B}, + {"RGBA", "BGAR", 32, unpackBGAR}, + {"RGBA", "ARGB", 32, unpackARGB}, + {"RGBA", "ABGR", 32, unpackABGR}, + {"RGBA", "YCCA;P", 32, ImagingUnpackYCCA}, + {"RGBA", "R", 8, band0}, + {"RGBA", "G", 8, band1}, + {"RGBA", "B", 8, band2}, + {"RGBA", "A", 8, band3}, + {"RGBA", "R;16L", 16, band016L}, + {"RGBA", "G;16L", 16, band116L}, + {"RGBA", "B;16L", 16, band216L}, + {"RGBA", "A;16L", 16, band316L}, + {"RGBA", "R;16B", 16, band016B}, + {"RGBA", "G;16B", 16, band116B}, + {"RGBA", "B;16B", 16, band216B}, + {"RGBA", "A;16B", 16, band316B}, + +#ifdef WORDS_BIGENDIAN + {"RGB", "RGB;16N", 48, unpackRGB16B}, + {"RGBA", "RGBa;16N", 64, unpackRGBa16B}, + {"RGBA", "RGBA;16N", 64, unpackRGBA16B}, + {"RGBX", "RGBX;16N", 64, unpackRGBA16B}, + {"RGB", "R;16N", 16, band016B}, + {"RGB", "G;16N", 16, band116B}, + {"RGB", "B;16N", 16, band216B}, + + {"RGBA", "R;16N", 16, band016B}, + {"RGBA", "G;16N", 16, band116B}, + {"RGBA", "B;16N", 16, band216B}, + {"RGBA", "A;16N", 16, band316B}, +#else + {"RGB", "RGB;16N", 48, unpackRGB16L}, + {"RGBA", "RGBa;16N", 64, unpackRGBa16L}, + {"RGBA", "RGBA;16N", 64, unpackRGBA16L}, + {"RGBX", "RGBX;16N", 64, unpackRGBA16L}, + {"RGB", "R;16N", 16, band016L}, + {"RGB", "G;16N", 16, band116L}, + {"RGB", "B;16N", 16, band216L}, + + {"RGBA", "R;16N", 16, band016L}, + {"RGBA", "G;16N", 16, band116L}, + {"RGBA", "B;16N", 16, band216L}, + {"RGBA", "A;16N", 16, band316L}, +#endif + + /* true colour w. alpha premultiplied */ + {"RGBa", "RGBa", 32, copy4}, + {"RGBa", "BGRa", 32, unpackBGRA}, + {"RGBa", "aRGB", 32, unpackARGB}, + {"RGBa", "aBGR", 32, unpackABGR}, + + /* true colour w. padding */ + {"RGBX", "RGB", 24, ImagingUnpackRGB}, + {"RGBX", "RGB;L", 24, unpackRGBL}, + {"RGBX", "RGB;16B", 48, unpackRGB16B}, + {"RGBX", "BGR", 24, ImagingUnpackBGR}, + {"RGBX", "RGB;15", 16, ImagingUnpackRGB15}, + {"RGBX", "BGR;15", 16, ImagingUnpackBGR15}, + {"RGBX", "RGB;4B", 16, ImagingUnpackRGB4B}, + {"RGBX", "BGR;5", 16, ImagingUnpackBGR15}, /* compat */ + {"RGBX", "RGBX", 32, copy4}, + {"RGBX", "RGBXX", 40, copy4skip1}, + {"RGBX", "RGBXXX", 48, copy4skip2}, + {"RGBX", "RGBX;L", 32, unpackRGBAL}, + {"RGBX", "RGBX;16L", 64, unpackRGBA16L}, + {"RGBX", "RGBX;16B", 64, unpackRGBA16B}, + {"RGBX", "BGRX", 32, ImagingUnpackBGRX}, + {"RGBX", "XRGB", 32, ImagingUnpackXRGB}, + {"RGBX", "XBGR", 32, ImagingUnpackXBGR}, + {"RGBX", "YCC;P", 24, ImagingUnpackYCC}, + {"RGBX", "R", 8, band0}, + {"RGBX", "G", 8, band1}, + {"RGBX", "B", 8, band2}, + {"RGBX", "X", 8, band3}, + + /* colour separation */ + {"CMYK", "CMYK", 32, copy4}, + {"CMYK", "CMYKX", 40, copy4skip1}, + {"CMYK", "CMYKXX", 48, copy4skip2}, + {"CMYK", "CMYK;I", 32, unpackCMYKI}, + {"CMYK", "CMYK;L", 32, unpackRGBAL}, + {"CMYK", "CMYK;16L", 64, unpackRGBA16L}, + {"CMYK", "CMYK;16B", 64, unpackRGBA16B}, + {"CMYK", "C", 8, band0}, + {"CMYK", "M", 8, band1}, + {"CMYK", "Y", 8, band2}, + {"CMYK", "K", 8, band3}, + {"CMYK", "C;I", 8, band0I}, + {"CMYK", "M;I", 8, band1I}, + {"CMYK", "Y;I", 8, band2I}, + {"CMYK", "K;I", 8, band3I}, + +#ifdef WORDS_BIGENDIAN + {"CMYK", "CMYK;16N", 64, unpackRGBA16B}, +#else + {"CMYK", "CMYK;16N", 64, unpackRGBA16L}, +#endif + + /* video (YCbCr) */ + {"YCbCr", "YCbCr", 24, ImagingUnpackRGB}, + {"YCbCr", "YCbCr;L", 24, unpackRGBL}, + {"YCbCr", "YCbCrX", 32, copy4}, + {"YCbCr", "YCbCrK", 32, copy4}, + + /* LAB Color */ + {"LAB", "LAB", 24, ImagingUnpackLAB}, + {"LAB", "L", 8, band0}, + {"LAB", "A", 8, band1}, + {"LAB", "B", 8, band2}, + + /* HSV Color */ + {"HSV", "HSV", 24, ImagingUnpackRGB}, + {"HSV", "H", 8, band0}, + {"HSV", "S", 8, band1}, + {"HSV", "V", 8, band2}, + + /* integer variations */ + {"I", "I", 32, copy4}, + {"I", "I;8", 8, unpackI8}, + {"I", "I;8S", 8, unpackI8S}, + {"I", "I;16", 16, unpackI16}, + {"I", "I;16S", 16, unpackI16S}, + {"I", "I;16B", 16, unpackI16B}, + {"I", "I;16BS", 16, unpackI16BS}, + {"I", "I;16N", 16, unpackI16N}, + {"I", "I;16NS", 16, unpackI16NS}, + {"I", "I;32", 32, unpackI32}, + {"I", "I;32S", 32, unpackI32S}, + {"I", "I;32B", 32, unpackI32B}, + {"I", "I;32BS", 32, unpackI32BS}, + {"I", "I;32N", 32, unpackI32N}, + {"I", "I;32NS", 32, unpackI32NS}, + + /* floating point variations */ + {"F", "F", 32, copy4}, + {"F", "F;8", 8, unpackF8}, + {"F", "F;8S", 8, unpackF8S}, + {"F", "F;16", 16, unpackF16}, + {"F", "F;16S", 16, unpackF16S}, + {"F", "F;16B", 16, unpackF16B}, + {"F", "F;16BS", 16, unpackF16BS}, + {"F", "F;16N", 16, unpackF16N}, + {"F", "F;16NS", 16, unpackF16NS}, + {"F", "F;32", 32, unpackF32}, + {"F", "F;32S", 32, unpackF32S}, + {"F", "F;32B", 32, unpackF32B}, + {"F", "F;32BS", 32, unpackF32BS}, + {"F", "F;32N", 32, unpackF32N}, + {"F", "F;32NS", 32, unpackF32NS}, + {"F", "F;32F", 32, unpackF32F}, + {"F", "F;32BF", 32, unpackF32BF}, + {"F", "F;32NF", 32, unpackF32NF}, +#ifdef FLOAT64 + {"F", "F;64F", 64, unpackF64F}, + {"F", "F;64BF", 64, unpackF64BF}, + {"F", "F;64NF", 64, unpackF64NF}, +#endif + + /* storage modes */ + {"I;16", "I;16", 16, copy2}, + {"I;16B", "I;16B", 16, copy2}, + {"I;16L", "I;16L", 16, copy2}, + {"I;16N", "I;16N", 16, copy2}, + + {"I;16", "I;16B", 16, unpackI16B_I16}, + {"I;16", "I;16N", 16, unpackI16N_I16}, // LibTiff native->image endian. + {"I;16L", "I;16N", 16, unpackI16N_I16}, // LibTiff native->image endian. + {"I;16B", "I;16N", 16, unpackI16N_I16B}, + + {"I;16", "I;16R", 16, unpackI16R_I16}, + + {"I;16", "I;12", 12, unpackI12_I16}, // 12 bit Tiffs stored in 16bits. + + {NULL} /* sentinel */ +}; + +ImagingShuffler +ImagingFindUnpacker(const char *mode, const char *rawmode, int *bits_out) { + int i; + + /* find a suitable pixel unpacker */ + for (i = 0; unpackers[i].rawmode; i++) { + if (strcmp(unpackers[i].mode, mode) == 0 && + strcmp(unpackers[i].rawmode, rawmode) == 0) { + if (bits_out) { + *bits_out = unpackers[i].bits; + } + return unpackers[i].unpack; + } + } + + /* FIXME: configure a general unpacker based on the type codes... */ + + return NULL; +} diff --git a/src/libImaging/UnpackYCC.c b/src/libImaging/UnpackYCC.c index 0b177bdd4..5bcea45b4 100644 --- a/src/libImaging/UnpackYCC.c +++ b/src/libImaging/UnpackYCC.c @@ -1,163 +1,163 @@ -/* - * The Python Imaging Library. - * $Id$ - * - * code to convert and unpack PhotoYCC data - * - * history: - * 97-01-25 fl Moved from PcdDecode.c - * - * Copyright (c) Fredrik Lundh 1996-97. - * Copyright (c) Secret Labs AB 1997. - * - * See the README file for information on usage and redistribution. - */ - -#include "Imaging.h" - -/* Tables generated by pcdtables.py, based on transforms taken from - the "Colour Space Conversions FAQ" by Roberts/Ford. */ - -static INT16 L[] = { - 0, 1, 3, 4, 5, 7, 8, 10, 11, 12, 14, 15, 16, 18, 19, 20, - 22, 23, 24, 26, 27, 29, 30, 31, 33, 34, 35, 37, 38, 39, 41, 42, - 43, 45, 46, 48, 49, 50, 52, 53, 54, 56, 57, 58, 60, 61, 62, 64, - 65, 67, 68, 69, 71, 72, 73, 75, 76, 77, 79, 80, 82, 83, 84, 86, - 87, 88, 90, 91, 92, 94, 95, 96, 98, 99, 101, 102, 103, 105, 106, 107, - 109, 110, 111, 113, 114, 115, 117, 118, 120, 121, 122, 124, 125, 126, 128, 129, - 130, 132, 133, 134, 136, 137, 139, 140, 141, 143, 144, 145, 147, 148, 149, 151, - 152, 153, 155, 156, 158, 159, 160, 162, 163, 164, 166, 167, 168, 170, 171, 173, - 174, 175, 177, 178, 179, 181, 182, 183, 185, 186, 187, 189, 190, 192, 193, 194, - 196, 197, 198, 200, 201, 202, 204, 205, 206, 208, 209, 211, 212, 213, 215, 216, - 217, 219, 220, 221, 223, 224, 225, 227, 228, 230, 231, 232, 234, 235, 236, 238, - 239, 240, 242, 243, 245, 246, 247, 249, 250, 251, 253, 254, 255, 257, 258, 259, - 261, 262, 264, 265, 266, 268, 269, 270, 272, 273, 274, 276, 277, 278, 280, 281, - 283, 284, 285, 287, 288, 289, 291, 292, 293, 295, 296, 297, 299, 300, 302, 303, - 304, 306, 307, 308, 310, 311, 312, 314, 315, 317, 318, 319, 321, 322, 323, 325, - 326, 327, 329, 330, 331, 333, 334, 336, 337, 338, 340, 341, 342, 344, 345, 346}; - -static INT16 CB[] = { - -345, -343, -341, -338, -336, -334, -332, -329, -327, -325, -323, -321, -318, -316, - -314, -312, -310, -307, -305, -303, -301, -298, -296, -294, -292, -290, -287, -285, - -283, -281, -278, -276, -274, -272, -270, -267, -265, -263, -261, -258, -256, -254, - -252, -250, -247, -245, -243, -241, -239, -236, -234, -232, -230, -227, -225, -223, - -221, -219, -216, -214, -212, -210, -207, -205, -203, -201, -199, -196, -194, -192, - -190, -188, -185, -183, -181, -179, -176, -174, -172, -170, -168, -165, -163, -161, - -159, -156, -154, -152, -150, -148, -145, -143, -141, -139, -137, -134, -132, -130, - -128, -125, -123, -121, -119, -117, -114, -112, -110, -108, -105, -103, -101, -99, - -97, -94, -92, -90, -88, -85, -83, -81, -79, -77, -74, -72, -70, -68, - -66, -63, -61, -59, -57, -54, -52, -50, -48, -46, -43, -41, -39, -37, - -34, -32, -30, -28, -26, -23, -21, -19, -17, -15, -12, -10, -8, -6, - -3, -1, 0, 2, 4, 7, 9, 11, 13, 16, 18, 20, 22, 24, - 27, 29, 31, 33, 35, 38, 40, 42, 44, 47, 49, 51, 53, 55, - 58, 60, 62, 64, 67, 69, 71, 73, 75, 78, 80, 82, 84, 86, - 89, 91, 93, 95, 98, 100, 102, 104, 106, 109, 111, 113, 115, 118, - 120, 122, 124, 126, 129, 131, 133, 135, 138, 140, 142, 144, 146, 149, - 151, 153, 155, 157, 160, 162, 164, 166, 169, 171, 173, 175, 177, 180, - 182, 184, 186, 189, 191, 193, 195, 197, 200, 202, 204, 206, 208, 211, - 213, 215, 217, 220}; - -static INT16 GB[] = { - 67, 67, 66, 66, 65, 65, 65, 64, 64, 63, 63, 62, 62, 62, 61, 61, - 60, 60, 59, 59, 59, 58, 58, 57, 57, 56, 56, 56, 55, 55, 54, 54, - 53, 53, 52, 52, 52, 51, 51, 50, 50, 49, 49, 49, 48, 48, 47, 47, - 46, 46, 46, 45, 45, 44, 44, 43, 43, 43, 42, 42, 41, 41, 40, 40, - 40, 39, 39, 38, 38, 37, 37, 37, 36, 36, 35, 35, 34, 34, 34, 33, - 33, 32, 32, 31, 31, 31, 30, 30, 29, 29, 28, 28, 28, 27, 27, 26, - 26, 25, 25, 25, 24, 24, 23, 23, 22, 22, 22, 21, 21, 20, 20, 19, - 19, 19, 18, 18, 17, 17, 16, 16, 15, 15, 15, 14, 14, 13, 13, 12, - 12, 12, 11, 11, 10, 10, 9, 9, 9, 8, 8, 7, 7, 6, 6, 6, - 5, 5, 4, 4, 3, 3, 3, 2, 2, 1, 1, 0, 0, 0, 0, 0, - -1, -1, -2, -2, -2, -3, -3, -4, -4, -5, -5, -5, -6, -6, -7, -7, - -8, -8, -8, -9, -9, -10, -10, -11, -11, -11, -12, -12, -13, -13, -14, -14, - -14, -15, -15, -16, -16, -17, -17, -18, -18, -18, -19, -19, -20, -20, -21, -21, - -21, -22, -22, -23, -23, -24, -24, -24, -25, -25, -26, -26, -27, -27, -27, -28, - -28, -29, -29, -30, -30, -30, -31, -31, -32, -32, -33, -33, -33, -34, -34, -35, - -35, -36, -36, -36, -37, -37, -38, -38, -39, -39, -39, -40, -40, -41, -41, -42}; - -static INT16 CR[] = { - -249, -247, -245, -243, -241, -239, -238, -236, -234, -232, -230, -229, -227, -225, - -223, -221, -219, -218, -216, -214, -212, -210, -208, -207, -205, -203, -201, -199, - -198, -196, -194, -192, -190, -188, -187, -185, -183, -181, -179, -178, -176, -174, - -172, -170, -168, -167, -165, -163, -161, -159, -157, -156, -154, -152, -150, -148, - -147, -145, -143, -141, -139, -137, -136, -134, -132, -130, -128, -127, -125, -123, - -121, -119, -117, -116, -114, -112, -110, -108, -106, -105, -103, -101, -99, -97, - -96, -94, -92, -90, -88, -86, -85, -83, -81, -79, -77, -76, -74, -72, - -70, -68, -66, -65, -63, -61, -59, -57, -55, -54, -52, -50, -48, -46, - -45, -43, -41, -39, -37, -35, -34, -32, -30, -28, -26, -25, -23, -21, - -19, -17, -15, -14, -12, -10, -8, -6, -4, -3, -1, 0, 2, 4, - 5, 7, 9, 11, 13, 15, 16, 18, 20, 22, 24, 26, 27, 29, - 31, 33, 35, 36, 38, 40, 42, 44, 46, 47, 49, 51, 53, 55, - 56, 58, 60, 62, 64, 66, 67, 69, 71, 73, 75, 77, 78, 80, - 82, 84, 86, 87, 89, 91, 93, 95, 97, 98, 100, 102, 104, 106, - 107, 109, 111, 113, 115, 117, 118, 120, 122, 124, 126, 128, 129, 131, - 133, 135, 137, 138, 140, 142, 144, 146, 148, 149, 151, 153, 155, 157, - 158, 160, 162, 164, 166, 168, 169, 171, 173, 175, 177, 179, 180, 182, - 184, 186, 188, 189, 191, 193, 195, 197, 199, 200, 202, 204, 206, 208, - 209, 211, 213, 215}; - -static INT16 GR[] = { - 127, 126, 125, 124, 123, 122, 121, 121, 120, 119, 118, 117, 116, 115, 114, - 113, 112, 111, 110, 109, 108, 108, 107, 106, 105, 104, 103, 102, 101, 100, - 99, 98, 97, 96, 95, 95, 94, 93, 92, 91, 90, 89, 88, 87, 86, - 85, 84, 83, 83, 82, 81, 80, 79, 78, 77, 76, 75, 74, 73, 72, - 71, 70, 70, 69, 68, 67, 66, 65, 64, 63, 62, 61, 60, 59, 58, - 57, 57, 56, 55, 54, 53, 52, 51, 50, 49, 48, 47, 46, 45, 45, - 44, 43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, 32, 32, 31, - 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 19, 18, 17, - 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 6, 5, 4, 3, - 2, 1, 0, 0, -1, -2, -3, -4, -5, -5, -6, -7, -8, -9, -10, - -11, -12, -13, -14, -15, -16, -17, -18, -18, -19, -20, -21, -22, -23, -24, - -25, -26, -27, -28, -29, -30, -31, -31, -32, -33, -34, -35, -36, -37, -38, - -39, -40, -41, -42, -43, -44, -44, -45, -46, -47, -48, -49, -50, -51, -52, - -53, -54, -55, -56, -56, -57, -58, -59, -60, -61, -62, -63, -64, -65, -66, - -67, -68, -69, -69, -70, -71, -72, -73, -74, -75, -76, -77, -78, -79, -80, - -81, -82, -82, -83, -84, -85, -86, -87, -88, -89, -90, -91, -92, -93, -94, - -94, -95, -96, -97, -98, -99, -100, -101, -102, -103, -104, -105, -106, -107, -107, - -108}; - -#define R 0 -#define G 1 -#define B 2 -#define A 3 - -#define YCC2RGB(rgb, y, cb, cr) \ - { \ - int l = L[y]; \ - int r = l + CR[cr]; \ - int g = l + GR[cr] + GB[cb]; \ - int b = l + CB[cb]; \ - rgb[0] = (r <= 0) ? 0 : (r >= 255) ? 255 : r; \ - rgb[1] = (g <= 0) ? 0 : (g >= 255) ? 255 : g; \ - rgb[2] = (b <= 0) ? 0 : (b >= 255) ? 255 : b; \ - } - -void -ImagingUnpackYCC(UINT8 *out, const UINT8 *in, int pixels) { - int i; - /* PhotoYCC triplets */ - for (i = 0; i < pixels; i++) { - YCC2RGB(out, in[0], in[1], in[2]); - out[A] = 255; - out += 4; - in += 3; - } -} - -void -ImagingUnpackYCCA(UINT8 *out, const UINT8 *in, int pixels) { - int i; - /* PhotoYCC triplets plus premultiplied alpha */ - for (i = 0; i < pixels; i++) { - /* Divide by alpha */ - UINT8 rgb[3]; - rgb[0] = (in[3] == 0) ? 0 : (((int)in[0] * 255) / in[3]); - rgb[1] = (in[3] == 0) ? 0 : (((int)in[1] * 255) / in[3]); - rgb[2] = (in[3] == 0) ? 0 : (((int)in[2] * 255) / in[3]); - /* Convert non-multiplied data to RGB */ - YCC2RGB(out, rgb[0], rgb[1], rgb[2]); - out[A] = in[3]; - out += 4; - in += 4; - } -} +/* + * The Python Imaging Library. + * $Id$ + * + * code to convert and unpack PhotoYCC data + * + * history: + * 97-01-25 fl Moved from PcdDecode.c + * + * Copyright (c) Fredrik Lundh 1996-97. + * Copyright (c) Secret Labs AB 1997. + * + * See the README file for information on usage and redistribution. + */ + +#include "Imaging.h" + +/* Tables generated by pcdtables.py, based on transforms taken from + the "Colour Space Conversions FAQ" by Roberts/Ford. */ + +static INT16 L[] = { + 0, 1, 3, 4, 5, 7, 8, 10, 11, 12, 14, 15, 16, 18, 19, 20, + 22, 23, 24, 26, 27, 29, 30, 31, 33, 34, 35, 37, 38, 39, 41, 42, + 43, 45, 46, 48, 49, 50, 52, 53, 54, 56, 57, 58, 60, 61, 62, 64, + 65, 67, 68, 69, 71, 72, 73, 75, 76, 77, 79, 80, 82, 83, 84, 86, + 87, 88, 90, 91, 92, 94, 95, 96, 98, 99, 101, 102, 103, 105, 106, 107, + 109, 110, 111, 113, 114, 115, 117, 118, 120, 121, 122, 124, 125, 126, 128, 129, + 130, 132, 133, 134, 136, 137, 139, 140, 141, 143, 144, 145, 147, 148, 149, 151, + 152, 153, 155, 156, 158, 159, 160, 162, 163, 164, 166, 167, 168, 170, 171, 173, + 174, 175, 177, 178, 179, 181, 182, 183, 185, 186, 187, 189, 190, 192, 193, 194, + 196, 197, 198, 200, 201, 202, 204, 205, 206, 208, 209, 211, 212, 213, 215, 216, + 217, 219, 220, 221, 223, 224, 225, 227, 228, 230, 231, 232, 234, 235, 236, 238, + 239, 240, 242, 243, 245, 246, 247, 249, 250, 251, 253, 254, 255, 257, 258, 259, + 261, 262, 264, 265, 266, 268, 269, 270, 272, 273, 274, 276, 277, 278, 280, 281, + 283, 284, 285, 287, 288, 289, 291, 292, 293, 295, 296, 297, 299, 300, 302, 303, + 304, 306, 307, 308, 310, 311, 312, 314, 315, 317, 318, 319, 321, 322, 323, 325, + 326, 327, 329, 330, 331, 333, 334, 336, 337, 338, 340, 341, 342, 344, 345, 346}; + +static INT16 CB[] = { + -345, -343, -341, -338, -336, -334, -332, -329, -327, -325, -323, -321, -318, -316, + -314, -312, -310, -307, -305, -303, -301, -298, -296, -294, -292, -290, -287, -285, + -283, -281, -278, -276, -274, -272, -270, -267, -265, -263, -261, -258, -256, -254, + -252, -250, -247, -245, -243, -241, -239, -236, -234, -232, -230, -227, -225, -223, + -221, -219, -216, -214, -212, -210, -207, -205, -203, -201, -199, -196, -194, -192, + -190, -188, -185, -183, -181, -179, -176, -174, -172, -170, -168, -165, -163, -161, + -159, -156, -154, -152, -150, -148, -145, -143, -141, -139, -137, -134, -132, -130, + -128, -125, -123, -121, -119, -117, -114, -112, -110, -108, -105, -103, -101, -99, + -97, -94, -92, -90, -88, -85, -83, -81, -79, -77, -74, -72, -70, -68, + -66, -63, -61, -59, -57, -54, -52, -50, -48, -46, -43, -41, -39, -37, + -34, -32, -30, -28, -26, -23, -21, -19, -17, -15, -12, -10, -8, -6, + -3, -1, 0, 2, 4, 7, 9, 11, 13, 16, 18, 20, 22, 24, + 27, 29, 31, 33, 35, 38, 40, 42, 44, 47, 49, 51, 53, 55, + 58, 60, 62, 64, 67, 69, 71, 73, 75, 78, 80, 82, 84, 86, + 89, 91, 93, 95, 98, 100, 102, 104, 106, 109, 111, 113, 115, 118, + 120, 122, 124, 126, 129, 131, 133, 135, 138, 140, 142, 144, 146, 149, + 151, 153, 155, 157, 160, 162, 164, 166, 169, 171, 173, 175, 177, 180, + 182, 184, 186, 189, 191, 193, 195, 197, 200, 202, 204, 206, 208, 211, + 213, 215, 217, 220}; + +static INT16 GB[] = { + 67, 67, 66, 66, 65, 65, 65, 64, 64, 63, 63, 62, 62, 62, 61, 61, + 60, 60, 59, 59, 59, 58, 58, 57, 57, 56, 56, 56, 55, 55, 54, 54, + 53, 53, 52, 52, 52, 51, 51, 50, 50, 49, 49, 49, 48, 48, 47, 47, + 46, 46, 46, 45, 45, 44, 44, 43, 43, 43, 42, 42, 41, 41, 40, 40, + 40, 39, 39, 38, 38, 37, 37, 37, 36, 36, 35, 35, 34, 34, 34, 33, + 33, 32, 32, 31, 31, 31, 30, 30, 29, 29, 28, 28, 28, 27, 27, 26, + 26, 25, 25, 25, 24, 24, 23, 23, 22, 22, 22, 21, 21, 20, 20, 19, + 19, 19, 18, 18, 17, 17, 16, 16, 15, 15, 15, 14, 14, 13, 13, 12, + 12, 12, 11, 11, 10, 10, 9, 9, 9, 8, 8, 7, 7, 6, 6, 6, + 5, 5, 4, 4, 3, 3, 3, 2, 2, 1, 1, 0, 0, 0, 0, 0, + -1, -1, -2, -2, -2, -3, -3, -4, -4, -5, -5, -5, -6, -6, -7, -7, + -8, -8, -8, -9, -9, -10, -10, -11, -11, -11, -12, -12, -13, -13, -14, -14, + -14, -15, -15, -16, -16, -17, -17, -18, -18, -18, -19, -19, -20, -20, -21, -21, + -21, -22, -22, -23, -23, -24, -24, -24, -25, -25, -26, -26, -27, -27, -27, -28, + -28, -29, -29, -30, -30, -30, -31, -31, -32, -32, -33, -33, -33, -34, -34, -35, + -35, -36, -36, -36, -37, -37, -38, -38, -39, -39, -39, -40, -40, -41, -41, -42}; + +static INT16 CR[] = { + -249, -247, -245, -243, -241, -239, -238, -236, -234, -232, -230, -229, -227, -225, + -223, -221, -219, -218, -216, -214, -212, -210, -208, -207, -205, -203, -201, -199, + -198, -196, -194, -192, -190, -188, -187, -185, -183, -181, -179, -178, -176, -174, + -172, -170, -168, -167, -165, -163, -161, -159, -157, -156, -154, -152, -150, -148, + -147, -145, -143, -141, -139, -137, -136, -134, -132, -130, -128, -127, -125, -123, + -121, -119, -117, -116, -114, -112, -110, -108, -106, -105, -103, -101, -99, -97, + -96, -94, -92, -90, -88, -86, -85, -83, -81, -79, -77, -76, -74, -72, + -70, -68, -66, -65, -63, -61, -59, -57, -55, -54, -52, -50, -48, -46, + -45, -43, -41, -39, -37, -35, -34, -32, -30, -28, -26, -25, -23, -21, + -19, -17, -15, -14, -12, -10, -8, -6, -4, -3, -1, 0, 2, 4, + 5, 7, 9, 11, 13, 15, 16, 18, 20, 22, 24, 26, 27, 29, + 31, 33, 35, 36, 38, 40, 42, 44, 46, 47, 49, 51, 53, 55, + 56, 58, 60, 62, 64, 66, 67, 69, 71, 73, 75, 77, 78, 80, + 82, 84, 86, 87, 89, 91, 93, 95, 97, 98, 100, 102, 104, 106, + 107, 109, 111, 113, 115, 117, 118, 120, 122, 124, 126, 128, 129, 131, + 133, 135, 137, 138, 140, 142, 144, 146, 148, 149, 151, 153, 155, 157, + 158, 160, 162, 164, 166, 168, 169, 171, 173, 175, 177, 179, 180, 182, + 184, 186, 188, 189, 191, 193, 195, 197, 199, 200, 202, 204, 206, 208, + 209, 211, 213, 215}; + +static INT16 GR[] = { + 127, 126, 125, 124, 123, 122, 121, 121, 120, 119, 118, 117, 116, 115, 114, + 113, 112, 111, 110, 109, 108, 108, 107, 106, 105, 104, 103, 102, 101, 100, + 99, 98, 97, 96, 95, 95, 94, 93, 92, 91, 90, 89, 88, 87, 86, + 85, 84, 83, 83, 82, 81, 80, 79, 78, 77, 76, 75, 74, 73, 72, + 71, 70, 70, 69, 68, 67, 66, 65, 64, 63, 62, 61, 60, 59, 58, + 57, 57, 56, 55, 54, 53, 52, 51, 50, 49, 48, 47, 46, 45, 45, + 44, 43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, 32, 32, 31, + 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 19, 18, 17, + 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 6, 5, 4, 3, + 2, 1, 0, 0, -1, -2, -3, -4, -5, -5, -6, -7, -8, -9, -10, + -11, -12, -13, -14, -15, -16, -17, -18, -18, -19, -20, -21, -22, -23, -24, + -25, -26, -27, -28, -29, -30, -31, -31, -32, -33, -34, -35, -36, -37, -38, + -39, -40, -41, -42, -43, -44, -44, -45, -46, -47, -48, -49, -50, -51, -52, + -53, -54, -55, -56, -56, -57, -58, -59, -60, -61, -62, -63, -64, -65, -66, + -67, -68, -69, -69, -70, -71, -72, -73, -74, -75, -76, -77, -78, -79, -80, + -81, -82, -82, -83, -84, -85, -86, -87, -88, -89, -90, -91, -92, -93, -94, + -94, -95, -96, -97, -98, -99, -100, -101, -102, -103, -104, -105, -106, -107, -107, + -108}; + +#define R 0 +#define G 1 +#define B 2 +#define A 3 + +#define YCC2RGB(rgb, y, cb, cr) \ + { \ + int l = L[y]; \ + int r = l + CR[cr]; \ + int g = l + GR[cr] + GB[cb]; \ + int b = l + CB[cb]; \ + rgb[0] = (r <= 0) ? 0 : (r >= 255) ? 255 : r; \ + rgb[1] = (g <= 0) ? 0 : (g >= 255) ? 255 : g; \ + rgb[2] = (b <= 0) ? 0 : (b >= 255) ? 255 : b; \ + } + +void +ImagingUnpackYCC(UINT8 *out, const UINT8 *in, int pixels) { + int i; + /* PhotoYCC triplets */ + for (i = 0; i < pixels; i++) { + YCC2RGB(out, in[0], in[1], in[2]); + out[A] = 255; + out += 4; + in += 3; + } +} + +void +ImagingUnpackYCCA(UINT8 *out, const UINT8 *in, int pixels) { + int i; + /* PhotoYCC triplets plus premultiplied alpha */ + for (i = 0; i < pixels; i++) { + /* Divide by alpha */ + UINT8 rgb[3]; + rgb[0] = (in[3] == 0) ? 0 : (((int)in[0] * 255) / in[3]); + rgb[1] = (in[3] == 0) ? 0 : (((int)in[1] * 255) / in[3]); + rgb[2] = (in[3] == 0) ? 0 : (((int)in[2] * 255) / in[3]); + /* Convert non-multiplied data to RGB */ + YCC2RGB(out, rgb[0], rgb[1], rgb[2]); + out[A] = in[3]; + out += 4; + in += 4; + } +} diff --git a/src/libImaging/UnsharpMask.c b/src/libImaging/UnsharpMask.c index 2853ce903..f60c5c503 100644 --- a/src/libImaging/UnsharpMask.c +++ b/src/libImaging/UnsharpMask.c @@ -1,97 +1,97 @@ -/* PILusm, a gaussian blur and unsharp masking library for PIL - By Kevin Cazabon, copyright 2003 - kevin_cazabon@hotmail.com - kevin@cazabon.com */ - -/* Originally released under LGPL. Graciously donated to PIL - for distribution under the standard PIL license in 2009." */ - -#include "Imaging.h" - -typedef UINT8 pixel[4]; - -static inline UINT8 -clip8(int in) { - if (in >= 255) { - return 255; - } - if (in <= 0) { - return 0; - } - return (UINT8)in; -} - -Imaging -ImagingUnsharpMask( - Imaging imOut, Imaging imIn, float radius, int percent, int threshold) { - ImagingSectionCookie cookie; - Imaging result; - - int x, y, diff; - - pixel *lineIn = NULL; - pixel *lineOut = NULL; - UINT8 *lineIn8 = NULL; - UINT8 *lineOut8 = NULL; - - /* First, do a gaussian blur on the image, putting results in imOut - temporarily. All format checks are in gaussian blur. */ - result = ImagingGaussianBlur(imOut, imIn, radius, radius, 3); - if (!result) { - return NULL; - } - - /* Now, go through each pixel, compare "normal" pixel to blurred - pixel. If the difference is more than threshold values, apply - the OPPOSITE correction to the amount of blur, multiplied by - percent. */ - - ImagingSectionEnter(&cookie); - - for (y = 0; y < imIn->ysize; y++) { - if (imIn->image8) { - lineIn8 = imIn->image8[y]; - lineOut8 = imOut->image8[y]; - for (x = 0; x < imIn->xsize; x++) { - /* compare in/out pixels, apply sharpening */ - diff = lineIn8[x] - lineOut8[x]; - if (abs(diff) > threshold) { - /* add the diff*percent to the original pixel */ - lineOut8[x] = clip8(lineIn8[x] + diff * percent / 100); - } else { - /* new pixel is the same as imIn */ - lineOut8[x] = lineIn8[x]; - } - } - } else { - lineIn = (pixel *)imIn->image32[y]; - lineOut = (pixel *)imOut->image32[y]; - for (x = 0; x < imIn->xsize; x++) { - /* compare in/out pixels, apply sharpening */ - diff = lineIn[x][0] - lineOut[x][0]; - lineOut[x][0] = abs(diff) > threshold - ? clip8(lineIn[x][0] + diff * percent / 100) - : lineIn[x][0]; - - diff = lineIn[x][1] - lineOut[x][1]; - lineOut[x][1] = abs(diff) > threshold - ? clip8(lineIn[x][1] + diff * percent / 100) - : lineIn[x][1]; - - diff = lineIn[x][2] - lineOut[x][2]; - lineOut[x][2] = abs(diff) > threshold - ? clip8(lineIn[x][2] + diff * percent / 100) - : lineIn[x][2]; - - diff = lineIn[x][3] - lineOut[x][3]; - lineOut[x][3] = abs(diff) > threshold - ? clip8(lineIn[x][3] + diff * percent / 100) - : lineIn[x][3]; - } - } - } - - ImagingSectionLeave(&cookie); - - return imOut; -} +/* PILusm, a gaussian blur and unsharp masking library for PIL + By Kevin Cazabon, copyright 2003 + kevin_cazabon@hotmail.com + kevin@cazabon.com */ + +/* Originally released under LGPL. Graciously donated to PIL + for distribution under the standard PIL license in 2009." */ + +#include "Imaging.h" + +typedef UINT8 pixel[4]; + +static inline UINT8 +clip8(int in) { + if (in >= 255) { + return 255; + } + if (in <= 0) { + return 0; + } + return (UINT8)in; +} + +Imaging +ImagingUnsharpMask( + Imaging imOut, Imaging imIn, float radius, int percent, int threshold) { + ImagingSectionCookie cookie; + Imaging result; + + int x, y, diff; + + pixel *lineIn = NULL; + pixel *lineOut = NULL; + UINT8 *lineIn8 = NULL; + UINT8 *lineOut8 = NULL; + + /* First, do a gaussian blur on the image, putting results in imOut + temporarily. All format checks are in gaussian blur. */ + result = ImagingGaussianBlur(imOut, imIn, radius, radius, 3); + if (!result) { + return NULL; + } + + /* Now, go through each pixel, compare "normal" pixel to blurred + pixel. If the difference is more than threshold values, apply + the OPPOSITE correction to the amount of blur, multiplied by + percent. */ + + ImagingSectionEnter(&cookie); + + for (y = 0; y < imIn->ysize; y++) { + if (imIn->image8) { + lineIn8 = imIn->image8[y]; + lineOut8 = imOut->image8[y]; + for (x = 0; x < imIn->xsize; x++) { + /* compare in/out pixels, apply sharpening */ + diff = lineIn8[x] - lineOut8[x]; + if (abs(diff) > threshold) { + /* add the diff*percent to the original pixel */ + lineOut8[x] = clip8(lineIn8[x] + diff * percent / 100); + } else { + /* new pixel is the same as imIn */ + lineOut8[x] = lineIn8[x]; + } + } + } else { + lineIn = (pixel *)imIn->image32[y]; + lineOut = (pixel *)imOut->image32[y]; + for (x = 0; x < imIn->xsize; x++) { + /* compare in/out pixels, apply sharpening */ + diff = lineIn[x][0] - lineOut[x][0]; + lineOut[x][0] = abs(diff) > threshold + ? clip8(lineIn[x][0] + diff * percent / 100) + : lineIn[x][0]; + + diff = lineIn[x][1] - lineOut[x][1]; + lineOut[x][1] = abs(diff) > threshold + ? clip8(lineIn[x][1] + diff * percent / 100) + : lineIn[x][1]; + + diff = lineIn[x][2] - lineOut[x][2]; + lineOut[x][2] = abs(diff) > threshold + ? clip8(lineIn[x][2] + diff * percent / 100) + : lineIn[x][2]; + + diff = lineIn[x][3] - lineOut[x][3]; + lineOut[x][3] = abs(diff) > threshold + ? clip8(lineIn[x][3] + diff * percent / 100) + : lineIn[x][3]; + } + } + } + + ImagingSectionLeave(&cookie); + + return imOut; +} diff --git a/src/libImaging/XbmDecode.c b/src/libImaging/XbmDecode.c index d6690de3d..6f2b6e3a1 100644 --- a/src/libImaging/XbmDecode.c +++ b/src/libImaging/XbmDecode.c @@ -1,78 +1,78 @@ -/* - * The Python Imaging Library. - * $Id$ - * - * decoder for XBM hex image data - * - * history: - * 96-04-13 fl Created - * - * Copyright (c) Fredrik Lundh 1996. - * Copyright (c) Secret Labs AB 1997. - * - * See the README file for information on usage and redistribution. - */ - -#include "Imaging.h" - -#define HEX(v) \ - ((v >= '0' && v <= '9') ? v - '0' \ - : (v >= 'a' && v <= 'f') ? v - 'a' + 10 \ - : (v >= 'A' && v <= 'F') ? v - 'A' + 10 \ - : 0) - -int -ImagingXbmDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) { - enum { BYTE = 1, SKIP }; - - UINT8 *ptr; - - if (!state->state) { - state->state = SKIP; - } - - ptr = buf; - - for (;;) { - if (state->state == SKIP) { - /* Skip forward until next 'x' */ - - while (bytes > 0) { - if (*ptr == 'x') { - break; - } - ptr++; - bytes--; - } - - if (bytes == 0) { - return ptr - buf; - } - - state->state = BYTE; - } - - if (bytes < 3) { - return ptr - buf; - } - - state->buffer[state->x] = (HEX(ptr[1]) << 4) + HEX(ptr[2]); - - if (++state->x >= state->bytes) { - /* Got a full line, unpack it */ - state->shuffle((UINT8 *)im->image[state->y], state->buffer, state->xsize); - - state->x = 0; - - if (++state->y >= state->ysize) { - /* End of file (errcode = 0) */ - return -1; - } - } - - ptr += 3; - bytes -= 3; - - state->state = SKIP; - } -} +/* + * The Python Imaging Library. + * $Id$ + * + * decoder for XBM hex image data + * + * history: + * 96-04-13 fl Created + * + * Copyright (c) Fredrik Lundh 1996. + * Copyright (c) Secret Labs AB 1997. + * + * See the README file for information on usage and redistribution. + */ + +#include "Imaging.h" + +#define HEX(v) \ + ((v >= '0' && v <= '9') ? v - '0' \ + : (v >= 'a' && v <= 'f') ? v - 'a' + 10 \ + : (v >= 'A' && v <= 'F') ? v - 'A' + 10 \ + : 0) + +int +ImagingXbmDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) { + enum { BYTE = 1, SKIP }; + + UINT8 *ptr; + + if (!state->state) { + state->state = SKIP; + } + + ptr = buf; + + for (;;) { + if (state->state == SKIP) { + /* Skip forward until next 'x' */ + + while (bytes > 0) { + if (*ptr == 'x') { + break; + } + ptr++; + bytes--; + } + + if (bytes == 0) { + return ptr - buf; + } + + state->state = BYTE; + } + + if (bytes < 3) { + return ptr - buf; + } + + state->buffer[state->x] = (HEX(ptr[1]) << 4) + HEX(ptr[2]); + + if (++state->x >= state->bytes) { + /* Got a full line, unpack it */ + state->shuffle((UINT8 *)im->image[state->y], state->buffer, state->xsize); + + state->x = 0; + + if (++state->y >= state->ysize) { + /* End of file (errcode = 0) */ + return -1; + } + } + + ptr += 3; + bytes -= 3; + + state->state = SKIP; + } +} diff --git a/src/libImaging/XbmEncode.c b/src/libImaging/XbmEncode.c index eec4c0d84..e3cfb23fe 100644 --- a/src/libImaging/XbmEncode.c +++ b/src/libImaging/XbmEncode.c @@ -1,96 +1,96 @@ -/* - * The Python Imaging Library. - * $Id$ - * - * encoder for Xbm data - * - * history: - * 96-11-01 fl created - * - * Copyright (c) Fredrik Lundh 1996. - * Copyright (c) Secret Labs AB 1997. - * - * See the README file for information on usage and redistribution. - */ - -#include "Imaging.h" - -int -ImagingXbmEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { - const char *hex = "0123456789abcdef"; - - UINT8 *ptr = buf; - int i, n; - - if (!state->state) { - /* 8 pixels are stored in no more than 6 bytes */ - state->bytes = 6 * (state->xsize + 7) / 8; - - state->state = 1; - } - - if (bytes < state->bytes) { - state->errcode = IMAGING_CODEC_MEMORY; - return 0; - } - - ptr = buf; - - while (bytes >= state->bytes) { - state->shuffle( - state->buffer, - (UINT8 *)im->image[state->y + state->yoff] + state->xoff * im->pixelsize, - state->xsize); - - if (state->y < state->ysize - 1) { - /* any line but the last */ - for (n = 0; n < state->xsize; n += 8) { - i = state->buffer[n / 8]; - - *ptr++ = '0'; - *ptr++ = 'x'; - *ptr++ = hex[(i >> 4) & 15]; - *ptr++ = hex[i & 15]; - *ptr++ = ','; - bytes -= 5; - - if (++state->count >= 79 / 5) { - *ptr++ = '\n'; - bytes--; - state->count = 0; - } - } - - state->y++; - - } else { - /* last line */ - for (n = 0; n < state->xsize; n += 8) { - i = state->buffer[n / 8]; - - *ptr++ = '0'; - *ptr++ = 'x'; - *ptr++ = hex[(i >> 4) & 15]; - *ptr++ = hex[i & 15]; - - if (n < state->xsize - 8) { - *ptr++ = ','; - if (++state->count >= 79 / 5) { - *ptr++ = '\n'; - bytes--; - state->count = 0; - } - } else { - *ptr++ = '\n'; - } - - bytes -= 5; - } - - state->errcode = IMAGING_CODEC_END; - break; - } - } - - return ptr - buf; -} +/* + * The Python Imaging Library. + * $Id$ + * + * encoder for Xbm data + * + * history: + * 96-11-01 fl created + * + * Copyright (c) Fredrik Lundh 1996. + * Copyright (c) Secret Labs AB 1997. + * + * See the README file for information on usage and redistribution. + */ + +#include "Imaging.h" + +int +ImagingXbmEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { + const char *hex = "0123456789abcdef"; + + UINT8 *ptr = buf; + int i, n; + + if (!state->state) { + /* 8 pixels are stored in no more than 6 bytes */ + state->bytes = 6 * (state->xsize + 7) / 8; + + state->state = 1; + } + + if (bytes < state->bytes) { + state->errcode = IMAGING_CODEC_MEMORY; + return 0; + } + + ptr = buf; + + while (bytes >= state->bytes) { + state->shuffle( + state->buffer, + (UINT8 *)im->image[state->y + state->yoff] + state->xoff * im->pixelsize, + state->xsize); + + if (state->y < state->ysize - 1) { + /* any line but the last */ + for (n = 0; n < state->xsize; n += 8) { + i = state->buffer[n / 8]; + + *ptr++ = '0'; + *ptr++ = 'x'; + *ptr++ = hex[(i >> 4) & 15]; + *ptr++ = hex[i & 15]; + *ptr++ = ','; + bytes -= 5; + + if (++state->count >= 79 / 5) { + *ptr++ = '\n'; + bytes--; + state->count = 0; + } + } + + state->y++; + + } else { + /* last line */ + for (n = 0; n < state->xsize; n += 8) { + i = state->buffer[n / 8]; + + *ptr++ = '0'; + *ptr++ = 'x'; + *ptr++ = hex[(i >> 4) & 15]; + *ptr++ = hex[i & 15]; + + if (n < state->xsize - 8) { + *ptr++ = ','; + if (++state->count >= 79 / 5) { + *ptr++ = '\n'; + bytes--; + state->count = 0; + } + } else { + *ptr++ = '\n'; + } + + bytes -= 5; + } + + state->errcode = IMAGING_CODEC_END; + break; + } + } + + return ptr - buf; +} diff --git a/src/libImaging/ZipCodecs.h b/src/libImaging/ZipCodecs.h index 50218b6c6..ee67e1336 100644 --- a/src/libImaging/ZipCodecs.h +++ b/src/libImaging/ZipCodecs.h @@ -1,58 +1,58 @@ -/* - * The Python Imaging Library. - * $Id$ - * - * declarations for the ZIP codecs - * - * Copyright (c) Fredrik Lundh 1996. - */ - -#include "zlib.h" - -/* modes */ -#define ZIP_PNG 0 /* continuous, filtered image data */ -#define ZIP_PNG_PALETTE 1 /* non-continuous data, disable filtering */ -#define ZIP_TIFF_PREDICTOR 2 /* TIFF, with predictor */ -#define ZIP_TIFF 3 /* TIFF, without predictor */ - -typedef struct { - /* CONFIGURATION */ - - /* Codec mode */ - int mode; - - /* Optimize (max compression) SLOW!!! */ - int optimize; - - /* 0 no compression, 9 best compression, -1 default compression */ - int compress_level; - /* compression strategy Z_XXX */ - int compress_type; - - /* Predefined dictionary (experimental) */ - char *dictionary; - int dictionary_size; - - /* PRIVATE CONTEXT (set by decoder/encoder) */ - - z_stream z_stream; /* (de)compression stream */ - - UINT8 *previous; /* previous line (allocated) */ - - int last_output; /* # bytes last output by inflate */ - - /* Compressor specific stuff */ - UINT8 *prior; /* filter storage (allocated) */ - UINT8 *up; - UINT8 *average; - UINT8 *paeth; - - UINT8 *output; /* output data */ - - int prefix; /* size of filter prefix (0 for TIFF data) */ - - int interlaced; /* is the image interlaced? (PNG) */ - - int pass; /* current pass of the interlaced image (PNG) */ - -} ZIPSTATE; +/* + * The Python Imaging Library. + * $Id$ + * + * declarations for the ZIP codecs + * + * Copyright (c) Fredrik Lundh 1996. + */ + +#include "zlib.h" + +/* modes */ +#define ZIP_PNG 0 /* continuous, filtered image data */ +#define ZIP_PNG_PALETTE 1 /* non-continuous data, disable filtering */ +#define ZIP_TIFF_PREDICTOR 2 /* TIFF, with predictor */ +#define ZIP_TIFF 3 /* TIFF, without predictor */ + +typedef struct { + /* CONFIGURATION */ + + /* Codec mode */ + int mode; + + /* Optimize (max compression) SLOW!!! */ + int optimize; + + /* 0 no compression, 9 best compression, -1 default compression */ + int compress_level; + /* compression strategy Z_XXX */ + int compress_type; + + /* Predefined dictionary (experimental) */ + char *dictionary; + int dictionary_size; + + /* PRIVATE CONTEXT (set by decoder/encoder) */ + + z_stream z_stream; /* (de)compression stream */ + + UINT8 *previous; /* previous line (allocated) */ + + int last_output; /* # bytes last output by inflate */ + + /* Compressor specific stuff */ + UINT8 *prior; /* filter storage (allocated) */ + UINT8 *up; + UINT8 *average; + UINT8 *paeth; + + UINT8 *output; /* output data */ + + int prefix; /* size of filter prefix (0 for TIFF data) */ + + int interlaced; /* is the image interlaced? (PNG) */ + + int pass; /* current pass of the interlaced image (PNG) */ + +} ZIPSTATE; diff --git a/src/libImaging/ZipDecode.c b/src/libImaging/ZipDecode.c index 874967834..1452dbcbf 100644 --- a/src/libImaging/ZipDecode.c +++ b/src/libImaging/ZipDecode.c @@ -1,299 +1,299 @@ -/* - * The Python Imaging Library. - * $Id$ - * - * decoder for ZIP (deflated) image data. - * - * history: - * 1996-12-14 fl Created (for PNG) - * 1997-01-15 fl Prepared to read TIFF/ZIP - * 2001-11-19 fl PNG incomplete read patch (from Bernhard Herzog) - * - * Copyright (c) Fredrik Lundh 1996. - * Copyright (c) Secret Labs AB 1997-2001. - * - * See the README file for information on usage and redistribution. - */ - -#include "Imaging.h" - -#ifdef HAVE_LIBZ - -#include "ZipCodecs.h" - -static const int OFFSET[] = {7, 3, 3, 1, 1, 0, 0}; -static const int STARTING_COL[] = {0, 4, 0, 2, 0, 1, 0}; -static const int STARTING_ROW[] = {0, 0, 4, 0, 2, 0, 1}; -static const int COL_INCREMENT[] = {8, 8, 4, 4, 2, 2, 1}; -static const int ROW_INCREMENT[] = {8, 8, 8, 4, 4, 2, 2}; - -/* Get the length in bytes of a scanline in the pass specified, - * for interlaced images */ -static int -get_row_len(ImagingCodecState state, int pass) { - int row_len = (state->xsize + OFFSET[pass]) / COL_INCREMENT[pass]; - return ((row_len * state->bits) + 7) / 8; -} - -/* -------------------------------------------------------------------- */ -/* Decoder */ -/* -------------------------------------------------------------------- */ - -int -ImagingZipDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) { - ZIPSTATE *context = (ZIPSTATE *)state->context; - int err; - int n; - UINT8 *ptr; - int i, bpp; - int row_len; - - if (!state->state) { - /* Initialization */ - if (context->mode == ZIP_PNG || context->mode == ZIP_PNG_PALETTE) { - context->prefix = 1; /* PNG */ - } - - /* overflow check for malloc */ - if (state->bytes > INT_MAX - 1) { - state->errcode = IMAGING_CODEC_MEMORY; - return -1; - } - /* Expand standard buffer to make room for the (optional) filter - prefix, and allocate a buffer to hold the previous line */ - free(state->buffer); - /* malloc check ok, overflow checked above */ - state->buffer = (UINT8 *)malloc(state->bytes + 1); - context->previous = (UINT8 *)malloc(state->bytes + 1); - if (!state->buffer || !context->previous) { - state->errcode = IMAGING_CODEC_MEMORY; - return -1; - } - - context->last_output = 0; - - /* Initialize to black */ - memset(context->previous, 0, state->bytes + 1); - - /* Setup decompression context */ - context->z_stream.zalloc = (alloc_func)NULL; - context->z_stream.zfree = (free_func)NULL; - context->z_stream.opaque = (voidpf)NULL; - - err = inflateInit(&context->z_stream); - if (err < 0) { - state->errcode = IMAGING_CODEC_CONFIG; - free(context->previous); - context->previous = NULL; - return -1; - } - - if (context->interlaced) { - context->pass = 0; - state->y = STARTING_ROW[context->pass]; - } - - /* Ready to decode */ - state->state = 1; - } - - if (context->interlaced) { - row_len = get_row_len(state, context->pass); - } else { - row_len = state->bytes; - } - - /* Setup the source buffer */ - context->z_stream.next_in = buf; - context->z_stream.avail_in = bytes; - - /* Decompress what we've got this far */ - while (context->z_stream.avail_in > 0) { - context->z_stream.next_out = state->buffer + context->last_output; - context->z_stream.avail_out = row_len + context->prefix - context->last_output; - - err = inflate(&context->z_stream, Z_NO_FLUSH); - - if (err < 0) { - /* Something went wrong inside the compression library */ - if (err == Z_DATA_ERROR) { - state->errcode = IMAGING_CODEC_BROKEN; - } else if (err == Z_MEM_ERROR) { - state->errcode = IMAGING_CODEC_MEMORY; - } else { - state->errcode = IMAGING_CODEC_CONFIG; - } - free(context->previous); - context->previous = NULL; - inflateEnd(&context->z_stream); - return -1; - } - - n = row_len + context->prefix - context->z_stream.avail_out; - - if (n < row_len + context->prefix) { - context->last_output = n; - break; /* need more input data */ - } - - /* Apply predictor */ - switch (context->mode) { - case ZIP_PNG: - switch (state->buffer[0]) { - case 0: - break; - case 1: - /* prior */ - bpp = (state->bits + 7) / 8; - for (i = bpp + 1; i <= row_len; i++) { - state->buffer[i] += state->buffer[i - bpp]; - } - break; - case 2: - /* up */ - for (i = 1; i <= row_len; i++) { - state->buffer[i] += context->previous[i]; - } - break; - case 3: - /* average */ - bpp = (state->bits + 7) / 8; - for (i = 1; i <= bpp; i++) { - state->buffer[i] += context->previous[i] / 2; - } - for (; i <= row_len; i++) { - state->buffer[i] += - (state->buffer[i - bpp] + context->previous[i]) / 2; - } - break; - case 4: - /* paeth filtering */ - bpp = (state->bits + 7) / 8; - for (i = 1; i <= bpp; i++) { - state->buffer[i] += context->previous[i]; - } - for (; i <= row_len; i++) { - int a, b, c; - int pa, pb, pc; - - /* fetch pixels */ - a = state->buffer[i - bpp]; - b = context->previous[i]; - c = context->previous[i - bpp]; - - /* distances to surrounding pixels */ - pa = abs(b - c); - pb = abs(a - c); - pc = abs(a + b - 2 * c); - - /* pick predictor with the shortest distance */ - state->buffer[i] += (pa <= pb && pa <= pc) ? a - : (pb <= pc) ? b - : c; - } - break; - default: - state->errcode = IMAGING_CODEC_UNKNOWN; - free(context->previous); - context->previous = NULL; - inflateEnd(&context->z_stream); - return -1; - } - break; - case ZIP_TIFF_PREDICTOR: - bpp = (state->bits + 7) / 8; - for (i = bpp + 1; i <= row_len; i++) { - state->buffer[i] += state->buffer[i - bpp]; - } - break; - } - - /* Stuff data into the image */ - if (context->interlaced) { - int col = STARTING_COL[context->pass]; - if (state->bits >= 8) { - /* Stuff pixels in their correct location, one by one */ - for (i = 0; i < row_len; i += ((state->bits + 7) / 8)) { - state->shuffle( - (UINT8 *)im->image[state->y] + col * im->pixelsize, - state->buffer + context->prefix + i, - 1); - col += COL_INCREMENT[context->pass]; - } - } else { - /* Handle case with more than a pixel in each byte */ - int row_bits = ((state->xsize + OFFSET[context->pass]) / - COL_INCREMENT[context->pass]) * - state->bits; - for (i = 0; i < row_bits; i += state->bits) { - UINT8 byte = *(state->buffer + context->prefix + (i / 8)); - byte <<= (i % 8); - state->shuffle( - (UINT8 *)im->image[state->y] + col * im->pixelsize, &byte, 1); - col += COL_INCREMENT[context->pass]; - } - } - /* Find next valid scanline */ - state->y += ROW_INCREMENT[context->pass]; - while (state->y >= state->ysize || row_len <= 0) { - context->pass++; - if (context->pass == 7) { - /* Force exit below */ - state->y = state->ysize; - break; - } - state->y = STARTING_ROW[context->pass]; - row_len = get_row_len(state, context->pass); - /* Since we're moving to the "first" line, the previous line - * should be black to make filters work correctly */ - memset(state->buffer, 0, state->bytes + 1); - } - } else { - state->shuffle( - (UINT8 *)im->image[state->y + state->yoff] + - state->xoff * im->pixelsize, - state->buffer + context->prefix, - state->xsize); - state->y++; - } - - /* all inflate output has been consumed */ - context->last_output = 0; - - if (state->y >= state->ysize || err == Z_STREAM_END) { - /* The image and the data should end simultaneously */ - /* if (state->y < state->ysize || err != Z_STREAM_END) - state->errcode = IMAGING_CODEC_BROKEN; */ - - free(context->previous); - context->previous = NULL; - inflateEnd(&context->z_stream); - return -1; /* end of file (errcode=0) */ - } - - /* Swap buffer pointers */ - ptr = state->buffer; - state->buffer = context->previous; - context->previous = ptr; - } - - return bytes; /* consumed all of it */ -} - -int -ImagingZipDecodeCleanup(ImagingCodecState state) { - /* called to free the decompression engine when the decode terminates - due to a corrupt or truncated image - */ - ZIPSTATE *context = (ZIPSTATE *)state->context; - - /* Clean up */ - if (context->previous) { - inflateEnd(&context->z_stream); - free(context->previous); - context->previous = NULL; - } - return -1; -} - -#endif +/* + * The Python Imaging Library. + * $Id$ + * + * decoder for ZIP (deflated) image data. + * + * history: + * 1996-12-14 fl Created (for PNG) + * 1997-01-15 fl Prepared to read TIFF/ZIP + * 2001-11-19 fl PNG incomplete read patch (from Bernhard Herzog) + * + * Copyright (c) Fredrik Lundh 1996. + * Copyright (c) Secret Labs AB 1997-2001. + * + * See the README file for information on usage and redistribution. + */ + +#include "Imaging.h" + +#ifdef HAVE_LIBZ + +#include "ZipCodecs.h" + +static const int OFFSET[] = {7, 3, 3, 1, 1, 0, 0}; +static const int STARTING_COL[] = {0, 4, 0, 2, 0, 1, 0}; +static const int STARTING_ROW[] = {0, 0, 4, 0, 2, 0, 1}; +static const int COL_INCREMENT[] = {8, 8, 4, 4, 2, 2, 1}; +static const int ROW_INCREMENT[] = {8, 8, 8, 4, 4, 2, 2}; + +/* Get the length in bytes of a scanline in the pass specified, + * for interlaced images */ +static int +get_row_len(ImagingCodecState state, int pass) { + int row_len = (state->xsize + OFFSET[pass]) / COL_INCREMENT[pass]; + return ((row_len * state->bits) + 7) / 8; +} + +/* -------------------------------------------------------------------- */ +/* Decoder */ +/* -------------------------------------------------------------------- */ + +int +ImagingZipDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) { + ZIPSTATE *context = (ZIPSTATE *)state->context; + int err; + int n; + UINT8 *ptr; + int i, bpp; + int row_len; + + if (!state->state) { + /* Initialization */ + if (context->mode == ZIP_PNG || context->mode == ZIP_PNG_PALETTE) { + context->prefix = 1; /* PNG */ + } + + /* overflow check for malloc */ + if (state->bytes > INT_MAX - 1) { + state->errcode = IMAGING_CODEC_MEMORY; + return -1; + } + /* Expand standard buffer to make room for the (optional) filter + prefix, and allocate a buffer to hold the previous line */ + free(state->buffer); + /* malloc check ok, overflow checked above */ + state->buffer = (UINT8 *)malloc(state->bytes + 1); + context->previous = (UINT8 *)malloc(state->bytes + 1); + if (!state->buffer || !context->previous) { + state->errcode = IMAGING_CODEC_MEMORY; + return -1; + } + + context->last_output = 0; + + /* Initialize to black */ + memset(context->previous, 0, state->bytes + 1); + + /* Setup decompression context */ + context->z_stream.zalloc = (alloc_func)NULL; + context->z_stream.zfree = (free_func)NULL; + context->z_stream.opaque = (voidpf)NULL; + + err = inflateInit(&context->z_stream); + if (err < 0) { + state->errcode = IMAGING_CODEC_CONFIG; + free(context->previous); + context->previous = NULL; + return -1; + } + + if (context->interlaced) { + context->pass = 0; + state->y = STARTING_ROW[context->pass]; + } + + /* Ready to decode */ + state->state = 1; + } + + if (context->interlaced) { + row_len = get_row_len(state, context->pass); + } else { + row_len = state->bytes; + } + + /* Setup the source buffer */ + context->z_stream.next_in = buf; + context->z_stream.avail_in = bytes; + + /* Decompress what we've got this far */ + while (context->z_stream.avail_in > 0) { + context->z_stream.next_out = state->buffer + context->last_output; + context->z_stream.avail_out = row_len + context->prefix - context->last_output; + + err = inflate(&context->z_stream, Z_NO_FLUSH); + + if (err < 0) { + /* Something went wrong inside the compression library */ + if (err == Z_DATA_ERROR) { + state->errcode = IMAGING_CODEC_BROKEN; + } else if (err == Z_MEM_ERROR) { + state->errcode = IMAGING_CODEC_MEMORY; + } else { + state->errcode = IMAGING_CODEC_CONFIG; + } + free(context->previous); + context->previous = NULL; + inflateEnd(&context->z_stream); + return -1; + } + + n = row_len + context->prefix - context->z_stream.avail_out; + + if (n < row_len + context->prefix) { + context->last_output = n; + break; /* need more input data */ + } + + /* Apply predictor */ + switch (context->mode) { + case ZIP_PNG: + switch (state->buffer[0]) { + case 0: + break; + case 1: + /* prior */ + bpp = (state->bits + 7) / 8; + for (i = bpp + 1; i <= row_len; i++) { + state->buffer[i] += state->buffer[i - bpp]; + } + break; + case 2: + /* up */ + for (i = 1; i <= row_len; i++) { + state->buffer[i] += context->previous[i]; + } + break; + case 3: + /* average */ + bpp = (state->bits + 7) / 8; + for (i = 1; i <= bpp; i++) { + state->buffer[i] += context->previous[i] / 2; + } + for (; i <= row_len; i++) { + state->buffer[i] += + (state->buffer[i - bpp] + context->previous[i]) / 2; + } + break; + case 4: + /* paeth filtering */ + bpp = (state->bits + 7) / 8; + for (i = 1; i <= bpp; i++) { + state->buffer[i] += context->previous[i]; + } + for (; i <= row_len; i++) { + int a, b, c; + int pa, pb, pc; + + /* fetch pixels */ + a = state->buffer[i - bpp]; + b = context->previous[i]; + c = context->previous[i - bpp]; + + /* distances to surrounding pixels */ + pa = abs(b - c); + pb = abs(a - c); + pc = abs(a + b - 2 * c); + + /* pick predictor with the shortest distance */ + state->buffer[i] += (pa <= pb && pa <= pc) ? a + : (pb <= pc) ? b + : c; + } + break; + default: + state->errcode = IMAGING_CODEC_UNKNOWN; + free(context->previous); + context->previous = NULL; + inflateEnd(&context->z_stream); + return -1; + } + break; + case ZIP_TIFF_PREDICTOR: + bpp = (state->bits + 7) / 8; + for (i = bpp + 1; i <= row_len; i++) { + state->buffer[i] += state->buffer[i - bpp]; + } + break; + } + + /* Stuff data into the image */ + if (context->interlaced) { + int col = STARTING_COL[context->pass]; + if (state->bits >= 8) { + /* Stuff pixels in their correct location, one by one */ + for (i = 0; i < row_len; i += ((state->bits + 7) / 8)) { + state->shuffle( + (UINT8 *)im->image[state->y] + col * im->pixelsize, + state->buffer + context->prefix + i, + 1); + col += COL_INCREMENT[context->pass]; + } + } else { + /* Handle case with more than a pixel in each byte */ + int row_bits = ((state->xsize + OFFSET[context->pass]) / + COL_INCREMENT[context->pass]) * + state->bits; + for (i = 0; i < row_bits; i += state->bits) { + UINT8 byte = *(state->buffer + context->prefix + (i / 8)); + byte <<= (i % 8); + state->shuffle( + (UINT8 *)im->image[state->y] + col * im->pixelsize, &byte, 1); + col += COL_INCREMENT[context->pass]; + } + } + /* Find next valid scanline */ + state->y += ROW_INCREMENT[context->pass]; + while (state->y >= state->ysize || row_len <= 0) { + context->pass++; + if (context->pass == 7) { + /* Force exit below */ + state->y = state->ysize; + break; + } + state->y = STARTING_ROW[context->pass]; + row_len = get_row_len(state, context->pass); + /* Since we're moving to the "first" line, the previous line + * should be black to make filters work correctly */ + memset(state->buffer, 0, state->bytes + 1); + } + } else { + state->shuffle( + (UINT8 *)im->image[state->y + state->yoff] + + state->xoff * im->pixelsize, + state->buffer + context->prefix, + state->xsize); + state->y++; + } + + /* all inflate output has been consumed */ + context->last_output = 0; + + if (state->y >= state->ysize || err == Z_STREAM_END) { + /* The image and the data should end simultaneously */ + /* if (state->y < state->ysize || err != Z_STREAM_END) + state->errcode = IMAGING_CODEC_BROKEN; */ + + free(context->previous); + context->previous = NULL; + inflateEnd(&context->z_stream); + return -1; /* end of file (errcode=0) */ + } + + /* Swap buffer pointers */ + ptr = state->buffer; + state->buffer = context->previous; + context->previous = ptr; + } + + return bytes; /* consumed all of it */ +} + +int +ImagingZipDecodeCleanup(ImagingCodecState state) { + /* called to free the decompression engine when the decode terminates + due to a corrupt or truncated image + */ + ZIPSTATE *context = (ZIPSTATE *)state->context; + + /* Clean up */ + if (context->previous) { + inflateEnd(&context->z_stream); + free(context->previous); + context->previous = NULL; + } + return -1; +} + +#endif diff --git a/src/libImaging/ZipEncode.c b/src/libImaging/ZipEncode.c index edbce3682..168715772 100644 --- a/src/libImaging/ZipEncode.c +++ b/src/libImaging/ZipEncode.c @@ -1,367 +1,367 @@ -/* - * The Python Imaging Library. - * $Id$ - * - * coder for ZIP (deflated) image data - * - * History: - * 96-12-29 fl created - * 96-12-30 fl adaptive filter selection, encoder tuning - * - * Copyright (c) Fredrik Lundh 1996. - * Copyright (c) Secret Labs AB 1997. - * - * See the README file for information on usage and redistribution. - */ - -#include "Imaging.h" - -#ifdef HAVE_LIBZ - -#include "ZipCodecs.h" - -int -ImagingZipEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { - ZIPSTATE *context = (ZIPSTATE *)state->context; - int err; - int compress_level, compress_type; - UINT8 *ptr; - int i, bpp, s, sum; - ImagingSectionCookie cookie; - - if (!state->state) { - /* Initialization */ - - /* Valid modes are ZIP_PNG, ZIP_PNG_PALETTE, and ZIP_TIFF */ - - /* overflow check for malloc */ - if (state->bytes > INT_MAX - 1) { - state->errcode = IMAGING_CODEC_MEMORY; - return -1; - } - - /* Expand standard buffer to make room for the filter selector, - and allocate filter buffers */ - free(state->buffer); - /* malloc check ok, overflow checked above */ - state->buffer = (UINT8 *)malloc(state->bytes + 1); - context->previous = (UINT8 *)malloc(state->bytes + 1); - context->prior = (UINT8 *)malloc(state->bytes + 1); - context->up = (UINT8 *)malloc(state->bytes + 1); - context->average = (UINT8 *)malloc(state->bytes + 1); - context->paeth = (UINT8 *)malloc(state->bytes + 1); - if (!state->buffer || !context->previous || !context->prior || !context->up || - !context->average || !context->paeth) { - free(context->paeth); - free(context->average); - free(context->up); - free(context->prior); - free(context->previous); - state->errcode = IMAGING_CODEC_MEMORY; - return -1; - } - - /* Initialise filter buffers */ - state->buffer[0] = 0; - context->prior[0] = 1; - context->up[0] = 2; - context->average[0] = 3; - context->paeth[0] = 4; - - /* Initialise previous buffer to black */ - memset(context->previous, 0, state->bytes + 1); - - /* Setup compression context */ - context->z_stream.zalloc = (alloc_func)0; - context->z_stream.zfree = (free_func)0; - context->z_stream.opaque = (voidpf)0; - context->z_stream.next_in = 0; - context->z_stream.avail_in = 0; - - compress_level = - (context->optimize) ? Z_BEST_COMPRESSION : context->compress_level; - - if (context->compress_type == -1) { - compress_type = - (context->mode == ZIP_PNG) ? Z_FILTERED : Z_DEFAULT_STRATEGY; - } else { - compress_type = context->compress_type; - } - - err = deflateInit2( - &context->z_stream, - /* compression level */ - compress_level, - /* compression method */ - Z_DEFLATED, - /* compression memory resources */ - 15, - 9, - /* compression strategy (image data are filtered)*/ - compress_type); - if (err < 0) { - state->errcode = IMAGING_CODEC_CONFIG; - return -1; - } - - if (context->dictionary && context->dictionary_size > 0) { - err = deflateSetDictionary( - &context->z_stream, - (unsigned char *)context->dictionary, - context->dictionary_size); - if (err < 0) { - state->errcode = IMAGING_CODEC_CONFIG; - return -1; - } - } - - /* Ready to decode */ - state->state = 1; - } - - /* Setup the destination buffer */ - context->z_stream.next_out = buf; - context->z_stream.avail_out = bytes; - if (context->z_stream.next_in && context->z_stream.avail_in > 0) { - /* We have some data from previous round, deflate it first */ - err = deflate(&context->z_stream, Z_NO_FLUSH); - - if (err < 0) { - /* Something went wrong inside the compression library */ - if (err == Z_DATA_ERROR) { - state->errcode = IMAGING_CODEC_BROKEN; - } else if (err == Z_MEM_ERROR) { - state->errcode = IMAGING_CODEC_MEMORY; - } else { - state->errcode = IMAGING_CODEC_CONFIG; - } - free(context->paeth); - free(context->average); - free(context->up); - free(context->prior); - free(context->previous); - deflateEnd(&context->z_stream); - return -1; - } - } - - ImagingSectionEnter(&cookie); - for (;;) { - switch (state->state) { - case 1: - - /* Compress image data */ - while (context->z_stream.avail_out > 0) { - if (state->y >= state->ysize) { - /* End of image; now flush compressor buffers */ - state->state = 2; - break; - } - - /* Stuff image data into the compressor */ - state->shuffle( - state->buffer + 1, - (UINT8 *)im->image[state->y + state->yoff] + - state->xoff * im->pixelsize, - state->xsize); - - state->y++; - - context->output = state->buffer; - - if (context->mode == ZIP_PNG) { - /* Filter the image data. For each line, select - the filter that gives the least total distance - from zero for the filtered data (taken from - LIBPNG) */ - - bpp = (state->bits + 7) / 8; - - /* 0. No filter */ - for (i = 1, sum = 0; i <= state->bytes; i++) { - UINT8 v = state->buffer[i]; - sum += (v < 128) ? v : 256 - v; - } - - /* 2. Up. We'll test this first to save time when - an image line is identical to the one above. */ - if (sum > 0) { - for (i = 1, s = 0; i <= state->bytes; i++) { - UINT8 v = state->buffer[i] - context->previous[i]; - context->up[i] = v; - s += (v < 128) ? v : 256 - v; - } - if (s < sum) { - context->output = context->up; - sum = s; /* 0 if line was duplicated */ - } - } - - /* 1. Prior */ - if (sum > 0) { - for (i = 1, s = 0; i <= bpp; i++) { - UINT8 v = state->buffer[i]; - context->prior[i] = v; - s += (v < 128) ? v : 256 - v; - } - for (; i <= state->bytes; i++) { - UINT8 v = state->buffer[i] - state->buffer[i - bpp]; - context->prior[i] = v; - s += (v < 128) ? v : 256 - v; - } - if (s < sum) { - context->output = context->prior; - sum = s; /* 0 if line is solid */ - } - } - - /* 3. Average (not very common in real-life images, - so its only used with the optimize option) */ - if (context->optimize && sum > 0) { - for (i = 1, s = 0; i <= bpp; i++) { - UINT8 v = state->buffer[i] - context->previous[i] / 2; - context->average[i] = v; - s += (v < 128) ? v : 256 - v; - } - for (; i <= state->bytes; i++) { - UINT8 v = - state->buffer[i] - - (state->buffer[i - bpp] + context->previous[i]) / 2; - context->average[i] = v; - s += (v < 128) ? v : 256 - v; - } - if (s < sum) { - context->output = context->average; - sum = s; - } - } - - /* 4. Paeth */ - if (sum > 0) { - for (i = 1, s = 0; i <= bpp; i++) { - UINT8 v = state->buffer[i] - context->previous[i]; - context->paeth[i] = v; - s += (v < 128) ? v : 256 - v; - } - for (; i <= state->bytes; i++) { - UINT8 v; - int a, b, c; - int pa, pb, pc; - - /* fetch pixels */ - a = state->buffer[i - bpp]; - b = context->previous[i]; - c = context->previous[i - bpp]; - - /* distances to surrounding pixels */ - pa = abs(b - c); - pb = abs(a - c); - pc = abs(a + b - 2 * c); - - /* pick predictor with the shortest distance */ - v = state->buffer[i] - ((pa <= pb && pa <= pc) ? a - : (pb <= pc) ? b - : c); - context->paeth[i] = v; - s += (v < 128) ? v : 256 - v; - } - if (s < sum) { - context->output = context->paeth; - sum = s; - } - } - } - - /* Compress this line */ - context->z_stream.next_in = context->output; - context->z_stream.avail_in = state->bytes + 1; - - err = deflate(&context->z_stream, Z_NO_FLUSH); - - if (err < 0) { - /* Something went wrong inside the compression library */ - if (err == Z_DATA_ERROR) { - state->errcode = IMAGING_CODEC_BROKEN; - } else if (err == Z_MEM_ERROR) { - state->errcode = IMAGING_CODEC_MEMORY; - } else { - state->errcode = IMAGING_CODEC_CONFIG; - } - free(context->paeth); - free(context->average); - free(context->up); - free(context->prior); - free(context->previous); - deflateEnd(&context->z_stream); - ImagingSectionLeave(&cookie); - return -1; - } - - /* Swap buffer pointers */ - ptr = state->buffer; - state->buffer = context->previous; - context->previous = ptr; - } - - if (context->z_stream.avail_out == 0) { - break; /* Buffer full */ - } - - case 2: - - /* End of image data; flush compressor buffers */ - - while (context->z_stream.avail_out > 0) { - err = deflate(&context->z_stream, Z_FINISH); - - if (err == Z_STREAM_END) { - free(context->paeth); - free(context->average); - free(context->up); - free(context->prior); - free(context->previous); - - deflateEnd(&context->z_stream); - - state->errcode = IMAGING_CODEC_END; - - break; - } - - if (context->z_stream.avail_out == 0) { - break; /* Buffer full */ - } - } - } - ImagingSectionLeave(&cookie); - return bytes - context->z_stream.avail_out; - } - - /* Should never ever arrive here... */ - state->errcode = IMAGING_CODEC_CONFIG; - ImagingSectionLeave(&cookie); - return -1; -} - -/* -------------------------------------------------------------------- */ -/* Cleanup */ -/* -------------------------------------------------------------------- */ - -int -ImagingZipEncodeCleanup(ImagingCodecState state) { - ZIPSTATE *context = (ZIPSTATE *)state->context; - - if (context->dictionary) { - free(context->dictionary); - context->dictionary = NULL; - } - - return -1; -} - -const char * -ImagingZipVersion(void) { - return zlibVersion(); -} - -#endif +/* + * The Python Imaging Library. + * $Id$ + * + * coder for ZIP (deflated) image data + * + * History: + * 96-12-29 fl created + * 96-12-30 fl adaptive filter selection, encoder tuning + * + * Copyright (c) Fredrik Lundh 1996. + * Copyright (c) Secret Labs AB 1997. + * + * See the README file for information on usage and redistribution. + */ + +#include "Imaging.h" + +#ifdef HAVE_LIBZ + +#include "ZipCodecs.h" + +int +ImagingZipEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { + ZIPSTATE *context = (ZIPSTATE *)state->context; + int err; + int compress_level, compress_type; + UINT8 *ptr; + int i, bpp, s, sum; + ImagingSectionCookie cookie; + + if (!state->state) { + /* Initialization */ + + /* Valid modes are ZIP_PNG, ZIP_PNG_PALETTE, and ZIP_TIFF */ + + /* overflow check for malloc */ + if (state->bytes > INT_MAX - 1) { + state->errcode = IMAGING_CODEC_MEMORY; + return -1; + } + + /* Expand standard buffer to make room for the filter selector, + and allocate filter buffers */ + free(state->buffer); + /* malloc check ok, overflow checked above */ + state->buffer = (UINT8 *)malloc(state->bytes + 1); + context->previous = (UINT8 *)malloc(state->bytes + 1); + context->prior = (UINT8 *)malloc(state->bytes + 1); + context->up = (UINT8 *)malloc(state->bytes + 1); + context->average = (UINT8 *)malloc(state->bytes + 1); + context->paeth = (UINT8 *)malloc(state->bytes + 1); + if (!state->buffer || !context->previous || !context->prior || !context->up || + !context->average || !context->paeth) { + free(context->paeth); + free(context->average); + free(context->up); + free(context->prior); + free(context->previous); + state->errcode = IMAGING_CODEC_MEMORY; + return -1; + } + + /* Initialise filter buffers */ + state->buffer[0] = 0; + context->prior[0] = 1; + context->up[0] = 2; + context->average[0] = 3; + context->paeth[0] = 4; + + /* Initialise previous buffer to black */ + memset(context->previous, 0, state->bytes + 1); + + /* Setup compression context */ + context->z_stream.zalloc = (alloc_func)0; + context->z_stream.zfree = (free_func)0; + context->z_stream.opaque = (voidpf)0; + context->z_stream.next_in = 0; + context->z_stream.avail_in = 0; + + compress_level = + (context->optimize) ? Z_BEST_COMPRESSION : context->compress_level; + + if (context->compress_type == -1) { + compress_type = + (context->mode == ZIP_PNG) ? Z_FILTERED : Z_DEFAULT_STRATEGY; + } else { + compress_type = context->compress_type; + } + + err = deflateInit2( + &context->z_stream, + /* compression level */ + compress_level, + /* compression method */ + Z_DEFLATED, + /* compression memory resources */ + 15, + 9, + /* compression strategy (image data are filtered)*/ + compress_type); + if (err < 0) { + state->errcode = IMAGING_CODEC_CONFIG; + return -1; + } + + if (context->dictionary && context->dictionary_size > 0) { + err = deflateSetDictionary( + &context->z_stream, + (unsigned char *)context->dictionary, + context->dictionary_size); + if (err < 0) { + state->errcode = IMAGING_CODEC_CONFIG; + return -1; + } + } + + /* Ready to decode */ + state->state = 1; + } + + /* Setup the destination buffer */ + context->z_stream.next_out = buf; + context->z_stream.avail_out = bytes; + if (context->z_stream.next_in && context->z_stream.avail_in > 0) { + /* We have some data from previous round, deflate it first */ + err = deflate(&context->z_stream, Z_NO_FLUSH); + + if (err < 0) { + /* Something went wrong inside the compression library */ + if (err == Z_DATA_ERROR) { + state->errcode = IMAGING_CODEC_BROKEN; + } else if (err == Z_MEM_ERROR) { + state->errcode = IMAGING_CODEC_MEMORY; + } else { + state->errcode = IMAGING_CODEC_CONFIG; + } + free(context->paeth); + free(context->average); + free(context->up); + free(context->prior); + free(context->previous); + deflateEnd(&context->z_stream); + return -1; + } + } + + ImagingSectionEnter(&cookie); + for (;;) { + switch (state->state) { + case 1: + + /* Compress image data */ + while (context->z_stream.avail_out > 0) { + if (state->y >= state->ysize) { + /* End of image; now flush compressor buffers */ + state->state = 2; + break; + } + + /* Stuff image data into the compressor */ + state->shuffle( + state->buffer + 1, + (UINT8 *)im->image[state->y + state->yoff] + + state->xoff * im->pixelsize, + state->xsize); + + state->y++; + + context->output = state->buffer; + + if (context->mode == ZIP_PNG) { + /* Filter the image data. For each line, select + the filter that gives the least total distance + from zero for the filtered data (taken from + LIBPNG) */ + + bpp = (state->bits + 7) / 8; + + /* 0. No filter */ + for (i = 1, sum = 0; i <= state->bytes; i++) { + UINT8 v = state->buffer[i]; + sum += (v < 128) ? v : 256 - v; + } + + /* 2. Up. We'll test this first to save time when + an image line is identical to the one above. */ + if (sum > 0) { + for (i = 1, s = 0; i <= state->bytes; i++) { + UINT8 v = state->buffer[i] - context->previous[i]; + context->up[i] = v; + s += (v < 128) ? v : 256 - v; + } + if (s < sum) { + context->output = context->up; + sum = s; /* 0 if line was duplicated */ + } + } + + /* 1. Prior */ + if (sum > 0) { + for (i = 1, s = 0; i <= bpp; i++) { + UINT8 v = state->buffer[i]; + context->prior[i] = v; + s += (v < 128) ? v : 256 - v; + } + for (; i <= state->bytes; i++) { + UINT8 v = state->buffer[i] - state->buffer[i - bpp]; + context->prior[i] = v; + s += (v < 128) ? v : 256 - v; + } + if (s < sum) { + context->output = context->prior; + sum = s; /* 0 if line is solid */ + } + } + + /* 3. Average (not very common in real-life images, + so its only used with the optimize option) */ + if (context->optimize && sum > 0) { + for (i = 1, s = 0; i <= bpp; i++) { + UINT8 v = state->buffer[i] - context->previous[i] / 2; + context->average[i] = v; + s += (v < 128) ? v : 256 - v; + } + for (; i <= state->bytes; i++) { + UINT8 v = + state->buffer[i] - + (state->buffer[i - bpp] + context->previous[i]) / 2; + context->average[i] = v; + s += (v < 128) ? v : 256 - v; + } + if (s < sum) { + context->output = context->average; + sum = s; + } + } + + /* 4. Paeth */ + if (sum > 0) { + for (i = 1, s = 0; i <= bpp; i++) { + UINT8 v = state->buffer[i] - context->previous[i]; + context->paeth[i] = v; + s += (v < 128) ? v : 256 - v; + } + for (; i <= state->bytes; i++) { + UINT8 v; + int a, b, c; + int pa, pb, pc; + + /* fetch pixels */ + a = state->buffer[i - bpp]; + b = context->previous[i]; + c = context->previous[i - bpp]; + + /* distances to surrounding pixels */ + pa = abs(b - c); + pb = abs(a - c); + pc = abs(a + b - 2 * c); + + /* pick predictor with the shortest distance */ + v = state->buffer[i] - ((pa <= pb && pa <= pc) ? a + : (pb <= pc) ? b + : c); + context->paeth[i] = v; + s += (v < 128) ? v : 256 - v; + } + if (s < sum) { + context->output = context->paeth; + sum = s; + } + } + } + + /* Compress this line */ + context->z_stream.next_in = context->output; + context->z_stream.avail_in = state->bytes + 1; + + err = deflate(&context->z_stream, Z_NO_FLUSH); + + if (err < 0) { + /* Something went wrong inside the compression library */ + if (err == Z_DATA_ERROR) { + state->errcode = IMAGING_CODEC_BROKEN; + } else if (err == Z_MEM_ERROR) { + state->errcode = IMAGING_CODEC_MEMORY; + } else { + state->errcode = IMAGING_CODEC_CONFIG; + } + free(context->paeth); + free(context->average); + free(context->up); + free(context->prior); + free(context->previous); + deflateEnd(&context->z_stream); + ImagingSectionLeave(&cookie); + return -1; + } + + /* Swap buffer pointers */ + ptr = state->buffer; + state->buffer = context->previous; + context->previous = ptr; + } + + if (context->z_stream.avail_out == 0) { + break; /* Buffer full */ + } + + case 2: + + /* End of image data; flush compressor buffers */ + + while (context->z_stream.avail_out > 0) { + err = deflate(&context->z_stream, Z_FINISH); + + if (err == Z_STREAM_END) { + free(context->paeth); + free(context->average); + free(context->up); + free(context->prior); + free(context->previous); + + deflateEnd(&context->z_stream); + + state->errcode = IMAGING_CODEC_END; + + break; + } + + if (context->z_stream.avail_out == 0) { + break; /* Buffer full */ + } + } + } + ImagingSectionLeave(&cookie); + return bytes - context->z_stream.avail_out; + } + + /* Should never ever arrive here... */ + state->errcode = IMAGING_CODEC_CONFIG; + ImagingSectionLeave(&cookie); + return -1; +} + +/* -------------------------------------------------------------------- */ +/* Cleanup */ +/* -------------------------------------------------------------------- */ + +int +ImagingZipEncodeCleanup(ImagingCodecState state) { + ZIPSTATE *context = (ZIPSTATE *)state->context; + + if (context->dictionary) { + free(context->dictionary); + context->dictionary = NULL; + } + + return -1; +} + +const char * +ImagingZipVersion(void) { + return zlibVersion(); +} + +#endif diff --git a/src/libImaging/codec_fd.c b/src/libImaging/codec_fd.c index 526168110..ad3b61403 100644 --- a/src/libImaging/codec_fd.c +++ b/src/libImaging/codec_fd.c @@ -1,69 +1,69 @@ -#include "Python.h" -#include "Imaging.h" - -Py_ssize_t -_imaging_read_pyFd(PyObject *fd, char *dest, Py_ssize_t bytes) { - /* dest should be a buffer bytes long, returns length of read - -1 on error */ - - PyObject *result; - char *buffer; - Py_ssize_t length; - int bytes_result; - - result = PyObject_CallMethod(fd, "read", "n", bytes); - - bytes_result = PyBytes_AsStringAndSize(result, &buffer, &length); - if (bytes_result == -1) { - goto err; - } - - if (length > bytes) { - goto err; - } - - memcpy(dest, buffer, length); - - Py_DECREF(result); - return length; - -err: - Py_DECREF(result); - return -1; -} - -Py_ssize_t -_imaging_write_pyFd(PyObject *fd, char *src, Py_ssize_t bytes) { - PyObject *result; - PyObject *byteObj; - - byteObj = PyBytes_FromStringAndSize(src, bytes); - result = PyObject_CallMethod(fd, "write", "O", byteObj); - - Py_DECREF(byteObj); - Py_DECREF(result); - - return bytes; -} - -int -_imaging_seek_pyFd(PyObject *fd, Py_ssize_t offset, int whence) { - PyObject *result; - - result = PyObject_CallMethod(fd, "seek", "ni", offset, whence); - - Py_DECREF(result); - return 0; -} - -Py_ssize_t -_imaging_tell_pyFd(PyObject *fd) { - PyObject *result; - Py_ssize_t location; - - result = PyObject_CallMethod(fd, "tell", NULL); - location = PyLong_AsSsize_t(result); - - Py_DECREF(result); - return location; -} +#include "Python.h" +#include "Imaging.h" + +Py_ssize_t +_imaging_read_pyFd(PyObject *fd, char *dest, Py_ssize_t bytes) { + /* dest should be a buffer bytes long, returns length of read + -1 on error */ + + PyObject *result; + char *buffer; + Py_ssize_t length; + int bytes_result; + + result = PyObject_CallMethod(fd, "read", "n", bytes); + + bytes_result = PyBytes_AsStringAndSize(result, &buffer, &length); + if (bytes_result == -1) { + goto err; + } + + if (length > bytes) { + goto err; + } + + memcpy(dest, buffer, length); + + Py_DECREF(result); + return length; + +err: + Py_DECREF(result); + return -1; +} + +Py_ssize_t +_imaging_write_pyFd(PyObject *fd, char *src, Py_ssize_t bytes) { + PyObject *result; + PyObject *byteObj; + + byteObj = PyBytes_FromStringAndSize(src, bytes); + result = PyObject_CallMethod(fd, "write", "O", byteObj); + + Py_DECREF(byteObj); + Py_DECREF(result); + + return bytes; +} + +int +_imaging_seek_pyFd(PyObject *fd, Py_ssize_t offset, int whence) { + PyObject *result; + + result = PyObject_CallMethod(fd, "seek", "ni", offset, whence); + + Py_DECREF(result); + return 0; +} + +Py_ssize_t +_imaging_tell_pyFd(PyObject *fd) { + PyObject *result; + Py_ssize_t location; + + result = PyObject_CallMethod(fd, "tell", NULL); + location = PyLong_AsSsize_t(result); + + Py_DECREF(result); + return location; +} diff --git a/src/map.c b/src/map.c index c298bd148..6c341ed34 100644 --- a/src/map.c +++ b/src/map.c @@ -1,146 +1,146 @@ -/* - * The Python Imaging Library. - * - * standard memory mapping interface for the Imaging library - * - * history: - * 1998-03-05 fl added Win32 read mapping - * 1999-02-06 fl added "I;16" support - * 2003-04-21 fl added PyImaging_MapBuffer primitive - * - * Copyright (c) 1998-2003 by Secret Labs AB. - * Copyright (c) 2003 by Fredrik Lundh. - * - * See the README file for information on usage and redistribution. - */ - -/* - * FIXME: should move the memory mapping primitives into libImaging! - */ - -#include "Python.h" - -#include "libImaging/Imaging.h" - -/* compatibility wrappers (defined in _imaging.c) */ -extern int -PyImaging_CheckBuffer(PyObject *buffer); -extern int -PyImaging_GetBuffer(PyObject *buffer, Py_buffer *view); - -extern PyObject * -PyImagingNew(Imaging im); - -/* -------------------------------------------------------------------- */ -/* Buffer mapper */ - -typedef struct ImagingBufferInstance { - struct ImagingMemoryInstance im; - PyObject *target; - Py_buffer view; -} ImagingBufferInstance; - -static void -mapping_destroy_buffer(Imaging im) { - ImagingBufferInstance *buffer = (ImagingBufferInstance *)im; - - PyBuffer_Release(&buffer->view); - Py_XDECREF(buffer->target); -} - -PyObject * -PyImaging_MapBuffer(PyObject *self, PyObject *args) { - Py_ssize_t y, size; - Imaging im; - - PyObject *target; - Py_buffer view; - char *mode; - char *codec; - Py_ssize_t offset; - int xsize, ysize; - int stride; - int ystep; - - if (!PyArg_ParseTuple( - args, - "O(ii)sn(sii)", - &target, - &xsize, - &ysize, - &codec, - &offset, - &mode, - &stride, - &ystep)) { - return NULL; - } - - if (!PyImaging_CheckBuffer(target)) { - PyErr_SetString(PyExc_TypeError, "expected string or buffer"); - return NULL; - } - - if (stride <= 0) { - if (!strcmp(mode, "L") || !strcmp(mode, "P")) { - stride = xsize; - } else if (!strncmp(mode, "I;16", 4)) { - stride = xsize * 2; - } else { - stride = xsize * 4; - } - } - - if (stride > 0 && ysize > PY_SSIZE_T_MAX / stride) { - PyErr_SetString(PyExc_MemoryError, "Integer overflow in ysize"); - return NULL; - } - - size = (Py_ssize_t)ysize * stride; - - if (offset > PY_SSIZE_T_MAX - size) { - PyErr_SetString(PyExc_MemoryError, "Integer overflow in offset"); - return NULL; - } - - /* check buffer size */ - if (PyImaging_GetBuffer(target, &view) < 0) { - return NULL; - } - - if (view.len < 0) { - PyErr_SetString(PyExc_ValueError, "buffer has negative size"); - PyBuffer_Release(&view); - return NULL; - } - if (offset + size > view.len) { - PyErr_SetString(PyExc_ValueError, "buffer is not large enough"); - PyBuffer_Release(&view); - return NULL; - } - - im = ImagingNewPrologueSubtype(mode, xsize, ysize, sizeof(ImagingBufferInstance)); - if (!im) { - PyBuffer_Release(&view); - return NULL; - } - - /* setup file pointers */ - if (ystep > 0) { - for (y = 0; y < ysize; y++) { - im->image[y] = (char *)view.buf + offset + y * stride; - } - } else { - for (y = 0; y < ysize; y++) { - im->image[ysize - y - 1] = (char *)view.buf + offset + y * stride; - } - } - - im->destroy = mapping_destroy_buffer; - - Py_INCREF(target); - ((ImagingBufferInstance *)im)->target = target; - ((ImagingBufferInstance *)im)->view = view; - - return PyImagingNew(im); -} +/* + * The Python Imaging Library. + * + * standard memory mapping interface for the Imaging library + * + * history: + * 1998-03-05 fl added Win32 read mapping + * 1999-02-06 fl added "I;16" support + * 2003-04-21 fl added PyImaging_MapBuffer primitive + * + * Copyright (c) 1998-2003 by Secret Labs AB. + * Copyright (c) 2003 by Fredrik Lundh. + * + * See the README file for information on usage and redistribution. + */ + +/* + * FIXME: should move the memory mapping primitives into libImaging! + */ + +#include "Python.h" + +#include "libImaging/Imaging.h" + +/* compatibility wrappers (defined in _imaging.c) */ +extern int +PyImaging_CheckBuffer(PyObject *buffer); +extern int +PyImaging_GetBuffer(PyObject *buffer, Py_buffer *view); + +extern PyObject * +PyImagingNew(Imaging im); + +/* -------------------------------------------------------------------- */ +/* Buffer mapper */ + +typedef struct ImagingBufferInstance { + struct ImagingMemoryInstance im; + PyObject *target; + Py_buffer view; +} ImagingBufferInstance; + +static void +mapping_destroy_buffer(Imaging im) { + ImagingBufferInstance *buffer = (ImagingBufferInstance *)im; + + PyBuffer_Release(&buffer->view); + Py_XDECREF(buffer->target); +} + +PyObject * +PyImaging_MapBuffer(PyObject *self, PyObject *args) { + Py_ssize_t y, size; + Imaging im; + + PyObject *target; + Py_buffer view; + char *mode; + char *codec; + Py_ssize_t offset; + int xsize, ysize; + int stride; + int ystep; + + if (!PyArg_ParseTuple( + args, + "O(ii)sn(sii)", + &target, + &xsize, + &ysize, + &codec, + &offset, + &mode, + &stride, + &ystep)) { + return NULL; + } + + if (!PyImaging_CheckBuffer(target)) { + PyErr_SetString(PyExc_TypeError, "expected string or buffer"); + return NULL; + } + + if (stride <= 0) { + if (!strcmp(mode, "L") || !strcmp(mode, "P")) { + stride = xsize; + } else if (!strncmp(mode, "I;16", 4)) { + stride = xsize * 2; + } else { + stride = xsize * 4; + } + } + + if (stride > 0 && ysize > PY_SSIZE_T_MAX / stride) { + PyErr_SetString(PyExc_MemoryError, "Integer overflow in ysize"); + return NULL; + } + + size = (Py_ssize_t)ysize * stride; + + if (offset > PY_SSIZE_T_MAX - size) { + PyErr_SetString(PyExc_MemoryError, "Integer overflow in offset"); + return NULL; + } + + /* check buffer size */ + if (PyImaging_GetBuffer(target, &view) < 0) { + return NULL; + } + + if (view.len < 0) { + PyErr_SetString(PyExc_ValueError, "buffer has negative size"); + PyBuffer_Release(&view); + return NULL; + } + if (offset + size > view.len) { + PyErr_SetString(PyExc_ValueError, "buffer is not large enough"); + PyBuffer_Release(&view); + return NULL; + } + + im = ImagingNewPrologueSubtype(mode, xsize, ysize, sizeof(ImagingBufferInstance)); + if (!im) { + PyBuffer_Release(&view); + return NULL; + } + + /* setup file pointers */ + if (ystep > 0) { + for (y = 0; y < ysize; y++) { + im->image[y] = (char *)view.buf + offset + y * stride; + } + } else { + for (y = 0; y < ysize; y++) { + im->image[ysize - y - 1] = (char *)view.buf + offset + y * stride; + } + } + + im->destroy = mapping_destroy_buffer; + + Py_INCREF(target); + ((ImagingBufferInstance *)im)->target = target; + ((ImagingBufferInstance *)im)->view = view; + + return PyImagingNew(im); +} diff --git a/src/outline.c b/src/outline.c index 27cc255cf..2a798e27f 100644 --- a/src/outline.c +++ b/src/outline.c @@ -1,187 +1,187 @@ -/* - * THIS IS WORK IN PROGRESS. - * - * The Python Imaging Library. - * - * "arrow" outline stuff. the contents of this module - * will be merged with the path module and the rest of - * the arrow graphics package, but not before PIL 1.1. - * use at your own risk. - * - * history: - * 99-01-10 fl Added to PIL (experimental) - * - * Copyright (c) Secret Labs AB 1999. - * Copyright (c) Fredrik Lundh 1999. - * - * See the README file for information on usage and redistribution. - */ - -#include "Python.h" - -#include "libImaging/Imaging.h" - -/* -------------------------------------------------------------------- */ -/* Class */ - -typedef struct { - PyObject_HEAD ImagingOutline outline; -} OutlineObject; - -static PyTypeObject OutlineType; - -#define PyOutline_Check(op) (Py_TYPE(op) == &OutlineType) - -static OutlineObject * -_outline_new(void) { - OutlineObject *self; - - if (PyType_Ready(&OutlineType) < 0) { - return NULL; - } - - self = PyObject_New(OutlineObject, &OutlineType); - if (self == NULL) { - return NULL; - } - - self->outline = ImagingOutlineNew(); - - return self; -} - -static void -_outline_dealloc(OutlineObject *self) { - ImagingOutlineDelete(self->outline); - PyObject_Del(self); -} - -ImagingOutline -PyOutline_AsOutline(PyObject *outline) { - if (PyOutline_Check(outline)) { - return ((OutlineObject *)outline)->outline; - } - - return NULL; -} - -/* -------------------------------------------------------------------- */ -/* Factories */ - -PyObject * -PyOutline_Create(PyObject *self, PyObject *args) { - if (!PyArg_ParseTuple(args, ":outline")) { - return NULL; - } - - return (PyObject *)_outline_new(); -} - -/* -------------------------------------------------------------------- */ -/* Methods */ - -static PyObject * -_outline_move(OutlineObject *self, PyObject *args) { - float x0, y0; - if (!PyArg_ParseTuple(args, "ff", &x0, &y0)) { - return NULL; - } - - ImagingOutlineMove(self->outline, x0, y0); - - Py_INCREF(Py_None); - return Py_None; -} - -static PyObject * -_outline_line(OutlineObject *self, PyObject *args) { - float x1, y1; - if (!PyArg_ParseTuple(args, "ff", &x1, &y1)) { - return NULL; - } - - ImagingOutlineLine(self->outline, x1, y1); - - Py_INCREF(Py_None); - return Py_None; -} - -static PyObject * -_outline_curve(OutlineObject *self, PyObject *args) { - float x1, y1, x2, y2, x3, y3; - if (!PyArg_ParseTuple(args, "ffffff", &x1, &y1, &x2, &y2, &x3, &y3)) { - return NULL; - } - - ImagingOutlineCurve(self->outline, x1, y1, x2, y2, x3, y3); - - Py_INCREF(Py_None); - return Py_None; -} - -static PyObject * -_outline_close(OutlineObject *self, PyObject *args) { - if (!PyArg_ParseTuple(args, ":close")) { - return NULL; - } - - ImagingOutlineClose(self->outline); - - Py_INCREF(Py_None); - return Py_None; -} - -static PyObject * -_outline_transform(OutlineObject *self, PyObject *args) { - double a[6]; - if (!PyArg_ParseTuple(args, "(dddddd)", a + 0, a + 1, a + 2, a + 3, a + 4, a + 5)) { - return NULL; - } - - ImagingOutlineTransform(self->outline, a); - - Py_INCREF(Py_None); - return Py_None; -} - -static struct PyMethodDef _outline_methods[] = { - {"line", (PyCFunction)_outline_line, METH_VARARGS}, - {"curve", (PyCFunction)_outline_curve, METH_VARARGS}, - {"move", (PyCFunction)_outline_move, METH_VARARGS}, - {"close", (PyCFunction)_outline_close, METH_VARARGS}, - {"transform", (PyCFunction)_outline_transform, METH_VARARGS}, - {NULL, NULL} /* sentinel */ -}; - -static PyTypeObject OutlineType = { - PyVarObject_HEAD_INIT(NULL, 0) "Outline", /*tp_name*/ - sizeof(OutlineObject), /*tp_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*/ -}; +/* + * THIS IS WORK IN PROGRESS. + * + * The Python Imaging Library. + * + * "arrow" outline stuff. the contents of this module + * will be merged with the path module and the rest of + * the arrow graphics package, but not before PIL 1.1. + * use at your own risk. + * + * history: + * 99-01-10 fl Added to PIL (experimental) + * + * Copyright (c) Secret Labs AB 1999. + * Copyright (c) Fredrik Lundh 1999. + * + * See the README file for information on usage and redistribution. + */ + +#include "Python.h" + +#include "libImaging/Imaging.h" + +/* -------------------------------------------------------------------- */ +/* Class */ + +typedef struct { + PyObject_HEAD ImagingOutline outline; +} OutlineObject; + +static PyTypeObject OutlineType; + +#define PyOutline_Check(op) (Py_TYPE(op) == &OutlineType) + +static OutlineObject * +_outline_new(void) { + OutlineObject *self; + + if (PyType_Ready(&OutlineType) < 0) { + return NULL; + } + + self = PyObject_New(OutlineObject, &OutlineType); + if (self == NULL) { + return NULL; + } + + self->outline = ImagingOutlineNew(); + + return self; +} + +static void +_outline_dealloc(OutlineObject *self) { + ImagingOutlineDelete(self->outline); + PyObject_Del(self); +} + +ImagingOutline +PyOutline_AsOutline(PyObject *outline) { + if (PyOutline_Check(outline)) { + return ((OutlineObject *)outline)->outline; + } + + return NULL; +} + +/* -------------------------------------------------------------------- */ +/* Factories */ + +PyObject * +PyOutline_Create(PyObject *self, PyObject *args) { + if (!PyArg_ParseTuple(args, ":outline")) { + return NULL; + } + + return (PyObject *)_outline_new(); +} + +/* -------------------------------------------------------------------- */ +/* Methods */ + +static PyObject * +_outline_move(OutlineObject *self, PyObject *args) { + float x0, y0; + if (!PyArg_ParseTuple(args, "ff", &x0, &y0)) { + return NULL; + } + + ImagingOutlineMove(self->outline, x0, y0); + + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject * +_outline_line(OutlineObject *self, PyObject *args) { + float x1, y1; + if (!PyArg_ParseTuple(args, "ff", &x1, &y1)) { + return NULL; + } + + ImagingOutlineLine(self->outline, x1, y1); + + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject * +_outline_curve(OutlineObject *self, PyObject *args) { + float x1, y1, x2, y2, x3, y3; + if (!PyArg_ParseTuple(args, "ffffff", &x1, &y1, &x2, &y2, &x3, &y3)) { + return NULL; + } + + ImagingOutlineCurve(self->outline, x1, y1, x2, y2, x3, y3); + + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject * +_outline_close(OutlineObject *self, PyObject *args) { + if (!PyArg_ParseTuple(args, ":close")) { + return NULL; + } + + ImagingOutlineClose(self->outline); + + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject * +_outline_transform(OutlineObject *self, PyObject *args) { + double a[6]; + if (!PyArg_ParseTuple(args, "(dddddd)", a + 0, a + 1, a + 2, a + 3, a + 4, a + 5)) { + return NULL; + } + + ImagingOutlineTransform(self->outline, a); + + Py_INCREF(Py_None); + return Py_None; +} + +static struct PyMethodDef _outline_methods[] = { + {"line", (PyCFunction)_outline_line, METH_VARARGS}, + {"curve", (PyCFunction)_outline_curve, METH_VARARGS}, + {"move", (PyCFunction)_outline_move, METH_VARARGS}, + {"close", (PyCFunction)_outline_close, METH_VARARGS}, + {"transform", (PyCFunction)_outline_transform, METH_VARARGS}, + {NULL, NULL} /* sentinel */ +}; + +static PyTypeObject OutlineType = { + PyVarObject_HEAD_INIT(NULL, 0) "Outline", /*tp_name*/ + sizeof(OutlineObject), /*tp_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*/ +}; diff --git a/src/path.c b/src/path.c index 6bc90abed..1d10c13df 100644 --- a/src/path.c +++ b/src/path.c @@ -1,615 +1,615 @@ -/* - * The Python Imaging Library. - * - * 2D path utilities - * - * history: - * 1996-11-04 fl Added to PIL (incomplete) - * 1996-11-05 fl Added sequence semantics - * 1997-02-28 fl Fixed getbbox - * 1997-06-12 fl Added id attribute - * 1997-06-14 fl Added slicing and setitem - * 1998-12-29 fl Improved sequence handling (from Richard Jones) - * 1999-01-10 fl Fixed IndexError test for 1.5 (from Fred Drake) - * 2000-10-12 fl Added special cases for tuples and lists - * 2002-10-27 fl Added clipping boilerplate - * 2004-09-19 fl Added tolist(flat) variant - * 2005-05-06 fl Added buffer interface support to path constructor - * - * notes: - * FIXME: fill in remaining slots in the sequence api - * - * Copyright (c) 1997-2005 by Secret Labs AB - * Copyright (c) 1997-2005 by Fredrik Lundh - * - * See the README file for information on usage and redistribution. - */ - -#include "Python.h" -#include "libImaging/Imaging.h" - -#include - -/* compatibility wrappers (defined in _imaging.c) */ -extern int -PyImaging_CheckBuffer(PyObject *buffer); -extern int -PyImaging_GetBuffer(PyObject *buffer, Py_buffer *view); - -/* -------------------------------------------------------------------- */ -/* Class */ -/* -------------------------------------------------------------------- */ - -typedef struct { - PyObject_HEAD Py_ssize_t count; - double *xy; - int index; /* temporary use, e.g. in decimate */ -} PyPathObject; - -static PyTypeObject PyPathType; - -static double * -alloc_array(Py_ssize_t count) { - double *xy; - if (count < 0) { - return ImagingError_MemoryError(); - } - if ((unsigned long long)count > (SIZE_MAX / (2 * sizeof(double))) - 1) { - return ImagingError_MemoryError(); - } - xy = calloc(2 * count + 1, sizeof(double)); - if (!xy) { - ImagingError_MemoryError(); - } - return xy; -} - -static PyPathObject * -path_new(Py_ssize_t count, double *xy, int duplicate) { - PyPathObject *path; - - if (duplicate) { - /* duplicate path */ - double *p = alloc_array(count); - if (!p) { - return NULL; - } - memcpy(p, xy, count * 2 * sizeof(double)); - xy = p; - } - - if (PyType_Ready(&PyPathType) < 0) { - free(xy); - return NULL; - } - - path = PyObject_New(PyPathObject, &PyPathType); - if (path == NULL) { - free(xy); - return NULL; - } - - path->count = count; - path->xy = xy; - - return path; -} - -static void -path_dealloc(PyPathObject *path) { - free(path->xy); - PyObject_Del(path); -} - -/* -------------------------------------------------------------------- */ -/* Helpers */ -/* -------------------------------------------------------------------- */ - -#define PyPath_Check(op) (Py_TYPE(op) == &PyPathType) - -Py_ssize_t -PyPath_Flatten(PyObject *data, double **pxy) { - Py_ssize_t i, j, n; - double *xy; - - if (PyPath_Check(data)) { - /* This was another path object. */ - PyPathObject *path = (PyPathObject *)data; - xy = alloc_array(path->count); - if (!xy) { - return -1; - } - memcpy(xy, path->xy, 2 * path->count * sizeof(double)); - *pxy = xy; - return path->count; - } - - if (PyImaging_CheckBuffer(data)) { - /* Assume the buffer contains floats */ - Py_buffer buffer; - if (PyImaging_GetBuffer(data, &buffer) == 0) { - float *ptr = (float *)buffer.buf; - n = buffer.len / (2 * sizeof(float)); - xy = alloc_array(n); - if (!xy) { - return -1; - } - for (i = 0; i < n + n; i++) { - xy[i] = ptr[i]; - } - *pxy = xy; - PyBuffer_Release(&buffer); - return n; - } - PyErr_Clear(); - } - - if (!PySequence_Check(data)) { - PyErr_SetString(PyExc_TypeError, "argument must be sequence"); - return -1; - } - - j = 0; - n = PyObject_Length(data); - /* Just in case __len__ breaks (or doesn't exist) */ - if (PyErr_Occurred()) { - return -1; - } - - /* Allocate for worst case */ - xy = alloc_array(n); - if (!xy) { - return -1; - } - -#define assign_item_to_array(op, decref) \ - if (PyFloat_Check(op)) { \ - xy[j++] = PyFloat_AS_DOUBLE(op); \ - } else if (PyLong_Check(op)) { \ - xy[j++] = (float)PyLong_AS_LONG(op); \ - } else if (PyNumber_Check(op)) { \ - xy[j++] = PyFloat_AsDouble(op); \ - } else if (PyArg_ParseTuple(op, "dd", &x, &y)) { \ - xy[j++] = x; \ - xy[j++] = y; \ - } else { \ - PyErr_SetString(PyExc_ValueError, "incorrect coordinate type"); \ - if (decref) { \ - Py_DECREF(op); \ - } \ - free(xy); \ - return -1; \ - } - - /* Copy table to path array */ - if (PyList_Check(data)) { - for (i = 0; i < n; i++) { - double x, y; - PyObject *op = PyList_GET_ITEM(data, i); - assign_item_to_array(op, 0); - } - } else if (PyTuple_Check(data)) { - for (i = 0; i < n; i++) { - double x, y; - PyObject *op = PyTuple_GET_ITEM(data, i); - assign_item_to_array(op, 0); - } - } else { - for (i = 0; i < n; i++) { - double x, y; - PyObject *op = PySequence_GetItem(data, i); - if (!op) { - /* treat IndexError as end of sequence */ - if (PyErr_Occurred() && PyErr_ExceptionMatches(PyExc_IndexError)) { - PyErr_Clear(); - break; - } else { - free(xy); - return -1; - } - } - assign_item_to_array(op, 1); - Py_DECREF(op); - } - } - - if (j & 1) { - PyErr_SetString(PyExc_ValueError, "wrong number of coordinates"); - free(xy); - return -1; - } - - *pxy = xy; - return j / 2; -} - -/* -------------------------------------------------------------------- */ -/* Factories */ -/* -------------------------------------------------------------------- */ - -PyObject * -PyPath_Create(PyObject *self, PyObject *args) { - PyObject *data; - Py_ssize_t count; - double *xy; - - if (PyArg_ParseTuple(args, "n:Path", &count)) { - /* number of vertices */ - xy = alloc_array(count); - if (!xy) { - return NULL; - } - - } else { - /* sequence or other path */ - PyErr_Clear(); - if (!PyArg_ParseTuple(args, "O", &data)) { - return NULL; - } - - count = PyPath_Flatten(data, &xy); - if (count < 0) { - return NULL; - } - } - - return (PyObject *)path_new(count, xy, 0); -} - -/* -------------------------------------------------------------------- */ -/* Methods */ -/* -------------------------------------------------------------------- */ - -static PyObject * -path_compact(PyPathObject *self, PyObject *args) { - /* Simple-minded method to shorten path. A point is removed if - the city block distance to the previous point is less than the - given distance */ - Py_ssize_t i, j; - double *xy; - - double cityblock = 2.0; - - if (!PyArg_ParseTuple(args, "|d:compact", &cityblock)) { - return NULL; - } - - xy = self->xy; - - /* remove bogus vertices */ - for (i = j = 1; i < self->count; i++) { - if (fabs(xy[j + j - 2] - xy[i + i]) + fabs(xy[j + j - 1] - xy[i + i + 1]) >= - cityblock) { - xy[j + j] = xy[i + i]; - xy[j + j + 1] = xy[i + i + 1]; - j++; - } - } - - i = self->count - j; - self->count = j; - - /* shrink coordinate array */ - /* malloc check ok, self->count is smaller than it was before */ - self->xy = realloc(self->xy, 2 * self->count * sizeof(double)); - - return Py_BuildValue("i", i); /* number of removed vertices */ -} - -static PyObject * -path_getbbox(PyPathObject *self, PyObject *args) { - /* Find bounding box */ - Py_ssize_t i; - double *xy; - double x0, y0, x1, y1; - - if (!PyArg_ParseTuple(args, ":getbbox")) { - return NULL; - } - - xy = self->xy; - - if (self->count == 0) { - x0 = x1 = 0; - y0 = y1 = 0; - } else { - x0 = x1 = xy[0]; - y0 = y1 = xy[1]; - - for (i = 1; i < self->count; i++) { - if (xy[i + i] < x0) { - x0 = xy[i + i]; - } - if (xy[i + i] > x1) { - x1 = xy[i + i]; - } - if (xy[i + i + 1] < y0) { - y0 = xy[i + i + 1]; - } - if (xy[i + i + 1] > y1) { - y1 = xy[i + i + 1]; - } - } - } - - return Py_BuildValue("dddd", x0, y0, x1, y1); -} - -static PyObject * -path_getitem(PyPathObject *self, Py_ssize_t i) { - if (i < 0) { - i = self->count + i; - } - if (i < 0 || i >= self->count) { - PyErr_SetString(PyExc_IndexError, "path index out of range"); - return NULL; - } - - return Py_BuildValue("dd", self->xy[i + i], self->xy[i + i + 1]); -} - -static PyObject * -path_getslice(PyPathObject *self, Py_ssize_t ilow, Py_ssize_t ihigh) { - /* adjust arguments */ - if (ilow < 0) { - ilow = 0; - } else if (ilow >= self->count) { - ilow = self->count; - } - if (ihigh < 0) { - ihigh = 0; - } - if (ihigh < ilow) { - ihigh = ilow; - } else if (ihigh > self->count) { - ihigh = self->count; - } - - return (PyObject *)path_new(ihigh - ilow, self->xy + ilow * 2, 1); -} - -static Py_ssize_t -path_len(PyPathObject *self) { - return self->count; -} - -static PyObject * -path_map(PyPathObject *self, PyObject *args) { - /* Map coordinate set through function */ - Py_ssize_t i; - double *xy; - PyObject *function; - - if (!PyArg_ParseTuple(args, "O:map", &function)) { - return NULL; - } - - xy = self->xy; - - /* apply function to coordinate set */ - for (i = 0; i < self->count; i++) { - double x = xy[i + i]; - double y = xy[i + i + 1]; - PyObject *item = PyObject_CallFunction(function, "dd", x, y); - if (!item || !PyArg_ParseTuple(item, "dd", &x, &y)) { - Py_XDECREF(item); - return NULL; - } - xy[i + i] = x; - xy[i + i + 1] = y; - Py_DECREF(item); - } - - Py_INCREF(Py_None); - return Py_None; -} - -static int -path_setitem(PyPathObject *self, Py_ssize_t i, PyObject *op) { - double *xy; - - if (i < 0 || i >= self->count) { - PyErr_SetString(PyExc_IndexError, "path assignment index out of range"); - return -1; - } - - if (op == NULL) { - PyErr_SetString(PyExc_TypeError, "cannot delete from path"); - return -1; - } - - xy = &self->xy[i + i]; - - if (!PyArg_ParseTuple(op, "dd", &xy[0], &xy[1])) { - return -1; - } - - return 0; -} - -static PyObject * -path_tolist(PyPathObject *self, PyObject *args) { - PyObject *list; - Py_ssize_t i; - - int flat = 0; - if (!PyArg_ParseTuple(args, "|i:tolist", &flat)) { - return NULL; - } - - if (flat) { - list = PyList_New(self->count * 2); - if (list == NULL) { - return NULL; - } - for (i = 0; i < self->count * 2; i++) { - PyObject *item; - item = PyFloat_FromDouble(self->xy[i]); - if (!item) { - goto error; - } - PyList_SetItem(list, i, item); - } - } else { - list = PyList_New(self->count); - if (list == NULL) { - return NULL; - } - for (i = 0; i < self->count; i++) { - PyObject *item; - item = Py_BuildValue("dd", self->xy[i + i], self->xy[i + i + 1]); - if (!item) { - goto error; - } - PyList_SetItem(list, i, item); - } - } - - return list; - -error: - Py_DECREF(list); - return NULL; -} - -static PyObject * -path_transform(PyPathObject *self, PyObject *args) { - /* Apply affine transform to coordinate set */ - Py_ssize_t i; - double *xy; - double a, b, c, d, e, f; - - double wrap = 0.0; - - if (!PyArg_ParseTuple( - args, "(dddddd)|d:transform", &a, &b, &c, &d, &e, &f, &wrap)) { - return NULL; - } - - xy = self->xy; - - /* transform the coordinate set */ - if (b == 0.0 && d == 0.0) { - /* scaling */ - for (i = 0; i < self->count; i++) { - xy[i + i] = a * xy[i + i] + c; - xy[i + i + 1] = e * xy[i + i + 1] + f; - } - } else { - /* affine transform */ - for (i = 0; i < self->count; i++) { - double x = xy[i + i]; - double y = xy[i + i + 1]; - xy[i + i] = a * x + b * y + c; - xy[i + i + 1] = d * x + e * y + f; - } - } - - /* special treatment of geographical map data */ - if (wrap != 0.0) { - for (i = 0; i < self->count; i++) { - xy[i + i] = fmod(xy[i + i], wrap); - } - } - - Py_INCREF(Py_None); - return Py_None; -} - -static struct PyMethodDef methods[] = { - {"getbbox", (PyCFunction)path_getbbox, METH_VARARGS}, - {"tolist", (PyCFunction)path_tolist, METH_VARARGS}, - {"compact", (PyCFunction)path_compact, METH_VARARGS}, - {"map", (PyCFunction)path_map, METH_VARARGS}, - {"transform", (PyCFunction)path_transform, METH_VARARGS}, - {NULL, NULL} /* sentinel */ -}; - -static PyObject * -path_getattr_id(PyPathObject *self, void *closure) { - return Py_BuildValue("n", (Py_ssize_t)self->xy); -} - -static struct PyGetSetDef getsetters[] = {{"id", (getter)path_getattr_id}, {NULL}}; - -static PyObject * -path_subscript(PyPathObject *self, PyObject *item) { - if (PyIndex_Check(item)) { - Py_ssize_t i; - i = PyNumber_AsSsize_t(item, PyExc_IndexError); - if (i == -1 && PyErr_Occurred()) { - return NULL; - } - return path_getitem(self, i); - } - if (PySlice_Check(item)) { - int len = 4; - Py_ssize_t start, stop, step, slicelength; - - if (PySlice_GetIndicesEx(item, len, &start, &stop, &step, &slicelength) < 0) { - return NULL; - } - - if (slicelength <= 0) { - double *xy = alloc_array(0); - return (PyObject *)path_new(0, xy, 0); - } else if (step == 1) { - return path_getslice(self, start, stop); - } else { - PyErr_SetString(PyExc_TypeError, "slice steps not supported"); - return NULL; - } - } else { - PyErr_Format( - PyExc_TypeError, - "Path indices must be integers, not %.200s", - Py_TYPE(item)->tp_name); - return NULL; - } -} - -static PySequenceMethods path_as_sequence = { - (lenfunc)path_len, /*sq_length*/ - (binaryfunc)0, /*sq_concat*/ - (ssizeargfunc)0, /*sq_repeat*/ - (ssizeargfunc)path_getitem, /*sq_item*/ - (ssizessizeargfunc)path_getslice, /*sq_slice*/ - (ssizeobjargproc)path_setitem, /*sq_ass_item*/ - (ssizessizeobjargproc)0, /*sq_ass_slice*/ -}; - -static PyMappingMethods path_as_mapping = { - (lenfunc)path_len, (binaryfunc)path_subscript, NULL}; - -static PyTypeObject PyPathType = { - PyVarObject_HEAD_INIT(NULL, 0) "Path", /*tp_name*/ - sizeof(PyPathObject), /*tp_basicsize*/ - 0, /*tp_itemsize*/ - /* methods */ - (destructor)path_dealloc, /*tp_dealloc*/ - 0, /*tp_vectorcall_offset*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_as_async*/ - 0, /*tp_repr*/ - 0, /*tp_as_number*/ - &path_as_sequence, /*tp_as_sequence*/ - &path_as_mapping, /*tp_as_mapping*/ - 0, /*tp_hash*/ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT, /*tp_flags*/ - 0, /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - methods, /*tp_methods*/ - 0, /*tp_members*/ - getsetters, /*tp_getset*/ -}; +/* + * The Python Imaging Library. + * + * 2D path utilities + * + * history: + * 1996-11-04 fl Added to PIL (incomplete) + * 1996-11-05 fl Added sequence semantics + * 1997-02-28 fl Fixed getbbox + * 1997-06-12 fl Added id attribute + * 1997-06-14 fl Added slicing and setitem + * 1998-12-29 fl Improved sequence handling (from Richard Jones) + * 1999-01-10 fl Fixed IndexError test for 1.5 (from Fred Drake) + * 2000-10-12 fl Added special cases for tuples and lists + * 2002-10-27 fl Added clipping boilerplate + * 2004-09-19 fl Added tolist(flat) variant + * 2005-05-06 fl Added buffer interface support to path constructor + * + * notes: + * FIXME: fill in remaining slots in the sequence api + * + * Copyright (c) 1997-2005 by Secret Labs AB + * Copyright (c) 1997-2005 by Fredrik Lundh + * + * See the README file for information on usage and redistribution. + */ + +#include "Python.h" +#include "libImaging/Imaging.h" + +#include + +/* compatibility wrappers (defined in _imaging.c) */ +extern int +PyImaging_CheckBuffer(PyObject *buffer); +extern int +PyImaging_GetBuffer(PyObject *buffer, Py_buffer *view); + +/* -------------------------------------------------------------------- */ +/* Class */ +/* -------------------------------------------------------------------- */ + +typedef struct { + PyObject_HEAD Py_ssize_t count; + double *xy; + int index; /* temporary use, e.g. in decimate */ +} PyPathObject; + +static PyTypeObject PyPathType; + +static double * +alloc_array(Py_ssize_t count) { + double *xy; + if (count < 0) { + return ImagingError_MemoryError(); + } + if ((unsigned long long)count > (SIZE_MAX / (2 * sizeof(double))) - 1) { + return ImagingError_MemoryError(); + } + xy = calloc(2 * count + 1, sizeof(double)); + if (!xy) { + ImagingError_MemoryError(); + } + return xy; +} + +static PyPathObject * +path_new(Py_ssize_t count, double *xy, int duplicate) { + PyPathObject *path; + + if (duplicate) { + /* duplicate path */ + double *p = alloc_array(count); + if (!p) { + return NULL; + } + memcpy(p, xy, count * 2 * sizeof(double)); + xy = p; + } + + if (PyType_Ready(&PyPathType) < 0) { + free(xy); + return NULL; + } + + path = PyObject_New(PyPathObject, &PyPathType); + if (path == NULL) { + free(xy); + return NULL; + } + + path->count = count; + path->xy = xy; + + return path; +} + +static void +path_dealloc(PyPathObject *path) { + free(path->xy); + PyObject_Del(path); +} + +/* -------------------------------------------------------------------- */ +/* Helpers */ +/* -------------------------------------------------------------------- */ + +#define PyPath_Check(op) (Py_TYPE(op) == &PyPathType) + +Py_ssize_t +PyPath_Flatten(PyObject *data, double **pxy) { + Py_ssize_t i, j, n; + double *xy; + + if (PyPath_Check(data)) { + /* This was another path object. */ + PyPathObject *path = (PyPathObject *)data; + xy = alloc_array(path->count); + if (!xy) { + return -1; + } + memcpy(xy, path->xy, 2 * path->count * sizeof(double)); + *pxy = xy; + return path->count; + } + + if (PyImaging_CheckBuffer(data)) { + /* Assume the buffer contains floats */ + Py_buffer buffer; + if (PyImaging_GetBuffer(data, &buffer) == 0) { + float *ptr = (float *)buffer.buf; + n = buffer.len / (2 * sizeof(float)); + xy = alloc_array(n); + if (!xy) { + return -1; + } + for (i = 0; i < n + n; i++) { + xy[i] = ptr[i]; + } + *pxy = xy; + PyBuffer_Release(&buffer); + return n; + } + PyErr_Clear(); + } + + if (!PySequence_Check(data)) { + PyErr_SetString(PyExc_TypeError, "argument must be sequence"); + return -1; + } + + j = 0; + n = PyObject_Length(data); + /* Just in case __len__ breaks (or doesn't exist) */ + if (PyErr_Occurred()) { + return -1; + } + + /* Allocate for worst case */ + xy = alloc_array(n); + if (!xy) { + return -1; + } + +#define assign_item_to_array(op, decref) \ + if (PyFloat_Check(op)) { \ + xy[j++] = PyFloat_AS_DOUBLE(op); \ + } else if (PyLong_Check(op)) { \ + xy[j++] = (float)PyLong_AS_LONG(op); \ + } else if (PyNumber_Check(op)) { \ + xy[j++] = PyFloat_AsDouble(op); \ + } else if (PyArg_ParseTuple(op, "dd", &x, &y)) { \ + xy[j++] = x; \ + xy[j++] = y; \ + } else { \ + PyErr_SetString(PyExc_ValueError, "incorrect coordinate type"); \ + if (decref) { \ + Py_DECREF(op); \ + } \ + free(xy); \ + return -1; \ + } + + /* Copy table to path array */ + if (PyList_Check(data)) { + for (i = 0; i < n; i++) { + double x, y; + PyObject *op = PyList_GET_ITEM(data, i); + assign_item_to_array(op, 0); + } + } else if (PyTuple_Check(data)) { + for (i = 0; i < n; i++) { + double x, y; + PyObject *op = PyTuple_GET_ITEM(data, i); + assign_item_to_array(op, 0); + } + } else { + for (i = 0; i < n; i++) { + double x, y; + PyObject *op = PySequence_GetItem(data, i); + if (!op) { + /* treat IndexError as end of sequence */ + if (PyErr_Occurred() && PyErr_ExceptionMatches(PyExc_IndexError)) { + PyErr_Clear(); + break; + } else { + free(xy); + return -1; + } + } + assign_item_to_array(op, 1); + Py_DECREF(op); + } + } + + if (j & 1) { + PyErr_SetString(PyExc_ValueError, "wrong number of coordinates"); + free(xy); + return -1; + } + + *pxy = xy; + return j / 2; +} + +/* -------------------------------------------------------------------- */ +/* Factories */ +/* -------------------------------------------------------------------- */ + +PyObject * +PyPath_Create(PyObject *self, PyObject *args) { + PyObject *data; + Py_ssize_t count; + double *xy; + + if (PyArg_ParseTuple(args, "n:Path", &count)) { + /* number of vertices */ + xy = alloc_array(count); + if (!xy) { + return NULL; + } + + } else { + /* sequence or other path */ + PyErr_Clear(); + if (!PyArg_ParseTuple(args, "O", &data)) { + return NULL; + } + + count = PyPath_Flatten(data, &xy); + if (count < 0) { + return NULL; + } + } + + return (PyObject *)path_new(count, xy, 0); +} + +/* -------------------------------------------------------------------- */ +/* Methods */ +/* -------------------------------------------------------------------- */ + +static PyObject * +path_compact(PyPathObject *self, PyObject *args) { + /* Simple-minded method to shorten path. A point is removed if + the city block distance to the previous point is less than the + given distance */ + Py_ssize_t i, j; + double *xy; + + double cityblock = 2.0; + + if (!PyArg_ParseTuple(args, "|d:compact", &cityblock)) { + return NULL; + } + + xy = self->xy; + + /* remove bogus vertices */ + for (i = j = 1; i < self->count; i++) { + if (fabs(xy[j + j - 2] - xy[i + i]) + fabs(xy[j + j - 1] - xy[i + i + 1]) >= + cityblock) { + xy[j + j] = xy[i + i]; + xy[j + j + 1] = xy[i + i + 1]; + j++; + } + } + + i = self->count - j; + self->count = j; + + /* shrink coordinate array */ + /* malloc check ok, self->count is smaller than it was before */ + self->xy = realloc(self->xy, 2 * self->count * sizeof(double)); + + return Py_BuildValue("i", i); /* number of removed vertices */ +} + +static PyObject * +path_getbbox(PyPathObject *self, PyObject *args) { + /* Find bounding box */ + Py_ssize_t i; + double *xy; + double x0, y0, x1, y1; + + if (!PyArg_ParseTuple(args, ":getbbox")) { + return NULL; + } + + xy = self->xy; + + if (self->count == 0) { + x0 = x1 = 0; + y0 = y1 = 0; + } else { + x0 = x1 = xy[0]; + y0 = y1 = xy[1]; + + for (i = 1; i < self->count; i++) { + if (xy[i + i] < x0) { + x0 = xy[i + i]; + } + if (xy[i + i] > x1) { + x1 = xy[i + i]; + } + if (xy[i + i + 1] < y0) { + y0 = xy[i + i + 1]; + } + if (xy[i + i + 1] > y1) { + y1 = xy[i + i + 1]; + } + } + } + + return Py_BuildValue("dddd", x0, y0, x1, y1); +} + +static PyObject * +path_getitem(PyPathObject *self, Py_ssize_t i) { + if (i < 0) { + i = self->count + i; + } + if (i < 0 || i >= self->count) { + PyErr_SetString(PyExc_IndexError, "path index out of range"); + return NULL; + } + + return Py_BuildValue("dd", self->xy[i + i], self->xy[i + i + 1]); +} + +static PyObject * +path_getslice(PyPathObject *self, Py_ssize_t ilow, Py_ssize_t ihigh) { + /* adjust arguments */ + if (ilow < 0) { + ilow = 0; + } else if (ilow >= self->count) { + ilow = self->count; + } + if (ihigh < 0) { + ihigh = 0; + } + if (ihigh < ilow) { + ihigh = ilow; + } else if (ihigh > self->count) { + ihigh = self->count; + } + + return (PyObject *)path_new(ihigh - ilow, self->xy + ilow * 2, 1); +} + +static Py_ssize_t +path_len(PyPathObject *self) { + return self->count; +} + +static PyObject * +path_map(PyPathObject *self, PyObject *args) { + /* Map coordinate set through function */ + Py_ssize_t i; + double *xy; + PyObject *function; + + if (!PyArg_ParseTuple(args, "O:map", &function)) { + return NULL; + } + + xy = self->xy; + + /* apply function to coordinate set */ + for (i = 0; i < self->count; i++) { + double x = xy[i + i]; + double y = xy[i + i + 1]; + PyObject *item = PyObject_CallFunction(function, "dd", x, y); + if (!item || !PyArg_ParseTuple(item, "dd", &x, &y)) { + Py_XDECREF(item); + return NULL; + } + xy[i + i] = x; + xy[i + i + 1] = y; + Py_DECREF(item); + } + + Py_INCREF(Py_None); + return Py_None; +} + +static int +path_setitem(PyPathObject *self, Py_ssize_t i, PyObject *op) { + double *xy; + + if (i < 0 || i >= self->count) { + PyErr_SetString(PyExc_IndexError, "path assignment index out of range"); + return -1; + } + + if (op == NULL) { + PyErr_SetString(PyExc_TypeError, "cannot delete from path"); + return -1; + } + + xy = &self->xy[i + i]; + + if (!PyArg_ParseTuple(op, "dd", &xy[0], &xy[1])) { + return -1; + } + + return 0; +} + +static PyObject * +path_tolist(PyPathObject *self, PyObject *args) { + PyObject *list; + Py_ssize_t i; + + int flat = 0; + if (!PyArg_ParseTuple(args, "|i:tolist", &flat)) { + return NULL; + } + + if (flat) { + list = PyList_New(self->count * 2); + if (list == NULL) { + return NULL; + } + for (i = 0; i < self->count * 2; i++) { + PyObject *item; + item = PyFloat_FromDouble(self->xy[i]); + if (!item) { + goto error; + } + PyList_SetItem(list, i, item); + } + } else { + list = PyList_New(self->count); + if (list == NULL) { + return NULL; + } + for (i = 0; i < self->count; i++) { + PyObject *item; + item = Py_BuildValue("dd", self->xy[i + i], self->xy[i + i + 1]); + if (!item) { + goto error; + } + PyList_SetItem(list, i, item); + } + } + + return list; + +error: + Py_DECREF(list); + return NULL; +} + +static PyObject * +path_transform(PyPathObject *self, PyObject *args) { + /* Apply affine transform to coordinate set */ + Py_ssize_t i; + double *xy; + double a, b, c, d, e, f; + + double wrap = 0.0; + + if (!PyArg_ParseTuple( + args, "(dddddd)|d:transform", &a, &b, &c, &d, &e, &f, &wrap)) { + return NULL; + } + + xy = self->xy; + + /* transform the coordinate set */ + if (b == 0.0 && d == 0.0) { + /* scaling */ + for (i = 0; i < self->count; i++) { + xy[i + i] = a * xy[i + i] + c; + xy[i + i + 1] = e * xy[i + i + 1] + f; + } + } else { + /* affine transform */ + for (i = 0; i < self->count; i++) { + double x = xy[i + i]; + double y = xy[i + i + 1]; + xy[i + i] = a * x + b * y + c; + xy[i + i + 1] = d * x + e * y + f; + } + } + + /* special treatment of geographical map data */ + if (wrap != 0.0) { + for (i = 0; i < self->count; i++) { + xy[i + i] = fmod(xy[i + i], wrap); + } + } + + Py_INCREF(Py_None); + return Py_None; +} + +static struct PyMethodDef methods[] = { + {"getbbox", (PyCFunction)path_getbbox, METH_VARARGS}, + {"tolist", (PyCFunction)path_tolist, METH_VARARGS}, + {"compact", (PyCFunction)path_compact, METH_VARARGS}, + {"map", (PyCFunction)path_map, METH_VARARGS}, + {"transform", (PyCFunction)path_transform, METH_VARARGS}, + {NULL, NULL} /* sentinel */ +}; + +static PyObject * +path_getattr_id(PyPathObject *self, void *closure) { + return Py_BuildValue("n", (Py_ssize_t)self->xy); +} + +static struct PyGetSetDef getsetters[] = {{"id", (getter)path_getattr_id}, {NULL}}; + +static PyObject * +path_subscript(PyPathObject *self, PyObject *item) { + if (PyIndex_Check(item)) { + Py_ssize_t i; + i = PyNumber_AsSsize_t(item, PyExc_IndexError); + if (i == -1 && PyErr_Occurred()) { + return NULL; + } + return path_getitem(self, i); + } + if (PySlice_Check(item)) { + int len = 4; + Py_ssize_t start, stop, step, slicelength; + + if (PySlice_GetIndicesEx(item, len, &start, &stop, &step, &slicelength) < 0) { + return NULL; + } + + if (slicelength <= 0) { + double *xy = alloc_array(0); + return (PyObject *)path_new(0, xy, 0); + } else if (step == 1) { + return path_getslice(self, start, stop); + } else { + PyErr_SetString(PyExc_TypeError, "slice steps not supported"); + return NULL; + } + } else { + PyErr_Format( + PyExc_TypeError, + "Path indices must be integers, not %.200s", + Py_TYPE(item)->tp_name); + return NULL; + } +} + +static PySequenceMethods path_as_sequence = { + (lenfunc)path_len, /*sq_length*/ + (binaryfunc)0, /*sq_concat*/ + (ssizeargfunc)0, /*sq_repeat*/ + (ssizeargfunc)path_getitem, /*sq_item*/ + (ssizessizeargfunc)path_getslice, /*sq_slice*/ + (ssizeobjargproc)path_setitem, /*sq_ass_item*/ + (ssizessizeobjargproc)0, /*sq_ass_slice*/ +}; + +static PyMappingMethods path_as_mapping = { + (lenfunc)path_len, (binaryfunc)path_subscript, NULL}; + +static PyTypeObject PyPathType = { + PyVarObject_HEAD_INIT(NULL, 0) "Path", /*tp_name*/ + sizeof(PyPathObject), /*tp_basicsize*/ + 0, /*tp_itemsize*/ + /* methods */ + (destructor)path_dealloc, /*tp_dealloc*/ + 0, /*tp_vectorcall_offset*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_as_async*/ + 0, /*tp_repr*/ + 0, /*tp_as_number*/ + &path_as_sequence, /*tp_as_sequence*/ + &path_as_mapping, /*tp_as_mapping*/ + 0, /*tp_hash*/ + 0, /*tp_call*/ + 0, /*tp_str*/ + 0, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT, /*tp_flags*/ + 0, /*tp_doc*/ + 0, /*tp_traverse*/ + 0, /*tp_clear*/ + 0, /*tp_richcompare*/ + 0, /*tp_weaklistoffset*/ + 0, /*tp_iter*/ + 0, /*tp_iternext*/ + methods, /*tp_methods*/ + 0, /*tp_members*/ + getsetters, /*tp_getset*/ +}; diff --git a/src/thirdparty/fribidi-shim/fribidi.c b/src/thirdparty/fribidi-shim/fribidi.c index 5663da86b..33454a09a 100644 --- a/src/thirdparty/fribidi-shim/fribidi.c +++ b/src/thirdparty/fribidi-shim/fribidi.c @@ -1,108 +1,108 @@ - -#ifndef _WIN32 -#include -#else -#define WIN32_LEAN_AND_MEAN -#include -#endif - -#define FRIBIDI_SHIM_IMPLEMENTATION -#include "fribidi.h" - - -/* FriBiDi>=1.0.0 adds bracket_types param, ignore and call legacy function */ -static FriBidiLevel fribidi_get_par_embedding_levels_ex_compat( - const FriBidiCharType *bidi_types, - const FriBidiBracketType *bracket_types, - const FriBidiStrIndex len, - FriBidiParType *pbase_dir, - FriBidiLevel *embedding_levels) -{ - return fribidi_get_par_embedding_levels( - bidi_types, len, pbase_dir, embedding_levels); -} - -/* FriBiDi>=1.0.0 gets bracket types here, ignore */ -static void fribidi_get_bracket_types_compat( - const FriBidiChar *str, - const FriBidiStrIndex len, - const FriBidiCharType *types, - FriBidiBracketType *btypes) -{ /* no-op*/ } - - -int load_fribidi(void) { - int error = 0; - const char **p_fribidi_version_info = 0; - - p_fribidi = 0; - - /* Microsoft needs a totally different system */ -#ifndef _WIN32 -#define LOAD_FUNCTION(func) \ - func = (t_##func)dlsym(p_fribidi, #func); \ - error = error || (func == 0); - - p_fribidi = dlopen("libfribidi.so", RTLD_LAZY); - if (!p_fribidi) { - p_fribidi = dlopen("libfribidi.so.0", RTLD_LAZY); - } - if (!p_fribidi) { - p_fribidi = dlopen("libfribidi.dylib", RTLD_LAZY); - } - if (!p_fribidi) { - p_fribidi = dlopen("/usr/local/lib/libfribidi.dylib", RTLD_LAZY); - } -#else -#define LOAD_FUNCTION(func) \ - func = (t_##func)GetProcAddress(p_fribidi, #func); \ - error = error || (func == 0); - - p_fribidi = LoadLibrary("fribidi"); - if (!p_fribidi) { - p_fribidi = LoadLibrary("fribidi-0"); - } - /* MSYS2 */ - if (!p_fribidi) { - p_fribidi = LoadLibrary("libfribidi-0"); - } -#endif - - if (!p_fribidi) { - return 1; - } - - /* load FriBiDi>=1.0.0 functions first, use error to detect version */ - LOAD_FUNCTION(fribidi_get_par_embedding_levels_ex); - LOAD_FUNCTION(fribidi_get_bracket_types); - if (error) { - /* using FriBiDi<1.0.0, ignore new parameters */ - error = 0; - fribidi_get_par_embedding_levels_ex = &fribidi_get_par_embedding_levels_ex_compat; - fribidi_get_bracket_types = &fribidi_get_bracket_types_compat; - } - - LOAD_FUNCTION(fribidi_unicode_to_charset); - LOAD_FUNCTION(fribidi_charset_to_unicode); - LOAD_FUNCTION(fribidi_get_bidi_types); - LOAD_FUNCTION(fribidi_get_par_embedding_levels); - -#ifndef _WIN32 - p_fribidi_version_info = (const char**)dlsym(p_fribidi, "fribidi_version_info"); - if (error || (p_fribidi_version_info == 0) || (*p_fribidi_version_info == 0)) { - dlclose(p_fribidi); - p_fribidi = 0; - return 2; - } -#else - p_fribidi_version_info = (const char**)GetProcAddress(p_fribidi, "fribidi_version_info"); - if (error || (p_fribidi_version_info == 0) || (*p_fribidi_version_info == 0)) { - FreeLibrary(p_fribidi); - p_fribidi = 0; - return 2; - } -#endif - fribidi_version_info = *p_fribidi_version_info; - - return 0; -} + +#ifndef _WIN32 +#include +#else +#define WIN32_LEAN_AND_MEAN +#include +#endif + +#define FRIBIDI_SHIM_IMPLEMENTATION +#include "fribidi.h" + + +/* FriBiDi>=1.0.0 adds bracket_types param, ignore and call legacy function */ +static FriBidiLevel fribidi_get_par_embedding_levels_ex_compat( + const FriBidiCharType *bidi_types, + const FriBidiBracketType *bracket_types, + const FriBidiStrIndex len, + FriBidiParType *pbase_dir, + FriBidiLevel *embedding_levels) +{ + return fribidi_get_par_embedding_levels( + bidi_types, len, pbase_dir, embedding_levels); +} + +/* FriBiDi>=1.0.0 gets bracket types here, ignore */ +static void fribidi_get_bracket_types_compat( + const FriBidiChar *str, + const FriBidiStrIndex len, + const FriBidiCharType *types, + FriBidiBracketType *btypes) +{ /* no-op*/ } + + +int load_fribidi(void) { + int error = 0; + const char **p_fribidi_version_info = 0; + + p_fribidi = 0; + + /* Microsoft needs a totally different system */ +#ifndef _WIN32 +#define LOAD_FUNCTION(func) \ + func = (t_##func)dlsym(p_fribidi, #func); \ + error = error || (func == 0); + + p_fribidi = dlopen("libfribidi.so", RTLD_LAZY); + if (!p_fribidi) { + p_fribidi = dlopen("libfribidi.so.0", RTLD_LAZY); + } + if (!p_fribidi) { + p_fribidi = dlopen("libfribidi.dylib", RTLD_LAZY); + } + if (!p_fribidi) { + p_fribidi = dlopen("/usr/local/lib/libfribidi.dylib", RTLD_LAZY); + } +#else +#define LOAD_FUNCTION(func) \ + func = (t_##func)GetProcAddress(p_fribidi, #func); \ + error = error || (func == 0); + + p_fribidi = LoadLibrary("fribidi"); + if (!p_fribidi) { + p_fribidi = LoadLibrary("fribidi-0"); + } + /* MSYS2 */ + if (!p_fribidi) { + p_fribidi = LoadLibrary("libfribidi-0"); + } +#endif + + if (!p_fribidi) { + return 1; + } + + /* load FriBiDi>=1.0.0 functions first, use error to detect version */ + LOAD_FUNCTION(fribidi_get_par_embedding_levels_ex); + LOAD_FUNCTION(fribidi_get_bracket_types); + if (error) { + /* using FriBiDi<1.0.0, ignore new parameters */ + error = 0; + fribidi_get_par_embedding_levels_ex = &fribidi_get_par_embedding_levels_ex_compat; + fribidi_get_bracket_types = &fribidi_get_bracket_types_compat; + } + + LOAD_FUNCTION(fribidi_unicode_to_charset); + LOAD_FUNCTION(fribidi_charset_to_unicode); + LOAD_FUNCTION(fribidi_get_bidi_types); + LOAD_FUNCTION(fribidi_get_par_embedding_levels); + +#ifndef _WIN32 + p_fribidi_version_info = (const char**)dlsym(p_fribidi, "fribidi_version_info"); + if (error || (p_fribidi_version_info == 0) || (*p_fribidi_version_info == 0)) { + dlclose(p_fribidi); + p_fribidi = 0; + return 2; + } +#else + p_fribidi_version_info = (const char**)GetProcAddress(p_fribidi, "fribidi_version_info"); + if (error || (p_fribidi_version_info == 0) || (*p_fribidi_version_info == 0)) { + FreeLibrary(p_fribidi); + p_fribidi = 0; + return 2; + } +#endif + fribidi_version_info = *p_fribidi_version_info; + + return 0; +} diff --git a/src/thirdparty/fribidi-shim/fribidi.h b/src/thirdparty/fribidi-shim/fribidi.h index 7e175c3db..fcae80cba 100644 --- a/src/thirdparty/fribidi-shim/fribidi.h +++ b/src/thirdparty/fribidi-shim/fribidi.h @@ -1,115 +1,115 @@ - -#define FRIBIDI_MAJOR_VERSION 1 - -/* fribidi-types.h */ - -# if defined (_SVR4) || defined (SVR4) || defined (__OpenBSD__) || \ - defined (_sgi) || defined (__sun) || defined (sun) || \ - defined (__digital__) || defined (__HP_cc) -# include -# elif defined (_AIX) -# include -# else -# include -# endif - -typedef uint32_t FriBidiChar; -typedef int FriBidiStrIndex; - -typedef FriBidiChar FriBidiBracketType; - - - -/* fribidi-char-sets.h */ - -typedef enum -{ - _FRIBIDI_CHAR_SET_NOT_FOUND, - FRIBIDI_CHAR_SET_UTF8, - FRIBIDI_CHAR_SET_CAP_RTL, - FRIBIDI_CHAR_SET_ISO8859_6, - FRIBIDI_CHAR_SET_ISO8859_8, - FRIBIDI_CHAR_SET_CP1255, - FRIBIDI_CHAR_SET_CP1256, - _FRIBIDI_CHAR_SETS_NUM_PLUS_ONE -} -FriBidiCharSet; - - - -/* fribidi-bidi-types.h */ - -typedef signed char FriBidiLevel; - -#define FRIBIDI_TYPE_LTR_VAL 0x00000110L -#define FRIBIDI_TYPE_RTL_VAL 0x00000111L -#define FRIBIDI_TYPE_ON_VAL 0x00000040L - -typedef uint32_t FriBidiCharType; -#define FRIBIDI_TYPE_LTR FRIBIDI_TYPE_LTR_VAL - -typedef uint32_t FriBidiParType; -#define FRIBIDI_PAR_LTR FRIBIDI_TYPE_LTR_VAL -#define FRIBIDI_PAR_RTL FRIBIDI_TYPE_RTL_VAL -#define FRIBIDI_PAR_ON FRIBIDI_TYPE_ON_VAL - -#define FRIBIDI_LEVEL_IS_RTL(lev) ((lev) & 1) -#define FRIBIDI_DIR_TO_LEVEL(dir) ((FriBidiLevel) (FRIBIDI_IS_RTL(dir) ? 1 : 0)) -#define FRIBIDI_IS_RTL(p) ((p) & 0x00000001L) -#define FRIBIDI_IS_EXPLICIT_OR_BN_OR_WS(p) ((p) & 0x00901000L) - - - -/* functions */ - -#ifdef FRIBIDI_SHIM_IMPLEMENTATION -#ifdef _MSC_VER -#define FRIBIDI_ENTRY -#else -#define FRIBIDI_ENTRY __attribute__((visibility ("hidden"))) -#endif -#else -#define FRIBIDI_ENTRY extern -#endif - -#define FRIBIDI_FUNC(ret, name, ...) \ - typedef ret (*t_##name) (__VA_ARGS__); \ - FRIBIDI_ENTRY t_##name name; - -FRIBIDI_FUNC(FriBidiStrIndex, fribidi_unicode_to_charset, - FriBidiCharSet, const FriBidiChar *, FriBidiStrIndex, char *); - -FRIBIDI_FUNC(FriBidiStrIndex, fribidi_charset_to_unicode, - FriBidiCharSet, const char *, FriBidiStrIndex, FriBidiChar *); - -FRIBIDI_FUNC(void, fribidi_get_bidi_types, - const FriBidiChar *, const FriBidiStrIndex, FriBidiCharType *); - -FRIBIDI_FUNC(FriBidiLevel, fribidi_get_par_embedding_levels, - const FriBidiCharType *, const FriBidiStrIndex, FriBidiParType *, - FriBidiLevel *); - -/* FriBiDi>=1.0.0 */ -FRIBIDI_FUNC(FriBidiLevel, fribidi_get_par_embedding_levels_ex, - const FriBidiCharType *, const FriBidiBracketType *, const FriBidiStrIndex, - FriBidiParType *, FriBidiLevel *); - -/* FriBiDi>=1.0.0 */ -FRIBIDI_FUNC(void, fribidi_get_bracket_types, - const FriBidiChar *, const FriBidiStrIndex, const FriBidiCharType *, - FriBidiBracketType *); - -#undef FRIBIDI_FUNC - -/* constant, not a function */ -FRIBIDI_ENTRY const char *fribidi_version_info; - - - -/* shim */ - -FRIBIDI_ENTRY void *p_fribidi; - -FRIBIDI_ENTRY int load_fribidi(void); - -#undef FRIBIDI_ENTRY + +#define FRIBIDI_MAJOR_VERSION 1 + +/* fribidi-types.h */ + +# if defined (_SVR4) || defined (SVR4) || defined (__OpenBSD__) || \ + defined (_sgi) || defined (__sun) || defined (sun) || \ + defined (__digital__) || defined (__HP_cc) +# include +# elif defined (_AIX) +# include +# else +# include +# endif + +typedef uint32_t FriBidiChar; +typedef int FriBidiStrIndex; + +typedef FriBidiChar FriBidiBracketType; + + + +/* fribidi-char-sets.h */ + +typedef enum +{ + _FRIBIDI_CHAR_SET_NOT_FOUND, + FRIBIDI_CHAR_SET_UTF8, + FRIBIDI_CHAR_SET_CAP_RTL, + FRIBIDI_CHAR_SET_ISO8859_6, + FRIBIDI_CHAR_SET_ISO8859_8, + FRIBIDI_CHAR_SET_CP1255, + FRIBIDI_CHAR_SET_CP1256, + _FRIBIDI_CHAR_SETS_NUM_PLUS_ONE +} +FriBidiCharSet; + + + +/* fribidi-bidi-types.h */ + +typedef signed char FriBidiLevel; + +#define FRIBIDI_TYPE_LTR_VAL 0x00000110L +#define FRIBIDI_TYPE_RTL_VAL 0x00000111L +#define FRIBIDI_TYPE_ON_VAL 0x00000040L + +typedef uint32_t FriBidiCharType; +#define FRIBIDI_TYPE_LTR FRIBIDI_TYPE_LTR_VAL + +typedef uint32_t FriBidiParType; +#define FRIBIDI_PAR_LTR FRIBIDI_TYPE_LTR_VAL +#define FRIBIDI_PAR_RTL FRIBIDI_TYPE_RTL_VAL +#define FRIBIDI_PAR_ON FRIBIDI_TYPE_ON_VAL + +#define FRIBIDI_LEVEL_IS_RTL(lev) ((lev) & 1) +#define FRIBIDI_DIR_TO_LEVEL(dir) ((FriBidiLevel) (FRIBIDI_IS_RTL(dir) ? 1 : 0)) +#define FRIBIDI_IS_RTL(p) ((p) & 0x00000001L) +#define FRIBIDI_IS_EXPLICIT_OR_BN_OR_WS(p) ((p) & 0x00901000L) + + + +/* functions */ + +#ifdef FRIBIDI_SHIM_IMPLEMENTATION +#ifdef _MSC_VER +#define FRIBIDI_ENTRY +#else +#define FRIBIDI_ENTRY __attribute__((visibility ("hidden"))) +#endif +#else +#define FRIBIDI_ENTRY extern +#endif + +#define FRIBIDI_FUNC(ret, name, ...) \ + typedef ret (*t_##name) (__VA_ARGS__); \ + FRIBIDI_ENTRY t_##name name; + +FRIBIDI_FUNC(FriBidiStrIndex, fribidi_unicode_to_charset, + FriBidiCharSet, const FriBidiChar *, FriBidiStrIndex, char *); + +FRIBIDI_FUNC(FriBidiStrIndex, fribidi_charset_to_unicode, + FriBidiCharSet, const char *, FriBidiStrIndex, FriBidiChar *); + +FRIBIDI_FUNC(void, fribidi_get_bidi_types, + const FriBidiChar *, const FriBidiStrIndex, FriBidiCharType *); + +FRIBIDI_FUNC(FriBidiLevel, fribidi_get_par_embedding_levels, + const FriBidiCharType *, const FriBidiStrIndex, FriBidiParType *, + FriBidiLevel *); + +/* FriBiDi>=1.0.0 */ +FRIBIDI_FUNC(FriBidiLevel, fribidi_get_par_embedding_levels_ex, + const FriBidiCharType *, const FriBidiBracketType *, const FriBidiStrIndex, + FriBidiParType *, FriBidiLevel *); + +/* FriBiDi>=1.0.0 */ +FRIBIDI_FUNC(void, fribidi_get_bracket_types, + const FriBidiChar *, const FriBidiStrIndex, const FriBidiCharType *, + FriBidiBracketType *); + +#undef FRIBIDI_FUNC + +/* constant, not a function */ +FRIBIDI_ENTRY const char *fribidi_version_info; + + + +/* shim */ + +FRIBIDI_ENTRY void *p_fribidi; + +FRIBIDI_ENTRY int load_fribidi(void); + +#undef FRIBIDI_ENTRY diff --git a/src/thirdparty/raqm/AUTHORS b/src/thirdparty/raqm/AUTHORS index bd5c3ac6b..9d597c379 100644 --- a/src/thirdparty/raqm/AUTHORS +++ b/src/thirdparty/raqm/AUTHORS @@ -1,9 +1,9 @@ -Abderraouf Adjal -Ali Yousuf -Anood Almuharbi -Asma Albahanta -Fahad Alsaidi -Ibtisam Almabsali -Khaled Hosny -Mazoon Almaamari -Shamsa Alqassabi +Abderraouf Adjal +Ali Yousuf +Anood Almuharbi +Asma Albahanta +Fahad Alsaidi +Ibtisam Almabsali +Khaled Hosny +Mazoon Almaamari +Shamsa Alqassabi diff --git a/src/thirdparty/raqm/COPYING b/src/thirdparty/raqm/COPYING index 97e2489b7..7db8f0615 100644 --- a/src/thirdparty/raqm/COPYING +++ b/src/thirdparty/raqm/COPYING @@ -1,22 +1,22 @@ -The MIT License (MIT) - -Copyright © 2015 Information Technology Authority (ITA) -Copyright © 2016-2023 Khaled Hosny - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +The MIT License (MIT) + +Copyright © 2015 Information Technology Authority (ITA) +Copyright © 2016-2023 Khaled Hosny + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/thirdparty/raqm/NEWS b/src/thirdparty/raqm/NEWS index e8bf32e0b..ff4e60ef6 100644 --- a/src/thirdparty/raqm/NEWS +++ b/src/thirdparty/raqm/NEWS @@ -1,152 +1,152 @@ -Overview of changes leading to 0.10.1 -Wednesday, April 12, 2023 -==================================== - -Make combining marks always inherit the script of their base. - -Overview of changes leading to 0.10.0 -Wednesday, January 11, 2023 -==================================== - -Fix font feature ranges. - -Fix resolved direction for all-neutral text. - -Implement letter and word spacing support. - -New API: - * raqm_set_text_utf16 - -Overview of changes leading to 0.9.0 -Sunday, January 30, 2022 -==================================== - -Raise the minimum versions of Raqm dependencies: no longer conditionally -enabling any features based on specific dependency version. - -raqm_t objects can now be reused by calling raqm_clear_contents() before -re-use, to potentially reduce the number memory allocations. - -Don't hardcode python3 in tests. - -New API: - * raqm_set_freetype_load_flags_range - * raqm_clear_contents - -Overview of changes leading to 0.8.0 -Monday, December 13, 2021 -==================================== - -Remove autotools build. - -Support using SheenBiDi instead of FriBiDi for Unicode BiDi support. - -Fix running tests with Python <= 3.6. - -New API: - * raqm_get_par_resolved_direction - * raqm_get_direction_at_index - -Overview of changes leading to 0.7.2 -Monday, September 27, 2021 -==================================== - -Fix test failure with newer HarfBuzz versions. - -Apply FT_Face transformation matrix when built against FreeType 2.11 or later. - -Add meson build system. Autotools build system will be dropped in next release. - -Improve MSVC support. - -Build and documentation fixes. - -Overview of changes leading to 0.7.1 -Sunday, November 22, 2020 -==================================== - -Require HarfBuzz >= 2.0.0 - -Build and documentation fixes. - -Overview of changes leading to 0.7.0 -Monday, May 27, 2019 -==================================== - -New API: - * raqm_version - * raqm_version_string - * raqm_version_atleast - * RAQM_VERSION_MAJOR - * RAQM_VERSION_MICRO - * RAQM_VERSION_MINOR - * RAQM_VERSION_STRING - * RAQM_VERSION_ATLEAST - -Overview of changes leading to 0.6.0 -Sunday, May 5, 2019 -==================================== - -Fix TTB direction regression from the previous release. - -Correctly detect script of Common and Inherite characters at start of text. - -Undef HAVE_CONFIG_H workaround, for older versions of Fribidi. - -Drop test suite dependency on GLib. - -Port test runner to Python instead of shell script. - -New API: -* raqm_set_invisible_glyph() - -Overview of changes leading to 0.5.0 -Saturday, February 24, 2018 -==================================== - -Use FriBiDi 1.x API when available. - -Overview of changes leading to 0.4.0 -Sunday, January 21, 2018 -==================================== - -Set begin-of-text and end-of-text HarfBuzz buffer flags. - -Dynamically allocate memory instead of using stack allocation for input text. - -Accept zero length text and do nothing instead of treating it as error. - -Overview of changes leading to 0.3.0 -Monday, August 21, 2017 -==================================== - -Fix stack corruption on MSVC. - -New API: -* raqm_set_freetype_load_flags - -Overview of changes leading to 0.2.0 -Wednesday, August 25, 2016 -==================================== - -Fix building with MSVC due to lacking C99 support. - -Make multiple fonts support actually work. Start and length now respect the -input encoding. - -New API: -* raqm_index_to_position -* raqm_position_to_index -* raqm_set_language - -Overview of changes leading to 0.1.1 -Sunday, May 1, 2016 -==================================== - -Fix make check on 32-bit systems. - -Overview of changes leading to 0.1.0 -Wednesday, January 20, 2016 -==================================== - -First release. +Overview of changes leading to 0.10.1 +Wednesday, April 12, 2023 +==================================== + +Make combining marks always inherit the script of their base. + +Overview of changes leading to 0.10.0 +Wednesday, January 11, 2023 +==================================== + +Fix font feature ranges. + +Fix resolved direction for all-neutral text. + +Implement letter and word spacing support. + +New API: + * raqm_set_text_utf16 + +Overview of changes leading to 0.9.0 +Sunday, January 30, 2022 +==================================== + +Raise the minimum versions of Raqm dependencies: no longer conditionally +enabling any features based on specific dependency version. + +raqm_t objects can now be reused by calling raqm_clear_contents() before +re-use, to potentially reduce the number memory allocations. + +Don't hardcode python3 in tests. + +New API: + * raqm_set_freetype_load_flags_range + * raqm_clear_contents + +Overview of changes leading to 0.8.0 +Monday, December 13, 2021 +==================================== + +Remove autotools build. + +Support using SheenBiDi instead of FriBiDi for Unicode BiDi support. + +Fix running tests with Python <= 3.6. + +New API: + * raqm_get_par_resolved_direction + * raqm_get_direction_at_index + +Overview of changes leading to 0.7.2 +Monday, September 27, 2021 +==================================== + +Fix test failure with newer HarfBuzz versions. + +Apply FT_Face transformation matrix when built against FreeType 2.11 or later. + +Add meson build system. Autotools build system will be dropped in next release. + +Improve MSVC support. + +Build and documentation fixes. + +Overview of changes leading to 0.7.1 +Sunday, November 22, 2020 +==================================== + +Require HarfBuzz >= 2.0.0 + +Build and documentation fixes. + +Overview of changes leading to 0.7.0 +Monday, May 27, 2019 +==================================== + +New API: + * raqm_version + * raqm_version_string + * raqm_version_atleast + * RAQM_VERSION_MAJOR + * RAQM_VERSION_MICRO + * RAQM_VERSION_MINOR + * RAQM_VERSION_STRING + * RAQM_VERSION_ATLEAST + +Overview of changes leading to 0.6.0 +Sunday, May 5, 2019 +==================================== + +Fix TTB direction regression from the previous release. + +Correctly detect script of Common and Inherite characters at start of text. + +Undef HAVE_CONFIG_H workaround, for older versions of Fribidi. + +Drop test suite dependency on GLib. + +Port test runner to Python instead of shell script. + +New API: +* raqm_set_invisible_glyph() + +Overview of changes leading to 0.5.0 +Saturday, February 24, 2018 +==================================== + +Use FriBiDi 1.x API when available. + +Overview of changes leading to 0.4.0 +Sunday, January 21, 2018 +==================================== + +Set begin-of-text and end-of-text HarfBuzz buffer flags. + +Dynamically allocate memory instead of using stack allocation for input text. + +Accept zero length text and do nothing instead of treating it as error. + +Overview of changes leading to 0.3.0 +Monday, August 21, 2017 +==================================== + +Fix stack corruption on MSVC. + +New API: +* raqm_set_freetype_load_flags + +Overview of changes leading to 0.2.0 +Wednesday, August 25, 2016 +==================================== + +Fix building with MSVC due to lacking C99 support. + +Make multiple fonts support actually work. Start and length now respect the +input encoding. + +New API: +* raqm_index_to_position +* raqm_position_to_index +* raqm_set_language + +Overview of changes leading to 0.1.1 +Sunday, May 1, 2016 +==================================== + +Fix make check on 32-bit systems. + +Overview of changes leading to 0.1.0 +Wednesday, January 20, 2016 +==================================== + +First release. diff --git a/src/thirdparty/raqm/README.md b/src/thirdparty/raqm/README.md index ab729cdc0..6c97ac3f2 100644 --- a/src/thirdparty/raqm/README.md +++ b/src/thirdparty/raqm/README.md @@ -1,85 +1,85 @@ -Raqm -==== - -[![Build](https://github.com/HOST-Oman/libraqm/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/HOST-Oman/libraqm/actions) - -Raqm is a small library that encapsulates the logic for complex text layout and -provides a convenient API. - -It currently provides bidirectional text support (using [FriBiDi][1] or -[SheenBidi][2]), shaping (using [HarfBuzz][3]), and proper script itemization. -As a result, Raqm can support most writing systems covered by Unicode. - -The documentation can be accessed on the web at: -> https://host-oman.github.io/libraqm/ - -Raqm (Arabic: رَقْم) is writing, also number or digit and the Arabic word for -digital (رَقَمÙيّ) shares the same root, so it is a play on “digital writingâ€. - -Building --------- - -Raqm depends on the following libraries: -* [FreeType][4] -* [HarfBuzz][3] -* [FriBiDi][1] or [SheenBidi][2] - -To build the documentation you will also need: -* [GTK-Doc][5] - -To install dependencies on Fedora: - - sudo dnf install freetype-devel harfbuzz-devel fribidi-devel meson gtk-doc - -To install dependencies on Ubuntu: - - sudo apt-get install libfreetype6-dev libharfbuzz-dev libfribidi-dev meson gtk-doc-tools - -On Mac OS X you can use Homebrew: - - brew install freetype harfbuzz fribidi meson gtk-doc - export XML_CATALOG_FILES="/usr/local/etc/xml/catalog" # for the docs - -Once you have the source code and the dependencies, you can proceed to build. -To do that, run the customary sequence of commands in the source code -directory: - - $ meson build - $ ninja -C build - $ ninja -C build install - -To build the documentation, pass `-Ddocs=true` to the `meson`. - -To run the tests: - - $ ninja -C build test - -Contributing ------------- - -Once you have made a change that you are happy with, contribute it back, we’ll -be happy to integrate it! Just fork the repository and make a pull request. - -Projects using Raqm -------------------- - -1. [ImageMagick](https://github.com/ImageMagick/ImageMagick) -2. [LibGD](https://github.com/libgd/libgd) -3. [FontView](https://github.com/googlei18n/fontview) -4. [Pillow](https://github.com/python-pillow) -5. [mplcairo](https://github.com/anntzer/mplcairo) -6. [CEGUI](https://github.com/cegui/cegui) - -The following projects have patches to support complex text layout using Raqm: - -2. SDL_ttf: https://bugzilla.libsdl.org/show_bug.cgi?id=3211 -3. Pygame: https://bitbucket.org/pygame/pygame/pull-requests/52 -4. Blender: https://developer.blender.org/D1809 - - - -[1]: https://github.com/fribidi/fribidi -[2]: https://github.com/Tehreer/SheenBidi -[3]: https://github.com/harfbuzz/harfbuzz -[4]: https://freetype.org/ -[5]: https://www.gtk.org/gtk-doc +Raqm +==== + +[![Build](https://github.com/HOST-Oman/libraqm/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/HOST-Oman/libraqm/actions) + +Raqm is a small library that encapsulates the logic for complex text layout and +provides a convenient API. + +It currently provides bidirectional text support (using [FriBiDi][1] or +[SheenBidi][2]), shaping (using [HarfBuzz][3]), and proper script itemization. +As a result, Raqm can support most writing systems covered by Unicode. + +The documentation can be accessed on the web at: +> https://host-oman.github.io/libraqm/ + +Raqm (Arabic: رَقْم) is writing, also number or digit and the Arabic word for +digital (رَقَمÙيّ) shares the same root, so it is a play on “digital writingâ€. + +Building +-------- + +Raqm depends on the following libraries: +* [FreeType][4] +* [HarfBuzz][3] +* [FriBiDi][1] or [SheenBidi][2] + +To build the documentation you will also need: +* [GTK-Doc][5] + +To install dependencies on Fedora: + + sudo dnf install freetype-devel harfbuzz-devel fribidi-devel meson gtk-doc + +To install dependencies on Ubuntu: + + sudo apt-get install libfreetype6-dev libharfbuzz-dev libfribidi-dev meson gtk-doc-tools + +On Mac OS X you can use Homebrew: + + brew install freetype harfbuzz fribidi meson gtk-doc + export XML_CATALOG_FILES="/usr/local/etc/xml/catalog" # for the docs + +Once you have the source code and the dependencies, you can proceed to build. +To do that, run the customary sequence of commands in the source code +directory: + + $ meson build + $ ninja -C build + $ ninja -C build install + +To build the documentation, pass `-Ddocs=true` to the `meson`. + +To run the tests: + + $ ninja -C build test + +Contributing +------------ + +Once you have made a change that you are happy with, contribute it back, we’ll +be happy to integrate it! Just fork the repository and make a pull request. + +Projects using Raqm +------------------- + +1. [ImageMagick](https://github.com/ImageMagick/ImageMagick) +2. [LibGD](https://github.com/libgd/libgd) +3. [FontView](https://github.com/googlei18n/fontview) +4. [Pillow](https://github.com/python-pillow) +5. [mplcairo](https://github.com/anntzer/mplcairo) +6. [CEGUI](https://github.com/cegui/cegui) + +The following projects have patches to support complex text layout using Raqm: + +2. SDL_ttf: https://bugzilla.libsdl.org/show_bug.cgi?id=3211 +3. Pygame: https://bitbucket.org/pygame/pygame/pull-requests/52 +4. Blender: https://developer.blender.org/D1809 + + + +[1]: https://github.com/fribidi/fribidi +[2]: https://github.com/Tehreer/SheenBidi +[3]: https://github.com/harfbuzz/harfbuzz +[4]: https://freetype.org/ +[5]: https://www.gtk.org/gtk-doc diff --git a/src/thirdparty/raqm/raqm-version.h b/src/thirdparty/raqm/raqm-version.h index 62d2d2064..98887e38d 100644 --- a/src/thirdparty/raqm/raqm-version.h +++ b/src/thirdparty/raqm/raqm-version.h @@ -1,44 +1,44 @@ -/* - * Copyright © 2011 Google, Inc. - * - * This is part of HarfBuzz, a text shaping library. - * - * Permission is hereby granted, without written agreement and without - * license or royalty fees, to use, copy, modify, and distribute this - * software and its documentation for any purpose, provided that the - * above copyright notice and the following two paragraphs appear in - * all copies of this software. - * - * IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR - * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES - * ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN - * IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH - * DAMAGE. - * - * THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, - * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND - * FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS - * ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO - * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. - * - * Google Author(s): Behdad Esfahbod - */ - -#ifndef _RAQM_H_IN_ -#error "Include instead." -#endif - -#ifndef _RAQM_VERSION_H_ -#define _RAQM_VERSION_H_ - -#define RAQM_VERSION_MAJOR 0 -#define RAQM_VERSION_MINOR 10 -#define RAQM_VERSION_MICRO 1 - -#define RAQM_VERSION_STRING "0.10.1" - -#define RAQM_VERSION_ATLEAST(major,minor,micro) \ - ((major)*10000+(minor)*100+(micro) <= \ - RAQM_VERSION_MAJOR*10000+RAQM_VERSION_MINOR*100+RAQM_VERSION_MICRO) - -#endif /* _RAQM_VERSION_H_ */ +/* + * Copyright © 2011 Google, Inc. + * + * This is part of HarfBuzz, a text shaping library. + * + * Permission is hereby granted, without written agreement and without + * license or royalty fees, to use, copy, modify, and distribute this + * software and its documentation for any purpose, provided that the + * above copyright notice and the following two paragraphs appear in + * all copies of this software. + * + * IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES + * ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN + * IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH + * DAMAGE. + * + * THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, + * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS + * ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + * Google Author(s): Behdad Esfahbod + */ + +#ifndef _RAQM_H_IN_ +#error "Include instead." +#endif + +#ifndef _RAQM_VERSION_H_ +#define _RAQM_VERSION_H_ + +#define RAQM_VERSION_MAJOR 0 +#define RAQM_VERSION_MINOR 10 +#define RAQM_VERSION_MICRO 1 + +#define RAQM_VERSION_STRING "0.10.1" + +#define RAQM_VERSION_ATLEAST(major,minor,micro) \ + ((major)*10000+(minor)*100+(micro) <= \ + RAQM_VERSION_MAJOR*10000+RAQM_VERSION_MINOR*100+RAQM_VERSION_MICRO) + +#endif /* _RAQM_VERSION_H_ */ diff --git a/src/thirdparty/raqm/raqm.c b/src/thirdparty/raqm/raqm.c index 2b331e1af..45c6fc8d9 100644 --- a/src/thirdparty/raqm/raqm.c +++ b/src/thirdparty/raqm/raqm.c @@ -1,2805 +1,2805 @@ -/* - * Copyright © 2015 Information Technology Authority (ITA) - * Copyright © 2016-2023 Khaled Hosny - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - * - */ - -#ifdef HAVE_CONFIG_H -#include "config.h" -#endif - -#include -#include - -#ifdef RAQM_SHEENBIDI -#include -#else -#ifdef HAVE_FRIBIDI_SYSTEM -#include -#else -#include "../fribidi-shim/fribidi.h" -#endif -#endif - -#include -#include - -#include "raqm.h" - -/** - * SECTION:raqm - * @title: Raqm - * @short_description: A library for complex text layout - * @include: raqm.h - * - * Raqm is a light weight text layout library with strong emphasis on - * supporting languages and writing systems that require complex text layout. - * - * The main object in Raqm API is #raqm_t, it stores all the states of the - * input text, its properties, and the output of the layout process. - * - * To start, you create a #raqm_t object, add text and font(s) to it, run the - * layout process, and finally query about the output. For example: - * - * |[ - * #include "raqm.h" - * - * int - * main (int argc, char *argv[]) - * { - * const char *fontfile; - * const char *text; - * const char *direction; - * const char *language; - * int ret = 1; - * - * FT_Library library = NULL; - * FT_Face face = NULL; - * - * if (argc < 5) - * { - * printf ("Usage: %s FONT_FILE TEXT DIRECTION LANG\n", argv[0]); - * return 1; - * } - * - * fontfile = argv[1]; - * text = argv[2]; - * direction = argv[3]; - * language = argv[4]; - * - * if (FT_Init_FreeType (&library) == 0) - * { - * if (FT_New_Face (library, fontfile, 0, &face) == 0) - * { - * if (FT_Set_Char_Size (face, face->units_per_EM, 0, 0, 0) == 0) - * { - * raqm_t *rq = raqm_create (); - * if (rq != NULL) - * { - * raqm_direction_t dir = RAQM_DIRECTION_DEFAULT; - * - * if (strcmp (direction, "r") == 0) - * dir = RAQM_DIRECTION_RTL; - * else if (strcmp (direction, "l") == 0) - * dir = RAQM_DIRECTION_LTR; - * - * if (raqm_set_text_utf8 (rq, text, strlen (text)) && - * raqm_set_freetype_face (rq, face) && - * raqm_set_par_direction (rq, dir) && - * raqm_set_language (rq, language, 0, strlen (text)) && - * raqm_layout (rq)) - * { - * size_t count, i; - * raqm_glyph_t *glyphs = raqm_get_glyphs (rq, &count); - * - * ret = !(glyphs != NULL || count == 0); - * - * printf("glyph count: %zu\n", count); - * for (i = 0; i < count; i++) - * { - * printf ("gid#%d off: (%d, %d) adv: (%d, %d) idx: %d\n", - * glyphs[i].index, - * glyphs[i].x_offset, - * glyphs[i].y_offset, - * glyphs[i].x_advance, - * glyphs[i].y_advance, - * glyphs[i].cluster); - * } - * } - * - * raqm_destroy (rq); - * } - * } - * - * FT_Done_Face (face); - * } - * - * FT_Done_FreeType (library); - * } - * - * return ret; - * } - * ]| - * To compile this example: - * |[ - * cc -o test test.c `pkg-config --libs --cflags raqm` - * ]| - */ - -/* For enabling debug mode */ -/*#define RAQM_DEBUG 1*/ -#ifdef RAQM_DEBUG -#define RAQM_DBG(...) fprintf (stderr, __VA_ARGS__) -#else -#define RAQM_DBG(...) -#endif - -#ifdef RAQM_TESTING -# define RAQM_TEST(...) printf (__VA_ARGS__) -# define SCRIPT_TO_STRING(script) \ - char buff[5]; \ - hb_tag_to_string (hb_script_to_iso15924_tag (script), buff); \ - buff[4] = '\0'; -#else -# define RAQM_TEST(...) -#endif - -#define RAQM_BIDI_LEVEL_IS_RTL(level) \ - ((level) & 1) - -#ifdef RAQM_SHEENBIDI - typedef SBLevel _raqm_bidi_level_t; -#else - typedef FriBidiLevel _raqm_bidi_level_t; -#endif - -typedef struct -{ - FT_Face ftface; - int ftloadflags; - hb_language_t lang; - hb_script_t script; - int spacing_after; -} _raqm_text_info; - -typedef struct _raqm_run raqm_run_t; - -struct _raqm -{ - int ref_count; - - uint32_t *text; - uint16_t *text_utf16; - char *text_utf8; - size_t text_len; - size_t text_capacity_bytes; - - _raqm_text_info *text_info; - - raqm_direction_t base_dir; - raqm_direction_t resolved_dir; - - hb_feature_t *features; - size_t features_len; - - raqm_run_t *runs; - raqm_run_t *runs_pool; - - raqm_glyph_t *glyphs; - size_t glyphs_capacity; - - int invisible_glyph; -}; - -struct _raqm_run -{ - uint32_t pos; - uint32_t len; - - hb_direction_t direction; - hb_script_t script; - hb_font_t *font; - hb_buffer_t *buffer; - - raqm_run_t *next; -}; - -static size_t -_raqm_encoding_to_u32_index (raqm_t *rq, - size_t index); - -static bool -_raqm_allowed_grapheme_boundary (hb_codepoint_t l_char, - hb_codepoint_t r_char); - -static void -_raqm_init_text_info (raqm_t *rq) -{ - hb_language_t default_lang = hb_language_get_default (); - for (size_t i = 0; i < rq->text_len; i++) - { - rq->text_info[i].ftface = NULL; - rq->text_info[i].ftloadflags = -1; - rq->text_info[i].lang = default_lang; - rq->text_info[i].script = HB_SCRIPT_INVALID; - rq->text_info[i].spacing_after = 0; - } -} - -static void -_raqm_release_text_info (raqm_t *rq) -{ - if (!rq->text_info) - return; - - for (size_t i = 0; i < rq->text_len; i++) - { - if (rq->text_info[i].ftface) - FT_Done_Face (rq->text_info[i].ftface); - } -} - -static bool -_raqm_compare_text_info (_raqm_text_info a, - _raqm_text_info b) -{ - if (a.ftface != b.ftface) - return false; - - if (a.ftloadflags != b.ftloadflags) - return false; - - if (a.lang != b.lang) - return false; - - if (a.script != b.script) - return false; - - /* Spacing shouldn't break runs, so we don't compare them here. */ - - return true; -} - -static void -_raqm_free_text(raqm_t* rq) -{ - free (rq->text); - rq->text = NULL; - rq->text_info = NULL; - rq->text_utf8 = NULL; - rq->text_utf16 = NULL; - rq->text_len = 0; - rq->text_capacity_bytes = 0; -} - -static bool -_raqm_alloc_text(raqm_t *rq, - size_t len, - bool need_utf8, - bool need_utf16) -{ - /* Allocate contiguous memory block for texts and text_info */ - size_t mem_size = (sizeof (uint32_t) + sizeof (_raqm_text_info)) * len; - if (need_utf8) - mem_size += sizeof (char) * len; - else if (need_utf16) - mem_size += sizeof (uint16_t) * len; - - if (mem_size > rq->text_capacity_bytes) - { - void* new_mem = realloc (rq->text, mem_size); - if (!new_mem) - { - _raqm_free_text (rq); - return false; - } - - rq->text_capacity_bytes = mem_size; - rq->text = new_mem; - } - - rq->text_info = (_raqm_text_info*)(rq->text + len); - rq->text_utf8 = need_utf8 ? (char*)(rq->text_info + len) : NULL; - rq->text_utf16 = need_utf16 ? (uint16_t*)(rq->text_info + len) : NULL; - - return true; -} - -static raqm_run_t* -_raqm_alloc_run (raqm_t *rq) -{ - raqm_run_t *run = rq->runs_pool; - if (run) - { - rq->runs_pool = run->next; - } - else - { - run = malloc (sizeof (raqm_run_t)); - run->font = NULL; - run->buffer = NULL; - } - - run->pos = 0; - run->len = 0; - run->direction = HB_DIRECTION_INVALID; - run->script = HB_SCRIPT_INVALID; - run->next = NULL; - - return run; -} - -static void -_raqm_free_runs (raqm_run_t *runs) -{ - while (runs) - { - raqm_run_t *run = runs; - runs = runs->next; - - if (run->buffer) - hb_buffer_destroy (run->buffer); - - if (run->font) - hb_font_destroy (run->font); - - free (run); - } -} - -/** - * raqm_create: - * - * Creates a new #raqm_t with all its internal states initialized to their - * defaults. - * - * Return value: - * A newly allocated #raqm_t with a reference count of 1. The initial reference - * count should be released with raqm_destroy() when you are done using the - * #raqm_t. Returns `NULL` in case of error. - * - * Since: 0.1 - */ -raqm_t * -raqm_create (void) -{ - raqm_t *rq; - - rq = malloc (sizeof (raqm_t)); - if (!rq) - return NULL; - - rq->ref_count = 1; - - rq->base_dir = RAQM_DIRECTION_DEFAULT; - rq->resolved_dir = RAQM_DIRECTION_DEFAULT; - - rq->features = NULL; - rq->features_len = 0; - - rq->invisible_glyph = 0; - - rq->text = NULL; - rq->text_utf16 = NULL; - rq->text_utf8 = NULL; - rq->text_info = NULL; - rq->text_capacity_bytes = 0; - rq->text_len = 0; - - rq->runs = NULL; - rq->runs_pool = NULL; - - rq->glyphs = NULL; - rq->glyphs_capacity = 0; - - return rq; -} - -/** - * raqm_reference: - * @rq: a #raqm_t. - * - * Increases the reference count on @rq by one. This prevents @rq from being - * destroyed until a matching call to raqm_destroy() is made. - * - * Return value: - * The referenced #raqm_t. - * - * Since: 0.1 - */ -raqm_t * -raqm_reference (raqm_t *rq) -{ - if (rq) - rq->ref_count++; - - return rq; -} - -/** - * raqm_destroy: - * @rq: a #raqm_t. - * - * Decreases the reference count on @rq by one. If the result is zero, then @rq - * and all associated resources are freed. - * See raqm_reference(). - * - * Since: 0.1 - */ -void -raqm_destroy (raqm_t *rq) -{ - if (!rq || --rq->ref_count != 0) - return; - - _raqm_release_text_info (rq); - _raqm_free_text (rq); - _raqm_free_runs (rq->runs); - _raqm_free_runs (rq->runs_pool); - free (rq->glyphs); - free (rq->features); - free (rq); -} - -/** - * raqm_clear_contents: - * @rq: a #raqm_t. - * - * Clears internal state of previously used raqm_t object, making it ready - * for reuse and keeping some of allocated memory to increase performance. - * - * Since: 0.9 - */ -void -raqm_clear_contents (raqm_t *rq) -{ - if (!rq) - return; - - _raqm_release_text_info (rq); - - /* Return allocated runs to the pool, keep hb buffers for reuse */ - raqm_run_t *run = rq->runs; - while (run) - { - if (run->buffer) - hb_buffer_reset (run->buffer); - - if (run->font) - { - hb_font_destroy (run->font); - run->font = NULL; - } - - if (!run->next) - { - run->next = rq->runs_pool; - rq->runs_pool = rq->runs; - rq->runs = NULL; - break; - } - - run = run->next; - } - - rq->text_len = 0; - rq->resolved_dir = RAQM_DIRECTION_DEFAULT; -} - -/** - * raqm_set_text: - * @rq: a #raqm_t. - * @text: a UTF-32 encoded text string. - * @len: the length of @text. - * - * Adds @text to @rq to be used for layout. It must be a valid UTF-32 text, any - * invalid character will be replaced with U+FFFD. The text should typically - * represent a full paragraph, since doing the layout of chunks of text - * separately can give improper output. - * - * Return value: - * `true` if no errors happened, `false` otherwise. - * - * Since: 0.1 - */ -bool -raqm_set_text (raqm_t *rq, - const uint32_t *text, - size_t len) -{ - if (!rq || !text) - return false; - - /* Call raqm_clear_contents to reuse this raqm_t */ - if (rq->text_len) - return false; - - /* Empty string, don’t fail but do nothing */ - if (!len) - return true; - - if (!_raqm_alloc_text(rq, len, false, false)) - return false; - - rq->text_len = len; - memcpy (rq->text, text, sizeof (uint32_t) * len); - _raqm_init_text_info (rq); - - return true; -} - -static void * -_raqm_get_utf8_codepoint (const void *str, - uint32_t *out_codepoint) -{ - const char *s = (const char *)str; - - if (0xf0 == (0xf8 & s[0])) - { - *out_codepoint = ((0x07 & s[0]) << 18) | ((0x3f & s[1]) << 12) | ((0x3f & s[2]) << 6) | (0x3f & s[3]); - s += 4; - } - else if (0xe0 == (0xf0 & s[0])) - { - *out_codepoint = ((0x0f & s[0]) << 12) | ((0x3f & s[1]) << 6) | (0x3f & s[2]); - s += 3; - } - else if (0xc0 == (0xe0 & s[0])) - { - *out_codepoint = ((0x1f & s[0]) << 6) | (0x3f & s[1]); - s += 2; - } - else - { - *out_codepoint = s[0]; - s += 1; - } - - return (void *)s; -} - -static size_t -_raqm_u8_to_u32 (const char *text, size_t len, uint32_t *unicode) -{ - size_t in_len = 0; - uint32_t *out_utf32 = unicode; - const char *in_utf8 = text; - - while ((*in_utf8 != '\0') && (in_len < len)) - { - in_utf8 = _raqm_get_utf8_codepoint (in_utf8, out_utf32); - ++out_utf32; - ++in_len; - } - - return (out_utf32 - unicode); -} - -static void * -_raqm_get_utf16_codepoint (const void *str, - uint32_t *out_codepoint) -{ - const uint16_t *s = (const uint16_t *)str; - - if (s[0] > 0xD800 && s[0] < 0xDBFF) - { - if (s[1] > 0xDC00 && s[1] < 0xDFFF) - { - uint32_t X = ((s[0] & ((1 << 6) -1)) << 10) | (s[1] & ((1 << 10) -1)); - uint32_t W = (s[0] >> 6) & ((1 << 5) - 1); - *out_codepoint = (W+1) << 16 | X; - s += 2; - } - else - { - /* A single high surrogate, this is an error. */ - *out_codepoint = s[0]; - s += 1; - } - } - else - { - *out_codepoint = s[0]; - s += 1; - } - return (void *)s; -} - -static size_t -_raqm_u16_to_u32 (const uint16_t *text, size_t len, uint32_t *unicode) -{ - size_t in_len = 0; - uint32_t *out_utf32 = unicode; - const uint16_t *in_utf16 = text; - - while ((*in_utf16 != '\0') && (in_len < len)) - { - in_utf16 = _raqm_get_utf16_codepoint (in_utf16, out_utf32); - ++out_utf32; - ++in_len; - } - - return (out_utf32 - unicode); -} - -/** - * raqm_set_text_utf8: - * @rq: a #raqm_t. - * @text: a UTF-8 encoded text string. - * @len: the length of @text in UTF-8 bytes. - * - * Same as raqm_set_text(), but for text encoded in UTF-8 encoding. - * - * Return value: - * `true` if no errors happened, `false` otherwise. - * - * Since: 0.1 - */ -bool -raqm_set_text_utf8 (raqm_t *rq, - const char *text, - size_t len) -{ - if (!rq || !text) - return false; - - /* Call raqm_clear_contents to reuse this raqm_t */ - if (rq->text_len) - return false; - - /* Empty string, don’t fail but do nothing */ - if (!len) - return true; - - if (!_raqm_alloc_text(rq, len, true, false)) - return false; - - rq->text_len = _raqm_u8_to_u32 (text, len, rq->text); - memcpy (rq->text_utf8, text, sizeof (char) * len); - _raqm_init_text_info (rq); - - return true; -} - -/** - * raqm_set_text_utf16: - * @rq: a #raqm_t. - * @text: a UTF-16 encoded text string. - * @len: the length of @text in UTF-16 shorts. - * - * Same as raqm_set_text(), but for text encoded in UTF-16 encoding. - * - * Return value: - * `true` if no errors happened, `false` otherwise. - * - * Since: 0.10 - */ -bool -raqm_set_text_utf16 (raqm_t *rq, - const uint16_t *text, - size_t len) -{ - if (!rq || !text) - return false; - - /* Call raqm_clear_contents to reuse this raqm_t */ - if (rq->text_len) - return false; - - /* Empty string, don’t fail but do nothing */ - if (!len) - return true; - - if (!_raqm_alloc_text(rq, len, false, true)) - return false; - - rq->text_len = _raqm_u16_to_u32 (text, len, rq->text); - memcpy (rq->text_utf16, text, sizeof (uint16_t) * len); - _raqm_init_text_info (rq); - - return true; -} -/** - * raqm_set_par_direction: - * @rq: a #raqm_t. - * @dir: the direction of the paragraph. - * - * Sets the paragraph direction, also known as block direction in CSS. For - * horizontal text, this controls the overall direction in the Unicode - * Bidirectional Algorithm, so when the text is mainly right-to-left (with or - * without some left-to-right) text, then the base direction should be set to - * #RAQM_DIRECTION_RTL and vice versa. - * - * The default is #RAQM_DIRECTION_DEFAULT, which determines the paragraph - * direction based on the first character with strong bidi type (see [rule - * P2](https://unicode.org/reports/tr9/#P2) in Unicode Bidirectional Algorithm), - * which can be good enough for many cases but has problems when a mainly - * right-to-left paragraph starts with a left-to-right character and vice versa - * as the detected paragraph direction will be the wrong one, or when text does - * not contain any characters with string bidi types (e.g. only punctuation or - * numbers) as this will default to left-to-right paragraph direction. - * - * For vertical, top-to-bottom text, #RAQM_DIRECTION_TTB should be used. Raqm, - * however, provides limited vertical text support and does not handle rotated - * horizontal text in vertical text, instead everything is treated as vertical - * text. - * - * Return value: - * `true` if no errors happened, `false` otherwise. - * - * Since: 0.1 - */ -bool -raqm_set_par_direction (raqm_t *rq, - raqm_direction_t dir) -{ - if (!rq) - return false; - - rq->base_dir = dir; - - return true; -} - -/** - * raqm_set_language: - * @rq: a #raqm_t. - * @lang: a BCP47 language code. - * @start: index of first character that should use @face. - * @len: number of characters using @face. - * - * Sets a [BCP47 language - * code](https://www.w3.org/International/articles/language-tags/) to be used - * for @len-number of characters staring at @start. The @start and @len are - * input string array indices (i.e. counting bytes in UTF-8 and scaler values - * in UTF-32). - * - * This method can be used repeatedly to set different languages for different - * parts of the text. - * - * Return value: - * `true` if no errors happened, `false` otherwise. - * - * Stability: - * Unstable - * - * Since: 0.2 - */ -bool -raqm_set_language (raqm_t *rq, - const char *lang, - size_t start, - size_t len) -{ - hb_language_t language; - size_t end; - - if (!rq) - return false; - - if (!rq->text_len) - return true; - - end = _raqm_encoding_to_u32_index (rq, start + len); - start = _raqm_encoding_to_u32_index (rq, start); - - if (start >= rq->text_len || end > rq->text_len) - return false; - - if (!rq->text_info) - return false; - - language = hb_language_from_string (lang, -1); - for (size_t i = start; i < end; i++) - { - rq->text_info[i].lang = language; - } - - return true; -} - -static bool -_raqm_add_font_feature (raqm_t *rq, - hb_feature_t fea) -{ - void* new_features; - - if (!rq) - return false; - - new_features = realloc (rq->features, - sizeof (hb_feature_t) * (rq->features_len + 1)); - if (!new_features) - return false; - - if (fea.start != HB_FEATURE_GLOBAL_START) - fea.start = _raqm_encoding_to_u32_index (rq, fea.start); - if (fea.end != HB_FEATURE_GLOBAL_END) - fea.end = _raqm_encoding_to_u32_index (rq, fea.end); - - rq->features = new_features; - rq->features[rq->features_len] = fea; - rq->features_len++; - - return true; -} - -/** - * raqm_add_font_feature: - * @rq: a #raqm_t. - * @feature: (transfer none): a font feature string. - * @len: length of @feature, -1 for `NULL`-terminated. - * - * Adds a font feature to be used by the #raqm_t during text layout. This is - * usually used to turn on optional font features that are not enabled by - * default, for example `dlig` or `ss01`, but can be also used to turn off - * default font features. - * - * @feature is string representing a single font feature, in the syntax - * understood by hb_feature_from_string(). - * - * This function can be called repeatedly, new features will be appended to the - * end of the features list and can potentially override previous features. - * - * Return value: - * `true` if parsing @feature succeeded, `false` otherwise. - * - * Since: 0.1 - */ -bool -raqm_add_font_feature (raqm_t *rq, - const char *feature, - int len) -{ - hb_bool_t ok; - hb_feature_t fea; - - if (!rq) - return false; - - ok = hb_feature_from_string (feature, len, &fea); - if (ok) - _raqm_add_font_feature (rq, fea); - - return ok; -} - -static hb_font_t * -_raqm_create_hb_font (raqm_t *rq, - FT_Face face, - int loadflags) -{ - hb_font_t *font = hb_ft_font_create_referenced (face); - - if (loadflags >= 0) - hb_ft_font_set_load_flags (font, loadflags); - - return font; -} - -static bool -_raqm_set_freetype_face (raqm_t *rq, - FT_Face face, - size_t start, - size_t end) -{ - if (!rq) - return false; - - if (!rq->text_len) - return true; - - if (start >= rq->text_len || end > rq->text_len) - return false; - - if (!rq->text_info) - return false; - - for (size_t i = start; i < end; i++) - { - if (rq->text_info[i].ftface) - FT_Done_Face (rq->text_info[i].ftface); - rq->text_info[i].ftface = face; - FT_Reference_Face (face); - } - - return true; -} - -/** - * raqm_set_freetype_face: - * @rq: a #raqm_t. - * @face: an #FT_Face. - * - * Sets an #FT_Face to be used for all characters in @rq. - * - * See also raqm_set_freetype_face_range(). - * - * Return value: - * `true` if no errors happened, `false` otherwise. - * - * Since: 0.1 - */ -bool -raqm_set_freetype_face (raqm_t *rq, - FT_Face face) -{ - return _raqm_set_freetype_face (rq, face, 0, rq->text_len); -} - -/** - * raqm_set_freetype_face_range: - * @rq: a #raqm_t. - * @face: an #FT_Face. - * @start: index of first character that should use @face from the input string. - * @len: number of elements using @face. - * - * Sets an #FT_Face to be used for @len-number of characters staring at @start. - * The @start and @len are input string array indices, counting elements - * according to the underlying encoding. @start must always be aligned to the - * start of an encoded codepoint, and @len must always end at a codepoint's - * final element. - * - * This method can be used repeatedly to set different faces for different - * parts of the text. It is the responsibility of the client to make sure that - * face ranges cover the whole text, and is properly aligned. - * - * See also raqm_set_freetype_face(). - * - * Return value: - * `true` if no errors happened, `false` otherwise. - * - * Since: 0.1 - */ -bool -raqm_set_freetype_face_range (raqm_t *rq, - FT_Face face, - size_t start, - size_t len) -{ - size_t end; - - if (!rq) - return false; - - if (!rq->text_len) - return true; - - end = _raqm_encoding_to_u32_index (rq, start + len); - start = _raqm_encoding_to_u32_index (rq, start); - - return _raqm_set_freetype_face (rq, face, start, end); -} - -static bool -_raqm_set_freetype_load_flags (raqm_t *rq, - int flags, - size_t start, - size_t end) -{ - if (!rq) - return false; - - if (!rq->text_len) - return true; - - if (start >= rq->text_len || end > rq->text_len) - return false; - - if (!rq->text_info) - return false; - - for (size_t i = start; i < end; i++) - rq->text_info[i].ftloadflags = flags; - - return true; -} - -/** - * raqm_set_freetype_load_flags: - * @rq: a #raqm_t. - * @flags: FreeType load flags. - * - * Sets the load flags passed to FreeType when loading glyphs, should be the - * same flags used by the client when rendering FreeType glyphs. - * - * This requires version of HarfBuzz that has hb_ft_font_set_load_flags(), for - * older version the flags will be ignored. - * - * Return value: - * `true` if no errors happened, `false` otherwise. - * - * Since: 0.3 - */ -bool -raqm_set_freetype_load_flags (raqm_t *rq, - int flags) -{ - return _raqm_set_freetype_load_flags(rq, flags, 0, rq->text_len); -} - -/** - * raqm_set_freetype_load_flags_range: - * @rq: a #raqm_t. - * @flags: FreeType load flags. - * @start: index of first character that should use @flags. - * @len: number of characters using @flags. - * - * Sets the load flags passed to FreeType when loading glyphs for @len-number - * of characters staring at @start. Flags should be the same as used by the - * client when rendering corresponding FreeType glyphs. The @start and @len - * are input string array indices (i.e. counting bytes in UTF-8 and scaler - * values in UTF-32). - * - * This method can be used repeatedly to set different flags for different - * parts of the text. It is the responsibility of the client to make sure that - * flag ranges cover the whole text. - * - * This requires version of HarfBuzz that has hb_ft_font_set_load_flags(), for - * older version the flags will be ignored. - * - * See also raqm_set_freetype_load_flags(). - * - * Return value: - * `true` if no errors happened, `false` otherwise. - * - * Since: 0.9 - */ -bool -raqm_set_freetype_load_flags_range (raqm_t *rq, - int flags, - size_t start, - size_t len) -{ - size_t end; - - if (!rq) - return false; - - if (!rq->text_len) - return true; - - end = _raqm_encoding_to_u32_index (rq, start + len); - start = _raqm_encoding_to_u32_index (rq, start); - - return _raqm_set_freetype_load_flags (rq, flags, start, end); -} - -static bool -_raqm_set_spacing (raqm_t *rq, - int spacing, - bool word_spacing, - size_t start, - size_t end) -{ - if (!rq) - return false; - - if (!rq->text_len) - return true; - - if (start >= rq->text_len || end > rq->text_len) - return false; - - if (!rq->text_info) - return false; - - for (size_t i = start; i < end; i++) - { - bool set_spacing = i == 0; - if (!set_spacing) - set_spacing = _raqm_allowed_grapheme_boundary (rq->text[i-1], rq->text[i]); - - if (set_spacing) - { - if (word_spacing) - { - if (_raqm_allowed_grapheme_boundary (rq->text[i], rq->text[i+1])) - { - /* CSS word seperators, word spacing is only applied on these.*/ - if (rq->text[i] == 0x0020 || /* Space */ - rq->text[i] == 0x00A0 || /* No Break Space */ - rq->text[i] == 0x1361 || /* Ethiopic Word Space */ - rq->text[i] == 0x10100 || /* Aegean Word Seperator Line */ - rq->text[i] == 0x10101 || /* Aegean Word Seperator Dot */ - rq->text[i] == 0x1039F || /* Ugaric Word Divider */ - rq->text[i] == 0x1091F) /* Phoenician Word Separator */ - { - rq->text_info[i].spacing_after = spacing; - } - } - } - else - { - rq->text_info[i].spacing_after = spacing; - } - } - } - - return true; -} - -/** - * raqm_set_letter_spacing_range: - * @rq: a #raqm_t. - * @spacing: amount of spacing in Freetype Font Units (26.6 format). - * @start: index of first character that should use @spacing. - * @len: number of characters using @spacing. - * - * Set the letter spacing or tracking for a given range, the value - * will be added onto the advance and offset for RTL, and the advance for - * other directions. Letter spacing will be applied between characters, so - * the last character will not have spacing applied after it. - * Note that not all scripts have a letter-spacing tradition, - * for example, Arabic does not, while Devanagari does. - * - * This will also add “disable `liga`, `clig`, `hlig`, `dlig`, and `calt`†font - * features to the internal features list, so call this function after setting - * the font features for best spacing results. - * - * Return value: - * `true` if no errors happened, `false` otherwise. - * - * Since: 0.10 - */ -bool -raqm_set_letter_spacing_range(raqm_t *rq, - int spacing, - size_t start, - size_t len) -{ - size_t end; - - if (!rq) - return false; - - if (!rq->text_len) - return true; - - end = start + len - 1; - - if (spacing != 0) - { -#define NUM_TAGS 5 - static char *tags[NUM_TAGS] = { "clig", "liga", "hlig", "dlig", "calt" }; - for (size_t i = 0; i < NUM_TAGS; i++) - { - hb_feature_t fea = { hb_tag_from_string(tags[i], 5), 0, start, end }; - _raqm_add_font_feature (rq, fea); - } -#undef NUM_TAGS - } - - start = _raqm_encoding_to_u32_index (rq, start); - end = _raqm_encoding_to_u32_index (rq, end); - - return _raqm_set_spacing (rq, spacing, false, start, end); -} - -/** - * raqm_set_word_spacing_range: - * @rq: a #raqm_t. - * @spacing: amount of spacing in Freetype Font Units (26.6 format). - * @start: index of first character that should use @spacing. - * @len: number of characters using @spacing. - * - * Set the word spacing for a given range. Word spacing will only be applied to - * 'word separator' characters, such as 'space', 'no break space' and - * Ethiopic word separator'. - * The value will be added onto the advance and offset for RTL, and the advance - * for other directions. - * - * Return value: - * `true` if no errors happened, `false` otherwise. - * - * Since: 0.10 - */ -bool -raqm_set_word_spacing_range(raqm_t *rq, - int spacing, - size_t start, - size_t len) -{ - size_t end; - - if (!rq) - return false; - - if (!rq->text_len) - return true; - - end = _raqm_encoding_to_u32_index (rq, start + len); - start = _raqm_encoding_to_u32_index (rq, start); - - return _raqm_set_spacing (rq, spacing, true, start, end); -} - -/** - * raqm_set_invisible_glyph: - * @rq: a #raqm_t. - * @gid: glyph id to use for invisible glyphs. - * - * Sets the glyph id to be used for invisible glyhphs. - * - * If @gid is negative, invisible glyphs will be suppressed from the output. - * - * If @gid is zero, invisible glyphs will be rendered as space. - * - * If @gid is a positive number, it will be used for invisible glyphs. - * - * Return value: - * `true` if no errors happened, `false` otherwise. - * - * Since: 0.6 - */ -bool -raqm_set_invisible_glyph (raqm_t *rq, - int gid) -{ - if (!rq) - return false; - - rq->invisible_glyph = gid; - return true; -} - -static bool -_raqm_itemize (raqm_t *rq); - -static bool -_raqm_shape (raqm_t *rq); - -/** - * raqm_layout: - * @rq: a #raqm_t. - * - * Run the text layout process on @rq. This is the main Raqm function where the - * Unicode Bidirectional Text algorithm will be applied to the text in @rq, - * text shaping, and any other part of the layout process. - * - * Return value: - * `true` if the layout process was successful, `false` otherwise. - * - * Since: 0.1 - */ -bool -raqm_layout (raqm_t *rq) -{ - if (!rq) - return false; - - if (!rq->text_len) - return true; - - if (!rq->text_info) - return false; - - for (size_t i = 0; i < rq->text_len; i++) - { - if (!rq->text_info[i].ftface) - return false; - } - - if (!_raqm_itemize (rq)) - return false; - - if (!_raqm_shape (rq)) - return false; - - return true; -} - -static uint32_t -_raqm_u32_to_u8_index (raqm_t *rq, - uint32_t index); -static uint32_t -_raqm_u32_to_u16_index (raqm_t *rq, - uint32_t index); -/** - * raqm_get_glyphs: - * @rq: a #raqm_t. - * @length: (out): output array length. - * - * Gets the final result of Raqm layout process, an array of #raqm_glyph_t - * containing the glyph indices in the font, their positions and other possible - * information. - * - * Return value: (transfer none): - * An array of #raqm_glyph_t, or `NULL` in case of error. This is owned by @rq - * and must not be freed. - * - * Since: 0.1 - */ -raqm_glyph_t * -raqm_get_glyphs (raqm_t *rq, - size_t *length) -{ - size_t count = 0; - - if (!rq || !rq->runs || !length) - { - if (length) - *length = 0; - return NULL; - } - - for (raqm_run_t *run = rq->runs; run != NULL; run = run->next) - count += hb_buffer_get_length (run->buffer); - - if (count > rq->glyphs_capacity) - { - void* new_mem = realloc (rq->glyphs, sizeof (raqm_glyph_t) * count); - if (!new_mem) - { - *length = 0; - return NULL; - } - - rq->glyphs = new_mem; - rq->glyphs_capacity = count; - } - - *length = count; - - RAQM_TEST ("Glyph information:\n"); - - count = 0; - for (raqm_run_t *run = rq->runs; run != NULL; run = run->next) - { - size_t len; - hb_glyph_info_t *info; - hb_glyph_position_t *position; - - len = hb_buffer_get_length (run->buffer); - info = hb_buffer_get_glyph_infos (run->buffer, NULL); - position = hb_buffer_get_glyph_positions (run->buffer, NULL); - - for (size_t i = 0; i < len; i++) - { - rq->glyphs[count + i].index = info[i].codepoint; - rq->glyphs[count + i].cluster = info[i].cluster; - rq->glyphs[count + i].x_advance = position[i].x_advance; - rq->glyphs[count + i].y_advance = position[i].y_advance; - rq->glyphs[count + i].x_offset = position[i].x_offset; - rq->glyphs[count + i].y_offset = position[i].y_offset; - rq->glyphs[count + i].ftface = rq->text_info[info[i].cluster].ftface; - - RAQM_TEST ("glyph [%d]\tx_offset: %d\ty_offset: %d\tx_advance: %d\tfont: %s\n", - rq->glyphs[count + i].index, rq->glyphs[count + i].x_offset, - rq->glyphs[count + i].y_offset, rq->glyphs[count + i].x_advance, - rq->glyphs[count + i].ftface->family_name); - } - - count += len; - } - - if (rq->text_utf8) - { -#ifdef RAQM_TESTING - RAQM_TEST ("\nUTF-32 clusters:"); - for (size_t i = 0; i < count; i++) - RAQM_TEST (" %02d", rq->glyphs[i].cluster); - RAQM_TEST ("\n"); -#endif - - for (size_t i = 0; i < count; i++) - rq->glyphs[i].cluster = _raqm_u32_to_u8_index (rq, - rq->glyphs[i].cluster); - -#ifdef RAQM_TESTING - RAQM_TEST ("UTF-8 clusters: "); - for (size_t i = 0; i < count; i++) - RAQM_TEST (" %02d", rq->glyphs[i].cluster); - RAQM_TEST ("\n"); -#endif - } - else if (rq->text_utf16) - { - for (size_t i = 0; i < count; i++) - rq->glyphs[i].cluster = _raqm_u32_to_u16_index (rq, - rq->glyphs[i].cluster); - } - return rq->glyphs; -} - -/** - * raqm_get_par_resolved_direction: - * @rq: a #raqm_t. - * - * Gets the resolved direction of the paragraph; - * - * Return value: - * The #raqm_direction_t specifying the resolved direction of text, - * or #RAQM_DIRECTION_DEFAULT if raqm_layout() has not been called on @rq. - * - * Since: 0.8 - */ -raqm_direction_t -raqm_get_par_resolved_direction (raqm_t *rq) -{ - if (!rq) - return RAQM_DIRECTION_DEFAULT; - - return rq->resolved_dir; -} - -/** - * raqm_get_direction_at_index: - * @rq: a #raqm_t. - * @index: (in): character index. - * - * Gets the resolved direction of the character at specified index; - * - * Return value: - * The #raqm_direction_t specifying the resolved direction of text at the - * specified index, or #RAQM_DIRECTION_DEFAULT if raqm_layout() has not been - * called on @rq. - * - * Since: 0.8 - */ -raqm_direction_t -raqm_get_direction_at_index (raqm_t *rq, - size_t index) -{ - if (!rq) - return RAQM_DIRECTION_DEFAULT; - - for (raqm_run_t *run = rq->runs; run != NULL; run = run->next) - { - if (run->pos <= index && index < run->pos + run->len) - { - switch (run->direction) - { - case HB_DIRECTION_LTR: - return RAQM_DIRECTION_LTR; - case HB_DIRECTION_RTL: - return RAQM_DIRECTION_RTL; - case HB_DIRECTION_TTB: - return RAQM_DIRECTION_TTB; - default: - return RAQM_DIRECTION_DEFAULT; - } - } - } - - return RAQM_DIRECTION_DEFAULT; -} - -static bool -_raqm_resolve_scripts (raqm_t *rq); - -static hb_direction_t -_raqm_hb_dir (raqm_t *rq, _raqm_bidi_level_t level) -{ - hb_direction_t dir = HB_DIRECTION_LTR; - - if (rq->base_dir == RAQM_DIRECTION_TTB) - dir = HB_DIRECTION_TTB; - else if (RAQM_BIDI_LEVEL_IS_RTL(level)) - dir = HB_DIRECTION_RTL; - - return dir; -} - -typedef struct -{ - size_t pos; - size_t len; - _raqm_bidi_level_t level; -} _raqm_bidi_run; - -#ifdef RAQM_SHEENBIDI -static _raqm_bidi_run * -_raqm_bidi_itemize (raqm_t *rq, size_t *run_count) -{ - _raqm_bidi_run *runs; - SBAlgorithmRef bidi; - SBParagraphRef par; - SBUInteger par_len; - SBLineRef line; - - SBLevel base_level = SBLevelDefaultLTR; - SBCodepointSequence input = { - SBStringEncodingUTF32, - (void *) rq->text, - rq->text_len - }; - - if (rq->base_dir == RAQM_DIRECTION_RTL) - base_level = 1; - else if (rq->base_dir == RAQM_DIRECTION_LTR) - base_level = 0; - - /* paragraph */ - bidi = SBAlgorithmCreate (&input); - par = SBAlgorithmCreateParagraph (bidi, 0, INT32_MAX, base_level); - par_len = SBParagraphGetLength (par); - - /* lines */ - line = SBParagraphCreateLine (par, 0, par_len); - *run_count = SBLineGetRunCount (line); - - if (SBParagraphGetBaseLevel (par) == 1) - rq->resolved_dir = RAQM_DIRECTION_RTL; - else - rq->resolved_dir = RAQM_DIRECTION_LTR; - - runs = malloc (sizeof (_raqm_bidi_run) * (*run_count)); - if (runs) - { - const SBRun *sheenbidi_runs = SBLineGetRunsPtr(line); - - for (size_t i = 0; i < (*run_count); ++i) - { - runs[i].pos = sheenbidi_runs[i].offset; - runs[i].len = sheenbidi_runs[i].length; - runs[i].level = sheenbidi_runs[i].level; - } - } - - SBLineRelease (line); - SBParagraphRelease (par); - SBAlgorithmRelease (bidi); - - return runs; -} -#else -static void -_raqm_reverse_run (_raqm_bidi_run *run, const size_t len) -{ - assert (run); - - for (size_t i = 0; i < len / 2; i++) - { - _raqm_bidi_run temp = run[i]; - run[i] = run[len - 1 - i]; - run[len - 1 - i] = temp; - } -} - -static _raqm_bidi_run * -_raqm_reorder_runs (const FriBidiCharType *types, - const size_t len, - const FriBidiParType base_dir, - /* input and output */ - FriBidiLevel *levels, - /* output */ - size_t *run_count) -{ - FriBidiLevel level; - FriBidiLevel last_level = -1; - FriBidiLevel max_level = 0; - size_t run_start = 0; - size_t run_index = 0; - _raqm_bidi_run *runs = NULL; - size_t count = 0; - - if (len == 0) - { - *run_count = 0; - return NULL; - } - - assert (types); - assert (levels); - - /* L1. Reset the embedding levels of some chars: - 4. any sequence of white space characters at the end of the line. */ - for (int i = len - 1; - i >= 0 && FRIBIDI_IS_EXPLICIT_OR_BN_OR_WS (types[i]); i--) - { - levels[i] = FRIBIDI_DIR_TO_LEVEL (base_dir); - } - - /* Find max_level of the line. We don't reuse the paragraph - * max_level, both for a cleaner API, and that the line max_level - * may be far less than paragraph max_level. */ - for (int i = len - 1; i >= 0; i--) - { - if (levels[i] > max_level) - max_level = levels[i]; - } - - for (size_t i = 0; i < len; i++) - { - if (levels[i] != last_level) - count++; - - last_level = levels[i]; - } - - runs = malloc (sizeof (_raqm_bidi_run) * count); - - while (run_start < len) - { - size_t run_end = run_start; - while (run_end < len && levels[run_start] == levels[run_end]) - { - run_end++; - } - - runs[run_index].pos = run_start; - runs[run_index].level = levels[run_start]; - runs[run_index].len = run_end - run_start; - run_start = run_end; - run_index++; - } - - /* L2. Reorder. */ - for (level = max_level; level > 0; level--) - { - for (int i = count - 1; i >= 0; i--) - { - if (runs[i].level >= level) - { - int end = i; - for (i--; (i >= 0 && runs[i].level >= level); i--) - ; - _raqm_reverse_run (runs + i + 1, end - i); - } - } - } - - *run_count = count; - return runs; -} - -static _raqm_bidi_run * -_raqm_bidi_itemize (raqm_t *rq, size_t *run_count) -{ - FriBidiParType par_type = FRIBIDI_PAR_ON; - _raqm_bidi_run *runs = NULL; - - FriBidiCharType *types; - _raqm_bidi_level_t *levels; - int max_level = 0; - FriBidiBracketType *btypes; - - types = calloc (rq->text_len, sizeof (FriBidiCharType)); - btypes = calloc (rq->text_len, sizeof (FriBidiBracketType)); - levels = calloc (rq->text_len, sizeof (_raqm_bidi_level_t)); - - if (!types || !levels || !btypes) - goto done; - - if (rq->base_dir == RAQM_DIRECTION_RTL) - par_type = FRIBIDI_PAR_RTL; - else if (rq->base_dir == RAQM_DIRECTION_LTR) - par_type = FRIBIDI_PAR_LTR; - - fribidi_get_bidi_types (rq->text, rq->text_len, types); - fribidi_get_bracket_types (rq->text, rq->text_len, types, btypes); - max_level = fribidi_get_par_embedding_levels_ex (types, btypes, - rq->text_len, &par_type, - levels); - - if (par_type == FRIBIDI_PAR_RTL) - rq->resolved_dir = RAQM_DIRECTION_RTL; - else - rq->resolved_dir = RAQM_DIRECTION_LTR; - - if (max_level == 0) - goto done; - - /* Get the number of bidi runs */ - runs = _raqm_reorder_runs (types, rq->text_len, par_type, levels, run_count); - -done: - free (types); - free (levels); - free (btypes); - - return runs; -} -#endif - -static bool -_raqm_itemize (raqm_t *rq) -{ - _raqm_bidi_run *runs = NULL; - raqm_run_t *last; - size_t run_count = 0; - bool ok = true; - -#ifdef RAQM_TESTING - static char *dir_names[] = { - "DEFAULT", - "RTL", - "LTR", - "TTB" - }; - - assert (rq->base_dir < sizeof (dir_names)); - RAQM_TEST ("Direction is: %s\n\n", dir_names[rq->base_dir]); -#endif - - if (!_raqm_resolve_scripts (rq)) - { - ok = false; - goto done; - } - - if (rq->base_dir == RAQM_DIRECTION_TTB) - { - /* Treat every thing as LTR in vertical text */ - run_count = 1; - rq->resolved_dir = RAQM_DIRECTION_TTB; - runs = malloc (sizeof (_raqm_bidi_run)); - if (runs) - { - runs->pos = 0; - runs->len = rq->text_len; - runs->level = 0; - } - } - else - runs = _raqm_bidi_itemize (rq, &run_count); - - if (!runs) - { - ok = false; - goto done; - } - -#ifdef RAQM_TESTING - assert (rq->resolved_dir < sizeof (dir_names)); - if (rq->base_dir == RAQM_DIRECTION_DEFAULT) - RAQM_TEST ("Resolved direction is: %s\n\n", dir_names[rq->resolved_dir]); - RAQM_TEST ("Number of runs before script itemization: %zu\n\n", run_count); - - RAQM_TEST ("BiDi Runs:\n"); - for (size_t i = 0; i < run_count; i++) - { - RAQM_TEST ("run[%zu]:\t start: %zu\tlength: %zu\tlevel: %d\n", - i, runs[i].pos, runs[i].len, runs[i].level); - } - RAQM_TEST ("\n"); -#endif - - last = NULL; - for (size_t i = 0; i < run_count; i++) - { - raqm_run_t *run = _raqm_alloc_run (rq); - if (!run) - { - ok = false; - goto done; - } - - if (!rq->runs) - rq->runs = run; - - if (last) - last->next = run; - - run->direction = _raqm_hb_dir (rq, runs[i].level); - - if (HB_DIRECTION_IS_BACKWARD (run->direction)) - { - run->pos = runs[i].pos + runs[i].len - 1; - run->script = rq->text_info[run->pos].script; - run->font = _raqm_create_hb_font (rq, rq->text_info[run->pos].ftface, - rq->text_info[run->pos].ftloadflags); - for (int j = runs[i].len - 1; j >= 0; j--) - { - _raqm_text_info info = rq->text_info[runs[i].pos + j]; - if (!_raqm_compare_text_info (rq->text_info[run->pos], info)) - { - raqm_run_t *newrun = _raqm_alloc_run (rq); - if (!newrun) - { - ok = false; - goto done; - } - newrun->pos = runs[i].pos + j; - newrun->len = 1; - newrun->direction = _raqm_hb_dir (rq, runs[i].level); - newrun->script = info.script; - newrun->font = _raqm_create_hb_font (rq, info.ftface, - info.ftloadflags); - run->next = newrun; - run = newrun; - } - else - { - run->len++; - run->pos = runs[i].pos + j; - } - } - } - else - { - run->pos = runs[i].pos; - run->script = rq->text_info[run->pos].script; - run->font = _raqm_create_hb_font (rq, rq->text_info[run->pos].ftface, - rq->text_info[run->pos].ftloadflags); - for (size_t j = 0; j < runs[i].len; j++) - { - _raqm_text_info info = rq->text_info[runs[i].pos + j]; - if (!_raqm_compare_text_info (rq->text_info[run->pos], info)) - { - raqm_run_t *newrun = _raqm_alloc_run (rq); - if (!newrun) - { - ok = false; - goto done; - } - newrun->pos = runs[i].pos + j; - newrun->len = 1; - newrun->direction = _raqm_hb_dir (rq, runs[i].level); - newrun->script = info.script; - newrun->font = _raqm_create_hb_font (rq, info.ftface, - info.ftloadflags); - run->next = newrun; - run = newrun; - } - else - run->len++; - } - } - - last = run; - last->next = NULL; - } - -#ifdef RAQM_TESTING - run_count = 0; - for (raqm_run_t *run = rq->runs; run != NULL; run = run->next) - run_count++; - RAQM_TEST ("Number of runs after script itemization: %zu\n\n", run_count); - - run_count = 0; - RAQM_TEST ("Final Runs:\n"); - for (raqm_run_t *run = rq->runs; run != NULL; run = run->next) - { - SCRIPT_TO_STRING (run->script); - RAQM_TEST ("run[%zu]:\t start: %d\tlength: %d\tdirection: %s\tscript: %s\tfont: %s\n", - run_count++, run->pos, run->len, - hb_direction_to_string (run->direction), buff, - rq->text_info[run->pos].ftface->family_name); - } - RAQM_TEST ("\n"); -#endif - -done: - free (runs); - - return ok; -} - -/* Stack to handle script detection */ -typedef struct -{ - size_t capacity; - size_t size; - int *pair_index; - hb_script_t *script; -} _raqm_stack_t; - -/* Special paired characters for script detection */ -static size_t paired_len = 34; -static const uint32_t paired_chars[] = -{ - 0x0028, 0x0029, /* ascii paired punctuation */ - 0x003c, 0x003e, - 0x005b, 0x005d, - 0x007b, 0x007d, - 0x00ab, 0x00bb, /* guillemets */ - 0x2018, 0x2019, /* general punctuation */ - 0x201c, 0x201d, - 0x2039, 0x203a, - 0x3008, 0x3009, /* chinese paired punctuation */ - 0x300a, 0x300b, - 0x300c, 0x300d, - 0x300e, 0x300f, - 0x3010, 0x3011, - 0x3014, 0x3015, - 0x3016, 0x3017, - 0x3018, 0x3019, - 0x301a, 0x301b -}; - -static void -_raqm_stack_free (_raqm_stack_t *stack) -{ - free (stack->script); - free (stack->pair_index); - free (stack); -} - -/* Stack handling functions */ -static _raqm_stack_t * -_raqm_stack_new (size_t max) -{ - _raqm_stack_t *stack; - stack = calloc (1, sizeof (_raqm_stack_t)); - if (!stack) - return NULL; - - stack->script = malloc (sizeof (hb_script_t) * max); - if (!stack->script) - { - _raqm_stack_free (stack); - return NULL; - } - - stack->pair_index = malloc (sizeof (int) * max); - if (!stack->pair_index) - { - _raqm_stack_free (stack); - return NULL; - } - - stack->size = 0; - stack->capacity = max; - - return stack; -} - -static bool -_raqm_stack_pop (_raqm_stack_t *stack) -{ - if (!stack->size) - { - RAQM_DBG ("Stack is Empty\n"); - return false; - } - - stack->size--; - - return true; -} - -static hb_script_t -_raqm_stack_top (_raqm_stack_t *stack) -{ - if (!stack->size) - { - RAQM_DBG ("Stack is Empty\n"); - return HB_SCRIPT_INVALID; /* XXX: check this */ - } - - return stack->script[stack->size]; -} - -static bool -_raqm_stack_push (_raqm_stack_t *stack, - hb_script_t script, - int pair_index) -{ - if (stack->size == stack->capacity) - { - RAQM_DBG ("Stack is Full\n"); - return false; - } - - stack->size++; - stack->script[stack->size] = script; - stack->pair_index[stack->size] = pair_index; - - return true; -} - -static int -_get_pair_index (const uint32_t ch) -{ - int lower = 0; - int upper = paired_len - 1; - - while (lower <= upper) - { - int mid = (lower + upper) / 2; - if (ch < paired_chars[mid]) - upper = mid - 1; - else if (ch > paired_chars[mid]) - lower = mid + 1; - else - return mid; - } - - return -1; -} - -#define STACK_IS_EMPTY(script) ((script)->size <= 0) -#define IS_OPEN(pair_index) (((pair_index) & 1) == 0) - -static hb_script_t -_raqm_unicode_script (hb_codepoint_t u) -{ - static hb_unicode_funcs_t* unicode_funcs; - - unicode_funcs = hb_unicode_funcs_get_default (); - - /* Make combining marks inherit the script of their bases, regardless of - * their own script. - */ - if (hb_unicode_general_category (unicode_funcs, u) == HB_UNICODE_GENERAL_CATEGORY_NON_SPACING_MARK) - return HB_SCRIPT_INHERITED; - - return hb_unicode_script (unicode_funcs, u); -} - -/* Resolve the script for each character in the input string, if the character - * script is common or inherited it takes the script of the character before it - * except paired characters which we try to make them use the same script. We - * then split the BiDi runs, if necessary, on script boundaries. - */ -static bool -_raqm_resolve_scripts (raqm_t *rq) -{ - int last_script_index = -1; - int last_set_index = -1; - hb_script_t last_script = HB_SCRIPT_INVALID; - _raqm_stack_t *stack = NULL; - - for (size_t i = 0; i < rq->text_len; ++i) - rq->text_info[i].script = _raqm_unicode_script (rq->text[i]); - -#ifdef RAQM_TESTING - RAQM_TEST ("Before script detection:\n"); - for (size_t i = 0; i < rq->text_len; ++i) - { - SCRIPT_TO_STRING (rq->text_info[i].script); - RAQM_TEST ("script for ch[%zu]\t%s\n", i, buff); - } - RAQM_TEST ("\n"); -#endif - - stack = _raqm_stack_new (rq->text_len); - if (!stack) - return false; - - for (int i = 0; i < (int) rq->text_len; i++) - { - if (rq->text_info[i].script == HB_SCRIPT_COMMON && last_script_index != -1) - { - int pair_index = _get_pair_index (rq->text[i]); - if (pair_index >= 0) - { - if (IS_OPEN (pair_index)) - { - /* is a paired character */ - rq->text_info[i].script = last_script; - last_set_index = i; - _raqm_stack_push (stack, rq->text_info[i].script, pair_index); - } - else - { - /* is a close paired character */ - /* find matching opening (by getting the last even index for current - * odd index) */ - while (!STACK_IS_EMPTY (stack) && - stack->pair_index[stack->size] != (pair_index & ~1)) - { - _raqm_stack_pop (stack); - } - if (!STACK_IS_EMPTY (stack)) - { - rq->text_info[i].script = _raqm_stack_top (stack); - last_script = rq->text_info[i].script; - last_set_index = i; - } - else - { - rq->text_info[i].script = last_script; - last_set_index = i; - } - } - } - else - { - rq->text_info[i].script = last_script; - last_set_index = i; - } - } - else if (rq->text_info[i].script == HB_SCRIPT_INHERITED && - last_script_index != -1) - { - rq->text_info[i].script = last_script; - last_set_index = i; - } - else - { - for (int j = last_set_index + 1; j < i; ++j) - rq->text_info[j].script = rq->text_info[i].script; - last_script = rq->text_info[i].script; - last_script_index = i; - last_set_index = i; - } - } - - /* Loop backwards and change any remaining Common or Inherit characters to - * take the script if the next character. - * https://github.com/HOST-Oman/libraqm/issues/95 - */ - for (int i = rq->text_len - 2; i >= 0; --i) - { - if (rq->text_info[i].script == HB_SCRIPT_INHERITED || - rq->text_info[i].script == HB_SCRIPT_COMMON) - rq->text_info[i].script = rq->text_info[i + 1].script; - } - -#ifdef RAQM_TESTING - RAQM_TEST ("After script detection:\n"); - for (size_t i = 0; i < rq->text_len; ++i) - { - SCRIPT_TO_STRING (rq->text_info[i].script); - RAQM_TEST ("script for ch[%zu]\t%s\n", i, buff); - } - RAQM_TEST ("\n"); -#endif - - _raqm_stack_free (stack); - - return true; -} - -static void -_raqm_ft_transform (int *x, - int *y, - FT_Matrix matrix) -{ - FT_Vector vector; - vector.x = *x; - vector.y = *y; - - FT_Vector_Transform (&vector, &matrix); - - *x = vector.x; - *y = vector.y; -} - -static bool -_raqm_shape (raqm_t *rq) -{ - hb_buffer_flags_t hb_buffer_flags = HB_BUFFER_FLAG_BOT | HB_BUFFER_FLAG_EOT; - - if (rq->invisible_glyph < 0) - hb_buffer_flags |= HB_BUFFER_FLAG_REMOVE_DEFAULT_IGNORABLES; - - for (raqm_run_t *run = rq->runs; run != NULL; run = run->next) - { - if (!run->buffer) - run->buffer = hb_buffer_create (); - - hb_buffer_add_utf32 (run->buffer, rq->text, rq->text_len, - run->pos, run->len); - hb_buffer_set_script (run->buffer, run->script); - hb_buffer_set_language (run->buffer, rq->text_info[run->pos].lang); - hb_buffer_set_direction (run->buffer, run->direction); - hb_buffer_set_flags (run->buffer, hb_buffer_flags); - - if (rq->invisible_glyph > 0) - hb_buffer_set_invisible_glyph (run->buffer, rq->invisible_glyph); - - hb_shape_full (run->font, run->buffer, rq->features, rq->features_len, - NULL); - - { - FT_Matrix matrix; - hb_glyph_info_t *info; - hb_glyph_position_t *pos; - unsigned int len; - - FT_Get_Transform (hb_ft_font_get_face (run->font), &matrix, NULL); - pos = hb_buffer_get_glyph_positions (run->buffer, &len); - info = hb_buffer_get_glyph_infos (run->buffer, &len); - - for (unsigned int i = 0; i < len; i++) - { - _raqm_ft_transform (&pos[i].x_advance, &pos[i].y_advance, matrix); - _raqm_ft_transform (&pos[i].x_offset, &pos[i].y_offset, matrix); - - bool set_spacing = false; - if (run->direction == HB_DIRECTION_RTL) - { - set_spacing = i == 0; - if (!set_spacing) - set_spacing = info[i].cluster != info[i-1].cluster; - } - else - { - set_spacing = i == len - 1; - if (!set_spacing) - set_spacing = info[i].cluster != info[i+1].cluster; - } - - _raqm_text_info rq_info = rq->text_info[info[i].cluster]; - - if (rq_info.spacing_after != 0 && set_spacing) - { - if (run->direction == HB_DIRECTION_TTB) - pos[i].y_advance -= rq_info.spacing_after; - else if (run->direction == HB_DIRECTION_RTL) - { - pos[i].x_advance += rq_info.spacing_after; - pos[i].x_offset += rq_info.spacing_after; - } - else - pos[i].x_advance += rq_info.spacing_after; - } - } - } - } - - return true; -} - -/* Count equivalent UTF-8 bytes in codepoint */ -static size_t -_raqm_count_codepoint_utf8_bytes (uint32_t chr) -{ - if (0 == ((uint32_t) 0xffffff80 & chr)) - return 1; - else if (0 == ((uint32_t) 0xfffff800 & chr)) - return 2; - else if (0 == ((uint32_t) 0xffff0000 & chr)) - return 3; - else - return 4; -} - -/* Convert index from UTF-32 to UTF-8 */ -static uint32_t -_raqm_u32_to_u8_index (raqm_t *rq, - uint32_t index) -{ - size_t length = 0; - - for (uint32_t i = 0; i < index; ++i) - length += _raqm_count_codepoint_utf8_bytes (rq->text[i]); - - return length; -} - -/* Convert index from UTF-8 to UTF-32 */ -static size_t -_raqm_u8_to_u32_index (raqm_t *rq, - size_t index) -{ - const unsigned char *s = (const unsigned char *) rq->text_utf8; - const unsigned char *t = s; - size_t length = 0; - - while (((size_t) (s - t) < index) && ('\0' != *s)) - { - if (0xf0 == (0xf8 & *s)) - s += 4; - else if (0xe0 == (0xf0 & *s)) - s += 3; - else if (0xc0 == (0xe0 & *s)) - s += 2; - else - s += 1; - - length++; - } - - if ((size_t) (s-t) > index) - length--; - - return length; -} - -/* Count equivalent UTF-16 short in codepoint */ -static size_t -_raqm_count_codepoint_utf16_short (uint32_t chr) -{ - if (chr > 0x010000) - return 2; - else - return 1; -} - -/* Convert index from UTF-32 to UTF-16 */ -static uint32_t -_raqm_u32_to_u16_index (raqm_t *rq, - uint32_t index) -{ - size_t length = 0; - - for (uint32_t i = 0; i < index; ++i) - length += _raqm_count_codepoint_utf16_short (rq->text[i]); - - return length; -} - -/* Convert index from UTF-16 to UTF-32 */ -static size_t -_raqm_u16_to_u32_index (raqm_t *rq, - size_t index) -{ - const uint16_t *s = (const uint16_t *) rq->text_utf16; - const uint16_t *t = s; - size_t length = 0; - - while (((size_t) (s - t) < index) && ('\0' != *s)) - { - if (*s < 0xD800 || *s > 0xDBFF) - s += 1; - else - s += 2; - - length++; - } - - if ((size_t) (s-t) > index) - length--; - - return length; -} - -static inline size_t -_raqm_encoding_to_u32_index (raqm_t *rq, - size_t index) -{ - if (rq->text_utf8) - return _raqm_u8_to_u32_index (rq, index); - else if (rq->text_utf16) - return _raqm_u16_to_u32_index (rq, index); - return index; -} - -static bool -_raqm_in_hangul_syllable (hb_codepoint_t ch); - -/** - * raqm_index_to_position: - * @rq: a #raqm_t. - * @index: (inout): character index. - * @x: (out): output x position. - * @y: (out): output y position. - * - * Calculates the cursor position after the character at @index. If the character - * is right-to-left, then the cursor will be at the left of it, whereas if the - * character is left-to-right, then the cursor will be at the right of it. - * - * Return value: - * `true` if the process was successful, `false` otherwise. - * - * Since: 0.2 - */ -bool -raqm_index_to_position (raqm_t *rq, - size_t *index, - int *x, - int *y) -{ - /* We don't currently support multiline, so y is always 0 */ - *y = 0; - *x = 0; - - if (rq == NULL) - return false; - - *index = _raqm_encoding_to_u32_index (rq, *index); - - if (*index >= rq->text_len) - return false; - - RAQM_TEST ("\n"); - - while (*index < rq->text_len) - { - if (_raqm_allowed_grapheme_boundary (rq->text[*index], rq->text[*index + 1])) - break; - - ++*index; - } - - for (raqm_run_t *run = rq->runs; run != NULL; run = run->next) - { - size_t len; - hb_glyph_info_t *info; - hb_glyph_position_t *position; - len = hb_buffer_get_length (run->buffer); - info = hb_buffer_get_glyph_infos (run->buffer, NULL); - position = hb_buffer_get_glyph_positions (run->buffer, NULL); - - for (size_t i = 0; i < len; i++) - { - uint32_t curr_cluster = info[i].cluster; - uint32_t next_cluster = curr_cluster; - *x += position[i].x_advance; - - if (run->direction == HB_DIRECTION_LTR) - { - for (size_t j = i + 1; j < len && next_cluster == curr_cluster; j++) - next_cluster = info[j].cluster; - } - else - { - for (int j = i - 1; i != 0 && j >= 0 && next_cluster == curr_cluster; - j--) - next_cluster = info[j].cluster; - } - - if (next_cluster == curr_cluster) - next_cluster = run->pos + run->len; - - if (*index < next_cluster && *index >= curr_cluster) - { - if (run->direction == HB_DIRECTION_RTL) - *x -= position[i].x_advance; - *index = curr_cluster; - goto found; - } - } - } - -found: - if (rq->text_utf8) - *index = _raqm_u32_to_u8_index (rq, *index); - else if (rq->text_utf16) - *index = _raqm_u32_to_u16_index (rq, *index); - RAQM_TEST ("The position is %d at index %zu\n",*x ,*index); - return true; -} - -/** - * raqm_position_to_index: - * @rq: a #raqm_t. - * @x: x position. - * @y: y position. - * @index: (out): output character index. - * - * Returns the @index of the character at @x and @y position within text. - * If the position is outside the text, the last character is chosen as - * @index. - * - * Return value: - * `true` if the process was successful, `false` in case of error. - * - * Since: 0.2 - */ -bool -raqm_position_to_index (raqm_t *rq, - int x, - int y, - size_t *index) -{ - int delta_x = 0, current_x = 0; - (void)y; - - if (rq == NULL) - return false; - - if (x < 0) /* Get leftmost index */ - { - if (rq->resolved_dir == RAQM_DIRECTION_RTL) - *index = rq->text_len; - else - *index = 0; - return true; - } - - RAQM_TEST ("\n"); - - for (raqm_run_t *run = rq->runs; run != NULL; run = run->next) - { - size_t len; - hb_glyph_info_t *info; - hb_glyph_position_t *position; - len = hb_buffer_get_length (run->buffer); - info = hb_buffer_get_glyph_infos (run->buffer, NULL); - position = hb_buffer_get_glyph_positions (run->buffer, NULL); - - for (size_t i = 0; i < len; i++) - { - delta_x = position[i].x_advance; - if (x < (current_x + delta_x)) - { - bool before = false; - if (run->direction == HB_DIRECTION_LTR) - before = (x < current_x + (delta_x / 2)); - else - before = (x > current_x + (delta_x / 2)); - - if (before) - *index = info[i].cluster; - else - { - uint32_t curr_cluster = info[i].cluster; - uint32_t next_cluster = curr_cluster; - if (run->direction == HB_DIRECTION_LTR) - for (size_t j = i + 1; j < len && next_cluster == curr_cluster; j++) - next_cluster = info[j].cluster; - else - for (int j = i - 1; i != 0 && j >= 0 && next_cluster == curr_cluster; - j--) - next_cluster = info[j].cluster; - - if (next_cluster == curr_cluster) - next_cluster = run->pos + run->len; - - *index = next_cluster; - } - if (_raqm_allowed_grapheme_boundary (rq->text[*index],rq->text[*index + 1])) - { - RAQM_TEST ("The start-index is %zu at position %d \n", *index, x); - return true; - } - - while (*index < (unsigned)run->pos + run->len) - { - if (_raqm_allowed_grapheme_boundary (rq->text[*index], - rq->text[*index + 1])) - { - *index += 1; - break; - } - *index += 1; - } - RAQM_TEST ("The start-index is %zu at position %d \n", *index, x); - return true; - } - else - current_x += delta_x; - } - } - - /* Get rightmost index*/ - if (rq->resolved_dir == RAQM_DIRECTION_RTL) - *index = 0; - else - *index = rq->text_len; - - RAQM_TEST ("The start-index is %zu at position %d \n", *index, x); - - return true; -} - -typedef enum -{ - RAQM_GRAPHEM_CR, - RAQM_GRAPHEM_LF, - RAQM_GRAPHEM_CONTROL, - RAQM_GRAPHEM_EXTEND, - RAQM_GRAPHEM_REGIONAL_INDICATOR, - RAQM_GRAPHEM_PREPEND, - RAQM_GRAPHEM_SPACING_MARK, - RAQM_GRAPHEM_HANGUL_SYLLABLE, - RAQM_GRAPHEM_OTHER -} _raqm_grapheme_t; - -static _raqm_grapheme_t -_raqm_get_grapheme_break (hb_codepoint_t ch, - hb_unicode_general_category_t category); - -static bool -_raqm_allowed_grapheme_boundary (hb_codepoint_t l_char, - hb_codepoint_t r_char) -{ - hb_unicode_general_category_t l_category; - hb_unicode_general_category_t r_category; - _raqm_grapheme_t l_grapheme, r_grapheme; - hb_unicode_funcs_t* unicode_funcs = hb_unicode_funcs_get_default (); - - l_category = hb_unicode_general_category (unicode_funcs, l_char); - r_category = hb_unicode_general_category (unicode_funcs, r_char); - l_grapheme = _raqm_get_grapheme_break (l_char, l_category); - r_grapheme = _raqm_get_grapheme_break (r_char, r_category); - - if (l_grapheme == RAQM_GRAPHEM_CR && r_grapheme == RAQM_GRAPHEM_LF) - return false; /*Do not break between a CR and LF GB3*/ - if (l_grapheme == RAQM_GRAPHEM_CONTROL || l_grapheme == RAQM_GRAPHEM_CR || - l_grapheme == RAQM_GRAPHEM_LF || r_grapheme == RAQM_GRAPHEM_CONTROL || - r_grapheme == RAQM_GRAPHEM_CR || r_grapheme == RAQM_GRAPHEM_LF) - return true; /*Break before and after CONTROL GB4, GB5*/ - if (r_grapheme == RAQM_GRAPHEM_HANGUL_SYLLABLE) - return false; /*Do not break Hangul syllable sequences. GB6, GB7, GB8*/ - if (l_grapheme == RAQM_GRAPHEM_REGIONAL_INDICATOR && - r_grapheme == RAQM_GRAPHEM_REGIONAL_INDICATOR) - return false; /*Do not break between regional indicator symbols. GB8a*/ - if (r_grapheme == RAQM_GRAPHEM_EXTEND) - return false; /*Do not break before extending characters. GB9*/ - /*Do not break before SpacingMarks, or after Prepend characters.GB9a, GB9b*/ - if (l_grapheme == RAQM_GRAPHEM_PREPEND) - return false; - if (r_grapheme == RAQM_GRAPHEM_SPACING_MARK) - return false; - return true; /*Otherwise, break everywhere. GB1, GB2, GB10*/ -} - -static _raqm_grapheme_t -_raqm_get_grapheme_break (hb_codepoint_t ch, - hb_unicode_general_category_t category) -{ - _raqm_grapheme_t gb_type; - - gb_type = RAQM_GRAPHEM_OTHER; - switch ((int)category) - { - case HB_UNICODE_GENERAL_CATEGORY_FORMAT: - if (ch == 0x200C || ch == 0x200D) - gb_type = RAQM_GRAPHEM_EXTEND; - else - gb_type = RAQM_GRAPHEM_CONTROL; - break; - - case HB_UNICODE_GENERAL_CATEGORY_CONTROL: - if (ch == 0x000D) - gb_type = RAQM_GRAPHEM_CR; - else if (ch == 0x000A) - gb_type = RAQM_GRAPHEM_LF; - else - gb_type = RAQM_GRAPHEM_CONTROL; - break; - - case HB_UNICODE_GENERAL_CATEGORY_SURROGATE: - case HB_UNICODE_GENERAL_CATEGORY_LINE_SEPARATOR: - case HB_UNICODE_GENERAL_CATEGORY_PARAGRAPH_SEPARATOR: - case HB_UNICODE_GENERAL_CATEGORY_UNASSIGNED: - if ((ch >= 0xFFF0 && ch <= 0xFFF8) || - (ch >= 0xE0000 && ch <= 0xE0FFF)) - gb_type = RAQM_GRAPHEM_CONTROL; - break; - - case HB_UNICODE_GENERAL_CATEGORY_NON_SPACING_MARK: - case HB_UNICODE_GENERAL_CATEGORY_ENCLOSING_MARK: - case HB_UNICODE_GENERAL_CATEGORY_SPACING_MARK: - if (ch != 0x102B && ch != 0x102C && ch != 0x1038 && - (ch < 0x1062 || ch > 0x1064) && (ch < 0x1067 || ch > 0x106D) && - ch != 0x1083 && (ch < 0x1087 || ch > 0x108C) && ch != 0x108F && - (ch < 0x109A || ch > 0x109C) && ch != 0x1A61 && ch != 0x1A63 && - ch != 0x1A64 && ch != 0xAA7B && ch != 0xAA70 && ch != 0x11720 && - ch != 0x11721) /**/ - gb_type = RAQM_GRAPHEM_SPACING_MARK; - - else if (ch == 0x09BE || ch == 0x09D7 || - ch == 0x0B3E || ch == 0x0B57 || ch == 0x0BBE || ch == 0x0BD7 || - ch == 0x0CC2 || ch == 0x0CD5 || ch == 0x0CD6 || - ch == 0x0D3E || ch == 0x0D57 || ch == 0x0DCF || ch == 0x0DDF || - ch == 0x1D165 || (ch >= 0x1D16E && ch <= 0x1D172)) - gb_type = RAQM_GRAPHEM_EXTEND; - break; - - case HB_UNICODE_GENERAL_CATEGORY_OTHER_LETTER: - if (ch == 0x0E33 || ch == 0x0EB3) - gb_type = RAQM_GRAPHEM_SPACING_MARK; - break; - - case HB_UNICODE_GENERAL_CATEGORY_OTHER_SYMBOL: - if (ch >= 0x1F1E6 && ch <= 0x1F1FF) - gb_type = RAQM_GRAPHEM_REGIONAL_INDICATOR; - break; - - default: - gb_type = RAQM_GRAPHEM_OTHER; - break; - } - - if (_raqm_in_hangul_syllable (ch)) - gb_type = RAQM_GRAPHEM_HANGUL_SYLLABLE; - - return gb_type; -} - -static bool -_raqm_in_hangul_syllable (hb_codepoint_t ch) -{ - (void)ch; - return false; -} - -/** - * raqm_version: - * @major: (out): Library major version component. - * @minor: (out): Library minor version component. - * @micro: (out): Library micro version component. - * - * Returns library version as three integer components. - * - * Since: 0.7 - **/ -void -raqm_version (unsigned int *major, - unsigned int *minor, - unsigned int *micro) -{ - *major = RAQM_VERSION_MAJOR; - *minor = RAQM_VERSION_MINOR; - *micro = RAQM_VERSION_MICRO; -} - -/** - * raqm_version_string: - * - * Returns library version as a string with three components. - * - * Return value: library version string. - * - * Since: 0.7 - **/ -const char * -raqm_version_string (void) -{ - return RAQM_VERSION_STRING; -} - -/** - * raqm_version_atleast: - * @major: Library major version component. - * @minor: Library minor version component. - * @micro: Library micro version component. - * - * Checks if library version is less than or equal the specified version. - * - * Return value: - * `true` if library version is less than or equal the specified version, - * `false` otherwise. - * - * Since: 0.7 - **/ -bool -raqm_version_atleast (unsigned int major, - unsigned int minor, - unsigned int micro) -{ - return RAQM_VERSION_ATLEAST (major, minor, micro); -} - -/** - * RAQM_VERSION_ATLEAST: - * @major: Library major version component. - * @minor: Library minor version component. - * @micro: Library micro version component. - * - * Checks if library version is less than or equal the specified version. - * - * Return value: - * `true` if library version is less than or equal the specified version, - * `false` otherwise. - * - * Since: 0.7 - **/ - -/** - * RAQM_VERSION_STRING: - * - * Library version as a string with three components. - * - * Since: 0.7 - **/ - -/** - * RAQM_VERSION_MAJOR: - * - * Library major version component. - * - * Since: 0.7 - **/ - -/** - * RAQM_VERSION_MINOR: - * - * Library minor version component. - * - * Since: 0.7 - **/ - -/** - * RAQM_VERSION_MICRO: - * - * Library micro version component. - * - * Since: 0.7 - **/ +/* + * Copyright © 2015 Information Technology Authority (ITA) + * Copyright © 2016-2023 Khaled Hosny + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + * + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include + +#ifdef RAQM_SHEENBIDI +#include +#else +#ifdef HAVE_FRIBIDI_SYSTEM +#include +#else +#include "../fribidi-shim/fribidi.h" +#endif +#endif + +#include +#include + +#include "raqm.h" + +/** + * SECTION:raqm + * @title: Raqm + * @short_description: A library for complex text layout + * @include: raqm.h + * + * Raqm is a light weight text layout library with strong emphasis on + * supporting languages and writing systems that require complex text layout. + * + * The main object in Raqm API is #raqm_t, it stores all the states of the + * input text, its properties, and the output of the layout process. + * + * To start, you create a #raqm_t object, add text and font(s) to it, run the + * layout process, and finally query about the output. For example: + * + * |[ + * #include "raqm.h" + * + * int + * main (int argc, char *argv[]) + * { + * const char *fontfile; + * const char *text; + * const char *direction; + * const char *language; + * int ret = 1; + * + * FT_Library library = NULL; + * FT_Face face = NULL; + * + * if (argc < 5) + * { + * printf ("Usage: %s FONT_FILE TEXT DIRECTION LANG\n", argv[0]); + * return 1; + * } + * + * fontfile = argv[1]; + * text = argv[2]; + * direction = argv[3]; + * language = argv[4]; + * + * if (FT_Init_FreeType (&library) == 0) + * { + * if (FT_New_Face (library, fontfile, 0, &face) == 0) + * { + * if (FT_Set_Char_Size (face, face->units_per_EM, 0, 0, 0) == 0) + * { + * raqm_t *rq = raqm_create (); + * if (rq != NULL) + * { + * raqm_direction_t dir = RAQM_DIRECTION_DEFAULT; + * + * if (strcmp (direction, "r") == 0) + * dir = RAQM_DIRECTION_RTL; + * else if (strcmp (direction, "l") == 0) + * dir = RAQM_DIRECTION_LTR; + * + * if (raqm_set_text_utf8 (rq, text, strlen (text)) && + * raqm_set_freetype_face (rq, face) && + * raqm_set_par_direction (rq, dir) && + * raqm_set_language (rq, language, 0, strlen (text)) && + * raqm_layout (rq)) + * { + * size_t count, i; + * raqm_glyph_t *glyphs = raqm_get_glyphs (rq, &count); + * + * ret = !(glyphs != NULL || count == 0); + * + * printf("glyph count: %zu\n", count); + * for (i = 0; i < count; i++) + * { + * printf ("gid#%d off: (%d, %d) adv: (%d, %d) idx: %d\n", + * glyphs[i].index, + * glyphs[i].x_offset, + * glyphs[i].y_offset, + * glyphs[i].x_advance, + * glyphs[i].y_advance, + * glyphs[i].cluster); + * } + * } + * + * raqm_destroy (rq); + * } + * } + * + * FT_Done_Face (face); + * } + * + * FT_Done_FreeType (library); + * } + * + * return ret; + * } + * ]| + * To compile this example: + * |[ + * cc -o test test.c `pkg-config --libs --cflags raqm` + * ]| + */ + +/* For enabling debug mode */ +/*#define RAQM_DEBUG 1*/ +#ifdef RAQM_DEBUG +#define RAQM_DBG(...) fprintf (stderr, __VA_ARGS__) +#else +#define RAQM_DBG(...) +#endif + +#ifdef RAQM_TESTING +# define RAQM_TEST(...) printf (__VA_ARGS__) +# define SCRIPT_TO_STRING(script) \ + char buff[5]; \ + hb_tag_to_string (hb_script_to_iso15924_tag (script), buff); \ + buff[4] = '\0'; +#else +# define RAQM_TEST(...) +#endif + +#define RAQM_BIDI_LEVEL_IS_RTL(level) \ + ((level) & 1) + +#ifdef RAQM_SHEENBIDI + typedef SBLevel _raqm_bidi_level_t; +#else + typedef FriBidiLevel _raqm_bidi_level_t; +#endif + +typedef struct +{ + FT_Face ftface; + int ftloadflags; + hb_language_t lang; + hb_script_t script; + int spacing_after; +} _raqm_text_info; + +typedef struct _raqm_run raqm_run_t; + +struct _raqm +{ + int ref_count; + + uint32_t *text; + uint16_t *text_utf16; + char *text_utf8; + size_t text_len; + size_t text_capacity_bytes; + + _raqm_text_info *text_info; + + raqm_direction_t base_dir; + raqm_direction_t resolved_dir; + + hb_feature_t *features; + size_t features_len; + + raqm_run_t *runs; + raqm_run_t *runs_pool; + + raqm_glyph_t *glyphs; + size_t glyphs_capacity; + + int invisible_glyph; +}; + +struct _raqm_run +{ + uint32_t pos; + uint32_t len; + + hb_direction_t direction; + hb_script_t script; + hb_font_t *font; + hb_buffer_t *buffer; + + raqm_run_t *next; +}; + +static size_t +_raqm_encoding_to_u32_index (raqm_t *rq, + size_t index); + +static bool +_raqm_allowed_grapheme_boundary (hb_codepoint_t l_char, + hb_codepoint_t r_char); + +static void +_raqm_init_text_info (raqm_t *rq) +{ + hb_language_t default_lang = hb_language_get_default (); + for (size_t i = 0; i < rq->text_len; i++) + { + rq->text_info[i].ftface = NULL; + rq->text_info[i].ftloadflags = -1; + rq->text_info[i].lang = default_lang; + rq->text_info[i].script = HB_SCRIPT_INVALID; + rq->text_info[i].spacing_after = 0; + } +} + +static void +_raqm_release_text_info (raqm_t *rq) +{ + if (!rq->text_info) + return; + + for (size_t i = 0; i < rq->text_len; i++) + { + if (rq->text_info[i].ftface) + FT_Done_Face (rq->text_info[i].ftface); + } +} + +static bool +_raqm_compare_text_info (_raqm_text_info a, + _raqm_text_info b) +{ + if (a.ftface != b.ftface) + return false; + + if (a.ftloadflags != b.ftloadflags) + return false; + + if (a.lang != b.lang) + return false; + + if (a.script != b.script) + return false; + + /* Spacing shouldn't break runs, so we don't compare them here. */ + + return true; +} + +static void +_raqm_free_text(raqm_t* rq) +{ + free (rq->text); + rq->text = NULL; + rq->text_info = NULL; + rq->text_utf8 = NULL; + rq->text_utf16 = NULL; + rq->text_len = 0; + rq->text_capacity_bytes = 0; +} + +static bool +_raqm_alloc_text(raqm_t *rq, + size_t len, + bool need_utf8, + bool need_utf16) +{ + /* Allocate contiguous memory block for texts and text_info */ + size_t mem_size = (sizeof (uint32_t) + sizeof (_raqm_text_info)) * len; + if (need_utf8) + mem_size += sizeof (char) * len; + else if (need_utf16) + mem_size += sizeof (uint16_t) * len; + + if (mem_size > rq->text_capacity_bytes) + { + void* new_mem = realloc (rq->text, mem_size); + if (!new_mem) + { + _raqm_free_text (rq); + return false; + } + + rq->text_capacity_bytes = mem_size; + rq->text = new_mem; + } + + rq->text_info = (_raqm_text_info*)(rq->text + len); + rq->text_utf8 = need_utf8 ? (char*)(rq->text_info + len) : NULL; + rq->text_utf16 = need_utf16 ? (uint16_t*)(rq->text_info + len) : NULL; + + return true; +} + +static raqm_run_t* +_raqm_alloc_run (raqm_t *rq) +{ + raqm_run_t *run = rq->runs_pool; + if (run) + { + rq->runs_pool = run->next; + } + else + { + run = malloc (sizeof (raqm_run_t)); + run->font = NULL; + run->buffer = NULL; + } + + run->pos = 0; + run->len = 0; + run->direction = HB_DIRECTION_INVALID; + run->script = HB_SCRIPT_INVALID; + run->next = NULL; + + return run; +} + +static void +_raqm_free_runs (raqm_run_t *runs) +{ + while (runs) + { + raqm_run_t *run = runs; + runs = runs->next; + + if (run->buffer) + hb_buffer_destroy (run->buffer); + + if (run->font) + hb_font_destroy (run->font); + + free (run); + } +} + +/** + * raqm_create: + * + * Creates a new #raqm_t with all its internal states initialized to their + * defaults. + * + * Return value: + * A newly allocated #raqm_t with a reference count of 1. The initial reference + * count should be released with raqm_destroy() when you are done using the + * #raqm_t. Returns `NULL` in case of error. + * + * Since: 0.1 + */ +raqm_t * +raqm_create (void) +{ + raqm_t *rq; + + rq = malloc (sizeof (raqm_t)); + if (!rq) + return NULL; + + rq->ref_count = 1; + + rq->base_dir = RAQM_DIRECTION_DEFAULT; + rq->resolved_dir = RAQM_DIRECTION_DEFAULT; + + rq->features = NULL; + rq->features_len = 0; + + rq->invisible_glyph = 0; + + rq->text = NULL; + rq->text_utf16 = NULL; + rq->text_utf8 = NULL; + rq->text_info = NULL; + rq->text_capacity_bytes = 0; + rq->text_len = 0; + + rq->runs = NULL; + rq->runs_pool = NULL; + + rq->glyphs = NULL; + rq->glyphs_capacity = 0; + + return rq; +} + +/** + * raqm_reference: + * @rq: a #raqm_t. + * + * Increases the reference count on @rq by one. This prevents @rq from being + * destroyed until a matching call to raqm_destroy() is made. + * + * Return value: + * The referenced #raqm_t. + * + * Since: 0.1 + */ +raqm_t * +raqm_reference (raqm_t *rq) +{ + if (rq) + rq->ref_count++; + + return rq; +} + +/** + * raqm_destroy: + * @rq: a #raqm_t. + * + * Decreases the reference count on @rq by one. If the result is zero, then @rq + * and all associated resources are freed. + * See raqm_reference(). + * + * Since: 0.1 + */ +void +raqm_destroy (raqm_t *rq) +{ + if (!rq || --rq->ref_count != 0) + return; + + _raqm_release_text_info (rq); + _raqm_free_text (rq); + _raqm_free_runs (rq->runs); + _raqm_free_runs (rq->runs_pool); + free (rq->glyphs); + free (rq->features); + free (rq); +} + +/** + * raqm_clear_contents: + * @rq: a #raqm_t. + * + * Clears internal state of previously used raqm_t object, making it ready + * for reuse and keeping some of allocated memory to increase performance. + * + * Since: 0.9 + */ +void +raqm_clear_contents (raqm_t *rq) +{ + if (!rq) + return; + + _raqm_release_text_info (rq); + + /* Return allocated runs to the pool, keep hb buffers for reuse */ + raqm_run_t *run = rq->runs; + while (run) + { + if (run->buffer) + hb_buffer_reset (run->buffer); + + if (run->font) + { + hb_font_destroy (run->font); + run->font = NULL; + } + + if (!run->next) + { + run->next = rq->runs_pool; + rq->runs_pool = rq->runs; + rq->runs = NULL; + break; + } + + run = run->next; + } + + rq->text_len = 0; + rq->resolved_dir = RAQM_DIRECTION_DEFAULT; +} + +/** + * raqm_set_text: + * @rq: a #raqm_t. + * @text: a UTF-32 encoded text string. + * @len: the length of @text. + * + * Adds @text to @rq to be used for layout. It must be a valid UTF-32 text, any + * invalid character will be replaced with U+FFFD. The text should typically + * represent a full paragraph, since doing the layout of chunks of text + * separately can give improper output. + * + * Return value: + * `true` if no errors happened, `false` otherwise. + * + * Since: 0.1 + */ +bool +raqm_set_text (raqm_t *rq, + const uint32_t *text, + size_t len) +{ + if (!rq || !text) + return false; + + /* Call raqm_clear_contents to reuse this raqm_t */ + if (rq->text_len) + return false; + + /* Empty string, don’t fail but do nothing */ + if (!len) + return true; + + if (!_raqm_alloc_text(rq, len, false, false)) + return false; + + rq->text_len = len; + memcpy (rq->text, text, sizeof (uint32_t) * len); + _raqm_init_text_info (rq); + + return true; +} + +static void * +_raqm_get_utf8_codepoint (const void *str, + uint32_t *out_codepoint) +{ + const char *s = (const char *)str; + + if (0xf0 == (0xf8 & s[0])) + { + *out_codepoint = ((0x07 & s[0]) << 18) | ((0x3f & s[1]) << 12) | ((0x3f & s[2]) << 6) | (0x3f & s[3]); + s += 4; + } + else if (0xe0 == (0xf0 & s[0])) + { + *out_codepoint = ((0x0f & s[0]) << 12) | ((0x3f & s[1]) << 6) | (0x3f & s[2]); + s += 3; + } + else if (0xc0 == (0xe0 & s[0])) + { + *out_codepoint = ((0x1f & s[0]) << 6) | (0x3f & s[1]); + s += 2; + } + else + { + *out_codepoint = s[0]; + s += 1; + } + + return (void *)s; +} + +static size_t +_raqm_u8_to_u32 (const char *text, size_t len, uint32_t *unicode) +{ + size_t in_len = 0; + uint32_t *out_utf32 = unicode; + const char *in_utf8 = text; + + while ((*in_utf8 != '\0') && (in_len < len)) + { + in_utf8 = _raqm_get_utf8_codepoint (in_utf8, out_utf32); + ++out_utf32; + ++in_len; + } + + return (out_utf32 - unicode); +} + +static void * +_raqm_get_utf16_codepoint (const void *str, + uint32_t *out_codepoint) +{ + const uint16_t *s = (const uint16_t *)str; + + if (s[0] > 0xD800 && s[0] < 0xDBFF) + { + if (s[1] > 0xDC00 && s[1] < 0xDFFF) + { + uint32_t X = ((s[0] & ((1 << 6) -1)) << 10) | (s[1] & ((1 << 10) -1)); + uint32_t W = (s[0] >> 6) & ((1 << 5) - 1); + *out_codepoint = (W+1) << 16 | X; + s += 2; + } + else + { + /* A single high surrogate, this is an error. */ + *out_codepoint = s[0]; + s += 1; + } + } + else + { + *out_codepoint = s[0]; + s += 1; + } + return (void *)s; +} + +static size_t +_raqm_u16_to_u32 (const uint16_t *text, size_t len, uint32_t *unicode) +{ + size_t in_len = 0; + uint32_t *out_utf32 = unicode; + const uint16_t *in_utf16 = text; + + while ((*in_utf16 != '\0') && (in_len < len)) + { + in_utf16 = _raqm_get_utf16_codepoint (in_utf16, out_utf32); + ++out_utf32; + ++in_len; + } + + return (out_utf32 - unicode); +} + +/** + * raqm_set_text_utf8: + * @rq: a #raqm_t. + * @text: a UTF-8 encoded text string. + * @len: the length of @text in UTF-8 bytes. + * + * Same as raqm_set_text(), but for text encoded in UTF-8 encoding. + * + * Return value: + * `true` if no errors happened, `false` otherwise. + * + * Since: 0.1 + */ +bool +raqm_set_text_utf8 (raqm_t *rq, + const char *text, + size_t len) +{ + if (!rq || !text) + return false; + + /* Call raqm_clear_contents to reuse this raqm_t */ + if (rq->text_len) + return false; + + /* Empty string, don’t fail but do nothing */ + if (!len) + return true; + + if (!_raqm_alloc_text(rq, len, true, false)) + return false; + + rq->text_len = _raqm_u8_to_u32 (text, len, rq->text); + memcpy (rq->text_utf8, text, sizeof (char) * len); + _raqm_init_text_info (rq); + + return true; +} + +/** + * raqm_set_text_utf16: + * @rq: a #raqm_t. + * @text: a UTF-16 encoded text string. + * @len: the length of @text in UTF-16 shorts. + * + * Same as raqm_set_text(), but for text encoded in UTF-16 encoding. + * + * Return value: + * `true` if no errors happened, `false` otherwise. + * + * Since: 0.10 + */ +bool +raqm_set_text_utf16 (raqm_t *rq, + const uint16_t *text, + size_t len) +{ + if (!rq || !text) + return false; + + /* Call raqm_clear_contents to reuse this raqm_t */ + if (rq->text_len) + return false; + + /* Empty string, don’t fail but do nothing */ + if (!len) + return true; + + if (!_raqm_alloc_text(rq, len, false, true)) + return false; + + rq->text_len = _raqm_u16_to_u32 (text, len, rq->text); + memcpy (rq->text_utf16, text, sizeof (uint16_t) * len); + _raqm_init_text_info (rq); + + return true; +} +/** + * raqm_set_par_direction: + * @rq: a #raqm_t. + * @dir: the direction of the paragraph. + * + * Sets the paragraph direction, also known as block direction in CSS. For + * horizontal text, this controls the overall direction in the Unicode + * Bidirectional Algorithm, so when the text is mainly right-to-left (with or + * without some left-to-right) text, then the base direction should be set to + * #RAQM_DIRECTION_RTL and vice versa. + * + * The default is #RAQM_DIRECTION_DEFAULT, which determines the paragraph + * direction based on the first character with strong bidi type (see [rule + * P2](https://unicode.org/reports/tr9/#P2) in Unicode Bidirectional Algorithm), + * which can be good enough for many cases but has problems when a mainly + * right-to-left paragraph starts with a left-to-right character and vice versa + * as the detected paragraph direction will be the wrong one, or when text does + * not contain any characters with string bidi types (e.g. only punctuation or + * numbers) as this will default to left-to-right paragraph direction. + * + * For vertical, top-to-bottom text, #RAQM_DIRECTION_TTB should be used. Raqm, + * however, provides limited vertical text support and does not handle rotated + * horizontal text in vertical text, instead everything is treated as vertical + * text. + * + * Return value: + * `true` if no errors happened, `false` otherwise. + * + * Since: 0.1 + */ +bool +raqm_set_par_direction (raqm_t *rq, + raqm_direction_t dir) +{ + if (!rq) + return false; + + rq->base_dir = dir; + + return true; +} + +/** + * raqm_set_language: + * @rq: a #raqm_t. + * @lang: a BCP47 language code. + * @start: index of first character that should use @face. + * @len: number of characters using @face. + * + * Sets a [BCP47 language + * code](https://www.w3.org/International/articles/language-tags/) to be used + * for @len-number of characters staring at @start. The @start and @len are + * input string array indices (i.e. counting bytes in UTF-8 and scaler values + * in UTF-32). + * + * This method can be used repeatedly to set different languages for different + * parts of the text. + * + * Return value: + * `true` if no errors happened, `false` otherwise. + * + * Stability: + * Unstable + * + * Since: 0.2 + */ +bool +raqm_set_language (raqm_t *rq, + const char *lang, + size_t start, + size_t len) +{ + hb_language_t language; + size_t end; + + if (!rq) + return false; + + if (!rq->text_len) + return true; + + end = _raqm_encoding_to_u32_index (rq, start + len); + start = _raqm_encoding_to_u32_index (rq, start); + + if (start >= rq->text_len || end > rq->text_len) + return false; + + if (!rq->text_info) + return false; + + language = hb_language_from_string (lang, -1); + for (size_t i = start; i < end; i++) + { + rq->text_info[i].lang = language; + } + + return true; +} + +static bool +_raqm_add_font_feature (raqm_t *rq, + hb_feature_t fea) +{ + void* new_features; + + if (!rq) + return false; + + new_features = realloc (rq->features, + sizeof (hb_feature_t) * (rq->features_len + 1)); + if (!new_features) + return false; + + if (fea.start != HB_FEATURE_GLOBAL_START) + fea.start = _raqm_encoding_to_u32_index (rq, fea.start); + if (fea.end != HB_FEATURE_GLOBAL_END) + fea.end = _raqm_encoding_to_u32_index (rq, fea.end); + + rq->features = new_features; + rq->features[rq->features_len] = fea; + rq->features_len++; + + return true; +} + +/** + * raqm_add_font_feature: + * @rq: a #raqm_t. + * @feature: (transfer none): a font feature string. + * @len: length of @feature, -1 for `NULL`-terminated. + * + * Adds a font feature to be used by the #raqm_t during text layout. This is + * usually used to turn on optional font features that are not enabled by + * default, for example `dlig` or `ss01`, but can be also used to turn off + * default font features. + * + * @feature is string representing a single font feature, in the syntax + * understood by hb_feature_from_string(). + * + * This function can be called repeatedly, new features will be appended to the + * end of the features list and can potentially override previous features. + * + * Return value: + * `true` if parsing @feature succeeded, `false` otherwise. + * + * Since: 0.1 + */ +bool +raqm_add_font_feature (raqm_t *rq, + const char *feature, + int len) +{ + hb_bool_t ok; + hb_feature_t fea; + + if (!rq) + return false; + + ok = hb_feature_from_string (feature, len, &fea); + if (ok) + _raqm_add_font_feature (rq, fea); + + return ok; +} + +static hb_font_t * +_raqm_create_hb_font (raqm_t *rq, + FT_Face face, + int loadflags) +{ + hb_font_t *font = hb_ft_font_create_referenced (face); + + if (loadflags >= 0) + hb_ft_font_set_load_flags (font, loadflags); + + return font; +} + +static bool +_raqm_set_freetype_face (raqm_t *rq, + FT_Face face, + size_t start, + size_t end) +{ + if (!rq) + return false; + + if (!rq->text_len) + return true; + + if (start >= rq->text_len || end > rq->text_len) + return false; + + if (!rq->text_info) + return false; + + for (size_t i = start; i < end; i++) + { + if (rq->text_info[i].ftface) + FT_Done_Face (rq->text_info[i].ftface); + rq->text_info[i].ftface = face; + FT_Reference_Face (face); + } + + return true; +} + +/** + * raqm_set_freetype_face: + * @rq: a #raqm_t. + * @face: an #FT_Face. + * + * Sets an #FT_Face to be used for all characters in @rq. + * + * See also raqm_set_freetype_face_range(). + * + * Return value: + * `true` if no errors happened, `false` otherwise. + * + * Since: 0.1 + */ +bool +raqm_set_freetype_face (raqm_t *rq, + FT_Face face) +{ + return _raqm_set_freetype_face (rq, face, 0, rq->text_len); +} + +/** + * raqm_set_freetype_face_range: + * @rq: a #raqm_t. + * @face: an #FT_Face. + * @start: index of first character that should use @face from the input string. + * @len: number of elements using @face. + * + * Sets an #FT_Face to be used for @len-number of characters staring at @start. + * The @start and @len are input string array indices, counting elements + * according to the underlying encoding. @start must always be aligned to the + * start of an encoded codepoint, and @len must always end at a codepoint's + * final element. + * + * This method can be used repeatedly to set different faces for different + * parts of the text. It is the responsibility of the client to make sure that + * face ranges cover the whole text, and is properly aligned. + * + * See also raqm_set_freetype_face(). + * + * Return value: + * `true` if no errors happened, `false` otherwise. + * + * Since: 0.1 + */ +bool +raqm_set_freetype_face_range (raqm_t *rq, + FT_Face face, + size_t start, + size_t len) +{ + size_t end; + + if (!rq) + return false; + + if (!rq->text_len) + return true; + + end = _raqm_encoding_to_u32_index (rq, start + len); + start = _raqm_encoding_to_u32_index (rq, start); + + return _raqm_set_freetype_face (rq, face, start, end); +} + +static bool +_raqm_set_freetype_load_flags (raqm_t *rq, + int flags, + size_t start, + size_t end) +{ + if (!rq) + return false; + + if (!rq->text_len) + return true; + + if (start >= rq->text_len || end > rq->text_len) + return false; + + if (!rq->text_info) + return false; + + for (size_t i = start; i < end; i++) + rq->text_info[i].ftloadflags = flags; + + return true; +} + +/** + * raqm_set_freetype_load_flags: + * @rq: a #raqm_t. + * @flags: FreeType load flags. + * + * Sets the load flags passed to FreeType when loading glyphs, should be the + * same flags used by the client when rendering FreeType glyphs. + * + * This requires version of HarfBuzz that has hb_ft_font_set_load_flags(), for + * older version the flags will be ignored. + * + * Return value: + * `true` if no errors happened, `false` otherwise. + * + * Since: 0.3 + */ +bool +raqm_set_freetype_load_flags (raqm_t *rq, + int flags) +{ + return _raqm_set_freetype_load_flags(rq, flags, 0, rq->text_len); +} + +/** + * raqm_set_freetype_load_flags_range: + * @rq: a #raqm_t. + * @flags: FreeType load flags. + * @start: index of first character that should use @flags. + * @len: number of characters using @flags. + * + * Sets the load flags passed to FreeType when loading glyphs for @len-number + * of characters staring at @start. Flags should be the same as used by the + * client when rendering corresponding FreeType glyphs. The @start and @len + * are input string array indices (i.e. counting bytes in UTF-8 and scaler + * values in UTF-32). + * + * This method can be used repeatedly to set different flags for different + * parts of the text. It is the responsibility of the client to make sure that + * flag ranges cover the whole text. + * + * This requires version of HarfBuzz that has hb_ft_font_set_load_flags(), for + * older version the flags will be ignored. + * + * See also raqm_set_freetype_load_flags(). + * + * Return value: + * `true` if no errors happened, `false` otherwise. + * + * Since: 0.9 + */ +bool +raqm_set_freetype_load_flags_range (raqm_t *rq, + int flags, + size_t start, + size_t len) +{ + size_t end; + + if (!rq) + return false; + + if (!rq->text_len) + return true; + + end = _raqm_encoding_to_u32_index (rq, start + len); + start = _raqm_encoding_to_u32_index (rq, start); + + return _raqm_set_freetype_load_flags (rq, flags, start, end); +} + +static bool +_raqm_set_spacing (raqm_t *rq, + int spacing, + bool word_spacing, + size_t start, + size_t end) +{ + if (!rq) + return false; + + if (!rq->text_len) + return true; + + if (start >= rq->text_len || end > rq->text_len) + return false; + + if (!rq->text_info) + return false; + + for (size_t i = start; i < end; i++) + { + bool set_spacing = i == 0; + if (!set_spacing) + set_spacing = _raqm_allowed_grapheme_boundary (rq->text[i-1], rq->text[i]); + + if (set_spacing) + { + if (word_spacing) + { + if (_raqm_allowed_grapheme_boundary (rq->text[i], rq->text[i+1])) + { + /* CSS word seperators, word spacing is only applied on these.*/ + if (rq->text[i] == 0x0020 || /* Space */ + rq->text[i] == 0x00A0 || /* No Break Space */ + rq->text[i] == 0x1361 || /* Ethiopic Word Space */ + rq->text[i] == 0x10100 || /* Aegean Word Seperator Line */ + rq->text[i] == 0x10101 || /* Aegean Word Seperator Dot */ + rq->text[i] == 0x1039F || /* Ugaric Word Divider */ + rq->text[i] == 0x1091F) /* Phoenician Word Separator */ + { + rq->text_info[i].spacing_after = spacing; + } + } + } + else + { + rq->text_info[i].spacing_after = spacing; + } + } + } + + return true; +} + +/** + * raqm_set_letter_spacing_range: + * @rq: a #raqm_t. + * @spacing: amount of spacing in Freetype Font Units (26.6 format). + * @start: index of first character that should use @spacing. + * @len: number of characters using @spacing. + * + * Set the letter spacing or tracking for a given range, the value + * will be added onto the advance and offset for RTL, and the advance for + * other directions. Letter spacing will be applied between characters, so + * the last character will not have spacing applied after it. + * Note that not all scripts have a letter-spacing tradition, + * for example, Arabic does not, while Devanagari does. + * + * This will also add “disable `liga`, `clig`, `hlig`, `dlig`, and `calt`†font + * features to the internal features list, so call this function after setting + * the font features for best spacing results. + * + * Return value: + * `true` if no errors happened, `false` otherwise. + * + * Since: 0.10 + */ +bool +raqm_set_letter_spacing_range(raqm_t *rq, + int spacing, + size_t start, + size_t len) +{ + size_t end; + + if (!rq) + return false; + + if (!rq->text_len) + return true; + + end = start + len - 1; + + if (spacing != 0) + { +#define NUM_TAGS 5 + static char *tags[NUM_TAGS] = { "clig", "liga", "hlig", "dlig", "calt" }; + for (size_t i = 0; i < NUM_TAGS; i++) + { + hb_feature_t fea = { hb_tag_from_string(tags[i], 5), 0, start, end }; + _raqm_add_font_feature (rq, fea); + } +#undef NUM_TAGS + } + + start = _raqm_encoding_to_u32_index (rq, start); + end = _raqm_encoding_to_u32_index (rq, end); + + return _raqm_set_spacing (rq, spacing, false, start, end); +} + +/** + * raqm_set_word_spacing_range: + * @rq: a #raqm_t. + * @spacing: amount of spacing in Freetype Font Units (26.6 format). + * @start: index of first character that should use @spacing. + * @len: number of characters using @spacing. + * + * Set the word spacing for a given range. Word spacing will only be applied to + * 'word separator' characters, such as 'space', 'no break space' and + * Ethiopic word separator'. + * The value will be added onto the advance and offset for RTL, and the advance + * for other directions. + * + * Return value: + * `true` if no errors happened, `false` otherwise. + * + * Since: 0.10 + */ +bool +raqm_set_word_spacing_range(raqm_t *rq, + int spacing, + size_t start, + size_t len) +{ + size_t end; + + if (!rq) + return false; + + if (!rq->text_len) + return true; + + end = _raqm_encoding_to_u32_index (rq, start + len); + start = _raqm_encoding_to_u32_index (rq, start); + + return _raqm_set_spacing (rq, spacing, true, start, end); +} + +/** + * raqm_set_invisible_glyph: + * @rq: a #raqm_t. + * @gid: glyph id to use for invisible glyphs. + * + * Sets the glyph id to be used for invisible glyhphs. + * + * If @gid is negative, invisible glyphs will be suppressed from the output. + * + * If @gid is zero, invisible glyphs will be rendered as space. + * + * If @gid is a positive number, it will be used for invisible glyphs. + * + * Return value: + * `true` if no errors happened, `false` otherwise. + * + * Since: 0.6 + */ +bool +raqm_set_invisible_glyph (raqm_t *rq, + int gid) +{ + if (!rq) + return false; + + rq->invisible_glyph = gid; + return true; +} + +static bool +_raqm_itemize (raqm_t *rq); + +static bool +_raqm_shape (raqm_t *rq); + +/** + * raqm_layout: + * @rq: a #raqm_t. + * + * Run the text layout process on @rq. This is the main Raqm function where the + * Unicode Bidirectional Text algorithm will be applied to the text in @rq, + * text shaping, and any other part of the layout process. + * + * Return value: + * `true` if the layout process was successful, `false` otherwise. + * + * Since: 0.1 + */ +bool +raqm_layout (raqm_t *rq) +{ + if (!rq) + return false; + + if (!rq->text_len) + return true; + + if (!rq->text_info) + return false; + + for (size_t i = 0; i < rq->text_len; i++) + { + if (!rq->text_info[i].ftface) + return false; + } + + if (!_raqm_itemize (rq)) + return false; + + if (!_raqm_shape (rq)) + return false; + + return true; +} + +static uint32_t +_raqm_u32_to_u8_index (raqm_t *rq, + uint32_t index); +static uint32_t +_raqm_u32_to_u16_index (raqm_t *rq, + uint32_t index); +/** + * raqm_get_glyphs: + * @rq: a #raqm_t. + * @length: (out): output array length. + * + * Gets the final result of Raqm layout process, an array of #raqm_glyph_t + * containing the glyph indices in the font, their positions and other possible + * information. + * + * Return value: (transfer none): + * An array of #raqm_glyph_t, or `NULL` in case of error. This is owned by @rq + * and must not be freed. + * + * Since: 0.1 + */ +raqm_glyph_t * +raqm_get_glyphs (raqm_t *rq, + size_t *length) +{ + size_t count = 0; + + if (!rq || !rq->runs || !length) + { + if (length) + *length = 0; + return NULL; + } + + for (raqm_run_t *run = rq->runs; run != NULL; run = run->next) + count += hb_buffer_get_length (run->buffer); + + if (count > rq->glyphs_capacity) + { + void* new_mem = realloc (rq->glyphs, sizeof (raqm_glyph_t) * count); + if (!new_mem) + { + *length = 0; + return NULL; + } + + rq->glyphs = new_mem; + rq->glyphs_capacity = count; + } + + *length = count; + + RAQM_TEST ("Glyph information:\n"); + + count = 0; + for (raqm_run_t *run = rq->runs; run != NULL; run = run->next) + { + size_t len; + hb_glyph_info_t *info; + hb_glyph_position_t *position; + + len = hb_buffer_get_length (run->buffer); + info = hb_buffer_get_glyph_infos (run->buffer, NULL); + position = hb_buffer_get_glyph_positions (run->buffer, NULL); + + for (size_t i = 0; i < len; i++) + { + rq->glyphs[count + i].index = info[i].codepoint; + rq->glyphs[count + i].cluster = info[i].cluster; + rq->glyphs[count + i].x_advance = position[i].x_advance; + rq->glyphs[count + i].y_advance = position[i].y_advance; + rq->glyphs[count + i].x_offset = position[i].x_offset; + rq->glyphs[count + i].y_offset = position[i].y_offset; + rq->glyphs[count + i].ftface = rq->text_info[info[i].cluster].ftface; + + RAQM_TEST ("glyph [%d]\tx_offset: %d\ty_offset: %d\tx_advance: %d\tfont: %s\n", + rq->glyphs[count + i].index, rq->glyphs[count + i].x_offset, + rq->glyphs[count + i].y_offset, rq->glyphs[count + i].x_advance, + rq->glyphs[count + i].ftface->family_name); + } + + count += len; + } + + if (rq->text_utf8) + { +#ifdef RAQM_TESTING + RAQM_TEST ("\nUTF-32 clusters:"); + for (size_t i = 0; i < count; i++) + RAQM_TEST (" %02d", rq->glyphs[i].cluster); + RAQM_TEST ("\n"); +#endif + + for (size_t i = 0; i < count; i++) + rq->glyphs[i].cluster = _raqm_u32_to_u8_index (rq, + rq->glyphs[i].cluster); + +#ifdef RAQM_TESTING + RAQM_TEST ("UTF-8 clusters: "); + for (size_t i = 0; i < count; i++) + RAQM_TEST (" %02d", rq->glyphs[i].cluster); + RAQM_TEST ("\n"); +#endif + } + else if (rq->text_utf16) + { + for (size_t i = 0; i < count; i++) + rq->glyphs[i].cluster = _raqm_u32_to_u16_index (rq, + rq->glyphs[i].cluster); + } + return rq->glyphs; +} + +/** + * raqm_get_par_resolved_direction: + * @rq: a #raqm_t. + * + * Gets the resolved direction of the paragraph; + * + * Return value: + * The #raqm_direction_t specifying the resolved direction of text, + * or #RAQM_DIRECTION_DEFAULT if raqm_layout() has not been called on @rq. + * + * Since: 0.8 + */ +raqm_direction_t +raqm_get_par_resolved_direction (raqm_t *rq) +{ + if (!rq) + return RAQM_DIRECTION_DEFAULT; + + return rq->resolved_dir; +} + +/** + * raqm_get_direction_at_index: + * @rq: a #raqm_t. + * @index: (in): character index. + * + * Gets the resolved direction of the character at specified index; + * + * Return value: + * The #raqm_direction_t specifying the resolved direction of text at the + * specified index, or #RAQM_DIRECTION_DEFAULT if raqm_layout() has not been + * called on @rq. + * + * Since: 0.8 + */ +raqm_direction_t +raqm_get_direction_at_index (raqm_t *rq, + size_t index) +{ + if (!rq) + return RAQM_DIRECTION_DEFAULT; + + for (raqm_run_t *run = rq->runs; run != NULL; run = run->next) + { + if (run->pos <= index && index < run->pos + run->len) + { + switch (run->direction) + { + case HB_DIRECTION_LTR: + return RAQM_DIRECTION_LTR; + case HB_DIRECTION_RTL: + return RAQM_DIRECTION_RTL; + case HB_DIRECTION_TTB: + return RAQM_DIRECTION_TTB; + default: + return RAQM_DIRECTION_DEFAULT; + } + } + } + + return RAQM_DIRECTION_DEFAULT; +} + +static bool +_raqm_resolve_scripts (raqm_t *rq); + +static hb_direction_t +_raqm_hb_dir (raqm_t *rq, _raqm_bidi_level_t level) +{ + hb_direction_t dir = HB_DIRECTION_LTR; + + if (rq->base_dir == RAQM_DIRECTION_TTB) + dir = HB_DIRECTION_TTB; + else if (RAQM_BIDI_LEVEL_IS_RTL(level)) + dir = HB_DIRECTION_RTL; + + return dir; +} + +typedef struct +{ + size_t pos; + size_t len; + _raqm_bidi_level_t level; +} _raqm_bidi_run; + +#ifdef RAQM_SHEENBIDI +static _raqm_bidi_run * +_raqm_bidi_itemize (raqm_t *rq, size_t *run_count) +{ + _raqm_bidi_run *runs; + SBAlgorithmRef bidi; + SBParagraphRef par; + SBUInteger par_len; + SBLineRef line; + + SBLevel base_level = SBLevelDefaultLTR; + SBCodepointSequence input = { + SBStringEncodingUTF32, + (void *) rq->text, + rq->text_len + }; + + if (rq->base_dir == RAQM_DIRECTION_RTL) + base_level = 1; + else if (rq->base_dir == RAQM_DIRECTION_LTR) + base_level = 0; + + /* paragraph */ + bidi = SBAlgorithmCreate (&input); + par = SBAlgorithmCreateParagraph (bidi, 0, INT32_MAX, base_level); + par_len = SBParagraphGetLength (par); + + /* lines */ + line = SBParagraphCreateLine (par, 0, par_len); + *run_count = SBLineGetRunCount (line); + + if (SBParagraphGetBaseLevel (par) == 1) + rq->resolved_dir = RAQM_DIRECTION_RTL; + else + rq->resolved_dir = RAQM_DIRECTION_LTR; + + runs = malloc (sizeof (_raqm_bidi_run) * (*run_count)); + if (runs) + { + const SBRun *sheenbidi_runs = SBLineGetRunsPtr(line); + + for (size_t i = 0; i < (*run_count); ++i) + { + runs[i].pos = sheenbidi_runs[i].offset; + runs[i].len = sheenbidi_runs[i].length; + runs[i].level = sheenbidi_runs[i].level; + } + } + + SBLineRelease (line); + SBParagraphRelease (par); + SBAlgorithmRelease (bidi); + + return runs; +} +#else +static void +_raqm_reverse_run (_raqm_bidi_run *run, const size_t len) +{ + assert (run); + + for (size_t i = 0; i < len / 2; i++) + { + _raqm_bidi_run temp = run[i]; + run[i] = run[len - 1 - i]; + run[len - 1 - i] = temp; + } +} + +static _raqm_bidi_run * +_raqm_reorder_runs (const FriBidiCharType *types, + const size_t len, + const FriBidiParType base_dir, + /* input and output */ + FriBidiLevel *levels, + /* output */ + size_t *run_count) +{ + FriBidiLevel level; + FriBidiLevel last_level = -1; + FriBidiLevel max_level = 0; + size_t run_start = 0; + size_t run_index = 0; + _raqm_bidi_run *runs = NULL; + size_t count = 0; + + if (len == 0) + { + *run_count = 0; + return NULL; + } + + assert (types); + assert (levels); + + /* L1. Reset the embedding levels of some chars: + 4. any sequence of white space characters at the end of the line. */ + for (int i = len - 1; + i >= 0 && FRIBIDI_IS_EXPLICIT_OR_BN_OR_WS (types[i]); i--) + { + levels[i] = FRIBIDI_DIR_TO_LEVEL (base_dir); + } + + /* Find max_level of the line. We don't reuse the paragraph + * max_level, both for a cleaner API, and that the line max_level + * may be far less than paragraph max_level. */ + for (int i = len - 1; i >= 0; i--) + { + if (levels[i] > max_level) + max_level = levels[i]; + } + + for (size_t i = 0; i < len; i++) + { + if (levels[i] != last_level) + count++; + + last_level = levels[i]; + } + + runs = malloc (sizeof (_raqm_bidi_run) * count); + + while (run_start < len) + { + size_t run_end = run_start; + while (run_end < len && levels[run_start] == levels[run_end]) + { + run_end++; + } + + runs[run_index].pos = run_start; + runs[run_index].level = levels[run_start]; + runs[run_index].len = run_end - run_start; + run_start = run_end; + run_index++; + } + + /* L2. Reorder. */ + for (level = max_level; level > 0; level--) + { + for (int i = count - 1; i >= 0; i--) + { + if (runs[i].level >= level) + { + int end = i; + for (i--; (i >= 0 && runs[i].level >= level); i--) + ; + _raqm_reverse_run (runs + i + 1, end - i); + } + } + } + + *run_count = count; + return runs; +} + +static _raqm_bidi_run * +_raqm_bidi_itemize (raqm_t *rq, size_t *run_count) +{ + FriBidiParType par_type = FRIBIDI_PAR_ON; + _raqm_bidi_run *runs = NULL; + + FriBidiCharType *types; + _raqm_bidi_level_t *levels; + int max_level = 0; + FriBidiBracketType *btypes; + + types = calloc (rq->text_len, sizeof (FriBidiCharType)); + btypes = calloc (rq->text_len, sizeof (FriBidiBracketType)); + levels = calloc (rq->text_len, sizeof (_raqm_bidi_level_t)); + + if (!types || !levels || !btypes) + goto done; + + if (rq->base_dir == RAQM_DIRECTION_RTL) + par_type = FRIBIDI_PAR_RTL; + else if (rq->base_dir == RAQM_DIRECTION_LTR) + par_type = FRIBIDI_PAR_LTR; + + fribidi_get_bidi_types (rq->text, rq->text_len, types); + fribidi_get_bracket_types (rq->text, rq->text_len, types, btypes); + max_level = fribidi_get_par_embedding_levels_ex (types, btypes, + rq->text_len, &par_type, + levels); + + if (par_type == FRIBIDI_PAR_RTL) + rq->resolved_dir = RAQM_DIRECTION_RTL; + else + rq->resolved_dir = RAQM_DIRECTION_LTR; + + if (max_level == 0) + goto done; + + /* Get the number of bidi runs */ + runs = _raqm_reorder_runs (types, rq->text_len, par_type, levels, run_count); + +done: + free (types); + free (levels); + free (btypes); + + return runs; +} +#endif + +static bool +_raqm_itemize (raqm_t *rq) +{ + _raqm_bidi_run *runs = NULL; + raqm_run_t *last; + size_t run_count = 0; + bool ok = true; + +#ifdef RAQM_TESTING + static char *dir_names[] = { + "DEFAULT", + "RTL", + "LTR", + "TTB" + }; + + assert (rq->base_dir < sizeof (dir_names)); + RAQM_TEST ("Direction is: %s\n\n", dir_names[rq->base_dir]); +#endif + + if (!_raqm_resolve_scripts (rq)) + { + ok = false; + goto done; + } + + if (rq->base_dir == RAQM_DIRECTION_TTB) + { + /* Treat every thing as LTR in vertical text */ + run_count = 1; + rq->resolved_dir = RAQM_DIRECTION_TTB; + runs = malloc (sizeof (_raqm_bidi_run)); + if (runs) + { + runs->pos = 0; + runs->len = rq->text_len; + runs->level = 0; + } + } + else + runs = _raqm_bidi_itemize (rq, &run_count); + + if (!runs) + { + ok = false; + goto done; + } + +#ifdef RAQM_TESTING + assert (rq->resolved_dir < sizeof (dir_names)); + if (rq->base_dir == RAQM_DIRECTION_DEFAULT) + RAQM_TEST ("Resolved direction is: %s\n\n", dir_names[rq->resolved_dir]); + RAQM_TEST ("Number of runs before script itemization: %zu\n\n", run_count); + + RAQM_TEST ("BiDi Runs:\n"); + for (size_t i = 0; i < run_count; i++) + { + RAQM_TEST ("run[%zu]:\t start: %zu\tlength: %zu\tlevel: %d\n", + i, runs[i].pos, runs[i].len, runs[i].level); + } + RAQM_TEST ("\n"); +#endif + + last = NULL; + for (size_t i = 0; i < run_count; i++) + { + raqm_run_t *run = _raqm_alloc_run (rq); + if (!run) + { + ok = false; + goto done; + } + + if (!rq->runs) + rq->runs = run; + + if (last) + last->next = run; + + run->direction = _raqm_hb_dir (rq, runs[i].level); + + if (HB_DIRECTION_IS_BACKWARD (run->direction)) + { + run->pos = runs[i].pos + runs[i].len - 1; + run->script = rq->text_info[run->pos].script; + run->font = _raqm_create_hb_font (rq, rq->text_info[run->pos].ftface, + rq->text_info[run->pos].ftloadflags); + for (int j = runs[i].len - 1; j >= 0; j--) + { + _raqm_text_info info = rq->text_info[runs[i].pos + j]; + if (!_raqm_compare_text_info (rq->text_info[run->pos], info)) + { + raqm_run_t *newrun = _raqm_alloc_run (rq); + if (!newrun) + { + ok = false; + goto done; + } + newrun->pos = runs[i].pos + j; + newrun->len = 1; + newrun->direction = _raqm_hb_dir (rq, runs[i].level); + newrun->script = info.script; + newrun->font = _raqm_create_hb_font (rq, info.ftface, + info.ftloadflags); + run->next = newrun; + run = newrun; + } + else + { + run->len++; + run->pos = runs[i].pos + j; + } + } + } + else + { + run->pos = runs[i].pos; + run->script = rq->text_info[run->pos].script; + run->font = _raqm_create_hb_font (rq, rq->text_info[run->pos].ftface, + rq->text_info[run->pos].ftloadflags); + for (size_t j = 0; j < runs[i].len; j++) + { + _raqm_text_info info = rq->text_info[runs[i].pos + j]; + if (!_raqm_compare_text_info (rq->text_info[run->pos], info)) + { + raqm_run_t *newrun = _raqm_alloc_run (rq); + if (!newrun) + { + ok = false; + goto done; + } + newrun->pos = runs[i].pos + j; + newrun->len = 1; + newrun->direction = _raqm_hb_dir (rq, runs[i].level); + newrun->script = info.script; + newrun->font = _raqm_create_hb_font (rq, info.ftface, + info.ftloadflags); + run->next = newrun; + run = newrun; + } + else + run->len++; + } + } + + last = run; + last->next = NULL; + } + +#ifdef RAQM_TESTING + run_count = 0; + for (raqm_run_t *run = rq->runs; run != NULL; run = run->next) + run_count++; + RAQM_TEST ("Number of runs after script itemization: %zu\n\n", run_count); + + run_count = 0; + RAQM_TEST ("Final Runs:\n"); + for (raqm_run_t *run = rq->runs; run != NULL; run = run->next) + { + SCRIPT_TO_STRING (run->script); + RAQM_TEST ("run[%zu]:\t start: %d\tlength: %d\tdirection: %s\tscript: %s\tfont: %s\n", + run_count++, run->pos, run->len, + hb_direction_to_string (run->direction), buff, + rq->text_info[run->pos].ftface->family_name); + } + RAQM_TEST ("\n"); +#endif + +done: + free (runs); + + return ok; +} + +/* Stack to handle script detection */ +typedef struct +{ + size_t capacity; + size_t size; + int *pair_index; + hb_script_t *script; +} _raqm_stack_t; + +/* Special paired characters for script detection */ +static size_t paired_len = 34; +static const uint32_t paired_chars[] = +{ + 0x0028, 0x0029, /* ascii paired punctuation */ + 0x003c, 0x003e, + 0x005b, 0x005d, + 0x007b, 0x007d, + 0x00ab, 0x00bb, /* guillemets */ + 0x2018, 0x2019, /* general punctuation */ + 0x201c, 0x201d, + 0x2039, 0x203a, + 0x3008, 0x3009, /* chinese paired punctuation */ + 0x300a, 0x300b, + 0x300c, 0x300d, + 0x300e, 0x300f, + 0x3010, 0x3011, + 0x3014, 0x3015, + 0x3016, 0x3017, + 0x3018, 0x3019, + 0x301a, 0x301b +}; + +static void +_raqm_stack_free (_raqm_stack_t *stack) +{ + free (stack->script); + free (stack->pair_index); + free (stack); +} + +/* Stack handling functions */ +static _raqm_stack_t * +_raqm_stack_new (size_t max) +{ + _raqm_stack_t *stack; + stack = calloc (1, sizeof (_raqm_stack_t)); + if (!stack) + return NULL; + + stack->script = malloc (sizeof (hb_script_t) * max); + if (!stack->script) + { + _raqm_stack_free (stack); + return NULL; + } + + stack->pair_index = malloc (sizeof (int) * max); + if (!stack->pair_index) + { + _raqm_stack_free (stack); + return NULL; + } + + stack->size = 0; + stack->capacity = max; + + return stack; +} + +static bool +_raqm_stack_pop (_raqm_stack_t *stack) +{ + if (!stack->size) + { + RAQM_DBG ("Stack is Empty\n"); + return false; + } + + stack->size--; + + return true; +} + +static hb_script_t +_raqm_stack_top (_raqm_stack_t *stack) +{ + if (!stack->size) + { + RAQM_DBG ("Stack is Empty\n"); + return HB_SCRIPT_INVALID; /* XXX: check this */ + } + + return stack->script[stack->size]; +} + +static bool +_raqm_stack_push (_raqm_stack_t *stack, + hb_script_t script, + int pair_index) +{ + if (stack->size == stack->capacity) + { + RAQM_DBG ("Stack is Full\n"); + return false; + } + + stack->size++; + stack->script[stack->size] = script; + stack->pair_index[stack->size] = pair_index; + + return true; +} + +static int +_get_pair_index (const uint32_t ch) +{ + int lower = 0; + int upper = paired_len - 1; + + while (lower <= upper) + { + int mid = (lower + upper) / 2; + if (ch < paired_chars[mid]) + upper = mid - 1; + else if (ch > paired_chars[mid]) + lower = mid + 1; + else + return mid; + } + + return -1; +} + +#define STACK_IS_EMPTY(script) ((script)->size <= 0) +#define IS_OPEN(pair_index) (((pair_index) & 1) == 0) + +static hb_script_t +_raqm_unicode_script (hb_codepoint_t u) +{ + static hb_unicode_funcs_t* unicode_funcs; + + unicode_funcs = hb_unicode_funcs_get_default (); + + /* Make combining marks inherit the script of their bases, regardless of + * their own script. + */ + if (hb_unicode_general_category (unicode_funcs, u) == HB_UNICODE_GENERAL_CATEGORY_NON_SPACING_MARK) + return HB_SCRIPT_INHERITED; + + return hb_unicode_script (unicode_funcs, u); +} + +/* Resolve the script for each character in the input string, if the character + * script is common or inherited it takes the script of the character before it + * except paired characters which we try to make them use the same script. We + * then split the BiDi runs, if necessary, on script boundaries. + */ +static bool +_raqm_resolve_scripts (raqm_t *rq) +{ + int last_script_index = -1; + int last_set_index = -1; + hb_script_t last_script = HB_SCRIPT_INVALID; + _raqm_stack_t *stack = NULL; + + for (size_t i = 0; i < rq->text_len; ++i) + rq->text_info[i].script = _raqm_unicode_script (rq->text[i]); + +#ifdef RAQM_TESTING + RAQM_TEST ("Before script detection:\n"); + for (size_t i = 0; i < rq->text_len; ++i) + { + SCRIPT_TO_STRING (rq->text_info[i].script); + RAQM_TEST ("script for ch[%zu]\t%s\n", i, buff); + } + RAQM_TEST ("\n"); +#endif + + stack = _raqm_stack_new (rq->text_len); + if (!stack) + return false; + + for (int i = 0; i < (int) rq->text_len; i++) + { + if (rq->text_info[i].script == HB_SCRIPT_COMMON && last_script_index != -1) + { + int pair_index = _get_pair_index (rq->text[i]); + if (pair_index >= 0) + { + if (IS_OPEN (pair_index)) + { + /* is a paired character */ + rq->text_info[i].script = last_script; + last_set_index = i; + _raqm_stack_push (stack, rq->text_info[i].script, pair_index); + } + else + { + /* is a close paired character */ + /* find matching opening (by getting the last even index for current + * odd index) */ + while (!STACK_IS_EMPTY (stack) && + stack->pair_index[stack->size] != (pair_index & ~1)) + { + _raqm_stack_pop (stack); + } + if (!STACK_IS_EMPTY (stack)) + { + rq->text_info[i].script = _raqm_stack_top (stack); + last_script = rq->text_info[i].script; + last_set_index = i; + } + else + { + rq->text_info[i].script = last_script; + last_set_index = i; + } + } + } + else + { + rq->text_info[i].script = last_script; + last_set_index = i; + } + } + else if (rq->text_info[i].script == HB_SCRIPT_INHERITED && + last_script_index != -1) + { + rq->text_info[i].script = last_script; + last_set_index = i; + } + else + { + for (int j = last_set_index + 1; j < i; ++j) + rq->text_info[j].script = rq->text_info[i].script; + last_script = rq->text_info[i].script; + last_script_index = i; + last_set_index = i; + } + } + + /* Loop backwards and change any remaining Common or Inherit characters to + * take the script if the next character. + * https://github.com/HOST-Oman/libraqm/issues/95 + */ + for (int i = rq->text_len - 2; i >= 0; --i) + { + if (rq->text_info[i].script == HB_SCRIPT_INHERITED || + rq->text_info[i].script == HB_SCRIPT_COMMON) + rq->text_info[i].script = rq->text_info[i + 1].script; + } + +#ifdef RAQM_TESTING + RAQM_TEST ("After script detection:\n"); + for (size_t i = 0; i < rq->text_len; ++i) + { + SCRIPT_TO_STRING (rq->text_info[i].script); + RAQM_TEST ("script for ch[%zu]\t%s\n", i, buff); + } + RAQM_TEST ("\n"); +#endif + + _raqm_stack_free (stack); + + return true; +} + +static void +_raqm_ft_transform (int *x, + int *y, + FT_Matrix matrix) +{ + FT_Vector vector; + vector.x = *x; + vector.y = *y; + + FT_Vector_Transform (&vector, &matrix); + + *x = vector.x; + *y = vector.y; +} + +static bool +_raqm_shape (raqm_t *rq) +{ + hb_buffer_flags_t hb_buffer_flags = HB_BUFFER_FLAG_BOT | HB_BUFFER_FLAG_EOT; + + if (rq->invisible_glyph < 0) + hb_buffer_flags |= HB_BUFFER_FLAG_REMOVE_DEFAULT_IGNORABLES; + + for (raqm_run_t *run = rq->runs; run != NULL; run = run->next) + { + if (!run->buffer) + run->buffer = hb_buffer_create (); + + hb_buffer_add_utf32 (run->buffer, rq->text, rq->text_len, + run->pos, run->len); + hb_buffer_set_script (run->buffer, run->script); + hb_buffer_set_language (run->buffer, rq->text_info[run->pos].lang); + hb_buffer_set_direction (run->buffer, run->direction); + hb_buffer_set_flags (run->buffer, hb_buffer_flags); + + if (rq->invisible_glyph > 0) + hb_buffer_set_invisible_glyph (run->buffer, rq->invisible_glyph); + + hb_shape_full (run->font, run->buffer, rq->features, rq->features_len, + NULL); + + { + FT_Matrix matrix; + hb_glyph_info_t *info; + hb_glyph_position_t *pos; + unsigned int len; + + FT_Get_Transform (hb_ft_font_get_face (run->font), &matrix, NULL); + pos = hb_buffer_get_glyph_positions (run->buffer, &len); + info = hb_buffer_get_glyph_infos (run->buffer, &len); + + for (unsigned int i = 0; i < len; i++) + { + _raqm_ft_transform (&pos[i].x_advance, &pos[i].y_advance, matrix); + _raqm_ft_transform (&pos[i].x_offset, &pos[i].y_offset, matrix); + + bool set_spacing = false; + if (run->direction == HB_DIRECTION_RTL) + { + set_spacing = i == 0; + if (!set_spacing) + set_spacing = info[i].cluster != info[i-1].cluster; + } + else + { + set_spacing = i == len - 1; + if (!set_spacing) + set_spacing = info[i].cluster != info[i+1].cluster; + } + + _raqm_text_info rq_info = rq->text_info[info[i].cluster]; + + if (rq_info.spacing_after != 0 && set_spacing) + { + if (run->direction == HB_DIRECTION_TTB) + pos[i].y_advance -= rq_info.spacing_after; + else if (run->direction == HB_DIRECTION_RTL) + { + pos[i].x_advance += rq_info.spacing_after; + pos[i].x_offset += rq_info.spacing_after; + } + else + pos[i].x_advance += rq_info.spacing_after; + } + } + } + } + + return true; +} + +/* Count equivalent UTF-8 bytes in codepoint */ +static size_t +_raqm_count_codepoint_utf8_bytes (uint32_t chr) +{ + if (0 == ((uint32_t) 0xffffff80 & chr)) + return 1; + else if (0 == ((uint32_t) 0xfffff800 & chr)) + return 2; + else if (0 == ((uint32_t) 0xffff0000 & chr)) + return 3; + else + return 4; +} + +/* Convert index from UTF-32 to UTF-8 */ +static uint32_t +_raqm_u32_to_u8_index (raqm_t *rq, + uint32_t index) +{ + size_t length = 0; + + for (uint32_t i = 0; i < index; ++i) + length += _raqm_count_codepoint_utf8_bytes (rq->text[i]); + + return length; +} + +/* Convert index from UTF-8 to UTF-32 */ +static size_t +_raqm_u8_to_u32_index (raqm_t *rq, + size_t index) +{ + const unsigned char *s = (const unsigned char *) rq->text_utf8; + const unsigned char *t = s; + size_t length = 0; + + while (((size_t) (s - t) < index) && ('\0' != *s)) + { + if (0xf0 == (0xf8 & *s)) + s += 4; + else if (0xe0 == (0xf0 & *s)) + s += 3; + else if (0xc0 == (0xe0 & *s)) + s += 2; + else + s += 1; + + length++; + } + + if ((size_t) (s-t) > index) + length--; + + return length; +} + +/* Count equivalent UTF-16 short in codepoint */ +static size_t +_raqm_count_codepoint_utf16_short (uint32_t chr) +{ + if (chr > 0x010000) + return 2; + else + return 1; +} + +/* Convert index from UTF-32 to UTF-16 */ +static uint32_t +_raqm_u32_to_u16_index (raqm_t *rq, + uint32_t index) +{ + size_t length = 0; + + for (uint32_t i = 0; i < index; ++i) + length += _raqm_count_codepoint_utf16_short (rq->text[i]); + + return length; +} + +/* Convert index from UTF-16 to UTF-32 */ +static size_t +_raqm_u16_to_u32_index (raqm_t *rq, + size_t index) +{ + const uint16_t *s = (const uint16_t *) rq->text_utf16; + const uint16_t *t = s; + size_t length = 0; + + while (((size_t) (s - t) < index) && ('\0' != *s)) + { + if (*s < 0xD800 || *s > 0xDBFF) + s += 1; + else + s += 2; + + length++; + } + + if ((size_t) (s-t) > index) + length--; + + return length; +} + +static inline size_t +_raqm_encoding_to_u32_index (raqm_t *rq, + size_t index) +{ + if (rq->text_utf8) + return _raqm_u8_to_u32_index (rq, index); + else if (rq->text_utf16) + return _raqm_u16_to_u32_index (rq, index); + return index; +} + +static bool +_raqm_in_hangul_syllable (hb_codepoint_t ch); + +/** + * raqm_index_to_position: + * @rq: a #raqm_t. + * @index: (inout): character index. + * @x: (out): output x position. + * @y: (out): output y position. + * + * Calculates the cursor position after the character at @index. If the character + * is right-to-left, then the cursor will be at the left of it, whereas if the + * character is left-to-right, then the cursor will be at the right of it. + * + * Return value: + * `true` if the process was successful, `false` otherwise. + * + * Since: 0.2 + */ +bool +raqm_index_to_position (raqm_t *rq, + size_t *index, + int *x, + int *y) +{ + /* We don't currently support multiline, so y is always 0 */ + *y = 0; + *x = 0; + + if (rq == NULL) + return false; + + *index = _raqm_encoding_to_u32_index (rq, *index); + + if (*index >= rq->text_len) + return false; + + RAQM_TEST ("\n"); + + while (*index < rq->text_len) + { + if (_raqm_allowed_grapheme_boundary (rq->text[*index], rq->text[*index + 1])) + break; + + ++*index; + } + + for (raqm_run_t *run = rq->runs; run != NULL; run = run->next) + { + size_t len; + hb_glyph_info_t *info; + hb_glyph_position_t *position; + len = hb_buffer_get_length (run->buffer); + info = hb_buffer_get_glyph_infos (run->buffer, NULL); + position = hb_buffer_get_glyph_positions (run->buffer, NULL); + + for (size_t i = 0; i < len; i++) + { + uint32_t curr_cluster = info[i].cluster; + uint32_t next_cluster = curr_cluster; + *x += position[i].x_advance; + + if (run->direction == HB_DIRECTION_LTR) + { + for (size_t j = i + 1; j < len && next_cluster == curr_cluster; j++) + next_cluster = info[j].cluster; + } + else + { + for (int j = i - 1; i != 0 && j >= 0 && next_cluster == curr_cluster; + j--) + next_cluster = info[j].cluster; + } + + if (next_cluster == curr_cluster) + next_cluster = run->pos + run->len; + + if (*index < next_cluster && *index >= curr_cluster) + { + if (run->direction == HB_DIRECTION_RTL) + *x -= position[i].x_advance; + *index = curr_cluster; + goto found; + } + } + } + +found: + if (rq->text_utf8) + *index = _raqm_u32_to_u8_index (rq, *index); + else if (rq->text_utf16) + *index = _raqm_u32_to_u16_index (rq, *index); + RAQM_TEST ("The position is %d at index %zu\n",*x ,*index); + return true; +} + +/** + * raqm_position_to_index: + * @rq: a #raqm_t. + * @x: x position. + * @y: y position. + * @index: (out): output character index. + * + * Returns the @index of the character at @x and @y position within text. + * If the position is outside the text, the last character is chosen as + * @index. + * + * Return value: + * `true` if the process was successful, `false` in case of error. + * + * Since: 0.2 + */ +bool +raqm_position_to_index (raqm_t *rq, + int x, + int y, + size_t *index) +{ + int delta_x = 0, current_x = 0; + (void)y; + + if (rq == NULL) + return false; + + if (x < 0) /* Get leftmost index */ + { + if (rq->resolved_dir == RAQM_DIRECTION_RTL) + *index = rq->text_len; + else + *index = 0; + return true; + } + + RAQM_TEST ("\n"); + + for (raqm_run_t *run = rq->runs; run != NULL; run = run->next) + { + size_t len; + hb_glyph_info_t *info; + hb_glyph_position_t *position; + len = hb_buffer_get_length (run->buffer); + info = hb_buffer_get_glyph_infos (run->buffer, NULL); + position = hb_buffer_get_glyph_positions (run->buffer, NULL); + + for (size_t i = 0; i < len; i++) + { + delta_x = position[i].x_advance; + if (x < (current_x + delta_x)) + { + bool before = false; + if (run->direction == HB_DIRECTION_LTR) + before = (x < current_x + (delta_x / 2)); + else + before = (x > current_x + (delta_x / 2)); + + if (before) + *index = info[i].cluster; + else + { + uint32_t curr_cluster = info[i].cluster; + uint32_t next_cluster = curr_cluster; + if (run->direction == HB_DIRECTION_LTR) + for (size_t j = i + 1; j < len && next_cluster == curr_cluster; j++) + next_cluster = info[j].cluster; + else + for (int j = i - 1; i != 0 && j >= 0 && next_cluster == curr_cluster; + j--) + next_cluster = info[j].cluster; + + if (next_cluster == curr_cluster) + next_cluster = run->pos + run->len; + + *index = next_cluster; + } + if (_raqm_allowed_grapheme_boundary (rq->text[*index],rq->text[*index + 1])) + { + RAQM_TEST ("The start-index is %zu at position %d \n", *index, x); + return true; + } + + while (*index < (unsigned)run->pos + run->len) + { + if (_raqm_allowed_grapheme_boundary (rq->text[*index], + rq->text[*index + 1])) + { + *index += 1; + break; + } + *index += 1; + } + RAQM_TEST ("The start-index is %zu at position %d \n", *index, x); + return true; + } + else + current_x += delta_x; + } + } + + /* Get rightmost index*/ + if (rq->resolved_dir == RAQM_DIRECTION_RTL) + *index = 0; + else + *index = rq->text_len; + + RAQM_TEST ("The start-index is %zu at position %d \n", *index, x); + + return true; +} + +typedef enum +{ + RAQM_GRAPHEM_CR, + RAQM_GRAPHEM_LF, + RAQM_GRAPHEM_CONTROL, + RAQM_GRAPHEM_EXTEND, + RAQM_GRAPHEM_REGIONAL_INDICATOR, + RAQM_GRAPHEM_PREPEND, + RAQM_GRAPHEM_SPACING_MARK, + RAQM_GRAPHEM_HANGUL_SYLLABLE, + RAQM_GRAPHEM_OTHER +} _raqm_grapheme_t; + +static _raqm_grapheme_t +_raqm_get_grapheme_break (hb_codepoint_t ch, + hb_unicode_general_category_t category); + +static bool +_raqm_allowed_grapheme_boundary (hb_codepoint_t l_char, + hb_codepoint_t r_char) +{ + hb_unicode_general_category_t l_category; + hb_unicode_general_category_t r_category; + _raqm_grapheme_t l_grapheme, r_grapheme; + hb_unicode_funcs_t* unicode_funcs = hb_unicode_funcs_get_default (); + + l_category = hb_unicode_general_category (unicode_funcs, l_char); + r_category = hb_unicode_general_category (unicode_funcs, r_char); + l_grapheme = _raqm_get_grapheme_break (l_char, l_category); + r_grapheme = _raqm_get_grapheme_break (r_char, r_category); + + if (l_grapheme == RAQM_GRAPHEM_CR && r_grapheme == RAQM_GRAPHEM_LF) + return false; /*Do not break between a CR and LF GB3*/ + if (l_grapheme == RAQM_GRAPHEM_CONTROL || l_grapheme == RAQM_GRAPHEM_CR || + l_grapheme == RAQM_GRAPHEM_LF || r_grapheme == RAQM_GRAPHEM_CONTROL || + r_grapheme == RAQM_GRAPHEM_CR || r_grapheme == RAQM_GRAPHEM_LF) + return true; /*Break before and after CONTROL GB4, GB5*/ + if (r_grapheme == RAQM_GRAPHEM_HANGUL_SYLLABLE) + return false; /*Do not break Hangul syllable sequences. GB6, GB7, GB8*/ + if (l_grapheme == RAQM_GRAPHEM_REGIONAL_INDICATOR && + r_grapheme == RAQM_GRAPHEM_REGIONAL_INDICATOR) + return false; /*Do not break between regional indicator symbols. GB8a*/ + if (r_grapheme == RAQM_GRAPHEM_EXTEND) + return false; /*Do not break before extending characters. GB9*/ + /*Do not break before SpacingMarks, or after Prepend characters.GB9a, GB9b*/ + if (l_grapheme == RAQM_GRAPHEM_PREPEND) + return false; + if (r_grapheme == RAQM_GRAPHEM_SPACING_MARK) + return false; + return true; /*Otherwise, break everywhere. GB1, GB2, GB10*/ +} + +static _raqm_grapheme_t +_raqm_get_grapheme_break (hb_codepoint_t ch, + hb_unicode_general_category_t category) +{ + _raqm_grapheme_t gb_type; + + gb_type = RAQM_GRAPHEM_OTHER; + switch ((int)category) + { + case HB_UNICODE_GENERAL_CATEGORY_FORMAT: + if (ch == 0x200C || ch == 0x200D) + gb_type = RAQM_GRAPHEM_EXTEND; + else + gb_type = RAQM_GRAPHEM_CONTROL; + break; + + case HB_UNICODE_GENERAL_CATEGORY_CONTROL: + if (ch == 0x000D) + gb_type = RAQM_GRAPHEM_CR; + else if (ch == 0x000A) + gb_type = RAQM_GRAPHEM_LF; + else + gb_type = RAQM_GRAPHEM_CONTROL; + break; + + case HB_UNICODE_GENERAL_CATEGORY_SURROGATE: + case HB_UNICODE_GENERAL_CATEGORY_LINE_SEPARATOR: + case HB_UNICODE_GENERAL_CATEGORY_PARAGRAPH_SEPARATOR: + case HB_UNICODE_GENERAL_CATEGORY_UNASSIGNED: + if ((ch >= 0xFFF0 && ch <= 0xFFF8) || + (ch >= 0xE0000 && ch <= 0xE0FFF)) + gb_type = RAQM_GRAPHEM_CONTROL; + break; + + case HB_UNICODE_GENERAL_CATEGORY_NON_SPACING_MARK: + case HB_UNICODE_GENERAL_CATEGORY_ENCLOSING_MARK: + case HB_UNICODE_GENERAL_CATEGORY_SPACING_MARK: + if (ch != 0x102B && ch != 0x102C && ch != 0x1038 && + (ch < 0x1062 || ch > 0x1064) && (ch < 0x1067 || ch > 0x106D) && + ch != 0x1083 && (ch < 0x1087 || ch > 0x108C) && ch != 0x108F && + (ch < 0x109A || ch > 0x109C) && ch != 0x1A61 && ch != 0x1A63 && + ch != 0x1A64 && ch != 0xAA7B && ch != 0xAA70 && ch != 0x11720 && + ch != 0x11721) /**/ + gb_type = RAQM_GRAPHEM_SPACING_MARK; + + else if (ch == 0x09BE || ch == 0x09D7 || + ch == 0x0B3E || ch == 0x0B57 || ch == 0x0BBE || ch == 0x0BD7 || + ch == 0x0CC2 || ch == 0x0CD5 || ch == 0x0CD6 || + ch == 0x0D3E || ch == 0x0D57 || ch == 0x0DCF || ch == 0x0DDF || + ch == 0x1D165 || (ch >= 0x1D16E && ch <= 0x1D172)) + gb_type = RAQM_GRAPHEM_EXTEND; + break; + + case HB_UNICODE_GENERAL_CATEGORY_OTHER_LETTER: + if (ch == 0x0E33 || ch == 0x0EB3) + gb_type = RAQM_GRAPHEM_SPACING_MARK; + break; + + case HB_UNICODE_GENERAL_CATEGORY_OTHER_SYMBOL: + if (ch >= 0x1F1E6 && ch <= 0x1F1FF) + gb_type = RAQM_GRAPHEM_REGIONAL_INDICATOR; + break; + + default: + gb_type = RAQM_GRAPHEM_OTHER; + break; + } + + if (_raqm_in_hangul_syllable (ch)) + gb_type = RAQM_GRAPHEM_HANGUL_SYLLABLE; + + return gb_type; +} + +static bool +_raqm_in_hangul_syllable (hb_codepoint_t ch) +{ + (void)ch; + return false; +} + +/** + * raqm_version: + * @major: (out): Library major version component. + * @minor: (out): Library minor version component. + * @micro: (out): Library micro version component. + * + * Returns library version as three integer components. + * + * Since: 0.7 + **/ +void +raqm_version (unsigned int *major, + unsigned int *minor, + unsigned int *micro) +{ + *major = RAQM_VERSION_MAJOR; + *minor = RAQM_VERSION_MINOR; + *micro = RAQM_VERSION_MICRO; +} + +/** + * raqm_version_string: + * + * Returns library version as a string with three components. + * + * Return value: library version string. + * + * Since: 0.7 + **/ +const char * +raqm_version_string (void) +{ + return RAQM_VERSION_STRING; +} + +/** + * raqm_version_atleast: + * @major: Library major version component. + * @minor: Library minor version component. + * @micro: Library micro version component. + * + * Checks if library version is less than or equal the specified version. + * + * Return value: + * `true` if library version is less than or equal the specified version, + * `false` otherwise. + * + * Since: 0.7 + **/ +bool +raqm_version_atleast (unsigned int major, + unsigned int minor, + unsigned int micro) +{ + return RAQM_VERSION_ATLEAST (major, minor, micro); +} + +/** + * RAQM_VERSION_ATLEAST: + * @major: Library major version component. + * @minor: Library minor version component. + * @micro: Library micro version component. + * + * Checks if library version is less than or equal the specified version. + * + * Return value: + * `true` if library version is less than or equal the specified version, + * `false` otherwise. + * + * Since: 0.7 + **/ + +/** + * RAQM_VERSION_STRING: + * + * Library version as a string with three components. + * + * Since: 0.7 + **/ + +/** + * RAQM_VERSION_MAJOR: + * + * Library major version component. + * + * Since: 0.7 + **/ + +/** + * RAQM_VERSION_MINOR: + * + * Library minor version component. + * + * Since: 0.7 + **/ + +/** + * RAQM_VERSION_MICRO: + * + * Library micro version component. + * + * Since: 0.7 + **/ diff --git a/src/thirdparty/raqm/raqm.h b/src/thirdparty/raqm/raqm.h index 6fd6089c7..85aca512d 100644 --- a/src/thirdparty/raqm/raqm.h +++ b/src/thirdparty/raqm/raqm.h @@ -1,220 +1,220 @@ -/* - * Copyright © 2015 Information Technology Authority (ITA) - * Copyright © 2016-2023 Khaled Hosny - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - * - */ - -#ifndef _RAQM_H_ -#define _RAQM_H_ -#define _RAQM_H_IN_ - -#ifdef HAVE_CONFIG_H -#include "config.h" -#endif - -#ifndef RAQM_API -#define RAQM_API -#endif - -#include -#include -#include -#include FT_FREETYPE_H - -#ifdef __cplusplus -extern "C" { -#endif - -#include "raqm-version.h" - -/** - * raqm_t: - * - * This is the main object holding all state of the currently processed text as - * well as its output. - * - * Since: 0.1 - */ -typedef struct _raqm raqm_t; - -/** - * raqm_direction_t: - * @RAQM_DIRECTION_DEFAULT: Detect paragraph direction automatically. - * @RAQM_DIRECTION_RTL: Paragraph is mainly right-to-left text. - * @RAQM_DIRECTION_LTR: Paragraph is mainly left-to-right text. - * @RAQM_DIRECTION_TTB: Paragraph is mainly vertical top-to-bottom text. - * - * Base paragraph direction, see raqm_set_par_direction(). - * - * Since: 0.1 - */ -typedef enum -{ - RAQM_DIRECTION_DEFAULT, - RAQM_DIRECTION_RTL, - RAQM_DIRECTION_LTR, - RAQM_DIRECTION_TTB -} raqm_direction_t; - -/** - * raqm_glyph_t: - * @index: the index of the glyph in the font file. - * @x_advance: the glyph advance width in horizontal text. - * @y_advance: the glyph advance width in vertical text. - * @x_offset: the horizontal movement of the glyph from the current point. - * @y_offset: the vertical movement of the glyph from the current point. - * @cluster: the index of original character in input text. - * @ftface: the @FT_Face of the glyph. - * - * The structure that holds information about output glyphs, returned from - * raqm_get_glyphs(). - */ -typedef struct raqm_glyph_t { - unsigned int index; - int x_advance; - int y_advance; - int x_offset; - int y_offset; - uint32_t cluster; - FT_Face ftface; -} raqm_glyph_t; - -RAQM_API raqm_t * -raqm_create (void); - -RAQM_API raqm_t * -raqm_reference (raqm_t *rq); - -RAQM_API void -raqm_destroy (raqm_t *rq); - -RAQM_API void -raqm_clear_contents (raqm_t *rq); - -RAQM_API bool -raqm_set_text (raqm_t *rq, - const uint32_t *text, - size_t len); - -RAQM_API bool -raqm_set_text_utf8 (raqm_t *rq, - const char *text, - size_t len); -RAQM_API bool -raqm_set_text_utf16 (raqm_t *rq, - const uint16_t *text, - size_t len); - -RAQM_API bool -raqm_set_par_direction (raqm_t *rq, - raqm_direction_t dir); - -RAQM_API bool -raqm_set_language (raqm_t *rq, - const char *lang, - size_t start, - size_t len); - -RAQM_API bool -raqm_add_font_feature (raqm_t *rq, - const char *feature, - int len); - -RAQM_API bool -raqm_set_freetype_face (raqm_t *rq, - FT_Face face); - -RAQM_API bool -raqm_set_freetype_face_range (raqm_t *rq, - FT_Face face, - size_t start, - size_t len); - -RAQM_API bool -raqm_set_freetype_load_flags (raqm_t *rq, - int flags); - -RAQM_API bool -raqm_set_freetype_load_flags_range (raqm_t *rq, - int flags, - size_t start, - size_t len); - -RAQM_API bool -raqm_set_letter_spacing_range(raqm_t *rq, - int spacing, - size_t start, - size_t len); -RAQM_API bool -raqm_set_word_spacing_range(raqm_t *rq, - int spacing, - size_t start, - size_t len); - -RAQM_API bool -raqm_set_invisible_glyph (raqm_t *rq, - int gid); - -RAQM_API bool -raqm_layout (raqm_t *rq); - -RAQM_API raqm_glyph_t * -raqm_get_glyphs (raqm_t *rq, - size_t *length); - -RAQM_API raqm_direction_t -raqm_get_par_resolved_direction (raqm_t *rq); - -RAQM_API raqm_direction_t -raqm_get_direction_at_index (raqm_t *rq, - size_t index); - -RAQM_API bool -raqm_index_to_position (raqm_t *rq, - size_t *index, - int *x, - int *y); - -RAQM_API bool -raqm_position_to_index (raqm_t *rq, - int x, - int y, - size_t *index); - -RAQM_API void -raqm_version (unsigned int *major, - unsigned int *minor, - unsigned int *micro); - -RAQM_API const char * -raqm_version_string (void); - -RAQM_API bool -raqm_version_atleast (unsigned int major, - unsigned int minor, - unsigned int micro); - - -#ifdef __cplusplus -} -#endif -#undef _RAQM_H_IN_ -#endif /* _RAQM_H_ */ +/* + * Copyright © 2015 Information Technology Authority (ITA) + * Copyright © 2016-2023 Khaled Hosny + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + * + */ + +#ifndef _RAQM_H_ +#define _RAQM_H_ +#define _RAQM_H_IN_ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#ifndef RAQM_API +#define RAQM_API +#endif + +#include +#include +#include +#include FT_FREETYPE_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "raqm-version.h" + +/** + * raqm_t: + * + * This is the main object holding all state of the currently processed text as + * well as its output. + * + * Since: 0.1 + */ +typedef struct _raqm raqm_t; + +/** + * raqm_direction_t: + * @RAQM_DIRECTION_DEFAULT: Detect paragraph direction automatically. + * @RAQM_DIRECTION_RTL: Paragraph is mainly right-to-left text. + * @RAQM_DIRECTION_LTR: Paragraph is mainly left-to-right text. + * @RAQM_DIRECTION_TTB: Paragraph is mainly vertical top-to-bottom text. + * + * Base paragraph direction, see raqm_set_par_direction(). + * + * Since: 0.1 + */ +typedef enum +{ + RAQM_DIRECTION_DEFAULT, + RAQM_DIRECTION_RTL, + RAQM_DIRECTION_LTR, + RAQM_DIRECTION_TTB +} raqm_direction_t; + +/** + * raqm_glyph_t: + * @index: the index of the glyph in the font file. + * @x_advance: the glyph advance width in horizontal text. + * @y_advance: the glyph advance width in vertical text. + * @x_offset: the horizontal movement of the glyph from the current point. + * @y_offset: the vertical movement of the glyph from the current point. + * @cluster: the index of original character in input text. + * @ftface: the @FT_Face of the glyph. + * + * The structure that holds information about output glyphs, returned from + * raqm_get_glyphs(). + */ +typedef struct raqm_glyph_t { + unsigned int index; + int x_advance; + int y_advance; + int x_offset; + int y_offset; + uint32_t cluster; + FT_Face ftface; +} raqm_glyph_t; + +RAQM_API raqm_t * +raqm_create (void); + +RAQM_API raqm_t * +raqm_reference (raqm_t *rq); + +RAQM_API void +raqm_destroy (raqm_t *rq); + +RAQM_API void +raqm_clear_contents (raqm_t *rq); + +RAQM_API bool +raqm_set_text (raqm_t *rq, + const uint32_t *text, + size_t len); + +RAQM_API bool +raqm_set_text_utf8 (raqm_t *rq, + const char *text, + size_t len); +RAQM_API bool +raqm_set_text_utf16 (raqm_t *rq, + const uint16_t *text, + size_t len); + +RAQM_API bool +raqm_set_par_direction (raqm_t *rq, + raqm_direction_t dir); + +RAQM_API bool +raqm_set_language (raqm_t *rq, + const char *lang, + size_t start, + size_t len); + +RAQM_API bool +raqm_add_font_feature (raqm_t *rq, + const char *feature, + int len); + +RAQM_API bool +raqm_set_freetype_face (raqm_t *rq, + FT_Face face); + +RAQM_API bool +raqm_set_freetype_face_range (raqm_t *rq, + FT_Face face, + size_t start, + size_t len); + +RAQM_API bool +raqm_set_freetype_load_flags (raqm_t *rq, + int flags); + +RAQM_API bool +raqm_set_freetype_load_flags_range (raqm_t *rq, + int flags, + size_t start, + size_t len); + +RAQM_API bool +raqm_set_letter_spacing_range(raqm_t *rq, + int spacing, + size_t start, + size_t len); +RAQM_API bool +raqm_set_word_spacing_range(raqm_t *rq, + int spacing, + size_t start, + size_t len); + +RAQM_API bool +raqm_set_invisible_glyph (raqm_t *rq, + int gid); + +RAQM_API bool +raqm_layout (raqm_t *rq); + +RAQM_API raqm_glyph_t * +raqm_get_glyphs (raqm_t *rq, + size_t *length); + +RAQM_API raqm_direction_t +raqm_get_par_resolved_direction (raqm_t *rq); + +RAQM_API raqm_direction_t +raqm_get_direction_at_index (raqm_t *rq, + size_t index); + +RAQM_API bool +raqm_index_to_position (raqm_t *rq, + size_t *index, + int *x, + int *y); + +RAQM_API bool +raqm_position_to_index (raqm_t *rq, + int x, + int y, + size_t *index); + +RAQM_API void +raqm_version (unsigned int *major, + unsigned int *minor, + unsigned int *micro); + +RAQM_API const char * +raqm_version_string (void); + +RAQM_API bool +raqm_version_atleast (unsigned int major, + unsigned int minor, + unsigned int micro); + + +#ifdef __cplusplus +} +#endif +#undef _RAQM_H_IN_ +#endif /* _RAQM_H_ */ diff --git a/tox.ini b/tox.ini index 85a2020d6..c7557743b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,48 +1,48 @@ -[tox] -requires = - tox>=4.2 -env_list = - lint - py{py3, 312, 311, 310, 39, 38} - -[testenv] -deps = - cffi - numpy -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 -deps = - check-manifest - pre-commit -pass_env = - PRE_COMMIT_COLOR -commands = - pre-commit run --all-files --show-diff-on-failure - check-manifest - -[testenv:mypy] -skip_install = true -deps = - -r .ci/requirements-mypy.txt - IceSpringPySideStubs-PyQt6 - IceSpringPySideStubs-PySide6 - ipython - numpy - packaging - types-cffi - types-defusedxml - types-olefile -extras = - typing -commands = - mypy src {posargs} +[tox] +requires = + tox>=4.2 +env_list = + lint + py{py3, 312, 311, 310, 39, 38} + +[testenv] +deps = + cffi + numpy +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 +deps = + check-manifest + pre-commit +pass_env = + PRE_COMMIT_COLOR +commands = + pre-commit run --all-files --show-diff-on-failure + check-manifest + +[testenv:mypy] +skip_install = true +deps = + -r .ci/requirements-mypy.txt + IceSpringPySideStubs-PyQt6 + IceSpringPySideStubs-PySide6 + ipython + numpy + packaging + types-cffi + types-defusedxml + types-olefile +extras = + typing +commands = + mypy src {posargs} diff --git a/wheels/README.md b/wheels/README.md index 8b412b7fe..29e687326 100644 --- a/wheels/README.md +++ b/wheels/README.md @@ -1,35 +1,35 @@ -README ------- - -[cibuildwheel](https://github.com/pypa/cibuildwheel) is used to build macOS and Linux -wheels for tagged versions of Pillow. - -This directory contains [multibuild](https://github.com/multi-build/multibuild) to -build dependencies for the wheels, and dependency licenses to be included. - -Archives --------- - -https://github.com/python-pillow/pillow-depends contains archives for libraries -that will be built as part of the Pillow build. - -In general, there is no need to put library archives there, because the -`multibuild` scripts will download them from their respective URLs. - -But, the build will look in that repository before downloading from the -URL, so if there is a library that often fails to download, or you think might -fail to download, then download it and add it to the Git repository. - -See `build` in `.github/workflows/wheels-dependencies.sh` and the `fetch_unpack` -routine in `multibuild/common_utils.sh` for the logic, and the build recipes in -`multibuild/library_builders.sh` for the filename to give to the downloaded -archive. - -Wheels ------- - -Wheels are -[GitHub Actions artifacts created for tags, relevant changes or manual builds](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml). - -Windows wheels are created separately. They are -[GitHub Actions artifacts created on each run of the Windows workflow](https://github.com/python-pillow/Pillow/actions/workflows/test-windows.yml?query=branch%3Amain). +README +------ + +[cibuildwheel](https://github.com/pypa/cibuildwheel) is used to build macOS and Linux +wheels for tagged versions of Pillow. + +This directory contains [multibuild](https://github.com/multi-build/multibuild) to +build dependencies for the wheels, and dependency licenses to be included. + +Archives +-------- + +https://github.com/python-pillow/pillow-depends contains archives for libraries +that will be built as part of the Pillow build. + +In general, there is no need to put library archives there, because the +`multibuild` scripts will download them from their respective URLs. + +But, the build will look in that repository before downloading from the +URL, so if there is a library that often fails to download, or you think might +fail to download, then download it and add it to the Git repository. + +See `build` in `.github/workflows/wheels-dependencies.sh` and the `fetch_unpack` +routine in `multibuild/common_utils.sh` for the logic, and the build recipes in +`multibuild/library_builders.sh` for the filename to give to the downloaded +archive. + +Wheels +------ + +Wheels are +[GitHub Actions artifacts created for tags, relevant changes or manual builds](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml). + +Windows wheels are created separately. They are +[GitHub Actions artifacts created on each run of the Windows workflow](https://github.com/python-pillow/Pillow/actions/workflows/test-windows.yml?query=branch%3Amain). diff --git a/wheels/dependency_licenses/BROTLI.txt b/wheels/dependency_licenses/BROTLI.txt index 33b7cdd2d..49550fc2d 100644 --- a/wheels/dependency_licenses/BROTLI.txt +++ b/wheels/dependency_licenses/BROTLI.txt @@ -1,19 +1,19 @@ -Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/wheels/dependency_licenses/BZIP2.txt b/wheels/dependency_licenses/BZIP2.txt index d3edf477d..3afeff227 100644 --- a/wheels/dependency_licenses/BZIP2.txt +++ b/wheels/dependency_licenses/BZIP2.txt @@ -1,42 +1,42 @@ - --------------------------------------------------------------------------- - -This program, "bzip2", the associated library "libbzip2", and all -documentation, are copyright (C) 1996-2019 Julian R Seward. 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. The origin of this software must not be misrepresented; you must - not claim that you wrote the original software. If you use this - software in a product, an acknowledgment in the product - documentation would be appreciated but is not required. - -3. Altered source versions must be plainly marked as such, and must - not be misrepresented as being the original software. - -4. The name of the author may not be used to endorse or promote - products derived from this software without specific prior written - permission. - -THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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. - -Julian Seward, jseward@acm.org -bzip2/libbzip2 version 1.0.8 of 13 July 2019 - --------------------------------------------------------------------------- + +-------------------------------------------------------------------------- + +This program, "bzip2", the associated library "libbzip2", and all +documentation, are copyright (C) 1996-2019 Julian R Seward. 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. The origin of this software must not be misrepresented; you must + not claim that you wrote the original software. If you use this + software in a product, an acknowledgment in the product + documentation would be appreciated but is not required. + +3. Altered source versions must be plainly marked as such, and must + not be misrepresented as being the original software. + +4. The name of the author may not be used to endorse or promote + products derived from this software without specific prior written + permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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. + +Julian Seward, jseward@acm.org +bzip2/libbzip2 version 1.0.8 of 13 July 2019 + +-------------------------------------------------------------------------- diff --git a/wheels/dependency_licenses/FREETYPE2.txt b/wheels/dependency_licenses/FREETYPE2.txt index 93efc6126..c7b72abdd 100644 --- a/wheels/dependency_licenses/FREETYPE2.txt +++ b/wheels/dependency_licenses/FREETYPE2.txt @@ -1,652 +1,652 @@ -The FreeType 2 font engine is copyrighted work and cannot be used -legally without a software license. In order to make this project -usable to a vast majority of developers, we distribute it under two -mutually exclusive open-source licenses. - -This means that *you* must choose *one* of the two licenses described -below, then obey all its terms and conditions when using FreeType 2 in -any of your projects or products. - - - The FreeType License, found in the file `docs/FTL.TXT`, which is - similar to the original BSD license *with* an advertising clause - that forces you to explicitly cite the FreeType project in your - product's documentation. All details are in the license file. - This license is suited to products which don't use the GNU General - Public License. - - Note that this license is compatible to the GNU General Public - License version 3, but not version 2. - - - The GNU General Public License version 2, found in - `docs/GPLv2.TXT` (any later version can be used also), for - programs which already use the GPL. Note that the FTL is - incompatible with GPLv2 due to its advertisement clause. - -The contributed BDF and PCF drivers come with a license similar to -that of the X Window System. It is compatible to the above two -licenses (see files `src/bdf/README` and `src/pcf/README`). The same -holds for the source code files `src/base/fthash.c` and -`include/freetype/internal/fthash.h`; they were part of the BDF driver -in earlier FreeType versions. - -The gzip module uses the zlib license (see `src/gzip/zlib.h`) which -too is compatible to the above two licenses. - -The files `src/autofit/ft-hb.c` and `src/autofit/ft-hb.h` contain code -taken almost verbatim from the HarfBuzz file `hb-ft.cc`, which uses -the 'Old MIT' license, compatible to the above two licenses. - -The MD5 checksum support (only used for debugging in development -builds) is in the public domain. - --------------------------------------------------------------------------- - - The FreeType Project LICENSE - ---------------------------- - - 2006-Jan-27 - - Copyright 1996-2002, 2006 by - David Turner, Robert Wilhelm, and Werner Lemberg - - - -Introduction -============ - - The FreeType Project is distributed in several archive packages; - some of them may contain, in addition to the FreeType font engine, - various tools and contributions which rely on, or relate to, the - FreeType Project. - - This license applies to all files found in such packages, and - which do not fall under their own explicit license. The license - affects thus the FreeType font engine, the test programs, - documentation and makefiles, at the very least. - - This license was inspired by the BSD, Artistic, and IJG - (Independent JPEG Group) licenses, which all encourage inclusion - and use of free software in commercial and freeware products - alike. As a consequence, its main points are that: - - o We don't promise that this software works. However, we will be - interested in any kind of bug reports. (`as is' distribution) - - o You can use this software for whatever you want, in parts or - full form, without having to pay us. (`royalty-free' usage) - - o You may not pretend that you wrote this software. If you use - it, or only parts of it, in a program, you must acknowledge - somewhere in your documentation that you have used the - FreeType code. (`credits') - - We specifically permit and encourage the inclusion of this - software, with or without modifications, in commercial products. - We disclaim all warranties covering The FreeType Project and - assume no liability related to The FreeType Project. - - - Finally, many people asked us for a preferred form for a - credit/disclaimer to use in compliance with this license. We thus - encourage you to use the following text: - - """ - Portions of this software are copyright © The FreeType - Project (www.freetype.org). All rights reserved. - """ - - Please replace with the value from the FreeType version you - actually use. - - -Legal Terms -=========== - -0. Definitions --------------- - - Throughout this license, the terms `package', `FreeType Project', - and `FreeType archive' refer to the set of files originally - distributed by the authors (David Turner, Robert Wilhelm, and - Werner Lemberg) as the `FreeType Project', be they named as alpha, - beta or final release. - - `You' refers to the licensee, or person using the project, where - `using' is a generic term including compiling the project's source - code as well as linking it to form a `program' or `executable'. - This program is referred to as `a program using the FreeType - engine'. - - This license applies to all files distributed in the original - FreeType Project, including all source code, binaries and - documentation, unless otherwise stated in the file in its - original, unmodified form as distributed in the original archive. - If you are unsure whether or not a particular file is covered by - this license, you must contact us to verify this. - - The FreeType Project is copyright (C) 1996-2000 by David Turner, - Robert Wilhelm, and Werner Lemberg. All rights reserved except as - specified below. - -1. No Warranty --------------- - - THE FREETYPE PROJECT IS PROVIDED `AS IS' WITHOUT WARRANTY OF ANY - KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, - WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - PURPOSE. IN NO EVENT WILL ANY OF THE AUTHORS OR COPYRIGHT HOLDERS - BE LIABLE FOR ANY DAMAGES CAUSED BY THE USE OR THE INABILITY TO - USE, OF THE FREETYPE PROJECT. - -2. Redistribution ------------------ - - This license grants a worldwide, royalty-free, perpetual and - irrevocable right and license to use, execute, perform, compile, - display, copy, create derivative works of, distribute and - sublicense the FreeType Project (in both source and object code - forms) and derivative works thereof for any purpose; and to - authorize others to exercise some or all of the rights granted - herein, subject to the following conditions: - - o Redistribution of source code must retain this license file - (`FTL.TXT') unaltered; any additions, deletions or changes to - the original files must be clearly indicated in accompanying - documentation. The copyright notices of the unaltered, - original files must be preserved in all copies of source - files. - - o Redistribution in binary form must provide a disclaimer that - states that the software is based in part of the work of the - FreeType Team, in the distribution documentation. We also - encourage you to put an URL to the FreeType web page in your - documentation, though this isn't mandatory. - - These conditions apply to any software derived from or based on - the FreeType Project, not just the unmodified files. If you use - our work, you must acknowledge us. However, no fee need be paid - to us. - -3. Advertising --------------- - - Neither the FreeType authors and contributors nor you shall use - the name of the other for commercial, advertising, or promotional - purposes without specific prior written permission. - - We suggest, but do not require, that you use one or more of the - following phrases to refer to this software in your documentation - or advertising materials: `FreeType Project', `FreeType Engine', - `FreeType library', or `FreeType Distribution'. - - As you have not signed this license, you are not required to - accept it. However, as the FreeType Project is copyrighted - material, only this license, or another one contracted with the - authors, grants you the right to use, distribute, and modify it. - Therefore, by using, distributing, or modifying the FreeType - Project, you indicate that you understand and accept all the terms - of this license. - -4. Contacts ------------ - - There are two mailing lists related to FreeType: - - o freetype@nongnu.org - - Discusses general use and applications of FreeType, as well as - future and wanted additions to the library and distribution. - If you are looking for support, start in this list if you - haven't found anything to help you in the documentation. - - o freetype-devel@nongnu.org - - Discusses bugs, as well as engine internals, design issues, - specific licenses, porting, etc. - - Our home page can be found at - - https://www.freetype.org - - ---- 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`: - -``` -License -******* - -Copyright (C) 2001-2002 by Francesco Zappa Nardelli - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -*** Portions of the driver (that is, bdflib.c and bdf.h): - -Copyright 2000 Computing Research Labs, New Mexico State University -Copyright 2001-2002, 2011 Francesco Zappa Nardelli - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -THE COMPUTING RESEARCH LAB OR NEW MEXICO STATE UNIVERSITY BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT -OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR -THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - -Credits -******* - -This driver is based on excellent Mark Leisher's bdf library. If you -find something good in this driver you should probably thank him, not -me. -``` - -The following license details are part of `src/pcf/README`: - -``` -License -******* - -Copyright (C) 2000 by Francesco Zappa Nardelli - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - -Credits -******* - -Keith Packard wrote the pcf driver found in XFree86. His work is at -the same time the specification and the sample implementation of the -PCF format. Undoubtedly, this driver is inspired from his work. -``` +The FreeType 2 font engine is copyrighted work and cannot be used +legally without a software license. In order to make this project +usable to a vast majority of developers, we distribute it under two +mutually exclusive open-source licenses. + +This means that *you* must choose *one* of the two licenses described +below, then obey all its terms and conditions when using FreeType 2 in +any of your projects or products. + + - The FreeType License, found in the file `docs/FTL.TXT`, which is + similar to the original BSD license *with* an advertising clause + that forces you to explicitly cite the FreeType project in your + product's documentation. All details are in the license file. + This license is suited to products which don't use the GNU General + Public License. + + Note that this license is compatible to the GNU General Public + License version 3, but not version 2. + + - The GNU General Public License version 2, found in + `docs/GPLv2.TXT` (any later version can be used also), for + programs which already use the GPL. Note that the FTL is + incompatible with GPLv2 due to its advertisement clause. + +The contributed BDF and PCF drivers come with a license similar to +that of the X Window System. It is compatible to the above two +licenses (see files `src/bdf/README` and `src/pcf/README`). The same +holds for the source code files `src/base/fthash.c` and +`include/freetype/internal/fthash.h`; they were part of the BDF driver +in earlier FreeType versions. + +The gzip module uses the zlib license (see `src/gzip/zlib.h`) which +too is compatible to the above two licenses. + +The files `src/autofit/ft-hb.c` and `src/autofit/ft-hb.h` contain code +taken almost verbatim from the HarfBuzz file `hb-ft.cc`, which uses +the 'Old MIT' license, compatible to the above two licenses. + +The MD5 checksum support (only used for debugging in development +builds) is in the public domain. + +-------------------------------------------------------------------------- + + The FreeType Project LICENSE + ---------------------------- + + 2006-Jan-27 + + Copyright 1996-2002, 2006 by + David Turner, Robert Wilhelm, and Werner Lemberg + + + +Introduction +============ + + The FreeType Project is distributed in several archive packages; + some of them may contain, in addition to the FreeType font engine, + various tools and contributions which rely on, or relate to, the + FreeType Project. + + This license applies to all files found in such packages, and + which do not fall under their own explicit license. The license + affects thus the FreeType font engine, the test programs, + documentation and makefiles, at the very least. + + This license was inspired by the BSD, Artistic, and IJG + (Independent JPEG Group) licenses, which all encourage inclusion + and use of free software in commercial and freeware products + alike. As a consequence, its main points are that: + + o We don't promise that this software works. However, we will be + interested in any kind of bug reports. (`as is' distribution) + + o You can use this software for whatever you want, in parts or + full form, without having to pay us. (`royalty-free' usage) + + o You may not pretend that you wrote this software. If you use + it, or only parts of it, in a program, you must acknowledge + somewhere in your documentation that you have used the + FreeType code. (`credits') + + We specifically permit and encourage the inclusion of this + software, with or without modifications, in commercial products. + We disclaim all warranties covering The FreeType Project and + assume no liability related to The FreeType Project. + + + Finally, many people asked us for a preferred form for a + credit/disclaimer to use in compliance with this license. We thus + encourage you to use the following text: + + """ + Portions of this software are copyright © The FreeType + Project (www.freetype.org). All rights reserved. + """ + + Please replace with the value from the FreeType version you + actually use. + + +Legal Terms +=========== + +0. Definitions +-------------- + + Throughout this license, the terms `package', `FreeType Project', + and `FreeType archive' refer to the set of files originally + distributed by the authors (David Turner, Robert Wilhelm, and + Werner Lemberg) as the `FreeType Project', be they named as alpha, + beta or final release. + + `You' refers to the licensee, or person using the project, where + `using' is a generic term including compiling the project's source + code as well as linking it to form a `program' or `executable'. + This program is referred to as `a program using the FreeType + engine'. + + This license applies to all files distributed in the original + FreeType Project, including all source code, binaries and + documentation, unless otherwise stated in the file in its + original, unmodified form as distributed in the original archive. + If you are unsure whether or not a particular file is covered by + this license, you must contact us to verify this. + + The FreeType Project is copyright (C) 1996-2000 by David Turner, + Robert Wilhelm, and Werner Lemberg. All rights reserved except as + specified below. + +1. No Warranty +-------------- + + THE FREETYPE PROJECT IS PROVIDED `AS IS' WITHOUT WARRANTY OF ANY + KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + PURPOSE. IN NO EVENT WILL ANY OF THE AUTHORS OR COPYRIGHT HOLDERS + BE LIABLE FOR ANY DAMAGES CAUSED BY THE USE OR THE INABILITY TO + USE, OF THE FREETYPE PROJECT. + +2. Redistribution +----------------- + + This license grants a worldwide, royalty-free, perpetual and + irrevocable right and license to use, execute, perform, compile, + display, copy, create derivative works of, distribute and + sublicense the FreeType Project (in both source and object code + forms) and derivative works thereof for any purpose; and to + authorize others to exercise some or all of the rights granted + herein, subject to the following conditions: + + o Redistribution of source code must retain this license file + (`FTL.TXT') unaltered; any additions, deletions or changes to + the original files must be clearly indicated in accompanying + documentation. The copyright notices of the unaltered, + original files must be preserved in all copies of source + files. + + o Redistribution in binary form must provide a disclaimer that + states that the software is based in part of the work of the + FreeType Team, in the distribution documentation. We also + encourage you to put an URL to the FreeType web page in your + documentation, though this isn't mandatory. + + These conditions apply to any software derived from or based on + the FreeType Project, not just the unmodified files. If you use + our work, you must acknowledge us. However, no fee need be paid + to us. + +3. Advertising +-------------- + + Neither the FreeType authors and contributors nor you shall use + the name of the other for commercial, advertising, or promotional + purposes without specific prior written permission. + + We suggest, but do not require, that you use one or more of the + following phrases to refer to this software in your documentation + or advertising materials: `FreeType Project', `FreeType Engine', + `FreeType library', or `FreeType Distribution'. + + As you have not signed this license, you are not required to + accept it. However, as the FreeType Project is copyrighted + material, only this license, or another one contracted with the + authors, grants you the right to use, distribute, and modify it. + Therefore, by using, distributing, or modifying the FreeType + Project, you indicate that you understand and accept all the terms + of this license. + +4. Contacts +----------- + + There are two mailing lists related to FreeType: + + o freetype@nongnu.org + + Discusses general use and applications of FreeType, as well as + future and wanted additions to the library and distribution. + If you are looking for support, start in this list if you + haven't found anything to help you in the documentation. + + o freetype-devel@nongnu.org + + Discusses bugs, as well as engine internals, design issues, + specific licenses, porting, etc. + + Our home page can be found at + + https://www.freetype.org + + +--- 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`: + +``` +License +******* + +Copyright (C) 2001-2002 by Francesco Zappa Nardelli + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +*** Portions of the driver (that is, bdflib.c and bdf.h): + +Copyright 2000 Computing Research Labs, New Mexico State University +Copyright 2001-2002, 2011 Francesco Zappa Nardelli + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE COMPUTING RESEARCH LAB OR NEW MEXICO STATE UNIVERSITY BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT +OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR +THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +Credits +******* + +This driver is based on excellent Mark Leisher's bdf library. If you +find something good in this driver you should probably thank him, not +me. +``` + +The following license details are part of `src/pcf/README`: + +``` +License +******* + +Copyright (C) 2000 by Francesco Zappa Nardelli + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +Credits +******* + +Keith Packard wrote the pcf driver found in XFree86. His work is at +the same time the specification and the sample implementation of the +PCF format. Undoubtedly, this driver is inspired from his work. +``` diff --git a/wheels/dependency_licenses/HARFBUZZ.txt b/wheels/dependency_licenses/HARFBUZZ.txt index 1dd917e9f..be7c0a1d2 100644 --- a/wheels/dependency_licenses/HARFBUZZ.txt +++ b/wheels/dependency_licenses/HARFBUZZ.txt @@ -1,42 +1,42 @@ -HarfBuzz is licensed under the so-called "Old MIT" license. Details follow. -For parts of HarfBuzz that are licensed under different licenses see individual -files names COPYING in subdirectories where applicable. - -Copyright © 2010-2022 Google, Inc. -Copyright © 2015-2020 Ebrahim Byagowi -Copyright © 2019,2020 Facebook, Inc. -Copyright © 2012,2015 Mozilla Foundation -Copyright © 2011 Codethink Limited -Copyright © 2008,2010 Nokia Corporation and/or its subsidiary(-ies) -Copyright © 2009 Keith Stribley -Copyright © 2011 Martin Hosken and SIL International -Copyright © 2007 Chris Wilson -Copyright © 2005,2006,2020,2021,2022,2023 Behdad Esfahbod -Copyright © 2004,2007,2008,2009,2010,2013,2021,2022,2023 Red Hat, Inc. -Copyright © 1998-2005 David Turner and Werner Lemberg -Copyright © 2016 Igalia S.L. -Copyright © 2022 Matthias Clasen -Copyright © 2018,2021 Khaled Hosny -Copyright © 2018,2019,2020 Adobe, Inc -Copyright © 2013-2015 Alexei Podtelezhnikov - -For full copyright notices consult the individual files in the package. - - -Permission is hereby granted, without written agreement and without -license or royalty fees, to use, copy, modify, and distribute this -software and its documentation for any purpose, provided that the -above copyright notice and the following two paragraphs appear in -all copies of this software. - -IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR -DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES -ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN -IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH -DAMAGE. - -THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, -BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND -FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS -ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO -PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +HarfBuzz is licensed under the so-called "Old MIT" license. Details follow. +For parts of HarfBuzz that are licensed under different licenses see individual +files names COPYING in subdirectories where applicable. + +Copyright © 2010-2022 Google, Inc. +Copyright © 2015-2020 Ebrahim Byagowi +Copyright © 2019,2020 Facebook, Inc. +Copyright © 2012,2015 Mozilla Foundation +Copyright © 2011 Codethink Limited +Copyright © 2008,2010 Nokia Corporation and/or its subsidiary(-ies) +Copyright © 2009 Keith Stribley +Copyright © 2011 Martin Hosken and SIL International +Copyright © 2007 Chris Wilson +Copyright © 2005,2006,2020,2021,2022,2023 Behdad Esfahbod +Copyright © 2004,2007,2008,2009,2010,2013,2021,2022,2023 Red Hat, Inc. +Copyright © 1998-2005 David Turner and Werner Lemberg +Copyright © 2016 Igalia S.L. +Copyright © 2022 Matthias Clasen +Copyright © 2018,2021 Khaled Hosny +Copyright © 2018,2019,2020 Adobe, Inc +Copyright © 2013-2015 Alexei Podtelezhnikov + +For full copyright notices consult the individual files in the package. + + +Permission is hereby granted, without written agreement and without +license or royalty fees, to use, copy, modify, and distribute this +software and its documentation for any purpose, provided that the +above copyright notice and the following two paragraphs appear in +all copies of this software. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR +DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN +IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO +PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. diff --git a/wheels/dependency_licenses/LCMS2.txt b/wheels/dependency_licenses/LCMS2.txt index 21ed6fb86..e2d5a0aab 100644 --- a/wheels/dependency_licenses/LCMS2.txt +++ b/wheels/dependency_licenses/LCMS2.txt @@ -1,8 +1,8 @@ -Little CMS -Copyright (c) 1998-2020 Marti Maria Saguer - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +Little CMS +Copyright (c) 1998-2020 Marti Maria Saguer + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/wheels/dependency_licenses/LIBJPEG.txt b/wheels/dependency_licenses/LIBJPEG.txt index b5451f405..cd2f09497 100644 --- a/wheels/dependency_licenses/LIBJPEG.txt +++ b/wheels/dependency_licenses/LIBJPEG.txt @@ -1,43 +1,43 @@ -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-2020, 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. +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-2020, 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. diff --git a/wheels/dependency_licenses/LIBLZMA.txt b/wheels/dependency_licenses/LIBLZMA.txt index 43c7a23ba..9a4c1bfb3 100644 --- a/wheels/dependency_licenses/LIBLZMA.txt +++ b/wheels/dependency_licenses/LIBLZMA.txt @@ -1,63 +1,63 @@ -XZ Utils Licensing -================== - - Different licenses apply to different files in this package. Here - is a rough summary of which licenses apply to which parts of this - package (but check the individual files to be sure!): - - - liblzma is in the public domain. - - - xz, xzdec, and lzmadec command line tools are in the public - domain unless GNU getopt_long had to be compiled and linked - in from the lib directory. The getopt_long code is under - GNU LGPLv2.1+. - - - The scripts to grep, diff, and view compressed files have been - adapted from gzip. These scripts and their documentation are - under GNU GPLv2+. - - - All the documentation in the doc directory and most of the - XZ Utils specific documentation files in other directories - are in the public domain. - - - Translated messages are in the public domain. - - - The build system contains public domain files, and files that - are under GNU GPLv2+ or GNU GPLv3+. None of these files end up - in the binaries being built. - - - Test files and test code in the tests directory, and debugging - utilities in the debug directory are in the public domain. - - - The extra directory may contain public domain files, and files - that are under various free software licenses. - - You can do whatever you want with the files that have been put into - the public domain. If you find public domain legally problematic, - take the previous sentence as a license grant. If you still find - the lack of copyright legally problematic, you have too many - lawyers. - - As usual, this software is provided "as is", without any warranty. - - If you copy significant amounts of public domain code from XZ Utils - into your project, acknowledging this somewhere in your software is - polite (especially if it is proprietary, non-free software), but - naturally it is not legally required. Here is an example of a good - notice to put into "about box" or into documentation: - - This software includes code from XZ Utils . - - The following license texts are included in the following files: - - COPYING.LGPLv2.1: GNU Lesser General Public License version 2.1 - - COPYING.GPLv2: GNU General Public License version 2 - - COPYING.GPLv3: GNU General Public License version 3 - - Note that the toolchain (compiler, linker etc.) may add some code - pieces that are copyrighted. Thus, it is possible that e.g. liblzma - binary wouldn't actually be in the public domain in its entirety - even though it contains no copyrighted code from the XZ Utils source - package. - - If you have questions, don't hesitate to ask the author(s) for more - information. +XZ Utils Licensing +================== + + Different licenses apply to different files in this package. Here + is a rough summary of which licenses apply to which parts of this + package (but check the individual files to be sure!): + + - liblzma is in the public domain. + + - xz, xzdec, and lzmadec command line tools are in the public + domain unless GNU getopt_long had to be compiled and linked + in from the lib directory. The getopt_long code is under + GNU LGPLv2.1+. + + - The scripts to grep, diff, and view compressed files have been + adapted from gzip. These scripts and their documentation are + under GNU GPLv2+. + + - All the documentation in the doc directory and most of the + XZ Utils specific documentation files in other directories + are in the public domain. + + - Translated messages are in the public domain. + + - The build system contains public domain files, and files that + are under GNU GPLv2+ or GNU GPLv3+. None of these files end up + in the binaries being built. + + - Test files and test code in the tests directory, and debugging + utilities in the debug directory are in the public domain. + + - The extra directory may contain public domain files, and files + that are under various free software licenses. + + You can do whatever you want with the files that have been put into + the public domain. If you find public domain legally problematic, + take the previous sentence as a license grant. If you still find + the lack of copyright legally problematic, you have too many + lawyers. + + As usual, this software is provided "as is", without any warranty. + + If you copy significant amounts of public domain code from XZ Utils + into your project, acknowledging this somewhere in your software is + polite (especially if it is proprietary, non-free software), but + naturally it is not legally required. Here is an example of a good + notice to put into "about box" or into documentation: + + This software includes code from XZ Utils . + + The following license texts are included in the following files: + - COPYING.LGPLv2.1: GNU Lesser General Public License version 2.1 + - COPYING.GPLv2: GNU General Public License version 2 + - COPYING.GPLv3: GNU General Public License version 3 + + Note that the toolchain (compiler, linker etc.) may add some code + pieces that are copyrighted. Thus, it is possible that e.g. liblzma + binary wouldn't actually be in the public domain in its entirety + even though it contains no copyrighted code from the XZ Utils source + package. + + If you have questions, don't hesitate to ask the author(s) for more + information. diff --git a/wheels/dependency_licenses/LIBPNG.txt b/wheels/dependency_licenses/LIBPNG.txt index c8ad24eec..a2fdb0327 100644 --- a/wheels/dependency_licenses/LIBPNG.txt +++ b/wheels/dependency_licenses/LIBPNG.txt @@ -1,134 +1,134 @@ -COPYRIGHT NOTICE, DISCLAIMER, and LICENSE -========================================= - -PNG Reference Library License version 2 ---------------------------------------- - - * Copyright (c) 1995-2022 The PNG Reference Library Authors. - * Copyright (c) 2018-2022 Cosmin Truta. - * Copyright (c) 2000-2002, 2004, 2006-2018 Glenn Randers-Pehrson. - * Copyright (c) 1996-1997 Andreas Dilger. - * Copyright (c) 1995-1996 Guy Eric Schalnat, Group 42, Inc. - -The software is supplied "as is", without warranty of any kind, -express or implied, including, without limitation, the warranties -of merchantability, fitness for a particular purpose, title, and -non-infringement. In no event shall the Copyright owners, or -anyone distributing the software, be liable for any damages or -other liability, whether in contract, tort or otherwise, arising -from, out of, or in connection with the software, or the use or -other dealings in the software, even if advised of the possibility -of such damage. - -Permission is hereby granted to use, copy, modify, and distribute -this software, or portions hereof, for any purpose, without fee, -subject to the following restrictions: - - 1. The origin of this software must not be misrepresented; you - must not claim that you wrote the original software. If you - use this software in a product, an acknowledgment in the product - documentation would be appreciated, but is not required. - - 2. Altered source versions must be plainly marked as such, and must - not be misrepresented as being the original software. - - 3. This Copyright notice may not be removed or altered from any - source or altered source distribution. - - -PNG Reference Library License version 1 (for libpng 0.5 through 1.6.35) ------------------------------------------------------------------------ - -libpng versions 1.0.7, July 1, 2000, through 1.6.35, July 15, 2018 are -Copyright (c) 2000-2002, 2004, 2006-2018 Glenn Randers-Pehrson, are -derived from libpng-1.0.6, and are distributed according to the same -disclaimer and license as libpng-1.0.6 with the following individuals -added to the list of Contributing Authors: - - Simon-Pierre Cadieux - Eric S. Raymond - Mans Rullgard - Cosmin Truta - Gilles Vollant - James Yu - Mandar Sahastrabuddhe - Google Inc. - Vadim Barkov - -and with the following additions to the disclaimer: - - There is no warranty against interference with your enjoyment of - the library or against infringement. There is no warranty that our - efforts or the library will fulfill any of your particular purposes - or needs. This library is provided with all faults, and the entire - risk of satisfactory quality, performance, accuracy, and effort is - with the user. - -Some files in the "contrib" directory and some configure-generated -files that are distributed with libpng have other copyright owners, and -are released under other open source licenses. - -libpng versions 0.97, January 1998, through 1.0.6, March 20, 2000, are -Copyright (c) 1998-2000 Glenn Randers-Pehrson, are derived from -libpng-0.96, and are distributed according to the same disclaimer and -license as libpng-0.96, with the following individuals added to the -list of Contributing Authors: - - Tom Lane - Glenn Randers-Pehrson - Willem van Schaik - -libpng versions 0.89, June 1996, through 0.96, May 1997, are -Copyright (c) 1996-1997 Andreas Dilger, are derived from libpng-0.88, -and are distributed according to the same disclaimer and license as -libpng-0.88, with the following individuals added to the list of -Contributing Authors: - - John Bowler - Kevin Bracey - Sam Bushell - Magnus Holmgren - Greg Roelofs - Tom Tanner - -Some files in the "scripts" directory have other copyright owners, -but are released under this license. - -libpng versions 0.5, May 1995, through 0.88, January 1996, are -Copyright (c) 1995-1996 Guy Eric Schalnat, Group 42, Inc. - -For the purposes of this copyright and license, "Contributing Authors" -is defined as the following set of individuals: - - Andreas Dilger - Dave Martindale - Guy Eric Schalnat - Paul Schmidt - Tim Wegner - -The PNG Reference Library is supplied "AS IS". The Contributing -Authors and Group 42, Inc. disclaim all warranties, expressed or -implied, including, without limitation, the warranties of -merchantability and of fitness for any purpose. The Contributing -Authors and Group 42, Inc. assume no liability for direct, indirect, -incidental, special, exemplary, or consequential damages, which may -result from the use of the PNG Reference Library, even if advised of -the possibility of such damage. - -Permission is hereby granted to use, copy, modify, and distribute this -source code, or portions hereof, for any purpose, without fee, subject -to the following restrictions: - - 1. The origin of this source code must not be misrepresented. - - 2. Altered versions must be plainly marked as such and must not - be misrepresented as being the original source. - - 3. This Copyright notice may not be removed or altered from any - source or altered source distribution. - -The Contributing Authors and Group 42, Inc. specifically permit, -without fee, and encourage the use of this source code as a component -to supporting the PNG file format in commercial products. If you use -this source code in a product, acknowledgment is not required but would -be appreciated. +COPYRIGHT NOTICE, DISCLAIMER, and LICENSE +========================================= + +PNG Reference Library License version 2 +--------------------------------------- + + * Copyright (c) 1995-2022 The PNG Reference Library Authors. + * Copyright (c) 2018-2022 Cosmin Truta. + * Copyright (c) 2000-2002, 2004, 2006-2018 Glenn Randers-Pehrson. + * Copyright (c) 1996-1997 Andreas Dilger. + * Copyright (c) 1995-1996 Guy Eric Schalnat, Group 42, Inc. + +The software is supplied "as is", without warranty of any kind, +express or implied, including, without limitation, the warranties +of merchantability, fitness for a particular purpose, title, and +non-infringement. In no event shall the Copyright owners, or +anyone distributing the software, be liable for any damages or +other liability, whether in contract, tort or otherwise, arising +from, out of, or in connection with the software, or the use or +other dealings in the software, even if advised of the possibility +of such damage. + +Permission is hereby granted to use, copy, modify, and distribute +this software, or portions hereof, for any purpose, without fee, +subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you + must not claim that you wrote the original software. If you + use this software in a product, an acknowledgment in the product + documentation would be appreciated, but is not required. + + 2. Altered source versions must be plainly marked as such, and must + not be misrepresented as being the original software. + + 3. This Copyright notice may not be removed or altered from any + source or altered source distribution. + + +PNG Reference Library License version 1 (for libpng 0.5 through 1.6.35) +----------------------------------------------------------------------- + +libpng versions 1.0.7, July 1, 2000, through 1.6.35, July 15, 2018 are +Copyright (c) 2000-2002, 2004, 2006-2018 Glenn Randers-Pehrson, are +derived from libpng-1.0.6, and are distributed according to the same +disclaimer and license as libpng-1.0.6 with the following individuals +added to the list of Contributing Authors: + + Simon-Pierre Cadieux + Eric S. Raymond + Mans Rullgard + Cosmin Truta + Gilles Vollant + James Yu + Mandar Sahastrabuddhe + Google Inc. + Vadim Barkov + +and with the following additions to the disclaimer: + + There is no warranty against interference with your enjoyment of + the library or against infringement. There is no warranty that our + efforts or the library will fulfill any of your particular purposes + or needs. This library is provided with all faults, and the entire + risk of satisfactory quality, performance, accuracy, and effort is + with the user. + +Some files in the "contrib" directory and some configure-generated +files that are distributed with libpng have other copyright owners, and +are released under other open source licenses. + +libpng versions 0.97, January 1998, through 1.0.6, March 20, 2000, are +Copyright (c) 1998-2000 Glenn Randers-Pehrson, are derived from +libpng-0.96, and are distributed according to the same disclaimer and +license as libpng-0.96, with the following individuals added to the +list of Contributing Authors: + + Tom Lane + Glenn Randers-Pehrson + Willem van Schaik + +libpng versions 0.89, June 1996, through 0.96, May 1997, are +Copyright (c) 1996-1997 Andreas Dilger, are derived from libpng-0.88, +and are distributed according to the same disclaimer and license as +libpng-0.88, with the following individuals added to the list of +Contributing Authors: + + John Bowler + Kevin Bracey + Sam Bushell + Magnus Holmgren + Greg Roelofs + Tom Tanner + +Some files in the "scripts" directory have other copyright owners, +but are released under this license. + +libpng versions 0.5, May 1995, through 0.88, January 1996, are +Copyright (c) 1995-1996 Guy Eric Schalnat, Group 42, Inc. + +For the purposes of this copyright and license, "Contributing Authors" +is defined as the following set of individuals: + + Andreas Dilger + Dave Martindale + Guy Eric Schalnat + Paul Schmidt + Tim Wegner + +The PNG Reference Library is supplied "AS IS". The Contributing +Authors and Group 42, Inc. disclaim all warranties, expressed or +implied, including, without limitation, the warranties of +merchantability and of fitness for any purpose. The Contributing +Authors and Group 42, Inc. assume no liability for direct, indirect, +incidental, special, exemplary, or consequential damages, which may +result from the use of the PNG Reference Library, even if advised of +the possibility of such damage. + +Permission is hereby granted to use, copy, modify, and distribute this +source code, or portions hereof, for any purpose, without fee, subject +to the following restrictions: + + 1. The origin of this source code must not be misrepresented. + + 2. Altered versions must be plainly marked as such and must not + be misrepresented as being the original source. + + 3. This Copyright notice may not be removed or altered from any + source or altered source distribution. + +The Contributing Authors and Group 42, Inc. specifically permit, +without fee, and encourage the use of this source code as a component +to supporting the PNG file format in commercial products. If you use +this source code in a product, acknowledgment is not required but would +be appreciated. diff --git a/wheels/dependency_licenses/LIBTIFF.txt b/wheels/dependency_licenses/LIBTIFF.txt index dc255dec6..c330e072c 100644 --- a/wheels/dependency_licenses/LIBTIFF.txt +++ b/wheels/dependency_licenses/LIBTIFF.txt @@ -1,21 +1,21 @@ -Copyright (c) 1988-1997 Sam Leffler -Copyright (c) 1991-1997 Silicon Graphics, Inc. - -Permission to use, copy, modify, distribute, and sell this software and -its documentation for any purpose is hereby granted without fee, provided -that (i) the above copyright notices and this permission notice appear in -all copies of the software and related documentation, and (ii) the names of -Sam Leffler and Silicon Graphics may not be used in any advertising or -publicity relating to the software without the specific, prior written -permission of Sam Leffler and Silicon Graphics. - -THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND, -EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY -WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. - -IN NO EVENT SHALL SAM LEFFLER OR SILICON GRAPHICS BE LIABLE FOR -ANY SPECIAL, INCIDENTAL, INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND, -OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, -WHETHER OR NOT ADVISED OF THE POSSIBILITY OF DAMAGE, AND ON ANY THEORY OF -LIABILITY, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE -OF THIS SOFTWARE. +Copyright (c) 1988-1997 Sam Leffler +Copyright (c) 1991-1997 Silicon Graphics, Inc. + +Permission to use, copy, modify, distribute, and sell this software and +its documentation for any purpose is hereby granted without fee, provided +that (i) the above copyright notices and this permission notice appear in +all copies of the software and related documentation, and (ii) the names of +Sam Leffler and Silicon Graphics may not be used in any advertising or +publicity relating to the software without the specific, prior written +permission of Sam Leffler and Silicon Graphics. + +THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND, +EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY +WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. + +IN NO EVENT SHALL SAM LEFFLER OR SILICON GRAPHICS BE LIABLE FOR +ANY SPECIAL, INCIDENTAL, INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER OR NOT ADVISED OF THE POSSIBILITY OF DAMAGE, AND ON ANY THEORY OF +LIABILITY, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE +OF THIS SOFTWARE. diff --git a/wheels/dependency_licenses/LIBWEBP.txt b/wheels/dependency_licenses/LIBWEBP.txt index 83e4e6f6d..872c1f141 100644 --- a/wheels/dependency_licenses/LIBWEBP.txt +++ b/wheels/dependency_licenses/LIBWEBP.txt @@ -1,29 +1,29 @@ -Copyright (c) 2010, Google Inc. 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. +Copyright (c) 2010, Google Inc. 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/OPENJPEG.txt b/wheels/dependency_licenses/OPENJPEG.txt index c41fc21d8..55b05f9ae 100644 --- a/wheels/dependency_licenses/OPENJPEG.txt +++ b/wheels/dependency_licenses/OPENJPEG.txt @@ -1,39 +1,39 @@ -* - * The copyright in this software is being made available under the 2-clauses - * BSD License, included below. This software may be subject to other third - * party and contributor rights, including patent rights, and no such rights - * are granted under this license. - * - * Copyright (c) 2002-2014, Universite catholique de Louvain (UCL), Belgium - * Copyright (c) 2002-2014, Professor Benoit Macq - * Copyright (c) 2003-2014, Antonin Descampe - * Copyright (c) 2003-2009, Francois-Olivier Devaux - * Copyright (c) 2005, Herve Drolon, FreeImage Team - * Copyright (c) 2002-2003, Yannick Verschueren - * Copyright (c) 2001-2003, David Janssens - * Copyright (c) 2011-2012, Centre National d'Etudes Spatiales (CNES), France - * Copyright (c) 2012, CS Systemes d'Information, France - * - * 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. - */ +* + * The copyright in this software is being made available under the 2-clauses + * BSD License, included below. This software may be subject to other third + * party and contributor rights, including patent rights, and no such rights + * are granted under this license. + * + * Copyright (c) 2002-2014, Universite catholique de Louvain (UCL), Belgium + * Copyright (c) 2002-2014, Professor Benoit Macq + * Copyright (c) 2003-2014, Antonin Descampe + * Copyright (c) 2003-2009, Francois-Olivier Devaux + * Copyright (c) 2005, Herve Drolon, FreeImage Team + * Copyright (c) 2002-2003, Yannick Verschueren + * Copyright (c) 2001-2003, David Janssens + * Copyright (c) 2011-2012, Centre National d'Etudes Spatiales (CNES), France + * Copyright (c) 2012, CS Systemes d'Information, France + * + * 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/RAQM.txt b/wheels/dependency_licenses/RAQM.txt index 196511ef6..adb533fc8 100644 --- a/wheels/dependency_licenses/RAQM.txt +++ b/wheels/dependency_licenses/RAQM.txt @@ -1,22 +1,22 @@ -The MIT License (MIT) - -Copyright © 2015 Information Technology Authority (ITA) -Copyright © 2016 Khaled Hosny - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +The MIT License (MIT) + +Copyright © 2015 Information Technology Authority (ITA) +Copyright © 2016 Khaled Hosny + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/wheels/dependency_licenses/XAU.txt b/wheels/dependency_licenses/XAU.txt index 64492ad80..e893a82a7 100644 --- a/wheels/dependency_licenses/XAU.txt +++ b/wheels/dependency_licenses/XAU.txt @@ -1,21 +1,21 @@ -Copyright 1988, 1993, 1994, 1998 The Open Group - -Permission to use, copy, modify, distribute, and sell this software and its -documentation for any purpose is hereby granted without fee, provided that -the above copyright notice appear in all copies and that both that -copyright notice and this permission notice appear in supporting -documentation. - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -OPEN GROUP BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN -AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -Except as contained in this notice, the name of The Open Group shall not be -used in advertising or otherwise to promote the sale, use or other dealings -in this Software without prior written authorization from The Open Group. +Copyright 1988, 1993, 1994, 1998 The Open Group + +Permission to use, copy, modify, distribute, and sell this software and its +documentation for any purpose is hereby granted without fee, provided that +the above copyright notice appear in all copies and that both that +copyright notice and this permission notice appear in supporting +documentation. + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +OPEN GROUP BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN +AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the name of The Open Group shall not be +used in advertising or otherwise to promote the sale, use or other dealings +in this Software without prior written authorization from The Open Group. diff --git a/wheels/dependency_licenses/XCB.txt b/wheels/dependency_licenses/XCB.txt index 54bfbe5b0..50a14e39e 100644 --- a/wheels/dependency_licenses/XCB.txt +++ b/wheels/dependency_licenses/XCB.txt @@ -1,30 +1,30 @@ -Copyright (C) 2001-2006 Bart Massey, Jamey Sharp, and Josh Triplett. -All Rights Reserved. - -Permission is hereby granted, free of charge, to any person -obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, -sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall -be included in all copies or substantial portions of the -Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY -KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE -WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR -PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS -BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. - -Except as contained in this notice, the names of the authors -or their institutions shall not be used in advertising or -otherwise to promote the sale, use or other dealings in this -Software without prior written authorization from the -authors. +Copyright (C) 2001-2006 Bart Massey, Jamey Sharp, and Josh Triplett. +All Rights Reserved. + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, +sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall +be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the names of the authors +or their institutions shall not be used in advertising or +otherwise to promote the sale, use or other dealings in this +Software without prior written authorization from the +authors. diff --git a/wheels/dependency_licenses/XDMCP.txt b/wheels/dependency_licenses/XDMCP.txt index 5532d143c..8fbc421a4 100644 --- a/wheels/dependency_licenses/XDMCP.txt +++ b/wheels/dependency_licenses/XDMCP.txt @@ -1,23 +1,23 @@ -Copyright 1989, 1998 The Open Group - -Permission to use, copy, modify, distribute, and sell this software and its -documentation for any purpose is hereby granted without fee, provided that -the above copyright notice appear in all copies and that both that -copyright notice and this permission notice appear in supporting -documentation. - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -OPEN GROUP BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN -AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -Except as contained in this notice, the name of The Open Group shall not be -used in advertising or otherwise to promote the sale, use or other dealings -in this Software without prior written authorization from The Open Group. - -Author: Keith Packard, MIT X Consortium +Copyright 1989, 1998 The Open Group + +Permission to use, copy, modify, distribute, and sell this software and its +documentation for any purpose is hereby granted without fee, provided that +the above copyright notice appear in all copies and that both that +copyright notice and this permission notice appear in supporting +documentation. + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +OPEN GROUP BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN +AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the name of The Open Group shall not be +used in advertising or otherwise to promote the sale, use or other dealings +in this Software without prior written authorization from The Open Group. + +Author: Keith Packard, MIT X Consortium diff --git a/wheels/dependency_licenses/ZLIB.txt b/wheels/dependency_licenses/ZLIB.txt index 84def6dc6..66b36e869 100644 --- a/wheels/dependency_licenses/ZLIB.txt +++ b/wheels/dependency_licenses/ZLIB.txt @@ -1,29 +1,29 @@ - (C) 1995-2017 Jean-loup Gailly and Mark Adler - - This software is provided 'as-is', without any express or implied - warranty. In no event will the authors be held liable for any damages - arising from the use of this software. - - Permission is granted to anyone to use this software for any purpose, - including commercial applications, and to alter it and redistribute it - freely, subject to the following restrictions: - - 1. The origin of this software must not be misrepresented; you must not - claim that you wrote the original software. If you use this software - in a product, an acknowledgment in the product documentation would be - appreciated but is not required. - 2. Altered source versions must be plainly marked as such, and must not be - misrepresented as being the original software. - 3. This notice may not be removed or altered from any source distribution. - - Jean-loup Gailly Mark Adler - jloup@gzip.org madler@alumni.caltech.edu - -If you use the zlib library in a product, we would appreciate *not* receiving -lengthy legal documents to sign. The sources are provided for free but without -warranty of any kind. The library has been entirely written by Jean-loup -Gailly and Mark Adler; it does not include third-party code. - -If you redistribute modified sources, we would appreciate that you include in -the file ChangeLog history information documenting your changes. Please read -the FAQ for more information on the distribution of modified source versions. + (C) 1995-2017 Jean-loup Gailly and Mark Adler + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + Jean-loup Gailly Mark Adler + jloup@gzip.org madler@alumni.caltech.edu + +If you use the zlib library in a product, we would appreciate *not* receiving +lengthy legal documents to sign. The sources are provided for free but without +warranty of any kind. The library has been entirely written by Jean-loup +Gailly and Mark Adler; it does not include third-party code. + +If you redistribute modified sources, we would appreciate that you include in +the file ChangeLog history information documenting your changes. Please read +the FAQ for more information on the distribution of modified source versions. diff --git a/winbuild/README.md b/winbuild/README.md index 7e81abcb0..5f4c2a048 100644 --- a/winbuild/README.md +++ b/winbuild/README.md @@ -1,29 +1,29 @@ -Quick README ------------- - -For more extensive info, see the [Windows build instructions](build.rst). - -* See [Current Windows Build/Testing process (Pillow#553)](https://github.com/python-pillow/Pillow/issues/553#issuecomment-37877416), - [Definitive docs for how to compile on Windows (matplotlib#1717)](https://github.com/matplotlib/matplotlib/issues/1717#issuecomment-13343859), - [Test Windows with GitHub Actions (Pillow#4084)](https://github.com/python-pillow/Pillow/pull/4084). - - -* Requires Microsoft Visual Studio 2017 or newer with C++ component. -* Requires NASM for libjpeg-turbo, a required dependency when using this script. -* Requires CMake 3.15 or newer (available as Visual Studio component). -* Tested on Windows Server 2016 with Visual Studio 2017 Community, and Windows Server 2019 with Visual Studio 2022 Community (AppVeyor). -* Tested on Windows Server 2022 with Visual Studio 2022 Enterprise (GitHub Actions). - -The following is a simplified version of the script used on AppVeyor: -``` -set PYTHON=C:\Python38\bin -cd /D C:\Pillow\winbuild -%PYTHON%\python.exe build_prepare.py -v --depends=C:\pillow-depends -build\build_dep_all.cmd -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 pip wheel -v -C raqm=vendor -C fribidi=vendor . -``` +Quick README +------------ + +For more extensive info, see the [Windows build instructions](build.rst). + +* See [Current Windows Build/Testing process (Pillow#553)](https://github.com/python-pillow/Pillow/issues/553#issuecomment-37877416), + [Definitive docs for how to compile on Windows (matplotlib#1717)](https://github.com/matplotlib/matplotlib/issues/1717#issuecomment-13343859), + [Test Windows with GitHub Actions (Pillow#4084)](https://github.com/python-pillow/Pillow/pull/4084). + + +* Requires Microsoft Visual Studio 2017 or newer with C++ component. +* Requires NASM for libjpeg-turbo, a required dependency when using this script. +* Requires CMake 3.15 or newer (available as Visual Studio component). +* Tested on Windows Server 2016 with Visual Studio 2017 Community, and Windows Server 2019 with Visual Studio 2022 Community (AppVeyor). +* Tested on Windows Server 2022 with Visual Studio 2022 Enterprise (GitHub Actions). + +The following is a simplified version of the script used on AppVeyor: +``` +set PYTHON=C:\Python38\bin +cd /D C:\Pillow\winbuild +%PYTHON%\python.exe build_prepare.py -v --depends=C:\pillow-depends +build\build_dep_all.cmd +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 pip wheel -v -C raqm=vendor -C fribidi=vendor . +``` diff --git a/winbuild/build.rst b/winbuild/build.rst index d0be2943e..1180c2d46 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -1,127 +1,127 @@ -Building Pillow on Windows -========================== - -.. note:: For most people, the `installation instructions - <../docs/installation.rst#windows-installation>`_ should - be sufficient. - -This page describes the steps necessary to build Pillow using the same -scripts used on GitHub Actions and AppVeyor CIs. - -Prerequisites -------------- - - -Compilers -^^^^^^^^^ - -Download and install: - -* `Microsoft Visual Studio 2017 or newer or Build Tools for Visual Studio 2017 or newer - `_ - (MSVC C++ build tools, and any Windows SDK version required) - -* `CMake 3.15 or newer `_ - (also available as Visual Studio component C++ CMake tools for Windows) - -* `Ninja `_ - (optional, use ``--nmake`` if not available; bundled in Visual Studio CMake component) - -* x86/AMD64: `Netwide Assembler (NASM) `_ - -Any version of Visual Studio 2017 or newer should be supported, -including Visual Studio 2017 Community, or Build Tools for Visual Studio 2019. - -Paths to CMake (if standalone) and NASM must be added to the ``PATH`` environment variable. -Visual Studio is found automatically with ``vswhere.exe``. - -Build configuration -------------------- - -Run ``build_prepare.py`` to configure the build:: - - usage: winbuild\build_prepare.py [-h] [-v] [-d PILLOW_BUILD] - [--depends PILLOW_DEPS] - [--architecture {x86,AMD64,ARM64}] [--nmake] - [--no-imagequant] [--no-fribidi] - - Download and generate build scripts for Pillow dependencies. - - options: - -h, --help show this help message and exit - -v, --verbose print generated scripts - -d PILLOW_BUILD, --dir PILLOW_BUILD, --build-dir PILLOW_BUILD - build directory (default: 'winbuild\build') - --depends PILLOW_DEPS - directory used to store cached dependencies (default: - 'winbuild\depends') - --architecture {x86,AMD64,ARM64} - build architecture (default: same as host Python) - --nmake build dependencies using NMake instead of Ninja - --no-imagequant skip GPL-licensed optional dependency libimagequant - --no-fribidi, --no-raqm - skip LGPL-licensed optional dependency FriBiDi - - Arguments can also be supplied using the environment variables PILLOW_BUILD, - PILLOW_DEPS, ARCHITECTURE. See winbuild\build.rst for more information. - -**Warning:** The build directory is wiped when ``build_prepare.py`` is run. - -Dependencies ------------- - -Dependencies will be automatically downloaded by ``build_prepare.py``. -By default, downloaded dependencies are stored in ``winbuild\depends``; -use the ``--depends`` argument or ``PILLOW_DEPS`` environment variable -to override this location. - -To build all dependencies, run ``winbuild\build\build_dep_all.cmd``, -or run the individual scripts in order to build each dependency separately. - -Building Pillow ---------------- - -Once the dependencies are built, make sure the required environment variables -are set by running ``winbuild\build\build_env.cmd`` and install Pillow with pip:: - - winbuild\build\build_env.cmd - python.exe -m pip install -v -C raqm=vendor -C fribidi=vendor . - -You can also install Pillow in `editable mode`_:: - - winbuild\build\build_env.cmd - python.exe -m pip install -v -C raqm=vendor -C fribidi=vendor -e . - -To build a binary wheel instead, run:: - - winbuild\build\build_env.cmd - python.exe -m pip wheel -v -C raqm=vendor -C fribidi=vendor . - -.. _editable mode: https://setuptools.pypa.io/en/stable/userguide/development_mode.html - -Testing Pillow --------------- - -Some binary dependencies (e.g. ``fribidi.dll``) will be stored in the -``winbuild\build\bin`` directory; this directory should be added to ``PATH`` -before running tests. - -Build and install Pillow, then run ``python3 -m pytest`` from the root Pillow -directory. - -Example -------- - -The following is a simplified version of the script used on AppVeyor:: - - set PYTHON=C:\Python38\bin - cd /D C:\Pillow\winbuild - %PYTHON%\python.exe build_prepare.py -v --depends C:\pillow-depends - build\build_dep_all.cmd - build\build_env.cmd - 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 pip wheel -v -C raqm=vendor -C fribidi=vendor . +Building Pillow on Windows +========================== + +.. note:: For most people, the `installation instructions + <../docs/installation.rst#windows-installation>`_ should + be sufficient. + +This page describes the steps necessary to build Pillow using the same +scripts used on GitHub Actions and AppVeyor CIs. + +Prerequisites +------------- + + +Compilers +^^^^^^^^^ + +Download and install: + +* `Microsoft Visual Studio 2017 or newer or Build Tools for Visual Studio 2017 or newer + `_ + (MSVC C++ build tools, and any Windows SDK version required) + +* `CMake 3.15 or newer `_ + (also available as Visual Studio component C++ CMake tools for Windows) + +* `Ninja `_ + (optional, use ``--nmake`` if not available; bundled in Visual Studio CMake component) + +* x86/AMD64: `Netwide Assembler (NASM) `_ + +Any version of Visual Studio 2017 or newer should be supported, +including Visual Studio 2017 Community, or Build Tools for Visual Studio 2019. + +Paths to CMake (if standalone) and NASM must be added to the ``PATH`` environment variable. +Visual Studio is found automatically with ``vswhere.exe``. + +Build configuration +------------------- + +Run ``build_prepare.py`` to configure the build:: + + usage: winbuild\build_prepare.py [-h] [-v] [-d PILLOW_BUILD] + [--depends PILLOW_DEPS] + [--architecture {x86,AMD64,ARM64}] [--nmake] + [--no-imagequant] [--no-fribidi] + + Download and generate build scripts for Pillow dependencies. + + options: + -h, --help show this help message and exit + -v, --verbose print generated scripts + -d PILLOW_BUILD, --dir PILLOW_BUILD, --build-dir PILLOW_BUILD + build directory (default: 'winbuild\build') + --depends PILLOW_DEPS + directory used to store cached dependencies (default: + 'winbuild\depends') + --architecture {x86,AMD64,ARM64} + build architecture (default: same as host Python) + --nmake build dependencies using NMake instead of Ninja + --no-imagequant skip GPL-licensed optional dependency libimagequant + --no-fribidi, --no-raqm + skip LGPL-licensed optional dependency FriBiDi + + Arguments can also be supplied using the environment variables PILLOW_BUILD, + PILLOW_DEPS, ARCHITECTURE. See winbuild\build.rst for more information. + +**Warning:** The build directory is wiped when ``build_prepare.py`` is run. + +Dependencies +------------ + +Dependencies will be automatically downloaded by ``build_prepare.py``. +By default, downloaded dependencies are stored in ``winbuild\depends``; +use the ``--depends`` argument or ``PILLOW_DEPS`` environment variable +to override this location. + +To build all dependencies, run ``winbuild\build\build_dep_all.cmd``, +or run the individual scripts in order to build each dependency separately. + +Building Pillow +--------------- + +Once the dependencies are built, make sure the required environment variables +are set by running ``winbuild\build\build_env.cmd`` and install Pillow with pip:: + + winbuild\build\build_env.cmd + python.exe -m pip install -v -C raqm=vendor -C fribidi=vendor . + +You can also install Pillow in `editable mode`_:: + + winbuild\build\build_env.cmd + python.exe -m pip install -v -C raqm=vendor -C fribidi=vendor -e . + +To build a binary wheel instead, run:: + + winbuild\build\build_env.cmd + python.exe -m pip wheel -v -C raqm=vendor -C fribidi=vendor . + +.. _editable mode: https://setuptools.pypa.io/en/stable/userguide/development_mode.html + +Testing Pillow +-------------- + +Some binary dependencies (e.g. ``fribidi.dll``) will be stored in the +``winbuild\build\bin`` directory; this directory should be added to ``PATH`` +before running tests. + +Build and install Pillow, then run ``python3 -m pytest`` from the root Pillow +directory. + +Example +------- + +The following is a simplified version of the script used on AppVeyor:: + + set PYTHON=C:\Python38\bin + cd /D C:\Pillow\winbuild + %PYTHON%\python.exe build_prepare.py -v --depends C:\pillow-depends + build\build_dep_all.cmd + build\build_env.cmd + 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 pip wheel -v -C raqm=vendor -C fribidi=vendor . diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 9837589b2..cba4621b9 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -1,774 +1,774 @@ -from __future__ import annotations - -import argparse -import os -import platform -import re -import shutil -import struct -import subprocess - - -def cmd_cd(path: str) -> str: - return f"cd /D {path}" - - -def cmd_set(name: str, value: str) -> str: - return f"set {name}={value}" - - -def cmd_append(name: str, value: str) -> str: - op = "path " if name == "PATH" else f"set {name}=" - return op + f"%{name}%;{value}" - - -def cmd_copy(src: str, tgt: str) -> str: - return f'copy /Y /B "{src}" "{tgt}"' - - -def cmd_xcopy(src: str, tgt: str) -> str: - return f'xcopy /Y /E "{src}" "{tgt}"' - - -def cmd_mkdir(path: str) -> str: - return f'mkdir "{path}"' - - -def cmd_rmdir(path: str) -> str: - return f'rmdir /S /Q "{path}"' - - -def cmd_nmake( - makefile: str | None = None, - target: str = "", - params: list[str] | None = None, -) -> str: - params = "" if params is None else " ".join(params) - - return " ".join( - [ - "{nmake}", - "-nologo", - f'-f "{makefile}"' if makefile is not None else "", - f"{params}", - f'"{target}"', - ] - ) - - -def cmds_cmake( - target: str | tuple[str, ...] | list[str], *params, build_dir: str = "." -) -> list[str]: - if not isinstance(target, str): - target = " ".join(target) - - return [ - " ".join( - [ - "{cmake}", - "-DCMAKE_BUILD_TYPE=Release", - "-DCMAKE_VERBOSE_MAKEFILE=ON", - "-DCMAKE_RULE_MESSAGES:BOOL=OFF", # for NMake - "-DCMAKE_C_COMPILER=cl.exe", # for Ninja - "-DCMAKE_CXX_COMPILER=cl.exe", # for Ninja - "-DCMAKE_C_FLAGS=-nologo", - "-DCMAKE_CXX_FLAGS=-nologo", - *params, - '-G "{cmake_generator}"', - f'-B "{build_dir}"', - "-S .", - ] - ), - f'{{cmake}} --build "{build_dir}" --clean-first --parallel --target {target}', - ] - - -def cmd_msbuild( - file: str, - configuration: str = "Release", - target: str = "Build", - plat: str = "{msbuild_arch}", -) -> str: - return " ".join( - [ - "{msbuild}", - f"{file}", - f'/t:"{target}"', - f'/p:Configuration="{configuration}"', - f"/p:Platform={plat}", - "/m", - ] - ) - - -SF_PROJECTS = "https://sourceforge.net/projects" - -ARCHITECTURES = { - "x86": {"vcvars_arch": "x86", "msbuild_arch": "Win32"}, - "AMD64": {"vcvars_arch": "x86_amd64", "msbuild_arch": "x64"}, - "ARM64": {"vcvars_arch": "x86_arm64", "msbuild_arch": "ARM64"}, -} - -V = { - "BROTLI": "1.1.0", - "FREETYPE": "2.13.2", - "FRIBIDI": "1.0.15", - "HARFBUZZ": "8.5.0", - "JPEGTURBO": "3.0.3", - "LCMS2": "2.16", - "LIBPNG": "1.6.43", - "LIBWEBP": "1.4.0", - "OPENJPEG": "2.5.2", - "TIFF": "4.6.0", - "XZ": "5.4.5", - "ZLIB": "1.3.1", -} -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 = { - "libjpeg": { - "url": f"{SF_PROJECTS}/libjpeg-turbo/files/{V['JPEGTURBO']}/" - f"libjpeg-turbo-{V['JPEGTURBO']}.tar.gz/download", - "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==========" - ".+(libjpeg-turbo Licenses\n======================\n\n.+)$" - ), - "patch": { - r"CMakeLists.txt": { - # libjpeg-turbo does not detect MSVC x86_arm64 cross-compiler correctly - 'if(MSVC_IDE AND CMAKE_GENERATOR_PLATFORM MATCHES "arm64")': "if({architecture} STREQUAL ARM64)", # noqa: E501 - }, - }, - "build": [ - *cmds_cmake( - ("jpeg-static", "cjpeg-static", "djpeg-static"), - "-DENABLE_SHARED:BOOL=FALSE", - "-DWITH_JPEG8:BOOL=TRUE", - "-DWITH_CRT_DLL:BOOL=TRUE", - ), - cmd_copy("jpeg-static.lib", "libjpeg.lib"), - cmd_copy("cjpeg-static.exe", "cjpeg.exe"), - cmd_copy("djpeg-static.exe", "djpeg.exe"), - ], - "headers": ["j*.h"], - "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(.+)$", - "build": [ - cmd_nmake(r"win32\Makefile.msc", "clean"), - cmd_nmake(r"win32\Makefile.msc", "zlib.lib"), - cmd_copy("zlib.lib", "z.lib"), - ], - "headers": [r"z*.h"], - "libs": [r"*.lib"], - }, - "xz": { - "url": f"{SF_PROJECTS}/lzmautils/files/xz-{V['XZ']}.tar.gz/download", - "filename": f"xz-{V['XZ']}.tar.gz", - "dir": f"xz-{V['XZ']}", - "license": "COPYING", - "build": [ - *cmds_cmake("liblzma", "-DBUILD_SHARED_LIBS:BOOL=OFF"), - cmd_mkdir(r"{inc_dir}\lzma"), - cmd_copy(r"src\liblzma\api\lzma\*.h", r"{inc_dir}\lzma"), - ], - "headers": [r"src\liblzma\api\lzma.h"], - "libs": [r"liblzma.lib"], - }, - "libwebp": { - "url": f"http://downloads.webmproject.org/releases/webp/libwebp-{V['LIBWEBP']}.tar.gz", - "filename": f"libwebp-{V['LIBWEBP']}.tar.gz", - "dir": f"libwebp-{V['LIBWEBP']}", - "license": "COPYING", - "patch": { - r"src\enc\picture_csp_enc.c": { - # link against libsharpyuv.lib - '#include "sharpyuv/sharpyuv.h"': '#include "sharpyuv/sharpyuv.h"\n#pragma comment(lib, "libsharpyuv.lib")', # noqa: E501 - } - }, - "build": [ - *cmds_cmake( - "webp webpdemux webpmux", - "-DBUILD_SHARED_LIBS:BOOL=OFF", - "-DWEBP_LINK_STATIC:BOOL=OFF", - ), - cmd_mkdir(r"{inc_dir}\webp"), - cmd_copy(r"src\webp\*.h", r"{inc_dir}\webp"), - ], - "libs": [r"libsharpyuv.lib", r"libwebp*.lib"], - }, - "libtiff": { - "url": f"https://download.osgeo.org/libtiff/tiff-{V['TIFF']}.tar.gz", - "filename": f"tiff-{V['TIFF']}.tar.gz", - "dir": f"tiff-{V['TIFF']}", - "license": "LICENSE.md", - "patch": { - r"libtiff\tif_lzma.c": { - # link against liblzma.lib - "#ifdef LZMA_SUPPORT": '#ifdef LZMA_SUPPORT\n#pragma comment(lib, "liblzma.lib")', # noqa: E501 - }, - r"libtiff\tif_webp.c": { - # link against libwebp.lib - "#ifdef WEBP_SUPPORT": '#ifdef WEBP_SUPPORT\n#pragma comment(lib, "libwebp.lib")', # noqa: E501 - }, - r"test\CMakeLists.txt": { - "add_executable(test_write_read_tags ../placeholder.h)": "", - "target_sources(test_write_read_tags PRIVATE test_write_read_tags.c)": "", # noqa: E501 - "target_link_libraries(test_write_read_tags PRIVATE tiff)": "", - "list(APPEND simple_tests test_write_read_tags)": "", - }, - }, - "build": [ - *cmds_cmake( - "tiff", - "-DBUILD_SHARED_LIBS:BOOL=OFF", - "-DWebP_LIBRARY=libwebp", - '-DCMAKE_C_FLAGS="-nologo -DLZMA_API_STATIC"', - ) - ], - "headers": [r"libtiff\tiff*.h"], - "libs": [r"libtiff\*.lib"], - }, - "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']}", - "license": "LICENSE", - "build": [ - *cmds_cmake("png_static", "-DPNG_SHARED:BOOL=OFF", "-DPNG_TESTS:BOOL=OFF"), - cmd_copy( - f"libpng{V['LIBPNG_XY']}_static.lib", f"libpng{V['LIBPNG_XY']}.lib" - ), - ], - "headers": [r"png*.h"], - "libs": [f"libpng{V['LIBPNG_XY']}.lib"], - }, - "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"), - cmd_xcopy(r"c\include", "{inc_dir}"), - ], - "libs": ["*.lib"], - }, - "freetype": { - "url": f"https://download.savannah.gnu.org/releases/freetype/freetype-{V['FREETYPE']}.tar.gz", - "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": { - # freetype setting is /MD for .dll and /MT for .lib, we need /MD - "MultiThreaded": "MultiThreadedDLL", # noqa: E501 - # freetype doesn't specify SDK version, MSBuild may guess incorrectly - '': '\n $(WindowsSDKVersion)', # noqa: E501 - }, - r"builds\windows\vc2010\freetype.user.props": { - "": "FT_CONFIG_OPTION_SYSTEM_ZLIB;FT_CONFIG_OPTION_USE_PNG;FT_CONFIG_OPTION_USE_HARFBUZZ;FT_CONFIG_OPTION_USE_BROTLI", # noqa: E501 - "": r"{dir_harfbuzz}\src;{inc_dir}", # noqa: E501 - "": "{lib_dir}", # noqa: E501 - "": f"zlib.lib;libpng{V['LIBPNG_XY']}.lib;brotlicommon.lib;brotlidec.lib", # noqa: E501 - }, - r"src/autofit/afshaper.c": { - # link against harfbuzz.lib - "#ifdef FT_CONFIG_OPTION_USE_HARFBUZZ": '#ifdef FT_CONFIG_OPTION_USE_HARFBUZZ\n#pragma comment(lib, "harfbuzz.lib")', # noqa: E501 - }, - }, - "build": [ - cmd_rmdir("objs"), - cmd_msbuild( - r"builds\windows\vc2010\freetype.sln", "Release Static", "Clean" - ), - cmd_msbuild( - r"builds\windows\vc2010\freetype.sln", "Release Static", "Build" - ), - cmd_xcopy("include", "{inc_dir}"), - ], - "libs": [r"objs\{msbuild_arch}\Release Static\freetype.lib"], - }, - "lcms2": { - "url": f"{SF_PROJECTS}/lcms/files/lcms/{V['LCMS2']}/lcms2-{V['LCMS2']}.tar.gz/download", # noqa: E501 - "filename": f"lcms2-{V['LCMS2']}.tar.gz", - "dir": f"lcms2-{V['LCMS2']}", - "license": "LICENSE", - "patch": { - r"Projects\VC2022\lcms2_static\lcms2_static.vcxproj": { - # default is /MD for x86 and /MT for x64, we need /MD always - "MultiThreaded": "MultiThreadedDLL", # noqa: E501 - # retarget to default toolset (selected by vcvarsall.bat) - "v143": "$(DefaultPlatformToolset)", # noqa: E501 - # retarget to latest (selected by vcvarsall.bat) - "10.0": "$(WindowsSDKVersion)", # noqa: E501 - } - }, - "build": [ - cmd_rmdir("Lib"), - cmd_rmdir(r"Projects\VC2022\Release"), - cmd_msbuild(r"Projects\VC2022\lcms2.sln", "Release", "Clean"), - cmd_msbuild( - r"Projects\VC2022\lcms2.sln", "Release", "lcms2_static:Rebuild" - ), - cmd_xcopy("include", "{inc_dir}"), - ], - "libs": [r"Lib\MS\*.lib"], - }, - "openjpeg": { - "url": 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( - "openjp2", "-DBUILD_CODEC:BOOL=OFF", "-DBUILD_SHARED_LIBS:BOOL=OFF" - ), - cmd_mkdir(rf"{{inc_dir}}\openjpeg-{V['OPENJPEG']}"), - cmd_copy(r"src\lib\openjp2\*.h", rf"{{inc_dir}}\openjpeg-{V['OPENJPEG']}"), - ], - "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", - "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"), - ], - "headers": [r"*.h"], - "libs": [r"imagequant.lib"], - }, - "harfbuzz": { - "url": f"https://github.com/harfbuzz/harfbuzz/archive/{V['HARFBUZZ']}.zip", - "filename": f"harfbuzz-{V['HARFBUZZ']}.zip", - "dir": f"harfbuzz-{V['HARFBUZZ']}", - "license": "COPYING", - "build": [ - *cmds_cmake( - "harfbuzz", - "-DHB_HAVE_FREETYPE:BOOL=TRUE", - '-DCMAKE_CXX_FLAGS="-nologo -d2FH4-"', - ), - ], - "headers": [r"src\*.h"], - "libs": [r"*.lib"], - }, - "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"), - cmd_copy(r"{winbuild_dir}\fribidi.cmake", r"CMakeLists.txt"), - # generated tab.i files cannot be cross-compiled - " ^&^& ".join( - [ - "if {architecture}==ARM64 cmd /c call {vcvarsall} x86", - *cmds_cmake("fribidi-gen", "-DARCH=x86", build_dir="build_x86"), - ] - ), - *cmds_cmake("fribidi", "-DARCH={architecture}"), - ], - "bins": [r"*.dll"], - }, -} - - -# based on distutils._msvccompiler from CPython 3.7.4 -def find_msvs(architecture: str) -> dict[str, str] | None: - root = os.environ.get("ProgramFiles(x86)") or os.environ.get("ProgramFiles") - if not root: - print("Program Files not found") - return None - - requires = ["-requires", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64"] - if architecture == "ARM64": - requires += ["-requires", "Microsoft.VisualStudio.Component.VC.Tools.ARM64"] - - try: - vspath = ( - subprocess.check_output( - [ - os.path.join( - root, "Microsoft Visual Studio", "Installer", "vswhere.exe" - ), - "-latest", - "-prerelease", - *requires, - "-property", - "installationPath", - "-products", - "*", - ] - ) - .decode(encoding="mbcs") - .strip() - ) - except (subprocess.CalledProcessError, OSError, UnicodeDecodeError): - print("vswhere not found") - return None - - if not os.path.isdir(os.path.join(vspath, "VC", "Auxiliary", "Build")): - print("Visual Studio seems to be missing C compiler") - return None - - # vs2017 - msbuild = os.path.join(vspath, "MSBuild", "15.0", "Bin", "MSBuild.exe") - if not os.path.isfile(msbuild): - # vs2019 - msbuild = os.path.join(vspath, "MSBuild", "Current", "Bin", "MSBuild.exe") - if not os.path.isfile(msbuild): - print("Visual Studio MSBuild not found") - return None - - vcvarsall = os.path.join(vspath, "VC", "Auxiliary", "Build", "vcvarsall.bat") - if not os.path.isfile(vcvarsall): - print("Visual Studio vcvarsall not found") - return None - - return { - "vs_dir": vspath, - "msbuild": f'"{msbuild}"', - "vcvarsall": f'"{vcvarsall}"', - "nmake": "nmake.exe", # nmake selected by vcvarsall - } - - -def download_dep(url: str, file: str) -> None: - import urllib.error - import urllib.request - - ex = None - for i in range(3): - try: - print(f"Fetching {url} (attempt {i + 1})...") - content = urllib.request.urlopen(url).read() - with open(file, "wb") as f: - f.write(content) - break - except urllib.error.URLError as e: - ex = e - else: - raise RuntimeError(ex) - - -def extract_dep(url: str, filename: str, prefs: dict[str, str]) -> None: - import tarfile - import zipfile - - depends_dir = prefs["depends_dir"] - sources_dir = prefs["src_dir"] - - file = os.path.join(depends_dir, filename) - if not os.path.exists(file): - # First try our mirror - mirror_url = ( - f"https://raw.githubusercontent.com/" - f"python-pillow/pillow-depends/main/{filename}" - ) - try: - download_dep(mirror_url, file) - except RuntimeError as exc: - # Otherwise try upstream - print(exc) - download_dep(url, file) - - print("Extracting " + filename) - sources_dir_abs = os.path.abspath(sources_dir) - if filename.endswith(".zip"): - with zipfile.ZipFile(file) as zf: - for member in zf.namelist(): - member_abspath = os.path.abspath(os.path.join(sources_dir, member)) - member_prefix = os.path.commonpath([sources_dir_abs, member_abspath]) - if sources_dir_abs != member_prefix: - msg = "Attempted Path Traversal in Zip File" - raise RuntimeError(msg) - zf.extractall(sources_dir) - elif filename.endswith((".tar.gz", ".tgz")): - with tarfile.open(file, "r:gz") as tgz: - for member in tgz.getnames(): - member_abspath = os.path.abspath(os.path.join(sources_dir, member)) - member_prefix = os.path.commonpath([sources_dir_abs, member_abspath]) - if sources_dir_abs != member_prefix: - msg = "Attempted Path Traversal in Tar File" - raise RuntimeError(msg) - tgz.extractall(sources_dir) - else: - msg = "Unknown archive type: " + filename - raise RuntimeError(msg) - - -def write_script( - name: str, lines: list[str], prefs: dict[str, str], verbose: bool -) -> None: - name = os.path.join(prefs["build_dir"], name) - lines = [line.format(**prefs) for line in lines] - print("Writing " + name) - with open(name, "w", newline="") as f: - f.write(os.linesep.join(lines)) - if verbose: - for line in lines: - print(" " + line) - - -def get_footer(dep: dict) -> list[str]: - lines = [] - for out in dep.get("headers", []): - lines.append(cmd_copy(out, "{inc_dir}")) - for out in dep.get("libs", []): - lines.append(cmd_copy(out, "{lib_dir}")) - for out in dep.get("bins", []): - lines.append(cmd_copy(out, "{bin_dir}")) - return lines - - -def build_env(prefs: dict[str, str], verbose: bool) -> None: - lines = [ - "if defined DISTUTILS_USE_SDK goto end", - cmd_set("INCLUDE", "{inc_dir}"), - cmd_set("INCLIB", "{lib_dir}"), - cmd_set("LIB", "{lib_dir}"), - cmd_append("PATH", "{bin_dir}"), - "call {vcvarsall} {vcvars_arch}", - cmd_set("DISTUTILS_USE_SDK", "1"), # use same compiler to build Pillow - cmd_set("py_vcruntime_redist", "true"), # always use /MD, never /MT - ":end", - "@echo on", - ] - write_script("build_env.cmd", lines, prefs, verbose) - - -def build_dep(name: str, prefs: dict[str, str], verbose: bool) -> str: - dep = DEPS[name] - directory = dep["dir"] - file = f"build_dep_{name}.cmd" - license_dir = prefs["license_dir"] - sources_dir = prefs["src_dir"] - - extract_dep(dep["url"], dep["filename"], prefs) - - licenses = dep["license"] - if isinstance(licenses, str): - licenses = [licenses] - license_text = "" - for license_file in licenses: - with open(os.path.join(sources_dir, directory, license_file)) as f: - license_text += f.read() - if "license_pattern" in dep: - match = re.search(dep["license_pattern"], license_text, re.DOTALL) - license_text = "\n".join(match.groups()) - assert len(license_text) > 50 - with open(os.path.join(license_dir, f"{directory}.txt"), "w") as f: - print(f"Writing license {directory}.txt") - f.write(license_text) - - for patch_file, patch_list in dep.get("patch", {}).items(): - patch_file = os.path.join(sources_dir, directory, patch_file.format(**prefs)) - with open(patch_file) as f: - text = f.read() - for patch_from, patch_to in patch_list.items(): - patch_from = patch_from.format(**prefs) - patch_to = patch_to.format(**prefs) - assert patch_from in text - text = text.replace(patch_from, patch_to) - with open(patch_file, "w") as f: - print(f"Patching {patch_file}") - f.write(text) - - banner = f"Building {name} ({directory})" - lines = [ - r'call "{build_dir}\build_env.cmd"', - "@echo " + ("=" * 70), - f"@echo ==== {banner:<60} ====", - "@echo " + ("=" * 70), - cmd_cd(os.path.join(sources_dir, directory)), - *dep.get("build", []), - *get_footer(dep), - ] - - write_script(file, lines, prefs, verbose) - return file - - -def build_dep_all(disabled: list[str], prefs: dict[str, str], verbose: bool) -> None: - lines = [r'call "{build_dir}\build_env.cmd"'] - gha_groups = "GITHUB_ACTIONS" in os.environ - for dep_name in DEPS: - print() - if dep_name in disabled: - print(f"Skipping disabled dependency {dep_name}") - continue - script = build_dep(dep_name, prefs, verbose) - if gha_groups: - lines.append(f"@echo ::group::Running {script}") - lines.append(rf'cmd.exe /c "{{build_dir}}\{script}"') - lines.append("if errorlevel 1 echo Build failed! && exit /B 1") - if gha_groups: - lines.append("@echo ::endgroup::") - print() - lines.append("@echo All Pillow dependencies built successfully!") - write_script("build_dep_all.cmd", lines, prefs, verbose) - - -def main() -> None: - winbuild_dir = os.path.dirname(os.path.realpath(__file__)) - - parser = argparse.ArgumentParser( - prog="winbuild\\build_prepare.py", - description="Download and generate build scripts for Pillow dependencies.", - epilog="""Arguments can also be supplied using the environment variables - PILLOW_BUILD, PILLOW_DEPS, ARCHITECTURE. See winbuild\\build.rst - for more information.""", - ) - parser.add_argument( - "-v", "--verbose", action="store_true", help="print generated scripts" - ) - parser.add_argument( - "-d", - "--dir", - "--build-dir", - dest="build_dir", - metavar="PILLOW_BUILD", - default=os.environ.get("PILLOW_BUILD", os.path.join(winbuild_dir, "build")), - help="build directory (default: 'winbuild\\build')", - ) - parser.add_argument( - "--depends", - dest="depends_dir", - metavar="PILLOW_DEPS", - default=os.environ.get("PILLOW_DEPS", os.path.join(winbuild_dir, "depends")), - help="directory used to store cached dependencies " - "(default: 'winbuild\\depends')", - ) - parser.add_argument( - "--architecture", - choices=ARCHITECTURES, - default=os.environ.get( - "ARCHITECTURE", - ( - "ARM64" - if platform.machine() == "ARM64" - else ("x86" if struct.calcsize("P") == 4 else "AMD64") - ), - ), - help="build architecture (default: same as host Python)", - ) - parser.add_argument( - "--nmake", - dest="cmake_generator", - action="store_const", - const="NMake Makefiles", - default="Ninja", - help="build dependencies using NMake instead of Ninja", - ) - parser.add_argument( - "--no-imagequant", - action="store_true", - help="skip GPL-licensed optional dependency libimagequant", - ) - parser.add_argument( - "--no-fribidi", - "--no-raqm", - action="store_true", - help="skip LGPL-licensed optional dependency FriBiDi", - ) - args = parser.parse_args() - - arch_prefs = ARCHITECTURES[args.architecture] - print("Target architecture:", args.architecture) - - msvs = find_msvs(args.architecture) - if msvs is None: - msg = "Visual Studio not found. Please install Visual Studio 2017 or newer." - raise RuntimeError(msg) - print("Found Visual Studio at:", msvs["vs_dir"]) - - # dependency cache directory - args.depends_dir = os.path.abspath(args.depends_dir) - os.makedirs(args.depends_dir, exist_ok=True) - print("Caching dependencies in:", args.depends_dir) - - args.build_dir = os.path.abspath(args.build_dir) - print("Using output directory:", args.build_dir) - - # build directory for *.h files - inc_dir = os.path.join(args.build_dir, "inc") - # build directory for *.lib files - lib_dir = os.path.join(args.build_dir, "lib") - # build directory for *.bin files - bin_dir = os.path.join(args.build_dir, "bin") - # directory for storing project files - sources_dir = os.path.join(args.build_dir, "src") - # copy dependency licenses to this directory - license_dir = os.path.join(args.build_dir, "license") - - shutil.rmtree(args.build_dir, ignore_errors=True) - os.makedirs(args.build_dir, exist_ok=False) - for path in [inc_dir, lib_dir, bin_dir, sources_dir, license_dir]: - os.makedirs(path, exist_ok=True) - - disabled = [] - if args.no_imagequant: - disabled += ["libimagequant"] - if args.no_fribidi: - disabled += ["fribidi"] - - prefs = { - "architecture": args.architecture, - **arch_prefs, - # Pillow paths - "winbuild_dir": winbuild_dir, - # Build paths - "bin_dir": bin_dir, - "build_dir": args.build_dir, - "depends_dir": args.depends_dir, - "inc_dir": inc_dir, - "lib_dir": lib_dir, - "license_dir": license_dir, - "src_dir": sources_dir, - # Compilers / Tools - **msvs, - "cmake": "cmake.exe", # TODO find CMAKE automatically - "cmake_generator": args.cmake_generator, - # TODO find NASM automatically - } - - for k, v in DEPS.items(): - prefs[f"dir_{k}"] = os.path.join(sources_dir, v["dir"]) - - print() - - write_script(".gitignore", ["*"], prefs, args.verbose) - build_env(prefs, args.verbose) - build_dep_all(disabled, prefs, args.verbose) - - -if __name__ == "__main__": - main() +from __future__ import annotations + +import argparse +import os +import platform +import re +import shutil +import struct +import subprocess + + +def cmd_cd(path: str) -> str: + return f"cd /D {path}" + + +def cmd_set(name: str, value: str) -> str: + return f"set {name}={value}" + + +def cmd_append(name: str, value: str) -> str: + op = "path " if name == "PATH" else f"set {name}=" + return op + f"%{name}%;{value}" + + +def cmd_copy(src: str, tgt: str) -> str: + return f'copy /Y /B "{src}" "{tgt}"' + + +def cmd_xcopy(src: str, tgt: str) -> str: + return f'xcopy /Y /E "{src}" "{tgt}"' + + +def cmd_mkdir(path: str) -> str: + return f'mkdir "{path}"' + + +def cmd_rmdir(path: str) -> str: + return f'rmdir /S /Q "{path}"' + + +def cmd_nmake( + makefile: str | None = None, + target: str = "", + params: list[str] | None = None, +) -> str: + params = "" if params is None else " ".join(params) + + return " ".join( + [ + "{nmake}", + "-nologo", + f'-f "{makefile}"' if makefile is not None else "", + f"{params}", + f'"{target}"', + ] + ) + + +def cmds_cmake( + target: str | tuple[str, ...] | list[str], *params, build_dir: str = "." +) -> list[str]: + if not isinstance(target, str): + target = " ".join(target) + + return [ + " ".join( + [ + "{cmake}", + "-DCMAKE_BUILD_TYPE=Release", + "-DCMAKE_VERBOSE_MAKEFILE=ON", + "-DCMAKE_RULE_MESSAGES:BOOL=OFF", # for NMake + "-DCMAKE_C_COMPILER=cl.exe", # for Ninja + "-DCMAKE_CXX_COMPILER=cl.exe", # for Ninja + "-DCMAKE_C_FLAGS=-nologo", + "-DCMAKE_CXX_FLAGS=-nologo", + *params, + '-G "{cmake_generator}"', + f'-B "{build_dir}"', + "-S .", + ] + ), + f'{{cmake}} --build "{build_dir}" --clean-first --parallel --target {target}', + ] + + +def cmd_msbuild( + file: str, + configuration: str = "Release", + target: str = "Build", + plat: str = "{msbuild_arch}", +) -> str: + return " ".join( + [ + "{msbuild}", + f"{file}", + f'/t:"{target}"', + f'/p:Configuration="{configuration}"', + f"/p:Platform={plat}", + "/m", + ] + ) + + +SF_PROJECTS = "https://sourceforge.net/projects" + +ARCHITECTURES = { + "x86": {"vcvars_arch": "x86", "msbuild_arch": "Win32"}, + "AMD64": {"vcvars_arch": "x86_amd64", "msbuild_arch": "x64"}, + "ARM64": {"vcvars_arch": "x86_arm64", "msbuild_arch": "ARM64"}, +} + +V = { + "BROTLI": "1.1.0", + "FREETYPE": "2.13.2", + "FRIBIDI": "1.0.15", + "HARFBUZZ": "8.5.0", + "JPEGTURBO": "3.0.3", + "LCMS2": "2.16", + "LIBPNG": "1.6.43", + "LIBWEBP": "1.4.0", + "OPENJPEG": "2.5.2", + "TIFF": "4.6.0", + "XZ": "5.4.5", + "ZLIB": "1.3.1", +} +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 = { + "libjpeg": { + "url": f"{SF_PROJECTS}/libjpeg-turbo/files/{V['JPEGTURBO']}/" + f"libjpeg-turbo-{V['JPEGTURBO']}.tar.gz/download", + "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==========" + ".+(libjpeg-turbo Licenses\n======================\n\n.+)$" + ), + "patch": { + r"CMakeLists.txt": { + # libjpeg-turbo does not detect MSVC x86_arm64 cross-compiler correctly + 'if(MSVC_IDE AND CMAKE_GENERATOR_PLATFORM MATCHES "arm64")': "if({architecture} STREQUAL ARM64)", # noqa: E501 + }, + }, + "build": [ + *cmds_cmake( + ("jpeg-static", "cjpeg-static", "djpeg-static"), + "-DENABLE_SHARED:BOOL=FALSE", + "-DWITH_JPEG8:BOOL=TRUE", + "-DWITH_CRT_DLL:BOOL=TRUE", + ), + cmd_copy("jpeg-static.lib", "libjpeg.lib"), + cmd_copy("cjpeg-static.exe", "cjpeg.exe"), + cmd_copy("djpeg-static.exe", "djpeg.exe"), + ], + "headers": ["j*.h"], + "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(.+)$", + "build": [ + cmd_nmake(r"win32\Makefile.msc", "clean"), + cmd_nmake(r"win32\Makefile.msc", "zlib.lib"), + cmd_copy("zlib.lib", "z.lib"), + ], + "headers": [r"z*.h"], + "libs": [r"*.lib"], + }, + "xz": { + "url": f"{SF_PROJECTS}/lzmautils/files/xz-{V['XZ']}.tar.gz/download", + "filename": f"xz-{V['XZ']}.tar.gz", + "dir": f"xz-{V['XZ']}", + "license": "COPYING", + "build": [ + *cmds_cmake("liblzma", "-DBUILD_SHARED_LIBS:BOOL=OFF"), + cmd_mkdir(r"{inc_dir}\lzma"), + cmd_copy(r"src\liblzma\api\lzma\*.h", r"{inc_dir}\lzma"), + ], + "headers": [r"src\liblzma\api\lzma.h"], + "libs": [r"liblzma.lib"], + }, + "libwebp": { + "url": f"http://downloads.webmproject.org/releases/webp/libwebp-{V['LIBWEBP']}.tar.gz", + "filename": f"libwebp-{V['LIBWEBP']}.tar.gz", + "dir": f"libwebp-{V['LIBWEBP']}", + "license": "COPYING", + "patch": { + r"src\enc\picture_csp_enc.c": { + # link against libsharpyuv.lib + '#include "sharpyuv/sharpyuv.h"': '#include "sharpyuv/sharpyuv.h"\n#pragma comment(lib, "libsharpyuv.lib")', # noqa: E501 + } + }, + "build": [ + *cmds_cmake( + "webp webpdemux webpmux", + "-DBUILD_SHARED_LIBS:BOOL=OFF", + "-DWEBP_LINK_STATIC:BOOL=OFF", + ), + cmd_mkdir(r"{inc_dir}\webp"), + cmd_copy(r"src\webp\*.h", r"{inc_dir}\webp"), + ], + "libs": [r"libsharpyuv.lib", r"libwebp*.lib"], + }, + "libtiff": { + "url": f"https://download.osgeo.org/libtiff/tiff-{V['TIFF']}.tar.gz", + "filename": f"tiff-{V['TIFF']}.tar.gz", + "dir": f"tiff-{V['TIFF']}", + "license": "LICENSE.md", + "patch": { + r"libtiff\tif_lzma.c": { + # link against liblzma.lib + "#ifdef LZMA_SUPPORT": '#ifdef LZMA_SUPPORT\n#pragma comment(lib, "liblzma.lib")', # noqa: E501 + }, + r"libtiff\tif_webp.c": { + # link against libwebp.lib + "#ifdef WEBP_SUPPORT": '#ifdef WEBP_SUPPORT\n#pragma comment(lib, "libwebp.lib")', # noqa: E501 + }, + r"test\CMakeLists.txt": { + "add_executable(test_write_read_tags ../placeholder.h)": "", + "target_sources(test_write_read_tags PRIVATE test_write_read_tags.c)": "", # noqa: E501 + "target_link_libraries(test_write_read_tags PRIVATE tiff)": "", + "list(APPEND simple_tests test_write_read_tags)": "", + }, + }, + "build": [ + *cmds_cmake( + "tiff", + "-DBUILD_SHARED_LIBS:BOOL=OFF", + "-DWebP_LIBRARY=libwebp", + '-DCMAKE_C_FLAGS="-nologo -DLZMA_API_STATIC"', + ) + ], + "headers": [r"libtiff\tiff*.h"], + "libs": [r"libtiff\*.lib"], + }, + "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']}", + "license": "LICENSE", + "build": [ + *cmds_cmake("png_static", "-DPNG_SHARED:BOOL=OFF", "-DPNG_TESTS:BOOL=OFF"), + cmd_copy( + f"libpng{V['LIBPNG_XY']}_static.lib", f"libpng{V['LIBPNG_XY']}.lib" + ), + ], + "headers": [r"png*.h"], + "libs": [f"libpng{V['LIBPNG_XY']}.lib"], + }, + "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"), + cmd_xcopy(r"c\include", "{inc_dir}"), + ], + "libs": ["*.lib"], + }, + "freetype": { + "url": f"https://download.savannah.gnu.org/releases/freetype/freetype-{V['FREETYPE']}.tar.gz", + "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": { + # freetype setting is /MD for .dll and /MT for .lib, we need /MD + "MultiThreaded": "MultiThreadedDLL", # noqa: E501 + # freetype doesn't specify SDK version, MSBuild may guess incorrectly + '': '\n $(WindowsSDKVersion)', # noqa: E501 + }, + r"builds\windows\vc2010\freetype.user.props": { + "": "FT_CONFIG_OPTION_SYSTEM_ZLIB;FT_CONFIG_OPTION_USE_PNG;FT_CONFIG_OPTION_USE_HARFBUZZ;FT_CONFIG_OPTION_USE_BROTLI", # noqa: E501 + "": r"{dir_harfbuzz}\src;{inc_dir}", # noqa: E501 + "": "{lib_dir}", # noqa: E501 + "": f"zlib.lib;libpng{V['LIBPNG_XY']}.lib;brotlicommon.lib;brotlidec.lib", # noqa: E501 + }, + r"src/autofit/afshaper.c": { + # link against harfbuzz.lib + "#ifdef FT_CONFIG_OPTION_USE_HARFBUZZ": '#ifdef FT_CONFIG_OPTION_USE_HARFBUZZ\n#pragma comment(lib, "harfbuzz.lib")', # noqa: E501 + }, + }, + "build": [ + cmd_rmdir("objs"), + cmd_msbuild( + r"builds\windows\vc2010\freetype.sln", "Release Static", "Clean" + ), + cmd_msbuild( + r"builds\windows\vc2010\freetype.sln", "Release Static", "Build" + ), + cmd_xcopy("include", "{inc_dir}"), + ], + "libs": [r"objs\{msbuild_arch}\Release Static\freetype.lib"], + }, + "lcms2": { + "url": f"{SF_PROJECTS}/lcms/files/lcms/{V['LCMS2']}/lcms2-{V['LCMS2']}.tar.gz/download", # noqa: E501 + "filename": f"lcms2-{V['LCMS2']}.tar.gz", + "dir": f"lcms2-{V['LCMS2']}", + "license": "LICENSE", + "patch": { + r"Projects\VC2022\lcms2_static\lcms2_static.vcxproj": { + # default is /MD for x86 and /MT for x64, we need /MD always + "MultiThreaded": "MultiThreadedDLL", # noqa: E501 + # retarget to default toolset (selected by vcvarsall.bat) + "v143": "$(DefaultPlatformToolset)", # noqa: E501 + # retarget to latest (selected by vcvarsall.bat) + "10.0": "$(WindowsSDKVersion)", # noqa: E501 + } + }, + "build": [ + cmd_rmdir("Lib"), + cmd_rmdir(r"Projects\VC2022\Release"), + cmd_msbuild(r"Projects\VC2022\lcms2.sln", "Release", "Clean"), + cmd_msbuild( + r"Projects\VC2022\lcms2.sln", "Release", "lcms2_static:Rebuild" + ), + cmd_xcopy("include", "{inc_dir}"), + ], + "libs": [r"Lib\MS\*.lib"], + }, + "openjpeg": { + "url": 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( + "openjp2", "-DBUILD_CODEC:BOOL=OFF", "-DBUILD_SHARED_LIBS:BOOL=OFF" + ), + cmd_mkdir(rf"{{inc_dir}}\openjpeg-{V['OPENJPEG']}"), + cmd_copy(r"src\lib\openjp2\*.h", rf"{{inc_dir}}\openjpeg-{V['OPENJPEG']}"), + ], + "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", + "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"), + ], + "headers": [r"*.h"], + "libs": [r"imagequant.lib"], + }, + "harfbuzz": { + "url": f"https://github.com/harfbuzz/harfbuzz/archive/{V['HARFBUZZ']}.zip", + "filename": f"harfbuzz-{V['HARFBUZZ']}.zip", + "dir": f"harfbuzz-{V['HARFBUZZ']}", + "license": "COPYING", + "build": [ + *cmds_cmake( + "harfbuzz", + "-DHB_HAVE_FREETYPE:BOOL=TRUE", + '-DCMAKE_CXX_FLAGS="-nologo -d2FH4-"', + ), + ], + "headers": [r"src\*.h"], + "libs": [r"*.lib"], + }, + "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"), + cmd_copy(r"{winbuild_dir}\fribidi.cmake", r"CMakeLists.txt"), + # generated tab.i files cannot be cross-compiled + " ^&^& ".join( + [ + "if {architecture}==ARM64 cmd /c call {vcvarsall} x86", + *cmds_cmake("fribidi-gen", "-DARCH=x86", build_dir="build_x86"), + ] + ), + *cmds_cmake("fribidi", "-DARCH={architecture}"), + ], + "bins": [r"*.dll"], + }, +} + + +# based on distutils._msvccompiler from CPython 3.7.4 +def find_msvs(architecture: str) -> dict[str, str] | None: + root = os.environ.get("ProgramFiles(x86)") or os.environ.get("ProgramFiles") + if not root: + print("Program Files not found") + return None + + requires = ["-requires", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64"] + if architecture == "ARM64": + requires += ["-requires", "Microsoft.VisualStudio.Component.VC.Tools.ARM64"] + + try: + vspath = ( + subprocess.check_output( + [ + os.path.join( + root, "Microsoft Visual Studio", "Installer", "vswhere.exe" + ), + "-latest", + "-prerelease", + *requires, + "-property", + "installationPath", + "-products", + "*", + ] + ) + .decode(encoding="mbcs") + .strip() + ) + except (subprocess.CalledProcessError, OSError, UnicodeDecodeError): + print("vswhere not found") + return None + + if not os.path.isdir(os.path.join(vspath, "VC", "Auxiliary", "Build")): + print("Visual Studio seems to be missing C compiler") + return None + + # vs2017 + msbuild = os.path.join(vspath, "MSBuild", "15.0", "Bin", "MSBuild.exe") + if not os.path.isfile(msbuild): + # vs2019 + msbuild = os.path.join(vspath, "MSBuild", "Current", "Bin", "MSBuild.exe") + if not os.path.isfile(msbuild): + print("Visual Studio MSBuild not found") + return None + + vcvarsall = os.path.join(vspath, "VC", "Auxiliary", "Build", "vcvarsall.bat") + if not os.path.isfile(vcvarsall): + print("Visual Studio vcvarsall not found") + return None + + return { + "vs_dir": vspath, + "msbuild": f'"{msbuild}"', + "vcvarsall": f'"{vcvarsall}"', + "nmake": "nmake.exe", # nmake selected by vcvarsall + } + + +def download_dep(url: str, file: str) -> None: + import urllib.error + import urllib.request + + ex = None + for i in range(3): + try: + print(f"Fetching {url} (attempt {i + 1})...") + content = urllib.request.urlopen(url).read() + with open(file, "wb") as f: + f.write(content) + break + except urllib.error.URLError as e: + ex = e + else: + raise RuntimeError(ex) + + +def extract_dep(url: str, filename: str, prefs: dict[str, str]) -> None: + import tarfile + import zipfile + + depends_dir = prefs["depends_dir"] + sources_dir = prefs["src_dir"] + + file = os.path.join(depends_dir, filename) + if not os.path.exists(file): + # First try our mirror + mirror_url = ( + f"https://raw.githubusercontent.com/" + f"python-pillow/pillow-depends/main/{filename}" + ) + try: + download_dep(mirror_url, file) + except RuntimeError as exc: + # Otherwise try upstream + print(exc) + download_dep(url, file) + + print("Extracting " + filename) + sources_dir_abs = os.path.abspath(sources_dir) + if filename.endswith(".zip"): + with zipfile.ZipFile(file) as zf: + for member in zf.namelist(): + member_abspath = os.path.abspath(os.path.join(sources_dir, member)) + member_prefix = os.path.commonpath([sources_dir_abs, member_abspath]) + if sources_dir_abs != member_prefix: + msg = "Attempted Path Traversal in Zip File" + raise RuntimeError(msg) + zf.extractall(sources_dir) + elif filename.endswith((".tar.gz", ".tgz")): + with tarfile.open(file, "r:gz") as tgz: + for member in tgz.getnames(): + member_abspath = os.path.abspath(os.path.join(sources_dir, member)) + member_prefix = os.path.commonpath([sources_dir_abs, member_abspath]) + if sources_dir_abs != member_prefix: + msg = "Attempted Path Traversal in Tar File" + raise RuntimeError(msg) + tgz.extractall(sources_dir) + else: + msg = "Unknown archive type: " + filename + raise RuntimeError(msg) + + +def write_script( + name: str, lines: list[str], prefs: dict[str, str], verbose: bool +) -> None: + name = os.path.join(prefs["build_dir"], name) + lines = [line.format(**prefs) for line in lines] + print("Writing " + name) + with open(name, "w", newline="") as f: + f.write(os.linesep.join(lines)) + if verbose: + for line in lines: + print(" " + line) + + +def get_footer(dep: dict) -> list[str]: + lines = [] + for out in dep.get("headers", []): + lines.append(cmd_copy(out, "{inc_dir}")) + for out in dep.get("libs", []): + lines.append(cmd_copy(out, "{lib_dir}")) + for out in dep.get("bins", []): + lines.append(cmd_copy(out, "{bin_dir}")) + return lines + + +def build_env(prefs: dict[str, str], verbose: bool) -> None: + lines = [ + "if defined DISTUTILS_USE_SDK goto end", + cmd_set("INCLUDE", "{inc_dir}"), + cmd_set("INCLIB", "{lib_dir}"), + cmd_set("LIB", "{lib_dir}"), + cmd_append("PATH", "{bin_dir}"), + "call {vcvarsall} {vcvars_arch}", + cmd_set("DISTUTILS_USE_SDK", "1"), # use same compiler to build Pillow + cmd_set("py_vcruntime_redist", "true"), # always use /MD, never /MT + ":end", + "@echo on", + ] + write_script("build_env.cmd", lines, prefs, verbose) + + +def build_dep(name: str, prefs: dict[str, str], verbose: bool) -> str: + dep = DEPS[name] + directory = dep["dir"] + file = f"build_dep_{name}.cmd" + license_dir = prefs["license_dir"] + sources_dir = prefs["src_dir"] + + extract_dep(dep["url"], dep["filename"], prefs) + + licenses = dep["license"] + if isinstance(licenses, str): + licenses = [licenses] + license_text = "" + for license_file in licenses: + with open(os.path.join(sources_dir, directory, license_file)) as f: + license_text += f.read() + if "license_pattern" in dep: + match = re.search(dep["license_pattern"], license_text, re.DOTALL) + license_text = "\n".join(match.groups()) + assert len(license_text) > 50 + with open(os.path.join(license_dir, f"{directory}.txt"), "w") as f: + print(f"Writing license {directory}.txt") + f.write(license_text) + + for patch_file, patch_list in dep.get("patch", {}).items(): + patch_file = os.path.join(sources_dir, directory, patch_file.format(**prefs)) + with open(patch_file) as f: + text = f.read() + for patch_from, patch_to in patch_list.items(): + patch_from = patch_from.format(**prefs) + patch_to = patch_to.format(**prefs) + assert patch_from in text + text = text.replace(patch_from, patch_to) + with open(patch_file, "w") as f: + print(f"Patching {patch_file}") + f.write(text) + + banner = f"Building {name} ({directory})" + lines = [ + r'call "{build_dir}\build_env.cmd"', + "@echo " + ("=" * 70), + f"@echo ==== {banner:<60} ====", + "@echo " + ("=" * 70), + cmd_cd(os.path.join(sources_dir, directory)), + *dep.get("build", []), + *get_footer(dep), + ] + + write_script(file, lines, prefs, verbose) + return file + + +def build_dep_all(disabled: list[str], prefs: dict[str, str], verbose: bool) -> None: + lines = [r'call "{build_dir}\build_env.cmd"'] + gha_groups = "GITHUB_ACTIONS" in os.environ + for dep_name in DEPS: + print() + if dep_name in disabled: + print(f"Skipping disabled dependency {dep_name}") + continue + script = build_dep(dep_name, prefs, verbose) + if gha_groups: + lines.append(f"@echo ::group::Running {script}") + lines.append(rf'cmd.exe /c "{{build_dir}}\{script}"') + lines.append("if errorlevel 1 echo Build failed! && exit /B 1") + if gha_groups: + lines.append("@echo ::endgroup::") + print() + lines.append("@echo All Pillow dependencies built successfully!") + write_script("build_dep_all.cmd", lines, prefs, verbose) + + +def main() -> None: + winbuild_dir = os.path.dirname(os.path.realpath(__file__)) + + parser = argparse.ArgumentParser( + prog="winbuild\\build_prepare.py", + description="Download and generate build scripts for Pillow dependencies.", + epilog="""Arguments can also be supplied using the environment variables + PILLOW_BUILD, PILLOW_DEPS, ARCHITECTURE. See winbuild\\build.rst + for more information.""", + ) + parser.add_argument( + "-v", "--verbose", action="store_true", help="print generated scripts" + ) + parser.add_argument( + "-d", + "--dir", + "--build-dir", + dest="build_dir", + metavar="PILLOW_BUILD", + default=os.environ.get("PILLOW_BUILD", os.path.join(winbuild_dir, "build")), + help="build directory (default: 'winbuild\\build')", + ) + parser.add_argument( + "--depends", + dest="depends_dir", + metavar="PILLOW_DEPS", + default=os.environ.get("PILLOW_DEPS", os.path.join(winbuild_dir, "depends")), + help="directory used to store cached dependencies " + "(default: 'winbuild\\depends')", + ) + parser.add_argument( + "--architecture", + choices=ARCHITECTURES, + default=os.environ.get( + "ARCHITECTURE", + ( + "ARM64" + if platform.machine() == "ARM64" + else ("x86" if struct.calcsize("P") == 4 else "AMD64") + ), + ), + help="build architecture (default: same as host Python)", + ) + parser.add_argument( + "--nmake", + dest="cmake_generator", + action="store_const", + const="NMake Makefiles", + default="Ninja", + help="build dependencies using NMake instead of Ninja", + ) + parser.add_argument( + "--no-imagequant", + action="store_true", + help="skip GPL-licensed optional dependency libimagequant", + ) + parser.add_argument( + "--no-fribidi", + "--no-raqm", + action="store_true", + help="skip LGPL-licensed optional dependency FriBiDi", + ) + args = parser.parse_args() + + arch_prefs = ARCHITECTURES[args.architecture] + print("Target architecture:", args.architecture) + + msvs = find_msvs(args.architecture) + if msvs is None: + msg = "Visual Studio not found. Please install Visual Studio 2017 or newer." + raise RuntimeError(msg) + print("Found Visual Studio at:", msvs["vs_dir"]) + + # dependency cache directory + args.depends_dir = os.path.abspath(args.depends_dir) + os.makedirs(args.depends_dir, exist_ok=True) + print("Caching dependencies in:", args.depends_dir) + + args.build_dir = os.path.abspath(args.build_dir) + print("Using output directory:", args.build_dir) + + # build directory for *.h files + inc_dir = os.path.join(args.build_dir, "inc") + # build directory for *.lib files + lib_dir = os.path.join(args.build_dir, "lib") + # build directory for *.bin files + bin_dir = os.path.join(args.build_dir, "bin") + # directory for storing project files + sources_dir = os.path.join(args.build_dir, "src") + # copy dependency licenses to this directory + license_dir = os.path.join(args.build_dir, "license") + + shutil.rmtree(args.build_dir, ignore_errors=True) + os.makedirs(args.build_dir, exist_ok=False) + for path in [inc_dir, lib_dir, bin_dir, sources_dir, license_dir]: + os.makedirs(path, exist_ok=True) + + disabled = [] + if args.no_imagequant: + disabled += ["libimagequant"] + if args.no_fribidi: + disabled += ["fribidi"] + + prefs = { + "architecture": args.architecture, + **arch_prefs, + # Pillow paths + "winbuild_dir": winbuild_dir, + # Build paths + "bin_dir": bin_dir, + "build_dir": args.build_dir, + "depends_dir": args.depends_dir, + "inc_dir": inc_dir, + "lib_dir": lib_dir, + "license_dir": license_dir, + "src_dir": sources_dir, + # Compilers / Tools + **msvs, + "cmake": "cmake.exe", # TODO find CMAKE automatically + "cmake_generator": args.cmake_generator, + # TODO find NASM automatically + } + + for k, v in DEPS.items(): + prefs[f"dir_{k}"] = os.path.join(sources_dir, v["dir"]) + + print() + + write_script(".gitignore", ["*"], prefs, args.verbose) + build_env(prefs, args.verbose) + build_dep_all(disabled, prefs, args.verbose) + + +if __name__ == "__main__": + main() diff --git a/winbuild/fribidi.cmake b/winbuild/fribidi.cmake index b16e0784c..d0c27add0 100644 --- a/winbuild/fribidi.cmake +++ b/winbuild/fribidi.cmake @@ -1,116 +1,116 @@ -cmake_minimum_required(VERSION 3.12) - -project(fribidi) - - -add_definitions(-D_CRT_SECURE_NO_WARNINGS) - -include_directories(lib) - -function(extract_regex_1 var text regex) - string(REGEX MATCH ${regex} _ ${text}) - set(${var} "${CMAKE_MATCH_1}" PARENT_SCOPE) -endfunction() - - -function(fribidi_conf) - file(READ configure.ac FRIBIDI_CONF) - extract_regex_1(FRIBIDI_MAJOR_VERSION "${FRIBIDI_CONF}" "\\(fribidi_major_version, ([0-9]+)\\)") - extract_regex_1(FRIBIDI_MINOR_VERSION "${FRIBIDI_CONF}" "\\(fribidi_minor_version, ([0-9]+)\\)") - extract_regex_1(FRIBIDI_MICRO_VERSION "${FRIBIDI_CONF}" "\\(fribidi_micro_version, ([0-9]+)\\)") - extract_regex_1(FRIBIDI_INTERFACE_VERSION "${FRIBIDI_CONF}" "\\(fribidi_interface_version, ([0-9]+)\\)") - extract_regex_1(FRIBIDI_INTERFACE_AGE "${FRIBIDI_CONF}" "\\(fribidi_interface_age, ([0-9]+)\\)") - extract_regex_1(FRIBIDI_BINARY_AGE "${FRIBIDI_CONF}" "\\(fribidi_binary_age, ([0-9]+)\\)") - set(FRIBIDI_VERSION "${FRIBIDI_MAJOR_VERSION}.${FRIBIDI_MINOR_VERSION}.${FRIBIDI_MICRO_VERSION}") - set(PACKAGE "fribidi") - set(PACKAGE_NAME "GNU FriBidi") - set(PACKAGE_BUGREPORT "https://github.com/fribidi/fribidi/issues/new") - set(SIZEOF_INT 4) - set(FRIBIDI_MSVC_BUILD_PLACEHOLDER "#define FRIBIDI_BUILT_WITH_MSVC") - message("Detected ${PACKAGE_NAME} version ${FRIBIDI_VERSION}") - configure_file(lib/fribidi-config.h.in ${CMAKE_CURRENT_SOURCE_DIR}/lib/fribidi-config.h @ONLY) -endfunction() -fribidi_conf() - - -option(ARCH "Target architecture") -if(${ARCH} STREQUAL ARM64) - set(GEN FALSE) -else() - set(GEN TRUE) -endif() -message("Generate tab.i files: " ${GEN}) - -function(prepend var prefix) - set(out "") - foreach(f ${ARGN}) - list(APPEND out "${prefix}${f}") - endforeach() - set(${var} "${out}" PARENT_SCOPE) -endfunction() - -macro(fribidi_definitions _TGT) - target_compile_definitions(${_TGT} PUBLIC - HAVE_MEMSET - HAVE_MEMMOVE - HAVE_STRDUP - HAVE_STDLIB_H=1 - HAVE_STRING_H=1 - HAVE_MEMORY_H=1 - #HAVE_STRINGS_H - #HAVE_SYS_TIMES_H - STDC_HEADERS=1 - HAVE_STRINGIZE=1) -endmacro() - -function(fribidi_gen _NAME _OUTNAME _PARAM) - set(_OUT ${CMAKE_CURRENT_SOURCE_DIR}/lib/${_OUTNAME}) - if(GEN) - prepend(_DEP "${CMAKE_CURRENT_SOURCE_DIR}/gen.tab/" ${ARGN}) - add_executable(gen-${_NAME} - gen.tab/gen-${_NAME}.c - gen.tab/packtab.c) - fribidi_definitions(gen-${_NAME}) - target_compile_definitions(gen-${_NAME} - PUBLIC DONT_HAVE_FRIBIDI_CONFIG_H) - add_custom_command( - COMMAND gen-${_NAME} ${_PARAM} ${_DEP} > ${_OUT} - DEPENDS ${_DEP} - OUTPUT ${_OUT}) - endif(GEN) - list(APPEND FRIBIDI_SOURCES_GENERATED "${_OUT}") - set(FRIBIDI_SOURCES_GENERATED ${FRIBIDI_SOURCES_GENERATED} PARENT_SCOPE) -endfunction() - -fribidi_gen(unicode-version fribidi-unicode-version.h "" - unidata/ReadMe.txt unidata/BidiMirroring.txt) - - -macro(fribidi_tab _NAME) - fribidi_gen(${_NAME}-tab ${_NAME}.tab.i 2 ${ARGN}) - if(GEN) - target_sources(gen-${_NAME}-tab - PRIVATE lib/fribidi-unicode-version.h) - endif(GEN) -endmacro() - -fribidi_tab(bidi-type unidata/UnicodeData.txt) -fribidi_tab(joining-type unidata/UnicodeData.txt unidata/ArabicShaping.txt) -fribidi_tab(arabic-shaping unidata/UnicodeData.txt) -fribidi_tab(mirroring unidata/BidiMirroring.txt) -fribidi_tab(brackets unidata/BidiBrackets.txt unidata/UnicodeData.txt) -fribidi_tab(brackets-type unidata/BidiBrackets.txt) - -add_custom_target(fribidi-gen DEPENDS ${FRIBIDI_SOURCES_GENERATED}) - - -file(GLOB FRIBIDI_SOURCES lib/*.c) -file(GLOB FRIBIDI_HEADERS lib/*.h) - -add_library(fribidi SHARED - ${FRIBIDI_SOURCES} - ${FRIBIDI_HEADERS} - ${FRIBIDI_SOURCES_GENERATED}) -fribidi_definitions(fribidi) -target_compile_definitions(fribidi - PUBLIC "-DFRIBIDI_BUILD") +cmake_minimum_required(VERSION 3.12) + +project(fribidi) + + +add_definitions(-D_CRT_SECURE_NO_WARNINGS) + +include_directories(lib) + +function(extract_regex_1 var text regex) + string(REGEX MATCH ${regex} _ ${text}) + set(${var} "${CMAKE_MATCH_1}" PARENT_SCOPE) +endfunction() + + +function(fribidi_conf) + file(READ configure.ac FRIBIDI_CONF) + extract_regex_1(FRIBIDI_MAJOR_VERSION "${FRIBIDI_CONF}" "\\(fribidi_major_version, ([0-9]+)\\)") + extract_regex_1(FRIBIDI_MINOR_VERSION "${FRIBIDI_CONF}" "\\(fribidi_minor_version, ([0-9]+)\\)") + extract_regex_1(FRIBIDI_MICRO_VERSION "${FRIBIDI_CONF}" "\\(fribidi_micro_version, ([0-9]+)\\)") + extract_regex_1(FRIBIDI_INTERFACE_VERSION "${FRIBIDI_CONF}" "\\(fribidi_interface_version, ([0-9]+)\\)") + extract_regex_1(FRIBIDI_INTERFACE_AGE "${FRIBIDI_CONF}" "\\(fribidi_interface_age, ([0-9]+)\\)") + extract_regex_1(FRIBIDI_BINARY_AGE "${FRIBIDI_CONF}" "\\(fribidi_binary_age, ([0-9]+)\\)") + set(FRIBIDI_VERSION "${FRIBIDI_MAJOR_VERSION}.${FRIBIDI_MINOR_VERSION}.${FRIBIDI_MICRO_VERSION}") + set(PACKAGE "fribidi") + set(PACKAGE_NAME "GNU FriBidi") + set(PACKAGE_BUGREPORT "https://github.com/fribidi/fribidi/issues/new") + set(SIZEOF_INT 4) + set(FRIBIDI_MSVC_BUILD_PLACEHOLDER "#define FRIBIDI_BUILT_WITH_MSVC") + message("Detected ${PACKAGE_NAME} version ${FRIBIDI_VERSION}") + configure_file(lib/fribidi-config.h.in ${CMAKE_CURRENT_SOURCE_DIR}/lib/fribidi-config.h @ONLY) +endfunction() +fribidi_conf() + + +option(ARCH "Target architecture") +if(${ARCH} STREQUAL ARM64) + set(GEN FALSE) +else() + set(GEN TRUE) +endif() +message("Generate tab.i files: " ${GEN}) + +function(prepend var prefix) + set(out "") + foreach(f ${ARGN}) + list(APPEND out "${prefix}${f}") + endforeach() + set(${var} "${out}" PARENT_SCOPE) +endfunction() + +macro(fribidi_definitions _TGT) + target_compile_definitions(${_TGT} PUBLIC + HAVE_MEMSET + HAVE_MEMMOVE + HAVE_STRDUP + HAVE_STDLIB_H=1 + HAVE_STRING_H=1 + HAVE_MEMORY_H=1 + #HAVE_STRINGS_H + #HAVE_SYS_TIMES_H + STDC_HEADERS=1 + HAVE_STRINGIZE=1) +endmacro() + +function(fribidi_gen _NAME _OUTNAME _PARAM) + set(_OUT ${CMAKE_CURRENT_SOURCE_DIR}/lib/${_OUTNAME}) + if(GEN) + prepend(_DEP "${CMAKE_CURRENT_SOURCE_DIR}/gen.tab/" ${ARGN}) + add_executable(gen-${_NAME} + gen.tab/gen-${_NAME}.c + gen.tab/packtab.c) + fribidi_definitions(gen-${_NAME}) + target_compile_definitions(gen-${_NAME} + PUBLIC DONT_HAVE_FRIBIDI_CONFIG_H) + add_custom_command( + COMMAND gen-${_NAME} ${_PARAM} ${_DEP} > ${_OUT} + DEPENDS ${_DEP} + OUTPUT ${_OUT}) + endif(GEN) + list(APPEND FRIBIDI_SOURCES_GENERATED "${_OUT}") + set(FRIBIDI_SOURCES_GENERATED ${FRIBIDI_SOURCES_GENERATED} PARENT_SCOPE) +endfunction() + +fribidi_gen(unicode-version fribidi-unicode-version.h "" + unidata/ReadMe.txt unidata/BidiMirroring.txt) + + +macro(fribidi_tab _NAME) + fribidi_gen(${_NAME}-tab ${_NAME}.tab.i 2 ${ARGN}) + if(GEN) + target_sources(gen-${_NAME}-tab + PRIVATE lib/fribidi-unicode-version.h) + endif(GEN) +endmacro() + +fribidi_tab(bidi-type unidata/UnicodeData.txt) +fribidi_tab(joining-type unidata/UnicodeData.txt unidata/ArabicShaping.txt) +fribidi_tab(arabic-shaping unidata/UnicodeData.txt) +fribidi_tab(mirroring unidata/BidiMirroring.txt) +fribidi_tab(brackets unidata/BidiBrackets.txt unidata/UnicodeData.txt) +fribidi_tab(brackets-type unidata/BidiBrackets.txt) + +add_custom_target(fribidi-gen DEPENDS ${FRIBIDI_SOURCES_GENERATED}) + + +file(GLOB FRIBIDI_SOURCES lib/*.c) +file(GLOB FRIBIDI_HEADERS lib/*.h) + +add_library(fribidi SHARED + ${FRIBIDI_SOURCES} + ${FRIBIDI_HEADERS} + ${FRIBIDI_SOURCES_GENERATED}) +fribidi_definitions(fribidi) +target_compile_definitions(fribidi + PUBLIC "-DFRIBIDI_BUILD")