diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt index fd4183aff..c824c10bc 100644 --- a/.ci/requirements-cibw.txt +++ b/.ci/requirements-cibw.txt @@ -1 +1 @@ -cibuildwheel==3.4.0 +cibuildwheel==3.4.1 diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt index c64343a73..ad78bcb67 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -1,4 +1,4 @@ -mypy==1.19.1 +mypy==1.20.2 arro3-compute arro3-core IceSpringPySideStubs-PyQt6 diff --git a/.ci/requirements-sbom.txt b/.ci/requirements-sbom.txt new file mode 100644 index 000000000..762812f80 --- /dev/null +++ b/.ci/requirements-sbom.txt @@ -0,0 +1 @@ +check-jsonschema==0.37.1 diff --git a/.github/INCIDENT_RESPONSE.md b/.github/INCIDENT_RESPONSE.md new file mode 100644 index 000000000..1c2e395dd --- /dev/null +++ b/.github/INCIDENT_RESPONSE.md @@ -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** — +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] \ +> +> Hi \, +> +> 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 — \ — disclosure \ +> +> This is an embargoed notification of a vulnerability in Pillow. Please keep this +> information confidential until the disclosure date listed below. +> +> **CVE:** \ +> +> **Affected versions:** \ +> +> **Fixed version:** \ +> +> **Severity:** \ (CVSS \: \) +> +> **Reporter:** \ +> +> **Public disclosure date:** \ +> +> **Summary:** +> \ +> +> **Proof of concept:** +> \ +> +> **Remediation:** +> Upgrade to Pillow \. 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:** \ +> +> **CVE:** \ +> +> **Affected versions:** Pillow \< \ +> +> **Fixed version:** \ +> +> **Severity:** \ (CVSS \) +> +> **Reporter:** \ +> +> **Details:** +> \ +> +> **Remediation:** +> ``` +> python3 -m pip install --upgrade Pillow +> ``` +> +> **Timeline:** +> - Reported: \ +> - Fixed: \ +> - Disclosed: \ diff --git a/.github/SECURITY.md b/.github/SECURITY.md index 42ff1615b..c9a396aa8 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -1,7 +1,21 @@ # Security policy +## Reporting a vulnerability + To report sensitive vulnerability information, report it [privately on GitHub](https://github.com/python-pillow/Pillow/security/advisories/new). -If you cannot use GitHub, use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. +If you cannot use GitHub, use the [Tidelift security contact](https://tidelift.com/docs/security). Tidelift will coordinate the fix and disclosure. -DO NOT report sensitive vulnerability information in public. +**DO NOT report sensitive vulnerability information in public.** + +## Threat model + +Pillow's primary attack surface is parsing untrusted image data. A full STRIDE threat model covering spoofing, tampering, repudiation, information disclosure, denial of service, and elevation of privilege is maintained in the [Security handbook page](https://pillow.readthedocs.io/en/latest/handbook/security.html). + +Key risks to be aware of when using Pillow to process untrusted images: + +- **Decompression bombs** — do not set `Image.MAX_IMAGE_PIXELS = None` in production. +- **EPS files invoke Ghostscript** — block EPS input at the application layer unless strictly required. +- **`ImageMath.unsafe_eval()`** — never pass user-controlled strings to this function; use `lambda_eval` instead. +- **C extension memory safety** — keep Pillow and its bundled C libraries (libjpeg, libpng, libtiff, libwebp, etc.) up to date. +- **Sandboxing** — for high-risk deployments, run image processing in a sandboxed subprocess. diff --git a/.github/dependencies.json b/.github/dependencies.json new file mode 100644 index 000000000..85db4ca0d --- /dev/null +++ b/.github/dependencies.json @@ -0,0 +1,19 @@ +{ + "brotli": "1.2.0", + "bzip2": "1.0.8", + "freetype": "2.14.3", + "fribidi": "1.0.16", + "harfbuzz": "14.2.0", + "jpegturbo": "3.1.4.1", + "lcms2": "2.19", + "libavif": "1.4.1", + "libimagequant": "4.4.1", + "libpng": "1.6.58", + "libwebp": "1.6.0", + "libxcb": "1.17.0", + "openjpeg": "2.5.4", + "tiff": "4.7.1", + "xz": "5.8.3", + "zlib-ng": "2.3.3", + "zstd": "1.5.7" +} diff --git a/.github/generate-sbom.py b/.github/generate-sbom.py new file mode 100755 index 000000000..3b15a7d91 --- /dev/null +++ b/.github/generate-sbom.py @@ -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 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 " + "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() diff --git a/.github/renovate.json b/.github/renovate.json index 8187fc15b..387630dd6 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -7,16 +7,169 @@ "Dependency" ], "minimumReleaseAge": "7 days", + "prCreation": "not-pending", + "schedule": [ + "* * 3 * *" + ], + "customManagers": [ + { + "customType": "regex", + "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], + "matchStrings": ["\"brotli\":\\s*\"(?\\d+[^\"]*)\""], + "depNameTemplate": "brotli", + "packageNameTemplate": "google/brotli", + "datasourceTemplate": "github-releases", + "extractVersionTemplate": "^v(?.+)$" + }, + { + "customType": "regex", + "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], + "matchStrings": ["\"bzip2\":\\s*\"(?\\d+[^\"]*)\""], + "depNameTemplate": "bzip2", + "packageNameTemplate": "bzip2/bzip2", + "datasourceTemplate": "gitlab-tags", + "extractVersionTemplate": "^bzip2-(?.+)$" + }, + { + "customType": "regex", + "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], + "matchStrings": ["\"freetype\":\\s*\"(?\\d+[^\"]*)\""], + "depNameTemplate": "freetype", + "packageNameTemplate": "freetype/freetype", + "datasourceTemplate": "gitlab-tags", + "registryUrlTemplate": "https://gitlab.freedesktop.org", + "extractVersionTemplate": "^VER-(?[\\d-]+)$", + "versioningTemplate": "regex:^(?\\d+)[.-](?\\d+)[.-](?\\d+)$" + }, + { + "customType": "regex", + "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], + "matchStrings": ["\"fribidi\":\\s*\"(?\\d+[^\"]*)\""], + "depNameTemplate": "fribidi", + "packageNameTemplate": "fribidi/fribidi", + "datasourceTemplate": "github-releases", + "extractVersionTemplate": "^v(?.+)$" + }, + { + "customType": "regex", + "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], + "matchStrings": ["\"harfbuzz\":\\s*\"(?\\d+[^\"]*)\""], + "depNameTemplate": "harfbuzz", + "packageNameTemplate": "harfbuzz/harfbuzz", + "datasourceTemplate": "github-releases" + }, + { + "customType": "regex", + "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], + "matchStrings": ["\"jpegturbo\":\\s*\"(?\\d+[^\"]*)\""], + "depNameTemplate": "jpegturbo", + "packageNameTemplate": "libjpeg-turbo/libjpeg-turbo", + "datasourceTemplate": "github-releases" + }, + { + "customType": "regex", + "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], + "matchStrings": ["\"lcms2\":\\s*\"(?\\d+[^\"]*)\""], + "depNameTemplate": "lcms2", + "packageNameTemplate": "mm2/Little-CMS", + "datasourceTemplate": "github-releases", + "extractVersionTemplate": "^lcms(?.+)$" + }, + { + "customType": "regex", + "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], + "matchStrings": ["\"libavif\":\\s*\"(?\\d+[^\"]*)\""], + "depNameTemplate": "libavif", + "packageNameTemplate": "AOMediaCodec/libavif", + "datasourceTemplate": "github-releases", + "extractVersionTemplate": "^v(?.+)$" + }, + { + "customType": "regex", + "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], + "matchStrings": ["\"libimagequant\":\\s*\"(?\\d+[^\"]*)\""], + "depNameTemplate": "libimagequant", + "packageNameTemplate": "ImageOptim/libimagequant", + "datasourceTemplate": "github-tags" + }, + { + "customType": "regex", + "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], + "matchStrings": ["\"libpng\":\\s*\"(?\\d+[^\"]*)\""], + "depNameTemplate": "libpng", + "packageNameTemplate": "pnggroup/libpng", + "datasourceTemplate": "github-tags", + "extractVersionTemplate": "^v(?.+)$" + }, + { + "customType": "regex", + "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], + "matchStrings": ["\"libwebp\":\\s*\"(?\\d+[^\"]*)\""], + "depNameTemplate": "libwebp", + "packageNameTemplate": "webmproject/libwebp", + "datasourceTemplate": "github-tags", + "extractVersionTemplate": "^v(?.+)$" + }, + { + "customType": "regex", + "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], + "matchStrings": ["\"libxcb\":\\s*\"(?\\d+[^\"]*)\""], + "depNameTemplate": "libxcb", + "packageNameTemplate": "xorg/lib/libxcb", + "datasourceTemplate": "gitlab-tags", + "registryUrlTemplate": "https://gitlab.freedesktop.org", + "extractVersionTemplate": "^libxcb-(?.+)$" + }, + { + "customType": "regex", + "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], + "matchStrings": ["\"openjpeg\":\\s*\"(?\\d+[^\"]*)\""], + "depNameTemplate": "openjpeg", + "packageNameTemplate": "uclouvain/openjpeg", + "datasourceTemplate": "github-releases", + "extractVersionTemplate": "^v(?.+)$" + }, + { + "customType": "regex", + "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], + "matchStrings": ["\"tiff\":\\s*\"(?\\d+[^\"]*)\""], + "depNameTemplate": "tiff", + "packageNameTemplate": "libtiff/libtiff", + "datasourceTemplate": "gitlab-tags", + "extractVersionTemplate": "^v(?.+)$" + }, + { + "customType": "regex", + "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], + "matchStrings": ["\"xz\":\\s*\"(?\\d+[^\"]*)\""], + "depNameTemplate": "xz", + "packageNameTemplate": "tukaani-project/xz", + "datasourceTemplate": "github-releases", + "extractVersionTemplate": "^v(?.+)$" + }, + { + "customType": "regex", + "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], + "matchStrings": ["\"zlib-ng\":\\s*\"(?\\d+[^\"]*)\""], + "depNameTemplate": "zlib-ng", + "packageNameTemplate": "zlib-ng/zlib-ng", + "datasourceTemplate": "github-releases" + }, + { + "customType": "regex", + "managerFilePatterns": ["/^\\.github/dependencies\\.json$/"], + "matchStrings": ["\"zstd\":\\s*\"(?\\d+[^\"]*)\""], + "depNameTemplate": "zstd", + "packageNameTemplate": "facebook/zstd", + "datasourceTemplate": "github-releases", + "extractVersionTemplate": "^v(?.+)$" + } + ], "packageRules": [ { "groupName": "github-actions", - "matchManagers": [ - "github-actions" - ], + "matchManagers": ["github-actions"], "separateMajorMinor": false } - ], - "schedule": [ - "* * 3 * *" ] } diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml index 263700780..a2e1112dc 100644 --- a/.github/workflows/cifuzz.yml +++ b/.github/workflows/cifuzz.yml @@ -4,17 +4,14 @@ on: push: branches: - "**" - paths: + paths: &paths + - ".github/dependencies.json" - ".github/workflows/cifuzz.yml" - ".github/workflows/wheels-dependencies.sh" - "**.c" - "**.h" pull_request: - paths: - - ".github/workflows/cifuzz.yml" - - ".github/workflows/wheels-dependencies.sh" - - "**.c" - - "**.h" + paths: *paths workflow_dispatch: permissions: @@ -33,27 +30,27 @@ jobs: steps: - name: Build Fuzzers id: build - uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master + uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@c11174f47deee98f260dede5d661614bda78ae39 # master with: oss-fuzz-project-name: 'pillow' language: python dry-run: false - name: Run Fuzzers id: run - uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master + uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@c11174f47deee98f260dede5d661614bda78ae39 # master with: oss-fuzz-project-name: 'pillow' fuzz-seconds: 600 language: python dry-run: false - name: Upload New Crash - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: failure() && steps.build.outcome == 'success' with: name: artifacts path: ./out/artifacts - name: Upload Legacy Crash - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: steps.run.outcome == 'success' with: name: crash diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 857881c01..3734a3306 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -4,15 +4,12 @@ on: push: branches: - "**" - paths: + paths: &paths - ".github/workflows/docs.yml" - "docs/**" - "src/PIL/**" pull_request: - paths: - - ".github/workflows/docs.yml" - - "docs/**" - - "src/PIL/**" + paths: *paths workflow_dispatch: permissions: @@ -32,12 +29,12 @@ jobs: name: Docs steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.x" cache: pip @@ -49,21 +46,21 @@ jobs: run: python3 .github/workflows/system-info.py - name: Cache libavif - uses: actions/cache@v5 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 id: cache-libavif with: path: ~/cache-libavif key: ${{ runner.os }}-libavif-${{ hashFiles('depends/install_libavif.sh', 'depends/libavif-svt4.patch') }} - name: Cache libimagequant - uses: actions/cache@v5 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 id: cache-libimagequant with: path: ~/cache-libimagequant key: ${{ runner.os }}-libimagequant-${{ hashFiles('depends/install_imagequant.sh') }} - name: Cache libwebp - uses: actions/cache@v5 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 id: cache-libwebp with: path: ~/cache-libwebp diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index e2f8bf47a..dacf40cc1 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -18,14 +18,14 @@ jobs: runs-on: ubuntu-latest name: Lint steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - - uses: actions/setup-python@v6 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.x" - name: Install uv - uses: astral-sh/setup-uv@v7 + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 - name: Lint run: uvx --with tox-uv tox -e lint - name: Mypy diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index aa8326b78..d62d2c3c2 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -26,6 +26,6 @@ jobs: runs-on: ubuntu-latest steps: # Drafts your next release notes as pull requests are merged into "main" - - uses: release-drafter/release-drafter@v7 + - uses: release-drafter/release-drafter@5de93583980a40bd78603b6dfdcda5b4df377b32 # v7.2.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index e4ccd1aa3..b2dca6dd2 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -25,7 +25,7 @@ jobs: steps: - name: "Check issues" - uses: actions/stale@v10 + uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} only-labels: "Awaiting OP Action" diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 515d77d17..e868b53a8 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -4,19 +4,14 @@ on: push: branches: - "**" - paths-ignore: + paths-ignore: &paths-ignore - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - "docs/**" - "wheels/**" pull_request: - paths-ignore: - - ".github/workflows/docs.yml" - - ".github/workflows/wheels*" - - ".gitmodules" - - "docs/**" - - "wheels/**" + paths-ignore: *paths-ignore workflow_dispatch: permissions: @@ -39,8 +34,8 @@ jobs: os: ["ubuntu-latest"] docker: [ # Run slower jobs first to give them a headstart and reduce waiting time - ubuntu-24.04-noble-ppc64le, - ubuntu-24.04-noble-s390x, + ubuntu-26.04-resolute-ppc64le, + ubuntu-26.04-resolute-s390x, # Then run the remainder alpine, amazon-2023-amd64, @@ -50,24 +45,26 @@ jobs: debian-13-trixie-x86, debian-13-trixie-amd64, fedora-43-amd64, + fedora-44-amd64, gentoo, ubuntu-22.04-jammy-amd64, ubuntu-24.04-noble-amd64, + ubuntu-26.04-resolute-amd64, ] dockerTag: [main] include: - - docker: "ubuntu-24.04-noble-ppc64le" + - docker: "ubuntu-26.04-resolute-ppc64le" qemu-arch: "ppc64le" - - docker: "ubuntu-24.04-noble-s390x" + - docker: "ubuntu-26.04-resolute-s390x" qemu-arch: "s390x" - - docker: "ubuntu-24.04-noble-arm64v8" + - docker: "ubuntu-26.04-resolute-arm64v8" os: "ubuntu-24.04-arm" dockerTag: main name: ${{ matrix.docker }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -76,7 +73,7 @@ jobs: - name: Set up QEMU if: "matrix.qemu-arch" - uses: docker/setup-qemu-action@v4 + uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 with: platforms: ${{ matrix.qemu-arch }} @@ -104,7 +101,7 @@ jobs: .ci/after_success.sh - name: Upload coverage - uses: codecov/codecov-action@v6 + uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 with: flags: GHA_Docker name: ${{ matrix.docker }} diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index 0dc6e2a0c..1c36e06c0 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -4,19 +4,14 @@ on: push: branches: - "**" - paths-ignore: + paths-ignore: &paths-ignore - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - "docs/**" - "wheels/**" pull_request: - paths-ignore: - - ".github/workflows/docs.yml" - - ".github/workflows/wheels*" - - ".gitmodules" - - "docs/**" - - "wheels/**" + paths-ignore: *paths-ignore workflow_dispatch: permissions: @@ -46,7 +41,7 @@ jobs: steps: - name: Checkout Pillow - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -87,7 +82,7 @@ jobs: .ci/test.sh - name: Upload coverage - uses: codecov/codecov-action@v6 + uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 with: files: ./coverage.xml flags: GHA_Windows diff --git a/.github/workflows/test-valgrind-memory.yml b/.github/workflows/test-valgrind-memory.yml index 87eace643..5ceb544ea 100644 --- a/.github/workflows/test-valgrind-memory.yml +++ b/.github/workflows/test-valgrind-memory.yml @@ -8,12 +8,13 @@ on: # branches: # - "**" # paths: - # - ".github/workflows/test-valgrind.yml" + # - ".github/workflows/test-valgrind-memory.yml" # - "**.c" # - "**.h" + # - "depends/docker-test-valgrind-memory.sh" pull_request: paths: - - ".github/workflows/test-valgrind.yml" + - ".github/workflows/test-valgrind-memory.yml" - "**.c" - "**.h" - "depends/docker-test-valgrind-memory.sh" @@ -44,7 +45,7 @@ jobs: name: ${{ matrix.docker }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false diff --git a/.github/workflows/test-valgrind.yml b/.github/workflows/test-valgrind.yml index f14dab616..c47a0d060 100644 --- a/.github/workflows/test-valgrind.yml +++ b/.github/workflows/test-valgrind.yml @@ -6,15 +6,12 @@ on: push: branches: - "**" - paths: + paths: &paths - ".github/workflows/test-valgrind.yml" - "**.c" - "**.h" pull_request: - paths: - - ".github/workflows/test-valgrind.yml" - - "**.c" - - "**.h" + paths: *paths workflow_dispatch: permissions: @@ -42,7 +39,7 @@ jobs: name: ${{ matrix.docker }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 0b2aad283..fa1898df2 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -4,19 +4,14 @@ on: push: branches: - "**" - paths-ignore: + paths-ignore: &paths-ignore - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - "docs/**" - "wheels/**" pull_request: - paths-ignore: - - ".github/workflows/docs.yml" - - ".github/workflows/wheels*" - - ".gitmodules" - - "docs/**" - - "wheels/**" + paths-ignore: *paths-ignore workflow_dispatch: permissions: @@ -49,19 +44,19 @@ jobs: steps: - name: Checkout Pillow - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Checkout cached dependencies - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false repository: python-pillow/pillow-depends path: winbuild\depends - name: Checkout extra test images - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false repository: python-pillow/test-images @@ -69,7 +64,7 @@ jobs: # sets env: pythonLocation - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: ${{ matrix.python-version }} allow-prereleases: true @@ -113,7 +108,7 @@ jobs: - name: Cache build id: build-cache - uses: actions/cache@v5 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: winbuild\build key: @@ -217,7 +212,7 @@ jobs: shell: bash - name: Upload errors - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: failure() with: name: errors @@ -229,7 +224,7 @@ jobs: shell: pwsh - name: Upload coverage - uses: codecov/codecov-action@v6 + uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 with: files: ./coverage.xml flags: GHA_Windows diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d84504a8f..d90cc805a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,19 +4,14 @@ on: push: branches: - "**" - paths-ignore: + paths-ignore: &paths-ignore - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - "docs/**" - "wheels/**" pull_request: - paths-ignore: - - ".github/workflows/docs.yml" - - ".github/workflows/wheels*" - - ".gitmodules" - - "docs/**" - - "wheels/**" + paths-ignore: *paths-ignore workflow_dispatch: permissions: @@ -69,12 +64,12 @@ jobs: name: ${{ matrix.os }} Python ${{ matrix.python-version }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: ${{ matrix.python-version }} allow-prereleases: true @@ -93,7 +88,7 @@ jobs: - name: Cache libavif if: startsWith(matrix.os, 'ubuntu') - uses: actions/cache@v5 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 id: cache-libavif with: path: ~/cache-libavif @@ -101,7 +96,7 @@ jobs: - name: Cache libimagequant if: startsWith(matrix.os, 'ubuntu') - uses: actions/cache@v5 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 id: cache-libimagequant with: path: ~/cache-libimagequant @@ -109,7 +104,7 @@ jobs: - name: Cache libwebp if: startsWith(matrix.os, 'ubuntu') - uses: actions/cache@v5 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 id: cache-libwebp with: path: ~/cache-libwebp @@ -162,7 +157,7 @@ jobs: mkdir -p Tests/errors - name: Upload errors - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: failure() with: name: errors @@ -173,7 +168,7 @@ jobs: .ci/after_success.sh - name: Upload coverage - uses: codecov/codecov-action@v6 + uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 with: flags: ${{ matrix.os == 'ubuntu-latest' && 'GHA_Ubuntu' || 'GHA_macOS' }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 7750a2e07..12593b3f5 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -89,22 +89,23 @@ fi ARCHIVE_SDIR=pillow-depends-main -# Package versions for fresh source builds. -FREETYPE_VERSION=2.14.3 -HARFBUZZ_VERSION=13.2.1 -LIBPNG_VERSION=1.6.56 -JPEGTURBO_VERSION=3.1.4.1 -OPENJPEG_VERSION=2.5.4 -XZ_VERSION=5.8.3 -ZSTD_VERSION=1.5.7 -TIFF_VERSION=4.7.1 -LCMS2_VERSION=2.18 -ZLIB_NG_VERSION=2.3.3 -LIBWEBP_VERSION=1.6.0 -BZIP2_VERSION=1.0.8 -LIBXCB_VERSION=1.17.0 -BROTLI_VERSION=1.2.0 -LIBAVIF_VERSION=1.4.1 +VERSIONS_FILE="$PROJECTDIR/.github/dependencies.json" +_get_ver() { python3 -c "import json; print(json.load(open('$VERSIONS_FILE'))['$1'])"; } +FREETYPE_VERSION=$(_get_ver freetype) +HARFBUZZ_VERSION=$(_get_ver harfbuzz) +LIBPNG_VERSION=$(_get_ver libpng) +JPEGTURBO_VERSION=$(_get_ver jpegturbo) +OPENJPEG_VERSION=$(_get_ver openjpeg) +XZ_VERSION=$(_get_ver xz) +ZSTD_VERSION=$(_get_ver zstd) +TIFF_VERSION=$(_get_ver tiff) +LCMS2_VERSION=$(_get_ver lcms2) +ZLIB_NG_VERSION=$(_get_ver zlib-ng) +LIBWEBP_VERSION=$(_get_ver libwebp) +BZIP2_VERSION=$(_get_ver bzip2) +LIBXCB_VERSION=$(_get_ver libxcb) +BROTLI_VERSION=$(_get_ver brotli) +LIBAVIF_VERSION=$(_get_ver libavif) function build_pkg_config { if [ -e pkg-config-stamp ]; then return; fi diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index a786f9939..e0edb3ac0 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -10,9 +10,12 @@ on: # │ │ │ │ │ - cron: "42 1 * * 0,3" push: - paths: + paths: &paths - ".ci/requirements-cibw.txt" - - ".github/workflows/wheel*" + - ".ci/requirements-sbom.txt" + - ".github/dependencies.json" + - ".github/generate-sbom.py" + - ".github/workflows/wheels*" - "pyproject.toml" - "setup.py" - "wheels/*" @@ -21,14 +24,7 @@ on: tags: - "*" pull_request: - paths: - - ".ci/requirements-cibw.txt" - - ".github/workflows/wheel*" - - "pyproject.toml" - - "setup.py" - - "wheels/*" - - "winbuild/build_prepare.py" - - "winbuild/fribidi.cmake" + paths: *paths workflow_dispatch: permissions: @@ -107,12 +103,12 @@ jobs: os: macos-15-intel cibw_arch: x86_64_iphonesimulator steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false submodules: true - - uses: actions/setup-python@v6 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.x" @@ -130,7 +126,7 @@ jobs: CIBW_ENABLE: cpython-prerelease pypy MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }} - - uses: actions/upload-artifact@v7 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: dist-${{ matrix.name }} path: ./wheelhouse/*.whl @@ -150,18 +146,18 @@ jobs: - cibw_arch: ARM64 os: windows-11-arm steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Checkout extra test images - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false repository: python-pillow/test-images path: Tests\test-images - - uses: actions/setup-python@v6 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.x" @@ -210,13 +206,13 @@ jobs: shell: bash - name: Upload wheels - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: dist-windows-${{ matrix.cibw_arch }} path: ./wheelhouse/*.whl - name: Upload fribidi.dll - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: fribidi-windows-${{ matrix.cibw_arch }} path: winbuild\build\bin\fribidi* @@ -225,18 +221,18 @@ jobs: if: github.event_name != 'schedule' || github.event.repository.fork == false runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.x" - run: make sdist - - uses: actions/upload-artifact@v7 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: dist-sdist path: dist/*.tar.gz @@ -246,7 +242,7 @@ jobs: runs-on: ubuntu-latest name: Count dists steps: - - uses: actions/download-artifact@v8 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: pattern: dist-* path: dist @@ -268,17 +264,64 @@ jobs: name: release-anaconda url: https://anaconda.org/channels/scientific-python-nightly-wheels/packages/pillow/overview steps: - - uses: actions/download-artifact@v8 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: pattern: dist-!(sdist)* path: dist merge-multiple: true - name: Upload wheels to scientific-python-nightly-wheels - uses: scientific-python/upload-nightly-action@5748273c71e2d8d3a61f3a11a16421c8954f9ecf # 0.6.3 + uses: scientific-python/upload-nightly-action@e76cfec8a4611fd02808a801b0ff5a7d7c1b2d99 # 0.6.4 with: artifacts_path: dist anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }} + sbom: + runs-on: ubuntu-latest + name: Generate SBOM + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: "3.x" + + - name: Generate CycloneDX SBOM + run: python3 .github/generate-sbom.py + + - name: Upload SBOM as workflow artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: sbom + path: "pillow-*.cdx.json" + + - name: Validate SBOM + run: | + python3 -m pip install -r .ci/requirements-sbom.txt + check-jsonschema --schemafile "https://raw.githubusercontent.com/CycloneDX/specification/1.7/schema/bom-1.7.schema.json" pillow-*.cdx.json + + sbom-publish: + if: | + github.event.repository.fork == false + && github.event_name == 'push' + && startsWith(github.ref, 'refs/tags') + needs: [count-dists, sbom] + runs-on: ubuntu-latest + name: Publish SBOM to GitHub release + permissions: + contents: write + steps: + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: sbom + path: . + + - name: Attach SBOM to GitHub release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh release upload "$GITHUB_REF_NAME" pillow-*.cdx.json + pypi-publish: if: github.event.repository.fork == false && github.event_name == 'push' && startsWith(github.ref, 'refs/tags') needs: count-dists @@ -290,12 +333,12 @@ jobs: permissions: id-token: write steps: - - uses: actions/download-artifact@v8 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: pattern: dist-* path: dist merge-multiple: true - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 with: attestations: true diff --git a/.github/zizmor.yml b/.github/zizmor.yml deleted file mode 100644 index 100026562..000000000 --- a/.github/zizmor.yml +++ /dev/null @@ -1,6 +0,0 @@ -# https://docs.zizmor.sh/configuration/ -rules: - unpinned-uses: - config: - policies: - "*": ref-pin diff --git a/.gitignore b/.gitignore index 3033c2ea7..4e9803957 100644 --- a/.gitignore +++ b/.gitignore @@ -97,3 +97,6 @@ pillow-test-images.zip # pyinstaller *.spec + +# Generated SBOM +pillow-*.cdx.json diff --git a/README.md b/README.md index c6d09a821..b4d83c2a9 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,10 @@ Pillow is the friendly PIL fork by [Jeffrey 'Alex' Clark and contributors](https://github.com/python-pillow/Pillow/graphs/contributors). PIL is the Python Imaging Library by Fredrik Lundh and contributors. -As of 2019, Pillow development is -[supported by Tidelift](https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=readme&utm_campaign=enterprise). +Development is supported by: +- [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) diff --git a/RELEASING.md b/RELEASING.md index 3c6188c82..fcf108943 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -19,6 +19,7 @@ Released as needed for security, installation or critical bug fixes. 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`. +* [ ] 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`. * [ ] 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`. @@ -38,6 +39,7 @@ Released as needed for security, installation or critical bug fixes. ```bash 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 diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index d4e8db4f4..d41bab307 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -1,6 +1,7 @@ from __future__ import annotations import io +import subprocess from pathlib import Path import pytest @@ -281,6 +282,11 @@ def test_bytesio_object() -> 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: assert_image_equal_tofile(im, "Tests/images/eps/1.bmp") diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 8fe2a5eac..734bcde92 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -502,8 +502,9 @@ class TestFilePng: im = roundtrip(im) assert im.info["transparency"] == (248, 248, 248) - im = roundtrip(im, transparency=(0, 1, 2)) - assert im.info["transparency"] == (0, 1, 2) + for transparency in ((0, 1, 2), [0, 1, 2]): + im = roundtrip(im, transparency=transparency) + assert im.info["transparency"] == (0, 1, 2) def test_trns_p(self, tmp_path: Path) -> None: # Check writing a transparency of 0, issue #528 @@ -518,6 +519,36 @@ class TestFilePng: 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: # Check reading images with null tRNS value, issue #1239 test_file = "Tests/images/tRNS_null_1x1.png" diff --git a/Tests/test_image.py b/Tests/test_image.py index dad269361..cda2ec9ca 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -871,7 +871,7 @@ class TestImage: def test_exif_webp(self, tmp_path: Path) -> None: with Image.open("Tests/images/hopper.webp") as im: exif = im.getexif() - assert exif == {} + assert dict(exif) == {} out = tmp_path / "temp.webp" exif[258] = 8 @@ -893,7 +893,7 @@ class TestImage: def test_exif_png(self, tmp_path: Path) -> None: with Image.open("Tests/images/exif.png") as im: exif = im.getexif() - assert exif == {274: 1} + assert dict(exif) == {274: 1} out = tmp_path / "temp.png" exif[258] = 8 diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index 73b25ed51..f188be81e 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -627,3 +627,37 @@ class TestCoreResampleBox: 0.4, 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}" diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index 07cb69719..180682c64 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -35,7 +35,7 @@ class TestImageGrab: ImageGrab.grab() ImageGrab.grab(xdisplay="") - except OSError as e: + except (OSError, subprocess.CalledProcessError) as e: pytest.skip(str(e)) @pytest.mark.skipif(Image.core.HAVE_XCB, reason="tests missing XCB") diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 35fe3bb8a..31b7abecd 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -256,6 +256,13 @@ def test_expand_palette(border: int | tuple[int, int, int, int]) -> None: 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: # Test the colorizing function with 2-color functionality diff --git a/docs/handbook/index.rst b/docs/handbook/index.rst index acdeff7db..23255eb8a 100644 --- a/docs/handbook/index.rst +++ b/docs/handbook/index.rst @@ -8,3 +8,4 @@ Handbook tutorial concepts appendices + security diff --git a/docs/handbook/security.rst b/docs/handbook/security.rst new file mode 100644 index 000000000..c13389134 --- /dev/null +++ b/docs/handbook/security.rst @@ -0,0 +1,259 @@ +Security +======== + +Pillow's primary attack surface is **parsing untrusted image data**. This page +documents the threat model for developers integrating Pillow into applications +that handle images from untrusted sources, along with recommended mitigations. + +To report a vulnerability see :ref:`security-reporting`. + +.. _security-threat-model: + +Threat model (STRIDE) +--------------------- + +The analysis below follows the `STRIDE +`_ framework and covers the +boundary between untrusted image input and the Pillow API. + +.. code-block:: text + + ┌──────────────────────────────────────────┐ + Untrusted zone │ Pillow API │ + ───────────── │ │ + Image files ────►│ Image.open() ──► Format plugins │ + Byte streams │ (40+ parsers) (Python + C FFI) │ + User metadata │ │ + │ ImageMath.unsafe_eval(expr) ───────────┼──► Python eval() + │ ImageShow.show(image) ─────────────────┼──► os.system / subprocess + │ EpsImagePlugin.open(eps) ──────────────┼──► Ghostscript (gs) + └──────────────┬───────────────────────────┘ + │ C extensions: + │ _imaging · _imagingft · _imagingcms + │ _webp · _avif · _imagingtk + │ _imagingmath · _imagingmorph + ▼ + ┌──────────────────────────────────────────┐ + │ C libraries (bundled or system) │ + │ libjpeg · libpng · libtiff · libwebp │ + │ openjpeg · freetype · littlecms2 │ + └──────────────────────────────────────────┘ + +Spoofing +^^^^^^^^ + +**S-1 — Format sniffing bypass** + +``Image.open()`` detects format by magic bytes, not file extension or MIME +type. An attacker can name a file ``safe.png`` while its content is TIFF, JPEG +2000, or EPS, causing a different — potentially more dangerous — parser to run. + +*Mitigations:* validate MIME type and magic bytes independently before calling +``Image.open()``; pass the ``formats`` argument with an allowlist of accepted +formats. + +**S-2 — Plugin registry spoofing** + +Pillow's format registry is a global mutable dictionary. A malicious package +installed in the same environment could register a replacement parser for a +well-known format. + +*Mitigations:* use isolated virtual environments with pinned, hash-verified +dependencies; audit ``Image.registered_extensions()`` at startup. + +Tampering +^^^^^^^^^ + +**T-1 — Malicious metadata propagation** + +Pillow preserves EXIF, XMP, IPTC, ICC profiles, and comments when +round-tripping images. Applications that store or render metadata without +sanitisation are vulnerable to second-order injection (SQLi, XSS, command +injection). + +*Mitigations:* treat all values from ``image.info``, ``image._getexif()``, +``image.getexif()``, and ``image.text`` as untrusted; sanitise before storing +or rendering; strip metadata when it is not required. + +**T-2 — Covert data channel (steganography)** + +Pillow does not remove hidden data (JPEG comments, PNG text chunks) when +re-saving. An attacker can embed data that survives the +encode-decode cycle invisibly. + +*Mitigations:* to guarantee a clean output when saving, create a new image instance via +``image.copy()`` and delete the ``image.info`` contents. + +**T-3 — Supply chain tampering** + +Pre-compiled wheels bundle libjpeg-turbo, libpng, libtiff, libwebp, openjpeg, +freetype, littlecms2, and other libraries. A compromised PyPI release or build pipeline +could ship malicious binaries. + +*Mitigations:* pin with hash verification +(``python3 -m pip install --require-hashes``); monitor `Pillow security advisories +`_; use +Dependabot or OSV-Scanner for bundled C library CVEs. + +Repudiation +^^^^^^^^^^^ + +**R-1 — No structured audit trail** + +Without application-level logging there is no record of which images were +opened, what formats were detected, or what operations were performed, making +forensic investigation harder after an incident. + +*Mitigations:* log the filename/hash, detected format, and dimensions of every +image processed; log and alert on ``Image.DecompressionBombWarning``, +``Image.DecompressionBombError``, and ``PIL.UnidentifiedImageError``. + +Information disclosure +^^^^^^^^^^^^^^^^^^^^^^ + +**I-1 — Metadata in saved images** + +GPS coordinates, author names, software version strings, and ICC profiles can +be inadvertently included in output images served publicly. + +*Mitigations:* explicitly strip EXIF and XMP on save (set ``exif=b""``, +``icc_profile=None``, omit ``pnginfo``); verify output with ``exiftool`` in CI. + +**I-2 — Temporary file exposure** + +Several code paths write pixel data to temporary files via +``tempfile.mkstemp()``. Exception paths can leave these files behind on shared +filesystems. + +*Mitigations:* files are created with mode ``0o600``; mount ``/tmp`` as a +per-container ``tmpfs``; ensure ``try/finally`` cleanup is in place. + +Denial of service +^^^^^^^^^^^^^^^^^ + +**D-1 — Decompression bomb** + +A small compressed image can expand to gigabytes in memory. +:py:data:`PIL.Image.MAX_IMAGE_PIXELS` raises +``Image.DecompressionBombError`` at 2× the limit and +``Image.DecompressionBombWarning`` at 1×. PNG text chunks are +separately capped by ``PngImagePlugin.MAX_TEXT_CHUNK`` and +``MAX_TEXT_MEMORY``. Check the values in your installed Pillow version at +runtime or in the reference/source for the current defaults. + +*Mitigations:* **never** set ``Image.MAX_IMAGE_PIXELS = None`` in production; +treat ``Image.DecompressionBombWarning`` as an error; set OS/container memory limits +per worker. + +**D-2 — CPU exhaustion** + +Large-but-legal images (within ``MAX_IMAGE_PIXELS``) can still saturate CPU +through high-quality resampling, convolution filters, or complex draw +operations. + +*Mitigations:* apply per-request CPU time limits; set a practical dimension +ceiling below ``MAX_IMAGE_PIXELS``; rate-limit processing requests. + +**D-3 — Algorithmic complexity in parsers** + +Formats such as TIFF (nested IFD chains), animated GIF/WebP (many frames), and +PNG (many text chunks) can exhaust CPU or memory before pixel data is decoded. + +*Mitigations:* restrict accepted formats to the minimum required; enforce a +file-size limit before passing data to Pillow; use per-request timeouts. + +Elevation of privilege +^^^^^^^^^^^^^^^^^^^^^^ + +**E-1 — C extension memory corruption (RCE)** + +Pillow's ~87 C source files and its bundled C libraries process +attacker-controlled bytes. Historical CVEs include buffer overflows, integer +overflows, and use-after-free vulnerabilities that allow arbitrary code +execution. + +*Mitigations:* keep Pillow and all C libraries up to date; compile with +hardening flags (ASLR, stack canaries, PIE, ``_FORTIFY_SOURCE=2``); run image +processing in a sandboxed subprocess (seccomp-bpf, AppArmor, or a restricted +container). + +**E-2 — Ghostscript exploitation via EPS (RCE)** + +Opening an EPS file invokes the system Ghostscript binary (``gs``) via +``subprocess``. Ghostscript has a long history of sandbox-escape CVEs +permitting arbitrary code execution from malicious PostScript. + +*Mitigations:* **block EPS files** at the application input layer before +passing files to Pillow; if EPS must be supported, run Ghostscript in a fully +isolated sandbox with no network and no sensitive mounts. Pillow does not +provide a stable public API for unregistering individual format plugins, so do +not rely on mutating internal registries such as ``Image.OPEN`` as a security +control. + + +**E-3 — ImageMath.unsafe_eval() code injection** + +:py:meth:`~PIL.ImageMath.unsafe_eval` calls Python's built-in ``eval()`` with +only a minimal ``__builtins__`` restriction, which can be bypassed via +introspection. Any user-controlled string passed to this function results in +arbitrary code execution. + +*Mitigations:* **never** pass user-controlled strings to +``ImageMath.unsafe_eval()``; use :py:meth:`~PIL.ImageMath.lambda_eval` instead, +which accepts a Python callable and never calls ``eval``. + +**E-4 — Font path traversal via ImageFont** + +``ImageFont.truetype(font, size)`` passes the filename to the FreeType C +library. If font paths are constructed from user input without +canonicalisation, an attacker may supply a path like +``../../../../etc/passwd``. + +*Mitigations:* never construct font paths from user input; if font selection +must be user-driven, resolve names against an explicit allowlist of +pre-validated absolute paths. + +.. _security-recommendations: + +Recommendations +--------------- + +The following mitigations are listed in priority order. + +1. **Sandbox image processing** — run Pillow workers in a seccomp/AppArmor + restricted subprocess, isolated from the main application process. +2. **Block or sandbox EPS** — reject EPS at the application boundary, or run + Ghostscript in an isolated container. +3. **Never use** ``ImageMath.unsafe_eval()`` **with user input** — migrate all + callers to :py:meth:`~PIL.ImageMath.lambda_eval`. +4. **Keep all dependencies current** — Pillow and its C library dependencies + (including libjpeg, libpng, libtiff, libwebp, openjpeg, freetype, + littlecms2, Ghostscript, and others). Subscribe to `Pillow security + advisories `_. +5. **Enforce** ``MAX_IMAGE_PIXELS`` — never set it to ``None``; treat + ``Image.DecompressionBombWarning`` as an error. +6. **Allowlist image formats** — restrict accepted formats when opening + images, for example with ``Image.open(..., formats=...)``, and isolate + installs/environments if you need to minimise supported formats. +7. **Strip metadata on output** — never pass through EXIF/XMP/ICC from user + uploads to publicly served images. +8. **Sanitise all metadata** returned by Pillow before using it downstream. +9. **Pin dependencies with hash verification** — use + ``pip install --require-hashes`` and lockfiles. +10. **Log and alert** on ``Image.DecompressionBombWarning``, + ``Image.DecompressionBombError``, ``PIL.UnidentifiedImageError``, + and all exceptions from ``Image.open()``. + +.. _security-reporting: + +Reporting a vulnerability +------------------------- + +To report sensitive vulnerability information, report it `privately on GitHub +`_. + +If you cannot use GitHub, use the `Tidelift security contact +`_. Tidelift will coordinate the fix and +disclosure. + +**Do not report sensitive vulnerability information in public.** diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index 1655b8f60..ecb892e1f 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -51,7 +51,7 @@ Many of Pillow's features require external libraries: * **littlecms** provides color management * Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and - above uses liblcms2. Tested with **1.19** and **2.7-2.18**. + above uses liblcms2. Tested with **1.19** and **2.7-2.19**. * **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. - 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 \ libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python3-tk \ diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index 0d6bc2777..90321d054 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -31,6 +31,8 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Fedora 43 | 3.14 | x86-64 | +----------------------------------+----------------------------+---------------------+ +| Fedora 44 | 3.14 | x86-64 | ++----------------------------------+----------------------------+---------------------+ | Gentoo | 3.13 | x86-64 | +----------------------------------+----------------------------+---------------------+ | 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 | | | 3.14, 3.15, PyPy3 | | -| +----------------------------+---------------------+ -| | 3.12 | arm64v8, ppc64le, | -| | | s390x | ++----------------------------------+----------------------------+---------------------+ +| Ubuntu Linux 26.04 LTS (Resolute)| 3.14 | x86-64, arm64v8, | +| | | ppc64le, s390x | +----------------------------------+----------------------------+---------------------+ | Windows Server 2022 | 3.10 | x86 | +----------------------------------+----------------------------+---------------------+ diff --git a/docs/reference/ImageColor.rst b/docs/reference/ImageColor.rst index 68e228dba..82bd359bc 100644 --- a/docs/reference/ImageColor.rst +++ b/docs/reference/ImageColor.rst @@ -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 ``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%, 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% diff --git a/docs/releasenotes/12.2.0.rst b/docs/releasenotes/12.2.0.rst index 0fee9fd82..da678a47b 100644 --- a/docs/releasenotes/12.2.0.rst +++ b/docs/releasenotes/12.2.0.rst @@ -13,28 +13,28 @@ introduced in Pillow 10.3.0. 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 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. -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 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. -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 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 ``ImagePath.Path``, :py:meth:`~PIL.ImageDraw.ImageDraw.polygon` diff --git a/src/PIL/Image.py b/src/PIL/Image.py index cd14bfb85..26366af30 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2639,11 +2639,8 @@ class Image: if is_path(fp): filename = os.fspath(fp) open_fp = True - elif fp == sys.stdout: - try: - fp = sys.stdout.buffer - except AttributeError: - pass + elif fp == sys.stdout and isinstance(sys.stdout, io.TextIOWrapper): + fp = sys.stdout.buffer if not filename and hasattr(fp, "name") and is_path(fp.name): # only set the name for metadata purposes filename = os.fspath(fp.name) diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 42b10bd7b..f0ae142b9 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -36,6 +36,9 @@ def _border(border: int | tuple[int, ...]) -> tuple[int, int, int, int]: left, top = right, bottom = border elif len(border) == 4: left, top, right, bottom = border + else: + msg = "border must be an integer, or a tuple of two or four elements" + raise ValueError(msg) else: left = top = right = bottom = border return left, top, right, bottom diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index 2a5ade773..b0b32b1c9 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -383,7 +383,7 @@ class PdfParser: msg = "specify buf or f or filename, but not both buf and f" raise RuntimeError(msg) self.filename = filename - self.buf: bytes | bytearray | mmap.mmap | None = buf + self.buf: bytes | bytearray | memoryview | mmap.mmap | None = buf self.f = f self.start_offset = start_offset self.should_close_buf = False @@ -435,7 +435,9 @@ class PdfParser: self.seek_end() 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 = None diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 76a15bd0d..3f21fa48e 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -1443,35 +1443,47 @@ def _save( palette_bytes += b"\0" 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": # limit to actual palette size alpha_bytes = colors if isinstance(transparency, bytes): chunk(fp, b"tRNS", transparency[:alpha_bytes]) - else: + elif isinstance(transparency, int): transparency = max(0, min(255, transparency)) alpha = b"\xff" * transparency + b"\0" 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"): - transparency = max(0, min(65535, transparency)) - chunk(fp, b"tRNS", o16(transparency)) + if isinstance(transparency, int): + 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": - red, green, blue = transparency - chunk(fp, b"tRNS", o16(red) + o16(green) + o16(blue)) - else: - if "transparency" in im.encoderinfo: - # don't bother with transparency if it's an RGBA - # and it's in the info dict. It's probably just stale. - msg = "cannot use transparency for this mode" - raise OSError(msg) - else: - if im.mode == "P" and im.im.getpalettemode() == "RGBA": - alpha = im.im.getpalette("RGBA", "A") - alpha_bytes = colors - chunk(fp, b"tRNS", alpha[:alpha_bytes]) + if not isinstance(transparency, (list, tuple)): + msg = "transparency for RGB must be list or tuple" + raise ValueError(msg) + elif len(transparency) != 3: + msg = "transparency for RGB must have length 3" + raise ValueError(msg) + else: + red, green, blue = transparency + chunk(fp, b"tRNS", o16(red) + o16(green) + o16(blue)) + elif im.encoderinfo.get("transparency") is not None: + # don't bother with transparency if it's an RGBA + # and it's in the info dict. It's probably just stale. + msg = "cannot use transparency for this mode" + raise OSError(msg) + 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"): chunk( diff --git a/src/_imagingtk.c b/src/_imagingtk.c index 68d7bf4cd..7b9607cb5 100644 --- a/src/_imagingtk.c +++ b/src/_imagingtk.c @@ -33,8 +33,10 @@ _tkinit(PyObject *self, PyObject *args) { } interp = (Tcl_Interp *)PyLong_AsVoidPtr(arg); + if (interp == NULL && PyErr_Occurred()) { + return NULL; + } - /* This will bomb if interp is invalid... */ TkImaging_Init(interp); Py_RETURN_NONE; diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c index 002497c32..f156810ff 100644 --- a/src/libImaging/Convert.c +++ b/src/libImaging/Convert.c @@ -37,8 +37,6 @@ #define MAX(a, b) (a) > (b) ? (a) : (b) #define MIN(a, b) (a) < (b) ? (a) : (b) -#define CLIP16(v) ((v) <= 0 ? 0 : (v) >= 65535 ? 65535 : (v)) - /* ITU-R Recommendation 601-2 (assuming nonlinear RGB) */ #define L(rgb) ((INT32)(rgb)[0] * 299 + (INT32)(rgb)[1] * 587 + (INT32)(rgb)[2] * 114) #define L24(rgb) ((rgb)[0] * 19595 + (rgb)[1] * 38470 + (rgb)[2] * 7471 + 0x8000) diff --git a/src/libImaging/ImagingUtils.h b/src/libImaging/ImagingUtils.h index 714458ad0..a362780d0 100644 --- a/src/libImaging/ImagingUtils.h +++ b/src/libImaging/ImagingUtils.h @@ -27,6 +27,8 @@ #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. GCC generates code with partial dependency which is 3 times slower. See: https://stackoverflow.com/a/26588074/253146 */ diff --git a/src/libImaging/Resample.c b/src/libImaging/Resample.c index fea00eea0..1647dca14 100644 --- a/src/libImaging/Resample.c +++ b/src/libImaging/Resample.c @@ -492,9 +492,9 @@ ImagingResampleHorizontal_16bpc( << 8)) * k[x]; } - ss_int = ROUND_UP(ss); - imOut->image8[yy][xx * 2 + (bigendian ? 1 : 0)] = CLIP8(ss_int % 256); - imOut->image8[yy][xx * 2 + (bigendian ? 0 : 1)] = CLIP8(ss_int >> 8); + ss_int = CLIP16(ROUND_UP(ss)); + imOut->image8[yy][xx * 2 + (bigendian ? 1 : 0)] = ss_int & 0xFF; + imOut->image8[yy][xx * 2 + (bigendian ? 0 : 1)] = ss_int >> 8; } } ImagingSectionLeave(&cookie); @@ -531,9 +531,9 @@ ImagingResampleVertical_16bpc( (imIn->image8[y + ymin][xx * 2 + (bigendian ? 0 : 1)] << 8)) * k[y]; } - ss_int = ROUND_UP(ss); - imOut->image8[yy][xx * 2 + (bigendian ? 1 : 0)] = CLIP8(ss_int % 256); - imOut->image8[yy][xx * 2 + (bigendian ? 0 : 1)] = CLIP8(ss_int >> 8); + ss_int = CLIP16(ROUND_UP(ss)); + imOut->image8[yy][xx * 2 + (bigendian ? 1 : 0)] = ss_int & 0xFF; + imOut->image8[yy][xx * 2 + (bigendian ? 0 : 1)] = ss_int >> 8; } } ImagingSectionLeave(&cookie); diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 3b16da58a..f55c82112 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -1,6 +1,7 @@ from __future__ import annotations import argparse +import json import os import platform import re @@ -8,6 +9,7 @@ import shutil import struct import subprocess import sys +from pathlib import Path from typing import Any @@ -112,30 +114,17 @@ ARCHITECTURES = { "ARM64": {"vcvars_arch": "x86_arm64", "msbuild_arch": "ARM64"}, } -V = { - "BROTLI": "1.2.0", - "FREETYPE": "2.14.3", - "FRIBIDI": "1.0.16", - "HARFBUZZ": "13.2.1", - "JPEGTURBO": "3.1.4.1", - "LCMS2": "2.18", - "LIBAVIF": "1.4.1", - "LIBIMAGEQUANT": "4.4.1", - "LIBPNG": "1.6.56", - "LIBWEBP": "1.6.0", - "OPENJPEG": "2.5.4", - "TIFF": "4.7.1", - "XZ": "5.8.3", - "ZLIBNG": "2.3.3", -} -V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2]) +V = json.loads( + (Path(__file__).parents[1] / ".github" / "dependencies.json").read_text() +) +V["libpng-xy"] = "".join(V["libpng"].split(".")[:2]) # dependencies, listed in order of compilation DEPS: dict[str, dict[str, Any]] = { "libjpeg": { - "url": f"https://github.com/libjpeg-turbo/libjpeg-turbo/releases/download/{V['JPEGTURBO']}/libjpeg-turbo-{V['JPEGTURBO']}.tar.gz", - "filename": f"libjpeg-turbo-{V['JPEGTURBO']}.tar.gz", + "url": f"https://github.com/libjpeg-turbo/libjpeg-turbo/releases/download/{V['jpegturbo']}/libjpeg-turbo-{V['jpegturbo']}.tar.gz", + "filename": f"libjpeg-turbo-{V['jpegturbo']}.tar.gz", "license": ["README.ijg", "LICENSE.md"], "license_pattern": ( "(LEGAL ISSUES\n============\n\n.+?)\n\nREFERENCES\n==========" @@ -162,8 +151,8 @@ DEPS: dict[str, dict[str, Any]] = { "bins": ["djpeg.exe"], }, "zlib": { - "url": f"https://github.com/zlib-ng/zlib-ng/archive/refs/tags/{V['ZLIBNG']}.tar.gz", - "filename": f"zlib-ng-{V['ZLIBNG']}.tar.gz", + "url": f"https://github.com/zlib-ng/zlib-ng/archive/refs/tags/{V['zlib-ng']}.tar.gz", + "filename": f"zlib-ng-{V['zlib-ng']}.tar.gz", "license": "LICENSE.md", "patch": { r"CMakeLists.txt": { @@ -179,8 +168,8 @@ DEPS: dict[str, dict[str, Any]] = { "libs": [r"zlib.lib"], }, "xz": { - "url": f"https://github.com/tukaani-project/xz/releases/download/v{V['XZ']}/FILENAME", - "filename": f"xz-{V['XZ']}.tar.gz", + "url": f"https://github.com/tukaani-project/xz/releases/download/v{V['xz']}/FILENAME", + "filename": f"xz-{V['xz']}.tar.gz", "license": "COPYING", "build": [ *cmds_cmake("liblzma", "-DBUILD_SHARED_LIBS:BOOL=OFF"), @@ -192,7 +181,7 @@ DEPS: dict[str, dict[str, Any]] = { }, "libwebp": { "url": "http://downloads.webmproject.org/releases/webp/FILENAME", - "filename": f"libwebp-{V['LIBWEBP']}.tar.gz", + "filename": f"libwebp-{V['libwebp']}.tar.gz", "license": "COPYING", "patch": { r"src\enc\picture_csp_enc.c": { @@ -213,7 +202,7 @@ DEPS: dict[str, dict[str, Any]] = { }, "libtiff": { "url": "https://download.osgeo.org/libtiff/FILENAME", - "filename": f"tiff-{V['TIFF']}.tar.gz", + "filename": f"tiff-{V['tiff']}.tar.gz", "license": "LICENSE.md", "patch": { r"libtiff\tif_lzma.c": { @@ -237,22 +226,22 @@ DEPS: dict[str, dict[str, Any]] = { "libs": [r"libtiff\*.lib"], }, "libpng": { - "url": f"{SF_PROJECTS}/libpng/files/libpng{V['LIBPNG_XY']}/{V['LIBPNG']}/" + "url": f"{SF_PROJECTS}/libpng/files/libpng{V['libpng-xy']}/{V['libpng']}/" f"FILENAME/download", - "filename": f"libpng-{V['LIBPNG']}.tar.gz", + "filename": f"libpng-{V['libpng']}.tar.gz", "license": "LICENSE", "build": [ *cmds_cmake("png_static", "-DPNG_SHARED:BOOL=OFF", "-DPNG_TESTS:BOOL=OFF"), cmd_copy( - f"libpng{V['LIBPNG_XY']}_static.lib", f"libpng{V['LIBPNG_XY']}.lib" + f"libpng{V['libpng-xy']}_static.lib", f"libpng{V['libpng-xy']}.lib" ), ], "headers": [r"png*.h"], - "libs": [f"libpng{V['LIBPNG_XY']}.lib"], + "libs": [f"libpng{V['libpng-xy']}.lib"], }, "brotli": { - "url": f"https://github.com/google/brotli/archive/refs/tags/v{V['BROTLI']}.tar.gz", - "filename": f"brotli-{V['BROTLI']}.tar.gz", + "url": f"https://github.com/google/brotli/archive/refs/tags/v{V['brotli']}.tar.gz", + "filename": f"brotli-{V['brotli']}.tar.gz", "license": "LICENSE", "build": [ *cmds_cmake(("brotlicommon", "brotlidec"), "-DBUILD_SHARED_LIBS:BOOL=OFF"), @@ -262,7 +251,7 @@ DEPS: dict[str, dict[str, Any]] = { }, "freetype": { "url": "https://download.savannah.gnu.org/releases/freetype/FILENAME", - "filename": f"freetype-{V['FREETYPE']}.tar.gz", + "filename": f"freetype-{V['freetype']}.tar.gz", "license": ["LICENSE.TXT", r"docs\FTL.TXT", r"docs\GPLv2.TXT"], "patch": { r"builds\windows\vc2010\freetype.vcxproj": { @@ -275,7 +264,7 @@ DEPS: dict[str, dict[str, Any]] = { "": "FT_CONFIG_OPTION_SYSTEM_ZLIB;FT_CONFIG_OPTION_USE_PNG;FT_CONFIG_OPTION_USE_HARFBUZZ;FT_CONFIG_OPTION_USE_BROTLI", # noqa: E501 "": r"{dir_harfbuzz}\src;{inc_dir}", # noqa: E501 "": "{lib_dir}", # noqa: E501 - "": f"zlib.lib;libpng{V['LIBPNG_XY']}.lib;brotlicommon.lib;brotlidec.lib", # noqa: E501 + "": f"zlib.lib;libpng{V['libpng-xy']}.lib;brotlicommon.lib;brotlidec.lib", # noqa: E501 }, r"src/autofit/afshaper.c": { # link against harfbuzz.lib @@ -295,8 +284,8 @@ DEPS: dict[str, dict[str, Any]] = { "libs": [r"objs\{msbuild_arch}\Release Static\freetype.lib"], }, "lcms2": { - "url": f"{SF_PROJECTS}/lcms/files/lcms/{V['LCMS2']}/FILENAME/download", - "filename": f"lcms2-{V['LCMS2']}.tar.gz", + "url": f"{SF_PROJECTS}/lcms/files/lcms/{V['lcms2']}/FILENAME/download", + "filename": f"lcms2-{V['lcms2']}.tar.gz", "license": "LICENSE", "patch": { r"Projects\VC2022\lcms2_static\lcms2_static.vcxproj": { @@ -320,21 +309,21 @@ DEPS: dict[str, dict[str, Any]] = { "libs": [r"Lib\MS\*.lib"], }, "openjpeg": { - "url": f"https://github.com/uclouvain/openjpeg/archive/v{V['OPENJPEG']}.tar.gz", - "filename": f"openjpeg-{V['OPENJPEG']}.tar.gz", + "url": f"https://github.com/uclouvain/openjpeg/archive/v{V['openjpeg']}.tar.gz", + "filename": f"openjpeg-{V['openjpeg']}.tar.gz", "license": "LICENSE", "build": [ *cmds_cmake( "openjp2", "-DBUILD_CODEC:BOOL=OFF", "-DBUILD_SHARED_LIBS:BOOL=OFF" ), - cmd_mkdir(rf"{{inc_dir}}\openjpeg-{V['OPENJPEG']}"), - cmd_copy(r"src\lib\openjp2\*.h", rf"{{inc_dir}}\openjpeg-{V['OPENJPEG']}"), + cmd_mkdir(rf"{{inc_dir}}\openjpeg-{V['openjpeg']}"), + cmd_copy(r"src\lib\openjp2\*.h", rf"{{inc_dir}}\openjpeg-{V['openjpeg']}"), ], "libs": [r"bin\*.lib"], }, "libimagequant": { - "url": "https://github.com/ImageOptim/libimagequant/archive/{V['LIBIMAGEQUANT']}.tar.gz", - "filename": f"libimagequant-{V['LIBIMAGEQUANT']}.tar.gz", + "url": "https://github.com/ImageOptim/libimagequant/archive/{V['libimagequant']}.tar.gz", + "filename": f"libimagequant-{V['libimagequant']}.tar.gz", "license": "COPYRIGHT", "build": [ cmd_cd("imagequant-sys"), @@ -344,8 +333,8 @@ DEPS: dict[str, dict[str, Any]] = { "libs": [r"..\target\release\imagequant_sys.lib"], }, "harfbuzz": { - "url": f"https://github.com/harfbuzz/harfbuzz/releases/download/{V['HARFBUZZ']}/FILENAME", - "filename": f"harfbuzz-{V['HARFBUZZ']}.tar.xz", + "url": f"https://github.com/harfbuzz/harfbuzz/releases/download/{V['harfbuzz']}/FILENAME", + "filename": f"harfbuzz-{V['harfbuzz']}.tar.xz", "license": "COPYING", "build": [ *cmds_cmake( @@ -358,11 +347,11 @@ DEPS: dict[str, dict[str, Any]] = { "libs": [r"*.lib"], }, "fribidi": { - "url": f"https://github.com/fribidi/fribidi/archive/v{V['FRIBIDI']}.zip", - "filename": f"fribidi-{V['FRIBIDI']}.zip", + "url": f"https://github.com/fribidi/fribidi/archive/v{V['fribidi']}.zip", + "filename": f"fribidi-{V['fribidi']}.zip", "license": "COPYING", "build": [ - cmd_copy(r"COPYING", rf"{{bin_dir}}\fribidi-{V['FRIBIDI']}-COPYING"), + cmd_copy(r"COPYING", rf"{{bin_dir}}\fribidi-{V['fribidi']}-COPYING"), cmd_copy(r"{winbuild_dir}\fribidi.cmake", r"CMakeLists.txt"), # generated tab.i files cannot be cross-compiled " ^&^& ".join( @@ -376,8 +365,8 @@ DEPS: dict[str, dict[str, Any]] = { "bins": [r"*.dll"], }, "libavif": { - "url": f"https://github.com/AOMediaCodec/libavif/archive/v{V['LIBAVIF']}.tar.gz", - "filename": f"libavif-{V['LIBAVIF']}.tar.gz", + "url": f"https://github.com/AOMediaCodec/libavif/archive/v{V['libavif']}.tar.gz", + "filename": f"libavif-{V['libavif']}.tar.gz", "license": "LICENSE", "build": [ "rustup update",