Compare commits
105 Commits
copilot-in
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
877527cefc | ||
|
|
dcd6d41e77 | ||
|
|
94ec04d33e | ||
|
|
764e315923 | ||
|
|
0802206cfc | ||
|
|
e7556b19b7 | ||
|
|
3f74d08263 | ||
|
|
e9855d1705 | ||
|
|
f0f67f8cf8 | ||
|
|
c48b302602 | ||
|
|
78ee80a6fd | ||
|
|
381e264e18 | ||
|
|
9289863c2c | ||
|
|
4dc442fb01 | ||
|
|
0582f43bad | ||
|
|
22e47e38bb | ||
|
|
954269051b | ||
|
|
3ce681240f | ||
|
|
ea5901535d | ||
|
|
24696af889 | ||
|
|
7be56cc100 | ||
|
|
70713d69b0 | ||
|
|
894c5d5335 | ||
|
|
f693a3a0e5 | ||
|
|
6a05e34b69 | ||
|
|
903065f5e9 | ||
|
|
689a7f37fd | ||
|
|
599ddd368c | ||
|
|
ab25042353 | ||
|
|
1cd2d0f67a | ||
|
|
5f469b6bd2 | ||
|
|
ead2f34515 | ||
|
|
a6fc9992f2 | ||
|
|
2128d6465c | ||
|
|
4bba24632f | ||
|
|
21790fc0da | ||
|
|
c234720aca | ||
|
|
575b33d811 | ||
|
|
82614324ed | ||
|
|
32b6c5f0ee | ||
|
|
956d434c68 | ||
|
|
3bbb7a2a04 | ||
|
|
b656f900b4 | ||
|
|
586604d0c3 | ||
|
|
d92b826c4a | ||
|
|
2d02654c54 | ||
|
|
7e4ca8b3ab | ||
|
|
be8563347b | ||
|
|
fc47d07603 | ||
|
|
7fe1b9ee04 | ||
|
|
4af29fb732 | ||
|
|
1f3b8a831d | ||
|
|
0ef81c33af | ||
|
|
3dda1d190f | ||
|
|
f2ee74b2f8 | ||
|
|
99869f0313 | ||
|
|
fe054a1b3f | ||
|
|
852a832832 | ||
|
|
755b73b274 | ||
|
|
f0fe496315 | ||
|
|
fba17910aa | ||
|
|
d2b20102e4 | ||
|
|
8c522096e8 | ||
|
|
855774a175 | ||
|
|
2ae2c4e84f | ||
|
|
a908c62460 | ||
|
|
53800d4fcf | ||
|
|
a0cd878bed | ||
|
|
4e0aeba4af | ||
|
|
5f9112e862 | ||
|
|
9605fccf00 | ||
|
|
1382fc4767 | ||
|
|
c8c391b9c0 | ||
|
|
a124ed208f | ||
|
|
ee24a11073 | ||
|
|
6e1ccab749 | ||
|
|
0cbdd2eff9 | ||
|
|
24b12dc84f | ||
|
|
d016c90108 | ||
|
|
6a0192a40a | ||
|
|
6fe81dd52e | ||
|
|
55989595ea | ||
|
|
b579577aa0 | ||
|
|
6f815c2d8d | ||
|
|
80a91fdb4e | ||
|
|
0d440b7d09 | ||
|
|
00ff8636a2 | ||
|
|
e74a89f70e | ||
|
|
20af4ec89c | ||
|
|
3f90d5c4da | ||
|
|
68be7f30ff | ||
|
|
e0f9e2b98e | ||
|
|
ad582c1a8e | ||
|
|
c2ac2da31c | ||
|
|
3aa076129f | ||
|
|
4a74a20b86 | ||
|
|
64ed4710b9 | ||
|
|
cdaa1bf9ef | ||
|
|
4d63d0b3a6 | ||
|
|
cb5736ea3e | ||
|
|
117de2b181 | ||
|
|
e58c67347a | ||
|
|
7f68decf2c | ||
|
|
e50d8a5192 | ||
|
|
f708c00527 |
@ -39,8 +39,8 @@ python3 -m pip install --only-binary=:all: pyarrow || true
|
|||||||
# PyQt6 doesn't support PyPy3
|
# PyQt6 doesn't support PyPy3
|
||||||
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
|
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
|
sudo apt-get -qq install libegl1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0
|
||||||
# TODO Update condition when pyqt6 supports free-threading
|
# pyqt6 doesn't yet support free-threading; only install if a wheel is available
|
||||||
if ! [[ "$PYTHON_GIL" == "0" ]]; then python3 -m pip install pyqt6 ; fi
|
python3 -m pip install --only-binary=:all: pyqt6 || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# webp
|
# webp
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
cibuildwheel==3.4.0
|
cibuildwheel==3.4.1
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
mypy==1.19.1
|
mypy==1.20.2
|
||||||
arro3-compute
|
arro3-compute
|
||||||
arro3-core
|
arro3-core
|
||||||
IceSpringPySideStubs-PyQt6
|
IceSpringPySideStubs-PyQt6
|
||||||
|
|||||||
1
.ci/requirements-sbom.txt
Normal file
1
.ci/requirements-sbom.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
check-jsonschema==0.37.1
|
||||||
424
.github/INCIDENT_RESPONSE.md
vendored
Normal file
424
.github/INCIDENT_RESPONSE.md
vendored
Normal file
@ -0,0 +1,424 @@
|
|||||||
|
# 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\>
|
||||||
271
.github/compare-dist-sizes.py
vendored
Normal file
271
.github/compare-dist-sizes.py
vendored
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
"""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())
|
||||||
6
.github/dependencies.json
vendored
6
.github/dependencies.json
vendored
@ -3,12 +3,12 @@
|
|||||||
"bzip2": "1.0.8",
|
"bzip2": "1.0.8",
|
||||||
"freetype": "2.14.3",
|
"freetype": "2.14.3",
|
||||||
"fribidi": "1.0.16",
|
"fribidi": "1.0.16",
|
||||||
"harfbuzz": "13.2.1",
|
"harfbuzz": "14.2.0",
|
||||||
"jpegturbo": "3.1.4.1",
|
"jpegturbo": "3.1.4.1",
|
||||||
"lcms2": "2.18",
|
"lcms2": "2.19",
|
||||||
"libavif": "1.4.1",
|
"libavif": "1.4.1",
|
||||||
"libimagequant": "4.4.1",
|
"libimagequant": "4.4.1",
|
||||||
"libpng": "1.6.56",
|
"libpng": "1.6.58",
|
||||||
"libwebp": "1.6.0",
|
"libwebp": "1.6.0",
|
||||||
"libxcb": "1.17.0",
|
"libxcb": "1.17.0",
|
||||||
"openjpeg": "2.5.4",
|
"openjpeg": "2.5.4",
|
||||||
|
|||||||
560
.github/generate-sbom.py
vendored
Executable file
560
.github/generate-sbom.py
vendored
Executable file
@ -0,0 +1,560 @@
|
|||||||
|
#!/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()
|
||||||
1
.github/renovate.json
vendored
1
.github/renovate.json
vendored
@ -7,6 +7,7 @@
|
|||||||
"Dependency"
|
"Dependency"
|
||||||
],
|
],
|
||||||
"minimumReleaseAge": "7 days",
|
"minimumReleaseAge": "7 days",
|
||||||
|
"prCreation": "not-pending",
|
||||||
"schedule": [
|
"schedule": [
|
||||||
"* * 3 * *"
|
"* * 3 * *"
|
||||||
],
|
],
|
||||||
|
|||||||
13
.github/workflows/cifuzz.yml
vendored
13
.github/workflows/cifuzz.yml
vendored
@ -4,19 +4,14 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- "**"
|
- "**"
|
||||||
paths:
|
paths: &paths
|
||||||
- ".github/dependencies.json"
|
- ".github/dependencies.json"
|
||||||
- ".github/workflows/cifuzz.yml"
|
- ".github/workflows/cifuzz.yml"
|
||||||
- ".github/workflows/wheels-dependencies.sh"
|
- ".github/workflows/wheels-dependencies.sh"
|
||||||
- "**.c"
|
- "**.c"
|
||||||
- "**.h"
|
- "**.h"
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths: *paths
|
||||||
- ".github/dependencies.json"
|
|
||||||
- ".github/workflows/cifuzz.yml"
|
|
||||||
- ".github/workflows/wheels-dependencies.sh"
|
|
||||||
- "**.c"
|
|
||||||
- "**.h"
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
@ -35,14 +30,14 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Build Fuzzers
|
- name: Build Fuzzers
|
||||||
id: build
|
id: build
|
||||||
uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@e41e2f295eb18d630932fdd33d072527ba74c87b # master
|
uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@d87225267726cf7ce1a3e17cf103c5ac943c4f05 # master
|
||||||
with:
|
with:
|
||||||
oss-fuzz-project-name: 'pillow'
|
oss-fuzz-project-name: 'pillow'
|
||||||
language: python
|
language: python
|
||||||
dry-run: false
|
dry-run: false
|
||||||
- name: Run Fuzzers
|
- name: Run Fuzzers
|
||||||
id: run
|
id: run
|
||||||
uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@e41e2f295eb18d630932fdd33d072527ba74c87b # master
|
uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@d87225267726cf7ce1a3e17cf103c5ac943c4f05 # master
|
||||||
with:
|
with:
|
||||||
oss-fuzz-project-name: 'pillow'
|
oss-fuzz-project-name: 'pillow'
|
||||||
fuzz-seconds: 600
|
fuzz-seconds: 600
|
||||||
|
|||||||
7
.github/workflows/docs.yml
vendored
7
.github/workflows/docs.yml
vendored
@ -4,15 +4,12 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- "**"
|
- "**"
|
||||||
paths:
|
paths: &paths
|
||||||
- ".github/workflows/docs.yml"
|
- ".github/workflows/docs.yml"
|
||||||
- "docs/**"
|
- "docs/**"
|
||||||
- "src/PIL/**"
|
- "src/PIL/**"
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths: *paths
|
||||||
- ".github/workflows/docs.yml"
|
|
||||||
- "docs/**"
|
|
||||||
- "src/PIL/**"
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
|||||||
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@ -25,7 +25,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
python-version: "3.x"
|
python-version: "3.x"
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||||
- name: Lint
|
- name: Lint
|
||||||
run: uvx --with tox-uv tox -e lint
|
run: uvx --with tox-uv tox -e lint
|
||||||
- name: Mypy
|
- name: Mypy
|
||||||
|
|||||||
21
.github/workflows/test-docker.yml
vendored
21
.github/workflows/test-docker.yml
vendored
@ -4,19 +4,14 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- "**"
|
- "**"
|
||||||
paths-ignore:
|
paths-ignore: &paths-ignore
|
||||||
- ".github/workflows/docs.yml"
|
- ".github/workflows/docs.yml"
|
||||||
- ".github/workflows/wheels*"
|
- ".github/workflows/wheels*"
|
||||||
- ".gitmodules"
|
- ".gitmodules"
|
||||||
- "docs/**"
|
- "docs/**"
|
||||||
- "wheels/**"
|
- "wheels/**"
|
||||||
pull_request:
|
pull_request:
|
||||||
paths-ignore:
|
paths-ignore: *paths-ignore
|
||||||
- ".github/workflows/docs.yml"
|
|
||||||
- ".github/workflows/wheels*"
|
|
||||||
- ".gitmodules"
|
|
||||||
- "docs/**"
|
|
||||||
- "wheels/**"
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
@ -39,8 +34,8 @@ jobs:
|
|||||||
os: ["ubuntu-latest"]
|
os: ["ubuntu-latest"]
|
||||||
docker: [
|
docker: [
|
||||||
# Run slower jobs first to give them a headstart and reduce waiting time
|
# Run slower jobs first to give them a headstart and reduce waiting time
|
||||||
ubuntu-24.04-noble-ppc64le,
|
ubuntu-26.04-resolute-ppc64le,
|
||||||
ubuntu-24.04-noble-s390x,
|
ubuntu-26.04-resolute-s390x,
|
||||||
# Then run the remainder
|
# Then run the remainder
|
||||||
alpine,
|
alpine,
|
||||||
amazon-2023-amd64,
|
amazon-2023-amd64,
|
||||||
@ -50,17 +45,19 @@ jobs:
|
|||||||
debian-13-trixie-x86,
|
debian-13-trixie-x86,
|
||||||
debian-13-trixie-amd64,
|
debian-13-trixie-amd64,
|
||||||
fedora-43-amd64,
|
fedora-43-amd64,
|
||||||
|
fedora-44-amd64,
|
||||||
gentoo,
|
gentoo,
|
||||||
ubuntu-22.04-jammy-amd64,
|
ubuntu-22.04-jammy-amd64,
|
||||||
ubuntu-24.04-noble-amd64,
|
ubuntu-24.04-noble-amd64,
|
||||||
|
ubuntu-26.04-resolute-amd64,
|
||||||
]
|
]
|
||||||
dockerTag: [main]
|
dockerTag: [main]
|
||||||
include:
|
include:
|
||||||
- docker: "ubuntu-24.04-noble-ppc64le"
|
- docker: "ubuntu-26.04-resolute-ppc64le"
|
||||||
qemu-arch: "ppc64le"
|
qemu-arch: "ppc64le"
|
||||||
- docker: "ubuntu-24.04-noble-s390x"
|
- docker: "ubuntu-26.04-resolute-s390x"
|
||||||
qemu-arch: "s390x"
|
qemu-arch: "s390x"
|
||||||
- docker: "ubuntu-24.04-noble-arm64v8"
|
- docker: "ubuntu-26.04-resolute-arm64v8"
|
||||||
os: "ubuntu-24.04-arm"
|
os: "ubuntu-24.04-arm"
|
||||||
dockerTag: main
|
dockerTag: main
|
||||||
|
|
||||||
|
|||||||
9
.github/workflows/test-mingw.yml
vendored
9
.github/workflows/test-mingw.yml
vendored
@ -4,19 +4,14 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- "**"
|
- "**"
|
||||||
paths-ignore:
|
paths-ignore: &paths-ignore
|
||||||
- ".github/workflows/docs.yml"
|
- ".github/workflows/docs.yml"
|
||||||
- ".github/workflows/wheels*"
|
- ".github/workflows/wheels*"
|
||||||
- ".gitmodules"
|
- ".gitmodules"
|
||||||
- "docs/**"
|
- "docs/**"
|
||||||
- "wheels/**"
|
- "wheels/**"
|
||||||
pull_request:
|
pull_request:
|
||||||
paths-ignore:
|
paths-ignore: *paths-ignore
|
||||||
- ".github/workflows/docs.yml"
|
|
||||||
- ".github/workflows/wheels*"
|
|
||||||
- ".gitmodules"
|
|
||||||
- "docs/**"
|
|
||||||
- "wheels/**"
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
|||||||
5
.github/workflows/test-valgrind-memory.yml
vendored
5
.github/workflows/test-valgrind-memory.yml
vendored
@ -8,12 +8,13 @@ on:
|
|||||||
# branches:
|
# branches:
|
||||||
# - "**"
|
# - "**"
|
||||||
# paths:
|
# paths:
|
||||||
# - ".github/workflows/test-valgrind.yml"
|
# - ".github/workflows/test-valgrind-memory.yml"
|
||||||
# - "**.c"
|
# - "**.c"
|
||||||
# - "**.h"
|
# - "**.h"
|
||||||
|
# - "depends/docker-test-valgrind-memory.sh"
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- ".github/workflows/test-valgrind.yml"
|
- ".github/workflows/test-valgrind-memory.yml"
|
||||||
- "**.c"
|
- "**.c"
|
||||||
- "**.h"
|
- "**.h"
|
||||||
- "depends/docker-test-valgrind-memory.sh"
|
- "depends/docker-test-valgrind-memory.sh"
|
||||||
|
|||||||
7
.github/workflows/test-valgrind.yml
vendored
7
.github/workflows/test-valgrind.yml
vendored
@ -6,15 +6,12 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- "**"
|
- "**"
|
||||||
paths:
|
paths: &paths
|
||||||
- ".github/workflows/test-valgrind.yml"
|
- ".github/workflows/test-valgrind.yml"
|
||||||
- "**.c"
|
- "**.c"
|
||||||
- "**.h"
|
- "**.h"
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths: *paths
|
||||||
- ".github/workflows/test-valgrind.yml"
|
|
||||||
- "**.c"
|
|
||||||
- "**.h"
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
|||||||
9
.github/workflows/test-windows.yml
vendored
9
.github/workflows/test-windows.yml
vendored
@ -4,19 +4,14 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- "**"
|
- "**"
|
||||||
paths-ignore:
|
paths-ignore: &paths-ignore
|
||||||
- ".github/workflows/docs.yml"
|
- ".github/workflows/docs.yml"
|
||||||
- ".github/workflows/wheels*"
|
- ".github/workflows/wheels*"
|
||||||
- ".gitmodules"
|
- ".gitmodules"
|
||||||
- "docs/**"
|
- "docs/**"
|
||||||
- "wheels/**"
|
- "wheels/**"
|
||||||
pull_request:
|
pull_request:
|
||||||
paths-ignore:
|
paths-ignore: *paths-ignore
|
||||||
- ".github/workflows/docs.yml"
|
|
||||||
- ".github/workflows/wheels*"
|
|
||||||
- ".gitmodules"
|
|
||||||
- "docs/**"
|
|
||||||
- "wheels/**"
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
|||||||
19
.github/workflows/test.yml
vendored
19
.github/workflows/test.yml
vendored
@ -4,19 +4,14 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- "**"
|
- "**"
|
||||||
paths-ignore:
|
paths-ignore: &paths-ignore
|
||||||
- ".github/workflows/docs.yml"
|
- ".github/workflows/docs.yml"
|
||||||
- ".github/workflows/wheels*"
|
- ".github/workflows/wheels*"
|
||||||
- ".gitmodules"
|
- ".gitmodules"
|
||||||
- "docs/**"
|
- "docs/**"
|
||||||
- "wheels/**"
|
- "wheels/**"
|
||||||
pull_request:
|
pull_request:
|
||||||
paths-ignore:
|
paths-ignore: *paths-ignore
|
||||||
- ".github/workflows/docs.yml"
|
|
||||||
- ".github/workflows/wheels*"
|
|
||||||
- ".gitmodules"
|
|
||||||
- "docs/**"
|
|
||||||
- "wheels/**"
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
@ -47,7 +42,6 @@ jobs:
|
|||||||
"3.15",
|
"3.15",
|
||||||
"3.14t",
|
"3.14t",
|
||||||
"3.14",
|
"3.14",
|
||||||
"3.13t",
|
|
||||||
"3.13",
|
"3.13",
|
||||||
"3.12",
|
"3.12",
|
||||||
"3.11",
|
"3.11",
|
||||||
@ -56,10 +50,6 @@ jobs:
|
|||||||
include:
|
include:
|
||||||
- { python-version: "3.12", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" }
|
- { python-version: "3.12", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" }
|
||||||
- { python-version: "3.11", PYTHONOPTIMIZE: 2 }
|
- { python-version: "3.11", PYTHONOPTIMIZE: 2 }
|
||||||
# Free-threaded
|
|
||||||
- { python-version: "3.15t", disable-gil: true }
|
|
||||||
- { python-version: "3.14t", disable-gil: true }
|
|
||||||
- { python-version: "3.13t", disable-gil: true }
|
|
||||||
# Intel
|
# Intel
|
||||||
- { os: "macos-26-intel", python-version: "3.10" }
|
- { os: "macos-26-intel", python-version: "3.10" }
|
||||||
exclude:
|
exclude:
|
||||||
@ -83,11 +73,6 @@ jobs:
|
|||||||
".ci/*.sh"
|
".ci/*.sh"
|
||||||
"pyproject.toml"
|
"pyproject.toml"
|
||||||
|
|
||||||
- name: Set PYTHON_GIL
|
|
||||||
if: "${{ matrix.disable-gil }}"
|
|
||||||
run: |
|
|
||||||
echo "PYTHON_GIL=0" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Build system information
|
- name: Build system information
|
||||||
run: python3 .github/workflows/system-info.py
|
run: python3 .github/workflows/system-info.py
|
||||||
|
|
||||||
|
|||||||
89
.github/workflows/wheels.yml
vendored
89
.github/workflows/wheels.yml
vendored
@ -10,9 +10,12 @@ on:
|
|||||||
# │ │ │ │ │
|
# │ │ │ │ │
|
||||||
- cron: "42 1 * * 0,3"
|
- cron: "42 1 * * 0,3"
|
||||||
push:
|
push:
|
||||||
paths:
|
paths: &paths
|
||||||
- ".ci/requirements-cibw.txt"
|
- ".ci/requirements-cibw.txt"
|
||||||
|
- ".ci/requirements-sbom.txt"
|
||||||
|
- ".github/compare-dist-sizes.py"
|
||||||
- ".github/dependencies.json"
|
- ".github/dependencies.json"
|
||||||
|
- ".github/generate-sbom.py"
|
||||||
- ".github/workflows/wheels*"
|
- ".github/workflows/wheels*"
|
||||||
- "pyproject.toml"
|
- "pyproject.toml"
|
||||||
- "setup.py"
|
- "setup.py"
|
||||||
@ -22,15 +25,7 @@ on:
|
|||||||
tags:
|
tags:
|
||||||
- "*"
|
- "*"
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths: *paths
|
||||||
- ".ci/requirements-cibw.txt"
|
|
||||||
- ".github/dependencies.json"
|
|
||||||
- ".github/workflows/wheels*"
|
|
||||||
- "pyproject.toml"
|
|
||||||
- "setup.py"
|
|
||||||
- "wheels/*"
|
|
||||||
- "winbuild/build_prepare.py"
|
|
||||||
- "winbuild/fribidi.cmake"
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
@ -106,7 +101,7 @@ jobs:
|
|||||||
cibw_arch: arm64_iphonesimulator
|
cibw_arch: arm64_iphonesimulator
|
||||||
- name: "iOS x86_64 simulator"
|
- name: "iOS x86_64 simulator"
|
||||||
platform: ios
|
platform: ios
|
||||||
os: macos-15-intel
|
os: macos-26-intel
|
||||||
cibw_arch: x86_64_iphonesimulator
|
cibw_arch: x86_64_iphonesimulator
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
@ -261,6 +256,28 @@ jobs:
|
|||||||
echo $files
|
echo $files
|
||||||
[ "$files" -eq $EXPECTED_DISTS ] || exit 1
|
[ "$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:
|
scientific-python-nightly-wheels-publish:
|
||||||
if: github.event.repository.fork == false && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')
|
if: github.event.repository.fork == false && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')
|
||||||
needs: count-dists
|
needs: count-dists
|
||||||
@ -276,11 +293,59 @@ jobs:
|
|||||||
path: dist
|
path: dist
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
- name: Upload wheels to scientific-python-nightly-wheels
|
- name: Upload wheels to scientific-python-nightly-wheels
|
||||||
uses: scientific-python/upload-nightly-action@5748273c71e2d8d3a61f3a11a16421c8954f9ecf # 0.6.3
|
uses: scientific-python/upload-nightly-action@e76cfec8a4611fd02808a801b0ff5a7d7c1b2d99 # 0.6.4
|
||||||
with:
|
with:
|
||||||
artifacts_path: dist
|
artifacts_path: dist
|
||||||
anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }}
|
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:
|
pypi-publish:
|
||||||
if: github.event.repository.fork == false && github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
|
if: github.event.repository.fork == false && github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
|
||||||
needs: count-dists
|
needs: count-dists
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -97,3 +97,6 @@ pillow-test-images.zip
|
|||||||
|
|
||||||
# pyinstaller
|
# pyinstaller
|
||||||
*.spec
|
*.spec
|
||||||
|
|
||||||
|
# Generated SBOM
|
||||||
|
pillow-*.cdx.json
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.15.9
|
rev: v0.15.12
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff-check
|
- id: ruff-check
|
||||||
args: [--exit-non-zero-on-fix]
|
args: [--exit-non-zero-on-fix]
|
||||||
@ -24,7 +24,7 @@ repos:
|
|||||||
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
|
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/mirrors-clang-format
|
- repo: https://github.com/pre-commit/mirrors-clang-format
|
||||||
rev: v22.1.2
|
rev: v22.1.4
|
||||||
hooks:
|
hooks:
|
||||||
- id: clang-format
|
- id: clang-format
|
||||||
types: [c]
|
types: [c]
|
||||||
@ -54,14 +54,14 @@ repos:
|
|||||||
exclude: ^\.github/.*TEMPLATE|^Tests/(fonts|images)/
|
exclude: ^\.github/.*TEMPLATE|^Tests/(fonts|images)/
|
||||||
|
|
||||||
- repo: https://github.com/python-jsonschema/check-jsonschema
|
- repo: https://github.com/python-jsonschema/check-jsonschema
|
||||||
rev: 0.37.1
|
rev: 0.37.2
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-github-workflows
|
- id: check-github-workflows
|
||||||
- id: check-readthedocs
|
- id: check-readthedocs
|
||||||
- id: check-renovate
|
- id: check-renovate
|
||||||
|
|
||||||
- repo: https://github.com/zizmorcore/zizmor-pre-commit
|
- repo: https://github.com/zizmorcore/zizmor-pre-commit
|
||||||
rev: v1.23.1
|
rev: v1.24.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: zizmor
|
- id: zizmor
|
||||||
|
|
||||||
@ -71,7 +71,7 @@ repos:
|
|||||||
- id: sphinx-lint
|
- id: sphinx-lint
|
||||||
|
|
||||||
- repo: https://github.com/tox-dev/pyproject-fmt
|
- repo: https://github.com/tox-dev/pyproject-fmt
|
||||||
rev: v2.21.0
|
rev: v2.21.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyproject-fmt
|
- id: pyproject-fmt
|
||||||
|
|
||||||
|
|||||||
@ -9,8 +9,10 @@
|
|||||||
Pillow is the friendly PIL fork by [Jeffrey 'Alex' Clark and
|
Pillow is the friendly PIL fork by [Jeffrey 'Alex' Clark and
|
||||||
contributors](https://github.com/python-pillow/Pillow/graphs/contributors).
|
contributors](https://github.com/python-pillow/Pillow/graphs/contributors).
|
||||||
PIL is the Python Imaging Library by Fredrik Lundh and contributors.
|
PIL is the Python Imaging Library by Fredrik Lundh and contributors.
|
||||||
As of 2019, Pillow development is
|
Development is supported by:
|
||||||
[supported by Tidelift](https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=readme&utm_campaign=enterprise).
|
- [Tidelift](https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=readme&utm_campaign=enterprise) (since 2018)
|
||||||
|
- [Thanks.dev](https://thanks.dev) (since 2023)
|
||||||
|
- [GitHub Sponsors](https://github.com/sponsors/python-pillow) (since 2026)
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@ -19,6 +19,7 @@ Released as needed for security, installation or critical bug fixes.
|
|||||||
git checkout -t remotes/origin/5.2.x
|
git checkout -t remotes/origin/5.2.x
|
||||||
```
|
```
|
||||||
* [ ] Cherry pick individual commits from `main` branch to release branch e.g. `5.2.x`, then `git push`.
|
* [ ] Cherry pick individual commits from `main` branch to release branch e.g. `5.2.x`, then `git push`.
|
||||||
|
* [ ] If this is a security fix: amend commits to include the CVE identifier in the commit message.
|
||||||
* [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) to confirm passing tests in release branch e.g. `5.2.x`.
|
* [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) to confirm passing tests in release branch e.g. `5.2.x`.
|
||||||
* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py`
|
* [ ] 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`.
|
* [ ] Run pre-release check via `make release-test`.
|
||||||
@ -38,6 +39,7 @@ Released as needed for security, installation or critical bug fixes.
|
|||||||
```bash
|
```bash
|
||||||
git push
|
git push
|
||||||
```
|
```
|
||||||
|
* [ ] If this is a security fix: publish the [GitHub Security Advisory or Advisories](https://github.com/python-pillow/Pillow/security/advisories).
|
||||||
|
|
||||||
## Embargoed release
|
## Embargoed release
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,17 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import io
|
import io
|
||||||
|
import sys
|
||||||
|
import sysconfig
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
FREE_THREADED_BUILD = bool(sysconfig.get_config_var("Py_GIL_DISABLED"))
|
||||||
|
|
||||||
|
gil_enabled_at_start = True
|
||||||
|
if FREE_THREADED_BUILD:
|
||||||
|
gil_enabled_at_start = sys._is_gil_enabled() # type: ignore[attr-defined]
|
||||||
|
|
||||||
|
|
||||||
def pytest_report_header(config: pytest.Config) -> str:
|
def pytest_report_header(config: pytest.Config) -> str:
|
||||||
try:
|
try:
|
||||||
@ -16,6 +24,25 @@ def pytest_report_header(config: pytest.Config) -> str:
|
|||||||
return f"pytest_report_header failed: {e}"
|
return f"pytest_report_header failed: {e}"
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_terminal_summary(terminalreporter: pytest.TerminalReporter) -> None:
|
||||||
|
if (
|
||||||
|
FREE_THREADED_BUILD
|
||||||
|
and not gil_enabled_at_start
|
||||||
|
and sys._is_gil_enabled() # type: ignore[attr-defined]
|
||||||
|
):
|
||||||
|
tr = terminalreporter
|
||||||
|
tr.ensure_newline()
|
||||||
|
tr.section("GIL re-enabled", red=True, bold=True)
|
||||||
|
tr.line("The GIL was re-enabled at runtime during the tests.")
|
||||||
|
tr.line("This can happen with no test failures if the RuntimeWarning")
|
||||||
|
tr.line("raised by Python when this happens is filtered by a test.")
|
||||||
|
tr.line("")
|
||||||
|
tr.line("Please ensure all new C modules declare support for running")
|
||||||
|
tr.line("without the GIL. Any new tests that intentionally imports")
|
||||||
|
tr.line("code that re-enables the GIL should do so in a subprocess.")
|
||||||
|
pytest.exit("GIL re-enabled during tests", returncode=1)
|
||||||
|
|
||||||
|
|
||||||
def pytest_configure(config: pytest.Config) -> None:
|
def pytest_configure(config: pytest.Config) -> None:
|
||||||
config.addinivalue_line(
|
config.addinivalue_line(
|
||||||
"markers",
|
"markers",
|
||||||
|
|||||||
@ -145,14 +145,14 @@ class TestFileAvif:
|
|||||||
|
|
||||||
# avifdec hopper.avif avif/hopper_avif_write.png
|
# avifdec hopper.avif avif/hopper_avif_write.png
|
||||||
assert_image_similar_tofile(
|
assert_image_similar_tofile(
|
||||||
reloaded, "Tests/images/avif/hopper_avif_write.png", 6.88
|
reloaded, "Tests/images/avif/hopper_avif_write.png", 6.93
|
||||||
)
|
)
|
||||||
|
|
||||||
# This test asserts that the images are similar. If the average pixel
|
# This test asserts that the images are similar. If the average pixel
|
||||||
# difference between the two images is less than the epsilon value,
|
# difference between the two images is less than the epsilon value,
|
||||||
# then we're going to accept that it's a reasonable lossy version of
|
# then we're going to accept that it's a reasonable lossy version of
|
||||||
# the image.
|
# the image.
|
||||||
assert_image_similar(reloaded, im, 9.28)
|
assert_image_similar(reloaded, im, 9.39)
|
||||||
|
|
||||||
def test_AvifEncoder_with_invalid_args(self) -> None:
|
def test_AvifEncoder_with_invalid_args(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import io
|
import io
|
||||||
|
import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@ -281,6 +282,11 @@ def test_bytesio_object() -> None:
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_1(filename: str) -> None:
|
def test_1(filename: str) -> None:
|
||||||
|
gs_binary = EpsImagePlugin.gs_binary
|
||||||
|
assert isinstance(gs_binary, str)
|
||||||
|
if subprocess.check_output([gs_binary, "--version"]) == b"10.06.0\n":
|
||||||
|
pytest.skip("Fails with Ghostscript 10.06.0")
|
||||||
|
|
||||||
with Image.open(filename) as im:
|
with Image.open(filename) as im:
|
||||||
assert_image_equal_tofile(im, "Tests/images/eps/1.bmp")
|
assert_image_equal_tofile(im, "Tests/images/eps/1.bmp")
|
||||||
|
|
||||||
|
|||||||
@ -502,8 +502,9 @@ class TestFilePng:
|
|||||||
im = roundtrip(im)
|
im = roundtrip(im)
|
||||||
assert im.info["transparency"] == (248, 248, 248)
|
assert im.info["transparency"] == (248, 248, 248)
|
||||||
|
|
||||||
im = roundtrip(im, transparency=(0, 1, 2))
|
for transparency in ((0, 1, 2), [0, 1, 2]):
|
||||||
assert im.info["transparency"] == (0, 1, 2)
|
im = roundtrip(im, transparency=transparency)
|
||||||
|
assert im.info["transparency"] == (0, 1, 2)
|
||||||
|
|
||||||
def test_trns_p(self, tmp_path: Path) -> None:
|
def test_trns_p(self, tmp_path: Path) -> None:
|
||||||
# Check writing a transparency of 0, issue #528
|
# Check writing a transparency of 0, issue #528
|
||||||
@ -518,6 +519,36 @@ class TestFilePng:
|
|||||||
|
|
||||||
assert_image_equal(im2.convert("RGBA"), im.convert("RGBA"))
|
assert_image_equal(im2.convert("RGBA"), im.convert("RGBA"))
|
||||||
|
|
||||||
|
def test_trns_invalid(self, tmp_path: Path) -> None:
|
||||||
|
out = tmp_path / "temp.png"
|
||||||
|
|
||||||
|
for mode in ("1", "L", "I;16"):
|
||||||
|
im = Image.new(mode, (1, 1))
|
||||||
|
with pytest.raises(
|
||||||
|
ValueError, match=f"transparency for {mode} must be an integer"
|
||||||
|
):
|
||||||
|
im.save(out, transparency="invalid")
|
||||||
|
|
||||||
|
im = Image.new("I", (1, 1))
|
||||||
|
with pytest.warns(DeprecationWarning, match="Saving I mode images as PNG"):
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
im.save(out, transparency="invalid")
|
||||||
|
|
||||||
|
im = Image.new("P", (1, 1))
|
||||||
|
with pytest.raises(
|
||||||
|
ValueError, match="transparency for P must be an integer or bytes"
|
||||||
|
):
|
||||||
|
im.save(out, transparency="invalid")
|
||||||
|
|
||||||
|
im = Image.new("RGB", (1, 1))
|
||||||
|
with pytest.raises(
|
||||||
|
ValueError, match="transparency for RGB must be list or tuple"
|
||||||
|
):
|
||||||
|
im.save(out, transparency="invalid")
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="transparency for RGB must have length 3"):
|
||||||
|
im.save(out, transparency=(1, 2))
|
||||||
|
|
||||||
def test_trns_null(self) -> None:
|
def test_trns_null(self) -> None:
|
||||||
# Check reading images with null tRNS value, issue #1239
|
# Check reading images with null tRNS value, issue #1239
|
||||||
test_file = "Tests/images/tRNS_null_1x1.png"
|
test_file = "Tests/images/tRNS_null_1x1.png"
|
||||||
|
|||||||
@ -49,6 +49,12 @@ class TestFileWebp:
|
|||||||
assert version is not None
|
assert version is not None
|
||||||
assert re.search(r"\d+\.\d+\.\d+$", version)
|
assert re.search(r"\d+\.\d+\.\d+$", version)
|
||||||
|
|
||||||
|
def test_invalid_file(self) -> None:
|
||||||
|
invalid_file = "Tests/images/flower.jpg"
|
||||||
|
|
||||||
|
with pytest.raises(SyntaxError):
|
||||||
|
WebPImagePlugin.WebPImageFile(invalid_file)
|
||||||
|
|
||||||
def test_read_rgb(self) -> None:
|
def test_read_rgb(self) -> None:
|
||||||
"""
|
"""
|
||||||
Can we read a RGB mode WebP file without error?
|
Can we read a RGB mode WebP file without error?
|
||||||
|
|||||||
@ -862,7 +862,7 @@ class TestImage:
|
|||||||
def test_exif_webp(self, tmp_path: Path) -> None:
|
def test_exif_webp(self, tmp_path: Path) -> None:
|
||||||
with Image.open("Tests/images/hopper.webp") as im:
|
with Image.open("Tests/images/hopper.webp") as im:
|
||||||
exif = im.getexif()
|
exif = im.getexif()
|
||||||
assert exif == {}
|
assert dict(exif) == {}
|
||||||
|
|
||||||
out = tmp_path / "temp.webp"
|
out = tmp_path / "temp.webp"
|
||||||
exif[258] = 8
|
exif[258] = 8
|
||||||
@ -884,7 +884,7 @@ class TestImage:
|
|||||||
def test_exif_png(self, tmp_path: Path) -> None:
|
def test_exif_png(self, tmp_path: Path) -> None:
|
||||||
with Image.open("Tests/images/exif.png") as im:
|
with Image.open("Tests/images/exif.png") as im:
|
||||||
exif = im.getexif()
|
exif = im.getexif()
|
||||||
assert exif == {274: 1}
|
assert dict(exif) == {274: 1}
|
||||||
|
|
||||||
out = tmp_path / "temp.png"
|
out = tmp_path / "temp.png"
|
||||||
exif[258] = 8
|
exif[258] = 8
|
||||||
|
|||||||
@ -627,3 +627,37 @@ class TestCoreResampleBox:
|
|||||||
0.4,
|
0.4,
|
||||||
f">>> {size} {box} {flt}",
|
f">>> {size} {box} {flt}",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestCoreResample16bpc:
|
||||||
|
# Lanczos weighting during downsampling can push accumulated float sums
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"offset",
|
||||||
|
(
|
||||||
|
# below 0. These must be clamped to 0, not corrupted byte-by-byte.
|
||||||
|
0, # Left half = 65535, right half = 0
|
||||||
|
# above 65535. These must be clamped to 65535, not corrupted byte-by-byte.
|
||||||
|
50, # # Left half = 0, right half = 65535
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_resampling_clamp_overflow(self, offset: int) -> None:
|
||||||
|
ims = {}
|
||||||
|
width, height = 100, 10
|
||||||
|
for mode in ("I;16", "F"):
|
||||||
|
im = Image.new(mode, (width, height))
|
||||||
|
im.paste(65535, (offset, 0, offset + width // 2, height))
|
||||||
|
|
||||||
|
# 5x downsampling with Lanczos
|
||||||
|
# creates ~8.7% overshoot or undershoot at the step edge
|
||||||
|
ims[mode] = im.resize((20, height), Image.Resampling.LANCZOS)
|
||||||
|
|
||||||
|
for y in range(height):
|
||||||
|
for x in range(20):
|
||||||
|
v = ims["F"].getpixel((x, y))
|
||||||
|
assert isinstance(v, float)
|
||||||
|
expected = max(0, min(65535, round(v)))
|
||||||
|
|
||||||
|
value = ims["I;16"].getpixel((x, y))
|
||||||
|
assert (
|
||||||
|
value == expected
|
||||||
|
), f"Pixel ({x}, {y}): expected {expected}, got {value}"
|
||||||
|
|||||||
@ -365,7 +365,7 @@ def test_rotated_transposed_font(
|
|||||||
bbox_b[2] - bbox_b[0],
|
bbox_b[2] - bbox_b[0],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check top left co-ordinates are correct
|
# Check top left coordinates are correct
|
||||||
assert bbox_b[:2] == (20, 20)
|
assert bbox_b[:2] == (20, 20)
|
||||||
|
|
||||||
# text length is undefined for vertical text
|
# text length is undefined for vertical text
|
||||||
@ -410,7 +410,7 @@ def test_unrotated_transposed_font(
|
|||||||
bbox_b[3] - bbox_b[1],
|
bbox_b[3] - bbox_b[1],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check top left co-ordinates are correct
|
# Check top left coordinates are correct
|
||||||
assert bbox_b[:2] == (20, 20)
|
assert bbox_b[:2] == (20, 20)
|
||||||
|
|
||||||
assert length_a == length_b
|
assert length_a == length_b
|
||||||
|
|||||||
@ -256,6 +256,13 @@ def test_expand_palette(border: int | tuple[int, int, int, int]) -> None:
|
|||||||
assert_image_equal(im_cropped, im)
|
assert_image_equal(im_cropped, im)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("border", ((1,), (1, 2, 3), (1, 2, 3, 4, 5)))
|
||||||
|
def test_expand_invalid_border(border: tuple[int, ...]) -> None:
|
||||||
|
im = Image.new("1", (1, 1))
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
ImageOps.expand(im, border)
|
||||||
|
|
||||||
|
|
||||||
def test_colorize_2color() -> None:
|
def test_colorize_2color() -> None:
|
||||||
# Test the colorizing function with 2-color functionality
|
# Test the colorizing function with 2-color functionality
|
||||||
|
|
||||||
|
|||||||
@ -51,7 +51,7 @@ Many of Pillow's features require external libraries:
|
|||||||
* **littlecms** provides color management
|
* **littlecms** provides color management
|
||||||
|
|
||||||
* Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and
|
* Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and
|
||||||
above uses liblcms2. Tested with **1.19** and **2.7-2.18**.
|
above uses liblcms2. Tested with **1.19** and **2.7-2.19**.
|
||||||
|
|
||||||
* **libwebp** provides the WebP format.
|
* **libwebp** provides the WebP format.
|
||||||
|
|
||||||
@ -116,7 +116,7 @@ Many of Pillow's features require external libraries:
|
|||||||
|
|
||||||
.. Note:: ``redhat-rpm-config`` is required on Fedora 23, but not earlier versions.
|
.. Note:: ``redhat-rpm-config`` is required on Fedora 23, but not earlier versions.
|
||||||
|
|
||||||
Prerequisites for **Ubuntu 16.04 LTS - 24.04 LTS** are installed with::
|
Prerequisites for **Ubuntu 16.04 LTS - 26.04 LTS** are installed with::
|
||||||
|
|
||||||
sudo apt-get install libtiff5-dev libjpeg8-dev libopenjp2-7-dev zlib1g-dev \
|
sudo apt-get install libtiff5-dev libjpeg8-dev libopenjp2-7-dev zlib1g-dev \
|
||||||
libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python3-tk \
|
libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python3-tk \
|
||||||
|
|||||||
@ -31,6 +31,8 @@ These platforms are built and tested for every change.
|
|||||||
+----------------------------------+----------------------------+---------------------+
|
+----------------------------------+----------------------------+---------------------+
|
||||||
| Fedora 43 | 3.14 | x86-64 |
|
| Fedora 43 | 3.14 | x86-64 |
|
||||||
+----------------------------------+----------------------------+---------------------+
|
+----------------------------------+----------------------------+---------------------+
|
||||||
|
| Fedora 44 | 3.14 | x86-64 |
|
||||||
|
+----------------------------------+----------------------------+---------------------+
|
||||||
| Gentoo | 3.13 | x86-64 |
|
| Gentoo | 3.13 | x86-64 |
|
||||||
+----------------------------------+----------------------------+---------------------+
|
+----------------------------------+----------------------------+---------------------+
|
||||||
| macOS 15 Sequoia | 3.11, 3.12, 3.13, 3.14, | arm64 |
|
| macOS 15 Sequoia | 3.11, 3.12, 3.13, 3.14, | arm64 |
|
||||||
@ -42,9 +44,9 @@ These platforms are built and tested for every change.
|
|||||||
+----------------------------------+----------------------------+---------------------+
|
+----------------------------------+----------------------------+---------------------+
|
||||||
| Ubuntu Linux 24.04 LTS (Noble) | 3.10, 3.11, 3.12, 3.13, | x86-64 |
|
| Ubuntu Linux 24.04 LTS (Noble) | 3.10, 3.11, 3.12, 3.13, | x86-64 |
|
||||||
| | 3.14, 3.15, PyPy3 | |
|
| | 3.14, 3.15, PyPy3 | |
|
||||||
| +----------------------------+---------------------+
|
+----------------------------------+----------------------------+---------------------+
|
||||||
| | 3.12 | arm64v8, ppc64le, |
|
| Ubuntu Linux 26.04 LTS (Resolute)| 3.14 | x86-64, arm64v8, |
|
||||||
| | | s390x |
|
| | | ppc64le, s390x |
|
||||||
+----------------------------------+----------------------------+---------------------+
|
+----------------------------------+----------------------------+---------------------+
|
||||||
| Windows Server 2022 | 3.10 | x86 |
|
| Windows Server 2022 | 3.10 | x86 |
|
||||||
+----------------------------------+----------------------------+---------------------+
|
+----------------------------------+----------------------------+---------------------+
|
||||||
|
|||||||
@ -27,6 +27,10 @@ The ImageColor module supports the following string formats:
|
|||||||
as three percentages (0% to 100%). For example, ``rgb(255,0,0)`` and
|
as three percentages (0% to 100%). For example, ``rgb(255,0,0)`` and
|
||||||
``rgb(100%,0%,0%)`` both specify pure red.
|
``rgb(100%,0%,0%)`` both specify pure red.
|
||||||
|
|
||||||
|
* RGBA functions, given as ``rgba(red, green, blue, alpha)`` where the color
|
||||||
|
values and the alpha value are integers in the range 0 to 255. For example,
|
||||||
|
``rgba(255,0,0,128)`` specifies pure red with 50% opacity.
|
||||||
|
|
||||||
* Hue-Saturation-Lightness (HSL) functions, given as ``hsl(hue, saturation%,
|
* Hue-Saturation-Lightness (HSL) functions, given as ``hsl(hue, saturation%,
|
||||||
lightness%)`` where hue is the color given as an angle between 0 and 360
|
lightness%)`` where hue is the color given as an angle between 0 and 360
|
||||||
(red=0, green=120, blue=240), saturation is a value between 0% and 100%
|
(red=0, green=120, blue=240), saturation is a value between 0% and 100%
|
||||||
|
|||||||
@ -13,28 +13,28 @@ introduced in Pillow 10.3.0.
|
|||||||
|
|
||||||
The data being read is now limited to only the necessary amount.
|
The data being read is now limited to only the necessary amount.
|
||||||
|
|
||||||
Fix OOB write with invalid tile extents
|
:cve:`2026-42311`: Fix OOB write with invalid tile extents
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
Pillow 12.1.1 addressed :cve:`2026-25990` by improving checks for tile extents to
|
Pillow 12.1.1 addressed :cve:`2026-25990` by improving checks for tile extents to
|
||||||
prevent an OOB write from specially crafted PSD images in Pillow >= 10.3.0. However,
|
prevent an OOB write from specially crafted PSD images in Pillow >= 10.3.0. However,
|
||||||
these checks did not consider integer overflow. This has been corrected.
|
these checks did not consider integer overflow. This has been corrected.
|
||||||
|
|
||||||
Prevent PDF parsing trailer infinite loop
|
:cve:`2026-42310`: Prevent PDF parsing trailer infinite loop
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
When parsing a PDF, if a trailer refers to itself, or a more complex cyclic loop
|
When parsing a PDF, if a trailer refers to itself, or a more complex cyclic loop
|
||||||
exists, then an infinite loop occurs. Pillow now keeps a record of which trailers it
|
exists, then an infinite loop occurs. Pillow now keeps a record of which trailers it
|
||||||
has already processed. PdfParser was added in Pillow 4.2.0.
|
has already processed. PdfParser was added in Pillow 4.2.0.
|
||||||
|
|
||||||
Integer overflow when processing fonts
|
:cve:`2026-42308`: Integer overflow when processing fonts
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
If a font advances for each glyph by an exceeding large amount, when Pillow keeps track
|
If a font advances for each glyph by an exceedingly large amount, when Pillow keeps
|
||||||
of the current position, it may lead to an integer overflow. This has been fixed.
|
track of the current position, it may lead to an integer overflow. This has been fixed.
|
||||||
|
|
||||||
Heap buffer overflow with nested list coordinates
|
:cve:`2026-42309`: Heap buffer overflow with nested list coordinates
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
Passing nested lists as coordinates to APIs that accept coordinates such as
|
Passing nested lists as coordinates to APIs that accept coordinates such as
|
||||||
``ImagePath.Path``, :py:meth:`~PIL.ImageDraw.ImageDraw.polygon`
|
``ImagePath.Path``, :py:meth:`~PIL.ImageDraw.ImageDraw.polygon`
|
||||||
|
|||||||
@ -81,7 +81,7 @@ Image.alpha_composite: dest
|
|||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
When calling :py:meth:`~PIL.Image.Image.alpha_composite`, the ``dest`` argument now
|
When calling :py:meth:`~PIL.Image.Image.alpha_composite`, the ``dest`` argument now
|
||||||
accepts negative co-ordinates, like the upper left corner of the ``box`` argument of
|
accepts negative coordinates, like the upper left corner of the ``box`` argument of
|
||||||
:py:meth:`~PIL.Image.Image.paste` can be negative. Naturally, this has effect of
|
:py:meth:`~PIL.Image.Image.paste` can be negative. Naturally, this has effect of
|
||||||
cropping the overlaid image.
|
cropping the overlaid image.
|
||||||
|
|
||||||
|
|||||||
2
setup.py
2
setup.py
@ -57,7 +57,7 @@ WEBP_ROOT = None
|
|||||||
ZLIB_ROOT = None
|
ZLIB_ROOT = None
|
||||||
FUZZING_BUILD = "LIB_FUZZING_ENGINE" in os.environ
|
FUZZING_BUILD = "LIB_FUZZING_ENGINE" in os.environ
|
||||||
|
|
||||||
if sys.platform == "win32" and sys.version_info >= (3, 15):
|
if sys.platform == "win32" and sys.version_info >= (3, 16):
|
||||||
import atexit
|
import atexit
|
||||||
|
|
||||||
atexit.register(
|
atexit.register(
|
||||||
|
|||||||
@ -2639,11 +2639,8 @@ class Image:
|
|||||||
if is_path(fp):
|
if is_path(fp):
|
||||||
filename = os.fspath(fp)
|
filename = os.fspath(fp)
|
||||||
open_fp = True
|
open_fp = True
|
||||||
elif fp == sys.stdout:
|
elif fp == sys.stdout and isinstance(sys.stdout, io.TextIOWrapper):
|
||||||
try:
|
fp = sys.stdout.buffer
|
||||||
fp = sys.stdout.buffer
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
if not filename and hasattr(fp, "name") and is_path(fp.name):
|
if not filename and hasattr(fp, "name") and is_path(fp.name):
|
||||||
# only set the name for metadata purposes
|
# only set the name for metadata purposes
|
||||||
filename = os.fspath(fp.name)
|
filename = os.fspath(fp.name)
|
||||||
|
|||||||
@ -175,7 +175,7 @@ class ImageDraw:
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Draw an arc."""
|
"""Draw an arc."""
|
||||||
ink, fill = self._getink(fill)
|
ink, fill = self._getink(fill)
|
||||||
if ink is not None:
|
if ink is not None and width != 0:
|
||||||
self.draw.draw_arc(xy, start, end, ink, width)
|
self.draw.draw_arc(xy, start, end, ink, width)
|
||||||
|
|
||||||
def bitmap(
|
def bitmap(
|
||||||
@ -235,12 +235,12 @@ class ImageDraw:
|
|||||||
self,
|
self,
|
||||||
xy: Coords,
|
xy: Coords,
|
||||||
fill: _Ink | None = None,
|
fill: _Ink | None = None,
|
||||||
width: int = 0,
|
width: int = 1,
|
||||||
joint: str | None = None,
|
joint: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Draw a line, or a connected sequence of line segments."""
|
"""Draw a line, or a connected sequence of line segments."""
|
||||||
ink = self._getink(fill)[0]
|
ink = self._getink(fill)[0]
|
||||||
if ink is not None:
|
if ink is not None and width != 0:
|
||||||
self.draw.draw_lines(xy, ink, width)
|
self.draw.draw_lines(xy, ink, width)
|
||||||
if joint == "curve" and width > 4:
|
if joint == "curve" and width > 4:
|
||||||
points: Sequence[Sequence[float]]
|
points: Sequence[Sequence[float]]
|
||||||
|
|||||||
@ -36,6 +36,9 @@ def _border(border: int | tuple[int, ...]) -> tuple[int, int, int, int]:
|
|||||||
left, top = right, bottom = border
|
left, top = right, bottom = border
|
||||||
elif len(border) == 4:
|
elif len(border) == 4:
|
||||||
left, top, right, bottom = border
|
left, top, right, bottom = border
|
||||||
|
else:
|
||||||
|
msg = "border must be an integer, or a tuple of two or four elements"
|
||||||
|
raise ValueError(msg)
|
||||||
else:
|
else:
|
||||||
left = top = right = bottom = border
|
left = top = right = bottom = border
|
||||||
return left, top, right, bottom
|
return left, top, right, bottom
|
||||||
|
|||||||
@ -148,10 +148,14 @@ def _write_image(
|
|||||||
strip_size=math.ceil(width / 8) * height,
|
strip_size=math.ceil(width / 8) * height,
|
||||||
)
|
)
|
||||||
elif decode_filter == "DCTDecode":
|
elif decode_filter == "DCTDecode":
|
||||||
Image.SAVE["JPEG"](im, op, filename)
|
from . import JpegImagePlugin
|
||||||
|
|
||||||
|
JpegImagePlugin._save(im, op, filename)
|
||||||
elif decode_filter == "JPXDecode":
|
elif decode_filter == "JPXDecode":
|
||||||
|
from . import Jpeg2KImagePlugin
|
||||||
|
|
||||||
del dict_obj["BitsPerComponent"]
|
del dict_obj["BitsPerComponent"]
|
||||||
Image.SAVE["JPEG2000"](im, op, filename)
|
Jpeg2KImagePlugin._save(im, op, filename)
|
||||||
else:
|
else:
|
||||||
msg = f"unsupported PDF filter ({decode_filter})"
|
msg = f"unsupported PDF filter ({decode_filter})"
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
|||||||
@ -383,7 +383,7 @@ class PdfParser:
|
|||||||
msg = "specify buf or f or filename, but not both buf and f"
|
msg = "specify buf or f or filename, but not both buf and f"
|
||||||
raise RuntimeError(msg)
|
raise RuntimeError(msg)
|
||||||
self.filename = filename
|
self.filename = filename
|
||||||
self.buf: bytes | bytearray | mmap.mmap | None = buf
|
self.buf: bytes | bytearray | memoryview | mmap.mmap | None = buf
|
||||||
self.f = f
|
self.f = f
|
||||||
self.start_offset = start_offset
|
self.start_offset = start_offset
|
||||||
self.should_close_buf = False
|
self.should_close_buf = False
|
||||||
@ -435,7 +435,9 @@ class PdfParser:
|
|||||||
self.seek_end()
|
self.seek_end()
|
||||||
|
|
||||||
def close_buf(self) -> None:
|
def close_buf(self) -> None:
|
||||||
if isinstance(self.buf, mmap.mmap):
|
if isinstance(self.buf, memoryview):
|
||||||
|
self.buf.release()
|
||||||
|
elif isinstance(self.buf, mmap.mmap):
|
||||||
self.buf.close()
|
self.buf.close()
|
||||||
self.buf = None
|
self.buf = None
|
||||||
|
|
||||||
@ -690,7 +692,7 @@ class PdfParser:
|
|||||||
self.read_prev_trailer(self.trailer_dict[b"Prev"])
|
self.read_prev_trailer(self.trailer_dict[b"Prev"])
|
||||||
|
|
||||||
def read_prev_trailer(
|
def read_prev_trailer(
|
||||||
self, xref_section_offset: int, processed_offsets: list[int] = []
|
self, xref_section_offset: int, processed_offsets: list[int] | None = None
|
||||||
) -> None:
|
) -> None:
|
||||||
assert self.buf is not None
|
assert self.buf is not None
|
||||||
trailer_offset = self.read_xref_table(xref_section_offset=xref_section_offset)
|
trailer_offset = self.read_xref_table(xref_section_offset=xref_section_offset)
|
||||||
@ -706,6 +708,8 @@ class PdfParser:
|
|||||||
)
|
)
|
||||||
trailer_dict = self.interpret_trailer(trailer_data)
|
trailer_dict = self.interpret_trailer(trailer_data)
|
||||||
if b"Prev" in trailer_dict:
|
if b"Prev" in trailer_dict:
|
||||||
|
if processed_offsets is None:
|
||||||
|
processed_offsets = []
|
||||||
processed_offsets.append(xref_section_offset)
|
processed_offsets.append(xref_section_offset)
|
||||||
check_format_condition(
|
check_format_condition(
|
||||||
trailer_dict[b"Prev"] not in processed_offsets, "trailer loop found"
|
trailer_dict[b"Prev"] not in processed_offsets, "trailer loop found"
|
||||||
|
|||||||
@ -1443,35 +1443,47 @@ def _save(
|
|||||||
palette_bytes += b"\0"
|
palette_bytes += b"\0"
|
||||||
chunk(fp, b"PLTE", palette_bytes)
|
chunk(fp, b"PLTE", palette_bytes)
|
||||||
|
|
||||||
transparency = im.encoderinfo.get("transparency", im.info.get("transparency", None))
|
transparency = im.encoderinfo.get("transparency", im.info.get("transparency"))
|
||||||
|
|
||||||
if transparency or transparency == 0:
|
if transparency is not None:
|
||||||
if im.mode == "P":
|
if im.mode == "P":
|
||||||
# limit to actual palette size
|
# limit to actual palette size
|
||||||
alpha_bytes = colors
|
alpha_bytes = colors
|
||||||
if isinstance(transparency, bytes):
|
if isinstance(transparency, bytes):
|
||||||
chunk(fp, b"tRNS", transparency[:alpha_bytes])
|
chunk(fp, b"tRNS", transparency[:alpha_bytes])
|
||||||
else:
|
elif isinstance(transparency, int):
|
||||||
transparency = max(0, min(255, transparency))
|
transparency = max(0, min(255, transparency))
|
||||||
alpha = b"\xff" * transparency + b"\0"
|
alpha = b"\xff" * transparency + b"\0"
|
||||||
chunk(fp, b"tRNS", alpha[:alpha_bytes])
|
chunk(fp, b"tRNS", alpha[:alpha_bytes])
|
||||||
|
else:
|
||||||
|
msg = "transparency for P must be an integer or bytes"
|
||||||
|
raise ValueError(msg)
|
||||||
elif im.mode in ("1", "L", "I", "I;16"):
|
elif im.mode in ("1", "L", "I", "I;16"):
|
||||||
transparency = max(0, min(65535, transparency))
|
if isinstance(transparency, int):
|
||||||
chunk(fp, b"tRNS", o16(transparency))
|
transparency = max(0, min(65535, transparency))
|
||||||
|
chunk(fp, b"tRNS", o16(transparency))
|
||||||
|
else:
|
||||||
|
msg = f"transparency for {im.mode} must be an integer"
|
||||||
|
raise ValueError(msg)
|
||||||
elif im.mode == "RGB":
|
elif im.mode == "RGB":
|
||||||
red, green, blue = transparency
|
if not isinstance(transparency, (list, tuple)):
|
||||||
chunk(fp, b"tRNS", o16(red) + o16(green) + o16(blue))
|
msg = "transparency for RGB must be list or tuple"
|
||||||
else:
|
raise ValueError(msg)
|
||||||
if "transparency" in im.encoderinfo:
|
elif len(transparency) != 3:
|
||||||
# don't bother with transparency if it's an RGBA
|
msg = "transparency for RGB must have length 3"
|
||||||
# and it's in the info dict. It's probably just stale.
|
raise ValueError(msg)
|
||||||
msg = "cannot use transparency for this mode"
|
else:
|
||||||
raise OSError(msg)
|
red, green, blue = transparency
|
||||||
else:
|
chunk(fp, b"tRNS", o16(red) + o16(green) + o16(blue))
|
||||||
if im.mode == "P" and im.im.getpalettemode() == "RGBA":
|
elif im.encoderinfo.get("transparency") is not None:
|
||||||
alpha = im.im.getpalette("RGBA", "A")
|
# don't bother with transparency if it's an RGBA
|
||||||
alpha_bytes = colors
|
# and it's in the info dict. It's probably just stale.
|
||||||
chunk(fp, b"tRNS", alpha[:alpha_bytes])
|
msg = "cannot use transparency for this mode"
|
||||||
|
raise OSError(msg)
|
||||||
|
elif im.mode == "P" and im.im.getpalettemode() == "RGBA":
|
||||||
|
alpha = im.im.getpalette("RGBA", "A")
|
||||||
|
alpha_bytes = colors
|
||||||
|
chunk(fp, b"tRNS", alpha[:alpha_bytes])
|
||||||
|
|
||||||
if dpi := im.encoderinfo.get("dpi"):
|
if dpi := im.encoderinfo.get("dpi"):
|
||||||
chunk(
|
chunk(
|
||||||
|
|||||||
@ -43,10 +43,15 @@ class WebPImageFile(ImageFile.ImageFile):
|
|||||||
__logical_frame = 0
|
__logical_frame = 0
|
||||||
|
|
||||||
def _open(self) -> None:
|
def _open(self) -> None:
|
||||||
|
assert self.fp is not None
|
||||||
|
s = self.fp.read()
|
||||||
|
if not _accept(s):
|
||||||
|
msg = "not a WEBP file"
|
||||||
|
raise SyntaxError(msg)
|
||||||
|
|
||||||
# Use the newer AnimDecoder API to parse the (possibly) animated file,
|
# Use the newer AnimDecoder API to parse the (possibly) animated file,
|
||||||
# and access muxed chunks like ICC/EXIF/XMP.
|
# and access muxed chunks like ICC/EXIF/XMP.
|
||||||
assert self.fp is not None
|
self._decoder = _webp.WebPAnimDecoder(s)
|
||||||
self._decoder = _webp.WebPAnimDecoder(self.fp.read())
|
|
||||||
|
|
||||||
# Get info from decoder
|
# Get info from decoder
|
||||||
self._size, self.info["loop"], bgcolor, self.n_frames, self.rawmode = (
|
self._size, self.info["loop"], bgcolor, self.n_frames, self.rawmode = (
|
||||||
|
|||||||
@ -3172,8 +3172,8 @@ _draw_lines(ImagingDrawObject *self, PyObject *args) {
|
|||||||
|
|
||||||
PyObject *data;
|
PyObject *data;
|
||||||
int ink;
|
int ink;
|
||||||
int width = 0;
|
int width;
|
||||||
if (!PyArg_ParseTuple(args, "Oi|i", &data, &ink, &width)) {
|
if (!PyArg_ParseTuple(args, "Oii", &data, &ink, &width)) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3182,7 +3182,7 @@ _draw_lines(ImagingDrawObject *self, PyObject *args) {
|
|||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (width <= 1) {
|
if (width == 1) {
|
||||||
double *p = NULL;
|
double *p = NULL;
|
||||||
for (i = 0; i < n - 1; i++) {
|
for (i = 0; i < n - 1; i++) {
|
||||||
p = &xy[i + i];
|
p = &xy[i + i];
|
||||||
|
|||||||
@ -33,8 +33,10 @@ _tkinit(PyObject *self, PyObject *args) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interp = (Tcl_Interp *)PyLong_AsVoidPtr(arg);
|
interp = (Tcl_Interp *)PyLong_AsVoidPtr(arg);
|
||||||
|
if (interp == NULL && PyErr_Occurred()) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
/* This will bomb if interp is invalid... */
|
|
||||||
TkImaging_Init(interp);
|
TkImaging_Init(interp);
|
||||||
|
|
||||||
Py_RETURN_NONE;
|
Py_RETURN_NONE;
|
||||||
|
|||||||
@ -37,8 +37,6 @@
|
|||||||
#define MAX(a, b) (a) > (b) ? (a) : (b)
|
#define MAX(a, b) (a) > (b) ? (a) : (b)
|
||||||
#define MIN(a, b) (a) < (b) ? (a) : (b)
|
#define MIN(a, b) (a) < (b) ? (a) : (b)
|
||||||
|
|
||||||
#define CLIP16(v) ((v) <= 0 ? 0 : (v) >= 65535 ? 65535 : (v))
|
|
||||||
|
|
||||||
/* ITU-R Recommendation 601-2 (assuming nonlinear RGB) */
|
/* ITU-R Recommendation 601-2 (assuming nonlinear RGB) */
|
||||||
#define L(rgb) ((INT32)(rgb)[0] * 299 + (INT32)(rgb)[1] * 587 + (INT32)(rgb)[2] * 114)
|
#define L(rgb) ((INT32)(rgb)[0] * 299 + (INT32)(rgb)[1] * 587 + (INT32)(rgb)[2] * 114)
|
||||||
#define L24(rgb) ((rgb)[0] * 19595 + (rgb)[1] * 38470 + (rgb)[2] * 7471 + 0x8000)
|
#define L24(rgb) ((rgb)[0] * 19595 + (rgb)[1] * 38470 + (rgb)[2] * 7471 + 0x8000)
|
||||||
|
|||||||
@ -27,6 +27,8 @@
|
|||||||
|
|
||||||
#define CLIP8(v) ((v) <= 0 ? 0 : (v) < 256 ? (v) : 255)
|
#define CLIP8(v) ((v) <= 0 ? 0 : (v) < 256 ? (v) : 255)
|
||||||
|
|
||||||
|
#define CLIP16(v) ((v) <= 0 ? 0 : (v) < 65536 ? (v) : 65535)
|
||||||
|
|
||||||
/* This is to work around a bug in GCC prior 4.9 in 64 bit mode.
|
/* This is to work around a bug in GCC prior 4.9 in 64 bit mode.
|
||||||
GCC generates code with partial dependency which is 3 times slower.
|
GCC generates code with partial dependency which is 3 times slower.
|
||||||
See: https://stackoverflow.com/a/26588074/253146 */
|
See: https://stackoverflow.com/a/26588074/253146 */
|
||||||
|
|||||||
@ -812,7 +812,7 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Adjust the tile co-ordinates based on the reduction (OpenJPEG
|
/* Adjust the tile coordinates based on the reduction (OpenJPEG
|
||||||
doesn't do this for us) */
|
doesn't do this for us) */
|
||||||
tile_info.x0 = (tile_info.x0 + correction) >> context->reduce;
|
tile_info.x0 = (tile_info.x0 + correction) >> context->reduce;
|
||||||
tile_info.y0 = (tile_info.y0 + correction) >> context->reduce;
|
tile_info.y0 = (tile_info.y0 + correction) >> context->reduce;
|
||||||
|
|||||||
@ -492,9 +492,9 @@ ImagingResampleHorizontal_16bpc(
|
|||||||
<< 8)) *
|
<< 8)) *
|
||||||
k[x];
|
k[x];
|
||||||
}
|
}
|
||||||
ss_int = ROUND_UP(ss);
|
ss_int = CLIP16(ROUND_UP(ss));
|
||||||
imOut->image8[yy][xx * 2 + (bigendian ? 1 : 0)] = CLIP8(ss_int % 256);
|
imOut->image8[yy][xx * 2 + (bigendian ? 1 : 0)] = ss_int & 0xFF;
|
||||||
imOut->image8[yy][xx * 2 + (bigendian ? 0 : 1)] = CLIP8(ss_int >> 8);
|
imOut->image8[yy][xx * 2 + (bigendian ? 0 : 1)] = ss_int >> 8;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ImagingSectionLeave(&cookie);
|
ImagingSectionLeave(&cookie);
|
||||||
@ -531,9 +531,9 @@ ImagingResampleVertical_16bpc(
|
|||||||
(imIn->image8[y + ymin][xx * 2 + (bigendian ? 0 : 1)] << 8)) *
|
(imIn->image8[y + ymin][xx * 2 + (bigendian ? 0 : 1)] << 8)) *
|
||||||
k[y];
|
k[y];
|
||||||
}
|
}
|
||||||
ss_int = ROUND_UP(ss);
|
ss_int = CLIP16(ROUND_UP(ss));
|
||||||
imOut->image8[yy][xx * 2 + (bigendian ? 1 : 0)] = CLIP8(ss_int % 256);
|
imOut->image8[yy][xx * 2 + (bigendian ? 1 : 0)] = ss_int & 0xFF;
|
||||||
imOut->image8[yy][xx * 2 + (bigendian ? 0 : 1)] = CLIP8(ss_int >> 8);
|
imOut->image8[yy][xx * 2 + (bigendian ? 0 : 1)] = ss_int >> 8;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ImagingSectionLeave(&cookie);
|
ImagingSectionLeave(&cookie);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user