Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68c6904c28 | ||
|
|
05a169d65c | ||
|
|
4081f9f6a5 | ||
|
|
d47611e6fb | ||
|
|
fa7817a4d3 | ||
|
|
a029e5da93 | ||
|
|
1ab78b8fb7 | ||
|
|
43883a9928 | ||
|
|
ef041675a2 | ||
|
|
cacc11eaed | ||
|
|
fb51604296 | ||
|
|
07c68e2d68 | ||
|
|
d06072ff46 |
@ -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
|
||||
22
.coveragerc
22
.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 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
|
||||
if Image.DEBUG:
|
||||
if DEBUG:
|
||||
@ -1,22 +0,0 @@
|
||||
# Top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
[*]
|
||||
# Unix-style newlines with a newline ending every file
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
charset = utf-8
|
||||
|
||||
# Four-space indentation
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{toml,yml}]
|
||||
# Two-space indentation
|
||||
indent_size = 2
|
||||
|
||||
# Tab indentation (no size specified)
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
@ -1,6 +0,0 @@
|
||||
# Flake8
|
||||
8de95676e0fd89f2326b3953488ab66ff29cd2d0
|
||||
# Format with Black
|
||||
53a7e3500437a9fd5826bc04758f7116bd7e52dc
|
||||
# Format the C code with ClangFormat
|
||||
46b7e86bab79450ec0a2866c6c0c679afb659d17
|
||||
2
.gitattributes
vendored
2
.gitattributes
vendored
@ -1,3 +1 @@
|
||||
*.eps binary
|
||||
*.ppm binary
|
||||
*.container binary
|
||||
|
||||
38
.github/CONTRIBUTING.md
vendored
38
.github/CONTRIBUTING.md
vendored
@ -1,38 +0,0 @@
|
||||
# Contributing to Pillow
|
||||
|
||||
Bug fixes, feature additions, tests, documentation and more can be contributed via [issues](https://github.com/python-pillow/Pillow/issues) and/or [pull requests](https://github.com/python-pillow/Pillow/pulls). All contributions are welcome.
|
||||
|
||||
## Bug fixes, feature additions, etc.
|
||||
|
||||
Please send a pull request to the `main` branch. Please include [documentation](https://pillow.readthedocs.io) and [tests](../Tests/README.rst) for new features. Tests or documentation without bug fixes or feature additions are welcome too. Feel free to ask questions [via issues](https://github.com/python-pillow/Pillow/issues/new), [discussions](https://github.com/python-pillow/Pillow/discussions/new), [Gitter](https://gitter.im/python-pillow/Pillow) or irc://irc.freenode.net#pil
|
||||
|
||||
- Fork the Pillow repository.
|
||||
- Create a branch from `main`.
|
||||
- Develop bug fixes, features, tests, etc.
|
||||
- Run the test suite. You can enable GitHub Actions (https://github.com/MY-USERNAME/Pillow/actions) on your repo to catch test failures prior to the pull request, and [Codecov](https://codecov.io/gh) to see if the changed code is covered by tests.
|
||||
- Create a pull request to pull the changes from your branch to the Pillow `main`.
|
||||
|
||||
### Guidelines
|
||||
|
||||
- Separate code commits from reformatting commits.
|
||||
- Provide tests for any newly added code.
|
||||
- Follow PEP 8.
|
||||
- When committing only documentation changes please include `[ci skip]` in the commit message to avoid running 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.
|
||||
|
||||
## Reporting Issues
|
||||
|
||||
When reporting issues, please include code that reproduces the issue and whenever possible, an image that demonstrates the issue. Please upload images to GitHub, not to third-party file hosting sites. If necessary, add the image to a zip or tar archive.
|
||||
|
||||
The best reproductions are self-contained scripts with minimal dependencies. If you are using a framework such as plone, Django, or buildout, try to replicate the issue just using Pillow.
|
||||
|
||||
### Provide details
|
||||
|
||||
- What did you do?
|
||||
- What did you expect to happen?
|
||||
- What actually happened?
|
||||
- What versions of Pillow and Python are you using?
|
||||
|
||||
## Security vulnerabilities
|
||||
|
||||
Please see our [security policy](https://github.com/python-pillow/Pillow/blob/main/.github/SECURITY.md).
|
||||
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\>
|
||||
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]]
|
||||
```
|
||||
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -1,7 +0,0 @@
|
||||
Fixes # .
|
||||
|
||||
Changes proposed in this pull request:
|
||||
|
||||
*
|
||||
*
|
||||
*
|
||||
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
|
||||
42
.gitignore
vendored
42
.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,3 @@ 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
|
||||
@ -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
|
||||
48
.travis.yml
Normal file
48
.travis.yml
Normal file
@ -0,0 +1,48 @@
|
||||
language: python
|
||||
|
||||
notifications:
|
||||
irc: "chat.freenode.net#pil"
|
||||
|
||||
env: MAX_CONCURRENCY=4
|
||||
|
||||
python:
|
||||
- "pypy"
|
||||
- 2.6
|
||||
- 2.7
|
||||
- 3.2
|
||||
- 3.3
|
||||
- 3.4
|
||||
|
||||
install:
|
||||
- "sudo apt-get -qq install libfreetype6-dev liblcms2-dev python-qt4 ghostscript libffi-dev libjpeg-turbo-progs cmake"
|
||||
- "pip install cffi"
|
||||
- "pip install coveralls nose pyroma"
|
||||
- if [ "$TRAVIS_PYTHON_VERSION" == "2.6" ]; then pip install unittest2; fi
|
||||
|
||||
# webp
|
||||
- pushd depends && ./install_webp.sh && popd
|
||||
|
||||
# openjpeg
|
||||
- pushd depends && ./install_openjpeg.sh && popd
|
||||
|
||||
script:
|
||||
- coverage erase
|
||||
- python setup.py clean
|
||||
- python setup.py build_ext --inplace
|
||||
|
||||
# Don't cover PyPy: it fails intermittently and is x5.8 slower (#640)
|
||||
- if [ "$TRAVIS_PYTHON_VERSION" == "pypy" ]; then time python selftest.py; fi
|
||||
- if [ "$TRAVIS_PYTHON_VERSION" == "pypy" ]; then time nosetests Tests/test_*.py; fi
|
||||
|
||||
# Cover the others
|
||||
- if [ "$TRAVIS_PYTHON_VERSION" != "pypy" ]; then time coverage run --append --include=PIL/* selftest.py; fi
|
||||
- if [ "$TRAVIS_PYTHON_VERSION" != "pypy" ]; then time coverage run --append --include=PIL/* -m nose Tests/test_*.py; fi
|
||||
|
||||
after_success:
|
||||
- coverage report
|
||||
- coveralls
|
||||
- pip install pep8 pyflakes
|
||||
- pep8 --statistics --count PIL/*.py
|
||||
- pep8 --statistics --count Tests/*.py
|
||||
- pyflakes PIL/*.py | tee >(wc -l)
|
||||
- pyflakes Tests/*.py | tee >(wc -l)
|
||||
8139
CHANGES.rst
8139
CHANGES.rst
File diff suppressed because it is too large
Load Diff
30
LICENSE
30
LICENSE
@ -1,30 +0,0 @@
|
||||
The Python Imaging Library (PIL) is
|
||||
|
||||
Copyright © 1997-2011 by Secret Labs AB
|
||||
Copyright © 1995-2011 by Fredrik Lundh and contributors
|
||||
|
||||
Pillow is the friendly PIL fork. It is
|
||||
|
||||
Copyright © 2010 by Jeffrey 'Alex' Clark and contributors
|
||||
|
||||
Like PIL, Pillow is licensed under the open source MIT-CMU License:
|
||||
|
||||
By obtaining, using, and/or copying this software and/or its associated
|
||||
documentation, you agree that you have read, understood, and will comply
|
||||
with the following terms and conditions:
|
||||
|
||||
Permission to use, copy, modify and distribute this software and its
|
||||
documentation for any purpose and without fee is hereby granted,
|
||||
provided that the above copyright notice appears in all copies, and that
|
||||
both that copyright notice and this permission notice appear in supporting
|
||||
documentation, and that the name of Secret Labs AB or the author not be
|
||||
used in advertising or publicity pertaining to distribution of the software
|
||||
without specific, written prior permission.
|
||||
|
||||
SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS
|
||||
SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS.
|
||||
IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR BE LIABLE FOR ANY SPECIAL,
|
||||
INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
|
||||
OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
PERFORMANCE OF THIS SOFTWARE.
|
||||
118
MANIFEST.in
118
MANIFEST.in
@ -1,46 +1,86 @@
|
||||
include *.c
|
||||
include *.h
|
||||
include *.in
|
||||
include *.md
|
||||
include *.py
|
||||
include *.rst
|
||||
include *.sh
|
||||
include *.toml
|
||||
include *.txt
|
||||
include *.yaml
|
||||
include .flake8
|
||||
include LICENSE
|
||||
include .coveragerc
|
||||
include .gitattributes
|
||||
include .travis.yml
|
||||
include Makefile
|
||||
include tox.ini
|
||||
graft Tests
|
||||
graft Tests/images
|
||||
graft checks
|
||||
graft src
|
||||
graft depends
|
||||
graft winbuild
|
||||
graft docs
|
||||
graft _custom_build
|
||||
|
||||
# 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
|
||||
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
|
||||
recursive-include Images *.bdf
|
||||
recursive-include Images *.fli
|
||||
recursive-include Images *.gif
|
||||
recursive-include Images *.icns
|
||||
recursive-include Images *.ico
|
||||
recursive-include Images *.jpg
|
||||
recursive-include Images *.pbm
|
||||
recursive-include Images *.pil
|
||||
recursive-include Images *.png
|
||||
recursive-include Images *.ppm
|
||||
recursive-include Images *.psd
|
||||
recursive-include Images *.tar
|
||||
recursive-include Images *.webp
|
||||
recursive-include Images *.xpm
|
||||
recursive-include PIL *.md
|
||||
recursive-include Sane *.c
|
||||
recursive-include Sane *.py
|
||||
recursive-include Sane *.txt
|
||||
recursive-include Sane CHANGES
|
||||
recursive-include Sane README
|
||||
recursive-include Scripts *.py
|
||||
recursive-include Scripts README
|
||||
recursive-include Tests *.bdf
|
||||
recursive-include Tests *.bin
|
||||
recursive-include Tests *.bmp
|
||||
recursive-include Tests *.doc
|
||||
recursive-include Tests *.eps
|
||||
recursive-include Tests *.fli
|
||||
recursive-include Tests *.gif
|
||||
recursive-include Tests *.gnuplot
|
||||
recursive-include Tests *.html
|
||||
recursive-include Tests *.icm
|
||||
recursive-include Tests *.icns
|
||||
recursive-include Tests *.ico
|
||||
recursive-include Tests *.j2k
|
||||
recursive-include Tests *.jp2
|
||||
recursive-include Tests *.jpg
|
||||
recursive-include Tests *.lut
|
||||
recursive-include Tests *.pbm
|
||||
recursive-include Tests *.pcf
|
||||
recursive-include Tests *.pcx
|
||||
recursive-include Tests *.pgm
|
||||
recursive-include Tests *.pil
|
||||
recursive-include Tests *.png
|
||||
recursive-include Tests *.ppm
|
||||
recursive-include Tests *.psd
|
||||
recursive-include Tests *.py
|
||||
recursive-include Tests *.rst
|
||||
recursive-include Tests *.spider
|
||||
recursive-include Tests *.tar
|
||||
recursive-include Tests *.tif
|
||||
recursive-include Tests *.tiff
|
||||
recursive-include Tests *.ttf
|
||||
recursive-include Tests *.txt
|
||||
recursive-include Tests *.webp
|
||||
recursive-include Tests *.xpm
|
||||
recursive-include Tk *.c
|
||||
recursive-include Tk *.txt
|
||||
recursive-include Tk *.rst
|
||||
recursive-include depends *.sh
|
||||
recursive-include depends *.rst
|
||||
recursive-include docs *.bat
|
||||
recursive-include docs *.gitignore
|
||||
recursive-include docs *.html
|
||||
recursive-include docs *.py
|
||||
recursive-include docs *.rst
|
||||
recursive-include docs *.txt
|
||||
recursive-include docs Guardfile
|
||||
recursive-include docs Makefile
|
||||
recursive-include docs BUILDME
|
||||
recursive-include docs COPYING
|
||||
recursive-include docs LICENSE
|
||||
recursive-include libImaging *.c
|
||||
recursive-include libImaging *.h
|
||||
recursive-include Sane *.rst
|
||||
recursive-include Scripts *.rst
|
||||
|
||||
168
Makefile
168
Makefile
@ -1,142 +1,42 @@
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
.PHONY: clean
|
||||
|
||||
pre:
|
||||
virtualenv .
|
||||
bin/pip install -r requirements.txt
|
||||
bin/python setup.py develop
|
||||
bin/python selftest.py
|
||||
bin/nosetests Tests/test_*.py
|
||||
bin/python setup.py install
|
||||
bin/python test-installed.py
|
||||
check-manifest
|
||||
pyroma .
|
||||
viewdoc
|
||||
|
||||
clean:
|
||||
rm src/PIL/*.so || true
|
||||
rm -r build || true
|
||||
find . -name __pycache__ | xargs rm -r || true
|
||||
python setup.py clean
|
||||
rm PIL/*.so || true
|
||||
find . -name __pycache__ | xargs rm -r
|
||||
|
||||
.PHONY: coverage
|
||||
coverage:
|
||||
python3 -c "import pytest" > /dev/null 2>&1 || python3 -m pip install pytest
|
||||
python3 -m pytest -qq
|
||||
rm -r htmlcov || true
|
||||
python3 -c "import coverage" > /dev/null 2>&1 || python3 -m pip install coverage
|
||||
python3 -m coverage report
|
||||
|
||||
.PHONY: doc
|
||||
.PHONY: html
|
||||
doc html:
|
||||
$(MAKE) -C docs html
|
||||
|
||||
.PHONY: htmlview
|
||||
htmlview:
|
||||
$(MAKE) -C docs htmlview
|
||||
|
||||
.PHONY: htmllive
|
||||
htmllive:
|
||||
$(MAKE) -C docs htmllive
|
||||
|
||||
.PHONY: doccheck
|
||||
doccheck:
|
||||
$(MAKE) doc
|
||||
# Don't make our tests rely on the links in the docs being up every single build.
|
||||
# We don't control them. But do check, and update them to the target of their redirects.
|
||||
$(MAKE) -C docs linkcheck || true
|
||||
|
||||
.PHONY: docserve
|
||||
docserve:
|
||||
cd docs/_build/html && python3 -m http.server 2> /dev/null&
|
||||
|
||||
.PHONY: help
|
||||
help:
|
||||
@echo "Welcome to Pillow development. Please use \`make <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"
|
||||
|
||||
.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
|
||||
test: install
|
||||
python test-installed.py
|
||||
|
||||
.PHONY: debug
|
||||
debug:
|
||||
# make a debug version if we don't have a -dbg python. Leaves in symbols
|
||||
# for our stuff, kills optimization, and redirects to dev null so we
|
||||
# see any build failures.
|
||||
make clean > /dev/null
|
||||
CFLAGS='-g -O0' python3 -m pip -v install . > /dev/null
|
||||
inplace: clean
|
||||
python setup.py build_ext --inplace
|
||||
|
||||
.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
|
||||
coverage:
|
||||
# requires nose-cov
|
||||
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
|
||||
|
||||
.PHONY: sdist
|
||||
sdist:
|
||||
python3 -m build --help > /dev/null 2>&1 || python3 -m pip install build
|
||||
python3 -m build --sdist
|
||||
python3 -m twine --help > /dev/null 2>&1 || python3 -m pip install twine
|
||||
python3 -m twine check --strict dist/*
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
python3 -c "import pytest" > /dev/null 2>&1 || python3 -m pip install pytest
|
||||
python3 -m pytest -qq
|
||||
|
||||
.PHONY: test-p
|
||||
test-p:
|
||||
python3 -c "import xdist" > /dev/null 2>&1 || python3 -m pip install pytest-xdist
|
||||
python3 -m pytest -qq -n auto
|
||||
|
||||
|
||||
.PHONY: valgrind
|
||||
valgrind:
|
||||
python3 -c "import pytest_valgrind" > /dev/null 2>&1 || python3 -m pip install pytest-valgrind
|
||||
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
|
||||
test-dep:
|
||||
pip install coveralls nose nose-cov pep8 pyflakes
|
||||
|
||||
132
PIL/BdfFontFile.py
Normal file
132
PIL/BdfFontFile.py
Normal file
@ -0,0 +1,132 @@
|
||||
#
|
||||
# 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 PIL import Image
|
||||
from PIL import 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(s) for s in props["BBX"].split()]
|
||||
[dx, dy] = [int(s) for s 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
|
||||
|
||||
font = []
|
||||
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
|
||||
260
PIL/BmpImagePlugin.py
Normal file
260
PIL/BmpImagePlugin.py
Normal file
@ -0,0 +1,260 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
|
||||
__version__ = "0.7"
|
||||
|
||||
|
||||
from PIL import Image, ImageFile, ImagePalette, _binary
|
||||
import math
|
||||
|
||||
i8 = _binary.i8
|
||||
i16 = _binary.i16le
|
||||
i32 = _binary.i32le
|
||||
o8 = _binary.o8
|
||||
o16 = _binary.o16le
|
||||
o32 = _binary.o32le
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
# 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):
|
||||
|
||||
format = "BMP"
|
||||
format_description = "Windows Bitmap"
|
||||
|
||||
def _bitmap(self, header = 0, offset = 0):
|
||||
|
||||
if header:
|
||||
self.fp.seek(header)
|
||||
|
||||
read = self.fp.read
|
||||
|
||||
# CORE/INFO
|
||||
s = read(4)
|
||||
s = s + ImageFile._safe_read(self.fp, i32(s)-4)
|
||||
|
||||
if len(s) == 12:
|
||||
|
||||
# OS/2 1.0 CORE
|
||||
bits = i16(s[10:])
|
||||
self.size = i16(s[4:]), i16(s[6:])
|
||||
compression = 0
|
||||
lutsize = 3
|
||||
colors = 0
|
||||
direction = -1
|
||||
|
||||
elif len(s) in [40, 64, 108, 124]:
|
||||
|
||||
# WIN 3.1 or OS/2 2.0 INFO
|
||||
bits = i16(s[14:])
|
||||
self.size = i32(s[4:]), i32(s[8:])
|
||||
compression = i32(s[16:])
|
||||
pxperm = (i32(s[24:]), i32(s[28:])) # Pixels per meter
|
||||
lutsize = 4
|
||||
colors = i32(s[32:])
|
||||
direction = -1
|
||||
if i8(s[11]) == 0xff:
|
||||
# upside-down storage
|
||||
self.size = self.size[0], 2**32 - self.size[1]
|
||||
direction = 0
|
||||
|
||||
self.info["dpi"] = tuple(map(lambda x: math.ceil(x / 39.3701), pxperm))
|
||||
|
||||
else:
|
||||
raise IOError("Unsupported BMP header type (%d)" % len(s))
|
||||
|
||||
if (self.size[0]*self.size[1]) > 2**31:
|
||||
# Prevent DOS for > 2gb images
|
||||
raise IOError("Unsupported BMP Size: (%dx%d)" % self.size)
|
||||
|
||||
if not colors:
|
||||
colors = 1 << bits
|
||||
|
||||
# MODE
|
||||
try:
|
||||
self.mode, rawmode = BIT2MODE[bits]
|
||||
except KeyError:
|
||||
raise IOError("Unsupported BMP pixel depth (%d)" % bits)
|
||||
|
||||
if compression == 3:
|
||||
# BI_BITFIELDS compression
|
||||
mask = i32(read(4)), i32(read(4)), i32(read(4))
|
||||
if bits == 32 and mask == (0xff0000, 0x00ff00, 0x0000ff):
|
||||
rawmode = "BGRX"
|
||||
elif bits == 16 and mask == (0x00f800, 0x0007e0, 0x00001f):
|
||||
rawmode = "BGR;16"
|
||||
elif bits == 16 and mask == (0x007c00, 0x0003e0, 0x00001f):
|
||||
rawmode = "BGR;15"
|
||||
else:
|
||||
# print bits, map(hex, mask)
|
||||
raise IOError("Unsupported BMP bitfields layout")
|
||||
elif compression != 0:
|
||||
raise IOError("Unsupported BMP compression (%d)" % compression)
|
||||
|
||||
# LUT
|
||||
if self.mode == "P":
|
||||
palette = []
|
||||
greyscale = 1
|
||||
if colors == 2:
|
||||
indices = (0, 255)
|
||||
elif colors > 2**16 or colors <=0: #We're reading a i32.
|
||||
raise IOError("Unsupported BMP Palette size (%d)" % colors)
|
||||
else:
|
||||
indices = list(range(colors))
|
||||
for i in indices:
|
||||
rgb = read(lutsize)[:3]
|
||||
if rgb != o8(i)*3:
|
||||
greyscale = 0
|
||||
palette.append(rgb)
|
||||
if greyscale:
|
||||
if colors == 2:
|
||||
self.mode = rawmode = "1"
|
||||
else:
|
||||
self.mode = rawmode = "L"
|
||||
else:
|
||||
self.mode = "P"
|
||||
self.palette = ImagePalette.raw(
|
||||
"BGR", b"".join(palette)
|
||||
)
|
||||
|
||||
if not offset:
|
||||
offset = self.fp.tell()
|
||||
|
||||
self.tile = [("raw",
|
||||
(0, 0) + self.size,
|
||||
offset,
|
||||
(rawmode, ((self.size[0]*bits+31)>>3)&(~3), direction))]
|
||||
|
||||
self.info["compression"] = compression
|
||||
|
||||
def _open(self):
|
||||
|
||||
# HEAD
|
||||
s = self.fp.read(14)
|
||||
if s[:2] != b"BM":
|
||||
raise SyntaxError("Not a BMP file")
|
||||
offset = i32(s[10:])
|
||||
|
||||
self._bitmap(offset=offset)
|
||||
|
||||
|
||||
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),
|
||||
}
|
||||
|
||||
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")
|
||||
68
PIL/BufrStubImagePlugin.py
Normal file
68
PIL/BufrStubImagePlugin.py
Normal file
@ -0,0 +1,68 @@
|
||||
#
|
||||
# The Python Imaging Library
|
||||
# $Id$
|
||||
#
|
||||
# BUFR stub adapter
|
||||
#
|
||||
# Copyright (c) 1996-2003 by Fredrik Lundh
|
||||
#
|
||||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
from PIL import Image, ImageFile
|
||||
|
||||
_handler = None
|
||||
|
||||
##
|
||||
# Install application-specific BUFR image handler.
|
||||
#
|
||||
# @param handler Handler object.
|
||||
|
||||
def register_handler(handler):
|
||||
global _handler
|
||||
_handler = handler
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Image adapter
|
||||
|
||||
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):
|
||||
|
||||
offset = self.fp.tell()
|
||||
|
||||
if not _accept(self.fp.read(8)):
|
||||
raise SyntaxError("Not a BUFR file")
|
||||
|
||||
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("BUFR save handler not installed")
|
||||
_handler.save(im, fp, filename)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Registry
|
||||
|
||||
Image.register_open(BufrStubImageFile.format, BufrStubImageFile, _accept)
|
||||
Image.register_save(BufrStubImageFile.format, _save)
|
||||
|
||||
Image.register_extension(BufrStubImageFile.format, ".bufr")
|
||||
116
PIL/ContainerIO.py
Normal file
116
PIL/ContainerIO.py
Normal file
@ -0,0 +1,116 @@
|
||||
#
|
||||
# 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:
|
||||
|
||||
##
|
||||
# Create file object.
|
||||
#
|
||||
# @param file Existing file.
|
||||
# @param offset Start of region, in bytes.
|
||||
# @param length Size of region, in bytes.
|
||||
|
||||
def __init__(self, file, offset, length):
|
||||
self.fh = file
|
||||
self.pos = 0
|
||||
self.offset = offset
|
||||
self.length = length
|
||||
self.fh.seek(offset)
|
||||
|
||||
##
|
||||
# Always false.
|
||||
|
||||
def isatty(self):
|
||||
return 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.
|
||||
|
||||
def seek(self, offset, mode = 0):
|
||||
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)
|
||||
|
||||
##
|
||||
# Get current file pointer.
|
||||
#
|
||||
# @return Offset from start of region, in bytes.
|
||||
|
||||
def tell(self):
|
||||
return self.pos
|
||||
|
||||
##
|
||||
# Read data.
|
||||
#
|
||||
# @def read(bytes=0)
|
||||
# @param bytes Number of bytes to read. If omitted or zero,
|
||||
# read until end of region.
|
||||
# @return An 8-bit string.
|
||||
|
||||
def read(self, n = 0):
|
||||
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)
|
||||
|
||||
##
|
||||
# Read a line of text.
|
||||
#
|
||||
# @return An 8-bit string.
|
||||
|
||||
def readline(self):
|
||||
s = ""
|
||||
while True:
|
||||
c = self.read(1)
|
||||
if not c:
|
||||
break
|
||||
s = s + c
|
||||
if c == "\n":
|
||||
break
|
||||
return s
|
||||
|
||||
##
|
||||
# Read multiple lines of text.
|
||||
#
|
||||
# @return A list of 8-bit strings.
|
||||
|
||||
def readlines(self):
|
||||
l = []
|
||||
while True:
|
||||
s = self.readline()
|
||||
if not s:
|
||||
break
|
||||
l.append(s)
|
||||
return l
|
||||
@ -15,61 +15,72 @@
|
||||
#
|
||||
# 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
|
||||
|
||||
__version__ = "0.1"
|
||||
|
||||
from PIL import Image, BmpImagePlugin, _binary
|
||||
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
i8 = _binary.i8
|
||||
i16 = _binary.i16le
|
||||
i32 = _binary.i32le
|
||||
|
||||
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 an 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
|
||||
if not m:
|
||||
msg = "No cursors were found"
|
||||
raise TypeError(msg)
|
||||
#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:])
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
Image.register_open(CurImageFile.format, CurImageFile, _accept)
|
||||
Image.register_open("CUR", CurImageFile, _accept)
|
||||
|
||||
Image.register_extension(CurImageFile.format, ".cur")
|
||||
Image.register_extension("CUR", ".cur")
|
||||
@ -20,36 +20,34 @@
|
||||
#
|
||||
# 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?
|
||||
from PIL import Image, _binary
|
||||
|
||||
from PIL.PcxImagePlugin import PcxImageFile
|
||||
|
||||
def _accept(prefix: bytes) -> bool:
|
||||
return len(prefix) >= 4 and i32(prefix) == MAGIC
|
||||
MAGIC = 0x3ADE68B1 # QUIZ: what's this value, then?
|
||||
|
||||
i32 = _binary.i32le
|
||||
|
||||
def _accept(prefix):
|
||||
return i32(prefix) == MAGIC
|
||||
|
||||
##
|
||||
# Image plugin for the Intel DCX format.
|
||||
|
||||
|
||||
class DcxImageFile(PcxImageFile):
|
||||
|
||||
format = "DCX"
|
||||
format_description = "Intel DCX"
|
||||
_close_exclusive_fp_after_loading = False
|
||||
|
||||
def _open(self) -> None:
|
||||
def _open(self):
|
||||
|
||||
# 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,26 +57,21 @@ 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
|
||||
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
|
||||
|
||||
|
||||
Image.register_open(DcxImageFile.format, DcxImageFile, _accept)
|
||||
Image.register_open("DCX", DcxImageFile, _accept)
|
||||
|
||||
Image.register_extension(DcxImageFile.format, ".dcx")
|
||||
Image.register_extension("DCX", ".dcx")
|
||||
446
PIL/EpsImagePlugin.py
Normal file
446
PIL/EpsImagePlugin.py
Normal file
@ -0,0 +1,446 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
__version__ = "0.5"
|
||||
|
||||
import re
|
||||
import io
|
||||
from PIL import Image, ImageFile, _binary
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
i32 = _binary.i32le
|
||||
o32 = _binary.o32le
|
||||
|
||||
split = re.compile(r"^%%([^:]*):[ \t]*(.*)[ \t]*$")
|
||||
field = re.compile(r"^%[%!\w]([^:]*)[ \t]*$")
|
||||
|
||||
gs_windows_binary = None
|
||||
import sys
|
||||
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:
|
||||
gs = subprocess.Popen(['gs','--version'], stdout=subprocess.PIPE)
|
||||
gs.stdout.read()
|
||||
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 dependend 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 tempfile, os, subprocess
|
||||
|
||||
out_fd, outfile = tempfile.mkstemp()
|
||||
os.close(out_fd)
|
||||
in_fd, infile = tempfile.mkstemp()
|
||||
os.close(in_fd)
|
||||
|
||||
# ignore length and offset!
|
||||
# ghostscript can read it
|
||||
# copy whole file to read in ghostscript
|
||||
with open(infile, '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
|
||||
length -= 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 -dSAFER", # don't pause between pages, 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:
|
||||
gs = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
|
||||
gs.stdin.close()
|
||||
status = gs.wait()
|
||||
if status:
|
||||
raise IOError("gs failed (status %d)" % status)
|
||||
im = Image.core.open_ppm(outfile)
|
||||
finally:
|
||||
try:
|
||||
os.unlink(outfile)
|
||||
os.unlink(infile)
|
||||
except: pass
|
||||
|
||||
return im
|
||||
|
||||
|
||||
class PSFile:
|
||||
"""Wrapper that treats either CR or LF as end of line."""
|
||||
def __init__(self, fp):
|
||||
self.fp = fp
|
||||
self.char = None
|
||||
def __getattr__(self, id):
|
||||
v = getattr(self.fp, id)
|
||||
setattr(self, id, v)
|
||||
return v
|
||||
def seek(self, offset, whence=0):
|
||||
self.char = None
|
||||
self.fp.seek(offset, whence)
|
||||
def read(self, count):
|
||||
return self.fp.read(count).decode('latin-1')
|
||||
def readbinary(self, count):
|
||||
return self.fp.read(count)
|
||||
def tell(self):
|
||||
pos = self.fp.tell()
|
||||
if self.char:
|
||||
pos -= 1
|
||||
return pos
|
||||
def readline(self):
|
||||
s = b""
|
||||
if self.char:
|
||||
c = self.char
|
||||
self.char = None
|
||||
else:
|
||||
c = self.fp.read(1)
|
||||
while c not in b"\r\n":
|
||||
s = s + c
|
||||
c = self.fp.read(1)
|
||||
if c == b"\r":
|
||||
self.char = self.fp.read(1)
|
||||
if self.char == b"\n":
|
||||
self.char = None
|
||||
return s.decode('latin-1') + "\n"
|
||||
|
||||
|
||||
def _accept(prefix):
|
||||
return prefix[:4] == b"%!PS" or 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"
|
||||
|
||||
def _open(self):
|
||||
|
||||
fp = PSFile(self.fp)
|
||||
|
||||
# FIX for: Some EPS file not handled correctly / issue #302
|
||||
# EPS can contain binary data
|
||||
# or start directly with latin coding
|
||||
# read header in both ways to handle both
|
||||
# file types
|
||||
# more info see http://partners.adobe.com/public/developer/en/ps/5002.EPSF_Spec.pdf
|
||||
|
||||
# for HEAD without binary preview
|
||||
s = fp.read(4)
|
||||
# for HEAD with binary preview
|
||||
fp.seek(0)
|
||||
sb = fp.readbinary(160)
|
||||
|
||||
if s[:4] == "%!PS":
|
||||
fp.seek(0, 2)
|
||||
length = fp.tell()
|
||||
offset = 0
|
||||
elif i32(sb[0:4]) == 0xC6D3D0C5:
|
||||
offset = i32(sb[4:8])
|
||||
length = i32(sb[8:12])
|
||||
else:
|
||||
raise SyntaxError("not an EPS file")
|
||||
|
||||
# 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()
|
||||
|
||||
while s:
|
||||
|
||||
if len(s) > 255:
|
||||
raise SyntaxError("not an EPS file")
|
||||
|
||||
if s[-2:] == '\r\n':
|
||||
s = s[:-2]
|
||||
elif s[-1:] == '\n':
|
||||
s = s[:-1]
|
||||
|
||||
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(s)) for s 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:1] == '%':
|
||||
# handle non-DSC Postscript comments that some
|
||||
# tools mistakenly put in the Comments section
|
||||
pass
|
||||
else:
|
||||
raise IOError("bad EPS header")
|
||||
|
||||
s = fp.readline()
|
||||
|
||||
if s[:1] != "%":
|
||||
break
|
||||
|
||||
|
||||
#
|
||||
# Scan for an "ImageData" descriptor
|
||||
|
||||
while s[0] == "%":
|
||||
|
||||
if len(s) > 255:
|
||||
raise SyntaxError("not an EPS file")
|
||||
|
||||
if s[-2:] == '\r\n':
|
||||
s = s[:-2]
|
||||
elif s[-1:] == '\n':
|
||||
s = s[:-1]
|
||||
|
||||
if s[:11] == "%ImageData:":
|
||||
|
||||
[x, y, bi, mo, z3, z4, en, id] =\
|
||||
s[11:].split(None, 7)
|
||||
|
||||
x = int(x); y = int(y)
|
||||
|
||||
bi = int(bi)
|
||||
mo = int(mo)
|
||||
|
||||
en = int(en)
|
||||
|
||||
if en == 1:
|
||||
decoder = "eps_binary"
|
||||
elif en == 2:
|
||||
decoder = "eps_hex"
|
||||
else:
|
||||
break
|
||||
if bi != 8:
|
||||
break
|
||||
if mo == 1:
|
||||
self.mode = "L"
|
||||
elif mo == 2:
|
||||
self.mode = "LAB"
|
||||
elif mo == 3:
|
||||
self.mode = "RGB"
|
||||
else:
|
||||
break
|
||||
|
||||
if id[:1] == id[-1:] == '"':
|
||||
id = id[1:-1]
|
||||
|
||||
# Scan forward to the actual image data
|
||||
while True:
|
||||
s = fp.readline()
|
||||
if not s:
|
||||
break
|
||||
if s[:len(id)] == id:
|
||||
self.size = x, y
|
||||
self.tile2 = [(decoder,
|
||||
(0, 0, x, y),
|
||||
fp.tell(),
|
||||
0)]
|
||||
return
|
||||
|
||||
s = fp.readline()
|
||||
if not s:
|
||||
break
|
||||
|
||||
if not box:
|
||||
raise IOError("cannot determine EPS bounding box")
|
||||
|
||||
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:
|
||||
def __init__(self, fp):
|
||||
self.fp = fp
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.fp, name)
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
base_fp = fp
|
||||
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")
|
||||
fp.flush()
|
||||
|
||||
ImageFile._save(im, base_fp, [("eps", (0,0)+im.size, 0, None)])
|
||||
|
||||
fp.write("\n%%%%EndBinary\n")
|
||||
fp.write("grestore end\n")
|
||||
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")
|
||||
193
PIL/ExifTags.py
Normal file
193
PIL/ExifTags.py
Normal file
@ -0,0 +1,193 @@
|
||||
#
|
||||
# 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
|
||||
0x00fe: "NewSubfileType",
|
||||
0x00ff: "SubfileType",
|
||||
0x0100: "ImageWidth",
|
||||
0x0101: "ImageLength",
|
||||
0x0102: "BitsPerSample",
|
||||
0x0103: "Compression",
|
||||
0x0106: "PhotometricInterpretation",
|
||||
0x0107: "Threshholding",
|
||||
0x0108: "CellWidth",
|
||||
0x0109: "CellLenght",
|
||||
0x010a: "FillOrder",
|
||||
0x010d: "DocumentName",
|
||||
0x011d: "PageName",
|
||||
0x010e: "ImageDescription",
|
||||
0x010f: "Make",
|
||||
0x0110: "Model",
|
||||
0x0111: "StripOffsets",
|
||||
0x0112: "Orientation",
|
||||
0x0115: "SamplesPerPixel",
|
||||
0x0116: "RowsPerStrip",
|
||||
0x0117: "StripByteConunts",
|
||||
0x0118: "MinSampleValue",
|
||||
0x0119: "MaxSampleValue",
|
||||
0x011a: "XResolution",
|
||||
0x011b: "YResolution",
|
||||
0x011c: "PlanarConfiguration",
|
||||
0x0120: "FreeOffsets",
|
||||
0x0121: "FreeByteCounts",
|
||||
0x0122: "GrayResponseUnit",
|
||||
0x0123: "GrayResponseCurve",
|
||||
0x0128: "ResolutionUnit",
|
||||
0x012d: "TransferFunction",
|
||||
0x0131: "Software",
|
||||
0x0132: "DateTime",
|
||||
0x013b: "Artist",
|
||||
0x013c: "HostComputer",
|
||||
0x013e: "WhitePoint",
|
||||
0x013f: "PrimaryChromaticities",
|
||||
0x0140: "ColorMap",
|
||||
0x0152: "ExtraSamples",
|
||||
0x0201: "JpegIFOffset",
|
||||
0x0202: "JpegIFByteCount",
|
||||
0x0211: "YCbCrCoefficients",
|
||||
0x0212: "YCbCrSubSampling",
|
||||
0x0213: "YCbCrPositioning",
|
||||
0x0214: "ReferenceBlackWhite",
|
||||
0x1000: "RelatedImageFileFormat",
|
||||
0x1001: "RelatedImageLength", # FIXME / Dictionary contains duplicate keys
|
||||
0x1001: "RelatedImageWidth", # FIXME \ Dictionary contains duplicate keys
|
||||
0x828d: "CFARepeatPatternDim",
|
||||
0x828e: "CFAPattern",
|
||||
0x828f: "BatteryLevel",
|
||||
0x8298: "Copyright",
|
||||
0x829a: "ExposureTime",
|
||||
0x829d: "FNumber",
|
||||
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",
|
||||
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",
|
||||
|
||||
}
|
||||
|
||||
##
|
||||
# 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",
|
||||
}
|
||||
73
PIL/FitsStubImagePlugin.py
Normal file
73
PIL/FitsStubImagePlugin.py
Normal file
@ -0,0 +1,73 @@
|
||||
#
|
||||
# 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 PIL import Image, ImageFile
|
||||
|
||||
_handler = None
|
||||
|
||||
##
|
||||
# Install application-specific FITS image handler.
|
||||
#
|
||||
# @param handler Handler object.
|
||||
|
||||
def register_handler(handler):
|
||||
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")
|
||||
141
PIL/FliImagePlugin.py
Normal file
141
PIL/FliImagePlugin.py
Normal file
@ -0,0 +1,141 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
|
||||
__version__ = "0.2"
|
||||
|
||||
from PIL import Image, ImageFile, ImagePalette, _binary
|
||||
|
||||
i8 = _binary.i8
|
||||
i16 = _binary.i16le
|
||||
i32 = _binary.i32le
|
||||
o8 = _binary.o8
|
||||
|
||||
#
|
||||
# decoder
|
||||
|
||||
def _accept(prefix):
|
||||
return 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"
|
||||
|
||||
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.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
|
||||
|
||||
def seek(self, frame):
|
||||
|
||||
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 = self.__offset + framesize
|
||||
|
||||
def tell(self):
|
||||
|
||||
return self.frame
|
||||
|
||||
#
|
||||
# registry
|
||||
|
||||
Image.register_open("FLI", FliImageFile, _accept)
|
||||
|
||||
Image.register_extension("FLI", ".fli")
|
||||
Image.register_extension("FLI", ".flc")
|
||||
146
PIL/FontFile.py
Normal file
146
PIL/FontFile.py
Normal file
@ -0,0 +1,146 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
import os
|
||||
from PIL import Image, _binary
|
||||
|
||||
import marshal
|
||||
|
||||
try:
|
||||
import zlib
|
||||
except ImportError:
|
||||
zlib = None
|
||||
|
||||
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:
|
||||
|
||||
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, yy = src[2] - src[0], 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 save1(self, filename):
|
||||
"Save font in version 1 format"
|
||||
|
||||
self.compile()
|
||||
|
||||
# font data
|
||||
self.bitmap.save(os.path.splitext(filename)[0] + ".pbm", "PNG")
|
||||
|
||||
# font metrics
|
||||
fp = open(os.path.splitext(filename)[0] + ".pil", "wb")
|
||||
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])
|
||||
fp.close()
|
||||
|
||||
|
||||
def save2(self, filename):
|
||||
"Save font in version 2 format"
|
||||
|
||||
# THIS IS WORK IN PROGRESS
|
||||
|
||||
self.compile()
|
||||
|
||||
data = marshal.dumps((self.metrics, self.info))
|
||||
|
||||
if zlib:
|
||||
data = b"z" + zlib.compress(data, 9)
|
||||
else:
|
||||
data = b"u" + data
|
||||
|
||||
fp = open(os.path.splitext(filename)[0] + ".pil", "wb")
|
||||
|
||||
fp.write(b"PILfont2\n" + self.name + "\n" + "DATA\n")
|
||||
|
||||
fp.write(data)
|
||||
|
||||
self.bitmap.save(fp, "PNG")
|
||||
|
||||
fp.close()
|
||||
|
||||
|
||||
save = save1 # for now
|
||||
224
PIL/FpxImagePlugin.py
Normal file
224
PIL/FpxImagePlugin.py
Normal file
@ -0,0 +1,224 @@
|
||||
#
|
||||
# THIS IS WORK IN PROGRESS
|
||||
#
|
||||
# The Python Imaging Library.
|
||||
# $Id$
|
||||
#
|
||||
# FlashPix support for PIL
|
||||
#
|
||||
# History:
|
||||
# 97-01-25 fl Created (reads uncompressed RGB images only)
|
||||
#
|
||||
# Copyright (c) Secret Labs AB 1997.
|
||||
# Copyright (c) Fredrik Lundh 1997.
|
||||
#
|
||||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
|
||||
__version__ = "0.1"
|
||||
|
||||
|
||||
from PIL import Image, ImageFile
|
||||
from PIL.OleFileIO import *
|
||||
|
||||
|
||||
# we map from colour field tuples to (mode, rawmode) descriptors
|
||||
MODES = {
|
||||
# opacity
|
||||
(0x00007ffe): ("A", "L"),
|
||||
# monochrome
|
||||
(0x00010000,): ("L", "L"),
|
||||
(0x00018000, 0x00017ffe): ("RGBA", "LA"),
|
||||
# photo YCC
|
||||
(0x00020000, 0x00020001, 0x00020002): ("RGB", "YCC;P"),
|
||||
(0x00028000, 0x00028001, 0x00028002, 0x00027ffe): ("RGBA", "YCCA;P"),
|
||||
# standard RGB (NIFRGB)
|
||||
(0x00030000, 0x00030001, 0x00030002): ("RGB","RGB"),
|
||||
(0x00038000, 0x00038001, 0x00038002, 0x00037ffe): ("RGBA","RGBA"),
|
||||
}
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
def _accept(prefix):
|
||||
return prefix[:8] == MAGIC
|
||||
|
||||
##
|
||||
# Image plugin for the FlashPix images.
|
||||
|
||||
class FpxImageFile(ImageFile.ImageFile):
|
||||
|
||||
format = "FPX"
|
||||
format_description = "FlashPix"
|
||||
|
||||
def _open(self):
|
||||
#
|
||||
# read the OLE directory and see if this is a likely
|
||||
# to be a FlashPix file
|
||||
|
||||
try:
|
||||
self.ole = OleFileIO(self.fp)
|
||||
except IOError:
|
||||
raise SyntaxError("not an FPX file; invalid OLE file")
|
||||
|
||||
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 = 1):
|
||||
#
|
||||
# get the Image Contents Property Set
|
||||
|
||||
prop = self.ole.getproperties([
|
||||
"Data Object Store %06d" % index,
|
||||
"\005Image Contents"
|
||||
])
|
||||
|
||||
# size (highest resolution)
|
||||
|
||||
self.size = prop[0x1000002], prop[0x1000003]
|
||||
|
||||
size = max(self.size)
|
||||
i = 1
|
||||
while size > 64:
|
||||
size = size / 2
|
||||
i += 1
|
||||
self.maxid = i - 1
|
||||
|
||||
# mode. instead of using a single field for this, flashpix
|
||||
# requires you to specify the mode for each channel in each
|
||||
# resolution subimage, and leaves it to the decoder to make
|
||||
# sure that they all match. for now, we'll cheat and assume
|
||||
# that this is always the case.
|
||||
|
||||
id = self.maxid << 16
|
||||
|
||||
s = prop[0x2000002|id]
|
||||
|
||||
colors = []
|
||||
for i in range(i32(s, 4)):
|
||||
# note: for now, we ignore the "uncalibrated" flag
|
||||
colors.append(i32(s, 8+i*4) & 0x7fffffff)
|
||||
|
||||
self.mode, self.rawmode = MODES[tuple(colors)]
|
||||
|
||||
# load JPEG tables, if any
|
||||
self.jpeg = {}
|
||||
for i in range(256):
|
||||
id = 0x3000001|(i << 16)
|
||||
if id in prop:
|
||||
self.jpeg[i] = prop[id]
|
||||
|
||||
# print len(self.jpeg), "tables loaded"
|
||||
|
||||
self._open_subimage(1, self.maxid)
|
||||
|
||||
def _open_subimage(self, index = 1, subimage = 0):
|
||||
#
|
||||
# setup tile descriptors for a given subimage
|
||||
|
||||
stream = [
|
||||
"Data Object Store %06d" % index,
|
||||
"Resolution %04d" % subimage,
|
||||
"Subimage 0000 Header"
|
||||
]
|
||||
|
||||
fp = self.ole.openstream(stream)
|
||||
|
||||
# skip prefix
|
||||
p = fp.read(28)
|
||||
|
||||
# header stream
|
||||
s = fp.read(36)
|
||||
|
||||
size = i32(s, 4), i32(s, 8)
|
||||
tilecount = i32(s, 12)
|
||||
tilesize = i32(s, 16), i32(s, 20)
|
||||
channels = i32(s, 24)
|
||||
offset = i32(s, 28)
|
||||
length = i32(s, 32)
|
||||
|
||||
# print size, self.mode, self.rawmode
|
||||
|
||||
if size != self.size:
|
||||
raise IOError("subimage mismatch")
|
||||
|
||||
# get tile descriptors
|
||||
fp.seek(28 + offset)
|
||||
s = fp.read(i32(s, 12) * length)
|
||||
|
||||
x = y = 0
|
||||
xsize, ysize = size
|
||||
xtile, ytile = tilesize
|
||||
self.tile = []
|
||||
|
||||
for i in range(0, len(s), length):
|
||||
|
||||
compression = i32(s, i+8)
|
||||
|
||||
if compression == 0:
|
||||
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(("fill", (x,y,x+xtile,y+ytile),
|
||||
i32(s, i) + 28, (self.rawmode, s[12:16])))
|
||||
|
||||
elif compression == 2:
|
||||
|
||||
internal_color_conversion = i8(s[14])
|
||||
jpeg_tables = i8(s[15])
|
||||
rawmode = self.rawmode
|
||||
|
||||
if internal_color_conversion:
|
||||
# The image is stored as usual (usually YCbCr).
|
||||
if rawmode == "RGBA":
|
||||
# For "RGBA", data is stored as YCbCrA based on
|
||||
# negative RGB. The following trick works around
|
||||
# this problem :
|
||||
jpegmode, rawmode = "YCbCrK", "CMYK"
|
||||
else:
|
||||
jpegmode = None # let the decoder decide
|
||||
|
||||
else:
|
||||
# The image is stored as defined by rawmode
|
||||
jpegmode = rawmode
|
||||
|
||||
self.tile.append(("jpeg", (x,y,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!
|
||||
|
||||
if jpeg_tables:
|
||||
self.tile_prefix = self.jpeg[jpeg_tables]
|
||||
|
||||
else:
|
||||
raise IOError("unknown/invalid compression")
|
||||
|
||||
x = x + xtile
|
||||
if x >= xsize:
|
||||
x, y = 0, y + ytile
|
||||
if y >= ysize:
|
||||
break # isn't really required
|
||||
|
||||
self.stream = stream
|
||||
self.fp = None
|
||||
|
||||
def load(self):
|
||||
|
||||
if not self.fp:
|
||||
self.fp = self.ole.openstream(self.stream[:2] + ["Subimage 0000 Data"])
|
||||
|
||||
ImageFile.ImageFile.load(self)
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
Image.register_open("FPX", FpxImageFile, _accept)
|
||||
|
||||
Image.register_extension("FPX", ".fpx")
|
||||
69
PIL/GbrImagePlugin.py
Normal file
69
PIL/GbrImagePlugin.py
Normal file
@ -0,0 +1,69 @@
|
||||
#
|
||||
# The Python Imaging Library
|
||||
# $Id$
|
||||
#
|
||||
# load a GIMP brush file
|
||||
#
|
||||
# History:
|
||||
# 96-03-14 fl Created
|
||||
#
|
||||
# Copyright (c) Secret Labs AB 1997.
|
||||
# Copyright (c) Fredrik Lundh 1996.
|
||||
#
|
||||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
from PIL import Image, ImageFile, _binary
|
||||
|
||||
i32 = _binary.i32be
|
||||
|
||||
def _accept(prefix):
|
||||
return i32(prefix) >= 20 and i32(prefix[4:8]) == 1
|
||||
|
||||
##
|
||||
# Image plugin for the GIMP brush format.
|
||||
|
||||
class GbrImageFile(ImageFile.ImageFile):
|
||||
|
||||
format = "GBR"
|
||||
format_description = "GIMP brush file"
|
||||
|
||||
def _open(self):
|
||||
|
||||
header_size = i32(self.fp.read(4))
|
||||
version = i32(self.fp.read(4))
|
||||
if header_size < 20 or version != 1:
|
||||
raise SyntaxError("not a GIMP brush")
|
||||
|
||||
width = i32(self.fp.read(4))
|
||||
height = i32(self.fp.read(4))
|
||||
bytes = i32(self.fp.read(4))
|
||||
if width <= 0 or height <= 0 or bytes != 1:
|
||||
raise SyntaxError("not a GIMP brush")
|
||||
|
||||
comment = self.fp.read(header_size - 20)[:-1]
|
||||
|
||||
self.mode = "L"
|
||||
self.size = width, height
|
||||
|
||||
self.info["comment"] = comment
|
||||
|
||||
# Since the brush is so small, we read the data immediately
|
||||
self.data = self.fp.read(width * height)
|
||||
|
||||
def load(self):
|
||||
|
||||
if not self.data:
|
||||
return
|
||||
|
||||
# create an image out of the brush data block
|
||||
self.im = Image.core.new(self.mode, self.size)
|
||||
self.im.frombytes(self.data)
|
||||
self.data = b""
|
||||
|
||||
#
|
||||
# registry
|
||||
|
||||
Image.register_open("GBR", GbrImageFile, _accept)
|
||||
|
||||
Image.register_extension("GBR", ".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.
|
||||
|
||||
|
||||
__version__ = "0.1"
|
||||
|
||||
from PIL import ImageFile, ImagePalette, _binary
|
||||
from PIL._util import isPath
|
||||
|
||||
try:
|
||||
import builtins
|
||||
except ImportError:
|
||||
import __builtin__
|
||||
builtins = __builtin__
|
||||
|
||||
i16 = _binary.i16be
|
||||
|
||||
##
|
||||
# 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))]
|
||||
|
||||
##
|
||||
# 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".
|
||||
# @return An image instance.
|
||||
# @exception IOError If the image could not be read.
|
||||
|
||||
def open(fp, mode = "r"):
|
||||
|
||||
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")
|
||||
506
PIL/GifImagePlugin.py
Normal file
506
PIL/GifImagePlugin.py
Normal file
@ -0,0 +1,506 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
|
||||
__version__ = "0.9"
|
||||
|
||||
|
||||
from PIL import Image, ImageFile, ImagePalette, _binary
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Helpers
|
||||
|
||||
i8 = _binary.i8
|
||||
i16 = _binary.i16le
|
||||
o8 = _binary.o8
|
||||
o16 = _binary.o16le
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# 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"
|
||||
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.seek(0) # get ready to read first frame
|
||||
|
||||
def seek(self, frame):
|
||||
|
||||
if frame == 0:
|
||||
# rewind
|
||||
self.__offset = 0
|
||||
self.dispose = None
|
||||
self.__frame = -1
|
||||
self.__fp.seek(self.__rewind)
|
||||
|
||||
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 = self.dispose
|
||||
self.dispose = None
|
||||
|
||||
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
|
||||
try:
|
||||
# disposal methods
|
||||
if flags & 8:
|
||||
# replace with background colour
|
||||
self.dispose = Image.core.fill("P", self.size,
|
||||
self.info["background"])
|
||||
elif flags & 16:
|
||||
# replace with previous contents
|
||||
self.dispose = self.im.copy()
|
||||
except (AttributeError, KeyError):
|
||||
pass
|
||||
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:])
|
||||
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)
|
||||
|
||||
if not self.tile:
|
||||
# self.__fp = None
|
||||
raise EOFError("no more images in GIF file")
|
||||
|
||||
self.mode = "L"
|
||||
if self.palette:
|
||||
self.mode = "P"
|
||||
|
||||
def tell(self):
|
||||
return self.__frame
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Write GIF files
|
||||
|
||||
try:
|
||||
import _imaging_gif
|
||||
except ImportError:
|
||||
_imaging_gif = None
|
||||
|
||||
RAWMODE = {
|
||||
"1": "L",
|
||||
"L": "L",
|
||||
"P": "P",
|
||||
}
|
||||
|
||||
def _save(im, fp, filename):
|
||||
|
||||
if _imaging_gif:
|
||||
# call external driver
|
||||
try:
|
||||
_imaging_gif.save(im, fp, filename)
|
||||
return
|
||||
except IOError:
|
||||
pass # write uncompressed file
|
||||
|
||||
try:
|
||||
rawmode = RAWMODE[im.mode]
|
||||
imOut = im
|
||||
except KeyError:
|
||||
# convert on the fly (EXPERIMENTAL -- I'm not sure PIL
|
||||
# should automatically convert images on save...)
|
||||
if Image.getmodebase(im.mode) == "RGB":
|
||||
palette_size = 256
|
||||
if im.palette:
|
||||
palette_size = len(im.palette.getdata()[1]) // 3
|
||||
imOut = im.convert("P", palette=1, colors=palette_size)
|
||||
rawmode = "P"
|
||||
else:
|
||||
imOut = im.convert("L")
|
||||
rawmode = "L"
|
||||
|
||||
# header
|
||||
try:
|
||||
palette = im.encoderinfo["palette"]
|
||||
except KeyError:
|
||||
palette = None
|
||||
im.encoderinfo["optimize"] = im.encoderinfo.get("optimize", True)
|
||||
if im.encoderinfo["optimize"]:
|
||||
# When the mode is L, and we optimize, we end up with
|
||||
# im.mode == P and rawmode = L, which fails.
|
||||
# If we're optimizing the palette, we're going to be
|
||||
# in a rawmode of P anyway.
|
||||
rawmode = 'P'
|
||||
|
||||
header, usedPaletteColors = getheader(imOut, palette, im.encoderinfo)
|
||||
for s in header:
|
||||
fp.write(s)
|
||||
|
||||
flags = 0
|
||||
|
||||
try:
|
||||
interlace = im.encoderinfo["interlace"]
|
||||
except KeyError:
|
||||
interlace = 1
|
||||
|
||||
# workaround for @PIL153
|
||||
if min(im.size) < 16:
|
||||
interlace = 0
|
||||
|
||||
if interlace:
|
||||
flags = flags | 64
|
||||
|
||||
try:
|
||||
transparency = im.encoderinfo["transparency"]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
transparency = int(transparency)
|
||||
# optimize the block away if transparent color is not used
|
||||
transparentColorExists = True
|
||||
# adjust the transparency index after optimize
|
||||
if usedPaletteColors is not None and len(usedPaletteColors) < 256:
|
||||
for i in range(len(usedPaletteColors)):
|
||||
if usedPaletteColors[i] == transparency:
|
||||
transparency = i
|
||||
transparentColorExists = True
|
||||
break
|
||||
else:
|
||||
transparentColorExists = False
|
||||
|
||||
# transparency extension block
|
||||
if transparentColorExists:
|
||||
fp.write(b"!" +
|
||||
o8(249) + # extension intro
|
||||
o8(4) + # length
|
||||
o8(1) + # transparency info present
|
||||
o16(0) + # duration
|
||||
o8(transparency) # transparency index
|
||||
+ o8(0))
|
||||
|
||||
# local image header
|
||||
fp.write(b"," +
|
||||
o16(0) + o16(0) + # bounding box
|
||||
o16(im.size[0]) + # size
|
||||
o16(im.size[1]) +
|
||||
o8(flags) + # flags
|
||||
o8(8)) # bits
|
||||
|
||||
imOut.encoderconfig = (8, interlace)
|
||||
ImageFile._save(imOut, fp, [("gif", (0,0)+im.size, 0, rawmode)])
|
||||
|
||||
fp.write(b"\0") # end of image data
|
||||
|
||||
fp.write(b";") # end of file
|
||||
|
||||
try:
|
||||
fp.flush()
|
||||
except: pass
|
||||
|
||||
|
||||
def _save_netpbm(im, fp, filename):
|
||||
|
||||
#
|
||||
# If you need real GIF compression and/or RGB quantization, you
|
||||
# can use the external NETPBM/PBMPLUS utilities. See comments
|
||||
# below for information on how to enable this.
|
||||
|
||||
import os
|
||||
from subprocess import Popen, check_call, PIPE, CalledProcessError
|
||||
import tempfile
|
||||
file = im._dump()
|
||||
|
||||
if im.mode != "RGB":
|
||||
with open(filename, 'wb') as f:
|
||||
stderr = tempfile.TemporaryFile()
|
||||
check_call(["ppmtogif", file], stdout=f, stderr=stderr)
|
||||
else:
|
||||
with open(filename, 'wb') as f:
|
||||
|
||||
# Pipe ppmquant output into ppmtogif
|
||||
# "ppmquant 256 %s | ppmtogif > %s" % (file, filename)
|
||||
quant_cmd = ["ppmquant", "256", file]
|
||||
togif_cmd = ["ppmtogif"]
|
||||
stderr = tempfile.TemporaryFile()
|
||||
quant_proc = Popen(quant_cmd, stdout=PIPE, stderr=stderr)
|
||||
stderr = tempfile.TemporaryFile()
|
||||
togif_proc = Popen(togif_cmd, stdin=quant_proc.stdout, stdout=f, stderr=stderr)
|
||||
|
||||
# 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:
|
||||
pass
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# GIF utilities
|
||||
|
||||
def getheader(im, palette=None, info=None):
|
||||
"""Return a list of strings representing a GIF header"""
|
||||
|
||||
optimize = info and info.get("optimize", 0)
|
||||
|
||||
# Header Block
|
||||
# http://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp
|
||||
header = [
|
||||
b"GIF87a" + # signature + version
|
||||
o16(im.size[0]) + # canvas width
|
||||
o16(im.size[1]) # canvas height
|
||||
]
|
||||
|
||||
if im.mode == "P":
|
||||
if palette and isinstance(palette, bytes):
|
||||
sourcePalette = palette[:768]
|
||||
else:
|
||||
sourcePalette = im.im.getpalette("RGB")[:768]
|
||||
else: # L-mode
|
||||
if palette and isinstance(palette, bytes):
|
||||
sourcePalette = palette[:768]
|
||||
else:
|
||||
sourcePalette = bytearray([i//3 for i in range(768)])
|
||||
|
||||
usedPaletteColors = paletteBytes = None
|
||||
|
||||
if optimize:
|
||||
usedPaletteColors = []
|
||||
|
||||
# check which colors are used
|
||||
i = 0
|
||||
for count in im.histogram():
|
||||
if count:
|
||||
usedPaletteColors.append(i)
|
||||
i += 1
|
||||
|
||||
# create the new palette if not every color is used
|
||||
if len(usedPaletteColors) < 256:
|
||||
paletteBytes = b""
|
||||
newPositions = {}
|
||||
|
||||
i = 0
|
||||
# pick only the used colors from the palette
|
||||
for oldPosition in usedPaletteColors:
|
||||
paletteBytes += sourcePalette[oldPosition*3:oldPosition*3+3]
|
||||
newPositions[oldPosition] = i
|
||||
i += 1
|
||||
|
||||
# replace the palette color id of all pixel with the new id
|
||||
imageBytes = bytearray(im.tobytes())
|
||||
for i in range(len(imageBytes)):
|
||||
imageBytes[i] = newPositions[imageBytes[i]]
|
||||
im.frombytes(bytes(imageBytes))
|
||||
newPaletteBytes = paletteBytes + (768 - len(paletteBytes)) * b'\x00'
|
||||
im.putpalette(newPaletteBytes)
|
||||
im.palette = ImagePalette.ImagePalette("RGB", palette = paletteBytes, size = len(paletteBytes))
|
||||
|
||||
if not paletteBytes:
|
||||
paletteBytes = sourcePalette
|
||||
|
||||
# Logical Screen Descriptor
|
||||
# calculate the palette size for the header
|
||||
import math
|
||||
colorTableSize = int(math.ceil(math.log(len(paletteBytes)//3, 2)))-1
|
||||
if colorTableSize < 0: colorTableSize = 0
|
||||
# size of global color table + global color table flag
|
||||
header.append(o8(colorTableSize + 128))
|
||||
# background + reserved/aspect
|
||||
header.append(o8(0) + o8(0))
|
||||
# end of Logical Screen Descriptor
|
||||
|
||||
# add the missing amount of bytes
|
||||
# the palette has to be 2<<n in size
|
||||
actualTargetSizeDiff = (2<<colorTableSize) - len(paletteBytes)//3
|
||||
if actualTargetSizeDiff > 0:
|
||||
paletteBytes += o8(0) * 3 * actualTargetSizeDiff
|
||||
|
||||
# Header + Logical Screen Descriptor + Global Color Table
|
||||
header.append(paletteBytes)
|
||||
return header, usedPaletteColors
|
||||
|
||||
|
||||
def getdata(im, offset = (0, 0), **params):
|
||||
"""Return a list of strings representing this image.
|
||||
The first string is a local image header, the rest contains
|
||||
encoded image data."""
|
||||
|
||||
class collector:
|
||||
data = []
|
||||
def write(self, data):
|
||||
self.data.append(data)
|
||||
|
||||
im.load() # make sure raster data is available
|
||||
|
||||
fp = collector()
|
||||
|
||||
try:
|
||||
im.encoderinfo = params
|
||||
|
||||
# local image header
|
||||
fp.write(b"," +
|
||||
o16(offset[0]) + # offset
|
||||
o16(offset[1]) +
|
||||
o16(im.size[0]) + # size
|
||||
o16(im.size[1]) +
|
||||
o8(0) + # flags
|
||||
o8(8)) # bits
|
||||
|
||||
ImageFile._save(im, fp, [("gif", (0,0)+im.size, 0, RAWMODE[im.mode])])
|
||||
|
||||
fp.write(b"\0") # end of image data
|
||||
|
||||
finally:
|
||||
del im.encoderinfo
|
||||
|
||||
return fp.data
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Registry
|
||||
|
||||
Image.register_open(GifImageFile.format, GifImageFile, _accept)
|
||||
Image.register_save(GifImageFile.format, _save)
|
||||
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)
|
||||
124
PIL/GimpGradientFile.py
Normal file
124
PIL/GimpGradientFile.py
Normal file
@ -0,0 +1,124 @@
|
||||
#
|
||||
# Python Imaging Library
|
||||
# $Id$
|
||||
#
|
||||
# stuff to read (and render) GIMP gradient files
|
||||
#
|
||||
# History:
|
||||
# 97-08-23 fl Created
|
||||
#
|
||||
# Copyright (c) Secret Labs AB 1997.
|
||||
# Copyright (c) Fredrik Lundh 1997.
|
||||
#
|
||||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
from math import pi, log, sin, sqrt
|
||||
from PIL._binary import o8
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Stuff to translate curve segments to palette values (derived from
|
||||
# the corresponding code in GIMP, written by Federico Mena Quintero.
|
||||
# See the GIMP distribution for more information.)
|
||||
#
|
||||
|
||||
EPSILON = 1e-10
|
||||
|
||||
def linear(middle, pos):
|
||||
if pos <= middle:
|
||||
if middle < EPSILON:
|
||||
return 0.0
|
||||
else:
|
||||
return 0.5 * pos / middle
|
||||
else:
|
||||
pos = pos - middle
|
||||
middle = 1.0 - middle
|
||||
if middle < EPSILON:
|
||||
return 1.0
|
||||
else:
|
||||
return 0.5 + 0.5 * pos / middle
|
||||
|
||||
def curved(middle, pos):
|
||||
return pos ** (log(0.5) / log(max(middle, EPSILON)))
|
||||
|
||||
def sine(middle, pos):
|
||||
return (sin((-pi / 2.0) + pi * linear(middle, pos)) + 1.0) / 2.0
|
||||
|
||||
def sphere_increasing(middle, pos):
|
||||
return sqrt(1.0 - (linear(middle, pos) - 1.0) ** 2)
|
||||
|
||||
def sphere_decreasing(middle, pos):
|
||||
return 1.0 - sqrt(1.0 - linear(middle, pos) ** 2)
|
||||
|
||||
SEGMENTS = [ linear, curved, sine, sphere_increasing, sphere_decreasing ]
|
||||
|
||||
class GradientFile:
|
||||
|
||||
gradient = None
|
||||
|
||||
def getpalette(self, entries = 256):
|
||||
|
||||
palette = []
|
||||
|
||||
ix = 0
|
||||
x0, x1, xm, rgb0, rgb1, segment = self.gradient[ix]
|
||||
|
||||
for i in range(entries):
|
||||
|
||||
x = i / float(entries-1)
|
||||
|
||||
while x1 < x:
|
||||
ix += 1
|
||||
x0, x1, xm, rgb0, rgb1, segment = self.gradient[ix]
|
||||
|
||||
w = x1 - x0
|
||||
|
||||
if w < EPSILON:
|
||||
scale = segment(0.5, 0.5)
|
||||
else:
|
||||
scale = segment((xm - x0) / w, (x - x0) / w)
|
||||
|
||||
# expand to RGBA
|
||||
r = o8(int(255 * ((rgb1[0] - rgb0[0]) * scale + rgb0[0]) + 0.5))
|
||||
g = o8(int(255 * ((rgb1[1] - rgb0[1]) * scale + rgb0[1]) + 0.5))
|
||||
b = o8(int(255 * ((rgb1[2] - rgb0[2]) * scale + rgb0[2]) + 0.5))
|
||||
a = o8(int(255 * ((rgb1[3] - rgb0[3]) * scale + rgb0[3]) + 0.5))
|
||||
|
||||
# add to palette
|
||||
palette.append(r + g + b + a)
|
||||
|
||||
return b"".join(palette), "RGBA"
|
||||
|
||||
##
|
||||
# File handler for GIMP's gradient format.
|
||||
|
||||
class GimpGradientFile(GradientFile):
|
||||
|
||||
def __init__(self, fp):
|
||||
|
||||
if fp.readline()[:13] != b"GIMP Gradient":
|
||||
raise SyntaxError("not a GIMP gradient file")
|
||||
|
||||
count = int(fp.readline())
|
||||
|
||||
gradient = []
|
||||
|
||||
for i in range(count):
|
||||
|
||||
s = fp.readline().split()
|
||||
w = [float(x) for x in s[:11]]
|
||||
|
||||
x0, x1 = w[0], w[2]
|
||||
xm = w[1]
|
||||
rgb0 = w[3:7]
|
||||
rgb1 = w[7:11]
|
||||
|
||||
segment = SEGMENTS[int(s[11])]
|
||||
cspace = int(s[12])
|
||||
|
||||
if cspace != 0:
|
||||
raise IOError("cannot handle HSV colour space")
|
||||
|
||||
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 PIL._binary import o8
|
||||
|
||||
##
|
||||
# File handler for GIMP's palette format.
|
||||
|
||||
class GimpPaletteFile:
|
||||
|
||||
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(b"\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
|
||||
68
PIL/GribStubImagePlugin.py
Normal file
68
PIL/GribStubImagePlugin.py
Normal file
@ -0,0 +1,68 @@
|
||||
#
|
||||
# The Python Imaging Library
|
||||
# $Id$
|
||||
#
|
||||
# GRIB stub adapter
|
||||
#
|
||||
# Copyright (c) 1996-2003 by Fredrik Lundh
|
||||
#
|
||||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
from PIL import Image, ImageFile
|
||||
|
||||
_handler = None
|
||||
|
||||
##
|
||||
# Install application-specific GRIB image handler.
|
||||
#
|
||||
# @param handler Handler object.
|
||||
|
||||
def register_handler(handler):
|
||||
global _handler
|
||||
_handler = handler
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Image adapter
|
||||
|
||||
def _accept(prefix):
|
||||
return prefix[0:4] == b"GRIB" and prefix[7] == b'\x01'
|
||||
|
||||
class GribStubImageFile(ImageFile.StubImageFile):
|
||||
|
||||
format = "GRIB"
|
||||
format_description = "GRIB"
|
||||
|
||||
def _open(self):
|
||||
|
||||
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
|
||||
|
||||
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("GRIB save handler not installed")
|
||||
_handler.save(im, fp, filename)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Registry
|
||||
|
||||
Image.register_open(GribStubImageFile.format, GribStubImageFile, _accept)
|
||||
Image.register_save(GribStubImageFile.format, _save)
|
||||
|
||||
Image.register_extension(GribStubImageFile.format, ".grib")
|
||||
70
PIL/Hdf5StubImagePlugin.py
Normal file
70
PIL/Hdf5StubImagePlugin.py
Normal file
@ -0,0 +1,70 @@
|
||||
#
|
||||
# The Python Imaging Library
|
||||
# $Id$
|
||||
#
|
||||
# HDF5 stub adapter
|
||||
#
|
||||
# Copyright (c) 2000-2003 by Fredrik Lundh
|
||||
#
|
||||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
from PIL import Image, ImageFile
|
||||
|
||||
_handler = None
|
||||
|
||||
##
|
||||
# Install application-specific HDF5 image handler.
|
||||
#
|
||||
# @param handler Handler object.
|
||||
|
||||
def register_handler(handler):
|
||||
global _handler
|
||||
_handler = handler
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Image adapter
|
||||
|
||||
def _accept(prefix):
|
||||
return prefix[:8] == b"\x89HDF\r\n\x1a\n"
|
||||
|
||||
class HDF5StubImageFile(ImageFile.StubImageFile):
|
||||
|
||||
format = "HDF5"
|
||||
format_description = "HDF5"
|
||||
|
||||
def _open(self):
|
||||
|
||||
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
|
||||
|
||||
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("HDF5 save handler not installed")
|
||||
_handler.save(im, fp, filename)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Registry
|
||||
|
||||
Image.register_open(HDF5StubImageFile.format, HDF5StubImageFile, _accept)
|
||||
Image.register_save(HDF5StubImageFile.format, _save)
|
||||
|
||||
Image.register_extension(HDF5StubImageFile.format, ".h5")
|
||||
Image.register_extension(HDF5StubImageFile.format, ".hdf")
|
||||
301
PIL/IcnsImagePlugin.py
Normal file
301
PIL/IcnsImagePlugin.py
Normal file
@ -0,0 +1,301 @@
|
||||
#
|
||||
# The Python Imaging Library.
|
||||
# $Id$
|
||||
#
|
||||
# Mac OS X 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, _binary
|
||||
import struct, io
|
||||
|
||||
enable_jpeg2k = hasattr(Image.core, 'jp2klib_version')
|
||||
if enable_jpeg2k:
|
||||
from PIL import Jpeg2KImagePlugin
|
||||
|
||||
i8 = _binary.i8
|
||||
|
||||
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, length) = start_length
|
||||
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:
|
||||
|
||||
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 read-only 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()
|
||||
|
||||
Image.register_open("ICNS", IcnsImageFile, lambda x: x[:4] == b'icns')
|
||||
Image.register_extension("ICNS", '.icns')
|
||||
|
||||
if __name__ == '__main__':
|
||||
import os, sys
|
||||
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")
|
||||
233
PIL/IcoImagePlugin.py
Normal file
233
PIL/IcoImagePlugin.py
Normal file
@ -0,0 +1,233 @@
|
||||
#
|
||||
# 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/p/casadebender/wiki/Win32IconImagePlugin
|
||||
#
|
||||
# Icon format references:
|
||||
# * http://en.wikipedia.org/wiki/ICO_(file_format)
|
||||
# * http://msdn.microsoft.com/en-us/library/ms997538.aspx
|
||||
|
||||
|
||||
__version__ = "0.1"
|
||||
|
||||
from PIL import Image, ImageFile, BmpImagePlugin, PngImagePlugin, _binary
|
||||
from math import log, ceil
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
i8 = _binary.i8
|
||||
i16 = _binary.i16le
|
||||
i32 = _binary.i32le
|
||||
|
||||
_MAGIC = b"\0\0\1\0"
|
||||
|
||||
def _accept(prefix):
|
||||
return prefix[:4] == _MAGIC
|
||||
|
||||
|
||||
class IcoFile:
|
||||
def __init__(self, buf):
|
||||
"""
|
||||
Parse image from file-like object containing ico file data
|
||||
"""
|
||||
|
||||
# check magic
|
||||
s = buf.read(6)
|
||||
if not _accept(s):
|
||||
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]), # Number 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 set((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 == 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)
|
||||
maskData = self.buf.read(total_bytes)
|
||||
|
||||
# convert raw data to image
|
||||
mask = Image.frombuffer(
|
||||
'1', # 1 bpp
|
||||
im.size, # (w, h)
|
||||
maskData, # 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/p/casadebender/wiki/Win32IconImagePlugin
|
||||
"""
|
||||
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):
|
||||
# Flage the ImageFile.Parser so that it just does all the decode at the end.
|
||||
pass
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
Image.register_open("ICO", IcoImageFile, _accept)
|
||||
Image.register_extension("ICO", ".ico")
|
||||
@ -24,14 +24,14 @@
|
||||
#
|
||||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
__version__ = "0.7"
|
||||
|
||||
import re
|
||||
from typing import IO, Any
|
||||
from PIL import Image, ImageFile, ImagePalette
|
||||
from PIL._binary import i8, o8
|
||||
|
||||
from . import Image, ImageFile, ImagePalette
|
||||
from ._util import DeferredError
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# 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,57 +69,52 @@ 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:
|
||||
return float(s)
|
||||
|
||||
|
||||
##
|
||||
# Image plugin for the IFUNC IM file format.
|
||||
|
||||
|
||||
class ImImageFile(ImageFile.ImageFile):
|
||||
|
||||
format = "IM"
|
||||
format_description = "IFUNC Image Memory"
|
||||
_close_exclusive_fp_after_loading = False
|
||||
|
||||
def _open(self) -> None:
|
||||
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)
|
||||
if not b"\n" in self.fp.read(100):
|
||||
raise SyntaxError("not an IM file")
|
||||
self.fp.seek(0)
|
||||
|
||||
n = 0
|
||||
@ -141,40 +127,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, v = m.group(1,2)
|
||||
|
||||
# Don't know if this is the correct encoding, but a decent guess
|
||||
# (I guess)
|
||||
k = k.decode('latin-1', 'replace')
|
||||
v = v.decode('latin-1', 'replace')
|
||||
|
||||
# Convert value as appropriate
|
||||
if k in [FRAMES, SCALE, SIZE]:
|
||||
@ -199,67 +185,62 @@ 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
|
||||
palette = self.fp.read(768)
|
||||
greyscale = 1 # greyscale palette
|
||||
linear = 1 # linear greyscale palette
|
||||
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 +249,20 @@ 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:
|
||||
return self.info[FRAMES]
|
||||
def seek(self, frame):
|
||||
|
||||
@property
|
||||
def is_animated(self) -> bool:
|
||||
return self.info[FRAMES] > 1
|
||||
if frame < 0 or frame >= self.info[FRAMES]:
|
||||
raise EOFError("seek outside sequence")
|
||||
|
||||
def seek(self, frame: int) -> None:
|
||||
if not self._seek_check(frame):
|
||||
if self.frame == frame:
|
||||
return
|
||||
if isinstance(self._fp, DeferredError):
|
||||
raise self._fp.ex
|
||||
|
||||
self.frame = frame
|
||||
|
||||
@ -305,21 +274,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,55 +302,41 @@ SAVE = {
|
||||
"RGBA": ("RGBA", "RGBA;L"),
|
||||
"RGBX": ("RGBX", "RGBX;L"),
|
||||
"CMYK": ("CMYK", "CMYK;L"),
|
||||
"YCbCr": ("YCC", "YCbCr;L"),
|
||||
"YCbCr": ("YCC", "YCbCr;L")
|
||||
}
|
||||
|
||||
def _save(im, fp, filename, check=0):
|
||||
|
||||
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
||||
try:
|
||||
image_type, rawmode = SAVE[im.mode]
|
||||
except KeyError as e:
|
||||
msg = f"Cannot save {im.mode} images as IM"
|
||||
raise ValueError(msg) from e
|
||||
type, rawmode = SAVE[im.mode]
|
||||
except KeyError:
|
||||
raise ValueError("Cannot save %s images as IM" % im.mode)
|
||||
|
||||
frames = im.encoderinfo.get("frames", 1)
|
||||
try:
|
||||
frames = im.encoderinfo["frames"]
|
||||
except KeyError:
|
||||
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" % 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("IM", ImImageFile)
|
||||
Image.register_save("IM", _save)
|
||||
|
||||
Image.register_open(ImImageFile.format, ImImageFile)
|
||||
Image.register_save(ImImageFile.format, _save)
|
||||
|
||||
Image.register_extension(ImImageFile.format, ".im")
|
||||
Image.register_extension("IM", ".im")
|
||||
2419
PIL/Image.py
Normal file
2419
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
|
||||
from PIL 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,157 +16,114 @@
|
||||
#
|
||||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
from __future__ import annotations
|
||||
|
||||
from PIL import Image
|
||||
import re
|
||||
from functools import lru_cache
|
||||
|
||||
from . import Image
|
||||
|
||||
|
||||
@lru_cache
|
||||
def getrgb(color: str) -> tuple[int, int, int] | tuple[int, int, int, int]:
|
||||
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)
|
||||
try:
|
||||
rgb = colormap[color]
|
||||
except KeyError:
|
||||
try:
|
||||
# fall back on case-insensitive lookup
|
||||
rgb = colormap[color.lower()]
|
||||
except KeyError:
|
||||
rgb = None
|
||||
# found color in cache
|
||||
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):
|
||||
m = re.match("#\w\w\w$", color)
|
||||
if m:
|
||||
return (
|
||||
int(color[1] * 2, 16),
|
||||
int(color[2] * 2, 16),
|
||||
int(color[3] * 2, 16),
|
||||
int(color[4] * 2, 16),
|
||||
)
|
||||
|
||||
if re.match("#[a-f0-9]{6}$", color):
|
||||
return int(color[1:3], 16), int(color[3:5], 16), int(color[5:7], 16)
|
||||
|
||||
if re.match("#[a-f0-9]{8}$", color):
|
||||
int(color[1]*2, 16),
|
||||
int(color[2]*2, 16),
|
||||
int(color[3]*2, 16)
|
||||
)
|
||||
m = re.match("#\w\w\w\w\w\w$", color)
|
||||
if m:
|
||||
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)
|
||||
int(color[5:7], 16)
|
||||
)
|
||||
m = re.match("rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$", color)
|
||||
if m:
|
||||
return int(m.group(1)), int(m.group(2)), int(m.group(3))
|
||||
|
||||
m = re.match(r"rgb\(\s*(\d+)%\s*,\s*(\d+)%\s*,\s*(\d+)%\s*\)$", color)
|
||||
return (
|
||||
int(m.group(1)),
|
||||
int(m.group(2)),
|
||||
int(m.group(3))
|
||||
)
|
||||
m = re.match("rgb\(\s*(\d+)%\s*,\s*(\d+)%\s*,\s*(\d+)%\s*\)$", color)
|
||||
if m:
|
||||
return (
|
||||
int((int(m.group(1)) * 255) / 100.0 + 0.5),
|
||||
int((int(m.group(2)) * 255) / 100.0 + 0.5),
|
||||
int((int(m.group(3)) * 255) / 100.0 + 0.5),
|
||||
)
|
||||
|
||||
m = re.match(
|
||||
r"hsl\(\s*(\d+\.?\d*)\s*,\s*(\d+\.?\d*)%\s*,\s*(\d+\.?\d*)%\s*\)$", color
|
||||
)
|
||||
int((int(m.group(3)) * 255) / 100.0 + 0.5)
|
||||
)
|
||||
m = re.match("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),
|
||||
)
|
||||
|
||||
m = re.match(
|
||||
r"hs[bv]\(\s*(\d+\.?\d*)\s*,\s*(\d+\.?\d*)%\s*,\s*(\d+\.?\d*)%\s*\)$", color
|
||||
)
|
||||
int(rgb[0] * 255 + 0.5),
|
||||
int(rgb[1] * 255 + 0.5),
|
||||
int(rgb[2] * 255 + 0.5)
|
||||
)
|
||||
m = re.match("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),
|
||||
)
|
||||
int(m.group(1)),
|
||||
int(m.group(2)),
|
||||
int(m.group(3)),
|
||||
int(m.group(4))
|
||||
)
|
||||
raise ValueError("unknown color specifier: %r" % color)
|
||||
|
||||
m = re.match(r"rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$", color)
|
||||
if m:
|
||||
return int(m.group(1)), int(m.group(2)), int(m.group(3)), int(m.group(4))
|
||||
msg = f"unknown color specifier: {repr(color)}"
|
||||
raise ValueError(msg)
|
||||
|
||||
|
||||
@lru_cache
|
||||
def getcolor(color: str, mode: str) -> int | tuple[int, ...]:
|
||||
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]] = {
|
||||
# X11 colour table from https://drafts.csswg.org/css-color-4/, with
|
||||
colormap = {
|
||||
# X11 colour table (from "CSS3 module: Color working draft"), with
|
||||
# gray/grey spelling issues fixed. This is a superset of HTML 4.0
|
||||
# colour names used in CSS 1.
|
||||
"aliceblue": "#f0f8ff",
|
||||
@ -288,7 +245,6 @@ colormap: dict[str, str | tuple[int, int, int]] = {
|
||||
"plum": "#dda0dd",
|
||||
"powderblue": "#b0e0e6",
|
||||
"purple": "#800080",
|
||||
"rebeccapurple": "#663399",
|
||||
"red": "#ff0000",
|
||||
"rosybrown": "#bc8f8f",
|
||||
"royalblue": "#4169e1",
|
||||
379
PIL/ImageDraw.py
Normal file
379
PIL/ImageDraw.py
Normal file
@ -0,0 +1,379 @@
|
||||
#
|
||||
# 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 PIL import Image, ImageColor
|
||||
from PIL._util import isStringType
|
||||
|
||||
try:
|
||||
import warnings
|
||||
except ImportError:
|
||||
warnings = None
|
||||
|
||||
##
|
||||
# A simple 2D drawing interface for PIL images.
|
||||
# <p>
|
||||
# Application code should use the <b>Draw</b> factory, instead of
|
||||
# directly.
|
||||
|
||||
class ImageDraw:
|
||||
|
||||
##
|
||||
# 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.
|
||||
|
||||
def __init__(self, im, mode=None):
|
||||
im.load()
|
||||
if im.readonly:
|
||||
im._copy() # make it writable
|
||||
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
|
||||
|
||||
##
|
||||
# Set the default pen color.
|
||||
|
||||
def setink(self, ink):
|
||||
# compatibility
|
||||
if warnings:
|
||||
warnings.warn(
|
||||
"'setink' is deprecated; use keyword arguments instead",
|
||||
DeprecationWarning, stacklevel=2
|
||||
)
|
||||
if isStringType(ink):
|
||||
ink = ImageColor.getcolor(ink, self.mode)
|
||||
if self.palette and not isinstance(ink, numbers.Number):
|
||||
ink = self.palette.getcolor(ink)
|
||||
self.ink = self.draw.draw_ink(ink, self.mode)
|
||||
|
||||
##
|
||||
# Set the default background color.
|
||||
|
||||
def setfill(self, onoff):
|
||||
# compatibility
|
||||
if warnings:
|
||||
warnings.warn(
|
||||
"'setfill' is deprecated; use keyword arguments instead",
|
||||
DeprecationWarning, stacklevel=2
|
||||
)
|
||||
self.fill = onoff
|
||||
|
||||
##
|
||||
# Set the default font.
|
||||
|
||||
def setfont(self, font):
|
||||
# compatibility
|
||||
self.font = font
|
||||
|
||||
##
|
||||
# Get the current default font.
|
||||
|
||||
def getfont(self):
|
||||
if not self.font:
|
||||
# FIXME: should add a font repository
|
||||
from PIL 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
|
||||
|
||||
##
|
||||
# Draw an arc.
|
||||
|
||||
def arc(self, xy, start, end, fill=None):
|
||||
ink, fill = self._getink(fill)
|
||||
if ink is not None:
|
||||
self.draw.draw_arc(xy, start, end, ink)
|
||||
|
||||
##
|
||||
# Draw a bitmap.
|
||||
|
||||
def bitmap(self, xy, bitmap, fill=None):
|
||||
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)
|
||||
|
||||
##
|
||||
# Draw a chord.
|
||||
|
||||
def chord(self, xy, start, end, fill=None, outline=None):
|
||||
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)
|
||||
|
||||
##
|
||||
# Draw an ellipse.
|
||||
|
||||
def ellipse(self, xy, fill=None, outline=None):
|
||||
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)
|
||||
|
||||
##
|
||||
# Draw a line, or a connected sequence of line segments.
|
||||
|
||||
def line(self, xy, fill=None, width=0):
|
||||
ink, fill = self._getink(fill)
|
||||
if ink is not None:
|
||||
self.draw.draw_lines(xy, ink, width)
|
||||
|
||||
##
|
||||
# (Experimental) Draw a shape.
|
||||
|
||||
def shape(self, shape, fill=None, outline=None):
|
||||
# experimental
|
||||
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)
|
||||
|
||||
##
|
||||
# Draw a pieslice.
|
||||
|
||||
def pieslice(self, xy, start, end, fill=None, outline=None):
|
||||
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)
|
||||
|
||||
##
|
||||
# Draw one or more individual pixels.
|
||||
|
||||
def point(self, xy, fill=None):
|
||||
ink, fill = self._getink(fill)
|
||||
if ink is not None:
|
||||
self.draw.draw_points(xy, ink)
|
||||
|
||||
##
|
||||
# Draw a polygon.
|
||||
|
||||
def polygon(self, xy, fill=None, outline=None):
|
||||
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)
|
||||
|
||||
##
|
||||
# Draw a rectangle.
|
||||
|
||||
def rectangle(self, xy, fill=None, outline=None):
|
||||
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)
|
||||
|
||||
##
|
||||
# Draw text.
|
||||
|
||||
def text(self, xy, text, fill=None, font=None, anchor=None):
|
||||
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)
|
||||
|
||||
##
|
||||
# Get the size of a given string, in pixels.
|
||||
|
||||
def textsize(self, text, font=None):
|
||||
if font is None:
|
||||
font = self.getfont()
|
||||
return font.getsize(text)
|
||||
|
||||
##
|
||||
# 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.
|
||||
|
||||
def Draw(im, mode=None):
|
||||
try:
|
||||
return im.getdraw(mode)
|
||||
except AttributeError:
|
||||
return ImageDraw(im, mode)
|
||||
|
||||
# experimental access to the outline API
|
||||
try:
|
||||
Outline = Image.core.outline
|
||||
except:
|
||||
Outline = 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.
|
||||
# @return A (drawing context, drawing resource factory) tuple.
|
||||
|
||||
def getdraw(im=None, hints=None):
|
||||
# FIXME: this needs more work!
|
||||
# FIXME: come up with a better 'hints' scheme.
|
||||
handler = None
|
||||
if not hints or "nicest" in hints:
|
||||
try:
|
||||
from PIL import _imagingagg as handler
|
||||
except ImportError:
|
||||
pass
|
||||
if handler is None:
|
||||
from PIL import ImageDraw2 as handler
|
||||
if im:
|
||||
im = handler.Draw(im)
|
||||
return im, handler
|
||||
|
||||
##
|
||||
# (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.
|
||||
|
||||
def floodfill(image, xy, value, border=None):
|
||||
"Fill bounded region."
|
||||
# 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 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
|
||||
106
PIL/ImageDraw2.py
Normal file
106
PIL/ImageDraw2.py
Normal file
@ -0,0 +1,106 @@
|
||||
#
|
||||
# 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 PIL import Image, ImageColor, ImageDraw, ImageFont, ImagePath
|
||||
|
||||
class Pen:
|
||||
def __init__(self, color, width=1, opacity=255):
|
||||
self.color = ImageColor.getrgb(color)
|
||||
self.width = width
|
||||
|
||||
class Brush:
|
||||
def __init__(self, color, opacity=255):
|
||||
self.color = ImageColor.getrgb(color)
|
||||
|
||||
class Font:
|
||||
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:
|
||||
|
||||
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
|
||||
from PIL import Image, ImageFilter, ImageStat
|
||||
|
||||
|
||||
class _Enhance:
|
||||
image: Image.Image
|
||||
degenerate: Image.Image
|
||||
|
||||
def enhance(self, factor: float) -> Image.Image:
|
||||
def enhance(self, factor):
|
||||
"""
|
||||
Returns an enhanced image.
|
||||
|
||||
@ -48,16 +45,9 @@ 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"
|
||||
|
||||
if self.intermediate_mode != image.mode:
|
||||
image = image.convert(self.intermediate_mode).convert(image.mode)
|
||||
self.degenerate = image
|
||||
self.degenerate = image.convert("L").convert(image.mode)
|
||||
|
||||
|
||||
class Contrast(_Enhance):
|
||||
@ -65,37 +55,25 @@ 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)
|
||||
|
||||
if "A" in self.image.getbands():
|
||||
self.degenerate.putalpha(self.image.getchannel("A"))
|
||||
mean = int(ImageStat.Stat(image.convert("L")).mean[0] + 0.5)
|
||||
self.degenerate = Image.new("L", image.size, mean).convert(image.mode)
|
||||
|
||||
|
||||
class Brightness(_Enhance):
|
||||
"""Adjust image brightness.
|
||||
|
||||
This class can be used to control the brightness of an image. An
|
||||
This class can be used to control the brighntess of an image. An
|
||||
enhancement factor of 0.0 gives a black image. A factor of 1.0 gives the
|
||||
original image.
|
||||
"""
|
||||
|
||||
def __init__(self, image: Image.Image) -> None:
|
||||
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"))
|
||||
|
||||
|
||||
class Sharpness(_Enhance):
|
||||
"""Adjust image sharpness.
|
||||
@ -104,10 +82,6 @@ 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"))
|
||||
506
PIL/ImageFile.py
Normal file
506
PIL/ImageFile.py
Normal file
@ -0,0 +1,506 @@
|
||||
#
|
||||
# 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 PIL import Image
|
||||
from PIL._util import isPath
|
||||
import traceback, os, sys
|
||||
import io
|
||||
|
||||
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
|
||||
else:
|
||||
# stream
|
||||
self.fp = fp
|
||||
self.filename = filename
|
||||
|
||||
try:
|
||||
self._open()
|
||||
except IndexError as v: # end of data
|
||||
if Image.DEBUG > 1:
|
||||
traceback.print_exc()
|
||||
raise SyntaxError(v)
|
||||
except TypeError as v: # end of data (ord)
|
||||
if Image.DEBUG > 1:
|
||||
traceback.print_exc()
|
||||
raise SyntaxError(v)
|
||||
except KeyError as v: # unsupported mode
|
||||
if Image.DEBUG > 1:
|
||||
traceback.print_exc()
|
||||
raise SyntaxError(v)
|
||||
except EOFError as v: # got header but not the first frame
|
||||
if Image.DEBUG > 1:
|
||||
traceback.print_exc()
|
||||
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.
|
||||
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
|
||||
|
||||
readonly = 0
|
||||
|
||||
if self.filename and len(self.tile) == 1 and not hasattr(sys, 'pypy_version_info'):
|
||||
# As of pypy 2.1.0, memory mapping was failing here.
|
||||
# try memory mapping
|
||||
d, e, o, a = self.tile[0]
|
||||
if d == "raw" and a[0] == self.mode and a[0] in Image._MAPMODES:
|
||||
try:
|
||||
if hasattr(Image.core, "map"):
|
||||
# use built-in mapper
|
||||
self.map = Image.core.map(self.filename)
|
||||
self.map.seek(o)
|
||||
self.im = self.map.readimage(
|
||||
self.mode, self.size, a[1], a[2]
|
||||
)
|
||||
else:
|
||||
# use mmap, if possible
|
||||
import mmap
|
||||
file = open(self.filename, "r+")
|
||||
size = os.path.getsize(self.filename)
|
||||
# FIXME: on Unix, use PROT_READ etc
|
||||
self.map = mmap.mmap(file.fileno(), size)
|
||||
self.im = Image.core.map_buffer(
|
||||
self.map, self.size, d, e, o, a
|
||||
)
|
||||
readonly = 1
|
||||
except (AttributeError, EnvironmentError, ImportError):
|
||||
self.map = None
|
||||
|
||||
self.load_prepare()
|
||||
|
||||
# look for read/seek overrides
|
||||
try:
|
||||
read = self.load_read
|
||||
except AttributeError:
|
||||
read = self.fp.read
|
||||
|
||||
try:
|
||||
seek = self.load_seek
|
||||
except AttributeError:
|
||||
seek = self.fp.seek
|
||||
|
||||
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 d, e, o, a in self.tile:
|
||||
d = Image._getdecoder(self.mode, d, a, self.decoderconfig)
|
||||
seek(o)
|
||||
try:
|
||||
d.setimage(self.im, e)
|
||||
except ValueError:
|
||||
continue
|
||||
b = prefix
|
||||
t = len(b)
|
||||
while True:
|
||||
try:
|
||||
s = read(self.decodermaxblock)
|
||||
except IndexError as ie: # truncated png/gif
|
||||
if LOAD_TRUNCATED_IMAGES:
|
||||
break
|
||||
else:
|
||||
raise IndexError(ie)
|
||||
|
||||
if not s and not d.handles_eof: # 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.
|
||||
d.cleanup()
|
||||
|
||||
if LOAD_TRUNCATED_IMAGES:
|
||||
break
|
||||
else:
|
||||
raise IOError("image file is truncated (%d bytes not processed)" % len(b))
|
||||
|
||||
b = b + s
|
||||
n, e = d.decode(b)
|
||||
if n < 0:
|
||||
break
|
||||
b = b[n:]
|
||||
t = t + n
|
||||
|
||||
self.tile = []
|
||||
self.readonly = readonly
|
||||
|
||||
self.fp = None # might be shared
|
||||
|
||||
if not self.map and (not LOAD_TRUNCATED_IMAGES or t == 0) and e < 0:
|
||||
# still raised if decoder fails to return anything
|
||||
raise_ioerror(e)
|
||||
|
||||
# post processing
|
||||
if hasattr(self, "tile_post_rotate"):
|
||||
# FIXME: This is a hack to handle rotated PCD's
|
||||
self.im = self.im.rotate(self.tile_post_rotate)
|
||||
self.size = self.im.size
|
||||
|
||||
self.load_end()
|
||||
|
||||
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:
|
||||
"""
|
||||
Incremental image parser. This class implements the standard
|
||||
feed/close consumer interface.
|
||||
|
||||
In Python 2.x, this is an old-style class.
|
||||
"""
|
||||
incremental = None
|
||||
image = None
|
||||
data = None
|
||||
decoder = None
|
||||
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 specifiy what it needs
|
||||
# But, it would need at least the image size in most cases. RawEncode is
|
||||
# a tricky case.
|
||||
bufsize = max(MAXBLOCK, bufsize, im.size[0] * 4) # see RawEncode.c
|
||||
try:
|
||||
fh = fp.fileno()
|
||||
fp.flush()
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
s = e.encode_to_file(fh, bufsize)
|
||||
if s < 0:
|
||||
raise IOError("encoder error %d when writing image file" % s)
|
||||
try:
|
||||
fp.flush()
|
||||
except: pass
|
||||
|
||||
|
||||
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)
|
||||
40
PIL/ImageFileIO.py
Normal file
40
PIL/ImageFileIO.py
Normal file
@ -0,0 +1,40 @@
|
||||
#
|
||||
# The Python Imaging Library.
|
||||
# $Id$
|
||||
#
|
||||
# kludge to get basic ImageFileIO functionality
|
||||
#
|
||||
# History:
|
||||
# 1998-08-06 fl Recreated
|
||||
#
|
||||
# Copyright (c) Secret Labs AB 1998-2002.
|
||||
#
|
||||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
"""
|
||||
The **ImageFileIO** module can be used to read an image from a
|
||||
socket, or any other stream device.
|
||||
|
||||
Deprecated. New code should use the :class:`PIL.ImageFile.Parser`
|
||||
class in the :mod:`PIL.ImageFile` module instead.
|
||||
|
||||
.. seealso:: modules :class:`PIL.ImageFile.Parser`
|
||||
"""
|
||||
|
||||
from io import BytesIO
|
||||
|
||||
|
||||
class ImageFileIO(BytesIO):
|
||||
def __init__(self, fp):
|
||||
"""
|
||||
Adds buffering to a stream file object, in order to
|
||||
provide **seek** and **tell** methods required
|
||||
by the :func:`PIL.Image.Image.open` method. The stream object must
|
||||
implement **read** and **close** methods.
|
||||
|
||||
:param fp: Stream file handle.
|
||||
|
||||
.. seealso:: modules :func:`PIL.Image.open`
|
||||
"""
|
||||
data = fp.read()
|
||||
BytesIO.__init__(self, data)
|
||||
269
PIL/ImageFilter.py
Normal file
269
PIL/ImageFilter.py
Normal file
@ -0,0 +1,269 @@
|
||||
#
|
||||
# The Python Imaging Library.
|
||||
# $Id$
|
||||
#
|
||||
# standard filters
|
||||
#
|
||||
# History:
|
||||
# 1995-11-27 fl Created
|
||||
# 2002-06-08 fl Added rank and mode filters
|
||||
# 2003-09-15 fl Fixed rank calculation in rank filter; added expand call
|
||||
#
|
||||
# Copyright (c) 1997-2003 by Secret Labs AB.
|
||||
# Copyright (c) 1995-2002 by Fredrik Lundh.
|
||||
#
|
||||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
from functools import reduce
|
||||
|
||||
|
||||
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 = 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.
|
||||
|
||||
.. _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
|
||||
)
|
||||
406
PIL/ImageFont.py
Normal file
406
PIL/ImageFont.py
Normal file
@ -0,0 +1,406 @@
|
||||
#
|
||||
# The Python Imaging Library.
|
||||
# $Id$
|
||||
#
|
||||
# PIL raster font management
|
||||
#
|
||||
# History:
|
||||
# 1996-08-07 fl created (experimental)
|
||||
# 1997-08-25 fl minor adjustments to handle fonts from pilfont 0.3
|
||||
# 1999-02-06 fl rewrote most font management stuff in C
|
||||
# 1999-03-17 fl take pth files into account in load_path (from Richard Jones)
|
||||
# 2001-02-17 fl added freetype support
|
||||
# 2001-05-09 fl added TransposedFont wrapper class
|
||||
# 2002-03-04 fl make sure we have a "L" or "1" font
|
||||
# 2002-12-04 fl skip non-directory entries in the system path
|
||||
# 2003-04-29 fl add embedded default font
|
||||
# 2003-09-27 fl added support for truetype charmap encodings
|
||||
#
|
||||
# Todo:
|
||||
# Adapt to PILFONT2 format (16-bit fonts, compressed, single file)
|
||||
#
|
||||
# Copyright (c) 1997-2003 by Secret Labs AB
|
||||
# Copyright (c) 1996-2003 by Fredrik Lundh
|
||||
#
|
||||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
from PIL import Image
|
||||
from PIL._util import isDirectory, isPath
|
||||
import os, sys
|
||||
|
||||
try:
|
||||
import warnings
|
||||
except ImportError:
|
||||
warnings = None
|
||||
|
||||
class _imagingft_not_installed:
|
||||
# module placeholder
|
||||
def __getattr__(self, id):
|
||||
raise ImportError("The _imagingft C module is not installed")
|
||||
|
||||
try:
|
||||
from PIL 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:
|
||||
"PIL font wrapper"
|
||||
|
||||
def _load_pilfont(self, filename):
|
||||
|
||||
file = open(filename, "rb")
|
||||
|
||||
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(file, image)
|
||||
|
||||
def _load_pilfont_data(self, file, image):
|
||||
|
||||
# read PILfont header
|
||||
if file.readline() != b"PILfont\n":
|
||||
raise SyntaxError("Not a PILfont file")
|
||||
d = 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:
|
||||
"FreeType font wrapper (requires _imagingft service)"
|
||||
|
||||
def __init__(self, font=None, size=10, index=0, encoding="", file=None):
|
||||
# FIXME: use service provider instead
|
||||
if file:
|
||||
if warnings:
|
||||
warnings.warn('file parameter deprecated, please use font parameter instead.', DeprecationWarning)
|
||||
font = file
|
||||
|
||||
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):
|
||||
return self.font.getsize(text)[0]
|
||||
|
||||
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
|
||||
|
||||
##
|
||||
# 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.
|
||||
|
||||
class TransposedFont:
|
||||
"Wrapper for writing rotated or mirrored text"
|
||||
|
||||
def __init__(self, font, orientation=None):
|
||||
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="", filename=None):
|
||||
"""
|
||||
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 filename: 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.
|
||||
"""
|
||||
|
||||
if filename:
|
||||
if warnings:
|
||||
warnings.warn('filename parameter deprecated, please use font parameter instead.', DeprecationWarning)
|
||||
font = filename
|
||||
|
||||
try:
|
||||
return FreeTypeFont(font, size, index, encoding)
|
||||
except IOError:
|
||||
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:
|
||||
filename = os.path.join(windir, "fonts", font)
|
||||
return FreeTypeFont(filename, 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 dir in sys.path:
|
||||
if isDirectory(dir):
|
||||
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(dir, 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.decodestring(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.decodestring(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
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# create font data chunk for embedding
|
||||
import base64, os, sys
|
||||
font = "../Tests/images/courB08"
|
||||
print(" f._load_pilfont_data(")
|
||||
print(" # %s" % os.path.basename(font))
|
||||
print(" BytesIO(base64.decodestring(b'''")
|
||||
base64.encode(open(font + ".pil", "rb"), sys.stdout)
|
||||
print("''')), Image.open(BytesIO(base64.decodestring(b'''")
|
||||
base64.encode(open(font + ".pbm", "rb"), sys.stdout)
|
||||
print("'''))))")
|
||||
49
PIL/ImageGrab.py
Normal file
49
PIL/ImageGrab.py
Normal file
@ -0,0 +1,49 @@
|
||||
#
|
||||
# The Python Imaging Library
|
||||
# $Id$
|
||||
#
|
||||
# screen grabber (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 PIL import Image
|
||||
|
||||
|
||||
try:
|
||||
# built-in driver (1.1.3 and later)
|
||||
grabber = Image.core.grabscreen
|
||||
except AttributeError:
|
||||
# stand-alone driver (pil plus)
|
||||
import _grabscreen
|
||||
grabber = _grabscreen.grab
|
||||
|
||||
|
||||
def grab(bbox=None):
|
||||
size, data = grabber()
|
||||
im = Image.frombytes(
|
||||
"RGB", size, data,
|
||||
# RGB, 32-bit line padding, origo in lower left corner
|
||||
"raw", "BGR", (size[0]*3 + 3) & -4, -1
|
||||
)
|
||||
if bbox:
|
||||
im = im.crop(bbox)
|
||||
return im
|
||||
|
||||
|
||||
def grabclipboard():
|
||||
debug = 0 # temporary interface
|
||||
data = Image.core.grabclipboard(debug)
|
||||
if isinstance(data, bytes):
|
||||
from PIL import BmpImagePlugin
|
||||
import io
|
||||
return BmpImagePlugin.DibImageFile(io.BytesIO(data))
|
||||
return data
|
||||
227
PIL/ImageMath.py
Normal file
227
PIL/ImageMath.py
Normal file
@ -0,0 +1,227 @@
|
||||
#
|
||||
# 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 PIL import Image
|
||||
from PIL import _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:
|
||||
# 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
|
||||
50
PIL/ImageMode.py
Normal file
50
PIL/ImageMode.py
Normal file
@ -0,0 +1,50 @@
|
||||
#
|
||||
# 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 = {}
|
||||
|
||||
##
|
||||
# Wrapper for mode strings.
|
||||
|
||||
class ModeDescriptor:
|
||||
|
||||
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
|
||||
|
||||
##
|
||||
# Gets a mode descriptor for the given mode.
|
||||
|
||||
def getmode(mode):
|
||||
if not _modes:
|
||||
# initialize mode cache
|
||||
from PIL import Image
|
||||
# core modes
|
||||
for m, (basemode, basetype, bands) in Image._MODEINFO.items():
|
||||
_modes[m] = ModeDescriptor(m, bands, basemode, basetype)
|
||||
# extra experimental modes
|
||||
_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")
|
||||
return _modes[mode]
|
||||
244
PIL/ImageMorph.py
Normal file
244
PIL/ImageMorph.py
Normal file
@ -0,0 +1,244 @@
|
||||
# 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 PIL import Image
|
||||
from PIL import _imagingmorph
|
||||
import re
|
||||
|
||||
LUT_SIZE = 1 << 9
|
||||
|
||||
|
||||
class LutBuilder:
|
||||
"""A class for building a MorphLut from a descriptive language
|
||||
|
||||
The input patterns is a list of a strings sequences like these:
|
||||
|
||||
4:(...
|
||||
.1.
|
||||
111)->1
|
||||
|
||||
(whitespaces including linebreaks are ignored). The option 4
|
||||
describes a series of symmetry operations (in this case a
|
||||
4-rotation), the pattern is described by:
|
||||
|
||||
. or X - Ignore
|
||||
1 - Pixel is on
|
||||
0 - Pixel is off
|
||||
|
||||
The result of the operation is described after "->" string.
|
||||
|
||||
The default is to return the current pixel value, which is
|
||||
returned if no other match is found.
|
||||
|
||||
Operations:
|
||||
4 - 4 way rotation
|
||||
N - Negate
|
||||
1 - Dummy op for no other operation (an op must always be given)
|
||||
M - Mirroring
|
||||
|
||||
Example:
|
||||
|
||||
lb = LutBuilder(patterns = ["4:(... .1. 111)->1"])
|
||||
lut = lb.build_lut()
|
||||
|
||||
"""
|
||||
def __init__(self, patterns=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 in range(len(patterns)):
|
||||
p = patterns[i][0].replace('.', 'X').replace('X', '[01]')
|
||||
p = re.compile(p)
|
||||
patterns[i] = (p, patterns[i][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:
|
||||
"""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')
|
||||
|
||||
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')
|
||||
|
||||
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."""
|
||||
|
||||
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
|
||||
|
||||
# End of file
|
||||
436
PIL/ImageOps.py
Normal file
436
PIL/ImageOps.py
Normal file
@ -0,0 +1,436 @@
|
||||
#
|
||||
# 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 PIL import Image
|
||||
from PIL._util import isStringType
|
||||
import operator
|
||||
from functools import reduce
|
||||
|
||||
#
|
||||
# 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 PIL 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 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: What resampling filter to use.
|
||||
: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 = (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.
|
||||
"""
|
||||
"Add border to 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 posterize.
|
||||
: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
|
||||
209
PIL/ImagePalette.py
Normal file
209
PIL/ImagePalette.py
Normal file
@ -0,0 +1,209 @@
|
||||
#
|
||||
# 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 PIL import Image, ImageColor
|
||||
|
||||
|
||||
class ImagePalette:
|
||||
"Color palette for palette mapped images"
|
||||
|
||||
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 list(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 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'):
|
||||
#py3k has a tobytes, tostring is deprecated.
|
||||
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 = [int(x) for x in 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, mode="RGB"):
|
||||
lut = []
|
||||
for i in range(256):
|
||||
lut.append(int(((i / 255.0) ** exp) * 255.0 + 0.5))
|
||||
return lut
|
||||
|
||||
def new(mode, data):
|
||||
return Image.core.new_palette(mode, data)
|
||||
|
||||
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
|
||||
|
||||
fp = open(filename, "rb")
|
||||
|
||||
lut = None
|
||||
|
||||
if not lut:
|
||||
try:
|
||||
from PIL import GimpPaletteFile
|
||||
fp.seek(0)
|
||||
p = GimpPaletteFile.GimpPaletteFile(fp)
|
||||
lut = p.getpalette()
|
||||
except (SyntaxError, ValueError):
|
||||
#import traceback
|
||||
#traceback.print_exc()
|
||||
pass
|
||||
|
||||
if not lut:
|
||||
try:
|
||||
from PIL import GimpGradientFile
|
||||
fp.seek(0)
|
||||
p = GimpGradientFile.GimpGradientFile(fp)
|
||||
lut = p.getpalette()
|
||||
except (SyntaxError, ValueError):
|
||||
#import traceback
|
||||
#traceback.print_exc()
|
||||
pass
|
||||
|
||||
if not lut:
|
||||
try:
|
||||
from PIL import PaletteFile
|
||||
fp.seek(0)
|
||||
p = PaletteFile.PaletteFile(fp)
|
||||
lut = p.getpalette()
|
||||
except (SyntaxError, ValueError):
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
pass
|
||||
|
||||
if not lut:
|
||||
raise IOError("cannot load palette")
|
||||
|
||||
return lut # data, rawmode
|
||||
66
PIL/ImagePath.py
Normal file
66
PIL/ImagePath.py
Normal file
@ -0,0 +1,66 @@
|
||||
#
|
||||
# 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 PIL import Image
|
||||
|
||||
|
||||
# the Python class below is overridden by the C implementation.
|
||||
|
||||
|
||||
class Path:
|
||||
|
||||
def __init__(self, xy):
|
||||
pass
|
||||
|
||||
##
|
||||
# Compacts the path, by removing points that are close to each
|
||||
# other. This method modifies the path in place.
|
||||
|
||||
def compact(self, distance=2):
|
||||
pass
|
||||
|
||||
##
|
||||
# Gets the bounding box.
|
||||
|
||||
def getbbox(self):
|
||||
pass
|
||||
|
||||
##
|
||||
# Maps the path through a function.
|
||||
|
||||
def map(self, function):
|
||||
pass
|
||||
|
||||
##
|
||||
# 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.
|
||||
|
||||
def tolist(self, flat=0):
|
||||
pass
|
||||
|
||||
##
|
||||
# Transforms the path.
|
||||
|
||||
def transform(self, matrix):
|
||||
pass
|
||||
|
||||
|
||||
# override with C implementation
|
||||
Path = Image.core.path
|
||||
89
PIL/ImageQt.py
Normal file
89
PIL/ImageQt.py
Normal file
@ -0,0 +1,89 @@
|
||||
#
|
||||
# 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 PIL import Image
|
||||
from PIL._util import isPath
|
||||
|
||||
try:
|
||||
from PyQt5.QtGui import QImage, qRgba
|
||||
except:
|
||||
from PyQt4.QtGui import QImage, qRgba
|
||||
|
||||
##
|
||||
# (Internal) Turns an RGB color into a Qt compatible color integer.
|
||||
|
||||
def rgb(r, g, b, a=255):
|
||||
# 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)
|
||||
|
||||
##
|
||||
# An PIL image wrapper for Qt. This is a subclass of PyQt4's QImage
|
||||
# class.
|
||||
#
|
||||
# @param im A PIL Image object, or a file name (given either as Python
|
||||
# string or a PyQt string object).
|
||||
|
||||
class ImageQt(QImage):
|
||||
|
||||
def __init__(self, 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?
|
||||
im = unicode(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)
|
||||
|
||||
# must keep a reference, or Qt will crash!
|
||||
self.__data = data or im.tobytes()
|
||||
|
||||
QImage.__init__(self, self.__data, im.size[0], im.size[1], format)
|
||||
|
||||
if colortable:
|
||||
self.setColorTable(colortable)
|
||||
41
PIL/ImageSequence.py
Normal file
41
PIL/ImageSequence.py
Normal file
@ -0,0 +1,41 @@
|
||||
#
|
||||
# The Python Imaging Library.
|
||||
# $Id$
|
||||
#
|
||||
# sequence support classes
|
||||
#
|
||||
# history:
|
||||
# 1997-02-20 fl Created
|
||||
#
|
||||
# Copyright (c) 1997 by Secret Labs AB.
|
||||
# Copyright (c) 1997 by Fredrik Lundh.
|
||||
#
|
||||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
##
|
||||
|
||||
class Iterator:
|
||||
"""
|
||||
This class implements an iterator object that can be used to loop
|
||||
over an image sequence.
|
||||
|
||||
You can use the ``[]`` operator to access elements by index. This operator
|
||||
will raise an :py:exc:`IndexError` if you try to access a nonexistent
|
||||
frame.
|
||||
|
||||
:param im: An image object.
|
||||
"""
|
||||
|
||||
def __init__(self, im):
|
||||
if not hasattr(im, "seek"):
|
||||
raise AttributeError("im must have seek method")
|
||||
self.im = im
|
||||
|
||||
def __getitem__(self, ix):
|
||||
try:
|
||||
if ix:
|
||||
self.im.seek(ix)
|
||||
return self.im
|
||||
except EOFError:
|
||||
raise IndexError # end of sequence
|
||||
171
PIL/ImageShow.py
Normal file
171
PIL/ImageShow.py
Normal file
@ -0,0 +1,171 @@
|
||||
#
|
||||
# The Python Imaging Library.
|
||||
# $Id$
|
||||
#
|
||||
# im.show() drivers
|
||||
#
|
||||
# History:
|
||||
# 2008-04-06 fl Created
|
||||
#
|
||||
# Copyright (c) Secret Labs AB 2008.
|
||||
#
|
||||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
from PIL import Image
|
||||
import os, sys
|
||||
|
||||
if sys.version_info >= (3, 3):
|
||||
from shlex import quote
|
||||
else:
|
||||
from pipes import quote
|
||||
|
||||
_viewers = []
|
||||
|
||||
def register(viewer, order=1):
|
||||
try:
|
||||
if issubclass(viewer, Viewer):
|
||||
viewer = viewer()
|
||||
except TypeError:
|
||||
pass # raised if viewer wasn't a class
|
||||
if order > 0:
|
||||
_viewers.append(viewer)
|
||||
elif order < 0:
|
||||
_viewers.insert(0, viewer)
|
||||
|
||||
##
|
||||
# Displays a given image.
|
||||
#
|
||||
# @param image An image object.
|
||||
# @param title Optional title. Not all viewers can display the title.
|
||||
# @param **options Additional viewer options.
|
||||
# @return True if a suitable viewer was found, false otherwise.
|
||||
|
||||
def show(image, title=None, **options):
|
||||
for viewer in _viewers:
|
||||
if viewer.show(image, title=title, **options):
|
||||
return 1
|
||||
return 0
|
||||
|
||||
##
|
||||
# Base class for viewers.
|
||||
|
||||
class Viewer:
|
||||
|
||||
# main api
|
||||
|
||||
def show(self, image, **options):
|
||||
|
||||
# save temporary image to disk
|
||||
if image.mode[:4] == "I;16":
|
||||
# @PIL88 @PIL101
|
||||
# "I;16" isn't an 'official' mode, but we still want to
|
||||
# provide a simple way to show 16-bit images.
|
||||
base = "L"
|
||||
# FIXME: auto-contrast if max() > 255?
|
||||
else:
|
||||
base = Image.getmodebase(image.mode)
|
||||
if base != image.mode and image.mode != "1":
|
||||
image = image.convert(base)
|
||||
|
||||
return self.show_image(image, **options)
|
||||
|
||||
# hook methods
|
||||
|
||||
format = None
|
||||
|
||||
def get_format(self, image):
|
||||
# return format name, or None to save as PGM/PPM
|
||||
return self.format
|
||||
|
||||
def get_command(self, file, **options):
|
||||
raise NotImplementedError
|
||||
|
||||
def save_image(self, image):
|
||||
# save to temporary file, and return filename
|
||||
return image._dump(format=self.get_format(image))
|
||||
|
||||
def show_image(self, image, **options):
|
||||
# display given image
|
||||
return self.show_file(self.save_image(image), **options)
|
||||
|
||||
def show_file(self, file, **options):
|
||||
# display given file
|
||||
os.system(self.get_command(file, **options))
|
||||
return 1
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
if sys.platform == "win32":
|
||||
|
||||
class WindowsViewer(Viewer):
|
||||
format = "BMP"
|
||||
def get_command(self, file, **options):
|
||||
return ('start "Pillow" /WAIT "%s" '
|
||||
'&& ping -n 2 127.0.0.1 >NUL '
|
||||
'&& del /f "%s"' % (file, file))
|
||||
|
||||
register(WindowsViewer)
|
||||
|
||||
elif sys.platform == "darwin":
|
||||
|
||||
class MacViewer(Viewer):
|
||||
format = "BMP"
|
||||
def get_command(self, file, **options):
|
||||
# on darwin open returns immediately resulting in the temp
|
||||
# file removal while app is opening
|
||||
command = "open -a /Applications/Preview.app"
|
||||
command = "(%s %s; sleep 20; rm -f %s)&" % (command, quote(file), quote(file))
|
||||
return command
|
||||
|
||||
register(MacViewer)
|
||||
|
||||
else:
|
||||
|
||||
# unixoids
|
||||
|
||||
def which(executable):
|
||||
path = os.environ.get("PATH")
|
||||
if not path:
|
||||
return None
|
||||
for dirname in path.split(os.pathsep):
|
||||
filename = os.path.join(dirname, executable)
|
||||
if os.path.isfile(filename):
|
||||
# FIXME: make sure it's executable
|
||||
return filename
|
||||
return None
|
||||
|
||||
class UnixViewer(Viewer):
|
||||
def show_file(self, file, **options):
|
||||
command, executable = self.get_command_ex(file, **options)
|
||||
command = "(%s %s; rm -f %s)&" % (command, quote(file), quote(file))
|
||||
os.system(command)
|
||||
return 1
|
||||
|
||||
# implementations
|
||||
|
||||
class DisplayViewer(UnixViewer):
|
||||
def get_command_ex(self, file, **options):
|
||||
command = executable = "display"
|
||||
return command, executable
|
||||
|
||||
if which("display"):
|
||||
register(DisplayViewer)
|
||||
|
||||
class XVViewer(UnixViewer):
|
||||
def get_command_ex(self, file, title=None, **options):
|
||||
# note: xv is pretty outdated. most modern systems have
|
||||
# imagemagick's display command instead.
|
||||
command = executable = "xv"
|
||||
if title:
|
||||
command += " -name %s" % quote(title)
|
||||
return command, executable
|
||||
|
||||
if which("xv"):
|
||||
register(XVViewer)
|
||||
|
||||
if __name__ == "__main__":
|
||||
# usage: python ImageShow.py imagefile [title]
|
||||
print(show(Image.open(sys.argv[1]), *sys.argv[2:]))
|
||||
147
PIL/ImageStat.py
Normal file
147
PIL/ImageStat.py
Normal file
@ -0,0 +1,147 @@
|
||||
#
|
||||
# The Python Imaging Library.
|
||||
# $Id$
|
||||
#
|
||||
# global image statistics
|
||||
#
|
||||
# History:
|
||||
# 1996-04-05 fl Created
|
||||
# 1997-05-21 fl Added mask; added rms, var, stddev attributes
|
||||
# 1997-08-05 fl Added median
|
||||
# 1998-07-05 hk Fixed integer overflow error
|
||||
#
|
||||
# Notes:
|
||||
# This class shows how to implement delayed evaluation of attributes.
|
||||
# To get a certain value, simply access the corresponding attribute.
|
||||
# The __getattr__ dispatcher takes care of the rest.
|
||||
#
|
||||
# Copyright (c) Secret Labs AB 1997.
|
||||
# Copyright (c) Fredrik Lundh 1996-97.
|
||||
#
|
||||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
import operator, math
|
||||
from functools import reduce
|
||||
|
||||
|
||||
class Stat:
|
||||
|
||||
def __init__(self, image_or_list, mask = None):
|
||||
try:
|
||||
if mask:
|
||||
self.h = image_or_list.histogram(mask)
|
||||
else:
|
||||
self.h = image_or_list.histogram()
|
||||
except AttributeError:
|
||||
self.h = image_or_list # assume it to be a histogram list
|
||||
if not isinstance(self.h, list):
|
||||
raise TypeError("first argument must be image or list")
|
||||
self.bands = list(range(len(self.h) // 256))
|
||||
|
||||
def __getattr__(self, id):
|
||||
"Calculate missing attribute"
|
||||
if id[:4] == "_get":
|
||||
raise AttributeError(id)
|
||||
# calculate missing attribute
|
||||
v = getattr(self, "_get" + id)()
|
||||
setattr(self, id, v)
|
||||
return v
|
||||
|
||||
def _getextrema(self):
|
||||
"Get min/max values for each band in the image"
|
||||
|
||||
def minmax(histogram):
|
||||
n = 255
|
||||
x = 0
|
||||
for i in range(256):
|
||||
if histogram[i]:
|
||||
n = min(n, i)
|
||||
x = max(x, i)
|
||||
return n, x # returns (255, 0) if there's no data in the histogram
|
||||
|
||||
v = []
|
||||
for i in range(0, len(self.h), 256):
|
||||
v.append(minmax(self.h[i:]))
|
||||
return v
|
||||
|
||||
def _getcount(self):
|
||||
"Get total number of pixels in each layer"
|
||||
|
||||
v = []
|
||||
for i in range(0, len(self.h), 256):
|
||||
v.append(reduce(operator.add, self.h[i:i+256]))
|
||||
return v
|
||||
|
||||
def _getsum(self):
|
||||
"Get sum of all pixels in each layer"
|
||||
|
||||
v = []
|
||||
for i in range(0, len(self.h), 256):
|
||||
sum = 0.0
|
||||
for j in range(256):
|
||||
sum += j * self.h[i + j]
|
||||
v.append(sum)
|
||||
return v
|
||||
|
||||
def _getsum2(self):
|
||||
"Get squared sum of all pixels in each layer"
|
||||
|
||||
v = []
|
||||
for i in range(0, len(self.h), 256):
|
||||
sum2 = 0.0
|
||||
for j in range(256):
|
||||
sum2 += (j ** 2) * float(self.h[i + j])
|
||||
v.append(sum2)
|
||||
return v
|
||||
|
||||
def _getmean(self):
|
||||
"Get average pixel level for each layer"
|
||||
|
||||
v = []
|
||||
for i in self.bands:
|
||||
v.append(self.sum[i] / self.count[i])
|
||||
return v
|
||||
|
||||
def _getmedian(self):
|
||||
"Get median pixel level for each layer"
|
||||
|
||||
v = []
|
||||
for i in self.bands:
|
||||
s = 0
|
||||
l = self.count[i]//2
|
||||
b = i * 256
|
||||
for j in range(256):
|
||||
s = s + self.h[b+j]
|
||||
if s > l:
|
||||
break
|
||||
v.append(j)
|
||||
return v
|
||||
|
||||
def _getrms(self):
|
||||
"Get RMS for each layer"
|
||||
|
||||
v = []
|
||||
for i in self.bands:
|
||||
v.append(math.sqrt(self.sum2[i] / self.count[i]))
|
||||
return v
|
||||
|
||||
|
||||
def _getvar(self):
|
||||
"Get variance for each layer"
|
||||
|
||||
v = []
|
||||
for i in self.bands:
|
||||
n = self.count[i]
|
||||
v.append((self.sum2[i]-(self.sum[i]**2.0)/n)/n)
|
||||
return v
|
||||
|
||||
def _getstddev(self):
|
||||
"Get standard deviation for each layer"
|
||||
|
||||
v = []
|
||||
for i in self.bands:
|
||||
v.append(math.sqrt(self.var[i]))
|
||||
return v
|
||||
|
||||
Global = Stat # compatibility
|
||||
@ -24,52 +24,36 @@
|
||||
#
|
||||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
from __future__ import annotations
|
||||
|
||||
import tkinter
|
||||
from io import BytesIO
|
||||
from typing import Any
|
||||
try:
|
||||
import tkinter
|
||||
except ImportError:
|
||||
import Tkinter
|
||||
tkinter = Tkinter
|
||||
del Tkinter
|
||||
|
||||
from . import Image, ImageFile
|
||||
from PIL import Image
|
||||
|
||||
TYPE_CHECKING = False
|
||||
if TYPE_CHECKING:
|
||||
from ._typing import CapsuleType
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Check for Tkinter interface hooks
|
||||
|
||||
_pilbitmap_ok = None
|
||||
|
||||
def _get_image_from_kw(kw: dict[str, Any]) -> ImageFile.ImageFile | None:
|
||||
source = None
|
||||
if "file" in kw:
|
||||
source = kw.pop("file")
|
||||
elif "data" in kw:
|
||||
source = BytesIO(kw.pop("data"))
|
||||
if not source:
|
||||
return None
|
||||
return Image.open(source)
|
||||
|
||||
|
||||
def _pyimagingtkcall(
|
||||
command: str, photo: PhotoImage | tkinter.PhotoImage, ptr: CapsuleType
|
||||
) -> None:
|
||||
tk = photo.tk
|
||||
try:
|
||||
tk.call(command, photo, repr(ptr))
|
||||
except tkinter.TclError:
|
||||
# activate Tkinter hook
|
||||
# may raise an error if it cannot attach to Tkinter
|
||||
from . import _imagingtk
|
||||
|
||||
_imagingtk.tkinit(tk.interpaddr())
|
||||
tk.call(command, photo, repr(ptr))
|
||||
|
||||
def _pilbitmap_check():
|
||||
global _pilbitmap_ok
|
||||
if _pilbitmap_ok is None:
|
||||
try:
|
||||
im = Image.new("1", (1,1))
|
||||
tkinter.BitmapImage(data="PIL:%d" % im.im.id)
|
||||
_pilbitmap_ok = 1
|
||||
except tkinter.TclError:
|
||||
_pilbitmap_ok = 0
|
||||
return _pilbitmap_ok
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# PhotoImage
|
||||
|
||||
|
||||
class PhotoImage:
|
||||
"""
|
||||
A Tkinter-compatible photo image. This can be used
|
||||
@ -77,7 +61,7 @@ class PhotoImage:
|
||||
image, pixels having alpha 0 are treated as transparent.
|
||||
|
||||
The constructor takes either a PIL image, or a mode and a size.
|
||||
Alternatively, you can use the ``file`` or ``data`` options to initialize
|
||||
Alternatively, you can use the **file** or **data** options to initialize
|
||||
the photo image object.
|
||||
|
||||
:param image: Either a PIL image, or a mode string. If a mode string is
|
||||
@ -90,36 +74,33 @@ class PhotoImage:
|
||||
image file).
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
image: Image.Image | str | None = None,
|
||||
size: tuple[int, int] | None = None,
|
||||
**kw: Any,
|
||||
) -> None:
|
||||
def __init__(self, image=None, size=None, **kw):
|
||||
|
||||
# Tk compatibility: file or data
|
||||
if image is None:
|
||||
image = _get_image_from_kw(kw)
|
||||
if "file" in kw:
|
||||
image = Image.open(kw["file"])
|
||||
del kw["file"]
|
||||
elif "data" in kw:
|
||||
from io import BytesIO
|
||||
image = Image.open(BytesIO(kw["data"]))
|
||||
del kw["data"]
|
||||
|
||||
if image is None:
|
||||
msg = "Image is required"
|
||||
raise ValueError(msg)
|
||||
elif isinstance(image, str):
|
||||
mode = image
|
||||
image = None
|
||||
|
||||
if size is None:
|
||||
msg = "If first argument is mode, size is required"
|
||||
raise ValueError(msg)
|
||||
else:
|
||||
if hasattr(image, "mode") and hasattr(image, "size"):
|
||||
# got an image instead of a mode
|
||||
mode = image.mode
|
||||
if mode == "P":
|
||||
# palette mapped data
|
||||
image.apply_transparency()
|
||||
image.load()
|
||||
mode = image.palette.mode if image.palette else "RGB"
|
||||
try:
|
||||
mode = image.palette.mode
|
||||
except AttributeError:
|
||||
mode = "RGB" # default
|
||||
size = image.size
|
||||
kw["width"], kw["height"] = size
|
||||
else:
|
||||
mode = image
|
||||
image = None
|
||||
|
||||
if mode not in ["1", "L", "RGB", "RGBA"]:
|
||||
mode = Image.getmodebase(mode)
|
||||
@ -131,18 +112,16 @@ class PhotoImage:
|
||||
if image:
|
||||
self.paste(image)
|
||||
|
||||
def __del__(self) -> None:
|
||||
try:
|
||||
name = self.__photo.name
|
||||
except AttributeError:
|
||||
return
|
||||
def __del__(self):
|
||||
name = self.__photo.name
|
||||
self.__photo.name = None
|
||||
try:
|
||||
self.__photo.tk.call("image", "delete", name)
|
||||
except Exception:
|
||||
pass # ignore internal errors
|
||||
except:
|
||||
pass # ignore internal errors
|
||||
|
||||
def __str__(self) -> str:
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
Get the Tkinter photo image identifier. This method is automatically
|
||||
called by Tkinter whenever a PhotoImage object is passed to a Tkinter
|
||||
@ -152,7 +131,8 @@ class PhotoImage:
|
||||
"""
|
||||
return str(self.__photo)
|
||||
|
||||
def width(self) -> int:
|
||||
|
||||
def width(self):
|
||||
"""
|
||||
Get the width of the image.
|
||||
|
||||
@ -160,7 +140,8 @@ class PhotoImage:
|
||||
"""
|
||||
return self.__size[0]
|
||||
|
||||
def height(self) -> int:
|
||||
|
||||
def height(self):
|
||||
"""
|
||||
Get the height of the image.
|
||||
|
||||
@ -168,7 +149,8 @@ class PhotoImage:
|
||||
"""
|
||||
return self.__size[1]
|
||||
|
||||
def paste(self, im: Image.Image) -> None:
|
||||
|
||||
def paste(self, im, box=None):
|
||||
"""
|
||||
Paste a PIL image into the photo image. Note that this can
|
||||
be very slow if the photo image is displayed.
|
||||
@ -176,17 +158,35 @@ class PhotoImage:
|
||||
:param im: A PIL image. The size must match the target region. If the
|
||||
mode does not match, the image is converted to the mode of
|
||||
the bitmap image.
|
||||
:param box: A 4-tuple defining the left, upper, right, and lower pixel
|
||||
coordinate. If None is given instead of a tuple, all of
|
||||
the image is assumed.
|
||||
"""
|
||||
|
||||
# convert to blittable
|
||||
ptr = im.getim()
|
||||
im.load()
|
||||
image = im.im
|
||||
if not image.isblock() or im.mode != self.__mode:
|
||||
block = Image.core.new_block(self.__mode, im.size)
|
||||
image.convert2(block, image) # convert directly between buffers
|
||||
ptr = block.ptr
|
||||
if image.isblock() and im.mode == self.__mode:
|
||||
block = image
|
||||
else:
|
||||
block = image.new_block(self.__mode, im.size)
|
||||
image.convert2(block, image) # convert directly between buffers
|
||||
|
||||
_pyimagingtkcall("PyImagingPhoto", self.__photo, ptr)
|
||||
tk = self.__photo.tk
|
||||
|
||||
try:
|
||||
tk.call("PyImagingPhoto", self.__photo, block.id)
|
||||
except tkinter.TclError as v:
|
||||
# activate Tkinter hook
|
||||
try:
|
||||
from PIL import _imagingtk
|
||||
try:
|
||||
_imagingtk.tkinit(tk.interpaddr(), 1)
|
||||
except AttributeError:
|
||||
_imagingtk.tkinit(id(tk), 0)
|
||||
tk.call("PyImagingPhoto", self.__photo, block.id)
|
||||
except (ImportError, AttributeError, tkinter.TclError):
|
||||
raise # configuration problem; cannot attach to Tkinter
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# BitmapImage
|
||||
@ -194,43 +194,54 @@ class PhotoImage:
|
||||
|
||||
class BitmapImage:
|
||||
"""
|
||||
|
||||
A Tkinter-compatible bitmap image. This can be used everywhere Tkinter
|
||||
expects an image object.
|
||||
|
||||
The given image must have mode "1". Pixels having value 0 are treated as
|
||||
transparent. Options, if any, are passed on to Tkinter. The most commonly
|
||||
used option is ``foreground``, which is used to specify the color for the
|
||||
used option is **foreground**, which is used to specify the color for the
|
||||
non-transparent parts. See the Tkinter documentation for information on
|
||||
how to specify colours.
|
||||
|
||||
:param image: A PIL image.
|
||||
"""
|
||||
|
||||
def __init__(self, image: Image.Image | None = None, **kw: Any) -> None:
|
||||
def __init__(self, image=None, **kw):
|
||||
|
||||
# Tk compatibility: file or data
|
||||
if image is None:
|
||||
image = _get_image_from_kw(kw)
|
||||
if "file" in kw:
|
||||
image = Image.open(kw["file"])
|
||||
del kw["file"]
|
||||
elif "data" in kw:
|
||||
from io import BytesIO
|
||||
image = Image.open(BytesIO(kw["data"]))
|
||||
del kw["data"]
|
||||
|
||||
if image is None:
|
||||
msg = "Image is required"
|
||||
raise ValueError(msg)
|
||||
self.__mode = image.mode
|
||||
self.__size = image.size
|
||||
|
||||
self.__photo = tkinter.BitmapImage(data=image.tobitmap(), **kw)
|
||||
if _pilbitmap_check():
|
||||
# fast way (requires the pilbitmap booster patch)
|
||||
image.load()
|
||||
kw["data"] = "PIL:%d" % image.im.id
|
||||
self.__im = image # must keep a reference
|
||||
else:
|
||||
# slow but safe way
|
||||
kw["data"] = image.tobitmap()
|
||||
self.__photo = tkinter.BitmapImage(**kw)
|
||||
|
||||
def __del__(self) -> None:
|
||||
try:
|
||||
name = self.__photo.name
|
||||
except AttributeError:
|
||||
return
|
||||
def __del__(self):
|
||||
name = self.__photo.name
|
||||
self.__photo.name = None
|
||||
try:
|
||||
self.__photo.tk.call("image", "delete", name)
|
||||
except Exception:
|
||||
pass # ignore internal errors
|
||||
except:
|
||||
pass # ignore internal errors
|
||||
|
||||
def width(self) -> int:
|
||||
|
||||
def width(self):
|
||||
"""
|
||||
Get the width of the image.
|
||||
|
||||
@ -238,7 +249,8 @@ class BitmapImage:
|
||||
"""
|
||||
return self.__size[0]
|
||||
|
||||
def height(self) -> int:
|
||||
|
||||
def height(self):
|
||||
"""
|
||||
Get the height of the image.
|
||||
|
||||
@ -246,7 +258,8 @@ class BitmapImage:
|
||||
"""
|
||||
return self.__size[1]
|
||||
|
||||
def __str__(self) -> str:
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
Get the Tkinter bitmap image identifier. This method is automatically
|
||||
called by Tkinter whenever a BitmapImage object is passed to a Tkinter
|
||||
@ -257,10 +270,27 @@ class BitmapImage:
|
||||
return str(self.__photo)
|
||||
|
||||
|
||||
def getimage(photo: PhotoImage) -> Image.Image:
|
||||
def getimage(photo):
|
||||
"""Copies the contents of a PhotoImage to a PIL image memory."""
|
||||
im = Image.new("RGBA", (photo.width(), photo.height()))
|
||||
photo.tk.call("PyImagingPhotoGet", photo)
|
||||
|
||||
_pyimagingtkcall("PyImagingPhotoGet", photo, im.getim())
|
||||
# --------------------------------------------------------------------
|
||||
# Helper for the Image.show method.
|
||||
|
||||
return im
|
||||
def _show(image, title):
|
||||
|
||||
class UI(tkinter.Label):
|
||||
def __init__(self, master, im):
|
||||
if im.mode == "1":
|
||||
self.image = BitmapImage(im, foreground="white", master=master)
|
||||
else:
|
||||
self.image = PhotoImage(im, master=master)
|
||||
tkinter.Label.__init__(self, master, image=self.image,
|
||||
bg="black", bd=0)
|
||||
|
||||
if not tkinter._default_root:
|
||||
raise IOError("tkinter not initialized")
|
||||
top = tkinter.Toplevel()
|
||||
if title:
|
||||
top.title(title)
|
||||
UI(top, image).pack()
|
||||
95
PIL/ImageTransform.py
Normal file
95
PIL/ImageTransform.py
Normal file
@ -0,0 +1,95 @@
|
||||
#
|
||||
# The Python Imaging Library.
|
||||
# $Id$
|
||||
#
|
||||
# transform wrappers
|
||||
#
|
||||
# History:
|
||||
# 2002-04-08 fl Created
|
||||
#
|
||||
# Copyright (c) 2002 by Secret Labs AB
|
||||
# Copyright (c) 2002 by Fredrik Lundh
|
||||
#
|
||||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
from PIL import Image
|
||||
|
||||
class Transform(Image.ImageTransformHandler):
|
||||
def __init__(self, data):
|
||||
self.data = data
|
||||
def getdata(self):
|
||||
return self.method, self.data
|
||||
def transform(self, size, image, **options):
|
||||
# can be overridden
|
||||
method, data = self.getdata()
|
||||
return image.transform(size, method, data, **options)
|
||||
|
||||
##
|
||||
# Define an affine image transform.
|
||||
# <p>
|
||||
# This function takes a 6-tuple (<i>a, b, c, d, e, f</i>) which
|
||||
# contain the first two rows from an affine transform matrix. For
|
||||
# each pixel (<i>x, y</i>) in the output image, the new value is
|
||||
# taken from a position (a <i>x</i> + b <i>y</i> + c,
|
||||
# d <i>x</i> + e <i>y</i> + f) in the input image, rounded to
|
||||
# nearest pixel.
|
||||
# <p>
|
||||
# This function can be used to scale, translate, rotate, and shear the
|
||||
# original image.
|
||||
#
|
||||
# @def AffineTransform(matrix)
|
||||
# @param matrix A 6-tuple (<i>a, b, c, d, e, f</i>) containing
|
||||
# the first two rows from an affine transform matrix.
|
||||
# @see Image#Image.transform
|
||||
|
||||
class AffineTransform(Transform):
|
||||
method = Image.AFFINE
|
||||
|
||||
##
|
||||
# Define a transform to extract a subregion from an image.
|
||||
# <p>
|
||||
# Maps a rectangle (defined by two corners) from the image to a
|
||||
# rectangle of the given size. The resulting image will contain
|
||||
# data sampled from between the corners, such that (<i>x0, y0</i>)
|
||||
# in the input image will end up at (0,0) in the output image,
|
||||
# and (<i>x1, y1</i>) at <i>size</i>.
|
||||
# <p>
|
||||
# This method can be used to crop, stretch, shrink, or mirror an
|
||||
# arbitrary rectangle in the current image. It is slightly slower than
|
||||
# <b>crop</b>, but about as fast as a corresponding <b>resize</b>
|
||||
# operation.
|
||||
#
|
||||
# @def ExtentTransform(bbox)
|
||||
# @param bbox A 4-tuple (<i>x0, y0, x1, y1</i>) which specifies
|
||||
# two points in the input image's coordinate system.
|
||||
# @see Image#Image.transform
|
||||
|
||||
class ExtentTransform(Transform):
|
||||
method = Image.EXTENT
|
||||
|
||||
##
|
||||
# Define an quad image transform.
|
||||
# <p>
|
||||
# Maps a quadrilateral (a region defined by four corners) from the
|
||||
# image to a rectangle of the given size.
|
||||
#
|
||||
# @def QuadTransform(xy)
|
||||
# @param xy An 8-tuple (<i>x0, y0, x1, y1, x2, y2, y3, y3</i>) which
|
||||
# contain the upper left, lower left, lower right, and upper right
|
||||
# corner of the source quadrilateral.
|
||||
# @see Image#Image.transform
|
||||
|
||||
class QuadTransform(Transform):
|
||||
method = Image.QUAD
|
||||
|
||||
##
|
||||
# Define an mesh image transform. A mesh transform consists of one
|
||||
# or more individual quad transforms.
|
||||
#
|
||||
# @def MeshTransform(data)
|
||||
# @param data A list of (bbox, quad) tuples.
|
||||
# @see Image#Image.transform
|
||||
|
||||
class MeshTransform(Transform):
|
||||
method = Image.MESH
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user