Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bfda6b4b39 | ||
|
|
959e9d9657 | ||
|
|
2b4486a588 | ||
|
|
c0e92ca5f5 | ||
|
|
e3190960b5 | ||
|
|
3f82790e54 | ||
|
|
ed72440381 | ||
|
|
926a778d40 | ||
|
|
e7f156c909 | ||
|
|
686b6bd282 |
@ -1,5 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# gather the coverage data
|
||||
python3 -m pip install coverage
|
||||
python3 -m coverage xml
|
||||
@ -1,7 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
python3 -m coverage erase
|
||||
make clean
|
||||
make install-coverage
|
||||
@ -1,59 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
aptget_update()
|
||||
{
|
||||
if [ -n "$1" ]; then
|
||||
echo ""
|
||||
echo "Retrying apt-get update..."
|
||||
echo ""
|
||||
fi
|
||||
output=$(sudo apt-get update 2>&1)
|
||||
echo "$output"
|
||||
if [[ $output == *[WE]:\ * ]]; then
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
aptget_update || aptget_update retry || aptget_update retry
|
||||
|
||||
set -e
|
||||
|
||||
sudo apt-get -qq install libfreetype6-dev liblcms2-dev libtiff-dev python3-tk\
|
||||
ghostscript libjpeg-turbo8-dev libopenjp2-7-dev\
|
||||
cmake meson imagemagick libharfbuzz-dev libfribidi-dev\
|
||||
sway wl-clipboard libopenblas-dev nasm
|
||||
|
||||
python3 -m pip install --upgrade pip
|
||||
python3 -m pip install --upgrade wheel
|
||||
python3 -m pip install coverage
|
||||
python3 -m pip install defusedxml
|
||||
python3 -m pip install ipython
|
||||
python3 -m pip install olefile
|
||||
python3 -m pip install -U pytest
|
||||
python3 -m pip install -U pytest-cov
|
||||
python3 -m pip install -U pytest-timeout
|
||||
python3 -m pip install pyroma
|
||||
# optional test dependencies, only install if there's a binary package.
|
||||
python3 -m pip install --only-binary=:all: numpy || true
|
||||
python3 -m pip install --only-binary=:all: pyarrow || true
|
||||
|
||||
# PyQt6 doesn't support PyPy3
|
||||
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
|
||||
sudo apt-get -qq install libegl1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0
|
||||
# pyqt6 doesn't yet support free-threading; only install if a wheel is available
|
||||
python3 -m pip install --only-binary=:all: pyqt6 || true
|
||||
fi
|
||||
|
||||
# webp
|
||||
pushd depends && ./install_webp.sh && popd
|
||||
|
||||
# libimagequant
|
||||
pushd depends && ./install_imagequant.sh && popd
|
||||
|
||||
# raqm
|
||||
pushd depends && sudo ./install_raqm.sh && popd
|
||||
|
||||
# libavif
|
||||
pushd depends && ./install_libavif.sh && popd
|
||||
|
||||
# extra test images
|
||||
pushd depends && ./install_extra_test_images.sh && popd
|
||||
@ -1 +0,0 @@
|
||||
cibuildwheel==3.4.1
|
||||
@ -1,15 +0,0 @@
|
||||
mypy==1.20.2
|
||||
arro3-compute
|
||||
arro3-core
|
||||
IceSpringPySideStubs-PyQt6
|
||||
IceSpringPySideStubs-PySide6
|
||||
ipython
|
||||
numpy
|
||||
packaging
|
||||
pyarrow-stubs
|
||||
pybind11
|
||||
pytest
|
||||
types-atheris
|
||||
types-defusedxml
|
||||
types-olefile
|
||||
types-setuptools
|
||||
@ -1 +0,0 @@
|
||||
check-jsonschema==0.37.1
|
||||
@ -1,3 +0,0 @@
|
||||
python.exe -c "from PIL import Image"
|
||||
IF ERRORLEVEL 1 EXIT /B
|
||||
python.exe -bb -m pytest -vv -x -W always --cov PIL --cov Tests --cov-report term --cov-report xml Tests
|
||||
@ -1,7 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
python3 -c "from PIL import Image"
|
||||
|
||||
python3 -bb -m pytest -vv -x -W always --cov PIL --cov Tests --cov-report term --cov-report xml Tests $REVERSE
|
||||
@ -1,41 +0,0 @@
|
||||
# A clang-format style that approximates Python's PEP 7
|
||||
# Useful for IDE integration
|
||||
Language: C
|
||||
BasedOnStyle: Google
|
||||
AlwaysBreakAfterReturnType: All
|
||||
AllowShortIfStatementsOnASingleLine: false
|
||||
AlignAfterOpenBracket: BlockIndent
|
||||
BinPackArguments: false
|
||||
BinPackParameters: false
|
||||
BreakBeforeBraces: Attach
|
||||
ColumnLimit: 88
|
||||
DerivePointerAlignment: false
|
||||
IndentGotoLabels: false
|
||||
IndentWidth: 4
|
||||
PointerAlignment: Right
|
||||
ReflowComments: true
|
||||
SortIncludes: false
|
||||
SpaceBeforeParens: ControlStatements
|
||||
SpacesInParentheses: false
|
||||
TabWidth: 4
|
||||
UseTab: Never
|
||||
---
|
||||
Language: Cpp
|
||||
BasedOnStyle: Google
|
||||
AlwaysBreakAfterReturnType: All
|
||||
AllowShortIfStatementsOnASingleLine: false
|
||||
AlignAfterOpenBracket: BlockIndent
|
||||
BinPackArguments: false
|
||||
BinPackParameters: false
|
||||
BreakBeforeBraces: Attach
|
||||
ColumnLimit: 88
|
||||
DerivePointerAlignment: false
|
||||
IndentGotoLabels: false
|
||||
IndentWidth: 4
|
||||
PointerAlignment: Right
|
||||
ReflowComments: true
|
||||
SortIncludes: false
|
||||
SpaceBeforeParens: ControlStatements
|
||||
SpacesInParentheses: false
|
||||
TabWidth: 4
|
||||
UseTab: Never
|
||||
20
.coveragerc
20
.coveragerc
@ -2,21 +2,13 @@
|
||||
|
||||
[report]
|
||||
# Regexes for lines to exclude from consideration
|
||||
exclude_also =
|
||||
# Don't complain if non-runnable code isn't run
|
||||
exclude_lines =
|
||||
# Have to re-enable the standard pragma:
|
||||
pragma: no cover
|
||||
|
||||
# Don't complain if non-runnable code isn't run:
|
||||
if 0:
|
||||
if __name__ == .__main__.:
|
||||
# Don't complain about debug code
|
||||
if Image.DEBUG:
|
||||
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 =
|
||||
checks/*.py
|
||||
Tests/createfontdatachunk.py
|
||||
|
||||
@ -13,9 +13,10 @@ indent_style = space
|
||||
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{toml,yml}]
|
||||
[*.yml]
|
||||
# Two-space indentation
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
# Tab indentation (no size specified)
|
||||
[Makefile]
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
# Flake8
|
||||
8de95676e0fd89f2326b3953488ab66ff29cd2d0
|
||||
# Format with Black
|
||||
53a7e3500437a9fd5826bc04758f7116bd7e52dc
|
||||
# Format the C code with ClangFormat
|
||||
46b7e86bab79450ec0a2866c6c0c679afb659d17
|
||||
1
.gitattributes
vendored
1
.gitattributes
vendored
@ -1,3 +1,2 @@
|
||||
*.eps binary
|
||||
*.ppm binary
|
||||
*.container binary
|
||||
|
||||
21
.github/CONTRIBUTING.md
vendored
21
.github/CONTRIBUTING.md
vendored
@ -4,27 +4,24 @@ Bug fixes, feature additions, tests, documentation and more can be contributed v
|
||||
|
||||
## 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
|
||||
Please send a pull request to the master branch. Please include [documentation](https://pillow.readthedocs.io) and [tests](../Tests/README.rst) for new features. Tests or documentation without bug fixes or feature additions are welcome too. Feel free to ask questions [via issues](https://github.com/python-pillow/Pillow/issues/new) or irc://irc.freenode.net#pil
|
||||
|
||||
- Fork the Pillow repository.
|
||||
- Create a branch from `main`.
|
||||
- Create a branch from master.
|
||||
- Develop bug fixes, features, tests, etc.
|
||||
- Run the test suite. You can enable GitHub Actions (https://github.com/MY-USERNAME/Pillow/actions) on your repo to catch test failures prior to the pull request, and [Codecov](https://codecov.io/gh) to see if the changed code is covered by tests.
|
||||
- Create a pull request to pull the changes from your branch to the Pillow `main`.
|
||||
- Run the test suite on both Python 2.x and 3.x. You can enable [Travis CI](https://travis-ci.org/profile/) and [AppVeyor](https://ci.appveyor.com/projects/new) on your repo to catch test failures prior to the pull request, and [Coveralls](https://coveralls.io/repos/new) to see if the changed code is covered by tests.
|
||||
- Create a pull request to pull the changes from your branch to the Pillow master.
|
||||
|
||||
### 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 extra tests.
|
||||
- Include [release notes](https://github.com/python-pillow/Pillow/tree/main/docs/releasenotes) as needed or appropriate with your bug fixes, feature additions and tests.
|
||||
- Follow PEP8.
|
||||
- When committing only documentation changes please include [ci skip] in the commit message to avoid running tests on Travis-CI and AppVeyor.
|
||||
|
||||
## 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.
|
||||
When reporting issues, please include code that reproduces the issue and whenever possible, an image that demonstrates the issue. The best reproductions are self-contained scripts with minimal dependencies.
|
||||
|
||||
### Provide details
|
||||
|
||||
@ -35,4 +32,6 @@ The best reproductions are self-contained scripts with minimal dependencies. If
|
||||
|
||||
## Security vulnerabilities
|
||||
|
||||
Please see our [security policy](https://github.com/python-pillow/Pillow/blob/main/.github/SECURITY.md).
|
||||
To report sensitive vulnerability information, email security@python-pillow.org.
|
||||
|
||||
If your organisation/employer is a distributor of Pillow and would like advance notification of security-related bugs, please let us know your preferred contact method.
|
||||
|
||||
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@ -1,2 +0,0 @@
|
||||
github: python-pillow
|
||||
tidelift: pypi/pillow
|
||||
424
.github/INCIDENT_RESPONSE.md
vendored
424
.github/INCIDENT_RESPONSE.md
vendored
@ -1,424 +0,0 @@
|
||||
# Incident Response Plan — Pillow
|
||||
|
||||
This document describes how the Pillow maintainers detect, triage, fix, communicate, and
|
||||
learn from security incidents. It supplements the existing [Security Policy](SECURITY.md)
|
||||
and [Release Checklist](../RELEASING.md).
|
||||
|
||||
---
|
||||
|
||||
## 1. Preparation
|
||||
|
||||
Maintaining readiness before an incident occurs reduces response time and errors under pressure.
|
||||
|
||||
### 1.1 Version Support Matrix
|
||||
|
||||
Security fixes are applied to the **latest stable release only**. Users on older versions
|
||||
are expected to upgrade. Reporters should assume only the latest release will receive a patch.
|
||||
|
||||
| Branch | Status |
|
||||
|---|---|
|
||||
| `main` / latest stable | ✅ Security fixes applied |
|
||||
| All older releases | ❌ No security support — please upgrade |
|
||||
|
||||
### 1.2 Team Readiness
|
||||
|
||||
The four members of the Pillow core team are in regular contact and share collective
|
||||
responsibility for incident response. Any core team member may act as Incident Lead.
|
||||
Contact details are known to all team members.
|
||||
|
||||
### 1.3 Readiness Review
|
||||
|
||||
At each quarterly release, maintainers should re-read this document and update any stale content.
|
||||
|
||||
---
|
||||
|
||||
## 2. Scope
|
||||
|
||||
This plan covers:
|
||||
|
||||
| Incident type | Examples |
|
||||
|---|---|
|
||||
| Vulnerability in Pillow's own Python or C code | Buffer overflow in an image decoder, integer overflow in `ImagingNew` |
|
||||
| Vulnerability in a bundled or wheel-shipped C library | libjpeg, libwebp, libtiff, libpng, openjpeg, libavif |
|
||||
| Supply-chain compromise | Malicious commit, stolen maintainer credentials, tampered PyPI wheel |
|
||||
| CI/CD or infrastructure compromise | GitHub Actions secret leak, Codecov breach, PyPI token exposure |
|
||||
| Critical non-security regression | Data-loss bug shipped in a release, crash on all supported platforms |
|
||||
|
||||
---
|
||||
|
||||
## 3. Definitions
|
||||
|
||||
| Term | Meaning |
|
||||
|---|---|
|
||||
| **Incident** | Any event that compromises or threatens the confidentiality, integrity, or availability of Pillow's code, release artifacts, or infrastructure. |
|
||||
| **Vulnerability** | A security flaw in Pillow or a bundled library that can be exploited by a crafted image or API call. |
|
||||
| **Incident Lead** | The maintainer who owns coordination of the response from triage to closure. |
|
||||
| **Embargo** | A period during which fix details are kept private to allow coordinated patching before public disclosure. |
|
||||
| **Yank** | A PyPI action that keeps a release downloadable by pinned users but removes it from default `pip install` resolution. |
|
||||
| **CVE** | Common Vulnerabilities and Exposures — a public identifier assigned to a specific vulnerability. |
|
||||
| **CNA** | CVE Numbering Authority — GitHub is a CNA and can assign CVEs directly through the advisory workflow. |
|
||||
|
||||
---
|
||||
|
||||
## 4. Roles
|
||||
|
||||
| Role | Responsibility |
|
||||
|---|---|
|
||||
| **Incident Lead** | First maintainer to triage the report. Owns the incident until resolution. |
|
||||
| **Patch Owner** | Writes and tests the fix (may be the same person as Incident Lead). |
|
||||
| **Release Manager** | Cuts the point release following [RELEASING.md](../RELEASING.md). |
|
||||
| **Communications Owner** | Drafts the GitHub Security Advisory, announces on Mastodon, notifies distros. |
|
||||
| **Tidelift Contact** | For reports that arrive via Tidelift, coordinate through the Tidelift security portal. |
|
||||
|
||||
One person may fill multiple roles.
|
||||
|
||||
---
|
||||
|
||||
## 5. Severity Classification
|
||||
|
||||
Use the [CVSS 4.0](https://www.first.org/cvss/v4.0/specification-document) base score as
|
||||
a guide, mapped to the following levels:
|
||||
|
||||
| Severity | CVSS | Definition | Target Response SLA |
|
||||
|---|---|---|---|
|
||||
| **Critical** | 9.0 – 10.0 | Remote code execution, arbitrary write, or complete integrity/confidentiality loss achievable by opening a crafted image | Best effort; embargoed release where possible |
|
||||
| **High** | 7.0 – 8.9 | Heap/stack buffer overflow, use-after-free, or significant information disclosure | Best effort |
|
||||
| **Medium** | 4.0 – 6.9 | Denial of service via crafted image, out-of-bounds read, limited info disclosure | Next scheduled quarterly release, or earlier point release if needed |
|
||||
| **Low** | 0.1 – 3.9 | Minor information disclosure, unlikely to be exploitable in practice | Next quarterly release |
|
||||
|
||||
Supply-chain and CI/CD incidents are always treated as **Critical** regardless of CVSS.
|
||||
|
||||
> **Note:** These are good-faith targets for a small volunteer maintainer team, not contractual SLAs. Public safety and transparency will always be prioritised, even when timing varies.
|
||||
|
||||
---
|
||||
|
||||
## 6. Detection Sources
|
||||
|
||||
Vulnerabilities and incidents may be reported or discovered through:
|
||||
|
||||
1. **GitHub private security advisory** — preferred channel; see [SECURITY.md](SECURITY.md)
|
||||
2. **Tidelift security contact** — <https://tidelift.com/docs/security>
|
||||
3. **External researcher / coordinated disclosure** — e.g. Google Project Zero, vendor PSIRT
|
||||
4. **Automated scanning** — Dependabot, GitHub code-scanning (CodeQL), CI fuzzing
|
||||
5. **Distro security teams** — Debian, Red Hat, Ubuntu, Alpine may report upstream
|
||||
6. **User bug report** — public issue (reassess if it has security implications and convert to a private advisory if needed)
|
||||
|
||||
---
|
||||
|
||||
## 7. Response Process
|
||||
|
||||
### 7.1 Triage (all severities)
|
||||
|
||||
1. **Acknowledge receipt** to the reporter within **72 hours** using the template in
|
||||
[Appendix A](#appendix-a-communication-templates). Ask the reporter:
|
||||
- How they would like to be credited (name, handle, or anonymous)
|
||||
- Whether they intend to publish their own advisory, and if so, their preferred timeline
|
||||
- Thank them explicitly — reporters do the project a favour by disclosing privately.
|
||||
2. Reproduce the issue. If the report is invalid, close it and notify the reporter.
|
||||
3. Assign a severity level ([Section 5: Severity Classification](#5-severity-classification)).
|
||||
4. If the GitHub Security Advisory was not created by the reporter, create one now and keep
|
||||
it **private** until the fix is released. Add the reporter as a collaborator if they wish
|
||||
to be involved.
|
||||
5. **Request a CVE** through the GitHub Security Advisory workflow (GitHub is a CVE
|
||||
Numbering Authority — no separate MITRE form required). The CVE is reserved privately
|
||||
and published automatically when the advisory goes public.
|
||||
6. **Escalation** — Escalate beyond the core maintainer team if any of the following apply:
|
||||
- The fix requires changes to CPython or a dependency outside Pillow's control → contact the relevant upstream immediately
|
||||
- A legal concern arises (e.g. GDPR-reportable data exposure) → contact the project's legal/fiscal sponsor
|
||||
- The Incident Lead is unreachable for > 24 hours on a Critical issue → any other maintainer may assume the role
|
||||
|
||||
### 7.2 Fix Development
|
||||
|
||||
1. Develop the fix in a **private fork** or directly in the private security advisory
|
||||
workspace on GitHub. Do **not** push to a public branch before the embargo lifts.
|
||||
2. Write a regression test that fails before the fix and passes after.
|
||||
3. Review the patch with at least one other maintainer.
|
||||
|
||||
### 7.3 Standard (Non-Embargoed) Release
|
||||
|
||||
For Medium and Low severity, or when no distro pre-notification is needed:
|
||||
|
||||
1. Merge the fix to `main`, then cherry-pick to all affected release branches
|
||||
(see [RELEASING.md — Point release](../RELEASING.md)).
|
||||
2. Amend commit messages to include the CVE identifier.
|
||||
3. Follow the [Point release](../RELEASING.md#point-release) process in RELEASING.md to
|
||||
tag, push, and confirm wheels are live on PyPI.
|
||||
4. Publish the GitHub Security Advisory (this simultaneously publishes the CVE).
|
||||
|
||||
### 7.4 Embargoed Release
|
||||
|
||||
For Critical and High severity where distro pre-notification improves user safety:
|
||||
|
||||
1. Prepare patches against all affected release branches and test locally.
|
||||
2. Agree on an **embargo date** with the reporter (typically 7–14 days out, up to 90 days for
|
||||
complex issues).
|
||||
3. Privately send the patch to distros via the
|
||||
[linux-distros](https://oss-security.openwall.org/wiki/mailing-lists/distros) mailing list
|
||||
or directly to individual distro security teams.
|
||||
4. On the embargo date:
|
||||
- Amend commit messages with the CVE identifier.
|
||||
- Follow the [Embargoed release](../RELEASING.md#embargoed-release) process in
|
||||
RELEASING.md to tag, push, and confirm wheels are live on PyPI.
|
||||
- Publish the GitHub Security Advisory.
|
||||
|
||||
### 7.5 Supply-Chain / Infrastructure Compromise
|
||||
|
||||
1. **Immediately** revoke any potentially compromised credentials:
|
||||
- PyPI API tokens
|
||||
- GitHub personal access tokens and OAuth apps
|
||||
- Codecov or other CI service tokens
|
||||
2. Audit recent commits and releases for tampering:
|
||||
- Verify release tags against known-good SHAs
|
||||
- Re-inspect any wheel published since the potential compromise window
|
||||
3. If a PyPI release is suspected to be tampered: yank it immediately via the
|
||||
[PyPI release management page](https://pypi.org/manage/project/Pillow/releases/)
|
||||
(login required); see [https://pypi.org/security/](https://pypi.org/security/) for
|
||||
reporting to the PyPI security team.
|
||||
4. Issue a public advisory describing the scope and any user action required.
|
||||
|
||||
### 7.6 Recovery
|
||||
|
||||
After the fix is released and the advisory is public:
|
||||
|
||||
1. Verify that the patched wheels are live on PyPI and passing CI across all supported platforms.
|
||||
2. Confirm any yanked releases are handled correctly .
|
||||
3. Resume normal development operations on `main`.
|
||||
4. Monitor the GitHub issue tracker and Mastodon for user reports of residual problems for at least **72 hours** post-release.
|
||||
5. Close the private GitHub Security Advisory once recovery is confirmed.
|
||||
|
||||
---
|
||||
|
||||
## 8. Communication
|
||||
|
||||
### Internal (during embargo)
|
||||
- Use the **private GitHub Security Advisory** thread for coordination with the reporter.
|
||||
- Use private communication channels for all other coordination.
|
||||
- Do not discuss details in public issues, PRs, or Gitter/IRC channels.
|
||||
|
||||
### External (at or after disclosure)
|
||||
|
||||
| Audience | Channel | Timing |
|
||||
|---|---|---|
|
||||
| General users | [GitHub Security Advisory](https://github.com/python-pillow/Pillow/security/advisories) | At release |
|
||||
| PyPI ecosystem | CVE published via advisory | At release |
|
||||
| Downstream distros | Direct email or linux-distros list | Before embargo date (embargoed) |
|
||||
| Tidelift subscribers | Tidelift security portal | At release (or coordinated) |
|
||||
| Community | [Mastodon @pillow](https://fosstodon.org/@pillow) | At release |
|
||||
|
||||
**Advisory content should include:**
|
||||
- CVE identifier and CVSS score
|
||||
- Affected Pillow versions
|
||||
- Fixed version(s)
|
||||
- Nature of the vulnerability (without full exploit details if still fresh)
|
||||
- Credit to the reporter (with their consent)
|
||||
- Upgrade instructions (`python3 -m pip install --upgrade Pillow`)
|
||||
|
||||
---
|
||||
|
||||
## 9. Dependency Map
|
||||
|
||||
Understanding what Pillow depends on (upstream) and what depends on Pillow (downstream)
|
||||
is essential for scoping impact and coordinating notifications during an incident.
|
||||
|
||||
### 9.1 Upstream Dependencies
|
||||
|
||||
#### Bundled C libraries (shipped in official wheels)
|
||||
|
||||
These libraries are compiled into Pillow's binary wheels. A CVE in any of them may
|
||||
require a Pillow point release even if Pillow's own code is unchanged.
|
||||
|
||||
| Library | Purpose | Security advisory tracker |
|
||||
|---|---|---|
|
||||
| [libjpeg-turbo](https://libjpeg-turbo.org/) | JPEG encode/decode | [GitHub](https://github.com/libjpeg-turbo/libjpeg-turbo/security) |
|
||||
| [libpng](http://www.libpng.org/pub/png/libpng.html) | PNG encode/decode within FreeType 2, OpenJPEG and WebP | [SourceForge](https://sourceforge.net/p/libpng/bugs/) |
|
||||
| [libtiff](https://libtiff.gitlab.io/libtiff/) | TIFF encode/decode | [GitLab](https://gitlab.com/libtiff/libtiff/-/work_items) |
|
||||
| [libwebp](https://chromium.googlesource.com/webm/libwebp) | WebP encode/decode | [Chromium tracker](https://issues.webmproject.org/issues) |
|
||||
| [libavif](https://github.com/AOMediaCodec/libavif) | AVIF encode/decode | [GitHub](https://github.com/AOMediaCodec/libavif/security) |
|
||||
| [aom](https://aomedia.googlesource.com/aom/) | AV1 codec (AVIF) | [Chromium tracker](https://aomedia.issues.chromium.org/issues) |
|
||||
| [dav1d](https://code.videolan.org/videolan/dav1d) | AV1 decode (AVIF) | [VideoLAN Security](https://www.videolan.org/security/) |
|
||||
| [openjpeg](https://www.openjpeg.org/) | JPEG 2000 encode/decode | [GitHub](https://github.com/uclouvain/openjpeg/security) |
|
||||
| [freetype2](https://freetype.org/) | Font rendering | [GitLab](https://gitlab.freedesktop.org/freetype/freetype/-/work_items) |
|
||||
| [lcms2](https://www.littlecms.com/) | ICC color management | [GitHub](https://github.com/mm2/Little-CMS/security) |
|
||||
| [harfbuzz](https://harfbuzz.github.io/) | Text shaping (via raqm) | [GitHub](https://github.com/harfbuzz/harfbuzz/security) |
|
||||
| [raqm](https://github.com/HOST-Oman/libraqm) | Complex text layout | [GitHub](https://github.com/HOST-Oman/libraqm) |
|
||||
| [fribidi](https://github.com/fribidi/fribidi) | Unicode bidi (via raqm) | [GitHub](https://github.com/fribidi/fribidi) |
|
||||
| [zlib](https://zlib.net/) | Deflate compression | [zlib.net](https://zlib.net/) |
|
||||
| [liblzma / xz-utils](https://tukaani.org/xz/) | XZ/LZMA compression | [GitHub](https://github.com/tukaani-project/xz/security) |
|
||||
| [bzip2](https://gitlab.com/bzip2/bzip2) | BZ2 compression | [GitLab](https://gitlab.com/bzip2/bzip2/-/work_items) |
|
||||
| [zstd](https://github.com/facebook/zstd) | Zstandard compression | [GitHub](https://github.com/facebook/zstd/security) |
|
||||
| [brotli](https://github.com/google/brotli) | Brotli compression | [GitHub](https://github.com/google/brotli/security) |
|
||||
| [libyuv](https://chromium.googlesource.com/libyuv/libyuv/) | YUV conversion | [Chromium tracker](https://libyuv.issues.chromium.org/issues) |
|
||||
|
||||
#### Python-level dependencies
|
||||
|
||||
| Package | Required? | Purpose |
|
||||
|---|---|---|
|
||||
| `setuptools` | Build-time only | Package build backend |
|
||||
| `pybind11` | Build-time only | Compile C files in parallel |
|
||||
| `olefile` | Optional (`fpx`, `mic` extras) | OLE2 container parsing (FPX, MIC formats) |
|
||||
| `defusedxml` | Optional (`xmp` extra) | Safe XML parsing for XMP metadata |
|
||||
|
||||
See [`pyproject.toml`](../pyproject.toml) for the complete and authoritative list of
|
||||
optional dependencies.
|
||||
|
||||
### 9.2 Responding to an Upstream Vulnerability
|
||||
|
||||
When a CVE is published for a bundled C library:
|
||||
|
||||
1. Assess whether the vulnerable code path is reachable through Pillow's API.
|
||||
2. If reachable, treat as a Pillow vulnerability and follow [Section 5: Severity Classification](#5-severity-classification).
|
||||
3. Update the bundled library version in the wheel build scripts and rebuild wheels.
|
||||
4. Reference the upstream CVE in Pillow's release notes and GitHub Security Advisory.
|
||||
5. If not reachable, document the rationale in a public issue so downstream distributors
|
||||
can make informed decisions about patching their system packages.
|
||||
|
||||
### 9.3 Downstream Dependencies
|
||||
|
||||
A vulnerability in Pillow can have wide impact. Notify or consider the blast radius of
|
||||
these downstream consumers when assessing severity and planning communications.
|
||||
|
||||
#### Linux distribution packages
|
||||
|
||||
| Distribution | Package name | Security contact |
|
||||
|---|---|---|
|
||||
| Debian / Ubuntu | `python3-pil` | [Debian Security](https://www.debian.org/security/) / [Ubuntu Security](https://ubuntu.com/security) |
|
||||
| Fedora / RHEL / CentOS | `python3-pillow` | [Red Hat Security](https://access.redhat.com/security/) |
|
||||
| Alpine Linux | `py3-pillow` | [Alpine security](https://security.alpinelinux.org/) |
|
||||
| Arch Linux | `python-pillow` | [Arch security tracker](https://security.archlinux.org/) |
|
||||
| Homebrew | `pillow` | [Homebrew maintainers](https://github.com/Homebrew/homebrew-core/security) |
|
||||
| conda-forge | `pillow` | [conda-forge](https://github.com/conda-forge/pillow-feedstock) |
|
||||
|
||||
#### Major Python ecosystem consumers
|
||||
|
||||
These are high-profile projects known to depend on Pillow; a critical vulnerability may
|
||||
warrant proactive notification.
|
||||
|
||||
| Project | Usage |
|
||||
|---|---|
|
||||
| [matplotlib](https://matplotlib.org/) | Image I/O for plots |
|
||||
| [scikit-image](https://scikit-image.org/) | Image processing |
|
||||
| [torchvision](https://github.com/pytorch/vision) (PyTorch) | Dataset loading, transforms |
|
||||
| [Keras / TensorFlow](https://keras.io/) | Image preprocessing utilities |
|
||||
| [Django](https://www.djangoproject.com/) | `ImageField` validation and thumbnail generation |
|
||||
| [Wagtail](https://wagtail.org/) | CMS image renditions |
|
||||
| [Plone](https://plone.org/) | CMS image handling |
|
||||
| [Jupyter / IPython](https://jupyter.org/) | Inline image display |
|
||||
| [ReportLab](https://www.reportlab.com/) | PDF image embedding |
|
||||
| [Tidelift subscribers](https://tidelift.com/) | Enterprise consumers (coordinated via Tidelift) |
|
||||
|
||||
#### Pillow ecosystem plugins
|
||||
|
||||
Third-party plugins extend Pillow and are distributed separately on PyPI. Their
|
||||
maintainers should be notified for Critical/High issues that affect the plugin API
|
||||
or the formats they decode. See the
|
||||
[full plugin list](https://pillow.readthedocs.io/en/stable/handbook/third-party-plugins.html#plugin-list).
|
||||
|
||||
---
|
||||
|
||||
## 11. Plan Maintenance
|
||||
|
||||
This document is a living record. It should be kept current so it is useful when an incident actually occurs. Revisit it during the [Section 1.3 readiness review](#13-readiness-review) at each quarterly release.
|
||||
|
||||
---
|
||||
|
||||
## 12. References
|
||||
|
||||
- [Security Policy](SECURITY.md)
|
||||
- [Release Checklist](../RELEASING.md)
|
||||
- [Contributing Guide](CONTRIBUTING.md)
|
||||
- [Tidelift Security Contact](https://tidelift.com/docs/security)
|
||||
- [GitHub: Privately reporting a security vulnerability](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability)
|
||||
- [GitHub as a CVE Numbering Authority (CNA)](https://docs.github.com/en/code-security/security-advisories/working-with-repository-security-advisories/about-repository-security-advisories)
|
||||
- [FIRST CVSS 4.0 Calculator](https://www.first.org/cvss/calculator/4.0)
|
||||
- [linux-distros mailing list](https://oss-security.openwall.org/wiki/mailing-lists/distros)
|
||||
- [OpenSSF CVD Guide](https://github.com/ossf/oss-vulnerability-guide) *(basis for this plan)*
|
||||
|
||||
---
|
||||
|
||||
## Appendix A: Communication Templates
|
||||
|
||||
### A.1 Reporter Acknowledgment
|
||||
|
||||
> Subject: Re: [Security] \<brief issue description\>
|
||||
>
|
||||
> Hi \<name\>,
|
||||
>
|
||||
> Thank you for taking the time to report this issue. We appreciate it.
|
||||
>
|
||||
> We have received your report and will review it as soon as possible. We will
|
||||
> keep you updated on our progress.
|
||||
>
|
||||
> Questions:
|
||||
>
|
||||
> - How would you like to be credited in the advisory? (name, handle,
|
||||
> organisation, or anonymous)
|
||||
> - Do you plan to publish your own write-up or advisory? If so, do you have a
|
||||
> disclosure date in mind?
|
||||
>
|
||||
> We apply coordinated disclosure principles to all vulnerability reports. If
|
||||
> you have any questions or concerns at any point, please reply to this thread.
|
||||
>
|
||||
> Thank you again,
|
||||
> The Pillow team
|
||||
|
||||
### A.2 Embargoed Distro Notification
|
||||
|
||||
> Subject: [EMBARGOED] Pillow security issue — \<CVE-XXXX-XXXXX\> — disclosure \<DATE\>
|
||||
>
|
||||
> This is an embargoed notification of a vulnerability in Pillow. Please keep this
|
||||
> information confidential until the disclosure date listed below.
|
||||
>
|
||||
> **CVE:** \<CVE-XXXX-XXXXX\>
|
||||
>
|
||||
> **Affected versions:** \<e.g. Pillow < 11.x.x\>
|
||||
>
|
||||
> **Fixed version:** \<version\>
|
||||
>
|
||||
> **Severity:** \<Critical / High / Medium / Low\> (CVSS \<score\>: \<vector\>)
|
||||
>
|
||||
> **Reporter:** \<name / affiliation, or "reported privately"\>
|
||||
>
|
||||
> **Public disclosure date:** \<DATE TIME UTC\>
|
||||
>
|
||||
> **Summary:**
|
||||
> \<One paragraph describing the vulnerability class and impact without a full exploit.\>
|
||||
>
|
||||
> **Proof of concept:**
|
||||
> \<Minimal reproducer or attached patch.\>
|
||||
>
|
||||
> **Remediation:**
|
||||
> Upgrade to Pillow \<fixed version\>. No known workaround.
|
||||
>
|
||||
> Please do not share this information, issue public patches, or make user communications
|
||||
> before the disclosure date. We will notify this list immediately if the date changes.
|
||||
>
|
||||
> — The Pillow maintainers
|
||||
|
||||
### A.3 Public Disclosure Advisory
|
||||
|
||||
*(Published as a GitHub Security Advisory; the CVE and date are included automatically.)*
|
||||
|
||||
> **Summary:** \<One-paragraph technical summary.\>
|
||||
>
|
||||
> **CVE:** \<CVE-XXXX-XXXXX\>
|
||||
>
|
||||
> **Affected versions:** Pillow \< \<fixed version\>
|
||||
>
|
||||
> **Fixed version:** \<version\>
|
||||
>
|
||||
> **Severity:** \<rating\> (CVSS \<score\>)
|
||||
>
|
||||
> **Reporter:** \<credited name / "reported privately"\>
|
||||
>
|
||||
> **Details:**
|
||||
> \<Fuller technical description. Include attack scenario where helpful.\>
|
||||
>
|
||||
> **Remediation:**
|
||||
> ```
|
||||
> python3 -m pip install --upgrade Pillow
|
||||
> ```
|
||||
>
|
||||
> **Timeline:**
|
||||
> - Reported: \<date\>
|
||||
> - Fixed: \<date\>
|
||||
> - Disclosed: \<date\>
|
||||
13
.github/ISSUE_TEMPLATE.md
vendored
Normal file
13
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
### What did you do?
|
||||
|
||||
### What did you expect to happen?
|
||||
|
||||
### What actually happened?
|
||||
|
||||
### What versions of Pillow and Python are you using?
|
||||
|
||||
Please include code that reproduces the issue and whenever possible, an image that demonstrates the issue. The best reproductions are self-contained scripts with minimal dependencies. If you are using a framework such as plone, django, or buildout, try to replicate the issue just using Pillow.
|
||||
|
||||
```python
|
||||
code goes here
|
||||
```
|
||||
74
.github/ISSUE_TEMPLATE/ISSUE_REPORT.md
vendored
74
.github/ISSUE_TEMPLATE/ISSUE_REPORT.md
vendored
@ -1,74 +0,0 @@
|
||||
---
|
||||
name: Issue report
|
||||
about: Create a report to help us improve Pillow
|
||||
---
|
||||
|
||||
<!--
|
||||
Thank you for reporting an issue.
|
||||
|
||||
Follow these guidelines to ensure your issue is handled properly.
|
||||
|
||||
If you have a ...
|
||||
|
||||
1. General question: consider asking the question on Stack Overflow
|
||||
with the python-imaging-library tag:
|
||||
|
||||
* https://stackoverflow.com/questions/tagged/python-imaging-library
|
||||
|
||||
Do not ask a question in both places.
|
||||
|
||||
If you think you have found a bug or have an unexplained exception
|
||||
then file a bug report here.
|
||||
|
||||
2. Bug report: include a self-contained, copy-pastable example that
|
||||
generates the issue if possible. Be concise with code posted.
|
||||
Guidelines on how to provide a good bug report:
|
||||
|
||||
* https://stackoverflow.com/help/mcve
|
||||
|
||||
Bug reports which follow these guidelines are easier to diagnose,
|
||||
and are often handled much more quickly.
|
||||
|
||||
3. Feature request: do a quick search of existing issues
|
||||
to make sure this has not been asked before.
|
||||
|
||||
We know asking good questions takes effort, and we appreciate your time.
|
||||
Thank you.
|
||||
-->
|
||||
|
||||
### 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)
|
||||
```
|
||||
|
||||
<!--
|
||||
Please include **code** that reproduces the issue and whenever possible, an **image** that demonstrates the issue. Please upload images to GitHub, not to third-party file hosting sites. If necessary, add the image to a zip or tar archive.
|
||||
|
||||
The best reproductions are self-contained scripts with minimal dependencies. If you are using a framework such as Plone, Django, or Buildout, try to replicate the issue just using Pillow.
|
||||
-->
|
||||
|
||||
```python
|
||||
code goes here
|
||||
```
|
||||
46
.github/ISSUE_TEMPLATE/RELEASE.md
vendored
46
.github/ISSUE_TEMPLATE/RELEASE.md
vendored
@ -1,46 +0,0 @@
|
||||
---
|
||||
name: "Maintainers only: Release"
|
||||
about: For maintainers to schedule a quarterly release
|
||||
labels: Release
|
||||
---
|
||||
|
||||
## Main release
|
||||
|
||||
Released quarterly on January 2nd, April 1st, July 1st and October 15th.
|
||||
|
||||
* [ ] Open a release ticket e.g. https://github.com/python-pillow/Pillow/issues/3154
|
||||
* [ ] Develop and prepare release in `main` branch.
|
||||
* [ ] Add release notes e.g. https://github.com/python-pillow/Pillow/pull/8885
|
||||
* [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) to confirm passing tests in `main` branch.
|
||||
* [ ] Check that all the wheel builds pass the tests in the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) jobs by manually triggering them.
|
||||
* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py`
|
||||
* [ ] Run pre-release check via `make release-test` in a freshly cloned repo.
|
||||
* [ ] Create branch and tag for release e.g.:
|
||||
```bash
|
||||
git branch [[MAJOR.MINOR]].x
|
||||
git tag [[MAJOR.MINOR]].0
|
||||
git push --tags
|
||||
```
|
||||
* [ ] Check the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) has passed, including the "Upload release to PyPI" job. This will have been triggered by the new tag.
|
||||
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases).
|
||||
* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), increment and append `.dev0` to version identifier in `src/PIL/_version.py` and then:
|
||||
```bash
|
||||
git push --all
|
||||
```
|
||||
|
||||
## Publicize release
|
||||
|
||||
* [ ] Announce release availability via [Mastodon](https://fosstodon.org/@pillow) e.g. https://fosstodon.org/@pillow/110639450470725321
|
||||
|
||||
## Documentation
|
||||
|
||||
* [ ] Make sure the [default version for Read the Docs](https://pillow.readthedocs.io/en/stable/) is up-to-date with the release changes
|
||||
|
||||
## Docker images
|
||||
|
||||
* [ ] Update Pillow in the Docker Images repository
|
||||
```bash
|
||||
git clone https://github.com/python-pillow/docker-images
|
||||
cd docker-images
|
||||
./update-pillow-tag.sh [[release tag]]
|
||||
```
|
||||
21
.github/SECURITY.md
vendored
21
.github/SECURITY.md
vendored
@ -1,21 +0,0 @@
|
||||
# Security policy
|
||||
|
||||
## Reporting a vulnerability
|
||||
|
||||
To report sensitive vulnerability information, report it [privately on GitHub](https://github.com/python-pillow/Pillow/security/advisories/new).
|
||||
|
||||
If you cannot use GitHub, use the [Tidelift security contact](https://tidelift.com/docs/security). Tidelift will coordinate the fix and disclosure.
|
||||
|
||||
**DO NOT report sensitive vulnerability information in public.**
|
||||
|
||||
## Threat model
|
||||
|
||||
Pillow's primary attack surface is parsing untrusted image data. A full STRIDE threat model covering spoofing, tampering, repudiation, information disclosure, denial of service, and elevation of privilege is maintained in the [Security handbook page](https://pillow.readthedocs.io/en/latest/handbook/security.html).
|
||||
|
||||
Key risks to be aware of when using Pillow to process untrusted images:
|
||||
|
||||
- **Decompression bombs** — do not set `Image.MAX_IMAGE_PIXELS = None` in production.
|
||||
- **EPS files invoke Ghostscript** — block EPS input at the application layer unless strictly required.
|
||||
- **`ImageMath.unsafe_eval()`** — never pass user-controlled strings to this function; use `lambda_eval` instead.
|
||||
- **C extension memory safety** — keep Pillow and its bundled C libraries (libjpeg, libpng, libtiff, libwebp, etc.) up to date.
|
||||
- **Sandboxing** — for high-risk deployments, run image processing in a sandboxed subprocess.
|
||||
271
.github/compare-dist-sizes.py
vendored
271
.github/compare-dist-sizes.py
vendored
@ -1,271 +0,0 @@
|
||||
"""Compare sizes of newly-built dists against the latest release on PyPI.
|
||||
|
||||
Fetches file sizes for the latest Pillow release from the PyPI JSON API
|
||||
(no download required) and compares them to a directory of freshly-built
|
||||
wheels and sdist. Outputs a table to stdout (and to
|
||||
`$GITHUB_STEP_SUMMARY` if set).
|
||||
|
||||
Usage:
|
||||
`uv run .github/compare-dist-sizes.py <dist-dir>`
|
||||
"""
|
||||
|
||||
# /// script
|
||||
# requires-python = ">=3.10"
|
||||
# dependencies = [
|
||||
# "humanize",
|
||||
# "prettytable",
|
||||
# "termcolor",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
import humanize
|
||||
from prettytable import PrettyTable, TableStyle
|
||||
from termcolor import colored
|
||||
|
||||
PYPI_JSON_URL = "https://pypi.org/pypi/pillow/json"
|
||||
|
||||
# Wheel filename: {distribution}-{version}(-{build})?-{python}-{abi}-{platform}.whl
|
||||
# sdist filename: {distribution}-{version}.tar.gz
|
||||
WHEEL_RE = re.compile(
|
||||
r"^[^-]+-[^-]+(?:-(?P<build>\d[^-]*))?"
|
||||
r"-(?P<python>[^-]+)-(?P<abi>[^-]+)-(?P<platform>[^-]+)\.whl$",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
SDIST_RE = re.compile(
|
||||
r"^(?P<dist>[^-]+)-(?P<version>.+)\.tar\.gz$",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def key_for(filename: str) -> str:
|
||||
"""Return a version-independent identifier for a dist file."""
|
||||
if m := WHEEL_RE.match(filename):
|
||||
build = f"{m['build']}-" if m["build"] else ""
|
||||
return f"wheel:{build}{m['python']}-{m['abi']}-{m['platform']}"
|
||||
if SDIST_RE.match(filename):
|
||||
return "sdist"
|
||||
msg = f"Unexpected dist name: {filename}"
|
||||
raise ValueError(msg)
|
||||
|
||||
|
||||
def display_for(filename: str) -> str:
|
||||
"""Strip the `pillow-{version}-` prefix for compact table display."""
|
||||
if m := WHEEL_RE.match(filename):
|
||||
build = f"{m['build']}-" if m["build"] else ""
|
||||
return f"{build}{m['python']}-{m['abi']}-{m['platform']}.whl"
|
||||
if SDIST_RE.match(filename):
|
||||
return "sdist (.tar.gz)"
|
||||
return filename
|
||||
|
||||
|
||||
def fetch_pypi_sizes() -> tuple[str, dict[str, tuple[str, int]]]:
|
||||
"""Return (version, {key: (filename, size)}) for the latest PyPI release."""
|
||||
with urllib.request.urlopen(PYPI_JSON_URL) as response:
|
||||
data = json.load(response)
|
||||
version = data["info"]["version"]
|
||||
sizes: dict[str, tuple[str, int]] = {}
|
||||
for entry in data.get("urls", []):
|
||||
filename = entry["filename"]
|
||||
key = key_for(filename)
|
||||
sizes[key] = (filename, entry["size"])
|
||||
return version, sizes
|
||||
|
||||
|
||||
def collect_local_sizes(dist_dir: Path) -> dict[str, tuple[str, int]]:
|
||||
sizes: dict[str, tuple[str, int]] = {}
|
||||
for path in sorted(dist_dir.iterdir()):
|
||||
if not path.is_file():
|
||||
continue
|
||||
key = key_for(path.name)
|
||||
sizes[key] = (path.name, path.stat().st_size)
|
||||
return sizes
|
||||
|
||||
|
||||
def human(n: int | None) -> str:
|
||||
if n is None:
|
||||
return "n/a"
|
||||
return humanize.naturalsize(n)
|
||||
|
||||
|
||||
def pct_change(before: int | None, after: int | None) -> str:
|
||||
if before is None or after is None:
|
||||
return "n/a"
|
||||
delta = 0 if before == 0 else (after - before) / before * 100
|
||||
return f"{delta:+.2f}%"
|
||||
|
||||
|
||||
def pct_severity(text: str) -> dict[str, str] | None:
|
||||
"""Return status indicators based on the change percent."""
|
||||
if text == "n/a":
|
||||
return None
|
||||
pct = float(text.rstrip("%"))
|
||||
if pct >= 5:
|
||||
return {"color": "red", "emoji": "🔴"}
|
||||
if pct > 0:
|
||||
return {"color": "yellow", "emoji": "🟡"}
|
||||
else:
|
||||
return {"color": "green", "emoji": "🟢"}
|
||||
|
||||
|
||||
def render_table(
|
||||
baseline_label: str,
|
||||
baseline_sizes: dict[str, tuple[str, int]],
|
||||
local_sizes: dict[str, tuple[str, int]],
|
||||
*,
|
||||
markdown: bool,
|
||||
) -> str:
|
||||
table = PrettyTable()
|
||||
table.set_style(TableStyle.MARKDOWN if markdown else TableStyle.SINGLE_BORDER)
|
||||
table.field_names = ["File", "Size before", "Size now", "Change"]
|
||||
table.align = "r"
|
||||
table.align["File"] = "l"
|
||||
|
||||
def style(cells: list[str], role: str) -> list[str]:
|
||||
severity = pct_severity(cells[3])
|
||||
if markdown:
|
||||
if severity:
|
||||
cells[3] = f"{severity['emoji']} {cells[3]}"
|
||||
if role == "orphan":
|
||||
return [f"*{c}*" for c in cells]
|
||||
if role == "summary":
|
||||
return [f"**{c}**" for c in cells]
|
||||
return cells
|
||||
|
||||
if role == "orphan":
|
||||
return [colored(c, "dark_grey") for c in cells]
|
||||
|
||||
bold_attrs = ["bold"] if role == "summary" else []
|
||||
if bold_attrs:
|
||||
cells[:3] = [colored(c, attrs=bold_attrs) for c in cells[:3]]
|
||||
if severity:
|
||||
cells[3] = colored(cells[3], severity["color"], attrs=bold_attrs)
|
||||
elif bold_attrs:
|
||||
cells[3] = colored(cells[3], attrs=bold_attrs)
|
||||
return cells
|
||||
|
||||
keys = list(set(baseline_sizes) | set(local_sizes))
|
||||
# Put sdist first for readability
|
||||
keys.sort(key=lambda k: (k != "sdist", k))
|
||||
|
||||
wheel_before = []
|
||||
wheel_after = []
|
||||
total_before = []
|
||||
total_after = []
|
||||
for key in keys:
|
||||
baseline_entry = baseline_sizes.get(key)
|
||||
local_entry = local_sizes.get(key)
|
||||
display_name = display_for((local_entry or baseline_entry)[0])
|
||||
before = baseline_entry[1] if baseline_entry else None
|
||||
after = local_entry[1] if local_entry else None
|
||||
if after is None:
|
||||
# Removed since baseline: ignore in totals
|
||||
role = "orphan"
|
||||
else:
|
||||
# Present locally (in both, or newly added): count in totals
|
||||
total_after.append(after)
|
||||
if before is not None:
|
||||
total_before.append(before)
|
||||
if key != "sdist":
|
||||
wheel_after.append(after)
|
||||
if before is not None:
|
||||
wheel_before.append(before)
|
||||
role = "data"
|
||||
cells = [
|
||||
display_name,
|
||||
human(before),
|
||||
human(after),
|
||||
pct_change(before, after),
|
||||
]
|
||||
table.add_row(style(cells, role))
|
||||
|
||||
if not markdown:
|
||||
table.add_divider()
|
||||
|
||||
if wheel_after:
|
||||
avg_before = sum(wheel_before) // len(wheel_before) if wheel_before else None
|
||||
table.add_row(
|
||||
style(
|
||||
[
|
||||
f"wheel average ({len(wheel_after)} wheels)",
|
||||
human(avg_before),
|
||||
human(sum(wheel_after) // len(wheel_after)),
|
||||
pct_change(avg_before, sum(wheel_after) // len(wheel_after)),
|
||||
],
|
||||
"summary",
|
||||
)
|
||||
)
|
||||
table.add_row(
|
||||
style(
|
||||
[
|
||||
f"wheel total ({len(wheel_after)} wheels)",
|
||||
human(sum(wheel_before)),
|
||||
human(sum(wheel_after)),
|
||||
pct_change(sum(wheel_before), sum(wheel_after)),
|
||||
],
|
||||
"summary",
|
||||
),
|
||||
divider=not markdown,
|
||||
)
|
||||
|
||||
if total_after:
|
||||
table.add_row(
|
||||
style(
|
||||
[
|
||||
f"artifacts total ({len(total_after)} artifacts)",
|
||||
human(sum(total_before)),
|
||||
human(sum(total_after)),
|
||||
pct_change(sum(total_before), sum(total_after)),
|
||||
],
|
||||
"summary",
|
||||
)
|
||||
)
|
||||
|
||||
title = f"## Dist size comparison vs {baseline_label}"
|
||||
if not markdown:
|
||||
title = colored(title, attrs=["bold"])
|
||||
return f"{title}\n\n{table.get_string()}\n"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description=__doc__, formatter_class=argparse.ArgumentDefaultsHelpFormatter
|
||||
)
|
||||
parser.add_argument(
|
||||
"dist_dir",
|
||||
type=Path,
|
||||
help="Directory containing newly-built wheels and sdist",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.dist_dir.is_dir():
|
||||
print(f"error: {args.dist_dir} is not a directory", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
baseline_version, baseline_sizes = fetch_pypi_sizes()
|
||||
baseline_label = f"Pillow {baseline_version} on PyPI"
|
||||
|
||||
local_sizes = collect_local_sizes(args.dist_dir)
|
||||
|
||||
print(render_table(baseline_label, baseline_sizes, local_sizes, markdown=False))
|
||||
|
||||
if summary_path := os.environ.get("GITHUB_STEP_SUMMARY"):
|
||||
with open(summary_path, "a", encoding="utf-8") as f:
|
||||
f.write(
|
||||
render_table(baseline_label, baseline_sizes, local_sizes, markdown=True)
|
||||
)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
19
.github/dependencies.json
vendored
19
.github/dependencies.json
vendored
@ -1,19 +0,0 @@
|
||||
{
|
||||
"brotli": "1.2.0",
|
||||
"bzip2": "1.0.8",
|
||||
"freetype": "2.14.3",
|
||||
"fribidi": "1.0.16",
|
||||
"harfbuzz": "14.2.0",
|
||||
"jpegturbo": "3.1.4.1",
|
||||
"lcms2": "2.19",
|
||||
"libavif": "1.4.1",
|
||||
"libimagequant": "4.4.1",
|
||||
"libpng": "1.6.58",
|
||||
"libwebp": "1.6.0",
|
||||
"libxcb": "1.17.0",
|
||||
"openjpeg": "2.5.4",
|
||||
"tiff": "4.7.1",
|
||||
"xz": "5.8.3",
|
||||
"zlib-ng": "2.3.3",
|
||||
"zstd": "1.5.7"
|
||||
}
|
||||
560
.github/generate-sbom.py
vendored
560
.github/generate-sbom.py
vendored
@ -1,560 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate a CycloneDX 1.7 SBOM for Pillow's C extensions and their
|
||||
vendored/optional native library dependencies.
|
||||
|
||||
Usage:
|
||||
python3 .github/generate-sbom.py [output-file]
|
||||
|
||||
Output defaults to pillow-{version}.cdx.json in the current directory.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import datetime as dt
|
||||
import difflib
|
||||
import hashlib
|
||||
import json
|
||||
import urllib.request
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_version() -> str:
|
||||
version_file = Path(__file__).parent.parent / "src" / "PIL" / "_version.py"
|
||||
return version_file.read_text(encoding="utf-8").split('"')[1]
|
||||
|
||||
|
||||
def load_dep_versions() -> dict[str, str]:
|
||||
deps_file = Path(__file__).parent / "dependencies.json"
|
||||
return json.loads(deps_file.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def sha256_file(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def upstream_diff_b64(
|
||||
upstream_url: str,
|
||||
upstream_display: bytes,
|
||||
local_path: Path,
|
||||
local_display: bytes,
|
||||
) -> str:
|
||||
"""
|
||||
Fetch an upstream file and return a base64-encoded unified diff vs the local copy.
|
||||
"""
|
||||
with urllib.request.urlopen(upstream_url) as resp:
|
||||
upstream_text = resp.read()
|
||||
local_text = local_path.read_bytes()
|
||||
diff_lines = difflib.diff_bytes(
|
||||
difflib.unified_diff,
|
||||
upstream_text.splitlines(keepends=True),
|
||||
local_text.splitlines(keepends=True),
|
||||
fromfile=b"a/" + upstream_display,
|
||||
tofile=b"b/" + local_display,
|
||||
)
|
||||
return base64.b64encode(b"".join(diff_lines)).decode()
|
||||
|
||||
|
||||
def generate(version: str) -> dict:
|
||||
serial = str(uuid.uuid4())
|
||||
now = dt.datetime.now(dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
purl = f"pkg:pypi/pillow@{version}"
|
||||
root = Path(__file__).parent.parent
|
||||
thirdparty = root / "src" / "thirdparty"
|
||||
versions = load_dep_versions()
|
||||
|
||||
metadata_component = {
|
||||
"bom-ref": purl,
|
||||
"type": "library",
|
||||
"name": "Pillow",
|
||||
"version": version,
|
||||
"description": "Python Imaging Library (fork)",
|
||||
"licenses": [{"license": {"id": "MIT-CMU"}}],
|
||||
"purl": purl,
|
||||
"externalReferences": [
|
||||
{"type": "website", "url": "https://python-pillow.github.io"},
|
||||
{"type": "vcs", "url": "https://github.com/python-pillow/Pillow"},
|
||||
{"type": "documentation", "url": "https://pillow.readthedocs.io"},
|
||||
{
|
||||
"type": "security-contact",
|
||||
"url": "https://github.com/python-pillow/Pillow/security/policy",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
c_extensions = [
|
||||
("PIL._avif", "AVIF image format extension"),
|
||||
(
|
||||
"PIL._imaging",
|
||||
"Core image processing extension "
|
||||
"(decode, encode, map, display, outline, path, libImaging)",
|
||||
),
|
||||
("PIL._imagingcms", "LittleCMS2 colour management extension"),
|
||||
("PIL._imagingft", "FreeType font rendering extension"),
|
||||
("PIL._imagingmath", "Image math operations extension"),
|
||||
("PIL._imagingmorph", "Image morphology extension"),
|
||||
("PIL._imagingtk", "Tk/Tcl display extension"),
|
||||
("PIL._webp", "WebP image format extension"),
|
||||
]
|
||||
|
||||
ext_components = [
|
||||
{
|
||||
"bom-ref": f"{purl}#c-ext/{name}",
|
||||
"type": "library",
|
||||
"name": name,
|
||||
"version": version,
|
||||
"description": desc,
|
||||
"licenses": [{"license": {"id": "MIT-CMU"}}],
|
||||
"purl": f"{purl}#c-ext/{name}",
|
||||
}
|
||||
for name, desc in c_extensions
|
||||
]
|
||||
|
||||
vendored_components = [
|
||||
{
|
||||
"bom-ref": f"{purl}#thirdparty/fribidi-shim",
|
||||
"type": "library",
|
||||
"name": "fribidi-shim",
|
||||
"version": "1.x",
|
||||
"description": "FriBiDi runtime-loading shim "
|
||||
"(vendored in src/thirdparty/fribidi-shim/); "
|
||||
"loads libfribidi dynamically",
|
||||
"licenses": [{"license": {"id": "LGPL-2.1-or-later"}}],
|
||||
"hashes": [
|
||||
{
|
||||
"alg": "SHA-256",
|
||||
"content": sha256_file(thirdparty / "fribidi-shim" / "fribidi.c"),
|
||||
}
|
||||
],
|
||||
"pedigree": {
|
||||
"notes": "Pillow-authored shim; not taken from an upstream project."
|
||||
},
|
||||
"externalReferences": [
|
||||
{"type": "website", "url": "https://github.com/fribidi/fribidi"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"bom-ref": "pkg:github/python/pythoncapi-compat",
|
||||
"type": "library",
|
||||
"name": "pythoncapi_compat",
|
||||
"description": "Backport header for new CPython C-API functions "
|
||||
"(vendored in src/thirdparty/pythoncapi_compat.h)",
|
||||
"licenses": [{"license": {"id": "0BSD"}}],
|
||||
"hashes": [
|
||||
{
|
||||
"alg": "SHA-256",
|
||||
"content": sha256_file(thirdparty / "pythoncapi_compat.h"),
|
||||
}
|
||||
],
|
||||
"pedigree": {
|
||||
"notes": "Vendored unmodified from upstream python/pythoncapi-compat."
|
||||
},
|
||||
"externalReferences": [
|
||||
{
|
||||
"type": "vcs",
|
||||
"url": "https://github.com/python/pythoncapi-compat",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"bom-ref": f"{purl}#thirdparty/raqm",
|
||||
"type": "library",
|
||||
"name": "raqm",
|
||||
"version": "0.10.5",
|
||||
"description": "Complex text layout library "
|
||||
"(vendored in src/thirdparty/raqm/)",
|
||||
"licenses": [{"license": {"id": "MIT"}}],
|
||||
"hashes": [
|
||||
{
|
||||
"alg": "SHA-256",
|
||||
"content": sha256_file(thirdparty / "raqm" / "raqm.c"),
|
||||
}
|
||||
],
|
||||
"pedigree": {
|
||||
"ancestors": [
|
||||
{
|
||||
"bom-ref": "pkg:github/HOST-Oman/libraqm@0.10.5#upstream",
|
||||
"type": "library",
|
||||
"name": "raqm",
|
||||
"version": "0.10.5",
|
||||
"purl": "pkg:github/HOST-Oman/libraqm@0.10.5",
|
||||
"externalReferences": [
|
||||
{
|
||||
"type": "distribution",
|
||||
"url": "https://github.com/HOST-Oman/libraqm/releases/tag/v0.10.5",
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
"patches": [
|
||||
{
|
||||
"type": "unofficial",
|
||||
"diff": {
|
||||
"text": {
|
||||
# raqm-version.h.in → raqm-version.h:
|
||||
# template @RAQM_VERSION_*@ placeholders replaced
|
||||
# with literal 0.10.5 values; filename changed to
|
||||
# drop the .in suffix; minor indentation fix.
|
||||
"content": upstream_diff_b64(
|
||||
"https://raw.githubusercontent.com/HOST-Oman/libraqm/v0.10.5/src/raqm-version.h.in",
|
||||
b"src/raqm-version.h.in",
|
||||
thirdparty / "raqm" / "raqm-version.h",
|
||||
b"src/raqm-version.h",
|
||||
),
|
||||
"encoding": "base64",
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "unofficial",
|
||||
"diff": {
|
||||
"text": {
|
||||
# raqm.c: wrap the <fribidi.h> include in an
|
||||
# #ifdef HAVE_FRIBIDI_SYSTEM guard so that when
|
||||
# building without a system FriBiDi Pillow's own
|
||||
# fribidi-shim is used instead.
|
||||
"content": upstream_diff_b64(
|
||||
"https://raw.githubusercontent.com/HOST-Oman/libraqm/v0.10.5/src/raqm.c",
|
||||
b"src/raqm.c",
|
||||
thirdparty / "raqm" / "raqm.c",
|
||||
b"src/raqm.c",
|
||||
),
|
||||
"encoding": "base64",
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
"notes": (
|
||||
"Vendored from upstream HOST-Oman/libraqm v0.10.5 with two "
|
||||
"Pillow-specific modifications: (1) raqm-version.h.in was "
|
||||
"pre-processed into raqm-version.h with version placeholders "
|
||||
"replaced by literal values; (2) raqm.c wraps the <fribidi.h> "
|
||||
"include in an #ifdef HAVE_FRIBIDI_SYSTEM guard so Pillow's "
|
||||
"bundled fribidi-shim is used when a system FriBiDi is absent."
|
||||
),
|
||||
},
|
||||
"externalReferences": [
|
||||
{
|
||||
"type": "vcs",
|
||||
"url": "https://github.com/python-pillow/Pillow/tree/main/src/thirdparty/raqm",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
native_deps = [
|
||||
{
|
||||
"bom-ref": "pkg:generic/freetype2",
|
||||
"type": "library",
|
||||
"name": "FreeType",
|
||||
"version": versions["freetype"],
|
||||
"scope": "optional",
|
||||
"description": "Font rendering (optional, used by PIL._imagingft). "
|
||||
"Required for text/font support.",
|
||||
"licenses": [{"license": {"id": "FTL"}}],
|
||||
"externalReferences": [
|
||||
{"type": "website", "url": "https://freetype.org"},
|
||||
{
|
||||
"type": "distribution",
|
||||
"url": "https://download.savannah.gnu.org/releases/freetype/",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"bom-ref": "pkg:generic/fribidi",
|
||||
"type": "library",
|
||||
"name": "FriBiDi",
|
||||
"version": versions["fribidi"],
|
||||
"scope": "optional",
|
||||
"description": "Unicode bidi algorithm library (optional, "
|
||||
"loaded at runtime by fribidi-shim).",
|
||||
"licenses": [{"license": {"id": "LGPL-2.1-or-later"}}],
|
||||
"externalReferences": [
|
||||
{"type": "website", "url": "https://github.com/fribidi/fribidi"},
|
||||
{
|
||||
"type": "distribution",
|
||||
"url": "https://github.com/fribidi/fribidi/releases",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"bom-ref": "pkg:generic/harfbuzz",
|
||||
"type": "library",
|
||||
"name": "HarfBuzz",
|
||||
"version": versions["harfbuzz"],
|
||||
"scope": "optional",
|
||||
"description": "Text shaping (optional, required by libraqm "
|
||||
"for complex text layout).",
|
||||
"licenses": [{"license": {"id": "MIT"}}],
|
||||
"externalReferences": [
|
||||
{"type": "website", "url": "https://harfbuzz.github.io"},
|
||||
{
|
||||
"type": "distribution",
|
||||
"url": "https://github.com/harfbuzz/harfbuzz/releases",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"bom-ref": "pkg:generic/libavif",
|
||||
"type": "library",
|
||||
"name": "libavif",
|
||||
"version": versions["libavif"],
|
||||
"scope": "optional",
|
||||
"description": "AVIF codec (optional, used by PIL._avif).",
|
||||
"licenses": [{"license": {"id": "BSD-2-Clause"}}],
|
||||
"externalReferences": [
|
||||
{"type": "website", "url": "https://github.com/AOMediaCodec/libavif"},
|
||||
{
|
||||
"type": "distribution",
|
||||
"url": "https://github.com/AOMediaCodec/libavif/releases",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"bom-ref": "pkg:generic/libimagequant",
|
||||
"type": "library",
|
||||
"name": "libimagequant",
|
||||
"version": versions["libimagequant"],
|
||||
"scope": "optional",
|
||||
"description": "Improved colour quantization (optional).",
|
||||
"licenses": [{"license": {"id": "GPL-3.0-or-later"}}],
|
||||
"externalReferences": [
|
||||
{"type": "website", "url": "https://pngquant.org/lib/"},
|
||||
{
|
||||
"type": "distribution",
|
||||
"url": "https://github.com/ImageOptim/libimagequant/tags",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"bom-ref": "pkg:generic/libjpeg",
|
||||
"type": "library",
|
||||
"name": "libjpeg / libjpeg-turbo",
|
||||
"version": versions["jpegturbo"],
|
||||
"description": "JPEG codec (required by default; disable with "
|
||||
"-C jpeg=disable).",
|
||||
"licenses": [
|
||||
{"license": {"id": "IJG"}},
|
||||
{"license": {"id": "BSD-3-Clause"}},
|
||||
],
|
||||
"externalReferences": [
|
||||
{"type": "website", "url": "https://ijg.org"},
|
||||
{"type": "website", "url": "https://libjpeg-turbo.org"},
|
||||
{
|
||||
"type": "distribution",
|
||||
"url": "https://github.com/libjpeg-turbo/libjpeg-turbo/releases",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"bom-ref": "pkg:generic/libtiff",
|
||||
"type": "library",
|
||||
"name": "libtiff",
|
||||
"version": versions["tiff"],
|
||||
"scope": "optional",
|
||||
"description": "TIFF codec (optional).",
|
||||
"licenses": [{"license": {"id": "libtiff"}}],
|
||||
"externalReferences": [
|
||||
{"type": "website", "url": "https://libtiff.gitlab.io/libtiff/"},
|
||||
{
|
||||
"type": "distribution",
|
||||
"url": "https://download.osgeo.org/libtiff/",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"bom-ref": "pkg:generic/libwebp",
|
||||
"type": "library",
|
||||
"name": "libwebp",
|
||||
"version": versions["libwebp"],
|
||||
"scope": "optional",
|
||||
"description": "WebP codec (optional, used by PIL._webp).",
|
||||
"licenses": [{"license": {"id": "BSD-3-Clause"}}],
|
||||
"externalReferences": [
|
||||
{
|
||||
"type": "website",
|
||||
"url": "https://chromium.googlesource.com/webm/libwebp",
|
||||
},
|
||||
{
|
||||
"type": "distribution",
|
||||
"url": "https://chromium.googlesource.com/webm/libwebp",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"bom-ref": "pkg:generic/libxcb",
|
||||
"type": "library",
|
||||
"name": "libxcb",
|
||||
"version": versions["libxcb"],
|
||||
"scope": "optional",
|
||||
"description": "X11 screen-grab support (optional, "
|
||||
"used by PIL._imaging on macOS and Linux).",
|
||||
"licenses": [{"license": {"id": "X11"}}],
|
||||
"externalReferences": [
|
||||
{"type": "website", "url": "https://xcb.freedesktop.org"},
|
||||
{
|
||||
"type": "distribution",
|
||||
"url": "https://xcb.freedesktop.org/dist/",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"bom-ref": "pkg:generic/littlecms2",
|
||||
"type": "library",
|
||||
"name": "Little CMS 2",
|
||||
"version": versions["lcms2"],
|
||||
"scope": "optional",
|
||||
"description": "Colour management (optional, used by PIL._imagingcms).",
|
||||
"licenses": [{"license": {"id": "MIT"}}],
|
||||
"externalReferences": [
|
||||
{"type": "website", "url": "https://www.littlecms.com"},
|
||||
{
|
||||
"type": "distribution",
|
||||
"url": "https://github.com/mm2/Little-CMS/releases",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"bom-ref": "pkg:generic/openjpeg",
|
||||
"type": "library",
|
||||
"name": "OpenJPEG",
|
||||
"version": versions["openjpeg"],
|
||||
"scope": "optional",
|
||||
"description": "JPEG 2000 codec (optional).",
|
||||
"licenses": [{"license": {"id": "BSD-2-Clause"}}],
|
||||
"externalReferences": [
|
||||
{"type": "website", "url": "https://www.openjpeg.org"},
|
||||
{
|
||||
"type": "distribution",
|
||||
"url": "https://github.com/uclouvain/openjpeg/releases",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"bom-ref": "pkg:pypi/pybind11",
|
||||
"type": "library",
|
||||
"name": "pybind11",
|
||||
"scope": "excluded",
|
||||
"description": "Parallel C compilation library (build-time dependency).",
|
||||
"licenses": [{"license": {"id": "BSD-3-Clause"}}],
|
||||
"externalReferences": [
|
||||
{"type": "website", "url": "https://pybind11.readthedocs.io"},
|
||||
{
|
||||
"type": "distribution",
|
||||
"url": "https://github.com/pybind/pybind11/releases",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"bom-ref": "pkg:generic/zlib",
|
||||
"type": "library",
|
||||
"name": "zlib",
|
||||
"version": versions["zlib-ng"],
|
||||
"description": "Deflate/PNG compression (required by default; "
|
||||
"disable with -C zlib=disable).",
|
||||
"licenses": [{"license": {"id": "Zlib"}}],
|
||||
"externalReferences": [
|
||||
{"type": "website", "url": "https://zlib.net"},
|
||||
{"type": "distribution", "url": "https://zlib.net"},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
dependencies = [
|
||||
{
|
||||
"ref": purl,
|
||||
"dependsOn": [e["bom-ref"] for e in ext_components],
|
||||
},
|
||||
{
|
||||
"ref": f"{purl}#c-ext/PIL._avif",
|
||||
"dependsOn": ["pkg:generic/libavif"],
|
||||
},
|
||||
{
|
||||
"ref": f"{purl}#c-ext/PIL._imaging",
|
||||
"dependsOn": [
|
||||
"pkg:generic/libimagequant",
|
||||
"pkg:generic/libjpeg",
|
||||
"pkg:generic/libtiff",
|
||||
"pkg:generic/libxcb",
|
||||
"pkg:generic/openjpeg",
|
||||
"pkg:generic/zlib",
|
||||
],
|
||||
},
|
||||
{
|
||||
"ref": f"{purl}#c-ext/PIL._imagingcms",
|
||||
"dependsOn": ["pkg:generic/littlecms2"],
|
||||
},
|
||||
{
|
||||
"ref": f"{purl}#c-ext/PIL._imagingft",
|
||||
"dependsOn": [
|
||||
"pkg:generic/freetype2",
|
||||
"pkg:generic/fribidi",
|
||||
"pkg:generic/harfbuzz",
|
||||
f"{purl}#thirdparty/fribidi-shim",
|
||||
f"{purl}#thirdparty/raqm",
|
||||
],
|
||||
},
|
||||
{
|
||||
"ref": f"{purl}#c-ext/PIL._webp",
|
||||
"dependsOn": ["pkg:generic/libwebp"],
|
||||
},
|
||||
{
|
||||
"ref": f"{purl}#thirdparty/raqm",
|
||||
"dependsOn": [
|
||||
"pkg:generic/harfbuzz",
|
||||
f"{purl}#thirdparty/fribidi-shim",
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
return {
|
||||
"$schema": "http://cyclonedx.org/schema/bom-1.7.schema.json",
|
||||
"bomFormat": "CycloneDX",
|
||||
"specVersion": "1.7",
|
||||
"serialNumber": f"urn:uuid:{serial}",
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"timestamp": now,
|
||||
"lifecycles": [{"phase": "build"}],
|
||||
"tools": {
|
||||
"components": [
|
||||
{
|
||||
"type": "application",
|
||||
"name": "generate-sbom.py",
|
||||
"group": "pillow",
|
||||
}
|
||||
]
|
||||
},
|
||||
"component": metadata_component,
|
||||
},
|
||||
"components": ext_components + vendored_components + native_deps,
|
||||
"dependencies": dependencies,
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
version = get_version()
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description=__doc__, formatter_class=argparse.ArgumentDefaultsHelpFormatter
|
||||
)
|
||||
parser.add_argument(
|
||||
"output",
|
||||
nargs="?",
|
||||
type=Path,
|
||||
default=Path(f"pillow-{version}.cdx.json"),
|
||||
help="output file",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
sbom = generate(version)
|
||||
args.output.write_text(json.dumps(sbom, indent=2) + "\n", encoding="utf-8")
|
||||
print(
|
||||
f"Wrote {args.output} (Pillow {version}, {len(sbom['components'])} components)"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
13
.github/mergify.yml
vendored
13
.github/mergify.yml
vendored
@ -1,13 +0,0 @@
|
||||
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
|
||||
actions:
|
||||
merge:
|
||||
method: merge
|
||||
18
.github/problem-matchers/gcc.json
vendored
18
.github/problem-matchers/gcc.json
vendored
@ -1,18 +0,0 @@
|
||||
{
|
||||
"__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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
27
.github/release-drafter.yml
vendored
27
.github/release-drafter.yml
vendored
@ -1,27 +0,0 @@
|
||||
name-template: "$NEXT_MINOR_VERSION"
|
||||
tag-template: "$NEXT_MINOR_VERSION"
|
||||
change-template: '- $TITLE #$NUMBER [@$AUTHOR]'
|
||||
|
||||
categories:
|
||||
- title: "Removals"
|
||||
label: "Removal"
|
||||
- title: "Deprecations"
|
||||
label: "Deprecation"
|
||||
- title: "Documentation"
|
||||
label: "Documentation"
|
||||
- title: "Dependencies"
|
||||
label: "Dependency"
|
||||
- title: "Testing"
|
||||
label: "Testing"
|
||||
- title: "Type hints"
|
||||
label: "Type hints"
|
||||
- title: "Other changes"
|
||||
|
||||
exclude-labels:
|
||||
- "changelog: skip"
|
||||
|
||||
template: |
|
||||
|
||||
https://pillow.readthedocs.io/en/stable/releasenotes/$NEXT_MINOR_VERSION.html
|
||||
|
||||
$CHANGES
|
||||
175
.github/renovate.json
vendored
175
.github/renovate.json
vendored
@ -1,175 +0,0 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended"
|
||||
],
|
||||
"labels": [
|
||||
"Dependency"
|
||||
],
|
||||
"minimumReleaseAge": "7 days",
|
||||
"prCreation": "not-pending",
|
||||
"schedule": [
|
||||
"* * 3 * *"
|
||||
],
|
||||
"customManagers": [
|
||||
{
|
||||
"customType": "regex",
|
||||
"managerFilePatterns": ["/^\\.github/dependencies\\.json$/"],
|
||||
"matchStrings": ["\"brotli\":\\s*\"(?<currentValue>\\d+[^\"]*)\""],
|
||||
"depNameTemplate": "brotli",
|
||||
"packageNameTemplate": "google/brotli",
|
||||
"datasourceTemplate": "github-releases",
|
||||
"extractVersionTemplate": "^v(?<version>.+)$"
|
||||
},
|
||||
{
|
||||
"customType": "regex",
|
||||
"managerFilePatterns": ["/^\\.github/dependencies\\.json$/"],
|
||||
"matchStrings": ["\"bzip2\":\\s*\"(?<currentValue>\\d+[^\"]*)\""],
|
||||
"depNameTemplate": "bzip2",
|
||||
"packageNameTemplate": "bzip2/bzip2",
|
||||
"datasourceTemplate": "gitlab-tags",
|
||||
"extractVersionTemplate": "^bzip2-(?<version>.+)$"
|
||||
},
|
||||
{
|
||||
"customType": "regex",
|
||||
"managerFilePatterns": ["/^\\.github/dependencies\\.json$/"],
|
||||
"matchStrings": ["\"freetype\":\\s*\"(?<currentValue>\\d+[^\"]*)\""],
|
||||
"depNameTemplate": "freetype",
|
||||
"packageNameTemplate": "freetype/freetype",
|
||||
"datasourceTemplate": "gitlab-tags",
|
||||
"registryUrlTemplate": "https://gitlab.freedesktop.org",
|
||||
"extractVersionTemplate": "^VER-(?<version>[\\d-]+)$",
|
||||
"versioningTemplate": "regex:^(?<major>\\d+)[.-](?<minor>\\d+)[.-](?<patch>\\d+)$"
|
||||
},
|
||||
{
|
||||
"customType": "regex",
|
||||
"managerFilePatterns": ["/^\\.github/dependencies\\.json$/"],
|
||||
"matchStrings": ["\"fribidi\":\\s*\"(?<currentValue>\\d+[^\"]*)\""],
|
||||
"depNameTemplate": "fribidi",
|
||||
"packageNameTemplate": "fribidi/fribidi",
|
||||
"datasourceTemplate": "github-releases",
|
||||
"extractVersionTemplate": "^v(?<version>.+)$"
|
||||
},
|
||||
{
|
||||
"customType": "regex",
|
||||
"managerFilePatterns": ["/^\\.github/dependencies\\.json$/"],
|
||||
"matchStrings": ["\"harfbuzz\":\\s*\"(?<currentValue>\\d+[^\"]*)\""],
|
||||
"depNameTemplate": "harfbuzz",
|
||||
"packageNameTemplate": "harfbuzz/harfbuzz",
|
||||
"datasourceTemplate": "github-releases"
|
||||
},
|
||||
{
|
||||
"customType": "regex",
|
||||
"managerFilePatterns": ["/^\\.github/dependencies\\.json$/"],
|
||||
"matchStrings": ["\"jpegturbo\":\\s*\"(?<currentValue>\\d+[^\"]*)\""],
|
||||
"depNameTemplate": "jpegturbo",
|
||||
"packageNameTemplate": "libjpeg-turbo/libjpeg-turbo",
|
||||
"datasourceTemplate": "github-releases"
|
||||
},
|
||||
{
|
||||
"customType": "regex",
|
||||
"managerFilePatterns": ["/^\\.github/dependencies\\.json$/"],
|
||||
"matchStrings": ["\"lcms2\":\\s*\"(?<currentValue>\\d+[^\"]*)\""],
|
||||
"depNameTemplate": "lcms2",
|
||||
"packageNameTemplate": "mm2/Little-CMS",
|
||||
"datasourceTemplate": "github-releases",
|
||||
"extractVersionTemplate": "^lcms(?<version>.+)$"
|
||||
},
|
||||
{
|
||||
"customType": "regex",
|
||||
"managerFilePatterns": ["/^\\.github/dependencies\\.json$/"],
|
||||
"matchStrings": ["\"libavif\":\\s*\"(?<currentValue>\\d+[^\"]*)\""],
|
||||
"depNameTemplate": "libavif",
|
||||
"packageNameTemplate": "AOMediaCodec/libavif",
|
||||
"datasourceTemplate": "github-releases",
|
||||
"extractVersionTemplate": "^v(?<version>.+)$"
|
||||
},
|
||||
{
|
||||
"customType": "regex",
|
||||
"managerFilePatterns": ["/^\\.github/dependencies\\.json$/"],
|
||||
"matchStrings": ["\"libimagequant\":\\s*\"(?<currentValue>\\d+[^\"]*)\""],
|
||||
"depNameTemplate": "libimagequant",
|
||||
"packageNameTemplate": "ImageOptim/libimagequant",
|
||||
"datasourceTemplate": "github-tags"
|
||||
},
|
||||
{
|
||||
"customType": "regex",
|
||||
"managerFilePatterns": ["/^\\.github/dependencies\\.json$/"],
|
||||
"matchStrings": ["\"libpng\":\\s*\"(?<currentValue>\\d+[^\"]*)\""],
|
||||
"depNameTemplate": "libpng",
|
||||
"packageNameTemplate": "pnggroup/libpng",
|
||||
"datasourceTemplate": "github-tags",
|
||||
"extractVersionTemplate": "^v(?<version>.+)$"
|
||||
},
|
||||
{
|
||||
"customType": "regex",
|
||||
"managerFilePatterns": ["/^\\.github/dependencies\\.json$/"],
|
||||
"matchStrings": ["\"libwebp\":\\s*\"(?<currentValue>\\d+[^\"]*)\""],
|
||||
"depNameTemplate": "libwebp",
|
||||
"packageNameTemplate": "webmproject/libwebp",
|
||||
"datasourceTemplate": "github-tags",
|
||||
"extractVersionTemplate": "^v(?<version>.+)$"
|
||||
},
|
||||
{
|
||||
"customType": "regex",
|
||||
"managerFilePatterns": ["/^\\.github/dependencies\\.json$/"],
|
||||
"matchStrings": ["\"libxcb\":\\s*\"(?<currentValue>\\d+[^\"]*)\""],
|
||||
"depNameTemplate": "libxcb",
|
||||
"packageNameTemplate": "xorg/lib/libxcb",
|
||||
"datasourceTemplate": "gitlab-tags",
|
||||
"registryUrlTemplate": "https://gitlab.freedesktop.org",
|
||||
"extractVersionTemplate": "^libxcb-(?<version>.+)$"
|
||||
},
|
||||
{
|
||||
"customType": "regex",
|
||||
"managerFilePatterns": ["/^\\.github/dependencies\\.json$/"],
|
||||
"matchStrings": ["\"openjpeg\":\\s*\"(?<currentValue>\\d+[^\"]*)\""],
|
||||
"depNameTemplate": "openjpeg",
|
||||
"packageNameTemplate": "uclouvain/openjpeg",
|
||||
"datasourceTemplate": "github-releases",
|
||||
"extractVersionTemplate": "^v(?<version>.+)$"
|
||||
},
|
||||
{
|
||||
"customType": "regex",
|
||||
"managerFilePatterns": ["/^\\.github/dependencies\\.json$/"],
|
||||
"matchStrings": ["\"tiff\":\\s*\"(?<currentValue>\\d+[^\"]*)\""],
|
||||
"depNameTemplate": "tiff",
|
||||
"packageNameTemplate": "libtiff/libtiff",
|
||||
"datasourceTemplate": "gitlab-tags",
|
||||
"extractVersionTemplate": "^v(?<version>.+)$"
|
||||
},
|
||||
{
|
||||
"customType": "regex",
|
||||
"managerFilePatterns": ["/^\\.github/dependencies\\.json$/"],
|
||||
"matchStrings": ["\"xz\":\\s*\"(?<currentValue>\\d+[^\"]*)\""],
|
||||
"depNameTemplate": "xz",
|
||||
"packageNameTemplate": "tukaani-project/xz",
|
||||
"datasourceTemplate": "github-releases",
|
||||
"extractVersionTemplate": "^v(?<version>.+)$"
|
||||
},
|
||||
{
|
||||
"customType": "regex",
|
||||
"managerFilePatterns": ["/^\\.github/dependencies\\.json$/"],
|
||||
"matchStrings": ["\"zlib-ng\":\\s*\"(?<currentValue>\\d+[^\"]*)\""],
|
||||
"depNameTemplate": "zlib-ng",
|
||||
"packageNameTemplate": "zlib-ng/zlib-ng",
|
||||
"datasourceTemplate": "github-releases"
|
||||
},
|
||||
{
|
||||
"customType": "regex",
|
||||
"managerFilePatterns": ["/^\\.github/dependencies\\.json$/"],
|
||||
"matchStrings": ["\"zstd\":\\s*\"(?<currentValue>\\d+[^\"]*)\""],
|
||||
"depNameTemplate": "zstd",
|
||||
"packageNameTemplate": "facebook/zstd",
|
||||
"datasourceTemplate": "github-releases",
|
||||
"extractVersionTemplate": "^v(?<version>.+)$"
|
||||
}
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
"groupName": "github-actions",
|
||||
"matchManagers": ["github-actions"],
|
||||
"separateMajorMinor": false
|
||||
}
|
||||
]
|
||||
}
|
||||
13
.github/workflows/Brewfile
vendored
13
.github/workflows/Brewfile
vendored
@ -1,13 +0,0 @@
|
||||
brew "aom"
|
||||
brew "dav1d"
|
||||
brew "freetype"
|
||||
brew "ghostscript"
|
||||
brew "jpeg-turbo"
|
||||
brew "libimagequant"
|
||||
brew "libraqm"
|
||||
brew "libtiff"
|
||||
brew "little-cms2"
|
||||
brew "openjpeg"
|
||||
brew "rav1e"
|
||||
brew "svt-av1"
|
||||
brew "webp"
|
||||
62
.github/workflows/cifuzz.yml
vendored
62
.github/workflows/cifuzz.yml
vendored
@ -1,62 +0,0 @@
|
||||
name: CIFuzz
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
paths: &paths
|
||||
- ".github/dependencies.json"
|
||||
- ".github/workflows/cifuzz.yml"
|
||||
- ".github/workflows/wheels-dependencies.sh"
|
||||
- "**.c"
|
||||
- "**.h"
|
||||
pull_request:
|
||||
paths: *paths
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
FORCE_COLOR: 1
|
||||
|
||||
jobs:
|
||||
Fuzzing:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Build Fuzzers
|
||||
id: build
|
||||
uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@d87225267726cf7ce1a3e17cf103c5ac943c4f05 # 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@d87225267726cf7ce1a3e17cf103c5ac943c4f05 # master
|
||||
with:
|
||||
oss-fuzz-project-name: 'pillow'
|
||||
fuzz-seconds: 600
|
||||
language: python
|
||||
dry-run: false
|
||||
- name: Upload New Crash
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
if: failure() && steps.build.outcome == 'success'
|
||||
with:
|
||||
name: artifacts
|
||||
path: ./out/artifacts
|
||||
- name: Upload Legacy Crash
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
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
|
||||
84
.github/workflows/docs.yml
vendored
84
.github/workflows/docs.yml
vendored
@ -1,84 +0,0 @@
|
||||
name: Docs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
paths: &paths
|
||||
- ".github/workflows/docs.yml"
|
||||
- "docs/**"
|
||||
- "src/PIL/**"
|
||||
pull_request:
|
||||
paths: *paths
|
||||
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
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 libavif
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
id: cache-libavif
|
||||
with:
|
||||
path: ~/cache-libavif
|
||||
key: ${{ runner.os }}-libavif-${{ hashFiles('depends/install_libavif.sh', 'depends/libavif-svt4.patch') }}
|
||||
|
||||
- name: Cache libimagequant
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
id: cache-libimagequant
|
||||
with:
|
||||
path: ~/cache-libimagequant
|
||||
key: ${{ runner.os }}-libimagequant-${{ hashFiles('depends/install_imagequant.sh') }}
|
||||
|
||||
- name: Cache libwebp
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
id: cache-libwebp
|
||||
with:
|
||||
path: ~/cache-libwebp
|
||||
key: ${{ runner.os }}-libwebp-${{ hashFiles('depends/install_webp.sh') }}
|
||||
|
||||
- name: Install Linux dependencies
|
||||
run: |
|
||||
.ci/install.sh
|
||||
env:
|
||||
GHA_PYTHON_VERSION: "3.x"
|
||||
GHA_LIBAVIF_CACHE_HIT: ${{ steps.cache-libavif.outputs.cache-hit }}
|
||||
GHA_LIBIMAGEQUANT_CACHE_HIT: ${{ steps.cache-libimagequant.outputs.cache-hit }}
|
||||
GHA_LIBWEBP_CACHE_HIT: ${{ steps.cache-libwebp.outputs.cache-hit }}
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
.ci/build.sh
|
||||
|
||||
- name: Docs
|
||||
run: |
|
||||
make doccheck
|
||||
32
.github/workflows/lint.yml
vendored
32
.github/workflows/lint.yml
vendored
@ -1,32 +0,0 @@
|
||||
name: Lint
|
||||
|
||||
on: [push, pull_request, workflow_dispatch]
|
||||
|
||||
permissions: {}
|
||||
|
||||
env:
|
||||
FORCE_COLOR: 1
|
||||
PREK_COLOR: always
|
||||
RUFF_OUTPUT_FORMAT: github
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
name: Lint
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.x"
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||
- name: Lint
|
||||
run: uvx --with tox-uv tox -e lint
|
||||
- name: Mypy
|
||||
run: uvx --with tox-uv tox -e mypy
|
||||
24
.github/workflows/macos-install.sh
vendored
24
.github/workflows/macos-install.sh
vendored
@ -1,24 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
brew bundle --file=.github/workflows/Brewfile
|
||||
export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
|
||||
|
||||
python3 -m pip install coverage
|
||||
python3 -m pip install defusedxml
|
||||
python3 -m pip install ipython
|
||||
python3 -m pip install olefile
|
||||
python3 -m pip install -U pytest
|
||||
python3 -m pip install -U pytest-cov
|
||||
python3 -m pip install -U pytest-timeout
|
||||
python3 -m pip install pyroma
|
||||
# optional test dependencies, only install if there's a binary package.
|
||||
python3 -m pip install --only-binary=:all: numpy || true
|
||||
python3 -m pip install --only-binary=:all: pyarrow || true
|
||||
|
||||
# libavif
|
||||
pushd depends && ./install_libavif.sh && popd
|
||||
|
||||
# extra test images
|
||||
pushd depends && ./install_extra_test_images.sh && popd
|
||||
31
.github/workflows/release-drafter.yml
vendored
31
.github/workflows/release-drafter.yml
vendored
@ -1,31 +0,0 @@
|
||||
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
|
||||
|
||||
env:
|
||||
FORCE_COLOR: 1
|
||||
|
||||
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@5de93583980a40bd78603b6dfdcda5b4df377b32 # v7.2.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
36
.github/workflows/stale.yml
vendored
36
.github/workflows/stale.yml
vendored
@ -1,36 +0,0 @@
|
||||
name: Close stale issues
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "10 0 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
FORCE_COLOR: 1
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
if: github.event.repository.fork == false
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: "Check issues"
|
||||
uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
|
||||
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"
|
||||
28
.github/workflows/system-info.py
vendored
28
.github/workflows/system-info.py
vendored
@ -1,28 +0,0 @@
|
||||
"""
|
||||
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())
|
||||
117
.github/workflows/test-docker.yml
vendored
117
.github/workflows/test-docker.yml
vendored
@ -1,117 +0,0 @@
|
||||
name: Test Docker
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
paths-ignore: &paths-ignore
|
||||
- ".github/workflows/docs.yml"
|
||||
- ".github/workflows/wheels*"
|
||||
- ".gitmodules"
|
||||
- "docs/**"
|
||||
- "wheels/**"
|
||||
pull_request:
|
||||
paths-ignore: *paths-ignore
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
FORCE_COLOR: 1
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: ["ubuntu-latest"]
|
||||
docker: [
|
||||
# Run slower jobs first to give them a headstart and reduce waiting time
|
||||
ubuntu-26.04-resolute-ppc64le,
|
||||
ubuntu-26.04-resolute-s390x,
|
||||
# Then run the remainder
|
||||
alpine,
|
||||
amazon-2023-amd64,
|
||||
arch,
|
||||
centos-stream-9-amd64,
|
||||
centos-stream-10-amd64,
|
||||
debian-13-trixie-x86,
|
||||
debian-13-trixie-amd64,
|
||||
fedora-43-amd64,
|
||||
fedora-44-amd64,
|
||||
gentoo,
|
||||
ubuntu-22.04-jammy-amd64,
|
||||
ubuntu-24.04-noble-amd64,
|
||||
ubuntu-26.04-resolute-amd64,
|
||||
]
|
||||
dockerTag: [main]
|
||||
include:
|
||||
- docker: "ubuntu-26.04-resolute-ppc64le"
|
||||
qemu-arch: "ppc64le"
|
||||
- docker: "ubuntu-26.04-resolute-s390x"
|
||||
qemu-arch: "s390x"
|
||||
- docker: "ubuntu-26.04-resolute-arm64v8"
|
||||
os: "ubuntu-24.04-arm"
|
||||
dockerTag: main
|
||||
|
||||
name: ${{ matrix.docker }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Build system information
|
||||
run: python3 .github/workflows/system-info.py
|
||||
|
||||
- name: Set up QEMU
|
||||
if: "matrix.qemu-arch"
|
||||
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
||||
with:
|
||||
platforms: ${{ matrix.qemu-arch }}
|
||||
|
||||
- name: Docker pull
|
||||
run: |
|
||||
docker pull ${{ matrix.qemu-arch && format('--platform=linux/{0}', matrix.qemu-arch)}} 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: |
|
||||
docker start pillow_container
|
||||
sudo docker cp pillow_container:/Pillow /Pillow
|
||||
sudo chown -R runner /Pillow
|
||||
pil_path=`docker exec pillow_container /vpy3/bin/python -c 'import os, PIL;print(os.path.realpath(os.path.dirname(PIL.__file__)))'`
|
||||
docker stop pillow_container
|
||||
sudo mkdir -p $pil_path
|
||||
sudo cp src/PIL/*.py $pil_path
|
||||
cd /Pillow
|
||||
.ci/after_success.sh
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
|
||||
with:
|
||||
flags: GHA_Docker
|
||||
name: ${{ matrix.docker }}
|
||||
|
||||
success:
|
||||
permissions:
|
||||
contents: none
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
name: Docker Test Successful
|
||||
steps:
|
||||
- name: Success
|
||||
run: echo Docker Test Successful
|
||||
89
.github/workflows/test-mingw.yml
vendored
89
.github/workflows/test-mingw.yml
vendored
@ -1,89 +0,0 @@
|
||||
name: Test MinGW
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
paths-ignore: &paths-ignore
|
||||
- ".github/workflows/docs.yml"
|
||||
- ".github/workflows/wheels*"
|
||||
- ".gitmodules"
|
||||
- "docs/**"
|
||||
- "wheels/**"
|
||||
pull_request:
|
||||
paths-ignore: *paths-ignore
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
COVERAGE_CORE: sysmon
|
||||
FORCE_COLOR: 1
|
||||
|
||||
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- 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-libavif \
|
||||
mingw-w64-x86_64-libimagequant \
|
||||
mingw-w64-x86_64-libjpeg-turbo \
|
||||
mingw-w64-x86_64-libraqm \
|
||||
mingw-w64-x86_64-libtiff \
|
||||
mingw-w64-x86_64-libwebp \
|
||||
mingw-w64-x86_64-openjpeg2 \
|
||||
mingw-w64-x86_64-python-numpy \
|
||||
mingw-w64-x86_64-python-olefile \
|
||||
mingw-w64-x86_64-python-pip \
|
||||
mingw-w64-x86_64-python-pytest \
|
||||
mingw-w64-x86_64-python-pytest-cov \
|
||||
mingw-w64-x86_64-python-pytest-timeout \
|
||||
mingw-w64-x86_64-python-pyqt6
|
||||
|
||||
pushd depends && ./install_extra_test_images.sh && popd
|
||||
|
||||
- name: Build Pillow
|
||||
run: CFLAGS="-coverage" python3 -m pip install .
|
||||
|
||||
- name: Test Pillow
|
||||
run: |
|
||||
python3 selftest.py --installed
|
||||
.ci/test.sh
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
|
||||
with:
|
||||
files: ./coverage.xml
|
||||
flags: GHA_Windows
|
||||
name: "MSYS2 MinGW"
|
||||
64
.github/workflows/test-valgrind-memory.yml
vendored
64
.github/workflows/test-valgrind-memory.yml
vendored
@ -1,64 +0,0 @@
|
||||
name: Test Valgrind Memory Leaks
|
||||
|
||||
# like the Docker tests, but running valgrind only on *.c/*.h changes.
|
||||
|
||||
# this is very expensive. Only run on the pull request.
|
||||
on:
|
||||
# push:
|
||||
# branches:
|
||||
# - "**"
|
||||
# paths:
|
||||
# - ".github/workflows/test-valgrind-memory.yml"
|
||||
# - "**.c"
|
||||
# - "**.h"
|
||||
# - "depends/docker-test-valgrind-memory.sh"
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/test-valgrind-memory.yml"
|
||||
- "**.c"
|
||||
- "**.h"
|
||||
- "depends/docker-test-valgrind-memory.sh"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
FORCE_COLOR: 1
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
docker: [
|
||||
ubuntu-22.04-jammy-amd64-valgrind,
|
||||
]
|
||||
dockerTag: [main]
|
||||
|
||||
name: ${{ matrix.docker }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Build system information
|
||||
run: python3 .github/workflows/system-info.py
|
||||
|
||||
- name: Docker pull
|
||||
run: |
|
||||
docker pull pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }}
|
||||
|
||||
- name: Build and Run Valgrind
|
||||
run: |
|
||||
# The Pillow user in the docker container is UID 1001
|
||||
sudo chown -R 1001 $GITHUB_WORKSPACE
|
||||
docker run --name pillow_container -e "PILLOW_VALGRIND_TEST=true" -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} /Pillow/depends/docker-test-valgrind-memory.sh
|
||||
sudo chown -R runner $GITHUB_WORKSPACE
|
||||
58
.github/workflows/test-valgrind.yml
vendored
58
.github/workflows/test-valgrind.yml
vendored
@ -1,58 +0,0 @@
|
||||
name: Test Valgrind
|
||||
|
||||
# like the Docker tests, but running valgrind only on *.c/*.h changes.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
paths: &paths
|
||||
- ".github/workflows/test-valgrind.yml"
|
||||
- "**.c"
|
||||
- "**.h"
|
||||
pull_request:
|
||||
paths: *paths
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
FORCE_COLOR: 1
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
docker: [
|
||||
ubuntu-22.04-jammy-amd64-valgrind,
|
||||
]
|
||||
dockerTag: [main]
|
||||
|
||||
name: ${{ matrix.docker }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Build system information
|
||||
run: python3 .github/workflows/system-info.py
|
||||
|
||||
- name: Docker pull
|
||||
run: |
|
||||
docker pull pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }}
|
||||
|
||||
- name: Build and Run Valgrind
|
||||
run: |
|
||||
# The Pillow user in the docker container is UID 1001
|
||||
sudo chown -R 1001 $GITHUB_WORKSPACE
|
||||
docker run --name pillow_container -e "PILLOW_VALGRIND_TEST=true" -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }}
|
||||
sudo chown -R runner $GITHUB_WORKSPACE
|
||||
241
.github/workflows/test-windows.yml
vendored
241
.github/workflows/test-windows.yml
vendored
@ -1,241 +0,0 @@
|
||||
name: Test Windows
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
paths-ignore: &paths-ignore
|
||||
- ".github/workflows/docs.yml"
|
||||
- ".github/workflows/wheels*"
|
||||
- ".gitmodules"
|
||||
- "docs/**"
|
||||
- "wheels/**"
|
||||
pull_request:
|
||||
paths-ignore: *paths-ignore
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
COVERAGE_CORE: sysmon
|
||||
FORCE_COLOR: 1
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["pypy3.11", "3.11", "3.12", "3.13", "3.14", "3.15"]
|
||||
architecture: ["x64"]
|
||||
os: ["windows-latest"]
|
||||
include:
|
||||
# Test the oldest Python on 32-bit
|
||||
- { python-version: "3.10", architecture: "x86", os: "windows-2022" }
|
||||
|
||||
timeout-minutes: 45
|
||||
|
||||
name: Python ${{ matrix.python-version }} (${{ matrix.architecture }})
|
||||
|
||||
steps:
|
||||
- name: Checkout Pillow
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Checkout cached dependencies
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
repository: python-pillow/pillow-depends
|
||||
path: winbuild\depends
|
||||
|
||||
- name: Checkout extra test images
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
repository: python-pillow/test-images
|
||||
path: Tests\test-images
|
||||
|
||||
# sets env: pythonLocation
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
allow-prereleases: true
|
||||
architecture: ${{ matrix.architecture }}
|
||||
cache: pip
|
||||
cache-dependency-path: ".github/workflows/test-windows.yml"
|
||||
|
||||
- name: Print build system information
|
||||
run: python3 .github/workflows/system-info.py
|
||||
|
||||
- name: Upgrade pip
|
||||
run: |
|
||||
python3 -m pip install --upgrade pip
|
||||
|
||||
- name: Install CPython dependencies
|
||||
if: "!contains(matrix.python-version, 'pypy') && matrix.architecture != 'x86'"
|
||||
run: |
|
||||
python3 -m pip install PyQt6
|
||||
|
||||
- name: Install PyArrow dependency
|
||||
run: |
|
||||
python3 -m pip install --only-binary=:all: pyarrow || true
|
||||
|
||||
- name: Install dependencies
|
||||
id: install
|
||||
run: |
|
||||
choco install nasm --no-progress
|
||||
echo "C:\Program Files\NASM" >> $env:GITHUB_PATH
|
||||
|
||||
choco install ghostscript --version=10.7.0 --no-progress
|
||||
echo "C:\Program Files\gs\gs10.07.0\bin" >> $env:GITHUB_PATH
|
||||
|
||||
# Install extra test images
|
||||
xcopy /S /Y Tests\test-images\* Tests\images
|
||||
|
||||
# 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@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
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"
|
||||
|
||||
- name: Build dependencies / libavif
|
||||
if: steps.build-cache.outputs.cache-hit != 'true' && matrix.architecture == 'x64'
|
||||
run: "& winbuild\\build\\build_dep_libavif.cmd"
|
||||
|
||||
# for FreeType WOFF2 font support
|
||||
- name: Build dependencies / brotli
|
||||
if: steps.build-cache.outputs.cache-hit != 'true'
|
||||
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: |
|
||||
rm -rf winbuild\build\src
|
||||
shell: bash
|
||||
|
||||
- name: Build Pillow
|
||||
run: |
|
||||
$FLAGS="-C raqm=vendor -C fribidi=vendor"
|
||||
cmd /c "winbuild\build\build_env.cmd && $env:pythonLocation\python.exe -m pip install -v $FLAGS .[tests]"
|
||||
& $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: |
|
||||
.ci\test.cmd
|
||||
|
||||
- name: Prepare to upload errors
|
||||
if: failure()
|
||||
run: |
|
||||
mkdir -p Tests/errors
|
||||
shell: bash
|
||||
|
||||
- name: Upload errors
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
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@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
|
||||
with:
|
||||
files: ./coverage.xml
|
||||
flags: GHA_Windows
|
||||
name: ${{ runner.os }} Python ${{ matrix.python-version }}
|
||||
|
||||
success:
|
||||
permissions:
|
||||
contents: none
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
name: Windows Test Successful
|
||||
steps:
|
||||
- name: Success
|
||||
run: echo Windows Test Successful
|
||||
174
.github/workflows/test.yml
vendored
174
.github/workflows/test.yml
vendored
@ -1,174 +0,0 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
paths-ignore: &paths-ignore
|
||||
- ".github/workflows/docs.yml"
|
||||
- ".github/workflows/wheels*"
|
||||
- ".gitmodules"
|
||||
- "docs/**"
|
||||
- "wheels/**"
|
||||
pull_request:
|
||||
paths-ignore: *paths-ignore
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
COVERAGE_CORE: sysmon
|
||||
FORCE_COLOR: 1
|
||||
PIP_DISABLE_PIP_VERSION_CHECK: 1
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [
|
||||
"macos-latest",
|
||||
"ubuntu-latest",
|
||||
]
|
||||
python-version: [
|
||||
"pypy3.11",
|
||||
"3.15t",
|
||||
"3.15",
|
||||
"3.14t",
|
||||
"3.14",
|
||||
"3.13",
|
||||
"3.12",
|
||||
"3.11",
|
||||
"3.10",
|
||||
]
|
||||
include:
|
||||
- { python-version: "3.12", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" }
|
||||
- { python-version: "3.11", PYTHONOPTIMIZE: 2 }
|
||||
# Intel
|
||||
- { os: "macos-26-intel", python-version: "3.10" }
|
||||
exclude:
|
||||
- { os: "macos-latest", python-version: "3.10" }
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
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 libavif
|
||||
if: startsWith(matrix.os, 'ubuntu')
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
id: cache-libavif
|
||||
with:
|
||||
path: ~/cache-libavif
|
||||
key: ${{ runner.os }}-libavif-${{ hashFiles('depends/install_libavif.sh', 'depends/libavif-svt4.patch') }}
|
||||
|
||||
- name: Cache libimagequant
|
||||
if: startsWith(matrix.os, 'ubuntu')
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
id: cache-libimagequant
|
||||
with:
|
||||
path: ~/cache-libimagequant
|
||||
key: ${{ runner.os }}-libimagequant-${{ hashFiles('depends/install_imagequant.sh') }}
|
||||
|
||||
- name: Cache libwebp
|
||||
if: startsWith(matrix.os, 'ubuntu')
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
id: cache-libwebp
|
||||
with:
|
||||
path: ~/cache-libwebp
|
||||
key: ${{ runner.os }}-libwebp-${{ hashFiles('depends/install_webp.sh') }}
|
||||
|
||||
- name: Install Linux dependencies
|
||||
if: startsWith(matrix.os, 'ubuntu')
|
||||
run: |
|
||||
.ci/install.sh
|
||||
env:
|
||||
GHA_PYTHON_VERSION: ${{ matrix.python-version }}
|
||||
GHA_LIBAVIF_CACHE_HIT: ${{ steps.cache-libavif.outputs.cache-hit }}
|
||||
GHA_LIBIMAGEQUANT_CACHE_HIT: ${{ steps.cache-libimagequant.outputs.cache-hit }}
|
||||
GHA_LIBWEBP_CACHE_HIT: ${{ steps.cache-libwebp.outputs.cache-hit }}
|
||||
|
||||
- name: Install macOS dependencies
|
||||
if: startsWith(matrix.os, 'macOS')
|
||||
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.14'"
|
||||
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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
if: failure()
|
||||
with:
|
||||
name: errors
|
||||
path: Tests/errors
|
||||
|
||||
- name: After success
|
||||
run: |
|
||||
.ci/after_success.sh
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
|
||||
with:
|
||||
flags: ${{ matrix.os == 'ubuntu-latest' && 'GHA_Ubuntu' || 'GHA_macOS' }}
|
||||
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
|
||||
|
||||
success:
|
||||
permissions:
|
||||
contents: none
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
name: Test Successful
|
||||
steps:
|
||||
- name: Success
|
||||
run: echo Test Successful
|
||||
414
.github/workflows/wheels-dependencies.sh
vendored
414
.github/workflows/wheels-dependencies.sh
vendored
@ -1,414 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Safety check - Pillow builds require that CIBW_ARCHS is set, and that it only
|
||||
# contains a single value (even though cibuildwheel allows multiple values in
|
||||
# CIBW_ARCHS). This check doesn't work on Linux because of how the CIBW_ARCHS
|
||||
# variable is exposed.
|
||||
function check_cibw_archs {
|
||||
if [[ -z "$CIBW_ARCHS" ]]; then
|
||||
echo "ERROR: Pillow builds require CIBW_ARCHS be defined."
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$CIBW_ARCHS" == *" "* ]]; then
|
||||
echo "ERROR: Pillow builds only support a single architecture in CIBW_ARCHS."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Setup that needs to be done before multibuild utils are invoked. Process
|
||||
# potential cross-build platforms before native platforms to ensure that we pick
|
||||
# up the cross environment.
|
||||
PROJECTDIR=$(pwd)
|
||||
if [[ "$CIBW_PLATFORM" == "ios" ]]; then
|
||||
check_cibw_archs
|
||||
# On iOS, CIBW_ARCHS is actually a multi-arch - arm64_iphoneos,
|
||||
# arm64_iphonesimulator or x86_64_iphonesimulator. Split into the CPU
|
||||
# platform, and the iOS SDK.
|
||||
PLAT=$(echo $CIBW_ARCHS | sed "s/\(.*\)_\(.*\)/\1/")
|
||||
IOS_SDK=$(echo $CIBW_ARCHS | sed "s/\(.*\)_\(.*\)/\2/")
|
||||
|
||||
# Build iOS builds in `build/iphoneos` or `build/iphonesimulator`
|
||||
# (depending on the build target). Install them into `build/deps/iphoneos`
|
||||
# or `build/deps/iphonesimulator`
|
||||
WORKDIR=$(pwd)/build/$IOS_SDK
|
||||
BUILD_PREFIX=$(pwd)/build/deps/$IOS_SDK
|
||||
|
||||
# GNU tooling insists on using aarch64 rather than arm64
|
||||
if [[ $PLAT == "arm64" ]]; then
|
||||
GNU_ARCH=aarch64
|
||||
else
|
||||
GNU_ARCH=x86_64
|
||||
fi
|
||||
|
||||
IOS_SDK_PATH=$(xcrun --sdk $IOS_SDK --show-sdk-path)
|
||||
CMAKE_SYSTEM_NAME=iOS
|
||||
IOS_HOST_TRIPLE=$PLAT-apple-ios$IPHONEOS_DEPLOYMENT_TARGET
|
||||
if [[ "$IOS_SDK" == "iphonesimulator" ]]; then
|
||||
IOS_HOST_TRIPLE=$IOS_HOST_TRIPLE-simulator
|
||||
fi
|
||||
|
||||
# GNU Autotools doesn't recognize the existence of arm64-apple-ios-simulator
|
||||
# as a valid host. However, the only difference between arm64-apple-ios and
|
||||
# arm64-apple-ios-simulator is the choice of sysroot, and that is
|
||||
# coordinated by CC, CFLAGS etc. From the perspective of configure, the two
|
||||
# platforms are identical, so we can use arm64-apple-ios consistently.
|
||||
# This (mostly) avoids us needing to patch config.sub in dependency sources.
|
||||
HOST_CONFIGURE_FLAGS="--disable-shared --enable-static --host=$GNU_ARCH-apple-ios --build=$GNU_ARCH-apple-darwin"
|
||||
|
||||
# CMake has native support for iOS. However, most of that support is based
|
||||
# on using the Xcode builder, which isn't very helpful for most of Pillow's
|
||||
# dependencies. Therefore, we lean on the OSX configurations, plus CC, CFLAGS
|
||||
# etc. to ensure the right sysroot is selected.
|
||||
HOST_CMAKE_FLAGS="-DCMAKE_SYSTEM_NAME=$CMAKE_SYSTEM_NAME -DCMAKE_SYSTEM_PROCESSOR=$GNU_ARCH -DCMAKE_OSX_DEPLOYMENT_TARGET=$IPHONEOS_DEPLOYMENT_TARGET -DCMAKE_OSX_SYSROOT=$IOS_SDK_PATH -DBUILD_SHARED_LIBS=NO -DENABLE_SHARED=NO"
|
||||
|
||||
# Meson needs to be pointed at a cross-platform configuration file
|
||||
# This will be generated once CC etc. have been evaluated.
|
||||
HOST_MESON_FLAGS="--cross-file $WORKDIR/meson-cross.txt -Dprefer_static=true -Ddefault_library=static"
|
||||
|
||||
elif [[ "$(uname -s)" == "Darwin" ]]; then
|
||||
check_cibw_archs
|
||||
# Build macOS dependencies in `build/darwin`
|
||||
# Install them into `build/deps/darwin`
|
||||
PLAT=$CIBW_ARCHS
|
||||
WORKDIR=$(pwd)/build/darwin
|
||||
BUILD_PREFIX=$(pwd)/build/deps/darwin
|
||||
else
|
||||
# Build prefix will default to /usr/local
|
||||
PLAT="${CIBW_ARCHS:-$AUDITWHEEL_ARCH}"
|
||||
WORKDIR=$(pwd)/build
|
||||
MB_ML_LIBC=${AUDITWHEEL_POLICY::9}
|
||||
MB_ML_VER=${AUDITWHEEL_POLICY:9}
|
||||
fi
|
||||
|
||||
# Define custom utilities
|
||||
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
|
||||
|
||||
VERSIONS_FILE="$PROJECTDIR/.github/dependencies.json"
|
||||
_get_ver() { python3 -c "import json; print(json.load(open('$VERSIONS_FILE'))['$1'])"; }
|
||||
FREETYPE_VERSION=$(_get_ver freetype)
|
||||
HARFBUZZ_VERSION=$(_get_ver harfbuzz)
|
||||
LIBPNG_VERSION=$(_get_ver libpng)
|
||||
JPEGTURBO_VERSION=$(_get_ver jpegturbo)
|
||||
OPENJPEG_VERSION=$(_get_ver openjpeg)
|
||||
XZ_VERSION=$(_get_ver xz)
|
||||
ZSTD_VERSION=$(_get_ver zstd)
|
||||
TIFF_VERSION=$(_get_ver tiff)
|
||||
LCMS2_VERSION=$(_get_ver lcms2)
|
||||
ZLIB_NG_VERSION=$(_get_ver zlib-ng)
|
||||
LIBWEBP_VERSION=$(_get_ver libwebp)
|
||||
BZIP2_VERSION=$(_get_ver bzip2)
|
||||
LIBXCB_VERSION=$(_get_ver libxcb)
|
||||
BROTLI_VERSION=$(_get_ver brotli)
|
||||
LIBAVIF_VERSION=$(_get_ver libavif)
|
||||
|
||||
function build_pkg_config {
|
||||
if [ -e pkg-config-stamp ]; then return; fi
|
||||
# This essentially duplicates the Homebrew recipe.
|
||||
# On iOS, we need a binary that can be executed on the build machine; but we
|
||||
# can create a host-specific pc-path to store iOS .pc files. To ensure a
|
||||
# macOS-compatible build, we temporarily clear environment flags that set
|
||||
# iOS-specific values.
|
||||
if [[ -n "$IOS_SDK" ]]; then
|
||||
ORIGINAL_HOST_CONFIGURE_FLAGS=$HOST_CONFIGURE_FLAGS
|
||||
ORIGINAL_IPHONEOS_DEPLOYMENT_TARGET=$IPHONEOS_DEPLOYMENT_TARGET
|
||||
unset HOST_CONFIGURE_FLAGS
|
||||
unset IPHONEOS_DEPLOYMENT_TARGET
|
||||
fi
|
||||
|
||||
CFLAGS="$CFLAGS -Wno-int-conversion" CPPFLAGS="" build_simple pkg-config 0.29.2 https://pkg-config.freedesktop.org/releases tar.gz \
|
||||
--disable-debug --disable-host-tool --with-internal-glib \
|
||||
--with-pc-path=$BUILD_PREFIX/share/pkgconfig:$BUILD_PREFIX/lib/pkgconfig \
|
||||
--with-system-include-path=$(xcrun --show-sdk-path --sdk macosx)/usr/include
|
||||
|
||||
if [[ -n "$IOS_SDK" ]]; then
|
||||
HOST_CONFIGURE_FLAGS=$ORIGINAL_HOST_CONFIGURE_FLAGS
|
||||
IPHONEOS_DEPLOYMENT_TARGET=$ORIGINAL_IPHONEOS_DEPLOYMENT_TARGET
|
||||
fi;
|
||||
|
||||
export PKG_CONFIG=$BUILD_PREFIX/bin/pkg-config
|
||||
touch pkg-config-stamp
|
||||
}
|
||||
|
||||
function build_zlib_ng {
|
||||
if [ -e zlib-stamp ]; then return; fi
|
||||
# zlib-ng uses a "configure" script, but it's not a GNU autotools script, so
|
||||
# it doesn't honor the usual flags. Temporarily disable any
|
||||
# cross-compilation flags.
|
||||
ORIGINAL_HOST_CONFIGURE_FLAGS=$HOST_CONFIGURE_FLAGS
|
||||
unset HOST_CONFIGURE_FLAGS
|
||||
|
||||
build_github zlib-ng/zlib-ng $ZLIB_NG_VERSION --installnamedir=$BUILD_PREFIX/lib --zlib-compat
|
||||
|
||||
HOST_CONFIGURE_FLAGS=$ORIGINAL_HOST_CONFIGURE_FLAGS
|
||||
touch zlib-stamp
|
||||
}
|
||||
|
||||
function build_brotli {
|
||||
if [ -e brotli-stamp ]; then return; fi
|
||||
local out_dir=$(fetch_unpack https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-$BROTLI_VERSION.tar.gz)
|
||||
(cd $out_dir \
|
||||
&& cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib -DCMAKE_MACOSX_BUNDLE=OFF $HOST_CMAKE_FLAGS . \
|
||||
&& make -j4 install)
|
||||
touch brotli-stamp
|
||||
}
|
||||
|
||||
function build_harfbuzz {
|
||||
if [ -e harfbuzz-stamp ]; then return; fi
|
||||
python3 -m pip install meson ninja
|
||||
|
||||
local out_dir=$(fetch_unpack https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION/harfbuzz-$HARFBUZZ_VERSION.tar.xz harfbuzz-$HARFBUZZ_VERSION.tar.xz)
|
||||
(cd $out_dir \
|
||||
&& meson setup build --prefix=$BUILD_PREFIX --libdir=$BUILD_PREFIX/lib --buildtype=minsize -Dfreetype=enabled -Dglib=disabled -Dtests=disabled $HOST_MESON_FLAGS)
|
||||
(cd $out_dir/build \
|
||||
&& meson install)
|
||||
touch harfbuzz-stamp
|
||||
}
|
||||
|
||||
function build_libavif {
|
||||
if [ -e libavif-stamp ]; then return; fi
|
||||
|
||||
python3 -m pip install meson ninja
|
||||
|
||||
if ([[ "$PLAT" == "x86_64" ]] && [[ -z "$IOS_SDK" ]]) || [ -n "$SANITIZER" ]; then
|
||||
build_simple nasm 2.16.03 https://www.nasm.us/pub/nasm/releasebuilds/2.16.03
|
||||
fi
|
||||
|
||||
local build_shared=ON
|
||||
local lto=ON
|
||||
|
||||
local libavif_cmake_flags
|
||||
|
||||
if [[ -n "$IS_MACOS" ]]; then
|
||||
lto=OFF
|
||||
libavif_cmake_flags=(
|
||||
-DCMAKE_C_FLAGS_MINSIZEREL="-Oz -DNDEBUG -flto" \
|
||||
-DCMAKE_CXX_FLAGS_MINSIZEREL="-Oz -DNDEBUG -flto" \
|
||||
-DCMAKE_SHARED_LINKER_FLAGS_INIT="-Wl,-S,-x,-dead_strip_dylibs" \
|
||||
)
|
||||
if [[ -n "$IOS_SDK" ]]; then
|
||||
build_shared=OFF
|
||||
fi
|
||||
else
|
||||
libavif_cmake_flags=(-DCMAKE_SHARED_LINKER_FLAGS_INIT="-Wl,--strip-all,-z,relro,-z,now")
|
||||
fi
|
||||
if [[ -n "$IOS_SDK" ]] && [[ "$PLAT" == "x86_64" ]]; then
|
||||
libavif_cmake_flags+=(-DAOM_TARGET_CPU=generic)
|
||||
else
|
||||
libavif_cmake_flags+=(
|
||||
-DAVIF_CODEC_AOM_DECODE=OFF \
|
||||
-DAVIF_CODEC_DAV1D=LOCAL
|
||||
)
|
||||
fi
|
||||
|
||||
local out_dir=$(fetch_unpack https://github.com/AOMediaCodec/libavif/archive/refs/tags/v$LIBAVIF_VERSION.tar.gz libavif-$LIBAVIF_VERSION.tar.gz)
|
||||
|
||||
# CONFIG_AV1_HIGHBITDEPTH=0 is a flag for libaom (included as a subproject
|
||||
# of libavif) that disables support for encoding high bit depth images.
|
||||
(cd $out_dir \
|
||||
&& cmake \
|
||||
-DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX \
|
||||
-DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib \
|
||||
-DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib \
|
||||
-DBUILD_SHARED_LIBS=$build_shared \
|
||||
-DAVIF_LIBSHARPYUV=LOCAL \
|
||||
-DAVIF_LIBYUV=LOCAL \
|
||||
-DAVIF_CODEC_AOM=LOCAL \
|
||||
-DCONFIG_AV1_HIGHBITDEPTH=0 \
|
||||
-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=$lto \
|
||||
-DCMAKE_C_VISIBILITY_PRESET=hidden \
|
||||
-DCMAKE_CXX_VISIBILITY_PRESET=hidden \
|
||||
-DCMAKE_BUILD_TYPE=MinSizeRel \
|
||||
"${libavif_cmake_flags[@]}" \
|
||||
$HOST_CMAKE_FLAGS . )
|
||||
|
||||
if [[ -n "$IOS_SDK" ]]; then
|
||||
# libavif's CMake configuration generates a meson cross file... but it
|
||||
# doesn't work for iOS cross-compilation. Copy in Pillow-generated
|
||||
# meson-cross config to replace the cmake-generated version.
|
||||
cp $WORKDIR/meson-cross.txt $out_dir/crossfile-apple.meson
|
||||
fi
|
||||
|
||||
(cd $out_dir && make -j4 install)
|
||||
|
||||
touch libavif-stamp
|
||||
}
|
||||
|
||||
function build_zstd {
|
||||
if [ -e zstd-stamp ]; then return; fi
|
||||
local out_dir=$(fetch_unpack https://github.com/facebook/zstd/releases/download/v$ZSTD_VERSION/zstd-$ZSTD_VERSION.tar.gz)
|
||||
(cd $out_dir \
|
||||
&& make -j4 install)
|
||||
touch zstd-stamp
|
||||
}
|
||||
|
||||
function build {
|
||||
build_xz
|
||||
if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then
|
||||
yum remove -y zlib-devel
|
||||
fi
|
||||
if [[ -n "$IS_MACOS" ]]; then
|
||||
CFLAGS="$CFLAGS -headerpad_max_install_names" build_zlib_ng
|
||||
else
|
||||
build_zlib_ng
|
||||
fi
|
||||
|
||||
build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto
|
||||
if [[ -n "$IS_MACOS" ]]; then
|
||||
build_simple xorgproto 2025.1 https://www.x.org/pub/individual/proto
|
||||
build_simple libXau 1.0.12 https://www.x.org/pub/individual/lib
|
||||
build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist
|
||||
else
|
||||
sed "s/\${pc_sysrootdir\}//" $BUILD_PREFIX/share/pkgconfig/xcb-proto.pc > $BUILD_PREFIX/lib/pkgconfig/xcb-proto.pc
|
||||
fi
|
||||
build_simple libxcb $LIBXCB_VERSION https://www.x.org/releases/individual/lib
|
||||
|
||||
build_libjpeg_turbo
|
||||
if [[ -n "$IS_MACOS" ]]; then
|
||||
# Custom tiff build to include jpeg; by default, configure won't include
|
||||
# headers/libs in the custom macOS/iOS prefix. Explicitly disable webp,
|
||||
# libdeflate and zstd, because on x86_64 macs, it will pick up the
|
||||
# Homebrew versions of those libraries from /usr/local.
|
||||
build_simple tiff $TIFF_VERSION https://download.osgeo.org/libtiff tar.gz \
|
||||
--with-jpeg-include-dir=$BUILD_PREFIX/include --with-jpeg-lib-dir=$BUILD_PREFIX/lib \
|
||||
--disable-webp --disable-libdeflate --disable-zstd
|
||||
else
|
||||
build_zstd
|
||||
build_tiff
|
||||
fi
|
||||
|
||||
build_libavif
|
||||
build_libpng
|
||||
build_lcms2
|
||||
build_openjpeg
|
||||
|
||||
webp_cflags="-O3 -DNDEBUG"
|
||||
if [[ -n "$IS_MACOS" ]]; then
|
||||
webp_cflags="$webp_cflags -Wl,-headerpad_max_install_names"
|
||||
fi
|
||||
webp_ldflags=""
|
||||
if [[ -n "$IOS_SDK" ]]; then
|
||||
webp_ldflags="$webp_ldflags -llzma -lz"
|
||||
fi
|
||||
CFLAGS="$CFLAGS $webp_cflags" LDFLAGS="$LDFLAGS $webp_ldflags" build_simple libwebp $LIBWEBP_VERSION \
|
||||
https://storage.googleapis.com/downloads.webmproject.org/releases/webp tar.gz \
|
||||
--enable-libwebpmux --enable-libwebpdemux
|
||||
|
||||
build_brotli
|
||||
|
||||
if [[ -n "$IS_MACOS" ]]; then
|
||||
# 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 "$IOS_SDK" ]]; then
|
||||
# On iOS, there's no vendor-provided raqm, and we can't ship it due to
|
||||
# licensing, so there's no point building harfbuzz.
|
||||
build_harfbuzz
|
||||
fi
|
||||
}
|
||||
|
||||
function create_meson_cross_config {
|
||||
cat << EOF > $WORKDIR/meson-cross.txt
|
||||
[binaries]
|
||||
pkg-config = '$BUILD_PREFIX/bin/pkg-config'
|
||||
cmake = '$(which cmake)'
|
||||
c = '$CC'
|
||||
cpp = '$CXX'
|
||||
strip = '$STRIP'
|
||||
|
||||
[built-in options]
|
||||
c_args = '$CFLAGS -I$BUILD_PREFIX/include'
|
||||
cpp_args = '$CXXFLAGS -I$BUILD_PREFIX/include'
|
||||
c_link_args = '$CFLAGS -L$BUILD_PREFIX/lib'
|
||||
cpp_link_args = '$CFLAGS -L$BUILD_PREFIX/lib'
|
||||
|
||||
[host_machine]
|
||||
system = 'darwin'
|
||||
subsystem = 'ios'
|
||||
kernel = 'xnu'
|
||||
cpu_family = '$(uname -m)'
|
||||
cpu = '$(uname -m)'
|
||||
endian = 'little'
|
||||
|
||||
EOF
|
||||
}
|
||||
|
||||
# Perform all dependency builds in the build subfolder.
|
||||
mkdir -p $WORKDIR
|
||||
pushd $WORKDIR > /dev/null
|
||||
|
||||
# Any stuff that you need to do before you start building the wheels
|
||||
# Runs in the root directory of this repository.
|
||||
if [[ ! -d $WORKDIR/pillow-depends-main ]]; then
|
||||
if [[ ! -f $PROJECTDIR/pillow-depends-main.zip ]]; then
|
||||
echo "Download pillow dependency sources..."
|
||||
curl -fSL -o $PROJECTDIR/pillow-depends-main.zip https://github.com/python-pillow/pillow-depends/archive/main.zip
|
||||
fi
|
||||
echo "Unpacking pillow dependency sources..."
|
||||
untar $PROJECTDIR/pillow-depends-main.zip
|
||||
fi
|
||||
|
||||
if [[ -n "$IS_MACOS" ]]; then
|
||||
# Ensure the basic structure of the build prefix directory exists.
|
||||
mkdir -p "$BUILD_PREFIX/bin"
|
||||
mkdir -p "$BUILD_PREFIX/lib"
|
||||
|
||||
# Ensure pkg-config is available. This is done *before* setting CC, CFLAGS
|
||||
# etc. to ensure that the build is *always* a macOS build, even when building
|
||||
# for iOS.
|
||||
build_pkg_config
|
||||
|
||||
# Ensure cmake is available, and that the default prefix used by CMake is
|
||||
# the build prefix
|
||||
python3 -m pip install cmake
|
||||
export CMAKE_PREFIX_PATH=$BUILD_PREFIX
|
||||
|
||||
if [[ -n "$IOS_SDK" ]]; then
|
||||
export AR="$(xcrun --find --sdk $IOS_SDK ar)"
|
||||
export CPP="$(xcrun --find --sdk $IOS_SDK clang) -E"
|
||||
export CC=$(xcrun --find --sdk $IOS_SDK clang)
|
||||
export CXX=$(xcrun --find --sdk $IOS_SDK clang++)
|
||||
export LD=$(xcrun --find --sdk $IOS_SDK ld)
|
||||
export STRIP=$(xcrun --find --sdk $IOS_SDK strip)
|
||||
|
||||
CPPFLAGS="$CPPFLAGS --sysroot=$IOS_SDK_PATH"
|
||||
CFLAGS="-target $IOS_HOST_TRIPLE --sysroot=$IOS_SDK_PATH -mios-version-min=$IPHONEOS_DEPLOYMENT_TARGET"
|
||||
CXXFLAGS="-target $IOS_HOST_TRIPLE --sysroot=$IOS_SDK_PATH -mios-version-min=$IPHONEOS_DEPLOYMENT_TARGET"
|
||||
|
||||
# Having IPHONEOS_DEPLOYMENT_TARGET in the environment causes problems
|
||||
# with some cross-building toolchains, because it introduces implicit
|
||||
# behavior into clang.
|
||||
unset IPHONEOS_DEPLOYMENT_TARGET
|
||||
|
||||
# Now that we know CC etc., we can create a meson cross-configuration file
|
||||
create_meson_cross_config
|
||||
fi
|
||||
fi
|
||||
|
||||
wrap_wheel_builder build
|
||||
|
||||
# A safety catch for iOS. iOS can't use dynamic libraries, but clang will prefer
|
||||
# to link dynamic libraries to static libraries. The only way to reliably
|
||||
# prevent this is to not have dynamic libraries available in the first place.
|
||||
# The build process *shouldn't* generate any dylibs... but just in case, purge
|
||||
# any dylibs that *have* been installed into the build prefix directory.
|
||||
if [[ -n "$IOS_SDK" ]]; then
|
||||
find "$BUILD_PREFIX" -name "*.dylib" -exec rm -rf {} \;
|
||||
fi
|
||||
|
||||
# Return to the project root to finish the build
|
||||
popd > /dev/null
|
||||
|
||||
# Append licenses
|
||||
for filename in wheels/dependency_licenses/*; do
|
||||
echo -e "\n\n----\n\n$(basename $filename | cut -f 1 -d '.')\n" | cat >> LICENSE
|
||||
cat $filename >> LICENSE
|
||||
done
|
||||
26
.github/workflows/wheels-test.ps1
vendored
26
.github/workflows/wheels-test.ps1
vendored
@ -1,26 +0,0 @@
|
||||
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\"
|
||||
if (Test-Path $venv\Scripts\pypy.exe) {
|
||||
$python = "pypy.exe"
|
||||
} else {
|
||||
$python = "python.exe"
|
||||
}
|
||||
& reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\python.exe" /v "GlobalFlag" /t REG_SZ /d "0x02000000" /f
|
||||
cd $pillow
|
||||
& $venv\Scripts\$python -VV
|
||||
if (!$?) { exit $LASTEXITCODE }
|
||||
& $venv\Scripts\$python selftest.py
|
||||
if (!$?) { exit $LASTEXITCODE }
|
||||
& $venv\Scripts\$python -m pytest -vv -x checks\check_wheel.py
|
||||
if (!$?) { exit $LASTEXITCODE }
|
||||
& $venv\Scripts\$python -m pytest -vv -x Tests
|
||||
if (!$?) { exit $LASTEXITCODE }
|
||||
37
.github/workflows/wheels-test.sh
vendored
37
.github/workflows/wheels-test.sh
vendored
@ -1,37 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Ensure fribidi is installed by the system.
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
# If Homebrew is on the path during the build, it may leak into the wheels.
|
||||
# However, we *do* need Homebrew to provide a copy of fribidi for
|
||||
# testing purposes so that we can verify the fribidi shim works as expected.
|
||||
if [[ "$(uname -m)" == "x86_64" ]]; then
|
||||
HOMEBREW_PREFIX=/usr/local
|
||||
else
|
||||
HOMEBREW_PREFIX=/opt/homebrew
|
||||
fi
|
||||
$HOMEBREW_PREFIX/bin/brew install fribidi
|
||||
|
||||
# Add the lib folder for fribidi so that the vendored library can be found.
|
||||
# Don't use $HOMEWBREW_PREFIX/lib directly - use the lib folder where the
|
||||
# installed copy of fribidi is cellared. This ensures we don't pick up the
|
||||
# Homebrew version of any other library that we're dependent on (most notably,
|
||||
# freetype).
|
||||
export DYLD_LIBRARY_PATH=$(dirname $(realpath $HOMEBREW_PREFIX/lib/libfribidi.dylib))
|
||||
elif [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then
|
||||
apk add curl fribidi
|
||||
else
|
||||
yum install -y fribidi
|
||||
fi
|
||||
|
||||
if [ ! -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 -vv -x checks/check_wheel.py
|
||||
python3 -m pytest -vv -x
|
||||
368
.github/workflows/wheels.yml
vendored
368
.github/workflows/wheels.yml
vendored
@ -1,368 +0,0 @@
|
||||
name: Wheels
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# ┌───────────── minute (0 - 59)
|
||||
# │ ┌───────────── hour (0 - 23)
|
||||
# │ │ ┌───────────── day of the month (1 - 31)
|
||||
# │ │ │ ┌───────────── month (1 - 12 or JAN-DEC)
|
||||
# │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
|
||||
# │ │ │ │ │
|
||||
- cron: "42 1 * * 0,3"
|
||||
push:
|
||||
paths: &paths
|
||||
- ".ci/requirements-cibw.txt"
|
||||
- ".ci/requirements-sbom.txt"
|
||||
- ".github/compare-dist-sizes.py"
|
||||
- ".github/dependencies.json"
|
||||
- ".github/generate-sbom.py"
|
||||
- ".github/workflows/wheels*"
|
||||
- "pyproject.toml"
|
||||
- "setup.py"
|
||||
- "wheels/*"
|
||||
- "winbuild/build_prepare.py"
|
||||
- "winbuild/fribidi.cmake"
|
||||
tags:
|
||||
- "*"
|
||||
pull_request:
|
||||
paths: *paths
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
EXPECTED_DISTS: 66
|
||||
FORCE_COLOR: 1
|
||||
|
||||
jobs:
|
||||
build-native-wheels:
|
||||
if: github.event_name != 'schedule' || github.event.repository.fork == false
|
||||
name: ${{ matrix.name }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- name: "macOS 10.10 x86_64"
|
||||
platform: macos
|
||||
os: macos-26-intel
|
||||
cibw_arch: x86_64
|
||||
build: "cp3{10,11}*"
|
||||
macosx_deployment_target: "10.10"
|
||||
- name: "macOS 10.13 x86_64"
|
||||
platform: macos
|
||||
os: macos-26-intel
|
||||
cibw_arch: x86_64
|
||||
build: "cp3{12,13}*"
|
||||
macosx_deployment_target: "10.13"
|
||||
- name: "macOS 10.15 x86_64"
|
||||
platform: macos
|
||||
os: macos-26-intel
|
||||
cibw_arch: x86_64
|
||||
build: "{cp314,pp3}*"
|
||||
macosx_deployment_target: "10.15"
|
||||
- name: "macOS arm64"
|
||||
platform: macos
|
||||
os: macos-latest
|
||||
cibw_arch: arm64
|
||||
macosx_deployment_target: "11.0"
|
||||
- name: "manylinux_2_28 x86_64"
|
||||
platform: linux
|
||||
os: ubuntu-latest
|
||||
cibw_arch: x86_64
|
||||
build: "*manylinux*"
|
||||
- name: "musllinux x86_64"
|
||||
platform: linux
|
||||
os: ubuntu-latest
|
||||
cibw_arch: x86_64
|
||||
build: "*musllinux*"
|
||||
- name: "manylinux_2_28 aarch64"
|
||||
platform: linux
|
||||
os: ubuntu-24.04-arm
|
||||
cibw_arch: aarch64
|
||||
build: "*manylinux*"
|
||||
- name: "musllinux aarch64"
|
||||
platform: linux
|
||||
os: ubuntu-24.04-arm
|
||||
cibw_arch: aarch64
|
||||
build: "*musllinux*"
|
||||
- name: "iOS arm64 device"
|
||||
platform: ios
|
||||
os: macos-latest
|
||||
cibw_arch: arm64_iphoneos
|
||||
- name: "iOS arm64 simulator"
|
||||
platform: ios
|
||||
os: macos-latest
|
||||
cibw_arch: arm64_iphonesimulator
|
||||
- name: "iOS x86_64 simulator"
|
||||
platform: ios
|
||||
os: macos-26-intel
|
||||
cibw_arch: x86_64_iphonesimulator
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: true
|
||||
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
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_PLATFORM: ${{ matrix.platform }}
|
||||
CIBW_ARCHS: ${{ matrix.cibw_arch }}
|
||||
CIBW_BUILD: ${{ matrix.build }}
|
||||
CIBW_ENABLE: cpython-prerelease pypy
|
||||
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }}
|
||||
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: dist-${{ matrix.name }}
|
||||
path: ./wheelhouse/*.whl
|
||||
|
||||
windows:
|
||||
if: github.event_name != 'schedule' || github.event.repository.fork == false
|
||||
name: Windows ${{ matrix.cibw_arch }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- cibw_arch: x86
|
||||
os: windows-latest
|
||||
- cibw_arch: AMD64
|
||||
os: windows-latest
|
||||
- cibw_arch: ARM64
|
||||
os: windows-11-arm
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Checkout extra test images
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
repository: python-pillow/test-images
|
||||
path: Tests\test-images
|
||||
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
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: |
|
||||
for f in winbuild/build/license/*; do
|
||||
name=$(basename "${f%.*}")
|
||||
# Skip FriBiDi license, it is not included in the wheel.
|
||||
[[ $name == fribidi* ]] && continue
|
||||
# Skip imagequant license, it is not included in the wheel.
|
||||
[[ $name == libimagequant* ]] && continue
|
||||
echo "" >> LICENSE
|
||||
echo "===== $name =====" >> LICENSE
|
||||
echo "" >> LICENSE
|
||||
cat "$f" >> LICENSE
|
||||
done
|
||||
cmd //c "winbuild\\build\\build_env.cmd && $pythonLocation\\python.exe -m cibuildwheel . --output-dir wheelhouse"
|
||||
env:
|
||||
CIBW_ARCHS: ${{ matrix.cibw_arch }}
|
||||
CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd"
|
||||
CIBW_CACHE_PATH: "C:\\cibw"
|
||||
CIBW_ENABLE: cpython-prerelease pypy
|
||||
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: bash
|
||||
|
||||
- name: Upload wheels
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: dist-windows-${{ matrix.cibw_arch }}
|
||||
path: ./wheelhouse/*.whl
|
||||
|
||||
- name: Upload fribidi.dll
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: fribidi-windows-${{ matrix.cibw_arch }}
|
||||
path: winbuild\build\bin\fribidi*
|
||||
|
||||
sdist:
|
||||
if: github.event_name != 'schedule' || github.event.repository.fork == false
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.x"
|
||||
|
||||
- run: make sdist
|
||||
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: dist-sdist
|
||||
path: dist/*.tar.gz
|
||||
|
||||
count-dists:
|
||||
needs: [build-native-wheels, windows, sdist]
|
||||
runs-on: ubuntu-latest
|
||||
name: Count dists
|
||||
steps:
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
pattern: dist-*
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
- name: "What did we get?"
|
||||
run: |
|
||||
ls -alR
|
||||
echo "Number of dists, should be $EXPECTED_DISTS:"
|
||||
files=$(ls dist 2>/dev/null | wc -l)
|
||||
echo $files
|
||||
[ "$files" -eq $EXPECTED_DISTS ] || exit 1
|
||||
|
||||
compare-dist-sizes:
|
||||
needs: [build-native-wheels, windows, sdist]
|
||||
runs-on: ubuntu-latest
|
||||
name: Compare dist sizes vs PyPI
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||
with:
|
||||
enable-cache: false
|
||||
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
pattern: dist-*
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
|
||||
- name: Compare dist sizes vs latest PyPI release
|
||||
run: uv run .github/compare-dist-sizes.py dist
|
||||
|
||||
scientific-python-nightly-wheels-publish:
|
||||
if: github.event.repository.fork == false && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')
|
||||
needs: count-dists
|
||||
runs-on: ubuntu-latest
|
||||
name: Upload wheels to scientific-python-nightly-wheels
|
||||
environment:
|
||||
name: release-anaconda
|
||||
url: https://anaconda.org/channels/scientific-python-nightly-wheels/packages/pillow/overview
|
||||
steps:
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
pattern: dist-!(sdist)*
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
- name: Upload wheels to scientific-python-nightly-wheels
|
||||
uses: scientific-python/upload-nightly-action@e76cfec8a4611fd02808a801b0ff5a7d7c1b2d99 # 0.6.4
|
||||
with:
|
||||
artifacts_path: dist
|
||||
anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }}
|
||||
|
||||
sbom:
|
||||
if: github.event_name != 'schedule' || github.event.repository.fork == false
|
||||
runs-on: ubuntu-latest
|
||||
name: Generate SBOM
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.x"
|
||||
|
||||
- name: Generate CycloneDX SBOM
|
||||
run: python3 .github/generate-sbom.py
|
||||
|
||||
- name: Upload SBOM as workflow artifact
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: sbom
|
||||
path: "pillow-*.cdx.json"
|
||||
|
||||
- name: Validate SBOM
|
||||
run: |
|
||||
python3 -m pip install -r .ci/requirements-sbom.txt
|
||||
check-jsonschema --schemafile "https://raw.githubusercontent.com/CycloneDX/specification/1.7/schema/bom-1.7.schema.json" pillow-*.cdx.json
|
||||
|
||||
sbom-publish:
|
||||
if: |
|
||||
github.event.repository.fork == false
|
||||
&& github.event_name == 'push'
|
||||
&& startsWith(github.ref, 'refs/tags')
|
||||
needs: [count-dists, sbom]
|
||||
runs-on: ubuntu-latest
|
||||
name: Publish SBOM to GitHub release
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: sbom
|
||||
path: .
|
||||
|
||||
- name: Attach SBOM to GitHub release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: gh release upload "$GITHUB_REF_NAME" pillow-*.cdx.json
|
||||
|
||||
pypi-publish:
|
||||
if: github.event.repository.fork == false && github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
|
||||
needs: count-dists
|
||||
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@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
pattern: dist-*
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
- name: Publish to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
|
||||
with:
|
||||
attestations: true
|
||||
36
.gitignore
vendored
36
.gitignore
vendored
@ -6,7 +6,6 @@ __pycache__/
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.eggs/
|
||||
.Python
|
||||
env/
|
||||
bin/
|
||||
@ -19,7 +18,6 @@ lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheelhouse/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
@ -33,12 +31,9 @@ htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.cache
|
||||
.pytest_cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
|
||||
# Test files
|
||||
test_images
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
|
||||
@ -57,9 +52,6 @@ test_images
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# viewdoc output
|
||||
.long-description.html
|
||||
|
||||
# Vim cruft
|
||||
.*.swp
|
||||
|
||||
@ -68,35 +60,9 @@ docs/_build/
|
||||
\#*#
|
||||
.#*
|
||||
|
||||
#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
|
||||
|
||||
# Test and dependency downloads
|
||||
pillow-depends-main.zip
|
||||
pillow-test-images.zip
|
||||
|
||||
# pyinstaller
|
||||
*.spec
|
||||
|
||||
# Generated SBOM
|
||||
pillow-*.cdx.json
|
||||
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -1,3 +0,0 @@
|
||||
[submodule "multibuild"]
|
||||
path = wheels/multibuild
|
||||
url = https://github.com/multi-build/multibuild.git
|
||||
3
.landscape.yaml
Normal file
3
.landscape.yaml
Normal file
@ -0,0 +1,3 @@
|
||||
strictness: medium
|
||||
test-warnings: yes
|
||||
max-line-length: 80
|
||||
@ -1,95 +0,0 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.15.12
|
||||
hooks:
|
||||
- id: ruff-check
|
||||
args: [--exit-non-zero-on-fix]
|
||||
|
||||
- repo: https://github.com/psf/black-pre-commit-mirror
|
||||
rev: 26.3.1
|
||||
hooks:
|
||||
- id: black
|
||||
|
||||
- repo: https://github.com/PyCQA/bandit
|
||||
rev: 1.9.4
|
||||
hooks:
|
||||
- id: bandit
|
||||
args: [--severity-level=high]
|
||||
files: ^src/
|
||||
|
||||
- repo: https://github.com/Lucas-C/pre-commit-hooks
|
||||
rev: v1.5.6
|
||||
hooks:
|
||||
- id: remove-tabs
|
||||
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-clang-format
|
||||
rev: v22.1.4
|
||||
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: v6.0.0
|
||||
hooks:
|
||||
- id: check-case-conflict
|
||||
- id: check-executables-have-shebangs
|
||||
- id: check-shebang-scripts-are-executable
|
||||
- id: check-merge-conflict
|
||||
- id: check-json
|
||||
- id: check-toml
|
||||
- id: check-yaml
|
||||
args: [--allow-multiple-documents]
|
||||
- id: end-of-file-fixer
|
||||
exclude: ^Tests/images/
|
||||
- id: file-contents-sorter
|
||||
files: .github/workflows/Brewfile
|
||||
- id: trailing-whitespace
|
||||
exclude: ^\.github/.*TEMPLATE|^Tests/(fonts|images)/
|
||||
|
||||
- repo: https://github.com/python-jsonschema/check-jsonschema
|
||||
rev: 0.37.2
|
||||
hooks:
|
||||
- id: check-github-workflows
|
||||
- id: check-readthedocs
|
||||
- id: check-renovate
|
||||
|
||||
- repo: https://github.com/zizmorcore/zizmor-pre-commit
|
||||
rev: v1.24.1
|
||||
hooks:
|
||||
- id: zizmor
|
||||
|
||||
- repo: https://github.com/sphinx-contrib/sphinx-lint
|
||||
rev: v1.0.2
|
||||
hooks:
|
||||
- id: sphinx-lint
|
||||
|
||||
- repo: https://github.com/tox-dev/pyproject-fmt
|
||||
rev: v2.21.1
|
||||
hooks:
|
||||
- id: pyproject-fmt
|
||||
|
||||
- repo: https://github.com/abravalheri/validate-pyproject
|
||||
rev: v0.25
|
||||
hooks:
|
||||
- id: validate-pyproject
|
||||
additional_dependencies: [trove-classifiers>=2024.10.12]
|
||||
|
||||
- repo: https://github.com/tox-dev/tox-ini-fmt
|
||||
rev: 1.7.1
|
||||
hooks:
|
||||
- id: tox-ini-fmt
|
||||
|
||||
- repo: meta
|
||||
hooks:
|
||||
- id: check-hooks-apply
|
||||
- id: check-useless-excludes
|
||||
|
||||
ci:
|
||||
autoupdate_schedule: monthly
|
||||
@ -1,22 +0,0 @@
|
||||
version: 2
|
||||
|
||||
sphinx:
|
||||
configuration: docs/conf.py
|
||||
|
||||
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
|
||||
81
.travis.yml
Normal file
81
.travis.yml
Normal file
@ -0,0 +1,81 @@
|
||||
language: python
|
||||
|
||||
notifications:
|
||||
irc: "chat.freenode.net#pil"
|
||||
|
||||
# Run slow PyPy* first, to give them a headstart and reduce waiting time.
|
||||
# Run latest 3.x and 2.x next, to get quick compatibility results.
|
||||
# Then run the remainder.
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
include:
|
||||
- python: "pypy"
|
||||
- python: "pypy3"
|
||||
- python: '3.6'
|
||||
- python: '2.7'
|
||||
- env: DOCKER="alpine"
|
||||
- env: DOCKER="arch" # contains PyQt5
|
||||
- env: DOCKER="ubuntu-trusty-x86"
|
||||
- env: DOCKER="ubuntu-xenial-amd64"
|
||||
- env: DOCKER="ubuntu-precise-amd64"
|
||||
- env: DOCKER="debian-stretch-x86"
|
||||
- python: "2.7_with_system_site_packages" # For PyQt4
|
||||
- python: '3.5'
|
||||
- python: '3.4'
|
||||
- python: '3.3'
|
||||
|
||||
dist: trusty
|
||||
|
||||
sudo: required
|
||||
|
||||
services:
|
||||
- docker
|
||||
|
||||
install:
|
||||
- if [ "$DOCKER" == "" ]; then .travis/install.sh; fi
|
||||
|
||||
before_install:
|
||||
- if [ "$DOCKER" ]; then docker pull pythonpillow/$DOCKER; fi
|
||||
|
||||
before_script:
|
||||
# Qt needs a display for some of the tests, and it's only run on the system site packages install
|
||||
- "export DISPLAY=:99.0"
|
||||
- "sh -e /etc/init.d/xvfb start"
|
||||
|
||||
script:
|
||||
- |
|
||||
if [ "$DOCKER" == "" ]; then
|
||||
.travis/script.sh
|
||||
else
|
||||
docker run -v $TRAVIS_BUILD_DIR:/Pillow pythonpillow/$DOCKER
|
||||
fi
|
||||
|
||||
after_success:
|
||||
- .travis/after_success.sh
|
||||
|
||||
after_failure:
|
||||
- |
|
||||
if [ "$TRAVIS_REPO_SLUG" = "python-pillow/Pillow" ] && [ "$TRAVIS_BRANCH" = "master" ] && [ "$TRAVIS_PULL_REQUEST" = "false" ]; then
|
||||
curl -Lo travis_after_all.py https://raw.github.com/dmakhno/travis_after_all/master/travis_after_all.py
|
||||
python travis_after_all.py
|
||||
export $(cat .to_export_back)
|
||||
if [ "$BUILD_LEADER" = "YES" ]; then
|
||||
if [ "$BUILD_AGGREGATE_STATUS" = "others_failed" ]; then
|
||||
echo "All jobs failed"
|
||||
else
|
||||
echo "Some jobs failed"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
after_script:
|
||||
- |
|
||||
if [ "$TRAVIS_REPO_SLUG" = "python-pillow/Pillow" ] && [ "$TRAVIS_BRANCH" = "master" ] && [ "$TRAVIS_PULL_REQUEST" = "false" ]; then
|
||||
echo leader=$BUILD_LEADER status=$BUILD_AGGREGATE_STATUS
|
||||
fi
|
||||
|
||||
env:
|
||||
global:
|
||||
# travis encrypt AUTH_TOKEN=
|
||||
secure: "Vzm7aG1Qv0SDQcqiPzZMedNLn5ZmpL7IzF0DYnqcD+/l+zmKU22SnJBcX0uVXumo+r7eZfpsShpqfcdsZvMlvmQnwz+Y6AGKQru9tCKZbTMnuRjWKKXekC+tr8Xt9CKvRVtte5PyXW31paxUI3/e+fQGBwoFjEEC+6EpEOjeRfE="
|
||||
47
.travis/after_success.sh
Executable file
47
.travis/after_success.sh
Executable file
@ -0,0 +1,47 @@
|
||||
#!/bin/bash
|
||||
|
||||
# gather the coverage data
|
||||
sudo apt-get -qq install lcov
|
||||
lcov --capture --directory . -b . --output-file coverage.info
|
||||
# filter to remove system headers
|
||||
lcov --remove coverage.info '/usr/*' -o coverage.filtered.info
|
||||
# convert to json
|
||||
gem install coveralls-lcov
|
||||
coveralls-lcov -v -n coverage.filtered.info > coverage.c.json
|
||||
|
||||
coverage report
|
||||
pip install coveralls-merge
|
||||
coveralls-merge coverage.c.json
|
||||
|
||||
if [ "$DOCKER" == "" ]; then
|
||||
pip install pep8 pyflakes
|
||||
pep8 --statistics --count PIL/*.py
|
||||
pep8 --statistics --count Tests/*.py
|
||||
pyflakes *.py | tee >(wc -l)
|
||||
pyflakes PIL/*.py | tee >(wc -l)
|
||||
pyflakes Tests/*.py | tee >(wc -l)
|
||||
fi
|
||||
|
||||
if [ "$TRAVIS_PYTHON_VERSION" == "2.7" ] && [ "$DOCKER" == "" ]; then
|
||||
# Coverage and quality reports on just the latest diff.
|
||||
# (Installation is very slow on Py3, so just do it for Py2.)
|
||||
depends/diffcover-install.sh
|
||||
depends/diffcover-run.sh
|
||||
fi
|
||||
|
||||
# after_all
|
||||
|
||||
if [ "$TRAVIS_REPO_SLUG" = "python-pillow/Pillow" ] && [ "$TRAVIS_BRANCH" = "master" ] && [ "$TRAVIS_PULL_REQUEST" = "false" ]; then
|
||||
curl -Lo travis_after_all.py https://raw.github.com/dmakhno/travis_after_all/master/travis_after_all.py
|
||||
python travis_after_all.py
|
||||
export $(cat .to_export_back)
|
||||
if [ "$BUILD_LEADER" = "YES" ]; then
|
||||
if [ "$BUILD_AGGREGATE_STATUS" = "others_succeeded" ]; then
|
||||
echo "All jobs succeded! Triggering macOS build..."
|
||||
# Trigger a macOS build at the pillow-wheels repo
|
||||
./build_children.sh
|
||||
else
|
||||
echo "Some jobs failed"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
34
.travis/install.sh
Executable file
34
.travis/install.sh
Executable file
@ -0,0 +1,34 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
sudo apt-get update
|
||||
sudo apt-get -qq install libfreetype6-dev liblcms2-dev\
|
||||
python-qt4 ghostscript libffi-dev libjpeg-turbo-progs cmake imagemagick
|
||||
pip install cffi
|
||||
pip install nose
|
||||
pip install check-manifest
|
||||
pip install olefile
|
||||
# Pyroma tests sometimes hang on PyPy; skip
|
||||
if [ "$TRAVIS_PYTHON_VERSION" != "pypy" ]; then pip install pyroma; fi
|
||||
|
||||
pip install coverage
|
||||
|
||||
# docs only on python 2.7
|
||||
if [ "$TRAVIS_PYTHON_VERSION" == "2.7" ]; then pip install -r requirements.txt ; fi
|
||||
|
||||
# clean checkout for manifest
|
||||
mkdir /tmp/check-manifest && cp -a . /tmp/check-manifest
|
||||
|
||||
# webp
|
||||
pushd depends && ./install_webp.sh && popd
|
||||
|
||||
# openjpeg
|
||||
pushd depends && ./install_openjpeg.sh && popd
|
||||
|
||||
# libimagequant
|
||||
pushd depends && ./install_imagequant.sh && popd
|
||||
|
||||
# extra test images
|
||||
pushd depends && ./install_extra_test_images.sh && popd
|
||||
|
||||
14
.travis/script.sh
Executable file
14
.travis/script.sh
Executable file
@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
coverage erase
|
||||
python setup.py clean
|
||||
CFLAGS="-coverage" python setup.py build_ext --inplace
|
||||
|
||||
coverage run --append --include=PIL/* selftest.py
|
||||
coverage run --append --include=PIL/* -m nose -vx Tests/test_*.py
|
||||
pushd /tmp/check-manifest && check-manifest --ignore ".coveragerc,.editorconfig,*.yml,*.yaml,tox.ini" && popd
|
||||
|
||||
# Docs
|
||||
if [ "$TRAVIS_PYTHON_VERSION" == "2.7" ]; then make install && make doccheck; fi
|
||||
6350
CHANGES.rst
6350
CHANGES.rst
File diff suppressed because it is too large
Load Diff
26
LICENSE
26
LICENSE
@ -1,30 +1,16 @@
|
||||
The Python Imaging Library (PIL) is
|
||||
|
||||
Copyright © 1997-2011 by Secret Labs AB
|
||||
Copyright © 1995-2011 by Fredrik Lundh and contributors
|
||||
Copyright © 1995-2011 by Fredrik Lundh
|
||||
|
||||
Pillow is the friendly PIL fork. It is
|
||||
|
||||
Copyright © 2010 by Jeffrey 'Alex' Clark and contributors
|
||||
Copyright © 2010-2017 by Alex Clark and contributors
|
||||
|
||||
Like PIL, Pillow is licensed under the open source MIT-CMU License:
|
||||
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:
|
||||
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.
|
||||
Permission to use, copy, modify, and distribute this software and its associated documentation for any purpose and without fee is hereby granted, provided that the above copyright notice appears in all copies, and that both that copyright notice and this permission notice appear in supporting documentation, and that the name of Secret Labs AB or the author not be used in advertising or publicity pertaining to distribution of the software without specific, written prior permission.
|
||||
|
||||
SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS
|
||||
SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS.
|
||||
IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR BE LIABLE FOR ANY SPECIAL,
|
||||
INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
|
||||
OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
PERFORMANCE OF THIS SOFTWARE.
|
||||
SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
37
MANIFEST.in
37
MANIFEST.in
@ -1,3 +1,4 @@
|
||||
|
||||
include *.c
|
||||
include *.h
|
||||
include *.in
|
||||
@ -5,42 +6,28 @@ include *.md
|
||||
include *.py
|
||||
include *.rst
|
||||
include *.sh
|
||||
include *.toml
|
||||
include *.txt
|
||||
include *.yaml
|
||||
include .flake8
|
||||
include LICENSE
|
||||
include Makefile
|
||||
include tox.ini
|
||||
graft Scripts
|
||||
graft Tests
|
||||
graft Tests/images
|
||||
graft checks
|
||||
graft src
|
||||
graft PIL
|
||||
graft Tk
|
||||
graft libImaging
|
||||
graft depends
|
||||
graft winbuild
|
||||
graft docs
|
||||
graft _custom_build
|
||||
prune docs/_static
|
||||
|
||||
# build/src control detritus
|
||||
exclude .clang-format
|
||||
exclude .coveragerc
|
||||
exclude .editorconfig
|
||||
exclude .readthedocs.yml
|
||||
exclude codecov.yml
|
||||
exclude renovate.json
|
||||
exclude Tests/images/README.md
|
||||
exclude Tests/images/crash*.tif
|
||||
exclude Tests/images/string_dimension.tiff
|
||||
exclude .landscape.yaml
|
||||
exclude .travis
|
||||
exclude .travis/*
|
||||
exclude appveyor.yml
|
||||
exclude build_children.sh
|
||||
exclude tox.ini
|
||||
global-exclude .git*
|
||||
global-exclude *.pyc
|
||||
global-exclude *.so
|
||||
prune .ci
|
||||
prune wheels
|
||||
prune winbuild/build
|
||||
prune winbuild/depends
|
||||
prune Tests/errors
|
||||
prune Tests/images/jpeg2000
|
||||
prune Tests/images/msp
|
||||
prune Tests/images/picins
|
||||
prune Tests/images/sunraster
|
||||
prune Tests/test-images
|
||||
|
||||
178
Makefile
178
Makefile
@ -1,142 +1,104 @@
|
||||
.DEFAULT_GOAL := help
|
||||
# https://www.gnu.org/software/make/manual/html_node/Phony-Targets.html
|
||||
.PHONY: clean coverage doc docserve help inplace install install-req release-test sdist test upload upload-test
|
||||
.DEFAULT_GOAL := release-test
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm src/PIL/*.so || true
|
||||
python setup.py clean
|
||||
rm 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
|
||||
BRANCHES=`git branch -a | grep -v HEAD | grep -v master | grep remote`
|
||||
co:
|
||||
-for i in $(BRANCHES) ; do \
|
||||
git checkout -t $$i ; \
|
||||
done
|
||||
|
||||
.PHONY: doc
|
||||
.PHONY: html
|
||||
doc html:
|
||||
coverage:
|
||||
coverage erase
|
||||
coverage run --parallel-mode --include=PIL/* selftest.py
|
||||
nosetests --with-cov --cov='PIL/' --cov-report=html Tests/test_*.py
|
||||
# Doesn't combine properly before report, writing report instead of displaying invalid report.
|
||||
rm -r htmlcov || true
|
||||
coverage combine
|
||||
coverage report
|
||||
|
||||
doc:
|
||||
$(MAKE) -C docs html
|
||||
|
||||
.PHONY: htmlview
|
||||
htmlview:
|
||||
$(MAKE) -C docs htmlview
|
||||
|
||||
.PHONY: htmllive
|
||||
htmllive:
|
||||
$(MAKE) -C docs htmllive
|
||||
|
||||
.PHONY: doccheck
|
||||
doccheck:
|
||||
$(MAKE) doc
|
||||
$(MAKE) -C docs html
|
||||
# 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&
|
||||
cd docs/_build/html && python -mSimpleHTTPServer 2> /dev/null&
|
||||
|
||||
.PHONY: help
|
||||
help:
|
||||
@echo "Welcome to Pillow development. Please use \`make <target>\` where <target> 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 " htmllive rebuild and reload HTML files in your browser"
|
||||
@echo " install make and install"
|
||||
@echo " install-coverage make and install with C coverage"
|
||||
@echo " lint run the lint checks"
|
||||
@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"
|
||||
@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 to make standalone HTML files"
|
||||
@echo " inplace make inplace extension"
|
||||
@echo " install make and install"
|
||||
@echo " install-req install documentation and test dependencies"
|
||||
@echo " install-venv install in virtualenv"
|
||||
@echo " release-test run code and package tests before release"
|
||||
@echo " test run tests on installed pillow"
|
||||
@echo " upload build and upload sdists to PyPI"
|
||||
@echo " upload-test build and upload sdists to test.pythonpackages.com"
|
||||
|
||||
inplace: clean
|
||||
python setup.py build_ext --inplace
|
||||
|
||||
.PHONY: install
|
||||
install:
|
||||
python3 -m pip -v install .
|
||||
python3 selftest.py
|
||||
python setup.py install
|
||||
python selftest.py --installed
|
||||
|
||||
.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
|
||||
# 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
|
||||
CFLAGS='-g -O0' python setup.py build_ext install > /dev/null
|
||||
|
||||
install-req:
|
||||
pip install -r requirements.txt
|
||||
|
||||
install-venv:
|
||||
virtualenv .
|
||||
bin/pip install -r requirements.txt
|
||||
|
||||
.PHONY: release-test
|
||||
release-test:
|
||||
python3 checks/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
|
||||
$(MAKE) install-req
|
||||
python setup.py develop
|
||||
python selftest.py
|
||||
nosetests Tests/test_*.py
|
||||
python setup.py install
|
||||
python test-installed.py
|
||||
check-manifest
|
||||
pyroma .
|
||||
viewdoc
|
||||
|
||||
.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/*
|
||||
python setup.py sdist --format=gztar,zip
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
python3 -c "import pytest" > /dev/null 2>&1 || python3 -m pip install pytest
|
||||
python3 -m pytest -qq
|
||||
python test-installed.py
|
||||
|
||||
.PHONY: test-p
|
||||
test-p:
|
||||
python3 -c "import xdist" > /dev/null 2>&1 || python3 -m pip install pytest-xdist
|
||||
python3 -m pytest -qq -n auto
|
||||
# https://docs.python.org/2/distutils/packageindex.html#the-pypirc-file
|
||||
upload-test:
|
||||
# [test]
|
||||
# username:
|
||||
# password:
|
||||
# repository = http://test.pythonpackages.com
|
||||
python setup.py sdist --format=gztar,zip upload -r test
|
||||
|
||||
upload:
|
||||
python setup.py sdist --format=gztar,zip upload
|
||||
|
||||
.PHONY: valgrind
|
||||
valgrind:
|
||||
python3 -c "import pytest_valgrind" > /dev/null 2>&1 || python3 -m pip install pytest-valgrind
|
||||
PILLOW_VALGRIND_TEST=true PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp --leak-check=no \
|
||||
--log-file=/tmp/valgrind-output \
|
||||
python3 -m pytest --no-memcheck -vv --valgrind --valgrind-log=/tmp/valgrind-output
|
||||
|
||||
.PHONY: valgrind-leak
|
||||
valgrind-leak:
|
||||
python3 -c "import pytest_valgrind" > /dev/null 2>&1 || python3 -m pip install pytest-valgrind
|
||||
PILLOW_VALGRIND_TEST=true PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp \
|
||||
--leak-check=full --show-leak-kinds=definite --errors-for-leak-kinds=definite \
|
||||
--log-file=/tmp/valgrind-output \
|
||||
python3 -m pytest -vv --valgrind --valgrind-log=/tmp/valgrind-output
|
||||
|
||||
.PHONY: readme
|
||||
readme:
|
||||
python3 -c "import markdown2" > /dev/null 2>&1 || python3 -m pip install markdown2
|
||||
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 check --fix .
|
||||
|
||||
.PHONY: mypy
|
||||
mypy:
|
||||
python3 -c "import tox" > /dev/null 2>&1 || python3 -m pip install tox
|
||||
python3 -m tox -e mypy
|
||||
viewdoc
|
||||
|
||||
133
PIL/BdfFontFile.py
Normal file
133
PIL/BdfFontFile.py
Normal file
@ -0,0 +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.
|
||||
#
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
from . import Image, FontFile
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# parse X Bitmap Distribution Format (BDF)
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
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):
|
||||
# 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 = []
|
||||
while True:
|
||||
s = f.readline()
|
||||
if not s or s[:7] == b"ENDCHAR":
|
||||
break
|
||||
bitmap.append(s[:-1])
|
||||
bitmap = b"".join(bitmap)
|
||||
|
||||
[x, y, l, d] = [int(p) for p in props["BBX"].split()]
|
||||
[dx, dy] = [int(p) for p in props["DWIDTH"].split()]
|
||||
|
||||
bbox = (dx, dy), (l, -d-y, x+l, -d), (0, 0, x, y)
|
||||
|
||||
try:
|
||||
im = Image.frombytes("1", (x, y), bitmap, "hex", "1")
|
||||
except ValueError:
|
||||
# deal with zero-width characters
|
||||
im = Image.new("1", (x, y))
|
||||
|
||||
return id, int(props["ENCODING"]), bbox, im
|
||||
|
||||
|
||||
##
|
||||
# Font file plugin for the X11 BDF format.
|
||||
|
||||
class BdfFontFile(FontFile.FontFile):
|
||||
|
||||
def __init__(self, fp):
|
||||
|
||||
FontFile.FontFile.__init__(self)
|
||||
|
||||
s = fp.readline()
|
||||
if s[:13] != b"STARTFONT 2.1":
|
||||
raise SyntaxError("not a valid BDF file")
|
||||
|
||||
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'))
|
||||
|
||||
# font = props["FONT"].split("-")
|
||||
|
||||
# font[4] = bdf_slant[font[4].upper()]
|
||||
# font[11] = bdf_spacing[font[11].upper()]
|
||||
|
||||
# ascent = int(props["FONT_ASCENT"])
|
||||
# descent = int(props["FONT_DESCENT"])
|
||||
|
||||
# fontname = ";".join(font[1:])
|
||||
|
||||
# print("#", fontname)
|
||||
# for i in comments:
|
||||
# print("#", i)
|
||||
|
||||
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
|
||||
290
PIL/BmpImagePlugin.py
Normal file
290
PIL/BmpImagePlugin.py
Normal file
@ -0,0 +1,290 @@
|
||||
#
|
||||
# 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 . import Image, ImageFile, ImagePalette
|
||||
from ._binary import i8, i16le as i16, i32le as i32, \
|
||||
o8, o16le as o16, o32le as o32
|
||||
import math
|
||||
|
||||
__version__ = "0.7"
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
# 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):
|
||||
return prefix[:2] == b"BM"
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# 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}
|
||||
RAW, RLE8, RLE4, BITFIELDS, JPEG, PNG = 0, 1, 2, 3, 4, 5
|
||||
|
||||
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)
|
||||
file_info = {}
|
||||
file_info['header_size'] = i32(read(4)) # read bmp header size @offset 14 (this is part of the header size)
|
||||
file_info['direction'] = -1
|
||||
# --------------------- If requested, read header at a specific position
|
||||
header_data = ImageFile._safe_read(self.fp, file_info['header_size'] - 4) # read the rest of the bmp header, without its size
|
||||
# --------------------------------------------------- IBM OS/2 Bitmap v1
|
||||
# ------ This format has different offsets because of width/height types
|
||||
if file_info['header_size'] == 12:
|
||||
file_info['width'] = i16(header_data[0:2])
|
||||
file_info['height'] = i16(header_data[2:4])
|
||||
file_info['planes'] = i16(header_data[4:6])
|
||||
file_info['bits'] = i16(header_data[6:8])
|
||||
file_info['compression'] = self.RAW
|
||||
file_info['palette_padding'] = 3
|
||||
# ---------------------------------------------- Windows Bitmap v2 to v5
|
||||
elif file_info['header_size'] in (40, 64, 108, 124): # v3, OS/2 v2, v4, v5
|
||||
if file_info['header_size'] >= 40: # v3 and OS/2
|
||||
file_info['y_flip'] = i8(header_data[7]) == 0xff
|
||||
file_info['direction'] = 1 if file_info['y_flip'] else -1
|
||||
file_info['width'] = i32(header_data[0:4])
|
||||
file_info['height'] = i32(header_data[4:8]) if not file_info['y_flip'] else 2**32 - i32(header_data[4:8])
|
||||
file_info['planes'] = i16(header_data[8:10])
|
||||
file_info['bits'] = i16(header_data[10:12])
|
||||
file_info['compression'] = i32(header_data[12:16])
|
||||
file_info['data_size'] = i32(header_data[16:20]) # byte size of pixel data
|
||||
file_info['pixels_per_meter'] = (i32(header_data[20:24]), i32(header_data[24:28]))
|
||||
file_info['colors'] = i32(header_data[28:32])
|
||||
file_info['palette_padding'] = 4
|
||||
self.info["dpi"] = tuple(
|
||||
map(lambda x: int(math.ceil(x / 39.3701)),
|
||||
file_info['pixels_per_meter']))
|
||||
if file_info['compression'] == self.BITFIELDS:
|
||||
if len(header_data) >= 52:
|
||||
for idx, mask in enumerate(['r_mask', 'g_mask', 'b_mask', 'a_mask']):
|
||||
file_info[mask] = i32(header_data[36+idx*4:40+idx*4])
|
||||
else:
|
||||
# 40 byte headers only have the three components in the bitfields masks,
|
||||
# ref: https://msdn.microsoft.com/en-us/library/windows/desktop/dd183376(v=vs.85).aspx
|
||||
# See also https://github.com/python-pillow/Pillow/issues/1293
|
||||
# There is a 4th component in the RGBQuad, in the alpha location, but it
|
||||
# is listed as a reserved component, and it is not generally an alpha channel
|
||||
file_info['a_mask'] = 0x0
|
||||
for mask in ['r_mask', 'g_mask', 'b_mask']:
|
||||
file_info[mask] = i32(read(4))
|
||||
file_info['rgb_mask'] = (file_info['r_mask'], file_info['g_mask'], file_info['b_mask'])
|
||||
file_info['rgba_mask'] = (file_info['r_mask'], file_info['g_mask'], file_info['b_mask'], file_info['a_mask'])
|
||||
else:
|
||||
raise IOError("Unsupported BMP header type (%d)" % file_info['header_size'])
|
||||
# ------------------ Special case : header is reported 40, which
|
||||
# ---------------------- is shorter than real size for bpp >= 16
|
||||
self.size = file_info['width'], file_info['height']
|
||||
# -------- If color count was not found in the header, compute from bits
|
||||
file_info['colors'] = file_info['colors'] if file_info.get('colors', 0) else (1 << file_info['bits'])
|
||||
# -------------------------------- Check abnormal values for DOS attacks
|
||||
if file_info['width'] * file_info['height'] > 2**31:
|
||||
raise IOError("Unsupported BMP Size: (%dx%d)" % self.size)
|
||||
# ----------------------- Check bit depth for unusual unsupported values
|
||||
self.mode, raw_mode = BIT2MODE.get(file_info['bits'], (None, None))
|
||||
if self.mode is None:
|
||||
raise IOError("Unsupported BMP pixel depth (%d)" % file_info['bits'])
|
||||
# ----------------- Process BMP with Bitfields compression (not palette)
|
||||
if file_info['compression'] == self.BITFIELDS:
|
||||
SUPPORTED = {
|
||||
32: [(0xff0000, 0xff00, 0xff, 0x0), (0xff0000, 0xff00, 0xff, 0xff000000), (0x0, 0x0, 0x0, 0x0), (0xff000000, 0xff0000, 0xff00, 0x0) ],
|
||||
24: [(0xff0000, 0xff00, 0xff)],
|
||||
16: [(0xf800, 0x7e0, 0x1f), (0x7c00, 0x3e0, 0x1f)]
|
||||
}
|
||||
MASK_MODES = {
|
||||
(32, (0xff0000, 0xff00, 0xff, 0x0)): "BGRX",
|
||||
(32, (0xff000000, 0xff0000, 0xff00, 0x0)): "XBGR",
|
||||
(32, (0xff0000, 0xff00, 0xff, 0xff000000)): "BGRA",
|
||||
(32, (0x0, 0x0, 0x0, 0x0)): "BGRA",
|
||||
(24, (0xff0000, 0xff00, 0xff)): "BGR",
|
||||
(16, (0xf800, 0x7e0, 0x1f)): "BGR;16",
|
||||
(16, (0x7c00, 0x3e0, 0x1f)): "BGR;15"
|
||||
}
|
||||
if file_info['bits'] in SUPPORTED:
|
||||
if file_info['bits'] == 32 and file_info['rgba_mask'] in SUPPORTED[file_info['bits']]:
|
||||
raw_mode = MASK_MODES[(file_info['bits'], file_info['rgba_mask'])]
|
||||
self.mode = "RGBA" if raw_mode in ("BGRA",) else self.mode
|
||||
elif file_info['bits'] in (24, 16) and file_info['rgb_mask'] in SUPPORTED[file_info['bits']]:
|
||||
raw_mode = MASK_MODES[(file_info['bits'], file_info['rgb_mask'])]
|
||||
else:
|
||||
raise IOError("Unsupported BMP bitfields layout")
|
||||
else:
|
||||
raise IOError("Unsupported BMP bitfields layout")
|
||||
elif file_info['compression'] == self.RAW:
|
||||
if file_info['bits'] == 32 and header == 22: # 32-bit .cur offset
|
||||
raw_mode, self.mode = "BGRA", "RGBA"
|
||||
else:
|
||||
raise IOError("Unsupported BMP compression (%d)" % file_info['compression'])
|
||||
# ---------------- Once the header is processed, process the palette/LUT
|
||||
if self.mode == "P": # Paletted for 1, 4 and 8 bit images
|
||||
# ----------------------------------------------------- 1-bit images
|
||||
if not (0 < file_info['colors'] <= 65536):
|
||||
raise IOError("Unsupported BMP Palette size (%d)" % file_info['colors'])
|
||||
else:
|
||||
padding = file_info['palette_padding']
|
||||
palette = read(padding * file_info['colors'])
|
||||
greyscale = True
|
||||
indices = (0, 255) if file_info['colors'] == 2 else list(range(file_info['colors']))
|
||||
# ------------------ Check if greyscale and ignore palette if so
|
||||
for ind, val in enumerate(indices):
|
||||
rgb = palette[ind*padding:ind*padding + 3]
|
||||
if rgb != o8(val) * 3:
|
||||
greyscale = False
|
||||
# -------- If all colors are grey, white or black, ditch palette
|
||||
if greyscale:
|
||||
self.mode = "1" if file_info['colors'] == 2 else "L"
|
||||
raw_mode = self.mode
|
||||
else:
|
||||
self.mode = "P"
|
||||
self.palette = ImagePalette.raw("BGRX" if padding == 4 else "BGR", palette)
|
||||
|
||||
# ----------------------------- Finally set the tile data for the plugin
|
||||
self.info['compression'] = file_info['compression']
|
||||
self.tile = [('raw', (0, 0, file_info['width'], file_info['height']), offset or self.fp.tell(),
|
||||
(raw_mode, ((file_info['width'] * file_info['bits'] + 31) >> 3) & (~3), file_info['direction'])
|
||||
)]
|
||||
|
||||
def _open(self):
|
||||
""" Open file, check magic number and read header """
|
||||
# read 14 bytes: magic number, filesize, reserved, header final offset
|
||||
head_data = self.fp.read(14)
|
||||
# choke if the file does not have the required magic bytes
|
||||
if head_data[0:2] != b"BM":
|
||||
raise SyntaxError("Not a BMP file")
|
||||
# read the start position of the BMP image data (u32)
|
||||
offset = i32(head_data[10:14])
|
||||
# load bitmap information (offset=raster info)
|
||||
self._bitmap(offset=offset)
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# Image plugin for the DIB format (BMP alias)
|
||||
# ==============================================================================
|
||||
class DibImageFile(BmpImageFile):
|
||||
|
||||
format = "DIB"
|
||||
format_description = "Windows Bitmap"
|
||||
|
||||
def _open(self):
|
||||
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 _save(im, fp, filename, check=0):
|
||||
try:
|
||||
rawmode, bits, colors = SAVE[im.mode]
|
||||
except KeyError:
|
||||
raise IOError("cannot write mode %s as BMP" % im.mode)
|
||||
|
||||
if check:
|
||||
return check
|
||||
|
||||
info = im.encoderinfo
|
||||
|
||||
dpi = info.get("dpi", (96, 96))
|
||||
|
||||
# 1 meter == 39.3701 inches
|
||||
ppm = tuple(map(lambda x: int(x * 39.3701), dpi))
|
||||
|
||||
stride = ((im.size[0]*bits+7)//8+3) & (~3)
|
||||
header = 40 # or 64 for OS/2 version 2
|
||||
offset = 14 + header + colors * 4
|
||||
image = stride * im.size[1]
|
||||
|
||||
# bitmap header
|
||||
fp.write(b"BM" + # file type (magic)
|
||||
o32(offset+image) + # file size
|
||||
o32(0) + # reserved
|
||||
o32(offset)) # image data offset
|
||||
|
||||
# bitmap info header
|
||||
fp.write(o32(header) + # info header size
|
||||
o32(im.size[0]) + # width
|
||||
o32(im.size[1]) + # height
|
||||
o16(1) + # planes
|
||||
o16(bits) + # depth
|
||||
o32(0) + # compression (0=uncompressed)
|
||||
o32(image) + # size of bitmap
|
||||
o32(ppm[0]) + o32(ppm[1]) + # resolution
|
||||
o32(colors) + # colors used
|
||||
o32(colors)) # colors important
|
||||
|
||||
fp.write(b"\0" * (header - 40)) # padding (for OS/2 format)
|
||||
|
||||
if im.mode == "1":
|
||||
for i in (0, 255):
|
||||
fp.write(o8(i) * 4)
|
||||
elif im.mode == "L":
|
||||
for i in range(256):
|
||||
fp.write(o8(i) * 4)
|
||||
elif im.mode == "P":
|
||||
fp.write(im.im.getpalette("RGB", "BGRX"))
|
||||
|
||||
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")
|
||||
@ -8,17 +8,13 @@
|
||||
#
|
||||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import IO
|
||||
|
||||
from . import Image, ImageFile
|
||||
|
||||
_handler = None
|
||||
|
||||
|
||||
def register_handler(handler: ImageFile.StubHandler | None) -> None:
|
||||
def register_handler(handler):
|
||||
"""
|
||||
Install application-specific BUFR image handler.
|
||||
|
||||
@ -31,35 +27,39 @@ def register_handler(handler: ImageFile.StubHandler | None) -> None:
|
||||
# --------------------------------------------------------------------
|
||||
# Image adapter
|
||||
|
||||
|
||||
def _accept(prefix: bytes) -> bool:
|
||||
return prefix.startswith((b"BUFR", b"ZCZC"))
|
||||
def _accept(prefix):
|
||||
return prefix[:4] == b"BUFR" or prefix[:4] == b"ZCZC"
|
||||
|
||||
|
||||
class BufrStubImageFile(ImageFile.StubImageFile):
|
||||
|
||||
format = "BUFR"
|
||||
format_description = "BUFR"
|
||||
|
||||
def _open(self) -> None:
|
||||
assert self.fp is not None
|
||||
if not _accept(self.fp.read(4)):
|
||||
msg = "Not a BUFR file"
|
||||
raise SyntaxError(msg)
|
||||
def _open(self):
|
||||
|
||||
self.fp.seek(-4, os.SEEK_CUR)
|
||||
offset = self.fp.tell()
|
||||
|
||||
if not _accept(self.fp.read(4)):
|
||||
raise SyntaxError("Not a BUFR file")
|
||||
|
||||
self.fp.seek(offset)
|
||||
|
||||
# make something up
|
||||
self._mode = "F"
|
||||
self._size = 1, 1
|
||||
self.mode = "F"
|
||||
self.size = 1, 1
|
||||
|
||||
def _load(self) -> ImageFile.StubHandler | None:
|
||||
loader = self._load()
|
||||
if loader:
|
||||
loader.open(self)
|
||||
|
||||
def _load(self):
|
||||
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)
|
||||
def _save(im, fp, filename):
|
||||
if _handler is None or not hasattr("_handler", "save"):
|
||||
raise IOError("BUFR save handler not installed")
|
||||
_handler.save(im, fp, filename)
|
||||
|
||||
|
||||
117
PIL/ContainerIO.py
Normal file
117
PIL/ContainerIO.py
Normal file
@ -0,0 +1,117 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
##
|
||||
# A file object that provides read access to a part of an existing
|
||||
# file (for example a TAR file).
|
||||
|
||||
|
||||
class ContainerIO(object):
|
||||
|
||||
def __init__(self, file, offset, length):
|
||||
"""
|
||||
Create file object.
|
||||
|
||||
:param file: Existing file.
|
||||
:param offset: Start of region, in bytes.
|
||||
:param length: Size of region, in bytes.
|
||||
"""
|
||||
self.fh = file
|
||||
self.pos = 0
|
||||
self.offset = offset
|
||||
self.length = length
|
||||
self.fh.seek(offset)
|
||||
|
||||
##
|
||||
# Always false.
|
||||
|
||||
def isatty(self):
|
||||
return 0
|
||||
|
||||
def seek(self, offset, mode=0):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
Get current file pointer.
|
||||
|
||||
:returns: Offset from start of region, in bytes.
|
||||
"""
|
||||
return self.pos
|
||||
|
||||
def read(self, n=0):
|
||||
"""
|
||||
Read data.
|
||||
|
||||
@def read(bytes=0)
|
||||
:param bytes: 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 ""
|
||||
self.pos = self.pos + n
|
||||
return self.fh.read(n)
|
||||
|
||||
def readline(self):
|
||||
"""
|
||||
Read a line of text.
|
||||
|
||||
:returns: An 8-bit string.
|
||||
"""
|
||||
s = ""
|
||||
while True:
|
||||
c = self.read(1)
|
||||
if not c:
|
||||
break
|
||||
s = s + c
|
||||
if c == "\n":
|
||||
break
|
||||
return s
|
||||
|
||||
def readlines(self):
|
||||
"""
|
||||
Read multiple lines of text.
|
||||
|
||||
:returns: A list of 8-bit strings.
|
||||
"""
|
||||
l = []
|
||||
while True:
|
||||
s = self.readline()
|
||||
if not s:
|
||||
break
|
||||
l.append(s)
|
||||
return l
|
||||
@ -15,56 +15,67 @@
|
||||
#
|
||||
# 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
|
||||
from __future__ import print_function
|
||||
|
||||
from . import Image, BmpImagePlugin
|
||||
from ._binary import i8, i16le as i16, i32le as i32
|
||||
|
||||
__version__ = "0.1"
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
|
||||
def _accept(prefix: bytes) -> bool:
|
||||
return prefix.startswith(b"\0\0\2\0")
|
||||
def _accept(prefix):
|
||||
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:
|
||||
assert self.fp is not None
|
||||
def _open(self):
|
||||
|
||||
offset = self.fp.tell()
|
||||
|
||||
# check magic
|
||||
s = self.fp.read(6)
|
||||
if not _accept(s):
|
||||
msg = "not a CUR file"
|
||||
raise SyntaxError(msg)
|
||||
raise SyntaxError("not a CUR file")
|
||||
|
||||
# pick the largest cursor in the file
|
||||
m = b""
|
||||
for i in range(i16(s, 4)):
|
||||
for i in range(i16(s[4:])):
|
||||
s = self.fp.read(16)
|
||||
if not m:
|
||||
m = s
|
||||
elif s[0] > m[0] and s[1] > m[1]:
|
||||
elif i8(s[0]) > i8(m[0]) and i8(s[1]) > i8(m[1]):
|
||||
m = s
|
||||
# print("width", i8(s[0]))
|
||||
# print("height", i8(s[1]))
|
||||
# print("colors", i8(s[2]))
|
||||
# print("reserved", i8(s[3]))
|
||||
# print("hotspot x", i16(s[4:]))
|
||||
# print("hotspot y", i16(s[6:]))
|
||||
# print("bytes", i32(s[8:]))
|
||||
# print("offset", i32(s[12:]))
|
||||
if not m:
|
||||
msg = "No cursors were found"
|
||||
raise TypeError(msg)
|
||||
raise TypeError("No cursors were found")
|
||||
|
||||
# load as bitmap
|
||||
self._bitmap(i32(m, 12) + offset)
|
||||
self._bitmap(i32(m[12:]) + offset)
|
||||
|
||||
# patch up the bitmap height
|
||||
self._size = self.size[0], self.size[1] // 2
|
||||
self.tile = [self.tile[0]._replace(extents=(0, 0) + self.size)]
|
||||
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
|
||||
|
||||
return
|
||||
|
||||
|
||||
#
|
||||
@ -20,36 +20,35 @@
|
||||
#
|
||||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
from __future__ import annotations
|
||||
|
||||
from . import Image
|
||||
from ._binary import i32le as i32
|
||||
from ._util import DeferredError
|
||||
from .PcxImagePlugin import PcxImageFile
|
||||
|
||||
__version__ = "0.2"
|
||||
|
||||
MAGIC = 0x3ADE68B1 # QUIZ: what's this value, then?
|
||||
|
||||
|
||||
def _accept(prefix: bytes) -> bool:
|
||||
def _accept(prefix):
|
||||
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):
|
||||
|
||||
def _open(self) -> None:
|
||||
# Header
|
||||
assert self.fp is not None
|
||||
s = self.fp.read(4)
|
||||
if not _accept(s):
|
||||
msg = "not a DCX file"
|
||||
raise SyntaxError(msg)
|
||||
if i32(s) != MAGIC:
|
||||
raise SyntaxError("not a DCX file")
|
||||
|
||||
# Component directory
|
||||
self._offset = []
|
||||
@ -59,23 +58,26 @@ class DcxImageFile(PcxImageFile):
|
||||
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.__fp = self.fp
|
||||
self.seek(0)
|
||||
|
||||
def seek(self, frame: int) -> None:
|
||||
if not self._seek_check(frame):
|
||||
return
|
||||
if isinstance(self._fp, DeferredError):
|
||||
raise self._fp.ex
|
||||
@property
|
||||
def n_frames(self):
|
||||
return len(self._offset)
|
||||
|
||||
@property
|
||||
def is_animated(self):
|
||||
return len(self._offset) > 1
|
||||
|
||||
def seek(self, frame):
|
||||
if frame >= len(self._offset):
|
||||
raise EOFError("attempt to seek outside DCX directory")
|
||||
self.frame = frame
|
||||
self.fp = self._fp
|
||||
self.fp = self.__fp
|
||||
self.fp.seek(self._offset[frame])
|
||||
PcxImageFile._open(self)
|
||||
|
||||
def tell(self) -> int:
|
||||
def tell(self):
|
||||
return self.frame
|
||||
|
||||
|
||||
172
PIL/DdsImagePlugin.py
Normal file
172
PIL/DdsImagePlugin.py
Normal file
@ -0,0 +1,172 @@
|
||||
"""
|
||||
A Pillow loader for .dds files (S3TC-compressed aka DXTC)
|
||||
Jerome Leclanche <jerome@leclan.ch>
|
||||
|
||||
Documentation:
|
||||
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/
|
||||
"""
|
||||
|
||||
import struct
|
||||
from io import BytesIO
|
||||
from . 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
|
||||
|
||||
|
||||
# dxgiformat.h
|
||||
|
||||
DXGI_FORMAT_BC7_TYPELESS = 97
|
||||
DXGI_FORMAT_BC7_UNORM = 98
|
||||
DXGI_FORMAT_BC7_UNORM_SRGB = 99
|
||||
|
||||
|
||||
class DdsImageFile(ImageFile.ImageFile):
|
||||
format = "DDS"
|
||||
format_description = "DirectDraw Surface"
|
||||
|
||||
def _open(self):
|
||||
magic, header_size = struct.unpack("<II", self.fp.read(8))
|
||||
if header_size != 124:
|
||||
raise IOError("Unsupported header size %r" % (header_size))
|
||||
header_bytes = self.fp.read(header_size - 4)
|
||||
if len(header_bytes) != 120:
|
||||
raise IOError("Incomplete header: %s bytes" % len(header_bytes))
|
||||
header = BytesIO(header_bytes)
|
||||
|
||||
flags, height, width = struct.unpack("<3I", header.read(12))
|
||||
self.size = (width, height)
|
||||
self.mode = "RGBA"
|
||||
|
||||
pitch, depth, mipmaps = struct.unpack("<3I", header.read(12))
|
||||
reserved = struct.unpack("<11I", header.read(44))
|
||||
|
||||
# pixel format
|
||||
pfsize, pfflags = struct.unpack("<2I", header.read(8))
|
||||
fourcc = header.read(4)
|
||||
bitcount, rmask, gmask, bmask, amask = struct.unpack("<5I",
|
||||
header.read(20))
|
||||
|
||||
data_start = header_size + 4
|
||||
n = 0
|
||||
if fourcc == b"DXT1":
|
||||
self.pixel_format = "DXT1"
|
||||
n = 1
|
||||
elif fourcc == b"DXT3":
|
||||
self.pixel_format = "DXT3"
|
||||
n = 2
|
||||
elif fourcc == b"DXT5":
|
||||
self.pixel_format = "DXT5"
|
||||
n = 3
|
||||
elif fourcc == b"DX10":
|
||||
data_start += 20
|
||||
# ignoring flags which pertain to volume textures and cubemaps
|
||||
dxt10 = BytesIO(self.fp.read(20))
|
||||
dxgi_format, dimension = struct.unpack("<II", dxt10.read(8))
|
||||
if dxgi_format in (DXGI_FORMAT_BC7_TYPELESS, DXGI_FORMAT_BC7_UNORM):
|
||||
self.pixel_format = "BC7"
|
||||
n = 7
|
||||
elif dxgi_format == DXGI_FORMAT_BC7_UNORM_SRGB:
|
||||
self.pixel_format = "BC7"
|
||||
self.im_info["gamma"] = 1/2.2
|
||||
n = 7
|
||||
else:
|
||||
raise NotImplementedError("Unimplemented DXGI format %d" %
|
||||
(dxgi_format))
|
||||
else:
|
||||
raise NotImplementedError("Unimplemented pixel format %r" %
|
||||
(fourcc))
|
||||
|
||||
self.tile = [
|
||||
("bcn", (0, 0) + self.size, data_start, (n))
|
||||
]
|
||||
|
||||
def load_seek(self, pos):
|
||||
pass
|
||||
|
||||
|
||||
def _validate(prefix):
|
||||
return prefix[:4] == b"DDS "
|
||||
|
||||
|
||||
Image.register_open(DdsImageFile.format, DdsImageFile, _validate)
|
||||
Image.register_extension(DdsImageFile.format, ".dds")
|
||||
423
PIL/EpsImagePlugin.py
Normal file
423
PIL/EpsImagePlugin.py
Normal file
@ -0,0 +1,423 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
import re
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
from . import Image, ImageFile
|
||||
from ._binary import i32le as i32
|
||||
|
||||
__version__ = "0.5"
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
split = re.compile(r"^%%([^:]*):[ \t]*(.*)[ \t]*$")
|
||||
field = re.compile(r"^%[%!\w]([^:]*)[ \t]*$")
|
||||
|
||||
gs_windows_binary = None
|
||||
if sys.platform.startswith('win'):
|
||||
import shutil
|
||||
if hasattr(shutil, 'which'):
|
||||
which = shutil.which
|
||||
else:
|
||||
# Python < 3.3
|
||||
import distutils.spawn
|
||||
which = distutils.spawn.find_executable
|
||||
for binary in ('gswin32c', 'gswin64c', 'gs'):
|
||||
if which(binary) is not None:
|
||||
gs_windows_binary = binary
|
||||
break
|
||||
else:
|
||||
gs_windows_binary = False
|
||||
|
||||
|
||||
def has_ghostscript():
|
||||
if gs_windows_binary:
|
||||
return True
|
||||
if not sys.platform.startswith('win'):
|
||||
import subprocess
|
||||
try:
|
||||
with open(os.devnull, 'wb') as devnull:
|
||||
subprocess.check_call(['gs', '--version'], stdout=devnull)
|
||||
return True
|
||||
except OSError:
|
||||
# no ghostscript
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
def Ghostscript(tile, size, fp, scale=1):
|
||||
"""Render an image using Ghostscript"""
|
||||
|
||||
# Unpack decoder tile
|
||||
decoder, tile, offset, data = tile[0]
|
||||
length, bbox = data
|
||||
|
||||
# Hack to support hi-res rendering
|
||||
scale = int(scale) or 1
|
||||
# orig_size = size
|
||||
# orig_bbox = bbox
|
||||
size = (size[0] * scale, size[1] * scale)
|
||||
# resolution is dependent on bbox and size
|
||||
res = (float((72.0 * size[0]) / (bbox[2]-bbox[0])),
|
||||
float((72.0 * size[1]) / (bbox[3]-bbox[1])))
|
||||
# print("Ghostscript", scale, size, orig_size, bbox, orig_bbox, res)
|
||||
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
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, 2)
|
||||
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)
|
||||
|
||||
# Build ghostscript command
|
||||
command = ["gs",
|
||||
"-q", # quiet mode
|
||||
"-g%dx%d" % size, # set output geometry (pixels)
|
||||
"-r%fx%f" % res, # set input DPI (dots per inch)
|
||||
"-dNOPAUSE", # don't pause between pages,
|
||||
"-dSAFER", # safe mode
|
||||
"-sDEVICE=ppmraw", # ppm driver
|
||||
"-sOutputFile=%s" % outfile, # output file
|
||||
"-c", "%d %d translate" % (-bbox[0], -bbox[1]),
|
||||
# adjust for image origin
|
||||
"-f", infile, # input file
|
||||
]
|
||||
|
||||
if gs_windows_binary is not None:
|
||||
if not gs_windows_binary:
|
||||
raise WindowsError('Unable to locate Ghostscript on paths')
|
||||
command[0] = gs_windows_binary
|
||||
|
||||
# push data through ghostscript
|
||||
try:
|
||||
with open(os.devnull, 'w+b') as devnull:
|
||||
subprocess.check_call(command, stdin=devnull, stdout=devnull)
|
||||
im = Image.open(outfile)
|
||||
im.load()
|
||||
finally:
|
||||
try:
|
||||
os.unlink(outfile)
|
||||
if infile_temp:
|
||||
os.unlink(infile_temp)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
return im.im.copy()
|
||||
|
||||
|
||||
class PSFile(object):
|
||||
"""
|
||||
Wrapper for bytesio object that treats either CR or LF as end of line.
|
||||
"""
|
||||
def __init__(self, fp):
|
||||
self.fp = fp
|
||||
self.char = None
|
||||
|
||||
def seek(self, offset, whence=0):
|
||||
self.char = None
|
||||
self.fp.seek(offset, whence)
|
||||
|
||||
def readline(self):
|
||||
s = self.char or b""
|
||||
self.char = None
|
||||
|
||||
c = self.fp.read(1)
|
||||
while c not in b"\r\n":
|
||||
s = s + 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 s.decode('latin-1')
|
||||
|
||||
|
||||
def _accept(prefix):
|
||||
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):
|
||||
(length, offset) = self._find_offset(self.fp)
|
||||
|
||||
# Rewrap the open file pointer in something that will
|
||||
# convert line endings and decode to latin-1.
|
||||
try:
|
||||
if bytes is str:
|
||||
# Python2, no encoding conversion necessary
|
||||
fp = open(self.fp.name, "Ur")
|
||||
else:
|
||||
# Python3, can use bare open command.
|
||||
fp = open(self.fp.name, "Ur", encoding='latin-1')
|
||||
except:
|
||||
# Expect this for bytesio/stringio
|
||||
fp = PSFile(self.fp)
|
||||
|
||||
# go to offset - start of "%!PS"
|
||||
fp.seek(offset)
|
||||
|
||||
box = None
|
||||
|
||||
self.mode = "RGB"
|
||||
self.size = 1, 1 # FIXME: huh?
|
||||
|
||||
#
|
||||
# Load EPS header
|
||||
|
||||
s = fp.readline().strip('\r\n')
|
||||
|
||||
while s:
|
||||
if len(s) > 255:
|
||||
raise SyntaxError("not an EPS file")
|
||||
|
||||
try:
|
||||
m = split.match(s)
|
||||
except re.error as v:
|
||||
raise SyntaxError("not an EPS file")
|
||||
|
||||
if m:
|
||||
k, v = m.group(1, 2)
|
||||
self.info[k] = v
|
||||
if k == "BoundingBox":
|
||||
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:
|
||||
pass
|
||||
|
||||
else:
|
||||
m = field.match(s)
|
||||
if m:
|
||||
k = m.group(1)
|
||||
|
||||
if k == "EndComments":
|
||||
break
|
||||
if k[:8] == "PS-Adobe":
|
||||
self.info[k[:8]] = 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:
|
||||
raise IOError("bad EPS header")
|
||||
|
||||
s = fp.readline().strip('\r\n')
|
||||
|
||||
if s[:1] != "%":
|
||||
break
|
||||
|
||||
#
|
||||
# Scan for an "ImageData" descriptor
|
||||
|
||||
while s[:1] == "%":
|
||||
|
||||
if len(s) > 255:
|
||||
raise SyntaxError("not an EPS file")
|
||||
|
||||
if s[:11] == "%ImageData:":
|
||||
# Encoded bitmapped image.
|
||||
x, y, bi, mo = s[11:].split(None, 7)[:4]
|
||||
|
||||
if int(bi) != 8:
|
||||
break
|
||||
try:
|
||||
self.mode = self.mode_map[int(mo)]
|
||||
except ValueError:
|
||||
break
|
||||
|
||||
self.size = int(x), int(y)
|
||||
return
|
||||
|
||||
s = fp.readline().strip('\r\n')
|
||||
if not s:
|
||||
break
|
||||
|
||||
if not box:
|
||||
raise IOError("cannot determine EPS bounding box")
|
||||
|
||||
def _find_offset(self, fp):
|
||||
|
||||
s = fp.read(160)
|
||||
|
||||
if s[:4] == b"%!PS":
|
||||
# for HEAD without binary preview
|
||||
fp.seek(0, 2)
|
||||
length = fp.tell()
|
||||
offset = 0
|
||||
elif i32(s[0:4]) == 0xC6D3D0C5:
|
||||
# FIX for: Some EPS file not handled correctly / issue #302
|
||||
# EPS can contain binary data
|
||||
# or start directly with latin coding
|
||||
# more info see:
|
||||
# https://web.archive.org/web/20160528181353/http://partners.adobe.com/public/developer/en/ps/5002.EPSF_Spec.pdf
|
||||
offset = i32(s[4:8])
|
||||
length = i32(s[8:12])
|
||||
else:
|
||||
raise SyntaxError("not an EPS file")
|
||||
|
||||
return (length, offset)
|
||||
|
||||
def load(self, scale=1):
|
||||
# Load EPS via Ghostscript
|
||||
if not self.tile:
|
||||
return
|
||||
self.im = Ghostscript(self.tile, self.size, self.fp, scale)
|
||||
self.mode = self.im.mode
|
||||
self.size = self.im.size
|
||||
self.tile = []
|
||||
|
||||
def load_seek(self, *args, **kwargs):
|
||||
# we can't incrementally load, so force ImageFile.parser to
|
||||
# use our custom load method by defining this method.
|
||||
pass
|
||||
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
def _save(im, fp, filename, eps=1):
|
||||
"""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, "image")
|
||||
elif im.mode == "RGB":
|
||||
operator = (8, 3, "false 3 colorimage")
|
||||
elif im.mode == "CMYK":
|
||||
operator = (8, 4, "false 4 colorimage")
|
||||
else:
|
||||
raise ValueError("image mode is not supported")
|
||||
|
||||
class NoCloseStream(object):
|
||||
def __init__(self, fp):
|
||||
self.fp = fp
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.fp, name)
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
base_fp = fp
|
||||
if fp != sys.stdout:
|
||||
fp = NoCloseStream(fp)
|
||||
if sys.version_info[0] > 2:
|
||||
fp = io.TextIOWrapper(fp, encoding='latin-1')
|
||||
|
||||
if eps:
|
||||
#
|
||||
# write EPS header
|
||||
fp.write("%!PS-Adobe-3.0 EPSF-3.0\n")
|
||||
fp.write("%%Creator: PIL 0.1 EpsEncode\n")
|
||||
# fp.write("%%CreationDate: %s"...)
|
||||
fp.write("%%%%BoundingBox: 0 0 %d %d\n" % im.size)
|
||||
fp.write("%%Pages: 1\n")
|
||||
fp.write("%%EndComments\n")
|
||||
fp.write("%%Page: 1 1\n")
|
||||
fp.write("%%ImageData: %d %d " % im.size)
|
||||
fp.write("%d %d 0 1 1 \"%s\"\n" % operator)
|
||||
|
||||
#
|
||||
# image header
|
||||
fp.write("gsave\n")
|
||||
fp.write("10 dict begin\n")
|
||||
fp.write("/buf %d string def\n" % (im.size[0] * operator[1]))
|
||||
fp.write("%d %d scale\n" % im.size)
|
||||
fp.write("%d %d 8\n" % im.size) # <= bits
|
||||
fp.write("[%d 0 0 -%d 0 %d]\n" % (im.size[0], im.size[1], im.size[1]))
|
||||
fp.write("{ currentfile buf readhexstring pop } bind\n")
|
||||
fp.write(operator[2] + "\n")
|
||||
if hasattr(fp, "flush"):
|
||||
fp.flush()
|
||||
|
||||
ImageFile._save(im, base_fp, [("eps", (0, 0)+im.size, 0, None)])
|
||||
|
||||
fp.write("\n%%%%EndBinary\n")
|
||||
fp.write("grestore end\n")
|
||||
if hasattr(fp, "flush"):
|
||||
fp.flush()
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
Image.register_open(EpsImageFile.format, EpsImageFile, _accept)
|
||||
|
||||
Image.register_save(EpsImageFile.format, _save)
|
||||
|
||||
Image.register_extension(EpsImageFile.format, ".ps")
|
||||
Image.register_extension(EpsImageFile.format, ".eps")
|
||||
|
||||
Image.register_mime(EpsImageFile.format, "application/postscript")
|
||||
315
PIL/ExifTags.py
Normal file
315
PIL/ExifTags.py
Normal file
@ -0,0 +1,315 @@
|
||||
#
|
||||
# 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.
|
||||
##
|
||||
|
||||
##
|
||||
# Maps EXIF tags to tag names.
|
||||
|
||||
TAGS = {
|
||||
|
||||
# possibly incomplete
|
||||
0x000b: "ProcessingSoftware",
|
||||
0x00fe: "NewSubfileType",
|
||||
0x00ff: "SubfileType",
|
||||
0x0100: "ImageWidth",
|
||||
0x0101: "ImageLength",
|
||||
0x0102: "BitsPerSample",
|
||||
0x0103: "Compression",
|
||||
0x0106: "PhotometricInterpretation",
|
||||
0x0107: "Thresholding",
|
||||
0x0108: "CellWidth",
|
||||
0x0109: "CellLength",
|
||||
0x010a: "FillOrder",
|
||||
0x010d: "DocumentName",
|
||||
0x010e: "ImageDescription",
|
||||
0x010f: "Make",
|
||||
0x0110: "Model",
|
||||
0x0111: "StripOffsets",
|
||||
0x0112: "Orientation",
|
||||
0x0115: "SamplesPerPixel",
|
||||
0x0116: "RowsPerStrip",
|
||||
0x0117: "StripByteCounts",
|
||||
0x0118: "MinSampleValue",
|
||||
0x0119: "MaxSampleValue",
|
||||
0x011a: "XResolution",
|
||||
0x011b: "YResolution",
|
||||
0x011c: "PlanarConfiguration",
|
||||
0x011d: "PageName",
|
||||
0x0120: "FreeOffsets",
|
||||
0x0121: "FreeByteCounts",
|
||||
0x0122: "GrayResponseUnit",
|
||||
0x0123: "GrayResponseCurve",
|
||||
0x0124: "T4Options",
|
||||
0x0125: "T6Options",
|
||||
0x0128: "ResolutionUnit",
|
||||
0x0129: "PageNumber",
|
||||
0x012d: "TransferFunction",
|
||||
0x0131: "Software",
|
||||
0x0132: "DateTime",
|
||||
0x013b: "Artist",
|
||||
0x013c: "HostComputer",
|
||||
0x013d: "Predictor",
|
||||
0x013e: "WhitePoint",
|
||||
0x013f: "PrimaryChromaticities",
|
||||
0x0140: "ColorMap",
|
||||
0x0141: "HalftoneHints",
|
||||
0x0142: "TileWidth",
|
||||
0x0143: "TileLength",
|
||||
0x0144: "TileOffsets",
|
||||
0x0145: "TileByteCounts",
|
||||
0x014a: "SubIFDs",
|
||||
0x014c: "InkSet",
|
||||
0x014d: "InkNames",
|
||||
0x014e: "NumberOfInks",
|
||||
0x0150: "DotRange",
|
||||
0x0151: "TargetPrinter",
|
||||
0x0152: "ExtraSamples",
|
||||
0x0153: "SampleFormat",
|
||||
0x0154: "SMinSampleValue",
|
||||
0x0155: "SMaxSampleValue",
|
||||
0x0156: "TransferRange",
|
||||
0x0157: "ClipPath",
|
||||
0x0158: "XClipPathUnits",
|
||||
0x0159: "YClipPathUnits",
|
||||
0x015a: "Indexed",
|
||||
0x015b: "JPEGTables",
|
||||
0x015f: "OPIProxy",
|
||||
0x0200: "JPEGProc",
|
||||
0x0201: "JpegIFOffset",
|
||||
0x0202: "JpegIFByteCount",
|
||||
0x0203: "JpegRestartInterval",
|
||||
0x0205: "JpegLosslessPredictors",
|
||||
0x0206: "JpegPointTransforms",
|
||||
0x0207: "JpegQTables",
|
||||
0x0208: "JpegDCTables",
|
||||
0x0209: "JpegACTables",
|
||||
0x0211: "YCbCrCoefficients",
|
||||
0x0212: "YCbCrSubSampling",
|
||||
0x0213: "YCbCrPositioning",
|
||||
0x0214: "ReferenceBlackWhite",
|
||||
0x02bc: "XMLPacket",
|
||||
0x1000: "RelatedImageFileFormat",
|
||||
0x1001: "RelatedImageWidth",
|
||||
0x1002: "RelatedImageLength",
|
||||
0x4746: "Rating",
|
||||
0x4749: "RatingPercent",
|
||||
0x800d: "ImageID",
|
||||
0x828d: "CFARepeatPatternDim",
|
||||
0x828e: "CFAPattern",
|
||||
0x828f: "BatteryLevel",
|
||||
0x8298: "Copyright",
|
||||
0x829a: "ExposureTime",
|
||||
0x829d: "FNumber",
|
||||
0x83bb: "IPTCNAA",
|
||||
0x8649: "ImageResources",
|
||||
0x8769: "ExifOffset",
|
||||
0x8773: "InterColorProfile",
|
||||
0x8822: "ExposureProgram",
|
||||
0x8824: "SpectralSensitivity",
|
||||
0x8825: "GPSInfo",
|
||||
0x8827: "ISOSpeedRatings",
|
||||
0x8828: "OECF",
|
||||
0x8829: "Interlace",
|
||||
0x882a: "TimeZoneOffset",
|
||||
0x882b: "SelfTimerMode",
|
||||
0x9000: "ExifVersion",
|
||||
0x9003: "DateTimeOriginal",
|
||||
0x9004: "DateTimeDigitized",
|
||||
0x9101: "ComponentsConfiguration",
|
||||
0x9102: "CompressedBitsPerPixel",
|
||||
0x9201: "ShutterSpeedValue",
|
||||
0x9202: "ApertureValue",
|
||||
0x9203: "BrightnessValue",
|
||||
0x9204: "ExposureBiasValue",
|
||||
0x9205: "MaxApertureValue",
|
||||
0x9206: "SubjectDistance",
|
||||
0x9207: "MeteringMode",
|
||||
0x9208: "LightSource",
|
||||
0x9209: "Flash",
|
||||
0x920a: "FocalLength",
|
||||
0x920b: "FlashEnergy",
|
||||
0x920c: "SpatialFrequencyResponse",
|
||||
0x920d: "Noise",
|
||||
0x9211: "ImageNumber",
|
||||
0x9212: "SecurityClassification",
|
||||
0x9213: "ImageHistory",
|
||||
0x9214: "SubjectLocation",
|
||||
0x9215: "ExposureIndex",
|
||||
0x9216: "TIFF/EPStandardID",
|
||||
0x927c: "MakerNote",
|
||||
0x9286: "UserComment",
|
||||
0x9290: "SubsecTime",
|
||||
0x9291: "SubsecTimeOriginal",
|
||||
0x9292: "SubsecTimeDigitized",
|
||||
0x9c9b: "XPTitle",
|
||||
0x9c9c: "XPComment",
|
||||
0x9c9d: "XPAuthor",
|
||||
0x9c9e: "XPKeywords",
|
||||
0x9c9f: "XPSubject",
|
||||
0xa000: "FlashPixVersion",
|
||||
0xa001: "ColorSpace",
|
||||
0xa002: "ExifImageWidth",
|
||||
0xa003: "ExifImageHeight",
|
||||
0xa004: "RelatedSoundFile",
|
||||
0xa005: "ExifInteroperabilityOffset",
|
||||
0xa20b: "FlashEnergy",
|
||||
0xa20c: "SpatialFrequencyResponse",
|
||||
0xa20e: "FocalPlaneXResolution",
|
||||
0xa20f: "FocalPlaneYResolution",
|
||||
0xa210: "FocalPlaneResolutionUnit",
|
||||
0xa214: "SubjectLocation",
|
||||
0xa215: "ExposureIndex",
|
||||
0xa217: "SensingMethod",
|
||||
0xa300: "FileSource",
|
||||
0xa301: "SceneType",
|
||||
0xa302: "CFAPattern",
|
||||
0xa401: "CustomRendered",
|
||||
0xa402: "ExposureMode",
|
||||
0xa403: "WhiteBalance",
|
||||
0xa404: "DigitalZoomRatio",
|
||||
0xa405: "FocalLengthIn35mmFilm",
|
||||
0xa406: "SceneCaptureType",
|
||||
0xa407: "GainControl",
|
||||
0xa408: "Contrast",
|
||||
0xa409: "Saturation",
|
||||
0xa40a: "Sharpness",
|
||||
0xa40b: "DeviceSettingDescription",
|
||||
0xa40c: "SubjectDistanceRange",
|
||||
0xa420: "ImageUniqueID",
|
||||
0xa430: "CameraOwnerName",
|
||||
0xa431: "BodySerialNumber",
|
||||
0xa432: "LensSpecification",
|
||||
0xa433: "LensMake",
|
||||
0xa434: "LensModel",
|
||||
0xa435: "LensSerialNumber",
|
||||
0xa500: "Gamma",
|
||||
0xc4a5: "PrintImageMatching",
|
||||
0xc612: "DNGVersion",
|
||||
0xc613: "DNGBackwardVersion",
|
||||
0xc614: "UniqueCameraModel",
|
||||
0xc615: "LocalizedCameraModel",
|
||||
0xc616: "CFAPlaneColor",
|
||||
0xc617: "CFALayout",
|
||||
0xc618: "LinearizationTable",
|
||||
0xc619: "BlackLevelRepeatDim",
|
||||
0xc61a: "BlackLevel",
|
||||
0xc61b: "BlackLevelDeltaH",
|
||||
0xc61c: "BlackLevelDeltaV",
|
||||
0xc61d: "WhiteLevel",
|
||||
0xc61e: "DefaultScale",
|
||||
0xc61f: "DefaultCropOrigin",
|
||||
0xc620: "DefaultCropSize",
|
||||
0xc621: "ColorMatrix1",
|
||||
0xc622: "ColorMatrix2",
|
||||
0xc623: "CameraCalibration1",
|
||||
0xc624: "CameraCalibration2",
|
||||
0xc625: "ReductionMatrix1",
|
||||
0xc626: "ReductionMatrix2",
|
||||
0xc627: "AnalogBalance",
|
||||
0xc628: "AsShotNeutral",
|
||||
0xc629: "AsShotWhiteXY",
|
||||
0xc62a: "BaselineExposure",
|
||||
0xc62b: "BaselineNoise",
|
||||
0xc62c: "BaselineSharpness",
|
||||
0xc62d: "BayerGreenSplit",
|
||||
0xc62e: "LinearResponseLimit",
|
||||
0xc62f: "CameraSerialNumber",
|
||||
0xc630: "LensInfo",
|
||||
0xc631: "ChromaBlurRadius",
|
||||
0xc632: "AntiAliasStrength",
|
||||
0xc633: "ShadowScale",
|
||||
0xc634: "DNGPrivateData",
|
||||
0xc635: "MakerNoteSafety",
|
||||
0xc65a: "CalibrationIlluminant1",
|
||||
0xc65b: "CalibrationIlluminant2",
|
||||
0xc65c: "BestQualityScale",
|
||||
0xc65d: "RawDataUniqueID",
|
||||
0xc68b: "OriginalRawFileName",
|
||||
0xc68c: "OriginalRawFileData",
|
||||
0xc68d: "ActiveArea",
|
||||
0xc68e: "MaskedAreas",
|
||||
0xc68f: "AsShotICCProfile",
|
||||
0xc690: "AsShotPreProfileMatrix",
|
||||
0xc691: "CurrentICCProfile",
|
||||
0xc692: "CurrentPreProfileMatrix",
|
||||
0xc6bf: "ColorimetricReference",
|
||||
0xc6f3: "CameraCalibrationSignature",
|
||||
0xc6f4: "ProfileCalibrationSignature",
|
||||
0xc6f6: "AsShotProfileName",
|
||||
0xc6f7: "NoiseReductionApplied",
|
||||
0xc6f8: "ProfileName",
|
||||
0xc6f9: "ProfileHueSatMapDims",
|
||||
0xc6fa: "ProfileHueSatMapData1",
|
||||
0xc6fb: "ProfileHueSatMapData2",
|
||||
0xc6fc: "ProfileToneCurve",
|
||||
0xc6fd: "ProfileEmbedPolicy",
|
||||
0xc6fe: "ProfileCopyright",
|
||||
0xc714: "ForwardMatrix1",
|
||||
0xc715: "ForwardMatrix2",
|
||||
0xc716: "PreviewApplicationName",
|
||||
0xc717: "PreviewApplicationVersion",
|
||||
0xc718: "PreviewSettingsName",
|
||||
0xc719: "PreviewSettingsDigest",
|
||||
0xc71a: "PreviewColorSpace",
|
||||
0xc71b: "PreviewDateTime",
|
||||
0xc71c: "RawImageDigest",
|
||||
0xc71d: "OriginalRawFileDigest",
|
||||
0xc71e: "SubTileBlockSize",
|
||||
0xc71f: "RowInterleaveFactor",
|
||||
0xc725: "ProfileLookTableDims",
|
||||
0xc726: "ProfileLookTableData",
|
||||
0xc740: "OpcodeList1",
|
||||
0xc741: "OpcodeList2",
|
||||
0xc74e: "OpcodeList3",
|
||||
0xc761: "NoiseProfile"
|
||||
}
|
||||
|
||||
##
|
||||
# Maps EXIF GPS tags to tag names.
|
||||
|
||||
GPSTAGS = {
|
||||
0: "GPSVersionID",
|
||||
1: "GPSLatitudeRef",
|
||||
2: "GPSLatitude",
|
||||
3: "GPSLongitudeRef",
|
||||
4: "GPSLongitude",
|
||||
5: "GPSAltitudeRef",
|
||||
6: "GPSAltitude",
|
||||
7: "GPSTimeStamp",
|
||||
8: "GPSSatellites",
|
||||
9: "GPSStatus",
|
||||
10: "GPSMeasureMode",
|
||||
11: "GPSDOP",
|
||||
12: "GPSSpeedRef",
|
||||
13: "GPSSpeed",
|
||||
14: "GPSTrackRef",
|
||||
15: "GPSTrack",
|
||||
16: "GPSImgDirectionRef",
|
||||
17: "GPSImgDirection",
|
||||
18: "GPSMapDatum",
|
||||
19: "GPSDestLatitudeRef",
|
||||
20: "GPSDestLatitude",
|
||||
21: "GPSDestLongitudeRef",
|
||||
22: "GPSDestLongitude",
|
||||
23: "GPSDestBearingRef",
|
||||
24: "GPSDestBearing",
|
||||
25: "GPSDestDistanceRef",
|
||||
26: "GPSDestDistance",
|
||||
27: "GPSProcessingMethod",
|
||||
28: "GPSAreaInformation",
|
||||
29: "GPSDateStamp",
|
||||
30: "GPSDifferential",
|
||||
31: "GPSHPositioningError",
|
||||
}
|
||||
76
PIL/FitsStubImagePlugin.py
Normal file
76
PIL/FitsStubImagePlugin.py
Normal file
@ -0,0 +1,76 @@
|
||||
#
|
||||
# The Python Imaging Library
|
||||
# $Id$
|
||||
#
|
||||
# FITS stub adapter
|
||||
#
|
||||
# Copyright (c) 1998-2003 by Fredrik Lundh
|
||||
#
|
||||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
from . import Image, ImageFile
|
||||
|
||||
_handler = None
|
||||
|
||||
|
||||
def register_handler(handler):
|
||||
"""
|
||||
Install application-specific FITS image handler.
|
||||
|
||||
:param handler: Handler object.
|
||||
"""
|
||||
global _handler
|
||||
_handler = handler
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Image adapter
|
||||
|
||||
|
||||
def _accept(prefix):
|
||||
return prefix[:6] == b"SIMPLE"
|
||||
|
||||
|
||||
class FITSStubImageFile(ImageFile.StubImageFile):
|
||||
|
||||
format = "FITS"
|
||||
format_description = "FITS"
|
||||
|
||||
def _open(self):
|
||||
|
||||
offset = self.fp.tell()
|
||||
|
||||
if not _accept(self.fp.read(6)):
|
||||
raise SyntaxError("Not a FITS file")
|
||||
|
||||
# FIXME: add more sanity checks here; mandatory header items
|
||||
# include SIMPLE, BITPIX, NAXIS, etc.
|
||||
|
||||
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):
|
||||
return _handler
|
||||
|
||||
|
||||
def _save(im, fp, filename):
|
||||
if _handler is None or not hasattr("_handler", "save"):
|
||||
raise IOError("FITS save handler not installed")
|
||||
_handler.save(im, fp, filename)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Registry
|
||||
|
||||
Image.register_open(FITSStubImageFile.format, FITSStubImageFile, _accept)
|
||||
Image.register_save(FITSStubImageFile.format, _save)
|
||||
|
||||
Image.register_extension(FITSStubImageFile.format, ".fit")
|
||||
Image.register_extension(FITSStubImageFile.format, ".fits")
|
||||
185
PIL/FliImagePlugin.py
Normal file
185
PIL/FliImagePlugin.py
Normal file
@ -0,0 +1,185 @@
|
||||
#
|
||||
# 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 . import Image, ImageFile, ImagePalette
|
||||
from ._binary import i8, i16le as i16, i32le as i32, o8
|
||||
|
||||
__version__ = "0.2"
|
||||
|
||||
|
||||
#
|
||||
# decoder
|
||||
|
||||
def _accept(prefix):
|
||||
return len(prefix) >= 6 and i16(prefix[4:6]) in [0xAF11, 0xAF12]
|
||||
|
||||
|
||||
##
|
||||
# Image plugin for the FLI/FLC animation format. Use the <b>seek</b>
|
||||
# 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)
|
||||
magic = i16(s[4:6])
|
||||
if not (magic in [0xAF11, 0xAF12] and
|
||||
i16(s[14:16]) in [0, 3] and # flags
|
||||
s[20:22] == b"\x00\x00"): # reserved
|
||||
raise SyntaxError("not an FLI/FLC file")
|
||||
|
||||
# image characteristics
|
||||
self.mode = "P"
|
||||
self.size = i16(s[8:10]), i16(s[10:12])
|
||||
|
||||
# animation speed
|
||||
duration = i32(s[16:20])
|
||||
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:6]) == 0xF100:
|
||||
# prefix chunk; ignore it
|
||||
self.__offset = self.__offset + i32(s)
|
||||
s = self.fp.read(16)
|
||||
|
||||
if i16(s[4:6]) == 0xF1FA:
|
||||
# look for palette chunk
|
||||
s = self.fp.read(6)
|
||||
if i16(s[4:6]) == 11:
|
||||
self._palette(palette, 2)
|
||||
elif i16(s[4:6]) == 4:
|
||||
self._palette(palette, 0)
|
||||
|
||||
palette = [o8(r)+o8(g)+o8(b) for (r, g, b) in palette]
|
||||
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._n_frames = None
|
||||
self._is_animated = None
|
||||
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 + i8(s[0])
|
||||
n = i8(s[1])
|
||||
if n == 0:
|
||||
n = 256
|
||||
s = self.fp.read(n * 3)
|
||||
for n in range(0, len(s), 3):
|
||||
r = i8(s[n]) << shift
|
||||
g = i8(s[n+1]) << shift
|
||||
b = i8(s[n+2]) << shift
|
||||
palette[i] = (r, g, b)
|
||||
i += 1
|
||||
|
||||
@property
|
||||
def n_frames(self):
|
||||
if self._n_frames is None:
|
||||
current = self.tell()
|
||||
try:
|
||||
while True:
|
||||
self.seek(self.tell() + 1)
|
||||
except EOFError:
|
||||
self._n_frames = self.tell() + 1
|
||||
self.seek(current)
|
||||
return self._n_frames
|
||||
|
||||
@property
|
||||
def is_animated(self):
|
||||
if self._is_animated is None:
|
||||
current = self.tell()
|
||||
|
||||
try:
|
||||
self.seek(1)
|
||||
self._is_animated = True
|
||||
except EOFError:
|
||||
self._is_animated = False
|
||||
|
||||
self.seek(current)
|
||||
return self._is_animated
|
||||
|
||||
def seek(self, frame):
|
||||
if frame == self.__frame:
|
||||
return
|
||||
if frame < self.__frame:
|
||||
self._seek(0)
|
||||
|
||||
last_frame = self.__frame
|
||||
for f in range(self.__frame + 1, frame + 1):
|
||||
try:
|
||||
self._seek(f)
|
||||
except EOFError:
|
||||
self.seek(last_frame)
|
||||
raise EOFError("no more images in FLI file")
|
||||
|
||||
def _seek(self, frame):
|
||||
if frame == 0:
|
||||
self.__frame = -1
|
||||
self.__fp.seek(self.__rewind)
|
||||
self.__offset = 128
|
||||
|
||||
if frame != self.__frame + 1:
|
||||
raise ValueError("cannot seek to frame %d" % frame)
|
||||
self.__frame = frame
|
||||
|
||||
# move to next frame
|
||||
self.fp = self.__fp
|
||||
self.fp.seek(self.__offset)
|
||||
|
||||
s = self.fp.read(4)
|
||||
if not s:
|
||||
raise EOFError
|
||||
|
||||
framesize = i32(s)
|
||||
|
||||
self.decodermaxblock = framesize
|
||||
self.tile = [("fli", (0, 0)+self.size, self.__offset, None)]
|
||||
|
||||
self.__offset += framesize
|
||||
|
||||
def tell(self):
|
||||
return self.__frame
|
||||
|
||||
#
|
||||
# registry
|
||||
|
||||
Image.register_open(FliImageFile.format, FliImageFile, _accept)
|
||||
|
||||
Image.register_extension(FliImageFile.format, ".fli")
|
||||
Image.register_extension(FliImageFile.format, ".flc")
|
||||
114
PIL/FontFile.py
Normal file
114
PIL/FontFile.py
Normal file
@ -0,0 +1,114 @@
|
||||
#
|
||||
# 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 print_function
|
||||
|
||||
import os
|
||||
from . import Image, _binary
|
||||
|
||||
WIDTH = 800
|
||||
|
||||
|
||||
def puti16(fp, values):
|
||||
# write network order (big-endian) 16-bit sequence
|
||||
for v in values:
|
||||
if v < 0:
|
||||
v += 65536
|
||||
fp.write(_binary.o16be(v))
|
||||
|
||||
|
||||
##
|
||||
# Base class for raster font file handlers.
|
||||
|
||||
class FontFile(object):
|
||||
|
||||
bitmap = None
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self.info = {}
|
||||
self.glyph = [None] * 256
|
||||
|
||||
def __getitem__(self, ix):
|
||||
return self.glyph[ix]
|
||||
|
||||
def compile(self):
|
||||
"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:
|
||||
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 = [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]
|
||||
# yy = src[3] - src[1]
|
||||
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)
|
||||
# print(chr(i), dst, s)
|
||||
self.metrics[i] = d, dst, s
|
||||
|
||||
def save(self, filename):
|
||||
"Save font"
|
||||
|
||||
self.compile()
|
||||
|
||||
# font data
|
||||
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((";;;;;;%d;\n" % self.ysize).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])
|
||||
@ -14,82 +14,79 @@
|
||||
#
|
||||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
from __future__ import annotations
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
from . import Image, ImageFile
|
||||
from ._binary import i32le as i32, i8
|
||||
|
||||
import olefile
|
||||
|
||||
from . import Image, ImageFile
|
||||
from ._binary import i32le as i32
|
||||
__version__ = "0.1"
|
||||
|
||||
# we map from colour field tuples to (mode, rawmode) descriptors
|
||||
MODES = {
|
||||
# opacity
|
||||
(0x00007FFE,): ("A", "L"),
|
||||
(0x00007ffe): ("A", "L"),
|
||||
# monochrome
|
||||
(0x00010000,): ("L", "L"),
|
||||
(0x00018000, 0x00017FFE): ("RGBA", "LA"),
|
||||
(0x00018000, 0x00017ffe): ("RGBA", "LA"),
|
||||
# photo YCC
|
||||
(0x00020000, 0x00020001, 0x00020002): ("RGB", "YCC;P"),
|
||||
(0x00028000, 0x00028001, 0x00028002, 0x00027FFE): ("RGBA", "YCCA;P"),
|
||||
(0x00028000, 0x00028001, 0x00028002, 0x00027ffe): ("RGBA", "YCCA;P"),
|
||||
# standard RGB (NIFRGB)
|
||||
(0x00030000, 0x00030001, 0x00030002): ("RGB", "RGB"),
|
||||
(0x00038000, 0x00038001, 0x00038002, 0x00037FFE): ("RGBA", "RGBA"),
|
||||
(0x00038000, 0x00038001, 0x00038002, 0x00037ffe): ("RGBA", "RGBA"),
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
|
||||
def _accept(prefix: bytes) -> bool:
|
||||
return prefix.startswith(olefile.MAGIC)
|
||||
def _accept(prefix):
|
||||
return prefix[:8] == olefile.MAGIC
|
||||
|
||||
|
||||
##
|
||||
# Image plugin for the FlashPix images.
|
||||
|
||||
|
||||
class FpxImageFile(ImageFile.ImageFile):
|
||||
|
||||
format = "FPX"
|
||||
format_description = "FlashPix"
|
||||
|
||||
def _open(self) -> None:
|
||||
def _open(self):
|
||||
#
|
||||
# read the OLE directory and see if this is a likely
|
||||
# to be a FlashPix file
|
||||
|
||||
assert self.fp is not None
|
||||
try:
|
||||
self.ole = olefile.OleFileIO(self.fp)
|
||||
except OSError as e:
|
||||
msg = "not an FPX file; invalid OLE file"
|
||||
raise SyntaxError(msg) from e
|
||||
except IOError:
|
||||
raise SyntaxError("not an FPX file; invalid OLE file")
|
||||
|
||||
root = self.ole.root
|
||||
if not root or root.clsid != "56616700-C154-11CE-8553-00AA00A1F95B":
|
||||
msg = "not an FPX file; bad root CLSID"
|
||||
raise SyntaxError(msg)
|
||||
if self.ole.root.clsid != "56616700-C154-11CE-8553-00AA00A1F95B":
|
||||
raise SyntaxError("not an FPX file; bad root CLSID")
|
||||
|
||||
self._open_index(1)
|
||||
|
||||
def _open_index(self, index: int = 1) -> None:
|
||||
def _open_index(self, index=1):
|
||||
#
|
||||
# get the Image Contents Property Set
|
||||
|
||||
prop = self.ole.getproperties(
|
||||
[f"Data Object Store {index:06d}", "\005Image Contents"]
|
||||
)
|
||||
prop = self.ole.getproperties([
|
||||
"Data Object Store %06d" % index,
|
||||
"\005Image Contents"
|
||||
])
|
||||
|
||||
# size (highest resolution)
|
||||
|
||||
assert isinstance(prop[0x1000002], int)
|
||||
assert isinstance(prop[0x1000003], int)
|
||||
self._size = prop[0x1000002], prop[0x1000003]
|
||||
self.size = prop[0x1000002], prop[0x1000003]
|
||||
|
||||
size = max(self.size)
|
||||
i = 1
|
||||
while size > 64:
|
||||
size = size // 2
|
||||
size = size / 2
|
||||
i += 1
|
||||
self.maxid = i - 1
|
||||
|
||||
@ -103,14 +100,12 @@ class FpxImageFile(ImageFile.ImageFile):
|
||||
|
||||
s = prop[0x2000002 | id]
|
||||
|
||||
if not isinstance(s, bytes) or (bands := i32(s, 4)) > 4:
|
||||
msg = "Invalid number of bands"
|
||||
raise OSError(msg)
|
||||
colors = []
|
||||
for i in range(i32(s, 4)):
|
||||
# note: for now, we ignore the "uncalibrated" flag
|
||||
colors.append(i32(s, 8+i*4) & 0x7fffffff)
|
||||
|
||||
# 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]
|
||||
self.mode, self.rawmode = MODES[tuple(colors)]
|
||||
|
||||
# load JPEG tables, if any
|
||||
self.jpeg = {}
|
||||
@ -119,16 +114,18 @@ class FpxImageFile(ImageFile.ImageFile):
|
||||
if id in prop:
|
||||
self.jpeg[i] = prop[id]
|
||||
|
||||
# print(len(self.jpeg), "tables loaded")
|
||||
|
||||
self._open_subimage(1, self.maxid)
|
||||
|
||||
def _open_subimage(self, index: int = 1, subimage: int = 0) -> None:
|
||||
def _open_subimage(self, index=1, subimage=0):
|
||||
#
|
||||
# setup tile descriptors for a given subimage
|
||||
|
||||
stream = [
|
||||
f"Data Object Store {index:06d}",
|
||||
f"Resolution {subimage:04d}",
|
||||
"Subimage 0000 Header",
|
||||
"Data Object Store %06d" % index,
|
||||
"Resolution %04d" % subimage,
|
||||
"Subimage 0000 Header"
|
||||
]
|
||||
|
||||
fp = self.ole.openstream(stream)
|
||||
@ -141,14 +138,15 @@ class FpxImageFile(ImageFile.ImageFile):
|
||||
|
||||
size = i32(s, 4), i32(s, 8)
|
||||
# tilecount = i32(s, 12)
|
||||
xtile, ytile = i32(s, 16), i32(s, 20)
|
||||
tilesize = i32(s, 16), i32(s, 20)
|
||||
# channels = i32(s, 24)
|
||||
offset = i32(s, 28)
|
||||
length = i32(s, 32)
|
||||
|
||||
# print(size, self.mode, self.rawmode)
|
||||
|
||||
if size != self.size:
|
||||
msg = "subimage mismatch"
|
||||
raise OSError(msg)
|
||||
raise IOError("subimage mismatch")
|
||||
|
||||
# get tile descriptors
|
||||
fp.seek(28 + offset)
|
||||
@ -156,38 +154,27 @@ class FpxImageFile(ImageFile.ImageFile):
|
||||
|
||||
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)
|
||||
compression = i32(s, i+8)
|
||||
|
||||
if compression == 0:
|
||||
self.tile.append(
|
||||
ImageFile._Tile(
|
||||
"raw",
|
||||
(x, y, x1, y1),
|
||||
i32(s, i) + 28,
|
||||
self.rawmode,
|
||||
)
|
||||
)
|
||||
self.tile.append(("raw", (x, y, x+xtile, y+ytile),
|
||||
i32(s, i) + 28, (self.rawmode)))
|
||||
|
||||
elif compression == 1:
|
||||
|
||||
# FIXME: the fill decoder is not implemented
|
||||
self.tile.append(
|
||||
ImageFile._Tile(
|
||||
"fill",
|
||||
(x, y, x1, y1),
|
||||
i32(s, i) + 28,
|
||||
(self.rawmode, s[12:16]),
|
||||
)
|
||||
)
|
||||
self.tile.append(("fill", (x, y, x+xtile, y+ytile),
|
||||
i32(s, i) + 28, (self.rawmode, s[12:16])))
|
||||
|
||||
elif compression == 2:
|
||||
internal_color_conversion = s[14]
|
||||
jpeg_tables = s[15]
|
||||
|
||||
internal_color_conversion = i8(s[14])
|
||||
jpeg_tables = i8(s[15])
|
||||
rawmode = self.rawmode
|
||||
|
||||
if internal_color_conversion:
|
||||
@ -204,14 +191,8 @@ class FpxImageFile(ImageFile.ImageFile):
|
||||
# The image is stored as defined by rawmode
|
||||
jpegmode = rawmode
|
||||
|
||||
self.tile.append(
|
||||
ImageFile._Tile(
|
||||
"jpeg",
|
||||
(x, y, x1, y1),
|
||||
i32(s, i) + 28,
|
||||
(rawmode, jpegmode),
|
||||
)
|
||||
)
|
||||
self.tile.append(("jpeg", (x, y, x+xtile, y+ytile),
|
||||
i32(s, i) + 28, (rawmode, jpegmode)))
|
||||
|
||||
# FIXME: jpeg tables are tile dependent; the prefix
|
||||
# data must be placed in the tile descriptor itself!
|
||||
@ -220,39 +201,28 @@ class FpxImageFile(ImageFile.ImageFile):
|
||||
self.tile_prefix = self.jpeg[jpeg_tables]
|
||||
|
||||
else:
|
||||
msg = "unknown/invalid compression"
|
||||
raise OSError(msg)
|
||||
raise IOError("unknown/invalid compression")
|
||||
|
||||
x += xtile
|
||||
x = x + xtile
|
||||
if x >= xsize:
|
||||
x, y = 0, y + ytile
|
||||
if y >= ysize:
|
||||
break # isn't really required
|
||||
|
||||
assert self.fp is not None
|
||||
self.stream = stream
|
||||
self._fp = self.fp
|
||||
self.fp = None
|
||||
|
||||
def load(self) -> Image.core.PixelAccess | None:
|
||||
def load(self):
|
||||
|
||||
if not self.fp:
|
||||
self.fp = self.ole.openstream(self.stream[:2] + ["Subimage 0000 Data"])
|
||||
self.fp = self.ole.openstream(self.stream[:2] +
|
||||
["Subimage 0000 Data"])
|
||||
|
||||
return ImageFile.ImageFile.load(self)
|
||||
|
||||
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")
|
||||
@ -9,8 +9,7 @@ Full text of the CC0 license:
|
||||
Independence War 2: Edge Of Chaos - Texture File Format - 16 October 2001
|
||||
|
||||
The textures used for 3D objects in Independence War 2: Edge Of Chaos are in a
|
||||
packed custom format called FTEX. This file format uses file extensions FTC
|
||||
and FTU.
|
||||
packed custom format called FTEX. This file format uses file extensions FTC and FTU.
|
||||
* FTC files are compressed textures (using standard texture compression).
|
||||
* FTU files are not compressed.
|
||||
Texture File Format
|
||||
@ -20,96 +19,77 @@ has the following structure:
|
||||
{format_directory}
|
||||
{data}
|
||||
Where:
|
||||
{header} = {
|
||||
u32:magic,
|
||||
u32:version,
|
||||
u32:width,
|
||||
u32:height,
|
||||
u32:mipmap_count,
|
||||
u32:format_count
|
||||
}
|
||||
{header} = { u32:magic, u32:version, u32:width, u32:height, u32:mipmap_count, u32:format_count }
|
||||
|
||||
* The "magic" number is "FTEX".
|
||||
* "width" and "height" are the dimensions of the texture.
|
||||
* "mipmap_count" is the number of mipmaps in the texture.
|
||||
* "format_count" is the number of texture formats (different versions of the
|
||||
same texture) in this file.
|
||||
* "format_count" is the number of texture formats (different versions of the same texture) in this file.
|
||||
|
||||
{format_directory} = format_count * { u32:format, u32:where }
|
||||
|
||||
The format value is 0 for DXT1 compressed textures and 1 for 24-bit RGB
|
||||
uncompressed textures.
|
||||
The format value is 0 for DXT1 compressed textures and 1 for 24-bit RGB uncompressed textures.
|
||||
The texture data for a format starts at the position "where" in the file.
|
||||
|
||||
Each set of texture data in the file has the following structure:
|
||||
{data} = format_count * { u32:mipmap_size, mipmap_size * { u8 } }
|
||||
* "mipmap_size" is the number of bytes in that mip level. For compressed
|
||||
textures this is the size of the texture data compressed with DXT1. For 24 bit
|
||||
uncompressed textures, this is 3 * width * height. Following this are the image
|
||||
bytes for that mipmap level.
|
||||
* "mipmap_size" is the number of bytes in that mip level. For compressed textures this is the
|
||||
size of the texture data compressed with DXT1. For 24 bit uncompressed textures, this is 3 * width * height.
|
||||
Following this are the image bytes for that mipmap level.
|
||||
|
||||
Note: All data is stored in little-Endian (Intel) byte order.
|
||||
"""
|
||||
|
||||
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
|
||||
FORMAT_DXT1 = 0
|
||||
FORMAT_UNCOMPRESSED = 1
|
||||
|
||||
|
||||
class FtexImageFile(ImageFile.ImageFile):
|
||||
format = "FTEX"
|
||||
format_description = "Texture File Format (IW2:EOC)"
|
||||
|
||||
def _open(self) -> None:
|
||||
assert self.fp is not None
|
||||
if not _accept(self.fp.read(4)):
|
||||
msg = "not an FTEX file"
|
||||
raise SyntaxError(msg)
|
||||
struct.unpack("<i", self.fp.read(4)) # version
|
||||
self._size = struct.unpack("<2i", self.fp.read(8))
|
||||
def _open(self):
|
||||
magic = struct.unpack("<I", self.fp.read(4))
|
||||
version = struct.unpack("<i", self.fp.read(4))
|
||||
self.size = struct.unpack("<2i", self.fp.read(8))
|
||||
mipmap_count, format_count = struct.unpack("<2i", self.fp.read(8))
|
||||
|
||||
# Only support single-format files.
|
||||
# I don't know of any multi-format file.
|
||||
self.mode = "RGB"
|
||||
|
||||
# Only support single-format files. I don't know of any multi-format file.
|
||||
assert format_count == 1
|
||||
|
||||
format, where = struct.unpack("<2i", self.fp.read(8))
|
||||
self.fp.seek(where)
|
||||
(mipmap_size,) = struct.unpack("<i", self.fp.read(4))
|
||||
mipmap_size, = struct.unpack("<i", self.fp.read(4))
|
||||
|
||||
data = self.fp.read(mipmap_size)
|
||||
|
||||
if format == Format.DXT1:
|
||||
self._mode = "RGBA"
|
||||
self.tile = [ImageFile._Tile("bcn", (0, 0) + self.size, 0, (1,))]
|
||||
elif format == Format.UNCOMPRESSED:
|
||||
self._mode = "RGB"
|
||||
self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, "RGB")]
|
||||
if format == FORMAT_DXT1:
|
||||
self.mode = "RGBA"
|
||||
self.tile = [("bcn", (0, 0) + self.size, 0, (1))]
|
||||
elif format == FORMAT_UNCOMPRESSED:
|
||||
self.tile = [("raw", (0, 0) + self.size, 0, ('RGB', 0, 1))]
|
||||
else:
|
||||
msg = f"Invalid texture compression format: {repr(format)}"
|
||||
raise ValueError(msg)
|
||||
raise ValueError("Invalid texture compression format: %r" % (format))
|
||||
|
||||
self.fp.close()
|
||||
self.fp = BytesIO(data)
|
||||
|
||||
def load_seek(self, pos: int) -> None:
|
||||
def load_seek(self, pos):
|
||||
pass
|
||||
|
||||
|
||||
def _accept(prefix: bytes) -> bool:
|
||||
return prefix.startswith(MAGIC)
|
||||
def _validate(prefix):
|
||||
return prefix[:4] == MAGIC
|
||||
|
||||
|
||||
Image.register_open(FtexImageFile.format, FtexImageFile, _accept)
|
||||
Image.register_extensions(FtexImageFile.format, [".ftc", ".ftu"])
|
||||
Image.register_open(FtexImageFile.format, FtexImageFile, _validate)
|
||||
Image.register_extension(FtexImageFile.format, ".ftc")
|
||||
Image.register_extension(FtexImageFile.format, ".ftu")
|
||||
@ -14,7 +14,7 @@
|
||||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
#
|
||||
# See https://github.com/GNOME/gimp/blob/mainline/devel-docs/gbr.txt for
|
||||
# See https://github.com/GNOME/gimp/blob/master/devel-docs/gbr.txt for
|
||||
# format documentation.
|
||||
#
|
||||
# This code Interprets version 1 and 2 .gbr files.
|
||||
@ -23,63 +23,58 @@
|
||||
# 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)
|
||||
def _accept(prefix):
|
||||
return len(prefix) >= 8 and i32(prefix[:4]) >= 20 and i32(prefix[4:8]) 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:
|
||||
assert self.fp is not None
|
||||
def _open(self):
|
||||
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 header_size < 20:
|
||||
raise SyntaxError("not a GIMP brush")
|
||||
if version not in (1, 2):
|
||||
msg = f"Unsupported GIMP brush version: {version}"
|
||||
raise SyntaxError(msg)
|
||||
raise SyntaxError("Unsupported GIMP brush version: %s" % version)
|
||||
|
||||
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 width <= 0 or height <= 0:
|
||||
raise SyntaxError("not a GIMP brush")
|
||||
if color_depth not in (1, 4):
|
||||
msg = f"Unsupported GIMP brush color depth: {color_depth}"
|
||||
raise SyntaxError(msg)
|
||||
raise SyntaxError("Unsupported GIMP brush color depth: %s" % color_depth)
|
||||
|
||||
if version == 1:
|
||||
comment_length = header_size - 20
|
||||
comment_length = header_size-20
|
||||
else:
|
||||
comment_length = header_size - 28
|
||||
comment_length = header_size-28
|
||||
magic_number = self.fp.read(4)
|
||||
if magic_number != b"GIMP":
|
||||
msg = "not a GIMP brush, bad magic number"
|
||||
raise SyntaxError(msg)
|
||||
self.info["spacing"] = i32(self.fp.read(4))
|
||||
if magic_number != b'GIMP':
|
||||
raise SyntaxError("not a GIMP brush, bad magic number")
|
||||
self.info['spacing'] = i32(self.fp.read(4))
|
||||
|
||||
self.info["comment"] = self.fp.read(comment_length)[:-1]
|
||||
comment = self.fp.read(comment_length)[:-1]
|
||||
|
||||
if color_depth == 1:
|
||||
self._mode = "L"
|
||||
self.mode = "L"
|
||||
else:
|
||||
self._mode = "RGBA"
|
||||
self.mode = 'RGBA'
|
||||
|
||||
self._size = width, height
|
||||
self.size = width, height
|
||||
|
||||
self.info["comment"] = comment
|
||||
|
||||
# Image might not be small
|
||||
Image._decompression_bomb_check(self.size)
|
||||
@ -87,17 +82,12 @@ class GbrImageFile(ImageFile.ImageFile):
|
||||
# Data is an uncompressed block of w * h * bytes/pixel
|
||||
self._data_size = width * height * color_depth
|
||||
|
||||
def load(self) -> Image.core.PixelAccess | None:
|
||||
if self._im is None:
|
||||
assert self.fp is not None
|
||||
self.im = Image.core.new(self.mode, self.size)
|
||||
self.frombytes(self.fp.read(self._data_size))
|
||||
return Image.Image.load(self)
|
||||
|
||||
def load(self):
|
||||
self.im = Image.core.new(self.mode, self.size)
|
||||
self.frombytes(self.fp.read(self._data_size))
|
||||
|
||||
#
|
||||
# registry
|
||||
|
||||
|
||||
Image.register_open(GbrImageFile.format, GbrImageFile, _accept)
|
||||
Image.register_extension(GbrImageFile.format, ".gbr")
|
||||
90
PIL/GdImageFile.py
Normal file
90
PIL/GdImageFile.py
Normal file
@ -0,0 +1,90 @@
|
||||
#
|
||||
# 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 Image.open(). To open a
|
||||
# gd file, use the GdImageFile.open() function instead.
|
||||
|
||||
# THE GD FORMAT IS NOT DESIGNED FOR DATA INTERCHANGE. This
|
||||
# implementation is provided for convenience and demonstrational
|
||||
# purposes only.
|
||||
|
||||
|
||||
from . import ImageFile, ImagePalette
|
||||
from ._binary import i16be as i16
|
||||
from ._util import isPath
|
||||
|
||||
__version__ = "0.1"
|
||||
|
||||
try:
|
||||
import builtins
|
||||
except ImportError:
|
||||
import __builtin__
|
||||
builtins = __builtin__
|
||||
|
||||
|
||||
##
|
||||
# Image plugin for the GD uncompressed format. Note that this format
|
||||
# is not supported by the standard <b>Image.open</b> function. To use
|
||||
# this plugin, you have to import the <b>GdImageFile</b> module and
|
||||
# use the <b>GdImageFile.open</b> function.
|
||||
|
||||
class GdImageFile(ImageFile.ImageFile):
|
||||
|
||||
format = "GD"
|
||||
format_description = "GD uncompressed images"
|
||||
|
||||
def _open(self):
|
||||
|
||||
# Header
|
||||
s = self.fp.read(775)
|
||||
|
||||
self.mode = "L" # FIXME: "P"
|
||||
self.size = i16(s[0:2]), i16(s[2:4])
|
||||
|
||||
# transparency index
|
||||
tindex = i16(s[5:7])
|
||||
if tindex < 256:
|
||||
self.info["transparent"] = tindex
|
||||
|
||||
self.palette = ImagePalette.raw("RGB", s[7:])
|
||||
|
||||
self.tile = [("raw", (0, 0)+self.size, 775, ("L", 0, -1))]
|
||||
|
||||
|
||||
def open(fp, mode="r"):
|
||||
"""
|
||||
Load texture from a GD image file.
|
||||
|
||||
:param filename: GD file name, or an opened file handle.
|
||||
:param mode: Optional mode. In this version, if the mode argument
|
||||
is given, it must be "r".
|
||||
:returns: An image instance.
|
||||
:raises IOError: If the image could not be read.
|
||||
"""
|
||||
if mode != "r":
|
||||
raise ValueError("bad mode")
|
||||
|
||||
if isPath(fp):
|
||||
filename = fp
|
||||
fp = builtins.open(fp, "rb")
|
||||
else:
|
||||
filename = ""
|
||||
|
||||
try:
|
||||
return GdImageFile(fp, filename)
|
||||
except SyntaxError:
|
||||
raise IOError("cannot identify this image file")
|
||||
790
PIL/GifImagePlugin.py
Normal file
790
PIL/GifImagePlugin.py
Normal file
@ -0,0 +1,790 @@
|
||||
#
|
||||
# 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 . import Image, ImageFile, ImagePalette, ImageChops, ImageSequence
|
||||
from ._binary import i8, i16le as i16, o8, o16le as o16
|
||||
|
||||
import itertools
|
||||
|
||||
__version__ = "0.9"
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Identify/read GIF files
|
||||
|
||||
def _accept(prefix):
|
||||
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):
|
||||
s = self.fp.read(1)
|
||||
if s and i8(s):
|
||||
return self.fp.read(i8(s))
|
||||
return None
|
||||
|
||||
def _open(self):
|
||||
|
||||
# Screen
|
||||
s = self.fp.read(13)
|
||||
if s[:6] not in [b"GIF87a", b"GIF89a"]:
|
||||
raise SyntaxError("not a GIF file")
|
||||
|
||||
self.info["version"] = s[:6]
|
||||
self.size = i16(s[6:]), i16(s[8:])
|
||||
self.tile = []
|
||||
flags = i8(s[10])
|
||||
bits = (flags & 7) + 1
|
||||
|
||||
if flags & 128:
|
||||
# get global palette
|
||||
self.info["background"] = i8(s[11])
|
||||
# check if palette contains colour indices
|
||||
p = self.fp.read(3 << bits)
|
||||
for i in range(0, len(p), 3):
|
||||
if not (i//3 == i8(p[i]) == i8(p[i+1]) == i8(p[i+2])):
|
||||
p = ImagePalette.raw("RGB", p)
|
||||
self.global_palette = self.palette = p
|
||||
break
|
||||
|
||||
self.__fp = self.fp # FIXME: hack
|
||||
self.__rewind = self.fp.tell()
|
||||
self._n_frames = None
|
||||
self._is_animated = None
|
||||
self._seek(0) # get ready to read first frame
|
||||
|
||||
@property
|
||||
def n_frames(self):
|
||||
if self._n_frames is None:
|
||||
current = self.tell()
|
||||
try:
|
||||
while True:
|
||||
self.seek(self.tell() + 1)
|
||||
except EOFError:
|
||||
self._n_frames = self.tell() + 1
|
||||
self.seek(current)
|
||||
return self._n_frames
|
||||
|
||||
@property
|
||||
def is_animated(self):
|
||||
if self._is_animated is None:
|
||||
current = self.tell()
|
||||
|
||||
try:
|
||||
self.seek(1)
|
||||
self._is_animated = True
|
||||
except EOFError:
|
||||
self._is_animated = False
|
||||
|
||||
self.seek(current)
|
||||
return self._is_animated
|
||||
|
||||
def seek(self, frame):
|
||||
if frame == self.__frame:
|
||||
return
|
||||
if frame < self.__frame:
|
||||
self._seek(0)
|
||||
|
||||
last_frame = self.__frame
|
||||
for f in range(self.__frame + 1, frame + 1):
|
||||
try:
|
||||
self._seek(f)
|
||||
except EOFError:
|
||||
self.seek(last_frame)
|
||||
raise EOFError("no more images in GIF file")
|
||||
|
||||
def _seek(self, frame):
|
||||
|
||||
if frame == 0:
|
||||
# rewind
|
||||
self.__offset = 0
|
||||
self.dispose = None
|
||||
self.dispose_extent = [0, 0, 0, 0] # x0, y0, x1, y1
|
||||
self.__frame = -1
|
||||
self.__fp.seek(self.__rewind)
|
||||
self._prev_im = None
|
||||
self.disposal_method = 0
|
||||
else:
|
||||
# ensure that the previous frame was loaded
|
||||
if not self.im:
|
||||
self.load()
|
||||
|
||||
if frame != self.__frame + 1:
|
||||
raise ValueError("cannot seek to frame %d" % frame)
|
||||
self.__frame = frame
|
||||
|
||||
self.tile = []
|
||||
|
||||
self.fp = self.__fp
|
||||
if self.__offset:
|
||||
# backup to last frame
|
||||
self.fp.seek(self.__offset)
|
||||
while self.data():
|
||||
pass
|
||||
self.__offset = 0
|
||||
|
||||
if self.dispose:
|
||||
self.im.paste(self.dispose, self.dispose_extent)
|
||||
|
||||
from copy import copy
|
||||
self.palette = copy(self.global_palette)
|
||||
|
||||
while True:
|
||||
|
||||
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 i8(s) == 249:
|
||||
#
|
||||
# graphic control extension
|
||||
#
|
||||
flags = i8(block[0])
|
||||
if flags & 1:
|
||||
self.info["transparency"] = i8(block[3])
|
||||
self.info["duration"] = i16(block[1:3]) * 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 i8(s) == 254:
|
||||
#
|
||||
# comment extension
|
||||
#
|
||||
self.info["comment"] = block
|
||||
elif i8(s) == 255:
|
||||
#
|
||||
# application extension
|
||||
#
|
||||
self.info["extension"] = block, self.fp.tell()
|
||||
if block[:11] == b"NETSCAPE2.0":
|
||||
block = self.data()
|
||||
if len(block) >= 3 and i8(block[0]) == 1:
|
||||
self.info["loop"] = i16(block[1:3])
|
||||
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:])
|
||||
self.dispose_extent = x0, y0, x1, y1
|
||||
flags = i8(s[8])
|
||||
|
||||
interlace = (flags & 64) != 0
|
||||
|
||||
if flags & 128:
|
||||
bits = (flags & 7) + 1
|
||||
self.palette =\
|
||||
ImagePalette.raw("RGB", self.fp.read(3 << bits))
|
||||
|
||||
# image data
|
||||
bits = i8(self.fp.read(1))
|
||||
self.__offset = self.fp.tell()
|
||||
self.tile = [("gif",
|
||||
(x0, y0, x1, y1),
|
||||
self.__offset,
|
||||
(bits, interlace))]
|
||||
break
|
||||
|
||||
else:
|
||||
pass
|
||||
# raise IOError, "illegal GIF tag `%x`" % i8(s)
|
||||
|
||||
try:
|
||||
if self.disposal_method < 2:
|
||||
# do not dispose or none specified
|
||||
self.dispose = None
|
||||
elif self.disposal_method == 2:
|
||||
# replace with background colour
|
||||
self.dispose = Image.core.fill("P", self.size,
|
||||
self.info["background"])
|
||||
else:
|
||||
# replace with previous contents
|
||||
if self.im:
|
||||
self.dispose = self.im.copy()
|
||||
|
||||
# only dispose the extent in this frame
|
||||
if self.dispose:
|
||||
self.dispose = self.dispose.crop(self.dispose_extent)
|
||||
except (AttributeError, KeyError):
|
||||
pass
|
||||
|
||||
if not self.tile:
|
||||
# self.__fp = None
|
||||
raise EOFError
|
||||
|
||||
self.mode = "L"
|
||||
if self.palette:
|
||||
self.mode = "P"
|
||||
|
||||
def tell(self):
|
||||
return self.__frame
|
||||
|
||||
def load_end(self):
|
||||
ImageFile.ImageFile.load_end(self)
|
||||
|
||||
# if the disposal method is 'do not dispose', transparent
|
||||
# pixels should show the content of the previous frame
|
||||
if self._prev_im and self.disposal_method == 1:
|
||||
# we do this by pasting the updated area onto the previous
|
||||
# frame which we then use as the current image content
|
||||
updated = self.im.crop(self.dispose_extent)
|
||||
self._prev_im.paste(updated, self.dispose_extent,
|
||||
updated.convert('RGBA'))
|
||||
self.im = self._prev_im
|
||||
self._prev_im = self.im.copy()
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Write GIF files
|
||||
|
||||
RAWMODE = {
|
||||
"1": "L",
|
||||
"L": "L",
|
||||
"P": "P"
|
||||
}
|
||||
|
||||
|
||||
def _normalize_mode(im, initial_call=False):
|
||||
"""
|
||||
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.
|
||||
|
||||
UNDONE: What is the point of mucking with the initial call palette, for
|
||||
an image that shouldn't have a palette, or it would be a mode 'P' and
|
||||
get returned in the RAWMODE clause.
|
||||
|
||||
:param im: Image object
|
||||
:param initial_call: Default false, set to true for a single frame.
|
||||
:returns: Image object
|
||||
"""
|
||||
if im.mode in RAWMODE:
|
||||
im.load()
|
||||
return im
|
||||
if Image.getmodebase(im.mode) == "RGB":
|
||||
if initial_call:
|
||||
palette_size = 256
|
||||
if im.palette:
|
||||
palette_size = len(im.palette.getdata()[1]) // 3
|
||||
return im.convert("P", palette=Image.ADAPTIVE, colors=palette_size)
|
||||
else:
|
||||
return im.convert("P")
|
||||
return im.convert("L")
|
||||
|
||||
def _normalize_palette(im, palette, info):
|
||||
"""
|
||||
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(itertools.chain.from_iterable(
|
||||
zip(palette.palette[:256],
|
||||
palette.palette[256:512],
|
||||
palette.palette[512:768])))
|
||||
|
||||
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 = _get_optimize(im, info)
|
||||
if used_palette_colors is not None:
|
||||
return im.remap_palette(used_palette_colors, source_palette)
|
||||
|
||||
im.palette.palette = source_palette
|
||||
return im
|
||||
|
||||
def _write_single_frame(im, fp, palette):
|
||||
im_out = _normalize_mode(im, True)
|
||||
im_out = _normalize_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 _write_multiple_frames(im, fp, palette):
|
||||
|
||||
duration = im.encoderinfo.get("duration", None)
|
||||
|
||||
im_frames = []
|
||||
frame_count = 0
|
||||
for imSequence in [im]+im.encoderinfo.get("append_images", []):
|
||||
for im_frame in ImageSequence.Iterator(imSequence):
|
||||
# a copy is required here since seek can still mutate the image
|
||||
im_frame = _normalize_mode(im_frame.copy())
|
||||
im_frame = _normalize_palette(im_frame, palette, im.encoderinfo)
|
||||
|
||||
encoderinfo = im.encoderinfo.copy()
|
||||
if isinstance(duration, (list, tuple)):
|
||||
encoderinfo['duration'] = duration[frame_count]
|
||||
frame_count += 1
|
||||
|
||||
if im_frames:
|
||||
# delta frame
|
||||
previous = im_frames[-1]
|
||||
if _get_palette_bytes(im_frame) == _get_palette_bytes(previous['im']):
|
||||
delta = ImageChops.subtract_modulo(im_frame,
|
||||
previous['im'])
|
||||
else:
|
||||
delta = ImageChops.subtract_modulo(im_frame.convert('RGB'),
|
||||
previous['im'].convert('RGB'))
|
||||
bbox = delta.getbbox()
|
||||
if not bbox:
|
||||
# This frame is identical to the previous frame
|
||||
if duration:
|
||||
previous['encoderinfo']['duration'] += encoderinfo['duration']
|
||||
continue
|
||||
else:
|
||||
bbox = None
|
||||
im_frames.append({
|
||||
'im':im_frame,
|
||||
'bbox':bbox,
|
||||
'encoderinfo':encoderinfo
|
||||
})
|
||||
|
||||
if len(im_frames) > 1:
|
||||
for frame_data in im_frames:
|
||||
im_frame = frame_data['im']
|
||||
if not frame_data['bbox']:
|
||||
# global header
|
||||
for s in _get_global_header(im_frame,
|
||||
frame_data['encoderinfo']):
|
||||
fp.write(s)
|
||||
offset = (0, 0)
|
||||
else:
|
||||
# compress difference
|
||||
frame_data['encoderinfo']['include_color_table'] = True
|
||||
|
||||
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, fp, filename):
|
||||
_save(im, fp, filename, save_all=True)
|
||||
|
||||
|
||||
def _save(im, fp, filename, save_all=False):
|
||||
im.encoderinfo.update(im.info)
|
||||
# header
|
||||
try:
|
||||
palette = im.encoderinfo["palette"]
|
||||
except KeyError:
|
||||
palette = None
|
||||
im.encoderinfo["optimize"] = im.encoderinfo.get("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):
|
||||
interlace = im.encoderinfo.get("interlace", 1)
|
||||
|
||||
# workaround for @PIL153
|
||||
if min(im.size) < 16:
|
||||
interlace = 0
|
||||
|
||||
return interlace
|
||||
|
||||
|
||||
def _write_local_header(fp, im, offset, flags):
|
||||
transparent_color_exists = False
|
||||
try:
|
||||
transparency = im.encoderinfo["transparency"]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
transparency = int(transparency)
|
||||
# optimize the block away if transparent color is not used
|
||||
transparent_color_exists = True
|
||||
|
||||
used_palette_colors = _get_optimize(im, im.encoderinfo)
|
||||
if used_palette_colors is not None:
|
||||
# adjust the transparency index after optimize
|
||||
try:
|
||||
transparency = used_palette_colors.index(transparency)
|
||||
except ValueError:
|
||||
transparent_color_exists = False
|
||||
|
||||
if "duration" in im.encoderinfo:
|
||||
duration = int(im.encoderinfo["duration"] / 10)
|
||||
else:
|
||||
duration = 0
|
||||
if transparent_color_exists or duration != 0:
|
||||
transparency_flag = 1 if transparent_color_exists else 0
|
||||
if not transparent_color_exists:
|
||||
transparency = 0
|
||||
|
||||
fp.write(b"!" +
|
||||
o8(249) + # extension intro
|
||||
o8(4) + # length
|
||||
o8(transparency_flag) + # packed fields
|
||||
o16(duration) + # duration
|
||||
o8(transparency) + # transparency index
|
||||
o8(0))
|
||||
|
||||
if "comment" in im.encoderinfo and 1 <= len(im.encoderinfo["comment"]) <= 255:
|
||||
fp.write(b"!" +
|
||||
o8(254) + # extension intro
|
||||
o8(len(im.encoderinfo["comment"])) +
|
||||
im.encoderinfo["comment"] +
|
||||
o8(0))
|
||||
if "loop" in im.encoderinfo:
|
||||
number_of_loops = im.encoderinfo["loop"]
|
||||
fp.write(b"!" +
|
||||
o8(255) + # extension intro
|
||||
o8(11) +
|
||||
b"NETSCAPE2.0" +
|
||||
o8(3) +
|
||||
o8(1) +
|
||||
o16(number_of_loops) + # number of loops
|
||||
o8(0))
|
||||
include_color_table = im.encoderinfo.get('include_color_table')
|
||||
if include_color_table:
|
||||
palette = im.encoderinfo.get("palette", None)
|
||||
palette_bytes = _get_palette_bytes(im)
|
||||
color_table_size = _get_color_table_size(palette_bytes)
|
||||
if color_table_size:
|
||||
flags = flags | 128 # local color table flag
|
||||
flags = flags | 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, fp, filename):
|
||||
|
||||
# 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.
|
||||
|
||||
import os
|
||||
from subprocess import Popen, check_call, PIPE, CalledProcessError
|
||||
file = im._dump()
|
||||
|
||||
with open(filename, 'wb') as f:
|
||||
if im.mode != "RGB":
|
||||
with open(os.devnull, 'wb') as devnull:
|
||||
check_call(["ppmtogif", file], stdout=f, stderr=devnull)
|
||||
else:
|
||||
# Pipe ppmquant output into ppmtogif
|
||||
# "ppmquant 256 %s | ppmtogif > %s" % (file, filename)
|
||||
quant_cmd = ["ppmquant", "256", file]
|
||||
togif_cmd = ["ppmtogif"]
|
||||
with open(os.devnull, 'wb') as devnull:
|
||||
quant_proc = Popen(quant_cmd, stdout=PIPE, stderr=devnull)
|
||||
togif_proc = Popen(togif_cmd, stdin=quant_proc.stdout,
|
||||
stdout=f, stderr=devnull)
|
||||
|
||||
# Allow ppmquant to receive SIGPIPE if ppmtogif exits
|
||||
quant_proc.stdout.close()
|
||||
|
||||
retcode = quant_proc.wait()
|
||||
if retcode:
|
||||
raise CalledProcessError(retcode, quant_cmd)
|
||||
|
||||
retcode = togif_proc.wait()
|
||||
if retcode:
|
||||
raise CalledProcessError(retcode, togif_cmd)
|
||||
|
||||
try:
|
||||
os.unlink(file)
|
||||
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, info):
|
||||
"""
|
||||
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", 0):
|
||||
# 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 (len(used_palette_colors) <= 128 and
|
||||
max(used_palette_colors) > len(used_palette_colors)):
|
||||
return used_palette_colors
|
||||
|
||||
def _get_color_table_size(palette_bytes):
|
||||
# calculate the palette size for the header
|
||||
import math
|
||||
color_table_size = int(math.ceil(math.log(len(palette_bytes)//3, 2)))-1
|
||||
if color_table_size < 0:
|
||||
color_table_size = 0
|
||||
return color_table_size
|
||||
|
||||
def _get_header_palette(palette_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<<n in size
|
||||
actual_target_size_diff = (2 << color_table_size) - len(palette_bytes)//3
|
||||
if actual_target_size_diff > 0:
|
||||
palette_bytes += o8(0) * 3 * actual_target_size_diff
|
||||
return palette_bytes
|
||||
|
||||
def _get_palette_bytes(im):
|
||||
"""
|
||||
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
|
||||
|
||||
def _get_global_header(im, info):
|
||||
"""Return a list of strings representing a GIF header"""
|
||||
|
||||
# Header Block
|
||||
# http://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp
|
||||
|
||||
version = b"87a"
|
||||
for extensionKey in ["transparency", "duration", "loop", "comment"]:
|
||||
if info and extensionKey in info:
|
||||
if ((extensionKey == "duration" and info[extensionKey] == 0) or
|
||||
(extensionKey == "comment" and not (1 <= len(info[extensionKey]) <= 255))):
|
||||
continue
|
||||
version = b"89a"
|
||||
break
|
||||
else:
|
||||
if im.info.get("version") == b"89a":
|
||||
version = b"89a"
|
||||
|
||||
palette_bytes = _get_palette_bytes(im)
|
||||
color_table_size = _get_color_table_size(palette_bytes)
|
||||
|
||||
background = info["background"] if "background" in info else 0
|
||||
|
||||
return [
|
||||
b"GIF"+version + # signature + version
|
||||
o16(im.size[0]) + # canvas width
|
||||
o16(im.size[1]), # canvas height
|
||||
|
||||
# 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)
|
||||
]
|
||||
|
||||
def _write_frame_data(fp, im_frame, offset, params):
|
||||
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, palette=None, info=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)
|
||||
|
||||
"""
|
||||
used_palette_colors = _get_optimize(im, info)
|
||||
|
||||
if info is None:
|
||||
info = {}
|
||||
|
||||
if not "background" 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
|
||||
|
||||
# To specify duration, add the time in milliseconds to getdata(),
|
||||
# e.g. getdata(im_frame, duration=1000)
|
||||
def getdata(im, offset=(0, 0), **params):
|
||||
"""
|
||||
Legacy Method
|
||||
|
||||
Return a list of strings representing this image.
|
||||
The first string is a local image header, the rest contains
|
||||
encoded image data.
|
||||
|
||||
: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
|
||||
|
||||
"""
|
||||
class Collector(object):
|
||||
data = []
|
||||
|
||||
def write(self, data):
|
||||
self.data.append(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)
|
||||
@ -13,28 +13,19 @@
|
||||
# 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 math import pi, log, sin, sqrt
|
||||
from ._binary import o8
|
||||
|
||||
TYPE_CHECKING = False
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
from typing import IO
|
||||
# --------------------------------------------------------------------
|
||||
# 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.)
|
||||
#
|
||||
|
||||
EPSILON = 1e-10
|
||||
"""""" # Enable auto-doc for data member
|
||||
|
||||
|
||||
def linear(middle: float, pos: float) -> float:
|
||||
def linear(middle, pos):
|
||||
if pos <= middle:
|
||||
if middle < EPSILON:
|
||||
return 0.0
|
||||
@ -49,50 +40,38 @@ def linear(middle: float, pos: float) -> float:
|
||||
return 0.5 + 0.5 * pos / middle
|
||||
|
||||
|
||||
def curved(middle: float, pos: float) -> float:
|
||||
def curved(middle, pos):
|
||||
return pos ** (log(0.5) / log(max(middle, EPSILON)))
|
||||
|
||||
|
||||
def sine(middle: float, pos: float) -> float:
|
||||
def sine(middle, pos):
|
||||
return (sin((-pi / 2.0) + pi * linear(middle, pos)) + 1.0) / 2.0
|
||||
|
||||
|
||||
def sphere_increasing(middle: float, pos: float) -> float:
|
||||
def sphere_increasing(middle, pos):
|
||||
return sqrt(1.0 - (linear(middle, pos) - 1.0) ** 2)
|
||||
|
||||
|
||||
def sphere_decreasing(middle: float, pos: float) -> float:
|
||||
def sphere_decreasing(middle, pos):
|
||||
return 1.0 - sqrt(1.0 - linear(middle, pos) ** 2)
|
||||
|
||||
|
||||
SEGMENTS = [linear, curved, sine, sphere_increasing, sphere_decreasing]
|
||||
"""""" # Enable auto-doc for data member
|
||||
|
||||
|
||||
class GradientFile:
|
||||
gradient: (
|
||||
list[
|
||||
tuple[
|
||||
float,
|
||||
float,
|
||||
float,
|
||||
list[float],
|
||||
list[float],
|
||||
Callable[[float, float], float],
|
||||
]
|
||||
]
|
||||
| None
|
||||
) = None
|
||||
class GradientFile(object):
|
||||
|
||||
gradient = None
|
||||
|
||||
def getpalette(self, entries=256):
|
||||
|
||||
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)
|
||||
|
||||
x = i / float(entries-1)
|
||||
|
||||
while x1 < x:
|
||||
ix += 1
|
||||
@ -117,13 +96,15 @@ class GradientFile:
|
||||
return b"".join(palette), "RGBA"
|
||||
|
||||
|
||||
class GimpGradientFile(GradientFile):
|
||||
"""File handler for GIMP's gradient format."""
|
||||
##
|
||||
# File handler for GIMP's gradient format.
|
||||
|
||||
def __init__(self, fp: IO[bytes]) -> None:
|
||||
if not fp.readline().startswith(b"GIMP Gradient"):
|
||||
msg = "not a GIMP gradient file"
|
||||
raise SyntaxError(msg)
|
||||
class GimpGradientFile(GradientFile):
|
||||
|
||||
def __init__(self, fp):
|
||||
|
||||
if fp.readline()[:13] != b"GIMP Gradient":
|
||||
raise SyntaxError("not a GIMP gradient file")
|
||||
|
||||
line = fp.readline()
|
||||
|
||||
@ -133,9 +114,10 @@ class GimpGradientFile(GradientFile):
|
||||
|
||||
count = int(line)
|
||||
|
||||
self.gradient = []
|
||||
gradient = []
|
||||
|
||||
for i in range(count):
|
||||
|
||||
s = fp.readline().split()
|
||||
w = [float(x) for x in s[:11]]
|
||||
|
||||
@ -148,7 +130,8 @@ class GimpGradientFile(GradientFile):
|
||||
cspace = int(s[12])
|
||||
|
||||
if cspace != 0:
|
||||
msg = "cannot handle HSV colour space"
|
||||
raise OSError(msg)
|
||||
raise IOError("cannot handle HSV colour space")
|
||||
|
||||
self.gradient.append((x0, x1, xm, rgb0, rgb1, segment))
|
||||
gradient.append((x0, x1, xm, rgb0, rgb1, segment))
|
||||
|
||||
self.gradient = gradient
|
||||
62
PIL/GimpPaletteFile.py
Normal file
62
PIL/GimpPaletteFile.py
Normal file
@ -0,0 +1,62 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
import re
|
||||
from ._binary import o8
|
||||
|
||||
|
||||
##
|
||||
# File handler for GIMP's palette format.
|
||||
|
||||
class GimpPaletteFile(object):
|
||||
|
||||
rawmode = "RGB"
|
||||
|
||||
def __init__(self, fp):
|
||||
|
||||
self.palette = [o8(i)*3 for i in range(256)]
|
||||
|
||||
if fp.readline()[:12] != b"GIMP Palette":
|
||||
raise SyntaxError("not a GIMP palette file")
|
||||
|
||||
i = 0
|
||||
|
||||
while i <= 255:
|
||||
|
||||
s = fp.readline()
|
||||
|
||||
if not s:
|
||||
break
|
||||
# skip fields and comment lines
|
||||
if re.match(br"\w+:|#", s):
|
||||
continue
|
||||
if len(s) > 100:
|
||||
raise SyntaxError("bad palette file")
|
||||
|
||||
v = tuple(map(int, s.split()[:3]))
|
||||
if len(v) != 3:
|
||||
raise ValueError("bad palette entry")
|
||||
|
||||
if 0 <= i <= 255:
|
||||
self.palette[i] = o8(v[0]) + o8(v[1]) + o8(v[2])
|
||||
|
||||
i += 1
|
||||
|
||||
self.palette = b"".join(self.palette)
|
||||
|
||||
def getpalette(self):
|
||||
|
||||
return self.palette, self.rawmode
|
||||
@ -8,17 +8,14 @@
|
||||
#
|
||||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import IO
|
||||
|
||||
from . import Image, ImageFile
|
||||
from ._binary import i8
|
||||
|
||||
_handler = None
|
||||
|
||||
|
||||
def register_handler(handler: ImageFile.StubHandler | None) -> None:
|
||||
def register_handler(handler):
|
||||
"""
|
||||
Install application-specific GRIB image handler.
|
||||
|
||||
@ -31,35 +28,39 @@ def register_handler(handler: ImageFile.StubHandler | None) -> None:
|
||||
# --------------------------------------------------------------------
|
||||
# Image adapter
|
||||
|
||||
|
||||
def _accept(prefix: bytes) -> bool:
|
||||
return len(prefix) >= 8 and prefix.startswith(b"GRIB") and prefix[7] == 1
|
||||
def _accept(prefix):
|
||||
return prefix[0:4] == b"GRIB" and i8(prefix[7]) == 1
|
||||
|
||||
|
||||
class GribStubImageFile(ImageFile.StubImageFile):
|
||||
|
||||
format = "GRIB"
|
||||
format_description = "GRIB"
|
||||
|
||||
def _open(self) -> None:
|
||||
assert self.fp is not None
|
||||
if not _accept(self.fp.read(8)):
|
||||
msg = "Not a GRIB file"
|
||||
raise SyntaxError(msg)
|
||||
def _open(self):
|
||||
|
||||
self.fp.seek(-8, os.SEEK_CUR)
|
||||
offset = self.fp.tell()
|
||||
|
||||
if not _accept(self.fp.read(8)):
|
||||
raise SyntaxError("Not a GRIB file")
|
||||
|
||||
self.fp.seek(offset)
|
||||
|
||||
# make something up
|
||||
self._mode = "F"
|
||||
self._size = 1, 1
|
||||
self.mode = "F"
|
||||
self.size = 1, 1
|
||||
|
||||
def _load(self) -> ImageFile.StubHandler | None:
|
||||
loader = self._load()
|
||||
if loader:
|
||||
loader.open(self)
|
||||
|
||||
def _load(self):
|
||||
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)
|
||||
def _save(im, fp, filename):
|
||||
if _handler is None or not hasattr("_handler", "save"):
|
||||
raise IOError("GRIB save handler not installed")
|
||||
_handler.save(im, fp, filename)
|
||||
|
||||
|
||||
@ -8,17 +8,13 @@
|
||||
#
|
||||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import IO
|
||||
|
||||
from . import Image, ImageFile
|
||||
|
||||
_handler = None
|
||||
|
||||
|
||||
def register_handler(handler: ImageFile.StubHandler | None) -> None:
|
||||
def register_handler(handler):
|
||||
"""
|
||||
Install application-specific HDF5 image handler.
|
||||
|
||||
@ -31,35 +27,39 @@ def register_handler(handler: ImageFile.StubHandler | None) -> None:
|
||||
# --------------------------------------------------------------------
|
||||
# Image adapter
|
||||
|
||||
|
||||
def _accept(prefix: bytes) -> bool:
|
||||
return prefix.startswith(b"\x89HDF\r\n\x1a\n")
|
||||
def _accept(prefix):
|
||||
return prefix[:8] == b"\x89HDF\r\n\x1a\n"
|
||||
|
||||
|
||||
class HDF5StubImageFile(ImageFile.StubImageFile):
|
||||
|
||||
format = "HDF5"
|
||||
format_description = "HDF5"
|
||||
|
||||
def _open(self) -> None:
|
||||
assert self.fp is not None
|
||||
if not _accept(self.fp.read(8)):
|
||||
msg = "Not an HDF file"
|
||||
raise SyntaxError(msg)
|
||||
def _open(self):
|
||||
|
||||
self.fp.seek(-8, os.SEEK_CUR)
|
||||
offset = self.fp.tell()
|
||||
|
||||
if not _accept(self.fp.read(8)):
|
||||
raise SyntaxError("Not an HDF file")
|
||||
|
||||
self.fp.seek(offset)
|
||||
|
||||
# make something up
|
||||
self._mode = "F"
|
||||
self._size = 1, 1
|
||||
self.mode = "F"
|
||||
self.size = 1, 1
|
||||
|
||||
def _load(self) -> ImageFile.StubHandler | None:
|
||||
loader = self._load()
|
||||
if loader:
|
||||
loader.open(self)
|
||||
|
||||
def _load(self):
|
||||
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)
|
||||
def _save(im, fp, filename):
|
||||
if _handler is None or not hasattr("_handler", "save"):
|
||||
raise IOError("HDF5 save handler not installed")
|
||||
_handler.save(im, fp, filename)
|
||||
|
||||
|
||||
@ -69,4 +69,5 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
||||
Image.register_open(HDF5StubImageFile.format, HDF5StubImageFile, _accept)
|
||||
Image.register_save(HDF5StubImageFile.format, _save)
|
||||
|
||||
Image.register_extensions(HDF5StubImageFile.format, [".h5", ".hdf"])
|
||||
Image.register_extension(HDF5StubImageFile.format, ".h5")
|
||||
Image.register_extension(HDF5StubImageFile.format, ".hdf")
|
||||
365
PIL/IcnsImagePlugin.py
Normal file
365
PIL/IcnsImagePlugin.py
Normal file
@ -0,0 +1,365 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# Copyright (c) 2004 by Bob Ippolito.
|
||||
# Copyright (c) 2004 by Secret Labs.
|
||||
# Copyright (c) 2004 by Fredrik Lundh.
|
||||
# Copyright (c) 2014 by Alastair Houghton.
|
||||
#
|
||||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
from PIL import Image, ImageFile, PngImagePlugin
|
||||
from PIL._binary import i8
|
||||
import io
|
||||
import os
|
||||
import shutil
|
||||
import struct
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
enable_jpeg2k = hasattr(Image.core, 'jp2klib_version')
|
||||
if enable_jpeg2k:
|
||||
from PIL import Jpeg2KImagePlugin
|
||||
|
||||
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':
|
||||
raise SyntaxError('Unknown signature, expecting 0x00000000')
|
||||
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 = i8(byte)
|
||||
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:
|
||||
raise SyntaxError(
|
||||
"Error reading channel [%r left]" % bytesleft
|
||||
)
|
||||
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)
|
||||
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:
|
||||
raise ValueError('Unsupported icon subimage format (rebuild PIL '
|
||||
'with JPEG 2000 support to fix this)')
|
||||
# j2k, jpc or j2c
|
||||
fobj.seek(start)
|
||||
jp2kstream = fobj.read(length)
|
||||
f = io.BytesIO(jp2kstream)
|
||||
im = Jpeg2KImagePlugin.Jpeg2KImageFile(f)
|
||||
if im.mode != 'RGBA':
|
||||
im = im.convert('RGBA')
|
||||
return {"RGBA": im}
|
||||
else:
|
||||
raise ValueError('Unsupported icon subimage format')
|
||||
|
||||
|
||||
class IcnsFile(object):
|
||||
|
||||
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 sig != b'icns':
|
||||
raise SyntaxError('not an icns file')
|
||||
i = HEADERSIZE
|
||||
while i < filesize:
|
||||
sig, blocksize = nextheader(fobj)
|
||||
if blocksize <= 0:
|
||||
raise SyntaxError('invalid block header')
|
||||
i += HEADERSIZE
|
||||
blocksize -= HEADERSIZE
|
||||
dct[sig] = (i, blocksize)
|
||||
fobj.seek(blocksize, 1)
|
||||
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:
|
||||
raise SyntaxError("No 32bit icon resources found")
|
||||
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):
|
||||
self.icns = IcnsFile(self.fp)
|
||||
self.mode = 'RGBA'
|
||||
self.best_size = self.icns.bestsize()
|
||||
self.size = (self.best_size[0] * self.best_size[2],
|
||||
self.best_size[1] * self.best_size[2])
|
||||
self.info['sizes'] = self.icns.itersizes()
|
||||
# Just use this to see if it's loaded or not yet.
|
||||
self.tile = ('',)
|
||||
|
||||
def load(self):
|
||||
if len(self.size) == 3:
|
||||
self.best_size = self.size
|
||||
self.size = (self.best_size[0] * self.best_size[2],
|
||||
self.best_size[1] * self.best_size[2])
|
||||
|
||||
Image.Image.load(self)
|
||||
if not self.tile:
|
||||
return
|
||||
self.load_prepare()
|
||||
# This is likely NOT the best way to do it, but whatever.
|
||||
im = self.icns.getimage(self.best_size)
|
||||
|
||||
# If this is a PNG or JPEG 2000, it won't be loaded yet
|
||||
im.load()
|
||||
|
||||
self.im = im.im
|
||||
self.mode = im.mode
|
||||
self.size = im.size
|
||||
self.fp = None
|
||||
self.icns = None
|
||||
self.tile = ()
|
||||
self.load_end()
|
||||
|
||||
|
||||
def _save(im, fp, filename):
|
||||
"""
|
||||
Saves the image as a series of PNG files,
|
||||
that are then converted to a .icns file
|
||||
using the macOS command line utility 'iconutil'.
|
||||
|
||||
macOS only.
|
||||
"""
|
||||
if hasattr(fp, "flush"):
|
||||
fp.flush()
|
||||
|
||||
# create the temporary set of pngs
|
||||
iconset = tempfile.mkdtemp('.iconset')
|
||||
last_w = None
|
||||
last_im = None
|
||||
for w in [16, 32, 128, 256, 512]:
|
||||
prefix = 'icon_{}x{}'.format(w, w)
|
||||
|
||||
if last_w == w:
|
||||
im_scaled = last_im
|
||||
else:
|
||||
im_scaled = im.resize((w, w), Image.LANCZOS)
|
||||
im_scaled.save(os.path.join(iconset, prefix+'.png'))
|
||||
|
||||
im_scaled = im.resize((w*2, w*2), Image.LANCZOS)
|
||||
im_scaled.save(os.path.join(iconset, prefix+'@2x.png'))
|
||||
last_im = im_scaled
|
||||
|
||||
# iconutil -c icns -o {} {}
|
||||
from subprocess import Popen, PIPE, CalledProcessError
|
||||
|
||||
convert_cmd = ["iconutil", "-c", "icns", "-o", filename, iconset]
|
||||
with open(os.devnull, 'wb') as devnull:
|
||||
convert_proc = Popen(convert_cmd, stdout=PIPE, stderr=devnull)
|
||||
|
||||
convert_proc.stdout.close()
|
||||
|
||||
retcode = convert_proc.wait()
|
||||
|
||||
# remove the temporary files
|
||||
shutil.rmtree(iconset)
|
||||
|
||||
if retcode:
|
||||
raise CalledProcessError(retcode, convert_cmd)
|
||||
|
||||
Image.register_open(IcnsImageFile.format, IcnsImageFile,
|
||||
lambda x: x[:4] == b'icns')
|
||||
Image.register_extension(IcnsImageFile.format, '.icns')
|
||||
|
||||
if sys.platform == 'darwin':
|
||||
Image.register_save(IcnsImageFile.format, _save)
|
||||
|
||||
Image.register_mime(IcnsImageFile.format, "image/icns")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
imf = IcnsImageFile(open(sys.argv[1], 'rb'))
|
||||
for size in imf.info['sizes']:
|
||||
imf.size = size
|
||||
imf.load()
|
||||
im = imf.im
|
||||
im.save('out-%s-%s-%s.png' % size)
|
||||
im = Image.open(open(sys.argv[1], "rb"))
|
||||
im.save("out.png")
|
||||
if sys.platform == 'windows':
|
||||
os.startfile("out.png")
|
||||
284
PIL/IcoImagePlugin.py
Normal file
284
PIL/IcoImagePlugin.py
Normal file
@ -0,0 +1,284 @@
|
||||
#
|
||||
# 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
|
||||
# <casadebender@gmail.com>.
|
||||
# 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
|
||||
|
||||
|
||||
import struct
|
||||
from io import BytesIO
|
||||
|
||||
from . import Image, ImageFile, BmpImagePlugin, PngImagePlugin
|
||||
from ._binary import i8, i16le as i16, i32le as i32
|
||||
from math import log, ceil
|
||||
|
||||
__version__ = "0.1"
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
_MAGIC = b"\0\0\1\0"
|
||||
|
||||
|
||||
def _save(im, fp, filename):
|
||||
fp.write(_MAGIC) # (2+2)
|
||||
sizes = im.encoderinfo.get("sizes",
|
||||
[(16, 16), (24, 24), (32, 32), (48, 48),
|
||||
(64, 64), (128, 128), (256, 256)])
|
||||
width, height = im.size
|
||||
sizes = filter(lambda x: False if (x[0] > width or x[1] > height or
|
||||
x[0] > 256 or x[1] > 256) else True,
|
||||
sizes)
|
||||
sizes = list(sizes)
|
||||
fp.write(struct.pack("<H", len(sizes))) # idCount(2)
|
||||
offset = fp.tell() + len(sizes)*16
|
||||
for size in sizes:
|
||||
width, height = size
|
||||
# 0 means 256
|
||||
fp.write(struct.pack("B", width if width < 256 else 0)) # bWidth(1)
|
||||
fp.write(struct.pack("B", height if height < 256 else 0)) # bHeight(1)
|
||||
fp.write(b"\0") # bColorCount(1)
|
||||
fp.write(b"\0") # bReserved(1)
|
||||
fp.write(b"\0\0") # wPlanes(2)
|
||||
fp.write(struct.pack("<H", 32)) # wBitCount(2)
|
||||
|
||||
image_io = BytesIO()
|
||||
tmp = im.copy()
|
||||
tmp.thumbnail(size, Image.LANCZOS)
|
||||
tmp.save(image_io, "png")
|
||||
image_io.seek(0)
|
||||
image_bytes = image_io.read()
|
||||
bytes_len = len(image_bytes)
|
||||
fp.write(struct.pack("<I", bytes_len)) # dwBytesInRes(4)
|
||||
fp.write(struct.pack("<I", offset)) # dwImageOffset(4)
|
||||
current = fp.tell()
|
||||
fp.seek(offset)
|
||||
fp.write(image_bytes)
|
||||
offset = offset + bytes_len
|
||||
fp.seek(current)
|
||||
|
||||
|
||||
def _accept(prefix):
|
||||
return prefix[:4] == _MAGIC
|
||||
|
||||
|
||||
class IcoFile(object):
|
||||
def __init__(self, buf):
|
||||
"""
|
||||
Parse image from file-like object containing ico file data
|
||||
"""
|
||||
|
||||
# check magic
|
||||
s = buf.read(6)
|
||||
if not _accept(s):
|
||||
raise SyntaxError("not an ICO file")
|
||||
|
||||
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': i8(s[0]),
|
||||
'height': i8(s[1]),
|
||||
'nb_color': i8(s[2]), # No. of colors in image (0 if >=8bpp)
|
||||
'reserved': i8(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['width'])
|
||||
self.entry = sorted(self.entry, key=lambda x: x['square'])
|
||||
self.entry.reverse()
|
||||
|
||||
def sizes(self):
|
||||
"""
|
||||
Get a list of all available icon sizes and color depths.
|
||||
"""
|
||||
return {(h['width'], h['height']) for h in self.entry}
|
||||
|
||||
def getimage(self, size, bpp=False):
|
||||
"""
|
||||
Get an image from the icon
|
||||
"""
|
||||
for (i, h) in enumerate(self.entry):
|
||||
if size == h['dim'] and (bpp is False or bpp == h['color_depth']):
|
||||
return self.frame(i)
|
||||
return self.frame(0)
|
||||
|
||||
def frame(self, idx):
|
||||
"""
|
||||
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'])
|
||||
|
||||
if data[:8] == PngImagePlugin._MAGIC:
|
||||
# png frame
|
||||
im = PngImagePlugin.PngImageFile(self.buf)
|
||||
else:
|
||||
# XOR + AND mask bmp frame
|
||||
im = BmpImagePlugin.DibImageFile(self.buf)
|
||||
|
||||
# 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
|
||||
mode = a[0]
|
||||
bpp = 8
|
||||
for k in BmpImagePlugin.BIT2MODE.keys():
|
||||
if mode == BmpImagePlugin.BIT2MODE[k][1]:
|
||||
bpp = k
|
||||
break
|
||||
|
||||
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
|
||||
|
||||
and_mask_offset = o + int(im.size[0] * im.size[1] *
|
||||
(bpp / 8.0))
|
||||
total_bytes = int((w * im.size[1]) / 8)
|
||||
|
||||
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.
|
||||
|
||||
This plugin is a refactored version of Win32IconImagePlugin by Bryan Davis
|
||||
<casadebender@gmail.com>.
|
||||
https://code.google.com/archive/p/casadebender/wikis/Win32IconImagePlugin.wiki
|
||||
"""
|
||||
format = "ICO"
|
||||
format_description = "Windows Icon"
|
||||
|
||||
def _open(self):
|
||||
self.ico = IcoFile(self.fp)
|
||||
self.info['sizes'] = self.ico.sizes()
|
||||
self.size = self.ico.entry[0]['dim']
|
||||
self.load()
|
||||
|
||||
def load(self):
|
||||
im = self.ico.getimage(self.size)
|
||||
# if tile is PNG, it won't really be loaded yet
|
||||
im.load()
|
||||
self.im = im.im
|
||||
self.mode = im.mode
|
||||
self.size = im.size
|
||||
|
||||
def load_seek(self):
|
||||
# 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")
|
||||
@ -24,14 +24,14 @@
|
||||
#
|
||||
# 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
|
||||
from ._util import DeferredError
|
||||
from ._binary import i8
|
||||
|
||||
__version__ = "0.7"
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Standard tags
|
||||
@ -46,17 +46,8 @@ 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,
|
||||
}
|
||||
TAGS = {COMMENT: 0, DATE: 0, EQUIPMENT: 0, FRAMES: 0, LUT: 0, NAME: 0,
|
||||
SCALE: 0, SIZE: 0, MODE: 0}
|
||||
|
||||
OPEN = {
|
||||
# ifunc93/p3cfunc formats
|
||||
@ -78,34 +69,33 @@ OPEN = {
|
||||
"RYB3 image": ("RGB", "RYB;T"),
|
||||
# extensions
|
||||
"LA image": ("LA", "LA;L"),
|
||||
"PA image": ("LA", "PA;L"),
|
||||
"RGBA image": ("RGBA", "RGBA;L"),
|
||||
"RGBX image": ("RGB", "RGBX;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}")
|
||||
OPEN["L %s image" % i] = ("F", "F;%s" % i)
|
||||
OPEN["L*%s image" % i] = ("F", "F;%s" % 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}")
|
||||
OPEN["L %s image" % i] = ("I;%s" % i, "I;%s" % i)
|
||||
OPEN["L*%s image" % i] = ("I;%s" % i, "I;%s" % 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}")
|
||||
OPEN["L %s image" % i] = ("I", "I;%s" % i)
|
||||
OPEN["L*%s image" % i] = ("I", "I;%s" % i)
|
||||
for i in range(2, 33):
|
||||
OPEN["L*%s image" % i] = ("F", "F;%s" % i)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Read IM directory
|
||||
|
||||
split = re.compile(rb"^([A-Za-z][^:]*):[ \t]*(.*)[ \t]*$")
|
||||
split = re.compile(br"^([A-Za-z][^:]*):[ \t]*(.*)[ \t]*$")
|
||||
|
||||
|
||||
def number(s: Any) -> float:
|
||||
def number(s):
|
||||
try:
|
||||
return int(s)
|
||||
except ValueError:
|
||||
@ -115,20 +105,19 @@ def number(s: Any) -> float:
|
||||
##
|
||||
# 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:
|
||||
def _open(self):
|
||||
|
||||
# Quick rejection: if there's not an LF among the first
|
||||
# 100 bytes, this is (probably) not a text header.
|
||||
|
||||
assert self.fp is not None
|
||||
if b"\n" not in self.fp.read(100):
|
||||
msg = "not an IM file"
|
||||
raise SyntaxError(msg)
|
||||
raise SyntaxError("not an IM file")
|
||||
self.fp.seek(0)
|
||||
|
||||
n = 0
|
||||
@ -141,40 +130,40 @@ class ImImageFile(ImageFile.ImageFile):
|
||||
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":
|
||||
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)
|
||||
raise SyntaxError("not an IM file")
|
||||
|
||||
if s.endswith(b"\r\n"):
|
||||
if s[-2:] == b'\r\n':
|
||||
s = s[:-2]
|
||||
elif s.endswith(b"\n"):
|
||||
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
|
||||
except re.error as v:
|
||||
raise SyntaxError("not an IM file")
|
||||
|
||||
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")
|
||||
k = k.decode('latin-1', 'replace')
|
||||
v = v.decode('latin-1', 'replace')
|
||||
|
||||
# Convert value as appropriate
|
||||
if k in [FRAMES, SCALE, SIZE]:
|
||||
@ -199,23 +188,22 @@ class ImImageFile(ImageFile.ImageFile):
|
||||
n += 1
|
||||
|
||||
else:
|
||||
msg = f"Syntax error in IM header: {s.decode('ascii', 'replace')}"
|
||||
raise SyntaxError(msg)
|
||||
|
||||
raise SyntaxError("Syntax error in IM header: " +
|
||||
s.decode('ascii', 'replace'))
|
||||
|
||||
if not n:
|
||||
msg = "Not an IM file"
|
||||
raise SyntaxError(msg)
|
||||
raise SyntaxError("Not an IM file")
|
||||
|
||||
# Basic attributes
|
||||
self._size = self.info[SIZE]
|
||||
self._mode = self.info[MODE]
|
||||
self.size = self.info[SIZE]
|
||||
self.mode = self.info[MODE]
|
||||
|
||||
# Skip forward to start of image data
|
||||
while s and not s.startswith(b"\x1a"):
|
||||
while s and s[0:1] != b'\x1A':
|
||||
s = self.fp.read(1)
|
||||
if not s:
|
||||
msg = "File truncated"
|
||||
raise SyntaxError(msg)
|
||||
raise SyntaxError("File truncated")
|
||||
|
||||
if LUT in self.info:
|
||||
# convert lookup table to palette or lut attribute
|
||||
@ -223,43 +211,40 @@ class ImImageFile(ImageFile.ImageFile):
|
||||
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:
|
||||
if palette[i] == palette[i+256] == palette[i+512]:
|
||||
if i8(palette[i]) != i:
|
||||
linear = 0
|
||||
else:
|
||||
greyscale = 0
|
||||
if self.mode in ["L", "LA", "P", "PA"]:
|
||||
if self.mode == "L" or self.mode == "LA":
|
||||
if greyscale:
|
||||
if not linear:
|
||||
self.lut = list(palette[:256])
|
||||
self.lut = [i8(c) for c in 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"
|
||||
if self.mode == "L":
|
||||
self.mode = self.rawmode = "P"
|
||||
elif self.mode == "LA":
|
||||
self.mode = self.rawmode = "PA"
|
||||
self.palette = ImagePalette.raw("RGB;L", palette)
|
||||
elif self.mode == "RGB":
|
||||
if not greyscale or not linear:
|
||||
self.lut = list(palette)
|
||||
self.lut = [i8(c) for c in palette]
|
||||
|
||||
self.frame = 0
|
||||
|
||||
self.__offset = offs = self.fp.tell()
|
||||
|
||||
self._fp = self.fp # FIXME: hack
|
||||
self.__fp = self.fp # FIXME: hack
|
||||
|
||||
if self.rawmode[:2] == "F;":
|
||||
|
||||
if self.rawmode.startswith("F;"):
|
||||
# ifunc95 formats
|
||||
try:
|
||||
# use bit decoder (if necessary)
|
||||
bits = int(self.rawmode[2:])
|
||||
if bits not in [8, 16, 32]:
|
||||
self.tile = [
|
||||
ImageFile._Tile(
|
||||
"bit", (0, 0) + self.size, offs, (bits, 8, 3, 0, -1)
|
||||
)
|
||||
]
|
||||
self.tile = [("bit", (0, 0)+self.size, offs,
|
||||
(bits, 8, 3, 0, -1))]
|
||||
return
|
||||
except ValueError:
|
||||
pass
|
||||
@ -268,32 +253,29 @@ class ImImageFile(ImageFile.ImageFile):
|
||||
# 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 = [
|
||||
ImageFile._Tile("raw", (0, 0) + self.size, offs, ("G", 0, -1)),
|
||||
ImageFile._Tile("raw", (0, 0) + self.size, offs + size, ("R", 0, -1)),
|
||||
ImageFile._Tile(
|
||||
"raw", (0, 0) + self.size, offs + 2 * size, ("B", 0, -1)
|
||||
),
|
||||
]
|
||||
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 = [
|
||||
ImageFile._Tile("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))
|
||||
]
|
||||
self.tile = [("raw", (0, 0)+self.size, offs,
|
||||
(self.rawmode, 0, -1))]
|
||||
|
||||
@property
|
||||
def n_frames(self) -> int:
|
||||
def n_frames(self):
|
||||
return self.info[FRAMES]
|
||||
|
||||
@property
|
||||
def is_animated(self) -> bool:
|
||||
def is_animated(self):
|
||||
return self.info[FRAMES] > 1
|
||||
|
||||
def seek(self, frame: int) -> None:
|
||||
if not self._seek_check(frame):
|
||||
def seek(self, frame):
|
||||
|
||||
if frame < 0 or frame >= self.info[FRAMES]:
|
||||
raise EOFError("seek outside sequence")
|
||||
|
||||
if self.frame == frame:
|
||||
return
|
||||
if isinstance(self._fp, DeferredError):
|
||||
raise self._fp.ex
|
||||
|
||||
self.frame = frame
|
||||
|
||||
@ -305,21 +287,18 @@ class ImImageFile(ImageFile.ImageFile):
|
||||
size = ((self.size[0] * bits + 7) // 8) * self.size[1]
|
||||
offs = self.__offset + frame * size
|
||||
|
||||
self.fp = self._fp
|
||||
self.fp = self.__fp
|
||||
|
||||
self.tile = [
|
||||
ImageFile._Tile("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))
|
||||
]
|
||||
self.tile = [("raw", (0, 0)+self.size, offs, (self.rawmode, 0, -1))]
|
||||
|
||||
def tell(self):
|
||||
|
||||
def tell(self) -> int:
|
||||
return self.frame
|
||||
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
# Save IM files
|
||||
|
||||
|
||||
SAVE = {
|
||||
# mode: (im type, raw mode)
|
||||
"1": ("0 1", "1"),
|
||||
@ -336,54 +315,38 @@ SAVE = {
|
||||
"RGBA": ("RGBA", "RGBA;L"),
|
||||
"RGBX": ("RGBX", "RGBX;L"),
|
||||
"CMYK": ("CMYK", "CMYK;L"),
|
||||
"YCbCr": ("YCC", "YCbCr;L"),
|
||||
"YCbCr": ("YCC", "YCbCr;L")
|
||||
}
|
||||
|
||||
|
||||
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
||||
def _save(im, fp, filename, check=0):
|
||||
|
||||
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
|
||||
except KeyError:
|
||||
raise ValueError("Cannot save %s images as IM" % im.mode)
|
||||
|
||||
frames = im.encoderinfo.get("frames", 1)
|
||||
|
||||
fp.write(f"Image type: {image_type} image\r\n".encode("ascii"))
|
||||
if check:
|
||||
return check
|
||||
|
||||
fp.write(("Image type: %s image\r\n" % image_type).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(f"Image size (x*y): {im.size[0]}*{im.size[1]}\r\n".encode("ascii"))
|
||||
fp.write(f"File size (no of images): {frames}\r\n".encode("ascii"))
|
||||
if im.mode in ["P", "PA"]:
|
||||
fp.write(("Name: %s\r\n" % filename).encode('ascii'))
|
||||
fp.write(("Image size (x*y): %d*%d\r\n" % im.size).encode('ascii'))
|
||||
fp.write(("File size (no of images): %d\r\n" % frames).encode('ascii'))
|
||||
if im.mode == "P":
|
||||
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, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, -1))]
|
||||
)
|
||||
|
||||
fp.write(b"\000" * (511-fp.tell()) + b"\032")
|
||||
if im.mode == "P":
|
||||
fp.write(im.im.getpalette("RGB", "RGB;L")) # 768 bytes
|
||||
ImageFile._save(im, fp, [("raw", (0, 0)+im.size, 0, (rawmode, 0, -1))])
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
# Registry
|
||||
|
||||
|
||||
Image.register_open(ImImageFile.format, ImImageFile)
|
||||
Image.register_save(ImImageFile.format, _save)
|
||||
|
||||
2710
PIL/Image.py
Normal file
2710
PIL/Image.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -15,13 +15,11 @@
|
||||
# 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.
|
||||
def constant(image, value):
|
||||
"""Fill a channel with a given grey level.
|
||||
|
||||
:rtype: :py:class:`~PIL.Image.Image`
|
||||
"""
|
||||
@ -29,7 +27,7 @@ def constant(image: Image.Image, value: int) -> Image.Image:
|
||||
return Image.new("L", image.size, value)
|
||||
|
||||
|
||||
def duplicate(image: Image.Image) -> Image.Image:
|
||||
def duplicate(image):
|
||||
"""Copy a channel. Alias for :py:meth:`PIL.Image.Image.copy`.
|
||||
|
||||
:rtype: :py:class:`~PIL.Image.Image`
|
||||
@ -38,9 +36,11 @@ def duplicate(image: Image.Image) -> Image.Image:
|
||||
return image.copy()
|
||||
|
||||
|
||||
def invert(image: Image.Image) -> Image.Image:
|
||||
def invert(image):
|
||||
"""
|
||||
Invert an image (channel). ::
|
||||
Invert an image (channel).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
out = MAX - image
|
||||
|
||||
@ -51,10 +51,12 @@ def invert(image: Image.Image) -> Image.Image:
|
||||
return image._new(image.im.chop_invert())
|
||||
|
||||
|
||||
def lighter(image1: Image.Image, image2: Image.Image) -> Image.Image:
|
||||
def lighter(image1, image2):
|
||||
"""
|
||||
Compares the two images, pixel by pixel, and returns a new image containing
|
||||
the lighter values. ::
|
||||
the lighter values.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
out = max(image1, image2)
|
||||
|
||||
@ -66,10 +68,12 @@ def lighter(image1: Image.Image, image2: Image.Image) -> Image.Image:
|
||||
return image1._new(image1.im.chop_lighter(image2.im))
|
||||
|
||||
|
||||
def darker(image1: Image.Image, image2: Image.Image) -> Image.Image:
|
||||
def darker(image1, image2):
|
||||
"""
|
||||
Compares the two images, pixel by pixel, and returns a new image containing
|
||||
the darker values. ::
|
||||
Compares the two images, pixel by pixel, and returns a new image
|
||||
containing the darker values.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
out = min(image1, image2)
|
||||
|
||||
@ -81,10 +85,12 @@ def darker(image1: Image.Image, image2: Image.Image) -> Image.Image:
|
||||
return image1._new(image1.im.chop_darker(image2.im))
|
||||
|
||||
|
||||
def difference(image1: Image.Image, image2: Image.Image) -> Image.Image:
|
||||
def difference(image1, image2):
|
||||
"""
|
||||
Returns the absolute value of the pixel-by-pixel difference between the two
|
||||
images. ::
|
||||
images.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
out = abs(image1 - image2)
|
||||
|
||||
@ -96,12 +102,14 @@ def difference(image1: Image.Image, image2: Image.Image) -> Image.Image:
|
||||
return image1._new(image1.im.chop_difference(image2.im))
|
||||
|
||||
|
||||
def multiply(image1: Image.Image, image2: Image.Image) -> Image.Image:
|
||||
def multiply(image1, image2):
|
||||
"""
|
||||
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. ::
|
||||
you multiply with a solid white image, the image is unaffected.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
out = image1 * image2 / MAX
|
||||
|
||||
@ -113,9 +121,11 @@ def multiply(image1: Image.Image, image2: Image.Image) -> Image.Image:
|
||||
return image1._new(image1.im.chop_multiply(image2.im))
|
||||
|
||||
|
||||
def screen(image1: Image.Image, image2: Image.Image) -> Image.Image:
|
||||
def screen(image1, image2):
|
||||
"""
|
||||
Superimposes two inverted images on top of each other. ::
|
||||
Superimposes two inverted images on top of each other.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
out = MAX - ((MAX - image1) * (MAX - image2) / MAX)
|
||||
|
||||
@ -127,48 +137,12 @@ def screen(image1: Image.Image, image2: Image.Image) -> Image.Image:
|
||||
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:
|
||||
def add(image1, image2, scale=1.0, offset=0):
|
||||
"""
|
||||
Adds two images, dividing the result by scale and adding the
|
||||
offset. If omitted, scale defaults to 1.0, and offset to 0.0. ::
|
||||
offset. If omitted, scale defaults to 1.0, and offset to 0.0.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
out = ((image1 + image2) / scale + offset)
|
||||
|
||||
@ -180,12 +154,12 @@ def add(
|
||||
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:
|
||||
def subtract(image1, image2, scale=1.0, offset=0):
|
||||
"""
|
||||
Subtracts two images, dividing the result by scale and adding the offset.
|
||||
If omitted, scale defaults to 1.0, and offset to 0.0. ::
|
||||
Subtracts two images, dividing the result by scale and adding the
|
||||
offset. If omitted, scale defaults to 1.0, and offset to 0.0.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
out = ((image1 - image2) / scale + offset)
|
||||
|
||||
@ -197,8 +171,10 @@ def subtract(
|
||||
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. ::
|
||||
def add_modulo(image1, image2):
|
||||
"""Add two images, without clipping the result.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
out = ((image1 + image2) % MAX)
|
||||
|
||||
@ -210,8 +186,10 @@ def add_modulo(image1: Image.Image, image2: Image.Image) -> Image.Image:
|
||||
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. ::
|
||||
def subtract_modulo(image1, image2):
|
||||
"""Subtract two images, without clipping the result.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
out = ((image1 - image2) % MAX)
|
||||
|
||||
@ -223,13 +201,10 @@ def subtract_modulo(image1: Image.Image, image2: Image.Image) -> Image.Image:
|
||||
return image1._new(image1.im.chop_subtract_modulo(image2.im))
|
||||
|
||||
|
||||
def logical_and(image1: Image.Image, image2: Image.Image) -> Image.Image:
|
||||
def logical_and(image1, image2):
|
||||
"""Logical AND between two images.
|
||||
|
||||
Both of the images must have mode "1". If you would like to perform a
|
||||
logical AND on an image with a mode other than "1", try
|
||||
:py:meth:`~PIL.ImageChops.multiply` instead, using a black-and-white mask
|
||||
as the second image. ::
|
||||
.. code-block:: python
|
||||
|
||||
out = ((image1 and image2) % MAX)
|
||||
|
||||
@ -241,10 +216,10 @@ def logical_and(image1: Image.Image, image2: Image.Image) -> Image.Image:
|
||||
return image1._new(image1.im.chop_and(image2.im))
|
||||
|
||||
|
||||
def logical_or(image1: Image.Image, image2: Image.Image) -> Image.Image:
|
||||
def logical_or(image1, image2):
|
||||
"""Logical OR between two images.
|
||||
|
||||
Both of the images must have mode "1". ::
|
||||
.. code-block:: python
|
||||
|
||||
out = ((image1 or image2) % MAX)
|
||||
|
||||
@ -256,10 +231,10 @@ def logical_or(image1: Image.Image, image2: Image.Image) -> Image.Image:
|
||||
return image1._new(image1.im.chop_or(image2.im))
|
||||
|
||||
|
||||
def logical_xor(image1: Image.Image, image2: Image.Image) -> Image.Image:
|
||||
def logical_xor(image1, image2):
|
||||
"""Logical XOR between two images.
|
||||
|
||||
Both of the images must have mode "1". ::
|
||||
.. code-block:: python
|
||||
|
||||
out = ((bool(image1) != bool(image2)) % MAX)
|
||||
|
||||
@ -271,9 +246,9 @@ def logical_xor(image1: Image.Image, image2: Image.Image) -> Image.Image:
|
||||
return image1._new(image1.im.chop_xor(image2.im))
|
||||
|
||||
|
||||
def blend(image1: Image.Image, image2: Image.Image, alpha: float) -> Image.Image:
|
||||
def blend(image1, image2, alpha):
|
||||
"""Blend images using constant transparency weight. Alias for
|
||||
:py:func:`PIL.Image.blend`.
|
||||
:py:meth:`PIL.Image.Image.blend`.
|
||||
|
||||
:rtype: :py:class:`~PIL.Image.Image`
|
||||
"""
|
||||
@ -281,11 +256,9 @@ def blend(image1: Image.Image, image2: Image.Image, alpha: float) -> Image.Image
|
||||
return Image.blend(image1, image2, alpha)
|
||||
|
||||
|
||||
def composite(
|
||||
image1: Image.Image, image2: Image.Image, mask: Image.Image
|
||||
) -> Image.Image:
|
||||
def composite(image1, image2, mask):
|
||||
"""Create composite using transparency mask. Alias for
|
||||
:py:func:`PIL.Image.composite`.
|
||||
:py:meth:`PIL.Image.Image.composite`.
|
||||
|
||||
:rtype: :py:class:`~PIL.Image.Image`
|
||||
"""
|
||||
@ -293,12 +266,11 @@ def composite(
|
||||
return Image.composite(image1, image2, mask)
|
||||
|
||||
|
||||
def offset(image: Image.Image, xoffset: int, yoffset: int | None = None) -> Image.Image:
|
||||
def offset(image, xoffset, yoffset=None):
|
||||
"""Returns a copy of the image where data has been offset by the given
|
||||
distances. Data wraps around the edges. If ``yoffset`` is omitted, it
|
||||
is assumed to be equal to ``xoffset``.
|
||||
distances. Data wraps around the edges. If **yoffset** is omitted, it
|
||||
is assumed to be equal to **xoffset**.
|
||||
|
||||
:param image: Input image.
|
||||
:param xoffset: The horizontal distance.
|
||||
:param yoffset: The vertical distance. If omitted, both
|
||||
distances are set to the same value.
|
||||
File diff suppressed because it is too large
Load Diff
@ -16,156 +16,130 @@
|
||||
#
|
||||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from functools import lru_cache
|
||||
|
||||
from . import Image
|
||||
import re
|
||||
|
||||
|
||||
@lru_cache
|
||||
def getrgb(color: str) -> tuple[int, int, int] | tuple[int, int, int, int]:
|
||||
def getrgb(color):
|
||||
"""
|
||||
Convert a color string to an RGB or RGBA tuple. If the string cannot be
|
||||
parsed, this function raises a :py:exc:`ValueError` exception.
|
||||
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
|
||||
|
||||
: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
|
||||
colormap[color] = rgb = getrgb(rgb)
|
||||
return rgb
|
||||
|
||||
# check for known string formats
|
||||
if re.match("#[a-f0-9]{3}$", color):
|
||||
return int(color[1] * 2, 16), int(color[2] * 2, 16), int(color[3] * 2, 16)
|
||||
|
||||
if re.match("#[a-f0-9]{4}$", color):
|
||||
if re.match('#[a-f0-9]{3}$', color):
|
||||
return (
|
||||
int(color[1] * 2, 16),
|
||||
int(color[2] * 2, 16),
|
||||
int(color[3] * 2, 16),
|
||||
int(color[4] * 2, 16),
|
||||
)
|
||||
int(color[1]*2, 16),
|
||||
int(color[2]*2, 16),
|
||||
int(color[3]*2, 16),
|
||||
)
|
||||
|
||||
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]{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]{8}$", color):
|
||||
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))
|
||||
return (
|
||||
int(m.group(1)),
|
||||
int(m.group(2)),
|
||||
int(m.group(3))
|
||||
)
|
||||
|
||||
m = re.match(r"rgb\(\s*(\d+)%\s*,\s*(\d+)%\s*,\s*(\d+)%\s*\)$", color)
|
||||
if m:
|
||||
return (
|
||||
int((int(m.group(1)) * 255) / 100.0 + 0.5),
|
||||
int((int(m.group(2)) * 255) / 100.0 + 0.5),
|
||||
int((int(m.group(3)) * 255) / 100.0 + 0.5),
|
||||
)
|
||||
int((int(m.group(3)) * 255) / 100.0 + 0.5)
|
||||
)
|
||||
|
||||
m = re.match(
|
||||
r"hsl\(\s*(\d+\.?\d*)\s*,\s*(\d+\.?\d*)%\s*,\s*(\d+\.?\d*)%\s*\)$", color
|
||||
)
|
||||
m = re.match(r"hsl\(\s*(\d+)\s*,\s*(\d+)%\s*,\s*(\d+)%\s*\)$", color)
|
||||
if m:
|
||||
from colorsys import hls_to_rgb
|
||||
|
||||
rgb_floats = hls_to_rgb(
|
||||
rgb = hls_to_rgb(
|
||||
float(m.group(1)) / 360.0,
|
||||
float(m.group(3)) / 100.0,
|
||||
float(m.group(2)) / 100.0,
|
||||
)
|
||||
)
|
||||
return (
|
||||
int(rgb_floats[0] * 255 + 0.5),
|
||||
int(rgb_floats[1] * 255 + 0.5),
|
||||
int(rgb_floats[2] * 255 + 0.5),
|
||||
)
|
||||
int(rgb[0] * 255 + 0.5),
|
||||
int(rgb[1] * 255 + 0.5),
|
||||
int(rgb[2] * 255 + 0.5)
|
||||
)
|
||||
|
||||
m = re.match(
|
||||
r"hs[bv]\(\s*(\d+\.?\d*)\s*,\s*(\d+\.?\d*)%\s*,\s*(\d+\.?\d*)%\s*\)$", color
|
||||
)
|
||||
m = re.match(r"rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\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)
|
||||
int(m.group(1)),
|
||||
int(m.group(2)),
|
||||
int(m.group(3)),
|
||||
int(m.group(4))
|
||||
)
|
||||
raise ValueError("unknown color specifier: %r" % color)
|
||||
|
||||
|
||||
@lru_cache
|
||||
def getcolor(color: str, mode: str) -> int | tuple[int, ...]:
|
||||
def getcolor(color, mode):
|
||||
"""
|
||||
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.
|
||||
Same as :py:func:`~PIL.ImageColor.getrgb`, but converts the RGB value to a
|
||||
greyscale value if the mode is not color or a palette image. If the string
|
||||
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])``
|
||||
:return: ``(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]
|
||||
color, alpha = getrgb(color), 255
|
||||
if len(color) == 4:
|
||||
color, alpha = color[0:3], color[3]
|
||||
|
||||
if mode == "HSV":
|
||||
from colorsys import rgb_to_hsv
|
||||
if Image.getmodebase(mode) == "L":
|
||||
r, g, b = color
|
||||
color = (r*299 + g*587 + b*114)//1000
|
||||
if mode[-1] == 'A':
|
||||
return (color, alpha)
|
||||
else:
|
||||
if mode[-1] == 'A':
|
||||
return color + (alpha,)
|
||||
return color
|
||||
|
||||
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]] = {
|
||||
colormap = {
|
||||
# X11 colour table from https://drafts.csswg.org/css-color-4/, with
|
||||
# gray/grey spelling issues fixed. This is a superset of HTML 4.0
|
||||
# colour names used in CSS 1.
|
||||
367
PIL/ImageDraw.py
Normal file
367
PIL/ImageDraw.py
Normal file
@ -0,0 +1,367 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
import numbers
|
||||
|
||||
from . import Image, ImageColor
|
||||
from ._util import isStringType
|
||||
|
||||
"""
|
||||
A simple 2D drawing interface for PIL images.
|
||||
<p>
|
||||
Application code should use the <b>Draw</b> factory, instead of
|
||||
directly.
|
||||
"""
|
||||
|
||||
|
||||
class ImageDraw(object):
|
||||
|
||||
def __init__(self, im, mode=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:
|
||||
raise ValueError("mode mismatch")
|
||||
if mode == "P":
|
||||
self.palette = im.palette
|
||||
else:
|
||||
self.palette = None
|
||||
self.im = im.im
|
||||
self.draw = Image.core.draw(self.im, blend)
|
||||
self.mode = mode
|
||||
if mode in ("I", "F"):
|
||||
self.ink = self.draw.draw_ink(1, mode)
|
||||
else:
|
||||
self.ink = self.draw.draw_ink(-1, mode)
|
||||
if mode in ("1", "P", "I", "F"):
|
||||
# FIXME: fix Fill2 to properly support matte for I+F images
|
||||
self.fontmode = "1"
|
||||
else:
|
||||
self.fontmode = "L" # aliasing is okay for other modes
|
||||
self.fill = 0
|
||||
self.font = None
|
||||
|
||||
def getfont(self):
|
||||
"""Get the current default font."""
|
||||
if not self.font:
|
||||
# FIXME: should add a font repository
|
||||
from . import ImageFont
|
||||
self.font = ImageFont.load_default()
|
||||
return self.font
|
||||
|
||||
def _getink(self, ink, fill=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 isStringType(ink):
|
||||
ink = ImageColor.getcolor(ink, self.mode)
|
||||
if self.palette and not isinstance(ink, numbers.Number):
|
||||
ink = self.palette.getcolor(ink)
|
||||
ink = self.draw.draw_ink(ink, self.mode)
|
||||
if fill is not None:
|
||||
if isStringType(fill):
|
||||
fill = ImageColor.getcolor(fill, self.mode)
|
||||
if self.palette and not isinstance(fill, numbers.Number):
|
||||
fill = self.palette.getcolor(fill)
|
||||
fill = self.draw.draw_ink(fill, self.mode)
|
||||
return ink, fill
|
||||
|
||||
def arc(self, xy, start, end, fill=None):
|
||||
"""Draw an arc."""
|
||||
ink, fill = self._getink(fill)
|
||||
if ink is not None:
|
||||
self.draw.draw_arc(xy, start, end, ink)
|
||||
|
||||
def bitmap(self, xy, bitmap, fill=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, start, end, fill=None, outline=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:
|
||||
self.draw.draw_chord(xy, start, end, ink, 0)
|
||||
|
||||
def ellipse(self, xy, fill=None, outline=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:
|
||||
self.draw.draw_ellipse(xy, ink, 0)
|
||||
|
||||
def line(self, xy, fill=None, width=0):
|
||||
"""Draw a line, or a connected sequence of line segments."""
|
||||
ink, fill = self._getink(fill)
|
||||
if ink is not None:
|
||||
self.draw.draw_lines(xy, ink, width)
|
||||
|
||||
def shape(self, shape, fill=None, outline=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:
|
||||
self.draw.draw_outline(shape, ink, 0)
|
||||
|
||||
def pieslice(self, xy, start, end, fill=None, outline=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:
|
||||
self.draw.draw_pieslice(xy, start, end, ink, 0)
|
||||
|
||||
def point(self, xy, fill=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, fill=None, outline=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:
|
||||
self.draw.draw_polygon(xy, ink, 0)
|
||||
|
||||
def rectangle(self, xy, fill=None, outline=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:
|
||||
self.draw.draw_rectangle(xy, ink, 0)
|
||||
|
||||
def _multiline_check(self, text):
|
||||
"""Draw text."""
|
||||
split_character = "\n" if isinstance(text, str) else b"\n"
|
||||
|
||||
return split_character in text
|
||||
|
||||
def _multiline_split(self, text):
|
||||
split_character = "\n" if isinstance(text, str) else b"\n"
|
||||
|
||||
return text.split(split_character)
|
||||
|
||||
def text(self, xy, text, fill=None, font=None, anchor=None,
|
||||
*args, **kwargs):
|
||||
if self._multiline_check(text):
|
||||
return self.multiline_text(xy, text, fill, font, anchor,
|
||||
*args, **kwargs)
|
||||
|
||||
ink, fill = self._getink(fill)
|
||||
if font is None:
|
||||
font = self.getfont()
|
||||
if ink is None:
|
||||
ink = fill
|
||||
if ink is not None:
|
||||
try:
|
||||
mask, offset = font.getmask2(text, self.fontmode)
|
||||
xy = xy[0] + offset[0], xy[1] + offset[1]
|
||||
except AttributeError:
|
||||
try:
|
||||
mask = font.getmask(text, self.fontmode)
|
||||
except TypeError:
|
||||
mask = font.getmask(text)
|
||||
self.draw.draw_bitmap(xy, mask, ink)
|
||||
|
||||
def multiline_text(self, xy, text, fill=None, font=None, anchor=None,
|
||||
spacing=4, align="left"):
|
||||
widths = []
|
||||
max_width = 0
|
||||
lines = self._multiline_split(text)
|
||||
line_spacing = self.textsize('A', font=font)[1] + spacing
|
||||
for line in lines:
|
||||
line_width, line_height = self.textsize(line, font)
|
||||
widths.append(line_width)
|
||||
max_width = max(max_width, line_width)
|
||||
left, top = xy
|
||||
for idx, line in enumerate(lines):
|
||||
if align == "left":
|
||||
pass # left = x
|
||||
elif align == "center":
|
||||
left += (max_width - widths[idx]) / 2.0
|
||||
elif align == "right":
|
||||
left += (max_width - widths[idx])
|
||||
else:
|
||||
assert False, 'align must be "left", "center" or "right"'
|
||||
self.text((left, top), line, fill, font, anchor)
|
||||
top += line_spacing
|
||||
left = xy[0]
|
||||
|
||||
def textsize(self, text, font=None, *args, **kwargs):
|
||||
"""Get the size of a given string, in pixels."""
|
||||
if self._multiline_check(text):
|
||||
return self.multiline_textsize(text, font, *args, **kwargs)
|
||||
|
||||
if font is None:
|
||||
font = self.getfont()
|
||||
return font.getsize(text)
|
||||
|
||||
def multiline_textsize(self, text, font=None, spacing=4):
|
||||
max_width = 0
|
||||
lines = self._multiline_split(text)
|
||||
line_spacing = self.textsize('A', font=font)[1] + spacing
|
||||
for line in lines:
|
||||
line_width, line_height = self.textsize(line, font)
|
||||
max_width = max(max_width, line_width)
|
||||
return max_width, len(lines)*line_spacing
|
||||
|
||||
|
||||
def Draw(im, mode=None):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
(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.
|
||||
"""
|
||||
# FIXME: this needs more work!
|
||||
# FIXME: come up with a better 'hints' scheme.
|
||||
handler = None
|
||||
if not hints or "nicest" in hints:
|
||||
try:
|
||||
from . import _imagingagg as handler
|
||||
except ImportError:
|
||||
pass
|
||||
if handler is None:
|
||||
from . import ImageDraw2 as handler
|
||||
if im:
|
||||
im = handler.Draw(im)
|
||||
return im, handler
|
||||
|
||||
|
||||
def floodfill(image, xy, value, border=None):
|
||||
"""
|
||||
(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.
|
||||
"""
|
||||
# based on an implementation by Eric S. Raymond
|
||||
pixel = image.load()
|
||||
x, y = xy
|
||||
try:
|
||||
background = pixel[x, y]
|
||||
if background == value:
|
||||
return # seed point already has fill color
|
||||
pixel[x, y] = value
|
||||
except (ValueError, IndexError):
|
||||
return # seed point outside image
|
||||
edge = [(x, y)]
|
||||
if border is None:
|
||||
while edge:
|
||||
newedge = []
|
||||
for (x, y) in edge:
|
||||
for (s, t) in ((x+1, y), (x-1, y), (x, y+1), (x, y-1)):
|
||||
try:
|
||||
p = pixel[s, t]
|
||||
except IndexError:
|
||||
pass
|
||||
else:
|
||||
if p == background:
|
||||
pixel[s, t] = value
|
||||
newedge.append((s, t))
|
||||
edge = newedge
|
||||
else:
|
||||
while edge:
|
||||
newedge = []
|
||||
for (x, y) in edge:
|
||||
for (s, t) in ((x+1, y), (x-1, y), (x, y+1), (x, y-1)):
|
||||
try:
|
||||
p = pixel[s, t]
|
||||
except IndexError:
|
||||
pass
|
||||
else:
|
||||
if p != value and p != border:
|
||||
pixel[s, t] = value
|
||||
newedge.append((s, t))
|
||||
edge = newedge
|
||||
111
PIL/ImageDraw2.py
Normal file
111
PIL/ImageDraw2.py
Normal file
@ -0,0 +1,111 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
from . import Image, ImageColor, ImageDraw, ImageFont, ImagePath
|
||||
|
||||
|
||||
class Pen(object):
|
||||
def __init__(self, color, width=1, opacity=255):
|
||||
self.color = ImageColor.getrgb(color)
|
||||
self.width = width
|
||||
|
||||
|
||||
class Brush(object):
|
||||
def __init__(self, color, opacity=255):
|
||||
self.color = ImageColor.getrgb(color)
|
||||
|
||||
|
||||
class Font(object):
|
||||
def __init__(self, color, file, size=12):
|
||||
# FIXME: add support for bitmap fonts
|
||||
self.color = ImageColor.getrgb(color)
|
||||
self.font = ImageFont.truetype(file, size)
|
||||
|
||||
|
||||
class Draw(object):
|
||||
|
||||
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):
|
||||
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):
|
||||
(xoffset, yoffset) = offset
|
||||
self.transform = (1, 0, xoffset, 0, 1, yoffset)
|
||||
|
||||
def arc(self, xy, start, end, *options):
|
||||
self.render("arc", xy, start, end, *options)
|
||||
|
||||
def chord(self, xy, start, end, *options):
|
||||
self.render("chord", xy, start, end, *options)
|
||||
|
||||
def ellipse(self, xy, *options):
|
||||
self.render("ellipse", xy, *options)
|
||||
|
||||
def line(self, xy, *options):
|
||||
self.render("line", xy, *options)
|
||||
|
||||
def pieslice(self, xy, start, end, *options):
|
||||
self.render("pieslice", xy, start, end, *options)
|
||||
|
||||
def polygon(self, xy, *options):
|
||||
self.render("polygon", xy, *options)
|
||||
|
||||
def rectangle(self, xy, *options):
|
||||
self.render("rectangle", xy, *options)
|
||||
|
||||
def symbol(self, xy, symbol, *options):
|
||||
raise NotImplementedError("not in this version")
|
||||
|
||||
def text(self, xy, text, font):
|
||||
if self.transform:
|
||||
xy = ImagePath.Path(xy)
|
||||
xy.transform(self.transform)
|
||||
self.draw.text(xy, text, font=font.font, fill=font.color)
|
||||
|
||||
def textsize(self, text, font):
|
||||
return self.draw.textsize(text, font=font.font)
|
||||
@ -17,16 +17,13 @@
|
||||
#
|
||||
# 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
|
||||
class _Enhance(object):
|
||||
|
||||
def enhance(self, factor: float) -> Image.Image:
|
||||
def enhance(self, factor):
|
||||
"""
|
||||
Returns an enhanced image.
|
||||
|
||||
@ -48,16 +45,13 @@ class Color(_Enhance):
|
||||
factor of 0.0 gives a black and white image. A factor of 1.0 gives
|
||||
the original image.
|
||||
"""
|
||||
|
||||
def __init__(self, image: Image.Image) -> None:
|
||||
def __init__(self, image):
|
||||
self.image = image
|
||||
self.intermediate_mode = "L"
|
||||
if "A" in image.getbands():
|
||||
self.intermediate_mode = "LA"
|
||||
self.intermediate_mode = 'L'
|
||||
if 'A' in image.getbands():
|
||||
self.intermediate_mode = 'LA'
|
||||
|
||||
if self.intermediate_mode != image.mode:
|
||||
image = image.convert(self.intermediate_mode).convert(image.mode)
|
||||
self.degenerate = image
|
||||
self.degenerate = image.convert(self.intermediate_mode).convert(image.mode)
|
||||
|
||||
|
||||
class Contrast(_Enhance):
|
||||
@ -65,20 +59,15 @@ class Contrast(_Enhance):
|
||||
|
||||
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.
|
||||
gives a solid grey image. A factor of 1.0 gives the original image.
|
||||
"""
|
||||
|
||||
def __init__(self, image: Image.Image) -> None:
|
||||
def __init__(self, image):
|
||||
self.image = image
|
||||
if image.mode != "L":
|
||||
image = image.convert("L")
|
||||
mean = int(ImageStat.Stat(image).mean[0] + 0.5)
|
||||
self.degenerate = Image.new("L", image.size, mean)
|
||||
if self.degenerate.mode != self.image.mode:
|
||||
self.degenerate = self.degenerate.convert(self.image.mode)
|
||||
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 self.image.getbands():
|
||||
self.degenerate.putalpha(self.image.getchannel("A"))
|
||||
if 'A' in image.getbands():
|
||||
self.degenerate.putalpha(image.split()[-1])
|
||||
|
||||
|
||||
class Brightness(_Enhance):
|
||||
@ -88,13 +77,12 @@ class Brightness(_Enhance):
|
||||
enhancement factor of 0.0 gives a black image. A factor of 1.0 gives the
|
||||
original image.
|
||||
"""
|
||||
|
||||
def __init__(self, image: Image.Image) -> None:
|
||||
def __init__(self, image):
|
||||
self.image = image
|
||||
self.degenerate = Image.new(image.mode, image.size, 0)
|
||||
|
||||
if "A" in image.getbands():
|
||||
self.degenerate.putalpha(image.getchannel("A"))
|
||||
if 'A' in image.getbands():
|
||||
self.degenerate.putalpha(image.split()[-1])
|
||||
|
||||
|
||||
class Sharpness(_Enhance):
|
||||
@ -104,10 +92,9 @@ class Sharpness(_Enhance):
|
||||
enhancement factor of 0.0 gives a blurred image, a factor of 1.0 gives the
|
||||
original image, and a factor of 2.0 gives a sharpened image.
|
||||
"""
|
||||
|
||||
def __init__(self, image: Image.Image) -> None:
|
||||
def __init__(self, image):
|
||||
self.image = image
|
||||
self.degenerate = image.filter(ImageFilter.SMOOTH)
|
||||
|
||||
if "A" in image.getbands():
|
||||
self.degenerate.putalpha(image.getchannel("A"))
|
||||
if 'A' in image.getbands():
|
||||
self.degenerate.putalpha(image.split()[-1])
|
||||
654
PIL/ImageFile.py
Normal file
654
PIL/ImageFile.py
Normal file
@ -0,0 +1,654 @@
|
||||
#
|
||||
# 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 . import Image
|
||||
from ._util import isPath
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
import struct
|
||||
|
||||
MAXBLOCK = 65536
|
||||
|
||||
SAFEBLOCK = 1024*1024
|
||||
|
||||
LOAD_TRUNCATED_IMAGES = False
|
||||
|
||||
ERRORS = {
|
||||
-1: "image buffer overrun error",
|
||||
-2: "decoding error",
|
||||
-3: "unknown error",
|
||||
-8: "bad configuration",
|
||||
-9: "out of memory error"
|
||||
}
|
||||
|
||||
|
||||
def raise_ioerror(error):
|
||||
try:
|
||||
message = Image.core.getcodecstatus(error)
|
||||
except AttributeError:
|
||||
message = ERRORS.get(error)
|
||||
if not message:
|
||||
message = "decoder error %d" % error
|
||||
raise IOError(message + " when reading image file")
|
||||
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
# Helpers
|
||||
|
||||
def _tilesort(t):
|
||||
# sort on offset
|
||||
return t[2]
|
||||
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
# ImageFile base class
|
||||
|
||||
class ImageFile(Image.Image):
|
||||
"Base class for image file format handlers."
|
||||
|
||||
def __init__(self, fp=None, filename=None):
|
||||
Image.Image.__init__(self)
|
||||
|
||||
self.tile = None
|
||||
self.readonly = 1 # until we know better
|
||||
|
||||
self.decoderconfig = ()
|
||||
self.decodermaxblock = MAXBLOCK
|
||||
|
||||
if isPath(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:
|
||||
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:
|
||||
# close the file only if we have opened it this constructor
|
||||
if self._exclusive_fp:
|
||||
self.fp.close()
|
||||
raise SyntaxError(v)
|
||||
|
||||
if not self.mode or self.size[0] <= 0:
|
||||
raise SyntaxError("not identified by this driver")
|
||||
|
||||
def draft(self, mode, size):
|
||||
"Set draft mode"
|
||||
|
||||
pass
|
||||
|
||||
def verify(self):
|
||||
"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"
|
||||
|
||||
pixel = Image.Image.load(self)
|
||||
|
||||
if self.tile is None:
|
||||
raise IOError("cannot load this image")
|
||||
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 decoder_name == "raw" and len(args) >= 3 and args[0] == self.mode \
|
||||
and args[0] in Image._MAPMODES:
|
||||
try:
|
||||
if hasattr(Image.core, "map"):
|
||||
# use built-in mapper WIN32 only
|
||||
self.map = Image.core.map(self.filename)
|
||||
self.map.seek(offset)
|
||||
self.im = self.map.readimage(
|
||||
self.mode, self.size, args[1], args[2]
|
||||
)
|
||||
else:
|
||||
# use mmap, if possible
|
||||
import mmap
|
||||
fp = open(self.filename, "r")
|
||||
size = os.path.getsize(self.filename)
|
||||
self.map = mmap.mmap(fp.fileno(), size, access=mmap.ACCESS_READ)
|
||||
self.im = Image.core.map_buffer(
|
||||
self.map, self.size, decoder_name, extents, offset, args
|
||||
)
|
||||
readonly = 1
|
||||
# After trashing self.im, we might need to reload the palette data.
|
||||
if self.palette:
|
||||
self.palette.dirty = 1
|
||||
except (AttributeError, EnvironmentError, ImportError):
|
||||
self.map = None
|
||||
|
||||
self.load_prepare()
|
||||
|
||||
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""
|
||||
|
||||
for decoder_name, extents, offset, args in self.tile:
|
||||
decoder = Image._getdecoder(self.mode, decoder_name,
|
||||
args, self.decoderconfig)
|
||||
seek(offset)
|
||||
decoder.setimage(self.im, extents)
|
||||
if decoder.pulls_fd:
|
||||
decoder.setfd(self.fp)
|
||||
status, err_code = decoder.decode(b"")
|
||||
else:
|
||||
b = prefix
|
||||
while True:
|
||||
try:
|
||||
s = read(self.decodermaxblock)
|
||||
except (IndexError, struct.error): # truncated png/gif
|
||||
if LOAD_TRUNCATED_IMAGES:
|
||||
break
|
||||
else:
|
||||
raise IOError("image file is truncated")
|
||||
|
||||
if not s: # truncated jpeg
|
||||
self.tile = []
|
||||
|
||||
# JpegDecode needs to clean things up here either way
|
||||
# If we don't destroy the decompressor,
|
||||
# we have a memory leak.
|
||||
decoder.cleanup()
|
||||
|
||||
if LOAD_TRUNCATED_IMAGES:
|
||||
break
|
||||
else:
|
||||
raise IOError("image file is truncated "
|
||||
"(%d bytes not processed)" % len(b))
|
||||
|
||||
b = b + s
|
||||
n, err_code = decoder.decode(b)
|
||||
if n < 0:
|
||||
break
|
||||
b = b[n:]
|
||||
|
||||
# Need to cleanup here to prevent leaks in PyPy
|
||||
decoder.cleanup()
|
||||
|
||||
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_ioerror(err_code)
|
||||
|
||||
return Image.Image.load(self)
|
||||
|
||||
def load_prepare(self):
|
||||
# create image memory if necessary
|
||||
if not self.im or\
|
||||
self.im.mode != self.mode or self.im.size != self.size:
|
||||
self.im = Image.core.new(self.mode, self.size)
|
||||
# create palette (optional)
|
||||
if self.mode == "P":
|
||||
Image.Image.load(self)
|
||||
|
||||
def load_end(self):
|
||||
# may be overridden
|
||||
pass
|
||||
|
||||
# may be defined for contained formats
|
||||
# def load_seek(self, pos):
|
||||
# pass
|
||||
|
||||
# may be defined for blocked formats (e.g. PNG)
|
||||
# def load_read(self, bytes):
|
||||
# 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):
|
||||
raise NotImplementedError(
|
||||
"StubImageFile subclass must implement _open"
|
||||
)
|
||||
|
||||
def load(self):
|
||||
loader = self._load()
|
||||
if loader is None:
|
||||
raise IOError("cannot find loader for this %s file" % self.format)
|
||||
image = loader.load(self)
|
||||
assert image is not None
|
||||
# become the other object (!)
|
||||
self.__class__ = image.__class__
|
||||
self.__dict__ = image.__dict__
|
||||
|
||||
def _load(self):
|
||||
"(Hook) Find actual image loader."
|
||||
raise NotImplementedError(
|
||||
"StubImageFile subclass must implement _load"
|
||||
)
|
||||
|
||||
|
||||
class Parser(object):
|
||||
"""
|
||||
Incremental image parser. This class implements the standard
|
||||
feed/close consumer interface.
|
||||
"""
|
||||
incremental = None
|
||||
image = None
|
||||
data = None
|
||||
decoder = None
|
||||
offset = 0
|
||||
finished = 0
|
||||
|
||||
def reset(self):
|
||||
"""
|
||||
(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 IOError: 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_ioerror(e)
|
||||
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:
|
||||
try:
|
||||
fp = io.BytesIO(self.data)
|
||||
im = Image.open(fp)
|
||||
finally:
|
||||
fp.close() # explicitly close the virtual file
|
||||
except IOError:
|
||||
# traceback.print_exc()
|
||||
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 close(self):
|
||||
"""
|
||||
(Consumer) Close the stream.
|
||||
|
||||
:returns: An image object.
|
||||
:exception IOError: 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:
|
||||
raise IOError("image was incomplete")
|
||||
if not self.image:
|
||||
raise IOError("cannot parse this image")
|
||||
if self.data:
|
||||
# incremental parsing not possible; reopen the file
|
||||
# not that we have all data
|
||||
try:
|
||||
fp = io.BytesIO(self.data)
|
||||
self.image = Image.open(fp)
|
||||
finally:
|
||||
self.image.load()
|
||||
fp.close() # explicitly close the virtual file
|
||||
return self.image
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
def _save(im, fp, tile, bufsize=0):
|
||||
"""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
|
||||
if fp == sys.stdout:
|
||||
fp.flush()
|
||||
return
|
||||
try:
|
||||
fh = fp.fileno()
|
||||
fp.flush()
|
||||
except (AttributeError, io.UnsupportedOperation):
|
||||
# compress to Python file-compatible object
|
||||
for e, b, o, a in tile:
|
||||
e = Image._getencoder(im.mode, e, a, im.encoderconfig)
|
||||
if o > 0:
|
||||
fp.seek(o, 0)
|
||||
e.setimage(im.im, b)
|
||||
if e.pushes_fd:
|
||||
e.setfd(fp)
|
||||
l, s = e.encode_to_pyfd()
|
||||
else:
|
||||
while True:
|
||||
l, s, d = e.encode(bufsize)
|
||||
fp.write(d)
|
||||
if s:
|
||||
break
|
||||
if s < 0:
|
||||
raise IOError("encoder error %d when writing image file" % s)
|
||||
e.cleanup()
|
||||
else:
|
||||
# slight speedup: compress to real file object
|
||||
for e, b, o, a in tile:
|
||||
e = Image._getencoder(im.mode, e, a, im.encoderconfig)
|
||||
if o > 0:
|
||||
fp.seek(o, 0)
|
||||
e.setimage(im.im, b)
|
||||
if e.pushes_fd:
|
||||
e.setfd(fp)
|
||||
l, s = e.encode_to_pyfd()
|
||||
else:
|
||||
s = e.encode_to_file(fh, bufsize)
|
||||
if s < 0:
|
||||
raise IOError("encoder error %d when writing image file" % s)
|
||||
e.cleanup()
|
||||
if hasattr(fp, "flush"):
|
||||
fp.flush()
|
||||
|
||||
|
||||
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 <b>read</b> method.
|
||||
:param size: Number of bytes to read.
|
||||
:returns: A string containing up to <i>size</i> bytes of data.
|
||||
"""
|
||||
if size <= 0:
|
||||
return b""
|
||||
if size <= SAFEBLOCK:
|
||||
return fp.read(size)
|
||||
data = []
|
||||
while size > 0:
|
||||
block = fp.read(min(size, SAFEBLOCK))
|
||||
if not block:
|
||||
break
|
||||
data.append(block)
|
||||
size -= len(block)
|
||||
return b"".join(data)
|
||||
|
||||
|
||||
class PyCodecState(object):
|
||||
def __init__(self):
|
||||
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 PyDecoder(object):
|
||||
"""
|
||||
Python implementation of a format decoder. Override this class and
|
||||
add the decoding logic in the `decode` method.
|
||||
|
||||
See :ref:`Writing Your Own File Decoder in Python<file-decoders-py>`
|
||||
"""
|
||||
|
||||
_pulls_fd = False
|
||||
|
||||
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 decoder specific initialization
|
||||
|
||||
:param args: Array of args items from the tile entry
|
||||
:returns: None
|
||||
"""
|
||||
self.args = args
|
||||
|
||||
@property
|
||||
def pulls_fd(self):
|
||||
return self._pulls_fd
|
||||
|
||||
def decode(self, buffer):
|
||||
"""
|
||||
Override to perform the decoding process.
|
||||
|
||||
:param buffer: A bytes object with the data to be decoded. If `handles_eof`
|
||||
is set, then `buffer` will be empty and `self.fd` will be set.
|
||||
:returns: A tuple of (bytes consumed, errcode). If finished with decoding
|
||||
return <0 for the bytes consumed. Err codes are from `ERRORS`
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def cleanup(self):
|
||||
"""
|
||||
Override to perform decoder specific cleanup
|
||||
|
||||
: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 decoder
|
||||
|
||||
: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:
|
||||
raise ValueError("Size cannot be negative")
|
||||
|
||||
if (self.state.xsize + self.state.xoff > self.im.size[0] or
|
||||
self.state.ysize + self.state.yoff > self.im.size[1]):
|
||||
raise ValueError("Tile cannot extend outside image")
|
||||
|
||||
def set_as_raw(self, data, rawmode=None):
|
||||
"""
|
||||
Convenience method to set the internal image from a stream of raw data
|
||||
|
||||
:param data: Bytes to be set
|
||||
:param rawmode: The rawmode to be used for the decoder. If not specified,
|
||||
it will default to the mode of the image
|
||||
: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:
|
||||
raise ValueError("not enough image data")
|
||||
if s[1] != 0:
|
||||
raise ValueError("cannot decode image data")
|
||||
275
PIL/ImageFilter.py
Normal file
275
PIL/ImageFilter.py
Normal file
@ -0,0 +1,275 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
import functools
|
||||
|
||||
|
||||
class Filter(object):
|
||||
pass
|
||||
|
||||
|
||||
class Kernel(Filter):
|
||||
"""
|
||||
Create a convolution kernel. The current version only
|
||||
supports 3x3 and 5x5 integer and floating point kernels.
|
||||
|
||||
In the current version, kernels can only be applied to
|
||||
"L" and "RGB" images.
|
||||
|
||||
:param size: Kernel size, given as (width, height). In the current
|
||||
version, this must be (3,3) or (5,5).
|
||||
:param kernel: A sequence containing kernel weights.
|
||||
:param scale: Scale factor. If given, the result for each pixel is
|
||||
divided by this value. the default is the sum of the
|
||||
kernel weights.
|
||||
:param offset: Offset. If given, this value is added to the result,
|
||||
after it has been divided by the scale factor.
|
||||
"""
|
||||
|
||||
def __init__(self, size, kernel, scale=None, offset=0):
|
||||
if scale is None:
|
||||
# default scale is sum of kernel
|
||||
scale = functools.reduce(lambda a, b: a+b, kernel)
|
||||
if size[0] * size[1] != len(kernel):
|
||||
raise ValueError("not enough coefficients in kernel")
|
||||
self.filterargs = size, scale, offset, kernel
|
||||
|
||||
def filter(self, image):
|
||||
if image.mode == "P":
|
||||
raise ValueError("cannot filter palette images")
|
||||
return image.filter(*self.filterargs)
|
||||
|
||||
|
||||
class BuiltinFilter(Kernel):
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
|
||||
class RankFilter(Filter):
|
||||
"""
|
||||
Create a rank filter. The rank filter sorts all pixels in
|
||||
a window of the given size, and returns the **rank**'th value.
|
||||
|
||||
:param size: The kernel size, in pixels.
|
||||
:param rank: What pixel value to pick. Use 0 for a min filter,
|
||||
``size * size / 2`` for a median filter, ``size * size - 1``
|
||||
for a max filter, etc.
|
||||
"""
|
||||
name = "Rank"
|
||||
|
||||
def __init__(self, size, rank):
|
||||
self.size = size
|
||||
self.rank = rank
|
||||
|
||||
def filter(self, image):
|
||||
if image.mode == "P":
|
||||
raise ValueError("cannot filter palette images")
|
||||
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=3):
|
||||
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=3):
|
||||
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=3):
|
||||
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=3):
|
||||
self.size = size
|
||||
|
||||
def filter(self, image):
|
||||
return image.modefilter(self.size)
|
||||
|
||||
|
||||
class GaussianBlur(Filter):
|
||||
"""Gaussian blur filter.
|
||||
|
||||
:param radius: Blur radius.
|
||||
"""
|
||||
name = "GaussianBlur"
|
||||
|
||||
def __init__(self, radius=2):
|
||||
self.radius = radius
|
||||
|
||||
def filter(self, image):
|
||||
return image.gaussian_blur(self.radius)
|
||||
|
||||
|
||||
class UnsharpMask(Filter):
|
||||
"""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=2, percent=150, threshold=3):
|
||||
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"
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
class CONTOUR(BuiltinFilter):
|
||||
name = "Contour"
|
||||
filterargs = (3, 3), 1, 255, (
|
||||
-1, -1, -1,
|
||||
-1, 8, -1,
|
||||
-1, -1, -1
|
||||
)
|
||||
|
||||
|
||||
class DETAIL(BuiltinFilter):
|
||||
name = "Detail"
|
||||
filterargs = (3, 3), 6, 0, (
|
||||
0, -1, 0,
|
||||
-1, 10, -1,
|
||||
0, -1, 0
|
||||
)
|
||||
|
||||
|
||||
class EDGE_ENHANCE(BuiltinFilter):
|
||||
name = "Edge-enhance"
|
||||
filterargs = (3, 3), 2, 0, (
|
||||
-1, -1, -1,
|
||||
-1, 10, -1,
|
||||
-1, -1, -1
|
||||
)
|
||||
|
||||
|
||||
class EDGE_ENHANCE_MORE(BuiltinFilter):
|
||||
name = "Edge-enhance More"
|
||||
filterargs = (3, 3), 1, 0, (
|
||||
-1, -1, -1,
|
||||
-1, 9, -1,
|
||||
-1, -1, -1
|
||||
)
|
||||
|
||||
|
||||
class EMBOSS(BuiltinFilter):
|
||||
name = "Emboss"
|
||||
filterargs = (3, 3), 1, 128, (
|
||||
-1, 0, 0,
|
||||
0, 1, 0,
|
||||
0, 0, 0
|
||||
)
|
||||
|
||||
|
||||
class FIND_EDGES(BuiltinFilter):
|
||||
name = "Find Edges"
|
||||
filterargs = (3, 3), 1, 0, (
|
||||
-1, -1, -1,
|
||||
-1, 8, -1,
|
||||
-1, -1, -1
|
||||
)
|
||||
|
||||
|
||||
class SMOOTH(BuiltinFilter):
|
||||
name = "Smooth"
|
||||
filterargs = (3, 3), 13, 0, (
|
||||
1, 1, 1,
|
||||
1, 5, 1,
|
||||
1, 1, 1
|
||||
)
|
||||
|
||||
|
||||
class SMOOTH_MORE(BuiltinFilter):
|
||||
name = "Smooth More"
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
class SHARPEN(BuiltinFilter):
|
||||
name = "Sharpen"
|
||||
filterargs = (3, 3), 16, 0, (
|
||||
-2, -2, -2,
|
||||
-2, 32, -2,
|
||||
-2, -2, -2
|
||||
)
|
||||
434
PIL/ImageFont.py
Normal file
434
PIL/ImageFont.py
Normal file
@ -0,0 +1,434 @@
|
||||
#
|
||||
# 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 . import Image
|
||||
from ._util import isDirectory, isPath
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
class _imagingft_not_installed(object):
|
||||
# module placeholder
|
||||
def __getattr__(self, id):
|
||||
raise ImportError("The _imagingft C module is not installed")
|
||||
|
||||
try:
|
||||
from . import _imagingft as core
|
||||
except ImportError:
|
||||
core = _imagingft_not_installed()
|
||||
|
||||
# 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(object):
|
||||
"PIL font wrapper"
|
||||
|
||||
def _load_pilfont(self, filename):
|
||||
|
||||
with open(filename, "rb") as fp:
|
||||
for ext in (".png", ".gif", ".pbm"):
|
||||
try:
|
||||
fullname = os.path.splitext(filename)[0] + ext
|
||||
image = Image.open(fullname)
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
if image and image.mode in ("1", "L"):
|
||||
break
|
||||
else:
|
||||
raise IOError("cannot find glyph data file")
|
||||
|
||||
self.file = fullname
|
||||
|
||||
return self._load_pilfont_data(fp, image)
|
||||
|
||||
def _load_pilfont_data(self, file, image):
|
||||
|
||||
# read PILfont header
|
||||
if file.readline() != b"PILfont\n":
|
||||
raise SyntaxError("Not a PILfont file")
|
||||
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"):
|
||||
raise TypeError("invalid font image mode")
|
||||
|
||||
image.load()
|
||||
|
||||
self.font = Image.core.font(image.im, data)
|
||||
|
||||
# delegate critical operations to internal type
|
||||
self.getsize = self.font.getsize
|
||||
self.getmask = self.font.getmask
|
||||
|
||||
|
||||
##
|
||||
# Wrapper for FreeType fonts. Application code should use the
|
||||
# <b>truetype</b> factory function to create font objects.
|
||||
|
||||
class FreeTypeFont(object):
|
||||
"FreeType font wrapper (requires _imagingft service)"
|
||||
|
||||
def __init__(self, font=None, size=10, index=0, encoding=""):
|
||||
# FIXME: use service provider instead
|
||||
|
||||
self.path = font
|
||||
self.size = size
|
||||
self.index = index
|
||||
self.encoding = encoding
|
||||
|
||||
if isPath(font):
|
||||
self.font = core.getfont(font, size, index, encoding)
|
||||
else:
|
||||
self.font_bytes = font.read()
|
||||
self.font = core.getfont(
|
||||
"", size, index, encoding, self.font_bytes)
|
||||
|
||||
def getname(self):
|
||||
return self.font.family, self.font.style
|
||||
|
||||
def getmetrics(self):
|
||||
return self.font.ascent, self.font.descent
|
||||
|
||||
def getsize(self, text):
|
||||
size, offset = self.font.getsize(text)
|
||||
return (size[0] + offset[0], size[1] + offset[1])
|
||||
|
||||
def getoffset(self, text):
|
||||
return self.font.getsize(text)[1]
|
||||
|
||||
def getmask(self, text, mode=""):
|
||||
return self.getmask2(text, mode)[0]
|
||||
|
||||
def getmask2(self, text, mode="", fill=Image.core.fill):
|
||||
size, offset = self.font.getsize(text)
|
||||
im = fill("L", size, 0)
|
||||
self.font.render(text, im.id, mode == "1")
|
||||
return im, offset
|
||||
|
||||
def font_variant(self, font=None, size=None, index=None, encoding=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.
|
||||
"""
|
||||
return FreeTypeFont(font=self.path if font is None else font,
|
||||
size=self.size if size is None else size,
|
||||
index=self.index if index is None else index,
|
||||
encoding=self.encoding if encoding is None else
|
||||
encoding)
|
||||
|
||||
|
||||
class TransposedFont(object):
|
||||
"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.FLIP_LEFT_RIGHT, Image.FLIP_TOP_BOTTOM,
|
||||
Image.ROTATE_90, Image.ROTATE_180, or Image.ROTATE_270.
|
||||
"""
|
||||
self.font = font
|
||||
self.orientation = orientation # any 'transpose' argument, or None
|
||||
|
||||
def getsize(self, text):
|
||||
w, h = self.font.getsize(text)
|
||||
if self.orientation in (Image.ROTATE_90, Image.ROTATE_270):
|
||||
return h, w
|
||||
return w, h
|
||||
|
||||
def getmask(self, text, mode=""):
|
||||
im = self.font.getmask(text, mode)
|
||||
if self.orientation is not None:
|
||||
return im.transpose(self.orientation)
|
||||
return im
|
||||
|
||||
|
||||
def load(filename):
|
||||
"""
|
||||
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 IOError: If the file could not be read.
|
||||
"""
|
||||
f = ImageFont()
|
||||
f._load_pilfont(filename)
|
||||
return f
|
||||
|
||||
|
||||
def truetype(font=None, size=10, index=0, encoding=""):
|
||||
"""
|
||||
Load a TrueType or OpenType font file, and create a font object.
|
||||
This function loads a font object from the given file, and creates
|
||||
a font object for a font of the given size.
|
||||
|
||||
This function requires the _imagingft service.
|
||||
|
||||
:param font: A truetype font file. Under Windows, if the file
|
||||
is not found in this filename, the loader also looks in
|
||||
Windows :file:`fonts/` directory.
|
||||
:param size: The requested size, in points.
|
||||
:param index: Which font face to load (default is first available face).
|
||||
:param encoding: Which font encoding to use (default is Unicode). Common
|
||||
encodings are "unic" (Unicode), "symb" (Microsoft
|
||||
Symbol), "ADOB" (Adobe Standard), "ADBE" (Adobe Expert),
|
||||
and "armn" (Apple Roman). See the FreeType documentation
|
||||
for more information.
|
||||
:return: A font object.
|
||||
:exception IOError: If the file could not be read.
|
||||
"""
|
||||
|
||||
try:
|
||||
return FreeTypeFont(font, size, index, encoding)
|
||||
except IOError:
|
||||
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:
|
||||
fontpath = os.path.join(walkroot, walkfilename)
|
||||
return FreeTypeFont(fontpath, size, index, encoding)
|
||||
elif not ext and os.path.splitext(walkfilename)[0] == ttf_filename:
|
||||
fontpath = os.path.join(walkroot, walkfilename)
|
||||
if os.path.splitext(fontpath)[1] == '.ttf':
|
||||
return FreeTypeFont(fontpath, size, index, encoding)
|
||||
if not ext and first_font_with_a_different_extension is None:
|
||||
first_font_with_a_different_extension = fontpath
|
||||
if first_font_with_a_different_extension:
|
||||
return FreeTypeFont(first_font_with_a_different_extension, size,
|
||||
index, encoding)
|
||||
raise
|
||||
|
||||
|
||||
def load_path(filename):
|
||||
"""
|
||||
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 IOError: If the file could not be read.
|
||||
"""
|
||||
for directory in sys.path:
|
||||
if isDirectory(directory):
|
||||
if not isinstance(filename, str):
|
||||
if bytes is str:
|
||||
filename = filename.encode("utf-8")
|
||||
else:
|
||||
filename = filename.decode("utf-8")
|
||||
try:
|
||||
return load(os.path.join(directory, filename))
|
||||
except IOError:
|
||||
pass
|
||||
raise IOError("cannot find font file")
|
||||
|
||||
|
||||
def load_default():
|
||||
"""Load a "better than nothing" default font.
|
||||
|
||||
.. versionadded:: 1.1.4
|
||||
|
||||
:return: A font object.
|
||||
"""
|
||||
from io import BytesIO
|
||||
import base64
|
||||
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
|
||||
81
PIL/ImageGrab.py
Normal file
81
PIL/ImageGrab.py
Normal file
@ -0,0 +1,81 @@
|
||||
#
|
||||
# The Python Imaging Library
|
||||
# $Id$
|
||||
#
|
||||
# screen grabber (macOS and Windows only)
|
||||
#
|
||||
# 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 . import Image
|
||||
|
||||
import sys
|
||||
if sys.platform not in ["win32", "darwin"]:
|
||||
raise ImportError("ImageGrab is macOS and Windows only")
|
||||
|
||||
if sys.platform == "win32":
|
||||
grabber = Image.core.grabscreen
|
||||
elif sys.platform == "darwin":
|
||||
import os
|
||||
import tempfile
|
||||
import subprocess
|
||||
|
||||
|
||||
def grab(bbox=None):
|
||||
if sys.platform == "darwin":
|
||||
fh, filepath = tempfile.mkstemp('.png')
|
||||
os.close(fh)
|
||||
subprocess.call(['screencapture', '-x', filepath])
|
||||
im = Image.open(filepath)
|
||||
im.load()
|
||||
os.unlink(filepath)
|
||||
else:
|
||||
size, data = grabber()
|
||||
im = Image.frombytes(
|
||||
"RGB", size, data,
|
||||
# RGB, 32-bit line padding, origin lower left corner
|
||||
"raw", "BGR", (size[0]*3 + 3) & -4, -1
|
||||
)
|
||||
if bbox:
|
||||
im = im.crop(bbox)
|
||||
return im
|
||||
|
||||
|
||||
def grabclipboard():
|
||||
if sys.platform == "darwin":
|
||||
fh, filepath = tempfile.mkstemp('.jpg')
|
||||
os.close(fh)
|
||||
commands = [
|
||||
"set theFile to (open for access POSIX file \""+filepath+"\" with write permission)",
|
||||
"try",
|
||||
"write (the clipboard as JPEG picture) 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
|
||||
else:
|
||||
debug = 0 # temporary interface
|
||||
data = Image.core.grabclipboard(debug)
|
||||
if isinstance(data, bytes):
|
||||
from . import BmpImagePlugin
|
||||
import io
|
||||
return BmpImagePlugin.DibImageFile(io.BytesIO(data))
|
||||
return data
|
||||
269
PIL/ImageMath.py
Normal file
269
PIL/ImageMath.py
Normal file
@ -0,0 +1,269 @@
|
||||
#
|
||||
# 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 . import Image, _imagingmath
|
||||
|
||||
try:
|
||||
import builtins
|
||||
except ImportError:
|
||||
import __builtin__
|
||||
builtins = __builtin__
|
||||
|
||||
VERBOSE = 0
|
||||
|
||||
|
||||
def _isconstant(v):
|
||||
return isinstance(v, int) or isinstance(v, float)
|
||||
|
||||
|
||||
class _Operand(object):
|
||||
"""Wraps an image operand, providing standard operators"""
|
||||
|
||||
def __init__(self, im):
|
||||
self.im = im
|
||||
|
||||
def __fixup(self, im1):
|
||||
# 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:
|
||||
raise ValueError("unsupported mode: %s" % im1.im.mode)
|
||||
else:
|
||||
# argument was a constant
|
||||
if _isconstant(im1) 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, im1, im2=None, mode=None):
|
||||
im1 = self.__fixup(im1)
|
||||
if im2 is None:
|
||||
# unary operation
|
||||
out = Image.new(mode or im1.mode, im1.size, None)
|
||||
im1.load()
|
||||
try:
|
||||
op = getattr(_imagingmath, op+"_"+im1.mode)
|
||||
except AttributeError:
|
||||
raise TypeError("bad operand type for '%s'" % op)
|
||||
_imagingmath.unop(op, out.im.id, im1.im.id)
|
||||
else:
|
||||
# binary operation
|
||||
im2 = self.__fixup(im2)
|
||||
if im1.mode != im2.mode:
|
||||
# convert both arguments to floating point
|
||||
if im1.mode != "F":
|
||||
im1 = im1.convert("F")
|
||||
if im2.mode != "F":
|
||||
im2 = im2.convert("F")
|
||||
if im1.mode != im2.mode:
|
||||
raise ValueError("mode mismatch")
|
||||
if im1.size != im2.size:
|
||||
# crop both arguments to a common size
|
||||
size = (min(im1.size[0], im2.size[0]),
|
||||
min(im1.size[1], im2.size[1]))
|
||||
if im1.size != size:
|
||||
im1 = im1.crop((0, 0) + size)
|
||||
if im2.size != size:
|
||||
im2 = im2.crop((0, 0) + size)
|
||||
out = Image.new(mode or im1.mode, size, None)
|
||||
else:
|
||||
out = Image.new(mode or im1.mode, im1.size, None)
|
||||
im1.load()
|
||||
im2.load()
|
||||
try:
|
||||
op = getattr(_imagingmath, op+"_"+im1.mode)
|
||||
except AttributeError:
|
||||
raise TypeError("bad operand type for '%s'" % op)
|
||||
_imagingmath.binop(op, out.im.id, im1.im.id, im2.im.id)
|
||||
return _Operand(out)
|
||||
|
||||
# unary operators
|
||||
def __bool__(self):
|
||||
# an image is "true" if it contains at least one non-zero pixel
|
||||
return self.im.getbbox() is not None
|
||||
|
||||
if bytes is str:
|
||||
# Provide __nonzero__ for pre-Py3k
|
||||
__nonzero__ = __bool__
|
||||
del __bool__
|
||||
|
||||
def __abs__(self):
|
||||
return self.apply("abs", self)
|
||||
|
||||
def __pos__(self):
|
||||
return self
|
||||
|
||||
def __neg__(self):
|
||||
return self.apply("neg", self)
|
||||
|
||||
# binary operators
|
||||
def __add__(self, other):
|
||||
return self.apply("add", self, other)
|
||||
|
||||
def __radd__(self, other):
|
||||
return self.apply("add", other, self)
|
||||
|
||||
def __sub__(self, other):
|
||||
return self.apply("sub", self, other)
|
||||
|
||||
def __rsub__(self, other):
|
||||
return self.apply("sub", other, self)
|
||||
|
||||
def __mul__(self, other):
|
||||
return self.apply("mul", self, other)
|
||||
|
||||
def __rmul__(self, other):
|
||||
return self.apply("mul", other, self)
|
||||
|
||||
def __truediv__(self, other):
|
||||
return self.apply("div", self, other)
|
||||
|
||||
def __rtruediv__(self, other):
|
||||
return self.apply("div", other, self)
|
||||
|
||||
def __mod__(self, other):
|
||||
return self.apply("mod", self, other)
|
||||
|
||||
def __rmod__(self, other):
|
||||
return self.apply("mod", other, self)
|
||||
|
||||
def __pow__(self, other):
|
||||
return self.apply("pow", self, other)
|
||||
|
||||
def __rpow__(self, other):
|
||||
return self.apply("pow", other, self)
|
||||
|
||||
if bytes is str:
|
||||
# Provide __div__ and __rdiv__ for pre-Py3k
|
||||
__div__ = __truediv__
|
||||
__rdiv__ = __rtruediv__
|
||||
del __truediv__
|
||||
del __rtruediv__
|
||||
|
||||
# bitwise
|
||||
def __invert__(self):
|
||||
return self.apply("invert", self)
|
||||
|
||||
def __and__(self, other):
|
||||
return self.apply("and", self, other)
|
||||
|
||||
def __rand__(self, other):
|
||||
return self.apply("and", other, self)
|
||||
|
||||
def __or__(self, other):
|
||||
return self.apply("or", self, other)
|
||||
|
||||
def __ror__(self, other):
|
||||
return self.apply("or", other, self)
|
||||
|
||||
def __xor__(self, other):
|
||||
return self.apply("xor", self, other)
|
||||
|
||||
def __rxor__(self, other):
|
||||
return self.apply("xor", other, self)
|
||||
|
||||
def __lshift__(self, other):
|
||||
return self.apply("lshift", self, other)
|
||||
|
||||
def __rshift__(self, other):
|
||||
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):
|
||||
return self.apply("lt", self, other)
|
||||
|
||||
def __le__(self, other):
|
||||
return self.apply("le", self, other)
|
||||
|
||||
def __gt__(self, other):
|
||||
return self.apply("gt", self, other)
|
||||
|
||||
def __ge__(self, other):
|
||||
return self.apply("ge", self, other)
|
||||
|
||||
|
||||
# conversions
|
||||
def imagemath_int(self):
|
||||
return _Operand(self.im.convert("I"))
|
||||
|
||||
|
||||
def imagemath_float(self):
|
||||
return _Operand(self.im.convert("F"))
|
||||
|
||||
|
||||
# logical
|
||||
def imagemath_equal(self, other):
|
||||
return self.apply("eq", self, other, mode="I")
|
||||
|
||||
|
||||
def imagemath_notequal(self, other):
|
||||
return self.apply("ne", self, other, mode="I")
|
||||
|
||||
|
||||
def imagemath_min(self, other):
|
||||
return self.apply("min", self, other)
|
||||
|
||||
|
||||
def imagemath_max(self, other):
|
||||
return self.apply("max", self, other)
|
||||
|
||||
|
||||
def imagemath_convert(self, mode):
|
||||
return _Operand(self.im.convert(mode))
|
||||
|
||||
ops = {}
|
||||
for k, v in list(globals().items()):
|
||||
if k[:10] == "imagemath_":
|
||||
ops[k[10:]] = v
|
||||
|
||||
|
||||
def eval(expression, _dict={}, **kw):
|
||||
"""
|
||||
Evaluates an image expression.
|
||||
|
||||
: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 = ops.copy()
|
||||
args.update(_dict)
|
||||
args.update(kw)
|
||||
for k, v in list(args.items()):
|
||||
if hasattr(v, "im"):
|
||||
args[k] = _Operand(v)
|
||||
|
||||
out = builtins.eval(expression, args)
|
||||
try:
|
||||
return out.im
|
||||
except AttributeError:
|
||||
return out
|
||||
55
PIL/ImageMode.py
Normal file
55
PIL/ImageMode.py
Normal file
@ -0,0 +1,55 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
# mode descriptor cache
|
||||
_modes = None
|
||||
|
||||
|
||||
class ModeDescriptor(object):
|
||||
"""Wrapper for mode strings."""
|
||||
|
||||
def __init__(self, mode, bands, basemode, basetype):
|
||||
self.mode = mode
|
||||
self.bands = bands
|
||||
self.basemode = basemode
|
||||
self.basetype = basetype
|
||||
|
||||
def __str__(self):
|
||||
return self.mode
|
||||
|
||||
|
||||
def getmode(mode):
|
||||
"""Gets a mode descriptor for the given mode."""
|
||||
global _modes
|
||||
if not _modes:
|
||||
# initialize mode cache
|
||||
|
||||
from . import Image
|
||||
modes = {}
|
||||
# core modes
|
||||
for m, (basemode, basetype, bands) in Image._MODEINFO.items():
|
||||
modes[m] = ModeDescriptor(m, bands, basemode, basetype)
|
||||
# extra experimental modes
|
||||
modes["RGBa"] = ModeDescriptor("RGBa", ("R", "G", "B", "a"), "RGB", "L")
|
||||
modes["LA"] = ModeDescriptor("LA", ("L", "A"), "L", "L")
|
||||
modes["La"] = ModeDescriptor("La", ("L", "a"), "L", "L")
|
||||
modes["PA"] = ModeDescriptor("PA", ("P", "A"), "RGB", "L")
|
||||
# mapping modes
|
||||
modes["I;16"] = ModeDescriptor("I;16", "I", "L", "L")
|
||||
modes["I;16L"] = ModeDescriptor("I;16L", "I", "L", "L")
|
||||
modes["I;16B"] = ModeDescriptor("I;16B", "I", "L", "L")
|
||||
# set global mode cache atomically
|
||||
_modes = modes
|
||||
return _modes[mode]
|
||||
250
PIL/ImageMorph.py
Normal file
250
PIL/ImageMorph.py
Normal file
@ -0,0 +1,250 @@
|
||||
# A binary morphology add-on for the Python Imaging Library
|
||||
#
|
||||
# History:
|
||||
# 2014-06-04 Initial version.
|
||||
#
|
||||
# Copyright (c) 2014 Dov Grobgeld <dov.grobgeld@gmail.com>
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
from . import Image, _imagingmorph
|
||||
import re
|
||||
|
||||
LUT_SIZE = 1 << 9
|
||||
|
||||
|
||||
class LutBuilder(object):
|
||||
"""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=None, op_name=None):
|
||||
if patterns is not None:
|
||||
self.patterns = patterns
|
||||
else:
|
||||
self.patterns = []
|
||||
self.lut = None
|
||||
if op_name is not None:
|
||||
known_patterns = {
|
||||
'corner': ['1:(... ... ...)->0',
|
||||
'4:(00. 01. ...)->1'],
|
||||
'dilation4': ['4:(... .0. .1.)->1'],
|
||||
'dilation8': ['4:(... .0. .1.)->1',
|
||||
'4:(... .0. ..1)->1'],
|
||||
'erosion4': ['4:(... .1. .0.)->0'],
|
||||
'erosion8': ['4:(... .1. .0.)->0',
|
||||
'4:(... .1. ..0)->0'],
|
||||
'edge': ['1:(... ... ...)->0',
|
||||
'4:(.0. .1. ...)->1',
|
||||
'4:(01. .1. ...)->1']
|
||||
}
|
||||
if op_name not in known_patterns:
|
||||
raise Exception('Unknown pattern '+op_name+'!')
|
||||
|
||||
self.patterns = known_patterns[op_name]
|
||||
|
||||
def add_patterns(self, patterns):
|
||||
self.patterns += patterns
|
||||
|
||||
def build_default_lut(self):
|
||||
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):
|
||||
return self.lut
|
||||
|
||||
def _string_permute(self, pattern, permutation):
|
||||
"""string_permute takes a pattern and a permutation and returns the
|
||||
string permuted according to the permutation list.
|
||||
"""
|
||||
assert(len(permutation) == 9)
|
||||
return ''.join(pattern[p] for p in permutation)
|
||||
|
||||
def _pattern_permute(self, basic_pattern, options, basic_result):
|
||||
"""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], [6, 3, 0,
|
||||
7, 4, 1,
|
||||
8, 5, 2]), res))
|
||||
# mirror
|
||||
if 'M' in options:
|
||||
n = len(patterns)
|
||||
for pattern, res in patterns[0:n]:
|
||||
patterns.append(
|
||||
(self._string_permute(pattern, [2, 1, 0,
|
||||
5, 4, 3,
|
||||
8, 7, 6]), res))
|
||||
|
||||
# negate
|
||||
if 'N' in options:
|
||||
n = len(patterns)
|
||||
for pattern, res in patterns[0:n]:
|
||||
# Swap 0 and 1
|
||||
pattern = (pattern
|
||||
.replace('0', 'Z')
|
||||
.replace('1', '0')
|
||||
.replace('Z', '1'))
|
||||
res = '%d' % (1-int(res))
|
||||
patterns.append((pattern, res))
|
||||
|
||||
return patterns
|
||||
|
||||
def build_lut(self):
|
||||
"""Compile all patterns into a morphology lut.
|
||||
|
||||
TBD :Build based on (file) morphlut:modify_lut
|
||||
"""
|
||||
self.build_default_lut()
|
||||
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:
|
||||
raise Exception('Syntax error in pattern "'+p+'"')
|
||||
options = m.group(1)
|
||||
pattern = m.group(2)
|
||||
result = int(m.group(3))
|
||||
|
||||
# Get rid of spaces
|
||||
pattern = pattern.replace(' ', '').replace('\n', '')
|
||||
|
||||
patterns += self._pattern_permute(pattern, options, result)
|
||||
|
||||
# # Debugging
|
||||
# for p,r in patterns:
|
||||
# print(p,r)
|
||||
# print('--')
|
||||
|
||||
# compile the patterns into regular expressions for speed
|
||||
for i, pattern in enumerate(patterns):
|
||||
p = pattern[0].replace('.', 'X').replace('X', '[01]')
|
||||
p = re.compile(p)
|
||||
patterns[i] = (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 p, r in patterns:
|
||||
if p.match(bitpattern):
|
||||
self.lut[i] = [0, 1][r]
|
||||
|
||||
return self.lut
|
||||
|
||||
|
||||
class MorphOp(object):
|
||||
"""A class for binary morphological operators"""
|
||||
|
||||
def __init__(self,
|
||||
lut=None,
|
||||
op_name=None,
|
||||
patterns=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):
|
||||
"""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:
|
||||
raise Exception('No operator loaded')
|
||||
|
||||
if image.mode != 'L':
|
||||
raise Exception('Image must be binary, meaning it must use mode L')
|
||||
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):
|
||||
"""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."""
|
||||
if self.lut is None:
|
||||
raise Exception('No operator loaded')
|
||||
|
||||
if image.mode != 'L':
|
||||
raise Exception('Image must be binary, meaning it must use mode L')
|
||||
return _imagingmorph.match(bytes(self.lut), image.im.id)
|
||||
|
||||
def get_on_pixels(self, image):
|
||||
"""Get a list of all turned on pixels in a binary image
|
||||
|
||||
Returns a list of tuples of (x,y) coordinates
|
||||
of all matching pixels."""
|
||||
|
||||
if image.mode != 'L':
|
||||
raise Exception('Image must be binary, meaning it must use mode L')
|
||||
return _imagingmorph.get_on_pixels(image.im.id)
|
||||
|
||||
def load_lut(self, filename):
|
||||
"""Load an operator from an mrl file"""
|
||||
with open(filename, 'rb') as f:
|
||||
self.lut = bytearray(f.read())
|
||||
|
||||
if len(self.lut) != 8192:
|
||||
self.lut = None
|
||||
raise Exception('Wrong size operator file!')
|
||||
|
||||
def save_lut(self, filename):
|
||||
"""Save an operator to an mrl file"""
|
||||
if self.lut is None:
|
||||
raise Exception('No operator loaded')
|
||||
with open(filename, 'wb') as f:
|
||||
f.write(self.lut)
|
||||
|
||||
def set_lut(self, lut):
|
||||
"""Set the lut from an external source"""
|
||||
self.lut = lut
|
||||
483
PIL/ImageOps.py
Normal file
483
PIL/ImageOps.py
Normal file
@ -0,0 +1,483 @@
|
||||
#
|
||||
# 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 . import Image
|
||||
from ._util import isStringType
|
||||
import operator
|
||||
import functools
|
||||
|
||||
|
||||
#
|
||||
# helpers
|
||||
|
||||
def _border(border):
|
||||
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, mode):
|
||||
if isStringType(color):
|
||||
from . import ImageColor
|
||||
color = ImageColor.getcolor(color, mode)
|
||||
return color
|
||||
|
||||
|
||||
def _lut(image, lut):
|
||||
if image.mode == "P":
|
||||
# FIXME: apply to lookup table, not image data
|
||||
raise NotImplementedError("mode P support coming soon")
|
||||
elif image.mode in ("L", "RGB"):
|
||||
if image.mode == "RGB" and len(lut) == 256:
|
||||
lut = lut + lut + lut
|
||||
return image.point(lut)
|
||||
else:
|
||||
raise IOError("not supported for this image mode")
|
||||
|
||||
#
|
||||
# actions
|
||||
|
||||
|
||||
def autocontrast(image, cutoff=0, ignore=None):
|
||||
"""
|
||||
Maximize (normalize) image contrast. This function calculates a
|
||||
histogram of the input image, removes **cutoff** percent of the
|
||||
lightest and darkest pixels from the histogram, and remaps the image
|
||||
so that the darkest pixel becomes black (0), and the lightest
|
||||
becomes white (255).
|
||||
|
||||
:param image: The image to process.
|
||||
:param cutoff: How many percent to cut off from the histogram.
|
||||
:param ignore: The background pixel value (use None for no background).
|
||||
:return: An image.
|
||||
"""
|
||||
histogram = image.histogram()
|
||||
lut = []
|
||||
for layer in range(0, len(histogram), 256):
|
||||
h = histogram[layer:layer+256]
|
||||
if ignore is not None:
|
||||
# get rid of outliers
|
||||
try:
|
||||
h[ignore] = 0
|
||||
except TypeError:
|
||||
# assume sequence
|
||||
for ix in ignore:
|
||||
h[ix] = 0
|
||||
if cutoff:
|
||||
# cut off pixels from both ends of the histogram
|
||||
# get number of pixels
|
||||
n = 0
|
||||
for ix in range(256):
|
||||
n = n + h[ix]
|
||||
# remove cutoff% pixels from the low end
|
||||
cut = n * cutoff // 100
|
||||
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 hi end
|
||||
cut = n * cutoff // 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, black, white):
|
||||
"""
|
||||
Colorize grayscale image. The **black** and **white**
|
||||
arguments should be RGB tuples; this function calculates a color
|
||||
wedge mapping all black pixels in the source image to the first
|
||||
color, and all white pixels to the second color.
|
||||
|
||||
: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.
|
||||
:return: An image.
|
||||
"""
|
||||
assert image.mode == "L"
|
||||
black = _color(black, "RGB")
|
||||
white = _color(white, "RGB")
|
||||
red = []
|
||||
green = []
|
||||
blue = []
|
||||
for i in range(256):
|
||||
red.append(black[0]+i*(white[0]-black[0])//255)
|
||||
green.append(black[1]+i*(white[1]-black[1])//255)
|
||||
blue.append(black[2]+i*(white[2]-black[2])//255)
|
||||
image = image.convert("RGB")
|
||||
return _lut(image, red + green + blue)
|
||||
|
||||
|
||||
def crop(image, border=0):
|
||||
"""
|
||||
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, factor, resample=Image.NEAREST):
|
||||
"""
|
||||
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 factor: The expansion factor, as a float.
|
||||
:param resample: An optional resampling filter. Same values possible as
|
||||
in the PIL.Image.resize function.
|
||||
:returns: An :py:class:`~PIL.Image.Image` object.
|
||||
"""
|
||||
if factor == 1:
|
||||
return image.copy()
|
||||
elif factor <= 0:
|
||||
raise ValueError("the factor must be greater than 0")
|
||||
else:
|
||||
size = (int(round(factor * image.width)),
|
||||
int(round(factor * image.height)))
|
||||
return image.resize(size, resample)
|
||||
|
||||
|
||||
def deform(image, deformer, resample=Image.BILINEAR):
|
||||
"""
|
||||
Deform the image.
|
||||
|
||||
:param image: The image to deform.
|
||||
:param deformer: A deformer object. Any object that implements a
|
||||
**getmesh** method can be used.
|
||||
:param resample: An optional resampling filter. Same values possible as
|
||||
in the PIL.Image.transform function.
|
||||
:return: An image.
|
||||
"""
|
||||
return image.transform(
|
||||
image.size, Image.MESH, deformer.getmesh(image), resample
|
||||
)
|
||||
|
||||
|
||||
def equalize(image, mask=None):
|
||||
"""
|
||||
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, border=0, fill=0):
|
||||
"""
|
||||
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
|
||||
out = Image.new(image.mode, (width, height), _color(fill, image.mode))
|
||||
out.paste(image, (left, top))
|
||||
return out
|
||||
|
||||
|
||||
def fit(image, size, method=Image.NEAREST, bleed=0.0, centering=(0.5, 0.5)):
|
||||
"""
|
||||
Returns a sized and cropped version of the image, cropped to the
|
||||
requested aspect ratio and size.
|
||||
|
||||
This function was contributed by Kevin Cazabon.
|
||||
|
||||
:param size: The requested output size in pixels, given as a
|
||||
(width, height) tuple.
|
||||
:param method: What resampling method to use. Default is
|
||||
:py:attr:`PIL.Image.NEAREST`.
|
||||
:param bleed: Remove a border around the outside of the image (from all
|
||||
four edges. The value is a decimal percentage (use 0.01 for
|
||||
one percent). The default value is 0 (no border).
|
||||
: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
|
||||
# http://www.cazabon.com
|
||||
|
||||
# ensure inputs are valid
|
||||
if not isinstance(centering, list):
|
||||
centering = [centering[0], centering[1]]
|
||||
|
||||
if centering[0] > 1.0 or centering[0] < 0.0:
|
||||
centering[0] = 0.50
|
||||
if centering[1] > 1.0 or centering[1] < 0.0:
|
||||
centering[1] = 0.50
|
||||
|
||||
if bleed > 0.49999 or bleed < 0.0:
|
||||
bleed = 0.0
|
||||
|
||||
# calculate the area to use for resizing and cropping, subtracting
|
||||
# the 'bleed' around the edges
|
||||
|
||||
# number of pixels to trim off on Top and Bottom, Left and Right
|
||||
bleedPixels = (
|
||||
int((float(bleed) * float(image.size[0])) + 0.5),
|
||||
int((float(bleed) * float(image.size[1])) + 0.5)
|
||||
)
|
||||
|
||||
liveArea = (0, 0, image.size[0], image.size[1])
|
||||
if bleed > 0.0:
|
||||
liveArea = (
|
||||
bleedPixels[0], bleedPixels[1], image.size[0] - bleedPixels[0] - 1,
|
||||
image.size[1] - bleedPixels[1] - 1
|
||||
)
|
||||
|
||||
liveSize = (liveArea[2] - liveArea[0], liveArea[3] - liveArea[1])
|
||||
|
||||
# calculate the aspect ratio of the liveArea
|
||||
liveAreaAspectRatio = float(liveSize[0])/float(liveSize[1])
|
||||
|
||||
# calculate the aspect ratio of the output image
|
||||
aspectRatio = float(size[0]) / float(size[1])
|
||||
|
||||
# figure out if the sides or top/bottom will be cropped off
|
||||
if liveAreaAspectRatio >= aspectRatio:
|
||||
# liveArea is wider than what's needed, crop the sides
|
||||
cropWidth = int((aspectRatio * float(liveSize[1])) + 0.5)
|
||||
cropHeight = liveSize[1]
|
||||
else:
|
||||
# liveArea is taller than what's needed, crop the top and bottom
|
||||
cropWidth = liveSize[0]
|
||||
cropHeight = int((float(liveSize[0])/aspectRatio) + 0.5)
|
||||
|
||||
# make the crop
|
||||
leftSide = int(liveArea[0] + (float(liveSize[0]-cropWidth) * centering[0]))
|
||||
if leftSide < 0:
|
||||
leftSide = 0
|
||||
topSide = int(liveArea[1] + (float(liveSize[1]-cropHeight) * centering[1]))
|
||||
if topSide < 0:
|
||||
topSide = 0
|
||||
|
||||
out = image.crop(
|
||||
(leftSide, topSide, leftSide + cropWidth, topSide + cropHeight)
|
||||
)
|
||||
|
||||
# resize the image and return it
|
||||
return out.resize(size, method)
|
||||
|
||||
|
||||
def flip(image):
|
||||
"""
|
||||
Flip the image vertically (top to bottom).
|
||||
|
||||
:param image: The image to flip.
|
||||
:return: An image.
|
||||
"""
|
||||
return image.transpose(Image.FLIP_TOP_BOTTOM)
|
||||
|
||||
|
||||
def grayscale(image):
|
||||
"""
|
||||
Convert the image to grayscale.
|
||||
|
||||
:param image: The image to convert.
|
||||
:return: An image.
|
||||
"""
|
||||
return image.convert("L")
|
||||
|
||||
|
||||
def invert(image):
|
||||
"""
|
||||
Invert (negate) the image.
|
||||
|
||||
:param image: The image to invert.
|
||||
:return: An image.
|
||||
"""
|
||||
lut = []
|
||||
for i in range(256):
|
||||
lut.append(255-i)
|
||||
return _lut(image, lut)
|
||||
|
||||
|
||||
def mirror(image):
|
||||
"""
|
||||
Flip image horizontally (left to right).
|
||||
|
||||
:param image: The image to mirror.
|
||||
:return: An image.
|
||||
"""
|
||||
return image.transpose(Image.FLIP_LEFT_RIGHT)
|
||||
|
||||
|
||||
def posterize(image, bits):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
lut = []
|
||||
mask = ~(2**(8-bits)-1)
|
||||
for i in range(256):
|
||||
lut.append(i & mask)
|
||||
return _lut(image, lut)
|
||||
|
||||
|
||||
def solarize(image, threshold=128):
|
||||
"""
|
||||
Invert all pixel values above a threshold.
|
||||
|
||||
:param image: The image to solarize.
|
||||
:param threshold: All pixels above this greyscale 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)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# PIL USM components, from Kevin Cazabon.
|
||||
|
||||
def gaussian_blur(im, radius=None):
|
||||
""" PIL_usm.gblur(im, [radius])"""
|
||||
|
||||
if radius is None:
|
||||
radius = 5.0
|
||||
|
||||
im.load()
|
||||
|
||||
return im.im.gaussian_blur(radius)
|
||||
|
||||
gblur = gaussian_blur
|
||||
|
||||
|
||||
def unsharp_mask(im, radius=None, percent=None, threshold=None):
|
||||
""" PIL_usm.usm(im, [radius, percent, threshold])"""
|
||||
|
||||
if radius is None:
|
||||
radius = 5.0
|
||||
if percent is None:
|
||||
percent = 150
|
||||
if threshold is None:
|
||||
threshold = 3
|
||||
|
||||
im.load()
|
||||
|
||||
return im.im.unsharp_mask(radius, percent, threshold)
|
||||
|
||||
usm = unsharp_mask
|
||||
|
||||
|
||||
def box_blur(image, radius):
|
||||
"""
|
||||
Blur the image by setting each pixel to the average value of the pixels
|
||||
in a square box extending radius pixels in each direction.
|
||||
Supports float radius of arbitrary size. Uses an optimized implementation
|
||||
which runs in linear time relative to the size of the image
|
||||
for any radius value.
|
||||
|
||||
:param image: The image to blur.
|
||||
:param radius: Size of the box in one direction. Radius 0 does not blur,
|
||||
returns an identical image. Radius 1 takes 1 pixel
|
||||
in each direction, i.e. 9 pixels in total.
|
||||
:return: An image.
|
||||
"""
|
||||
image.load()
|
||||
|
||||
return image._new(image.im.box_blur(radius))
|
||||
216
PIL/ImagePalette.py
Normal file
216
PIL/ImagePalette.py
Normal file
@ -0,0 +1,216 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
import array
|
||||
from . import ImageColor, GimpPaletteFile, GimpGradientFile, PaletteFile
|
||||
|
||||
|
||||
class ImagePalette(object):
|
||||
"""
|
||||
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 and of length ``size``
|
||||
times the number of colors in ``mode``. The list must be aligned
|
||||
by channel (All R values must be contiguous in the list before G
|
||||
and B values.) Defaults to 0 through 255 per channel.
|
||||
:param size: An optional palette size. If given, it cannot be equal to
|
||||
or greater than 256. Defaults to 0.
|
||||
"""
|
||||
|
||||
def __init__(self, mode="RGB", palette=None, size=0):
|
||||
self.mode = mode
|
||||
self.rawmode = None # if set, palette contains raw data
|
||||
self.palette = palette or bytearray(range(256))*len(self.mode)
|
||||
self.colors = {}
|
||||
self.dirty = None
|
||||
if ((size == 0 and len(self.mode)*256 != len(self.palette)) or
|
||||
(size != 0 and size != len(self.palette))):
|
||||
raise ValueError("wrong palette size")
|
||||
|
||||
def copy(self):
|
||||
new = ImagePalette()
|
||||
|
||||
new.mode = self.mode
|
||||
new.rawmode = self.rawmode
|
||||
if self.palette is not None:
|
||||
new.palette = self.palette[:]
|
||||
new.colors = self.colors.copy()
|
||||
new.dirty = self.dirty
|
||||
|
||||
return new
|
||||
|
||||
def getdata(self):
|
||||
"""
|
||||
Get palette contents in format suitable # for the low-level
|
||||
``im.putpalette`` primitive.
|
||||
|
||||
.. warning:: This method is experimental.
|
||||
"""
|
||||
if self.rawmode:
|
||||
return self.rawmode, self.palette
|
||||
return self.mode + ";L", self.tobytes()
|
||||
|
||||
def tobytes(self):
|
||||
"""Convert palette to bytes.
|
||||
|
||||
.. warning:: This method is experimental.
|
||||
"""
|
||||
if self.rawmode:
|
||||
raise ValueError("palette contains raw palette data")
|
||||
if isinstance(self.palette, bytes):
|
||||
return self.palette
|
||||
arr = array.array("B", self.palette)
|
||||
if hasattr(arr, 'tobytes'):
|
||||
return arr.tobytes()
|
||||
return arr.tostring()
|
||||
|
||||
# Declare tostring as an alias for tobytes
|
||||
tostring = tobytes
|
||||
|
||||
def getcolor(self, color):
|
||||
"""Given an rgb tuple, allocate palette entry.
|
||||
|
||||
.. warning:: This method is experimental.
|
||||
"""
|
||||
if self.rawmode:
|
||||
raise ValueError("palette contains raw palette data")
|
||||
if isinstance(color, tuple):
|
||||
try:
|
||||
return self.colors[color]
|
||||
except KeyError:
|
||||
# allocate new color slot
|
||||
if isinstance(self.palette, bytes):
|
||||
self.palette = bytearray(self.palette)
|
||||
index = len(self.colors)
|
||||
if index >= 256:
|
||||
raise ValueError("cannot allocate more than 256 colors")
|
||||
self.colors[color] = index
|
||||
self.palette[index] = color[0]
|
||||
self.palette[index+256] = color[1]
|
||||
self.palette[index+512] = color[2]
|
||||
self.dirty = 1
|
||||
return index
|
||||
else:
|
||||
raise ValueError("unknown color specifier: %r" % color)
|
||||
|
||||
def save(self, fp):
|
||||
"""Save palette to text file.
|
||||
|
||||
.. warning:: This method is experimental.
|
||||
"""
|
||||
if self.rawmode:
|
||||
raise ValueError("palette contains raw palette data")
|
||||
if isinstance(fp, str):
|
||||
fp = open(fp, "w")
|
||||
fp.write("# Palette\n")
|
||||
fp.write("# Mode: %s\n" % self.mode)
|
||||
for i in range(256):
|
||||
fp.write("%d" % i)
|
||||
for j in range(i*len(self.mode), (i+1)*len(self.mode)):
|
||||
try:
|
||||
fp.write(" %d" % self.palette[j])
|
||||
except IndexError:
|
||||
fp.write(" 0")
|
||||
fp.write("\n")
|
||||
fp.close()
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Internal
|
||||
|
||||
def raw(rawmode, data):
|
||||
palette = ImagePalette()
|
||||
palette.rawmode = rawmode
|
||||
palette.palette = data
|
||||
palette.dirty = 1
|
||||
return palette
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Factories
|
||||
|
||||
def make_linear_lut(black, white):
|
||||
lut = []
|
||||
if black == 0:
|
||||
for i in range(256):
|
||||
lut.append(white*i//255)
|
||||
else:
|
||||
raise NotImplementedError # FIXME
|
||||
return lut
|
||||
|
||||
|
||||
def make_gamma_lut(exp):
|
||||
lut = []
|
||||
for i in range(256):
|
||||
lut.append(int(((i / 255.0) ** exp) * 255.0 + 0.5))
|
||||
return lut
|
||||
|
||||
|
||||
def negative(mode="RGB"):
|
||||
palette = list(range(256))
|
||||
palette.reverse()
|
||||
return ImagePalette(mode, palette * len(mode))
|
||||
|
||||
|
||||
def random(mode="RGB"):
|
||||
from random import randint
|
||||
palette = []
|
||||
for i in range(256*len(mode)):
|
||||
palette.append(randint(0, 255))
|
||||
return ImagePalette(mode, palette)
|
||||
|
||||
|
||||
def sepia(white="#fff0c0"):
|
||||
r, g, b = ImageColor.getrgb(white)
|
||||
r = make_linear_lut(0, r)
|
||||
g = make_linear_lut(0, g)
|
||||
b = make_linear_lut(0, b)
|
||||
return ImagePalette("RGB", r + g + b)
|
||||
|
||||
|
||||
def wedge(mode="RGB"):
|
||||
return ImagePalette(mode, list(range(256)) * len(mode))
|
||||
|
||||
|
||||
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):
|
||||
# import traceback
|
||||
# traceback.print_exc()
|
||||
pass
|
||||
else:
|
||||
raise IOError("cannot load palette")
|
||||
|
||||
return lut # data, rawmode
|
||||
60
PIL/ImagePath.py
Normal file
60
PIL/ImagePath.py
Normal file
@ -0,0 +1,60 @@
|
||||
#
|
||||
# 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 . import Image
|
||||
|
||||
|
||||
# the Python class below is overridden by the C implementation.
|
||||
|
||||
|
||||
class Path(object):
|
||||
|
||||
def __init__(self, xy):
|
||||
pass
|
||||
|
||||
def compact(self, distance=2):
|
||||
"""
|
||||
Compacts the path, by removing points that are close to each other.
|
||||
This method modifies the path in place.
|
||||
"""
|
||||
pass
|
||||
|
||||
def getbbox(self):
|
||||
"""Gets the bounding box."""
|
||||
pass
|
||||
|
||||
def map(self, function):
|
||||
"""Maps the path through a function."""
|
||||
pass
|
||||
|
||||
def tolist(self, flat=0):
|
||||
"""
|
||||
Converts the path to Python list.
|
||||
#
|
||||
@param flat By default, this function returns a list of 2-tuples
|
||||
[(x, y), ...]. If this argument is true, it returns a flat list
|
||||
[x, y, ...] instead.
|
||||
@return A list of coordinates.
|
||||
"""
|
||||
pass
|
||||
|
||||
def transform(self, matrix):
|
||||
"""Transforms the path."""
|
||||
pass
|
||||
|
||||
|
||||
# override with C implementation
|
||||
Path = Image.core.path
|
||||
203
PIL/ImageQt.py
Normal file
203
PIL/ImageQt.py
Normal file
@ -0,0 +1,203 @@
|
||||
#
|
||||
# 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 . import Image
|
||||
from ._util import isPath
|
||||
from io import BytesIO
|
||||
|
||||
qt_is_installed = True
|
||||
qt_version = None
|
||||
try:
|
||||
from PyQt5.QtGui import QImage, qRgba, QPixmap
|
||||
from PyQt5.QtCore import QBuffer, QIODevice
|
||||
qt_version = '5'
|
||||
except (ImportError, RuntimeError):
|
||||
try:
|
||||
from PyQt4.QtGui import QImage, qRgba, QPixmap
|
||||
from PyQt4.QtCore import QBuffer, QIODevice
|
||||
qt_version = '4'
|
||||
except (ImportError, RuntimeError):
|
||||
try:
|
||||
from PySide.QtGui import QImage, qRgba, QPixmap
|
||||
from PySide.QtCore import QBuffer, QIODevice
|
||||
qt_version = 'side'
|
||||
except ImportError:
|
||||
qt_is_installed = False
|
||||
|
||||
|
||||
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: A PIL Image object, or a file name
|
||||
(given either as Python string or a PyQt string object)
|
||||
"""
|
||||
buffer = QBuffer()
|
||||
buffer.open(QIODevice.ReadWrite)
|
||||
# preserve alha 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()
|
||||
try:
|
||||
b.write(buffer.data())
|
||||
except TypeError:
|
||||
# workaround for Python 2
|
||||
b.write(str(buffer.data()))
|
||||
buffer.close()
|
||||
b.seek(0)
|
||||
|
||||
return Image.open(b)
|
||||
|
||||
|
||||
def fromqpixmap(im):
|
||||
return fromqimage(im)
|
||||
# buffer = QBuffer()
|
||||
# buffer.open(QIODevice.ReadWrite)
|
||||
# # im.save(buffer)
|
||||
# # What if png doesn't support some image features like animation?
|
||||
# im.save(buffer, 'ppm')
|
||||
# bytes_io = BytesIO()
|
||||
# bytes_io.write(buffer.data())
|
||||
# buffer.close()
|
||||
# bytes_io.seek(0)
|
||||
# return Image.open(bytes_io)
|
||||
|
||||
|
||||
def align8to32(bytes, width, mode):
|
||||
"""
|
||||
converts each scanline of data from 8 bit to 32 bit aligned
|
||||
"""
|
||||
|
||||
bits_per_pixel = {
|
||||
'1': 1,
|
||||
'L': 8,
|
||||
'P': 8,
|
||||
}[mode]
|
||||
|
||||
# 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 = []
|
||||
for i in range(len(bytes) // bytes_per_line):
|
||||
new_data.append(bytes[i*bytes_per_line:(i+1)*bytes_per_line] + b'\x00' * extra_padding)
|
||||
|
||||
return b''.join(new_data)
|
||||
|
||||
|
||||
def _toqclass_helper(im):
|
||||
data = None
|
||||
colortable = None
|
||||
|
||||
# handle filename, if given instead of image name
|
||||
if hasattr(im, "toUtf8"):
|
||||
# FIXME - is this really the best way to do this?
|
||||
if str is bytes:
|
||||
im = unicode(im.toUtf8(), "utf-8")
|
||||
else:
|
||||
im = str(im.toUtf8(), "utf-8")
|
||||
if isPath(im):
|
||||
im = Image.open(im)
|
||||
|
||||
if im.mode == "1":
|
||||
format = QImage.Format_Mono
|
||||
elif im.mode == "L":
|
||||
format = QImage.Format_Indexed8
|
||||
colortable = []
|
||||
for i in range(256):
|
||||
colortable.append(rgb(i, i, i))
|
||||
elif im.mode == "P":
|
||||
format = QImage.Format_Indexed8
|
||||
colortable = []
|
||||
palette = im.getpalette()
|
||||
for i in range(0, len(palette), 3):
|
||||
colortable.append(rgb(*palette[i:i+3]))
|
||||
elif im.mode == "RGB":
|
||||
data = im.tobytes("raw", "BGRX")
|
||||
format = QImage.Format_RGB32
|
||||
elif im.mode == "RGBA":
|
||||
try:
|
||||
data = im.tobytes("raw", "BGRA")
|
||||
except SystemError:
|
||||
# workaround for earlier versions
|
||||
r, g, b, a = im.split()
|
||||
im = Image.merge("RGBA", (b, g, r, a))
|
||||
format = QImage.Format_ARGB32
|
||||
else:
|
||||
raise ValueError("unsupported image mode %r" % im.mode)
|
||||
|
||||
__data = data or align8to32(im.tobytes(), im.size[0], im.mode)
|
||||
return {
|
||||
'data': __data, 'im': im, 'format': format, 'colortable': colortable
|
||||
}
|
||||
|
||||
if qt_is_installed:
|
||||
class ImageQt(QImage):
|
||||
|
||||
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']
|
||||
QImage.__init__(self,
|
||||
self.__data, im_data['im'].size[0],
|
||||
im_data['im'].size[1], im_data['format'])
|
||||
if im_data['colortable']:
|
||||
self.setColorTable(im_data['colortable'])
|
||||
|
||||
|
||||
def toqimage(im):
|
||||
return ImageQt(im)
|
||||
|
||||
|
||||
def toqpixmap(im):
|
||||
# # This doesn't work. For now using a dumb approach.
|
||||
# im_data = _toqclass_helper(im)
|
||||
# result = QPixmap(im_data['im'].size[0], im_data['im'].size[1])
|
||||
# result.loadFromData(im_data['data'])
|
||||
# Fix some strange bug that causes
|
||||
if im.mode == 'RGB':
|
||||
im = im.convert('RGBA')
|
||||
|
||||
qimage = toqimage(im)
|
||||
return QPixmap.fromImage(qimage)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user