Compare commits

...

179 Commits
12.2.0 ... main

Author SHA1 Message Date
Hugo van Kemenade
877527cefc
Fix typo (#9632) 2026-05-15 12:41:28 +03:00
Andrew Murray
dcd6d41e77 Fixed typo 2026-05-15 18:42:04 +10:00
Hugo van Kemenade
94ec04d33e
Switch iOS back to macos-26-intel (#9631) 2026-05-15 11:41:19 +03:00
Andrew Murray
764e315923 Revert "Switch iOS back to macos-15-intel"
This reverts commit 27de86483d.
2026-05-15 15:02:42 +10:00
Hugo van Kemenade
0802206cfc
Update free-threading CI (#9625) 2026-05-14 15:02:56 +03:00
Hugo van Kemenade
e7556b19b7
Don't use list as default in PdfParser read_prev_trailer (#9629) 2026-05-14 14:09:33 +03:00
Hugo van Kemenade
3f74d08263
Remove default sep="="
Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com>
2026-05-14 14:05:55 +03:00
Hugo van Kemenade
e9855d1705
Remove unused params
Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com>
2026-05-13 13:30:01 +03:00
danigm
f0f67f8cf8
PdfParser: Fix typing in read_prev_trailer
Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com>
2026-05-13 11:37:39 +02:00
Hugo van Kemenade
c48b302602
Consistently use "coordinates" instead of "co-ordinates" (#9628) 2026-05-13 12:25:09 +03:00
Daniel Garcia Moreno
78ee80a6fd PdfParser: Don't use list as def in read_prev_trailer
It's not recommended to use the empty list as default value in functions
or methods because Python interpreter evaluates during parsing, so it
will be the same list for different calls.

https://pylint.pycqa.org/en/latest/user_guide/messages/warning/dangerous-default-value.html
2026-05-13 11:14:41 +02:00
Andrew Murray
381e264e18 Consistently use "coordinates" instead of "co-ordinates"
Co-authored-by: mokashang <mokashang@users.noreply.github.com>
2026-05-13 19:13:21 +10:00
Andrew Murray
9289863c2c
Add support for Python 3.15 (#9624) 2026-05-13 07:54:40 +10:00
Hugo van Kemenade
4dc442fb01 Don't force PYTHON_GIL=0, instead fail if anything re-enables 2026-05-12 23:45:03 +03:00
Hugo van Kemenade
0582f43bad No longer test experimental 3.13t 2026-05-12 20:41:07 +03:00
Hugo van Kemenade
22e47e38bb Simplify setting PYTHON_GIL 2026-05-12 20:41:07 +03:00
Hugo van Kemenade
954269051b
Do not draw line or arc if width is zero (#9589) 2026-05-12 19:55:18 +03:00
Andrew Murray
3ce681240f
Use _accept check in WebP _open (#9605) 2026-05-12 12:11:38 +10:00
Hugo van Kemenade
ea5901535d
Compare dist sizes vs latest PyPI release (#9621)
Co-authored-by: Andrew Murray <radarhere@users.noreply.github.com>
2026-05-12 07:31:03 +10:00
Andrew Murray
24696af889
Increase AVIF test epsilon for riscv64 (#9606) 2026-05-08 19:50:29 +10:00
Hugo van Kemenade
7be56cc100
Do not generate SBOM in scheduled run on fork (#9620) 2026-05-07 22:59:33 +03:00
Andrew Murray
70713d69b0 Do not generate SBOM in scheduled run on fork 2026-05-07 23:53:24 +10:00
Andrew Murray
894c5d5335 Width is always provided 2026-05-07 19:48:08 +10:00
Andrew Murray
f693a3a0e5
Use plugin method directly when saving PDFs (#9547) 2026-05-06 23:51:16 +10:00
mergify[bot]
6a05e34b69
[pre-commit.ci] pre-commit autoupdate (#9617) 2026-05-04 17:58:03 +00:00
pre-commit-ci[bot]
903065f5e9
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.15.9 → v0.15.12](https://github.com/astral-sh/ruff-pre-commit/compare/v0.15.9...v0.15.12)
- [github.com/pre-commit/mirrors-clang-format: v22.1.2 → v22.1.4](https://github.com/pre-commit/mirrors-clang-format/compare/v22.1.2...v22.1.4)
- [github.com/python-jsonschema/check-jsonschema: 0.37.1 → 0.37.2](https://github.com/python-jsonschema/check-jsonschema/compare/0.37.1...0.37.2)
- [github.com/zizmorcore/zizmor-pre-commit: v1.23.1 → v1.24.1](https://github.com/zizmorcore/zizmor-pre-commit/compare/v1.23.1...v1.24.1)
- [github.com/tox-dev/pyproject-fmt: v2.21.0 → v2.21.1](https://github.com/tox-dev/pyproject-fmt/compare/v2.21.0...v2.21.1)
2026-05-04 17:17:50 +00:00
renovate[bot]
689a7f37fd
Update google/oss-fuzz digest to d872252 (#9614) 2026-05-04 21:45:55 +10:00
Hugo van Kemenade
599ddd368c
Set Renovate prCreation to not-pending (#9616) 2026-05-04 13:44:13 +03:00
Andrew Murray
ab25042353 Set prCreation to not-pending 2026-05-04 19:42:55 +10:00
Hugo van Kemenade
1cd2d0f67a
Update dependency lcms2 to v2.19 (#9609) 2026-05-03 19:06:59 +03:00
Hugo van Kemenade
5f469b6bd2
Update dependency libpng to v1.6.58 (#9608) 2026-05-03 19:06:30 +03:00
Hugo van Kemenade
ead2f34515
Update dependency harfbuzz to v14 (#9610) 2026-05-03 19:06:13 +03:00
Hugo van Kemenade
a6fc9992f2
Update dependency mypy to v1.20.2 (#9599) 2026-05-03 19:05:49 +03:00
Andrew Murray
2128d6465c Do not draw line or arc if width is zero 2026-05-03 22:41:33 +10:00
Andrew Murray
4bba24632f Update docs 2026-05-03 22:13:11 +10:00
Andrew Murray
21790fc0da Check if sys.stdout is a TextIOWrapper instance 2026-05-03 13:26:42 +03:00
Andrew Murray
c234720aca Convert Exif to dictionary before checking 2026-05-03 13:26:42 +03:00
renovate[bot]
575b33d811 Update dependency mypy to v1.20.2 2026-05-03 13:26:42 +03:00
Hugo van Kemenade
82614324ed
Raise error if PNG transparency has incorrect type or length when saving (#9536) 2026-05-03 13:25:49 +03:00
renovate[bot]
32b6c5f0ee
Update dependency harfbuzz to v14 2026-05-03 10:25:32 +00:00
renovate[bot]
956d434c68
Update dependency lcms2 to v2.19 2026-05-03 10:25:27 +00:00
renovate[bot]
3bbb7a2a04
Update dependency libpng to v1.6.58 2026-05-03 10:25:22 +00:00
Hugo van Kemenade
b656f900b4
If PdfParser buffer is memoryview, release it when closing (#9596) 2026-05-03 13:23:51 +03:00
Hugo van Kemenade
586604d0c3
Update github-actions (#9611) 2026-05-03 10:20:37 +03:00
renovate[bot]
d92b826c4a
Update github-actions 2026-05-03 06:03:07 +00:00
renovate[bot]
2d02654c54
Update dependency cibuildwheel to v3.4.1 (#9607) 2026-05-03 14:11:33 +10:00
Hayato Ikoma
7e4ca8b3ab
Correct integer overflow in 16-bit resampling (#9480)
Co-authored-by: Andrew Murray <radarhere@users.noreply.github.com>
2026-05-02 14:36:20 +10:00
Hugo van Kemenade
be8563347b
SBOM: Use real versions from dependencies.json (#9593) 2026-05-01 00:05:37 +03:00
Hugo van Kemenade
fc47d07603
No need to sort a sorted list 2026-04-30 16:17:39 +03:00
Hugo van Kemenade
7fe1b9ee04
Restrict SBOM upload to only Pillow JSON (#9598) 2026-04-30 16:13:24 +03:00
Andrew Murray
4af29fb732 Restrict SBOM upload to Pillow JSON 2026-04-30 18:41:41 +10:00
Andrew Murray
1f3b8a831d If PdfParser buffer is memoryview, release it when closing 2026-04-30 00:13:37 +10:00
Andrew Murray
0ef81c33af
Add Fedora 44 (#9594) 2026-04-29 10:30:17 +10:00
Hugo van Kemenade
3dda1d190f Git ignore generated SBOM 2026-04-28 15:58:33 +03:00
Hugo van Kemenade
f2ee74b2f8 Use versions from dependencies.json, remove historical 'tested on' 2026-04-28 15:58:33 +03:00
Hugo van Kemenade
99869f0313 Sort things alphabetically to make easier to find 2026-04-28 15:52:41 +03:00
Andrew Murray
fe054a1b3f
Added CVEs to 12.2.0 release notes (#9591)
Co-authored-by: Andrew Murray <radarhere@users.noreply.github.com>
2026-04-28 08:53:21 +10:00
Hugo van Kemenade
852a832832
Deduplicate path triggers in workflows (#9590) 2026-04-27 18:35:58 +03:00
Hugo van Kemenade
755b73b274 Deduplicate path triggers in workflows 2026-04-27 14:14:13 +03:00
Hugo van Kemenade
f0fe496315 Fix typo to trigger on self change 2026-04-27 13:44:52 +03:00
Hugo van Kemenade
fba17910aa
Test Ubuntu 26.04 LTS (Resolute Raccoon) (#9587) 2026-04-26 12:05:56 +03:00
Jeffrey 'Alex' Clark
d2b20102e4
Generate CycloneDX SBOM at release time via CI (#9550)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com>
Co-authored-by: Andrew Murray <radarhere@users.noreply.github.com>
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Jan Kowalleck <jan.kowalleck@gmail.com>
2026-04-26 00:35:21 +03:00
Hugo van Kemenade
8c522096e8 Archive non-amd64 variants of 24.04 2026-04-25 14:38:17 +03:00
Hugo van Kemenade
855774a175 Test Ubuntu 26.04
Co-authored-by: Andrew Murray <radarhere@users.noreply.github.com>
2026-04-25 14:06:06 +03:00
Hugo van Kemenade
2ae2c4e84f
Skip EPS test_1 for Ghostscript 10.06.0 (#9588) 2026-04-25 08:58:02 +03:00
Andrew Murray
a908c62460 Skip test_1 for Ghostscript 10.06.0 2026-04-25 13:19:01 +10:00
Andrew Murray
53800d4fcf
Raise ValueError if ImageOps border has unsupported format (#9426) 2026-04-24 21:10:05 +10:00
Andrew Murray
a0cd878bed
Check PyLong_AsVoidPtr result (#9548) 2026-04-24 21:04:00 +10:00
Jeffrey 'Alex' Clark
4e0aeba4af
Revise development support information in README (#9583) 2026-04-22 22:22:50 -04:00
Jeffrey 'Alex' Clark
5f9112e862
Update README.md
Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com>
2026-04-22 22:22:33 -04:00
Jeffrey 'Alex' Clark
9605fccf00
Revise development support information in README
Updated development support section with new sponsors.
2026-04-22 21:25:52 -04:00
Jeffrey 'Alex' Clark
1382fc4767
Add INCIDENT_RESPONSE.md (#9555) 2026-04-22 20:12:57 -04:00
Jeffrey 'Alex' Clark
c8c391b9c0 Update .github/INCIDENT_RESPONSE.md
Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com>
2026-04-22 20:11:03 -04:00
Jeffrey 'Alex' Clark
ecef4fb33f
Add STRIDE threat model to security docs (#9562) 2026-04-22 12:33:03 -04:00
Jeffrey 'Alex' Clark
0cb00acc92 Update docs/handbook/security.rst
Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com>
2026-04-22 12:32:08 -04:00
Jeffrey 'Alex' Clark
da06640873 docs: fix nested inline markup in E-3 and E-4 headings
RST does not allow inline markup (backticks) nested inside bold
markers. Remove backticks from the E-3 and E-4 heading text so
they render correctly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-21 11:58:06 -04:00
Jeffrey 'Alex' Clark
d3b73ea462
Update docs/handbook/security.rst
Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com>
2026-04-21 11:33:48 -04:00
Jeffrey 'Alex' Clark
5af49b380e docs: address Andrew's review comments on security.rst
- Add image.getexif() alongside image._getexif() in T-1 mitigations
- Remove 'appended bytes' from T-2 (Pillow does not preserve them on resave)
- Reframe R-1 threat as user-facing (not Pillow dev advice); add
  DecompressionBombError to the log/alert list
- Add blank line before E-3 heading
- Qualify dependency list in recommendation #4 as non-exhaustive

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-21 11:32:36 -04:00
Jeffrey 'Alex' Clark
1f026416f9
Update docs/handbook/security.rst
Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com>
2026-04-21 11:23:54 -04:00
Jeffrey 'Alex' Clark
114e4d5695 docs: list all 8 C extensions in security threat model diagram
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-21 11:22:58 -04:00
Jeffrey 'Alex' Clark
2911422753 s/littlecms/littlecms2/ 2026-04-21 11:11:00 -04:00
Jeffrey 'Alex' Clark
13433dc0a9 Update docs/handbook/security.rst
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-21 11:10:19 -04:00
Hugo van Kemenade
9f6a6a6921
Catch subprocess.CalledProcessError in test_grab_x11 (#9578) 2026-04-21 13:17:20 +03:00
Andrew Murray
9867b51d89 Catch subprocess.CalledProcessError in test_grab_x11 2026-04-21 07:51:50 +10:00
Hugo van Kemenade
087376dc18
Hash pin GitHub Actions (#9568) 2026-04-17 17:18:41 +03:00
Hugo van Kemenade
2593703e51 Hash pin GitHub Actions 2026-04-17 15:54:41 +03:00
Jeffrey 'Alex' Clark
74e07b5b8a Lint 2026-04-16 06:48:09 -04:00
Jeffrey 'Alex' Clark
07b20b3b33 Remove Sensitive exception messages 2026-04-16 06:45:55 -04:00
Jeffrey 'Alex' Clark
0c0bdf8d5a Update security docs
- docs/handbook/security.rst
- .github/SECURITY.md

Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com>
2026-04-15 13:07:37 -04:00
Jeffrey 'Alex' Clark
b300e78838 Update docs/handbook/security.rst
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-15 13:07:30 -04:00
Andrew Murray
b893310045
Reorder renovate.json (#9565) 2026-04-16 00:22:23 +10:00
Andrew Murray
b27ae0b2fd Reorder to match dependencies order 2026-04-15 22:46:51 +10:00
Andrew Murray
237ab0763c Remove unneeded ? from matchStrings regex 2026-04-15 22:46:51 +10:00
Andrew Murray
ff00aaa6d3 Use keys from dependencies JSON 2026-04-15 22:46:51 +10:00
Andrew Murray
658d9ce258 Updated wheels path regex 2026-04-15 22:46:51 +10:00
Hugo van Kemenade
433e46471e
Move dependency versions to single JSON and enable Renovate (#9559) 2026-04-15 15:43:14 +03:00
Jeffrey 'Alex' Clark
082cf04e85
Add python-pillow GitHub Sponsors to FUNDING.yml (#9563) 2026-04-14 22:39:25 -04:00
Jeffrey 'Alex' Clark
2d89dcc7eb
Update .github/FUNDING.yml
Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com>
2026-04-14 22:37:55 -04:00
Jeffrey 'Alex' Clark
b71b4b98d9 Lint 2026-04-14 19:56:59 -04:00
Jeffrey 'Alex' Clark
c07f7e56a1 Add python-pillow GitHub Sponsors to FUNDING.yml
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-14 19:54:25 -04:00
Jeffrey 'Alex' Clark
9f24881521 Add STRIDE threat model to security docs
- Update .github/SECURITY.md with threat model summary and link to handbook
- Add docs/handbook/security.rst with full STRIDE analysis (14 threats
  across Spoofing, Tampering, Repudiation, Information Disclosure,
  Denial of Service, and Elevation of Privilege categories)
- Add prioritised mitigation recommendations
- Link security.rst into the handbook toctree

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-14 12:13:45 -04:00
Jeffrey 'Alex' Clark
a124ed208f Update template wording 2026-04-14 11:36:33 -04:00
Jeffrey 'Alex' Clark
ee24a11073 Update .github/INCIDENT_RESPONSE.md
Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com>
2026-04-14 11:26:03 -04:00
Hugo van Kemenade
6dd03edba8
Use GitLab as data source for FreeType
Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com>
2026-04-13 15:39:38 +03:00
Hugo van Kemenade
65767a0cf7
Use GitLab as data source for libtiff
Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com>
2026-04-12 12:08:07 +03:00
Hugo van Kemenade
a49c63208a Move dependency versions to single JSON and enable Renovate 2026-04-12 12:07:07 +03:00
Andrew Murray
3a3dab8bb0
Updated raqm to 0.10.5 (#9557) 2026-04-12 15:13:32 +10:00
Andrew Murray
4b911c889b
Correct environment URL (#9558) 2026-04-11 20:22:22 +10:00
Hugo van Kemenade
b04c9a3d2f
Add CVEs to 12.2.0 release notes (#9556) 2026-04-11 11:03:38 +03:00
Andrew Murray
3157407762
Remove or protect secrets in Actions (#9544) 2026-04-11 17:05:49 +10:00
Andrew Murray
fb1375d93b Added CVEs 2026-04-11 08:34:08 +10:00
Jeffrey 'Alex' Clark
6e1ccab749 Address review feedback on INCIDENT_RESPONSE.md
- Update CVSS v3.1 to CVSS 4.0 throughout
- Remove 'Direct maintainer contact' from detection sources
- Fix 'before it stays public' wording for user bug reports
- Simplify sections 7.3 and 7.4 to reference RELEASING.md instead
  of duplicating release process steps
- Update RELEASING.md Point release section with security-specific
  steps (amend CVE in commits, publish GitHub Security Advisory)
- Fix PyPI API tokens entry (remove GitHub secrets reference)
- Fix 404 PyPI manage URL (use correct case and /releases/ path)
- Replace security@pypi.org mailto with https://pypi.org/security/
- Remove unconfirmed 'Notify GitHub Security' bullet
- Fix section numbering: 10.x → 9.x under Section 9. Dependency Map
- Reorder: move 9.3 Responding to Upstream Vulnerability before 9.3
  Downstream Dependencies (now 9.2 and 9.3 respectively)
- Add anchor link for Section 5 reference in 9.2
- Add #plugin-list anchor to third-party plugins handbook link
- Fix GitLab issue tracker URLs to use /-/work_items for libtiff,
  freetype2, and bzip2
- Add pyproject.toml reference for complete optional dependencies list

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-10 10:58:43 -04:00
Jeffrey 'Alex' Clark
0cbdd2eff9
Update .github/INCIDENT_RESPONSE.md
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
2026-04-10 10:37:34 -04:00
Hugo van Kemenade
eda14b6c4a Restrict nightly Anaconda uploads to environment 2026-04-10 16:33:18 +03:00
Jeffrey 'Alex' Clark
24b12dc84f Combine plan maintenance into a single paragraph
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-10 07:49:37 -04:00
Jeffrey 'Alex' Clark
d016c90108 Remove active exploitation escalation bullet from incident response
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-10 07:44:27 -04:00
Jeffrey 'Alex' Clark
6a0192a40a Update .github/INCIDENT_RESPONSE.md
Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com>
2026-04-10 07:44:16 -04:00
Jeffrey 'Alex' Clark
6fe81dd52e Remove Wand from downstream dependencies
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-09 18:19:22 -04:00
Jeffrey 'Alex' Clark
55989595ea Add private channels note to internal communication guidance
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-09 18:17:39 -04:00
Jeffrey 'Alex' Clark
b579577aa0 Link to section 1.3 in Plan Maintenance
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-09 18:15:25 -04:00
Jeffrey 'Alex' Clark
6f815c2d8d Clarify advisory thread purpose as reporter coordination
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-09 18:13:43 -04:00
Jeffrey 'Alex' Clark
80a91fdb4e Add setuptools to Python-level dependencies
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-09 18:08:44 -04:00
Jeffrey 'Alex' Clark
0d440b7d09 Trim Plan Maintenance section
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-09 18:04:00 -04:00
Jeffrey 'Alex' Clark
00ff8636a2 Remove section 7.5 Rollback Procedures
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-09 18:01:08 -04:00
Jeffrey 'Alex' Clark
e74a89f70e Trim version support matrix prose
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-09 17:59:29 -04:00
Jeffrey 'Alex' Clark
20af4ec89c Change Critical/High SLA targets to best effort
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-09 17:55:11 -04:00
Jeffrey 'Alex' Clark
3f90d5c4da Replace section sign (§) with plain Section references
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-09 17:53:04 -04:00
Jeffrey 'Alex' Clark
68be7f30ff Remove Tidelift notification step from triage
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-09 17:50:45 -04:00
Jeffrey 'Alex' Clark
e0f9e2b98e Fix severity classification cross-reference, remove incident lead assignment step
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-09 17:46:58 -04:00
Jeffrey 'Alex' Clark
ad582c1a8e Simplify Roles section note
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-09 17:38:34 -04:00
Jeffrey 'Alex' Clark
c2ac2da31c Inline Readiness Review procedure as prose
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-09 17:28:42 -04:00
Jeffrey 'Alex' Clark
3aa076129f Remove backport comment from version support matrix
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-09 17:25:32 -04:00
Jeffrey 'Alex' Clark
4a74a20b86 Update Readiness Review: quarterly cadence, trim checklist
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-09 17:23:52 -04:00
Jeffrey 'Alex' Clark
64ed4710b9 Fix version support matrix to reflect main-only security policy
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-09 16:59:41 -04:00
Jeffrey 'Alex' Clark
cdaa1bf9ef Add sections from Bootstrap example
At the risk of making this document larger, add in sections in Bootstrap
IRP but not ours.

- https://github.com/twbs/bootstrap/blob/main/.github/INCIDENT_RESPONSE.md
2026-04-09 12:57:16 -04:00
Jeffrey 'Alex' Clark
4d63d0b3a6 Fix links 2026-04-09 12:47:50 -04:00
Jeffrey 'Alex' Clark
cb5736ea3e Add INCIDENT_RESPONSE.md 2026-04-09 12:36:00 -04:00
Hugo van Kemenade
5ada8c8306
Use github.event.repository.fork (#9551) 2026-04-09 18:43:23 +03:00
Andrew Murray
6ede62874b
Update README with revised security policy (#9553) 2026-04-09 19:01:17 +10:00
Jeffrey 'Alex' Clark
b97034ae02 Link to New draft security advisory 2026-04-08 20:01:39 -04:00
Jeffrey 'Alex' Clark
77b2f6791a
Update security policy (#9552) 2026-04-08 16:23:51 -04:00
Jeffrey 'Alex' Clark
8f625f19ef
Update .github/SECURITY.md
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
2026-04-08 16:17:52 -04:00
Jeffrey 'Alex' Clark
8edb7734b5
Update .github/SECURITY.md
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
2026-04-08 14:52:36 -04:00
Jeffrey 'Alex' Clark
05860779a1
Update .github/SECURITY.md
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
2026-04-08 14:52:19 -04:00
Jeffrey 'Alex' Clark
ab02e810b0 Update security policy 2026-04-08 13:16:37 -04:00
Andrew Murray
ed89b93940 Use github.event.repository.fork 2026-04-08 21:51:43 +10:00
Hugo van Kemenade
7cf4dac7ae
Move Homebrew dependencies into Brewfile (#9546) 2026-04-07 19:09:30 +10:00
Trần Bách
117de2b181 fix(security)(_imagingtk.c): unsafe pointer dereference from unchecked python i
In `_tkinit`, `PyLong_AsVoidPtr(arg)` converts an arbitrary Python object to a `void*` pointer which is then cast to `Tcl_Interp*` and passed to `TkImaging_Init`. If `PyLong_AsVoidPtr` fails (returns NULL and sets an error), or if the caller passes an arbitrary integer value, the code proceeds to dereference it without any validation, potentially leading to a crash or arbitrary memory access.

Affected files: _imagingtk.c

Signed-off-by: Trần Bách <45133811+barttran2k@users.noreply.github.com>
2026-04-07 09:41:12 +07:00
Hugo van Kemenade
43a3e5ca21 Remove Codecov token 2026-04-06 23:35:44 +03:00
Hugo van Kemenade
c722aaec53
Do not precompute horizontal coefficients if not horizontal resizing (#9543) 2026-04-06 20:29:12 +03:00
pre-commit-ci[bot]
b72f5730e1 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-04-06 17:25:20 +00:00
pre-commit-ci[bot]
ecc48f9b3e
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.15.4 → v0.15.9](https://github.com/astral-sh/ruff-pre-commit/compare/v0.15.4...v0.15.9)
- [github.com/psf/black-pre-commit-mirror: 26.1.0 → 26.3.1](https://github.com/psf/black-pre-commit-mirror/compare/26.1.0...26.3.1)
- [github.com/pre-commit/mirrors-clang-format: v22.1.0 → v22.1.2](https://github.com/pre-commit/mirrors-clang-format/compare/v22.1.0...v22.1.2)
- [github.com/python-jsonschema/check-jsonschema: 0.37.0 → 0.37.1](https://github.com/python-jsonschema/check-jsonschema/compare/0.37.0...0.37.1)
- [github.com/zizmorcore/zizmor-pre-commit: v1.22.0 → v1.23.1](https://github.com/zizmorcore/zizmor-pre-commit/compare/v1.22.0...v1.23.1)
- [github.com/tox-dev/pyproject-fmt: v2.16.2 → v2.21.0](https://github.com/tox-dev/pyproject-fmt/compare/v2.16.2...v2.21.0)
2026-04-06 17:24:37 +00:00
Hugo van Kemenade
fcf033bdfb
Fix comparison warnings (#9541) 2026-04-06 12:37:24 +03:00
Hugo van Kemenade
698fbb768a
Correct feature name (#9542) 2026-04-06 12:37:07 +03:00
Andrew Murray
abb9b200ef Do not precompute horizontal coefficients if not horizontal resizing 2026-04-06 14:21:21 +10:00
Andrew Murray
b65bc406d8 Fixed comparison warning 2026-04-06 13:19:32 +10:00
Andrew Murray
d7d2df8ab2 Correct feature name 2026-04-06 09:39:23 +10:00
Hugo van Kemenade
f5ab7bb37b
Skip test if FreeType is not available (#9540) 2026-04-05 13:04:12 +03:00
Andrew Murray
17612be407 Skip test if FreeType is not available 2026-04-05 12:57:01 +10:00
Andrew Murray
64f6d4ebd8
Close PdfParser if error occurs during init (#9539)
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
2026-04-05 05:30:11 +10:00
Hugo van Kemenade
a865345add
Remove type hint ignore (#9538) 2026-04-04 15:24:25 +03:00
Andrew Murray
1dd1c9a3e5 Replace custom class with TextIOWrapper 2026-04-04 19:33:07 +11:00
Andrew Murray
7f3751d498 Remove type hint ignore 2026-04-04 19:29:11 +11:00
Hugo van Kemenade
e81acb8f79
Drop experimental Python 3.13 free-threaded wheels (#9535) 2026-04-03 15:54:13 +03:00
Andrew Murray
e58c67347a Raise error if transparency is incorrect type or length when saving 2026-04-03 22:19:52 +11:00
Andrew Murray
7f68decf2c Clarified condition 2026-04-03 22:16:51 +11:00
Andrew Murray
c03ba8b3c0 Added release notes 2026-04-03 21:41:13 +11:00
Hugo van Kemenade
82ac16d89d
Update macOS tested Python versions (#9534) 2026-04-03 09:56:30 +03:00
Andrew Murray
20307667f9 Remove deprecated option to allow Python 3.13t wheels 2026-04-03 15:51:01 +11:00
Andrew Murray
9d790af50c Update macOS tested Python versions 2026-04-03 15:41:02 +11:00
renovate[bot]
3f78ebb542
Update dependency cibuildwheel to v3.4.0 (#9532) 2026-04-03 15:38:40 +11:00
renovate[bot]
c39eda6348
Update github-actions (#9533) 2026-04-03 15:20:29 +11:00
Hugo van Kemenade
abb1d2bf6e
Remove Debian 12 and Fedora 42 from CI (#9530) 2026-04-02 18:11:35 +11:00
Hugo van Kemenade
d16c00fa0c
Remove manylinux2014 and Amazon Linux 2 (#9528) 2026-04-02 08:05:42 +03:00
Andrew Murray
4dc9398402 Remove manylinux2014 2026-04-02 07:55:58 +11:00
Andrew Murray
30b3dff0cb Remove Amazon Linux 2 2026-04-02 07:55:58 +11:00
Hugo van Kemenade
7d78ac519b 12.3.0.dev0 version bump 2026-04-01 17:53:55 +03:00
Varun Chawla
e50d8a5192
Improve border validation error message wording 2026-02-22 18:50:14 -08:00
Varun Chawla
f708c00527 Fix UnboundLocalError in _border for invalid tuple lengths and document rgba() color format
The _border helper in ImageOps raised UnboundLocalError when given a tuple
with a length other than 2 or 4 (e.g. 1-tuple or 3-tuple). This changes
it to raise a clear ValueError instead.

Also adds documentation for the rgba() color format in ImageColor, which
was supported in code and tested but missing from the docs.
2026-02-13 19:38:48 -08:00
76 changed files with 2347 additions and 417 deletions

View File

@ -39,8 +39,8 @@ python3 -m pip install --only-binary=:all: pyarrow || true
# PyQt6 doesn't support PyPy3
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
sudo apt-get -qq install libegl1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0
# TODO Update condition when pyqt6 supports free-threading
if ! [[ "$PYTHON_GIL" == "0" ]]; then python3 -m pip install pyqt6 ; fi
# pyqt6 doesn't yet support free-threading; only install if a wheel is available
python3 -m pip install --only-binary=:all: pyqt6 || true
fi
# webp

View File

@ -1 +1 @@
cibuildwheel==3.3.1
cibuildwheel==3.4.1

View File

@ -1,4 +1,4 @@
mypy==1.19.1
mypy==1.20.2
arro3-compute
arro3-core
IceSpringPySideStubs-PyQt6

View File

@ -0,0 +1 @@
check-jsonschema==0.37.1

3
.github/FUNDING.yml vendored
View File

@ -1 +1,2 @@
tidelift: "pypi/pillow"
github: python-pillow
tidelift: pypi/pillow

424
.github/INCIDENT_RESPONSE.md vendored Normal file
View File

@ -0,0 +1,424 @@
# Incident Response Plan — Pillow
This document describes how the Pillow maintainers detect, triage, fix, communicate, and
learn from security incidents. It supplements the existing [Security Policy](SECURITY.md)
and [Release Checklist](../RELEASING.md).
---
## 1. Preparation
Maintaining readiness before an incident occurs reduces response time and errors under pressure.
### 1.1 Version Support Matrix
Security fixes are applied to the **latest stable release only**. Users on older versions
are expected to upgrade. Reporters should assume only the latest release will receive a patch.
| Branch | Status |
|---|---|
| `main` / latest stable | ✅ Security fixes applied |
| All older releases | ❌ No security support — please upgrade |
### 1.2 Team Readiness
The four members of the Pillow core team are in regular contact and share collective
responsibility for incident response. Any core team member may act as Incident Lead.
Contact details are known to all team members.
### 1.3 Readiness Review
At each quarterly release, maintainers should re-read this document and update any stale content.
---
## 2. Scope
This plan covers:
| Incident type | Examples |
|---|---|
| Vulnerability in Pillow's own Python or C code | Buffer overflow in an image decoder, integer overflow in `ImagingNew` |
| Vulnerability in a bundled or wheel-shipped C library | libjpeg, libwebp, libtiff, libpng, openjpeg, libavif |
| Supply-chain compromise | Malicious commit, stolen maintainer credentials, tampered PyPI wheel |
| CI/CD or infrastructure compromise | GitHub Actions secret leak, Codecov breach, PyPI token exposure |
| Critical non-security regression | Data-loss bug shipped in a release, crash on all supported platforms |
---
## 3. Definitions
| Term | Meaning |
|---|---|
| **Incident** | Any event that compromises or threatens the confidentiality, integrity, or availability of Pillow's code, release artifacts, or infrastructure. |
| **Vulnerability** | A security flaw in Pillow or a bundled library that can be exploited by a crafted image or API call. |
| **Incident Lead** | The maintainer who owns coordination of the response from triage to closure. |
| **Embargo** | A period during which fix details are kept private to allow coordinated patching before public disclosure. |
| **Yank** | A PyPI action that keeps a release downloadable by pinned users but removes it from default `pip install` resolution. |
| **CVE** | Common Vulnerabilities and Exposures — a public identifier assigned to a specific vulnerability. |
| **CNA** | CVE Numbering Authority — GitHub is a CNA and can assign CVEs directly through the advisory workflow. |
---
## 4. Roles
| Role | Responsibility |
|---|---|
| **Incident Lead** | First maintainer to triage the report. Owns the incident until resolution. |
| **Patch Owner** | Writes and tests the fix (may be the same person as Incident Lead). |
| **Release Manager** | Cuts the point release following [RELEASING.md](../RELEASING.md). |
| **Communications Owner** | Drafts the GitHub Security Advisory, announces on Mastodon, notifies distros. |
| **Tidelift Contact** | For reports that arrive via Tidelift, coordinate through the Tidelift security portal. |
One person may fill multiple roles.
---
## 5. Severity Classification
Use the [CVSS 4.0](https://www.first.org/cvss/v4.0/specification-document) base score as
a guide, mapped to the following levels:
| Severity | CVSS | Definition | Target Response SLA |
|---|---|---|---|
| **Critical** | 9.0 10.0 | Remote code execution, arbitrary write, or complete integrity/confidentiality loss achievable by opening a crafted image | Best effort; embargoed release where possible |
| **High** | 7.0 8.9 | Heap/stack buffer overflow, use-after-free, or significant information disclosure | Best effort |
| **Medium** | 4.0 6.9 | Denial of service via crafted image, out-of-bounds read, limited info disclosure | Next scheduled quarterly release, or earlier point release if needed |
| **Low** | 0.1 3.9 | Minor information disclosure, unlikely to be exploitable in practice | Next quarterly release |
Supply-chain and CI/CD incidents are always treated as **Critical** regardless of CVSS.
> **Note:** These are good-faith targets for a small volunteer maintainer team, not contractual SLAs. Public safety and transparency will always be prioritised, even when timing varies.
---
## 6. Detection Sources
Vulnerabilities and incidents may be reported or discovered through:
1. **GitHub private security advisory** — preferred channel; see [SECURITY.md](SECURITY.md)
2. **Tidelift security contact**<https://tidelift.com/docs/security>
3. **External researcher / coordinated disclosure** — e.g. Google Project Zero, vendor PSIRT
4. **Automated scanning** — Dependabot, GitHub code-scanning (CodeQL), CI fuzzing
5. **Distro security teams** — Debian, Red Hat, Ubuntu, Alpine may report upstream
6. **User bug report** — public issue (reassess if it has security implications and convert to a private advisory if needed)
---
## 7. Response Process
### 7.1 Triage (all severities)
1. **Acknowledge receipt** to the reporter within **72 hours** using the template in
[Appendix A](#appendix-a-communication-templates). Ask the reporter:
- How they would like to be credited (name, handle, or anonymous)
- Whether they intend to publish their own advisory, and if so, their preferred timeline
- Thank them explicitly — reporters do the project a favour by disclosing privately.
2. Reproduce the issue. If the report is invalid, close it and notify the reporter.
3. Assign a severity level ([Section 5: Severity Classification](#5-severity-classification)).
4. If the GitHub Security Advisory was not created by the reporter, create one now and keep
it **private** until the fix is released. Add the reporter as a collaborator if they wish
to be involved.
5. **Request a CVE** through the GitHub Security Advisory workflow (GitHub is a CVE
Numbering Authority — no separate MITRE form required). The CVE is reserved privately
and published automatically when the advisory goes public.
6. **Escalation** — Escalate beyond the core maintainer team if any of the following apply:
- The fix requires changes to CPython or a dependency outside Pillow's control → contact the relevant upstream immediately
- A legal concern arises (e.g. GDPR-reportable data exposure) → contact the project's legal/fiscal sponsor
- The Incident Lead is unreachable for > 24 hours on a Critical issue → any other maintainer may assume the role
### 7.2 Fix Development
1. Develop the fix in a **private fork** or directly in the private security advisory
workspace on GitHub. Do **not** push to a public branch before the embargo lifts.
2. Write a regression test that fails before the fix and passes after.
3. Review the patch with at least one other maintainer.
### 7.3 Standard (Non-Embargoed) Release
For Medium and Low severity, or when no distro pre-notification is needed:
1. Merge the fix to `main`, then cherry-pick to all affected release branches
(see [RELEASING.md — Point release](../RELEASING.md)).
2. Amend commit messages to include the CVE identifier.
3. Follow the [Point release](../RELEASING.md#point-release) process in RELEASING.md to
tag, push, and confirm wheels are live on PyPI.
4. Publish the GitHub Security Advisory (this simultaneously publishes the CVE).
### 7.4 Embargoed Release
For Critical and High severity where distro pre-notification improves user safety:
1. Prepare patches against all affected release branches and test locally.
2. Agree on an **embargo date** with the reporter (typically 714 days out, up to 90 days for
complex issues).
3. Privately send the patch to distros via the
[linux-distros](https://oss-security.openwall.org/wiki/mailing-lists/distros) mailing list
or directly to individual distro security teams.
4. On the embargo date:
- Amend commit messages with the CVE identifier.
- Follow the [Embargoed release](../RELEASING.md#embargoed-release) process in
RELEASING.md to tag, push, and confirm wheels are live on PyPI.
- Publish the GitHub Security Advisory.
### 7.5 Supply-Chain / Infrastructure Compromise
1. **Immediately** revoke any potentially compromised credentials:
- PyPI API tokens
- GitHub personal access tokens and OAuth apps
- Codecov or other CI service tokens
2. Audit recent commits and releases for tampering:
- Verify release tags against known-good SHAs
- Re-inspect any wheel published since the potential compromise window
3. If a PyPI release is suspected to be tampered: yank it immediately via the
[PyPI release management page](https://pypi.org/manage/project/Pillow/releases/)
(login required); see [https://pypi.org/security/](https://pypi.org/security/) for
reporting to the PyPI security team.
4. Issue a public advisory describing the scope and any user action required.
### 7.6 Recovery
After the fix is released and the advisory is public:
1. Verify that the patched wheels are live on PyPI and passing CI across all supported platforms.
2. Confirm any yanked releases are handled correctly .
3. Resume normal development operations on `main`.
4. Monitor the GitHub issue tracker and Mastodon for user reports of residual problems for at least **72 hours** post-release.
5. Close the private GitHub Security Advisory once recovery is confirmed.
---
## 8. Communication
### Internal (during embargo)
- Use the **private GitHub Security Advisory** thread for coordination with the reporter.
- Use private communication channels for all other coordination.
- Do not discuss details in public issues, PRs, or Gitter/IRC channels.
### External (at or after disclosure)
| Audience | Channel | Timing |
|---|---|---|
| General users | [GitHub Security Advisory](https://github.com/python-pillow/Pillow/security/advisories) | At release |
| PyPI ecosystem | CVE published via advisory | At release |
| Downstream distros | Direct email or linux-distros list | Before embargo date (embargoed) |
| Tidelift subscribers | Tidelift security portal | At release (or coordinated) |
| Community | [Mastodon @pillow](https://fosstodon.org/@pillow) | At release |
**Advisory content should include:**
- CVE identifier and CVSS score
- Affected Pillow versions
- Fixed version(s)
- Nature of the vulnerability (without full exploit details if still fresh)
- Credit to the reporter (with their consent)
- Upgrade instructions (`python3 -m pip install --upgrade Pillow`)
---
## 9. Dependency Map
Understanding what Pillow depends on (upstream) and what depends on Pillow (downstream)
is essential for scoping impact and coordinating notifications during an incident.
### 9.1 Upstream Dependencies
#### Bundled C libraries (shipped in official wheels)
These libraries are compiled into Pillow's binary wheels. A CVE in any of them may
require a Pillow point release even if Pillow's own code is unchanged.
| Library | Purpose | Security advisory tracker |
|---|---|---|
| [libjpeg-turbo](https://libjpeg-turbo.org/) | JPEG encode/decode | [GitHub](https://github.com/libjpeg-turbo/libjpeg-turbo/security) |
| [libpng](http://www.libpng.org/pub/png/libpng.html) | PNG encode/decode within FreeType 2, OpenJPEG and WebP | [SourceForge](https://sourceforge.net/p/libpng/bugs/) |
| [libtiff](https://libtiff.gitlab.io/libtiff/) | TIFF encode/decode | [GitLab](https://gitlab.com/libtiff/libtiff/-/work_items) |
| [libwebp](https://chromium.googlesource.com/webm/libwebp) | WebP encode/decode | [Chromium tracker](https://issues.webmproject.org/issues) |
| [libavif](https://github.com/AOMediaCodec/libavif) | AVIF encode/decode | [GitHub](https://github.com/AOMediaCodec/libavif/security) |
| [aom](https://aomedia.googlesource.com/aom/) | AV1 codec (AVIF) | [Chromium tracker](https://aomedia.issues.chromium.org/issues) |
| [dav1d](https://code.videolan.org/videolan/dav1d) | AV1 decode (AVIF) | [VideoLAN Security](https://www.videolan.org/security/) |
| [openjpeg](https://www.openjpeg.org/) | JPEG 2000 encode/decode | [GitHub](https://github.com/uclouvain/openjpeg/security) |
| [freetype2](https://freetype.org/) | Font rendering | [GitLab](https://gitlab.freedesktop.org/freetype/freetype/-/work_items) |
| [lcms2](https://www.littlecms.com/) | ICC color management | [GitHub](https://github.com/mm2/Little-CMS/security) |
| [harfbuzz](https://harfbuzz.github.io/) | Text shaping (via raqm) | [GitHub](https://github.com/harfbuzz/harfbuzz/security) |
| [raqm](https://github.com/HOST-Oman/libraqm) | Complex text layout | [GitHub](https://github.com/HOST-Oman/libraqm) |
| [fribidi](https://github.com/fribidi/fribidi) | Unicode bidi (via raqm) | [GitHub](https://github.com/fribidi/fribidi) |
| [zlib](https://zlib.net/) | Deflate compression | [zlib.net](https://zlib.net/) |
| [liblzma / xz-utils](https://tukaani.org/xz/) | XZ/LZMA compression | [GitHub](https://github.com/tukaani-project/xz/security) |
| [bzip2](https://gitlab.com/bzip2/bzip2) | BZ2 compression | [GitLab](https://gitlab.com/bzip2/bzip2/-/work_items) |
| [zstd](https://github.com/facebook/zstd) | Zstandard compression | [GitHub](https://github.com/facebook/zstd/security) |
| [brotli](https://github.com/google/brotli) | Brotli compression | [GitHub](https://github.com/google/brotli/security) |
| [libyuv](https://chromium.googlesource.com/libyuv/libyuv/) | YUV conversion | [Chromium tracker](https://libyuv.issues.chromium.org/issues) |
#### Python-level dependencies
| Package | Required? | Purpose |
|---|---|---|
| `setuptools` | Build-time only | Package build backend |
| `pybind11` | Build-time only | Compile C files in parallel |
| `olefile` | Optional (`fpx`, `mic` extras) | OLE2 container parsing (FPX, MIC formats) |
| `defusedxml` | Optional (`xmp` extra) | Safe XML parsing for XMP metadata |
See [`pyproject.toml`](../pyproject.toml) for the complete and authoritative list of
optional dependencies.
### 9.2 Responding to an Upstream Vulnerability
When a CVE is published for a bundled C library:
1. Assess whether the vulnerable code path is reachable through Pillow's API.
2. If reachable, treat as a Pillow vulnerability and follow [Section 5: Severity Classification](#5-severity-classification).
3. Update the bundled library version in the wheel build scripts and rebuild wheels.
4. Reference the upstream CVE in Pillow's release notes and GitHub Security Advisory.
5. If not reachable, document the rationale in a public issue so downstream distributors
can make informed decisions about patching their system packages.
### 9.3 Downstream Dependencies
A vulnerability in Pillow can have wide impact. Notify or consider the blast radius of
these downstream consumers when assessing severity and planning communications.
#### Linux distribution packages
| Distribution | Package name | Security contact |
|---|---|---|
| Debian / Ubuntu | `python3-pil` | [Debian Security](https://www.debian.org/security/) / [Ubuntu Security](https://ubuntu.com/security) |
| Fedora / RHEL / CentOS | `python3-pillow` | [Red Hat Security](https://access.redhat.com/security/) |
| Alpine Linux | `py3-pillow` | [Alpine security](https://security.alpinelinux.org/) |
| Arch Linux | `python-pillow` | [Arch security tracker](https://security.archlinux.org/) |
| Homebrew | `pillow` | [Homebrew maintainers](https://github.com/Homebrew/homebrew-core/security) |
| conda-forge | `pillow` | [conda-forge](https://github.com/conda-forge/pillow-feedstock) |
#### Major Python ecosystem consumers
These are high-profile projects known to depend on Pillow; a critical vulnerability may
warrant proactive notification.
| Project | Usage |
|---|---|
| [matplotlib](https://matplotlib.org/) | Image I/O for plots |
| [scikit-image](https://scikit-image.org/) | Image processing |
| [torchvision](https://github.com/pytorch/vision) (PyTorch) | Dataset loading, transforms |
| [Keras / TensorFlow](https://keras.io/) | Image preprocessing utilities |
| [Django](https://www.djangoproject.com/) | `ImageField` validation and thumbnail generation |
| [Wagtail](https://wagtail.org/) | CMS image renditions |
| [Plone](https://plone.org/) | CMS image handling |
| [Jupyter / IPython](https://jupyter.org/) | Inline image display |
| [ReportLab](https://www.reportlab.com/) | PDF image embedding |
| [Tidelift subscribers](https://tidelift.com/) | Enterprise consumers (coordinated via Tidelift) |
#### Pillow ecosystem plugins
Third-party plugins extend Pillow and are distributed separately on PyPI. Their
maintainers should be notified for Critical/High issues that affect the plugin API
or the formats they decode. See the
[full plugin list](https://pillow.readthedocs.io/en/stable/handbook/third-party-plugins.html#plugin-list).
---
## 11. Plan Maintenance
This document is a living record. It should be kept current so it is useful when an incident actually occurs. Revisit it during the [Section 1.3 readiness review](#13-readiness-review) at each quarterly release.
---
## 12. References
- [Security Policy](SECURITY.md)
- [Release Checklist](../RELEASING.md)
- [Contributing Guide](CONTRIBUTING.md)
- [Tidelift Security Contact](https://tidelift.com/docs/security)
- [GitHub: Privately reporting a security vulnerability](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability)
- [GitHub as a CVE Numbering Authority (CNA)](https://docs.github.com/en/code-security/security-advisories/working-with-repository-security-advisories/about-repository-security-advisories)
- [FIRST CVSS 4.0 Calculator](https://www.first.org/cvss/calculator/4.0)
- [linux-distros mailing list](https://oss-security.openwall.org/wiki/mailing-lists/distros)
- [OpenSSF CVD Guide](https://github.com/ossf/oss-vulnerability-guide) *(basis for this plan)*
---
## Appendix A: Communication Templates
### A.1 Reporter Acknowledgment
> Subject: Re: [Security] \<brief issue description\>
>
> Hi \<name\>,
>
> Thank you for taking the time to report this issue. We appreciate it.
>
> We have received your report and will review it as soon as possible. We will
> keep you updated on our progress.
>
> Questions:
>
> - How would you like to be credited in the advisory? (name, handle,
> organisation, or anonymous)
> - Do you plan to publish your own write-up or advisory? If so, do you have a
> disclosure date in mind?
>
> We apply coordinated disclosure principles to all vulnerability reports. If
> you have any questions or concerns at any point, please reply to this thread.
>
> Thank you again,
> The Pillow team
### A.2 Embargoed Distro Notification
> Subject: [EMBARGOED] Pillow security issue — \<CVE-XXXX-XXXXX\> — disclosure \<DATE\>
>
> This is an embargoed notification of a vulnerability in Pillow. Please keep this
> information confidential until the disclosure date listed below.
>
> **CVE:** \<CVE-XXXX-XXXXX\>
>
> **Affected versions:** \<e.g. Pillow < 11.x.x\>
>
> **Fixed version:** \<version\>
>
> **Severity:** \<Critical / High / Medium / Low\> (CVSS \<score\>: \<vector\>)
>
> **Reporter:** \<name / affiliation, or "reported privately"\>
>
> **Public disclosure date:** \<DATE TIME UTC\>
>
> **Summary:**
> \<One paragraph describing the vulnerability class and impact without a full exploit.\>
>
> **Proof of concept:**
> \<Minimal reproducer or attached patch.\>
>
> **Remediation:**
> Upgrade to Pillow \<fixed version\>. No known workaround.
>
> Please do not share this information, issue public patches, or make user communications
> before the disclosure date. We will notify this list immediately if the date changes.
>
> — The Pillow maintainers
### A.3 Public Disclosure Advisory
*(Published as a GitHub Security Advisory; the CVE and date are included automatically.)*
> **Summary:** \<One-paragraph technical summary.\>
>
> **CVE:** \<CVE-XXXX-XXXXX\>
>
> **Affected versions:** Pillow \< \<fixed version\>
>
> **Fixed version:** \<version\>
>
> **Severity:** \<rating\> (CVSS \<score\>)
>
> **Reporter:** \<credited name / "reported privately"\>
>
> **Details:**
> \<Fuller technical description. Include attack scenario where helpful.\>
>
> **Remediation:**
> ```
> python3 -m pip install --upgrade Pillow
> ```
>
> **Timeline:**
> - Reported: \<date\>
> - Fixed: \<date\>
> - Disclosed: \<date\>

20
.github/SECURITY.md vendored
View File

@ -1,5 +1,21 @@
# Security policy
To report sensitive vulnerability information, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure.
## Reporting a vulnerability
If your organisation/employer is a distributor of Pillow and would like advance notification of security-related bugs, please let us know your preferred contact method.
To report sensitive vulnerability information, report it [privately on GitHub](https://github.com/python-pillow/Pillow/security/advisories/new).
If you cannot use GitHub, use the [Tidelift security contact](https://tidelift.com/docs/security). Tidelift will coordinate the fix and disclosure.
**DO NOT report sensitive vulnerability information in public.**
## Threat model
Pillow's primary attack surface is parsing untrusted image data. A full STRIDE threat model covering spoofing, tampering, repudiation, information disclosure, denial of service, and elevation of privilege is maintained in the [Security handbook page](https://pillow.readthedocs.io/en/latest/handbook/security.html).
Key risks to be aware of when using Pillow to process untrusted images:
- **Decompression bombs** — do not set `Image.MAX_IMAGE_PIXELS = None` in production.
- **EPS files invoke Ghostscript** — block EPS input at the application layer unless strictly required.
- **`ImageMath.unsafe_eval()`** — never pass user-controlled strings to this function; use `lambda_eval` instead.
- **C extension memory safety** — keep Pillow and its bundled C libraries (libjpeg, libpng, libtiff, libwebp, etc.) up to date.
- **Sandboxing** — for high-risk deployments, run image processing in a sandboxed subprocess.

271
.github/compare-dist-sizes.py vendored Normal file
View File

@ -0,0 +1,271 @@
"""Compare sizes of newly-built dists against the latest release on PyPI.
Fetches file sizes for the latest Pillow release from the PyPI JSON API
(no download required) and compares them to a directory of freshly-built
wheels and sdist. Outputs a table to stdout (and to
`$GITHUB_STEP_SUMMARY` if set).
Usage:
`uv run .github/compare-dist-sizes.py <dist-dir>`
"""
# /// script
# requires-python = ">=3.10"
# dependencies = [
# "humanize",
# "prettytable",
# "termcolor",
# ]
# ///
from __future__ import annotations
import argparse
import json
import os
import re
import sys
import urllib.request
from pathlib import Path
import humanize
from prettytable import PrettyTable, TableStyle
from termcolor import colored
PYPI_JSON_URL = "https://pypi.org/pypi/pillow/json"
# Wheel filename: {distribution}-{version}(-{build})?-{python}-{abi}-{platform}.whl
# sdist filename: {distribution}-{version}.tar.gz
WHEEL_RE = re.compile(
r"^[^-]+-[^-]+(?:-(?P<build>\d[^-]*))?"
r"-(?P<python>[^-]+)-(?P<abi>[^-]+)-(?P<platform>[^-]+)\.whl$",
re.IGNORECASE,
)
SDIST_RE = re.compile(
r"^(?P<dist>[^-]+)-(?P<version>.+)\.tar\.gz$",
re.IGNORECASE,
)
def key_for(filename: str) -> str:
"""Return a version-independent identifier for a dist file."""
if m := WHEEL_RE.match(filename):
build = f"{m['build']}-" if m["build"] else ""
return f"wheel:{build}{m['python']}-{m['abi']}-{m['platform']}"
if SDIST_RE.match(filename):
return "sdist"
msg = f"Unexpected dist name: {filename}"
raise ValueError(msg)
def display_for(filename: str) -> str:
"""Strip the `pillow-{version}-` prefix for compact table display."""
if m := WHEEL_RE.match(filename):
build = f"{m['build']}-" if m["build"] else ""
return f"{build}{m['python']}-{m['abi']}-{m['platform']}.whl"
if SDIST_RE.match(filename):
return "sdist (.tar.gz)"
return filename
def fetch_pypi_sizes() -> tuple[str, dict[str, tuple[str, int]]]:
"""Return (version, {key: (filename, size)}) for the latest PyPI release."""
with urllib.request.urlopen(PYPI_JSON_URL) as response:
data = json.load(response)
version = data["info"]["version"]
sizes: dict[str, tuple[str, int]] = {}
for entry in data.get("urls", []):
filename = entry["filename"]
key = key_for(filename)
sizes[key] = (filename, entry["size"])
return version, sizes
def collect_local_sizes(dist_dir: Path) -> dict[str, tuple[str, int]]:
sizes: dict[str, tuple[str, int]] = {}
for path in sorted(dist_dir.iterdir()):
if not path.is_file():
continue
key = key_for(path.name)
sizes[key] = (path.name, path.stat().st_size)
return sizes
def human(n: int | None) -> str:
if n is None:
return "n/a"
return humanize.naturalsize(n)
def pct_change(before: int | None, after: int | None) -> str:
if before is None or after is None:
return "n/a"
delta = 0 if before == 0 else (after - before) / before * 100
return f"{delta:+.2f}%"
def pct_severity(text: str) -> dict[str, str] | None:
"""Return status indicators based on the change percent."""
if text == "n/a":
return None
pct = float(text.rstrip("%"))
if pct >= 5:
return {"color": "red", "emoji": "🔴"}
if pct > 0:
return {"color": "yellow", "emoji": "🟡"}
else:
return {"color": "green", "emoji": "🟢"}
def render_table(
baseline_label: str,
baseline_sizes: dict[str, tuple[str, int]],
local_sizes: dict[str, tuple[str, int]],
*,
markdown: bool,
) -> str:
table = PrettyTable()
table.set_style(TableStyle.MARKDOWN if markdown else TableStyle.SINGLE_BORDER)
table.field_names = ["File", "Size before", "Size now", "Change"]
table.align = "r"
table.align["File"] = "l"
def style(cells: list[str], role: str) -> list[str]:
severity = pct_severity(cells[3])
if markdown:
if severity:
cells[3] = f"{severity['emoji']} {cells[3]}"
if role == "orphan":
return [f"*{c}*" for c in cells]
if role == "summary":
return [f"**{c}**" for c in cells]
return cells
if role == "orphan":
return [colored(c, "dark_grey") for c in cells]
bold_attrs = ["bold"] if role == "summary" else []
if bold_attrs:
cells[:3] = [colored(c, attrs=bold_attrs) for c in cells[:3]]
if severity:
cells[3] = colored(cells[3], severity["color"], attrs=bold_attrs)
elif bold_attrs:
cells[3] = colored(cells[3], attrs=bold_attrs)
return cells
keys = list(set(baseline_sizes) | set(local_sizes))
# Put sdist first for readability
keys.sort(key=lambda k: (k != "sdist", k))
wheel_before = []
wheel_after = []
total_before = []
total_after = []
for key in keys:
baseline_entry = baseline_sizes.get(key)
local_entry = local_sizes.get(key)
display_name = display_for((local_entry or baseline_entry)[0])
before = baseline_entry[1] if baseline_entry else None
after = local_entry[1] if local_entry else None
if after is None:
# Removed since baseline: ignore in totals
role = "orphan"
else:
# Present locally (in both, or newly added): count in totals
total_after.append(after)
if before is not None:
total_before.append(before)
if key != "sdist":
wheel_after.append(after)
if before is not None:
wheel_before.append(before)
role = "data"
cells = [
display_name,
human(before),
human(after),
pct_change(before, after),
]
table.add_row(style(cells, role))
if not markdown:
table.add_divider()
if wheel_after:
avg_before = sum(wheel_before) // len(wheel_before) if wheel_before else None
table.add_row(
style(
[
f"wheel average ({len(wheel_after)} wheels)",
human(avg_before),
human(sum(wheel_after) // len(wheel_after)),
pct_change(avg_before, sum(wheel_after) // len(wheel_after)),
],
"summary",
)
)
table.add_row(
style(
[
f"wheel total ({len(wheel_after)} wheels)",
human(sum(wheel_before)),
human(sum(wheel_after)),
pct_change(sum(wheel_before), sum(wheel_after)),
],
"summary",
),
divider=not markdown,
)
if total_after:
table.add_row(
style(
[
f"artifacts total ({len(total_after)} artifacts)",
human(sum(total_before)),
human(sum(total_after)),
pct_change(sum(total_before), sum(total_after)),
],
"summary",
)
)
title = f"## Dist size comparison vs {baseline_label}"
if not markdown:
title = colored(title, attrs=["bold"])
return f"{title}\n\n{table.get_string()}\n"
def main() -> int:
parser = argparse.ArgumentParser(
description=__doc__, formatter_class=argparse.ArgumentDefaultsHelpFormatter
)
parser.add_argument(
"dist_dir",
type=Path,
help="Directory containing newly-built wheels and sdist",
)
args = parser.parse_args()
if not args.dist_dir.is_dir():
print(f"error: {args.dist_dir} is not a directory", file=sys.stderr)
return 1
baseline_version, baseline_sizes = fetch_pypi_sizes()
baseline_label = f"Pillow {baseline_version} on PyPI"
local_sizes = collect_local_sizes(args.dist_dir)
print(render_table(baseline_label, baseline_sizes, local_sizes, markdown=False))
if summary_path := os.environ.get("GITHUB_STEP_SUMMARY"):
with open(summary_path, "a", encoding="utf-8") as f:
f.write(
render_table(baseline_label, baseline_sizes, local_sizes, markdown=True)
)
return 0
if __name__ == "__main__":
sys.exit(main())

19
.github/dependencies.json vendored Normal file
View File

@ -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"
}

560
.github/generate-sbom.py vendored Executable file
View File

@ -0,0 +1,560 @@
#!/usr/bin/env python3
"""Generate a CycloneDX 1.7 SBOM for Pillow's C extensions and their
vendored/optional native library dependencies.
Usage:
python3 .github/generate-sbom.py [output-file]
Output defaults to pillow-{version}.cdx.json in the current directory.
"""
from __future__ import annotations
import argparse
import base64
import datetime as dt
import difflib
import hashlib
import json
import urllib.request
import uuid
from pathlib import Path
def get_version() -> str:
version_file = Path(__file__).parent.parent / "src" / "PIL" / "_version.py"
return version_file.read_text(encoding="utf-8").split('"')[1]
def load_dep_versions() -> dict[str, str]:
deps_file = Path(__file__).parent / "dependencies.json"
return json.loads(deps_file.read_text(encoding="utf-8"))
def sha256_file(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def upstream_diff_b64(
upstream_url: str,
upstream_display: bytes,
local_path: Path,
local_display: bytes,
) -> str:
"""
Fetch an upstream file and return a base64-encoded unified diff vs the local copy.
"""
with urllib.request.urlopen(upstream_url) as resp:
upstream_text = resp.read()
local_text = local_path.read_bytes()
diff_lines = difflib.diff_bytes(
difflib.unified_diff,
upstream_text.splitlines(keepends=True),
local_text.splitlines(keepends=True),
fromfile=b"a/" + upstream_display,
tofile=b"b/" + local_display,
)
return base64.b64encode(b"".join(diff_lines)).decode()
def generate(version: str) -> dict:
serial = str(uuid.uuid4())
now = dt.datetime.now(dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
purl = f"pkg:pypi/pillow@{version}"
root = Path(__file__).parent.parent
thirdparty = root / "src" / "thirdparty"
versions = load_dep_versions()
metadata_component = {
"bom-ref": purl,
"type": "library",
"name": "Pillow",
"version": version,
"description": "Python Imaging Library (fork)",
"licenses": [{"license": {"id": "MIT-CMU"}}],
"purl": purl,
"externalReferences": [
{"type": "website", "url": "https://python-pillow.github.io"},
{"type": "vcs", "url": "https://github.com/python-pillow/Pillow"},
{"type": "documentation", "url": "https://pillow.readthedocs.io"},
{
"type": "security-contact",
"url": "https://github.com/python-pillow/Pillow/security/policy",
},
],
}
c_extensions = [
("PIL._avif", "AVIF image format extension"),
(
"PIL._imaging",
"Core image processing extension "
"(decode, encode, map, display, outline, path, libImaging)",
),
("PIL._imagingcms", "LittleCMS2 colour management extension"),
("PIL._imagingft", "FreeType font rendering extension"),
("PIL._imagingmath", "Image math operations extension"),
("PIL._imagingmorph", "Image morphology extension"),
("PIL._imagingtk", "Tk/Tcl display extension"),
("PIL._webp", "WebP image format extension"),
]
ext_components = [
{
"bom-ref": f"{purl}#c-ext/{name}",
"type": "library",
"name": name,
"version": version,
"description": desc,
"licenses": [{"license": {"id": "MIT-CMU"}}],
"purl": f"{purl}#c-ext/{name}",
}
for name, desc in c_extensions
]
vendored_components = [
{
"bom-ref": f"{purl}#thirdparty/fribidi-shim",
"type": "library",
"name": "fribidi-shim",
"version": "1.x",
"description": "FriBiDi runtime-loading shim "
"(vendored in src/thirdparty/fribidi-shim/); "
"loads libfribidi dynamically",
"licenses": [{"license": {"id": "LGPL-2.1-or-later"}}],
"hashes": [
{
"alg": "SHA-256",
"content": sha256_file(thirdparty / "fribidi-shim" / "fribidi.c"),
}
],
"pedigree": {
"notes": "Pillow-authored shim; not taken from an upstream project."
},
"externalReferences": [
{"type": "website", "url": "https://github.com/fribidi/fribidi"},
],
},
{
"bom-ref": "pkg:github/python/pythoncapi-compat",
"type": "library",
"name": "pythoncapi_compat",
"description": "Backport header for new CPython C-API functions "
"(vendored in src/thirdparty/pythoncapi_compat.h)",
"licenses": [{"license": {"id": "0BSD"}}],
"hashes": [
{
"alg": "SHA-256",
"content": sha256_file(thirdparty / "pythoncapi_compat.h"),
}
],
"pedigree": {
"notes": "Vendored unmodified from upstream python/pythoncapi-compat."
},
"externalReferences": [
{
"type": "vcs",
"url": "https://github.com/python/pythoncapi-compat",
},
],
},
{
"bom-ref": f"{purl}#thirdparty/raqm",
"type": "library",
"name": "raqm",
"version": "0.10.5",
"description": "Complex text layout library "
"(vendored in src/thirdparty/raqm/)",
"licenses": [{"license": {"id": "MIT"}}],
"hashes": [
{
"alg": "SHA-256",
"content": sha256_file(thirdparty / "raqm" / "raqm.c"),
}
],
"pedigree": {
"ancestors": [
{
"bom-ref": "pkg:github/HOST-Oman/libraqm@0.10.5#upstream",
"type": "library",
"name": "raqm",
"version": "0.10.5",
"purl": "pkg:github/HOST-Oman/libraqm@0.10.5",
"externalReferences": [
{
"type": "distribution",
"url": "https://github.com/HOST-Oman/libraqm/releases/tag/v0.10.5",
}
],
}
],
"patches": [
{
"type": "unofficial",
"diff": {
"text": {
# raqm-version.h.in → raqm-version.h:
# template @RAQM_VERSION_*@ placeholders replaced
# with literal 0.10.5 values; filename changed to
# drop the .in suffix; minor indentation fix.
"content": upstream_diff_b64(
"https://raw.githubusercontent.com/HOST-Oman/libraqm/v0.10.5/src/raqm-version.h.in",
b"src/raqm-version.h.in",
thirdparty / "raqm" / "raqm-version.h",
b"src/raqm-version.h",
),
"encoding": "base64",
}
},
},
{
"type": "unofficial",
"diff": {
"text": {
# raqm.c: wrap the <fribidi.h> include in an
# #ifdef HAVE_FRIBIDI_SYSTEM guard so that when
# building without a system FriBiDi Pillow's own
# fribidi-shim is used instead.
"content": upstream_diff_b64(
"https://raw.githubusercontent.com/HOST-Oman/libraqm/v0.10.5/src/raqm.c",
b"src/raqm.c",
thirdparty / "raqm" / "raqm.c",
b"src/raqm.c",
),
"encoding": "base64",
}
},
},
],
"notes": (
"Vendored from upstream HOST-Oman/libraqm v0.10.5 with two "
"Pillow-specific modifications: (1) raqm-version.h.in was "
"pre-processed into raqm-version.h with version placeholders "
"replaced by literal values; (2) raqm.c wraps the <fribidi.h> "
"include in an #ifdef HAVE_FRIBIDI_SYSTEM guard so Pillow's "
"bundled fribidi-shim is used when a system FriBiDi is absent."
),
},
"externalReferences": [
{
"type": "vcs",
"url": "https://github.com/python-pillow/Pillow/tree/main/src/thirdparty/raqm",
},
],
},
]
native_deps = [
{
"bom-ref": "pkg:generic/freetype2",
"type": "library",
"name": "FreeType",
"version": versions["freetype"],
"scope": "optional",
"description": "Font rendering (optional, used by PIL._imagingft). "
"Required for text/font support.",
"licenses": [{"license": {"id": "FTL"}}],
"externalReferences": [
{"type": "website", "url": "https://freetype.org"},
{
"type": "distribution",
"url": "https://download.savannah.gnu.org/releases/freetype/",
},
],
},
{
"bom-ref": "pkg:generic/fribidi",
"type": "library",
"name": "FriBiDi",
"version": versions["fribidi"],
"scope": "optional",
"description": "Unicode bidi algorithm library (optional, "
"loaded at runtime by fribidi-shim).",
"licenses": [{"license": {"id": "LGPL-2.1-or-later"}}],
"externalReferences": [
{"type": "website", "url": "https://github.com/fribidi/fribidi"},
{
"type": "distribution",
"url": "https://github.com/fribidi/fribidi/releases",
},
],
},
{
"bom-ref": "pkg:generic/harfbuzz",
"type": "library",
"name": "HarfBuzz",
"version": versions["harfbuzz"],
"scope": "optional",
"description": "Text shaping (optional, required by libraqm "
"for complex text layout).",
"licenses": [{"license": {"id": "MIT"}}],
"externalReferences": [
{"type": "website", "url": "https://harfbuzz.github.io"},
{
"type": "distribution",
"url": "https://github.com/harfbuzz/harfbuzz/releases",
},
],
},
{
"bom-ref": "pkg:generic/libavif",
"type": "library",
"name": "libavif",
"version": versions["libavif"],
"scope": "optional",
"description": "AVIF codec (optional, used by PIL._avif).",
"licenses": [{"license": {"id": "BSD-2-Clause"}}],
"externalReferences": [
{"type": "website", "url": "https://github.com/AOMediaCodec/libavif"},
{
"type": "distribution",
"url": "https://github.com/AOMediaCodec/libavif/releases",
},
],
},
{
"bom-ref": "pkg:generic/libimagequant",
"type": "library",
"name": "libimagequant",
"version": versions["libimagequant"],
"scope": "optional",
"description": "Improved colour quantization (optional).",
"licenses": [{"license": {"id": "GPL-3.0-or-later"}}],
"externalReferences": [
{"type": "website", "url": "https://pngquant.org/lib/"},
{
"type": "distribution",
"url": "https://github.com/ImageOptim/libimagequant/tags",
},
],
},
{
"bom-ref": "pkg:generic/libjpeg",
"type": "library",
"name": "libjpeg / libjpeg-turbo",
"version": versions["jpegturbo"],
"description": "JPEG codec (required by default; disable with "
"-C jpeg=disable).",
"licenses": [
{"license": {"id": "IJG"}},
{"license": {"id": "BSD-3-Clause"}},
],
"externalReferences": [
{"type": "website", "url": "https://ijg.org"},
{"type": "website", "url": "https://libjpeg-turbo.org"},
{
"type": "distribution",
"url": "https://github.com/libjpeg-turbo/libjpeg-turbo/releases",
},
],
},
{
"bom-ref": "pkg:generic/libtiff",
"type": "library",
"name": "libtiff",
"version": versions["tiff"],
"scope": "optional",
"description": "TIFF codec (optional).",
"licenses": [{"license": {"id": "libtiff"}}],
"externalReferences": [
{"type": "website", "url": "https://libtiff.gitlab.io/libtiff/"},
{
"type": "distribution",
"url": "https://download.osgeo.org/libtiff/",
},
],
},
{
"bom-ref": "pkg:generic/libwebp",
"type": "library",
"name": "libwebp",
"version": versions["libwebp"],
"scope": "optional",
"description": "WebP codec (optional, used by PIL._webp).",
"licenses": [{"license": {"id": "BSD-3-Clause"}}],
"externalReferences": [
{
"type": "website",
"url": "https://chromium.googlesource.com/webm/libwebp",
},
{
"type": "distribution",
"url": "https://chromium.googlesource.com/webm/libwebp",
},
],
},
{
"bom-ref": "pkg:generic/libxcb",
"type": "library",
"name": "libxcb",
"version": versions["libxcb"],
"scope": "optional",
"description": "X11 screen-grab support (optional, "
"used by PIL._imaging on macOS and Linux).",
"licenses": [{"license": {"id": "X11"}}],
"externalReferences": [
{"type": "website", "url": "https://xcb.freedesktop.org"},
{
"type": "distribution",
"url": "https://xcb.freedesktop.org/dist/",
},
],
},
{
"bom-ref": "pkg:generic/littlecms2",
"type": "library",
"name": "Little CMS 2",
"version": versions["lcms2"],
"scope": "optional",
"description": "Colour management (optional, used by PIL._imagingcms).",
"licenses": [{"license": {"id": "MIT"}}],
"externalReferences": [
{"type": "website", "url": "https://www.littlecms.com"},
{
"type": "distribution",
"url": "https://github.com/mm2/Little-CMS/releases",
},
],
},
{
"bom-ref": "pkg:generic/openjpeg",
"type": "library",
"name": "OpenJPEG",
"version": versions["openjpeg"],
"scope": "optional",
"description": "JPEG 2000 codec (optional).",
"licenses": [{"license": {"id": "BSD-2-Clause"}}],
"externalReferences": [
{"type": "website", "url": "https://www.openjpeg.org"},
{
"type": "distribution",
"url": "https://github.com/uclouvain/openjpeg/releases",
},
],
},
{
"bom-ref": "pkg:pypi/pybind11",
"type": "library",
"name": "pybind11",
"scope": "excluded",
"description": "Parallel C compilation library (build-time dependency).",
"licenses": [{"license": {"id": "BSD-3-Clause"}}],
"externalReferences": [
{"type": "website", "url": "https://pybind11.readthedocs.io"},
{
"type": "distribution",
"url": "https://github.com/pybind/pybind11/releases",
},
],
},
{
"bom-ref": "pkg:generic/zlib",
"type": "library",
"name": "zlib",
"version": versions["zlib-ng"],
"description": "Deflate/PNG compression (required by default; "
"disable with -C zlib=disable).",
"licenses": [{"license": {"id": "Zlib"}}],
"externalReferences": [
{"type": "website", "url": "https://zlib.net"},
{"type": "distribution", "url": "https://zlib.net"},
],
},
]
dependencies = [
{
"ref": purl,
"dependsOn": [e["bom-ref"] for e in ext_components],
},
{
"ref": f"{purl}#c-ext/PIL._avif",
"dependsOn": ["pkg:generic/libavif"],
},
{
"ref": f"{purl}#c-ext/PIL._imaging",
"dependsOn": [
"pkg:generic/libimagequant",
"pkg:generic/libjpeg",
"pkg:generic/libtiff",
"pkg:generic/libxcb",
"pkg:generic/openjpeg",
"pkg:generic/zlib",
],
},
{
"ref": f"{purl}#c-ext/PIL._imagingcms",
"dependsOn": ["pkg:generic/littlecms2"],
},
{
"ref": f"{purl}#c-ext/PIL._imagingft",
"dependsOn": [
"pkg:generic/freetype2",
"pkg:generic/fribidi",
"pkg:generic/harfbuzz",
f"{purl}#thirdparty/fribidi-shim",
f"{purl}#thirdparty/raqm",
],
},
{
"ref": f"{purl}#c-ext/PIL._webp",
"dependsOn": ["pkg:generic/libwebp"],
},
{
"ref": f"{purl}#thirdparty/raqm",
"dependsOn": [
"pkg:generic/harfbuzz",
f"{purl}#thirdparty/fribidi-shim",
],
},
]
return {
"$schema": "http://cyclonedx.org/schema/bom-1.7.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.7",
"serialNumber": f"urn:uuid:{serial}",
"version": 1,
"metadata": {
"timestamp": now,
"lifecycles": [{"phase": "build"}],
"tools": {
"components": [
{
"type": "application",
"name": "generate-sbom.py",
"group": "pillow",
}
]
},
"component": metadata_component,
},
"components": ext_components + vendored_components + native_deps,
"dependencies": dependencies,
}
def main() -> None:
version = get_version()
parser = argparse.ArgumentParser(
description=__doc__, formatter_class=argparse.ArgumentDefaultsHelpFormatter
)
parser.add_argument(
"output",
nargs="?",
type=Path,
default=Path(f"pillow-{version}.cdx.json"),
help="output file",
)
args = parser.parse_args()
sbom = generate(version)
args.output.write_text(json.dumps(sbom, indent=2) + "\n", encoding="utf-8")
print(
f"Wrote {args.output} (Pillow {version}, {len(sbom['components'])} components)"
)
if __name__ == "__main__":
main()

165
.github/renovate.json vendored
View File

@ -7,16 +7,169 @@
"Dependency"
],
"minimumReleaseAge": "7 days",
"prCreation": "not-pending",
"schedule": [
"* * 3 * *"
],
"customManagers": [
{
"customType": "regex",
"managerFilePatterns": ["/^\\.github/dependencies\\.json$/"],
"matchStrings": ["\"brotli\":\\s*\"(?<currentValue>\\d+[^\"]*)\""],
"depNameTemplate": "brotli",
"packageNameTemplate": "google/brotli",
"datasourceTemplate": "github-releases",
"extractVersionTemplate": "^v(?<version>.+)$"
},
{
"customType": "regex",
"managerFilePatterns": ["/^\\.github/dependencies\\.json$/"],
"matchStrings": ["\"bzip2\":\\s*\"(?<currentValue>\\d+[^\"]*)\""],
"depNameTemplate": "bzip2",
"packageNameTemplate": "bzip2/bzip2",
"datasourceTemplate": "gitlab-tags",
"extractVersionTemplate": "^bzip2-(?<version>.+)$"
},
{
"customType": "regex",
"managerFilePatterns": ["/^\\.github/dependencies\\.json$/"],
"matchStrings": ["\"freetype\":\\s*\"(?<currentValue>\\d+[^\"]*)\""],
"depNameTemplate": "freetype",
"packageNameTemplate": "freetype/freetype",
"datasourceTemplate": "gitlab-tags",
"registryUrlTemplate": "https://gitlab.freedesktop.org",
"extractVersionTemplate": "^VER-(?<version>[\\d-]+)$",
"versioningTemplate": "regex:^(?<major>\\d+)[.-](?<minor>\\d+)[.-](?<patch>\\d+)$"
},
{
"customType": "regex",
"managerFilePatterns": ["/^\\.github/dependencies\\.json$/"],
"matchStrings": ["\"fribidi\":\\s*\"(?<currentValue>\\d+[^\"]*)\""],
"depNameTemplate": "fribidi",
"packageNameTemplate": "fribidi/fribidi",
"datasourceTemplate": "github-releases",
"extractVersionTemplate": "^v(?<version>.+)$"
},
{
"customType": "regex",
"managerFilePatterns": ["/^\\.github/dependencies\\.json$/"],
"matchStrings": ["\"harfbuzz\":\\s*\"(?<currentValue>\\d+[^\"]*)\""],
"depNameTemplate": "harfbuzz",
"packageNameTemplate": "harfbuzz/harfbuzz",
"datasourceTemplate": "github-releases"
},
{
"customType": "regex",
"managerFilePatterns": ["/^\\.github/dependencies\\.json$/"],
"matchStrings": ["\"jpegturbo\":\\s*\"(?<currentValue>\\d+[^\"]*)\""],
"depNameTemplate": "jpegturbo",
"packageNameTemplate": "libjpeg-turbo/libjpeg-turbo",
"datasourceTemplate": "github-releases"
},
{
"customType": "regex",
"managerFilePatterns": ["/^\\.github/dependencies\\.json$/"],
"matchStrings": ["\"lcms2\":\\s*\"(?<currentValue>\\d+[^\"]*)\""],
"depNameTemplate": "lcms2",
"packageNameTemplate": "mm2/Little-CMS",
"datasourceTemplate": "github-releases",
"extractVersionTemplate": "^lcms(?<version>.+)$"
},
{
"customType": "regex",
"managerFilePatterns": ["/^\\.github/dependencies\\.json$/"],
"matchStrings": ["\"libavif\":\\s*\"(?<currentValue>\\d+[^\"]*)\""],
"depNameTemplate": "libavif",
"packageNameTemplate": "AOMediaCodec/libavif",
"datasourceTemplate": "github-releases",
"extractVersionTemplate": "^v(?<version>.+)$"
},
{
"customType": "regex",
"managerFilePatterns": ["/^\\.github/dependencies\\.json$/"],
"matchStrings": ["\"libimagequant\":\\s*\"(?<currentValue>\\d+[^\"]*)\""],
"depNameTemplate": "libimagequant",
"packageNameTemplate": "ImageOptim/libimagequant",
"datasourceTemplate": "github-tags"
},
{
"customType": "regex",
"managerFilePatterns": ["/^\\.github/dependencies\\.json$/"],
"matchStrings": ["\"libpng\":\\s*\"(?<currentValue>\\d+[^\"]*)\""],
"depNameTemplate": "libpng",
"packageNameTemplate": "pnggroup/libpng",
"datasourceTemplate": "github-tags",
"extractVersionTemplate": "^v(?<version>.+)$"
},
{
"customType": "regex",
"managerFilePatterns": ["/^\\.github/dependencies\\.json$/"],
"matchStrings": ["\"libwebp\":\\s*\"(?<currentValue>\\d+[^\"]*)\""],
"depNameTemplate": "libwebp",
"packageNameTemplate": "webmproject/libwebp",
"datasourceTemplate": "github-tags",
"extractVersionTemplate": "^v(?<version>.+)$"
},
{
"customType": "regex",
"managerFilePatterns": ["/^\\.github/dependencies\\.json$/"],
"matchStrings": ["\"libxcb\":\\s*\"(?<currentValue>\\d+[^\"]*)\""],
"depNameTemplate": "libxcb",
"packageNameTemplate": "xorg/lib/libxcb",
"datasourceTemplate": "gitlab-tags",
"registryUrlTemplate": "https://gitlab.freedesktop.org",
"extractVersionTemplate": "^libxcb-(?<version>.+)$"
},
{
"customType": "regex",
"managerFilePatterns": ["/^\\.github/dependencies\\.json$/"],
"matchStrings": ["\"openjpeg\":\\s*\"(?<currentValue>\\d+[^\"]*)\""],
"depNameTemplate": "openjpeg",
"packageNameTemplate": "uclouvain/openjpeg",
"datasourceTemplate": "github-releases",
"extractVersionTemplate": "^v(?<version>.+)$"
},
{
"customType": "regex",
"managerFilePatterns": ["/^\\.github/dependencies\\.json$/"],
"matchStrings": ["\"tiff\":\\s*\"(?<currentValue>\\d+[^\"]*)\""],
"depNameTemplate": "tiff",
"packageNameTemplate": "libtiff/libtiff",
"datasourceTemplate": "gitlab-tags",
"extractVersionTemplate": "^v(?<version>.+)$"
},
{
"customType": "regex",
"managerFilePatterns": ["/^\\.github/dependencies\\.json$/"],
"matchStrings": ["\"xz\":\\s*\"(?<currentValue>\\d+[^\"]*)\""],
"depNameTemplate": "xz",
"packageNameTemplate": "tukaani-project/xz",
"datasourceTemplate": "github-releases",
"extractVersionTemplate": "^v(?<version>.+)$"
},
{
"customType": "regex",
"managerFilePatterns": ["/^\\.github/dependencies\\.json$/"],
"matchStrings": ["\"zlib-ng\":\\s*\"(?<currentValue>\\d+[^\"]*)\""],
"depNameTemplate": "zlib-ng",
"packageNameTemplate": "zlib-ng/zlib-ng",
"datasourceTemplate": "github-releases"
},
{
"customType": "regex",
"managerFilePatterns": ["/^\\.github/dependencies\\.json$/"],
"matchStrings": ["\"zstd\":\\s*\"(?<currentValue>\\d+[^\"]*)\""],
"depNameTemplate": "zstd",
"packageNameTemplate": "facebook/zstd",
"datasourceTemplate": "github-releases",
"extractVersionTemplate": "^v(?<version>.+)$"
}
],
"packageRules": [
{
"groupName": "github-actions",
"matchManagers": [
"github-actions"
],
"matchManagers": ["github-actions"],
"separateMajorMinor": false
}
],
"schedule": [
"* * 3 * *"
]
}

13
.github/workflows/Brewfile vendored Normal file
View File

@ -0,0 +1,13 @@
brew "aom"
brew "dav1d"
brew "freetype"
brew "ghostscript"
brew "jpeg-turbo"
brew "libimagequant"
brew "libraqm"
brew "libtiff"
brew "little-cms2"
brew "openjpeg"
brew "rav1e"
brew "svt-av1"
brew "webp"

View File

@ -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@d87225267726cf7ce1a3e17cf103c5ac943c4f05 # master
with:
oss-fuzz-project-name: 'pillow'
language: python
dry-run: false
- name: Run Fuzzers
id: run
uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master
uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@d87225267726cf7ce1a3e17cf103c5ac943c4f05 # master
with:
oss-fuzz-project-name: 'pillow'
fuzz-seconds: 600
language: python
dry-run: false
- name: Upload New Crash
uses: actions/upload-artifact@v6
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@v6
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: steps.run.outcome == 'success'
with:
name: crash

View File

@ -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

View File

@ -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

View File

@ -2,20 +2,7 @@
set -e
brew install \
aom \
dav1d \
freetype \
ghostscript \
jpeg-turbo \
libimagequant \
libraqm \
libtiff \
little-cms2 \
openjpeg \
rav1e \
svt-av1 \
webp
brew bundle --file=.github/workflows/Brewfile
export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
python3 -m pip install coverage

View File

@ -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@v6
- uses: release-drafter/release-drafter@5de93583980a40bd78603b6dfdcda5b4df377b32 # v7.2.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -17,7 +17,7 @@ env:
jobs:
stale:
if: github.repository_owner == 'python-pillow'
if: github.event.repository.fork == false
permissions:
issues: write
@ -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"

View File

@ -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,39 +34,37 @@ 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-2-amd64,
amazon-2023-amd64,
arch,
centos-stream-9-amd64,
centos-stream-10-amd64,
debian-12-bookworm-x86,
debian-12-bookworm-amd64,
debian-13-trixie-x86,
debian-13-trixie-amd64,
fedora-42-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
@ -80,7 +73,7 @@ jobs:
- name: Set up QEMU
if: "matrix.qemu-arch"
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
with:
platforms: ${{ matrix.qemu-arch }}
@ -108,11 +101,10 @@ jobs:
.ci/after_success.sh
- name: Upload coverage
uses: codecov/codecov-action@v5
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
with:
flags: GHA_Docker
name: ${{ matrix.docker }}
token: ${{ secrets.CODECOV_ORG_TOKEN }}
success:
permissions:

View File

@ -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,9 +82,8 @@ jobs:
.ci/test.sh
- name: Upload coverage
uses: codecov/codecov-action@v5
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
with:
files: ./coverage.xml
flags: GHA_Windows
name: "MSYS2 MinGW"
token: ${{ secrets.CODECOV_ORG_TOKEN }}

View File

@ -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

View File

@ -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

View File

@ -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@v6
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: failure()
with:
name: errors
@ -229,12 +224,11 @@ jobs:
shell: pwsh
- name: Upload coverage
uses: codecov/codecov-action@v5
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
with:
files: ./coverage.xml
flags: GHA_Windows
name: ${{ runner.os }} Python ${{ matrix.python-version }}
token: ${{ secrets.CODECOV_ORG_TOKEN }}
success:
permissions:

View File

@ -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:
@ -47,7 +42,6 @@ jobs:
"3.15",
"3.14t",
"3.14",
"3.13t",
"3.13",
"3.12",
"3.11",
@ -56,10 +50,6 @@ jobs:
include:
- { python-version: "3.12", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" }
- { python-version: "3.11", PYTHONOPTIMIZE: 2 }
# Free-threaded
- { python-version: "3.15t", disable-gil: true }
- { python-version: "3.14t", disable-gil: true }
- { python-version: "3.13t", disable-gil: true }
# Intel
- { os: "macos-26-intel", python-version: "3.10" }
exclude:
@ -69,12 +59,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
@ -83,17 +73,12 @@ jobs:
".ci/*.sh"
"pyproject.toml"
- name: Set PYTHON_GIL
if: "${{ matrix.disable-gil }}"
run: |
echo "PYTHON_GIL=0" >> $GITHUB_ENV
- name: Build system information
run: python3 .github/workflows/system-info.py
- 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 +86,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 +94,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 +147,7 @@ jobs:
mkdir -p Tests/errors
- name: Upload errors
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: failure()
with:
name: errors
@ -173,11 +158,10 @@ jobs:
.ci/after_success.sh
- name: Upload coverage
uses: codecov/codecov-action@v5
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 }}
token: ${{ secrets.CODECOV_ORG_TOKEN }}
success:
permissions:

View File

@ -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
@ -178,7 +179,6 @@ function build_libavif {
build_simple nasm 2.16.03 https://www.nasm.us/pub/nasm/releasebuilds/2.16.03
fi
local build_type=MinSizeRel
local build_shared=ON
local lto=ON
@ -195,9 +195,6 @@ function build_libavif {
build_shared=OFF
fi
else
if [[ "$MB_ML_VER" == 2014 ]] && [[ "$PLAT" == "x86_64" ]]; then
build_type=Release
fi
libavif_cmake_flags=(-DCMAKE_SHARED_LINKER_FLAGS_INIT="-Wl,--strip-all,-z,relro,-z,now")
fi
if [[ -n "$IOS_SDK" ]] && [[ "$PLAT" == "x86_64" ]]; then
@ -226,7 +223,7 @@ function build_libavif {
-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=$lto \
-DCMAKE_C_VISIBILITY_PRESET=hidden \
-DCMAKE_CXX_VISIBILITY_PRESET=hidden \
-DCMAKE_BUILD_TYPE=$build_type \
-DCMAKE_BUILD_TYPE=MinSizeRel \
"${libavif_cmake_flags[@]}" \
$HOST_CMAKE_FLAGS . )

View File

@ -10,9 +10,13 @@ on:
# │ │ │ │ │
- cron: "42 1 * * 0,3"
push:
paths:
paths: &paths
- ".ci/requirements-cibw.txt"
- ".github/workflows/wheel*"
- ".ci/requirements-sbom.txt"
- ".github/compare-dist-sizes.py"
- ".github/dependencies.json"
- ".github/generate-sbom.py"
- ".github/workflows/wheels*"
- "pyproject.toml"
- "setup.py"
- "wheels/*"
@ -21,14 +25,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:
@ -39,12 +36,12 @@ concurrency:
cancel-in-progress: true
env:
EXPECTED_DISTS: 91
EXPECTED_DISTS: 66
FORCE_COLOR: 1
jobs:
build-native-wheels:
if: github.event_name != 'schedule' || github.repository_owner == 'python-pillow'
if: github.event_name != 'schedule' || github.event.repository.fork == false
name: ${{ matrix.name }}
runs-on: ${{ matrix.os }}
strategy:
@ -74,26 +71,26 @@ jobs:
os: macos-latest
cibw_arch: arm64
macosx_deployment_target: "11.0"
- name: "manylinux2014 and musllinux x86_64"
platform: linux
os: ubuntu-latest
cibw_arch: x86_64
manylinux: "manylinux2014"
- name: "manylinux_2_28 x86_64"
platform: linux
os: ubuntu-latest
cibw_arch: x86_64
build: "*manylinux*"
- name: "manylinux2014 and musllinux aarch64"
- name: "musllinux x86_64"
platform: linux
os: ubuntu-24.04-arm
cibw_arch: aarch64
manylinux: "manylinux2014"
os: ubuntu-latest
cibw_arch: x86_64
build: "*musllinux*"
- name: "manylinux_2_28 aarch64"
platform: linux
os: ubuntu-24.04-arm
cibw_arch: aarch64
build: "*manylinux*"
- name: "musllinux aarch64"
platform: linux
os: ubuntu-24.04-arm
cibw_arch: aarch64
build: "*musllinux*"
- name: "iOS arm64 device"
platform: ios
os: macos-latest
@ -104,15 +101,15 @@ jobs:
cibw_arch: arm64_iphonesimulator
- name: "iOS x86_64 simulator"
platform: ios
os: macos-15-intel
os: macos-26-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"
@ -127,20 +124,16 @@ jobs:
CIBW_PLATFORM: ${{ matrix.platform }}
CIBW_ARCHS: ${{ matrix.cibw_arch }}
CIBW_BUILD: ${{ matrix.build }}
CIBW_ENABLE: cpython-prerelease cpython-freethreading pypy
CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.manylinux }}
CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.manylinux }}
CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }}
CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }}
CIBW_ENABLE: cpython-prerelease pypy
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }}
- uses: actions/upload-artifact@v6
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: dist-${{ matrix.name }}
path: ./wheelhouse/*.whl
windows:
if: github.event_name != 'schedule' || github.repository_owner == 'python-pillow'
if: github.event_name != 'schedule' || github.event.repository.fork == false
name: Windows ${{ matrix.cibw_arch }}
runs-on: ${{ matrix.os }}
strategy:
@ -154,18 +147,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"
@ -202,7 +195,7 @@ jobs:
CIBW_ARCHS: ${{ matrix.cibw_arch }}
CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd"
CIBW_CACHE_PATH: "C:\\cibw"
CIBW_ENABLE: cpython-prerelease cpython-freethreading pypy
CIBW_ENABLE: cpython-prerelease pypy
CIBW_TEST_SKIP: "*-win_arm64"
CIBW_TEST_COMMAND: 'docker run --rm
-v {project}:C:\pillow
@ -214,33 +207,33 @@ jobs:
shell: bash
- name: Upload wheels
uses: actions/upload-artifact@v6
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@v6
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: fribidi-windows-${{ matrix.cibw_arch }}
path: winbuild\build\bin\fribidi*
sdist:
if: github.event_name != 'schedule' || github.repository_owner == 'python-pillow'
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@v6
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: dist-sdist
path: dist/*.tar.gz
@ -250,7 +243,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
@ -263,25 +256,98 @@ jobs:
echo $files
[ "$files" -eq $EXPECTED_DISTS ] || exit 1
compare-dist-sizes:
needs: [build-native-wheels, windows, sdist]
runs-on: ubuntu-latest
name: Compare dist sizes vs PyPI
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
enable-cache: false
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: dist-*
path: dist
merge-multiple: true
- name: Compare dist sizes vs latest PyPI release
run: uv run .github/compare-dist-sizes.py dist
scientific-python-nightly-wheels-publish:
if: github.repository_owner == 'python-pillow' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')
if: github.event.repository.fork == false && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')
needs: count-dists
runs-on: ubuntu-latest
name: Upload wheels to scientific-python-nightly-wheels
environment:
name: release-anaconda
url: https://anaconda.org/channels/scientific-python-nightly-wheels/packages/pillow/overview
steps:
- uses: actions/download-artifact@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:
if: github.event_name != 'schedule' || github.event.repository.fork == false
runs-on: ubuntu-latest
name: Generate SBOM
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.x"
- name: Generate CycloneDX SBOM
run: python3 .github/generate-sbom.py
- name: Upload SBOM as workflow artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: sbom
path: "pillow-*.cdx.json"
- name: Validate SBOM
run: |
python3 -m pip install -r .ci/requirements-sbom.txt
check-jsonschema --schemafile "https://raw.githubusercontent.com/CycloneDX/specification/1.7/schema/bom-1.7.schema.json" pillow-*.cdx.json
sbom-publish:
if: |
github.event.repository.fork == false
&& github.event_name == 'push'
&& startsWith(github.ref, 'refs/tags')
needs: [count-dists, sbom]
runs-on: ubuntu-latest
name: Publish SBOM to GitHub release
permissions:
contents: write
steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: sbom
path: .
- name: Attach SBOM to GitHub release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh release upload "$GITHUB_REF_NAME" pillow-*.cdx.json
pypi-publish:
if: github.repository_owner == 'python-pillow' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
if: github.event.repository.fork == false && github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
needs: count-dists
runs-on: ubuntu-latest
name: Upload release to PyPI
@ -291,12 +357,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

6
.github/zizmor.yml vendored
View File

@ -1,6 +0,0 @@
# https://docs.zizmor.sh/configuration/
rules:
unpinned-uses:
config:
policies:
"*": ref-pin

3
.gitignore vendored
View File

@ -97,3 +97,6 @@ pillow-test-images.zip
# pyinstaller
*.spec
# Generated SBOM
pillow-*.cdx.json

View File

@ -1,12 +1,12 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.4
rev: v0.15.12
hooks:
- id: ruff-check
args: [--exit-non-zero-on-fix]
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 26.1.0
rev: 26.3.1
hooks:
- id: black
@ -24,7 +24,7 @@ repos:
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
- repo: https://github.com/pre-commit/mirrors-clang-format
rev: v22.1.0
rev: v22.1.4
hooks:
- id: clang-format
types: [c]
@ -48,18 +48,20 @@ repos:
args: [--allow-multiple-documents]
- id: end-of-file-fixer
exclude: ^Tests/images/
- id: file-contents-sorter
files: .github/workflows/Brewfile
- id: trailing-whitespace
exclude: ^\.github/.*TEMPLATE|^Tests/(fonts|images)/
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.37.0
rev: 0.37.2
hooks:
- id: check-github-workflows
- id: check-readthedocs
- id: check-renovate
- repo: https://github.com/zizmorcore/zizmor-pre-commit
rev: v1.22.0
rev: v1.24.1
hooks:
- id: zizmor
@ -69,7 +71,7 @@ repos:
- id: sphinx-lint
- repo: https://github.com/tox-dev/pyproject-fmt
rev: v2.16.2
rev: v2.21.1
hooks:
- id: pyproject-fmt

View File

@ -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)
<table>
<tr>
@ -106,4 +108,8 @@ The core image library is designed for fast access to data stored in a few basic
## Report a vulnerability
To report a security vulnerability, please follow the procedure described in the [Tidelift security policy](https://tidelift.com/docs/security).
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.
DO NOT report sensitive vulnerability information in public.

View File

@ -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

View File

@ -1,9 +1,17 @@
from __future__ import annotations
import io
import sys
import sysconfig
import pytest
FREE_THREADED_BUILD = bool(sysconfig.get_config_var("Py_GIL_DISABLED"))
gil_enabled_at_start = True
if FREE_THREADED_BUILD:
gil_enabled_at_start = sys._is_gil_enabled() # type: ignore[attr-defined]
def pytest_report_header(config: pytest.Config) -> str:
try:
@ -16,6 +24,25 @@ def pytest_report_header(config: pytest.Config) -> str:
return f"pytest_report_header failed: {e}"
def pytest_terminal_summary(terminalreporter: pytest.TerminalReporter) -> None:
if (
FREE_THREADED_BUILD
and not gil_enabled_at_start
and sys._is_gil_enabled() # type: ignore[attr-defined]
):
tr = terminalreporter
tr.ensure_newline()
tr.section("GIL re-enabled", red=True, bold=True)
tr.line("The GIL was re-enabled at runtime during the tests.")
tr.line("This can happen with no test failures if the RuntimeWarning")
tr.line("raised by Python when this happens is filtered by a test.")
tr.line("")
tr.line("Please ensure all new C modules declare support for running")
tr.line("without the GIL. Any new tests that intentionally imports")
tr.line("code that re-enables the GIL should do so in a subprocess.")
pytest.exit("GIL re-enabled during tests", returncode=1)
def pytest_configure(config: pytest.Config) -> None:
config.addinivalue_line(
"markers",

View File

@ -145,14 +145,14 @@ class TestFileAvif:
# avifdec hopper.avif avif/hopper_avif_write.png
assert_image_similar_tofile(
reloaded, "Tests/images/avif/hopper_avif_write.png", 6.88
reloaded, "Tests/images/avif/hopper_avif_write.png", 6.93
)
# This test asserts that the images are similar. If the average pixel
# difference between the two images is less than the epsilon value,
# then we're going to accept that it's a reasonable lossy version of
# the image.
assert_image_similar(reloaded, im, 9.28)
assert_image_similar(reloaded, im, 9.39)
def test_AvifEncoder_with_invalid_args(self) -> None:
"""

View File

@ -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")

View File

@ -178,9 +178,9 @@ def test_default_num_resolutions(
def test_reduce() -> None:
with Image.open("Tests/images/test-card-lossless.jp2") as im:
assert callable(im.reduce)
assert isinstance(im, Jpeg2KImagePlugin.Jpeg2KImageFile)
im.reduce = 2 # type: ignore[assignment, method-assign]
im.reduce = 2
assert im.reduce == 2
im.load()

View File

@ -4,7 +4,7 @@ import re
import sys
import warnings
import zlib
from io import BytesIO
from io import BytesIO, TextIOWrapper
from pathlib import Path
from types import ModuleType
from typing import Any, cast
@ -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"
@ -821,19 +852,15 @@ class TestFilePng:
@pytest.mark.parametrize("buffer", (True, False))
def test_save_stdout(self, buffer: bool, monkeypatch: pytest.MonkeyPatch) -> None:
class MyStdOut:
buffer = BytesIO()
mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
fp = BytesIO()
mystdout = TextIOWrapper(fp) if buffer else fp
monkeypatch.setattr(sys, "stdout", mystdout)
with Image.open(TEST_PNG_FILE) as im:
im.save(sys.stdout, "PNG") # type: ignore[arg-type]
if isinstance(mystdout, MyStdOut):
mystdout = mystdout.buffer
with Image.open(mystdout) as reloaded:
with Image.open(fp) as reloaded:
assert_image_equal_tofile(reloaded, TEST_PNG_FILE)
def test_truncated_end_chunk(self, monkeypatch: pytest.MonkeyPatch) -> None:

View File

@ -1,7 +1,7 @@
from __future__ import annotations
import sys
from io import BytesIO
from io import BytesIO, TextIOWrapper
from pathlib import Path
import pytest
@ -381,17 +381,13 @@ def test_mimetypes(tmp_path: Path) -> None:
@pytest.mark.parametrize("buffer", (True, False))
def test_save_stdout(buffer: bool, monkeypatch: pytest.MonkeyPatch) -> None:
class MyStdOut:
buffer = BytesIO()
mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
fp = BytesIO()
mystdout = TextIOWrapper(fp) if buffer else fp
monkeypatch.setattr(sys, "stdout", mystdout)
with Image.open(TEST_FILE) as im:
im.save(sys.stdout, "PPM") # type: ignore[arg-type]
if isinstance(mystdout, MyStdOut):
mystdout = mystdout.buffer
with Image.open(mystdout) as reloaded:
with Image.open(fp) as reloaded:
assert_image_equal_tofile(reloaded, TEST_FILE)

View File

@ -49,6 +49,12 @@ class TestFileWebp:
assert version is not None
assert re.search(r"\d+\.\d+\.\d+$", version)
def test_invalid_file(self) -> None:
invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError):
WebPImagePlugin.WebPImageFile(invalid_file)
def test_read_rgb(self) -> None:
"""
Can we read a RGB mode WebP file without error?

View File

@ -2,6 +2,8 @@ from __future__ import annotations
from PIL import Image, ImageDraw, ImageFont
from .helper import skip_unless_feature
class TestFontCrash:
def _fuzz_font(self, font: ImageFont.FreeTypeFont) -> None:
@ -14,6 +16,7 @@ class TestFontCrash:
draw.multiline_textbbox((10, 10), "ABC\nAaaa", font, stroke_width=2)
draw.text((10, 10), "Test Text", font=font, fill="#000")
@skip_unless_feature("freetype2")
def test_segfault(self) -> None:
font = ImageFont.truetype("Tests/fonts/fuzz_font-5203009437302784")
self._fuzz_font(font)

View File

@ -862,7 +862,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
@ -884,7 +884,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

View File

@ -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}"

View File

@ -365,7 +365,7 @@ def test_rotated_transposed_font(
bbox_b[2] - bbox_b[0],
)
# Check top left co-ordinates are correct
# Check top left coordinates are correct
assert bbox_b[:2] == (20, 20)
# text length is undefined for vertical text
@ -410,7 +410,7 @@ def test_unrotated_transposed_font(
bbox_b[3] - bbox_b[1],
)
# Check top left co-ordinates are correct
# Check top left coordinates are correct
assert bbox_b[:2] == (20, 20)
assert length_a == length_b

View File

@ -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")

View File

@ -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

View File

@ -2,7 +2,7 @@
# install raqm
archive=libraqm-0.10.3
archive=libraqm-0.10.5
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz

View File

@ -8,3 +8,4 @@ Handbook
tutorial
concepts
appendices
security

259
docs/handbook/security.rst Normal file
View File

@ -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
<https://en.wikipedia.org/wiki/STRIDE_model>`_ 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
<https://github.com/python-pillow/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 <https://github.com/python-pillow/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
<https://github.com/python-pillow/Pillow/security/advisories/new>`_.
If you cannot use GitHub, use the `Tidelift security contact
<https://tidelift.com/docs/security>`_. Tidelift will coordinate the fix and
disclosure.
**Do not report sensitive vulnerability information in public.**

View File

@ -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 \

View File

@ -19,8 +19,6 @@ These platforms are built and tested for every change.
+==================================+============================+=====================+
| Alpine | 3.12 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Amazon Linux 2 | 3.10 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Amazon Linux 2023 | 3.11 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Arch | 3.14 | x86-64 |
@ -29,14 +27,12 @@ These platforms are built and tested for every change.
+----------------------------------+----------------------------+---------------------+
| CentOS Stream 10 | 3.12 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Debian 12 Bookworm | 3.11 | x86, x86-64 |
+----------------------------------+----------------------------+---------------------+
| Debian 13 Trixie | 3.13 | x86, x86-64 |
+----------------------------------+----------------------------+---------------------+
| Fedora 42 | 3.13 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| 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 |
@ -48,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 |
+----------------------------------+----------------------------+---------------------+
@ -75,7 +71,7 @@ These platforms have been reported to work at the versions mentioned.
| Operating system | | Tested Python | | Latest tested | | Tested |
| | | versions | | Pillow version | | processors |
+==================================+=============================+==================+==============+
| macOS 26 Tahoe | 3.10, 3.11, 3.12, 3.13, 3.14| 12.1.1 |arm |
| macOS 26 Tahoe | 3.10, 3.11, 3.12, 3.13, 3.14| 12.2.0 |arm |
| +-----------------------------+------------------+ |
| | 3.9 | 11.3.0 | |
+----------------------------------+-----------------------------+------------------+--------------+

View File

@ -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%

View File

@ -4,8 +4,8 @@
Security
========
Prevent FITS decompression bomb
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
:cve:`2026-40192`: Prevent FITS decompression bomb
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
When decompressing GZIP data from a FITS image, Pillow did not limit the amount of data
being read, meaning that it was vulnerable to GZIP decompression bombs. This was
@ -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 added improved 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.
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.
If a font advances for each glyph by an exceedingly 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`

View File

@ -0,0 +1,57 @@
12.3.0
------
Security
========
TODO
^^^^
TODO
:cve:`YYYY-XXXXX`: TODO
^^^^^^^^^^^^^^^^^^^^^^^
TODO
Backwards incompatible changes
==============================
TODO
^^^^
TODO
Deprecations
============
TODO
^^^^
TODO
API changes
===========
TODO
^^^^
TODO
API additions
=============
TODO
^^^^
TODO
Other changes
=============
Removed Python 3.13 free-threaded wheels
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Python 3.13 added an experimental free-threaded mode, and Pillow 11.0.0 added
corresponding wheels. Now that Python 3.14 includes official support for it, Pillow has
removed wheels for Python 3.13 free-threaded mode.

View File

@ -81,7 +81,7 @@ Image.alpha_composite: dest
^^^^^^^^^^^^^^^^^^^^^^^^^^^
When calling :py:meth:`~PIL.Image.Image.alpha_composite`, the ``dest`` argument now
accepts negative co-ordinates, like the upper left corner of the ``box`` argument of
accepts negative coordinates, like the upper left corner of the ``box`` argument of
:py:meth:`~PIL.Image.Image.paste` can be negative. Naturally, this has effect of
cropping the overlaid image.

View File

@ -15,6 +15,7 @@ expected to be backported to earlier versions.
:maxdepth: 2
versioning
12.3.0
12.2.0
12.1.1
12.1.0

View File

@ -186,12 +186,6 @@ lint.isort.required-imports = [
[tool.pyproject-fmt]
max_supported_python = "3.14"
[tool.pytest]
addopts = [ "-ra", "--color=auto" ]
testpaths = [
"Tests",
]
[tool.mypy]
python_version = "3.10"
pretty = true
@ -203,3 +197,9 @@ follow_imports = "silent"
warn_redundant_casts = true
warn_unreachable = true
warn_unused_ignores = true
[tool.pytest]
addopts = [ "-ra", "--color=auto" ]
testpaths = [
"Tests",
]

View File

@ -57,7 +57,7 @@ WEBP_ROOT = None
ZLIB_ROOT = None
FUZZING_BUILD = "LIB_FUZZING_ENGINE" in os.environ
if sys.platform == "win32" and sys.version_info >= (3, 15):
if sys.platform == "win32" and sys.version_info >= (3, 16):
import atexit
atexit.register(

View File

@ -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)

View File

@ -175,7 +175,7 @@ class ImageDraw:
) -> None:
"""Draw an arc."""
ink, fill = self._getink(fill)
if ink is not None:
if ink is not None and width != 0:
self.draw.draw_arc(xy, start, end, ink, width)
def bitmap(
@ -235,12 +235,12 @@ class ImageDraw:
self,
xy: Coords,
fill: _Ink | None = None,
width: int = 0,
width: int = 1,
joint: str | None = None,
) -> None:
"""Draw a line, or a connected sequence of line segments."""
ink = self._getink(fill)[0]
if ink is not None:
if ink is not None and width != 0:
self.draw.draw_lines(xy, ink, width)
if joint == "curve" and width > 4:
points: Sequence[Sequence[float]]

View File

@ -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

View File

@ -148,10 +148,14 @@ def _write_image(
strip_size=math.ceil(width / 8) * height,
)
elif decode_filter == "DCTDecode":
Image.SAVE["JPEG"](im, op, filename)
from . import JpegImagePlugin
JpegImagePlugin._save(im, op, filename)
elif decode_filter == "JPXDecode":
from . import Jpeg2KImagePlugin
del dict_obj["BitsPerComponent"]
Image.SAVE["JPEG2000"](im, op, filename)
Jpeg2KImagePlugin._save(im, op, filename)
else:
msg = f"unsupported PDF filter ({decode_filter})"
raise ValueError(msg)

View File

@ -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
@ -402,7 +402,11 @@ class PdfParser:
self.pages_ref: IndirectReference | None
self.last_xref_section_offset: int | None
if self.buf:
self.read_pdf_info()
try:
self.read_pdf_info()
except PdfFormatError:
self.close()
raise
else:
self.file_size_total = self.file_size_this = 0
self.root = PdfDict()
@ -431,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
@ -686,7 +692,7 @@ class PdfParser:
self.read_prev_trailer(self.trailer_dict[b"Prev"])
def read_prev_trailer(
self, xref_section_offset: int, processed_offsets: list[int] = []
self, xref_section_offset: int, processed_offsets: list[int] | None = None
) -> None:
assert self.buf is not None
trailer_offset = self.read_xref_table(xref_section_offset=xref_section_offset)
@ -702,6 +708,8 @@ class PdfParser:
)
trailer_dict = self.interpret_trailer(trailer_data)
if b"Prev" in trailer_dict:
if processed_offsets is None:
processed_offsets = []
processed_offsets.append(xref_section_offset)
check_format_condition(
trailer_dict[b"Prev"] not in processed_offsets, "trailer loop found"

View File

@ -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(

View File

@ -43,10 +43,15 @@ class WebPImageFile(ImageFile.ImageFile):
__logical_frame = 0
def _open(self) -> None:
assert self.fp is not None
s = self.fp.read()
if not _accept(s):
msg = "not a WEBP file"
raise SyntaxError(msg)
# Use the newer AnimDecoder API to parse the (possibly) animated file,
# and access muxed chunks like ICC/EXIF/XMP.
assert self.fp is not None
self._decoder = _webp.WebPAnimDecoder(self.fp.read())
self._decoder = _webp.WebPAnimDecoder(s)
# Get info from decoder
self._size, self.info["loop"], bgcolor, self.n_frames, self.rawmode = (

View File

@ -1,4 +1,4 @@
# Master version for Pillow
from __future__ import annotations
__version__ = "12.2.0"
__version__ = "12.3.0.dev0"

View File

@ -3172,8 +3172,8 @@ _draw_lines(ImagingDrawObject *self, PyObject *args) {
PyObject *data;
int ink;
int width = 0;
if (!PyArg_ParseTuple(args, "Oi|i", &data, &ink, &width)) {
int width;
if (!PyArg_ParseTuple(args, "Oii", &data, &ink, &width)) {
return NULL;
}
@ -3182,7 +3182,7 @@ _draw_lines(ImagingDrawObject *self, PyObject *args) {
return NULL;
}
if (width <= 1) {
if (width == 1) {
double *p = NULL;
for (i = 0; i < n - 1; i++) {
p = &xy[i + i];

View File

@ -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;

View File

@ -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)

View File

@ -49,7 +49,7 @@ ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t byt
framesize = I32(ptr);
// there can be one pad byte in the framesize
if (bytes + (bytes % 2) < framesize) {
if ((unsigned)(bytes + (bytes % 2)) < framesize) {
return 0;
}
@ -259,7 +259,7 @@ ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t byt
state->errcode = IMAGING_CODEC_BROKEN;
return -1;
}
if (advance < 0 || advance > bytes) {
if (advance < 0 || advance > (unsigned)bytes) {
state->errcode = IMAGING_CODEC_OVERRUN;
return -1;
}

View File

@ -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 */

View File

@ -812,7 +812,7 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) {
break;
}
/* Adjust the tile co-ordinates based on the reduction (OpenJPEG
/* Adjust the tile coordinates based on the reduction (OpenJPEG
doesn't do this for us) */
tile_info.x0 = (tile_info.x0 + correction) >> context->reduce;
tile_info.y0 = (tile_info.y0 + correction) >> context->reduce;

View File

@ -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);
@ -726,19 +726,10 @@ ImagingResampleInner(
need_horizontal = xsize != imIn->xsize || box[0] || box[2] != xsize;
need_vertical = ysize != imIn->ysize || box[1] || box[3] != ysize;
ksize_horiz = precompute_coeffs(
imIn->xsize, box[0], box[2], xsize, filterp, &bounds_horiz, &kk_horiz
);
if (!ksize_horiz) {
return NULL;
}
ksize_vert = precompute_coeffs(
imIn->ysize, box[1], box[3], ysize, filterp, &bounds_vert, &kk_vert
);
if (!ksize_vert) {
free(bounds_horiz);
free(kk_horiz);
return NULL;
}
@ -749,6 +740,15 @@ ImagingResampleInner(
/* two-pass resize, horizontal pass */
if (need_horizontal) {
ksize_horiz = precompute_coeffs(
imIn->xsize, box[0], box[2], xsize, filterp, &bounds_horiz, &kk_horiz
);
if (!ksize_horiz) {
free(bounds_vert);
free(kk_vert);
return NULL;
}
// Shift bounds for vertical pass
for (i = 0; i < ysize; i++) {
bounds_vert[i * 2] -= ybox_first;
@ -768,10 +768,6 @@ ImagingResampleInner(
return NULL;
}
imOut = imIn = imTemp;
} else {
// Free in any case
free(bounds_horiz);
free(kk_horiz);
}
/* vertical pass */

View File

@ -1,3 +1,21 @@
Overview of changes leading to 0.10.5
Saturday, April 11, 2026
====================================
Check for NULL return from malloc in couple of places.
Overview of changes leading to 0.10.4
Thursday, February 5, 2026
====================================
Add build option to skip tests.
Add dependency override for use as a subproject.
Fix tests when b_ndebug=true.
Build, CI and documentation updates.
Overview of changes leading to 0.10.3
Tuesday, August 5, 2025
====================================

View File

@ -33,9 +33,9 @@
#define RAQM_VERSION_MAJOR 0
#define RAQM_VERSION_MINOR 10
#define RAQM_VERSION_MICRO 3
#define RAQM_VERSION_MICRO 5
#define RAQM_VERSION_STRING "0.10.3"
#define RAQM_VERSION_STRING "0.10.5"
#define RAQM_VERSION_ATLEAST(major,minor,micro) \
((major)*10000+(minor)*100+(micro) <= \

View File

@ -54,7 +54,7 @@
* @short_description: A library for complex text layout
* @include: raqm.h
*
* Raqm is a light weight text layout library with strong emphasis on
* Raqm is a lightweight text layout library with strong emphasis on
* supporting languages and writing systems that require complex text layout.
*
* The main object in Raqm API is #raqm_t, it stores all the states of the
@ -338,6 +338,8 @@ _raqm_alloc_run (raqm_t *rq)
else
{
run = malloc (sizeof (raqm_run_t));
if (!run)
return NULL;
run->font = NULL;
run->buffer = NULL;
}
@ -515,7 +517,7 @@ raqm_clear_contents (raqm_t *rq)
* @len: the length of @text.
*
* Adds @text to @rq to be used for layout. It must be a valid UTF-32 text, any
* invalid character will be replaced with U+FFFD. The text should typically
* invalid characters will be replaced with U+FFFD. The text should typically
* represent a full paragraph, since doing the layout of chunks of text
* separately can give improper output.
*
@ -765,13 +767,13 @@ raqm_set_par_direction (raqm_t *rq,
* raqm_set_language:
* @rq: a #raqm_t.
* @lang: a BCP47 language code.
* @start: index of first character that should use @face.
* @start: index of the first character that should use @face.
* @len: number of characters using @face.
*
* Sets a [BCP47 language
* code](https://www.w3.org/International/articles/language-tags/) to be used
* for @len-number of characters staring at @start. The @start and @len are
* input string array indices (i.e. counting bytes in UTF-8 and scaler values
* for @len-number of characters starting at @start. The @start and @len are
* input string array indices (i.e. counting bytes in UTF-8 and scalar values
* in UTF-32).
*
* This method can be used repeatedly to set different languages for different
@ -951,7 +953,7 @@ raqm_set_freetype_face (raqm_t *rq,
* raqm_set_freetype_face_range:
* @rq: a #raqm_t.
* @face: an #FT_Face.
* @start: index of first character that should use @face from the input string.
* @start: index of the first character that should use @face from the input string.
* @len: number of elements using @face.
*
* Sets an #FT_Face to be used for @len-number of characters staring at @start.
@ -962,7 +964,7 @@ raqm_set_freetype_face (raqm_t *rq,
*
* This method can be used repeatedly to set different faces for different
* parts of the text. It is the responsibility of the client to make sure that
* face ranges cover the whole text, and is properly aligned.
* face ranges cover the whole text, and are properly aligned.
*
* See also raqm_set_freetype_face().
*
@ -1023,9 +1025,6 @@ _raqm_set_freetype_load_flags (raqm_t *rq,
* Sets the load flags passed to FreeType when loading glyphs, should be the
* same flags used by the client when rendering FreeType glyphs.
*
* This requires version of HarfBuzz that has hb_ft_font_set_load_flags(), for
* older version the flags will be ignored.
*
* Return value:
* `true` if no errors happened, `false` otherwise.
*
@ -1042,22 +1041,19 @@ raqm_set_freetype_load_flags (raqm_t *rq,
* raqm_set_freetype_load_flags_range:
* @rq: a #raqm_t.
* @flags: FreeType load flags.
* @start: index of first character that should use @flags.
* @start: index of the first character that should use @flags.
* @len: number of characters using @flags.
*
* Sets the load flags passed to FreeType when loading glyphs for @len-number
* of characters staring at @start. Flags should be the same as used by the
* client when rendering corresponding FreeType glyphs. The @start and @len
* are input string array indices (i.e. counting bytes in UTF-8 and scaler
* are input string array indices (i.e. counting bytes in UTF-8 and scalar
* values in UTF-32).
*
* This method can be used repeatedly to set different flags for different
* parts of the text. It is the responsibility of the client to make sure that
* flag ranges cover the whole text.
*
* This requires version of HarfBuzz that has hb_ft_font_set_load_flags(), for
* older version the flags will be ignored.
*
* See also raqm_set_freetype_load_flags().
*
* Return value:
@ -1143,7 +1139,7 @@ _raqm_set_spacing (raqm_t *rq,
* raqm_set_letter_spacing_range:
* @rq: a #raqm_t.
* @spacing: amount of spacing in Freetype Font Units (26.6 format).
* @start: index of first character that should use @spacing.
* @start: index of the first character that should use @spacing.
* @len: number of characters using @spacing.
*
* Set the letter spacing or tracking for a given range, the value
@ -1200,12 +1196,12 @@ raqm_set_letter_spacing_range(raqm_t *rq,
* raqm_set_word_spacing_range:
* @rq: a #raqm_t.
* @spacing: amount of spacing in Freetype Font Units (26.6 format).
* @start: index of first character that should use @spacing.
* @start: index of the first character that should use @spacing.
* @len: number of characters using @spacing.
*
* Set the word spacing for a given range. Word spacing will only be applied to
* 'word separator' characters, such as 'space', 'no break space' and
* Ethiopic word separator'.
* 'Ethiopic word separator'.
* The value will be added onto the advance and offset for RTL, and the advance
* for other directions.
*
@ -1239,7 +1235,7 @@ raqm_set_word_spacing_range(raqm_t *rq,
* @rq: a #raqm_t.
* @gid: glyph id to use for invisible glyphs.
*
* Sets the glyph id to be used for invisible glyhphs.
* Sets the glyph id to be used for invisible glyphs.
*
* If @gid is negative, invisible glyphs will be suppressed from the output.
*
@ -1629,6 +1625,11 @@ _raqm_reorder_runs (const FriBidiCharType *types,
}
runs = malloc (sizeof (_raqm_bidi_run) * count);
if (!runs)
{
*run_count = 0;
return NULL;
}
while (run_start < len)
{
@ -2747,10 +2748,10 @@ raqm_version_string (void)
* @minor: Library minor version component.
* @micro: Library micro version component.
*
* Checks if library version is less than or equal the specified version.
* Checks if library version is less than or equal to the specified version.
*
* Return value:
* `true` if library version is less than or equal the specified version,
* `true` if library version is less than or equal to the specified version,
* `false` otherwise.
*
* Since: 0.7
@ -2769,10 +2770,10 @@ raqm_version_atleast (unsigned int major,
* @minor: Library minor version component.
* @micro: Library micro version component.
*
* Checks if library version is less than or equal the specified version.
* Checks if library version is less than or equal to the specified version.
*
* Return value:
* `true` if library version is less than or equal the specified version,
* `true` if library version is less than or equal to the specified version,
* `false` otherwise.
*
* Since: 0.7

View File

@ -48,7 +48,7 @@ extern "C" {
/**
* raqm_t:
*
* This is the main object holding all state of the currently processed text as
* This is the main object holding all the states of the currently processed text as
* well as its output.
*
* Since: 0.1
@ -81,7 +81,7 @@ typedef enum
* @y_advance: the glyph advance width in vertical text.
* @x_offset: the horizontal movement of the glyph from the current point.
* @y_offset: the vertical movement of the glyph from the current point.
* @cluster: the index of original character in input text.
* @cluster: the index of the original character in the input text.
* @ftface: the @FT_Face of the glyph.
*
* The structure that holds information about output glyphs, returned from

View File

@ -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]] = {
"<UserDefines></UserDefines>": "<UserDefines>FT_CONFIG_OPTION_SYSTEM_ZLIB;FT_CONFIG_OPTION_USE_PNG;FT_CONFIG_OPTION_USE_HARFBUZZ;FT_CONFIG_OPTION_USE_BROTLI</UserDefines>", # noqa: E501
"<UserIncludeDirectories></UserIncludeDirectories>": r"<UserIncludeDirectories>{dir_harfbuzz}\src;{inc_dir}</UserIncludeDirectories>", # noqa: E501
"<UserLibraryDirectories></UserLibraryDirectories>": "<UserLibraryDirectories>{lib_dir}</UserLibraryDirectories>", # noqa: E501
"<UserDependencies></UserDependencies>": f"<UserDependencies>zlib.lib;libpng{V['LIBPNG_XY']}.lib;brotlicommon.lib;brotlidec.lib</UserDependencies>", # noqa: E501
"<UserDependencies></UserDependencies>": f"<UserDependencies>zlib.lib;libpng{V['libpng-xy']}.lib;brotlicommon.lib;brotlidec.lib</UserDependencies>", # 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",