Compare commits

...

607 Commits
12.0.x ... 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
Hugo van Kemenade
3c41c09506 12.2.0 version bump 2026-04-01 15:11:14 +03:00
Hugo van Kemenade
cdaa29eb52
Check calloc return value (#9527) 2026-04-01 15:11:00 +03:00
Andrew Murray
585b2f5a78 Check calloc return value 2026-04-01 22:57:56 +11:00
Hugo van Kemenade
ecf011ea15
Check all allocs in the Arrow tree (#9488) 2026-04-01 14:56:15 +03:00
Hugo van Kemenade
cf6de8ca9b
Reject non-numeric elements inside list coords (#9526) 2026-04-01 22:50:45 +11:00
Andrew Murray
ffdcede651
Update 12.2.0 release notes (#9522) 2026-04-01 17:43:36 +11:00
Hugo van Kemenade
7929d7760f
Added security release notes (#149) 2026-04-01 09:02:36 +03:00
Andrew Murray
c4f7aa5dfb Added security release notes 2026-04-01 16:49:20 +11:00
Hugo van Kemenade
22cdb5f2e4
Move variable declaration inside define (#9525) 2026-04-01 06:35:32 +03:00
Hugo van Kemenade
fc15b3b018
Resize tall images vertically first (#9524) 2026-04-01 06:34:26 +03:00
Hugo van Kemenade
44db0708c4
Update xz to 5.8.3 (#9523) 2026-04-01 06:31:15 +03:00
Andrew Murray
58f9a1d166
Avoid overflow by not adding extents together (#9520) 2026-04-01 13:45:30 +11:00
Andrew Murray
459bdf766f Move variable declaration inside define 2026-04-01 10:38:22 +11:00
Andrew Murray
4ef0ac611d Resize tall images vertically first 2026-04-01 10:00:39 +11:00
Andrew Murray
f5e893e46e Seek raises OverFlowError on 32-bit 2026-04-01 09:46:09 +11:00
Hugo van Kemenade
ec8272044d
Use long for glyph position (#9518)
Co-authored-by: Andrew Murray <radarhere@users.noreply.github.com>
2026-04-01 08:52:09 +11:00
Andrew Murray
d9035515f2
Merge branch 'main' into psd_size 2026-04-01 08:42:16 +11:00
Andrew Murray
cf4a8ee0b9 Updated xz to 5.8.3 2026-04-01 08:26:13 +11:00
Hugo van Kemenade
3bf614e4b8
Raise an error if the trailer chain loops back on itself (#9519) 2026-04-01 08:03:15 +11:00
Hugo van Kemenade
3cb854e8b2
Only read as much data from gzip-decompressed data as necessary (#9521) 2026-04-01 08:02:08 +11:00
Hugo van Kemenade
3cb814f338 Update 12.2.0 release notes 2026-03-31 23:15:06 +03:00
Gareth Davidson
2696e962c2
Add loader plugins: AMOS abk, Atari Degas, 40+ more obscure formats via Netpbm (#9482)
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
2026-03-31 23:03:12 +03:00
Hugo van Kemenade
6dfc2be807
Allow None extents in C setimage() (#9504) 2026-03-31 22:02:41 +03:00
Hugo van Kemenade
da0ed929a0
Use critical sections to protect FontObject (#9498) 2026-03-31 21:54:29 +03:00
Hugo van Kemenade
2c2c2a1eae
Add ImageText.Text.wrap() to wrap text (#9286) 2026-03-31 21:49:22 +03:00
Andrew Murray
cc22efda7a Parametrize tests 2026-03-31 21:42:16 +03:00
Andrew Murray
b2a16f0dbe Copy offset check from C into Python 2026-03-31 21:42:16 +03:00
Andrew Murray
591ce38ca5 Skip OverflowError on Windows Python 3.10 2026-03-31 21:42:16 +03:00
Andrew Murray
4bada07dc6 Avoid overflow by not adding extents together 2026-03-31 21:42:16 +03:00
Hugo van Kemenade
d66a77223b
Cleanup .spider extension in the same test where it is added (#9517) 2026-03-31 15:55:30 +03:00
Andrew Murray
09c585dc21 Cleanup .spider extension in the same test where it is added 2026-03-31 22:02:23 +11:00
Andrew Murray
1f74a55be2
Run tests in parallel via tox for 3.5x speedup (#9516) 2026-03-31 21:58:13 +11:00
Hugo van Kemenade
228a85e56e Safer test_file_spider teardown under pytest-xdist 2026-03-31 11:22:11 +03:00
Andrew Murray
751b373d41
Always call StubHandler open() when opening StubImageFile (#9412) 2026-03-31 09:20:47 +11:00
Andrew Murray
f6b50a540d
Improved BCn overflow check (#9043) 2026-03-31 08:05:58 +11:00
Hugo van Kemenade
8d801bcafa
Image will never be None (#9512) 2026-03-30 18:49:06 +03:00
Hugo van Kemenade
40168cca95
Update libjpeg-turbo to 3.1.4.1 (#9507) 2026-03-30 18:47:54 +03:00
Hugo van Kemenade
7406b371ca
Raise EOFError when seeking too far in PSD (#9388) 2026-03-30 18:34:08 +03:00
Hugo van Kemenade
ded95a6c3d
Raise error if ImageGrab subprocess gives non-zero returncode (#9321) 2026-03-30 18:33:05 +03:00
Andrew Murray
73e1ed91e3 For DXT1, only check if 8 bytes are left 2026-03-30 18:23:49 +03:00
Hugo van Kemenade
ea9d4ecf4e
Update Python versions (#9515) 2026-03-30 18:10:36 +03:00
Hugo van Kemenade
f80de2152c Run tests in parallel via tox 2026-03-30 16:34:07 +03:00
Hugo van Kemenade
b2e3f788f9
Allow for different palette entry sizes when correcting BMP pixel data offset (#9472) 2026-03-30 16:06:55 +03:00
Hugo van Kemenade
33e1518cc7
Ignore unspecified extra samples for TIFF separate planar configuration (#9514) 2026-03-30 15:54:41 +03:00
Andrew Murray
a03b7b52f9 Updated Python versions 2026-03-30 22:57:51 +11:00
Andrew Murray
007974d35b Ignore EXTRASAMPLES tag from separate planes image when saving 2026-03-30 20:04:39 +11:00
Andrew Murray
84cb30d7a7 For separate planar configuration, ignore unspecified extra components 2026-03-30 19:42:07 +11:00
Andrew Murray
07c180b21e Simplify SAMPLEFORMAT when all values match for values other than 1 2026-03-30 19:40:04 +11:00
Jeffrey 'Alex' Clark
602acd5828
Jeffrey A. Clark -> Jeffrey 'Alex' Clark (#9513) 2026-03-29 12:42:15 -04:00
Jeffrey 'Alex' Clark
7c121637c9 Jeffrey A. Clark -> Jeffrey 'Alex' Clark
Follow up to 4197263dff. People cannot figure out
my preferred name, hence this final (I hope!) update to my name in Pillow.
2026-03-29 10:05:18 -04:00
Andrew Murray
7ef54f6bfd Image will never be None
Co-authored-by: jorenham <jhammudoglu@gmail.com>
2026-03-29 19:40:16 +11:00
Andrew Murray
f298638632
Merge branch 'main' into arrow_malloc_guard 2026-03-29 19:13:53 +11:00
Andrew Murray
a69b4ec228 Merge branch 'main' into wrap 2026-03-28 22:44:21 +11:00
Andrew Murray
b62ff96779
Add PERF to lint and fix findings (#9510) 2026-03-28 21:56:07 +11:00
Hugo van Kemenade
4b8ae8ede4
Add release notes for #9394 and #9419 (#9467) 2026-03-28 11:31:58 +02:00
Hugo van Kemenade
a9ef0e2922
PERF203 and PERF401 fixes (#148) 2026-03-28 11:30:49 +02:00
Andrew Murray
3121c77cad Added release notes for #9456 2026-03-28 19:19:48 +11:00
Andrew Murray
ccf9863ba8 Added release notes for #9394 2026-03-28 19:11:51 +11:00
Andrew Murray
1ed39726c5 Added release notes for #9419 2026-03-28 19:11:51 +11:00
Andrew Murray
9f3f6de109 Allow None extents in C setimage 2026-03-28 18:31:49 +11:00
Andrew Murray
701b49adc5 PERF401 fix 2026-03-28 15:13:42 +11:00
Andrew Murray
9a7b91e5db PERF203 fixes 2026-03-28 15:13:41 +11:00
Andrew Murray
018801805f Simplify setimage() 2026-03-28 14:08:32 +11:00
Andrew Murray
65c4f4ea8d Updated libjpeg-turbo to 3.1.4 2026-03-28 13:19:27 +11:00
Andrew Murray
9006c305cf
Merge branch 'main' into perflint 2026-03-28 06:51:26 +11:00
Andrew Murray
91a5a09595
Switch iOS back to macos-15-intel (#9509) 2026-03-28 06:49:38 +11:00
Hugo van Kemenade
754c7ea3a0 PERF203 and fixes 2026-03-27 14:18:37 +02:00
Hugo van Kemenade
090ca9461b PERF403 and fixes 2026-03-27 14:18:37 +02:00
Hugo van Kemenade
9a358fa289 PERF402 and fixes 2026-03-27 14:18:37 +02:00
Hugo van Kemenade
b85b8534d7 PERF401 and fixes 2026-03-27 14:18:37 +02:00
Hugo van Kemenade
624fc87d2d PERF102 2026-03-27 14:15:30 +02:00
Hugo van Kemenade
b337b33564 PERF101 2026-03-27 14:15:30 +02:00
Andrew Murray
27de86483d Switch iOS back to macos-15-intel 2026-03-27 21:54:45 +11:00
Andrew Murray
9568bceeb8
Catch struct.error (#9505) 2026-03-27 21:22:28 +11:00
Andrew Murray
396b0a2a39
Check PyCapsule_GetPointer and PyBytes_FromStringAndSize return values (#9508) 2026-03-27 20:40:20 +11:00
Andrew Murray
20a9401971 Check PyBytes_FromStringAndSize return value 2026-03-27 15:26:41 +11:00
Andrew Murray
40400edd62 Check PyCapsule_GetPointer return value 2026-03-27 15:26:25 +11:00
wiredfool
7672b19af4
Fix missing null dereference checks (#9489)
Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com>
2026-03-27 15:23:01 +11:00
Hugo van Kemenade
ef6951d1a5
CI: Retry failed downloads (#9506) 2026-03-27 09:57:43 +11:00
Andrew Murray
f176f5dad6
Update libpng to 1.6.56 (#9499) 2026-03-27 08:57:45 +11:00
Andrew Murray
9b7dccfe32
Use PyModule_AddObjectRef (#9503) 2026-03-27 08:47:58 +11:00
Andrew Murray
92ccedea87
Release reference to encoder on error (#9500) 2026-03-27 08:46:33 +11:00
Andrew Murray
fcecc8c6c4
Fixed AVIF and WEBP dealloc (#9501) 2026-03-27 08:45:40 +11:00
Andrew Murray
d305ee6a25
Check PyType_Ready return values (#9502) 2026-03-27 08:45:02 +11:00
Andrew Murray
da729c832c
Check if PyObject_CallMethod result is NULL (#9494) 2026-03-27 08:43:32 +11:00
Hugo van Kemenade
43e4ebe037
Do not use palette from grayscale or bilevel colorspace when reading JPEG2000 images (#9468) 2026-03-26 15:33:18 +02:00
Hugo van Kemenade
051fb0b995
If TGA v2 extension area specifies no alpha, fill alpha channel (#9478) 2026-03-26 15:32:35 +02:00
Andrew Murray
67c0767b64 If Photoshop blocks are truncated, do not raise struct.error 2026-03-26 23:43:35 +11:00
Andrew Murray
f551ecdc43 If Makernote is truncated, do not raise struct.error 2026-03-26 23:43:35 +11:00
Sam Gross
e4d72b53f5 Use critical sections to protect FontObject
FreeType FT_Face objects are not thread-safe. Use per-object critical
sections to protect FontObject methods that access the underlying FT_Face
in the free-threaded build.

Fixes #9497
2026-03-26 14:42:00 +02:00
Hugo van Kemenade
8e9068e36f
Set image pixels individually on 32-bit Windows (#9492) 2026-03-26 14:41:22 +02:00
Hugo van Kemenade
d4f78128ab
Revert "Skip build 1.4.1 for lint" (#9495) 2026-03-26 07:47:22 +11:00
Hugo van Kemenade
e7f150df7f
Update freetype to 2.14.3 (#9485) 2026-03-25 14:37:14 +02:00
Andrew Murray
5b69607c35
Skip build 1.4.1 for lint (#9491)
Co-authored-by: Andrew Murray <radarhere@users.noreply.github.com>
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
2026-03-25 14:35:27 +02:00
Hugo van Kemenade
2654d73626
Add error messages before returning NULL when encoding (#9493) 2026-03-25 14:25:43 +02:00
Andrew Murray
33d62fc8a1 Added error messages 2026-03-25 23:11:59 +11:00
Andrew Murray
93729a0062 Removed unused code 2026-03-25 23:04:35 +11:00
Hugo van Kemenade
9a89944e73
Fix _getxy refcount leaks (#9487) 2026-03-25 23:00:18 +11:00
Andrew Murray
47386d191c Set image pixels individually on 32-bit Windows 2026-03-25 22:33:37 +11:00
Hugo van Kemenade
3a83d6abc3
Enable colour in CI logs (#9486) 2026-03-25 10:54:16 +11:00
wiredfool
ffd32a861a Check all allocs in the Arrow tree
* handle alloc failure
* Ensure we're calling release so the refcount on the image is
decremented
* Ensure that release array/schema can handle partially allocated
children arrays.
2026-03-24 21:14:16 +00:00
Andrew Murray
f0b5f56e9f
Updated libavif to 1.4.1 (#9479) 2026-03-24 22:34:11 +11:00
Andrew Murray
4e85badfc1 Updated freetype to 2.14.3 2026-03-23 21:23:24 +11:00
Andrew Murray
fc0f65998f
Updated harfbuzz to 13.2.1 (#9461) 2026-03-23 21:21:51 +11:00
Andrew Murray
43bc816e88
Merge branch 'main' into jpeg2000_l 2026-03-21 23:44:44 +11:00
Andrew Murray
0d7f5077a7 If v2 extension area specifies no alpha, fill alpha channel 2026-03-21 23:43:26 +11:00
Hugo van Kemenade
1bb14c4ef5
Fix invalid test font (#9483) 2026-03-21 14:14:00 +02:00
Andrew Murray
4d0089141c Fixed invalid test font 2026-03-21 19:26:55 +11:00
Hugo van Kemenade
a4b0e3ecab
Add Exif tag "FrameRate" (#9470) 2026-03-20 16:20:09 +02:00
Hugo van Kemenade
c0fbe54978
Update Ghostscript to 10.7.0 (#9469) 2026-03-20 16:14:24 +02:00
Andrew Murray
77df8a36c1 Merge branch 'main' into jpeg2000_l 2026-03-21 01:10:35 +11:00
Hugo van Kemenade
a67ce7fba1
Support reading JPEG2000 images with CMYK palettes (#9456) 2026-03-20 16:03:55 +02:00
Andrew Murray
3b1f70da61
Simplify setimage() by always passing extents (#9395) 2026-03-21 01:01:20 +11:00
Hugo van Kemenade
6ab139eaab
If bitmap buffer is empty, do not render anything (#8324) 2026-03-20 15:53:02 +02:00
Hugo van Kemenade
46c529fa69
Simplify TGA test code (#9477) 2026-03-20 15:46:57 +02:00
Andrew Murray
c304186190 Simplified code 2026-03-20 10:02:14 +11:00
Andrew Murray
735d02584b Allow for different palette entry sizes when correcting offset 2026-03-19 10:38:28 +11:00
Andrew Murray
93de6a78d8 Generate test image programmatically 2026-03-19 10:10:06 +11:00
Andrew Murray
98c149f030 Simplified code 2026-03-19 09:26:58 +11:00
Zhiyuan Ouyang
e6bb8626c8 Add a ExifTag "FrameRate" to be supported in PIL.
Reference: https://exiftool.org/TagNames/EXIF.html
2026-03-17 10:30:40 -07:00
Andrew Murray
e34c7bee91 Updated Ghostscript to 10.7.0 2026-03-17 10:56:32 +11:00
Andrew Murray
8442a8541c Support saving images with non-RGB palettes as PNGs 2026-03-16 23:52:43 +11:00
Andrew Murray
6a06285bf8 Support reading JPEG2000 images with CMYK palettes 2026-03-16 23:52:33 +11:00
Andrew Murray
4f5802b6b1 Do not use palette from grayscale or bilevel colorspace 2026-03-16 23:45:22 +11:00
Andrew Murray
29509ffa75 Detect CMYK palette in JPEG2000 images 2026-03-16 20:48:46 +11:00
Andrew Murray
d5d0734169 Add CMYK palettes 2026-03-16 20:48:25 +11:00
Gareth Davidson
3a44ba1c75
Add Amiga Workbench .info loader to 3rd party plugins list (#9459) 2026-03-14 09:42:15 +11:00
Hugo van Kemenade
5e91231ed6
Update tests to check for ValueError when encoding an empty image (#9464) 2026-03-13 16:17:38 +02:00
Andrew Murray
dd042da9c2 Update tests to change for ValueError when encoding an empty image 2026-03-13 06:32:15 +11:00
Hugo van Kemenade
8004234d87
Change to ValueError when encoding an empty image (#9394) 2026-03-12 16:57:01 +02:00
Hugo van Kemenade
c66ab56b4b
Update harfbuzz to 13.0.1 (#9453) 2026-03-10 16:12:57 +02:00
Hugo van Kemenade
27ca696c07
Update libavif to 1.4.0 (#9460) 2026-03-10 16:12:14 +02:00
Andrew Murray
686174b5cc Updated libavif to 1.4.0 2026-03-10 20:26:31 +11:00
Andrew Murray
de2845b19a Revert "Patch libavif for svt-av1 4.0 compatibility"
This reverts commit f86ad8b36d.
2026-03-10 10:18:55 +11:00
Andrew Murray
42a4af5c81
Merge branch 'main' into harfbuzz 2026-03-09 22:41:04 +11:00
Andrew Murray
28524f2069
Update freetype to 2.14.2 (#9449) 2026-03-09 22:39:27 +11:00
Andrew Murray
5450f9d08a Updated harfbuzz to 13.0.1 2026-03-08 07:12:17 +11:00
Andrew Murray
2c87ce2d3d
Add FontFile.to_imagefont() (#9419) 2026-03-07 17:24:43 +11:00
Frank Henigman
abbd515e9b Improve efficiency of FontFile._encode_metrics()
Build up mutable sequences instead of recreating mutable ones.
2026-03-06 22:30:59 -05:00
fjhenigman
97bdfeb4a5
Merge branch 'python-pillow:main' into usepcf 2026-03-06 22:00:52 -05:00
Andrew Murray
c68cc49d8e
Upgrade CI from macos-15-intel to macos-26-intel (#9454) 2026-03-05 22:51:03 +11:00
Hugo van Kemenade
55b0cbc273 Update CI targets docs 2026-03-05 10:01:13 +02:00
Hugo van Kemenade
c27d24bad8
[pre-commit.ci] pre-commit autoupdate (#9450) 2026-03-03 17:04:57 +02:00
Andrew Murray
f7582b8d58
Updated documentation terms
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
2026-03-03 23:04:00 +11:00
Andrew Murray
f7ee26575e
Avoid shadowing built-in
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
2026-03-03 22:57:34 +11:00
Andrew Murray
04470d5151
Removed unused argument
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
2026-03-03 22:51:41 +11:00
Andrew Murray
a8cf13010b
Use native configuration
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
2026-03-03 18:02:49 +11:00
renovate[bot]
0fae74731d
Update actions/download-artifact action to v8 (#9451) 2026-03-03 16:36:24 +11:00
pre-commit-ci[bot]
7fc49a5cf4 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-03-02 17:27:45 +00:00
pre-commit-ci[bot]
0c2dc2047e
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.14.14 → v0.15.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.14...v0.15.4)
- [github.com/PyCQA/bandit: 1.9.3 → 1.9.4](https://github.com/PyCQA/bandit/compare/1.9.3...1.9.4)
- [github.com/pre-commit/mirrors-clang-format: v21.1.8 → v22.1.0](https://github.com/pre-commit/mirrors-clang-format/compare/v21.1.8...v22.1.0)
- [github.com/python-jsonschema/check-jsonschema: 0.36.1 → 0.37.0](https://github.com/python-jsonschema/check-jsonschema/compare/0.36.1...0.37.0)
- [github.com/tox-dev/pyproject-fmt: v2.12.1 → v2.16.2](https://github.com/tox-dev/pyproject-fmt/compare/v2.12.1...v2.16.2)
- [github.com/abravalheri/validate-pyproject: v0.24.1 → v0.25](https://github.com/abravalheri/validate-pyproject/compare/v0.24.1...v0.25)
2026-03-02 17:25:35 +00:00
Hugo van Kemenade
f273619682 Test on macos-26-intel 2026-03-02 15:44:23 +02:00
Hugo van Kemenade
bb54c5020f
Use walrus operator (#9448) 2026-02-28 23:24:04 +02:00
Andrew Murray
26c70950e9 Use walrus operator 2026-02-27 08:13:18 +11:00
Andrew Murray
e96c5a5a53
Updated libpng to 1.6.55 (#9425) 2026-02-24 21:17:12 +11:00
Kadir Can Ozden
2fe7c42148
Only close file handle in ImagePalette.save() if it was opened internally (#9444)
Co-authored-by: Andrew Murray <radarhere@users.noreply.github.com>
2026-02-24 18:01:24 +11:00
Varun Chawla
e50d8a5192
Improve border validation error message wording 2026-02-22 18:50:14 -08:00
Andrew Murray
81e0cf2bc4
Add check-case-conflict hook (#9446) 2026-02-22 15:17:59 +02:00
Kadir Can Ozden
43c12af730
Fix self.decode typo (#9445)
Co-authored-by: Andrew Murray <radarhere@users.noreply.github.com>
2026-02-21 14:23:38 +02:00
Kadir Can Ozden
4777a0b318
Fix BMP RLE delta escape reading from wrong file position (#9443)
Co-authored-by: Andrew Murray <radarhere@users.noreply.github.com>
2026-02-21 14:21:48 +02:00
Andrew Murray
02764a0077
Correct error check when encoding AVIF images (#9442)
Co-authored-by: Andrew Murray <radarhere@users.noreply.github.com>
2026-02-19 14:09:59 +02:00
Andrew Murray
3cd69cb12f
Specify platform when pulling docker image (#9440)
Co-authored-by: Andrew Murray <radarhere@users.noreply.github.com>
2026-02-17 09:57:29 +02:00
Andrew Murray
a5c9eba30a
Fix unexpected error when saving zero dimension images (#9391)
Co-authored-by: Andrew Murray <radarhere@users.noreply.github.com>
2026-02-16 12:57:27 +02:00
Hugo van Kemenade
2c00c6f80e
GHA: Cache libavif and webp builds for Ubuntu (#9437)
Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com>
Co-authored-by: Andrew Murray <radarhere@users.noreply.github.com>
2026-02-16 12:29:42 +02: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
fjhenigman
a18a62cda6
Merge pull request #2 from radarhere/usepcf
Updated documentation
2026-02-13 19:51:49 -05:00
Andrew Murray
3c087bb58b
Merge branch 'main' into wrap 2026-02-14 11:14:42 +11:00
Hugo van Kemenade
d4111967a8
Merge PFM documentation into PPM (#9434) 2026-02-14 00:38:40 +02:00
Andrew Murray
f71d74eec2
Use versionadded
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
2026-02-13 18:29:41 +11:00
Andrew Murray
0ce21f98e7 Updated documentation 2026-02-13 18:06:29 +11:00
fjhenigman
97673f4e70
Merge branch 'python-pillow:main' into usepcf 2026-02-12 13:50:53 -05:00
fjhenigman
57be9dc25b
Merge pull request #1 from radarhere/usepcf
Remove temporary buffer
2026-02-12 13:49:10 -05:00
Hugo van Kemenade
1457c6032a
Use uppercase format ID for PALM (#9435) 2026-02-12 15:13:27 +02:00
Andrew Murray
657d0414f0 Merge PFM into PPM 2026-02-12 21:51:01 +11:00
Andrew Murray
3795a1b916 Use uppercase format id 2026-02-12 21:47:04 +11:00
Hugo van Kemenade
913698b667
Update macOS tested Pillow versions (#9431) 2026-02-11 18:52:47 +02:00
Andrew Murray
27765189c8 Updated macOS tested Pillow versions 2026-02-11 23:51:33 +11:00
Hugo van Kemenade
a15f9c6121
Fix CVE number (#9430) 2026-02-11 22:48:11 +11:00
Andrew Murray
54ba4db542
Fix OOB Write with invalid tile extents (#9427)
Co-authored-by: Eric Soroos <eric-github@soroos.net>
2026-02-11 10:24:50 +11:00
Andrew Murray
723e764826 Improved coverage 2026-02-09 22:20:33 +11:00
Andrew Murray
612e3c24a4 Remove temporary buffer 2026-02-09 22:20:33 +11:00
Andrew Murray
0604d6a2c9 Remove unused argument 2026-02-09 22:20:31 +11:00
Andrew Murray
3e14bea593
Use assert_image_equal_tofile when similarity is zero 2026-02-09 22:18:01 +11:00
Andrew Murray
f78663b806
CI: Disable pip upgrade warning (#9424) 2026-02-09 22:16:01 +11:00
Hugo van Kemenade
657d6ea4b6 CI: Disable pip upgrade warning 2026-02-09 11:07:07 +02:00
Andrew Murray
ea9baaf99f
Merge branch 'main' into usepcf 2026-02-09 07:03:05 +11:00
Hugo van Kemenade
49bc134ee1
Use assert_image_equal* when similarity is zero (#9421) 2026-02-08 14:20:25 +02:00
Hugo van Kemenade
26a188c062
Simplify code in FpxImagePlugin.py (#9423) 2026-02-07 14:36:32 +02:00
Andrew Murray
fd8fa7df79 Simplified code 2026-02-07 11:19:18 +11:00
Andrew Murray
18cab11437 Use assert_image_equal* when similarity is zero 2026-02-06 08:34:13 +11:00
Frank Henigman
a90075a668 Add FontFile.to_imagefont(). 2026-02-04 21:35:06 -05:00
Hugo van Kemenade
2a2638e58f
Update harfbuzz to 12.3.2 (#9402) 2026-02-04 18:34:10 +02:00
Hugo van Kemenade
8eddb86076
Updated zlib-ng to 2.3.3 (#9418) 2026-02-04 18:33:31 +02:00
Andrew Murray
1ac7691fe5 Updated zlib-ng to 2.3.3 2026-02-04 20:39:31 +11:00
Andrew Murray
e108e646da
Updated lcms2 to 2.18 (#9387) 2026-02-04 08:57:34 +11:00
Andrew Murray
62aa42f9da
Update dependency cibuildwheel to v3.3.1 (#9416) 2026-02-03 18:23:26 +11:00
renovate[bot]
508e9c9984
Update dependency cibuildwheel to v3.3.1 2026-02-03 01:32:07 +00:00
Hugo van Kemenade
095cdb3c4a
[pre-commit.ci] pre-commit autoupdate (#9415) 2026-02-02 21:46:00 +01:00
pre-commit-ci[bot]
7cbe8c4924 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-02-02 17:17:55 +00:00
pre-commit-ci[bot]
27924be4fd
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.14.10 → v0.14.14](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.10...v0.14.14)
- [github.com/psf/black-pre-commit-mirror: 25.12.0 → 26.1.0](https://github.com/psf/black-pre-commit-mirror/compare/25.12.0...26.1.0)
- [github.com/PyCQA/bandit: 1.9.2 → 1.9.3](https://github.com/PyCQA/bandit/compare/1.9.2...1.9.3)
- [github.com/Lucas-C/pre-commit-hooks: v1.5.5 → v1.5.6](https://github.com/Lucas-C/pre-commit-hooks/compare/v1.5.5...v1.5.6)
- [github.com/python-jsonschema/check-jsonschema: 0.36.0 → 0.36.1](https://github.com/python-jsonschema/check-jsonschema/compare/0.36.0...0.36.1)
- [github.com/zizmorcore/zizmor-pre-commit: v1.19.0 → v1.22.0](https://github.com/zizmorcore/zizmor-pre-commit/compare/v1.19.0...v1.22.0)
- [github.com/tox-dev/pyproject-fmt: v2.11.1 → v2.12.1](https://github.com/tox-dev/pyproject-fmt/compare/v2.11.1...v2.12.1)
2026-02-02 17:17:12 +00:00
Andrew Murray
fc4dbc3810
Remove unnecessary code in WmfHandler (#9411)
Co-authored-by: Andrew Murray <radarhere@users.noreply.github.com>
2026-01-30 17:41:44 +02:00
Andrew Murray
799564dd52 Always call StubHandler open() when opening StubImageFile 2026-01-30 23:26:45 +11:00
Andrew Murray
0e8bb72a66
Patch libavif for svt-av1 4.0 compatibility (#9413) 2026-01-30 23:25:42 +11:00
Hugo van Kemenade
f86ad8b36d Patch libavif for svt-av1 4.0 compatibility 2026-01-29 23:26:20 +01:00
Andrew Murray
29ff5fcb55
Use monkeypatch (#9406)
Co-authored-by: Andrew Murray <radarhere@users.noreply.github.com>
2026-01-27 23:43:14 +02:00
Andrew Murray
6a5c588c5f
Fix docstring typo (#9407) 2026-01-27 08:58:11 +11:00
Hugo van Kemenade
a293273b31 Fix docstring typo 2026-01-26 16:10:37 +02:00
Andrew Murray
6564325e43
Encode using latin-1 in PSDraw text() to match the latin-1 specification in setfont() (#9403) 2026-01-26 13:25:27 +11:00
Andrew Murray
93c8a60784
Lazy import only required plugin: open 2.3-15.6x & save 2.2-9x faster (#9398) 2026-01-26 13:25:14 +11:00
Andrew Murray
b6178303a1
Improve error message (#9392)
Co-authored-by: Andrew Murray <radarhere@users.noreply.github.com>
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
2026-01-25 23:01:02 +02:00
Hugo van Kemenade
d568c8d9e3
Check ext is not empty during save (#145) 2026-01-25 14:46:11 +02:00
Andrew Murray
d08d7ee99e Check ext is not empty during save 2026-01-25 22:55:19 +11:00
Hugo van Kemenade
2b186fceb8 Use __spec__.parent instead of calculating each time 2026-01-24 23:02:39 +02:00
Hugo van Kemenade
c036185514
Ensure lower before checking if ext in EXTENSION
Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com>
2026-01-24 22:48:41 +02:00
Andrew Murray
d737687fc3 Updated harfbuzz to 12.3.2 2026-01-25 06:45:13 +11:00
Hugo van Kemenade
34814d8d2f
Improve wording
Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com>
2026-01-24 12:52:49 +02:00
Hugo van Kemenade
3968886cf6
format overrides file extension when saving (#144) 2026-01-24 11:08:37 +02:00
Andrew Murray
bc64ccbf28
Updated libpng to 1.6.54 (#9397) 2026-01-24 12:16:14 +11:00
Andrew Murray
76d3116ef0 Added logger messages to match init() 2026-01-24 09:44:31 +11:00
Andrew Murray
a6b36f0b6b format overrides file extension when saving 2026-01-24 09:44:31 +11:00
Andrew Murray
a0f51493ca Refer to lazy importing, as lazy loading of images is separate 2026-01-24 09:44:31 +11:00
Steve Dougherty
a6a701c4db
Match PSDraw text() encoding to the latin-1 specification in setfont()
Without this, characters that are in latin-1 but reflected differently in UTF-8 will not be properly rendered. For example,"ó" becomes "ó".
2026-01-21 06:01:00 -05:00
Hugo van Kemenade
e08f910db4
Improve PaletteFile coverage (#9396) 2026-01-20 13:04:44 +02:00
Hugo van Kemenade
d1974d76f7
Updated MinGW Python version (#9400) 2026-01-20 10:56:17 +02:00
Andrew Murray
5ea2d3a056 Updated MinGW Python version 2026-01-20 18:16:34 +11:00
Hugo van Kemenade
d23a899f23
Link to m from _imagingmath, except on Windows (#9393) 2026-01-19 12:21:29 +02:00
Hugo van Kemenade
096c479cfb
If plugin has already been imported and registered the extension, return early
Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com>
2026-01-19 11:28:42 +02:00
Hugo van Kemenade
7f38f980dd
Check that _EXTENSION_PLUGIN contains all registered extensions (#143) 2026-01-19 11:21:00 +02:00
Andrew Murray
b06118c2b3 Do not register empty extension 2026-01-19 17:24:28 +11:00
Andrew Murray
9c8059fdea Cleanup .spider extension registered by test code during save 2026-01-19 17:18:30 +11:00
Andrew Murray
1baf141146 Check that _EXTENSION_PLUGIN contains all registered extensions 2026-01-19 17:13:43 +11:00
Hugo van Kemenade
6b9de40533 Lazy import only required plugin 2026-01-18 22:59:28 +02:00
Andrew Murray
ef8ff756fa Updated libpng to 1.6.54 2026-01-15 12:10:01 +11:00
Andrew Murray
2e9d54887b Improved coverage 2026-01-14 19:42:18 +11:00
Andrew Murray
7e208ccf9d Change to ValueError when encoding an empty image 2026-01-13 23:49:10 +11:00
Andrew Murray
0f4becea73 Link to m from _imagingmath, except on Windows 2026-01-13 16:26:08 +11:00
Hugo van Kemenade
e2b87a0420
Fix joining rounded rectangle corners (#9384) 2026-01-12 12:21:06 +02:00
Andrew Murray
400ffbc18d Raise EOFError when seeking too far 2026-01-10 14:37:18 +11:00
Andrew Murray
d7dfeeb7ad Updated lcms2 to 2.18 2026-01-10 06:46:04 +11:00
Andrew Murray
426ad8307d Fix joining rounded rectangle corners 2026-01-08 19:27:19 +11:00
Hugo van Kemenade
627d8743b7
Simplify test code (#9382) 2026-01-06 14:21:49 +02:00
Andrew Murray
dcd52ebf65 Simplified code 2026-01-06 09:56:56 +11:00
Andrew Murray
d6e0a8d174
[pre-commit.ci] pre-commit autoupdate (#9381) 2026-01-06 09:33:57 +11:00
pre-commit-ci[bot]
2210714a43
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.14.7 → v0.14.10](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.7...v0.14.10)
- [github.com/psf/black-pre-commit-mirror: 25.11.0 → 25.12.0](https://github.com/psf/black-pre-commit-mirror/compare/25.11.0...25.12.0)
- [github.com/pre-commit/mirrors-clang-format: v21.1.6 → v21.1.8](https://github.com/pre-commit/mirrors-clang-format/compare/v21.1.6...v21.1.8)
- [github.com/python-jsonschema/check-jsonschema: 0.35.0 → 0.36.0](https://github.com/python-jsonschema/check-jsonschema/compare/0.35.0...0.36.0)
- [github.com/zizmorcore/zizmor-pre-commit: v1.18.0 → v1.19.0](https://github.com/zizmorcore/zizmor-pre-commit/compare/v1.18.0...v1.19.0)
- [github.com/tox-dev/tox-ini-fmt: 1.7.0 → 1.7.1](https://github.com/tox-dev/tox-ini-fmt/compare/1.7.0...1.7.1)
2026-01-05 17:20:09 +00:00
Hugo van Kemenade
3d7801417a
Move from deprecated getdata to get_flattened_data (#9373) 2026-01-04 15:36:32 +02:00
Andrew Murray
a85d3b135d
Only update Python palette when loading an image if rawmode was different (#9309) 2026-01-04 06:20:56 +11:00
Andrew Murray
932aa68d2a
Add seven-day cooldown to Renovate (#9380) 2026-01-04 05:22:19 +11:00
Hugo van Kemenade
fe236d77a5 Add seven-day cooldown to Renovate 2026-01-03 11:32:19 +02:00
Andrew Murray
bc0e2c0e61
Remove add-imaging-libs option from setup.py (#9378)
Co-authored-by: Alexander Karpinsky <homm86@gmail.com>
2026-01-03 20:18:57 +11:00
Hugo van Kemenade
e66dd607f0
Update xorgproto to 2025.1 (#9379) 2026-01-03 10:56:55 +02:00
Hugo van Kemenade
d5d8a91597
Replace shell: cmd with shell: bash (#9359) 2026-01-03 10:12:48 +02:00
Andrew Murray
b8351fde41
Added type hints to map_metadata_keys() (#9337) 2026-01-03 17:08:17 +11:00
Andrew Murray
36cf82ae76 Updated xorgproto to 2025.1 2026-01-03 16:25:37 +11:00
renovate[bot]
525842215f
Update dependency mypy to v1.19.1 (#9374) 2026-01-03 13:59:38 +11:00
renovate[bot]
844b10f894
Update github-actions (#9375) 2026-01-03 13:55:50 +11:00
Andrew Murray
555fb8371c Move from deprecated getdata to get_flattened_data 2026-01-03 08:16:37 +11:00
Hugo van Kemenade
0a1d6c3c61
Remove Sphinx dependency from mypy (#9370) 2026-01-02 18:30:53 +02:00
mergify[bot]
00ec73dfd1
Fix unclosed file warning (#9371) 2026-01-02 12:33:25 +00:00
Andrew Murray
e924cfd181 Fix unclosed file warning 2026-01-02 21:32:22 +11:00
Hugo van Kemenade
2360d0df17 Revert "Use minimum supported Python version for Lint (#9364)"
This reverts commit 900636e7db.
2026-01-02 12:31:22 +02:00
Hugo van Kemenade
499b796556 Remove Sphinx dependency from mypy 2026-01-02 12:30:14 +02:00
Andrew Murray
1918c6811d Merge branch 'main' into wrap 2026-01-02 20:44:12 +11:00
Andrew Murray
5b677ca1c6 Assert palette is not None 2026-01-02 20:31:47 +11:00
Andrew Murray
b71109d435
Merge branch 'main' into load_palette 2026-01-02 20:21:23 +11:00
Andrew Murray
4337139f0c 12.2.0.dev0 version bump 2026-01-02 20:16:49 +11:00
Andrew Murray
46f45f674d 12.1.0 version bump 2026-01-02 17:03:05 +11:00
Hugo van Kemenade
c9ac097edb
Simplify band splitting (#9291) 2026-01-02 07:42:46 +02:00
Andrew Murray
3baedf2648
Deprecate getdata(), in favour of new get_flattened_data() (#9292) 2026-01-02 10:59:56 +11:00
Hugo van Kemenade
b51a036685
Specify APNG duration type when opening (#9368) 2026-01-01 23:28:16 +02:00
Hugo van Kemenade
8d08e31533
Add release notes for #9348 (#9369) 2026-01-01 20:24:18 +02:00
Andrew Murray
432707ea81 Added release notes for #9348 2026-01-02 04:18:15 +11:00
Andrew Murray
2d589107fb Specify APNG duration type when opening 2026-01-02 03:49:56 +11:00
Hugo van Kemenade
8dee8dd5ba
Add ImageFile context manager (#9367) 2026-01-01 15:50:26 +02:00
Hugo van Kemenade
b2d9bc3c76
Support saving APNG float durations (#9365) 2026-01-01 15:49:03 +02:00
Hugo van Kemenade
f130c10a9c
Allow 1 mode images in MorphOp (#9348) 2026-01-01 15:30:47 +02:00
Andrew Murray
ce11a0c499 Added ImageFile context manager 2026-01-01 20:31:22 +11:00
Andrew Murray
51b35d17e1 Added fp type hint 2026-01-01 20:31:22 +11:00
Andrew Murray
a868c29eb1
Assert fp is not None (#8617) 2026-01-01 20:01:38 +11:00
Andrew Murray
43f8efad79
Added release notes for #9350 (#9366)
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
2026-01-01 18:41:34 +11:00
Andrew Murray
91f219fdcf Support saving float durations 2026-01-01 17:32:59 +11:00
Andrew Murray
900636e7db
Use minimum supported Python version for Lint (#9364) 2026-01-01 17:31:36 +11:00
Andrew Murray
d62955031b
Allow for duplicate font variation styles (#9362)
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
2026-01-01 08:53:04 +11:00
Andrew Murray
2ebfe30ae3
Added return type to ImageFile _close_fp() (#9356) 2025-12-31 14:47:50 +02:00
Andrew Murray
19910ed03e
Call parent verify method (#9357) 2025-12-31 14:47:33 +02:00
Andrew Murray
6b892c495c Merge branch 'main' into imagemorph_get_on_pixels 2025-12-31 23:10:34 +11:00
Andrew Murray
0a9a47fb9b
Update ImageMorph documentation (#9349)
Co-authored-by: Andrew Murray <radarhere@users.noreply.github.com>
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
2025-12-31 14:02:31 +02:00
Andrew Murray
15c9d11f35
Replace pre-commit with prek (#9360) 2025-12-31 14:32:39 +11:00
Hugo van Kemenade
81e80f7a50 Install and run tox/lint/mypy via uv 2025-12-29 19:35:41 +02:00
Hugo van Kemenade
72931475f2 Replace shell: cmd with shell: bash 2025-12-29 14:57:25 +02:00
Andrew Murray
79357a2718 Revert "Disable https://docs.zizmor.sh/audits/#obfuscation"
This reverts commit 9342e209b2.
2025-12-29 14:44:12 +02:00
Andrew Murray
3abb62ed29 Do not use cmd shell 2025-12-29 14:44:03 +02:00
Hugo van Kemenade
080afe1bf7 Replace pre-commit with prek 2025-12-28 23:46:02 +02:00
Andrew Murray
2ebb3e9964
Use different variables for Image and ImageFile instances (#9316) 2025-12-28 23:09:46 +02:00
Andrew Murray
a04c9806b1
Return LUT from LutBuilder build_default_lut() (#9350) 2025-12-28 23:03:47 +02:00
Andrew Murray
faa843e9c2
Simplify WebP code (#9329) 2025-12-28 23:01:23 +02:00
Andrew Murray
66e3d65a72
Update harfbuzz to 12.3.0 (#9355) 2025-12-28 17:04:44 +02:00
Andrew Murray
4be5b8a2fb
Use unsigned long for DWORD (#9352) 2025-12-28 07:34:57 +11:00
Andrew Murray
e85700fe48
Test PyQt6 on Python 3.14 on Windows (#9353) 2025-12-27 15:54:10 +02:00
Andrew Murray
a704711404 Allow 1 mode images in apply() and match() 2025-12-23 14:13:51 +11:00
Andrew Murray
9b7200d2b4 Allow 1 mode images in MorphOp get_on_pixels() 2025-12-23 12:50:26 +11:00
Andrew Murray
ca21683316
Cast to UINT32 before shifting bits (#9347) 2025-12-22 18:12:10 +11:00
Hugo van Kemenade
4cbef1667f
Revert "Pin docutils to 0.21 (#9344)" (#9346) 2025-12-22 08:09:20 +02:00
Andrew Murray
9dd756f9fe Revert "Pin docutils to 0.21 (#9344)"
This reverts commit 6df6cd4480.
2025-12-22 09:34:03 +11:00
Hugo van Kemenade
00e2198eeb
Test 32-bit Windows on Windows Server 2022 (#9345) 2025-12-21 23:36:12 +02:00
Andrew Murray
9d3555c37e Test Windows Server 2022 2025-12-21 22:39:19 +11:00
Andrew Murray
6df6cd4480
Pin docutils to 0.21 (#9344) 2025-12-20 23:51:05 +11:00
Hugo van Kemenade
205e52b1ee
Update xz to 5.8.2 (#9343) 2025-12-18 00:43:51 +02:00
Andrew Murray
6bf4313a68 Updated xz to 5.8.2 2025-12-18 07:44:40 +11:00
Hugo van Kemenade
8494b06c71
Correct variable type (#9335) 2025-12-11 18:38:35 +02:00
Andrew Murray
6a769da21b Corrected variable type 2025-12-11 23:27:29 +11:00
Andrew Murray
2c6fd36f10
Docs: update major bump cadence (#9334) 2025-12-11 20:02:54 +11:00
Hugo van Kemenade
c0b8c2f0a2
Updated libjpeg-turbo to 3.1.3 (#9333) 2025-12-11 09:47:37 +02:00
Hugo van Kemenade
79ae888d45 Docs: update major bump cadence 2025-12-11 09:29:54 +02:00
Andrew Murray
11d599c798 Added documentation 2025-12-11 18:20:58 +11:00
Andrew Murray
b3da65df94 Updated libjpeg-turbo to 3.1.3 2025-12-11 10:53:04 +11:00
Hugo van Kemenade
1f424efd25
Updated zlib-ng to 2.3.2 (#9324) 2025-12-10 23:13:28 +02:00
Andrew Murray
4b2d4811e1 Added scaling argument to wrap() 2025-12-11 07:51:12 +11:00
Andrew Murray
16691657cc Added height argument to wrap() 2025-12-11 07:51:11 +11:00
Andrew Murray
9ac4edc54b Added wrap() 2025-12-11 07:51:11 +11:00
Andrew Murray
374957cefd
Fix ResourceWarnings in selftest.py (#9332) 2025-12-11 07:45:47 +11:00
Hugo van Kemenade
6d493aa817
Fix testing good P mode BMP images (#9319) 2025-12-10 22:04:23 +02:00
Hugo van Kemenade
33204aac4d
Add release notes for #9070 (#9320) 2025-12-10 21:59:36 +02:00
Hugo van Kemenade
4eb7cd6f29
Improve type hints (#9317) 2025-12-10 21:58:50 +02:00
Hugo van Kemenade
76532808f4 Fix ResourceWarning in selftest.py 2025-12-10 15:26:14 +02:00
Andrew Murray
3332c1d82e
Updated libpng to 1.6.53 (#9325) 2025-12-10 18:23:16 +11:00
Andrew Murray
b3d7263f74
Test Python 3.15 pre-release (#9331) 2025-12-07 22:28:13 +11:00
Hugo van Kemenade
a01fa7d08e Test Python 3.15 pre-release 2025-12-07 11:12:38 +02:00
Andrew Murray
db7a994ad6 Updated libpng to 1.6.53 2025-12-06 10:15:33 +11:00
Andrew Murray
07fee96880
[pre-commit.ci] pre-commit autoupdate (#9318) 2025-12-05 20:53:18 +11:00
Andrew Murray
fd1ddd6d56 Use consistent type 2025-12-03 22:46:42 +11:00
Andrew Murray
7c3ece07c9 Changed type so that im has fp attribute 2025-12-03 22:46:42 +11:00
Andrew Murray
61b1c3c841 Do not change variable type 2025-12-03 22:46:42 +11:00
Andrew Murray
46ac30aa80 Use different variables for Image and ImageFile instances 2025-12-03 22:46:42 +11:00
Andrew Murray
4024f0287d Assert image type 2025-12-03 22:46:42 +11:00
Andrew Murray
4d511d86ed Changed argument type to match use 2025-12-03 22:46:42 +11:00
Andrew Murray
fd3d44d2ef Updated zlib-ng to 2.3.2 2025-12-03 22:38:32 +11:00
Andrew Murray
24b1702360
Update actions/checkout action to v6 (#9323) 2025-12-03 20:09:44 +11:00
mergify[bot]
ae45187719
Update dependency mypy to v1.19.0 (#9322) 2025-12-03 08:47:56 +00:00
renovate[bot]
b633f49b9c
Update actions/checkout action to v6 2025-12-03 07:14:19 +00:00
renovate[bot]
7adecb792c
Update dependency mypy to v1.19.0 2025-12-03 07:14:12 +00:00
Andrew Murray
b428f7209f Open a macOS window on CI 2025-12-02 23:53:45 +11:00
Andrew Murray
04ee0cc3b1 Raise error if subprocess gives non-zero returncode 2025-12-02 23:06:18 +11:00
Andrew Murray
dbc5a4fc90 Added release notes for #9070 2025-12-02 22:26:23 +11:00
Andrew Murray
b3d9ba8e88 Removed unused files 2025-12-02 10:42:14 +11:00
Andrew Murray
47c6aae0ca Fixed testing good P mode BMP images 2025-12-02 10:40:43 +11:00
Andrew Murray
9342e209b2 Disable https://docs.zizmor.sh/audits/#obfuscation 2025-12-02 10:21:55 +11:00
pre-commit-ci[bot]
ce3e085751
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.14.3 → v0.14.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.3...v0.14.7)
- [github.com/psf/black-pre-commit-mirror: 25.9.0 → 25.11.0](https://github.com/psf/black-pre-commit-mirror/compare/25.9.0...25.11.0)
- [github.com/PyCQA/bandit: 1.8.6 → 1.9.2](https://github.com/PyCQA/bandit/compare/1.8.6...1.9.2)
- [github.com/pre-commit/mirrors-clang-format: v21.1.2 → v21.1.6](https://github.com/pre-commit/mirrors-clang-format/compare/v21.1.2...v21.1.6)
- [github.com/python-jsonschema/check-jsonschema: 0.34.1 → 0.35.0](https://github.com/python-jsonschema/check-jsonschema/compare/0.34.1...0.35.0)
- [github.com/zizmorcore/zizmor-pre-commit: v1.16.2 → v1.18.0](https://github.com/zizmorcore/zizmor-pre-commit/compare/v1.16.2...v1.18.0)
- [github.com/sphinx-contrib/sphinx-lint: v1.0.1 → v1.0.2](https://github.com/sphinx-contrib/sphinx-lint/compare/v1.0.1...v1.0.2)
- [github.com/tox-dev/pyproject-fmt: v2.11.0 → v2.11.1](https://github.com/tox-dev/pyproject-fmt/compare/v2.11.0...v2.11.1)
2025-12-01 17:26:21 +00:00
Andrew Murray
b0a5bc2a6b
Allow window ID to be passed to ImageGrab.grab() on macOS (#9070) 2025-12-01 20:42:09 +11:00
Andrew Murray
370da461cf
Updated libpng to 1.6.51 (#9305)
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
2025-12-01 10:02:09 +11:00
Hugo van Kemenade
77e16b1030
Apply encoder options when saving multiple PNG frames (#9300) 2025-11-30 22:21:42 +02:00
Hugo van Kemenade
2150f088ed
Read all non-zero transparency from mode 1 PNG images as 255 (#9282) 2025-11-30 22:20:38 +02:00
Hugo van Kemenade
972bb4c39b
Test ImageFont.ImageFont, in case freetype2 is not supported (#9287) 2025-11-30 22:20:15 +02:00
Hugo van Kemenade
c9095cb02a
Support writing IFD, SIGNED_RATIONAL and InkNames TIFF tags (#9276) 2025-11-30 22:14:18 +02:00
Hugo van Kemenade
416f02338b
Remove unused modes (#9275) 2025-11-30 22:12:22 +02:00
Hugo van Kemenade
89795df94f
Use different variables for Image and ImageFile instances (#9268) 2025-11-30 22:11:22 +02:00
Hugo van Kemenade
93aa55cece
Updated brotli to 1.2.0 (#9284) 2025-11-30 22:08:30 +02:00
Hugo van Kemenade
8d7dc9db5b
Update libimagequant to 4.4.1 (#9301) 2025-11-30 22:07:42 +02:00
Hugo van Kemenade
4a733e5092
Correct allocating new color to RGBA palette (#9313) 2025-11-30 21:57:22 +02:00
Hugo van Kemenade
da76f6d99b
Close image on ImageFont exception (#9304) 2025-11-30 21:56:35 +02:00
Hugo van Kemenade
65c32ecca4
retina -> Retina 2025-11-30 21:55:59 +02:00
Hugo van Kemenade
5543e85ad2
Update zlib-ng to 2.3.1, except on manylinux2014 aarch64 (#9312) 2025-11-29 11:13:07 +02:00
Andrew Murray
37da2ba381 Corrected allocating new color to RGBA palette 2025-11-29 17:22:44 +11:00
Andrew Murray
8814d42fd9 Update zlib-ng to 2.3.1, except on manylinux2014 aarch64 2025-11-29 14:24:43 +11:00
Andrew Murray
d06c8b3591 Test drawing a new color onto a dirty palette 2025-11-27 13:12:42 +11:00
Andrew Murray
6a9960e8c1 Only update Python palette if rawmode was different to the mode 2025-11-25 23:40:34 +11:00
Hugo van Kemenade
ec40c546d7
Updated Ubuntu version (#9306) 2025-11-22 13:40:52 +02:00
Andrew Murray
7055937eb1 Updated Ubuntu version 2025-11-22 17:47:09 +11:00
Andrew Murray
cce73b1e89 Close image on ImageFont exception 2025-11-19 21:52:21 +11:00
Andrew Murray
88247a9ef3
Updated version 2025-11-19 21:31:27 +11:00
Andrew Murray
71b3e5c015
Updated harfbuzz to 12.2.0 (#9289) 2025-11-18 18:00:44 +11:00
Andrew Murray
75280b8b0f
Merge branch 'main' into brotli 2025-11-15 19:59:23 +11:00
Andrew Murray
6107b9e82d Update libimagequant to 4.4.1 2025-11-15 07:41:59 +11:00
Hugo van Kemenade
38c6c478e0
Reapply "Use macos-latest for iOS arm64 simulator" (#9259) 2025-11-14 14:17:40 +02:00
Andrew Murray
bc1237ef3d Update dependency cibuildwheel to v3.3.0 2025-11-14 21:22:58 +11:00
Andrew Murray
142c1320b2 Apply encoder options when saving multiple PNG frames 2025-11-14 20:08:49 +11:00
Andrew Murray
3b8fd040de
Escape period in pre-commit-config (#9036) 2025-11-10 22:35:49 +11:00
Andrew Murray
8fbb801275
Add Apache-2.0 notice to IcoImagePlugin (#8947) 2025-11-10 21:56:10 +11:00
Andrew Murray
921a470506 Simplified code 2025-11-06 18:24:10 +11:00
Hugo van Kemenade
b33a31524a
Add Fedora 43 (#9290) 2025-11-05 19:24:43 +02:00
Andrew Murray
18c7f87fe3 Added Fedora 43 2025-11-05 23:21:46 +11:00
Andrew Murray
e44ce2f00e Updated harfbuzz to 12.2.0 2025-11-05 19:09:49 +11:00
Andrew Murray
b59e031256
[pre-commit.ci] pre-commit autoupdate (#9288) 2025-11-04 08:03:42 +11:00
Hugo van Kemenade
666dd52478 Drop removed rule 2025-11-03 19:56:21 +02:00
pre-commit-ci[bot]
85d783fb52
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.13.3 → v0.14.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.13.3...v0.14.3)
- [github.com/python-jsonschema/check-jsonschema: 0.34.0 → 0.34.1](https://github.com/python-jsonschema/check-jsonschema/compare/0.34.0...0.34.1)
- [github.com/zizmorcore/zizmor-pre-commit: v1.14.2 → v1.16.2](https://github.com/zizmorcore/zizmor-pre-commit/compare/v1.14.2...v1.16.2)
- [github.com/sphinx-contrib/sphinx-lint: v1.0.0 → v1.0.1](https://github.com/sphinx-contrib/sphinx-lint/compare/v1.0.0...v1.0.1)
- [github.com/tox-dev/pyproject-fmt: v2.7.0 → v2.11.0](https://github.com/tox-dev/pyproject-fmt/compare/v2.7.0...v2.11.0)
- [github.com/tox-dev/tox-ini-fmt: 1.6.0 → 1.7.0](https://github.com/tox-dev/tox-ini-fmt/compare/1.6.0...1.7.0)
2025-11-03 17:20:17 +00:00
Andrew Murray
b3d9bd9950 Test ImageFont.ImageFont, in case freetype2 is not supported 2025-11-03 23:07:15 +11:00
Andrew Murray
1a27f958d7 Updated brotli to 1.2.0 2025-10-31 18:19:05 +11:00
Andrew Murray
dfd24ba615 Read all non-zero transparency from mode 1 images in the same way 2025-10-30 22:03:39 +11:00
Hugo van Kemenade
e36e67081a
Simplify code now that I;16* modes are the only IMAGING_TYPE_SPECIAL (#9263) 2025-10-27 15:19:54 +02:00
Hugo van Kemenade
b90a00eccb
Remove BytesIO from DdsImagePlugin (#9273) 2025-10-27 15:16:27 +02:00
Hugo van Kemenade
ce3323afa9
Update github-actions (#9277) 2025-10-27 07:23:50 +02:00
renovate[bot]
29c5ffe745
Update github-actions 2025-10-27 02:48:10 +00:00
Hugo van Kemenade
cc4ca5bf17
Added type hints (#9269) 2025-10-24 16:56:36 +03:00
Hugo van Kemenade
148a19eee4
Fix ZeroDivisionError in DdsImagePlugin (#9272) 2025-10-24 16:44:46 +03:00
Hugo van Kemenade
a63ba0e3b6
Correct __getitem__ return type (#9264) 2025-10-24 16:42:14 +03:00
Andrew Murray
82cdaa456c Support writing SIGNED_RATIONAL tag types 2025-10-24 03:55:45 +11:00
Andrew Murray
ddd4f00720 Support writing IFD tag types 2025-10-23 20:03:14 +11:00
Andrew Murray
b04d8792f5 Support writing InkNames 2025-10-23 08:53:00 +11:00
Andrew Murray
109ee1569d Removed I;32L rawmode 2025-10-22 22:24:15 +11:00
Andrew Murray
208bbe95f9 Remove I;32L and I;32B modes 2025-10-22 22:22:00 +11:00
Andrew Murray
b1e2f2e652 Improved coverage 2025-10-22 20:08:22 +11:00
Andrew Murray
7d6f2ce90b Removed BytesIO 2025-10-21 23:35:17 +11:00
Andrew Murray
e1f4352ce9 Fixed ZeroDivisionError 2025-10-21 23:11:18 +11:00
Andrew Murray
e90bb1559c Rearranged code 2025-10-20 21:00:29 +11:00
Andrew Murray
4b90888a7d Added type hints 2025-10-20 19:38:29 +11:00
Andrew Murray
51e3fe45bf Use different variables for Image and ImageFile instances 2025-10-20 19:18:00 +11:00
Hugo van Kemenade
76f04b46c5
Fix warnings (#9257) 2025-10-17 21:18:10 +03:00
Hugo van Kemenade
2d23257595
Update macOS tested Pillow versions (#9265) 2025-10-17 15:29:08 +03:00
Andrew Murray
03d48f4011 Updated macOS tested Pillow versions 2025-10-17 23:05:33 +11:00
Andrew Murray
e969fa7aea Correct __getitem__ return type 2025-10-17 06:14:02 +11:00
Andrew Murray
ae43b36030 Simplified code now that I;16* modes are the only IMAGING_TYPE_SPECIAL 2025-10-16 20:55:56 +11:00
Hugo van Kemenade
5122c8356d
Remove Fedora 41 (#9260) 2025-10-16 07:19:52 +03:00
Yan-Ke Guo
424168b69c
Merge branch 'main' into main 2025-10-16 09:46:04 +08:00
Andrew Murray
ae7d28eddb Removed Fedora 41 2025-10-16 12:03:13 +11:00
Andrew Murray
933df2450d Reapply "Use macos-latest for iOS arm64 simulator"
This reverts commit 592b2f820a.
2025-10-16 07:21:15 +11:00
Hugo van Kemenade
3620d48459 12.1.0.dev0 version bump 2025-10-15 21:28:16 +03:00
Andrew Murray
c680ff029f
Merge branch 'main' into main 2025-10-15 22:31:20 +11:00
Andrew Murray
7d89946688 Removed duplicate library 2025-10-15 22:21:51 +11:00
Andrew Murray
3eecafd62c Fixed warning 2025-10-15 22:19:38 +11:00
Andrew Murray
ca3528f46e
Document that macOS window value is a CGWindowID 2025-09-16 21:43:24 +10:00
Andrew Murray
610d564aea
Merge branch 'main' into main 2025-09-16 19:43:34 +10:00
Andrew Murray
53302c2281
Split versionadded info
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
2025-09-16 19:43:03 +10:00
Andrew Murray
79914ec8a5 Check for scaling in macOS windows 2025-07-13 15:11:23 +08:00
Andrew Murray
7eaac3fcf0 Updated documentation 2025-07-13 15:11:23 +08:00
Yan-Ke Guo
1f7e9c3b51 Apply suggestions from code review
Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com>
2025-07-13 15:11:23 +08:00
GUO YANKE
5ce88dbe53 feat(ImageGrab): enhance grab function to support window-based screenshot capturing on macOS 2025-07-13 15:11:23 +08:00
Andrew Murray
49efe40f28 Escape period 2025-06-30 22:19:14 +10:00
Andrew Murray
9f0614de7e
Merge branch 'main' into patch-1 2025-05-24 10:34:10 +10:00
Stefan
a666057989
HTTP -> HTTPS
Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com>
2025-05-21 15:40:16 +02:00
Stefan
349cc44fd4
Add Apache-2.0 notice to IcoImagePlugin
Closes #8946.
2025-05-07 17:21:22 +02:00
Andrew Murray
94c3ee6944
Merge branch 'main' into bitmap_buffer 2024-09-30 19:47:11 +10:00
Andrew Murray
f9f7ba4ce9 Do not raise error if bitmap buffer is empty 2024-08-23 18:24:04 +10:00
278 changed files with 5925 additions and 2880 deletions

View File

@ -27,21 +27,20 @@ python3 -m pip install --upgrade wheel
python3 -m pip install coverage
python3 -m pip install defusedxml
python3 -m pip install ipython
python3 -m pip install numpy
python3 -m pip install olefile
python3 -m pip install -U pytest
python3 -m pip install -U pytest-cov
python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma
# optional test dependency, only install if there's a binary package.
# fails on beta 3.14 and PyPy
# optional test dependencies, only install if there's a binary package.
python3 -m pip install --only-binary=:all: numpy || true
python3 -m pip install --only-binary=:all: pyarrow || true
# PyQt6 doesn't support PyPy3
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
sudo apt-get -qq install libegl1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0
# 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
@ -54,7 +53,7 @@ pushd depends && ./install_imagequant.sh && popd
pushd depends && sudo ./install_raqm.sh && popd
# libavif
pushd depends && sudo ./install_libavif.sh && popd
pushd depends && ./install_libavif.sh && popd
# extra test images
pushd depends && ./install_extra_test_images.sh && popd

View File

@ -1 +1 @@
cibuildwheel==3.2.1
cibuildwheel==3.4.1

View File

@ -1,4 +1,4 @@
mypy==1.18.2
mypy==1.20.2
arro3-compute
arro3-core
IceSpringPySideStubs-PyQt6
@ -9,7 +9,6 @@ packaging
pyarrow-stubs
pybind11
pytest
sphinx
types-atheris
types-defusedxml
types-olefile

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

166
.github/renovate.json vendored
View File

@ -6,16 +6,170 @@
"labels": [
"Dependency"
],
"minimumReleaseAge": "7 days",
"prCreation": "not-pending",
"schedule": [
"* * 3 * *"
],
"customManagers": [
{
"customType": "regex",
"managerFilePatterns": ["/^\\.github/dependencies\\.json$/"],
"matchStrings": ["\"brotli\":\\s*\"(?<currentValue>\\d+[^\"]*)\""],
"depNameTemplate": "brotli",
"packageNameTemplate": "google/brotli",
"datasourceTemplate": "github-releases",
"extractVersionTemplate": "^v(?<version>.+)$"
},
{
"customType": "regex",
"managerFilePatterns": ["/^\\.github/dependencies\\.json$/"],
"matchStrings": ["\"bzip2\":\\s*\"(?<currentValue>\\d+[^\"]*)\""],
"depNameTemplate": "bzip2",
"packageNameTemplate": "bzip2/bzip2",
"datasourceTemplate": "gitlab-tags",
"extractVersionTemplate": "^bzip2-(?<version>.+)$"
},
{
"customType": "regex",
"managerFilePatterns": ["/^\\.github/dependencies\\.json$/"],
"matchStrings": ["\"freetype\":\\s*\"(?<currentValue>\\d+[^\"]*)\""],
"depNameTemplate": "freetype",
"packageNameTemplate": "freetype/freetype",
"datasourceTemplate": "gitlab-tags",
"registryUrlTemplate": "https://gitlab.freedesktop.org",
"extractVersionTemplate": "^VER-(?<version>[\\d-]+)$",
"versioningTemplate": "regex:^(?<major>\\d+)[.-](?<minor>\\d+)[.-](?<patch>\\d+)$"
},
{
"customType": "regex",
"managerFilePatterns": ["/^\\.github/dependencies\\.json$/"],
"matchStrings": ["\"fribidi\":\\s*\"(?<currentValue>\\d+[^\"]*)\""],
"depNameTemplate": "fribidi",
"packageNameTemplate": "fribidi/fribidi",
"datasourceTemplate": "github-releases",
"extractVersionTemplate": "^v(?<version>.+)$"
},
{
"customType": "regex",
"managerFilePatterns": ["/^\\.github/dependencies\\.json$/"],
"matchStrings": ["\"harfbuzz\":\\s*\"(?<currentValue>\\d+[^\"]*)\""],
"depNameTemplate": "harfbuzz",
"packageNameTemplate": "harfbuzz/harfbuzz",
"datasourceTemplate": "github-releases"
},
{
"customType": "regex",
"managerFilePatterns": ["/^\\.github/dependencies\\.json$/"],
"matchStrings": ["\"jpegturbo\":\\s*\"(?<currentValue>\\d+[^\"]*)\""],
"depNameTemplate": "jpegturbo",
"packageNameTemplate": "libjpeg-turbo/libjpeg-turbo",
"datasourceTemplate": "github-releases"
},
{
"customType": "regex",
"managerFilePatterns": ["/^\\.github/dependencies\\.json$/"],
"matchStrings": ["\"lcms2\":\\s*\"(?<currentValue>\\d+[^\"]*)\""],
"depNameTemplate": "lcms2",
"packageNameTemplate": "mm2/Little-CMS",
"datasourceTemplate": "github-releases",
"extractVersionTemplate": "^lcms(?<version>.+)$"
},
{
"customType": "regex",
"managerFilePatterns": ["/^\\.github/dependencies\\.json$/"],
"matchStrings": ["\"libavif\":\\s*\"(?<currentValue>\\d+[^\"]*)\""],
"depNameTemplate": "libavif",
"packageNameTemplate": "AOMediaCodec/libavif",
"datasourceTemplate": "github-releases",
"extractVersionTemplate": "^v(?<version>.+)$"
},
{
"customType": "regex",
"managerFilePatterns": ["/^\\.github/dependencies\\.json$/"],
"matchStrings": ["\"libimagequant\":\\s*\"(?<currentValue>\\d+[^\"]*)\""],
"depNameTemplate": "libimagequant",
"packageNameTemplate": "ImageOptim/libimagequant",
"datasourceTemplate": "github-tags"
},
{
"customType": "regex",
"managerFilePatterns": ["/^\\.github/dependencies\\.json$/"],
"matchStrings": ["\"libpng\":\\s*\"(?<currentValue>\\d+[^\"]*)\""],
"depNameTemplate": "libpng",
"packageNameTemplate": "pnggroup/libpng",
"datasourceTemplate": "github-tags",
"extractVersionTemplate": "^v(?<version>.+)$"
},
{
"customType": "regex",
"managerFilePatterns": ["/^\\.github/dependencies\\.json$/"],
"matchStrings": ["\"libwebp\":\\s*\"(?<currentValue>\\d+[^\"]*)\""],
"depNameTemplate": "libwebp",
"packageNameTemplate": "webmproject/libwebp",
"datasourceTemplate": "github-tags",
"extractVersionTemplate": "^v(?<version>.+)$"
},
{
"customType": "regex",
"managerFilePatterns": ["/^\\.github/dependencies\\.json$/"],
"matchStrings": ["\"libxcb\":\\s*\"(?<currentValue>\\d+[^\"]*)\""],
"depNameTemplate": "libxcb",
"packageNameTemplate": "xorg/lib/libxcb",
"datasourceTemplate": "gitlab-tags",
"registryUrlTemplate": "https://gitlab.freedesktop.org",
"extractVersionTemplate": "^libxcb-(?<version>.+)$"
},
{
"customType": "regex",
"managerFilePatterns": ["/^\\.github/dependencies\\.json$/"],
"matchStrings": ["\"openjpeg\":\\s*\"(?<currentValue>\\d+[^\"]*)\""],
"depNameTemplate": "openjpeg",
"packageNameTemplate": "uclouvain/openjpeg",
"datasourceTemplate": "github-releases",
"extractVersionTemplate": "^v(?<version>.+)$"
},
{
"customType": "regex",
"managerFilePatterns": ["/^\\.github/dependencies\\.json$/"],
"matchStrings": ["\"tiff\":\\s*\"(?<currentValue>\\d+[^\"]*)\""],
"depNameTemplate": "tiff",
"packageNameTemplate": "libtiff/libtiff",
"datasourceTemplate": "gitlab-tags",
"extractVersionTemplate": "^v(?<version>.+)$"
},
{
"customType": "regex",
"managerFilePatterns": ["/^\\.github/dependencies\\.json$/"],
"matchStrings": ["\"xz\":\\s*\"(?<currentValue>\\d+[^\"]*)\""],
"depNameTemplate": "xz",
"packageNameTemplate": "tukaani-project/xz",
"datasourceTemplate": "github-releases",
"extractVersionTemplate": "^v(?<version>.+)$"
},
{
"customType": "regex",
"managerFilePatterns": ["/^\\.github/dependencies\\.json$/"],
"matchStrings": ["\"zlib-ng\":\\s*\"(?<currentValue>\\d+[^\"]*)\""],
"depNameTemplate": "zlib-ng",
"packageNameTemplate": "zlib-ng/zlib-ng",
"datasourceTemplate": "github-releases"
},
{
"customType": "regex",
"managerFilePatterns": ["/^\\.github/dependencies\\.json$/"],
"matchStrings": ["\"zstd\":\\s*\"(?<currentValue>\\d+[^\"]*)\""],
"depNameTemplate": "zstd",
"packageNameTemplate": "facebook/zstd",
"datasourceTemplate": "github-releases",
"extractVersionTemplate": "^v(?<version>.+)$"
}
],
"packageRules": [
{
"groupName": "github-actions",
"matchManagers": [
"github-actions"
],
"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:
@ -24,33 +21,36 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
FORCE_COLOR: 1
jobs:
Fuzzing:
runs-on: ubuntu-latest
steps:
- name: Build Fuzzers
id: build
uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@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@v4
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@v4
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@v5
- 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
@ -48,19 +45,35 @@ jobs:
- name: Build system information
run: python3 .github/workflows/system-info.py
- name: Cache libavif
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
id: cache-libavif
with:
path: ~/cache-libavif
key: ${{ runner.os }}-libavif-${{ hashFiles('depends/install_libavif.sh', 'depends/libavif-svt4.patch') }}
- name: Cache libimagequant
uses: actions/cache@v4
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
id: cache-libimagequant
with:
path: ~/cache-libimagequant
key: ${{ runner.os }}-libimagequant-${{ hashFiles('depends/install_imagequant.sh') }}
- name: Cache libwebp
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
id: cache-libwebp
with:
path: ~/cache-libwebp
key: ${{ runner.os }}-libwebp-${{ hashFiles('depends/install_webp.sh') }}
- name: Install Linux dependencies
run: |
.ci/install.sh
env:
GHA_PYTHON_VERSION: "3.x"
GHA_LIBAVIF_CACHE_HIT: ${{ steps.cache-libavif.outputs.cache-hit }}
GHA_LIBIMAGEQUANT_CACHE_HIT: ${{ steps.cache-libimagequant.outputs.cache-hit }}
GHA_LIBWEBP_CACHE_HIT: ${{ steps.cache-libwebp.outputs.cache-hit }}
- name: Build
run: |

View File

@ -2,55 +2,31 @@ name: Lint
on: [push, pull_request, workflow_dispatch]
permissions: {}
env:
FORCE_COLOR: 1
permissions:
contents: read
PREK_COLOR: always
RUFF_OUTPUT_FORMAT: github
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
lint:
runs-on: ubuntu-latest
name: Lint
steps:
- uses: actions/checkout@v5
with:
persist-credentials: false
- name: pre-commit cache
uses: actions/cache@v4
with:
path: ~/.cache/pre-commit
key: lint-pre-commit-${{ hashFiles('**/.pre-commit-config.yaml') }}
restore-keys: |
lint-pre-commit-
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.x"
cache: pip
cache-dependency-path: "setup.py"
- name: Build system information
run: python3 .github/workflows/system-info.py
- name: Install dependencies
run: |
python3 -m pip install -U pip
python3 -m pip install -U tox
- name: Lint
run: tox -e lint
env:
PRE_COMMIT_COLOR: always
- name: Mypy
run: tox -e mypy
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.x"
- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
- name: Lint
run: uvx --with tox-uv tox -e lint
- name: Mypy
run: uvx --with tox-uv tox -e mypy

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
@ -26,9 +13,8 @@ python3 -m pip install -U pytest
python3 -m pip install -U pytest-cov
python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma
python3 -m pip install numpy
# optional test dependency, only install if there's a binary package.
# fails on beta 3.14 and PyPy
# optional test dependencies, only install if there's a binary package.
python3 -m pip install --only-binary=:all: numpy || true
python3 -m pip install --only-binary=:all: pyarrow || true
# libavif

View File

@ -14,6 +14,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
FORCE_COLOR: 1
jobs:
update_release_draft:
permissions:
@ -23,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

@ -12,9 +12,12 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
FORCE_COLOR: 1
jobs:
stale:
if: github.repository_owner == 'python-pillow'
if: github.event.repository.fork == false
permissions:
issues: write
@ -22,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:
@ -26,6 +21,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
FORCE_COLOR: 1
jobs:
build:
@ -36,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-41-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@v5
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
@ -77,13 +73,13 @@ 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 }}
- name: Docker pull
run: |
docker pull pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }}
docker pull ${{ matrix.qemu-arch && format('--platform=linux/{0}', matrix.qemu-arch)}} pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }}
- name: Docker build
run: |
@ -105,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:
@ -28,6 +23,7 @@ concurrency:
env:
COVERAGE_CORE: sysmon
FORCE_COLOR: 1
jobs:
build:
@ -45,7 +41,7 @@ jobs:
steps:
- name: Checkout Pillow
uses: actions/checkout@v5
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
@ -86,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"
@ -26,6 +27,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
FORCE_COLOR: 1
jobs:
build:
@ -41,7 +45,7 @@ jobs:
name: ${{ matrix.docker }}
steps:
- uses: actions/checkout@v5
- 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:
@ -24,6 +21,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
FORCE_COLOR: 1
jobs:
build:
@ -39,7 +39,7 @@ jobs:
name: ${{ matrix.docker }}
steps:
- uses: actions/checkout@v5
- 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:
@ -28,18 +23,20 @@ concurrency:
env:
COVERAGE_CORE: sysmon
FORCE_COLOR: 1
jobs:
build:
runs-on: windows-latest
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
python-version: ["pypy3.11", "3.11", "3.12", "3.13", "3.14"]
python-version: ["pypy3.11", "3.11", "3.12", "3.13", "3.14", "3.15"]
architecture: ["x64"]
os: ["windows-latest"]
include:
# Test the oldest Python on 32-bit
- { python-version: "3.10", architecture: "x86" }
- { python-version: "3.10", architecture: "x86", os: "windows-2022" }
timeout-minutes: 45
@ -47,19 +44,19 @@ jobs:
steps:
- name: Checkout Pillow
uses: actions/checkout@v5
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Checkout cached dependencies
uses: actions/checkout@v5
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@v5
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
repository: python-pillow/test-images
@ -67,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
@ -83,7 +80,7 @@ jobs:
python3 -m pip install --upgrade pip
- name: Install CPython dependencies
if: "!contains(matrix.python-version, 'pypy') && !contains(matrix.python-version, '3.14') && matrix.architecture != 'x86'"
if: "!contains(matrix.python-version, 'pypy') && matrix.architecture != 'x86'"
run: |
python3 -m pip install PyQt6
@ -97,8 +94,8 @@ jobs:
choco install nasm --no-progress
echo "C:\Program Files\NASM" >> $env:GITHUB_PATH
choco install ghostscript --version=10.6.0 --no-progress
echo "C:\Program Files\gs\gs10.06.0\bin" >> $env:GITHUB_PATH
choco install ghostscript --version=10.7.0 --no-progress
echo "C:\Program Files\gs\gs10.07.0\bin" >> $env:GITHUB_PATH
# Install extra test images
xcopy /S /Y Tests\test-images\* Tests\images
@ -111,7 +108,7 @@ jobs:
- name: Cache build
id: build-cache
uses: actions/cache@v4
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: winbuild\build
key:
@ -187,8 +184,9 @@ jobs:
# trim ~150MB for each job
- name: Optimize build cache
if: steps.build-cache.outputs.cache-hit != 'true'
run: rmdir /S /Q winbuild\build\src
shell: cmd
run: |
rm -rf winbuild\build\src
shell: bash
- name: Build Pillow
run: |
@ -205,9 +203,7 @@ jobs:
- name: Test Pillow
run: |
path %GITHUB_WORKSPACE%\winbuild\build\bin;%PATH%
.ci\test.cmd
shell: cmd
- name: Prepare to upload errors
if: failure()
@ -216,7 +212,7 @@ jobs:
shell: bash
- name: Upload errors
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: failure()
with:
name: errors
@ -228,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:
@ -29,6 +24,7 @@ concurrency:
env:
COVERAGE_CORE: sysmon
FORCE_COLOR: 1
PIP_DISABLE_PIP_VERSION_CHECK: 1
jobs:
build:
@ -42,9 +38,10 @@ jobs:
]
python-version: [
"pypy3.11",
"3.15t",
"3.15",
"3.14t",
"3.14",
"3.13t",
"3.13",
"3.12",
"3.11",
@ -53,11 +50,8 @@ jobs:
include:
- { python-version: "3.12", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" }
- { python-version: "3.11", PYTHONOPTIMIZE: 2 }
# Free-threaded
- { python-version: "3.14t", disable-gil: true }
- { python-version: "3.13t", disable-gil: true }
# Intel
- { os: "macos-15-intel", python-version: "3.10" }
- { os: "macos-26-intel", python-version: "3.10" }
exclude:
- { os: "macos-latest", python-version: "3.10" }
@ -65,12 +59,12 @@ jobs:
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
steps:
- uses: actions/checkout@v5
- 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
@ -79,29 +73,42 @@ 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@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
id: cache-libavif
with:
path: ~/cache-libavif
key: ${{ runner.os }}-libavif-${{ hashFiles('depends/install_libavif.sh', 'depends/libavif-svt4.patch') }}
- name: Cache libimagequant
if: startsWith(matrix.os, 'ubuntu')
uses: actions/cache@v4
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
id: cache-libimagequant
with:
path: ~/cache-libimagequant
key: ${{ runner.os }}-libimagequant-${{ hashFiles('depends/install_imagequant.sh') }}
- name: Cache libwebp
if: startsWith(matrix.os, 'ubuntu')
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
id: cache-libwebp
with:
path: ~/cache-libwebp
key: ${{ runner.os }}-libwebp-${{ hashFiles('depends/install_webp.sh') }}
- name: Install Linux dependencies
if: startsWith(matrix.os, 'ubuntu')
run: |
.ci/install.sh
env:
GHA_PYTHON_VERSION: ${{ matrix.python-version }}
GHA_LIBAVIF_CACHE_HIT: ${{ steps.cache-libavif.outputs.cache-hit }}
GHA_LIBIMAGEQUANT_CACHE_HIT: ${{ steps.cache-libimagequant.outputs.cache-hit }}
GHA_LIBWEBP_CACHE_HIT: ${{ steps.cache-libwebp.outputs.cache-hit }}
- name: Install macOS dependencies
if: startsWith(matrix.os, 'macOS')
@ -140,7 +147,7 @@ jobs:
mkdir -p Tests/errors
- name: Upload errors
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: failure()
with:
name: errors
@ -151,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

@ -32,7 +32,6 @@ if [[ "$CIBW_PLATFORM" == "ios" ]]; then
# or `build/deps/iphonesimulator`
WORKDIR=$(pwd)/build/$IOS_SDK
BUILD_PREFIX=$(pwd)/build/deps/$IOS_SDK
PATCH_DIR=$(pwd)/patches/iOS
# GNU tooling insists on using aarch64 rather than arm64
if [[ $PLAT == "arm64" ]]; then
@ -90,28 +89,23 @@ fi
ARCHIVE_SDIR=pillow-depends-main
# Package versions for fresh source builds. Version numbers with "Patched"
# annotations have a source code patch that is required for some platforms. If
# you change those versions, ensure the patch is also updated.
if [[ -n "$IOS_SDK" ]]; then
FREETYPE_VERSION=2.13.3
else
FREETYPE_VERSION=2.14.1
fi
HARFBUZZ_VERSION=12.1.0
LIBPNG_VERSION=1.6.50
JPEGTURBO_VERSION=3.1.2
OPENJPEG_VERSION=2.5.4
XZ_VERSION=5.8.1
ZSTD_VERSION=1.5.7
TIFF_VERSION=4.7.1
LCMS2_VERSION=2.17
ZLIB_NG_VERSION=2.2.5
LIBWEBP_VERSION=1.6.0
BZIP2_VERSION=1.0.8
LIBXCB_VERSION=1.17.0
BROTLI_VERSION=1.1.0 # Patched; next release won't need patching. See patch file.
LIBAVIF_VERSION=1.3.0
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
@ -149,18 +143,9 @@ function build_zlib_ng {
ORIGINAL_HOST_CONFIGURE_FLAGS=$HOST_CONFIGURE_FLAGS
unset HOST_CONFIGURE_FLAGS
build_github zlib-ng/zlib-ng $ZLIB_NG_VERSION --zlib-compat
build_github zlib-ng/zlib-ng $ZLIB_NG_VERSION --installnamedir=$BUILD_PREFIX/lib --zlib-compat
HOST_CONFIGURE_FLAGS=$ORIGINAL_HOST_CONFIGURE_FLAGS
if [[ -n "$IS_MACOS" ]] && [[ -z "$IOS_SDK" ]]; then
# Ensure that on macOS, the library name is an absolute path, not an
# @rpath, so that delocate picks up the right library (and doesn't need
# DYLD_LIBRARY_PATH to be set). The default Makefile doesn't have an
# option to control the install_name. This isn't needed on iOS, as iOS
# only builds the static library.
install_name_tool -id $BUILD_PREFIX/lib/libz.1.dylib $BUILD_PREFIX/lib/libz.1.dylib
fi
touch zlib-stamp
}
@ -168,7 +153,7 @@ function build_brotli {
if [ -e brotli-stamp ]; then return; fi
local out_dir=$(fetch_unpack https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-$BROTLI_VERSION.tar.gz)
(cd $out_dir \
&& cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib $HOST_CMAKE_FLAGS . \
&& cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib -DCMAKE_MACOSX_BUNDLE=OFF $HOST_CMAKE_FLAGS . \
&& make -j4 install)
touch brotli-stamp
}
@ -194,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
@ -211,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
@ -242,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 . )
@ -279,7 +260,7 @@ function build {
build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto
if [[ -n "$IS_MACOS" ]]; then
build_simple xorgproto 2024.1 https://www.x.org/pub/individual/proto
build_simple xorgproto 2025.1 https://www.x.org/pub/individual/proto
build_simple libXau 1.0.12 https://www.x.org/pub/individual/lib
build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist
else
@ -322,10 +303,6 @@ function build {
if [[ -n "$IS_MACOS" ]]; then
# Custom freetype build
if [[ -z "$IOS_SDK" ]]; then
build_simple sed 4.9 https://mirrors.middlendian.com/gnu/sed
fi
build_simple freetype $FREETYPE_VERSION https://download.savannah.gnu.org/releases/freetype tar.gz --with-harfbuzz=no
else
build_freetype

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:
@ -53,19 +50,19 @@ jobs:
include:
- name: "macOS 10.10 x86_64"
platform: macos
os: macos-15-intel
os: macos-26-intel
cibw_arch: x86_64
build: "cp3{10,11}*"
macosx_deployment_target: "10.10"
- name: "macOS 10.13 x86_64"
platform: macos
os: macos-15-intel
os: macos-26-intel
cibw_arch: x86_64
build: "cp3{12,13}*"
macosx_deployment_target: "10.13"
- name: "macOS 10.15 x86_64"
platform: macos
os: macos-15-intel
os: macos-26-intel
cibw_arch: x86_64
build: "{cp314,pp3}*"
macosx_deployment_target: "10.15"
@ -74,45 +71,45 @@ 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
cibw_arch: arm64_iphoneos
- name: "iOS arm64 simulator"
platform: ios
os: macos-14
os: macos-latest
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@v5
- 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@v4
- 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@v5
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Checkout extra test images
uses: actions/checkout@v5
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"
@ -186,29 +179,23 @@ jobs:
- name: Build wheels
run: |
setlocal EnableDelayedExpansion
for %%f in (winbuild\build\license\*) do (
set x=%%~nf
rem Skip FriBiDi license, it is not included in the wheel.
set fribidi=!x:~0,7!
if NOT !fribidi!==fribidi (
rem Skip imagequant license, it is not included in the wheel.
set libimagequant=!x:~0,13!
if NOT !libimagequant!==libimagequant (
echo. >> LICENSE
echo ===== %%~nf ===== >> LICENSE
echo. >> LICENSE
type %%f >> LICENSE
)
)
)
call winbuild\\build\\build_env.cmd
%pythonLocation%\python.exe -m cibuildwheel . --output-dir wheelhouse
for f in winbuild/build/license/*; do
name=$(basename "${f%.*}")
# Skip FriBiDi license, it is not included in the wheel.
[[ $name == fribidi* ]] && continue
# Skip imagequant license, it is not included in the wheel.
[[ $name == libimagequant* ]] && continue
echo "" >> LICENSE
echo "===== $name =====" >> LICENSE
echo "" >> LICENSE
cat "$f" >> LICENSE
done
cmd //c "winbuild\\build\\build_env.cmd && $pythonLocation\\python.exe -m cibuildwheel . --output-dir wheelhouse"
env:
CIBW_ARCHS: ${{ matrix.cibw_arch }}
CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd"
CIBW_CACHE_PATH: "C:\\cibw"
CIBW_ENABLE: cpython-prerelease cpython-freethreading pypy
CIBW_ENABLE: cpython-prerelease pypy
CIBW_TEST_SKIP: "*-win_arm64"
CIBW_TEST_COMMAND: 'docker run --rm
-v {project}:C:\pillow
@ -217,36 +204,36 @@ jobs:
-e CI -e GITHUB_ACTIONS
mcr.microsoft.com/windows/servercore:ltsc2022
powershell C:\pillow\.github\workflows\wheels-test.ps1 %CD%\..\venv-test'
shell: cmd
shell: bash
- name: Upload wheels
uses: actions/upload-artifact@v4
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@v4
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@v5
- 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@v4
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: dist-sdist
path: dist/*.tar.gz
@ -256,7 +243,7 @@ jobs:
runs-on: ubuntu-latest
name: Count dists
steps:
- uses: actions/download-artifact@v5
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: dist-*
path: dist
@ -269,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@v5
- 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@b36e8c0c10dbcfd2e05bf95f17ef8c14fd708dbf # 0.6.2
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
@ -297,12 +357,12 @@ jobs:
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v5
- 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

7
.github/zizmor.yml vendored
View File

@ -1,7 +0,0 @@
# Configuration for the zizmor static analysis tool, run via pre-commit in CI
# 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,30 +1,30 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.13.3
rev: v0.15.12
hooks:
- id: ruff-check
args: [--exit-non-zero-on-fix]
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 25.9.0
rev: 26.3.1
hooks:
- id: black
- repo: https://github.com/PyCQA/bandit
rev: 1.8.6
rev: 1.9.4
hooks:
- id: bandit
args: [--severity-level=high]
files: ^src/
- repo: https://github.com/Lucas-C/pre-commit-hooks
rev: v1.5.5
rev: v1.5.6
hooks:
- id: remove-tabs
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$|\.patch$)
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
- repo: https://github.com/pre-commit/mirrors-clang-format
rev: v21.1.2
rev: v22.1.4
hooks:
- id: clang-format
types: [c]
@ -38,6 +38,7 @@ repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: check-case-conflict
- id: check-executables-have-shebangs
- id: check-shebang-scripts-are-executable
- id: check-merge-conflict
@ -46,40 +47,42 @@ repos:
- id: check-yaml
args: [--allow-multiple-documents]
- id: end-of-file-fixer
exclude: ^Tests/images/|\.patch$
exclude: ^Tests/images/
- id: file-contents-sorter
files: .github/workflows/Brewfile
- id: trailing-whitespace
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/|\.patch$
exclude: ^\.github/.*TEMPLATE|^Tests/(fonts|images)/
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.34.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.14.2
rev: v1.24.1
hooks:
- id: zizmor
- repo: https://github.com/sphinx-contrib/sphinx-lint
rev: v1.0.0
rev: v1.0.2
hooks:
- id: sphinx-lint
- repo: https://github.com/tox-dev/pyproject-fmt
rev: v2.7.0
rev: v2.21.1
hooks:
- id: pyproject-fmt
- repo: https://github.com/abravalheri/validate-pyproject
rev: v0.24.1
rev: v0.25
hooks:
- id: validate-pyproject
additional_dependencies: [trove-classifiers>=2024.10.12]
- repo: https://github.com/tox-dev/tox-ini-fmt
rev: 1.6.0
rev: 1.7.1
hooks:
- id: tox-ini-fmt

View File

@ -5,7 +5,7 @@ The Python Imaging Library (PIL) is
Pillow is the friendly PIL fork. It is
Copyright © 2010 by Jeffrey A. Clark and contributors
Copyright © 2010 by Jeffrey 'Alex' Clark and contributors
Like PIL, Pillow is licensed under the open source MIT-CMU License:

View File

@ -15,7 +15,6 @@ include tox.ini
graft Tests
graft Tests/images
graft checks
graft patches
graft src
graft depends
graft winbuild

View File

@ -6,11 +6,13 @@
## Python Imaging Library (Fork)
Pillow is the friendly PIL fork by [Jeffrey A. Clark and
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",

Binary file not shown.

View File

@ -2,7 +2,7 @@
NotoNastaliqUrdu-Regular.ttf and NotoSansSymbols-Regular.ttf, from https://github.com/googlei18n/noto-fonts
NotoSans-Regular.ttf, from https://www.google.com/get/noto/
NotoSansJP-Thin.otf, from https://www.google.com/get/noto/help/cjk/
AdobeVFPrototype.ttf, from https://github.com/adobe-fonts/adobe-variable-font-prototype
AdobeVFPrototype.ttf, from https://github.com/adobe-fonts/adobe-variable-font-prototype. AdobeVFPrototypeDuplicates.ttf is a modified version of this
TINY5x3GX.ttf, from http://velvetyne.fr/fonts/tiny
ArefRuqaa-Regular.ttf, from https://github.com/google/fonts/tree/master/ofl/arefruqaa
ter-x20b.pcf, from http://terminus-font.sourceforge.net/

View File

@ -1,10 +1,10 @@
STARTFONT
FONT ÿ
SIZE 10
FONTBOUNDINGBOX
CHARS
FONTBOUNDINGBOX 1 1 0 0
CHARS 1
STARTCHAR
ENCODING
ENCODING 65
BBX 2 5
ENDCHAR
ENDFONT

View File

@ -55,8 +55,8 @@ def convert_to_comparable(
if a.mode == "P":
new_a = Image.new("L", a.size)
new_b = Image.new("L", b.size)
new_a.putdata(a.getdata())
new_b.putdata(b.getdata())
new_a.putdata(a.get_flattened_data())
new_b.putdata(b.get_flattened_data())
elif a.mode == "I;16":
new_a = a.convert("I")
new_b = b.convert("I")
@ -104,10 +104,9 @@ def assert_image_equal_tofile(
msg: str | None = None,
mode: str | None = None,
) -> None:
with Image.open(filename) as img:
if mode:
img = img.convert(mode)
assert_image_equal(a, img, msg)
with Image.open(filename) as im:
converted_im = im.convert(mode) if mode else im
assert_image_equal(a, converted_im, msg)
def assert_image_similar(

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 B

View File

@ -1,578 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>BMP Suite Image List</title>
<style>
.b { background:url(bkgd.png); }
.q { background-color:#fff0e0; }
.bad { background-color:#ffa0a0; }
</style>
</head>
<body>
<h1>BMP Suite Image List</h1>
<p><i>For <a href="http://entropymine.com/jason/bmpsuite/">BMP Suite</a>
version 2.3</i></p>
<p>This document describes the images in <i>BMP Suite</i>, and shows what
I allege to be the correct way to interpret them. PNG and JPEG images are
used for reference.
</p>
<p>It also shows how your web browser displays the BMP images,
but that&rsquo;s not its main purpose.
BMP is poor image format to use on web pages, so a web browser&rsquo;s
level of support for it is arguably not important.</p>
<table border=1 cellpadding=8>
<tr>
<th>File</th>
<th>Ver.</th>
<th>Correct display</th>
<th>In your browser</th>
<th>Notes</th>
</tr>
<tr>
<td>g/pal1.bmp</td>
<td>3</td>
<td class=b><img src="pal1.png"></td>
<td class=b><img src="../g/pal1.bmp"></td>
<td>1 bit/pixel paletted image, in which black is the first color in
the palette.</td>
</tr>
<tr>
<td>g/pal1wb.bmp</td>
<td>3</td>
<td class=b><img src="pal1.png"></td>
<td class=b><img src="../g/pal1wb.bmp"></td>
<td>1 bit/pixel paletted image, in which white is the first color in
the palette.</td>
</tr>
<tr>
<td>g/pal1bg.bmp</td>
<td>3</td>
<td class=b><img src="pal1bg.png"></td>
<td class=b><img src="../g/pal1bg.bmp"></td>
<td>1 bit/pixel paletted image, with colors other than black and white.</td>
</tr>
<tr>
<td class=q>q/pal1p1.bmp</td>
<td>3</td>
<td class=b><img src="pal1p1.png"></td>
<td class=b><img src="../q/pal1p1.bmp"></td>
<td>1 bit/pixel paletted image, with only one color in the palette.
The documentation says that 1-bpp images have a palette size of 2
(not &ldquo;up to 2&rdquo;), but it would be silly for a viewer not to
support a size of 1.</td>
</tr>
<tr>
<td class=q>q/pal2.bmp</td>
<td>3</td>
<td class=b><img src="pal2.png"></td>
<td class=b><img src="../q/pal2.bmp"></td>
<td>A paletted image with 2 bits/pixel. Usually only 1, 4,
and 8 are allowed, but 2 is legal on Windows CE.</td>
</tr>
<tr>
<td>g/pal4.bmp</td>
<td>3</td>
<td class=b><img src="pal4.png"></td>
<td class=b><img src="../g/pal4.bmp"></td>
<td>Paletted image with 12 palette colors, and 4 bits/pixel.</td>
</tr>
<tr>
<td>g/pal4rle.bmp</td>
<td>3</td>
<td class=b><img src="pal4.png"></td>
<td class=b><img src="../g/pal4rle.bmp"></td>
<td>4-bit image that uses RLE compression.</td>
</tr>
<tr>
<td class=q>q/pal4rletrns.bmp</td>
<td>3</td>
<td class=b><img src="pal4rletrns.png"><br>
or<br><img src="pal4rletrns-0.png"><br>
or<br><img src="pal4rletrns-b.png"></td>
<td class=b><img src="../q/pal4rletrns.bmp"></td>
<td>An RLE-compressed image that used &ldquo;delta&rdquo;
codes to skip over some pixels, leaving them undefined. Some viewers
make undefined pixels transparent, others make them black, and
others assign them palette color 0 (purple, in this case).</td>
</tr>
<tr>
<td>g/pal8.bmp</td>
<td>3</td>
<td class=b><img src="pal8.png"></td>
<td class=b><img src="../g/pal8.bmp"></td>
<td>Our standard paletted image, with 252 palette colors, and 8
bits/pixel.</td>
</tr>
<tr>
<td>g/pal8-0.bmp</td>
<td>3</td>
<td class=b><img src="pal8.png"></td>
<td class=b><img src="../g/pal8-0.bmp"></td>
<td>Every field that can be set to 0 is set to 0: pixels/meter=0;
colors used=0 (meaning the default 256); size-of-image=0.</td>
</tr>
<tr>
<td>g/pal8rle.bmp</td>
<td>3</td>
<td class=b><img src="pal8.png"></td>
<td class=b><img src="../g/pal8rle.bmp"></td>
<td>8-bit image that uses RLE compression.</td>
</tr>
<tr>
<td class=q>q/pal8rletrns.bmp</td>
<td>3</td>
<td class=b><img src="pal8rletrns.png"><br>
or<br><img src="pal8rletrns-0.png"><br>
or<br><img src="pal8rletrns-b.png"></td>
<td class=b><img src="../q/pal8rletrns.bmp"></td>
<td>8-bit version of q/pal4rletrns.bmp.</td>
</tr>
<tr>
<td>g/pal8w126.bmp</td>
<td>3</td>
<td class=b><img src="pal8w126.png"></td>
<td class=b><img src="../g/pal8w126.bmp"></td>
<td rowspan=3>Images with different widths and heights.
In BMP format, rows are padded to a multiple of four bytes, so we
test all four possibilities.</td>
</tr>
<tr>
<td>g/pal8w125.bmp</td>
<td>3</td>
<td class=b><img src="pal8w125.png"></td>
<td class=b><img src="../g/pal8w125.bmp"></td>
</tr>
<tr>
<td>g/pal8w124.bmp</td>
<td>3</td>
<td class=b><img src="pal8w124.png"></td>
<td class=b><img src="../g/pal8w124.bmp"></td>
</tr>
<tr>
<td>g/pal8topdown.bmp</td>
<td>3</td>
<td class=b><img src="pal8.png"></td>
<td class=b><img src="../g/pal8topdown.bmp"></td>
<td>BMP images are normally stored from the bottom up, but
there is a way to store them from the top down.</td>
</tr>
<tr>
<td class=q>q/pal8offs.bmp</td>
<td>3</td>
<td class=b><img src="pal8.png"></td>
<td class=b><img src="../q/pal8offs.bmp"></td>
<td>A file with some unused bytes between the palette and the
image. This is probably valid, but I&rsquo;m not 100% sure.</td>
</tr>
<tr>
<td class=q>q/pal8oversizepal.bmp</td>
<td>3</td>
<td class=b><img src="pal8.png"></td>
<td class=b><img src="../q/pal8oversizepal.bmp"></td>
<td>An 8-bit image with 300 palette colors. This may be invalid,
because the documentation could
be interpreted to imply that 8-bit images aren&rsquo;t allowed
to have more than 256 colors.</td>
</tr>
<tr>
<td>g/pal8nonsquare.bmp</td>
<td>3</td>
<td class=b>
<img src="pal8nonsquare-v.png"><br>
or<br>
<img src="pal8nonsquare-e.png">
</td>
<td class=b><img src="../g/pal8nonsquare.bmp"></td>
<td>An image with non-square pixels: the X pixels/meter is twice
the Y pixels/meter. Image <i>editors</i> can be expected to
leave the image &ldquo;squashed&rdquo;; image <i>viewers</i> should
consider stretching it to its correct proportions.</td>
</tr>
<tr>
<td>g/pal8os2.bmp</td>
<td>OS/2v1</td>
<td class=b><img src="pal8.png"></td>
<td class=b><img src="../g/pal8os2.bmp"></td>
<td>An OS/2-style bitmap.</td>
</tr>
<tr>
<td class=q>q/pal8os2sp.bmp</td>
<td>OS/2v1</td>
<td class=b><img src="pal8.png"></td>
<td class=b><img src="../q/pal8os2sp.bmp"></td>
<td>An OS/2v1 with a less-than-full-sized palette.
Probably not valid, but such files have been seen in the wild.</td>
</tr>
<tr>
<td class=q>q/pal8os2v2.bmp</td>
<td>OS/2v2</td>
<td class=b><img src="pal8.png"></td>
<td class=b><img src="../q/pal8os2v2.bmp"></td>
<td>My attempt to make an OS/2v2 bitmap.</td>
</tr>
<tr>
<td class=q>q/pal8os2v2-16.bmp</td>
<td>OS/2v2</td>
<td class=b><img src="pal8.png"></td>
<td class=b><img src="../q/pal8os2v2-16.bmp"></td>
<td>An OS/2v2 bitmap whose header has only 16 bytes, instead of the full 64.</td>
</tr>
<tr>
<td>g/pal8v4.bmp</td>
<td>4</td>
<td class=b><img src="pal8.png"></td>
<td class=b><img src="../g/pal8v4.bmp"></td>
<td>A v4 bitmap. I&rsquo;m not sure that the gamma and chromaticity values in
this file are sensible, because I can&rsquo;t find any detailed documentation
of them.</td>
</tr>
<tr>
<td>g/pal8v5.bmp</td>
<td>5</td>
<td class=b><img src="pal8.png"></td>
<td class=b><img src="../g/pal8v5.bmp"></td>
<td>A v5 bitmap. Version 5 has additional colorspace options over v4, so it
is easier to create, and ought to be more portable.</td>
</tr>
<tr>
<td>g/rgb16.bmp</td>
<td>3</td>
<td class=b><img src="rgb16.png"></td>
<td class=b><img src="../g/rgb16.bmp"></td>
<td>A 16-bit image with the default color format: 5 bits each for red,
green, and blue, and 1 unused bit.
The whitest colors should (I assume) be displayed as pure white:
<span style="background-color:rgb(255,255,255)">(255,255,255)</span>, not
<span style="background-color:rgb(248,248,248)">(248,248,248)</span>.</td>
</tr>
<tr>
<td>g/rgb16-565.bmp</td>
<td>3</td>
<td class=b><img src="rgb16-565.png"></td>
<td class=b><img src="../g/rgb16-565.bmp"></td>
<td>A 16-bit image with a BITFIELDS segment indicating 5 red, 6 green,
and 5 blue bits. This is a standard 16-bit format, even supported by
old versions of Windows that don&rsquo;t support any other non-default 16-bit
formats.
The whitest colors should be displayed as pure white:
<span style="background-color:rgb(255,255,255)">(255,255,255)</span>, not
<span style="background-color:rgb(248,252,248)">(248,252,248)</span>.</td>
</tr>
<tr>
<td>g/rgb16-565pal.bmp</td>
<td>3</td>
<td class=b><img src="rgb16-565.png"></td>
<td class=b><img src="../g/rgb16-565pal.bmp"></td>
<td>A 16-bit image with both a BITFIELDS segment and a palette.</td>
</tr>
<tr>
<td class=q>q/rgb16-231.bmp</td>
<td>3</td>
<td class=b><img src="rgb16-231.png"></td>
<td class=b><img src="../q/rgb16-231.bmp"></td>
<td>An unusual and silly 16-bit image, with 2 red bits, 3 green bits, and 1
blue bit. Most viewers do support this image, but the colors may be darkened
with a yellow-green shadow. That&rsquo;s because they&rsquo;re doing simple
bit-shifting (possibly including one round of bit replication), instead of
proper scaling.</td>
</tr>
<tr>
<td class=q>q/rgba16-4444.bmp</td>
<td>5</td>
<td class=b><img src="rgba16-4444.png"></td>
<td class=b><img src="../q/rgba16-4444.bmp"></td>
<td>A 16-bit image with an alpha channel. There are 4 bits for each color
channel, and 4 bits for the alpha channel.
It&rsquo;s not clear if this is valid, but I can&rsquo;t find anything that
suggests it isn&rsquo;t.
</td>
</tr>
<tr>
<td>g/rgb24.bmp</td>
<td>3</td>
<td class=b><img src="rgb24.png"></td>
<td class=b><img src="../g/rgb24.bmp"></td>
<td>A perfectly ordinary 24-bit (truecolor) image.</td>
</tr>
<tr>
<td>g/rgb24pal.bmp</td>
<td>3</td>
<td class=b><img src="rgb24.png"></td>
<td class=b><img src="../g/rgb24pal.bmp"></td>
<td>A 24-bit image, with a palette containing 256 colors. There is little if
any reason for a truecolor image to contain a palette, but it is legal.</td>
</tr>
<tr>
<td class=q>q/rgb24largepal.bmp</td>
<td>3</td>
<td class=b><img src="rgb24.png"></td>
<td class=b><img src="../q/rgb24largepal.bmp"></td>
<td>A 24-bit image, with a palette containing 300 colors.
The fact that the palette has more than 256 colors may cause some viewers
to complain, but the documentation does not mention a size limit.</td>
</tr>
<tr>
<td class=q>q/rgb24prof.bmp</td>
<td>5</td>
<td class=b><img src="rgb24.png"></td>
<td class=b><img src="../q/rgb24prof.bmp"></td>
<td>My attempt to make a BMP file with an embedded color profile.</td>
</tr>
<tr>
<td class=q>q/rgb24lprof.bmp</td>
<td>5</td>
<td class=b><img src="rgb24.png"></td>
<td class=b><img src="../q/rgb24lprof.bmp"></td>
<td>My attempt to make a BMP file with a linked color profile.</td>
</tr>
<tr>
<td class=q>q/rgb24jpeg.bmp</td>
<td>5</td>
<td class=b><img src="rgb24.jpg"></td>
<td class=b><img src="../q/rgb24jpeg.bmp"></td>
<td rowspan=2>My attempt to make BMP files with embedded JPEG and PNG images.
These are not likely to be supported by much of anything (they&rsquo;re
intended for printers).</td>
</tr>
<tr>
<td class=q>q/rgb24png.bmp</td>
<td>5</td>
<td class=b><img src="rgb24.png"></td>
<td class=b><img src="../q/rgb24png.bmp"></td>
</tr>
<tr>
<td>g/rgb32.bmp</td>
<td>3</td>
<td class=b><img src="rgb24.png"></td>
<td class=b><img src="../g/rgb32.bmp"></td>
<td>A 32-bit image using the default color format for 32-bit images (no
BITFIELDS segment). There are 8 bits per color channel, and 8 unused
bits. The unused bits are set to 0.</td>
</tr>
<tr>
<td>g/rgb32bf.bmp</td>
<td>3</td>
<td class=b><img src="rgb24.png"></td>
<td class=b><img src="../g/rgb32bf.bmp"></td>
<td>A 32-bit image with a BITFIELDS segment. As usual, there are 8 bits per
color channel, and 8 unused bits. But the color channels are in an unusual
order, so the viewer must read the BITFIELDS, and not just guess.</td>
</tr>
<tr>
<td class=q>q/rgb32fakealpha.bmp</td>
<td>3</td>
<td class=b><img src="rgb24.png"><br>
or<br>
<img class=b src="fakealpha.png">
</td>
<td class=b><img src="../q/rgb32fakealpha.bmp"></td>
<td>Same as g/rgb32.bmp, except that the unused bits are set to something
other than 0.
If the image becomes transparent toward the bottom, it probably means
the viewer uses heuristics to guess whether the undefined
data represents transparency.</td>
</tr>
<tr>
<td class=q>q/rgb32-111110.bmp</td>
<td>3</td>
<td class=b><img src="rgb24.png"></td>
<td class=b><img src="../q/rgb32-111110.bmp"></td>
<td>A 32 bits/pixel image, with all 32 bits used: 11 each for red and
green, and 10 for blue. As far as I know, this is perfectly valid, but it
is unusual.</td>
</tr>
<tr>
<td class=q>q/rgba32.bmp</td>
<td>5</td>
<td class=b><img src="rgba32.png"></td>
<td class=b><img src="../q/rgba32.bmp"></td>
<td>A BMP with an alpha channel. Transparency is barely documented,
so it&rsquo;s <i>possible</i> that this file is not correctly formed.
The color channels are in an unusual order, to prevent viewers from
passing this test by making a lucky guess.</td>
</tr>
<tr>
<td class=q>q/rgba32abf.bmp</td>
<td>3</td>
<td class=b><img src="rgba32.png"></td>
<td class=b><img src="../q/rgba32abf.bmp"></td>
<td>An image of type BI_ALHPABITFIELDS. Supposedly, this was used on
Windows CE. I don&rsquo;t know whether it is constructed correctly.</td>
</tr>
<tr>
<td class=bad>b/badbitcount.bmp</td>
<td>3</td>
<td class=b>N/A</td>
<td class=b><img src="../b/badbitcount.bmp"></td>
<td>Header indicates an absurdly large number of bits/pixel.</td>
</tr>
<tr>
<td class=bad>b/badbitssize.bmp</td>
<td>3</td>
<td class=b>N/A</td>
<td class=b><img src="../b/badbitssize.bmp"></td>
<td>Header incorrectly indicates that the bitmap is several GB in size.</td>
</tr>
<tr>
<td class=bad>b/baddens1.bmp</td>
<td>3</td>
<td class=b>N/A</td>
<td class=b><img src="../b/baddens1.bmp"></td>
<td rowspan=2>Density (pixels per meter) suggests the image is <i>much</i>
larger in one dimension than the other.</td>
</tr>
<tr>
<td class=bad>b/baddens2.bmp</td>
<td>3</td>
<td class=b>N/A</td>
<td class=b><img src="../b/baddens2.bmp"></td>
</tr>
<tr>
<td class=bad>b/badfilesize.bmp</td>
<td>3</td>
<td class=b>N/A</td>
<td class=b><img src="../b/badfilesize.bmp"></td>
<td>Header incorrectly indicates that the file is several GB in size.</td>
</tr>
<tr>
<td class=bad>b/badheadersize.bmp</td>
<td>?</td>
<td class=b>N/A</td>
<td class=b><img src="../b/badheadersize.bmp"></td>
<td>Header size is 66 bytes, which is not a valid size for any known BMP
version.</td>
</tr>
<tr>
<td class=bad>b/badpalettesize.bmp</td>
<td>3</td>
<td class=b>N/A</td>
<td class=b><img src="../b/badpalettesize.bmp"></td>
<td>Header incorrectly indicates that the palette contains an absurdly large
number of colors.</td>
</tr>
<tr>
<td class=bad>b/badplanes.bmp</td>
<td>3</td>
<td class=b>N/A</td>
<td class=b><img src="../b/badplanes.bmp"></td>
<td>The &ldquo;planes&rdquo; setting, which is required to be 1, is not 1.</td>
</tr>
<tr>
<td class=bad>b/badrle.bmp</td>
<td>3</td>
<td class=b>N/A</td>
<td class=b><img src="../b/badrle.bmp"></td>
<td>An invalid RLE-compressed image that tries to cause buffer overruns.</td>
</tr>
<tr>
<td class=bad>b/badwidth.bmp</td>
<td>3</td>
<td class=b>N/A</td>
<td class=b><img src="../b/badwidth.bmp"></td>
<td>The image claims to be a negative number of pixels in width.</td>
</tr>
<tr>
<td class=bad>b/pal8badindex.bmp</td>
<td>3</td>
<td class=b>N/A</td>
<td class=b><img src="../b/pal8badindex.bmp"></td>
<td>Many of the palette indices used in the image are not present in the
palette.</td>
</tr>
<tr>
<td class=bad>b/reallybig.bmp</td>
<td>3</td>
<td class=b>N/A</td>
<td class=b><img src="../b/reallybig.bmp"></td>
<td>An image with a very large reported width and height.</td>
</tr>
<tr>
<td class=bad>b/rletopdown.bmp</td>
<td>3</td>
<td class=b>N/A</td>
<td class=b><img src="../b/rletopdown.bmp"></td>
<td>An RLE-compressed image that tries to use top-down orientation,
which isn&rsquo;t allowed.</td>
</tr>
<tr>
<td class=bad>b/shortfile.bmp</td>
<td>3</td>
<td class=b>N/A</td>
<td class=b><img src="../b/shortfile.bmp"></td>
<td>A file that has been truncated in the middle of the bitmap.</td>
</tr>
</table>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 961 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 B

After

Width:  |  Height:  |  Size: 79 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -213,7 +213,7 @@ INT32 = DataShape(
),
)
def test_fromarray(mode: str, data_tp: DataShape, mask: list[int] | None) -> None:
(dtype, elt, elts_per_pixel) = data_tp
dtype, elt, elts_per_pixel = data_tp
ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1]
if dtype == fl_uint8_4_type:
@ -239,7 +239,7 @@ def test_fromarray(mode: str, data_tp: DataShape, mask: list[int] | None) -> Non
)
@pytest.mark.parametrize("data_tp", (UINT32, INT32))
def test_from_int32array(mode: str, mask: list[int] | None, data_tp: DataShape) -> None:
(dtype, elt, elts_per_pixel) = data_tp
dtype, elt, elts_per_pixel = data_tp
ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1]
arr = Array([elt] * (ct_pixels * elts_per_pixel), type=dtype)

View File

@ -68,7 +68,7 @@ def test_multiblock_l_image() -> None:
img = Image.new("L", size, 128)
with pytest.raises(ValueError):
(schema, arr) = img.__arrow_c_array__()
schema, arr = img.__arrow_c_array__()
def test_multiblock_rgba_image() -> None:
@ -79,7 +79,7 @@ def test_multiblock_rgba_image() -> None:
img = Image.new("RGBA", size, (128, 127, 126, 125))
with pytest.raises(ValueError):
(schema, arr) = img.__arrow_c_array__()
schema, arr = img.__arrow_c_array__()
def test_multiblock_l_schema() -> None:
@ -114,7 +114,7 @@ def test_singleblock_l_image() -> None:
img = Image.new("L", size, 128)
assert img.im.isblock()
(schema, arr) = img.__arrow_c_array__()
schema, arr = img.__arrow_c_array__()
assert schema
assert arr
@ -130,7 +130,7 @@ def test_singleblock_rgba_image() -> None:
img = Image.new("RGBA", size, (128, 127, 126, 125))
assert img.im.isblock()
(schema, arr) = img.__arrow_c_array__()
schema, arr = img.__arrow_c_array__()
assert schema
assert arr
Image.core.set_use_block_allocator(0)

View File

@ -56,7 +56,7 @@ def test_questionable() -> None:
im.load()
if os.path.basename(f) not in supported:
print(f"Please add {f} to the partially supported bmp specs.")
except Exception: # as msg:
except Exception: # noqa: PERF203
if os.path.basename(f) in supported:
raise
@ -72,7 +72,7 @@ def test_good() -> None:
"pal8-0.bmp": "pal8.png",
"pal8rle.bmp": "pal8.png",
"pal8topdown.bmp": "pal8.png",
"pal8nonsquare.bmp": "pal8nonsquare-v.png",
"pal8nonsquare.bmp": "pal8nonsquare-e.png",
"pal8os2.bmp": "pal8.png",
"pal8os2sp.bmp": "pal8.png",
"pal8os2v2.bmp": "pal8.png",
@ -95,18 +95,18 @@ def test_good() -> None:
for f in get_files("g"):
try:
with Image.open(f) as im:
im.load()
with Image.open(get_compare(f)) as compare:
compare.load()
if im.mode == "P":
# assert image similar doesn't really work
# with paletized image, since the palette might
# be differently ordered for an equivalent image.
im = im.convert("RGBA")
compare = im.convert("RGBA")
assert_image_similar(im, compare, 5)
# assert image similar doesn't really work
# with paletized image, since the palette might
# be differently ordered for an equivalent image.
im_converted = im.convert("RGBA") if im.mode == "P" else im
compare_converted = (
compare.convert("RGBA") if im.mode == "P" else compare
)
except Exception as msg:
assert_image_similar(im_converted, compare_converted, 5)
except Exception as msg: # noqa: PERF203
# there are three here that are unsupported:
unsupported = (
os.path.join(base, "g", "rgb32bf.bmp"),

View File

@ -28,9 +28,13 @@ def box_blur(image: Image.Image, radius: float = 1, n: int = 1) -> Image.Image:
def assert_image(im: Image.Image, data: list[list[int]], delta: int = 0) -> None:
it = iter(im.getdata())
it = iter(im.get_flattened_data())
for data_row in data:
im_row = [next(it) for _ in range(im.size[0])]
im_row = []
for _ in range(im.width):
im_v = next(it)
assert isinstance(im_v, (int, float))
im_row.append(im_v)
if any(abs(data_v - im_v) > delta for data_v, im_v in zip(data_row, im_row)):
assert im_row == data_row
with pytest.raises(StopIteration):

View File

@ -1,5 +1,6 @@
from __future__ import annotations
from io import BytesIO
from pathlib import Path
import pytest
@ -277,25 +278,25 @@ def test_apng_mode() -> None:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.mode == "P"
im.seek(im.n_frames - 1)
im = im.convert("RGB")
assert im.getpixel((0, 0)) == (0, 255, 0)
assert im.getpixel((64, 32)) == (0, 255, 0)
im_rgb = im.convert("RGB")
assert im_rgb.getpixel((0, 0)) == (0, 255, 0)
assert im_rgb.getpixel((64, 32)) == (0, 255, 0)
with Image.open("Tests/images/apng/mode_palette_alpha.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.mode == "P"
im.seek(im.n_frames - 1)
im = im.convert("RGBA")
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
im_rgba = im.convert("RGBA")
assert im_rgba.getpixel((0, 0)) == (0, 255, 0, 255)
assert im_rgba.getpixel((64, 32)) == (0, 255, 0, 255)
with Image.open("Tests/images/apng/mode_palette_1bit_alpha.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.mode == "P"
im.seek(im.n_frames - 1)
im = im.convert("RGBA")
assert im.getpixel((0, 0)) == (0, 0, 255, 128)
assert im.getpixel((64, 32)) == (0, 0, 255, 128)
im_rgba = im.convert("RGBA")
assert im_rgba.getpixel((0, 0)) == (0, 0, 255, 128)
assert im_rgba.getpixel((64, 32)) == (0, 0, 255, 128)
def test_apng_chunk_errors() -> None:
@ -517,6 +518,24 @@ def test_apng_save_duration_loop(tmp_path: Path) -> None:
assert im.info["duration"] == 600
def test_apng_save_duration_float(tmp_path: Path) -> None:
test_file = tmp_path / "temp.png"
im = Image.new("1", (1, 1))
im2 = Image.new("1", (1, 1), 1)
im.save(test_file, save_all=True, append_images=[im2], duration=0.5)
with Image.open(test_file) as reloaded:
assert reloaded.info["duration"] == 0.5
def test_apng_save_large_duration(tmp_path: Path) -> None:
test_file = tmp_path / "temp.png"
im = Image.new("1", (1, 1))
im2 = Image.new("1", (1, 1), 1)
with pytest.raises(ValueError, match="cannot write duration"):
im.save(test_file, save_all=True, append_images=[im2], duration=65536000)
def test_apng_save_disposal(tmp_path: Path) -> None:
test_file = tmp_path / "temp.png"
size = (128, 64)
@ -718,6 +737,25 @@ def test_apng_save_size(tmp_path: Path) -> None:
assert reloaded.size == (200, 200)
def test_compress_level() -> None:
compress_level_sizes = {}
for compress_level in (0, 9):
out = BytesIO()
im = Image.new("L", (100, 100))
im.save(
out,
"PNG",
save_all=True,
append_images=[Image.new("L", (200, 200))],
compress_level=compress_level,
)
compress_level_sizes[compress_level] = len(out.getvalue())
assert compress_level_sizes[0] > compress_level_sizes[9]
def test_seek_after_close() -> None:
im = Image.open("Tests/images/apng/delay.png")
im.seek(1)

View File

@ -121,7 +121,6 @@ class TestFileAvif:
assert image.size == (128, 128)
assert image.format == "AVIF"
assert image.get_format_mimetype() == "image/avif"
image.getdata()
# generated with:
# avifdec hopper.avif hopper_avif_write.png
@ -143,18 +142,17 @@ class TestFileAvif:
assert reloaded.mode == "RGB"
assert reloaded.size == (128, 128)
assert reloaded.format == "AVIF"
reloaded.getdata()
# avifdec hopper.avif avif/hopper_avif_write.png
assert_image_similar_tofile(
reloaded, "Tests/images/avif/hopper_avif_write.png", 6.02
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, 8.62)
assert_image_similar(reloaded, im, 9.39)
def test_AvifEncoder_with_invalid_args(self) -> None:
"""
@ -463,12 +461,9 @@ class TestFileAvif:
@pytest.mark.parametrize(
"advanced",
[
{
"aq-mode": "1",
"enable-chroma-deltaq": "1",
},
(("aq-mode", "1"), ("enable-chroma-deltaq", "1")),
[("aq-mode", "1"), ("enable-chroma-deltaq", "1")],
{"tune": "psnr"},
(("tune", "psnr"),),
[("tune", "psnr")],
],
)
def test_encoder_advanced_codec_options(

View File

@ -42,7 +42,7 @@ def test_fallback_if_mmap_errors() -> None:
# This image has been truncated,
# so that the buffer is not large enough when using mmap
with Image.open("Tests/images/mmap_error.bmp") as im:
assert_image_equal_tofile(im, "Tests/images/pal8_offset.bmp")
assert_image_equal_tofile(im, "Tests/images/bmp/g/pal8.bmp")
def test_save_to_bytes() -> None:
@ -165,9 +165,9 @@ def test_rgba_bitfields() -> None:
with Image.open("Tests/images/rgb32bf-rgba.bmp") as im:
# So before the comparing the image, swap the channels
b, g, r = im.split()[1:]
im = Image.merge("RGB", (r, g, b))
im_rgb = Image.merge("RGB", (r, g, b))
assert_image_equal_tofile(im, "Tests/images/bmp/q/rgb32bf-xbgr.bmp")
assert_image_equal_tofile(im_rgb, "Tests/images/bmp/q/rgb32bf-xbgr.bmp")
# This test image has been manually hexedited
# to change the bitfield compression in the header from XBGR to ABGR
@ -221,6 +221,11 @@ def test_rle8_eof(file_name: str, length: int) -> None:
im.load()
def test_rle_delta() -> None:
with Image.open("Tests/images/bmp/q/pal8rletrns.bmp") as im:
assert_image_equal_tofile(im, "Tests/images/pal8rletrns.png")
def test_unsupported_bmp_bitfields_layout() -> None:
fp = io.BytesIO(
o32(40) # header size
@ -233,11 +238,21 @@ def test_unsupported_bmp_bitfields_layout() -> None:
Image.open(fp)
def test_offset() -> None:
# This image has been hexedited
# to exclude the palette size from the pixel data offset
with Image.open("Tests/images/pal8_offset.bmp") as im:
assert_image_equal_tofile(im, "Tests/images/bmp/g/pal8.bmp")
@pytest.mark.parametrize(
"offset, path",
(
(26, "pal8os2.bmp"),
(54, "pal8.bmp"),
),
)
def test_offset(offset: int, path: str) -> None:
image_path = "Tests/images/bmp/g/" + path
# Exclude the palette size from the pixel data offset
with open(image_path, "rb") as fp:
data = fp.read()
data = data[:10] + o32(offset) + data[14:]
with Image.open(io.BytesIO(data)) as im:
assert_image_equal_tofile(im, image_path)
def test_use_raw_alpha(monkeypatch: pytest.MonkeyPatch) -> None:

View File

@ -61,6 +61,7 @@ def test_handler(tmp_path: Path) -> None:
def load(self, im: ImageFile.StubImageFile) -> Image.Image:
self.loaded = True
assert im.fp is not None
im.fp.close()
return Image.new("RGB", (1, 1))

View File

@ -179,9 +179,7 @@ def test_iter(bytesmode: bool) -> None:
container = ContainerIO.ContainerIO(fh, 0, 120)
# Act
data = []
for line in container:
data.append(line)
data = list(container)
# Assert
if bytesmode:

View File

@ -57,7 +57,7 @@ TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds"
def test_sanity_dxt1_bc1(image_path: str) -> None:
"""Check DXT1 and BC1 images can be opened"""
with Image.open(TEST_FILE_DXT1.replace(".dds", ".png")) as target:
target = target.convert("RGBA")
target_rgba = target.convert("RGBA")
with Image.open(image_path) as im:
im.load()
@ -65,7 +65,7 @@ def test_sanity_dxt1_bc1(image_path: str) -> None:
assert im.mode == "RGBA"
assert im.size == (256, 256)
assert_image_equal(im, target)
assert_image_equal(im, target_rgba)
def test_sanity_dxt3() -> None:
@ -380,6 +380,11 @@ def test_palette() -> None:
assert_image_equal_tofile(im, "Tests/images/transparent.gif")
def test_zero_mask_totals() -> None:
with Image.open("Tests/images/zero_mask_totals.dds") as im:
im.load()
def test_unsupported_header_size() -> None:
with pytest.raises(OSError, match="Unsupported header size 0"):
with Image.open(BytesIO(b"DDS " + b"\x00" * 4)):
@ -515,9 +520,9 @@ def test_save_dx10_bc5(tmp_path: Path) -> None:
im.save(out, pixel_format="BC5")
assert_image_similar_tofile(im, out, 9.56)
im = hopper("L")
im_l = hopper("L")
with pytest.raises(OSError, match="only RGB mode can be written as BC5"):
im.save(out, pixel_format="BC5")
im_l.save(out, pixel_format="BC5")
@pytest.mark.parametrize(

View File

@ -1,6 +1,7 @@
from __future__ import annotations
import io
import subprocess
from pathlib import Path
import pytest
@ -265,9 +266,9 @@ def test_bytesio_object() -> None:
img.load()
with Image.open(FILE1_COMPARE) as image1_scale1_compare:
image1_scale1_compare = image1_scale1_compare.convert("RGB")
image1_scale1_compare.load()
assert_image_similar(img, image1_scale1_compare, 5)
image1_scale1_compare_rgb = image1_scale1_compare.convert("RGB")
image1_scale1_compare_rgb.load()
assert_image_similar(img, image1_scale1_compare_rgb, 5)
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
@ -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")
@ -301,17 +307,17 @@ def test_render_scale1() -> None:
with Image.open(FILE1) as image1_scale1:
image1_scale1.load()
with Image.open(FILE1_COMPARE) as image1_scale1_compare:
image1_scale1_compare = image1_scale1_compare.convert("RGB")
image1_scale1_compare.load()
assert_image_similar(image1_scale1, image1_scale1_compare, 5)
image1_scale1_compare_rgb = image1_scale1_compare.convert("RGB")
image1_scale1_compare_rgb.load()
assert_image_similar(image1_scale1, image1_scale1_compare_rgb, 5)
# Non-zero bounding box
with Image.open(FILE2) as image2_scale1:
image2_scale1.load()
with Image.open(FILE2_COMPARE) as image2_scale1_compare:
image2_scale1_compare = image2_scale1_compare.convert("RGB")
image2_scale1_compare.load()
assert_image_similar(image2_scale1, image2_scale1_compare, 10)
image2_scale1_compare_rgb = image2_scale1_compare.convert("RGB")
image2_scale1_compare_rgb.load()
assert_image_similar(image2_scale1, image2_scale1_compare_rgb, 10)
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
@ -324,18 +330,16 @@ def test_render_scale2() -> None:
assert isinstance(image1_scale2, EpsImagePlugin.EpsImageFile)
image1_scale2.load(scale=2)
with Image.open(FILE1_COMPARE_SCALE2) as image1_scale2_compare:
image1_scale2_compare = image1_scale2_compare.convert("RGB")
image1_scale2_compare.load()
assert_image_similar(image1_scale2, image1_scale2_compare, 5)
image1_scale2_compare_rgb = image1_scale2_compare.convert("RGB")
assert_image_similar(image1_scale2, image1_scale2_compare_rgb, 5)
# Non-zero bounding box
with Image.open(FILE2) as image2_scale2:
assert isinstance(image2_scale2, EpsImagePlugin.EpsImageFile)
image2_scale2.load(scale=2)
with Image.open(FILE2_COMPARE_SCALE2) as image2_scale2_compare:
image2_scale2_compare = image2_scale2_compare.convert("RGB")
image2_scale2_compare.load()
assert_image_similar(image2_scale2, image2_scale2_compare, 10)
image2_scale2_compare_rgb = image2_scale2_compare.convert("RGB")
assert_image_similar(image2_scale2, image2_scale2_compare_rgb, 10)
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
@ -345,8 +349,8 @@ def test_render_scale2() -> None:
def test_resize(filename: str) -> None:
with Image.open(filename) as im:
new_size = (100, 100)
im = im.resize(new_size)
assert im.size == new_size
im_resized = im.resize(new_size)
assert im_resized.size == new_size
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")

View File

@ -33,7 +33,7 @@ def test_multiple_load_operations() -> None:
assert_image_equal_tofile(im, "Tests/images/gbr.png")
def create_gbr_image(info: dict[str, int] = {}, magic_number=b"") -> BytesIO:
def create_gbr_image(info: dict[str, int] = {}, magic_number: bytes = b"") -> BytesIO:
return BytesIO(
b"".join(
_binary.o32be(i)

View File

@ -310,6 +310,14 @@ def test_roundtrip_save_all_1(tmp_path: Path) -> None:
assert reloaded.getpixel((0, 0)) == 255
@pytest.mark.parametrize("size", ((0, 1), (1, 0), (0, 0)))
def test_save_zero(size: tuple[int, int]) -> None:
b = BytesIO()
im = Image.new("RGB", size)
with pytest.raises(ValueError, match="cannot write empty image"):
im.save(b, "GIF")
@pytest.mark.parametrize(
"path, mode",
(
@ -327,14 +335,13 @@ def test_loading_multiple_palettes(path: str, mode: str) -> None:
im.seek(1)
assert im.mode == mode
if mode == "RGBA":
im = im.convert("RGB")
im_rgb = im.convert("RGB") if mode == "RGBA" else im
# Check a color only from the old palette
assert im.getpixel((0, 0)) == original_color
assert im_rgb.getpixel((0, 0)) == original_color
# Check a color from the new palette
assert im.getpixel((24, 24)) not in first_frame_colors
assert im_rgb.getpixel((24, 24)) not in first_frame_colors
def test_headers_saving_for_animated_gifs(tmp_path: Path) -> None:
@ -354,16 +361,16 @@ def test_palette_handling(tmp_path: Path) -> None:
# see https://github.com/python-pillow/Pillow/issues/513
with Image.open(TEST_GIF) as im:
im = im.convert("RGB")
im_rgb = im.convert("RGB")
im = im.resize((100, 100), Image.Resampling.LANCZOS)
im2 = im.convert("P", palette=Image.Palette.ADAPTIVE, colors=256)
im_rgb = im_rgb.resize((100, 100), Image.Resampling.LANCZOS)
im_p = im_rgb.convert("P", palette=Image.Palette.ADAPTIVE, colors=256)
f = tmp_path / "temp.gif"
im2.save(f, optimize=True)
f = tmp_path / "temp.gif"
im_p.save(f, optimize=True)
with Image.open(f) as reloaded:
assert_image_similar(im, reloaded.convert("RGB"), 10)
assert_image_similar(im_rgb, reloaded.convert("RGB"), 10)
def test_palette_434(tmp_path: Path) -> None:
@ -383,35 +390,36 @@ def test_palette_434(tmp_path: Path) -> None:
with roundtrip(im, optimize=True) as reloaded:
assert_image_similar(im, reloaded, 1)
im = im.convert("RGB")
# check automatic P conversion
with roundtrip(im) as reloaded:
reloaded = reloaded.convert("RGB")
assert_image_equal(im, reloaded)
im_rgb = im.convert("RGB")
# check automatic P conversion
with roundtrip(im_rgb) as reloaded:
reloaded = reloaded.convert("RGB")
assert_image_equal(im_rgb, reloaded)
@pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available")
def test_save_netpbm_bmp_mode(tmp_path: Path) -> None:
with Image.open(TEST_GIF) as img:
img = img.convert("RGB")
img_rgb = img.convert("RGB")
tempfile = str(tmp_path / "temp.gif")
b = BytesIO()
GifImagePlugin._save_netpbm(img, b, tempfile)
with Image.open(tempfile) as reloaded:
assert_image_similar(img, reloaded.convert("RGB"), 0)
tempfile = str(tmp_path / "temp.gif")
b = BytesIO()
GifImagePlugin._save_netpbm(img_rgb, b, tempfile)
with Image.open(tempfile) as reloaded:
assert_image_equal(img_rgb, reloaded.convert("RGB"))
@pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available")
def test_save_netpbm_l_mode(tmp_path: Path) -> None:
with Image.open(TEST_GIF) as img:
img = img.convert("L")
img_l = img.convert("L")
tempfile = str(tmp_path / "temp.gif")
b = BytesIO()
GifImagePlugin._save_netpbm(img, b, tempfile)
GifImagePlugin._save_netpbm(img_l, b, tempfile)
with Image.open(tempfile) as reloaded:
assert_image_similar(img, reloaded.convert("L"), 0)
assert_image_equal(img_l, reloaded.convert("L"))
def test_seek() -> None:
@ -1038,9 +1046,9 @@ def test_webp_background(tmp_path: Path) -> None:
im.save(out)
# Test non-opaque WebP background
im = Image.new("L", (100, 100), "#000")
im.info["background"] = (0, 0, 0, 0)
im.save(out)
im2 = Image.new("L", (100, 100), "#000")
im2.info["background"] = (0, 0, 0, 0)
im2.save(out)
def test_comment(tmp_path: Path) -> None:
@ -1048,16 +1056,16 @@ def test_comment(tmp_path: Path) -> None:
assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0"
out = tmp_path / "temp.gif"
im = Image.new("L", (100, 100), "#000")
im.info["comment"] = b"Test comment text"
im.save(out)
im2 = Image.new("L", (100, 100), "#000")
im2.info["comment"] = b"Test comment text"
im2.save(out)
with Image.open(out) as reread:
assert reread.info["comment"] == im.info["comment"]
assert reread.info["comment"] == im2.info["comment"]
im.info["comment"] = "Test comment text"
im.save(out)
im2.info["comment"] = "Test comment text"
im2.save(out)
with Image.open(out) as reread:
assert reread.info["comment"] == im.info["comment"].encode()
assert reread.info["comment"] == im2.info["comment"].encode()
# Test that GIF89a is used for comments
assert reread.info["version"] == b"GIF89a"
@ -1433,7 +1441,7 @@ def test_getdata(monkeypatch: pytest.MonkeyPatch) -> None:
# with open('Tests/images/gif_header_data.pkl', 'wb') as f:
# pickle.dump((h, d), f, 1)
with open("Tests/images/gif_header_data.pkl", "rb") as f:
(h_target, d_target) = pickle.load(f)
h_target, d_target = pickle.load(f)
assert h == h_target
assert d == d_target

View File

@ -59,8 +59,9 @@ def test_handler(tmp_path: Path) -> None:
def open(self, im: Image.Image) -> None:
self.opened = True
def load(self, im: Image.Image) -> Image.Image:
def load(self, im: ImageFile.ImageFile) -> Image.Image:
self.loaded = True
assert im.fp is not None
im.fp.close()
return Image.new("RGB", (1, 1))

View File

@ -61,8 +61,9 @@ def test_handler(tmp_path: Path) -> None:
def open(self, im: Image.Image) -> None:
self.opened = True
def load(self, im: Image.Image) -> Image.Image:
def load(self, im: ImageFile.ImageFile) -> Image.Image:
self.loaded = True
assert im.fp is not None
im.fp.close()
return Image.new("RGB", (1, 1))

View File

@ -6,13 +6,13 @@ import pytest
from PIL import Image, IptcImagePlugin, TiffImagePlugin, TiffTags
from .helper import assert_image_equal, hopper
from .helper import assert_image_equal
TEST_FILE = "Tests/images/iptc.jpg"
def create_iptc_image(info: dict[str, int] = {}) -> BytesIO:
def field(tag, value):
def field(tag: tuple[int, int], value: bytes) -> bytes:
return bytes((0x1C,) + tag + (0, len(value))) + value
data = field((3, 60), bytes((info.get("layers", 1), info.get("component", 0))))
@ -85,7 +85,7 @@ def test_getiptcinfo() -> None:
def test_getiptcinfo_jpg_none() -> None:
# Arrange
with hopper() as im:
with Image.open("Tests/images/hopper.jpg") as im:
# Act
iptc = IptcImagePlugin.getiptcinfo(im)
@ -143,6 +143,7 @@ def test_getiptcinfo_tiff() -> None:
# Test with LONG tag type
with Image.open("Tests/images/hopper.Lab.tif") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
im.tag_v2.tagtype[TiffImagePlugin.IPTC_NAA_CHUNK] = TiffTags.LONG
iptc = IptcImagePlugin.getiptcinfo(im)

View File

@ -85,7 +85,7 @@ class TestFileJpeg:
def test_zero(self, size: tuple[int, int], tmp_path: Path) -> None:
f = tmp_path / "temp.jpg"
im = Image.new("RGB", size)
with pytest.raises(ValueError):
with pytest.raises(ValueError, match="cannot write empty image"):
im.save(f)
def test_app(self) -> None:
@ -590,9 +590,7 @@ class TestFileJpeg:
assert im2.quantization == {0: bounds_qtable}
# values from wizard.txt in jpeg9-a src package.
standard_l_qtable = [
int(s)
for s in """
standard_l_qtable = [int(s) for s in """
16 11 10 16 24 40 51 61
12 12 14 19 26 58 60 55
14 13 16 24 40 57 69 56
@ -601,14 +599,9 @@ class TestFileJpeg:
24 35 55 64 81 104 113 92
49 64 78 87 103 121 120 101
72 92 95 98 112 100 103 99
""".split(
None
)
]
""".split(None)]
standard_chrominance_qtable = [
int(s)
for s in """
standard_chrominance_qtable = [int(s) for s in """
17 18 24 47 99 99 99 99
18 21 26 66 99 99 99 99
24 26 56 99 99 99 99 99
@ -617,10 +610,7 @@ class TestFileJpeg:
99 99 99 99 99 99 99 99
99 99 99 99 99 99 99 99
99 99 99 99 99 99 99 99
""".split(
None
)
]
""".split(None)]
for quality in range(101):
qtable_from_qtable_quality = self.roundtrip(
@ -1133,8 +1123,9 @@ class TestFileCloseW32:
im.save(tmpfile)
im = Image.open(tmpfile)
assert im.fp is not None
assert not im.fp.closed
fp = im.fp
assert not fp.closed
with pytest.raises(OSError):
os.remove(tmpfile)
im.load()

View File

@ -148,6 +148,22 @@ def test_prog_res_rt(card: ImageFile.ImageFile) -> None:
assert_image_equal(im, card)
def test_unknown_progression(tmp_path: Path) -> None:
outfile = tmp_path / "temp.jp2"
im = Image.new("1", (1, 1))
with pytest.raises(ValueError, match="unknown progression"):
im.save(outfile, progression="invalid")
def test_unknown_cinema_mode(tmp_path: Path) -> None:
outfile = tmp_path / "temp.jp2"
im = Image.new("1", (1, 1))
with pytest.raises(ValueError, match="unknown cinema mode"):
im.save(outfile, cinema_mode="invalid")
@pytest.mark.parametrize("num_resolutions", range(2, 6))
def test_default_num_resolutions(
card: ImageFile.ImageFile, num_resolutions: int
@ -162,7 +178,7 @@ 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
assert im.reduce == 2
@ -440,11 +456,19 @@ def test_pclr() -> None:
assert len(im.palette.colors) == 256
assert im.palette.colors[(255, 255, 255)] == 0
for enumcs in (0, 15, 17):
with open(f"{EXTRA_DIR}/issue104_jpxstream.jp2", "rb") as fp:
data = bytearray(fp.read())
data[114:115] = bytes([enumcs])
with Image.open(BytesIO(data)) as im:
assert im.mode == "L"
with Image.open(
f"{EXTRA_DIR}/147af3f1083de4393666b7d99b01b58b_signal_sigsegv_130c531_6155_5136.jp2"
) as im:
assert im.mode == "P"
assert im.palette is not None
assert im.palette.mode == "CMYK"
assert len(im.palette.colors) == 139
assert im.palette.colors[(0, 0, 0, 0)] == 0

View File

@ -11,7 +11,15 @@ from typing import Any, NamedTuple
import pytest
from PIL import Image, ImageFilter, ImageOps, TiffImagePlugin, TiffTags, features
from PIL import (
Image,
ImageFile,
ImageFilter,
ImageOps,
TiffImagePlugin,
TiffTags,
features,
)
from PIL.TiffImagePlugin import OSUBFILETYPE, SAMPLEFORMAT, STRIPOFFSETS, SUBIFD
from .helper import (
@ -27,14 +35,13 @@ from .helper import (
@skip_unless_feature("libtiff")
class LibTiffTestCase:
def _assert_noerr(self, tmp_path: Path, im: TiffImagePlugin.TiffImageFile) -> None:
def _assert_noerr(self, tmp_path: Path, im: ImageFile.ImageFile) -> None:
"""Helper tests that assert basic sanity about the g4 tiff reading"""
# 1 bit
assert im.mode == "1"
# Does the data actually load
im.load()
im.getdata()
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert im._compression == "group4"
@ -217,10 +224,7 @@ class TestFileLibTiff(LibTiffTestCase):
with Image.open("Tests/images/hopper_g4.tif") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
for tag in im.tag_v2:
try:
del core_items[tag]
except KeyError:
pass
core_items.pop(tag, None)
del core_items[320] # colormap is special, tested below
# Type codes:
@ -355,6 +359,36 @@ class TestFileLibTiff(LibTiffTestCase):
# Should not segfault
im.save(outfile)
@pytest.mark.parametrize("tagtype", (TiffTags.SIGNED_RATIONAL, TiffTags.IFD))
def test_tag_type(
self, tagtype: int, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
ifd = TiffImagePlugin.ImageFileDirectory_v2()
ifd[37000] = 100
ifd.tagtype[37000] = tagtype
out = tmp_path / "temp.tif"
im = Image.new("L", (1, 1))
im.save(out, tiffinfo=ifd)
with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert reloaded.tag_v2[37000] == 100
def test_inknames_tag(
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
out = tmp_path / "temp.tif"
hopper("L").save(out, tiffinfo={333: "name\x00"})
with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert reloaded.tag_v2[333] in ("name", "name\x00")
def test_whitepoint_tag(
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
@ -478,12 +512,12 @@ class TestFileLibTiff(LibTiffTestCase):
# and save to compressed tif.
out = tmp_path / "temp.tif"
with Image.open("Tests/images/pport_g4.tif") as im:
im = im.convert("L")
im_l = im.convert("L")
im = im.filter(ImageFilter.GaussianBlur(4))
im.save(out, compression="tiff_adobe_deflate")
im_l = im_l.filter(ImageFilter.GaussianBlur(4))
im_l.save(out, compression="tiff_adobe_deflate")
assert_image_equal_tofile(im, out)
assert_image_equal_tofile(im_l, out)
def test_compressions(self, tmp_path: Path) -> None:
# Test various tiff compressions and assert similar image content but reduced
@ -572,8 +606,9 @@ class TestFileLibTiff(LibTiffTestCase):
im.save(out, compression=compression)
def test_fp_leak(self) -> None:
im: Image.Image | None = Image.open("Tests/images/hopper_g4_500.tif")
im: ImageFile.ImageFile | None = Image.open("Tests/images/hopper_g4_500.tif")
assert im is not None
assert im.fp is not None
fn = im.fp.fileno()
os.fstat(fn)
@ -700,7 +735,7 @@ class TestFileLibTiff(LibTiffTestCase):
buffer_io.seek(0)
with Image.open(buffer_io) as saved_im:
assert_image_similar(pilim, saved_im, 0)
assert_image_equal(pilim, saved_im)
save_bytesio()
save_bytesio("raw")
@ -1020,6 +1055,15 @@ class TestFileLibTiff(LibTiffTestCase):
with Image.open("Tests/images/tiff_strip_planar_16bit_RGBa.tiff") as im:
assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGBa_target.png")
def test_separate_planar_extra_samples(self, tmp_path: Path) -> None:
out = tmp_path / "temp.tif"
with Image.open("Tests/images/separate_planar_extra_samples.tiff") as im:
assert im.mode == "L"
im.save(out)
with Image.open(out) as reloaded:
assert reloaded.mode == "L"
@pytest.mark.parametrize("compression", (None, "jpeg"))
def test_block_tile_tags(self, compression: str | None, tmp_path: Path) -> None:
im = hopper()
@ -1049,8 +1093,10 @@ class TestFileLibTiff(LibTiffTestCase):
data = data[:102] + b"\x02" + data[103:]
with Image.open(io.BytesIO(data)) as im:
im = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png")
im_transposed = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
assert_image_equal_tofile(
im_transposed, "Tests/images/old-style-jpeg-compression.png"
)
def test_open_missing_samplesperpixel(self) -> None:
with Image.open(
@ -1117,9 +1163,9 @@ class TestFileLibTiff(LibTiffTestCase):
with Image.open("Tests/images/g4_orientation_1.tif") as base_im:
for i in range(2, 9):
with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im:
im = ImageOps.exif_transpose(im)
im_transposed = ImageOps.exif_transpose(im)
assert_image_similar(base_im, im, 0.7)
assert_image_similar(base_im, im_transposed, 0.7)
@pytest.mark.parametrize(
"test_file",
@ -1204,7 +1250,7 @@ class TestFileLibTiff(LibTiffTestCase):
def test_save_zero(self, compression: str | None, tmp_path: Path) -> None:
im = Image.new("RGB", (0, 0))
out = tmp_path / "temp.tif"
with pytest.raises(SystemError):
with pytest.raises(ValueError, match="cannot write empty image"):
im.save(out, compression=compression)
def test_save_many_compressed(self, tmp_path: Path) -> None:

View File

@ -22,10 +22,10 @@ def test_sanity() -> None:
# Adjust for the gamma of 2.2 encoded into the file
lut = ImagePalette.make_gamma_lut(1 / 2.2)
im = Image.merge("RGBA", [chan.point(lut) for chan in im.split()])
im1 = Image.merge("RGBA", [chan.point(lut) for chan in im.split()])
im2 = hopper("RGBA")
assert_image_similar(im, im2, 10)
assert_image_similar(im1, im2, 10)
def test_n_frames() -> None:

View File

@ -6,7 +6,14 @@ from typing import Any
import pytest
from PIL import Image, ImageFile, JpegImagePlugin, MpoImagePlugin
from PIL import (
Image,
ImageFile,
JpegImagePlugin,
MpoImagePlugin,
TiffImagePlugin,
_binary,
)
from .helper import (
assert_image_equal,
@ -145,6 +152,32 @@ def test_parallax() -> None:
assert exif.get_ifd(0x927C)[0xB211] == -3.125
def test_truncated_makernote() -> None:
def check(ifd: TiffImagePlugin.ImageFileDirectory_v2) -> None:
fp = BytesIO()
ifd.save(fp)
e = Image.Exif()
e.load(fp.getvalue())
assert e.get_ifd(37500) == {}
# Nintendo
ifd = TiffImagePlugin.ImageFileDirectory_v2()
ifd[271] = "Nintendo"
ifd[34665] = {37500: b" "}
check(ifd)
# Fujifilm
for data in (
b"FUJIFILM",
b"FUJIFILM" + _binary.o32le(50),
b"FUJIFILM" + _binary.o32le(0),
):
ifd = TiffImagePlugin.ImageFileDirectory_v2()
ifd[34665] = {37500: data}
check(ifd)
def test_reload_exif_after_seek() -> None:
with Image.open("Tests/images/sugarshack.mpo") as im:
exif = im.getexif()
@ -300,12 +333,12 @@ def test_save_all() -> None:
im_reloaded.seek(1)
assert_image_similar(im, im_reloaded, 30)
im = Image.new("RGB", (1, 1))
im_rgb = Image.new("RGB", (1, 1))
for colors in (("#f00",), ("#f00", "#0f0")):
append_images = [Image.new("RGB", (1, 1), color) for color in colors]
im_reloaded = roundtrip(im, save_all=True, append_images=append_images)
im_reloaded = roundtrip(im_rgb, save_all=True, append_images=append_images)
assert_image_equal(im, im_reloaded)
assert_image_equal(im_rgb, im_reloaded)
assert isinstance(im_reloaded, MpoImagePlugin.MpoImageFile)
assert im_reloaded.mpinfo is not None
assert im_reloaded.mpinfo[45056] == b"0100"
@ -315,7 +348,7 @@ def test_save_all() -> None:
assert_image_similar(im_reloaded, im_expected, 1)
# Test that a single frame image will not be saved as an MPO
jpg = roundtrip(im, save_all=True)
jpg = roundtrip(im_rgb, save_all=True)
assert "mp" not in jpg.info

View File

@ -37,6 +37,14 @@ def test_sanity(tmp_path: Path) -> None:
im.save(f)
@pytest.mark.parametrize("size", ((0, 1), (1, 0), (0, 0)))
def test_save_zero(size: tuple[int, int]) -> None:
b = io.BytesIO()
im = Image.new("1", size)
with pytest.raises(ValueError):
im.save(b, "PCX")
def test_p_4_planes() -> None:
with Image.open("Tests/images/p_4_planes.pcx") as im:
assert im.getpixel((0, 0)) == 3
@ -119,36 +127,36 @@ def test_large_count(tmp_path: Path) -> None:
_roundtrip(tmp_path, im)
def _test_buffer_overflow(tmp_path: Path, im: Image.Image, size: int = 1024) -> None:
_last = ImageFile.MAXBLOCK
ImageFile.MAXBLOCK = size
try:
_roundtrip(tmp_path, im)
finally:
ImageFile.MAXBLOCK = _last
def _test_buffer_overflow(
tmp_path: Path, im: Image.Image, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(ImageFile, "MAXBLOCK", 1024)
_roundtrip(tmp_path, im)
def test_break_in_count_overflow(tmp_path: Path) -> None:
def test_break_in_count_overflow(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
im = Image.new("L", (256, 5))
px = im.load()
assert px is not None
for y in range(4):
for x in range(256):
px[x, y] = x % 128
_test_buffer_overflow(tmp_path, im)
_test_buffer_overflow(tmp_path, im, monkeypatch)
def test_break_one_in_loop(tmp_path: Path) -> None:
def test_break_one_in_loop(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
im = Image.new("L", (256, 5))
px = im.load()
assert px is not None
for y in range(5):
for x in range(256):
px[x, y] = x % 128
_test_buffer_overflow(tmp_path, im)
_test_buffer_overflow(tmp_path, im, monkeypatch)
def test_break_many_in_loop(tmp_path: Path) -> None:
def test_break_many_in_loop(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
im = Image.new("L", (256, 5))
px = im.load()
assert px is not None
@ -157,10 +165,10 @@ def test_break_many_in_loop(tmp_path: Path) -> None:
px[x, y] = x % 128
for x in range(8):
px[x, 4] = 16
_test_buffer_overflow(tmp_path, im)
_test_buffer_overflow(tmp_path, im, monkeypatch)
def test_break_one_at_end(tmp_path: Path) -> None:
def test_break_one_at_end(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
im = Image.new("L", (256, 5))
px = im.load()
assert px is not None
@ -168,10 +176,10 @@ def test_break_one_at_end(tmp_path: Path) -> None:
for x in range(256):
px[x, y] = x % 128
px[0, 3] = 128 + 64
_test_buffer_overflow(tmp_path, im)
_test_buffer_overflow(tmp_path, im, monkeypatch)
def test_break_many_at_end(tmp_path: Path) -> None:
def test_break_many_at_end(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
im = Image.new("L", (256, 5))
px = im.load()
assert px is not None
@ -181,10 +189,10 @@ def test_break_many_at_end(tmp_path: Path) -> None:
for x in range(4):
px[x * 2, 3] = 128 + 64
px[x + 256 - 4, 3] = 0
_test_buffer_overflow(tmp_path, im)
_test_buffer_overflow(tmp_path, im, monkeypatch)
def test_break_padding(tmp_path: Path) -> None:
def test_break_padding(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
im = Image.new("L", (257, 5))
px = im.load()
assert px is not None
@ -193,4 +201,4 @@ def test_break_padding(tmp_path: Path) -> None:
px[x, y] = x % 128
for x in range(5):
px[x, 3] = 0
_test_buffer_overflow(tmp_path, im)
_test_buffer_overflow(tmp_path, im, monkeypatch)

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
@ -101,12 +101,13 @@ class TestFilePng:
assert im.get_format_mimetype() == "image/png"
for mode in ["1", "L", "P", "RGB", "I;16", "I;16B"]:
im = hopper(mode)
im.save(test_file)
im1 = hopper(mode)
im1.save(test_file)
with Image.open(test_file) as reloaded:
if mode == "I;16B":
reloaded = reloaded.convert(mode)
assert_image_equal(reloaded, im)
converted_reloaded = (
reloaded.convert(mode) if mode == "I;16B" else reloaded
)
assert_image_equal(converted_reloaded, im1)
def test_invalid_file(self) -> None:
invalid_file = "Tests/images/flower.jpg"
@ -225,11 +226,11 @@ class TestFilePng:
test_file = "Tests/images/pil123p.png"
with Image.open(test_file) as im:
assert_image(im, "P", (162, 150))
im = im.convert("RGBA")
assert_image(im, "RGBA", (162, 150))
im_rgba = im.convert("RGBA")
assert_image(im_rgba, "RGBA", (162, 150))
# image has 124 unique alpha values
colors = im.getchannel("A").getcolors()
colors = im_rgba.getchannel("A").getcolors()
assert colors is not None
assert len(colors) == 124
@ -239,11 +240,11 @@ class TestFilePng:
assert im.info["transparency"] == (0, 255, 52)
assert_image(im, "RGB", (64, 64))
im = im.convert("RGBA")
assert_image(im, "RGBA", (64, 64))
im_rgba = im.convert("RGBA")
assert_image(im_rgba, "RGBA", (64, 64))
# image has 876 transparent pixels
colors = im.getchannel("A").getcolors()
colors = im_rgba.getchannel("A").getcolors()
assert colors is not None
assert colors[0][0] == 876
@ -262,11 +263,11 @@ class TestFilePng:
assert len(im.info["transparency"]) == 256
assert_image(im, "P", (162, 150))
im = im.convert("RGBA")
assert_image(im, "RGBA", (162, 150))
im_rgba = im.convert("RGBA")
assert_image(im_rgba, "RGBA", (162, 150))
# image has 124 unique alpha values
colors = im.getchannel("A").getcolors()
colors = im_rgba.getchannel("A").getcolors()
assert colors is not None
assert len(colors) == 124
@ -285,13 +286,13 @@ class TestFilePng:
assert im.info["transparency"] == 164
assert im.getpixel((31, 31)) == 164
assert_image(im, "P", (64, 64))
im = im.convert("RGBA")
assert_image(im, "RGBA", (64, 64))
im_rgba = im.convert("RGBA")
assert_image(im_rgba, "RGBA", (64, 64))
assert im.getpixel((31, 31)) == (0, 255, 52, 0)
assert im_rgba.getpixel((31, 31)) == (0, 255, 52, 0)
# image has 876 transparent pixels
colors = im.getchannel("A").getcolors()
colors = im_rgba.getchannel("A").getcolors()
assert colors is not None
assert colors[0][0] == 876
@ -338,6 +339,15 @@ class TestFilePng:
assert colors is not None
assert colors[0][0] == num_transparent
def test_save_1_transparency(self, tmp_path: Path) -> None:
out = tmp_path / "temp.png"
im = Image.new("1", (1, 1), 1)
im.save(out, transparency=1)
with Image.open(out) as reloaded:
assert reloaded.info["transparency"] == 255
def test_save_rgb_single_transparency(self, tmp_path: Path) -> None:
in_file = "Tests/images/caption_6_33_22.png"
with Image.open(in_file) as im:
@ -492,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
@ -508,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"
@ -644,21 +685,17 @@ class TestFilePng:
with pytest.raises(SyntaxError, match="Unknown compression method"):
PngImagePlugin.PngImageFile("Tests/images/unknown_compression_method.png")
def test_padded_idat(self) -> None:
def test_padded_idat(self, monkeypatch: pytest.MonkeyPatch) -> None:
# This image has been manually hexedited
# so that the IDAT chunk has padding at the end
# Set MAXBLOCK to the length of the actual data
# so that the decoder finishes reading before the chunk ends
MAXBLOCK = ImageFile.MAXBLOCK
ImageFile.MAXBLOCK = 45
ImageFile.LOAD_TRUNCATED_IMAGES = True
monkeypatch.setattr(ImageFile, "MAXBLOCK", 45)
monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True)
with Image.open("Tests/images/padded_idat.png") as im:
im.load()
ImageFile.MAXBLOCK = MAXBLOCK
ImageFile.LOAD_TRUNCATED_IMAGES = False
assert_image_equal_tofile(im, "Tests/images/bw_gradient.png")
@pytest.mark.parametrize(
@ -701,6 +738,16 @@ class TestFilePng:
assert reloaded.png.im_palette is not None
assert len(reloaded.png.im_palette[1]) == 3
def test_plte_cmyk(self, tmp_path: Path) -> None:
im = Image.new("P", (1, 1))
im.putpalette((0, 100, 150, 200), "CMYK")
out = tmp_path / "temp.png"
im.save(out)
with Image.open(out) as reloaded:
assert reloaded.convert("CMYK").getpixel((0, 0)) == (200, 222, 232, 0)
def test_getxmp(self) -> None:
with Image.open("Tests/images/color_snakes.png") as im:
if ElementTree is None:
@ -778,7 +825,9 @@ class TestFilePng:
im.save(test_file, exif=im.getexif())
with Image.open(test_file) as reloaded:
assert isinstance(reloaded, PngImagePlugin.PngImageFile)
exif = reloaded._getexif()
assert exif is not None
assert exif[305] == "Adobe Photoshop CS Macintosh"
def test_exif_argument(self, tmp_path: Path) -> None:
@ -803,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")
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")
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

@ -1,12 +1,18 @@
from __future__ import annotations
import sys
import warnings
import pytest
from PIL import Image, PsdImagePlugin
from .helper import assert_image_equal_tofile, assert_image_similar, hopper, is_pypy
from .helper import (
assert_image_equal_tofile,
assert_image_similar,
hopper,
is_pypy,
)
test_file = "Tests/images/hopper.psd"
@ -85,6 +91,11 @@ def test_eoferror() -> None:
# Test that seeking to the last frame does not raise an error
im.seek(n_frames - 1)
# Test seeking past the last frame without calling n_frames first
with Image.open(test_file) as im:
with pytest.raises(EOFError):
im.seek(3)
def test_seek_tell() -> None:
with Image.open(test_file) as im:
@ -100,7 +111,7 @@ def test_seek_tell() -> None:
im.seek(2)
layer_number = im.tell()
assert layer_number == 2
assert layer_number == 2
def test_seek_eoferror() -> None:
@ -138,7 +149,7 @@ def test_icc_profile() -> None:
assert "icc_profile" in im.info
icc_profile = im.info["icc_profile"]
assert len(icc_profile) == 3144
assert len(icc_profile) == 3144
def test_no_icc_profile() -> None:
@ -158,17 +169,16 @@ def test_combined_larger_than_size() -> None:
@pytest.mark.parametrize(
"test_file,raises",
"test_file",
[
("Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd", OSError),
("Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", OSError),
"Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd",
"Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd",
],
)
def test_crashes(test_file: str, raises: type[Exception]) -> None:
with open(test_file, "rb") as f:
with pytest.raises(raises):
with Image.open(f):
pass
def test_crashes(test_file: str) -> None:
with pytest.raises(OSError):
with Image.open(test_file):
pass
@pytest.mark.parametrize(
@ -179,8 +189,38 @@ def test_crashes(test_file: str, raises: type[Exception]) -> None:
],
)
def test_layer_crashes(test_file: str) -> None:
with open(test_file, "rb") as f:
with Image.open(f) as im:
assert isinstance(im, PsdImagePlugin.PsdImageFile)
with pytest.raises(SyntaxError):
im.layers
with Image.open(test_file) as im:
assert isinstance(im, PsdImagePlugin.PsdImageFile)
with pytest.raises(SyntaxError):
im.layers
@pytest.mark.parametrize(
"test_file",
[
"Tests/images/psd-oob-write.psd",
"Tests/images/psd-oob-write-x.psd",
"Tests/images/psd-oob-write-y.psd",
],
)
def test_bounds_crash(test_file: str) -> None:
with Image.open(test_file) as im:
assert isinstance(im, PsdImagePlugin.PsdImageFile)
im.seek(im.n_frames)
with pytest.raises(ValueError):
im.load()
def test_bounds_crash_overflow() -> None:
with Image.open("Tests/images/psd-oob-write-overflow.psd") as im:
assert isinstance(im, PsdImagePlugin.PsdImageFile)
im.load()
if sys.maxsize <= 2**32:
with pytest.raises(OverflowError):
im.seek(im.n_frames)
else:
im.seek(im.n_frames)
with pytest.raises(ValueError):
im.load()

View File

@ -63,6 +63,16 @@ def test_save(tmp_path: Path) -> None:
assert im2.size == (128, 128)
assert im2.format == "SPIDER"
del Image.EXTENSION[".spider"]
@pytest.mark.parametrize("size", ((0, 1), (1, 0), (0, 0)))
def test_save_zero(size: tuple[int, int]) -> None:
b = BytesIO()
im = Image.new("1", size)
with pytest.raises(ValueError, match="cannot write empty image"):
im.save(b, "SPIDER")
def test_tempfile() -> None:
# Arrange

View File

@ -84,8 +84,8 @@ def test_rgbx() -> None:
with Image.open(io.BytesIO(data)) as im:
r, g, b = im.split()
im = Image.merge("RGB", (b, g, r))
assert_image_equal_tofile(im, os.path.join(EXTRA_DIR, "32bpp.png"))
im_rgb = Image.merge("RGB", (b, g, r))
assert_image_equal_tofile(im_rgb, os.path.join(EXTRA_DIR, "32bpp.png"))
@pytest.mark.skipif(

View File

@ -1,11 +1,12 @@
from __future__ import annotations
import os
from io import BytesIO
from pathlib import Path
import pytest
from PIL import Image, UnidentifiedImageError
from PIL import Image, UnidentifiedImageError, _binary
from .helper import assert_image_equal, assert_image_equal_tofile, hopper
@ -13,8 +14,6 @@ _TGA_DIR = os.path.join("Tests", "images", "tga")
_TGA_DIR_COMMON = os.path.join(_TGA_DIR, "common")
_ORIGINS = ("tl", "bl")
_ORIGIN_TO_ORIENTATION = {"tl": 1, "bl": -1}
@ -29,7 +28,7 @@ _ORIGIN_TO_ORIENTATION = {"tl": 1, "bl": -1}
("200x32", "RGBA"),
),
)
@pytest.mark.parametrize("origin", _ORIGINS)
@pytest.mark.parametrize("origin", _ORIGIN_TO_ORIENTATION)
@pytest.mark.parametrize("rle", (True, False))
def test_sanity(
size_mode: tuple[str, str], origin: str, rle: str, tmp_path: Path
@ -94,6 +93,25 @@ def test_rgba_16() -> None:
assert im.getpixel((1, 0)) == (0, 255, 82, 0)
def test_v2_no_alpha() -> None:
test_file = "Tests/images/tga/common/200x32_rgba_tl_rle.tga"
with open(test_file, "rb") as fp:
data = fp.read()
data += (
b"\x00" * 495
+ _binary.o32le(len(data))
+ _binary.o32le(0)
+ b"TRUEVISION-XFILE.\x00"
)
with Image.open(BytesIO(data)) as im:
with Image.open(test_file) as im2:
r, g, b = im2.split()[:3]
a = Image.new("L", im2.size, 255)
expected = Image.merge("RGBA", (r, g, b, a))
assert_image_equal(im, expected)
def test_id_field() -> None:
# tga file with id field
test_file = "Tests/images/tga_id_field.tga"

View File

@ -16,6 +16,7 @@ from PIL import (
TiffImagePlugin,
TiffTags,
UnidentifiedImageError,
_binary,
)
from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION
@ -764,9 +765,9 @@ class TestFileTiff:
# Test appending images
mp = BytesIO()
im = Image.new("RGB", (100, 100), "#f00")
im_rgb = Image.new("RGB", (100, 100), "#f00")
ims = [Image.new("RGB", (100, 100), color) for color in ["#0f0", "#00f"]]
im.copy().save(mp, format="TIFF", save_all=True, append_images=ims)
im_rgb.copy().save(mp, format="TIFF", save_all=True, append_images=ims)
mp.seek(0, os.SEEK_SET)
with Image.open(mp) as reread:
@ -778,7 +779,7 @@ class TestFileTiff:
yield from ims
mp = BytesIO()
im.save(mp, format="TIFF", save_all=True, append_images=im_generator(ims))
im_rgb.save(mp, format="TIFF", save_all=True, append_images=im_generator(ims))
mp.seek(0, os.SEEK_SET)
with Image.open(mp) as reread:
@ -941,6 +942,15 @@ class TestFileTiff:
4001,
]
def test_truncated_photoshop_blocks(self) -> None:
with Image.open("Tests/images/hopper.tif") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
im.tag_v2[34377] = b"8BIM"
assert im.get_photoshop_blocks() == {}
im.tag_v2[34377] = b"8BIM" + _binary.o16be(0) + _binary.o8(2) + b" " * 5
assert im.get_photoshop_blocks() == {}
def test_tiff_chunks(self, tmp_path: Path) -> None:
tmpfile = tmp_path / "temp.tif"
@ -971,6 +981,7 @@ class TestFileTiff:
im = Image.open(tmpfile)
fp = im.fp
assert fp is not None
assert not fp.closed
im.load()
assert fp.closed
@ -984,6 +995,7 @@ class TestFileTiff:
with open(tmpfile, "rb") as f:
im = Image.open(f)
fp = im.fp
assert fp is not None
assert not fp.closed
im.load()
assert not fp.closed
@ -1034,8 +1046,9 @@ class TestFileTiffW32:
im.save(tmpfile)
im = Image.open(tmpfile)
assert im.fp is not None
assert not im.fp.closed
fp = im.fp
assert not fp.closed
with pytest.raises(OSError):
os.remove(tmpfile)
im.load()

View File

@ -175,13 +175,13 @@ def test_change_stripbytecounts_tag_type(tmp_path: Path) -> None:
del info[278]
# Resize the image so that STRIPBYTECOUNTS will be larger than a SHORT
im = im.resize((500, 500))
info[TiffImagePlugin.IMAGEWIDTH] = im.width
im_resized = im.resize((500, 500))
info[TiffImagePlugin.IMAGEWIDTH] = im_resized.width
# STRIPBYTECOUNTS can be a SHORT or a LONG
info.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] = TiffTags.SHORT
im.save(out, tiffinfo=info)
im_resized.save(out, tiffinfo=info)
with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)

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?
@ -60,7 +66,6 @@ class TestFileWebp:
assert image.size == (128, 128)
assert image.format == "WEBP"
image.load()
image.getdata()
# generated with:
# dwebp -ppm ../../Tests/images/hopper.webp -o hopper_webp_bits.ppm
@ -77,7 +82,6 @@ class TestFileWebp:
assert image.size == (128, 128)
assert image.format == "WEBP"
image.load()
image.getdata()
if mode == self.rgb_mode:
# generated with: dwebp -ppm temp.webp -o hopper_webp_write.ppm

View File

@ -29,7 +29,6 @@ def test_read_rgba() -> None:
assert image.size == (200, 150)
assert image.format == "WEBP"
image.load()
image.getdata()
image.tobytes()
@ -60,7 +59,6 @@ def test_write_lossless_rgb(tmp_path: Path) -> None:
assert image.size == pil_image.size
assert image.format == "WEBP"
image.load()
image.getdata()
assert_image_equal(image, pil_image)
@ -83,7 +81,6 @@ def test_write_rgba(tmp_path: Path) -> None:
assert image.size == (10, 10)
assert image.format == "WEBP"
image.load()
image.getdata()
assert_image_similar(image, pil_image, 1.0)
@ -133,7 +130,6 @@ def test_write_unsupported_mode_PA(tmp_path: Path) -> None:
assert image.format == "WEBP"
image.load()
image.getdata()
with Image.open(file_path) as im:
target = im.convert("RGBA")

View File

@ -24,6 +24,5 @@ def test_write_lossless_rgb(tmp_path: Path) -> None:
assert image.size == (128, 128)
assert image.format == "WEBP"
image.load()
image.getdata()
assert_image_equal(image, hopper(RGB_MODE))

View File

@ -18,7 +18,7 @@ def test_load_raw() -> None:
# Currently, support for WMF/EMF is Windows-only
im.load()
# Compare to reference rendering
assert_image_similar_tofile(im, "Tests/images/drawing_emf_ref.png", 0)
assert_image_equal_tofile(im, "Tests/images/drawing_emf_ref.png")
# Test basic WMF open and rendering
with Image.open("Tests/images/drawing.wmf") as im:

View File

@ -1,7 +1,5 @@
from __future__ import annotations
import pytest
from PIL import Image, ImageDraw, ImageFont
from .helper import skip_unless_feature
@ -20,6 +18,5 @@ class TestFontCrash:
@skip_unless_feature("freetype2")
def test_segfault(self) -> None:
with pytest.raises(OSError):
font = ImageFont.truetype("Tests/fonts/fuzz_font-5203009437302784")
self._fuzz_font(font)
font = ImageFont.truetype("Tests/fonts/fuzz_font-5203009437302784")
self._fuzz_font(font)

Some files were not shown because too many files have changed in this diff Show More